feat: add boo ui, a full-screen session manager#12
Merged
Conversation
Sessions are listed in a left sidebar; the focused session renders in a viewport on the right. Sessions can be focused (click or C-a n/p), created (C-a c or the + button), and killed (C-a k or the per-row x) without leaving the UI. Session output is never passed through raw: the UI feeds the focused session into a client-side libghostty terminal sized to the viewport and repaints changed rows at a column offset, so absolute cursor addressing, scrolling, and clears cannot trample the sidebar. The local terminal also answers terminal queries and drives mouse, focus, and bracketed-paste forwarding based on the modes the application actually enabled, with mouse coordinates translated into viewport space. Running boo ui inside a boo session never auto-attaches its host session (the BOO env var), which would feed the UI's own output back into itself.
- 'boo rename <name> <new-name>' and a daemon 'rename' control verb: the listening socket is renamed in place, so the running program and any attached client are unaffected. - boo ui: C-a r opens a rename prompt in the status bar, pre-filled with the current name. - boo ui: each sidebar entry now shows the window title dim under the session name. - boo ui: the last row is a full-width status bar. It hints 'Press Ctrl+A for keybinds' and lists every binding while the prefix is armed; prompts and messages render there too. - boo ui: the selection highlight is the only selection marker; the '>' glyph is gone and '*' only marks sessions attached elsewhere.
…e prefix - Replace the per-row idle timer with a green activity dot that shows while a session produced output within the last 2 seconds (the same settle window 'wait --idle' uses). Rows stop re-rendering every second as a side effect. - Drop the 'boo N sessions' header; '+ new session' moves to the top row, freeing a sidebar row for the list. - Esc backs out of an armed C-a prefix, and the keybind bar says so.
…vity dot Dragging in the viewport now selects text when the focused application has not requested mouse reporting: the selection is highlighted in reverse video and copied on release via OSC 52, so it works over SSH and through nested multiplexers. While the C-a prefix is armed, an escape sequence (mouse click, arrow key) cancels the prefix and is reparsed instead of leaking its tail bytes into the focused session's pty. The sidebar activity dot is gone; the title row already shows what a session is doing. The status hint is now 'Keybinds: Ctrl+A' and the keybind bar no longer repeats the prefix.
A session that enabled button tracking and SGR before the UI attached must get clicks forwarded with viewport-relative coordinates rather than starting a UI selection: the attach replay carries the mouse reporting modes into the view terminal.
The no-sessions view now shows the boo wordmark and ghost with a keybind hint, and a gap row separates the new-session button from the list. composeViewportCell erased the row after drawing it, which ate the last cell of any row touching the terminal's right edge: the cursor rests on that cell in the pending-wrap state and EL erases from the cursor inclusive. Erase first, then draw. The integration harness gains a renderScreen helper that replays captured output through ghostty-vt, so tests can assert on the rendered screen instead of raw byte streams.
An idle UI auto-attached any session its refresh discovered, stealing sessions held by other clients: a second boo ui (or a plain attach) would silently take over whatever appeared, and the loser sat on a dead 'attached elsewhere' view forever. Attachment intent is now explicit. A deliberate click or keypress still steals, but automatic focus (startup, discovery, recovery) only binds sessions no other client holds. A focused session whose attachment broke is reclaimed once it frees up: stolen views recover when the thief lets go, lost sockets when the daemon answers again. A live view also outlives a transient listing failure instead of being torn down and re-attached. The viewport empty state distinguishes a session held elsewhere (click to take it over) from no focus at all.
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.
boo uiopens a full-screen interface: every session in a left sidebar, the focused session rendered live in a viewport on the right.Each sidebar entry is a session: its name, a kill target, and the window title dim underneath. The focused session is marked by the highlight alone;
*marks sessions attached by another client.+ new sessionsits on the top row. With no sessions at all, the viewport shows the boo wordmark and its ghost with a keybind hint.What you can do
attach);C-a n/C-a p/C-a C-aswitch from the keyboard.C-a cor by clicking+ new session.C-a kor the per-rowx, with a y/n confirmation.C-a r: the status bar becomes a prompt pre-filled with the current name. Also available asboo rename <name> <new-name>; the daemon renames its listening socket in place, so the running program and any attached client are unaffected.Keybinds: Ctrl+A; while the prefix is armed it lists every binding, andEsc(or a mouse click) backs out.C-a dquits; sessions keep running.How it works
Unlike
boo attach, session output is never passed through to the terminal raw: absolute cursor addressing, scrolling regions, and clears from the session would trample the sidebar. Instead the UI is a client-side compositor:ScreenFormatter(single-row rectangle selections) at a column offset. Rows are erased before drawing: erasing after would eat the last cell of a row touching the terminal's right edge, where the cursor rests in the pending-wrap state and EL erases from the cursor inclusive. Frames are row-diffed, coalesced to ~60fps, and wrapped in synchronized-update markers.encodeMouse), focus, and paste markers are forwarded to the app.OSC 52.C-aprefix is handled client-side;C-a a/C-a lare relayed through the daemon's own prefix parser.Design notes and edge cases
boo uiinside a boo session never auto-attaches its host session (detected via theBOOenv var); attaching it would feed the UI's own output back into itself forever. Clicking it shows a message instead.infofailure during a refresh no longer tears down and re-attaches a healthy view; the sidebar selection just returns when the listing recovers.Esccancels the prefix;Escfollowed by more bytes is the start of an escape sequence, so it cancels the prefix and is reparsed through the input state machine. Mouse clicks while the keybind list is open therefore act normally instead of leaking sequence tails into the session's pty.rename(2): established connections survive, new clients resolve the new name. TheBOOenv var inside the session keeps the spawn-time name (a child's environment cannot be rewritten), so renaming a session that hosts a nested UI from outside can stale that UI's guard; the UI itself can never rename its host because the host is never selectable.boo new -dso every inherited CLOEXEC descriptor is dropped; a forked daemon would otherwise pin the UI's sockets open, and naming/fallback behavior stays identical to the CLI.poll()so an EOF-readable fd cannot spin the loop. A generation counter guards against reading a freshly attached socket with a stale poll result.0x01bytes reach the application: bracketed paste suspends prefix scanning (a quirk plain attach inherits from GNU screen).peek --scrollbackstill works for history.Testing
xtarget, rename via the CLI and via theC-a rprompt, the keybind bar + esc cancel, live title updates, quit + terminal restore, viewport sizing + SIGWINCH propagation, steal/steal-back, startup leaving a held session alone until it frees, automatic reclaim after a thief detaches, no-tty error.zig fmt --check,zig build test,test-integration(repeated runs, no flakes), andtest-all -Doptimize=ReleaseSafeall pass locally; manually exercised vim/alt-screen apps, nestedboo ui, rename/title flows, drag-copy, clicks while the keybind list is open, and the no-steal/reclaim flows against a second attached client.This PR was generated by Coder Agents on behalf of @kylecarbs.