Testing Claude Code integration + tab state + notifications¶
This doc maps the end-to-end UI surface a Claude Code (or any agent) session lights up in Roost, plus the exact CLI commands you can drive from a sibling shell to exercise each path. Use it to verify any UI change in the area, or to demo the integration.
What the UI shows¶
| Surface | What it means | Where it lives |
|---|---|---|
| Pill dot (10pt circle, leading edge of a tab pill) | The tab's agent state. Color encodes which state. | TabPillView.statusSlot (Mac); Linux uses an Adw.TabPage icon. |
| Sidebar stripe (3pt vertical band on the leading edge of a project row) | The rollup of all tabs in the project's agent states. Priority: needs_input > running > idle > none. Tabs with hook_active=true are SKIPPED in the rollup math (Claude owns the urgency signal on those). |
ProjectRowCellView.stripe (Mac); Linux uses a CSS class on the row. |
| Tab pill badge dot (8pt accent circle, trailing edge of inactive notified pills) | The tab has a pending notification. Cleared when the user focuses the tab. | TabPillView.badgeDot (Mac). |
| Project row badge dot (sidebar trailing-edge dot on a project row) | At least one tab in this project has a pending notification. Same focus-clear behavior. | ProjectRowCellView.badgeDot (Mac). |
| Desktop banner | A macOS banner (UNUserNotificationCenter) with title + body. Clicking it brings Roost to front and focuses the originating tab. | DesktopNotifications (Mac); Linux uses NotificationCenter. |
State model¶
| State | Pill dot color | Set via CLI | Triggered by Claude hook |
|---|---|---|---|
none |
no dot | tab set-state --state none --tab N |
session-end |
running |
blue (#5fa3f0) |
tab set-state --state running --tab N |
prompt-submit |
needs_input |
amber (#f0a040) |
tab set-state --state needs_input --tab N |
notification |
idle |
gray (#7a7a7a) |
tab set-state --state idle --tab N |
stop |
CLI cheatsheet¶
Pre-req: roostctl tab list to find a tab id. Either export ROOST_TAB_ID=<id> or pass --tab <id> explicitly to each command. (When a shell is running inside a Roost tab, ROOST_TAB_ID is set automatically.)
| Command | Effect |
|---|---|
roostctl tab list |
Print all tabs grouped by project + their current state. |
roostctl tab set-state --state STATE --tab N |
Set state. STATE ∈ {none, running, needs_input, idle}. |
roostctl notify --title "Hi" --body "..." --tab N |
Fire a desktop banner + set the pill badge. |
roostctl tab clear-notification --tab N |
Clear the pill badge (state unchanged). |
roostctl tab focus --tab N |
Equivalent to clicking the pill; clears the badge as a side effect. |
roostctl screenshot --out /tmp/shot.png |
Render the whole window to a PNG in-process (no OS screen capture) — read it back to see the UI state you just drove. Add --scale 2 for a crisper image. |
ROOST_TAB_ID=N roostctl claude-hook session-start |
Engages hook_active suppression on the tab. (OSC 9/777 from the shell becomes a no-op; only create-notification RPCs emit banners.) |
echo '{"message":"need input"}' \| ROOST_TAB_ID=N roostctl claude-hook notification |
Sets needs_input + fires "Claude Code" banner. |
ROOST_TAB_ID=N roostctl claude-hook stop |
Sets idle + fires "Turn complete" banner. |
ROOST_TAB_ID=N roostctl claude-hook session-end |
Releases hook_active + sets none. |
ROOST_TAB_ID=N roostctl claude-hook prompt-submit |
Sets running + clears pending notification. |
Test checklist¶
T1 — state color progression¶
tab set-state --state idle --tab N→ pill dot gray; sidebar stripe gray (if no higher-priority tab in the project).tab set-state --state running --tab N→ pill dot blue; sidebar stripe blue.tab set-state --state needs_input --tab N→ pill dot amber; sidebar stripe amber.tab set-state --state none --tab N→ pill dot disappears; sidebar stripe reflects the next-highest state in the project (or hides).
T2 — notification banner + per-tab badge¶
Pre-req: focus a different tab in the same project so the test tab is inactive — badges only show on inactive pills.
notify --title "Test" --body "Body" --tab N→ macOS banner top-right (title "Test", body "Body"); pill N grows a small accent badge dot on the trailing edge.- Click the banner. → Roost activates, tab N becomes focused, badge dot vanishes (focus-clears).
- Re-fire
notify, thentab clear-notification --tab N. → Badge clears without focusing. State stays whatever it was.
T3 — hook suppression + sidebar rollup¶
- With 2+ tabs in a project, set Tab A
runningand Tab Bneeds_input. → Sidebar stripe = amber (needs_inputwins). ROOST_TAB_ID=<Tab B id> roostctl claude-hook session-start. → Sidebar stripe drops to blue (Tab B'sneeds_inputis now suppressed in rollup; Tab A'srunningbecomes max).ROOST_TAB_ID=<Tab B id> roostctl claude-hook session-end. → Stripe back to amber. Tab B's state goes tonone.
T4 — project-row badge (separate from per-tab badge)¶
- With Tab A in Project P notified (
tab set-state --state needs_input --tab A,notify --tab A ...), focus a tab in a different project. → Project P's sidebar row shows an accent badge dot AND its stripe is amber. - Click Tab A (or focus from CLI). → Tab A's pill badge + Project P's sidebar row badge both clear. Stripe stays amber (state unchanged).
T5 — end-to-end Claude lifecycle simulation¶
ROOST_TAB_ID=N roostctl claude-hook session-start. → No visible change (Claude hook engages silently). → Internally:hook_active=trueso OSC 9/777 from the shell is now suppressed.ROOST_TAB_ID=N roostctl claude-hook prompt-submit. → Pill dot blue; sidebar stripe blue (no other tabs with higher-priority state). → Any prior pending notification is cleared.echo '{"message":"choose a path"}' | ROOST_TAB_ID=N roostctl claude-hook notification. → Pill dot amber; banner "Claude Code: choose a path"; sidebar stripe NOT updated (hook-active demotes this tab in rollup).- Click the banner → focuses Tab N. Pill badge clears.
ROOST_TAB_ID=N roostctl claude-hook stop. → Pill dot gray; banner "Claude Code: Turn complete".ROOST_TAB_ID=N roostctl claude-hook session-end. → Pill dot disappears; sidebar stripe drops to next-highest-priority tab in the project (or hides).
T6 — UI log inspection¶
There is no shared daemon. Watch the running UI's log while driving the above tests:
# macOS (Swift Roost.app)
tail -f ~/Library/Logs/Roost/roost.log
# Linux (gtk4-rs roost) — also tees to stdout
tail -f "${XDG_STATE_HOME:-$HOME/.local/state}/roost/roost.log"
Each CLI command above lands as a corresponding log entry —
set_tab_state, set_hook_active, tab_notification,
create_notification. If the UI doesn't react to an expected
event, the log line tells you whether the UI received the
IPC request at all.
T7 — visual verification via screenshot¶
Instead of (or alongside) reading the log, capture the live UI as a PNG and inspect it directly. The UI renders its own window in-process, so this works even when the window is unfocused or behind other windows — no OS screen-capture permission needed.
- Drive a visible change, e.g.
tab set-state --state needs_input --tab N. roostctl screenshot --out /tmp/roost.png(add--scale 2for a crisper image; target a specific UI with--target mac/--target gtk).- Open
/tmp/roost.png— confirm the pill dot color, sidebar stripe, and badge match what the state change should produce.
This is the fastest way for an automated agent to see the result of a UI edit rather than infer it from log lines.
Permanent hook setup (Claude Code)¶
To wire the actual Claude Code CLI so it drives these events automatically when you run a session:
This writes ~/.config/roost/claude-settings.json with hook
commands for each lifecycle event, then prints an alias line:
Add that alias to your shell rc. Now every claude session
inside a Roost tab automatically drives the integration:
- Start of session →
claude-hook session-start(engages hook_active). - Each prompt submission →
claude-hook prompt-submit(state=running). - Claude needs input (e.g. tool approval) →
claude-hook notification(state=needs_input + banner). - Claude finishes a turn →
claude-hook stop(state=idle + "Turn complete" banner). - End of session →
claude-hook session-end(releases hook_active).