@tryinget/pi-activity-strip 0.3.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 (42) hide show
  1. package/README.md +96 -152
  2. package/bin/pi-activity-strip.mjs +31 -62
  3. package/examples/simulate-sessions.mjs +1 -1
  4. package/extensions/activity-strip.js +2 -2
  5. package/native/bin/linux-x64-gnu/artifact.json +21 -0
  6. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  7. package/native/panel/Cargo.lock +1045 -0
  8. package/native/panel/Cargo.toml +23 -0
  9. package/native/panel/src/app.rs +606 -0
  10. package/native/panel/src/card_view.rs +303 -0
  11. package/native/panel/src/main.rs +31 -0
  12. package/native/panel/src/protocol.rs +234 -0
  13. package/native/panel/src/runtime.rs +110 -0
  14. package/native/panel/src/style.css +176 -0
  15. package/package.json +20 -7
  16. package/src/broker/server.mjs +43 -7
  17. package/src/broker/session-store.mjs +53 -11
  18. package/src/client/broker-client.mjs +25 -4
  19. package/src/client/session-telemetry.mjs +123 -13
  20. package/src/common/activity-order.mjs +15 -9
  21. package/src/common/alignment-controller.mjs +24 -42
  22. package/src/common/card-display.mjs +53 -0
  23. package/src/common/compatibility.mjs +15 -13
  24. package/src/common/constants.mjs +12 -28
  25. package/src/common/contracts.d.ts +54 -5
  26. package/src/common/contracts.ts +54 -5
  27. package/src/common/niri-focus.mjs +79 -79
  28. package/src/common/protocol.mjs +49 -14
  29. package/src/common/session-cards.mjs +102 -0
  30. package/src/common/status-report.mjs +2 -1
  31. package/src/common/telemetry.mjs +11 -1
  32. package/src/common/terminal-identity.mjs +149 -0
  33. package/src/native/main.mjs +393 -0
  34. package/src/native/panel-projection.mjs +99 -0
  35. package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +4 -1
  36. package/src/common/electron.mjs +0 -57
  37. package/src/electron/main.mjs +0 -499
  38. package/src/electron/niri-native-window-runtime.mjs +0 -178
  39. package/src/electron/preload.cjs +0 -50
  40. package/src/electron/renderer-visibility-runtime.mjs +0 -107
  41. package/src/electron/workspace-view-runtime.mjs +0 -360
  42. package/src/ui/strip-html.mjs +0 -484
@@ -5,6 +5,7 @@
5
5
  // ---
6
6
 
7
7
  /** @typedef {import("../common/contracts.ts").SessionSnapshot} SessionSnapshot */
8
+ /** @typedef {import("../common/contracts.ts").TurnEndEventLike} TurnEndEventLike */
8
9
  /** @typedef {import("../common/contracts.ts").SessionTelemetryOptions} SessionTelemetryOptions */
9
10
  /** @typedef {import("../common/contracts.ts").SessionStartContextLike} SessionStartContextLike */
10
11
  /** @typedef {import("../common/contracts.ts").BeforeAgentStartEventLike} BeforeAgentStartEventLike */
@@ -12,6 +13,7 @@
12
13
  /** @typedef {import("../common/contracts.ts").MessageUpdateEventLike} MessageUpdateEventLike */
13
14
  /** @typedef {import("../common/contracts.ts").ToolExecutionEventLike} ToolExecutionEventLike */
14
15
  import {
16
+ ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS,
15
17
  ACTIVITY_STRIP_HEARTBEAT_MS,
16
18
  ACTIVITY_STRIP_SEND_THROTTLE_MS,
17
19
  } from "../common/constants.mjs";
@@ -28,6 +30,7 @@ import {
28
30
  extractToolResultText,
29
31
  summarizeToolResult,
30
32
  } from "../common/telemetry.mjs";
33
+ import { resolveTerminalIdentity as resolveTerminalIdentityDefault } from "../common/terminal-identity.mjs";
31
34
  import { publishSessionSnapshot, removeSession } from "./broker-client.mjs";
32
35
 
