Skip to content

fix(web-shell): settle parallel-agents collapse and unify agent detail transcript - #9640

Merged
ytahdn merged 2 commits into
QwenLM:mainfrom
ytahdn:worktree-web-shell-agent-panel-fixes
Aug 21, 2026
Merged

fix(web-shell): settle parallel-agents collapse and unify agent detail transcript#9640
ytahdn merged 2 commits into
QwenLM:mainfrom
ytahdn:worktree-web-shell-agent-panel-fixes

Conversation

@ytahdn

@ytahdn ytahdn commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Four web-shell UI fixes around subagents:

  1. Agent detail panel: the subagent prompt now renders as a normal user bubble inside the transcript and the whole message area scrolls as one unit, matching the main agent. Previously the prompt was pinned above a separate fixed-height box, splitting the panel into two disconnected scroll regions.
  2. Parallel-agents collapse: a parallel-agents group now collapses and returns to its chronological position as soon as its agents are all terminal, even while the main agent keeps issuing new tool calls. The collapse is deferred only while the completion summary narration is still expected, and that wait is now bounded by a grace window so a missing narration can no longer pin the group expanded forever.
  3. Infinite update loop: opening the side panel could crash React with "Maximum update depth exceeded" (a session whose artifact list cannot load returned a fresh empty array every render, re-running consumer effects in a loop). The artifacts hook now returns a stable empty array, and the loop is gone.
  4. Running-subagent hover: hovering a running subagent row in the main message area now brightens the summary text together with the leading and trailing icons.

Why it's needed

The detail panel layout made the user message and the agent's activity feel disconnected from each other. Parallel agents stayed expanded and pinned at the bottom of the turn after completing, distorting transcript order while the main agent kept working, and one edge case (a completion notification landing after the final answer with no follow-up narration) kept the group expanded indefinitely. The update loop froze turn-collapse toggles across the whole chat once the side panel was open, and the running-agent row gave no hover feedback on its content.

Reviewer Test Plan

How to verify

  1. Open a subagent's detail panel from the transcript: the subagent's prompt should appear as a user bubble at the top of the transcript, and the panel body should scroll as one area (scrollbar visible at the panel edge). The description/status header stays fixed above the scroll.
  2. Trigger parallel subagents in a session: while any agent is still running the group stays expanded at the turn's tail; once the last agent completes, the group collapses back to its summary row and returns to its chronological position within about 1.5s, even while the main agent continues streaming further tool calls.
  3. Open the side panel with a subagent tab while the daemon session has no artifacts endpoint: no console errors, and the "Processed/已处理" turn toggle in both the main transcript and the subagent panel still expands and collapses.
  4. Hover a running subagent row in the main message area: the summary text should brighten along with the icons.

Evidence (Before & After)

N/A (UI changes; behavior verified via unit/DOM tests and live dev:daemon run, no screenshots captured).

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

npm run dev:daemon in the worktree; relevant test suites pass: MessageList 263, parallel-agents/tool 124, useSessionArtifacts 10, App 490; typecheck, eslint and prettier clean on changed files.

Risk & Scope

  • Main risk or tradeoff: the auto-collapse timing change is user-visible; the group now folds sooner than before (right after completion rather than at turn end), which is the requested behavior. The color: inherit hover rule also brightens the running-agent summary text in the expanded state, a minor visual side effect.
  • Not validated / out of scope: E2E/browser automation was not run (verified via unit/DOM tests and a live daemon run on macOS).
  • Breaking changes / migration notes: none.

Linked Issues

N/A

中文说明

这个 PR 做了什么

四项 web-shell 关于 subagent 的 UI 修复:

  1. Agent 详情面板:subagent 的 prompt 现在以正常用户气泡渲染在 transcript 内,整个消息区域作为整体滚动,与主 Agent 一致。之前 prompt 被固定在一个独立的高度受限区域上方,面板被切成两个互不相连的滚动区。
  2. 并行智能体收起:并行组在其全部 agent 进入终态后立即收起并回到时间顺序位置,即使主 Agent 仍在继续发出新的工具调用。收起仅在等待完成总结叙述时延迟,且该等待现在有宽限窗口兜底,缺失叙述不再让组无限期保持展开。
  3. 无限更新循环:打开侧边面板曾导致 React "Maximum update depth exceeded" 崩溃(无法加载产物列表的会话每次渲染都返回新的空数组,导致消费方 effect 循环执行)。产物 hook 现在返回稳定空数组,循环消失。
  4. 运行中 subagent 的 hover:主消息区 hover 运行中的 subagent 行时,摘要文字现在与前后图标一起变亮。

为什么需要

详情面板布局让用户消息与 agent 活动显得割裂。并行智能体完成后仍保持展开并钉在本轮底部,扭曲了时间顺序,且存在一个边界情况(完成通知落在最终回答之后且无后续叙述)会让组无限期保持展开。无限循环在侧边面板打开后冻结了整段对话的折叠切换。运行中的 agent 行内容没有任何 hover 反馈。

Reviewer 验证计划

  1. 从 transcript 打开 subagent 详情面板:prompt 应作为用户气泡出现在 transcript 顶部,面板主体整体滚动(滚动条显示在面板边缘)。描述/状态头部保持固定。
  2. 触发并行智能体:任一 agent 运行中时组保持在轮尾展开;最后一个 agent 完成后约 1.5s 内组收起为摘要行并回到时间顺序位置,即使主 Agent 继续流式输出新的工具调用。
  3. 在会话无 artifacts 端点时打开含 subagent 标签页的侧边面板:无控制台报错,主 transcript 与 subagent 面板的"已处理"折叠开关仍可正常展开/收起。
  4. hover 主消息区运行中的 subagent 行:摘要文字应随图标一起变亮。

