@tryinget/pi-activity-strip 0.4.0 → 0.6.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 +136 -157
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +50 -60
- 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 +602 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +297 -0
- package/native/panel/src/runtime.rs +151 -0
- package/native/panel/src/style.css +250 -0
- package/package.json +16 -4
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +41 -13
- package/src/client/broker-client.mjs +10 -3
- package/src/client/session-telemetry.mjs +54 -14
- package/src/common/activity-order.mjs +15 -9
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +5 -0
- package/src/common/claude-events.mjs +190 -0
- package/src/common/claude-hook-config.mjs +56 -0
- package/src/common/claude-transcript.mjs +163 -0
- package/src/common/codex-transcript.mjs +153 -0
- package/src/common/compatibility.mjs +28 -13
- package/src/common/constants.mjs +10 -28
- package/src/common/contracts.d.ts +68 -4
- package/src/common/contracts.ts +105 -4
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +209 -140
- package/src/common/protocol.mjs +48 -16
- package/src/common/session-cards.mjs +105 -0
- package/src/common/status-report.mjs +53 -1
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/telemetry.mjs +8 -1
- package/src/common/terminal-identity.mjs +212 -0
- package/src/common/window-placement.mjs +59 -0
- package/src/common/workspace-view.mjs +161 -0
- package/src/native/agent-discovery.mjs +294 -0
- package/src/native/ak-runtime.mjs +164 -0
- package/src/native/codex-discovery.mjs +128 -0
- package/src/native/ghostty-tab-inventory.py +99 -0
- package/src/native/height-repair.mjs +154 -0
- package/src/native/main.mjs +499 -0
- package/src/native/panel-projection.mjs +161 -0
- package/src/native/placement.mjs +225 -0
- package/src/native/tab-inventory.mjs +188 -0
- package/src/native/theme-runtime.mjs +72 -0
- package/src/native/theme-source.mjs +197 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
- 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 -500
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "builds the Claude Code settings fragment that lets sessions publish live ribbon state"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing which Claude Code hook events the ribbon subscribes to"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Only low-frequency events are hooked. Tool, thinking and idle state already come from the
|
|
9
|
+
* transcript at no cost, so nothing runs per tool call; hooks add exactly what the transcript
|
|
10
|
+
* cannot express, which is that a session is blocked waiting for the operator.
|
|
11
|
+
*/
|
|
12
|
+
export const CLAUDE_HOOK_EVENTS = Object.freeze([
|
|
13
|
+
"SessionStart",
|
|
14
|
+
"UserPromptSubmit",
|
|
15
|
+
"Notification",
|
|
16
|
+
"Stop",
|
|
17
|
+
"SessionEnd",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} hookCommand absolute command Claude Code runs for each event
|
|
22
|
+
* @returns {{hooks: Record<string, Array<{matcher: string; hooks: Array<{type: "command"; command: string}>}>>}}
|
|
23
|
+
*/
|
|
24
|
+
export function claudeHookSettings(hookCommand) {
|
|
25
|
+
const command = String(hookCommand ?? "").trim();
|
|
26
|
+
if (!command) throw new Error("A hook command is required.");
|
|
27
|
+
/** @type {Record<string, Array<{matcher: string; hooks: Array<{type: "command"; command: string}>}>>} */
|
|
28
|
+
const hooks = {};
|
|
29
|
+
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
30
|
+
hooks[event] = [{ matcher: "", hooks: [{ type: "command", command }] }];
|
|
31
|
+
}
|
|
32
|
+
return { hooks };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merge the ribbon's hooks into existing settings without disturbing unrelated configuration or
|
|
37
|
+
* duplicating an entry that already runs this exact command.
|
|
38
|
+
* @param {Record<string, any>} settings
|
|
39
|
+
* @param {string} hookCommand
|
|
40
|
+
*/
|
|
41
|
+
export function mergeClaudeHookSettings(settings, hookCommand) {
|
|
42
|
+
const desired = claudeHookSettings(hookCommand);
|
|
43
|
+
const merged = { ...(settings ?? {}) };
|
|
44
|
+
const existingHooks = { ...(merged.hooks ?? {}) };
|
|
45
|
+
for (const [event, entries] of Object.entries(desired.hooks)) {
|
|
46
|
+
const current = Array.isArray(existingHooks[event]) ? [...existingHooks[event]] : [];
|
|
47
|
+
const alreadyPresent = current.some((entry) =>
|
|
48
|
+
(entry?.hooks ?? []).some(
|
|
49
|
+
/** @param {any} hook */ (hook) => String(hook?.command ?? "") === hookCommand,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
existingHooks[event] = alreadyPresent ? current : [...current, ...entries];
|
|
53
|
+
}
|
|
54
|
+
merged.hooks = existingHooks;
|
|
55
|
+
return merged;
|
|
56
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "derives live Claude Code session state from the tail of its transcript"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing Claude Code card telemetry or the transcript records it depends on"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Claude Code appends newline-delimited records to a per-session transcript. The format is internal
|
|
9
|
+
* to Claude Code and carries no stability guarantee, so every field is optional here and a record
|
|
10
|
+
* that does not parse is skipped. When nothing usable is found the caller keeps the process-only
|
|
11
|
+
* card rather than inventing activity.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const PREVIEW_LIMIT = 100;
|
|
15
|
+
const DETAIL_LIMIT = 100;
|
|
16
|
+
|
|
17
|
+
/** @typedef {"idle" | "thinking" | "tool"} ClaudeState */
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* title: string; lastPrompt: string; assistantPreview: string; state: ClaudeState;
|
|
21
|
+
* toolName: string; toolTarget: string; turnIndex: number; lastEventAt: number;
|
|
22
|
+
* mode: string; permissionMode: string; sessionId: string; cwd: string; gitBranch: string;
|
|
23
|
+
* }} ClaudeTelemetry
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @param {unknown} value @param {number} limit */
|
|
27
|
+
function preview(value, limit) {
|
|
28
|
+
const text = String(value ?? "")
|
|
29
|
+
.replace(/\s+/g, " ")
|
|
30
|
+
.trim();
|
|
31
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {unknown} value */
|
|
35
|
+
function timestampMs(value) {
|
|
36
|
+
const parsed = Date.parse(String(value ?? ""));
|
|
37
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {any} record */
|
|
41
|
+
function contentBlocks(record) {
|
|
42
|
+
const content = record?.message?.content;
|
|
43
|
+
return Array.isArray(content) ? content : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Name the thing a tool call is acting on, preferring the fields that read well on a card.
|
|
48
|
+
* @param {any} block
|
|
49
|
+
*/
|
|
50
|
+
export function describeToolTarget(block) {
|
|
51
|
+
const input = block?.input ?? {};
|
|
52
|
+
for (const key of ["description", "command", "file_path", "path", "pattern", "query", "url"]) {
|
|
53
|
+
const value = input?.[key];
|
|
54
|
+
if (typeof value === "string" && value.trim()) return preview(value, DETAIL_LIMIT);
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @returns {ClaudeTelemetry} */
|
|
60
|
+
function emptyTelemetry() {
|
|
61
|
+
return {
|
|
62
|
+
title: "",
|
|
63
|
+
lastPrompt: "",
|
|
64
|
+
assistantPreview: "",
|
|
65
|
+
state: "idle",
|
|
66
|
+
toolName: "",
|
|
67
|
+
toolTarget: "",
|
|
68
|
+
turnIndex: 0,
|
|
69
|
+
lastEventAt: 0,
|
|
70
|
+
mode: "",
|
|
71
|
+
permissionMode: "",
|
|
72
|
+
sessionId: "",
|
|
73
|
+
cwd: "",
|
|
74
|
+
gitBranch: "",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read the tail of a transcript. The first line is usually a fragment and is skipped by the parse
|
|
80
|
+
* failure path. State comes from the newest record that carries a timestamp, because the latched
|
|
81
|
+
* records Claude Code rewrites after every turn have none and would otherwise mask real activity.
|
|
82
|
+
* @param {string} tail
|
|
83
|
+
* @returns {ClaudeTelemetry}
|
|
84
|
+
*/
|
|
85
|
+
export function parseClaudeTranscriptTail(tail) {
|
|
86
|
+
const telemetry = emptyTelemetry();
|
|
87
|
+
if (!tail) return telemetry;
|
|
88
|
+
|
|
89
|
+
/** @type {any[]} */
|
|
90
|
+
const records = [];
|
|
91
|
+
for (const line of String(tail).split("\n")) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed.startsWith("{")) continue;
|
|
94
|
+
try {
|
|
95
|
+
records.push(JSON.parse(trimmed));
|
|
96
|
+
} catch {
|
|
97
|
+
// A truncated first line or a partially written last line is not evidence.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (records.length === 0) return telemetry;
|
|
101
|
+
|
|
102
|
+
for (const record of records) {
|
|
103
|
+
switch (record?.type) {
|
|
104
|
+
case "ai-title":
|
|
105
|
+
if (typeof record.aiTitle === "string") telemetry.title = record.aiTitle;
|
|
106
|
+
break;
|
|
107
|
+
case "last-prompt":
|
|
108
|
+
telemetry.lastPrompt = preview(record.lastPrompt, PREVIEW_LIMIT);
|
|
109
|
+
break;
|
|
110
|
+
case "mode":
|
|
111
|
+
telemetry.mode = String(record.mode ?? "");
|
|
112
|
+
break;
|
|
113
|
+
case "permission-mode":
|
|
114
|
+
telemetry.permissionMode = String(record.permissionMode ?? "");
|
|
115
|
+
break;
|
|
116
|
+
case "system":
|
|
117
|
+
if (record.subtype === "turn_duration" && Number.isFinite(Number(record.messageCount))) {
|
|
118
|
+
telemetry.turnIndex = Number(record.messageCount);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (typeof record?.sessionId === "string") telemetry.sessionId = record.sessionId;
|
|
125
|
+
if (typeof record?.cwd === "string") telemetry.cwd = record.cwd;
|
|
126
|
+
if (typeof record?.gitBranch === "string") telemetry.gitBranch = record.gitBranch;
|
|
127
|
+
if (record?.type === "assistant") {
|
|
128
|
+
const text = contentBlocks(record)
|
|
129
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
130
|
+
.map((block) => block.text)
|
|
131
|
+
.join(" ");
|
|
132
|
+
if (text.trim()) telemetry.assistantPreview = preview(text, PREVIEW_LIMIT);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const timed = records.filter(
|
|
137
|
+
(record) => record?.type !== "attachment" && timestampMs(record?.timestamp) > 0,
|
|
138
|
+
);
|
|
139
|
+
const latest = timed.at(-1);
|
|
140
|
+
if (!latest) return telemetry;
|
|
141
|
+
telemetry.lastEventAt = timestampMs(latest.timestamp);
|
|
142
|
+
|
|
143
|
+
if (latest.type === "system" && latest.subtype === "turn_duration") {
|
|
144
|
+
telemetry.state = "idle";
|
|
145
|
+
return telemetry;
|
|
146
|
+
}
|
|
147
|
+
if (latest.type === "assistant") {
|
|
148
|
+
const toolBlock = contentBlocks(latest).find((block) => block?.type === "tool_use");
|
|
149
|
+
if (toolBlock) {
|
|
150
|
+
telemetry.state = "tool";
|
|
151
|
+
telemetry.toolName = String(toolBlock.name ?? "");
|
|
152
|
+
telemetry.toolTarget = describeToolTarget(toolBlock);
|
|
153
|
+
return telemetry;
|
|
154
|
+
}
|
|
155
|
+
telemetry.state = latest.message?.stop_reason === "end_turn" ? "idle" : "thinking";
|
|
156
|
+
return telemetry;
|
|
157
|
+
}
|
|
158
|
+
if (latest.type === "user") {
|
|
159
|
+
telemetry.state = "thinking";
|
|
160
|
+
return telemetry;
|
|
161
|
+
}
|
|
162
|
+
return telemetry;
|
|
163
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "derives live Codex session state from the tail of its rollout file"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing Codex card telemetry or the rollout records it depends on"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Codex appends newline-delimited records to a per-session rollout. Every record carries a
|
|
9
|
+
* top-level `timestamp`, a `type` of `session_meta`, `turn_context`, `event_msg` or
|
|
10
|
+
* `response_item`, and a `payload` whose own `type` names the event. The format belongs to Codex
|
|
11
|
+
* and carries no stability guarantee, so every field is optional here and an unparseable record is
|
|
12
|
+
* skipped. When nothing usable is found the caller keeps the process-only card.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PREVIEW_LIMIT = 100;
|
|
16
|
+
const TOOL_ARGUMENT_KEYS = ["cmd", "command", "query", "path", "file_path", "pattern", "workdir"];
|
|
17
|
+
|
|
18
|
+
/** @typedef {import("./claude-transcript.mjs").ClaudeTelemetry} AgentTelemetry */
|
|
19
|
+
|
|
20
|
+
/** @param {unknown} value */
|
|
21
|
+
function preview(value) {
|
|
22
|
+
const text = String(value ?? "")
|
|
23
|
+
.replace(/\s+/g, " ")
|
|
24
|
+
.trim();
|
|
25
|
+
return text.length > PREVIEW_LIMIT ? `${text.slice(0, PREVIEW_LIMIT - 1)}…` : text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {unknown} value */
|
|
29
|
+
function timestampMs(value) {
|
|
30
|
+
const parsed = Date.parse(String(value ?? ""));
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Name what a Codex tool call is acting on. Arguments arrive as a JSON string, and a call whose
|
|
36
|
+
* arguments do not parse still reports its tool name.
|
|
37
|
+
* @param {any} payload
|
|
38
|
+
*/
|
|
39
|
+
export function describeCodexToolTarget(payload) {
|
|
40
|
+
let args = payload?.arguments;
|
|
41
|
+
if (typeof args === "string") {
|
|
42
|
+
try {
|
|
43
|
+
args = JSON.parse(args);
|
|
44
|
+
} catch {
|
|
45
|
+
return preview(args);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!args || typeof args !== "object") return "";
|
|
49
|
+
for (const key of TOOL_ARGUMENT_KEYS) {
|
|
50
|
+
const value = args[key];
|
|
51
|
+
if (typeof value === "string" && value.trim()) return preview(value);
|
|
52
|
+
if (Array.isArray(value) && value.length > 0) return preview(value.join(" "));
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {any} payload */
|
|
58
|
+
function toolNameFor(payload) {
|
|
59
|
+
const type = String(payload?.type ?? "");
|
|
60
|
+
if (type === "function_call" || type === "custom_tool_call") {
|
|
61
|
+
return String(payload?.name ?? "") || "tool";
|
|
62
|
+
}
|
|
63
|
+
if (type === "local_shell_call") return "shell";
|
|
64
|
+
if (type === "web_search_call") return "web_search";
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @returns {AgentTelemetry} */
|
|
69
|
+
function emptyTelemetry() {
|
|
70
|
+
return {
|
|
71
|
+
title: "",
|
|
72
|
+
lastPrompt: "",
|
|
73
|
+
assistantPreview: "",
|
|
74
|
+
state: "idle",
|
|
75
|
+
toolName: "",
|
|
76
|
+
toolTarget: "",
|
|
77
|
+
turnIndex: 0,
|
|
78
|
+
lastEventAt: 0,
|
|
79
|
+
mode: "",
|
|
80
|
+
permissionMode: "",
|
|
81
|
+
sessionId: "",
|
|
82
|
+
cwd: "",
|
|
83
|
+
gitBranch: "",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read the tail of a rollout. The first line is usually a fragment and is skipped by the parse
|
|
89
|
+
* failure path. State comes from the newest record that carries a timestamp, so a tool call is
|
|
90
|
+
* reported as running only while it is still the newest record; its output lands after it.
|
|
91
|
+
* @param {string} tail
|
|
92
|
+
* @returns {AgentTelemetry}
|
|
93
|
+
*/
|
|
94
|
+
export function parseCodexRolloutTail(tail) {
|
|
95
|
+
const telemetry = emptyTelemetry();
|
|
96
|
+
if (!tail) return telemetry;
|
|
97
|
+
|
|
98
|
+
/** @type {any[]} */
|
|
99
|
+
const records = [];
|
|
100
|
+
for (const line of String(tail).split("\n")) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed.startsWith("{")) continue;
|
|
103
|
+
try {
|
|
104
|
+
records.push(JSON.parse(trimmed));
|
|
105
|
+
} catch {
|
|
106
|
+
// A truncated first or last line is not evidence.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (records.length === 0) return telemetry;
|
|
110
|
+
|
|
111
|
+
for (const record of records) {
|
|
112
|
+
const payload = record?.payload;
|
|
113
|
+
const payloadType = String(payload?.type ?? "");
|
|
114
|
+
if (record?.type === "session_meta") {
|
|
115
|
+
telemetry.sessionId = String(payload?.id ?? telemetry.sessionId);
|
|
116
|
+
telemetry.cwd = String(payload?.cwd ?? telemetry.cwd);
|
|
117
|
+
const branch = payload?.git?.branch;
|
|
118
|
+
if (typeof branch === "string") telemetry.gitBranch = branch;
|
|
119
|
+
} else if (record?.type === "turn_context") {
|
|
120
|
+
telemetry.cwd = String(payload?.cwd ?? telemetry.cwd);
|
|
121
|
+
telemetry.mode = String(payload?.approval_policy ?? telemetry.mode);
|
|
122
|
+
const sandbox = payload?.sandbox_policy?.type;
|
|
123
|
+
if (typeof sandbox === "string") telemetry.permissionMode = sandbox;
|
|
124
|
+
} else if (payloadType === "user_message") {
|
|
125
|
+
telemetry.lastPrompt = preview(payload?.message);
|
|
126
|
+
} else if (payloadType === "agent_message") {
|
|
127
|
+
telemetry.assistantPreview = preview(payload?.message);
|
|
128
|
+
} else if (payloadType === "task_complete") {
|
|
129
|
+
telemetry.turnIndex += 1;
|
|
130
|
+
if (payload?.last_agent_message) {
|
|
131
|
+
telemetry.assistantPreview = preview(payload.last_agent_message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const timed = records.filter((record) => timestampMs(record?.timestamp) > 0);
|
|
137
|
+
const latest = timed.at(-1);
|
|
138
|
+
if (!latest) return telemetry;
|
|
139
|
+
telemetry.lastEventAt = timestampMs(latest.timestamp);
|
|
140
|
+
|
|
141
|
+
const payload = latest.payload;
|
|
142
|
+
const payloadType = String(payload?.type ?? "");
|
|
143
|
+
// A tool call that is the newest record is still running: its output would appear after it.
|
|
144
|
+
const toolName = toolNameFor(payload);
|
|
145
|
+
if (toolName) {
|
|
146
|
+
telemetry.state = "tool";
|
|
147
|
+
telemetry.toolName = toolName;
|
|
148
|
+
telemetry.toolTarget = describeCodexToolTarget(payload);
|
|
149
|
+
return telemetry;
|
|
150
|
+
}
|
|
151
|
+
telemetry.state = payloadType === "task_complete" ? "idle" : "thinking";
|
|
152
|
+
return telemetry;
|
|
153
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
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 {
|
|
9
|
+
import { detectTabInventorySupport } from "../native/tab-inventory.mjs";
|
|
10
10
|
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
|
|
@@ -67,23 +67,29 @@ export async function detectDisplayCount(options = {}) {
|
|
|
67
67
|
/**
|
|
68
68
|
* @param {{
|
|
69
69
|
* env?: NodeJS.ProcessEnv;
|
|
70
|
-
* locateElectronImpl?: typeof locateElectron;
|
|
71
70
|
* execFileAsyncImpl?: typeof execFileAsync;
|
|
71
|
+
* platform?: NodeJS.Platform;
|
|
72
|
+
* arch?: string;
|
|
72
73
|
* }} [options]
|
|
73
74
|
* @returns {Promise<import("./contracts.ts").ActivityStripCompatibilityReport>}
|
|
74
75
|
*/
|
|
75
76
|
export async function assessActivityStripCompatibility(options = {}) {
|
|
76
77
|
const env = options.env || process.env;
|
|
77
|
-
const
|
|
78
|
+
const backend = "native";
|
|
79
|
+
const platform = options.platform ?? process.platform;
|
|
80
|
+
const arch = options.arch ?? process.arch;
|
|
78
81
|
const displayServer = detectDisplayServer(env);
|
|
79
82
|
const windowManager = detectWindowManager(env);
|
|
80
83
|
const displayCount = await detectDisplayCount({
|
|
81
84
|
env,
|
|
82
85
|
execFileAsyncImpl: options.execFileAsyncImpl,
|
|
83
86
|
});
|
|
87
|
+
const tabInventory = await detectTabInventorySupport({
|
|
88
|
+
execFileAsync: options.execFileAsyncImpl ?? execFileAsync,
|
|
89
|
+
env,
|
|
90
|
+
});
|
|
84
91
|
const blockers = [];
|
|
85
92
|
const warnings = [];
|
|
86
|
-
let electronPath = null;
|
|
87
93
|
|
|
88
94
|
if (displayServer === "headless") {
|
|
89
95
|
blockers.push(
|
|
@@ -97,10 +103,11 @@ export async function assessActivityStripCompatibility(options = {}) {
|
|
|
97
103
|
);
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
106
|
+
if (displayServer !== "headless" && displayServer !== "wayland") {
|
|
107
|
+
blockers.push("The native Activity Strip panel requires a Wayland session.");
|
|
108
|
+
}
|
|
109
|
+
if (platform !== "linux" || arch !== "x64") {
|
|
110
|
+
blockers.push(`The packaged native panel requires Linux x64; detected ${platform} ${arch}.`);
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
if (displayCount && displayCount > 1) {
|
|
@@ -111,19 +118,26 @@ export async function assessActivityStripCompatibility(options = {}) {
|
|
|
111
118
|
|
|
112
119
|
if (windowManager !== "niri") {
|
|
113
120
|
warnings.push(
|
|
114
|
-
"
|
|
121
|
+
"Workspace-local projection is optimized for Niri; other layer-shell compositors show the global card view.",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (windowManager === "niri" && !tabInventory.available) {
|
|
126
|
+
warnings.push(
|
|
127
|
+
`Hidden Ghostty tabs that were never shown cannot be placed: ${tabInventory.detail}. Tabs shown while the strip runs are still remembered.`,
|
|
115
128
|
);
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
return {
|
|
119
132
|
ok: blockers.length === 0,
|
|
133
|
+
backend,
|
|
120
134
|
displayServer,
|
|
121
135
|
windowManager: typeof windowManager === "string" ? windowManager : null,
|
|
122
|
-
electronPath,
|
|
123
136
|
displayCount,
|
|
124
|
-
alignmentMode:
|
|
137
|
+
alignmentMode: "layer-shell",
|
|
125
138
|
primaryDisplayOnly: true,
|
|
126
139
|
clickThroughDefault: env.PI_ACTIVITY_STRIP_CLICK_THROUGH === "1",
|
|
140
|
+
tabInventory,
|
|
127
141
|
blockers,
|
|
128
142
|
warnings,
|
|
129
143
|
};
|
|
@@ -137,10 +151,11 @@ export function formatCompatibilityReport(report) {
|
|
|
137
151
|
`Compatibility: ${report.ok ? "compatible" : "blocked"}`,
|
|
138
152
|
`Display server: ${report.displayServer}`,
|
|
139
153
|
`Window manager: ${report.windowManager || "unknown"}`,
|
|
140
|
-
`
|
|
154
|
+
`Backend: ${report.backend}`,
|
|
141
155
|
`Alignment mode: ${report.alignmentMode}`,
|
|
142
156
|
`Primary-display only: ${report.primaryDisplayOnly ? "yes" : "no"}`,
|
|
143
157
|
`Click-through mode: ${report.clickThroughDefault ? "enabled by environment" : "disabled (interactive default)"}`,
|
|
158
|
+
`Hidden-tab inventory: ${report.tabInventory?.available ? "available" : "unavailable"} (${report.tabInventory?.detail ?? "unknown"})`,
|
|
144
159
|
];
|
|
145
160
|
|
|
146
161
|
if (typeof report.displayCount === "number") {
|
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,40 +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 DEFAULT_ELECTRON_CANDIDATES = [
|
|
35
|
-
"electron",
|
|
36
|
-
"electron39",
|
|
37
|
-
"electron38",
|
|
38
|
-
"electron37",
|
|
39
|
-
"electron36",
|
|
40
|
-
"electron35",
|
|
41
|
-
"electron34",
|
|
42
|
-
"electron33",
|
|
43
|
-
"electron32",
|
|
44
|
-
"electron31",
|
|
45
|
-
];
|
|
46
28
|
export const ACTIVITY_STRIP_EVENT_STALL_MS = 15 * 60_000;
|
|
47
29
|
export const ACTIVITY_STRIP_FLUSH_RETRY_DELAYS_MS = [250, 750];
|
|
@@ -9,7 +9,16 @@ export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success"
|
|
|
9
9
|
export interface SessionSnapshot {
|
|
10
10
|
sessionId: string;
|
|
11
11
|
publisherId: string;
|
|
12
|
+
publisherSequence: number;
|
|
12
13
|
processId: number;
|
|
14
|
+
terminalKind: "ghostty-surface" | "unbound";
|
|
15
|
+
terminalKey: string;
|
|
16
|
+
terminalFamily: string;
|
|
17
|
+
terminalSurfaceId: string;
|
|
18
|
+
cardId?: string;
|
|
19
|
+
publisherCount?: number;
|
|
20
|
+
publisherIds?: string[];
|
|
21
|
+
publisherRecordKeys?: string[];
|
|
13
22
|
cwd: string;
|
|
14
23
|
repoLabel: string;
|
|
15
24
|
sessionName: string;
|
|
@@ -29,6 +38,32 @@ export interface SessionSnapshot {
|
|
|
29
38
|
errorMessage: string;
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
export type AkTaskChipState = "active" | "deferred" | "orphaned";
|
|
42
|
+
|
|
43
|
+
/** One read-only AK task reference joined onto a terminal card by the native projection. */
|
|
44
|
+
export interface AkTaskChip {
|
|
45
|
+
id: number;
|
|
46
|
+
title: string;
|
|
47
|
+
state: AkTaskChipState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A live-lease task claim parsed from read-only `ak task list` output. */
|
|
51
|
+
export interface AkTaskClaim {
|
|
52
|
+
id: number;
|
|
53
|
+
title: string;
|
|
54
|
+
repo: string;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
leaseExpiresAt: number;
|
|
57
|
+
claimedAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A task with an active deferral parsed from read-only `ak task deferred` output. */
|
|
61
|
+
export interface AkTaskDeferred {
|
|
62
|
+
id: number;
|
|
63
|
+
title: string;
|
|
64
|
+
repo: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
export interface BrokerSnapshot {
|
|
33
68
|
generatedAt: number;
|
|
34
69
|
sessions: SessionSnapshot[];
|
|
@@ -38,13 +73,25 @@ export type ActivityStripRuntimeState = "starting" | "ready" | "error";
|
|
|
38
73
|
|
|
39
74
|
export interface ActivityStripRuntimeStatus {
|
|
40
75
|
state: ActivityStripRuntimeState;
|
|
76
|
+
backend?: "native-layer-shell";
|
|
41
77
|
startedAt: number;
|
|
42
78
|
readyAt?: number | null;
|
|
43
79
|
windowVisible?: boolean;
|
|
44
80
|
displayServer?: "wayland" | "x11" | "headless" | "unknown";
|
|
45
81
|
windowManager?: string | null;
|
|
46
82
|
displayCount?: number | null;
|
|
47
|
-
alignmentMode?: "
|
|
83
|
+
alignmentMode?: "layer-shell";
|
|
84
|
+
rendererCardCount?: number;
|
|
85
|
+
rendererCardIds?: string[];
|
|
86
|
+
rendererVisibilityTransitionCount?: number;
|
|
87
|
+
controllerPid?: number;
|
|
88
|
+
panelPid?: number | null;
|
|
89
|
+
panelExpanded?: boolean;
|
|
90
|
+
clickThrough?: boolean;
|
|
91
|
+
panelMoveCount?: number;
|
|
92
|
+
panelActivationCount?: number;
|
|
93
|
+
panelRestartCount?: number;
|
|
94
|
+
lastMovedCardId?: string | null;
|
|
48
95
|
warnings?: string[];
|
|
49
96
|
error?: string | null;
|
|
50
97
|
}
|
|
@@ -65,6 +112,7 @@ export interface BrokerClientOptions {
|
|
|
65
112
|
|
|
66
113
|
export interface SessionStoreOptions {
|
|
67
114
|
staleAfterMs?: number;
|
|
115
|
+
maxSessions?: number;
|
|
68
116
|
}
|
|
69
117
|
|
|
70
118
|
export interface ActivityStripBrokerOptions {
|
|
@@ -76,7 +124,8 @@ export interface ActivityStripBrokerOptions {
|
|
|
76
124
|
remove(sessionId: string, publisherId?: string): boolean;
|
|
77
125
|
};
|
|
78
126
|
getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
|
|
79
|
-
focusSession?: (
|
|
127
|
+
focusSession?: (targetId: string) => Promise<{ ok: boolean; error?: string; windowId?: number }>;
|
|
128
|
+
focusStrip?: () => Promise<{ ok: boolean; error?: string }>;
|
|
80
129
|
}
|
|
81
130
|
|
|
82
131
|
export interface ToolCallDescription {
|
|
@@ -143,6 +192,21 @@ export interface SessionTelemetryOptions {
|
|
|
143
192
|
pi?: TelemetryPiLike;
|
|
144
193
|
cwd?: string;
|
|
145
194
|
sessionName?: string;
|
|
195
|
+
env?: NodeJS.ProcessEnv;
|
|
196
|
+
stdinIsTTY?: boolean;
|
|
197
|
+
resolveTerminalIdentity?: (options: {
|
|
198
|
+
env?: NodeJS.ProcessEnv;
|
|
199
|
+
hasUI?: boolean;
|
|
200
|
+
stdinIsTTY?: boolean;
|
|
201
|
+
ttyPath?: string;
|
|
202
|
+
processId?: number;
|
|
203
|
+
ancestorExecutable?: string;
|
|
204
|
+
}) => {
|
|
205
|
+
terminalKind: "ghostty-surface" | "unbound";
|
|
206
|
+
terminalKey: string;
|
|
207
|
+
terminalFamily: string;
|
|
208
|
+
terminalSurfaceId: string;
|
|
209
|
+
};
|
|
146
210
|
transport?: {
|
|
147
211
|
publish: (session: SessionSnapshot) => Promise<unknown>;
|
|
148
212
|
remove: (session: { sessionId: string; publisherId: string }) => Promise<unknown>;
|
|
@@ -151,11 +215,11 @@ export interface SessionTelemetryOptions {
|
|
|
151
215
|
|
|
152
216
|
export interface ActivityStripCompatibilityReport {
|
|
153
217
|
ok: boolean;
|
|
218
|
+
backend: string;
|
|
154
219
|
displayServer: "wayland" | "x11" | "headless" | "unknown";
|
|
155
220
|
windowManager: string | null;
|
|
156
|
-
electronPath: string | null;
|
|
157
221
|
displayCount: number | null;
|
|
158
|
-
alignmentMode: "
|
|
222
|
+
alignmentMode: "layer-shell";
|
|
159
223
|
primaryDisplayOnly: boolean;
|
|
160
224
|
clickThroughDefault: boolean;
|
|
161
225
|
blockers: string[];
|