diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx index 8971d488d3..d5cbdabe60 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx @@ -69,6 +69,11 @@ describe('', () => { ui: { errorVerbosity: 'full' }, }, }); + const lowVerbositySettings = createMockSettings({ + merged: { + ui: { errorVerbosity: 'low' }, + }, + }); describe('Golden Snapshots', () => { it('renders single successful tool call', async () => { @@ -721,6 +726,245 @@ describe('', () => { expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); + + it('does not render a bottom-border fragment when all tools are filtered out', async () => { + const toolCalls = [ + createToolCall({ + callId: 'hidden-error-tool', + name: 'error-tool', + status: CoreToolCallStatus.Error, + resultDisplay: 'Hidden in low verbosity', + isClientInitiated: false, + }), + ]; + const item = createItem(toolCalls); + + const { lastFrame, unmount, waitUntilReady } = renderWithProviders( + , + { + config: baseMockConfig, + settings: lowVerbositySettings, + }, + ); + + await waitUntilReady(); + expect(lastFrame({ allowEmpty: true })).toBe(''); + unmount(); + }); + + it('still renders explicit closing slices for split static/pending groups', async () => { + const toolCalls: IndividualToolCallDisplay[] = []; + const item = createItem(toolCalls); + + const { lastFrame, unmount, waitUntilReady } = renderWithProviders( + , + { + config: baseMockConfig, + settings: fullVerbositySettings, + }, + ); + + await waitUntilReady(); + expect(lastFrame({ allowEmpty: true })).not.toBe(''); + unmount(); + }); + + it('does not render a border fragment when plan-mode tools are filtered out', async () => { + const toolCalls = [ + createToolCall({ + callId: 'plan-write', + name: WRITE_FILE_DISPLAY_NAME, + approvalMode: ApprovalMode.PLAN, + status: CoreToolCallStatus.Success, + resultDisplay: 'Plan file written', + }), + ]; + const item = createItem(toolCalls); + + const { lastFrame, unmount, waitUntilReady } = renderWithProviders( + , + { + config: baseMockConfig, + settings: fullVerbositySettings, + }, + ); + + await waitUntilReady(); + expect(lastFrame({ allowEmpty: true })).toBe(''); + unmount(); + }); + + it('does not render a border fragment when only confirming tools are present', async () => { + const toolCalls = [ + createToolCall({ + callId: 'confirm-only', + status: CoreToolCallStatus.AwaitingApproval, + confirmationDetails: { + type: 'info', + title: 'Confirm', + prompt: 'Proceed?', + }, + }), + ]; + const item = createItem(toolCalls); + + const { lastFrame, unmount, waitUntilReady } = renderWithProviders( + , + { + config: baseMockConfig, + settings: fullVerbositySettings, + }, + ); + + await waitUntilReady(); + expect(lastFrame({ allowEmpty: true })).toBe(''); + unmount(); + }); + + it('does not leave a border stub when transitioning from visible to fully filtered tools', async () => { + const visibleTools = [ + createToolCall({ + callId: 'visible-success', + name: 'visible-tool', + status: CoreToolCallStatus.Success, + resultDisplay: 'visible output', + }), + ]; + const hiddenTools = [ + createToolCall({ + callId: 'hidden-error', + name: 'hidden-error-tool', + status: CoreToolCallStatus.Error, + resultDisplay: 'hidden output', + isClientInitiated: false, + }), + ]; + + const initialItem = createItem(visibleTools); + const hiddenItem = createItem(hiddenTools); + + const firstRender = renderWithProviders( + , + { + config: baseMockConfig, + settings: lowVerbositySettings, + }, + ); + await firstRender.waitUntilReady(); + expect(firstRender.lastFrame()).toContain('visible-tool'); + firstRender.unmount(); + + const secondRender = renderWithProviders( + , + { + config: baseMockConfig, + settings: lowVerbositySettings, + }, + ); + await secondRender.waitUntilReady(); + expect(secondRender.lastFrame({ allowEmpty: true })).toBe(''); + secondRender.unmount(); + }); + + it('keeps visible tools rendered with many filtered tools (stress case)', async () => { + const visibleTool = createToolCall({ + callId: 'visible-tool', + name: 'visible-tool', + status: CoreToolCallStatus.Success, + resultDisplay: 'visible output', + }); + const hiddenTools = Array.from({ length: 50 }, (_, index) => + createToolCall({ + callId: `hidden-${index}`, + name: `hidden-error-${index}`, + status: CoreToolCallStatus.Error, + resultDisplay: `hidden output ${index}`, + isClientInitiated: false, + }), + ); + const toolCalls = [visibleTool, ...hiddenTools]; + const item = createItem(toolCalls); + + const { lastFrame, unmount, waitUntilReady } = renderWithProviders( + , + { + config: baseMockConfig, + settings: lowVerbositySettings, + }, + ); + + await waitUntilReady(); + const output = lastFrame(); + expect(output).toContain('visible-tool'); + expect(output).not.toContain('hidden-error-0'); + expect(output).not.toContain('hidden-error-49'); + unmount(); + }); + + it('renders explicit closing slice even at very narrow terminal width', async () => { + const toolCalls: IndividualToolCallDisplay[] = []; + const item = createItem(toolCalls); + + const { lastFrame, unmount, waitUntilReady } = renderWithProviders( + , + { + config: baseMockConfig, + settings: fullVerbositySettings, + }, + ); + + await waitUntilReady(); + expect(lastFrame({ allowEmpty: true })).not.toBe(''); + unmount(); + }); }); describe('Plan Mode Filtering', () => { diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 05f9984d69..01cec31727 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -141,11 +141,15 @@ export const ToolGroupMessage: React.FC = ({ const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN; - // If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools), - // only render if we need to close a border from previous - // tool groups. borderBottomOverride=true means we must render the closing border; - // undefined or false means there's nothing to display. - if (visibleToolCalls.length === 0 && borderBottomOverride !== true) { + // If all tools are filtered out (e.g., in-progress AskUser tools, low-verbosity + // internal errors, plan-mode hidden write/edit), we should not emit standalone + // border fragments. The only case where an empty group should render is the + // explicit "closing slice" (tools: []) used to bridge static/pending sections. + const isExplicitClosingSlice = allToolCalls.length === 0; + if ( + visibleToolCalls.length === 0 && + (!isExplicitClosingSlice || borderBottomOverride !== true) + ) { return null; }