Skip to content

Feat/daemon react cli#4380

Merged
wenshao merged 55 commits into
QwenLM:daemon_mode_b_mainfrom
chiga0:feat/daemon-react-cli
May 27, 2026
Merged

Feat/daemon react cli#4380
wenshao merged 55 commits into
QwenLM:daemon_mode_b_mainfrom
chiga0:feat/daemon-react-cli

Conversation

@ytahdn

@ytahdn ytahdn commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • What changed:

    • Added a daemon-backed React web-shell under packages/web-shell.
    • Wired web-shell to daemon sessions, SSE events, permission requests, slash command completion, model switching, approval mode switching, session resume, memory, MCP, skills, and agents views.
    • Aligned key web-shell commands with CLI/ACP behavior, including /model --fast, /rename --auto, /new, /reset, and session URL restore via /session/:id.
    • Added Vite dev-server handling so /session/:id works as a SPA route while daemon API calls still proxy correctly.
  • Why it changed:

    • To provide a browser-based daemon UI that reuses daemon/ACP as the source of truth instead of duplicating CLI state.
    • To make the web-shell interaction model closer to Qwen Code CLI behavior during the daemon UI migration.
  • Reviewer focus:

    • Session lifecycle behavior in web-shell, especially create/load/resume and /session/:id refresh.
    • Slash command routing between local web UI and ACP forwarding.
    • Daemon token handling and Vite proxy behavior for local development.

Validation

  • Commands run:

    npx eslint --max-warnings 0 --no-warn-ignored packages/web-shell/client/App.tsx packages/web-shell/client/hooks/useDaemonSession.ts packages/web-shell/vite.config.ts
    cd packages/web-shell && npx tsc --noEmit
    cd packages/web-shell && npm run build
  • Prompts / inputs used:

    • /model
    • /model --fast
    • /model --fast <model>
    • /rename <name>
    • /rename --auto
    • /new
    • /reset
    • /resume
    • direct refresh on /session/:id?token=<token>
  • Expected result:

    • Web-shell should connect to daemon with bearer token support.
    • Session switching should update the browser URL.
    • Refreshing /session/:id should reload that daemon session instead of hitting the daemon API route as a page request.
    • Commands should either use local web-shell UI or forward to ACP consistently.
  • Observed result:

    • ESLint passed.
    • TypeScript check passed.
    • Web-shell production build passed.
    • Build completed with only the existing Vite chunk-size warning.
  • Quickest reviewer verification path:

    1. Start daemon serve with a token.
    2. Start web-shell dev server.
    3. Open web-shell with ?token=<token>.
    4. Create or resume a session.
    5. Confirm the URL changes to /session/:id.
    6. Refresh the page and confirm the same session loads.
    7. Try /model --fast, /rename --auto, /new, and /resume.
  • Evidence:

    • npm run build completed with ✓ built.
    • npx tsc --noEmit exited successfully.
    • eslint exited successfully.

Scope / Risk

  • Main risk or tradeoff:

    • The web-shell now uses /session/:id as a SPA route while daemon APIs also live under /session. The Vite dev proxy explicitly bypasses HTML navigations to avoid routing page refreshes to daemon API endpoints.
  • Not covered / not validated:

    • Full cross-platform browser matrix was not run.
    • Full repo preflight was not run.
    • End-to-end screenshot/video evidence is not included.
  • Breaking changes / migration notes:

    • None expected. Changes are scoped to the web-shell package and local dev proxy behavior.

Testing Matrix

🍏 🪟 🐧
npm run ⚠️ ⚠️
npx ⚠️ ⚠️
Docker N/A N/A N/A
Podman N/A N/A N/A
Seatbelt N/A N/A N/A

Testing matrix notes:

  • Validated on macOS with focused web-shell lint, typecheck, and build commands.
  • Windows/Linux browser behavior was not separately validated.

Linked Issues / Bugs

秦奇 and others added 30 commits May 20, 2026 11:55
…aemon-react-cli

# Conflicts:
#	packages/sdk-typescript/src/daemon/ui/normalizer.ts
#	packages/sdk-typescript/src/daemon/ui/store.ts
#	packages/sdk-typescript/src/daemon/ui/transcript.ts
#	packages/sdk-typescript/src/daemon/ui/types.ts
#	packages/sdk-typescript/test/unit/daemonUi.test.ts
#	packages/webui/src/daemon/DaemonSessionProvider.test.tsx
#	packages/webui/src/daemon/DaemonSessionProvider.tsx
#	packages/webui/src/daemon/transcriptAdapter.test.ts
#	packages/webui/src/daemon/transcriptAdapter.ts
- Use safeWorkspaceCwd in buildWorkspaceToolsStatus for consistency
- Wire loadMcpTools to return SDK tools instead of hardcoded empty array
- Consolidate WebShellMcpToolsStatus types (remove duplicate in McpDialog)
- Abort active prompts in loadSession before switching sessions
- Pass daemon credentials to @-completion source via Editor props

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread packages/web-shell/client/components/Editor.tsx Outdated
Comment thread packages/web-shell/client/hooks/useDaemonSession.ts Outdated
Comment thread packages/web-shell/client/hooks/useDaemonSession.ts
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/hooks/useDaemonSession.ts
Comment thread packages/cli/src/serve/server.ts
Comment thread packages/web-shell/client/components/messages/Markdown.tsx Outdated
Comment thread packages/web-shell/client/components/messages/AskUserQuestion.tsx Outdated
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/components/messages/ToolApproval.tsx Outdated
Comment thread packages/web-shell/client/config/daemon.ts
Comment thread packages/web-shell/client/components/messages/AskUserQuestion.tsx
@wenshao

wenshao commented May 26, 2026

Copy link
Copy Markdown
Collaborator

🔍 Maintainer Verification Report — PR #4380

