Text can be added after /plan command (#22833)

This commit is contained in:
Alex Stephen
2026-03-30 07:31:20 -07:00
committed by GitHub
parent a255529c6b
commit 9cf410478c
11 changed files with 303 additions and 22 deletions

View File

@@ -137,4 +137,105 @@ describe('parseSlashCommand', () => {
expect(result.args).toBe('');
expect(result.canonicalPath).toEqual([]);
});
describe('backtracking', () => {
const backtrackingCommands: readonly SlashCommand[] = [
{
name: 'parent',
description: 'Parent command',
kind: CommandKind.BUILT_IN,
action: async () => {},
subCommands: [
{
name: 'notakes',
description: 'Subcommand that does not take arguments',
kind: CommandKind.BUILT_IN,
takesArgs: false,
action: async () => {},
},
{
name: 'takes',
description: 'Subcommand that takes arguments',
kind: CommandKind.BUILT_IN,
takesArgs: true,
action: async () => {},
},
],
},
];
it('should backtrack to parent if subcommand has takesArgs: false and args are provided', () => {
const result = parseSlashCommand(
'/parent notakes some prompt',
backtrackingCommands,
);
expect(result.commandToExecute?.name).toBe('parent');
expect(result.args).toBe('notakes some prompt');
expect(result.canonicalPath).toEqual(['parent']);
});
it('should NOT backtrack if subcommand has takesArgs: false but NO args are provided', () => {
const result = parseSlashCommand('/parent notakes', backtrackingCommands);
expect(result.commandToExecute?.name).toBe('notakes');
expect(result.args).toBe('');
expect(result.canonicalPath).toEqual(['parent', 'notakes']);
});
it('should NOT backtrack if subcommand has takesArgs: true and args are provided', () => {
const result = parseSlashCommand(
'/parent takes some args',
backtrackingCommands,
);
expect(result.commandToExecute?.name).toBe('takes');
expect(result.args).toBe('some args');
expect(result.canonicalPath).toEqual(['parent', 'takes']);
});
it('should NOT backtrack if parent has NO action', () => {
const noActionCommands: readonly SlashCommand[] = [
{
name: 'parent',
description: 'Parent without action',
kind: CommandKind.BUILT_IN,
subCommands: [
{
name: 'notakes',
description: 'Subcommand without args',
kind: CommandKind.BUILT_IN,
takesArgs: false,
action: async () => {},
},
],
},
];
const result = parseSlashCommand(
'/parent notakes some args',
noActionCommands,
);
// It stays with the subcommand because parent can't handle it
expect(result.commandToExecute?.name).toBe('notakes');
expect(result.args).toBe('some args');
expect(result.canonicalPath).toEqual(['parent', 'notakes']);
});
it('should NOT backtrack if subcommand is NOT marked with takesArgs: false', () => {
const result = parseSlashCommand(
'/parent takes some args',
backtrackingCommands,
);
expect(result.commandToExecute?.name).toBe('takes');
expect(result.args).toBe('some args');
expect(result.canonicalPath).toEqual(['parent', 'takes']);
});
it('should backtrack if subcommand has takesArgs: false and args are provided (like /plan copy foo)', () => {
const result = parseSlashCommand(
'/parent notakes some prompt',
backtrackingCommands,
);
expect(result.commandToExecute?.name).toBe('parent');
expect(result.args).toBe('notakes some prompt');
expect(result.canonicalPath).toEqual(['parent']);
});
});
});

View File

@@ -33,6 +33,7 @@ export const parseSlashCommand = (
let commandToExecute: SlashCommand | undefined;
let pathIndex = 0;
const canonicalPath: string[] = [];
let parentCommand: SlashCommand | undefined;
for (const part of commandPath) {
// TODO: For better performance and architectural clarity, this two-pass
@@ -52,6 +53,7 @@ export const parseSlashCommand = (
}
if (foundCommand) {
parentCommand = commandToExecute;
commandToExecute = foundCommand;
canonicalPath.push(foundCommand.name);
pathIndex++;
@@ -67,5 +69,21 @@ export const parseSlashCommand = (
const args = parts.slice(pathIndex).join(' ');
// Backtrack if the matched (sub)command doesn't take arguments but some were provided,
// AND the parent command is capable of handling them.
if (
commandToExecute &&
commandToExecute.takesArgs === false &&
args.length > 0 &&
parentCommand &&
parentCommand.action
) {
return {
commandToExecute: parentCommand,
args: parts.slice(pathIndex - 1).join(' '),
canonicalPath: canonicalPath.slice(0, -1),
};
}
return { commandToExecute, args, canonicalPath };
};