summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/passthroughCommandProcessor.ts
blob: 97244e8c32d01e4277d35c46355caf605403c550 (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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
 * @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 usePassthroughProcessor = (
  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 handlePassthroughCommand = useCallback(
    (rawQuery: PartListUnion): boolean => {
      if (typeof rawQuery !== 'string') {
        return false; // Passthrough only works with string commands
      }

      const trimmedQuery = rawQuery.trim();
      if (!trimmedQuery) {
        return false;
      }

      const [symbol, command] = getCommandFromQuery(trimmedQuery);

      // Passthrough commands don't start with symbol
      if (symbol !== undefined) {
        return false;
      }

      if (config.getPassthroughCommands().includes(command)) {
        // Add user message *before* execution starts
        const userMessageTimestamp = Date.now();
        addHistoryItem(
          setHistory,
          { type: 'user', text: trimmedQuery },
          userMessageTimestamp,
        );

        // Execute and capture output
        const targetDir = config.getTargetDir();
        setDebugMessage(
          `Executing pass through command in ${targetDir}: ${trimmedQuery}`,
        );
        const execOptions = {
          cwd: targetDir,
        };

        // Set state to Responding while the command runs
        setStreamingState(StreamingState.Responding);

        _exec(trimmedQuery, 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
      }

      return false; // Not a passthrough command
    },
    [config, setDebugMessage, setHistory, setStreamingState, getNextMessageId],
  );

  return { handlePassthroughCommand };
};