@tryinget/pi-activity-strip 0.4.0 → 0.5.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 (40) hide show
  1. package/README.md +96 -152
  2. package/bin/pi-activity-strip.mjs +31 -62
  3. package/native/bin/linux-x64-gnu/artifact.json +21 -0
  4. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  5. package/native/panel/Cargo.lock +1045 -0
  6. package/native/panel/Cargo.toml +23 -0
  7. package/native/panel/src/app.rs +606 -0
  8. package/native/panel/src/card_view.rs +303 -0
  9. package/native/panel/src/main.rs +31 -0
  10. package/native/panel/src/protocol.rs +234 -0
  11. package/native/panel/src/runtime.rs +110 -0
  12. package/native/panel/src/style.css +176 -0
  13. package/package.json +13 -1
  14. package/src/broker/server.mjs +43 -7
  15. package/src/broker/session-store.mjs +41 -13
  16. package/src/client/broker-client.mjs +10 -3
  17. package/src/client/session-telemetry.mjs +54 -14
  18. package/src/common/activity-order.mjs +15 -9
  19. package/src/common/alignment-controller.mjs +24 -42
  20. package/src/common/card-display.mjs +5 -0
  21. package/src/common/compatibility.mjs +15 -13
  22. package/src/common/constants.mjs +10 -28
  23. package/src/common/contracts.d.ts +42 -4
  24. package/src/common/contracts.ts +42 -4
  25. package/src/common/niri-focus.mjs +79 -79
  26. package/src/common/protocol.mjs +48 -16
  27. package/src/common/session-cards.mjs +102 -0
  28. package/src/common/status-report.mjs +2 -1
  29. package/src/common/telemetry.mjs +8 -1
  30. package/src/common/terminal-identity.mjs +149 -0
  31. package/src/native/main.mjs +393 -0
  32. package/src/native/panel-projection.mjs +99 -0
  33. package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +4 -1
  34. package/src/common/electron.mjs +0 -57
  35. package/src/electron/main.mjs +0 -499
  36. package/src/electron/niri-native-window-runtime.mjs +0 -178
  37. package/src/electron/preload.cjs +0 -50
  38. package/src/electron/renderer-visibility-runtime.mjs +0 -107
  39. package/src/electron/workspace-view-runtime.mjs +0 -360
  40. package/src/ui/strip-html.mjs +0 -500
package/README.md CHANGED
@@ -1,91 +1,77 @@
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 live Pi sessions running in Ghostty.
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
23
+ - aggregates independent publishers beneath stable terminal cards
24
+ - displays repo, phase, tool, detail, elapsed time, and freshness
25
+ - marks the exact currently focused terminal card
26
+ - keeps monitoring-success cards beside the Activity tile, then active and settled cards
27
+ - expands rich details on hover or keyboard focus
28
+ - supports Left/Right navigation and Shift+Left/Right manual movement
29
+ - focuses the exact matching Ghostty window on click or Enter
30
+ - hides completely on workspaces without tracked cards
31
+ - releases its 84px exclusive zone automatically when hidden or crashed
41
32
 
42
33
  ## Architecture
43
34
 
44
35
  ```text
45
- Pi session
46
- -> activity-strip extension
47
- -> local unix-socket broker
48
- -> Electron top-row overlay
36
+ Pi publisher streams
37
+ -> Node Unix-socket broker and session store
38
+ -> exact terminal identity + Niri workspace projection
39
+ -> versioned NDJSON child protocol
40
+ -> Rust / Relm4 / GTK4 panel
41
+ -> wlr-layer-shell top surface with an 84px exclusive zone
49
42
  ```
50
43
 
51
- This is intentionally **local-first**.
52
- It does not require moving your workflow onto `pi-server` first.
44
+ 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
45
 
