Skip to content

fix(cli): show file path in compact tool summary for single collapsible tools - #6448

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
Alex-ai-future:fix/read-name
Jul 8, 2026
Merged

fix(cli): show file path in compact tool summary for single collapsible tools#6448
wenshao merged 4 commits into
QwenLM:mainfrom
Alex-ai-future:fix/read-name

Conversation

@Alex-ai-future

Copy link
Copy Markdown
Contributor

What this PR does

Changes the compact tool group summary to display the actual file path or search pattern when a single collapsible tool (read/search/list) is executed, instead of a generic count like "Read 1 file". When multiple tools of the same type are batched together, the count format is preserved. If the tool description is unavailable, it falls back to the count format.

Why it's needed

Collapsible tools (ReadFile, Grep, Glob, ListFiles) include a description field containing the file path or search parameters, but buildToolSummary() only used the tool name for categorization and discarded this information. Users executing a read operation would see "Read 1 file" with no indication of which file was read, making it harder to follow agent actions in the TUI.

Reviewer Test Plan

How to verify

  1. Start the CLI and ask it to read a single file (e.g., "read src/index.ts")
  2. Observe the tool summary line shows "Read src/index.ts" instead of "Read 1 file"
  3. Ask it to read multiple files at once — summary should show "Read N files" (count format preserved)
  4. Run the unit test: cd packages/cli && npx vitest run src/ui/components/messages/CompactToolGroupDisplay.test.tsx — all 21 tests pass

Evidence (Before & After)

Scenario Before After
Single read_file Read 1 file Read src/a.ts
Single grep Searched 1 pattern Searched 'foo' in path './'
Multiple reads Read 3 files Read 3 files (unchanged)
Mixed (1 read + 1 shell) Read 1 file, ran 1 command Read src/a.ts, ran ls
Description empty (fallback) Read 1 file Read 1 file (unchanged)

Tested on

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

Environment

cd packages/cli && npx vitest run src/ui/components/messages/CompactToolGroupDisplay.test.tsx — 21 tests passed

Risk & Scope

  • Main risk or tradeoff: Display string length may vary more widely with file paths, but ink's wrap="truncate-end" handles overflow
  • Not validated / out of scope: Non-English locale rendering of file paths
  • Breaking changes / migration notes: None — purely a display format change

Linked Issues

N/A

中文说明

此 PR 做了什么

修改了紧凑工具组摘要的显示逻辑:当执行单个可折叠工具(read/search/list)时,显示实际的文件路径或搜索模式,而不是通用的 "Read 1 file" 计数。当多个同类型工具批量执行时,保留计数格式。如果工具描述不可用,则回退到计数格式。

为什么需要

可折叠工具(ReadFile、Grep、Glob、ListFiles)包含一个 description 字段,记录了文件路径或搜索参数,但 buildToolSummary() 只使用工具名称进行分类计数,丢弃了这些信息。用户执行读取操作时只能看到 "Read 1 file",无法知道读了哪个文件,难以在 TUI 中跟踪 agent 的操作。

评审者测试计划

如何验证

  1. 启动 CLI 并让它读取单个文件(如 "read src/index.ts")
  2. 观察工具摘要行显示 "Read src/index.ts" 而非 "Read 1 file"
  3. 让它一次读取多个文件 — 摘要应显示 "Read N files"(保留计数格式)
  4. 运行单元测试:cd packages/cli && npx vitest run src/ui/components/messages/CompactToolGroupDisplay.test.tsx — 21 个测试全部通过

证据(前后对比)

场景 之前 之后
单个 read_file Read 1 file Read src/a.ts
单个 grep Searched 1 pattern Searched 'foo' in path './'
多个 reads Read 3 files Read 3 files(不变)
混合(1 read + 1 shell) Read 1 file, ran 1 command Read src/a.ts, ran ls
description 为空(回退) Read 1 file Read 1 file(不变)

测试平台

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

环境

cd packages/cli && npx vitest run src/ui/components/messages/CompactToolGroupDisplay.test.tsx — 21 个测试通过

风险与范围

  • 主要风险或权衡:显示字符串长度会因文件路径而变化较大,但 inkwrap="truncate-end" 会处理溢出
  • 未验证/超出范围:非英文 locale 下文件路径的渲染
  • 破坏性变更/迁移说明:无 — 纯显示格式变更

