Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions packages/app/src/components/dialog-release-notes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSignal } from "solid-js"
import { createEffect, createSignal } from "solid-js"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
Expand All @@ -20,6 +20,7 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
const language = useLanguage()
const settings = useSettings()
const [index, setIndex] = createSignal(0)
let descriptionRef: HTMLParagraphElement | undefined

const total = () => props.highlights.length
const last = () => Math.max(0, total() - 1)
Expand All @@ -28,6 +29,12 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
const isLast = () => index() >= last()
const paged = () => total() > 1

createEffect(() => {
// Reset scroll position when page changes
index()
if (descriptionRef) descriptionRef.scrollTop = 0
})

function handleNext() {
if (isLast()) return
setIndex(index() + 1)
Expand Down Expand Up @@ -68,20 +75,27 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
>
<div class="flex flex-1 min-w-0 min-h-0" tabIndex={0} autofocus onKeyDown={handleKeyDown}>
{/* Left side - Text content */}
<div class="flex flex-col flex-1 min-w-0 p-8">
<div class="flex flex-col flex-1 min-w-0 min-h-0 p-8">
{/* Top section - feature content (fixed position from top) */}
<div class="flex flex-col gap-2 pt-22">
<div class="flex items-center gap-2">
<h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
<div class="flex flex-col flex-1 min-h-0 gap-2 pt-22 pb-4">
<div class="flex items-center gap-2 shrink-0">
<h1 id="release-notes-title" class="text-16-medium text-text-strong">
{feature()?.title ?? ""}
</h1>
</div>
<p class="text-13-regular text-text-base">{feature()?.description ?? ""}</p>
<p
ref={descriptionRef}
role="region"
aria-labelledby="release-notes-title"
tabIndex={0}
class="text-13-regular text-text-base whitespace-pre-line overflow-y-auto min-h-0 flex-1 pr-2"
>
{feature()?.description ?? ""}
</p>
</div>

{/* Spacer to push buttons to bottom */}
<div class="flex-1" />

{/* Bottom section - buttons and indicators (fixed position) */}
<div class="flex flex-col gap-12">
<div class="flex flex-col gap-12 shrink-0">
<div class="flex flex-col items-start gap-3">
{isLast() ? (
<Button variant="primary" size="large" onClick={handleClose}>
Expand Down
211 changes: 166 additions & 45 deletions packages/app/src/context/highlights.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,34 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => {
},
]
const highlights = loadReleaseHighlights(payload, "0.2.10", "0.2.9", "zh")
expect(highlights).toHaveLength(2)
expect(highlights).toHaveLength(1)
expect(highlights[0]).toMatchObject({
title: "爪印 v0.2.10",
description: "修复首条消息崩溃",
})
expect(highlights[1]).toMatchObject({
title: "爪印 v0.2.10",
description: "调整更新提示",
description: "• 修复首条消息崩溃\n• 调整更新提示",
})
})

test("falls back to all bullets directly under 中文版本 when 主要更新 is absent", () => {
const payload = [
{
tag_name: "v0.2.10",
body: ["## App Update Notice", "", "- Fixed first-message crash", "", "## 中文版本", "", "- 修复首条消息崩溃", "- 调整更新提示"].join("\n"),
body: [
"## App Update Notice",
"",
"- Fixed first-message crash",
"",
"## 中文版本",
"",
"- 修复首条消息崩溃",
"- 调整更新提示",
].join("\n"),
},
]
const highlights = loadReleaseHighlights(payload, "0.2.10", "0.2.9", "zh")
expect(highlights).toHaveLength(2)
expect(highlights).toHaveLength(1)
expect(highlights[0]).toMatchObject({
title: "爪印 v0.2.10",
description: "修复首条消息崩溃",
})
expect(highlights[1]).toMatchObject({
title: "爪印 v0.2.10",
description: "调整更新提示",
description: "• 修复首条消息崩溃\n• 调整更新提示",
})
})

Expand All @@ -78,15 +79,17 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => {
expect(highlights).toHaveLength(1)
expect(highlights[0]).toMatchObject({
title: "爪印 v0.2.10",
description: "Fixed first-message crash",
description: "• Fixed first-message crash",
})
})

