/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useCallback, useMemo } from 'react'; import { type PartListUnion } from '@google/genai'; import open from 'open'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; import { Config } from '@gemini-code/server'; import { Message, MessageType, HistoryItemWithoutId } from '../types.js'; import { createShowMemoryAction } from './useShowMemoryCommand.js'; import { addMemoryEntry } from '../../config/memoryUtils.js'; export interface SlashCommand { name: string; altName?: string; description?: string; action: (mainCommand: string, subCommand?: string, args?: string) => void; } /** * Hook to define and process slash commands (e.g., /help, /clear). */ export const useSlashCommandProcessor = ( config: Config | null, addItem: UseHistoryManagerReturn['addItem'], clearItems: UseHistoryManagerReturn['clearItems'], refreshStatic: () => void, setShowHelp: React.Dispatch>, onDebugMessage: (message: string) => void, openThemeDialog: () => void, performMemoryRefresh: () => Promise, toggleCorgiMode: () => void, cliVersion: string, ) => { const addMessage = useCallback( (message: Message) => { // Convert Message to HistoryItemWithoutId const historyItemContent: HistoryItemWithoutId = { type: message.type, // MessageType enum should be compatible with HistoryItemWithoutId string literal types text: message.content, }; addItem(historyItemContent, message.timestamp.getTime()); }, [addItem], ); const showMemoryAction = useCallback(async () => { const actionFn = createShowMemoryAction(config, addMessage); await actionFn(); }, [config, addMessage]); const addMemoryAction = useCallback( async (_mainCommand: string, _subCommand?: string, args?: string) => { if (!args || args.trim() === '') { addMessage({ type: MessageType.ERROR, content: 'Usage: /memory add ', timestamp: new Date(), }); return; } try { await addMemoryEntry(args); addMessage({ type: MessageType.INFO, content: `Successfully added to memory: "${args}"`, timestamp: new Date(), }); await performMemoryRefresh(); // Refresh memory to reflect changes } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); addMessage({ type: MessageType.ERROR, content: `Failed to add memory: ${errorMessage}`, timestamp: new Date(), }); } }, [addMessage, performMemoryRefresh], ); const slashCommands: SlashCommand[] = useMemo( () => [ { name: 'help', altName: '?', description: 'for help on gemini-code', action: (_mainCommand, _subCommand, _args) => { onDebugMessage('Opening help.'); setShowHelp(true); }, }, { name: 'clear', description: 'clear the screen', action: (_mainCommand, _subCommand, _args) => { onDebugMessage('Clearing terminal.'); clearItems(); console.clear(); refreshStatic(); }, }, { name: 'theme', description: 'change the theme', action: (_mainCommand, _subCommand, _args) => { openThemeDialog(); }, }, { name: 'memory', description: 'Manage memory. Usage: /memory [text for add]', action: (mainCommand, subCommand, args) => { switch (subCommand) { case 'show': showMemoryAction(); break; case 'refresh': performMemoryRefresh(); break; case 'add': addMemoryAction(mainCommand, subCommand, args); break; default: addMessage({ type: MessageType.ERROR, content: `Unknown /memory command: ${subCommand}. Available: show, refresh, add`, timestamp: new Date(), }); } }, }, { name: 'corgi', action: (_mainCommand, _subCommand, _args) => { toggleCorgiMode(); }, }, { name: 'bug', description: 'Submit a bug report.', action: (_mainCommand, _subCommand, args) => { let bugDescription = _subCommand || ''; if (args) { bugDescription += ` ${args}`; } bugDescription = bugDescription.trim(); const osVersion = `${process.platform} ${process.version}`; let sandboxEnv = 'no sandbox'; if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { sandboxEnv = process.env.SANDBOX.replace(/^gemini-(?:code-)?/, ''); } else if (process.env.SANDBOX === 'sandbox-exec') { sandboxEnv = `sandbox-exec (${process.env.SEATBELT_PROFILE || 'unknown'})`; } const modelVersion = config?.getModel() || 'Unknown'; const diagnosticInfo = ` ## Describe the bug A clear and concise description of what the bug is. ## Additional context Add any other context about the problem here. ## Diagnostic Information * **CLI Version:** ${cliVersion} * **Operating System:** ${osVersion} * **Sandbox Environment:** ${sandboxEnv} * **Model Version:** ${modelVersion} `; let bugReportUrl = 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md'; if (bugDescription) { const encodedArgs = encodeURIComponent(bugDescription); bugReportUrl += `&title=${encodedArgs}`; } const encodedBody = encodeURIComponent(diagnosticInfo); bugReportUrl += `&body=${encodedBody}`; addMessage({ type: MessageType.INFO, content: `To submit your bug report, please open the following URL in your browser:\n${bugReportUrl}`, timestamp: new Date(), }); // Open the URL in the default browser (async () => { try { await open(bugReportUrl); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); addMessage({ type: MessageType.ERROR, content: `Could not open URL in browser: ${errorMessage}`, timestamp: new Date(), }); } })(); }, }, { name: 'quit', altName: 'exit', description: 'exit the cli', action: (_mainCommand, _subCommand, _args) => { onDebugMessage('Quitting. Good-bye.'); process.exit(0); }, }, ], [ onDebugMessage, setShowHelp, refreshStatic, openThemeDialog, clearItems, performMemoryRefresh, showMemoryAction, addMemoryAction, addMessage, toggleCorgiMode, config, // Added config to dependency array cliVersion, ], ); const handleSlashCommand = useCallback( (rawQuery: PartListUnion): boolean => { if (typeof rawQuery !== 'string') { return false; } const trimmed = rawQuery.trim(); if (!trimmed.startsWith('/') && !trimmed.startsWith('?')) { return false; } const userMessageTimestamp = Date.now(); addItem({ type: MessageType.USER, text: trimmed }, userMessageTimestamp); let subCommand: string | undefined; let args: string | undefined; const commandToMatch = (() => { if (trimmed.startsWith('?')) { return 'help'; // No subCommand or args for '?' acting as help } // For other slash commands like /memory add foo const parts = trimmed.substring(1).trim().split(/\s+/); if (parts.length > 1) { subCommand = parts[1]; } if (parts.length > 2) { args = parts.slice(2).join(' '); } return parts[0]; // This is the main command name })(); const mainCommand = commandToMatch; for (const cmd of slashCommands) { if (mainCommand === cmd.name || mainCommand === cmd.altName) { cmd.action(mainCommand, subCommand, args); return true; } } addMessage({ type: MessageType.ERROR, content: `Unknown command: ${trimmed}`, timestamp: new Date(), }); return true; }, [addItem, slashCommands, addMessage], ); return { handleSlashCommand, slashCommands }; };