@spacek33z/autoauto 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/package.json +51 -0
- package/src/App.tsx +224 -0
- package/src/cli.ts +772 -0
- package/src/components/AgentPanel.tsx +254 -0
- package/src/components/Chat.test.tsx +71 -0
- package/src/components/Chat.tsx +308 -0
- package/src/components/CycleField.tsx +23 -0
- package/src/components/ModelPicker.tsx +97 -0
- package/src/components/PostUpdatePrompt.tsx +46 -0
- package/src/components/ResultsTable.tsx +172 -0
- package/src/components/RunCompletePrompt.tsx +90 -0
- package/src/components/RunSettingsOverlay.tsx +49 -0
- package/src/components/RunsTable.tsx +219 -0
- package/src/components/StatsHeader.tsx +100 -0
- package/src/daemon.ts +264 -0
- package/src/index.tsx +8 -0
- package/src/lib/agent/agent-provider.test.ts +133 -0
- package/src/lib/agent/claude-provider.ts +277 -0
- package/src/lib/agent/codex-provider.ts +413 -0
- package/src/lib/agent/default-providers.ts +10 -0
- package/src/lib/agent/index.ts +32 -0
- package/src/lib/agent/mock-provider.ts +61 -0
- package/src/lib/agent/opencode-provider.ts +424 -0
- package/src/lib/agent/types.ts +73 -0
- package/src/lib/auth.ts +11 -0
- package/src/lib/config.ts +152 -0
- package/src/lib/daemon-callbacks.ts +59 -0
- package/src/lib/daemon-client.ts +16 -0
- package/src/lib/daemon-lifecycle.ts +368 -0
- package/src/lib/daemon-spawn.ts +122 -0
- package/src/lib/daemon-status.ts +189 -0
- package/src/lib/daemon-watcher.ts +192 -0
- package/src/lib/experiment-loop.ts +679 -0
- package/src/lib/experiment.ts +356 -0
- package/src/lib/finalize.test.ts +143 -0
- package/src/lib/finalize.ts +511 -0
- package/src/lib/format.test.ts +32 -0
- package/src/lib/format.ts +44 -0
- package/src/lib/git.ts +176 -0
- package/src/lib/ideas-backlog.test.ts +54 -0
- package/src/lib/ideas-backlog.ts +109 -0
- package/src/lib/measure.ts +472 -0
- package/src/lib/model-options.ts +24 -0
- package/src/lib/programs.ts +247 -0
- package/src/lib/push-stream.ts +48 -0
- package/src/lib/run-context.ts +112 -0
- package/src/lib/run-setup.ts +34 -0
- package/src/lib/run.ts +383 -0
- package/src/lib/syntax-theme.ts +39 -0
- package/src/lib/system-prompts/experiment.ts +77 -0
- package/src/lib/system-prompts/finalize.ts +90 -0
- package/src/lib/system-prompts/index.ts +7 -0
- package/src/lib/system-prompts/setup.ts +516 -0
- package/src/lib/system-prompts/update.ts +188 -0
- package/src/lib/tool-events.ts +99 -0
- package/src/lib/validate-measurement.ts +326 -0
- package/src/lib/worktree.ts +40 -0
- package/src/screens/AuthErrorScreen.tsx +31 -0
- package/src/screens/ExecutionScreen.tsx +851 -0
- package/src/screens/FirstSetupScreen.tsx +168 -0
- package/src/screens/HomeScreen.tsx +406 -0
- package/src/screens/PreRunScreen.tsx +206 -0
- package/src/screens/SettingsScreen.tsx +189 -0
- package/src/screens/SetupScreen.tsx +226 -0
- package/src/tui.tsx +17 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from "react"
|
|
2
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react"
|
|
3
|
+
import type { Screen, ProgramConfig } from "../lib/programs.ts"
|
|
4
|
+
import { getProgramDir } from "../lib/programs.ts"
|
|
5
|
+
import { formatModelLabel, type ModelSlot } from "../lib/config.ts"
|
|
6
|
+
import type { RunState, ExperimentResult, TerminationReason } from "../lib/run.ts"
|
|
7
|
+
import { getRunStats } from "../lib/run.ts"
|
|
8
|
+
import { removeWorktree } from "../lib/worktree.ts"
|
|
9
|
+
import { runFinalize, type FinalizeResult } from "../lib/finalize.ts"
|
|
10
|
+
import { formatShellError } from "../lib/git.ts"
|
|
11
|
+
import {
|
|
12
|
+
spawnDaemon,
|
|
13
|
+
watchRunDir,
|
|
14
|
+
sendStop,
|
|
15
|
+
sendAbort,
|
|
16
|
+
forceKillDaemon,
|
|
17
|
+
reconstructState,
|
|
18
|
+
getDaemonStatus,
|
|
19
|
+
updateMaxExperiments,
|
|
20
|
+
getMaxExperiments,
|
|
21
|
+
type DaemonWatcher,
|
|
22
|
+
} from "../lib/daemon-client.ts"
|
|
23
|
+
import { RunCompletePrompt } from "../components/RunCompletePrompt.tsx"
|
|
24
|
+
import { RunSettingsOverlay } from "../components/RunSettingsOverlay.tsx"
|
|
25
|
+
import { StatsHeader } from "../components/StatsHeader.tsx"
|
|
26
|
+
import { ResultsTable } from "../components/ResultsTable.tsx"
|
|
27
|
+
import { AgentPanel } from "../components/AgentPanel.tsx"
|
|
28
|
+
import { syntaxStyle } from "../lib/syntax-theme.ts"
|
|
29
|
+
|
|
30
|
+
type ExecutionPhase = "starting" | "running" | "complete" | "finalizing" | "finalize_complete" | "error"
|
|
31
|
+
|
|
32
|
+
function truncateStreamText(prev: string, text: string): string {
|
|
33
|
+
const next = prev + text
|
|
34
|
+
return next.length > 8000 ? next.slice(-6000) : next
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ExecutionScreenProps {
|
|
38
|
+
cwd: string
|
|
39
|
+
programSlug: string
|
|
40
|
+
modelConfig: ModelSlot
|
|
41
|
+
supportModelConfig: ModelSlot
|
|
42
|
+
ideasBacklogEnabled: boolean
|
|
43
|
+
navigate: (screen: Screen) => void
|
|
44
|
+
maxExperiments: number
|
|
45
|
+
/** Use git worktree for isolation (default true). When false, runs in-place in main checkout. */
|
|
46
|
+
useWorktree?: boolean
|
|
47
|
+
/** If set, attach to an existing run instead of starting a new one */
|
|
48
|
+
attachRunId?: string
|
|
49
|
+
readOnly?: boolean
|
|
50
|
+
/** Called when user chooses to update the program (from run complete or error screen) */
|
|
51
|
+
onUpdateProgram?: (programSlug: string) => void
|
|
52
|
+
/** If true, automatically start finalize when the run is loaded as complete */
|
|
53
|
+
autoFinalize?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PHASE_LABELS: Record<string, string> = {
|
|
57
|
+
baseline: "Establishing baseline...",
|
|
58
|
+
agent_running: "Agent running...",
|
|
59
|
+
measuring: "Measuring...",
|
|
60
|
+
reverting: "Reverting...",
|
|
61
|
+
kept: "Kept improvement!",
|
|
62
|
+
idle: "Running experiments...",
|
|
63
|
+
stopping: "Stopping...",
|
|
64
|
+
complete: "Complete",
|
|
65
|
+
finalizing: "Finalizing...",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getPhaseLabel(phase: RunState["phase"], error?: string | null, isStopping = false): string {
|
|
69
|
+
if (isStopping) return "Stopping after current experiment..."
|
|
70
|
+
if (phase === "crashed") return `Crashed: ${error ?? "unknown"}`
|
|
71
|
+
return PHASE_LABELS[phase] ?? phase
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const formatHeaderModelLabel = formatModelLabel
|
|
75
|
+
|
|
76
|
+
function getRunModelConfig(state: RunState | null, fallback: ModelSlot): ModelSlot {
|
|
77
|
+
if (
|
|
78
|
+
state?.model &&
|
|
79
|
+
(state.provider === "claude" || state.provider === "codex" || state.provider === "opencode") &&
|
|
80
|
+
(state.effort === "low" || state.effort === "medium" || state.effort === "high" || state.effort === "max")
|
|
81
|
+
) {
|
|
82
|
+
return { provider: state.provider, model: state.model, effort: state.effort }
|
|
83
|
+
}
|
|
84
|
+
return fallback
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function IdeasPanel({ text }: { text: string }) {
|
|
88
|
+
return (
|
|
89
|
+
<scrollbox flexGrow={1} stickyScroll stickyStart="bottom">
|
|
90
|
+
<box paddingX={1} flexDirection="column">
|
|
91
|
+
<markdown content={text} syntaxStyle={syntaxStyle} conceal />
|
|
92
|
+
</box>
|
|
93
|
+
</scrollbox>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Divider({ width, label }: { width: number; label?: string }) {
|
|
98
|
+
const innerWidth = Math.max(width - 2, 0)
|
|
99
|
+
if (label) {
|
|
100
|
+
const labelStr = `─ ${label} `
|
|
101
|
+
const rest = "─".repeat(Math.max(innerWidth - labelStr.length, 0))
|
|
102
|
+
return <text fg="#666666">{labelStr}{rest}</text>
|
|
103
|
+
}
|
|
104
|
+
return <text fg="#666666">{"─".repeat(innerWidth)}</text>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function ExecutionScreen({ cwd, programSlug, modelConfig, supportModelConfig, ideasBacklogEnabled, navigate, maxExperiments, useWorktree = true, attachRunId, readOnly = false, autoFinalize = false, onUpdateProgram }: ExecutionScreenProps) {
|
|
108
|
+
const { width: termWidth, height: termHeight } = useTerminalDimensions()
|
|
109
|
+
const compact = termHeight < 30
|
|
110
|
+
const [phase, setPhase] = useState<ExecutionPhase>("starting")
|
|
111
|
+
const [runState, setRunState] = useState<RunState | null>(null)
|
|
112
|
+
const [currentPhaseLabel, setCurrentPhaseLabel] = useState(attachRunId ? "Connecting..." : "Starting daemon...")
|
|
113
|
+
const [experimentNumber, setExperimentNumber] = useState(0)
|
|
114
|
+
const [lastError, setLastError] = useState<string | null>(null)
|
|
115
|
+
const [terminationReason, setTerminationReason] = useState<TerminationReason | null>(null)
|
|
116
|
+
|
|
117
|
+
const [results, setResults] = useState<ExperimentResult[]>([])
|
|
118
|
+
const [metricHistory, setMetricHistory] = useState<number[]>([])
|
|
119
|
+
const [agentStreamText, setAgentStreamText] = useState("")
|
|
120
|
+
const [toolStatus, setToolStatus] = useState<string | null>(null)
|
|
121
|
+
const [totalCostUsd, setTotalCostUsd] = useState(0)
|
|
122
|
+
const [programConfig, setProgramConfig] = useState<ProgramConfig | null>(null)
|
|
123
|
+
const [runDir, setRunDir] = useState<string | null>(null)
|
|
124
|
+
const [finalizeResult, setFinalizeResult] = useState<FinalizeResult | null>(null)
|
|
125
|
+
const [tableFocused, setTableFocused] = useState(false)
|
|
126
|
+
const [selectedResult, setSelectedResult] = useState<ExperimentResult | null>(null)
|
|
127
|
+
const [showStopConfirm, setShowStopConfirm] = useState(false)
|
|
128
|
+
const [stopping, setStopping] = useState(false)
|
|
129
|
+
const [showSettings, setShowSettings] = useState(false)
|
|
130
|
+
const [maxExpText, setMaxExpText] = useState(String(maxExperiments))
|
|
131
|
+
const maxExpTextRef = useRef(maxExpText)
|
|
132
|
+
const [settingsError, setSettingsError] = useState<string | null>(null)
|
|
133
|
+
const [ideasText, setIdeasText] = useState("")
|
|
134
|
+
const [showIdeas, setShowIdeas] = useState(true)
|
|
135
|
+
|
|
136
|
+
const secondaryMetricsConfig = useMemo(() => programConfig?.secondary_metrics, [programConfig])
|
|
137
|
+
const ideasVisible = showIdeas && ideasText.length > 0
|
|
138
|
+
|
|
139
|
+
const watcherRef = useRef<DaemonWatcher | null>(null)
|
|
140
|
+
const abortControllerRef = useRef<AbortController>(new AbortController())
|
|
141
|
+
const abortSentRef = useRef(false)
|
|
142
|
+
const abortTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
143
|
+
|
|
144
|
+
const stoppingRef = useRef(false)
|
|
145
|
+
stoppingRef.current = stopping // ref for use inside effect closures
|
|
146
|
+
const parsedMaxExperiments = Number.parseInt(maxExpText, 10)
|
|
147
|
+
const displayMaxExperiments = Number.isFinite(parsedMaxExperiments) && parsedMaxExperiments > 0
|
|
148
|
+
? parsedMaxExperiments
|
|
149
|
+
: maxExperiments
|
|
150
|
+
const headerModelLabel = useMemo(
|
|
151
|
+
() => formatHeaderModelLabel(getRunModelConfig(runState, modelConfig)),
|
|
152
|
+
[runState, modelConfig],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
let cancelled = false
|
|
157
|
+
const programDir = getProgramDir(cwd, programSlug)
|
|
158
|
+
|
|
159
|
+
;(async () => {
|
|
160
|
+
try {
|
|
161
|
+
let activeRunDir: string
|
|
162
|
+
|
|
163
|
+
if (attachRunId) {
|
|
164
|
+
// Attach mode: reconstruct state from existing run
|
|
165
|
+
activeRunDir = `${programDir}/runs/${attachRunId}`
|
|
166
|
+
const status = await getDaemonStatus(activeRunDir)
|
|
167
|
+
if (!status.alive) {
|
|
168
|
+
// Daemon died — show complete state
|
|
169
|
+
try {
|
|
170
|
+
const reconstructed = await reconstructState(activeRunDir, programDir)
|
|
171
|
+
const currentMax = await getMaxExperiments(activeRunDir)
|
|
172
|
+
if (!cancelled) {
|
|
173
|
+
setRunDir(activeRunDir)
|
|
174
|
+
setRunState(reconstructed.state)
|
|
175
|
+
setResults(reconstructed.results)
|
|
176
|
+
setMetricHistory(reconstructed.metricHistory)
|
|
177
|
+
setProgramConfig(reconstructed.programConfig)
|
|
178
|
+
setTotalCostUsd(reconstructed.state.total_cost_usd ?? 0)
|
|
179
|
+
setExperimentNumber(reconstructed.state.experiment_number)
|
|
180
|
+
setAgentStreamText(reconstructed.streamText)
|
|
181
|
+
setIdeasText(reconstructed.ideasText)
|
|
182
|
+
setTerminationReason(reconstructed.state.termination_reason ?? null)
|
|
183
|
+
if (currentMax != null) {
|
|
184
|
+
const text = String(currentMax)
|
|
185
|
+
setMaxExpText(text)
|
|
186
|
+
maxExpTextRef.current = text
|
|
187
|
+
}
|
|
188
|
+
if (reconstructed.state.phase === "crashed") {
|
|
189
|
+
setLastError(reconstructed.state.error ?? "Daemon crashed")
|
|
190
|
+
setPhase("error")
|
|
191
|
+
} else if (reconstructed.state.phase === "complete") {
|
|
192
|
+
setPhase("complete")
|
|
193
|
+
} else {
|
|
194
|
+
setLastError(`Daemon is not running; last phase was ${reconstructed.state.phase}`)
|
|
195
|
+
setPhase("error")
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (err: unknown) {
|
|
199
|
+
if (!cancelled) {
|
|
200
|
+
setLastError(formatShellError(err))
|
|
201
|
+
setPhase("error")
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Daemon alive — reconstruct and watch
|
|
208
|
+
const reconstructed = await reconstructState(activeRunDir, programDir)
|
|
209
|
+
if (!cancelled) {
|
|
210
|
+
setRunDir(activeRunDir)
|
|
211
|
+
setRunState(reconstructed.state)
|
|
212
|
+
setResults(reconstructed.results)
|
|
213
|
+
setMetricHistory(reconstructed.metricHistory)
|
|
214
|
+
setProgramConfig(reconstructed.programConfig)
|
|
215
|
+
setTotalCostUsd(reconstructed.state.total_cost_usd ?? 0)
|
|
216
|
+
setExperimentNumber(reconstructed.state.experiment_number)
|
|
217
|
+
setAgentStreamText(reconstructed.streamText)
|
|
218
|
+
setIdeasText(reconstructed.ideasText)
|
|
219
|
+
setPhase("running")
|
|
220
|
+
// Sync maxExpText from run-config for settings panel
|
|
221
|
+
const currentMax = await getMaxExperiments(activeRunDir)
|
|
222
|
+
if (!cancelled && currentMax != null) {
|
|
223
|
+
const text = String(currentMax)
|
|
224
|
+
setMaxExpText(text)
|
|
225
|
+
maxExpTextRef.current = text
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
// Spawn mode: create worktree, spawn daemon
|
|
230
|
+
const result = await spawnDaemon(cwd, programSlug, modelConfig, maxExperiments, ideasBacklogEnabled, useWorktree)
|
|
231
|
+
if (cancelled) return
|
|
232
|
+
|
|
233
|
+
activeRunDir = result.runDir
|
|
234
|
+
setRunDir(result.runDir)
|
|
235
|
+
|
|
236
|
+
// Load program config for display
|
|
237
|
+
const { loadProgramConfig } = await import("../lib/programs.ts")
|
|
238
|
+
const config = await loadProgramConfig(programDir)
|
|
239
|
+
if (!cancelled) {
|
|
240
|
+
setProgramConfig(config)
|
|
241
|
+
setPhase("running")
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Start watching the run directory
|
|
246
|
+
if (!cancelled) {
|
|
247
|
+
const watcher = watchRunDir(activeRunDir, {
|
|
248
|
+
onStateChange: (state) => {
|
|
249
|
+
if (cancelled) return
|
|
250
|
+
setRunState(state)
|
|
251
|
+
setExperimentNumber(state.experiment_number)
|
|
252
|
+
setTotalCostUsd(state.total_cost_usd ?? 0)
|
|
253
|
+
setCurrentPhaseLabel(getPhaseLabel(state.phase, state.error, stoppingRef.current))
|
|
254
|
+
|
|
255
|
+
// Detect completion
|
|
256
|
+
if (state.phase === "complete" || state.phase === "crashed") {
|
|
257
|
+
setTerminationReason(state.termination_reason ?? null)
|
|
258
|
+
if (state.phase === "crashed") {
|
|
259
|
+
setLastError(state.error ?? "Daemon crashed")
|
|
260
|
+
setPhase("error")
|
|
261
|
+
} else {
|
|
262
|
+
setPhase("complete")
|
|
263
|
+
}
|
|
264
|
+
watcher.stop()
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
onResultsChange: (newResults, newMetricHistory) => {
|
|
268
|
+
if (cancelled) return
|
|
269
|
+
setResults(newResults)
|
|
270
|
+
setMetricHistory(newMetricHistory)
|
|
271
|
+
},
|
|
272
|
+
onStreamChange: (text) => {
|
|
273
|
+
if (cancelled) return
|
|
274
|
+
setAgentStreamText(prev => truncateStreamText(prev, text))
|
|
275
|
+
},
|
|
276
|
+
onStreamReset: () => {
|
|
277
|
+
if (cancelled) return
|
|
278
|
+
setAgentStreamText("")
|
|
279
|
+
setToolStatus(null)
|
|
280
|
+
},
|
|
281
|
+
onToolStatus: (status) => {
|
|
282
|
+
if (cancelled) return
|
|
283
|
+
setToolStatus(status)
|
|
284
|
+
},
|
|
285
|
+
onIdeasChange: (text) => {
|
|
286
|
+
if (cancelled) return
|
|
287
|
+
setIdeasText(text)
|
|
288
|
+
},
|
|
289
|
+
onDaemonDied: () => {
|
|
290
|
+
if (cancelled) return
|
|
291
|
+
// Re-read final state
|
|
292
|
+
reconstructState(activeRunDir, programDir).then((final) => {
|
|
293
|
+
if (cancelled) return
|
|
294
|
+
setRunState(final.state)
|
|
295
|
+
setResults(final.results)
|
|
296
|
+
setMetricHistory(final.metricHistory)
|
|
297
|
+
setTerminationReason(final.state.termination_reason ?? null)
|
|
298
|
+
if (final.state.phase === "crashed") {
|
|
299
|
+
setLastError(final.state.error ?? "Daemon died unexpectedly")
|
|
300
|
+
setPhase("error")
|
|
301
|
+
} else if (final.state.phase === "complete") {
|
|
302
|
+
setPhase("complete")
|
|
303
|
+
} else {
|
|
304
|
+
setLastError(`Daemon died unexpectedly while ${final.state.phase}`)
|
|
305
|
+
setPhase("error")
|
|
306
|
+
}
|
|
307
|
+
}).catch(() => {
|
|
308
|
+
if (!cancelled) {
|
|
309
|
+
setLastError("Daemon died and state could not be read")
|
|
310
|
+
setPhase("error")
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
watcher.stop()
|
|
314
|
+
},
|
|
315
|
+
}, { startAtEnd: Boolean(attachRunId) })
|
|
316
|
+
watcherRef.current = watcher
|
|
317
|
+
}
|
|
318
|
+
} catch (err: unknown) {
|
|
319
|
+
if (!cancelled) {
|
|
320
|
+
setLastError(formatShellError(err))
|
|
321
|
+
setPhase("error")
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
})()
|
|
325
|
+
|
|
326
|
+
return () => {
|
|
327
|
+
cancelled = true
|
|
328
|
+
watcherRef.current?.stop()
|
|
329
|
+
}
|
|
330
|
+
}, [cwd, programSlug, modelConfig, maxExperiments, useWorktree, attachRunId, ideasBacklogEnabled])
|
|
331
|
+
|
|
332
|
+
const cleanupRunEnvironment = useCallback(async () => {
|
|
333
|
+
if (runState?.in_place) {
|
|
334
|
+
if (runState.original_branch) {
|
|
335
|
+
const { checkoutBranch } = await import("../lib/git.ts")
|
|
336
|
+
await checkoutBranch(cwd, runState.original_branch).catch(() => {})
|
|
337
|
+
}
|
|
338
|
+
} else if (runState?.worktree_path) {
|
|
339
|
+
await removeWorktree(cwd, runState.worktree_path).catch(() => {})
|
|
340
|
+
}
|
|
341
|
+
}, [cwd, runState])
|
|
342
|
+
|
|
343
|
+
const handleAbandon = useCallback(async () => {
|
|
344
|
+
await cleanupRunEnvironment()
|
|
345
|
+
navigate("home")
|
|
346
|
+
}, [cleanupRunEnvironment, navigate])
|
|
347
|
+
|
|
348
|
+
const handleFinalize = useCallback(async () => {
|
|
349
|
+
if (!runState || !runDir || !programConfig) return
|
|
350
|
+
|
|
351
|
+
const finalizeAbort = new AbortController()
|
|
352
|
+
abortControllerRef.current = finalizeAbort
|
|
353
|
+
|
|
354
|
+
setPhase("finalizing")
|
|
355
|
+
setAgentStreamText("")
|
|
356
|
+
setToolStatus(null)
|
|
357
|
+
setCurrentPhaseLabel("Finalizing...")
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const result = await runFinalize(
|
|
361
|
+
cwd,
|
|
362
|
+
programSlug,
|
|
363
|
+
runDir,
|
|
364
|
+
runState,
|
|
365
|
+
programConfig,
|
|
366
|
+
supportModelConfig,
|
|
367
|
+
{
|
|
368
|
+
onStreamText: (text) => setAgentStreamText(prev => truncateStreamText(prev, text)),
|
|
369
|
+
onToolStatus: (status) => setToolStatus(status),
|
|
370
|
+
},
|
|
371
|
+
finalizeAbort.signal,
|
|
372
|
+
runState.in_place ? undefined : runState.worktree_path, // In-place mode uses projectRoot (cwd)
|
|
373
|
+
)
|
|
374
|
+
// In-place mode: restore original branch after finalize
|
|
375
|
+
if (runState.in_place && runState.original_branch) {
|
|
376
|
+
const { checkoutBranch } = await import("../lib/git.ts")
|
|
377
|
+
await checkoutBranch(cwd, runState.original_branch).catch(() => {})
|
|
378
|
+
}
|
|
379
|
+
setFinalizeResult(result)
|
|
380
|
+
setTotalCostUsd(prev => prev + (result.cost?.total_cost_usd ?? 0))
|
|
381
|
+
setPhase("finalize_complete")
|
|
382
|
+
} catch (err: unknown) {
|
|
383
|
+
setLastError(formatShellError(err, "Finalize failed"))
|
|
384
|
+
setPhase("error")
|
|
385
|
+
}
|
|
386
|
+
}, [cwd, programSlug, runDir, runState, programConfig, supportModelConfig])
|
|
387
|
+
|
|
388
|
+
const handleUpdateProgram = useCallback(async () => {
|
|
389
|
+
await cleanupRunEnvironment()
|
|
390
|
+
onUpdateProgram?.(programSlug)
|
|
391
|
+
}, [cleanupRunEnvironment, programSlug, onUpdateProgram])
|
|
392
|
+
|
|
393
|
+
// Auto-finalize: trigger finalize immediately when attaching to a completed run with autoFinalize
|
|
394
|
+
const autoFinalizeTriggered = useRef(false)
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (autoFinalize && phase === "complete" && !autoFinalizeTriggered.current) {
|
|
397
|
+
autoFinalizeTriggered.current = true
|
|
398
|
+
handleFinalize()
|
|
399
|
+
}
|
|
400
|
+
}, [autoFinalize, phase, handleFinalize])
|
|
401
|
+
|
|
402
|
+
useKeyboard((key) => {
|
|
403
|
+
if (phase === "finalize_complete") {
|
|
404
|
+
if (key.name === "escape") {
|
|
405
|
+
navigate("home")
|
|
406
|
+
}
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (phase === "error") {
|
|
411
|
+
if (key.name === "escape") {
|
|
412
|
+
navigate("home")
|
|
413
|
+
} else if (key.name === "u" && runState && !readOnly && onUpdateProgram) {
|
|
414
|
+
handleUpdateProgram()
|
|
415
|
+
}
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (phase === "complete" && readOnly) {
|
|
420
|
+
if (key.name === "escape") {
|
|
421
|
+
navigate("home")
|
|
422
|
+
} else if (key.name === "i") {
|
|
423
|
+
setShowIdeas(v => !v)
|
|
424
|
+
} else if (key.name === "f") {
|
|
425
|
+
handleFinalize()
|
|
426
|
+
}
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (phase === "complete") {
|
|
431
|
+
// Escape navigates home; RunCompletePrompt handles the rest
|
|
432
|
+
if (key.name === "escape") {
|
|
433
|
+
navigate("home")
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Settings overlay
|
|
440
|
+
if (showSettings) {
|
|
441
|
+
if (key.name === "escape") {
|
|
442
|
+
setShowSettings(false)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
if (key.name === "backspace" || /^\d$/.test(key.name)) {
|
|
446
|
+
const prev = maxExpTextRef.current
|
|
447
|
+
const next = key.name === "backspace" ? prev.slice(0, -1) : prev + key.name
|
|
448
|
+
maxExpTextRef.current = next
|
|
449
|
+
setMaxExpText(next)
|
|
450
|
+
|
|
451
|
+
// Validate and auto-save
|
|
452
|
+
const parsed = parseInt(next, 10)
|
|
453
|
+
if (next === "" || isNaN(parsed) || parsed < 1) {
|
|
454
|
+
setSettingsError("Must be a positive integer")
|
|
455
|
+
} else if (parsed < experimentNumber) {
|
|
456
|
+
setSettingsError(`Must be at least ${experimentNumber} (experiments already done)`)
|
|
457
|
+
} else {
|
|
458
|
+
setSettingsError(null)
|
|
459
|
+
if (runDir) updateMaxExperiments(runDir, parsed)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Stop confirmation dialog
|
|
466
|
+
if (showStopConfirm) {
|
|
467
|
+
if (key.name === "y") {
|
|
468
|
+
setShowStopConfirm(false)
|
|
469
|
+
setStopping(true)
|
|
470
|
+
setCurrentPhaseLabel("Stopping after current experiment...")
|
|
471
|
+
if (runDir) sendStop(runDir).catch(() => {})
|
|
472
|
+
} else if (key.name === "n" || key.name === "escape") {
|
|
473
|
+
setShowStopConfirm(false)
|
|
474
|
+
}
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// During execution: Tab to toggle table focus
|
|
479
|
+
if ((phase === "starting" || phase === "running") && key.name === "tab") {
|
|
480
|
+
setTableFocused(f => !f)
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Escape: deselect first, then unfocus table, then detach (go back while daemon continues)
|
|
485
|
+
if (key.name === "escape") {
|
|
486
|
+
if (selectedResult) {
|
|
487
|
+
setSelectedResult(null)
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
if (tableFocused) {
|
|
491
|
+
setTableFocused(false)
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
// Detach from daemon — it keeps running in background
|
|
495
|
+
watcherRef.current?.stop()
|
|
496
|
+
navigate("home")
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Stop/abort during execution
|
|
501
|
+
if (phase === "starting" || phase === "running") {
|
|
502
|
+
if (key.name === "i") {
|
|
503
|
+
setShowIdeas(v => !v)
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (key.name === "s") {
|
|
508
|
+
setShowSettings(true)
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (key.name === "q") {
|
|
513
|
+
// Show stop confirmation
|
|
514
|
+
setShowStopConfirm(true)
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (key.ctrl && key.name === "c") {
|
|
519
|
+
if (abortSentRef.current) {
|
|
520
|
+
// Second Ctrl+C: force kill after timeout
|
|
521
|
+
if (runDir) {
|
|
522
|
+
forceKillDaemon(runDir).catch(() => {})
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
// First Ctrl+C: abort
|
|
526
|
+
abortSentRef.current = true
|
|
527
|
+
if (runDir) sendAbort(runDir).catch(() => {})
|
|
528
|
+
// Set up SIGKILL escalation after 5s
|
|
529
|
+
abortTimerRef.current = setTimeout(() => {
|
|
530
|
+
if (runDir) forceKillDaemon(runDir).catch(() => {})
|
|
531
|
+
}, 5_000)
|
|
532
|
+
}
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// During finalize: Ctrl+C to abort
|
|
538
|
+
if (phase === "finalizing" && (key.ctrl && key.name === "c")) {
|
|
539
|
+
abortControllerRef.current.abort()
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// Clean up abort timer on unmount
|
|
544
|
+
useEffect(() => {
|
|
545
|
+
return () => {
|
|
546
|
+
if (abortTimerRef.current) clearTimeout(abortTimerRef.current)
|
|
547
|
+
}
|
|
548
|
+
}, [])
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<box flexDirection="column" flexGrow={1}>
|
|
552
|
+
{(phase === "starting" || phase === "running") && (
|
|
553
|
+
<box flexDirection="column" flexGrow={1} border borderStyle="rounded" title={`${programSlug}`}>
|
|
554
|
+
<StatsHeader
|
|
555
|
+
experimentNumber={experimentNumber}
|
|
556
|
+
maxExperiments={displayMaxExperiments}
|
|
557
|
+
width={termWidth}
|
|
558
|
+
modelLabel={headerModelLabel}
|
|
559
|
+
totalKeeps={runState?.total_keeps ?? 0}
|
|
560
|
+
totalDiscards={runState?.total_discards ?? 0}
|
|
561
|
+
totalCrashes={runState?.total_crashes ?? 0}
|
|
562
|
+
currentBaseline={runState?.current_baseline ?? 0}
|
|
563
|
+
originalBaseline={runState?.original_baseline ?? 0}
|
|
564
|
+
bestMetric={runState?.best_metric ?? 0}
|
|
565
|
+
direction={programConfig?.direction ?? "lower"}
|
|
566
|
+
metricField={programConfig?.metric_field ?? "metric"}
|
|
567
|
+
totalCostUsd={totalCostUsd}
|
|
568
|
+
metricHistory={metricHistory}
|
|
569
|
+
currentPhaseLabel={currentPhaseLabel}
|
|
570
|
+
improvementPct={runState && programConfig ? getRunStats(runState, programConfig.direction).improvement_pct : 0}
|
|
571
|
+
/>
|
|
572
|
+
|
|
573
|
+
{compact ? (
|
|
574
|
+
<>
|
|
575
|
+
<box paddingX={1}>
|
|
576
|
+
<text>
|
|
577
|
+
{tableFocused ? (
|
|
578
|
+
<>
|
|
579
|
+
<span fg="#7aa2f7"><strong>[ Results ]</strong></span>
|
|
580
|
+
{" "}
|
|
581
|
+
<span fg="#666666">Agent</span>
|
|
582
|
+
</>
|
|
583
|
+
) : (
|
|
584
|
+
<>
|
|
585
|
+
<span fg="#666666">Results</span>
|
|
586
|
+
{" "}
|
|
587
|
+
<span fg="#7aa2f7"><strong>[ {selectedResult ? `Experiment #${selectedResult.experiment_number}` : "Agent"} ]</strong></span>
|
|
588
|
+
</>
|
|
589
|
+
)}
|
|
590
|
+
{" "}
|
|
591
|
+
<span fg="#666666">Tab ⇄</span>
|
|
592
|
+
</text>
|
|
593
|
+
</box>
|
|
594
|
+
{tableFocused ? (
|
|
595
|
+
<ResultsTable
|
|
596
|
+
results={results}
|
|
597
|
+
metricField={programConfig?.metric_field ?? "metric"}
|
|
598
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
599
|
+
width={termWidth}
|
|
600
|
+
experimentNumber={experimentNumber}
|
|
601
|
+
focused={tableFocused}
|
|
602
|
+
selectedResult={selectedResult}
|
|
603
|
+
onSelect={setSelectedResult}
|
|
604
|
+
/>
|
|
605
|
+
) : (
|
|
606
|
+
<AgentPanel
|
|
607
|
+
streamingText={agentStreamText}
|
|
608
|
+
toolStatus={toolStatus}
|
|
609
|
+
isRunning={phase === "running"}
|
|
610
|
+
selectedResult={selectedResult}
|
|
611
|
+
phaseLabel={currentPhaseLabel}
|
|
612
|
+
experimentNumber={experimentNumber}
|
|
613
|
+
|
|
614
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
615
|
+
/>
|
|
616
|
+
)}
|
|
617
|
+
</>
|
|
618
|
+
) : (
|
|
619
|
+
<>
|
|
620
|
+
<Divider width={termWidth} label="Results" />
|
|
621
|
+
|
|
622
|
+
<ResultsTable
|
|
623
|
+
results={results}
|
|
624
|
+
metricField={programConfig?.metric_field ?? "metric"}
|
|
625
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
626
|
+
width={termWidth}
|
|
627
|
+
experimentNumber={experimentNumber}
|
|
628
|
+
focused={tableFocused}
|
|
629
|
+
selectedResult={selectedResult}
|
|
630
|
+
onSelect={setSelectedResult}
|
|
631
|
+
/>
|
|
632
|
+
|
|
633
|
+
{ideasVisible ? (
|
|
634
|
+
<>
|
|
635
|
+
<box flexDirection="row">
|
|
636
|
+
<box flexGrow={3}>
|
|
637
|
+
<Divider width={Math.ceil(termWidth * 0.6)} label={selectedResult ? `Experiment #${selectedResult.experiment_number}` : "Agent"} />
|
|
638
|
+
</box>
|
|
639
|
+
<box flexGrow={2}>
|
|
640
|
+
<Divider width={Math.floor(termWidth * 0.4)} label="Ideas" />
|
|
641
|
+
</box>
|
|
642
|
+
</box>
|
|
643
|
+
<box flexDirection="row" flexGrow={1}>
|
|
644
|
+
<box flexDirection="column" flexGrow={3}>
|
|
645
|
+
<AgentPanel
|
|
646
|
+
streamingText={agentStreamText}
|
|
647
|
+
toolStatus={toolStatus}
|
|
648
|
+
isRunning={phase === "running"}
|
|
649
|
+
selectedResult={selectedResult}
|
|
650
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
651
|
+
/>
|
|
652
|
+
</box>
|
|
653
|
+
<box flexDirection="column" flexGrow={2}>
|
|
654
|
+
<IdeasPanel text={ideasText} />
|
|
655
|
+
</box>
|
|
656
|
+
</box>
|
|
657
|
+
</>
|
|
658
|
+
) : (
|
|
659
|
+
<>
|
|
660
|
+
<Divider width={termWidth} label={selectedResult ? `Experiment #${selectedResult.experiment_number}` : "Agent"} />
|
|
661
|
+
<AgentPanel
|
|
662
|
+
streamingText={agentStreamText}
|
|
663
|
+
toolStatus={toolStatus}
|
|
664
|
+
isRunning={phase === "running"}
|
|
665
|
+
selectedResult={selectedResult}
|
|
666
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
667
|
+
/>
|
|
668
|
+
</>
|
|
669
|
+
)}
|
|
670
|
+
</>
|
|
671
|
+
)}
|
|
672
|
+
|
|
673
|
+
{showSettings && (
|
|
674
|
+
<RunSettingsOverlay
|
|
675
|
+
maxExpText={maxExpText}
|
|
676
|
+
experimentNumber={experimentNumber}
|
|
677
|
+
validationError={settingsError}
|
|
678
|
+
/>
|
|
679
|
+
)}
|
|
680
|
+
|
|
681
|
+
{showStopConfirm && (
|
|
682
|
+
<box paddingX={1}>
|
|
683
|
+
<text fg="#e0af68" selectable>Stop after current experiment finishes? (y/n)</text>
|
|
684
|
+
</box>
|
|
685
|
+
)}
|
|
686
|
+
|
|
687
|
+
{stopping && !showStopConfirm && (
|
|
688
|
+
<box paddingX={1}>
|
|
689
|
+
<text fg="#e0af68" selectable>Stopping after current experiment...</text>
|
|
690
|
+
</box>
|
|
691
|
+
)}
|
|
692
|
+
|
|
693
|
+
{lastError && (
|
|
694
|
+
<box paddingX={1}>
|
|
695
|
+
<text fg="#ff5555" selectable>{lastError}</text>
|
|
696
|
+
</box>
|
|
697
|
+
)}
|
|
698
|
+
</box>
|
|
699
|
+
)}
|
|
700
|
+
|
|
701
|
+
{phase === "complete" && runState && readOnly && (
|
|
702
|
+
<box flexDirection="column" flexGrow={1} border borderStyle="rounded" title={`${programSlug}`}>
|
|
703
|
+
<StatsHeader
|
|
704
|
+
experimentNumber={experimentNumber}
|
|
705
|
+
maxExperiments={displayMaxExperiments}
|
|
706
|
+
width={termWidth}
|
|
707
|
+
modelLabel={headerModelLabel}
|
|
708
|
+
totalKeeps={runState.total_keeps}
|
|
709
|
+
totalDiscards={runState.total_discards}
|
|
710
|
+
totalCrashes={runState.total_crashes}
|
|
711
|
+
currentBaseline={runState.current_baseline}
|
|
712
|
+
originalBaseline={runState.original_baseline}
|
|
713
|
+
bestMetric={runState.best_metric}
|
|
714
|
+
direction={programConfig?.direction ?? "lower"}
|
|
715
|
+
metricField={programConfig?.metric_field ?? "metric"}
|
|
716
|
+
totalCostUsd={totalCostUsd}
|
|
717
|
+
metricHistory={metricHistory}
|
|
718
|
+
currentPhaseLabel="Complete"
|
|
719
|
+
improvementPct={programConfig ? getRunStats(runState, programConfig.direction).improvement_pct : 0}
|
|
720
|
+
/>
|
|
721
|
+
<Divider width={termWidth} label="Results" />
|
|
722
|
+
<ResultsTable
|
|
723
|
+
results={results}
|
|
724
|
+
metricField={programConfig?.metric_field ?? "metric"}
|
|
725
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
726
|
+
width={termWidth}
|
|
727
|
+
experimentNumber={experimentNumber}
|
|
728
|
+
focused={tableFocused}
|
|
729
|
+
selectedResult={selectedResult}
|
|
730
|
+
onSelect={setSelectedResult}
|
|
731
|
+
/>
|
|
732
|
+
<Divider width={termWidth} label={selectedResult ? `Experiment #${selectedResult.experiment_number}` : "Agent"} />
|
|
733
|
+
{ideasVisible ? (
|
|
734
|
+
<box flexDirection="row" flexGrow={1}>
|
|
735
|
+
<box flexDirection="column" flexGrow={3}>
|
|
736
|
+
<AgentPanel
|
|
737
|
+
streamingText={agentStreamText}
|
|
738
|
+
toolStatus={toolStatus}
|
|
739
|
+
isRunning={false}
|
|
740
|
+
selectedResult={selectedResult}
|
|
741
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
742
|
+
/>
|
|
743
|
+
</box>
|
|
744
|
+
<box flexDirection="column" flexGrow={2}>
|
|
745
|
+
<Divider width={Math.floor(termWidth * 0.38)} label="Ideas" />
|
|
746
|
+
<IdeasPanel text={ideasText} />
|
|
747
|
+
</box>
|
|
748
|
+
</box>
|
|
749
|
+
) : (
|
|
750
|
+
<AgentPanel
|
|
751
|
+
streamingText={agentStreamText}
|
|
752
|
+
toolStatus={toolStatus}
|
|
753
|
+
isRunning={false}
|
|
754
|
+
selectedResult={selectedResult}
|
|
755
|
+
secondaryMetrics={secondaryMetricsConfig}
|
|
756
|
+
/>
|
|
757
|
+
)}
|
|
758
|
+
<box paddingX={1}>
|
|
759
|
+
<text fg="#888888">Esc back · f finalize{ideasText.length > 0 ? " · i toggle ideas" : ""}</text>
|
|
760
|
+
</box>
|
|
761
|
+
</box>
|
|
762
|
+
)}
|
|
763
|
+
|
|
764
|
+
{phase === "complete" && runState && !readOnly && (
|
|
765
|
+
<RunCompletePrompt
|
|
766
|
+
state={runState}
|
|
767
|
+
direction={programConfig?.direction ?? "lower"}
|
|
768
|
+
terminationReason={terminationReason}
|
|
769
|
+
error={null}
|
|
770
|
+
onFinalize={handleFinalize}
|
|
771
|
+
onAbandon={handleAbandon}
|
|
772
|
+
onUpdateProgram={handleUpdateProgram}
|
|
773
|
+
/>
|
|
774
|
+
)}
|
|
775
|
+
|
|
776
|
+
{phase === "finalizing" && (
|
|
777
|
+
<box flexDirection="column" flexGrow={1} border borderStyle="rounded" title="Finalize">
|
|
778
|
+
<StatsHeader
|
|
779
|
+
experimentNumber={experimentNumber}
|
|
780
|
+
maxExperiments={displayMaxExperiments}
|
|
781
|
+
width={termWidth}
|
|
782
|
+
modelLabel={headerModelLabel}
|
|
783
|
+
totalKeeps={runState?.total_keeps ?? 0}
|
|
784
|
+
totalDiscards={runState?.total_discards ?? 0}
|
|
785
|
+
totalCrashes={runState?.total_crashes ?? 0}
|
|
786
|
+
currentBaseline={runState?.current_baseline ?? 0}
|
|
787
|
+
originalBaseline={runState?.original_baseline ?? 0}
|
|
788
|
+
bestMetric={runState?.best_metric ?? 0}
|
|
789
|
+
direction={programConfig?.direction ?? "lower"}
|
|
790
|
+
metricField={programConfig?.metric_field ?? "metric"}
|
|
791
|
+
totalCostUsd={totalCostUsd}
|
|
792
|
+
metricHistory={metricHistory}
|
|
793
|
+
currentPhaseLabel={currentPhaseLabel}
|
|
794
|
+
improvementPct={runState && programConfig ? getRunStats(runState, programConfig.direction).improvement_pct : 0}
|
|
795
|
+
/>
|
|
796
|
+
|
|
797
|
+
<Divider width={termWidth} label="Agent" />
|
|
798
|
+
|
|
799
|
+
<AgentPanel
|
|
800
|
+
streamingText={agentStreamText}
|
|
801
|
+
toolStatus={toolStatus}
|
|
802
|
+
isRunning={true}
|
|
803
|
+
/>
|
|
804
|
+
</box>
|
|
805
|
+
)}
|
|
806
|
+
|
|
807
|
+
{phase === "finalize_complete" && finalizeResult && (
|
|
808
|
+
<box flexDirection="column" flexGrow={1} border borderStyle="rounded" title="Finalize Complete">
|
|
809
|
+
<box flexDirection="column" paddingX={1}>
|
|
810
|
+
{finalizeResult.mode === "grouped" && finalizeResult.groups.length > 0 ? (
|
|
811
|
+
<>
|
|
812
|
+
<text fg="#9ece6a" selectable>Created {finalizeResult.groups.length} branch{finalizeResult.groups.length > 1 ? "es" : ""}:</text>
|
|
813
|
+
<box height={1} />
|
|
814
|
+
{finalizeResult.groups.map((g) => (
|
|
815
|
+
<box key={g.name} flexDirection="column">
|
|
816
|
+
<text fg="#9ece6a" selectable> {g.branchName}</text>
|
|
817
|
+
<text fg="#666666" selectable> {g.files.length} file{g.files.length > 1 ? "s" : ""}: {g.files.map((f) => f.split("/").pop()).join(", ")}</text>
|
|
818
|
+
</box>
|
|
819
|
+
))}
|
|
820
|
+
</>
|
|
821
|
+
) : (
|
|
822
|
+
<text fg="#888888" selectable>No group branches created</text>
|
|
823
|
+
)}
|
|
824
|
+
<box height={1} />
|
|
825
|
+
<text fg="#ffffff">Summary saved to run directory. Press Escape to go back.</text>
|
|
826
|
+
</box>
|
|
827
|
+
<scrollbox flexGrow={1} focused>
|
|
828
|
+
<box paddingX={1} flexDirection="column">
|
|
829
|
+
<markdown content={finalizeResult.summary} syntaxStyle={syntaxStyle} />
|
|
830
|
+
</box>
|
|
831
|
+
</scrollbox>
|
|
832
|
+
</box>
|
|
833
|
+
)}
|
|
834
|
+
|
|
835
|
+
{phase === "error" && (
|
|
836
|
+
<box flexDirection="column" flexGrow={1} border borderStyle="rounded" title="Error">
|
|
837
|
+
<box padding={1}>
|
|
838
|
+
<text fg="#ff5555" selectable>{lastError ?? "Unknown error"}</text>
|
|
839
|
+
</box>
|
|
840
|
+
<box padding={1}>
|
|
841
|
+
<text fg="#888888">
|
|
842
|
+
{runState && !readOnly && onUpdateProgram
|
|
843
|
+
? "Press u to update program · Escape to go back"
|
|
844
|
+
: "Press Escape to go back"}
|
|
845
|
+
</text>
|
|
846
|
+
</box>
|
|
847
|
+
</box>
|
|
848
|
+
)}
|
|
849
|
+
</box>
|
|
850
|
+
)
|
|
851
|
+
}
|