Skip to content

Commit 8625d3e

Browse files
LyalinDotComkunal-10-cloud
authored andcommitted
feat(cli): unify /chat and /resume command UX (google-gemini#20256)
1 parent 6432065 commit 8625d3e

18 files changed

+617
-88
lines changed

docs/cli/session-management.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ Browser**:
6161
/resume
6262
```
6363

64+
When typing `/resume` (or `/chat`) in slash completion, commands are grouped
65+
under titled separators:
66+
67+
- `-- auto --` (session browser)
68+
- `list` is selectable and opens the session browser
69+
- `-- checkpoints --` (manual tagged checkpoint commands)
70+
71+
Unique prefixes such as `/resum` and `/cha` resolve to the same grouped menu.
72+
6473
The Session Browser provides an interactive interface where you can perform the
6574
following actions:
6675

@@ -72,6 +81,21 @@ following actions:
7281
- **Select:** Press **Enter** to resume the selected session.
7382
- **Esc:** Press **Esc** to exit the Session Browser.
7483

84+
### Manual chat checkpoints
85+
86+
For named branch points inside a session, use chat checkpoints:
87+
88+
```text
89+
/resume save decision-point
90+
/resume list
91+
/resume resume decision-point
92+
```
93+
94+
Compatibility aliases:
95+
96+
- `/chat ...` works for the same commands.
97+
- `/resume checkpoints ...` also remains supported during migration.
98+
7599
## Managing sessions
76100

77101
You can list and delete sessions to keep your history organized and manage disk

docs/cli/tutorials/session-management.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ Gemini gives you granular control over the undo process. You can choose to:
8989
Sometimes you want to try two different approaches to the same problem.
9090

9191
1. Start a session and get to a decision point.
92-
2. Save the current state with `/chat save decision-point`.
92+
2. Save the current state with `/resume save decision-point`.
9393
3. Try your first approach.
94-
4. Later, use `/chat resume decision-point` to fork the conversation back to
94+
4. Later, use `/resume resume decision-point` to fork the conversation back to
9595
that moment and try a different approach.
9696

9797
This creates a new branch of history without losing your original work.
@@ -101,5 +101,5 @@ This creates a new branch of history without losing your original work.
101101
- Learn about [Checkpointing](../../cli/checkpointing.md) to understand the
102102
underlying safety mechanism.
103103
- Explore [Task planning](task-planning.md) to keep complex sessions organized.
104-
- See the [Command reference](../../reference/commands.md) for all `/chat` and
105-
`/resume` options.
104+
- See the [Command reference](../../reference/commands.md) for `/resume`
105+
options, grouped checkpoint menus, and `/chat` compatibility aliases.

docs/reference/commands.md

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,33 @@ Slash commands provide meta-level control over the CLI itself.
2828

2929
### `/chat`
3030

31-
- **Description:** Save and resume conversation history for branching
32-
conversation state interactively, or resuming a previous state from a later
33-
session.
31+
- **Description:** Alias for `/resume`. Both commands now expose the same
32+
session browser action and checkpoint subcommands.
33+
- **Menu layout when typing `/chat` (or `/resume`)**:
34+
- `-- auto --`
35+
- `list` (selecting this opens the auto-saved session browser)
36+
- `-- checkpoints --`
37+
- `list`, `save`, `resume`, `delete`, `share` (manual tagged checkpoints)
38+
- **Note:** Unique prefixes (for example `/cha` or `/resum`) resolve to the
39+
same grouped menu.
3440
- **Sub-commands:**
3541
- **`debug`**
3642
- **Description:** Export the most recent API request as a JSON payload.
3743
- **`delete <tag>`**
3844
- **Description:** Deletes a saved conversation checkpoint.
45+
- **Equivalent:** `/resume delete <tag>`
3946
- **`list`**
40-
- **Description:** Lists available tags for chat state resumption.
47+
- **Description:** Lists available tags for manually saved checkpoints.
4148
- **Note:** This command only lists chats saved within the current project.
4249
Because chat history is project-scoped, chats saved in other project
4350
directories will not be displayed.
51+
- **Equivalent:** `/resume list`
4452
- **`resume <tag>`**
4553
- **Description:** Resumes a conversation from a previous save.
4654
- **Note:** You can only resume chats that were saved within the current
4755
project. To resume a chat from a different project, you must run the
4856
Gemini CLI from that project's directory.
57+
- **Equivalent:** `/resume resume <tag>`
4958
- **`save <tag>`**
5059
- **Description:** Saves the current conversation history. You must add a
5160
`<tag>` for identifying the conversation state.
@@ -60,10 +69,12 @@ Slash commands provide meta-level control over the CLI itself.
6069
conversation states. For automatic checkpoints created before file
6170
modifications, see the
6271
[Checkpointing documentation](../cli/checkpointing.md).
72+
- **Equivalent:** `/resume save <tag>`
6373
- **`share [filename]`**
6474
- **Description** Writes the current conversation to a provided Markdown or
6575
JSON file. If no filename is provided, then the CLI will generate one.
6676
- **Usage** `/chat share file.md` or `/chat share file.json`.
77+
- **Equivalent:** `/resume share [filename]`
6778

6879
### `/clear`
6980

@@ -314,10 +325,13 @@ Slash commands provide meta-level control over the CLI itself.
314325

315326
### `/resume`
316327

317-
- **Description:** Browse and resume previous conversation sessions. Opens an
318-
interactive session browser where you can search, filter, and select from
319-
automatically saved conversations.
328+
- **Description:** Browse and resume previous conversation sessions, and manage
329+
manual chat checkpoints.
320330
- **Features:**
331+
- **Auto sessions:** Run `/resume` to open the interactive session browser for
332+
automatically saved conversations.
333+
- **Chat checkpoints:** Use checkpoint subcommands directly (`/resume save`,
334+
`/resume resume`, etc.).
321335
- **Management:** Delete unwanted sessions directly from the browser
322336
- **Resume:** Select any session to resume and continue the conversation
323337
- **Search:** Use `/` to search through conversation content across all
@@ -328,6 +342,23 @@ Slash commands provide meta-level control over the CLI itself.
328342
- **Note:** All conversations are automatically saved as you chat - no manual
329343
saving required. See [Session Management](../cli/session-management.md) for
330344
complete details.
345+
- **Alias:** `/chat` provides the same behavior and subcommands.
346+
- **Sub-commands:**
347+
- **`list`**
348+
- **Description:** Lists available tags for manual chat checkpoints.
349+
- **`save <tag>`**
350+
- **Description:** Saves the current conversation as a tagged checkpoint.
351+
- **`resume <tag>`** (alias: `load`)
352+
- **Description:** Loads a previously saved tagged checkpoint.
353+
- **`delete <tag>`**
354+
- **Description:** Deletes a tagged checkpoint.
355+
- **`share [filename]`**
356+
- **Description:** Exports the current conversation to Markdown or JSON.
357+
- **`debug`**
358+
- **Description:** Export the most recent API request as JSON payload
359+
(nightly builds).
360+
- **Compatibility alias:** `/resume checkpoints ...` is still accepted for the
361+
same checkpoint commands.
331362

332363
### `/settings`
333364

packages/cli/src/services/BuiltinCommandLoader.test.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,17 @@ vi.mock('../ui/commands/agentsCommand.js', () => ({
7373
}));
7474
vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} }));
7575
vi.mock('../ui/commands/chatCommand.js', () => ({
76-
chatCommand: { name: 'chat', subCommands: [] },
76+
chatCommand: {
77+
name: 'chat',
78+
subCommands: [
79+
{ name: 'list' },
80+
{ name: 'save' },
81+
{ name: 'resume' },
82+
{ name: 'delete' },
83+
{ name: 'share' },
84+
{ name: 'checkpoints', hidden: true, subCommands: [{ name: 'list' }] },
85+
],
86+
},
7787
debugCommand: { name: 'debug' },
7888
}));
7989
vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} }));
@@ -94,7 +104,19 @@ vi.mock('../ui/commands/modelCommand.js', () => ({
94104
}));
95105
vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} }));
96106
vi.mock('../ui/commands/quitCommand.js', () => ({ quitCommand: {} }));
97-
vi.mock('../ui/commands/resumeCommand.js', () => ({ resumeCommand: {} }));
107+
vi.mock('../ui/commands/resumeCommand.js', () => ({
108+
resumeCommand: {
109+
name: 'resume',
110+
subCommands: [
111+
{ name: 'list' },
112+
{ name: 'save' },
113+
{ name: 'resume' },
114+
{ name: 'delete' },
115+
{ name: 'share' },
116+
{ name: 'checkpoints', hidden: true, subCommands: [{ name: 'list' }] },
117+
],
118+
},
119+
}));
98120
vi.mock('../ui/commands/statsCommand.js', () => ({ statsCommand: {} }));
99121
vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} }));
100122
vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} }));
@@ -256,7 +278,7 @@ describe('BuiltinCommandLoader', () => {
256278
});
257279

258280
describe('chat debug command', () => {
259-
it('should NOT add debug subcommand to chatCommand if not a nightly build', async () => {
281+
it('should NOT add debug subcommand to chat/resume commands if not a nightly build', async () => {
260282
vi.mocked(isNightly).mockResolvedValue(false);
261283
const loader = new BuiltinCommandLoader(mockConfig);
262284
const commands = await loader.loadCommands(new AbortController().signal);
@@ -265,9 +287,30 @@ describe('BuiltinCommandLoader', () => {
265287
expect(chatCmd?.subCommands).toBeDefined();
266288
const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug');
267289
expect(hasDebug).toBe(false);
290+
291+
const resumeCmd = commands.find((c) => c.name === 'resume');
292+
const resumeHasDebug =
293+
resumeCmd?.subCommands?.some((c) => c.name === 'debug') ?? false;
294+
expect(resumeHasDebug).toBe(false);
295+
296+
const chatCheckpointsCmd = chatCmd?.subCommands?.find(
297+
(c) => c.name === 'checkpoints',
298+
);
299+
const chatCheckpointHasDebug =
300+
chatCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
301+
false;
302+
expect(chatCheckpointHasDebug).toBe(false);
303+
304+
const resumeCheckpointsCmd = resumeCmd?.subCommands?.find(
305+
(c) => c.name === 'checkpoints',
306+
);
307+
const resumeCheckpointHasDebug =
308+
resumeCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
309+
false;
310+
expect(resumeCheckpointHasDebug).toBe(false);
268311
});
269312

270-
it('should add debug subcommand to chatCommand if it is a nightly build', async () => {
313+
it('should add debug subcommand to chat/resume commands if it is a nightly build', async () => {
271314
vi.mocked(isNightly).mockResolvedValue(true);
272315
const loader = new BuiltinCommandLoader(mockConfig);
273316
const commands = await loader.loadCommands(new AbortController().signal);
@@ -276,6 +319,27 @@ describe('BuiltinCommandLoader', () => {
276319
expect(chatCmd?.subCommands).toBeDefined();
277320
const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug');
278321
expect(hasDebug).toBe(true);
322+
323+
const resumeCmd = commands.find((c) => c.name === 'resume');
324+
const resumeHasDebug =
325+
resumeCmd?.subCommands?.some((c) => c.name === 'debug') ?? false;
326+
expect(resumeHasDebug).toBe(true);
327+
328+
const chatCheckpointsCmd = chatCmd?.subCommands?.find(
329+
(c) => c.name === 'checkpoints',
330+
);
331+
const chatCheckpointHasDebug =
332+
chatCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
333+
false;
334+
expect(chatCheckpointHasDebug).toBe(true);
335+
336+
const resumeCheckpointsCmd = resumeCmd?.subCommands?.find(
337+
(c) => c.name === 'checkpoints',
338+
);
339+
const resumeCheckpointHasDebug =
340+
resumeCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
341+
false;
342+
expect(resumeCheckpointHasDebug).toBe(true);
279343
});
280344
});
281345
});

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,41 @@ export class BuiltinCommandLoader implements ICommandLoader {
7878
const handle = startupProfiler.start('load_builtin_commands');
7979

8080
const isNightlyBuild = await isNightly(process.cwd());
81+
const addDebugToChatResumeSubCommands = (
82+
subCommands: SlashCommand[] | undefined,
83+
): SlashCommand[] | undefined => {
84+
if (!subCommands) {
85+
return subCommands;
86+
}
87+
88+
const withNestedCompatibility = subCommands.map((subCommand) => {
89+
if (subCommand.name !== 'checkpoints') {
90+
return subCommand;
91+
}
92+
93+
return {
94+
...subCommand,
95+
subCommands: addDebugToChatResumeSubCommands(subCommand.subCommands),
96+
};
97+
});
98+
99+
if (!isNightlyBuild) {
100+
return withNestedCompatibility;
101+
}
102+
103+
return withNestedCompatibility.some(
104+
(cmd) => cmd.name === debugCommand.name,
105+
)
106+
? withNestedCompatibility
107+
: [
108+
...withNestedCompatibility,
109+
{ ...debugCommand, suggestionGroup: 'checkpoints' },
110+
];
111+
};
112+
113+
const chatResumeSubCommands = addDebugToChatResumeSubCommands(
114+
chatCommand.subCommands,
115+
);
81116

82117
const allDefinitions: Array<SlashCommand | null> = [
83118
aboutCommand,
@@ -86,9 +121,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
86121
bugCommand,
87122
{
88123
...chatCommand,
89-
subCommands: isNightlyBuild
90-
? [...(chatCommand.subCommands || []), debugCommand]
91-
: chatCommand.subCommands,
124+
subCommands: chatResumeSubCommands,
92125
},
93126
clearCommand,
94127
commandsCommand,
@@ -155,7 +188,10 @@ export class BuiltinCommandLoader implements ICommandLoader {
155188
...(isDevelopment ? [profileCommand] : []),
156189
quitCommand,
157190
restoreCommand(this.config),
158-
resumeCommand,
191+
{
192+
...resumeCommand,
193+
subCommands: addDebugToChatResumeSubCommands(resumeCommand.subCommands),
194+
},
159195
statsCommand,
160196
themeCommand,
161197
toolsCommand,

packages/cli/src/ui/commands/chatCommand.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,11 @@ describe('chatCommand', () => {
9999

100100
it('should have the correct main command definition', () => {
101101
expect(chatCommand.name).toBe('chat');
102-
expect(chatCommand.description).toBe('Manage conversation history');
103-
expect(chatCommand.subCommands).toHaveLength(5);
102+
expect(chatCommand.description).toBe(
103+
'Browse auto-saved conversations and manage chat checkpoints',
104+
);
105+
expect(chatCommand.autoExecute).toBe(true);
106+
expect(chatCommand.subCommands).toHaveLength(6);
104107
});
105108

106109
describe('list subcommand', () => {
@@ -158,7 +161,7 @@ describe('chatCommand', () => {
158161
expect(result).toEqual({
159162
type: 'message',
160163
messageType: 'error',
161-
content: 'Missing tag. Usage: /chat save <tag>',
164+
content: 'Missing tag. Usage: /resume save <tag>',
162165
});
163166
});
164167

@@ -252,7 +255,7 @@ describe('chatCommand', () => {
252255
expect(result).toEqual({
253256
type: 'message',
254257
messageType: 'error',
255-
content: 'Missing tag. Usage: /chat resume <tag>',
258+
content: 'Missing tag. Usage: /resume resume <tag>',
256259
});
257260
});
258261

@@ -386,7 +389,7 @@ describe('chatCommand', () => {
386389
expect(result).toEqual({
387390
type: 'message',
388391
messageType: 'error',
389-
content: 'Missing tag. Usage: /chat delete <tag>',
392+
content: 'Missing tag. Usage: /resume delete <tag>',
390393
});
391394
});
392395

0 commit comments

Comments
 (0)