@tryinget/pi-activity-strip 0.2.0 → 0.4.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.
@@ -0,0 +1,68 @@
1
+ // ---
2
+ // summary: "provides deterministic monitoring-first strip ordering while preserving operator-defined order"
3
+ // read_when:
4
+ // - "changing activity grouping, calm-cadence reconciliation, or manual card order"
5
+ // ---
6
+
7
+ const ACTIVE_STATES = new Set(["thinking", "tool", "waiting"]);
8
+
9
+ /** @param {{ state?: string; agentActive?: boolean }} session */
10
+ export function isActiveSession(session) {
11
+ return Boolean(session?.agentActive || ACTIVE_STATES.has(String(session?.state ?? "")));
12
+ }
13
+
14
+ /** @param {{ state?: string; toolName?: string; toolTarget?: string }} session */
15
+ export function isMonitoringSession(session) {
16
+ return Boolean(
17
+ session?.state === "success" &&
18
+ !String(session?.toolName ?? "").trim() &&
19
+ !String(session?.toolTarget ?? "").trim(),
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Reconcile at the calm clock: green settled/monitoring cards first, then active cards,
25
+ * then other inactive cards. Preserve previous/manual order inside each group and append
26
+ * new cards deterministically.
27
+ * @param {Array<{ sessionId: string; state?: string; agentActive?: boolean; toolName?: string; toolTarget?: string; updatedAt?: number }>} sessions
28
+ * @param {string[]} previousOrder
29
+ * @param {{ regroup?: boolean }} [options]
30
+ */
31
+ export function reconcileActivityOrder(sessions, previousOrder = [], { regroup = true } = {}) {
32
+ const byId = new Map(sessions.map((session) => [session.sessionId, session]));
33
+ const surviving = previousOrder.filter((id) => byId.has(id));
34
+ const known = new Set(surviving);
35
+ for (const session of sessions) {
36
+ if (!session.sessionId || known.has(session.sessionId)) continue;
37
+ surviving.push(session.sessionId);
38
+ known.add(session.sessionId);
39
+ }
40
+ if (!regroup) return surviving;
41
+
42
+ const priorIndex = new Map(surviving.map((id, index) => [id, index]));
43
+ const ordered = [...sessions].sort((left, right) => {
44
+ const leftPriority = isMonitoringSession(left) ? 2 : isActiveSession(left) ? 1 : 0;
45
+ const rightPriority = isMonitoringSession(right) ? 2 : isActiveSession(right) ? 1 : 0;
46
+ const groupDelta = rightPriority - leftPriority;
47
+ if (groupDelta) return groupDelta;
48
+ const leftPrior = priorIndex.get(left.sessionId);
49
+ const rightPrior = priorIndex.get(right.sessionId);
50
+ if (leftPrior != null || rightPrior != null) {
51
+ return (leftPrior ?? Number.MAX_SAFE_INTEGER) - (rightPrior ?? Number.MAX_SAFE_INTEGER);
52
+ }
53
+ const updateDelta = Number(right.updatedAt ?? 0) - Number(left.updatedAt ?? 0);
54
+ if (updateDelta) return updateDelta;
55
+ return left.sessionId.localeCompare(right.sessionId);
56
+ });
57
+ return ordered.map((session) => session.sessionId).filter((id) => byId.has(id));
58
+ }
59
+
60
+ /** @param {string[]} order @param {string} sessionId @param {-1 | 1} delta */
61
+ export function moveOrderItem(order, sessionId, delta) {
62
+ const next = [...order];
63
+ const from = next.indexOf(sessionId);
64
+ const to = from + delta;
65
+ if (from < 0 || to < 0 || to >= next.length) return next;
66
+ [next[from], next[to]] = [next[to], next[from]];
67
+ return next;
68
+ }
@@ -0,0 +1,74 @@
1
+ // ---
2
+ // summary: "coalesces native-window alignment so stale async work cannot overwrite newer geometry"
3
+ // read_when:
4
+ // - "changing Electron/Niri geometry reconciliation or testing rapid expansion transitions"
5
+ // ---
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
+ /**
34
+ * @param {(attempt: { generation: number, isCurrent: () => boolean }) => Promise<void>} run
35
+ */
36
+ export function createLatestOnlyRunner(run) {
37
+ if (typeof run !== "function") throw new TypeError("run must be a function");
38
+
39
+ let requestedGeneration = 0;
40
+ /** @type {Promise<void> | null} */
41
+ let worker = null;
42
+
43
+ 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.
51
+ }
52
+ if (isCurrent()) return;
53
+ }
54
+ }
55
+
56
+ function request() {
57
+ requestedGeneration += 1;
58
+ if (!worker) {
59
+ worker = drain().finally(() => {
60
+ worker = null;
61
+ });
62
+ }
63
+ return requestedGeneration;
64
+ }
65
+
66
+ async function waitForIdle() {
67
+ await worker;
68
+ }
69
+
70
+ return {
71
+ request,
72
+ waitForIdle,
73
+ };
74
+ }
@@ -0,0 +1,48 @@
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
+ }
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: "detects desktop and Electron prerequisites and formats actionable activity-strip compatibility reports"
3
+ // read_when:
4
+ // - "changing host detection, compatibility blockers, warnings, or doctor output"
5
+ // ---
6
+
1
7
  import { execFile } from "node:child_process";
