feat(desktop): 更新重启只在有任务运行时才二次确认,并改成中断警告 - #1197
Conversation
侧栏 UpdateBanner 的重启入口原来是无条件「就地两段式」:点「立即重启」先切 确认态,再点「确认重启」才真重启。没有任何任务在跑时,第二步只有一句「应用会 自动重启」——不带信息量,纯粹多要一次点击。 改成与关窗链路(WindowControls.handleCloseClick)同一套判定:点入口先查 anySessionInTurn(),没有任务在跑就直接重启;只有真的有任务时才拦一次。探针 失败(桥不可用 / handler 未注册)按「没有任务」处理,与关窗链路一致。 因此 confirming 态的语义从「泛化二次确认」收窄为「有任务在运行,重启会打断它」 的中断警告:标题点明状态、副标题讲后果(警告色,不再有分支)、主按钮改为 「仍要重启」。退役无信息量的 confirmHint,四语言同步。 - 入口点击加 ref 防重入:探针在飞时的连点不会重复探针或重复重启 - 收起 / rail 态共用同一判定;那里没有文案位置,中断提示落在 ✓ 的 tooltip 上 - 测试随语义更名为 updateBannerRelaunchEntry.test.tsx,覆盖 busy 拦一次、非 busy 一击直达、探针抛错、连点防重入、收起态同判定 验证:pnpm test:unit、desktop typecheck、check:i18n-glossary、check:i18n 全绿。 更新链路改动已与 owner 确认;未触及 updateService.ts 与 cindy-updater。 实机:dev 下更新链路被 isDev() 短路,拿不到真实 ready 态,双模式目检未在真实 ready 态下完成;本次未新增样式与颜色,确认态仍复用既有 --warning-fg / --update-btn-* 语义 token。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
|
| Filename | Overview |
|---|---|
| apps/desktop/src/main/relaunchBusyActivity.ts | 新增六类活动来源的统一 fail-closed 聚合判定,并在 SQLite 查询后复采内存来源。 |
| apps/desktop/src/main/relaunchBusyActivityIpc.ts | 新增带受信 renderer 校验和幂等注册的忙闲查询 IPC。 |
| apps/desktop/src/main/bootstrap-electron.ts | 为手动更新重启装配 Maker、Ghost、Cindy slot、后台 Bash 与 scheduler 活动来源。 |
| apps/desktop/src/main/cindy-brain/cindySlot.ts | 为同步、异步及媒体操作增加统一的在途工作快照。 |
| apps/desktop/src/main/cindy-brain/ghostSessionActivity.ts | 为 Ghost 活动跟踪器增加跨会话忙碌查询。 |
| apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx | 重启入口改为先查询忙闲状态,并增加 fail-closed、异步结果失效及点击防重入处理。 |
| apps/desktop/src/preload/preload.ts | 向 renderer 暴露手动重启阻断活动查询接口。 |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[用户点击立即重启] --> B[renderer 请求 blocking-activity]
B --> C[main 聚合六类活动来源]
C -->|忙碌或探针失败| D[显示中断警告]
D -->|仍要重启| E[发送 update-relaunch]
D -->|取消| F[返回 ready 状态]
C -->|确认空闲| E
E --> G[main 执行更新重启]
Reviews (11): Last reviewed commit: "fix(desktop): 重启探针 IPC 改为幂等注册,防 splash 重..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6c2b63c60
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
本 PR 针对 Desktop 侧栏 UpdateBanner 的“更新后重启”入口做交互优化:只有在检测到仍有任务(turn)运行时才进入二次确认,并把该确认态收窄为“中断警告”;无任务时改为一次点击直接重启。同时同步更新四语言文案与相关单测,确保展开态与收起/rail 态入口使用同一判定逻辑。
Changes:
UpdateBanner的重启入口改为点击后先探测anySessionInTurn():busy 才进入 confirming(中断警告),否则直接relaunchToUpdate,并增加探针 in-flight 防重入。- confirming 态文案语义收窄为“仍有任务在运行中 / 重启会中断运行中的任务 / 仍要重启”,退役无信息量的
confirmHint,四语言同步更新。 - 新增/重写与更名相关测试用例,覆盖 busy / non-busy / 探针 throw / 连点防重入 / collapsed 入口一致性;并修复一条依赖旧行为的 release-notes link 用例。
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx | 重启入口改为 busy 探针后再决定“直接重启 vs 中断警告确认态”,并在探针 in-flight 期间防重入 |
| apps/desktop/src/renderer/i18n/locales/zh-CN/common.json | 更新 confirming 态文案为中断警告语义,移除 confirmHint |
| apps/desktop/src/renderer/i18n/locales/en/common.json | 同步英文 confirming 态文案为中断警告语义,移除 confirmHint |
| apps/desktop/src/renderer/i18n/locales/ja/common.json | 同步日文 confirming 态文案为中断警告语义,移除 confirmHint |
| apps/desktop/src/renderer/i18n/locales/ko/common.json | 同步韩文 confirming 态文案为中断警告语义,移除 confirmHint |
| apps/desktop/src/renderer/tests/updateBannerReleaseNotesLink.test.tsx | 适配入口点击新增 busy 探针;修正“confirming 期间隐藏公告链接”的断言方式 |
| apps/desktop/src/renderer/tests/updateBannerRelaunchEntry.test.tsx | 新增重启入口判定测试,覆盖 busy/non-busy/throw/连点防重入/收起态一致性 |
| apps/desktop/src/renderer/tests/updateBannerBusyHint.test.tsx | 退役旧测试文件(原覆盖 confirmHint/busy 分支),由新语义用例替代 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
review 指出 handleRelaunchClick 的 await 之后缺少失效保护。归族后是同一条 不变量:**一次点击的探针结论,只有在这次点击仍然有效时才能驱动副作用**。 三条对称路径在探针在飞期间都会让它失效,之前一条都没管: - 用户点「稍后再说」→ 探针 resolve 后仍会重启(点了稍后却突然重启),或 setConfirming(true) 残留,下次被火焰按钮唤回时直接落在第二步; - 已就绪补丁被 superseding 顶掉 → 仍按旧快照重启,装回旧补丁; - 组件卸载 → 仍会真的把 app 重启掉。 修法是一个点击有效性令牌(relaunchEpochRef):dismiss、卸载、status 离开 ready 都让它前进,在飞的 continuation 靠 epoch 不匹配自我作废。status 变化 额外再读一次 statusRef —— effect 打点会晚一拍,「已 setState 未跑 effect」 那段窗口里探针恰好 resolve 会读到过期的 ready,所以 continuation 直接读最新 值关掉它。两道判定针对同一不变量的不同触发路径,不是防御性冗余。 测试补齐四条对称用例(dismiss、dismiss+busy 结果的状态残留、superseding、 卸载)。每条都验证过能区分错误修法:临时移除两道判定后四条全红,恢复后全绿。 其中「dismiss + busy」初版写成了永远通过的无效断言(dismiss 后 banner 本就 return null),已改成模拟「点 X → 火焰按钮 restore 唤回」来真正暴露残留的 confirming state。 验证:pnpm test:unit(56 workspace PASS / 0 FAIL)、desktop typecheck 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9100d10fa
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:190
- 这里的注释仍沿用旧的“确认重启”表述,但 confirming 态现在语义已收窄为“仍要重启”的中断警告(仅 busy 时出现)。建议同步更新注释用词,避免和 UI/状态语义不一致。
// 直接落在「确认重启」界面(那是两步流程的第二步,越过第一步显示不合适)。
// 传入当前 status/version 让 store 记录快照,用于后续区分「同一更新 remount」
// 与「真正新更新到达」,避免导航到 /settings 再回来时误 restore。
// 同时作废在飞的探针:用户点「稍后再说」就是明确表达「现在不要重启」,几毫秒后 resolve
// 的探针结论不能反过来推翻它(否则轻则 confirming 残留、重则直接重启)。
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:79
- 这里注释把“用户又点了一次入口”也列为会让 epoch 前进的失效路径,但当前实现里
relaunchEpochRef只会在 status 离开 ready、dismiss、卸载时递增;入口重复点击是在relaunchProbeRef层面被忽略的。建议把注释改成与实际失效条件一致,避免误导后续维护。
// 一次点击的有效性令牌。探针是异步的,点击那一刻成立的前提在 resolve 时可能已经不成立:
// 用户点了右上角「稍后再说」、新版本把已就绪补丁顶成 superseding、组件被卸载,或用户
// 又点了一次入口。任一情况都让 epoch 前进,在飞的 continuation 靠 epoch 不匹配自我作废。
两条 review 反馈,都采纳: 1. 探针失败不再当作「没有任务」。上一版把 catch 兜底写成 false,论证是「与 WindowControls.handleCloseClick 同构」——那个论证只看了半个仓库。 bootstrap-electron.ts 的托盘退出路径对同一个探针是 fail closed 的,注释写得 很明确:「A failed busy probe must not turn the tray into an unguarded exit path.」重启会不可撤销地杀掉 in-flight turn,跟托盘退出同属破坏性入口,所以 跟的是这条口径,不是关窗那条更宽松的一半。「无法确认」不能当成「确认没有」。 代价只有探针真失败时多出来的一次确认;正常路径(handler 已注册、main 侧是 纯同步 snapshot)不受影响。初值也从 false 改成 true,让所有「没拿到可信答案」 的路径都落在保守那侧,而不是只补 catch 分支。 2. 确认按钮补上动作对象并改 Title Case:Restart anyway → Restart App Anyway, 仍要重启 → 仍要重启应用,ja/ko 同步。DESIGN.md §11.1 明确要求确认对话的主 按钮带宾语(好让它脱离上下文也读得通),§11.2 要求英文按钮 Title Case。上一版 在 PR 描述里把「沿用同 banner 内既有 sentence case」写成刻意偏离,但既有文案 本身就是欠的债,不构成新写的文案继续欠的理由。入口按钮(Relaunch / 立即重启) 与 cancel 属既有 key,不在本 PR 范围内动。 测试:探针 throw 的用例从「直接重启」改为「进入中断警告且不重启」,并补一条 reject 路径(此前只覆盖同步 throw)。 验证:pnpm test:unit(56 PASS / 0 FAIL)、desktop typecheck、check:i18n-glossary、 check:i18n 全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/desktop/src/renderer/i18n/locales/en/common.json:4151
- 英文确认按钮文案目前是 "Restart App Anyway"(Title Case + 插入 App),与同一块 banner 里既有的 sentence case(例如 "Newer version found"、历史上的 "Restart now")不一致,也与 confirmAria 的 "Restart anyway…" 语气不一致。建议改为 sentence case 的 "Restart anyway"(或至少保持同一风格)。
"confirmButton": "Restart App Anyway",
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:236
- 这里对 busy 探针失败走 fail-closed(hasInFlight 初值 true,catch 里也强制 true),会把“桥不可用/IPC reject”等情况当作仍有任务在跑,从而额外进入一次 confirming 警告。PR 描述中写的是与 WindowControls.handleCloseClick 同构(WindowControls.tsx:90-95 的 catch→false),即探针失败按“没有任务”直接执行一次点击重启;两者口径不一致,需确认到底以 PR 描述还是以当前实现为准,并同步对齐注释与测试期望。
let hasInFlight = true;
try {
hasInFlight = await window.electronAPI.anySessionInTurn();
} catch {
hasInFlight = true;
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fd8dd0aa1d
ℹ️ 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".
|
@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
review 指出 anySessionInTurn() 查不到后台活动,这是改成单击直达后**新增**的 无保护场景 —— 旧流程无论如何都有第二次确认,用户至少能看到一屏并中止。 channels.ts 把「turn 已结束但 CC 子进程仍在调模型」(后台子 agent、 run_in_background 的 Bash) 维护成与逻辑 turn 完全独立的状态 (SESSION_BACKGROUND_ACTIVITY),anySessionInTurn 只看 SessionTurnActivityTracker 与 live session 的 isTurnRunning,两者互不覆盖。所以父 turn 刚结束、后台子 agent 还在跑时,点一下就会经 forceQuit() 直接 process.exit(0) 把它打断或留成失联进程。 改为并行查两个来源,任一为真就进中断警告态: - anySessionInTurn() - maker.listSessionBackgroundActivity()(preload 已有,renderer 侧现成 API) Promise.all 任一 reject 仍落到 catch → fail closed,与上一版一致。 刻意未覆盖并已在注释与 PR 描述写明:scheduler「run 已启动但 turn 尚未开始」的 窄窗口。它没有 renderer 侧查询入口(main 的无人值守探针另走 SQLite hasRunningRuns),且 run 真跑起来后会产生 turn,落回第一个来源。没有把它当成 已覆盖。 未采用「复用 main 侧无人值守重启探针」的建议:那套判定含远程 controller 与 remote invoke,对无人值守是对的,但手动重启是用户主动发起 —— 有远程设备在看 会话列表并不构成「会被打断的任务」,套过来会产生误报警告。 测试补两条并验证过能区分错误修法(临时移除后台活动查询后两条全红): - 只有后台活动、无 turn → 进警告态且不重启,点确认才重启 - 后台活动查询 reject → fail closed 验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。首跑时 apps/mobile 的 startupSplashOverlay 用例 5s 超时,单独跑 733ms 通过、重跑全量 亦通过,是负载下的 flake,与本改动(desktop renderer)无关。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b13938639d
ℹ️ 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".
|
@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
|
review threads 现在是 0 unresolved、CI 全绿,但这不代表改动已经完备。上一条 review thread 是按「已回应」resolve 的(auto-review 流程的判据),不是按「已修复」: 当前实现的 busy 判定覆盖「逻辑 turn」与「Claude 后台活动」两个来源,没有覆盖 Ghost 的 card-action 后台活动( 根因不是漏了某个来源,而是「有哪些活动来源」在本仓没有单一事实源 —— 详见 PR 描述里的「 方向确定并落地前,请不要合并。 落地结果会更新到本 PR 描述与对应 thread。 |
|
本 PR 命中 UI 路径但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范。 |
UI 证据:UpdateBanner 三态 × Light / Dark(静态复现)auto-review 提示本 PR 命中 UI 路径但缺界面证据。截图这条路走不通:dev 实例被 诚实标注边界:这不是实机渲染。颜色值逐个抄自 三态里只有 ② 是本 PR 的可见变化(①③ 原样):
另附一条静态复现暴露的观察(非本 PR 引入,本 PR 不改):Light 模式下 展开:自包含 HTML(存成 .html 双击即可看,无外部依赖)<!doctype html>
<meta charset="utf-8">
<title>UpdateBanner — 更新重启入口三态 × Light / Dark</title>
<style>
/* token 值抄自 apps/desktop/src/renderer/themes/builtin/cindy-{light,dark}.ts
与 themes/colors.ts,未新增任何颜色。 */
.light {
--sidebar: hsl(0 0% 92.9%);
--sidebar-border: hsl(214.3 11.1% 87.6%);
--sidebar-muted: hsl(220 4.7% 62.2%);
--foreground: hsl(214.3 5.5% 24.9%);
--sidebar-item-hover: rgba(0,0,0,.05);
--update-btn-bg: #3C3F43;
--update-btn-border: #3C3F43;
--update-btn-text: #FCFCFC;
--warning-fg: #F3A115;
}
.dark {
--sidebar: hsl(0 2.4% 16.1%);
--sidebar-border: hsl(0 0% 26.3%);
--sidebar-muted: hsl(0 0% 43.5%);
--foreground: hsl(0 0% 83.1%);
--sidebar-item-hover: rgba(255,255,255,.09);
--update-btn-bg: #EEEEEE;
--update-btn-border: #EEEEEE;
--update-btn-text: #252222;
--warning-fg: #F3A115;
}
body { margin:0; padding:24px; font:13px/1.5 -apple-system,"SF Pro Text","PingFang SC",system-ui,sans-serif;
background:#6b6b6b; color:#fff; }
h1 { font-size:15px; font-weight:600; margin:0 0 4px; }
p.note { font-size:12px; opacity:.85; margin:0 0 20px; max-width:900px; }
.grid { display:flex; gap:20px; flex-wrap:wrap; }
.col { }
.col > h2 { font-size:12px; font-weight:600; margin:0 0 8px; opacity:.9; }
.sidebar { width:248px; background:var(--sidebar); border-radius:10px; overflow:hidden;
box-shadow:0 1px 3px rgba(0,0,0,.35); }
.filler { height:56px; }
.banner { border-top:1px solid var(--sidebar-border); position:relative;
display:flex; flex-direction:column; align-items:center; gap:10px; padding:12px 16px; }
.x { position:absolute; right:6px; top:6px; width:24px; height:24px; border-radius:999px;
display:flex; align-items:center; justify-content:center; color:var(--sidebar-muted); }
.x svg { width:14px; height:14px; }
.flame { color:var(--sidebar-muted); }
.flame svg { width:36px; height:36px; }
.title { font-size:14px; font-weight:600; color:var(--foreground); margin:0; }
.sub { font-size:12px; text-align:center; color:var(--sidebar-muted); margin:0; }
.sub.warn { color:var(--warning-fg); }
.notes-link { font-size:12px; color:var(--sidebar-muted); text-decoration:underline;
text-underline-offset:2px; }
.pill { width:100%; box-sizing:border-box; display:flex; align-items:center; justify-content:center;
gap:8px; border-radius:999px; border:1px solid var(--update-btn-border);
background:var(--update-btn-bg); color:var(--update-btn-text);
font-size:13px; font-weight:500; padding:8px 0; }
.ghost { width:100%; box-sizing:border-box; display:flex; align-items:center; justify-content:center;
border-radius:999px; padding:6px 0; font-size:13px; font-weight:500;
color:var(--sidebar-muted); }
.stack { display:flex; flex-direction:column; gap:8px; width:100%; }
.spinner { width:36px; height:36px; border-radius:999px; border:2px solid var(--sidebar-muted);
border-top-color:transparent; }
.cap { font-size:11px; opacity:.8; margin:6px 0 0; text-align:center; }
</style>
<h1>UpdateBanner — 更新重启入口三态 × Light / Dark</h1>
<p class="note">
静态复现,非实机截图:dev 实例被 <code>isDev()</code> 短路、拿不到真实 <code>ready</code> 更新态,
所以无法截到真机画面。这里的颜色全部抄自 <code>themes/builtin/cindy-{light,dark}.ts</code> 与
<code>themes/colors.ts</code> 的 token 值,布局按 <code>UpdateBanner.tsx</code> 的 class 等价还原
(图标为等价占位)。本 PR 未新增任何颜色与样式,改的是「何时进入第二态」与该态的文案。
</p>
<div class="grid" id="grid"></div>
<script>
const STATES = [
{ key:'ready', label:'① ready(入口,未变)',
title:'已更新到 1.2.3', sub:'重启以应用更新', warn:false, link:true,
primary:'立即重启', cancel:null, spinner:false,
cap:'点一下 → 探针确认没有任务在跑 → 直接重启(本 PR 的主改动)' },
{ key:'confirming', label:'② 有任务在跑(本 PR 的可见变化)',
title:'仍有任务在运行中', sub:'重启会中断运行中的任务', warn:true, link:false,
primary:'仍要重启应用', cancel:'取消', spinner:false,
cap:'只在探到有任务 / 探针拿不到可信答案时出现;焦点默认落在「取消」' },
{ key:'superseding', label:'③ superseding(未变)',
title:'检测到新版本', sub:'更新中…', warn:false, link:false,
primary:'准备中', cancel:null, spinner:true,
cap:'入口按钮 disabled,点击是 noop' },
];
const FLAME = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2s4 4.5 4 9a4 4 0 0 1-8 0c0-1.5.5-2.5.5-2.5S6 11 6 14a6 6 0 0 0 12 0c0-5-6-12-6-12z"/></svg>';
const XICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>';
let html = '';
for (const mode of ['light','dark']) {
html += `<div class="col"><h2>${mode === 'light' ? 'Light' : 'Dark'}</h2>`;
for (const s of STATES) {
html += `<div class="${mode}" style="margin-bottom:18px">
<div style="font-size:11px;opacity:.85;margin-bottom:6px">${s.label}</div>
<div class="sidebar">
<div class="filler"></div>
<div class="banner">
<div class="x">${XICON}</div>
${s.spinner ? '<div class="spinner"></div>' : `<div class="flame">${FLAME}</div>`}
<p class="title">${s.title}</p>
<div style="display:flex;flex-direction:column;align-items:center;gap:4px">
<p class="sub${s.warn ? ' warn' : ''}">${s.sub}</p>
${s.link ? '<span class="notes-link">查看更新公告</span>' : ''}
</div>
${s.cancel
? `<div class="stack"><div class="pill">${s.primary}</div><div class="ghost">${s.cancel}</div></div>`
: `<div class="pill"${s.spinner ? ' style="opacity:.7"' : ''}>${s.primary}</div>`}
</div>
</div>
<p class="cap">${s.cap}</p>
</div>`;
}
html += '</div>';
}
document.getElementById('grid').innerHTML = html;
</script> |
review 连续三轮指出同一形状的问题:又一个活动来源没被 busy 判定覆盖(逻辑 turn → Claude 后台活动 → Ghost card-action)。根因不是漏了某个来源,而是判定放在 renderer 侧逐个枚举,而「有哪些活动来源」在本仓没有单一事实源——每加一个来源就 漏一次,漏掉的后果是静默打断用户任务且不可撤销。 改成 main 侧一处判定,renderer 只问一次结论: - 新增 relaunchBusyActivity.ts:纯函数聚合三个来源,依赖全注入。三源等价无主次, 任一报忙即忙;**任一来源读取抛错也算忙**(「无法确认」不等于「确认没有」), 标签带 -probe-failed 后缀以便在日志里区分「真有活动」与「探针坏了」。不短路, reasons 完整反映现场。 - GhostSessionActivityTracker 加 anySessionBusy():它此前只有 per-session 的 isSessionBusy,而 renderer 侧 ghostSessionActivityStore 只靠 0↔1 推送累积、 没有全量快照通道,首次订阅拿不到已在跑的活动,不能当权威来源。 - bootstrap-electron 注册 update-relaunch:blocking-activity:那是本进程唯一能同时 看到 maker 与 cindy-brain 两侧跟踪器的位置。与既有的无人值守探针 (setUpdateAutoRelaunchBusyProbe) 刻意分开——后者连「有远程设备在看会话」都让路, 手动重启只该关心「这一下会打断哪些正在跑的活」,把远程 controller 纳进来只会 产生误报警告。 - UpdateBanner 改为单次查询,renderer 不再枚举来源;新增来源以后只改 main 那一处。 device-link allowlist:不登记。allowlist 顶部注释把 updater 类列为「永不放行」, 且远程控制端不会代替用户点被控端的更新重启。 刻意未覆盖并已写进注释:scheduler「run 已启动但 turn 尚未开始」的窄窗口——纳入它 要引入 SQLite 异步查询,而 run 真跑起来会产生 turn、落回第一个来源。 未触及 updateService.ts 与 cindy-updater/:本 PR 只提供判定查询,不改重启执行路径, 因此不在 docs/dev-rules/cindy-updater.md 的门禁范围内。 测试:relaunchBusyActivity 9 条(三源各一条 + 多源并发 + 三源各自 fail-closed + 一源抛错不影响其它源)、ghostSessionActivity 补 anySessionBusy 4 条(含多会话引用 计数与 TTL 兜底)。renderer 侧删掉两条已搬走职责的用例,改为只测「拿到 true 就拦、 false 才走、拿不到就保守」这一契约。 验证:pnpm test:unit 56 PASS / 0 FAIL(desktop 60.3s、mobile 11.0s)、desktop typecheck 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f746a583f
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (3)
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220
- 这里写「有三个互不相干的来源」,但 main/relaunchBusyActivity.ts 实际聚合了 5 类来源(还包含后台 Bash 与 scheduler run)。建议避免在 renderer 侧写死来源数量/清单,改为明确“由 main 侧聚合判定”。
// 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
// 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
// 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
// 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。
apps/desktop/src/preload/preload.ts:2942
- 注释里写的是「聚合三个来源」,但 main 侧实际还会把后台 Bash 与 scheduler run 计入 busy(apps/desktop/src/main/relaunchBusyActivity.ts:43-62)。建议更新注释,避免与真实行为不一致。
/**
* 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
* Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
* (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
* 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。
*/
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11
- 文件头注释仍在按「两个来源」描述 busy 判定,但现在 renderer 侧只调用 anyActivityBlockingRelaunch(),实际聚合范围已收敛到 main/relaunchBusyActivity.ts(包含 turn/Claude/Ghost/后台 Bash/scheduler run)。建议把这里的来源枚举改成指向 main 侧聚合判定,避免注释过时。
This issue also appears on line 217 of the same file.
* 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
* 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
* 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。
mode:'submit' 的图片 / 视频生成由 cindySlot.ts 的 `void runExec()` 脱离调用链执行, job 只活在 GhostCindySlot 私有的 jobs Map 里(无对外查询)。发起它的 turn 结束后, 既有五个来源全部返回空闲,而 forceQuit() 会连 Ghost Node runtime 一起 destroyAll, 正在生成的图 / 视频直接没了。 GhostCindySlot 新增 anyAsyncJobRunning(),接入聚合判定作为第六源。 同时修正上一轮对这个问题的定性。我此前把「判定不完备」上报成需要重新设计的阻塞项, 那个判断是错的:**改动前那次二次确认根本不做任何活动判定**(文案只有「应用会自动 重启」),对全部六类活动都是「点一下就 forceQuit」。所以 - 覆盖到的来源 = 净收益(从无警告变成有警告); - 没覆盖到的来源 = 与改动前行为完全一致,不构成回归。 reviewer 说「旧流程还有第二次确认,所以这是改成单击后新增的无保护场景」——那次点击 不含任何活动信息,用户不知道有任务在跑,拦不住任何人,不构成保护。 据此在 relaunchBusyActivity.ts 文件头写明:这份清单不保证完备(异步活动持有者是开放 集合),但漏掉某个来源不是回归,发现新来源就往这里加一条。重启路径本身沿用既有逻辑, 一行未改。 测试:新增「Cindy slot 异步代办在途时阻断」,fail-closed 参数化用例扩到五个同步源, 多源并发用例覆盖六源全命中。 验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 539de40e7c
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (5)
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220
- 这里的注释写「有三个互不相干的来源」,但 main 侧的
evaluateRelaunchBusyActivity已扩展为更多来源(例如后台 Bash、Cindy slot 异步 job、scheduler run 等),并且强调“新增来源只改 main 侧那一个函数”。建议此处注释不要写死来源数量/枚举,以免再次过期。
// 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
// 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
// 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
// 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:428
- 注释写 confirming 态“一律是『有任务在跑』才进来的”,但
handleRelaunchClick对探针 reject/throw 也会 fail-closed 进入 confirming(此时实际是“无法确认”)。建议把注释改成“探针判定 busy(含 fail-closed)”以匹配真实语义。
{/* confirming 态一律是「有任务在跑」才进来的,所以警告色与 busy 文案不再分支。 */}
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11
- UpdateBanner 顶部注释仍在描述「逻辑 turn + Claude 后台活动」两来源、且写了“只有真的有任务时才进入确认态”,但当前实现已改为通过
anyActivityBlockingRelaunch()走 main 侧多源聚合判定,并且探针失败也会 fail-closed 进入 confirming。建议把注释改成不枚举来源、并准确描述 fail-closed 语义,避免后续维护者按旧注释误改。
This issue also appears in the following locations of the same file:
- line 217
- line 428
* 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
* 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
* 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。
apps/desktop/src/preload/preload.ts:2941
- preload 侧
anyActivityBlockingRelaunch的注释仍写“聚合三个来源”,但 main 侧判定已扩展为更多来源。建议这里改成引用relaunchBusyActivity.ts的“多源聚合”而不是写死数量,避免注释再次过期。
* 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
* Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
* (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
* 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。
apps/desktop/src/main/bootstrap-electron.ts:3820
- 这里的注释写“四个活动来源的聚合”,但传入
registerRelaunchBusyActivityIpc的 sources 实际上已有 6 个(turn / Claude / Ghost / 后台 Bash / Cindy slot job / scheduler run)。建议去掉数量描述或改成“多源聚合”,避免注释与实现不一致。
// 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见
|
@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/main/cindy-brain/cindySlot.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
上一版加的 anyAsyncJobRunning() 只遍历异步 jobs Map,漏了同一个类里的 inflight ——那是同步代办的 per-ghost 在途计数(gen_image / gen_video 的同步等待、明确不进 会话的 oneshot_text)。插件面板发起的同步请求不一定伴随 turn 或 card-action,所以 其它探针同样不命中,首次点击就会 forceQuit() 掉正在生成的付费结果。 按 review 建议的第二种改法做:方法改名 anyInflightWork(),一次给出「所有 Cindy slot 在途工作」的统一快照(jobs 有 running 的 ∪ inflight 计数 > 0),而不是某一种。 顺带修正三处注释里的事实错误:异步提交(mode:'submit')**只对视频类开放**—— cindySlot.ts 第 449 行明确拒绝图像的 submit(「图像代办秒级完成,直接同步等待」)。 此前 cindySlot.ts、relaunchBusyActivity.ts 与提交说明都写成「异步图 / 视频生成」, 与实现不符。这个错误是写测试时被真实代码路径打回来才发现的:用 gen_image + submit 构造的用例返回 ok:false。 测试(走真实 handleModelRequest 路径,不直接摸私有 Map): - 空闲为 false - 同步代办在途期间为 true(在 generateImage 里断言),结算后回到 false - 异步视频代办 mode:'submit' 受理后为 true 验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 19edfef5e7
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (4)
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220
- 这里仍写“有任务在跑”只有三类来源,但 main 侧
relaunchBusyActivity.ts当前聚合的不止三类(还包含 scheduler run / 后台 Bash / Cindy slot 在途代办等)。建议把注释改成“来源是开放集合,统一由 main 聚合”,避免注释与实现脱节。
// 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
// 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
// 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
// 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。
apps/desktop/src/preload/preload.ts:2942
- preload 侧注释仍写“聚合三个互不相干的活动来源”,但 main/relaunchBusyActivity.ts 已扩展为 6 类来源聚合;注释与实现不一致,容易让调用方误判覆盖面。建议改成引用 main 侧聚合点(或更新为“多个来源”)。
/**
* 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
* Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
* (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
* 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11
- 这里的组件头注释仍按“逻辑 turn + Claude 后台活动(两源)”描述 busy 判定,但当前实现已改为通过
anyActivityBlockingRelaunch()走 main 侧relaunchBusyActivity.ts的聚合判定(且该文件明确列出 6 类来源)。注释与真实语义不一致,后续维护容易误导。
This issue also appears on line 217 of the same file.
* 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
* 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
* 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。
apps/desktop/src/main/bootstrap-electron.ts:3822
- 这里的注释写“四个活动来源的聚合…”,但紧接着注册的 sources 实际提供了 6 个来源(turn/Claude/Ghost/后台 Bash/Cindy slot/scheduler)。建议把数量描述改成“多个来源”或直接引用 relaunchBusyActivity.ts,避免后续再扩展时注释长期漂移。
// 手动更新重启(侧栏 UpdateBanner)的阻断判定。与上面那个**无人值守**探针刻意分开:
// 无人值守要连「有远程设备在看会话」都让路,手动重启是用户主动发起的,只该关心
// 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见
// relaunchBusyActivity.ts,handler 与 sender 断言见 relaunchBusyActivityIpc.ts;
// 这里只提供来源 —— 本进程唯一能同时看到 maker、cindy-brain 与 scheduler 三侧的位置。
review(P2)指出 deposit_media / release_media 在第 407 行就 return 了,走不到代办链 的 inflight 记账,所以寄存期间探针看不到;被 forceQuit() 打断会卡在 blob 落盘与账本 挂引用之间,留下孤儿 blob。 **没有照建议把它们计入 inflight**:那个 Map 同时是代办限流账(getInflightLimit, 超限返回「同时进行的代办已达上限」)。把面板里粘贴图 / 删素材算进去,会让这些操作 在限流触顶时被拒 —— 那是用户可见的行为变更,不该由本改动附带。改用独立的 mediaOps 计数,只服务于「重启会打断什么」的判定,限流语义一行未动。 释放放在 finally,抛错路径同样归零 —— 计数泄漏会让重启入口从此永久卡在「有任务在 跑」,比漏计更糟。 另外补一处上一个 commit 的不实之处:19edfef5 的说明写了「修正三处注释里的事实错误」, 但那次用来批量替换的脚本对 cindySlot.ts 那段没有加断言、静默没匹配上,实际只改到了 relaunchBusyActivity.ts。这次把 cindySlot.ts 的注释一并补齐(jobs = 异步**视频**代办, inflight = 同步代办,mediaOps = 寄存),三处状态各自的边界都写清。 测试(走真实 handleModelRequest,不摸私有字段): - 寄存在途期间为 true(在 depositMedia 内部断言),结算后回到 false - 寄存抛错也归零(验证 finally) 两条都验证过能区分错误修法:移除 finally 后各自捕获到计数泄漏。 验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34c57a6924
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (7)
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220
- 这里仍把阻断来源写成“三个来源”,但
anyActivityBlockingRelaunch()的判定已在 main 侧聚合更多来源(例如 scheduler / Cindy slot / 后台 Bash 等)。建议避免在 renderer 侧注释里枚举来源,改为指向 main 侧单一事实源,防止来源清单继续漂移。
// 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
// 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
// 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
// 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11
- 这里的头部注释仍描述为“两个来源都要看”,但实际实现已经改为调用 main 侧的聚合探针
anyActivityBlockingRelaunch()(聚合多个活动来源,见relaunchBusyActivity.ts)。注释应与当前行为一致,避免后续维护时按旧语义误改。
This issue also appears on line 217 of the same file.
* 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
* 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
* 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。
apps/desktop/src/preload/preload.ts:2942
- preload 侧对
anyActivityBlockingRelaunch的注释仍写“三个活动来源”,但 main 侧聚合判定当前包含更多来源(见relaunchBusyActivity.ts)。建议把注释改成“聚合多个来源”并指向单一事实源,避免文档与实现漂移。
/**
* 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
* Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
* (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
* 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。
*/
apps/desktop/src/main/bootstrap-electron.ts:3822
- 这里的注释写“四个活动来源的聚合”,但
registerRelaunchBusyActivityIpc传入的 sources 实际包含 6 类来源(turn / Claude 后台 / Ghost / 后台 Bash / Cindy slot / scheduler)。建议更新注释,避免后续读代码时误以为漏了一些来源。
// 手动更新重启(侧栏 UpdateBanner)的阻断判定。与上面那个**无人值守**探针刻意分开:
// 无人值守要连「有远程设备在看会话」都让路,手动重启是用户主动发起的,只该关心
// 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见
// relaunchBusyActivity.ts,handler 与 sender 断言见 relaunchBusyActivityIpc.ts;
// 这里只提供来源 —— 本进程唯一能同时看到 maker、cindy-brain 与 scheduler 三侧的位置。
apps/desktop/src/main/bootstrap-electron.ts:3836
- 这里把 Cindy slot 的异步
mode:'submit'注释为“图 / 视频”,但GhostCindySlot.handleModelRequest会拒绝非视频类的mode:'submit'(仅视频支持异步提交)。建议修正注释,避免误导后续把图片也当作异步来源之一。
// Cindy slot 的全部在途工作:异步(mode:'submit' 的图 / 视频)与同步代办各自独立记账,
// 都可能不伴随任何 turn 或 card-action,只查一半就漏一半。
apps/desktop/src/main/relaunchBusyActivity.ts:69
anyCindySlotJobRunning这个来源名/标签与实际语义不一致:调用方传入的是getGhostCindySlot().anyInflightWork()(包含同步 inflight、mediaOps 等),但在聚合判定里标记为cindy-slot-async-job,会让日志 reasons 看起来像“只有异步 job 在跑”。建议把来源字段与 reasons 标签改成更贴近语义的命名(例如anyCindySlotInflightWork/cindy-slot-inflight-work),并同步更新调用方与测试断言。
* 是否有任意 Cindy slot 在途代办(异步 jobs + 同步 inflight 两半都算)。
* **必须单独查**:两半各自独立记账、都可能不伴随 turn 或 card-action,而 forceQuit() 会连
* Ghost Node runtime 一起销毁 —— 正在生成的付费结果直接丢掉。
*/
anyCindySlotJobRunning: () => boolean;
/**
apps/desktop/src/main/tests/relaunchBusyActivity.test.ts:110
- 这里的测试注释写“mode:'submit' 的图片 / 视频生成”,但实现里
mode:'submit'仅支持视频类(见cindySlot.ts对info.category !== 'video'的拒绝)。建议改成“视频生成”,避免测试注释与实现语义冲突。
// Cindy slot 异步代办(mode:'submit' 的图片 / 视频生成):void runExec() 脱链执行,只记在
// GhostCindySlot 私有 jobs Map,发起 turn 结束后其它来源全看不到。
review 指出的问题成立,而且后果比 P2 的定级更重。 调用点 registerMakerIpcsAfterSplash 里,本 handler 注册在第 3826 行,而 makerIpcsRegistered = true 在第 3957 行;中间的 getMakerCore() 与 waitForInitialCustomMcpRefresh() 都可能抛,那个 try 的 catch 明写「不阻塞启动 —— 下次 splash retry 再尝试」。重试时 flag 仍是 false,早退不生效,于是这行被执行 第二次,ipcMain.handle 对同 channel 抛「Attempted to register a second handler」。 关键在于异常会从这里穿出去:不只是本 handler 注册失败,**排在它后面的全部 maker IPC 注册都被一起掀掉**,而且每次重试都卡在同一行 —— 结果是 maker 链路永久不可用。 修法用注册函数内幂等(先 removeHandler 再 handle),而不是把调用挪到成功之后:它对 调用位置不敏感,以后调用点顺序再变也不会重新踩坑。 测试:边界测试的 electron mock 改成仿真真实行为(同 channel 第二次 handle 直接抛) —— 原来那个只 set 的假 Map 根本测不出重复注册。新增用例「重复注册不抛错且 handler 仍可用」,验证过能区分错误修法:移除 removeHandler 后该用例精确失败。 验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 471c6e1f95
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (7)
apps/desktop/src/main/relaunchBusyActivity.ts:106
probe('cindy-slot-async-job', ...)的 reason label 会在日志/诊断里呈现,但当前来源实际覆盖的是 Cindy slot 的“全部在途工作”(异步 jobs + 同步 inflight + mediaOps),不只是 async job。建议把 label 改成更准确的名称(例如cindy-slot-inflight-work),并同步更新相关测试期望。
probe('cindy-slot-async-job', () => sources.anyCindySlotJobRunning());
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:221
- 这里的注释仍把 busy 来源写成“3 个来源”,但 main 侧的聚合探针已包含更多来源(例如后台 Bash / Cindy slot 在途 / scheduler run)。建议要么列全,要么改成“不在 renderer 枚举来源,由 main 侧聚合”为准,避免注释过期。
// 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
// 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
// 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
// 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。
// 探针失败 = **无法确认**,不等于「没有任务」。重启会杀掉 in-flight turn,属于不可撤销的
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11
- 文件头注释里仍写“两个来源都要看”(逻辑 turn + Claude 后台活动),但实际阻断判定已经收敛到 main 侧并聚合了更多来源(如 Ghost card-action / 后台 Bash / Cindy slot 在途 / scheduler run)。这里的描述会误导后续维护。
* 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
* 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
* 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:244
relaunchProbeRef作为防重入标记只有在 Promise settle 的finally才会复位;如果 IPC handler 因某个来源(例如 scheduler 查询)挂起导致anyActivityBlockingRelaunch()永不 settle,这个入口会被永久锁死(后续点击全部 return)。建议为探针加一个超时兜底(超时按 busy/fail-closed 处理),确保 ref 一定能复位。
const handleRelaunchClick = async (): Promise<void> => {
if (relaunchProbeRef.current) return;
relaunchProbeRef.current = true;
const epoch = relaunchEpochRef.current;
// 初值取 true:探针没给出可信答案的任何路径(reject、桥同步 throw)都落在保守的那一侧。
let hasInFlight = true;
try {
hasInFlight = await window.electronAPI.anyActivityBlockingRelaunch();
} catch {
hasInFlight = true;
} finally {
relaunchProbeRef.current = false;
}
apps/desktop/src/preload/preload.ts:2942
- preload 侧注释仍写“三个活动来源聚合”,但 main 侧的阻断探针已经扩展为聚合更多来源(后台 Bash / Cindy slot 在途 / scheduler run 等)。建议同步更新注释,避免误导。
/**
* 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
* Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
* (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
* 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。
apps/desktop/src/main/bootstrap-electron.ts:3822
- 这里的注释写“四个活动来源的聚合”,但
registerRelaunchBusyActivityIpc实际注入了 6 个来源(含后台 Bash / Cindy slot / scheduler)。建议把数字去掉或改成准确的数量,避免注释过期。
// 手动更新重启(侧栏 UpdateBanner)的阻断判定。与上面那个**无人值守**探针刻意分开:
// 无人值守要连「有远程设备在看会话」都让路,手动重启是用户主动发起的,只该关心
// 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见
// relaunchBusyActivity.ts,handler 与 sender 断言见 relaunchBusyActivityIpc.ts;
// 这里只提供来源 —— 本进程唯一能同时看到 maker、cindy-brain 与 scheduler 三侧的位置。
apps/desktop/src/main/relaunchBusyActivity.ts:68
anyCindySlotJobRunning的命名与注释/实际语义不一致:注释明确这是“异步 jobs + 同步 inflight 两半都算”(还包括 mediaOps),并非仅“job running”。命名不准会误导调用方与日志诊断。建议重命名为更贴近语义的名称(例如anyCindySlotWorkInFlight),并同步调整注入方与测试。
This issue also appears on line 106 of the same file.
* 是否有任意 Cindy slot 在途代办(异步 jobs + 同步 inflight 两半都算)。
* **必须单独查**:两半各自独立记账、都可能不伴随 turn 或 card-action,而 forceQuit() 会连
* Ghost Node runtime 一起销毁 —— 正在生成的付费结果直接丢掉。
*/
anyCindySlotJobRunning: () => boolean;
MagicLizi
left a comment
There was a problem hiding this comment.
Review passed — well-architected relaunch detection with fail-closed semantics at every level, epoch-based probe invalidation, proper IPC sender verification, and comprehensive test coverage for all 6 activity sources.
|
六路活动探测加上 epoch 失效机制,既不放过正在跑的任务,也不拿假阳性烦用户——fail-closed 到位,已合入。 |
这次改了什么
摘要
应用更新的重启入口原来要点两次:点侧栏 banner 的「立即重启」→ banner 就地切成确认态 →
再点「确认重启」才真重启。问题是没有任何任务在跑时,第二步只写着「应用会自动重启」 ——
不带任何信息量,纯粹多要一次点击。
现在点入口先查
anySessionInTurn():(「仍有任务在运行中」)、副标题讲后果(警告色,「重启会中断运行中的任务」)、主按钮
「仍要重启应用」,取消仍在下方次级位。
探针失败刻意 fail closed:重启会不可撤销地杀掉 in-flight turn,「无法确认」不能当成
「确认没有」。这条口径跟的是 main 侧托盘退出路径 ——
bootstrap-electron.ts的hasActiveTurn对同一个探针catch → true,注释写明「A failed busy probe must not turnthe tray into an unguarded exit path.」。(renderer 侧
WindowControls.handleCloseClick的
catch → false是既有行为,本 PR 不动它;要统一应作为同时覆盖关窗与重启的独立改动。)变更类型
feat新功能fix缺陷修复refactor/perf重构或性能优化docs/test/chore文档、测试或工程维护范围
UpdateBanner重启入口改为「先查 busy 探针,再决定直接重启 vs 进中断警告态」,展开态与收起 / rail 态共用同一判定;
confirming态语义收窄为「有任务在运行,重启会打断它」,警告色与文案不再有分支;confirmHint,四语言(zh-CN / en / ja / ko)同步改写确认态文案;apps/desktop/src/main/updateService.ts、apps/desktop/cindy-updater/(Tauri)或重启命令本身 —— 纯 renderer 交互层 + i18n 文案;
anySessionInTurn()的 busy 判定口径(沿用sessionTurnActivityTracker既有语义);「仍有任务在运行中 / 重启会中断运行中的任务 / 仍要重启」。
UI 变化
不涉及新增视觉元素与样式:布局、图标、按钮形态、间距、动效全部沿用既有 banner 的
ready / confirming 两态结构,本次改的是进入 confirming 的条件与其中的文案,以及
把原先按 busy 分支的警告色改为常量(confirming 态一定是 busy 才进来的)。
DESIGN.md§10「Light / Dark Dual-Mode Delivery Gate」——颜色一律走语义 token:警告文案沿用
--warning-fg,按钮沿用--update-btn-bg/--update-btn-border/--update-btn-text/--update-btn-hover,取消沿用text-sidebar-muted+hover:bg-sidebar-item-hover。本次没有新增任何硬编码颜色,也没有只适配单一模式的条件补丁。双模式实机目检未完成,原因见「未执行的验证」,按该节「Verification is
best-effort and must be reported honestly」如实登记,不把「复用了 themed token」
当成「双模式已验证」。
DESIGN.md§11.1「Actions = verb + object, never a bare verb」+ §11.2 英文按钮Title Case——主按钮从「确认重启」改为「仍要重启应用」/
Restart App Anyway(ja「それでもアプリを再起動」、ko「그래도 앱 재시작」),带上动作对象、脱离上下文也读得通,
没有使用被禁的裸「确定 / 确认 / OK」;副标题按「what happened + what to do」的精神写成
「状态 + 后果」,让用户能判断该不该现在重启。
DESIGN.md§11.3「Self-Check (when touching copy)」——四份common.json全部更新,按各语言标点惯例书写(zh-CN 无句末句号、ja/ko 沿用各自惯用表达,未自造译法)。
i18n/GLOSSARY.mdRunning 条目——zh-CN 用裁决译法「运行中」。初版写的「进行中」是该条目下的条件禁用译法,已被
pnpm check:i18n-glossary拦下并改正。Relaunch/ 「立即重启」)与cancel是本 PR 未改语义的既有 key,动它们会连带
tooltipReady/ariaCollapsed/ariaExpanded一串既有文案,属于「整块 banner 文案对齐规范」的独立改动;
confirmTitle(A task is still running)是状态陈述句而非标签,与同 banner 的
Newer version found同形,保持 sentence case。UI 证据:截图这条路走不通(dev 被
isDev()短路、拿不到真实ready态),已按 auto-review提示的另一条路附上自包含 HTML 静态复现 —— 三态 × Light / Dark,见
UI 证据评论。
那不是实机渲染:颜色抄自主题 token 文件、布局按 class 等价还原,边界已在评论里标注。
评论里另记了一条静态复现暴露的既有问题(Light 下
--warning-fg对比度约 1.8:1,非本 PR引入、未被本 PR 加重,按 §10 留给设计裁决,本 PR 不自行改色)。
不变量清单(给 reviewer 的锚点)
入口判定只有两条不变量,全部 UI 分支与两个入口(展开态 / 收起 rail 态)都复用同一份实现:
只有「确认没有任务」才允许直接执行。 判定在
handleRelaunchClick一处完成,但「有任务」有两个独立来源,必须都查(并行,互不依赖):
anySessionInTurn()SessionTurnActivityTracker+ live session 的isTurnRunning)maker.listSessionBackgroundActivity()run_in_background的 Bash。channels.ts把它维护成与 turn 完全独立的状态forceQuit()直接打断或留成失联进程hasInFlight初值与catch都取true,所以任何拿不到可信答案的路径(任一查询reject、桥同步 throw)都落在保守侧 —— 破坏性入口上「无法确认」不能当成「确认没有」,
口径同
bootstrap-electron.ts托盘退出的hasActiveTurn。刻意未覆盖(不假装全覆盖):scheduler「run 已启动但 turn 尚未开始」的窄窗口。
renderer 侧没有该查询入口(main 的无人值守探针另走 SQLite
hasRunningRuns),且 run真跑起来后会产生 turn、落回第一个来源。
一次点击的探针结论,只有在这次点击仍然有效时才能驱动副作用。 失效路径全枚举:
handleDismiss→relaunchEpochRef前进confirming残留,下次被火焰按钮唤回时直接落在第二步status离开ready(superseding / error / idle)statuseffect → epoch 前进 + continuation 直接读statusRefrelaunchProbeRef防重入status那一行有两道判定不是冗余:effect 打点会晚一拍,「已 setState 但 effect 未执行」的区间里探针恰好 resolve 会读到过期的
ready,所以 continuation 额外直接读最新值。四条失效路径各有一条测试用例,两个 busy 来源各有一条,且都验证过能区分错误修法 —— 临时
移除对应判定后相关用例全红,恢复后全绿。
已知债:仓内「什么算 busy」有五处不同定义
review 过程中三轮都在同一个主题上(判定收窄后的覆盖面),根因是这个语义在仓里没有单一
事实源。按当前 HEAD 盘一遍,供后续 reviewer 与后续改动参考:
WindowControls.handleCloseClick(renderer 关窗)quitFromWindowsTray的hasActiveTurn(main 托盘退出)setUpdateAutoRelaunchBusyProbe(main 无人值守自动重启)SESSION_IN_TURN(per-session,控制端 stall 看门狗)本 PR 不统一它们:关窗链路与 main 侧无人值守探针分别属于既有行为和
docs/dev-rules/cindy-updater.md门禁管辖范围,合并需要独立 PR 并与维护者确认。这里只保证新入口自身的覆盖面是完整且保守的。也刻意没有把无人值守那套直接套用到手动入口 ——
它含远程 controller / remote invoke,对无人值守是对的,但用户主动点重启时「有远程设备在看
会话列表」不构成「会被打断的任务」,套过来只会产生误报警告。
判定收敛:三个活动来源合并到 main 侧一处
review 连续三轮指出同一形状的问题:又一个活动来源没被覆盖(逻辑 turn → Claude 后台活动 →
Ghost card-action)。根因不是漏了某个来源,而是判定放在 renderer 侧逐个枚举,而「有哪些活动
来源」在本仓没有单一事实源。已改成 main 侧一处判定(
main/relaunchBusyActivity.ts),renderer 只问一次结论 —— 新增活动来源以后只改那一个函数。
SessionTurnActivityTracker+ live sessionisTurnRunning()listActiveClaudeBackgroundActivitySessions()GhostSessionActivityTracker.anySessionBusy()(本 PR 新增)run_in_background,不调模型、不折算 running)listBackgroundTasks()mode:'submit'的图 / 视频生成,脱链执行)GhostCindySlot.anyAsyncJobRunning()(本 PR 新增)readUpdateRelaunchScheduleBusy()(SQLite,异步)getUpdateRelaunchControllers()/hasInFlightRemoteInvokes()范围边界(给 reviewer 的明确口径)
基线是「无条件重启」。 改动前用户点「立即重启」要点两次,但那次二次确认不做任何活动
判定 —— 文案只有「确认重启更新?/ 应用会自动重启」,全程不提任何正在跑的东西。用户不知道
有任务在途,点下去照样
forceQuit()。所以「旧流程还有第二次确认」不构成保护,把一次没有信息量的点击算作安全层,是把用户的无知当成机制。
本 PR 的目标是挡住影响最大的那一类,不做穷尽枚举。 已覆盖的六个来源都指向同一类事故:
用户能感知的、正在产出结果的工作被打断(agent turn、后台 subagent、后台 Bash、Ghost
card-action、生成图 / 视频的代办、定时任务)。仓里的异步持有者是开放集合(插件网络请求、
语音、媒体导入、plugin market 下载、device-link 在途 invoke……),继续逐个纳入的边际收益
递减、误报成本递增 —— 而误报也是产品损伤:判定越宽,被拦的比例越高,就越接近本 PR 要
消除的那个「几乎总是要确认两次」。每一条未覆盖来源的基线都是「改动前同样被杀」,因此
不构成回归、不阻塞本 PR。
真要做到「重启不打断任何在途工作」,正确形态是让手动重启走优雅退出链、由各活动持有者自己
收尾 —— 那是一个独立的、需要动更新链路的改动,不该由这个交互优化附带。
这份清单不保证完备,但漏掉某个来源不构成回归。 仓里的异步活动持有者是开放集合,每个模块
各自在私有结构里管在途状态,没有统一注册处。关键是:改动前那次二次确认根本不做任何活动
判定(文案只有「应用会自动重启」),对所有来源都是「点一下就
forceQuit()」。所以覆盖到的来源是净收益(从无警告被杀变成明确警告),没覆盖到的与改动前行为完全一致。重启路径本身沿用
既有逻辑、一行未改;发现新来源就往
relaunchBusyActivity.ts加一条。ghostSessionActivityStore只靠ghosts:session-activity推送累积集合,preload 里没有 list / 快照 IPC(Claude 后台活动有),而推送只送 0↔1 转变、
store 又是「首次被消费时才挂订阅」—— banner 挂载时若某会话早已在跑 card-action,renderer
根本看不到。所以「在 renderer 补第三个查询」这条路做不对。
pre-run hook 阶段明确不创建 session(
script-runner.ts的'script execution does not support worktrees or bound sessions'),所以必须单独查。判定因此变成 async:先读内存源,都空闲才查 SQLite,拿到结果后复采一次内存源关掉查库期间的窗口。
setUpdateAutoRelaunchBusyProbe连「有远程设备在看会话列表」都让路)。手动重启是用户主动发起,远程 viewer 不构成「会被
打断的任务」,纳进来只会产生误报警告 —— 反而回到本 PR 要消除的那种无信息量确认。
try/catch(抛错记为<source>-probe-failed并算忙),renderer 再兜住整条 IPC 失败。未触及更新链路:
updateService.ts与cindy-updater/一行未碰 —— 本 PR 只提供判定查询,不改下载 / 校验 / 替换 / 重启执行路径,因此不在
docs/dev-rules/cindy-updater.md的门禁范围内。远程与手机版适配结论(
remote-and-mobile-adaptation.md门禁三选一)Ghost 活动),不碰 workdir 文件,SSH 远程工作区下语义不变。
update-relaunch:blocking-activity:不登记 device-link allowlist。依据是
packages/device-link/src/allowlist.ts顶部注释把「updater / release-notes」列为永不放行类别;且远程控制端不会代替用户点被控端的更新重启(更新是本机行为)。不进表即天然不可
远程调用,符合该文件的默认拒绝制。
怎么验证的
自动验证
第二轮(c9100d10,review 反馈修复后)复跑:
第三轮(fd8dd0aa,fail-closed + 按钮文案)复跑:
第四轮(b1393863,busy 判定补后台活动)复跑:
首跑时
apps/mobile的startupSplashOverlay有一条用例 5s 超时;单独跑 733ms 通过、重跑全量亦通过,是负载下的 flake,与本改动无关。
第五轮(f27cdfad,判定收到 main 侧一处 + 覆盖 Ghost)复跑:
新增用例:
relaunchBusyActivity9 条(三源各一条 + 多源并发 + 三源各自 fail-closed + 一源抛错不影响其它源)、
ghostSessionActivity的anySessionBusy4 条(含多会话引用计数与 TTL兜底)。renderer 侧删掉两条职责已搬走的用例,改为只测「拿到 true 就拦、false 才走、拿不到
就保守」这一契约。
针对性用例(
apps/desktop/src/renderer/__tests__/updateBannerRelaunchEntry.test.tsx,由
updateBannerBusyHint.test.tsx更名重写):relaunchToUpdate另修
updateBannerReleaseNotesLink.test.tsx:其中「确认态隐藏公告文字链」一条依赖「点入口必然进确认态」,现改为把探针 mock 成 busy 后再断言,用例原意不变;该文件同时补上了
window.electronAPI桩(入口现在会调 IPC)。手工验证
不涉及 —— 见下。
未执行的验证
isDev()短路(apps/desktop/src/main/updateService.ts的
updateChannelKind()/ relaunch 策略),开发实例拿不到真实ready态,走不完「ready → 点入口 → 探针 → 重启 / 中断警告」的真流程,也无法验证真实重启动作。
完整渲染并逐态检查过(胶囊反相中性在两侧都可读、警告色可辨、布局无挤压),但它用的是抄过去
的 token 值而非真实渲染管线,不能替代实机目检;真实
ready态下的观感仍属未验证。按DESIGN.md§10「Verification is best-effort and must be reported honestly」如实登记到这个粒度,不把静态复现说成实机已验证。
pnpm test:all:改动局限于 renderer 单组件 + i18n 文案,按风险选择test:unit作为门禁,其余交给 CI。
风险
风险分类
docs/dev-rules/cindy-updater.md管辖范围的交互层)影响与回滚
UpdateBanner的重启入口交互与四语言确认态文案。更新器改动已与 owner 确认(按
docs/dev-rules/cindy-updater.md的核心门禁),且本次未触及
updateService.ts与cindy-updater(Tauri),不改变下载、校验、替换、重启命令本身的任何行为,Windows / macOS 的安装—替换—重启路径不受影响。
banner、需要明确点击,且这与「点窗口 X 直接关掉应用」的既有风险等级一致(关窗同样
会中断 turn,也同样只在有 in-flight turn 时才确认)。真正需要保护的场景(有任务在跑)
反而比改动前更明确 —— 从一句中性的「应用会自动重启」变成写明会中断任务。
git revert即可完全恢复原「无条件两段式」行为;没有持久化状态、没有 schema 与协议变更,回滚无残留。
提交前检查
git commit -s,见 DCO)