fix(site): link a release's other highlights when an area filter hides them (#62500)

This commit is contained in:
Adam
2026-11-06 21:36:01 -06:00
committed by GitHub
Unverified
parent e8171f1ca5
commit 10952cd57e
42 changed files with 192 additions and 22 deletions
@@ -100,14 +100,22 @@
/* Settings › Changelog title › mono: the earlier mono titles, headings and labels. */
[data-title="mono"]
&
:is([data-slot="subhead"], [data-slot="group-label"], [data-slot="area"], [data-slot="show-more"]) {
:is(
[data-slot="subhead"],
[data-slot="group-label"],
[data-slot="area"],
[data-slot="show-more"],
[data-slot="all-highlights"]
) {
font-family: var(--font-mono);
font-size: var(--mono-size);
font-weight: 400;
line-height: 1.3;
}
[data-title="mono"] & :is([data-slot="area"], [data-slot="show-more"], [data-slot="group-label"]) {
[data-title="mono"]
&
:is([data-slot="area"], [data-slot="show-more"], [data-slot="all-highlights"], [data-slot="group-label"]) {
font-size: var(--fs-meta);
line-height: 1.55;
}
@@ -484,9 +492,17 @@
}
}
/* Under a filtered entry's highlights, a link to the release's page for the ones the filter left out. */
[data-slot="all-highlights"] {
margin-top: -1.25rem;
}
[data-slot="show-more"] {
align-self: flex-start;
margin-inline-start: 7.5rem;
}
:is([data-slot="show-more"], [data-slot="all-highlights"]) {
align-self: flex-start;
font-size: var(--fs-meta);
color: var(--text-weak);
text-decoration: none;
@@ -563,6 +579,11 @@
margin-block: -0.75rem;
}
[data-slot="all-highlights"] {
padding-block: 0.75rem;
margin-block: -1.75rem -0.75rem;
}
[data-slot="meta"] :is(a, [data-slot="copy-link"]) {
padding-block: 0.75rem;
margin-block: -0.75rem;
@@ -775,6 +775,11 @@ function EntryBody(props: { entry: ChangelogEntry; level: 1 | 2 | 3; media?: Sli
</section>
)}
</For>
<Show when={props.entry.hiddenFeatures > 0}>
<a data-slot="all-highlights" href={page()}>
{i18n.t("changelog.highlights.all", { tag: props.entry.releases[0].tag })}
</a>
</Show>
<Show when={props.entry.breaking.length > 0}>
<section data-slot="group" data-kind="breaking">
+1
View File
@@ -282,6 +282,7 @@ export const dict = {
"changelog.media.pause": "إيقاف مؤقت",
"changelog.media.play": "تشغيل الفيديو",
"changelog.list.more": "{{count}} أخرى",
"changelog.highlights.all": "كل أبرز ما في {{tag}}",
"site.newTab": "{{label}} (يفتح في علامة تبويب جديدة)",
"site.products.label": "المنتجات",
"site.products.gridLabel": "منتجات OpenCode",
+1
View File
@@ -284,6 +284,7 @@ export const dict = {
"changelog.media.pause": "Pausar vídeo",
"changelog.media.play": "Tocar vídeo",
"changelog.list.more": "Mais {{count}}",
"changelog.highlights.all": "Ver todos os destaques da {{tag}}",
"site.newTab": "{{label}} (abre em uma nova aba)",
"site.products.label": "Produtos",
"site.products.gridLabel": "Produtos OpenCode",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "Pause",
"changelog.media.play": "Afspil",
"changelog.list.more": "{{count}} mere",
"changelog.highlights.all": "Se alle højdepunkter i {{tag}}",
"site.newTab": "{{label}} (åbner i en ny fane)",
"site.products.label": "Produkter",
"site.products.gridLabel": "OpenCode-produkter",
+1
View File
@@ -284,6 +284,7 @@ export const dict = {
"changelog.media.pause": "Pausieren",
"changelog.media.play": "Abspielen",
"changelog.list.more": "{{count}} weitere",
"changelog.highlights.all": "Alle Highlights in {{tag}} ansehen",
"site.newTab": "{{label}} (öffnet in neuem Tab)",
"site.products.label": "Produkte",
"site.products.gridLabel": "OpenCode-Produkte",
+1
View File
@@ -284,6 +284,7 @@ export const dict = {
"changelog.media.pause": "Pause video",
"changelog.media.play": "Play video",
"changelog.list.more": "{{count}} more",
"changelog.highlights.all": "See all highlights in {{tag}}",
"site.newTab": "{{label}} (opens in a new tab)",
"site.products.label": "Products",
"site.products.gridLabel": "OpenCode products",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "Pausar",
"changelog.media.play": "Reproducir",
"changelog.list.more": "{{count}} más",
"changelog.highlights.all": "Ver todos los destacados de {{tag}}",
"site.newTab": "{{label}} (se abre en una pestaña nueva)",
"site.products.label": "Productos",
"site.products.gridLabel": "Productos de OpenCode",
+1
View File
@@ -284,6 +284,7 @@ export const dict = {
"changelog.media.pause": "Pause vidéo",
"changelog.media.play": "Lire la vidéo",
"changelog.list.more": "{{count}} de plus",
"changelog.highlights.all": "Voir tous les points forts de {{tag}}",
"site.newTab": "{{label}} (s'ouvre dans un nouvel onglet)",
"site.products.label": "Produits",
"site.products.gridLabel": "Produits OpenCode",
+1
View File
@@ -284,6 +284,7 @@ export const dict = {
"changelog.media.pause": "Metti in pausa",
"changelog.media.play": "Riproduci",
"changelog.list.more": "Altri {{count}}",
"changelog.highlights.all": "Vedi tutte le novità principali di {{tag}}",
"site.newTab": "{{label}} (si apre in una nuova scheda)",
"site.products.label": "Prodotti",
"site.products.gridLabel": "Prodotti OpenCode",
+1
View File
@@ -284,6 +284,7 @@ export const dict = {
"changelog.media.pause": "動画を一時停止",
"changelog.media.play": "動画を再生",
"changelog.list.more": "ほか{{count}}件",
"changelog.highlights.all": "{{tag}} のハイライトをすべて見る",
"site.newTab": "{{label}}(新しいタブで開きます)",
"site.products.label": "製品",
"site.products.gridLabel": "OpenCodeの製品",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "동영상 일시정지",
"changelog.media.play": "동영상 재생",
"changelog.list.more": "{{count}}개 더 보기",
"changelog.highlights.all": "{{tag}}의 하이라이트 모두 보기",
"site.newTab": "{{label}} (새 탭에서 열림)",
"site.products.label": "제품",
"site.products.gridLabel": "OpenCode 제품",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "Pause",
"changelog.media.play": "Spill av",
"changelog.list.more": "{{count}} til",
"changelog.highlights.all": "Se alle høydepunkter i {{tag}}",
"site.newTab": "{{label}} (åpnes i ny fane)",
"site.products.label": "Produkter",
"site.products.gridLabel": "OpenCode-produkter",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "Wstrzymaj",
"changelog.media.play": "Odtwórz",
"changelog.list.more": "Jeszcze {{count}}",
"changelog.highlights.all": "Zobacz wszystkie najważniejsze zmiany w {{tag}}",
"site.newTab": "{{label}} (otwiera się w nowej karcie)",
"site.products.label": "Produkty",
"site.products.gridLabel": "Produkty OpenCode",
+1
View File
@@ -281,6 +281,7 @@ export const dict = {
"changelog.media.pause": "Пауза",
"changelog.media.play": "Смотреть",
"changelog.list.more": "Ещё {{count}}",
"changelog.highlights.all": "Все главные изменения {{tag}}",
"site.newTab": "{{label}} (откроется в новой вкладке)",
"site.products.label": "Продукты",
"site.products.gridLabel": "Продукты OpenCode",
+1
View File
@@ -50,6 +50,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -48,6 +48,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -48,6 +48,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -48,6 +48,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -48,6 +48,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -50,6 +50,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -49,6 +49,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -50,6 +50,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -48,6 +48,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -48,6 +48,7 @@
"changelog.fixes": "3bifmqr5ao3y0",
"changelog.hero.subtitle": "1dauy167rac9p",
"changelog.hero.title": "2mneds13xamjs",
"changelog.highlights.all": "2r9inwsci9o03",
"changelog.improvements": "28n9u0fcxvske",
"changelog.list.more": "ctoxownatowu",
"changelog.media.label": "2v1244mw9mfts",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "หยุดวิดีโอชั่วคราว",
"changelog.media.play": "เล่นวิดีโอ",
"changelog.list.more": "อีก {{count}} รายการ",
"changelog.highlights.all": "ดูไฮไลต์ทั้งหมดใน {{tag}}",
"site.newTab": "{{label}} (เปิดในแท็บใหม่)",
"site.products.label": "ผลิตภัณฑ์",
"site.products.gridLabel": "ผลิตภัณฑ์ OpenCode",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "Durdur",
"changelog.media.play": "Oynat",
"changelog.list.more": "{{count}} tane daha",
"changelog.highlights.all": "{{tag}} sürümünün tüm öne çıkanlarını gör",
"site.newTab": "{{label}} (yeni sekmede açılır)",
"site.products.label": "Ürünler",
"site.products.gridLabel": "OpenCode ürünleri",
+1
View File
@@ -280,6 +280,7 @@ export const dict = {
"changelog.media.pause": "Пауза",
"changelog.media.play": "Дивитися",
"changelog.list.more": "Ще {{count}}",
"changelog.highlights.all": "Усі головні зміни {{tag}}",
"site.newTab": "{{label}} (відкриється в новій вкладці)",
"site.products.label": "Продукти",
"site.products.gridLabel": "Продукти OpenCode",
+1
View File
@@ -284,6 +284,7 @@ export const dict = {
"changelog.media.pause": "暂停视频",
"changelog.media.play": "播放视频",
"changelog.list.more": "还有 {{count}} 项",
"changelog.highlights.all": "查看 {{tag}} 的全部亮点",
"site.newTab": "{{label}}(在新标签页中打开)",
"site.products.label": "产品",
"site.products.gridLabel": "OpenCode 产品",
+1
View File
@@ -285,6 +285,7 @@ export const dict = {
"changelog.media.pause": "暫停影片",
"changelog.media.play": "播放影片",
"changelog.list.more": "還有 {{count}} 項",
"changelog.highlights.all": "查看 {{tag}} 的所有亮點",
"site.newTab": "{{label}}(在新分頁中開啟)",
"site.products.label": "產品",
"site.products.gridLabel": "OpenCode 產品",
@@ -96,6 +96,11 @@ export function changelogEntryMarkdown(
...entry.features.map((feature) =>
blocks(`${heading}# ${feature.title}`, feature.body && `${feature.body}${refsMarkdown(feature)}`).trimEnd(),
),
entry.hiddenFeatures > 0 &&
link(
options.context.t("changelog.highlights.all", { tag: entry.releases[0].tag }),
options.context.route(`/changelog/${entry.releases[0].tag}`),
),
entry.breaking.length > 0 &&
blocks(
`${heading}# ${options.context.t("changelog.breaking")}`,
+17 -11
View File
@@ -36,6 +36,8 @@ export type ChangelogEntry = {
improved: ChangelogNote[]
fixed: ChangelogNote[]
media: ChangelogMedia[]
/** The release's highlights an area filter left out; the entry links to its page for them. */
hiddenFeatures: number
}
/** An area the notes are filed under: its key in `?area=`, and its label. */
@@ -183,11 +185,15 @@ export function parseReleaseNotes(body: string): ReleaseNotes {
export function changelogTimeline(releases: ChangelogRelease[], areas?: string[]) {
const units = releases
.filter((release) => /^v\d/.test(release.tag))
.map((release) => ({
date: release.date.slice(0, 10),
releases: [{ tag: release.tag, url: release.url }],
notes: areas ? onlyAreas(release.notes, areas) : release.notes,
}))
.map((release) => {
const notes = areas ? onlyAreas(release.notes, areas) : release.notes
return {
date: release.date.slice(0, 10),
releases: [{ tag: release.tag, url: release.url }],
notes,
hiddenFeatures: release.notes.features.length - notes.features.length,
}
})
.filter((unit) => noteCount(unit.notes) > 0)
const days = units.reduce<(typeof units)[]>((acc, unit) => {
const last = acc.at(-1)
@@ -374,9 +380,11 @@ export function bodyBlocks(body: string) {
.blocks.filter((block) => block.type !== "p" || block.text)
}
function entry(unit: { date: string; releases: { tag: string; url: string }[]; notes: ReleaseNotes }): ChangelogEntry {
type Unit = { date: string; releases: { tag: string; url: string }[]; notes: ReleaseNotes; hiddenFeatures: number }
function entry(unit: Unit): ChangelogEntry {
const notes = unit.notes
const base = { date: unit.date, releases: unit.releases, ...notes }
const base = { date: unit.date, releases: unit.releases, hiddenFeatures: unit.hiddenFeatures, ...notes }
const lead = notes.features[0]
if (lead)
return {
@@ -534,13 +542,11 @@ function tiny(notes: ReleaseNotes) {
return notes.features.length === 0 && notes.breaking.length === 0 && notes.improved.length + notes.fixed.length <= 3
}
function merge(
a: { date: string; releases: { tag: string; url: string }[]; notes: ReleaseNotes },
b: { date: string; releases: { tag: string; url: string }[]; notes: ReleaseNotes },
) {
function merge(a: Unit, b: Unit) {
return {
date: a.date,
releases: [...a.releases, ...b.releases],
hiddenFeatures: a.hiddenFeatures + b.hiddenFeatures,
notes: {
media: [...a.notes.media, ...b.notes.media],
features: [...a.notes.features, ...b.notes.features],
+4 -2
View File
@@ -54,12 +54,12 @@ function item(entry: ChangelogEntry) {
<guid isPermaLink="true">${url}</guid>
<pubDate>${new Date(`${entry.date}T12:00:00Z`).toUTCString()}</pubDate>
<description>${escape(plain(entry.summary || entry.title))}</description>
<content:encoded><![CDATA[${html(entry).replaceAll("]]>", "]]&gt;")}]]></content:encoded>
<content:encoded><![CDATA[${html(entry, url).replaceAll("]]>", "]]&gt;")}]]></content:encoded>
</item>`
}
/** An entry as simple HTML for feed readers: summary, highlights, then the lists. */
function html(entry: ChangelogEntry) {
function html(entry: ChangelogEntry, url: string) {
const list = (title: string, notes: ChangelogNote[]) =>
notes.length > 0 &&
`<h3>${title}</h3><ul>${notes
@@ -73,6 +73,8 @@ function html(entry: ChangelogEntry) {
...entry.features.map(
(feature) => `<h3>${inline(feature.title)}</h3>${feature.body ? `<p>${inline(feature.body)}</p>` : ""}`,
),
entry.hiddenFeatures > 0 &&
`<p><a href="${escape(url)}">See all highlights in ${escape(entry.releases[0].tag)}</a></p>`,
entry.breaking.length > 0 &&
`<h3>Breaking changes</h3>${entry.breaking.map((change) => `<p>${change.title ? `<strong>${inline(change.title)}</strong> ` : ""}${inline(change.body)}</p>`).join("")}`,
list("Improvements", entry.improved),
+8 -6
View File
@@ -3,8 +3,8 @@ import { extensionPages } from "~/component/site/extension-data"
const browse = extensionPages.find((extension) => extension.slug === "browse")!
// The browse page (anomalyco/browse): the Chrome Web Store is the main action, the standalone
// installer is the command, and the product name is always lowercase.
// The browse page (anomalyco/browse): the Chrome Web Store is the main action, the install leads
// with the extension and then lists the CLI commands, and the product name is always lowercase.
describe("browse", () => {
test("is a product at /product/browse with the Chrome Web Store as its main action", () => {
expect(browse.product).toBe(true)
@@ -12,10 +12,12 @@ describe("browse", () => {
expect(browse.source).toBe("https://github.com/anomalyco/browse")
})
test("installs from the standalone script, with no Node.js requirement", () => {
expect(browse.install.command).toBe("curl -fsSL https://opencode.ai/browse/install | bash")
expect(browse.install.lead).toContain("https://opencode.ai/browse/install.ps1")
expect(browse.install.lead).not.toMatch(/node\.?js|npx/i)
test("leads the install with the extension, then lists the standalone scripts first", () => {
expect(browse.install.lead).toContain("[Chrome extension]")
expect(browse.install.commands?.slice(0, 2)).toEqual([
{ method: "curl", command: "curl -fsSL https://opencode.ai/browse/install | bash" },
{ method: "pwsh", command: "irm https://opencode.ai/browse/install.ps1 | iex" },
])
})
test("keeps the documented title and description lengths", () => {
@@ -0,0 +1,94 @@
import { describe, expect, test } from "bun:test"
import type { ChangelogRelease } from "../src/lib/changelog"
import { changelogEntryView, changelogView, parseReleaseNotes } from "../src/lib/changelog-notes"
const pr = (number: number) => `[#${number}](https://github.com/anomalyco/opencode/pull/${number})`
// The shape of v2.2.6: five highlights, each repeated as a note under the area it belongs to.
const body = [
"### Highlights",
"",
`- **Shared MCP Connections**: Share one connection across projects. (${pr(61185)})`,
`- **GUI Plugin Themes and New Slots**: GUI plugins can add color themes. (${pr(61485)}, ${pr(61533)})`,
`- **Mobile Composer Dock**: Composer actions sit in a touch dock. (${pr(61773)})`,
`- **Scripted Runs in Worktrees**: Run \`opencode run -w\`. (${pr(60712)})`,
`- **Automatically Archived Runs**: Add \`--archived\` to \`opencode run\`. (${pr(61270)})`,
"",
"### Core",
"",
"#### Improvements",
"",
`- Added opt-in MCP connection sharing through \`pool: true\`. (${pr(61185)})`,
`- Added \`opencode run --archived\`. (${pr(61270)})`,
`- Added \`opencode run --worktree\`. (${pr(60712)})`,
`- Added Azure Messages support for Foundry Claude models. (${pr(60934)})`,
"",
"### Desktop",
"",
"#### Improvements",
"",
`- Added a mobile composer dock for touch devices. (${pr(61773)})`,
`- Added close-other-tabs and close-tabs-after actions to the tab menu. (${pr(61631)})`,
"",
"### Extensions",
"",
"#### Improvements",
"",
`- GUI plugins can contribute themes through the \`Theme\` registry. (${pr(61485)})`,
`- Added \`CodeBlock\` and \`CodeBlockAction\` GUI registries. (${pr(61533)})`,
].join("\n")
const release: ChangelogRelease = {
tag: "v2.2.6",
name: "v2.2.6",
date: "2026-11-06T23:37:20Z",
url: "https://github.com/anomalyco/opencode/releases/tag/v2.2.6",
highlights: [],
sections: [],
notes: parseReleaseNotes(body),
}
describe("release highlights", () => {
test("every highlight is read, with the areas of the notes that repeat it", () => {
expect(release.notes.features.map((feature) => [feature.title, feature.areas])).toEqual([
["Shared MCP Connections", ["Core"]],
["GUI Plugin Themes and New Slots", ["Extensions"]],
["Mobile Composer Dock", ["Desktop"]],
["Scripted Runs in Worktrees", ["Core"]],
["Automatically Archived Runs", ["Core"]],
])
})
test("all areas show every highlight: the first leads, the rest follow", () => {
const entry = changelogView([release]).entries[0]
expect(entry.title).toBe("Shared MCP Connections")
expect(entry.features.map((feature) => feature.title)).toEqual([
"GUI Plugin Themes and New Slots",
"Mobile Composer Dock",
"Scripted Runs in Worktrees",
"Automatically Archived Runs",
])
expect(entry.hiddenFeatures).toBe(0)
})
test("a release's own page shows every highlight", () => {
const entry = changelogEntryView([release], "v2.2.6").entries[0]
expect([entry.title, ...entry.features.map((feature) => feature.title)]).toHaveLength(5)
expect(entry.hiddenFeatures).toBe(0)
})
test("an area shows its own highlights and counts the ones it leaves out", () => {
const desktop = changelogView([release], { filter: ["desktop"] }).entries[0]
expect(desktop.title).toBe("Mobile Composer Dock")
expect(desktop.features).toEqual([])
expect(desktop.hiddenFeatures).toBe(4)
const core = changelogView([release], { filter: ["core"] }).entries[0]
expect([core.title, ...core.features.map((feature) => feature.title)]).toEqual([
"Shared MCP Connections",
"Scripted Runs in Worktrees",
"Automatically Archived Runs",
])
expect(core.hiddenFeatures).toBe(2)
})
})