@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.
- package/README.md +96 -152
- package/bin/pi-activity-strip.mjs +31 -62
- package/examples/simulate-sessions.mjs +1 -1
- package/extensions/activity-strip.js +2 -2
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +303 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +234 -0
- package/native/panel/src/runtime.rs +110 -0
- package/native/panel/src/style.css +176 -0
- package/package.json +20 -7
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +53 -11
- package/src/client/broker-client.mjs +25 -4
- package/src/client/session-telemetry.mjs +123 -13
- package/src/common/activity-order.mjs +15 -9
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +53 -0
- package/src/common/compatibility.mjs +15 -13
- package/src/common/constants.mjs +12 -28
- package/src/common/contracts.d.ts +54 -5
- package/src/common/contracts.ts +54 -5
- package/src/common/niri-focus.mjs +79 -79
- package/src/common/protocol.mjs +49 -14
- package/src/common/session-cards.mjs +102 -0
- package/src/common/status-report.mjs +2 -1
- package/src/common/telemetry.mjs +11 -1
- package/src/common/terminal-identity.mjs +149 -0
- package/src/native/main.mjs +393 -0
- package/src/native/panel-projection.mjs +99 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +4 -1
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- 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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
106
|
+
await publish({ ...snapshot });
|
|
107
|
+
confirmedSignature = signature;
|
|
108
|
+
retryIndex = 0;
|
|
65
109
|
} catch {
|
|
66
|
-
|
|
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
|
|
122
|
+
async function flush() {
|
|
123
|
+
clearScheduledFlush();
|
|
71
124
|
if (disposed) return;
|
|
72
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
49
|
-
const rightPrior = priorIndex.get(right
|
|
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.
|
|
61
|
+
return activityCardKey(left).localeCompare(activityCardKey(right));
|
|
56
62
|
});
|
|
57
|
-
return ordered.map(
|
|
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
|
|
2
|
+
// summary: "coalesces asynchronous reconciliation so stale work cannot publish over newer state"
|
|
3
3
|
// read_when:
|
|
4
|
-
// - "changing
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
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
|
-
"
|
|
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:
|
|
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
|
-
`
|
|
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)"}`,
|
package/src/common/constants.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---
|
|
2
|
-
// summary: "defines shared
|
|
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 =
|
|
12
|
-
|
|
13
|
-
".pi",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
35
|
-
|
|
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];
|