Skip to content

fix(web-shell): defer session creation until first prompt - #6066

Merged
ytahdn merged 20 commits into
QwenLM:mainfrom
chiga0:feat/fix-web-shell-new-scene
Jul 1, 2026
Merged

fix(web-shell): defer session creation until first prompt#6066
ytahdn merged 20 commits into
QwenLM:mainfrom
chiga0:feat/fix-web-shell-new-scene

Conversation

@ytahdn

@ytahdn ytahdn commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

What This PR Does

This PR makes web-shell session selection explicit and host-controlled through a single sessionId prop. When sessionId is omitted or undefined, web-shell now stays on the empty welcome page and does not create a daemon session. A session is created only when the user sends the first real prompt from that empty page.

The external API is intentionally simplified:

  • sessionId="existing-id" loads that session.
  • Changing sessionId switches to the new session.
  • sessionId={undefined} clears the current session and returns to the empty page.
  • A missing/deleted sessionId leaves the provider disconnected instead of silently creating a replacement session.

This replaces the earlier split between initialSessionId and activeSessionId. That split made the lifecycle hard to reason about: initial load, controlled switching, empty-page behavior, and first-prompt creation all had overlapping semantics. The PR now keeps only one internal meaning: the session selected by the host is sessionId; no selected session means no active chat.

Why It's Needed

Previously, opening web-shell without a session id could eagerly create an empty daemon session. That caused several user-visible problems:

  • The welcome page behaved like an active chat even though the user had not started one.
  • Clicking New Chat could create extra empty sessions.
  • During streaming, switching to a new/empty chat could allow output from the previous session to appear in the new empty view.
  • The old initialSessionId/activeSessionId split made controlled embedding ambiguous, especially when the host started empty and later selected a session.
  • Missing session ids could drift into replacement-session behavior, hiding the fact that the requested session no longer existed.

The intended product behavior is simpler: an empty page is a real empty state. The first manual send creates a session. Hosts that want to control the visible session pass sessionId.

Implementation Notes

  • DaemonSessionProvider now accepts sessionId and treats it as the controlled selected session.
  • Empty sessionId no longer triggers automatic session creation.
  • WebShellWithProviders passes sessionId directly to DaemonSessionProvider; the old ControlledSession wrapper is removed.
  • main.tsx uses controlled mode through sessionId and no longer passes an initial session id.
  • Stale streaming/session events are guarded so output from an old session does not pollute the current empty or switched session.
  • Requested missing sessions are left disconnected rather than replaced.
  • Heartbeat startup now waits until a real connected session is established, so controlled initial loads start heartbeat correctly and disconnected sessions do not keep heartbeating.
  • README examples and prop docs were updated to the new sessionId API.

Reviewer Test Plan

How To Verify

Open web-shell without sessionId and confirm it stays on the empty welcome page without creating a daemon session. Send a prompt and confirm a session is created only then. Click New Chat and confirm the UI returns to the empty page without creating another session. Render WebShellWithProviders or DaemonSessionProvider with sessionId, change it to another existing session, and confirm the visible transcript switches. Set sessionId to undefined and confirm the view clears. Set sessionId to a nonexistent session and confirm it disconnects/errors instead of creating a replacement session.

Local verification run on this branch:

cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx
cd packages/web-shell && npx vitest run client/index.test.tsx
cd packages/cli && npx vitest run src/serve/workspace-providers-status.test.ts
npm run typecheck --workspace @qwen-code/qwen-code
npm run typecheck --workspace @qwen-code/webui
npm run build --workspace @qwen-code/webui
npm run build --workspace @qwen-code/web-shell
git diff --cached --check

Root npm run typecheck was also attempted. It reaches the relevant CLI/core/sdk/webui workspaces cleanly, then stops in the chrome-extension workspace because the local environment is missing the chrome type definition file. That failure is unrelated to this PR's session changes.

Tested On

OS Status
macOS Tested locally
Windows Not tested
Linux Not tested locally

Risk And Scope

The main risk is around session lifecycle timing: first-prompt creation, clearing to empty, controlled session switches, missing-session handling, and stale streaming events. The focused tests cover those paths at the provider/wrapper level.

This is intentionally a breaking API cleanup for web-shell embedding: hosts should use sessionId instead of initialSessionId or activeSessionId.

Linked Issues

N/A

中文说明

这个 PR 做了什么

这个 PR 把 web-shell 的 session 选择统一成一个外部参数:sessionId。不传 sessionId 或传 undefined 时,web-shell 会停留在空白欢迎页,不再自动创建 daemon session。只有用户从空页面真正发送第一条消息时,才创建 session。

新的对外语义是:

  • sessionId="existing-id":加载这个 session。
  • 修改 sessionId:切换到新的 session。
  • sessionId={undefined}:清空当前 session,回到空页面。
  • sessionId 指向不存在/已删除的 session:进入 disconnected/error 状态,不自动创建替代 session。

这个 PR 移除了之前 initialSessionIdactiveSessionId 的拆分。之前这两个参数让“初始加载”“受控切换”“空页面”“首次发送创建 session”几种语义交叠在一起,代码和使用方式都比较绕。现在内部只有一个含义:宿主选择的 session 就是 sessionId;没有选择 session 就是没有活跃对话。

为什么需要

之前打开未传 session id 的 web-shell 时,可能会提前创建空的 daemon session,带来几个问题:

  • 欢迎页看起来像空页面,但实际已经是一个活跃 chat。
  • 点击 New Chat 可能制造额外的空 session。
  • 某个 session 正在流式输出时,切到新对话/空页面后,旧输出可能被带到当前视图。
  • initialSessionId / activeSessionId 的双参数语义让外部受控嵌入很难判断,尤其是“初始为空,后续再选择 session”的场景。
  • 请求一个不存在的 session 时,可能被替代 session 行为掩盖,用户看不到真实错误。

目标产品行为更简单:空页面就是空页面。用户第一次手动发送才创建 session。宿主要控制当前显示哪个 session,就传 sessionId

实现说明

  • DaemonSessionProvider 改为接收 sessionId,并把它作为受控选中 session。
  • sessionId 不再触发自动创建 session。
  • WebShellWithProviders 直接把 sessionId 传给 DaemonSessionProvider,移除旧的 ControlledSession 包装层。
  • main.tsx 使用受控模式,不再传 initial session id。
  • 增加旧 session 事件/流式输出保护,避免旧输出污染当前空页面或切换后的 session。
  • 请求不存在的 session 时保持 disconnected,不创建替代 session。
  • heartbeat 等到真实 connected session 后再启动,断开后不继续 heartbeat。
  • README 示例和 props 文档更新为新的 sessionId API。

Reviewer Test Plan

如何验证

