@yunazgr/pi-companion 0.2.3 → 0.2.5
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 +26 -5
- package/package.json +6 -3
- package/src/bridge.ts +21 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The UI is a static SvelteKit application. There is no Node runtime in production
|
|
|
13
13
|
<img src="docs/mobile.webp" alt="Session detail on a phone" width="28%" />
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
Screenshots use demo sessions, not private conversation data. See the [session menu](docs/session-menu.webp), [desktop session rows](docs/session-list-desktop.webp) and [mobile session list](docs/session-list.webp), plus [attachment previews](docs/attachments.webp), [file-grouped changes](docs/changes.webp) and [connection recovery](docs/connection-error.webp).
|
|
16
|
+
Screenshots use demo sessions, not private conversation data, and are regenerated with `node tools/screenshots.mjs` (see [Running locally from source](#running-locally-from-source)). See the [session menu](docs/session-menu.webp), [desktop session rows](docs/session-list-desktop.webp) and [mobile session list](docs/session-list.webp), plus [attachment previews](docs/attachments.webp), [file-grouped changes](docs/changes.webp) and [connection recovery](docs/connection-error.webp).
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -115,6 +115,7 @@ Optional environment variables (none are required):
|
|
|
115
115
|
| PI_COMPANION_AUTOSTART=0 | never launch a daemon, only connect |
|
|
116
116
|
| PI_COMPANION_PUBLIC_URL | daemon-side: public URL embedded in pairing QR codes |
|
|
117
117
|
| PI_COMPANION_ALLOWED_ORIGINS | daemon-side: extra comma-separated browser origins accepted besides the loopback addresses and the public URL |
|
|
118
|
+
| PI_COMPANION_ADMIN_ADDR, PI_COMPANION_DEVICE_ADDR | daemon-side: loopback listen addresses (default `127.0.0.1:43721` / `127.0.0.1:43722`), for running a second daemon next to the usual one, e.g. for screenshots. The extension still looks for 43721 unless `PI_COMPANION_URL` points elsewhere |
|
|
118
119
|
|
|
119
120
|
Local admin:
|
|
120
121
|
|
|
@@ -148,6 +149,16 @@ Pi's `companion_ask_user` tool asks one to four questions at once. Each question
|
|
|
148
149
|
|
|
149
150
|
Dialogs from other extensions (`ctx.ui.select`, `ctx.ui.confirm`, `ctx.ui.input`) are relayed to the same sheet while the terminal dialog stays open: whichever side answers first wins and the other closes. Question tools that draw their own `ctx.ui.custom` picker are relayed through a small adapter: pi-jar's `jar_ask` is supported, and a companion answer completes its terminal picker. Other `ctx.ui.custom` components and `ctx.ui.editor` stay terminal-only because they cannot be answered from outside. Pending questions are part of the session snapshot, so a browser that connects later still sees them.
|
|
150
151
|
|
|
152
|
+
## Notifications, camera and catching up
|
|
153
|
+
|
|
154
|
+
Settings → **Notifications and camera** (on every device, not just the console) asks the browser for both permissions from a tap, as browsers require, and shows whether each is allowed, blocked or unavailable. Overview also offers a one-time "Turn on notifications" banner.
|
|
155
|
+
|
|
156
|
+
With notifications on, the device gets a system notification (through the service worker, so it works for installed apps and Android) when Pi asks a question, finishes a turn or a session ends. Tapping it opens that session. Nothing is sent while you are already looking at that session. iPhone and iPad only allow web notifications from an app added to the Home Screen. The camera is used only for the pairing QR scanner.
|
|
157
|
+
|
|
158
|
+
Activity no longer lives only in the open tab. The daemon keeps a bounded, in-memory log of each session's recent feed (streamed text is merged, up to 800 entries) at `GET /api/sessions/{id}/activity` on both surfaces (paired devices: shared sessions only). The UI rebuilds the feed from it when a session opens and after every reconnect or resync, so a phone that slept, dropped its connection or was closed catches up without sending a new message. Prompts, steers and answers from any device are added to that log too. The log is cleared when the daemon restarts or the session is archived.
|
|
159
|
+
|
|
160
|
+
Attaching files uses plain HTTP, so it keeps working while the live socket is reconnecting (common right after a phone's file picker closes); only a deliberate disconnect or revoked access blocks it.
|
|
161
|
+
|
|
151
162
|
## Pairing and devices
|
|
152
163
|
|
|
153
164
|
Pairing is daemon-wide rather than session-specific. Everything lives on the **Devices** page of the local console (port 43721):
|
|
@@ -167,6 +178,8 @@ Paired devices (name, browser user agent, pairing and last-seen times, credentia
|
|
|
167
178
|
|
|
168
179
|
A phone pairs once with the daemon. Individual Pi sessions still opt in using /companion.
|
|
169
180
|
|
|
181
|
+
Network loss and daemon restarts reconnect automatically with bounded backoff. Heartbeats detect silent connections, and returning to the foreground or restoring network access retries immediately. Pairing credentials and unsent drafts survive outages; session snapshots, pending questions, and previously loaded upload lists refresh on reconnect. Missed activity is not replayed, and prompts/answers are never automatically resent. A deliberate **Disconnect** waits for the device’s explicit retry; **Revoke** removes access and requires pairing again.
|
|
182
|
+
|
|
170
183
|
## Settings
|
|
171
184
|
|
|
172
185
|
The **Settings** page (local console only) covers:
|
|
@@ -246,9 +259,10 @@ A single-page SvelteKit app, embedded in the daemon binary:
|
|
|
246
259
|
| Page | Who sees it | What it is for |
|
|
247
260
|
|---|---|---|
|
|
248
261
|
| Overview | everyone | Dot-field hero, live counts, sessions that need your answer, a labeled live-session table, and a spaced onboarding/help panel with Claymorphism actions |
|
|
249
|
-
| Sessions | everyone | Searchable, filterable full-width session rows (Working / Waiting / Ended)
|
|
262
|
+
| Sessions | everyone | Searchable, filterable full-width session rows (Working / Waiting / Ended); the whole row opens the session, the status badge sits top-right, and ended sessions offer a safe Archive action. Paired devices only see shared sessions |
|
|
250
263
|
| Session detail | everyone | Activity-first transcript with Markdown, highlighted code, Mermaid diagrams, collapsible tool output/thinking and question sheets. Compact header and navigation leave more space for the shell. The expanding composer includes Auto/Plan selection, attachment previews and a full-width labeled Send/Steer action; Plan uses the existing `/plan` command, not an agent permission setting. The top-right session menu opens Shared files, Changes and Archive session. The session header shows a status dot; clicking its title reveals status and session metadata. Changes are grouped by file with clear separators and a filename filter |
|
|
251
|
-
| Devices
|
|
264
|
+
| Devices | local console only | Pairing, connection status, disconnect and revoke |
|
|
265
|
+
| Settings | everyone | Theme, notification and camera permissions; on the console also daemon settings with a save bar that appears only for unsaved edits |
|
|
252
266
|
| Help, Privacy, Terms | everyone | Setup stepper, commands, tools, troubleshooting; data handling; terms of use. Help is opened from the Overview |
|
|
253
267
|
|
|
254
268
|
The Live indicator in the header and sidebar opens the connection sheet (status, version and, on this computer, the console and device addresses and data folders).
|
|
@@ -266,7 +280,7 @@ Design notes:
|
|
|
266
280
|
- installable PWA: scoped manifest with explicit 192×192 and 512×512 PNG icons plus a maskable icon, standalone display mode, Apple home-screen metadata, and a service worker that caches only the public shell and static assets; API/session traffic and uploads are never cached
|
|
267
281
|
- mobile: bottom tab bar, safe-area insets, 44px touch targets, 16px inputs (no iOS zoom), Enter inserts a newline on touch keyboards
|
|
268
282
|
- accessibility: skip link, visible focus rings, labeled controls and table metadata, accessible session menus, live regions for the feed and questions, reduced-motion support
|
|
269
|
-
- the dot field is Raksara's component, loaded when the browser is idle, paused when hidden, and static under reduced motion
|
|
283
|
+
- the dot field is Raksara's component, loaded when the browser is idle, paused when hidden, and static under reduced motion. It opens on the Pi Companion mark, then morphs through a computer, a phone and a terminal
|
|
270
284
|
- dropping a file anywhere outside the Shared files drop zone is ignored. Without this, the browser tries to open the file itself (Firefox reports this as "may not load or link to file:///")
|
|
271
285
|
|
|
272
286
|
### Caching and updates
|
|
@@ -312,7 +326,7 @@ Clone the repository, then:
|
|
|
312
326
|
|
|
313
327
|
`npm run serve` builds the embedded Svelte UI and starts the Rust daemon in the foreground. It prints:
|
|
314
328
|
|
|
315
|
-
Pi Companion v0.2.
|
|
329
|
+
Pi Companion v0.2.5
|
|
316
330
|
|
|
317
331
|
Console http://127.0.0.1:43721
|
|
318
332
|
Paired devices http://127.0.0.1:43722
|
|
@@ -345,6 +359,13 @@ which proxies /api and /ws to the daemon on 43721.
|
|
|
345
359
|
|
|
346
360
|
If you start Pi from the checkout without running `npm run serve`, running `/companion` can still auto-start a local daemon build if one exists under `server/target/debug` or `server/target/release`. For predictable UI work, prefer `npm run serve` in one terminal and `pi -e ./src/index.ts` in another.
|
|
347
361
|
|
|
362
|
+
To regenerate the README screenshots from demo sessions (your running daemon is left alone; the demo daemon uses ports 43731/43732 and a throwaway data folder):
|
|
363
|
+
|
|
364
|
+
npm run ui:build && cargo build --release --manifest-path server/Cargo.toml
|
|
365
|
+
node tools/screenshots.mjs
|
|
366
|
+
|
|
367
|
+
It needs Playwright and `cwebp`. Set `PLAYWRIGHT=/path/to/node_modules/playwright/index.mjs` if Playwright isn't installed in this repo, and `CHROME=/path/to/chrome` to use an existing browser.
|
|
368
|
+
|
|
348
369
|
To test the same behavior as a published install, stop any development daemon first and install the package normally:
|
|
349
370
|
|
|
350
371
|
pi install npm:@yunazgr/pi-companion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yunazgr/pi-companion",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Local-first remote control for Pi sessions: live activity feed, steering, git diff, file drop and phone pairing from a single Rust daemon.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"highlight.js": "^11.12.0",
|
|
72
72
|
"jsqr": "^1.4.0",
|
|
73
73
|
"marked": "^18.1.0",
|
|
74
|
-
"mermaid": "^
|
|
74
|
+
"mermaid": "^10.9.8",
|
|
75
75
|
"svelte": "^5.57.2",
|
|
76
76
|
"svelte-check": "^4.7.6",
|
|
77
77
|
"typebox": "^1.0.13",
|
|
@@ -79,7 +79,10 @@
|
|
|
79
79
|
"vite": "^8.0.12"
|
|
80
80
|
},
|
|
81
81
|
"overrides": {
|
|
82
|
-
"node-domexception": "file:./tools/shims/node-domexception"
|
|
82
|
+
"node-domexception": "file:./tools/shims/node-domexception",
|
|
83
|
+
"mermaid": {
|
|
84
|
+
"katex": "^0.18.2"
|
|
85
|
+
}
|
|
83
86
|
},
|
|
84
87
|
"allowScripts": {
|
|
85
88
|
"esbuild": true,
|
package/src/bridge.ts
CHANGED
|
@@ -16,6 +16,7 @@ export class CompanionBridge implements AskChannel {
|
|
|
16
16
|
private ws?: WebSocket;
|
|
17
17
|
private ctx?: ExtensionContext;
|
|
18
18
|
private reconnect?: NodeJS.Timeout;
|
|
19
|
+
private heartbeat?: NodeJS.Timeout;
|
|
19
20
|
private closed = false;
|
|
20
21
|
private activated = false;
|
|
21
22
|
private restoreDialogs?: () => void;
|
|
@@ -80,6 +81,7 @@ export class CompanionBridge implements AskChannel {
|
|
|
80
81
|
this.pendingDeletes.clear();
|
|
81
82
|
if (this.reconnect) clearTimeout(this.reconnect);
|
|
82
83
|
this.reconnect = undefined;
|
|
84
|
+
if (this.heartbeat) clearTimeout(this.heartbeat);
|
|
83
85
|
const ws = this.ws;
|
|
84
86
|
this.ws = undefined;
|
|
85
87
|
ws?.close();
|
|
@@ -115,6 +117,9 @@ export class CompanionBridge implements AskChannel {
|
|
|
115
117
|
if (allowSpawn) await ensureDaemon((message, level = "info") => this.log(message, level));
|
|
116
118
|
if (this.closed || !this.snapshot.remoteEnabled) return;
|
|
117
119
|
this.open();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.log("Could not reconnect to Pi Companion: " + String(error), "warning");
|
|
122
|
+
this.scheduleReconnect();
|
|
118
123
|
} finally {
|
|
119
124
|
this.connecting = false;
|
|
120
125
|
}
|
|
@@ -122,10 +127,18 @@ export class CompanionBridge implements AskChannel {
|
|
|
122
127
|
|
|
123
128
|
private open() {
|
|
124
129
|
const base = process.env.PI_COMPANION_URL ?? "ws://127.0.0.1:43721";
|
|
125
|
-
const ws = new WebSocket(base.replace(/\/$/, "") + "/ws/bridge/" + this.sessionId);
|
|
130
|
+
const ws = new WebSocket(base.replace(/\/$/, "") + "/ws/bridge/" + this.sessionId, { handshakeTimeout: 15_000 });
|
|
126
131
|
this.ws = ws;
|
|
132
|
+
const alive = () => {
|
|
133
|
+
if (this.ws !== ws || this.closed) return;
|
|
134
|
+
if (this.heartbeat) clearTimeout(this.heartbeat);
|
|
135
|
+
this.heartbeat = setTimeout(() => ws.terminate(), 45_000);
|
|
136
|
+
this.heartbeat.unref();
|
|
137
|
+
};
|
|
138
|
+
ws.on("ping", alive); // ws automatically pongs; the watchdog detects silent loss.
|
|
127
139
|
ws.on("open", () => {
|
|
128
|
-
if (this.ws !== ws || this.closed || !this.snapshot.remoteEnabled)
|
|
140
|
+
if (this.ws !== ws || this.closed || !this.snapshot.remoteEnabled) return ws.terminate();
|
|
141
|
+
alive();
|
|
129
142
|
this.everConnected = true;
|
|
130
143
|
this.downSince = 0;
|
|
131
144
|
this.retryDelay = 1000;
|
|
@@ -133,11 +146,14 @@ export class CompanionBridge implements AskChannel {
|
|
|
133
146
|
});
|
|
134
147
|
ws.on("message", raw => {
|
|
135
148
|
if (this.ws !== ws || this.closed || !this.snapshot.remoteEnabled) return;
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
alive();
|
|
150
|
+
const failure = (error: unknown) => this.send({ type: "error", message: "Invalid server message: " + String(error) });
|
|
151
|
+
try { void this.handle(JSON.parse(raw.toString()) as ServerMessage).catch(failure); }
|
|
152
|
+
catch (error) { failure(error); }
|
|
138
153
|
});
|
|
139
154
|
ws.on("close", () => {
|
|
140
155
|
if (this.ws !== ws) return;
|
|
156
|
+
if (this.heartbeat) clearTimeout(this.heartbeat);
|
|
141
157
|
if (!this.downSince) this.downSince = Date.now();
|
|
142
158
|
this.scheduleReconnect();
|
|
143
159
|
});
|
|
@@ -156,6 +172,7 @@ export class CompanionBridge implements AskChannel {
|
|
|
156
172
|
this.restoreDialogs = undefined;
|
|
157
173
|
for (const entry of [...this.asks.values()]) entry.settle(null);
|
|
158
174
|
if (this.reconnect) clearTimeout(this.reconnect);
|
|
175
|
+
if (this.heartbeat) clearTimeout(this.heartbeat);
|
|
159
176
|
this.ws?.close();
|
|
160
177
|
}
|
|
161
178
|
|