From ff481ed7f8ef37a23fb2ac5cd7848408bc5064a5 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 3 May 2026 11:23:41 +0800 Subject: [PATCH 1/6] fix(app): paginate release notes by version instead of bullet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change MAX_RELEASE_HIGHLIGHTS from 15 to 5 (version pages, not bullets) - Introduce ParsedNotice type to distinguish bullets from summary paragraphs - Refactor parseNoticeDescriptions to return ParsedNotice | undefined - Add formatReleaseNoticeDescription to merge bullets with • prefix - Update parseRelease body path: one version = one Highlight - Keep structured highlights schema behavior unchanged - Make DialogReleaseNotes description scrollable with whitespace-pre-line - Fix title and buttons to stay fixed while description scrolls - Update tests to expect merged bullets per version page - Add regression tests for version granularity and structured schema Closes #398 --- .../src/components/dialog-release-notes.tsx | 15 +- packages/app/src/context/highlights.test.ts | 137 ++++++++++++------ packages/app/src/context/highlights.tsx | 60 ++++++-- 3 files changed, 148 insertions(+), 64 deletions(-) diff --git a/packages/app/src/components/dialog-release-notes.tsx b/packages/app/src/components/dialog-release-notes.tsx index e83bc754d..7488fb7eb 100644 --- a/packages/app/src/components/dialog-release-notes.tsx +++ b/packages/app/src/components/dialog-release-notes.tsx @@ -68,20 +68,19 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) { >
{/* Left side - Text content */} -
+
{/* Top section - feature content (fixed position from top) */} -
-
+
+

{feature()?.title ?? ""}

-

{feature()?.description ?? ""}

+

+ {feature()?.description ?? ""} +