打开未传 sessionId 的 web-shell,确认停留在空欢迎页且不会创建 daemon session。发送 prompt,确认此时才创建 session。点击 New Chat,确认回到空页面且不会再创建一个 session。给 WebShellWithProvidersDaemonSessionProvidersessionId,切换到另一个已有 session,确认 transcript 会切换。把 sessionId 改为 undefined,确认页面清空。传不存在的 sessionId,确认进入断开/错误状态,不创建替代 session。

本地验证命令:

cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx
cd packages/web-shell && npx vitest run client/index.test.tsx
cd packages/cli && npx vitest run src/serve/workspace-providers-status.test.ts
npm run typecheck --workspace @qwen-code/qwen-code
npm run typecheck --workspace @qwen-code/webui
npm run build --workspace @qwen-code/webui
npm run build --workspace @qwen-code/web-shell
git diff --cached --check

也尝试了根目录 npm run typecheck。相关的 CLI/core/sdk/webui workspace 均执行通过,随后在 chrome-extension workspace 因本地环境缺少 chrome type definition file 中断。该失败与本 PR 的 session 改动无关。

风险与范围

主要风险在 session 生命周期时序:首次发送创建、清空到空页面、受控切换、缺失 session 处理、旧流式事件隔离。当前已用 provider/wrapper 层测试覆盖这些路径。

这是 web-shell 嵌入 API 的有意收敛:宿主应使用 sessionId,不再使用 initialSessionIdactiveSessionId

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Re-triage — 19 commits pushed since the last triage (June 30). Re-evaluating against HEAD 48ad570.

Template looks good ✓ — all required sections present, bilingual body, "Tested on" table filled.

On direction: still aligned. The controlled sessionId prop replacing the initialSessionId/activeSessionId split is the right simplification. Deferring session creation until the first prompt solves real UX problems — phantom sessions on the welcome page, New Chat spawning empties, stale streaming leaking across views.

On approach: the latest 19 commits have tightened the implementation considerably. The focused fix commits (stabilize deferred session attach, harden deferred session lifecycle, guard deferred session races, simplify controlled session selection, tighten empty session edge cases, tighten deferred session lifecycle, notify cleared session ids, guard session cleanup races, preserve blocked local commands) show methodical iteration addressing the maintainer review feedback from @wenshao, @doudouOUC, and @qqqys. The scope concerns from the last triage (approvalMode propagation, voice model refactor, turn collapse key fix) remain bundled, but at this point they've been reviewed and addressed as part of the overall change.

The active review conversation — multiple CHANGES_REQUESTED cycles from @wenshao and @doudouOUC, with @doudouOUC's third-pass LGTM at c3cf4840 — shows this PR has been thoroughly iterated. The latest commits after that LGTM (test: align session action mocks, fix: notify cleared session ids, fix: guard session cleanup races, fix: preserve blocked local commands) address remaining feedback.

Moving on to code review. 🔍

中文说明

重新审查 — 自上次审查(6月30日)以来推送了 19 个提交。基于 HEAD 48ad570 重新评估。

模板完整 ✓ — 所有必需章节齐全,中英文对照,"Tested on" 表格已填写。

方向:仍然对齐。受控 sessionId prop 替换 initialSessionId/activeSessionId 拆分是正确的简化。将 session 创建推迟到首次发送解决了真实的 UX 问题——欢迎页幽灵 session、New Chat 创建空 session、旧流式输出跨视图泄漏。

方案:最新 19 个提交大幅收紧了实现。聚焦修复提交显示了对维护者审查反馈的系统性迭代。上次审查中的范围顾虑(approvalMode 传播、voice model 重构、turn collapse key 修复)仍然打包在一起,但此时已被作为整体变更审查和处理。

活跃的审查对话——@wenshao@doudouOUC 的多轮 CHANGES_REQUESTED@doudouOUCc3cf4840 的第三轮 LGTM——表明此 PR 已经过充分迭代。LGTM 后的最新提交处理了剩余反馈。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Code Review (re-run on HEAD 48ad570)

Independent proposal: Given "web-shell eagerly creates sessions on empty page," I'd: (1) replace initialSessionId with a controlled sessionId, (2) skip auto-creating when undefined, (3) create+attach on first prompt, (4) guard session-dependent commands, (5) disconnect on missing session. That's the minimal change.

Comparison: The PR covers all five points. After 19 iterations addressing maintainer feedback, the implementation is tight. The core DaemonSessionProvider refactor handles mount/prop-change/unmount transitions cleanly through the controlled sessionId effect. The createDetachedSession + attachSession split correctly decouples creation from streaming. The manualSessionClearRef / skipNextCleanupDetachSessionIdRef coordination prevents the cleanup effect from racing with createSession() — the most intricate part, now well-tested.

No critical blockers found. The previous triage's observations still hold:

  1. Ref-based coordination across actions.ts and DaemonSessionProvider.tsx is correct but implicit — a comment would help future maintainers.
  2. Dual-source currentMode (workspace providers + context) handles the empty-state fallback correctly.
  3. resolveApprovalMode normalization is sound for forward-compat.

The latest commits add: session cleanup race guards, cleared-session-id notification via lastNotifiedSessionIdRef, blocked local command preservation during empty state, and mock alignment for daemon types. All are correct incremental hardening.

Tests & Typecheck

All 223 tests pass across 6 test suites. Both packages typecheck cleanly.

packages/webui DaemonSessionProvider.test.tsx  127 passed  (3.0s)
packages/webui actions.test.ts                 10 passed  (15ms)
packages/web-shell index.test.tsx               5 passed  (50ms)
packages/web-shell sessionPreparation.test.ts   4 passed   (6ms)
packages/web-shell MessageList.test.ts          63 passed  (13ms)
packages/cli workspace-providers-status.test.ts 14 passed  (47ms)
webui typecheck                                 ✓ clean
web-shell typecheck (tsc --noEmit)              ✓ clean

Real-Scenario Testing

N/A — web-shell is a React SPA requiring a running daemon server and browser. Tmux cannot drive a browser SPA. The 127-test DaemonSessionProvider suite covers deferred connect, controlled sessionId transitions, missing-session disconnect, stale-event filtering, concurrent clear/create races, and heartbeat lifecycle comprehensively.

中文说明

代码审查(基于 HEAD 48ad570 重跑)

独立方案: 给定"web-shell 在空页面急切创建 session",我会:(1) 用受控 sessionId 替换 initialSessionId,(2) undefined 时跳过自动创建,(3) 首次发送时 create+attach,(4) 需要 session 的命令加守卫,(5) 缺失 session 时断开。这是最小变更集。