2
8
  import { promisify } from "node:util";
3
9
  import { locateElectron } from "./electron.mjs";
@@ -85,6 +91,12 @@ export async function assessActivityStripCompatibility(options = {}) {
85
91
  );
86
92
  }
87
93
 
94
+ if (windowManager === "niri" && displayCount === 0) {
95
+ blockers.push(
96
+ "Niri reports no connected display outputs. Turn on or reconnect the monitor before opening the activity strip.",
97
+ );
98
+ }
99
+
88
100
  try {
89
101
  electronPath = await locateElectronImpl();
90
102
  } catch (error) {
@@ -111,7 +123,7 @@ export async function assessActivityStripCompatibility(options = {}) {
111
123
  displayCount,
112
124
  alignmentMode: windowManager === "niri" ? "niri" : "generic",
113
125
  primaryDisplayOnly: true,
114
- clickThroughDefault: env.PI_ACTIVITY_STRIP_CLICK_THROUGH !== "0",
126
+ clickThroughDefault: env.PI_ACTIVITY_STRIP_CLICK_THROUGH === "1",
115
127
  blockers,
116
128
  warnings,
117
129
  };
@@ -128,7 +140,7 @@ export function formatCompatibilityReport(report) {
128
140
  `Electron: ${report.electronPath || "not found"}`,
129
141
  `Alignment mode: ${report.alignmentMode}`,
130
142
  `Primary-display only: ${report.primaryDisplayOnly ? "yes" : "no"}`,
131
- `Click-through default: ${report.clickThroughDefault ? "yes" : "no"}`,
143
+ `Click-through mode: ${report.clickThroughDefault ? "enabled by environment" : "disabled (interactive default)"}`,
132
144
  ];
133
145
 
134
146
  if (typeof report.displayCount === "number") {
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: "defines shared socket paths, window dimensions, timing thresholds, and Electron binary candidates"
3
+ // read_when:
4
+ // - "tuning activity-strip runtime defaults or locating shared configuration values"
5
+ // ---
6
+
1
7
  import os from "node:os";
2
8
  import path from "node:path";
3
9
 
@@ -15,6 +21,10 @@ export const ACTIVITY_STRIP_SOCKET_PATH = path.join(
15
21
  );
16
22
  export const ACTIVITY_STRIP_WIDTH_PADDING = 16;
17
23
  export const ACTIVITY_STRIP_HEIGHT = 84;
24
+ export const ACTIVITY_STRIP_EXPANDED_HEIGHT = 252;
25
+ export const ACTIVITY_STRIP_ORDER_REFRESH_MS = 15_000;
26
+ export const ACTIVITY_STRIP_WORKSPACE_SYNC_MS = 1500;
27
+ export const ACTIVITY_STRIP_NIRI_ANIMATION_SETTLE_MS = 450;
18
28
  export const ACTIVITY_STRIP_HEARTBEAT_MS = 2500;
19
29
  export const ACTIVITY_STRIP_STALE_AFTER_MS = 12000;
20
30
  export const ACTIVITY_STRIP_BROADCAST_TICK_MS = 1000;
@@ -33,3 +43,5 @@ export const DEFAULT_ELECTRON_CANDIDATES = [
33
43
  "electron32",
34
44
  "electron31",
35
45
  ];
46
+ export const ACTIVITY_STRIP_EVENT_STALL_MS = 15 * 60_000;
47
+ export const ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS = [250, 750];
@@ -1,7 +1,14 @@
1
+ // ---
2
+ // summary: "declares the public TypeScript shapes shared by activity-strip runtime and JavaScript consumers"
3
+ // read_when:
4
+ // - "checking emitted declaration contracts for sessions, broker messages, telemetry, or compatibility"
5
+ // ---
6
+
1
7
  export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success" | "error";
2
8
 
3
9
  export interface SessionSnapshot {
4
10
  sessionId: string;
11
+ publisherId: string;
5
12
  processId: number;
6
13
  cwd: string;
7
14
  repoLabel: string;
@@ -14,6 +21,7 @@ export interface SessionSnapshot {
14
21
  state: SessionState;
15
22
  turnIndex: number;
16
23
  updatedAt: number;
24
+ lastEventAt: number;
17
25
  startedAt: number;
18
26
  agentStartedAt: number | null;
19
27
  agentActive: boolean;
@@ -65,9 +73,10 @@ export interface ActivityStripBrokerOptions {
65
73
  store?: {
66
74
  snapshot(): BrokerSnapshot;
67
75
  upsert(session: Partial<SessionSnapshot> | Record<string, unknown>): boolean;
68
- remove(sessionId: string): boolean;
76
+ remove(sessionId: string, publisherId?: string): boolean;
69
77
  };
70
78
  getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
79
+ focusSession?: (sessionId: string) => Promise<{ ok: boolean; error?: string; windowId?: number }>;
71
80
  }
72
81
 
73
82
  export interface ToolCallDescription {
@@ -95,6 +104,9 @@ export interface SessionStartContextLike {
95
104
  notify?: (message: string, level?: "info" | "warning" | "error") => void;
96
105
  editor?: (title: string, text: string) => Promise<unknown>;
97
106
  };
107
+ sessionManager?: {
108
+ getSessionId?: () => string;
109
+ };
98
110
  }
99
111
 
100
112
  export interface BeforeAgentStartEventLike {
@@ -105,6 +117,11 @@ export interface TurnStartEventLike {
105
117
  turnIndex?: number;
106
118
  }
107
119
 
120
+ export interface TurnEndEventLike {
121
+ turnIndex?: number;
122
+ message?: unknown;
123
+ }
124
+
108
125
  export interface AssistantMessageEventLike {
109
126
  type?: string;
110
127
  delta?: unknown;
@@ -126,6 +143,10 @@ export interface SessionTelemetryOptions {
126
143
  pi?: TelemetryPiLike;
127
144
  cwd?: string;
128
145
  sessionName?: string;
146
+ transport?: {
147
+ publish: (session: SessionSnapshot) => Promise<unknown>;
148
+ remove: (session: { sessionId: string; publisherId: string }) => Promise<unknown>;
149
+ };
129
150
  }
130
151
 
131
152
  export interface ActivityStripCompatibilityReport {
@@ -1,7 +1,14 @@
1
+ // ---
2
+ // summary: "defines source TypeScript contracts for session telemetry, broker transport, and runtime compatibility"
3
+ // read_when:
4
+ // - "changing shared activity-strip types or the interfaces consumed through JSDoc imports"
5
+ // ---
6
+
1
7
  export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success" | "error";
2
8
 
3
9
  export interface SessionSnapshot {
4
10
  sessionId: string;
11
+ publisherId: string;
5
12
  processId: number;
6
13
  cwd: string;
7
14
  repoLabel: string;
@@ -14,6 +21,7 @@ export interface SessionSnapshot {
14
21
  state: SessionState;
15
22
  turnIndex: number;
16
23
  updatedAt: number;
24
+ lastEventAt: number;
17
25
  startedAt: number;
18
26
  agentStartedAt: number | null;
19
27
  agentActive: boolean;
@@ -65,9 +73,10 @@ export interface ActivityStripBrokerOptions {
65
73
  store?: {
66
74
  snapshot(): BrokerSnapshot;
67
75
  upsert(session: Partial<SessionSnapshot> | Record<string, unknown>): boolean;
68
- remove(sessionId: string): boolean;
76
+ remove(sessionId: string, publisherId?: string): boolean;
69
77
  };
70
78
  getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
79
+ focusSession?: (sessionId: string) => Promise<{ ok: boolean; error?: string; windowId?: number }>;
71
80
  }
72
81
 
73
82
  export interface ToolCallDescription {
@@ -95,6 +104,9 @@ export interface SessionStartContextLike {
95
104
  notify?: (message: string, level?: "info" | "warning" | "error") => void;
96
105
  editor?: (title: string, text: string) => Promise<unknown>;
97
106
  };
107
+ sessionManager?: {
108
+ getSessionId?: () => string;
109
+ };
98
110
  }
99
111
 
100
112
  export interface BeforeAgentStartEventLike {
@@ -105,6 +117,11 @@ export interface TurnStartEventLike {
105
117
  turnIndex?: number;
106
118
  }
107
119
 
120
+ export interface TurnEndEventLike {
121
+ turnIndex?: number;
122
+ message?: unknown;
123
+ }
124
+
108
125
  export interface AssistantMessageEventLike {
109
126
  type?: string;
110
127
  delta?: unknown;
@@ -126,6 +143,10 @@ export interface SessionTelemetryOptions {
126
143
  pi?: TelemetryPiLike;
127
144
  cwd?: string;
128
145
  sessionName?: string;
146
+ transport?: {
147
+ publish: (session: SessionSnapshot) => Promise<unknown>;
148
+ remove: (session: { sessionId: string; publisherId: string }) => Promise<unknown>;
149
+ };
129
150
  }
130
151
 
131
152
  export interface ActivityStripCompatibilityReport {
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: "resolves an executable Electron binary from explicit settings, local dependencies, or PATH candidates"
3
+ // read_when:
4
+ // - "changing Electron discovery order, environment overrides, or executable checks"
5
+ // ---
6
+
1
7
  import { constants } from "node:fs";
2
8
  import { access } from "node:fs/promises";
3
9
  import path from "node:path";
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: "provides ANSI-safe truncation, whitespace compaction, and concise path, command, and session labels"
3
+ // read_when:
4
+ // - "changing text previews or labels displayed in activity-strip telemetry"
5
+ // ---
6
+
1
7
  import path from "node:path";
2
8
 
3
9
  // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping requires an ESC control character pattern.