feat: mark unread output and bell "your turn" in the boo ui#86
Merged
Conversation
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.
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.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 uitwo signals: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 clientWire / JSON
The
inforeply gainsbell_idle_ms: the age in ms of the last bell rung while detached (-1for none), computed against the samenowasout_idleso the UI can compare the two without clock skew.ls --jsongainsbell_idle_ms.unreadis 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
bell_idle_msin theinforeply.belleffect (sets a latch the daemon consumes each service cycle).bell_idle_ms.●your-turn /•unread (selection-safe, exact row width preserved); 250ms refresh.Tests
*, selection highlight re-applied after the marker's SGR reset, exact row width).bell_idle_ms >= 0(andunread) 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, andzig build test-all -Doptimize=ReleaseSafeall pass.nix buildwas 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:
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.tcgetpgrpon the pty + matchclaude/codex) to gate on known agents — rejected as agent-specific.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 (asleep, a slow compile) makes a still-running session look finished.unread+ bell "your turn" (this PR): keepunreadfor "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.