feat(webui): add global expand/collapse control to exported HTML viewer - #9367
Conversation
Add an optional "Expand all / Collapse all" toolbar to ChatViewer (showExpandControl prop, enabled in the /export HTML template) that broadcasts expand/collapse commands to every collapsible section: thinking blocks, tool call outputs, and file references. A new ExpandControlContext carries a monotonic signal plus target state; a useControlledExpanded hook replaces the local useState of each collapsible component so global commands sync all sections while individual toggles keep working between commands. Components mounted after a global command inherit the latest target. Without the provider, all components behave exactly as before. Fixes #8208
|
Thanks for the PR!
Moving on to code review. 🔍 中文说明感谢贡献!
进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewReviewed at the commit below; static review only (per policy, PR code is never executed here — test evidence comes from the PR's own CI, quoted further down). Before reading the diff I sketched my own take: a context carrying a monotonic command counter + target state, a small hook that replaces each section's local What I verified against the codebase:
Two non-blocking notes:
Files changed (12 of 12 shown)
Test evidence (PR's own CI, fetched via API — I do not run PR code)At the time of this pass the main unit suite is still running (this repo's suite takes ~30 minutes; I fetch once and report what is there rather than polling). The
Sandboxed verification would settle the one claim CI cannot: 中文说明代码审查按下方记录的 commit 审查;仅静态审查(按规则这里不执行 PR 代码——测试证据引自 PR 自己的 CI,见下文)。 读 diff 之前我先独立构想了方案:一个携带单调命令计数 + 目标状态的 context,一个替换各区域本地 对照代码库核实的内容:
两条不阻塞的备注:
测试证据(引自 PR 自己的 CI,通过 API 获取——不运行 PR 代码)截至本次审查,主单测套件仍在运行(本仓库套件约需 30 分钟;只拉取一次并如实呈报,不轮询)。 沙箱验证可以落实 CI 无法覆盖的那一项: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — closes a triaged, welcome-labeled feature request with the same design I'd have proposed independently; two non-blocking nits only, and CI is still in flight. Stepping back: this is what a good community PR looks like. The problem is documented and triaged (#8208 explicitly lists a global expand/collapse control as its remaining item), the scope stops exactly where the problem stops, and the implementation is the smallest idiomatic mechanism that works — one context, one hook, and a one-line swap in each collapsible surface. Nothing in the diff is on loan from some other ambition: no drive-by refactors, no formatting churn, no speculative options beyond the unused The design also gets the subtle parts right: repeated commands are distinguishable via the monotonic signal, late-mounting sections inherit the latest target, and the whole mechanism is inert unless a consumer opts in — so nothing outside the My two reservations are both small: the untested late-mount inheritance path, and the exported-but-unconsumed The unit suite was still running when this pass happened, so approval is deferred until CI lands green on 中文说明置信度:4/5 —— 以一个与我的独立构想一致的设计,关闭了一个经过 triage、标记为欢迎贡献的 feature request;只有两条不阻塞的小问题,且 CI 仍在运行中。 退一步看:这是一个标准的优质社区 PR。问题有据可查(#8208 明确把全局展开/收起控制列为剩余项),范围恰好停在问题边界处,实现是能工作的最小惯用机制——一个 context、一个 hook,外加每个可折叠面上的一行替换。diff 里没有夹带任何其他野心:没有顺手重构、没有格式噪声,除了上文提到的未被使用的 设计在微妙之处也做对了:单调信号让重复命令可区分,命令发出后才挂载的区域会继承最新目标,而且整套机制在消费方不启用时完全惰性——因此 我仅有的两点保留意见都很小:未测试的"后挂载继承"路径,以及导出但无消费方的 本次审查时主单测套件仍在运行,因此批准推迟到 CI 在 — Qwen Code · qwen3.8-max Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterℹ️ No screenshot changed against the PR base — but this PR edits 9 render-shaping files:
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 Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more.
中文说明
Test Plan(非阻断):src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more。
— qwen3.8-max via Qwen Code /review (v0.21.13)
doudouOUC
left a comment
There was a problem hiding this comment.
Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
[Critical] R1-1: CDN dependency will break the export HTML at runtime
The export HTML template at packages/web-templates/src/export-html/src/index.html loads @qwen-code/webui from unpkg CDN with version resolved from package.json ("latest"). The exported HTML loads the published npm package, not the local build. Until a new version of @qwen-code/webui is published (with the showExpandControl prop, ExpandControlContext, and useControlledExpanded), the exported HTML will crash at runtime because QwenCodeWebUI.ChatViewer won't accept the showExpandControl prop.
Fix: The PR must be released together with a new @qwen-code/webui npm publish, or the build pipeline must be updated to bundle the local webui build instead of loading from CDN.
— qwen-code-review via Qwen Code /review (v0.21.10)
doudouOUC
left a comment
There was a problem hiding this comment.
Two-stage review summary (deepseek-v4-flash only; round 2 skipped because issues were found)
Verdict: ISSUES_FOUND
This PR adds a global expand/collapse toolbar to the exported HTML viewer. The implementation is clean, but the review identified concerns that should be addressed before merging.
Findings
-
Critical — CDN dependency will break the export HTML at runtime
- The exported HTML loads
@qwen-code/webuifrom the unpkg CDN. The newshowExpandControlprop,ExpandControlContext, anduseControlledExpandedhook do not exist in the currently published npm package, so the exported HTML will fail at runtime until a new version is published or the build pipeline bundles the local webui build instead.
- The exported HTML loads
-
Suggestion —
useEffectdependency semantic correctness- In
useControlledExpanded, the dependency array[control]should be[control?.signal]so the effect only re-runs when the monotonic signal actually changes. Left as an inline comment on the diff.
- In
Notes
- Round 1 model:
deepseek-v4-flash - Round 2 model: not run (round 1 found issues)
- PR head at review time:
bac39560123b22340aaaaf734dabebb2f07f9e79
…etch toggle a11y - useControlledExpanded now adjusts state during render (React's documented pattern) instead of a useEffect, so a global expand/collapse command no longer commits every collapsible section once with its stale value before syncing; the ref guard keeps the update idempotent under StrictMode. - Give the web_fetch OutputCard toggle the same stopPropagation, aria-expanded and aria-label treatment as the shared CollapsibleOutput toggle it mirrors.
…and file/read coverage - Toolbar must not render for an empty message list even with showExpandControl enabled. - File references and read tool outputs expand/collapse with the global buttons. - A section mounted after a global command inherits the latest target state without another click.
|
Closeout for the round-1 review, pushed in 989125d + b2ededd:
webui suite 536/536 green; typecheck, eslint, prettier clean. |
|
Patrol closeout for the R1 review round (fixes in 989125d + b2ededd):
Verification: webui 536/536 (serial run), typecheck + eslint + prettier clean on touched files. |
doudouOUC
left a comment
There was a problem hiding this comment.
Re-review of the current state (commit b2ededd).
Summary
This PR has been reviewed thoroughly by earlier passes. All 7 findings (1 Critical, 6 Suggestions) from the initial review have been addressed:
-
R1-1 (Critical) — CDN publish dependency: acknowledged. The export-html template loads @qwen-code/webui from unpkg at view time, and the new
showExpandControlprop is inert until a webui version containing it is published. This is a release-policy concern (npm org access, release.yml changes) that needs a maintainer decision. The thread is left open for that — not a code defect in this diff. -
R1-2 (Suggestion) — untested empty-message guard: ✅ fixed in b2ededd (new test asserts no toolbar for
messages={[]}). -
R1-3 (Suggestion) — toolbar on plain Q&A transcripts: deliberately kept. The author's reasoning is sound: deriving "has at least one collapsible section" from message data would duplicate per-component collapse thresholds at the ChatViewer level, creating a drift hazard. Acceptable for v1.
-
R1-4 (Suggestion) — missing file reference / read tool call test coverage: ✅ fixed in b2ededd (new test exercises both with the global buttons).
-
R1-5 (Suggestion) — WebFetchToolCall OutputCard drift: ✅ fixed in 989125d (aria-expanded, aria-label, stopPropagation synced onto the copy). Full component reuse deferred as a follow-up.
-
R1-6 (Suggestion) — untested late-mount inheritance: ✅ fixed in b2ededd (new test: click Expand all, then add a new message → mounts expanded without another click).
-
R1-7 (Suggestion) — useEffect double-render per global command: ✅ fixed in 989125d (useEffect removed; signal syncs during render behind the ref guard, idempotent under StrictMode).
Code quality assessment
The implementation is clean and idiomatic React. The design — a context carrying a monotonic signal + target state, a hook that replaces local useState, and an opt-in toolbar prop — is the right approach for this problem. The render-time sync pattern (adjusting state during render rather than in an effect) is correctly implemented with a ref guard for StrictMode safety.
The test coverage is thorough: 6 test files, 16 tests, exercising the toolbar-off default, expand-all, collapse-all, individual-toggles-after-global-command, empty-message-list, file-reference + read tool call coverage, and late-mount inheritance. Each test uses flip-verification (mutating the implementation turns the test red).
Remaining
The only unresolved thread is R1-1 (CDN publish pipeline), which is a release-policy decision outside this diff. The code itself is ready from a correctness and test-coverage standpoint.
— manually reviewed via gh CLI (qwen-code CLI bundle not available in this environment)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/webui/src/components/ChatViewer/ChatViewer.tsx:364 — [review] R1-3 still stands — toolbar renders even when the transcript has zero collapsible sections (author deliberately kept the current gating in round 1)packages/web-templates/src/export-html/src/main.tsx:85 — [review] the export template's structural QwenCodeWebUI.ChatViewer type is never checked against the real webui prop and packages/web-templates has no tests (code unchanged since roun…packages/webui/src/components/ChatViewer/ChatViewer.tsx:397 — [review] role="toolbar" promises arrow-key navigation the two buttons do not implement (WAI-ARIA APG) (code unchanged since round 1)packages/webui/src/components/ChatViewer/ChatViewer.tsx:419 — [probe] conditional Provider wrap remounts the whole message subtree if showExpandControl ever toggles at runtime — probe-confirmed; no current call site toggles it (code unchang…
中文说明
Test Plan(非阻断):src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
…al expand control The last two converted collapsibles had no global-control coverage: no test rendered a search or web_fetch tool call under showExpandControl. A future edit reverting either local component to useState would leave its output collapsed after 'Expand all' (including in the exported HTML, where the toolbar is enabled) with nothing turning red. - search: long text output — collapsed body absent at baseline, present after Expand all, absent again after Collapse all. - web_fetch: content over EXPAND_THRESHOLD — OutputCard toggle carries aria-expanded and the card flips between the 120px collapsed max-height and no cap under the global buttons. Both cases flip-verified red by reverting the respective useControlledExpanded to useState.
|
Bot-review rerun round closeout (pushed 2d9c209):
Verified: webui suite 538/538 serial (parallel-run flakes in |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more.
[Critical] R1-1 The exported HTML loads its ChatViewer from the npm-published @qwen-code/webui (unpkg @latest, resolved in the viewer's browser at view time), but nothing in this repo ever publishes @qwen-code/webui — so this feature is silently inert in every shipped CLI. Re-checked at this head (2d9c209): still stands — packages/web-templates/src/export-html/package.json still declares "@qwen-code/webui": "latest"; the release pipeline's published-package list (scripts/get-release-version.js PUBLISHED_PACKAGES) still contains only qwen-code / audio-capture / channel-base / channels; npm view @qwen-code/webui still resolves latest = 0.1.0-beta.4 (published 2026-01-28), whose UMD predates this feature (round-2 probe: published UMD renders no toolbar, local build renders it — probe flips). This round's delta is test-only and changes nothing about the mechanism; the open maintainer-decision thread (publish webui from the release flow before the CLI bundle, or vendor the UMD into the template) must be resolved before/at merge. Do not pin the template to the workspace version before that version exists on the registry — every export would 404. (Not anchorable: main.tsx is outside this round's incremental diff; carried under its original id from rounds 1-2.)
中文说明
Test Plan(非阻断):src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more。
[Critical] R1-1 The exported HTML loads its ChatViewer from the npm-published @qwen-code/webui (unpkg @latest, resolved in the viewer's browser at view time), but nothing in this repo ever publishes @qwen-code/webui — so this feature is silently inert in every shipped CLI. Re-checked at this head (2d9c209): still stands — packages/web-templates/src/export-html/package.json still declares "@qwen-code/webui": "latest"; the release pipeline's published-package list (scripts/get-release-version.js PUBLISHED_PACKAGES) still contains only qwen-code / audio-capture / channel-base / channels; npm view @qwen-code/webui still resolves latest = 0.1.0-beta.4 (published 2026-01-28), whose UMD predates this feature (round-2 probe: published UMD renders no toolbar, local build renders it — probe flips). This round's delta is test-only and changes nothing about the mechanism; the open maintainer-decision thread (publish webui from the release flow before the CLI bundle, or vendor the UMD into the template) must be resolved before/at merge. Do not pin the template to the workspace version before that version exists on the registry — every export would 404. (Not anchorable: main.tsx is outside this round's incremental diff; carried under its original id from rounds 1-2.)
— qwen3.8-max via Qwen Code /review (v0.21.13)
Address R3-1: give the WebFetchToolCall OutputCard content div a stable webfetch-output-content class and consolidate the three .break-words query+cast sites in the expand-control test onto one helper, so the selector cannot rebind to other .break-words renderables.
|
Patrol closeout (pushed 27bb254):
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Test Plan (not a blocker): src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/webui/src/components/ChatViewer/ChatViewer.tsx:418 — [probe] Toggling showExpandControl at runtime remounts the whole messages region (conditional Provider wrap swaps the element type)packages/web-templates/src/export-html/src/main.tsx:25 — [probe] The /export template wiring of showExpandControl is outside every validation gate — never typechecked and never testedpackages/webui/src/components/toolcalls/WebFetchToolCall.tsx:109 — [probe] New aria-label on the WebFetch toggle violates WCAG 2.5.3 Label in Name (accessible name lacks visible text "Show more")
中文说明
仅完成部分审查,审查缺口已披露。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
Test Plan(非阻断):src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
…d sync Resolve the two outstanding review Criticals on the exported-HTML expand/collapse feature: - The export template loaded its ChatViewer from unpkg as @qwen-code/webui@latest, resolved in the viewer's browser at view time. Nothing in this repo publishes @qwen-code/webui as part of the release flow, so the toolbar was silently inert in every shipped CLI, and any future publish would retroactively change how already-generated exports render. The template build now compiles the webui entry to a UMD bundle (React/ReactDOM stay external, matching the pinned UMD scripts the template already loads) plus its processed styles.css from webui sources and inlines both, the same way the template already inlines its own script/CSS. The export no longer depends on any CDN resolution of @qwen-code/webui. Building from source keeps the template independent of webui's own build, which runs after web-templates in scripts/build.js and is skipped by --cli-only builds. - useControlledExpanded tracked the last seen global signal in a ref while adjusting state during render. A ref mutation persists even when React discards a render pass under concurrent scheduling, which can permanently desync a consumer from the signal (global expand/collapse silently does nothing). Track the previous signal in state instead: render-phase state updates are re-applied on the retried render, the sync still commits once per command, and StrictMode stays safe. - Post-global-command assertions in the expand-control suite now wait for the state via vi.waitFor instead of reading it synchronously, so the suite is deterministic under degraded scheduling; every assertion is unchanged. A mutation flip (sync disabled) still fails exactly the seven command-driven tests.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Test Plan (not a blocker): src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/webui/src/components/ChatViewer/ChatViewer.tsx:418 — [probe] Conditional Provider wrap remounts the whole messages region if showExpandControl ever toggles at runtime (round-4 deferral, still stands)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
Test Plan(非阻断):src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
…toggles - Use function-form String.replace when inlining the webui UMD bundle, CSS, and template assets so $& / $' / $` sequences in the bundles are not interpreted as substitution patterns - Fail the export-html build if the inlined bundle contains a <script sequence (HTML5 script-data double-escaping hazard with the <!-- sequences markdown-it ships) or if any placeholder survives substitution - Add aria-expanded/aria-label to ReadToolCall toggle buttons and make SearchToolCall's collapsible trigger keyboard reachable, matching the WebFetchToolCall wiring; assert both in the expand-control tests
|
@qwen-code /review --timeout=360 |
|
Qwen Code review request accepted. Review is queued in workflow run. |
doudouOUC
left a comment
There was a problem hiding this comment.
Review of PR #9367 — feat(webui): add global expand/collapse control to exported HTML viewer
Commit reviewed: 1640a8581ed159bb356e2ea58cc08ad4fe208ba3
Files: 15 files, +819/-73
Verification: ✅ typecheck passes, ✅ all 46 tests pass (10 test files), ✅ eslint clean
Summary
This PR adds an optional global "Expand all / Collapse all" toolbar to the ChatViewer component and enables it in the /export HTML template. The implementation is clean, well-scoped, and follows the idiomatic React pattern for broadcasting a global command while preserving individual toggle state.
Design
A new ExpandControlContext carries a monotonic signal counter plus the target expanded state. A useControlledExpanded hook replaces the local useState in each collapsible component:
- Sync semantics: Components sync to the global command only when the
signalchanges (object identity), so unrelated re-renders never disturb local toggle state. - Late-mount inheritance: Sections mounted after a global command inherit the latest target via the
signal > 0initializer guard, rather than falling back to the component default. - Backwards compatibility: Without a provider,
useControlledExpandeddegenerates to plainuseState. The toolbar is opt-in (showExpandControldefaults tofalse).
Coverage
All 6 collapsible surfaces in the codebase are wired:
| Component | File |
|---|---|
| Thinking blocks | ThinkingMessage.tsx |
| File references | CollapsibleFileContent.tsx |
| Read tool output | ReadToolCall.tsx |
| Search tool output | SearchToolCall.tsx |
| Web fetch output | WebFetchToolCall.tsx |
| Shared output (Shell/Generic/Think) | shared/CollapsibleOutput.tsx |
Safety improvements in build.mjs
The export-html build script was hardened significantly:
- No CDN dependency: The
@qwen-code/webuibundle is now built inline via Vite as a UMD bundle, so the exported HTML is fully self-contained and doesn't depend on unpkg availability or version timing. - Function-form
.replace(): Switched to callback replacers to avoid$&/$'/$`substitution pattern corruption in the inlined bundles. <scriptguard: The build throws if the UMD bundle contains a<scriptsequence, preventing HTML5 script-data double-escaping issues.- Residual placeholder check: The build throws if any
__PLACEHOLDER__survives substitution, preventing silent template corruption.
Accessibility
All expand/collapse toggles across the 6 surfaces now carry aria-expanded, aria-label ("Expand output" / "Collapse output"), and the SearchToolCall trigger additionally has role="button", tabIndex={0}, and Enter/Space onKeyDown support.
Tests
The new ChatViewer.expandControl.test.tsx (462 lines, 9 tests) covers:
- No toolbar rendered by default (
showExpandControlnot set) - Toolbar hidden for empty message list
- Expand all expands thinking blocks and long tool outputs
- Collapse all collapses previously expanded sections
- Individual toggles remain functional after a global command
- File references and read outputs respond to global buttons
- Late-mounted sections inherit the latest global target
- SearchToolCall and WebFetchToolCall collapsible outputs respond to global buttons
Previous review findings
All earlier findings have been addressed:
| Finding | Resolution |
|---|---|
| R1-1 (Critical) — CDN dependency | ✅ Webui UMD is now built inline via Vite |
R5-1 (Critical) — $& substitution pattern |
✅ Function-form replacers |
R5-3 (Suggestion) — <script guard |
✅ Build-time <script detection + throw |
| R5-4 (Suggestion) — Read/Search a11y wiring | ✅ aria-expanded/aria-label/role="button" added |
Unused useExpandControl export |
Non-blocking (3 lines); defer to follow-up |
| Late-mount inheritance test coverage | Non-blocking; the behavior is tested in the "mounts sections added after a global command" case |
Verdict
APPROVED — The PR is well-designed, thoroughly tested, and all earlier review findings have been addressed. The typecheck, all 46 tests, and eslint pass cleanly.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. LGTM! ✅
Test Plan (not a blocker): src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/web-templates/src/export-html/build.mjs:122 — [probe] Residual-placeholder guard omits the two React UMD version placeholderspackages/webui/src/components/toolcalls/SearchToolCall.tsx:44 — [probe] SearchToolCall keyboard toggle has no test coveragepackages/webui/src/components/toolcalls/SearchToolCall.tsx:42 — [probe] SearchToolCall aria-label overrides the content-derived accessible namepackages/webui/src/components/ChatViewer/ChatViewer.expandControl.test.tsx:398 — [probe] Late-mount inheritance only tested for the expand directionpackages/web-templates/src/export-html/build.mjs:85 — [review] Inlined webui CSS has no </style escape or guardpackages/webui/src/components/ChatViewer/ChatViewer.expandControl.test.tsx:294 — [probe] Repeat same global command after a local override is unguardedpackages/webui/src/components/toolcalls/ReadToolCall.tsx:270 — [review] ReadToolCall toggle aria-label diverges from visible text (WCAG 2.5.3)packages/webui/src/context/ExpandControlContext.tsx:77 — [probe] Discarded-render survival invariant (state vs ref lastSignal) has no test coverage
中文说明
无阻断问题。LGTM!✅
Test Plan(非阻断):src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory; and 1 more。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 8 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/web-templates/src/export-html/build.mjs:121 — [probe] Residual-placeholder guard omits the two react-version placeholderspackages/web-templates/src/export-html/build.mjs:85 — [review] Inlined CSS lacks the </style fail-closed guard the JS path haspackages/webui/src/components/toolcalls/SearchToolCall.tsx:44 — [probe] New keyboard toggle on the search-output trigger is untestedpackages/webui/src/components/ChatViewer/ChatViewer.tsx:418 — [probe] Runtime showExpandControl toggle remounts the message tree and replays a stale signalpackages/webui/src/components/ChatViewer/ChatViewer.expandControl.test.tsx:395 — [probe] Late-mount inheritance is untested in the collapse direction (mutant survives)packages/webui/src/components/toolcalls/ReadToolCall.tsx:273 (+2 locations) — [review] New aria-labels violate Label in Name on three collapsible toggles
中文说明
Test Plan(非阻断):src/components/toolcalls/shared/CollapsibleOutput.test.tsx — no such file or directory; src/components/toolcalls/ShellToolCall.test.tsx — no such file or directory; src/components/toolcalls/ThinkToolCall.test.tsx — no such file or directory; src/components/toolcalls/GenericToolCall.test.tsx — no such file or directory; src/export-html/build.mjs — no such file or directory。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
|
Released in v0.21.14. |
What this PR does
This PR adds an optional global "Expand all / Collapse all" toolbar to the
ChatViewercomponent and enables it in the/exportHTML template. When the control is enabled, clicking either button broadcasts the command to every collapsible section in the exported page — thinking blocks, tool call outputs, and collapsible file references — so a reader can open or close everything at once instead of toggling each section individually. The toolbar renders above the message list and only appears when there are messages.Internally a new
ExpandControlContextcarries a monotonic signal plus the target expanded state, and auseControlledExpandedhook replaces the localuseStatein each collapsible component (ThinkingMessage,CollapsibleFileContent,ReadToolCall,SearchToolCall,WebFetchToolCall, and the sharedCollapsibleOutputused byShellToolCall/GenericToolCall/ThinkToolCall). Components sync to a global command only when the signal changes, keep their own toggles working between global commands, and inherit the latest target if they mount after a command was issued.Why it's needed
Issue #8208 asks for the exported HTML to match the TUI's expand/collapse experience. Triage confirmed the base rendering of thinking and tool results already exists, and #8251 already replaced hard truncation of long tool output with collapsible sections, so the remaining gap is a page-level control: long exports currently require clicking every section one by one. This PR closes that gap with a small, opt-in UX improvement.
Reviewer Test Plan
How to verify
/exportand open the HTML file: two buttons ("Expand all" / "Collapse all") appear at the top right of the chat container.ChatViewerwithoutshowExpandControlsee no toolbar and unchanged behavior (the prop defaults tofalse, anduseControlledExpandedwithout a provider is identical touseState).Automated coverage:
packages/webui/src/components/ChatViewer/ChatViewer.expandControl.test.tsxrenders a transcript with a thinking block and a long shell output and asserts all of the above.Evidence (Before & After)
Before: exported HTML had per-section toggles only; no page-level control. After: an "Expand all / Collapse all" toolbar appears above the messages and drives every collapsible section. Behavior is covered by the unit tests listed above (DOM-level assertions on
aria-expanded, max-height, and rendered content).Tested on
Environment (optional)
Unit tests + typecheck +
packages/web-templatesexport-html build (node src/export-html/build.mjs).Risk & Scope
showExpandControldefaults tofalse), so only the/exportHTML template is user-visible affected.@qwen-code/webui.Linked Issues
Fixes #8208
中文说明
这个 PR 做了什么
本 PR 为
ChatViewer组件新增了一个可选的全局「Expand all / Collapse all」工具栏,并在/exportHTML 模板中启用。启用后,点击任一按钮会向导出页面中所有可折叠区域广播命令——包括思考块、工具调用输出和可折叠文件引用——读者可以一次性展开或收起全部内容,而不必逐个点击。工具栏渲染在消息列表上方,且仅在有消息时显示。实现上新增
ExpandControlContext,携带一个单调递增的信号和目标展开状态;useControlledExpandedhook 替换了各可折叠组件(ThinkingMessage、CollapsibleFileContent、ReadToolCall、SearchToolCall、WebFetchToolCall,以及ShellToolCall/GenericToolCall/ThinkToolCall共用的CollapsibleOutput)内部的useState。组件仅在信号变化时同步全局命令,两次全局命令之间各自的开关保持可用,命令发出后才挂载的组件会继承最新的目标状态。为什么需要
Issue #8208 希望导出的 HTML 具备与 TUI 一致的展开/收起体验。Triage 确认思考内容与工具结果的基础渲染已经存在,#8251 也已把长工具输出的硬截断改为可折叠区域,因此剩余缺口是页面级控制:长导出目前只能逐个点击每个区域。本 PR 以一个小巧、可选开启的 UX 改进补上这个缺口。
评审者测试计划
如何验证
/export并打开 HTML:聊天容器右上角出现 "Expand all" / "Collapse all" 两个按钮。showExpandControl的程序化使用方不会看到工具栏,行为不变(该 prop 默认false,无 Provider 时useControlledExpanded与useState完全等价)。自动化覆盖:
packages/webui/src/components/ChatViewer/ChatViewer.expandControl.test.tsx渲染包含思考块和长 shell 输出的会话记录,并断言以上全部行为。前后对比证据
改动前:导出 HTML 只有各区域自己的开关,没有页面级控制。改动后:消息列表上方出现 "Expand all / Collapse all" 工具栏,可驱动所有可折叠区域。行为由上文列出的单元测试覆盖(DOM 层断言
aria-expanded、max-height 与渲染内容)。测试环境
环境(可选)
单元测试 + typecheck +
packages/web-templates的 export-html 构建(node src/export-html/build.mjs)。风险与范围
showExpandControl默认false),因此对用户可见的影响仅限/exportHTML 模板。@qwen-code/webui的新导出。关联 Issue
Fixes #8208