clideck 1.25.3 → 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 +2 -2
- package/package.json +1 -1
- package/public/js/app.js +2 -3
- package/public/js/terminals.js +29 -12
- package/telemetry-receiver.js +10 -0
- package/transcript.js +39 -5
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
package/public/js/app.js
CHANGED
|
@@ -90,6 +90,7 @@ function connect() {
|
|
|
90
90
|
}
|
|
91
91
|
break;
|
|
92
92
|
}
|
|
93
|
+
/* [OLD-STATUS] I/O burst heuristic — replaced by onRender detection in terminals.js
|
|
93
94
|
case 'stats': {
|
|
94
95
|
for (const [sid, st] of Object.entries(msg.stats)) {
|
|
95
96
|
const entry = state.terms.get(sid);
|
|
@@ -101,12 +102,9 @@ function connect() {
|
|
|
101
102
|
const userTyping = (st.rawRateIn || 0) > 0 && (st.rawRateIn || 0) < 50;
|
|
102
103
|
entry.prevBurst = st.burstMs || 0;
|
|
103
104
|
|
|
104
|
-
// Working: burst increasing + net >= 800B + no typing
|
|
105
105
|
const isWorking = burstUp && net >= 800 && !userTyping;
|
|
106
|
-
// Idle: burst not increasing + net < 800B
|
|
107
106
|
const isIdle = !burstUp && net < 800;
|
|
108
107
|
|
|
109
|
-
// Sustain for ~1.5s (2 ticks)
|
|
110
108
|
if (isWorking) entry.workTicks = (entry.workTicks || 0) + 1;
|
|
111
109
|
else entry.workTicks = 0;
|
|
112
110
|
if (isIdle) entry.idleTicks = (entry.idleTicks || 0) + 1;
|
|
@@ -122,6 +120,7 @@ function connect() {
|
|
|
122
120
|
}
|
|
123
121
|
break;
|
|
124
122
|
}
|
|
123
|
+
[OLD-STATUS] */
|
|
125
124
|
case 'transcript.cache':
|
|
126
125
|
state.transcriptCache = msg.cache;
|
|
127
126
|
for (const [id, text] of Object.entries(msg.cache)) {
|
package/public/js/terminals.js
CHANGED
|
@@ -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 =
|
|
321
|
+
const stopBounce = null;
|
|
322
322
|
|
|
323
323
|
const el = document.createElement('div');
|
|
324
324
|
el.className = 'term-wrap';
|
|
@@ -338,6 +338,30 @@ 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
|
+
// [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
|
+
}
|
|
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);
|
|
364
|
+
|
|
341
365
|
term.open(el);
|
|
342
366
|
attachToTerminal(term);
|
|
343
367
|
let fitted = false, pending = [];
|
|
@@ -368,7 +392,7 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
|
|
|
368
392
|
// Safety: if RO hasn't fired within 500ms, flush anyway to avoid unbounded queue
|
|
369
393
|
setTimeout(() => { if (!fitted) { fitted = true; for (const chunk of pending) term.write(chunk); pending = null; updatePreview(id); } }, 500);
|
|
370
394
|
const cancelFitRaf = () => { if (fitRaf) { cancelAnimationFrame(fitRaf); fitRaf = 0; } };
|
|
371
|
-
state.terms.set(id, { term, fit, el, ro, cancelFitRaf, themeId, commandId, projectId: projectId || null, muted: !!muted, working:
|
|
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: '' });
|
|
372
396
|
document.getElementById('empty').style.display = 'none';
|
|
373
397
|
document.getElementById('terminals').style.pointerEvents = '';
|
|
374
398
|
if (muted) requestAnimationFrame(() => updateMuteIndicator(id));
|
|
@@ -514,16 +538,9 @@ function setStatus(id, working) {
|
|
|
514
538
|
}
|
|
515
539
|
}
|
|
516
540
|
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const lines = [];
|
|
521
|
-
for (let i = 0; i < buf.length; i++) {
|
|
522
|
-
const line = buf.getLine(i);
|
|
523
|
-
if (line) lines.push(line.translateToString(true));
|
|
524
|
-
}
|
|
525
|
-
send({ type: 'terminal.buffer', id, lines });
|
|
526
|
-
}
|
|
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?.(); }
|
|
527
544
|
|
|
528
545
|
if (working) entry.workStartedAt = Date.now();
|
|
529
546
|
|
package/telemetry-receiver.js
CHANGED
|
@@ -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
|
-
|
|
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') {
|
|
@@ -91,11 +105,17 @@ function flush(id) {
|
|
|
91
105
|
// xterm buffer as rendered by the browser. No diffing, no JSONL — just the
|
|
92
106
|
// clean screen content. Mobile reads this for "last agent message".
|
|
93
107
|
function storeBuffer(id, lines) {
|
|
94
|
-
const isChrome = t => !t
|
|
108
|
+
const isChrome = t => !t
|
|
109
|
+
|| /^[─━═\u2500-\u257f]+$/.test(t) // box-drawing horizontal lines
|
|
110
|
+
|| /^[▀▄█▌▐░▒▓╭╮╰╯│╔╗╚╝║]+$/.test(t) // block elements, box corners, vertical bars
|
|
111
|
+
|| (/[█▀▄▌▐░▒▓]/.test(t) && /^[█▀▄▌▐░▒▓\s]+$/.test(t)) // ASCII art (blocks + whitespace, e.g. logos)
|
|
112
|
+
|| /^[❯>$%#]\s*$/.test(t) // bare prompt markers
|
|
113
|
+
|| /^(esc to interrupt|\? for shortcuts)$/i.test(t); // Claude Code chrome
|
|
95
114
|
const filtered = lines.filter(l => !isChrome(l.trim()));
|
|
96
115
|
while (filtered.length && !filtered[filtered.length - 1].trim()) filtered.pop();
|
|
97
116
|
const screenPath = join(DIR, `${id}.screen`);
|
|
98
117
|
if (filtered.length) writeFileSync(screenPath, filtered.join('\n'));
|
|
118
|
+
else try { unlinkSync(screenPath); } catch {}
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
// Read the clean screen snapshot for a session (if available).
|
|
@@ -111,7 +131,7 @@ const agentParsers = {
|
|
|
111
131
|
const turns = [];
|
|
112
132
|
let current = null;
|
|
113
133
|
for (const line of lines) {
|
|
114
|
-
const m = line.match(/^(?:[│ ]\s*)?([❯›]|[
|
|
134
|
+
const m = line.match(/^(?:[│ ]\s*)?([❯›]|[⏺•●])\s(.*)$/);
|
|
115
135
|
if (m) {
|
|
116
136
|
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
117
137
|
current = { role: m[1] === '❯' || m[1] === '›' ? 'user' : 'agent', text: m[2] };
|
|
@@ -146,9 +166,20 @@ const agentParsers = {
|
|
|
146
166
|
return turns.length >= 2 ? turns : null;
|
|
147
167
|
},
|
|
148
168
|
'gemini-cli': (lines) => {
|
|
169
|
+
const geminiChrome = t => {
|
|
170
|
+
const s = t.trim();
|
|
171
|
+
return /^shift\+tab to accept/i.test(s)
|
|
172
|
+
|| /^(Type your message|@path\/to\/)/i.test(s)
|
|
173
|
+
|| /^(\/\w+ |no sandbox|\/model )/i.test(s)
|
|
174
|
+
|| /^[~\/\\].*\(main[*]?\)\s*$/i.test(s)
|
|
175
|
+
|| /^(Logged in with|Plan:|Tips for getting started)/i.test(s)
|
|
176
|
+
|| /^\d+\.\s+(Ask questions|Be specific|Create GEMINI)/i.test(s)
|
|
177
|
+
|| /^ℹ\s/.test(s);
|
|
178
|
+
};
|
|
149
179
|
const turns = [];
|
|
150
180
|
let current = null;
|
|
151
181
|
for (const line of lines) {
|
|
182
|
+
if (geminiChrome(line)) continue;
|
|
152
183
|
const isUser = line.startsWith(' > ');
|
|
153
184
|
const isAgent = line.startsWith('✦ ');
|
|
154
185
|
if (isUser || isAgent) {
|
|
@@ -212,7 +243,10 @@ function getScreenTurns(id, agent) {
|
|
|
212
243
|
if (!screen) return null;
|
|
213
244
|
const lines = screen.split('\n');
|
|
214
245
|
const parser = agentParsers[agent];
|
|
215
|
-
|
|
246
|
+
const turns = parser ? parser(lines) : anchorParse(id, lines);
|
|
247
|
+
// Drop trailing user turn — it's the empty prompt or unanswered input
|
|
248
|
+
if (turns?.length && turns[turns.length - 1].role === 'user') turns.pop();
|
|
249
|
+
return turns?.length >= 2 ? turns : null;
|
|
216
250
|
}
|
|
217
251
|
|
|
218
252
|
function getLastTurns(id, n) {
|