33
36
  function now() {
@@ -41,7 +44,17 @@ function extractAssistantDelta(event) {
41
44
  }
42
45
 
43
46
  /** @param {SessionTelemetryOptions} [options] */
44
- export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName = "" } = {}) {
47
+ export function createSessionTelemetry({
48
+ pi,
49
+ cwd = process.cwd(),
50
+ sessionName = "",
51
+ env = process.env,
52
+ stdinIsTTY = Boolean(process.stdin.isTTY),
53
+ resolveTerminalIdentity = resolveTerminalIdentityDefault,
54
+ transport,
55
+ } = {}) {
56
+ const publish = transport?.publish ?? publishSessionSnapshot;
57
+ const removePublisher = transport?.remove ?? removeSession;
45
58
  /** @type {SessionSnapshot} */
46
59
  let snapshot = createInitialSnapshot({ cwd, sessionName });
47
60
  /** @type {NodeJS.Timeout | null} */
@@ -50,29 +63,88 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
50
63
  let flushTimer = null;
51
64
  let disposed = false;
52
65
  let pendingAssistant = "";
66
+ let lastStopReason = "";
67
+ let confirmedSignature = "";
68
+ let retryIndex = 0;
69
+ /** @type {Promise<void> | null} */
70
+ let flushPromise = null;
71
+ let flushAgain = false;
53
72
 
54
- async function flush() {
73
+ /** Signature of the display-relevant fields, used to detect lost transitions. */
74
+ function publishSignature() {
75
+ return [
76
+ snapshot.sessionId,
77
+ snapshot.state,
78
+ snapshot.phase,
79
+ snapshot.detail,
80
+ snapshot.agentActive,
81
+ snapshot.turnIndex,
82
+ snapshot.toolName,
83
+ snapshot.toolTarget,
84
+ snapshot.errorMessage,
85
+ snapshot.terminalKey,
86
+ ].join("\u001f");
87
+ }
88
+
89
+ function clearScheduledFlush() {
90
+ if (!flushTimer) return;
91
+ clearTimeout(flushTimer);
55
92
  flushTimer = null;
56
- if (disposed) return;
93
+ }
94
+
95
+ async function publishLatest() {
96
+ // Heartbeats refresh transport liveness only; lastEventAt remains the last real event.
57
97
  snapshot.updatedAt = now();
98
+ snapshot.publisherSequence += 1;
58
99
  snapshot.repoLabel = formatRepoLabel(
59
100
  snapshot.cwd,
60
101
  pi?.getSessionName?.() ?? snapshot.sessionName,
61
102
  );
62
103
  snapshot.sessionName = compactWhitespace(pi?.getSessionName?.() ?? snapshot.sessionName);
104
+ const signature = publishSignature();
63
105
  try {
64
- await publishSessionSnapshot(snapshot);
106
+ await publish({ ...snapshot });
107
+ confirmedSignature = signature;
108
+ retryIndex = 0;
65
109
  } catch {
66
- // Broker is optional at runtime; silent failure keeps pi stable.
110
+ if (
111
+ !disposed &&
112
+ signature !== confirmedSignature &&
113
+ retryIndex < ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS.length
114
+ ) {
115
+ const delayMs = ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS[retryIndex];
116
+ retryIndex += 1;
117
+ scheduleFlush(delayMs);
118
+ }
67
119
  }
68
120
  }
69
121
 
70
- function scheduleFlush(delayMs = ACTIVITY_STRIP_SEND_THROTTLE_MS) {
122
+ async function flush() {
123
+ clearScheduledFlush();
71
124
  if (disposed) return;
72
- if (flushTimer) return;
125
+ if (flushPromise) {
126
+ flushAgain = true;
127
+ return flushPromise;
128
+ }
129
+ flushPromise = (async () => {
130
+ do {
131
+ flushAgain = false;
132
+ if (disposed) break;
133
+ await publishLatest();
134
+ } while (flushAgain && !disposed);
135
+ })().finally(() => {
136
+ flushPromise = null;
137
+ });
138
+ return flushPromise;
139
+ }
140
+
141
+ function scheduleFlush(delayMs = ACTIVITY_STRIP_SEND_THROTTLE_MS) {
142
+ if (disposed || flushTimer) return;
73
143
  flushTimer = setTimeout(() => {
144
+ flushTimer = null;
74
145
  flush().catch(() => {});
75
146
  }, delayMs);
147
+ flushTimer.unref?.();
76
148
  }
77
149
 
78
150
  function startHeartbeat() {
@@ -95,6 +167,7 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
95
167
  ...snapshot,
96
168
  ...partial,
97
169
  updatedAt: now(),
170
+ lastEventAt: now(),
98
171
  };
99
172
  scheduleFlush();
100
173
  }
@@ -107,8 +180,15 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
107
180
  async onSessionStart(ctx) {
108
181
  const exactSessionId = String(ctx?.sessionManager?.getSessionId?.() ?? "").trim();
109
182
  const nextCwd = ctx?.cwd ?? snapshot.cwd;
183
+ const terminalIdentity = resolveTerminalIdentity({
184
+ env,
185
+ hasUI: Boolean(ctx?.hasUI),
186
+ stdinIsTTY,
187
+ processId: process.pid,
188
+ });
110
189
  update({
111
190
  ...(exactSessionId ? { sessionId: exactSessionId } : {}),
191
+ ...terminalIdentity,
112
192
  cwd: nextCwd,
113
193
  sessionName: pi?.getSessionName?.() ?? snapshot.sessionName,
114
194
  detail: previewPath(nextCwd, 72) || "Ready",
@@ -185,8 +265,32 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
185
265
  toolName,
186
266
  });
187
267
  },
188
- onTurnEnd() {
268
+ /** @param {TurnEndEventLike} event */
269
+ onTurnEnd(event) {
189
270
  if (!snapshot.agentActive) return;
271
+ const rawMessage = event?.message;
272
+ const message =
273
+ rawMessage && typeof rawMessage === "object"
274
+ ? /** @type {Record<string, unknown>} */ (rawMessage)
275
+ : null;
276
+ const stopReason = String(message?.stopReason ?? "");
277
+ if (stopReason) lastStopReason = stopReason;
278
+ if (stopReason === "error") {
279
+ const errorText = previewText(message?.errorMessage, 104) || "Provider error";
280
+ update({
281
+ state: "error",
282
+ phase: "Needs attention",
283
+ detail: errorText,
284
+ errorMessage: errorText,
285
+ });
286
+ return;
287
+ }
288
+ if (stopReason === "aborted") {
289
+ update({
290
+ detail: snapshot.assistantPreview || snapshot.detail || "Stopped",
291
+ });
292
+ return;
293
+ }
190
294
  update({
191
295
  state: snapshot.errorMessage ? "error" : "thinking",
192
296
  phase: snapshot.errorMessage ? "Needs attention" : "Thinking",
@@ -196,11 +300,12 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
196
300
  onAgentSettled() {
197
301
  const detail =
198
302
  snapshot.errorMessage || snapshot.assistantPreview || snapshot.lastPromptPreview || "Done";
303
+ const aborted = !snapshot.errorMessage && lastStopReason === "aborted";
199
304
  update({
200
305
  agentActive: false,
201
306
  agentStartedAt: null,
202
307
  state: snapshot.errorMessage ? "error" : "success",
203
- phase: snapshot.errorMessage ? "Stopped" : "Done",
308
+ phase: snapshot.errorMessage ? "Stopped" : aborted ? "Stopped" : "Done",
204
309
  detail,
205
310
  toolName: "",
206
311
  toolTarget: "",
@@ -209,12 +314,17 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
209
314
  async shutdown() {
210
315
  disposed = true;
211
316
  stopHeartbeat();
212
- if (flushTimer) {
213
- clearTimeout(flushTimer);
214
- flushTimer = null;
317
+ clearScheduledFlush();
318
+ try {
319
+ await flushPromise;
320
+ } catch {
321
+ // Broker absence remains non-fatal; publisher removal is still attempted below.
215
322
  }
216
323
  try {
217
- await removeSession(snapshot.sessionId);
324
+ await removePublisher({
325
+ sessionId: snapshot.sessionId,
326
+ publisherId: snapshot.publisherId,
327
+ });
218
328
  } catch {
219
329
  // ignore broker absence on shutdown
220
330
  }
@@ -6,6 +6,11 @@
6
6
 
7
7
  const ACTIVE_STATES = new Set(["thinking", "tool", "waiting"]);
8
8
 
9
+ /** @param {{cardId?: string; sessionId?: string}} session */
10
+ export function activityCardKey(session) {
11
+ return String(session?.cardId ?? session?.sessionId ?? "");
12
+ }
13
+
9
14
  /** @param {{ state?: string; agentActive?: boolean }} session */
10
15
  export function isActiveSession(session) {
11
16
  return Boolean(session?.agentActive || ACTIVE_STATES.has(String(session?.state ?? "")));
@@ -24,18 +29,19 @@ export function isMonitoringSession(session) {
24
29
  * Reconcile at the calm clock: green settled/monitoring cards first, then active cards,
25
30
  * then other inactive cards. Preserve previous/manual order inside each group and append
26
31
  * new cards deterministically.
27
- * @param {Array<{ sessionId: string; state?: string; agentActive?: boolean; toolName?: string; toolTarget?: string; updatedAt?: number }>} sessions
32
+ * @param {Array<{ sessionId: string; cardId?: string; state?: string; agentActive?: boolean; toolName?: string; toolTarget?: string; updatedAt?: number }>} sessions
28
33
  * @param {string[]} previousOrder
29
34
  * @param {{ regroup?: boolean }} [options]
30
35
  */
31
36
  export function reconcileActivityOrder(sessions, previousOrder = [], { regroup = true } = {}) {
32
- const byId = new Map(sessions.map((session) => [session.sessionId, session]));
37
+ const byId = new Map(sessions.map((session) => [activityCardKey(session), session]));
33
38
  const surviving = previousOrder.filter((id) => byId.has(id));
34
39
  const known = new Set(surviving);
35
40
  for (const session of sessions) {
36
- if (!session.sessionId || known.has(session.sessionId)) continue;
37
- surviving.push(session.sessionId);
38
- known.add(session.sessionId);
41
+ const id = activityCardKey(session);
42
+ if (!id || known.has(id)) continue;
43
+ surviving.push(id);
44
+ known.add(id);
39
45
  }
40
46
  if (!regroup) return surviving;
41
47
 
@@ -45,16 +51,16 @@ export function reconcileActivityOrder(sessions, previousOrder = [], { regroup =
45
51
  const rightPriority = isMonitoringSession(right) ? 2 : isActiveSession(right) ? 1 : 0;
46
52
  const groupDelta = rightPriority - leftPriority;
47
53
  if (groupDelta) return groupDelta;
48
- const leftPrior = priorIndex.get(left.sessionId);
49
- const rightPrior = priorIndex.get(right.sessionId);
54
+ const leftPrior = priorIndex.get(activityCardKey(left));
55
+ const rightPrior = priorIndex.get(activityCardKey(right));
50
56
  if (leftPrior != null || rightPrior != null) {
51
57
  return (leftPrior ?? Number.MAX_SAFE_INTEGER) - (rightPrior ?? Number.MAX_SAFE_INTEGER);
52
58
  }
53
59
  const updateDelta = Number(right.updatedAt ?? 0) - Number(left.updatedAt ?? 0);
54
60
  if (updateDelta) return updateDelta;
55
- return left.sessionId.localeCompare(right.sessionId);
61
+ return activityCardKey(left).localeCompare(activityCardKey(right));
56
62
  });
57
- return ordered.map((session) => session.sessionId).filter((id) => byId.has(id));
63
+ return ordered.map(activityCardKey).filter((id) => byId.has(id));
58
64
  }
59
65
 
60
66
  /** @param {string[]} order @param {string} sessionId @param {-1 | 1} delta */
@@ -1,35 +1,9 @@
1
1
  // ---
2
- // summary: "coalesces native-window alignment so stale async work cannot overwrite newer geometry"
2
+ // summary: "coalesces asynchronous reconciliation so stale work cannot publish over newer state"
3
3
  // read_when:
4
- // - "changing Electron/Niri geometry reconciliation or testing rapid expansion transitions"
4
+ // - "changing latest-only native workspace reconciliation"
5
5
  // ---
6
6
 
7
- /**
8
- * @param {{ is_floating?: boolean, layout?: { tile_pos_in_workspace_view?: unknown } } | null | undefined} window
9
- */
10
- export function hasNiriFloatingPosition(window) {
11
- const position = window?.layout?.tile_pos_in_workspace_view;
12
- return window?.is_floating === true && Array.isArray(position) && position.length >= 2;
13
- }
14
-
15
- /**
16
- * @param {{ is_floating?: boolean, layout?: { tile_pos_in_workspace_view?: unknown, window_size?: unknown } } | null | undefined} window
17
- * @param {{x: number, y: number, width: number, height: number}} target
18
- */
19
- export function isNiriWindowAligned(window, target) {
20
- if (!hasNiriFloatingPosition(window)) return false;
21
- const position = window?.layout?.tile_pos_in_workspace_view;
22
- const size = window?.layout?.window_size;
23
- return Boolean(
24
- Array.isArray(position) &&
25
- Array.isArray(size) &&
26
- Number(size[0]) === target.width &&
27
- Number(size[1]) === target.height &&
28
- Math.abs(Number(position[0]) - target.x) < 1 &&
29
- Math.abs(Number(position[1]) - target.y) < 1,
30
- );
31
- }
32
-
33
7
  /**
34
8
  * @param {(attempt: { generation: number, isCurrent: () => boolean }) => Promise<void>} run
35
9
  */
@@ -37,34 +11,42 @@ export function createLatestOnlyRunner(run) {
37
11
  if (typeof run !== "function") throw new TypeError("run must be a function");
38
12
 
39
13
  let requestedGeneration = 0;
14
+ let completedGeneration = 0;
40
15
  /** @type {Promise<void> | null} */
41
16
  let worker = null;
42
17
 
18
+ function ensureWorker() {
19
+ if (!worker) worker = drain();
20
+ }
21
+
43
22
  async function drain() {
44
- while (true) {
45
- const generation = requestedGeneration;
46
- const isCurrent = () => generation === requestedGeneration;
47
- try {
48
- await run({ generation, isCurrent });
49
- } catch {
50
- // Alignment is best effort. A newer request must still be allowed to reconcile the window.
23
+ try {
24
+ while (completedGeneration < requestedGeneration) {
25
+ const generation = requestedGeneration;
26
+ const isCurrent = () => generation === requestedGeneration;
27
+ try {
28
+ await run({ generation, isCurrent });
29
+ } catch {
30
+ // Alignment is best effort. A newer request must still be allowed to reconcile the window.
31
+ }
32
+ completedGeneration = generation;
51
33
  }
52
- if (isCurrent()) return;
34
+ } finally {
35
+ worker = null;
36
+ // A request can arrive after the loop condition was checked but before this finally block.
37
+ // Hand it to a successor synchronously so no requested generation is lost.
38
+ if (completedGeneration < requestedGeneration) ensureWorker();
53
39
  }
54
40
  }
55
41
 
56
42
  function request() {
57
43
  requestedGeneration += 1;
58
- if (!worker) {
59
- worker = drain().finally(() => {
60
- worker = null;
61
- });
62
- }
44
+ ensureWorker();
63
45
  return requestedGeneration;
64
46
  }
65
47
 
66
48
  async function waitForIdle() {
67
- await worker;
49
+ while (worker) await worker;
68
50
  }
69
51
 
70
52
  return {
@@ -0,0 +1,53 @@
1
+ // ---
2
+ // summary: "derives card display facts that depend on more than one session field"
3
+ // read_when:
4
+ // - "changing stalled-session detection, duplicate label disambiguation, or card label rendering"
5
+ // ---
6
+
7
+ /**
8
+ * A session is stalled when it claims to be mid-activity but no lifecycle
9
+ * event has arrived for longer than the stall threshold. Wedged streams keep
10
+ * heartbeating liveness, so only real event time can expose them.
11
+ * @param {Record<string, unknown>} session
12
+ * @param {number} nowMs
13
+ * @param {number} stallMs
14
+ */
15
+ export function isStalledSession(session, nowMs, stallMs) {
16
+ if (!session.agentActive) return false;
17
+ const state = String(session.state ?? "");
18
+ if (state !== "thinking" && state !== "tool" && state !== "waiting") return false;
19
+ const anchor = Number(session.lastEventAt || session.updatedAt || 0);
20
+ return anchor > 0 && nowMs - anchor > stallMs;
21
+ }
22
+
23
+ /**
24
+ * @param {Array<Record<string, unknown>>} sessions
25
+ * @returns {Set<string>}
26
+ */
27
+ export function findDuplicateLabels(sessions) {
28
+ const counts = new Map();
29
+ for (const session of sessions) {
30
+ const label = String(session.repoLabel || "pi session");
31
+ counts.set(label, (counts.get(label) || 0) + 1);
32
+ }
33
+ return new Set([...counts.entries()].filter(([, count]) => count > 1).map(([label]) => label));
34
+ }
35
+
36
+ /**
37
+ * Two live processes can share one repo label; append a short process suffix
38
+ * so their cards stay distinguishable at a glance.
39
+ * @param {Record<string, unknown>} session
40
+ * @param {Set<string>} duplicateLabels
41
+ */
42
+ export function disambiguatedRepoLabel(session, duplicateLabels) {
43
+ const label = String(session.repoLabel || "pi session");
44
+ const processId = Number(session.processId ?? 0);
45
+ return duplicateLabels.has(label) && processId > 0
46
+ ? `${label} · ${String(processId).slice(-4)}`
47
+ : label;
48
+ }
49
+
50
+ /** @param {{hovered?: boolean; activeElement?: boolean; documentFocused?: boolean}} state */
51
+ export function shouldRetainExpandedCard({ hovered, activeElement, documentFocused }) {
52
+ return Boolean(hovered || (documentFocused && activeElement));
53
+ }
@@ -1,12 +1,11 @@
1
1
  // ---
2
- // summary: "detects desktop and Electron prerequisites and formats actionable activity-strip compatibility reports"
2
+ // summary: "detects native layer-shell prerequisites and formats actionable activity-strip compatibility reports"
3
3
  // read_when:
4
4
  // - "changing host detection, compatibility blockers, warnings, or doctor output"
5
5
  // ---
6
6
 
7
7
  import { execFile } from "node:child_process";
8
8
  import { promisify } from "node:util";
9
- import { locateElectron } from "./electron.mjs";
10
9
 
11
10
  const execFileAsync = promisify(execFile);
12
11
 
@@ -67,14 +66,17 @@ export async function detectDisplayCount(options = {}) {
67
66
  /**
68
67
  * @param {{
69
68
  * env?: NodeJS.ProcessEnv;
70
- * locateElectronImpl?: typeof locateElectron;
71
69
  * execFileAsyncImpl?: typeof execFileAsync;
70
+ * platform?: NodeJS.Platform;
71
+ * arch?: string;
72
72
  * }} [options]
73
73
  * @returns {Promise<import("./contracts.ts").ActivityStripCompatibilityReport>}
74
74
  */
75
75
  export async function assessActivityStripCompatibility(options = {}) {
76
76
  const env = options.env || process.env;
77
- const locateElectronImpl = options.locateElectronImpl || locateElectron;
77
+ const backend = "native";
78
+ const platform = options.platform ?? process.platform;
79
+ const arch = options.arch ?? process.arch;
78
80
  const displayServer = detectDisplayServer(env);
79
81
  const windowManager = detectWindowManager(env);
80
82
  const displayCount = await detectDisplayCount({
@@ -83,7 +85,6 @@ export async function assessActivityStripCompatibility(options = {}) {
83
85
  });
84
86
  const blockers = [];
85
87
  const warnings = [];
86
- let electronPath = null;
87
88
 
88
89
  if (displayServer === "headless") {
89
90
  blockers.push(
@@ -97,10 +98,11 @@ export async function assessActivityStripCompatibility(options = {}) {
97
98
  );
98
99
  }
99
100
 
100
- try {
101
- electronPath = await locateElectronImpl();
102
- } catch (error) {
103
- blockers.push(error instanceof Error ? error.message : String(error));
101
+ if (displayServer !== "headless" && displayServer !== "wayland") {
102
+ blockers.push("The native Activity Strip panel requires a Wayland session.");
103
+ }
104
+ if (platform !== "linux" || arch !== "x64") {
105
+ blockers.push(`The packaged native panel requires Linux x64; detected ${platform} ${arch}.`);
104
106
  }
105
107
 
106
108
  if (displayCount && displayCount > 1) {
@@ -111,17 +113,17 @@ export async function assessActivityStripCompatibility(options = {}) {
111
113
 
112
114
  if (windowManager !== "niri") {
113
115
  warnings.push(
114
- "Top-edge repair is optimized for Niri. Other window managers fall back to generic Electron bounds and may need manual adjustment.",
116
+ "Workspace-local projection is optimized for Niri; other layer-shell compositors show the global card view.",
115
117
  );
116
118
  }
117
119
 
118
120
  return {
119
121
  ok: blockers.length === 0,
122
+ backend,
120
123
  displayServer,
121
124
  windowManager: typeof windowManager === "string" ? windowManager : null,
122
- electronPath,
123
125
  displayCount,
124
- alignmentMode: windowManager === "niri" ? "niri" : "generic",
126
+ alignmentMode: "layer-shell",
125
127
  primaryDisplayOnly: true,
126
128
  clickThroughDefault: env.PI_ACTIVITY_STRIP_CLICK_THROUGH === "1",
127
129
  blockers,
@@ -137,7 +139,7 @@ export function formatCompatibilityReport(report) {
137
139
  `Compatibility: ${report.ok ? "compatible" : "blocked"}`,
138
140
  `Display server: ${report.displayServer}`,
139
141
  `Window manager: ${report.windowManager || "unknown"}`,
140
- `Electron: ${report.electronPath || "not found"}`,
142
+ `Backend: ${report.backend}`,
141
143
  `Alignment mode: ${report.alignmentMode}`,
142
144
  `Primary-display only: ${report.primaryDisplayOnly ? "yes" : "no"}`,
143
145
  `Click-through mode: ${report.clickThroughDefault ? "enabled by environment" : "disabled (interactive default)"}`,
@@ -1,5 +1,5 @@
1
1
  // ---
2
- // summary: "defines shared socket paths, window dimensions, timing thresholds, and Electron binary candidates"
2
+ // summary: "defines shared broker paths, timing thresholds, and transport bounds"
3
3
  // read_when:
4
4
  // - "tuning activity-strip runtime defaults or locating shared configuration values"
5
5
  // ---
@@ -8,38 +8,22 @@ import os from "node:os";
8
8
  import path from "node:path";
9
9
 
10
10
  export const ACTIVITY_STRIP_NAME = "pi-activity-strip";
11
- export const ACTIVITY_STRIP_SOCKET_DIR = path.join(
12
- os.homedir(),
13
- ".pi",
14
- "agent",
15
- "state",
16
- ACTIVITY_STRIP_NAME,
17
- );
18
- export const ACTIVITY_STRIP_SOCKET_PATH = path.join(
19
- ACTIVITY_STRIP_SOCKET_DIR,
20
- "activity-strip.sock",
21
- );
22
- export const ACTIVITY_STRIP_WIDTH_PADDING = 16;
23
- export const ACTIVITY_STRIP_HEIGHT = 84;
24
- export const ACTIVITY_STRIP_EXPANDED_HEIGHT = 252;
11
+ export const ACTIVITY_STRIP_SOCKET_DIR =
12
+ process.env.PI_ACTIVITY_STRIP_SOCKET_DIR?.trim() ||
13
+ path.join(os.homedir(), ".pi", "agent", "state", ACTIVITY_STRIP_NAME);
14
+ export const ACTIVITY_STRIP_SOCKET_PATH =
15
+ process.env.PI_ACTIVITY_STRIP_SOCKET_PATH?.trim() ||
16
+ path.join(ACTIVITY_STRIP_SOCKET_DIR, "activity-strip.sock");
25
17
  export const ACTIVITY_STRIP_ORDER_REFRESH_MS = 15_000;
26
18
  export const ACTIVITY_STRIP_WORKSPACE_SYNC_MS = 1500;
27
- export const ACTIVITY_STRIP_NIRI_ANIMATION_SETTLE_MS = 450;
28
19
  export const ACTIVITY_STRIP_HEARTBEAT_MS = 2500;
29
20
  export const ACTIVITY_STRIP_STALE_AFTER_MS = 12000;
21
+ export const ACTIVITY_STRIP_MAX_SESSIONS = 256;
22
+ export const ACTIVITY_STRIP_MAX_MESSAGE_BYTES = 256 * 1024;
23
+ export const ACTIVITY_STRIP_CLIENT_IDLE_TIMEOUT_MS = 2000;
30
24
  export const ACTIVITY_STRIP_BROADCAST_TICK_MS = 1000;
31
25
  export const ACTIVITY_STRIP_SEND_THROTTLE_MS = 120;
32
26
  export const ACTIVITY_STRIP_CONNECT_TIMEOUT_MS = 450;
33
27
  export const ACTIVITY_STRIP_START_TIMEOUT_MS = 5000;
34
- export const DEFAULT_ELECTRON_CANDIDATES = [
35
- "electron",
36
- "electron39",
37
- "electron38",
38
- "electron37",
39
- "electron36",
40
- "electron35",
41
- "electron34",
42
- "electron33",
43
- "electron32",
44
- "electron31",
45
- ];
28
+ export const ACTIVITY_STRIP_EVENT_STALL_MS = 15 * 60_000;
29
+ export const ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS = [250, 750];