From 5f5edb4c9bac24c4875ffc1a5a97ad8cf11f4436 Mon Sep 17 00:00:00 2001 From: Seth Troisi Date: Wed, 30 Apr 2025 00:26:07 +0000 Subject: Added bang(!) commands as a shell passthrough --- packages/cli/src/ui/hooks/shellCommandProcessor.ts | 93 ++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 packages/cli/src/ui/hooks/shellCommandProcessor.ts (limited to 'packages/cli/src/ui/hooks/shellCommandProcessor.ts') diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts new file mode 100644 index 00000000..300f21fe --- /dev/null +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { exec as _exec } from 'child_process'; +import { useCallback } from 'react'; +import { Config } from '@gemini-code/server'; +import { type PartListUnion } from '@google/genai'; +import { HistoryItem, StreamingState } from '../types.js'; +import { getCommandFromQuery } from '../utils/commandUtils.js'; + +// Helper function (consider moving to a shared util if used elsewhere) +const addHistoryItem = ( + setHistory: React.Dispatch>, + itemData: Omit, + id: number, +) => { + setHistory((prevHistory) => [ + ...prevHistory, + { ...itemData, id } as HistoryItem, + ]); +}; + +export const useShellCommandProcessor = ( + setHistory: React.Dispatch>, + setStreamingState: React.Dispatch>, + setDebugMessage: React.Dispatch>, + getNextMessageId: (baseTimestamp: number) => number, + config: Config, +) => { + const handleShellCommand = useCallback( + (rawQuery: PartListUnion): boolean => { + if (typeof rawQuery !== 'string') { + return false; // Passthrough only works with string commands + } + + const [symbol] = getCommandFromQuery(rawQuery); + if (symbol !== '!' && symbol !== '$') { + return false; + } + // Remove symbol from rawQuery + const trimmed = rawQuery.trim().slice(1); + + // Add user message *before* execution starts + const userMessageTimestamp = Date.now(); + addHistoryItem( + setHistory, + { type: 'user', text: rawQuery }, + userMessageTimestamp, + ); + + // Execute and capture output + const targetDir = config.getTargetDir(); + setDebugMessage(`Executing shell command in ${targetDir}: ${trimmed}`); + const execOptions = { + cwd: targetDir, + }; + + // Set state to Responding while the command runs + setStreamingState(StreamingState.Responding); + + _exec(trimmed, execOptions, (error, stdout, stderr) => { + const timestamp = getNextMessageId(userMessageTimestamp); // Use user message time as base + if (error) { + addHistoryItem( + setHistory, + { type: 'error', text: error.message }, + timestamp, + ); + } else if (stderr) { + // Treat stderr as info for passthrough, as some tools use it for non-error output + addHistoryItem(setHistory, { type: 'info', text: stderr }, timestamp); + } else { + // Add stdout as an info message + addHistoryItem( + setHistory, + { type: 'info', text: stdout || '(Command produced no output)' }, + timestamp, + ); + } + // Set state back to Idle *after* command finishes and output is added + setStreamingState(StreamingState.Idle); + }); + + return true; // Command was handled + }, + [config, setDebugMessage, setHistory, setStreamingState, getNextMessageId], + ); + + return { handleShellCommand }; +}; -- cgit v1.2.3