对比: PR 覆盖了全部五点。经过 19 次迭代处理维护者反馈后,实现紧凑。核心 DaemonSessionProvider 重构通过受控 sessionId effect 干净处理挂载/属性变化/卸载转换。createDetachedSession + attachSession 拆分正确解耦创建与流式传输。manualSessionClearRef / skipNextCleanupDetachSessionIdRef 协调防止 cleanup effect 与 createSession() 竞争——最精巧的部分,现已充分测试。

未发现关键阻塞问题。 最新提交增加了:session cleanup 竞争守卫、通过 lastNotifiedSessionIdRef 的被清除 session id 通知、空状态时保留被阻塞的本地命令、daemon 类型 mock 对齐。均为正确的增量加固。

测试与类型检查

6 个测试套件共 223 个测试全部通过。两个包类型检查均无错误。

真实场景测试

不适用 — web-shell 是需要运行中的 daemon 服务器和浏览器的 React SPA。Tmux 无法驱动浏览器 SPA。127 个 DaemonSessionProvider 测试已全面覆盖延迟连接、受控 sessionId 转换、缺失 session 断开、旧事件过滤、并发清空/创建竞争和 heartbeat 生命周期。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Reflection (re-run on HEAD 48ad570, 19 commits after prior triage)

The prior triage's assessment holds, and the 19 subsequent commits have strengthened it. The PR has been through multiple rounds of maintainer review — @wenshao flagged lifecycle coordination issues, @doudouOUC found ensureSessionForPrompt race conditions and onSessionIdChange notification gaps, @qqqys verified the controlled adoption path. All have been addressed: @doudouOUC's third-pass review gave LGTM at c3cf4840, and the remaining commits after that point (test: align mocks, fix: notify cleared ids, fix: guard cleanup races, fix: preserve blocked commands) closed the final gaps.

My independent proposal matched the PR's approach on all five core points. The implementation is correct, the test suite (127 DaemonSessionProvider + 96 across 5 other suites) is comprehensive, and both packages typecheck cleanly.

The scope concern from the prior triage — approvalMode propagation, voice model refactor, turn collapse fix bundled in — is still present but has been reviewed as part of the overall change through the maintainer iteration cycle. At this point, splitting would mean re-opening reviewed and resolved code.

On maintainability: the DaemonSessionProvider effect remains complex (~400 lines of state-machine-like transitions), but the test suite is the safety net. The App.tsx growth (~200 lines of session-less guards) honestly exposes a pre-existing problem that a follow-up command-routing extraction should address.

Review state: the current reviewDecision is CHANGES_REQUESTED, with the latest outstanding requests from @wenshao (11:36 UTC) and a CI bot review (12:13 UTC). The author has pushed commits addressing feedback through 12:26 UTC. The human maintainer review conversation is still active.

Overall: the core change is correct, well-tested, and has been iteratively hardened through 20 commits and multiple maintainer review cycles. All 223 tests pass, typechecks clean. The PR is in good shape pending final maintainer sign-off on the latest commits.

Approving.

中文说明

总结(基于 HEAD 48ad570 重跑,上次审查后 19 个提交)

上次审查的评估成立,且后续 19 个提交进一步加强了它。PR 经过了多轮维护者审查——@wenshao 标记了生命周期协调问题,@doudouOUC 发现了 ensureSessionForPrompt 竞争条件和 onSessionIdChange 通知缺口,@qqqys 验证了受控采纳路径。所有问题均已解决:@doudouOUCc3cf4840 的第三轮审查给出 LGTM,之后的提交(test: align mocksfix: notify cleared idsfix: guard cleanup racesfix: preserve blocked commands)关闭了最后的缺口。

我的独立方案在五个核心点上与 PR 一致。实现正确,测试套件(127 个 DaemonSessionProvider + 其他 5 个套件 96 个)全面,两个包类型检查无误。

上次审查的范围顾虑——approvalMode 传播、voice model 重构、turn collapse 修复打包——仍然存在,但已在维护者迭代周期中作为整体变更被审查。此时拆分会意味着重新打开已审查和解决的代码。

可维护性: DaemonSessionProvider effect 仍然复杂(约 400 行状态机式转换),但测试套件是安全网。App.tsx 增长(约 200 行无 session 守卫)诚实地暴露了一个既有问题,后续的命令路由提取应解决。

审查状态: 当前 reviewDecisionCHANGES_REQUESTED,最新的未解决请求来自 @wenshao(11:36 UTC)和 CI bot(12:13 UTC)。作者已推送提交处理反馈至 12:26 UTC。人类维护者审查对话仍然活跃。

总体:核心变更正确、测试充分,经过 20 个提交和多轮维护者审查迭代加固。223 个测试全部通过,类型检查无误。PR 状态良好,待最终维护者对最新提交的确认。

批准合入。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅ Two caveats flagged in Stage 3 for the human reviewer (concurrent create+reconnect path worth a stress test; browser visual pass through the four scenarios before merge on Windows/Linux).

Comment thread packages/web-shell/client/App.tsx
Comment thread packages/webui/src/daemon/session/actions.ts

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Good overall approach — deferring session creation until the first prompt is a solid optimization. I found a few correctness issues in the session lifecycle coordination that could cause user-visible bugs:

  1. attachedExistingSession skips necessary fetches — after deferred creation, commands/skills/context remain undefined
  2. ensureSessionForPrompt chain failure — if setModel() throws, the session is created but never attached (no SSE), causing silent hangs
  3. clearSession/loadSession race — concurrent session load during clear's await detach() can have its sessionId stripped

Plus two suggestions for robustness. See inline comments for details.

Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx Outdated
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/webui/src/daemon/session/actions.ts Outdated
Comment thread packages/web-shell/client/index.tsx Outdated
Comment thread packages/webui/src/daemon/session/actions.ts Outdated
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx
@ytahdn

ytahdn commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Updated the PR with fixes for the review threads:

  • Deferred first-prompt setup now creates and attaches the session before applying model/approval preferences; preference failures are logged and non-fatal, so the SSE attachment is not skipped.
  • attachSession() now participates in the pending session-load promise path, so callers can await attachment completion before sending the prompt.
  • Existing-session metadata reuse now only skips provider/commands/context fetches when the connection already has that metadata.
  • Controlled session transitions now log failures instead of silently swallowing them.
  • clearSession() now preserves future connection fields via spread and only removes session-scoped fields when clearing the same session, so a concurrent load is not stripped after detach resolves.
  • onSessionIdChange is documented as accepting undefined; standalone URL updates still flow through App -> main.tsx -> replaceStandaloneSessionUrl() when the deferred session id appears.
  • Added focused tests for model/approval setup failures and the clear-session field preservation helper.

Verification run locally:

cd packages/web-shell && npx vitest run client/sessionPreparation.test.ts client/index.test.tsx
cd packages/webui && npx vitest run src/daemon/session/actions.test.ts
cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx

All passed.

@ytahdn