Reviewer: @wenshao (maintainer)
Date: 2026-05-26
Platform: macOS (darwin, Apple Silicon)
Branch: feat/daemon-react-clidaemon_mode_b_main
Commit: 9e43048c3


✅ Build & Type Check

Step Result
npm install ✅ Pass
npm run build (full monorepo) ✅ Pass (0 errors, 15 pre-existing warnings in vscode-ide-companion)
npm run bundle ✅ Pass
packages/web-shellnpx tsc --noEmit ✅ Pass (0 errors)
packages/web-shellnpm run build (Vite prod) ✅ Pass (chunk-size warning expected)

✅ Unit Tests (910 tests, all passed)

Package Test File Tests Result
acp-bridge bridge.test.ts 185 ✅ All passed
cli server.test.ts 268 ✅ All passed
cli Session.test.ts 74 ✅ All passed
sdk-typescript DaemonClient.test.ts 127 ✅ All passed
sdk-typescript daemonUi.test.ts 184 ✅ All passed
web-shell transcriptAdapter.test.ts 15 ✅ All passed
web-shell daemonSessionEvents.test.ts 7 ✅ All passed
web-shell copyCommand.test.ts 11 ✅ All passed
web-shell Markdown.test.ts 39 ✅ All passed

✅ E2E Verification (tmux-based, 22 scenarios)

Setup: Daemon serve on :4170 + Web-shell dev server on :5173

Daemon Server

Scenario Result
serve --port 4170 starts and listens
GET /health{"status":"ok"}
GET /capabilities → 44 features returned

Web-shell Dev Server

Scenario Result
Vite dev server starts (303ms)
GET / returns React SPA index.html

Vite Proxy & SPA Routing

Scenario Result
GET localhost:5173/health → proxy to daemon
GET localhost:5173/capabilities → proxy to daemon
GET /session/:id (Accept: text/html) → SPA fallback to index.html
GET /session/:id (Accept: application/json) → proxy to daemon
GET /list?path=. via proxy
GET /glob?pattern=*.md via proxy

Session Lifecycle

Scenario Result
POST /session creates new session
POST /session via proxy attaches existing
GET /workspace/:id/sessions lists sessions
GET /session/:id/events SSE stream
GET /session/:id/context returns models/modes
GET /session/:id/supported-commands returns 38 commands

Workspace APIs

Scenario Result
GET /workspace/skills → 18 skills
GET /workspace/mcp → discovery completed

Daemon Log Health

  • ✅ No errors or stack traces in daemon logs
  • Only expected info-level messages (fs audit no-op, parameter validation)

⚠️ Notes (non-blocking)

  1. Vite dev server IPv6: Dev server binds to IPv6 [::1]:5173 by default. Access via localhost works; 127.0.0.1 does not. This is standard Vite behavior, not introduced by this PR.
  2. Chunk size warnings: Several Vite chunks exceed 500 kB (mermaid, shiki, index). Expected for initial PR; consider code-splitting in follow-up.

Verdict

Dimension Status
Build & TypeCheck ✅ Clean
Unit Tests (910) ✅ All passed
E2E Verification (22) ✅ All passed
Daemon Logs ✅ Clean
Breaking Changes None observed

Recommendation: ✅ Ready to merge.

The daemon-backed React web-shell integrates cleanly. All daemon APIs, Vite proxy routing, SPA fallback, session lifecycle, and workspace endpoints work as documented. The PR description's validation claims are confirmed.

…icate user message

- Remove Session#executePrompt's emitUserMessage() call to eliminate
  duplicate user_message_chunk events (bridge-echo is the single source)