风险与范围

  • 主要风险/权衡:自动收起时机是用户可见行为变化;组现在比之前更早折叠(完成后即折叠而非整轮结束),这是所要求的行为。color: inherit 的 hover 规则也会让展开态的运行中摘要文字变亮,属于轻微的视觉附带变化。
  • 未验证/超出范围:未运行 E2E/浏览器自动化(通过单元/DOM 测试与 macOS 上的 dev:daemon 实测验证)。
  • 破坏性变更/迁移说明:无。

…l transcript

- Show the subagent prompt as a user bubble inside the detail panel's
  transcript and let the whole message area scroll as one unit instead of
  pinning the prompt above a separate fixed-height box.
- Collapse a parallel-agents group as soon as its agents are terminal, even
  while the main agent keeps responding, and bound the awaited-summary hold
  with a grace window so a missing narration cannot pin the turn open.
- Return a stable empty artifacts array from useSessionArtifacts so a
  session without an artifacts endpoint stops re-running consumer effects
  every render, which previously cascaded into an infinite update loop.
- Brighten a running subagent's summary text on hover alongside its icons.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on cfea416 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— cfea416 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: these read as observed issues, not theoretical hardening — the side-panel crash cites a concrete failure mode (React "Maximum update depth exceeded" when a session's artifact list cannot load), and the collapse/layout behaviors are specific and reproducible from the description. No linked issue or before/after capture is attached (Evidence says N/A), but the accompanying test updates encode regressions for each behavior, and the author is the web-shell domain maintainer, so this clears the existence bar.

Direction: aligned. This is a continuation of the ongoing web-shell subagent UX work (agent detail panel, parallel-agents rendering, side-panel tabs) seen in recent merged PRs in this area. Nothing here touches auth, sandbox, telemetry, or public contracts.

