826 lines
27 KiB
TypeScript
826 lines
27 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { useCallback, useMemo } from 'react';
|
|
import { type PartListUnion } from '@google/genai';
|
|
import open from 'open';
|
|
import process from 'node:process';
|
|
import { UseHistoryManagerReturn } from './useHistoryManager.js';
|
|
import {
|
|
Config,
|
|
GitService,
|
|
Logger,
|
|
MCPDiscoveryState,
|
|
MCPServerStatus,
|
|
getMCPDiscoveryState,
|
|
getMCPServerStatus,
|
|
} from '@gemini-cli/core';
|
|
import { useSessionStats } from '../contexts/SessionContext.js';
|
|
import {
|
|
Message,
|
|
MessageType,
|
|
HistoryItemWithoutId,
|
|
HistoryItem,
|
|
} from '../types.js';
|
|
import { promises as fs } from 'fs';
|
|
import path from 'path';
|
|
import { createShowMemoryAction } from './useShowMemoryCommand.js';
|
|
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
|
import { formatDuration, formatMemoryUsage } from '../utils/formatters.js';
|
|
import { getCliVersion } from '../../utils/version.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
|
|
| SlashCommandActionReturn
|
|
| Promise<void | SlashCommandActionReturn>; // Action can now return this object
|
|
}
|
|
|
|
/**
|
|
* Hook to define and process slash commands (e.g., /help, /clear).
|
|
*/
|
|
export const useSlashCommandProcessor = (
|
|
config: Config | null,
|
|
history: HistoryItem[],
|
|
addItem: UseHistoryManagerReturn['addItem'],
|
|
clearItems: UseHistoryManagerReturn['clearItems'],
|
|
loadHistory: UseHistoryManagerReturn['loadHistory'],
|
|
refreshStatic: () => void,
|
|
setShowHelp: React.Dispatch<React.SetStateAction<boolean>>,
|
|
onDebugMessage: (message: string) => void,
|
|
openThemeDialog: () => void,
|
|
performMemoryRefresh: () => Promise<void>,
|
|
toggleCorgiMode: () => void,
|
|
showToolDescriptions: boolean = false,
|
|
setQuittingMessages: (message: HistoryItem[]) => void,
|
|
) => {
|
|
const session = useSessionStats();
|
|
const gitService = useMemo(() => {
|
|
if (!config?.getProjectRoot()) {
|
|
return;
|
|
}
|
|
return new GitService(config.getProjectRoot());
|
|
}, [config]);
|
|
|
|
const addMessage = useCallback(
|
|
(message: Message) => {
|
|
// Convert Message to HistoryItemWithoutId
|
|
let historyItemContent: HistoryItemWithoutId;
|
|
if (message.type === MessageType.ABOUT) {
|
|
historyItemContent = {
|
|
type: 'about',
|
|
cliVersion: message.cliVersion,
|
|
osVersion: message.osVersion,
|
|
sandboxEnv: message.sandboxEnv,
|
|
modelVersion: message.modelVersion,
|
|
};
|
|
} else if (message.type === MessageType.STATS) {
|
|
historyItemContent = {
|
|
type: 'stats',
|
|
stats: message.stats,
|
|
lastTurnStats: message.lastTurnStats,
|
|
duration: message.duration,
|
|
};
|
|
} else if (message.type === MessageType.QUIT) {
|
|
historyItemContent = {
|
|
type: 'quit',
|
|
stats: message.stats,
|
|
duration: message.duration,
|
|
};
|
|
} else {
|
|
historyItemContent = {
|
|
type: message.type as
|
|
| MessageType.INFO
|
|
| MessageType.ERROR
|
|
| MessageType.USER,
|
|
text: message.content,
|
|
};
|
|
}
|
|
addItem(historyItemContent, message.timestamp.getTime());
|
|
},
|
|
[addItem],
|
|
);
|
|
|
|
const showMemoryAction = useCallback(async () => {
|
|
const actionFn = createShowMemoryAction(config, addMessage);
|
|
await actionFn();
|
|
}, [config, addMessage]);
|
|
|
|
const addMemoryAction = useCallback(
|
|
(
|
|
_mainCommand: string,
|
|
_subCommand?: string,
|
|
args?: string,
|
|
): SlashCommandActionReturn | void => {
|
|
if (!args || args.trim() === '') {
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: 'Usage: /memory add <text to remember>',
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
// 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],
|
|
);
|
|
|
|
const slashCommands: SlashCommand[] = useMemo(() => {
|
|
const commands: SlashCommand[] = [
|
|
{
|
|
name: 'help',
|
|
altName: '?',
|
|
description: 'for help on gemini-cli',
|
|
action: (_mainCommand, _subCommand, _args) => {
|
|
onDebugMessage('Opening help.');
|
|
setShowHelp(true);
|
|
},
|
|
},
|
|
{
|
|
name: 'clear',
|
|
description: 'clear the screen',
|
|
action: (_mainCommand, _subCommand, _args) => {
|
|
onDebugMessage('Clearing terminal.');
|
|
clearItems();
|
|
console.clear();
|
|
refreshStatic();
|
|
},
|
|
},
|
|
{
|
|
name: 'theme',
|
|
description: 'change the theme',
|
|
action: (_mainCommand, _subCommand, _args) => {
|
|
openThemeDialog();
|
|
},
|
|
},
|
|
{
|
|
name: 'stats',
|
|
altName: 'usage',
|
|
description: 'check session stats',
|
|
action: (_mainCommand, _subCommand, _args) => {
|
|
const now = new Date();
|
|
const { sessionStartTime, cumulative, currentTurn } = session.stats;
|
|
const wallDuration = now.getTime() - sessionStartTime.getTime();
|
|
|
|
addMessage({
|
|
type: MessageType.STATS,
|
|
stats: cumulative,
|
|
lastTurnStats: currentTurn,
|
|
duration: formatDuration(wallDuration),
|
|
timestamp: new Date(),
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'mcp',
|
|
description: 'list configured MCP servers and tools',
|
|
action: async (_mainCommand, _subCommand, _args) => {
|
|
// Check if the _subCommand includes a specific flag to control description visibility
|
|
let useShowDescriptions = showToolDescriptions;
|
|
if (_subCommand === 'desc' || _subCommand === 'descriptions') {
|
|
useShowDescriptions = true;
|
|
} else if (
|
|
_subCommand === 'nodesc' ||
|
|
_subCommand === 'nodescriptions'
|
|
) {
|
|
useShowDescriptions = false;
|
|
} else if (_args === 'desc' || _args === 'descriptions') {
|
|
useShowDescriptions = true;
|
|
} else if (_args === 'nodesc' || _args === 'nodescriptions') {
|
|
useShowDescriptions = false;
|
|
}
|
|
|
|
const toolRegistry = await config?.getToolRegistry();
|
|
if (!toolRegistry) {
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: 'Could not retrieve tool registry.',
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const mcpServers = config?.getMcpServers() || {};
|
|
const serverNames = Object.keys(mcpServers);
|
|
|
|
if (serverNames.length === 0) {
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: 'No MCP servers configured.',
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Check if any servers are still connecting
|
|
const connectingServers = serverNames.filter(
|
|
(name) => getMCPServerStatus(name) === MCPServerStatus.CONNECTING,
|
|
);
|
|
const discoveryState = getMCPDiscoveryState();
|
|
|
|
let message = '';
|
|
|
|
// Add overall discovery status message if needed
|
|
if (
|
|
discoveryState === MCPDiscoveryState.IN_PROGRESS ||
|
|
connectingServers.length > 0
|
|
) {
|
|
message += `\u001b[33m⏳ MCP servers are starting up (${connectingServers.length} initializing)...\u001b[0m\n`;
|
|
message += `\u001b[90mNote: First startup may take longer. Tool availability will update automatically.\u001b[0m\n\n`;
|
|
}
|
|
|
|
message += 'Configured MCP servers:\n\n';
|
|
|
|
for (const serverName of serverNames) {
|
|
const serverTools = toolRegistry.getToolsByServer(serverName);
|
|
const status = getMCPServerStatus(serverName);
|
|
|
|
// Add status indicator with descriptive text
|
|
let statusIndicator = '';
|
|
let statusText = '';
|
|
switch (status) {
|
|
case MCPServerStatus.CONNECTED:
|
|
statusIndicator = '🟢';
|
|
statusText = 'Ready';
|
|
break;
|
|
case MCPServerStatus.CONNECTING:
|
|
statusIndicator = '🔄';
|
|
statusText = 'Starting... (first startup may take longer)';
|
|
break;
|
|
case MCPServerStatus.DISCONNECTED:
|
|
default:
|
|
statusIndicator = '🔴';
|
|
statusText = 'Disconnected';
|
|
break;
|
|
}
|
|
|
|
// Get server description if available
|
|
const server = mcpServers[serverName];
|
|
|
|
// Format server header with bold formatting and status
|
|
message += `${statusIndicator} \u001b[1m${serverName}\u001b[0m - ${statusText}`;
|
|
|
|
// Add tool count with conditional messaging
|
|
if (status === MCPServerStatus.CONNECTED) {
|
|
message += ` (${serverTools.length} tools)`;
|
|
} else if (status === MCPServerStatus.CONNECTING) {
|
|
message += ` (tools will appear when ready)`;
|
|
} else {
|
|
message += ` (${serverTools.length} tools cached)`;
|
|
}
|
|
|
|
// Add server description with proper handling of multi-line descriptions
|
|
if (useShowDescriptions && server?.description) {
|
|
const greenColor = '\u001b[32m';
|
|
const resetColor = '\u001b[0m';
|
|
|
|
const descLines = server.description.split('\n');
|
|
message += `: ${greenColor}${descLines[0]}${resetColor}`;
|
|
message += '\n';
|
|
|
|
// If there are multiple lines, add proper indentation for each line
|
|
if (descLines.length > 1) {
|
|
for (let i = 1; i < descLines.length; i++) {
|
|
// Skip empty lines at the end
|
|
if (i === descLines.length - 1 && descLines[i].trim() === '')
|
|
continue;
|
|
message += ` ${greenColor}${descLines[i]}${resetColor}\n`;
|
|
}
|
|
}
|
|
} else {
|
|
message += '\n';
|
|
}
|
|
|
|
// Reset formatting after server entry
|
|
message += '\u001b[0m';
|
|
|
|
if (serverTools.length > 0) {
|
|
serverTools.forEach((tool) => {
|
|
if (useShowDescriptions && tool.description) {
|
|
// Format tool name in cyan using simple ANSI cyan color
|
|
message += ` - \u001b[36m${tool.name}\u001b[0m: `;
|
|
|
|
// Apply green color to the description text
|
|
const greenColor = '\u001b[32m';
|
|
const resetColor = '\u001b[0m';
|
|
|
|
// Handle multi-line descriptions by properly indenting and preserving formatting
|
|
const descLines = tool.description.split('\n');
|
|
message += `${greenColor}${descLines[0]}${resetColor}\n`;
|
|
|
|
// If there are multiple lines, add proper indentation for each line
|
|
if (descLines.length > 1) {
|
|
for (let i = 1; i < descLines.length; i++) {
|
|
// Skip empty lines at the end
|
|
if (
|
|
i === descLines.length - 1 &&
|
|
descLines[i].trim() === ''
|
|
)
|
|
continue;
|
|
message += ` ${greenColor}${descLines[i]}${resetColor}\n`;
|
|
}
|
|
}
|
|
// Reset is handled inline with each line now
|
|
} else {
|
|
// Use cyan color for the tool name even when not showing descriptions
|
|
message += ` - \u001b[36m${tool.name}\u001b[0m\n`;
|
|
}
|
|
});
|
|
} else {
|
|
message += ' No tools available\n';
|
|
}
|
|
message += '\n';
|
|
}
|
|
|
|
// Make sure to reset any ANSI formatting at the end to prevent it from affecting the terminal
|
|
message += '\u001b[0m';
|
|
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: message,
|
|
timestamp: new Date(),
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'memory',
|
|
description:
|
|
'manage memory. Usage: /memory <show|refresh|add> [text for add]',
|
|
action: (mainCommand, subCommand, args) => {
|
|
switch (subCommand) {
|
|
case 'show':
|
|
showMemoryAction();
|
|
return; // Explicitly return void
|
|
case 'refresh':
|
|
performMemoryRefresh();
|
|
return; // Explicitly return void
|
|
case 'add':
|
|
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
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'tools',
|
|
description: 'list available Gemini CLI tools',
|
|
action: async (_mainCommand, _subCommand, _args) => {
|
|
const toolRegistry = await config?.getToolRegistry();
|
|
const tools = toolRegistry?.getAllTools();
|
|
if (!tools) {
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: 'Could not retrieve tools.',
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Filter out MCP tools by checking if they have a serverName property
|
|
const geminiTools = tools.filter((tool) => !('serverName' in tool));
|
|
const geminiToolList = geminiTools.map((tool) => tool.displayName);
|
|
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: `Available Gemini CLI tools:\n\n${geminiToolList.join('\n')}`,
|
|
timestamp: new Date(),
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'corgi',
|
|
action: (_mainCommand, _subCommand, _args) => {
|
|
toggleCorgiMode();
|
|
},
|
|
},
|
|
{
|
|
name: 'about',
|
|
description: 'show version info',
|
|
action: (_mainCommand, _subCommand, _args) => {
|
|
const osVersion = process.platform;
|
|
let sandboxEnv = 'no sandbox';
|
|
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
|
sandboxEnv = process.env.SANDBOX;
|
|
} else if (process.env.SANDBOX === 'sandbox-exec') {
|
|
sandboxEnv = `sandbox-exec (${
|
|
process.env.SEATBELT_PROFILE || 'unknown'
|
|
})`;
|
|
}
|
|
const modelVersion = config?.getModel() || 'Unknown';
|
|
const cliVersion = getCliVersion();
|
|
addMessage({
|
|
type: MessageType.ABOUT,
|
|
timestamp: new Date(),
|
|
cliVersion,
|
|
osVersion,
|
|
sandboxEnv,
|
|
modelVersion,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'bug',
|
|
description: 'submit a bug report',
|
|
action: (_mainCommand, _subCommand, args) => {
|
|
let bugDescription = _subCommand || '';
|
|
if (args) {
|
|
bugDescription += ` ${args}`;
|
|
}
|
|
bugDescription = bugDescription.trim();
|
|
|
|
const osVersion = `${process.platform} ${process.version}`;
|
|
let sandboxEnv = 'no sandbox';
|
|
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
|
sandboxEnv = process.env.SANDBOX.replace(/^gemini-(?:code-)?/, '');
|
|
} else if (process.env.SANDBOX === 'sandbox-exec') {
|
|
sandboxEnv = `sandbox-exec (${
|
|
process.env.SEATBELT_PROFILE || 'unknown'
|
|
})`;
|
|
}
|
|
const modelVersion = config?.getModel() || 'Unknown';
|
|
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
|
|
const cliVersion = getCliVersion();
|
|
|
|
const diagnosticInfo = `
|
|
## Describe the bug
|
|
A clear and concise description of what the bug is.
|
|
|
|
## Additional context
|
|
Add any other context about the problem here.
|
|
|
|
## Diagnostic Information
|
|
* **CLI Version:** ${cliVersion}
|
|
* **Git Commit:** ${GIT_COMMIT_INFO}
|
|
* **Operating System:** ${osVersion}
|
|
* **Sandbox Environment:** ${sandboxEnv}
|
|
* **Model Version:** ${modelVersion}
|
|
* **Memory Usage:** ${memoryUsage}
|
|
`;
|
|
|
|
let bugReportUrl =
|
|
'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md';
|
|
if (bugDescription) {
|
|
const encodedArgs = encodeURIComponent(bugDescription);
|
|
bugReportUrl += `&title=${encodedArgs}`;
|
|
}
|
|
const encodedBody = encodeURIComponent(diagnosticInfo);
|
|
bugReportUrl += `&body=${encodedBody}`;
|
|
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: `To submit your bug report, please open the following URL in your browser:\n${bugReportUrl}`,
|
|
timestamp: new Date(),
|
|
});
|
|
(async () => {
|
|
try {
|
|
await open(bugReportUrl);
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: `Could not open URL in browser: ${errorMessage}`,
|
|
timestamp: new Date(),
|
|
});
|
|
}
|
|
})();
|
|
},
|
|
},
|
|
{
|
|
name: 'save',
|
|
description: 'save conversation checkpoint. Usage: /save [tag]',
|
|
action: async (_mainCommand, subCommand, _args) => {
|
|
const tag = (subCommand || '').trim();
|
|
const logger = new Logger(config?.getSessionId() || '');
|
|
await logger.initialize();
|
|
const chat = await config?.getGeminiClient()?.getChat();
|
|
const history = chat?.getHistory() || [];
|
|
if (history.length > 0) {
|
|
await logger.saveCheckpoint(chat?.getHistory() || [], tag);
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: `Conversation checkpoint saved${tag ? ' with tag: ' + tag : ''}.`,
|
|
timestamp: new Date(),
|
|
});
|
|
} else {
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: 'No conversation found to save.',
|
|
timestamp: new Date(),
|
|
});
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'resume',
|
|
description:
|
|
'resume from conversation checkpoint. Usage: /resume [tag]',
|
|
action: async (_mainCommand, subCommand, _args) => {
|
|
const tag = (subCommand || '').trim();
|
|
const logger = new Logger(config?.getSessionId() || '');
|
|
await logger.initialize();
|
|
const conversation = await logger.loadCheckpoint(tag);
|
|
if (conversation.length === 0) {
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: `No saved checkpoint found${tag ? ' with tag: ' + tag : ''}.`,
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
const chat = await config?.getGeminiClient()?.getChat();
|
|
if (!chat) {
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: 'No chat client available to resume conversation.',
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
clearItems();
|
|
chat.clearHistory();
|
|
const rolemap: { [key: string]: MessageType } = {
|
|
user: MessageType.USER,
|
|
model: MessageType.GEMINI,
|
|
};
|
|
let i = 0;
|
|
for (const item of conversation) {
|
|
i += 1;
|
|
const text =
|
|
item.parts
|
|
?.filter((m) => !!m.text)
|
|
.map((m) => m.text)
|
|
.join('') || '';
|
|
if (i <= 2) {
|
|
// Skip system prompt back and forth.
|
|
continue;
|
|
}
|
|
if (!text) {
|
|
// Parsing Part[] back to various non-text output not yet implemented.
|
|
continue;
|
|
}
|
|
addItem(
|
|
{
|
|
type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
|
|
text,
|
|
} as HistoryItemWithoutId,
|
|
i,
|
|
);
|
|
chat.addHistory(item);
|
|
}
|
|
console.clear();
|
|
refreshStatic();
|
|
},
|
|
},
|
|
{
|
|
name: 'quit',
|
|
altName: 'exit',
|
|
description: 'exit the cli',
|
|
action: async (mainCommand, _subCommand, _args) => {
|
|
const now = new Date();
|
|
const { sessionStartTime, cumulative } = session.stats;
|
|
const wallDuration = now.getTime() - sessionStartTime.getTime();
|
|
|
|
setQuittingMessages([
|
|
{
|
|
type: 'user',
|
|
text: `/${mainCommand}`,
|
|
id: now.getTime() - 1,
|
|
},
|
|
{
|
|
type: 'quit',
|
|
stats: cumulative,
|
|
duration: formatDuration(wallDuration),
|
|
id: now.getTime(),
|
|
},
|
|
]);
|
|
|
|
setTimeout(() => {
|
|
process.exit(0);
|
|
}, 100);
|
|
},
|
|
},
|
|
];
|
|
|
|
if (config?.getCheckpointEnabled()) {
|
|
commands.push({
|
|
name: 'restore',
|
|
description:
|
|
'restore a tool call. This will reset the conversation and file history to the state it was in when the tool call was suggested',
|
|
action: async (_mainCommand, subCommand, _args) => {
|
|
const checkpointDir = config?.getGeminiDir()
|
|
? path.join(config.getGeminiDir(), 'checkpoints')
|
|
: undefined;
|
|
|
|
if (!checkpointDir) {
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: 'Could not determine the .gemini directory path.',
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Ensure the directory exists before trying to read it.
|
|
await fs.mkdir(checkpointDir, { recursive: true });
|
|
const files = await fs.readdir(checkpointDir);
|
|
const jsonFiles = files.filter((file) => file.endsWith('.json'));
|
|
|
|
if (!subCommand) {
|
|
if (jsonFiles.length === 0) {
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: 'No restorable tool calls found.',
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
const truncatedFiles = jsonFiles.map((file) => {
|
|
const components = file.split('.');
|
|
if (components.length <= 1) {
|
|
return file;
|
|
}
|
|
components.pop();
|
|
return components.join('.');
|
|
});
|
|
const fileList = truncatedFiles.join('\n');
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: `Available tool calls to restore:\n\n${fileList}`,
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const selectedFile = subCommand.endsWith('.json')
|
|
? subCommand
|
|
: `${subCommand}.json`;
|
|
|
|
if (!jsonFiles.includes(selectedFile)) {
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: `File not found: ${selectedFile}`,
|
|
timestamp: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const filePath = path.join(checkpointDir, selectedFile);
|
|
const data = await fs.readFile(filePath, 'utf-8');
|
|
const toolCallData = JSON.parse(data);
|
|
|
|
if (toolCallData.history) {
|
|
loadHistory(toolCallData.history);
|
|
}
|
|
|
|
if (toolCallData.clientHistory) {
|
|
await config
|
|
?.getGeminiClient()
|
|
?.setHistory(toolCallData.clientHistory);
|
|
}
|
|
|
|
if (toolCallData.commitHash) {
|
|
await gitService?.restoreProjectFromSnapshot(
|
|
toolCallData.commitHash,
|
|
);
|
|
addMessage({
|
|
type: MessageType.INFO,
|
|
content: `Restored project to the state before the tool call.`,
|
|
timestamp: new Date(),
|
|
});
|
|
}
|
|
|
|
return {
|
|
shouldScheduleTool: true,
|
|
toolName: toolCallData.toolCall.name,
|
|
toolArgs: toolCallData.toolCall.args,
|
|
};
|
|
} catch (error) {
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: `Could not read restorable tool calls. This is the error: ${error}`,
|
|
timestamp: new Date(),
|
|
});
|
|
}
|
|
},
|
|
});
|
|
}
|
|
return commands;
|
|
}, [
|
|
onDebugMessage,
|
|
setShowHelp,
|
|
refreshStatic,
|
|
openThemeDialog,
|
|
clearItems,
|
|
performMemoryRefresh,
|
|
showMemoryAction,
|
|
addMemoryAction,
|
|
addMessage,
|
|
toggleCorgiMode,
|
|
config,
|
|
showToolDescriptions,
|
|
session,
|
|
gitService,
|
|
loadHistory,
|
|
addItem,
|
|
setQuittingMessages,
|
|
]);
|
|
|
|
const handleSlashCommand = useCallback(
|
|
async (
|
|
rawQuery: PartListUnion,
|
|
): Promise<SlashCommandActionReturn | boolean> => {
|
|
if (typeof rawQuery !== 'string') {
|
|
return false;
|
|
}
|
|
const trimmed = rawQuery.trim();
|
|
if (!trimmed.startsWith('/') && !trimmed.startsWith('?')) {
|
|
return false;
|
|
}
|
|
const userMessageTimestamp = Date.now();
|
|
if (trimmed !== '/quit' && trimmed !== '/exit') {
|
|
addItem(
|
|
{ type: MessageType.USER, text: trimmed },
|
|
userMessageTimestamp,
|
|
);
|
|
}
|
|
|
|
let subCommand: string | undefined;
|
|
let args: string | undefined;
|
|
|
|
const commandToMatch = (() => {
|
|
if (trimmed.startsWith('?')) {
|
|
return 'help';
|
|
}
|
|
const parts = trimmed.substring(1).trim().split(/\s+/);
|
|
if (parts.length > 1) {
|
|
subCommand = parts[1];
|
|
}
|
|
if (parts.length > 2) {
|
|
args = parts.slice(2).join(' ');
|
|
}
|
|
return parts[0];
|
|
})();
|
|
|
|
const mainCommand = commandToMatch;
|
|
|
|
for (const cmd of slashCommands) {
|
|
if (mainCommand === cmd.name || mainCommand === cmd.altName) {
|
|
const actionResult = await 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
|
|
}
|
|
}
|
|
|
|
addMessage({
|
|
type: MessageType.ERROR,
|
|
content: `Unknown command: ${trimmed}`,
|
|
timestamp: new Date(),
|
|
});
|
|
return true; // Indicate command was processed (even if unknown)
|
|
},
|
|
[addItem, slashCommands, addMessage],
|
|
);
|
|
|
|
return { handleSlashCommand, slashCommands };
|
|
};
|