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 │
│ │
"
`;