summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/shellCommandProcessor.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/hooks/shellCommandProcessor.ts')
-rw-r--r--packages/cli/src/ui/hooks/shellCommandProcessor.ts93
1 files changed, 93 insertions, 0 deletions
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<React.SetStateAction<HistoryItem[]>>,
+ itemData: Omit<HistoryItem, 'id'>,
+ id: number,
+) => {
+ setHistory((prevHistory) => [
+ ...prevHistory,
+ { ...itemData, id } as HistoryItem,
+ ]);
+};
+
+export const useShellCommandProcessor = (
+ setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
+ setStreamingState: React.Dispatch<React.SetStateAction<StreamingState>>,
+ setDebugMessage: React.Dispatch<React.SetStateAction<string>>,
+ 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 };
+};