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.
@@ -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
- try {
1330
- const es = new EventSource(`${ORIGIN}/api/events`);
1331
- es.onmessage = (m) => {
1332
- const e = JSON.parse(m.data);
1333
- if (e.type === 'tweak') addTweak(e);
1334
- if (e.type === 'totals') showTotals(e.totals);
1335
- };
1336
- } catch { /* daemon offline */ }
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",
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
- let nextId = 1;
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
- req.on('close', () => sseClients.delete(res));
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