| import * as React from 'react' |
| import { randomUUID } from 'crypto' |
| import { getTotalCost as getTotalCostUSD } from '../../cost-tracker.js' |
| import { getTotalTokensUsed } from '../../bootstrap/state.js' |
| import { Box, Text, useInput } from '../../ink.js' |
| import type { Goal, GoalStatus } from '../../state/AppStateStore.js' |
| import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js' |
| import { |
| buildObjectiveUpdatedPrompt, |
| formatElapsed, |
| formatGoalStatus, |
| isGoalContinuationPrompt, |
| } from '../../utils/goal.js' |
| import { clearGoal, saveGoal } from '../../utils/sessionStorage.js' |
| import { enqueue, removeByFilter } from '../../utils/messageQueueManager.js' |
| import { renderToString } from '../../utils/staticRender.js' |
|
|
| function statusColor(status: GoalStatus): string { |
| switch (status) { |
| case 'pursuing': |
| return 'green' |
| case 'paused': |
| return 'yellow' |
| case 'achieved': |
| return 'cyan' |
| case 'blocked': |
| return 'red' |
| case 'usage-limited': |
| return 'gray' |
| case 'budget-limited': |
| return 'magenta' |
| } |
| } |
|
|
| function GoalDisplay({ |
| goal, |
| now, |
| }: { |
| goal: Goal |
| now: number |
| }): React.ReactNode { |
| const elapsed = formatElapsed(now - goal.startedAt) |
|
|
| return ( |
| <Box flexDirection="column"> |
| <Text bold>Goal</Text> |
| <Text>{goal.objective}</Text> |
| <Box marginTop={1}> |
| <Text>Status: </Text> |
| <Text color={statusColor(goal.status)}> |
| {formatGoalStatus(goal.status)} |
| </Text> |
| </Box> |
| <Text dimColor> |
| Elapsed: {elapsed} · Continuations:{' '} |
| {goal.continuationCount} |
| </Text> |
| {goal.lastReason ? ( |
| <Text dimColor>Last update: {goal.lastReason}</Text> |
| ) : null} |
| </Box> |
| ) |
| } |
|
|
| function GoalOverwriteConfirm({ |
| existing, |
| objective, |
| context, |
| onDone, |
| inPlanMode, |
| }: { |
| existing: Goal |
| objective: string |
| context: LocalJSXCommandContext |
| onDone: LocalJSXCommandOnDone |
| inPlanMode: boolean |
| }): React.ReactNode { |
| const [choice, setChoice] = React.useState<'replace' | 'cancel' | null>(null) |
|
|
| useInput((input, key) => { |
| if (choice !== null) return |
| if (input === 'y' || input === 'Y' || key.return) { |
| setChoice('replace') |
| } else if (input === 'n' || input === 'N' || key.escape) { |
| setChoice('cancel') |
| } |
| }) |
|
|
| React.useEffect(() => { |
| if (choice === null) return |
| const { setAppState } = context |
|
|
| if (choice === 'cancel') { |
| onDone('Goal not changed.') |
| return |
| } |
|
|
| const now = Date.now() |
| const newGoal: Goal = { |
| id: randomUUID(), |
| objective, |
| status: 'pursuing', |
| startedAt: now, |
| startCostUSD: getTotalCostUSD(), |
| startTokensUsed: getTotalTokensUsed(), |
| continuationCount: 0, |
| lastUpdatedAt: now, |
| } |
| setAppState((prev: AppState) => ({ ...prev, goal: newGoal } satisfies AppState)) |
| clearQueuedGoalContinuations() |
|
|
| const message = inPlanMode |
| ? `Goal set: ${objective}\nAuto-continuation is disabled while in Plan mode. Exit plan mode (Shift+Tab) to begin pursuit.` |
| : `Goal set: ${objective}\nThe agent will auto-continue toward this objective until it is achieved, blocked, or paused. Use /goal pause, /goal resume, /goal edit, or /goal clear to manage it.` |
| onDone(message, { |
| metaMessages: [ |
| `[goal] Active goal id: ${newGoal.id}. If calling update_goal for this goal, include goal_id='${newGoal.id}'.`, |
| ], |
| }) |
| }, [choice, objective, context, onDone, inPlanMode]) |
|
|
| return ( |
| <Box flexDirection="column"> |
| <Text bold>Replace existing goal?</Text> |
| <Box marginTop={1}> |
| <Text dimColor> |
| Current goal ({formatGoalStatus(existing.status)}): |
| </Text> |
| </Box> |
| <Text>{existing.objective}</Text> |
| <Box marginTop={1}> |
| <Text dimColor>New objective:</Text> |
| </Box> |
| <Text>{objective}</Text> |
| <Box marginTop={1}> |
| <Text dimColor> |
| Press <Text bold>Enter</Text> to replace, <Text bold>Esc</Text> to |
| cancel. |
| </Text> |
| </Box> |
| </Box> |
| ) |
| } |
|
|
| type AppState = ReturnType<typeof context.getAppState> |
|
|
| function setGoal( |
| setAppState: LocalJSXCommandContext['setAppState'], |
| updater: (prev: Goal | undefined) => Goal | undefined, |
| ): void { |
| setAppState((prev: AppState) => { |
| const newGoal = updater(prev.goal) |
| if (newGoal) { |
| saveGoal({ type: 'goal', ...newGoal }) |
| } else { |
| clearGoal() |
| } |
| return { ...prev, goal: newGoal } satisfies AppState |
| }) |
| } |
|
|
| function clearQueuedGoalContinuations(): void { |
| removeByFilter(cmd => isGoalContinuationPrompt(cmd.value)) |
| } |
|
|
| export async function call( |
| onDone: LocalJSXCommandOnDone, |
| context: LocalJSXCommandContext, |
| args: string, |
| ): Promise<React.ReactNode> { |
| const { getAppState, setAppState } = context |
| const trimmed = args.trim() |
| const firstTokenMatch = trimmed.match(/^\S+/) |
| const firstToken = firstTokenMatch?.[0] ?? '' |
| const sub = firstToken.toLowerCase() |
| const rest = firstTokenMatch |
| ? trimmed.slice(firstTokenMatch[0].length).trimStart() |
| : '' |
|
|
| const appState = getAppState() |
| const existing = appState.goal |
| const inPlanMode = appState.toolPermissionContext.mode === 'plan' |
|
|
| if (sub === 'pause') { |
| if (!existing) { |
| onDone('No active goal.') |
| return null |
| } |
| if (existing.status !== 'pursuing') { |
| onDone(`Goal is already ${formatGoalStatus(existing.status)}.`) |
| return null |
| } |
| setGoal(setAppState, g => |
| g ? { ...g, status: 'paused', lastUpdatedAt: Date.now() } : g, |
| ) |
| clearQueuedGoalContinuations() |
| onDone('Goal paused. Auto-continuation suspended.') |
| return null |
| } |
|
|
| if (sub === 'resume') { |
| if (!existing) { |
| onDone('No active goal.') |
| return null |
| } |
| if (existing.status === 'pursuing') { |
| onDone('Goal already pursuing.') |
| return null |
| } |
| if (existing.status === 'achieved') { |
| onDone( |
| `Goal already ${formatGoalStatus(existing.status)}. Use /goal <objective> to start a new one.`, |
| ) |
| return null |
| } |
| setGoal(setAppState, g => |
| g |
| ? { |
| ...g, |
| status: 'pursuing', |
| continuationCount: 0, |
| startedAt: Date.now(), |
| startCostUSD: getTotalCostUSD(), |
| startTokensUsed: getTotalTokensUsed(), |
| lastUpdatedAt: Date.now(), |
| lastReason: undefined, |
| } |
| : g, |
| ) |
| clearQueuedGoalContinuations() |
| onDone( |
| 'Goal resumed. Continuation count and budget window reset; auto-continuation will start on the next idle tick.', |
| ) |
| return null |
| } |
|
|
| if (sub === 'clear') { |
| if (!existing) { |
| onDone('No active goal.') |
| return null |
| } |
| setGoal(setAppState, () => undefined) |
| clearQueuedGoalContinuations() |
| onDone('Goal cleared.') |
| return null |
| } |
|
|
| if (sub === 'edit') { |
| if (!existing) { |
| onDone('No active goal. Set one with: /goal <objective>') |
| return null |
| } |
| if (!rest) { |
| onDone('Usage: /goal edit <new objective>') |
| return null |
| } |
| setGoal(setAppState, g => |
| g |
| ? { |
| ...g, |
| objective: rest, |
| lastUpdatedAt: Date.now(), |
| } |
| : g, |
| ) |
| clearQueuedGoalContinuations() |
| onDone(`Goal objective updated to: ${rest}`, { |
| metaMessages: [ |
| buildObjectiveUpdatedPrompt({ ...existing, objective: rest }), |
| ], |
| }) |
| return null |
| } |
|
|
| if (trimmed === '') { |
| if (!existing) { |
| onDone( |
| 'No active goal. Set one with: /goal <objective>\nThen the agent will auto-continue toward it across turns.', |
| ) |
| return null |
| } |
| const display = ( |
| <GoalDisplay |
| goal={existing as Goal} |
| now={Date.now()} |
| /> |
| ) |
| const output = await renderToString(display) |
| onDone(output) |
| return null |
| } |
|
|
| const objective = (sub === 'set' ? rest : trimmed).trim() |
| if (!objective) { |
| onDone('Missing objective.\nUsage: /goal <objective>') |
| return null |
| } |
|
|
| if (existing && (existing.status === 'pursuing' || existing.status === 'paused')) { |
| return ( |
| <GoalOverwriteConfirm |
| existing={existing} |
| objective={objective} |
| context={context} |
| onDone={onDone} |
| inPlanMode={inPlanMode} |
| /> |
| ) |
| } |
|
|
| const now = Date.now() |
| const newGoal: Goal = { |
| id: randomUUID(), |
| objective, |
| status: 'pursuing', |
| startedAt: now, |
| startCostUSD: getTotalCostUSD(), |
| startTokensUsed: getTotalTokensUsed(), |
| continuationCount: 0, |
| lastUpdatedAt: now, |
| } |
| setGoal(setAppState, () => newGoal) |
| clearQueuedGoalContinuations() |
|
|
| const message = inPlanMode |
| ? `Goal set: ${objective}\nAuto-continuation is disabled while in Plan mode. Exit plan mode (Shift+Tab) to begin pursuit.` |
| : `Goal set: ${objective}\nThe agent will auto-continue toward this objective until it is achieved, blocked, or paused. Use /goal pause, /goal resume, /goal edit, or /goal clear to manage it.` |
| onDone(message, { |
| metaMessages: [ |
| `[goal] Active goal id: ${newGoal.id}. If calling update_goal for this goal, include goal_id='${newGoal.id}'.`, |
| ], |
| }) |
| return null |
| } |