diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx index f1f02e4f..570fd481 100644 --- a/desktop/src/__tests__/pages.test.tsx +++ b/desktop/src/__tests__/pages.test.tsx @@ -159,6 +159,22 @@ describe('Content-only pages render without errors', () => { expect(screen.queryByText('/internal-only')).not.toBeInTheDocument() }) + it('EmptySession shows /goal as one command with argument hints, not pseudo subcommands', async () => { + vi.mocked(skillsApi.list).mockResolvedValueOnce({ skills: [] }) + + render() + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '/goal', selectionStart: 5 }, + }) + + expect(await screen.findAllByText('/goal')).toHaveLength(2) + expect(screen.getByText('[status|pause|resume|complete|clear|--tokens |]')).toBeInTheDocument() + expect(screen.getByText('Create or manage an autonomous completion goal')).toBeInTheDocument() + expect(screen.queryByText('/goal status')).not.toBeInTheDocument() + expect(screen.queryByText('/goal --tokens')).not.toBeInTheDocument() + }) + it('EmptySession renders mascot and composer', async () => { let container!: HTMLElement await act(async () => { diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index 579e7b26..54d90a28 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -887,12 +887,12 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro : 'hover:bg-[var(--color-surface-hover)]' }`} > - - + + /{command.name} {command.argumentHint ? ( - + {command.argumentHint} ) : null} diff --git a/desktop/src/components/chat/composerUtils.test.ts b/desktop/src/components/chat/composerUtils.test.ts index 70fd0f26..acc14e5b 100644 --- a/desktop/src/components/chat/composerUtils.test.ts +++ b/desktop/src/components/chat/composerUtils.test.ts @@ -18,9 +18,9 @@ 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' }) + it('closes slash completion once /goal arguments start', () => { + expect(findSlashToken('/goal ', 6)).toBeNull() + expect(findSlashToken('/goal sta', 9)).toBeNull() expect(findSlashToken('/goal build app', 15)).toBeNull() }) @@ -82,28 +82,22 @@ describe('composerUtils', () => { ) }) - 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', - ]) + it('keeps /goal as a single command with argument hints instead of pseudo subcommands', () => { + const commands = filterSlashCommands(mergeSlashCommands([]), 'goal') expect( - filterSlashCommands(mergeSlashCommands([]), 'goal s').map((command) => command.name), - ).toEqual(['goal status']) + commands.map((command) => command.name), + ).toEqual(['goal']) + expect(commands[0]).toMatchObject({ + description: 'Create or manage an autonomous completion goal', + argumentHint: '[status|pause|resume|complete|clear|--tokens |]', + }) + expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal status') + expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal --tokens') }) - it('replaces a /goal subcommand fragment with the selected subcommand', () => { - expect(replaceSlashCommand('/goal sta', 9, 'goal status')).toEqual({ - value: '/goal status ', - cursorPos: 13, - }) + it('does not replace /goal arguments as slash command fragments', () => { + expect(replaceSlashCommand('/goal sta', 9, 'goal status')).toBeNull() }) it('ranks slash command name matches before broad description matches', () => { diff --git a/desktop/src/components/chat/composerUtils.ts b/desktop/src/components/chat/composerUtils.ts index e804495c..b35c36a4 100644 --- a/desktop/src/components/chat/composerUtils.ts +++ b/desktop/src/components/chat/composerUtils.ts @@ -19,19 +19,6 @@ 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 })), @@ -42,7 +29,6 @@ export const FALLBACK_SLASH_COMMANDS = [ description: 'Create or manage an autonomous completion goal', 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' }, @@ -168,7 +154,7 @@ export function findSlashTrigger(value: string, cursorPos: number): SlashTrigger const filter = textBeforeCursor.slice(slashPos + 1) if (filter.includes('\n')) return null - if (/\s/.test(filter) && !/^goal\s+[^\s]*$/i.test(filter)) return null + if (/\s/.test(filter)) return null return { slashPos, filter } } diff --git a/desktop/src/pages/EmptySession.tsx b/desktop/src/pages/EmptySession.tsx index a2ba8154..3ba97704 100644 --- a/desktop/src/pages/EmptySession.tsx +++ b/desktop/src/pages/EmptySession.tsx @@ -640,7 +640,14 @@ export function EmptySession() { index === slashSelectedIndex ? 'bg-[var(--color-surface-hover)]' : 'hover:bg-[var(--color-surface-hover)]' }`} > - /{command.name} + + /{command.name} + {command.argumentHint ? ( + + {command.argumentHint} + + ) : null} + {command.description} ))}