test("keeps hard-wrapped paragraph notices as one card", () => {
const payload = [
{
tag_name: "v0.2.10",
body: ["## App Update Notice", "", "Fixed first-message crash and improved", "the update notice parser."].join("\n"),
body: ["## App Update Notice", "", "Fixed first-message crash and improved", "the update notice parser."].join(
"\n",
),
},
]
const highlights = loadReleaseHighlights(payload, "0.2.10", "0.2.9", "en")
Expand All @@ -102,21 +105,28 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => {
},
]
const highlights = loadReleaseHighlights(payload, "0.3.0", "0.2.3", "en")
expect(highlights.map((highlight) => highlight.description)).toEqual(["Added dark theme", "Fixed dock icon"])
expect(highlights).toHaveLength(1)
expect(highlights[0].description).toBe("• Added dark theme\n• Fixed dock icon")
})

test("keeps wrapped bullet continuation lines", () => {
const payload = [
{
tag_name: "v0.3.0",
body: ["## App Update Notice", "", "- Fixed first-message crash", " when startup takes longer", "- Added update notice parser"].join("\n"),
body: [
"## App Update Notice",
"",
"- Fixed first-message crash",
" when startup takes longer",
"- Added update notice parser",
].join("\n"),
},
]
const highlights = loadReleaseHighlights(payload, "0.3.0", "0.2.3", "en")
expect(highlights.map((highlight) => highlight.description)).toEqual([
"Fixed first-message crash when startup takes longer",
"Added update notice parser",
])
expect(highlights).toHaveLength(1)
expect(highlights[0].description).toBe(
"• Fixed first-message crash when startup takes longer\n• Added update notice parser",
)
})

test("keeps all localized update notice bullets", () => {
Expand Down Expand Up @@ -145,18 +155,13 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => {
},
]
const highlights = loadReleaseHighlights(payload, "2026.4.29", "2026.4.28", "zh")
expect(highlights.map((highlight) => highlight.description)).toEqual([
"刷新桌面界面",
"修复首次进入 Home 时左右侧栏默认打开的问题",
"移除内置 Trash 工具",
"提升 session 稳定性",
"新增前台 subagent 生命周期支持",
"默认启用 open permissions",
"修复 Windows 拖拽上传",
])
expect(highlights).toHaveLength(1)
expect(highlights[0].description).toBe(
"PawWork 2026.4.29 刷新桌面界面。\n• 刷新桌面界面\n• 修复首次进入 Home 时左右侧栏默认打开的问题\n• 移除内置 Trash 工具\n• 提升 session 稳定性\n• 新增前台 subagent 生命周期支持\n• 默认启用 open permissions\n• 修复 Windows 拖拽上传",
)
})

