cmdzero 0.8.3 → 0.8.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/overlay/overlay.js +30 -8
- package/package.json +1 -1
- package/src/server.js +23 -3
package/overlay/overlay.js
CHANGED
|
@@ -1326,14 +1326,36 @@
|
|
|
1326
1326
|
for (const rec of saved) addTweak(rec, true); // chronological replay → newest ends at the bottom
|
|
1327
1327
|
})();
|
|
1328
1328
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1329
|
+
// A restarted daemon leaves the EventSource holding a socket to a process that
|
|
1330
|
+
// is gone: the browser keeps it in readyState OPEN, fires no error and never
|
|
1331
|
+
// reconnects, so alerts stop arriving while the edits behind them still land
|
|
1332
|
+
// on disk. Nothing surfaces that — the tray just quietly stops moving. The
|
|
1333
|
+
// daemon pings every 15s, so silence well past that means the stream is a
|
|
1334
|
+
// zombie; drop it and dial again.
|
|
1335
|
+
const SSE_SILENCE_MS = 45000;
|
|
1336
|
+
let es = null;
|
|
1337
|
+
let lastSeen = Date.now();
|
|
1338
|
+
function connectEvents() {
|
|
1339
|
+
try {
|
|
1340
|
+
if (es) es.close();
|
|
1341
|
+
es = new EventSource(`${ORIGIN}/api/events`);
|
|
1342
|
+
es.onopen = () => { lastSeen = Date.now(); };
|
|
1343
|
+
es.onmessage = (m) => {
|
|
1344
|
+
lastSeen = Date.now();
|
|
1345
|
+
const e = JSON.parse(m.data);
|
|
1346
|
+
if (e.type === 'tweak') addTweak(e);
|
|
1347
|
+
if (e.type === 'totals') showTotals(e.totals);
|
|
1348
|
+
// 'ping' needs no handling — arriving at all is the whole point.
|
|
1349
|
+
};
|
|
1350
|
+
} catch { /* daemon offline */ }
|
|
1351
|
+
}
|
|
1352
|
+
connectEvents();
|
|
1353
|
+
setInterval(() => {
|
|
1354
|
+
if (Date.now() - lastSeen > SSE_SILENCE_MS) {
|
|
1355
|
+
lastSeen = Date.now(); // reset first, so a daemon that is still down doesn't reconnect every tick
|
|
1356
|
+
connectEvents();
|
|
1357
|
+
}
|
|
1358
|
+
}, 5000);
|
|
1337
1359
|
fetch(`${ORIGIN}/api/health`)
|
|
1338
1360
|
.then((r) => r.json())
|
|
1339
1361
|
.then((h) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cmdzero",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Tweak your UI live in the browser and write the changes straight to source. Copy and Tailwind edits cost zero tokens; everything else routes to a right-sized model via headless claude.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/server.js
CHANGED
|
@@ -28,11 +28,18 @@ function detectTailwind(root) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function startServer({ root, port = 4100 }) {
|
|
31
|
+
export function startServer({ root, port = 4100, heartbeatMs = 15000 }) {
|
|
32
32
|
const undoStack = new Map(); // id -> { abs, before }
|
|
33
33
|
const running = new Map(); // id -> child process (for cancellation)
|
|
34
34
|
const sseClients = new Set();
|
|
35
|
-
|
|
35
|
+
// Seeded from the clock, not 1. The overlay persists its alert history in
|
|
36
|
+
// localStorage and keys each row by tweak id, and that history outlives the
|
|
37
|
+
// daemon — so a counter starting over at 1 hands a fresh tweak the id of a row
|
|
38
|
+
// hydrated from an earlier session. addTweak() updates a known id in place, so
|
|
39
|
+
// the newest alert would silently overwrite that old row wherever it sits
|
|
40
|
+
// instead of appending to the bottom of the tray. Still ascending within a
|
|
41
|
+
// session; just never reissued across a restart.
|
|
42
|
+
let nextId = Date.now();
|
|
36
43
|
const tailwind = detectTailwind(root);
|
|
37
44
|
const telemetry = initTelemetry({ version: VERSION, tailwind });
|
|
38
45
|
|
|
@@ -98,7 +105,20 @@ export function startServer({ root, port = 4100 }) {
|
|
|
98
105
|
});
|
|
99
106
|
res.write('\n');
|
|
100
107
|
sseClients.add(res);
|
|
101
|
-
|
|
108
|
+
// Idle streams have to say something. A browser can't tell a quiet
|
|
109
|
+
// daemon from a dead one: the EventSource stays in readyState OPEN,
|
|
110
|
+
// never fires error and never reconnects, so every alert after a
|
|
111
|
+
// restart is dropped while the writes still land on disk. A ping the
|
|
112
|
+
// client can actually see (a comment would not reach onmessage) lets it
|
|
113
|
+
// notice the silence and reconnect.
|
|
114
|
+
const beat = setInterval(() => {
|
|
115
|
+
res.write(`data: ${JSON.stringify({ type: 'ping' })}\n\n`);
|
|
116
|
+
}, heartbeatMs);
|
|
117
|
+
beat.unref?.(); // never hold the process open for a heartbeat
|
|
118
|
+
req.on('close', () => {
|
|
119
|
+
clearInterval(beat);
|
|
120
|
+
sseClients.delete(res);
|
|
121
|
+
});
|
|
102
122
|
return;
|
|
103
123
|
}
|
|
104
124
|
|