fix(desktop): tidy the What's new in beta page and drop its command (#58256)

This commit is contained in:
Kit Langton
2026-10-22 16:45:32 -07:00
committed by GitHub
Unverified
parent bcd713e2b7
commit e997d7b8a0
5 changed files with 102 additions and 27 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ export default defineConfig(async ({ command }) => {
const nodePtyPkg = nodePtyPackage()
// Beta builds carry their commit and recent history for the updater's "What's new in beta"; the dev server carries
// them without asking GitHub, so the page and command can be tried; other channels none.
// them without asking GitHub, so the page can be tried; other channels none.
const betaNotes =
channel === "beta"
? await BetaNotes.enrich(BetaNotes.embed(), { token: process.env.GITHUB_TOKEN })
@@ -195,7 +195,7 @@ export const PreviousBuildUnknown = {
render: () => <Fixture picked={commits} previous="0.0.0-beta-20500" previousCommit={sha(5000)} />,
}
/** Settings > What's new in beta: every change the build carries, from ⌘K, Settings or the dialog's View all. */
/** Settings > What's new in beta: every change the build carries, from Settings or the dialog's View all. */
export const Page = {
render: () => (
<SettingsPanel>
@@ -115,7 +115,7 @@ const FADE = 24
// Every entry starts on the heading's edge; a visual sits small at the row's end, so rows keep one rhythm with or
// without one.
export function Section(props: { title: string; notes: readonly BetaNote[]; open: (href: string) => void }) {
function Section(props: { title: string; notes: readonly BetaNote[]; open: (href: string) => void }) {
return (
<Show when={props.notes.length > 0}>
<section class="flex flex-col gap-3">
@@ -141,7 +141,7 @@ export function Section(props: { title: string; notes: readonly BetaNote[]; open
}
/** What changed, drawn: the pull request's looping SVG at half size, only ever as an image, in the icon colour. */
function Visual(props: { visual: NonNullable<BetaNote["visual"]>; label?: string }) {
export function Visual(props: { visual: NonNullable<BetaNote["visual"]>; label?: string }) {
const [color, setColor] = createSignal<string>()
let frame!: HTMLSpanElement
@@ -173,11 +173,13 @@ function Visual(props: { visual: NonNullable<BetaNote["visual"]>; label?: string
)
}
/** Who made it and its pull request, quietly. */
function Meta(props: { note: BetaNote; open: (href: string) => void }) {
/** Who made it and its pull request, quietly; `class` sets the type, the dialog's own scale by default. */
export function Meta(props: { note: BetaNote; open: (href: string) => void; class?: string }) {
return (
<Show when={props.note.author || props.note.pr}>
<div class="flex min-w-0 flex-wrap items-center gap-x-1.5 text-[12px] leading-4 font-[400] text-v2-text-text-faint">
<div
class={`flex min-w-0 flex-wrap items-center gap-x-1.5 ${props.class ?? "text-[12px] leading-4 font-[400] text-v2-text-text-faint"}`}
>
<Show when={props.note.author}>
{(author) => (
<span class="flex min-w-0 items-center gap-1.5">
@@ -1,12 +1,13 @@
import { Show } from "solid-js"
import { createSignal, For, onCleanup, onMount, Show } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { Option } from "effect"
import { Button } from "@opencode/ui/button"
import { useExtension } from "../sdk"
import { Section } from "./beta-dialog"
import { allBetaNotes, betaHistory, decodeEmbeddedNotes, type EmbeddedNotes } from "./beta-notes"
import { Meta, Visual } from "./beta-dialog"
import { allBetaNotes, betaHistory, decodeEmbeddedNotes, type BetaNote, type EmbeddedNotes } from "./beta-notes"
import type definition from "./index"
/** Settings > What's new in beta: every change this build carries, as the dialog shows them. */
/** Settings > What's new in beta: every change this build carries, in the host's settings list markup. */
export default function BetaNotesPage() {
const ctx = useExtension<typeof definition>()
@@ -20,6 +21,9 @@ export default function BetaNotesPage() {
export function BetaNotesView(props: { embedded: EmbeddedNotes | undefined; open: (href: string) => void }) {
const ctx = useExtension<typeof definition>()
// Below 640px a row's control slot wraps under its title, so narrow windows leave the visuals to the dialog.
const wide = createMediaQuery("(min-width: 640px)")
const watch = createWatch()
return (
<Show when={props.embedded}>
@@ -30,15 +34,15 @@ export function BetaNotesView(props: { embedded: EmbeddedNotes | undefined; open
<>
<div class="settings-tab-header">
<div class="settings-tab-header-row">
<div class="flex flex-col gap-1">
<div class="flex min-w-0 flex-col gap-1">
<h2 class="settings-tab-title">{ctx.t("beta.title")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{ctx.t("beta.page.description")}</span>
</div>
<div class="settings-tab-header-actions">
<Button
type="button"
variant="ghost"
class="-me-2.5"
variant="ghost-muted"
class="whitespace-nowrap"
onClick={() => props.open(betaHistory(embedded()))}
>
{ctx.t("beta.github")}
@@ -47,8 +51,8 @@ export function BetaNotesView(props: { embedded: EmbeddedNotes | undefined; open
</div>
</div>
<div class="settings-tab-body settings-tab-body--sectioned">
<Section title={ctx.t("beta.added")} notes={notes.added} open={props.open} />
<Section title={ctx.t("beta.fixed")} notes={notes.fixed} open={props.open} />
<Notes title={ctx.t("beta.added")} notes={notes.added} open={props.open} wide={wide()} watch={watch} />
<Notes title={ctx.t("beta.fixed")} notes={notes.fixed} open={props.open} wide={wide()} watch={watch} />
</div>
</>
)
@@ -56,3 +60,81 @@ export function BetaNotesView(props: { embedded: EmbeddedNotes | undefined; open
</Show>
)
}
type Watch = (element: Element, inView: (value: boolean) => void) => () => void
function Notes(props: {
title: string
notes: readonly BetaNote[]
open: (href: string) => void
wide: boolean
watch: Watch
}) {
return (
<Show when={props.notes.length > 0}>
<section class="settings-section" aria-label={props.title}>
<h3 class="settings-section-title">{props.title}</h3>
<div data-component="settings-list" data-variant="catalog">
<For each={props.notes}>
{(note) => (
<div data-component="settings-row">
<div data-slot="settings-row-copy">
<div data-slot="settings-row-title" class="[overflow-wrap:anywhere]">
{note.summary}
</div>
<Show when={note.author || note.pr}>
<div data-slot="settings-row-description">
<Meta note={note} open={props.open} class="" />
</div>
</Show>
</div>
<Show when={props.wide && note.visual}>
{(visual) => <RowVisual visual={visual()} label={note.description} watch={props.watch} />}
</Show>
</div>
)}
</For>
</div>
</section>
</Show>
)
}
/** A row's visual in its control slot, mounted only while the row is near the viewport so off-screen loops stop. */
function RowVisual(props: { visual: NonNullable<BetaNote["visual"]>; label?: string; watch: Watch }) {
const [visible, setVisible] = createSignal(false)
let slot!: HTMLDivElement
onMount(() => onCleanup(props.watch(slot, setVisible)))
return (
// Inline, so the host's unlayered control-slot width cannot collapse the space before the visual mounts.
<div ref={slot} data-slot="settings-row-control" style={{ width: "120px", height: "36px" }}>
<Show when={visible()}>
<Visual visual={props.visual} label={props.label} />
</Show>
</div>
)
}
/** One IntersectionObserver for every row on the page; each watched element hears when it nears or leaves the viewport. */
function createWatch(): Watch {
const rows = new Map<Element, (value: boolean) => void>()
const observer = new IntersectionObserver(
(entries) => entries.forEach((entry) => rows.get(entry.target)?.(entry.isIntersecting)),
{ rootMargin: "200px 0px" },
)
onCleanup(() => observer.disconnect())
return (element, inView) => {
rows.set(element, inView)
observer.observe(element)
return () => {
rows.delete(element)
observer.unobserve(element)
}
}
}
@@ -88,8 +88,8 @@ const setup: Setup<typeof definition> = (ctx) => {
}
/**
* What's new in beta as a settings page and a command, so it is a keystroke away after its dialog closes. Only builds
* that embed the notes have it: beta builds and the desktop dev server.
* What's new in beta as a settings page, to come back to after its dialog closes. Only builds that embed the notes
* have it: beta builds and the desktop dev server.
*/
function betaNotes(ctx: SetupContext<typeof definition>) {
if (!import.meta.env.OPENCODE_BETA_NOTES) return
@@ -111,15 +111,6 @@ function betaNotes(ctx: SetupContext<typeof definition>) {
</Suspense>
),
})
ctx.add(Command, {
id: "whats-new",
get title() {
return ctx.t("beta.title")
},
keywords: "changelog release notes beta",
run: () => ctx.layout.settings("whats-new"),
})
}
/**