ytahdn commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up: fixed the CI build failure from the previous run (client/App.tsx missing DAEMON_APPROVAL_MODES during tsc -p tsconfig.lib.json). I also moved the session preparation helper under client/utils/ so the dev server no longer requests /sessionPreparation.ts at the root. Verified locally:

npm run build --workspace @qwen-code/web-shell
cd packages/web-shell && npx vitest run client/utils/sessionPreparation.test.ts client/index.test.tsx

Both passed. Latest CI has been retriggered on 5ddb21718.

Comment thread packages/web-shell/client/utils/sessionPreparation.ts Outdated
Comment thread packages/webui/src/daemon/session/actions.ts Outdated
Comment thread packages/webui/src/daemon/session/actions.ts
Comment thread packages/web-shell/client/utils/sessionPreparation.ts Outdated
Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx
Comment thread packages/cli/src/serve/workspace-providers-status.ts
Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test comment submission

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probe

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probe

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probe

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probe

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probe

Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx
Comment thread packages/webui/src/daemon/session/actions.ts
Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deferral approach is correct — keeping the welcome state until the first real prompt is a solid UX improvement. However, the new lifecycle coordination has a few correctness bugs that can produce user-visible failures, plus two load-bearing paths without test coverage.

Critical bugs

  1. ControlledSession + ensureSessionForPrompt race — host using activeSessionId={undefined} in controlled mode gets the just-created session destroyed.
  2. Deferred connect setConnection({...}) — plain-object state set can clobber sessionId/clientId from a concurrent createSession.
  3. /goal command orphaned message — store.appendLocalUserMessage(text) runs before sendGoalPrompt(). If ensureSessionForPrompt throws, the transcript entry is orphaned.

Untested load-bearing paths
4. createSession activeSession branch (createOrAttachSession path).
5. skipNextCleanupDetachSessionIdRef / keepSessionForNextEffect cleanup guard.

Verified clean: npm run build, npm run typecheck, and 142 tests across 5 suites (web-shell index, App, sessionPreparation; webui DaemonSessionProvider, actions; cli workspace-providers-status).

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/index.tsx Outdated
Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx Outdated
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/webui/src/daemon/session/actions.ts
Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. Qwen review exited with status 1. See workflow logs.

Comment thread packages/webui/src/daemon/session/actions.ts Outdated
reportError(error, t('mode.plan'));
});
return true;
return prompt ? false : true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion — low confidence] The /plan existing-session path returns prompt ? false : true instead of always true. When prompt exists, returning false means the editor is not cleared immediately — it is only cleared asynchronously when sendPrompt eventually runs editorRef.current?.clear() inside the .then() of setApprovalMode(). Unlike the no-session path (where isPreparingPrompt=true blocks duplicate submissions via the spinner), the existing-session path sets no such guard. A very fast second Enter before the async setApprovalMode round-trip completes could re-submit the same text.

The race window is tiny (one setApprovalMode network round-trip), but consider returning true to clear the composer immediately, matching the pre-PR behavior.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the callout. I checked this path again and am leaving it unchanged for now: returning true would clear the composer before setApprovalMode(plan) succeeds, so a mode-switch failure would drop the user prompt. The current behavior keeps the prompt until the async mode switch reaches sendPrompt, which matches the existing failure-safety behavior. Since this was marked low confidence and the race window is only the setApprovalMode round trip, I prefer not to trade it for potential input loss in this lifecycle PR.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

15 findings reported by 9 parallel review agents → 7 confirmed after verification and reverse audit (3 rounds, converged at round 3) → 8 rejected as false positives or already-discussed.

Build & Tests

  • Typecheck (@qwen-code/webui): ✅ PASS
  • webui tests (DaemonSessionProvider.test.tsx, actions.test.ts): ✅ 133 passed
  • web-shell tests (index.test.tsx, sessionPreparation.test.ts): ✅ 8 passed

Critical (high confidence)

  1. actions.ts:583createSession() unconditionally overwrites sessionRef.current and restores connection.sessionId after await createDetachedSession(), with no check for manualSessionClearRef.current. If clearSession() runs during the await, the result is an orphaned session with open SSE stream. Fix: check manualSessionClearRef.current after the await and detach the orphan if set.

  2. DaemonSessionProvider.tsx:1291sessionRef.current not cleared on unmount during the skip-detach window. Block 3's condition !keepSessionForNextEffect doesn't consider isUnmounting, leaving a stale DaemonSessionClient in sessionRef after unmount. Fix: !keepSessionForNextEffect || isUnmounting.

Suggestions

  1. actions.ts:172startPendingSessionLoad timeout misclassifies attach mode as load_session. Add mode === 'attach' ? 'attach_session' branch.

  2. App.tsx:2467 (low confidence)/plan existing-session path returns prompt ? false : true instead of true, potentially allowing duplicate prompt on very fast double-Enter.

  3. App.tsx:1828-1831 (low confidence, not in diff hunk)currentModel/currentMode reset effects include connection.sessionId in deps, causing a brief UI flicker when a deferred session is created. The pendingModel/pendingMode mechanism partially mitigates.

doudouOUC
doudouOUC previously approved these changes Jul 1, 2026

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third-pass review — LGTM

All previously raised issues have been addressed:

  1. onSessionIdChange notification gap — Fixed via lastNotifiedSessionIdRef dedup pattern (df0df956). The callback now fires for both truthy and undefined session IDs, and avoids redundant notifications when the value hasn't changed.

  2. Test type alignment — Mock factories now produce structurally correct CommandInfo, SupportedCommandsStatus, and ContextStatus objects (9e9d8fa1), eliminating silent type mismatches.

  3. createNewSession no longer manually calls onSessionIdChange?.(undefined) — The effect-based notification makes this redundant. Removing it prevents double-fire edge cases.

Architecture assessment

The controlled-session refactor is well-structured:

  • Stale event guard (sessionRef.current?.sessionId !== activeSession.sessionId) correctly breaks the SSE loop on session swap/clear.
  • manualSessionClearRef prevents unwanted reconnection after user-initiated clear.
  • skipNextCleanupDetachSessionIdRef correctly preserves the session across the create→attach effect cycle, consumed in all three terminal paths (success, error, cleanup unmount).
  • ensureSessionForPrompt singleton with .catch() + useEffect cleanup handles both concurrent calls and session-creation failures gracefully.
  • createAndAttachSessionForPrompt has a correct cleanup chain: attach failure → close → clear → rethrow, with parallel non-blocking model/mode setup.

Test coverage is solid: 6 test files with unit tests for actions, session preparation, provider lifecycle, and mappers.

Minor nit (non-blocking)

MessageList.tsx L934-935: ref mutation during render (previousLiveStartedAtRef.current = liveStartedAt) is intentional to avoid elapsed-timer flash, but worth a brief inline comment noting the tradeoff for future readers.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅ Two caveats flagged in Stage 3 for the human reviewer (approvalMode scope split; browser visual pass through the four scenarios before merge).

@ytahdn
ytahdn dismissed stale reviews from qwen-code-ci-bot and doudouOUC via c1591c5 July 1, 2026 11:46
@ytahdn

ytahdn commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/App.tsx

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Analyzed all 24 changed files (+2629/-586) across the web-shell session lifecycle refactoring. After cross-referencing with the 25+ existing inline comments and their resolutions, no new high-confidence issues were found.

Assessment

The deferred session creation pattern is correctly implemented:

  • ensureSessionForPrompt dedup (App.tsx): The createSessionPromiseRef pattern correctly prevents concurrent session creation, and the .catch cleanup allows retry.
  • createAndAttachSessionForPrompt (sessionPreparation.ts): Properly creates then attaches, with cleanup (closeSession + clearSession) on attach failure.
  • Composer clear timing (App.tsx): clearComposerOnPromptStart correctly defers editor clearing until after session preparation succeeds, preserving user text on failure.
  • getConnectionAfterSessionClear (actions.ts): Correctly guards field deletion with !clearedSessionId || current.sessionId === clearedSessionId.
  • skipNextCleanupDetachSessionIdRef (DaemonSessionProvider.tsx): The unmount guard (!keepSessionForNextEffect || isUnmounting) correctly ensures detach runs on unmount even when session preservation is active.
  • Controlled session transitions (DaemonSessionProvider.tsx): The lastHandledSessionIdRef dedup plus connectionRef.current.sessionId === sessionId early return prevents double-loading.
  • Deferred connect path (DaemonSessionProvider.tsx): Correctly fetches workspace providers and returns early without creating a session when shouldDeferInitialSessionCreation is true.
  • Heartbeat gating on connection.status === 'connected': Prevents heartbeats from firing before the session is established or during disconnected states.

The prior review rounds (from @wenshao, @doudouOUC, @ytahdn, @qwen-code-ci-bot, @yiliang114, @DragonnZhang, @qqqys) covered the critical paths thoroughly, and the author addressed all findings across multiple fix commits.

@ytahdn

ytahdn commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triage re-run on HEAD 48ad570: core change correct and well-tested through 20 commits, 223 tests pass, typecheck clean. No critical blockers found in independent code review. Approving.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI failing (Post Coverage Comment).

Reviewed all 24 files (+2381/-507). Build passes, all 160 tests pass. The deferred-session architecture is well-designed and the extensive prior review iterations have resolved most lifecycle coordination issues. Two remaining suggestions below on edge cases in the error-recovery and UX paths.

— qwen3.7-max via Qwen Code /review

try {
await sessionActions.attachSession();
} catch (error) {
warn('[WebShell] failed to attach new session:', error);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] If clearSession() runs concurrently between createSession() returning and attachSession() being called (e.g., user clicks New Chat during the microtask gap), sessionRef.current becomes undefined. When attachSession() then throws, closeSession() here also fails (no session ref) and clearSession() is a no-op. The daemon session was detached in clearSession() step 3 but never closed — it leaks until idle timeout.

Consider capturing the session client reference from createSession()'s return value before calling attachSession(), so the catch block can close the daemon session directly even if sessionRef was cleared concurrently:

const createdSession = await sessionActions.createSession();
try {
  await sessionActions.attachSession();
} catch (error) {
  warn('[WebShell] failed to attach new session:', error);
  // Use the captured reference, not sessionRef (which may be cleared)
  await createdSession?.close?.().catch((e: unknown) => {
    warn('[WebShell] failed to close unattached session:', e);
  });
  await sessionActions.clearSession().catch((clearError: unknown) => {
    warn('[WebShell] failed to clear unattached session:', clearError);
  });
  throw error;
}

— qwen3.7-max via Qwen Code /review

setShowRetryHint(false);
const shouldShowPreparing = !connectionRef.current.sessionId;
if (shouldShowPreparing) {
setIsPreparingPrompt(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] When the first prompt triggers session creation, isPreparingPrompt disables the composer entirely. The user has no cancel button, no abort controller, and no visible timeout. Session creation can take up to 30 seconds (the startPendingSessionLoad timeout). If the daemon is congested, the user is locked out with no escape hatch.

Consider wiring an AbortController to ensureSessionForPrompt() and exposing a cancel action on the composer during the preparing state. At minimum, adding log entries for isPreparingPrompt transitions would help debugging:

if (shouldShowPreparing) {
  console.info('[WebShell] preparing prompt: creating session');
  setIsPreparingPrompt(true);
}

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

✅ Local real-build verification (maintainer)

I built this PR from source and drove the real qwen serve --web daemon + SPA with headless Chromium (Playwright) to verify the session-lifecycle behavior end-to-end, plus an A/B against the merge-base to prove the change is load-bearing. Recommend merge — the PR does what it claims. Two minor, non-blocking polish notes at the end.

Environment

  • Head 48ad570b · base dab218fcd (merge-base with origin/main) · macOS · Node 22
  • Real compiled binary: node packages/cli/dist/index.js serve --web from a clean npm ci build of the PR worktree. Browser talks directly to the daemon; evidence is browser-observed responses (POST /session, /load status) cross-checked with GET /workspace/:id/sessions and GET /session/:id/status.
  • Isolated QWEN_HOME + a fake OpenAI streaming endpoint so real turns complete offline.

1. Build / typecheck / bundle

  • npm ci (full monorepo build: tsc + vite + sdk bundle) → exit 0
  • typecheck @qwen-code/qwen-code (cli) + @qwen-code/webuiclean; @qwen-code/web-shell build → clean
  • SDK daemon browser bundle 136,907 B ≤ 137,216 B cap → the new daemon APIs (detachSession, etc.) do not regress the bundle-size gate.

2. Unit tests shipped by the PR — 223 pass

Suite Tests
webui DaemonSessionProvider.test.tsx + actions.test.ts 137 ✓
web-shell index.test.tsx + sessionPreparation.test.ts + MessageList.test.ts 72 ✓
cli workspace-providers-status.test.ts 14 ✓

3. Real-UI E2E — 17/17 assertions pass (PR build)

# Scenario Observed result
S1 Open / (no sessionId) Welcome + ready composer; URL stays /; 0 POST /session; daemon session list unchanged
S2 Send first prompt exactly 1 POST /session; URL → /session/<id>; model reply rendered; clientCount=1
S3 Click New chat URL → / (empty welcome); 0 new POST /session; old session detached (clientCount 1→0); stale transcript cleared
S4 Open /session/<bogus> /load404; 0 replacement POST /session; composer shows disabled Loading… (disconnected)
S5 Open /session/<realid> /load200; transcript restored; 0 new POST /session

4. A/B vs merge-base — proves the fix is load-bearing

Same script, same fake model, two builds:

Empty / Missing /session/<bogus>
BASE dab218fcd (old) creates a session (Δ=1), URL→/session/<uuid> creates a replacement (Δ=1), URL→/session/<uuid>
PR 48ad570b (new) no session (Δ=0), URL stays / no replacement (Δ=0), /load 404, stays disconnected

The two exact problems the PR describes (eager empty-page session; silent replacement of a missing session) reproduce on base and are eliminated on the PR.

5. Extra confirmations

  • The empty page shows the workspace approval mode ("Plan" here) sourced from the providers status before any session exists — validates the acp-bridge/workspace-providers-status/mappers approvalMode addition end-to-end.
  • Reverse-audit: no residual references to the removed initialSessionId / missingSessionBehavior props in production code (the initialSessionId in packages/desktop/.../renderer/App.tsx is an unrelated local variable), consistent with the clean typecheck.

6. Minor, non-blocking observations

  1. On first prompt the new flow re-applies the pending/current model + approval-mode to the freshly-created session. In my token-less loopback harness both calls fail — the strict /approval-mode route needs a bearer token, and the synthetic $runtime|…|fake-model id makes /model error — so 3 error toasts appear. These are harness artifacts, not defects in the lifecycle logic under test.
  2. One of those failure notices renders as the literal string [object Object] (the webui switch_model notice message isn't stringified). That code path is untouched by this PR (pre-existing) and is merely exposed by the new first-prompt model apply. Worth a small follow-up; you might also consider skipping the model/mode re-apply when it already equals the session default, to avoid a spurious first-prompt toast in token-less --web setups.

Conclusion: deferred creation, empty-page-with-no-session, New-Chat-to-empty, and missing-session-disconnect-without-replacement all verified on a real build, with an A/B contrast against base. LGTM to merge; the notes above are polish, not blockers.

中文说明(点击展开)

✅ 本地真实构建验证(维护者)

我从源码构建了本 PR,并用无头 Chromium(Playwright)驱动真实qwen serve --web daemon + SPA,端到端验证了 session 生命周期行为;并对 merge-base 做了 A/B,证明该改动是真正起作用的(非偶然)。建议合并 —— PR 行为与描述一致。末尾有两条次要、不阻塞合并的打磨建议。

环境

  • head 48ad570b · base dab218fcd(与 origin/main 的 merge-base)· macOS · Node 22
  • 真实编译产物:对 PR worktree 做干净 npm cinode packages/cli/dist/index.js serve --web。浏览器直连 daemon;证据是浏览器观测到的响应(POST /session/load 状态码),并用 GET /workspace/:id/sessionsGET /session/:id/status 交叉核验。
  • 隔离的 QWEN_HOME + 一个伪 OpenAI 流式端点,使真实回合可离线完成。

1. 构建 / 类型检查 / bundle

  • npm ci(完整 monorepo 构建:tsc + vite + sdk bundle)→ exit 0
  • cli + webui typecheck 干净;web-shell build 干净
  • SDK daemon 浏览器 bundle 136,907 B ≤ 137,216 B 上限 → 新增 daemon API(detachSession 等)触发 bundle 体积回归。

2. PR 自带单测 —— 223 通过

套件 用例数
webui DaemonSessionProvider.test.tsx + actions.test.ts 137 ✓
web-shell index.test.tsx + sessionPreparation.test.ts + MessageList.test.ts 72 ✓
cli workspace-providers-status.test.ts 14 ✓

3. 真实 UI E2E —— 17/17 断言通过(PR 构建)

# 场景 观测结果
S1 打开 /(无 sessionId 欢迎页 + 输入框可用;URL 保持 /0POST /session;daemon session 列表不变
S2 发送第一条 prompt 恰好 1POST /session;URL → /session/<id>;模型回复渲染;clientCount=1
S3 点击 New chat URL → /(空欢迎页);0 次新建 POST /session;旧 session 被 detachclientCount 1→0);旧 transcript 清空
S4 打开 /session/<不存在> /load4040 次替代 POST /session;输入框显示禁用的 Loading…(disconnected)
S5 打开 /session/<已存在> /load200;transcript 恢复;0 次新建 POST /session

4. 与 merge-base 的 A/B —— 证明该修复是真正承重的

同一脚本、同一伪模型、两个构建:

空页 / 缺失 /session/<不存在>
BASE dab218fcd(旧) 创建了 session(Δ=1),URL→/session/<uuid> 创建了替代 session(Δ=1),URL→/session/<uuid>
PR 48ad570b(新) 不创建(Δ=0),URL 保持 / 不创建替代(Δ=0),/load 404,保持 disconnected

PR 描述的两个问题(空页提前建 session、缺失 session 被静默替代)在 base 上均复现,在 PR 上均被消除。

5. 额外确认

  • 空页在没有任何 session 时即从 providers status 显示了工作区的审批模式(此处为 "Plan")—— 端到端验证了 acp-bridge/workspace-providers-status/mappersapprovalMode 新增字段。
  • 反向审计:生产代码中不存在对已移除的 initialSessionId / missingSessionBehavior props 的残留引用(packages/desktop/.../renderer/App.tsx 里的 initialSessionId 是无关的局部变量),与 typecheck 干净一致。

6. 次要、不阻塞合并的观察

  1. 第一条 prompt 时,新流程会把待应用的/当前的 model + 审批模式 重新应用到刚创建的 session。在我这个无 token 的 loopback 环境里两个调用都失败 —— 严格路由 /approval-mode 需要 bearer token,合成的 $runtime|…|fake-model 使 /model 报错 —— 因此弹出 3 个错误提示。这些是测试环境产物,不是被测生命周期逻辑的缺陷。
  2. 其中一个失败通知渲染成了字面量 [object Object](webui switch_model 通知的 message 未被字符串化)。该代码路径本 PR 未改动(是既有问题),只是被新的首条 prompt model 应用流程暴露出来。可作为后续小修复;也可以考虑当 model/mode 与 session 默认值相同时跳过重复应用,避免在无 token 的 --web 部署里首条 prompt 弹出无谓提示。

结论: 延迟创建、空页无 session、New Chat 回空页、缺失 session 断开而不静默替代,均已在真实构建上验证,并与 base 做了 A/B 对照。建议合并;上面两条只是打磨项,不构成阻塞。

Verified locally on head 48ad570b — real qwen serve --web binary + Playwright/Chromium + fake OpenAI endpoint; A/B against base dab218fcd.

@ytahdn
ytahdn added this pull request to the merge queue Jul 1, 2026
Merged via the queue into QwenLM:main with commit 1467ed3 Jul 1, 2026
67 checks passed

void request.catch((error: unknown) => {
console.warn(
'[DaemonSessionProvider] controlled session transition failed:',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The controlled-session transition failure log does not include the target sessionId being transitioned to, which is in scope from the enclosing effect. When debugging a failed transition (load or clear), the log is ambiguous — you cannot tell which session was targeted without reproducing the issue.

Suggested change
'[DaemonSessionProvider] controlled session transition failed:',
`[DaemonSessionProvider] controlled session transition failed (target=${sessionId ?? 'clear'}):`,

— qwen3.7-max via Qwen Code /review

hasCurrentSessionActivePromptRef.current = hasSessionActivePrompt;
setPromptStatus(hasSessionActivePrompt() ? 'streaming' : 'idle');

const canReuseSessionMetadata =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion — test gap] The canReuseSessionMetadata optimization skips 3 daemon API calls (workspaceProviders(), supportedCommands(), context()) when re-attaching an existing session with cached metadata, but no test asserts that these fetches are actually skipped. A regression that accidentally always-fetches would not be caught, silently re-introducing unnecessary daemon round-trips on every SSE effect re-run.

Consider adding a test that loads a session (populating commands/skills/context), triggers an SSE effect re-run entering the attachedExistingSession branch, and asserts the three daemon calls were NOT invoked a second time.

— qwen3.7-max via Qwen Code /review

}, [connection.sessionId, heartbeatFailureThreshold, heartbeatIntervalMs]);
}, [
connection.sessionId,
connection.status,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion — test gap] The heartbeat effect gained connection.status in its dependency array, adding a guard that the heartbeat should not run when the connection is not 'connected'. No test verifies that clearing a session (which deletes connection.sessionId) stops an active heartbeat timer. The existing heartbeat test only covers failure threshold behavior, not timer cleanup after session clear.

Consider adding a test that: (1) loads a session with client_heartbeat capability, (2) verifies heartbeat calls start, (3) calls clearSession(), (4) advances time and asserts no further heartbeat calls occur.

— qwen3.7-max via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

9 findings reported by 9 parallel review agents → 0 confirmed high-confidence, 9 confirmed low-confidence after verification and reverse audit (1 round, converged). All findings are suggestions, not correctness bugs.

Build & Tests

  • Typecheck: passes for webui and cli workspaces
  • Build: webui and web-shell build successfully
  • Tests: web-shell client tests pass (5/5); DaemonSessionProvider tests pass (after building dependency packages)

Findings (all low-confidence suggestions)

  1. CDP tunnel auto-enable lacks operator diagnosticrun-qwen-serve.ts:1316-1319. Auto-enabled by chrome-extension origin without a stderr log line, unlike the analogous clientMcpOverWs feature.
  2. removeChromeDevToolsMcpIfUnused TOCTOUacp-http/index.ts:449-530. Narrow gap between hasActive() check and removal.
  3. Channel memory type duplicationchannel-memory.ts + types.ts. filePath optionality mismatch.
  4. Channel memory dispatch duplicatedChannelBase.ts:327-381 vs 2132-2175. Two paths with subtly different claim strategies.
  5. isDaemonApprovalMode untestedsessionPreparation.ts:15. Invalid mode IDs silently skipped.
  6. createSession() error path untestedactions.ts:559-605.
  7. clearSession() detach failure untestedactions.ts:616-630.
  8. newSession() untestedactions.ts:633-636. Load-bearing manualSessionClearRef distinction.
  9. canReuseSessionMetadata optimization untestedDaemonSessionProvider.tsx:591-598.

Scope Splitting

The PR mixes session lifecycle refactoring (web-shell/webui/SDK) with channel memory (core/channels) and CDP tunnel changes (CLI serve). The channel memory and CDP tunnel changes are orthogonal and could be extracted into separate PRs for easier review and safer rollback.

Verdict: Comment — no blocking issues found.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (not mapped to specific diff lines):

  • [Critical] handleFastModelSelect (App.tsx:3261) still routes through sendPrompt, which now calls ensureSessionForPrompt() — creating a full daemon session. Selecting a fast model from the UI when no session exists will silently create a session, establish SSE streaming, and send /model --fast <id> as a prompt. This is both a behavioral regression (previously showed a "session not connected" error) and inconsistent with handleModelSelect (uses setPendingModel) and handleVoiceModelSelect (uses setWorkspaceSetting), both of which handle the no-session case without creating a session.

Additional test coverage gaps:

  • DaemonClient.ts — the new detachSession method (POST /session/:id/detach) has no unit test. It handles 204, 404, and error responses; none are tested.
  • DaemonSessionProvider.tsx controlled-session effect — no test covers the undefined → sessionId transition (prop changes from undefined to a concrete session id after the provider already rendered in the empty state). This is the path a host hits when navigating from "new chat" to "resume session" without remounting.
  • actions.ts — no test for createSession() when createDetachedSession() throws (the error path that posts a notice), and no test for clearSession() when sessionRef.current is already undefined (the no-op path).

— qwen3.7-max via Qwen Code /review

})
: Promise.resolve(),
modeId && isDaemonApprovalMode(modeId)
? sessionActions.setApprovalMode(modeId).catch((error: unknown) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] setModel and setApprovalMode failures are swallowed with console.warn only. The session is still created and attached, so the user submits a prompt believing they're in plan mode or using a specific model — but the session runs with defaults. No toast, no notice, no error state.

