Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
116 changes: 111 additions & 5 deletions packages/app/src/context/highlights.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -37,26 +37,34 @@ 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",
body: ["## App Update Notice", "", "- Fixed first-message crash", "", "## 中文版本", "", "- 修复首条消息崩溃", "- 调整更新提示"].join("\n"),
},
]
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", () => {
Expand All @@ -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 = [
{
Expand All @@ -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", () => {
Expand Down
47 changes: 34 additions & 13 deletions packages/app/src/context/highlights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 })),
}
}
}
Expand Down Expand Up @@ -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) {
Expand Down
Loading