# Glasspane / colibri-tui — operator usability enhancements (working doc) Working/design notes for the operator-facing supervision surface. The published summary lives in [`wiki/glasspane.md`](./wiki/glasspane.md) (_Usability roadmap_); this doc is the scratch space where we collect references and shape the work before it lands. **Premise:** make _"does this agent need me right now?"_ the primary, impossible-to-miss object. We already have the spine — the glasspane state machine (`Idle → Working → Done / Error / Stalled`), the snapshot API, and the `colibri-tui` dashboard. The open work is surfacing and _pushing_ attention, not new machinery. Ideas are drawn from agent-cockpit terminal UIs; all wiring is our own (no external code, no dependency). ## Current colibri-tui keybindings (baseline) | Key | Action | | ---------------------- | ----------------------------------------------- | | `q` / `Esc` | Quit, or close detail pane if open | | `r` | Refresh snapshot now | | `s` | Spawn a local `colibri-test-agent` | | `x` | Stop the selected pane | | `Enter` | Open/close the detail pane for the selected row | | `Tab` / `Shift-Tab` | Cycle through distinct sessions | | `j` / `k` or `↓` / `↑` | Navigate the pane table | → `crates/colibri-glasspane-tui/src/main.rs` ## Reference: agent-cockpit shortcut vocabulary (example, not a spec) Captured as an example of the _interaction vocabulary_ a mature agent cockpit settles on. Most of it is GUI-only (canvas, embedded browser, diff viewer, split panes) and irrelevant to a terminal dashboard — kept here only so we can see the shape. The **transferable** clusters for `colibri-tui` are marked ★. Legend: ⌘ Cmd · ⌥ Option · ⌃ Control · ⇧ Shift · ↩ Enter. **Notifications ★ — the "attention" model we care about** - ⌘I — show notifications - ⌘⇧U — jump to latest unread - ⌃⌘U — mark oldest-unread and jump to next - ⌥⌘U — toggle current item unread - ⌘⇧H — flash focused panel **Navigation ★** - `J`/`K` (+ `⌃N`/`P`, `H`/`L`) — vim-style row/pane movement (we already do `j`/`k`) - ⌘1…9 / ⌃1…9 — jump directly to workspace / surface N **Workspaces / surfaces (GUI tab model — mostly N/A to a TUI)** - ⌘N new workspace · ⌘T new surface · ⌘W close · ⌘⇧R rename · ⌘⇧O reopen previous session - ⌃⌘] / ⌃⌘[ next/prev workspace · ⌘⇧] / ⌘⇧[ next/prev surface **Input / focus ★ (relevant once we add "answer a blocked agent")** - ⌘⇧A — switch focus between terminal and a text-input box - ⌥⌘⇧A — attach file to the input box **GUI-only (canvas, browser, diff, find, splits)** — out of scope for the TUI; listed for completeness only: ⌘D/⌘⇧D splits, ⌃⌘C canvas, ⌘⇧L browser, ⌃⌘⇧D diff viewer, ⌘F find, ⌥⌘= / ⌥⌘- zoom, etc. ## Resolved decisions (25.jun.2026 brainstorm — Sam & Claude) ### What counts as "attention"? ```rust fn needs_attention(pane: &Pane) -> bool { pane.state == AgentState::Error || pane.state == AgentState::Blocked || pane.stalled } ``` **Error + Blocked + Stalled.** Blocked is included because the doc comments at `colibri-glasspane/src/lib.rs:71` explicitly say Blocked = "operator attention needed" (it's the state for `queue_update` / pending steering / approval). A free function on `&Pane`, used by the attention bar, the filter, and the jump keys — one place to change the definition. ### Stall threshold stays at 4h `DEFAULT_STALL_AFTER = 4 * 60 * 60` (4 hours). Stalled is a rare but critical signal, not a frequent one. The attention bar will mostly show Errors and Blocked panes; Stalled is the "something is deeply wrong" escalation. ### Attention bar replaces the header when active When any pane `needs_attention()`, the header slot is replaced by a red-bordered attention bar (same 3-line vertical footprint). Otherwise the normal header renders. This makes attention impossible to miss without consuming extra space. Proposed bar layout: ``` ╔═ ⚠ ATTENTION ════════════════════════════════════════════╗ ║ 3 panes need attention (1 error · 1 blocked · 1 stalled) ║ ║ scraper-19: Error · worker-7: Blocked · db-3: Stalled ║ ╚══════════════════════════════════════════════════════════╝ ``` - **Counter line** — derives from existing `snapshot.count()` + `stalled_count()`. No new API calls. - **Pane list line** — pane IDs + state label, truncated to terminal width. If more than fit, ends with `· (N more — press a)`. - The bar stays visible even when the attention filter (`a`) is active, so the operator always sees the total count at a glance. ### Row highlight inverts on selection | Row state | Normal | Selected | | --------- | --------------------------- | -------------------------------------- | | Attention | `bg(DarkRed)` + `fg(White)` | `bg(DarkGray)` + `fg(LightRed)` + bold | | Normal | (current — plain) | `bg(DarkGray)` (current) | Attention rows are impossible to miss. The inversion on selection confirms which one the cursor is on without losing the attention signal. ### Filter composes with session filter (AND) `attention_only: bool` is a separate field from `session_filter`. In `filtered_panes()`, the filters chain: session filter AND attention filter. Tab cycles sessions within the attention-only view. Turning off the attention filter returns to the session-scoped view. ## Implementation plan — build order All work in a single file: `crates/colibri-glasspane-tui/src/main.rs` (727 lines currently). Estimated diff: ~100-120 lines added, ~30 modified. ### Tier 1: Attention bar + `needs_attention()` (~40 lines) - Add free function `needs_attention(&Pane) -> bool` - Add `render_attention_bar()` method - Modify `render()`: conditional — attention bar replaces header when any pane needs attention - Unit tests: `needs_attention_error`, `needs_attention_blocked`, `needs_attention_stalled`, `needs_attention_working_false`, `needs_attention_done_false` ### Tier 2: Jump keys — `n` / `N` (~20 lines) - `n` — move selection to next attention pane (wrapping) - `N` — move selection to previous attention pane (wrapping) - Uses `needs_attention()` over `filtered_panes()` (respects session scope) - If no attention panes: toast `"no panes need attention"` - Detail pane follows the jump (existing sync logic) - Footer updated: add `n/N attn` ### Tier 3: Attention filter — `a` key (~15 lines) - Add `attention_only: bool` field to `App` - Modify `filtered_panes()`: chain `needs_attention()` when `attention_only` - `a` toggles the field; toast on toggle: `"attention filter on (N panes)"` / `"attention filter off"` - Footer updated: add `a attn` ### Tier 4: Row highlight (~20 lines) - Modify `render_table()`: per-row style based on `needs_attention()` and selection state - Attention + not selected → `Style::new().bg(DarkRed).fg(White)` - Attention + selected → `Style::new().bg(DarkGray).fg(LightRed).add_modifier(BOLD)` - Non-attention rows unchanged ### Tier 5: Answer-from-dashboard — separate PR Not part of this work. Needs: - TextArea widget (from `tui-textarea` crate or `ratatui::widgets::Textarea`) - New socket command `send-input { pane_id, text }` - Daemon routes to agent subprocess stdin - Design pass: which states accept input? (Blocked at minimum) ## Proposed colibri-tui keymap (draft — to be refined as we build) Additive to the baseline; nothing here collides with existing keys. | Key | Proposed action | Tier | Status | | --------- | ------------------------------------------------------------------- | ---- | ------ | | `n` / `N` | Jump to next / previous **attention** pane | 2 | Ready | | `a` | Toggle the attention filter (show only panes needing the operator) | 3 | Ready | | `i` | Send input / answer the selected pane (when it is blocked on input) | 5 | Future | | `t` | Toggle Telegram/desktop notification mute for the session | — | Future | (Mnemonics provisional — `n`ext-attention, `a`ttention-filter, `i`nput, `t`oggle-notify.) ## Remaining open design questions - **Outbound notify transport:** a `colibri notify` subcommand, or a glasspane event type a zot/Pi hook fires? Desktop (XFCE) + Telegram (token already provisioned) are the two sinks. - **Interactive write path:** "send input to pane N" turns the daemon socket from read-only supervision into interactive control — needs its own design pass (auth, which panes accept input, echo/confirmation). - **Persisted pane history:** how much timeline to keep across daemon restarts so "what happened while I was away" survives a reboot, without growing unbounded. - **Pre-existing "All sessions" bug:** once any pane has a `session_id`, `rebuild_session_list()` forces `session_filter = Some(...)`, making the aggregated view unreachable. Out of scope for the attention work, but the attention filter must compose cleanly (AND) with the session filter. ## See also - [`wiki/glasspane.md`](./wiki/glasspane.md) — state machine + published roadmap - [`wiki/tui.md`](./wiki/tui.md) — the dashboard client and its current keybindings