关联 Issue

…le tools

buildToolSummary() previously discarded the description field from
collapsible tools (ReadFile, Grep, Glob, ListFiles), showing only
generic counts like 'Read 1 file'. Now shows the actual file path or
search pattern for single tools, while preserving count format for
batches of multiple tools of the same type. Falls back to count format
when description is unavailable.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: Real UX issue — when the CLI reads a single file, the compact summary shows "Read 1 file" with no indication of which file. The description field already carries the file path or search pattern, but buildToolSummary() was discarding it. The before/after table in the PR body demonstrates the improvement clearly, and the prior verification report by @wenshao confirms the rendered output matches.

Direction: Aligned. Showing "Read src/index.ts" instead of "Read 1 file" is strictly more useful with no downside. This is a straightforward TUI enhancement.

Size: 3 files changed, +125/-26. Production logic: 47 lines in CompactToolGroupDisplay.tsx (of which ~15 are the safeDescription helper and ~15 are updated JSDoc). Tests: 78 lines across the two test files. No core paths touched — UI component code only.

Approach: Clean and minimal. The counts Map → toolsByCategory Map change is the right tradeoff — preserves tool references without over-engineering. The safeDescription() helper is well-designed: strips ANSI sequences, rejects JSON error blobs, falls back gracefully. New tests cover all edge cases (empty, undefined, JSON blob, array blob, ANSI CSI, ANSI non-CSI, embedded newlines). The diff is focused — no unrelated changes.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:真实的 UX 问题——当 CLI 读取单个文件时,紧凑摘要显示 "Read 1 file",无法知道读的是哪个文件。description 字段已经包含了文件路径或搜索模式,但 buildToolSummary() 丢弃了它。PR 正文中的 before/after 表格清楚展示了改进效果,@wenshao 的验证报告也确认了实际渲染输出与预期一致。

方向:对齐。显示 "Read src/index.ts" 比 "Read 1 file" 更有用,没有副作用。这是一个简单的 TUI 增强。

规模:3 个文件,+125/-26。生产逻辑:CompactToolGroupDisplay.tsx 中 47 行(其中 ~15 行是 safeDescription 辅助函数,~15 行是更新的 JSDoc)。测试:两个测试文件共 78 行。未触及核心路径——仅 UI 组件代码。

方案:干净且最小化。counts Map → toolsByCategory Map 是正确的权衡——保留工具引用而不过度工程化。safeDescription() 辅助函数设计良好:去除 ANSI 序列、拒绝 JSON 错误 blob、优雅回退。新增测试覆盖所有边界情况。diff 聚焦——无无关改动。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I'd solve this by making buildToolSummary() access the description field of single-tool categories. The minimal path is changing from a Map<cat, count> to Map<cat, tools[]>, then using tools[0].description when tools.length === 1. Need to handle empty/undefined descriptions and the JSON error fallback (useReactToolScheduler sets description = JSON.stringify(request.args) on error — confirmed at line 300). Should also strip ANSI escape sequences from descriptions to avoid rendering garbage in the TUI.

Comparison with PR: The PR's approach matches my proposal exactly — counts Map → toolsByCategory Map, safeDescription() helper that strips ANSI, rejects JSON blobs, and falls back to count format. The implementation is clean and correct. Two notes:

  1. The safeDescription function correctly handles the undefined case defensively even though the type says string — this is appropriate since runtime values can diverge from types.
  2. The ANSI stripping regex covers CSI, OSC, charset, and single-byte ESC sequences — thorough. The C0 control character replacement ([\x00-\x1f\x7f]) handles newlines, tabs, and other embedded control chars.
  3. The JSON blob rejection (startsWith('{') / startsWith('[')) correctly catches the error fallback path from useReactToolScheduler.ts:300.

No correctness bugs, no security issues, no AGENTS.md violations. The description field on IndividualToolCallDisplay is string (not optional), so this doesn't introduce a new data dependency.

Reuse check: safeDescription is a focused helper for this specific sanitization need. No existing utility in the codebase covers this exact combination (ANSI strip + control char cleanup + JSON rejection). Appropriate to add inline.