Size: not applicable — all changes are in packages/web-shell/client/**, outside the core-module paths; ~160 production lines vs ~408 test lines.

Approach: scope feels right. Four fixes, but all are small, self-contained, and clustered in the same subagent UI surface; each carries its own tests. The PR itself calls out the one user-visible timing change (parallel groups now fold right after completion rather than at turn end) and a minor hover side effect — honest risk framing. No drive-by changes spotted in a skim of the diff structure.

Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这些是已观测到的问题,不是理论性加固——侧边面板崩溃引用了具体的失败模式(会话产物列表无法加载时 React "Maximum update depth exceeded"),收起/布局行为也有具体、可按描述复现的场景。PR 未关联 issue、未附 before/after 截图(Evidence 写 N/A),但随附的测试更新为每个行为编码了回归用例,且作者是 web-shell 领域的维护者,通过问题存在性检查。

方向:对齐。这是近期已合并的 web-shell subagent UX 系列工作(agent 详情面板、并行智能体渲染、侧边面板标签页)的延续,不涉及 auth、sandbox、telemetry 或公共契约。

规模:不适用——全部改动位于 packages/web-shell/client/**,不在核心模块路径内;约 160 行生产代码 vs 约 408 行测试代码。

方案:范围合理。四项修复都小且自包含,集中在同一 subagent UI 面,各自带有测试。PR 自身也说明了唯一的用户可见时序变化(并行组在全部完成后立即收起而非整轮结束)和轻微的 hover 附带效果——风险描述诚实。diff 结构速览未发现顺手改动。

风险:无升级风险信号——改动文件均未命中与回滚相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Code review

I formed my independent proposal before reading the diff (drop the pinned prompt box and render the prompt as the transcript's first bubble; collapse groups once their agents are terminal and bound any narration-wait with a timeout; hoist the empty array to a module constant; one CSS line for hover). The PR lands on exactly those mechanisms in all four cases — no simpler path was missed. What I verified statically:

Detail panel unification. getSubagentPrompt and the pinned <pre> block are deleted, hideFirstUserMessage is no longer passed, and the two scroll regions (the prompt's own max-height: 240px; overflow: auto box and the transcript's fixed height: min(65vh, 720px)) are gone. The panel is a bounded flex column (.detail height 100% → .transcript flex:1), so MessageList becomes the single scroll area with the header fixed above it — matching the described intent. Confirmed getSubagentPrompt and hideFirstUserMessage have no other consumers.

Parallel-agents collapse. The deferral formula shrinks from "any latest-turn group while responding" to "only the group owning the awaited notification, and only while a summary narration is genuinely expected." The previously unbounded ordering wait (final answer on screen before the completion notification — the case that pinned groups open forever) now gets the same 5s grace the unmatched-completion latch already uses, keyed to the turn's own agent-notification id so a monitor banner mid-wait neither restarts nor re-arms it. The new memo/effect pair has no feedback loop: the hold-check passes a literal true for the narration wait, so the expiry flag cannot re-arm its own timer. Reuses UNMATCHED_AGENT_COMPLETION_GRACE_MS instead of inventing a constant; the dropped context fields (agentNotificationIsLatestBackground, latestBackgroundNotificationInLatestTurn) have no consumers outside the rewritten formula.

Infinite update loop. Module-level EMPTY_ARTIFACTS — the textbook identity fix. The crash path checks out: SubagentDetailContent keys an effect on artifacts and calls onArtifactsChange on every identity change, so a fresh [] per render cascaded into parent re-renders. No consumer mutates the array, so sharing the constant is safe.

Hover. .chatSummary:hover already lifts the row to --primary; the active summary text kept its own color, so color: inherit completes the brightening. One line on the right rule.

Tests were rewritten to encode the new semantics rather than just made to pass: the two old "defer while responding" tests were replaced with collapse-despite-responding tests, and new DOM tests pin the chronological repositioning (compareDocumentPosition), the grace boundary at 4999/5000ms, and the monitor-notification no-restart rule. No critical findings, no convention violations.

Files changed (9 of 9)
File What changed
packages/web-shell/client/components/MessageList.tsx Collapse deferral limited to the owner group; ordered-narration wait bounded by a 5s grace timer
packages/web-shell/client/components/MessageList.dom.test.tsx Deferral tests rewritten for collapse-on-completion; new tests pin grace boundary, chronological return, monitor no-restart
packages/web-shell/client/components/artifacts/SubagentDetail.tsx Pinned prompt block and hideFirstUserMessage removed; prompt renders as a transcript bubble
packages/web-shell/client/components/artifacts/SubagentDetail.module.css Prompt box and fixed transcript height removed, leaving one scroll area
packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx Tests for the removed getSubagentPrompt helper deleted
packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx Asserts the prompt renders as a bubble, no standalone prompt panel
packages/web-shell/client/components/messages/tools/ToolChrome.module.css Active summary text inherits the row color on hover
packages/web-shell/client/hooks/useSessionArtifacts.ts Stable module-level empty array while no load owner is established
packages/web-shell/client/hooks/useSessionArtifacts.test.tsx Asserts empty-array reference stability across re-renders after a failed load

Testing — the PR's own CI (this run executes no PR code)

At review time the main unit suite (Test ubuntu-latest, ~30 min) and the Web-shell Visuals capture job were still in flight on the reviewed commit; build, desktop-shell, secret-scan, and CVE checks had already landed green. The macOS/Windows test and CLI-integration jobs show "skipped" by design — ci.yml gates them to merge_group, so ubuntu carries the PR suite. The table below reflects the fetch-time state; the finalize job rewrites it once CI settles. Not verified in this run: live-browser layout behavior (single scroll region, hover brightening) — CI runs no browser assertions for it, and the author's evidence is a dev:daemon run on macOS (author's claim, not independently re-run).

Final CI results for cfea416 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle the remaining behavioural claim: @qwen-code /verify — the loop fix is pinned by the hook identity test and a mocked integration render, but no single test reproduces the actual "Maximum update depth exceeded" cascade (a real failing artifacts endpoint feeding the artifacts-keyed consumer effect); an A/B harness would show the cascade on the base build and its absence on this head. The collapse-timing semantics, by contrast, are genuinely pinned by the DOM tests above.

中文说明

代码审查:先独立构思再看 diff,四项修复都落在最简机制上,没有遗漏更简路径。静态验证结论:

  1. 详情面板统一:删除了 getSubagentPrompt 与固定的 prompt 块,不再传 hideFirstUserMessage,两处滚动区(prompt 自带的 240px 滚动盒与 transcript 的固定高度)均已移除,面板是有界 flex 列,MessageList 成为唯一滚动区,头部固定其上。已确认被删符号无其他消费方。
  2. 并行组收起:延迟条件从"响应中所有最新轮组"收窄为"仅拥有等待中通知的组、且确实在等总结叙述"。原本无上界的顺序等待(最终回答先于完成通知上屏)现在复用未匹配完成闩锁的 5 秒宽限,且以本轮自身的 agent 通知 id 为键,监控横幅不会重启或重置它。新的 memo/effect 无反馈回路;被移除的两个 context 字段在重写公式之外无消费方。
  3. 无限更新循环:模块级 EMPTY_ARTIFACTS 常量——标准同一性修复。崩溃路径核实:SubagentDetailContent 的 effect 以 artifacts 为键并在每次同一性变化时调用 onArtifactsChange,旧代码每次渲染返回新 [] 造成级联重渲染。无消费方修改该数组,共享常量安全。
  4. Hover:.chatSummary:hover 已把行颜色提为 --primary,补上 color: inherit 让摘要文字随图标一起变亮,一行改在正确的规则上。

测试是按新语义重写的,不是简单改到通过:两个旧的"响应中延迟"测试被替换为"完成即收起"测试,新增 DOM 测试钉住时序回归(compareDocumentPosition)、4999/5000ms 宽限边界、监控通知不重启规则。未发现阻断问题,无规范违规。

测试证据:本次运行不执行任何 PR 代码,引用的是 PR 自身 CI。审查时主单测套件(ubuntu,约 30 分钟)与 Web-shell Visuals 截图任务仍在进行;构建、desktop-shell、密钥扫描、CVE 审计已通过。macOS/Windows 单测与 CLI 集成任务按 ci.yml 配置仅在 merge_group 触发,属设计内跳过。未验证项:浏览器实际布局效果(单滚动区、hover 变亮)——CI 无相应浏览器断言,作者的证据是 macOS 上的 dev:daemon 实测(作者自述,未独立复核)。上一段已给出可补齐该行为证据的沙箱验证触发方式。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, focused, well-tested; the only thing I can't attest from here is the pixel-level layout behavior in a real browser.

Stepping back: this is four small fixes that each land on the minimal mechanism — a deleted helper and CSS block instead of a new layout system, a deferral formula that got strictly simpler, a module-level constant for the identity bug, and one CSS property. My independent proposal for each problem is what the PR did; I didn't find a cheaper path it missed. The crash fix is the clearest win: I traced the actual cascade (an artifacts-keyed effect calling into the parent on every identity change) and the fix removes it at the root. The collapse work is the trickiest part and it behaves — the tests don't just pass, they encode the new contract: collapse-on-completion mid-response, chronological repositioning, the 5s grace boundary at 4999/5000ms, and monitor banners neither restarting nor re-arming the wait.

The reservation, such as it is: "the panel scrolls as one unit" and "the summary brightens on hover" are CSS claims no test in this PR (or its CI) mechanically pins. The author verified live on macOS and the repo's Web-shell Visuals job captures this surface on every PR — between those, the residual risk is a visual nit, not a behavioral regression. The timing change (groups fold right after completion rather than at turn end) is intentional and disclosed in the PR's own risk section.

If I'm maintaining this in six months, the comments answer the only non-obvious question (why the grace is keyed to the turn's own notification), and the DOM tests replay the whole decision table. That's a thank-the-author diff.

Verdict: approve. CI is still running on the reviewed commit (unit suite + web-shell visuals), so approval is deferred until CI lands green on cfea4162921d9b0e10ce66bde5c3b1a0942e2f40; the finalize job posts the commit-pinned approval once every check completes green, and withholds it if anything lands red.

中文说明

整体回顾:四项小修复都落在最简机制上——删除 helper 与 CSS 块而非新增布局系统、严格变简单的延迟判定公式、模块级常量修同一性 bug、一行 CSS。我对每个问题的独立设想与 PR 实现一致,没有找到更便宜的遗漏路径。崩溃修复收益最明确:实际级联路径(以 artifacts 为键的 effect 在每次同一性变化时回调父组件)已追溯核实,修复从根上消除。收起逻辑是最复杂的部分,但测试不只是通过,而是编码了新契约:响应中完成即收起、回到时序位置、4999/5000ms 宽限边界、监控横幅既不重启也不重置等待。

唯一的保留意见:"整体滚动"与"hover 变亮"是 CSS 层面的断言,本 PR 及其 CI 均无机械化断言钉住。作者已在 macOS 实测,且仓库的 Web-shell Visuals 任务会为每个 PR 截图该界面——两者结合,残余风险是视觉小瑕疵而非行为回归。时序变化(组在完成后立即折叠而非整轮结束)是有意的,且 PR 风险一节已如实披露。

六个月后维护这段代码时,注释回答了唯一不自明的问题(宽限为何以本轮自身通知为键),DOM 测试可回放整个决策表。这是值得感谢的 diff。

结论:通过。审查时该提交的 CI 仍在运行(单测套件 + web-shell 截图),因此批准推迟到 CI 在该提交上全绿后由收尾任务代为提交;若有检查变红则不会批准。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 21, 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 236541c. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

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

Qwen Code · web-shell visuals

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

…ent-panel-fixes

# Conflicts:
#	packages/web-shell/client/components/MessageList.tsx

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blocking findings. CI is pending (Test + Capture web-shell visuals in_progress), so posting a comment now.

Scope: 9 files changed, +402/-166. Reviewed all four fix areas against the implementation and tests.

Checked:

Fix 1 — SubagentDetail unified scroll (SubagentDetail.tsx, .css)

  • Removing hideFirstUserMessage and the <pre className={styles.prompt}\> block is consistent: the first user bubble now shows in the transcript and the fixed-height split region is gone.
  • Note: before the transcript loads there's a brief empty state (the old rootTool.args.prompt fallback is removed). Intentional per PR description; the integration test confirms no pre[class*="prompt"] remains.

Fix 2 — parallel-agents collapse (MessageList.tsx)

  • deferAutomaticCollapse simplified to backgroundSummaryAgentContext.key === displayItem.key && latestTurnAwaitsAgentSummary — no longer holds all latest-turn groups while responding. Matches new tests.
  • New waitForOrderedNarration / orderedSummaryGraceExpired state: the three turnAwaitsBackgroundSummary call sites are consistent — latestTurnHoldsOrderedSummary always passes true (detects the condition), latestTurnAwaitsAgentSummary passes !orderedSummaryGraceExpired, and the applyTurnCollapse option threads the flag through.
  • Grace keyed on latestTurnOrderedNotificationId (agent-completion notification ID, not global latest notification): monitor/shell banners mid-wait don't restart or re-arm the window. Verified by the new test at the end of the suite.
  • backgroundSummaryAgentContext correctly stripped of the now-unused agentNotificationIsLatestBackground and latestBackgroundNotificationInLatestTurn fields, and latestTurnStartIndex dropped from its deps.

Fix 3 — infinite update loop (useSessionArtifacts.ts)

  • EMPTY_ARTIFACTS constant provides a stable reference when loadedOwnerRef.current !== owner. Returning a fresh [] on every render was the root cause of the loop. New test validates reference stability across rerenders.

Fix 4 — hover (ToolChrome.module.css)

  • color: inherit added alongside the existing -webkit-text-fill-color: currentColor rule on [aria-expanded='true'] and :focus-visible selectors. Correct: without color set, the fill would stay at the pre-hover value even when currentColor is referenced.

Cross-check: prior review (qwen-code-ci-bot, DISMISSED) was a plain LGTM with no specific findings; nothing to confirm or refute.

Unreviewed dimension: Test (ubuntu-latest, Node 22.x) and Capture web-shell visuals are still in_progress. The test job covers the changed MessageList, SubagentDetail, and useSessionArtifacts suites — will approve once it lands green.

Reviewed with AI assistance.

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving per request — no blocking findings (reviewed in prior comment). Note: Test (ubuntu-latest, Node 22.x) is still in_progress; the code review itself found no issues.

Reviewed with AI assistance.

@ytahdn

ytahdn commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 4829 passed · 0 failed · 4829 total

Flakiness gate: ✅ 4 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:4829 通过 · 0 失败 · 4829 总计

抖动门:✅ 4 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR 9640 verification — fix(web-shell): settle parallel-agents collapse and unify agent detail transcript

Verdict: merge-ready — 4829/4829 scripted assertions passed, 0 unexpected failures. Verified head: 236541ccba6cf553ba00d0540aa281527a406842 (base tip 2c64ebe980e7d0d9d7a97cf1de6dc47014bc3449). First verification round (no previous-report.md). One non-blocking Suggestion (a test fixture that under-pins one guard; code verified correct) and one observation; details below.

中文摘要
  • 结论: merge-ready。4829 条脚本化断言全部通过,0 个意外失败。
  • A/B 结论: 五项声明全部在 head/base 两臂上得到行为级证明。核心崩溃修复(无限更新循环):base 上真实 SubagentDetail + 真实 useSessionArtifacts + 逐字复制的 App 快照 reducer 在产物列表加载失败时触发失控循环(消费者回调 >60 次/渲染级联,React 打印 "Maximum update depth exceeded"),head 上同一场景 1 次回调即收敛;并行组收起的 6 个行为测试在 base 全红、head 全绿;详情面板的 hideFirstUserMessage/独立 prompt 块在 base 存在、head 移除。
  • Findings: 1 条非阻塞 Suggestion —— "monitor 不重启宽限窗" 的测试把 monitor 放在 t=0,无法区分"宽限被重启"的变体(M4 突变在 PR 套件 128/128 全绿存活);中间时刻(t=4000)到达的 fixture 能抓住该突变(突变上红、head 上绿),建议作者把该 fixture 补进套件。1 条观察:删除 getSubagentPrompt 后,子会话 transcript 加载完成前不再显示父侧截断 prompt(改为加载态),属重构固有取舍,无断言失败。
  • 未覆盖: 视觉渲染(单一滚动区、hover 变亮、面板布局)jsdom 无法断言;真实 daemon E2E 未跑;浅克隆下 fix 提交 cfea4162 不可达,仅验证聚合 diff(含 merge 冲突解决)。

Scope

  • Central claim: opening the side panel on a session whose artifact list cannot load no longer crashes React with an infinite update loop (useSessionArtifacts returns a stable empty array instead of a fresh [] per render).
  • Secondary 1: a parallel-agents group collapses and returns to its chronological position as soon as its agents are terminal, even while the main agent keeps responding; the awaited-summary hold is bounded by a 5 s grace keyed on the turn's own agent-completion notification.
  • Secondary 2: the subagent detail panel renders the prompt as the transcript's own user bubble (no pinned pre.prompt, no hideFirstUserMessage), one scroll unit.
  • Minor (source-level only): hover brightens the running-agent summary text (color: inherit in ToolChrome.module.css).

A/B load-bearing proof

Environment: base worktree at HEAD^1 under tmp/ (removed after capture); the PR touches only packages/web-shell, so root node_modules (react 19.2.4, vitest 3.2.4) and the web-shell-local node_modules (symlinked into the base tree after an A/A load failure on @datafe-open/markdown-chart) are identical on both arms; readlink -f asserted @qwen-code/sdk resolves to the untouched head-tree packages/sdk-typescript/dist. Identical 5-file set (head's versions) run on both arms.

cell head base
loop-cascade.verify.test.tsx — real SubagentDetail + verbatim App snapshot reducer, loadArtifacts rejects ✅ converges: 1 consumer callback, ≤10 renders LOOP_RUNAWAY_DETECTED (>60 callbacks); React logs "Maximum update depth exceeded"
same file — detection control (unstable identity simulated outside the unit) ✅ loops (oracle live) ✅ loops
useSessionArtifacts.test.tsx — stable empty reference expected [] to be [] (identity)
useSessionArtifacts.test.tsx — 9 pre-existing behaviors ✅ 9/9 ✅ 9/9
SubagentDetail.integration.test.tsx — no hideFirstUserMessage, no pre.prompt hideFirstUserMessage is true
MessageList.dom.test.tsx — 6 collapse/grace behavior tests ❌ all 6 (5 new + 1 flipped)
MessageList.dom.test.tsx — 122 pre-existing collapse/virtualizer tests ✅ (no collateral behavior change)
SubagentDetail.test.tsxfindSubagentRootTool ✅ 2/2 ✅ 2/2
totals (5-file set) 143/143 134 pass / 9 red — every red is a claimed flip

Witnesses: evidence/01-loop-cascade-base-runaway.png, evidence/02-loop-cascade-head-converges.png, evidence/03-collapse-dom-base-six-flips.png, evidence/04-collapse-dom-head-all-green.png.

The 9 base reds fail on intended expected-vs-actual values (e.g. aria-expanded 'true' vs 'false'; group summary non-null vs null at grace expiry; textContent still containing "group B task"), not on crashes or import errors. The base arm's 122 green pre-existing DOM tests show the head test file's edits introduce no head-only test infrastructure.

Full-package gate on head: 193 files / 4013 tests, all pass (logs/head-full-suite.log).

Reviewer Test Plan walk-through

  1. Detail panel bubble + one scroll area — bubble half proven (integration assertions flip on base); "scrolls as one unit / scrollbar at panel edge" is layout: .transcript lost its fixed height: min(65vh, 720px) and the pinned .prompt block is gone (source-verified), but jsdom cannot assert real scrolling → visual part not covered.
  2. Parallel collapse while main agent streams — proven by DOM cells (group collapses after a 2 s fake-timer advance with isResponding: true; returns above a later tool call via compareDocumentPosition). Constants: AGENT_SUMMARY_COLLAPSE_DELAY_MS = 400, collapse animation 180 ms; the bounded awaited-summary grace is UNMATCHED_AGENT_COMPLETION_GRACE_MS = 5000 (the plan's "~1.5 s" is the unawaited path; the 5 s bound is what the grace tests pin at 4999/5000 ms).
  3. No artifacts endpoint, no crash, toggles work — crash half proven by the cascade harness; "Processed" turn toggles are exercised by the pre-existing DOM toggle tests (green in the full suite).
  4. Hover brightening — source-level only: .chatSummary:hover sets the button color to var(--primary); the added color: inherit lets .chatSummaryTextActive (gradient + transparent fill at rest) resolve -webkit-text-fill-color: currentColor to the hover color. Rendering not asserted in jsdom.

Mutation matrix (head)

mutation-matrix.sh + patches in mutations/; witness evidence/05-mutation-matrix.png.

mutation (guard removed) suites run result
control (unmutated head) hooks + cascade + DOM + integration 141/141 green
M1 [] literal instead of EMPTY_ARTIFACTS hooks + cascade caught: 2 red (stable-ref test + cascade runaway)
M2 defer latest-turn groups while responding DOM caught: 6 red (4 new collapse tests + 2 pre-existing)
M3 ordered wait unbounded (waitForOrderedNarration: true) DOM caught: exactly the 2 grace tests
M4 grace keyed on global latest background notification DOM + mid-wait fixture PR suite survives (128/128); mid-wait fixture catches (1 red) → coverage gap, see F1
M5 re-add hideFirstUserMessage integration caught: 1 red

No single-hunk survivor besides M4, so no combination row was needed (no layered guards hiding each other among these hunks). Every guard the PR introduces is load-bearing; M4's guard is correct but pinned only by my mid-wait fixture (harness/midwait-monitor.dom.test.tsx, green 129/129 on unmutated head).

Findings

F1 — Suggestion (non-blocking, completeness): the monitor-non-restart test cannot observe a restarted grace. does not restart the awaited-summary grace for a monitor notification lands the monitor banner at t=0, before the 5 s timer has progressed; a mutant that keys the grace on the global latest background notification (restarting the bound on every banner) therefore expires within the test window anyway and passes 128/128. The hazard the guard names in its own comment — a stream of monitor/shell banners arriving mid-wait re-arming the bound and pinning the turn open indefinitely — is only pinned by a fixture that lands the banner at t=4000: that fixture is red on the mutant and green on head (matrix row M4). Suggested action: move the monitor arrival in the existing test (or add the mid-wait variant, provided verbatim in harness/midwait-monitor.dom.test.tsx). The code as shipped is correct; this is a missing pin, not a defect.

F2 — Observation: the pre-transcript prompt fallback is gone. Base showed the parent tool call's (truncated) prompt in the pinned pre.prompt before the child transcript loaded (getSubagentPrompt fallback, pinned by the deleted unit tests). Head shows the loading state until blocks arrive, then the full first user bubble. Inherent to the redesign; no assertion fails; flagged so the tradeoff is a decision, not a surprise.

Not covered

  • Visual rendering: single scroll unit / scrollbar placement, hover brightening, panel layout — jsdom cannot assert layout; the package's playwright visual e2e was not run.
  • Real-daemon E2E (dev:daemon) not run; the cascade harness reproduces the root-cause mechanism (unstable identity + the App snapshot reducer's always-new-Map path) with the real hook and real component, and React's own "Maximum update depth exceeded" message was observed on the unbounded variant under act(). The exact browser-flush crash string from the issue is not reproduced end-to-end (shape and cause both reproduced at the mechanism level; the production flush path differs from act's).
  • Per-commit attribution: shallow checkout (depth 2) makes the fix commit cfea4162 unreachable (git rev-list HEAD^1..HEAD^2 returns only the merge commit); the aggregate HEAD^1..HEAD diff was verified, including the merge-conflict resolution in MessageList.tsx.
  • Under act(), the unfixed loop does not surface React's depth guard as a throw — it allocates until the worker OOMs (observed once before the sentinel cap was added); the bounded LOOP_RUNAWAY_DETECTED sentinel is the scripted oracle.

Methodology

Container node:22-bookworm, react 19.2.4, vitest 3.2.4; head tree pre-built by CI. A/B base arm = scratch worktree at HEAD^1 with root node_modules resolved by walk-up and web-shell-local deps symlinked (realpath-asserted; PR touches only packages/web-shell). Harnesses are mock-free w.r.t. the unit under test: loop-cascade.verify.test.tsx mounts the real SubagentDetail + real useSessionArtifacts against a verbatim copy of App.tsx's setPaneArtifactSnapshots reducer, stubbing only the SDK seam and MessageList; the DOM suite drives the real MessageList. Mutation matrix applies/reverts unified patches in mutations/. Raw logs in logs/ (base-arm, head-arm, head full suite); harnesses in harness/; captures in evidence/. Assertion ledger: head full suite 4013 + base-arm green 134 + A/B inversion cells 9 + matrix control 141 + matrix green 514 + matrix caught 12 + mid-wait-on-head 1 + gate liveness 2 + gates clean 3 = 4829 pass, 0 fail.

Flakiness gate log

rounds=5 files=4 skipped=0
file packages/web-shell/client/components/MessageList.dom.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/MessageList.dom.test.tsx
file packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/artifacts/SubagentDetail.integration.test.tsx
file packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/artifacts/SubagentDetail.test.tsx
file packages/web-shell/client/hooks/useSessionArtifacts.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/hooks/useSessionArtifacts.test.tsx


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/web-shell/client/components/MessageList.dom.test.tsx: PPPPP
  packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx: PPPPP
  packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx: PPPPP
  packages/web-shell/client/hooks/useSessionArtifacts.test.tsx: PPPPP

verdict: pass
summary: 4 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/web-shell/client/components/MessageList.dom.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/hooks/useSessionArtifacts.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/MessageList.dom.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/hooks/useSessionArtifacts.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/MessageList.dom.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/hooks/useSessionArtifacts.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/MessageList.dom.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/hooks/useSessionArtifacts.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/MessageList.dom.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/artifacts/SubagentDetail.integration.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/artifacts/SubagentDetail.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/hooks/useSessionArtifacts.test.tsx: P (exit 0)

Evidence images

01-loop-cascade-base-runaway

02-loop-cascade-head-converges

03-collapse-dom-base-six-flips

04-collapse-dom-head-all-green

05-mutation-matrix

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@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 — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines 238 to 239
hideSessionTimeline
hideFirstUserMessage
firstTurnMetrics={metrics}

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] Removing hideFirstUserMessage here deletes the prop's only production setter, leaving MessageList's prop declaration (MessageList.tsx:125), its default, the first-user-message filter branch (~line 3448), and its memo deps (3356, 3485) as dead code. MessageList is not exported from the package's public entry (client/index.tsx), so no external embedder can set it — only DOM-test fixtures (MessageList.dom.test.tsx:365, 791) still pass it. The cost is an always-false dead switch plus an unreachable filter branch in a ~5,300-line component, and the DOM test exercises a path no production consumer can reach, so a future reader will infer that first-user-message hiding is a live mode worth preserving or building on. Consider deleting the prop, the filter branch and memo deps, and the DOM-test usage in this PR — or note that a near-term caller will use it.

中文说明

在此处移除 hideFirstUserMessage 后,该 prop 的唯一生产调用方被删除,MessageList 中的 prop 声明(MessageList.tsx:125)、默认值、首条用户消息过滤分支(约 3448 行)及其 memo 依赖(3356、3485)全部成为死代码。MessageList 并未从包的公开入口(client/index.tsx)导出,外部使用方无法设置该 prop——如今只有 DOM 测试 fixture(MessageList.dom.test.tsx:365、791)仍在传入它。代价是一个恒为 false 的死开关和一条不可达的过滤分支留在这个约 5,300 行的组件中,且 DOM 测试仍在覆盖一条生产环境不可达的路径,未来读者会误以为"隐藏首条用户消息"是一个值得保留或可以继续使用的有效模式。建议在本 PR 中删除该 prop、过滤分支、memo 依赖及 DOM 测试用法——或说明近期会有调用方使用它。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +3032 to +3039
useEffect(() => {
if (!latestTurnHoldsOrderedSummary) return;
const timer = setTimeout(
() => setOrderedSummaryGraceExpired(true),
UNMATCHED_AGENT_COMPLETION_GRACE_MS,
);
return () => clearTimeout(timer);
}, [

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 ordered-summary grace (latch state + reset effect + this timer effect) is a near-verbatim second copy of the unmatched-completion grace latch ~40 lines above, and the two copies already encode the isResponding gate differently (the older block guards inline inside its timer effect; this one folds !isResponding into its hold memo). UNMATCHED_AGENT_COMPLETION_GRACE_MS now bounds both waits while its name still claims exclusivity to the unmatched path. Any future tuning — lengthening the window for a slow summary narration, changing re-arm semantics — must then be applied in two places, a one-sided edit silently diverges the two latches, and editing the shared constant silently moves both windows. Consider extracting one small hook local to this file (latch + reset + timer) used by both graces, and renaming the constant to reflect its shared scope (e.g. BACKGROUND_SUMMARY_GRACE_MS).

中文说明

这个 ordered-summary 宽限机制(latch 状态 + 重置 effect + 此定时 effect)是约 40 行上方 unmatched-completion 宽限 latch 的近乎逐字复制,且两份副本对 isResponding 门控的编码方式已经不同(旧的一块在其定时 effect 内联守卫,这一块把 !isResponding 合并进了 hold memo)。UNMATCHED_AGENT_COMPLETION_GRACE_MS 现在同时约束两个等待,但其名称仍声称仅用于 unmatched 路径。未来任何调参——为较慢的总结叙述加长窗口、修改重新武装语义——都必须改两处,单边修改会让两个 latch 悄然分叉,而修改这个共享常量也会悄然同时移动两个窗口。建议抽取一个本文件内的小 hook(latch + 重置 + 定时器)供两个宽限共用,并将常量重命名为反映其共享作用域的名称(如 BACKGROUND_SUMMARY_GRACE_MS)。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +630 to +632
* arrives cannot pin the turn (and its parallel-agents group) open forever.
*/
waitForOrderedNarration?: boolean;

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 new grace latch keys on agentNotificationsOnly=true (via latestTurnBackgroundSummaryState) while applyTurnCollapse evaluates the ordering rule with agentNotificationsOnly=false. A trailing monitor/shell-task banner landing after the final answer therefore keeps the turn-level ordering wait true, but latestTurnHoldsOrderedSummary requires sawAgentCompletion, so the latch never arms, orderedSummaryGraceExpired stays false, and the wait remains unbounded — while this JSDoc promises that a summary that never arrives "cannot pin the turn (and its parallel-agents group) open forever". A probe on this commit shows the split:

same turn, no trailing monitor:      toggle=false, group=null  -> turn folds normally
trailing monitor after final answer: toggle=true,  group=false -> still pinned open at 30s (6x the 5s grace)

The pin itself predates this PR (manual collapse and a model reply to the banner both release it), so the minimal fix is to narrow this doc and the latch comment to matched agent completions; the fuller fix is to key the latch on the same all-notifications view applyTurnCollapse uses — the notification-id key already keeps monitor banners from re-arming it.

中文说明

新的宽限 latch 以 agentNotificationsOnly=true(经由 latestTurnBackgroundSummaryState)为键,而 applyTurnCollapseagentNotificationsOnly=false 评估排序规则。因此,落在最终回答之后的 monitor/shell-task 横幅仍会让轮级排序等待为真,但 latestTurnHoldsOrderedSummary 要求 sawAgentCompletion,latch 永远不会武装,orderedSummaryGraceExpired 保持 false,该等待仍然无界——而这段 JSDoc 却承诺"永远不到达的总结不会把该轮(及其并行智能体组)无限期钉在展开状态"。在本次提交上的探测显示了这一差异:

同一轮、无尾部 monitor:      toggle=false, group=null  -> 轮正常折叠
最终回答后出现尾部 monitor:  toggle=true,  group=false -> 30 秒后仍钉在展开状态(5 秒宽限的 6 倍)

