Skip to content

feat(ui): add ui.showToolCallArgs to render tool-call arguments inline - #10565

Open
TianYuan1024 wants to merge 20 commits into
QwenLM:mainfrom
TianYuan1024:feat/show-tool-call-args
Open

feat(ui): add ui.showToolCallArgs to render tool-call arguments inline#10565
TianYuan1024 wants to merge 20 commits into
QwenLM:mainfrom
TianYuan1024:feat/show-tool-call-args

Conversation

@TianYuan1024

Copy link
Copy Markdown
Contributor

What this PR does

Adds an opt-in setting, ui.showToolCallArgs (default false), that renders every tool call on its own row with its full raw arguments printed inline underneath the tool header, instead of the type-based compact summary. When it is off nothing changes — the compact baseline stays exactly as it is today.

Turning it on does two things. It stops information-gathering batches (read / search / list) from collapsing into a single summary row, so each call is visible individually. And it prints the arguments the tool was actually invoked with as a one-line JSON row under each header. Arguments are capped at 1000 characters inline so a large file-content parameter cannot bury the conversation; Ctrl+O lifts that cap, and continues to own result-output expansion exactly as before. The setting deliberately does not touch thinking-block folding or result truncation — it only ever adds the arguments row.

The raw arguments are carried on the tool display object from both the live scheduler and the session-resume path, so a resumed session shows the same rows as a live one. They are held as the same object reference the scheduler already keeps, so there is no extra copy. The daemon boundary does not carry arguments, so the row is simply skipped there, and the key is registered as TUI-only — the web shell neither exposes nor persists it.

One detail worth calling out: MCP invocations already return their serialized parameters as the tool description, so for those the arguments row would print the same payload twice. It is suppressed in that case, and a description that merely resembles JSON still gets its arguments row.

Why it's needed

The main transcript folds information-gathering batches into a single summary line, and each tool header shows only the invocation's human-readable summary. For most built-in tools that summary drops the parameters entirely — an edit shows just the filename, a read just the path. There is currently no way to see what a tool was actually called with while it stays in the main view, which makes debugging an MCP server or a tool schema considerably harder than it needs to be.

This is intentionally a narrow switch for tool-call verbosity rather than a revival of the old global compact/verbose mode, which was deliberately retired. The default remains the single stable compact baseline; this only gives people who are debugging tool integrations a way to opt into seeing the calls in full.

Reviewer Test Plan

How to verify

Put {"ui": {"showToolCallArgs": true}} in your settings and start an interactive session. Ask something that fans out into several file reads and searches. With the setting off you should see those calls folded into one summary row, as today. With it on, each call should occupy its own row with its arguments printed beneath it — including the parameters that are normally summarized away, such as the old and new strings on an edit.

If you have an MCP server configured, invoke one of its tools and confirm its payload appears exactly once rather than twice. Then press Ctrl+O and confirm result output expands and the argument row is no longer truncated. Finally remove the setting and restart to confirm the view returns to the current compact baseline with no residue.

The unit suites covering the affected areas pass, including new cases for the MCP de-duplication, the character cap and its lifting under full detail, unserializable arguments, the daemon path where no arguments exist, and the web shell rejecting the key.

Evidence (Before & After)

Rendering the real tool-group and tool-message components with a mixed batch (two information-gathering calls, an edit, and an MCP call):

Before (ui.showToolCallArgs absent or false — unchanged behavior)

✓ Searched 'foo' in src, read src/a.ts
✓ Edit src/a.ts
✓ mcp__github__list_issues {"owner":"QwenLM","repo":"qwen-code","state":"open"}

After (ui.showToolCallArgs: true)

✓ ReadFile src/a.ts
  {"absolute_path":"/repo/src/a.ts","limit":50}
✓ Grep 'foo' in src
  {"pattern":"foo","path":"src","include":"*.ts"}
✓ Edit src/a.ts
  {"file_path":"src/a.ts","old_string":"const a = 1","new_string":"const a = 2"}
✓ mcp__github__list_issues {"owner":"QwenLM","repo":"qwen-code","state":"open"}

Note the last row: the MCP payload is printed once, not twice, and the edit now exposes the parameters that the compact header omits.

Tested on

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

Environment (optional)

Unit tests and component-level rendering of the real components on macOS. The interactive session steps above were not driven end to end in a live terminal.

Risk & Scope

  • Main risk or tradeoff: the setting is off by default and every new code path is gated on it, so the default rendering is untouched. The one always-on change is that raw arguments now ride along on the in-memory tool display object; it is the same object reference the scheduler already holds, so it costs no extra allocation, but it does mean the existing history-item escaping pass now walks it too.
  • Not validated / out of scope: the interactive verification steps above were reasoned through and covered by unit and component tests rather than driven in a live terminal; Windows and Linux rely on CI. Two collapse paths are deliberately left alone — pure parallel-agent groups, because bypassing that panel reintroduces a known duplicate-roster and screen-clearing bug, and the memory-operation badge, which is unrelated to inspecting tool arguments.
  • Breaking changes / migration notes: none. New optional setting, default off.

Separately, and left out of this PR on purpose: the compact summary line discards any description that parses as a JSON object, on the assumption that it is an error fallback. That misfires on MCP tools, whose descriptions are legitimately JSON. In practice MCP calls are never routed into that summary, so it is not reachable today — but it is worth a follow-up.

Linked Issues

Closes #9767

中文说明

这个 PR 做了什么

新增一个可选设置 ui.showToolCallArgs(默认 false):开启后,每个工具调用各占一行,并在工具名下方内联打印它被调用时的完整原始参数,取代当前按类型折叠的精简摘要。关闭时行为完全不变——精简基线与今天完全一致。

开启后做两件事。一是让信息收集类的批次(读取 / 搜索 / 列举)不再折叠成单行摘要,每个调用都单独可见。二是在每个工具名下方以单行 JSON 打印该工具实际收到的参数。参数在内联时截断到 1000 字符,避免超大的文件内容参数淹没整个对话;按 Ctrl+O 可解除该上限,并且它仍然像以前一样负责结果输出的展开。这个设置刻意不改动思考块折叠与结果截断——它只会新增参数行。

原始参数由实时调度路径和会话恢复路径共同带到工具展示对象上,因此恢复的会话与实时会话呈现完全相同的行。参数持有的是调度器本来就持有的同一个对象引用,没有额外拷贝。守护进程边界不传递参数,那条路径会直接跳过该行;该配置项同时被登记为仅限终端界面使用,Web Shell 既不暴露也不写入它。

有一点值得单独说明:MCP 调用的工具描述本身就是序列化后的参数,因此对它们而言参数行会把同一份内容打印两次。这种情况下参数行会被抑制;而仅仅"长得像" JSON 的描述仍然会正常渲染参数行。

为什么需要

主对话视图会把信息收集类批次折叠成一行摘要,而每个工具名后面只显示该次调用的人类可读摘要。对大多数内置工具而言,这个摘要完全丢弃了参数——编辑只显示文件名,读取只显示路径。目前没有任何办法在主视图里看到工具实际是用什么参数调用的,这让调试 MCP 服务或工具 schema 变得比应有的困难得多。

这里刻意做成一个窄口径的工具调用详细度开关,而不是复活已被有意移除的全局精简/详细二态模式。默认仍然是那个单一、稳定的精简基线;本改动只是给正在调试工具集成的人一个选择,让他们能完整看到调用内容。

复核测试方案

如何验证

在设置中写入 {"ui": {"showToolCallArgs": true}} 并启动交互式会话。提一个会触发多次文件读取与搜索的问题。设置关闭时,这些调用应当像今天一样折叠成一行摘要;开启后,每个调用应各占一行,参数打印在其下方——包括平时被摘要掉的参数,例如编辑操作的原字符串与新字符串。

如果你配置了 MCP 服务,调用它的某个工具,确认其参数只出现一次而非两次。然后按 Ctrl+O,确认结果输出展开、参数行不再被截断。最后删除该设置并重启,确认视图回到当前的精简基线且没有任何残留。

受影响区域的单元测试全部通过,其中新增用例覆盖了 MCP 去重、字符上限及其在全详情模式下的解除、无法序列化的参数、守护进程路径下参数缺失的情形,以及 Web Shell 拒绝该配置键。

证据(前后对比)

用真实的工具组与工具消息组件渲染一个混合批次(两个信息收集类调用、一个编辑、一个 MCP 调用):

改动前ui.showToolCallArgs 未设置或为 false——行为不变)

✓ Searched 'foo' in src, read src/a.ts
✓ Edit src/a.ts
✓ mcp__github__list_issues {"owner":"QwenLM","repo":"qwen-code","state":"open"}

改动后ui.showToolCallArgs: true

✓ ReadFile src/a.ts
  {"absolute_path":"/repo/src/a.ts","limit":50}
✓ Grep 'foo' in src
  {"pattern":"foo","path":"src","include":"*.ts"}
✓ Edit src/a.ts
  {"file_path":"src/a.ts","old_string":"const a = 1","new_string":"const a = 2"}
✓ mcp__github__list_issues {"owner":"QwenLM","repo":"qwen-code","state":"open"}

请注意最后一行:MCP 的参数只打印了一次而不是两次;编辑操作则暴露出了精简表头省略掉的参数。

测试平台

操作系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

运行环境(可选)

在 macOS 上运行单元测试,并对真实组件做了组件级渲染。上述交互式会话步骤未在实际终端中端到端跑通。

风险与范围

  • 主要风险或取舍:该设置默认关闭,所有新增代码路径都由它把关,因此默认渲染完全未受影响。唯一始终生效的改动是原始参数现在会随内存中的工具展示对象一起携带;它是调度器本来就持有的同一个对象引用,不产生额外分配,但这确实意味着既有的历史条目转义遍历现在也会走到它。
  • 未验证 / 不在范围内:上述交互式验证步骤是通过推演并由单元测试与组件测试覆盖的,未在实际终端中驱动;Windows 与 Linux 依赖 CI。有两处折叠路径被刻意保留未动——纯并行子代理组,因为绕过该面板会重新引入已知的重复列表与清屏问题;以及内存操作徽章,它与查看工具参数无关。
  • 破坏性变更 / 迁移说明:无。新增可选设置,默认关闭。

另外,有一点刻意未纳入本 PR:精简摘要行会丢弃任何能解析成 JSON 对象的描述,因为它假定那是错误回退产生的内容。这对 MCP 工具是误伤——它们的描述本来就是合法的 JSON。实践中 MCP 调用不会被路由进那条摘要,所以目前无法触发,但值得后续单独处理。

关联 Issue

Closes #9767

The main transcript folds read/search/list batches into a single "Read 3
files" summary line, and every tool header shows only the invocation's
human summary — which for most built-in tools drops the parameters
entirely (Edit shows just the filename). That leaves no way to see what a
tool was actually called with while debugging an MCP server or a tool
schema.

Add an opt-in `ui.showToolCallArgs` (default false, so the compact
baseline is unchanged). When on, the type-based partition is disabled so
every call renders on its own row, and the raw arguments print as a JSON
row under the tool header. Args are capped at 1000 characters inline;
Ctrl+O lifts the cap and expands result output as before.

Raw arguments are carried on the tool display object from both the live
scheduler and the resume path, so a resumed session shows the same rows
as a live one. The daemon path carries no args, so the row is skipped
there, and the setting is TUI-only — the web shell is unaffected.

MCP invocations already return their serialized params as the
description, so the args row is suppressed when it would print the same
payload twice.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 30, 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 Aug 30, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

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

Test Plan (not a blocker): src/a.tsno such file or directory.

中文说明

已审查。 建议见行内评论。

Test Plan(非阻断):src/a.tsno such file or directory

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

Comment thread packages/cli/src/ui/hooks/useReactToolScheduler.ts
Comment thread packages/cli/src/ui/components/messages/ToolMessage.tsx
Comment thread packages/cli/src/config/settingsSchema.ts Outdated
Comment thread packages/cli/src/ui/components/messages/ToolGroupMessage.tsx Outdated
Comment thread packages/cli/src/ui/components/messages/ToolMessage.tsx Outdated
Comment thread packages/cli/src/ui/components/messages/ToolMessage.test.tsx Outdated
Comment thread packages/cli/src/ui/types.ts
Sanitize the arguments row through the shared sanitizeTerminalText
pipeline. JSON.stringify escapes C0 controls and escapeAnsiCtrlCodes
neutralizes ESC-prefixed sequences, but neither strips Unicode bidi
overrides, so a malicious argument could visually reorder the very
payload the row exists to expose (Trojan Source, CVE-2021-42572). The
sanitizer runs last so the dedup comparison and the hidden-character
count still read the raw JSON.

Carry the raw arguments through the two remaining builders of the tool
display object — the agent view and the accepted-speculation renderer —
so the setting no longer half-applies, showing arguments in the main
transcript but silently never in those views. The agent view needed the
value threaded through its intermediate call map as well as the message
metadata.

Only tear down the compact partition when the group actually has
arguments to show. Daemon-attached sessions carry none across the
boundary, so gating on the setting alone expanded every fold while
rendering zero argument rows — a noisier transcript that reads as "these
tools were called without arguments". Memory-only groups now expand too,
since the "Wrote 1 memory" badge hid exactly the parameters the setting
recovers; groups of running parallel subagents deliberately keep their
compact roster, because expanding them alongside the live agent panel
reintroduces the viewport overflow and scroll-snap loop of QwenLM#5798. The
setting description and the docs row now state both exceptions instead
of promising every tool call.

Reuse the exported key-hint constant in the truncation suffix so the
message cannot drift from the actual binding, and pin the cap test to
the exact output: it previously passed with the cap mutated to 100 or
the hidden-character arithmetic broken.

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

Test Plan (not a blocker): src/a.tsno such file or directory.

中文说明

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

Test Plan(非阻断):src/a.tsno such file or directory

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

Comment thread docs/users/configuration/settings.md Outdated
Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment thread packages/vscode-ide-companion/schemas/settings.schema.json Outdated
… cap

The truncated arguments row tells the user to press Ctrl+O for the rest,
but the agent view never passed full-detail down to its transcript
items, so the key did nothing there. Thinking blocks in that view
already honored the toggle, since the item renderer reads the context
itself; only the tool side was missing. Read the same state the main
transcript reads and forward it to both render sites, which makes the
advertised escape hatch real rather than qualifying it away in prose.

Extract the accepted-speculation display builder out of the submit
handler so it can be unit-tested like the other three builders of the
tool display object. It was the only one of the four with no test
pinning that it carries the raw arguments, so a later refactor dropping
them would have shipped green.

The settings dialog renders the setting's description and was the only
surface not mentioning that inline rows are truncated at 1000
characters; say so there, and stop calling the rows "full".
The new agent-view test built a ThoughtExpandedValue with `toggleExpanded`
instead of `toggle`, which broke `tsc --build` and failed the CLI package
build before any test or lint step could run.

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): chunk 3: executed verification of the three test files in my territory ( agentHistoryAdapter.test.ts , AgentChatContent.fullDetail.test.tsx , ToolGroupMessage.test.tsx….

Test Plan (not a blocker): src/a.tsno such file or directory.

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/components/messages/ToolMessage.tsx:932 — [probe] fullDetail→uncapped wiring untested
  • docs/users/configuration/settings.md:140 — [review] settings.md conflates the two compact-view cases
  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1149 — [probe] Args cap splits surrogate pairs (UTF-16 slice)
  • packages/cli/src/ui/components/agent-view/agentHistoryAdapter.test.ts:354 — [probe] Adapter tool_result merge branch unpinned for args
  • packages/cli/src/ui/components/messages/ToolGroupMessage.tsx:237 — [probe] Group-expand .some() quantifier unpinned
中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):chunk 3:executed verification of the three test files in my territory ( agentHistoryAdapter.test.ts , AgentChatContent.fullDetail.test.tsx , ToolGroupMessage.test.tsx…

Test Plan(非阻断):src/a.tsno such file or directory

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。

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

Comment thread packages/cli/src/ui/components/agent-view/AgentChatContent.tsx
The agent-view test fixture held only an assistant message, so every
item was committed and the live area never rendered — the test captured
one of the two render sites and a mutation dropping full-detail from the
executing/confirming block survived it. Add an unmatched tool call, which
the adapter maps to a still-executing tool group and the committed/live
split keeps in the live area, and assert both sites receive the flag.

Without this a running tool group would keep advertising ctrl+o for a key
that does nothing on it, which is the half-bug the agent-view wiring set
out to remove.
@qqqys

qqqys commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

tmux e2e report — head 5c6ef8ce95

Verified ui.showToolCallArgs in an interactive terminal (tmux) against a full source build (npm ci + npm run bundle) of this PR's head. A/B run in a scratch workspace with the setting in workspace-scoped .qwen/settings.json; each case is a fresh session with the same prompt (three separate file reads).

Setting on — each call renders on its own row with its raw args underneath; the read batch is not folded into a summary row:

✓ ReadFile one.txt
  {"file_path":"/tmp/e2e-10565/one.txt"}
✓ ReadFile two.txt
  {"file_path":"/tmp/e2e-10565/two.txt"}
✓ ReadFile three.txt
  {"file_path":"/tmp/e2e-10565/three.txt"}

Ctrl+O expanded thoughts and tool results while the args rows stayed in place (main-view full detail composes with the setting as described).

Resumed session — quit, then --resume <session-id> with the setting still on: the restored history renders the same three rows with args lines, so the resume builder carries the persisted args as advertised.

Setting off (control) — the compact baseline is unchanged, folding the batch into one row with no args lines:

✓ Read one.txt, two.txt, three.txt

Review result: reviewed the full diff at head 5c6ef8ce95 — no merge-blocking issue found. The round-2 items are all addressed at this head: the agent view forwards fullDetail at both render sites (with a test pinning both), the accepted-speculation builder is extracted as buildSpeculativeToolDisplays with a dedicated args test, and the schema description now discloses the 1000-char cap and the Ctrl+O lift. Also verified: the args row goes through sanitizeTerminalText (bidi override/isolate chars are stripped), daemon-attached groups stay compact via the hasRenderableToolCallArgs gate, and ui.showToolCallArgs is rejected by POST /workspace/settings as TUI-only.

— tmux e2e · Qwen Code (built from this PR head) · 2026-08-31

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

Partially reviewed — gaps disclosed.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • surrogate-pair split in the inline-args cap (ToolMessage.tsx:1149) — already reported (round-3 deferred list, review 5063380208)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): src/a.tsno such file or directory.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx:147 — [probe] parallel-agent panel exemption unpinned by tests
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

Test Plan(非阻断):src/a.tsno such file or directory

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

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

Truncating the arguments row counted UTF-16 code units, so the cut could
land between the two halves of a surrogate pair — an emoji or a
supplementary-plane character in an argument — and leave a lone high
surrogate that the terminal draws as a replacement glyph. Drop the
orphan and count it among the hidden characters, so the reported total
stays honest.

Also pin the parallel-agent carve-out. Running agents deliberately keep
their dense roster under this setting, because rendering them inline
while the live agent panel also lists them overflows the viewport and
triggers the clear-screen scroll-snap loop of QwenLM#5798. No test held that
exemption in place, so a later edit made in the name of consistency
could have reintroduced the bug silently.

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): src/a.tsno such file or directory.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/components/messages/ToolGroupMessage.tsx:230 — [review] D5-1: daemon 'no args across boundary' premise is false — rawInput is on the wire (transcript-replay.ts:359), the TUI adapter drops it, so ui.showToolCallArgs silen…
中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

Test Plan(非阻断):src/a.tsno such file or directory

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

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

Comment thread packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx Outdated
… frame

The test rendered the real InlineParallelAgentsDisplay against the bare
mock config, whose missing background-task registry made the render
throw; ink swallowed it and the frame came out empty. Its only assertion
was a negation, which an empty frame satisfies on its own, so the test
held nothing in place.

Give it the stubbed registry config the rest of this file already uses
for that component, and assert the panel actually rendered before
asserting the per-agent rows did not.

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

Partially reviewed — gaps disclosed.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R6-1 settings.md conflates the two compact-view cases under a false shared reason (settings.md:140) — already reported (round-3 deferred list, review 5063380208)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): chunk 4: executing ToolGroupMessage.test.tsx / ToolMessage.test.tsx to confirm green — blocked by the vitest globalSetup build guard, and the prerequisite npm run b….

