summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/services/CommandService.test.ts13
-rw-r--r--packages/cli/src/services/CommandService.ts2
-rw-r--r--packages/cli/src/ui/commands/chatCommand.test.ts277
-rw-r--r--packages/cli/src/ui/commands/chatCommand.ts197
-rw-r--r--packages/cli/src/ui/commands/types.ts15
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts167
6 files changed, 510 insertions, 161 deletions
diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts
index c309da34..b1f6e496 100644
--- a/packages/cli/src/services/CommandService.test.ts
+++ b/packages/cli/src/services/CommandService.test.ts
@@ -10,6 +10,7 @@ import { type SlashCommand } from '../ui/commands/types.js';
import { memoryCommand } from '../ui/commands/memoryCommand.js';
import { helpCommand } from '../ui/commands/helpCommand.js';
import { clearCommand } from '../ui/commands/clearCommand.js';
+import { chatCommand } from '../ui/commands/chatCommand.js';
import { authCommand } from '../ui/commands/authCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js';
import { statsCommand } from '../ui/commands/statsCommand.js';
@@ -47,6 +48,8 @@ vi.mock('../ui/commands/extensionsCommand.js', () => ({
}));
describe('CommandService', () => {
+ const subCommandLen = 10;
+
describe('when using default production loader', () => {
let commandService: CommandService;
@@ -70,13 +73,14 @@ describe('CommandService', () => {
const tree = commandService.getCommands();
// Post-condition assertions
- expect(tree.length).toBe(9);
+ expect(tree.length).toBe(subCommandLen);
const commandNames = tree.map((cmd) => cmd.name);
expect(commandNames).toContain('auth');
expect(commandNames).toContain('memory');
expect(commandNames).toContain('help');
expect(commandNames).toContain('clear');
+ expect(commandNames).toContain('chat');
expect(commandNames).toContain('theme');
expect(commandNames).toContain('stats');
expect(commandNames).toContain('privacy');
@@ -87,14 +91,14 @@ describe('CommandService', () => {
it('should overwrite any existing commands when called again', async () => {
// Load once
await commandService.loadCommands();
- expect(commandService.getCommands().length).toBe(9);
+ expect(commandService.getCommands().length).toBe(subCommandLen);
// Load again
await commandService.loadCommands();
const tree = commandService.getCommands();
// Should not append, but overwrite
- expect(tree.length).toBe(9);
+ expect(tree.length).toBe(subCommandLen);
});
});
@@ -106,10 +110,11 @@ describe('CommandService', () => {
await commandService.loadCommands();
const loadedTree = commandService.getCommands();
- expect(loadedTree.length).toBe(9);
+ expect(loadedTree.length).toBe(subCommandLen);
expect(loadedTree).toEqual([
aboutCommand,
authCommand,
+ chatCommand,
clearCommand,
extensionsCommand,
helpCommand,
diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts
index 379d0638..50f2c63a 100644
--- a/packages/cli/src/services/CommandService.ts
+++ b/packages/cli/src/services/CommandService.ts
@@ -10,6 +10,7 @@ import { helpCommand } from '../ui/commands/helpCommand.js';
import { clearCommand } from '../ui/commands/clearCommand.js';
import { authCommand } from '../ui/commands/authCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js';
+import { chatCommand } from '../ui/commands/chatCommand.js';
import { statsCommand } from '../ui/commands/statsCommand.js';
import { privacyCommand } from '../ui/commands/privacyCommand.js';
import { aboutCommand } from '../ui/commands/aboutCommand.js';
@@ -18,6 +19,7 @@ import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
const loadBuiltInCommands = async (): Promise<SlashCommand[]> => [
aboutCommand,
authCommand,
+ chatCommand,
clearCommand,
extensionsCommand,
helpCommand,
diff --git a/packages/cli/src/ui/commands/chatCommand.test.ts b/packages/cli/src/ui/commands/chatCommand.test.ts
new file mode 100644
index 00000000..5318c330
--- /dev/null
+++ b/packages/cli/src/ui/commands/chatCommand.test.ts
@@ -0,0 +1,277 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ vi,
+ describe,
+ it,
+ expect,
+ beforeEach,
+ afterEach,
+ Mocked,
+} from 'vitest';
+
+import {
+ type CommandContext,
+ MessageActionReturn,
+ SlashCommand,
+} from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+import { Content } from '@google/genai';
+import { GeminiClient } from '@google/gemini-cli-core';
+
+import * as fsPromises from 'fs/promises';
+import { chatCommand } from './chatCommand.js';
+import { Stats } from 'fs';
+import { HistoryItemWithoutId } from '../types.js';
+
+vi.mock('fs/promises', () => ({
+ stat: vi.fn(),
+ readdir: vi.fn().mockResolvedValue(['file1.txt', 'file2.txt'] as string[]),
+}));
+
+describe('chatCommand', () => {
+ const mockFs = fsPromises as Mocked<typeof fsPromises>;
+
+ let mockContext: CommandContext;
+ let mockGetChat: ReturnType<typeof vi.fn>;
+ let mockSaveCheckpoint: ReturnType<typeof vi.fn>;
+ let mockLoadCheckpoint: ReturnType<typeof vi.fn>;
+ let mockGetHistory: ReturnType<typeof vi.fn>;
+
+ const getSubCommand = (name: 'list' | 'save' | 'resume'): SlashCommand => {
+ const subCommand = chatCommand.subCommands?.find(
+ (cmd) => cmd.name === name,
+ );
+ if (!subCommand) {
+ throw new Error(`/memory ${name} command not found.`);
+ }
+ return subCommand;
+ };
+
+ beforeEach(() => {
+ mockGetHistory = vi.fn().mockReturnValue([]);
+ mockGetChat = vi.fn().mockResolvedValue({
+ getHistory: mockGetHistory,
+ });
+ mockSaveCheckpoint = vi.fn().mockResolvedValue(undefined);
+ mockLoadCheckpoint = vi.fn().mockResolvedValue([]);
+
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getProjectTempDir: () => '/tmp/gemini',
+ getGeminiClient: () =>
+ ({
+ getChat: mockGetChat,
+ }) as unknown as GeminiClient,
+ },
+ logger: {
+ saveCheckpoint: mockSaveCheckpoint,
+ loadCheckpoint: mockLoadCheckpoint,
+ initialize: vi.fn().mockResolvedValue(undefined),
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('should have the correct main command definition', () => {
+ expect(chatCommand.name).toBe('chat');
+ expect(chatCommand.description).toBe('Manage conversation history.');
+ expect(chatCommand.subCommands).toHaveLength(3);
+ });
+
+ describe('list subcommand', () => {
+ let listCommand: SlashCommand;
+
+ beforeEach(() => {
+ listCommand = getSubCommand('list');
+ });
+
+ it('should inform when no checkpoints are found', async () => {
+ mockFs.readdir.mockImplementation(
+ (async (_: string): Promise<string[]> =>
+ [] as string[]) as unknown as typeof fsPromises.readdir,
+ );
+ const result = await listCommand?.action?.(mockContext, '');
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'info',
+ content: 'No saved conversation checkpoints found.',
+ });
+ });
+
+ it('should list found checkpoints', async () => {
+ const fakeFiles = ['checkpoint-test1.json', 'checkpoint-test2.json'];
+ const date = new Date();
+
+ mockFs.readdir.mockImplementation(
+ (async (_: string): Promise<string[]> =>
+ fakeFiles as string[]) as unknown as typeof fsPromises.readdir,
+ );
+ mockFs.stat.mockImplementation((async (path: string): Promise<Stats> => {
+ if (path.endsWith('test1.json')) {
+ return { mtime: date } as Stats;
+ }
+ return { mtime: new Date(date.getTime() + 1000) } as Stats;
+ }) as unknown as typeof fsPromises.stat);
+
+ const result = (await listCommand?.action?.(
+ mockContext,
+ '',
+ )) as MessageActionReturn;
+
+ const content = result?.content ?? '';
+ expect(result?.type).toBe('message');
+ expect(content).toContain('List of saved conversations:');
+ const index1 = content.indexOf('- \u001b[36mtest1\u001b[0m');
+ const index2 = content.indexOf('- \u001b[36mtest2\u001b[0m');
+ expect(index1).toBeGreaterThanOrEqual(0);
+ expect(index2).toBeGreaterThan(index1);
+ });
+ });
+ describe('save subcommand', () => {
+ let saveCommand: SlashCommand;
+ const tag = 'my-tag';
+ beforeEach(() => {
+ saveCommand = getSubCommand('save');
+ });
+
+ it('should return an error if tag is missing', async () => {
+ const result = await saveCommand?.action?.(mockContext, ' ');
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'error',
+ content: 'Missing tag. Usage: /chat save <tag>',
+ });
+ });
+
+ it('should inform if conversation history is empty', async () => {
+ mockGetHistory.mockReturnValue([]);
+ const result = await saveCommand?.action?.(mockContext, tag);
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'info',
+ content: 'No conversation found to save.',
+ });
+ });
+
+ it('should save the conversation', async () => {
+ const history: HistoryItemWithoutId[] = [
+ {
+ type: 'user',
+ text: 'hello',
+ },
+ ];
+ mockGetHistory.mockReturnValue(history);
+ const result = await saveCommand?.action?.(mockContext, tag);
+
+ expect(mockSaveCheckpoint).toHaveBeenCalledWith(history, tag);
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'info',
+ content: `Conversation checkpoint saved with tag: ${tag}.`,
+ });
+ });
+ });
+
+ describe('resume subcommand', () => {
+ const goodTag = 'good-tag';
+ const badTag = 'bad-tag';
+
+ let resumeCommand: SlashCommand;
+ beforeEach(() => {
+ resumeCommand = getSubCommand('resume');
+ });
+
+ it('should return an error if tag is missing', async () => {
+ const result = await resumeCommand?.action?.(mockContext, '');
+
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'error',
+ content: 'Missing tag. Usage: /chat resume <tag>',
+ });
+ });
+
+ it('should inform if checkpoint is not found', async () => {
+ mockLoadCheckpoint.mockResolvedValue([]);
+
+ const result = await resumeCommand?.action?.(mockContext, badTag);
+
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'info',
+ content: `No saved checkpoint found with tag: ${badTag}.`,
+ });
+ });
+
+ it('should resume a conversation', async () => {
+ const conversation: Content[] = [
+ { role: 'user', parts: [{ text: 'hello gemini' }] },
+ { role: 'model', parts: [{ text: 'hello world' }] },
+ ];
+ mockLoadCheckpoint.mockResolvedValue(conversation);
+
+ const result = await resumeCommand?.action?.(mockContext, goodTag);
+
+ expect(result).toEqual({
+ type: 'load_history',
+ history: [
+ { type: 'user', text: 'hello gemini' },
+ { type: 'gemini', text: 'hello world' },
+ ] as HistoryItemWithoutId[],
+ clientHistory: conversation,
+ });
+ });
+
+ describe('completion', () => {
+ it('should provide completion suggestions', async () => {
+ const fakeFiles = ['checkpoint-alpha.json', 'checkpoint-beta.json'];
+ mockFs.readdir.mockImplementation(
+ (async (_: string): Promise<string[]> =>
+ fakeFiles as string[]) as unknown as typeof fsPromises.readdir,
+ );
+
+ mockFs.stat.mockImplementation(
+ (async (_: string): Promise<Stats> =>
+ ({
+ mtime: new Date(),
+ }) as Stats) as unknown as typeof fsPromises.stat,
+ );
+
+ const result = await resumeCommand?.completion?.(mockContext, 'a');
+
+ expect(result).toEqual(['alpha']);
+ });
+
+ it('should suggest filenames sorted by modified time (newest first)', async () => {
+ const fakeFiles = ['checkpoint-test1.json', 'checkpoint-test2.json'];
+ const date = new Date();
+ mockFs.readdir.mockImplementation(
+ (async (_: string): Promise<string[]> =>
+ fakeFiles as string[]) as unknown as typeof fsPromises.readdir,
+ );
+ mockFs.stat.mockImplementation((async (
+ path: string,
+ ): Promise<Stats> => {
+ if (path.endsWith('test1.json')) {
+ return { mtime: date } as Stats;
+ }
+ return { mtime: new Date(date.getTime() + 1000) } as Stats;
+ }) as unknown as typeof fsPromises.stat);
+
+ const result = await resumeCommand?.completion?.(mockContext, '');
+ // Sort items by last modified time (newest first)
+ expect(result).toEqual(['test2', 'test1']);
+ });
+ });
+ });
+});
diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts
new file mode 100644
index 00000000..fd56afbd
--- /dev/null
+++ b/packages/cli/src/ui/commands/chatCommand.ts
@@ -0,0 +1,197 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as fsPromises from 'fs/promises';
+import { CommandContext, SlashCommand, MessageActionReturn } from './types.js';
+import path from 'path';
+import { HistoryItemWithoutId, MessageType } from '../types.js';
+
+interface ChatDetail {
+ name: string;
+ mtime: Date;
+}
+
+const getSavedChatTags = async (
+ context: CommandContext,
+ mtSortDesc: boolean,
+): Promise<ChatDetail[]> => {
+ const geminiDir = context.services.config?.getProjectTempDir();
+ if (!geminiDir) {
+ return [];
+ }
+ try {
+ const file_head = 'checkpoint-';
+ const file_tail = '.json';
+ const files = await fsPromises.readdir(geminiDir);
+ const chatDetails: Array<{ name: string; mtime: Date }> = [];
+
+ for (const file of files) {
+ if (file.startsWith(file_head) && file.endsWith(file_tail)) {
+ const filePath = path.join(geminiDir, file);
+ const stats = await fsPromises.stat(filePath);
+ chatDetails.push({
+ name: file.slice(file_head.length, -file_tail.length),
+ mtime: stats.mtime,
+ });
+ }
+ }
+
+ chatDetails.sort((a, b) =>
+ mtSortDesc
+ ? b.mtime.getTime() - a.mtime.getTime()
+ : a.mtime.getTime() - b.mtime.getTime(),
+ );
+
+ return chatDetails;
+ } catch (_err) {
+ return [];
+ }
+};
+
+const listCommand: SlashCommand = {
+ name: 'list',
+ description: 'List saved conversation checkpoints',
+ action: async (context): Promise<MessageActionReturn> => {
+ const chatDetails = await getSavedChatTags(context, false);
+ if (chatDetails.length === 0) {
+ return {
+ type: 'message',
+ messageType: 'info',
+ content: 'No saved conversation checkpoints found.',
+ };
+ }
+
+ let message = 'List of saved conversations:\n\n';
+ for (const chat of chatDetails) {
+ message += ` - \u001b[36m${chat.name}\u001b[0m\n`;
+ }
+ message += `\n\u001b[90mNote: Newest last, oldest first\u001b[0m`;
+ return {
+ type: 'message',
+ messageType: 'info',
+ content: message,
+ };
+ },
+};
+
+const saveCommand: SlashCommand = {
+ name: 'save',
+ description:
+ 'Save the current conversation as a checkpoint. Usage: /chat save <tag>',
+ action: async (context, args): Promise<MessageActionReturn> => {
+ const tag = args.trim();
+ if (!tag) {
+ return {
+ type: 'message',
+ messageType: 'error',
+ content: 'Missing tag. Usage: /chat save <tag>',
+ };
+ }
+
+ const { logger, config } = context.services;
+ await logger.initialize();
+ const chat = await config?.getGeminiClient()?.getChat();
+ if (!chat) {
+ return {
+ type: 'message',
+ messageType: 'error',
+ content: 'No chat client available to save conversation.',
+ };
+ }
+
+ const history = chat.getHistory();
+ if (history.length > 0) {
+ await logger.saveCheckpoint(history, tag);
+ return {
+ type: 'message',
+ messageType: 'info',
+ content: `Conversation checkpoint saved with tag: ${tag}.`,
+ };
+ } else {
+ return {
+ type: 'message',
+ messageType: 'info',
+ content: 'No conversation found to save.',
+ };
+ }
+ },
+};
+
+const resumeCommand: SlashCommand = {
+ name: 'resume',
+ altName: 'load',
+ description:
+ 'Resume a conversation from a checkpoint. Usage: /chat resume <tag>',
+ action: async (context, args) => {
+ const tag = args.trim();
+ if (!tag) {
+ return {
+ type: 'message',
+ messageType: 'error',
+ content: 'Missing tag. Usage: /chat resume <tag>',
+ };
+ }
+
+ const { logger } = context.services;
+ await logger.initialize();
+ const conversation = await logger.loadCheckpoint(tag);
+
+ if (conversation.length === 0) {
+ return {
+ type: 'message',
+ messageType: 'info',
+ content: `No saved checkpoint found with tag: ${tag}.`,
+ };
+ }
+
+ const rolemap: { [key: string]: MessageType } = {
+ user: MessageType.USER,
+ model: MessageType.GEMINI,
+ };
+
+ const uiHistory: HistoryItemWithoutId[] = [];
+ let hasSystemPrompt = false;
+ let i = 0;
+
+ for (const item of conversation) {
+ i += 1;
+ const text =
+ item.parts
+ ?.filter((m) => !!m.text)
+ .map((m) => m.text)
+ .join('') || '';
+ if (!text) {
+ continue;
+ }
+ if (i === 1 && text.match(/context for our chat/)) {
+ hasSystemPrompt = true;
+ }
+ if (i > 2 || !hasSystemPrompt) {
+ uiHistory.push({
+ type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
+ text,
+ } as HistoryItemWithoutId);
+ }
+ }
+ return {
+ type: 'load_history',
+ history: uiHistory,
+ clientHistory: conversation,
+ };
+ },
+ completion: async (context, partialArg) => {
+ const chatDetails = await getSavedChatTags(context, true);
+ return chatDetails
+ .map((chat) => chat.name)
+ .filter((name) => name.startsWith(partialArg));
+ },
+};
+
+export const chatCommand: SlashCommand = {
+ name: 'chat',
+ description: 'Manage conversation history.',
+ subCommands: [listCommand, saveCommand, resumeCommand],
+};
diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts
index 68b22543..27db2be2 100644
--- a/packages/cli/src/ui/commands/types.ts
+++ b/packages/cli/src/ui/commands/types.ts
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import { Content } from '@google/genai';
+import { HistoryItemWithoutId } from '../types.js';
import { Config, GitService, Logger } from '@google/gemini-cli-core';
import { LoadedSettings } from '../../config/settings.js';
import { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
@@ -69,10 +71,21 @@ export interface OpenDialogActionReturn {
dialog: 'help' | 'auth' | 'theme' | 'privacy';
}
+/**
+ * The return type for a command action that results in replacing
+ * the entire conversation history.
+ */
+export interface LoadHistoryActionReturn {
+ type: 'load_history';
+ history: HistoryItemWithoutId[];
+ clientHistory: Content[]; // The history for the generative client
+}
+
export type SlashCommandActionReturn =
| ToolActionReturn
| MessageActionReturn
- | OpenDialogActionReturn;
+ | OpenDialogActionReturn
+ | LoadHistoryActionReturn;
// The standardized contract for any command in the system.
export interface SlashCommand {
name: string;
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index 139de06e..181c4980 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -198,23 +198,6 @@ export const useSlashCommandProcessor = (
load();
}, [commandService]);
- const savedChatTags = useCallback(async () => {
- const geminiDir = config?.getProjectTempDir();
- if (!geminiDir) {
- return [];
- }
- try {
- const files = await fs.readdir(geminiDir);
- return files
- .filter(
- (file) => file.startsWith('checkpoint-') && file.endsWith('.json'),
- )
- .map((file) => file.replace('checkpoint-', '').replace('.json', ''));
- } catch (_err) {
- return [];
- }
- }, [config]);
-
// Define legacy commands
// This list contains all commands that have NOT YET been migrated to the
// new system. As commands are migrated, they are removed from this list.
@@ -588,142 +571,7 @@ export const useSlashCommandProcessor = (
})();
},
},
- {
- name: 'chat',
- description:
- 'Manage conversation history. Usage: /chat <list|save|resume> <tag>',
- action: async (_mainCommand, subCommand, args) => {
- const tag = (args || '').trim();
- const logger = new Logger(config?.getSessionId() || '');
- await logger.initialize();
- const chat = await config?.getGeminiClient()?.getChat();
- if (!chat) {
- addMessage({
- type: MessageType.ERROR,
- content: 'No chat client available for conversation status.',
- timestamp: new Date(),
- });
- return;
- }
- if (!subCommand) {
- addMessage({
- type: MessageType.ERROR,
- content: 'Missing command\nUsage: /chat <list|save|resume> <tag>',
- timestamp: new Date(),
- });
- return;
- }
- switch (subCommand) {
- case 'save': {
- if (!tag) {
- addMessage({
- type: MessageType.ERROR,
- content: 'Missing tag. Usage: /chat save <tag>',
- timestamp: new Date(),
- });
- return;
- }
- const history = chat.getHistory();
- if (history.length > 0) {
- await logger.saveCheckpoint(chat?.getHistory() || [], tag);
- addMessage({
- type: MessageType.INFO,
- content: `Conversation checkpoint saved with tag: ${tag}.`,
- timestamp: new Date(),
- });
- } else {
- addMessage({
- type: MessageType.INFO,
- content: 'No conversation found to save.',
- timestamp: new Date(),
- });
- }
- return;
- }
- case 'resume':
- case 'restore':
- case 'load': {
- if (!tag) {
- addMessage({
- type: MessageType.ERROR,
- content: 'Missing tag. Usage: /chat resume <tag>',
- timestamp: new Date(),
- });
- return;
- }
- const conversation = await logger.loadCheckpoint(tag);
- if (conversation.length === 0) {
- addMessage({
- type: MessageType.INFO,
- content: `No saved checkpoint found with tag: ${tag}.`,
- timestamp: new Date(),
- });
- return;
- }
-
- clearItems();
- chat.clearHistory();
- const rolemap: { [key: string]: MessageType } = {
- user: MessageType.USER,
- model: MessageType.GEMINI,
- };
- let hasSystemPrompt = false;
- let i = 0;
- for (const item of conversation) {
- i += 1;
-
- // Add each item to history regardless of whether we display
- // it.
- chat.addHistory(item);
- const text =
- item.parts
- ?.filter((m) => !!m.text)
- .map((m) => m.text)
- .join('') || '';
- if (!text) {
- // Parsing Part[] back to various non-text output not yet implemented.
- continue;
- }
- if (i === 1 && text.match(/context for our chat/)) {
- hasSystemPrompt = true;
- }
- if (i > 2 || !hasSystemPrompt) {
- addItem(
- {
- type:
- (item.role && rolemap[item.role]) || MessageType.GEMINI,
- text,
- } as HistoryItemWithoutId,
- i,
- );
- }
- }
- console.clear();
- refreshStatic();
- return;
- }
- case 'list':
- addMessage({
- type: MessageType.INFO,
- content:
- 'list of saved conversations: ' +
- (await savedChatTags()).join(', '),
- timestamp: new Date(),
- });
- return;
- default:
- addMessage({
- type: MessageType.ERROR,
- content: `Unknown /chat command: ${subCommand}. Available: list, save, resume`,
- timestamp: new Date(),
- });
- return;
- }
- },
- completion: async () =>
- (await savedChatTags()).map((tag) => 'resume ' + tag),
- },
{
name: 'quit',
altName: 'exit',
@@ -932,18 +780,14 @@ export const useSlashCommandProcessor = (
addMessage,
openEditorDialog,
toggleCorgiMode,
- savedChatTags,
config,
showToolDescriptions,
session,
gitService,
loadHistory,
- addItem,
setQuittingMessages,
pendingCompressionItemRef,
setPendingCompressionItem,
- clearItems,
- refreshStatic,
]);
const handleSlashCommand = useCallback(
@@ -1041,6 +885,16 @@ export const useSlashCommandProcessor = (
);
}
}
+ case 'load_history': {
+ await config
+ ?.getGeminiClient()
+ ?.setHistory(result.clientHistory);
+ commandContext.ui.clear();
+ result.history.forEach((item, index) => {
+ commandContext.ui.addItem(item, index);
+ });
+ return { type: 'handled' };
+ }
default: {
const unhandled: never = result;
throw new Error(`Unhandled slash command result: ${unhandled}`);
@@ -1109,6 +963,7 @@ export const useSlashCommandProcessor = (
return { type: 'handled' };
},
[
+ config,
addItem,
setShowHelp,
openAuthDialog,