mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
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:
parent
5c5c5933f0
commit
7f74f1bde7
@ -169,8 +169,8 @@ describe('Content-only pages render without errors', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(await screen.findAllByText('/goal')).toHaveLength(2)
|
expect(await screen.findAllByText('/goal')).toHaveLength(2)
|
||||||
expect(screen.getByText('[status|pause|resume|complete|clear|--tokens <budget>|<objective>]')).toBeInTheDocument()
|
expect(screen.getByText('[<condition> | clear]')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Create or manage an autonomous completion goal')).toBeInTheDocument()
|
expect(screen.getByText('Set a completion goal')).toBeInTheDocument()
|
||||||
expect(screen.queryByText('/goal status')).not.toBeInTheDocument()
|
expect(screen.queryByText('/goal status')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('/goal --tokens')).not.toBeInTheDocument()
|
expect(screen.queryByText('/goal --tokens')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -75,8 +75,8 @@ describe('composerUtils', () => {
|
|||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
{
|
{
|
||||||
name: 'goal',
|
name: 'goal',
|
||||||
description: 'Create or manage an autonomous completion goal',
|
description: 'Set a completion goal',
|
||||||
argumentHint: '[status|pause|resume|complete|clear|--tokens <budget>|<objective>]',
|
argumentHint: '[<condition> | clear]',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@ -89,8 +89,8 @@ describe('composerUtils', () => {
|
|||||||
commands.map((command) => command.name),
|
commands.map((command) => command.name),
|
||||||
).toEqual(['goal'])
|
).toEqual(['goal'])
|
||||||
expect(commands[0]).toMatchObject({
|
expect(commands[0]).toMatchObject({
|
||||||
description: 'Create or manage an autonomous completion goal',
|
description: 'Set a completion goal',
|
||||||
argumentHint: '[status|pause|resume|complete|clear|--tokens <budget>|<objective>]',
|
argumentHint: '[<condition> | clear]',
|
||||||
})
|
})
|
||||||
expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal status')
|
expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal status')
|
||||||
expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal --tokens')
|
expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('goal --tokens')
|
||||||
|
|||||||
@ -26,8 +26,8 @@ export const FALLBACK_SLASH_COMMANDS = [
|
|||||||
{ name: 'clear', description: 'Clear conversation history' },
|
{ name: 'clear', description: 'Clear conversation history' },
|
||||||
{
|
{
|
||||||
name: 'goal',
|
name: 'goal',
|
||||||
description: 'Create or manage an autonomous completion goal',
|
description: 'Set a completion goal',
|
||||||
argumentHint: '[status|pause|resume|complete|clear|--tokens <budget>|<objective>]',
|
argumentHint: '[<condition> | clear]',
|
||||||
},
|
},
|
||||||
{ name: 'review', description: 'Review code changes' },
|
{ name: 'review', description: 'Review code changes' },
|
||||||
{ name: 'commit', description: 'Create a git commit' },
|
{ name: 'commit', description: 'Create a git commit' },
|
||||||
|
|||||||
@ -79,6 +79,20 @@ describe('/goal command', () => {
|
|||||||
expect(result.options?.shouldQuery).toBeUndefined()
|
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 () => {
|
test('hydrates completed goal state from persisted slash command history', async () => {
|
||||||
switchSession(`goal-command-${crypto.randomUUID()}` as SessionId)
|
switchSession(`goal-command-${crypto.randomUUID()}` as SessionId)
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const goal = {
|
|||||||
type: 'local-jsx',
|
type: 'local-jsx',
|
||||||
supportsNonInteractive: true,
|
supportsNonInteractive: true,
|
||||||
name: 'goal',
|
name: 'goal',
|
||||||
description: 'Set a completion goal that keeps the session working until it is met',
|
description: 'Set a completion goal',
|
||||||
argumentHint: '[<condition> | clear]',
|
argumentHint: '[<condition> | clear]',
|
||||||
load: () => import('./goal.js'),
|
load: () => import('./goal.js'),
|
||||||
} satisfies Command
|
} satisfies Command
|
||||||
|
|||||||
@ -23,6 +23,11 @@ describe('goalState', () => {
|
|||||||
})
|
})
|
||||||
expect(parseGoalCommand('clear')).toEqual({ type: 'clear' })
|
expect(parseGoalCommand('clear')).toEqual({ type: 'clear' })
|
||||||
expect(() => parseGoalCommand('')).toThrow('Usage: /goal <condition> | 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', () => {
|
test('stores and formats the current thread goal', () => {
|
||||||
|
|||||||
@ -25,11 +25,15 @@ export type ParsedGoalCommand =
|
|||||||
| { type: 'set'; objective: string }
|
| { type: 'set'; objective: string }
|
||||||
|
|
||||||
const goalsByThread = new Map<string, ThreadGoal>()
|
const goalsByThread = new Map<string, ThreadGoal>()
|
||||||
|
const RESERVED_GOAL_ARGS = new Set(['status', 'pause', 'resume', 'complete'])
|
||||||
|
|
||||||
export function parseGoalCommand(args: string): ParsedGoalCommand {
|
export function parseGoalCommand(args: string): ParsedGoalCommand {
|
||||||
const trimmed = args.trim()
|
const trimmed = args.trim()
|
||||||
if (!trimmed) throw new Error('Usage: /goal <condition> | clear')
|
if (!trimmed) throw new Error('Usage: /goal <condition> | clear')
|
||||||
if (trimmed === 'clear') return { type: '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 }
|
return { type: 'set', objective: trimmed }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user