summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/commands
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/commands')
-rw-r--r--packages/cli/src/ui/commands/chatCommand.test.ts50
-rw-r--r--packages/cli/src/ui/commands/chatCommand.ts26
-rw-r--r--packages/cli/src/ui/commands/types.ts16
3 files changed, 89 insertions, 3 deletions
diff --git a/packages/cli/src/ui/commands/chatCommand.test.ts b/packages/cli/src/ui/commands/chatCommand.test.ts
index aad0897c..ccdfd4b2 100644
--- a/packages/cli/src/ui/commands/chatCommand.test.ts
+++ b/packages/cli/src/ui/commands/chatCommand.test.ts
@@ -168,8 +168,12 @@ describe('chatCommand', () => {
describe('save subcommand', () => {
let saveCommand: SlashCommand;
const tag = 'my-tag';
+ let mockCheckpointExists: ReturnType<typeof vi.fn>;
+
beforeEach(() => {
saveCommand = getSubCommand('save');
+ mockCheckpointExists = vi.fn().mockResolvedValue(false);
+ mockContext.services.logger.checkpointExists = mockCheckpointExists;
});
it('should return an error if tag is missing', async () => {
@@ -191,7 +195,48 @@ describe('chatCommand', () => {
});
});
- it('should save the conversation', async () => {
+ it('should save the conversation if checkpoint does not exist', async () => {
+ const history: HistoryItemWithoutId[] = [
+ {
+ type: 'user',
+ text: 'hello',
+ },
+ ];
+ mockGetHistory.mockReturnValue(history);
+ mockCheckpointExists.mockResolvedValue(false);
+
+ const result = await saveCommand?.action?.(mockContext, tag);
+
+ expect(mockCheckpointExists).toHaveBeenCalledWith(tag);
+ expect(mockSaveCheckpoint).toHaveBeenCalledWith(history, tag);
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'info',
+ content: `Conversation checkpoint saved with tag: ${tag}.`,
+ });
+ });
+
+ it('should return confirm_action if checkpoint already exists', async () => {
+ mockCheckpointExists.mockResolvedValue(true);
+ mockContext.invocation = {
+ raw: `/chat save ${tag}`,
+ name: 'save',
+ args: tag,
+ };
+
+ const result = await saveCommand?.action?.(mockContext, tag);
+
+ expect(mockCheckpointExists).toHaveBeenCalledWith(tag);
+ expect(mockSaveCheckpoint).not.toHaveBeenCalled();
+ expect(result).toMatchObject({
+ type: 'confirm_action',
+ originalInvocation: { raw: `/chat save ${tag}` },
+ });
+ // Check that prompt is a React element
+ expect(result).toHaveProperty('prompt');
+ });
+
+ it('should save the conversation if overwrite is confirmed', async () => {
const history: HistoryItemWithoutId[] = [
{
type: 'user',
@@ -199,8 +244,11 @@ describe('chatCommand', () => {
},
];
mockGetHistory.mockReturnValue(history);
+ mockContext.overwriteConfirmed = true;
+
const result = await saveCommand?.action?.(mockContext, tag);
+ expect(mockCheckpointExists).not.toHaveBeenCalled(); // Should skip existence check
expect(mockSaveCheckpoint).toHaveBeenCalledWith(history, tag);
expect(result).toEqual({
type: 'message',
diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts
index a5fa13da..56eebe1a 100644
--- a/packages/cli/src/ui/commands/chatCommand.ts
+++ b/packages/cli/src/ui/commands/chatCommand.ts
@@ -5,11 +5,15 @@
*/
import * as fsPromises from 'fs/promises';
+import React from 'react';
+import { Text } from 'ink';
+import { Colors } from '../colors.js';
import {
CommandContext,
SlashCommand,
MessageActionReturn,
CommandKind,
+ SlashCommandActionReturn,
} from './types.js';
import path from 'path';
import { HistoryItemWithoutId, MessageType } from '../types.js';
@@ -96,7 +100,7 @@ const saveCommand: SlashCommand = {
description:
'Save the current conversation as a checkpoint. Usage: /chat save <tag>',
kind: CommandKind.BUILT_IN,
- action: async (context, args): Promise<MessageActionReturn> => {
+ action: async (context, args): Promise<SlashCommandActionReturn | void> => {
const tag = args.trim();
if (!tag) {
return {
@@ -108,6 +112,26 @@ const saveCommand: SlashCommand = {
const { logger, config } = context.services;
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: Colors.AccentPurple }, tag),
+ ' already exists. Do you want to overwrite it?',
+ ),
+ originalInvocation: {
+ raw: context.invocation?.raw || `/chat save ${tag}`,
+ },
+ };
+ }
+ }
+
const chat = await config?.getGeminiClient()?.getChat();
if (!chat) {
return {
diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts
index 09d79e9d..529f4eb8 100644
--- a/packages/cli/src/ui/commands/types.ts
+++ b/packages/cli/src/ui/commands/types.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import { type ReactNode } from 'react';
import { Content } from '@google/genai';
import { HistoryItemWithoutId } from '../types.js';
import { Config, GitService, Logger } from '@google/gemini-cli-core';
@@ -67,6 +68,8 @@ export interface CommandContext {
/** A transient list of shell commands the user has approved for this session. */
sessionShellAllowlist: Set<string>;
};
+ // Flag to indicate if an overwrite has been confirmed
+ overwriteConfirmed?: boolean;
}
/**
@@ -135,6 +138,16 @@ export interface ConfirmShellCommandsActionReturn {
};
}
+export interface ConfirmActionReturn {
+ type: 'confirm_action';
+ /** The React node to display as the confirmation prompt. */
+ prompt: ReactNode;
+ /** The original invocation context to be re-run after confirmation. */
+ originalInvocation: {
+ raw: string;
+ };
+}
+
export type SlashCommandActionReturn =
| ToolActionReturn
| MessageActionReturn
@@ -142,7 +155,8 @@ export type SlashCommandActionReturn =
| OpenDialogActionReturn
| LoadHistoryActionReturn
| SubmitPromptActionReturn
- | ConfirmShellCommandsActionReturn;
+ | ConfirmShellCommandsActionReturn
+ | ConfirmActionReturn;
export enum CommandKind {
BUILT_IN = 'built-in',