@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.
Files changed (67) hide show
  1. package/README.md +197 -0
  2. package/package.json +51 -0
  3. package/src/App.tsx +224 -0
  4. package/src/cli.ts +772 -0
  5. package/src/components/AgentPanel.tsx +254 -0
  6. package/src/components/Chat.test.tsx +71 -0
  7. package/src/components/Chat.tsx +308 -0
  8. package/src/components/CycleField.tsx +23 -0
  9. package/src/components/ModelPicker.tsx +97 -0
  10. package/src/components/PostUpdatePrompt.tsx +46 -0
  11. package/src/components/ResultsTable.tsx +172 -0
  12. package/src/components/RunCompletePrompt.tsx +90 -0
  13. package/src/components/RunSettingsOverlay.tsx +49 -0
  14. package/src/components/RunsTable.tsx +219 -0
  15. package/src/components/StatsHeader.tsx +100 -0
  16. package/src/daemon.ts +264 -0
  17. package/src/index.tsx +8 -0
  18. package/src/lib/agent/agent-provider.test.ts +133 -0
  19. package/src/lib/agent/claude-provider.ts +277 -0
  20. package/src/lib/agent/codex-provider.ts +413 -0
  21. package/src/lib/agent/default-providers.ts +10 -0
  22. package/src/lib/agent/index.ts +32 -0
  23. package/src/lib/agent/mock-provider.ts +61 -0
  24. package/src/lib/agent/opencode-provider.ts +424 -0
  25. package/src/lib/agent/types.ts +73 -0
  26. package/src/lib/auth.ts +11 -0
  27. package/src/lib/config.ts +152 -0
  28. package/src/lib/daemon-callbacks.ts +59 -0
  29. package/src/lib/daemon-client.ts +16 -0
  30. package/src/lib/daemon-lifecycle.ts +368 -0
  31. package/src/lib/daemon-spawn.ts +122 -0
  32. package/src/lib/daemon-status.ts +189 -0
  33. package/src/lib/daemon-watcher.ts +192 -0
  34. package/src/lib/experiment-loop.ts +679 -0
  35. package/src/lib/experiment.ts +356 -0
  36. package/src/lib/finalize.test.ts +143 -0
  37. package/src/lib/finalize.ts +511 -0
  38. package/src/lib/format.test.ts +32 -0
  39. package/src/lib/format.ts +44 -0
  40. package/src/lib/git.ts +176 -0
  41. package/src/lib/ideas-backlog.test.ts +54 -0
  42. package/src/lib/ideas-backlog.ts +109 -0
  43. package/src/lib/measure.ts +472 -0
  44. package/src/lib/model-options.ts +24 -0
  45. package/src/lib/programs.ts +247 -0
  46. package/src/lib/push-stream.ts +48 -0
  47. package/src/lib/run-context.ts +112 -0
  48. package/src/lib/run-setup.ts +34 -0
  49. package/src/lib/run.ts +383 -0
  50. package/src/lib/syntax-theme.ts +39 -0
  51. package/src/lib/system-prompts/experiment.ts +77 -0
  52. package/src/lib/system-prompts/finalize.ts +90 -0
  53. package/src/lib/system-prompts/index.ts +7 -0
  54. package/src/lib/system-prompts/setup.ts +516 -0
  55. package/src/lib/system-prompts/update.ts +188 -0
  56. package/src/lib/tool-events.ts +99 -0
  57. package/src/lib/validate-measurement.ts +326 -0
  58. package/src/lib/worktree.ts +40 -0
  59. package/src/screens/AuthErrorScreen.tsx +31 -0
  60. package/src/screens/ExecutionScreen.tsx +851 -0
  61. package/src/screens/FirstSetupScreen.tsx +168 -0
  62. package/src/screens/HomeScreen.tsx +406 -0
  63. package/src/screens/PreRunScreen.tsx +206 -0
  64. package/src/screens/SettingsScreen.tsx +189 -0
  65. package/src/screens/SetupScreen.tsx +226 -0
  66. package/src/tui.tsx +17 -0
  67. package/tsconfig.json +17 -0
@@ -0,0 +1,168 @@
1
+ import { useState } from "react"
2
+ import { useKeyboard } from "@opentui/react"
3
+ import type { Screen } from "../lib/programs.ts"
4
+ import type { ModelSlot, ProjectConfig } from "../lib/config.ts"
5
+ import {
6
+ DEFAULT_CONFIG,
7
+ cycleChoice,
8
+ formatEffortSlot,
9
+ formatModelSlot,
10
+ getEffortChoicesForSlot,
11
+ isEffortConfigurable,
12
+ mergeSelectedModelSlot,
13
+ saveProjectConfig,
14
+ PROVIDER_CHOICES,
15
+ PROVIDER_LABELS,
16
+ } from "../lib/config.ts"
17
+ import { checkAuth } from "../lib/auth.ts"
18
+ import { formatShellError } from "../lib/git.ts"
19
+ import { CycleField } from "../components/CycleField.tsx"
20
+ import { ModelPicker } from "../components/ModelPicker.tsx"
21
+
22
+ interface FirstSetupScreenProps {
23
+ cwd: string
24
+ navigate: (screen: Screen) => void
25
+ onConfigChange: (config: ProjectConfig) => void
26
+ }
27
+
28
+ export function FirstSetupScreen({ cwd, navigate, onConfigChange }: FirstSetupScreenProps) {
29
+ const [slot, setSlot] = useState<ModelSlot>({ ...DEFAULT_CONFIG.executionModel })
30
+ const [selected, setSelected] = useState(0)
31
+ const [picking, setPicking] = useState(false)
32
+ const [checking, setChecking] = useState(false)
33
+ const [error, setError] = useState<string | null>(null)
34
+
35
+ const showEffort = isEffortConfigurable(slot)
36
+ const continueRow = showEffort ? 3 : 2
37
+ const rowCount = continueRow + 1
38
+
39
+ async function handleContinue() {
40
+ setChecking(true)
41
+ setError(null)
42
+ try {
43
+ const result = await checkAuth(slot.provider)
44
+ if (!result.authenticated) {
45
+ setError(result.error)
46
+ setChecking(false)
47
+ return
48
+ }
49
+ const config: ProjectConfig = {
50
+ executionModel: { ...slot },
51
+ supportModel: { ...slot },
52
+ ideasBacklogEnabled: true,
53
+ }
54
+ await saveProjectConfig(cwd, config)
55
+ onConfigChange(config)
56
+ navigate("setup")
57
+ } catch (err) {
58
+ setError(formatShellError(err))
59
+ setChecking(false)
60
+ }
61
+ }
62
+
63
+ function cycleProvider(direction: -1 | 1) {
64
+ const nextProvider = cycleChoice(PROVIDER_CHOICES, slot.provider, direction)
65
+ const defaultModel = nextProvider === "claude" ? "sonnet" : "default"
66
+ setSlot({ provider: nextProvider, model: defaultModel, effort: "high" })
67
+ setError(null)
68
+ }
69
+
70
+ function cycleEffort(direction: -1 | 1) {
71
+ if (!isEffortConfigurable(slot)) return
72
+ const choices = getEffortChoicesForSlot(slot)
73
+ setSlot((prev) => ({ ...prev, effort: cycleChoice(choices, prev.effort, direction) }))
74
+ }
75
+
76
+ function cycleValue(direction: -1 | 1) {
77
+ if (selected === 0) return cycleProvider(direction)
78
+ if (selected === 1) {
79
+ setPicking(true)
80
+ return
81
+ }
82
+ if (showEffort && selected === 2) return cycleEffort(direction)
83
+ // continue row — no cycling
84
+ }
85
+
86
+ useKeyboard((key) => {
87
+ if (picking || checking) return
88
+ if (key.name === "up" || key.name === "k")
89
+ setSelected((s) => Math.max(0, s - 1))
90
+ if (key.name === "down" || key.name === "j")
91
+ setSelected((s) => Math.min(rowCount - 1, s + 1))
92
+ if (key.name === "left" || key.name === "h") cycleValue(-1)
93
+ if (key.name === "right" || key.name === "l") cycleValue(1)
94
+ if (key.name === "return") {
95
+ if (selected === 1) {
96
+ setPicking(true)
97
+ } else if (selected === continueRow) {
98
+ handleContinue()
99
+ }
100
+ }
101
+ })
102
+
103
+ if (picking) {
104
+ return (
105
+ <ModelPicker
106
+ cwd={cwd}
107
+ title={`Model — ${PROVIDER_LABELS[slot.provider]}`}
108
+ providerId={slot.provider}
109
+ onCancel={() => setPicking(false)}
110
+ onSelect={(newSlot) => {
111
+ setSlot(mergeSelectedModelSlot(slot, newSlot))
112
+ setPicking(false)
113
+ }}
114
+ />
115
+ )
116
+ }
117
+
118
+ const effortInfo = formatEffortSlot(slot)
119
+ const isContinueFocused = selected === continueRow
120
+
121
+ return (
122
+ <box flexDirection="column" flexGrow={1} border borderStyle="rounded" title="First-Time Setup">
123
+ <box height={1} />
124
+ <text>{" Welcome! Pick a provider and model to get started."}</text>
125
+ <text fg="#888888">{" You can change these later in Settings."}</text>
126
+ <box height={1} />
127
+ <CycleField
128
+ label="Provider"
129
+ value={PROVIDER_LABELS[slot.provider]}
130
+ isFocused={selected === 0}
131
+ />
132
+ <CycleField
133
+ label="Model"
134
+ value={formatModelSlot(slot)}
135
+ isFocused={selected === 1}
136
+ />
137
+ {showEffort && (
138
+ <CycleField
139
+ label="Effort"
140
+ value={effortInfo.label}
141
+ description={effortInfo.description}
142
+ isFocused={selected === 2}
143
+ />
144
+ )}
145
+ <box height={1} />
146
+ <text>
147
+ {isContinueFocused ? (
148
+ <span fg="#7aa2f7"><strong>{" [ Continue ]"}</strong></span>
149
+ ) : (
150
+ <span fg="#888888">{" [ Continue ]"}</span>
151
+ )}
152
+ </text>
153
+ {checking && (
154
+ <>
155
+ <box height={1} />
156
+ <text fg="#888888">{" Checking authentication..."}</text>
157
+ </>
158
+ )}
159
+ {error && (
160
+ <>
161
+ <box height={1} />
162
+ <text fg="#ff5555">{` Auth failed: ${error}`}</text>
163
+ <text fg="#888888">{" Change provider or fix credentials, then try again."}</text>
164
+ </>
165
+ )}
166
+ </box>
167
+ )
168
+ }
@@ -0,0 +1,406 @@
1
+ import { useState, useEffect } from "react"
2
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react"
3
+ import {
4
+ listPrograms,
5
+ loadProgramConfig,
6
+ getProgramDir,
7
+ type ProgramInfo,
8
+ type ProgramConfig,
9
+ type Screen,
10
+ } from "../lib/programs.ts"
11
+ import { listRuns, isRunActive, deleteRun, deleteProgram, type RunInfo } from "../lib/run.ts"
12
+ import { RunsTable } from "../components/RunsTable.tsx"
13
+ import { formatShellError } from "../lib/git.ts"
14
+
15
+ interface HomeScreenProps {
16
+ cwd: string
17
+ navigate: (screen: Screen) => void
18
+ onSelectProgram: (slug: string) => void
19
+ onSelectRun: (run: RunInfo) => void
20
+ onUpdateProgram: (slug: string) => void
21
+ onFinalizeRun: (run: RunInfo) => void
22
+ }
23
+
24
+ interface HomeData {
25
+ programs: ProgramInfo[]
26
+ allRuns: RunInfo[]
27
+ programConfigs: Record<string, ProgramConfig>
28
+ }
29
+
30
+ const MAX_RUNS = 50
31
+ const SIDE_BY_SIDE_MIN_WIDTH = 120
32
+ const PROGRAMS_PANEL_WIDTH = 50
33
+
34
+ function relativeTime(iso: string): string {
35
+ const ms = Date.now() - new Date(iso).getTime()
36
+ const sec = Math.floor(ms / 1000)
37
+ if (sec < 60) return "just now"
38
+ const min = Math.floor(sec / 60)
39
+ if (min < 60) return `${min}m ago`
40
+ const hrs = Math.floor(min / 60)
41
+ if (hrs < 24) return `${hrs}h ago`
42
+ const days = Math.floor(hrs / 24)
43
+ return `${days}d ago`
44
+ }
45
+
46
+ /** Single-pass loader: iterates programs once to build program info, all runs, and configs. */
47
+ async function loadHomeData(cwd: string): Promise<HomeData> {
48
+ const programs = await listPrograms(cwd)
49
+ const allRuns: RunInfo[] = []
50
+ const programInfos: ProgramInfo[] = []
51
+ const programConfigs: Record<string, ProgramConfig> = {}
52
+
53
+ await Promise.all(
54
+ programs.map(async (p) => {
55
+ const programDir = getProgramDir(cwd, p.name)
56
+ const [runs, config] = await Promise.all([
57
+ listRuns(programDir),
58
+ loadProgramConfig(programDir).catch(() => null),
59
+ ])
60
+
61
+ allRuns.push(...runs)
62
+ if (config) programConfigs[p.name] = config
63
+
64
+ const latest = runs.length > 0 ? runs[0] : null
65
+ programInfos.push({
66
+ name: p.name,
67
+ totalRuns: runs.length,
68
+ lastRunDate: latest?.state?.started_at ?? null,
69
+ hasActiveRun: runs.some(isRunActive),
70
+ })
71
+ }),
72
+ )
73
+
74
+ // Programs: active pinned to top, then most recently used first
75
+ programInfos.sort((a, b) => {
76
+ if (a.hasActiveRun && !b.hasActiveRun) return -1
77
+ if (!a.hasActiveRun && b.hasActiveRun) return 1
78
+ if (a.lastRunDate && b.lastRunDate) return b.lastRunDate.localeCompare(a.lastRunDate)
79
+ if (a.lastRunDate && !b.lastRunDate) return -1
80
+ if (!a.lastRunDate && b.lastRunDate) return 1
81
+ return a.name.localeCompare(b.name)
82
+ })
83
+
84
+ // In-progress pinned to top, then newest first
85
+ allRuns.sort((a, b) => {
86
+ const aActive = isRunActive(a)
87
+ const bActive = isRunActive(b)
88
+ if (aActive && !bActive) return -1
89
+ if (!aActive && bActive) return 1
90
+ return b.run_id.localeCompare(a.run_id)
91
+ })
92
+
93
+ return {
94
+ programs: programInfos,
95
+ allRuns: allRuns.slice(0, MAX_RUNS),
96
+ programConfigs,
97
+ }
98
+ }
99
+
100
+ type Panel = "programs" | "runs"
101
+
102
+ export function HomeScreen({ cwd, navigate, onSelectProgram, onSelectRun, onUpdateProgram, onFinalizeRun }: HomeScreenProps) {
103
+ const { width } = useTerminalDimensions()
104
+ const [data, setData] = useState<HomeData | null>(null)
105
+ const [loading, setLoading] = useState(true)
106
+ const [error, setError] = useState("")
107
+ const [selectedIndex, setSelectedIndex] = useState(0)
108
+ const [selectedRunIndex, setSelectedRunIndex] = useState(0)
109
+ const [focusedPanel, setFocusedPanel] = useState<Panel>("programs")
110
+ const [confirmDelete, setConfirmDelete] = useState<RunInfo | null>(null)
111
+ const [confirmDeleteProgram, setConfirmDeleteProgram] = useState<ProgramInfo | null>(null)
112
+ const [deleting, setDeleting] = useState(false)
113
+
114
+ const sideBySide = width >= SIDE_BY_SIDE_MIN_WIDTH
115
+
116
+ useEffect(() => {
117
+ loadHomeData(cwd)
118
+ .then(setData)
119
+ .catch((err: unknown) => {
120
+ setError(formatShellError(err))
121
+ })
122
+ .finally(() => setLoading(false))
123
+ }, [cwd])
124
+
125
+ const programs = data?.programs ?? []
126
+ const selectableRuns = (data?.allRuns ?? []).filter((r) => r.state != null)
127
+
128
+ const performDelete = (run: RunInfo) => {
129
+ setDeleting(true)
130
+ deleteRun(cwd, run)
131
+ .then(() => {
132
+ setConfirmDelete(null)
133
+ setDeleting(false)
134
+ // Reload data
135
+ loadHomeData(cwd).then((newData) => {
136
+ setData(newData)
137
+ // Clamp selection index
138
+ const newSelectableRuns = (newData.allRuns ?? []).filter((r) => r.state != null)
139
+ setSelectedRunIndex((i) => Math.min(i, Math.max(0, newSelectableRuns.length - 1)))
140
+ })
141
+ })
142
+ .catch(() => {
143
+ setConfirmDelete(null)
144
+ setDeleting(false)
145
+ })
146
+ }
147
+
148
+ const performDeleteProgram = (program: ProgramInfo) => {
149
+ setDeleting(true)
150
+ deleteProgram(cwd, program.name)
151
+ .then(() => {
152
+ setConfirmDeleteProgram(null)
153
+ setDeleting(false)
154
+ loadHomeData(cwd).then((newData) => {
155
+ setData(newData)
156
+ setSelectedIndex((i) => Math.min(i, Math.max(0, newData.programs.length - 1)))
157
+ const newSelectableRuns = (newData.allRuns ?? []).filter((r) => r.state != null)
158
+ setSelectedRunIndex((i) => Math.min(i, Math.max(0, newSelectableRuns.length - 1)))
159
+ })
160
+ })
161
+ .catch(() => {
162
+ setConfirmDeleteProgram(null)
163
+ setDeleting(false)
164
+ })
165
+ }
166
+
167
+ useKeyboard((key) => {
168
+ // Confirmation dialog intercepts all keys
169
+ if (confirmDeleteProgram) {
170
+ if (key.name === "return") {
171
+ performDeleteProgram(confirmDeleteProgram)
172
+ } else if (key.name === "escape" || key.name === "n") {
173
+ setConfirmDeleteProgram(null)
174
+ }
175
+ return
176
+ }
177
+ if (confirmDelete) {
178
+ if (key.name === "return") {
179
+ performDelete(confirmDelete)
180
+ } else if (key.name === "escape" || key.name === "n") {
181
+ setConfirmDelete(null)
182
+ }
183
+ return
184
+ }
185
+
186
+ if (key.name === "n") {
187
+ navigate("setup")
188
+ return
189
+ }
190
+ if (key.name === "s") {
191
+ navigate("settings")
192
+ return
193
+ }
194
+ if (key.name === "tab") {
195
+ setFocusedPanel((p) => (p === "programs" ? "runs" : "programs"))
196
+ return
197
+ }
198
+
199
+ if (focusedPanel === "programs" && programs.length > 0) {
200
+ if (key.name === "up" || key.name === "k") {
201
+ setSelectedIndex((i) => Math.max(0, i - 1))
202
+ } else if (key.name === "down" || key.name === "j") {
203
+ setSelectedIndex((i) => Math.min(programs.length - 1, i + 1))
204
+ } else if (key.name === "return") {
205
+ onSelectProgram(programs[selectedIndex].name)
206
+ } else if (key.name === "e") {
207
+ const program = programs[selectedIndex]
208
+ if (program && !program.hasActiveRun) {
209
+ onUpdateProgram(program.name)
210
+ }
211
+ } else if (key.name === "d") {
212
+ const program = programs[selectedIndex]
213
+ if (program && !program.hasActiveRun) {
214
+ setConfirmDeleteProgram(program)
215
+ }
216
+ }
217
+ } else if (focusedPanel === "runs" && selectableRuns.length > 0) {
218
+ if (key.name === "up" || key.name === "k") {
219
+ setSelectedRunIndex((i) => Math.max(0, i - 1))
220
+ } else if (key.name === "down" || key.name === "j") {
221
+ setSelectedRunIndex((i) => Math.min(selectableRuns.length - 1, i + 1))
222
+ } else if (key.name === "return") {
223
+ const run = selectableRuns[selectedRunIndex]
224
+ if (run) onSelectRun(run)
225
+ } else if (key.name === "f") {
226
+ const run = selectableRuns[selectedRunIndex]
227
+ if (run && run.state?.phase === "complete") {
228
+ onFinalizeRun(run)
229
+ }
230
+ } else if (key.name === "d") {
231
+ const run = selectableRuns[selectedRunIndex]
232
+ if (run && !isRunActive(run)) {
233
+ setConfirmDelete(run)
234
+ }
235
+ }
236
+ }
237
+ })
238
+
239
+ if (loading) {
240
+ return (
241
+ <box flexGrow={1} justifyContent="center" alignItems="center">
242
+ <text fg="#888888">Loading...</text>
243
+ </box>
244
+ )
245
+ }
246
+
247
+ if (error) {
248
+ return (
249
+ <box flexGrow={1} justifyContent="center" alignItems="center">
250
+ <text fg="#ff5555" selectable>Error: {error}</text>
251
+ </box>
252
+ )
253
+ }
254
+
255
+ const programsFocused = focusedPanel === "programs"
256
+ const runsFocused = focusedPanel === "runs"
257
+
258
+ const programsPanel = (
259
+ <box
260
+ flexDirection="column"
261
+ flexGrow={sideBySide ? 0 : 1}
262
+ width={sideBySide ? PROGRAMS_PANEL_WIDTH : undefined}
263
+ border
264
+ borderStyle="rounded"
265
+ borderColor={programsFocused ? "#7aa2f7" : "#666666"}
266
+ title="Programs"
267
+ >
268
+ {programs.length === 0 ? (
269
+ <box flexGrow={1} justifyContent="center" alignItems="center">
270
+ <text fg="#666666">No programs yet.</text>
271
+ </box>
272
+ ) : (
273
+ <scrollbox flexGrow={1}>
274
+ {programs.map((p, i) => {
275
+ const isSelected = programsFocused && i === selectedIndex
276
+ return (
277
+ <box
278
+ key={p.name}
279
+ paddingX={1}
280
+ backgroundColor={isSelected ? "#333333" : undefined}
281
+ >
282
+ <text>
283
+ {p.hasActiveRun ? (
284
+ <span fg="#7aa2f7">{"● "}</span>
285
+ ) : (
286
+ <span fg="#333333">{" "}</span>
287
+ )}
288
+ <span fg={isSelected ? "#ffffff" : "#ffffff"}>{p.name}</span>
289
+ <span fg="#666666">
290
+ {" "}
291
+ {p.totalRuns > 0 ? `${p.totalRuns}r` : ""}
292
+ {p.lastRunDate ? ` ${relativeTime(p.lastRunDate)}` : ""}
293
+ </span>
294
+ </text>
295
+ </box>
296
+ )
297
+ })}
298
+ </scrollbox>
299
+ )}
300
+ {programsFocused && programs[selectedIndex]?.hasActiveRun && (
301
+ <box paddingX={1}>
302
+ <text fg="#666666">Cannot edit/delete while run is active</text>
303
+ </box>
304
+ )}
305
+ </box>
306
+ )
307
+
308
+ const runsTableWidth = sideBySide ? width - PROGRAMS_PANEL_WIDTH : width
309
+ const runsPanel = (
310
+ <box
311
+ flexDirection="column"
312
+ flexGrow={1}
313
+ border
314
+ borderStyle="rounded"
315
+ borderColor={runsFocused ? "#7aa2f7" : "#666666"}
316
+ title="Runs"
317
+ >
318
+ <RunsTable
319
+ runs={data?.allRuns ?? []}
320
+ programConfigs={data?.programConfigs ?? {}}
321
+ width={runsTableWidth}
322
+ focused={runsFocused}
323
+ selectedIndex={selectedRunIndex}
324
+ />
325
+ </box>
326
+ )
327
+
328
+ const deleteDialog = confirmDelete ? (
329
+ <box
330
+ position="absolute"
331
+ top="40%"
332
+ left="30%"
333
+ width="40%"
334
+ flexDirection="column"
335
+ border
336
+ borderStyle="rounded"
337
+ borderColor="#ff5555"
338
+ backgroundColor="#1a1b26"
339
+ padding={1}
340
+ title="Delete Run"
341
+ >
342
+ <text fg="#ff5555"><strong>Delete this run?</strong></text>
343
+ <box height={1} />
344
+ <text fg="#ffffff">
345
+ {confirmDelete.state?.program_slug ?? "?"} / {confirmDelete.run_id}
346
+ </text>
347
+ <text fg="#666666">
348
+ {confirmDelete.state?.phase ?? "unknown"} · {confirmDelete.state ? (confirmDelete.state.total_keeps + confirmDelete.state.total_discards + confirmDelete.state.total_crashes) : 0} experiments
349
+ </text>
350
+ <box height={1} />
351
+ <text fg="#666666">
352
+ {deleting ? "Deleting..." : "This will remove the run directory, worktree, and branch."}
353
+ </text>
354
+ <box height={1} />
355
+ <text fg="#888888">Enter to confirm · Esc to cancel</text>
356
+ </box>
357
+ ) : null
358
+
359
+ const deleteProgramDialog = confirmDeleteProgram ? (
360
+ <box
361
+ position="absolute"
362
+ top="40%"
363
+ left="30%"
364
+ width="40%"
365
+ flexDirection="column"
366
+ border
367
+ borderStyle="rounded"
368
+ borderColor="#ff5555"
369
+ backgroundColor="#1a1b26"
370
+ padding={1}
371
+ title="Delete Program"
372
+ >
373
+ <text fg="#ff5555"><strong>Delete this program?</strong></text>
374
+ <box height={1} />
375
+ <text fg="#ffffff">{confirmDeleteProgram.name}</text>
376
+ <text fg="#666666">
377
+ {confirmDeleteProgram.totalRuns} run{confirmDeleteProgram.totalRuns !== 1 ? "s" : ""} will also be deleted
378
+ </text>
379
+ <box height={1} />
380
+ <text fg="#666666">
381
+ {deleting ? "Deleting..." : "This will remove all program files, runs, worktrees, and branches."}
382
+ </text>
383
+ <box height={1} />
384
+ <text fg="#888888">Enter to confirm · Esc to cancel</text>
385
+ </box>
386
+ ) : null
387
+
388
+ if (sideBySide) {
389
+ return (
390
+ <box flexGrow={1} flexDirection="row">
391
+ {programsPanel}
392
+ {runsPanel}
393
+ {deleteDialog}
394
+ {deleteProgramDialog}
395
+ </box>
396
+ )
397
+ }
398
+
399
+ return (
400
+ <box flexGrow={1}>
401
+ {focusedPanel === "programs" ? programsPanel : runsPanel}
402
+ {deleteDialog}
403
+ {deleteProgramDialog}
404
+ </box>
405
+ )
406
+ }