Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6d5fa82
feat(tui): remove tool group borders and collapse completed tool results
qinqi-ali Jun 11, 2026
a0b018c
fix(tui): gate collapse on compact mode and fix innerWidth calculation
qinqi-ali Jun 13, 2026
9948cbe
fix(tui): address review feedback on collapse and visual alignment
qinqi-ali Jun 22, 2026
ea80b74
feat(tui): unify tool output with semantic summaries
qinqi-ali Jun 22, 2026
89faa9c
test(tui): add buildToolSummary unit tests and fix stale comment
qinqi-ali Jun 22, 2026
4146279
fix(tui): address audit findings for unified tool output
qinqi-ali Jun 23, 2026
3275994
fix(tui): address inline review findings
qinqi-ali Jun 23, 2026
500200c
fix(tui): remove unused compactMode import in HistoryItemDisplay
qinqi-ali Jun 23, 2026
089ffaf
ci: trigger re-run with updated merge ref
qinqi-ali Jun 23, 2026
f496048
refactor(tui): partition tools by type instead of completion status
qinqi-ali Jun 23, 2026
39f5076
fix(tui): collapse text/ANSI output for completed tools
qinqi-ali Jun 23, 2026
480f7db
fix(tui): unify summary format and show results in error-expanded groups
qinqi-ali Jun 23, 2026
7cbf39f
test(tui): improve coverage for partition logic and result collapse
qinqi-ali Jun 23, 2026
2e33ae7
fix(tui): align CompactToolGroupDisplay style with ToolInfo
qinqi-ali Jun 23, 2026
626f89f
fix(tui): address review findings on result collapse and memory safety
qinqi-ali Jun 24, 2026
ccda337
docs(tui): sync design doc with actual implementation
qinqi-ali Jun 24, 2026
50ea752
test(tui): update AppContainer test for compactToggleHasVisualEffect …
qinqi-ali Jun 24, 2026
a0f1ed3
Merge branch 'main' into worktree-feat+tui-tool-collapse
wenshao Jun 24, 2026
5ccbcd4
fix(tui): restore per-tool forceShowResult and harden edge cases
qinqi-ali Jun 24, 2026
6a50310
test(tui): add coverage for canceled partition, memory badge, and leg…
qinqi-ali Jun 24, 2026
3ab8365
fix(tui): document dual effect of isCollapsibleTool and add ANSI coll…
qinqi-ali Jun 24, 2026
b58839c
Merge branch 'main' into worktree-feat+tui-tool-collapse
wenshao Jun 25, 2026
b20b744
fix(tui): address review round 6 — type fix, stale refs, and test gaps
qinqi-ali Jun 25, 2026
71e3422
test(tui): add isUserInitiated and memory-only error test coverage
qinqi-ali Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions docs/design/tool-use-summary/unified-tool-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Unified Tool Output Rendering

## Background

The TUI previously had two rendering modes for tool results:

- **Compact mode** (Ctrl+O): collapsed completed tool results into a one-line summary
- **Normal mode**: showed full tool results inline, causing excessive vertical noise

Users had to manually toggle between modes. Most of the time, completed tool results (file contents, search results, etc.) added no value to the conversation flow.

## Design

### Core Principle

**One unified mode**: tool rendering is determined by tool category, not by a user-toggled mode. Information-gathering tools (read/search/list) are collapsed into a summary; mutation tools (edit/write/command/agent) always render individually with full results.

### Semantic Summary (`buildToolSummary`)

Instead of showing raw tool names and counts (`ReadFile x 3`), generate human-readable summaries using a count-based format:

| Scenario | Output |
| ------------------ | --------------------------------------------- |
| Single tool | `Read 1 file` / `Ran 1 command` |
| Multiple same-type | `Read 3 files` |
| Mixed types | `Ran 1 command, read 3 files, edited 2 files` |
| Active (executing) | `Reading 1 file` (present progressive) |
| Completed | `Read 1 file` (past tense) |

### Tool Categories

| Category | Display Names | Past Verb | Active Verb | Collapsible |
| -------- | ---------------------------- | --------- | ----------- | ----------- |
| read | ReadFile, Read File(s) | Read | Reading | Yes |
| edit | Edit, NotebookEdit | Edited | Editing | No |
| write | WriteFile | Wrote | Writing | No |
| search | Grep, Glob | Searched | Searching | Yes |
| list | ListFiles, Read Directory | Listed | Listing | Yes |
| command | Shell | Ran | Running | No |
| agent | Agent, Workflow, SendMessage | Ran | Running | No |
| other | (everything else) | Used | Using | No |

