feat(web-shell): add contextual task panels - #7929
Conversation
|
@qwen-code /takeover |
|
|
|
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 ( Approach: the scope is ambitious — configurable chat header, responsive context/environment panel, tabbed extension area, side tasks with first-message naming, Risk: Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是一个功能新增,不是 bug 修复——不需要复现。动机清晰:实时任务状态(子智能体、Monitor 任务、Shell 后台任务)此前分散在输入框元数据、消息流、弹窗和独立页面中,用户在继续主会话时难以持续跟踪长时间运行的任务。PR 将这些整合到常驻右侧工作区,并新增"侧边任务"(基于主会话上下文快照创建的隐藏持久会话)。 方向:与 Web Shell 向更丰富工作区演进的方向一致。CHANGELOG 中没有直接提及侧边任务,但该领域(会话管理、多智能体 UX)与项目路线图明显相关。 规模:PR 触及核心路径( 方案:范围很大——可配置聊天 Header、响应式环境面板、页签化拓展区、侧边任务(含首条消息命名)、 风险: 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code ReviewIndependent 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 — No critical blockers found. A few observations:
Files changed (30 of 72 shown)
Testing
All PR CI checks are green on Sandboxed verification would settle the remaining behavioural claims: 中文说明代码审查独立方案: 对于添加上下文任务面板,我会扩展现有的 fork/branch 机制(添加来源类型标记),添加 transcript 过滤以在侧边任务回放中隐藏继承记录,添加 serve 路由和 SDK 方法用于创建侧边任务,并构建 Web Shell 面板(带页签的右侧面板、环境面板)作为新组件复用现有原语。 对比: PR 与上述方案高度一致。核心改动最小且范围明确—— 未发现关键阻塞问题。几点观察:
测试所有 PR CI 检查在 沙箱验证可以确认剩余的行为声明: — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 3/5 — clean review with no correctness or security blockers, but the core-module escalation (500+ production lines touching 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 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 Deferring to @wenshao for the maintainer call on scope and the core-module gate. 中文说明置信度:3/5 —— 审查未发现正确性或安全性阻塞问题,但核心模块升级(500+ 生产代码行触及 实现质量扎实。侧边任务机制复用了现有的 branch/fork 基础设施而非另建路径,核心改动最小(3 个文件共 102 行生产代码),Web Shell 组件遵循现有约定。CI 全绿,包括 web-shell E2E 冒烟测试和视觉捕获。测试覆盖充分(约 4,341 行测试代码)。 让我犹豫的不是质量而是范围。约 4,284 行生产代码、72 个文件、6 个包,PR 打包了可配置聊天 Header、响应式环境面板、页签化拓展区、侧边任务生命周期管理、Shell/Monitor 任务详情视图、宿主自定义 API 和 并发侧边任务创建(在响应中快照父上下文而不中断)和继承记录隐藏(模型可见但回放中不显示)的行为声明是静态审查最难验证的部分。 转交 @wenshao 做维护者决策。 — Qwen Code · qwen3.8-max-preview Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
🩺 serve daemon A/BBuilt the PR base vs this PR head
|
| field | PR base (before) | this PR (after) |
|---|---|---|
features[] |
— | "session_side_task" |
— Qwen Code · serve A/B
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
|
🔀 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 将重新运行。 |
Maintainer-side local verification — PR #7929 (head
|
| 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:
Subagent detail tabs work against the real backend too (real background subagent, real transcript, completed badge):
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 @smoke → main 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:
Root cause — getEnvironmentAgentTasks (App.tsx) dedupes on three keys, and on a real daemon none of them match:
task.toolUseId === tool.callId— the/taskssnapshot for a finished agent carries notoolUseId(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 bothtaskIdandtoolUseId; the real completion record (user/notification) carriesbackgroundTask.taskIdonly;task.id === ${subagentType}-${tool.callId}— the daemon's id isgeneral-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). Changingif (sourceBoundary >= 0)toif (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.hideInheritedHistoryin 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.branchlooks like the label that was meant to go there.
Suggested merge path
- Fix A, B, C (CI is red on all three; each fix is small and verified above).
- Fix D or file it as an immediate follow-up — it is visible on the very first background subagent a user launches.
- 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-task,9/9 通过:
| 验证点 | 证据 |
|---|---|
| 路由与能力位 | 广播 session_side_task;返回 201,含 parentSessionId、displayName,无 forkedFrom,sourcePersisted=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 0、prettier --check、tsc --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 @smoke → main 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:该映射来自同时带taskId与toolUseId的 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_title、virtual-subagent-sessions.ts 中为 /fork 增加的 task-id 匹配。)
🔍 小问题
- 三个新增 i18n key 在中英文里都未被使用:
environment.branch、environment.history、environment.changeCount。分支行只渲染值、没有标签(非 git 工作区下只显示 “Unavailable”),environment.branch看起来正是本该用在那里的标签。
建议的合入路径
- 修复 A、B、C(三项都对应当前 CI 红灯,改动都很小且已验证)。
- 修复 D,或立刻作为紧随其后的跟进项——用户启动第一个后台子智能体就会看到。
- 为 E(边界强制性)与 F(合并 key)补测试,尤其是 E:一行改动即可造成泄漏,而目前 CI 抓不到。
核心设计——session_source 边界记录、转录读取器的切片、daemon 路由、SDK 方法,以及「保留上下文但不展示继承历史」的行为——在真实端到端使用下都站得住。
| if (directive.toLowerCase() === 'sider') { | ||
| createSideTask(); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
[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.
| 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_task 或 workspaceCwd 未定义时,用户执行该命令后不会收到任何反馈(无提示、无面板、无报错),而普通 /fork 路径在失败时会推送 toast 提示。
— qwen3.7-max via Qwen Code /review
| return workspace.client | ||
| .listWorkspaceSessions(summary.workspaceCwd, { pageSize: 200 }) | ||
| .then((sessions) => { | ||
| if (worktreeSessionIdRef.current !== sid) return; |
There was a problem hiding this comment.
[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.
| 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
| export function MonitorIcon(props: SVGProps<SVGSVGElement>) { | ||
| return ( | ||
| <svg |
There was a problem hiding this comment.
[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.
| 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
| const handleRightPanelOpen = (request: TurnOutputOpenRequest) => { | ||
| if (request.kind === 'subagent') { | ||
| onRightPanelOpen?.({ |
There was a problem hiding this comment.
[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.
| 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
|
@qwen-code /resolve |
|
🤖 Could not address the latest feedback automatically (round 1/100). A human should take over this PR. Review feedback addressedSame-run verification repair
No inline review comments were present in this round. Verification
中文说明已处理的评审反馈同轮验证修复
Why it was not pushed: tests failed in packages/web-shell Run log: https://github.com/QwenLM/qwen-code/actions/runs/30364965651 🧠 Handled by Qwen Code · model/模型 |
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge conflict resolution for PR #7929Root causePR #7731 ( Textual or semanticTextual only. The two sides modified adjacent import lines with no overlapping logic. The resolution preserves main's import ordering ( import { BranchPickerPopover } from '../BranchPickerPopover';
import { useI18n } from '../../i18n';
import {
SESSION_LIST_PAGE_SIZE,
WEB_SHELL_SESSION_SOURCE_TYPE,
} from '../../constants/sessions';The standalone What is load-bearing
What I could not verifyNo build, typecheck, or tests were run. The auto-merged files (SDK types, 中文说明PR #7929 合并冲突解决根因PR #7731( 文本冲突还是语义冲突纯文本冲突。两侧修改的是相邻的 import 行,没有逻辑重叠。解决方案保留了 main 的 import 排序( import { BranchPickerPopover } from '../BranchPickerPopover';
import { useI18n } from '../../i18n';
import {
SESSION_LIST_PAGE_SIZE,
WEB_SHELL_SESSION_SOURCE_TYPE,
} from '../../constants/sessions';main 原本在 关键依赖
未能验证的内容未运行构建、类型检查或测试。git 自动合并的文件(SDK 类型、 |
|
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-ci-bot
left a comment
There was a problem hiding this comment.
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
| The artifact-panel action remains available when no tab exists. Opening an | ||
| The empty panel shows Review and, when session-source metadata is supported, |
There was a problem hiding this comment.
[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.
| 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
| .contextBodyWithEnvironmentPanel [data-web-shell-message-list] { | ||
| box-sizing: border-box; | ||
| width: calc(100% + 332px); | ||
| padding-right: 356px; |
There was a problem hiding this comment.
[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.
| .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
| const originalResizeObserver = globalThis.ResizeObserver; | ||
| globalThis.ResizeObserver = class { |
There was a problem hiding this comment.
[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.
| 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
| export function ShellTaskDetail({ | ||
| task, | ||
| actions: providedActions, |
There was a problem hiding this comment.
[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.
| 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
| const closeArtifactPanel = useCallback(() => { | ||
| setArtifactPanelOpen(false); |
There was a problem hiding this comment.
[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.
| 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>()); |
There was a problem hiding this comment.
[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.
| 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
| 'session_source_metadata', | ||
| 'session_side_task', | ||
| 'session_prompt', |
There was a problem hiding this comment.
[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.
| '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
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Round summary — no new changes neededAll actionable findings from this round were already addressed in commit e14ce42 ( Critical:
|
…t-panels # Conflicts: # packages/web-shell/client/App.test.tsx
|
🤖 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 feedbackCritical-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/模型 |
|
@qwen-code /triage |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. |
Maintainer local verification — PR #7929 round 4 (head
|
| # | 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 |
re-reproduced at R4 | |
| 3 | [coverage] mergeSideTaskCatalog loaded: true mutant survives |
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 | 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)); // ← newReverting 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 preserved | clause 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.
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 b00fe1c7 | branchPickerOpen 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-out — App.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: true — App.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 401 — SideTaskPanel.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 atSideTaskPanel.tsx:221, before the first attempt) - persisted
displayNameread 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.
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 && activeBranch → gitWorkspaceCwd) |
nothing asserts the environment.unavailable fallback row |
|
drop onOpenCommit pass-through to the picker |
the test passes onOpenGitCommit but never asserts it arrives |
|
gitWorkspaceCwd={connection.sessionId ? … : undefined} → unconditional |
nothing asserts the row is inert without a session | |
mergeSideTaskCatalog loaded (carried from R3) |
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/modelis my syntheticOPENAI_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'sPopoverTrigger— 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, andlocator.click()vs a singlepage.mouse.click()both keep the picker open for 3.2s+. The product is fine; my selector wasn't. monitorisadditionalProperties: false(command/description/max_events/idle_timeout_ms/directoryonly) — 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:
getEnvironmentAgentTasks— key the dedupe onid/toolUseId, notdescription(§3).- Assert the
onOpenCommitpass-through from the environment panel's branch row (§4). - Classify a 401 from
renameSessionas 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 去重活跃子智能体 |
|
| 3 | [覆盖] mergeSideTaskCatalog 的 loaded: 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 个正是该提交新增的测试(gitBranch 从 visibleToolbarActions 消失、StatusBar 的 tasks 变成 [])。这同时量化了 R3 那个覆盖缺口:293 个里有 291 个在"有修复"和"无修复"两种情况下都通过 —— 新增的这两个测试才让该行为可被观测。
真实守护进程上的验证(legacy 宿主 = 只给 renderChatHeader、不给 header,且有真实运行中的后台任务):
head b00fe1c7 |
回退该子句 | |
|---|---|---|
composer git chip 的 aria-label |
Current Git branch: main — 1 modified |
(不存在) |
| 底栏任务胶囊文本 | 1 shell, 1 monitor |
(不存在) |
一个残留项(我判断是设计如此,但建议补一行文档):同时设置 header(其 items 含 environment,注意 header: {} 默认就是三项全开)和 renderChatHeader 的宿主,替换逻辑仍会启用,因此它的渲染函数必须自己调用 onEnvironmentPanelOpenChange,否则面板不可达。真正的旧宿主根本不会传 header,所以回归这一类已经关闭;这里只是显式 opt-in 的路径。
2. R4 新行为在真实守护进程上的验证
该提交还把环境面板的分支行改成了 BranchPickerPopover 触发器,并在选择器打开期间抑制浮动面板的"点击外部关闭"。
停靠态(1500px):面板里是三个来自真实守护进程的运行中任务(后台子智能体 + run_shell_command + monitor);新的分支行能打开选择器,并列出工作区真实的引用 —— chore/deps-bump、feat/context-panels、fix/side-task-boundary、main,data-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 会被该提交新增的测试杀死。值得记住它为什么重要:宿主未接 onSideTaskCreated 时 ArtifactPanel 会退回 ignoreSideTaskCreated,所以在这个提交之前,创建成功后的一次标题变更就可能重新进入 create() 并再造一个守护进程会话。
3. 仍然存在的问题
[次要] 活跃子智能体去重会吞掉并行扇出 —— App.tsx:1356,未改动。收尾的对账循环把每个活跃 agent 与同一循环刚刚 push 进去的行比较,且键是 description,所以 id 与 toolUseId 都不同也没用。直接对 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 中存在的工具调用,根本不会走到收尾循环。
[覆盖] mergeSideTaskCatalog 的 loaded: true —— App.tsx:1116。在同 parent 分支上改成 false,App.test.tsx 293 个测试全部通过:只有 parent 变化那个测试断言了整个对象,三个同 parent 的测试只断言 items,而 loaded 控制着 sideTasksLoading(App.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 秒以上。产品没问题,是我的选择器有问题。 monitor是additionalProperties: false(只接受command/description/max_events/idle_timeout_ms/directory),多传一个键就不会创建任务 —— 面板看起来坏了,其实是 harness 坏了。
结论
可以合并。 R3 的阻塞问题已修复且守卫是承重的,既有单子句变异 A/B 也有真实守护进程上的实际应用验证;新的分支选择器接线在停靠态与浮动态都可用,其 dismiss 抑制在两个方向上都已验证。本轮没有发现发布阻塞项。
建议单独开 issue 跟进、而不是卡住合并:
getEnvironmentAgentTasks—— 去重改用id/toolUseId,不要用description(§3)。- 为环境面板分支行的
onOpenCommit透传补一个断言(§4)。 - 把
renameSession的 401 归类为永久错误:只尝试一次,且不要让标签页显示一个没有落盘的标题(§3)—— 既有问题,但本 PR 是它变得用户可见的地方。
…t-panels # Conflicts: # packages/sdk-typescript/scripts/build.js
|
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 /resolve |
|
🤖 Addressed the latest review feedback (round 16/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 16/100 轮)。改动内容与我反驳保留之处如下: Round summaryNo 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 resolutionMerged Conflicted file: Both branches bumped
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 项目按策略不在处理范围内。 冲突解决将 冲突文件: 两个分支都将
解决方式: 按时间顺序保留两条上调注释(177→178→184),并保留较高的 184KB 预算,该预算已涵盖双方的变更。 Verification
Deferred non-Critical feedbackCritical-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:
中文说明完成 5 个产生改动的轮次后进入仅处理 Critical 的模式:以上为自动评审的非 Critical 建议,予以延后、保持开放并留待人工跟进——不要为其修改代码、解决线程或代为回复。维护者的反馈仅在其本人于本窗口 Critical-only 阶段已被处理 2 批常规反馈之后才会被延后——账号可能挂着自动评审循环,因此刹车依据实测的再生频度而非身份;达到预算的作者(如有)在下方点名。(如需解除该模式,评论 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/模型 |
|
📊 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 中文说明📊 接管里程碑 —— 第 16/100 轮(当前窗口)。统计:推送修复 9 次、审阅无需改动 3 次、超时 5 次、验证拒绝 2 次、其他轮次(崩溃/模型错误/门错误/infra)0 次、base 更新 5 次。 轮次到这个量值得人工看一眼。可选:继续(无需操作);若轮次持续累积,考虑拆分或缩减 PR;或释放接管(移除 |
|
Qwen Code did not run conflict resolution for this request. PR #7929 does not currently have merge conflicts with main. |
Follow-up: verified one-line fix for Finding 1, remaining creation routes covered, and one suspected bug ruled outContinuing the verification above. Three updates. 1. Finding 1 — the fix is verified, and it is one lineDropping the 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
raw text (if the image has not resolved yet)No other assertion changed, so the fix is contained. 2. Test-plan step 3 is now fully covered — all four creation routesThe first pass covered All four routes produce a session with the 3. Ruled out: the tab strip is not overlapping, it scrollsWhile testing the fix I hit what looked like a second defect — with 3 tabs open,
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 中文版(合并参考)补充:发现 1 的修复已验证(一行),补齐剩余创建入口,并排除一个疑似缺陷接续上一条验证报告,三点更新。 1. 发现 1 —— 修复已验证,且只需一行删掉 if (nextTabs.length === 0) {
- setArtifactPanelOpen(false);
setActiveArtifactPanelTabId(null);改动
其余断言均无变化,修复影响范围可控。 2. 测试计划第 3 步现已完全覆盖 —— 四个创建入口首轮覆盖了 四个入口都会生成带 3. 已排除:页签条不是遮挡,而是横向滚动验证修复时我遇到一个疑似缺陷:开 3 个页签后,
仅剩一个观感上的小问题:没有可见的滚动提示(滚动条或箭头),因此 3 个以上页签时最后一个页签的 X 在滚动前处于视野外。写在这里只是为了避免他人重复上报。 对应地,上一条报告中有一句需要准确理解:关闭页签的发现是通过关闭激活页签(其 X 始终可见可点)得出的,因此结论不变;但复现者若开多个页签,需要先悬停对应页签。 环境不变:worktree 位于 PR HEAD |
|
Released in v0.21.2. |


































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.
/tasksopens the context panel,/btw <question>keeps the existing lightweight one-shot interaction, and/btw side <question>creates a persistent side task when the daemon advertisessession_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
/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.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
Environment (optional)
Local Web Shell connected to the local daemon on macOS.
Risk & Scope
Linked Issues
N/A
Pictures
中文说明
这个 PR 做了什么
这个 PR 将 Web Shell 右侧区域完善为常驻的上下文工作区。新增了可配置的聊天顶部 Header、用于展示环境信息、子智能体、Monitor 任务和 Shell 后台任务的响应式信息面板,以及可通过页签展示审查、任务详情、产物和文件的右侧拓展区,用户无需离开当前会话即可查看和处理相关内容。
同时新增了“侧边任务”:它是基于主会话创建瞬间的序列化上下文快照生成的、可持久化但不会出现在普通会话列表中的会话,并支持在主会话仍在响应时创建。侧边任务支持持续对话、切换模型和模式、使用第一条消息命名、刷新后重新打开,并支持在右侧继续打开审查或产物页签;其聊天界面不会展示继承自主会话的历史消息。
宿主可以配置 Header 操作项、右侧拓展区入口和信息面板区块,也可以完整替换聊天 Header,并通过公开的 Web Shell API 创建侧边任务。
/tasks会打开信息面板;/btw <问题>保留原有的轻量单次问答;当 daemon 支持session_side_task时,/btw side <问题>会在右侧拓展区创建可持续对话的侧边任务。为什么需要
此前实时任务状态分散在输入框底部信息、消息流、弹窗和独立页面中,用户在继续主会话时不容易持续跟踪子智能体和长时间运行的任务。新的布局让任务上下文和相关操作始终靠近聊天区域,为 Monitor 与 Shell 任务提供一致的详情交互,并允许用户创建一个持久的次级会话,同时不干扰主会话列表。
审查者测试计划
如何验证
/btw side <问题>创建侧边任务,并在主会话响应期间重复验证。确认主响应不中断,侧边任务在创建时获得已持久化的主会话上下文,但不展示继承消息、不出现在普通会话列表中,支持切换模型和模式,并能在刷新后重新打开;同时确认/btw <问题>仍保持轻量单次问答。前后对比
改动前:任务上下文分散在消息流、输入框底部信息和弹窗中,也没有用于承载审查和次级会话的常驻工作区。
改动后:聊天 Header、响应式信息面板和页签化右侧拓展区将子智能体、Monitor、Shell、审查、产物和侧边任务相关流程集中在同一处。
测试环境
环境
macOS 本地 Web Shell 连接本地 daemon。
风险与范围
关联 Issue
无