Prevent removed goal subcommands from replacing goals

The simplified /goal surface only supports setting a condition and clearing it. Removed subcommand names such as status were still valid free-form objectives, so a user trying the old query flow could overwrite the real goal with a goal named status and make the desktop state look stuck in progress.

Constraint: /goal should stay as /goal <condition> and /goal clear for the prelaunch simplified UX

Rejected: Reintroduce /goal status | it expands the command surface the product direction intentionally removed

Confidence: high

Scope-risk: narrow

Directive: Do not add pseudo subcommands back to the desktop picker unless the CLI command surface is deliberately expanded again

Tested: bun test src/commands/goal/goal.test.tsx src/goals/goalState.test.ts src/goals/goalEvaluator.test.ts src/server/__tests__/ws-memory-events.test.ts

Tested: cd desktop && bun run test -- --run src/components/chat/composerUtils.test.ts src/__tests__/pages.test.tsx src/stores/chatStore.test.ts src/components/chat/MessageList.test.tsx
This commit is contained in:
程序员阿江(Relakkes) 2026-05-16 14:00:48 +08:00
parent 5c5c5933f0
commit 7f74f1bde7
7 changed files with 32 additions and 9 deletions

View File

@ -169,8 +169,8 @@ describe('Content-only pages render without errors', () => {
})
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.getByText('[<condition> | clear]')).toBeInTheDocument()
expect(screen.getByText('Set a completion goal')).toBeInTheDocument()
expect(screen.queryByText('/goal status')).not.toBeInTheDocument()
expect(screen.queryByText('/goal --tokens')).not.toBeInTheDocument()
})

View File

@ -75,8 +75,8 @@ describe('composerUtils', () => {
expect.arrayContaining([
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '[status|pause|resume|complete|clear|--tokens <budget>|<objective>]',
description: 'Set a completion goal',
argumentHint: '[<condition> | clear]',
},
]),
)
@ -89,8 +89,8 @@ describe('composerUtils', () => {
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>]',
description: 'Set a completion goal',
argumentHint: '[<condition> | clear]',
})
expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal status')
expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal --tokens')

View File

@ -26,8 +26,8 @@ export const FALLBACK_SLASH_COMMANDS = [
{ name: 'clear', description: 'Clear conversation history' },
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '[status|pause|resume|complete|clear|--tokens <budget>|<objective>]',
description: 'Set a completion goal',
argumentHint: '[<condition> | clear]',
},
{ name: 'review', description: 'Review code changes' },
{ name: 'commit', description: 'Create a git commit' },

View File

@ -79,6 +79,20 @@ describe('/goal command', () => {
expect(result.options?.shouldQuery).toBeUndefined()
})
test('does not treat removed subcommands as replacement goals', async () => {
switchSession(`goal-command-${crypto.randomUUID()}` as SessionId)
const created = await runGoal('ship the smoke test')
expect(created.result).toBe('Goal set: ship the smoke test')
const status = await runGoal('status')
expect(status.result).toBe('Usage: /goal <condition> | clear')
expect(status.options?.shouldQuery).toBeUndefined()
const cleared = await runGoal('clear')
expect(cleared.result).toBe('Goal cleared: ship the smoke test')
})
test('hydrates completed goal state from persisted slash command history', async () => {
switchSession(`goal-command-${crypto.randomUUID()}` as SessionId)

View File

@ -4,7 +4,7 @@ const goal = {
type: 'local-jsx',
supportsNonInteractive: true,
name: 'goal',
description: 'Set a completion goal that keeps the session working until it is met',
description: 'Set a completion goal',
argumentHint: '[<condition> | clear]',
load: () => import('./goal.js'),
} satisfies Command

View File

@ -23,6 +23,11 @@ describe('goalState', () => {
})
expect(parseGoalCommand('clear')).toEqual({ type: 'clear' })
expect(() => parseGoalCommand('')).toThrow('Usage: /goal <condition> | clear')
expect(() => parseGoalCommand('status')).toThrow('Usage: /goal <condition> | clear')
expect(() => parseGoalCommand('pause')).toThrow('Usage: /goal <condition> | clear')
expect(() => parseGoalCommand('resume')).toThrow('Usage: /goal <condition> | clear')
expect(() => parseGoalCommand('complete')).toThrow('Usage: /goal <condition> | clear')
expect(() => parseGoalCommand('--tokens 100 ship it')).toThrow('Usage: /goal <condition> | clear')
})
test('stores and formats the current thread goal', () => {

View File

@ -25,11 +25,15 @@ export type ParsedGoalCommand =
| { type: 'set'; objective: string }
const goalsByThread = new Map<string, ThreadGoal>()
const RESERVED_GOAL_ARGS = new Set(['status', 'pause', 'resume', 'complete'])
export function parseGoalCommand(args: string): ParsedGoalCommand {
const trimmed = args.trim()
if (!trimmed) throw new Error('Usage: /goal <condition> | clear')
if (trimmed === 'clear') return { type: 'clear' }
if (RESERVED_GOAL_ARGS.has(trimmed) || trimmed.startsWith('--tokens')) {
throw new Error('Usage: /goal <condition> | clear')
}
return { type: 'set', objective: trimmed }
}