/** * @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 }; };