Skip to content

feat(web-shell): add contextual task panels - #7929

Merged
ytahdn merged 28 commits into
QwenLM:mainfrom
ytahdn:feat/web-shell-context-panels
Jul 30, 2026
Merged

feat(web-shell): add contextual task panels#7929
ytahdn merged 28 commits into
QwenLM:mainfrom
ytahdn:feat/web-shell-context-panels

Conversation

@ytahdn

@ytahdn ytahdn commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR turns the Web Shell right side into a persistent contextual workspace. It adds a configurable chat header, a responsive context panel for environment information, subagents, Monitor jobs, and shell background tasks, and a tabbed extension area that can show reviews, task details, artifacts, and files without leaving the current conversation.

It also adds side tasks: hidden, persistent sessions created from a serialized snapshot of the main conversation context, including while the main response is still running. A side task supports continued conversation, model and mode selection, first-message naming, reload and reopen, and nested review or artifact tabs, while remaining absent from the normal session list and hiding the inherited transcript from its own chat UI.

Hosts can choose which header actions, right-panel entries, and context-panel sections are available, replace the full chat header, and create a side task through the public Web Shell API. /tasks opens the context panel, /btw <question> keeps the existing lightweight one-shot interaction, and /btw side <question> creates a persistent side task when the daemon advertises session_side_task.

Why it's needed

Live task state was previously spread across composer metadata, inline messages, dialogs, and separate pages. That made it difficult to follow subagents and long-running jobs while continuing the main conversation. The new layout keeps execution context and related work close to the chat, gives Monitor and shell jobs consistent detail views, and provides a lightweight way to branch into a durable secondary conversation without adding noise to the main session list.

Reviewer Test Plan

How to verify

  1. Open a Web Shell session and confirm the chat header shows the configured title and actions. Resize the message area around the responsive breakpoint and verify the context panel changes between docked and floating behavior. With the right extension area already open, confirm the context panel opens as a floating popover; when opening a task detail from the context panel, confirm the context panel remains visible and switches to floating if needed.
  2. Run a subagent, a Monitor job, and a background shell command. Confirm each appears in the context panel with the correct status and icon, and opens the matching detail tab. Closing the last detail tab should return to the empty extension page instead of closing the extension area.
  3. Create a side task from the empty extension page, the add menu, the public shell API, and /btw side <question>. Repeat while the main session is responding. Confirm the main response continues, the side task receives the persisted main-conversation context at creation time, does not display inherited messages, does not appear in the normal session list, supports model and mode changes, and remains available after reload. Also confirm /btw <question> still uses the lightweight one-shot interaction.
  4. Send the first message in a new or restored-empty side task and confirm the title changes to that message. A transient title persistence failure should retry, while an existing side task with user history must keep its title.
  5. From a subagent, fork, or side task, create or open a file, artifact, or review and confirm it opens as another right-panel tab.
  6. Configure subsets of header actions, right-panel items, and context-panel sections and confirm omitted items are not rendered. Passing an empty header item list should render no default header actions.

Evidence (Before & After)

Before: task context was distributed across inline messages, composer metadata, and dialogs, and there was no persistent workspace for related reviews or secondary conversations.

After: the chat header, responsive context panel, and tabbed right extension area provide a single place to inspect and continue subagent, Monitor, shell, review, artifact, and side-task workflows.

Tested on

OS Status
macOS ✅ Build, typecheck, lint, and targeted tests passed
Windows Not tested
Linux Not tested

Environment (optional)

Local Web Shell connected to the local daemon on macOS.

Risk & Scope

  • Main risk or tradeoff: The change spans session metadata, transcript replay, daemon routes, SDK types, and Web Shell state. Side-task source boundaries must continue to hide inherited records without losing the context snapshot used by the agent.
  • Not validated / out of scope: Windows and Linux visual QA, browser-specific pixel-level differences, and production-scale transcript performance beyond the covered replay and pagination tests.
  • Breaking changes / migration notes: None expected. New host customization options and APIs are additive, and existing embeddings without a configured chat header continue to omit the header.

Linked Issues

N/A

Pictures

image image image image
中文说明

这个 PR 做了什么

这个 PR 将 Web Shell 右侧区域完善为常驻的上下文工作区。新增了可配置的聊天顶部 Header、用于展示环境信息、子智能体、Monitor 任务和 Shell 后台任务的响应式信息面板,以及可通过页签展示审查、任务详情、产物和文件的右侧拓展区,用户无需离开当前会话即可查看和处理相关内容。

同时新增了“侧边任务”:它是基于主会话创建瞬间的序列化上下文快照生成的、可持久化但不会出现在普通会话列表中的会话,并支持在主会话仍在响应时创建。侧边任务支持持续对话、切换模型和模式、使用第一条消息命名、刷新后重新打开,并支持在右侧继续打开审查或产物页签;其聊天界面不会展示继承自主会话的历史消息。

宿主可以配置 Header 操作项、右侧拓展区入口和信息面板区块,也可以完整替换聊天 Header,并通过公开的 Web Shell API 创建侧边任务。/tasks 会打开信息面板;/btw <问题> 保留原有的轻量单次问答;当 daemon 支持 session_side_task 时,/btw side <问题> 会在右侧拓展区创建可持续对话的侧边任务。

为什么需要

此前实时任务状态分散在输入框底部信息、消息流、弹窗和独立页面中,用户在继续主会话时不容易持续跟踪子智能体和长时间运行的任务。新的布局让任务上下文和相关操作始终靠近聊天区域,为 Monitor 与 Shell 任务提供一致的详情交互,并允许用户创建一个持久的次级会话,同时不干扰主会话列表。

审查者测试计划

如何验证

  1. 打开 Web Shell 会话,确认聊天 Header 展示配置的标题和操作入口。调整消息区域宽度跨过响应式临界值,确认信息面板能在占位和悬浮形态间切换。右侧拓展区已打开时,确认信息面板可以悬浮打开;从信息面板打开任务详情时,确认信息面板保持展示,并在空间不足时切换为悬浮。
  2. 分别运行子智能体、Monitor 任务和 Shell 后台命令,确认它们以正确的状态和图标出现在信息面板中,并能打开对应的详情页签。关闭最后一个详情页签后,应返回拓展区空白页,而不是关闭整个拓展区。
  3. 分别从拓展区空白页、加号菜单、公开 Web Shell API 和 /btw side <问题> 创建侧边任务,并在主会话响应期间重复验证。确认主响应不中断,侧边任务在创建时获得已持久化的主会话上下文,但不展示继承消息、不出现在普通会话列表中,支持切换模型和模式,并能在刷新后重新打开;同时确认 /btw <问题> 仍保持轻量单次问答。
  4. 在全新或恢复后仍为空的侧边任务中发送第一条消息,确认标题更新为该消息。标题持久化遇到瞬时失败时应自动重试;已有用户历史的侧边任务不应被重新命名。
  5. 从子智能体、fork 或侧边任务中创建或打开文件、产物或审查,确认内容会作为新的右侧页签打开。
  6. 分别配置 Header 操作项、右侧拓展区入口和信息面板区块的子集,确认未配置的内容不会展示。Header 的操作项传空数组时,不应展示默认操作。

前后对比

改动前:任务上下文分散在消息流、输入框底部信息和弹窗中,也没有用于承载审查和次级会话的常驻工作区。

改动后:聊天 Header、响应式信息面板和页签化右侧拓展区将子智能体、Monitor、Shell、审查、产物和侧边任务相关流程集中在同一处。

测试环境

操作系统 状态
macOS ✅ Build、typecheck、lint 和定向测试通过
Windows 未测试
Linux 未测试

环境

macOS 本地 Web Shell 连接本地 daemon。

风险与范围

  • 主要风险或取舍:改动涉及会话来源元数据、历史回放、daemon 路由、SDK 类型和 Web Shell 状态。侧边任务的来源边界需要持续保证继承记录不会显示在界面中,同时不能丢失智能体使用的上下文快照。
  • 未验证或不在范围内:Windows 和 Linux 的视觉验收、不同浏览器的像素级差异,以及超大历史记录在现有回放和分页测试范围之外的性能表现。
  • 破坏性变更或迁移说明:预计没有。新增的宿主配置和 API 均为增量能力;未配置聊天 Header 的现有接入仍然不会展示 Header。

关联 Issue

@ytahdn

ytahdn commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@yiliang114

Copy link
Copy Markdown
Collaborator

⚠️ Failed to process this request. Please re-mention the bot to retry.

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 28, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: this is a feature addition, not a bug fix — no reproduction needed. The motivation is clear: live task state (subagents, Monitor jobs, shell background tasks) was spread across composer metadata, inline messages, dialogs, and separate pages, making it hard to follow long-running work while continuing the main conversation. The PR consolidates this into a persistent right-panel workspace and adds "side tasks" (hidden, persistent sessions forked from the main conversation context).

Direction: aligned with the Web Shell's evolution toward a richer workspace. The CHANGELOG has no direct reference to side tasks, but the area (session management, multi-agent UX) is clearly relevant to the project's roadmap.

Size: this PR touches core paths (packages/core/src/services/sessionService.ts, session-transcript-reader.ts, packages/core/src/utils/transcript-records.ts) — 102 production lines in core out of ~4,284 total production lines (8,165 additions + 572 deletions across 72 files; ~4,341 test lines, 112 docs lines). Because production logic exceeds 500 lines, this is flagged for maintainer awareness per the core-module gate. The 1,000+ advisory also applies — this is a large PR; splitting the panels infrastructure, side-task lifecycle, and environment panel into separate PRs would make each easier to review and revert, though I recognize the features are interdependent.

Approach: the scope is ambitious — configurable chat header, responsive context/environment panel, tabbed extension area, side tasks with first-message naming, /btw side command, host customization API, and shell/monitor task detail views. Each piece is reasonable on its own; together they make for a heavy review. The core changes are focused (adding forkedFrom to transcript records, source-boundary filtering in the transcript reader, and extending forkSession with a source option). The side-task mechanism reuses the existing branch/fork infrastructure rather than building a parallel path, which is the right call.

Risk: packages/cli/src/acp-integration/acpAgent.ts matched the high-risk path signal (correlated with post-merge reverts in this repo). The changes there add selectVisibleHistoryRecords for inherited-record filtering and the sessionSideTask ext-method handler — both are additive and follow existing patterns, but this file warrants careful review.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个功能新增,不是 bug 修复——不需要复现。动机清晰:实时任务状态(子智能体、Monitor 任务、Shell 后台任务)此前分散在输入框元数据、消息流、弹窗和独立页面中,用户在继续主会话时难以持续跟踪长时间运行的任务。PR 将这些整合到常驻右侧工作区,并新增"侧边任务"(基于主会话上下文快照创建的隐藏持久会话)。

方向:与 Web Shell 向更丰富工作区演进的方向一致。CHANGELOG 中没有直接提及侧边任务,但该领域(会话管理、多智能体 UX)与项目路线图明显相关。

规模:PR 触及核心路径(packages/core/src/services/sessionService.tssession-transcript-reader.tspackages/core/src/utils/transcript-records.ts)——核心生产代码 102 行,总生产代码约 4,284 行(8,165 行新增 + 572 行删除,72 个文件;约 4,341 行测试代码,112 行文档)。因生产代码超过 500 行,按核心模块门控规则标记为维护者关注。1,000+ 大 PR 建议也适用——如果将面板基础设施、侧边任务生命周期和环境面板拆成独立 PR,每个会更容易审查和回滚,尽管这些功能存在相互依赖。

方案:范围很大——可配置聊天 Header、响应式环境面板、页签化拓展区、侧边任务(含首条消息命名)、/btw side 命令、宿主自定义 API、Shell/Monitor 任务详情视图。每部分单独看都合理,合在一起审查负担较重。核心改动聚焦(为 transcript 记录添加 forkedFrom、transcript reader 中的来源边界过滤、扩展 forkSession 的 source 选项)。侧边任务机制复用了现有的 branch/fork 基础设施而非另建路径,这是正确的选择。

风险:packages/cli/src/acp-integration/acpAgent.ts 命中高风险路径信号(与该仓库合并后回滚相关)。该文件的改动新增了 selectVisibleHistoryRecords(用于过滤继承记录)和 sessionSideTask ext-method 处理——均为增量改动且遵循现有模式,但该文件值得仔细审查。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at b00fe1c79e253923e6c89eee2e179a5be7ef6f5a · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: for adding contextual task panels, I would extend the existing fork/branch mechanism with a source-type marker, add transcript filtering to hide inherited records from side-task replays, add a serve route + SDK method for side-task creation, and build the web-shell panels (right panel with tabs, environment panel) as new components reusing existing primitives.

Comparison: the PR matches this approach closely. The core changes are minimal and well-scoped — forkedFrom added to TranscriptRecordInput, source-boundary filtering in session-transcript-reader.ts, and forkSession extended with an optional source parameter. The side-task mechanism reuses branchSession in the bridge rather than building a parallel path, which is the right call.

No critical blockers found. A few observations:

  • Duplicated boundary logic. selectVisibleHistoryRecords in acpAgent.ts and the activeUuids filtering in session-transcript-reader.ts both locate the session_source record with sourceType === 'side_task' and filter out forkedFrom records. This is intentional — they operate at different layers (ACP replay vs. transcript reading) — but if the boundary semantics ever change, both sites need updating. Worth a shared constant or helper if a third consumer appears.

  • Concurrent side-task creation. bridge.ts allows side tasks to bypass BranchWhilePromptActiveError (concurrentSideTask = isSideTask && entry.promptActive) and skips chaining onto promptQueue. This is safe because forkSession reads the append-only transcript file and writes a new file, but it is a subtle concurrency path — the comment in the PR description ("supports creation while the main response is still running") matches the implementation.

  • Serve route cleanup. The POST /session/:id/side-task route mirrors the existing branch route's cleanup pattern (kill + remove on generation-guard failure, detach on non-writable response). Consistent and correct.

  • getTasks simplification. The webui/actions.ts change replaces requireSessionForAction with a direct null check so ChatPane can call getTasks speculatively for monitor-task correlation without triggering error notices on disconnected sessions. Reasonable.

  • First-prompt naming. SideTaskPanel retries renameSession up to 3 times on transient failure and guards against renaming sessions that already have user history. The canNameFromFirstPrompt logic (!hasUserPrompt && (shouldNameFromFirstPrompt || restoredEmptySession)) is correct.

