diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx index b650ee4d9d..9e9255d3c4 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx @@ -132,6 +132,15 @@ describe('', () => { { 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' }, diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx index f34aa08bfb..3c5a09f402 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx @@ -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 = ({ description, + args, + resultDisplay, status, @@ -77,6 +82,7 @@ export const ShellToolMessage: React.FC = ({ constrainHeight, } = useUIState(); const isAlternateBuffer = useAlternateBuffer(); + const settings = useSettings(); const isThisShellFocused = checkIsShellFocused( name, @@ -165,6 +171,8 @@ export const ShellToolMessage: React.FC = ({ resultDisplay, ); + const shellCommand = args?.['command']; + return ( <> = ({ paddingX={1} flexDirection="column" > + {typeof shellCommand === 'string' && ( + + $ + {colorizeCode({ + code: shellCommand, + language: 'bash', + maxWidth: terminalWidth - 4, // account for padding and borders + settings, + hideLineNumbers: true, + })} + + )} = ({ name, description, + args: _args, resultDisplay, status, kind, diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap index 1847b8ce67..d1b0608df8 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap @@ -331,3 +331,13 @@ exports[` > Snapshots > renders in Success state (history mo │ Test result │ " `; + +exports[` > Snapshots > renders with colorized command in body 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ Shell Command Loop through numbers │ +│ │ +│ $ for i in {1..3}; do echo $i; done │ +│ │ +│ Test result │ +" +`; diff --git a/packages/cli/src/ui/hooks/toolMapping.ts b/packages/cli/src/ui/hooks/toolMapping.ts index e06ebf5bb5..01fef7f95a 100644 --- a/packages/cli/src/ui/hooks/toolMapping.ts +++ b/packages/cli/src/ui/hooks/toolMapping.ts @@ -51,6 +51,7 @@ export function mapToDisplay( parentCallId: call.request.parentCallId, name: displayName, description, + args: call.request.args, renderOutputAsMarkdown, }; diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 2f8e414a83..f86fedf554 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -102,6 +102,7 @@ export interface IndividualToolCallDisplay { parentCallId?: string; name: string; description: string; + args?: Record; resultDisplay: ToolResultDisplay | undefined; status: CoreToolCallStatus; // True when the tool was initiated directly by the user (slash/@/shell flows). diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index ace59cd7cf..7f4dd28029 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -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( diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 8917d281bd..242bb4c425 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -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]'; }