docs: concrete attention system design for colibri-tui #189

Merged
clawdie merged 1 commit from docs/tui-attention-plan into main 2026-06-25 18:44:31 +02:00

View file

@ -58,25 +58,131 @@ Legend: ⌘ Cmd · ⌥ Option · ⌃ Control · ⇧ Shift · ↩ Enter.
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 | Roadmap item |
| --- | --- | --- |
| `n` / `N` | Jump to next / previous **attention** pane (Error/Stalled/waiting) | jump-to-next-attention |
| `a` | Toggle the attention filter (show only panes needing the operator) | attention signal |
| `i` | Send input / answer the selected pane (when it is blocked on input) | answer-from-dashboard |
| `t` | Toggle Telegram/desktop notification mute for the session | outbound notifications |
| 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.)
## Open design questions
## Remaining open design questions
- **Attention as flag vs. state:** keep the `AgentState` enum small and derive an
attention boolean over it, rather than adding a sixth state. Where does
"waiting for input" come from — inferred from stall + a prompt marker, or an
explicit agent-emitted event?
- **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.
@ -85,6 +191,10 @@ Additive to the baseline; nothing here collides with existing keys.
(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