@tryinget/pi-activity-strip 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +59 -21
  2. package/bin/pi-activity-strip.mjs +32 -4
  3. package/native/bin/linux-x64-gnu/artifact.json +4 -2
  4. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  5. package/native/panel/src/app.rs +1141 -84
  6. package/native/panel/src/app_gui_tests.rs +730 -0
  7. package/native/panel/src/app_hover_gui_tests.rs +917 -0
  8. package/native/panel/src/card_view.rs +19 -4
  9. package/native/panel/src/directory_view.rs +471 -0
  10. package/native/panel/src/hints.rs +65 -0
  11. package/native/panel/src/hover-lifecycle-fixtures.json +30 -0
  12. package/native/panel/src/hover_keyboard.rs +237 -0
  13. package/native/panel/src/interaction.rs +156 -0
  14. package/native/panel/src/keyboard-observation-fixtures.json +241 -0
  15. package/native/panel/src/keyboard_observation.rs +70 -0
  16. package/native/panel/src/main.rs +5 -0
  17. package/native/panel/src/protocol.rs +93 -8
  18. package/native/panel/src/runtime.rs +85 -12
  19. package/native/panel/src/style.css +85 -0
  20. package/package.json +6 -7
  21. package/src/broker/server.mjs +31 -14
  22. package/src/client/launcher.mjs +3 -7
  23. package/src/client/runtime-lock.mjs +2 -1
  24. package/src/common/agent-identity.mjs +6 -1
  25. package/src/common/contracts.d.ts +2 -0
  26. package/src/common/contracts.ts +39 -0
  27. package/src/common/keyboard-observation.mjs +35 -0
  28. package/src/common/overlay-readiness.mjs +11 -0
  29. package/src/common/presence-apps.mjs +57 -0
  30. package/src/common/recovery-identity.mjs +85 -0
  31. package/src/common/recovery-manifest.mjs +417 -0
  32. package/src/common/recovery-types.ts +75 -0
  33. package/src/common/ribbon-status.mjs +41 -0
  34. package/src/common/status-report.mjs +37 -2
  35. package/src/common/workspace-view.mjs +118 -0
  36. package/src/native/ak-runtime.mjs +50 -9
  37. package/src/native/focus-visibility.mjs +67 -0
  38. package/src/native/ghostty-tab-inventory.py +40 -23
  39. package/src/native/hover-keyboard.mjs +32 -0
  40. package/src/native/interaction-context.mjs +45 -0
  41. package/src/native/keyboard-entry.mjs +32 -0
  42. package/src/native/main.mjs +91 -42
  43. package/src/native/panel-binary.mjs +25 -8
  44. package/src/native/panel-neutral-source.mjs +53 -0
  45. package/src/native/panel-presentation.mjs +137 -0
  46. package/src/native/panel-projection.mjs +354 -18
  47. package/src/native/panel-protocol.mjs +23 -0
  48. package/src/native/panel-window-jump.mjs +90 -0
  49. package/src/native/placement.mjs +83 -20
  50. package/src/native/recovery-inventory.mjs +209 -0
  51. package/src/native/recovery-proc.mjs +178 -0
  52. package/src/native/recovery-sources.mjs +358 -0
  53. package/src/native/runtime-diagnostics.mjs +242 -0
  54. package/src/native/strip-hotkey-kdl.mjs +252 -0
  55. package/src/native/strip-hotkey.mjs +62 -0
  56. package/src/native/tab-inventory.mjs +62 -11
  57. package/src/native/window-jump.mjs +43 -0
  58. package/src/native/workspace-events.mjs +123 -2
package/README.md CHANGED
@@ -20,7 +20,7 @@ The runtime is Electron-free. A Node controller retains the tested telemetry, id
20
20
 
21
21
  - auto-starts with interactive Pi TUI sessions
22
22
  - shows one card per admitted Ghostty terminal on the focused Niri workspace, including tabs hidden behind another tab of their Ghostty window
