Refactor(cli): Move memory add logic to server tool call (#493)
This commit is contained in:
parent
70277591c4
commit
f8c4276e69
|
@ -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
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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 <text to remember>',
|
||||
}),
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue