Repository navigation
perf(web-shell): derive the session workflow projection once and share it across surfaces (#10865) - #11237
Conversation
…er (QwenLM#10865) The cockpit, the workflow inspector and the graph embedded in the cockpit each derived their own copy of the session workflow projection for a single render. The app now builds one projection per render and hands the same object to every surface; each surface keeps a raw-props fallback so standalone mounts (ToolApproval, TasksStatusMessage, isolated tests) still work unchanged. To make the sharing possible without a circular import, the task-execution lookups PlanExecutionView owned (the index, its walkers, the node-state and active-agent derivations) move to a shared taskExecutionIndex module that both the projection and the graph depend on. The projection now carries its task index and the unassigned-tools bucket, so the embedded graph reads grouping, node states, counts and dependents from the shared object instead of recomputing them, and its dependents map comes from the projection's own derivation rather than a third copy of the blockedBy walk. Pinned by tests: a mount of cockpit + inspector + embedded graph with the shared projection derives nothing extra (spy on buildSessionWorkflowProjection and createTaskExecutionIndex); a standalone cockpit tree or inspector derives exactly one projection and one index; hovering a node re-renders without re-running layerPlanTodos or the topology serialization; a same-frame resize storm coalesces to one measure per animation frame. Fixes QwenLM#10865
Verification report — built and run locally against a real daemonI rebuilt this PR in an isolated worktree and drove it through a real Short answer: the change delivers what it says and is behaviourally invisible — but every one of the 11 open review findings reproduces, and the branch no longer merges cleanly. Rig — how the numbers below were produced
1. The perf claim holds, and the UI is unchanged
With the cockpit, its embedded graph and the inspector all mounted, a transcript update derives the projection 2× instead of 4× and builds the task index 2× instead of 6×, at identical render counts. Hover was already clean at the merge base (#10871) and stays clean. The two cockpits render the same values, so this is invisible to the user, as intended. Two smaller measurements worth recording:
2. Repo gates
The single failure is 3. The 11 open review findings — all reproduceEach mutant was applied to the PR head and the suites that claim to cover the behaviour were run (
One correction to the review. The dead The other ten are real gaps in the new suites. The two I would want closed before merge are the App-level pass-down (the headline contract of the PR, currently asserted nowhere — an accidental revert of two lines in 4. The branch no longer merges
Everything above was measured on 5. Not coveredmacOS only (no Windows/Linux run); web-shell package suites only (no full-repo run, no Playwright e2e/visual suites); the post-rebase tree was not exercised. One thing a reviewer reproducing this will hit: after a full page reload the workflow surfaces show "This session has no structured workflow yet", because the replayed transcript does not carry VerdictThe perf work is correct, measurable in the real app, and user-invisible. I am happy to merge it once (a) it is rebased on 中文说明验证报告 —— 本地真实环境实测我在隔离 worktree 里重建了本 PR,并连同它的 merge base 一起,接到真实 结论:改动确实做到了它声称的事,且对用户不可见;但 11 条未解决的评审意见全部复现,并且分支已经无法干净合并。 装置
1. 性能收益成立,界面无变化
cockpit、内嵌图与 inspector 同时挂载时,一次 transcript 更新的 projection 推导从 4 次降到 2 次、task 索引构建从 6 次降到 2 次,而渲染次数完全一致。hover 在 merge base 上就已经干净(#10871),本 PR 保持干净。两臂 cockpit 读数一致,对用户不可见 —— 正是预期。 另外两个值得记录的测量:
2. 仓库门禁
唯一失败是 3. 11 条评审意见 —— 全部复现每个变异体都打在 PR head 上,然后跑声称覆盖该行为的套件(
对评审的一处更正。 死掉的 其余十条都是新套件的真实缺口。合并前我希望先补上的是两条:App 层的传递(这是本 PR 的核心契约,目前无处断言 —— 误删 4. 分支已无法干净合并
以上所有测量都是在提交时的 5. 未覆盖仅 macOS(未跑 Windows/Linux);仅 web-shell 包的套件(未跑全仓,未跑 Playwright e2e / 视觉套件);未验证 rebase 之后的树。另外,复现时会遇到一个现象:整页刷新后 workflow 面板显示 "This session has no structured workflow yet",因为回放的 transcript 不带 结论这项性能工作是正确的、在真实应用里可测量、且对用户不可见。满足两点后我乐意合并:(a) 基于 |
|
@qwen-code /resolve |
Resolve the PlanExecutionView.tsx conflict between the shared session-workflow projection refactor and main's dependency-navigation/gating work: derive stepNumberByTodo in the surviving graph-local memo, and restore the toolFormatting/toolClassification imports main's new node-face and subagent-details gating code needs.
|
Qwen Code resolved the merge conflicts and pushed the branch update. Root cause. Since the PR last merged main (base Semantic, not adjacent. Both sides changed the same memo. The resolution keeps the PR's projection structure and re-adds main's step-number derivation to the surviving graph-local memo: const { stepNumberByTodo, topology, … } = useMemo(() => {
const stepNumberByTodo = new Map(
todos.map((todo, index) => [todo.id, index + 1]),
);
const topology = …; // unchanged PR code
return { stepNumberByTodo, topology, … };
}, [projection, todos]);Imports: kept the PR's Load-bearing.
Not verified. No build/typecheck/tests run. Two non-conflicted dependencies for CI: (1) main's 中文说明根因:自 PR 上次合并 main(基点 语义冲突:解决保留 PR 的 projection 结构,把 main 的编号派生放回幸存的图形局部 memo(见上方代码)。导入同时保留两侧:PR 的 关键约束: 未验证:未运行构建/类型检查/测试。两处交由 CI:① main 的 |
Verification report, round 2 — the merged head
|
| Scenario | Surface renders cockpit / inspector / graph |
base e5969d6 |
head 6c44841 |
|---|---|---|---|
| Enter the cockpit — cockpit, its embedded graph and the inspector all mounted (3 repetitions per arm) | 2 / 2 / 4 — identical in both arms | 4 projections / 6 index builds | 2 / 2 |
| Workflow inspector alone | 0 / 4 / 0 | 2 / 2 | 2 / 2 |
| Hover storm, 10 pointer events over graph nodes | 0 / 0 / 2 | 0 / 0 | 0 / 0 |
| Session page load with no workflow surface mounted (3 repetitions) | 0 / 0 / 0 | 0 / 0 | 13 / 13 |
With all three surfaces mounted, the merged head derives the projection 2× instead of 4× and builds the task index 2× instead of 6×, at identical render counts — the same result my first report measured before the merge. Hover was already clean at the base and stays clean.
And it is invisible to the user. The cockpit's rendered text is the same on both arms — 1080 characters, checksum 1442004915 — including main's step numbers 1…8 and every "Depends on" chip. The two screenshots above are the same session, side by side.
The one real cost is unchanged and still small: the App-level memo runs on every todos/tools/tasks change even with every workflow surface closed — 13 builds per session load where the base does none. At this session's shape (8 todos / 0 tools / 3 agent tasks) a build measures 2.8–4.0 µs (4×4000 iterations in the live page), so ~40 µs per load. A design note, not a regression.
3. Repo gates — the ones the resolve bot skipped
Typecheck, ESLint, Prettier and the whole-repo build all pass on the merged tree. The web-shell suite is 328 files / 8672 tests with 3 failures, and none of them belongs to this PR:
build-artifact.test.ts › keeps the transcript entry a fraction of the interactive entry— 3/3 red on both arms. It is a byte cap: 1 341 442 (head) vs 1 339 305 (base) against a 1 300 000 ceiling, so it is already over onmain.AddMenu.test.tsx › returns keyboard focus to the trigger after Escape— flaky; red in the full run on both arms, green in 2 of 3 re-runs.BranchPickerPopover.test.tsx › resets the remotes view after a workspace switch— flaky; did not reproduce once in three re-runs.
Worth recording about that first one: two byte budgets are breached on this machine — the document export renderer (1 976 702 vs a 1 930 000 cap) and the transcript entry above — and both are breached by main alone. My workspace was installed with pnpm; CI installs with npm ci against package-lock.json, where the same build is green (main's nightly e2e lane builds fine) and neither budget runs on pull requests. This PR's own share is +1 802 B and +2 137 B. Not a blocker and not this PR's doing — but if anyone tightens those caps later, this is the arithmetic.
One caveat on method: a first full run while this machine was at load average 150–250 reported 43 failures on head; re-run at load 60 it was 3. The numbers above are the low-load run plus three targeted repetitions per arm.
4. The two blocking review gaps are unchanged
Mutation applied to 6c44841 |
What it breaks for a user | Result |
|---|---|---|
Delete both App-level pass-down sites (projection: sessionWorkflowProjection, and projection={sessionWorkflowProjection} — 2 lines) |
Every surface silently goes back to deriving its own projection; the headline contract of this PR is gone, and so is the 4→2 / 6→2 win | survived, 88/88 green |
Drop sharedProjection from the adopting memo's dependency list in all three surfaces |
An adopted projection freezes at the identity it had on first render: the graph stops moving while the transcript keeps streaming | survived, 88/88 green |
Battery: components/workflow + components/workflows + PlanExecutionView* → 10 files / 88 tests, green unmutated. This is the same result as round 1 — expected, because the merge did not touch a test file, and all 11 review threads are still open and not outdated.
5. Not covered
macOS only (no Windows or Linux run). web-shell package suites only — no Playwright e2e or visual suites, no full-repo vitest. This round re-tested the two findings I called blocking, not all eleven; the other nine anchor on test files the merge left byte-identical, so I take round 1's results as standing. One thing a reviewer reproducing this will notice: the inspector reports "0 active Agents" while three agent tasks are genuinely running — identical on both arms, unrelated to this PR.
Verdict
The merge is sound and I would not ask for it to be redone. Of the two things I asked for last time, (a) the rebase onto main with stepNumberByTodo folded in is done, and now verified; (b) real assertions for the App-level pass-down and for the adopting memo's freshness are still open. Close (b) and this is ready to merge from my side; the remaining findings are worth a follow-up sweep but should not hold it.
中文说明
验证报告 · 第二轮 —— 合并后的 head 6c4484138c
在我的第一轮报告之后,@qwen-code /resolve 把 main 合进本分支并推出了 6c44841,并明确写着 "未验证。 未运行构建 / 类型检查 / 测试。" 本轮就是把那棵合并后的树重新构建,再接回真实 qwen serve daemon + 真实浏览器,与它的 main 父提交并排跑,回答被留下的三个问题:这次冲突解决是否等价、性能收益在合并后是否还成立、仓库门禁是否通过。
结论:解决是正确的——我从两个方向以及运行中的应用里都做了核对;收益完整保留在 4→2 / 6→2,渲染次数一致、界面输出逐字节相同;类型检查、lint、格式化、构建与包内测试全部通过。上一轮我判为阻塞的两条评审缺口原样未动,因为这次合并没有改动任何一个测试文件。
装置
- 在合并后的 head
6c44841与它的main父提交e5969d6(即6c44841^2,同一棵树但不含本 PR)各检出 worktree,均用 pnpm 11.24.0 安装并完整构建。macOS 25.6、Node 24.18.1。 - 两臂共用一个真实 daemon(来自 head 树):
node scripts/dev.js serve --port 4180 --workspace <scratch>,隔离QWEN_HOME,experimental.sessionWorkflow: true、tools.todoWrite.enabled: true、Plan 模式。 - 模型用仓库自带的脚本化端点(
integration-tests/fake-openai-server.ts),会话完全确定:todo_write写入 8 步带blockedBy的计划 →exit_plan_mode→ 在浏览器里批准 → 派出 3 个带todo_id的真实后台子智能体,每个都被我从外部控制的闸门挂住。是真的 daemon agent task 和真的子进程,不是夹具。 - 两个 Vite dev server 接同一个 daemon —— head 在
:5191、base 在:5192,两臂只差被审查的客户端代码,读的是同一个会话。 - 计数:在
buildSessionWorkflowProjection、createTaskExecutionIndex以及三个 surface 组件的函数体首行各加一条自增语句,由页面读出。两臂形状完全一致,跑门禁前已全部回滚(git status干净)。 - 下面所有对照都是在会话冻结后(三个子智能体均已结束)采集的,两臂看到的数据完全相同。
1. 冲突解决是等价的 —— 从两个方向核对
合并结果 − main 在十个文件中的九个上与 PR 自身的改动逐字节相同,三个测试文件全部在内。唯一冲突的 PlanExecutionView.tsx 上,全部差异就是它的 import 块:解决保留了 isSubAgentToolCall(重构曾删掉它,但 main 新的节点面 agentCount 需要),并保留了 main 扩充过的多行 toolFormatting 导入。反方向看,合并结果 − PR head 等于 main 自己那 342 行改动加上同样这几行 import。main 在 #10938 与 #11434 中新增的标记全部存活,而它删掉的 data-plan-input 依然不存在。
需要「发明」的两处语义接缝都成立:
stepNumberByTodo的推导与 main 的逐字符相同,作用在 inspector 用来编号的同一个todosprop 上,供给同样的五个消费点,且todos在幸存 memo 的依赖列表里。feat(web-shell): make Session Workflow dependencies navigable and quiet its chrome #10938 要求「图、inspector、chips 对同一步给出相同编号」的约束得以保留 —— 而且我是在运行中的应用里核对的,不只是读代码。dependentsByTodo改为从共享 projection 读取,而不再做一次局部的blockedBy遍历。两者都用Set去重、丢弃自引用、丢弃指不到任何步骤的 id,且顺序都按todos。
2. 在运行中的应用里:收益完整,界面无变化
| 场景 | surface 渲染次数 cockpit / inspector / graph |
base e5969d6 |
head 6c44841 |
|---|---|---|---|
| 进入 cockpit —— cockpit、内嵌图与 inspector 全部挂载(每臂 3 次重复) | 2 / 2 / 4 —— 两臂完全相同 | 4 次 projection / 6 次索引构建 | 2 / 2 |
| 只打开 Workflow inspector | 0 / 4 / 0 | 2 / 2 | 2 / 2 |
| Hover 风暴,10 次指针事件划过图节点 | 0 / 0 / 2 | 0 / 0 | 0 / 0 |
| 会话页加载,未挂载任何 workflow 界面(3 次重复) | 0 / 0 / 0 | 0 / 0 | 13 / 13 |
三个界面同时挂载时,合并后的 head 把 projection 推导从 4 次降到 2 次、task 索引构建从 6 次降到 2 次,而渲染次数完全一致 —— 与我第一轮在合并前测到的结果相同。hover 在 base 上本就干净,合并后保持干净。
而且对用户不可见。 cockpit 渲染出的文本两臂完全相同 —— 1080 个字符、校验和 1442004915 —— 包含 main 的 1…8 步骤编号和每一个「Depends on」chip。上面两张截图就是同一个会话的并排对比。
唯一的真实代价未变且仍然很小:App 层的 memo 在 todos/tools/tasks 变化时都会跑,即使所有 workflow 界面都关着 —— 一次会话加载跑 13 次,base 是 0 次。在本会话的形状下(8 todos / 0 工具 / 3 个 agent task)单次耗时 2.8–4.0 µs(页面内 4×4000 次迭代实测),整次加载约 40 µs。属于设计备注,不是回归。
3. 仓库门禁 —— 机器人跳过的那些
类型检查、ESLint、Prettier 与全仓构建在合并后的树上全部通过。web-shell 套件 328 文件 / 8672 用例,3 条失败,没有一条属于本 PR:
build-artifact.test.ts › keeps the transcript entry a fraction of the interactive entry—— 两臂都是 3/3 红。它是一个字节上限:1 341 442(head)对 1 339 305(base),上限 1 300 000,也就是说main本身就已超标。AddMenu.test.tsx › returns keyboard focus to the trigger after Escape—— 抖动;全量跑时两臂都红,重跑 3 次里绿 2 次。BranchPickerPopover.test.tsx › resets the remotes view after a workspace switch—— 抖动;重跑 3 次一次都没复现。
关于第一条值得记录:本机上有两个字节预算被突破 —— 文档导出渲染器(1 976 702,上限 1 930 000)与上面那个 transcript 入口 —— 而且都是 main 自己就已突破。我的工作区是用 pnpm 安装的;CI 用 npm ci 配 package-lock.json,同一个构建在那边是绿的(main 的 nightly e2e 腿构建正常),而且这两个预算都不在 PR 门禁上。本 PR 自身的份额是 +1 802 B 与 +2 137 B。不构成阻塞、也不是本 PR 造成的 —— 但将来谁要收紧这两个上限,这就是账。
方法上的一个说明:第一次全量跑时本机负载 150–250,head 报了 43 条失败;负载降到 60 重跑只剩 3 条。上表用的是低负载那次,外加每臂三次定向重复。
4. 两条阻塞性评审缺口原样未动
打在 6c44841 上的变异 |
对用户意味着什么 | 结果 |
|---|---|---|
删掉 App 层两处传递(projection: sessionWorkflowProjection, 与 projection={sessionWorkflowProjection},共 2 行) |
每个界面都悄悄退回各自推导 projection;本 PR 的核心契约没了,4→2 / 6→2 的收益也没了 | 存活,88/88 全绿 |
从三个界面的采纳 memo 依赖列表里去掉 sharedProjection |
被采纳的 projection 冻结在首次渲染时的身份:transcript 还在流,图却不动了 | 存活,88/88 全绿 |
变异靶场:components/workflow + components/workflows + PlanExecutionView*,共 10 文件 / 88 用例,未变异时全绿。结果与第一轮一致 —— 这是预期的,因为合并没有动任何测试文件,且 11 条评审 thread 至今全部未解决、也未过期。
5. 未覆盖
仅 macOS(未跑 Windows / Linux)。仅 web-shell 包内套件 —— 未跑 Playwright e2e 与视觉套件,未跑全仓 vitest。本轮只复测了我判为阻塞的那两条,而非全部十一条;其余九条锚定的测试文件在合并前后逐字节相同,因此沿用第一轮的结论。复现时会看到一个现象:三个 agent task 确实在运行时,inspector 却显示「0 active Agents」—— 两臂表现一致,与本 PR 无关。
结论
这次合并是稳的,我不会要求重做。上一轮我提的两点里,(a)「基于 main 合并并把 stepNumberByTodo 收进来」已完成,且本轮已验证;(b)「给 App 层传递与采纳 memo 的新鲜度补上真实断言」仍未完成。把 (b) 补上,从我这边就可以合并了;其余意见值得后续统一清理,但不必卡住本 PR。
|
@qwen-code /triage |
Independent verification at head
|
| graph used to compute | projection supplies | verdict |
|---|---|---|
todosById |
same new Map(todos.map(...)) |
identical |
toolsByTodo / unassigned |
same loop, !todoId || !todosById.has(todoId) ≡ !todoId || !knownIds.has(todoId) |
identical |
statesByTodo |
same getPlanNodeStateFromIndex(todo, todosById, toolsByTodo.get(id) ?? [], taskIndex) |
identical |
completedCount |
same filter(status === 'completed').length |
identical |
progressPercent |
same todos.length === 0 ? 0 : Math.floor(...) |
identical — Math.floor, not Math.round |
activeAgentCount |
activeAgents.length where activeAgents = getActiveAgentsFromIndex(tools, taskIndex) |
identical |
attentionCount |
attentionTodos.length where attentionTodos = todos.filter(t => states.get(t.id)?.attention) |
identical |
dependentsByTodo |
same new Set(blockedBy ?? []) walk with the same dependencyId === todo.id || !todosById.has(dependencyId) skip |
identical |
Two of these are worth calling out because they are the ones a refactor like this usually gets wrong. The Math.floor in progressPercent is load-bearing — it feeds aria-valuenow, and rounding would report 100% completion on a long plan while a step is still outstanding; it survives. And dependentsByTodo keeps the self-dependency and unknown-id filter, so the graph's downstream-step set is the same set of edges its topology already serialized.
The memo dependency list changed from [taskIndex, todos, tools] to [projection, todos], which is still correct: the projection is itself memoized on [sharedProjection, tasks, todos, tools] and carries taskIndex, so it changes whenever any of the old inputs did.
I also traced the new optional projection prop through every hop to make sure it is not a declared-but-never-forwarded switch. App puts it in the workflow object it hands ArtifactPanel; ArtifactPanel reaches <SessionWorkflowInspector {...workflow} /> by spread; SessionWorkflowCockpit passes its resolved projection into PlanExecutionView explicitly. All three surfaces really do receive the shared object.
3. Executed evidence
Built the head commit in an isolated worktree (npm run generate && npm run build, both exit 0) and ran the suites:
- The PR's three suites: 6/6 passed —
PlanExecutionView.derivation.test.tsx(2),session-workflow-surfaces.test.tsx(3),session-workflow-model.index.test.ts(1). - The whole
@qwen-code/web-shellsuite: 8671 passed / 1 failed of 8672 across 328 files, 259 s. - Real-Chromium Playwright arm on this PR's exact surface:
client/e2e/visuals/session-workflow.spec.ts— 2 passed (18.6 s), light and dark. That spec is the relevant one here because it does not just screenshot: it asserts the canvas renders 4 nodes and 3 edges, and then asserts the1m 14sruntime is visible both on the node face and in the inspector's agent row. That second pair is the tool-call ↔ task linkage, which is precisely what threading one sharedtaskIndexcould have broken silently while still drawing a plausible graph. Playwright started its own dev server (13[WebServer]lines) on a port no spec uses.
About the one failure, since a green claim that hides it would be worthless. It is BranchPickerPopover.test.tsx > resets the remotes view and restores no focus after a workspace switch — an activeElement assertion. That file is not in this PR's changed set, and it is green three independent ways: 139/139 passed in isolation, twice, and 144/144 when run together with both of this PR's new test files (so the new files do not pollute it). The owning CI lane, Test (ubuntu-latest, Node 22.x), is success at this same head. It is a load-sensitive focus flake under a 259 s / 8672-test run, not a regression from this PR.
4. Mutation witnesses — the counting tests actually count
A derivation-count assertion is exactly the kind of test that can pass while measuring nothing, so I broke the sharing on purpose, once per surface, and restored:
| mutation | result |
|---|---|
| none (head as-is) | 3/3 passed |
SessionWorkflowCockpit ignores sharedProjection and rebuilds |
1 failed — expected 1 to be +0 on builds no extra projection or index when the app passes one down; the two standalone cases still pass, correctly |
PlanExecutionView (the embedded graph) ignores sharedProjection and rebuilds |
2 failed — the shared case expected 1 to be +0, and derives the projection once for a standalone cockpit tree expected 2 to be 1 |
The second row is the one that matters: 2 → 1 is the graph's own private rebuild being counted and then caught, which is the deduplication this PR claims. Restoration is witnessed, not assumed — git diff --exit-code <head> -- packages/web-shell returns 0 and git status --porcelain is empty afterwards.
5. CI, stated precisely
Product lanes at this head are green: Lint & Static success, Test (ubuntu-latest, Node 22.x) success, Capture web-shell visuals success, web-shell E2E Smoke success, Desktop Shell (ubuntu-22.04) and (windows-2022) success, Integration Tests (no-AK, No Sandbox) success; Test (macos), Test (windows) and build-cli are skipped, not failing. Lane census total_count=42, items_fetched=42, match.
I am deliberately not writing "CI is green". The rollup reads statusCheckRollup.state = FAILURE and mergeStateStatus = BLOCKED, and the single non-green run in the census is the bot lane review-pr (completed / failure), whose latest run under that name is skipped — so a per-lane dedup hides the failure the rollup still counts. That lane is review infrastructure, not a product test.
6. The one thing still blocking this PR is not code
reviewDecision = CHANGES_REQUESTED comes from the triage bot's 2026-09-07 review at the then-head a0e85e8063c3, which is a description-template gate and says so itself — "nothing in this pass is a judgment on the code itself". A maintainer has since approved the current head (wenshao, APPROVED at 6c4484138c2c, 2026-09-18T22:09:29Z, which is after the head commit's 2026-09-18T03:52:19Z).
The description was reworked afterwards (updated_at 2026-09-18T22:52:29Z) and now carries What this PR does, Why it's needed, Reviewer Test Plan and How to verify. Four template sections are still absent as headings: ### Evidence (Before & After), ### Tested on, ## Risk & Scope, ## Linked Issues (Fixes #10865 is at the top of the body rather than under the last one). Whether that gate is now satisfied is a maintainer's call, not mine — I am flagging it because it, not the code, is what keeps mergeStateStatus at BLOCKED with auto-merge armed since 2026-09-18T22:09:39Z.
One observation the triage bot already made at the stale head is still accurate at this one, so I am not re-filing it, only confirming it: the new app-level memo is gated on sessionWorkflowEnabled alone. planAgentTools short-circuits to [] when no workflow surface is open, but environmentAgentTasks is a plain memo over messages, so a chat-only session with the experimental flag on now pays one buildSessionWorkflowProjection plus one createTaskExecutionIndex per message update where before it paid nothing. That is a deliberate-trade-off question for the author, not a correctness defect.
Conclusion
No Critical found. The refactor does what it says: one projection and one task-execution index per render, shared by the cockpit, the inspector and the embedded graph, with the extraction verified behaviour-preserving function by function and every handed-over derivation checked against its original. The perf claim is pinned by tests that fail when the sharing is broken, and the surfaces still render and link correctly in a real browser.
As of the state read immediately before posting, this comment carries no approval and requests no changes. I am not approving here because the outstanding blocker is the documentation gate above, which only a maintainer can waive — and a maintainer already has approved, so an additional approval from me would not change reviewDecision while the bot's review stands undismissed.
中文说明
在 head 6c4484138c2c 的独立检出上完整验证了这个 PR 自己的保证,并用变异测试证明这些检查不是空转。代码改动中没有发现阻塞合并的缺陷。
先确认范围。 merge_base(base, head) == base(e5969d67d4fb),分支未分叉;三点 diff 与 GitHub 自身数字完全一致 —— 10 个文件,+931 / −470。覆盖是文件完备的:7/7 个生产文件都按 head blob 读过,而不是只看补丁。其余 3 个是测试。
1. 抽取是行为等价的,逐个函数比对。 把现在位于 taskExecutionIndex.ts 的每个函数与合并基线上 PlanExecutionView.tsx 里的原实现做空白归一化比对:18 个全部对上 —— 14 个逐字节相同,3 个只差新增的 export,1 个只是把 return cond ? tool : undefined 改写成 if (cond) return tool; return undefined。 语义零差异。编译层面:PlanExecutionView.tsx 导入或再导出的 16 个符号在新模块中全部存在;也没有引入循环依赖。
2. 图交出去的每一项推导都语义相同。 八项逐一核对(见上表)。其中两项最值得点出:progressPercent 保留了 Math.floor(它喂给 aria-valuenow,取整会在长计划仍有步骤未完成时报出 100%);dependentsByTodo 保留了自依赖与未知 id 的过滤条件。memo 依赖从 [taskIndex, todos, tools] 改为 [projection, todos] 仍然正确。我也把新增的可选 projection prop 沿每一跳追到底,确认它不是"声明了却从未转发"的空开关 —— ArtifactPanel 是通过 {...workflow} 展开传给 SessionWorkflowInspector 的。
3. 执行证据。 在隔离 worktree 中构建 head(npm run generate && npm run build 均 exit 0):PR 自带的三个套件 6/6 通过;@qwen-code/web-shell 整套 8672 个测试通过 8671、失败 1(328 个文件,259 秒);真实 Chromium 的 Playwright 臂 session-workflow.spec.ts 2 passed (18.6s)(明暗两套主题)。该 spec 之所以关键,是因为它不只截图:它断言画布渲染出 4 个节点、3 条边,并断言 1m 14s 运行时长同时出现在节点面和 inspector 的 agent 行上 —— 后者正是"共享同一个 taskIndex"最可能悄悄弄坏、却仍能画出看似正常图形的工具调用↔任务关联。
关于那一个失败。 它是 BranchPickerPopover.test.tsx 里的一个 activeElement 断言,该文件不在本 PR 的改动集内,并且三种方式独立复现为绿:单独运行 139/139 通过,两次;与本 PR 两个新测试文件一起运行 144/144 通过(说明新文件没有污染它);同一 head 上归属的 CI lane Test (ubuntu-latest, Node 22.x) 为 success。它是 259 秒 / 8672 测试负载下的焦点时序偶发失败,不是本 PR 引入的回归。
4. 变异见证 —— 计数测试真的在计数。
| 变异 | 结果 |
|---|---|
| 无(head 原样) | 3/3 通过 |
SessionWorkflowCockpit 忽略 sharedProjection 自行重建 |
1 失败 —— expected 1 to be +0;两个 standalone 用例仍正确通过 |
PlanExecutionView(内嵌图)忽略 sharedProjection 自行重建 |
2 失败 —— 共享用例 expected 1 to be +0,standalone cockpit 用例 expected 2 to be 1 |
第二行最关键:2 → 1 正是图自己那次私有重建被计入并被抓住,也就是本 PR 声称消除的那一次。恢复是有见证的:git diff --exit-code <head> -- packages/web-shell 返回 0,git status --porcelain 为空。
5. CI 的精确表述。 该 head 上生产 lane 全绿(Lint & Static、Test (ubuntu-latest, Node 22.x)、Capture web-shell visuals、web-shell E2E Smoke、两个 Desktop Shell、Integration Tests (no-AK, No Sandbox) 均为 success;Test (macos)、Test (windows)、build-cli 为 skipped 而非失败)。lane 普查 total_count=42、items_fetched=42,一致。
我刻意不写"CI 全绿":rollup 读作 statusCheckRollup.state = FAILURE、mergeStateStatus = BLOCKED,普查中唯一非绿的运行是机器人 lane review-pr(completed / failure),而该名称下最新一次运行是 skipped —— 按 lane 去重会掩盖 rollup 仍然计入的那次失败。该 lane 属于评审基础设施,不是产品测试。
6. 唯一还阻塞这个 PR 的不是代码。 reviewDecision = CHANGES_REQUESTED 来自 triage 机器人 2026-09-07 在当时 head a0e85e8063c3 上的评审,那是描述模板关卡,它自己也这么写 —— "nothing in this pass is a judgment on the code itself"。维护者随后已批准当前 head(wenshao,APPROVED @ 6c4484138c2c,2026-09-18T22:09:29Z,晚于 head 提交的 2026-09-18T03:52:19Z)。描述在此之后被重写过(updated_at 2026-09-18T22:52:29Z),现已包含 What this PR does、Why it's needed、Reviewer Test Plan、How to verify;仍有四个模板章节作为标题缺失:### Evidence (Before & After)、### Tested on、## Risk & Scope、## Linked Issues。这道关卡是否已满足属于维护者判断,我只是指出:让 mergeStateStatus 停在 BLOCKED(且自 2026-09-18T22:09:39Z 起已开启自动合并)的是它,而不是代码。
triage 机器人在旧 head 上提过的一条观察在当前 head 依然成立,所以我不再重复提交,只作确认:新的 app 级 memo 只以 sessionWorkflowEnabled 为条件。planAgentTools 在没有工作流界面时会短路成 [],但 environmentAgentTasks 是对 messages 的普通 memo,因此开着该实验开关的纯聊天会话,现在每次消息更新都会付出一次 buildSessionWorkflowProjection 加一次 createTaskExecutionIndex,而此前是零。这是留给作者权衡的取舍问题,不是正确性缺陷。
结论:未发现 Critical。 按发帖前即时读到的状态,本条评论不含批准、也不要求修改。我不在此批准,是因为尚未解除的阻塞是上面那道文档关卡,只有维护者能豁免 —— 而维护者已经批准过,在机器人评审未被 dismiss 的情况下,我再加一个批准也不会改变 reviewDecision。
— independent verification by qqqys; built and executed at head 6c4484138c2c3f0669cd727b098304192535f8d7








perf(web-shell): derive the session workflow projection once per render
Fixes #10865
What this PR does
One session workflow projection per render, shared by every surface.
Appbuilds the projection once (memoized on the same inputs the cockpit already receives) and hands the same object to the cockpit, the artifact-panel inspector, and the graph embedded in the cockpit. Each surface accepts an optionalprojectionprop and falls back to deriving from raw props, so standalone mounts (ToolApproval,TasksStatusMessage, isolated tests) are unchanged.To make the sharing possible without a circular import, the task-execution lookups
PlanExecutionViewowned — the index, its walkers, the node-state / attention / active-agent derivations — move to a sharedtaskExecutionIndexmodule that both the projection (session-workflow-model) and the graph now depend on.PlanExecutionViewre-exports them, so every existing import path keeps working.The projection now carries what the graph used to recompute privately:
taskIndex— the singlecreateTaskExecutionIndexbuild; the graph's executions read live status through it instead of raising their own;unassignedTools— tools whosetodo_idis missing or outside the plan, previously collected in the graph and dropped by the projection;toolsByTodo), node states, counts and its dependents map now come from the projection — the dependents map used to be a third copy of the sameblockedBywalk (after the projection's own and the one perf(web-shell): derive the session workflow projection once #10871 replaced in the inspector).What remains graph-local in
PlanExecutionViewis genuinely graph-specific: the topological layering, the topology serialization and the edge budget — still one memoized derivation that hover cannot re-run.Why it's needed
Follow-up to #10871 (merged). That PR made each derivation cheap — one index build per projection instead of one per todo and per tool — and memoized each surface's own derivation. What it left in place is that a single render still ran that derivation three times: the cockpit, the inspector (auto-opened beside it,
App.tsx→ArtifactPanel.tsx) and the embedded graph each built their own projection, each with its own index and its own copies of the grouping, node-state, count and dependents walks. The issue's remaining acceptance criterion is exactly this: one projection for the cockpit, the inspector and the embedded graph per render.Wiring the projection through
PlanExecutionViewalso removes the last semantic duplication inside the graph: its derived block re-derived an equivalent set from raw props inline, so any future divergence between the projection and the graph had two places to happen. They now cannot disagree — they read one object.Reviewer Test Plan
How to verify
The behavioural guarantees are pinned by tests rather than by inspection:
session-workflow-surfaces.test.tsx— mounts the cockpit (with its embedded graph) and the inspector together:buildSessionWorkflowProjectionandcreateTaskExecutionIndexboth count 0 extra calls, and both surfaces render their content from the shared object;PlanExecutionView.derivation.test.tsx:data-focused(the re-render happened) whilelayerPlanTodosandJSON.stringifycall counts stay flat — no topological re-sort, no topology re-serialization per hover;requestAnimationFrame, and that frame runs one measure pass (onegetBoundingClientRectbatch over the graph container and its nodes, not one per schedule call).session-workflow-model.index.test.ts(from perf(web-shell): derive the session workflow projection once #10871) — the projection still builds the task index exactly once; its mock now points at the extractedtaskExecutionIndexmodule, which is what the projection actually imports.Sanity of the spies: temporarily making
PlanExecutionViewignore the passed-in projection turns the surfaces suite red (1 extra projection + 1 extra index where 0 are expected, and 2 where 1 is expected for the standalone tree) — the counts fail on reintroduction, not just on absence.Manual check if you prefer: enable
experimental.sessionWorkflow, open the Workflow cockpit with a plan that has dependencies and agent steps, and confirm the graph, the inspector summary, the progress strip and the unassigned bucket all read the same values as before — the change is intended to be invisible. Everything in the existingPlanExecutionView,SessionWorkflowCockpit,SessionWorkflowInspectorandsession-workflow-modelsuites stays green, including the CSS source-shape guards.Not covered / out of scope
getAgentToolsForPlan's full-message rescan (issue problem 7) is untouched — it is memoized at theApplevel on the same message list and is a separate, self-contained change if it is wanted.中文说明
本 PR 做了什么
每次渲染只推导一份 session workflow projection,全部 surface 共享。
App层用 useMemo 构建一次(输入与 cockpit 现有 props 相同),把同一个对象传给 cockpit、artifact 面板里的 inspector、以及 cockpit 内嵌的依赖图。每个 surface 都接受可选的projectionprop,未传入时回退为从原始 props 自行推导——独立挂载场景(ToolApproval、TasksStatusMessage、隔离的测试)行为不变。为了让共享不产生循环依赖,原先由
PlanExecutionView持有的 task-execution 查询(索引本身、各类遍历、节点状态 / attention / 活跃 agent 推导)抽取到共享模块taskExecutionIndex,projection(session-workflow-model)与依赖图都改为依赖它。PlanExecutionView对这些导出做了 re-export,所有既有 import 路径不受影响。projection 现在携带图内此前各自重算的内容:
taskIndex—— 唯一一次createTaskExecutionIndex构建;图内的执行列表直接经由它读实时状态,不再自建;unassignedTools——todo_id缺失或指向计划外的工具,此前只在图内收集、projection 直接丢弃;toolsByTodo)、节点状态、各类计数、dependents 映射全部改从 projection 读取——dependents 映射此前是同一blockedBy遍历的第三份拷贝(projection 自身一份、perf(web-shell): derive the session workflow projection once #10871 在 inspector 里替换掉的那份之外又一份)。PlanExecutionView里保留的只剩真正图专属的部分:拓扑分层、拓扑序列化与边数预算——仍是单份 memo 化推导,hover 不会重跑。为什么需要
这是 #10871(已合并)的后续。那个 PR 让每次推导变便宜了——每个 projection 只建一次索引而不是每个 todo、每个 tool 各建一次——并把每个 surface 各自的推导 memo 化。它遗留的问题是:一次渲染仍会把这套推导跑三遍——cockpit、inspector(进入 cockpit 时自动在旁边打开)与内嵌图各建一份 projection,各自带自己的索引和各自的分组 / 节点状态 / 计数 / dependents 遍历。issue 剩下的验收标准正是这一点:cockpit、inspector 与内嵌图每次渲染共享一个 projection。
把 projection 接进
PlanExecutionView还消除了图内部最后一处语义重复:它的派生块此前用原始 props 内联重推一份等价集合,projection 与图之间未来任何行为分歧都有两个可以发生的地方;现在它们读同一个对象,不可能不一致。审查者验证计划
行为保证由测试钉住,而非人工检查:
session-workflow-surfaces.test.tsx—— 同时挂载 cockpit(含内嵌图)与 inspector:buildSessionWorkflowProjection与createTaskExecutionIndex的 spy 计数均为 0,且两个 surface 都基于共享对象正常渲染;PlanExecutionView.derivation.test.tsx:data-focused(确实发生了 re-render),而layerPlanTodos与JSON.stringify的调用计数保持不变——hover 不重跑拓扑排序、不重新序列化拓扑;requestAnimationFrame,该帧只跑一轮 measure(对图容器和各节点各读一次getBoundingClientRect,而不是每次调度一轮)。session-workflow-model.index.test.ts(perf(web-shell): derive the session workflow projection once #10871 引入)—— projection 每次仍然只建一次 task index;其 mock 路径已改为指向抽取后的taskExecutionIndex模块(projection 实际 import 的模块)。spy 的锋利度验证:临时让
PlanExecutionView无视传入的 projection,surfaces 套件立刻变红(共享场景多出 1 次 projection + 1 次 index,独立树场景 2 次而非 1 次)——计数在回退引入时会失败,而不是只对"不存在"敏感。偏好手动验证的话:开启
experimental.sessionWorkflow,打开 Workflow cockpit(计划带依赖与 agent 步骤),确认图、inspector 摘要、进度条与 unassigned 分组的读数与之前完全一致——本变更应当是不可见的。现有PlanExecutionView、SessionWorkflowCockpit、SessionWorkflowInspector、session-workflow-model套件全部保持绿色,包括 CSS 源形状守卫测试。未覆盖 / 范围之外
getAgentToolsForPlan的全消息重扫(issue 问题 7)未动——它在App层以同一消息列表为依赖做了 memoize,若需要是一个独立、内聚的后续变更。