test("keeps skipped-version highlights beyond the old five item cap", () => {
test("keeps skipped-version highlights at version granularity", () => {
const payload = [
{
tag_name: "v2026.4.29",
Expand All @@ -168,27 +173,47 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => {
},
]

expect(loadReleaseHighlights(payload, "2026.4.29", "2026.4.27", "en").map((highlight) => highlight.description)).toEqual([
"A",
"B",
"C",
"D",
"E",
"F",
const highlights = loadReleaseHighlights(payload, "2026.4.29", "2026.4.27", "en")
expect(highlights).toHaveLength(2)
expect(highlights.map((highlight) => highlight.description)).toEqual(["• A\n• B\n• C", "• D\n• E\n• F"])
})

test("limits long skipped-version ranges to five version pages", () => {
const payload = Array.from({ length: 6 }, (_, index) => {
const patch = 6 - index

return {
tag_name: `v1.0.${patch}`,
body: `## App Update Notice\n\n- Item ${patch}\n`,
}
})

const highlights = loadReleaseHighlights(payload, "1.0.6", "1.0.0", "en")

expect(highlights).toHaveLength(5)
expect(highlights.map((highlight) => highlight.title)).toEqual([
"PawWork v1.0.6",
"PawWork v1.0.5",
"PawWork v1.0.4",
"PawWork v1.0.3",
"PawWork v1.0.2",
])
})

test("limits long skipped-version highlight ranges", () => {
test("does not cap bullets inside a single version page", () => {
const payload = [
{
tag_name: "v2026.4.29",
body: ["## App Update Notice", "", ...Array.from({ length: 20 }, (_, index) => `- Item ${index + 1}`)].join("\n"),
tag_name: "v1.0.0",
body: ["## App Update Notice", "", "- Item 1", "- Item 2", "- Item 3", "- Item 4", "- Item 5", "- Item 6"].join(
"\n",
),
},
]

const highlights = loadReleaseHighlights(payload, "2026.4.29", "2026.4.28", "en")
expect(highlights).toHaveLength(15)
expect(highlights.at(-1)?.description).toBe("Item 15")
const highlights = loadReleaseHighlights(payload, "1.0.0", "0.9.0", "en")

expect(highlights).toHaveLength(1)
expect(highlights[0].description).toBe("• Item 1\n• Item 2\n• Item 3\n• Item 4\n• Item 5\n• Item 6")
})

test("truncates long summaries with an ellipsis", () => {
Expand Down Expand Up @@ -218,7 +243,7 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => {
]
const highlights = loadReleaseHighlights(payload, "0.2.6", "0.2.5", "en")
expect(highlights).toHaveLength(1)
expect(highlights[0].description).toBe("Fixed update notices")
expect(highlights[0].description).toBe("• Fixed update notices")
})

test("returns no highlights when the body is empty or only headings", () => {
Expand Down Expand Up @@ -258,4 +283,100 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => {
const highlights = loadReleaseHighlights(payload, "0.2.5", "0.2.4", "zh")
expect(highlights[0]?.title).toBe("PawWork card")
})

test("keeps structured highlight items as separate pages", () => {
const payload = [
{
tag: "v0.2.5",
highlights: [
{
source: "desktop",
items: [
{ title: "Card A", description: "Description A" },
{ title: "Card B", description: "Description B" },
],
},
],
},
]

const highlights = loadReleaseHighlights(payload, "0.2.5", "0.2.4", "en")

expect(highlights).toHaveLength(2)
expect(highlights.map((highlight) => highlight.title)).toEqual(["Card A", "Card B"])
})

test("keeps structured highlight cap independent from release-body version cap", () => {
const payload = [
{
tag: "v0.2.5",
highlights: [
{
source: "desktop",
items: Array.from({ length: 6 }, (_, index) => ({
title: `Card ${index + 1}`,
description: `Description ${index + 1}`,
})),
},
],
},
]

const highlights = loadReleaseHighlights(payload, "0.2.5", "0.2.4", "en")

expect(highlights).toHaveLength(6)
expect(highlights.map((highlight) => highlight.title)).toEqual([
"Card 1",
"Card 2",
"Card 3",
"Card 4",
"Card 5",
"Card 6",
])
})

test("keeps structured highlight limit at fifteen cards", () => {
const payload = [
{
tag: "v0.2.5",
highlights: [
{
source: "desktop",
items: Array.from({ length: 16 }, (_, index) => ({
title: `Card ${index + 1}`,
description: `Description ${index + 1}`,
})),
},
],
},
]

const highlights = loadReleaseHighlights(payload, "0.2.5", "0.2.4", "en")

expect(highlights).toHaveLength(15)
expect(highlights.at(-1)?.title).toBe("Card 15")
})

test("preserves intro prose before bullets in mixed content notices", () => {
const payload = [
{
tag_name: "v1.0.0",
body: [
"## App Update Notice",
"",
"Important migration note for this release.",
"",
"- Fixed crash on startup",
"- Added dark mode support",
].join("\n"),
},
]

const highlights = loadReleaseHighlights(payload, "1.0.0", "0.9.0", "en")

expect(highlights).toHaveLength(1)
expect(highlights[0].description).toBe(
"Important migration note for this release.\n• Fixed crash on startup\n• Added dark mode support",
)
})
})
Loading
Loading