diff --git a/packages/app/src/context/highlights.test.ts b/packages/app/src/context/highlights.test.ts index 596e26abd..08fa19b9f 100644 --- a/packages/app/src/context/highlights.test.ts +++ b/packages/app/src/context/highlights.test.ts @@ -18,7 +18,7 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => { }) }) - test("prefers the Chinese update notice for zh locale", () => { + test("prefers all Chinese update notice bullets for zh locale", () => { const payload = [ { tag_name: "v0.2.10", @@ -37,14 +37,18 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => { }, ] const highlights = loadReleaseHighlights(payload, "0.2.10", "0.2.9", "zh") - expect(highlights).toHaveLength(1) + expect(highlights).toHaveLength(2) expect(highlights[0]).toMatchObject({ title: "爪印 v0.2.10", description: "修复首条消息崩溃", }) + expect(highlights[1]).toMatchObject({ + title: "爪印 v0.2.10", + description: "调整更新提示", + }) }) - test("falls back to bullets directly under 中文版本 when 主要更新 is absent", () => { + test("falls back to all bullets directly under 中文版本 when 主要更新 is absent", () => { const payload = [ { tag_name: "v0.2.10", @@ -52,11 +56,15 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => { }, ] const highlights = loadReleaseHighlights(payload, "0.2.10", "0.2.9", "zh") - expect(highlights).toHaveLength(1) + expect(highlights).toHaveLength(2) expect(highlights[0]).toMatchObject({ title: "爪印 v0.2.10", description: "修复首条消息崩溃", }) + expect(highlights[1]).toMatchObject({ + title: "爪印 v0.2.10", + description: "调整更新提示", + }) }) test("falls back to the English update notice when Chinese summary is missing", () => { @@ -74,6 +82,18 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => { }) }) + 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"), + }, + ] + const highlights = loadReleaseHighlights(payload, "0.2.10", "0.2.9", "en") + expect(highlights).toHaveLength(1) + expect(highlights[0].description).toBe("Fixed first-message crash and improved the update notice parser.") + }) + test("skips markdown headings and strips bullet markers inside the app update notice section", () => { const payload = [ { @@ -82,7 +102,93 @@ describe("loadReleaseHighlights (GitHub Releases API)", () => { }, ] const highlights = loadReleaseHighlights(payload, "0.3.0", "0.2.3", "en") - expect(highlights[0].description).toBe("Added dark theme") + expect(highlights.map((highlight) => highlight.description)).toEqual(["Added dark theme", "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"), + }, + ] + 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", + ]) + }) + + test("keeps all localized update notice bullets", () => { + const payload = [ + { + tag_name: "v2026.4.29", + body: [ + "## App Update Notice", + "", + "PawWork refreshes the desktop.", + "", + "## 中文版本", + "", + "### 主要更新", + "", + "PawWork 2026.4.29 刷新桌面界面。", + "", + "- 刷新桌面界面", + "- 修复首次进入 Home 时左右侧栏默认打开的问题", + "- 移除内置 Trash 工具", + "- 提升 session 稳定性", + "- 新增前台 subagent 生命周期支持", + "- 默认启用 open permissions", + "- 修复 Windows 拖拽上传", + ].join("\n"), + }, + ] + 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 拖拽上传", + ]) + }) + + test("keeps skipped-version highlights beyond the old five item cap", () => { + const payload = [ + { + tag_name: "v2026.4.29", + body: "## App Update Notice\n\n- A\n- B\n- C\n", + }, + { + tag_name: "v2026.4.28", + body: "## App Update Notice\n\n- D\n- E\n- F\n", + }, + ] + + expect(loadReleaseHighlights(payload, "2026.4.29", "2026.4.27", "en").map((highlight) => highlight.description)).toEqual([ + "A", + "B", + "C", + "D", + "E", + "F", + ]) + }) + + test("limits long skipped-version highlight ranges", () => { + const payload = [ + { + tag_name: "v2026.4.29", + body: ["## App Update Notice", "", ...Array.from({ length: 20 }, (_, index) => `- Item ${index + 1}`)].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") }) test("truncates long summaries with an ellipsis", () => { diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx index 86536edd5..af18b7aea 100644 --- a/packages/app/src/context/highlights.tsx +++ b/packages/app/src/context/highlights.tsx @@ -9,6 +9,7 @@ import { persisted } from "@/utils/persist" import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes" const CHANGELOG_URL = "https://api.github.com/repos/Astro-Han/pawwork/releases" +const MAX_RELEASE_HIGHLIGHTS = 15 type Store = { version?: string @@ -88,24 +89,44 @@ function findChineseUpdateNotice(body: string) { return findHeadingSection(chinese, /^#{3,6}\s+主要更新\s*$/) ?? chinese } -function summarizeNotice(notice: string | undefined): string | undefined { - if (!notice) return +function trimNoticeItem(value: string) { + const text = value.trim() + return text.length > 200 ? text.slice(0, 200).trimEnd() + "…" : text +} + +function parseNoticeDescriptions(notice: string | undefined): string[] { + if (!notice) return [] const lines = notice .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0 && !line.startsWith("#")) - if (lines.length === 0) return - const first = lines[0].replace(/^[-*+]\s+/, "").replace(/^\d+\.\s+/, "") - return first.length > 200 ? first.slice(0, 200).trimEnd() + "…" : first + + const bullets: string[] = [] + let currentBullet: string | undefined + for (const line of lines) { + const match = line.match(/^(?:[-*+]\s+|\d+\.\s+)(.+)$/) + if (match) { + if (currentBullet) bullets.push(trimNoticeItem(currentBullet)) + currentBullet = match[1].trim() + continue + } + if (currentBullet) currentBullet += ` ${line}` + } + if (currentBullet) bullets.push(trimNoticeItem(currentBullet)) + + if (bullets.length > 0) return bullets + + const summary = trimNoticeItem(lines.join(" ")) + return summary ? [summary] : [] } -function summarizeReleaseBody(body: string, locale: ReleaseLocale) { +function parseReleaseBodyDescriptions(body: string, locale: ReleaseLocale) { if (locale === "zh") { - const chinese = summarizeNotice(findChineseUpdateNotice(body)) - if (chinese) return chinese + const chinese = parseNoticeDescriptions(findChineseUpdateNotice(body)) + if (chinese.length > 0) return chinese } - return summarizeNotice(findAppUpdateNotice(body)) + return parseNoticeDescriptions(findAppUpdateNotice(body)) } function releaseTitle(tag: string, locale: ReleaseLocale) { @@ -138,11 +159,11 @@ function parseRelease(value: unknown, locale: ReleaseLocale): ParsedRelease | un const body = getText(value.body) if (tag && body) { - const summary = summarizeReleaseBody(body, locale) - if (summary) { + const descriptions = parseReleaseBodyDescriptions(body, locale) + if (descriptions.length > 0) { return { tag, - highlights: [{ title: releaseTitle(tag, locale), description: summary }], + highlights: descriptions.map((description) => ({ title: releaseTitle(tag, locale), description })), } } } @@ -190,7 +211,7 @@ function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; p seen.add(key) return true }) - return unique.slice(0, 5) + return unique.slice(0, MAX_RELEASE_HIGHLIGHTS) } function dedupeKey(highlight: Highlight) {