clideck 1.25.4 → 1.25.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.
package/handlers.js CHANGED
@@ -130,6 +130,7 @@ function onConnection(ws) {
130
130
  case 'input': sessions.input(msg); break;
131
131
  case 'session.statusReport':
132
132
  if (sessions.getSessions().has(msg.id)) {
133
+ sessions.broadcast({ type: 'session.status', id: msg.id, working: !!msg.working });
133
134
  plugins.notifyStatus(msg.id, !!msg.working);
134
135
  }
135
136
  break;
@@ -306,8 +307,7 @@ function onConnection(ws) {
306
307
  }
307
308
 
308
309
  case 'remote.getHistory': {
309
- const turns = transcript.getScreenTurns(msg.id, sessions.getSessions().get(msg.id)?.presetId)
310
- || transcript.getLastTurns(msg.id, msg.limit || 50);
310
+ const turns = transcript.getScreenTurns(msg.id, sessions.getSessions().get(msg.id)?.presetId);
311
311
  ws.send(JSON.stringify({ type: 'remote.history', id: msg.id, turns: turns || [] }));
312
312
  break;
313
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.25.4",
3
+ "version": "1.25.5",
4
4
  "description": "One screen for all your AI coding agents — run, monitor, and manage multiple CLI agents from a single browser tab",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -318,7 +318,7 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
318
318
  const statusEl = item.querySelector('.session-status');
319
319
  const cmd = state.cfg.commands.find(c => c.id === commandId);
320
320
  const hasBridge = !!cmd?.bridge;
321
- const stopBounce = hasBridge ? null : startBounce(statusEl);
321
+ const stopBounce = null;
322
322
 
323
323
  const el = document.createElement('div');
324
324
  el.className = 'term-wrap';
@@ -338,20 +338,29 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
338
338
  term.loadAddon(fit);
339
339
  term.onData(data => send({ type: 'input', id, data }));
340
340
 
341
- // [RENDER-STATUS] agent working/idle detection via onRender + onWriteParsed
342
- let _lastTyping = 0, _renderWorking = false, _renderTimer = null, _hasRender = false, _hasParsed = false;
343
- term.onData(() => { _lastTyping = Date.now(); });
344
- function _statusTick() {
345
- if (Date.now() - _lastTyping < 500) return;
346
- const cmd = state.cfg.commands.find(c => c.id === commandId);
347
- if (cmd?.bridge) return;
348
- if (_hasRender && _hasParsed && !_renderWorking) {
349
- _renderWorking = true;
350
- send({ type: 'session.statusReport', id, working: true }); setStatus(id, true);
351
- }
341
+ // [SCREEN-CAPTURE] extract terminal buffer when BOTH idle AND render-silent (2s)
342
+ // Decoupled from status: telemetry knows when agent is done, onRender knows when terminal is done
343
+ let _screenTimer = null, _renderSilent = false;
344
+ function _tryScreenCapture() {
345
+ const entry = state.terms.get(id);
346
+ if (!entry?.pendingScreenCapture || !_renderSilent || !entry.term) return;
347
+ entry.pendingScreenCapture = false;
348
+ const buf = entry.term.buffer.active;
349
+ const lines = [];
350
+ for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); }
351
+ send({ type: 'terminal.buffer', id, lines });
352
352
  }
353
- term.onWriteParsed(() => { _hasParsed = true; _statusTick(); });
354
- term.onRender(() => { _hasRender = true; _statusTick(); clearTimeout(_renderTimer); _renderTimer = setTimeout(() => { _renderWorking = false; _hasRender = false; _hasParsed = false; send({ type: 'session.statusReport', id, working: false }); setStatus(id, false); }, 2000); });
353
+ term.onRender(() => {
354
+ _renderSilent = false;
355
+ clearTimeout(_screenTimer);
356
+ _screenTimer = setTimeout(() => { _renderSilent = true; _tryScreenCapture(); }, 2000);
357
+ });
358
+ let _idleTimer = null, _workTimer = null, _lastTyping = 0;
359
+ term.onData(() => { _lastTyping = Date.now(); });
360
+ term.onWriteParsed(() => { if (Date.now() - _lastTyping < 500) return; const entry = state.terms.get(id); if (entry) entry.lastRenderAt = Date.now(); if (!_workTimer) _workTimer = setTimeout(() => { _workTimer = null; setStatus(id, true); }, 1000); clearTimeout(_idleTimer); _idleTimer = setTimeout(() => { clearTimeout(_workTimer); _workTimer = null; setStatus(id, false); send({ type: 'session.statusReport', id, working: false }); }, 1500); });
361
+
362
+ // Expose capture function so setStatus can trigger it when idle arrives after render silence
363
+ setTimeout(() => { const e = state.terms.get(id); if (e) e.tryScreenCapture = _tryScreenCapture; }, 0);
355
364
 
356
365
  term.open(el);
357
366
  attachToTerminal(term);
@@ -383,7 +392,7 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
383
392
  // Safety: if RO hasn't fired within 500ms, flush anyway to avoid unbounded queue
384
393
  setTimeout(() => { if (!fitted) { fitted = true; for (const chunk of pending) term.write(chunk); pending = null; updatePreview(id); } }, 500);
385
394
  const cancelFitRaf = () => { if (fitRaf) { cancelAnimationFrame(fitRaf); fitRaf = 0; } };
386
- state.terms.set(id, { term, fit, el, ro, cancelFitRaf, themeId, commandId, projectId: projectId || null, muted: !!muted, working: !hasBridge, workStartedAt: hasBridge ? null : Date.now(), stopBounce, queue: (data) => { if (!fitted) { pending.push(data); return true; } return false; }, lastActivityAt: Date.now(), unread: false, lastPreviewText: lastPreview || '', searchText: '' });
395
+ state.terms.set(id, { term, fit, el, ro, cancelFitRaf, themeId, commandId, projectId: projectId || null, muted: !!muted, working: false, workStartedAt: null, stopBounce, queue: (data) => { if (!fitted) { pending.push(data); return true; } return false; }, lastActivityAt: Date.now(), unread: false, lastPreviewText: lastPreview || '', searchText: '' });
387
396
  document.getElementById('empty').style.display = 'none';
388
397
  document.getElementById('terminals').style.pointerEvents = '';
389
398
  if (muted) requestAnimationFrame(() => updateMuteIndicator(id));
@@ -529,16 +538,9 @@ function setStatus(id, working) {
529
538
  }
530
539
  }
