@tryinget/pi-activity-strip 0.3.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.
- package/examples/simulate-sessions.mjs +1 -1
- package/extensions/activity-strip.js +2 -2
- package/package.json +8 -7
- package/src/broker/server.mjs +1 -1
- package/src/broker/session-store.mjs +18 -4
- package/src/client/broker-client.mjs +17 -3
- package/src/client/session-telemetry.mjs +76 -6
- package/src/common/card-display.mjs +48 -0
- package/src/common/constants.mjs +2 -0
- package/src/common/contracts.d.ts +12 -1
- package/src/common/contracts.ts +12 -1
- package/src/common/protocol.mjs +3 -0
- package/src/common/telemetry.mjs +4 -1
- package/src/ui/strip-html.mjs +24 -8
|
@@ -194,8 +194,8 @@ export default function activityStripExtension(pi) {
|
|
|
194
194
|
telemetry.onToolExecutionEnd(event);
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
-
pi.on("turn_end", async () => {
|
|
198
|
-
telemetry.onTurnEnd();
|
|
197
|
+
pi.on("turn_end", async (event) => {
|
|
198
|
+
telemetry.onTurnEnd(event);
|
|
199
199
|
});
|
|
200
200
|
|
|
201
201
|
pi.on("agent_settled", async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryinget/pi-activity-strip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "pi extension package for activity-strip workflows in monorepo runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"quality:ci": "bash ./scripts/quality-gate.sh ci",
|
|
36
36
|
"check": "npm run quality:ci",
|
|
37
37
|
"test": "npm run quality:ci",
|
|
38
|
-
"docs:list": "
|
|
39
|
-
"docs:list:workspace": "
|
|
40
|
-
"docs:list:json": "
|
|
38
|
+
"docs:list": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs",
|
|
39
|
+
"docs:list:workspace": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs --workspace --discover",
|
|
40
|
+
"docs:list:json": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs --json",
|
|
41
41
|
"release:check": "bash ./scripts/release-check.sh",
|
|
42
42
|
"release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh",
|
|
43
43
|
"strip:open": "node ./bin/pi-activity-strip.mjs open",
|
|
@@ -76,15 +76,16 @@
|
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@biomejs/biome": "2.4.4",
|
|
79
|
-
"@earendil-works/pi-
|
|
80
|
-
"@earendil-works/pi-
|
|
79
|
+
"@earendil-works/pi-agent-core": "0.84.3",
|
|
80
|
+
"@earendil-works/pi-ai": "0.84.3",
|
|
81
|
+
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
81
82
|
"@types/node": "22.14.0",
|
|
82
83
|
"@typescript/native-preview": "7.0.0-dev.20260417.1",
|
|
83
84
|
"typebox": "1.3.7",
|
|
84
85
|
"typescript": "5.9.2"
|
|
85
86
|
},
|
|
86
87
|
"overrides": {
|
|
87
|
-
"fast-xml-parser": "5.
|
|
88
|
+
"fast-xml-parser": "5.7.0"
|
|
88
89
|
},
|
|
89
90
|
"peerDependencies": {
|
|
90
91
|
"@earendil-works/pi-ai": "*",
|
package/src/broker/server.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export class ActivityStripBroker extends EventEmitter {
|
|
|
152
152
|
}, 20);
|
|
153
153
|
return;
|
|
154
154
|
case "remove":
|
|
155
|
-
this.store.remove(message.sessionId);
|
|
155
|
+
this.store.remove(String(message.sessionId ?? ""), String(message.publisherId ?? ""));
|
|
156
156
|
this.emitSnapshot();
|
|
157
157
|
return;
|
|
158
158
|
case "upsert":
|
|
@@ -11,6 +11,16 @@ import { normalizeSessionSnapshot, sortSessions } from "../common/protocol.mjs";
|
|
|
11
11
|
/** @typedef {import("../common/contracts.ts").SessionStoreOptions} SessionStoreOptions */
|
|
12
12
|
/** @typedef {import("../common/contracts.ts").BrokerSnapshot} BrokerSnapshot */
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Stable store key. Sessions resumed into a second process keep publishing under
|
|
16
|
+
* their own publisherId so two live processes never fight over one card; legacy
|
|
17
|
+
* publishers without a publisherId keep the bare sessionId key.
|
|
18
|
+
* @param {SessionSnapshot} session
|
|
19
|
+
*/
|
|
20
|
+
function sessionKey(session) {
|
|
21
|
+
return session.publisherId ? `${session.sessionId}|${session.publisherId}` : session.sessionId;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export class SessionStore {
|
|
15
25
|
/** @param {SessionStoreOptions} [options] */
|
|
16
26
|
constructor(options = {}) {
|
|
@@ -23,13 +33,17 @@ export class SessionStore {
|
|
|
23
33
|
upsert(session) {
|
|
24
34
|
const normalized = normalizeSessionSnapshot(session);
|
|
25
35
|
if (!normalized.sessionId) return false;
|
|
26
|
-
this.sessions.set(normalized
|
|
36
|
+
this.sessions.set(sessionKey(normalized), normalized);
|
|
27
37
|
return true;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
|
-
/** @param {string} sessionId */
|
|
31
|
-
remove(sessionId) {
|
|
32
|
-
|
|
40
|
+
/** @param {string} sessionId @param {string} [publisherId] */
|
|
41
|
+
remove(sessionId, publisherId) {
|
|
42
|
+
const id = String(sessionId ?? "");
|
|
43
|
+
if (!id) return false;
|
|
44
|
+
return publisherId
|
|
45
|
+
? this.sessions.delete(`${id}|${String(publisherId)}`)
|
|
46
|
+
: this.sessions.delete(id);
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
/** @param {number} [now] */
|
|
@@ -115,7 +115,21 @@ export async function publishSessionSnapshot(session, options = {}) {
|
|
|
115
115
|
await sendBrokerMessage(makeMessage("upsert", { session }), options);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/** @param {string}
|
|
119
|
-
export async function removeSession(
|
|
120
|
-
|
|
118
|
+
/** @param {{ sessionId: string; publisherId: string }} session @param {BrokerClientOptions} [options] */
|
|
119
|
+
export async function removeSession(session, options = {}) {
|
|
120
|
+
/** @type {Record<string, unknown>} */
|
|
121
|
+
let record = {};
|
|
122
|
+
let fallbackId = "";
|
|
123
|
+
if (session && typeof session === "object") {
|
|
124
|
+
record = session;
|
|
125
|
+
} else {
|
|
126
|
+
fallbackId = String(session ?? "");
|
|
127
|
+
}
|
|
128
|
+
await sendBrokerMessage(
|
|
129
|
+
makeMessage("remove", {
|
|
130
|
+
sessionId: String(record.sessionId ?? fallbackId),
|
|
131
|
+
publisherId: String(record.publisherId ?? ""),
|
|
132
|
+
}),
|
|
133
|
+
options,
|
|
134
|
+
);
|
|
121
135
|
}
|
|
@@ -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";
|
|
@@ -41,7 +43,14 @@ function extractAssistantDelta(event) {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/** @param {SessionTelemetryOptions} [options] */
|
|
44
|
-
export function createSessionTelemetry({
|
|
46
|
+
export function createSessionTelemetry({
|
|
47
|
+
pi,
|
|
48
|
+
cwd = process.cwd(),
|
|
49
|
+
sessionName = "",
|
|
50
|
+
transport,
|
|
51
|
+
} = {}) {
|
|
52
|
+
const publish = transport?.publish ?? publishSessionSnapshot;
|
|
53
|
+
const removePublisher = transport?.remove ?? removeSession;
|
|
45
54
|
/** @type {SessionSnapshot} */
|
|
46
55
|
let snapshot = createInitialSnapshot({ cwd, sessionName });
|
|
47
56
|
/** @type {NodeJS.Timeout | null} */
|
|
@@ -50,20 +59,52 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
|
|
|
50
59
|
let flushTimer = null;
|
|
51
60
|
let disposed = false;
|
|
52
61
|
let pendingAssistant = "";
|
|
62
|
+
let lastStopReason = "";
|
|
63
|
+
let confirmedSignature = "";
|
|
64
|
+
let retryIndex = 0;
|
|
65
|
+
|
|
66
|
+
/** Signature of the display-relevant fields, used to detect lost transitions. */
|
|
67
|
+
function publishSignature() {
|
|
68
|
+
return [
|
|
69
|
+
snapshot.sessionId,
|
|
70
|
+
snapshot.state,
|
|
71
|
+
snapshot.phase,
|
|
72
|
+
snapshot.detail,
|
|
73
|
+
snapshot.agentActive,
|
|
74
|
+
snapshot.turnIndex,
|
|
75
|
+
snapshot.toolName,
|
|
76
|
+
snapshot.toolTarget,
|
|
77
|
+
snapshot.errorMessage,
|
|
78
|
+
].join("\u001f");
|
|
79
|
+
}
|
|
53
80
|
|
|
54
81
|
async function flush() {
|
|
55
82
|
flushTimer = null;
|
|
56
83
|
if (disposed) return;
|
|
84
|
+
// Heartbeat republish refreshes transport liveness only; lastEventAt stays
|
|
85
|
+
// pinned to the last real lifecycle event so stalled streams stay visible.
|
|
57
86
|
snapshot.updatedAt = now();
|
|
58
87
|
snapshot.repoLabel = formatRepoLabel(
|
|
59
88
|
snapshot.cwd,
|
|
60
89
|
pi?.getSessionName?.() ?? snapshot.sessionName,
|
|
61
90
|
);
|
|
62
91
|
snapshot.sessionName = compactWhitespace(pi?.getSessionName?.() ?? snapshot.sessionName);
|
|
92
|
+
const signature = publishSignature();
|
|
63
93
|
try {
|
|
64
|
-
await
|
|
94
|
+
await publish(snapshot);
|
|
95
|
+
confirmedSignature = signature;
|
|
96
|
+
retryIndex = 0;
|
|
65
97
|
} catch {
|
|
66
|
-
// Broker is optional at runtime; silent failure keeps pi stable.
|
|
98
|
+
// Broker is optional at runtime; silent failure keeps pi stable. Retry a
|
|
99
|
+
// bounded number of times when a state transition may not have landed so
|
|
100
|
+
// a transient connect failure cannot freeze the displayed state.
|
|
101
|
+
if (
|
|
102
|
+
signature !== confirmedSignature &&
|
|
103
|
+
retryIndex < ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS.length
|
|
104
|
+
) {
|
|
105
|
+
scheduleFlush(ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS[retryIndex]);
|
|
106
|
+
retryIndex += 1;
|
|
107
|
+
}
|
|
67
108
|
}
|
|
68
109
|
}
|
|
69
110
|
|
|
@@ -95,6 +136,7 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
|
|
|
95
136
|
...snapshot,
|
|
96
137
|
...partial,
|
|
97
138
|
updatedAt: now(),
|
|
139
|
+
lastEventAt: now(),
|
|
98
140
|
};
|
|
99
141
|
scheduleFlush();
|
|
100
142
|
}
|
|
@@ -185,8 +227,32 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
|
|
|
185
227
|
toolName,
|
|
186
228
|
});
|
|
187
229
|
},
|
|
188
|
-
|
|
230
|
+
/** @param {TurnEndEventLike} event */
|
|
231
|
+
onTurnEnd(event) {
|
|
189
232
|
if (!snapshot.agentActive) return;
|
|
233
|
+
const rawMessage = event?.message;
|
|
234
|
+
const message =
|
|
235
|
+
rawMessage && typeof rawMessage === "object"
|
|
236
|
+
? /** @type {Record<string, unknown>} */ (rawMessage)
|
|
237
|
+
: null;
|
|
238
|
+
const stopReason = String(message?.stopReason ?? "");
|
|
239
|
+
if (stopReason) lastStopReason = stopReason;
|
|
240
|
+
if (stopReason === "error") {
|
|
241
|
+
const errorText = previewText(message?.errorMessage, 104) || "Provider error";
|
|
242
|
+
update({
|
|
243
|
+
state: "error",
|
|
244
|
+
phase: "Needs attention",
|
|
245
|
+
detail: errorText,
|
|
246
|
+
errorMessage: errorText,
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (stopReason === "aborted") {
|
|
251
|
+
update({
|
|
252
|
+
detail: snapshot.assistantPreview || snapshot.detail || "Stopped",
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
190
256
|
update({
|
|
191
257
|
state: snapshot.errorMessage ? "error" : "thinking",
|
|
192
258
|
phase: snapshot.errorMessage ? "Needs attention" : "Thinking",
|
|
@@ -196,11 +262,12 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
|
|
|
196
262
|
onAgentSettled() {
|
|
197
263
|
const detail =
|
|
198
264
|
snapshot.errorMessage || snapshot.assistantPreview || snapshot.lastPromptPreview || "Done";
|
|
265
|
+
const aborted = !snapshot.errorMessage && lastStopReason === "aborted";
|
|
199
266
|
update({
|
|
200
267
|
agentActive: false,
|
|
201
268
|
agentStartedAt: null,
|
|
202
269
|
state: snapshot.errorMessage ? "error" : "success",
|
|
203
|
-
phase: snapshot.errorMessage ? "Stopped" : "Done",
|
|
270
|
+
phase: snapshot.errorMessage ? "Stopped" : aborted ? "Stopped" : "Done",
|
|
204
271
|
detail,
|
|
205
272
|
toolName: "",
|
|
206
273
|
toolTarget: "",
|
|
@@ -214,7 +281,10 @@ export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName =
|
|
|
214
281
|
flushTimer = null;
|
|
215
282
|
}
|
|
216
283
|
try {
|
|
217
|
-
await
|
|
284
|
+
await removePublisher({
|
|
285
|
+
sessionId: snapshot.sessionId,
|
|
286
|
+
publisherId: snapshot.publisherId,
|
|
287
|
+
});
|
|
218
288
|
} catch {
|
|
219
289
|
// ignore broker absence on shutdown
|
|
220
290
|
}
|
|
@@ -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
|
+
}
|
package/src/common/constants.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success"
|
|
|
8
8
|
|
|
9
9
|
export interface SessionSnapshot {
|
|
10
10
|
sessionId: string;
|
|
11
|
+
publisherId: string;
|
|
11
12
|
processId: number;
|
|
12
13
|
cwd: string;
|
|
13
14
|
repoLabel: string;
|
|
@@ -20,6 +21,7 @@ export interface SessionSnapshot {
|
|
|
20
21
|
state: SessionState;
|
|
21
22
|
turnIndex: number;
|
|
22
23
|
updatedAt: number;
|
|
24
|
+
lastEventAt: number;
|
|
23
25
|
startedAt: number;
|
|
24
26
|
agentStartedAt: number | null;
|
|
25
27
|
agentActive: boolean;
|
|
@@ -71,7 +73,7 @@ export interface ActivityStripBrokerOptions {
|
|
|
71
73
|
store?: {
|
|
72
74
|
snapshot(): BrokerSnapshot;
|
|
73
75
|
upsert(session: Partial<SessionSnapshot> | Record<string, unknown>): boolean;
|
|
74
|
-
remove(sessionId: string): boolean;
|
|
76
|
+
remove(sessionId: string, publisherId?: string): boolean;
|
|
75
77
|
};
|
|
76
78
|
getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
|
|
77
79
|
focusSession?: (sessionId: string) => Promise<{ ok: boolean; error?: string; windowId?: number }>;
|
|
@@ -115,6 +117,11 @@ export interface TurnStartEventLike {
|
|
|
115
117
|
turnIndex?: number;
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
export interface TurnEndEventLike {
|
|
121
|
+
turnIndex?: number;
|
|
122
|
+
message?: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
export interface AssistantMessageEventLike {
|
|
119
126
|
type?: string;
|
|
120
127
|
delta?: unknown;
|
|
@@ -136,6 +143,10 @@ export interface SessionTelemetryOptions {
|
|
|
136
143
|
pi?: TelemetryPiLike;
|
|
137
144
|
cwd?: string;
|
|
138
145
|
sessionName?: string;
|
|
146
|
+
transport?: {
|
|
147
|
+
publish: (session: SessionSnapshot) => Promise<unknown>;
|
|
148
|
+
remove: (session: { sessionId: string; publisherId: string }) => Promise<unknown>;
|
|
149
|
+
};
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
export interface ActivityStripCompatibilityReport {
|
package/src/common/contracts.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success"
|
|
|
8
8
|
|
|
9
9
|
export interface SessionSnapshot {
|
|
10
10
|
sessionId: string;
|
|
11
|
+
publisherId: string;
|
|
11
12
|
processId: number;
|
|
12
13
|
cwd: string;
|
|
13
14
|
repoLabel: string;
|
|
@@ -20,6 +21,7 @@ export interface SessionSnapshot {
|
|
|
20
21
|
state: SessionState;
|
|
21
22
|
turnIndex: number;
|
|
22
23
|
updatedAt: number;
|
|
24
|
+
lastEventAt: number;
|
|
23
25
|
startedAt: number;
|
|
24
26
|
agentStartedAt: number | null;
|
|
25
27
|
agentActive: boolean;
|
|
@@ -71,7 +73,7 @@ export interface ActivityStripBrokerOptions {
|
|
|
71
73
|
store?: {
|
|
72
74
|
snapshot(): BrokerSnapshot;
|
|
73
75
|
upsert(session: Partial<SessionSnapshot> | Record<string, unknown>): boolean;
|
|
74
|
-
remove(sessionId: string): boolean;
|
|
76
|
+
remove(sessionId: string, publisherId?: string): boolean;
|
|
75
77
|
};
|
|
76
78
|
getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
|
|
77
79
|
focusSession?: (sessionId: string) => Promise<{ ok: boolean; error?: string; windowId?: number }>;
|
|
@@ -115,6 +117,11 @@ export interface TurnStartEventLike {
|
|
|
115
117
|
turnIndex?: number;
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
export interface TurnEndEventLike {
|
|
121
|
+
turnIndex?: number;
|
|
122
|
+
message?: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
export interface AssistantMessageEventLike {
|
|
119
126
|
type?: string;
|
|
120
127
|
delta?: unknown;
|
|
@@ -136,6 +143,10 @@ export interface SessionTelemetryOptions {
|
|
|
136
143
|
pi?: TelemetryPiLike;
|
|
137
144
|
cwd?: string;
|
|
138
145
|
sessionName?: string;
|
|
146
|
+
transport?: {
|
|
147
|
+
publish: (session: SessionSnapshot) => Promise<unknown>;
|
|
148
|
+
remove: (session: { sessionId: string; publisherId: string }) => Promise<unknown>;
|
|
149
|
+
};
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
export interface ActivityStripCompatibilityReport {
|
package/src/common/protocol.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export function makeMessage(type, payload = {}) {
|
|
|
25
25
|
export function normalizeSessionSnapshot(session = {}) {
|
|
26
26
|
const now = Date.now();
|
|
27
27
|
const updatedAt = Number(session.updatedAt ?? now) || now;
|
|
28
|
+
const lastEventAt = Number(session.lastEventAt ?? updatedAt) || updatedAt;
|
|
28
29
|
const startedAt = Number(session.startedAt ?? updatedAt) || updatedAt;
|
|
29
30
|
const agentStartedAt =
|
|
30
31
|
session.agentStartedAt == null ? null : Number(session.agentStartedAt) || null;
|
|
@@ -36,6 +37,7 @@ export function normalizeSessionSnapshot(session = {}) {
|
|
|
36
37
|
|
|
37
38
|
return {
|
|
38
39
|
sessionId: String(session.sessionId ?? ""),
|
|
40
|
+
publisherId: String(session.publisherId ?? ""),
|
|
39
41
|
processId: Number(session.processId ?? 0) || 0,
|
|
40
42
|
cwd: String(session.cwd ?? ""),
|
|
41
43
|
repoLabel: String(session.repoLabel ?? "pi session"),
|
|
@@ -48,6 +50,7 @@ export function normalizeSessionSnapshot(session = {}) {
|
|
|
48
50
|
state,
|
|
49
51
|
turnIndex: Number(session.turnIndex ?? 0) || 0,
|
|
50
52
|
updatedAt,
|
|
53
|
+
lastEventAt,
|
|
51
54
|
startedAt,
|
|
52
55
|
agentStartedAt,
|
|
53
56
|
agentActive: Boolean(session.agentActive),
|
package/src/common/telemetry.mjs
CHANGED
|
@@ -67,15 +67,17 @@ export function createSessionId() {
|
|
|
67
67
|
return `${os.hostname()}-${process.pid}-${Date.now()}-${randomUUID().slice(0, 6)}`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/** @param {{ cwd?: string; sessionName?: string; sessionId?: string }} [options] @returns {SessionSnapshot} */
|
|
70
|
+
/** @param {{ cwd?: string; sessionName?: string; sessionId?: string; publisherId?: string }} [options] @returns {SessionSnapshot} */
|
|
71
71
|
export function createInitialSnapshot({
|
|
72
72
|
cwd = process.cwd(),
|
|
73
73
|
sessionName = "",
|
|
74
74
|
sessionId = "",
|
|
75
|
+
publisherId = "",
|
|
75
76
|
} = {}) {
|
|
76
77
|
const now = Date.now();
|
|
77
78
|
return {
|
|
78
79
|
sessionId: sessionId.trim() || createSessionId(),
|
|
80
|
+
publisherId: publisherId.trim() || randomUUID(),
|
|
79
81
|
processId: process.pid,
|
|
80
82
|
cwd,
|
|
81
83
|
repoLabel: formatRepoLabel(cwd, sessionName),
|
|
@@ -88,6 +90,7 @@ export function createInitialSnapshot({
|
|
|
88
90
|
state: "idle",
|
|
89
91
|
turnIndex: 0,
|
|
90
92
|
updatedAt: now,
|
|
93
|
+
lastEventAt: now,
|
|
91
94
|
startedAt: now,
|
|
92
95
|
agentStartedAt: null,
|
|
93
96
|
agentActive: false,
|
package/src/ui/strip-html.mjs
CHANGED
|
@@ -10,7 +10,15 @@ import {
|
|
|
10
10
|
moveOrderItem,
|
|
11
11
|
reconcileActivityOrder,
|
|
12
12
|
} from "../common/activity-order.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
disambiguatedRepoLabel,
|
|
15
|
+
findDuplicateLabels,
|
|
16
|
+
isStalledSession,
|
|
17
|
+
} from "../common/card-display.mjs";
|
|
18
|
+
import {
|
|
19
|
+
ACTIVITY_STRIP_EVENT_STALL_MS,
|
|
20
|
+
ACTIVITY_STRIP_ORDER_REFRESH_MS,
|
|
21
|
+
} from "../common/constants.mjs";
|
|
14
22
|
|
|
15
23
|
const ORDER_RUNTIME = [
|
|
16
24
|
'const ACTIVE_STATES = new Set(["thinking", "tool", "waiting"]);',
|
|
@@ -18,6 +26,9 @@ const ORDER_RUNTIME = [
|
|
|
18
26
|
isMonitoringSession.toString(),
|
|
19
27
|
reconcileActivityOrder.toString(),
|
|
20
28
|
moveOrderItem.toString(),
|
|
29
|
+
isStalledSession.toString(),
|
|
30
|
+
findDuplicateLabels.toString(),
|
|
31
|
+
disambiguatedRepoLabel.toString(),
|
|
21
32
|
].join("\n");
|
|
22
33
|
|
|
23
34
|
export function shouldRetainExpandedCard({ hovered, activeElement, documentFocused }) {
|
|
@@ -184,6 +195,7 @@ export function createStripHtml({ interactive = true, initiallyVisible = true }
|
|
|
184
195
|
.placeholder__title { color: var(--text); font-size: 14px; font-weight: 650; }
|
|
185
196
|
.placeholder__copy { color: var(--muted); font-size: 11px; line-height: 1.35; }
|
|
186
197
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
198
|
+
.card[data-stalled="true"] { opacity: 0.72; } .card[data-stalled="true"] .card__phase { text-decoration: line-through; }
|
|
187
199
|
|
|
188
200
|
@media (prefers-reduced-motion: reduce) {
|
|
189
201
|
.card { transition: none; }
|
|
@@ -208,6 +220,7 @@ export function createStripHtml({ interactive = true, initiallyVisible = true }
|
|
|
208
220
|
${ORDER_RUNTIME}
|
|
209
221
|
${INTERACTION_RUNTIME}
|
|
210
222
|
const ORDER_REFRESH_MS = ${ACTIVITY_STRIP_ORDER_REFRESH_MS};
|
|
223
|
+
const EVENT_STALL_MS = ${ACTIVITY_STRIP_EVENT_STALL_MS};
|
|
211
224
|
const meta = document.getElementById("meta");
|
|
212
225
|
const cards = document.getElementById("cards");
|
|
213
226
|
const announcer = document.getElementById("announcer");
|
|
@@ -236,7 +249,7 @@ export function createStripHtml({ interactive = true, initiallyVisible = true }
|
|
|
236
249
|
return formatDuration(Math.max(0, Math.floor((Date.now() - anchor) / 1000)));
|
|
237
250
|
}
|
|
238
251
|
function formatLastSeen(session) {
|
|
239
|
-
const anchor = Number(session.updatedAt || snapshot.generatedAt || Date.now());
|
|
252
|
+
const anchor = Number(session.lastEventAt || session.updatedAt || snapshot.generatedAt || Date.now());
|
|
240
253
|
return formatDuration(Math.max(0, Math.floor((Date.now() - anchor) / 1000)));
|
|
241
254
|
}
|
|
242
255
|
function sessionById(sessionId) {
|
|
@@ -271,11 +284,13 @@ export function createStripHtml({ interactive = true, initiallyVisible = true }
|
|
|
271
284
|
].join("");
|
|
272
285
|
return card;
|
|
273
286
|
}
|
|
274
|
-
function updateCard(card, session) {
|
|
275
|
-
const
|
|
276
|
-
const
|
|
287
|
+
function updateCard(card, session, duplicateLabels) {
|
|
288
|
+
const stalled = isStalledSession(session, Date.now(), EVENT_STALL_MS);
|
|
289
|
+
const stateColor = stalled ? "var(--waiting)" : STATE_COLORS[session.state] || "var(--accent)";
|
|
290
|
+
const stateLabel = stalled ? "stalled" : STATE_LABELS[session.state] || session.state || "idle";
|
|
277
291
|
const isCurrent = session.sessionId === snapshot.focusedSessionId;
|
|
278
292
|
card.style.setProperty("--state-color", stateColor);
|
|
293
|
+
card.dataset.stalled = stalled ? "true" : "false";
|
|
279
294
|
card.dataset.group = isMonitoringSession(session)
|
|
280
295
|
? "monitoring"
|
|
281
296
|
: isActiveSession(session)
|
|
@@ -284,9 +299,9 @@ export function createStripHtml({ interactive = true, initiallyVisible = true }
|
|
|
284
299
|
card.dataset.current = isCurrent ? "true" : "false";
|
|
285
300
|
if (isCurrent) card.setAttribute("aria-current", "true");
|
|
286
301
|
else card.removeAttribute("aria-current");
|
|
287
|
-
card.setAttribute("aria-label", (isCurrent ? "Current terminal, " : "") + (session.repoLabel || "Pi session") + ", " + (session.phase || "Idle") + ". Press Enter to focus its Ghostty window.");
|
|
302
|
+
card.setAttribute("aria-label", (stalled ? "Stalled, " : isCurrent ? "Current terminal, " : "") + (session.repoLabel || "Pi session") + ", " + (stalled ? "no events recently" : session.phase || "Idle") + ". Press Enter to focus its Ghostty window.");
|
|
288
303
|
card.title = (isCurrent ? "Current terminal · " : "Focus ") + (session.repoLabel || "Pi session");
|
|
289
|
-
setText(card, ".card__repo", session
|
|
304
|
+
setText(card, ".card__repo", disambiguatedRepoLabel(session, duplicateLabels));
|
|
290
305
|
setText(card, ".card__phase", session.phase || "Idle");
|
|
291
306
|
setText(card, ".card__state", stateLabel);
|
|
292
307
|
setText(card, ".tool", session.toolName || session.toolTarget || "monitoring");
|
|
@@ -332,6 +347,7 @@ export function createStripHtml({ interactive = true, initiallyVisible = true }
|
|
|
332
347
|
setExpanded(null, false).catch(() => {});
|
|
333
348
|
}
|
|
334
349
|
let targetIndex = 0;
|
|
350
|
+
const duplicateLabels = findDuplicateLabels(sessions);
|
|
335
351
|
for (const sessionId of orderedIds) {
|
|
336
352
|
const session = byId.get(sessionId);
|
|
337
353
|
if (!session) continue;
|
|
@@ -340,7 +356,7 @@ export function createStripHtml({ interactive = true, initiallyVisible = true }
|
|
|
340
356
|
card = cardTemplate(sessionId);
|
|
341
357
|
cardById.set(sessionId, card);
|
|
342
358
|
}
|
|
343
|
-
updateCard(card, session);
|
|
359
|
+
updateCard(card, session, duplicateLabels);
|
|
344
360
|
const nodeAtTarget = cards.children[targetIndex] ?? null;
|
|
345
361
|
if (nodeAtTarget !== card) cards.insertBefore(card, nodeAtTarget);
|
|
346
362
|
targetIndex += 1;
|