### Rendering Rules

1. **Type-based partition**: tools are split by `isCollapsibleTool()` — collapsible tools (read/search/list) render as a `CompactToolGroupDisplay` summary line; non-collapsible tools (edit/write/command/agent/other) render individually via `ToolMessage`
2. **Memory-only groups** have a dedicated rendering path (read/write counts badge) that takes priority, but only when all ops succeed (`!hasErrorTool && every status === Success`)
3. **Result collapse**: only collapsible tools with `Success` status have their text/ANSI output collapsed. Non-collapsible tools (including MCP tools, WebFetch, etc.) always show results. Canceled tools keep partial output visible
4. **Tool names** render bold regardless of status, providing consistent styling across both `CompactToolGroupDisplay` and individual `ToolMessage` paths
5. **Force-expand conditions**: when any tool in a group is confirming, errored, user-initiated, in a focused shell, or a terminal subagent, ALL tools render individually (no partition) with results forced visible only for the triggering tools (errored, confirming, terminal subagent) — successful siblings keep normal collapse behavior
6. **`tool_use_summary`** items (LLM-generated semantic summaries) render unconditionally alongside `CompactToolGroupDisplay`'s mechanical count — they serve different purposes (semantic context vs tool count)
7. **Memory badge**: rendered in both the all-collapsible path and the mixed path when memory ops are present in a non-memory-only group

### Key Changes