Testing

Unit tests: All 71 tests pass (ran locally in worktree).

 ✓ src/ui/components/messages/CompactToolGroupDisplay.test.tsx (26 tests) 53ms
 ✓ src/ui/components/messages/ToolGroupMessage.test.tsx (45 tests) 127ms

 Test Files  2 passed (2)
      Tests  71 passed (71)
   Duration  8.67s

New tests added (8):

  • falls back to count format when description is emptydescription: '' → "Read 1 file" ✓
  • falls back to count format when description is undefineddescription: undefined → "Read 1 file" ✓
  • falls back to count format when description is JSON (error args)description: '{"file_path":...}' → "Read 1 file" ✓
  • falls back to count format when description starts with array bracketdescription: '["ls", "-la"]' → "Ran 1 command" ✓
  • strips ANSI CSI escape sequences from description\x1b[32ma.ts\x1b[0m → "Read a.ts" ✓
  • strips non-CSI ANSI sequences (charset, OSC) from description\x1b(Bls -la\x1b[0m → "Ran ls -la" ✓
  • replaces embedded newlines with spaces in descriptionecho hello\nworld → "Ran echo hello world" ✓
  • Updated existing tests to assert description-based format for single tools

Real-scenario tmux test: Not feasible in this CI environment — the compact tool summary is rendered by the Ink React TUI in interactive mode only, and requires a live model interaction to trigger tool calls. qwen -p (non-interactive/headless mode) does not render the compact summary. However, @wenshao's prior verification report provides comprehensive real TUI A/B testing (BASE vs PR, tmux capture-pane, Linux), confirming the feature works exactly as described in a real interactive session, including edge cases (long paths with truncation, multi-category summaries, errored tool calls).

中文说明

代码审查

独立方案: 我会让 buildToolSummary() 在单工具类别时访问 description 字段。最小路径是将 Map<cat, count> 改为 Map<cat, tools[]>,当 tools.length === 1 时使用 tools[0].description。需要处理空/undefined 描述和 JSON 错误回退(useReactToolScheduler 在出错时设置 description = JSON.stringify(request.args)——已在第 300 行确认)。还应去除描述中的 ANSI 转义序列,避免在 TUI 中渲染乱码。

与 PR 对比: PR 的方案与我的方案完全一致——counts Map → toolsByCategory Map,safeDescription() 辅助函数去除 ANSI、拒绝 JSON blob、回退到计数格式。实现干净且正确。

无正确性 bug、无安全问题、无 AGENTS.md 违规。IndividualToolCallDisplaydescription 字段是 string(非可选),不引入新的数据依赖。

复用检查:safeDescription 是针对此特定清理需求的聚焦辅助函数。代码库中没有现成的工具覆盖这种精确组合(ANSI 去除 + 控制字符清理 + JSON 拒绝)。适合内联添加。

测试

单元测试: 71 个测试全部通过(在 worktree 本地运行)。

 ✓ src/ui/components/messages/CompactToolGroupDisplay.test.tsx (26 tests) 53ms
 ✓ src/ui/components/messages/ToolGroupMessage.test.tsx (45 tests) 127ms

 Test Files  2 passed (2)
      Tests  71 passed (71)
   Duration  8.67s

tmux 真实场景测试: 在此 CI 环境中不可行——紧凑工具摘要仅在交互模式下由 Ink React TUI 渲染,需要实际的模型交互来触发工具调用。qwen -p(无头模式)不会渲染紧凑摘要。但 @wenshao 之前的验证报告提供了全面的真实 TUI A/B 测试(BASE vs PR,tmux capture-pane,Linux),确认功能在实际交互会话中完全按描述工作,包括边界情况。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

This is a clean, well-scoped display improvement. The change does exactly what it says — uses the existing description field instead of discarding it, falls back gracefully when it's not useful. The safeDescription() helper is well-engineered: it handles every edge case I can think of (ANSI sequences, JSON error blobs, control characters, empty/undefined values) without over-complicating things.

The test suite is strong — 71 tests across both files, with 8 new tests that thoroughly pin the new behavior and the fallback paths. @wenshao's real TUI verification with A/B comparison and mutation testing (reverting source while keeping tests → 12 failures) confirms the tests are non-vacuous and the feature works in practice.

My independent proposal matched the PR's approach exactly, which is a good sign — this is the natural, minimal way to solve the problem. No simpler path exists.

No concerns. Shipping it.

中文说明

这是一个干净、范围明确的显示改进。改动完全符合描述——利用已有的 description 字段而非丢弃它,在无用值时优雅回退。safeDescription() 辅助函数工程化良好:处理了我能想到的所有边界情况(ANSI 序列、JSON 错误 blob、控制字符、空/undefined 值),且没有过度复杂化。

测试套件扎实——两个文件共 71 个测试,8 个新测试全面锁定新行为和回退路径。@wenshao 的真实 TUI A/B 验证和变异测试(回退源码保留测试 → 12 个失败)确认测试非空转、功能在实践中有效。

我的独立方案与 PR 的方案完全一致——这是解决问题的自然、最小方式。不存在更简单的路径。

无顾虑,可以合入。

Qwen Code · qwen3.7-max

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

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

[Suggestion] Stale JSDoc — The JSDoc comment above buildToolSummary (line ~195) still documents the old count-based behavior (Single tool → "Read 1 file" / "Ran 1 command"), but the function now produces "Read a.ts" / "Ran ls -la" when a description is available. Suggest updating to:

 * Single tool (with description) → "Read a.ts" / "Ran ls -la"
 * Single tool (no description)   → "Read 1 file" / "Ran 1 command"
 * Multi  same                    → "Read 3 files"
 * Multi mixed                    → "Read a.ts, ran npm test, edited b.ts"


if (count === 1) {
if (tools.length === 1 && tools[0].description) {
parts.push(`${v} ${tools[0].description}`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The description field is interpolated directly here without sanitization. Two concerns:

  1. In useReactToolScheduler.ts, when a tool call errors, description is set to JSON.stringify(request.args), producing output like Read {"file_path":"/some/path"} instead of falling back to Read 1 file.
  2. The in-process path applies no ANSI/control-character stripping (unlike the daemon path which uses sanitizeDisplayText()), so shell commands or file paths with escape sequences would render verbatim.

Consider sanitizing and filtering:

Suggested change
parts.push(`${v} ${tools[0].description}`);
const desc = tools[0].description?.replace(/[\x00-\x1f\x7f]/g, '') ?? '';
const isDisplaySafe = desc.length > 0 && !desc.startsWith('{') && !desc.startsWith('[');
if (isDisplaySafe) {
parts.push(`${v} ${desc}`);
} else if (tools.length === 1) {
parts.push(`${v} 1 ${template.singular}`);
} else {

— qwen3.7-max via Qwen Code /review

… ANSI

When a tool call errors, useReactToolScheduler sets description to
JSON.stringify(args) which produces '{...}' blobs. Strip ANSI escape
sequences and reject JSON-looking descriptions so the summary falls
back to the count format instead of rendering raw JSON.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

const stripped = raw.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
// Reject control characters (except tab/newline)
// eslint-disable-next-line no-control-regex
const cleaned = stripped.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The control-character regex deliberately preserves \n (0x0a) and \r (0x0d), but shell tool descriptions can contain literal newlines — e.g., heredocs, multi-line bash -c commands, or commands with embedded \n. These survive sanitization and flow into the summary string rendered by <Text wrap="truncate-end">.

Ink's <Text> breaks on embedded newlines, which would render the compact summary across multiple rows — defeating the "compact" layout with no error logged.

Suggested change
const cleaned = stripped.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
const cleaned = stripped.replace(/[\x00-\x1f\x7f]/g, '');

This removes all C0 control characters including tab/newline/CR. Alternatively, replace them with spaces to preserve readability: .replace(/[\r\n]+/g, ' ').

— qwen3.7-max via Qwen Code /review


// Strip ANSI escape sequences
// eslint-disable-next-line no-control-regex
const stripped = raw.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The CSI regex \x1b\[[0-9;]*[a-zA-Z] only matches SGR/CSI sequences. Non-CSI ANSI sequences like \x1b(B (charset selection), \x1b7 (save cursor), and OSC \x1b]... pass through unmatched.

The second-pass control-character regex strips the ESC byte (0x1B falls in \x0e-\x1f), but the residual characters remain as garbled text: \x1b(B(B, \x1b77. For typical file paths and commands this is unlikely, but terminal output or unusual tool descriptions could trigger it.

Consider broadening the ANSI strip to cover common non-CSI forms, or stripping all ESC-prefixed sequences:

Suggested change
const stripped = raw.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
const stripped = raw.replace(/\x1b\][^\x07]*\x07|\x1b[()][A-Z0-9]|\x1b\[[0-9;]*[a-zA-Z]|\x1b./g, '');

— qwen3.7-max via Qwen Code /review

…summary

Strip all common ANSI escape sequences (OSC, charset, CSI, single-byte
ESC) instead of just CSI. Replace all C0 control characters including
newlines with spaces so embedded \n in shell descriptions does not
break the single-line compact summary layout.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
… summary

The buildToolSummary change from 'Read 1 file' to 'Read a.ts' broke
4 assertions in ToolGroupMessage.test.tsx. Update all 5 occurrences
to use the new description-based format.

Signed-off-by: Alex <alex.tech.lab@outlook.com>

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

No review findings. Downgraded from Approve to Comment: CI still running.

The implementation is clean and well-scoped — safeDescription() handles all common edge cases (ANSI escapes, JSON error blobs, empty descriptions), tests are thorough at 21+ cases, and the count-format fallback preserves backward compatibility for multi-tool groups.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

✅ Verification report — PR #6448 (real TUI build, BASE↔PR A/B)

Verified the built qwen CLI (v0.19.6) from this PR's head (f9d17c3) in a real interactive TUI (tmux, Linux) driven by a deterministic mock OpenAI provider, comparing the compiled component against main. The feature works exactly as described.

Verdict: LGTM — safe to merge. Behavior is correct, fallbacks hold, truncation is graceful. A few non-blocking notes for the record are at the bottom.

This PR marks 🐧 Linux as ⚠️ untested — this run provides the Linux ✅.

Headline: single collapsible tool now shows the path/pattern

Same session, same mock tool calls; the only variable is the compiled CompactToolGroupDisplay.js (esbuild BASE vs PR, verified byte-behaviorally identical to the real tsc build).

BASE vs PR side by side

Scenario (1 model turn) main (BASE) PR #6448
single read_file Read 1 file Read src/hello.ts
single grep_search Searched 1 pattern Searched 'buildToolSummary'
single list_directory Listed 1 directory Listed src
read_file (multi) Read 3 files Read 3 files (count preserved ✓)

Edge cases exercised in the live TUI

Multi-category, all collapsible in one turn — descriptions joined in category order (search → read → list):

multi-category one turn

  • Long path, narrow (62-col) pane → truncates to one line with , no layout break: ✓ Read src/very/deeply/nested/directory/structure/that/… — confirms the wrap="truncate-end" mitigation for the PR's stated main risk.
  • Errored read_file (non-existent file) → force-expands to an individual ✗ ReadFile {…} message; it never reaches the compact summary (see note 1).

Unit tests & non-vacuity

  • CompactToolGroupDisplay.test.tsx 26/26 ✓ · ToolGroupMessage.test.tsx 45/45 ✓ (71 total).
  • Mutation check (proves the tests aren't vacuous): reverting only the source to merge-base while keeping the PR's tests fails exactly 12 assertions with the precise Read 1 file → Read a.ts, Searched 1 pattern, listed 1 directory → Searched pattern, listed /src deltas. The tests genuinely pin the new behavior.

Notes (non-blocking — for the record)

  1. safeDescription's JSON/args guard is defensive but unreachable via ToolGroupMessage. A JSON-blob description only ever occurs when a tool call errors (mapToDisplay sets description = JSON.stringify(request.args)), but hasErrorTool triggers forceExpandAll, so errored tools are partitioned into nonCollapsibleTools and never reach buildToolSummary. Verified live (errored read renders individually as ✗ ReadFile {"file_path":…}). The guard is harmless and its unit tests are green, but it's guarding a path this component already prevents — worth knowing, not worth changing.
  2. Two PR-body examples don't match the rendered TUI (cosmetic doc drift):
    • grep shows Searched 'buildToolSummary', not Searched 'foo' in path './' — the default tool is RipGrepTool, whose getDescription() omits in path … when no path arg is given (the in path './' form is the legacy GrepTool).
    • The "1 read + 1 shell → Read src/a.ts, ran ls" row is a buildToolSummary()-in-isolation result. In the real TUI, command/edit/write tools are non-collapsible and render as their own ToolMessage; the compact summary only ever receives read/search/list. So a mixed read+shell turn shows Read src/a.ts in the summary plus a separate shell message — not a combined string.
  3. Truncation tradeoff (already acknowledged in the PR). Long descriptions truncate the whole summary line to one row, so in a mixed multi-category summary later categories can be truncated off in narrow terminals — the old count format never truncated. Confirmed graceful (no wrap/overflow); just a UX note.
🔧 How this was verified (method)
  • git worktree at PR head f9d17c3; node_modules hardlinked from a healthy checkout; core + cli rebuilt from source in the worktree (fully consistent PR build).
  • A/B at dist granularity: the merge-base version of CompactToolGroupDisplay.tsx is byte-identical to current main, so main's compiled output is a valid BASE. Both arms deployed via esbuild-transpile of the respective source (identical toolchain; only the source differs) and confirmed behaviorally identical to the real tsc build (buildToolSummary parity on all sample inputs). A swap-audit log grepped a PR-only symbol (safeDescription) in the deployed file before each arm.
  • Driver: the repo's zero-dep mock OpenAI server, scripted per-prompt to emit read_file/grep_search/list_directory tool calls against real files in an isolated workspace; CLI run interactively with --approval-mode yolo (auto-approve avoids the confirmation force-expand) so model-initiated collapsible groups render through the compact path.
  • "Screenshots": tmux capture-pane -e (ANSI preserved) → HTML → headless Chromium.

🇨🇳 中文版验证报告(点击展开)

✅ 验证报告 — PR #6448(真实 TUI 构建,BASE↔PR A/B 对比)

真实交互式 TUI(tmux,Linux)中验证了本 PR head(f9d17c3)构建出的 qwen CLI(v0.19.6),由确定性的 mock OpenAI provider 驱动,并与 main 的编译产物逐一对比。该功能完全符合描述

结论:LGTM — 可以合并。 行为正确、回退逻辑成立、截断处理优雅。文末列了几条不阻塞合并的备注。

本 PR 将 🐧 Linux 标为 ⚠️ 未测试 —— 本次运行补上了 Linux ✅。

核心:单个可折叠工具现在显示路径/模式

同一会话、同一批 mock 工具调用,唯一变量是编译后的 CompactToolGroupDisplay.js(esbuild 编译的 BASE vs PR,已验证与真实 tsc 构建行为逐字节一致)。

(截图见上方英文版 side-by-side 图)

场景(单次模型轮次) main(BASE) PR #6448
单个 read_file Read 1 file Read src/hello.ts
单个 grep_search Searched 1 pattern Searched 'buildToolSummary'
单个 list_directory Listed 1 directory Listed src
3 个 read_file(多个) Read 3 files Read 3 files (计数格式保留 ✓)

实际 TUI 中验证的边界情况

  • 单轮多类别、全部可折叠 → 描述按类别顺序(search → read → list)拼接:Searched 'greeting', read src/hello.ts, listed src/utils(见上方 multi 图)。
  • 长路径 + 窄(62 列)终端 → 截断为单行并带 ,不破坏布局:✓ Read src/very/deeply/nested/directory/structure/that/… —— 印证了 PR 声明的主要风险已被 wrap="truncate-end" 处理。
  • read_file 出错(文件不存在) → 强制展开为独立的 ✗ ReadFile {…} 消息,不会进入紧凑摘要行(见备注 1)。

单元测试与非空验证

  • CompactToolGroupDisplay.test.tsx 26/26 ✓ · ToolGroupMessage.test.tsx 45/45 ✓(共 71)。
  • 变异测试(证明测试并非空转): 仅把源码回退到 merge-base、保留 PR 的测试,会精确失败 12 条断言,且失败信息正是 Read 1 file → Read a.tsSearched 1 pattern, listed 1 directory → Searched pattern, listed /src 等差异。测试确实锁定了新行为。

备注(不阻塞合并,仅作记录)

  1. safeDescription 中的 JSON/args 防护是防御性的,但通过 ToolGroupMessage 路径不可达。 只有工具调用出错时 description 才会是 JSON blob(mapToDisplaydescription = JSON.stringify(request.args)),而 hasErrorTool 会触发 forceExpandAll,出错工具被归入 nonCollapsibleTools,永远到不了 buildToolSummary。已实测(出错的 read 渲染为独立的 ✗ ReadFile {"file_path":…})。这段防护无害、其单测也是绿的,但它守的是该组件本身已经规避掉的路径 —— 知道即可,不必改。
  2. PR 描述里有两个示例与实际 TUI 渲染不符(文案层面的小偏差):
    • grep 显示的是 Searched 'buildToolSummary',而非 Searched 'foo' in path './' —— 默认工具是 RipGrepTool,其 getDescription() 在未传 path 时不带 in path …in path './' 是旧版 GrepTool 的形式)。
    • "1 read + 1 shell → Read src/a.ts, ran ls" 这一行是 buildToolSummary() 单独调用的结果。真实 TUI 中 command/edit/write 属于非折叠工具,会渲染成各自的 ToolMessage;紧凑摘要只会收到 read/search/list。所以 read+shell 混合轮次实际显示的是摘要行里的 Read src/a.ts 加上一条独立的 shell 消息,而不是拼成一个字符串。
  3. 截断的取舍(PR 已声明)。 长描述会把整行摘要截断成一行,因此在窄终端下、多类别混合摘要里靠后的类别可能被截掉 —— 旧的计数格式从不截断。已确认截断优雅(不换行/不溢出),仅作 UX 提示。
🔧 验证方法
  • 在 PR head f9d17c3git worktreenode_modules 从健康 checkout 硬链接;在 worktree 内从源码重建 core + cli(完全一致的 PR 构建)。
  • dist 粒度 A/B: CompactToolGroupDisplay.tsx 的 merge-base 版本与当前 main 逐字节相同,故 main 的编译产物即有效 BASE。两侧均用 esbuild 转译各自源码部署(工具链一致,唯一变量是源码),并确认与真实 tsc 构建行为一致(buildToolSummary 在所有样例输入上一致)。每次切换前用 swap 审计日志 grep 部署文件里的 PR 专有符号(safeDescription)。
  • 驱动: 仓库自带零依赖 mock OpenAI server,按 prompt 脚本化地对隔离工作区的真实文件发出 read_file/grep_search/list_directory 调用;CLI 以 --approval-mode yolo 交互式运行(自动批准以避开确认框导致的强制展开),使模型发起的可折叠工具组走紧凑摘要路径。
  • "截图": tmux capture-pane -e(保留 ANSI)→ HTML → 无头 Chromium。

@wenshao

wenshao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

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

@qwen-code-ci-bot qwen-code-ci-bot added category/ui User interface and display scope/components UI components and widgets labels Jul 8, 2026
@wenshao
wenshao added this pull request to the merge queue Jul 8, 2026
Merged via the queue into QwenLM:main with commit 49aa4c8 Jul 8, 2026
35 checks passed
chiga0 pushed a commit to chiga0/qwen-code that referenced this pull request Jul 9, 2026
Resolve conflicts in three files:

- CompactToolGroupDisplay.tsx: combine this branch's i18n count-phrase
  summaries with main's single-tool description display (QwenLM#6448). Kept the
  localized {{count}} phrases for the multi-tool / no-description paths and
  added English verb prefixes (pastVerb/activeVerb) for the single-tool
  "Read a.ts" case, since the description it precedes is a language-neutral
  path/command and bare-verb i18n keys would collide with existing entries.
- resumeHistoryUtils.ts: keep both import groups (isCollapsibleTool and
  main's history-gap-notice helpers).
- AppContainer.tsx: call main's shouldDrainMessageQueue() guard and keep this
  branch's extra guard that suppresses queue draining while the transcript is
  open.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category/ui User interface and display scope/components UI components and widgets

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants