diff options
| author | Jacob Richman <[email protected]> | 2025-07-18 17:30:28 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-07-19 00:30:28 +0000 |
| commit | f650be2c3ab5084958952ff14d2235ac14f777ea (patch) | |
| tree | f302c514918eaf5d48f07b9c1bd65c480dd7e8a6 /packages/cli/src/ui/hooks | |
| parent | 4dbd9f30b6df879661e968e493f817667954bfce (diff) | |
Make shell output consistent. (#4469)
Diffstat (limited to 'packages/cli/src/ui/hooks')
| -rw-r--r-- | packages/cli/src/ui/hooks/shellCommandProcessor.test.ts | 35 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/shellCommandProcessor.ts | 55 |
2 files changed, 72 insertions, 18 deletions
diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts index 4549f929..53dcb0d4 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts @@ -10,6 +10,7 @@ import { useShellCommandProcessor } from './shellCommandProcessor'; import { Config, GeminiClient } from '@google/gemini-cli-core'; import * as fs from 'fs'; import EventEmitter from 'events'; +import { ToolCallStatus } from '../types'; // Mock dependencies vi.mock('child_process'); @@ -104,8 +105,15 @@ describe('useShellCommandProcessor', () => { expect(addItemToHistoryMock).toHaveBeenCalledTimes(2); expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({ - type: 'info', - text: 'file1.txt\nfile2.txt', + type: 'tool_group', + tools: [ + expect.objectContaining({ + name: 'Shell Command', + description: 'ls -l', + status: ToolCallStatus.Success, + resultDisplay: 'file1.txt\nfile2.txt', + }), + ], }); expect(geminiClientMock.addHistory).toHaveBeenCalledTimes(1); }); @@ -140,8 +148,16 @@ describe('useShellCommandProcessor', () => { expect(addItemToHistoryMock).toHaveBeenCalledTimes(2); expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({ - type: 'info', - text: '[Command produced binary output, which is not shown.]', + type: 'tool_group', + tools: [ + expect.objectContaining({ + name: 'Shell Command', + description: 'cat myimage.png', + status: ToolCallStatus.Success, + resultDisplay: + '[Command produced binary output, which is not shown.]', + }), + ], }); }); @@ -172,8 +188,15 @@ describe('useShellCommandProcessor', () => { expect(addItemToHistoryMock).toHaveBeenCalledTimes(2); expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({ - type: 'error', - text: 'Command exited with code 127.\ncommand not found', + type: 'tool_group', + tools: [ + expect.objectContaining({ + name: 'Shell Command', + description: 'a-bad-command', + status: ToolCallStatus.Error, + resultDisplay: 'Command exited with code 127.\ncommand not found', + }), + ], }); }); }); diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index c6b89515..6f7aff2d 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -6,13 +6,18 @@ import { spawn } from 'child_process'; import { StringDecoder } from 'string_decoder'; -import type { HistoryItemWithoutId } from '../types.js'; +import { + HistoryItemWithoutId, + IndividualToolCallDisplay, + ToolCallStatus, +} from '../types.js'; import { useCallback } from 'react'; import { Config, GeminiClient } from '@google/gemini-cli-core'; import { type PartListUnion } from '@google/genai'; import { formatMemoryUsage } from '../utils/formatters.js'; import { isBinary } from '../utils/textUtils.js'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; +import { SHELL_COMMAND_NAME } from '../constants.js'; import crypto from 'crypto'; import path from 'path'; import os from 'os'; @@ -204,6 +209,7 @@ ${modelContent} * Hook to process shell commands. * Orchestrates command execution and updates history and agent context. */ + export const useShellCommandProcessor = ( addItemToHistory: UseHistoryManagerReturn['addItem'], setPendingHistoryItem: React.Dispatch< @@ -221,6 +227,7 @@ export const useShellCommandProcessor = ( } const userMessageTimestamp = Date.now(); + const callId = `shell-${userMessageTimestamp}`; addItemToHistory( { type: 'user_shell', text: rawQuery }, userMessageTimestamp, @@ -246,6 +253,20 @@ export const useShellCommandProcessor = ( const execPromise = new Promise<void>((resolve) => { let lastUpdateTime = 0; + const initialToolDisplay: IndividualToolCallDisplay = { + callId, + name: SHELL_COMMAND_NAME, + description: rawQuery, + status: ToolCallStatus.Executing, + resultDisplay: '', + confirmationDetails: undefined, + }; + + setPendingHistoryItem({ + type: 'tool_group', + tools: [initialToolDisplay], + }); + onDebugMessage(`Executing in ${targetDir}: ${commandToExecute}`); executeShellCommand( commandToExecute, @@ -254,23 +275,22 @@ export const useShellCommandProcessor = ( (streamedOutput) => { // Throttle pending UI updates to avoid excessive re-renders. if (Date.now() - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS) { - setPendingHistoryItem({ type: 'info', text: streamedOutput }); + setPendingHistoryItem({ + type: 'tool_group', + tools: [ + { ...initialToolDisplay, resultDisplay: streamedOutput }, + ], + }); lastUpdateTime = Date.now(); } }, onDebugMessage, ) .then((result) => { - // TODO(abhipatel12) - Consider updating pending item and using timeout to ensure - // there is no jump where intermediate output is skipped. setPendingHistoryItem(null); - let historyItemType: HistoryItemWithoutId['type'] = 'info'; let mainContent: string; - // The context sent to the model utilizes a text tokenizer which means raw binary data is - // cannot be parsed and understood and thus would only pollute the context window and waste - // tokens. if (isBinary(result.rawOutput)) { mainContent = '[Command produced binary output, which is not shown.]'; @@ -280,17 +300,19 @@ export const useShellCommandProcessor = ( } let finalOutput = mainContent; + let finalStatus = ToolCallStatus.Success; if (result.error) { - historyItemType = 'error'; + finalStatus = ToolCallStatus.Error; finalOutput = `${result.error.message}\n${finalOutput}`; } else if (result.aborted) { + finalStatus = ToolCallStatus.Canceled; finalOutput = `Command was cancelled.\n${finalOutput}`; } else if (result.signal) { - historyItemType = 'error'; + finalStatus = ToolCallStatus.Error; finalOutput = `Command terminated by signal: ${result.signal}.\n${finalOutput}`; } else if (result.exitCode !== 0) { - historyItemType = 'error'; + finalStatus = ToolCallStatus.Error; finalOutput = `Command exited with code ${result.exitCode}.\n${finalOutput}`; } @@ -302,9 +324,18 @@ export const useShellCommandProcessor = ( } } + const finalToolDisplay: IndividualToolCallDisplay = { + ...initialToolDisplay, + status: finalStatus, + resultDisplay: finalOutput, + }; + // Add the complete, contextual result to the local UI history. addItemToHistory( - { type: historyItemType, text: finalOutput }, + { + type: 'tool_group', + tools: [finalToolDisplay], + } as HistoryItemWithoutId, userMessageTimestamp, ); |
