Skip to content

feat: mark unread output and bell "your turn" in the boo ui#86

Merged
kylecarbs merged 5 commits into
mainfrom
feat-unread-idle-markers
Jun 23, 2026
Merged

feat: mark unread output and bell "your turn" in the boo ui#86
kylecarbs merged 5 commits into
mainfrom
feat-unread-idle-markers

Conversation

@kylecarbs

@kylecarbs kylecarbs commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

When you run several coding agents (Claude Code, Codex, …) you can't tell which session needs you without looking at each one. This teaches boo ui two signals:

  • unread — output produced while no client is attached. Flags the session in the sidebar; attaching (viewing it) clears it.
  • your turn — a terminal bell (BEL) that rings while no client is attached. A bell is an explicit "I need you" from the program (agents ring it when a turn finishes), a deliberate signal rather than an inference from silence.

The daemon already feeds every window's output through ghostty-vt, so both reuse state it already has. The parser distinguishes a real BEL from the BEL that terminates an OSC string, so setting a window title never counts as a bell.

Sidebar markers (one cell, left of the name), in priority order:

  • bold blue — your turn (a bell rang while you were away)
  • dim — unread output you haven't viewed
  • * — attached by another client
  • space — nothing to flag

Wire / JSON

The info reply gains bell_idle_ms: the age in ms of the last bell rung while detached (-1 for none), computed against the same now as out_idle so the UI can compare the two without clock skew. ls --json gains bell_idle_ms. unread is unchanged. Parsing stays backward compatible: leading flag fields peel off the front and the tab-free title is the tail, so a daemon predating the field parses with no bell.

The sidebar also now re-polls every 250ms (was 1s) so background rows track title/unread/bell changes promptly even when the focused session is idle. Only the focused session has a live socket; the rest are polled.

Changes

  • daemon: record the last bell rung while detached, clear it on attach, report bell_idle_ms in the info reply.
  • window: wire the ghostty-vt bell effect (sets a latch the daemon consumes each service cycle).
  • ls --json: add bell_idle_ms.
  • ui: render your-turn / unread (selection-safe, exact row width preserved); 250ms refresh.
  • help: document the markers and the JSON field.

Tests

  • Unit: marker rendering (your-turn vs unread, priority over *, selection highlight re-applied after the marker's SGR reset, exact row width).
  • PTY integration: a bell while detached flips bell_idle_ms >= 0 (and unread) and clears on attach; the sidebar shows for a belled session and for plain unread.

Run locally: zig fmt --check, build, zig build test, zig build test-integration, and zig build test-all -Doptimize=ReleaseSafe all pass. nix build was not run in my environment (no nix available), but there are no build-graph or dependency changes.

Decision log

This grew out of #84 (thanks @nkthiebaut for the original idea and groundwork). It went through several framings:

  1. Bell → waiting (the feat: mark sessions waiting for input in the boo ui #84 approach): mark a session waiting when it rings the BEL, until input.
  2. Generic signal store — rejected as too broad / YAGNI.
  3. Foreground-process detection (tcgetpgrp on the pty + match claude/codex) to gate on known agents — rejected as agent-specific.
  4. unread + output-idle "your turn": "output since last view" is general and honest, and output settling (quiet ≥ 2s) promoted it to "your turn." Shipped first, but output-settling is a fragile proxy: a pause in a script (a sleep, a slow compile) makes a still-running session look finished.
  5. unread + bell "your turn" (this PR): keep unread for "activity since you looked," and use an explicit bell for "your turn." A bell is intentional, so a paused script never falsely looks done, and a title-setting OSC (BEL-terminated) is correctly ignored. The bell time is stored and exposed as an age (bell_idle_ms) so the UI can combine it with output idle time rather than gate on a fragile "bell was the last byte" comparison (a trailing newline after the bell would break that).

Semantics:

  • unread = output arrived while no client was attached; cleared on attach.
  • bell_idle_ms = ms since the last bell rung while detached (-1 = none); cleared on attach.

Marker styling also iterated: the original filled for unread was visually heavy and crowded the name, so unread became a dim and the bold-blue is now reserved for "your turn."


🤖 Generated by Coder Agent on behalf of @kylecarbs.

The session daemon already feeds every window's output through
ghostty-vt. It now tracks a per-session `unread` flag: output produced
while no client is attached marks the session unread, and attaching
(viewing it) clears it. This is a terminal-multiplexer-native "activity
since you last looked" signal that doubles as a coding-agent "your turn"
cue without any agent-specific detection or bell heuristics.

- daemon: set `unread` on detached output, clear on attach, report it in
  the `info` reply. Backward compatible: a tab separates the flag from
  the tab-free title, and an older daemon's shorter reply parses with
  unread=false.
- ls --json: add `"unread"`. `idle_ms` is already present, so idleness
  is left for consumers to threshold rather than duplicated.
- ui: the sidebar marks unread sessions with a `●`, bold yellow once the
  session's output has settled (idle: waiting on you) and dim while
  still producing output. Unread takes priority over the `*`
  attached-elsewhere marker.
- help: document the markers and the new JSON field.

Generated by Coder Agent on behalf of @kylecarbs.
The filled circle (U+25CF) was visually heavy and crowded the session
name. Switch to a lighter bullet (U+2022) and color the settled
"your turn" marker blue (bold) instead of yellow. The in-progress
"working" state stays dim.
Background session rows (title, unread, idle) are refreshed by
polling; only the focused session has a live socket. At a 1s cadence
those rows looked frozen between ticks when the focused session was
idle. Drop the interval to 250ms so background rows track changes
without waiting on the focused session's own title churn to force a
re-poll.
Replace the output-settle heuristic for the "your turn" marker with an
explicit signal: a terminal bell (BEL) that rings while no client is
attached. Output that settles after a pause (a sleep in a script, a
slow compile) no longer looks finished; only an intentional bell does.

The window parser already distinguishes a real BEL from the BEL that
terminates an OSC string, so setting a title never trips it. The daemon
records the time of the last bell rung while detached and clears it on
attach. The info reply exposes it as an age (bell_idle_ms, -1 for none)
computed against the same now as out_idle, so the ui can combine the
two. ls --json gains bell_idle_ms.

The sidebar now shows a bold-blue ● (your turn) when a bell rang while
you were away, a dim • when there is unread output but no bell, '*'
when attached elsewhere, and a space otherwise.
@kylecarbs kylecarbs changed the title feat: mark sessions with unread output in the boo ui feat: mark unread output and bell "your turn" in the boo ui Jun 23, 2026
Reverse video inverted the selected row to a harsh bright block that
washed out the dim title row beneath the name. Give the selected row a
dark gray background (256-color 238) with normal-intensity text instead,
so the subtitle stays readable. Reverse video is kept for the in-progress
mouse text selection in the viewport, where it is the conventional look.
@kylecarbs kylecarbs merged commit ffed97e into main Jun 23, 2026
5 checks passed
@kylecarbs kylecarbs mentioned this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant