diff --git a/packages/desktop/electron.vite.config.ts b/packages/desktop/electron.vite.config.ts index d80d820d144..479d5e2fd90 100644 --- a/packages/desktop/electron.vite.config.ts +++ b/packages/desktop/electron.vite.config.ts @@ -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 }) diff --git a/packages/gui-extensions/src/updater/beta-dialog.stories.tsx b/packages/gui-extensions/src/updater/beta-dialog.stories.tsx index 24ef2577731..4e5fc0faca1 100644 --- a/packages/gui-extensions/src/updater/beta-dialog.stories.tsx +++ b/packages/gui-extensions/src/updater/beta-dialog.stories.tsx @@ -195,7 +195,7 @@ export const PreviousBuildUnknown = { render: () => , } -/** 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: () => ( diff --git a/packages/gui-extensions/src/updater/beta-dialog.tsx b/packages/gui-extensions/src/updater/beta-dialog.tsx index 87a51fe29f6..9006bcc6282 100644 --- a/packages/gui-extensions/src/updater/beta-dialog.tsx +++ b/packages/gui-extensions/src/updater/beta-dialog.tsx @@ -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 ( 0}>
@@ -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; label?: string }) { +export function Visual(props: { visual: NonNullable; label?: string }) { const [color, setColor] = createSignal() let frame!: HTMLSpanElement @@ -173,11 +173,13 @@ function Visual(props: { visual: NonNullable; 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 ( -
+
{(author) => ( diff --git a/packages/gui-extensions/src/updater/beta-page.tsx b/packages/gui-extensions/src/updater/beta-page.tsx index 7dfd3129402..84065a32279 100644 --- a/packages/gui-extensions/src/updater/beta-page.tsx +++ b/packages/gui-extensions/src/updater/beta-page.tsx @@ -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() @@ -20,6 +21,9 @@ export default function BetaNotesPage() { export function BetaNotesView(props: { embedded: EmbeddedNotes | undefined; open: (href: string) => void }) { const ctx = useExtension() + // 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 ( @@ -30,15 +34,15 @@ export function BetaNotesView(props: { embedded: EmbeddedNotes | undefined; open <>
-
+

{ctx.t("beta.title")}

{ctx.t("beta.page.description")}
-
-
+ +
) @@ -56,3 +60,81 @@ export function BetaNotesView(props: { embedded: EmbeddedNotes | undefined; open ) } + +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 ( + 0}> +
+

{props.title}

+
+ + {(note) => ( +
+
+
+ {note.summary} +
+ +
+ +
+
+
+ + {(visual) => } + +
+ )} +
+
+
+
+ ) +} + +/** 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; 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. +
+ + + +
+ ) +} + +/** 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 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) + } + } +} diff --git a/packages/gui-extensions/src/updater/renderer.tsx b/packages/gui-extensions/src/updater/renderer.tsx index 6f9991f516f..73974ecab53 100644 --- a/packages/gui-extensions/src/updater/renderer.tsx +++ b/packages/gui-extensions/src/updater/renderer.tsx @@ -88,8 +88,8 @@ const setup: Setup = (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) { if (!import.meta.env.OPENCODE_BETA_NOTES) return @@ -111,15 +111,6 @@ function betaNotes(ctx: SetupContext) { ), }) - - ctx.add(Command, { - id: "whats-new", - get title() { - return ctx.t("beta.title") - }, - keywords: "changelog release notes beta", - run: () => ctx.layout.settings("whats-new"), - }) } /**