@tryinget/pi-activity-strip 0.5.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.
Files changed (41) hide show
  1. package/README.md +41 -6
  2. package/bin/pi-activity-strip-claude-hook.mjs +71 -0
  3. package/bin/pi-activity-strip.mjs +22 -1
  4. package/native/bin/linux-x64-gnu/artifact.json +2 -2
  5. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  6. package/native/panel/src/app.rs +20 -20
  7. package/native/panel/src/card_view.rs +308 -9
  8. package/native/panel/src/protocol.rs +63 -0
  9. package/native/panel/src/runtime.rs +50 -9
  10. package/native/panel/src/style.css +134 -60
  11. package/package.json +4 -4
  12. package/src/common/agent-identity.mjs +163 -0
  13. package/src/common/ak-tasks.mjs +354 -0
  14. package/src/common/claude-events.mjs +190 -0
  15. package/src/common/claude-hook-config.mjs +56 -0
  16. package/src/common/claude-transcript.mjs +163 -0
  17. package/src/common/codex-transcript.mjs +153 -0
  18. package/src/common/compatibility.mjs +13 -0
  19. package/src/common/contracts.d.ts +26 -0
  20. package/src/common/contracts.ts +64 -1
  21. package/src/common/ghostty-present.mjs +100 -0
  22. package/src/common/ghostty-theme.mjs +149 -0
  23. package/src/common/niri-focus.mjs +179 -110
  24. package/src/common/session-cards.mjs +5 -2
  25. package/src/common/status-report.mjs +51 -0
  26. package/src/common/surface-bindings.mjs +234 -0
  27. package/src/common/terminal-identity.mjs +75 -12
  28. package/src/common/window-placement.mjs +59 -0
  29. package/src/common/workspace-view.mjs +161 -0
  30. package/src/native/agent-discovery.mjs +294 -0
  31. package/src/native/ak-runtime.mjs +164 -0
  32. package/src/native/codex-discovery.mjs +128 -0
  33. package/src/native/ghostty-tab-inventory.py +99 -0
  34. package/src/native/height-repair.mjs +154 -0
  35. package/src/native/main.mjs +120 -14
  36. package/src/native/panel-projection.mjs +67 -5
  37. package/src/native/placement.mjs +225 -0
  38. package/src/native/tab-inventory.mjs +188 -0
  39. package/src/native/theme-runtime.mjs +72 -0
  40. package/src/native/theme-source.mjs +197 -0
  41. package/src/native/workspace-events.mjs +16 -1
@@ -1,56 +1,65 @@
1
+ /*
2
+ * Every colour here derives from the handful of names the controller defines from Ghostty's own
3
+ * theme, so the ribbon follows the terminal into light and dark without a second palette to keep
4
+ * in sync. mix() against the background and alpha() against the foreground both invert correctly
5
+ * between schemes: a raised surface is lighter on a dark theme and darker on a light one.
6
+ */
7
+
1
8
  /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
2
9
  window {
3
10
  background: transparent;
4
- color: rgba(247, 250, 255, 0.96);
5
- font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
11
+ color: @pi_foreground;
12
+ font-family: "IBM Plex Sans", "Inter", "Segoe UI", sans-serif;
6
13
  }
7
14
 
8
- .brand,
9
- .cards-panel {
10
- background: linear-gradient(to bottom, rgba(12, 19, 33, 0.96), rgba(8, 13, 22, 0.92));
11
- border: 1px solid rgba(255, 255, 255, 0.08);
12
- border-radius: 20px;
15
+ /* One surface on the same rhythm and radius as tiled windows, so it reads as a peer of them. */
16
+ .band {
17
+ /* Solid: the ribbon is the one surface that never dims, so it reads as the fixed ground. */
18
+ background: @pi_background;
19
+ border: 1px solid alpha(@pi_foreground, 0.16);
20
+ border-radius: 12px;
21
+ padding: 0 14px 0 16px;
13
22
  }
14
23
 
15
24
  .brand {
16
- padding: 10px 12px;
25
+ border-right: 1px solid alpha(@pi_foreground, 0.12);
26
+ margin-right: 12px;
27
+ padding: 11px 14px 11px 0;
17
28
  }
18
29
 
19
30
  .brand-eyebrow,
20
31
  .meta,
21
32
  .card-state,
22
33
  .card-tool,
34
+ .card-elapsed,
23
35
  .inspector-key {
24
36
  font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
25
37
  }
26
38
 
27
39
  .brand-eyebrow {
28
- color: #79b8ff;
40
+ color: alpha(@pi_accent, 0.9);
29
41
  font-size: 9px;
30
- letter-spacing: 1px;
42
+ font-weight: 600;
43
+ letter-spacing: 1.4px;
31
44
  }
32
45
 
33
46
  .brand-title {
34
- color: rgba(247, 250, 255, 0.96);
35
- font-size: 18px;
47
+ color: @pi_foreground;
48
+ font-size: 17px;
36
49
  font-weight: 650;
37
- }
38
-
39
- .brand-subtitle {
40
- color: rgba(194, 205, 224, 0.74);
41
- font-size: 10px;
50
+ letter-spacing: -0.2px;
42
51
  }
43
52
 
44
53
  .meta {
45
- color: rgba(167, 180, 203, 0.58);
54
+ color: alpha(@pi_foreground, 0.56);
46
55
  font-size: 9px;
47
56
  }
48
57
 
49
58
  /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
59
+ .cards-panel,
50
60
  .cards-panel > viewport {
51
- background: linear-gradient(to bottom, rgba(10, 16, 28, 0.94), rgba(6, 10, 18, 0.9));
52
- border-radius: 20px;
53
- padding: 6px 8px 8px;
61
+ background: transparent;
62
+ padding: 0;
54
63
  }
55
64
 
56
65
  /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
@@ -61,32 +70,42 @@ window {
61
70
  }
62
71
 
63
72
  .card {
64
- color: rgba(247, 250, 255, 0.96);
65
- background: linear-gradient(to bottom, rgba(13, 20, 35, 0.98), rgba(9, 14, 24, 0.96));
66
- border: 1px solid rgba(255, 255, 255, 0.14);
67
- border-left: 3px solid #79b8ff;
68
- border-radius: 16px;
69
- padding: 8px 10px 8px 12px;
70
- box-shadow: none;
71
- transition: 120ms ease;
73
+ color: @pi_foreground;
74
+ background: mix(@pi_background, @pi_foreground, 0.07);
75
+ border: 1px solid alpha(@pi_foreground, 0.1);
76
+ border-left: 3px solid alpha(@pi_foreground, 0.35);
77
+ border-radius: 12px;
78
+ padding: 7px 11px 7px 10px;
79
+ transition:
80
+ background 120ms ease,
81
+ border-color 120ms ease;
72
82
  }
73
83
 
74
84
  .card:hover,
75
85
  .card:focus-visible,
76
86
  .card.open {
77
- background: linear-gradient(to bottom, rgba(18, 28, 47, 0.99), rgba(10, 16, 28, 0.97));
78
- border-color: rgba(121, 184, 255, 0.72);
87
+ background: mix(@pi_background, @pi_foreground, 0.14);
88
+ border-color: alpha(@pi_accent, 0.5);
79
89
  }
80
90
 
91
+ /* The tab the compositor is actually showing, marked by weight rather than another border. */
81
92
  .card.current {
82
- border-left-width: 5px;
83
- border-color: rgba(150, 202, 255, 0.92);
84
- outline: 1px solid rgba(121, 184, 255, 0.38);
85
- outline-offset: -3px;
93
+ background: mix(@pi_background, @pi_foreground, 0.16);
94
+ border-color: alpha(@pi_accent, 0.42);
95
+ box-shadow: inset 0 0 0 1px alpha(@pi_accent, 0.22);
96
+ }
97
+
98
+ .card.hidden-tab {
99
+ background: alpha(@pi_background, 0.55);
100
+ border-style: dashed;
101
+ }
102
+
103
+ .card.hidden-tab .card-repo {
104
+ color: alpha(@pi_foreground, 0.78);
86
105
  }
87
106
 
88
107
  .card.stalled {
89
- opacity: 0.72;
108
+ opacity: 0.62;
90
109
  }
91
110
 
92
111
  .card.stalled .card-phase {
@@ -94,83 +113,138 @@ window {
94
113
  }
95
114
 
96
115
  .card.state-thinking {
97
- border-left-color: #8ab4ff;
116
+ border-left-color: @pi_thinking;
98
117
  }
99
118
  .card.state-tool {
100
- border-left-color: #ffd166;
119
+ border-left-color: @pi_tool;
101
120
  }
102
121
  .card.state-waiting {
103
- border-left-color: #ff9f7a;
122
+ border-left-color: @pi_waiting;
104
123
  }
105
124
  .card.state-success {
106
- border-left-color: #57d9a3;
125
+ border-left-color: @pi_success;
107
126
  }
108
127
  .card.state-error {
109
- border-left-color: #ff7d7d;
128
+ border-left-color: @pi_error;
110
129
  }
111
130
 
112
131
  .card-repo {
113
- color: rgba(247, 250, 255, 0.96);
132
+ color: @pi_foreground;
114
133
  font-size: 12px;
115
134
  font-weight: 650;
116
135
  }
117
136
 
118
137
  .card-phase,
119
138
  .inspector-value {
120
- color: rgba(194, 205, 224, 0.76);
139
+ color: alpha(@pi_foreground, 0.74);
121
140
  font-size: 10px;
122
141
  }
123
142
 
143
+ /* A quiet pill: the state is carried mostly by the card's left edge. */
124
144
  .card-state {
125
- border: 1px solid rgba(121, 184, 255, 0.36);
145
+ background: alpha(@pi_foreground, 0.07);
146
+ border: 1px solid alpha(@pi_foreground, 0.16);
126
147
  border-radius: 999px;
127
- color: #79b8ff;
148
+ color: alpha(@pi_foreground, 0.68);
128
149
  font-size: 8px;
129
- padding: 4px 6px;
150
+ letter-spacing: 0.4px;
151
+ padding: 3px 7px;
130
152
  }
131
153
 
154
+ .state-thinking .card-state {
155
+ background: alpha(@pi_thinking, 0.14);
156
+ border-color: alpha(@pi_thinking, 0.38);
157
+ color: @pi_thinking;
158
+ }
132
159
  .state-tool .card-state {
133
- color: #ffd166;
134
- border-color: rgba(255, 209, 102, 0.42);
160
+ background: alpha(@pi_tool, 0.14);
161
+ border-color: alpha(@pi_tool, 0.38);
162
+ color: @pi_tool;
135
163
  }
136
164
  .state-waiting .card-state {
137
- color: #ff9f7a;
138
- border-color: rgba(255, 159, 122, 0.42);
165
+ background: alpha(@pi_waiting, 0.16);
166
+ border-color: alpha(@pi_waiting, 0.42);
167
+ color: @pi_waiting;
139
168
  }
140
169
  .state-success .card-state {
141
- color: #57d9a3;
142
- border-color: rgba(87, 217, 163, 0.42);
170
+ background: alpha(@pi_success, 0.14);
171
+ border-color: alpha(@pi_success, 0.38);
172
+ color: @pi_success;
143
173
  }
144
174
  .state-error .card-state {
145
- color: #ff7d7d;
146
- border-color: rgba(255, 125, 125, 0.42);
175
+ background: alpha(@pi_error, 0.16);
176
+ border-color: alpha(@pi_error, 0.44);
177
+ color: @pi_error;
147
178
  }
148
179
 
149
180
  .card-tool,
150
181
  .card-elapsed,
151
182
  .inspector-key {
152
- color: rgba(167, 180, 203, 0.58);
183
+ color: alpha(@pi_foreground, 0.55);
153
184
  font-size: 8px;
154
185
  }
155
186
 
156
187
  .inspector {
157
- border-top: 1px solid rgba(255, 255, 255, 0.08);
188
+ border-top: 1px solid alpha(@pi_foreground, 0.12);
158
189
  padding-top: 9px;
159
190
  }
160
191
 
161
192
  .inspector-value {
162
- line-height: 1.25;
193
+ line-height: 1.3;
163
194
  }
164
195
 
165
196
  .activation {
166
- color: rgba(167, 180, 203, 0.72);
197
+ color: alpha(@pi_foreground, 0.7);
167
198
  font-size: 9px;
168
199
  }
169
200
 
170
201
  .activation.success {
171
- color: #57d9a3;
202
+ color: @pi_success;
172
203
  }
173
204
 
174
205
  .activation.error {
175
- color: #ff7d7d;
206
+ color: @pi_error;
207
+ }
208
+
209
+ /*
210
+ * AK task references. An active chip is the one clickable element inside a card besides the card
211
+ * itself: it reuses the card's activation, so it carries the accent colour and a pointer cursor.
212
+ * Badge states (a claim that outlives its session, a deferred task) are deliberately inert pills.
213
+ */
214
+ .ak-task {
215
+ background: alpha(@pi_foreground, 0.07);
216
+ border: 1px solid alpha(@pi_foreground, 0.16);
217
+ border-radius: 999px;
218
+ color: alpha(@pi_foreground, 0.68);
219
+ font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
220
+ font-size: 8px;
221
+ padding: 3px 8px;
222
+ }
223
+
224
+ .ak-task.ak-active {
225
+ background: alpha(@pi_accent, 0.14);
226
+ border-color: alpha(@pi_accent, 0.42);
227
+ color: @pi_accent;
228
+ }
229
+
230
+ .ak-task.ak-active:hover {
231
+ background: alpha(@pi_accent, 0.26);
232
+ }
233
+
234
+ .ak-task.ak-orphaned {
235
+ background: alpha(@pi_error, 0.12);
236
+ border: 1px dashed alpha(@pi_error, 0.4);
237
+ color: alpha(@pi_error, 0.92);
238
+ }
239
+
240
+ .ak-task.ak-deferred {
241
+ background: alpha(@pi_tool, 0.12);
242
+ border: 1px dashed alpha(@pi_tool, 0.4);
243
+ color: alpha(@pi_tool, 0.92);
244
+ }
245
+
246
+ .ak-overflow {
247
+ color: alpha(@pi_foreground, 0.5);
248
+ font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
249
+ font-size: 8px;
176
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryinget/pi-activity-strip",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -88,9 +88,9 @@
88
88
  },
89
89
  "devDependencies": {
90
90
  "@biomejs/biome": "2.4.4",
91
- "@earendil-works/pi-agent-core": "0.84.3",
92
- "@earendil-works/pi-ai": "0.84.3",
93
- "@earendil-works/pi-coding-agent": "0.84.3",
91
+ "@earendil-works/pi-agent-core": "0.84.4",
92
+ "@earendil-works/pi-ai": "0.84.4",
93
+ "@earendil-works/pi-coding-agent": "0.84.4",
94
94
  "@types/node": "22.14.0",
95
95
  "@typescript/native-preview": "7.0.0-dev.20260417.1",
96
96
  "typebox": "1.3.7",
@@ -0,0 +1,163 @@
1
+ // ---
2
+ // summary: "recognizes terminal-based coding agents and shapes them into activity-strip session records"
3
+ // read_when:
4
+ // - "adding an agent CLI, changing agent card fields, or changing agent title matching"
5
+ // ---
6
+
7
+ import path from "node:path";
8
+
9
+ /**
10
+ * Terminal-based coding agents that occupy a Ghostty tab the way Pi does. Recognition is by the
11
+ * executable name of the process that owns the terminal, never by a window title, so a tab is
12
+ * admitted on process evidence alone.
13
+ * @type {ReadonlyArray<{kind: string; label: string; commands: readonly string[]}>}
14
+ */
15
+ export const AGENT_CATALOG = Object.freeze([
16
+ { kind: "claude", label: "Claude Code", commands: Object.freeze(["claude"]) },
17
+ { kind: "codex", label: "Codex", commands: Object.freeze(["codex"]) },
18
+ { kind: "gemini", label: "Gemini", commands: Object.freeze(["gemini"]) },
19
+ { kind: "amp", label: "Amp", commands: Object.freeze(["amp"]) },
20
+ { kind: "aider", label: "Aider", commands: Object.freeze(["aider"]) },
21
+ { kind: "opencode", label: "opencode", commands: Object.freeze(["opencode"]) },
22
+ { kind: "crush", label: "Crush", commands: Object.freeze(["crush"]) },
23
+ { kind: "goose", label: "Goose", commands: Object.freeze(["goose"]) },
24
+ { kind: "cursor", label: "Cursor Agent", commands: Object.freeze(["cursor-agent"]) },
25
+ { kind: "zcode", label: "zcode", commands: Object.freeze(["zcode"]) },
26
+ ]);
27
+
28
+ /** Pi publishes its own telemetry, so its processes are never discovered this way. */
29
+ const EXCLUDED_COMMANDS = new Set(["pi", "node", "bash", "sh", "zsh", "fish", "python3"]);
30
+ /**
31
+ * Executables that only ever host another program. When one of these is the process name, the
32
+ * command line names the real agent, so it is worth the extra read to look.
33
+ */
34
+ const RUNTIME_COMMANDS = [/^node/i, /^python/i, /^(bash|sh|zsh|fish|dash|env)$/i, /^(deno|bun)$/i];
35
+
36
+ /** @param {unknown} command */
37
+ export function isRuntimeCommand(command) {
38
+ const name = path.basename(String(command ?? "").trim());
39
+ return name.length > 0 && RUNTIME_COMMANDS.some((pattern) => pattern.test(name));
40
+ }
41
+ export const AGENT_ACTIVE_WINDOW_MS = 45_000;
42
+
43
+ /** @param {NodeJS.ProcessEnv} [env] */
44
+ export function agentCatalogFor(env = process.env) {
45
+ const disabled = new Set(
46
+ String(env.PI_ACTIVITY_STRIP_AGENT_KINDS_DISABLED ?? "")
47
+ .split(",")
48
+ .map((value) => value.trim().toLowerCase())
49
+ .filter(Boolean),
50
+ );
51
+ return AGENT_CATALOG.filter((agent) => !disabled.has(agent.kind));
52
+ }
53
+
54
+ /**
55
+ * Classify one process by the executable it runs. `command` is the kernel's comm value, which the
56
+ * kernel truncates to 15 characters and which names the runtime rather than the agent when the
57
+ * agent is launched through one; the command line entries name the agent in that case.
58
+ * @param {{command?: unknown; argv0?: unknown; argv1?: unknown; env?: NodeJS.ProcessEnv}} process
59
+ */
60
+ export function classifyAgentProcess({ command, argv0, argv1, env = process.env } = {}) {
61
+ const names = [command, argv0, argv1]
62
+ .map((value) => path.basename(String(value ?? "").trim()).toLowerCase())
63
+ .filter(Boolean);
64
+ if (names.length === 0 || names.every((name) => EXCLUDED_COMMANDS.has(name))) return null;
65
+ for (const agent of agentCatalogFor(env)) {
66
+ if (agent.commands.some((candidate) => names.includes(candidate))) return agent;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /** @param {unknown} cwd */
72
+ export function repoLabelFor(cwd) {
73
+ const normalized = String(cwd ?? "").trim();
74
+ if (!normalized || normalized === "/") return "";
75
+ return path.basename(normalized);
76
+ }
77
+
78
+ /**
79
+ * Shape one discovered agent tab into the record the card projection consumes. The record carries a
80
+ * terminal identity, so it groups and places exactly like a Pi terminal card, but its session id is
81
+ * namespaced by agent kind and can never be mistaken for a Pi session id.
82
+ * @param {{
83
+ * kind: string;
84
+ * label: string;
85
+ * processId: number;
86
+ * cwd: string;
87
+ * terminalKey: string;
88
+ * terminalFamily: string;
89
+ * terminalSurfaceId: string;
90
+ * sessionKey?: string;
91
+ * titleSuffix?: string;
92
+ * startedAt: number;
93
+ * lastEventAt?: number;
94
+ * now?: number;
95
+ * telemetry?: Partial<import("./claude-transcript.mjs").ClaudeTelemetry> | null;
96
+ * }} tab
97
+ */
98
+ export function agentSessionRecord({
99
+ kind,
100
+ label,
101
+ processId,
102
+ cwd,
103
+ terminalKey,
104
+ terminalFamily,
105
+ terminalSurfaceId,
106
+ sessionKey = "",
107
+ titleSuffix = "",
108
+ startedAt,
109
+ lastEventAt = 0,
110
+ now = Date.now(),
111
+ telemetry = null,
112
+ }) {
113
+ // Process start times and file modification times are fractional milliseconds, while the panel
114
+ // protocol carries whole-millisecond integers, so every timestamp is rounded at the boundary.
115
+ const startedAtMs = Math.round(startedAt) || 0;
116
+ const lastEventMs = Math.round(lastEventAt) || 0;
117
+ const nowMs = Math.round(now) || 0;
118
+ const activity = lastEventMs > 0 ? lastEventMs : startedAtMs;
119
+ // Reported activity decides liveness. A published state only counts while the transcript is
120
+ // still moving; a stale "tool" record is not evidence that a tool is running right now.
121
+ const recent = lastEventMs > 0 && nowMs - lastEventMs <= AGENT_ACTIVE_WINDOW_MS;
122
+ const reported = String(telemetry?.state ?? "");
123
+ // "waiting" and "error" describe a session that is stopped on purpose, so they survive the
124
+ // liveness window: a permission prompt can sit unanswered for a long time and is still true.
125
+ const sticky = reported === "waiting" || reported === "error";
126
+ const busy = reported === "tool" || reported === "thinking";
127
+ const active = sticky || (recent && (busy || !reported));
128
+ const state = sticky ? reported : active ? (busy ? reported : "thinking") : "idle";
129
+ const toolName = active ? String(telemetry?.toolName ?? "") : "";
130
+ const toolTarget = active ? String(telemetry?.toolTarget ?? "") : "";
131
+ const detail = [toolTarget, String(telemetry?.assistantPreview ?? ""), cwd].find(Boolean) ?? "";
132
+ return {
133
+ sessionId: `agent:${kind}:${sessionKey || processId}`,
134
+ publisherId: `agent-scan:${processId}`,
135
+ publisherSequence: 0,
136
+ processId,
137
+ agentKind: kind,
138
+ agentSessionKey: sessionKey,
139
+ terminalKind: "ghostty-surface",
140
+ terminalKey,
141
+ terminalFamily,
142
+ terminalSurfaceId,
143
+ titleSuffix,
144
+ cwd,
145
+ repoLabel: repoLabelFor(cwd),
146
+ sessionName: "",
147
+ agentLabel: label,
148
+ phase: String(telemetry?.title ?? "").trim() || label,
149
+ detail,
150
+ assistantPreview: String(telemetry?.assistantPreview ?? ""),
151
+ toolName,
152
+ toolTarget,
153
+ state,
154
+ turnIndex: Number(telemetry?.turnIndex ?? 0) || 0,
155
+ updatedAt: nowMs,
156
+ lastEventAt: activity,
157
+ startedAt: startedAtMs,
158
+ agentStartedAt: active ? activity : null,
159
+ agentActive: active,
160
+ lastPromptPreview: String(telemetry?.lastPrompt ?? ""),
161
+ errorMessage: "",
162
+ };
163
+ }