@tryinget/pi-activity-strip 0.4.0 → 0.6.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 (61) hide show
  1. package/README.md +136 -157
  2. package/bin/pi-activity-strip-claude-hook.mjs +71 -0
  3. package/bin/pi-activity-strip.mjs +50 -60
  4. package/native/bin/linux-x64-gnu/artifact.json +21 -0
  5. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  6. package/native/panel/Cargo.lock +1045 -0
  7. package/native/panel/Cargo.toml +23 -0
  8. package/native/panel/src/app.rs +606 -0
  9. package/native/panel/src/card_view.rs +602 -0
  10. package/native/panel/src/main.rs +31 -0
  11. package/native/panel/src/protocol.rs +297 -0
  12. package/native/panel/src/runtime.rs +151 -0
  13. package/native/panel/src/style.css +250 -0
  14. package/package.json +16 -4
  15. package/src/broker/server.mjs +43 -7
  16. package/src/broker/session-store.mjs +41 -13
  17. package/src/client/broker-client.mjs +10 -3
  18. package/src/client/session-telemetry.mjs +54 -14
  19. package/src/common/activity-order.mjs +15 -9
  20. package/src/common/agent-identity.mjs +163 -0
  21. package/src/common/ak-tasks.mjs +354 -0
  22. package/src/common/alignment-controller.mjs +24 -42
  23. package/src/common/card-display.mjs +5 -0
  24. package/src/common/claude-events.mjs +190 -0
  25. package/src/common/claude-hook-config.mjs +56 -0
  26. package/src/common/claude-transcript.mjs +163 -0
  27. package/src/common/codex-transcript.mjs +153 -0
  28. package/src/common/compatibility.mjs +28 -13
  29. package/src/common/constants.mjs +10 -28
  30. package/src/common/contracts.d.ts +68 -4
  31. package/src/common/contracts.ts +105 -4
  32. package/src/common/ghostty-present.mjs +100 -0
  33. package/src/common/ghostty-theme.mjs +149 -0
  34. package/src/common/niri-focus.mjs +209 -140
  35. package/src/common/protocol.mjs +48 -16
  36. package/src/common/session-cards.mjs +105 -0
  37. package/src/common/status-report.mjs +53 -1
  38. package/src/common/surface-bindings.mjs +234 -0
  39. package/src/common/telemetry.mjs +8 -1
  40. package/src/common/terminal-identity.mjs +212 -0
  41. package/src/common/window-placement.mjs +59 -0
  42. package/src/common/workspace-view.mjs +161 -0
  43. package/src/native/agent-discovery.mjs +294 -0
  44. package/src/native/ak-runtime.mjs +164 -0
  45. package/src/native/codex-discovery.mjs +128 -0
  46. package/src/native/ghostty-tab-inventory.py +99 -0
  47. package/src/native/height-repair.mjs +154 -0
  48. package/src/native/main.mjs +499 -0
  49. package/src/native/panel-projection.mjs +161 -0
  50. package/src/native/placement.mjs +225 -0
  51. package/src/native/tab-inventory.mjs +188 -0
  52. package/src/native/theme-runtime.mjs +72 -0
  53. package/src/native/theme-source.mjs +197 -0
  54. package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
  55. package/src/common/electron.mjs +0 -57
  56. package/src/electron/main.mjs +0 -499
  57. package/src/electron/niri-native-window-runtime.mjs +0 -178
  58. package/src/electron/preload.cjs +0 -50
  59. package/src/electron/renderer-visibility-runtime.mjs +0 -107
  60. package/src/electron/workspace-view-runtime.mjs +0 -360
  61. package/src/ui/strip-html.mjs +0 -500
package/README.md CHANGED
@@ -1,91 +1,82 @@
1
1
  ---
2
- summary: "Top-row live activity strip for local Pi sessions running in Ghostty or other terminals."
2
+ summary: "Native Niri layer-shell activity ribbon for live Pi sessions."
3
3
  read_when:
4
4
  - "Starting work in this package workspace."
5
- - "Installing or verifying the activity strip in Pi."
5
+ - "Installing or verifying the Activity Strip in Pi."
6
6
  system4d:
