clideck 1.25.4 → 1.25.6
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/terminals.js +29 -25
- package/telemetry-receiver.js +36 -1
- package/transcript.js +47 -15
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/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,20 +338,31 @@ 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
|
-
// [
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
const _telemetryOnly = cmd?.presetId === 'claude-code' || cmd?.presetId === 'codex' || cmd?.presetId === 'gemini-cli';
|
|
344
|
+
let _screenTimer = null, _renderSilent = false;
|
|
345
|
+
function _tryScreenCapture() {
|
|
346
|
+
const entry = state.terms.get(id);
|
|
347
|
+
if (!entry?.pendingScreenCapture || (!_renderSilent && !_telemetryOnly) || !entry.term) return;
|
|
348
|
+
entry.pendingScreenCapture = false;
|
|
349
|
+
const buf = entry.term.buffer.active;
|
|
350
|
+
const lines = [];
|
|
351
|
+
for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); }
|
|
352
|
+
send({ type: 'terminal.buffer', id, lines });
|
|
352
353
|
}
|
|
353
|
-
|
|
354
|
-
term.
|
|
354
|
+
let _idleTimer = null, _workTimer = null, _lastTyping = 0, _lastRender = 0;
|
|
355
|
+
term.onData(() => { _lastTyping = Date.now(); });
|
|
356
|
+
term.onRender(() => {
|
|
357
|
+
_lastRender = Date.now();
|
|
358
|
+
_renderSilent = false;
|
|
359
|
+
clearTimeout(_screenTimer);
|
|
360
|
+
_screenTimer = setTimeout(() => { _renderSilent = true; _tryScreenCapture(); }, 2000);
|
|
361
|
+
});
|
|
362
|
+
term.onWriteParsed(() => { if (Date.now() - _lastTyping < 500) return; const entry = state.terms.get(id); if (entry) entry.lastRenderAt = Date.now(); if (_telemetryOnly) return; if (!_workTimer) _workTimer = setTimeout(() => { _workTimer = null; if (Date.now() - _lastRender < 500) setStatus(id, true); }, 1500); clearTimeout(_idleTimer); _idleTimer = setTimeout(() => { clearTimeout(_workTimer); _workTimer = null; setStatus(id, false); send({ type: 'session.statusReport', id, working: false }); }, 1500); });
|
|
363
|
+
|
|
364
|
+
// Expose capture function so setStatus can trigger it when idle arrives after render silence
|
|
365
|
+
setTimeout(() => { const e = state.terms.get(id); if (e) e.tryScreenCapture = _tryScreenCapture; }, 0);
|
|
355
366
|
|
|
356
367
|
term.open(el);
|
|
357
368
|
attachToTerminal(term);
|
|
@@ -383,7 +394,7 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
|
|
|
383
394
|
// Safety: if RO hasn't fired within 500ms, flush anyway to avoid unbounded queue
|
|
384
395
|
setTimeout(() => { if (!fitted) { fitted = true; for (const chunk of pending) term.write(chunk); pending = null; updatePreview(id); } }, 500);
|
|
385
396
|
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:
|
|
397
|
+
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
398
|
document.getElementById('empty').style.display = 'none';
|
|
388
399
|
document.getElementById('terminals').style.pointerEvents = '';
|
|
389
400
|
if (muted) requestAnimationFrame(() => updateMuteIndicator(id));
|
|
@@ -529,16 +540,9 @@ function setStatus(id, working) {
|
|
|
529
540
|
}
|
|
530
541
|
}
|
|
531
542
|
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
}
|
|
543
|
+
// Mark idle so the onRender silence watcher can capture .screen
|
|
544
|
+
// Also try immediately — renders may already be silent
|
|
545
|
+
if (wasWorking && !working) { entry.pendingScreenCapture = true; entry.tryScreenCapture?.(); }
|
|
542
546
|
|
|
543
547
|
if (working) entry.workStartedAt = Date.now();
|
|
544
548
|
|
package/telemetry-receiver.js
CHANGED
|
@@ -32,7 +32,7 @@ function handleLogs(req, res) {
|
|
|
32
32
|
const resAttrs = parseAttrs(rl.resource?.attributes);
|
|
33
33
|
const sessionId = resAttrs['clideck.session_id'];
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// service.name values: claude-code, codex_cli_rs, gemini-cli
|
|
36
36
|
const serviceName = resAttrs['service.name'] || 'unknown';
|
|
37
37
|
let resolvedId = sessionId;
|
|
38
38
|
|
|
@@ -68,6 +68,41 @@ 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 (serviceName === 'codex_cli_rs' && eventName) console.log(`[telemetry:codex] ${eventName}`);
|
|
73
|
+
// if (serviceName === 'gemini-cli' && eventName) console.log(`[telemetry:gemini] ${eventName}`);
|
|
74
|
+
|
|
75
|
+
// Telemetry-based status
|
|
76
|
+
const startEvents = new Set(['user_prompt', 'gemini_cli.user_prompt', 'codex.user_prompt']);
|
|
77
|
+
if (startEvents.has(eventName)) {
|
|
78
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
79
|
+
}
|
|
80
|
+
// Claude: telemetry-only status. user_prompt/any event → working, api_request → idle.
|
|
81
|
+
if (serviceName === 'claude-code' && eventName) {
|
|
82
|
+
if (eventName === 'api_request') {
|
|
83
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
|
|
84
|
+
} else if (eventName !== 'user_prompt') {
|
|
85
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Codex: telemetry-only status. codex.user_prompt/any event → working, codex.sse_event → idle.
|
|
89
|
+
if (serviceName === 'codex_cli_rs' && eventName) {
|
|
90
|
+
if (eventName === 'codex.sse_event') {
|
|
91
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
|
|
92
|
+
} else if (eventName !== 'codex.user_prompt') {
|
|
93
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Gemini: telemetry-only status. Whitelisted events → working, api_response (role=main) → idle.
|
|
97
|
+
if (serviceName === 'gemini-cli' && eventName) {
|
|
98
|
+
if (eventName === 'gemini_cli.api_response' && attrs['role'] === 'main') {
|
|
99
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
|
|
100
|
+
} else if (eventName === 'gemini_cli.api_request' || eventName === 'gemini_cli.model_routing'
|
|
101
|
+
|| (eventName === 'gemini_cli.api_response' && attrs['role'] !== 'main')) {
|
|
102
|
+
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
71
106
|
const agentSessionId = attrs['session.id'] || attrs['conversation.id'];
|
|
72
107
|
if (agentSessionId && sess) {
|
|
73
108
|
// Prefer interactive session ID (Gemini sends non-interactive init events first)
|
package/transcript.js
CHANGED
|
@@ -10,6 +10,7 @@ const inputBuf = {};
|
|
|
10
10
|
const outputBuf = {};
|
|
11
11
|
const cache = {};
|
|
12
12
|
const prefixes = {};
|
|
13
|
+
const userTexts = {}; // sessionId → [text, ...] — user prompts for parser matching
|
|
13
14
|
let broadcast = null;
|
|
14
15
|
let notifyPlugin = null;
|
|
15
16
|
|
|
@@ -47,17 +48,31 @@ function store(id, role, text) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function trackInput(id, data) {
|
|
50
|
-
if (!inputBuf[id]) inputBuf[id] = { text: '', esc: false };
|
|
51
|
+
if (!inputBuf[id]) inputBuf[id] = { text: '', esc: false, osc: false };
|
|
51
52
|
const buf = inputBuf[id];
|
|
52
53
|
for (const ch of data) {
|
|
54
|
+
// OSC sequence: ESC ] ... (terminated by BEL or ESC \)
|
|
55
|
+
if (buf.osc) {
|
|
56
|
+
if (ch === '\x07') { buf.osc = false; continue; } // BEL terminator
|
|
57
|
+
if (ch === '\\' && buf.escPending) { buf.osc = false; buf.escPending = false; continue; } // ESC \ terminator
|
|
58
|
+
buf.escPending = (ch === '\x1b');
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
53
61
|
if (ch === '\x1b') { buf.esc = true; continue; }
|
|
54
62
|
if (buf.esc) {
|
|
55
|
-
|
|
63
|
+
buf.esc = false;
|
|
64
|
+
if (ch === ']') { buf.osc = true; continue; } // Start OSC
|
|
65
|
+
if (ch === '[') { buf.csi = true; continue; } // Start CSI
|
|
66
|
+
continue; // Simple ESC + char
|
|
67
|
+
}
|
|
68
|
+
// CSI sequence: ESC [ ... (terminated by letter or ~)
|
|
69
|
+
if (buf.csi) {
|
|
70
|
+
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '~') buf.csi = false;
|
|
56
71
|
continue;
|
|
57
72
|
}
|
|
58
73
|
if (ch === '\r' || ch === '\n') {
|
|
59
74
|
const line = buf.text.trim();
|
|
60
|
-
if (line) store(id, 'user', line);
|
|
75
|
+
if (line) { store(id, 'user', line); if (!userTexts[id]) userTexts[id] = []; userTexts[id].push(line); }
|
|
61
76
|
buf.text = '';
|
|
62
77
|
} else if (ch === '\x7f' || ch === '\x08') {
|
|
63
78
|
const chars = Array.from(buf.text);
|
|
@@ -113,14 +128,21 @@ function getScreen(id) {
|
|
|
113
128
|
|
|
114
129
|
// Per-agent screen parsers. Each returns [{role, text}] from .screen content.
|
|
115
130
|
const agentParsers = {
|
|
116
|
-
'claude-code': (lines) => {
|
|
131
|
+
'claude-code': (lines, id) => {
|
|
132
|
+
const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
|
|
117
133
|
const turns = [];
|
|
118
134
|
let current = null;
|
|
119
135
|
for (const line of lines) {
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
136
|
+
const agent = line.match(/^(?:[│ ]\s*)?[⏺•●]\s(.*)$/);
|
|
137
|
+
if (agent) {
|
|
122
138
|
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
123
|
-
current = { role:
|
|
139
|
+
current = { role: 'agent', text: agent[1] };
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const userM = line.match(/^(?:[│ ]\s*)?[❯›]\s(.*)$/);
|
|
143
|
+
if (userM && (known ? known.has(userM[1].trim()) : true)) {
|
|
144
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
145
|
+
current = { role: 'user', text: userM[1] };
|
|
124
146
|
continue;
|
|
125
147
|
}
|
|
126
148
|
if (!current) continue;
|
|
@@ -132,14 +154,21 @@ const agentParsers = {
|
|
|
132
154
|
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
133
155
|
return turns.length >= 2 ? turns : null;
|
|
134
156
|
},
|
|
135
|
-
'codex': (lines) => {
|
|
157
|
+
'codex': (lines, id) => {
|
|
158
|
+
const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
|
|
136
159
|
const turns = [];
|
|
137
160
|
let current = null;
|
|
138
161
|
for (const line of lines) {
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
162
|
+
const agent = line.match(/^(?:│\s*)?•\s(.*)$/);
|
|
163
|
+
if (agent) {
|
|
164
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
165
|
+
current = { role: 'agent', text: agent[1] };
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const userM = line.match(/^(?:│\s*)?›\s(.*)$/);
|
|
169
|
+
if (userM && (known ? known.has(userM[1].trim()) : true)) {
|
|
141
170
|
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
142
|
-
current = { role:
|
|
171
|
+
current = { role: 'user', text: userM[1] };
|
|
143
172
|
continue;
|
|
144
173
|
}
|
|
145
174
|
if (!current) continue;
|
|
@@ -151,7 +180,8 @@ const agentParsers = {
|
|
|
151
180
|
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
152
181
|
return turns.length >= 2 ? turns : null;
|
|
153
182
|
},
|
|
154
|
-
'gemini-cli': (lines) => {
|
|
183
|
+
'gemini-cli': (lines, id) => {
|
|
184
|
+
const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
|
|
155
185
|
const geminiChrome = t => {
|
|
156
186
|
const s = t.trim();
|
|
157
187
|
return /^shift\+tab to accept/i.test(s)
|
|
@@ -166,11 +196,12 @@ const agentParsers = {
|
|
|
166
196
|
let current = null;
|
|
167
197
|
for (const line of lines) {
|
|
168
198
|
if (geminiChrome(line)) continue;
|
|
169
|
-
const isUser = line.startsWith(' > ');
|
|
170
199
|
const isAgent = line.startsWith('✦ ');
|
|
200
|
+
const userM = line.startsWith(' > ') ? line.slice(3) : null;
|
|
201
|
+
const isUser = userM && (known ? known.has(userM.trim()) : true);
|
|
171
202
|
if (isUser || isAgent) {
|
|
172
203
|
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
173
|
-
current = { role: isUser ? 'user' : 'agent', text: isUser ?
|
|
204
|
+
current = { role: isUser ? 'user' : 'agent', text: isUser ? userM : line.slice(2) };
|
|
174
205
|
continue;
|
|
175
206
|
}
|
|
176
207
|
if (!current) continue;
|
|
@@ -229,7 +260,7 @@ function getScreenTurns(id, agent) {
|
|
|
229
260
|
if (!screen) return null;
|
|
230
261
|
const lines = screen.split('\n');
|
|
231
262
|
const parser = agentParsers[agent];
|
|
232
|
-
const turns = parser ? parser(lines) : anchorParse(id, lines);
|
|
263
|
+
const turns = parser ? parser(lines, id) : anchorParse(id, lines);
|
|
233
264
|
// Drop trailing user turn — it's the empty prompt or unanswered input
|
|
234
265
|
if (turns?.length && turns[turns.length - 1].role === 'user') turns.pop();
|
|
235
266
|
return turns?.length >= 2 ? turns : null;
|
|
@@ -267,6 +298,7 @@ function clear(id) {
|
|
|
267
298
|
}
|
|
268
299
|
delete cache[id];
|
|
269
300
|
delete prefixes[id];
|
|
301
|
+
delete userTexts[id];
|
|
270
302
|
try { unlinkSync(join(DIR, `${id}.screen`)); } catch {}
|
|
271
303
|
}
|
|
272
304
|
|