Download packages/cli/src/ui/commands/chatCommand.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/commands/chatCommand.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/commands/chatCommand.ts
-
curl -L -o chatCommand.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/commands/chatCommand.ts
12.1 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as fsPromises from 'node:fs/promises'; | |
| import React from 'react'; | |
| import { Text } from 'ink'; | |
| import { theme } from '../semantic-colors.js'; | |
| import type { | |
| CommandContext, | |
| SlashCommand, | |
| SlashCommandActionReturn, | |
| } from './types.js'; | |
| import { CommandKind } from './types.js'; | |
| import { | |
| decodeTagName, | |
| type MessageActionReturn, | |
| INITIAL_HISTORY_LENGTH, | |
| } from '@google/gemini-cli-core'; | |
| import path from 'node:path'; | |
| import type { | |
| HistoryItemWithoutId, | |
| HistoryItemChatList, | |
| ChatDetail, | |
| } from '../types.js'; | |
| import { MessageType } from '../types.js'; | |
| import { exportHistoryToFile } from '../utils/historyExportUtils.js'; | |
| import { convertToRestPayload } from '@google/gemini-cli-core'; | |
| const CHECKPOINT_MENU_GROUP = 'checkpoints'; | |
| const getSavedChatTags = async ( | |
| context: CommandContext, | |
| mtSortDesc: boolean, | |
| ): Promise<ChatDetail[]> => { | |
| const cfg = context.services.agentContext?.config; | |
| const geminiDir = cfg?.storage?.getProjectTempDir(); | |
| if (!geminiDir) { | |
| return []; | |
| } | |
| try { | |
| const file_head = 'checkpoint-'; | |
| const file_tail = '.json'; | |
| const files = await fsPromises.readdir(geminiDir); | |
| const chatDetails: ChatDetail[] = []; | |
| 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); | |
| const tagName = file.slice(file_head.length, -file_tail.length); | |
| chatDetails.push({ | |
| name: decodeTagName(tagName), | |
| mtime: stats.mtime.toISOString(), | |
| }); | |
| } | |
| } | |
| chatDetails.sort((a, b) => | |
| mtSortDesc | |
| ? b.mtime.localeCompare(a.mtime) | |
| : a.mtime.localeCompare(b.mtime), | |
| ); | |
| return chatDetails; | |
| } catch { | |
| return []; | |
| } | |
| }; | |
| const listCommand: SlashCommand = { | |
| name: 'list', | |
| description: 'List saved manual conversation checkpoints', | |
| kind: CommandKind.BUILT_IN, | |
| autoExecute: true, | |
| takesArgs: false, | |
| action: async (context): Promise<void> => { | |
| const chatDetails = await getSavedChatTags(context, false); | |
| const item: HistoryItemChatList = { | |
| type: MessageType.CHAT_LIST, | |
| chats: chatDetails, | |
| }; | |
| context.ui.addItem(item); | |
| }, | |
| }; | |
| const saveCommand: SlashCommand = { | |
| name: 'save', | |
| description: | |
| 'Save the current conversation as a checkpoint. Usage: /resume save <tag>', | |
| kind: CommandKind.BUILT_IN, | |
| autoExecute: false, | |
| action: async (context, args): Promise<SlashCommandActionReturn | void> => { | |
| const tag = args.trim(); | |
| if (!tag) { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: 'Missing tag. Usage: /resume save <tag>', | |
| }; | |
| } | |
| const { logger } = context.services; | |
| const config = context.services.agentContext?.config; | |
| await logger.initialize(); | |
| if (!context.overwriteConfirmed) { | |
| const exists = await logger.checkpointExists(tag); | |
| if (exists) { | |
| return { | |
| type: 'confirm_action', | |
| prompt: React.createElement( | |
| Text, | |
| null, | |
| 'A checkpoint with the tag ', | |
| React.createElement(Text, { color: theme.text.accent }, tag), | |
| ' already exists. Do you want to overwrite it?', | |
| ), | |
| originalInvocation: { | |
| raw: context.invocation?.raw || `/resume save ${tag}`, | |
| }, | |
| }; | |
| } | |
| } | |
| const chat = context.services.agentContext?.geminiClient?.getChat(); | |
| if (!chat) { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: 'No chat client available to save conversation.', | |
| }; | |
| } | |
| const history = chat.getHistory(); | |
| if (history.length > INITIAL_HISTORY_LENGTH) { | |
| const authType = config?.getContentGeneratorConfig()?.authType; | |
| await logger.saveCheckpoint({ history, authType }, tag); | |
| return { | |
| type: 'message', | |
| messageType: 'info', | |
| content: `Conversation checkpoint saved with tag: ${decodeTagName( | |
| tag, | |
| )}.`, | |
| }; | |
| } else { | |
| return { | |
| type: 'message', | |
| messageType: 'info', | |
| content: 'No conversation found to save.', | |
| }; | |
| } | |
| }, | |
| }; | |
| const resumeCheckpointCommand: SlashCommand = { | |
| name: 'resume', | |
| altNames: ['load'], | |
| description: | |
| 'Resume a conversation from a checkpoint. Usage: /resume resume <tag>', | |
| kind: CommandKind.BUILT_IN, | |
| autoExecute: true, | |
| action: async (context, args) => { | |
| const tag = args.trim(); | |
| if (!tag) { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: 'Missing tag. Usage: /resume resume <tag>', | |
| }; | |
| } | |
| const { logger } = context.services; | |
| const config = context.services.agentContext?.config; | |
| await logger.initialize(); | |
| const checkpoint = await logger.loadCheckpoint(tag); | |
| const conversation = checkpoint.history; | |
| if (conversation.length === 0) { | |
| return { | |
| type: 'message', | |
| messageType: 'info', | |
| content: `No saved checkpoint found with tag: ${decodeTagName(tag)}.`, | |
| }; | |
| } | |
| const currentAuthType = config?.getContentGeneratorConfig()?.authType; | |
| if ( | |
| checkpoint.authType && | |
| currentAuthType && | |
| checkpoint.authType !== currentAuthType | |
| ) { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: `Cannot resume chat. It was saved with a different authentication method (${checkpoint.authType}) than the current one (${currentAuthType}).`, | |
| }; | |
| } | |
| const rolemap: { [key: string]: MessageType } = { | |
| user: MessageType.USER, | |
| model: MessageType.GEMINI, | |
| }; | |
| const uiHistory: HistoryItemWithoutId[] = []; | |
| for (const item of conversation.slice(INITIAL_HISTORY_LENGTH)) { | |
| const text = | |
| item.parts | |
| ?.filter((m) => !!m.text) | |
| .map((m) => m.text) | |
| .join('') || ''; | |
| if (!text) { | |
| continue; | |
| } | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| 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)); | |
| }, | |
| }; | |
| const deleteCommand: SlashCommand = { | |
| name: 'delete', | |
| description: 'Delete a conversation checkpoint. Usage: /resume delete <tag>', | |
| kind: CommandKind.BUILT_IN, | |
| autoExecute: true, | |
| action: async (context, args): Promise<MessageActionReturn> => { | |
| const tag = args.trim(); | |
| if (!tag) { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: 'Missing tag. Usage: /resume delete <tag>', | |
| }; | |
| } | |
| const { logger } = context.services; | |
| await logger.initialize(); | |
| const deleted = await logger.deleteCheckpoint(tag); | |
| if (deleted) { | |
| return { | |
| type: 'message', | |
| messageType: 'info', | |
| content: `Conversation checkpoint '${decodeTagName(tag)}' has been deleted.`, | |
| }; | |
| } else { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: `Error: No checkpoint found with tag '${decodeTagName(tag)}'.`, | |
| }; | |
| } | |
| }, | |
| completion: async (context, partialArg) => { | |
| const chatDetails = await getSavedChatTags(context, true); | |
| return chatDetails | |
| .map((chat) => chat.name) | |
| .filter((name) => name.startsWith(partialArg)); | |
| }, | |
| }; | |
| const shareCommand: SlashCommand = { | |
| name: 'share', | |
| description: | |
| 'Share the current conversation to a markdown or json file. Usage: /resume share <file>', | |
| kind: CommandKind.BUILT_IN, | |
| autoExecute: false, | |
| action: async (context, args): Promise<MessageActionReturn> => { | |
| let filePathArg = args.trim(); | |
| if (!filePathArg) { | |
| filePathArg = `gemini-conversation-${Date.now()}.json`; | |
| } | |
| const filePath = path.resolve(filePathArg); | |
| const extension = path.extname(filePath); | |
| if (extension !== '.md' && extension !== '.json') { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: 'Invalid file format. Only .md and .json are supported.', | |
| }; | |
| } | |
| const chat = context.services.agentContext?.geminiClient?.getChat(); | |
| if (!chat) { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: 'No chat client available to share conversation.', | |
| }; | |
| } | |
| const history = chat.getHistory(); | |
| // An empty conversation has a hidden message that sets up the context for | |
| // the chat. Thus, to check whether a conversation has been started, we | |
| // can't check for length 0. | |
| if (history.length <= INITIAL_HISTORY_LENGTH) { | |
| return { | |
| type: 'message', | |
| messageType: 'info', | |
| content: 'No conversation found to share.', | |
| }; | |
| } | |
| try { | |
| await exportHistoryToFile({ history, filePath }); | |
| return { | |
| type: 'message', | |
| messageType: 'info', | |
| content: `Conversation shared to ${filePath}`, | |
| }; | |
| } catch (err) { | |
| const errorMessage = err instanceof Error ? err.message : String(err); | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: `Error sharing conversation: ${errorMessage}`, | |
| }; | |
| } | |
| }, | |
| }; | |
| export const debugCommand: SlashCommand = { | |
| name: 'debug', | |
| description: 'Export the most recent API request as a JSON payload', | |
| kind: CommandKind.BUILT_IN, | |
| autoExecute: true, | |
| action: async (context): Promise<MessageActionReturn> => { | |
| const req = context.services.agentContext?.config.getLatestApiRequest(); | |
| if (!req) { | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: 'No recent API request found to export.', | |
| }; | |
| } | |
| const restPayload = convertToRestPayload(req); | |
| const filename = `gcli-request-${Date.now()}.json`; | |
| const filePath = path.join(process.cwd(), filename); | |
| try { | |
| await fsPromises.writeFile( | |
| filePath, | |
| JSON.stringify(restPayload, null, 2), | |
| ); | |
| return { | |
| type: 'message', | |
| messageType: 'info', | |
| content: `Debug API request saved to ${filename}`, | |
| }; | |
| } catch (err) { | |
| const errorMessage = err instanceof Error ? err.message : String(err); | |
| return { | |
| type: 'message', | |
| messageType: 'error', | |
| content: `Error saving debug request: ${errorMessage}`, | |
| }; | |
| } | |
| }, | |
| }; | |
| export const checkpointSubCommands: SlashCommand[] = [ | |
| listCommand, | |
| saveCommand, | |
| resumeCheckpointCommand, | |
| deleteCommand, | |
| shareCommand, | |
| ]; | |
| const checkpointCompatibilityCommand: SlashCommand = { | |
| name: 'checkpoints', | |
| altNames: ['checkpoint'], | |
| description: 'Compatibility command for nested checkpoint operations', | |
| kind: CommandKind.BUILT_IN, | |
| hidden: true, | |
| autoExecute: false, | |
| subCommands: checkpointSubCommands, | |
| }; | |
| export const chatResumeSubCommands: SlashCommand[] = [ | |
| ...checkpointSubCommands.map((subCommand) => ({ | |
| ...subCommand, | |
| suggestionGroup: CHECKPOINT_MENU_GROUP, | |
| })), | |
| checkpointCompatibilityCommand, | |
| ]; | |
| import { parseSlashCommand } from '../../utils/commands.js'; | |
| export const chatCommand: SlashCommand = { | |
| name: 'chat', | |
| description: 'Browse auto-saved conversations and manage chat checkpoints', | |
| kind: CommandKind.BUILT_IN, | |
| autoExecute: true, | |
| action: async (context, args) => { | |
| if (args) { | |
| const parsed = parseSlashCommand(`/${args}`, chatResumeSubCommands); | |
| if (parsed.commandToExecute?.action) { | |
| return parsed.commandToExecute.action(context, parsed.args); | |
| } | |
| } | |
| return { | |
| type: 'dialog', | |
| dialog: 'sessionBrowser', | |
| }; | |
| }, | |
| subCommands: chatResumeSubCommands, | |
| }; | |