@yunazgr/pi-companion 0.2.3 → 0.2.4

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 CHANGED
@@ -167,6 +167,8 @@ Paired devices (name, browser user agent, pairing and last-seen times, credentia
167
167
 
168
168
  A phone pairs once with the daemon. Individual Pi sessions still opt in using /companion.
169
169
 
170
+ 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.
171
+
170
172
  ## Settings
171
173
 
172
174
  The **Settings** page (local console only) covers:
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.4",
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