fix(desktop): scope project groups by runtime host - #5527
Conversation
jackwener
left a comment
There was a problem hiding this comment.
Review of exact head 7f865b2868cd031fe1e184db1d2a7e03be29ec60 (17 files, +472/−94). No P0–P2 from my line. Not approving — the branch is CONFLICTING, and the only check on this SHA is label; test has not run.
A clean review is only worth anything if it says what was actually checked, so:
The id-space discipline holds everywhere I looked
This is a PR about two id spaces being confused, so I went after the places where such a change usually leaks:
- The key is collision-free.
sessionProjectIdentity/projectKeyboth useJSON.stringify([hostId, projectId]). That matters more than it looks: a separator-joined key (${hostId}:${projectId}) collides as soon as either component can contain the separator, and the failure mode would be a lifecycle mutation routed to the wrong Host. JSON escaping removes the ambiguity rather than hoping the inputs stay well-behaved. project.idis deliberately overwritten with the scoped key, and the consumers agree. The comment at the substitution says the UI treats it as an opaque action target; the check I cared about is whether everyone downstream honours that.relinkableProjectIdsis built fromscope.keyand compared againstproject.id— same space, so the capability gate actually matches rather than silently never matching, which is exactly how this kind of change usually breaks.- Unresolvable groups cannot fire actions. A session whose scope is absent lands in the
missing:bucket, and that group is pushed without aproject— sosession-history-list.tsxcomputesactionsasundefinedand renders no project actions. That is the right pairing formutateScopedProject'sif (!scope) return;: the silent no-op exists, but the UI never offers the button that would reach it. - The rail's actions all route through the scoped commands (
onNew/onRename/onArchive/onRestore/onRelink→ the*Project(projectKey)family). I looked for a call site still passing a raw project id into a key parameter and did not find one;selectLocalProjectsurvives as API but no longer has an app-shell caller.
[P3] — one encoding, two definitions
JSON.stringify([hostId, projectId]) is written out twice: session-navigation-groups.ts:30 and use-task-entry-controller.ts. I traced whether this is load-bearing and it is not — the grouping module only uses its local copy to build a lookup from observed identity to the scope.key it was handed, so the two could diverge without breaking routing. That is why this is a [P3] and not higher.
It is still worth one shared helper, for a reason specific to this PR: the whole change rests on "the key is opaque and comes from one place", and a reader who finds two definitions cannot tell from the types (string, string) which one is authoritative. The next person to add a field to the key has to find both.
Not covered
testhas not run on this SHA; I ran nothing myself — no build, no suites, no browser, no Electron. Every number in your Verification section is unverified by me.- The branch is
CONFLICTING(merge-based3292393c, 2 commits behindmainas measured at 07:41Z), so the resolution will change code this review is bound to. - I reviewed the identity and routing story. I did not review the sidebar's visual grouping, the label formatting (
名称 · profile) as a product decision, or WSL/SSH behaviour beyond reading how the scopes are built.
Automated review notice: Posted by an automated review agent (Claude Opus 5) through the shared
jackweneraccount (seat: kabi-opus). Not an independent human review.
简体中文
审查 exact head 7f865b286(17 文件 +472/−94)。我这条线无 P0–P2;不批准 —— 分支 CONFLICTING,且该 SHA 上只有 label 一个 check,test 没跑。
一份"干净"的评审只有写清查了什么才有价值,所以:
我看过的地方,id 空间的纪律都守住了(这是一个关于"两个 id 空间被混淆"的 PR,所以我专挑这类改动最容易漏的地方):
① 键不会碰撞:两处都用 JSON.stringify([hostId, projectId])。这比看上去重要 —— 用分隔符拼接(${hostId}:${projectId})在任一分量可能含该分隔符时就会碰撞,而失败形态是"生命周期操作被路由到另一个 Host"。JSON 转义是消除歧义,而不是指望输入乖巧。
② project.id 被有意换成作用域键,而下游确实遵守:我真正关心的是"所有消费者是否都按不透明处理" —— relinkableProjectIds 由 scope.key 构建,并与 project.id 比较,同一空间,所以能力开关真的能命中,而不是永远不命中(这类改动通常正是这样坏掉的)。
③ 无法解析的分组无法触发操作:作用域缺失的会话进入 missing: 桶,而该分组不带 project ⇒ UI 侧 actions 为 undefined、不渲染任何项目操作。这与 mutateScopedProject 的 if (!scope) return; 正好配套:静默 no-op 存在,但 UI 从不提供能走到它的那个按钮。
④ 侧栏的操作全部走作用域命令;我找过"仍把裸 projectId 传进 key 形参"的调用点,没有找到;selectLocalProject 作为 API 保留,但 app-shell 已无调用者。
[P3] 同一种编码写了两遍(session-navigation-groups.ts:30 与 use-task-entry-controller.ts)。我追过是否承重:不承重 —— 分组模块只用本地副本建立"观测身份 → 传入的 scope.key"的查找,两者即使分叉也不会破坏路由。**这正是它只定 P3 的原因。**但仍值得抽一个共享 helper,理由与本 PR 直接相关:整个改动立足于"键是不透明的、且只来自一个地方",而读者从类型(string、string)上分不出哪个是权威;下一个要往键里加字段的人必须同时找到两处。
未覆盖:该 SHA 上 test 没跑,我自己也什么都没跑(无构建、无套件、无浏览器、无 Electron)⇒ 你 Verification 一节的数字我一个都没验证;分支 CONFLICTING(merge-base d3292393c,07:41Z 测得落后 main 2 个提交),解冲突会改动本评审所绑的代码;我审的是身份与路由这条线,未评侧栏的视觉分组、名称 · profile 这一标签形式的产品取舍,也未超出"作用域如何构建"去审 WSL/SSH 行为。
7f865b2 to
491d59b
Compare
|
Addressed on
Revalidated with a full rebuild, 58 focused Desktop/UI tests, 112 renderer architecture checks, lint, format, all four non-incremental Desktop TypeScript checks, and |
jackwener
left a comment
There was a problem hiding this comment.
Re-review at exact head 491d59bfc3559f226bf3cf8df512787f866c1641. My [P3] is closed. No P0–P2 from my line. Not approving yet — test is still running on this SHA.
What changed since 7f865b286
The branch was rebased and squashed: base d3292393c → 0dc1142aa (picking up #5468 and #5412), branch now MERGEABLE. The author-side delta is small and I checked it rather than inferring it from the commit count — the PR's own diff went from 17 files +472/−94 to 18 files +489/−94, and the one new file is the fix.
The [P3] is closed, and closed the right way
runtimeHostProjectKey now lives alone in application/contracts/runtime-host-project-key.ts, imported by both session-navigation-groups.ts and use-task-entry-controller.ts. I checked for leftovers — git grep "JSON.stringify(\[hostId" outside that module returns nothing, so there is no third copy waiting to drift.
Two things make this better than a mechanical de-duplication. Putting it under application/contracts says what it is: a Desktop-wide identity contract, not a helper that happens to be shared — which is the thing a reader needed in order to know which definition was authoritative. And the doc comment states the invariant the key exists for ("a Project whose id is Runtime Host-local"), so the next person adding a field has the reason in front of them, not just the format.
Everything I verified in the previous pass — the collision-free encoding, relinkableProjectIds and project.id sharing one space, missing: groups carrying no project and therefore no actions, no call site passing a raw project id into a key parameter — is unchanged by this delta; I re-derived the file list rather than assuming it.
Not covered
testis still IN_PROGRESS on this SHA, so nothing here is corroborated by CI yet.- I still ran nothing myself — no build, no suites, no browser, no Electron. The figures in the Verification section remain unverified by me.
- The rebase brought in #5468 and #5412. I reviewed this branch against its new base but did not review whether those two interact with this change; a semantic conflict with either is outside what I checked.
- Unchanged from last time: I reviewed the identity and routing story, not the sidebar's visual grouping or the
name · profilelabel as a product decision.
Automated review notice: Posted by an automated review agent (Claude Opus 5) through the shared
jackweneraccount (seat: kabi-opus). Not an independent human review.
简体中文
在 exact head 491d59bfc 上复审。我那条 [P3] 已关闭;我这条线无 P0–P2;暂不批准 —— 该 SHA 上 test 仍在跑。
自 7f865b286 以来的变化:分支已变基并压缩(base d3292393c → 0dc1142aa,带入 #5468 与 #5412),现为 MERGEABLE。作者侧增量很小,我是核过的、不是从提交数推断的 —— PR 自身 diff 由 17 文件 +472/−94 变为 18 文件 +489/−94,新增的那一个文件就是修复本身。
[P3] 已关闭,而且关得对:runtimeHostProjectKey 现在独居于 application/contracts/runtime-host-project-key.ts,由分组模块与 task-entry 控制器共同引入。我查过残留 —— 该模块之外 git grep "JSON.stringify(\[hostId" 无命中,不存在第三份副本等着漂移。
有两点比"机械去重"更好:放在 application/contracts 下说明了它是什么 —— 一份 Desktop 全局的身份契约,而不是"恰好被共用的工具函数";这正是读者判断"哪个定义是权威"所缺的信息。而且文档注释写出了这个键存在的不变量("项目 id 是 Runtime Host 本地的"),下一个要加字段的人看到的是理由,不只是格式。
上一轮我验过的其余各点(编码不碰撞、relinkableProjectIds 与 project.id 同一空间、missing: 分组不带 project 因而无操作、无调用点把裸 projectId 传进 key 形参)不受本次增量影响 —— 文件清单我重新导出过,没有假设。
未覆盖:该 SHA 上 test 仍在跑,尚无 CI 佐证;我自己仍然什么都没跑(无构建/套件/浏览器/Electron),Verification 的数字仍未被我验证;变基带入了 #5468 与 #5412,我是对着新 base 审这个分支的,但没有审这两者是否与本改动相互作用;与上轮相同,我审的是身份与路由这条线,不含视觉分组与 名称 · profile 标签的产品取舍。
Generated-by: OpenAI Codex
491d59b to
611d9e7
Compare
jackwener
left a comment
There was a problem hiding this comment.
Approving at exact head 611d9e718fcaccbd92ea7f40a12067527290e38b, at @me2seeks's request, who asked not to wait for CI. test is still IN_PROGRESS on this SHA and has confirmed nothing — that waiver is his to give, and it is recorded here rather than left implicit.
The delta since 491d59bfc is two clean removals
I checked both rather than trusting the shape of the diff, because a deletion is the one change where "it compiles" and "it is correct" come apart:
deriveProjectGroupsremoved fromsession-project-grouping.ts— no references remain anywhere;deriveWorktreeSessionIdsstays in that file and its importer still resolves.SessionNavigationProjectScopedropped from the feature barrel — every consumer imports it from../ports.jsdirectly, so the barrel entry had no users.
Superseded code going away with the change that supersedes it is the right thing to do; both removals are complete rather than partial.
What this approval rests on
The identity and routing story, verified across two earlier passes and unchanged by this delta: the key is collision-free (JSON.stringify([hostId, projectId]), not separator-joined, so a component containing the separator cannot route a mutation to the wrong Host); relinkableProjectIds and project.id live in the same space, so the capability gate matches instead of silently never matching; missing: groups carry no project, so the UI never offers an action that mutateScopedProject's if (!scope) return; would swallow; and no call site passes a raw project id into a key parameter. The [P3] I raised is closed by runtimeHostProjectKey now being a single definition under application/contracts, with no second copy left.
What it does not rest on
I ran nothing — no build, no typecheck, no suites, no browser, no Electron. Every figure in the Verification section is unverified by me, and CI has not corroborated anything on this SHA. This approval is a source-level judgment about identity and routing; it is not evidence that the branch builds or that its tests pass. The rebase also brought in #5468 and #5412, whose interaction with this change I did not review.
This approval covers this SHA only. If the head moves, it stops describing the code and I will say so on the PR.
Automated review notice: Approved by an automated review agent (Claude Opus 5) through the shared
jackweneraccount (seat: kabi-opus), at the author's request and with CI explicitly waived by him. Not an independent human review, and the merge decision remains a human's.
简体中文
在 exact head 611d9e718 上批准,应 @me2seeks 要求,他明确表示不必等 CI。该 SHA 上 test 仍在跑、尚未证实任何东西 —— 这个豁免是他给的,我把它记在这里而不是默认带过。
自 491d59bfc 以来的增量是两处干净的删除(删除是"能编译"与"正确"最容易分家的改动,所以我两处都核了):
session-project-grouping.ts里移除deriveProjectGroups—— 全仓已无任何引用;deriveWorktreeSessionIds仍在该文件中,其导入方仍可解析;- 特性桶(barrel)去掉
SessionNavigationProjectScope的再导出 —— 所有使用者都直接从../ports.js导入,该条目本就无人使用。
被取代的代码随取代它的改动一起消失是对的,而且这两处删除是彻底的,不是删一半。
这条批准依托什么:身份与路由这条线(两轮已验,本次增量未触及)—— 键不会碰撞(JSON.stringify 而非分隔符拼接 ⇒ 分量含分隔符也无法把操作路由到别的 Host);relinkableProjectIds 与 project.id 同一空间 ⇒ 能力开关真能命中而非永远不命中;missing: 分组不带 project ⇒ UI 从不提供会被 if (!scope) return; 吞掉的按钮;无调用点把裸 projectId 传进 key 形参。我提的 [P3] 已由 runtimeHostProjectKey 单一定义(位于 application/contracts)关闭,无第二份副本残留。
它不依托什么:我什么都没跑 —— 无构建、无类型检查、无套件、无浏览器、无 Electron。Verification 一节的数字我一个都没验证,CI 在这个 SHA 上也没有证实任何东西。这是一条关于身份与路由的源码级判断,不构成"分支能构建"或"测试通过"的证据。变基另带入 #5468 与 #5412,它们与本改动的相互作用我没有审。
本批准只覆盖这个 SHA;head 一旦变动即不再描述代码,届时我会在 PR 上说明。
…sion-between-projects apache#5527 landed the same concern from the other side: project groups are now scoped by Runtime Host at one level, and the rail draws a row per (host, project). Drop gating is re-expressed on that model instead of beside it — the shell answers `moveDropGroupKeys` (the static rows the window guard lets a task through on) and `moveTargets(sessionId)` (where one task may go, in its own Host's raw project ids), so the rows that carry the marker and the destinations a task is offered cannot disagree. That replaces the `kind` field, the rail's own project list, and the per-session host predicate this branch had added.
Summary
Fixes #5525
Verification
npm run rebuildnpm run check:renderer-architecture— 112 passednpm run lintnpm run format:checknpm run check:staleReview focus
Project IDs are Runtime Host-local. The sidebar now carries an opaque Host-scoped Project key through grouping and actions, while IPC receives only the owning
{ profileId, hostId }.AI use
Select exactly one:
Tool(s) and scope: OpenAI Codex implemented the Host-scoped grouping and action routing, added regression coverage, and performed the pre-submission review.
Checklist
Does this PR entail a change in behavior?