Skip to content

Commit 33bb62b

Browse files
authored
Merge pull request #10 from lewisnsmith/claude/tui-control-room-dispatch-drd8c
TUI Control Room (Layer 2) — filter, live feed, 3-panel layout
2 parents ff48461 + 2cb97aa commit 33bb62b

11 files changed

Lines changed: 1711 additions & 112 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ dist/
44
.DS_Store
55
coverage/
66
project_master_roadmap copy 2.md
7+
.claude/worktrees/
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# TUI Control Room (Layer 2)
2+
3+
**Date:** 2026-04-16
4+
**Status:** APPROVED (eng + CEO). Sequential-only.
5+
**Author:** Lewis + Claude
6+
7+
## Context & Motivation
8+
9+
Flight's TUI is currently a single-pane viewer. Operators working live agent runs need more: the ability to quickly filter down to a specific `tool_name`, `agent_id`, or `execution_outcome`; watch new runs arrive without manual refresh; and annotate verdicts inline without leaving the terminal.
10+
11+
This spec formalizes Layer 2 of the TUI roadmap — the "control room" layer. It ships as three sequential PRs against `src/tui/`. Each PR is a single focused commit that builds on the prior.
12+
13+
---
14+
15+
## PR Boundaries
16+
17+
### PR 1: Filter & Navigation
18+
19+
Adds a `/`-activated filter bar to the main tree view.
20+
21+
- `/` opens an inline filter bar at the bottom of the tree pane.
22+
- Filter operates with AND semantics across three fields: `tool_name`, `agent_id`, `execution_outcome`.
23+
- Filter state is held on `TuiApp` (existing app state struct).
24+
- `refresh()` re-applies the active filter on each poll tick — no separate filter-render path.
25+
- `Enter` drills from the tree into the detail pane for the selected row.
26+
- `Esc` returns to the tree; when focus is on the filter bar, `Esc` also clears the filter and restores the full tree.
27+
- An empty-result placeholder ("No matching entries") is shown when the filter produces zero rows.
28+
29+
### PR 2: Annotations + Live Feed
30+
31+
Adds verdict keybinds and a 1-second poll loop that feeds new rows into a tail pane.
32+
33+
**Verdict keybinds:**
34+
- `g` — good verdict; writes via `runAnnotate`, badge appears on tree row.
35+
- `b` — bad verdict; writes via `runAnnotate`, badge appears on tree row.
36+
- `n` — note modal (reuses existing `createAnnotateModal` from `src/tui/annotate.ts`).
37+
38+
**Live tail feed:**
39+
- `setInterval(..., 1000)` in `TuiApp.start()` polls for new rows.
40+
- Diff logic appends only new rows into the tail pane (avoids full re-render).
41+
- Scroll-lock: tail pane auto-scrolls unless the operator has scrolled up (scroll-up breaks lock; new content arriving re-locks only if operator scrolls back to bottom).
42+
- Tail pane scope follows the current tree selection — switching selection refreshes the tail pane from indexed rows for that session/turn.
43+
44+
**Modal safety:**
45+
- `pausePolling()` called on modal open; `resumePolling()` on modal close. Prevents poll ticks from mutating state during modal render.
46+
- `clearInterval` called in `destroy()` to prevent leaked timers on exit.
47+
48+
**Error handling:**
49+
- Annotation write failure is non-fatal. Surfaces as an ephemeral status line in the tail pane; does not crash or freeze the TUI.
50+
51+
### PR 3: 3-Panel Layout
52+
53+
Extracts a persistent layout module and reorganises the screen into three columns.
54+
55+
- New file: `src/tui/layout.ts`.
56+
- Layout: Tree (30%) | Detail (45%) | Tail (25%) — persistent across the session, not toggled.
57+
- `s` / `v` / `q` open summary / view / query as modal overlays. `Esc` dismisses.
58+
- `Tab` cycles focus: Tree → Detail → Tail → Tree.
59+
- Overlays re-layout on terminal resize (SIGWINCH handling delegated to layout module).
60+
61+
---
62+
63+
## Hard Constraints
64+
65+
### Sequential Only
66+
67+
Each PR depends on the prior:
68+
69+
- PR 1 introduces filter state on `TuiApp`.
70+
- PR 2's poll loop wraps `TuiApp.refresh()` — which must already honour the filter (PR 1) before the loop is added.
71+
- PR 3's layout refactor moves everything PR 1 and PR 2 touched. Attempting layout changes before the earlier code is stable will cause repeated merge conflicts.
72+
73+
No worktree parallelism available. Land in order.
74+
75+
### Cleanup First
76+
77+
Per the repo CLAUDE.md, dead-code removal lands as its own commit before PR 1. Specifically: removal of 4 unused TUI exports. This is a pre-requisite commit, not a numbered PR.
78+
79+
---
80+
81+
## Acceptance Criteria
82+
83+
### PR 1: Filter & Navigation
84+
85+
- [ ] Filter by `tool_name` — only matching rows shown.
86+
- [ ] Filter by `agent_id` — only matching rows shown.
87+
- [ ] Filter by `execution_outcome` — only matching rows shown.
88+
- [ ] Combined AND across all three fields — intersection applied correctly.
89+
- [ ] Empty-result state — placeholder renders when filter produces zero rows.
90+
- [ ] `Esc` clears filter (when filter bar is focused) and restores full tree.
91+
92+
### PR 2: Annotations + Live Feed
93+
94+
- [ ] `g` writes good verdict — tree badge appears immediately.
95+
- [ ] `b` writes bad verdict — tree badge appears immediately.
96+
- [ ] `n` writes note — extend existing modal test.
97+
- [ ] Poll tick appends new rows to tail pane without full re-render.
98+
- [ ] Poll is paused during modal open (regression-critical — verify no state mutation during modal render).
99+
- [ ] Annotation write failure is non-fatal — surfaces as tail pane status line, TUI remains interactive.
100+
101+
### PR 3: 3-Panel Layout
102+
103+
- [ ] Layout snapshot: column widths match spec (30 / 45 / 25 %).
104+
- [ ] Overlay open/close restores prior focus to the panel that was active before the overlay.
105+
- [ ] `Tab` cycles focus Tree → Detail → Tail → Tree.
106+
- [ ] Terminal resize: tested manually and documented in PR notes (automated snapshot not required).
107+
108+
---
109+
110+
## Resolved Design Questions
111+
112+
**[P2] Tail pane scope** — RESOLVED: follows current tree selection, not global. Switching selection refreshes the tail pane from indexed rows for that session/turn. A global feed was considered but discarded as too noisy during deep inspection.
113+
114+
**[S2] Verdict annotation export** — RESOLVED: verdicts written via `g`/`b` use the same JSONL schema as `flight log annotate` and flow through `flight log export` unchanged. PR 2 must verify end-to-end: `flight log export <session> | grep verdict` surfaces the new row.
115+
116+
---
117+
118+
## Reused Infrastructure
119+
120+
| Component | Source | Role in this work |
121+
|---|---|---|
122+
| `FlightDB` | `src/query.ts` | SQLite-indexed `tool_name`, `agent_id`, `execution_outcome` columns. PR 1 reuses existing indexes — no schema migration required. |
123+
| `runAnnotate` | annotations module | Verdict write path for `g`/`b` keybinds. Unchanged. |
124+
| `createAnnotateModal` | `src/tui/annotate.ts` | Existing note modal. Reused by PR 2 for the `n` keybind. |
125+
| `TuiApp.refresh()` | `src/tui/` | Existing refresh method. PR 2 wraps it in `setInterval(..., 1000)`. |
126+
127+
---
128+
129+
## Dogfood Window
130+
131+
CEO requirement: 4 weeks of internal dogfood before any marketing. Note this in each PR description.
132+
133+
---
134+
135+
## Out of Scope
136+
137+
- Keyboard-chord bindings (dotted-path field filters).
138+
- Remote collaboration or session sharing.
139+
- Persistent layout preferences (panel width saved to disk).

packages/claude-code/test/init.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { describe, it, expect } from "vitest";
22
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
33
import { mkdtempSync, readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
44
import { tmpdir } from "node:os";
5-
import { join, resolve } from "node:path";
5+
import { join, resolve, dirname } from "node:path";
6+
import { fileURLToPath } from "node:url";
67

7-
const BIN = resolve(import.meta.dirname, "../bin/init.js");
8+
const BIN = resolve(dirname(fileURLToPath(import.meta.url)), "../bin/init.js");
89

910
function run(args: string[], env?: Record<string, string>): SpawnSyncReturns<string> {
1011
return spawnSync(process.execPath, [BIN, ...args], {

packages/flight-proxy/src/tui/detail.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function formatAnnotations(annotations: Annotation[]): string[] {
110110
});
111111
}
112112

113-
export function resolveSessionIdForItem(item: TuiTreeItem | null): string | null {
113+
function resolveSessionIdForItem(item: TuiTreeItem | null): string | null {
114114
if (!item) {
115115
return null;
116116
}
@@ -123,7 +123,7 @@ export function resolveSessionIdForItem(item: TuiTreeItem | null): string | null
123123
return latestSession?.session.session_id ?? null;
124124
}
125125

126-
export function loadSessionEntries(db: FlightDB, sessionId: string, limit: number = ENTRY_LIMIT): LogEntry[] {
126+
function loadSessionEntries(db: FlightDB, sessionId: string, limit: number = ENTRY_LIMIT): LogEntry[] {
127127
const rows = db.query({ sessionId, limit });
128128
const entries = rows.map((row) => rowToIndexedEntry(row));
129129

0 commit comments

Comments
 (0)