@tryinget/pi-activity-strip 0.2.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/LICENSE +78 -0
- package/README.md +203 -0
- package/bin/pi-activity-strip.mjs +226 -0
- package/examples/simulate-sessions.mjs +64 -0
- package/extensions/activity-strip.js +202 -0
- package/extensions/activity-strip.ts +1 -0
- package/package.json +94 -0
- package/policy/security-policy.json +10 -0
- package/prompts/pi-activity-strip-implementation-planning.md +21 -0
- package/prompts/pi-activity-strip-security-review.md +21 -0
- package/src/broker/server.mjs +165 -0
- package/src/broker/session-store.mjs +49 -0
- package/src/client/broker-client.mjs +115 -0
- package/src/client/launcher.mjs +116 -0
- package/src/client/session-telemetry.mjs +214 -0
- package/src/common/compatibility.mjs +153 -0
- package/src/common/constants.mjs +35 -0
- package/src/common/contracts.d.ts +142 -0
- package/src/common/contracts.ts +142 -0
- package/src/common/electron.mjs +51 -0
- package/src/common/format.mjs +75 -0
- package/src/common/protocol.mjs +71 -0
- package/src/common/status-report.mjs +58 -0
- package/src/common/telemetry.mjs +180 -0
- package/src/electron/main.mjs +319 -0
- package/src/electron/preload.cjs +10 -0
- package/src/ui/strip-html.mjs +375 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { assessActivityStripCompatibility } from "../common/compatibility.mjs";
|
|
4
|
+
import { ACTIVITY_STRIP_START_TIMEOUT_MS } from "../common/constants.mjs";
|
|
5
|
+
import { getBrokerStatus } from "./broker-client.mjs";
|
|
6
|
+
|
|
7
|
+
/** @param {import("../common/contracts.ts").BrokerResponse | null | undefined} result */
|
|
8
|
+
function isBrokerReady(result) {
|
|
9
|
+
return Boolean(result?.ok && (!result.runtimeStatus || result.runtimeStatus.state === "ready"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** @param {import("../common/contracts.ts").BrokerResponse | null | undefined} result */
|
|
13
|
+
function describeBrokerWait(result) {
|
|
14
|
+
if (!result?.ok) return "Activity strip broker did not answer.";
|
|
15
|
+
if (!result.runtimeStatus) {
|
|
16
|
+
return "Activity strip broker answered without overlay readiness metadata.";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const runtime = result.runtimeStatus;
|
|
20
|
+
const warnings =
|
|
21
|
+
Array.isArray(runtime.warnings) && runtime.warnings.length > 0
|
|
22
|
+
? ` Warnings: ${runtime.warnings.join(" ")}`
|
|
23
|
+
: "";
|
|
24
|
+
|
|
25
|
+
if (runtime.state === "error") {
|
|
26
|
+
return runtime.error || `Activity strip overlay reported an error.${warnings}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (runtime.state === "starting") {
|
|
30
|
+
return `Activity strip broker answered, but the overlay is still starting.${warnings}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `Activity strip broker is running (state=${runtime.state}).${warnings}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} binPath
|
|
38
|
+
* @param {{
|
|
39
|
+
* env?: NodeJS.ProcessEnv;
|
|
40
|
+
* timeoutMs?: number;
|
|
41
|
+
* getBrokerStatusImpl?: typeof getBrokerStatus;
|
|
42
|
+
* assessCompatibilityImpl?: typeof assessActivityStripCompatibility;
|
|
43
|
+
* spawnProcess?: (binPath: string, env: NodeJS.ProcessEnv) => void;
|
|
44
|
+
* }} [options]
|
|
45
|
+
*/
|
|
46
|
+
export async function ensureActivityStripRunning(binPath, options = {}) {
|
|
47
|
+
const mergedEnv = {
|
|
48
|
+
...process.env,
|
|
49
|
+
...options.env,
|
|
50
|
+
};
|
|
51
|
+
const getStatus = options.getBrokerStatusImpl || getBrokerStatus;
|
|
52
|
+
const assessCompatibility = options.assessCompatibilityImpl || assessActivityStripCompatibility;
|
|
53
|
+
|
|
54
|
+
let latestStatus = null;
|
|
55
|
+
try {
|
|
56
|
+
latestStatus = await getStatus({ timeoutMs: 250 });
|
|
57
|
+
} catch {
|
|
58
|
+
latestStatus = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isBrokerReady(latestStatus)) {
|
|
62
|
+
return { ok: true, started: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const compatibility = await assessCompatibility({ env: mergedEnv });
|
|
66
|
+
if (!compatibility.ok) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
started: false,
|
|
70
|
+
error: compatibility.blockers.join(" "),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let started = false;
|
|
75
|
+
if (!latestStatus?.ok) {
|
|
76
|
+
const spawnProcess =
|
|
77
|
+
options.spawnProcess ||
|
|
78
|
+
((targetBinPath, env) => {
|
|
79
|
+
const child = spawn(process.execPath, [targetBinPath, "open"], {
|
|
80
|
+
detached: true,
|
|
81
|
+
stdio: "ignore",
|
|
82
|
+
env,
|
|
83
|
+
});
|
|
84
|
+
child.unref();
|
|
85
|
+
});
|
|
86
|
+
spawnProcess(binPath, mergedEnv);
|
|
87
|
+
started = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const timeoutMs = options.timeoutMs ?? ACTIVITY_STRIP_START_TIMEOUT_MS;
|
|
91
|
+
const timeoutAt = Date.now() + timeoutMs;
|
|
92
|
+
while (Date.now() < timeoutAt) {
|
|
93
|
+
try {
|
|
94
|
+
latestStatus = await getStatus({ timeoutMs: 250 });
|
|
95
|
+
} catch {
|
|
96
|
+
latestStatus = null;
|
|
97
|
+
}
|
|
98
|
+
if (isBrokerReady(latestStatus)) {
|
|
99
|
+
return { ok: true, started };
|
|
100
|
+
}
|
|
101
|
+
if (latestStatus?.runtimeStatus?.state === "error") {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
started,
|
|
105
|
+
error: describeBrokerWait(latestStatus),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
await delay(125);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
started,
|
|
114
|
+
error: `${describeBrokerWait(latestStatus)} Timeout: ${timeoutMs}ms.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** @typedef {import("../common/contracts.ts").SessionSnapshot} SessionSnapshot */
|
|
2
|
+
/** @typedef {import("../common/contracts.ts").SessionTelemetryOptions} SessionTelemetryOptions */
|
|
3
|
+
/** @typedef {import("../common/contracts.ts").SessionStartContextLike} SessionStartContextLike */
|
|
4
|
+
/** @typedef {import("../common/contracts.ts").BeforeAgentStartEventLike} BeforeAgentStartEventLike */
|
|
5
|
+
/** @typedef {import("../common/contracts.ts").TurnStartEventLike} TurnStartEventLike */
|
|
6
|
+
/** @typedef {import("../common/contracts.ts").MessageUpdateEventLike} MessageUpdateEventLike */
|
|
7
|
+
/** @typedef {import("../common/contracts.ts").ToolExecutionEventLike} ToolExecutionEventLike */
|
|
8
|
+
import {
|
|
9
|
+
ACTIVITY_STRIP_HEARTBEAT_MS,
|
|
10
|
+
ACTIVITY_STRIP_SEND_THROTTLE_MS,
|
|
11
|
+
} from "../common/constants.mjs";
|
|
12
|
+
import {
|
|
13
|
+
compactWhitespace,
|
|
14
|
+
formatRepoLabel,
|
|
15
|
+
previewPath,
|
|
16
|
+
previewText,
|
|
17
|
+
truncate,
|
|
18
|
+
} from "../common/format.mjs";
|
|
19
|
+
import {
|
|
20
|
+
createInitialSnapshot,
|
|
21
|
+
describeToolCall,
|
|
22
|
+
extractToolResultText,
|
|
23
|
+
summarizeToolResult,
|
|
24
|
+
} from "../common/telemetry.mjs";
|
|
25
|
+
import { publishSessionSnapshot, removeSession } from "./broker-client.mjs";
|
|
26
|
+
|
|
27
|
+
function now() {
|
|
28
|
+
return Date.now();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {MessageUpdateEventLike} event */
|
|
32
|
+
function extractAssistantDelta(event) {
|
|
33
|
+
if (event?.assistantMessageEvent?.type !== "text_delta") return "";
|
|
34
|
+
return String(event.assistantMessageEvent.delta ?? "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {SessionTelemetryOptions} [options] */
|
|
38
|
+
export function createSessionTelemetry({ pi, cwd = process.cwd(), sessionName = "" } = {}) {
|
|
39
|
+
/** @type {SessionSnapshot} */
|
|
40
|
+
let snapshot = createInitialSnapshot({ cwd, sessionName });
|
|
41
|
+
/** @type {NodeJS.Timeout | null} */
|
|
42
|
+
let heartbeatTimer = null;
|
|
43
|
+
/** @type {NodeJS.Timeout | null} */
|
|
44
|
+
let flushTimer = null;
|
|
45
|
+
let disposed = false;
|
|
46
|
+
let pendingAssistant = "";
|
|
47
|
+
|
|
48
|
+
async function flush() {
|
|
49
|
+
flushTimer = null;
|
|
50
|
+
if (disposed) return;
|
|
51
|
+
snapshot.updatedAt = now();
|
|
52
|
+
snapshot.repoLabel = formatRepoLabel(
|
|
53
|
+
snapshot.cwd,
|
|
54
|
+
pi?.getSessionName?.() ?? snapshot.sessionName,
|
|
55
|
+
);
|
|
56
|
+
snapshot.sessionName = compactWhitespace(pi?.getSessionName?.() ?? snapshot.sessionName);
|
|
57
|
+
try {
|
|
58
|
+
await publishSessionSnapshot(snapshot);
|
|
59
|
+
} catch {
|
|
60
|
+
// Broker is optional at runtime; silent failure keeps pi stable.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scheduleFlush(delayMs = ACTIVITY_STRIP_SEND_THROTTLE_MS) {
|
|
65
|
+
if (disposed) return;
|
|
66
|
+
if (flushTimer) return;
|
|
67
|
+
flushTimer = setTimeout(() => {
|
|
68
|
+
flush().catch(() => {});
|
|
69
|
+
}, delayMs);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function startHeartbeat() {
|
|
73
|
+
if (heartbeatTimer) return;
|
|
74
|
+
heartbeatTimer = setInterval(() => {
|
|
75
|
+
scheduleFlush(0);
|
|
76
|
+
}, ACTIVITY_STRIP_HEARTBEAT_MS);
|
|
77
|
+
heartbeatTimer.unref?.();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stopHeartbeat() {
|
|
81
|
+
if (!heartbeatTimer) return;
|
|
82
|
+
clearInterval(heartbeatTimer);
|
|
83
|
+
heartbeatTimer = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @param {Partial<SessionSnapshot>} partial */
|
|
87
|
+
function update(partial) {
|
|
88
|
+
snapshot = {
|
|
89
|
+
...snapshot,
|
|
90
|
+
...partial,
|
|
91
|
+
updatedAt: now(),
|
|
92
|
+
};
|
|
93
|
+
scheduleFlush();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
getSnapshot() {
|
|
98
|
+
return snapshot;
|
|
99
|
+
},
|
|
100
|
+
/** @param {SessionStartContextLike} ctx */
|
|
101
|
+
async onSessionStart(ctx) {
|
|
102
|
+
update({
|
|
103
|
+
cwd: ctx?.cwd ?? snapshot.cwd,
|
|
104
|
+
sessionName: pi?.getSessionName?.() ?? snapshot.sessionName,
|
|
105
|
+
detail: previewPath(ctx?.cwd ?? snapshot.cwd, 72) || "Ready",
|
|
106
|
+
});
|
|
107
|
+
startHeartbeat();
|
|
108
|
+
await flush();
|
|
109
|
+
},
|
|
110
|
+
/** @param {BeforeAgentStartEventLike} event */
|
|
111
|
+
onBeforeAgentStart(event) {
|
|
112
|
+
pendingAssistant = "";
|
|
113
|
+
update({
|
|
114
|
+
agentActive: true,
|
|
115
|
+
agentStartedAt: now(),
|
|
116
|
+
state: "thinking",
|
|
117
|
+
phase: "Thinking",
|
|
118
|
+
lastPromptPreview: previewText(event?.prompt, 96),
|
|
119
|
+
detail: previewText(event?.prompt, 104) || "Thinking…",
|
|
120
|
+
errorMessage: "",
|
|
121
|
+
toolName: "",
|
|
122
|
+
toolTarget: "",
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
/** @param {TurnStartEventLike} event */
|
|
126
|
+
onTurnStart(event) {
|
|
127
|
+
update({
|
|
128
|
+
turnIndex: Number(event?.turnIndex ?? snapshot.turnIndex + 1) || snapshot.turnIndex + 1,
|
|
129
|
+
state: snapshot.agentActive ? snapshot.state : "thinking",
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
/** @param {MessageUpdateEventLike} event */
|
|
133
|
+
onMessageUpdate(event) {
|
|
134
|
+
const delta = extractAssistantDelta(event);
|
|
135
|
+
if (!delta) return;
|
|
136
|
+
pendingAssistant = truncate(`${pendingAssistant}${delta}`, 240);
|
|
137
|
+
if (snapshot.state === "tool" || snapshot.state === "waiting") return;
|
|
138
|
+
update({
|
|
139
|
+
state: "thinking",
|
|
140
|
+
phase: "Thinking",
|
|
141
|
+
assistantPreview: previewText(pendingAssistant, 104),
|
|
142
|
+
detail: previewText(pendingAssistant, 104) || snapshot.detail,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
/** @param {ToolExecutionEventLike} event */
|
|
146
|
+
onToolExecutionStart(event) {
|
|
147
|
+
const toolName = String(event?.toolName ?? "tool");
|
|
148
|
+
const description = describeToolCall(toolName, event?.args ?? {});
|
|
149
|
+
update({
|
|
150
|
+
state: description.state,
|
|
151
|
+
phase: description.phase,
|
|
152
|
+
detail: description.detail,
|
|
153
|
+
toolName,
|
|
154
|
+
toolTarget: description.toolTarget,
|
|
155
|
+
errorMessage: "",
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
/** @param {ToolExecutionEventLike} event */
|
|
159
|
+
onToolExecutionUpdate(event) {
|
|
160
|
+
if (snapshot.state !== "tool" && snapshot.state !== "waiting") return;
|
|
161
|
+
const partial = previewText(extractToolResultText(event?.partialResult), 104);
|
|
162
|
+
if (!partial) return;
|
|
163
|
+
update({
|
|
164
|
+
detail: partial,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
/** @param {ToolExecutionEventLike} event */
|
|
168
|
+
onToolExecutionEnd(event) {
|
|
169
|
+
const toolName = String(event?.toolName ?? snapshot.toolName ?? "tool");
|
|
170
|
+
const summary = summarizeToolResult(toolName, event?.result, Boolean(event?.isError));
|
|
171
|
+
update({
|
|
172
|
+
state: summary.state,
|
|
173
|
+
phase: summary.phase,
|
|
174
|
+
detail: summary.detail,
|
|
175
|
+
errorMessage: summary.errorMessage,
|
|
176
|
+
toolName,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
onTurnEnd() {
|
|
180
|
+
if (!snapshot.agentActive) return;
|
|
181
|
+
update({
|
|
182
|
+
state: snapshot.errorMessage ? "error" : "thinking",
|
|
183
|
+
phase: snapshot.errorMessage ? "Needs attention" : "Thinking",
|
|
184
|
+
detail: snapshot.assistantPreview || snapshot.detail || "Thinking…",
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
onAgentSettled() {
|
|
188
|
+
const detail =
|
|
189
|
+
snapshot.errorMessage || snapshot.assistantPreview || snapshot.lastPromptPreview || "Done";
|
|
190
|
+
update({
|
|
191
|
+
agentActive: false,
|
|
192
|
+
agentStartedAt: null,
|
|
193
|
+
state: snapshot.errorMessage ? "error" : "success",
|
|
194
|
+
phase: snapshot.errorMessage ? "Stopped" : "Done",
|
|
195
|
+
detail,
|
|
196
|
+
toolName: "",
|
|
197
|
+
toolTarget: "",
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
async shutdown() {
|
|
201
|
+
disposed = true;
|
|
202
|
+
stopHeartbeat();
|
|
203
|
+
if (flushTimer) {
|
|
204
|
+
clearTimeout(flushTimer);
|
|
205
|
+
flushTimer = null;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
await removeSession(snapshot.sessionId);
|
|
209
|
+
} catch {
|
|
210
|
+
// ignore broker absence on shutdown
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { locateElectron } from "./electron.mjs";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
/** @param {NodeJS.ProcessEnv} [env] */
|
|
8
|
+
export function detectDisplayServer(env = process.env) {
|
|
9
|
+
if (env.WAYLAND_DISPLAY) return "wayland";
|
|
10
|
+
if (env.DISPLAY) return "x11";
|
|
11
|
+
if (env.XDG_SESSION_TYPE === "wayland") return "wayland";
|
|
12
|
+
if (env.XDG_SESSION_TYPE === "x11") return "x11";
|
|
13
|
+
return "headless";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {NodeJS.ProcessEnv} [env] */
|
|
17
|
+
export function detectWindowManager(env = process.env) {
|
|
18
|
+
if (env.NIRI_SOCKET) return "niri";
|
|
19
|
+
return (
|
|
20
|
+
env.XDG_CURRENT_DESKTOP ||
|
|
21
|
+
env.XDG_SESSION_DESKTOP ||
|
|
22
|
+
env.DESKTOP_SESSION ||
|
|
23
|
+
env.SWAYSOCK ||
|
|
24
|
+
null
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {unknown} payload
|
|
30
|
+
* @returns {number | null}
|
|
31
|
+
*/
|
|
32
|
+
function inferDisplayCount(payload) {
|
|
33
|
+
if (Array.isArray(payload)) return payload.length;
|
|
34
|
+
if (payload && typeof payload === "object") {
|
|
35
|
+
const values = Object.values(payload);
|
|
36
|
+
if (values.every((entry) => entry && typeof entry === "object")) {
|
|
37
|
+
return values.length;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{ env?: NodeJS.ProcessEnv; execFileAsyncImpl?: typeof execFileAsync }} [options]
|
|
45
|
+
*/
|
|
46
|
+
export async function detectDisplayCount(options = {}) {
|
|
47
|
+
const env = options.env || process.env;
|
|
48
|
+
const execFileImpl = options.execFileAsyncImpl || execFileAsync;
|
|
49
|
+
if (!env.NIRI_SOCKET) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execFileImpl("niri", ["msg", "-j", "outputs"], { env });
|
|
55
|
+
return inferDisplayCount(JSON.parse(stdout));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {{
|
|
63
|
+
* env?: NodeJS.ProcessEnv;
|
|
64
|
+
* locateElectronImpl?: typeof locateElectron;
|
|
65
|
+
* execFileAsyncImpl?: typeof execFileAsync;
|
|
66
|
+
* }} [options]
|
|
67
|
+
* @returns {Promise<import("./contracts.ts").ActivityStripCompatibilityReport>}
|
|
68
|
+
*/
|
|
69
|
+
export async function assessActivityStripCompatibility(options = {}) {
|
|
70
|
+
const env = options.env || process.env;
|
|
71
|
+
const locateElectronImpl = options.locateElectronImpl || locateElectron;
|
|
72
|
+
const displayServer = detectDisplayServer(env);
|
|
73
|
+
const windowManager = detectWindowManager(env);
|
|
74
|
+
const displayCount = await detectDisplayCount({
|
|
75
|
+
env,
|
|
76
|
+
execFileAsyncImpl: options.execFileAsyncImpl,
|
|
77
|
+
});
|
|
78
|
+
const blockers = [];
|
|
79
|
+
const warnings = [];
|
|
80
|
+
let electronPath = null;
|
|
81
|
+
|
|
82
|
+
if (displayServer === "headless") {
|
|
83
|
+
blockers.push(
|
|
84
|
+
"No graphical display session detected. Start Pi from a Wayland/X11 desktop before opening the activity strip.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
electronPath = await locateElectronImpl();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (displayCount && displayCount > 1) {
|
|
95
|
+
warnings.push(
|
|
96
|
+
`Detected ${displayCount} displays; the strip currently renders on the primary display only.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (windowManager !== "niri") {
|
|
101
|
+
warnings.push(
|
|
102
|
+
"Top-edge repair is optimized for Niri. Other window managers fall back to generic Electron bounds and may need manual adjustment.",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ok: blockers.length === 0,
|
|
108
|
+
displayServer,
|
|
109
|
+
windowManager: typeof windowManager === "string" ? windowManager : null,
|
|
110
|
+
electronPath,
|
|
111
|
+
displayCount,
|
|
112
|
+
alignmentMode: windowManager === "niri" ? "niri" : "generic",
|
|
113
|
+
primaryDisplayOnly: true,
|
|
114
|
+
clickThroughDefault: env.PI_ACTIVITY_STRIP_CLICK_THROUGH !== "0",
|
|
115
|
+
blockers,
|
|
116
|
+
warnings,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {import("./contracts.ts").ActivityStripCompatibilityReport} report
|
|
122
|
+
*/
|
|
123
|
+
export function formatCompatibilityReport(report) {
|
|
124
|
+
const lines = [
|
|
125
|
+
`Compatibility: ${report.ok ? "compatible" : "blocked"}`,
|
|
126
|
+
`Display server: ${report.displayServer}`,
|
|
127
|
+
`Window manager: ${report.windowManager || "unknown"}`,
|
|
128
|
+
`Electron: ${report.electronPath || "not found"}`,
|
|
129
|
+
`Alignment mode: ${report.alignmentMode}`,
|
|
130
|
+
`Primary-display only: ${report.primaryDisplayOnly ? "yes" : "no"}`,
|
|
131
|
+
`Click-through default: ${report.clickThroughDefault ? "yes" : "no"}`,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
if (typeof report.displayCount === "number") {
|
|
135
|
+
lines.push(`Detected displays: ${report.displayCount}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (report.blockers.length > 0) {
|
|
139
|
+
lines.push("", "Blockers:");
|
|
140
|
+
for (const blocker of report.blockers) {
|
|
141
|
+
lines.push(`- ${blocker}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (report.warnings.length > 0) {
|
|
146
|
+
lines.push("", "Warnings:");
|
|
147
|
+
for (const warning of report.warnings) {
|
|
148
|
+
lines.push(`- ${warning}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const ACTIVITY_STRIP_NAME = "pi-activity-strip";
|
|
5
|
+
export const ACTIVITY_STRIP_SOCKET_DIR = path.join(
|
|
6
|
+
os.homedir(),
|
|
7
|
+
".pi",
|
|
8
|
+
"agent",
|
|
9
|
+
"state",
|
|
10
|
+
ACTIVITY_STRIP_NAME,
|
|
11
|
+
);
|
|
12
|
+
export const ACTIVITY_STRIP_SOCKET_PATH = path.join(
|
|
13
|
+
ACTIVITY_STRIP_SOCKET_DIR,
|
|
14
|
+
"activity-strip.sock",
|
|
15
|
+
);
|
|
16
|
+
export const ACTIVITY_STRIP_WIDTH_PADDING = 16;
|
|
17
|
+
export const ACTIVITY_STRIP_HEIGHT = 84;
|
|
18
|
+
export const ACTIVITY_STRIP_HEARTBEAT_MS = 2500;
|
|
19
|
+
export const ACTIVITY_STRIP_STALE_AFTER_MS = 12000;
|
|
20
|
+
export const ACTIVITY_STRIP_BROADCAST_TICK_MS = 1000;
|
|
21
|
+
export const ACTIVITY_STRIP_SEND_THROTTLE_MS = 120;
|
|
22
|
+
export const ACTIVITY_STRIP_CONNECT_TIMEOUT_MS = 450;
|
|
23
|
+
export const ACTIVITY_STRIP_START_TIMEOUT_MS = 5000;
|
|
24
|
+
export const DEFAULT_ELECTRON_CANDIDATES = [
|
|
25
|
+
"electron",
|
|
26
|
+
"electron39",
|
|
27
|
+
"electron38",
|
|
28
|
+
"electron37",
|
|
29
|
+
"electron36",
|
|
30
|
+
"electron35",
|
|
31
|
+
"electron34",
|
|
32
|
+
"electron33",
|
|
33
|
+
"electron32",
|
|
34
|
+
"electron31",
|
|
35
|
+
];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success" | "error";
|
|
2
|
+
|
|
3
|
+
export interface SessionSnapshot {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
processId: number;
|
|
6
|
+
cwd: string;
|
|
7
|
+
repoLabel: string;
|
|
8
|
+
sessionName: string;
|
|
9
|
+
phase: string;
|
|
10
|
+
detail: string;
|
|
11
|
+
assistantPreview: string;
|
|
12
|
+
toolName: string;
|
|
13
|
+
toolTarget: string;
|
|
14
|
+
state: SessionState;
|
|
15
|
+
turnIndex: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
agentStartedAt: number | null;
|
|
19
|
+
agentActive: boolean;
|
|
20
|
+
lastPromptPreview: string;
|
|
21
|
+
errorMessage: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BrokerSnapshot {
|
|
25
|
+
generatedAt: number;
|
|
26
|
+
sessions: SessionSnapshot[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ActivityStripRuntimeState = "starting" | "ready" | "error";
|
|
30
|
+
|
|
31
|
+
export interface ActivityStripRuntimeStatus {
|
|
32
|
+
state: ActivityStripRuntimeState;
|
|
33
|
+
startedAt: number;
|
|
34
|
+
readyAt?: number | null;
|
|
35
|
+
windowVisible?: boolean;
|
|
36
|
+
displayServer?: "wayland" | "x11" | "headless" | "unknown";
|
|
37
|
+
windowManager?: string | null;
|
|
38
|
+
displayCount?: number | null;
|
|
39
|
+
alignmentMode?: "niri" | "generic";
|
|
40
|
+
warnings?: string[];
|
|
41
|
+
error?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BrokerResponse {
|
|
45
|
+
ok?: boolean;
|
|
46
|
+
type?: string;
|
|
47
|
+
snapshot?: BrokerSnapshot;
|
|
48
|
+
runtimeStatus?: ActivityStripRuntimeStatus;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BrokerClientOptions {
|
|
53
|
+
expectReply?: boolean;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
socketPath?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SessionStoreOptions {
|
|
59
|
+
staleAfterMs?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ActivityStripBrokerOptions {
|
|
63
|
+
socketPath?: string;
|
|
64
|
+
socketDir?: string;
|
|
65
|
+
store?: {
|
|
66
|
+
snapshot(): BrokerSnapshot;
|
|
67
|
+
upsert(session: Partial<SessionSnapshot> | Record<string, unknown>): boolean;
|
|
68
|
+
remove(sessionId: string): boolean;
|
|
69
|
+
};
|
|
70
|
+
getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ToolCallDescription {
|
|
74
|
+
state: "tool" | "waiting";
|
|
75
|
+
phase: string;
|
|
76
|
+
detail: string;
|
|
77
|
+
toolTarget?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ToolResultSummary {
|
|
81
|
+
state: "thinking" | "error";
|
|
82
|
+
phase: string;
|
|
83
|
+
detail: string;
|
|
84
|
+
errorMessage?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TelemetryPiLike {
|
|
88
|
+
getSessionName?: () => string | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SessionStartContextLike {
|
|
92
|
+
cwd?: string;
|
|
93
|
+
hasUI?: boolean;
|
|
94
|
+
ui?: {
|
|
95
|
+
notify?: (message: string, level?: "info" | "warning" | "error") => void;
|
|
96
|
+
editor?: (title: string, text: string) => Promise<unknown>;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BeforeAgentStartEventLike {
|
|
101
|
+
prompt?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TurnStartEventLike {
|
|
105
|
+
turnIndex?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface AssistantMessageEventLike {
|
|
109
|
+
type?: string;
|
|
110
|
+
delta?: unknown;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface MessageUpdateEventLike {
|
|
114
|
+
assistantMessageEvent?: AssistantMessageEventLike;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ToolExecutionEventLike {
|
|
118
|
+
toolName?: string;
|
|
119
|
+
args?: Record<string, unknown>;
|
|
120
|
+
partialResult?: unknown;
|
|
121
|
+
result?: unknown;
|
|
122
|
+
isError?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SessionTelemetryOptions {
|
|
126
|
+
pi?: TelemetryPiLike;
|
|
127
|
+
cwd?: string;
|
|
128
|
+
sessionName?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ActivityStripCompatibilityReport {
|
|
132
|
+
ok: boolean;
|
|
133
|
+
displayServer: "wayland" | "x11" | "headless" | "unknown";
|
|
134
|
+
windowManager: string | null;
|
|
135
|
+
electronPath: string | null;
|
|
136
|
+
displayCount: number | null;
|
|
137
|
+
alignmentMode: "niri" | "generic";
|
|
138
|
+
primaryDisplayOnly: boolean;
|
|
139
|
+
clickThroughDefault: boolean;
|
|
140
|
+
blockers: string[];
|
|
141
|
+
warnings: string[];
|
|
142
|
+
}
|