diff --git a/desktop/src/components/chat/composerUtils.test.ts b/desktop/src/components/chat/composerUtils.test.ts index 2d9b569a..70fd0f26 100644 --- a/desktop/src/components/chat/composerUtils.test.ts +++ b/desktop/src/components/chat/composerUtils.test.ts @@ -18,6 +18,12 @@ describe('composerUtils', () => { expect(findSlashToken('/ review', 8)).toBeNull() }) + it('keeps /goal argument completion active for one subcommand token', () => { + expect(findSlashToken('/goal ', 6)).toEqual({ start: 0, filter: 'goal ' }) + expect(findSlashToken('/goal sta', 9)).toEqual({ start: 0, filter: 'goal sta' }) + expect(findSlashToken('/goal build app', 15)).toBeNull() + }) + it('inserts a slash trigger without appending a trailing space', () => { expect(insertSlashTrigger('', 0)).toEqual({ value: '/', cursorPos: 1 }) expect(insertSlashTrigger('hello', 5)).toEqual({ value: 'hello /', cursorPos: 7 }) @@ -70,12 +76,36 @@ describe('composerUtils', () => { { name: 'goal', description: 'Create or manage an autonomous completion goal', - argumentHint: '|status|pause|resume|clear|complete', + argumentHint: '[status|pause|resume|complete|clear|--tokens |]', }, ]), ) }) + it('suggests /goal subcommands after the /goal argument separator', () => { + expect( + filterSlashCommands(mergeSlashCommands([]), 'goal ').map((command) => command.name), + ).toEqual([ + 'goal status', + 'goal pause', + 'goal resume', + 'goal complete', + 'goal clear', + 'goal --tokens', + ]) + + expect( + filterSlashCommands(mergeSlashCommands([]), 'goal s').map((command) => command.name), + ).toEqual(['goal status']) + }) + + it('replaces a /goal subcommand fragment with the selected subcommand', () => { + expect(replaceSlashCommand('/goal sta', 9, 'goal status')).toEqual({ + value: '/goal status ', + cursorPos: 13, + }) + }) + it('ranks slash command name matches before broad description matches', () => { expect( filterSlashCommands([ diff --git a/desktop/src/components/chat/composerUtils.ts b/desktop/src/components/chat/composerUtils.ts index d56f9f9d..e804495c 100644 --- a/desktop/src/components/chat/composerUtils.ts +++ b/desktop/src/components/chat/composerUtils.ts @@ -19,6 +19,19 @@ export const SLASH_COMMAND_ALIASES = [ { name: 'plugins', target: 'plugin' }, ] as const +export const GOAL_SLASH_SUBCOMMANDS = [ + { name: 'goal status', description: 'Show the current goal status' }, + { name: 'goal pause', description: 'Pause the active goal loop' }, + { name: 'goal resume', description: 'Resume the active goal loop' }, + { name: 'goal complete', description: 'Mark the active goal complete' }, + { name: 'goal clear', description: 'Clear the active goal' }, + { + name: 'goal --tokens', + description: 'Create a goal with a token budget', + argumentHint: ' ', + }, +] as const + export const FALLBACK_SLASH_COMMANDS = [ ...PANEL_SLASH_COMMANDS, ...SETTINGS_SLASH_COMMANDS.map(({ name, description }) => ({ name, description })), @@ -27,8 +40,9 @@ export const FALLBACK_SLASH_COMMANDS = [ { name: 'goal', description: 'Create or manage an autonomous completion goal', - argumentHint: '|status|pause|resume|clear|complete', + argumentHint: '[status|pause|resume|complete|clear|--tokens |]', }, + ...GOAL_SLASH_SUBCOMMANDS, { name: 'review', description: 'Review code changes' }, { name: 'commit', description: 'Create a git commit' }, { name: 'pr', description: 'Create a pull request' }, @@ -127,8 +141,8 @@ export function filterSlashCommands( commands: ReadonlyArray, filter: string, ): SlashCommandOption[] { - const normalized = filter.trim().toLowerCase() - if (!normalized) return [...commands] + const normalized = filter.toLowerCase() + if (!normalized.trim()) return [...commands] return commands .map((command, index) => ({ @@ -148,26 +162,13 @@ export type SlashTrigger = { export function findSlashTrigger(value: string, cursorPos: number): SlashTrigger | null { const textBeforeCursor = value.slice(0, cursorPos) - let slashPos = -1 - - for (let i = textBeforeCursor.length - 1; i >= 0; i--) { - const ch = textBeforeCursor[i]! - if (ch === '/') { - if (i === 0 || /\s/.test(textBeforeCursor[i - 1]!)) { - slashPos = i - break - } - break - } - if (/\s/.test(ch)) { - break - } - } - + const slashPos = textBeforeCursor.lastIndexOf('/') if (slashPos < 0) return null + if (slashPos > 0 && !/\s/.test(textBeforeCursor[slashPos - 1]!)) return null const filter = textBeforeCursor.slice(slashPos + 1) - if (/\s/.test(filter)) return null + if (filter.includes('\n')) return null + if (/\s/.test(filter) && !/^goal\s+[^\s]*$/i.test(filter)) return null return { slashPos, filter } }