From 071e2923bb80f4b06a0361eae3221db6c6c2ea56 Mon Sep 17 00:00:00 2001 From: Dev Randalpura Date: Thu, 30 Apr 2026 12:01:47 -0500 Subject: [PATCH] fix(ui): made shell tool header wrap on Ctrl+O (#26229) --- .../messages/ShellToolMessage.test.tsx | 61 +++++++++++++++++++ .../components/messages/ShellToolMessage.tsx | 9 +++ .../ui/components/messages/ToolMessage.tsx | 9 +++ .../components/messages/ToolShared.test.tsx | 37 ++++++++++- .../src/ui/components/messages/ToolShared.tsx | 14 ++++- .../ToolGroupMessage.test.tsx.snap | 9 ++- .../ToolMessageFocusHint.test.tsx.snap | 4 +- 7 files changed, 136 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx index 676051501c..09906495dd 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx @@ -356,4 +356,65 @@ describe('', () => { unmount(); }); }); + + describe('Header Expansion', () => { + const LONG_DESCRIPTION = 'very long '.repeat(20); + + it('truncates header by default', async () => { + const { lastFrame, waitUntilReady } = await renderWithProviders( + , + { uiActions }, + ); + + await waitUntilReady(); + const output = lastFrame(); + // Should be a single line header + expect(output.split('\n')[1]).toContain(SHELL_COMMAND_NAME); // name + // We check if it's truncated. In our ToolInfo, it's height 1. + // The StickyHeader adds some structure, but the ToolInfo Box is inside. + }); + + it('expands header when availableTerminalHeight is undefined', async () => { + const { lastFrame, waitUntilReady } = await renderWithProviders( + , + { uiActions }, + ); + + await waitUntilReady(); + const output = lastFrame(); + // When expanded, the header (ToolInfo) should wrap and take multiple lines. + // Since it's at the top, we check if the first few lines contain parts of the description. + const lines = output.split('\n'); + expect(lines.length).toBeGreaterThan(5); + }); + + it('expands header when isExpanded is true in context', async () => { + const { lastFrame, waitUntilReady } = await renderWithProviders( + , + { + uiActions, + toolActions: { + isExpanded: (id: string) => id === baseProps.callId, + }, + }, + ); + + await waitUntilReady(); + const output = lastFrame(); + // Should be expanded due to context + expect(output.split('\n').length).toBeGreaterThan(5); + }); + }); }); diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx index f3694f3490..db950a6e51 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx @@ -24,6 +24,7 @@ import type { ToolMessageProps } from './ToolMessage.js'; import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js'; import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js'; import { useUIState } from '../../contexts/UIStateContext.js'; +import { useToolActions } from '../../contexts/ToolActionsContext.js'; import { type Config, ShellExecutionService, @@ -41,6 +42,7 @@ export interface ShellToolMessageProps extends ToolMessageProps { } export const ShellToolMessage: React.FC = ({ + callId, name, description, resultDisplay, @@ -57,6 +59,12 @@ export const ShellToolMessage: React.FC = ({ isExpandable, originalRequestName, }) => { + const { isExpanded: isExpandedInContext } = useToolActions(); + + const isExpanded = + (isExpandedInContext ? isExpandedInContext(callId) : false) || + availableTerminalHeight === undefined; + const { activePtyId: activeShellPtyId, embeddedShellFocused, @@ -169,6 +177,7 @@ export const ShellToolMessage: React.FC = ({ description={description} emphasis={emphasis} originalRequestName={originalRequestName} + isExpanded={isExpanded} /> = ({ + callId, name, description, resultDisplay, @@ -63,6 +65,12 @@ export const ToolMessage: React.FC = ({ progress, progressTotal, }) => { + const { isExpanded: isExpandedInContext } = useToolActions(); + + const isExpanded = + (isExpandedInContext ? isExpandedInContext(callId) : false) || + availableTerminalHeight === undefined; + const isThisShellFocused = checkIsShellFocused( name, status, @@ -102,6 +110,7 @@ export const ToolMessage: React.FC = ({ emphasis={emphasis} progressMessage={progressMessage} originalRequestName={originalRequestName} + isExpanded={isExpanded} /> ({ GeminiRespondingSpinner: () => MockSpinner, @@ -65,3 +66,37 @@ describe('McpProgressIndicator', () => { expect(output).not.toContain('150%'); }); }); + +describe('ToolInfo', () => { + const longDescription = 'long '.repeat(50); + + it('truncates description by default', async () => { + const { lastFrame } = await render( + , + ); + const output = lastFrame(); + // In Ink, a single line Box with wrap="truncate" will be truncated. + // Since we don't know the exact terminal width in this test, we check if it is short. + expect(output.trim().split('\n').length).toBe(1); + }); + + it('wraps description when isExpanded is true', async () => { + const { lastFrame } = await render( + , + ); + const output = lastFrame(); + // When expanded, it should wrap into multiple lines. + expect(output.trim().split('\n').length).toBeGreaterThan(1); + }); +}); diff --git a/packages/cli/src/ui/components/messages/ToolShared.tsx b/packages/cli/src/ui/components/messages/ToolShared.tsx index 2aa5ed992a..b246db308b 100644 --- a/packages/cli/src/ui/components/messages/ToolShared.tsx +++ b/packages/cli/src/ui/components/messages/ToolShared.tsx @@ -194,6 +194,7 @@ type ToolInfoProps = { emphasis: TextEmphasis; progressMessage?: string; originalRequestName?: string; + isExpanded?: boolean; }; export const ToolInfo: React.FC = ({ @@ -203,6 +204,7 @@ export const ToolInfo: React.FC = ({ emphasis, progressMessage: _progressMessage, originalRequestName, + isExpanded = false, }) => { const status = mapCoreStatusToDisplayStatus(coreStatus); const nameColor = React.useMemo(() => { @@ -224,8 +226,16 @@ export const ToolInfo: React.FC = ({ const isCompletedAskUser = isCompletedAskUserTool(name, status); return ( - - + + {name} 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 f61b9274c9..b0d33feebd 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 @@ -62,9 +62,9 @@ exports[` > Golden Snapshots > renders empty tool calls arra exports[` > Golden Snapshots > renders header when scrolled 1`] = ` "╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool-1 Description 1. This is a long description that will need to b… │ ▄ +│ ✓ tool-1 Description 1. This is a long description that will need to be │ +│ truncated if the terminal width is small. │ ▄ │──────────────────────────────────────────────────────────────────────────│ █ -│ line3 │ █ │ line4 │ █ │ line5 │ █ │ │ █ @@ -161,7 +161,10 @@ exports[` > Golden Snapshots > renders with limited terminal exports[` > Golden Snapshots > renders with narrow terminal width 1`] = ` "╭──────────────────────────────────╮ -│ ✓ very-long-tool-name-that-mig… │ +│ ✓ very-long-tool-name-that-migh │ +│ t-wrap This is a very long │ +│ description that might cause │ +│ wrapping issues │ │ │ │ Test result │ ╰──────────────────────────────────╯ diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap index 8da15d7fdb..22904f2b29 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap @@ -58,7 +58,9 @@ exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output > exports[`Focus Hint > handles long descriptions by shrinking them to show the focus hint > long-description 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ ⊶ Shell Command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… (Tab to focus) │ +│ ⊶ Shell Command (Tab to focus) │ +│ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA │ +│ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA │ │ │ " `;