summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/config/memoryUtils.test.ts92
-rw-r--r--packages/cli/src/config/memoryUtils.ts37
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.test.ts244
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts89
-rw-r--r--packages/cli/src/ui/hooks/useGeminiStream.ts24
5 files changed, 136 insertions, 350 deletions
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<typeof import('@gemini-code/server')>();
- 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<typeof fs>; // 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<void> {
- 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),
);
- });
- it('should show usage error if no text is provided', async () => {
- const { handleSlashCommand } = getProcessor();
- await act(async () => {
- handleSlashCommand('/memory add ');
+ expect(commandResult).toEqual({
+ shouldScheduleTool: true,
+ toolName: 'save_memory',
+ toolArgs: { fact },
});
- expect(performAddMemoryEntrySpy).not.toHaveBeenCalled();
- expect(mockAddItem).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- type: MessageType.ERROR,
- text: 'Usage: /memory add <text to remember>',
- }),
- expect.any(Number),
- );
+
+ // performMemoryRefresh is no longer called directly here
+ expect(mockPerformMemoryRefresh).not.toHaveBeenCalled();
});
- it('should handle error from MemoryTool.performAddMemoryEntry', async () => {
- const fact = 'Another fact';
- performAddMemoryEntrySpy.mockRejectedValue(
- new Error('[MemoryTool] Failed to add memory entry: Disk full'),
- );
+ 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 ${fact}`);
+ commandResult = handleSlashCommand('/memory add ');
});
- expect(performAddMemoryEntrySpy).toHaveBeenCalledWith(
- fact,
- memoryUtils.getGlobalMemoryFilePath(),
- {
- readFile: fsPromises.readFile,
- writeFile: fsPromises.writeFile,
- mkdir: fsPromises.mkdir,
- },
- );
+
expect(mockAddItem).toHaveBeenNthCalledWith(
- 2,
+ 2, // After user message
expect.objectContaining({
type: MessageType.ERROR,
- text: 'Failed to add memory: [MemoryTool] Failed to add memory entry: Disk full',
+ text: 'Usage: /memory add <text to remember>',
}),
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,
);
-
- await act(async () => {
- 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(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,
- );
+ let commandResult: SlashCommandActionReturn | boolean = false;
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}`);
+ commandResult = handleSlashCommand(`/bug ${bugDescription}`);
});
+ expect(mockAddItem).toHaveBeenCalledTimes(2);
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<string, unknown>;
+ 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<React.SetStateAction<boolean>>,
config: Config,
onDebugMessage: (message: string) => void,
- handleSlashCommand: (cmd: PartListUnion) => boolean,
+ handleSlashCommand: (
+ cmd: PartListUnion,
+ ) => import('./slashCommandProcessor.js').SlashCommandActionReturn | boolean,
shellModeActive: boolean,
) => {
const [initError, setInitError] = useState<string | null>(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 };
}