7
- container: "Monorepo package for a local broker + Electron overlay + Pi extension telemetry seam."
8
- compass: "Make Pi session activity visible at a glance without changing the operator's normal terminal workflow."
9
- engine: "Pi extension emits session telemetry -> local broker aggregates -> top-row Electron strip renders live state."
10
- fog: "Main risks are runtime drift across Pi host versions, Electron availability, and stale-session behavior under real long-running work."
7
+ container: "Monorepo package with a Node telemetry broker/controller and native GTK4 layer-shell panel."
8
+ compass: "Show exact workspace-local Pi activity without changing normal terminal workflows or mutating compositor configuration."
9
+ engine: "Pi telemetry -> local broker -> Niri workspace projection -> native layer-shell panel."
10
+ fog: "Main risks are native ABI availability, stale-session identity, and unverified multi-output behavior."
11
11
  ---
12
12
 
13
13
  # @tryinget/pi-activity-strip
14
14
 
15
- A Pi extension package that gives you a **screen-top activity strip** showing what your live Pi sessions are doing.
15
+ A screen-top activity ribbon for the coding agents running in your Ghostty tabs. Pi sessions publish their own telemetry; other terminal agents such as Claude Code are discovered from the process table.
16
16
 
17
- This package is designed for the exact workflow you asked for:
18
- - multiple Ghostty tabs
19
- - multiple Pi sessions
20
- - a persistent top-row ribbon
21
- - fine-grained live detail without changing how you normally run Pi
17
+ The runtime is Electron-free. A Node controller retains the tested telemetry, identity, ordering, and exact-focus logic; a small Rust/Relm4/GTK4 panel owns rendering and the Wayland layer-shell surface.
22
18
 
23
19
  ## What it does
24
20
 
25
- - auto-starts a local top-row overlay when Pi starts in a TUI session
26
- - tracks each active Pi session independently
27
- - on Niri, shows one card per tracked live Pi terminal on only the focused workspace; non-Niri desktops retain the global live-session view
28
- - surfaces:
29
- - repo/session label
30
- - current phase
31
- - current tool or target
32
- - fine-grained detail text
33
- - elapsed time plus last-seen freshness
34
- - state color (`thinking`, `tool`, `waiting`, `done`, `error`)
35
- - keeps a local broker so multiple Pi processes can report into one strip
36
- - marks the Pi session in the currently focused Niri/Ghostty terminal with a stronger border and left rail, without adding another label
37
- - keeps green `done`/`monitoring` cards directly beside the Activity tile, followed by active work and then other settled sessions, on a calm 15-second ordering clock
38
- - reveals prompt, response, path, and full activity detail on hover or keyboard focus
39
- - focuses the exact matching Ghostty/Niri window on click or Enter, failing closed when identity is missing or ambiguous
40
- - keeps an aligned strip resident on its Niri workspace while that workspace still has tracked terminals, so visiting an empty workspace does not unmap or reposition it
21
+ - auto-starts with interactive Pi TUI sessions
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
24
+ - restores tiled windows left at the wrong height when its reserved band appears or disappears
25
+ - draws itself in Ghostty's own theme and follows the desktop between light and dark
26
+ - sits on the same gap rhythm and corner radius as tiled windows, rather than as a bar on the screen edge
27
+ - aggregates independent publishers beneath stable terminal cards
28
+ - shows the AK task a session is working on: read-only `ak` output is joined onto cards, clicking the task reference focuses the claiming terminal, and claims that outlive their session or deferred tasks appear as non-clickable badges
29
+ - displays repo, phase, tool, detail, elapsed time, and freshness
30
+ - marks the exact currently focused terminal card and prefixes hidden-tab cards with `⧉`
31
+ - keeps monitoring-success cards beside the Activity tile, then active and settled cards
32
+ - expands rich details on hover or keyboard focus
33
+ - supports Left/Right navigation and Shift+Left/Right manual movement
34
+ - focuses the exact matching Ghostty window on click or Enter, presenting a hidden tab first
35
+ - hides completely on workspaces without tracked cards
36
+ - releases its 84px exclusive zone automatically when hidden or crashed
41
37
 
42
38
  ## Architecture
43
39
 
44
40
  ```text
45
- Pi session
46
- -> activity-strip extension
47
- -> local unix-socket broker
48
- -> Electron top-row overlay
41
+ Pi publisher streams
42
+ -> Node Unix-socket broker and session store
43
+ -> exact terminal identity + Niri workspace projection
44
+ -> versioned NDJSON child protocol
45
+ -> Rust / Relm4 / GTK4 panel
46
+ -> wlr-layer-shell top surface with an 84px exclusive zone
49
47
  ```
50
48
 
51
- This is intentionally **local-first**.
52
- It does not require moving your workflow onto `pi-server` first.
49
+ 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
50
 