23
- - covers Pi sessions and other terminal agents (Claude Code, Codex, Gemini and more) found in Ghostty tabs
23
+ - covers Pi sessions and other terminal agents (Claude Code, Codex, Gemini, Cursor's `agent`, nodecode, angel and more) found in Ghostty tabs
24
24
  - restores tiled windows left at the wrong height when its reserved band appears or disappears
25
25
  - draws itself in Ghostty's own theme and follows the desktop between light and dark
26
26
  - sits on the same gap rhythm and corner radius as tiled windows, rather than as a bar on the screen edge
@@ -30,10 +30,12 @@ The runtime is Electron-free. A Node controller retains the tested telemetry, id
30
30
  - marks the exact currently focused terminal card and prefixes hidden-tab cards with `⧉`
31
31
  - labels each card with its Niri window id (`#43`), so a window an agent or tool names by id can be found on the ribbon
32
32
  - keeps monitoring-success cards beside the Activity tile, then active and settled cards
33
- - expands rich details on hover or keyboard focus
33
+ - expands rich details only on actual pointer hover or explicit keyboard-mode entry
34
34
  - supports Left/Right navigation and Shift+Left/Right manual movement
35
35
  - focuses the exact matching Ghostty window on click or Enter, presenting a hidden tab first
36
- - hides completely on workspaces without tracked cards
36
+ - stays present across browser, Obsidian and other app focus, competing overlays, and empty focused workspaces; its reserved band stays constant instead of resizing the desktop
37
+ - keeps presentation separate from keyboard permission: the existing agent/presence-app admission still guards keyboard entry and hover typing; other apps and overlays retain their input
38
+ - teaches its own keyboard shortcut progressively: every card and brand tooltip names it, the window list says what the keyboard would do right now, and keyboard mode shows its key map in the brand block
37
39
  - releases its 84px exclusive zone automatically when hidden or crashed
38
40
 
39
41
  ## Architecture
@@ -47,7 +49,7 @@ Pi publisher streams
47
49
  -> wlr-layer-shell top surface with an 84px exclusive zone
48
50
  ```
49
51
 
50
- Every child-protocol message carries `protocol: 1`, and the panel drops any view carrying another version. A new card field is therefore added as an optional field: a panel that does not know it ignores it, and a panel talking to a controller that does not send it falls back to a default. The version changes only for a change an existing panel would misread.
52
+ The **private controller/panel** NDJSON protocol is now `protocol: 2`; this does not change Pi publisher or broker message envelopes. The controller requires `panelProtocol: 2` in the artifact receipt and checks the actual child readiness/event version before sending any view or command. The panel rejects incompatible input with a diagnostic and exit 1. Separately receipted older overrides therefore fail closed rather than silently misreading ownership claim ids as data revisions. Optional card fields remain additive; incompatible private changes require a version change.
51
53
 
52
54
  Layer-shell replaces the old floating Electron window and dynamic Niri-config strut helper. The package no longer edits `~/.config/niri/config.kdl`, resets tiled heights, or requires Electron.
53
55
 
@@ -99,6 +101,7 @@ node ./bin/pi-activity-strip.mjs focus-session <full-pi-session-id>
99
101
  node ./bin/pi-activity-strip.mjs status
100
102
  node ./bin/pi-activity-strip.mjs doctor
101
103
  node ./bin/pi-activity-strip.mjs snapshot
104
+ node ./bin/pi-activity-strip.mjs manifest
102
105
  node ./bin/pi-activity-strip.mjs claude-hooks
103
106
  node ./bin/pi-activity-strip.mjs stop
104
107
  ```
@@ -107,6 +110,36 @@ node ./bin/pi-activity-strip.mjs stop
107
110
 
108
111
  `fix-top` is now a compatibility no-op: layer-shell placement is compositor-owned.
109
112
 
113
+ A broker pong alone does not establish overlay readiness. Launcher/startup waiting and CLI
114
+ `status` succeed only with explicit `runtimeStatus.state: ready`. CLI `open` reports unknown
115
+ or starting readiness as non-success without stopping or replacing that live runtime. In the optional focus-visibility mode, a ready
116
+ ribbon may correctly be hidden while a non-agent window is focused; visibility is not startup
117
+ readiness. This check does not itself approve the controller/panel artifact pair.
118
+
119
+ `manifest` is a separate read-only recovery inventory CLI: it includes registry/process observations
120
+ missing from the broker-only `snapshot`, per-link provenance, uncertainty and coverage counts.
121
+ Paths are redacted by default; no transcript body, title or prompt text is emitted. Process-only
122
+ rows are not counted as established session identities. It never starts the ribbon or reopens a
123
+ session. See the [schema, invocation and evidence limits](docs/project/recovery-manifest.md).
124
+ The panel's existing discovery and AK-chip behavior is unchanged.
125
+
126
+ ### Read-only ribbon diagnostics
127
+
128
+ `status` also explains controller focus eligibility and reserved keyboard authorization
129
+ (`none`, `hover`, `explicit`), flags expansion reports as stale on hidden surfaces, and
130
+ shows sampled panel-PID executable/receipt/protocol hashes. Samples run asynchronously;
131
+ `pending`, unavailable, incompatible and changed-during-observation states are explicit.
132
+ The controller startup-source fingerprint is a source-tree observation, not proof of every
133
+ loaded byte; a matched receipt is integrity metadata, not approval or original-child custody.
134
+ Older controllers report missing diagnostics rather than guessed facts.
135
+
136
+ This optional broker observation is separate from the recovery `manifest`: it creates no AK
137
+ claim or custody. It emits no typed query, prompt, credential or executable path. A reserved
138
+ grant is not proof of actual capture. Exact expiry remains **not observed** because private
139
+ protocol2 does not report GTK's monotonic deadline; expansion cause is likewise not inferred
140
+ from an expanded bit alone. Nothing in diagnostics authorizes restart, repair or activation.
141
+
142
+
110
143
  ## Keyboard-only entry
111
144
 
112
145
  Bind a Niri key to the fail-closed CLI entrypoint:
@@ -117,36 +150,47 @@ binds {
117
150
  }
118
151
  ```
119
152
 
120
- The shortcut toggles exclusive keyboard mode. On entry, the first card is selected. Left/Right wraps through cards, Shift+Left/Right moves the selected card with wraparound, Enter focuses its exact Ghostty terminal and exits, and Escape or a second shortcut press exits without activation. Space does not activate a card. Pointer hover expands detail but deliberately does not capture keyboard input. Empty workspaces and click-through mode reject keyboard entry.
153
+ The shortcut enters explicit exclusive keyboard mode (or upgrades an existing hover grab); a second shortcut exits. On entry the card under the pointer is selected, else the focused window's card, else the first card fully in view, so entering never scrolls the row away from what you are looking at. Left/Right wraps through cards, Shift+Left/Right moves the selected card with wraparound, Enter focuses its exact Ghostty terminal and exits, and Escape exits without activation. Space does not activate a card. Hover-and-type navigation is also available below; it does not inherit explicit mode's arrow navigation or ability to jump to unlisted windows. Empty workspaces and click-through mode reject both entry paths.
154
+
155
+ The ribbon reads the bound chord from the Niri config (`$NIRI_CONFIG`, else `$XDG_CONFIG_HOME/niri/config.kdl`, else `~/.config/niri/config.kdl`), finds the bind that spawns `focus-strip`, and shows it as `Super+Shift+A`; `PI_ACTIVITY_STRIP_HOTKEY` overrides it. If no bind is found, the hints name keyboard mode without inventing a chord. The hints come in three steps, each shown once you are already doing the step before it:
156
+
157
+ 1. **Hovering anything:** card and brand tooltips end with the shortcut (`Super+Shift+A drives the strip from the keyboard`); the brand tooltip also carries the whole key map.
158
+ 2. **Hovering the expanded ribbon:** the window list's status line says what the keyboard can do now — `Click or Super+Shift+A to type` before it may capture keys, `Type a name or number · ⏎ go` once it may.
159
+ 3. **In keyboard mode:** the brand block shows `⌨ KEYBOARD MODE` and `←→ move · ⏎ open · Esc` where the project and folder normally are, and returns to them on exit.
121
160
 
122
161
  ## Interaction contract
123
162
 
124
163
  - **Workspace locality:** Niri focus events and workspace list changes trigger immediate reprojection; bounded polling remains a fallback.
125
- - **Hide/reclaim:** zero cards unmaps the layer surface. Niri then removes its exclusive zone as part of normal Wayland surface lifecycle.
164
+ - **Persistent presentation:** by default, once a focused Niri workspace is known, the compact surface stays mapped even without cards or a focused application window. Switching apps or opening another overlay does not change its reservation. Compositor stacking remains normal: the strip uses the Top layer, not Overlay. Presence does not grant keyboard access, and losing an interaction source still releases input immediately. `PI_ACTIVITY_STRIP_VISIBILITY=focus` restores the earlier hide/reclaim policy below.
165
+ - **Keyboard admission / optional hide/reclaim:** the ribbon is eligible for keyboard interaction while the focused window on the focused workspace is either exactly one admitted agent card's window or a presence app (`PI_ACTIVITY_STRIP_PRESENCE_APP_IDS`, default every Ghostty window and 1Password). A presence app is a neutral source: the strip stays up, accepts the shortcut and hover typing, and a jump from it is checked against that same window, but no card is claimed as "you are here". Other non-agent focus, missing/ambiguous ownership, or zero cards revokes keyboard eligibility; only in focus-visibility mode does this unmap the surface and release its exclusive zone. A controller-reserved hover or explicit keyboard request may preserve eligibility through Niri's transient NULL focused window on that same workspace; a known non-agent focus always revokes it, even if an old panel acknowledgement arrives later. Returning starts compact, unless there is actual pointer hover or a new explicit shortcut. Hiding, losing keyboard ownership, and pointer leave outside explicit keyboard mode clear stale hover, brand, search and keyboard engagement and revoke delayed callbacks.
126
166
  - **Crash behavior:** panel lifetime is bound to the Node controller through Linux parent-death signaling and stdin EOF. Unexpected panel exits are restarted with bounded backoff; a dead surface cannot retain an exclusive zone.
127
167
  - **Exact focus:** card activation returns to Node, which performs existing fail-closed terminal identity resolution and Niri focus.
128
168
  - **Hidden tabs:** a Ghostty window title only names its active tab. A bound surface whose title is not visible is placed through its Ghostty host process: one host window is exact containment; several host windows use the window remembered for that tab. Memory comes from titles seen while the strip runs and from a read-only AT-SPI inventory of tab labels, and is persisted per Niri instance under `~/.pi/agent/state/pi-activity-strip/surface-bindings.json`. A tab whose window has never been observed stays unplaced rather than guessed; `status` reports that count. Activating a hidden-tab card calls the host process's `present-surface` action on the session bus, then focuses the window, and reports success only after the title proves the tab is visible.
129
- - **Window ids:** agents and tools name windows by their Niri window id (`claude-window` prints `window_id`, continuity receipts carry it). Each card shows that id as a quiet `#43` chip at the start of its footer; a hidden tab has no window of its own, so its chip shows the window hosting it. Hover or keyboard detail adds a `window` row with the workspace number the operator sees, Niri's workspace `idx` and never its internal workspace id, for example `#43 · workspace 2` or `host #43 · workspace 2` for a hidden tab. The tooltip and accessible label name the window too. Off Niri there is no window id, so no chip is drawn.
169
+ - **Window ids:** agents and tools name windows by their Niri window id (`agent-window` prints `window_id`, continuity receipts carry it). Each card shows that id as a quiet `#43` chip at the start of its footer; a hidden tab has no window of its own, so its chip shows the window hosting it. Hover or keyboard detail adds a `window` row with the workspace number the operator sees, Niri's workspace `idx` and never its internal workspace id, for example `#43 · workspace 2` or `host #43 · workspace 2` for a hidden tab. The tooltip and accessible label name the window too. Off Niri there is no window id, so no chip is drawn.
130
170
  - **Agent tabs:** a tab is admitted as an agent when the process owning its terminal is a recognized agent CLI, never on a window title alone, so plain terminal programs are not cards. Claude Code tabs are identified exactly through the per-session scratchpad the process holds open, which yields the session id and the title Claude Code put on the terminal; that title places the tab in its window and is remembered so the tab stays placed once hidden.
131
- - **AK task references:** cards can show the Agent Kernel task their session is working on. The controller reads the read-only `ak` CLI (`ak task list --status claimed --format json --all --verbose` and `ak task deferred --format json --all`) on a calm 15-second clock and joins claims by exact Pi session id under the AK5700 claim semantics: only `session-<uuid>` claims count, and an expired lease is vacant custody that renders nothing. Chips sit in the card's footer as bare task numbers (`AK5701`, title in the tooltip and the expanded inspector), one per card with the rest folded into a `+N` count, so the folder name keeps the header. A card whose session holds a live claim shows a clickable chip that reuses the card's own activation, so clicking it focuses the exact Ghostty window of the claiming session, presenting hidden tabs first. Claims whose lease has not lapsed but whose claiming session no longer exists ("claim outlives session") and tasks carrying an active deferral are joined by task repo onto cards working inside that repo and render as inert badges, never buttons — there is no live window to focus and no AK action is ever offered. Ambiguous session matches (one logical session resumed into two terminals), a missing `ak` binary, or malformed output bind nothing: fail closed, no chip, no invented state, strip behavior unchanged. The strip never writes AK state and never touches the society database directly.
171
+ - **AK task references:** cards can show the Agent Kernel task their session is working on. The controller reads the read-only `ak` CLI (`ak task list --status claimed --format json --all --verbose` every 15 seconds, then `ak task deferred --format json --all` at most every 5 minutes, since every AK call holds one workstation-wide lock and the full deferral list held it 3 s of every 15; AK7943) and joins claims by exact Pi session id under the AK5700 claim semantics: only `session-<uuid>` claims count, and an expired lease is vacant custody that renders nothing. Chips sit in the card's footer as bare task numbers (`AK5701`, title in the tooltip and the expanded inspector), one per card with the rest folded into a `+N` count, so the folder name keeps the header. A card whose session holds a live claim shows a clickable chip that reuses the card's own activation, so clicking it focuses the exact Ghostty window of the claiming session, presenting hidden tabs first. Claims whose lease has not lapsed but whose claiming session no longer exists ("claim outlives session") and tasks carrying an active deferral are joined by task repo onto cards working inside that repo and render as inert badges, never buttons — there is no live window to focus and no AK action is ever offered. Because the deferral list is reused, a new deferral badge can appear, or a resumed task's badge linger, up to about five minutes late; a resumed task that a Pi session claims again rereads the list within one claim tick, since AK never lets a deferred task be claimed. `status` shows when the list was read. Ambiguous session matches (one logical session resumed into two terminals), a missing `ak` binary, or malformed output bind nothing: fail closed, no chip, no invented state, strip behavior unchanged. The strip never writes AK state and never touches the society database directly.
132
172
  - **Codex telemetry:** Codex keeps a thread index naming every session's rollout file, working directory and title. A process binds to its thread by an open rollout descriptor, or, before any task has run, by being the only session created in that directory after the process started; anything ambiguous binds nothing. The rollout tail then reports the running tool and its command, turn count, approval and sandbox policy, prompt and reply. Reading the index needs the runtime's built-in SQLite, and a host without it degrades to a process-only card.
133
173
  - **Claude Code telemetry:** cards read live state from the tail of the session transcript, giving the topic, current tool and its target, last prompt, latest reply, turn count and activity clock with no configuration. That format is internal to Claude Code and can change between releases, so a transcript that no longer parses degrades to a process-only card rather than inventing activity. Optional hooks add the one state a transcript cannot express, that a session is blocked waiting for you; run `claude-hooks` for the settings fragment. Only low-frequency events are hooked, so nothing runs per tool call. OpenTelemetry is deliberately not used: it reports aggregate usage and cost, not which tool a session is running now.
134
- - **You are here:** the block at the ribbon's left names the focused window's project in large type with its folder beneath (home written as `~`, cut from the front so the deepest folder stays visible), and the matching card carries a solid accent ring. With no agent window focused it shows the session counts instead.
174
+ - **You are here:** the block at the ribbon's left names the focused window's project in large type with its folder beneath (home written as `~`, cut from the front so the deepest folder stays visible), and the matching card carries a solid accent ring. When the panel itself takes the keyboard, Niri briefly reports no focused window; the ring and the block keep the last focused window on that same workspace instead of blanking, so nothing reflows under the pointer. A workspace change still clears them.
175
+ - **Window list and jump by number:** while expanded, the brand column lists every admitted agent window across workspaces, alphabetically by project, with its workspace (`ws2`) and Niri window number (`#432`). The current window is ringed and its card scrolls into view once the pointer has left the ribbon and it no longer holds the keyboard; while you point at or type into the row it never scrolls on its own. Brand hover opens every card's details; card hover opens only that card. **Hover, type a project name or listed agent window number, then Enter** to focus that exact target. Matching is case-insensitive and Backspace edits. Real pointer entry requests permission from the controller before the panel captures keys; GTK focus alone cannot authorize capture. Body, card, brand and directory crossings form one interaction region. Hover cannot jump to an unlisted window. Leaving the region, Escape, activation/jump, or loss of eligible focus ends the engagement: keyboard, query and list are released and the ribbon collapses. About three seconds without relevant typing releases only the keyboard and clears the query—even a nonempty query—while the card or list under a still pointer stays open. Periodic telemetry cannot recapture it: leave/re-enter or click for fresh intent. An unsupported key (including an arrow/shortcut) interrupts hover: it releases the keyboard and leaves whatever is open under the pointer open; that interrupting key is **not replayed** to another application, but subsequent typing goes there. The explicit shortcut (`focus-strip`) upgrades hover and retains Left/Right and Shift+Left/Right card navigation/manual movement; only explicit mode can jump to an existing unlisted number. Clicks still activate exact cards or directory entries, including hidden tabs. Every activation/jump returns the keyboard immediately.
135
176
  - **Appearance:** the ribbon reads the same theme files Ghostty reads. It resolves the `theme` setting for the desktop's current colour scheme, including the `light:…,dark:…` form, and takes colours set directly in the config over the theme file, exactly as Ghostty layers them. State colours reuse the terminal's own meanings, so green is settled, yellow is working, red failed, and the cursor colour marks a session waiting for you. The panel derives every shade from eight named colours, so a theme change is a handful of values and one stylesheet reload rather than a restart. A theme that cannot be read falls back to a neutral palette.
136
177
  - **Height repair:** showing or hiding the ribbon changes the output working area, and a window whose height is not automatic keeps the old value. After each change the strip compares window heights before and after, and resets to automatic only those windows still sitting at a height the moving windows just vacated. A height nobody vacated is never touched, each window is reset at most once per height, and a pass is bounded.
137
178
  - **Two identity keys:** a Ghostty surface id is a per-process handle that can drift away from the value a long-lived Pi process captured at startup, while the 32-hex session token in the same title never does. Memory therefore stores both, and lookups prefer the exact surface. A session token that two windows claim is ambiguous and binds nothing; two terminals resuming one logical session in the same window are both placed there.
138
- - **Ordering:** monitoring, active, and settled groups refresh on a calm 15-second clock. Manual moves survive until regroup or restart.
179
+ - **Ordering:** monitoring, active, and settled groups refresh on a calm 15-second clock. A regroup waits while the pointer is over the ribbon or it holds the keyboard, so cards never trade places under the pointer. Manual moves survive until regroup or restart.
139
180
  - **Accessibility:** cards expose native GTK labels, selected/expanded state, activation descriptions, and GTK accessible announcements.
140
181
 
141
182
  ## Environment controls
142
183
 
143
184
  - `PI_ACTIVITY_STRIP_AUTO_START=0` disables extension autostart.
185
+ - `PI_ACTIVITY_STRIP_VISIBILITY=focus` opts into the earlier focus-dependent hide/reclaim behavior. Default is persistent presentation, including empty focused workspaces. Environment controls are read by the controller at startup; restart the strip to apply a change.
144
186
  - `PI_ACTIVITY_STRIP_CLICK_THROUGH=1` installs an empty Wayland input region and disables keyboard entry.
145
- - `PI_ACTIVITY_STRIP_NATIVE_PANEL_BIN=/absolute/path` selects another receipted panel artifact.
187
+ - `PI_ACTIVITY_STRIP_NATIVE_PANEL_BIN=/absolute/path` selects another receipted panel artifact. A nonempty selection is binding: if missing or nonexecutable, startup fails instead of silently falling back to the packaged panel, including with the development escape hatch.
146
188
  - `PI_ACTIVITY_STRIP_SOCKET_DIR` and `PI_ACTIVITY_STRIP_SOCKET_PATH` isolate broker fixtures and nested-compositor tests.
147
189
  - `PI_ACTIVITY_STRIP_TAB_INVENTORY=0` disables the read-only AT-SPI tab inventory; hidden tabs are then placed only from titles seen while the strip runs.
148
190
  - `PI_ACTIVITY_STRIP_AK_TASKS=0` disables AK task reference joining.
149
191
  - `PI_ACTIVITY_STRIP_AK_BIN=/absolute/path` selects the `ak` binary used for the read-only task queries (default `ak` on `PATH`).
192
+ - `PI_ACTIVITY_STRIP_PRESENCE_APP_IDS` edits which Niri apps admit keyboard interaction (and keep the ribbon up in optional focus-visibility mode). A comma-separated list adds ids to the defaults (`com.mitchellh.ghostty`, `com.tryinget.ghosttysidequest`, `com.onepassword.OnePassword`, `1Password`), `-id` removes one, `none` empties the set (strict agent-only behaviour), and `*` admits every app as a keyboard interaction source. The wildcard is unnecessary for persistent visibility; it also broadens which apps keep the ribbon up in focus-visibility mode.
193
+ - `PI_ACTIVITY_STRIP_HOTKEY="Super+Shift+A"` sets the shortcut the hints name, instead of reading it from the Niri config.
150
194
  - `PI_ACTIVITY_STRIP_AGENT_TABS=0` disables discovery of non-Pi agent tabs.
151
195
  - `PI_ACTIVITY_STRIP_AGENT_KINDS_DISABLED=claude,codex` excludes named agent kinds from discovery.
152
196
  - `CODEX_HOME` selects a non-default Codex home when reading its thread index (default `~/.codex`).
@@ -154,7 +198,7 @@ The shortcut toggles exclusive keyboard mode. On entry, the first card is select
154
198
  - `PI_ACTIVITY_STRIP_HEIGHT_REPAIR=0` disables restoring windows stranded at the previous working-area height.
155
199
  - `PI_ACTIVITY_STRIP_COLOR_SCHEME=light|dark` pins the ribbon to one scheme instead of following the desktop.
156
200
 
157
- Unverified native binaries are rejected unless `PI_ACTIVITY_STRIP_ALLOW_UNVERIFIED_PANEL=1` is explicitly set for development fixtures.
201
+ Unverified native binaries are rejected unless `PI_ACTIVITY_STRIP_ALLOW_UNVERIFIED_PANEL=1` is explicitly set for development fixtures. That flag does **not** bypass the private-protocol receipt or actual readiness version checks.
158
202
 
159
203
  ## Verification
160
204
 
@@ -174,15 +218,9 @@ The staged artifact receipt binds:
174
218
  - glibc symbol floor
175
219
  - required shared libraries
176
220
 
177
- To see the panel without touching the live ribbon, run its built-in demo cards in a nested Niri with a config that spawns nothing:
178
-
179
- ```bash
180
- printf 'prefer-no-csd\nhotkey-overlay {\n skip-at-startup\n}\n' >/tmp/pi-activity-preview.kdl
181
- niri -c /tmp/pi-activity-preview.kdl -- env PI_ACTIVITY_STRIP_PANEL_DEMO=1 \
182
- "$PWD/native/bin/linux-x64-gnu/pi-activity-strip-panel"
183
- ```
221
+ For isolated GUI checks, use the canonical `nested-niri-gui-isolation` skill's helper (`up`, `env`, `check`, `down`) with an owned state directory under `$TMPDIR`, never a bare compositor/terminal helper. Scope every client and IPC call to that nested display/socket; never address operator windows. The panel is unique per Wayland display.
184
222
 
185
- The panel is unique per Wayland display: this demo runs beside the live ribbon, while a second panel on the live display is refused with an error rather than reserving a second band. Never run the panel under a private session bus such as `dbus-run-session`: GTK then starts a second accessibility bus that takes over the desktop's AT-SPI socket and leaves it dead when it exits.
223
+ Do not run GTK under a private session bus while sharing the host AT-SPI runtime socket: a second accessibility bus can take it over and leave the desktop's bridge dead. The isolated fixture in `tests/support/isolated-niri-check.mjs` instead uses a private `XDG_RUNTIME_DIR` and session bus **and** disables the bridge with `GTK_A11Y=none` / `NO_AT_BRIDGE=1`; accessibility is explicitly untested there. It requires the owned helper's `state.json` and matching nested environment before launching any GUI. See [independent-review repair evidence](docs/project/2026-10-16-ak7114-independent-review-repair.md) for current commands and coverage limits; the [prior focus/hover record](docs/project/2026-10-15-ak7114-focus-hover.md) preserves the superseded pointer-only choice.
186
224
 
187
225
  Live verification must inspect Niri layers rather than regular windows:
188
226
 
@@ -197,7 +235,7 @@ Implemented:
197
235
  - local broker and telemetry publishers
198
236
  - native GTK4 layer-shell rendering
199
237
  - workspace-local Niri projection
200
- - hide/reclaim and restore
238
+ - persistent presentation with stable reserved space; optional focus-dependent hide/reclaim
201
239
  - pointer and keyboard card interaction
202
240
  - exact Ghostty activation, including hidden tabs via `present-surface`
203
241
  - hidden Ghostty tab placement through host process containment and learned window memory
@@ -31,6 +31,7 @@ import {
31
31
  ACTIVITY_STRIP_START_TIMEOUT_MS,
32
32
  ACTIVITY_STRIP_STOP_TIMEOUT_MS,
33
33
  } from "../src/common/constants.mjs";
34
+ import { isOverlayReady } from "../src/common/overlay-readiness.mjs";
34
35
  import { makeMessage } from "../src/common/protocol.mjs";
35
36
  import { formatBrokerRuntimeStatus } from "../src/common/status-report.mjs";
36
37
 
@@ -45,7 +46,7 @@ const STOP_MESSAGES = {
45
46
 
46
47
  function usage() {
47
48
  console.log(
48
- `Usage: pi-activity-strip <open|focus-strip|focus-session|status|doctor|snapshot|claude-hooks|fix-top|stop|serve> [options]\n\nCommands:\n open Start the interactive top-row activity strip (--click-through opts out)\n focus-strip Focus the visible strip already resident on the focused Niri workspace\n focus-session ID Focus the one Ghostty/Niri window matching an exact Pi session identity\n status Check broker + overlay readiness and surface runtime warnings\n doctor Inspect host compatibility assumptions before opening the strip\n snapshot Print the current broker snapshot as JSON\n claude-hooks Print the Claude Code settings fragment for live agent telemetry\n fix-top Confirm compositor-owned layer-shell placement\n stop Ask the running strip to shut down\n serve Internal helper; starts the native runtime in the foreground\n`,
49
+ `Usage: pi-activity-strip <open|focus-strip|focus-session|status|doctor|snapshot|manifest|claude-hooks|fix-top|stop|serve> [options]\n\nCommands:\n open Start the interactive top-row activity strip (--click-through opts out)\n focus-strip Focus the visible strip already resident on the focused Niri workspace\n focus-session ID Focus the one Ghostty/Niri window matching an exact Pi session identity\n status Check broker + overlay readiness and surface runtime warnings\n doctor Inspect host compatibility assumptions before opening the strip\n snapshot Print the current broker snapshot as JSON\n manifest Read-only recovery inventory JSON (no autostart; paths redacted by default)\n [--include-paths] [--pi-session-root PATH] [--no-ak]\n claude-hooks Print the Claude Code settings fragment for live agent telemetry\n fix-top Confirm compositor-owned layer-shell placement\n stop Ask the running strip to shut down\n serve Internal helper; starts the native runtime in the foreground\n`,
49
50
  );
50
51
  }
51
52
 
@@ -53,10 +54,14 @@ function usage() {
53
54
  async function openStrip({ detached = true } = {}) {
54
55
  if (await isBrokerAlive()) {
55
56
  const status = await getBrokerStatus({ expectReply: true }).catch(() => null);
56
- if (status?.runtimeStatus?.state !== "error") {
57
+ if (isOverlayReady(status)) {
57
58
  console.log("Activity strip is already running.");
58
59
  return 0;
59
60
  }
61
+ if (status?.runtimeStatus?.state !== "error") {
62
+ console.error(formatBrokerRuntimeStatus(status));
63
+ return 1; // Unknown/starting is not success and does not authorize replacement.
64
+ }
60
65
  await requestBrokerShutdown().catch(() => null);
61
66
  // Its broker closes before it exits; starting before the lock is released would lose it.
62
67
  const exit = await waitForRuntimeExit(runtimeLockPath, {
@@ -141,6 +146,30 @@ async function main() {
141
146
  }
142
147
 
143
148
  switch (command) {
149
+ case "manifest": {
150
+ let includePaths = false;
151
+ let noAk = false;
152
+ let piSessionRoot;
153
+ const args = process.argv.slice(3);
154
+ for (let i = 0; i < args.length; i++) {
155
+ if (args[i] === "--include-paths") includePaths = true;
156
+ else if (args[i] === "--no-ak") noAk = true;
157
+ else if (args[i] === "--json") {
158
+ /* JSON is always emitted. */
159
+ } else if (args[i] === "--pi-session-root" && args[i + 1]?.startsWith("/"))
160
+ piSessionRoot = args[++i];
161
+ else throw new Error("manifest: unsupported option or missing absolute --pi-session-root");
162
+ }
163
+ const { collectRecoveryManifest } = await import("../src/native/recovery-inventory.mjs");
164
+ console.log(
165
+ JSON.stringify(
166
+ await collectRecoveryManifest({ includePaths, noAk, piSessionRoot }),
167
+ null,
168
+ 2,
169
+ ),
170
+ );
171
+ return;
172
+ }
144
173
  case "open":
145
174
  process.exitCode = await openStrip({ detached: true });
146
175
  return;
@@ -194,8 +223,7 @@ async function main() {
194
223
  } else {
195
224
  console.log(formatBrokerRuntimeStatus(result));
196
225
  }
197
- process.exitCode =
198
- result?.ok && (!result.runtimeStatus || result.runtimeStatus.state === "ready") ? 0 : 1;
226
+ process.exitCode = isOverlayReady(result) ? 0 : 1;
199
227
  } catch {
200
228
  console.log(
201
229
  jsonOutput ? JSON.stringify({ ok: false, state: "stopped" }, null, 2) : "stopped",
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "schema": "pi-activity-strip-native-artifact.v1",
3
3
  "target": "x86_64-unknown-linux-gnu",
4
- "sha256": "99b6f36e858b9c241c5aa73f7517c0f5f418673f54d9fa245156272c39bf004b",
4
+ "panelProtocol": 2,
5
+ "sha256": "fb39af413e44b9bb28976c60752d883201735ef7b490164d5054fd87a224d279",
5
6
  "cargoLockSha256": "f8ebc11bc721c569af982169e448a5ea4fed15bc06001017d97acda22b1c890b",
6
- "sourceSha256": "b272f4f3131cc4335ed6324ad051042fbd3d577b16944189fcdeaed7e776628f",
7
+ "sourceSha256": "17b10a2894b0377e7502feeb798462d74a5b427633bc75f20dd4044dde43291c",
7
8
  "rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
8
9
  "glibcFloor": "GLIBC_2.34",
9
10
  "neededSonames": [
10
11
  "libgtk4-layer-shell.so.0",
11
12
  "libgtk-4.so.1",
12
13
  "libcairo.so.2",
14
+ "libgraphene-1.0.so.0",
13
15
  "libgio-2.0.so.0",
14
16
  "libgobject-2.0.so.0",
15
17
  "libglib-2.0.so.0",