54
- ## Current scope
46
+ The compact surface is 84px tall. One engaged card expands the surface to 252px while the exclusive zone remains 84px, so detail overlays content without repeatedly resizing tiled windows.
55
47
 
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`
48
+ ## Supported host
70
49
 
71
- ## Installation in Pi
50
+ The packaged native artifact currently supports:
72
51
 
73
- From this package directory:
52
+ - Linux x86_64
53
+ - Wayland
54
+ - a compositor implementing `wlr-layer-shell` (dogfooded on Niri 26.04)
55
+ - GTK4 and gtk4-layer-shell runtime libraries
56
+
57
+ On Arch Linux:
74
58
 
75
59
  ```bash
76
- cd ~/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip
77
- pi install "$PWD"
60
+ sudo pacman -S gtk4 gtk4-layer-shell
78
61
  ```
79
62
 
80
- Then for **existing Pi tabs**:
81
- - run `/reload` in each tab you want tracked
63
+ Multi-output replication remains unimplemented. The current surface is single-output and must not be described as multi-monitor complete.
82
64
 
83
- For **new Pi tabs**:
84
- - the package will load automatically from your Pi settings
65
+ ## Installation
66
+
67
+ ```bash
68
+ cd ~/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip
69
+ pi install "$PWD"
70
+ ```
85
71
 
86
- ## Operator commands
72
+ Reload already-running Pi tabs with `/reload`. New tabs load the package automatically.
87
73
 
88
- ### Package-local CLI
74
+ ## Commands
89
75
 
90
76
  ```bash
91
77
  npm run strip:open
@@ -96,7 +82,7 @@ npm run strip:fix-top
96
82
  npm run strip:stop
97
83
  ```
98
84
 
99
- or directly:
85
+ Direct CLI:
100
86
 
101
87
  ```bash
102
88
  node ./bin/pi-activity-strip.mjs open
@@ -105,130 +91,88 @@ node ./bin/pi-activity-strip.mjs focus-session <full-pi-session-id>
105
91
  node ./bin/pi-activity-strip.mjs status
106
92
  node ./bin/pi-activity-strip.mjs doctor
107
93
  node ./bin/pi-activity-strip.mjs snapshot
108
- node ./bin/pi-activity-strip.mjs fix-top
94
+ node ./bin/pi-activity-strip.mjs stop
109
95
  ```
110
96
 
111
- ### Pi slash commands
97
+ `fix-top` is now a compatibility no-op: layer-shell placement is compositor-owned.
112
98
 
113
- Inside Pi:
99
+ ## Keyboard-only entry
114
100
 
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:
101
+ Bind a Niri key to the fail-closed CLI entrypoint:
140
102
 
141
103
  ```kdl
142
104
  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"; }
105
+ 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
106
  }
145
107
  ```
146
108
 
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
109
+ 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.
150
110
 
151
- ### Package checks
111
+ ## Interaction contract
152
112
 
153
- ```bash
154
- npm install
155
- npm run check
156
- npm run release:check:quick
157
- ```
113
+ - **Workspace locality:** Niri focus events trigger immediate reprojection; bounded polling remains a fallback.
114
+ - **Hide/reclaim:** zero cards unmaps the layer surface. Niri then removes its exclusive zone as part of normal Wayland surface lifecycle.
115
+ - **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.
116
+ - **Exact focus:** card activation returns to Node, which performs existing fail-closed terminal identity resolution and Niri focus.
117
+ - **Ordering:** monitoring, active, and settled groups refresh on a calm 15-second clock. Manual moves survive until regroup or restart.
118
+ - **Accessibility:** cards expose native GTK labels, selected/expanded state, activation descriptions, and GTK accessible announcements.
158
119
 
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
- ```
167
-
168
- ### Capture what the agent should inspect
120
+ ## Environment controls
169
121
 
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
- ```
122
+ - `PI_ACTIVITY_STRIP_AUTO_START=0` disables extension autostart.
123
+ - `PI_ACTIVITY_STRIP_CLICK_THROUGH=1` installs an empty Wayland input region and disables keyboard entry.
124
+ - `PI_ACTIVITY_STRIP_NATIVE_PANEL_BIN=/absolute/path` selects another receipted panel artifact.
125
+ - `PI_ACTIVITY_STRIP_SOCKET_DIR` and `PI_ACTIVITY_STRIP_SOCKET_PATH` isolate broker fixtures and nested-compositor tests.
174
126
 
175
- These are specifically useful so the agent can inspect the current visual state without you manually posting screenshots.
127
+ Unverified native binaries are rejected unless `PI_ACTIVITY_STRIP_ALLOW_UNVERIFIED_PANEL=1` is explicitly set for development fixtures.
176
128
 
177
- ### Simulate multiple sessions
129
+ ## Verification
178
130
 
179
131
  ```bash
