clideck 1.25.5 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.25.5",
3
+ "version": "1.25.6",
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": {
@@ -340,24 +340,26 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
340
340
 
341
341
  // [SCREEN-CAPTURE] extract terminal buffer when BOTH idle AND render-silent (2s)
342
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';
343
344
  let _screenTimer = null, _renderSilent = false;
344
345
  function _tryScreenCapture() {
345
346
  const entry = state.terms.get(id);
346
- if (!entry?.pendingScreenCapture || !_renderSilent || !entry.term) return;
347
+ if (!entry?.pendingScreenCapture || (!_renderSilent && !_telemetryOnly) || !entry.term) return;
347
348
  entry.pendingScreenCapture = false;
348
349
  const buf = entry.term.buffer.active;
349
350
  const lines = [];
350
351
  for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); }
351
352
  send({ type: 'terminal.buffer', id, lines });
352
353
  }
354
+ let _idleTimer = null, _workTimer = null, _lastTyping = 0, _lastRender = 0;
355
+ term.onData(() => { _lastTyping = Date.now(); });
353
356
  term.onRender(() => {
357
+ _lastRender = Date.now();
354
358
  _renderSilent = false;
355
359
  clearTimeout(_screenTimer);
356
360
  _screenTimer = setTimeout(() => { _renderSilent = true; _tryScreenCapture(); }, 2000);
357
361
  });
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); });
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); });
361
363
 
362
364
  // Expose capture function so setStatus can trigger it when idle arrives after render silence
363
365
  setTimeout(() => { const e = state.terms.get(id); if (e) e.tryScreenCapture = _tryScreenCapture; }, 0);
@@ -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
- // DEBUG: log all incoming telemetry with resource attributes
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
 
@@ -69,14 +69,39 @@ function handleLogs(req, res) {
69
69
  const attrs = parseAttrs(lr.attributes);
70
70
 
71
71
  const eventName = attrs['event.name'];
72
- if (eventName) console.log(`[telemetry] ${resolvedId?.slice(0,8)} ${eventName}`);
72
+ // if (serviceName === 'codex_cli_rs' && eventName) console.log(`[telemetry:codex] ${eventName}`);
73
+ // if (serviceName === 'gemini-cli' && eventName) console.log(`[telemetry:gemini] ${eventName}`);
73
74
 
74
- // Telemetry: only used for working=true (user prompt). Idle is handled by frontend write-silence.
75
+ // Telemetry-based status
75
76
  const startEvents = new Set(['user_prompt', 'gemini_cli.user_prompt', 'codex.user_prompt']);
76
-
77
77
  if (startEvents.has(eventName)) {
78
78
  broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
79
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
+ }
80
105
 
81
106
  const agentSessionId = attrs['session.id'] || attrs['conversation.id'];
82
107
  if (agentSessionId && sess) {
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
 
@@ -71,7 +72,7 @@ function trackInput(id, data) {
71
72
  }
72
73
  if (ch === '\r' || ch === '\n') {
73
74
  const line = buf.text.trim();
74
- if (line) store(id, 'user', line);
75
+ if (line) { store(id, 'user', line); if (!userTexts[id]) userTexts[id] = []; userTexts[id].push(line); }
75
76
  buf.text = '';
76
77
  } else if (ch === '\x7f' || ch === '\x08') {
77
78
  const chars = Array.from(buf.text);
@@ -127,14 +128,21 @@ function getScreen(id) {
127
128
 
128
129
  // Per-agent screen parsers. Each returns [{role, text}] from .screen content.
129
130
  const agentParsers = {
130
- 'claude-code': (lines) => {
131
+ 'claude-code': (lines, id) => {
132
+ const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
131
133
  const turns = [];
132
134
  let current = null;
133
135
  for (const line of lines) {
134
- const m = line.match(/^(?:[│ ]\s*)?([❯›]|[⏺•●])\s(.*)$/);
135
- if (m) {
136
+ const agent = line.match(/^(?:[│ ]\s*)?[⏺•●]\s(.*)$/);
137
+ if (agent) {
136
138
  if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
137
- current = { role: m[1] === '❯' || m[1] === '›' ? 'user' : 'agent', text: m[2] };
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] };
138
146
  continue;
139
147
  }
140
148
  if (!current) continue;
@@ -146,14 +154,21 @@ const agentParsers = {
146
154
  if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
147
155
  return turns.length >= 2 ? turns : null;
148
156
  },
149
- 'codex': (lines) => {
157
+ 'codex': (lines, id) => {
158
+ const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
150
159
  const turns = [];
151
160
  let current = null;
152
161
  for (const line of lines) {
153
- const m = line.match(/^(?:│\s*)?([›•])\s(.*)$/);
154
- if (m) {
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)) {
155
170
  if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
156
- current = { role: m[1] === '›' ? 'user' : 'agent', text: m[2] };
171
+ current = { role: 'user', text: userM[1] };
157
172
  continue;
158
173
  }
159
174
  if (!current) continue;
@@ -165,7 +180,8 @@ const agentParsers = {
165
180
  if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
166
181
  return turns.length >= 2 ? turns : null;
167
182
  },
168
- 'gemini-cli': (lines) => {
183
+ 'gemini-cli': (lines, id) => {
184
+ const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
169
185
  const geminiChrome = t => {
170
186
  const s = t.trim();
171
187
  return /^shift\+tab to accept/i.test(s)
@@ -180,11 +196,12 @@ const agentParsers = {
180
196
  let current = null;
181
197
  for (const line of lines) {
182
198
  if (geminiChrome(line)) continue;
183
- const isUser = line.startsWith(' > ');
184
199
  const isAgent = line.startsWith('✦ ');
200
+ const userM = line.startsWith(' > ') ? line.slice(3) : null;
201
+ const isUser = userM && (known ? known.has(userM.trim()) : true);
185
202
  if (isUser || isAgent) {
186
203
  if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
187
- current = { role: isUser ? 'user' : 'agent', text: isUser ? line.slice(3) : line.slice(2) };
204
+ current = { role: isUser ? 'user' : 'agent', text: isUser ? userM : line.slice(2) };
188
205
  continue;
189
206
  }
190
207
  if (!current) continue;
@@ -243,7 +260,7 @@ function getScreenTurns(id, agent) {
243
260
  if (!screen) return null;
244
261
  const lines = screen.split('\n');
245
262
  const parser = agentParsers[agent];
246
- const turns = parser ? parser(lines) : anchorParse(id, lines);
263
+ const turns = parser ? parser(lines, id) : anchorParse(id, lines);
247
264
  // Drop trailing user turn — it's the empty prompt or unanswered input
248
265
  if (turns?.length && turns[turns.length - 1].role === 'user') turns.pop();
249
266
  return turns?.length >= 2 ? turns : null;
@@ -281,6 +298,7 @@ function clear(id) {
281
298
  }
282
299
  delete cache[id];
283
300
  delete prefixes[id];
301
+ delete userTexts[id];
284
302
  try { unlinkSync(join(DIR, `${id}.screen`)); } catch {}
285
303
  }
286
304