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 (
Goal
{goal.objective}
Status:
{formatGoalStatus(goal.status)}
Elapsed: {elapsed} ยท Continuations:{' '}
{goal.continuationCount}
{goal.lastReason ? (
Last update: {goal.lastReason}
) : null}
)
}
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 (
Replace existing goal?
Current goal ({formatGoalStatus(existing.status)}):
{existing.objective}
New objective:
{objective}
Press Enter to replace, Esc to
cancel.
)
}
type AppState = ReturnType
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 {
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 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 ')
return null
}
if (!rest) {
onDone('Usage: /goal edit ')
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 \nThen the agent will auto-continue toward it across turns.',
)
return null
}
const display = (
)
const output = await renderToString(display)
onDone(output)
return null
}
const objective = (sub === 'set' ? rest : trimmed).trim()
if (!objective) {
onDone('Missing objective.\nUsage: /goal ')
return null
}
if (existing && (existing.status === 'pursuing' || existing.status === 'paused')) {
return (
)
}
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
}