From f90dcff2992ee163728fa8d0cda59aa9f5e5b8b2 Mon Sep 17 00:00:00 2001 From: Sam & Claude Date: Fri, 26 Jun 2026 22:03:12 +0200 Subject: [PATCH] docs: fold glasspane TUI design into wiki/tui.md, delete scratch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GLASSPANE-TUI-DESIGN.md was a self-declared "scratch space" working doc — but everything in it had shipped (the attention bar, n/N jump keys, the `a` filter, the All-sessions fix). Its enduring decisions lived only in this stale plan, while the wiki carried just a keybindings table and a TODO roadmap stub. Fold the durable design decisions into wiki/tui.md (the natural home — it already had the keybindings section): - complete the keybindings table (was missing n/N + a) - "The attention model" section: needs_attention() definition, the 4h stall threshold rationale, attention-bar layout spec, row-highlight color spec, and the session-filter-AND composition contract Repoint the one code reference (the all_sessions regression comment in main.rs) from GLASSPANE-TUI-DESIGN.md to the wiki section it now lives in. Delete the 208-line scratch doc — zero remaining references. wiki-lint --strict: 147 pass. TUI crate: fmt/clippy/20 tests green. (Sam & Claude) --- crates/colibri-glasspane-tui/src/main.rs | 10 +- docs/GLASSPANE-TUI-DESIGN.md | 208 ----------------------- docs/wiki/tui.md | 70 +++++++- 3 files changed, 74 insertions(+), 214 deletions(-) delete mode 100644 docs/GLASSPANE-TUI-DESIGN.md diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index 12cffd3..b7840f9 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -887,11 +887,11 @@ mod tests { #[test] fn all_sessions_view_is_reachable_with_sessions_present() { - // Regression for the pre-existing bug documented in - // GLASSPANE-TUI-DESIGN.md: once any pane had a session_id, - // rebuild_session_list() forced session_filter = Some(first), making - // the aggregated "All sessions" view unreachable. It must now default - // to All and stay selectable via Tab. + // Regression for the pre-existing bug documented in docs/wiki/tui.md + // ("Attention filter composes with session filter (AND)"): once any + // pane had a session_id, rebuild_session_list() forced session_filter + // = Some(first), making the aggregated "All sessions" view unreachable. + // It must now default to All and stay selectable via Tab. let snap = GlasspaneSnapshot::new( "osa", "2026-06-25T12:00:00Z", diff --git a/docs/GLASSPANE-TUI-DESIGN.md b/docs/GLASSPANE-TUI-DESIGN.md deleted file mode 100644 index 2a3819b..0000000 --- a/docs/GLASSPANE-TUI-DESIGN.md +++ /dev/null @@ -1,208 +0,0 @@ -# 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 diff --git a/docs/wiki/tui.md b/docs/wiki/tui.md index 485b13b..6073b4d 100644 --- a/docs/wiki/tui.md +++ b/docs/wiki/tui.md @@ -81,8 +81,76 @@ should be revisited. | `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 | +| `Tab` / `Shift+Tab` | Cycle through distinct sessions (incl. "All") | | `j` / `k` or `↓` / `↑` | Navigate the pane table | +| `n` / `N` | Jump to next / previous **attention** pane | +| `a` | Toggle the attention filter (only attention) | + +## The attention model + +The dashboard's primary question is _"does any pane need me right now?"_ +Attention is the impossible-to-miss signal for that. + +→ `crates/colibri-glasspane-tui/src/main.rs` (`needs_attention`) + +### 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 glasspane state +machine explicitly marks Blocked = "operator attention needed" (it is the state +for `queue_update` / pending steering / approval). This single free function is +shared by the attention bar, the jump keys (`n`/`N`), and the filter (`a`) — +one definition to change. + +### 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 mostly shows 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. + +``` +╔═ ⚠ ATTENTION ══════════════════════════════════════════╗ +║ 3 panes need attention (1 error · 1 blocked · 1 stalled) ║ +║ scraper-19: Error · worker-7: Blocked · db-3: Stalled ║ +╚═════════════════════════════════════════════════════════╝ +``` + +- The counter line derives from `snapshot.count()` + `stalled_count()` — no + extra API calls. +- The pane-list line shows pane IDs + state label, truncated to terminal width; + if more fit than room allows, it 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 | (plain) | `bg(DarkGray)` | + +Attention rows are impossible to miss; the inversion on selection confirms +which one the cursor is on without losing the attention signal. + +### Attention filter composes with session filter (AND) + +`attention_only` is a separate field from `session_filter`. In `filtered_panes()` +the two 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. ## When to use the TUI vs the CLI