@tryinget/pi-activity-strip 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +96 -152
  2. package/bin/pi-activity-strip.mjs +31 -62
  3. package/examples/simulate-sessions.mjs +1 -1
  4. package/extensions/activity-strip.js +2 -2
  5. package/native/bin/linux-x64-gnu/artifact.json +21 -0
  6. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  7. package/native/panel/Cargo.lock +1045 -0
  8. package/native/panel/Cargo.toml +23 -0
  9. package/native/panel/src/app.rs +606 -0
  10. package/native/panel/src/card_view.rs +303 -0
  11. package/native/panel/src/main.rs +31 -0
  12. package/native/panel/src/protocol.rs +234 -0
  13. package/native/panel/src/runtime.rs +110 -0
  14. package/native/panel/src/style.css +176 -0
  15. package/package.json +20 -7
  16. package/src/broker/server.mjs +43 -7
  17. package/src/broker/session-store.mjs +53 -11
  18. package/src/client/broker-client.mjs +25 -4
  19. package/src/client/session-telemetry.mjs +123 -13
  20. package/src/common/activity-order.mjs +15 -9
  21. package/src/common/alignment-controller.mjs +24 -42
  22. package/src/common/card-display.mjs +53 -0
  23. package/src/common/compatibility.mjs +15 -13
  24. package/src/common/constants.mjs +12 -28
  25. package/src/common/contracts.d.ts +54 -5
  26. package/src/common/contracts.ts +54 -5
  27. package/src/common/niri-focus.mjs +79 -79
  28. package/src/common/protocol.mjs +49 -14
  29. package/src/common/session-cards.mjs +102 -0
  30. package/src/common/status-report.mjs +2 -1
  31. package/src/common/telemetry.mjs +11 -1
  32. package/src/common/terminal-identity.mjs +149 -0
  33. package/src/native/main.mjs +393 -0
  34. package/src/native/panel-projection.mjs +99 -0
  35. package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +4 -1
  36. package/src/common/electron.mjs +0 -57
  37. package/src/electron/main.mjs +0 -499
  38. package/src/electron/niri-native-window-runtime.mjs +0 -178
  39. package/src/electron/preload.cjs +0 -50
  40. package/src/electron/renderer-visibility-runtime.mjs +0 -107
  41. package/src/electron/workspace-view-runtime.mjs +0 -360
  42. package/src/ui/strip-html.mjs +0 -484
