mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Restore /goal as a single desktop slash command
The desktop composer had treated /goal lifecycle arguments as standalone slash-command rows. That made the UI look like /goal had second-level commands even though the CLI contract is one /goal command with arguments. Constraint: Keep /goal status/pause/resume/complete/clear usable as CLI arguments. Rejected: Keep pseudo subcommands in the picker | it contradicts the slash command model and confuses objective entry. Confidence: high Scope-risk: narrow Tested: cd desktop && bun run test -- --run src/components/chat/composerUtils.test.ts src/__tests__/pages.test.tsx src/pages/EmptySession.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build
This commit is contained in:
parent
7dc0b59784
commit
d76c09c3d6
@ -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(<EmptySession />)
|
||||
|
||||
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 <budget>|<objective>]')).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 () => {
|
||||
|
||||
@ -887,12 +887,12 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
: 'hover:bg-[var(--color-surface-hover)]'
|
||||
}`}
|
||||
>
|
||||
<span className="flex min-w-0 max-w-[48%] shrink-0 items-baseline gap-1.5">
|
||||
<span className="truncate text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
<span className="flex min-w-0 max-w-[52%] shrink-0 items-baseline gap-1.5">
|
||||
<span className="shrink-0 text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
/{command.name}
|
||||
</span>
|
||||
{command.argumentHint ? (
|
||||
<span className="truncate font-mono text-[11px] text-[var(--color-text-tertiary)]">
|
||||
<span className="min-w-0 truncate font-mono text-[11px] text-[var(--color-text-tertiary)]">
|
||||
{command.argumentHint}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@ -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 <budget>|<objective>]',
|
||||
})
|
||||
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', () => {
|
||||
|
||||
@ -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: '<budget> <objective>',
|
||||
},
|
||||
] 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 <budget>|<objective>]',
|
||||
},
|
||||
...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 }
|
||||
}
|
||||
|
||||
@ -640,7 +640,14 @@ export function EmptySession() {
|
||||
index === slashSelectedIndex ? 'bg-[var(--color-surface-hover)]' : 'hover:bg-[var(--color-surface-hover)]'
|
||||
}`}
|
||||
>
|
||||
<span className="shrink-0 text-sm font-semibold text-[var(--color-text-primary)]">/{command.name}</span>
|
||||
<span className="flex min-w-0 max-w-[52%] shrink-0 items-baseline gap-1.5">
|
||||
<span className="shrink-0 text-sm font-semibold text-[var(--color-text-primary)]">/{command.name}</span>
|
||||
{command.argumentHint ? (
|
||||
<span className="min-w-0 truncate font-mono text-[11px] text-[var(--color-text-tertiary)]">
|
||||
{command.argumentHint}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-[var(--color-text-tertiary)]">{command.description}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user