fix(web-shell): report intended workspace to host when starting a new chat - #7910
Conversation
… chat Clearing a session leaves connection.workspaceCwd pointing at the previous session's workspace. The onSessionIdChange notification read that stale value, so starting a new chat in workspace A routed the host back to the old workspace (e.g. one with a running task) and the composer showed the wrong workspace. With no active session, report the workspace picked for the next session instead.
|
Thanks for the PR! Template looks good ✓ Problem: this is an observed bug, not theoretical hardening. The PR carries a regression test that fails on Direction: aligned. Multi-workspace routing in the Web Shell is squarely in scope, and the fix makes the host notification agree with what the composer chip already reports. No auth/sandbox/model-selection/telemetry surface is touched. CHANGELOG has no direct reference, but the area is clearly relevant. Size: not applicable — Approach: the scope feels right and genuinely minimal. Rather than adding new state, it reuses the existing Risk: no elevated risk signals — none of the changed files match the high-risk revert-correlated paths. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是一个已观测到的 bug,而非理论性加固。PR 附带了一个回归测试,在 方向:对齐。Web Shell 的多 workspace 路由完全在范围内,且修复让 host 通知与 composer 标签已上报的内容保持一致。未触及 auth/sandbox/模型选择/telemetry 等敏感面。CHANGELOG 无直接引用,但该领域明显相关。 规模:不适用—— 方案:范围合理且真正最小化。它没有新增状态,而是复用已有的 风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewMy independent take before reading the diff: when no session is active, the host notification should report the same workspace the composer chip shows (the next-session selection), not the stale A few things I verified in the code:
No correctness, security, or regression concerns. No convention violations — this is a surgical 11-line change that reuses existing infrastructure rather than adding a parallel helper. The four new tests target the exact states that matter (explicit selection, unset→primary fallback, active-session-unchanged, and re-notification on switch). Test evidenceThis is an unattended CI run, so I'm quoting the PR's own CI checks on the reviewed commit rather than running anything. The directly relevant lanes are green:
The Not verified: a manual multi-workspace TUI reproduction. The routing behavior is covered by the unit regression tests and the web-shell E2E smoke; a maintainer can trigger the isolated 中文说明代码审查读 diff 前我的独立判断:当没有活跃会话时,host 通知应上报 composer 标签所显示的同一 workspace(下一会话的选择),而非清空会话后残留的 我在代码中核实的几点:
无正确性、安全性或回归问题。无规范违规——这是一个 11 行的外科手术式改动,复用既有基础设施而非新增并行 helper。四个新测试精确覆盖了关键状态(显式选择、未设置→primary 回退、活跃会话不变、切换时重新通知)。 测试证据这是无人值守的 CI 运行,因此我引用 PR 自身在被审 commit 上的 CI 检查,而非自行运行任何东西。直接相关的流水线为绿:单元套件(ubuntu)通过(含新增的 未验证:手动多 workspace TUI 复现。路由行为已由单元回归测试和 web-shell E2E smoke 覆盖;若合并前需要真实多 workspace 抓取,maintainer 可触发隔离的 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 5/5 — a surgical, well-tested fix that reuses existing infrastructure; I'd merge without hesitation. Stepping back: the problem is real and demonstrated (the regression test fails on CI is green on the reviewed commit, including the web-shell E2E smoke that exercises this exact package, and there are no pending checks. Approving now, pinned to the reviewed commit. 中文说明置信度:5/5 —— 一个外科手术式、充分测试的修复,复用既有基础设施;我会毫不犹豫地合并。 退一步看:问题是真实且有演示的(回归测试在 被审 commit 上 CI 为绿,包括演练本包正好所在包的 web-shell E2E smoke,且无待处理检查。现在批准,固定到被审 commit。 — Qwen Code · qwen3.8-max-preview Reviewed at |
🖼️ 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 1 render-shaping file:
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 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action neededNo actionable review feedback was found for PR #7910 in this round:
The PR diff (2 files, +51/−4) is unchanged. No code changes were made. 中文说明Autofix 审查轮次 — 无需操作本轮在 PR #7910 中未发现需要处理的审查反馈:
PR 差异(2 个文件,+51/−4)未发生变化。本轮未进行任何代码更改。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— qwen3.8-max-preview via Qwen Code /review
Local verification report — real build, real browserI verified this locally by rendering the real Verdict: the mechanism is correct and the added regression test is non-vacuous — but the fix only covers half the entry points. The half it misses includes the PR's own manual repro. One line closes the rest; details and a verified patch below. ✅ What is verified working
Real-browser proof of the fix, starting from the state the PR describes ( Left panel is a host that routes on
|
| Mutation of the new code | Suite |
|---|---|
always selectedWorkspaceCwd ?? connection.workspaceCwd (drop the active-session branch) |
survives |
connection.workspaceCwd (full revert) |
killed |
drop the ?? connection.workspaceCwd fallback |
survives |
| invert the ternary condition | killed |
swap the ?? operands |
killed |
look up by the new value but report connection.workspaceCwd |
killed |
keep the dedupe guard keyed on connection.workspaceCwd |
survives |
drop selectedWorkspaceCwd from the dependency array |
survives |
look up activeWorkspace by the old value |
killed |
Two of the four die with two small tests worth adding:
- active session + a conflicting next-session pick → must still report the session's own workspace (this is the “behaviour with an active session is unchanged” claim from the description; nothing currently pins it);
- deferred workspace switch A → B with no session → the host must be notified for each switch (this is the new notification path; today only a
sessionIdchange makes the effect re-run, so dropping the dependency goes unnoticed).
中文完整版
本地验证报告 —— 真实构建、真实浏览器
我在本地做了真实验证:在无头 Chromium 中渲染真实的 <App/>(daemon 层打桩),点击真实的侧栏按钮,并在 merge base(b3873571a)与本 PR head(f18c257e3)之间对 client/App.tsx 做 A/B。两侧都跑了完整单测。
结论:机制正确,新增的回归测试也不是空测试 —— 但这个修复只覆盖了一半的入口,而漏掉的那一半正好包含 PR 自己写的手动复现步骤。 一行即可补齐,补丁在下面,我已验证过。
✅ 已验证有效的部分
| 检查项 | 结果 |
|---|---|
本 head 上的 client/App.test.tsx |
234 / 234 通过 |
本 head 上整个 @qwen-code/web-shell 套件 |
2626 通过;仅有的 10 个失败是 build-artifact.test.ts(ENOENT … /dist/index.js,需要先 npm run build;在 base 上同样失败) |
| 新回归测试非空测 | 在 base 上失败,实收 (undefined, 'secondary', '/work/secondary') —— 确实钉住了残留 workspace |
| 有活跃会话时行为不变 | 故意冲突的「下一会话」选择不会泄漏进活跃会话的上报 |
死会话(missingSession) |
依然静默,与之前一致 |
| 无会话时切换 workspace | base 只发出初始那一条上报;本 head 每次切换都上报 —— 即 Risk & Scope 中提到的行为变化,符合描述 |
修复生效的真实浏览器证据,起始状态即 PR 描述的场景(session-1 活跃在非 primary 的 wsA),点击另一个非 primary workspace wsB 下的「New task」:
左栏是一个会根据 onSessionIdChange 路由并重挂 shell 的 host,行为对齐 WorkspaceSessionProvider。修复前:host 被路由回 wsA,composer 标签卡在 a。修复后:host 路由到 wsB,标签显示 b。完全符合预期。
⚠️ 发现 1(主要)—— 这里的 undefined 表示 primary,不是「未知」
在本代码库中,selectedWorkspaceCwd === undefined 不是「没有选择」,而正是「primary workspace」的写法:
client/components/WorkspaceSelector.tsx:130→onSelectWorkspace(next.primary ? undefined : next.cwd)client/components/sidebar/WebShellSidebar.tsx:4231→const wsCwd = ws.primary ? undefined : ws.cwd;(在:4274传给handleNewSession)- 侧栏顶部的 New task(
:3999)直接handleNewSession()不带参数;/clear、/new、/reset、shellApi.createNewSession()、缺失会话恢复、新会话建议、定时任务「create via chat」、Goals 也都一样 —— 全部走到createNewSession()→setSelectedWorkspaceCwd(undefined)。
因此在这些路径上,selectedWorkspaceCwd ?? connection.workspaceCwd(App.tsx:4627)会直接回落到本 PR 想要避开的那个残留值。
这其中就包含 PR 自己的手动测试计划。 「点击 workspace A 下的 new task」在 A 是 primary 项目(最常见的情况)时,走的正是 wsCwd = undefined。我在真实浏览器里点了这个按钮,BASE 与 PR 的截图逐字节相同(两者 md5 acc1afc2…):
侧栏顶部的 New task 同理(BASE 与 PR 截图同样逐字节相同,md5 4564707e…):
这个分歧不只是显示问题。我在 secondary workspace 的会话里执行 /clear 后,让延迟会话真正被创建出来,并同时记录了两侧:
daemon 被要求创建会话的 workspace = "/workspace"(primary)
composer 标签 = "/workspace"
上报给 host 的 = (undefined, "secondary", "/work/secondary")
ensureSessionForPrompt 解析新会话 workspace 的表达式是 lockedWorkspaceCwd ?? acceptedWorkspaceCwd ?? primaryWorkspaceCwd(App.tsx:3312),connection.workspaceCwd 完全不参与。也就是说,host 被路由到了一个新会话根本不会落在其中的 workspace。
建议的修复(一行,已验证)
activeWorkspaceCwd(App.tsx:1570)的语义恰好就是**「会话所在 workspace,否则是为下一个会话选定的 workspace(locked / selected / primary)」**,而且 composer 标签与 getComposerWorkspaceCwd()(App.tsx:3359)用的就是它。复用它可以从构造上保证 host 上报与标签一致:
- const reportedWorkspaceCwd = connection.sessionId
- ? connection.workspaceCwd
- : (selectedWorkspaceCwd ?? connection.workspaceCwd);
+ const reportedWorkspaceCwd = activeWorkspaceCwd ?? connection.workspaceCwd; onSessionIdChange,
- selectedWorkspaceCwd,
+ activeWorkspaceCwd,
workspace.capabilities,我实测过:上面三个浏览器场景全部变正确(后两张截图的右侧面板);「创建所在 workspace」与「上报 workspace」一致;你新增的回归测试仍然通过;App.test.tsx 保持 234 / 234;整个包保持 2626 通过。同时也消除了同一意图的第三份近似重复实现。
⚠️ 发现 2(低)—— 未受信任的 workspace 会被上报一拍
侧栏的 onSelectWorkspace={setSelectedWorkspaceCwd} 写入选择时不做信任校验,随后由 trust-clearing effect(App.tsx:1509-1518)在下一拍重置。无活跃会话时,base 发出一条上报(残留 workspace);本 head 发出两条,第一条是 (undefined, 'untrusted', '/work/untrusted')。
不会创建出不安全的东西 —— ensureSessionForPrompt 仍然按 trusted === true 过滤 —— 但会路由的 host 会短暂挂载到未受信任 workspace 的路由上。上面建议的一行改动也改善了随后的回落(回落到 primary,而不是残留 workspace)。
📝 发现 3(文档)—— 出厂的 standalone web shell 无法复现该现象
建议在描述里补一句,否则 reviewer 按 「Manual (multi-workspace deployment)」 在 qwen serve 上什么都看不到:
client/main.tsx:150——handleSessionIdChange只接收(sessionId, workspaceId),第三个参数被丢弃。client/main.tsx:96—— 只要sessionId为 undefined(新建会话正是这个状态),replaceStandaloneSessionUrl就会删除workspace参数。所以上报的 workspace 在这里根本影响不到 URL。client/main.tsx:124-127——sessionId/workspaceId在挂载时只读一次(const [sessionId] = useState(...)),standalone 应用在运行期从不重新路由或重挂。
我的 harness 会打印 standalone host 会算出的 URL:在所有 arm、所有场景下都是 /。真正能观察到该现象的是使用 @qwen-code/web-shell 的 WebShell / WebShellWithProviders 且基于该回调做路由的宿主 —— 在描述中点明这个宿主,复现步骤才可执行。
(前提本身是成立的:packages/webui/src/daemon/session/actions.ts:84 的 getConnectionAfterSessionClear 删除了 sessionId、clientId、displayName、tokenUsage、tokenCount、supportedCommands、context,并且有意保留 workspaceCwd。)
🧪 测试强度 —— 9 个变异中有 4 个能在 234 个测试下存活
| 对新代码的变异 | 套件结果 |
|---|---|
恒取 selectedWorkspaceCwd ?? connection.workspaceCwd(去掉活跃会话分支) |
存活 |
恒取 connection.workspaceCwd(完全回退) |
被杀 |
去掉 ?? connection.workspaceCwd 兜底 |
存活 |
| 反转三元条件 | 被杀 |
交换 ?? 两侧 |
被杀 |
用新值查找但仍上报 connection.workspaceCwd |
被杀 |
去重守卫仍以 connection.workspaceCwd 为键 |
存活 |
从依赖数组中删掉 selectedWorkspaceCwd |
存活 |
activeWorkspace 仍按旧值查找 |
被杀 |
其中两个可以用两个小测试杀掉,建议补上:
- 有活跃会话 + 冲突的下一会话选择 → 仍必须上报会话自己的 workspace(这正是描述里「有活跃会话时行为不变」的断言,目前没有任何测试钉住它);
- 无会话时 workspace 从 A 切到 B → 每次切换 host 都必须收到通知(这是新增的通知路径;目前只有
sessionId变化才会让 effect 重跑,所以删掉该依赖不会被发现)。
|
🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Review round summary — PR #7910Addressed the inline suggestion and the matching main finding from the local Feedback points and decisions[rc:3664011121] [Suggestion] — no-session fallback reverts to the stale The report effect computed Change:
Finding 1 (main, issue-level report) — same root cause → Implemented by the Test coverage suggestions (issue-level report) → Implemented. Added three
Finding 2 (Low, issue-level report) — untrusted workspace reported for one Finding 3 (docs, issue-level report) — standalone shell cannot show this → Conflict notes
Verification
中文说明审查轮次总结 — PR #7910通过复用已有的 反馈点与处理[rc:3664011121] [建议] — 无会话时的回退退回到过期的 上报 effect 在无会话时计算 改动:
发现 1(主要,issue 级报告)— 同一根因 → 已实现,即上面这处改动。 测试覆盖建议(issue 级报告)→ 已实现。 在
发现 2(低,issue 级报告)— 未受信任的 workspace 会被上报一拍 → 部分处理;拒绝额外加固。 同一处改动已经改善了随后的上报:在 trust-clearing effect 重置选择之后,上报现在回退到 primary workspace,而不是过期的那个(正如报告所指出的)。刚选中的未受信任 workspace 在第一拍仍会被短暂上报。这属于既有行为(base 在选择时同样会发出一条上报),并不存在安全问题( 发现 3(文档,issue 级报告)— 出厂 standalone shell 无法复现 → 已记录,不改代码。 这是一条 PR 描述层面的澄清(该可观察现象适用于基于 冲突说明
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
中文说明
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No changes were made this round. The only review feedback newer than the last evaluation is an automated Since there is nothing actionable, the PR head is left unchanged. 中文说明本轮未做任何改动。 自上次评估以来唯一新增的审查反馈是 @qwen-code-ci-bot 发出的一条自动 由于没有任何可操作的内容,PR 的 head 分支保持不变。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
Local verification report — real browser, real client, before/afterI built a local harness and verified this PR end-to-end at 1. Unit A/B
Worth noting: the fourth new test ( 2. Real-browser A/B — the fix works, and it is not cosmeticTwo workspaces (
That last row is the important one: this is not just a wrong label — on base the next chat is really created in the wrong workspace, so the agent would run against the wrong repo. Stability: 9/9 over I also measured the trade-off called out in "Risk & Scope" (a deferred workspace switch now notifies the host): an unsent composer draft is dropped by the workspace switch itself on both base and head, so the extra notification costs nothing there. 3. Findings the reviewer should weighF1 — the motivating symptom still reproduces in the standalone shell (pre-existing, not caused and not fixed by this PR). The cause is a second copy of the same stale value: if (currentSessionId || selectedWorkspaceCwd !== undefined) return;
…
if (connectedWorkspace && !connectedWorkspace.primary) {
onSelectWorkspace?.(connectedWorkspace.cwd); // stale connection.workspaceCwdBecause "new chat in the primary workspace" is spelled F2 — new with this PR: the report can stop converging for a host that routes asynchronously.
F3 — small contract wart (pre-existing). On every (re)mount the shell first reports 4. RecommendationThe change itself is correct, minimal, well-tested and does exactly what it claims for re-mounting embedder hosts, including the part that matters most (the workspace the next session is created in). I have no objection to merging it. I would follow up on ReproductionWorktree at # unit A/B
cd packages/web-shell && npx vitest run --config vitest.config.ts client/App.test.tsx
git checkout 9461aa8 -- packages/web-shell/client/App.tsx # then re-run for the red side
# browser A/B (vite dev server is started by the playwright config)
npx playwright test --config playwright.config.ts pr7910The harness is an extra page under 中文说明本地验证报告 —— 真实浏览器、真实前端、before/after 对比我在本地搭建了验证环境,对 1. 单测 A/B
补充一点:第四个新增用例( 2. 真实浏览器 A/B —— 修复有效,而且不只是「显示问题」两个 workspace(
最后一行最关键:这不只是标签显示错了——在 base 上,新会话确实被创建在错误的 workspace 里,agent 会跑在错误的仓库上。稳定性: (截图见英文部分) 我也实测了「风险与范围」里提到的权衡(无会话时切换 workspace 现在也会通知宿主):composer 里未发送的草稿在 base 和 head 上都会被这次 workspace 切换本身清掉,所以这条新增通知在这一点上没有额外代价。 3. 需要评审一并权衡的发现F1 —— 触发本 PR 的那个现象,在 standalone shell 里依然复现(既非本 PR 引入,也未被本 PR 修复)。 根因是同一个残留值的第二处副本: if (currentSessionId || selectedWorkspaceCwd !== undefined) return;
…
if (connectedWorkspace && !connectedWorkspace.primary) {
onSelectWorkspace?.(connectedWorkspace.cwd); // 残留的 connection.workspaceCwd而「在 primary 里新建会话」的表示方式恰恰就是 F2 —— 本 PR 新引入:对于异步路由的宿主,上报可能不再收敛。
F3 —— 一个小的契约瑕疵(既有问题)。 每次(重新)挂载时,shell 会先上报一次 4. 结论与建议改动本身是正确的、最小的、测试也到位,对「会重新挂载的嵌入宿主」确实做到了它承诺的事,包括最要紧的那一点——下一个会话实际创建在哪个 workspace。我不反对合并。 建议后续单开一个 PR 处理 Verified locally with Claude Code (Opus 5, 1M context) — real Chromium E2E against the repo's mock daemon; harness kept outside the PR. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Done. Replied on PR #7910 acknowledging wenshao's verification report. No code changes needed — the fix is confirmed working for its intended scope (re-mounting embedder hosts). Agreed to:
✅ completed |
|
Thanks for the incredibly thorough verification — the real-browser A/B with the routing host harness is exactly the kind of evidence that makes this review solid. Agree on all three findings:
No code changes needed in this PR — the fix does what it claims for re-mounting embedder hosts, and the follow-up belongs in the sidebar. Appreciate the harness offer; if you push it to a branch I would be happy to see it land as a regression test separately. |
doudouOUC
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
中文说明
未发现问题。LGTM!✅
— qwen3.7-max via Qwen Code /review
Local real-stack verificationI verified this locally against a real stack rather than only the mocked unit tests, so the routing behaviour is exercised end to end. What "real" means here
1. Unit RED/GREEN — confirmed, with one caveat about the baselineAgainst the merge base, 3 of the 4 new tests fail and all 237 pass with the PR: Two notes for anyone re-running this:
2. Real browser A/B — the fix works ✅Ran a real turn in
The reported bug reproduces on main in the real stack and is gone with this PR. That is the claim in the PR body, confirmed outside the unit mocks. 3. Residual gap — the shipped standalone Web Shell is not fixed by this 🟠Same daemon and workspaces, but driving the shipped entry (
The "next prompt ran in" column is daemon ground truth ( The determinant is not // no session + selection unset -> re-select whatever the connection last pointed at
const connectedWorkspace = workspaces.find((e) => e.cwd === connection.workspaceCwd);
if (connectedWorkspace && !connectedWorkspace.primary) {
onSelectWorkspace?.(connectedWorkspace.cwd);
This is not a regression — main behaves identically. But the PR body's manual test plan ("Expected: the composer's workspace indicator shows A") does not hold for One more thing seen on both arms, so also pre-existing: during a re-mount the shell briefly reports 4. Regression checks
VerdictThe change is correct and does fix the reported bug for hosts that route on 🤖 Generated with Claude Code — Claude Opus 5 (1M context) 中文说明本地真实环境验证我没有只跑带 mock 的单测,而是在本地搭了一套真实链路做端到端验证。 这里的「真实」指
1. 单测 RED/GREEN —— 确认成立,但基线有个坑以 merge base 为基线,4 个新测试中有 3 个失败,打上本 PR 后 237 个全过: 给后面复现的人两点提醒:
2. 真实浏览器 A/B —— 修复有效 ✅先在
所报告的问题在真实链路的 main 上确实复现,打上本 PR 后消失。这正是 PR 描述里的主张,且是在单测 mock 之外得到确认的。 3. 遗留缺口 —— 出货形态的 standalone Web Shell 并没有被修好 🟠同一个 daemon、同样两个 workspace,但驱动的是出货入口(
「下一条 prompt 实际运行于」这一列是 daemon 侧的事实( 决定性因素根本不在 // 无 session + 未选中 -> 重新选中 connection 上一次指向的 workspace
const connectedWorkspace = workspaces.find((e) => e.cwd === connection.workspaceCwd);
if (connectedWorkspace && !connectedWorkspace.primary) {
onSelectWorkspace?.(connectedWorkspace.cwd);
这不是回归——main 的表现完全一致。但 PR 描述里的手动测试计划(「预期:composer 的 workspace 指示显示 A」)对 还有一点在两个分支上都能看到,所以同样是既有问题:重新挂载期间 shell 会短暂上报 4. 回归检查
结论改动是正确的,对于按 |
yiliang114
left a comment
There was a problem hiding this comment.
LGTM — clean fix. No active session → report locked ?? selected ?? primary workspace instead of stale connection cwd. Active-session path unchanged. Last-notified guard consistent. 4 regression tests cover the bug scenario, /clear, active-session isolation, and selection re-notification.
doudouOUC
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
中文说明
未发现问题。LGTM!✅
— qwen3.7-max via Qwen Code /review
|
Released in v0.21.1. |









What this PR does
When you start a new chat in a multi-workspace Web Shell, the workspace reported to the host (and therefore shown in the composer's workspace chip/selector) is now the workspace you actually picked for the new chat. Previously, if another workspace had the active session — for example one with a running task — the new chat could be reported against that other workspace instead, so the composer showed the wrong workspace right after the new session appeared.
Why it's needed
Clearing a session to start a fresh chat drops the session id but keeps the connection's last workspace around as a leftover. The notification that tells the host "the view changed" read that leftover workspace, so a "new chat in workspace A" was routed back to the previous workspace B. The host then re-mounted the shell for workspace B, and the composer displayed B even though the user asked for A. The leftover workspace only matters when there is no active session, which is exactly the state a cleared/new chat is in — so with no active session we now report the workspace picked for the next session rather than the stale connection workspace. Behavior with an active session is unchanged.
Reviewer Test Plan
How to verify
main(reports the stale secondary) and passes with this change.Evidence (Before & After)
Verified via the regression test in
client/App.test.tsx:reports the selected workspace...fails —onSessionIdChangewas called with(undefined, 'secondary', '/work/secondary').onSessionIdChangeis called with(undefined, undefined, '/workspace')and never with the stale secondary.client/App.test.tsxsuite: 234 passed.Manual TUI capture in a real multi-workspace daemon was not recorded; the routing behavior is exercised by the unit test above.
Tested on
Environment (optional)
Unit tests only (
npx vitest run client/App.test.tsxinpackages/web-shell), plus package typecheck and ESLint.Risk & Scope
Linked Issues
中文说明
这个 PR 做了什么
在多 workspace 的 Web Shell 中新建会话时,上报给 host(也就是 composer 里 workspace 标签/选择器显示的)workspace,现在会是你真正为本次新会话选择的 workspace。之前,如果另一个 workspace 持有当前活跃会话(例如它有正在运行的任务),新会话可能被上报成那个 workspace,导致新会话出现后 composer 立刻显示错误的 workspace。
为什么需要
清空会话以开启新聊天时,会清掉 session id,但 connection 上一次的 workspace 会作为残留保留下来。那个告诉 host「视图变了」的通知读取了这个残留 workspace,于是「在 A 新建会话」被路由回了上一个 workspace B。host 随后按 workspace B 重新挂载 shell,composer 就显示 B,即使用户要的是 A。残留 workspace 只在没有活跃会话时才有影响,而清空/新建会话恰恰就处于这种状态——所以在没有活跃会话时,现在上报「为下一个会话选择的 workspace」,而不是残留的 connection workspace。有活跃会话时的行为保持不变。
评审测试计划
如何验证
main上失败(上报残留的 secondary),在本改动后通过。证据(Before & After)
通过
client/App.test.tsx中的回归测试验证:reports the selected workspace...失败——onSessionIdChange被以(undefined, 'secondary', '/work/secondary')调用。onSessionIdChange被以(undefined, undefined, '/workspace')调用,且从不以残留的 secondary 调用。client/App.test.tsx套件:234 通过。未在真实多 workspace daemon 中录制手动 TUI;上述路由行为由单测覆盖。
测试环境
环境(可选)
仅单测(在
packages/web-shell中npx vitest run client/App.test.tsx),外加 package typecheck 与 ESLint。风险与范围
关联 Issue