@@ -0,0 +1,176 @@
1
+ /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
2
+ window {
3
+ background: transparent;
4
+ color: rgba(247, 250, 255, 0.96);
5
+ font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
6
+ }
7
+
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;
13
+ }
14
+
15
+ .brand {
16
+ padding: 10px 12px;
17
+ }
18
+
19
+ .brand-eyebrow,
20
+ .meta,
21
+ .card-state,
22
+ .card-tool,
23
+ .inspector-key {
24
+ font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
25
+ }
26
+
27
+ .brand-eyebrow {
28
+ color: #79b8ff;
29
+ font-size: 9px;
30
+ letter-spacing: 1px;
31
+ }
32
+
33
+ .brand-title {
34
+ color: rgba(247, 250, 255, 0.96);
35
+ font-size: 18px;
36
+ font-weight: 650;
37
+ }
38
+
39
+ .brand-subtitle {
40
+ color: rgba(194, 205, 224, 0.74);
41
+ font-size: 10px;
42
+ }
43
+
44
+ .meta {
45
+ color: rgba(167, 180, 203, 0.58);
46
+ font-size: 9px;
47
+ }
48
+
49
+ /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
50
+ .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;
54
+ }
55
+
56
+ /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
57
+ .cards-panel scrollbar {
58
+ opacity: 0;
59
+ min-width: 0;
60
+ min-height: 0;
61
+ }
62
+
63
+ .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;
72
+ }
73
+
74
+ .card:hover,
75
+ .card:focus-visible,
76
+ .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);
79
+ }
80
+
81
+ .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;
86
+ }
87
+
88
+ .card.stalled {
89
+ opacity: 0.72;
90
+ }
91
+
92
+ .card.stalled .card-phase {
93
+ text-decoration-line: line-through;
94
+ }
95
+
96
+ .card.state-thinking {
97
+ border-left-color: #8ab4ff;
98
+ }
99
+ .card.state-tool {
100
+ border-left-color: #ffd166;
101
+ }
102
+ .card.state-waiting {
103
+ border-left-color: #ff9f7a;
104
+ }
105
+ .card.state-success {
106
+ border-left-color: #57d9a3;
107
+ }
108
+ .card.state-error {
109
+ border-left-color: #ff7d7d;
110
+ }
111
+
112
+ .card-repo {
113
+ color: rgba(247, 250, 255, 0.96);
114
+ font-size: 12px;
115
+ font-weight: 650;
116
+ }
117
+
118
+ .card-phase,
119
+ .inspector-value {
120
+ color: rgba(194, 205, 224, 0.76);
121
+ font-size: 10px;
122
+ }
123
+
124
+ .card-state {
125
+ border: 1px solid rgba(121, 184, 255, 0.36);
126
+ border-radius: 999px;
127
+ color: #79b8ff;
128
+ font-size: 8px;
129
+ padding: 4px 6px;
130
+ }
131
+
132
+ .state-tool .card-state {
133
+ color: #ffd166;
134
+ border-color: rgba(255, 209, 102, 0.42);
135
+ }
136
+ .state-waiting .card-state {
137
+ color: #ff9f7a;
138
+ border-color: rgba(255, 159, 122, 0.42);
139
+ }
140
+ .state-success .card-state {
141
+ color: #57d9a3;
142
+ border-color: rgba(87, 217, 163, 0.42);
143
+ }
144
+ .state-error .card-state {
145
+ color: #ff7d7d;
146
+ border-color: rgba(255, 125, 125, 0.42);
147
+ }
148
+
149
+ .card-tool,
150
+ .card-elapsed,
151
+ .inspector-key {
152
+ color: rgba(167, 180, 203, 0.58);
153
+ font-size: 8px;
154
+ }
155
+
156
+ .inspector {
157
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
158
+ padding-top: 9px;
159
+ }
160
+
161
+ .inspector-value {
162
+ line-height: 1.25;
163
+ }
164
+
165
+ .activation {
166
+ color: rgba(167, 180, 203, 0.72);
167
+ font-size: 9px;
168
+ }
169
+
170
+ .activation.success {
171
+ color: #57d9a3;
172
+ }
173
+
174
+ .activation.error {
175
+ color: #ff7d7d;
176
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryinget/pi-activity-strip",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
@@ -26,6 +26,12 @@
26
26
  "engines": {
27
27
  "node": ">=22.19.0"
28
28
  },
29
+ "os": [
30
+ "linux"
31
+ ],
32
+ "cpu": [
33
+ "x64"
34
+ ],
29
35
  "scripts": {
30
36
  "fix": "bash ./scripts/quality-gate.sh fix",
31
37
  "lint": "bash ./scripts/quality-gate.sh lint",
@@ -35,11 +41,13 @@
35
41
  "quality:ci": "bash ./scripts/quality-gate.sh ci",
36
42
  "check": "npm run quality:ci",
37
43
  "test": "npm run quality:ci",
38
- "docs:list": "bash ./scripts/docs-list.sh",
39
- "docs:list:workspace": "bash ./scripts/docs-list.sh --workspace --discover",
40
- "docs:list:json": "bash ./scripts/docs-list.sh --json",
44
+ "docs:list": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs",
45
+ "docs:list:workspace": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs --workspace --discover",
46
+ "docs:list:json": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs --json",
41
47
  "release:check": "bash ./scripts/release-check.sh",
42
48
  "release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh",
49
+ "native:check": "cd native/panel && RUSTUP_TOOLCHAIN=stable cargo fmt --check && RUSTUP_TOOLCHAIN=stable cargo test",
50
+ "native:build": "bash ./scripts/build-native-panel.sh",
43
51
  "strip:open": "node ./bin/pi-activity-strip.mjs open",
44
52
  "strip:status": "node ./bin/pi-activity-strip.mjs status",
45
53
  "strip:doctor": "node ./bin/pi-activity-strip.mjs doctor",
@@ -56,6 +64,10 @@
56
64
  "extensions/activity-strip.js",
57
65
  "extensions/activity-strip.ts",
58
66
  "src",
67
+ "native/bin",
68
+ "native/panel/Cargo.toml",
69
+ "native/panel/Cargo.lock",
70
+ "native/panel/src",
59
71
  "prompts",
60
72
  "examples",
61
73
  "policy/security-policy.json"
@@ -76,15 +88,16 @@
76
88
  },
77
89
  "devDependencies": {
78
90
  "@biomejs/biome": "2.4.4",
79
- "@earendil-works/pi-ai": "0.83.0",
80
- "@earendil-works/pi-coding-agent": "0.83.0",
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",
81
94
  "@types/node": "22.14.0",
82
95
  "@typescript/native-preview": "7.0.0-dev.20260417.1",
83
96
  "typebox": "1.3.7",
84
97
  "typescript": "5.9.2"
85
98
  },
86
99
  "overrides": {
87
- "fast-xml-parser": "5.3.6"
100
+ "fast-xml-parser": "5.7.0"
88
101
  },
89
102
  "peerDependencies": {
90
103
  "@earendil-works/pi-ai": "*",
@@ -10,6 +10,8 @@ import net from "node:net";
10
10
  /** @typedef {import("../common/contracts.ts").ActivityStripBrokerOptions} ActivityStripBrokerOptions */
11
11
  import {
12
12
  ACTIVITY_STRIP_BROADCAST_TICK_MS,
13
+ ACTIVITY_STRIP_CLIENT_IDLE_TIMEOUT_MS,
14
+ ACTIVITY_STRIP_MAX_MESSAGE_BYTES,
13
15
  ACTIVITY_STRIP_SOCKET_DIR,
14
16
  ACTIVITY_STRIP_SOCKET_PATH,
15
17
  } from "../common/constants.mjs";
@@ -49,6 +51,8 @@ export class ActivityStripBroker extends EventEmitter {
49
51
  this.getRuntimeStatus = options.getRuntimeStatus ?? (() => undefined);
50
52
  this.focusSession =
51
53
  options.focusSession ?? (async () => ({ ok: false, error: "Focus unavailable" }));
54
+ this.focusStrip =
55
+ options.focusStrip ?? (async () => ({ ok: false, error: "Strip focus unavailable" }));
52
56
  this.server = net.createServer((socket) => this.handleConnection(socket));
53
57
  this.tick = null;
54
58
  }
@@ -98,12 +102,18 @@ export class ActivityStripBroker extends EventEmitter {
98
102
  handleConnection(socket) {
99
103
  let buffer = "";
100
104
  socket.setEncoding("utf8");
105
+ socket.setTimeout?.(ACTIVITY_STRIP_CLIENT_IDLE_TIMEOUT_MS, () => socket.destroy());
101
106
  socket.on("error", (error) => {
102
107
  if (!isExpectedClientDisconnect(error)) this.emit("client-error", error);
103
108
  });
104
109
 
105
110
  socket.on("data", (chunk) => {
106
111
  buffer += chunk;
112
+ if (Buffer.byteLength(buffer, "utf8") > ACTIVITY_STRIP_MAX_MESSAGE_BYTES) {
113
+ this.reply(socket, { ok: false, error: "Activity strip broker message is too large." });
114
+ socket.destroy();
115
+ return;
116
+ }
107
117
  let newlineIndex = buffer.indexOf("\n");
108
118
  while (newlineIndex >= 0) {
109
119
  const line = buffer.slice(0, newlineIndex);
@@ -139,26 +149,52 @@ export class ActivityStripBroker extends EventEmitter {
139
149
  });
140
150
  return;
141
151
  case "focus":
142
- Promise.resolve(this.focusSession(String(message.sessionId ?? "")))
152
+ Promise.resolve(this.focusSession(String(message.cardId ?? message.sessionId ?? "")))
143
153
  .then((result) => this.reply(socket, { type: "focus", ...result }))
144
154
  .catch(() =>
145
155
  this.reply(socket, { ok: false, type: "focus", error: "Focus failed closed." }),
146
156
  );
147
157
  return;
158
+ case "focus-strip":
159
+ Promise.resolve(this.focusStrip())
160
+ .then((result) => this.reply(socket, { type: "focus-strip", ...result }))
161
+ .catch(() =>
162
+ this.reply(socket, {
163
+ ok: false,
164
+ type: "focus-strip",
165
+ error: "Strip focus failed closed.",
166
+ }),
167
+ );
168
+ return;
148
169
  case "shutdown":
149
170
  this.reply(socket, { ok: true, type: "shutdown" });
150
171
  setTimeout(() => {
151
172
  this.emit("shutdown-requested");
152
173
  }, 20);
153
174
  return;
154
- case "remove":
155
- this.store.remove(message.sessionId);
156
- this.emitSnapshot();
175
+ case "remove": {
176
+ const accepted = this.store.remove(
177
+ String(message.sessionId ?? ""),
178
+ String(message.publisherId ?? ""),
179
+ );
180
+ if (accepted) this.emitSnapshot();
181
+ this.reply(socket, {
182
+ ok: accepted,
183
+ type: "remove",
184
+ ...(accepted ? {} : { error: "Activity strip broker rejected remove." }),
185
+ });
157
186
  return;
158
- case "upsert":
159
- this.store.upsert(message.session);
160
- this.emitSnapshot();
187
+ }
188
+ case "upsert": {
189
+ const accepted = this.store.upsert(message.session);
190
+ if (accepted) this.emitSnapshot();
191
+ this.reply(socket, {
192
+ ok: accepted,
193
+ type: "upsert",
194
+ ...(accepted ? {} : { error: "Activity strip broker rejected upsert." }),
195
+ });
161
196
  return;
197
+ }
162
198
  default:
163
199
  this.reply(socket, { ok: false, error: `Unsupported message type: ${message.type}` });
164
200
  }
@@ -4,43 +4,85 @@
4
4
  // - "changing session retention, upsert semantics, or broker snapshot ordering"
5
5
  // ---
6
6
 
7
- import { ACTIVITY_STRIP_STALE_AFTER_MS } from "../common/constants.mjs";
7
+ import {
8
+ ACTIVITY_STRIP_MAX_SESSIONS,
9
+ ACTIVITY_STRIP_STALE_AFTER_MS,
10
+ } from "../common/constants.mjs";
8
11
  import { normalizeSessionSnapshot, sortSessions } from "../common/protocol.mjs";
9
12
 
10
13
  /** @typedef {import("../common/contracts.ts").SessionSnapshot} SessionSnapshot */
11
14
  /** @typedef {import("../common/contracts.ts").SessionStoreOptions} SessionStoreOptions */
12
15
  /** @typedef {import("../common/contracts.ts").BrokerSnapshot} BrokerSnapshot */
13
16
 
17
+ /**
18
+ * Stable store key. Sessions resumed into a second process keep publishing under
19
+ * their own publisherId so two live processes never fight over one card; legacy
20
+ * publishers without a publisherId keep the bare sessionId key.
21
+ * @param {SessionSnapshot} session
22
+ */
23
+ function sessionKey(session) {
24
+ return session.publisherId ? `${session.sessionId}|${session.publisherId}` : session.sessionId;
25
+ }
26
+
14
27
  export class SessionStore {
15
28
  /** @param {SessionStoreOptions} [options] */
16
29
  constructor(options = {}) {
17
30
  this.staleAfterMs = options.staleAfterMs ?? ACTIVITY_STRIP_STALE_AFTER_MS;
18
- /** @type {Map<string, SessionSnapshot>} */
31
+ this.maxSessions = options.maxSessions ?? ACTIVITY_STRIP_MAX_SESSIONS;
32
+ /** @type {Map<string, {session: SessionSnapshot; receivedAt: number}>} */
19
33
  this.sessions = new Map();
34
+ /** @type {Map<string, number>} */
35
+ this.tombstones = new Map();
20
36
  }
21
37
 
22
- /** @param {Partial<SessionSnapshot> | Record<string, unknown>} session */
23
- upsert(session) {
38
+ /** @param {Partial<SessionSnapshot> | Record<string, unknown>} session @param {number} [receivedAt] */
39
+ upsert(session, receivedAt = Date.now()) {
40
+ if (!session || typeof session !== "object") return false;
41
+ this.purge(receivedAt);
24
42
  const normalized = normalizeSessionSnapshot(session);
25
43
  if (!normalized.sessionId) return false;
26
- this.sessions.set(normalized.sessionId, normalized);
44
+ const key = sessionKey(normalized);
45
+ if (this.tombstones.has(key)) return false;
46
+ const current = this.sessions.get(key);
47
+ if (
48
+ current &&
49
+ current.session.publisherSequence > 0 &&
50
+ (normalized.publisherSequence === 0 ||
51
+ normalized.publisherSequence <= current.session.publisherSequence)
52
+ ) {
53
+ return false;
54
+ }
55
+ if (!current && this.sessions.size >= this.maxSessions) return false;
56
+ this.sessions.set(key, { session: normalized, receivedAt });
27
57
  return true;
28
58
  }
29
59
 
30
- /** @param {string} sessionId */
31
- remove(sessionId) {
32
- return this.sessions.delete(String(sessionId ?? ""));
60
+ /** @param {string} sessionId @param {string} [publisherId] @param {number} [removedAt] */
61
+ remove(sessionId, publisherId, removedAt = Date.now()) {
62
+ const id = String(sessionId ?? "");
63
+ if (!id) return false;
64
+ if (!publisherId) return this.sessions.delete(id);
65
+ const key = `${id}|${String(publisherId)}`;
66
+ this.purge(removedAt);
67
+ if (!this.sessions.has(key)) return false;
68
+ if (!this.tombstones.has(key) && this.tombstones.size >= this.maxSessions) return false;
69
+ this.sessions.delete(key);
70
+ this.tombstones.set(key, removedAt);
71
+ return true;
33
72
  }
34
73
 
35
74
  /** @param {number} [now] */
36
75
  purge(now = Date.now()) {
37
76
  let changed = false;
38
- for (const [sessionId, session] of this.sessions.entries()) {
39
- if (now - session.updatedAt > this.staleAfterMs) {
77
+ for (const [sessionId, entry] of this.sessions.entries()) {
78
+ if (now - entry.receivedAt > this.staleAfterMs) {
40
79
  this.sessions.delete(sessionId);
41
80
  changed = true;
42
81
  }
43
82
  }
83
+ for (const [sessionId, removedAt] of this.tombstones.entries()) {
84
+ if (now - removedAt > this.staleAfterMs) this.tombstones.delete(sessionId);
85
+ }
44
86
  return changed;
45
87
  }
46
88
 
@@ -49,7 +91,7 @@ export class SessionStore {
49
91
  this.purge(now);
50
92
  return {
51
93
  generatedAt: now,
52
- sessions: sortSessions([...this.sessions.values()]),
94
+ sessions: sortSessions([...this.sessions.values()].map((entry) => entry.session)),
53
95
  };
54
96
  }
55
97
  }
@@ -112,10 +112,31 @@ export async function requestBrokerShutdown(options = {}) {
112
112
 
113
113
  /** @param {SessionSnapshot} session @param {BrokerClientOptions} [options] */
114
114
  export async function publishSessionSnapshot(session, options = {}) {
115
- await sendBrokerMessage(makeMessage("upsert", { session }), options);
115
+ const result = await sendBrokerMessage(makeMessage("upsert", { session }), {
116
+ ...options,
117
+ expectReply: true,
118
+ });
119
+ if (result?.ok !== true)
120
+ throw new Error(result?.error || "Activity strip broker rejected upsert.");
116
121
  }
117
122
 
118
- /** @param {string} sessionId @param {BrokerClientOptions} [options] */
119
- export async function removeSession(sessionId, options = {}) {
120
- await sendBrokerMessage(makeMessage("remove", { sessionId }), options);
123
+ /** @param {{ sessionId: string; publisherId: string }} session @param {BrokerClientOptions} [options] */
124
+ export async function removeSession(session, options = {}) {
125
+ /** @type {Record<string, unknown>} */
126
+ let record = {};
127
+ let fallbackId = "";
128
+ if (session && typeof session === "object") {
129
+ record = session;
130
+ } else {
131
+ fallbackId = String(session ?? "");
132
+ }
133
+ const result = await sendBrokerMessage(
134
+ makeMessage("remove", {
135
+ sessionId: String(record.sessionId ?? fallbackId),
136
+ publisherId: String(record.publisherId ?? ""),
137
+ }),
138
+ { ...options, expectReply: true },
139
+ );
140
+ if (result?.ok !== true)
141
+ throw new Error(result?.error || "Activity strip broker rejected remove.");
121
142
  }