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
@@ -73,7 +73,17 @@ vi.mock('../ui/commands/agentsCommand.js', () => ({
}));
vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} }));
vi.mock('../ui/commands/chatCommand.js', () => ({
chatCommand: { name: 'chat', subCommands: [] },
chatCommand: {
name: 'chat',
subCommands: [
{ name: 'list' },
{ name: 'save' },
{ name: 'resume' },
{ name: 'delete' },
{ name: 'share' },
{ name: 'checkpoints', hidden: true, subCommands: [{ name: 'list' }] },
],
},
debugCommand: { name: 'debug' },
}));
vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} }));
@@ -94,7 +104,19 @@ vi.mock('../ui/commands/modelCommand.js', () => ({
}));
vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} }));
vi.mock('../ui/commands/quitCommand.js', () => ({ quitCommand: {} }));
vi.mock('../ui/commands/resumeCommand.js', () => ({ resumeCommand: {} }));
vi.mock('../ui/commands/resumeCommand.js', () => ({
resumeCommand: {
name: 'resume',
subCommands: [
{ name: 'list' },
{ name: 'save' },
{ name: 'resume' },
{ name: 'delete' },
{ name: 'share' },
{ name: 'checkpoints', hidden: true, subCommands: [{ name: 'list' }] },
],
},
}));
vi.mock('../ui/commands/statsCommand.js', () => ({ statsCommand: {} }));
vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} }));
vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} }));
@@ -256,7 +278,7 @@ describe('BuiltinCommandLoader', () => {
});
describe('chat debug command', () => {
it('should NOT add debug subcommand to chatCommand if not a nightly build', async () => {
it('should NOT add debug subcommand to chat/resume commands if not a nightly build', async () => {
vi.mocked(isNightly).mockResolvedValue(false);
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
@@ -265,9 +287,30 @@ describe('BuiltinCommandLoader', () => {
expect(chatCmd?.subCommands).toBeDefined();
const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug');
expect(hasDebug).toBe(false);
const resumeCmd = commands.find((c) => c.name === 'resume');
const resumeHasDebug =
resumeCmd?.subCommands?.some((c) => c.name === 'debug') ?? false;
expect(resumeHasDebug).toBe(false);
const chatCheckpointsCmd = chatCmd?.subCommands?.find(
(c) => c.name === 'checkpoints',
);
const chatCheckpointHasDebug =
chatCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
false;
expect(chatCheckpointHasDebug).toBe(false);
const resumeCheckpointsCmd = resumeCmd?.subCommands?.find(
(c) => c.name === 'checkpoints',
);
const resumeCheckpointHasDebug =
resumeCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
false;
expect(resumeCheckpointHasDebug).toBe(false);
});
it('should add debug subcommand to chatCommand if it is a nightly build', async () => {
it('should add debug subcommand to chat/resume commands if it is a nightly build', async () => {
vi.mocked(isNightly).mockResolvedValue(true);
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
@@ -276,6 +319,27 @@ describe('BuiltinCommandLoader', () => {
expect(chatCmd?.subCommands).toBeDefined();
const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug');
expect(hasDebug).toBe(true);
const resumeCmd = commands.find((c) => c.name === 'resume');
const resumeHasDebug =
resumeCmd?.subCommands?.some((c) => c.name === 'debug') ?? false;
expect(resumeHasDebug).toBe(true);
const chatCheckpointsCmd = chatCmd?.subCommands?.find(
(c) => c.name === 'checkpoints',
);
const chatCheckpointHasDebug =
chatCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
false;
expect(chatCheckpointHasDebug).toBe(true);
const resumeCheckpointsCmd = resumeCmd?.subCommands?.find(
(c) => c.name === 'checkpoints',
);
const resumeCheckpointHasDebug =
resumeCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ??
false;
expect(resumeCheckpointHasDebug).toBe(true);
});
});
});
@@ -78,6 +78,41 @@ export class BuiltinCommandLoader implements ICommandLoader {
const handle = startupProfiler.start('load_builtin_commands');
const isNightlyBuild = await isNightly(process.cwd());
const addDebugToChatResumeSubCommands = (
subCommands: SlashCommand[] | undefined,
): SlashCommand[] | undefined => {
if (!subCommands) {
return subCommands;
}
const withNestedCompatibility = subCommands.map((subCommand) => {
if (subCommand.name !== 'checkpoints') {
return subCommand;
}
return {
...subCommand,
subCommands: addDebugToChatResumeSubCommands(subCommand.subCommands),
};
});
if (!isNightlyBuild) {
return withNestedCompatibility;
}
return withNestedCompatibility.some(
(cmd) => cmd.name === debugCommand.name,
)
? withNestedCompatibility
: [
...withNestedCompatibility,
{ ...debugCommand, suggestionGroup: 'checkpoints' },
];
};
const chatResumeSubCommands = addDebugToChatResumeSubCommands(
chatCommand.subCommands,
);
const allDefinitions: Array<SlashCommand | null> = [
aboutCommand,
@@ -86,9 +121,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
bugCommand,
{
...chatCommand,
subCommands: isNightlyBuild
? [...(chatCommand.subCommands || []), debugCommand]
: chatCommand.subCommands,
subCommands: chatResumeSubCommands,
},
clearCommand,
commandsCommand,
@@ -155,7 +188,10 @@ export class BuiltinCommandLoader implements ICommandLoader {
...(isDevelopment ? [profileCommand] : []),
quitCommand,
restoreCommand(this.config),
resumeCommand,
{
...resumeCommand,
subCommands: addDebugToChatResumeSubCommands(resumeCommand.subCommands),
},
statsCommand,
themeCommand,
toolsCommand,