fix(vscode): give the edit back when a permission diff is closed - #11171
Conversation
Closing a host-owned permission diff by hand left the user having to approve or reject an edit they could no longer look at. `onDidCloseTextDocument` routes the close to `DiffManager.cancelDiff`, which fires `ide/diffClosed` on `onDidChange` — and the only consumer of that is `ide-server.ts`, i.e. IDE-mode MCP transports. The web shell that asked for the diff is not one, so it never learned. `EmbeddedApp` kept the request in `openPermissionDiffsRef`, so `updateTranscript` never re-posted `openDiff`, and `ToolGroup` kept the row locked because `hostOwnsEditDiffPreview` says the host owns the preview. DiffManager now also fires a typed `onDidClosePermissionDiff` when the closed diff had a `permissionRequestId`. `extension.ts` fans it out to the permission-aware providers, the same registry the `qwen.diff.accept` and `qwen.diff.cancel` commands already use, and `WebViewProvider` posts it to its webview. `EmbeddedApp` treats that as the host handing the preview back rather than as a vote: it drops the request from `openPermissionDiffsRef`, stops passing `hostOwnsEditDiffPreview`, and does not reopen the tab the user just closed. The row unlocks and the web shell renders the diff inline, so the edit is visible again and the approval can be answered. Ownership returns to the host on the next permission request, or when the pending diffs are torn down. Two paths deliberately do not trigger it: `qwen.diff.accept` / `qwen.diff.cancel` never reach `cancelDiff` for a request-bound diff (they route the vote through `respondToPendingPermission`), and `closeDiffEditor` drops the map entry before the tab closes, so a close the web shell itself asked for does not echo back as a dismissal. Both are pinned by tests. Run here: `diff-manager.test.ts` 8 passed, `webview/EmbeddedApp.test.tsx` 21 passed. `extension.test.ts` could not run on this machine — `@qwen-code/qwen-code-core` has no build here — so the activation-time subscription is CI's to confirm. No build or typecheck was run. Fixes #10557
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
@yiliang114 the change reads as tightly scoped — 6 files, ~79 production lines against 192 test lines, all inside the VS Code companion — and #10557 documents the gap end to end. But the PR body doesn't follow the PR template, so I have to stop here and ask for a restructure first.
Missing:
## Why it's needed— the motivation is currently folded into## What this PR does(the "The gap, end to end" bullets). It already reads well, it's just in the wrong section; pull it out.### How to verify— your numbered 5-step plan under## Reviewer Test Planis exactly what this subsection asks for, it just has no heading.### What was and was not run herecan stay as an extra subsection.### Evidence (Before & After)— this is the substantive gap. Closing the diff tab and watching the row unlock and render the diff inline is user-visible behaviour in the companion, so the template wants before-and-after evidence here (screenshots or a short recording), notN/A. Pass counts fromdiff-manager.test.tsandEmbeddedApp.test.tsxdon't show the tab staying closed or the row actually unlocking.### Tested on— the macOS / Windows / Linux table. Worth filling in for an extension change; the body says which suites ran but not where they ran.- the
<details><summary>中文说明</summary>block with the Chinese translation of the body.
## Risk & Scope and ## Linked Issues are both present and substantive — no change needed there. ### Environment (optional) is genuinely optional.
To be clear about what this is: a process gate, not a judgment on your fix. The substantive code review happens once the body is complete — add the missing sections and re-run @qwen-code /triage.
中文说明
@yiliang114 改动本身范围很小(6 个文件,约 79 行生产代码对 192 行测试代码,全部在 VS Code companion 内),#10557 也把这个缺口从头到尾描述清楚了。但 PR 描述没有遵循本仓库的 PR 模板,所以我只能先停在这里,请你调整结构。
缺少的部分:
## Why it's needed—— 动机目前写在## What this PR does里("The gap, end to end" 那几条)。内容本身写得不错,只是放错了小节,拆出来即可。### How to verify—— 你在## Reviewer Test Plan下的 5 步编号清单正是这个小节要的内容,只是没有标题。### What was and was not run here可以作为额外小节保留。### Evidence (Before & After)—— 这是真正缺的一块。关闭 diff 标签页后工具行解锁、内联渲染 diff,属于 companion 里用户可见的行为,模板要求这里给出 before/after 证据(截图或短录屏),不能写N/A。diff-manager.test.ts和EmbeddedApp.test.tsx的通过数量无法证明"标签页没有重新弹开、工具行确实解锁了"。### Tested on—— macOS / Windows / Linux 表格。扩展类改动值得填一下;目前描述只说了跑了哪些测试,没说在哪个系统上跑的。<details><summary>中文说明</summary>中文翻译段落。
## Risk & Scope 和 ## Linked Issues 都在,内容也充实,不需要改动。### Environment (optional) 确实是可选的。
需要说明的是:这是流程性拦截,不是对你这个修复本身的判断。等描述补全后才会进行实质性的代码审查 —— 补齐缺失部分后重新运行 @qwen-code /triage 即可。
— Qwen Code · qwen3.8-max-2026-09-02
Nine of the twelve test-witness gaps in #10585, all in the companion. No production code changes. - R3-2 / R3-3 (commands/index.test.ts): both diff command hops exercised with a DEFINED permissionRequestId. Every existing case left it undefined, so the binding that ties a diff to one approval was never crossed. - R3-9 (diff-manager.test.ts): all three halves of that binding — stored by showDiff, read back by getPermissionRequestId/hasDiff, and used by closeDiff to refuse a diff a different approval owns. - R3-4 / R3-5 / R3-6 / R3-16 (webview/EmbeddedApp.test.tsx): the cleanup loop's requestId-scoped closeDiff; the host-side gate refusing a decision bound to another request; the 'reject' half of the decision vocabulary; and the count assertion for opening a native diff only for the FIRST pending permission. - R3-7 / R3-8 (extension.test.ts): the !permissionRequestId guard on both vote commands — a request-bound diff must reach the approval owner rather than acceptDiff/cancelDiff, and an unbound one must still resolve locally. Run here: commands/index.test.ts 10 passed, diff-manager.test.ts 13 passed, webview/EmbeddedApp.test.tsx 25 passed. extension.test.ts could NOT be run on this machine — it pulls @qwen-code/qwen-code-core and @qwen-code/acp-bridge, neither of which has a build here — so R3-7 and R3-8 are CI's to confirm. R3-11, R3-13 and R3-15 are in web-shell and are not in this commit. The three behavioural observations (R3-10, R3-12, R3-14) are untouched: #10585 says to verify each before writing a fix, and none has been verified. Refs #10585
The last three test-witness gaps in #10585, closing the set. - R3-15 (App.test.tsx): the exact-request-id gate in respondToPendingPermission was only ever crossed with a matching id. A stale native diff left over from an approval that already moved on must not resolve the current request; the test votes with a wrong id, asserts the refusal, then votes with the right one to show the approval is still live. - R3-11 (ToolGroup.test.tsx): a nested edit approval keeps the sub-agent row open, so the edit the user is asked to approve stays visible. Paired with a control that the same agent with nothing pending stays collapsed, so the assertion is about the approval rather than about agent rows always rendering their sub-tools. - R3-13 (EmbeddedApp.test.tsx): the host file-open hand-off, which had zero coverage in either package, routed to the extension as an openFile message. Run here: App.test.tsx (filtered) 1 passed, ToolGroup.test.tsx 106 passed, EmbeddedApp.test.tsx 26 passed. R3-14 verified while writing R3-13 and NOT fixed here: the onWorkspaceFileOpen hand-off in App.tsx does early-return before the pre-existing stat() guard, so directories and missing files reach the host path. That is a behaviour change to decide, not a witness gap; recorded on #10585. Refs #10585
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterℹ️ No screenshot changed against the PR base — but this PR edits 2 render-shaping files:
Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
Two hardening fixes on the notification this PR adds, both matching what the code immediately next to it already does. The webview handler accepted `permissionDiffClosed` from any window. MCP apps and artifact previews run in scriptable sandboxed iframes inside this webview and can postMessage into it — which is exactly why the decision handler two branches below checks `event.source === window.parent`, with two tests pinning it. Handing the edit preview back is not a vote, so this is not an escalation, but it is a spoofable state flip and it should not be reachable from a sandboxed frame. Same gate, and a test that dispatches from a real nested iframe and from a sourceless event. The extension-side fan-out ran unguarded inside a VS Code event handler, so one disposed or half-torn-down surface would take down the emitter and with it every other surface's notification. The two vote commands above it already wrap their fan-out in try/catch with a warn; this now matches. Run here: webview/EmbeddedApp.test.tsx 27 passed.
Closes the two items #9911 still owns after the audit. Its premise is stale — per-message edit/rewind shipped with the cutover, daemon-backed via getRewindSnapshots/rewindSession — but its verification gate never ran and its failure path was silent. **A rejected preflight is no longer silent.** Both submit paths cancelled the prompt with nothing but a console.warn. Hosts put user-facing text in these errors: the companion's rewind throws composer.editUnavailable and composer.editExpired, both fully localized in English and Chinese, and nothing in the repository consumed them other than the throws. They were written to be read by a user and could never reach one — a rewind that failed because the snapshot aged out looked to the user like the edit did nothing, with the composer still in editing mode and no explanation. The toast fires only while the user is still on the session the submission belonged to. That guard is deliberately narrower than submissionOwnerIsCurrent: the full guard also tracks composer identity, and submitting is itself what moves that, so reusing it would suppress the very message the user needs. The first draft of this change did reuse it, and the new test caught that the toast never fired. **The rewind flow now has tests.** getRewindSnapshots and rewindSession previously appeared in EmbeddedApp.test.tsx only as mock stubs, with no case calling them. Added: a rewind that resolves the snapshot for the edited turn rather than the newest one (the fixture lists turns out of order, so a max/last-element bug fails), and the expired-snapshot rejection asserting no rewind is attempted. A third case was written and then dropped because it passed for the wrong reason — it exercised the expired path under a name about session readiness. Run here: web-shell App.test.tsx 746 passed (full file, since this changes a shared submit path), vscode-ide-companion webview/EmbeddedApp.test.tsx 29 passed. Refs #9911
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI and their suites did not run locally; Agent 7 built and tested on Linux only, so no verification of this VS Code companion change ran on the two other platforms it ships to, which matters because the path-normalization concern in R1-3 is Windows-specific.
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
[Critical] Still-standing blocker re-checked from the project's own triage gate (review 5124755540, CHANGES_REQUESTED, 2026-09-06) — this is a ruling on an existing live review, NOT a new code defect found by this review, and it is about the pull request description rather than the diff. That review stops the pull request at a process gate: "the PR body doesn't follow the PR template, so I have to stop here and ask for a restructure first", and "The substantive code review happens once the body is complete". Checked against the description as it stands at bfce659, all five sections it named as missing are still absent: ## Why it's needed (the motivation is still folded into ## What this PR does), ### How to verify (the numbered 5-step plan is still under ## Reviewer Test Plan with no heading of its own), ### Evidence (Before & After) (still no screenshot or recording of the diff tab staying closed and the tool row unlocking — the pass counts under ### What was and was not run here do not show that), ### Tested on (still no macOS / Windows / Linux table, which matters for an extension change), and the <details><summary>中文说明</summary> Chinese-translation block. ## Risk & Scope and ## Linked Issues are both present and substantive, and ### Environment (optional) is genuinely optional, so nothing else is outstanding. To clear it: restructure the description into the template and re-run @qwen-code /triage. The code findings in this review are all recommendations and are independent of this gate — none of them is a merge blocker on its own.
— qwen3.8-max via Qwen Code /review (v0.23.0)
…rite The dismissal chain added for #10557 had three unpinned hops, and the web shell's two edit-name tables disagreed about one alias: - shouldAutoExpand listed write_file/writefile/edit/editfile while isEditToolName also matches bare `write`. Now that EmbeddedApp hands hostOwnsEditDiffPreview back as state, a pending `write` approval unlocked without ever auto-expanding, so the edit the user was asked to approve stayed off screen. Reuse isEditToolName so the two sets cannot drift apart again. - notifyPermissionDiffClosed re-implemented sendMessageToWebView instead of calling it; route it through the funnel so any cross-cutting behaviour added there covers dismissals too. - Add witnesses for the posted payload key (requestId), which the webview receiver is the only thing that cares about, and for the emitter teardown in DiffManager.dispose(). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtq0vr74ni
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
14 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-1 inert new test file (packages/vscode-ide-companion/src/commands/index.test.ts:261) — already reported (comment 3944217715)
- R1-3 unread filePath on the new event payload (packages/vscode-ide-companion/src/diff-manager.ts:479) — already reported (comment 3944217721)
- R1-4 unwitnessed dismissal fan-out subscription (packages/vscode-ide-companion/src/extension.ts:251) — already reported (comment 3944217722)
- R1-5 duplicated permission-block fixture (packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1066) — already reported (comment 3944217727)
- R1-6 rewind flow cases partly landed (packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1198) — already reported (comment 3944217729)
- R1-7 unpinned hostOwnsEditDiffPreview resets (packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx:506) — already reported (comment 3944217732)
- R1-8 untested openPermissionDiffsRef cleanup (packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx:708) — already reported (comment 3944217733)
- R1-10 silent drop points on the dismissal chain (packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts:2394) — already reported (comment 3944217736)
- R1-12 web-shell half of the R3-13 witness missing (packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1108) — already reported (comment 3944217742)
- R1-13 hand-rolled preflight toasts (packages/web-shell/client/App.tsx:9246) — already reported (comment 3944217744)
- R1-14 predicate duplicating an existing guard's conjuncts (packages/web-shell/client/App.tsx:9973) — already reported (comment 3944217746)
- R1-15 queued-submit preflight catch unwitnessed (packages/web-shell/client/App.tsx:10033) — already reported (comment 3944217749)
- R1-16 ParallelAgentsGroup conditional untested (packages/web-shell/client/components/messages/ToolGroup.test.tsx:2743) — already reported (comment 3944217755)
- R1-17 control render passes the same flag value as the case render (packages/web-shell/client/components/messages/ToolGroup.test.tsx:2777) — already reported (comment 3944217759)
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI at this commit and their suites did not run locally; Agent 7 built and tested on Linux only, which matters because R2-1 is a Windows-only test failure that no lane run here could have caught.
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
[Critical] R1-19 Still-standing blocker re-checked from the project's own triage gate (review 5124755540, CHANGES_REQUESTED, 2026-09-06). This is a ruling on an existing live review, not a new code defect found by this round, and it is about the pull request description rather than the diff. That review stops the pull request at a process gate: "the PR body doesn't follow the PR template, so I have to stop here and ask for a restructure first", and "The substantive code review happens once the body is complete". Checked against the description as it stands at dea09dc, all five sections it named as missing are still absent: ## Why it's needed (the motivation is still folded into ## What this PR does), ### How to verify (the numbered 5-step plan is still under ## Reviewer Test Plan with no heading of its own), ### Evidence (Before & After) (still no screenshot or recording of the diff tab staying closed and the tool row unlocking — the pass counts under ### What was and was not run here do not show that, and the web-shell visual preview comment renders mock-daemon screenshots of the web shell rather than the companion interaction), ### Tested on (still no macOS / Windows / Linux table, which matters for an extension change and matters more this round because R2-1 is a Windows-only test failure that no lane run here could catch), and the <details><summary>中文说明</summary> Chinese-translation block. ## Risk & Scope and ## Linked Issues are both present and substantive, and ### Environment (optional) is genuinely optional, so nothing else is outstanding. To clear it: restructure the description into the template and re-run @qwen-code /triage. The code findings in this review are all independent of this gate.
— qwen3.8-max via Qwen Code /review (v0.23.0)
The payload carries the normalized path showDiff stored, not the callers argument, so pinning a raw POSIX literal made the assertion red on Windows (path.normalize turns /workspace/foo.ts into a backslash form) while the pull-request lane never showed it. Verified both ways with a win32 probe: the old form fails on the emitted filePath, the derived form passes under win32 and posix (14 tests). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtqfvvmto2
The fixture carried no content, so extractDiff returned an empty string and the expanded card rendered nothing: both assertions passed with no edit on screen. Dropping the bare write alias from the detail renderer kept all 107 tests in the file green; with the fixture content and the new assertion that mutant is caught (1 failed | 106 passed). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtqfvvmto2
…event `onDidClosePermissionDiff` carried `filePath` alongside the request id, and the only consumer — the fan-out in extension.ts — destructures just `permissionRequestId`. The field had no reader anywhere in the repo, and a deep-equality test froze it, so it looked load-bearing while being dead weight in a different path space from every consumer. Narrow the payload to the id the fan-out actually needs and keep the assertion deep-equal, so a field re-added without a reader fails the test. Drops the now-unused node:path import. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtqqlolgof
…n base `submissionSessionIsCurrent` restated the first four conjuncts of `submissionOwnerIsCurrent` verbatim five lines below it, so the two could drift apart while looking independent. Build the wider guard on the narrow base instead. Every operand is a pure ref read (`getComposerWorkspaceCwd` only reads `pendingSessionContextRef`/`connectionRef`/`workspacesRef`), so evaluating the shared four before the write-block and composer-version checks returns the identical boolean for every state. No behaviour change. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtqqlolgof
CI attribution for the three red checks at
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
7 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-4 unwitnessed dismissal fan-out subscription (packages/vscode-ide-companion/src/extension.ts:251) — already reported (comment 3944217722)
- R1-6 rewind flow cases partly landed (packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1198) — already reported (comment 3944217729)
- R1-7 unpinned hostOwnsEditDiffPreview resets (packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx:506) — already reported (comment 3944217732)
- R1-8 untested openPermissionDiffsRef cleanup (packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx:708) — already reported (comment 3944217733)
- R1-10 silent drop points on the dismissal chain (packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts:2394) — already reported (comment 3944217736)
- R1-15 queued-submit preflight catch unwitnessed (packages/web-shell/client/App.tsx:10033) — already reported (comment 3944217749)
- R1-17 control render passes the same flag value as the case render (packages/web-shell/client/components/messages/ToolGroup.test.tsx:2777) — already reported (comment 3944217759)
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI at this commit and their suites did not run locally; Agent 7 built and tested on Linux only, which matters because this is a VS Code companion change and the previous round's R2-1 was a Windows-only test failure no Linux lane could catch.
Not explored to full depth (tool budget reached): "agent 1a": verifying whether a legacy-ACP chat surface and a web-shell surface (or an IDE-mode MCP client and a web-shell approval) can be live simultaneously — the reacha…; "agent 1a": running the full packages/web-shell/client/App.test.tsx suite (746 tests) rather than the two new cases by name filter, so a regression elsewhere in that file….
Not reviewed: reverse audit — stopped before round 5 by the review time budget.
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/components/messages/ToolGroup.test.tsx:2838 — [probe] Hand-back witness never flips the prop on a mounted rowpackages/web-shell/client/App.test.tsx:25855 — [probe] The !hostOwnsEditDiffPreview conjunct has no witness
[Critical] R1-19 Still-standing blocker re-checked from the project's own triage gate (review 5124755540, CHANGES_REQUESTED, 2026-09-06). This is a ruling on an existing live review, not a new code defect found by this round, and it is about the pull request description rather than the diff. That review stops the pull request at a process gate: "the PR body doesn't follow the PR template, so I have to stop here and ask for a restructure first", and "The substantive code review happens once the body is complete". Checked against the description as it stands at 9d0df1c, all five sections it named as missing are still absent: ## Why it's needed (the motivation is still folded into ## What this PR does as "The gap, end to end"), ### How to verify (the numbered 5-step plan is still under ## Reviewer Test Plan with no heading of its own), ### Evidence (Before & After) (still no screenshot or recording of the diff tab staying closed and the tool row unlocking — the pass counts under ### What was and was not run here do not show that, and the web-shell visual preview comment renders mock-daemon screenshots of the web shell rather than the companion interaction), ### Tested on (still no macOS / Windows / Linux table, which matters for an extension change and matters again this round because the windows-latest and macos-latest test lanes were skipped in CI at this commit and their suites did not run locally either), and the <details><summary>中文说明</summary> Chinese-translation block. ## Risk & Scope and ## Linked Issues are both present and substantive, and ### Environment (optional) is genuinely optional, so nothing else is outstanding. To clear it: restructure the description into the template and re-run @qwen-code /triage. The code findings in this review are all independent of this gate.
— qwen3.8-max@e6bf8ffe via Qwen Code /review (v0.23.0)
The Lint & Static job failed at "Check core subpath exports resolve" with `npm error Missing script: "check:core-subpath-exports"`: main added that script and its CI step in 7036781 (#10957) after this branch's last merge, and CI checks out refs/pull/<n>/head, so the step ran against a package.json that predates it. Merging main brings both the script and scripts/check-core-subpath-exports.mjs. The gate scans packages/{cli,acp-bridge,sdk-typescript}/src for `@qwen-code/qwen-code-core/<subpath>` specifiers; this branch touches only vscode-ide-companion and web-shell and imports the core package root, so it contributes no specifiers to the checked set. Merge is clean (no conflicts). Verified locally: the PR's per-file numstat against main is identical before and after the merge, so nothing was lost across the 37-commit gap. Tests not run: no dependency tree on this machine matches this lockfile. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtrcr08gpe
With the missing `check:core-subpath-exports` script resolved by the previous merge, the Lint & Static job now reaches Run Prettier and fails there on this branch's own file: [warn] packages/web-shell/client/components/messages/ToolGroup.test.tsx [warn] Code style issues found in 1 file. The fixture line is 82 columns against printWidth 80, so prettier splits the object literal. Formatting only — no fixture value changes. Verified with prettier 3.6.1, the version this branch's lockfile pins. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtrcr08gpe
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
10 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-1 inert new command-forwarding tests (packages/vscode-ide-companion/src/commands/index.test.ts:261) — already reported (comment 3944217715)
- R1-4 unwitnessed dismissal fan-out subscription (packages/vscode-ide-companion/src/extension.ts:251) — already reported (comment 3944217722)
- R1-5 duplicated permission-block fixture (packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1155) — already reported (comment 3944217727)
- R1-7 unpinned hostOwnsEditDiffPreview resets (packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx:512) — already reported (comment 3944217732)
- R1-8 untested openPermissionDiffsRef cleanup (packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx:714) — already reported (comment 3944217733)
- R1-10 silent drop points on the dismissal chain (packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts:2463) — already reported (comment 3944217736)
- R1-13 hand-rolled preflight toasts bypassing reportError (packages/web-shell/client/App.tsx:9288) — already reported (comment 3944217744)
- R1-15 queued-submit preflight catch unwitnessed (packages/web-shell/client/App.tsx:10076) — already reported (comment 3944217749)
- D3-1 hand-back witness never flips the prop on a mounted row (packages/web-shell/client/components/messages/ToolGroup.test.tsx:2840) — already reported (round 3 deferred list, review 5131595846)
- D3-2 the !hostOwnsEditDiffPreview conjunct has no witness (packages/web-shell/client/App.test.tsx:25855) — already reported (round 3 deferred list, review 5131595846)
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI at this commit and their suites did not run locally; Agent 7 built and tested on Linux only, which matters because this is a VS Code companion change that ships to all three platforms.
Not explored to full depth (tool budget reached): "agent 1b": none — but two things I verified statically rather than by execution, for the record: I did not run packages/web-shell/client/App.test.tsx or packages/vscode….
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/App.tsx:10020 — [probe] The two preflight toast gates disagree on identical input and the comment's stated premise is disproved by measurement
[Critical] R1-19 Still-standing blocker re-checked from the project's own triage gate (review 5124755540, CHANGES_REQUESTED, 2026-09-06). This is a ruling on an existing live review, not a new code defect found by this round, and it is about the pull request description rather than the diff. That review stops the pull request at a process gate: "the PR body doesn't follow the PR template, so I have to stop here and ask for a restructure first", and "The substantive code review happens once the body is complete". Checked against the live description at head 211f0b7, line by line against .github/pull_request_template.md, all five sections it named as missing are still absent: ## Why it's needed (template line 11 — the motivation is still folded into ## What this PR does as "The gap, end to end"), ### How to verify (template line 24 — the numbered 5-step plan is still under ## Reviewer Test Plan with no heading of its own), ### Evidence (Before & After) (template line 28 — still no screenshot or recording of the diff tab staying closed and the tool row unlocking; the pass counts under ### What was and was not run here do not show that, and the web-shell visual preview comment renders mock-daemon screenshots of the web shell rather than the companion interaction), ### Tested on (template line 32 — still no macOS / Windows / Linux table, which matters for an extension change and matters again this round because the windows-latest and macos-latest test lanes were skipped in CI at this commit and their suites did not run locally either), and the <details><summary>中文说明</summary> block (template tail — "完整翻译上面的英文正文,逐段对应,不要省略或缩写"). ## What this PR does, ## Reviewer Test Plan, ## Risk & Scope and ## Linked Issues are all present and substantive, and ### Environment (optional) is genuinely optional, so nothing else is outstanding. To clear it: restructure the description into the template and re-run @qwen-code /triage. The code findings in this review are all independent of this gate. The fix rests on one premise worth naming: this gate is the triage review's own, not this review's invention, and that review states "The code findings in this review are all recommendations and are independent of this gate" — so clearing the code findings does not clear it, and clearing it does not dismiss them.
— qwen3.8-max via Qwen Code /review (v0.23.0)
closeDiff() matches by path alone when no request id is given, which is how the IDE-mode MCP tool calls it. That close deleted a permission-bound entry before the tab closed, so the onDidCloseTextDocument -> cancelDiff hop found nothing and onDidClosePermissionDiff never fired: the web shell kept a locked approval row for an edit the user could no longer see, the #10557 symptom through a second door. Fire the dismissal from closeDiff itself when the caller supplied no id but the matched entry carries one. A caller that passed the id is the surface holding the request and has already cleared its own state, so that close still does not echo back. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtrow8ztpw
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-4 unwitnessed dismissal fan-out subscription (packages/vscode-ide-companion/src/extension.ts:251) — already reported (comment 3944217722)
- R1-10 no success-path log on the dismissal chain (packages/vscode-ide-companion/src/diff-manager.ts:407) — already reported (comment 3944217736, anchored at WebViewProvider.ts:2463)
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI at this commit and their suites did not run locally; Agent 7 built and tested on Linux only, which matters because this is a VS Code companion change that ships to all three platforms and the round's own findings are about test fidelity that no Linux lane can fully rule on.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 3)": could not finish tracing whether a companion-attached web shell can ever receive a non-UUID permission requestId — packages/acp-bridge/src/bridgeClient.ts:952 ….
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/vscode-ide-companion/src/diff-manager.ts:169 — [review] dispose() has no production caller, so the added emitter disposal never runs, and the new test comment attributes reload safety to the wrong mechanismpackages/vscode-ide-companion/src/diff-manager.ts:490 — [probe] A hand-close in the window after a native vote fires a dismissal for an already-decided request and re-exposes the vote affordance
Convergence: round 5 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 1 (1 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
[Critical] R1-19 Still-standing blocker re-checked from the project's own triage gate (review 5124755540, CHANGES_REQUESTED, 2026-09-06). This is a ruling on an existing live review, not a new code defect found by this round, and it is about the pull request description rather than the diff. That review stops the pull request at a process gate: "the PR body doesn't follow the PR template, so I have to stop here and ask for a restructure first", and "The substantive code review happens once the body is complete". Checked line by line against .github/pull_request_template.md at the live description for head c04b22d, all five sections it named as missing are still absent: ## Why it's needed (the motivation is still folded into ## What this PR does as "The gap, end to end"), ### How to verify (the numbered 5-step plan is still under ## Reviewer Test Plan with no heading of its own), ### Evidence (Before & After) (still no screenshot or recording of the diff tab staying closed and the tool row unlocking — the pass counts under ### What was and was not run here do not show that, and the web-shell visual preview comment renders mock-daemon screenshots of the web shell rather than the companion interaction), ### Tested on (still no macOS / Windows / Linux table, which matters for an extension change and matters again this round because the windows-latest and macos-latest test lanes were skipped in CI at this commit and their suites did not run locally either), and the <details><summary>中文说明</summary> block (the template tail asks to translate the English body paragraph by paragraph without omission or abbreviation). ## What this PR does, ## Reviewer Test Plan, ## Risk & Scope and ## Linked Issues are all present and substantive, and ### Environment (optional) is genuinely optional, so nothing else is outstanding. To clear it: restructure the description into the template and re-run @qwen-code /triage. The code findings in this review are all independent of this gate. The fix rests on one premise worth naming: this gate is the triage review's own, not this review's invention, and that review states "The code findings in this review are all recommendations and are independent of this gate" — so clearing the code findings does not clear it, and clearing it does not dismiss them.
— qwen3.8-max via Qwen Code /review (v0.23.0)
Four review findings from the last /review round, all of them gaps where a production line this PR ships could be reverted with the suite still green: - diff-manager.test.ts: the vscode.Uri mock rendered a `with()` copy with the original's scheme and query, so the left and the right side of one diff shared a single map key and every witness about which document a dismissal is keyed on was vacuous. `with()` now derives a new uri, `Uri.parse` exists for `closeAll()`, and a new case pins that the entry is keyed on the writable side. - diff-manager.test.ts: both id-less dismissal cases passed `suppressNotification = true`, leaving the default arm -- the one `IdeClient.disconnect()` and the MCP closeDiff tool actually send -- unpinned. - EmbeddedApp.test.tsx: the teardown half of the recovery path had no witness. One case now dismisses, tears the pending diffs down through the automatic approval mode, and asserts ownership returns to the host, no second closeDiff goes out for the tab the user already closed, and the same request can own a native diff again. Mutation-checked locally: gating the dismissal fire on `suppressNotification` fails 1/18, keying `addDiffDocument` on `leftDocUri` fails 5/18, dropping the two reset lines in `closeOpenPermissionDiffs` fails 1/31, and dropping the `openPermissionDiffsRef` delete in the dismissal handler fails 1/31. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmts86p0gqr
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- the rewind sessionId assertion cannot discriminate submission.sessionId from the runtime.sessionId fallback (packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1341) — already reported as R1-6 (comment 3944217729)
- duplicated permission-block fixture (packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:999) — already reported as R1-5 (comment 3944217727), which the author declined with recorded reasons
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI and their suites did not run locally; Agent 7 built and tested on Linux only, which matters because this is a VS Code companion change that ships to all three platforms.
Not reviewed: reverse audit — stopped before round 8 by the review time budget.
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/vscode-ide-companion/src/diff-manager.test.ts:61 — [probe] The added Uri.parse mock member is unreachable and its…packages/vscode-ide-companion/src/diff-manager.test.ts:99 — [probe] R5-1: (fix-induced) lastOpened*Uri() helpers return the…packages/vscode-ide-companion/src/diff-manager.test.ts:354 — [probe] hasExistingDiff's request-id comparison has no test at allpackages/vscode-ide-companion/src/diff-manager.test.ts:444 — [probe] Nothing pins which same-path entry an id-less close selectspackages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:46 — [probe] RewindSnapshotStub hand-copies an exported SDK type…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:55 — [probe] Hoisted rewind mocks lost their defaults and leak…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1024 — [probe] The typeof-requestId half of the new dismissal gate has no…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1085 — [probe] Request-id keying of the reopen suppression is never…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1102 — [probe] The ownership-recovery reset is unwitnessed for its…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1114 — [probe] The modeChanged else arm and the modeInfo fallback are…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1134 — [probe] The teardown witness dispatches modeChanged with no…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1173 — [probe] The forged-source witness misses the map delete behind the…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1334 — [probe] The newest-snapshot reduce arm of prepareSubmit is dead in…packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1354 — [probe] The rewind witness cannot pin the clientId leg of the call
[Critical] R1-19 Still-standing blocker re-checked from the project's own triage gate (review 5124755540, CHANGES_REQUESTED, 2026-09-06). This is a ruling on an existing live review, not a new code defect found by this round, and it is about the pull request description rather than the diff. That review stops the pull request at a process gate: "the PR body doesn't follow the PR template, so I have to stop here and ask for a restructure first", and "The substantive code review happens once the body is complete". Checked heading by heading against .github/pull_request_template.md at the live description for head 637a3e9, the same five sections are still absent: ## Why it's needed (the motivation is still folded into ## What this PR does as "The gap, end to end"), ### How to verify (the numbered 5-step plan is still under ## Reviewer Test Plan with no heading of its own), ### Evidence (Before & After) (still no screenshot or recording of the diff tab staying closed and the tool row unlocking — the pass counts under ### What was and was not run here do not show that, and the web-shell visual preview comment renders mock-daemon screenshots of the web shell rather than the companion interaction), ### Tested on (still no macOS / Windows / Linux table, which matters for an extension change and matters again this round because the windows-latest and macos-latest test lanes did not run here either), and the <details><summary>中文说明</summary> block (the template tail asks to translate the English body paragraph by paragraph without omission or abbreviation). ## What this PR does, ## Reviewer Test Plan, ## Risk & Scope and ## Linked Issues are all present and substantive, and ### Environment (optional) is genuinely optional, so nothing else is outstanding. To clear it: restructure the description into the template and re-run @qwen-code /triage. The code findings in this review are all independent of this gate. The fix rests on one premise worth naming: this gate is the triage review's own, not this review's invention, and that review states "The code findings in this review are all recommendations and are independent of this gate" — so clearing the code findings does not clear it, and clearing it does not dismiss them. The gate is the triage review's own and is independent of the code findings — review 5125593364 states "The code findings in this review are all recommendations and are independent of this gate". Clearing the code findings does not clear it, and clearing it does not dismiss them. The triage gate itself: re-running @qwen-code /triage after the restructure must clear review 5124755540's process gate rather than stopping at it again.
— qwen3.8-max via Qwen Code /review (v0.23.0)
…ast + chain logs - extension.test.ts: drive the listener registered on onDidClosePermissionDiff and assert the fan-out reaches every permission-aware provider (the only product code connecting the two ends of the dismissal chain). - App.test.tsx: witness the queued-submit half of the #9911 preflight toast, which the immediate-path case never exercised. - extension.ts / WebViewProvider.ts: log the two silent drop points on the dismissal chain (no provider, no webview) so a "closed the tab, row stayed locked" field report can be triaged. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmttf1wj0so
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 37 passed · 0 failed · 37 total Flakiness gate: ✅ 7 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:37 通过 · 0 失败 · 37 总计 抖动门:✅ 7 changed test file(s) x 5 identical rounds, no divergence Verification reportSandboxed verification: ❌ not passed — findings reported (agent verdict) — follow-up round Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 37 passed · 0 failed · 37 total 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)· 后续轮本轮为后续轮:上一轮验证于
Verification reportPR #11171 — deep verification (follow-up round)Verdict: Previous-finding status (re-measured at
|
| # | finding | severity | status at new head | evidence this round |
|---|---|---|---|---|
| F1 | the narrow queued-toast guard (submissionSessionIsCurrent) is unpinned; the description's claim that a test caught its reuse is contradicted by measurement |
medium | stands | mutation A1 (swap in the full submissionOwnerIsCurrent) SURVIVED 835 passed (835), up from 799/799; sibling A2 on the immediate path also SURVIVED 835/835 |
| F2 | the user's ✕ (onDidCloseTextDocument → cancelDiff) is unwitnessed: deleting the trigger leaves every new test green |
medium | stands | mutation X3 (delete the subscription body) SURVIVED 149 passed (149); in-file positive control X4 (delete the fan-out loop) KILLED 1 failed | 148 passed, so the harness demonstrably can make extension.test.ts red |
| F3 | the dismissal fan-out's try/catch is unpinned, and the shipped test wraps listener calls in its own try/catch | low | stands | mutation X1 (rethrow) SURVIVED 149/149 |
| F4 | shouldAutoExpand now auto-expands a bare write tool; real, intentional, tested, absent from the description |
low | stands | mutation T1 (revert to the two base lines) KILLED by hands a pending bare-write row back to the shell already expanded (1 failed | 108 passed); isEditToolName at ToolGroup.tsx:153 matches edit|editfile|write|write_file|writefile |
| X2 | empty-registry log + early return unpinned | — | stands, correctly | SURVIVED 149/149; observability-only (an empty for loop is the same behaviour) |
| W1 | notifyPermissionDiffClosed's no-active-webview guard unpinned |
— | stands, correctly | SURVIVED 149/149; in-file control W2 (rename the message type) KILLED, so the file is collected and the survivor is real |
| C1 | correction: the description's mechanism for the narrow guard ("submitting moves composer identity") is wrong; the operative conjunct is the write-block term | — | stands | re-censused: the 6 composerSourceVersionRef.current += 1 writers are at App.tsx 3567, 12090, 12208, 12392, 12818, 16902 (line numbers shifted by the merge), all session/workspace-switch events, none on the submit path |
| C2 | correction: test counts in the description are stale | — | stands, wider | at this head: companion 149 (5 files), App.test.tsx 835, ToolGroup.test.tsx 109 (web-shell total 944); the body still says 31 / 746 / 106 |
| R3-14 | declined by the author as a behaviour decision recorded on the issue | — | declined-with-rationale; I agree | unchanged; not re-litigated |
The previous round's two measured candidate fixes (App.guard.test.tsx for F1, extension.trigger.test.ts for F2) were not re-applied this round; both findings stand on their own mutations, and the fixes remain candidates, not measurements, at this head. Recorded under Not covered.
Central claim and A/B load-bearing proof
Central claim. Closing a host-owned permission diff by hand (✕, no vote) hands the edit preview back to the web shell: the tool row unlocks, renders the diff inline, and the tab does not spring back open.
Secondary claims. (a) voting from the diff editor must not trigger the dismissal; (b) a web-shell-initiated closeDiff must not echo back as a dismissal; (c) the #9911 preflight-rejection toast fires only while the user is still on the submission's session.
All three arms were re-run live for the capture, not reprinted from saved logs:
| arm | code | tests | result | oracle |
|---|---|---|---|---|
| ARM 0 (A/A control) | base e09c461a |
base tests | 117 passed (5 files) | base tree healthy; the A/B is not an environment artifact |
| ARM 1 (control) | base e09c461a |
HEAD tests | 12 failed | 137 passed (149) | the new behaviour is absent at base; failures name values (expected true to be false on hostOwnsEditDiffPreview, expected 0 to be greater than 0 on fan-out call count), not import errors |
| ARM 2 (head) | head 8b1ebb20 |
HEAD tests | 149 passed (5 files) | the fix restores every flipped test |
Witness: evidence/01-ab-base-vs-head.png — the three arms as printed, with the 12 flipped test names and both trees reported clean afterwards. Raw per-arm logs: raw/ab-arm0-base-base.log, raw/ab-arm1-base-headtests.log, raw/gate-companion-head.log.
The 12 flips are exactly the dismissal chain: 7 in diff-manager.test.ts, 3 in EmbeddedApp.test.tsx, 1 in extension.test.ts, 1 in WebViewProvider.test.ts — the same breakdown as the previous round.
Control hygiene. The base worktree (tmp/base-tree, removed after the cells were captured) had no node_modules, so @qwen-code/* would have resolved into the HEAD tree. @qwen-code/web-shell is changed by this PR, so it was re-pointed into the base tree and the realpath asserted (readlink -f → …/tmp/base-tree/packages/web-shell, distinct from head's …/packages/web-shell). core/acp-bridge/sdk are untouched by the PR (no package.json/lockfile in the diff) and were left resolving to the head builds. The base tree's web-shell lib entry had to be built (vite.lib.config.ts, raw/build-base-webshell-lib.log) because the base EmbeddedApp.test.tsx resolves the package entry at transform time; the base app build fails in a bare worktree on a tailwind resolution error (raw/build-base-webshell.log), which is an environment fact about the worktree, not about the PR. Per-package node_modules were symlinked from the head tree — safe because the PR changes no dependency manifest, and neither contains @qwen-code links, so the base interception still wins (asserted). Base production files were asserted byte-identical to HEAD^1 by sha256 (diff-manager.ts, extension.ts, EmbeddedApp.tsx, WebViewProvider.ts, App.tsx, ToolGroup.tsx), and base dist/index.js sha256 differs from head's.
Sibling sweep (diff-manager.sibling.test.ts in this artifact dir, 6 probes appended to a copy of the shipped suite; file total 24 passed, 0 red): acceptDiff on a request-bound diff stays quiet and really drops the entry; closeAll() stays quiet, really drops the entry, and a later cancelDiff hop still finds nothing; with two diffs open an id-less close reports exactly the request that lost its diff, once, leaving the unowned neighbour untouched; an id-less close on the unowned path stays quiet; a repeated cancelDiff plus the id-less door on the same gone diff fires exactly once; a matching-id close on a diff no approval owns stays quiet. All green at head.
Destination check (re-measured). hostOwnsEditDiffPreview plumbs App.tsx:2947 (default false) → customization App.tsx:3218 → useWebShellCustomization() at ToolGroup.tsx:1163 → locksPendingEditApproval (1169/1217) → approval={hostOwnsEditDiffPreview ? approval : undefined} at ToolGroup.tsx:1284. Unchanged from the previous round.
Corrections to the description
- C1 stands (see status table): the narrow toast guard's load-bearing conjunct is the write-block term, not composer identity. The refactor that composes
submissionOwnerIsCurrentonsubmissionSessionIsCurrent(App.tsx:10247-10256) removes the drift risk the description worried about, but not the coverage gap F1 names. - C2 stands and widened: every count in the body's "Run:" list is stale at this head (companion 149 vs "18+31+10",
App.test.tsx835 vs 746,ToolGroup.test.tsx109 vs 106). Expected drift across 19 commits; noted for the record.
Findings
F5 (new, medium-low) — the exact-request-id gate's two stateful conjuncts are unpinned, and one of them is the flag this PR makes stateful
Commit 19's new test pins the gate's id conjunct and the answers argument (see the delta section below). The same gate at App.tsx:12683-12688 has two further conjuncts that nothing exercises:
node tmp/mut-run.mjs … D3 # delete `!hostOwnsEditDiffPreview ||` -> SURVIVED 835 passed (835)
node tmp/mut-run.mjs … D4 # delete `request.hasDiffPreview !== true` -> SURVIVED 835 passed (835)Census: hostOwnsEditDiffPreview: false occurs 0 times in App.test.tsx (2 occurrences of : true), and hasDiffPreview occurs 0 times in App.test.tsx. Both conjuncts are live in production — hasDiffPreview is produced by transcriptAdapter.ts:46, and hostOwnsEditDiffPreview is exactly the value this PR turns from a constant true into state — so these are coverage gaps, not dead clauses. The sharp case: after a hand-back the flag is false, and the gate then refuses a host-relayed vote (correct — the host's tab is gone and the user votes inline); if a future refactor dropped that conjunct, a stale native vote would resolve an approval the host can no longer display and all 835 tests would stay green. No test votes through respondToPendingPermission after a hand-back.
Suggested fix (candidate, not applied, not measured this round)
One test in App.test.tsx: render with a pending permission, post permissionDiffClosed to hand the preview back, then call shellApi.respondToPendingPermission('req-1', 'allow') and assert it resolves false with submitPermission not called; and a second case with a block whose adapter record has hasDiffPreview: false, asserting the same refusal. Per the vacuity rule this ships with its mutation (D3/D4 must go SURVIVED → KILLED); neither was applied here, so the fixture that would pin this axis is named but not yet written.
F1 (carried, medium) — the narrow queued-toast guard is still unpinned
Re-measured: A1 SURVIVED 835 passed (835). The shipped queued test sets streamingState='responding' but never write-blocks the session, so the full guard evaluates true in the test and the two guards are indistinguishable there. The previous round's measured fixture (mid-flight write-block) remains the fix candidate; not re-applied this round.
F2 (carried, medium) — the user's ✕ is still unwitnessed
Re-measured: X3 SURVIVED 149 passed (149). The whole dismissal chain hangs on the pre-existing onDidCloseTextDocument → cancelDiff subscription at extension.ts:242; every shipped test calls cancelDiff directly. The in-file positive control X4 (delete the fan-out loop) was KILLED (1 failed \| 148 passed, red = forwards a closed permission diff to every permission-aware provider), which is what makes the survivor credible: the harness can make this file red. Pre-existing code, so the author is not blamed — but this PR is what makes the subscription load-bearing.
F3 (carried, low) — the fan-out try/catch is still unpinned
X1 (rethrow) SURVIVED 149/149. Mirrors the pre-existing vote fan-outs; a pre-existing pattern, not a PR-introduced hazard.
F4 (carried, low) — the bare-write auto-expand widening is real and pinned, but absent from the description
T1 KILLED by hands a pending bare-write row back to the shell already expanded. Deliberate and load-bearing for the hand-back; still a user-visible change for every web-shell host that the description never mentions. Note, not a defect.
The delta since the last round (commit 19)
Commit 19 adds one assertion: expect(mockSessionActions.submitPermission).toHaveBeenCalledWith('req-1', 'proceed_once', undefined) inside refuses a native edit approval vote bound to a different request id. It is load-bearing and correctly attributed:
| mutation | change | result | red test(s) |
|---|---|---|---|
| D1 | delete request.id !== requestId from the gate |
KILLED 1 failed | 834 passed |
refuses a native edit approval vote bound to a different request id (expected true to be false) |
| D2 | host-relayed vote forwards an answers object (handleConfirm(id, option, {relayed:'host'})) |
KILLED 2 failed | 833 passed |
the commit-19 test and the pre-existing submits allow_once for a native structured edit approval accept; assertion expected "spy" to be called with arguments: [ 'req-1', 'proceed_once', undefined ] |
D2's two reds are the point: the third argument is asserted as undefined by construction, so a host-relayed vote that ever started carrying answers would go red on both the new and the old test. (The matrix's killer-hit column prints NO for D2 only because that column string-matches the killer against a test name and my D2 killer string names the assertion; verified by hand from raw/mut-D2.log.)
Not covered
- Wire-handoff harness not re-measured this round. The previous round's two-process harness (real
WebViewProvider.notifyPermissionDiffClosed→ JSON round-trip → realEmbeddedAppMessageEvent) was not rebuilt at this head; budget went to the delta mutations and the F5 census instead. The property it proved is still pinned transitively: W2 (rename the producer's message type) is KILLED byrelays a permission diff dismissal to the webview under requestId, and M2/M3 pin the consumer's handling of exactly that payload shape. What is not re-proven this round is the structured-clone fidelity of the transport. - F1/F2 candidate fixes not re-applied at this head (see their rows).
- Real VS Code Extension Development Host / real TUI. Every harness drives the shipped fake
vscodeboundary or a syntheticMessageEvent; steps 2 and 5 of the Reviewer Test Plan (native tab staying closed, reopening on the next request) remain host-side and unrecorded, as the author states. - Playwright e2e (
test:e2e*) not run; repo-wide suite not run (only the two affected workspaces). - No trial merge into current
main. The snapshot'sbaseRefOid(1919ff97…) is not present locally and had already drifted fromHEAD^1; merge freshness is unmeasured. - Per-commit attribution. Depth-2 checkout: only the merge commit,
HEAD^1, andHEAD^2exist; the snapshot lists 19 commits. All results are for the aggregateHEAD^1..HEADdiff. - macOS / Windows. Not executed here or, per the author, in CI at this commit; F2-class and path-separator behaviour on those platforms remains CI's to confirm.
- A harness bug of mine, disclosed. The first mutation runner classified every mutation SURVIVED because it anchored on
/^\s*Tests/while vitest colourises that label (ESC[2m Tests ESC[22m). The runs themselves were valid (exitcodes andmutatedShaDifferswere correct); only the classification was broken.tmp/reparse-muts.mjsrecomputes from the raw logs and additionally requires a KILLED row to name a failing test and an assertion, so an exit 1 from a collection error cannot masquerade as a kill.mut-run.mjsin the artifact dir carries the fix. D1 was re-run after its anchor was corrected (my indentation error,occurrences=0), and is reported from the re-run.
Methodology
Environment: the CI verify container (node v22.23.2, no GitHub token), tree = refs/pull/11171/merge at depth 2 with npm ci + npm run build pre-existing at HEAD. The A/B used one scratch git worktree under tmp/ at HEAD^1 with internal @qwen-code/* links re-pointed and realpath-asserted per arm, its web-shell lib built in place, removed after the cells were captured. Harnesses drove real production code with only the external vscode API and the webview transport faked: the real DiffManager, real activate() listener, real WebViewProvider, real EmbeddedApp render, real web-shell ToolGroup. The mutation runner applies one exact-string mutation, asserts the anchor occurs exactly once, runs the named suite, records exit/status/failing-test/assertion lines, then restores and re-verifies by sha256; every row is in raw/mut-*.jsonl and raw/mut-*.log, and the corrected matrix is raw/mutation-matrix.txt with its witness at evidence/02-mutation-matrix.png. Gates: vitest per affected workspace (companion 149, web-shell 944), tsc --noEmit per package (both exit 0), eslint over all 13 changed files (exit 0) with a planted-unused-variable liveness control that was reported and then restored by sha256. The fail: 0 in assertions.json is honest: no executed assertion failed; the verdict is findings because F5 and the carried F1–F4 are concrete, reviewer-relevant coverage and description gaps. The 37 counted assertions are: 8 A/B and control-hygiene checks (three arm outcomes, ARM 1's failures naming values, base realpath, six base-file sha256 identities, base-vs-head dist sha, post-arm tree cleanliness); 6 gates (companion 149, web-shell 944, two tsc --noEmit, eslint over the 13 changed files, eslint planted-violation liveness); 15 mutation rows whose recorded status matched a stated expectation (M1–M4, X1–X4, W1, W2, T1, A1, A2, D1, D2); the 6 sibling probes S1–S6; and 2 census checks (hostOwnsEditDiffPreview: false and hasDiffPreview each occurring 0 times in App.test.tsx). The D3/D4 mutation runs are probes with no prior expectation, so they are reported as F5's evidence rather than counted as pass/fail assertions.
Evidence images
Harness scripts (mut-run.mjs, reparse-muts.mjs, mutations.json, diff-manager.sibling.test.ts) and raw logs are in this directory.
— Qwen Code · sandboxed verification
Flakiness gate log
rounds=5 files=7 skipped=0
file packages/vscode-ide-companion/src/commands/index.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/commands/index.test.ts
file packages/vscode-ide-companion/src/diff-manager.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/diff-manager.test.ts
file packages/vscode-ide-companion/src/extension.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/extension.test.ts
file packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/EmbeddedApp.test.tsx
file packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/providers/WebViewProvider.test.ts
file packages/web-shell/client/App.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/App.test.tsx
file packages/web-shell/client/components/messages/ToolGroup.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/messages/ToolGroup.test.tsx
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/vscode-ide-companion/src/commands/index.test.ts: PPPPP
packages/vscode-ide-companion/src/diff-manager.test.ts: PPPPP
packages/vscode-ide-companion/src/extension.test.ts: PPPPP
packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: PPPPP
packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: PPPPP
packages/web-shell/client/App.test.tsx: PPPPP
packages/web-shell/client/components/messages/ToolGroup.test.tsx: PPPPP
verdict: pass
summary: 7 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 1 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 2 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 3 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 4 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 5 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 5 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 5 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 5 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 5 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 5 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
Re-running at Template ✓ — I checked the live body heading by heading against Problem: observed, not theoretical. #10557 is open and carries Direction: aligned. This is a correctness fix in two first-party surfaces — the VS Code companion and the web shell — on the permission flow, which is the thing users actually feel. Nothing here adds a customization knob or a new public contract; Size: core scope, by the cross-package rule — the diff spans
163 production lines is well under the 500-line escalation threshold, and the title is Approach: the scope feels right, and it matches what I would have written independently. Faced with "the host owns the preview and the preview just vanished", the minimal fix is a typed close event on Two scope notes, neither a blocker and neither worth a tenth round:
Risk: no elevated risk signals. Stage 1e matched nothing — none of the changed files are in the revert-correlated set ( Moving on to code review. 🔍 中文说明在 模板 ✓ —— 我把线上正文逐个标题对照 问题: 已观测到的,不是理论性的。#10557 处于 open,带有 方向: 对齐。这是两个第一方界面(VS Code companion 与 web shell)上权限流程的正确性修复,而权限流程正是用户真正会感知到的东西。这里没有新增任何定制开关或公共契约; 规模: 属于核心范围,依据是跨包规则 —— diff 横跨 163 行生产代码远低于 500 行的升级阈值,标题是 方案: 范围合理,也与我独立会写出的方案一致。面对「宿主拥有预览、而预览刚刚消失了」这个问题,最小修复就是在 两点范围说明,都不是阻断项,也不值得为此走第十轮:
风险: 无升级风险信号。Stage 1e 没有命中任何一项 —— 改动的文件都不在与 revert 相关的那组路径里( 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewNo Critical findings at My independent proposal, written before reading the diff: put a typed close event on The chain, walked end to end. The last run's reason for deferring was that the host-side behaviour had no witness and "no lane in this repository can produce one". Walking the actual code narrows that a great deal, because almost none of the chain is new:
So the only genuinely new host-side dependency is one The three paths that must not fire — each checked, not assumed.
I am striking the round-8 deferred item about Why the newly-dynamic Non-blocking, noted and not requested. The two #9911 toast gates still disagree: the The hand-back flowsequenceDiagram
participant P1 as User
participant P2 as DiffManager
participant P3 as extension.ts
participant P4 as WebViewProvider
participant P5 as EmbeddedApp
participant P6 as ToolGroup
P1->>P2: closes the native diff tab by hand
P2->>P2: onDidCloseTextDocument then cancelDiff
P2->>P3: fire onDidClosePermissionDiff with requestId
P3->>P4: notifyPermissionDiffClosed
P4->>P5: postMessage permissionDiffClosed
P5->>P5: drop the request, mark it dismissed
P5->>P6: hostOwnsEditDiffPreview false
P6->>P1: row unlocks and renders the diff inline
Note over P5,P6: the next permission request resets ownership to the host
Files changed (13 of 13)
Test evidence — the PR's own CI at
|
| Check | Conclusion |
|---|---|
| Test (ubuntu-latest, Node 22.x) | success |
| Lint & Static (ubuntu-latest, Node 22.x) | success |
| Integration Tests (no-AK, No Sandbox) | success |
| web-shell E2E Smoke (ubuntu-latest, Node 22.x) | success |
| Capture web-shell visuals (ubuntu-latest, Node 22.x) | success |
| Desktop Shell (ubuntu-22.04) | success |
| Desktop Shell (windows-2022) | success |
| Classify PR | success |
| Test (macos-latest, Node 22.x) | skipped |
| Test (windows-latest, Node 22.x) | skipped |
| Integration Tests (CLI, No Sandbox) | skipped |
The other 31 checks are skipped bot-orchestration jobs (assign, route, label, authorize, review-pr, publish-resolution, and similar). Table region maintained by the finalize workflow.
On the two skipped platform lanes — this is a repo condition, not a gap in this PR. Both prior runs treated the missing macOS and Windows confirmation as something owed before merge. It is not owed by this author, and it cannot be: test_macos and test_windows in ci.yml are gated on merge_group || schedule || workflow_dispatch, so they never run on pull_request at all. The workflow's own comment says why, and it is worth quoting because it settles the question:
The macOS and Windows lanes' ONLY remaining trigger, and therefore this repository's only signal about a host that is not Linux with a GNU userland. Those two are otherwise gated on
merge_group, and the merge queue is not enabled here — no queue run since 2026-07-02 — while their pull-request trigger is off until the standing Windows failures are fixed. A regression therefore surfaces here, one day later, onmain, and nowhere else: treat a red nightly as a blocker, not as noise.
There is no merge queue, so merge_group never fires either. The nightly on main is the only place those two lanes run, for every PR in this repository. Deferring this PR for platform evidence would be asking the author for something no lane can produce, and the Windows trigger is off precisely because main already has standing Windows failures that are not this PR's. Note also that Desktop Shell (windows-2022) is green but is not evidence for these files — that lane compiles and tests the Tauri crate, and this diff contains no Rust.
Against that, what does exist: the Linux unit lane covers all thirteen changed files; the author ran the five vscode-ide-companion suites on macOS arm64 / Node 22 at this head and reports 163 passed (their claim, not something I re-ran); and the two web-shell files pass in the green Linux lane. Reviewing the diff, nothing in it is platform-sensitive — the additions are an EventEmitter, React state and refs, a postMessage, and a lowercased string comparison. The pre-existing path handling in closeDiff is untouched.
Not verified, and why: there is no capture from a VS Code Extension Development Host, so steps 2 and 5 of the test plan — the native tab staying closed, then reopening on the next request — rest on the code walk above rather than on an observation. I am satisfied by that walk for the reason in the first section: the trigger hop is pre-existing and already load-bearing for ide/diffClosed. Windows is unverified by anyone, and per the above the nightly is the designed place it would surface.
Sandboxed verification would settle part of this: @qwen-code /verify — that the web-shell half of the hand-back (row unlocks, diff renders inline, the vote still lands from the row) is load-bearing against the base build is assertable A/B, and it would also confirm the #9911 toast actually fires rather than being gated away, which no current test pins on the false branch. It cannot reach the native tab: nothing in this repository drives an Extension Development Host, so the host-side half stays a code-walk conclusion no lane can upgrade. You have write access, so /tmux is also available, but this is a VS Code webview surface rather than a TUI one and it would not add signal here.
中文说明
代码审查:在 dc665a60 上未发现 Critical。 在一个已经走到第十轮的 PR 上我刻意不再新增 Suggestion —— AGENTS.md 的规定是超过约 5 轮之后只落地 Critical 修复、其余延后,而上一轮 /review 确认的那十条 Suggestion 已经记录在这个 thread 里了。下面写的是我核实过的东西,因为这一轮的价值在于补上上一轮留下的缺口,而不是往堆里再加东西。
我在读 diff 之前独立写下的方案是:在 DiffManager 上加一个带类型的关闭事件,通过投票命令已经在用的那个 registry 分发出去,并在 shell 一侧把它当作「交还」而不是「投票」处理 —— 从 open-diffs map 里移除该请求、让那一行不再被锁、并且不重新打开用户刚刚关掉的标签页。这个 PR 就是这么做的。我没有找到它漏掉的更简路径。
整条链路我逐段走过。 上一轮延后的理由是宿主侧行为没有见证、而「本仓库没有任何 lane 能产出这样的见证」。真去读代码之后,这个说法要收窄很多,因为这条链上几乎没有一段是新的:
EmbeddedApp发出带data.requestId的openDiff→FileMessageHandler.handleOpenDiff映射成permissionRequestId→showDiffCommand→showDiff存进DiffInfo。全部既有,本 PR 没有碰这个方向。- 用户点标签页关闭 →
onDidCloseTextDocument(以DIFF_SCHEME为门)→cancelDiff。既有接线,未改动。 cancelDiff在closeDiffEditor删除条目之前读到diffInfo,发出ide/diffClosed—— 现在还用同一个diffInfo发出onDidClosePermissionDiff。这是承重点:新的 fire 恰好挂在已上线的ide/diffClosed通知所依赖的那个对象上,所以只要 CLI 现有的关闭 diff 流程是通的,它就一定会触发。extension.ts→chatProviderRegistry.getPermissionAwareProviders()→notifyPermissionDiffClosed→sendMessageToWebView→getActiveWebview().postMessage。与已经在工作的webShellPermissionDecision、permissionResolved是同一个调用、同一条通道。不需要新增中继,也不存在漏掉新类型的白名单。EmbeddedApp的处理器以event.source === window.parent做来源门禁,与决策处理器一致 —— 这是对的,因为 MCP app 和 artifact 预览是这个 webview 内部可执行脚本的 iframe。hostOwnsEditDiffPreview=false→Appcustomization →ToolGroup.isHostOwnedEditApproval=false→locksPendingEditApproval=false→ 那个已经把locksPendingEditApproval列进依赖数组的useEffect重算expanded→ 该行展开,approval抵达内联渲染器。
所以真正新增的宿主侧依赖,只是一个已经会在标签页关闭时被抵达的函数里多了一次 EventEmitter.fire。这比「宿主侧行为」这个说法所暗示的未验证面要小得多。
三条不该触发的路径 —— 每一条都是查过的,不是假设的。
qwen.diff.accept/qwen.diff.cancel:两者都只在if (docUri && isManagedDiff && !permissionRequestId)下才调用acceptDiff/cancelDiff。绑定请求的 diff 永远到不了cancelDiff,所以从 diff 编辑器投票不会作为撤销回声传回来。这个守卫是既有的,不是本 PR 新加的。- web shell 主动发起的
closeDiff:closeDiffEditor在vscode.window.tabGroups.close之前删掉 map 条目,因此随后的onDidCloseTextDocument→cancelDiff找不到diffInfo,提前返回。另外,closeDiff自己新增的 fire 以permissionRequestId === undefined为门,而这个调用方传了 id。两个独立的理由说明它不会成环。 - 投票之后才姗姗来迟的撤销:解锁以
webShellPermissionRequestIdRef.current === requestId为门,而updateTranscript一旦看到permissionToFocus离开该 id,就会同时重置dismissedPermissionDiffIdRef和hostOwnsEditDiffPreview。最坏情况是在一个已经解决的行上闪一帧,并且会自愈。
第 8 轮那条关于 closeAll() 的延后项,我划掉它,而且是我自己查过才划的,不是复述上一轮的理由。qwen.diff.closeAll 不在 package.json 的 contributes.commands 里 —— 没有命令面板入口、没有快捷键 —— 所以用户无法调用它。唯一的调用方是 WebViewProvider 的 permissionResponse 处理器,而它运行在 this.pendingPermissionResolve?.(optionId) 之后。在那里触发撤销,会解锁一个权限已经解决的行。不触发才是对的。
为什么变成动态的 hostOwnsEditDiffPreview 风险比看上去低。 这个 flag 的每一个消费者 —— App.tsx:3249(customization context)、App.tsx:12863(按 id 定向的宿主决策守卫)、ToolGroup.tsx:1170/1218/1285、ParallelAgentsGroup.tsx:587 —— 都把 false 读作「shell 拥有预览」。而 false 正是 App.tsx:2970 里的默认值,也就是所有非 VS Code 宿主一直在跑的取值:浏览器 web shell 和桌面 shell 长期以来都在生产环境里走这条分支。VS Code companion 是唯一把它钉死为 true 的消费者。所以这个 PR 不是打开了一条死分支,而是让一个宿主加入其他宿主早已在用的配置。这是这里最有力的一条安全性论证,而它在之前几轮里是缺失的。
非阻断,仅记录、不作为要求。 两处 #9911 toast 的门禁仍然不一致:prepareSubmit 那条路径用 admissionOwnerIsCurrent(),其中包含 composerSourceVersionRef 这一项;而排队那条路径用更窄的 submissionSessionIsCurrent()。我确认了前者不是死代码 —— startPreparing() 不会递增该版本号,唯一的递增点是 composer 编辑与会话切换 —— 所以常规情况下 toast 确实会触发;只是在预检在飞行途中用户开始打字时会被压掉。这就是 D4-1,第 4 轮已报告,两种取舍都说得通。另外描述里值得补一句:ToolGroup.tsx 里 shouldAutoExpand → isEditToolName 的替换对裸名 write 别名是承重的,不是清理(见 Stage 1)。
测试证据。 我没有构建或运行本 PR 的任何代码;按 triage 规则,审查是静态的,测试信号来自 PR 自己的 CI、通过 API 读取。head commit 上共 42 个 check:15 success、27 skipped、0 failure、0 pending,没有失败的 job,因此没有日志摘录可引。表格见上。
关于两条被跳过的平台 lane —— 这是仓库层面的状况,不是本 PR 的缺口。 之前两轮都把缺失的 macOS 与 Windows 确认当作合并前欠着的东西。它不欠在这位作者身上,也不可能由他来还:ci.yml 里的 test_macos 与 test_windows 以 merge_group || schedule || workflow_dispatch 为门,因此在 pull_request 上根本不会跑。workflow 自己的注释说明了原因,值得直接引用(原文见上方英文部分),因为它把这个问题结掉了:合并队列没有启用(自 2026-07-02 起就没有),所以 merge_group 也不会触发;这两个 lane 唯一的触发点是每日夜间跑在 main 上的那次,对本仓库的每一个 PR 都是如此。而 Windows 的 PR 触发器之所以关着,正是因为 main 上已经有与本 PR 无关的、长期存在的 Windows 失败。为了平台证据而延后这个 PR,等于要求作者提供一个没有任何 lane 能产出的东西。另外请注意 Desktop Shell (windows-2022) 虽然是绿的,但对这些文件不构成证据 —— 那条 lane 编译并测试的是 Tauri crate,而本 diff 不含 Rust。
在此前提下,现有的证据是:Linux 单元 lane 覆盖了全部十三个改动文件;作者在本 head 上于 macOS arm64 / Node 22 跑了 vscode-ide-companion 的五个套件并报告 163 passed(这是作者的说法,不是我复跑的结果);两个 web-shell 文件在绿色的 Linux lane 中通过。就 diff 本身来看,其中没有任何平台敏感的东西 —— 新增的是一个 EventEmitter、React state 与 ref、一次 postMessage,以及一个小写化后的字符串比较。closeDiff 里既有的路径处理未被改动。
未验证的部分及原因: 没有 VS Code Extension Development Host 的录证,所以测试计划的第 2 步和第 5 步 —— 原生标签页保持关闭、并在下一次请求时重新打开 —— 依赖的是上面那次代码走查,而不是一次观测。我对这个走查是满意的,理由见第一节:触发它的那一跳是既有的,并且已经承载着 ide/diffClosed。Windows 无人验证,而按上述说明,夜间 lane 正是它按设计会暴露的地方。
沙箱验证能结掉其中一部分:@qwen-code /verify —— 交还机制在 web-shell 这一半(行解锁、diff 内联渲染、投票仍能从该行落地)相对基线构建是否承重,是可以做 A/B 判定的;它还能确认 #9911 的 toast 真的会触发、而不是被门禁挡掉,这一点目前没有任何测试钉住 false 分支。它够不到原生标签页:本仓库没有任何东西驱动 Extension Development Host,所以宿主侧那一半仍然是一个没有 lane 能升级的代码走查结论。你有写权限,因此 /tmux 也可用,但这是 VS Code webview 界面而非 TUI 界面,它在这里不会增加信号。
— Qwen Code · qwen3.8-max-2026-09-02
Reviewed at dc665a602f686b935e080479bdeec3b7b2d83f16 · re-run with @qwen-code /triage
|
Confidence: 4/5 — clean across every stage; the one point I am withholding is the platform evidence no lane in this repo can produce, and that is a repo condition rather than a doubt about this diff. Stepping back. The production change is 163 lines across six files and it does one thing: it gives a closed permission diff a channel back to the surface that was waiting on it. Everything I traced behaves the way the description says, including the three paths that must not fire — and two of those three are guarded by code that predates this PR, which is the strongest thing I can say about a fix like this: it hangs off invariants that already had to hold. The question that made me defer at The second thing that moved me is one I should have caught earlier. On scope. #10585 belongs here — twelve witnesses on exactly the paths this changes. #9911 does not, strictly: it is a different user-visible behaviour on a shared submit path, and the two toast gates still disagree about when to speak. I am not asking for a split at round ten, and I am not deferring over it either. The re-run rule is explicit that scope and hygiene concerns get noted and the PR gets approved, and that is the right call here: the cost of an eleventh round exceeds the value of a tidier history, and D4-1 has been on the record since round four without anyone arguing it is a defect. The stale-review loop, which is the reason you re-triggered. Your reading was correct and mine was over-cautious. Two things I want on the record rather than buried, neither of which blocks:
If I had to maintain this in six months I would read the event name, read the two guards, and know what it does. The comments explain the why at exactly the three places where the why is non-obvious — the id-less close branch, the source gate, and the narrower toast guard — and nowhere else. That is the house style, and it is what makes a state-handoff fix across an extension boundary reviewable at all. Approving. ✅ Postscript —
#11628 is complementary to this PR's #9911 half, not a duplicate of it. I checked instead of assuming, because the titles overlap, the subject is the same, and it is by the same author. #11628 localized the throw sites in the companion's rewind preflight and added a new One correction falls out of that merge: the description says How to read the approval, given this. The merge is not gated on me in any case. 中文说明Confidence: 4/5 —— 每个阶段都干净;我扣掉的那一分是平台证据,而本仓库没有任何 lane 能产出它,那是仓库层面的状况,不是对这个 diff 的疑虑。 退一步看整体。生产代码改动是六个文件里的 163 行,只做一件事:给一个被关闭的权限 diff 一条回到正在等待它的那个界面的通道。我追过的每一处都按描述所说的方式行为,包括那三条不该触发的路径 —— 而这三条里有两条是由早于本 PR 的代码守卫着的。对于这样一个修复,这是我能给出的最强评价:它挂在一些本来就必须成立的不变量上。 让我在 第二件让我改变判断的事,是我本该更早发现的。 关于范围。#10585 属于这里 —— 十二处见证,正好在本 PR 改动的路径上。严格说 #9911 不属于:它是共享提交路径上另一处用户可见行为,而且两处 toast 门禁对于「什么时候该说话」至今仍不一致。我在第十轮不要求拆分,也不会为此延后。重跑规则写得很清楚:范围与整洁性问题记下来、然后批准 PR,而这里这正是对的判断 —— 第十一轮周期的代价超过了一段更整洁历史的收益,而 D4-1 自第四轮起就在记录上,从来没有人主张它是一个缺陷。 关于那个过期 review 的循环,也就是你重新触发的原因。你的解读是对的,我上次过于谨慎了。 有两件我要写进记录、而不是埋起来的事,都不构成阻断:
如果六个月后这东西要由我来维护,我读一下事件名、读一下那两处守卫,就知道它做什么。注释恰好在「为什么」并不显而易见的三个地方解释了为什么 —— id-less 的关闭分支、来源门禁、以及更窄的那个 toast 守卫 —— 别处一句都没有。这就是本仓库的风格,也正是一个跨扩展边界的状态交接修复之所以还能被审查的原因。批准。✅ 附记 —— 在这次审查撰写期间
#11628 与本 PR 的 #9911 那一半是互补关系,不是重复。 我是查过才这么说的,而不是假设 —— 因为两者标题重叠、主题相同、而且是同一位作者。#11628 把 companion 里 rewind 预检的抛出点本地化了,并新增了一条 由此得出一处需要更正的地方:描述里说 在这种情况下该如何看待这次批准。 无论如何,合并并不卡在我这里。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
doudouOUC
left a comment
There was a problem hiding this comment.
Round 9 re-review at 8b1ebb2
Scope: Static re-review of the same HEAD as round 8 (no new commits since then). severityFloor: auto → critical (round 9).
Standing Critical (R1-19): RESOLVED
The triage gate's template check (R1-19) is cleared. The PR body now carries all nine required headings — including ## Why it's needed, ### How to verify, ### Evidence (Before & After), ### Tested on with the macOS/Windows/Linux status table, and the <details><summary>中文说明</summary> block — updated at 14:52Z on 2026-09-09 after round 8 had already run. I confirm this by heading-by-heading comparison against .github/pull_request_template.md at the reviewed commit: each section named in the template is present. The two remaining CHANGES_REQUESTED reviews (5124755540, the template gate, and the round reviews behind it) should be dismissed by a maintainer so R1-19 stops re-arming on future rounds.
New Criticals: None found
The codebase is identical to round 7/8. I read the full diff (1705 lines across 13 files, +1382/−28) and examined every new code path in diff-manager.ts, extension.ts, EmbeddedApp.tsx, WebViewProvider.ts, App.tsx, and ToolGroup.tsx. No Critical-level issues were found — I concur with round 8's assessment.
Concurrence with round 8
Round 8 (qwen-code-ci-bot, CHANGES_REQUESTED) found no Criticals and reported 9 Suggestion-level findings, which are below the critical posting floor for round 9. I have verified the same production code and agree:
- The new onDidClosePermissionDiff event path is additive and scoped correctly
- The three must-not-fire guards (qwen.diff.accept/cancel blocked by !permissionRequestId; closeDiffEditor delete-before-close) are each pinned by tests
- The hostOwnsEditDiffPreview state management resets correctly on both the request-change path and closeOpenPermissionDiffs
Round 8 deferred three decisions to me (doudouOUC):
- Dismiss stale CHANGES_REQUESTED reviews — the code findings in those reviews are not Criticals, the template gate is cleared, and a maintainer should dismiss them to stop the process loop.
- Host-side evidence before merge — the unit witnesses for the web-shell half are solid (tool-group unlocking, inline diff rendering, no reopen). The host-side behaviour (native tab staying closed, reopening on next request) has no automated witness because no lane in this repository drives a VS Code Extension Development Host. Whether that blocks merge is a maintainer judgement call; I do not consider it a review blocker given the test coverage and the additive nature of the change.
- #9911 toast riding along — this is a separate user-visible behaviour on a shared submit path. The toast is narrow (fires only for the session the submission belonged to) and is pinned by two tests including the session-switch case. I would keep it in the PR as a quality-of-life fix on the same code path; splitting it out is a judgement call for the author and maintainer.
Test coverage
The test-to-production ratio (~7.5:1) is appropriate for a state-handoff fix across an extension boundary. diff-manager.test.ts (18 passed), EmbeddedApp.test.tsx (31 passed), commands/index.test.ts (10 passed), ToolGroup.test.tsx (106 passed) each witness distinct paths. extension.test.ts (R3-7, R3-8) was not collected on this machine (missing core/acp-bridge builds) and remains CI's to confirm.
Verdict: No Criticals. R1-19 resolved. Concur with round 8.
— qwen3.8-max via Qwen Code /review (v0.22.0)
doudouOUC
left a comment
There was a problem hiding this comment.
Two-stage review summary for PR #11171
Round 1 (deepseek-v4-flash): no new Critical findings; one standing triage gate (R1-19) is resolved.
Round 2 (qwen3.8-max): no new Critical findings; four Suggestion-level items and two severity corrections are noted below.
Overall verdict: ISSUES_FOUND_R2 — blocker-free, but the second pass surfaces additional concerns the first pass missed.
Suggestions from round 2
-
App.tsx:10247— misleading comment for the load-bearing guard.
The comment attributes the narrowersubmissionSessionIsCurrent()guard to composer-identity movement caused by submitting, but that mechanism does not exist on the submit path. The narrowing is genuinely load-bearing throughsessionWriteBlocked = Boolean(connection.loadingTranscript)(App.tsx:3234). The code is correct; the comment should describe the actual conjunct or be removed. -
Toast tests only exercise the TRUE branch.
App.test.tsx:20116and:20142render an unswitched session, so removing or forcing the guard true leaves both tests green. The suppression path (the guard's purpose) has no coverage. -
extension.ts:251—trywraps the whole provider loop.
A throw from an early provider silently skips later providers in the same fan-out. While consistent withqwen.diff.accept/cancel, the comment overstates the protection. -
Silent-drop log branches are unwitnessed.
extension.ts:260(empty provider list),extension.ts:271(catch), andWebViewProvider.ts:2469(no active webview) have no test coverage.
Corrections to the standing record
closeAll()never firing the dismissal is real code-wise but has no reachable failure path; treat as latent, not user-facing.- Ungated
modeChangedrevoking a dismissal is confirmed, but the test suite now witnesses the teardown as intentional behavior — this is a threat-model asymmetry rather than an oversight.
Already resolved since round 8
diff-manager.ts:407"no test sends an id" — covered bydiff-manager.test.ts:257/276.- R1-19 triage gate — PR body now carries every required heading.
- Source gate and negative
requestIdcases are genuinely witnessed.
|
Closeout sweep — pinning down the one remaining blocker, because it is now a one-click maintainer action rather than author work. The sole standing Critical on this PR is The bot already ruled on this itself in triage stage 1 (comment, 2026-09-09T15:12:39Z): "The template gate that stopped this PR on 2026-09-06 (review Re-verified independently at the same head
All nine template headings plus the optional Why
What is needed: a maintainer to dismiss review Deliberately not firing either bot command again:
One genuine gap that remains, and it is not the body gate: the |
Resolve diff-manager closeDiff conflict: keep main's resolvedFilePath exact-form matching and add the PR's permission-diff dismissal via openDiff.permissionRequestId; adapt the refactored test mock (vi.hoisted + missing EventEmitter.dispose / openTextDocument default) to the PR's disposal coverage. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtvxsd4xwa
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
10 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-1 inert new commands/index.test.ts cases - already reported (comment 3944217715)
- R1-5 duplicated permission-block fixture and permissionBlock name collision - already reported (comment 3944217727)
- R1-10 per-provider drop-point log fires on success and is mis-tagged [Extension] - already reported (comment 3944217736)
- R1-15 both new toast guards witnessed only on the true branch - already reported (comment 3944217749)
- R1-16 ParallelAgentsGroup conditional has no witness for the newly dynamic prop - already reported (comment 3944217755)
- R1-17 sub-agent control render passes the same flag value as the case render - already reported (comment 3944217759)
- D3-1 hand-back witness never flips the prop on a mounted row - already reported (round-3 deferred list, review 5131595846)
- D4-1 the two preflight toast gates disagree on identical input - already reported (round-4 deferred list, review 5135150418)
- D5-1 dispose() has no production caller, so the added line and its witness cover a dead path - already reported (round-5 deferred list, review 5135780307)
- D6-1 the newest-snapshot reduce arm of prepareSubmit is untested - already reported (round-6 deferred list, review 5140664973)
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; this review built and tested on Linux only, which matters for a VS Code companion change that ships to three platforms and because the round-2 Critical R2-1 was a Windows-only test failure no Linux lane could catch.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally; this review built and tested on Linux only.
Not reviewed: verifier probes — qwen review scratch-tree was unavailable (the repository's local git config carries includeIf entries pointing at a missing git-credentials file); verifiers hand-rolled isolation in /tmp copies and in-memory vitest mutants and confirmed the shared worktree stayed clean, but two findings (T9-4, T9-6) could not be probed at all and rest on reading.
Not explored to full depth (tool budget reached): "agent 1d": none — I did not run vitest or tsc over the touched packages (static scan only, per my dimension); test-execution evidence belongs to the walk/verifier agen…; "agent reverse-audit (round 5)": I did not walk WebViewProvider.webShellPermissionOwners registration and teardown ( :205-215 , :940-960 , :1940-1975 ) against the new dismissal routing end…; "agent reverse-audit (round 5)": I did not read the modeChanged handler in EmbeddedApp.tsx that the new R5-3 test drives, so the teardown-reset assertions in that test are unverified agains…; "agent reverse-audit (round 5)": I ran no test suite and no check-types ; every claim above is from reading source at the reviewed state, not from execution.; "agent reverse-audit (round 4)": did not execute the web-shell or vscode-ide-companion suites, so the t('composer.editExpired') → 'The original message can no longer be edited.' string the ….
Not reviewed: reverse audit — stopped before round 7 by the review time budget.
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:
packages/vscode-ide-companion/src/commands/index.test.ts:277 — [review] the openDiff wire-key to permissionRequestId hop (FileMessageHandler.handleOpenDiff) has no testpackages/web-shell/client/App.tsx:10427 — [review] the queued-path catch wraps enqueuePreparedPrompt, so a post-enqueue host failure toasts 'not submitted'packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx:1158 — [review] the two new describes re-derive latestProps()/dismiss() helpers the file already haspackages/vscode-ide-companion/src/diff-manager.test.ts:489 — [review] four byte-identical createManager() copies plus two identical new beforeEach blocks
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 10 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; this review built and tested on Linux only, which matters for a VS Code companion change that ships to three platforms and because the round-2 Critical R2-1 was a Windows-only test failure no Linux lane could catch.
未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally; this review built and tested on Linux only.
未审查(原文为英文):verifier probes — qwen review scratch-tree was unavailable (the repository's local git config carries includeIf entries pointing at a missing git-credentials file); verifiers hand-rolled isolation in /tmp copies and in-memory vitest mutants and confirmed the shared worktree stayed clean, but two findings (T9-4, T9-6) could not be probed at all and rest on reading.
未探索到全部深度(达到工具调用预算):"agent 1d":none — I did not run vitest or tsc over the touched packages (static scan only, per my dimension); test-execution evidence belongs to the walk/verifier agen…;"agent reverse-audit (round 5)":I did not walk WebViewProvider.webShellPermissionOwners registration and teardown ( :205-215 , :940-960 , :1940-1975 ) against the new dismissal routing end…;"agent reverse-audit (round 5)":I did not read the modeChanged handler in EmbeddedApp.tsx that the new R5-3 test drives, so the teardown-reset assertions in that test are unverified agains…;"agent reverse-audit (round 5)":I ran no test suite and no check-types ; every claim above is from reading source at the reviewed state, not from execution.;"agent reverse-audit (round 4)":did not execute the web-shell or vscode-ide-companion suites, so the t('composer.editExpired') → 'The original message can no longer be edited.' string the …。
未审查:反向审计——评审时间预算不足,未能开始第 7 轮。
Test Plan(非阻断):src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more。
收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.3)
|
Closeout follow-up. Two things changed since my 2026-09-10 sweep, and one of them removes the reason I gave for not re-running The blocking review is no longer at head. Review Current state at macOS evidence, since the That covers the VS Code companion half — the platform-sensitive part of this change. The two web-shell files were not run locally: Nothing in the diff changed. No new commits from me. |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 55 passed · 0 failed · 55 total Flakiness gate: 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:55 通过 · 0 失败 · 55 总计 抖动门: Verification reportPR #11171 — deep verification (follow-up round 3)Verdict: 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)· 后续轮 3本轮验证于
Previous-finding status (all re-measured at
|
| # | finding | severity | status at new head | evidence this round |
|---|---|---|---|---|
| F1 | the narrow queued-toast guard (submissionSessionIsCurrent) is unpinned |
medium | stands, and is wider | A1 (swap in the full submissionOwnerIsCurrent) SURVIVED 0 failed | 884 passed; in-file control A3 (delete the toast) KILLED 1 failed | 883 passed, so the harness demonstrably can make this file red. New: sibling A2 on the immediate path (admissionOwnerIsCurrent() → admissionSourceIsCurrent()) also SURVIVED 884/884, with its own control A4 KILLED — so the gap covers both toast sites, not one |
| F2 | the user's ✕ (onDidCloseTextDocument → cancelDiff) is unwitnessed |
medium | stands | X3 (gut the subscription body) SURVIVED 0 failed | 163 passed; in-file control X4 (delete the fan-out loop) KILLED 1 failed | 162 passed, red = forwards a closed permission diff to every permission-aware provider, assertion expected "spy" to be called with arguments: [ 'req-1' ] |
| F3 | the dismissal fan-out's try/catch is unpinned | low | stands | X1 (catch → throw err) SURVIVED 163/163 |
| F4 | shouldAutoExpand widened to a bare write; real, intentional, tested, absent from the description |
low | stands | T1 (revert to the two base lines) KILLED by hands a pending bare-write row back to the shell already expanded, assertion expected null not to be null; isEditToolName matches edit|editfile|write|write_file|writefile |
| F5 | the request-id gate's two stateful conjuncts are unpinned | medium-low | stands | D3 (delete !hostOwnsEditDiffPreview) SURVIVED 884/884; D4 (delete request.hasDiffPreview !== true) SURVIVED 884/884. Census re-run: in App.test.tsx, hostOwnsEditDiffPreview: false occurs 0 times (: true occurs 2), hasDiffPreview occurs 0 times. D1 (delete request.id !== requestId) KILLED, so the gate itself is live |
| X2 | empty-registry log + early return unpinned | — | stands, correctly | SURVIVED 163/163; observability-only — an empty for loop is the same behaviour |
| W1 | notifyPermissionDiffClosed's no-active-webview guard unpinned |
— | stands, correctly | SURVIVED 163/163; in-file control W2 (rename the message type) KILLED, so the file is collected and the survivor is real |
| C1 | correction: the code comment's stated mechanism for the narrow guard is wrong | — | stands | re-censused: the 6 composerSourceVersionRef.current += 1 writers are at App.tsx 3599, 12302, 12421, 12605, 13031, 17217 — every one inside a session/workspace-switch context (primaryCwd selection, setPendingSessionContext, selectedWorkspaceCwdRef assignment, standalone-context reset). None is on the submit path |
| C2 | correction: test counts in the description are stale | — | stands, wider | see C2 below |
| R3-14 | declined by the author as a behaviour decision recorded on the issue | — | declined-with-rationale; I agree | unchanged; not re-litigated |
The previous round's two candidate fixes (App.guard.test.tsx for F1, extension.trigger.test.ts for F2) were not re-applied; both findings stand on their own mutations. Recorded under Not covered.
Central claim and A/B load-bearing proof
Central claim. Closing a host-owned permission diff by hand (✕, no vote) hands the edit preview back to the web shell: the tool row unlocks, renders the diff inline, and the tab does not spring back open.
Secondary claims. (a) voting from the diff editor must not trigger the dismissal; (b) a web-shell-initiated closeDiff must not echo back as a dismissal; (c) the #9911 preflight-rejection toast fires only while the user is still on the submission's session.
| arm | code | tests | companion | web-shell | oracle |
|---|---|---|---|---|---|
| ARM 0 (A/A control) | base ae78d5b8 |
base tests | 131 passed (5 files) | 988 passed (2 files) | both trees healthy; the A/B is not an environment artifact |
| ARM 1 (control) | base ae78d5b8 |
HEAD tests | 12 failed | 151 passed (163) | 3 failed | 990 passed (993) | the new behaviour is absent at base; failures name values, not import errors |
| ARM 2 (head) | head dc665a60 |
HEAD tests | 163 passed (5 files) | 993 passed (2 files) | the fix restores every flipped test |
15 tests flip red → green between ARM 1 and ARM 2. Witness: evidence/01-ab-base-vs-head.png — the two decisive companion cells re-run live for the capture, with the four remaining cells printed from their saved logs and each cell labelled [LIVE] or [from saved log] so provenance is visible per row. Raw logs: raw/ab-arm0-companion-base-base.log, raw/ab-arm1-companion-base-headtests.log, raw/ab-arm2-companion-head-headtests.log, and the three web-shell equivalents.
The 12 companion flips are exactly the dismissal chain — 7 in diff-manager.test.ts, 3 in EmbeddedApp.test.tsx, 1 in extension.test.ts, 1 in WebViewProvider.test.ts — the same breakdown as the previous round at a larger suite. The 3 web-shell flips are surfaces a rejected preparation instead of cancelling silently and surfaces a queued preparation rejection instead of cancelling silently (both expected "spy" to be called with arguments: [ 'error', …(1) ] — the pushToast) plus hands a pending bare-write row back to the shell already expanded (expected null not to be null).
Control hygiene. One scratch git worktree at HEAD^1 under tmp/, removed after the cells were captured. The PR changes no dependency manifest, so the base tree shares the head's node_modules (root plus both per-package dirs, symlinked). That leaves @qwen-code/web-shell resolving into the head tree (readlink -f from inside the base worktree → /__w/qwen-code/qwen-code/packages/web-shell), which would normally be a confound because the PR changes web-shell. It is inert here, and I verified why rather than assuming it:
- the companion's
EmbeddedApp.test.tsxreplaces@qwen-code/web-shellwith a totalvi.mockfactory (noimportOriginal), so the real module never executes; - web-shell's own
vitest.config.tsaliases@qwen-code/web-shell/daemon-react-sdkto./client/daemon-react-sdk.ts, i.e. to the base tree's own source via__dirname, andApp.test.tsxadditionally mocks that specifier totally — so no headdistis loaded either. (This is why last round's base web-shell lib build was unnecessary this round.) @qwen-code/sdkdoes resolve to the head build; the PR does not touchpackages/sdk-typescriptand changes no manifest, so it is identical on both arms.
Base production files were asserted byte-identical to HEAD^1 by sha256 (all 6: diff-manager.ts, extension.ts, EmbeddedApp.tsx, WebViewProvider.ts, App.tsx, ToolGroup.tsx), and the 7 head test files copied into the base tree were asserted byte-identical to head (raw/control-hygiene-sha.txt).
Merge fidelity and merge interaction (new this round)
Commit 20 is a merge, so the round's real question is not "does the new code work" but "did the merge preserve the old code, and does it still work on the newer base it landed on".
Fidelity — proven. git cat-file -p dc665a60 shows parents 8b1ebb20 (last round's verified head) and c46cb85c (the snapshot's baseRefOid); both are hidden from rev-list --parents because dc665a60 is a shallow graft, which is why this needed cat-file rather than the usual rev syntax. The PR's net contribution is identical against both main OIDs: 13 files changed, 1345 insertions(+), 9 deletions(-) for c46cb85c..dc665a60 and for HEAD^1..HEAD, and the sorted multiset of all 1354 content lines (+/-, headers and context excluded) has the same sha256 f3617609db940cafb8f94a77a1d64da81265ffc3db04e270f9ba3877a54c93f8 on both sides. Only hunk offsets moved — uniformly +8 across all five App.tsx hunks (9644→9652, 9838→9846, 10376→10389, 10415→10445, 10430→10473), which is what a clean re-application looks like. No conflict markers in any of the 13 files at HEAD. Because HEAD is the merge into the current base tip, this also closes last round's "no trial merge into current main" gap: the merge is conflict-free and content-preserving.
Interaction — probed, risk disproved. Between the previous merge point and c46cb85c, main landed a resolvedFilePath / originalFilePath split in diff-manager.ts (resolveWorkspacePath, utils/file-path.ts), rewriting exactly the closeDiff matching that this PR's id-less-close dismissal hook reads, plus hasExistingDiff and onActiveEditorChange. Two things could have broken silently:
- Could the ✕ trigger stop finding its entry? No.
cancelDifflooks up by URI string —this.diffDocuments.get(rightDocUri.toString())— not by path, so main's path-key refactor cannot reach it. (This is a static fact about the merged code, not an inference from the diff.) - Could the id-less close stop matching across path forms? Probed with 6 new siblings (
diff-manager.mergesibling.test.tsin this artifact dir, appended to a copy of the shipped suite; file total 35 passed, 0 red), all withworkspaceFolders = [{ uri: { fsPath: '/test/ws' } }]— a configuration the shipped dismissal suite never sets, since it uses absolute/workspace/...paths throughout: relative open + relative close fires once withreq-1; relative open + absolute close fires; absolute open + relative close fires; relative open +./foo.tsdotted close fires; an id-bound close stays quiet for every form; and a siblingbar.tsrequest is untouched whenfoo.tscloses (toHaveBeenCalledTimes(1), idreq-1). All green at head.
So the merge is faithful and behaviour-preserving on the newer base. This is the measurement that a two-cell A/B against base cannot supply, because both arms would carry the same main-side refactor.
Corrections to the description
-
C1 stands. The comment this PR adds above
submissionSessionIsCurrentsays the full guard "also tracks composer identity, and submitting is itself what moves that — so reusing it here would suppress the very message the user needs". The census says otherwise: all sixcomposerSourceVersionRef.current += 1writers are session/workspace-switch events, none on the submit path. Corroborating evidence from this round's matrix: the immediate path's toast is guarded byadmissionOwnerIsCurrent(), which does includecomposerSourceVersionRef.current === admissionSource.sourceVersion(App.tsx:9614), and A4 (deleting that toast) is KILLED bysurfaces a rejected preparation instead of cancelling silently— i.e. that test passes at head with the composer-version conjunct in the guard, so submitting does not move composer identity in a way that suppresses a toast. The refactor composingsubmissionOwnerIsCurrentonsubmissionSessionIsCurrentremoves the drift risk the comment worries about; it does not fix the coverage gap F1 names, and the stated mechanism is still not the operative one. -
C2 stands and widened further. Every count in the body's "Run:" list is stale at this head:
body claims measured at dc665a60diff-manager.test.ts18 passed29 EmbeddedApp.test.tsx31 passed33 ToolGroup.test.tsx106 passed109 App.test.tsx746 passed884 extension.test.ts"Not run — cannot be collected here"collected and passing: 18 tests -
C3 (new). The body's "Not run:
src/extension.test.ts—@qwen-code/qwen-code-corehas no build on this machine, so the file cannot be collected here" is a statement about the author's machine, but it reads as a coverage limitation of the change. In this container the file collects and runs (18 tests, green at head), and it is precisely where F2's control X4 lands. The dismissal fan-out inextension.tsis therefore not uncollected — it is collected, green, and still unwitnessed for the ✕ trigger. Worth correcting so a reader does not discount F2 as "that file was never run".
Findings
F1 (carried, medium, wider this round) — both preflight-toast guards are unpinned
node tmp/mut-run.mjs tmp/mutations.json A1 # queued guard -> full submissionOwnerIsCurrent -> SURVIVED 0F|884P
node tmp/mut-run.mjs tmp/mutations.json A2 # immediate guard -> admissionSourceIsCurrent -> SURVIVED 0F|884P
node tmp/mut-run.mjs tmp/mutations.json A3 # CONTROL: delete the queued toast -> KILLED 1F|883P
node tmp/mut-run.mjs tmp/mutations.json A4 # CONTROL: delete the immediate toast -> KILLED 1F|883PA1 is the carried finding: the shipped queued test sets streamingState='responding' but never write-blocks the session, so the narrow and full guards evaluate identically there and nothing distinguishes them. A2 is new: the immediate path has the same shape — admissionOwnerIsCurrent() versus the fuller admissionSourceIsCurrent() (which adds !sessionWriteBlockedRef.current and the write-block-generation match) — and swapping in the fuller guard also leaves all 884 green. So the untested axis is "a toast the user should have seen was suppressed because the session was write-blocked mid-flight", and it is untested on both submission paths, not one. The two controls A3/A4 land in the same file as their mutants and both go red, so this is a real coverage gap rather than a harness that never collected App.test.tsx.
Classification: coverage gap, not a defect — the guards are correct as written.
Suggested fix (candidate, not applied, not measured)
One test per path in App.test.tsx: drive a preflight rejection while sessionWriteBlockedRef is set (or the write-block generation has advanced) between submission and rejection, and assert pushToast was not called — then the same scenario without the write block, asserting it was. Per the vacuity rule this ships with its mutations: A1 and A2 must both go SURVIVED → KILLED. Neither was applied this round, so the fixture that would pin this axis is named but not written.
F2 (carried, medium) — the user's ✕ is still unwitnessed
X3 SURVIVED 0 failed | 163 passed. The whole dismissal chain hangs on the pre-existing onDidCloseTextDocument → cancelDiff(doc.uri) subscription at extension.ts:242; every shipped test calls cancelDiff directly. Control X4 KILLED (1 failed \| 162 passed), which is what makes the survivor credible. Pre-existing code, so the author is not blamed — but this PR is what makes the subscription load-bearing. Note the merged-code fact that bounds it: cancelDiff is URI-keyed, so main's path refactor cannot silently break this hop (see Merge interaction).
F5 (carried, medium-low) — the gate's two stateful conjuncts are still unpinned
D3 and D4 both SURVIVED 884/884; census unchanged (0 occurrences of hostOwnsEditDiffPreview: false and of hasDiffPreview in App.test.tsx). Both conjuncts are live in production — hostOwnsEditDiffPreview is exactly the value this PR turns from a constant true into state, and it is plumbed App.tsx:2970 (default false) → App.tsx:3249 customization → ToolGroup. The sharp case is unchanged: after a hand-back the flag is false, so the gate correctly refuses a host-relayed vote; if a future refactor dropped that conjunct, a stale native vote would resolve an approval the host can no longer display and all 884 tests would stay green. Coverage gap, not a defect.
F3 (carried, low) — the fan-out try/catch is still unpinned
X1 SURVIVED 163/163. Mirrors the pre-existing vote fan-outs; a pre-existing pattern, not a PR-introduced hazard.
F4 (carried, low) — the bare-write auto-expand widening is real and pinned, but absent from the description
T1 KILLED by hands a pending bare-write row back to the shell already expanded. Deliberate and load-bearing for the hand-back; still a user-visible change for every web-shell host that the description never mentions. Note, not a defect.
Positive results worth recording (new this round)
- The iframe source gate is pinned. E0 (delete
&& event.source === window.parentfrom thepermissionDiffClosedbranch) is KILLED byignores a dismissal posted by a nested iframe window(expected false to be true). This is the guard that stops a scriptable sandboxed MCP-app iframe inside the webview from flipping who owns the edit preview. It has a witness; I probed it because an unpinned security guard would have been the sharpest finding available here, and it is not one. - No hidden layered guard. E1 (delete the
dismissedPermissionDiffIdRef.current !== pendingPermission.requestIddo-not-reopen conjunct) is KILLED withexpected [ { type: 'openDiff', …(1) }, …(1) ] to have a length of 1 but got 2— the tab springs back open, which is the exact bug the PR exists to fix. E2 (reverthostOwnsEditDiffPreviewto the constant) is KILLED with 3 reds. Since both halves die alone, the E3 combination row (both reverted together, 3 reds) adds no new information: this is not a defence-in-depth case where singles mask each other, and I report that explicitly so the combination row is not read as the only thing that caught it. - Teardown and next-request reclaim are pinned. E4 KILLED by
returns preview ownership to the host when the pending diffs are torn down; E5 KILLED bytakes the preview back for the next permission request. - The diff-manager guards are pinned. M1 (drop
permissionRequestId === undefined) KILLED bydoes not echo a close the chat surface asked for; M2 (drop thediffInfo.permissionRequestIdguard) KILLED bystays quiet for a diff that no approval is waiting on; M3 (delete the emitter.dispose()) KILLED bystops notifying once the manager is disposed. All three withexpected "spy" to not be called at all, but actually been called 1 times.
Full matrix: raw/mutation-matrix.txt, witness evidence/02-mutation-matrix.png, per-row JSON in raw/mut-<id>.json and logs in raw/mut-<id>.log. 23 rows, KILLED=15, SURVIVED=8, expectations-not-met=0. Every survivor is classified above as a coverage gap or observability-only; none is dead code and none is a defect.
Pre-existing base failures, attributed
The whole companion package at head is 5 failed | 567 passed | 1 skipped (573), 1 failed | 44 passed (45 files). All 5 reds are in src/ide-server.test.ts, which the PR does not touch. An A/A control ran that file at base: 5 failed | 7 passed | 1 skipped (13), exit 1, and after ANSI-stripping, the five failing test names are byte-identical on both arms (should set environment variables and workspace path on start with multiple folders, should set a single folder path, should set an empty string if no folders are open, should update the path when workspace folders change, should clear env vars and delete lock file on stop). Delta attributable to the PR: +0 failing. Logs: raw/gate-companion-head.log, raw/aa-ide-server-base.log.
Not covered
- F1/F2/F5 candidate fixes not written or measured. The suggested fixtures above are named, not applied; per the unpinned-axis rule, a suite that is green with and without a candidate fix proves nothing, so none is claimed as evidence.
- The previous round's two-process wire-handoff harness was not rebuilt (real
WebViewProvider.notifyPermissionDiffClosed→ JSON round-trip → realEmbeddedAppMessageEvent). The property is pinned transitively — W2 is KILLED byrelays a permission diff dismissal to the webview under requestId, and E0/E1/E2 pin the consumer's handling of that payload — but structured-clone fidelity of the transport is not re-proven this round. - Real VS Code Extension Development Host and real TUI. Every harness drives the shipped fake
vscodeboundary or a syntheticMessageEvent. Reviewer Test Plan steps that require a native tab to stay closed, or to reopen on the next request, remain host-side and unrecorded. - Playwright e2e (
test:e2e*) not run. Repo-wide suite not run — only the two affected workspaces. The whole-package companion run is reported above with its pre-existing failures attributed; the whole-package web-shell run was not done (only the two changed files). - Per-commit attribution. Depth-2 checkout:
git rev-list HEAD^1..HEAD^2returns 1 locally reachable commit while the snapshot lists 20, andgit rev-parse --is-shallow-repositoryistrue. All results are for the aggregateHEAD^1..HEADdiff. (The merge's own parents were recoverable viagit cat-file -p, which is how the delta was scoped — but the 19 earlier commits are not individually exercisable.) - macOS / Windows. Not executed here.
resolveWorkspacePathusesvscode.Uri.joinPath(...).fsPathandpath.win32.isAbsolute, and my sibling sweep's fakejoinPathis a plain${base.fsPath}/${filePath}string join — so the path-form siblings prove the matching logic, not Windows separator behaviour. That remains CI's to confirm. - A harness incident of mine, disclosed. A tool-level timeout killed the mutation runner mid-D4, leaving
App.tsxmutated on disk. I detected it viagit status(not by assuming the restore had run), restored withgit checkout --, and sha256-verified the file back toHEAD(a9ac27e8…) before continuing. One eslint run had already executed during that window; it was discarded and the gate re-run on the verified-clean tree, and the number reported above is from the re-run.tmp/mut-run.mjsnow restores in afinallyand on SIGINT/SIGTERM so an interrupt cannot leave residue. D4 was re-run to completion in the background afterwards and is reported from that clean run. - The first eslint liveness control was inconclusive and is not counted. I planted an unused
private readonlyclass field; eslint exited 0 and reported nothing, so that violation class is not owned by this config. I re-planteddebugger;+ an unused local +var, which eslint reported as 4 errors with exit 1 (raw/eslint-planted2.log), and only that run is cited as proving the gate live. The failed first attempt is preserved asraw/eslint-planted.log.
Methodology
Environment: the CI verify container (node v22.23.2, no GitHub token), tree = refs/pull/11171/merge at depth 2 with npm ci + npm run build pre-existing at HEAD. The delta was scoped first, from git cat-file -p on the grafted head, which showed commit 20 to be a pure merge of origin/main onto the previously verified head — that is what made merge fidelity and merge interaction, rather than new-code correctness, the round's central questions. The A/B used one scratch git worktree at HEAD^1 under tmp/ with node_modules symlinked from the head tree; the @qwen-code/web-shell link pointing into head was proven inert by showing both consumers mock it totally and that web-shell's own vitest aliases daemon-react-sdk to base source, and base production files were sha256-asserted equal to HEAD^1; the worktree was removed after the cells were captured. Merge fidelity was measured as a sorted-multiset sha256 over the 1354 +/- content lines of the PR's diff against two different main OIDs, plus a per-hunk offset comparison and a conflict-marker scan. Harnesses drove real production code with only the external vscode API faked: the real DiffManager, real activate() listener, real WebViewProvider, real EmbeddedApp render, real web-shell ToolGroup and App. The mutation runner (tmp/mut-run.mjs, mutations in tmp/mutations.json) applies one or more exact-string edits, asserts each anchor occurs exactly once, runs the named suite, records exit/status/failing-test/assertion lines, then restores in a finally and re-verifies by sha256; classification requires a KILLED row to name a failing test, so a collection error cannot masquerade as a kill. Gates: vitest per affected workspace, tsc --noEmit per package (both exit 0), eslint over all 13 changed files (exit 0) with a planted-violation liveness control that was reported and then restored by sha256. The fail: 0 in assertions.json is honest — no executed assertion failed, and ARM 1's reds are encoded as expected control outcomes, so they count as passes; the verdict is findings because F1 (widened), F2, F3 and F5 are concrete, reviewer-relevant coverage gaps and C1/C2/C3 are description corrections. The 55 counted assertions are: 6 A/B arm outcomes; 13 control-hygiene sha256 identities (7 test copies + 6 base production files); 2 merge-fidelity checks (content-line sha256 identity, conflict-marker scan); 1 A/A ide-server failing-name identity; 20 mutation rows whose recorded status matched a stated expectation (X1–X4, W1, W2, M1–M3, E2–E5, A1, A3, A4, T1, D1, D3, D4); 6 sibling probes S1–S6; 4 gates (two tsc --noEmit, eslint clean, eslint liveness); and 3 census checks (hostOwnsEditDiffPreview: false = 0, hasDiffPreview = 0, six composer-version writers all off the submit path). The E0, E1, A2 and E3 mutation runs were probes with no prior expectation, so they are reported as evidence rather than counted as pass/fail assertions.
Flakiness gate log
rounds=5 files=7 skipped=0
file packages/vscode-ide-companion/src/commands/index.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/commands/index.test.ts
file packages/vscode-ide-companion/src/diff-manager.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/diff-manager.test.ts
file packages/vscode-ide-companion/src/extension.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/extension.test.ts
file packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/EmbeddedApp.test.tsx
file packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/providers/WebViewProvider.test.ts
file packages/web-shell/client/App.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/App.test.tsx
file packages/web-shell/client/components/messages/ToolGroup.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/messages/ToolGroup.test.tsx
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/vscode-ide-companion/src/commands/index.test.ts: PPPPP
packages/vscode-ide-companion/src/diff-manager.test.ts: PPPPP
packages/vscode-ide-companion/src/extension.test.ts: PPPPP
packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: PPPPP
packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: PPPPP
packages/web-shell/client/App.test.tsx: PPPP
packages/web-shell/client/components/messages/ToolGroup.test.tsx: PPPP
verdict: timeout
summary: only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 1 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 2 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 3 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 4 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 5 · packages/vscode-ide-companion/src/commands/index.test.ts: P (exit 0)
round 5 · packages/vscode-ide-companion/src/diff-manager.test.ts: P (exit 0)
round 5 · packages/vscode-ide-companion/src/extension.test.ts: P (exit 0)
round 5 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 5 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
doudouOUC
left a comment
There was a problem hiding this comment.
Agent-assisted review at dc665a602f686b935e080479bdeec3b7b2d83f16 — Partial review — coverage gaps; no new confirmed Critical in the inspected paths.
Reviewed the complete current production diff (6 production files, approximately 163 changed lines), with surrounding permission-diff lifecycle and submit functions, plus the changed dismissal/request-binding/rewind/toast tests. Traced onDidClosePermissionDiff through the registry, provider and parent-frame message handler into the existing Web Shell preview consumers, including nested agent display. Closing is not a vote: request-bound accept/cancel commands route through respondToPendingPermission, whereas dismissal changes preview ownership. Request-bound programmatic close deletes the map entry before the close callback; id-less closes independently emit dismissal. The receiver requires parent-frame provenance and the current request ID before handing ownership back; transcript updates suppress reopening the dismissed request, and request change/teardown restore host ownership.
Historical reassessment:
- R2-1 (Windows-specific assertion, thread 3945351861) no longer applies: the current event and the deep-equality assertion at
packages/vscode-ide-companion/src/diff-manager.test.ts:370–373carry only permissionRequestId, not the former platform-dependent filePath. This is a source-level check, not a Windows test execution. - R1-19 was a PR-template/process concern, not a code Critical. The current body contains the previously missing review sections; it should not be carried as a current correctness defect.
- The existing outer-loop fan-out catch, toast-guard explanation and missing negative/logging witnesses remain deferred Suggestions, not newly promoted blockers. Adjacent vote commands also use an outer catch; the earlier claim of a different adjacent pattern was incorrect.
The immediate and queued preflight catches preserve their existing admission gates and surface errors only through the respective ownership checks. The companion's producer captures the rewind target session before awaiting snapshots. No new daemon endpoint is introduced.
Coverage gaps: no Extension Development Host run, real window/tab teardown race exercise, Windows/macOS run, build, typecheck, tests or mutation probes. Not every existing Web Shell regression or lifecycle interleaving was re-reviewed; historical reviewer execution claims are not my test evidence. Therefore this is not a full runtime pass or a CLEAN verdict. Bug-fix scope is below the size gate; no maintainer exemption is assumed.
Comment only; no approval implied.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Resolve the EmbeddedApp.test.tsx conflict by keeping both test blocks: - the PR's "permission diff dismissal" / "request-id wiring" / "message edit rewind" suites - main's "rewind preflight localization" suite and adopt main's typed getRewindSnapshots/rewindSession mocks. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtwtxrtzxr
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- the two new commands/index.test.ts id-forwarding cases gate nothing this diff introduces (re-derived this round by Agent 7's test-efficacy revert probe: all 10 tests in the file still pass with every source change reverted) - already report…
- closeAll() is a third close path that never fires the new dismissal event - already reported (round-8 deferred list, review 5154433504); @doudouOUC recorded it in round 9 as latent rather than user-facing
- DiffManager.dispose() has no production caller, so the added emitter disposal and its new witness cover a dead path - already reported as D5-1 (round-5 deferred list, review 5135780307)
- the extension.test.ts fan-out witness drives every listener on every emitter built during activate, so it cannot discriminate the loop from a first-element-only call - already reported as R1-4 (round-7 deferred list, review 5150914176)
- the per-provider drop-point log is mis-tagged [Extension] inside WebViewProvider.ts, fires once per dead provider on a successful delivery and carries no request id - already reported as R1-10 (comment 3944217736)
- both new preflight toast guards are witnessed only on their true branch, so deleting either guard leaves the tests green - already reported as R1-15 (comment 3944217749)
- the guard-split comment names a mechanism the code disproves and the two preflight toast gates disagree on identical input - already reported as D4-1 (round-4 deferred list, review 5135150418)
- the openDiff wire-key to permissionRequestId hop in FileMessageHandler.handleOpenDiff has no test, so the whole dismissal chain is witnessed from both ends but never across the inbound seam - already reported (round-9 deferred list, review …
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; Agent 7 built and tested on Linux only, which matters for a VS Code companion change that ships to all three platforms and because the round-2 Critical R2-1 was a Windows-only test failure no Linux lane could catch.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally; Agent 7 built and tested on Linux only.
Not reviewed: issue-fidelity — GitHub's strong closing-issue metadata could not be resolved (review issue-context reported gh >= 2.72.0 is required for closing-issue references), so the closing-issue set is unknown rather than empty and discovery relied on the PR body's explicit Fixes #10557 / Refs #10585 / Refs #9911; all three were fetched and read in full and both motivating incidents were replayed against the merged world, but a target issue linked only through closing metadata and not named in the body would have been missed.
Not explored to full depth (tool budget reached): "agent 1a": full package test suites for vscode-ide-companion and web-shell (only the six touched test files plus the whole of App.test.tsx were run); "agent reverse-audit (round 1)": I did not read FileMessageHandler.handleOpenDiff (FileMessageHandler.ts:611-640) in full — I confirmed via grep that it maps data.requestId to permissionRe…; "agent reverse-audit (round 3)": the remaining composerSourceVersionRef.current += 1 sites (App.tsx:3621, 12525, 17323) — I read :12406 and :13135 and both are session-open transitions that i…; "agent 1b": none — I read the entire 1616-line diff (offsets 0–1615), extracted the full deletion set two ways ( ^-[^-] and ^-$ ), and verified the one non-obvious replac….
Test Plan (not a blocker): src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more.
Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:
packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx:675 — [probe] the hand-back stickiness conjunct has no witness: deleting it leaves all 35 of the PR's tests green while a later transcript tick re-locks the row and reopens the close…
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; Agent 7 built and tested on Linux only, which matters for a VS Code companion change that ships to all three platforms and because the round-2 Critical R2-1 was a Windows-only test failure no Linux lane could catch.
未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally; Agent 7 built and tested on Linux only.
未审查(原文为英文):issue-fidelity — GitHub's strong closing-issue metadata could not be resolved (review issue-context reported gh >= 2.72.0 is required for closing-issue references), so the closing-issue set is unknown rather than empty and discovery relied on the PR body's explicit Fixes #10557 / Refs #10585 / Refs #9911; all three were fetched and read in full and both motivating incidents were replayed against the merged world, but a target issue linked only through closing metadata and not named in the body would have been missed.
未探索到全部深度(达到工具调用预算):"agent 1a":full package test suites for vscode-ide-companion and web-shell (only the six touched test files plus the whole of App.test.tsx were run);"agent reverse-audit (round 1)":I did not read FileMessageHandler.handleOpenDiff (FileMessageHandler.ts:611-640) in full — I confirmed via grep that it maps data.requestId to permissionRe…;"agent reverse-audit (round 3)":the remaining composerSourceVersionRef.current += 1 sites (App.tsx:3621, 12525, 17323) — I read :12406 and :13135 and both are session-open transitions that i…;"agent 1b":none — I read the entire 1616-line diff (offsets 0–1615), extracted the full deletion set two ways ( ^-[^-] and ^-$ ), and verified the one non-obvious replac…。
Test Plan(非阻断):src/diff-manager.test.ts — no such file or directory; src/webview/EmbeddedApp.test.tsx — no such file or directory; src/commands/index.test.ts — no such file or directory; client/App.test.tsx — no such file or directory; client/components/messages/ToolGroup.test.tsx — no such file or directory; and 6 more。
收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.3)
|
Closeout triage: the latest sandbox report contains coverage/description follow-ups (F1/F2/F3/F5 and F4/C1/C2/C3), not a confirmed implementation defect. The template-gate Critical is already cleared, and the PR currently has zero unresolved review threads with all required CI lanes green. Given the established review history and scope, I am deferring these non-blocking suggestions rather than widening the PR. |
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
APPROVE
已核对 head 6e73fd15c71d3b6d2593822872632b03285f766b(vs origin/main merge-base 20ecdaf6b2)。
历史 Critical 已关闭:R2-1(Windows 上 path.normalize 让断言只能在 POSIX 主机通过)在当前 head 已从根上消失 —— onDidClosePermissionDiff 的载荷只剩 { permissionRequestId }(diff-manager.ts:93-110),cancelDiff 与 id-less closeDiff 两处 fire 都只带 request id,测试断言相应改为深比较 expect(closed).toHaveBeenCalledWith({ permissionRequestId: 'req-1' })(diff-manager.test.ts:359-374)。我在该 head 的完整 diff 上扫过所有新增断言:只剩这一处对事件的深比较,没有任何「把裸 POSIX 字面量当作实现变换输出」的比较,路径类断言仍走 expect.stringContaining('foo.ts'),因此这一类 Windows-only 失败不会再回来。
其余历史项:24 条线程逐条对过当前 head —— filePath 冗余字段、sendMebageToWebView 漏斗复用、订阅 fan-out 的丢弃点日志、closeDiff 归属判定、isEditToolName 表合并(ToolGroup.tsx:555,现覆盖 edit/editfile/write/write_file/writefile)等均已落地;4 条明确标注为跨包范围外、由作者记为 follow-up。
独立复查未发现新的 Critical:emitter 在 dispose() 中释放(diff-manager.ts:179);closeDiffEditor 先删表,onDidCloseTextDocument → cancelDiff 那一跳查不到条目,因此同一次关闭不会双发;acceptDiff 不触发(投票不是撤销);webview 侧 permissionDiffClosed 处理保留了与投票处理同样的 event.source === window.parent 来源门(EmbeddedApp.tsx:810-821),dismissedPermissionDiffIdRef 在 focus 切换与 reset 两处都会清掉,不会出现粘死。
CI:required 四项均 success(Test (ubuntu-latest, Node 22.x)、Lint & Static、Integration Tests (no-AK, No Sandbox)、web-shell E2E Smoke)。
本地验证:packages/vscode-ide-companion 四个改动测试文件 Test Files 4 passed (4) / Tests 155 passed (155)。web-shell 客户端用例在本机无法收集(remark-cjk-friendly 未安装在本工作树,属环境缺依赖,非本 PR 引入),该侧以 CI 的 Test (ubuntu-latest) 为准。
不阻塞、请在 follow-up 里保住的事(本轮不要求改动,按 PR 已收敛到第 10 轮的姿态记录,避免静默丢失):
closeAll()是第三条关闭路径,不触发新事件 —— 人已在第 9 轮记为 latent 而非 user-facing;DiffManager.dispose()目前无生产调用方,新增的 emitter 释放及其用例覆盖的是死路径(D5-1);commands/index.test.ts两条新增 id 透传用例对本 diff 无门控力(源码全撤回仍 10/10 绿);extension.test.ts的 fan-out 见证驱动所有监听器,无法区分「逐个调用」与「只调第一个」;WebViewProvider.ts内的丢弃点日志前缀写成[Extension],且成功投递时也会按 dead provider 逐条打、不带 request id(R1-10);- 两处新增 preflight toast 守卫只在 true 分支有见证,删掉任一守卫用例仍绿(R1-15);
EmbeddedApp.tsx:675的 hand-back 粘住条件无用例,删掉后 35 条用例仍全绿;- 本轮 ci-bot 亦披露
Test (windows-latest)/Test (macos-latest)对本 head 为 skip,即合并前拿不到 Windows 侧信号;鉴于 R2-1 正是只有 Windows 才会暴露的那类问题,这一条值得在合并时留意(当前结论:载荷里已不含路径字段,该类失败无载体)。
chiga0
left a comment
There was a problem hiding this comment.
No blocking findings. Approving.
Scope reviewed: all 6 production files (diff-manager.ts, extension.ts, EmbeddedApp.tsx, WebViewProvider.ts, App.tsx, ToolGroup.tsx) plus the 7 companion test patches.
What I checked:
-
Double-fire analysis (class 1 / class 3): Traced both (Site 2) and id-less path (Site 1) against a shared invariant:
closeDiffEditordeletes the map entry synchronously before anytabGroups.closeawait. AonDidCloseTextDocument → cancelDiffhop that arrives during or after the tab close finds the entry gone and returns early. No double-fire path exists. -
Vote-path isolation (class 10): The PR claims
qwen.diff.accept/qwen.diff.cancelnever reachcancelDifffor request-bound diffs — guarded by!permissionRequestId→ routes throughrespondToPendingPermissioninstead. R3-7 and R3-8 inextension.test.tswitness this guard directly (getPermissionRequestIdreturning 'req-1' →acceptDiffnot called; returningundefined→ called once). The guard is independently traceable statically. -
EmbeddedApp state machine (class 8):
dismissedPermissionDiffIdRefis cleared byupdateTranscriptwhenpermissionToFocuschanges (new request) or becomesundefined(permissions resolved).hostOwnsEditDiffPreviewresets correctly through both paths. The!has(id) && dismissedId !== idguard prevents re-opening the dismissed diff. -
submissionOwnerIsCurrent refactor (App.tsx):
submissionSessionIsCurrentis the four conjuncts that form the base;submissionOwnerIsCurrentcomposes on it — boolean AND is commutative and both calls are pure reads. Semantic equivalence holds. -
WebViewProvider.notifyPermissionDiffClosed:
sendMessageToWebViewis wrapped in try/catch in extension.ts. NullchatProviderRegistryis logged and dropped.WebViewProvider.test.tswitnesses the{type: 'permissionDiffClosed', data: {requestId: ...}}payload shape.
Disclosure:
- extension.test.ts not run locally — R3-7/R3-8 (the extension.ts fan-out witnesses) couldn't be collected on the author's machine (missing
@qwen-code/qwen-code-corebuild). The 7-line binding is simple enough to verify statically and the two ends of the chain are independently tested, but this rung is CI's to confirm. - macOS and Windows CI lanes were skipped at this commit — stated by the author; platform confirmation owed by CI before merge.
- Post-vote close race (deferred from round 5): If a native-vote command doesn't close the diff tab, a subsequent manual tab close fires
onDidClosePermissionDiffEmitterfor the already-decided request.updateTranscriptresets the state on the next transcript change (permission block resolving), so the window is narrow. Not a blocker; already recorded on the thread.
Cross-check: Prior reviews (rounds 1–5 by qwen-code-ci-bot) raised 14 Suggestion-level findings, all addressed. The round 5 deferred items (dispose() caller; post-vote close race) are noted above as observations. Round 9 triage confirmed no Critical findings at dc665a6 and traced the mechanism end-to-end. No finding in the prior reviews is unaddressed or unacknowledged.
Reviewed with AI assistance.
yiliang114
left a comment
There was a problem hiding this comment.
Reviewed at 6e73fd1 (base 20ecdaf). The main flow is right: the emitter/dispose wiring is clean, closeDiffEditor deletes the map entry before tabGroups.close, so the onDidCloseTextDocument -> cancelDiff hop can't double-fire, and the vote paths never mis-fire the dismissal. The event.source === window.parent gate is sound and the payload carries only the request id.
This landed before I finished, so treating everything as follow-up rather than blocking. One of them I'd still fix: the dismissal notification routes by active webview instead of by the request's owning webview, so in the panel-open + sidebar-owns-the-request configuration #10557 is unchanged. Details inline.
Two non-blocking notes on the diff overall:
- Scope: 1341 additions for ~156 production lines, bundling #10557 + #10585 (twelve test witnesses) + #9911, after ~5 review rounds. Per AGENTS.md ("Don't let review rounds balloon the PR"), the #10585 witness set would have been better as its own PR.
Test (macos-latest, Node 22.x)andTest (windows-latest, Node 22.x)were skipping at this commit, and R2-1 on this PR was a Windows-only failure — so the platform that previously broke had no signal here.
| ); | ||
| return; | ||
| } | ||
| this.sendMessageToWebView({ |
There was a problem hiding this comment.
This routes by active webview, but permission ownership is per-webview: webShellPermissionOwners maps webview -> requestId, and sendMessageToWebView posts to getActiveWebview(), which prefers the panel over the sidebar.
Concrete drop: sidebar webview owns the pending request, user then opens the chat editor-tab panel, then closes the diff. getActiveWebview() returns the panel, the panel's webShellPermissionRequestIdRef.current !== requestId, the flag never flips, and the row stays locked — i.e. #10557 unchanged in that configuration. Panel dispose deliberately keeps the sidebar owner entry (~L209), so the two coexisting is a supported state, not an edge case.
respondToPendingPermission right below (~L2541) already does this correctly — it looks the owner up out of webShellPermissionOwners and posts to that webview directly. Mirroring it here is about three lines.
| // leaving the user to approve or reject something they can no longer look | ||
| // at (#10557). | ||
| diffManager.onDidClosePermissionDiff(({ permissionRequestId }) => { | ||
| try { |
There was a problem hiding this comment.
The try wraps the whole loop, so the first throwing provider skips notification for every provider after it — which is the opposite of what the comment below says it's protecting ("must not take down ... every other surface's notification"). Moving the try inside the for gets the stated behavior.
| // Narrower than submissionOwnerIsCurrent below: it answers "is the user | ||
| // still looking at the session this submission belonged to", which is | ||
| // what decides whether a failure is worth telling them about. The full | ||
| // guard also tracks composer identity, and submitting is itself what |
There was a problem hiding this comment.
The rationale here doesn't match the code. "submitting is itself what moves that" — composerSourceVersionRef.current += 1 only happens on workspace-trust change, new session, workspace picker, and standalone session; never on submit. And the immediate-failure path a few hundred lines up gates its identical toast on admissionOwnerIsCurrent(), which does include the sourceVersion conjunct this comment says would suppress the message.
Also worth noting the split drops three conjuncts, not one: !sessionWriteBlockedRef.current, the write-block generation match, and the version match. So the two toast paths now have materially different suppression rules for the same user-visible error. Either is defensible, but not both — and the comment should describe whichever one you keep.
| if (isAskUserQuestionToolName(tool.toolName)) return true; | ||
| if (name === 'write_file' || name === 'writefile') return true; | ||
| if (name === 'edit' || name === 'editfile') return true; | ||
| if (isEditToolName(name)) return true; |
There was a problem hiding this comment.
Non-blocking, but this widens behavior beyond the swap it looks like: isEditToolName also matches write, which shouldAutoExpand didn't cover before, so plain write rows now auto-expand for every web-shell host. hasDetailView already includes write so it renders fine — just calling out that it's an unrelated behavior change riding along.
|
Released in v0.23.4. |






What this PR does
Closing a host-owned permission diff by hand left the user having to approve or reject an edit they could no longer look at.
The gap, end to end:
onDidCloseTextDocumentroutes the close toDiffManager.cancelDiff, which fireside/diffClosedononDidChange. The only consumer of that event iside-server.ts— IDE-mode MCP transports. The web shell that asked for the diff is not one, so it was never told.EmbeddedAppkept the request inopenPermissionDiffsRef, soupdateTranscriptnever re-postedopenDifffor it.ToolGroupkept the row locked, becausehostOwnsEditDiffPreviewsays the host owns the edit preview — andToolApprovaltherefore renders no diff.DiffManagernow also fires a typedonDidClosePermissionDiffwhen the diff it closed had apermissionRequestId.extension.tsfans it out to the permission-aware providers — the same registryqwen.diff.acceptandqwen.diff.cancelalready use — andWebViewProviderposts it to its webview.EmbeddedApptreats that as the host handing the preview back, not as a vote: it drops the request fromopenPermissionDiffsRef, stops passinghostOwnsEditDiffPreview, and does not reopen the tab the user just closed. The row unlocks, the web shell renders the diff inline, and the approval can be answered against something visible. Ownership returns to the host on the next permission request, or when the pending diffs are torn down.This is direction 1 and 2 from the issue combined, and it needs no new customization surface:
hostOwnsEditDiffPreviewis already a boolean, and only one permission is pending at a time.Two paths that deliberately do not trigger it
Both are pinned by tests, because either one firing would be worse than the bug:
qwen.diff.accept/qwen.diff.cancelnever reachcancelDifffor a request-bound diff — they are guarded by!permissionRequestIdand route the vote throughrespondToPendingPermissioninstead.closeDiffEditordeletes the map entry before closing the tab, so theonDidCloseTextDocument→cancelDiffhop that follows a web-shell-initiatedcloseDifffinds nothing. Without this, a close the web shell asked for would echo back as a dismissal and the diff would reopen in a loop.Why it's needed
The approval flow lost the artifact it was asking the user to judge. Once the host opened the diff,
hostOwnsEditDiffPreviewmade the web shell render no diff of its own, so the native tab was the only place the change could be read. Closing that tab was a normal, user-initiated act — but nothing told the web shell it had happened, so the tool row stayed locked and the pending approval stayed live. The user was left with a vote to cast and no way to see what they were voting on: approving blind, rejecting blind, or abandoning the turn.Two follow-on gaps are fixed in the same PR because both were found while witnessing this one:
console.warnand nothing user-visible, so a failed rewind looked like the edit had simply done nothing while the composer stayed in editing mode. The localizedcomposer.editUnavailable/composer.editExpiredstrings the companion already threw had no consumer anywhere in the repository — they were written to be read by a user and could never reach one.Reviewer Test Plan
How to verify
qwen.diff.accept). Expected: unchanged behavior — no unlock, no reopen.What was and was not executed locally:
packages/vscode-ide-companion→src/diff-manager.test.ts18 passed,src/webview/EmbeddedApp.test.tsx31 passed,src/commands/index.test.ts10 passed.packages/web-shell→client/App.test.tsx746 passed (the full file, since Restore VS Code message edit and rewind after the WebShell cutover #9911's fix touches a shared submit path),client/components/messages/ToolGroup.test.tsx106 passed.src/extension.test.ts—@qwen-code/qwen-code-corehas no build on this machine, so the file cannot be collected here. The change toextension.tsis a 7-line additive subscription inside the existingcontext.subscriptions.push(...)list, against an event that exists on the realDiffManager(that file's tests use the real class and spy only onhasDiff/getPermissionRequestId), but the activation path is CI's to confirm.Evidence (Before & After)
Outstanding — no visual capture exists for this PR, and the automated witnesses below are not a substitute for one. Steps 2 and 5 are host-side behaviors (the native diff tab staying closed, then reopening on the next request) that need a VS Code Extension Development Host session to record; none was run. This is stated plainly rather than papered over with pass counts.
What the suites do pin, at the unit level:
openPermissionDiffsRefand lefthostOwnsEditDiffPreviewasserted, soToolApprovalrendered no diff and the row stayed locked. After:diff-manager.test.ts(18 passed) witnessesonDidClosePermissionDifffiring only for a diff carrying apermissionRequestId, andEmbeddedApp.test.tsx(31 passed) witnesses the request being dropped, the flag no longer passed, and the tab not being reopened.ToolGroup.test.tsx(106 passed) witnesses the row unlocking and rendering the diff inline once the host hands the preview back.App.test.tsx(746 passed) witnesses the toast firing on a rejected preflight, including the session-switch case that pins thesessionIdcaptured before theawaitinprepareSubmit.Tested on
✅ tested ·⚠️ not tested · N/A
Linux is where the unit suites above were executed. macOS and Windows were not tested locally, and at this commit the
Test (macos-latest, Node 22.x)andTest (windows-latest, Node 22.x)lanes were skipped in CI, so their suites did not run there either. That matters for a VS Code companion change shipping to all three platforms, and it matters again because the round-2 Critical R2-1 was a Windows-only test failure no Linux lane could catch — platform confirmation is owed by CI before merge.Environment (optional)
N/A — unit tests only. No Extension Development Host session, no Docker/Podman sandbox, no local daemon runtime.
Risk & Scope
hostOwnsEditDiffPreviewbecomes stateful in the companion instead of a constanttrue. If it were ever leftfalseafter the pending permission resolved, later requests would render inline instead of in a native diff — hence the reset on both the request-change path andcloseOpenPermissionDiffs, and a test for each. The user-visible behavior change is that closing a permission diff no longer means losing the edit; nothing about voting from the diff editor changes.src/extension.test.tswas not collected locally (missing core/acp-bridge builds) so the activation fan-out and its R3-7 / R3-8 witnesses are CI's to confirm; no macOS or Windows execution locally or in CI at this commit; no Before/After recording of the host-side diff tab. Also out of scope: the related single-slotonExitconcern from the same review round, already fixed onmain(see vscode-ide-companion: superseded daemon child still fires onExit, showing a false crash banner #10378); and R3-14, verified as a real observation and deliberately not fixed because it is a behaviour decision recorded on the issue. Whether Extension Development Host screenshots are still wanted for Restore VS Code message edit and rewind after the WebShell cutover #9911 now that the interaction is not new code remains an open question for the maintainer.onDidClosePermissionDiffis additive onDiffManager, and thecomposer.editUnavailable/composer.editExpiredstrings already existed and were merely given a consumer.Also in this PR
#10585 — all twelve test-witness gaps. Two commits: the companion set (
commands/index.test.ts,diff-manager.test.ts,webview/EmbeddedApp.test.tsx,extension.test.ts) and the web-shell set (App.test.tsx,ToolGroup.test.tsx). Ten were run here; R3-7 and R3-8 live inextension.test.ts, which pulls@qwen-code/qwen-code-coreand@qwen-code/acp-bridge— neither has a build on this machine — so those two are CI's to confirm. R3-14 was verified as a real observation and deliberately not fixed; it is a behaviour decision, recorded on the issue.#9911 — the two items that survived the audit. The issue's premise is stale: per-message edit/rewind shipped with the cutover, daemon-backed through
getRewindSnapshots/rewindSession, so the ACP contract it was opened to design was never needed. What was actually missing:console.warnand nothing else. The companion throwscomposer.editUnavailableandcomposer.editExpiredhere — fully localized, English and Chinese — and nothing else in the repository consumed them. They were written to be read by a user and could never reach one. A failed rewind looked like the edit did nothing, with the composer still in editing mode.getRewindSnapshotsandrewindSessionappeared inEmbeddedApp.test.tsxonly as mock stubs.The toast fires only while the user is still on the session the submission belonged to, and that guard is deliberately narrower than the existing
submissionOwnerIsCurrent— the full guard tracks composer identity, and submitting is what moves it, so reusing it would suppress the very message the user needs. The first draft did reuse it; the new test caught that the toast never fired.Remaining on #9911: whether the Extension Development Host screenshots are still wanted now that the interaction is not new code. All three rewind cases the audit named are now witnessed: the chosen-turn and expired-snapshot ones, plus the session-switch case (a switch between
getRewindSnapshotsandrewindSession), which pins thesessionIdcapture taken before the await inprepareSubmit.Linked Issues
Fixes #10557
Refs #10585
Refs #9911
Follow-up from the #9811 WebShell cutover.
中文说明
这个 PR 做了什么
手动关掉一个由宿主(host)持有的权限 diff 之后,用户仍然被要求批准或拒绝一个自己已经看不到的改动。
完整的缺口链路:
onDidCloseTextDocument把关闭事件路由到DiffManager.cancelDiff,后者在onDidChange上发出ide/diffClosed。这个事件唯一的消费者是ide-server.ts——也就是 IDE 模式的 MCP 传输层。发起这个 diff 的 web shell 不属于这一类,所以它从来没有被告知。EmbeddedApp把该请求继续留在openPermissionDiffsRef里,于是updateTranscript再也不会为它重新投递openDiff。ToolGroup让这一行保持锁定,因为hostOwnsEditDiffPreview声明宿主拥有编辑预览——于是ToolApproval根本不渲染 diff。现在
DiffManager在关闭的 diff 带有permissionRequestId时,还会额外发出一个带类型的onDidClosePermissionDiff。extension.ts把它分发给具备权限能力的 provider——用的正是qwen.diff.accept和qwen.diff.cancel已经在用的那套注册表——WebViewProvider再把它投递给自己的 webview。EmbeddedApp把它当作宿主把预览权交还回来,而不是一次投票:它把请求从openPermissionDiffsRef中移除,不再传hostOwnsEditDiffPreview,并且不会把用户刚关掉的标签页重新打开。这一行随即解锁,web shell 内联渲染 diff,用户可以在看得见的东西上做出批准决定。宿主的所有权会在下一次权限请求时恢复,或者在待处理 diff 被销毁时恢复。这是 issue 里方向 1 和方向 2 的合并实现,且不需要引入任何新的可定制面:
hostOwnsEditDiffPreview本来就是布尔值,而且同一时刻只会有一个待处理权限。两条刻意不触发它的路径
两条都有测试钉住,因为它们任意一条被触发都比原来的 bug 更糟:
qwen.diff.accept/qwen.diff.cancel对绑定了请求的 diff 永远不会走到cancelDiff——它们被!permissionRequestId拦住,改为通过respondToPendingPermission走投票路径。closeDiffEditor会在关闭标签页之前先删掉 map 里的条目,因此由 web shell 主动发起closeDiff之后紧跟着的onDidCloseTextDocument→cancelDiff这一跳找不到任何东西。少了这一步,web shell 自己要求的关闭就会被回声成一次「宿主撤销」,diff 会陷入反复重开的循环。为什么需要它
审批流程把用户需要判断的那个对象弄丢了。宿主一旦打开 diff,
hostOwnsEditDiffPreview就让 web shell 不再渲染自己的 diff,于是那个原生标签页成了唯一能读到改动内容的地方。关掉这个标签页是完全正常的用户操作——但没有任何机制把这件事告诉 web shell,所以工具行仍然锁定、待处理的审批仍然存活。用户被留在一个必须投票却无从查看投票对象的处境里:要么盲批,要么盲拒,要么放弃这一轮。同一个 PR 里还修掉了两个连带缺口,因为它们都是在为上面这个问题补测试见证时发现的:
console.warn取消提示,用户侧毫无反馈,于是一次失败的 rewind 看起来就像这次编辑什么都没做,而 composer 还停在编辑态。companion 本来就会抛出的、已完整本地化(中英文)的composer.editUnavailable/composer.editExpired文案,在整个仓库里没有任何消费者——它们是写给用户看的,却永远到不了用户眼前。审阅者测试计划
如何验证
qwen.diff.accept)。预期: 行为不变——不解锁、不重开。本地实际执行与未执行的部分:
packages/vscode-ide-companion→src/diff-manager.test.ts18 passed、src/webview/EmbeddedApp.test.tsx31 passed、src/commands/index.test.ts10 passed。packages/web-shell→client/App.test.tsx746 passed(跑了整个文件,因为 Restore VS Code message edit and rewind after the WebShell cutover #9911 的修复动到了一条共享的提交路径)、client/components/messages/ToolGroup.test.tsx106 passed。src/extension.test.ts——本机上@qwen-code/qwen-code-core没有构建产物,该文件无法被收集。对extension.ts的改动是在既有context.subscriptions.push(...)列表里新增 7 行订阅,订阅的事件在真实的DiffManager上确实存在(该文件的测试用的是真实类,只对hasDiff/getPermissionRequestId打桩),但激活路径需要由 CI 确认。证据(改动前后)
尚缺——本 PR 没有任何可视化录证,下面这些自动化见证不能替代它。 第 2 步和第 5 步是宿主侧行为(原生 diff 标签页保持关闭、随后在下一次请求时重新打开),需要一次 VS Code Extension Development Host 会话才能录制;这个会话没有跑过。这里如实写明,而不是用通过数糊过去。
套件在单元层面确实钉住的内容:
openPermissionDiffsRef中,hostOwnsEditDiffPreview仍被断言为真,于是ToolApproval不渲染任何 diff,该行保持锁定。改动后:diff-manager.test.ts(18 passed)见证onDidClosePermissionDiff只对带permissionRequestId的 diff 触发;EmbeddedApp.test.tsx(31 passed)见证该请求被移除、该标志不再传递、标签页没有被重开。ToolGroup.test.tsx(106 passed)见证宿主交还预览权之后该行解锁并内联渲染 diff。App.test.tsx(746 passed)见证预检被拒时 toast 会触发,其中包含会话切换这一例,钉住了prepareSubmit里在await之前捕获的sessionId。测试环境
✅ 已测试 ·⚠️ 未测试 · N/A
Linux 是上面那些单元测试套件实际执行的系统。macOS 与 Windows 本地都没有测试,而且在当前这个 commit 上,CI 里的
Test (macos-latest, Node 22.x)与Test (windows-latest, Node 22.x)两条 lane 都被 skipped,所以它们在 CI 上也没有跑。这一点对一个要发往三个平台的 VS Code companion 改动很重要;而且本轮再次重要,因为第 2 轮的 Critical R2-1 正是一个只有 Windows 才会暴露、任何 Linux lane 都抓不到的测试失败——平台确认在合并前仍然欠着,需要由 CI 补上。环境(可选)
N/A——只跑了单元测试。没有 Extension Development Host 会话,没有 Docker/Podman 沙箱,没有本地 daemon 运行时。
风险与范围
hostOwnsEditDiffPreview在 companion 里从恒为true的常量变成了有状态的量。如果它在待处理权限结束后被留在false,后续请求就会内联渲染而不是走原生 diff——因此在「请求变化」路径和closeOpenPermissionDiffs上都做了复位,并且各配了一条测试。用户可见的行为变化是:关掉权限 diff 不再等于丢掉这次编辑;从 diff 编辑器投票的一切行为都不变。src/extension.test.ts本地未被收集(缺 core / acp-bridge 构建产物),因此激活分发路径及其 R3-7 / R3-8 见证需要 CI 确认;本地与 CI 在当前 commit 上都没有 macOS / Windows 执行记录;没有宿主侧 diff 标签页的改动前后录证。同样超出范围:同一轮 review 里提到的相关单槽onExit问题,已在main上修好(见 vscode-ide-companion: superseded daemon child still fires onExit, showing a false crash banner #10378);以及 R3-14,已核实是真实观察但刻意不修,因为它是一个记录在 issue 上的行为决策。至于 Restore VS Code message edit and rewind after the WebShell cutover #9911 在这个交互已不算新代码之后是否仍然需要 Extension Development Host 截图,留给维护者决定。onDidClosePermissionDiff是在DiffManager上的新增项,而composer.editUnavailable/composer.editExpired这两条文案本来就存在,只是终于有了消费者。本 PR 还包含
#10585——全部十二条测试见证缺口。 两个 commit:companion 那一组(
commands/index.test.ts、diff-manager.test.ts、webview/EmbeddedApp.test.tsx、extension.test.ts)和 web-shell 那一组(App.test.tsx、ToolGroup.test.tsx)。其中十条在本机跑过;R3-7 与 R3-8 位于extension.test.ts,该文件会引入@qwen-code/qwen-code-core和@qwen-code/acp-bridge——本机两者都没有构建产物——所以这两条需要 CI 确认。R3-14 已核实为真实观察但刻意未修;它是一个行为决策,已记录在 issue 上。#9911——审计后仍然成立的两项。 该 issue 的前提已经过时:按消息粒度的编辑/rewind 随 cutover 一起上线了,由 daemon 通过
getRewindSnapshots/rewindSession支撑,所以它当初要设计的那份 ACP 契约从来就不需要。真正缺失的是:console.warn取消提示,别无其他。companion 在这里抛出composer.editUnavailable和composer.editExpired——中英文都已完整本地化——而仓库里没有任何其他地方消费它们。它们是写给用户看的,却永远到不了用户眼前。一次失败的 rewind 看起来就像这次编辑什么都没做,而 composer 仍停在编辑态。getRewindSnapshots和rewindSession在EmbeddedApp.test.tsx里只以 mock 桩的形式出现过。toast 只在用户仍停留在该次提交所属会话时才触发,而这个守卫刻意比既有的
submissionOwnerIsCurrent更窄——完整守卫跟踪的是 composer 身份,而提交动作本身就会改变它,所以复用它会恰好压掉用户最需要看到的那条消息。第一版确实复用了它;是新加的测试发现 toast 从来没触发过。#9911 上还剩的事项:在这个交互已不算新代码之后,是否仍然需要 Extension Development Host 截图。审计点名的三个 rewind 场景现在都有见证:选定轮次那一例、快照过期那一例,以及会话切换那一例(在
getRewindSnapshots与rewindSession之间切换会话),后者钉住了prepareSubmit中在 await 之前捕获的sessionId。关联 Issue
Fixes #10557
Refs #10585
Refs #9911
承接 #9811 WebShell cutover 的后续工作。