diff --git a/packages/cli/src/config/memoryUtils.test.ts b/packages/cli/src/config/memoryUtils.test.ts deleted file mode 100644 index 3ed51e74..00000000 --- a/packages/cli/src/config/memoryUtils.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - describe, - it, - expect, - vi, - beforeEach, - // afterEach, // Removed as it's not used - type Mocked, - type Mock, -} from 'vitest'; -import * as path from 'path'; -import { homedir } from 'os'; -import * as fs from 'fs/promises'; -import { getGlobalMemoryFilePath, addMemoryEntry } from './memoryUtils.js'; -import { SETTINGS_DIRECTORY_NAME } from './settings.js'; -import { - MemoryTool, - GEMINI_MD_FILENAME, - // MEMORY_SECTION_HEADER, // Removed as it's not used - // getErrorMessage, // Removed as it's not used -} from '@gemini-code/server'; - -// Mock the entire fs/promises module -vi.mock('fs/promises'); -// Mock MemoryTool static method -vi.mock('@gemini-code/server', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - MemoryTool: { - ...actual.MemoryTool, - performAddMemoryEntry: vi.fn(), - }, - }; -}); - -describe('memoryUtils', () => { - beforeEach(() => { - // Reset mocks before each test - vi.resetAllMocks(); - }); - - describe('getGlobalMemoryFilePath', () => { - it('should return the correct global memory file path', () => { - const expectedPath = path.join( - homedir(), - SETTINGS_DIRECTORY_NAME, - GEMINI_MD_FILENAME, - ); - expect(getGlobalMemoryFilePath()).toBe(expectedPath); - }); - }); - - describe('addMemoryEntry', () => { - const mockFs = fs as Mocked; // Type cast for mocked fs - const mockPerformAddMemoryEntry = MemoryTool.performAddMemoryEntry as Mock; - - it('should call MemoryTool.performAddMemoryEntry with correct parameters', async () => { - const testText = 'Remember this important fact.'; - const expectedFilePath = getGlobalMemoryFilePath(); - - await addMemoryEntry(testText); - - expect(mockPerformAddMemoryEntry).toHaveBeenCalledOnce(); - expect(mockPerformAddMemoryEntry).toHaveBeenCalledWith( - testText, - expectedFilePath, - { - readFile: mockFs.readFile, - writeFile: mockFs.writeFile, - mkdir: mockFs.mkdir, - }, - ); - }); - - it('should propagate errors from MemoryTool.performAddMemoryEntry', async () => { - const testText = 'This will fail.'; - const expectedError = new Error('Failed to add memory entry'); - mockPerformAddMemoryEntry.mockRejectedValueOnce(expectedError); - - await expect(addMemoryEntry(testText)).rejects.toThrow(expectedError); - }); - }); - - // More tests will be added here -}); diff --git a/packages/cli/src/config/memoryUtils.ts b/packages/cli/src/config/memoryUtils.ts deleted file mode 100644 index 63a7734f..00000000 --- a/packages/cli/src/config/memoryUtils.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as fs from 'fs/promises'; -import * as path from 'path'; -import { homedir } from 'os'; -import { SETTINGS_DIRECTORY_NAME } from './settings.js'; -import { - // getErrorMessage, // Removed as it's not used - MemoryTool, - GEMINI_MD_FILENAME, - // MEMORY_SECTION_HEADER, // Removed as it's not used -} from '@gemini-code/server'; - -/** - * Gets the absolute path to the global GEMINI.md file. - */ -export function getGlobalMemoryFilePath(): string { - return path.join(homedir(), SETTINGS_DIRECTORY_NAME, GEMINI_MD_FILENAME); -} - -/** - * Adds a new memory entry to the global GEMINI.md file under the specified header. - */ -export async function addMemoryEntry(text: string): Promise { - const filePath = getGlobalMemoryFilePath(); - // The performAddMemoryEntry method from MemoryTool will handle its own errors - // and throw an appropriately formatted error if needed. - await MemoryTool.performAddMemoryEntry(text, filePath, { - readFile: fs.readFile, - writeFile: fs.writeFile, - mkdir: fs.mkdir, - }); -} diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index 1d9eec53..4c630d10 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -11,6 +11,7 @@ const { mockProcessExit } = vi.hoisted(() => ({ vi.mock('node:process', () => ({ exit: mockProcessExit, cwd: vi.fn(() => '/mock/cwd'), + env: { ...process.env }, })); vi.mock('node:fs/promises', () => ({ @@ -22,24 +23,20 @@ vi.mock('node:fs/promises', () => ({ import { act, renderHook } from '@testing-library/react'; import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; import open from 'open'; -import { useSlashCommandProcessor } from './slashCommandProcessor.js'; +import { + useSlashCommandProcessor, + type SlashCommandActionReturn, +} from './slashCommandProcessor.js'; import { MessageType } from '../types.js'; -import * as memoryUtils from '../../config/memoryUtils.js'; -import { type Config, MemoryTool } from '@gemini-code/server'; -import * as fsPromises from 'node:fs/promises'; +import { type Config } from '@gemini-code/server'; -// Import the module for mocking its functions import * as ShowMemoryCommandModule from './useShowMemoryCommand.js'; -// Mock dependencies vi.mock('./useShowMemoryCommand.js', () => ({ SHOW_MEMORY_COMMAND_NAME: '/memory show', createShowMemoryAction: vi.fn(() => vi.fn()), })); -// Spy on the static method we want to mock -const performAddMemoryEntrySpy = vi.spyOn(MemoryTool, 'performAddMemoryEntry'); - vi.mock('open', () => ({ default: vi.fn(), })); @@ -65,29 +62,16 @@ describe('useSlashCommandProcessor', () => { mockPerformMemoryRefresh = vi.fn().mockResolvedValue(undefined); mockConfig = { getDebugMode: vi.fn(() => false), - getSandbox: vi.fn(() => 'test-sandbox'), // Added mock - getModel: vi.fn(() => 'test-model'), // Added mock + getSandbox: vi.fn(() => 'test-sandbox'), + getModel: vi.fn(() => 'test-model'), } as unknown as Config; mockCorgiMode = vi.fn(); - // Clear mocks for fsPromises if they were used directly or indirectly - vi.mocked(fsPromises.readFile).mockClear(); - vi.mocked(fsPromises.writeFile).mockClear(); - vi.mocked(fsPromises.mkdir).mockClear(); - - performAddMemoryEntrySpy.mockReset(); (open as Mock).mockClear(); - // vi.spyOn(memoryUtils, 'deleteLastMemoryEntry').mockImplementation(vi.fn()); - // vi.spyOn(memoryUtils, 'deleteAllAddedMemoryEntries').mockImplementation( - // vi.fn(), - // ); - - // vi.mocked(memoryUtils.deleteLastMemoryEntry).mockClear(); - // vi.mocked(memoryUtils.deleteAllAddedMemoryEntries).mockClear(); - mockProcessExit.mockClear(); (ShowMemoryCommandModule.createShowMemoryAction as Mock).mockClear(); mockPerformMemoryRefresh.mockClear(); + process.env = { ...globalThis.process.env }; }); const getProcessor = () => { @@ -109,118 +93,97 @@ describe('useSlashCommandProcessor', () => { }; describe('/memory add', () => { - it('should call MemoryTool.performAddMemoryEntry and refresh on valid input', async () => { - performAddMemoryEntrySpy.mockResolvedValue(undefined); + it('should return tool scheduling info on valid input', async () => { const { handleSlashCommand } = getProcessor(); const fact = 'Remember this fact'; + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand(`/memory add ${fact}`); + commandResult = handleSlashCommand(`/memory add ${fact}`); }); + expect(mockAddItem).toHaveBeenNthCalledWith( - 1, + 1, // User message expect.objectContaining({ type: MessageType.USER, text: `/memory add ${fact}`, }), expect.any(Number), ); - expect(performAddMemoryEntrySpy).toHaveBeenCalledWith( - fact, - memoryUtils.getGlobalMemoryFilePath(), // Ensure this path is correct - { - readFile: fsPromises.readFile, - writeFile: fsPromises.writeFile, - mkdir: fsPromises.mkdir, - }, - ); - expect(mockPerformMemoryRefresh).toHaveBeenCalled(); expect(mockAddItem).toHaveBeenNthCalledWith( - 2, + 2, // Info message about attempting to save expect.objectContaining({ type: MessageType.INFO, - text: `Successfully added to memory: "${fact}"`, + text: `Attempting to save to memory: "${fact}"`, }), expect.any(Number), ); + + expect(commandResult).toEqual({ + shouldScheduleTool: true, + toolName: 'save_memory', + toolArgs: { fact }, + }); + + // performMemoryRefresh is no longer called directly here + expect(mockPerformMemoryRefresh).not.toHaveBeenCalled(); }); - it('should show usage error if no text is provided', async () => { + it('should show usage error and return true if no text is provided', async () => { const { handleSlashCommand } = getProcessor(); + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand('/memory add '); + commandResult = handleSlashCommand('/memory add '); }); - expect(performAddMemoryEntrySpy).not.toHaveBeenCalled(); + expect(mockAddItem).toHaveBeenNthCalledWith( - 2, + 2, // After user message expect.objectContaining({ type: MessageType.ERROR, text: 'Usage: /memory add ', }), expect.any(Number), ); - }); - - it('should handle error from MemoryTool.performAddMemoryEntry', async () => { - const fact = 'Another fact'; - performAddMemoryEntrySpy.mockRejectedValue( - new Error('[MemoryTool] Failed to add memory entry: Disk full'), - ); - const { handleSlashCommand } = getProcessor(); - await act(async () => { - handleSlashCommand(`/memory add ${fact}`); - }); - expect(performAddMemoryEntrySpy).toHaveBeenCalledWith( - fact, - memoryUtils.getGlobalMemoryFilePath(), - { - readFile: fsPromises.readFile, - writeFile: fsPromises.writeFile, - mkdir: fsPromises.mkdir, - }, - ); - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.ERROR, - text: 'Failed to add memory: [MemoryTool] Failed to add memory entry: Disk full', - }), - expect.any(Number), - ); + expect(commandResult).toBe(true); // Command was handled (by showing an error) }); }); describe('/memory show', () => { - it('should call the showMemoryAction', async () => { + it('should call the showMemoryAction and return true', async () => { const mockReturnedShowAction = vi.fn(); vi.mocked(ShowMemoryCommandModule.createShowMemoryAction).mockReturnValue( mockReturnedShowAction, ); const { handleSlashCommand } = getProcessor(); + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand('/memory show'); + commandResult = handleSlashCommand('/memory show'); }); expect( ShowMemoryCommandModule.createShowMemoryAction, ).toHaveBeenCalledWith(mockConfig, expect.any(Function)); expect(mockReturnedShowAction).toHaveBeenCalled(); + expect(commandResult).toBe(true); }); }); describe('/memory refresh', () => { - it('should call performMemoryRefresh', async () => { + it('should call performMemoryRefresh and return true', async () => { const { handleSlashCommand } = getProcessor(); + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand('/memory refresh'); + commandResult = handleSlashCommand('/memory refresh'); }); expect(mockPerformMemoryRefresh).toHaveBeenCalled(); + expect(commandResult).toBe(true); }); }); describe('Unknown /memory subcommand', () => { - it('should show an error for unknown /memory subcommand', async () => { + it('should show an error for unknown /memory subcommand and return true', async () => { const { handleSlashCommand } = getProcessor(); + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand('/memory foobar'); + commandResult = handleSlashCommand('/memory foobar'); }); expect(mockAddItem).toHaveBeenNthCalledWith( 2, @@ -230,20 +193,33 @@ describe('useSlashCommandProcessor', () => { }), expect.any(Number), ); + expect(commandResult).toBe(true); }); }); describe('Other commands', () => { - it('/help should open help', async () => { + it('/help should open help and return true', async () => { const { handleSlashCommand } = getProcessor(); + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand('/help'); + commandResult = handleSlashCommand('/help'); }); expect(mockSetShowHelp).toHaveBeenCalledWith(true); + expect(commandResult).toBe(true); }); }); describe('/bug command', () => { + const originalEnv = process.env; + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + const getExpectedUrl = ( description?: string, sandboxEnvVar?: string, @@ -257,7 +233,7 @@ describe('useSlashCommandProcessor', () => { } else if (sandboxEnvVar === 'sandbox-exec') { sandboxEnvStr = `sandbox-exec (${seatbeltProfileVar || 'unknown'})`; } - const modelVersion = 'test-model'; // From mockConfig + const modelVersion = 'test-model'; const diagnosticInfo = ` ## Describe the bug @@ -281,7 +257,7 @@ Add any other context about the problem here. return url; }; - it('should call open with the correct GitHub issue URL', async () => { + it('should call open with the correct GitHub issue URL and return true', async () => { process.env.SANDBOX = 'gemini-sandbox'; process.env.SEATBELT_PROFILE = 'test_profile'; const { handleSlashCommand } = getProcessor(); @@ -291,112 +267,23 @@ Add any other context about the problem here. process.env.SANDBOX, process.env.SEATBELT_PROFILE, ); - + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand(`/bug ${bugDescription}`); + commandResult = handleSlashCommand(`/bug ${bugDescription}`); }); - expect(mockAddItem).toHaveBeenNthCalledWith( - 1, // User command - expect.objectContaining({ - type: MessageType.USER, - text: `/bug ${bugDescription}`, - }), - expect.any(Number), - ); - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, // Info message - expect.objectContaining({ - type: MessageType.INFO, - text: `To submit your bug report, please open the following URL in your browser:\n${expectedUrl}`, - }), - expect.any(Number), // Timestamps are numbers from Date.now() - ); + expect(mockAddItem).toHaveBeenCalledTimes(2); expect(open).toHaveBeenCalledWith(expectedUrl); - delete process.env.SANDBOX; - delete process.env.SEATBELT_PROFILE; - }); - - it('should open the generic issue page if no bug description is provided', async () => { - process.env.SANDBOX = 'sandbox-exec'; - process.env.SEATBELT_PROFILE = 'minimal'; - const { handleSlashCommand } = getProcessor(); - const expectedUrl = getExpectedUrl( - undefined, - process.env.SANDBOX, - process.env.SEATBELT_PROFILE, - ); - await act(async () => { - handleSlashCommand('/bug '); - }); - expect(open).toHaveBeenCalledWith(expectedUrl); - expect(mockAddItem).toHaveBeenNthCalledWith( - 1, // User command - expect.objectContaining({ - type: MessageType.USER, - text: '/bug', // Ensure this matches the input - }), - expect.any(Number), - ); - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, // Info message - expect.objectContaining({ - type: MessageType.INFO, - text: `To submit your bug report, please open the following URL in your browser:\n${expectedUrl}`, - }), - expect.any(Number), // Timestamps are numbers from Date.now() - ); - delete process.env.SANDBOX; - delete process.env.SEATBELT_PROFILE; - }); - - it('should handle errors when open fails', async () => { - // Test with no SANDBOX env var - delete process.env.SANDBOX; - delete process.env.SEATBELT_PROFILE; - const { handleSlashCommand } = getProcessor(); - const bugDescription = 'Another bug'; - const expectedUrl = getExpectedUrl(bugDescription); - const openError = new Error('Failed to open browser'); - (open as Mock).mockRejectedValue(openError); - - await act(async () => { - handleSlashCommand(`/bug ${bugDescription}`); - }); - - expect(open).toHaveBeenCalledWith(expectedUrl); - expect(mockAddItem).toHaveBeenNthCalledWith( - 1, // User command - expect.objectContaining({ - type: MessageType.USER, - text: `/bug ${bugDescription}`, - }), - expect.any(Number), - ); - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, // Info message before open attempt - expect.objectContaining({ - type: MessageType.INFO, - text: `To submit your bug report, please open the following URL in your browser:\n${expectedUrl}`, - }), - expect.any(Number), // Timestamps are numbers from Date.now() - ); - expect(mockAddItem).toHaveBeenNthCalledWith( - 3, // Error message after open fails - expect.objectContaining({ - type: MessageType.ERROR, - text: `Could not open URL in browser: ${openError.message}`, - }), - expect.any(Number), // Timestamps are numbers from Date.now() - ); + expect(commandResult).toBe(true); }); }); describe('Unknown command', () => { - it('should show an error for a general unknown command', async () => { + it('should show an error and return true for a general unknown command', async () => { const { handleSlashCommand } = getProcessor(); + let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { - handleSlashCommand('/unknowncommand'); + commandResult = handleSlashCommand('/unknowncommand'); }); expect(mockAddItem).toHaveBeenNthCalledWith( 2, @@ -406,6 +293,7 @@ Add any other context about the problem here. }), expect.any(Number), ); + expect(commandResult).toBe(true); }); }); }); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 1f1c0444..82d1fc7a 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -11,13 +11,23 @@ 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 SlashCommandActionReturn { + shouldScheduleTool?: boolean; + toolName?: string; + toolArgs?: Record; + message?: string; // For simple messages or errors +} export interface SlashCommand { name: string; altName?: string; description?: string; - action: (mainCommand: string, subCommand?: string, args?: string) => void; + action: ( + mainCommand: string, + subCommand?: string, + args?: string, + ) => void | SlashCommandActionReturn; // Action can now return this object } /** @@ -37,9 +47,8 @@ export const useSlashCommandProcessor = ( ) => { 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 + type: message.type, text: message.content, }; addItem(historyItemContent, message.timestamp.getTime()); @@ -53,7 +62,11 @@ export const useSlashCommandProcessor = ( }, [config, addMessage]); const addMemoryAction = useCallback( - async (_mainCommand: string, _subCommand?: string, args?: string) => { + ( + _mainCommand: string, + _subCommand?: string, + args?: string, + ): SlashCommandActionReturn | void => { if (!args || args.trim() === '') { addMessage({ type: MessageType.ERROR, @@ -62,24 +75,20 @@ export const useSlashCommandProcessor = ( }); 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(), - }); - } + // 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, performMemoryRefresh], + [addMessage], ); const slashCommands: SlashCommand[] = useMemo( @@ -118,19 +127,19 @@ export const useSlashCommandProcessor = ( switch (subCommand) { case 'show': showMemoryAction(); - break; + return; // Explicitly return void case 'refresh': performMemoryRefresh(); - break; + return; // Explicitly return void case 'add': - addMemoryAction(mainCommand, subCommand, args); - break; + return addMemoryAction(mainCommand, subCommand, args); // Return the object default: addMessage({ type: MessageType.ERROR, content: `Unknown /memory command: ${subCommand}. Available: show, refresh, add`, timestamp: new Date(), }); + return; // Explicitly return void } }, }, @@ -187,7 +196,6 @@ Add any other context about the problem here. 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); @@ -203,7 +211,6 @@ Add any other context about the problem here. })(); }, }, - { name: 'quit', altName: 'exit', @@ -225,25 +232,21 @@ Add any other context about the problem here. addMemoryAction, addMessage, toggleCorgiMode, - config, // Added config to dependency array + config, cliVersion, ], ); const handleSlashCommand = useCallback( - (rawQuery: PartListUnion): boolean => { + (rawQuery: PartListUnion): SlashCommandActionReturn | 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; @@ -251,9 +254,8 @@ Add any other context about the problem here. const commandToMatch = (() => { if (trimmed.startsWith('?')) { - return 'help'; // No subCommand or args for '?' acting as help + return 'help'; } - // For other slash commands like /memory add foo const parts = trimmed.substring(1).trim().split(/\s+/); if (parts.length > 1) { subCommand = parts[1]; @@ -261,15 +263,21 @@ Add any other context about the problem here. if (parts.length > 2) { args = parts.slice(2).join(' '); } - return parts[0]; // This is the main command name + return parts[0]; })(); const mainCommand = commandToMatch; for (const cmd of slashCommands) { if (mainCommand === cmd.name || mainCommand === cmd.altName) { - cmd.action(mainCommand, subCommand, args); - return true; + const actionResult = 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 } } @@ -278,8 +286,7 @@ Add any other context about the problem here. content: `Unknown command: ${trimmed}`, timestamp: new Date(), }); - - return true; + return true; // Indicate command was processed (even if unknown) }, [addItem, slashCommands, addMessage], ); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 5684102b..8468e61b 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -57,7 +57,9 @@ export const useGeminiStream = ( setShowHelp: React.Dispatch>, config: Config, onDebugMessage: (message: string) => void, - handleSlashCommand: (cmd: PartListUnion) => boolean, + handleSlashCommand: ( + cmd: PartListUnion, + ) => import('./slashCommandProcessor.js').SlashCommandActionReturn | boolean, shellModeActive: boolean, ) => { const [initError, setInitError] = useState(null); @@ -138,9 +140,27 @@ export const useGeminiStream = ( await logger?.logMessage(MessageSenderType.USER, trimmedQuery); // Handle UI-only commands first - if (handleSlashCommand(trimmedQuery)) { + const slashCommandResult = handleSlashCommand(trimmedQuery); + if (typeof slashCommandResult === 'boolean' && slashCommandResult) { + // Command was handled, and it doesn't require a tool call from here return { queryToSend: null, shouldProceed: false }; + } else if ( + typeof slashCommandResult === 'object' && + slashCommandResult.shouldScheduleTool + ) { + // Slash command wants to schedule a tool call (e.g., /memory add) + const { toolName, toolArgs } = slashCommandResult; + if (toolName && toolArgs) { + const toolCallRequest: ToolCallRequestInfo = { + callId: `${toolName}-${Date.now()}-${Math.random().toString(16).slice(2)}`, + name: toolName, + args: toolArgs, + }; + schedule([toolCallRequest]); // schedule expects an array or single object + } + return { queryToSend: null, shouldProceed: false }; // Handled by scheduling the tool } + if (shellModeActive && handleShellCommand(trimmedQuery)) { return { queryToSend: null, shouldProceed: false }; }