Files changed (30 of 72 shown)
File What changed
docs/design/web-shell-context-panels.md Design doc for the feature
packages/core/src/services/sessionService.ts Extends forkSession with optional source metadata for side tasks
packages/core/src/services/session-transcript-reader.ts Adds source-boundary filtering to hide inherited records in side-task replays
packages/core/src/utils/transcript-records.ts Adds forkedFrom field to TranscriptRecordInput
packages/cli/src/acp-integration/acpAgent.ts Adds selectVisibleHistoryRecords and sessionSideTask ext-method handler
packages/cli/src/serve/routes/session.ts New POST /session/:id/side-task route with cleanup logic
packages/cli/src/serve/capabilities.ts Registers session_side_task capability
packages/acp-bridge/src/bridge.ts Adds createSideTaskSession, concurrent fork support, source metadata persistence
packages/acp-bridge/src/bridgeTypes.ts New types for side-task requests, hideInheritedHistory, source fields
packages/sdk-typescript/src/daemon/DaemonClient.ts New createSideTaskSession client method
packages/sdk-typescript/src/daemon/types.ts SideTaskSessionRequest and DaemonSideTaskSession types
packages/web-shell/client/App.tsx Major expansion: environment panel, side-task catalog, header config, task activity tracking
packages/web-shell/client/components/artifacts/ArtifactPanel.tsx Adds side-task, shell, and monitor tab kinds; empty-state add menu
packages/web-shell/client/components/artifacts/SideTaskPanel.tsx New component: side-task creation, session lifecycle, first-prompt naming
packages/web-shell/client/components/ChatPane.tsx Embedded mode, monitor detail support, first-prompt callback
packages/web-shell/client/components/ChatContextHeader.tsx New configurable chat header component
packages/web-shell/client/components/panels/EnvironmentPanel.tsx New responsive environment panel (subagents, background tasks, environment info)
packages/web-shell/client/components/messages/TasksStatusMessage.tsx Adds ShellTaskDetail component
packages/web-shell/client/customization.tsx New host customization types for header, right panel, environment panel
packages/web-shell/client/constants/localCommands.ts /btw side argument hint when side tasks available
packages/web-shell/client/constants/sessions.ts Side-task feature and source-type constants
packages/web-shell/client/i18n.tsx i18n strings for new UI surfaces
packages/web-shell/client/hooks/useBackgroundTasks.ts Extends background task tracking for shell tasks
packages/web-shell/client/utils/sessionErrors.ts Adds isSessionDisconnectedError helper
packages/webui/src/daemon/session/actions.ts Simplifies getTasks error handling for speculative calls
packages/web-shell/client/App.test.tsx Extensive test coverage for new App features (1703+ lines)
packages/web-shell/client/components/artifacts/SideTaskPanel.test.tsx Side-task panel tests (547 lines)
packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx Environment panel tests (532 lines)
packages/web-shell/client/components/artifacts/ArtifactPanel.test.tsx Artifact panel tests (484+ lines)
packages/core/src/services/sessionService.test.ts Core forkSession source-metadata tests
…and 42 more files

Testing

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
macos-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success
precheck-pr / precheck ✅ success
Classify PR ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped

All PR CI checks are green on b00fe1c79e253923e6c89eee2e179a5be7ef6f5a. macOS and Windows Node tests were skipped (infra config, not PR-caused). The web-shell E2E smoke test and visual capture both passed, which covers the new panel surfaces at a basic level.

Sandboxed verification would settle the remaining behavioural claims: @qwen-code /verify — that side-task creation during an active prompt correctly snapshots the parent context without interrupting the main response, and that inherited records are truly hidden from the side-task replay while remaining available as model context, is not observable from the diff alone. This is a sponsored run (fork PR — the author lacks write access); a maintainer's @qwen-code /verify comment approves the head it was written against, and the run carries a pre-execution risk screen plus a full workspace wipe. Read the resulting report with the same skepticism as the fork's own CI logs.

中文说明

代码审查

独立方案: 对于添加上下文任务面板,我会扩展现有的 fork/branch 机制(添加来源类型标记),添加 transcript 过滤以在侧边任务回放中隐藏继承记录,添加 serve 路由和 SDK 方法用于创建侧边任务,并构建 Web Shell 面板(带页签的右侧面板、环境面板)作为新组件复用现有原语。

对比: PR 与上述方案高度一致。核心改动最小且范围明确——TranscriptRecordInput 新增 forkedFromsession-transcript-reader.ts 中的来源边界过滤、forkSession 扩展可选 source 参数。侧边任务机制在 bridge 层复用 branchSession 而非另建路径,这是正确的选择。

未发现关键阻塞问题。几点观察:

  • 边界逻辑重复。 acpAgent.ts 中的 selectVisibleHistoryRecordssession-transcript-reader.ts 中的 activeUuids 过滤都定位 sourceType === 'side_task'session_source 记录并过滤 forkedFrom 记录。这是有意为之(不同层),但如果边界语义变更,两处都需更新。
  • 并发侧边任务创建。 bridge.ts 允许侧边任务绕过 BranchWhilePromptActiveError,不链入 promptQueue。因为 forkSession 读取追加式 transcript 文件并写入新文件,这是安全的。
  • Serve 路由清理。 POST /session/:id/side-task 路由的清理逻辑与现有 branch 路由模式一致。
  • 首条消息命名。 SideTaskPanel 在瞬时失败时最多重试 3 次 renameSession,并防止重命名已有用户历史的会话。逻辑正确。

测试

所有 PR CI 检查在 b00fe1c79e253923e6c89eee2e179a5be7ef6f5a 上均为绿色。web-shell E2E 冒烟测试和视觉捕获均通过。

沙箱验证可以确认剩余的行为声明:@qwen-code /verify——在活动提示期间创建侧边任务是否正确快照父上下文而不中断主响应,以及继承记录是否真正从侧边任务回放中隐藏但仍可用作模型上下文。这是赞助运行(fork PR——作者无写入权限)。

Qwen Code · qwen3.8-max-preview

Reviewed at b00fe1c79e253923e6c89eee2e179a5be7ef6f5a · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review with no correctness or security blockers, but the core-module escalation (500+ production lines touching packages/core/src/) needs a maintainer's sign-off before approval.

The implementation is solid. The side-task mechanism reuses the existing branch/fork infrastructure instead of building a parallel path, the core changes are minimal (102 production lines across three files), and the web-shell components follow existing conventions. CI is fully green, including the web-shell E2E smoke test and visual capture. The test coverage is substantial (~4,341 test lines).

What gives me pause is not quality but scope. At ~4,284 production lines across 72 files and 6 packages, this PR bundles a configurable chat header, a responsive environment panel, a tabbed extension area, side-task lifecycle management, shell/monitor task detail views, host customization API, and a /btw side command. Each piece is reasonable; together they make a review that is hard to fully absorb. The high-risk path signal on acpAgent.ts adds weight to the case for a careful human pass.

The behavioural claims around concurrent side-task creation (snapshotting parent context mid-response without interrupting it) and inherited-record hiding (visible to the model but absent from the replay) are the parts I am least able to verify statically. A @qwen-code /verify sponsored run would close that gap.

Deferring to @wenshao for the maintainer call on scope and the core-module gate.

中文说明

置信度:3/5 —— 审查未发现正确性或安全性阻塞问题,但核心模块升级(500+ 生产代码行触及 packages/core/src/)需要维护者签核后才能批准。

实现质量扎实。侧边任务机制复用了现有的 branch/fork 基础设施而非另建路径,核心改动最小(3 个文件共 102 行生产代码),Web Shell 组件遵循现有约定。CI 全绿,包括 web-shell E2E 冒烟测试和视觉捕获。测试覆盖充分(约 4,341 行测试代码)。

让我犹豫的不是质量而是范围。约 4,284 行生产代码、72 个文件、6 个包,PR 打包了可配置聊天 Header、响应式环境面板、页签化拓展区、侧边任务生命周期管理、Shell/Monitor 任务详情视图、宿主自定义 API 和 /btw side 命令。每部分单独看都合理,合在一起审查负担很重。acpAgent.ts 的高风险路径信号增加了仔细人工审查的必要性。

并发侧边任务创建(在响应中快照父上下文而不中断)和继承记录隐藏(模型可见但回放中不显示)的行为声明是静态审查最难验证的部分。@qwen-code /verify 赞助运行可以弥合这一差距。

转交 @wenshao 做维护者决策。

Qwen Code · qwen3.8-max-preview

Reviewed at b00fe1c79e253923e6c89eee2e179a5be7ef6f5a · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 1f5d68e. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

mermaid-diagram-dark before/after

mermaid-diagram-light before/after

model-dialog-dark before/after

model-dialog-light before/after

permission-panel-dark before/after

permission-panel-light before/after

session-transcript-dark before/after

session-transcript-light before/after

sidebar-attention-dark before/after

sidebar-attention-light before/after

slash-menu-dark before/after

slash-menu-light before/after

theme-dialog-dark before/after

theme-dialog-light before/after

workspace-sidebar-dark before/after

workspace-sidebar-light before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 1f5d68e, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

capabilities

field PR base (before) this PR (after)
features[] "session_side_task"

Qwen Code · serve A/B

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

qwen-code-ci-bot pushed a commit that referenced this pull request Jul 28, 2026
qwen-code-ci-bot pushed a commit that referenced this pull request Jul 28, 2026
@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Maintainer-side local verification — PR #7929 (head 8dbb4b8)

I built this PR in an isolated worktree (own npm ci-equivalent install, own npm run build + npm run bundle) and verified it against a real qwen serve daemon and a real browser, not mocks: a local fake OpenAI-compatible model server drives real turns, real background subagents, real Monitor/shell tasks, and the daemon serves the built Web Shell dist. On top of that I ran a mutation matrix against the PR's own tests and A/B'd every failure against an origin/main worktree on the same machine.

Verdict: the side-task feature itself is solid — but the PR is not mergeable as-is. Three CI-red failures are real and PR-caused; all three are reproduced locally, root-caused, and each has a fix I verified. One functional defect in the new context panel shows up on a live daemon. Everything else I could exercise behaves as advertised.

verification evidence


✅ What is verified working (real daemon, real browser)

I wrote a reviewer-side E2E (integration-tests/cli/reviewer-side-task.test.ts, not part of this PR) that drives POST /session/:id/side-task end to end. 9/9 pass:

Property Evidence
Route + capability session_side_task advertised; 201 with parentSessionId, displayName, no forkedFrom, sourcePersisted=true
Persisted boundary side-task JSONL: record[0] = system/session_source {sourceType:"side_task", sourceId:<parent>}, 4 inherited records each carrying forkedFrom.sessionId=<parent>
Hidden from the creation replay parent marker absent from the 201 payload
Hidden from the persisted transcript page GET /session/:id/transcript has no parent marker — control: the same route on the parent does return it
Boundary is authoritative a plain loadSession that never asks to hide inherited history still replays nothing from the parent
Context is not lost prompting the side task produces a model request whose messages[] does contain the parent's marker
Not vacuous after the side task's own turn, its own user message is returned by the same route, parent history still absent
Session list sourceType=default → parent yes / side task no; sourceType=side_task → side task yes

In the live UI, /fork sider issues 201 POST /session/:id/side-task, opens the side task as a right-panel tab with an empty transcript, and the sidebar still lists only the parent session:

side task via /fork sider on a live daemon

Subagent detail tabs work against the real backend too (real background subagent, real transcript, completed badge):

subagent detail tab

Local gates on the PR's 67 files: web-shell 150 files / 2459 tests, core 182, cli 1274 (4 targeted files), acp-bridge 459, sdk 300, webui 249 — all green; eslint --max-warnings 0, prettier --check, tsc --noEmit all exit 0; the 20 new i18n keys are balanced en/zh.

Verified non-issue: I specifically checked whether the new sourceType: 'default' sidebar filter would hide legacy sessions. On a freshly restarted daemon (persisted path, nothing live) ?sourceType=default still returns sessions with no source metadata, so pre-existing / CLI / channel sessions do not disappear.


⛔ Blockers (CI is red; reproduced locally; fixes verified)

A. packages/cli/src/serve/server/telemetry-catalog.test.ts — route drift guard
POST /session/:id/side-task is registered but not added to the telemetry catalog:
expected [ 'DELETE /session/:id', …(50) ] to have a length of 50 but got 51. Passes on main.
Fix I ran locally (test then passes):

// packages/cli/src/serve/server/telemetry.ts — next to the /branch and /fork entries
{
  method: 'POST',
  path: '/session/:id/side-task',
  attribution: 'handler_resolved',
  route: 'POST /session/:id/side-task',
},
// packages/cli/src/serve/server/telemetry-catalog.test.ts
expect(registered).toHaveLength(51);

B. Duplicated data-testid="chat-pane" breaks web-shell.split-persist.spec.ts
App.tsx:8713 adds a second data-testid="chat-pane" on the chat-pane container while ChatPane.tsx:633 already carries it, so the split view now resolves 3 nodes instead of 2. Removing just that attribute makes both split-persist tests pass. Suggest a distinct id (e.g. chat-pane-container) rather than changing the spec, since the per-pane id is the meaningful one.

C. The new sidebar sourceType filter hard-requires session_source_metadata
The queries added in WebShellSidebar.tsx / WorkspaceSection.tsx pass sourceType, which makes the SDK call requireCapability('session_source_metadata'). Against a daemon that does not advertise that feature the whole list request throws and the sidebar renders “Failed to load sessions. Click to retry.” — that is exactly why web-shell.collapsed-groups-persist.spec.ts fails (adding that one capability to the scenario makes it pass). Current daemons advertise it (since: 'v1'), but this is a hard failure rather than graceful degradation for older/embedded daemons. Suggest gating it the way SESSION_ORGANIZATION_FEATURE already is, and omitting sourceType when the capability is absent.

A/B on the same machine: --grep @smokemain 23 passed, PR head 21 passed / 2 failed (identical to GitHub CI).


🐛 Functional finding — the same subagent is listed twice with contradictory statuses

With a real background subagent (launched by the model, finished successfully), the new Subagents section shows it twice — once derived from the transcript tool card (Running) and once from the daemon's live task snapshot (Completed). Reproduced on two independent daemon runs, and it survives a session reload:

duplicate subagent entry in the context panel

