diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx index 570fd481..0c60ed13 100644 --- a/desktop/src/__tests__/pages.test.tsx +++ b/desktop/src/__tests__/pages.test.tsx @@ -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 |]')).toBeInTheDocument() - expect(screen.getByText('Create or manage an autonomous completion goal')).toBeInTheDocument() + expect(screen.getByText('[ | clear]')).toBeInTheDocument() + expect(screen.getByText('Set a completion goal')).toBeInTheDocument() expect(screen.queryByText('/goal status')).not.toBeInTheDocument() expect(screen.queryByText('/goal --tokens')).not.toBeInTheDocument() }) diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index d5ade279..d498525f 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -282,7 +282,7 @@ describe('chatStore history mapping', () => { ]) }) - it('restores replacement /goal output as a replacement event', () => { + it('restores repeated /goal set output as the current created event', () => { const messages: MessageEntry[] = [ { id: 'goal-command', @@ -294,16 +294,7 @@ describe('chatStore history mapping', () => { id: 'goal-output', type: 'system', timestamp: '2026-04-06T00:00:01.000Z', - content: [ - '', - 'Goal replaced.', - 'Goal: active', - 'Objective: ship the replacement target', - 'Budget: 0 / unlimited tokens', - 'Elapsed: 0s', - 'Continuations: 0', - '', - ].join('\n'), + content: 'Goal set: ship the replacement target', }, ] @@ -316,10 +307,9 @@ describe('chatStore history mapping', () => { { id: 'goal-output', type: 'goal_event', - action: 'replaced', + action: 'created', status: 'active', objective: 'ship the replacement target', - budget: '0 / unlimited tokens', }, ]) }) @@ -337,16 +327,7 @@ describe('chatStore history mapping', () => { id: 'goal-output', type: 'system', timestamp: '2026-04-06T00:00:01.000Z', - content: [ - '', - 'Goal created.', - 'Goal: active', - 'Objective: ship the smoke test', - 'Budget: 0 / 2,000 tokens', - 'Elapsed: 0s', - 'Continuations: 0', - '', - ].join('\n'), + content: 'Goal set: ship the smoke test', }, { id: 'goal-complete', @@ -354,18 +335,6 @@ describe('chatStore history mapping', () => { timestamp: '2026-04-06T00:00:02.000Z', content: 'Goal marked complete.', }, - { - id: 'goal-status-command', - type: 'system', - timestamp: '2026-04-06T00:00:03.000Z', - content: '/goal\nstatus', - }, - { - id: 'goal-stale-empty-output', - type: 'system', - timestamp: '2026-04-06T00:00:04.000Z', - content: 'No active goal.', - }, ], }) @@ -395,17 +364,6 @@ describe('chatStore history mapping', () => { action: 'completed', message: 'Goal marked complete.', }, - { - id: 'goal-status-command', - type: 'user_text', - content: '/goal status', - }, - { - id: 'goal-stale-empty-output', - type: 'goal_event', - action: 'message', - message: 'No active goal.', - }, ]) expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.activeGoal).toMatchObject({ action: 'completed', @@ -1329,7 +1287,7 @@ describe('chatStore history mapping', () => { useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { type: 'system_notification', subtype: 'goal_event', - message: 'Goal: active', + message: 'Goal set: ship the smoke test', data: { action: 'created', status: 'active', @@ -1360,9 +1318,9 @@ describe('chatStore history mapping', () => { useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { type: 'system_notification', subtype: 'goal_event', - message: 'Goal replaced.', + message: 'Goal set: ship the replacement target', data: { - action: 'replaced', + action: 'created', status: 'active', objective: 'ship the replacement target', budget: '0 / unlimited tokens', @@ -1371,7 +1329,7 @@ describe('chatStore history mapping', () => { }) expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.activeGoal).toMatchObject({ - action: 'replaced', + action: 'created', status: 'active', objective: 'ship the replacement target', budget: '0 / unlimited tokens', diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index 51df7293..19ac96c3 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -1048,7 +1048,6 @@ type AssistantHistoryBlock = { type: string; text?: string; thinking?: string; n type UserHistoryBlock = { type: string; text?: string; tool_use_id?: string; content?: unknown; is_error?: boolean; source?: { data?: string }; mimeType?: string; media_type?: string; name?: string } const TASK_NOTIFICATION_RE = /^\s*[\s\S]*<\/task-notification>$/i -const GOAL_CLEAR_ALIASES = new Set(['clear']) const GOAL_EVENT_ACTIONS = new Set([ 'created', 'replaced', @@ -1211,7 +1210,7 @@ function parseGoalEventFromLocalCommandOutput( if (trimmed === 'Goal cleared.' || trimmed.startsWith('Goal cleared:')) return { action: 'cleared', message: trimmed } if (trimmed === 'Goal marked complete.') return { action: 'completed', message: trimmed } - if (trimmed === 'No active goal.' || trimmed === 'No goal to resume.') return { action: 'message', message: trimmed } + if (trimmed === 'No active goal.') return { action: 'message', message: trimmed } if (trimmed.startsWith('Goal set:')) { const objective = trimmed.slice('Goal set:'.length).trim() return { @@ -1222,52 +1221,7 @@ function parseGoalEventFromLocalCommandOutput( } } - const actionPrefix = trimmed.startsWith('Goal replaced.\n') - ? 'replaced' - : trimmed.startsWith('Goal created.\n') - ? 'created' - : null - const body = actionPrefix - ? trimmed.split(/\r?\n/).slice(1).join('\n').trim() - : trimmed - - const fields = Object.fromEntries( - body - .split(/\r?\n/) - .map((line) => { - const index = line.indexOf(':') - if (index < 0) return null - return [line.slice(0, index).trim().toLowerCase(), line.slice(index + 1).trim()] - }) - .filter((entry): entry is [string, string] => entry !== null), - ) - if (!fields.goal) return command?.name === 'goal' ? { action: 'message', message: trimmed } : null - - return { - action: actionPrefix ?? resolveHistoryGoalAction(command, fields.goal), - status: fields.goal, - objective: fields.objective, - budget: fields.budget, - elapsed: fields.elapsed, - continuations: fields.continuations, - message: trimmed, - } -} - -function resolveHistoryGoalAction( - command: { name: string; args: string } | null, - status: string, -): GoalEventAction { - const args = command?.args.trim() ?? '' - if (args === 'pause') return 'paused' - if (args === 'resume') return 'resumed' - if (GOAL_CLEAR_ALIASES.has(args)) return 'cleared' - if (args === 'complete') return 'completed' - if (!args || args === 'status') return 'status' - if (status === 'active') return 'created' - if (status === 'paused') return 'paused' - if (status === 'complete') return 'completed' - return 'status' + return command?.name === 'goal' ? { action: 'message', message: trimmed } : null } function extractTaskNotification(content: unknown): AgentTaskNotification | null { diff --git a/src/commands/goal/goal.test.tsx b/src/commands/goal/goal.test.tsx index c2f7c971..eea39602 100644 --- a/src/commands/goal/goal.test.tsx +++ b/src/commands/goal/goal.test.tsx @@ -90,28 +90,14 @@ describe('/goal command', () => { ].join('\n')), createCommandInputMessage([ '', - 'Goal created.', - 'Goal: active', - 'Objective: ship persisted goal', - 'Budget: 42 / 2,000 tokens', - 'Elapsed: 1m', - 'Continuations: 3', + 'Goal set: ship persisted goal', '', ].join('\n')), createCommandInputMessage([ '', - 'Goal: complete', - 'Objective: ship persisted goal', - 'Budget: 1,234 / 2,000 tokens', - 'Elapsed: 23m', - 'Continuations: 2', + 'Goal marked complete.', '', ].join('\n')), - createCommandInputMessage([ - '/goal', - 'status', - ].join('\n')), - createCommandInputMessage('No active goal.'), ], }) diff --git a/src/goals/goalEvaluator.test.ts b/src/goals/goalEvaluator.test.ts index 5fa77f73..d965a0b5 100644 --- a/src/goals/goalEvaluator.test.ts +++ b/src/goals/goalEvaluator.test.ts @@ -149,12 +149,7 @@ describe('goalEvaluator', () => { createUserMessage({ content: [ '', - 'Goal created.', - 'Goal: active', - 'Objective: ship persisted goal', - 'Budget: 42 / 2,000 tokens', - 'Elapsed: 1m', - 'Continuations: 3', + 'Goal set: ship persisted goal', '', ].join('\n'), }), @@ -174,8 +169,8 @@ describe('goalEvaluator', () => { expect(decision.action).toBe('continue') const restored = getThreadGoal(threadId) expect(restored?.objective).toBe('ship persisted goal') - expect(restored?.tokensUsed).toBe(42) - expect(restored?.tokenBudget).toBe(2_000) - expect(restored?.continuationCount).toBe(4) + expect(restored?.tokensUsed).toBe(0) + expect(restored?.tokenBudget).toBeNull() + expect(restored?.continuationCount).toBe(1) }) }) diff --git a/src/goals/goalState.ts b/src/goals/goalState.ts index 5a09209b..029164f8 100644 --- a/src/goals/goalState.ts +++ b/src/goals/goalState.ts @@ -220,7 +220,7 @@ function goalFromLocalCommandOutput( if (trimmed === 'Goal cleared.' || trimmed.startsWith('Goal cleared:')) { return null } - if (trimmed === 'No active goal.' || trimmed === 'No goal to resume.') return current + if (trimmed === 'No active goal.') return current if (trimmed === 'Goal marked complete.') { return current ? { ...current, status: 'complete', updatedAt: now } : null } @@ -241,27 +241,7 @@ function goalFromLocalCommandOutput( } } - const body = trimmed.startsWith('Goal created.\n') || trimmed.startsWith('Goal replaced.\n') - ? trimmed.split(/\r?\n/).slice(1).join('\n').trim() - : trimmed - const fields = parseStatusFields(body) - const status = parseGoalStatus(fields.goal) - if (!status || !fields.objective) return current - - const budget = parseBudget(fields.budget) - const elapsedMs = parseElapsed(fields.elapsed) - return { - goalId: randomUUID(), - threadId, - objective: fields.objective, - status, - tokenBudget: budget.tokenBudget, - tokensUsed: budget.tokensUsed, - continuationCount: parseInteger(fields.continuations) ?? 0, - lastReason: fields['latest reason'] ?? null, - createdAt: now - elapsedMs, - updatedAt: now, - } + return current } function messageToText(message: Message): string { @@ -291,70 +271,10 @@ function readXmlTag(text: string, tag: string): string | null { function looksLikeGoalStatusOutput(output: string): boolean { const trimmed = output.trim() return ( - trimmed.startsWith('Goal created.\n') || - trimmed.startsWith('Goal replaced.\n') || trimmed.startsWith('Goal set:') || - trimmed.startsWith('Goal: ') || trimmed === 'Goal cleared.' || trimmed.startsWith('Goal cleared:') || - trimmed === 'Goal marked complete.' + trimmed === 'Goal marked complete.' || + trimmed === 'No active goal.' ) } - -function parseStatusFields(output: string): Record { - return Object.fromEntries( - output - .split(/\r?\n/) - .map((line) => { - const index = line.indexOf(':') - if (index < 0) return null - return [line.slice(0, index).trim().toLowerCase(), line.slice(index + 1).trim()] - }) - .filter((entry): entry is [string, string] => entry !== null), - ) -} - -function parseGoalStatus(raw: string | undefined): ThreadGoalStatus | null { - if ( - raw === 'active' || - raw === 'paused' || - raw === 'complete' || - raw === 'budget_limited' - ) { - return raw - } - return null -} - -function parseBudget(raw: string | undefined): { - tokenBudget: number | null - tokensUsed: number -} { - if (!raw) return { tokenBudget: null, tokensUsed: 0 } - const match = raw.match(/^([\d,]+)\s*\/\s*(unlimited|[\d,]+)\s+tokens$/i) - if (!match) return { tokenBudget: null, tokensUsed: 0 } - return { - tokensUsed: parseInteger(match[1]) ?? 0, - tokenBudget: match[2]?.toLowerCase() === 'unlimited' - ? null - : parseInteger(match[2]) ?? null, - } -} - -function parseElapsed(raw: string | undefined): number { - if (!raw) return 0 - let ms = 0 - for (const match of raw.matchAll(/(\d+)\s*([hms])/g)) { - const value = Number(match[1]) - if (match[2] === 'h') ms += value * 60 * 60 * 1000 - if (match[2] === 'm') ms += value * 60 * 1000 - if (match[2] === 's') ms += value * 1000 - } - return ms -} - -function parseInteger(raw: string | undefined): number | null { - if (!raw) return null - const value = Number(raw.replace(/,/g, '')) - return Number.isFinite(value) ? value : null -} diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts index ab784960..866e86bb 100644 --- a/src/server/__tests__/sessions.test.ts +++ b/src/server/__tests__/sessions.test.ts @@ -758,16 +758,7 @@ describe('SessionService', () => { isSidechain: false, type: 'system', subtype: 'local_command', - content: [ - '', - 'Goal created.', - 'Goal: active', - 'Objective: ship persisted goal', - 'Budget: 0 / unlimited tokens', - 'Elapsed: 0s', - 'Continuations: 0', - '', - ].join('\n'), + content: 'Goal set: ship persisted goal', level: 'info', timestamp: '2026-01-01T00:00:02.000Z', uuid: 'goal-output', @@ -786,7 +777,7 @@ describe('SessionService', () => { { id: 'goal-output', type: 'system', - content: expect.stringContaining('Goal created.'), + content: expect.stringContaining('Goal set: ship persisted goal'), }, { type: 'assistant', diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts index d416eb6f..e617b888 100644 --- a/src/server/services/sessionService.ts +++ b/src/server/services/sessionService.ts @@ -561,13 +561,11 @@ export class SessionService { private isGoalLocalCommandOutput(output: string): boolean { const trimmed = output.trim() return ( - trimmed.startsWith('Goal created.\n') || - trimmed.startsWith('Goal replaced.\n') || - trimmed.startsWith('Goal: ') || + trimmed.startsWith('Goal set:') || + trimmed.startsWith('Goal cleared:') || trimmed === 'Goal cleared.' || trimmed === 'Goal marked complete.' || - trimmed === 'No active goal.' || - trimmed === 'No goal to resume.' + trimmed === 'No active goal.' ) } @@ -614,7 +612,7 @@ export class SessionService { if (commandName !== 'goal') return null const args = this.readXmlTag(entry.content, 'command-args')?.trim() - if (!args || /^(status|pause|resume|complete|clear)\b/i.test(args)) return null + if (!args || /^clear\b/i.test(args)) return null const title = cleanSessionTitleSource(`/goal ${args}`) return title ? title.length > 80 ? title.slice(0, 80) + '...' : title : null diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts index 6989ea99..19cd8a24 100644 --- a/src/server/ws/handler.ts +++ b/src/server/ws/handler.ts @@ -1488,7 +1488,7 @@ function extractGoalEvent( if (trimmed === 'Goal marked complete.') { return { action: 'completed', message: trimmed } } - if (trimmed === 'No active goal.' || trimmed === 'No goal to resume.') { + if (trimmed === 'No active goal.') { return { action: 'message', message: trimmed } } @@ -1502,37 +1502,7 @@ function extractGoalEvent( } } - const actionPrefix = trimmed.startsWith('Goal replaced.\n') - ? 'replaced' - : trimmed.startsWith('Goal created.\n') - ? 'created' - : null - const body = actionPrefix - ? trimmed.split(/\r?\n/).slice(1).join('\n').trim() - : trimmed - - const lines = Object.fromEntries( - body - .split(/\r?\n/) - .map((line) => { - const index = line.indexOf(':') - if (index < 0) return null - return [line.slice(0, index).trim().toLowerCase(), line.slice(index + 1).trim()] - }) - .filter((entry): entry is [string, string] => entry !== null), - ) - - if (!lines.goal) return command?.name === 'goal' ? { action: 'message', message: trimmed } : null - - return { - action: actionPrefix ?? resolveGoalEventAction(command, lines.goal), - status: lines.goal, - objective: lines.objective, - budget: lines.budget, - elapsed: lines.elapsed, - continuations: lines.continuations, - message: trimmed, - } + return command?.name === 'goal' ? { action: 'message', message: trimmed } : null } function looksLikeGoalCommandOutput(output: string): boolean { @@ -1542,28 +1512,10 @@ function looksLikeGoalCommandOutput(output: string): boolean { trimmed.startsWith('Goal cleared:') || trimmed === 'Goal cleared.' || trimmed === 'Goal marked complete.' || - trimmed === 'No active goal.' || - trimmed === 'No goal to resume.' || - trimmed.startsWith('Goal created.\n') || - trimmed.startsWith('Goal replaced.\n') || - trimmed.startsWith('Goal: ') + trimmed === 'No active goal.' ) } -function resolveGoalEventAction( - command: { name: string; args: string } | undefined, - status: string, -): GoalEventData['action'] { - const args = command?.args.trim() ?? '' - if (args === 'pause') return 'paused' - if (args === 'resume') return 'resumed' - if (!args || args === 'status') return 'status' - if (status === 'paused') return 'paused' - if (status === 'complete') return 'completed' - if (status === 'active') return 'created' - return 'status' -} - function getCompactBoundaryMessage(cliMsg: any): string { const message = typeof cliMsg?.message === 'string' ? cliMsg.message.trim() : '' if (message) return message