Skip to content

Commit 272934a

Browse files
author
Chris Jean
committed
Streamline TUI candidate inspection
1 parent 4cdf64c commit 272934a

4 files changed

Lines changed: 88 additions & 7 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ crucible tui
8282
crucible tui --run previous
8383
```
8484

85-
The TUI loads the selected run archive directly from `.crucible/runs/`, shows run status, leaderboard rows, selected candidate details, and forms for auto run setup, discovery, evaluator generation, competitor generation, adoption, promotion, evaluation, next-round preparation, evolution, reports, index rebuilds, archive queries, and candidate inspection. The new-run form defaults to `run --auto`, so typing the optimization request and pressing Enter is enough to create the run, generate and validate an evaluator, record baseline metrics, and return to a leaderboard. The forms execute the same command paths as the shell CLI, show a live spinner with elapsed time while actions run, allow cancellation requests for long-running actions, and show captured command output when the action finishes. Canceled evaluation work records an event and marks affected unevaluated candidates as `canceled` without overwriting completed `passed` or `failed` results. Canceled generation also records valid unadopted candidate artifacts as `canceled` and records partial candidate directories in the cancellation event.
85+
The TUI loads the selected run archive directly from `.crucible/runs/`, shows run status, leaderboard rows, selected candidate details, and forms for auto run setup, discovery, evaluator generation, competitor generation, adoption, promotion, evaluation, next-round preparation, evolution, reports, index rebuilds, archive queries, and candidate inspection. When a non-baseline candidate has passed, the dashboard selects it by default; otherwise it selects the first non-baseline candidate before falling back to the baseline. The candidate table is the picker for candidate-specific actions, so inspecting the selected candidate runs directly from the table instead of asking for an ID. The new-run form defaults to `run --auto`, so typing the optimization request and pressing Enter is enough to create the run, generate and validate an evaluator, record baseline metrics, and return to a leaderboard. The forms execute the same command paths as the shell CLI, show a live spinner with elapsed time while actions run, allow cancellation requests for long-running actions, and show captured command output when the action finishes. Canceled evaluation work records an event and marks affected unevaluated candidates as `canceled` without overwriting completed `passed` or `failed` results. Canceled generation also records valid unadopted candidate artifacts as `canceled` and records partial candidate directories in the cancellation event.
8686

8787
Create a tournament run from inside an existing project:
8888

@@ -707,6 +707,7 @@ Current priorities:
707707
- [x] Add prompt and TUI actions for evaluator generation on existing runs
708708
- [x] Fail fast with clear guidance when a selected agent executable is unavailable
709709
- [x] Add CLI and TUI candidate promotion back into the source project with dry-run previews and archived promotion reports
710+
- [x] Make TUI candidate inspection act on the selected row without requiring candidate ID entry
710711

711712
Deferred roadmap:
712713

docs/tui.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Code Crucible uses Bubble Tea v1.2.x and Bubbles v0.20.x because that pair suppo
1919

2020
- Keep `crucible tui` as the explicit entrypoint instead of replacing bare `crucible`.
2121
- Use the existing `internal/cli.WorkflowController` from prompt mode and the TUI so both interfaces share command construction and execution.
22-
- The first implementation includes a run dashboard: latest or selected run status, leaderboard rows, selected candidate detail, and common next actions. Actions that have TUI forms are shown as key-first prompts; workflows that are not yet implemented in the TUI use short CLI hints instead of long absolute commands.
22+
- The first implementation includes a run dashboard: latest or selected run status, leaderboard rows, selected candidate detail, and common next actions. The dashboard defaults to a passed non-baseline candidate when one exists, and the candidate table is the picker for candidate-specific actions such as inspect. Actions that have TUI forms are shown as key-first prompts; workflows that are not yet implemented in the TUI use short CLI hints instead of long absolute commands.
2323
- Forms cover low-prompt auto run setup, discovery, evaluator generation, generation, adoption, promotion, evaluation, next-round preparation, evolution, reports, index rebuilds, archive queries, and inspection. The new-run form defaults to `crucible run --auto`, so the operator can type only the optimization request and press Enter to create the run, generate and validate an evaluator, record baseline metrics, and refresh the dashboard. Forms use Bubbles text inputs, execute through the shared CLI controller, show a live spinner with elapsed time while actions run, allow cancellation requests for long-running actions, and show captured output in a scrollable viewport after completion.
2424
- Cancellation is conservative: Code Crucible records a cancellation event and marks affected unevaluated evaluation candidates as `canceled`, but does not overwrite completed `passed` or `failed` results. Canceled generation records valid unadopted candidate artifacts as `canceled` and records partial candidate directories in the same event stream.
2525
- Candidate selection uses the Bubbles table widget, and candidate details use a scrollable viewport.

internal/cli/tui.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,11 @@ func loadTUIDashboard(projectDir, runSelector string) (tuiDashboardData, error)
203203

204204
func newTUIDashboardModel(data tuiDashboardData, message string) tuiDashboardModel {
205205
m := tuiDashboardModel{
206-
data: data,
207-
width: 100,
208-
message: message,
209-
spinner: spinner.New(),
206+
data: data,
207+
selected: defaultTUISelectedIndex(data.Results),
208+
width: 100,
209+
message: message,
210+
spinner: spinner.New(),
210211
}
211212
m.configureBubbles()
212213
return m
@@ -323,7 +324,12 @@ func (m tuiDashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
323324
case "s":
324325
m.openForm(tuiActionQuery)
325326
case "p":
326-
m.openForm(tuiActionInspect)
327+
if m.selectedCandidateID() == "" {
328+
m.message = "No candidate is selected."
329+
return m, nil
330+
}
331+
m.form = m.newForm(tuiActionInspect)
332+
return m.submitForm()
327333
case "up", "down", "k", "j", "home", "end", "pgup", "pgdown":
328334
var cmd tea.Cmd
329335
m.table, cmd = m.table.Update(msg)
@@ -1392,6 +1398,20 @@ func (m tuiDashboardModel) selectedCandidateID() string {
13921398
return m.data.Results[selected].Candidate.ID
13931399
}
13941400

1401+
func defaultTUISelectedIndex(results []model.CandidateResult) int {
1402+
for i, result := range results {
1403+
if result.Status == model.CandidateStatusPassed && !result.Candidate.Baseline {
1404+
return i
1405+
}
1406+
}
1407+
for i, result := range results {
1408+
if !result.Candidate.Baseline {
1409+
return i
1410+
}
1411+
}
1412+
return 0
1413+
}
1414+
13951415
func tuiStatusCounts(results []model.CandidateResult) (passed, failed, canceled, pending int) {
13961416
for _, result := range results {
13971417
switch result.Status {

internal/cli/tui_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func TestTUIDashboardNavigationChangesSelectedCandidate(t *testing.T) {
9191
{Candidate: model.Candidate{ID: "candidate-0001"}},
9292
},
9393
}, "")
94+
dashboard.selected = 0
95+
dashboard.configureBubbles()
9496

9597
updated, _ := dashboard.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}})
9698
navigated := updated.(tuiDashboardModel)
@@ -108,6 +110,64 @@ func TestTUIDashboardNavigationChangesSelectedCandidate(t *testing.T) {
108110
}
109111
}
110112

113+
func TestTUIDashboardDefaultsToFirstNonBaselinePassedCandidate(t *testing.T) {
114+
dashboard := newTUIDashboardModel(tuiDashboardData{
115+
Config: model.RunConfig{ID: "run-1"},
116+
Results: []model.CandidateResult{
117+
{
118+
Candidate: model.Candidate{ID: "candidate-0000-baseline", Baseline: true},
119+
Status: model.CandidateStatusPassed,
120+
Score: 100,
121+
},
122+
{
123+
Candidate: model.Candidate{ID: "candidate-0001"},
124+
Status: model.CandidateStatusPassed,
125+
Score: 95,
126+
},
127+
},
128+
}, "")
129+
130+
if dashboard.selected != 1 {
131+
t.Fatalf("selected = %d, want first non-baseline passed candidate", dashboard.selected)
132+
}
133+
if got := dashboard.selectedCandidateID(); got != "candidate-0001" {
134+
t.Fatalf("selected candidate = %q, want candidate-0001", got)
135+
}
136+
}
137+
138+
func TestTUIInspectKeyRunsSelectedCandidateDirectly(t *testing.T) {
139+
dashboard := newTUIDashboardModel(tuiDashboardData{
140+
ProjectDir: "/tmp/code crucible fixture",
141+
Config: model.RunConfig{
142+
ID: "run-1",
143+
},
144+
Results: []model.CandidateResult{
145+
{
146+
Candidate: model.Candidate{ID: "candidate-0000-baseline", Baseline: true},
147+
Status: model.CandidateStatusPassed,
148+
},
149+
{
150+
Candidate: model.Candidate{ID: "candidate-0001"},
151+
Status: model.CandidateStatusPassed,
152+
},
153+
},
154+
}, "")
155+
156+
updated, cmd := dashboard.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'p'}})
157+
running := updated.(tuiDashboardModel)
158+
if cmd == nil {
159+
t.Fatal("inspect key did not start an action")
160+
}
161+
if !running.busy {
162+
t.Fatal("inspect key did not enter busy action state")
163+
}
164+
for _, want := range []string{"crucible inspect", "--run run-1", "candidate-0001"} {
165+
if !strings.Contains(running.actionCmd, want) {
166+
t.Fatalf("inspect command did not contain %q:\n%s", want, running.actionCmd)
167+
}
168+
}
169+
}
170+
111171
func TestTUIStartsWithoutExistingRun(t *testing.T) {
112172
projectDir := t.TempDir()
113173
data, message, err := loadInitialTUIDashboard(projectDir, "latest")

0 commit comments

Comments
 (0)