54
- ## Current scope
51
+ The compact surface is 84px tall and sits inset by an 8px margin on the top, left and right, matching the compositor's window gaps, with the same 12px corner radius as tiled windows. It therefore reserves 92px in total. One engaged card expands the surface to 276px while the reservation is unchanged, so detail overlays content without repeatedly resizing tiled windows.
55
52
 
56
- Implemented now:
57
- - local per-host broker
58
- - primary-display top-row strip
59
- - one card per tracked live Pi terminal on the focused Niri workspace, regardless of activity state
60
- - headless-safe telemetry publishing
61
- - explicit open/focus-strip/focus-session/status/doctor/snapshot/fix-top/stop commands
62
- - focus-scoped Left/Right navigation and Shift+Left/Right manual card movement
63
- - local visual capture helpers so the agent can inspect the strip directly
64
-
65
- Not implemented yet:
66
- - multi-monitor strip replication
67
- - historical timeline
68
- - persisted manual card order across strip restarts
69
- - remote observers via `pi-server`
53
+ ## Supported host
70
54
 
71
- ## Installation in Pi
55
+ The packaged native artifact currently supports:
72
56
 
73
- From this package directory:
57
+ - Linux x86_64
58
+ - Wayland
59
+ - a compositor implementing `wlr-layer-shell` (dogfooded on Niri 26.04)
60
+ - GTK4 and gtk4-layer-shell runtime libraries
61
+
62
+ On Arch Linux:
74
63
 
75
64
  ```bash
76
- cd ~/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip
77
- pi install "$PWD"
65
+ sudo pacman -S gtk4 gtk4-layer-shell
78
66
  ```
79
67
 
80
- Then for **existing Pi tabs**:
81
- - run `/reload` in each tab you want tracked
68
+ Multi-output replication remains unimplemented. The current surface is single-output and must not be described as multi-monitor complete.
82
69
 
83
- For **new Pi tabs**:
84
- - the package will load automatically from your Pi settings
70
+ ## Installation
85
71
 
86
- ## Operator commands
72
+ ```bash
73
+ cd ~/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip
74
+ pi install "$PWD"
75
+ ```
76
+
77
+ Reload already-running Pi tabs with `/reload`. New tabs load the package automatically.
87
78
 
88
- ### Package-local CLI
79
+ ## Commands
89
80
 
90
81
  ```bash
91
82
  npm run strip:open
@@ -96,7 +87,7 @@ npm run strip:fix-top
96
87
  npm run strip:stop
97
88
  ```
98
89
 
99
- or directly:
90
+ Direct CLI:
100
91
 
101
92
  ```bash
102
93
  node ./bin/pi-activity-strip.mjs open
@@ -105,130 +96,118 @@ node ./bin/pi-activity-strip.mjs focus-session <full-pi-session-id>
105
96
  node ./bin/pi-activity-strip.mjs status
106
97
  node ./bin/pi-activity-strip.mjs doctor
107
98
  node ./bin/pi-activity-strip.mjs snapshot
108
- node ./bin/pi-activity-strip.mjs fix-top
99
+ node ./bin/pi-activity-strip.mjs claude-hooks
100
+ node ./bin/pi-activity-strip.mjs stop
109
101
  ```
110
102
 
111
- ### Pi slash commands
103
+ `fix-top` is now a compatibility no-op: layer-shell placement is compositor-owned.
112
104
 
113
- Inside Pi:
105
+ ## Keyboard-only entry
114
106
 
