diff options
Diffstat (limited to 'packages/cli/src/ui/hooks')
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.ts | 78 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useGeminiStream.ts | 58 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useRefreshMemoryCommand.ts | 7 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useShowMemoryCommand.ts | 80 |
4 files changed, 166 insertions, 57 deletions
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index aa7323ca..e2b478e2 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -8,32 +8,59 @@ import { useCallback, useMemo } from 'react'; import { type PartListUnion } from '@google/genai'; import { getCommandFromQuery } from '../utils/commandUtils.js'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; +import { Config } from '@gemini-code/server'; // Import Config +import { Message, MessageType, HistoryItemWithoutId } from '../types.js'; // Import Message types +import { + createShowMemoryAction, + SHOW_MEMORY_COMMAND_NAME, +} from './useShowMemoryCommand.js'; +import { REFRESH_MEMORY_COMMAND_NAME } from './useRefreshMemoryCommand.js'; // Only import name now +import process from 'node:process'; // For process.exit export interface SlashCommand { name: string; altName?: string; description: string; - action: (value: PartListUnion) => void; + action: (value: PartListUnion | string) => void; // Allow string for simpler actions } /** * Hook to define and process slash commands (e.g., /help, /clear). */ export const useSlashCommandProcessor = ( + config: Config | null, // Add config here addItem: UseHistoryManagerReturn['addItem'], clearItems: UseHistoryManagerReturn['clearItems'], refreshStatic: () => void, setShowHelp: React.Dispatch<React.SetStateAction<boolean>>, onDebugMessage: (message: string) => void, openThemeDialog: () => void, + performMemoryRefresh: () => Promise<void>, // Add performMemoryRefresh prop ) => { + 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 slashCommands: SlashCommand[] = useMemo( () => [ { name: 'help', altName: '?', description: 'for help on gemini-code', - action: (_value: PartListUnion) => { + action: (_value: PartListUnion | string) => { onDebugMessage('Opening help.'); setShowHelp(true); }, @@ -41,7 +68,7 @@ export const useSlashCommandProcessor = ( { name: 'clear', description: 'clear the screen', - action: (_value: PartListUnion) => { + action: (_value: PartListUnion | string) => { onDebugMessage('Clearing terminal.'); clearItems(); refreshStatic(); @@ -50,29 +77,41 @@ export const useSlashCommandProcessor = ( { name: 'theme', description: 'change the theme', - action: (_value: PartListUnion) => { + action: (_value) => { openThemeDialog(); }, }, { + name: REFRESH_MEMORY_COMMAND_NAME.substring(1), // Remove leading '/' + description: 'Reloads instructions from all GEMINI.md files.', + action: performMemoryRefresh, // Use the passed in function + }, + { + name: SHOW_MEMORY_COMMAND_NAME.substring(1), // Remove leading '/' + description: 'Displays the current hierarchical memory content.', + action: showMemoryAction, + }, + { name: 'quit', altName: 'exit', description: '', - action: (_value: PartListUnion) => { + action: (_value: PartListUnion | string) => { onDebugMessage('Quitting. Good-bye.'); process.exit(0); }, }, ], - [onDebugMessage, setShowHelp, refreshStatic, openThemeDialog, clearItems], + [ + onDebugMessage, + setShowHelp, + refreshStatic, + openThemeDialog, + clearItems, + performMemoryRefresh, // Add to dependencies + showMemoryAction, + ], ); - /** - * 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') { @@ -87,26 +126,27 @@ export const useSlashCommandProcessor = ( } const userMessageTimestamp = Date.now(); - addItem({ type: 'user', text: trimmed }, userMessageTimestamp); + // Add user message to history only if it's not a silent command or handled internally + // For now, adding all slash commands to history for transparency. + addItem({ type: MessageType.USER, text: trimmed }, userMessageTimestamp); for (const cmd of slashCommands) { if ( test === cmd.name || test === cmd.altName || - symbol === cmd.altName + (symbol === '?' && cmd.altName === '?') // Special handling for ? as help ) { - cmd.action(trimmed); + cmd.action(trimmed); // Pass the full trimmed command for context if needed return true; } } - // Unknown command: Add error message addItem( - { type: 'error', text: `Unknown command: ${trimmed}` }, - userMessageTimestamp, // Use same base timestamp for related error + { type: MessageType.ERROR, text: `Unknown command: ${trimmed}` }, + userMessageTimestamp, ); - return true; // Indicate command was processed (even though invalid) + return true; }, [addItem, slashCommands], ); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 15239bb1..2b18f0a1 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -26,6 +26,7 @@ import { IndividualToolCallDisplay, ToolCallStatus, HistoryItemWithoutId, + MessageType, } from '../types.js'; import { isAtCommand } from '../utils/commandUtils.js'; import { useShellCommandProcessor } from './shellCommandProcessor.js'; @@ -34,16 +35,14 @@ import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js'; import { useStateAndRef } from './useStateAndRef.js'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; -/** - * Hook to manage the Gemini stream, handle user input, process commands, - * and interact with the Gemini API and history manager. - */ export const useGeminiStream = ( addItem: UseHistoryManagerReturn['addItem'], + _clearItems: UseHistoryManagerReturn['clearItems'], refreshStatic: () => void, setShowHelp: React.Dispatch<React.SetStateAction<boolean>>, config: Config, onDebugMessage: (message: string) => void, + _openThemeDialog: () => void, handleSlashCommand: (cmd: PartListUnion) => boolean, ) => { const toolRegistry = config.getToolRegistry(); @@ -72,7 +71,7 @@ export const useGeminiStream = ( } catch (error: unknown) { const errorMsg = `Failed to initialize client: ${getErrorMessage(error) || 'Unknown error'}`; setInitError(errorMsg); - addItem({ type: 'error', text: errorMsg }, Date.now()); + addItem({ type: MessageType.ERROR, text: errorMsg }, Date.now()); } } }, [config, addItem]); @@ -100,11 +99,9 @@ export const useGeminiStream = ( const trimmedQuery = query.trim(); onDebugMessage(`User query: '${trimmedQuery}'`); - // Handle UI-only commands first if (handleSlashCommand(trimmedQuery)) return; if (handleShellCommand(trimmedQuery)) return; - // Handle @-commands (which might involve tool calls) if (isAtCommand(trimmedQuery)) { const atCommandResult = await handleAtCommand({ query: trimmedQuery, @@ -117,12 +114,13 @@ export const useGeminiStream = ( if (!atCommandResult.shouldProceed) return; queryToSendToGemini = atCommandResult.processedQuery; } else { - // Normal query for Gemini - addItem({ type: 'user', text: trimmedQuery }, userMessageTimestamp); + addItem( + { type: MessageType.USER, text: trimmedQuery }, + userMessageTimestamp, + ); queryToSendToGemini = trimmedQuery; } } else { - // It's a function response (PartListUnion that isn't a string) queryToSendToGemini = query; } @@ -137,7 +135,7 @@ export const useGeminiStream = ( if (!client) { const errorMsg = 'Gemini client is not available.'; setInitError(errorMsg); - addItem({ type: 'error', text: errorMsg }, Date.now()); + addItem({ type: MessageType.ERROR, text: errorMsg }, Date.now()); return; } @@ -147,7 +145,7 @@ export const useGeminiStream = ( } catch (err: unknown) { const errorMsg = `Failed to start chat: ${getErrorMessage(err)}`; setInitError(errorMsg); - addItem({ type: 'error', text: errorMsg }, Date.now()); + addItem({ type: MessageType.ERROR, text: errorMsg }, Date.now()); setStreamingState(StreamingState.Idle); return; } @@ -172,12 +170,10 @@ export const useGeminiStream = ( pendingHistoryItemRef.current?.type !== 'gemini' && pendingHistoryItemRef.current?.type !== 'gemini_content' ) { - // Flush out existing pending history item. if (pendingHistoryItemRef.current) { addItem(pendingHistoryItemRef.current, userMessageTimestamp); } setPendingHistoryItem({ - // Use the 'gemini' type for the initial history item. type: 'gemini', text: '', }); @@ -206,7 +202,7 @@ export const useGeminiStream = ( // broken up so that there are more "statically" rendered. const beforeText = geminiMessageBuffer.substring(0, splitPoint); const afterText = geminiMessageBuffer.substring(splitPoint); - geminiMessageBuffer = afterText; // Continue accumulating from split point + geminiMessageBuffer = afterText; addItem( { type: pendingHistoryItemRef.current?.type as @@ -230,7 +226,6 @@ export const useGeminiStream = ( } if (pendingHistoryItemRef.current?.type !== 'tool_group') { - // Flush out existing pending history item. if (pendingHistoryItemRef.current) { addItem(pendingHistoryItemRef.current, userMessageTimestamp); } @@ -256,9 +251,7 @@ export const useGeminiStream = ( confirmationDetails: undefined, }; - // Add pending tool call to the UI history group setPendingHistoryItem((pending) => - // Should always be true. pending?.type === 'tool_group' ? { ...pending, @@ -280,11 +273,9 @@ export const useGeminiStream = ( confirmationDetails, ); setStreamingState(StreamingState.WaitingForConfirmation); - return; // Wait for user confirmation + return; } else if (event.type === ServerGeminiEventType.UserCancelled) { - // Flush out existing pending history item. if (pendingHistoryItemRef.current) { - // If the pending item is a tool_group, update statuses to Canceled if (pendingHistoryItemRef.current.type === 'tool_group') { const updatedTools = pendingHistoryItemRef.current.tools.map( (tool) => { @@ -307,25 +298,26 @@ export const useGeminiStream = ( setPendingHistoryItem(null); } addItem( - { type: 'info', text: 'User cancelled the request.' }, + { type: MessageType.INFO, text: 'User cancelled the request.' }, userMessageTimestamp, ); setStreamingState(StreamingState.Idle); - return; // Stop processing the stream + return; } else if (event.type === ServerGeminiEventType.Error) { - // Flush out existing pending history item. if (pendingHistoryItemRef.current) { addItem(pendingHistoryItemRef.current, userMessageTimestamp); setPendingHistoryItem(null); } addItem( - { type: 'error', text: `[API Error: ${event.value.message}]` }, + { + type: MessageType.ERROR, + text: `[API Error: ${event.value.message}]`, + }, userMessageTimestamp, ); } - } // End stream loop + } - // We're waiting for user input now so all pending history can be committed. if (pendingHistoryItemRef.current) { addItem(pendingHistoryItemRef.current, userMessageTimestamp); setPendingHistoryItem(null); @@ -336,7 +328,7 @@ export const useGeminiStream = ( if (!isNodeError(error) || error.name !== 'AbortError') { addItem( { - type: 'error', + type: MessageType.ERROR, text: `[Stream Error: ${getErrorMessage(error)}]`, }, userMessageTimestamp, @@ -347,8 +339,6 @@ export const useGeminiStream = ( abortControllerRef.current = null; } - // --- Helper functions for updating tool UI --- - function updateConfirmingFunctionStatusUI( callId: string, confirmationDetails: ToolCallConfirmationDetails | undefined, @@ -396,7 +386,6 @@ export const useGeminiStream = ( ); } - // Wires the server-side confirmation callback to UI updates and state changes function wireConfirmationSubmission( confirmationDetails: ServerToolCallConfirmationDetails, ): ToolCallConfirmationDetails { @@ -405,10 +394,8 @@ export const useGeminiStream = ( const resubmittingConfirm = async ( outcome: ToolConfirmationOutcome, ) => { - // Call the original server-side handler first originalConfirmationDetails.onConfirm(outcome); - // Ensure UI updates before potentially long-running operations if (pendingHistoryItemRef?.current?.type === 'tool_group') { setPendingHistoryItem((item) => item?.type === 'tool_group' @@ -511,7 +498,6 @@ export const useGeminiStream = ( error: new Error(declineMessage), }; - // Update conversation history without re-issuing another request to indicate the decline. const history = chatSessionRef.current?.getHistory(); if (history) { history.push({ @@ -520,7 +506,6 @@ export const useGeminiStream = ( }); } - // Update UI to show cancellation/error updateFunctionResponseUI(responseInfo, status); if (pendingHistoryItemRef.current) { addItem(pendingHistoryItemRef.current, Date.now()); @@ -555,9 +540,6 @@ export const useGeminiStream = ( streamingState, submitQuery, initError, - // Normally we would be concerned that the ref would not be up-to-date, but - // this isn't a concern as the ref is updated whenever the corresponding - // state is updated. pendingHistoryItem: pendingHistoryItemRef.current, }; }; diff --git a/packages/cli/src/ui/hooks/useRefreshMemoryCommand.ts b/packages/cli/src/ui/hooks/useRefreshMemoryCommand.ts new file mode 100644 index 00000000..025eb9a0 --- /dev/null +++ b/packages/cli/src/ui/hooks/useRefreshMemoryCommand.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export const REFRESH_MEMORY_COMMAND_NAME = '/refreshmemory'; diff --git a/packages/cli/src/ui/hooks/useShowMemoryCommand.ts b/packages/cli/src/ui/hooks/useShowMemoryCommand.ts new file mode 100644 index 00000000..c15b27cd --- /dev/null +++ b/packages/cli/src/ui/hooks/useShowMemoryCommand.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Message, MessageType } from '../types.js'; +import { Config } from '@gemini-code/server'; +import { getGeminiMdFilePaths } from '../../config/config.js'; +import { homedir } from 'os'; +import process from 'node:process'; + +export const SHOW_MEMORY_COMMAND_NAME = '/showmemory'; + +export function createShowMemoryAction( + config: Config | null, + addMessage: (message: Message) => void, +) { + return async () => { + if (!config) { + addMessage({ + type: MessageType.ERROR, + content: 'Configuration not available. Cannot show memory.', + timestamp: new Date(), + }); + return; + } + + const debugMode = config.getDebugMode(); + const cwd = process.cwd(); + const homeDir = homedir(); + + if (debugMode) { + console.log(`[DEBUG] Show Memory: CWD=${cwd}, Home=${homeDir}`); + } + + const filePaths = await getGeminiMdFilePaths(cwd, homeDir, debugMode); + + if (filePaths.length > 0) { + addMessage({ + type: MessageType.INFO, + content: `The following GEMINI.md files are being used (in order of precedence):\n- ${filePaths.join('\n- ')}`, + timestamp: new Date(), + }); + } else { + addMessage({ + type: MessageType.INFO, + content: 'No GEMINI.md files found in the hierarchy.', + timestamp: new Date(), + }); + } + + const currentMemory = config.getUserMemory(); + + if (config.getDebugMode()) { + console.log( + `[DEBUG] Showing memory. Content from config.getUserMemory() (first 200 chars): ${currentMemory.substring(0, 200)}...`, + ); + } + + if (currentMemory && currentMemory.trim().length > 0) { + addMessage({ + type: MessageType.INFO, + // Display with a clear heading, and potentially format for readability if very long. + // For now, direct display. Consider using Markdown formatting for code blocks if memory contains them. + content: `Current combined GEMINI.md memory content:\n\`\`\`markdown\n${currentMemory}\n\`\`\``, + timestamp: new Date(), + }); + } else { + // This message might be redundant if filePaths.length === 0, but kept for explicitness + // if somehow memory is empty even if files were found (e.g., all files are empty). + addMessage({ + type: MessageType.INFO, + content: + 'No hierarchical memory (GEMINI.md) is currently loaded or memory is empty.', + timestamp: new Date(), + }); + } + }; +} |