Root cause — getEnvironmentAgentTasks (App.tsx) dedupes on three keys, and on a real daemon none of them match:

  • task.toolUseId === tool.callId — the /tasks snapshot for a finished agent carries no toolUseId (the sidecar meta doesn't persist it either), so this never matches after completion;
  • task.id === taskId — that map is built from system messages carrying both taskId and toolUseId; the real completion record (user/notification) carries backgroundTask.taskId only;
  • task.id === ${subagentType}-${tool.callId} — the daemon's id is general-purpose-8c7ed91f, where the suffix is the internal agent id, not the tool call id.

The daemon can correlate the two: GET /session/:id/subagents/<toolCallId> returns {taskId: "general-purpose-8c7ed91f", status: "completed"} for exactly that tool call. That is the endpoint #7561 used, and this PR removes the last caller of it for this purpose (useMessages.ts −233 lines, useMessages.test.ts −430 lines); after a restore the page now issues zero /subagents/ requests. Either resolve through that endpoint again, or fall back to matching on description/prompt, or have the daemon keep toolUseId on completed agent tasks.


🧪 Coverage gaps found by mutation testing (3 killed / 2 survived)

Two of the PR's new invariants have no test holding them:

  • selectVisibleHistoryRecords — the “persisted boundary is authoritative” rule (acpAgent.ts). Changing if (sourceBoundary >= 0) to if (sourceBoundary >= 0 && hideInheritedHistory) keeps 336/336 acpAgent tests green, while my E2E fails immediately: a plain re-load leaks the parent transcript into the side task. This is the highest-value missing test in the PR — the failure mode is a privacy/UX leak on reload.
  • hideInheritedHistory in the bridge's in-flight restore coalescing key. Removing it keeps 459/459 bridge tests green.

(Killed, i.e. genuinely load-bearing: the transcript-reader boundary slice, the custom_title drop in sourced forks, and the /fork task-id match in virtual-subagent-sessions.ts.)

🔍 Nits

  • Three new i18n keys are unused in both locales: environment.branch, environment.history, environment.changeCount. The branch row renders only a value with no label (in a non-git workspace it reads as a bare “Unavailable”, see the panel crop) — environment.branch looks like the label that was meant to go there.

Suggested merge path

  1. Fix A, B, C (CI is red on all three; each fix is small and verified above).
  2. Fix D or file it as an immediate follow-up — it is visible on the very first background subagent a user launches.
  3. Add tests for E (authoritative boundary) and F (coalescing key) — E especially, since the mutation is a one-liner that CI would not catch today.

Everything about the core design — the session_source boundary record, the transcript reader slice, the daemon route, the SDK method, and the context-snapshot-without-visible-history behaviour — held up under real end-to-end use.

中文说明

维护者本地验证 —— PR #7929(head 8dbb4b8

我在独立 worktree 中完整构建了该 PR(独立依赖安装、独立 npm run build + npm run bundle),并用真实的 qwen serve daemon + 真实浏览器验证,而非 mock:本地假 OpenAI 模型服务驱动真实对话轮次、真实后台子智能体、真实 Monitor / Shell 任务,daemon 提供的是已构建的 Web Shell dist。此外还对 PR 自带测试做了变异矩阵测试,并在同一台机器上用 origin/main worktree 做了 A/B 对照。

结论:侧边任务功能本身是扎实的,但当前状态不建议直接合并。 三个 CI 红灯都是本 PR 引入的真实问题,均已在本地复现、定位根因,并验证了修复方案。另有一个在真实 daemon 上可见的界面缺陷。其余我能验证的行为都与描述一致。

✅ 已验证正常(真实 daemon + 真实浏览器)

我编写了 reviewer 侧的 E2E(integration-tests/cli/reviewer-side-task.test.ts,不属于本 PR),端到端驱动 POST /session/:id/side-task9/9 通过

验证点 证据
路由与能力位 广播 session_side_task;返回 201,含 parentSessionIddisplayName,无 forkedFromsourcePersisted=true
持久化边界 侧边任务 JSONL:record[0] = system/session_source {sourceType:"side_task", sourceId:<父会话>},4 条继承记录均带 forkedFrom.sessionId=<父会话>
创建回放中隐藏 201 响应体中不含父会话标记
持久化转录分页中隐藏 GET /session/:id/transcript 无父会话标记 —— 对照组:同一路由在父会话上确实返回该标记
边界具有强制性 未携带隐藏参数的普通 loadSession 依然不会回放父会话内容
上下文丢失 在侧边任务中发送提问后,模型请求的 messages[]确实包含父会话标记
非空洞断言 侧边任务自身轮次结束后,同一路由会返回它自己的用户消息,父会话历史仍不可见
会话列表 sourceType=default → 含父会话、不含侧边任务;sourceType=side_task → 含侧边任务

真实界面中,/fork sider 会触发 201 POST /session/:id/side-task,在右侧拓展区以页签打开侧边任务且聊天区为空,左侧会话列表仍只显示父会话(见上方英文部分截图)。子智能体详情页签在真实后端下同样正常(真实后台子智能体、真实转录、completed 标记)。

本地质量门(针对本 PR 的 67 个文件):web-shell 150 文件 / 2459 用例、core 182、cli 1274(4 个定向文件)、acp-bridge 459、sdk 300、webui 249 全部通过;eslint --max-warnings 0prettier --checktsc --noEmit 均为 0;新增的 20 个 i18n key 中英文一一对应。

已排除的风险:我专门验证了新增的 sourceType: 'default' 侧栏过滤是否会隐藏历史会话。在重启后的 daemon 上(走持久化路径、无存活会话),?sourceType=default 仍会返回没有来源元数据的会话,因此旧会话 / CLI / 渠道创建的会话不会消失。

⛔ 阻塞项(CI 已红,本地复现,修复已验证)

A. telemetry-catalog.test.ts 路由漂移守卫:新增的 POST /session/:id/side-task 未登记进 telemetry 目录,expected … length of 50 but got 51;该用例在 main 上通过。修复(本地验证通过):在 packages/cli/src/serve/server/telemetry.ts/branch/fork 条目旁补一条 { method: 'POST', path: '/session/:id/side-task', attribution: 'handler_resolved', route: 'POST /session/:id/side-task' },并把守卫的 toHaveLength(50) 改为 51

B. data-testid="chat-pane" 重复App.tsx:8713 在容器上新增了该 testid,而 ChatPane.tsx:633 本就有,导致分屏下匹配到 3 个节点(期望 2),web-shell.split-persist.spec.ts 失败。仅删除该属性后两个用例即通过。建议容器改用其他 id(如 chat-pane-container),而不是改测试。

C. 侧栏 sourceType 过滤硬依赖 session_source_metadata 能力位WebShellSidebar.tsx / WorkspaceSection.tsx 新增的查询会让 SDK 执行 requireCapability('session_source_metadata');若 daemon 未广播该能力,整个列表请求会抛错,侧栏显示 “Failed to load sessions. Click to retry.” —— 这正是 web-shell.collapsed-groups-persist.spec.ts 失败的原因(给场景加上该能力位后即通过)。当前 daemon 都会广播(since: 'v1'),但对旧版本或嵌入式 daemon 这是硬失败而非降级。建议参照 SESSION_ORGANIZATION_FEATURE 的做法做能力位判断,缺失时不下发 sourceType

同机 A/B:--grep @smokemain 23 通过PR head 21 通过 / 2 失败(与 GitHub CI 完全一致)。

🐛 功能缺陷:同一个子智能体被重复列出且状态矛盾

在真实场景下(模型启动后台子智能体并成功结束),新的 Subagents 区块会把它显示两次:一条来自转录中的工具卡片(Running),一条来自 daemon 实时任务快照(Completed)。两次独立的 daemon 运行均可复现,刷新会话后依然存在(见上方英文部分截图)。

根因 —— getEnvironmentAgentTasks(App.tsx)用三个 key 去重,而真实 daemon 下三个都匹配不上:

  • task.toolUseId === tool.callId已完成智能体的 /tasks 快照中没有 toolUseId(meta sidecar 也未持久化),完成后永远匹配不上;
  • task.id === taskId:该映射来自同时带 taskIdtoolUseId 的 system 消息,而真实完成记录(user/notification)只带 backgroundTask.taskId
  • task.id === ${subagentType}-${tool.callId}:daemon 的 id 是 general-purpose-8c7ed91f,后缀是内部 agent id,不是工具调用 id。

daemon 本身是能关联的:GET /session/:id/subagents/<toolCallId> 对该工具调用返回 {taskId: "general-purpose-8c7ed91f", status: "completed"}。这正是 #7561 使用的接口,而本 PR 删除了它在该用途上的最后一个调用方(useMessages.ts −233 行、useMessages.test.ts −430 行);会话恢复后页面不再发起任何 /subagents/ 请求。建议重新走该接口,或以 description/prompt 兜底匹配,或让 daemon 在已完成的 agent 任务上保留 toolUseId

🧪 变异测试发现的覆盖缺口(3 被杀 / 2 存活)

  • selectVisibleHistoryRecords 的「持久化边界具有强制性」规则(acpAgent.ts):把 if (sourceBoundary >= 0) 改成 if (sourceBoundary >= 0 && hideInheritedHistory) 后,336/336 acpAgent 用例仍全绿,而我的 E2E 立即失败——普通重新加载会把父会话转录泄漏进侧边任务。这是本 PR 最值得补的测试。
  • bridge 在途恢复合并 key 中的 hideInheritedHistory:移除后 459/459 bridge 用例仍全绿

(被杀死、即确有测试保护的:转录读取器的边界切片、来源 fork 丢弃 custom_titlevirtual-subagent-sessions.ts 中为 /fork 增加的 task-id 匹配。)

🔍 小问题

  • 三个新增 i18n key 在中英文里都未被使用:environment.branchenvironment.historyenvironment.changeCount。分支行只渲染值、没有标签(非 git 工作区下只显示 “Unavailable”),environment.branch 看起来正是本该用在那里的标签。

建议的合入路径

  1. 修复 ABC(三项都对应当前 CI 红灯,改动都很小且已验证)。
  2. 修复 D,或立刻作为紧随其后的跟进项——用户启动第一个后台子智能体就会看到。
  3. E(边界强制性)与 F(合并 key)补测试,尤其是 E:一行改动即可造成泄漏,而目前 CI 抓不到。

核心设计——session_source 边界记录、转录读取器的切片、daemon 路由、SDK 方法,以及「保留上下文但不展示继承历史」的行为——在真实端到端使用下都站得住。

@qwen-code-dev-bot

qwen-code-dev-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 16 finishedview run. See this round's report below.

中文说明

AutoFix 第 16 轮已完成 —— 查看运行。本轮报告见下方。

Comment thread packages/web-shell/client/App.tsx Outdated
Comment on lines +6651 to +6654
if (directive.toLowerCase() === 'sider') {
createSideTask();
return 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.

[Critical] /fork sider discards the createSideTask() return value and unconditionally returns true, giving the user no feedback when side tasks are unavailable (e.g., the daemon doesn't advertise session_side_task, or workspaceCwd is undefined). Compare with the regular /fork path which pushes a toast on failure. — Failure scenario: user types /fork sider → command reports handled but nothing visible happens, no toast, no panel, no error.

Suggested change
if (directive.toLowerCase() === 'sider') {
createSideTask();
return true;
}
if (directive.toLowerCase() === 'sider') {
if (!createSideTask()) {
pushToast('warning', t('sideTask.unavailable'));
}
return true;
}
中文说明

/fork sider 丢弃了 createSideTask() 的返回值,无论侧边任务是否可用都无条件返回 true。当 daemon 不支持 session_side_taskworkspaceCwd 未定义时,用户执行该命令后不会收到任何反馈(无提示、无面板、无报错),而普通 /fork 路径在失败时会推送 toast 提示。

— qwen3.7-max via Qwen Code /review

Comment on lines +1846 to +1849
return workspace.client
.listWorkspaceSessions(summary.workspaceCwd, { pageSize: 200 })
.then((sessions) => {
if (worktreeSessionIdRef.current !== sid) 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] listWorkspaceSessions fires even when the outer worktreeSessionIdRef check has already determined the session is stale, causing an unnecessary network call for an irrelevant workspace. — Failure scenario: rapid session switching sends read-only API calls (up to 200 records each) for sessions the user has already navigated away from.

Suggested change
return workspace.client
.listWorkspaceSessions(summary.workspaceCwd, { pageSize: 200 })
.then((sessions) => {
if (worktreeSessionIdRef.current !== sid) return;
if (worktreeSessionIdRef.current !== sid) return;
return workspace.client
.listWorkspaceSessions(summary.workspaceCwd, { pageSize: 200 })
.then((sessions) => {
if (worktreeSessionIdRef.current !== sid) return;
中文说明

即使外层 worktreeSessionIdRef 检查已判定当前会话已过时,listWorkspaceSessions 仍会发出请求,导致对无关工作区的不必要网络调用。建议在外层判定为过时时直接 return,避免无效请求。

— qwen3.7-max via Qwen Code /review

Comment on lines +3 to +5
export function MonitorIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

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] MonitorIcon is exported but never imported anywhere in the codebase or this PR's diff. The old TabMonitorIcon was removed from ArtifactPanel, but the replacement uses SquareActivityIcon from lucide-react, not this component. — Failure scenario: orphan file carried in the bundle, read by future developers looking for where the monitor icon lives.

Suggested change
export function MonitorIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
// Consider deleting this file — it is unused.
// If intended as a replacement, import and use it
// where the monitor tab icon is rendered.
中文说明

MonitorIcon 被导出但在整个代码库中未被导入。旧的 TabMonitorIcon 已从 ArtifactPanel 中移除,但替换使用的是 lucide-react 的 SquareActivityIcon,而非此组件。建议删除此无用文件。

— qwen3.7-max via Qwen Code /review

Comment on lines +183 to +185
const handleRightPanelOpen = (request: TurnOutputOpenRequest) => {
if (request.kind === 'subagent') {
onRightPanelOpen?.({

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] handleRightPanelOpen is a plain inline function recreated every render, but it is passed as onTurnOutputOpen to MessageList, which is wrapped in React.memo. During active subagent transcript streaming, each new message triggers a re-render that produces a new reference, defeating memo's shallow prop comparison and forcing a full re-render of the MessageList subtree. — Concrete cost: every streaming message causes unnecessary virtualized-row recalculation.

Suggested change
const handleRightPanelOpen = (request: TurnOutputOpenRequest) => {
if (request.kind === 'subagent') {
onRightPanelOpen?.({
const handleRightPanelOpen = useCallback(
(request: TurnOutputOpenRequest) => {
if (request.kind === 'subagent') {
onRightPanelOpen?.({
中文说明

handleRightPanelOpen 是一个每次渲染都会重新创建的普通内联函数,但作为 onTurnOutputOpen 传递给了被 React.memo 包裹的 MessageList。在子智能体消息流式传输期间,每条新消息都会导致 MessageList 完整重新渲染,建议用 useCallback 包裹以保持引用稳定。

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Could not address the latest feedback automatically (round 1/100). A human should take over this PR.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:

Review feedback addressed

Same-run verification repair

telemetry.test.ts route count assertions — Fixed. The previous commit added POST /session/:id/side-task to legacySessionTelemetryRoutes in telemetry.ts and updated the drift guard in telemetry-catalog.test.ts (50→51), but missed the sibling catalog audit test in telemetry.test.ts which independently asserts the total route count, uniqueness, and attribution split. Updated all four assertions: total 50→51, unique 50→51, handler_resolved 43→44 (the new route uses handler_resolved attribution), and the test name to match.

No inline review comments were present in this round.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/cli/src/serve/server/telemetry.test.ts — 50 passed
  • vitest packages/cli/src/serve/server/telemetry-catalog.test.ts — 1 passed
中文说明

已处理的评审反馈

同轮验证修复

telemetry.test.ts 路由计数断言 — 已修复。上一个提交在 telemetry.tslegacySessionTelemetryRoutes 中添加了 POST /session/:id/side-task,并更新了 telemetry-catalog.test.ts 中的漂移守卫(50→51),但遗漏了 telemetry.test.ts 中独立断言路由总数、唯一性和归因拆分的同级目录审计测试。已更新全部四个断言:总数 50→51、唯一数 50→51、handler_resolved 43→44(新路

Why it was not pushed:

tests failed in packages/web-shell

)�[22m�[32m 14�[2mms�[22m�[39m
 �[32m✓�[39m utils/sessionPreparation.test.ts �[2m(�[22m�[2m16 tests�[22m�[2m)�[22m�[32m 131�[2mms�[22m�[39m
 �[32m✓�[39m constants/localCommands.test.ts �[2m(�[22m�[2m10 tests�[22m�[2m)�[22m�[32m 8�[2mms�[22m�[39m
 �[32m✓�[39m components/messages/SettingsMessage.test.ts �[2m(�[22m�[2m10 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m hooks/useComposerCore.test.ts �[2m(�[22m�[2m16 tests�[22m�[2m)�[22m�[32m 7�[2mms�[22m�[39m
 �[32m✓�[39m components/MessageList.test.ts �[2m(�[22m�[2m95 tests�[22m�[2m)�[22m�[32m 45�[2mms�[22m�[39m
 �[32m✓�[39m components/artifacts/TurnOutputs.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 4�[2mms�[22m�[39m
 �[32m✓�[39m utils/sessionErrors.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 3�[2mms�[22m�[39m
 �[32m✓�[39m components/GitModePopover.test.tsx �[2m(�[22m�[2m16 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m components/artifacts/SubagentDetail.test.tsx �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 3�[2mms�[22m�[39m

�[31m⎯⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Tests 1 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m

�[41m�[1m FAIL �[22m�[49m components/sidebar/WebShellSidebar.workspace-removal.test.tsx�[2m > �[22mWebShellSidebar workspace removal�[2m > �[22mscopes pinned and archived sessions to a locked secondary workspace
�[31m�[1mAssertionError�[22m: expected false to be true // Object.is equality�[39m

�[32m- Expected�[39m
�[31m+ Received�[39m

�[32m- true�[39m
�[31m+ false�[39m

�[36m �[2m❯�[22m components/sidebar/WebShellSidebar.workspace-removal.test.tsx:�[2m595:7�[22m�[39m
    �[90m593| �[39m        ([options]) �[33m=>�[39m options�[33m?.�[39msourceType �[33m===�[39m �[32m'default'�[39m�[33m,�[39m
    �[90m594| �[39m      )�[33m,�[39m
    �[90m595| �[39m    )�[33m.�[39m�[34mtoBe�[39m(�[35mtrue�[39m)�[33m;�[39m
    �[90m   | �[39m      �[31m^�[39m
    �[90m596| �[39m    �[34mexpect�[39m(
    �[90m597| �[39m      listSecondarySessions�[33m.�[39mmock�[33m.�[39mcalls�[33m.�[39m�[34mevery�[39m(

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m


�[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m85 passed�[39m�[22m�[90m (86)�[39m
�[2m      Tests �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m1768 passed�[39m�[22m�[90m (1769)�[39m
�[2m   Start at �[22m 15:49:18
�[2m   Duration �[22m 55.65s�[2m (transform 6.12s, setup 805ms, collect 39.69s, tests 60.82s, environment 32.84s, prepare 7.39s)�[22m

JUNIT report written to /home/runner/work/qwen-code/qwen-code/packages/web-shell/junit.xml
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /home/runner/work/qwen-code/qwen-code/packages/web-shell
npm error workspace @qwen-code/web-shell@0.21.0
npm error location /home/runner/work/qwen-code/qwen-code/packages/web-shell
npm error command failed
npm error command sh -c vitest run --config vitest.config.ts --changed origin/main --passWithNoTests

Run log: https://github.com/QwenLM/qwen-code/actions/runs/30364965651


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge conflict resolution for PR #7929

Root cause

PR #7731 (feat(web-shell): add git branch picker, commit dialog, and create PR flow) landed on main after the PR branch's last merge. It added import { BranchPickerPopover } from '../BranchPickerPopover' to WorkspaceSection.tsx and reordered the surrounding imports. PR #7929 independently expanded the SESSION_LIST_PAGE_SIZE import to also include WEB_SHELL_SESSION_SOURCE_TYPE. Both modifications touched the same import block, producing a single content conflict.

Textual or semantic

Textual only. The two sides modified adjacent import lines with no overlapping logic. The resolution preserves main's import ordering (BranchPickerPopover after GitBranchIndicator, useI18n before session constants) and the PR's expanded named-import block:

import { BranchPickerPopover } from '../BranchPickerPopover';
import { useI18n } from '../../i18n';
import {
  SESSION_LIST_PAGE_SIZE,
  WEB_SHELL_SESSION_SOURCE_TYPE,
} from '../../constants/sessions';

The standalone import { SESSION_LIST_PAGE_SIZE } that main had placed after useI18n was folded into the PR's multi-name import to avoid a duplicate specifier.

What is load-bearing

  • WEB_SHELL_SESSION_SOURCE_TYPE must remain imported — it is consumed by loadSessions() as sourceType: WEB_SHELL_SESSION_SOURCE_TYPE in the listWorkspaceSessions call.
  • BranchPickerPopover must remain imported — it is used in the JSX for the git branch picker popover (already present in the non-conflicted portion of the file from the previous merge).

What I could not verify

No build, typecheck, or tests were run. The auto-merged files (SDK types, DaemonClient, App.tsx, App.test.tsx, WebShellSidebar.tsx, i18n.tsx) resolved cleanly by git and were not inspected beyond confirming the merge succeeded. If any of those auto-merged files have semantic conflicts (e.g., the SDK type additions from main colliding with the PR's own SDK extensions), the PR's own CI will surface it.

中文说明

PR #7929 合并冲突解决

根因

PR #7731feat(web-shell): add git branch picker, commit dialog, and create PR flow)在 PR 分支上次合并后合入了 main。它在 WorkspaceSection.tsx 中添加了 BranchPickerPopover 的导入并重新排序了周围的 import。PR #7929 则独立地将 SESSION_LIST_PAGE_SIZE 的导入扩展为同时包含 WEB_SHELL_SESSION_SOURCE_TYPE。两侧修改触及同一 import 区块,产生了一个内容冲突。

文本冲突还是语义冲突

纯文本冲突。两侧修改的是相邻的 import 行,没有逻辑重叠。解决方案保留了 main 的 import 排序(BranchPickerPopoverGitBranchIndicator 之后,useI18n 在 session 常量之前)以及 PR 的扩展命名导入:

import { BranchPickerPopover } from '../BranchPickerPopover';
import { useI18n } from '../../i18n';
import {
  SESSION_LIST_PAGE_SIZE,
  WEB_SHELL_SESSION_SOURCE_TYPE,
} from '../../constants/sessions';

main 原本在 useI18n 之后单独导入的 SESSION_LIST_PAGE_SIZE 已合并到 PR 的多名称导入中,避免重复。

关键依赖

  • WEB_SHELL_SESSION_SOURCE_TYPE 必须保持导入——它在 loadSessions() 中作为 sourceType 参数使用。
  • BranchPickerPopover 必须保持导入——它在 JSX 中用于 git 分支选择器弹窗。

未能验证的内容

未运行构建、类型检查或测试。git 自动合并的文件(SDK 类型、DaemonClientApp.tsxApp.test.tsxWebShellSidebar.tsxi18n.tsx)仅确认合并成功,未做进一步检查。如果这些自动合并文件存在语义冲突,PR 自身的 CI 会暴露问题。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. Run review failed. See workflow logs for details. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

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

Reviewed. Suggestions are inline. Unresolved, please confirm: [Critical] packages/web-shell/client/App.tsx (existing comment 3677182364): task polling (getTaskActivityKey(messages) -> useBackgroundTasks) is still NOT gated on environment-panel section visibility — the requested gate was never implemented (the dev-bot escalated rather than landing it). Whether this is merge-blocking is genuinely contested: the same getTasks() snapshot also feeds the non-agent footer status pills (a legitimate consumer independent of the panel), and the maintainer who filed it did not include it in their final block-the-merge list (rounds 2-3). Could not rule definitively whether it blocks. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; it is the job that exercises this PR's new session_side_task serve route.

中文说明

已审查。 建议见行内评论。 未决,请确认:[Critical] packages/web-shell/client/App.tsx (existing comment 3677182364): task polling (getTaskActivityKey(messages) -> useBackgroundTasks) is still NOT gated on environment-panel section visibility — the requested gate was never implemented (the dev-bot escalated rather than landing it). Whether this is merge-blocking is genuinely contested: the same getTasks() snapshot also feeds the non-agent footer status pills (a legitimate consumer independent of the panel), and the maintainer who filed it did not include it in their final block-the-merge list (rounds 2-3). Could not rule definitively whether it blocks. 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; it is the job that exercises this PR's new session_side_task serve route。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +24 to +25
The artifact-panel action remains available when no tab exists. Opening an
The empty panel shows Review and, when session-source metadata is supported,

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 design-doc sentence is truncated: "...when no tab exists. Opening an The empty panel shows Review..." — the clause "Opening an" has no object and runs directly into the next sentence. — Failure scenario: a reader following the Header section hits an unparseable fragment; the description of what opening the panel does is silently lost.

Suggested change
The artifact-panel action remains available when no tab exists. Opening an
The empty panel shows Review and, when session-source metadata is supported,
The artifact-panel action remains available when no tab exists. The empty panel shows Review and, when session-source metadata is supported,
中文说明

设计文档中的句子被截断:"Opening an" 没有宾语并直接接入下一句,导致预期行为描述丢失。请补全或删除该悬空子句。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +99 to +102
.contextBodyWithEnvironmentPanel [data-web-shell-message-list] {
box-sizing: border-box;
width: calc(100% + 332px);
padding-right: 356px;

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 message-list reservation hardcodes calc(100% + 332px) / padding-right: 356px, derived from the docked EnvironmentPanel's 320px width that lives in a different CSS module (EnvironmentPanel.module.css) with no shared variable — and the two offsets don't even use a consistent gap (332 = 320+12, 356 = 320+36). — Failure scenario: if the docked panel width in EnvironmentPanel.module.css changes, this rule does not track it and the message list silently misaligns (overlap or gutter) with no build/test signal, because the coupling is purely visual.

Suggested change
.contextBodyWithEnvironmentPanel [data-web-shell-message-list] {
box-sizing: border-box;
width: calc(100% + 332px);
padding-right: 356px;
.contextBodyWithEnvironmentPanel [data-web-shell-message-list] {
box-sizing: border-box;
width: calc(100% + var(--web-shell-environment-panel-width, 320px) + 12px);
padding-right: calc(var(--web-shell-environment-panel-width, 320px) + 36px);
中文说明

消息列表预留宽度硬编码 332px/356px,源自另一个 CSS 模块中 docked 面板的 320px 宽度,两者无共享变量(且间隙不一致:332=320+12,356=320+36)。若面板宽度变更,此处不会同步,导致静默错位且无构建/测试信号。建议定义共享的 --web-shell-environment-panel-width 自定义属性(与现有 --web-shell-* 约定一致)。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +4383 to +4384
const originalResizeObserver = globalThis.ResizeObserver;
globalThis.ResizeObserver = class {

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] Three tests replace globalThis.ResizeObserver with a manual mock and restore it only on the last line of the test body; the afterEach (vi.restoreAllMocks()) does not restore a direct global assignment. — Failure scenario: if an assertion fails before the final restore line, every later test in the file runs with the no-op ResizeObserver, so resize-dependent tests pass vacuously and the suite becomes order-dependent and hard to diagnose.

Suggested change
const originalResizeObserver = globalThis.ResizeObserver;
globalThis.ResizeObserver = class {
// save/restore centrally instead of per-test:
// beforeEach(() => { savedResizeObserver = globalThis.ResizeObserver; });
// afterEach(() => { globalThis.ResizeObserver = savedResizeObserver; });
const originalResizeObserver = globalThis.ResizeObserver;
globalThis.ResizeObserver = class {
中文说明

三个测试用手动 mock 替换 globalThis.ResizeObserver,仅在测试体最后一行恢复;afterEach(vi.restoreAllMocks)不会恢复直接的全局赋值。若断言在恢复前失败,文件内后续测试都会使用空 ResizeObserver,导致依赖 resize 的测试空通过、套件顺序相关且难以诊断。建议在 beforeEach/afterEach 中集中保存/恢复。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +908 to +910
export function ShellTaskDetail({
task,
actions: providedActions,

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 cancel flow + prop-sync race-guard + error-reset logic is duplicated verbatim between MonitorTaskDetail and the new ShellTaskDetail (differing only in the kind literal, the type guard, and log strings), and the two copies have already drifted — MonitorTaskDetail.handleCancel calls setActionError(null) after setCurrentTask while ShellTaskDetail does not (benign today, since the error is cleared at the top of the callback). — Failure scenario: any future fix to the cancel race-guard, the optimistic endTime: Date.now() update, or the getTasks() refresh-and-reconcile step must be applied in two places by hand; landing it in one but not the other silently leaves the sibling component with the old behavior.

Suggested change
export function ShellTaskDetail({
task,
actions: providedActions,
export function ShellTaskDetail({
task,
actions: providedActions,
// extract the shared cancel/race-guard/error-reset logic into a
// useTaskCancel(actions, currentTask, setCurrentTask, kind) hook
// consumed by both MonitorTaskDetail and ShellTaskDetail
中文说明

取消流程 + 属性同步竞态保护 + 错误重置逻辑在 MonitorTaskDetail 与新增 ShellTaskDetail 间逐字重复(仅 kind 字面量、类型保护、日志字符串不同),且两份已出现漂移——MonitorTaskDetail.handleCancel 在 setCurrentTask 后调用 setActionError(null),ShellTaskDetail 没有(目前无害,因回调开头已清空错误)。未来对竞态保护、乐观 endTime 更新或 getTasks 刷新的修复需手工改两处,漏改其一会让兄弟组件静默保留旧行为。建议抽取按 kind 参数化的共享 hook。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines 3026 to 3027
const closeArtifactPanel = useCallback(() => {
setArtifactPanelOpen(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] No test verifies that artifact-panel tabs survive a close/reopen cycle — the central behavioral change in closeArtifactPanel (the old version eagerly cleared artifactPanelTabs, reviewChanges, selectedReviewPath, extraArtifacts, and paneArtifactSnapshots; the new one preserves them per the design doc). — Failure scenario: if a future change re-introduces eager cleanup, or the per-session state bucket incorrectly discards tabs on close, no test catches it. Concrete trigger: open a Review tab → close the panel via the toggle → reopen — the Review tab should still be active; nothing in App.test.tsx exercises this sequence.

Suggested change
const closeArtifactPanel = useCallback(() => {
setArtifactPanelOpen(false);
const closeArtifactPanel = useCallback(() => {
setArtifactPanelOpen(false);
// add an App.test.tsx test: open a review tab, close via toggle,
// reopen, and assert the tab is still present and active
中文说明

没有测试验证 artifact panel 标签页在关闭/重开后仍然保留——这是 closeArtifactPanel 的核心行为变更(旧版会清空标签页/评审变更/选中路径/额外产物/快照,新版按设计文档保留)。若未来重新引入清空逻辑,或按会话状态桶在关闭时错误丢弃标签页,都没有测试能捕获。具体触发:打开 Review 标签页→用切换按钮关闭面板→重开,Review 标签页应仍处于激活状态;App.test.tsx 中没有覆盖此序列。

— qwen3.8-max-preview via Qwen Code /review

items: [],
loaded: false,
});
const optimisticSideTaskIdsRef = useRef(new Set<string>());

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] optimisticSideTaskIdsRef is added to on side-task creation but only pruned when the same parent session's listing echoes an id back; it is never cleared when the parent session changes, so ids accumulate for the life of the mount. — Failure scenario: create a side task under parent A, then switch to parent B before A's listing resolves (or while the panel is closed, where the listing effect returns early) — A's id is never removed; repeating across many parents grows the Set without bound. This is not a correctness bug (mergeSideTaskCatalog replaces the catalog wholesale on parent change and session ids are unique), so the cost is negligible memory growth plus a collection whose size no longer reflects live state.

Suggested change
const optimisticSideTaskIdsRef = useRef(new Set<string>());
const optimisticSideTaskIdsRef = useRef(new Set<string>());
// clear it when the parent session changes (e.g. in the listing
// effect's reset branch) so it stays scoped to the current parent
中文说明

optimisticSideTaskIdsRef 在创建侧边任务时加入,但仅在相同父会话的列表回显该 id 时才移除;父会话切换时从不清空,因此 id 会在整个挂载周期内累积。触发:在父会话 A 下创建侧边任务,在 A 的列表返回前切到 B(或在面板关闭、列表 effect 提前返回时),A 的 id 永不移除;跨多个父会话重复会使该 Set 无限增长。这不是正确性 bug(mergeSideTaskCatalog 在父会话切换时整体替换目录、会话 id 唯一),代价是可忽略的内存增长。建议在父会话变化时清空该集合。

— qwen3.8-max-preview via Qwen Code /review

Comment on lines 312 to 314
'session_source_metadata',
'session_side_task',
'session_prompt',

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] The only test asserting the new session_side_task route appears in the qwen serve capabilities envelope lives in integration-tests/, outside every npm workspace, so no workspace npm test (including the one run in this review) ever collects it — and the Integration Tests (CLI, No Sandbox) CI job that runs it was skipped for this PR. — Failure scenario: a regression that drops or renames session_side_task from the envelope would ship with every unit suite green and the integration job skipped, so nothing fails.

Suggested change
'session_source_metadata',
'session_side_task',
'session_prompt',
'session_source_metadata',
'session_side_task',
'session_prompt',
// confirm the Integration Tests (CLI, No Sandbox) CI check actually
// ran (was not skipped) and passed; locally this file runs via
// npm run test:integration:cli:sandbox:none
中文说明

唯一断言新 session_side_task 路由出现在 qwen serve 能力信封中的测试位于 integration-tests/,不在任何 npm workspace 内,因此没有 workspace 的 npm test 会收集它;而运行它的 "Integration Tests (CLI, No Sandbox)" CI 任务在本 PR 被跳过。若回归从信封中删除/重命名 session_side_task,所有单测仍绿、集成任务被跳过,无任何失败。建议确认集成 CI 确实运行并通过;本地可用 npm run test:integration:cli:sandbox:none 执行该文件。

— qwen3.8-max-preview via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Round summary — no new changes needed

All actionable findings from this round were already addressed in commit e14ce42 (fix(web-shell): preserve contextual panel fallbacks), which landed after the reviewed head (5fb9583).

Critical: renderChatHeader hosts lose Git chip and task pills

Status: already fixed (e14ce42).

environmentPanelReachable now includes (!renderChatHeader || Boolean(header)), so hosts that pass only a custom renderChatHeader (no header prop) keep the legacy composer Git chip and footer task pills. The environment panel replacement is only active when the built-in header toggle is actually rendered.

Two regression tests verify this:

  • keeps the git branch action for a custom header without explicit header configuration — asserts gitBranch remains in visibleToolbarActions
  • keeps legacy task status for a custom header without explicit header configuration — asserts StatusBar receives the full task list

Critical: task polling not gated on environment-panel visibility

Status: needs a maintainer's decision — left unresolved.

Whether getTaskActivityKey(messages) → useBackgroundTasks polling should be gated on environment-panel section visibility is genuinely contested. The same getTasks() snapshot also feeds the non-agent footer status pills (a legitimate consumer independent of the panel), and the maintainer who originally filed it did not include it in their final block-the-merge list (rounds 2–3). This is a product/scope tradeoff that is not mine to settle: gating the poll saves a network call when the panel is hidden, but it also removes the data source for the footer pills unless a second consumer is added. Leaving this thread open for an explicit maintainer ruling.

Suggestion rc:3679479611: SideTaskPanel auto-create re-fires on prop change

Status: already fixed (e14ce42).

A didAttemptCreateRef guard now prevents the auto-create effect from re-firing when create gets a new identity (e.g. language switch or title update). The Retry button remains the only path for subsequent attempts. A regression test (does not retry creation after a prop change until the user requests it) verifies the exact failure scenario described in the suggestion.

Deferred non-Critical feedback

Not touched per critical-only mode (5 change-producing rounds completed).

Verification

  • vitest run client/App.test.tsx (packages/web-shell) — 283 passed
  • vitest run client/components/artifacts/SideTaskPanel.test.tsx (packages/web-shell) — 13 passed
  • vitest run client/components/panels/EnvironmentPanel.test.tsx (packages/web-shell) — 14 passed
中文说明

轮次总结 — 无需新改动

本轮所有可操作的发现已在提交 e14ce42fix(web-shell): preserve contextual panel fallbacks)中解决,该提交在被审查的 head(5fb95833)之后合入。

Critical:renderChatHeader 宿主丢失 Git 分支标签和任务状态

状态:已修复(e14ce42f6)。

environmentPanelReachable 现在包含 (!renderChatHeader || Boolean(header)) 条件,因此仅传入自定义 renderChatHeader(无 header prop)的宿主会保留原有的编辑器 Git 分支标签和页脚任务状态。环境面板的替代行为仅在内置头部切换按钮实际渲染时才生效。

两个回归测试验证了此行为:

  • keeps the git branch action for a custom header without explicit header configuration — 断言 gitBranch 保留在 visibleToolbarActions
  • keeps legacy task status for a custom header without explicit header configuration — 断言 StatusBar 收到完整的任务列表

Critical:任务轮询未受环境面板可见性控制

状态:需要维护者决定 — 保持未解决。

getTaskActivityKey(messages) → useBackgroundTasks 轮询是否应受环境面板区域可见性控制存在争议。同一个 getTasks() 快照也为非代理页脚状态标签提供数据(这是独立于面板的合法消费者),且最初提出此问题的维护者未将其纳入最终的合并阻止列表(第 2-3 轮)。这是一个产品/范围权衡,不由我决定:限制轮询可以在面板隐藏时节省网络调用,但也会移除页脚状态标签的数据源,除非添加第二个消费者。保持此线程开放,等待维护者明确裁定。

Suggestion rc:3679479611:SideTaskPanel 自动创建在 prop 变更时重复触发

状态:已修复(e14ce42f6)。

didAttemptCreateRef 守卫现在防止自动创建效果在 create 获得新标识时(例如语言切换或标题更新)重复触发。重试按钮仍然是后续尝试的唯一路径。回归测试(does not retry creation after a prop change until the user requests it)验证了建议中描述的确切失败场景。

延迟的非 Critical 反馈

按仅处理 Critical 模式(已完成 5 个产生改动的轮次)不予处理。

验证

  • vitest run client/App.test.tsx(packages/web-shell)— 283 通过
  • vitest run client/components/artifacts/SideTaskPanel.test.tsx(packages/web-shell)— 13 通过
  • vitest run client/components/panels/EnvironmentPanel.test.tsx(packages/web-shell)— 14 通过

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf.

中文说明

完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

…t-panels

# Conflicts:
#	packages/web-shell/client/App.test.tsx
qwen-code-ci-bot pushed a commit that referenced this pull request Jul 30, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No actionable feedback this round. All review sections (reviews, inline comments, issue-level comments, failed checks, still-red checks) are empty. Critical-only mode is active after 5 change-producing rounds, and no Critical findings were raised. No changes made.

中文说明

本轮没有可处理的反馈。所有审查部分(审查、行内评论、Issue 级评论、失败的检查、持续失败的检查)均为空。经过 5 个产生改动的轮次后,已进入仅处理 Critical 的模式,且没有提出任何 Critical 发现。未做任何更改。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf.

中文说明

完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has no review of its own on b00fe1c79e253923e6c89eee2e179a5be7ef6f5a. If this re-run was meant to approve, it did not — an approval left by another account is a separate vote and does not count as the bot's own.

The stage comments above were updated with the latest result. View workflow run.

@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Maintainer local verification — PR #7929 round 4 (head b00fe1c7)

Re-verification of my round-3 report (head 5fb9583).
Delta since R3 is one PR-authored commite14ce42f fix(web-shell): preserve contextual panel fallbacks — plus a merge of origin/main (37 upstream commits, no PR code).

Built and run in an isolated worktree at b00fe1c7: npm run build && npm run bundle && npm run build -w packages/web-shell (exit 0), then a real qwen serve daemon over a real git workspace driven by a fake model that emits genuine agent / run_shell_command is_background / monitor tool calls, reached through a reviewer host harness that mounts the public WebShellWithProviders entry and differs from the shipped page only in the chat-header props.

Previous findings → status at b00fe1c7

# R3 finding Status Evidence
1 [blocker] chatHeaderEnabled OR-ed the legacy renderChatHeader prop, so every pre-#7929 embedding silently lost the composer git chip and the footer task pills with no replacement Fixed by e14ce42f mutation A/B + live A/B below
2 [minor] getEnvironmentAgentTasks dedupes live agents by description alone ⚠️ Still open re-reproduced at R4
3 [coverage] mergeSideTaskCatalog loaded: true mutant survives ⚠️ Still open mutant survives all 293 App.test.tsx tests
4 [contract] forkSession/branchSession drop the side-task boundary ⏸️ Untouched by R4; not re-tested this round (unchanged code, no UI path — as reported at R3)
5 [pre-existing] first-prompt rename treats a permanent 401 as transient ⚠️ Still open, now with a stronger oracle live daemon below

packages/web-shell full suite at this head: 156 files / 2588 tests, all green.


1. The R3 blocker is genuinely fixed, and the guard is load-bearing

e14ce42f leaves chatHeaderEnabled alone and instead narrows the replacement gate (App.tsx:1602):

const environmentPanelReachable =
  chatHeaderEnabled &&
  environmentHeaderItemVisible &&
  (!renderChatHeader || Boolean(header));   // ← new

Reverting only that clause (single-variable mutant, everything else identical):

App.test.tsx result
head b00fe1c7 293 passed
clause reverted 2 failed, 291 passed

and the two failures are exactly the two tests the commit adds:

× lets a custom renderer replace the complete persistent chat header
  → expected [ 'approvalMode', 'model', …(3) ] to include 'gitBranch'
× keeps legacy task status for a custom header without explicit header configuration
  → expected [] to deeply equal [ { kind: 'monitor', …(6) } ]

That also quantifies the R3 coverage gap that let the regression through: 291 of 293 tests pass with and without the fix — the new pair is what makes the behaviour observable at all.

Live confirmation on the real daemon (legacy host = renderChatHeader set, header omitted, background tasks actually running):

head b00fe1c7 clause reverted
composer git chip aria-label Current Git branch: main — 1 modified (absent)
footer task pill text 1 shell, 1 monitor (absent)
head b00fe1c7 — chip preservedclause reverted — chip gone

One residual, by design as far as I can tell, worth a doc line: a host that sets both header (whose items include environment — note header: {} defaults to all three) and renderChatHeader still gets the replacement enabled, so its renderer must call onEnvironmentPanelOpenChange or the panel is unreachable. A genuinely legacy host never passes header, so the regression class is closed; this is only the explicit opt-in path.

2. New R4 behaviour, verified against the live daemon

The commit also turns the environment panel's branch row into a BranchPickerPopover trigger and suppresses the floating panel's outside-dismiss while that picker is open.

Docked panel (1500px): three genuinely running tasks from the real daemon (background subagent + run_shell_command + monitor), and the new branch row opens a picker listing the workspace's real refs — chore/deps-bump, feat/context-panels, fix/side-task-boundary, main — with data-side="left". No page errors.

environment panel + branch picker on a live daemon

Floating panel (1040px, position: absolute, context body 780px < 800+332): the picker content is portaled to <body>, i.e. outside the panel, so a pointerdown in it is exactly what the panel's dismiss listener sees.

panel after clicking inside the picker picker still open
head b00fe1c7 ✅ survives ✅ yes
|| branchPickerOpen reverted ❌ dismissed ❌ closed with it
head b00fe1c7branchPickerOpen guard reverted

The control still holds on both arms: with the picker closed, an outside click does dismiss the floating panel.

SideTaskPanel create-once guard: on the live daemon a side task issues exactly one 201 POST /session/:id/side-task, and removing didAttemptCreateRef is killed by the commit's new test (does not retry creation after a prop change until the user requests it). Worth keeping in mind why this mattered: ArtifactPanel falls back to ignoreSideTaskCreated when a host doesn't wire onSideTaskCreated, so before this commit a title change after a completed create could re-enter create() and mint another daemon session.

3. Still open

[minor] Live-agent dedupe collapses parallel fan-outApp.tsx:1356, unchanged. The trailing reconciliation loop compares each live agent against rows the same loop just pushed, keyed on description, so distinct ids and distinct toolUseIds are not enough. Probed directly against R4's exported getEnvironmentAgentTasks with an empty transcript (live agents with no matching tool_group message — a restored session, or agents spawned outside the loaded window):

input rows
2 live agents, same description, distinct ids + toolUseIds 1
3 live agents, same description 1
3 live agents, distinct descriptions 3 (positive control — names description as the cause)
2 live agents, empty descriptions 2 (control — the !== '' guard is the only escape)

The agent tool documents description as a "3-5 word" summary, so parallel fan-out collides routinely. The suite's lists two precisely-linked agents that share a description once each doesn't cover this — both its agents are linked to tool calls present in messages, so they never reach the trailing loop.

[coverage] mergeSideTaskCatalog loaded: trueApp.tsx:1116. Flipping it to false in the same-parent branch survives all 293 App.test.tsx tests: only the parent-change test asserts the whole object, the three same-parent tests assert items only, and loaded gates sideTasksLoading (App.tsx:2469).

[pre-existing, not a regression] First-prompt rename swallows a permanent 401SideTaskPanel.tsx:49,217-241. On the default loopback daemon (no --token), mutate({ strict: true }) requires a bearer token, so the rename 401s permanently. Live measurement:

  • 401 PATCH /session/:id/metadata × 3 — all three attempts, no backoff, no permanent-vs-transient classification
  • tab label in the UI: Investigate the flaky retry path (set optimistically at SideTaskPanel.tsx:221, before the first attempt)
  • persisted displayName read back from the daemon: "Side task"
  • toasts: none for this failure, even though onSideTaskError={reportError} is wired (App.tsx:10322)

The "no toast" claim is measured, not eyeballed: toasts auto-dismiss, so I recorded every [data-web-shell-toast] that ever mounts via a MutationObserver and compared two arms of the same build — with a side task (3 extra 401s) and without one. Identical toast sets. Route and gate are unchanged from main, so this is not introduced here — but the new retry loop is what turns it into a silent divergence between the tab and the transcript.

side task after its first prompt

4. Mutation matrix on the new code — 4/9 killed

mutant result killed by
side="left""bottom" ✅ killed opens the branch actions without dismissing a floating panel
drop branchPickerOpen from the dismiss guard ✅ killed same test
never auto-close the picker on collapse/hide ✅ killed same test
remove didAttemptCreateRef ✅ killed does not retry creation after a prop change…
environmentPanelReachable legacy clause ✅ killed the 2 tests in §1
render the interactive row without a resolved branch (gitWorkspaceCwd && activeBranchgitWorkspaceCwd) ⚠️ survived nothing asserts the environment.unavailable fallback row
drop onOpenCommit pass-through to the picker ⚠️ survived the test passes onOpenGitCommit but never asserts it arrives
gitWorkspaceCwd={connection.sessionId ? … : undefined} → unconditional ⚠️ survived nothing asserts the row is inert without a session
mergeSideTaskCatalog loaded (carried from R3) ⚠️ survived see §3

The three new survivors are coverage gaps over correct code, not defects — I checked each by reading the branch they leave unexercised. The onOpenCommit one is the one I'd actually add an assertion for: a broken Commit wire from the new panel row would ship green.

5. Harness notes, so the numbers above can be trusted

Two of my own readings were wrong before I chased them down — recording them so nobody re-derives them as PR defects:

  • 500 POST /session/:id/model is my synthetic OPENAI_MODEL=fake-model (Model 'fake-model' not found for authType 'openai'), and the [object Object] error toast that follows it reproduces with no side task at all. Both are harness cascade, not this PR.
  • "the branch picker closes itself" was my harness. A naive button[aria-expanded="false"] sweep to expand the panel sections also matches Radix's PopoverTrigger — so the sweep clicked the branch row and toggled the picker shut. A width × expand matrix (1500/1040 × with/without the sweep) isolated it: the variable was the sweep, not the layout, and locator.click() vs a single page.mouse.click() both keep the picker open for 3.2s+. The product is fine; my selector wasn't.
  • monitor is additionalProperties: false (command/description/max_events/idle_timeout_ms/directory only) — an extra key means no task is created and the panel looks broken when the harness is.

Verdict

Mergeable. The R3 blocker is fixed with a load-bearing guard, proven both by a single-clause mutation A/B and by the real app on a live daemon; the new branch-picker wiring works docked and floating and its dismiss suppression is verified in both directions. Nothing found this round is a release blocker.

Follow-ups I'd file rather than block on:

  1. getEnvironmentAgentTasks — key the dedupe on id/toolUseId, not description (§3).
  2. Assert the onOpenCommit pass-through from the environment panel's branch row (§4).
  3. Classify a 401 from renameSession as permanent: stop after one attempt and don't leave the tab showing an unpersisted title (§3) — pre-existing, but this PR is where it becomes user-visible.
中文版本(点击展开)

维护者本地验证 —— PR #7929 第 4 轮(head b00fe1c7

这是对我第 3 轮报告(head 5fb9583)的再验证。相比 R3,PR 自身只多了一个提交 —— e14ce42f fix(web-shell): preserve contextual panel fallbacks,其余是合并 origin/main(37 个上游提交,不含 PR 代码)。

b00fe1c7 的隔离 worktree 中构建(npm run build && npm run bundle && npm run build -w packages/web-shell,exit 0),然后用真实 qwen serve 守护进程 + 真实 git 工作区,配一个会发出真正 agent / run_shell_command is_background / monitor 工具调用的假模型;页面通过一个 reviewer 宿主 harness 挂载公开入口 WebShellWithProviders,与线上页面只差 chat-header 的 props

上一轮问题 → 在 b00fe1c7 的状态

# R3 问题 状态
1 [阻塞] chatHeaderEnabled 把旧的 renderChatHeader 也 OR 进去,导致所有 #7929 之前的嵌入宿主静默丢失 composer git chip 底栏任务胶囊,且没有替代入口 已修复e14ce42f
2 [次要] getEnvironmentAgentTasks 仅按 description 去重活跃子智能体 ⚠️ 仍存在(已在 R4 重现)
3 [覆盖] mergeSideTaskCatalogloaded: true 变异存活 ⚠️ 仍存在
4 [契约] forkSession/branchSession 丢失 side-task 边界 ⏸️ R4 未触及;本轮未重测(代码未变、无 UI 路径,同 R3 结论)
5 [既有问题] 首条消息重命名把永久性 401 当作可重试错误 ⚠️ 仍存在,本轮有了更强的判据

packages/web-shell 全量:156 文件 / 2588 测试全绿。

1. R3 的阻塞问题确实修好了,且新加的守卫是"承重"的

e14ce42f 没有动 chatHeaderEnabled,而是收窄了替换开关(App.tsx:1602):新增 (!renderChatHeader || Boolean(header))

只回退这一个子句(单变量变异):head 上 App.test.tsx 293 全通过;回退后恰好 2 个失败、291 通过,而这 2 个正是该提交新增的测试(gitBranchvisibleToolbarActions 消失、StatusBartasks 变成 [])。这同时量化了 R3 那个覆盖缺口:293 个里有 291 个在"有修复"和"无修复"两种情况下都通过 —— 新增的这两个测试才让该行为可被观测。

真实守护进程上的验证(legacy 宿主 = 只给 renderChatHeader、不给 header,且有真实运行中的后台任务):

head b00fe1c7 回退该子句
composer git chip 的 aria-label Current Git branch: main — 1 modified (不存在)
底栏任务胶囊文本 1 shell, 1 monitor (不存在)

一个残留项(我判断是设计如此,但建议补一行文档):同时设置 header(其 itemsenvironment,注意 header: {} 默认就是三项全开) renderChatHeader 的宿主,替换逻辑仍会启用,因此它的渲染函数必须自己调用 onEnvironmentPanelOpenChange,否则面板不可达。真正的旧宿主根本不会传 header,所以回归这一类已经关闭;这里只是显式 opt-in 的路径。

2. R4 新行为在真实守护进程上的验证

该提交还把环境面板的分支行改成了 BranchPickerPopover 触发器,并在选择器打开期间抑制浮动面板的"点击外部关闭"。

停靠态(1500px):面板里是三个来自真实守护进程的运行中任务(后台子智能体 + run_shell_command + monitor);新的分支行能打开选择器,并列出工作区真实的引用 —— chore/deps-bumpfeat/context-panelsfix/side-task-boundarymaindata-side="left",无页面错误。

浮动态(1040px,position: absolute,context body 780px < 800+332):选择器内容被 portal 到 <body>,也就是在面板之外,所以在其中按下指针正是面板 dismiss 监听器看到的事件。

在选择器内部点击后的面板 选择器是否仍打开
head b00fe1c7 ✅ 保留 ✅ 是
回退 || branchPickerOpen ❌ 被关闭 ❌ 一起消失

两个臂上对照都成立:当选择器已关闭时,点击外部仍会关闭浮动面板。

SideTaskPanel 只创建一次的守卫:真实守护进程上一个 side task 只发出一次 201 POST /session/:id/side-task;移除 didAttemptCreateRef 会被该提交新增的测试杀死。值得记住它为什么重要:宿主未接 onSideTaskCreatedArtifactPanel 会退回 ignoreSideTaskCreated,所以在这个提交之前,创建成功后的一次标题变更就可能重新进入 create() 并再造一个守护进程会话。

3. 仍然存在的问题

[次要] 活跃子智能体去重会吞掉并行扇出 —— App.tsx:1356,未改动。收尾的对账循环把每个活跃 agent 与同一循环刚刚 push 进去的行比较,且键是 description,所以 idtoolUseId 都不同也没用。直接对 R4 导出的 getEnvironmentAgentTasks 用空 transcript 探测(活跃 agent 没有对应的 tool_group 消息 —— 恢复的会话,或在已加载窗口之外派生的 agent):

输入 行数
2 个同 description、id 与 toolUseId 均不同 1
3 个同 description 1
3 个不同 description 3 (阳性对照 —— 指认 description 就是原因)
2 个空 description 2 (对照 —— !== '' 是唯一逃生口)

agent 工具把 description 文档化为"3-5 个词"的摘要,所以并行扇出会经常撞车。测试里的 lists two precisely-linked agents that share a description once each 覆盖不到这里 —— 它的两个 agent 都关联到了 messages 中存在的工具调用,根本不会走到收尾循环。

[覆盖] mergeSideTaskCatalogloaded: true —— App.tsx:1116。在同 parent 分支上改成 falseApp.test.tsx 293 个测试全部通过:只有 parent 变化那个测试断言了整个对象,三个同 parent 的测试只断言 items,而 loaded 控制着 sideTasksLoadingApp.tsx:2469)。

[既有问题,非本 PR 引入] 首条消息重命名吞掉永久性 401 —— SideTaskPanel.tsx:49,217-241。在默认 loopback 守护进程(不带 --token)上,mutate({ strict: true }) 需要 bearer token,所以重命名会永久 401。实测:

  • 401 PATCH /session/:id/metadata × 3 —— 三次尝试全失败,无退避、不区分永久/临时错误
  • UI 上的标签页标题:Investigate the flaky retry path(在第一次尝试之前就乐观写入,SideTaskPanel.tsx:221
  • 从守护进程读回的持久化 displayName"Side task"
  • toast:该失败一个都没有,尽管 onSideTaskError={reportError} 已接好(App.tsx:10322

"没有 toast"是量出来的、不是看一眼:toast 会自动消失,所以我用 MutationObserver 记录了所有曾经挂载过的 [data-web-shell-toast],并在同一构建上对比两个臂 —— 有 side task(多 3 次 401)与没有。两者 toast 集合完全相同。 路由与门禁与 main 一致,所以问题不是这里引入的 —— 但新的重试循环让它变成了标签页与 transcript 之间一次静默的分歧。

4. 新代码的变异矩阵 —— 杀死 4/9

杀死:side="left""bottom"、去掉 dismiss 守卫里的 branchPickerOpen、折叠/隐藏时不再自动关闭选择器、移除 didAttemptCreateRef、以及 §1 的 environmentPanelReachable 子句。

存活(三个是正确代码上的覆盖缺口,不是缺陷 —— 我逐个读了它们留空的分支):没有断言"分支名未解析时的 environment.unavailable 回退行"、没有断言 onOpenCommit 真的透传到选择器、没有断言"无会话时该行是惰性的",外加从 R3 带过来的 mergeSideTaskCatalog loaded。其中我真会去补断言的是 onOpenCommit:新面板行上 Commit 的接线若断了,测试仍然全绿。

5. Harness 说明(用于判断上面数字的可信度)

我自己有两个读数在追查前是错的,记录下来以免别人把它们重新推导成 PR 缺陷:

  • 500 POST /session/:id/model 来自我合成的 OPENAI_MODEL=fake-model;紧随其后的 [object Object] 错误 toast 在完全不创建 side task 时同样出现。两者都是 harness 级联,与本 PR 无关。
  • **"分支选择器会自己关闭"**是我的 harness 问题。用 button[aria-expanded="false"] 粗暴地展开面板分区时,也会匹配到 Radix 的 PopoverTrigger —— 于是这个扫描点到了分支行并把选择器切回关闭。宽度 × 展开的矩阵(1500/1040 × 有/无扫描)把变量隔离出来了:变量是扫描而不是布局;locator.click() 与单次 page.mouse.click() 都能让选择器稳定打开 3.2 秒以上。产品没问题,是我的选择器有问题。
  • monitoradditionalProperties: false(只接受 command/description/max_events/idle_timeout_ms/directory),多传一个键就不会创建任务 —— 面板看起来坏了,其实是 harness 坏了。

结论

可以合并。 R3 的阻塞问题已修复且守卫是承重的,既有单子句变异 A/B 也有真实守护进程上的实际应用验证;新的分支选择器接线在停靠态与浮动态都可用,其 dismiss 抑制在两个方向上都已验证。本轮没有发现发布阻塞项。

建议单独开 issue 跟进、而不是卡住合并:

  1. getEnvironmentAgentTasks —— 去重改用 id/toolUseId,不要用 description(§3)。
  2. 为环境面板分支行的 onOpenCommit 透传补一个断言(§4)。
  3. renameSession 的 401 归类为永久错误:只尝试一次,且不要让标签页显示一个没有落盘的标题(§3)—— 既有问题,但本 PR 是它变得用户可见的地方。

…t-panels

# Conflicts:
#	packages/sdk-typescript/scripts/build.js
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=240. See workflow logs.

@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 16/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 16/100 轮)。改动内容与我反驳保留之处如下:

Round summary

No actionable feedback this round — all review sections (Reviews, Inline comments, Issue-level comments, Failed checks) were empty. Critical-only mode is active; deferred non-Critical items are out of scope per policy.

Conflict resolution

Merged origin/main (at f3ad4fcff) into feat/web-shell-context-panels to resolve the reported base conflict.

Conflicted file: packages/sdk-typescript/scripts/build.js

Both branches bumped MAX_DAEMON_BROWSER_BUNDLE_BYTES from 177KB:

Resolution: Kept both bump comments in chronological order (177→178→184) and retained the higher 184KB budget, which already accommodates both sets of changes.

中文说明

轮次总结

本轮无可处理的反馈——所有评审区块(Reviews、Inline comments、Issue-level comments、Failed checks)均为空。当前处于仅处理 Critical 的模式;延后的非 Critical 项目按策略不在处理范围内。

冲突解决

origin/main(位于 f3ad4fcff)合并到 feat/web-shell-context-panels 以解决报告的基线冲突。

冲突文件: packages/sdk-typescript/scripts/build.js

两个分支都将 MAX_DAEMON_BROWSER_BUNDLE_BYTES 从 177KB 上调:

解决方式: 按时间顺序保留两条上调注释(177→178→184),并保留较高的 184KB 预算,该预算已涵盖双方的变更。

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/sdk-typescript — 31 test files, 1442 tests passed

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds: the automated reviewer's non-Critical suggestions below are deferred and stay open for human follow-up — do not modify code, resolve threads, or reply on their behalf. Maintainer feedback defers only once its author has already had 2 regular feedback batches addressed in this window's Critical-only tail — an account can host an automated reviewer loop, so the brake keys on measured regeneration, not identity; authors at their budget, if any, are named below. (A maintainer can lift the mode itself: @qwen-code /retry starts a fresh counting window.)

  • PR comment by @wenshao: feat(web-shell): add contextual task panels #7929 (comment)

  • @doudouOUC is at this window's regular-feedback budget — to continue: tag [Critical], submit a Request changes review, or comment @qwen-code /retry for a fresh window. / @doudouOUC 本窗口常规反馈预算已用完——继续请标 [Critical]、提交 Request changes、或评论 @qwen-code /retry 开新窗口。

  • @wenshao is at this window's regular-feedback budget — to continue: tag [Critical], submit a Request changes review, or comment @qwen-code /retry for a fresh window. / @wenshao 本窗口常规反馈预算已用完——继续请标 [Critical]、提交 Request changes、或评论 @qwen-code /retry 开新窗口。

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式:以上为自动评审的非 Critical 建议,予以延后、保持开放并留待人工跟进——不要为其修改代码、解决线程或代为回复。维护者的反馈仅在其本人于本窗口 Critical-only 阶段已被处理 2 批常规反馈之后才会被延后——账号可能挂着自动评审循环,因此刹车依据实测的再生频度而非身份;达到预算的作者(如有)在下方点名。(如需解除该模式,评论 @qwen-code /retry 即可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

📊 Takeover milestone — round 16/100, in the current window. Census: 9 pushed fix(es), 3 no-change review(s), 5 timeout(s), 2 rejected attempt(s), 0 other round(s) (crash / model error / gate error / infra), 5 base update(s).

This many rounds deserves a human look. Options: keep going (fine — nothing changes), split or reduce the PR if rounds keep accumulating, or release takeover (remove the autofix/takeover label or comment @qwen-code /takeover stop). Management continues unchanged unless you act.

中文说明

📊 接管里程碑 —— 第 16/100 轮(当前窗口)。统计:推送修复 9 次、审阅无需改动 3 次、超时 5 次、验证拒绝 2 次、其他轮次(崩溃/模型错误/门错误/infra)0 次、base 更新 5 次。

轮次到这个量值得人工看一眼。可选:继续(无需操作);若轮次持续累积,考虑拆分或缩减 PR;或释放接管(移除 autofix/takeover 标签或评论 @qwen-code /takeover stop)。不操作则托管照常继续。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code did not run conflict resolution for this request.

PR #7929 does not currently have merge conflicts with main.

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

LGTM对号

qwen-code-ci-bot pushed a commit that referenced this pull request Jul 30, 2026
@ytahdn
ytahdn added this pull request to the merge queue Jul 30, 2026
Merged via the queue into QwenLM:main with commit 0a3098a Jul 30, 2026
54 of 55 checks passed
@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Local verification report — real build, real daemon, real browser

Verdict: the feature works as designed. 1 real defect found (the PR's own test-plan step 2 is not satisfied), plus 2 notes. Recommend fixing the tab-close behaviour before merge; everything else I could measure holds up.

I verified 1f5d68e66 in an isolated worktree with a real npm ci/npm install + full npm run build, then drove the production packages/web-shell/dist that qwen serve actually serves (qwen serve: Web Shell UI served from …/web-shell/dist) with Playwright/Chromium against a real daemon and a mock OpenAI endpoint that logs every request body. No component mounting, no mock daemon — the shipped bundle, the shipped routes.

Scope note: the branch has 20+ merge commits, but the true diff against the merge-base (b2c77d224) is the reported 72 files / +8165 −572, so the review surface is real and not merge-inflated.

What I confirmed

# Claim (from the PR test plan) Result Evidence
1 Daemon advertises session_side_task 105 features incl. session_side_task; pre-feature build: absent (82)
2 POST /session/:id/side-task exists ✅ 201 pre-feature build: 404 Cannot POST …/side-task
3 Side task receives the main context snapshot killer the side task's first model request carries the parent turns (messages=4, roles system,user,assistant,user) and the model answers from them
4 Side task does not display inherited messages killer same run, same frame: side pane shows only its own turn
5 Side task absent from the normal session list …/sessions?sourceType=default → parent only; ?sourceType=side_task → the side task only
6 Creatable while the main turn is streaming 34 ms vs 24 606 ms for the untouched /branch control (724×)
7 Main response continues afterwards stalled turn still completed
8 Snapshot serialised against transcript writes mid-turn side task persisted with an intact session_source boundary, no torn JSONL record
9 Title from first message; existing history keeps its title renamed to the first prompt; a 2nd message left it unchanged
10 /btw <q> still the lightweight one-shot one-shot system-reminder observed at the wire; no side task created
11 Survives a full page reload after a fresh load the saved task is listed under the right panel's Side task menu and reopens
12 Subagent / Monitor / background shell in the context panel all three, correct status + icon
13 Each opens its matching detail tab Agent: panel probe agent, sleep 600, panel probe monitor
14 Context panel stays visible beside the detail see screenshot 4
15 Docked ⇄ floating responsive switch measured transition between 1375 px and 1400 px
16 Host item subsets are honoured rebuilt the bundle with subsets; omitted items disappear
17 Closing the last detail tab returns to the empty extension page the whole extension area closes — see Finding 1

Test suites for every changed package pass: web-shell 2588, acp-bridge bridge.test.ts 461, cli acpAgent+virtual-subagent-sessions 354, core sessionService+session-transcript-reader 178, sdk DaemonClient 303, webui 31.

The killer observable pair

The design splits one thing two ways: primeTurnFromHistory(records) gets the full records (the agent inherits) while replay uses selectVisibleHistoryRecords(...) (the UI hides). Both halves are visible in a single frame — the side task answers ZEBRA-42 from context it never displays, and the sidebar lists only the parent session:

side task inherits context but hides the transcript

raw text (if the image has not resolved yet)
LEFT  (main chat)  : "Remember this for later: the secret code is ZEBRA-42."  → "Noted, the secret code ZEBRA-42 is stored."
RIGHT (side task)  : "What was the secret code I told you?"                   → "The secret code you told me is ZEBRA-42."
                     ^ no inherited messages rendered, yet the answer proves the context arrived
SIDEBAR            : one session only ("Remember thi… / just now") — the side task is not listed

A/B, one function toggled. I neutralised only selectVisibleHistoryRecords in the built artifact (return records;) and re-ran the identical scenario. The inherited parent transcript now leaks into the side-task pane, and the tab never gets named (the restoredEmptySession guard sees user history, so the initial prompt is never sent) — so the hiding is load-bearing for the whole /btw side flow, not cosmetic. Structural checks (source boundary, forkedFrom tagging, list exclusion) stayed green, confirming the patch was surgical:

A/B with the fix neutralized

raw text (if the image has not resolved yet)
Same run, fix neutralized — the side-task pane now shows the PARENT conversation:
  "Remember this for later: the secret code is ZEBRA-42. …"
  "Noted, the secret code ZEBRA-42 is stored."
Tab title stays the generic "Side task" (never renamed).

  PR build      KILLER B  sideList.hasPlanted = false   → 11/11 pass
  neutralized   KILLER B  sideList.hasPlanted = true    →  8/11 pass

On disk the side-task transcript is exactly as documented: first record system/session_source with sourceType: side_task + sourceId: <parent>, then 4 inherited records all tagged forkedFrom, then the side task's own 8.

Concurrent creation, measured against an in-build control

Two independent sessions were each put into a 25 s stalled turn, then /branch and /side-task fired concurrently. /branch is untouched by this PR and chains onto entry.promptQueue; /side-task bypasses it via concurrentSideTask:

POST /session/:id/branch      -> 201 after 24606 ms   (blocked for the whole turn)
POST /session/:id/side-task   -> 201 after     34 ms   (724x faster)

The stalled main turn still completed, and the mid-turn snapshot was intact. Name sanitisation also holds: control chars stripped, capped at exactly 200 chars.

Context panel, driven by real running work

A real background subagent, a real background shell (sleep 600, is_background: true) and a real Monitor job. The daemon's own registry reported [{agent, completed}, {shell, running}, {monitor, running}] and the panel matched it section for section:

context panel with three live tasks

raw text (if the image has not resolved yet)
Environment      Changes Clean · Workspace ws
Subagents        panel probe agent            ⊘ Completed
Background tasks sleep 600                    ↻ Running
                 panel probe monitor          ↻ Running

Clicking each row opened its own tab, and the context panel stayed visible beside the detail (the Monitor detail shows live PID/elapsed/events):

detail tabs and monitor detail

raw text (if the image has not resolved yet)
tabs: ["Agent: panel probe agent", "sleep 600", "panel probe monitor"]
active tab "panel probe monitor" — ● Running | Stop | 11s Time | 0 Events | 2263710 PID | Command: sleep 600
context panel still docked to its left (Environment / Subagents / Background tasks)

Responsive threshold, swept at 25 px resolution with the extension area closed (data-floating on [data-testid="environment-panel"]):

1800 1600 1500 1450 1400  -> data-floating=false  (docked, 320px)
1375 1350 1300 1200 1000  -> data-floating=true   (floating popover)

matching MIN_DOCKED_MESSAGE_AREA_WIDTH (800) + DOCKED_ENVIRONMENT_PANEL_WIDTH (332) = 1132px of message area. With the extension area open the panel is floating at every width — that is the documented artifact-panel priority, not a regression.

floating context panel

Host customization — A/B by rebuilding the bundle

main.tsx is the host for the served bundle, so I rebuilt dist with subsets and re-probed the same daemon:

probe full items (as shipped) header:[], rightPanel:['review'], environmentPanel:['environment']
chat header present yes no
header action buttons 2 (environment + right panel) 0
right-panel empty actions Review… / Side task… not rendered
context panel sections Environment + Subagents Environment only

Omitted items are genuinely not rendered. See Note 1 on the empty-header case.


Finding 1 — closing the last detail tab closes the whole extension area (contradicts the PR's own test plan)

The PR's Reviewer Test Plan, step 2, says:

Closing the last detail tab should return to the empty extension page instead of closing the extension area.

It does the opposite. App.tsx:3111 closes the area unconditionally when the last tab goes:

const closeArtifactPanelTab = useCallback((tabId: string) => {
  setArtifactPanelTabs((tabs) => {
    const nextTabs = tabs.filter((tab) => tab.id !== tabId);
    if (nextTabs.length === 0) {
      setArtifactPanelOpen(false);   // <-- closes the extension AREA
      setActiveArtifactPanelTabId(null);
      ...

Measured in the real UI — open a task detail, close it, and the tab strip, the right-panel-empty-actions page and the panel itself are all gone (rightPanel: 0, emptyActionsInDom: 0); only the context panel remains:

before closing the last tab after closing it
before after
raw text (if the images have not resolved yet)
OPENED           {"tabs":["sleep 600"], "closeButtons":["Close sleep 600"], "rightPanel":2,
                  "emptyActionsInDom":0, "envPanel":1}
AFTER CLOSE 1    {"tabs":[],            "closeButtons":[],                 "rightPanel":0,
                  "emptyActionsInDom":0, "envPanel":1}

VERDICT: right panel still present = false | empty page rendered = false

Effect: a user inspecting several tasks in a row has the workspace collapse out from under them after each one, and must reopen it from the header every time — the exact "leaving the current conversation" churn the PR is meant to remove. Suggested fix: keep artifactPanelOpen true and clear only the active tab, so the render falls through to the !activeTab empty-actions branch that already exists at ArtifactPanel.tsx:441.

Note 1 — header: { items: [] } removes the header entirely, not just its actions

The plan says "Passing an empty header item list should render no default header actions." Literally true, but App.tsx:1590 gates the whole header on the list being non-empty:

const chatHeaderEnabled = chatHeaderItems.length > 0 && Boolean(header || renderChatHeader);

so items: [] also removes the only built-in affordance for opening the context panel and the right extension area (I could not open either in that build without the host's own shellRef API). Worth either documenting, or treating items: [] as "header with no actions" and keeping the row. Not a blocker.

Note 2 — benign daemon stderr noise on every side-task creation

Each creation logs an internal error immediately after the 201, because the client detaches the fresh session (createSideTaskSessiondetachSession), the daemon closes it (last_client_detached), and a session/cancel notification then arrives for the closed id:

POST /session/<parent>/side-task … status=201
qwen serve: closing session "<side>" (reason: last_client_detached)
Error handling notification { method: 'session/cancel', params: { sessionId: '<side>' } }
  { code: -32603, message: 'Internal error', data: { details: 'Session not found: <side>' } }

It is a fire-and-forget notification, nothing surfaces to the user, and the side task works — but it puts an Internal error in the daemon log on a fully successful path, which will confuse operators.


Harness notes (for anyone reproducing)

  • qwen serve prints listening on before its deferred runtime is mounted; until then every request — including the pre-auth Web Shell static mount — answers 401. Wait for GET /capabilities 200 and GET / returning HTML.
  • main.tsx strips ?token= from the URL via history.replaceState in production builds, so page.reload() re-requests the shell unauthenticated and renders a blank page. Re-navigate with the token. /session/<id> is a real REST route (the SPA fallback is mounted after the API routes), so it cannot be navigated to directly either — load the base URL and re-enter from the sidebar.
  • A background tool keeps the turn "running" long after its marker text appears; the submit button carries aria-label="Loading...". Gate each send on that, not on isEnabled() (an empty composer is also disabled). Without this the 2nd and 3rd tool never launch and the panel looks empty — that cost me two false "the panel doesn't show shell/Monitor tasks" results.
  • A mock LLM must break the tool loop per tool name, not on "any tool traffic in this conversation" — the blanket guard silently mutes the 2nd and 3rd tool of a multi-turn scenario.
  • The tab close button is a sibling of [role="tab"] (button[aria-label^="Close "] inside [role="tablist"]), not a descendant.
  • Run the responsive sweep with the extension area closed; measure detail tabs before resizing (a narrow pass leaves the panel floating and swallows row clicks).
  • packages/web-shell npm run build is three steps; client/build-artifact.test.ts reads dist/index.js from step 2. A bare vite build wipes it and reds 10 tests — that failure is self-inflicted, not the PR's.
  • Register the mock model in modelProviders.openai[] with envKey (not apiKeyEnvKey), or the shell's session/set_model round-trip 500s and drops a Set model failed + [object Object] toast over every screenshot. It reproduces identically on the pre-feature build, so it is not this PR's.

Environment: Linux, Node v22.22.2, worktree at PR head 1f5d68e66, baseline = a pre-feature build of the main line (no session_side_task in the artifact).

中文版(合并参考)

本地验证报告 —— 真实构建、真实 daemon、真实浏览器

结论:功能符合设计。发现 1 个真实缺陷(PR 自己的测试计划第 2 步未满足),另有 2 点说明。 建议合并前修掉页签关闭行为,其余我能测量的部分都成立。

我在独立 worktree 中对 1f5d68e66 执行了真实的 npm ci/npm install 与完整 npm run build,然后用 Playwright/Chromium 驱动 qwen serve 实际提供的生产 packages/web-shell/dist(日志 qwen serve: Web Shell UI served from …/web-shell/dist),后端是真实 daemon,模型端是一个会记录每个请求体的 mock OpenAI。没有组件挂载测试、没有 mock daemon —— 用的就是发布产物和真实路由。

范围说明:分支包含 20 多个 merge 提交,但相对 merge-base(b2c77d224)的真实 diff 就是所报告的 72 文件 / +8165 −572,因此评审面积真实,未被 merge 放大。

已确认项

# 声明(来自 PR 测试计划) 结果 证据
1 daemon 广告 session_side_task 105 项能力含该项;改动前构建:无(82 项)
2 POST /session/:id/side-task 存在 ✅ 201 改动前构建:404 Cannot POST …/side-task
3 侧边任务获得主会话上下文快照 关键 侧边任务首个模型请求携带父会话轮次(messages=4,角色 system,user,assistant,user),模型据此作答
4 侧边任务不展示继承消息 关键 同一次运行、同一帧画面:侧边区只显示自己的轮次
5 侧边任务不出现在普通会话列表 …/sessions?sourceType=default 只返回父会话;?sourceType=side_task 只返回侧边任务
6 主会话响应中也可创建 34 ms,对照未改动的 /branch24 606 ms(724 倍)
7 主响应随后正常完成 被打断的停顿轮次仍完成
8 快照与历史写入串行化 响应中创建的侧边任务 session_source 边界完整,无截断 JSONL 记录
9 首条消息命名;已有历史不改名 标题变为首条 prompt;第二条消息未改名
10 /btw <q> 仍为轻量单次问答 在 wire 上观察到单次问答的 system-reminder,且未创建侧边任务
11 刷新后仍可用 全新加载后,保存的侧边任务出现在右侧面板 Side task 菜单中并可重新打开
12 子智能体 / Monitor / 后台 Shell 出现在信息面板 三者齐全,状态与图标正确
13 各自打开对应详情页签 Agent: panel probe agentsleep 600panel probe monitor
14 信息面板在详情旁保持展示 见截图 4
15 占位 ⇄ 悬浮响应式切换 实测切换点在 1375 px 与 1400 px 之间
16 宿主配置子集生效 用子集配置重新构建 bundle,未配置项确实不渲染
17 关闭最后一个详情页签应回到拓展区空白页 整个拓展区被关闭 —— 见发现 1

所有涉及包的测试套件通过:web-shell 2588、acp-bridge bridge.test.ts 461、cli acpAgent+virtual-subagent-sessions 354、core sessionService+session-transcript-reader 178、sdk DaemonClient 303、webui 31

关键的一对可观测量

设计上把同一份记录分成两路:primeTurnFromHistory(records) 拿到完整记录(智能体继承上下文),而回放使用 selectVisibleHistoryRecords(...)(界面隐藏继承记录)。两侧在同一帧内同时可见 —— 侧边任务用它从未展示的上下文答出了 ZEBRA-42,且左侧会话列表只有父会话(截图 1)。

A/B:只切换一个函数。 我仅把构建产物中的 selectVisibleHistoryRecords 改为 return records;,再跑同一场景:继承的父会话历史立刻泄漏到侧边区,且页签始终未被命名(restoredEmptySession 判定存在用户历史,初始 prompt 不再发送)—— 说明这层隐藏是整个 /btw side 流程的承重结构,而非外观修饰。结构性检查(来源边界、forkedFrom 标记、列表过滤)保持通过,证明该改动是精准的(截图 2)。

PR 构建      KILLER B  sideList.hasPlanted = false   → 11/11 通过
中和后       KILLER B  sideList.hasPlanted = true    →  8/11 通过

磁盘上侧边任务历史与文档一致:首条为 system/session_sourcesourceType: side_task + sourceId: <父会话>),随后 4 条继承记录全部带 forkedFrom,再是侧边任务自身的 8 条。

并发创建,与同构建内的对照组比较

两个独立会话各自进入 25 秒停顿轮次,然后并发触发 /branch/side-task/branch 未被本 PR 改动、会挂在 entry.promptQueue 上;/side-task 通过 concurrentSideTask 绕过队列:

POST /session/:id/branch      -> 201,耗时 24606 ms(被整轮阻塞)
POST /session/:id/side-task   -> 201,耗时     34 ms(快 724 倍)

停顿的主轮次随后仍正常完成,响应中创建的快照完整。名称净化同样成立:控制字符被剥离,长度精确截断到 200。

信息面板由真实运行任务驱动

真实后台子智能体、真实后台 Shell(sleep 600is_background: true)和真实 Monitor 任务。daemon 自身注册表返回 [{agent, completed}, {shell, running}, {monitor, running}],面板逐区块与之一致(截图 3)。点击每一行都打开了各自的页签,且信息面板在详情旁保持展示,Monitor 详情显示实时 PID/耗时/事件数(截图 4)。

响应式阈值(关闭拓展区,按 25 px 步长扫描 data-floating):

1800 1600 1500 1450 1400  -> data-floating=false(占位,320px)
1375 1350 1300 1200 1000  -> data-floating=true (悬浮)

MIN_DOCKED_MESSAGE_AREA_WIDTH (800) + DOCKED_ENVIRONMENT_PANEL_WIDTH (332) = 1132px 消息区宽度一致。拓展区打开时面板在所有宽度下均为悬浮 —— 这是文档所述的拓展区优先级,不是回归。

宿主配置 —— 通过重新构建 bundle 做 A/B

main.tsx 就是所服务 bundle 的宿主,因此我用子集配置重建 dist 后对同一 daemon 重测:

探测项 完整配置(当前发布) header:[]rightPanel:['review']environmentPanel:['environment']
聊天 Header 存在
Header 操作按钮 2(环境 + 右侧拓展区) 0
右侧拓展区空白页操作 Review… / Side task… 完全不渲染
信息面板区块 Environment + Subagents 仅 Environment

未配置项确实不渲染。空 Header 的情况见说明 1。


发现 1 —— 关闭最后一个详情页签会关掉整个拓展区(与 PR 自己的测试计划矛盾)

PR 审查者测试计划第 2 步写的是:

关闭最后一个详情页签后,应返回拓展区空白页,而不是关闭整个拓展区。

实际行为相反。App.tsx:3111 在最后一个页签消失时无条件关闭拓展区:

if (nextTabs.length === 0) {
  setArtifactPanelOpen(false);   // <-- 关闭整个拓展区
  setActiveArtifactPanelTabId(null);

在真实界面中实测 —— 打开一个任务详情再关闭,页签条、right-panel-empty-actions 空白页以及面板本体全部消失(rightPanel: 0emptyActionsInDom: 0),只剩信息面板(截图 5 / 6):

OPENED           {"tabs":["sleep 600"], "rightPanel":2, "emptyActionsInDom":0, "envPanel":1}
AFTER CLOSE 1    {"tabs":[],            "rightPanel":0, "emptyActionsInDom":0, "envPanel":1}

影响:用户连续查看多个任务时,每关一个工作区就整块塌陷,必须每次从 Header 重新打开 —— 正是本 PR 想消除的「离开当前会话」的打断感。建议修法:保持 artifactPanelOpen 为 true、只清空当前页签,让渲染落到 ArtifactPanel.tsx:441 已有的 !activeTab 空白页分支。

说明 1 —— header: { items: [] } 会移除整个 Header,而不只是操作项

计划中写「Header 的操作项传空数组时,不应展示默认操作」。字面成立,但 App.tsx:1590 用列表非空来控制整个 Header:

const chatHeaderEnabled = chatHeaderItems.length > 0 && Boolean(header || renderChatHeader);

于是 items: [] 同时移除了打开信息面板和右侧拓展区的唯一内置入口(该构建下我无法通过界面打开二者,只能靠宿主自己的 shellRef API)。建议补充文档说明,或把 items: [] 理解为「保留 Header 行、不展示操作」。非阻塞项。

说明 2 —— 每次创建侧边任务都会产生一条无害的 daemon stderr 噪声

由于客户端在创建后立即 detach(createSideTaskSessiondetachSession),daemon 关闭该会话(last_client_detached),随后到达的 session/cancel 通知找不到该会话:

POST /session/<parent>/side-task … status=201
qwen serve: closing session "<side>" (reason: last_client_detached)
Error handling notification { method: 'session/cancel', … }
  { code: -32603, message: 'Internal error', data: { details: 'Session not found: <side>' } }

它是 fire-and-forget 通知,界面无任何异常,侧边任务功能正常 —— 但在一条完全成功的路径上写入 Internal error,会让运维困惑。


复现要点(给后续验证者)

  • qwen serve 打印 listening on 时其延迟运行时尚未挂载,在此之前所有请求(包括免鉴权的 Web Shell 静态挂载)都返回 401。应等待 GET /capabilities 返回 200 GET / 返回 HTML。
  • 生产构建下 main.tsx 会用 history.replaceState 去掉 URL 里的 ?token=,因此 page.reload() 会无鉴权重新请求 shell 并渲染空白页,必须带 token 重新导航。/session/<id> 是真实 REST 路由(SPA 兜底注册在 API 路由之后),也不能直接导航 —— 应加载根路径再从侧边栏进入会话。
  • 后台工具会让整轮长时间处于 running,提交按钮带 aria-label="Loading..."。每次发送前应等待该状态解除,而不是等 isEnabled()(空输入框同样是 disabled)。否则第 2、3 个工具根本不会启动、面板看起来是空的 —— 这让我先后得到两次「面板不显示 shell/Monitor 任务」的假结论。
  • mock LLM 必须按工具名打断工具调用循环,而不是按「本会话是否已有工具流量」—— 后者会静默吞掉多轮场景中的第 2、3 个工具。
  • 页签关闭按钮是 [role="tab"]兄弟节点[role="tablist"] 内的 button[aria-label^="Close "]),不是子节点。
  • 响应式扫描要在拓展区关闭状态下做;详情页签要在改变视口之前测(窄视口会让面板变悬浮并吞掉行点击)。
  • packages/web-shellnpm run build 有三步,client/build-artifact.test.ts 读的是第二步产出的 dist/index.js。只跑 vite build 会删掉它并红 10 个用例 —— 那是自造故障,不是本 PR 的问题。
  • modelProviders.openai[] 注册 mock 模型时字段是 envKey(不是 apiKeyEnvKey),否则 shell 的 session/set_model 会 500 并在每张截图上盖一个 Set model failed + [object Object] 的 toast。该现象在改动前构建上同样出现,与本 PR 无关。

环境:Linux,Node v22.22.2,worktree 位于 PR HEAD 1f5d68e66,基线为主线改动前构建(产物中不含 session_side_task)。

@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Follow-up: verified one-line fix for Finding 1, remaining creation routes covered, and one suspected bug ruled out

Continuing the verification above. Three updates.

1. Finding 1 — the fix is verified, and it is one line

Dropping the setArtifactPanelOpen(false) call is sufficient; the render already falls through to the !activeTab empty-actions branch:

   const closeArtifactPanelTab = useCallback((tabId: string) => {
     setArtifactPanelTabs((tabs) => {
       const nextTabs = tabs.filter((tab) => tab.id !== tabId);
       if (nextTabs.length === 0) {
-        setArtifactPanelOpen(false);
         setActiveArtifactPanelTabId(null);
         setReviewChanges([]);

Applied to App.tsx, rebuilt packages/web-shell and re-ran the same E2E against the same daemon:

close-last-tab result full context-panel suite
PR as-is rightPanel: 0, emptyActionsInDom: 0 — area gone 12/13
with the one-line fix rightPanel: 1, emptyActionsInDom: 1, empty page 475×74 visible with both actions 13/13

empty extension page after the fix

raw text (if the image has not resolved yet)
WITH FIX — AFTER CLOSING THE LAST TAB
{"tabs":[], "closeButtons":[], "rightPanel":1, "emptyActionsInDom":1,
 "emptyActionsRect":{"w":475,"h":74,"display":"grid","visibility":"visible","opacity":"1","children":2},
 "emptyActionsText":"ReviewView recent file changesSide taskView or create side tasks",
 "envPanel":1}

VERDICT: right panel still present = true | empty page rendered = true

No other assertion changed, so the fix is contained.

2. Test-plan step 3 is now fully covered — all four creation routes

The first pass covered /btw side and the REST route. I have now also driven the right-panel empty page and the public Web Shell API. For the API route I exposed the existing shellRef prop from main.tsx (one line, reverted afterwards) — no product change:

the public Web Shell handle exposes createSideTask()                              ok
shellRef.current.createSideTask() opens a draft tab and returns true              ok   tabs=["Side task"]
the API-created side task inherits the main context (verified at the wire)        ok   req#2 messages=4
the API-created side task is persisted with a side_task source boundary           ok
the right-panel empty page is reachable from the header toggle                    ok
the right-panel empty page creates a side task (New action)                       ok
the public API creates a side task WHILE the main turn is streaming               ok   returned true in 11ms
the main turn was genuinely still open at that moment (request in flight, no reply yet)   ok
the main turn still completes after the mid-turn API creation                     ok
9/9

All four routes produce a session with the side_task source boundary, inherit the parent context at the wire, and stay out of the sourceType=default listing.

3. Ruled out: the tab strip is not overlapping, it scrolls

While testing the fix I hit what looked like a second defect — with 3 tabs open, document.elementFromPoint at the last tab's close button returns the "Add panel" button, and Playwright refuses to click it. I chased it before reporting, and it is not a bug:

  • inactive tabs' close buttons are opacity: 0; pointer-events: none by design (hover-reveal), so the automation, not the product, was wrong;
  • the strip genuinely overflows and scrolls — scrollWidth 458 > clientWidth 401, overflow-x: auto;
  • after scrollLeft = scrollWidth, the last tab's close button reports reachable: true and an ordinary click closes it (3 → 2 tabs).

The only nit left is cosmetic: there is no visible affordance (scrollbar or chevrons) that the strip scrolls, so at 3+ tabs the last tab's X is off-screen until the user scrolls it. Mentioning it only so nobody else re-files it as a bug.

Correspondingly, one line of my earlier report should be read carefully: the close-tab finding was established by closing the active tab (whose X is always visible and clickable), so it stands unchanged — but any reviewer reproducing it with several tabs must hover the tab first.

Environment unchanged: worktree at PR head 1f5d68e66, Node v22.22.2, real daemon + prod web-shell/dist. All product files were restored and the bundle rebuilt to the shipped configuration after each experiment.

中文版(合并参考)

补充:发现 1 的修复已验证(一行),补齐剩余创建入口,并排除一个疑似缺陷

接续上一条验证报告,三点更新。

1. 发现 1 —— 修复已验证,且只需一行

删掉 setArtifactPanelOpen(false) 即可;渲染本来就会落到已有的 !activeTab 空白页分支:

       if (nextTabs.length === 0) {
-        setArtifactPanelOpen(false);
         setActiveArtifactPanelTabId(null);

改动 App.tsx 后重新构建 packages/web-shell,对同一 daemon 重跑同一套 E2E:

关闭最后一个页签的结果 信息面板完整用例
当前 PR rightPanel: 0emptyActionsInDom: 0 —— 拓展区消失 12/13
加上这一行修复 rightPanel: 1emptyActionsInDom: 1,空白页 475×74 可见,两个操作均在 13/13

其余断言均无变化,修复影响范围可控。

2. 测试计划第 3 步现已完全覆盖 —— 四个创建入口

首轮覆盖了 /btw side 与 REST 路由。本轮补充驱动了右侧拓展区空白页公开 Web Shell API。为验证 API 入口,我从 main.tsx 暴露了现有的 shellRef 属性(一行,事后已还原),没有改动产品逻辑:

公开 Web Shell handle 暴露 createSideTask()                       ok
shellRef.current.createSideTask() 打开草稿页签并返回 true          ok   tabs=["Side task"]
API 创建的侧边任务继承主会话上下文(wire 层验证)                  ok   req#2 messages=4
API 创建的侧边任务落盘且带 side_task 来源边界                      ok
右侧拓展区空白页可从 Header 入口打开                               ok
右侧拓展区空白页可创建侧边任务(New 操作)                         ok
主会话响应期间通过公开 API 创建侧边任务                            ok   11ms 返回 true
该时刻主轮次确实仍在进行(请求在途、尚无回复)                     ok
主轮次在中途创建后仍正常完成                                       ok
9/9

四个入口都会生成带 side_task 来源边界的会话、在 wire 层继承父上下文,并且都不出现在 sourceType=default 列表中。

3. 已排除:页签条不是遮挡,而是横向滚动

验证修复时我遇到一个疑似缺陷:开 3 个页签后,document.elementFromPoint 在最后一个页签关闭按钮中心返回的是 「Add panel」 按钮,Playwright 也拒绝点击。上报前我追查了原因,这不是缺陷

  • 非激活页签的关闭按钮设计上是 opacity: 0; pointer-events: none(悬停才显示),所以出错的是自动化脚本而不是产品;
  • 页签条确实溢出并可滚动 —— scrollWidth 458 > clientWidth 401overflow-x: auto
  • scrollLeft 设为 scrollWidth 后,最后一个页签的关闭按钮 reachable: true,普通点击即可关闭(3 → 2)。

仅剩一个观感上的小问题:没有可见的滚动提示(滚动条或箭头),因此 3 个以上页签时最后一个页签的 X 在滚动前处于视野外。写在这里只是为了避免他人重复上报。

对应地,上一条报告中有一句需要准确理解:关闭页签的发现是通过关闭激活页签(其 X 始终可见可点)得出的,因此结论不变;但复现者若开多个页签,需要先悬停对应页签。

环境不变:worktree 位于 PR HEAD 1f5d68e66,Node v22.22.2,真实 daemon + 生产 web-shell/dist。每次实验后所有产品文件均已还原并按发布配置重新构建。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants