@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.
Files changed (3) hide show
  1. package/README.md +26 -5
  2. package/package.json +6 -3
  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), with short titles, workspace/model metadata, View details and safe Archive actions. Paired devices only see shared sessions |
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, Settings | local console only | Pairing, connection status, disconnect and revoke; daemon settings with a save bar that appears only for unsaved edits |
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.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",
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": "^12.1.0",
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) { ws.close(); return; }
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
- try { void this.handle(JSON.parse(raw.toString()) as ServerMessage); }
137
- catch (error) { this.send({ type: "error", message: "Invalid server message: " + String(error) }); }
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