From d5235234a80a830c29f5cba2b605f8b236e11c30 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sun, 25 Jan 2026 20:56:37 -0500 Subject: [PATCH 1/3] Add highlight tag parsing for changelog with video support --- .../console/app/src/routes/changelog.json.ts | 110 ++++++++++++++++++ .../app/src/routes/changelog/index.css | 35 ++++++ .../app/src/routes/changelog/index.tsx | 83 ++++++++++++- 3 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 packages/console/app/src/routes/changelog.json.ts diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts new file mode 100644 index 00000000000..b668229f813 --- /dev/null +++ b/packages/console/app/src/routes/changelog.json.ts @@ -0,0 +1,110 @@ +type Release = { + tag_name: string + name: string + body: string + published_at: string + html_url: string +} + +type Highlight = { + source: string + title: string + description: string + shortDescription?: string + image?: { + src: string + width: string + height: string + } + video?: string +} + +function parseHighlights(body: string): Highlight[] { + const highlights: Highlight[] = [] + const regex = /([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/

([^<]+)<\/h2>/) + const pMatch = content.match(/([^<]+)<\/p>/) + const imgMatch = content.match(/ { + const parsed = parseMarkdown(release.body || "") + return { + tag: release.tag_name, + name: release.name, + date: release.published_at, + url: release.html_url, + highlights: parsed.highlights, + sections: parsed.sections, + } + }), + } +} diff --git a/packages/console/app/src/routes/changelog/index.css b/packages/console/app/src/routes/changelog/index.css index a445c74474a..a06fb00554c 100644 --- a/packages/console/app/src/routes/changelog/index.css +++ b/packages/console/app/src/routes/changelog/index.css @@ -465,6 +465,41 @@ } } } + + [data-component="highlights"] { + display: flex; + flex-direction: column; + gap: 2rem; + margin-bottom: 1.5rem; + } + + [data-component="highlight"] { + h4 { + font-size: 14px; + font-weight: 600; + color: var(--color-text-strong); + margin-bottom: 8px; + } + + p[data-slot="title"] { + font-weight: 500; + color: var(--color-text-strong); + margin-bottom: 4px; + } + + p { + color: var(--color-text); + line-height: 1.5; + margin-bottom: 12px; + } + + img, + video { + max-width: 100%; + height: auto; + border-radius: 4px; + } + } } a { diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index c1b931fe3e0..34fd5f83b73 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -40,6 +40,59 @@ function formatDate(dateString: string) { }) } +type Highlight = { + source: string + title: string + description: string + shortDescription?: string + image?: { + src: string + width: string + height: string + } + video?: string +} + +function parseHighlights(body: string): Highlight[] { + const highlights: Highlight[] = [] + const regex = /([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/

([^<]+)<\/h2>/) + const pMatch = content.match(/([^<]+)<\/p>/) + const imgMatch = content.match(/ +

{props.highlight.source}

+

{props.highlight.title}

+

{props.highlight.description}

+ + + + {props.highlight.title} + + + ) +} + export default function Changelog() { const releases = createAsync(() => getReleases()) @@ -120,6 +196,11 @@ export default function Changelog() {
+ 0}> +
+ {(highlight) => } +
+
{(section) => (
From 17e6190fcfcbcbcf7dc6cb09917d86bbd4baed85 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 25 Jan 2026 21:05:18 -0500 Subject: [PATCH 2/3] tweak: adjust tui syncing logic to help prevent case where agents would be undefined / missing --- .../opencode/src/cli/cmd/tui/context/sync.tsx | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 392cfb7f121..eb8ed2d9bba 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -333,32 +333,57 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const start = Date.now() - 30 * 24 * 60 * 60 * 1000 const sessionListPromise = sdk.client.session .list({ start: start }) - .then((x) => setStore("session", reconcile((x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))))) + .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) // blocking - include session.list when continuing a session + const providersPromise = sdk.client.config.providers({}, { throwOnError: true }) + const providerListPromise = sdk.client.provider.list({}, { throwOnError: true }) + const agentsPromise = sdk.client.app.agents({}, { throwOnError: true }) + const configPromise = sdk.client.config.get({}, { throwOnError: true }) const blockingRequests: Promise[] = [ - sdk.client.config.providers({}, { throwOnError: true }).then((x) => { - batch(() => { - setStore("provider", reconcile(x.data!.providers)) - setStore("provider_default", reconcile(x.data!.default)) - }) - }), - sdk.client.provider.list({}, { throwOnError: true }).then((x) => { - batch(() => { - setStore("provider_next", reconcile(x.data!)) - }) - }), - sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", reconcile(x.data ?? []))), - sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", reconcile(x.data!))), + providersPromise, + providerListPromise, + agentsPromise, + configPromise, ...(args.continue ? [sessionListPromise] : []), ] await Promise.all(blockingRequests) + .then(() => { + const providersResponse = providersPromise.then((x) => x.data!) + const providerListResponse = providerListPromise.then((x) => x.data!) + const agentsResponse = agentsPromise.then((x) => x.data ?? []) + const configResponse = configPromise.then((x) => x.data!) + const sessionListResponse = args.continue ? sessionListPromise : undefined + + return Promise.all([ + providersResponse, + providerListResponse, + agentsResponse, + configResponse, + ...(sessionListResponse ? [sessionListResponse] : []), + ]).then((responses) => { + const providers = responses[0] + const providerList = responses[1] + const agents = responses[2] + const config = responses[3] + const sessions = responses[4] + + batch(() => { + setStore("provider", reconcile(providers.providers)) + setStore("provider_default", reconcile(providers.default)) + setStore("provider_next", reconcile(providerList)) + setStore("agent", reconcile(agents)) + setStore("config", reconcile(config)) + if (sessions !== undefined) setStore("session", reconcile(sessions)) + }) + }) + }) .then(() => { if (store.status !== "complete") setStore("status", "partial") // non-blocking Promise.all([ - ...(args.continue ? [] : [sessionListPromise]), + ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]), sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))), sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))), sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))), From 00031b9cd636f23e659ccd0e47ecfc8487255d64 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sun, 25 Jan 2026 21:18:26 -0500 Subject: [PATCH 3/3] Add collapsible sections, sticky version header, and style refinements for changelog highlights --- .../console/app/src/routes/changelog.json.ts | 54 ++++--- .../app/src/routes/changelog/index.css | 114 ++++++++++++-- .../app/src/routes/changelog/index.tsx | 148 +++++++++++------- 3 files changed, 223 insertions(+), 93 deletions(-) diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts index b668229f813..a23f2050324 100644 --- a/packages/console/app/src/routes/changelog.json.ts +++ b/packages/console/app/src/routes/changelog.json.ts @@ -6,21 +6,22 @@ type Release = { html_url: string } -type Highlight = { - source: string +type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } + +type HighlightItem = { title: string description: string shortDescription?: string - image?: { - src: string - width: string - height: string - } - video?: string + media: HighlightMedia +} + +type HighlightGroup = { + source: string + items: HighlightItem[] } -function parseHighlights(body: string): Highlight[] { - const highlights: Highlight[] = [] +function parseHighlights(body: string): HighlightGroup[] { + const groups = new Map() const regex = /([\s\S]*?)<\/highlight>/g let match @@ -30,29 +31,32 @@ function parseHighlights(body: string): Highlight[] { const titleMatch = content.match(/

([^<]+)<\/h2>/) const pMatch = content.match(/([^<]+)<\/p>/) - const imgMatch = content.match(/ ({ source, items })) } function parseMarkdown(body: string) { diff --git a/packages/console/app/src/routes/changelog/index.css b/packages/console/app/src/routes/changelog/index.css index a06fb00554c..233d85cc0e6 100644 --- a/packages/console/app/src/routes/changelog/index.css +++ b/packages/console/app/src/routes/changelog/index.css @@ -367,11 +367,18 @@ display: flex; flex-direction: column; gap: 4px; + position: sticky; + top: 80px; + align-self: start; + background: var(--color-background); + padding: 8px 0; @media (max-width: 50rem) { + position: static; flex-direction: row; align-items: center; gap: 12px; + padding: 0; } [data-slot="version"] { @@ -402,24 +409,26 @@ [data-component="section"] { h3 { - font-size: 14px; + font-size: 13px; font-weight: 600; color: var(--color-text-strong); - margin-bottom: 8px; + margin-bottom: 6px; } ul { list-style: none; padding: 0; margin: 0; + padding-left: 16px; display: flex; flex-direction: column; - gap: 6px; + gap: 4px; li { color: var(--color-text); + font-size: 13px; line-height: 1.5; - padding-left: 16px; + padding-left: 12px; position: relative; &::before { @@ -431,7 +440,7 @@ [data-slot="author"] { color: var(--color-text-weak); - font-size: 13px; + font-size: 12px; margin-left: 4px; text-decoration: none; @@ -473,6 +482,72 @@ margin-bottom: 1.5rem; } + [data-component="collapsible-sections"] { + display: flex; + flex-direction: column; + gap: 0; + } + + [data-component="collapsible-section"] { + [data-slot="toggle"] { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + padding: 6px 0; + cursor: pointer; + font-family: inherit; + font-size: 13px; + font-weight: 600; + color: var(--color-text-weak); + + &:hover { + color: var(--color-text); + } + + [data-slot="icon"] { + font-size: 10px; + } + } + + ul { + list-style: none; + padding: 0; + margin: 0; + padding-left: 16px; + padding-bottom: 8px; + + li { + color: var(--color-text); + font-size: 13px; + line-height: 1.5; + padding-left: 12px; + position: relative; + + &::before { + content: "-"; + position: absolute; + left: 0; + color: var(--color-text-weak); + } + + [data-slot="author"] { + color: var(--color-text-weak); + font-size: 12px; + margin-left: 4px; + text-decoration: none; + + &:hover { + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + } + } + } + } + } + [data-component="highlight"] { h4 { font-size: 14px; @@ -481,16 +556,29 @@ margin-bottom: 8px; } - p[data-slot="title"] { - font-weight: 500; - color: var(--color-text-strong); - margin-bottom: 4px; + hr { + border: none; + border-top: 1px solid var(--color-border-weak); + margin-bottom: 16px; } - p { - color: var(--color-text); - line-height: 1.5; - margin-bottom: 12px; + [data-slot="highlight-item"] { + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } + + p[data-slot="title"] { + font-weight: 600; + font-size: 16px; + margin-bottom: 4px; + } + + p { + font-size: 14px; + margin-bottom: 12px; + } } img, diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index 34fd5f83b73..87e021ec88c 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -5,7 +5,7 @@ import { Header } from "~/component/header" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { config } from "~/config" -import { For, Show } from "solid-js" +import { For, Show, createSignal } from "solid-js" type Release = { tag_name: string @@ -40,21 +40,22 @@ function formatDate(dateString: string) { }) } -type Highlight = { - source: string +type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } + +type HighlightItem = { title: string description: string shortDescription?: string - image?: { - src: string - width: string - height: string - } - video?: string + media: HighlightMedia } -function parseHighlights(body: string): Highlight[] { - const highlights: Highlight[] = [] +type HighlightGroup = { + source: string + items: HighlightItem[] +} + +function parseHighlights(body: string): HighlightGroup[] { + const groups = new Map() const regex = /([\s\S]*?)<\/highlight>/g let match @@ -64,33 +65,32 @@ function parseHighlights(body: string): Highlight[] { const titleMatch = content.match(/

([^<]+)<\/h2>/) const pMatch = content.match(/([^<]+)<\/p>/) - const imgMatch = content.match(/ ({ source, items })) } function parseMarkdown(body: string) { @@ -142,27 +142,60 @@ function ReleaseItem(props: { item: string }) { ) } -function HighlightCard(props: { highlight: Highlight }) { +function HighlightSection(props: { group: HighlightGroup }) { return (
-

{props.highlight.source}

-

{props.highlight.title}

-

{props.highlight.description}

- - - - {props.highlight.title} +

{props.group.source}

+
+ + {(item) => ( +
+

{item.title}

+

{item.description}

+ + + + {item.title} + +
+ )} +
+
+ ) +} + +function CollapsibleSection(props: { section: { title: string; items: string[] } }) { + const [open, setOpen] = createSignal(false) + + return ( +
+ + +
    + {(item) => } +
) } +function CollapsibleSections(props: { sections: { title: string; items: string[] }[] }) { + return ( +
+ {(section) => } +
+ ) +} + export default function Changelog() { const releases = createAsync(() => getReleases()) @@ -198,19 +231,24 @@ export default function Changelog() {
0}>
- {(highlight) => } + {(group) => }
- - {(section) => ( -
-

{section.title}

-
    - {(item) => } -
-
- )} -
+ 0 && parsed().sections.length > 0}> + + + + + {(section) => ( +
+

{section.title}

+
    + {(item) => } +
+
+ )} +
+
)