该钉住行为在本 PR 之前就存在(手动折叠或对横幅的模型回复都能解除),因此最小修复是收窄此处文档与 latch 注释,说明该界限仅适用于匹配的 agent 完成通知;更完整的修复是让 latch 使用与 applyTurnCollapse 一致的全通知视图——通知 id 键已能防止 monitor 横幅重新武装它。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +4508 to +4513
// A monitor banner lands mid-wait. It is not the awaited agent summary,
// so it must neither restart the 5s bound nor re-arm an expired one;
// the group still collapses when the grace window closes.
rerenderMessages(c, [...settled, monitorNotificationMsg('monitor')], {
isResponding: 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] This test cannot discriminate the regression it names: the monitor banner lands in a re-render immediately after the grace arms, with zero simulated time between the two renders, and no monitor arrival after expiry is ever exercised (the comment's "re-arm an expired one" half). A mutation probe on this commit confirms it — flipping the grace state memo's agentNotificationsOnly argument to false (making the grace key on the latest background notification of any kind) passes this test, the full file (128/128), and the whole web-shell suite, because both renders happen at simulated t=0 so the mutant's re-armed timer and the correct timer expire at the same instant. If that keying regresses later, repeated monitor banners arriving <5s apart restart the bound indefinitely and pin the turn open forever — the exact failure mode this PR fixes — while this test stays green. Advance ~3s of simulated time before the banner lands (and/or move a monitor arrival after expiry and assert the summary row does not reappear); verified that with the 3s advance the mutant fails (at t=7000 the group summary is still in the DOM where the test expects null) while the correct code passes:

act(() => vi.advanceTimersByTime(3_000));
rerenderMessages(c, [...settled, monitorNotificationMsg('monitor')], {
  isResponding: false,
});
中文说明

该测试无法判别它命名的回归:monitor 横幅在宽限武装后紧接着的一次重渲染中到达,两次渲染之间模拟时间为零,且从未测试过到期之后才到达的情况(即注释中"re-arm an expired one"的那一半)。在本次提交上做的变异探测证实了这一点——把宽限状态 memo 的 agentNotificationsOnly 参数翻转为 false(使宽限以任意类型的最新后台通知为键)后,本测试、整个文件(128/128)乃至整个 web-shell 套件全部通过,因为两次渲染都发生在模拟时间 t=0,变异代码重新武装的定时器与正确代码的定时器在同一时刻到期。如果该键控将来回归,间隔小于 5 秒反复到达的 monitor 横幅会无限重启该界限,把该轮永远钉在展开状态——正是本 PR 修复的失效模式——而本测试仍然是绿的。建议在横幅到达前先推进约 3 秒模拟时间(和/或把横幅到达移到到期之后,断言摘要行不再重新出现);已验证:先推进 3 秒后,变异代码在 t=7000 时组摘要仍在 DOM 中(测试期望为 null)而失败,正确代码通过:

— qwen3.8-max via Qwen Code /review (v0.21.15)

@ytahdn
ytahdn added this pull request to the merge queue Aug 21, 2026
Merged via the queue into QwenLM:main with commit ef86d1f Aug 21, 2026
135 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.0.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants