Skip to content

fix(webui): make long tool output collapsible - #8251

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
destire-mio:codex/feat-export-collapsible-output-8208
Aug 1, 2026
Merged

fix(webui): make long tool output collapsible#8251
wenshao merged 2 commits into
QwenLM:mainfrom
destire-mio:codex/feat-export-collapsible-output-8208

Conversation

@destire-mio

Copy link
Copy Markdown
Contributor

What this PR does

This PR replaces the 500-character hard truncation for successful Bash/Execute output and long think content with an expandable view that keeps the complete text while remaining collapsed by default. It extracts the existing generic-tool collapse UI into a renderer-agnostic shared component, preserves GenericToolCall's current threshold and rendering, keeps Shell's OUT-row full-content action, and prevents the nested expand/collapse button from triggering that row action.

For Shell output, the OUT scroll container now owns horizontal overflow so a long single-line result remains scrollable in both Bash and Execute variants instead of being clipped by the nested <pre>.

Why it's needed

The shared WebUI, including exported conversations, currently replaces everything after 500 characters with an ellipsis for these paths. That permanently removes the tail of command output and reasoning traces, so users cannot inspect the complete result even after opening the exported page. Keeping the full content behind a compact default view preserves readability without losing diagnostic information.

Reviewer Test Plan

How to verify

  1. Render or export a completed Bash tool call with output longer than 500 characters and a unique marker at the end. Confirm it initially shows Show more, expanding reveals the marker, and collapsing restores the compact view.
  2. Repeat with an Execute tool call and a think tool call longer than 500 characters.
  3. Use a Shell output containing one long line without whitespace. Confirm the OUT content can scroll horizontally after expansion.
  4. Click the Shell expand/collapse button and confirm it does not invoke the surrounding OUT-row action; then click the OUT row and confirm it still opens the complete output.
  5. Confirm exactly 500 characters do not receive the new toggle and that error, empty-output, and short-thinking paths retain their existing behavior.

Automated verification from packages/webui: npm exec vitest run src/components/toolcalls/shared/CollapsibleOutput.test.tsx src/components/toolcalls/ShellToolCall.test.tsx src/components/toolcalls/ThinkToolCall.test.tsx; full npm test; npm run lint; npm run typecheck; npm run build.

Evidence (Before & After)

Before: failure-first tests for the three long-output paths could not find their unique tail markers because the renderers replaced content after 500 characters with .... A real Chrome layout probe for a long Shell line measured the scroll container at clientWidth/scrollWidth = 200/200 while the nested <pre> overflowed, so the hidden tail was not horizontally reachable.

After: the focused regression suite passes 7/7 and the full WebUI suite passes 398/398. The same Chrome probe measures clientWidth/scrollWidth = 200/831 for both Bash and Execute, with the complete tail preserved and reachable. A local /export screenshot is not presented as evidence because the normal export page loads the published WebUI bundle from unpkg rather than this worktree bundle; the included E2E plan records the release-bundle manual scenario.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS 26.3.1, Node.js 24.14.0, Vitest 3.2.4, system Google Chrome in headless mode.

Risk & Scope

  • Main risk or tradeoff: the shared wrapper is consumed by multiple WebUI surfaces, so regressions could affect exported conversations and other @qwen-code/webui consumers; the existing GenericToolCall behavior is preserved and covered through the full WebUI suite.
  • Not validated / out of scope: a global Expand All/Collapse All control, changes to the existing <=500/error-output collapse policy, and a release-bundle /export smoke test are outside this bounded fix.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #8208

中文说明

本 PR 做了什么

本 PR 将成功的 Bash/Execute 输出和较长 think 内容原有的 500 字符硬截断,替换为保留完整文本的可展开视图,同时默认保持折叠。它把 GenericToolCall 现有的折叠 UI 提取为与渲染器无关的共享组件,保留 GenericToolCall 当前的阈值和渲染行为,保留 Shell 的 OUT 行全文打开操作,并阻止内层展开/收起按钮触发外层 OUT 行操作。

对于 Shell 输出,现在由 OUT 滚动容器负责横向溢出,因此无论 Bash 还是 Execute,长单行结果都能横向滚动,不再被内层 <pre> 裁掉。

为什么需要它

共享 WebUI(包括导出的会话)目前会在这些路径中把 500 字符之后的全部内容替换为省略号。这会永久丢失命令输出和思考轨迹的尾部,用户即使打开导出页面也无法检查完整结果。默认紧凑显示、按需展开完整内容,可以兼顾可读性和诊断信息的完整性。

Reviewer 测试计划

如何验证

  1. 渲染或导出一个输出超过 500 字符、末尾带唯一标记的已完成 Bash 工具调用。确认初始显示 Show more,展开后能看到尾部标记,收起后恢复紧凑视图。
  2. 对 Execute 工具调用和超过 500 字符的 think 工具调用重复上述验证。
  3. 使用一条不含空白的超长 Shell 单行输出,确认展开后 OUT 内容可以横向滚动。
  4. 点击 Shell 展开/收起按钮,确认不会触发外层 OUT 行操作;再点击 OUT 行,确认仍会打开完整输出。
  5. 确认恰好 500 字符时不会出现新开关,并且错误、空输出和短思考路径保持原有行为。

packages/webui 中执行的自动验证:npm exec vitest run src/components/toolcalls/shared/CollapsibleOutput.test.tsx src/components/toolcalls/ShellToolCall.test.tsx src/components/toolcalls/ThinkToolCall.test.tsx;完整 npm testnpm run lintnpm run typechecknpm run build

证据(修改前与修改后)

修改前:三个长输出路径的失败优先测试都找不到各自唯一的尾部标记,因为渲染器把 500 字符后的内容替换为 ...。真实 Chrome 对长 Shell 单行内容的布局探针显示,滚动容器为 clientWidth/scrollWidth = 200/200,而内层 <pre> 已溢出,因此被隐藏的尾部无法通过横向滚动访问。

修改后:focused 回归测试 7/7 通过,完整 WebUI 测试 398/398 通过。同一个 Chrome 探针在 Bash 和 Execute 中都测得 clientWidth/scrollWidth = 200/831,完整尾部已保留并可访问。这里没有把本地 /export 截图作为证据,因为常规导出页面会从 unpkg 加载已发布的 WebUI bundle,而不是当前 worktree 的 bundle;随 PR 提交的 E2E 计划记录了发布 bundle 可用后的手动验证场景。

已测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS 26.3.1、Node.js 24.14.0、Vitest 3.2.4、无头模式下的系统 Google Chrome。

