@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 ADDED
@@ -0,0 +1,78 @@
1
+ MIT License (with OpenAI/Anthropic/xAI/PRC Frontier Labs Rider)
2
+
3
+ Copyright (c) contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ ADDITIONAL RIDER / RESTRICTION (OpenAI / Anthropic / xAI / PRC Frontier Labs):
13
+
14
+ This rider is part of the "conditions" of this License. In the event of any
15
+ conflict between this rider and any other portion of this License, this rider
16
+ controls.
17
+
18
+ "Restricted Parties" means OpenAI, L.L.C.; Anthropic, PBC; xAI Corp.; and the
19
+ following PRC frontier AI labs and related entities: Baidu (including ERNIE-
20
+ related entities), Alibaba and Alibaba Cloud (including Qwen-related entities),
21
+ Tencent (including Hunyuan-related entities), ByteDance (including Doubao/Seed-
22
+ related entities), DeepSeek, Zhipu AI, Moonshot AI, MiniMax, 01.AI
23
+ (Lingyiwanwu), and iFlyTek. "Restricted Parties" also includes any of their
24
+ respective Affiliates and any person or entity acting directly or indirectly
25
+ on behalf of, for the benefit of, or under the direction of any of the
26
+ foregoing (including any officer, director, employee, contractor, agent,
27
+ consultant, service provider, or representative).
28
+
29
+ Notwithstanding any other provision of this License, no rights are granted to
30
+ any Restricted Party. Any purported license, sublicense, assignment, transfer,
31
+ or other permission to any Restricted Party is null and void absent the
32
+ express prior written permission of the copyright holders.
33
+
34
+ You may not provide, disclose, distribute, sublicense, sell, lease, lend,
35
+ host, make available, or otherwise permit access to the Software or any
36
+ derivative work of the Software (as defined in applicable copyright law)
37
+ (collectively, "Derivative Works") to or for any Restricted Party.
38
+
39
+ For purposes of this rider, "use" includes, without limitation: copying,
40
+ modifying, merging, publishing, distributing, sublicensing, selling,
41
+ transferring, making available, hosting, deploying, executing, benchmarking,
42
+ testing, analyzing, indexing, or incorporating the Software or any Derivative
43
+ Works into any dataset, training corpus, evaluation harness, or pipeline for
44
+ machine learning or other automated systems.
45
+
46
+ This rider applies to the Software and all Derivative Works. As a condition of
47
+ use, you agree that this rider is a precondition to exercising any rights
48
+ under this License, and you agree that any distribution of the Software or any
49
+ Derivative Works must include this rider provision unmodified.
50
+
51
+ Any breach of this rider automatically and immediately terminates the
52
+ permissions granted by this License. Upon termination, you must immediately
53
+ cease all use and distribution of the Software and any Derivative Works and
54
+ destroy all copies under your control.
55
+
56
+ You agree that a breach of this rider would cause irreparable harm and that
57
+ the copyright holders may seek injunctive or other equitable relief to enforce
58
+ this rider, in addition to any other remedies available at law. To the maximum
59
+ extent permitted by applicable law, the prevailing party in any action to
60
+ enforce this rider shall be entitled to recover reasonable attorneys' fees and
61
+ costs.
62
+
63
+ For purposes of this rider, "Affiliate" means any entity that directly or
64
+ indirectly controls, is controlled by, or is under common control with the
65
+ specified party. "Control" means ownership of more than 50% of the voting
66
+ securities or other ownership interest, or the power to direct management or
67
+ policies by contract or otherwise.
68
+
69
+ The above copyright notice and this permission notice shall be included in all
70
+ copies or substantial portions of the Software.
71
+
72
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
74
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
75
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
76
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
77
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
78
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ ---
2
+ summary: "Top-row live activity strip for local Pi sessions running in Ghostty or other terminals."
3
+ read_when:
4
+ - "Starting work in this package workspace."
5
+ - "Installing or verifying the activity strip in Pi."
6
+ system4d:
7
+ container: "Monorepo package for a local broker + Electron overlay + Pi extension telemetry seam."
8
+ compass: "Make Pi session activity visible at a glance without changing the operator's normal terminal workflow."
9
+ engine: "Pi extension emits session telemetry -> local broker aggregates -> top-row Electron strip renders live state."
10
+ fog: "Main risks are runtime drift across Pi host versions, Electron availability, and stale-session behavior under real long-running work."
11
+ ---
12
+
13
+ # @tryinget/pi-activity-strip
14
+
15
+ A Pi extension package that gives you a **screen-top activity strip** showing what your live Pi sessions are doing.
16
+
17
+ This package is designed for the exact workflow you asked for:
18
+ - multiple Ghostty tabs
19
+ - multiple Pi sessions
20
+ - a persistent top-row ribbon
21
+ - fine-grained live detail without changing how you normally run Pi
22
+
23
+ ## What it does
24
+
25
+ - auto-starts a local top-row overlay when Pi starts in a TUI session
26
+ - tracks each active Pi session independently
27
+ - shows one card per live session
28
+ - surfaces:
29
+ - repo/session label
30
+ - current phase
31
+ - current tool or target
32
+ - fine-grained detail text
33
+ - elapsed time plus last-seen freshness
34
+ - state color (`thinking`, `tool`, `waiting`, `done`, `error`)
35
+ - keeps a local broker so multiple Pi processes can report into one strip
36
+
37
+ ## Architecture
38
+
39
+ ```text
40
+ Pi session
41
+ -> activity-strip extension
42
+ -> local unix-socket broker
43
+ -> Electron top-row overlay
44
+ ```
45
+
46
+ This is intentionally **local-first**.
47
+ It does not require moving your workflow onto `pi-server` first.
48
+
49
+ ## Current scope
50
+
51
+ Implemented now:
52
+ - local per-host broker
53
+ - primary-display top-row strip
54
+ - one card per active Pi session
55
+ - headless-safe telemetry publishing
56
+ - explicit open/status/doctor/snapshot/fix-top/stop commands
57
+ - local visual capture helpers so the agent can inspect the strip directly
58
+
59
+ Not implemented yet:
60
+ - multi-monitor strip replication
61
+ - historical timeline / expand-on-hover detail
62
+ - remote observers via `pi-server`
63
+
64
+ ## Installation in Pi
65
+
66
+ From this package directory:
67
+
68
+ ```bash
69
+ cd ~/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip
70
+ pi install "$PWD"
71
+ ```
72
+
73
+ Then for **existing Pi tabs**:
74
+ - run `/reload` in each tab you want tracked
75
+
76
+ For **new Pi tabs**:
77
+ - the package will load automatically from your Pi settings
78
+
79
+ ## Operator commands
80
+
81
+ ### Package-local CLI
82
+
83
+ ```bash
84
+ npm run strip:open
85
+ npm run strip:status
86
+ npm run strip:doctor
87
+ npm run strip:snapshot
88
+ npm run strip:fix-top
89
+ npm run strip:stop
90
+ ```
91
+
92
+ or directly:
93
+
94
+ ```bash
95
+ node ./bin/pi-activity-strip.mjs open
96
+ node ./bin/pi-activity-strip.mjs status
97
+ node ./bin/pi-activity-strip.mjs doctor
98
+ node ./bin/pi-activity-strip.mjs snapshot
99
+ node ./bin/pi-activity-strip.mjs fix-top
100
+ ```
101
+
102
+ ### Pi slash commands
103
+
104
+ Inside Pi:
105
+
106
+ ```text
107
+ /activity-strip
108
+ /activity-strip status
109
+ /activity-strip doctor
110
+ /activity-strip fix-top
111
+ /activity-strip stop
112
+ /activity-strip-stop
113
+ ```
114
+
115
+ In Pi with UI support:
116
+ - `/activity-strip status` opens a detailed runtime status report when an editor surface is available
117
+ - `/activity-strip doctor` opens the host-compatibility report
118
+
119
+ ## Verification commands
120
+
121
+ ### Package checks
122
+
123
+ ```bash
124
+ npm install
125
+ npm run check
126
+ npm run release:check:quick
127
+ ```
128
+
129
+ ### Run the strip locally
130
+
131
+ ```bash
132
+ npm run strip:open
133
+ npm run strip:status
134
+ npm run strip:doctor
135
+ npm run strip:snapshot
136
+ ```
137
+
138
+ ### Capture what the agent should inspect
139
+
140
+ ```bash
141
+ npm run capture:strip # just the Pi activity strip window
142
+ npm run capture:top # top band of the focused output, including the strip + upper window area
143
+ ```
144
+
145
+ These are specifically useful so the agent can inspect the current visual state without you manually posting screenshots.
146
+
147
+ ### Simulate multiple sessions
148
+
149
+ ```bash
150
+ npm run demo:simulate
151
+ ```
152
+
153
+ ### Real Pi smoke on the live broker
154
+
155
+ ```bash
156
+ npm run smoke:headless-live
157
+ ```
158
+
159
+ This smoke:
160
+ - opens the strip
161
+ - runs a real headless Pi session with this extension loaded
162
+ - exercises a real tool call
163
+ - verifies that the broker observed the session while it was active
164
+
165
+ ### Compatibility diagnostics
166
+
167
+ ```bash
168
+ npm run strip:doctor
169
+ node ./bin/pi-activity-strip.mjs doctor --json
170
+ ```
171
+
172
+ Use `doctor` before opening the strip when the host/display assumptions are uncertain. It reports:
173
+ - whether a graphical session is present
174
+ - whether Electron can be resolved
175
+ - whether Niri-specific top-edge repair is available
176
+ - whether the current setup is multi-display even though the strip remains primary-display-only
177
+
178
+ ## Environment controls
179
+
180
+ - `PI_ACTIVITY_STRIP_AUTO_START=0`
181
+ - disable automatic strip opening on Pi session start
182
+ - `PI_ACTIVITY_STRIP_CLICK_THROUGH=0`
183
+ - make the overlay clickable instead of mouse-transparent
184
+ - `PI_ACTIVITY_STRIP_ELECTRON_BIN=/path/to/electron`
185
+ - override Electron binary discovery
186
+ - `GLIMPSE_ELECTRON_BIN=/path/to/electron`
187
+ - shared Electron override also respected
188
+
189
+ ## Practical usage for your Ghostty tabs
190
+
191
+ If you want this for all current tabs:
192
+
193
+ 1. install the package once with `pi install`
194
+ 2. run `/reload` inside each already-open Pi tab
195
+ 3. open the strip once with `/activity-strip` or `npm run strip:open`
196
+ 4. from then on, every loaded Pi session should report into the same top-row ribbon
197
+
198
+ ## References
199
+
200
+ - [Project vision](docs/project/vision.md)
201
+ - [Project resources](docs/project/resources.md)
202
+ - [Verification notes](docs/project/verification.md)
203
+ - [Next session prompt](next_session_prompt.md)
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import { execFile, spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import {
8
+ getBrokerStatus,
9
+ isBrokerAlive,
10
+ requestBrokerShutdown,
11
+ sendBrokerMessage,
12
+ } from "../src/client/broker-client.mjs";
13
+ import {
14
+ assessActivityStripCompatibility,
15
+ formatCompatibilityReport,
16
+ } from "../src/common/compatibility.mjs";
17
+ import { ACTIVITY_STRIP_START_TIMEOUT_MS } from "../src/common/constants.mjs";
18
+ import { locateElectron } from "../src/common/electron.mjs";
19
+ import { makeMessage } from "../src/common/protocol.mjs";
20
+ import { formatBrokerRuntimeStatus } from "../src/common/status-report.mjs";
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+ const electronEntry = path.resolve(__dirname, "..", "src", "electron", "main.mjs");
25
+ const execFileAsync = promisify(execFile);
26
+
27
+ function usage() {
28
+ console.log(
29
+ `Usage: pi-activity-strip <open|status|doctor|snapshot|fix-top|stop|serve> [--json]\n\nCommands:\n open Start the top-row activity strip if it is not already running\n status Check broker + overlay readiness and surface runtime warnings\n doctor Inspect host compatibility assumptions before opening the strip\n snapshot Print the current broker snapshot as JSON\n fix-top Move the strip window flush to the top edge in Niri\n stop Ask the running strip to shut down\n serve Internal helper; starts the Electron shell in the foreground\n`,
30
+ );
31
+ }
32
+
33
+ async function moveStripToTop() {
34
+ const { stdout } = await execFileAsync("niri", ["msg", "-j", "windows"], {
35
+ env: process.env,
36
+ });
37
+ const windows = JSON.parse(stdout);
38
+ if (!Array.isArray(windows)) {
39
+ throw new Error("Unexpected niri windows payload");
40
+ }
41
+
42
+ const stripWindow = windows.find((window) => window?.title === "Pi Activity Strip");
43
+ if (!stripWindow?.id) {
44
+ throw new Error("Could not find Pi Activity Strip window in niri");
45
+ }
46
+
47
+ const currentY = Number(stripWindow.layout?.tile_pos_in_workspace_view?.[1] ?? 0);
48
+ if (Math.abs(currentY) < 1) {
49
+ return 0;
50
+ }
51
+
52
+ await execFileAsync(
53
+ "niri",
54
+ [
55
+ "msg",
56
+ "action",
57
+ "move-floating-window",
58
+ "--id",
59
+ String(stripWindow.id),
60
+ "-y",
61
+ String(-Math.round(currentY)),
62
+ ],
63
+ { env: process.env },
64
+ );
65
+
66
+ console.log("Moved activity strip to the top edge.");
67
+ return 0;
68
+ }
69
+
70
+ async function waitForBrokerReady(timeoutMs = ACTIVITY_STRIP_START_TIMEOUT_MS) {
71
+ const timeoutAt = Date.now() + timeoutMs;
72
+ /** @type {import("../src/common/contracts.ts").BrokerResponse | null} */
73
+ let latestStatus = null;
74
+
75
+ while (Date.now() < timeoutAt) {
76
+ try {
77
+ latestStatus = await getBrokerStatus();
78
+ } catch {
79
+ latestStatus = null;
80
+ }
81
+
82
+ if (
83
+ latestStatus?.ok &&
84
+ (!latestStatus.runtimeStatus || latestStatus.runtimeStatus.state === "ready")
85
+ ) {
86
+ return { ok: true };
87
+ }
88
+ if (latestStatus?.runtimeStatus?.state === "error") {
89
+ return { ok: false, error: formatBrokerRuntimeStatus(latestStatus) };
90
+ }
91
+
92
+ await delay(125);
93
+ }
94
+
95
+ return {
96
+ ok: false,
97
+ error: `${formatBrokerRuntimeStatus(latestStatus || { ok: false })}\nTimeout: ${timeoutMs}ms`,
98
+ };
99
+ }
100
+
101
+ /** @param {{ detached?: boolean }} [options] @returns {Promise<number>} */
102
+ async function openStrip({ detached = true } = {}) {
103
+ if (await isBrokerAlive()) {
104
+ console.log("Activity strip is already running.");
105
+ return 0;
106
+ }
107
+
108
+ const compatibility = await assessActivityStripCompatibility();
109
+ if (!compatibility.ok) {
110
+ console.error(formatCompatibilityReport(compatibility));
111
+ return 1;
112
+ }
113
+
114
+ const electron = await locateElectron();
115
+ const child = spawn(electron, [electronEntry], {
116
+ detached,
117
+ stdio: detached ? "ignore" : "inherit",
118
+ env: process.env,
119
+ });
120
+
121
+ if (detached) {
122
+ child.unref();
123
+ const ready = await waitForBrokerReady();
124
+ if (!ready.ok) {
125
+ console.error(ready.error || "Activity strip did not become ready.");
126
+ return 1;
127
+ }
128
+ console.log("Started activity strip.");
129
+ return 0;
130
+ }
131
+
132
+ return await new Promise((resolve) => {
133
+ child.on("exit", (code) => resolve(typeof code === "number" ? code : 0));
134
+ });
135
+ }
136
+
137
+ async function main() {
138
+ const command = process.argv[2] ?? "open";
139
+ const jsonOutput = process.argv.includes("--json");
140
+
141
+ switch (command) {
142
+ case "open":
143
+ process.exitCode = await openStrip({ detached: true });
144
+ return;
145
+ case "serve":
146
+ process.exitCode = await openStrip({ detached: false });
147
+ return;
148
+ case "status": {
149
+ try {
150
+ const result = await getBrokerStatus({ expectReply: true });
151
+ if (jsonOutput) {
152
+ console.log(JSON.stringify(result, null, 2));
153
+ } else {
154
+ console.log(formatBrokerRuntimeStatus(result));
155
+ }
156
+ process.exitCode =
157
+ result?.ok && (!result.runtimeStatus || result.runtimeStatus.state === "ready") ? 0 : 1;
158
+ } catch {
159
+ console.log(
160
+ jsonOutput ? JSON.stringify({ ok: false, state: "stopped" }, null, 2) : "stopped",
161
+ );
162
+ process.exitCode = 1;
163
+ }
164
+ return;
165
+ }
166
+ case "doctor": {
167
+ const report = await assessActivityStripCompatibility();
168
+ if (jsonOutput) {
169
+ console.log(JSON.stringify(report, null, 2));
170
+ } else {
171
+ console.log(formatCompatibilityReport(report));
172
+ }
173
+ process.exitCode = report.ok ? 0 : 1;
174
+ return;
175
+ }
176
+ case "snapshot": {
177
+ try {
178
+ const result = await sendBrokerMessage(makeMessage("ping"), { expectReply: true });
179
+ console.log(JSON.stringify(result?.snapshot ?? { sessions: [] }, null, 2));
180
+ process.exitCode = result?.ok ? 0 : 1;
181
+ } catch (error) {
182
+ console.error(error instanceof Error ? error.message : String(error));
183
+ process.exitCode = 1;
184
+ }
185
+ return;
186
+ }
187
+ case "fix-top": {
188
+ try {
189
+ process.exitCode = await moveStripToTop();
190
+ } catch (error) {
191
+ console.error(error instanceof Error ? error.message : String(error));
192
+ process.exitCode = 1;
193
+ }
194
+ return;
195
+ }
196
+ case "stop": {
197
+ try {
198
+ const result = await requestBrokerShutdown();
199
+ console.log(result?.ok ? "stopping" : "not-running");
200
+ process.exitCode = result?.ok ? 0 : 1;
201
+ } catch {
202
+ console.log("not-running");
203
+ process.exitCode = 1;
204
+ }
205
+ return;
206
+ }
207
+ case "-h":
208
+ case "--help":
209
+ usage();
210
+ process.exitCode = 0;
211
+ return;
212
+ default:
213
+ usage();
214
+ console.error(`Unknown command: ${command}`);
215
+ process.exitCode = 1;
216
+ }
217
+ }
218
+
219
+ main().catch((error) => {
220
+ if (error instanceof Error) {
221
+ console.error(error.stack ?? error.message);
222
+ } else {
223
+ console.error(String(error));
224
+ }
225
+ process.exit(1);
226
+ });
@@ -0,0 +1,64 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { publishSessionSnapshot, removeSession } from "../src/client/broker-client.mjs";
3
+ import { createInitialSnapshot } from "../src/common/telemetry.mjs";
4
+
5
+ function makeSession(label, cwd, state, phase, detail, offset = 0) {
6
+ const snapshot = createInitialSnapshot({ cwd, sessionName: label });
7
+ snapshot.state = state;
8
+ snapshot.phase = phase;
9
+ snapshot.detail = detail;
10
+ snapshot.agentActive = state === "tool" || state === "thinking" || state === "waiting";
11
+ snapshot.agentStartedAt = snapshot.startedAt - offset;
12
+ snapshot.updatedAt = Date.now();
13
+ return snapshot;
14
+ }
15
+
16
+ const sessions = [
17
+ makeSession(
18
+ "api refactor",
19
+ "~/ai-society/softwareco/owned/pi-server",
20
+ "tool",
21
+ "Editing file",
22
+ "src/routes/session.ts",
23
+ 18000,
24
+ ),
25
+ makeSession(
26
+ "vault replay",
27
+ "~/ai-society/softwareco/owned/pi-extensions/packages/pi-vault-client",
28
+ "thinking",
29
+ "Thinking",
30
+ "Replaying execution receipt and comparing current render path",
31
+ 9000,
32
+ ),
33
+ makeSession(
34
+ "frontend polish",
35
+ "~/ai-society/softwareco/owned/pi-web",
36
+ "waiting",
37
+ "Waiting for input",
38
+ "interactive form is open",
39
+ 22000,
40
+ ),
41
+ ];
42
+
43
+ for (const session of sessions) {
44
+ await publishSessionSnapshot(session);
45
+ }
46
+
47
+ await delay(1500);
48
+ sessions[0].detail = "tests/session-contract.test.mjs";
49
+ sessions[0].updatedAt = Date.now();
50
+ await publishSessionSnapshot(sessions[0]);
51
+
52
+ await delay(1500);
53
+ sessions[1].state = "success";
54
+ sessions[1].phase = "Done";
55
+ sessions[1].detail = "Replay matches stored receipt";
56
+ sessions[1].agentActive = false;
57
+ sessions[1].agentStartedAt = null;
58
+ sessions[1].updatedAt = Date.now();
59
+ await publishSessionSnapshot(sessions[1]);
60
+
61
+ await delay(2000);
62
+ for (const session of sessions) {
63
+ await removeSession(session.sessionId);
64
+ }