Skip to content

feat: add boo ui, a full-screen session manager#12

Merged
kylecarbs merged 9 commits into
mainfrom
ui-session-manager
Jun 10, 2026
Merged

feat: add boo ui, a full-screen session manager#12
kylecarbs merged 9 commits into
mainfrom
ui-session-manager

Conversation

@kylecarbs

@kylecarbs kylecarbs commented Jun 10, 2026

Copy link
Copy Markdown
Member

boo ui opens a full-screen interface: every session in a left sidebar, the focused session rendered live in a viewport on the right.

 + new session          │bash-5.1$ echo BETA_CONTENT_77
                        │BETA_CONTENT_77
 alpha                x │bash-5.1$
  vim notes             │
 beta                 x │
  ~/code/api            │
 Keybinds: Ctrl+A

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 session sits 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

  • Click a session to focus it (steals politely, like attach); C-a n/C-a p/C-a C-a switch from the keyboard.
  • Automatic focus never steals: on startup and as sessions appear, the UI only binds sessions no other client holds. A session held elsewhere shows a take-it-over hint instead, and a focused session that was stolen or whose socket dropped is reclaimed automatically once it frees up.
  • Create sessions with C-a c or by clicking + new session.
  • Kill sessions with C-a k or the per-row x, with a y/n confirmation.
  • Rename sessions with C-a r: the status bar becomes a prompt pre-filled with the current name. Also available as boo rename <name> <new-name>; the daemon renames its listening socket in place, so the running program and any attached client are unaffected.
  • Select and copy text by dragging in the viewport when the app has not requested mouse reporting: the selection highlights as you drag and is copied on release via OSC 52, so the copy works over SSH and through nested multiplexers.
  • Discover keybinds: the bottom bar reads Keybinds: Ctrl+A; while the prefix is armed it lists every binding, and Esc (or a mouse click) backs out.
  • Everything else is typed into the focused session, including full-screen apps (vim, htop), with mouse, focus, and bracketed-paste forwarding when the app asks for them.
  • C-a d quits; 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:

  • The focused session attaches over the normal socket protocol, but its output feeds a local ghostty-vt terminal sized to the viewport (mirroring how the daemon itself tracks state).
  • Changed viewport rows are re-rendered through libghostty's 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.
  • The last screen row is a full-width status bar owned by the UI: hint, keybind list, transient messages, and the kill/rename prompts all render there; the viewport sits above it.
  • The local terminal answers terminal queries (DSR, DA, XTVERSION, ...) by sending replies back as input, and its mode state decides whether mouse (coordinates translated into viewport space via encodeMouse), focus, and paste markers are forwarded to the app.
  • When the app has not asked for mouse reporting, a left drag selects viewport text instead: the span is repainted in reverse video over the rendered row, and release copies the formatter's plain-text extraction as OSC 52.
  • The C-a prefix is handled client-side; C-a a/C-a l are relayed through the daemon's own prefix parser.
Design notes and edge cases
  • Nested guard: boo ui inside a boo session never auto-attaches its host session (detected via the BOO env var); attaching it would feed the UI's own output back into itself forever. Clicking it shows a message instead.
  • Attachment intent is explicit: only a deliberate click or keypress steals. Automatic focus (startup, discovery, recovery) picks the most recently active session that no other client holds, so two UIs over the same directory can no longer silently steal sessions from each other, leaving the loser on a dead "attached elsewhere" view.
  • A live view outlives the listing: the view's own socket decides when an attachment is over. A transient info failure during a refresh no longer tears down and re-attaches a healthy view; the sidebar selection just returns when the listing recovers.
  • Armed-prefix input: a lone Esc cancels the prefix; Esc followed 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 moves the daemon's Unix socket with rename(2): established connections survive, new clients resolve the new name. The BOO env 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.
  • Creating sessions re-execs boo new -d so 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.
  • Stolen/ended/lost sessions render an empty-state message in the viewport; a dead view's socket is dropped from 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.
  • Selections live in viewport cell coordinates, so a resize or a view switch clears them rather than highlighting the wrong cells.
  • Pasted 0x01 bytes reach the application: bracketed paste suspends prefix scanning (a quirk plain attach inherits from GNU screen).
  • Not covered (v1): cursor style (DECSCUSR) mirroring, kitty CSI-u key re-encoding toward apps, viewport scrollback. peek --scrollback still works for history.

Testing

  • Unit tests: input parser (prefix/esc-cancel/mouse-click-while-armed/mouse/paste/focus, split feeds), layout + hit testing (two-row entries, top button, gap row, full-width bar), sidebar name/title rows, automatic-focus policy (skips held and host sessions, prefers recent), single-row VT rendering.
  • 16 PTY integration tests: sidebar render + focus-follows-recency, drag-select + OSC 52 copy, native mouse forwarding through the attach replay, right-edge rows keep their last cell, the ghost empty state, keyboard switching + typing, click-to-switch, create via the top button, kill via keys and via the x target, rename via the CLI and via the C-a r prompt, 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), and test-all -Doptimize=ReleaseSafe all pass locally; manually exercised vim/alt-screen apps, nested boo 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.

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.
@kylecarbs kylecarbs merged commit b59b991 into main Jun 10, 2026
4 checks passed
@kylecarbs kylecarbs deleted the ui-session-manager branch June 10, 2026 21:39
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