mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-01 07:24:38 -07:00
feat(cli): add --delete flag to /exit command for session deletion (#19332)
Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
@@ -33,11 +33,12 @@ describe('quitCommand', () => {
|
||||
});
|
||||
|
||||
if (!quitCommand.action) throw new Error('Action is not defined');
|
||||
const result = quitCommand.action(mockContext, 'quit');
|
||||
const result = quitCommand.action(mockContext, '');
|
||||
|
||||
expect(formatDuration).toHaveBeenCalledWith(3600000); // 1 hour in ms
|
||||
expect(result).toEqual({
|
||||
type: 'quit',
|
||||
deleteSession: false,
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
@@ -52,4 +53,54 @@ describe('quitCommand', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('sets deleteSession to true when --delete flag is provided', () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
session: {
|
||||
stats: {
|
||||
sessionStartTime: new Date('2025-01-01T00:00:00Z'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!quitCommand.action) throw new Error('Action is not defined');
|
||||
const result = quitCommand.action(mockContext, '--delete');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'quit',
|
||||
deleteSession: true,
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
text: '/quit',
|
||||
id: expect.any(Number),
|
||||
},
|
||||
{
|
||||
type: 'quit',
|
||||
duration: '1h 0m 0s',
|
||||
id: expect.any(Number),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set deleteSession for unrecognized args', () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
session: {
|
||||
stats: {
|
||||
sessionStartTime: new Date('2025-01-01T00:00:00Z'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!quitCommand.action) throw new Error('Action is not defined');
|
||||
const result = quitCommand.action(mockContext, 'some-random-arg');
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'quit',
|
||||
deleteSession: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,13 +13,16 @@ export const quitCommand: SlashCommand = {
|
||||
description: 'Exit the cli',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => {
|
||||
action: (context, args) => {
|
||||
const now = Date.now();
|
||||
const { sessionStartTime } = context.session.stats;
|
||||
const wallDuration = now - sessionStartTime.getTime();
|
||||
|
||||
const deleteSession = args.trim() === '--delete';
|
||||
|
||||
return {
|
||||
type: 'quit',
|
||||
deleteSession,
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
|
||||
@@ -108,6 +108,8 @@ export interface CommandContext {
|
||||
export interface QuitActionReturn {
|
||||
type: 'quit';
|
||||
messages: HistoryItem[];
|
||||
/** When true, the current session's history and temporary files will be deleted on exit. */
|
||||
deleteSession?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -646,6 +646,108 @@ describe('useSlashCommandProcessor', () => {
|
||||
|
||||
expect(mockSetQuittingMessages).toHaveBeenCalledWith(['bye']);
|
||||
});
|
||||
|
||||
it('should delete the current session when quit action has deleteSession flag', async () => {
|
||||
const mockDeleteCurrentSessionAsync = vi
|
||||
.fn()
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const mockClient = {
|
||||
getChatRecordingService: vi.fn().mockReturnValue({
|
||||
deleteCurrentSessionAsync: mockDeleteCurrentSessionAsync,
|
||||
}),
|
||||
} as unknown as GeminiClient;
|
||||
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(mockClient);
|
||||
|
||||
const quitAction = vi.fn().mockResolvedValue({
|
||||
type: 'quit',
|
||||
deleteSession: true,
|
||||
messages: ['bye'],
|
||||
});
|
||||
const command = createTestCommand({
|
||||
name: 'exit',
|
||||
action: quitAction,
|
||||
});
|
||||
const result = await setupProcessorHook({
|
||||
builtinCommands: [command],
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/exit --delete');
|
||||
});
|
||||
|
||||
expect(mockDeleteCurrentSessionAsync).toHaveBeenCalled();
|
||||
expect(mockSetQuittingMessages).toHaveBeenCalledWith(['bye']);
|
||||
});
|
||||
|
||||
it('should not delete session when quit action does not have deleteSession flag', async () => {
|
||||
const mockDeleteCurrentSessionAsync = vi
|
||||
.fn()
|
||||
.mockResolvedValue(undefined);
|
||||
const mockClient = {
|
||||
getChatRecordingService: vi.fn().mockReturnValue({
|
||||
deleteCurrentSessionAsync: mockDeleteCurrentSessionAsync,
|
||||
}),
|
||||
} as unknown as GeminiClient;
|
||||
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(mockClient);
|
||||
|
||||
const quitAction = vi.fn().mockResolvedValue({
|
||||
type: 'quit',
|
||||
messages: ['bye'],
|
||||
});
|
||||
const command = createTestCommand({
|
||||
name: 'exit',
|
||||
action: quitAction,
|
||||
});
|
||||
const result = await setupProcessorHook({
|
||||
builtinCommands: [command],
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/exit');
|
||||
});
|
||||
|
||||
expect(mockDeleteCurrentSessionAsync).not.toHaveBeenCalled();
|
||||
expect(mockSetQuittingMessages).toHaveBeenCalledWith(['bye']);
|
||||
});
|
||||
|
||||
it('should still quit even if session deletion fails', async () => {
|
||||
const mockClient = {
|
||||
getChatRecordingService: vi.fn().mockReturnValue({
|
||||
deleteCurrentSessionAsync: vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Deletion failed')),
|
||||
}),
|
||||
} as unknown as GeminiClient;
|
||||
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(mockClient);
|
||||
|
||||
const quitAction = vi.fn().mockResolvedValue({
|
||||
type: 'quit',
|
||||
deleteSession: true,
|
||||
messages: ['bye'],
|
||||
});
|
||||
const command = createTestCommand({
|
||||
name: 'exit',
|
||||
action: quitAction,
|
||||
});
|
||||
const result = await setupProcessorHook({
|
||||
builtinCommands: [command],
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/exit --delete');
|
||||
});
|
||||
|
||||
// Should still quit even though deletion threw
|
||||
expect(mockSetQuittingMessages).toHaveBeenCalledWith(['bye']);
|
||||
});
|
||||
|
||||
it('should handle "submit_prompt" action returned from a file-based command', async () => {
|
||||
const fileCommand = createTestCommand(
|
||||
{
|
||||
|
||||
@@ -557,6 +557,18 @@ export const useSlashCommandProcessor = (
|
||||
return { type: 'handled' };
|
||||
}
|
||||
case 'quit':
|
||||
if (result.deleteSession) {
|
||||
try {
|
||||
const chatRecordingService = config
|
||||
?.getGeminiClient()
|
||||
?.getChatRecordingService();
|
||||
if (chatRecordingService) {
|
||||
await chatRecordingService.deleteCurrentSessionAsync();
|
||||
}
|
||||
} catch {
|
||||
// Don't let deletion errors prevent exit.
|
||||
}
|
||||
}
|
||||
actions.quit(result.messages);
|
||||
return { type: 'handled' };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user