summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/slashCommandProcessor.ts
blob: aa7323ca891aa4df9c5a3e6a2cc1e7f6937d0cd9 (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
109
110
111
112
113
114
115
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { useCallback, useMemo } from 'react';
import { type PartListUnion } from '@google/genai';
import { getCommandFromQuery } from '../utils/commandUtils.js';
import { UseHistoryManagerReturn } from './useHistoryManager.js';

export interface SlashCommand {
  name: string;
  altName?: string;
  description: string;
  action: (value: PartListUnion) => void;
}

/**
 * Hook to define and process slash commands (e.g., /help, /clear).
 */
export const useSlashCommandProcessor = (
  addItem: UseHistoryManagerReturn['addItem'],
  clearItems: UseHistoryManagerReturn['clearItems'],
  refreshStatic: () => void,
  setShowHelp: React.Dispatch<React.SetStateAction<boolean>>,
  onDebugMessage: (message: string) => void,
  openThemeDialog: () => void,
) => {
  const slashCommands: SlashCommand[] = useMemo(
    () => [
      {
        name: 'help',
        altName: '?',
        description: 'for help on gemini-code',
        action: (_value: PartListUnion) => {
          onDebugMessage('Opening help.');
          setShowHelp(true);
        },
      },
      {
        name: 'clear',
        description: 'clear the screen',
        action: (_value: PartListUnion) => {
          onDebugMessage('Clearing terminal.');
          clearItems();
          refreshStatic();
        },
      },
      {
        name: 'theme',
        description: 'change the theme',
        action: (_value: PartListUnion) => {
          openThemeDialog();
        },
      },
      {
        name: 'quit',
        altName: 'exit',
        description: '',
        action: (_value: PartListUnion) => {
          onDebugMessage('Quitting. Good-bye.');
          process.exit(0);
        },
      },
    ],
    [onDebugMessage, setShowHelp, refreshStatic, openThemeDialog, clearItems],
  );

  /**
   * Checks if the query is a slash command and executes it if found.
   * Adds user query and potential error messages to history.
   * @returns True if the query was handled as a slash command (valid or invalid),
   *          false otherwise.
   */
  const handleSlashCommand = useCallback(
    (rawQuery: PartListUnion): boolean => {
      if (typeof rawQuery !== 'string') {
        return false;
      }

      const trimmed = rawQuery.trim();
      const [symbol, test] = getCommandFromQuery(trimmed);

      if (symbol !== '/' && symbol !== '?') {
        return false;
      }

      const userMessageTimestamp = Date.now();
      addItem({ type: 'user', text: trimmed }, userMessageTimestamp);

      for (const cmd of slashCommands) {
        if (
          test === cmd.name ||
          test === cmd.altName ||
          symbol === cmd.altName
        ) {
          cmd.action(trimmed);
          return true;
        }
      }

      // Unknown command: Add error message
      addItem(
        { type: 'error', text: `Unknown command: ${trimmed}` },
        userMessageTimestamp, // Use same base timestamp for related error
      );

      return true; // Indicate command was processed (even though invalid)
    },
    [addItem, slashCommands],
  );

  return { handleSlashCommand, slashCommands };
};