531
540
 
532
- // Extract terminal buffer on working→idle for clean transcript
533
- if (wasWorking && !working && entry.term) {
534
- const buf = entry.term.buffer.active;
535
- const lines = [];
536
- for (let i = 0; i < buf.length; i++) {
537
- const line = buf.getLine(i);
538
- if (line) lines.push(line.translateToString(true));
539
- }
540
- send({ type: 'terminal.buffer', id, lines });
541
- }
541
+ // Mark idle so the onRender silence watcher can capture .screen
542
+ // Also try immediately renders may already be silent
543
+ if (wasWorking && !working) { entry.pendingScreenCapture = true; entry.tryScreenCapture?.(); }
542
544
 
543
545
  if (working) entry.workStartedAt = Date.now();
544
546
 
@@ -68,6 +68,16 @@ function handleLogs(req, res) {
68
68
  for (const lr of sl.logRecords || []) {
69
69
  const attrs = parseAttrs(lr.attributes);
70
70
 
71
+ const eventName = attrs['event.name'];
72
+ if (eventName) console.log(`[telemetry] ${resolvedId?.slice(0,8)} ${eventName}`);
73
+
74
+ // Telemetry: only used for working=true (user prompt). Idle is handled by frontend write-silence.
75
+ const startEvents = new Set(['user_prompt', 'gemini_cli.user_prompt', 'codex.user_prompt']);
76
+
77
+ if (startEvents.has(eventName)) {
78
+ broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
79
+ }
80
+
71
81
  const agentSessionId = attrs['session.id'] || attrs['conversation.id'];
72
82
  if (agentSessionId && sess) {
73
83
  // Prefer interactive session ID (Gemini sends non-interactive init events first)
package/transcript.js CHANGED
@@ -47,12 +47,26 @@ function store(id, role, text) {
47
47
  }
48
48
 
49
49
  function trackInput(id, data) {
50
- if (!inputBuf[id]) inputBuf[id] = { text: '', esc: false };
50
+ if (!inputBuf[id]) inputBuf[id] = { text: '', esc: false, osc: false };
51
51
  const buf = inputBuf[id];
52
52
  for (const ch of data) {
53
+ // OSC sequence: ESC ] ... (terminated by BEL or ESC \)
54
+ if (buf.osc) {
55
+ if (ch === '\x07') { buf.osc = false; continue; } // BEL terminator
56
+ if (ch === '\\' && buf.escPending) { buf.osc = false; buf.escPending = false; continue; } // ESC \ terminator
57
+ buf.escPending = (ch === '\x1b');
58
+ continue;
59
+ }
53
60
  if (ch === '\x1b') { buf.esc = true; continue; }
54
61
  if (buf.esc) {
55
- if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '~') buf.esc = false;
62
+ buf.esc = false;
63
+ if (ch === ']') { buf.osc = true; continue; } // Start OSC
64
+ if (ch === '[') { buf.csi = true; continue; } // Start CSI
65
+ continue; // Simple ESC + char
66
+ }
67
+ // CSI sequence: ESC [ ... (terminated by letter or ~)
68
+ if (buf.csi) {
69
+ if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '~') buf.csi = false;
56
70
  continue;
57
71
  }
58
72
  if (ch === '\r' || ch === '\n') {