2025-04-29 13:29:57 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
import { useCallback, useMemo, useEffect, useState } from 'react';
|
2025-04-29 13:29:57 -07:00
|
|
|
import { type PartListUnion } from '@google/genai';
|
2025-05-30 22:18:01 +00:00
|
|
|
import process from 'node:process';
|
2025-05-06 16:20:28 -07:00
|
|
|
import { UseHistoryManagerReturn } from './useHistoryManager.js';
|
2025-06-13 21:21:40 -07:00
|
|
|
import { useStateAndRef } from './useStateAndRef.js';
|
2025-07-15 22:35:05 -04:00
|
|
|
import { Config, GitService, Logger } from '@google/gemini-cli-core';
|
2025-06-09 20:25:37 -04:00
|
|
|
import { useSessionStats } from '../contexts/SessionContext.js';
|
2025-06-11 15:33:09 -04:00
|
|
|
import {
|
|
|
|
|
Message,
|
|
|
|
|
MessageType,
|
|
|
|
|
HistoryItemWithoutId,
|
|
|
|
|
HistoryItem,
|
2025-07-07 16:45:44 -04:00
|
|
|
SlashCommandProcessorResult,
|
2025-06-11 15:33:09 -04:00
|
|
|
} from '../types.js';
|
|
|
|
|
import { promises as fs } from 'fs';
|
|
|
|
|
import path from 'path';
|
2025-07-16 21:46:35 -04:00
|
|
|
import { formatDuration } from '../utils/formatters.js';
|
2025-06-21 12:15:43 -07:00
|
|
|
import { LoadedSettings } from '../../config/settings.js';
|
2025-07-07 16:45:44 -04:00
|
|
|
import {
|
|
|
|
|
type CommandContext,
|
|
|
|
|
type SlashCommandActionReturn,
|
|
|
|
|
type SlashCommand,
|
|
|
|
|
} from '../commands/types.js';
|
|
|
|
|
import { CommandService } from '../../services/CommandService.js';
|
|
|
|
|
|
|
|
|
|
// This interface is for the old, inline command definitions.
|
|
|
|
|
// It will be removed once all commands are migrated to the new system.
|
|
|
|
|
export interface LegacySlashCommand {
|
2025-05-06 16:20:28 -07:00
|
|
|
name: string;
|
|
|
|
|
altName?: string;
|
2025-05-17 21:57:27 -07:00
|
|
|
description?: string;
|
2025-06-15 11:40:39 -07:00
|
|
|
completion?: () => Promise<string[]>;
|
2025-05-23 08:47:19 -07:00
|
|
|
action: (
|
|
|
|
|
mainCommand: string,
|
|
|
|
|
subCommand?: string,
|
|
|
|
|
args?: string,
|
2025-06-11 15:33:09 -04:00
|
|
|
) =>
|
|
|
|
|
| void
|
|
|
|
|
| SlashCommandActionReturn
|
2025-07-07 16:45:44 -04:00
|
|
|
| Promise<void | SlashCommandActionReturn>;
|
2025-04-29 13:29:57 -07:00
|
|
|
}
|
|
|
|
|
|
2025-05-06 16:20:28 -07:00
|
|
|
/**
|
|
|
|
|
* Hook to define and process slash commands (e.g., /help, /clear).
|
|
|
|
|
*/
|
2025-04-29 13:29:57 -07:00
|
|
|
export const useSlashCommandProcessor = (
|
2025-05-21 13:31:18 -07:00
|
|
|
config: Config | null,
|
2025-06-21 12:15:43 -07:00
|
|
|
settings: LoadedSettings,
|
2025-06-11 15:33:09 -04:00
|
|
|
history: HistoryItem[],
|
2025-05-06 16:20:28 -07:00
|
|
|
addItem: UseHistoryManagerReturn['addItem'],
|
|
|
|
|
clearItems: UseHistoryManagerReturn['clearItems'],
|
2025-06-11 15:33:09 -04:00
|
|
|
loadHistory: UseHistoryManagerReturn['loadHistory'],
|
2025-05-05 17:52:29 +00:00
|
|
|
refreshStatic: () => void,
|
2025-05-05 20:48:34 +00:00
|
|
|
setShowHelp: React.Dispatch<React.SetStateAction<boolean>>,
|
2025-05-13 23:55:49 +00:00
|
|
|
onDebugMessage: (message: string) => void,
|
2025-04-30 22:26:28 +00:00
|
|
|
openThemeDialog: () => void,
|
2025-06-19 16:52:22 -07:00
|
|
|
openAuthDialog: () => void,
|
2025-06-12 02:21:54 +01:00
|
|
|
openEditorDialog: () => void,
|
2025-05-17 21:57:27 -07:00
|
|
|
toggleCorgiMode: () => void,
|
2025-06-11 20:08:32 -04:00
|
|
|
setQuittingMessages: (message: HistoryItem[]) => void,
|
2025-06-27 12:07:38 -07:00
|
|
|
openPrivacyNotice: () => void,
|
2025-04-29 13:29:57 -07:00
|
|
|
) => {
|
2025-06-09 20:25:37 -04:00
|
|
|
const session = useSessionStats();
|
2025-07-07 16:45:44 -04:00
|
|
|
const [commands, setCommands] = useState<SlashCommand[]>([]);
|
2025-06-11 15:33:09 -04:00
|
|
|
const gitService = useMemo(() => {
|
|
|
|
|
if (!config?.getProjectRoot()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
return new GitService(config.getProjectRoot());
|
|
|
|
|
}, [config]);
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
const logger = useMemo(() => {
|
|
|
|
|
const l = new Logger(config?.getSessionId() || '');
|
|
|
|
|
// The logger's initialize is async, but we can create the instance
|
|
|
|
|
// synchronously. Commands that use it will await its initialization.
|
|
|
|
|
return l;
|
|
|
|
|
}, [config]);
|
|
|
|
|
|
2025-06-13 21:21:40 -07:00
|
|
|
const [pendingCompressionItemRef, setPendingCompressionItem] =
|
|
|
|
|
useStateAndRef<HistoryItemWithoutId | null>(null);
|
2025-07-07 16:45:44 -04:00
|
|
|
|
|
|
|
|
const pendingHistoryItems = useMemo(() => {
|
|
|
|
|
const items: HistoryItemWithoutId[] = [];
|
|
|
|
|
if (pendingCompressionItemRef.current != null) {
|
|
|
|
|
items.push(pendingCompressionItemRef.current);
|
|
|
|
|
}
|
|
|
|
|
return items;
|
|
|
|
|
}, [pendingCompressionItemRef]);
|
2025-06-13 21:21:40 -07:00
|
|
|
|
2025-05-14 12:37:17 -07:00
|
|
|
const addMessage = useCallback(
|
|
|
|
|
(message: Message) => {
|
2025-05-23 10:34:15 -07:00
|
|
|
// 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,
|
2025-06-27 08:46:27 -07:00
|
|
|
selectedAuthType: message.selectedAuthType,
|
|
|
|
|
gcpProject: message.gcpProject,
|
2025-05-23 10:34:15 -07:00
|
|
|
};
|
2025-06-10 15:59:52 -04:00
|
|
|
} else if (message.type === MessageType.STATS) {
|
|
|
|
|
historyItemContent = {
|
|
|
|
|
type: 'stats',
|
|
|
|
|
duration: message.duration,
|
|
|
|
|
};
|
2025-06-29 20:44:33 -04:00
|
|
|
} else if (message.type === MessageType.MODEL_STATS) {
|
|
|
|
|
historyItemContent = {
|
|
|
|
|
type: 'model_stats',
|
|
|
|
|
};
|
|
|
|
|
} else if (message.type === MessageType.TOOL_STATS) {
|
|
|
|
|
historyItemContent = {
|
|
|
|
|
type: 'tool_stats',
|
|
|
|
|
};
|
2025-06-11 16:40:31 -04:00
|
|
|
} else if (message.type === MessageType.QUIT) {
|
|
|
|
|
historyItemContent = {
|
|
|
|
|
type: 'quit',
|
|
|
|
|
duration: message.duration,
|
|
|
|
|
};
|
2025-06-13 21:21:40 -07:00
|
|
|
} else if (message.type === MessageType.COMPRESSION) {
|
|
|
|
|
historyItemContent = {
|
|
|
|
|
type: 'compression',
|
|
|
|
|
compression: message.compression,
|
|
|
|
|
};
|
2025-05-23 10:34:15 -07:00
|
|
|
} else {
|
|
|
|
|
historyItemContent = {
|
2025-06-30 04:06:03 +09:00
|
|
|
type: message.type,
|
2025-05-23 10:34:15 -07:00
|
|
|
text: message.content,
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-05-14 12:37:17 -07:00
|
|
|
addItem(historyItemContent, message.timestamp.getTime());
|
|
|
|
|
},
|
|
|
|
|
[addItem],
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
const commandContext = useMemo(
|
|
|
|
|
(): CommandContext => ({
|
|
|
|
|
services: {
|
|
|
|
|
config,
|
|
|
|
|
settings,
|
|
|
|
|
git: gitService,
|
|
|
|
|
logger,
|
|
|
|
|
},
|
|
|
|
|
ui: {
|
|
|
|
|
addItem,
|
|
|
|
|
clear: () => {
|
|
|
|
|
clearItems();
|
|
|
|
|
console.clear();
|
|
|
|
|
refreshStatic();
|
|
|
|
|
},
|
|
|
|
|
setDebugMessage: onDebugMessage,
|
2025-07-15 21:59:16 -04:00
|
|
|
pendingItem: pendingCompressionItemRef.current,
|
|
|
|
|
setPendingItem: setPendingCompressionItem,
|
2025-07-07 16:45:44 -04:00
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
stats: session.stats,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
[
|
|
|
|
|
config,
|
|
|
|
|
settings,
|
|
|
|
|
gitService,
|
|
|
|
|
logger,
|
|
|
|
|
addItem,
|
|
|
|
|
clearItems,
|
|
|
|
|
refreshStatic,
|
|
|
|
|
session.stats,
|
|
|
|
|
onDebugMessage,
|
2025-07-15 21:59:16 -04:00
|
|
|
pendingCompressionItemRef,
|
|
|
|
|
setPendingCompressionItem,
|
2025-07-07 16:45:44 -04:00
|
|
|
],
|
2025-05-16 16:36:50 -07:00
|
|
|
);
|
|
|
|
|
|
2025-07-16 18:36:14 -04:00
|
|
|
const commandService = useMemo(() => new CommandService(config), [config]);
|
2025-07-07 16:45:44 -04:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const load = async () => {
|
|
|
|
|
await commandService.loadCommands();
|
|
|
|
|
setCommands(commandService.getCommands());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
load();
|
|
|
|
|
}, [commandService]);
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
const legacyCommands: LegacySlashCommand[] = useMemo(() => {
|
|
|
|
|
const commands: LegacySlashCommand[] = [
|
|
|
|
|
// `/help` and `/clear` have been migrated and REMOVED from this list.
|
2025-05-17 21:57:27 -07:00
|
|
|
{
|
|
|
|
|
name: 'corgi',
|
|
|
|
|
action: (_mainCommand, _subCommand, _args) => {
|
|
|
|
|
toggleCorgiMode();
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-05-06 14:48:49 -07:00
|
|
|
{
|
|
|
|
|
name: 'quit',
|
|
|
|
|
altName: 'exit',
|
2025-05-14 16:01:29 -07:00
|
|
|
description: 'exit the cli',
|
2025-06-11 20:08:32 -04:00
|
|
|
action: async (mainCommand, _subCommand, _args) => {
|
2025-06-11 16:40:31 -04:00
|
|
|
const now = new Date();
|
2025-06-29 20:44:33 -04:00
|
|
|
const { sessionStartTime } = session.stats;
|
2025-06-11 16:40:31 -04:00
|
|
|
const wallDuration = now.getTime() - sessionStartTime.getTime();
|
|
|
|
|
|
2025-06-11 20:08:32 -04:00
|
|
|
setQuittingMessages([
|
|
|
|
|
{
|
|
|
|
|
type: 'user',
|
|
|
|
|
text: `/${mainCommand}`,
|
|
|
|
|
id: now.getTime() - 1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'quit',
|
|
|
|
|
duration: formatDuration(wallDuration),
|
|
|
|
|
id: now.getTime(),
|
|
|
|
|
},
|
|
|
|
|
]);
|
2025-06-11 16:40:31 -04:00
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}, 100);
|
2025-05-06 14:48:49 -07:00
|
|
|
},
|
2025-04-29 13:29:57 -07:00
|
|
|
},
|
2025-06-11 15:33:09 -04:00
|
|
|
];
|
|
|
|
|
|
2025-06-20 00:39:15 -04:00
|
|
|
if (config?.getCheckpointingEnabled()) {
|
2025-06-11 15:33:09 -04:00
|
|
|
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',
|
2025-06-20 01:18:11 -04:00
|
|
|
completion: async () => {
|
|
|
|
|
const checkpointDir = config?.getProjectTempDir()
|
|
|
|
|
? path.join(config.getProjectTempDir(), 'checkpoints')
|
|
|
|
|
: undefined;
|
|
|
|
|
if (!checkpointDir) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const files = await fs.readdir(checkpointDir);
|
|
|
|
|
return files
|
|
|
|
|
.filter((file) => file.endsWith('.json'))
|
|
|
|
|
.map((file) => file.replace('.json', ''));
|
|
|
|
|
} catch (_err) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-06-11 15:33:09 -04:00
|
|
|
action: async (_mainCommand, subCommand, _args) => {
|
2025-06-20 00:39:15 -04:00
|
|
|
const checkpointDir = config?.getProjectTempDir()
|
|
|
|
|
? path.join(config.getProjectTempDir(), 'checkpoints')
|
2025-06-11 15:33:09 -04:00
|
|
|
: 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 {
|
2025-07-07 16:45:44 -04:00
|
|
|
type: 'tool',
|
2025-06-11 15:33:09 -04:00
|
|
|
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;
|
|
|
|
|
}, [
|
2025-07-07 16:45:44 -04:00
|
|
|
addMessage,
|
2025-06-11 15:33:09 -04:00
|
|
|
toggleCorgiMode,
|
|
|
|
|
config,
|
|
|
|
|
session,
|
|
|
|
|
gitService,
|
|
|
|
|
loadHistory,
|
2025-06-11 20:08:32 -04:00
|
|
|
setQuittingMessages,
|
2025-06-11 15:33:09 -04:00
|
|
|
]);
|
2025-04-29 13:29:57 -07:00
|
|
|
|
|
|
|
|
const handleSlashCommand = useCallback(
|
2025-06-11 15:33:09 -04:00
|
|
|
async (
|
|
|
|
|
rawQuery: PartListUnion,
|
2025-07-07 16:45:44 -04:00
|
|
|
): Promise<SlashCommandProcessorResult | false> => {
|
2025-04-29 13:29:57 -07:00
|
|
|
if (typeof rawQuery !== 'string') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-07-07 16:45:44 -04:00
|
|
|
|
2025-04-30 00:26:07 +00:00
|
|
|
const trimmed = rawQuery.trim();
|
2025-05-16 16:36:50 -07:00
|
|
|
if (!trimmed.startsWith('/') && !trimmed.startsWith('?')) {
|
2025-04-30 00:26:07 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-07-07 16:45:44 -04:00
|
|
|
|
2025-05-06 16:20:28 -07:00
|
|
|
const userMessageTimestamp = Date.now();
|
2025-06-11 20:08:32 -04:00
|
|
|
if (trimmed !== '/quit' && trimmed !== '/exit') {
|
|
|
|
|
addItem(
|
|
|
|
|
{ type: MessageType.USER, text: trimmed },
|
|
|
|
|
userMessageTimestamp,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-05-06 16:20:28 -07:00
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
const parts = trimmed.substring(1).trim().split(/\s+/);
|
|
|
|
|
const commandPath = parts.filter((p) => p); // The parts of the command, e.g., ['memory', 'add']
|
2025-05-16 16:36:50 -07:00
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
// --- Start of New Tree Traversal Logic ---
|
|
|
|
|
|
|
|
|
|
let currentCommands = commands;
|
|
|
|
|
let commandToExecute: SlashCommand | undefined;
|
|
|
|
|
let pathIndex = 0;
|
|
|
|
|
|
|
|
|
|
for (const part of commandPath) {
|
|
|
|
|
const foundCommand = currentCommands.find(
|
|
|
|
|
(cmd) => cmd.name === part || cmd.altName === part,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (foundCommand) {
|
|
|
|
|
commandToExecute = foundCommand;
|
|
|
|
|
pathIndex++;
|
|
|
|
|
if (foundCommand.subCommands) {
|
|
|
|
|
currentCommands = foundCommand.subCommands;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
2025-05-16 16:36:50 -07:00
|
|
|
}
|
2025-07-07 16:45:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (commandToExecute) {
|
|
|
|
|
const args = parts.slice(pathIndex).join(' ');
|
|
|
|
|
|
|
|
|
|
if (commandToExecute.action) {
|
|
|
|
|
const result = await commandToExecute.action(commandContext, args);
|
|
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
|
switch (result.type) {
|
|
|
|
|
case 'tool':
|
|
|
|
|
return {
|
|
|
|
|
type: 'schedule_tool',
|
|
|
|
|
toolName: result.toolName,
|
|
|
|
|
toolArgs: result.toolArgs,
|
|
|
|
|
};
|
|
|
|
|
case 'message':
|
|
|
|
|
addItem(
|
|
|
|
|
{
|
|
|
|
|
type:
|
|
|
|
|
result.messageType === 'error'
|
|
|
|
|
? MessageType.ERROR
|
|
|
|
|
: MessageType.INFO,
|
|
|
|
|
text: result.content,
|
|
|
|
|
},
|
|
|
|
|
Date.now(),
|
|
|
|
|
);
|
|
|
|
|
return { type: 'handled' };
|
|
|
|
|
case 'dialog':
|
|
|
|
|
switch (result.dialog) {
|
|
|
|
|
case 'help':
|
|
|
|
|
setShowHelp(true);
|
|
|
|
|
return { type: 'handled' };
|
2025-07-14 12:22:37 -04:00
|
|
|
case 'auth':
|
|
|
|
|
openAuthDialog();
|
|
|
|
|
return { type: 'handled' };
|
2025-07-11 16:01:28 -04:00
|
|
|
case 'theme':
|
|
|
|
|
openThemeDialog();
|
|
|
|
|
return { type: 'handled' };
|
2025-07-16 20:27:36 -04:00
|
|
|
case 'editor':
|
|
|
|
|
openEditorDialog();
|
|
|
|
|
return { type: 'handled' };
|
2025-07-15 01:45:06 -04:00
|
|
|
case 'privacy':
|
|
|
|
|
openPrivacyNotice();
|
|
|
|
|
return { type: 'handled' };
|
2025-07-07 16:45:44 -04:00
|
|
|
default: {
|
|
|
|
|
const unhandled: never = result.dialog;
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Unhandled slash command result: ${unhandled}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-16 08:47:56 +08:00
|
|
|
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' };
|
|
|
|
|
}
|
2025-07-07 16:45:44 -04:00
|
|
|
default: {
|
|
|
|
|
const unhandled: never = result;
|
|
|
|
|
throw new Error(`Unhandled slash command result: ${unhandled}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { type: 'handled' };
|
|
|
|
|
} else if (commandToExecute.subCommands) {
|
|
|
|
|
const helpText = `Command '/${commandToExecute.name}' requires a subcommand. Available:\n${commandToExecute.subCommands
|
|
|
|
|
.map((sc) => ` - ${sc.name}: ${sc.description || ''}`)
|
|
|
|
|
.join('\n')}`;
|
|
|
|
|
addMessage({
|
|
|
|
|
type: MessageType.INFO,
|
|
|
|
|
content: helpText,
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
});
|
|
|
|
|
return { type: 'handled' };
|
2025-05-16 16:36:50 -07:00
|
|
|
}
|
2025-07-07 16:45:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- End of New Tree Traversal Logic ---
|
2025-05-16 16:36:50 -07:00
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
// --- Legacy Fallback Logic (for commands not yet migrated) ---
|
2025-05-16 16:36:50 -07:00
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
const mainCommand = parts[0];
|
|
|
|
|
const subCommand = parts[1];
|
|
|
|
|
const legacyArgs = parts.slice(2).join(' ');
|
|
|
|
|
|
|
|
|
|
for (const cmd of legacyCommands) {
|
2025-05-16 16:36:50 -07:00
|
|
|
if (mainCommand === cmd.name || mainCommand === cmd.altName) {
|
2025-07-07 16:45:44 -04:00
|
|
|
const actionResult = await cmd.action(
|
|
|
|
|
mainCommand,
|
|
|
|
|
subCommand,
|
|
|
|
|
legacyArgs,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (actionResult?.type === 'tool') {
|
|
|
|
|
return {
|
|
|
|
|
type: 'schedule_tool',
|
|
|
|
|
toolName: actionResult.toolName,
|
|
|
|
|
toolArgs: actionResult.toolArgs,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (actionResult?.type === 'message') {
|
|
|
|
|
addItem(
|
|
|
|
|
{
|
|
|
|
|
type:
|
|
|
|
|
actionResult.messageType === 'error'
|
|
|
|
|
? MessageType.ERROR
|
|
|
|
|
: MessageType.INFO,
|
|
|
|
|
text: actionResult.content,
|
|
|
|
|
},
|
|
|
|
|
Date.now(),
|
|
|
|
|
);
|
2025-05-23 08:47:19 -07:00
|
|
|
}
|
2025-07-07 16:45:44 -04:00
|
|
|
return { type: 'handled' };
|
2025-04-29 13:29:57 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 16:36:50 -07:00
|
|
|
addMessage({
|
|
|
|
|
type: MessageType.ERROR,
|
|
|
|
|
content: `Unknown command: ${trimmed}`,
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
});
|
2025-07-07 16:45:44 -04:00
|
|
|
return { type: 'handled' };
|
2025-04-29 13:29:57 -07:00
|
|
|
},
|
2025-07-07 16:45:44 -04:00
|
|
|
[
|
2025-07-16 08:47:56 +08:00
|
|
|
config,
|
2025-07-07 16:45:44 -04:00
|
|
|
addItem,
|
|
|
|
|
setShowHelp,
|
2025-07-14 12:22:37 -04:00
|
|
|
openAuthDialog,
|
2025-07-07 16:45:44 -04:00
|
|
|
commands,
|
|
|
|
|
legacyCommands,
|
|
|
|
|
commandContext,
|
|
|
|
|
addMessage,
|
2025-07-11 16:01:28 -04:00
|
|
|
openThemeDialog,
|
2025-07-15 01:45:06 -04:00
|
|
|
openPrivacyNotice,
|
2025-07-16 20:27:36 -04:00
|
|
|
openEditorDialog,
|
2025-07-07 16:45:44 -04:00
|
|
|
],
|
2025-04-29 13:29:57 -07:00
|
|
|
);
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
const allCommands = useMemo(() => {
|
|
|
|
|
// Adapt legacy commands to the new SlashCommand interface
|
|
|
|
|
const adaptedLegacyCommands: SlashCommand[] = legacyCommands.map(
|
|
|
|
|
(legacyCmd) => ({
|
|
|
|
|
name: legacyCmd.name,
|
|
|
|
|
altName: legacyCmd.altName,
|
|
|
|
|
description: legacyCmd.description,
|
|
|
|
|
action: async (_context: CommandContext, args: string) => {
|
|
|
|
|
const parts = args.split(/\s+/);
|
|
|
|
|
const subCommand = parts[0] || undefined;
|
|
|
|
|
const restOfArgs = parts.slice(1).join(' ') || undefined;
|
|
|
|
|
|
|
|
|
|
return legacyCmd.action(legacyCmd.name, subCommand, restOfArgs);
|
|
|
|
|
},
|
|
|
|
|
completion: legacyCmd.completion
|
|
|
|
|
? async (_context: CommandContext, _partialArg: string) =>
|
|
|
|
|
legacyCmd.completion!()
|
|
|
|
|
: undefined,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const newCommandNames = new Set(commands.map((c) => c.name));
|
|
|
|
|
const filteredAdaptedLegacy = adaptedLegacyCommands.filter(
|
|
|
|
|
(c) => !newCommandNames.has(c.name),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [...commands, ...filteredAdaptedLegacy];
|
|
|
|
|
}, [commands, legacyCommands]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
handleSlashCommand,
|
|
|
|
|
slashCommands: allCommands,
|
|
|
|
|
pendingHistoryItems,
|
|
|
|
|
commandContext,
|
|
|
|
|
};
|
2025-04-29 13:29:57 -07:00
|
|
|
};
|