summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/shellCommandProcessor.ts
blob: 300f21fe6c936dc746857da8ccfe068c6e6adef3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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 };
};