115
- ```text
116
- /activity-strip
117
- /activity-strip status
118
- /activity-strip doctor
119
- /activity-strip fix-top
120
- /activity-strip stop
121
- /activity-strip-stop
122
- ```
123
-
124
- In Pi with UI support:
125
- - `/activity-strip status` opens a detailed runtime status report when an editor surface is available
126
- - `/activity-strip doctor` opens the host-compatibility report
127
-
128
- ## Interaction model
129
-
130
- - **Workspace locality:** one strip follows the Niri workspace selected with Up/Down and renders only tracked Pi terminals whose exact Ghostty windows are on that workspace. Focused-workspace events trigger reconciliation immediately, with polling retained as a fallback. When an empty workspace is visited, an aligned strip whose resident workspace still has tracked terminals remains rendered on that prior workspace; Niri keeps it off the empty workspace and returning brings the already-positioned strip back with its row. If its resident terminals disappear, the renderer is concealed and input-disabled. When an actual remap or floating correction is unavoidable, reveal waits beyond Niri's compositor movement animation and then re-verifies placement and membership. The broker remains global and non-Niri desktops retain the global card view.
131
- - **Ordering:** green `done` cards whose footer reads `monitoring` stay at the far left beside the Activity tile. Active tool/thinking/waiting cards follow, then other settled cards. The group order refreshes every 15 seconds rather than on every telemetry packet; text and timers still update live.
132
- - **Current terminal:** on Niri, the card matching the focused Ghostty window gets a stronger border and left rail without an extra label. Matching uses the same exact session-title identity seam as click-to-focus and fails closed when focus or identity is missing or ambiguous.
133
- - **Pointer:** hover expands the strip and reveals detail, last prompt, assistant preview, and path. Leaving the strip or activating another window collapses it immediately. Single click asks Niri to focus the one Ghostty title carrying that exact Pi session-id suffix.
134
- - **Keyboard inside the strip:** Left/Right changes card focus, Enter activates the focused card, and Shift+Left/Right manually moves it. Manual movement lasts until a later activity regroup or runtime restart.
135
- - **Fail-closed focus:** current telemetry uses the exact Pi identity directly. For already-running tabs that still publish a legacy broker id, the strip may recover only the process-bound `pi-session-presence` sidecar when its source, PID, and cwd all agree; this is not repo-name guessing or arbitrary-PID focusing. If identity still matches zero or multiple Ghostty windows, focus does nothing and asks for one `/reload`.
136
-
137
- ### Keyboard-only entry on Niri
138
-
139
- The package deliberately does not reserve a global Electron shortcut. Bind one compositor key to the fail-closed CLI entrypoint instead:
107
+ Bind a Niri key to the fail-closed CLI entrypoint:
140
108
 
141
109
  ```kdl
142
110
  binds {
143
- Mod+Shift+A { spawn "node" "/home/tryinget/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip/bin/pi-activity-strip.mjs" "focus-strip"; }
111
+ Mod+Shift+A repeat=false allow-inhibiting=false hotkey-overlay-title="Toggle Pi Activity Ribbon keyboard mode" { spawn "/usr/bin/node" "/home/tryinget/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip/bin/pi-activity-strip.mjs" "focus-strip"; }
144
112
  }
145
113
  ```
146
114
 
147
- `focus-strip` gives keyboard focus only to the unique strip already resident on the currently focused workspace; it never moves a strip between workspaces. When the focused workspace has no tracked live Pi terminals, the command fails closed rather than forcing an empty bar into view. This keeps shortcut ownership explicit in Niri and avoids application-level global-key collisions.
148
-
149
- ## Verification commands
150
-
151
- ### Package checks
152
-
153
- ```bash
154
- npm install
155
- npm run check
156
- npm run release:check:quick
157
- ```
158
-
159
- ### Run the strip locally
160
-
161
- ```bash
162
- npm run strip:open
163
- npm run strip:status
164
- npm run strip:doctor
165
- npm run strip:snapshot
166
- ```
115
+ 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.
116
+
117
+ ## Interaction contract
118
+
119
+ - **Workspace locality:** Niri focus events trigger immediate reprojection; bounded polling remains a fallback.
120
+ - **Hide/reclaim:** zero cards unmaps the layer surface. Niri then removes its exclusive zone as part of normal Wayland surface lifecycle.
121
+ - **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.
122
+ - **Exact focus:** card activation returns to Node, which performs existing fail-closed terminal identity resolution and Niri focus.
123
+ - **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.
124
+ - **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.
125
+ - **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. A card whose session holds a live claim shows a clickable `AK #id · title` 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.
126
+ - **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.
127
+ - **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.
128
+ - **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.
129
+ - **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.
130
+ - **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.
131
+ - **Ordering:** monitoring, active, and settled groups refresh on a calm 15-second clock. Manual moves survive until regroup or restart.
132
+ - **Accessibility:** cards expose native GTK labels, selected/expanded state, activation descriptions, and GTK accessible announcements.
167
133
 
168
- ### Capture what the agent should inspect
134
+ ## Environment controls
169
135
 
170
- ```bash
171
- npm run capture:strip # just the Pi activity strip window
172
- npm run capture:top # top band of the focused output, including the strip + upper window area
173
- ```
136
+ - `PI_ACTIVITY_STRIP_AUTO_START=0` disables extension autostart.
137
+ - `PI_ACTIVITY_STRIP_CLICK_THROUGH=1` installs an empty Wayland input region and disables keyboard entry.
138
+ - `PI_ACTIVITY_STRIP_NATIVE_PANEL_BIN=/absolute/path` selects another receipted panel artifact.
139
+ - `PI_ACTIVITY_STRIP_SOCKET_DIR` and `PI_ACTIVITY_STRIP_SOCKET_PATH` isolate broker fixtures and nested-compositor tests.
140
+ - `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.
141
+ - `PI_ACTIVITY_STRIP_AK_TASKS=0` disables AK task reference joining.
142
+ - `PI_ACTIVITY_STRIP_AK_BIN=/absolute/path` selects the `ak` binary used for the read-only task queries (default `ak` on `PATH`).
143
+ - `PI_ACTIVITY_STRIP_AGENT_TABS=0` disables discovery of non-Pi agent tabs.
144
+ - `PI_ACTIVITY_STRIP_AGENT_KINDS_DISABLED=claude,codex` excludes named agent kinds from discovery.
145
+ - `CODEX_HOME` selects a non-default Codex home when reading its thread index (default `~/.codex`).
146
+ - `PI_ACTIVITY_STRIP_PYTHON` selects the interpreter for the tab inventory (default `python3`).
147
+ - `PI_ACTIVITY_STRIP_HEIGHT_REPAIR=0` disables restoring windows stranded at the previous working-area height.
148
+ - `PI_ACTIVITY_STRIP_COLOR_SCHEME=light|dark` pins the ribbon to one scheme instead of following the desktop.
174
149
 
175
- These are specifically useful so the agent can inspect the current visual state without you manually posting screenshots.
150
+ Unverified native binaries are rejected unless `PI_ACTIVITY_STRIP_ALLOW_UNVERIFIED_PANEL=1` is explicitly set for development fixtures.
176
151
 
177
- ### Simulate multiple sessions
152
+ ## Verification
178
153
 
179
154
  ```bash
180
- npm run demo:simulate
155
+ npm run native:build # exact Rust 1.98 build, tests, staged artifact receipt
156
+ npm run check # canonical Node/package quality gate
157
+ npm run native:check # Rust formatting and tests
158
+ npm run release:check # full packed install and native-artifact smoke
181
159
  ```
182
160
 
183
- ### Real Pi smoke on the live broker
161
+ The staged artifact receipt binds:
184
162
 
185
- ```bash
186
- npm run smoke:headless-live
187
- ```
188
-
189
- This smoke:
190
- - opens the strip
191
- - runs a real headless Pi session with this extension loaded
192
- - exercises a real tool call
193
- - verifies that the broker observed the session while it was active
163
+ - binary SHA-256
164
+ - Cargo lock SHA-256
165
+ - complete Rust/CSS source SHA-256
166
+ - Rust compiler version
167
+ - glibc symbol floor
168
+ - required shared libraries
194
169
 
195
- ### Compatibility diagnostics
170
+ Live verification must inspect Niri layers rather than regular windows:
196
171
 
197
172
  ```bash
198
- npm run strip:doctor
199
- node ./bin/pi-activity-strip.mjs doctor --json
173
+ niri msg -j layers | jq '[.[] | select(.namespace == "pi-activity-strip")]'
200
174
  ```
201
175
 
202
- Use `doctor` before opening the strip when the host/display assumptions are uncertain. It reports:
203
- - whether a graphical session is present
204
- - whether Electron can be resolved
205
- - whether Niri-specific top-edge repair is available
206
- - whether the current setup is multi-display even though the strip remains primary-display-only
207
-
208
- ## Environment controls
209
-
210
- - `PI_ACTIVITY_STRIP_AUTO_START=0`
211
- - disable automatic strip opening on Pi session start
212
- - `PI_ACTIVITY_STRIP_CLICK_THROUGH=1`
213
- - opt out of interaction and restore a mouse-transparent overlay; interactive hover/click/keyboard behavior is the default
214
- - `PI_ACTIVITY_STRIP_ELECTRON_BIN=/path/to/electron`
215
- - override Electron binary discovery
216
- - `GLIMPSE_ELECTRON_BIN=/path/to/electron`
217
- - shared Electron override also respected
218
-
219
- ## Practical usage for your Ghostty tabs
220
-
221
- If you want this for all current tabs:
176
+ ## Current scope
222
177
 
223
- 1. install the package once with `pi install`
224
- 2. run `/reload` inside each already-open Pi tab
225
- 3. open the strip once with `/activity-strip` or `npm run strip:open`
226
- 4. from then on, every loaded Pi session should report into the same top-row ribbon
227
- 5. ensure `pi-little-helpers` session presence is loaded when you want exact click-to-Ghostty focus; its `· <full-32-hex-session-id-token>` title suffix is the preferred fail-closed identity seam (an 8-hex legacy title remains usable only when no legacy duplicate or migrated full title shares its prefix)
178
+ Implemented:
179
+
180
+ - local broker and telemetry publishers
181
+ - native GTK4 layer-shell rendering
182
+ - workspace-local Niri projection
183
+ - hide/reclaim and restore
184
+ - pointer and keyboard card interaction
185
+ - exact Ghostty activation, including hidden tabs via `present-surface`
186
+ - hidden Ghostty tab placement through host process containment and learned window memory
187
+ - clickable AK task references joined from read-only `ak` output, with claim-outlives-session and deferred badges
188
+ - non-Pi agent tab discovery with an exact Claude Code adapter
189
+ - live Claude Code telemetry from its transcript, with optional hooks for blocked-on-you states
190
+ - live Codex telemetry from its thread index and rollout files
191
+ - Ghostty theme following, light and dark
192
+ - repair of windows stranded at the previous working-area height
193
+ - bounded panel restart and parent-death cleanup
194
+ - click-through input region
195
+
196
+ Not implemented:
197
+
198
+ - placement of a tab whose window has never been observed, on a host without the AT-SPI inventory (Ghostty exposes no surface listing of its own)
199
+ - distinguishing two terminals that resume one logical session when only the drifted session token is available; both are placed in the one window that claims that token
200
+ - live phase and tool detail for agents other than Claude Code and Codex, which have no adapter yet; their cards show the agent, directory, elapsed time and pid
201
+ - placement of an agent other than Claude Code whose tab is hidden inside a multi-window Ghostty process, since only Claude Code exposes a per-tab title identity
202
+ - one panel per output
203
+ - historical timeline
204
+ - any AK mutation from the strip (unclaim, land, reconstruct, apply); the ribbon is a read-only projection by construction
205
+ - persisted manual ordering
206
+ - remote observers via `pi-server`
228
207
 
229
208
  ## References
230
209
 
231
210
  - [Project vision](docs/project/vision.md)
232
211
  - [Project resources](docs/project/resources.md)
233
- - [Verification notes](docs/project/verification.md)
234
- - [Next session prompt](next_session_prompt.md)
212
+ - [Verification evidence](docs/project/verification.md)
213
+ - [Superseded adaptive-strut design investigation](docs/project/2026-08-31-adaptive-niri-space-design.md)
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // ---
3
+ // summary: "Claude Code hook entrypoint that records one session's live state for the activity ribbon"
4
+ // read_when:
5
+ // - "changing how Claude Code sessions publish live state to the ribbon"
6
+ // ---
7
+
8
+ // Claude Code runs this for each configured hook event and waits for it, so it must be cheap and
9
+ // must never fail the session: every path exits 0 and nothing is written to stdout.
10
+
11
+ import fs from "node:fs";
12
+ import {
13
+ CLAUDE_EVENT_DIR,
14
+ claudeEventPath,
15
+ claudeEventRecord,
16
+ isSessionEndEvent,
17
+ } from "../src/common/claude-events.mjs";
18
+
19
+ const STDIN_LIMIT_BYTES = 1024 * 1024;
20
+
21
+ async function readPayload() {
22
+ const chunks = [];
23
+ let total = 0;
24
+ for await (const chunk of process.stdin) {
25
+ total += chunk.length;
26
+ if (total > STDIN_LIMIT_BYTES) break;
27
+ chunks.push(chunk);
28
+ }
29
+ try {
30
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ async function main() {
37
+ const payload = await readPayload();
38
+ if (!payload || typeof payload !== "object") return;
39
+ const record = claudeEventRecord(payload, { env: process.env });
40
+ if (!record) return;
41
+ const filePath = claudeEventPath(record.sessionId);
42
+ if (!filePath) return;
43
+
44
+ if (isSessionEndEvent(record)) {
45
+ try {
46
+ fs.unlinkSync(filePath);
47
+ } catch {
48
+ // The session never published, or another hook already retired it.
49
+ }
50
+ return;
51
+ }
52
+
53
+ const temporaryPath = `${filePath}.tmp.${process.pid}`;
54
+ try {
55
+ fs.mkdirSync(CLAUDE_EVENT_DIR, { recursive: true, mode: 0o700 });
56
+ fs.writeFileSync(temporaryPath, JSON.stringify(record), { mode: 0o600 });
57
+ fs.renameSync(temporaryPath, filePath);
58
+ } catch {
59
+ try {
60
+ fs.unlinkSync(temporaryPath);
61
+ } catch {
62
+ // Nothing was staged.
63
+ }
64
+ }
65
+ }
66
+
67
+ main()
68
+ .catch(() => {})
69
+ .finally(() => {
70
+ process.exitCode = 0;
71
+ });
@@ -5,11 +5,11 @@
5
5
  // - "operating or diagnosing the activity strip from a terminal"
6
6
  // ---
7
7
 
8
- import { execFile, spawn } from "node:child_process";
8
+ import { spawn } from "node:child_process";
9
+ import fs from "node:fs";
9
10
  import path from "node:path";
10
11
  import { setTimeout as delay } from "node:timers/promises";
11
12
  import { fileURLToPath } from "node:url";
12
- import { promisify } from "node:util";
13
13
  import {
14
14
  getBrokerStatus,
15
15
  isBrokerAlive,
@@ -20,63 +20,24 @@ import {
20
20
  assessActivityStripCompatibility,
21
21
  formatCompatibilityReport,
22
22
  } from "../src/common/compatibility.mjs";
23
- import { ACTIVITY_STRIP_START_TIMEOUT_MS } from "../src/common/constants.mjs";
24
- import { locateElectron } from "../src/common/electron.mjs";
25
- import { focusNiriStrip, resolveActivityStripWindow } from "../src/common/niri-focus.mjs";
23
+ import {
24
+ ACTIVITY_STRIP_SOCKET_DIR,
25
+ ACTIVITY_STRIP_START_TIMEOUT_MS,
26
+ } from "../src/common/constants.mjs";
26
27
  import { makeMessage } from "../src/common/protocol.mjs";
27
28
  import { formatBrokerRuntimeStatus } from "../src/common/status-report.mjs";
28
29
 
29
30
  const __filename = fileURLToPath(import.meta.url);
30
31
  const __dirname = path.dirname(__filename);
31
- const electronEntry = path.resolve(__dirname, "..", "src", "electron", "main.mjs");
32
- const execFileAsync = promisify(execFile);
32
+ const nativeEntry = path.resolve(__dirname, "..", "src", "native", "main.mjs");
33
+ const runtimeLockPath = path.join(ACTIVITY_STRIP_SOCKET_DIR, "runtime.lock");
33
34
 
34
35
  function usage() {
35
36
  console.log(
36
- `Usage: pi-activity-strip <open|focus-strip|focus-session|status|doctor|snapshot|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 fix-top Move the strip window flush to the top edge in Niri\n stop Ask the running strip to shut down\n serve Internal helper; starts the Electron shell in the foreground\n`,
37
+ `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`,
37
38
  );
38
39
  }
39
40
 
40
- async function moveStripToTop() {
41
- const { stdout } = await execFileAsync("niri", ["msg", "-j", "windows"], {
42
- env: process.env,
43
- });
44
- const windows = JSON.parse(stdout);
45
- if (!Array.isArray(windows)) {
46
- throw new Error("Unexpected niri windows payload");
47
- }
48
-
49
- const stripWindow = resolveActivityStripWindow(windows);
50
- if (!stripWindow) {
51
- throw new Error("Could not find one unique Pi Activity Strip window in niri");
52
- }
53
-
54
- const layout = /** @type {{ tile_pos_in_workspace_view?: unknown[] }} */ (
55
- stripWindow.layout ?? {}
56
- );
57
- const currentY = Number(layout.tile_pos_in_workspace_view?.[1] ?? 0);
58
- if (Math.abs(currentY) < 1) {
59
- return 0;
60
- }
61
-
62
- await execFileAsync(
63
- "niri",
64
- [
65
- "msg",
66
- "action",
67
- "move-floating-window",
68
- "--id",
69
- String(stripWindow.id),
70
- "-y",
71
- String(-Math.round(currentY)),
72
- ],
73
- { env: process.env },
74
- );
75
-
76
- console.log("Moved activity strip to the top edge.");
77
- return 0;
78
- }
79
-
80
41
  async function waitForBrokerReady(timeoutMs = ACTIVITY_STRIP_START_TIMEOUT_MS) {
81
42
  const timeoutAt = Date.now() + timeoutMs;
82
43
  /** @type {import("../src/common/contracts.ts").BrokerResponse | null} */
@@ -111,8 +72,19 @@ async function waitForBrokerReady(timeoutMs = ACTIVITY_STRIP_START_TIMEOUT_MS) {
111
72
  /** @param {{ detached?: boolean }} [options] @returns {Promise<number>} */
112
73
  async function openStrip({ detached = true } = {}) {
113
74
  if (await isBrokerAlive()) {
114
- console.log("Activity strip is already running.");
115
- return 0;
75
+ const status = await getBrokerStatus({ expectReply: true }).catch(() => null);
76
+ if (status?.runtimeStatus?.state !== "error") {
77
+ console.log("Activity strip is already running.");
78
+ return 0;
79
+ }
80
+ await requestBrokerShutdown().catch(() => null);
81
+ for (let attempt = 0; attempt < 20 && (await isBrokerAlive()); attempt += 1) {
82
+ await delay(50);
83
+ }
84
+ if (await isBrokerAlive()) {
85
+ console.error(formatBrokerRuntimeStatus(status));
86
+ return 1;
87
+ }
116
88
  }
117
89
 
118
90
  const compatibility = await assessActivityStripCompatibility();
@@ -121,11 +93,11 @@ async function openStrip({ detached = true } = {}) {
121
93
  return 1;
122
94
  }
123
95
 
124
- const electron = await locateElectron();
125
- const child = spawn(electron, [electronEntry], {
96
+ fs.mkdirSync(ACTIVITY_STRIP_SOCKET_DIR, { recursive: true, mode: 0o700 });
97
+ const child = spawn("flock", ["--nonblock", runtimeLockPath, process.execPath, nativeEntry], {
126
98
  detached,
127
99
  stdio: detached ? "ignore" : "inherit",
128
- env: process.env,
100
+ env: { ...process.env, PI_ACTIVITY_STRIP_RUNTIME_LOCK_HELD: "1" },
129
101
  });
130
102
 
131
103
  if (detached) {
@@ -166,7 +138,9 @@ async function main() {
166
138
  process.exitCode = 1;
167
139
  return;
168
140
  }
169
- const result = await focusNiriStrip(execFileAsync, process.env, status?.snapshot?.sessions);
141
+ const result = await sendBrokerMessage(makeMessage("focus-strip"), {
142
+ expectReply: true,
143
+ });
170
144
  if (!result.ok) console.error(result.error || "Strip focus did nothing.");
171
145
  process.exitCode = result.ok ? 0 : 1;
172
146
  } catch {
@@ -234,15 +208,31 @@ async function main() {
234
208
  }
235
209
  return;
236
210
  }
237
- case "fix-top": {
238
- try {
239
- process.exitCode = await moveStripToTop();
240
- } catch (error) {
241
- console.error(error instanceof Error ? error.message : String(error));
242
- process.exitCode = 1;
211
+ case "claude-hooks": {
212
+ const { claudeHookSettings } = await import("../src/common/claude-hook-config.mjs");
213
+ const hookPath = path.join(
214
+ path.dirname(fileURLToPath(import.meta.url)),
215
+ "pi-activity-strip-claude-hook.mjs",
216
+ );
217
+ const settings = claudeHookSettings(`${process.execPath} ${hookPath}`);
218
+ if (jsonOutput) {
219
+ console.log(JSON.stringify(settings, null, 2));
220
+ } else {
221
+ console.log(
222
+ "Merge this into ~/.claude/settings.json so Claude Code sessions report live state:\n",
223
+ );
224
+ console.log(JSON.stringify(settings, null, 2));
225
+ console.log(
226
+ "\nOnly low-frequency events are hooked, so nothing runs per tool call. Tool and thinking",
227
+ );
228
+ console.log("state already come from the transcript without any configuration.");
243
229
  }
244
230
  return;
245
231
  }
232
+ case "fix-top":
233
+ console.log("Native layer-shell placement is compositor-owned; no repair was needed.");
234
+ process.exitCode = 0;
235
+ return;
246
236
  case "stop": {
247
237
  try {
248
238
  const result = await requestBrokerShutdown();
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": "pi-activity-strip-native-artifact.v1",
3
+ "target": "x86_64-unknown-linux-gnu",
4
+ "sha256": "999724e6bb34a0cceea631d1cbe58541f20221291b3e9d065e9bb19e193a3b27",
5
+ "cargoLockSha256": "f8ebc11bc721c569af982169e448a5ea4fed15bc06001017d97acda22b1c890b",
6
+ "sourceSha256": "1fa65ddf13e1a3c1267081de641e65617822fd409c88e865fec6f769b8161a35",
7
+ "rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
8
+ "glibcFloor": "GLIBC_2.34",
9
+ "neededSonames": [
10
+ "libgtk4-layer-shell.so.0",
11
+ "libgtk-4.so.1",
12
+ "libcairo.so.2",
13
+ "libgio-2.0.so.0",
14
+ "libgobject-2.0.so.0",
15
+ "libglib-2.0.so.0",
16
+ "libgcc_s.so.1",
17
+ "libm.so.6",
18
+ "libc.so.6",
19
+ "ld-linux-x86-64.so.2"
20
+ ]
21
+ }