feat(cli): unify /chat and /resume command UX (#20256)

This commit is contained in:
Dmitry Lyalin
2026-03-08 18:50:51 -04:00
committed by GitHub
parent d012929a28
commit d41735d6a9
18 changed files with 619 additions and 90 deletions
@@ -99,8 +99,11 @@ describe('chatCommand', () => {
it('should have the correct main command definition', () => {
expect(chatCommand.name).toBe('chat');
expect(chatCommand.description).toBe('Manage conversation history');
expect(chatCommand.subCommands).toHaveLength(5);
expect(chatCommand.description).toBe(
'Browse auto-saved conversations and manage chat checkpoints',
);
expect(chatCommand.autoExecute).toBe(true);
expect(chatCommand.subCommands).toHaveLength(6);
});
describe('list subcommand', () => {
@@ -158,7 +161,7 @@ describe('chatCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat save <tag>',
content: 'Missing tag. Usage: /resume save <tag>',
});
});
@@ -252,7 +255,7 @@ describe('chatCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat resume <tag>',
content: 'Missing tag. Usage: /resume resume <tag>',
});
});
@@ -386,7 +389,7 @@ describe('chatCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat delete <tag>',
content: 'Missing tag. Usage: /resume delete <tag>',
});
});
+45 -19
View File
@@ -29,6 +29,8 @@ 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,
@@ -70,7 +72,7 @@ const getSavedChatTags = async (
const listCommand: SlashCommand = {
name: 'list',
description: 'List saved conversation checkpoints',
description: 'List saved manual conversation checkpoints',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context): Promise<void> => {
@@ -88,7 +90,7 @@ const listCommand: SlashCommand = {
const saveCommand: SlashCommand = {
name: 'save',
description:
'Save the current conversation as a checkpoint. Usage: /chat save <tag>',
'Save the current conversation as a checkpoint. Usage: /resume save <tag>',
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: async (context, args): Promise<SlashCommandActionReturn | void> => {
@@ -97,7 +99,7 @@ const saveCommand: SlashCommand = {
return {
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat save <tag>',
content: 'Missing tag. Usage: /resume save <tag>',
};
}
@@ -117,7 +119,7 @@ const saveCommand: SlashCommand = {
' already exists. Do you want to overwrite it?',
),
originalInvocation: {
raw: context.invocation?.raw || `/chat save ${tag}`,
raw: context.invocation?.raw || `/resume save ${tag}`,
},
};
}
@@ -153,11 +155,11 @@ const saveCommand: SlashCommand = {
},
};
const resumeCommand: SlashCommand = {
const resumeCheckpointCommand: SlashCommand = {
name: 'resume',
altNames: ['load'],
description:
'Resume a conversation from a checkpoint. Usage: /chat resume <tag>',
'Resume a conversation from a checkpoint. Usage: /resume resume <tag>',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, args) => {
@@ -166,7 +168,7 @@ const resumeCommand: SlashCommand = {
return {
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat resume <tag>',
content: 'Missing tag. Usage: /resume resume <tag>',
};
}
@@ -235,7 +237,7 @@ const resumeCommand: SlashCommand = {
const deleteCommand: SlashCommand = {
name: 'delete',
description: 'Delete a conversation checkpoint. Usage: /chat delete <tag>',
description: 'Delete a conversation checkpoint. Usage: /resume delete <tag>',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, args): Promise<MessageActionReturn> => {
@@ -244,7 +246,7 @@ const deleteCommand: SlashCommand = {
return {
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat delete <tag>',
content: 'Missing tag. Usage: /resume delete <tag>',
};
}
@@ -277,7 +279,7 @@ const deleteCommand: SlashCommand = {
const shareCommand: SlashCommand = {
name: 'share',
description:
'Share the current conversation to a markdown or json file. Usage: /chat share <file>',
'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> => {
@@ -376,16 +378,40 @@ export const debugCommand: SlashCommand = {
},
};
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,
];
export const chatCommand: SlashCommand = {
name: 'chat',
description: 'Manage conversation history',
description: 'Browse auto-saved conversations and manage chat checkpoints',
kind: CommandKind.BUILT_IN,
autoExecute: false,
subCommands: [
listCommand,
saveCommand,
resumeCommand,
deleteCommand,
shareCommand,
],
autoExecute: true,
action: async () => ({
type: 'dialog',
dialog: 'sessionBrowser',
}),
subCommands: chatResumeSubCommands,
};
@@ -0,0 +1,41 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { resumeCommand } from './resumeCommand.js';
import type { CommandContext } from './types.js';
describe('resumeCommand', () => {
it('should open the session browser for bare /resume', async () => {
const result = await resumeCommand.action?.({} as CommandContext, '');
expect(result).toEqual({
type: 'dialog',
dialog: 'sessionBrowser',
});
});
it('should expose unified chat subcommands directly under /resume', () => {
const visibleSubCommandNames = (resumeCommand.subCommands ?? [])
.filter((subCommand) => !subCommand.hidden)
.map((subCommand) => subCommand.name);
expect(visibleSubCommandNames).toEqual(
expect.arrayContaining(['list', 'save', 'resume', 'delete', 'share']),
);
});
it('should keep a hidden /resume checkpoints compatibility alias', () => {
const checkpoints = resumeCommand.subCommands?.find(
(subCommand) => subCommand.name === 'checkpoints',
);
expect(checkpoints?.hidden).toBe(true);
expect(
checkpoints?.subCommands?.map((subCommand) => subCommand.name),
).toEqual(
expect.arrayContaining(['list', 'save', 'resume', 'delete', 'share']),
);
});
});
@@ -10,10 +10,11 @@ import type {
SlashCommand,
} from './types.js';
import { CommandKind } from './types.js';
import { chatResumeSubCommands } from './chatCommand.js';
export const resumeCommand: SlashCommand = {
name: 'resume',
description: 'Browse and resume auto-saved conversations',
description: 'Browse auto-saved conversations and manage chat checkpoints',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (
@@ -23,4 +24,5 @@ export const resumeCommand: SlashCommand = {
type: 'dialog',
dialog: 'sessionBrowser',
}),
subCommands: chatResumeSubCommands,
};
+6 -1
View File
@@ -190,6 +190,11 @@ export interface SlashCommand {
altNames?: string[];
description: string;
hidden?: boolean;
/**
* Optional grouping label for slash completion UI sections.
* Commands with the same label are rendered under one separator.
*/
suggestionGroup?: string;
kind: CommandKind;
@@ -217,7 +222,7 @@ export interface SlashCommand {
| SlashCommandActionReturn
| Promise<void | SlashCommandActionReturn>;
// Provides argument completion (e.g., completing a tag for `/chat resume <tag>`).
// Provides argument completion (e.g., completing a tag for `/resume resume <tag>`).
completion?: (
context: CommandContext,
partialArg: string,