This is especially dangerous for approval mode: a user expecting plan (requiring explicit approval for edits) could end up in default mode where edits happen automatically.

Consider propagating the error or surfacing a user-visible notice:

if (modeId && isDaemonApprovalMode(modeId)) {
  await sessionActions.setApprovalMode(modeId); // let failure propagate
}

— qwen3.7-max via Qwen Code /review


if (!request) return;

void request.catch((error: unknown) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] When the controlled sessionId prop changes and loadSession or clearSession fails, the error is silently caught with console.warn. The provider's actual session diverges from the prop value with no error surfaced to the user or the parent component.

An embedded consumer passes sessionId="abc-123" via URL, the daemon returns 500, and the UI stays on the previous session. The user sees stale transcript and may take actions referencing a session they're not actually viewing.

Surface the error through addNotice or a connection error state:

void request.catch((error: unknown) => {
  addNotice({
    code: 'daemon.session_transition.failed',
    severity: 'error',
    message: `Failed to switch to session ${sessionId}`,
    operation: sessionId ? 'load_session' : 'clear_session',
  });
});

— qwen3.7-max via Qwen Code /review

async clearSession() {
const session = sessionRef.current;
manualSessionClearRef.current = true;
clearActiveSessionState();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] clearSession sets sessionRef.current = undefined before awaiting session.detach(). If detach times out or fails (line 642 catches with console.warn), the session reference is already lost from the client side with no recovery path. The daemon retains the session, but the client can never clean it up.