180
- npm run demo:simulate
132
+ npm run native:build # exact Rust 1.98 build, tests, staged artifact receipt
133
+ npm run check # canonical Node/package quality gate
134
+ npm run native:check # Rust formatting and tests
135
+ npm run release:check # full packed install and native-artifact smoke
181
136
  ```
182
137
 
183
- ### Real Pi smoke on the live broker
138
+ The staged artifact receipt binds:
184
139
 
185
- ```bash
186
- npm run smoke:headless-live
187
- ```
140
+ - binary SHA-256
141
+ - Cargo lock SHA-256
142
+ - complete Rust/CSS source SHA-256
143
+ - Rust compiler version
144
+ - glibc symbol floor
145
+ - required shared libraries
188
146
 
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
194
-
195
- ### Compatibility diagnostics
147
+ Live verification must inspect Niri layers rather than regular windows:
196
148
 
197
149
  ```bash
198
- npm run strip:doctor
199
- node ./bin/pi-activity-strip.mjs doctor --json
150
+ niri msg -j layers | jq '[.[] | select(.namespace == "pi-activity-strip")]'
200
151
  ```
201
152
 
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
153
+ ## Current scope
209
154
 
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
155
+ Implemented:
218
156
 
219
- ## Practical usage for your Ghostty tabs
157
+ - local broker and telemetry publishers
158
+ - native GTK4 layer-shell rendering
159
+ - workspace-local Niri projection
160
+ - hide/reclaim and restore
161
+ - pointer and keyboard card interaction
162
+ - exact Ghostty activation
163
+ - bounded panel restart and parent-death cleanup
164
+ - click-through input region
220
165
 
221
- If you want this for all current tabs:
166
+ Not implemented:
222
167
 
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)
168
+ - one panel per output
169
+ - historical timeline
170
+ - persisted manual ordering
171
+ - remote observers via `pi-server`
228
172
 
229
173
  ## References
230
174
 
231
175
  - [Project vision](docs/project/vision.md)
232
176
  - [Project resources](docs/project/resources.md)
233
- - [Verification notes](docs/project/verification.md)
234
- - [Next session prompt](next_session_prompt.md)
177
+ - [Verification evidence](docs/project/verification.md)
178
+ - [Superseded adaptive-strut design investigation](docs/project/2026-08-31-adaptive-niri-space-design.md)
@@ -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,61 +20,22 @@ 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
- );
38
- }
39
-
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 ?? {}
37
+ `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 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`,
56
38
  );
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
39
  }
79
40
 
80
41
  async function waitForBrokerReady(timeoutMs = ACTIVITY_STRIP_START_TIMEOUT_MS) {
@@ -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,10 @@ 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;
243
- }
211
+ case "fix-top":
212
+ console.log("Native layer-shell placement is compositor-owned; no repair was needed.");
213
+ process.exitCode = 0;
244
214
  return;
245
- }
246
215
  case "stop": {
247
216
  try {
248
217
  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": "4cc72663dda90369a3a3ad60aabeefc4a609f42c6cc4a4661d2dc052570a3f0a",
5
+ "cargoLockSha256": "f8ebc11bc721c569af982169e448a5ea4fed15bc06001017d97acda22b1c890b",
6
+ "sourceSha256": "9467e315aa20fef76c25b88d29e69f3ef6960481b55b639c602e77805986f0e1",
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
+ }