|
| 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). |
0 commit comments