fix(ui): drive the update card + consent from one atomic snapshot (stale-expectation recovery)#45
Merged
Conversation
The home screen kept two independently-refreshed sources — `status` (macStatus: local detect + adoption) and `report` (macPlanUpdate: detect + plan). A re-check refreshed only `report`, but rendering and the perform expectation read `installed` from `status` first. After an out-of-band install change (manual downgrade, Codex self-update) the card could pair a stale installed version with a fresh plan — e.g. "当前 26.608.12217 → 新版 26.608.12217" — and "立即更新" then sent a from/to/path stitched across two moments, which the backend's consent guard rightly rejected with a dead-end error the user couldn't act on. Treat the plan as a lease over one coherent snapshot: - Home.tsx: when `report` exists it is the single source for both the card and the perform expectation (installed detected together with the plan); `status` only fills in pre-check and drives the managed/external badge. `check()` now refreshes plan AND detect together (Promise.all). - errors.rs: new `AppError::StaleExpectation` (code `stale_expectation`) for the perform-time TOCTOU guards — install path moved, from-build changed, to-build changed, and a vanished install (previously a bare engine_error). It is the machine signal for "reality moved, re-check". - Home.tsx: on `stale_expectation`, auto re-check and post a NOTICE (separate `notice` state via ResultBanner, NOT `error`) so the card settles into update / up-to-date / none — instead of the `error`-driven "检查失败" hero hijacking the now-uninstalled case. - Home.tsx: a window-focus listener silently re-detects; if the install identity (build OR path, either direction) differs from the snapshot it re-checks and drops any open confirm sheet (which was built for the old target — leaving it up could run perform against an unseen snapshot and bypass the external→adopt boundary). - managerApi.ts: `errorCode()` helper to read the stable CommandError code. - WinHome.tsx: prefer `report.installed` over `status.installed` for the same reason (consistency; Windows has no stale-code path yet). - i18n: `home.stale.rechecked` in all 11 locales. Verified live (arm64): downgrade Codex out-of-band, adopt, swap it back to the latest under the manager's feet, then 立即更新 → backend returns stale_expectation → the UI auto-re-checks, shows a green info banner (not a red error), and the card correctly settles instead of stranding on a stale plan.
Wangnov
added a commit
that referenced
this pull request
Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题(用户实测截图)
手动把已装 Codex 降级到旧版后,Manager 能识别"有新版本",但点「立即更新」报错
update engine error: 已装版本已变化(确认时 build 3722,现在 build 3685):请重新检查后再试,且卡片出现「当前 26.608.12217 → 新版 26.608.12217」这种自相矛盾的显示。根因:主界面持有两份独立刷新的状态——
status(macStatus:本地检测 + 托管状态)和report(macPlanUpdate:检测 + plan)。「重新检查」只刷新report,但渲染和 perform 的 expectation 都优先从status读installed。带外安装变更后,卡片把过期的 installed 版本和新的 plan 配在一起,点更新发出的from/to/path是跨两个时刻拼凑的,被后端的同意完整性护栏正确拒绝——但用户撞上的是一个无法操作的死错误。修复:把 plan 当作「对单一快照的租约」
report存在时,它是卡片和 perform expectation 的唯一来源(installed 与 plan 同时检测);status只在首次检查前兜底、并驱动 managed/external 徽章。check()现在用Promise.all同时刷新 plan 和检测。AppError::StaleExpectation(codestale_expectation),覆盖 perform 期的 TOCTOU 护栏——路径变、from-build 变、to-build 变,以及安装消失(原先是裸 engine_error)。这是"现实已变,请重检"的机器信号。stale_expectation→ 自动重检 + 发一条 notice(独立notice状态走 ResultBanner,不复用error)。这样卡片能落到 update / 已最新 / none(Codex 被删)任一态,而不会被error驱动的「检查失败」页劫持。errorCode()读取稳定的 CommandError code。report.installed(一致性;Windows 暂无 stale-code 路径)。home.stale.rechecked11 语言。真机验证(arm64)
带外降级 Codex → adopt → 在 Manager 眼皮底下把磁盘换回最新 → 点「立即更新」→ 后端返回
stale_expectation→ UI 自动重检,显示绿色信息条(而非红色错误),卡片正确落位,不再卡在过期 plan。同时 delta 成功路径与「已是最新」态均回归正常。关系说明
本 PR 与 #43(vendor BinaryDelta)、#44(quit 确认框)独立但相关:#44 也改
mac_update.rs(quit 调用点),本 PR 改的是 expectation 校验点与detect_installed,位置不同。建议合并顺序 #43 → #44 → 本 PR,本 PR 如遇mac_update.rs冲突仅为相邻 import/函数,易解。codex review --uncommitted迭代 5 轮至无意见;cargo clippy --workspace --all-targets -- -D warnings、cargo test --workspace、npm run check、npm run test(11)全部通过。