风险与范围

  • 主要风险或权衡:共享包装组件会被多个 WebUI 表面使用,因此回归可能影响导出会话和其他 @qwen-code/webui 消费方;本 PR 保留了 GenericToolCall 的现有行为,并通过完整 WebUI 测试覆盖。
  • 未验证 / 范围外:全局 Expand All/Collapse All 控件、现有 <=500/错误输出折叠策略的修改,以及发布 bundle 的 /export 冒烟测试不属于这个边界明确的修复。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Refs #8208

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

  • Template: complete ✓ — all required sections present, including the bilingual summary.
  • Problem: real and observed, not theoretical. I confirmed the 500-character hard truncation in the base code — ShellToolCall.tsx and ThinkToolCall.tsx both do substring(0, 500) + '...', which permanently drops the tail of command output and reasoning traces in the shared WebUI / exported conversations. The PR carries failure-first tests and a Chrome layout probe as before/after evidence, and it references the open welcome-pr issue (/export HTML should show thinking and tool results with expand/collapse #8208, roadmap/export-data).
  • Direction: aligned. /export HTML should show thinking and tool results with expand/collapse #8208 explicitly asks for full content preserved behind a collapsed-by-default expand/collapse view, and this PR is a bounded piece of that — replacing the lossy truncation rather than rewriting the export renderer. The issue's roadmap/export-data label confirms the area is wanted.
  • Size: not applicable — no core-infrastructure paths are touched (everything is under packages/webui/src/components/toolcalls/ plus a Playwright harness under packages/web-shell/client/e2e/). Production logic is well under the thresholds; the rest is tests and an E2E plan doc.
  • Approach: the scope feels right. Rather than adding a parallel utility, it extracts the collapse UI that already lived locally inside GenericToolCall into a shared, renderer-agnostic CollapsibleOutput (takes children instead of content) and reuses it in ShellToolCall and ThinkToolCall. GenericToolCall's existing 400-char threshold and Markdown rendering are preserved, and the diff carries no drive-by refactors or unrelated churn. The follow-up commit ("contain collapsible shell output") stays in scope too — it fixes a layout regression the first commit introduced (a long single line widening the message box) with a targeted CSS containment fix and a real-browser layout test, nothing more.
  • Risk: no elevated risk signals — none of the changed files match the high-revert-path patterns.

Moving on to code review. 🔍

中文说明

感谢贡献!

  • 模板:完整 ✓,包含所有必需章节及中文翻译。
  • 问题:真实且已观测到,不是理论问题。我在 base 代码中确认了 500 字符硬截断——ShellToolCall.tsxThinkToolCall.tsx 都执行 substring(0, 500) + '...',会在共享 WebUI / 导出会话中永久丢失命令输出和推理轨迹的尾部。PR 提供了失败优先测试和 Chrome 布局探针作为 before/after 证据,并关联了开放的 welcome-pr issue(/export HTML should show thinking and tool results with expand/collapse #8208roadmap/export-data)。
  • 方向:对齐。/export HTML should show thinking and tool results with expand/collapse #8208 明确要求保留完整内容、默认折叠的可展开视图,本 PR 是其中一个有边界的改动——替换有损截断,而非重写导出渲染器。issue 的 roadmap/export-data 标签确认该方向是被需要的。
  • 规模:不适用——未触及核心基础设施路径(全部位于 packages/webui/src/components/toolcalls/,外加 packages/web-shell/client/e2e/ 下的 Playwright harness)。生产逻辑远低于阈值,其余为测试和 E2E 计划文档。
  • 方案:范围合理。它没有新增平行工具,而是把原本位于 GenericToolCall 内部的折叠 UI 提取为与渲染器无关的共享组件 CollapsibleOutput(接收 children 而非 content),并在 ShellToolCallThinkToolCall 中复用。GenericToolCall 现有的 400 字符阈值和 Markdown 渲染得以保留,diff 中没有顺手重构或无关改动。后续 commit("contain collapsible shell output")同样在范围内——它用一个有针对性的 CSS 容器约束修复和一个真实浏览器布局测试,修复了首个 commit 引入的布局回归(长单行撑宽消息框),没有多余改动。
  • 风险:无升级风险信号——改动文件均未命中高回滚路径模式。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Before reading the diff, my independent take was: the cleanest fix is to stop truncating and reuse the collapse UI that already exists locally in GenericToolCall — extract it into a shared component and apply it to the Shell and Think renderers. That is exactly what this PR does, and it does it well.

  • Truncation removed, full content kept. Both ShellToolCall.tsx and ThinkToolCall.tsx drop the substring(0, 500) + '...' path and render the complete output / thoughts, wrapped in the new shared CollapsibleOutput only when longer than 500 chars. The 500 boundary itself correctly gets no toggle (covered by a test).
  • The nested-toggle hazard is handled. The Shell OUT row has its own onClick (open full output in a temp file); the new toggle calls event.stopPropagation(), so expanding/collapsing no longer triggers that row action, while clicking the OUT row still opens the complete output. The Shell test asserts both — openTempFile is not called on toggle click, and is called with the full output on row click.
  • The CSS change is sound. Adding ${classPrefix}-toolcall-full to the row-content disables the base :not(...-full) 60px fade so CollapsibleOutput owns the collapse with matching 60px/40px values (no double fade), and scoping overflow: visible to .-toolcall-output-subtle .-toolcall-pre hands horizontal overflow to the existing overflow-x: auto container so a long single line stays scrollable. The IN-row and error <pre> are not inside output-subtle, so they are unaffected.
  • GenericToolCall behavior is preserved — same 400-char threshold, same MarkdownRenderer child, no dead code left behind (COLLAPSED_HEIGHT and the useState import are cleaned up). Accessibility is a small improvement: the toggle now exposes aria-expanded / aria-label.

Follow-up commit ("contain collapsible shell output"). The first revision had a layout regression: a long single-line result made the output box expand to ~5.7k px and widened the enclosing message. The follow-up fixes it the right way:

  • min-width: 0 containment. It adds min-width: 0 to .bash/execute-toolcall-row-content.-toolcall-full. A flex child defaults to min-width: auto, so it refuses to shrink below its content's intrinsic width — that is what forced the message to widen. Setting min-width: 0 lets the row shrink and hands the overflow to the inner overflow-x: auto container. It is scoped to the -full (collapsible) variant only, so short output is untouched. I confirmed the final CSS chain is coherent: row-content (min-width: 0) → output-subtle (overflow-x: auto; min-width: 0) → pre (overflow: visible).
  • DRY refactor. The duplicated <div class=...-output-subtle><pre>…</pre></div> block in the collapsible and non-collapsible branches is hoisted into a single outputContent const and reused. Behavior-preserving, removes duplication.
  • A brittle assertion replaced with a real one. The old unit test read ShellToolCall.css as a string and matched an exact CSS rule; that is removed in favor of a real-Chromium Playwright layout test (webui-tool-output-layout.spec.ts, tagged @smoke) that asserts the actual behavior — output scrollWidth > clientWidth, the message container does not widen (messagesScrollWidth == messagesClientWidth), the toggle stays inside the message bounds, and a non-zero scrollLeft is reachable, in both collapsed and expanded states. That is strictly stronger evidence. The harness (webui-tool-output-layout-harness.html / .tsx) follows the repo's existing composer-layout-harness convention exactly (same #root render, same /e2e/*.html?param navigation, same @smoke tag) and imports ChatViewer / ChatMessageData, which are genuine public exports of @qwen-code/webui.
  • GenericToolCall.test.tsx added — pins the preserved 400-char collapse behavior and the no-toggle-at-boundary case for the component the shared extraction touched.

No correctness bugs, security concerns, or regressions found; conventions are followed (collocated tests, ESM, PascalCase.tsx, established e2e harness pattern). Nothing rises to a blocker.

Testing evidence

This is an unattended CI run, so I did not build or execute the PR's code — the signal below is the PR's own CI on the reviewed commit, read via the API. CI is fully green on this head, including the primary unit suite and the web-shell E2E smoke suite that runs the new layout spec.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
precheck-pr / precheck ✅ success

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

The new failure-first component tests (CollapsibleOutput, GenericToolCall, ShellToolCall, ThinkToolCall) run inside the ubuntu unit suite and pin the rendering behavior, and the new Playwright layout cases run inside web-shell E2E Smoke in real Chromium — so both the truncation fix and the layout containment are now substantiated by CI, not just claimed.

The one surface still not exercised end-to-end is the literal /export page, which loads the published WebUI bundle from unpkg rather than this branch's build — that path can only be smoke-tested after a release. The layout claim itself is now pinned by the in-CI Playwright test, so this is a release-time follow-up, not a merge gate. If a maintainer wants it settled before release, @qwen-code /verify runs as a sponsored run (a maintainer comment approves the head it was written against, with a pre-execution risk screen and workspace wipe) — read the resulting report with the same skepticism as the fork's own CI logs, since the code under verification is adversarial input.

中文说明

代码审查

在读 diff 之前,我的独立判断是:最干净的修复是停止截断,并复用 GenericToolCall 中已有的本地折叠 UI——把它提取为共享组件,应用到 Shell 和 Think 渲染器。本 PR 正是这么做的,而且做得很好。

  • 移除截断,保留完整内容。 ShellToolCall.tsxThinkToolCall.tsx 都去掉了 substring(0, 500) + '...',渲染完整的 output / thoughts,仅在超过 500 字符时用新的共享 CollapsibleOutput 包裹。500 字符边界本身正确地不出现开关(有测试覆盖)。
  • 处理了内层开关的隐患。 Shell 的 OUT 行自身有 onClick(在临时文件中打开完整输出);新开关调用 event.stopPropagation(),因此展开/收起不再触发外层 OUT 行操作,而点击 OUT 行仍会打开完整输出。Shell 测试对两者都做了断言。
  • CSS 改动合理。 给 row-content 加上 ${classPrefix}-toolcall-full 会禁用 base 的 :not(...-full) 60px 淡出,使 CollapsibleOutput 以匹配的 60px/40px 值独占折叠(无双重淡出);把 overflow: visible 限定在 .-toolcall-output-subtle .-toolcall-pre 上,将横向溢出交给已有的 overflow-x: auto 容器。IN 行和 error 的 <pre> 不在 output-subtle 内,因此不受影响。
  • GenericToolCall 行为得以保留——同样的 400 字符阈值、同样的 MarkdownRenderer 子元素,没有遗留死代码。无障碍性有小改进:开关现在暴露 aria-expanded / aria-label

后续 commit("contain collapsible shell output")。 首个修订有一个布局回归:长单行结果会让输出框扩展到约 5.7k 像素并撑宽外层消息。后续 commit 用正确的方式修复了它:

  • min-width: 0 容器约束。 它给 .bash/execute-toolcall-row-content.-toolcall-full 加上 min-width: 0。flex 子元素默认 min-width: auto,因此不会缩小到内容固有宽度以下——这正是消息被撑宽的原因。设为 min-width: 0 让该行可以收缩,把溢出交给内层 overflow-x: auto 容器。它只限定在 -full(可折叠)变体上,因此短输出不受影响。我确认最终的 CSS 链是自洽的:row-content(min-width: 0)→ output-subtle(overflow-x: auto; min-width: 0)→ pre(overflow: visible)。
  • DRY 重构。 可折叠与不可折叠两个分支中重复的 <div class=...-output-subtle><pre>…</pre></div> 块被提取为单个 outputContent 常量并复用。行为保持不变,消除了重复。
  • 用真实断言替换脆弱断言。 旧的单元测试把 ShellToolCall.css 当字符串读取并精确匹配某条 CSS 规则;它被移除,取而代之的是一个真实 Chromium 的 Playwright 布局测试(webui-tool-output-layout.spec.ts,标记 @smoke),断言真实行为——输出 scrollWidth > clientWidth、消息容器被撑宽(messagesScrollWidth == messagesClientWidth)、开关保持在消息边界内、并且在折叠与展开两种状态下都可达到非零 scrollLeft。这是严格更强的证据。harness(webui-tool-output-layout-harness.html / .tsx)完全遵循仓库现有的 composer-layout-harness 约定(同样的 #root 渲染、同样的 /e2e/*.html?param 导航、同样的 @smoke 标签),并导入 ChatViewer / ChatMessageData——它们确实是 @qwen-code/webui 的公开导出。
  • 新增 GenericToolCall.test.tsx——为共享提取所触及的组件钉住保留的 400 字符折叠行为以及边界处无开关的情况。

未发现正确性 bug、安全问题或回归;遵循项目约定(同目录测试、ESM、PascalCase.tsx、既有的 e2e harness 模式)。没有阻塞项。

测试证据

这是无人值守的 CI 运行,因此我没有构建或执行 PR 的代码——以下信号来自通过 API 读取的、该被审查 commit 上 PR 自身的 CI。该 head 上 CI 全绿,包括主单元测试套件和运行新布局 spec 的 web-shell E2E smoke 套件。

新的失败优先组件测试(CollapsibleOutputGenericToolCallShellToolCallThinkToolCall)在 ubuntu 单元套件中运行并钉住渲染行为,新的 Playwright 布局用例在真实 Chromium 的 web-shell E2E Smoke 中运行——因此截断修复和布局容器约束现在都由 CI 证实,而不仅是声明。

唯一仍未端到端验证的表面是字面意义上的 /export 页面,它从 unpkg 加载已发布的 WebUI bundle,而非本分支的构建——该路径只能在发布后做冒烟测试。布局结论本身现已由 CI 内的 Playwright 测试钉住,因此这属于发布时的后续事项,而非合并门槛。如果维护者想在发布前补齐,@qwen-code /verify 可作为赞助运行(维护者评论批准其写入时的 head,并带执行前风险筛查与工作区清理)——请以与 fork 自身 CI 日志相同的怀疑态度阅读生成的报告,因为被验证的代码是对抗性输入。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage; CI is green on the reviewed commit and I would merge without hesitation.

Stepping back: this solves a real, observable problem — the shared WebUI was silently throwing away everything past 500 characters of shell output and reasoning traces, including in exported conversations — and it does so with the minimum viable change. Instead of bolting on a new widget, it extracts the collapse behavior that already lived inside GenericToolCall and reuses it, preserving that component's existing thresholds and rendering. The two fiddly bits — the nested toggle not firing the OUT-row action, and the long single line staying horizontally scrollable — are both handled and both pinned by tests.

The follow-up commit turned my one real reservation into a strength. The first revision had a layout regression (a long single line widening the message box); the follow-up contains it with the correct flexbox min-width: 0 fix and, rather than asserting a CSS string, adds a real-Chromium Playwright test that proves the message no longer widens and the output stays scrollable. That is exactly the evidence I wanted, and it now runs green in CI. If I had to maintain this in six months I'd thank the author: it's small, the intent is obvious, and the failure-first tests guard the boundaries.

CI is fully green on this commit (primary unit suite plus the web-shell E2E smoke suite that runs the new layout cases), so I'm approving now rather than deferring. The only residual is non-blocking and unfixable here: the live /export page pulls the published bundle from unpkg, so the release-bundle rendering can only be smoke-tested after a release — worth a quick browser look then, but not a merge gate.

中文说明

置信度:5/5 —— 每个阶段都干净;被审查 commit 上 CI 全绿,我会毫不犹豫地合并。

退一步看:这解决了一个真实、可观测的问题——共享 WebUI 会静默丢弃 shell 输出和推理轨迹 500 字符之后的全部内容,导出会话也不例外——而且用最小可行改动做到。它没有新加组件,而是提取 GenericToolCall 中已有的折叠行为并复用,保留了该组件现有的阈值和渲染。两个棘手的点——内层开关不触发 OUT 行操作、长单行仍可横向滚动——都被处理并有测试钉住。

后续 commit 把我唯一真正的保留变成了优点。首个修订有一个布局回归(长单行撑宽消息框);后续 commit 用正确的 flexbox min-width: 0 修复约束了它,并且不再断言 CSS 字符串,而是新增一个真实 Chromium 的 Playwright 测试,证明消息不再被撑宽、输出仍可横向滚动。这正是我之前想要的证据,而且现在在 CI 中变绿。如果半年后由我维护,我会感谢作者:改动小、意图清晰、失败优先测试守住了边界。

该 commit 上 CI 全绿(主单元测试套件,以及运行新布局用例的 web-shell E2E smoke 套件),因此我现在批准,而不再推迟。唯一的残留是非阻塞且此处无法修复的:线上 /export 页面从 unpkg 拉取已发布的 bundle,因此发布 bundle 的渲染只能在发布后做冒烟测试——届时值得快速做一次浏览器检查,但不是合并门槛。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 6 render-shaping files:

  • packages/webui/src/components/toolcalls/GenericToolCall.tsx
  • packages/webui/src/components/toolcalls/ShellToolCall.css
  • packages/webui/src/components/toolcalls/ShellToolCall.stories.tsx
  • packages/webui/src/components/toolcalls/ShellToolCall.tsx
  • packages/webui/src/components/toolcalls/ThinkToolCall.tsx
  • packages/webui/src/components/toolcalls/shared/CollapsibleOutput.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

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

Qwen Code · web-shell visuals

@yiliang114

Copy link
Copy Markdown
Collaborator

Thanks @destire-mio for the contribution! The shared-component extraction and the stopPropagation handling for the nested toggle are clean — appreciate the failure-first tests too.

One request (non-blocking): could you add before/after screenshots showing the collapsed and expanded states? I understand the /export page pulls the published bundle from unpkg so it won't reflect this branch, but a local dev-server capture (e.g. via Storybook or a manually seeded session) would make the visual change much easier to review at a glance — especially the horizontal scroll behavior for long single-line output.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

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

Comment on lines +258 to +263
{isCollapsible ? (
<CollapsibleOutput
isCollapsible
collapsedHeight={60}
fadeStart={40}
>

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 output-subtle div + pre block is duplicated verbatim across both branches of the isCollapsible ternary — Concrete cost: a future change to the output rendering (class rename, data attribute, syntax highlighting) must be applied to both copies independently; missing one produces a visual inconsistency that only manifests for one length range (>500 vs ≤500 chars).

Suggested change
{isCollapsible ? (
<CollapsibleOutput
isCollapsible
collapsedHeight={60}
fadeStart={40}
>
{isCollapsible ? (
<CollapsibleOutput
isCollapsible
collapsedHeight={60}
fadeStart={40}
>
{outputContent}
</CollapsibleOutput>
) : (
outputContent
)}

(where outputContent is extracted as a local const above the return.)

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

Comment on lines +69 to +72
<CollapsibleOutput
isCollapsible={output.length > EXPAND_THRESHOLD}
className="text-[13px] opacity-90"
>

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] GenericToolCall was refactored to use the shared CollapsibleOutput but has no test covering its specific wiring — Concrete cost: the 400-char threshold (EXPAND_THRESHOLD, distinct from the 500 used by Shell/Think) and the className="text-[13px] opacity-90" pass-through are unique to this consumer; if a future change breaks either, no test catches the regression. Consider adding a GenericToolCall.test.tsx mirroring ThinkToolCall.test.tsx with two cases: output > 400 chars renders a toggle and preserves full content; output ≤ 400 chars renders no toggle.

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

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

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Local verification report — build + real-browser E2E

I built this branch locally (88b5f33, merge-base af63c9d) and drove the real exported-conversation page in real Chromium, plus a minimal standalone consumer harness. Everything you claimed in the PR body reproduces. I also found one blocking layout regression that the jsdom tests structurally cannot catch, with a verified one-line fix.

1. What reproduces ✅

Check Result
npx vitest run (packages/webui) 398 passed (30 files) — matches your number exactly
npx tsc --noEmit clean
npx eslint src --ext .ts,.tsx clean
npm run build ok; both new rules present in shipped dist/styles.css
Failure-first: ShellToolCall.test.tsx + ThinkToolCall.test.tsx applied to the merge-base 3 failed — exactly the three tail-marker assertions; the 500-char boundary and short-thinking cases pass on both sides, so they are genuine non-regression guards

Real-browser behaviour on the export page (real packages/web-templates HTML template, real injectDataIntoHtmlTemplate, real ChatViewer, locally built WebUI bundle, real git log output as the fixture):

  • bash / execute / think / generic all keep the complete text; the tail marker is in the DOM and becomes visible on expand.
  • Toggle click does not trigger the OUT-row action — no temp-file modal opens (stopPropagation works in a real browser, not just jsdom).
  • OUT row click still opens the modal with the full 1700-char output.
  • Error, <= 500, and short-thinking paths render identically to main — same text, same measured geometry.
  • aria-expanded / aria-label on the toggle is a real improvement — GenericToolCall had neither before.

export before/after

Side note on your "not validated" item: the stock export page does load WebUI from unpkg, but a local /export smoke test is reachable today — build the package with formats: ['umd'], name: 'QwenCodeWebUI' and rewrite the two unpkg URLs in the generated template to the local files. That is how I ran everything below, and it is what surfaced the next section.

2. Blocking: the collapsible OUT row blows out the layout 🔴

.X-toolcall-output-subtle clientWidth / scrollWidth, measured in Chromium inside a plain 600px column with only @qwen-code/webui styles (no app CSS at all):

Case main this PR PR + 1-line fix
bash, 820-char single line (> 500, collapsible) 507 / 507 (tail cut at 500) 6711 / 6711 — not scrollable 507 / 6711 — scrolls ✅
bash, 22-line log (> 500, collapsible) 507 / 507 859 / 859 — box overflows the card 507 / 859 ✅
execute, 820-char single line (> 500) 507 / 507 6711 / 6711 507 / 6711 ✅
bash, 400-char single line (≤ 500, not collapsible) 507 / 507 507 / 3274 — scrolls ✅ 507 / 3274 ✅

The 600px host column becomes a 6795px page. On the real export page the same input gives .chat-viewer-messages clientWidth 632 / scrollWidth 5553, and that element is overflow-x: hidden, so nothing scrolls anywhere: for exactly the > 500-char case this PR targets, the tail is still unreachable — just for a different reason. The OUT background also bleeds outside the card border, and because the toggle sits in a flex justify-center row that is now ~5.4k px wide, the "Show more" button for a long single-line output lands roughly 2700px off-screen.

layout regression

Root cause. .X-toolcall-row-content:not(.X-toolcall-full) was the only rule giving that grid item overflow: hidden, which is what made its automatic minimum size 0. Adding -toolcall-full removes it, and together with the new pre { overflow: visible } the max-content width now propagates up through .X-toolcall-row (grid-template-columns: max-content 1fr) and the card. The new overflow-hidden on .toolcall-collapsible-output-content does not help, because that div is not the grid item. Row 4 of the table is why your isolated probe read 200/831: on the non-collapsible path the old clamp is still there, so overflow: visible on the <pre> behaves exactly as you intended — the regression is confined to the collapsible path this PR adds.

Fix I verified (restores every number in the last column, and your 7 focused tests still pass 7/7):

.bash-toolcall-row-content.bash-toolcall-full {
  min-width: 0;
}

.execute-toolcall-row-content.execute-toolcall-full {
  min-width: 0;
}

That the existing suite passes both with and without this fix is the real lesson: jsdom has no layout engine, so no test in this PR can see the bug. A guard that asserts subtle.scrollWidth > subtle.clientWidth and messages.scrollWidth === messages.clientWidth in a real browser would be worth adding. This is a shared component, so the blow-out reaches every @qwen-code/webui consumer — export HTML, web-shell, and the VS Code webview — not just the export page.

3. Smaller notes 🟡

  1. ShellToolCall.test.tsx readFileSyncs the stylesheet and asserts the exact substring `.${kind}-toolcall-output-subtle .${kind}-toolcall-pre {\n overflow: visible;\n}`. It couples the test to CSS source formatting (a prettier reflow or one extra property breaks it) while proving nothing about rendering — and it passes unchanged with the layout bug present. I would drop it in favour of the browser assertion above.
  2. Same file: resolve(process.cwd(), 'src/components/toolcalls/ShellToolCall.css') only works when vitest runs with packages/webui as CWD. new URL('./ShellToolCall.css', import.meta.url) is CWD-independent.
  3. The threshold is character-based while the clamp is height-based, so a 516-char thought renders at 220px collapsed and 220px expanded — a "Show more" that visibly does nothing. Conversely a 400-char, 20-line output is still clamped to 60px with no toggle at all. Pre-existing policy, not a regression, but the toggle showing up where it changes nothing reads as a bug to users.

Verdict

The idea and most of the implementation are right, and the collapse/expand, click-isolation and a11y work all hold up under a real browser. I would not merge as-is, though: the layout blow-out defeats the PR's own goal for long single-line output and visibly breaks the card in every consumer. Add the two min-width: 0 rules plus a real-browser regression guard and I am happy to see this land.

中文说明

本地验证报告 —— 构建 + 真实浏览器 E2E

我在本地构建了这个分支(88b5f33,merge-base af63c9d),在真实 Chromium 中驱动了真实的导出会话页面,另外还做了一个最小化的独立消费方 harness。PR 描述里的结论都能复现。同时我发现了一个阻塞性的布局回归,现有的 jsdom 测试在结构上不可能发现它;我验证了一个一行的修复。

1. 可复现的部分 ✅

检查项 结果
npx vitest run(packages/webui) 398 passed(30 个文件) —— 与你给的数字完全一致
npx tsc --noEmit 通过
npx eslint src --ext .ts,.tsx 通过
npm run build 通过;两条新规则都出现在最终产物 dist/styles.css
失败优先:把 ShellToolCall.test.tsx + ThinkToolCall.test.tsx 放到 merge-base 上跑 3 个失败 —— 正好是三个尾部标记断言;500 字符边界和短思考用例两边都通过,说明它们是真正的防回归守卫

导出页面上的真实浏览器行为(真实的 packages/web-templates HTML 模板、真实的 injectDataIntoHtmlTemplate、真实的 ChatViewer、本地构建的 WebUI bundle,用真实 git log 输出作为素材):

  • bash / execute / think / generic 都保留了完整文本;尾部标记存在于 DOM 中,展开后可见。
  • 点击开关不会触发 OUT 行的动作 —— 没有打开临时文件弹窗(stopPropagation 在真实浏览器里同样有效,不只是 jsdom)。
  • 点击 OUT 行仍然会打开弹窗,内容是完整的 1700 字符输出。
  • 错误、<= 500、短思考路径与 main 渲染一致 —— 文本相同,实测几何尺寸也相同。
  • 开关上的 aria-expanded / aria-label 是实打实的改进 —— GenericToolCall 之前两者都没有。

关于你标注为"未验证"的那一项:标准导出页确实从 unpkg 加载 WebUI,但今天已经可以做本地 /export 冒烟测试 —— 用 formats: ['umd'], name: 'QwenCodeWebUI' 构建该包,再把生成模板里的两个 unpkg URL 改写成本地文件即可。下面所有结论都是这样跑出来的,也正是这样才发现了下一节的问题。

2. 阻塞问题:可折叠的 OUT 行撑爆了布局 🔴

在只加载 @qwen-code/webui 样式(完全没有应用侧 CSS)的 600px 纯列容器中,用 Chromium 测得的 .X-toolcall-output-subtle clientWidth / scrollWidth:

用例 main 本 PR PR + 一行修复
bash,820 字符单行(> 500,可折叠) 507 / 507(尾部在 500 处被砍) 6711 / 6711 —— 不可滚动 507 / 6711 —— 可滚动 ✅
bash,22 行日志(> 500,可折叠) 507 / 507 859 / 859 —— 输出框溢出卡片 507 / 859 ✅
execute,820 字符单行(> 500) 507 / 507 6711 / 6711 507 / 6711 ✅
bash,400 字符单行(≤ 500,可折叠) 507 / 507 507 / 3274 —— 可滚动 ✅ 507 / 3274 ✅

600px 的宿主列变成了 6795px 的页面。在真实导出页面上,同样的输入让 .chat-viewer-messages 变成 clientWidth 632 / scrollWidth 5553,而该元素是 overflow-x: hidden,所以任何地方都没有滚动条:恰恰是本 PR 想要解决的 > 500 字符场景,尾部仍然够不到,只是换了一个原因。 OUT 背景也溢出到卡片边框之外;由于开关位于一个现在宽约 5.4k px 的 flex justify-center 行里,长单行输出的 "Show more" 按钮被挤到大约屏幕外 2700px 处。

根因。 .X-toolcall-row-content:not(.X-toolcall-full) 是唯一给这个 grid item 加 overflow: hidden 的规则,而正是它让该 item 的自动最小尺寸变成 0。加上 -toolcall-full 之后这条规则失效,再叠加新的 pre { overflow: visible },max-content 宽度就沿着 .X-toolcall-rowgrid-template-columns: max-content 1fr)和卡片一路向上传播。新加在 .toolcall-collapsible-output-content 上的 overflow-hidden 帮不上忙,因为那个 div 并不是 grid item。表格第 4 行解释了你的隔离探针为什么读到 200/831:非折叠路径上旧的 clamp 还在,所以 <pre> 上的 overflow: visible 完全按你设想的方式工作 —— 回归只局限在本 PR 新增的可折叠路径上。

我验证过的修复(能恢复最后一列的全部数值,并且你的 7 个 focused 测试依旧 7/7 通过):

.bash-toolcall-row-content.bash-toolcall-full {
  min-width: 0;
}

.execute-toolcall-row-content.execute-toolcall-full {
  min-width: 0;
}

现有测试在加不加这个修复的情况下都通过,这才是真正的教训:jsdom 没有布局引擎,所以本 PR 里的任何测试都看不见这个 bug。值得补一个在真实浏览器中断言 subtle.scrollWidth > subtle.clientWidth messages.scrollWidth === messages.clientWidth 的守卫。这是共享组件,所以撑爆布局会影响每一个 @qwen-code/webui 消费方 —— 导出 HTML、web-shell、VS Code webview,而不只是导出页。

3. 次要问题 🟡

  1. ShellToolCall.test.tsxreadFileSync 读样式表并断言精确子串 `.${kind}-toolcall-output-subtle .${kind}-toolcall-pre {\n overflow: visible;\n}`。这把测试绑死在 CSS 源码格式上(prettier 重排或多加一个属性就会失败),却完全没有验证渲染 —— 而且在布局 bug 存在的情况下它照样通过。我建议换成上面说的浏览器断言。
  2. 同一个文件:resolve(process.cwd(), 'src/components/toolcalls/ShellToolCall.css') 只在 vitest 以 packages/webui 为 CWD 运行时才成立。new URL('./ShellToolCall.css', import.meta.url) 与 CWD 无关。
  3. 阈值按字符数算,而 clamp 按高度算,因此一段 516 字符的思考在折叠和展开状态下都是 220px —— "Show more" 点了看不出任何变化。反过来,400 字符、20 行的输出仍然被 clamp 到 60px 且根本没有开关。这是既有策略、不算回归,但开关出现在点了没用的地方,用户会当成 bug。

结论

思路和大部分实现是对的,折叠/展开、点击隔离和无障碍改进在真实浏览器下都站得住。但我不建议按当前状态合并:布局撑爆在长单行输出上恰好抵消了本 PR 自己的目标,并且在所有消费方里都会把卡片撑破。补上两条 min-width: 0 规则以及一个真实浏览器的防回归守卫之后,我很乐意看到它合入。


🤖 Generated with Claude Code — Claude Opus 5 (1M context)

@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-preview via Qwen Code /review

Comment on lines +66 to +70
) : (
<div className="italic opacity-90 leading-relaxed">
{thoughts}
</div>
)}

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 non-collapsible branch duplicates the className="italic opacity-90 leading-relaxed" and {thoughts} child already passed to CollapsibleOutput, and diverges from GenericToolCall's always-render form in this same PR. — Concrete cost: restyling the thought text (or changing the wrapper) requires editing two places, and the shared component's isCollapsible={false} path is exercised by only one of three callers. CollapsibleOutput already renders cleanly with isCollapsible={false} (no maxHeight/mask, no toggle), so the whole ternary could collapse to:

<CollapsibleOutput
  isCollapsible={isCollapsible}
  className="italic opacity-90 leading-relaxed"
>
  {thoughts}
</CollapsibleOutput>

(The only structural difference for the 200–500-char band is an inert flex flex-col wrapper plus overflow-hidden with no maxHeight, which does not clip fitting text.)

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

Comment on lines +266 to +267
collapsedHeight={60}
fadeStart={40}

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 shell OUT row's collapsed appearance is encoded twice with nothing keeping the copies in sync: outputs ≤ 500 chars are clipped by .bash-toolcall-row-content:not(.bash-toolcall-full) in ShellToolCall.css (max-height: 60px, mask 40px → 60px), while outputs > 500 chars are clipped by these inline props (60/40) — the same row, two rendering paths selected by a length threshold. — Concrete cost: a maintainer tuning the short-output clip in CSS (say to 80px/50px, duplicated again for bash and execute) gets no error and no test failure, but 499-char and 501-char outputs suddenly collapse at different heights with different fades — a "random" layout bug with the two halves of the behavior living in different languages. Consider exporting shared constants used by both the CSS (via custom properties) and these props, or always wrapping the OUT content in CollapsibleOutput (toggle only when isCollapsible) and dropping the :not(.-toolcall-full) mask rule so one code path owns the collapsed appearance.

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

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Re-verification on f8d1681 — the blocking regression is fixed

Follow-up to my report on 88b5f33. I rebuilt this branch locally (f8d1681, merge-base af63c9d) and drove the real exported-conversation page in real Chromium again — real packages/web-templates template, real injectDataIntoHtmlTemplate, locally built WebUI UMD bundle, real git log / git rev-list output as the fixture. The layout blow-out is gone, and the new Playwright guard genuinely catches the bug it was written for. LGTM.

1. Everything in the updated PR body reproduces ✅

Check Claimed Measured here
npx vitest run (packages/webui) 31 files / 400 tests 31 files / 400 passed
Focused component run (4 files) 9 tests 9 passed
npm run test:e2e:smoke (web-shell) 26 tests 26 passed, including both new layout cases
webui typecheck / lint / build pass pass — both min-width: 0 rules present in the shipped dist/styles.css
web-shell lint / typecheck pass pass (after building sdk-typescript first, as your notes say)
Prettier on changed files pass pass — the 4 remaining web-shell warnings are pre-existing on main and untouched here

Behaviour on the real export page is unchanged from my previous pass: bash / execute / think / generic all keep the complete text, clicking the toggle does not open the OUT-row temp-file modal (stopPropagation holds in a real browser), and clicking the OUT row still opens the modal with the full output.

2. The blocking regression is fixed 🟢

Same fixture as before — one 2 459-character line with no whitespace — measured on the real export page at a 1180 px viewport:

main af63c9d 88b5f33 f8d1681
.chat-viewer-messages clientWidth / scrollWidth 812 / 812 812 / 16405 🔴 812 / 812 ✅
.bash-toolcall-card clientWidth / scrollWidth 740 / 740 740 / 16354 🔴 740 / 740 ✅
OUT text length 503, ends with ... 2459 2459
Tail reachable? no — cut at 500 no — 0 px scrollable yes — scrollLeft 15622 of scrollWidth 16306

layout fix

The two min-width: 0 rules restore the automatic minimum size that :not(...-full)'s overflow: hidden used to provide, which is exactly what I measured last time. The ShellToolCall.tsx change in the same commit is a pure extraction of outputContent — no behavioural difference, confirmed by the identical geometry on both branches of the ternary.

3. The new guard actually catches it — failure-first ✅

I removed only the two min-width: 0 rules from ShellToolCall.css and re-ran:

  • packages/web-shellwebui-tool-output-layout.spec.ts2 failed (bash and execute), at expect(collapsed.outputScrollWidth).toBeGreaterThan(collapsed.outputClientWidth) with Expected: > 5544, Received: 5544.
  • packages/webui → the whole jsdom tool-call suite — 6 files / 28 tests, all passing.

That is precisely the gap I flagged: jsdom has no layout engine and cannot see this class of bug, and the new browser assertion sits exactly where it needs to. Restoring the rules turns both back green.

4. Feature evidence — collapsed / expanded screenshots

@yiliang114 — the captures you asked for, taken from the real export page rather than Storybook, so they reflect the shipped bundle.

Multi-line output, where the 60 px clamp actually bites:

collapse / expand

Long single-line output — the case that motivated the PR — including the horizontal-scroll behaviour:

export before / after

5. Non-blocking notes 🟡

  1. For single-line output the toggle is a visual no-op. The threshold is character-based (> 500) while the clamp is height-based (60px), so the 2 459-character single line renders at 18 px collapsed and 18 px expanded on the export page — "▼ Show more" appears, and pressing it changes nothing visible; the tail is reached by horizontal scrolling either way. Multi-line output behaves correctly (60 px → 318 px; the think card 200 px → 395 px). This one is new — main showed no toggle there at all because the content was truncated. Gating isCollapsible on measured overflow (scrollHeight > collapsedHeight) rather than character count would fix it, and would also cover the inverse case (a 400-character, 20-line output still clamped to 60 px with no toggle). Pre-existing policy question, worth a follow-up rather than a change here.

  2. A caveat about the new harness, not a defect. packages/web-shell serves @qwen-code/webui from source in dev, so webui's own Tailwind utility sheet is not loaded — min-w-0 on ToolCallRow / ToolCallCard computes to auto in the harness but to 0px with the shipped dist/styles.css. The guard's assertions are all against ShellToolCall.css, which is a plain CSS import and always applies, so the test is sound; it just would not see a regression that only affects the Tailwind-generated utilities. Worth knowing before anyone extends this harness. (I chased an apparent think/generic overflow in that harness first — it does not reproduce against the shipped stylesheet.)

  3. The <= 500 path is not byte-identical to main. A 400-character single-line output measures 470 / 470 on main and 470 / 2699 here — it gains horizontal scrolling from the pre { overflow: visible } change. That is an improvement and clearly intentional, just not literally "unchanged"; error and empty-output paths are identical.

  4. My earlier notes 1 and 2 are both resolved — the CSS-source substring assertion and the process.cwd()-relative readFileSync are gone, replaced by a real browser assertion. 👍

Verdict

Ready to merge from my side. The bug that blocked 88b5f33 is fixed, the fix is the minimal one, and it now has a regression guard that fails without it.

中文说明

针对 f8d1681 的复验 —— 阻塞性回归已修复

这是我在 88b5f33 上那份报告的后续。我在本地重新构建了这个分支(f8d1681,merge-base af63c9d),再次在真实 Chromium 中驱动了真实的导出会话页面 —— 真实的 packages/web-templates 模板、真实的 injectDataIntoHtmlTemplate、本地构建的 WebUI UMD bundle,并用真实的 git log / git rev-list 输出作为素材。布局撑爆的问题已经消失,新增的 Playwright 守卫也确实能抓住它本该抓的 bug。LGTM。

1. 更新后的 PR 描述里的结论都能复现 ✅

检查项 PR 声称 我这边实测
npx vitest run(packages/webui) 31 个文件 / 400 个测试 31 个文件 / 400 通过
focused 组件测试(4 个文件) 9 个测试 9 通过
npm run test:e2e:smoke(web-shell) 26 个测试 26 通过,包含两个新的布局用例
webui typecheck / lint / build 通过 通过 —— 两条 min-width: 0 规则都出现在最终产物 dist/styles.css
web-shell lint / typecheck 通过 通过(按你的说明,先构建 sdk-typescript
改动文件的 Prettier 通过 通过 —— web-shell 剩余的 4 个告警在 main 上就存在,本 PR 未触及

真实导出页面上的行为与我上一轮一致:bash / execute / think / generic 都保留完整文本;点击开关不会打开 OUT 行的临时文件弹窗(stopPropagation 在真实浏览器中同样有效);点击 OUT 行仍然会打开包含完整输出的弹窗。

2. 阻塞性回归已修复 🟢

素材与上次相同 —— 一条 2 459 字符、不含空白的单行 —— 在真实导出页面、1180 px 视口下测得:

main af63c9d 88b5f33 f8d1681
.chat-viewer-messages clientWidth / scrollWidth 812 / 812 812 / 16405 🔴 812 / 812 ✅
.bash-toolcall-card clientWidth / scrollWidth 740 / 740 740 / 16354 🔴 740 / 740 ✅
OUT 文本长度 503,以 ... 结尾 2459 2459
尾部可达? 否 —— 在 500 处被砍 否 —— 0 px 可滚动 是 —— scrollLeft 15622 / scrollWidth 16306

那两条 min-width: 0 恢复了原本由 :not(...-full)overflow: hidden 提供的自动最小尺寸,与我上次测出的结论完全一致。同一个 commit 里的 ShellToolCall.tsx 改动只是把 outputContent 提取出来,没有行为差异 —— 三元表达式两个分支的几何尺寸完全相同,可以确认这一点。

3. 新守卫确实能抓住问题 —— 失败优先验证 ✅

删掉了 ShellToolCall.css 里的两条 min-width: 0 规则,然后重新运行:

  • packages/web-shellwebui-tool-output-layout.spec.ts —— 2 个失败(bash 与 execute),失败点在 expect(collapsed.outputScrollWidth).toBeGreaterThan(collapsed.outputClientWidth),报 Expected: > 5544, Received: 5544
  • packages/webui → 整个 jsdom tool-call 测试集 —— 6 个文件 / 28 个测试全部通过

这正是我之前指出的覆盖缺口:jsdom 没有布局引擎,看不见这类 bug,而新增的浏览器断言恰好补在该补的位置。把规则加回去,两边都恢复为绿色。

4. 功能证据 —— 折叠 / 展开截图

@yiliang114 —— 你要的截图在这里,是从真实导出页面(而非 Storybook)截取的,因此反映的是实际发布产物的效果。

多行输出,60 px 的 clamp 在这种情况下真正生效:

长单行输出 —— 也就是本 PR 想解决的场景 —— 包含横向滚动行为:

(截图见上方英文部分)

5. 非阻塞的观察 🟡

  1. 对单行输出来说,展开开关在视觉上是个空操作。 阈值按字符数算(> 500),而 clamp 按高度算(60px),所以那条 2 459 字符的单行在导出页面上折叠时是 18 px,展开后仍然是 18 px —— "▼ Show more" 会出现,但点下去看不到任何变化;尾部无论如何都只能靠横向滚动到达。多行输出则表现正常(60 px → 318 px;think 卡片 200 px → 395 px)。这一点是本 PR 新引入的 —— main 上因为内容被截断,那里根本不会出现开关。把 isCollapsible 改成基于实测溢出(scrollHeight > collapsedHeight)而不是字符数即可解决,同时也能覆盖反向的情况(400 字符、20 行的输出仍被 clamp 到 60 px 却没有开关)。这属于既有策略问题,建议后续单独处理,不必在本 PR 里改。

  2. 关于新 harness 的一个说明,不是缺陷。 packages/web-shell 在 dev 模式下把 @qwen-code/webui 指向源码,因此不会加载 webui 自己的 Tailwind 工具类样式表 —— ToolCallRow / ToolCallCard 上的 min-w-0 在 harness 里计算为 auto,而在实际发布的 dist/styles.css 下是 0px。新守卫的断言全部针对 ShellToolCall.css,那是普通 CSS import、始终生效,所以测试本身是可靠的;只是它看不到仅影响 Tailwind 工具类的回归。后续有人扩展这个 harness 时值得注意。(我一开始在该 harness 里看到 think/generic 似乎也撑爆,但在实际发布样式表下并不复现。)

  3. <= 500 这条路径与 main 并非逐字节一致。 400 字符的单行输出在 main 上是 470 / 470,在这里是 470 / 2699 —— 由于 pre { overflow: visible } 的改动,它获得了横向滚动能力。这是改进,显然也是有意为之,只是并不完全等于"保持不变";错误路径和空输出路径确实完全一致。

  4. 我之前的第 1、2 条意见都已解决 —— 断言 CSS 源码子串的测试和依赖 process.cwd()readFileSync 都已移除,换成了真实浏览器断言。👍

结论

从我这边看可以合并。阻塞 88b5f33 的 bug 已修复,修复方式是最小改动,并且现在有了一个"去掉修复就会失败"的回归守卫。

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 1, 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: 453 passed · 0 failed · 453 total

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

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

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

Verification report

PR #8251 Deep Verification — fix(webui): make long tool output collapsible

Verdict: merge-ready — 453 scripted assertions passed, 0 failed.
Verified head: f8d16811dc5eb16b6b4d11e7cc182556b54b011a (git rev-parse HEAD^2).

中文摘要

结论: merge-ready

A/B 结论: 在 base(HEAD^1)上,ThinkToolCall 和 ShellToolCall 均在 DOM 层面将超过 500 字符的内容硬截断(substring(0, 500) + '...'),尾部标记丢失;在 head 上,完整内容保留在 DOM 中,折叠 UI 提供展开/收起控制。A/B harness 11 项断言:head 全部通过(11/11),base 仅 3 项通过(边界与既有行为对照),8 项按预期失败——证明变更是承重的。见 01-ab-head-all-pass.png02-ab-base-8-fail.png

Findings: 无阻塞性问题。

未覆盖范围: CSS 水平滚动(jsdom 不做布局)、mask 渐变视觉效果、导出页面行为(需发布 bundle)、e2e 测试计划文件(文档性质)。

Central claim + A/B table

Central claim: Long tool output (>500 chars) in Shell (Bash/Execute) and Think tool calls is preserved in full behind a collapsible expand/collapse UI, replacing the base's hard truncation at 500 characters.

Secondary claims:

  1. The expand/collapse button's event.stopPropagation() prevents triggering the Shell OUT-row's openTempFile action.
  2. GenericToolCall's existing collapsible behavior (threshold 400) is preserved through the refactoring to the shared CollapsibleOutput component.

A/B harness

The identical vitest harness (ab-harness.test.tsx, 11 tests) was run against both trees. The base worktree reused the head's node_modules (no dependency changes in this PR — verified via git diff HEAD^1..HEAD --name-only | grep package). All imports in the code under test are relative within packages/webui, so no workspace symlink confound applies.

Cell Test Head Base Oracle
T1 ThinkToolCall: tail marker in DOM ✅ PASS ❌ FAIL (truncated) textContent.toContain('__THINK_TAIL_MARKER__')
T2 ThinkToolCall: expand button present ✅ PASS ❌ FAIL (null) querySelector('button[aria-label="Expand output"]')
T3 ThinkToolCall: expand/collapse cycle ✅ PASS ❌ FAIL (null) aria-expanded + maxHeight toggling
T4 ThinkToolCall: 500 chars → no toggle ✅ PASS ✅ PASS boundary control
S1-bash ShellToolCall bash: tail in DOM ✅ PASS ❌ FAIL (truncated) textContent.toContain('__SHELL_TAIL_MARKER__')
S1-exec ShellToolCall execute: tail in DOM ✅ PASS ❌ FAIL (truncated) same
S2-bash ShellToolCall bash: expand button ✅ PASS ❌ FAIL (null) button presence
S2-exec ShellToolCall execute: expand button ✅ PASS ❌ FAIL (null) button presence
S3 Expand click ≠ openTempFile ✅ PASS ❌ FAIL (no button) openTempFile not called
S4 OUT row click → openTempFile ✅ PASS ✅ PASS existing behavior control
S5 Shell 500 chars → no toggle ✅ PASS ✅ PASS boundary control

Head: 11/11 pass. Base: 3/11 pass, 8 fail (all expected).

Evidence: 01-ab-head-all-pass.png, 02-ab-base-8-fail.png.

Correction to PR description: The description states "the OUT scroll container now owns horizontal overflow so a long single-line result remains scrollable […] instead of being clipped by the nested <pre>." This is accurate for the CSS change, but the description's framing may suggest the base had full content in the DOM with only visual clipping. In fact, the base ShellToolCall also hard-truncated at 500 chars via output.substring(0, 500) + '...' (base line 214–215), so the base had actual data loss in the DOM, not just CSS clipping. The PR correctly fixes both the data loss and the CSS overflow.

Vacuity check (mutation matrix)

Four single-point mutations were applied to the head source; each was killed by the PR's own tests. The unmutated control is green (evidence: 03-vacuity-head-green.png).

# Mutation File Suite that catches it Failure message Killed?
M1 isCollapsible = false (was thoughts.length > 500) ThinkToolCall.tsx ThinkToolCall.test.tsx expected null not to be null (button absent)
M2 isCollapsible = false (was output.length > 500) ShellToolCall.tsx ShellToolCall.test.tsx expected null not to be null (button absent, both bash+execute)
M3 maxHeight: '' (was `${collapsedHeight}px`) CollapsibleOutput.tsx CollapsibleOutput.test.tsx + ThinkToolCall.test.tsx expected '' to be '200px'
M4 Removed event.stopPropagation() CollapsibleOutput.tsx ShellToolCall.test.tsx expected "spy" to not be called at all, but actually been called 1 times

All four mutations fail the intended behavioural assertion with the expected-versus-actual mismatch the test exists to catch. No survivors.

Targeted gates

Gate Result Detail
Webui test suite ✅ 416/416 pass, 32 files npx vitest run in packages/webui
TypeScript typecheck ✅ clean tsc --noEmit exit 0
ESLint (changed dir) ✅ clean npx eslint packages/webui/src/components/toolcalls/ exit 0
ESLint liveness ✅ live Planted const unused_var = 42; → caught @typescript-eslint/no-unused-vars; removed

Findings

None. No blocking or non-blocking issues found.

Not covered

  • CSS horizontal scroll behaviour: The PR adds *-toolcall-output-subtle .*-toolcall-pre { overflow: visible } to enable horizontal scrolling within the output container. jsdom does not perform layout, so this CSS-only claim cannot be behaviourally tested here. The PR author's Chrome layout probe (clientWidth/scrollWidth = 200/831) is cited but not independently reproduced.
  • Mask gradient visual effect: The maskImage / WebkitMaskImage linear-gradient fade is a visual-only concern; the inline style values are asserted in tests but the rendered appearance is not verified.
  • Export page (/export) behaviour: The PR description notes the export page loads the published WebUI bundle from unpkg, not the worktree bundle, so the export scenario was not tested.
  • E2E test plan files: .qwen/e2e-tests/2026-07-31-export-collapsible-tool-output.md and packages/webui/client/e2e/ are documentation/harness files for manual or future automated verification; they were not executed.
  • Per-commit attribution: The checkout is shallow (depth 2); only the aggregate HEAD^1..HEAD diff was verified.
  • Storybook stories: The one-line comment change in ShellToolCall.stories.tsx was not rendered.

Methodology

Verification ran in the CI container (node:22-bookworm) on the merge-ref checkout (HEAD = merge commit, HEAD^1 = base tip 2bbd82f, HEAD^2 = PR head f8d1681). The A/B harness was a vitest test file placed identically in both the head tree and a git worktree at HEAD^1; the base worktree reused the head's root node_modules via symlink (no dependency changes — verified). The code under test imports only relative paths within packages/webui, so no workspace symlink confound applies. Vacuity mutations were applied in-place with backup/restore. Raw logs: head-ab.log, base-ab.log, webui-tests.log, typecheck.log, lint.log.

Evidence images

01-ab-head-all-pass

02-ab-base-8-fail

03-vacuity-head-green

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.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Aug 1, 2026
Merged via the queue into QwenLM:main with commit 1bb7209 Aug 1, 2026
79 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.3.

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