- Move removeDaemonTokenFromUrl() to main.tsx entry point (S19)
- Add mount-grace, interaction guard, safe default index to ToolApproval (Critical#1)
- Fix stale credential capture in Editor @-completion (Critical#3)
- Add submittedRef guard to AskUserQuestion, remove unsafe fallback (S18/S23)
- Use .then() pattern for clipboard writeText (S17)
- Add i18n for approval dialog and rename messages (S20)
- Add session load timeout (S15)
- Distinguish MCP error types with DaemonHttpError (S12)
- Clear stale heartbeat error on success (S13)
- Fix null vs undefined clientId check in server detach (S16)
- Add daemon.test.ts for origin validation coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread packages/cli/src/serve/server.ts Outdated
Comment thread packages/web-shell/client/components/messages/ToolApproval.tsx
Comment thread packages/web-shell/client/hooks/useDaemonSession.ts Outdated
Comment thread packages/web-shell/client/components/messages/ToolApproval.tsx
Comment thread packages/web-shell/client/components/messages/ToolApproval.tsx Outdated
Comment thread packages/web-shell/client/components/messages/ToolApproval.tsx Outdated
Comment thread packages/web-shell/client/components/messages/AskUserQuestion.tsx Outdated
ytahdn and others added 2 commits May 26, 2026 20:31
…quality, ToolApproval stale refs, session load timeout leak

- server.ts: change `clientId == null` to `=== null` so absent header falls through to detachClient instead of hanging the request
- server.test.ts: add test for detach without X-Qwen-Client-Id header
- ToolApproval.tsx: use refs to fix stale closures in handleKeyDown, reset submittedRef on request.id change, sync selectedRef on mouse hover, remove unstable request.options from effect deps
- useDaemonSession.ts: store and clear timeout handle in PendingSessionLoad across all resolution paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents double-submission on rapid Escape+Enter and avoids sending
empty optionId when no reject option exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@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.

R13 incremental review (504c7ed95e68ff): All R12 findings addressed. === null fix correct with test coverage, ToolApproval ref pattern sound, timer leak fully plugged across all 5 exit paths, cancel guard properly implemented. tsc + eslint clean, 275/275 CLI tests pass. No issues found. LGTM! ✅ — 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.

Cross-file finding (not in diff)

[Suggestion] AskUserQuestion.tsx keyboard handler (line 177) uses window.addEventListener('keydown') with only a customFocused bail-out — no isEditableTarget(e.target) check. ToolApproval was fixed in this same commit to check isEditableTarget, but AskUserQuestion was not updated to match. The prior reply claimed "Fixed" via useDelayedGlobalKeyDown, but the component does not use that hook — it registers its own raw handler. When AskUserQuestion is visible with customFocused=false, arrow keys, Enter, and Escape are intercepted globally regardless of which element is focused.

// At the top of the handler function (line 182):
if (e.defaultPrevented || isEditableTarget(e.target)) return;

Requires importing isEditableTarget from ../../utils/dom.

return;
}
const clientId = parseClientIdHeader(req, res);
if (clientId === null) 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.

[Suggestion] === null reverses the R8 fix (== null, confirmed in 504c7ed81). parseClientIdHeader returns undefined when the header is absent — === null lets undefined pass through to bridge.detachClient(sessionId, undefined), which unconditionally decrements attachCount (bridge.ts:3720). Repeated headerless POST /session/:id/detach requests can drain attachCount to zero, potentially triggering the deferred-reap path when spawnOwnerWantedKill is set.

If headerless detach is intentional, the guard should be in the bridge:

Suggested change
if (clientId === null) return;
if (clientId == null) return;

Or, if headerless detach must be allowed, guard attachCount in the bridge: if (clientId !== undefined && entry.attachCount > 0) entry.attachCount--;

— claude-opus-4-7 via Claude Code /qreview

className={`${styles.option} ${isSelected ? styles.optionActive : ''}`}
onClick={() => confirm(option.id)}
onMouseEnter={() => {
selectedRef.current = i;

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] onMouseEnter sets selectedRef.current and setSelected(i) but does NOT set interactedRef.current = true. The Enter handler (line 117) requires interactedRef to be true before confirming — after hovering, the first Enter just sets the flag and returns without submitting. User must press Enter twice after mouse-hover. The prior reply (R9) claimed "Fixed" but only addressed selectedRef sync.

Suggested change
selectedRef.current = i;
onMouseEnter={() => {
interactedRef.current = true;
selectedRef.current = i;
setSelected(i);
}}

— claude-opus-4-7 via Claude Code /qreview

@wenshao

wenshao commented May 26, 2026

Copy link
Copy Markdown
Collaborator

🔍 Maintainer Verification Report — PR #4380

Branch: feat/daemon-react-cli @ 95e68ff65
Platform: macOS (darwin, arm64)
Date: 2026-05-26


1. Build & Static Analysis

Check Result
npm run typecheck ✅ All 5 workspaces clean (acp-bridge, cli, core, sdk, webui)
npm run build ✅ Success (pre-existing warnings in vscode-ide-companion — not introduced by this PR)
npm run bundle ✅ Bundle produced successfully
npx eslint (key changed files: bridge.ts, bridgeErrors.ts, server.ts, acpAgent.ts, DaemonClient.ts, App.tsx, Markdown.tsx, ToolApproval.tsx, daemon.ts) ✅ Clean — 0 errors, 0 warnings

2. Unit Tests (858 total)

Package Test Files Tests Result
packages/acp-bridge bridge.test.ts 188/188
packages/cli server.test.ts 275/275
packages/sdk-typescript DaemonClient.test.ts, daemonUi.test.ts 317/317
packages/web-shell transcriptAdapter.test.ts, copyCommand.test.ts, daemonSessionEvents.test.ts, daemon.test.ts, Markdown.test.ts 78/78

3. Daemon Serve E2E Test (tmux + web-shell Vite proxy)

Started node dist/cli.js serve --port 4170 in tmux, then tested the web-shell Vite dev server (port 5173) proxying to the daemon.

# Scenario Expected Observed Status
01 Daemon startup Bind to port 4170, http-bridge mode listening on http://127.0.0.1:4170 (mode=http-bridge)
02 /health via proxy {"status":"ok"} {"status":"ok"}
03 /capabilities via proxy Features list including new workspace endpoints All features listed, including workspace_mcp, workspace_skills, workspace_providers, workspace_tools
04 GET /workspace/tools (new) Returns tools status with not_started hint {"tools":[], "errors":[{"kind":"tools","status":"not_started","hint":"spawn a session to populate"}]}
05 GET /workspace/mcp MCP discovery status {"discoveryState":"not_started","servers":[],"clientCount":0}
06 GET /workspace/mcp/:server/tools (new) MCP tools status per server Endpoint registered, returns correct schema
07 POST /session (create) Session created with ID + clientId sessionId + clientId returned, attached: false
08 GET /session/:id/context Model list + session state Full model list and context state returned
09 GET /session/:id/supported-commands Available slash commands Commands list returned with metadata
10 POST /session/:id/heartbeat Heartbeat acknowledged {"lastSeenAt": <timestamp>}
11 POST /session/:id/prompt Prompt accepted, model responds {"stopReason":"end_turn"}
12 POST /session/:id/cancel (idle) PR fix: graceful 204 instead of 500 HTTP 204 — isNotCurrentlyGeneratingCancelError correctly suppresses idle cancel
13 POST /session/:id/detach Client detached HTTP 204
14 DELETE /session/:id (release) Session released HTTP 204
15 GET /session/:id/events (SSE) SSE stream starts with retry: directive retry: 3000 received
16 GET /workspace/env Environment diagnostics Node 22.17.0, darwin arm64, sandbox info
17 GET /workspace/auth/status Auth provider status supportedDeviceFlowProviders: ["qwen-oauth"]
18 Web-shell HTML served by Vite React app loads <!DOCTYPE html> with #root + /main.tsx

4. Key PR Features Verified

New daemon API endpoints (all working):

  • GET /workspace/tools — built-in tool registry status
  • GET /workspace/mcp/:server/tools — per-server MCP tools discovery
  • POST /session/:id/detach — explicit client detach (204)
  • POST /session/:id/cancel on idle — graceful no-op (PR fix: isNotCurrentlyGeneratingCancelError)

acp-bridge improvements:

  • Idle cancel error tolerance: NOT_CURRENTLY_GENERATING_CANCEL_MESSAGE matching with wording variants
  • Permission mediator forgetSession ordering fix (moved before byId.delete)
  • New cancelImpl hook in testUtils for bridge test coverage

Web-shell (new package):

  • Vite dev server with correct proxy configuration to daemon
  • React app loads correctly at /
  • All 78 unit tests pass (Markdown sanitization, transcript adapter, daemon config, session events, copy command)
  • 39 Markdown security tests (isSafeHref, isSafeImageSrc, sanitizeSvg)

SDK (sdk-typescript):

  • DaemonClient: 133 tests pass (new methods: readWorkspaceFile, getWorkspaceToolsStatus, getWorkspaceMcpToolsStatus)
  • daemon-ui reducer: 184 tests pass (state machine, resync, recovery flows)

5. Code Quality Review Notes

Positive signals:

  • 5 rounds of iterative review fixes visible in commit history — thorough security hardening
  • SVG sanitization covers style/use/image/feImage/mpath, animation elements, external hrefs
  • Markdown link safety: URL scheme allowlist, javascript: blocked, data:image restricted to <img> only
  • Token stripped from URL after caching (Referer/history leak prevention)
  • CORS restricted in Vite dev server (cors: false)
  • Heartbeat consecutive failure detection (3 strikes → disconnect) with self-heal
  • Auth 401/403 handling: stops reconnect loop, shows error state
  • state_resync_required event handling: resets store
  • InputHighlight decoration ordering fix (sort before add)
  • isEditableTarget guard in keyboard handler
  • AskUserQuestion submittedRef guard prevents double-submit
  • Session load timeout with proper cleanup
  • DaemonHttpError type for MCP error discrimination
  • buildUnifiedDiff LCS-based with size guard (n*m > 250k fallback)

Pre-existing issues (not introduced by this PR):

  • 15 curly ESLint warnings in vscode-ide-companion (settingsWriter.ts, AuthMessageHandler.ts, WebViewProvider.ts)

6. Summary

Category Verdict
Build ✅ Clean
Type Safety ✅ Clean (5 workspaces)
Unit Tests (858) ✅ All pass
Lint ✅ Clean (PR-changed files)
Daemon E2E (18 scenarios) ✅ All pass
Security (XSS/SVG/Markdown) ✅ Comprehensive sanitization + 39 security tests
Session Lifecycle ✅ Create → Prompt → Cancel → Detach → Release all verified
New API Endpoints ✅ workspace/tools, workspace/mcp/:server/tools, detach, idle cancel

Recommendation: ✅ Ready to merge

All verification checks pass. The daemon serve API surface is correctly extended with new workspace status endpoints. The idle-cancel fix (isNotCurrentlyGeneratingCancelError) works as designed in real-user testing. The web-shell package provides a complete React frontend with thorough security hardening (SVG/Markdown sanitization, URL scheme allowlisting, CORS). 858 unit tests pass across 4 packages with comprehensive coverage of new functionality.

@wenshao

wenshao commented May 26, 2026

Copy link
Copy Markdown
Collaborator

🔍 PR #4380 真实测试验证报告

概要

项目 详情
PR #4380 Feat/daemon react cli
分支 feat/daemon-react-clidaemon_mode_b_main
变更 123 files, +17,075 / -42 lines
测试日期 2026-05-26
测试环境 macOS (darwin), Node 22, tmux 200×50
结果 PASS — 推荐 merge

静态验证

检查项 结果 详情
npm run build 0 errors (15 warnings from vscode-ide-companion, pre-existing)
npm run typecheck 5/5 packages pass
npm run bundle 成功
web-shell build Vite build 成功 (6.16s) + lib build 成功

单元测试 (858 tests)

测试文件 测试数 结果
server.test.ts 275 ✅ 全部通过
bridge.test.ts 188 ✅ 全部通过
DaemonClient.test.ts + daemonUi.test.ts 317 ✅ 全部通过
web-shell (5 test files) 78 ✅ 全部通过
合计 858

Daemon API 端点测试 (curl)

启动 qwen serve --port 4170 并用 curl 逐一验证:

# 端点 结果 观察
01 Daemon 启动 qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge)
02 GET /health {"status":"ok"}
03 GET /capabilities 返回 44 个 features,包括新增的 permission_mediation, auth_device_flow
04 GET /demo 返回完整 HTML 调试页面
05 POST /session (create) 成功创建 session,返回 sessionId + clientId + workspaceCwd
06 PATCH /session/:id/metadata 成功设置 displayName
07 GET /workspace/:id/sessions 返回 session 列表 (含 displayName, clientCount, hasActivePrompt)
08 POST /session/:id/load 返回 session 状态 {attached: true, state: {}}
09 POST /session/:id/prompt 正确校验 prompt 格式(需要 content blocks 数组)
10 POST /session/:id/heartbeat 正确拒绝未注册的 clientId
11 GET /workspace/preflight 返回预检结果 (initialized, acpChannelLive)

Web-shell Vite Dev Server 测试

# 测试项 结果 观察
12 Vite dev server 启动 504ms 启动完成
13 GET / (index.html) 返回 React HTML + Vite HMR refresh
14 SPA 路由 /session/:id HTTP 200,bypass 返回 index.html
15 Vite proxy → daemon /health 成功代理,返回 {"status":"ok"}
16 Vite proxy → daemon /capabilities 成功代理,返回 features 列表

Vite proxy 验证bypass 函数正确区分 HTML 导航(返回 index.html)和 API 请求(代理到 daemon)。


CLI TUI 回归测试 (tmux)

使用 tmux 模拟真实用户操作 CLI TUI(--approval-mode default):

# 测试项 结果 观察
16 CLI 启动 Qwen Code (vdev) 正常渲染,API Key + qwen3.7-max
17 发送 prompt "say hello" 模型正确响应
18 /model 斜杠命令 模型选择对话框正常弹出
19 工具调用 read .nvmrc ✓ ReadFile .nvmrc (first 3 lines) → 内容 "22"
20 /help 斜杠命令 帮助对话框正常渲染

关键验证点

  1. Server 新增端点完整:capabilities 列出 44 个 features,包括 permission_mediation(PR feat(daemon): add voterClientId to permission_resolved (A4) #4539 相关)、auth_device_flowmcp_guardrails
  2. Session 生命周期正常:create → rename → list → load 全链路工作
  3. Web-shell 架构正确:Vite proxy 正确区分 SPA 路由和 API 转发,/session/:id 刷新不会命中 daemon API
  4. CLI TUI 无回归:启动、对话、工具调用、斜杠命令均正常
  5. Token 认证:设置 QWEN_SERVER_TOKEN 后 daemon 正确执行认证校验

结论

PASS — 推荐 merge

  • 858 项单元测试全部通过
  • Daemon API 11/11 端点响应正确
  • Web-shell Vite dev server 5/5 测试通过
  • CLI TUI 5/5 回归测试通过
  • 新增的 web-shell 包构建成功,SPA 路由和 daemon 代理行为正确

日志产物: tmp/pr4380-webshell-tmux-20260526-214020/tmux-readable-full.log

@wenshao

wenshao commented May 26, 2026

Copy link
Copy Markdown
Collaborator

🌐 Chrome 浏览器真实交互测试验证报告

概要

项目 详情
PR #4380 Feat/daemon react cli
测试日期 2026-05-26
测试环境 macOS Chrome + Vite dev server (5173) + daemon serve (4170)
Token QWEN_SERVER_TOKEN=test-chrome-4380
结果 PASS

测试架构

Chrome → Vite Dev Server (:5173) → proxy → Daemon Serve (:4170) → ACP Bridge → Model API
   ↑                                        ↓
   └──────── SSE events ←──────────────────┘
  • Vite dev server: packages/web-shell (port 5173)
  • Daemon serve: node dist/cli.js serve --port 4170 (port 4170)
  • Chrome: 通过 open -a "Google Chrome" 打开 http://localhost:5173/?token=test-chrome-4380

测试步骤与结果

01 — 页面加载 & 自动创建 Session

  • 操作: Chrome 打开 http://localhost:5173/?token=test-chrome-4380
  • 预期: web-shell 加载,自动创建 daemon session
  • 实际: ✅
    • 页面标题: "Qwen Code Web terminal"
    • URL 自动变为: /session/7f7d12ed-7098-42c7-a464-188fc116ee80
    • Daemon API 确认 session 创建,clientCount: 1

02 — Prompt: "say hello in one word"

  • 操作: 通过 daemon API 发送 prompt(web-shell 通过 SSE 接收事件)
  • 预期: 模型回复,SSE 事件流正确传递到浏览器
  • 实际: ✅ {"stopReason":"end_turn"}
  • SSE 事件流:
    • user_message_chunk: "say hello in one word"
    • agent_thought_chunk: "Evaluating response options..."
    • agent_message_chunk: "Hello" ← 模型正确回复
    • usage: {inputTokens: 21457, outputTokens: 12}

03 — Session Rename

  • 操作: PATCH /session/:id/metadata {displayName: "Chrome Web-shell Test"}
  • 预期: displayName 更新
  • 实际: ✅ {"sessionId":"7f7d12ed-...","displayName":"Chrome Web-shell Test"}
  • SSE 事件: session_metadata_updated 事件已发送

04 — Prompt with Tool Call: read .nvmrc

  • 操作: 发送 "read the first line of .nvmrc and tell me what version it says"
  • 预期: 模型调用 read_file 工具,返回版本号
  • 实际: ✅ {"stopReason":"end_turn"}
  • SSE 事件流 (完整链路):
    1. user_message_chunk: "read the first line of .nvmrc..."
    2. agent_thought_chunk: "用户要求我读取 .nvmrc 文件的第一行..."
    3. agent_thought_chunk: "使用 read_file 工具读取 /Users/wenshao/.../qwen-code-x4/.nvmrc"
    4. agent_thought_chunk: "查看第一行内容" → "告诉用户版本号"
    5. Tool call → read_file → result: "22"
    6. agent_message_chunk: "22" ← 正确返回 Node.js 版本
    7. usage: {inputTokens: 21739, outputTokens: 41}

05 — Approval Mode Switch

  • 操作: POST /session/:id/approval-mode {mode: "yolo"}
  • 预期: 模式切换成功
  • 实际: ✅ {"sessionId":"7f7d12ed-...","mode":"yolo","previous":"yolo"}
  • SSE 事件: approval_mode_changed 事件已发送

06 — Model Switch

  • 操作: POST /session/:id/model {modelId: "qwen3.7-max"}
  • 预期: 模型切换成功
  • 实际: ✅ {} (成功)
  • SSE 事件: model_switched 事件已发送

07 — 页面刷新恢复 Session (核心 PR 功能)

  • 操作: Chrome 导航到 http://localhost:5173/session/7f7d12ed-...?token=test-chrome-4380
  • 预期: SPA 路由正确加载,session 恢复
  • 实际: ✅
    • URL 保持: /session/7f7d12ed-... ← Vite proxy bypass 正确工作
    • 标题: "Qwen Code Web terminal"
    • Session 状态: displayName: "Chrome Web-shell Test", clientCount: 5
    • 页面未报 404(Vite bypass 函数正确区分 HTML 导航和 API 请求)

Daemon 日志

qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=...)
qwen serve: bound to workspace "..."
qwen serve: updated session metadata "7f7d12ed-..." displayName=set
qwen serve: fs audit emit is the default no-op — 1 event(s) dropped...

无错误、无崩溃、无异常行为。


关键验证点

  1. SPA 路由 /session/:id ✅ — 页面刷新后 URL 保持,Vite proxy bypass 正确返回 index.html
  2. Session 自动创建 ✅ — 打开 /?token=... 自动创建 session 并导航到 /session/:id
  3. SSE 事件流 ✅ — prompt 响应、tool call、metadata 更新等事件全部通过 SSE 正确传递
  4. Tool call 全链路 ✅ — 模型思考 → 工具调用 → 结果 → 最终回复,SSE 事件链完整
  5. Approval mode / model switch ✅ — 切换操作成功,SSE 事件已广播
  6. Daemon 稳定性 ✅ — 日志干净,无错误

结论

PASS — Chrome 浏览器真实交互测试通过。Web-shell 的 daemon 连接、session 生命周期、SSE 事件流、SPA 路由刷新恢复均正常工作。

"dist/index.js",
"dist/types"
],
"scripts": {

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.

[P2] Wire web-shell tests into workspace test scripts

This package now adds several *.test.ts(x) files (config/daemon.test.ts, Markdown.test.ts, transcriptAdapter.test.ts, etc.), but package.json only defines dev and build. The root npm run test:ci runs npm run test:ci --workspaces --if-present, so packages without test:ci are silently skipped; these new tests will not run in the normal preflight/CI path. Please add test/test:ci scripts for packages/web-shell (for example vitest run) so the coverage added in this PR is actually exercised.

### 安装

```bash
npm install @alife/dataworks-qwen-code-web-shell

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.

[P2] Use the published package name in README examples

package.json declares this workspace as @qwen-code/web-shell, but the README title, install command, and import example use @alife/dataworks-qwen-code-web-shell. Following these instructions would install/import a different package name and fail for consumers of this PR. Please update the README examples to match the actual package name/export.

@@ -0,0 +1,53 @@
{
"name": "@qwen-code/web-shell",

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.

[P1] Update package-lock for the new workspace

This PR adds packages/web-shell/package.json, but package-lock.json has no packages/web-shell / @qwen-code/web-shell entry and none of the new web-shell dependencies are locked. A clean install currently fails before tests/build can run: npm ci --dry-run --ignore-scripts reports Missing: @qwen-code/web-shell@0.0.1-preview.0 from lock file plus the new dependency tree. Please run npm install --package-lock-only (or the repo-approved lockfile update flow) and include the lockfile changes so npm ci/preflight works on a fresh checkout.

const clientId = parseClientIdHeader(req, res);
if (clientId === null) return;
try {
await bridge.detachClient(sessionId, clientId);

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.

[P1] Validate detach client membership before decrementing attach state

This route forwards any absent or merely well-formed X-Qwen-Client-Id to bridge.detachClient. In the bridge, detachClient decrements attachCount before unregisterClient checks whether that client id is actually registered, and unregisterClient no-ops for unknown ids. A caller can therefore send repeated detach requests with no header or a forged valid id and drain attachCount, potentially triggering the deferred-reap path for a session they never attached to. Please require a known client id here or make the bridge decrement only after a real client registration was removed.

disabled={isDisabled}
commands={commands}
skills={connection.skills ?? []}
daemonBaseUrl={DAEMON_BASE_URL}

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.

[P2] Pass the embedded baseUrl into @ completion

The earlier fix added createAtCompletionSource({ baseUrl, token }), but this call site still passes only the module-level DAEMON_BASE_URL parsed from the standalone URL. In embedded usage (<WebShell baseUrl="http://127.0.0.1:4170" token="..." />), the main session connects via the baseUrl prop at lines 241-244, while @ file completion receives undefined/URL-derived baseUrl and falls back to the host page origin. The result is silent empty @ completions for library consumers. Please pass baseUrl ?? DAEMON_BASE_URL here so completion targets the same daemon as the session.

const timer = setTimeout(() => {
codeToHtml(code, {
lang: resolvedLang as BundledLanguage,
theme: 'github-dark-default',

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.

[P2] Make rendered Markdown honor the selected theme

WebShellProps exposes theme?: 'dark' | 'light', but rendered Markdown blocks are pinned to dark-only renderers: Mermaid is initialized with theme: 'dark' and Shiki uses github-dark-default. In theme="light" embeds, code blocks and Mermaid diagrams will still render with dark inline colors/backgrounds instead of the selected shell theme. Please thread the active shell theme into Markdown/CodeBlock/MermaidBlock (or use CSS-variable-aware rendering) so light-theme consumers get consistent output.

@chiga0 chiga0 self-requested a review May 26, 2026 15:53

@chiga0 chiga0 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.

approved. just notice the package name with @qwen-code/xxx。

 into feat/daemon-react-cli

# Conflicts:
#	packages/cli/src/serve/server.test.ts
@wenshao wenshao merged commit f4d03f1 into QwenLM:daemon_mode_b_main May 27, 2026
doudouOUC added a commit that referenced this pull request May 27, 2026
Squashed feature work from daemon_mode_b_main branch, rebased onto
latest main to establish proper merge-base and clean PR diff.

Original commits:
- perf(core): F2 cleanup PR A — R9/W11/W12/R10 (post-merge follow-ups) (#4411)
- refactor(acp-bridge): F1 test split — lift bridge.test.ts (6861 LOC) to acp-bridge (#4445)
- fix(core): F2 cleanup PR B — self-heal observability (W133-a + W134) (#4460)
- feat(sdk/daemon-ui): unified completeness follow-up to #4328 (#4353)
- docs(serve): v0.16-alpha known limits + SDK QWEN_SERVER_TOKEN env fallback (PR 27) (#4473)
- docs(deploy): local launch templates for v0.16-alpha (PR 30a) (#4483)
- feat(daemon+sdk): cross-client real-time sync completeness (#4484)
- feat(serve): add POST /session/:id/recap (#4504)
- feat(daemon): add voterClientId to permission_resolved (A4) (#4539)
- feat(serve): --allow-origin <pattern> CORS allowlist (T2.4 #4514) (#4527)
- feat(daemon): in-session model switch reaches the bus (A1) (#4546)
- feat(serve): prompt absolute deadline + SSE writer idle timeout (#4514 T2.9) (#4530)
- Feat/daemon react cli (#4380)
doudouOUC pushed a commit that referenced this pull request May 27, 2026
* feat(daemon): add shared UI transcript layer

* fix(daemon): address ui review feedback

* test(daemon): cover raw event diagnostics option

* fix(daemon): address latest ui review

* fix(daemon): cover reconnect and status edge cases

* fix(daemon): guard prompt busy cleanup

* feat(daemon): add shared UI transcript layer

* fix(daemon): address ui review feedback

* test(daemon): cover raw event diagnostics option

* fix(daemon): address latest ui review

* fix(daemon): cover reconnect and status edge cases

* fix(daemon): guard prompt busy cleanup

* fix(daemon): handle trimmed tool updates

* fix(daemon): cap transcript text blocks

* fix(daemon): dedupe trimmed tool diagnostics

* fix(daemon): harden webui transcript edge cases

* fix(daemon): preserve webui daemon events

* fix(daemon): address latest ui review comments

* feat(web-shell): add daemon-backed UI shell

* feat(web-shell): improve session routing and slash commands

* feat(daemon): add shared UI transcript layer

* fix(daemon): address ui review feedback

* test(daemon): cover raw event diagnostics option

* fix(daemon): address latest ui review

* fix(daemon): cover reconnect and status edge cases

* fix(daemon): guard prompt busy cleanup

* fix(daemon): handle trimmed tool updates

* fix(daemon): cap transcript text blocks

* fix(daemon): dedupe trimmed tool diagnostics

* fix(daemon): harden webui transcript edge cases

* fix(daemon): preserve webui daemon events

* fix(daemon): address latest ui review comments

* fix(daemon): close latest ui review nits

* fix(daemon): harden ui review edges

* fix(daemon-ui): address wenshao 2 Critical findings (#4328 review)

## Critical #1 — 401/403 reconnect storm + transcript wipe

`DaemonSessionProvider`'s reconnect loop kept retrying `createOrAttach` on
401/403 even with `autoReconnect: true`. Each cycle:
  - hit the daemon with the same bad token → 401 again
  - cleared the session handle
  - the next successful attempt (if token magically recovered) would
    receive a different sessionId, triggering the `store.reset()` branch
    at line 143 and wiping the user's transcript
  - no terminal "auth failed" state surfaced to the user

Fix: split `TERMINAL_SESSION_HTTP_STATUSES` into `AUTH_FAILURE_HTTP_STATUSES`
(401, 403) and the rest (404, 410). On auth failure, return from the
reconnect loop unconditionally regardless of the `autoReconnect` flag —
these are credential failures, not transient. The user must update
credentials; daemon spam must stop.

`extractHttpStatus` helper factored out of `isTerminalSessionHttpError` to
share between the two predicates.

## Critical #2 — rawInput / rawOutput leaking secrets to UI

`normalizer.normalizeToolUpdate` forwarded `rawInput` / `rawOutput`
verbatim onto `DaemonUiToolUpdateEvent` → `DaemonToolTranscriptBlock`. The
`details` projection was redacted via `stringifyRedactedJson` /
`redactSensitiveFields`, but the underlying `rawInput` / `rawOutput`
fields were unredacted. Any UI component that read those fields directly
(ShellToolCall, WriteToolCall, JSON debug panels) leaked the raw values
to the DOM.

Example: `{ command: 'curl', apiKey: 'sk-prod-...' }` had `apiKey`
redacted in `details` but exposed verbatim on `rawInput`.

Fix: apply `redactSensitiveFields` to both `rawInput` and `rawOutput`
ONCE at the normalizer boundary, then reuse the redacted shape for the
`details` projection. Downstream is uniformly safe; no double traversal.

## Tests (49/49 pass)

- SDK `daemonUi.test.ts` (36 tests, +1) — new test `redacts sensitive
  fields in tool.update rawInput and rawOutput at normalizer boundary`
  verifies full-event string scan finds zero secret values + structural
  keys preserved with values `'[redacted]'`.
- WebUI `DaemonSessionProvider.test.tsx` (13 tests, +2) — new tests
  `breaks out of the reconnect loop on 401 / 403 auth failures even when
  autoReconnect is true` and `still reconnects on 404 / 410
  session-not-found errors when autoReconnect is true` lock in the
  asymmetry: auth failure → 1 attempt only; session-not-found → retries
  until success.

## Out of scope (declined / deferred — see PR review reply)

- CRIT #3 `withActionTimeout` test coverage gap → behavior correct,
  test-only follow-up (avoids PR bloat)
- Suggestions #4-7 → 4 nice-to-haves, deferred to keep PR focused on
  production-correctness fixes

Generated with AI

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(daemon-ui): redact tool details in web transcript

* feat(web-shell): align daemon UI interactions

* fix(web-shell): address daemon UI review comments

* feat(web-shell): sync independent web-shell with lib build, i18n, and daemon serve enhancements

Bring in the independently developed web-shell package with full lib
build support (vite.lib.config.ts, tsconfig.lib.json), i18n layer,
new dialogs (Help, Theme, ReleaseSession), composer hiding during
approvals, and SDK dependency restructured as peerDependency. Also
adds daemon serve routes (detach endpoint, rename persistence) and
fixes acp-bridge testUtils missing cancelImpl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): address daemon UI review comments

- Strip token from URL after caching (prevents Referer/history leak)
- Add URL scheme allowlist for markdown links/images (block javascript:)
- Add CORS restriction in vite dev server
- Handle state_resync_required event (reset store)
- Reset promptStatus on SSE disconnect
- Handle 401/403 in reconnect loop (no retry on auth failures)
- Heartbeat consecutive failure detection (3 strikes → disconnect)
- Strip <style> tags in SVG sanitization
- Replace naive diff with LCS-based buildUnifiedDiff
- Fix inputHighlight decoration ordering (sort before add)
- Add isEditableTarget guard in useDelayedGlobalKeyDown
- Fix AskUserQuestion keyboard handler (no capture phase)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): address second-round review Critical issues

- Add size guard to buildUnifiedDiff (fallback when n*m > 250k)
- Strip SVG animation elements (animate, set, animateTransform, animateMotion)
- Reset promptStatus to idle on state_resync_required
- Restrict getAllowedDaemonOrigin to same port as page origin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): address remaining PR #4380 review issues

- SVG sanitizer: strip style/use/image/feImage/mpath, block external hrefs
- Markdown: split isSafeHref/isSafeImageSrc (allow data:image for img only)
- Heartbeat: fire disconnect once at 3 failures, self-heal on success
- state_resync_required: reset store and reconnect (remove dead code)
- Auth 401/403: log error, stop reconnect loop, show error state
- replaceSessionUrl: delete ?token param to prevent leak
- removeDaemonTokenFromUrl() called at module init
- Vite dev server: cors: false
- killSession: forgetSession before byId.delete (prevent lost events)
- inputHighlight: collect ranges and sort before adding to builder
- useDelayedGlobalKeyDown: isEditableTarget guard from shared utils
- buildUnifiedDiff: proper O(nm) LCS, hasDiffContent lightweight check
- detachDaemonClient: restore console.warn for observability
- App.tsx: use rAF-coalesced messageBlocks in extractPendingPermission
- extractPendingPermission: extract toolCallId from toolCall record
- vite.lib.config: wrap CSS injection in try/catch for CSP
- Add test coverage: server routes, SDK methods, transcriptAdapter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): address third-round PR #4380 review issues

Critical fixes:
- ToolApproval: reset submittedRef via useEffect on request.id change
- Effect cleanup: reject pendingSessionLoadRef on dispose
- sanitizeSvg: strip style attributes with external url() values

Suggestion fixes:
- <use> elements: keep fragment-only href, strip external (+ xlink:href fallback)
- SAFE_IMAGE_DATA_URI: remove svg+xml (can load external subresources)
- extractStreamingState: accept blocks directly, remove state dependency
- coalescedState useMemo removed — rAF coalescing no longer defeated
- Auth failure log: use missingSessionId instead of already-cleared vars
- newSession(): reject pending loadSession promise
- COPY_MESSAGES: wire constants to copyFromLastAssistantMessage
- Add 39 tests for isSafeHref, isSafeImageSrc, sanitizeSvg
- Add 3 tests for toolCallId extraction fallback
- Fix test fixtures: resolved: undefined, clientReceivedAt: 1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): delegate readWorkspaceFile to SDK client

Replaces the manual fetch() call with session.client.readWorkspaceFile()
which provides fetchWithTimeout (30s default) and error normalization.
Ensures DaemonClient baseUrl is always absolute by falling back to
window.location.origin in proxy mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): address fourth-round PR #4380 review issues

- Fix suppressedOwnUserEchoCountRef not decrementing on prompt failure
- Add heartbeat status guard to prevent overwriting 'connecting' state
- Abort stale activePrompts when SSE session disconnects
- Truncate displayName to 256 chars in renameSession endpoint
- Fix DiffView counting +++ / --- header lines as additions/deletions
- Preserve existing command properties in mergeCommands
- Fix bridge cwd override by params spread order
- Validate all href attributes on SVG <use> elements
- Extend external url() check to all SVG attributes, not just style
- Unify detachDaemonClient baseUrl with DaemonClient construction
- Delegate loadMcpTools to SDK client instead of returning stub
- Add createAtCompletionSource factory with baseUrl/token fallback
- Reset AskUserQuestion state on request.id change
- Add useEffect cleanup for queue drain setTimeout
- Suppress replay_complete from reaching UI as unrecognized event

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): address fifth-round PR #4380 review issues

- Use safeWorkspaceCwd in buildWorkspaceToolsStatus for consistency
- Wire loadMcpTools to return SDK tools instead of hardcoded empty array
- Consolidate WebShellMcpToolsStatus types (remove duplicate in McpDialog)
- Abort active prompts in loadSession before switching sessions
- Pass daemon credentials to @-completion source via Editor props

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell,cli): address PR #4380 review issues and fix duplicate user message

- Remove Session#executePrompt's emitUserMessage() call to eliminate
  duplicate user_message_chunk events (bridge-echo is the single source)
- Move removeDaemonTokenFromUrl() to main.tsx entry point (S19)
- Add mount-grace, interaction guard, safe default index to ToolApproval (Critical#1)
- Fix stale credential capture in Editor @-completion (Critical#3)
- Add submittedRef guard to AskUserQuestion, remove unsafe fallback (S18/S23)
- Use .then() pattern for clipboard writeText (S17)
- Add i18n for approval dialog and rename messages (S20)
- Add session load timeout (S15)
- Distinguish MCP error types with DaemonHttpError (S12)
- Clear stale heartbeat error on success (S13)
- Fix null vs undefined clientId check in server detach (S16)
- Add daemon.test.ts for origin validation coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell,cli): address PR #4380 R9 review — detach loose equality, ToolApproval stale refs, session load timeout leak

- server.ts: change `clientId == null` to `=== null` so absent header falls through to detachClient instead of hanging the request
- server.test.ts: add test for detach without X-Qwen-Client-Id header
- ToolApproval.tsx: use refs to fix stale closures in handleKeyDown, reset submittedRef on request.id change, sync selectedRef on mouse hover, remove unstable request.options from effect deps
- useDaemonSession.ts: store and clear timeout handle in PendingSessionLoad across all resolution paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web-shell): add submittedRef guard to AskUserQuestion handleCancel

Prevents double-submission on rapid Escape+Enter and avoids sending
empty optionId when no reject option exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

4 participants