feat(cli): improve visibility of shell commands in chat history

- Render the high-level intent description in the tool header.
- Display the full colorized bash command inside the tool's content box.
- Pass tool arguments to the UI to enable detailed rendering of completed tools.
This commit is contained in:
Taylor Mullen
2026-03-17 16:20:33 -07:00
parent e1eefffcf1
commit 1b4301d498
8 changed files with 78 additions and 5 deletions
@@ -132,6 +132,15 @@ describe('<ShellToolMessage />', () => {
{ status: CoreToolCallStatus.Success },
undefined,
],
[
'renders with colorized command in body',
{
status: CoreToolCallStatus.Success,
args: { command: 'for i in {1..3}; do echo $i; done' },
description: 'Loop through numbers',
},
undefined,
],
[
'renders in Error state',
{ status: CoreToolCallStatus.Error, resultDisplay: 'Error output' },
@@ -5,7 +5,7 @@
*/
import React from 'react';
import { Box, type DOMElement } from 'ink';
import { Box, Text, type DOMElement } from 'ink';
import { ShellInputPrompt } from '../ShellInputPrompt.js';
import { StickyHeader } from '../StickyHeader.js';
import { useUIActions } from '../../contexts/UIActionsContext.js';
@@ -24,6 +24,9 @@ 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 { colorizeCode } from '../../utils/CodeColorizer.js';
import { useSettings } from '../../contexts/SettingsContext.js';
import { theme } from '../../semantic-colors.js';
import {
type Config,
ShellExecutionService,
@@ -45,6 +48,8 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
description,
args,
resultDisplay,
status,
@@ -77,6 +82,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
constrainHeight,
} = useUIState();
const isAlternateBuffer = useAlternateBuffer();
const settings = useSettings();
const isThisShellFocused = checkIsShellFocused(
name,
@@ -165,6 +171,8 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
resultDisplay,
);
const shellCommand = args?.['command'];
return (
<>
<StickyHeader
@@ -209,6 +217,18 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
paddingX={1}
flexDirection="column"
>
{typeof shellCommand === 'string' && (
<Box flexDirection="row" marginBottom={1}>
<Text color={theme.text.secondary}>$ </Text>
{colorizeCode({
code: shellCommand,
language: 'bash',
maxWidth: terminalWidth - 4, // account for padding and borders
settings,
hideLineNumbers: true,
})}
</Box>
)}
<ToolResultDisplay
resultDisplay={resultDisplay}
availableTerminalHeight={availableTerminalHeight}
@@ -44,6 +44,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
export const ToolMessage: React.FC<ToolMessageProps> = ({
name,
description,
args: _args,
resultDisplay,
status,
kind,
@@ -331,3 +331,13 @@ exports[`<ShellToolMessage /> > Snapshots > renders in Success state (history mo
│ Test result │
"
`;
exports[`<ShellToolMessage /> > Snapshots > renders with colorized command in body 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ Shell Command Loop through numbers │
│ │
│ $ for i in {1..3}; do echo $i; done │
│ │
│ Test result │
"
`;
+1
View File
@@ -51,6 +51,7 @@ export function mapToDisplay(
parentCallId: call.request.parentCallId,
name: displayName,
description,
args: call.request.args,
renderOutputAsMarkdown,
};
+1
View File
@@ -102,6 +102,7 @@ export interface IndividualToolCallDisplay {
parentCallId?: string;
name: string;
description: string;
args?: Record<string, unknown>;
resultDisplay: ToolResultDisplay | undefined;
status: CoreToolCallStatus;
// True when the tool was initiated directly by the user (slash/@/shell flows).
+27
View File
@@ -658,6 +658,33 @@ describe('ShellTool', () => {
expect(shellTool.description).toMatchSnapshot();
});
it('should return the intent description if provided', () => {
const invocation = shellTool.build({
command: 'ls',
description: 'Listing files',
});
expect(invocation.getDescription()).toBe('Listing files');
});
it('should return the intent description with background suffix if provided', () => {
const invocation = shellTool.build({
command: 'ls',
description: 'Listing files',
is_background: true,
});
expect(invocation.getDescription()).toBe('Listing files [background]');
});
it('should return command and directory if intent description is not provided', () => {
const invocation = shellTool.build({
command: 'ls',
dir_path: 'subdir',
});
const description = invocation.getDescription();
expect(description).toContain('ls');
expect(description).toContain('[in subdir]');
});
it('should not include efficiency guidelines when disabled', () => {
mockPlatform.mockReturnValue('linux');
vi.mocked(mockConfig.getEnableShellOutputEfficiency).mockReturnValue(
+8 -4
View File
@@ -73,6 +73,14 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
getDescription(): string {
if (this.params.description) {
let description = this.params.description.replace(/\n/g, ' ');
if (this.params.is_background) {
description += ' [background]';
}
return description;
}
let description = `${this.params.command}`;
// append optional [in directory]
// note description is needed even if validation fails due to absolute path
@@ -81,10 +89,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
} else {
description += ` [current working directory ${process.cwd()}]`;
}
// append optional (description), replacing any line breaks with spaces
if (this.params.description) {
description += ` (${this.params.description.replace(/\n/g, ' ')})`;
}
if (this.params.is_background) {
description += ' [background]';
}