Test Plan (not a blocker): src/a.tsno such file or directory.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1160 — [probe] D6-1: SessionPreview advertises a dead (ctrl+o) hint on capped args rows — the preview never forwards fullDetail nor handles Ctrl+O
  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1160 — [probe] D6-2: inline-args truncation suffix is hardcoded English, bypassing the i18n t() every sibling transcript hint uses
  • docs/users/configuration/settings.md:140 — [probe] D6-3: 'running' qualifier understates the status-agnostic parallel-agent compact-view carve-out (schema + vscode schema too)
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):chunk 4:executing ToolGroupMessage.test.tsx / ToolMessage.test.tsx to confirm green — blocked by the vitest globalSetup build guard, and the prerequisite npm run b…

Test Plan(非阻断):src/a.tsno such file or directory

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。

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

@TianYuan1024

Copy link
Copy Markdown
Contributor Author

@wenshao @chiga0 @yiliang114 Could you take a look when you have a moment? /review has converged and CI is green — just needs a human approval.

@wenshao

wenshao commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the pre-execution risk screen refused this sponsored run: the diff adds long opaque single-line content. A maintainer who has reviewed the diff can run the verification manually in a disposable environment.

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the pre-execution risk screen refused this sponsored run: the diff adds long opaque single-line content. A maintainer who has reviewed the diff can run the verification manually in a disposable environment。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — every section is filled in, including the bilingual summary and an honest "not driven in a live terminal" note.

Problem: this answers an open feature request (#9767), not a hypothesis — users debugging MCP integrations currently have no way to see tool-call arguments inline, since the compact baseline summarizes them away and Ctrl+O moved to opening the transcript. The ask in the issue (a setting, default off, each call rendered with its full args, web shell unaffected) matches what this PR delivers.

Direction: aligned. The retired ui.compactMode was deliberately removed, and this does not revive it — it is a narrow, opt-in verbosity switch for tool calls only, with the compact baseline untouched when off. Claude Code likewise keeps a persisted verbose setting alongside its compact default, so the area is a live product concern rather than a one-off.

Size: touches core-ish paths (packages/cli/src/config/settingsSchema.ts, plus the VS Code companion schema). Breakdown: 269 production lines (including 9 one-line locale entries), 651 test lines, 69 doc lines, 5 schema lines — well under the 500-line maintainer-awareness bar for feat PRs. No Stage 0 escalation needed; the change proceeds under the usual 100%-confidence bar for core paths.

Approach: scope feels right for the stated goal. The two visible behaviors — unfolding information-gathering batches and printing the raw args row — are both necessary to satisfy the issue (collapsed rows would hide individual calls). The carve-outs are reasoned rather than scope creep: MCP payloads already surface as the tool description, so deduping them avoids double-printing; the daemon boundary carries no args, so the row is skipped there; the key is registered TUI-only so the web shell neither shows nor persists it. The follow-up about JSON-looking descriptions being dropped by the summary line is correctly left out of this PR. One thing I'll look at closely in code review is the always-on part — raw args now ride along on the tool display object even when the setting is off.

Risk: no elevated risk signals — no revert-correlated paths touched.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ —— 各部分齐全,包含双语说明,并如实标注了"未在实际终端中驱动"。

问题:本 PR 响应的是一个开放的 feature request(#9767),而非假设——调试 MCP 集成的用户目前无法在主视图中内联看到工具调用参数,因为精简基线会把参数摘要掉,而 Ctrl+O 已改为打开完整记录。issue 中的诉求(一个默认关闭的设置、每个调用连同完整参数各占一行、不影响 web shell)与本 PR 的交付一致。

方向:对齐。已移除的 ui.compactMode 是有意下线的,本 PR 并未复活它——只是一个窄口径、可选开启的工具调用详细度开关,关闭时精简基线完全不变。Claude Code 同样在精简默认之外保留了持久化的 verbose 设置,说明这是一个真实的产品关注点。

规模:触及核心路径(packages/cli/src/config/settingsSchema.ts,以及 VS Code 伴侣扩展的 schema)。拆分:269 行生产代码(含 9 行单行 locale 条目)、651 行测试、69 行文档、5 行 schema——远低于 feat 类 PR 500 行的维护者关注线。无需 Stage 0 升级;按核心路径的 100% 置信标准继续评审。

方案:范围与目标匹配。两个可见行为——展开信息收集类批次、打印原始参数行——对满足 issue 都是必要的(折叠行会隐藏单个调用)。各处取舍是有理据的,不属于范围蔓延:MCP 的载荷本身就是工具描述,去重可避免重复打印;守护进程边界不携带参数,该行直接跳过;配置项登记为仅限终端界面,web shell 既不显示也不持久化。关于"长得像 JSON 的描述会被摘要行丢弃"的后续问题被正确地留在本 PR 之外。代码审查阶段我会重点看始终生效的那部分——即使设置关闭,原始参数现在也会随工具展示对象一起携带。

风险:无升级风险信号——未触及与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

I wrote my independent proposal before reading the diff (schema key → carry the scheduler's existing args reference onto the display object → render a capped one-line JSON row under each header → stop folding batches → dedupe MCP). The PR matches it point for point, and beats it in two places: the expansion gate is hasRenderableToolCallArgs (setting on and args actually present) rather than the raw setting, so daemon-attached sessions don't unfold every fold into zero-args noise; and it wires Ctrl+O fullDetail into the agent view's both render sites, because a truncated args row advertising (ctrl+o) for a key that does nothing there would be a small lie.

Security surface is handled: the args row is model- and MCP-controlled text and goes through sanitizeTerminalText (ANSI escape neutralization + bidi-override stripping — the Trojan Source case is pinned by a test), sanitization runs last so the +N chars accounting stays honest about the raw JSON, surrogate-pair boundaries at the 1000-char cap are handled and pinned, circular args fall back to no row instead of throwing, and the TUI-only rejection on POST /workspace/settings is tested. The MCP dedup is conservative: only an exact JSON-equivalent description suppresses the row, a lookalike still renders it. The parallel-agent carve-out is pinned with a positive-then-negative assertion so an empty frame can't pass the test vacuously.

Two non-blocking notes:

  1. The PR description is stale on one point. Risk & Scope says the memory-operation badge is deliberately left untouched, but the diff deliberately bypasses it when the setting is on (allMemOpsComplete gains !hasRenderableToolCallArgs), with a test pinning exactly that and a comment explaining why — "Wrote 1 memory" would hide the parameters this setting exists to surface. The code's behavior looks right; the description should be updated so nobody merges under a wrong mental model.
  2. Always-on cost, disclosed but worth naming: args ride on the display object even when the setting is off (same reference, no copy — fine), but HistoryItemDisplay's escapeAnsiCtrlCodes(item) walk now traverses them on every tool-group render. The walk only clones when escaping is needed, so the cost is a scan, but a 500 KB WriteFile content arg gets scanned regardless of the setting. Accepted tradeoff for the shared escaping pass; just make sure nobody later assumes the walk is free.

Everything else follows house conventions: kebab-case hook file mirroring the useMouseTrackingEnabled pattern (raw context read, mounts without a provider), tests colocated and genuinely adversarial (vacuous-pass traps, oracle pinning against the documented 1000-char cap), no any, comments explain why.

Files changed (31 files, grouped into 23 rows)
File What changed
packages/cli/src/config/settingsSchema.ts new ui.showToolCallArgs key: boolean, default false, shown in the settings dialog, no restart
packages/cli/src/i18n/locales/*.js (9 files) translated label for the new settings-dialog entry
packages/cli/src/serve/routes/workspace-settings.ts key added to the TUI-only rejection list for the web shell
packages/cli/src/ui/types.ts optional args field on IndividualToolCallDisplay with the daemon-path contract documented
packages/cli/src/ui/hooks/use-show-tool-call-args.ts new hook reading the merged setting, non-throwing without a provider
packages/cli/src/ui/hooks/useReactToolScheduler.ts mapToDisplay carries the scheduler's request.args reference
packages/cli/src/ui/AppContainer.tsx speculation builder extracted as buildSpeculativeToolDisplays and carrying args
packages/cli/src/ui/utils/resumeHistoryUtils.ts resumed sessions carry the persisted functionCall args
packages/cli/src/ui/components/agent-view/agentHistoryAdapter.ts agent view forwards args from tool_call metadata
packages/cli/src/ui/components/agent-view/AgentChatContent.tsx Ctrl+O fullDetail forwarded to committed and live render sites
packages/cli/src/ui/components/messages/ToolGroupMessage.tsx hasRenderableToolCallArgs gates batch unfolding and the memory badge
packages/cli/src/ui/components/messages/ToolMessage.tsx args row rendering plus formatInlineToolArgs (cap, MCP dedup, sanitize, surrogate safety)
docs/users/configuration/settings.md settings-table entry documenting cap, Ctrl+O, and the two compact carve-outs
packages/vscode-ide-companion/schemas/settings.schema.json companion schema entry
packages/cli/src/config/settingsSchema.test.ts pins type, default false, dialog visibility, no-restart
packages/cli/src/serve/routes/workspace-settings.test.ts web shell rejects the TUI-only key
packages/cli/src/ui/AppContainer.test.tsx speculation carries args; empty-args fallback renders no row
packages/cli/src/ui/hooks/useReactToolScheduler.test.tsx args carried on success and error branches
packages/cli/src/ui/utils/resumeHistoryUtils.test.ts resumed history carries the persisted args
packages/cli/src/ui/components/agent-view/agentHistoryAdapter.test.ts agent view forwards args
packages/cli/src/ui/components/agent-view/AgentChatContent.fullDetail.test.tsx pins fullDetail at both agent-view render sites
packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx on/off, daemon-compact, empty-args, memory badge, parallel-agent exemption
packages/cli/src/ui/components/messages/ToolMessage.test.tsx formatInlineToolArgs unit suite plus render on/off, MCP once-not-twice, daemon absence

Testing — the PR's own CI, read via the API

CI on the reviewed head is fully settled — every pull_request-event workflow run completed, nothing pending. The meaningful lanes are green: the full ubuntu unit suite, no-AK integration tests, Serve A/B, the web-shell smoke, and both desktop-shell builds. The macOS/Windows unit legs read skipped — that is workflow design, not a failure: ci.yml runs them only for merge-queue/schedule/dispatch events, never on pull_request. The redacted lane here is live TUI behavior — this run is unattended CI, so I did not build or execute PR code. There is a third-party tmux e2e report in this thread (qqqys, 2026-08-31) against a full source build of head 5c6ef8ce95: setting-on rows with args, off-control unchanged, resume carrying args, Ctrl+O composing — attributed as that report, not re-run by me. The current head is two commits newer; both are the surrogate-cap fix and its test plus a test-hardening commit, each pinned by the unit tests listed above.

Check Conclusion
Qwen Code CI / Test (ubuntu-latest, Node 22.x) ✅ success
Qwen Code CI / Integration Tests (no-AK, No Sandbox) ✅ success
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Qwen Code CI / Desktop Shell (ubuntu-22.04) ✅ success
Qwen Code CI / Desktop Shell (windows-2022) ✅ success
Serve A/B / Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Security Checks / Secret scan (TruffleHog) ✅ success
Security Checks / Dependency CVE audit ✅ success
SDK Java ✅ success
Qwen Code CI / Test (macos-latest, Node 22.x) ⏭️ skipped (merge-queue-only by workflow design)
Qwen Code CI / Test (windows-latest, Node 22.x) ⏭️ skipped (merge-queue-only by workflow design)
Qwen Code CI / Integration Tests (CLI, No Sandbox) ⏭️ skipped

Sandboxed verification would settle the remaining gap: @qwen-code /verify — that the args row renders (and stays hidden when off) on the exact head is currently backed by unit tests plus a tmux report from two commits back, not by a run at 49bb0e82. The author lacks write access, so this is a sponsored run: a maintainer's @qwen-code /verify comment approves the head it is written against, the run carries a pre-execution risk screen and a full workspace wipe, and its report should be read with the same skepticism as the fork's own CI logs — the code under verification is adversarial input, and a crafted PR can shape what the report says even though the sandbox bounds what it can do. (A prior sponsored attempt was refused by the risk screen over long opaque single-line content in the diff; the tmux lane is unavailable to this author.)

中文说明

代码审查

读 diff 之前我先写了独立方案(设置键 → 把调度器已持有的 args 引用带到展示对象 → 在每个工具名下渲染有上限的单行 JSON → 停止批次折叠 → MCP 去重)。PR 与之一一对应,且有两处更好:展开门槛是 hasRenderableToolCallArgs(设置开启确实有参数)而非裸设置,守护进程会话不会把每个折叠展开成零参数噪音;同时把 Ctrl+O 的 fullDetail 接入了子代理视图的两个渲染点——否则被截断的参数行会为一个在该视图无效的按键做广告。

安全面已处理:参数行是模型与 MCP 可控文本,经过 sanitizeTerminalText(ANSI 转义中和 + bidi 覆盖字符剥离,Trojan Source 场景有测试钉住);净化放在最后,+N chars 计数对原始 JSON 保持诚实;1000 字符上限处的代理对边界已处理并有测试;循环引用的 args 回退为不渲染而不是抛异常;web shell 对 TUI-only 键的拒绝有测试。MCP 去重是保守的:只有 JSON 完全等价的描述才会抑制参数行。并行子代理豁免用"先正向后反向"的断言钉住,空帧无法使测试空过。

两条非阻塞意见:

  1. PR 描述有一处过时。"风险与范围"说内存操作徽章刻意未动,但 diff 在设置开启时有意绕过徽章(allMemOpsComplete 增加了 !hasRenderableToolCallArgs),并有测试与注释说明理由——"Wrote 1 memory" 会藏起这个设置恰恰要暴露的参数。代码行为是对的;描述应当更新,避免有人在错误的心智模型下合并。
  2. **始终生效的成本,已披露但值得点名:**即使设置关闭,args 也随展示对象携带(同一引用、无拷贝——没问题),但 HistoryItemDisplayescapeAnsiCtrlCodes(item) 遍历现在会在每次工具组渲染时走到它们。该遍历只在需要转义时才克隆,成本是一次扫描;不过 500 KB 的 WriteFile content 参数无论设置开关都会被扫到。这是共享转义通路的可接受取舍;只是别让后人误以为这次遍历是免费的。

其余均符合仓库惯例:kebab-case 的 hook 文件镜像 useMouseTrackingEnabled 模式(读原始 context,无 provider 也能挂载)、测试与源码同目录且真正有对抗性(空过陷阱、对着文档承诺的 1000 字符上限钉住 oracle)、无 any、注释解释"为什么"。

测试 —— 通过 API 读取本 PR 自己的 CI

被审 commit 上的 CI 已全部落定——所有 pull_request 事件的 workflow 运行均已完成,无 pending。关键通道全绿:ubuntu 完整单测、无 AK 集成测试、Serve A/B、web-shell 冒烟、两个桌面壳构建。macOS/Windows 单测腿显示 skipped——这是 workflow 设计,不是失败:ci.yml 只在 merge-queue/定时/手动触发时运行它们。本运行缺失的通道是真实终端行为——本次是无人值守 CI,未构建或执行 PR 代码。本帖有一份针对 head 5c6ef8ce95 完整源码构建的第三方 tmux e2e 报告(qqqys,2026-08-31):开启时的参数行、关闭时的对照不变、恢复会话携带参数、Ctrl+O 叠加效果——按该报告归属引用,并非我重跑。当前 head 新两个 commit:代理对上限修复及其测试、测试加固,均由上述单测钉住。

沙箱验证

剩余缺口可用 @qwen-code /verify 收口:参数行在精确 head 上的渲染(及关闭时的隐藏)目前由单测加两份 commit 前的 tmux 报告支撑,尚无针对 49bb0e82 的运行。作者无写权限,因此这是一次代跑(sponsored run):维护者评论 @qwen-code /verify 即批准其所针对的 head;该运行带有执行前风险筛查与完整工作区清理,报告应像对待 fork 自身 CI 日志一样保持怀疑——被验证的代码是对抗性输入,精心构造的 PR 可以影响报告_怎么说_,尽管沙箱限定了它_能做什么_。(此前一次代跑因 diff 中的长不透明单行内容被风险筛查拒绝;该作者不可用 tmux 通道。)

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — a clean, well-tested delivery of a requested feature; the two nits (stale description line, always-on walk note) don't block.

Stepping back: this is what a good fork contribution looks like. It answers an open feature request (#9767) with exactly the scope the issue asked for — an opt-in switch, default off, compact baseline untouched. My independent proposal and the diff converged on the same shape, and where the PR diverged it was smarter than my baseline: gating the unfold on renderable args rather than the raw setting keeps daemon-attached sessions compact, and wiring Ctrl+O into the agent view closes a hint that would otherwise advertise a dead key.

The author has clearly iterated — the commit history is a sequence of review-round responses (agent-view fullDetail, cap disclosure, surrogate safety, vacuous-test hardening), and the test suite is better than the diff had to be: oracle pinning against the documented 1000-char cap, a Trojan-Source bidi case, empty-frame traps closed with positive-then-negative assertions. CI on the reviewed head is fully green and fully settled. The one live-terminal signal is a third-party tmux report from two commits back; the delta since is exactly the surrogate-cap fix and two test commits, all unit-pinned, and the sponsored @qwen-code /verify lane named above can close that last gap if a maintainer wants it.

Not a 5 because of the one-liner in the PR body claiming the memory badge is untouched when the diff deliberately (and sensibly) bypasses it under the setting, and because the always-on escape-walk over carried args deserves to be remembered when someone profiles history rendering later. Neither blocks.

Approving. 🚀

中文说明

置信度:4/5 —— 对一个已立项需求的干净、测试充分的交付;两条小意见(描述过时的一行、始终生效的遍历说明)不构成阻塞。

退一步看:这是一次质量很高的 fork 贡献。它以 issue 所要求的范围回应了一个开放的 feature request(#9767)——一个可选开关,默认关闭,精简基线完全不变。我的独立方案与 diff 收敛到同一形状,而 PR 偏出的两处比我的基线更聪明:以"有可渲染参数"而非裸设置作为展开门槛,使守护进程会话保持精简;把 Ctrl+O 接入子代理视图,堵住了一个否则会宣传无效按键的提示。

作者显然在多轮迭代——提交历史是一连串评审回应(子代理视图 fullDetail、上限披露、代理对安全、空测试加固),测试套件比 diff 的最低要求更好:对着文档承诺的 1000 字符上限钉住 oracle、Trojan Source bidi 用例、用先正向后反向的断言封住空帧陷阱。被审 head 上的 CI 全绿且全部落定。唯一的真实终端信号来自两份 commit 之前的第三方 tmux 报告;其后的差量恰好是代理对修复与两个测试 commit,均有单测钉住;若维护者需要,可用上文命名的代跑 @qwen-code /verify 通道收口。

没给 5 分的原因:PR 正文中有一行声称内存徽章未动,而 diff 实际在设置开启时有意(且合理地)绕过了它;另外,对携带参数的始终生效的转义遍历值得在日后有人给历史渲染做性能剖析时被记起。两者都不构成阻塞。

批准合并。🚀

Qwen Code · qwen3.8-max

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

@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 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Local verification report — head 49bb0e8221, on Linux

I built this PR head and its merge-base (3a0c4c6108) from source (npm run build && npm run bundle) and drove the real dist/cli.js in a real pty (tmux, 120×45) against a scripted OpenAI-compatible provider, a real stdio MCP server, and a real qwen serve daemon. This complements the earlier tmux report on 5c6ef8ce95 — that head is four commits behind, so the surrogate-pair guard (2d461a3f) and the parallel-agent test fix (ea9bc0b5) were not covered there.

Result: no merge-blocking issue. Every behavioural claim in the PR description reproduced on Linux, including the ones the description marks as reasoned-through rather than driven end to end. Three non-blocking notes at the bottom.

The PR's own table marks 🐧 Linux as ⚠️ (CI only) — this report covers that arm in an interactive terminal.


What was checked

# Claim Method Result
1 Default off ⇒ rendering unchanged Same scenarios on the PR build and the merge-base build Captures byte-identical
2 On ⇒ read/search/list batch stops folding, one row per call Real TUI, 3 read_file calls
3 On ⇒ raw args printed under each header, including params the summary drops Real TUI, read + grep + edit old_string/new_string visible
4 1000-char inline cap with a +N chars marker 1400-char and 200 000-char content args +456, +199057; counts exact
5 Ctrl+O lifts the cap Real key injection ✅ full payload, no marker
6 Cap does not split a surrogate pair (2d461a3f) Argument aligned so the pair straddles index 999/1000, plus a hunk-revert counterfactual build ✅ guard present → clean cut; guard reverted → U+FFFD
7 MCP payload printed once, not twice Real stdio MCP server
8 Resumed session shows the same rows --resume into the session ✅ identical
9 Daemon boundary carries no args ⇒ row skipped, view unchanged Real reduceDaemonEventToTuiUpdates → real ToolGroupMessage ✅ off and on identical
10 Key is TUI-only, web shell neither exposes nor persists it Real qwen serve, GET/POST /workspace/settings ✅ 400 disallowed_key; absent from the exposed list
11 Memory badge bypassed so the params are visible 2 managed-memory writes
12 Discoverable, no restart needed /settings search + live toggle ✅ on-screen history re-renders
13 Both render modes ui.useTerminalBuffer true and false ✅ identical
14 Args row is sanitised Bidi override + ESC in an argument value ✅ U+202E stripped, ESC JSON-escaped
15 Unit / lint / typecheck 13 affected suites, eslint, tsc --build packages/cli 526 passed, both clean

1. The switch

Same build, same prompt, same three read_file calls — only the setting differs. The OFF pane is byte-identical to the same run on the merge-base build, so the default rendering really is untouched.

2. What the compact header hides

This is the case that motivates the setting: with it off, an edit shows only the filename and the strings it replaced never appear anywhere in the transcript.

3. The cap, and Ctrl+O lifting it

1456 − 1000 = 456, and the marker says +456. Pushed further: a 200 000-character content argument capped at … +199057 chars (ctrl+o), the TUI stayed responsive, and process RSS moved 174 064 KB → 174 112 KB across the turn.

4. The surrogate-pair guard, with a counterfactual

Attribution is by hunk-revert, not by comparing against an older head: I removed only the 0xd800..0xdbff guard from formatInlineToolArgs, re-bundled, and re-ran the same scenario. The bug reproduces (U+FFFD before the ellipsis, and the hidden count short by one) and disappears when the guard is restored. After Ctrl+O the emoji itself survives intact.

5. MCP de-duplication and the memory badge

6. The two boundaries

7. Discoverability, live toggle, resume


Notes (none blocking)

a. The hasRenderableToolCallArgs comment cites a case that isn't reachable — but the gate is still load-bearing for a different one.
The comment says gating on the raw setting alone "would expand every Read 3 files fold on an attached session". A daemon-attached session has no such fold to expand: TOOL_NAME_TO_CATEGORY is keyed on display names, and the daemon adapter sets name from the ACP kind, so isCollapsibleTool('read_file') === false while isCollapsibleTool('ReadFile') === true. Daemon groups already render one row per call, before and after this PR.

What the gate does buy is the empty-args case on the live path: a batch of calls invoked with {} stays collapsed instead of expanding into three rows that would each render no arguments.

isCollapsibleTool("ReadFile")  = true    <- live scheduler name
isCollapsibleTool("read_file") = false   <- daemon adapter name (ACP kind)

live group, args = {}, setting ON   ->  ✓ Read file1.txt, file2.txt, file3.txt   (gate holds the fold)

Worth a one-line comment correction; the code is right either way.

b. The MCP dedup compares serialisations, so it is key-order sensitive.
formatInlineToolArgs suppresses the row when description re-serialises to the same JSON. It correctly handles a pretty-printed description, but a description whose key order differs from args still gets a row:

MCP: description IS the args json                    -> undefined            (suppressed)
MCP: description pretty-printed                      -> undefined            (suppressed)
MCP: same json, different key ORDER in description   -> {"owner":"Q","repo":"r"}   (row rendered)

Not reachable through McpToolInvocation, whose getDescription() serialises the very same object, so this is informational rather than a defect.

c. The always-on cost is small but not zero.
The description says carrying args "costs no extra allocation" — that holds, it is the same reference. The traversal it adds to escapeAnsiCtrlCodes(item) is what has a price. Measured on the real function:

content arg size base shape (no args) PR shape (args carried)
1 KB 0.0008 ms 0.0015 ms
100 KB 0.0008 ms 0.0318 ms
1 MB 0.0003 ms 0.3294 ms
5 MB 0.0003 ms 1.6968 ms

Per escape pass, memoised on history-item identity. At realistic argument sizes this is noise; I mention it only so the number is on the record rather than the qualitative claim.


Method / how to reproduce
  • Builds: PR head 49bb0e8221 and merge-base 3a0c4c6108, each a full npm run build + npm run bundle, driven as node dist/cli.js.
  • Provider: a local OpenAI-compatible SSE server that routes on the last user message and emits a fixed tool_calls batch per scenario (reads, mixed, bigargs, hugeargs, surrogate, mcp, memops), then plain text once tool results come back.
  • Surrogate alignment: filler length solved so JSON.stringify(args).charCodeAt(999) === 0xD83D and charCodeAt(1000) === 0xDE00; the oracle is the raw bytes of tmux capture-pane at the cut, not the rendered glyph.
  • MCP: a 30-line stdio JSON-RPC server exposing one tool, wired through mcpServers in workspace settings.
  • Daemon boundary: the real reduceDaemonEventToTuiUpdates builds the group from a session_update event; that group is then rendered through the real ToolGroupMessage under both settings.
  • qwen serve: real daemon on 127.0.0.1:18566, --no-web --workspace <dir>; ui.compactMode used as the positive control for the same route.
  • Environment caveat: this box's node_modules was missing @opentui/*, @tanstack/react-table and some web-shell deps. Those are local install drift, not PR faults; packages/web-shell could not be built here and is untouched by this PR anyway.
中文版报告

本地验证报告 —— head 49bb0e8221,Linux 环境

我从源码分别构建了本 PR 的 head 与其 merge-base(3a0c4c6108)(npm run build && npm run bundle),并在真实 pty(tmux,120×45)中驱动真实的 dist/cli.js,配合一个脚本化的 OpenAI 兼容 provider、一个真实的 stdio MCP 服务,以及一个真实的 qwen serve 守护进程。

这份报告是对此前针对 5c6ef8ce95 的 tmux 报告的补充:那个 head 落后四个提交,因此代理对错误对(surrogate pair)的保护(2d461a3f)与并行子代理测试修复(ea9bc0b5)都不在其覆盖范围内。

结论:未发现阻塞合并的问题。 PR 描述中的每一条行为主张都在 Linux 上复现成功,包括描述里标注为"仅推演、未端到端跑通"的那些。文末有三条不阻塞的说明。

PR 自带的表格把 🐧 Linux 标为 ⚠️(依赖 CI)——本报告在交互式终端里补上了这一栏。

验证项

# 主张 方法 结果
1 默认关闭 ⇒ 渲染完全不变 同一批场景分别跑 PR 构建与 merge-base 构建 输出逐字节一致
2 开启 ⇒ 读取/搜索/列举批次不再折叠,每个调用独占一行 真实 TUI,3 次 read_file
3 开启 ⇒ 表头下方打印原始参数,包含摘要丢弃的参数 真实 TUI,read + grep + edit ✅ 能看到 old_string/new_string
4 1000 字符内联上限 + +N chars 标记 1400 与 200 000 字符的 content 参数 +456+199057,计数精确
5 Ctrl+O 解除上限 真实按键注入 ✅ 完整载荷,标记消失
6 上限不会切断代理对(2d461a3f 构造参数使代理对恰好横跨 999/1000,并做了 hunk 回退的反事实构建 ✅ 有保护→干净截断;回退→出现 U+FFFD
7 MCP 载荷只打印一次 真实 stdio MCP 服务
8 恢复会话呈现相同行 --resume 回到该会话 ✅ 完全一致
9 守护进程边界不带参数 ⇒ 跳过该行且视图不变 真实 reduceDaemonEventToTuiUpdates → 真实 ToolGroupMessage ✅ 开关两态完全一致
10 该配置项仅限终端界面,Web Shell 不暴露也不写入 真实 qwen serveGET/POST /workspace/settings ✅ 400 disallowed_key;不在暴露列表中
11 绕过内存徽章以便看到参数 2 次托管内存写入
12 可发现、无需重启 /settings 搜索 + 实时切换 ✅ 屏幕上已有的历史即刻重渲染
13 两种渲染模式 ui.useTerminalBuffer true / false ✅ 一致
14 参数行经过消毒 参数值中放入 bidi 覆盖符与 ESC ✅ U+202E 被剥离,ESC 被 JSON 转义
15 单测 / lint / 类型检查 13 个受影响套件、eslint、tsc --build packages/cli 526 通过,两者均干净

截图说明

  1. 开关本身:同一构建、同一提示、同样三次 read_file,只有设置不同。关闭态与 merge-base 构建逐字节一致,说明默认渲染确实未受影响。
  2. 精简表头隐藏了什么:这正是该设置存在的理由——关闭时编辑操作只显示文件名,它实际替换的字符串在整个对话里无处可见。
  3. 上限与 Ctrl+O:1456 − 1000 = 456,标记正好写 +456。进一步压测:200 000 字符的 content 参数被截断为 … +199057 chars (ctrl+o),TUI 保持响应,进程 RSS 在该轮前后为 174 064 KB → 174 112 KB。
  4. 代理对保护 + 反事实:归因方式是 hunk 回退,而非与旧 head 比较:我只移除了 formatInlineToolArgs0xd800..0xdbff 那段保护,重新打包并跑同一场景。缺陷复现(省略号前出现 U+FFFD,且隐藏字符数少一),恢复保护后消失。按 Ctrl+O 后该 emoji 完整无损。
  5. MCP 去重与内存徽章
  6. 两条不该越过的边界
  7. 可发现性、实时切换与会话恢复

说明(均不阻塞)

a. hasRenderableToolCallArgs 的注释举了一个实际到不了的场景——但这个把关本身在另一个场景下确实起作用。

注释说,若只用原始开关把关,"会把附着会话上的每一个 Read 3 files 折叠展开"。但守护进程附着的会话根本不存在这样的折叠:TOOL_NAME_TO_CATEGORY 是按展示名建索引的,而守护进程适配器用 ACP 的 kindname,因此 isCollapsibleTool('read_file') === false,而 isCollapsibleTool('ReadFile') === true。守护进程分组在本 PR 前后都是每个调用一行。

这个把关真正买到的是实时路径上的空参数场景:一批以 {} 调用的工具会保持折叠,而不是展开成三行、每行都渲染不出任何参数。

isCollapsibleTool("ReadFile")  = true    <- 实时调度器使用的名字
isCollapsibleTool("read_file") = false   <- 守护进程适配器使用的名字(ACP kind)

实时分组,args = {},设置开启  ->  ✓ Read file1.txt, file2.txt, file3.txt   (折叠被保住)

值得改一行注释;代码本身两种说法下都是对的。

b. MCP 去重比较的是序列化结果,因此对键顺序敏感。

formatInlineToolArgsdescription 重新序列化后与 args 相同时抑制该行。它能正确处理美化过的描述,但如果描述的键顺序args 不同,仍会渲染出参数行:

MCP:描述就是参数 json                     -> undefined                    (被抑制)
MCP:描述是美化打印的                       -> undefined                    (被抑制)
MCP:同样的 json,但键顺序不同              -> {"owner":"Q","repo":"r"}     (渲染出该行)

由于 McpToolInvocationgetDescription() 序列化的就是同一个对象,这条路径实际走不到,因此仅作说明而非缺陷。

c. 始终生效的那部分开销很小,但不是零。

描述里说携带 args"不产生额外分配"——这一点成立,它确实是同一个引用。有代价的是它给 escapeAnsiCtrlCodes(item) 增加的遍历。对真实函数实测:

content 参数大小 基线形状(无 args PR 形状(携带 args
1 KB 0.0008 ms 0.0015 ms
100 KB 0.0008 ms 0.0318 ms
1 MB 0.0003 ms 0.3294 ms
5 MB 0.0003 ms 1.6968 ms

按每次转义遍历计,并且按历史条目标识做了 memo。在现实参数规模下这属于噪声;我列出来只是想把具体数字而不是定性描述留在记录里。

方法 / 复现

  • 构建:PR head 49bb0e8221 与 merge-base 3a0c4c6108,各自完整 npm run build + npm run bundle,以 node dist/cli.js 驱动。
  • Provider:本地 OpenAI 兼容 SSE 服务,按最后一条用户消息路由,为每个场景(readsmixedbigargshugeargssurrogatemcpmemops)发出固定的 tool_calls 批次,待工具结果回来后再回纯文本。
  • 代理对对齐:解出填充长度,使 JSON.stringify(args).charCodeAt(999) === 0xD83DcharCodeAt(1000) === 0xDE00;判定依据是 tmux capture-pane 在截断处的原始字节,而不是渲染出的字形。
  • MCP:一个 30 行的 stdio JSON-RPC 服务,暴露单个工具,通过工作区设置的 mcpServers 接入。
  • 守护进程边界:由真实的 reduceDaemonEventToTuiUpdatessession_update 事件构建分组,再把该分组交给真实的 ToolGroupMessage 在开关两态下渲染。
  • qwen serve:真实守护进程监听 127.0.0.1:18566--no-web --workspace <dir>;用 ui.compactMode 作为同一路由的正向对照。
  • 环境说明:这台机器的 node_modules 缺少 @opentui/*@tanstack/react-table 及若干 web-shell 依赖。这是本地安装漂移而非 PR 的问题;packages/web-shell 在此无法构建,而本 PR 也未改动它。

wenshao
wenshao previously approved these changes Sep 1, 2026

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

Reviewed head 49bb0e82. One finding, inline; not blocking.

Scope. Source: all 13 non-test files, the docs row and the 9 locale files. The 4 large test patches (~635 lines) were read by title and then mutation-tested rather than read line-by-line. Not reviewed: behaviour in a real terminal (none available here), macOS/Windows.

CI on this head. Test (ubuntu-latest) is green — and that job also carries the generated-schema gate (ci.yml:540-551), so settings.schema.json staleness is covered by CI rather than by me. Test (macos-latest), Test (windows-latest), Integration Tests (CLI, No Sandbox) and Post Coverage Comment are SKIPPED; I ran the affected suites locally instead of inferring from them. No process.platform branch appears in this diff, so the platform skips cost little.

Ran locally (detached worktree at head, node 24.20):

  • 8 affected suites: 333 tests pass.
  • Efficacy matrix — 12 behaviours reverted one at a time, 12/12 caught: args-row JSX, useShowToolCallArgs -> false, hasRenderableToolCallArgs removed from forceExpandAll and from allMemOpsComplete, the surrogate-pair trim, the MCP dedup, the args carry-through in each of the four builders (live / resume / agent-adapter / speculation), the TUI_ONLY_SETTINGS entry, and the agent-view live-area fullDetail. That last one is the previously flagged "pinned by no test" — it bites now.
  • Height probe: the numbers quoted inline.

Cross-checked against the 7 earlier rounds. Of the 12 existing suggestions, 11 are fixed at this head. R6-1 is still open and I did not repeat it. Searching every prior comment and review body for availableTerminalHeight, height budget, countOneLineToolCalls, scrollback and live frame returns zero hits, so the inline finding is new to this thread — including to the round that cites #5798 itself, which used it only as a reason not to widen the parallel-agent gate.

Deferred to one-liners, no new threads (this PR is deep into review rounds):

  • docs/users/configuration/settings.md:140 — "because no arguments are available to render" is false for the parallel-agent case: those calls do carry args, and the exemption at ToolGroupMessage.tsx:305-310 exists for the #5798 reason. Only the daemon case is a genuine absence. Same root as R6-1.
  • ToolMessage.tsx:1155-1162 — the cap and the +N chars counter are counted in UTF-16 code units while the rest of the file truncates via toCodePoints, so the advertised hidden count is off by one per astral character. Cosmetic.

Checked and clean, so nobody re-litigates: TUI_ONLY_SETTINGS has a live consumer at workspace-settings.ts:141 and WEB_SHELL_SETTINGS is an add-list that does not contain the key; all 9 locale files updated; live-vs-resume MCP suppression holds because the resume path calls the same tool.build(args).getDescription() and the tool_result merge mutates in place so args survives; baseDisplayProperties is spread into every mapToDisplay arm; Ctrl+O reaches the agent view through a global handler with no view guard; IndividualToolCallDisplay is never serialized outside packages/cli/src/ui, so printing raw args adds no new durable write or wire exposure; requiresRestart: false is sound because the hook reads the context at render time.

I am leaving this at a comment rather than an approval for two reasons: there is one thing I would want looked at before this ships, and one dimension (a real terminal) I genuinely could not test. Neither reflects doubt about the rest — the four-builder symmetry, the sanitization and the test pinning are careful work.

Reviewed with AI assistance.

Comment thread packages/cli/src/ui/components/messages/ToolMessage.tsx
The `ui.showToolCallArgs` row renders outside every height budget
`ToolGroupMessage` keeps: `availableTerminalHeightPerToolMessage` reaches
only the result renderers, and `countOneLineToolCalls` still counts a
result-less tool as exactly one line. Its only bound was 1000 characters,
so at contentWidth 100 a single pending batch of six calls drew 72 rows
into a 20-row frame — the condition under which ink's
`shouldClearTerminalForFrame` wipes scrollback on every repaint (QwenLM#5798).

Cap the row at two wrapped rows (still never more than 1000 characters,
whichever is tighter), measured in columns so a full-width CJK argument
wraps at half the character count, and reserve the `+N chars` marker
inside that budget so the marker itself is not what spills over. Ctrl+O
still lifts both caps. Also reserve the rows in `staticHeight`, the way
`collapsibleSummaryHeight` already is, so the per-tool result budget does
not hand out height the args rows have spent.

The truncation now walks code points rather than UTF-16 code units, which
removes the lone-surrogate special case and makes `+N chars` agree with
the rest of the file's `toCodePoints` accounting — a code-unit count
over-reported by one per astral character.

Docs and the setting description now state the line cap. The
`hasRenderableToolCallArgs` comment cited a daemon-side `Read 3 files`
fold that cannot occur (`isCollapsibleTool` keys on display names, the
daemon adapter fills `name` from the ACP kind); it now names the case the
gate actually buys, an empty-args batch on the live path.

New `ToolGroupMessage.heightBudget.test.tsx` renders the real
ToolGroupMessage -> ToolMessage chain (the sibling suite mocks
ToolMessage away, so it cannot see how tall the row draws) and pins the
frame to the terminal height; dropping the width argument turns it red at
72 rows.
@TianYuan1024

Copy link
Copy Markdown
Contributor Author

@wenshao The Test (ubuntu-latest, Node 22.x) failure on this PR is not caused by this PR's diff, and it is not specific to this PR — it reproduces on other fork PRs. Could you take a look, or route it to @yiliang114 who owns #10648?

The failure

FAIL  packages/core/src/utils/shellAstParser.test.ts
      > classifyShellCommandSafety > classifies adversarial rule inputs within the CPU budget
AssertionError: expected 1328.265 to be less than 1000

That is the only red test in the job: Tests 1 failed | 22994 passed | 10 skipped. This PR touches 8 files — all under packages/cli/src/ui/components/messages/, docs/, and the generated settings schema. Nothing in packages/core.

It tracks the runner, not the PR

ci.yml:155-159 routes in-repo PRs and fork PRs from OWNER/MEMBER/COLLABORATOR authors to ECS, and leaves other fork PRs on GitHub-hosted runners. This PR is a fork PR from a CONTRIBUTOR, so its Test job runs hosted (Image: ubuntu-24.04, artifact test-results-fork-22.x-ubuntu-latest), while main runs the same job on ECS (ecs-qwen-hk4-2).

Sampling every Test (ubuntu-latest) job I could reach from the last few hours:

runner job measured CPU ms this test
hosted this PR, 99756185111 1232.8 fail
hosted this PR, 99813954665 1328.3 fail
hosted #10553, 99800920078 1360.8 fail
hosted #9541, 99773655173 1322.3 fail
hosted 99794706508 pass
ECS 99792071369, 99788530462, 99777016890, 99769261795, 99769024141, 99802727109 all pass

Four of the five hosted-runner jobs fail on this exact assertion, across three different PRs by three different authors. None of the six ECS jobs do. Locally, on an idle M-series Mac, both budget tests in that file finish in 247 ms.

Why it started now

#10648 (358efdceeb, merged 2026-08-31 16:25 UTC) changed both budget assertions from wall clock to CPU time:

-    const startedAt = performance.now();
-    expect(performance.now() - startedAt).toBeLessThan(1000);
+    const startedCpuUsage = process.cpuUsage();
+    const cpuUsage = process.cpuUsage(startedCpuUsage);
+    expect((cpuUsage.user + cpuUsage.system) / 1000).toBeLessThan(
+      maxClassificationCpuMs,
+    );

The body under measurement is Promise.all(commands.map(classifyShellCommandSafety)) — six classifications in flight at once. Under wall clock the concurrent work overlaps; under process.cpuUsage() it sums user + system across the process, so the same work reports a larger number. The 1000 ms budget still holds on ECS and does not hold on the hosted runners.

The previously green head on this branch, 49bb0e8221, predates 358efdceeb and does not contain it — which is why the ubuntu job was green during review round 8 and flipped red afterwards. It arrived here when I merged origin/main.

Not fixing it in this PR

The budget belongs to #10648 and to CI, not to a TUI PR, and whether the right answer is a larger budget, a runner-aware budget, or a return to wall clock is not my call. Happy to open a separate PR if that helps.

中文说明

@wenshao 本 PR 上 Test (ubuntu-latest, Node 22.x) 的失败与本 PR 的改动无关,也不是本 PR 独有的——它在其它 fork PR 上同样复现。麻烦你看一下,或者转给 #10648 的作者 @yiliang114

失败内容

FAIL  packages/core/src/utils/shellAstParser.test.ts
      > classifyShellCommandSafety > classifies adversarial rule inputs within the CPU budget
AssertionError: expected 1328.265 to be less than 1000

这是整个 job 里唯一一条红的测试:Tests 1 failed | 22994 passed | 10 skipped。本 PR 改动 8 个文件,全部位于 packages/cli/src/ui/components/messages/docs/ 以及生成的 settings schema,packages/core 一行未动。

决定因素是 runner,不是 PR

ci.yml:155-159 的路由规则是:仓库内 PR、以及作者 association 为 OWNER/MEMBER/COLLABORATOR 的 fork PR 跑在 ECS 上,其余 fork PR 留在 GitHub 托管 runner 上。本 PR 是 CONTRIBUTOR 身份的 fork PR,因此 Test job 跑在托管 runner(Image: ubuntu-24.04,产物名 test-results-fork-22.x-ubuntu-latest),而 main 的同一个 job 跑在 ECS(ecs-qwen-hk4-2)。

抽样了最近几小时内所有能取到的 Test (ubuntu-latest) job:

runner job 实测 CPU ms 该测试
托管 本 PR,99756185111 1232.8 失败
托管 本 PR,99813954665 1328.3 失败
托管 #10553,99800920078 1360.8 失败
托管 #9541,99773655173 1322.3 失败
托管 99794706508 通过
ECS 99792071369、99788530462、99777016890、99769261795、99769024141、99802727109 全部通过

5 个托管 runner 的 job 里有 4 个挂在同一条断言上,分属 3 个不同作者的 3 个 PR;6 个 ECS job 全部通过。本地(空闲的 M 系列 Mac)该文件的两条预算测试共耗时 247ms。

为什么现在才开始失败

#10648358efdceeb,2026-08-31 16:25 UTC 合入)把两条预算断言从墙钟时间改成了 CPU 时间:

-    const startedAt = performance.now();
-    expect(performance.now() - startedAt).toBeLessThan(1000);
+    const startedCpuUsage = process.cpuUsage();
+    const cpuUsage = process.cpuUsage(startedCpuUsage);
+    expect((cpuUsage.user + cpuUsage.system) / 1000).toBeLessThan(
+      maxClassificationCpuMs,
+    );

被度量的是 Promise.all(commands.map(classifyShellCommandSafety))——6 条命令并发分类。墙钟计时下并发是重叠的;换成 process.cpuUsage() 后是把整个进程的 user + system 累加,同样的工作量报出的数字更大。1000ms 的预算在 ECS 上仍然成立,在托管 runner 上不成立。

本分支此前绿的那个 head 49bb0e8221 早于 358efdceeb、并不包含它——这正是第 8 轮 review 时 ubuntu job 还是绿的、之后才变红的原因:它是随我合并 origin/main 一起进来的。

不在本 PR 中修复

这条预算属于 #10648 和 CI,不属于一个 TUI PR;到底该放宽阈值、按 runner 区分、还是改回墙钟计时,也不该由我决定。如果需要,我可以另开一个 PR。

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

Partially reviewed — one finding, inline; gaps disclosed.

Scope: all 32 changed files reviewed in the diff. Full test files read by diff. Cross-file context: ToolGroupMessage.tsx full (for height budget), useReactToolScheduler.ts live path, resumeHistoryUtils.ts resume path, agentHistoryAdapter.ts agent path, AppContainer.tsx speculative path, settingsSchema.ts, types.ts.

CI: web-shell E2E Smoke ✅. Integration Tests (CLI, No Sandbox) — SKIPPED (not a build or unit-test pass). Test (macos-latest, Node 22.x) — SKIPPED. Unit-test suite not run locally; no toolchain in this environment.

Not reviewed: platform runtime behavior (can only reason from code).


Cross-check against prior reviews:

Prior reviews (chiga0 @ head 49bb0e82): filed the height budget issue at ToolMessage.tsx:981. The current head fixes it for non-collapsible tools with inlineArgsHeight, but the gap for collapsible tools (below) was not raised. Confirmed as a new residual issue.

Bot reviews R1–R5: suggestions about agent path, bidi sanitization, description accuracy, speculative path test — all appear addressed in the current diff. R5 parallel-agent test vacuity concern was not re-raised here since that test's positive assertion (expect(frame).toContain('Parallel agents')) guards against the vacuous-pass risk.

Reviewed with AI assistance.

Comment thread packages/cli/src/ui/components/messages/ToolGroupMessage.tsx

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

Partially reviewed — gaps disclosed.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • docs exclusion sentence misdescribing both compact-view cases (docs/users/configuration/settings.md:141) — already reported (round-3 deferred list, review 5063380208; re-dropped as R6-1 in review 5068708701)
  • agentHistoryAdapter.test.ts:354 tool_result merge branch unpinned for args — already reported (round-3 deferred list, review 5063380208)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): src/a.tsno such file or directory.

Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/cli/src/ui/components/agent-view/AgentChatContent.tsx:265 — [probe] Critical [fails-closed] [new-surface] D7-1: Ctrl+O fullDetail unlocks uncapped rendering in the agent-view live area — no height backstop, #5798-class scrollback w…
  • packages/cli/src/ui/components/messages/ToolGroupMessage.heightBudget.test.tsx:65 — [probe] D7-2: height-budget test never exercises the inlineArgsHeight reservation — dropping the reservation leaves both assertions green (mutation probe)
  • docs/users/configuration/settings.md:141 — [probe] D7-3: settings.md asserts a batch-level height invariant the code does not provide (probe: 7-call batch draws 21 rows into a 20-row viewport)
  • packages/cli/src/ui/components/messages/ToolGroupMessage.tsx:485 — [probe] D7-4: phantom 2-row height reservation for tools whose args row never renders (MCP/error-path dedup) — probe: 12 result rows lost for 6 MCP calls at H=73
  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1187 — [probe] D7-5: wide-char (emoji/CJK) args wrap to 3 rows against the 2-row reservation — real ink render, 103 of 207 widths violate
  • packages/cli/src/ui/components/messages/ToolMessage.test.tsx:2080 — [probe] D7-6: surrogate-pair cap test fixture sits at the cut boundary, not astride it — naive-slice mutant survives the shipped fixture (mutation probe)
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

Test Plan(非阻断):src/a.tsno such file or directory

收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 6 条(原文未翻译,列表见上方英文部分)。

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

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

Partially reviewed — gaps disclosed.

4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R8-1 docs batch-level height guarantee (settings.md:141) — already reported (round-7 deferred list as D7-3, review 5078284431)
  • R8-2 inlineArgsHeight reservation unpinned by tests (ToolGroupMessage.tsx:485) — already reported (round-7 deferred list as D7-2, review 5078284431)
  • R8-3 phantom 2-row reservation for deduped MCP args rows (ToolGroupMessage.tsx:485) — already reported (round-7 deferred list as D7-4, review 5078284431)
  • R8-6 'running' qualifier on the parallel-subagent carve-out (settings.md:141) — already reported (round-6 deferred list as D6-3, review 5068708701)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — packages/cli full unit suite timed out at the harness deadline under machine load (no full-suite verdict; affected suites ran green in targeted runs), and the test-efficacy probe was inconclusive.

Test Plan (not a blocker): src/a.tsno such file or directory.

Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1187 — [probe] Critical [fails-closed] [new-surface] word-aware wrapping defeats the 2-line args cap; a pending batch can overflow the terminal (#5798)
  • packages/vscode-ide-companion/schemas/settings.schema.json:473 — [probe] 'Does not change result-output truncation' is false under height pressure (probe: 29 -> 21 result rows)
  • docs/users/configuration/settings.md:141 — [probe] daemon clause misdescribes the compact view; Ctrl+O can never show args there
  • docs/users/configuration/settings.md:141 — [probe] 'Useful when debugging MCP integrations' is a no-op for MCP calls
  • packages/cli/src/ui/components/messages/ToolGroupMessage.tsx:407 — [probe] budget divisor counts results that shouldCollapseResult never draws (probe: 2/15 vs 15/15 rows)
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — packages/cli full unit suite timed out at the harness deadline under machine load (no full-suite verdict; affected suites ran green in targeted runs), and the test-efficacy probe was inconclusive。

Test Plan(非阻断):src/a.tsno such file or directory

收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 5 条(原文未翻译,列表见上方英文部分)。

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

7 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R9-1 height-budget test fixture cannot exercise the inlineArgsHeight reservation — already reported (round-7 deferred list as D7-2, review 5078284431; re-noted as R8-2 in review 5081609082)
  • R9-2 phantom 2-row reservation for deduped MCP args rows — already reported (round-7 deferred list as D7-4, review 5078284431)
  • R9-3 'Does not change result-output truncation' description claim — already reported (round-8 deferred list, review 5081609082)
  • R9-4 surrogate-pair cap fixture mutation-blindness — already reported (round-7 deferred list as D7-6, review 5078284431)
  • R9-5 docs batch-level height guarantee — already reported (round-7 deferred list as D7-3, review 5078284431; re-noted as R8-1 in review 5081609082)
  • R9-6 agentHistoryAdapter tool_result merge branch unpinned for args — already reported (round-3 deferred list, review 5063380208; re-noted in review 5078284431)
  • R9-7 'running' qualifier understates the status-agnostic parallel-agent compact-view carve-out — already reported (round-6 deferred list as D6-3, review 5068708701; re-noted as R8-6 in review 5081609082)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): src/a.tsno such file or directory.

Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round; 4 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1187 — [probe] Critical [fails-closed] [new-surface] CJK/full-width args wrap to 3 rows against the 2-line reservation at odd widths (re-confirms D7-5)
  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1220 — [probe] Critical [fails-closed] [new-surface] narrow-width marker floor overdraws the 2-row cap; innerWidth<=0 draws 1013 rows (new this round)
  • packages/cli/src/ui/components/agent-view/AgentChatContent.tsx:265 — [probe] Critical [fails-closed] [new-surface] fullDetail lifts every height cap on the agent route, which has no backstop (re-confirms D7-1)
  • packages/cli/src/ui/components/messages/ToolMessage.tsx:1003 — [probe] Critical [fails-closed] [new-surface] word-aware wrap defeats the 2-line args cap for spaced payloads (re-confirms round-8 deferred Critical)
  • packages/cli/src/ui/components/messages/ToolMessage.test.tsx:2140 — [probe] uncapped full-detail sanitize guard has no test witness (new this round)
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 7 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

Test Plan(非阻断):src/a.tsno such file or directory

收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改;其中 4 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 5 条(原文未翻译,列表见上方英文部分)。

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

@wenshao

wenshao commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Deep verification (maintainer-local round) — verdict: findings

31/33 scripted assertions passed, 2 failed. Verified head 3bba019899f4fd1da993a0ca805b3b4af56a9db1, base 4f212873b59f20fa0154b36feb3db382f90b766a. The central claim is proven load-bearing by a four-cell A/B driven in a real terminal; the two failures are non-blocking Suggestions, neither of which touches the default (showToolCallArgs: false) path.

中文摘要

结论:findings(有发现,但不影响默认路径) — 33 条脚本化断言中 31 条通过、2 条失败。head 3bba0198,base 4f212873

  • 核心结论(真实终端 A/B):在真实 tmux pty 中用脚本化假模型驱动真实 qwen 二进制,同一批工具调用(read + grep + edit)下:head+offbase+offbase+on 三格渲染完全一致(紧凑折叠行),只有 head+on 变成"每个调用一行 + 行内参数 JSON"。设置确实由本 PR 引入且生效,base 上完全惰性。见下图 1、2。
  • 次要结论均成立:MCP 参数只打印一次(head+onhead+off 帧完全一致,图 4);4000 字符参数被限制为 2 行并带 +N chars (ctrl+o) 标记,30/60/100 列实测恰好 2 行(图 5、6);Ctrl+O 解除上限后显示完整 JSON 含右花括号(图 3);bidi 覆盖字符在新参数行上被正确剥离,而结果预览行残留 bidi 在 base 两臂上完全相同,属既有行为、非本 PR 引入。
  • 发现 1(Suggestion)formatInlineToolArgsrowWidth ≤ 11(约终端 ≤ 15 列)时违反自身"最多 2 行"契约——固定输出 23 列而预算只允许 2..22 列(探针 P8 实测 11 处违约)。真实终端 30/60/100 列均正常;≤20 列未能驱动,实际后果未实测。
  • 发现 2(Suggestion):截断标记的列数在"是否需要截断"之前就被预留,导致本来放得下的负载也被截断:100 列下预算 188 列,但 168..188 列的负载仍被截断并显示 +16..+1 chars。PR 旗舰场景实测渲染出 …"new_string":"export const a = 2;"… +1 chars (ctrl+o)——右花括号被 19 列的标记挡住,JSON 看起来是坏的(图 1 下半可见)。
  • 门禁:受影响套件 head 529/529(10 文件)、base 490/490(8 文件),tsc --noEmit 干净。变异矩阵:禁用 MCP 去重 → 2 个 PR 自带测试变红;2 行上限改 200 → 4 个 PR 自带测试变红(含两个 heightBudget 测试)。关键护栏均被测试钉住。
  • 未覆盖:daemon 路径、resume 路径、web-shell 拒绝该键、≤15 列的真实后果、转义遍历新增 args 的开销、Windows/Linux。

Central claim — proven load-bearing

Real dist/cli.js of each tree, driven in a real tmux pty (100×32) against a scripted fake OpenAI server (SSE shape ported byte-for-byte from integration-tests/fake-openai-server.ts, sha256-identical on both arms). Same scripted batch on every cell: read_file + grep_search + edit in one turn; settings isolated via scratch HOME/QWEN_HOME.

cell tree setting rendered tool region
A head off folded summary (✓ Searched 'foo' in ~/ws/src, read …)
B head on one row per call, each with its args JSON; edit exposes old_string/new_string
C base off identical to A
D base on identical to A/C — the setting is inert without this PR

A ≡ C (default unchanged) · C ≡ D (load-bearing control) · B ≠ A (the change is real).

A/B: showToolCallArgs OFF (fold) vs ON (per-call rows + args)

Load-bearing control: base+on == base+off

Secondary claims — all held

claim measurement result
MCP payload printed once, not twice real stdio MCP server; head+on frame ≡ head+off frame PASS
args row bounded to 2 wrapped rows 4000-char write_file arg → exactly 2 rows + … +3955 chars (ctrl+o); on/off row delta = exactly 2 at 30, 60 and 100 cols PASS
Ctrl+O lifts the cap truncated row becomes the complete JSON incl. closing brace; head-off+Ctrl+Obase-on+Ctrl+O (result expansion unchanged) PASS
bidi overrides stripped on the new row probe + live cell render {"content":"aaaXXXbbbccc"}; the result preview row still carries raw bidi on both base arms → pre-existing, not introduced here PASS

MCP de-dup: payload printed exactly once

Ctrl+O lifts the args cap

4000-char arg bounded to 2 rows

Cap holds at 30 cols

Corrections to the PR description (description only, not code)

  1. "Arguments are capped at 1000 characters inline" is stale. At a known row width the binding cap is min(1000, floor(rowWidth) * 2) columns — 188 columns at a 100-col terminal. The settingsSchema.ts description ("capped at 2 wrapped lines (and never more than 1000 characters)") is the accurate one; the body describes an earlier commit.
  2. The illustrative "After" block invents two parameter names. It shows absolute_path (ReadFile) and include (Grep); the real schemas are read_file: {file_path, offset, limit, pages} and grep_search: {pattern, glob, path, limit} (read-file.ts:547, grep.ts:687). Since the feature exists to show the actual arguments, a reviewer diffing the body against a live session will see names that never occur.

Findings (both Suggestion, neither blocking)

F1 — the row-cap is violated for rowWidth ≤ 11. formatInlineToolArgs reserves the marker's columns and floors the payload budget at 1 (Math.max(1, budget - markerWidth)), so at small budgets it emits a ~23-column string into a rowWidth * 2 budget. Probe over the real exported function, rowWidth 1..120: violations=11 (w=1 rendered=23 allowed=2w=11 rendered=23 allowed=22) — precisely the overflow TOOL_ARGS_INLINE_MAX_LINES and the ToolGroupMessage height reservation exist to prevent. Reachability: innerWidth ≈ cols - 4, so the window is terminal ≲ 15 columns; the cap was measured holding at 30/60/100 cols, and the live consequence below ~20 cols is unmeasured (the harness could not drive the TUI that narrow meaningfully).

F2 — payloads that fit the row budget are truncated anyway, advertising "+1 chars". markerWidth is reserved before the code knows truncation is needed, so effective capacity is budget - markerWidth (167 cols at 100-col width) instead of budget (188). Measured band: 183–192-column payloads cut with hidden = 16..25. In the PR's own flagship scenario the live Edit row renders …"new_string":"export const a = 2;"… +1 chars (ctrl+o) — the hidden character is the closing }, so the row shows invalid-looking JSON plus a 19-column marker saying "1 char hidden" (visible in image 1).

Measured candidate fix for F1+F2 (applied in a scratch copy, then reverted)

Measure the whole payload against budget first and return it whole when it fits; reserve markerWidth only once truncation is known to be needed; return undefined when budget <= markerWidth + 1 (a row too narrow to hold its own marker). With the patch: F1 violations 11 → 0; F2 degenerate band 10 → 4 (the 4 residual are genuinely over budget). But one of the PR's own tests goes rednever cuts a surrogate pair in half at the cap boundary expects json.slice(0, 979) + "… +3 chars" for a 1000-code-unit payload whose column width is 999, i.e. it pins the reserve-first semantics. So the fix is not drop-in: that expectation is a decision the author owns. The other 539 assertions stayed green.

Gates and mutation matrix

  • Affected suites: head 529/529 (10 files) · base 490/490 (8 files) · tsc --noEmit -p packages/cli clean.
  • Mutation "disable MCP de-dup" → 2 of the PR's tests red (skips the row when the description already IS the args JSON (MCP), prints an MCP payload once, not twice) — guard pinned.
  • Mutation "TOOL_ARGS_INLINE_MAX_LINES 2 → 200" → 4 of the PR's tests red, including both ToolGroupMessage.heightBudget tests and bounds the row to two wrapped rows when the row width is known — cap pinned.
  • Post-restore re-run of the gate: 529/529 (tree left pristine).

Not covered

Daemon path (args row skipped) — reasoned from daemon-tui-adapter.ts:433 (name = ACP kind, so those groups never fold) but not executed; resume path (unit tests green, no live resume); web-shell rejection of the key (unit tests green, live route not hit); live consequence of F1 at ≤ ~15 cols; the hasRenderableToolCallArgs fold-teardown gate was not mutated (behavior proven by the live A/B instead); perf cost of the always-on escapeAnsiCtrlCodes(item) walk now traversing args (HistoryItemDisplay.tsx:267) — verified present, not measured; a read-only observation that inlineArgsHeight reserves 2 phantom rows for MCP tools whose row is suppressed by de-duplication; Windows/Linux; real-model sessions.

Methodology

Detached worktrees at the resolved baseRefOid/headRefOid, each with its own npm ci + build (both exit 0); root and packages/cli manifests byte-identical between arms, so the A/B is code-only. Frames captured with tmux capture-pane -p (assertions) and -e (screenshots above). Unit probes import the real exported formatInlineToolArgs with no mocks, run under the tree's own vitest. Raw logs, harness (ab-tui.mjs) and per-cell frames are in the local round's artifact dir; the probe source was deleted after the run.

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.

feat(ui): add a setting to show full tool-call name + arguments inline (restore pre-compact verbosity)

5 participants