Download packages/cli/src/ui/hooks/slashCommandProcessor.ts from Jimmi42/openCLI: direct link, hf CLI and curl.
- Browser
- Download file 37.7 kB
-
https://huggingface.co/Jimmi42/openCLI/resolve/main/packages/cli/src/ui/hooks/slashCommandProcessor.ts
- Command line
-
hf download hf://Jimmi42/openCLI/packages/cli/src/ui/hooks/slashCommandProcessor.ts
-
curl -L -o slashCommandProcessor.ts https://huggingface.co/Jimmi42/openCLI/resolve/main/packages/cli/src/ui/hooks/slashCommandProcessor.ts
37.7 kB
| /** | |
| * @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 process from 'node:process'; | |
| import { UseHistoryManagerReturn } from './useHistoryManager.js'; | |
| import { useStateAndRef } from './useStateAndRef.js'; | |
| import { | |
| Config, | |
| GitService, | |
| Logger, | |
| MCPDiscoveryState, | |
| MCPServerStatus, | |
| getMCPDiscoveryState, | |
| getMCPServerStatus, | |
| } from '@google/gemini-cli-core'; | |
| import { useSessionStats } from '../contexts/SessionContext.js'; | |
| import { | |
| Message, | |
| MessageType, | |
| HistoryItemWithoutId, | |
| HistoryItem, | |
| } from '../types.js'; | |
| import { promises as fs } from 'fs'; | |
| import path from 'path'; | |
| import { createShowMemoryAction } from './useShowMemoryCommand.js'; | |
| import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; | |
| import { formatDuration, formatMemoryUsage } from '../utils/formatters.js'; | |
| import { getCliVersion } from '../../utils/version.js'; | |
| import { LoadedSettings } from '../../config/settings.js'; | |
| export interface SlashCommandActionReturn { | |
| shouldScheduleTool?: boolean; | |
| toolName?: string; | |
| toolArgs?: Record<string, unknown>; | |
| message?: string; // For simple messages or errors | |
| } | |
| export interface SlashCommand { | |
| name: string; | |
| altName?: string; | |
| description?: string; | |
| completion?: () => Promise<string[]>; | |
| action: ( | |
| mainCommand: string, | |
| subCommand?: string, | |
| args?: string, | |
| ) => | |
| | void | |
| | SlashCommandActionReturn | |
| | Promise<void | SlashCommandActionReturn>; // Action can now return this object | |
| } | |
| /** | |
| * Hook to define and process slash commands (e.g., /help, /clear). | |
| */ | |
| export const useSlashCommandProcessor = ( | |
| config: Config | null, | |
| settings: LoadedSettings, | |
| history: HistoryItem[], | |
| addItem: UseHistoryManagerReturn['addItem'], | |
| clearItems: UseHistoryManagerReturn['clearItems'], | |
| loadHistory: UseHistoryManagerReturn['loadHistory'], | |
| refreshStatic: () => void, | |
| setShowHelp: React.Dispatch<React.SetStateAction<boolean>>, | |
| onDebugMessage: (message: string) => void, | |
| openThemeDialog: () => void, | |
| openAuthDialog: () => void, | |
| openEditorDialog: () => void, | |
| performMemoryRefresh: () => Promise<void>, | |
| toggleCorgiMode: () => void, | |
| showToolDescriptions: boolean = false, | |
| setQuittingMessages: (message: HistoryItem[]) => void, | |
| openPrivacyNotice: () => void, | |
| ) => { | |
| const session = useSessionStats(); | |
| const gitService = useMemo(() => { | |
| if (!config?.getProjectRoot()) { | |
| return; | |
| } | |
| return new GitService(config.getProjectRoot()); | |
| }, [config]); | |
| const pendingHistoryItems: HistoryItemWithoutId[] = []; | |
| const [pendingCompressionItemRef, setPendingCompressionItem] = | |
| useStateAndRef<HistoryItemWithoutId | null>(null); | |
| if (pendingCompressionItemRef.current != null) { | |
| pendingHistoryItems.push(pendingCompressionItemRef.current); | |
| } | |
| const addMessage = useCallback( | |
| (message: Message) => { | |
| // Convert Message to HistoryItemWithoutId | |
| let historyItemContent: HistoryItemWithoutId; | |
| if (message.type === MessageType.ABOUT) { | |
| historyItemContent = { | |
| type: 'about', | |
| cliVersion: message.cliVersion, | |
| osVersion: message.osVersion, | |
| sandboxEnv: message.sandboxEnv, | |
| modelVersion: message.modelVersion, | |
| selectedAuthType: message.selectedAuthType, | |
| gcpProject: message.gcpProject, | |
| }; | |
| } else if (message.type === MessageType.STATS) { | |
| historyItemContent = { | |
| type: 'stats', | |
| stats: message.stats, | |
| lastTurnStats: message.lastTurnStats, | |
| duration: message.duration, | |
| }; | |
| } else if (message.type === MessageType.QUIT) { | |
| historyItemContent = { | |
| type: 'quit', | |
| stats: message.stats, | |
| duration: message.duration, | |
| }; | |
| } else if (message.type === MessageType.COMPRESSION) { | |
| historyItemContent = { | |
| type: 'compression', | |
| compression: message.compression, | |
| }; | |
| } else { | |
| historyItemContent = { | |
| type: message.type as | |
| | MessageType.INFO | |
| | MessageType.ERROR | |
| | MessageType.USER, | |
| text: message.content, | |
| }; | |
| } | |
| addItem(historyItemContent, message.timestamp.getTime()); | |
| }, | |
| [addItem], | |
| ); | |
| const showMemoryAction = useCallback(async () => { | |
| const actionFn = createShowMemoryAction(config, settings, addMessage); | |
| await actionFn(); | |
| }, [config, settings, addMessage]); | |
| const addMemoryAction = useCallback( | |
| ( | |
| _mainCommand: string, | |
| _subCommand?: string, | |
| args?: string, | |
| ): SlashCommandActionReturn | void => { | |
| if (!args || args.trim() === '') { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: 'Usage: /memory add <text to remember>', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| // UI feedback for attempting to schedule | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `Attempting to save to memory: "${args.trim()}"`, | |
| timestamp: new Date(), | |
| }); | |
| // Return info for scheduling the tool call | |
| return { | |
| shouldScheduleTool: true, | |
| toolName: 'save_memory', | |
| toolArgs: { fact: args.trim() }, | |
| }; | |
| }, | |
| [addMessage], | |
| ); | |
| const savedChatTags = useCallback(async () => { | |
| const geminiDir = config?.getProjectTempDir(); | |
| if (!geminiDir) { | |
| return []; | |
| } | |
| try { | |
| const files = await fs.readdir(geminiDir); | |
| return files | |
| .filter( | |
| (file) => file.startsWith('checkpoint-') && file.endsWith('.json'), | |
| ) | |
| .map((file) => file.replace('checkpoint-', '').replace('.json', '')); | |
| } catch (_err) { | |
| return []; | |
| } | |
| }, [config]); | |
| const slashCommands: SlashCommand[] = useMemo(() => { | |
| const commands: SlashCommand[] = [ | |
| { | |
| name: 'help', | |
| altName: '?', | |
| description: 'for help on gemini-cli', | |
| action: (_mainCommand, _subCommand, _args) => { | |
| onDebugMessage('Opening help.'); | |
| setShowHelp(true); | |
| }, | |
| }, | |
| { | |
| name: 'docs', | |
| description: 'open full Gemini CLI documentation in your browser', | |
| action: async (_mainCommand, _subCommand, _args) => { | |
| const docsUrl = 'https://goo.gle/gemini-cli-docs'; | |
| if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`, | |
| timestamp: new Date(), | |
| }); | |
| } else { | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `Opening documentation in your browser: ${docsUrl}`, | |
| timestamp: new Date(), | |
| }); | |
| await open(docsUrl); | |
| } | |
| }, | |
| }, | |
| { | |
| name: 'clear', | |
| description: 'clear the screen and conversation history', | |
| action: async (_mainCommand, _subCommand, _args) => { | |
| onDebugMessage('Clearing terminal and resetting chat.'); | |
| clearItems(); | |
| await config?.getGeminiClient()?.resetChat(); | |
| console.clear(); | |
| refreshStatic(); | |
| }, | |
| }, | |
| { | |
| name: 'theme', | |
| description: 'change the theme', | |
| action: (_mainCommand, _subCommand, _args) => { | |
| openThemeDialog(); | |
| }, | |
| }, | |
| { | |
| name: 'auth', | |
| description: 'change the auth method', | |
| action: (_mainCommand, _subCommand, _args) => { | |
| openAuthDialog(); | |
| }, | |
| }, | |
| { | |
| name: 'editor', | |
| description: 'set external editor preference', | |
| action: (_mainCommand, _subCommand, _args) => { | |
| openEditorDialog(); | |
| }, | |
| }, | |
| { | |
| name: 'privacy', | |
| description: 'display the privacy notice', | |
| action: (_mainCommand, _subCommand, _args) => { | |
| openPrivacyNotice(); | |
| }, | |
| }, | |
| { | |
| name: 'stats', | |
| altName: 'usage', | |
| description: 'check session stats', | |
| action: (_mainCommand, _subCommand, _args) => { | |
| const now = new Date(); | |
| const { sessionStartTime, cumulative, currentTurn } = session.stats; | |
| const wallDuration = now.getTime() - sessionStartTime.getTime(); | |
| addMessage({ | |
| type: MessageType.STATS, | |
| stats: cumulative, | |
| lastTurnStats: currentTurn, | |
| duration: formatDuration(wallDuration), | |
| timestamp: new Date(), | |
| }); | |
| }, | |
| }, | |
| { | |
| name: 'mcp', | |
| description: 'list configured MCP servers and tools', | |
| action: async (_mainCommand, _subCommand, _args) => { | |
| // Check if the _subCommand includes a specific flag to control description visibility | |
| let useShowDescriptions = showToolDescriptions; | |
| if (_subCommand === 'desc' || _subCommand === 'descriptions') { | |
| useShowDescriptions = true; | |
| } else if ( | |
| _subCommand === 'nodesc' || | |
| _subCommand === 'nodescriptions' | |
| ) { | |
| useShowDescriptions = false; | |
| } else if (_args === 'desc' || _args === 'descriptions') { | |
| useShowDescriptions = true; | |
| } else if (_args === 'nodesc' || _args === 'nodescriptions') { | |
| useShowDescriptions = false; | |
| } | |
| // Check if the _subCommand includes a specific flag to show detailed tool schema | |
| let useShowSchema = false; | |
| if (_subCommand === 'schema' || _args === 'schema') { | |
| useShowSchema = true; | |
| } | |
| const toolRegistry = await config?.getToolRegistry(); | |
| if (!toolRegistry) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: 'Could not retrieve tool registry.', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| const mcpServers = config?.getMcpServers() || {}; | |
| const serverNames = Object.keys(mcpServers); | |
| if (serverNames.length === 0) { | |
| const docsUrl = 'https://goo.gle/gemini-cli-docs-mcp'; | |
| if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `No MCP servers configured. Please open the following URL in your browser to view documentation:\n${docsUrl}`, | |
| timestamp: new Date(), | |
| }); | |
| } else { | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `No MCP servers configured. Opening documentation in your browser: ${docsUrl}`, | |
| timestamp: new Date(), | |
| }); | |
| await open(docsUrl); | |
| } | |
| return; | |
| } | |
| // Check if any servers are still connecting | |
| const connectingServers = serverNames.filter( | |
| (name) => getMCPServerStatus(name) === MCPServerStatus.CONNECTING, | |
| ); | |
| const discoveryState = getMCPDiscoveryState(); | |
| let message = ''; | |
| // Add overall discovery status message if needed | |
| if ( | |
| discoveryState === MCPDiscoveryState.IN_PROGRESS || | |
| connectingServers.length > 0 | |
| ) { | |
| message += `\u001b[33m⏳ MCP servers are starting up (${connectingServers.length} initializing)...\u001b[0m\n`; | |
| message += `\u001b[90mNote: First startup may take longer. Tool availability will update automatically.\u001b[0m\n\n`; | |
| } | |
| message += 'Configured MCP servers:\n\n'; | |
| for (const serverName of serverNames) { | |
| const serverTools = toolRegistry.getToolsByServer(serverName); | |
| const status = getMCPServerStatus(serverName); | |
| // Add status indicator with descriptive text | |
| let statusIndicator = ''; | |
| let statusText = ''; | |
| switch (status) { | |
| case MCPServerStatus.CONNECTED: | |
| statusIndicator = '🟢'; | |
| statusText = 'Ready'; | |
| break; | |
| case MCPServerStatus.CONNECTING: | |
| statusIndicator = '🔄'; | |
| statusText = 'Starting... (first startup may take longer)'; | |
| break; | |
| case MCPServerStatus.DISCONNECTED: | |
| default: | |
| statusIndicator = '🔴'; | |
| statusText = 'Disconnected'; | |
| break; | |
| } | |
| // Get server description if available | |
| const server = mcpServers[serverName]; | |
| // Format server header with bold formatting and status | |
| message += `${statusIndicator} \u001b[1m${serverName}\u001b[0m - ${statusText}`; | |
| // Add tool count with conditional messaging | |
| if (status === MCPServerStatus.CONNECTED) { | |
| message += ` (${serverTools.length} tools)`; | |
| } else if (status === MCPServerStatus.CONNECTING) { | |
| message += ` (tools will appear when ready)`; | |
| } else { | |
| message += ` (${serverTools.length} tools cached)`; | |
| } | |
| // Add server description with proper handling of multi-line descriptions | |
| if ((useShowDescriptions || useShowSchema) && server?.description) { | |
| const greenColor = '\u001b[32m'; | |
| const resetColor = '\u001b[0m'; | |
| const descLines = server.description.trim().split('\n'); | |
| if (descLines) { | |
| message += ':\n'; | |
| for (let i = 0; i < descLines.length; i++) { | |
| message += ` ${greenColor}${descLines[i]}${resetColor}\n`; | |
| } | |
| } else { | |
| message += '\n'; | |
| } | |
| } else { | |
| message += '\n'; | |
| } | |
| // Reset formatting after server entry | |
| message += '\u001b[0m'; | |
| if (serverTools.length > 0) { | |
| serverTools.forEach((tool) => { | |
| if ( | |
| (useShowDescriptions || useShowSchema) && | |
| tool.description | |
| ) { | |
| // Format tool name in cyan using simple ANSI cyan color | |
| message += ` - \u001b[36m${tool.name}\u001b[0m`; | |
| // Apply green color to the description text | |
| const greenColor = '\u001b[32m'; | |
| const resetColor = '\u001b[0m'; | |
| // Handle multi-line descriptions by properly indenting and preserving formatting | |
| const descLines = tool.description.trim().split('\n'); | |
| if (descLines) { | |
| message += ':\n'; | |
| for (let i = 0; i < descLines.length; i++) { | |
| message += ` ${greenColor}${descLines[i]}${resetColor}\n`; | |
| } | |
| } else { | |
| message += '\n'; | |
| } | |
| // Reset is handled inline with each line now | |
| } else { | |
| // Use cyan color for the tool name even when not showing descriptions | |
| message += ` - \u001b[36m${tool.name}\u001b[0m\n`; | |
| } | |
| if (useShowSchema) { | |
| // Prefix the parameters in cyan | |
| message += ` \u001b[36mParameters:\u001b[0m\n`; | |
| // Apply green color to the parameter text | |
| const greenColor = '\u001b[32m'; | |
| const resetColor = '\u001b[0m'; | |
| const paramsLines = JSON.stringify( | |
| tool.schema.parameters, | |
| null, | |
| 2, | |
| ) | |
| .trim() | |
| .split('\n'); | |
| if (paramsLines) { | |
| for (let i = 0; i < paramsLines.length; i++) { | |
| message += ` ${greenColor}${paramsLines[i]}${resetColor}\n`; | |
| } | |
| } | |
| } | |
| }); | |
| } else { | |
| message += ' No tools available\n'; | |
| } | |
| message += '\n'; | |
| } | |
| // Make sure to reset any ANSI formatting at the end to prevent it from affecting the terminal | |
| message += '\u001b[0m'; | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: message, | |
| timestamp: new Date(), | |
| }); | |
| }, | |
| }, | |
| { | |
| name: 'memory', | |
| description: | |
| 'manage memory. Usage: /memory <show|refresh|add> [text for add]', | |
| action: (mainCommand, subCommand, args) => { | |
| switch (subCommand) { | |
| case 'show': | |
| showMemoryAction(); | |
| return; | |
| case 'refresh': | |
| performMemoryRefresh(); | |
| return; | |
| case 'add': | |
| return addMemoryAction(mainCommand, subCommand, args); // Return the object | |
| case undefined: | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: | |
| 'Missing command\nUsage: /memory <show|refresh|add> [text for add]', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| default: | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: `Unknown /memory command: ${subCommand}. Available: show, refresh, add`, | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| }, | |
| }, | |
| { | |
| name: 'tools', | |
| description: 'list available Gemini CLI tools', | |
| action: async (_mainCommand, _subCommand, _args) => { | |
| // Check if the _subCommand includes a specific flag to control description visibility | |
| let useShowDescriptions = showToolDescriptions; | |
| if (_subCommand === 'desc' || _subCommand === 'descriptions') { | |
| useShowDescriptions = true; | |
| } else if ( | |
| _subCommand === 'nodesc' || | |
| _subCommand === 'nodescriptions' | |
| ) { | |
| useShowDescriptions = false; | |
| } else if (_args === 'desc' || _args === 'descriptions') { | |
| useShowDescriptions = true; | |
| } else if (_args === 'nodesc' || _args === 'nodescriptions') { | |
| useShowDescriptions = false; | |
| } | |
| const toolRegistry = await config?.getToolRegistry(); | |
| const tools = toolRegistry?.getAllTools(); | |
| if (!tools) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: 'Could not retrieve tools.', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| // Filter out MCP tools by checking if they have a serverName property | |
| const geminiTools = tools.filter((tool) => !('serverName' in tool)); | |
| let message = 'Available Gemini CLI tools:\n\n'; | |
| if (geminiTools.length > 0) { | |
| geminiTools.forEach((tool) => { | |
| if (useShowDescriptions && tool.description) { | |
| // Format tool name in cyan using simple ANSI cyan color | |
| message += ` - \u001b[36m${tool.displayName} (${tool.name})\u001b[0m:\n`; | |
| // Apply green color to the description text | |
| const greenColor = '\u001b[32m'; | |
| const resetColor = '\u001b[0m'; | |
| // Handle multi-line descriptions by properly indenting and preserving formatting | |
| const descLines = tool.description.trim().split('\n'); | |
| // If there are multiple lines, add proper indentation for each line | |
| if (descLines) { | |
| for (let i = 0; i < descLines.length; i++) { | |
| message += ` ${greenColor}${descLines[i]}${resetColor}\n`; | |
| } | |
| } | |
| } else { | |
| // Use cyan color for the tool name even when not showing descriptions | |
| message += ` - \u001b[36m${tool.displayName}\u001b[0m\n`; | |
| } | |
| }); | |
| } else { | |
| message += ' No tools available\n'; | |
| } | |
| message += '\n'; | |
| // Make sure to reset any ANSI formatting at the end to prevent it from affecting the terminal | |
| message += '\u001b[0m'; | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: message, | |
| timestamp: new Date(), | |
| }); | |
| }, | |
| }, | |
| { | |
| name: 'corgi', | |
| action: (_mainCommand, _subCommand, _args) => { | |
| toggleCorgiMode(); | |
| }, | |
| }, | |
| { | |
| name: 'about', | |
| description: 'show version info', | |
| action: async (_mainCommand, _subCommand, _args) => { | |
| const osVersion = process.platform; | |
| let sandboxEnv = 'no sandbox'; | |
| if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { | |
| sandboxEnv = process.env.SANDBOX; | |
| } else if (process.env.SANDBOX === 'sandbox-exec') { | |
| sandboxEnv = `sandbox-exec (${ | |
| process.env.SEATBELT_PROFILE || 'unknown' | |
| })`; | |
| } | |
| const modelVersion = config?.getModel() || 'Unknown'; | |
| const cliVersion = await getCliVersion(); | |
| const selectedAuthType = settings.merged.selectedAuthType || ''; | |
| const gcpProject = process.env.GOOGLE_CLOUD_PROJECT || ''; | |
| addMessage({ | |
| type: MessageType.ABOUT, | |
| timestamp: new Date(), | |
| cliVersion, | |
| osVersion, | |
| sandboxEnv, | |
| modelVersion, | |
| selectedAuthType, | |
| gcpProject, | |
| }); | |
| }, | |
| }, | |
| { | |
| name: 'bug', | |
| description: 'submit a bug report', | |
| action: async (_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 cliVersion = await getCliVersion(); | |
| const memoryUsage = formatMemoryUsage(process.memoryUsage().rss); | |
| const info = ` | |
| * **CLI Version:** ${cliVersion} | |
| * **Git Commit:** ${GIT_COMMIT_INFO} | |
| * **Operating System:** ${osVersion} | |
| * **Sandbox Environment:** ${sandboxEnv} | |
| * **Model Version:** ${modelVersion} | |
| * **Memory Usage:** ${memoryUsage} | |
| `; | |
| let bugReportUrl = | |
| 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}'; | |
| const bugCommand = config?.getBugCommand(); | |
| if (bugCommand?.urlTemplate) { | |
| bugReportUrl = bugCommand.urlTemplate; | |
| } | |
| bugReportUrl = bugReportUrl | |
| .replace('{title}', encodeURIComponent(bugDescription)) | |
| .replace('{info}', encodeURIComponent(info)); | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `To submit your bug report, please open the following URL in your browser:\n${bugReportUrl}`, | |
| timestamp: new Date(), | |
| }); | |
| (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: 'chat', | |
| description: | |
| 'Manage conversation history. Usage: /chat <list|save|resume> [tag]', | |
| action: async (_mainCommand, subCommand, args) => { | |
| const tag = (args || '').trim(); | |
| const logger = new Logger(config?.getSessionId() || ''); | |
| await logger.initialize(); | |
| const chat = await config?.getGeminiClient()?.getChat(); | |
| if (!chat) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: 'No chat client available for conversation status.', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| if (!subCommand) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: 'Missing command\nUsage: /chat <list|save|resume> [tag]', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| switch (subCommand) { | |
| case 'save': { | |
| const history = chat.getHistory(); | |
| if (history.length > 0) { | |
| await logger.saveCheckpoint(chat?.getHistory() || [], tag); | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `Conversation checkpoint saved${tag ? ' with tag: ' + tag : ''}.`, | |
| timestamp: new Date(), | |
| }); | |
| } else { | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: 'No conversation found to save.', | |
| timestamp: new Date(), | |
| }); | |
| } | |
| return; | |
| } | |
| case 'resume': | |
| case 'restore': | |
| case 'load': { | |
| const conversation = await logger.loadCheckpoint(tag); | |
| if (conversation.length === 0) { | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `No saved checkpoint found${tag ? ' with tag: ' + tag : ''}.`, | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| clearItems(); | |
| chat.clearHistory(); | |
| const rolemap: { [key: string]: MessageType } = { | |
| user: MessageType.USER, | |
| model: MessageType.GEMINI, | |
| }; | |
| let hasSystemPrompt = false; | |
| let i = 0; | |
| for (const item of conversation) { | |
| i += 1; | |
| // Add each item to history regardless of whether we display | |
| // it. | |
| chat.addHistory(item); | |
| const text = | |
| item.parts | |
| ?.filter((m) => !!m.text) | |
| .map((m) => m.text) | |
| .join('') || ''; | |
| if (!text) { | |
| // Parsing Part[] back to various non-text output not yet implemented. | |
| continue; | |
| } | |
| if (i === 1 && text.match(/context for our chat/)) { | |
| hasSystemPrompt = true; | |
| } | |
| if (i > 2 || !hasSystemPrompt) { | |
| addItem( | |
| { | |
| type: | |
| (item.role && rolemap[item.role]) || MessageType.GEMINI, | |
| text, | |
| } as HistoryItemWithoutId, | |
| i, | |
| ); | |
| } | |
| } | |
| console.clear(); | |
| refreshStatic(); | |
| return; | |
| } | |
| case 'list': | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: | |
| 'list of saved conversations: ' + | |
| (await savedChatTags()).join(', '), | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| default: | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: `Unknown /chat command: ${subCommand}. Available: list, save, resume`, | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| }, | |
| completion: async () => | |
| (await savedChatTags()).map((tag) => 'resume ' + tag), | |
| }, | |
| { | |
| name: 'quit', | |
| altName: 'exit', | |
| description: 'exit the cli', | |
| action: async (mainCommand, _subCommand, _args) => { | |
| const now = new Date(); | |
| const { sessionStartTime, cumulative } = session.stats; | |
| const wallDuration = now.getTime() - sessionStartTime.getTime(); | |
| setQuittingMessages([ | |
| { | |
| type: 'user', | |
| text: `/${mainCommand}`, | |
| id: now.getTime() - 1, | |
| }, | |
| { | |
| type: 'quit', | |
| stats: cumulative, | |
| duration: formatDuration(wallDuration), | |
| id: now.getTime(), | |
| }, | |
| ]); | |
| setTimeout(() => { | |
| process.exit(0); | |
| }, 100); | |
| }, | |
| }, | |
| { | |
| name: 'compress', | |
| altName: 'summarize', | |
| description: 'Compresses the context by replacing it with a summary.', | |
| action: async (_mainCommand, _subCommand, _args) => { | |
| if (pendingCompressionItemRef.current !== null) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: | |
| 'Already compressing, wait for previous request to complete', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| setPendingCompressionItem({ | |
| type: MessageType.COMPRESSION, | |
| compression: { | |
| isPending: true, | |
| originalTokenCount: null, | |
| newTokenCount: null, | |
| }, | |
| }); | |
| try { | |
| const compressed = await config! | |
| .getGeminiClient()! | |
| .tryCompressChat(true); | |
| if (compressed) { | |
| addMessage({ | |
| type: MessageType.COMPRESSION, | |
| compression: { | |
| isPending: false, | |
| originalTokenCount: compressed.originalTokenCount, | |
| newTokenCount: compressed.newTokenCount, | |
| }, | |
| timestamp: new Date(), | |
| }); | |
| } else { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: 'Failed to compress chat history.', | |
| timestamp: new Date(), | |
| }); | |
| } | |
| } catch (e) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: `Failed to compress chat history: ${e instanceof Error ? e.message : String(e)}`, | |
| timestamp: new Date(), | |
| }); | |
| } | |
| setPendingCompressionItem(null); | |
| }, | |
| }, | |
| ]; | |
| if (config?.getCheckpointingEnabled()) { | |
| commands.push({ | |
| name: 'restore', | |
| description: | |
| 'restore a tool call. This will reset the conversation and file history to the state it was in when the tool call was suggested', | |
| completion: async () => { | |
| const checkpointDir = config?.getProjectTempDir() | |
| ? path.join(config.getProjectTempDir(), 'checkpoints') | |
| : undefined; | |
| if (!checkpointDir) { | |
| return []; | |
| } | |
| try { | |
| const files = await fs.readdir(checkpointDir); | |
| return files | |
| .filter((file) => file.endsWith('.json')) | |
| .map((file) => file.replace('.json', '')); | |
| } catch (_err) { | |
| return []; | |
| } | |
| }, | |
| action: async (_mainCommand, subCommand, _args) => { | |
| const checkpointDir = config?.getProjectTempDir() | |
| ? path.join(config.getProjectTempDir(), 'checkpoints') | |
| : undefined; | |
| if (!checkpointDir) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: 'Could not determine the .gemini directory path.', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| try { | |
| // Ensure the directory exists before trying to read it. | |
| await fs.mkdir(checkpointDir, { recursive: true }); | |
| const files = await fs.readdir(checkpointDir); | |
| const jsonFiles = files.filter((file) => file.endsWith('.json')); | |
| if (!subCommand) { | |
| if (jsonFiles.length === 0) { | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: 'No restorable tool calls found.', | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| const truncatedFiles = jsonFiles.map((file) => { | |
| const components = file.split('.'); | |
| if (components.length <= 1) { | |
| return file; | |
| } | |
| components.pop(); | |
| return components.join('.'); | |
| }); | |
| const fileList = truncatedFiles.join('\n'); | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `Available tool calls to restore:\n\n${fileList}`, | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| const selectedFile = subCommand.endsWith('.json') | |
| ? subCommand | |
| : `${subCommand}.json`; | |
| if (!jsonFiles.includes(selectedFile)) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: `File not found: ${selectedFile}`, | |
| timestamp: new Date(), | |
| }); | |
| return; | |
| } | |
| const filePath = path.join(checkpointDir, selectedFile); | |
| const data = await fs.readFile(filePath, 'utf-8'); | |
| const toolCallData = JSON.parse(data); | |
| if (toolCallData.history) { | |
| loadHistory(toolCallData.history); | |
| } | |
| if (toolCallData.clientHistory) { | |
| await config | |
| ?.getGeminiClient() | |
| ?.setHistory(toolCallData.clientHistory); | |
| } | |
| if (toolCallData.commitHash) { | |
| await gitService?.restoreProjectFromSnapshot( | |
| toolCallData.commitHash, | |
| ); | |
| addMessage({ | |
| type: MessageType.INFO, | |
| content: `Restored project to the state before the tool call.`, | |
| timestamp: new Date(), | |
| }); | |
| } | |
| return { | |
| shouldScheduleTool: true, | |
| toolName: toolCallData.toolCall.name, | |
| toolArgs: toolCallData.toolCall.args, | |
| }; | |
| } catch (error) { | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: `Could not read restorable tool calls. This is the error: ${error}`, | |
| timestamp: new Date(), | |
| }); | |
| } | |
| }, | |
| }); | |
| } | |
| return commands; | |
| }, [ | |
| onDebugMessage, | |
| setShowHelp, | |
| refreshStatic, | |
| openThemeDialog, | |
| openAuthDialog, | |
| openEditorDialog, | |
| clearItems, | |
| performMemoryRefresh, | |
| showMemoryAction, | |
| addMemoryAction, | |
| addMessage, | |
| toggleCorgiMode, | |
| savedChatTags, | |
| config, | |
| settings, | |
| showToolDescriptions, | |
| session, | |
| gitService, | |
| loadHistory, | |
| addItem, | |
| setQuittingMessages, | |
| pendingCompressionItemRef, | |
| setPendingCompressionItem, | |
| openPrivacyNotice, | |
| ]); | |
| const handleSlashCommand = useCallback( | |
| async ( | |
| rawQuery: PartListUnion, | |
| ): Promise<SlashCommandActionReturn | boolean> => { | |
| if (typeof rawQuery !== 'string') { | |
| return false; | |
| } | |
| const trimmed = rawQuery.trim(); | |
| if (!trimmed.startsWith('/') && !trimmed.startsWith('?')) { | |
| return false; | |
| } | |
| const userMessageTimestamp = Date.now(); | |
| if (trimmed !== '/quit' && trimmed !== '/exit') { | |
| addItem( | |
| { type: MessageType.USER, text: trimmed }, | |
| userMessageTimestamp, | |
| ); | |
| } | |
| let subCommand: string | undefined; | |
| let args: string | undefined; | |
| const commandToMatch = (() => { | |
| if (trimmed.startsWith('?')) { | |
| return 'help'; | |
| } | |
| 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]; | |
| })(); | |
| const mainCommand = commandToMatch; | |
| for (const cmd of slashCommands) { | |
| if (mainCommand === cmd.name || mainCommand === cmd.altName) { | |
| const actionResult = await cmd.action(mainCommand, subCommand, args); | |
| if ( | |
| typeof actionResult === 'object' && | |
| actionResult?.shouldScheduleTool | |
| ) { | |
| return actionResult; // Return the object for useGeminiStream | |
| } | |
| return true; // Command was handled, but no tool to schedule | |
| } | |
| } | |
| addMessage({ | |
| type: MessageType.ERROR, | |
| content: `Unknown command: ${trimmed}`, | |
| timestamp: new Date(), | |
| }); | |
| return true; // Indicate command was processed (even if unknown) | |
| }, | |
| [addItem, slashCommands, addMessage], | |
| ); | |
| return { handleSlashCommand, slashCommands, pendingHistoryItems }; | |
| }; | |