@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 +2 -0
- package/package.json +6 -3
- package/src/bridge.ts +21 -4
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
|
+
"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": "^
|
|
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
|
|