@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,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
|
+
}
|