diff --git a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap index b4f2bc919c..813a6ae36f 100644 --- a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap @@ -37,10 +37,13 @@ Tips for getting started: 2. /help for more information 3. Ask coding questions, edit code or run commands 4. Be specific for the best results + ╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool1 Description for tool 1 │ │ │ ╰──────────────────────────────────────────────────────────────────────────╯ + + ╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool2 Description for tool 2 │ │ │ @@ -81,10 +84,13 @@ Tips for getting started: 2. /help for more information 3. Ask coding questions, edit code or run commands 4. Be specific for the best results + ╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool1 Description for tool 1 │ │ │ ╰──────────────────────────────────────────────────────────────────────────╯ + + ╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool2 Description for tool 2 │ │ │ diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap index c0043bf6f9..bfadffa240 100644 --- a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap @@ -3,16 +3,15 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focused shell should expand' 1`] = ` "ScrollableList AppHeader(full) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ Shell Command Running a long command... │ │ │ -│ Line 10 │ -│ Line 11 │ │ Line 12 │ │ Line 13 │ │ Line 14 │ -│ Line 15 █ │ -│ Line 16 █ │ +│ Line 15 │ +│ Line 16 │ │ Line 17 █ │ │ Line 18 █ │ │ Line 19 █ │ @@ -24,16 +23,15 @@ AppHeader(full) exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocused shell' 1`] = ` "ScrollableList AppHeader(full) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ Shell Command Running a long command... │ │ │ -│ Line 10 │ -│ Line 11 │ │ Line 12 │ │ Line 13 │ │ Line 14 │ -│ Line 15 █ │ -│ Line 16 █ │ +│ Line 15 │ +│ Line 16 │ │ Line 17 █ │ │ Line 18 █ │ │ Line 19 █ │ @@ -44,12 +42,11 @@ AppHeader(full) exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = ` "AppHeader(full) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ Shell Command Running a long command... │ │ │ -│ ... first 11 lines hidden (Ctrl+O to show) ... │ -│ Line 12 │ -│ Line 13 │ +│ ... first 13 lines hidden (Ctrl+O to show) ... │ │ Line 14 │ │ Line 15 │ │ Line 16 │ @@ -63,6 +60,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Con exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = ` "AppHeader(full) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ Shell Command Running a long command... │ │ │ @@ -92,6 +90,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unc exports[`MainContent > renders a split tool group without a gap between static and pending areas 1`] = ` "AppHeader(full) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ✓ test-tool A tool for testing │ │ │ diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx index a8f5ee5fb9..2751f4fd2c 100644 --- a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx @@ -536,6 +536,7 @@ export const DenseToolMessage: React.FC = (props) => { = (props) => { )} {showPayload && (!isAlternateBuffer || !diff) && viewParts.payload && ( - + {viewParts.payload} )} {showPayload && outputFile && ( - + (Output saved to: {outputFile}) diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx index 5789a698c7..fcc64a0fe2 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx @@ -55,7 +55,7 @@ describe('ToolGroupMessage Compact Rendering', () => { expect(output).toMatchSnapshot(); }); - it('adds an empty line between a compact tool and a standard tool', async () => { + it('does not add an extra empty line between a compact tool and a standard tool', async () => { const toolCalls = [ { callId: 'call1', @@ -81,7 +81,7 @@ describe('ToolGroupMessage Compact Rendering', () => { expect(output).toMatchSnapshot(); }); - it('adds an empty line if a compact tool has a dense payload', async () => { + it('does not add an extra empty line if a compact tool has a dense payload', async () => { const toolCalls = [ { callId: 'call1', @@ -107,7 +107,7 @@ describe('ToolGroupMessage Compact Rendering', () => { expect(output).toMatchSnapshot(); }); - it('adds an empty line between a standard tool and a compact tool', async () => { + it('does not add an extra empty line between a standard tool and a compact tool', async () => { const toolCalls = [ { callId: 'call1', diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index b0e4418604..0fea53d69d 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -54,7 +54,7 @@ const COMPACT_OUTPUT_ALLOWLIST = new Set([ ]); // Helper to identify if a tool should use the compact view -const isCompactTool = ( +export const isCompactTool = ( tool: IndividualToolCallDisplay, isCompactModeEnabled: boolean, ): boolean => { @@ -68,7 +68,7 @@ const isCompactTool = ( }; // Helper to identify if a compact tool has a payload (diff, list, etc.) -const hasDensePayload = (tool: IndividualToolCallDisplay): boolean => { +export const hasDensePayload = (tool: IndividualToolCallDisplay): boolean => { if (tool.outputFile) return true; const res = tool.resultDisplay; if (!res) return false; @@ -121,7 +121,7 @@ export const ToolGroupMessage: React.FC = ({ toolCalls: allToolCalls, availableTerminalHeight, terminalWidth, - // borderTop: borderTopOverride, + borderTop: borderTopOverride, borderBottom: borderBottomOverride, isExpandable, }) => { @@ -207,29 +207,26 @@ export const ToolGroupMessage: React.FC = ({ const prevIsCompact = prevTool ? isCompactTool(prevTool, isCompactModeEnabled) : false; - const hasPayload = hasDensePayload(tool); - const prevHasPayload = prevTool ? hasDensePayload(prevTool) : false; if (isCompact) { height += 1; // Base height for compact tool // Spacing logic (matching marginTop) - if ( - isFirst || - isCompact !== prevIsCompact || - hasPayload || - prevHasPayload - ) { + if (isFirst) { + height += borderTopOverride ?? true ? 1 : 0; + } else if (!prevIsCompact) { height += 1; } } else { height += 3; // Static overhead for standard tool - if (isFirst || prevIsCompact) { - height += 1; + if (isFirst) { + height += borderTopOverride ?? true ? 1 : 0; + } else { + height += 1; // marginTop is always 1 for non-compact tools (not first) } } } return height; - }, [visibleToolCalls, isCompactModeEnabled]); + }, [visibleToolCalls, isCompactModeEnabled, borderTopOverride]); let countToolCallsWithResults = 0; for (const tool of visibleToolCalls) { @@ -273,20 +270,18 @@ export const ToolGroupMessage: React.FC = ({ */ width={terminalWidth} paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN} - marginBottom={1} + marginBottom={borderBottomOverride ?? true ? 1 : 0} > {visibleToolCalls.map((tool, index) => { const isFirst = index === 0; const isLast = index === visibleToolCalls.length - 1; const isShellToolCall = isShellTool(tool.name); const isCompact = isCompactTool(tool, isCompactModeEnabled); - const hasPayload = hasDensePayload(tool); const prevTool = index > 0 ? visibleToolCalls[index - 1] : null; const prevIsCompact = prevTool ? isCompactTool(prevTool, isCompactModeEnabled) : false; - const prevHasPayload = prevTool ? hasDensePayload(prevTool) : false; const nextTool = !isLast ? visibleToolCalls[index + 1] : null; const nextIsCompact = nextTool @@ -295,12 +290,8 @@ export const ToolGroupMessage: React.FC = ({ let marginTop = 0; if (isFirst) { - marginTop = 1; - } else if (isCompact !== prevIsCompact) { - marginTop = 1; - } else if (isCompact && (hasPayload || prevHasPayload)) { - marginTop = 1; - } else if (!isCompact && prevIsCompact) { + marginTop = borderTopOverride ?? true ? 1 : 0; + } else if (!(isCompact && prevIsCompact)) { marginTop = 1; } @@ -309,7 +300,11 @@ export const ToolGroupMessage: React.FC = ({ availableTerminalHeight: availableTerminalHeightPerToolMessage, terminalWidth: contentWidth, emphasis: 'medium' as const, - isFirst: isCompact ? false : prevIsCompact || isFirst, + isFirst: isCompact + ? false + : isFirst + ? (borderTopOverride ?? true) + : prevIsCompact, borderColor, borderDimColor, isExpandable, @@ -358,7 +353,7 @@ export const ToolGroupMessage: React.FC = ({ borderLeft={true} borderRight={true} borderTop={false} - borderBottom={borderBottomOverride ?? true} + borderBottom={isLast ? (borderBottomOverride ?? true) : true} borderColor={borderColor} borderDimColor={borderDimColor} borderStyle="round" diff --git a/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx b/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx index acec8a2404..8f4069ce87 100644 --- a/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx @@ -123,9 +123,9 @@ describe('ToolMessage Sticky Header Regression', () => { // Content lines 1-4 should be scrolled off expect(lastFrame()).not.toContain('c1-01'); expect(lastFrame()).not.toContain('c1-04'); - // Line 6 and 7 should be visible (terminalHeight=5 means only 2 lines of content show below 3-line header) + // Line 5 and 6 should be visible (terminalHeight=5 means only 2 lines of content show below 3-line header + margin) + expect(lastFrame()).toContain('c1-05'); expect(lastFrame()).toContain('c1-06'); - expect(lastFrame()).toContain('c1-07'); expect(lastFrame()).toMatchSnapshot(); // Scroll further so tool-1 is completely gone and tool-2's header should be stuck diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap new file mode 100644 index 0000000000..1dfdb5b8fe --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap @@ -0,0 +1,132 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DenseToolMessage > Toggleable Diff View (Alternate Buffer) > hides diff content by default when in alternate buffer mode 1`] = ` +" ✓ test-tool test.ts → Accepted [Show Diff] +" +`; + +exports[`DenseToolMessage > Toggleable Diff View (Alternate Buffer) > shows diff content by default when NOT in alternate buffer mode 1`] = ` +" ✓ test-tool test.ts → Accepted + + 1 - old line + 1 + new line +" +`; + +exports[`DenseToolMessage > does not render result arrow if resultDisplay is missing 1`] = ` +" o test-tool Test description +" +`; + +exports[`DenseToolMessage > flattens newlines in string results 1`] = ` +" ✓ test-tool Test description → Line 1 Line 2 +" +`; + +exports[`DenseToolMessage > renders correctly for Edit tool using confirmationDetails 1`] = ` +" ? Edit styles.scss → Confirming + + 1 - body { color: blue; } + 1 + body { color: red; } +" +`; + +exports[`DenseToolMessage > renders correctly for Errored Edit tool 1`] = ` +" x Edit styles.scss → Failed [Show Diff] +" +`; + +exports[`DenseToolMessage > renders correctly for ReadManyFiles results 1`] = ` +" ✓ test-tool Attempting to read files from **/*.ts → Read 3 file(s) (1 ignored) + + file1.ts + file2.ts + file3.ts +" +`; + +exports[`DenseToolMessage > renders correctly for Rejected Edit tool 1`] = ` +" - Edit styles.scss → Rejected (+1, -1) + + 1 - old line + 1 + new line +" +`; + +exports[`DenseToolMessage > renders correctly for Rejected Edit tool with confirmationDetails and diffStat 1`] = ` +" - Edit styles.scss → Rejected (+1, -1) + + 1 - body { color: blue; } + 1 + body { color: red; } +" +`; + +exports[`DenseToolMessage > renders correctly for Rejected WriteFile tool 1`] = ` +" - WriteFile config.json → Rejected + + 1 - old content + 1 + new content +" +`; + +exports[`DenseToolMessage > renders correctly for WriteFile tool 1`] = ` +" ✓ WriteFile config.json → Accepted (+1, -1) + + 1 - old content + 1 + new content +" +`; + +exports[`DenseToolMessage > renders correctly for a successful string result 1`] = ` +" ✓ test-tool Test description → Success result +" +`; + +exports[`DenseToolMessage > renders correctly for error status with string message 1`] = ` +" x test-tool Test description → Error occurred +" +`; + +exports[`DenseToolMessage > renders correctly for file diff results with stats 1`] = ` +" ✓ test-tool test.ts → Accepted (+15, -6) + + 1 - old line + 1 + diff content +" +`; + +exports[`DenseToolMessage > renders correctly for grep results 1`] = ` +" ✓ test-tool Test description → Found 2 matches + + file1.ts:10: match 1 + file2.ts:20: match 2 +" +`; + +exports[`DenseToolMessage > renders correctly for ls results 1`] = ` +" ✓ test-tool Test description → Listed 2 files. (1 ignored) +" +`; + +exports[`DenseToolMessage > renders correctly for todo updates 1`] = ` +" ✓ test-tool Test description → Todos updated +" +`; + +exports[`DenseToolMessage > renders generic failure message for error status without string message 1`] = ` +" x test-tool Test description → Failed +" +`; + +exports[`DenseToolMessage > renders generic output message for unknown object results 1`] = ` +" ✓ test-tool Test description → Output received +" +`; + +exports[`DenseToolMessage > truncates long string results 1`] = ` +" ✓ test-tool Test description + → + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAA... +" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.compact.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.compact.test.tsx.snap index fbceeac794..34a0d86bfb 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.compact.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.compact.test.tsx.snap @@ -1,8 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`ToolGroupMessage Compact Rendering > adds an empty line between a compact tool and a standard tool 1`] = ` +exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line between a compact tool and a standard tool 1`] = ` " - ✓ list_directory → file1.txt + ✓ ReadFolder → file1.txt ╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ non-compact-tool │ @@ -12,7 +12,7 @@ exports[`ToolGroupMessage Compact Rendering > adds an empty line between a compa " `; -exports[`ToolGroupMessage Compact Rendering > adds an empty line between a standard tool and a compact tool 1`] = ` +exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line between a standard tool and a compact tool 1`] = ` " ╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ non-compact-tool │ @@ -20,21 +20,20 @@ exports[`ToolGroupMessage Compact Rendering > adds an empty line between a stand │ some large output │ ╰──────────────────────────────────────────────────────────────────────────╯ - ✓ list_directory → file1.txt + ✓ ReadFolder → file1.txt " `; -exports[`ToolGroupMessage Compact Rendering > adds an empty line if a compact tool has a dense payload 1`] = ` +exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line if a compact tool has a dense payload 1`] = ` " - ✓ list_directory → file1.txt - + ✓ ReadFolder → file1.txt ✓ ReadFile → read file " `; exports[`ToolGroupMessage Compact Rendering > renders consecutive compact tools without empty lines between them 1`] = ` " - ✓ list_directory → file1.txt file2.txt - ✓ list_directory → file3.txt + ✓ ReadFolder → file1.txt file2.txt + ✓ ReadFolder → file3.txt " `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap index c1ea071bc5..9b3dda89cd 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -1,7 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[` > Ask User Filtering > filtering logic for status='error' and hasResult='error message' 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ x Ask User │ │ │ │ error message │ @@ -10,7 +11,8 @@ exports[` > Ask User Filtering > filtering logic for status= `; exports[` > Ask User Filtering > filtering logic for status='success' and hasResult='test result' 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ Ask User │ │ │ │ test result │ @@ -19,7 +21,8 @@ exports[` > Ask User Filtering > filtering logic for status= `; exports[` > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ other-tool A tool for testing │ │ │ │ Test result │ @@ -28,7 +31,8 @@ exports[` > Ask User Filtering > shows other tools when ask_ `; exports[` > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ test-tool A tool for testing │ │ │ │ Test result │ @@ -41,7 +45,8 @@ exports[` > Border Color Logic > uses gray border when all t `; exports[` > Border Color Logic > uses yellow border for shell commands even when successful 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ run_shell_command A tool for testing │ │ │ │ Test result │ @@ -55,18 +60,19 @@ exports[` > Golden Snapshots > renders header when scrolled "╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool-1 Description 1. This is a long description that will need to b… │ │──────────────────────────────────────────────────────────────────────────│ -│ line5 │ █ -│ │ █ +│ │ ▄ │ ✓ tool-2 Description 2 │ █ │ │ █ │ line1 │ █ │ line2 │ █ ╰──────────────────────────────────────────────────────────────────────────╯ █ + █ " `; exports[` > Golden Snapshots > renders mixed tool calls including shell command 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ read_file Read a file │ │ │ │ Test result │ @@ -83,7 +89,8 @@ exports[` > Golden Snapshots > renders mixed tool calls incl `; exports[` > Golden Snapshots > renders multiple tool calls with different statuses (only visible ones) 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ successful-tool This tool succeeded │ │ │ │ Test result │ @@ -100,7 +107,8 @@ exports[` > Golden Snapshots > renders multiple tool calls w `; exports[` > Golden Snapshots > renders single successful tool call 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ test-tool A tool for testing │ │ │ │ Test result │ @@ -109,7 +117,8 @@ exports[` > Golden Snapshots > renders single successful too `; exports[` > Golden Snapshots > renders tool call with outputFile 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool-with-file Tool that saved output to file │ │ │ │ Test result │ @@ -119,17 +128,18 @@ exports[` > Golden Snapshots > renders tool call with output `; exports[` > Golden Snapshots > renders two tool groups where only the last line of the previous group is visible 1`] = ` -"╰──────────────────────────────────────────────────────────────────────────╯ -╭──────────────────────────────────────────────────────────────────────────╮ +"╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool-2 Description 2 │ -│ │ ▄ -│ line1 │ █ +│ │ +│ line1 │ ╰──────────────────────────────────────────────────────────────────────────╯ █ + █ " `; exports[` > Golden Snapshots > renders with limited terminal height 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ tool-with-result Tool with output │ │ │ │ This is a long result that might need height constraints │ @@ -142,7 +152,8 @@ exports[` > Golden Snapshots > renders with limited terminal `; exports[` > Golden Snapshots > renders with narrow terminal width 1`] = ` -"╭──────────────────────────────────╮ +" +╭──────────────────────────────────╮ │ ✓ very-long-tool-name-that-mig… │ │ │ │ Test result │ @@ -151,7 +162,8 @@ exports[` > Golden Snapshots > renders with narrow terminal `; exports[` > Height Calculation > calculates available height correctly with multiple tools with results 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ +" +╭──────────────────────────────────────────────────────────────────────────╮ │ ✓ test-tool A tool for testing │ │ │ │ Result 1 │ diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap index dda93c1c21..da341abe37 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap @@ -1,11 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 1`] = ` -"╭────────────────────────────────────────────────────────────────────────╮ █ -│ ✓ Shell Command Description for Shell Command │ █ +" █ +╭────────────────────────────────────────────────────────────────────────╮ ▀ +│ ✓ Shell Command Description for Shell Command │ │ │ │ shell-01 │ -│ shell-02 │ " `; @@ -13,17 +13,17 @@ exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage i "╭────────────────────────────────────────────────────────────────────────╮ │ ✓ Shell Command Description for Shell Command │ ▄ │────────────────────────────────────────────────────────────────────────│ █ -│ shell-06 │ ▀ -│ shell-07 │ +│ shell-05 │ +│ shell-06 │ " `; exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 1`] = ` -"╭────────────────────────────────────────────────────────────────────────╮ █ +" █ +╭────────────────────────────────────────────────────────────────────────╮ │ ✓ tool-1 Description for tool-1 │ │ │ │ c1-01 │ -│ c1-02 │ " `; @@ -31,8 +31,8 @@ exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessa "╭────────────────────────────────────────────────────────────────────────╮ │ ✓ tool-1 Description for tool-1 │ █ │────────────────────────────────────────────────────────────────────────│ +│ c1-05 │ │ c1-06 │ -│ c1-07 │ " `; @@ -40,7 +40,7 @@ exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessa "│ │ │ ✓ tool-2 Description for tool-2 │ │────────────────────────────────────────────────────────────────────────│ -│ c2-10 │ -╰────────────────────────────────────────────────────────────────────────╯ █ +│ c2-09 │ ▄ +│ c2-10 │ ▀ " `; diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index c394b866ad..b314f1382b 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -75,6 +75,9 @@ import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { useLogger } from './useLogger.js'; import { SHELL_COMMAND_NAME } from '../constants.js'; import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js'; +import { + isCompactTool, +} from '../components/messages/ToolGroupMessage.js'; import { useToolScheduler, type TrackedToolCall, @@ -291,9 +294,34 @@ export const useGeminiStream = ( (tc) => !pushedToolCallIdsRef.current.has(tc.request.callId), ); if (toolsToPush.length > 0) { + const isCompactModeEnabled = + settings.merged.ui?.compactToolOutput === true; + const firstToolToPush = toolsToPush[0]; + const tcIndex = toolCalls.indexOf(firstToolToPush); + const prevTool = tcIndex > 0 ? toolCalls[tcIndex - 1] : null; + + let borderTop = isFirstToolInGroupRef.current; + if (!borderTop && prevTool) { + // If the first tool in this push is non-compact but follows a compact tool, + // we must start a new border group. + const currentIsCompact = isCompactTool( + mapTrackedToolCallsToDisplay(firstToolToPush as TrackedToolCall) + .tools[0], + isCompactModeEnabled, + ); + const prevWasCompact = isCompactTool( + mapTrackedToolCallsToDisplay(prevTool as TrackedToolCall) + .tools[0], + isCompactModeEnabled, + ); + if (!currentIsCompact && prevWasCompact) { + borderTop = true; + } + } + addItem( mapTrackedToolCallsToDisplay(toolsToPush as TrackedToolCall[], { - borderTop: isFirstToolInGroupRef.current, + borderTop, borderBottom: true, borderColor: theme.border.default, borderDimColor: false, @@ -426,14 +454,18 @@ export const useGeminiStream = ( if (toolsToPush.length > 0) { const newPushed = new Set(pushedToolCallIdsRef.current); let isFirst = isFirstToolInGroupRef.current; + const isCompactModeEnabled = + settings.merged.ui?.compactToolOutput === true; for (const tc of toolsToPush) { newPushed.add(tc.request.callId); + const tcIndex = toolCalls.indexOf(tc); + const prevTool = tcIndex > 0 ? toolCalls[tcIndex - 1] : null; + const nextTool = + tcIndex < toolCalls.length - 1 ? toolCalls[tcIndex + 1] : null; const isLastInBatch = tc === toolCalls[toolCalls.length - 1]; const historyItem = mapTrackedToolCallsToDisplay(tc, { - borderTop: isFirst, - borderBottom: isLastInBatch, ...getToolGroupBorderAppearance( { type: 'tool_group', tools: toolCalls }, activeShellPtyId, @@ -442,6 +474,33 @@ export const useGeminiStream = ( backgroundShells, ), }); + + const currentIsCompact = historyItem.tools[0] + ? isCompactTool(historyItem.tools[0], isCompactModeEnabled) + : false; + + let nextIsCompact = false; + if (nextTool) { + const nextHistoryItem = mapTrackedToolCallsToDisplay(nextTool); + nextIsCompact = nextHistoryItem.tools[0] + ? isCompactTool(nextHistoryItem.tools[0], isCompactModeEnabled) + : false; + } + + let prevWasCompact = false; + if (prevTool) { + const prevHistoryItem = mapTrackedToolCallsToDisplay(prevTool); + prevWasCompact = prevHistoryItem.tools[0] + ? isCompactTool(prevHistoryItem.tools[0], isCompactModeEnabled) + : false; + } + + historyItem.borderTop = + isFirst || (!currentIsCompact && prevWasCompact); + historyItem.borderBottom = currentIsCompact + ? isLastInBatch && !nextIsCompact + : isLastInBatch || nextIsCompact; + addItem(historyItem); isFirst = false; } @@ -459,6 +518,7 @@ export const useGeminiStream = ( activeShellPtyId, isShellFocused, backgroundShells, + settings.merged.ui?.compactToolOutput, ]); const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => { @@ -502,8 +562,7 @@ export const useGeminiStream = ( toolCalls.length > 0 && toolCalls.every((tc) => pushedToolCallIds.has(tc.request.callId)); - const anyVisibleInHistory = pushedToolCallIds.size > 0; - const anyVisibleInPending = remainingTools.some((tc) => { + const isToolVisible = (tc: TrackedToolCall) => { // AskUser tools are rendered by AskUserDialog, not ToolGroupMessage const isInProgress = tc.status !== 'success' && @@ -517,12 +576,28 @@ export const useGeminiStream = ( tc.status !== 'validating' && tc.status !== 'awaiting_approval' ); - }); + }; + + const anyVisibleInHistory = pushedToolCallIds.size > 0; + const anyVisibleInPending = remainingTools.some(isToolVisible); + + let lastVisibleIsCompact = false; + const isCompactModeEnabled = settings.merged.ui?.compactToolOutput === true; + for (let i = toolCalls.length - 1; i >= 0; i--) { + if (isToolVisible(toolCalls[i])) { + const mapped = mapTrackedToolCallsToDisplay(toolCalls[i]); + lastVisibleIsCompact = mapped.tools[0] + ? isCompactTool(mapped.tools[0], isCompactModeEnabled) + : false; + break; + } + } if ( toolCalls.length > 0 && !(allTerminal && allPushed) && - (anyVisibleInHistory || anyVisibleInPending) + (anyVisibleInHistory || anyVisibleInPending) && + !lastVisibleIsCompact ) { items.push({ type: 'tool_group' as const, @@ -540,6 +615,7 @@ export const useGeminiStream = ( activeShellPtyId, isShellFocused, backgroundShells, + settings.merged.ui?.compactToolOutput, ]); const lastQueryRef = useRef(null); diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg index 6a693d318b..bb05edfd84 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg @@ -1,8 +1,8 @@ - + - + @@ -17,16 +17,16 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - - - google_web_search - + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + google_web_search - Searching... - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + Searching... + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg index 1c0ff4b121..4fe69bb788 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg @@ -1,8 +1,8 @@ - + - + @@ -17,16 +17,16 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - - - run_shell_command - + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + run_shell_command - Running command... - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + Running command... + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg index 6a693d318b..bb05edfd84 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg @@ -1,8 +1,8 @@ - + - + @@ -17,16 +17,16 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - - - google_web_search - + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + google_web_search - Searching... - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + Searching... + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap index bdf1e95332..296b210124 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap @@ -7,11 +7,13 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho ▗▟▀ ▝▀ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ google_web_search │ │ │ │ Searching... │ -╰──────────────────────────────────────────────────────────────────────────────────────────────╯" +╰──────────────────────────────────────────────────────────────────────────────────────────────╯ +" `; exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for a shell tool 1`] = ` @@ -21,11 +23,13 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho ▗▟▀ ▝▀ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ run_shell_command │ │ │ │ Running command... │ -╰──────────────────────────────────────────────────────────────────────────────────────────────╯" +╰──────────────────────────────────────────────────────────────────────────────────────────────╯ +" `; exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for an empty slice following a search tool 1`] = ` @@ -35,9 +39,11 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho ▗▟▀ ▝▀ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ google_web_search │ │ │ │ Searching... │ -╰──────────────────────────────────────────────────────────────────────────────────────────────╯" +╰──────────────────────────────────────────────────────────────────────────────────────────────╯ +" `;