- {/* Spacer to push buttons to bottom */} -
- {/* Bottom section - buttons and indicators (fixed position) */} -
+
{isLast() ? (

{ const highlights = loadReleaseHighlights(payload, "2026.4.29", "2026.4.28", "zh") expect(highlights).toHaveLength(1) expect(highlights[0].description).toBe( - "• 刷新桌面界面\n• 修复首次进入 Home 时左右侧栏默认打开的问题\n• 移除内置 Trash 工具\n• 提升 session 稳定性\n• 新增前台 subagent 生命周期支持\n• 默认启用 open permissions\n• 修复 Windows 拖拽上传", + "PawWork 2026.4.29 刷新桌面界面。\n• 刷新桌面界面\n• 修复首次进入 Home 时左右侧栏默认打开的问题\n• 移除内置 Trash 工具\n• 提升 session 稳定性\n• 新增前台 subagent 生命周期支持\n• 默认启用 open permissions\n• 修复 Windows 拖拽上传", ) }) @@ -356,4 +356,27 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => { 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", + ) + }) }) diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx index a240f7a5b..12a2c916f 100644 --- a/packages/app/src/context/highlights.tsx +++ b/packages/app/src/context/highlights.tsx @@ -100,6 +100,8 @@ type ParsedNotice = | { kind: "bullets" items: string[] + intro?: string + outro?: string } | { kind: "summary" @@ -115,22 +117,34 @@ function parseNoticeContent(notice: string | undefined): ParsedNotice | undefine .filter((line) => line.length > 0 && !line.startsWith("#")) const bullets: string[] = [] + const prose: string[] = [] let currentBullet: string | undefined + let hasSeenBullet = false + for (const line of lines) { const match = line.match(/^(?:[-*+]\s+|\d+\.\s+)(.+)$/) if (match) { if (currentBullet) bullets.push(trimNoticeItem(currentBullet)) currentBullet = match[1].trim() + hasSeenBullet = true continue } - if (currentBullet) currentBullet += ` ${line}` + + if (currentBullet) { + currentBullet += ` ${line}` + } else if (!hasSeenBullet) { + prose.push(line) + } + // trailing prose after last bullet is ignored (intentionally) } if (currentBullet) bullets.push(trimNoticeItem(currentBullet)) if (bullets.length > 0) { + const intro = prose.length > 0 ? trimNoticeItem(prose.join(" ")) : undefined return { kind: "bullets", items: bullets, + ...(intro ? { intro } : {}), } } @@ -153,7 +167,11 @@ function parseReleaseBodyNotice(body: string, locale: ReleaseLocale): ParsedNoti function formatReleaseNoticeDescription(notice: ParsedNotice) { if (notice.kind === "bullets") { - return notice.items.map((item) => `• ${item}`).join("\n") + const bullets = notice.items.map((item) => `• ${item}`).join("\n") + if (notice.intro) { + return `${notice.intro}\n${bullets}` + } + return bullets } return notice.text From a0dc47e38c2d580b2faa8870194c6c31e0fb13de Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 3 May 2026 12:04:21 +0800 Subject: [PATCH 4/6] fix(app): use queueMicrotask for scrollTop reset Ensure DOM is fully updated before resetting scroll position when page index changes. --- packages/app/src/components/dialog-release-notes.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-release-notes.tsx b/packages/app/src/components/dialog-release-notes.tsx index d645f29a2..7c642d3f1 100644 --- a/packages/app/src/components/dialog-release-notes.tsx +++ b/packages/app/src/components/dialog-release-notes.tsx @@ -32,7 +32,9 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) { createEffect(() => { // Reset scroll position when page changes index() - if (descriptionRef) descriptionRef.scrollTop = 0 + queueMicrotask(() => { + if (descriptionRef) descriptionRef.scrollTop = 0 + }) }) function handleNext() { From 4979c03ac4820d5a26f5612d6cafcd39e06b8f46 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 3 May 2026 12:11:36 +0800 Subject: [PATCH 5/6] fix(app): prevent trailing prose from merging into last bullet - Stop filtering empty lines early; use them as paragraph breaks - Flush currentBullet when encountering an empty line - Add test ensuring trailing prose is not folded into final bullet --- packages/app/src/context/highlights.test.ts | 14 ++++++++++++++ packages/app/src/context/highlights.tsx | 13 +++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/app/src/context/highlights.test.ts b/packages/app/src/context/highlights.test.ts index 2e8d274b1..e076c44cf 100644 --- a/packages/app/src/context/highlights.test.ts +++ b/packages/app/src/context/highlights.test.ts @@ -379,4 +379,18 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => { "Important migration note for this release.\n• Fixed crash on startup\n• Added dark mode support", ) }) + + test("does not fold trailing prose into the final bullet", () => { + const payload = [ + { + tag_name: "v1.0.0", + body: ["## App Update Notice", "", "- Fixed sync", "", "Known issue: restart required."].join("\n"), + }, + ] + + const highlights = loadReleaseHighlights(payload, "1.0.0", "0.9.0", "en") + + expect(highlights).toHaveLength(1) + expect(highlights[0].description).toBe("• Fixed sync") + }) }) diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx index 12a2c916f..33ff0f2d8 100644 --- a/packages/app/src/context/highlights.tsx +++ b/packages/app/src/context/highlights.tsx @@ -114,7 +114,7 @@ function parseNoticeContent(notice: string | undefined): ParsedNotice | undefine const lines = notice .split(/\r?\n/) .map((line) => line.trim()) - .filter((line) => line.length > 0 && !line.startsWith("#")) + .filter((line) => !line.startsWith("#")) const bullets: string[] = [] const prose: string[] = [] @@ -122,6 +122,15 @@ function parseNoticeContent(notice: string | undefined): ParsedNotice | undefine let hasSeenBullet = false for (const line of lines) { + if (line.length === 0) { + // Empty line acts as a paragraph break: flush any active bullet + if (currentBullet) { + bullets.push(trimNoticeItem(currentBullet)) + currentBullet = undefined + } + continue + } + const match = line.match(/^(?:[-*+]\s+|\d+\.\s+)(.+)$/) if (match) { if (currentBullet) bullets.push(trimNoticeItem(currentBullet)) @@ -148,7 +157,7 @@ function parseNoticeContent(notice: string | undefined): ParsedNotice | undefine } } - const summary = trimNoticeItem(lines.join(" ")) + const summary = trimNoticeItem(lines.filter((line) => line.length > 0).join(" ")) if (!summary) return return { From f095785ba29f1dfce565420b95e5c7e81845f67b Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 3 May 2026 12:26:29 +0800 Subject: [PATCH 6/6] chore(app): remove unused outro field from ParsedNotice type --- packages/app/src/context/highlights.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx index 33ff0f2d8..e04b542ab 100644 --- a/packages/app/src/context/highlights.tsx +++ b/packages/app/src/context/highlights.tsx @@ -101,7 +101,6 @@ type ParsedNotice = kind: "bullets" items: string[] intro?: string - outro?: string } | { kind: "summary"