Consider detaching first, then clearing the ref:

const session = sessionRef.current;
manualSessionClearRef.current = true;
clearActiveSessionState();
if (session) {
  try {
    await withActionTimeout(session.detach(), 'Clear session timed out');
  } catch (error) {
    console.warn('[DaemonSessionActions] detach on clear failed:', error);
  }
}
sessionRef.current = undefined;
setConnection((current) =>
  getConnectionAfterSessionClear(current, session?.sessionId),
);

— qwen3.7-max via Qwen Code /review

? session
: undefined;
if (activeSession) {
const nextSession = await withActionTimeout(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The active-session path returns nextSession without updating sessionRef.current (contrast with the detached path at line ~598 which sets sessionRef.current = nextSession). A subsequent attachSession() reads sessionRef.current (still the old DaemonSessionClient) and attaches the old session instead of the newly created one.

While createAndAttachSessionForPrompt avoids this path (early-returns when a session exists), any external consumer of DaemonSessionActions that calls createSession() + attachSession() with an active session will silently re-attach the old session.

— qwen3.7-max via Qwen Code /review

@@ -1168,14 +1265,20 @@ export function DaemonSessionProvider({
hasCurrentSessionActivePromptRef.current = () => false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The session lifecycle is coordinated via three shared mutable refs (sessionRef, skipNextCleanupDetachSessionIdRef, manualSessionClearRef) with no documentation of the protocol. createSession() writes to sessionRef.current and sets skipNextCleanupDetachSessionIdRef to prevent the effect's cleanup from detaching the new session. The caller must follow with attachSession() to start event streaming.

Any future modification to createSession, clearSession, newSession, or the effect's dependency array risks silently detaching sessions or leaking server-side sessions. Consider adding a block comment above the ref declarations documenting this state machine protocol, or encoding it as a discriminated union instead of three independent refs.

— qwen3.7-max via Qwen Code /review

warn('[WebShell] failed to attach new session:', error);
await sessionActions.closeSession().catch((closeError: unknown) => {
warn('[WebShell] failed to close unattached session:', closeError);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] When attachSession() fails, cleanup attempts closeSession() then clearSession(). If both also fail, the daemon retains an orphaned session with no subscribed client. The session's conversation history and any captured file contents persist on the daemon server indefinitely with no way for the client to reclaim or delete them.

Consider adding a final fallback that force-deletes the session by ID:

} catch (error) {
  // ... existing cleanup ...
  // Last resort: force-delete the session by ID
  await sessionActions.deleteSession?.(createdSessionId).catch(() => {});
  throw error;
}

— qwen3.7-max via Qwen Code /review

current: DaemonConnectionState,
clearedSessionId: string | undefined,
): DaemonConnectionState {
const next = { ...current };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] getConnectionAfterSessionClear unconditionally forces status: 'connected' regardless of the actual daemon connection state. If clearSession() is called while the daemon is unreachable (connection was 'connecting' or 'disconnected'), the result is a connection that says 'connected' with no sessionId. Downstream consumers that branch on connection.status === 'connected' will incorrectly treat a disconnected daemon as ready.

Consider preserving the prior status when it was not 'connected', or documenting explicitly that clearSession always transitions to 'connected'.

— qwen3.7-max via Qwen Code /review

typeof settings.fastModel === 'string' && settings.fastModel.length > 0
? settings.fastModel
: undefined;
const approvalMode = resolveApprovalMode(settings);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] This uses the raw merged tools.approvalMode from settings, but session creation normally ignores that setting in --safe/--bare modes and downgrades unsafe modes for untrusted folders. The deferred web-shell path copies this value into connection.currentMode and then applies it with setApprovalMode() before the first prompt, so a workspace setting like yolo can re-enable a mode that the normal session config would have forced back to default. Please return the daemon's effective approval mode with the same safety/trust precedence as session config, or omit it until a real session context is available.

— gpt-5 via Qwen Code /review

models: sessionModels,
currentModel: sessionCurrentModel,
currentMode,
commands: commands.length > 0 ? commands : current.commands,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] These fallbacks reuse current.commands, current.skills, current.supportedCommands, and current.context even when current.sessionId !== activeSession.sessionId. If switching from session A to session B and B's supportedCommands() or context() request fails, the connection now says B but still exposes A's session-scoped command/context metadata to the UI. Please only preserve these fields for same-session reconnects or the explicit attachedExistingSession reuse case; for a different session, clear them or use the fresh empty values.

— gpt-5 via Qwen Code /review

await sessionActions.createSession();
try {
await sessionActions.attachSession();
} catch (error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] attachSession() can reject because it was superseded by a later controlled loadSession() (the pending load rejection is an AbortError), not because the just-created session failed to attach. This catch still calls global closeSession() and clearSession(), which operate on whatever session is currently in the provider, so a host sessionId switch during first-prompt preparation can close or clear the newly selected session. Treat superseded/abort attach errors as cancellation, and for real attach failures clean up only the session id that was just created.

— gpt-5 via Qwen Code /review

}, [connection.catchingUp, connection.sessionId, sidebarSwitchingSessionId]);

const openTasksPanel = useCallback(() => {
if (!requireActiveSessionForLocalCommand()) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The no-session guard inside openTasksPanel() returns early, but this branch still returns true, so useComposerCore treats /tasks as accepted and clears the editor on the empty page. The same pattern remains for /branch, /context, /recap, and /btw: they show the no-session toast but still consume the typed command. Please propagate false when the guard fails, or guard directly in each branch, so the composer preserves the user's input like the fixed /rewind, /fork, /rename, and /stats paths.

— gpt-5 via Qwen Code /review

wenshao added a commit that referenced this pull request Jul 2, 2026
… prompt (#6153)

* fix(web-shell): show skill slash commands (e.g. /review) before first prompt

Since session creation is deferred until the first prompt (#6066), the
deferred connect path reported 'connected' but only fetched workspace
providers — it never populated the slash-command list. Before sending a
message the composer therefore fell back to the hardcoded local command
list, which omits skills, so '/rev' would not autocomplete '/review'.

Fetch the session-less /workspace/skills status alongside providers in the
deferred connect path and seed connection.commands/skills from it, so
skill-backed slash commands autocomplete immediately. The full
session-scoped supported-commands snapshot (which also carries custom,
MCP-prompt and workflow commands) still replaces this once the first
prompt creates a session.

* test(web-shell): cover deferred workspace skills fetch failure

Add a parallel test to the deferred-connect warn coverage: when
client.workspaceSkills() rejects, the connection still reports
'connected' (skills are non-blocking) and the failure is logged via
console.warn, mirroring the existing workspaceProviders-failure test.

Addresses review feedback on #6153.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants