Skip to content

perf(desktop): /issues 首屏读落盘快照,加载中不再谎报「没有 issue」 - #1229

Merged
dashhuang merged 2 commits into
dash/issues-enhancement-fallbackfrom
dash/issues-first-paint-cache
Jul 31, 2026
Merged

perf(desktop): /issues 首屏读落盘快照,加载中不再谎报「没有 issue」#1229
dashhuang merged 2 commits into
dash/issues-enhancement-fallbackfrom
dash/issues-first-paint-cache

Conversation

@dashhuang

Copy link
Copy Markdown
Member

这次改了什么

摘要

用户实测反馈:点开 /issues 先看到一个「没有 issue」的状态,几秒后才跳出 35 条。

两个成因叠加:

  1. 首屏没有任何本地数据可显示。 service 那层的 60s TTL 缓存是内存的,进程一重启即失效、冷启动必然 miss;而列表要等平台通道与 GitHub 增强都落地才出得来(增强走插件失败 + gh CLI 回退时实测 2565ms)。

  2. 等待期间显示的是一句错误断言。 feat(desktop): Issue 页展示自己提交过的 Issue #1103 为遵守 engineering-conventions §7「取数期间界面不发生变化」让首屏保留引导内容,但那个引导带着结论性标题 —— 有 35 条的用户先被告知「暂时查不到你的 Issue」,再跳成列表。比 loading 文案更糟:它是错的,而且照样是两次形态切换。

修复后冷启动实测:读快照 1ms 出 35 条,远端 2565ms 到达后原子替换。

变更类型

  • feat 新功能
  • fix 缺陷修复
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护

范围

  • 关联:用户实机反馈;延续 feat(desktop): Issue 页展示自己提交过的 Issue #1103 / fix(desktop): GitHub 增强搜不到时回退 gh CLI,空列表不再谎称从未提交 #1224本 PR 基于 fix(desktop): GitHub 增强搜不到时回退 gh CLI,空列表不再谎称从未提交 #1224 的分支,见文末)
  • 本 PR 包含:
    • 新增 myIssuesSnapshotStoreelectron-store + ownerScopedUserDataPath(),形状照同目录的 submittedIssueLedger
    • service 的 settle() 里落盘快照,判据与内存缓存同一个
    • 新 channel MY_ISSUES_SNAPSHOT 供 renderer 首屏读取
    • useMyIssues 里 fresh 与快照分开存 + 新增 hasFreshData
    • 空态标题在「这一轮还没查完」时整个不渲染
  • 明确不包含:
    • 不设过期时间。旧数据也比空白好,且进页面一定会刷新;离线时显示上次结果正是快照的价值(同 mirrorCacheStore 的取舍)
    • 不做 clearAll:与同目录账本一致,靠 owner 目录隔离
  • 用户可见变化:
    • 第二次及以后进 /issues 立刻看到上次的列表(内容可读可点),header 刷新图标转着,fresh 到达后原子替换
    • 首次使用(无快照)时引导正文与 CTA 照旧在场,但不再显示任何结论性标题
  • 是否存在 breaking change:无

UI 变化

深色模式实机截图(macOS):导航到 /issues 后仅 350ms 抓帧 —— 远端要 2.5s,所以画面内容只可能来自快照。DOM 探针同时确认 rows: 35hasEmptyTitle: false

  • 引用的设计规范:
    • DESIGN.md §14.4 Motion & Transitions:常见路径上不再有形态切换(快照 → fresh 是同构内容的原子替换);进度反馈仍只在 header 图标(已登记的 animate-spinner
    • DESIGN.md §11 Voice & Content:标题是结论,未查证时不显示 —— 不用「正在加载…」这类 loading 文案占位(§7 默认不做 loading 态界面)
    • DESIGN.md §10 Token Selection Rules for New UI:无新增 token、无新增样式,仅条件渲染既有 <h2>
    • 文案零新增(沿用 fix(desktop): GitHub 增强搜不到时回退 gh CLI,空列表不再谎称从未提交 #1224emptyTitle / emptyTitleUnverified),四语无改动

怎么验证的

自动验证

bash Skills/git/scripts/run-unit-gate.sh
结果:GATE_EXIT=0(56 个 workspace PASS / 0 FAIL)

pnpm --filter desktop run --if-present typecheck
结果:通过

相关用例:166 例全绿(新增快照 store 15 例 + scope 隔离 4 例 + service 写入 5 例 + 布局 3 例)

变异验证trustable 不看 hasFreshData / settle 不写快照 / 快照 url 改信落盘值 —— 4 条用例各自失败、其余不受影响;恢复后全绿。

手工验证

--preserve-running passive dev 实例(macOS,复用真实登录态与插件 PAT),经 CDP 取证:

第一步 —— 首次使用(无快照)到写入:

查询前读快照 → {"snapshot": null}
触发查询     → {"count":35,"degraded":"platform-unavailable","enhancementFailed":false}
查询后读快照 → {"count":35,"cachedAt":"2026-07-31T13:59:49.771Z",
                "enhancement":{"login":"dashhuang","source":"ghost"},
                "firstTwo":[{"n":1222,"state":"open","url":"https://github.com/makecindy/cindy/issues/1222"}, …],
                "hasDegradedField":false,"hasFailedField":false}

hasDegradedField / hasFailedField 均为 false —— 「这一次查得怎么样」确实没进快照。

第二步 —— 杀掉实例重启(内存缓存全空)后对比两条路径:

{"snapshot":{"ms":1,"count":35},"fresh":{"ms":2565,"count":35}}

第三步 —— 首屏渲染取证: 导航到 /issues 后 350ms 抓帧,DOM 探针 {"rows":35,"hasEmptyTitle":false},截图为完整列表。

落盘位置: ~/Library/Application Support/Cindy/owners/<ownerHash>/my-issues-snapshot.json(13.5 KB),不在仓库内,git status 干净。

未执行的验证

  • 浅色模式未实机目检(同 fix(desktop): GitHub 增强搜不到时回退 gh CLI,空列表不再谎称从未提交 #1224 的原因:应用主题由 React 按设置注入 token 值,CDP 只能改 data-theme 标记;真正切换需改显示模式设置,而 --preserve-running 与用户主实例共享 userData,不动它)。本次无新增样式,仅条件渲染既有 <h2>
  • 切号瞬间的首屏未真机验证(需要两个真实 Cindy 账号)。账号隔离由 4 条 scope 用例覆盖:切 owner 后读不到上一个账号的快照、切回仍能读到自己那份、B 的写入不污染 A。

风险

风险分类

  • 无已知风险
  • 权限 / 安全 / 用户数据 ← 见下方说明

影响与回滚

  • 影响范围:仅 /issues 首屏。快照是只读加速层,写路径(提交 issue)永不读它;fresh 一到即整体接管。
  • 用户数据:快照含 issue 标题与 GitHub 用户名,属账号私有数据,因此落在 ownerScopedUserDataPath() 下(与同目录账本同一处理),换号 / 登出后天然读不到;renderer 不直接读写磁盘(electron-security-and-process-boundaries §2),读取经 IPC 且带 assertTrustedAppRendererEvent。落盘内容按不可信输入清洗,其中 url 一律按 number 派生、不采纳落盘值。
  • 回滚 / 降级方式:整个 PR 可直接 revert,/issues 回到「冷启动空等几秒」的行为;残留的快照 JSON 无害(无人读取),也可随 owner 目录一并清理。不涉及数据迁移或协议变更。

依赖关系

本 PR 基于 #1224dash/issues-enhancement-fallback)的分支,因为要复用它引入的 canTrustEmptyList()#1224 合并进 main 后本分支 rebase 即可(其提交已在 main,git 自动处理)。建议在 #1224 之后再合并本 PR。

🤖 Generated with Claude Code

用户实测:点开 /issues 先看到一个「没有 issue」的状态,几秒后才跳出 35 条。

两个成因叠加:
1) 首屏没有任何本地数据可显示。service 那层的 60s TTL 缓存是**内存**的,进程一重启即
   失效、冷启动必然 miss;而列表要等平台通道与 GitHub 增强都落地才出得来(增强走插件
   失败 + gh CLI 回退时实测约 2s)。
2) 等待期间显示的是一句错误断言。#1103 为遵守 §7「取数期间界面不发生变化」让首屏保留
   引导内容,但那个引导带着结论性标题 —— 有 35 条的用户先被告知「暂时查不到你的 Issue」
   再跳成列表。比 loading 文案更糟:它是错的,而且照样是两次形态切换。

本次:
- 新增 myIssuesSnapshotStore(electron-store + ownerScopedUserDataPath,形状照同目录的
  submittedIssueLedger)。**不照搬** device-link/mirrorCacheStore 那 1500 行:它的 purge
  队列、跨进程锁、作废屏障是为多设备消息文件与内联媒体设计的。但借用它的语义边界 ——
  快照是可重建的首屏镜像、不是真相,fresh 一到即整体接管。
- 快照**刻意不含** degraded / githubEnhancementFailed / truncated:那三个描述的是
  「这一次查得怎么样」,缓存它们会让用户进页面就看到一条过期的错误提示。
- 写入点放 settle()(结果落地的唯一收口,scope 校验已在那里),判据与内存缓存**同一个**:
  epoch 变了说明期间有提交成功过,落一份已知过时的镜像没有收益。不为快照另立判据。
- 读取走新 channel MY_ISSUES_SNAPSHOT(同一道 assertTrustedAppRendererEvent —— 快照含
  issue 标题与 GitHub 用户名,是账号私有数据)。不给 listMyIssues 加 cachedOnly 参数:
  那样得再引入字段区分「缓存是空列表」与「根本没有缓存」,把 list 的契约搞混。
- useMyIssues 里 fresh 与快照**分开存**,并新增 hasFreshData。快照的空列表**不构成**
  「查证过的空」—— 若空态标题只看 canTrustEmptyList,首屏又会冒出「还没有提交过 Issue」,
  就是这一族错误的第三次。hasFreshData 为 false 时整个标题不渲染(引导正文与 CTA 照旧
  在场,它们无论有没有 issue 都成立)。
- 快照读取与真实查询**并行发起**,不 await 它再查:占位不该挡在最慢那条路前面。

变异验证:trustable 不看 hasFreshData / settle 不写快照 / 快照 url 改信落盘值 ——
4 条用例各自失败、其余不受影响。门禁 GATE_EXIT=0(56 workspace PASS / 0 FAIL)、
desktop typecheck 通过、改动文件无 lint 问题;相关用例 166 例全绿。

文案零新增(沿用 #1224 的 emptyTitle / emptyTitleUnverified),四语无改动。

Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com>
Signed-off-by: Dash <dashhuang@gmail.com>
@dashhuang
dashhuang requested a review from a team as a code owner July 31, 2026 14:04
@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 为 /issues 增加按账号隔离的落盘快照,并调整首屏渲染与菜单文案。

  • main 进程在成功查询后保存快照,并通过受信 renderer IPC 提供读取。
  • renderer 并行读取快照和 fresh 数据,优先展示 fresh 结果。
  • 未完成 fresh 查询时不再渲染结论性的空态标题。
  • 更新中日文菜单文案及相关类型和测试。

Confidence Score: 4/5

该 PR 暂不适合合并,因为已报告的快照静默截断问题仍然存在,需要先修复。

当合并后的 Issue 超过 200 条时,快照存储会丢弃后续条目,但 renderer 将该快照标记为未截断;若 fresh 查询失败,界面会继续展示这份不完整数据且不会给出截断提示。

Files Needing Attention: apps/desktop/src/main/github-issue/myIssuesSnapshotStore.ts, apps/desktop/src/renderer/features/issue-tracker/hooks/useMyIssues.ts

Important Files Changed

Filename Overview
apps/desktop/src/main/github-issue/myIssuesSnapshotStore.ts 新增按 owner 隔离的 Issue 快照存储、落盘数据清洗及条目数量上限。
apps/desktop/src/main/github-issue/myIssuesService.ts 在查询结果满足现有缓存判据时,以 best-effort 方式写入首屏快照。
apps/desktop/src/main/maker-ipc/my-issues.ts 新增经过受信 renderer 来源校验的快照读取 IPC。
apps/desktop/src/renderer/features/issue-tracker/hooks/useMyIssues.ts 将快照与 fresh 状态分开管理,并让 fresh 数据到达后接管视图。
apps/desktop/src/renderer/features/issue-tracker/IssueTrackerFeatureLayout.tsx fresh 查询完成前隐藏结论性空态标题,同时保留既有引导内容。

Sequence Diagram

sequenceDiagram
    participant UI as /issues Renderer
    participant IPC as Preload / IPC
    participant Main as MyIssuesService
    participant Store as Owner-scoped Snapshot
    participant Remote as Platform / GitHub
    UI->>IPC: 并行请求快照与 fresh 列表
    IPC->>Store: 读取上次快照
    Store-->>UI: 快照或 null
    IPC->>Main: listMyIssues()
    Main->>Remote: 查询最新 Issue
    Remote-->>Main: fresh 结果
    Main->>Store: 写入首屏快照
    Main-->>UI: 原子替换为 fresh 结果
Loading

Reviews (2): Last reviewed commit: "i18n(desktop): 菜单入口用「问题反馈」,不再暴露 Issue 术语" | Re-trigger Greptile

*/
export function writeMyIssuesSnapshot(snapshot: MyIssuesSnapshot): void {
getStore().set('snapshot', {
items: snapshot.items.slice(0, MAX_SNAPSHOT_ITEMS),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 快照静默丢失截断状态

当合并后的 issue 超过 200 条时,这里只保存前 200 条,而 snapshotAsResult 又将快照固定标记为 truncated: false,导致冷启动首屏把不完整列表展示成完整结果;本轮 fresh 查询失败后,这份列表还会持续保留且没有截断提示。

Context Used: 使用和PR描述相同的语言进行评论 (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/github-issue/myIssuesSnapshotStore.ts
Line: 158

Comment:
**快照静默丢失截断状态**

当合并后的 issue 超过 200 条时,这里只保存前 200 条,而 `snapshotAsResult` 又将快照固定标记为 `truncated: false`,导致冷启动首屏把不完整列表展示成完整结果;本轮 fresh 查询失败后,这份列表还会持续保留且没有截断提示。

**Context Used:** 使用和PR描述相同的语言进行评论 ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a50501ea43

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// 落一份已知过时的首屏镜像没有收益(下次进页面反正要查)。刻意只带 items 与身份 ——
// degraded / failed / truncated 是「这一次查得怎么样」,缓存它们会让用户进页面
// 就看到过期的错误提示。
this.persistSnapshot(result);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 避免用降级结果覆盖完整快照

当平台请求失败或 GitHub 增强搜索失败时,load() 仍会返回只包含本机账本或单一路数据的降级结果,而这里无条件将其持久化;例如已有包含 GitHub 账号 Issue 的完整快照后离线打开一次页面,就会用空列表或部分列表覆盖它,导致下次冷启动连原本可展示的旧数据也消失。这与快照“旧数据也比空白好”的用途相悖,应仅用完整结果替换快照,或在降级时保留/合并已有快照。

Useful? React with 👍 / 👎.

菜单项回答的是「点这里能干什么」,而它的邻居全是动作短语(帮助 / 检查更新 /
最新更新介绍 / 新建对话)—— zh-CN 与 ja 里 issues 是**唯一**一个孤立的英文名词,
既断了风格,也让不熟悉 GitHub 的用户不知道该不该点。

zh-CN 用「问题反馈」、ja 用「フィードバック」(ja 同级项同样全是日文,Issue 一样孤立)。
en 保持 Issues、ko 保持 이슈 —— 两者在本语言里本就是自然的入口词。

**这不是给 Issue 起中文译名**,术语裁决不变:指代该类对象的位置(页面标题、正文、
提示)仍写 Issue,因为点进去就跳 GitHub,名字必须对得上。判据与 glossary 里 ja 的
Jira 豁免同源 —— 跟用户实际看到的外部界面保持一致。已在 glossary 的 issue 条目
note 里写明这条区分,免得后来人当成漏统一。

顺带修掉 MenuButton 注释里的「议题」—— 那是被 forbidden 的旧译名。

check:i18n-glossary 通过(0 新增违规;改 glossary.json 后按提示跑了
i18n:glossary-doc 重新生成人读版)。desktop typecheck 通过、
run-unit-gate.sh GATE_EXIT=0(56 workspace PASS / 0 FAIL)。

Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com>
Signed-off-by: Dash <dashhuang@gmail.com>
@dashhuang

Copy link
Copy Markdown
Member Author

追加一条:菜单入口改用「问题反馈」

Dash 提出「菜单入口用什么文字更合适」,查证后确认这是个真问题 —— titleBar.menuItems.issues 与系统菜单 labels.issueszh-CN 与 ja 里是唯一一个孤立的英文名词:

菜单项 zh-CN(改前) ja(改前)
help 帮助 ヘルプ
releaseNotes 最新更新介绍 最新情報
checkForUpdates 检查更新 アップデートを確認
issues Issue Issue

菜单项回答的是「点这里能干什么」,邻居全是动作短语;夹一个英文名词既断风格,也让不熟悉 GitHub 的用户不知道该不该点。

改为 zh-CN「问题反馈」、ja「フィードバック」。en 保持 Issues、ko 保持 이슈 —— 两者在本语言里本就是自然的入口词(ko 的 이슈 也是 glossary 里 45:3 的主流形态)。

这不是给 Issue 起中文译名

术语裁决不变:指代该类对象的位置(页面标题、正文、提示)仍写 Issue,因为点进去就跳 GitHub,名字必须对得上。判据与 glossary 里 ja 的 Jira 豁免同源 —— 跟用户实际看到的外部界面保持一致,而不是「一律保留英文」。

已在 glossary.json 的 issue 条目 note 里写明这条区分,免得后来人当成漏统一(改 json 后按提示跑了 i18n:glossary-doc 重新生成人读版)。

顺带修掉 MenuButton.tsx 注释里的「议题」—— 那是 forbidden 里的旧译名。

验证

实机 dev 实例经 CDP 真实坐标点击打开 title bar 菜单,取到:

["帮助", "问题反馈", "检查更新"]

check:i18n-glossary 通过(0 新增违规 —— 「问题反馈」不在 forbidden 里,checkCase 本就关闭)、desktop typecheck 通过、run-unit-gate.sh GATE_EXIT=0(56 workspace PASS / 0 FAIL)。

未验证:macOS 系统菜单「帮助 → 问题反馈」的实机目检 —— 读系统菜单要把 dev 实例切到前台,会打断用户正在用的正式版实例,所以没做。那条走 main 侧 applicationMenuLabels.ts(与 renderer 是两条路径),标签值已确认为「问题反馈」。

@MagicLizi

Copy link
Copy Markdown
Contributor

@dashhuang 👋 这个 PR 还有 2 条 review conversation 没 resolve(apps/desktop/src/main/github-issue/myIssuesSnapshotStore.ts / apps/desktop/src/main/github-issue/myIssuesService.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@dashhuang
dashhuang merged commit 5f16c79 into dash/issues-enhancement-fallback Jul 31, 2026
5 of 6 checks passed
@dashhuang

Copy link
Copy Markdown
Member Author

说明:这个 "merged" 不是合进 main

按 Dash 的要求把两个 PR 合成一个。做法是把本 PR 的 base 分支(dash/issues-enhancement-fallback)快进到本 PR 的 head —— GitHub 检测到内容已全部包含,于是自动把它标成 merged。

内容进的是 #1224 的分支,不是 main。 后续都在 #1224 跟进,那里现在是 4 个 commit:

commit 内容
bea2dd85 GitHub 增强搜不到时回退 gh CLI,空列表不再谎称从未提交
a50501ea /issues 首屏读落盘快照,加载中不再谎报「没有 issue」
5f16c794 菜单入口用「问题反馈」,不再暴露 Issue 术语
a8c9d1cc 空态确证判据补上「增强真查过」,失败提示按来源分版(本轮 review 修复)

为什么不继续用 stacked

base 是非 main 分支时,CodeQL 全套(6 个 Analyze job + 汇总)整个不触发 —— 本 PR 只有 6 项 check,#1224 有 12 项,等于安全扫描完全没覆盖。这是我起 stacked PR 时没预料到的代价,合并后消掉了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants