colibri/docs/GLASSPANE-TUI-DESIGN.md
Sam & Claude b8d499e85c
Some checks are pending
CI / rust (pull_request) Waiting to run
CI / markdown (pull_request) Waiting to run
CI / port (pull_request) Waiting to run
CI / agent-jail-pkgs (pull_request) Waiting to run
docs: rename PLAN/PROPOSAL/HANDOFF/ENHANCEMENT → implementation names
7 renames (no plan/proposal/handoff/enhancement in filenames):

    CLAWDIE-INSTALLER-HANDOFF.md → CLAWDIE-INSTALLER-VALIDATION.md
    CLAWDIE-STUDIO-PROPOSAL.md   → CLAWDIE-STUDIO.md
    COLIBRI-SKILLS-PLAN.md       → COLIBRI-SKILLS.md
    FREEBSD-BUILD-LANE-HANDOFF.md→ FREEBSD-BUILD-LANE.md
    GLASSPANE-TUI-ENHANCEMENTS.md→ GLASSPANE-TUI-DESIGN.md
    MULTI-AGENT-HOST-PLAN.md     → MULTI-AGENT-HOST.md
    PLAN-WIKI-CLAWDIE-SI.md      → WIKI-CLAWDIE-SI.md

  16 cross-references updated across 10 files.
  wiki-lint --strict: PASS (146 refs, 0 failures).
2026-06-26 17:32:39 +02:00

9.6 KiB

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 (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"?

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 — next-attention, attention-filter, input, toggle-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