| File | Change |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CompactToolGroupDisplay.tsx` | Added `buildToolSummary()` with count format, `isCollapsibleTool()`, removed border styles |
| `ToolMessage.tsx` | `shouldCollapseResult` gated on `isCollapsibleTool()` and `Success` only; `isDim` removed |
| `ToolGroupMessage.tsx` | Type-based partition replaces `showCompact`; `forceShowResult` simplified to `forceExpandAll`; height budget accounts for collapsible summary row |

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 Key Changes table states forceShowResult simplified to forceExpandAll, but the code was reverted to per-tool computation (commit 5ccbcd4). Rule 5 in the same doc correctly says "results forced visible only for the triggering tools" — the table contradicts it. A future maintainer reading this table will make incorrect assumptions about the implementation.

Suggested change
| `ToolGroupMessage.tsx` | Type-based partition replaces `showCompact`; `forceShowResult` simplified to `forceExpandAll`; height budget accounts for collapsible summary row |
| `ToolGroupMessage.tsx` | Type-based partition replaces `showCompact`; `forceShowResult` uses per-tool computation (Confirming, Error, agent-pending, terminal-subagent); height budget accounts for collapsible summary row |

— qwen3.7-max via Qwen Code /review

| `MainContent.tsx` | Removed `mergedHistory` alias, `absorbedCallIds`, `summaryByCallId`, cross-group merging |
| `HistoryItemDisplay.tsx` | `tool_use_summary` renders unconditionally (removed `summaryAbsorbed` gate) |
| `mergeCompactToolGroups.ts` | `compactToggleHasVisualEffect` no longer triggers on `tool_group` (compact mode has no effect on tool rendering) |

## Alternatives Considered

1. **Keep two modes with improved summaries**: Rejected — unnecessary cognitive overhead for users
2. **Per-tool summary (Gemini CLI style)**: Each tool gets its own summary arrow. Rejected — still too verbose for large tool batches
3. **Phased rollout**: Rejected — user preference for single implementation pass
4 changes: 2 additions & 2 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3443,7 +3443,7 @@ describe('AppContainer State Management', () => {
);
});

it('calls refreshStatic on Ctrl+O when history contains a tool_group', () => {
it('skips refreshStatic on Ctrl+O when history contains only tool_group (no visual effect)', () => {
mockedUseHistory.mockReturnValue({
history: [
{ type: 'user', id: 1, text: 'run ls' },
Expand Down Expand Up @@ -3483,7 +3483,7 @@ describe('AppContainer State Management', () => {
expect(handler).toBeDefined();
handler!(ctrlOKey);

expect(mockStdout.write).toHaveBeenCalledWith(
expect(mockStdout.write).not.toHaveBeenCalledWith(
ansiEscapes.clearTerminal,
);
});
Expand Down
49 changes: 5 additions & 44 deletions packages/cli/src/ui/components/HistoryItemDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import { BtwMessage } from './messages/BtwMessage.js';
import { MemorySavedMessage } from './messages/MemorySavedMessage.js';
import { DiffStatsDisplay } from './messages/DiffStatsDisplay.js';
import { GoalStatusMessage } from './messages/GoalStatusMessage.js';
import { useCompactMode } from '../contexts/CompactModeContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useThoughtExpanded } from '../contexts/ThoughtExpandedContext.js';
import { useThinkingViewer } from '../contexts/ThinkingViewerContext.js';
Expand All @@ -76,21 +75,6 @@ interface HistoryItemDisplayProps {
activeShellPtyId?: number | null;
embeddedShellFocused?: boolean;
availableTerminalHeightGemini?: number;
/**
* When the item is a `tool_group`, an optional short LLM-generated label
* summarizing the batch. Replaces the generic "Tool × N" line in compact
* mode. Computed by the parent from `tool_use_summary` history items.
*/
compactLabel?: string;
/**
* When the item is a `tool_use_summary`, true if a sibling tool_group has
* absorbed this label via its compact-mode header. The standalone `● <label>`
* line is suppressed in that case. False for force-expanded groups in
* compact mode (they render through the full ToolGroupMessage path and
* don't consume compactLabel, so the standalone line is the label's only
* path to the screen) and for all tool_use_summary items in full mode.
*/
summaryAbsorbed?: boolean;
sourceCopyIndexOffsets?: MarkdownSourceCopyIndexOffsets;
/** Force thinking blocks expanded (e.g. in SessionPreview). */
thoughtExpanded?: boolean;
Expand Down Expand Up @@ -206,15 +190,12 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
activeShellPtyId,
embeddedShellFocused,
availableTerminalHeightGemini,
compactLabel,
summaryAbsorbed = false,
sourceCopyIndexOffsets,
thoughtExpanded,
thinkingFullText,
}) => {
const marginTop = getHistoryItemMarginTop(item);

const { compactMode } = useCompactMode();
const contextThoughtExpanded = useThoughtExpanded();
const resolvedThoughtExpanded = thoughtExpanded ?? contextThoughtExpanded;
const settings = useSettings();
Expand Down Expand Up @@ -355,33 +336,13 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
memoryWriteCount={itemForDisplay.memoryWriteCount}
memoryReadCount={itemForDisplay.memoryReadCount}
isUserInitiated={itemForDisplay.isUserInitiated}
compactLabel={compactLabel}
/>
)}
{/*
`tool_use_summary` as a standalone inline item.

In full mode (`compactMode=false`), the label arrives via the fast-model
call AFTER the tool_group has been committed to Ink's append-only
<Static>, so we cannot update the tool_group's header retroactively.
Rendering a standalone `● <label>` line appends cleanly.

In compact mode, the label is normally absorbed into the merged
tool_group's header (via `compactLabel` prop to CompactToolGroupDisplay),
and `summaryAbsorbed=true` is set so this block does nothing. But when
the sibling tool_group is force-expanded (errors, confirmations,
user-initiated, focused shell), the full-expand path ignores
`compactLabel`, and `MainContent` leaves `summaryAbsorbed=false` —
the standalone line below is then the label's only route to the UI,
which is exactly the case where a summary is most diagnostically
useful ("Fixed NPE in UserService" on an errored batch).
*/}
{itemForDisplay.type === 'tool_use_summary' &&
(!compactMode || !summaryAbsorbed) && (
<Box paddingLeft={1}>
<Text dimColor>● {itemForDisplay.summary}</Text>
</Box>
)}
{itemForDisplay.type === 'tool_use_summary' && (
Comment thread
chiga0 marked this conversation as resolved.
<Box paddingLeft={1}>
<Text dimColor>● {itemForDisplay.summary}</Text>
Comment thread
chiga0 marked this conversation as resolved.
</Box>
)}
{itemForDisplay.type === 'compression' && (
<CompressionMessage compression={itemForDisplay.compression} />
)}
Expand Down
Loading
Loading