Notifications¶
Roost's notification pipeline has three input paths and four output surfaces. All input paths converge on the same internal events, so the user-visible behavior is identical no matter how the notification was triggered.
Input paths¶
| Source | Triggered by | Best for |
|---|---|---|
roostctl notify |
A process running inside a Roost tab | Claude Code hooks, build scripts, structured pings |
| OSC 9 escape sequence | Any process printing \x1b]9;<message>\x07 |
iTerm2-style apps that already emit OSC 9 |
| OSC 777 escape sequence | Any process printing \x1b]777;notify;<title>;<body>\x07 |
Konsole / KDE-style apps |
roostctl is the preferred path because it carries structured fields (separate title and body, target tab) and bypasses VT parsing. The OSC paths exist as a fallback for tools that can't be modified.
OSC 9 disambiguation: bodies starting with a digit followed by ; (or only digits) are treated as ConEmu extensions (sleep / progress / message-box / etc.) and dropped — they are not iTerm2-style notifications. This is why Claude Code's frequent OSC 9;4 progress pings don't surface as banners. A genuine iTerm2 notification whose text happens to start with a digit (\x1b]9;1 file changed\x07) still passes through unchanged.
Output surfaces¶
A notification has four places it can show up. They are independent — clearing one does not clear the others, except where noted.
- Pending-attention badge on the tab. The built-in libadwaita "needs attention" pulse (a subtle dot / underline). Set when a notification arrives for a non-focused tab; cleared when you select that tab.
- Sticky agent-state indicator on the tab. A small colored circle next to the title — blue (running), orange (needs your input), gray (idle / turn complete), or none. State only changes from agent hook events (
roostctl claude-hook ...or future equivalents); it survives focus events. - Project rollup stripe on the sidebar row. A 3px left-edge color stripe colored by the highest-severity state across the project's tabs. needs-input wins because the most actionable signal should dominate — a project with one blocked tab and four running tabs flags the user, not "busy."
- Desktop notification banner.
- macOS: shells out to
terminal-notifier(Homebrew). Click the banner →roostctl tab focus --tab Nruns → window raises and the right tab becomes active. Withoutterminal-notifierinstalled, banners are silent no-ops; in-app indicators still work. (Distribution will declare it as a Homebrew dependency.) - Linux:
gio.Notification→ freedesktop notification daemon over DBus, with a default action wired to the in-processapp.tab-focusGIO action. Clicking the banner focuses the tab in-process — no IPC round-trip needed.
If the target tab is the currently focused one and the window is active, the badge and the desktop banner are both suppressed — you're already looking at it. State indicators still update.
Triaging across projects¶
Pending notifications are also a navigable list, so you can jump straight to what needs attention instead of hunting through the sidebar:
- Jump to unread (
Cmd-Shift-U/Ctrl-Shift-U) focuses the next tab with a pending notification — the active project first, then the others. Focusing the tab clears its badge, so repeating the shortcut walks through everything pending. - The notification inbox is the command palette's View Notifications entry: one row per pending tab, labeled
<project> · <tab>with the message body. Activating a row jumps to that tab and clears it. - Clear All Notifications (also in the command palette) empties the inbox and drops every pending badge at once.
Per-tab cooldown¶
Identical (title, body) pairs on the same tab inside one second are dropped silently. Distinct content fires immediately. This protects against scripts that double-fire and against pathological OSC streams that slip past the ConEmu filter.
Tab targeting¶
roostctl resolves the target tab in this order:
- The
--tab <id>flag, if provided - The
ROOST_TAB_IDenvironment variable, set by Roost when it spawns each tab's shell - Error: tab id required
Roost injects these environment variables into every spawned shell:
| Variable | Value |
|---|---|
ROOST_TAB_ID |
The integer tab id this shell is bound to |
ROOST_SOCKET |
The Unix-socket path the GUI is listening on |
So roostctl invoked from inside any tab needs no flags or config — it knows where to send and which tab to mark.
Hook-session OSC suppression¶
When a structured hook session is driving a tab (e.g. Claude Code with roostctl claude install wired up), raw OSC 9 / 777 from inside the agent is dropped. Hooks are the trusted channel; OSC is the fallback for tools that can't be modified, and hook-driven agents emit OSC noise we don't want to surface twice. The suppression is automatic — session-start engages it, session-end releases it.
Examples¶
From inside a Roost tab:
From outside Roost, target a specific tab:
ROOST_SOCKET="$HOME/Library/Caches/roost/roost.sock" \
roostctl notify --title "From CI" --body "deploy ready" --tab 3
Manually drive the agent-state indicator (e.g. for a non-Claude agent):
OSC 9 from any shell inside a Roost tab:
OSC 777 with a separate title and body:
What you do not have to do¶
- You do not have to configure the socket path manually (
ROOST_SOCKETis auto-set). - You do not have to track tab ids manually (
ROOST_TAB_IDis auto-set). - You do not have to add anything to your shell config — the env vars come from the parent process.
Limits and caveats¶
- Body length is capped at 8 KB on the OSC parser to bound buffer growth on a misbehaving sender. Longer bodies are truncated.
- A second desktop notification on the same tab supersedes the first via
terminal-notifier -groupon macOS and the GApplication notification id on Linux. Supersede on macOS Sonoma+ is best-effort for unsigned senders. - On Wayland,
gtk.Window.Present()without an XDG-activation token may only flash the taskbar instead of raising. Click-from-banner paths typically pass a token through; CLI scripts that callroostctl tab focusdirectly may not. - macOS banners are currently branded as "terminal-notifier" rather than "Roost." Roost-branded banners need a code-signed
.appbundle (Layer 3, separate work).
See the Claude Code Hooks guide for the most common Claude Code wire-up.