clideck 1.31.3 → 1.31.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/sessions.js CHANGED
@@ -10,7 +10,7 @@ const opencodeBridge = require('./opencode-bridge');
10
10
  const plugins = require('./plugin-loader');
11
11
 
12
12
  const THEMES = require('./themes');
13
- const MAX_BUFFER = 200 * 1024;
13
+ const MAX_BUFFER = 2 * 1024 * 1024;
14
14
  const { PORT, localUrl } = require('./runtime');
15
15
  const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b./g;
16
16
  const PRESETS = JSON.parse(require('fs').readFileSync(join(__dirname, 'agent-presets.json'), 'utf8'));
@@ -59,9 +59,26 @@ function broadcast(msg) {
59
59
 
60
60
  // --- Spawn a PTY and wire up a session ---
61
61
 
62
+ function presetForCommand(cmd) {
63
+ if (cmd?.presetId) {
64
+ const preset = PRESETS.find(p => p.presetId === cmd.presetId);
65
+ if (preset) return preset;
66
+ }
67
+ const bin = binName(cmd?.command);
68
+ return PRESETS.find(p => binName(p.command) === bin);
69
+ }
70
+
71
+ function commandEnv(cmd) {
72
+ const env = {};
73
+ if (!cmd?.env || typeof cmd.env !== 'object' || Array.isArray(cmd.env)) return env;
74
+ for (const [key, value] of Object.entries(cmd.env)) {
75
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) env[key] = String(value ?? '');
76
+ }
77
+ return env;
78
+ }
79
+
62
80
  function buildTelemetryEnv(id, cmd) {
63
- const bin = binName(cmd.command);
64
- const preset = PRESETS.find(p => binName(p.command) === bin);
81
+ const preset = presetForCommand(cmd);
65
82
  const telemetryEnabled = cmd.telemetryEnabled ?? (preset?.presetId === 'claude-code');
66
83
  const env = { CLIDECK_SESSION_ID: id, CLIDECK_PORT: String(PORT), CLIDECK_URL: localUrl() };
67
84
  if (!preset?.telemetryEnv || !telemetryEnabled) return env;
@@ -85,19 +102,19 @@ function isLightTheme(themeId) {
85
102
  function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken, projectId, cols, rows) {
86
103
  const telemetryEnv = buildTelemetryEnv(id, cmd);
87
104
  const colorEnv = isLightTheme(themeId) ? { COLORFGBG: '0;15' } : { COLORFGBG: '15;0' };
105
+ const extraEnv = commandEnv(cmd);
88
106
  let term;
89
107
  try {
90
108
  term = pty.spawn(parts[0], parts.slice(1), {
91
109
  name: 'xterm-256color', cols: cols || 80, rows: rows || 24, cwd,
92
- env: { ...process.env, ...telemetryEnv, ...colorEnv },
110
+ env: { ...process.env, ...extraEnv, ...telemetryEnv, ...colorEnv },
93
111
  });
94
112
  } catch (e) {
95
113
  return e;
96
114
  }
97
115
 
98
116
  const sessionIdRe = cmd.sessionIdPattern ? new RegExp(cmd.sessionIdPattern, 'i') : null;
99
- const bin = binName(cmd.command);
100
- const preset = PRESETS.find(p => binName(p.command) === bin);
117
+ const preset = presetForCommand(cmd);
101
118
  const session = { name, themeId, commandId, cwd, pty: term, chunks: [], chunksSize: 0, sessionToken: savedToken || null, projectId: projectId || null, presetId: preset?.presetId || 'shell', working: undefined };
102
119
  sessions.set(id, session);
103
120
  transcript.setFinalizeOnIdle(id, ['claude-code', 'codex', 'gemini-cli', 'opencode', 'clideck-agent'].includes(session.presetId) ? session.presetId : null);
@@ -105,32 +122,10 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
105
122
  // Always watch telemetry-backed agents so OTLP fallback matching can attach
106
123
  // early events to this session even when the agent omits clideck.session_id.
107
124
  // The receiver itself decides whether to surface a setup prompt.
108
- if (preset?.telemetryEnv) telemetry.watchSession(id, bin);
125
+ if (preset?.telemetryEnv) telemetry.watchSession(id, binName(cmd.command));
109
126
  if (preset?.bridge === 'opencode') opencodeBridge.watchSession(id, cwd);
110
127
 
111
- function injectRolePrompt() {
112
- if (!session.pendingRolePrompt) return;
113
- transcript.recordInjectedInput(id, session.pendingRolePrompt);
114
- term.write(session.pendingRolePrompt);
115
- setTimeout(() => term.write('\r'), 150);
116
- console.log(`Session ${id.slice(0, 8)}: injected role prompt`);
117
- delete session.pendingRolePrompt;
118
- delete session._rolePromptTimer;
119
- }
120
-
121
128
  term.onData((data) => {
122
- // Role prompts should be injected only when the agent is likely ready for
123
- // input. For Codex, use the first OTLP startup event instead of a blind
124
- // fixed startup delay; other agents keep the existing delayed path.
125
- if (session.pendingRolePrompt && !session._rolePromptTimer) {
126
- if (session.presetId === 'codex') {
127
- if (telemetry.hasEvents(id)) injectRolePrompt();
128
- } else {
129
- session._rolePromptTimer = setTimeout(() => {
130
- if (session.pendingRolePrompt) injectRolePrompt();
131
- }, 3000);
132
- }
133
- }
134
129
  session.chunks.push(data);
135
130
  session.chunksSize += data.length;
136
131
  while (session.chunksSize > MAX_BUFFER && session.chunks.length > 1) {
@@ -164,7 +159,6 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
164
159
  resumable.push({
165
160
  id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
166
161
  themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
167
- roleName: s.roleName || null,
168
162
  lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
169
163
  savedAt: new Date().toISOString(),
170
164
  });
@@ -202,25 +196,12 @@ function create(msg, ws, cfg) {
202
196
  return;
203
197
  }
204
198
 
205
- // If a role was selected, store identity on session and queue prompt injection
206
- if (msg.roleId) {
207
- const role = (cfg.roles || []).find(r => r.id === msg.roleId);
208
- if (role) {
209
- const s = sessions.get(id);
210
- if (s) {
211
- s.roleName = role.name;
212
- if (role.instructions) s.pendingRolePrompt = role.instructions;
213
- }
214
- }
215
- }
216
-
217
- const createdPresetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
199
+ const createdPresetId = presetForCommand(cmd)?.presetId || 'shell';
218
200
  const installId = msg.installId || undefined;
219
201
  broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId: createdPresetId, projectId, installId });
220
202
 
221
203
  // Immediate setup notification if config not detected
222
- const bin = binName(cmd.command);
223
- const preset = PRESETS.find(p => binName(p.command) === bin);
204
+ const preset = presetForCommand(cmd);
224
205
  if (preset && (preset.telemetrySetup || preset.bridge) && !(cmd.telemetryEnabled && cmd.telemetryStatus?.ok)) {
225
206
  broadcast({ type: 'session.needsSetup', id });
226
207
  }
@@ -245,10 +226,9 @@ function createProgrammatic(opts, cfg) {
245
226
  if (err) return { error: err.message };
246
227
 
247
228
  const s = sessions.get(id);
248
- if (s && opts.roleName) s.roleName = opts.roleName;
249
229
  if (s && opts.ephemeral) s.ephemeral = true;
250
230
 
251
- const presetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
231
+ const presetId = presetForCommand(cmd)?.presetId || 'shell';
252
232
  broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId, projectId });
253
233
  return { id };
254
234
  }
@@ -292,14 +272,13 @@ function resume(msg, ws, cfg) {
292
272
  const s = sessions.get(id);
293
273
  if (s) {
294
274
  if (saved.muted) s.muted = true;
295
- if (saved.roleName) s.roleName = saved.roleName;
296
275
  }
297
276
 
298
277
  // Remove from resumable list and notify all clients
299
278
  resumable = resumable.filter(s => s.id !== id);
300
279
  broadcast({ type: 'sessions.resumable', list: getResumable(cfg) });
301
280
 
302
- const resumePresetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || saved.presetId || 'shell';
281
+ const resumePresetId = presetForCommand(cmd)?.presetId || saved.presetId || 'shell';
303
282
  broadcast({ type: 'created', id, name: saved.name, themeId: saved.themeId || saved.profileId || 'default', commandId: saved.commandId, presetId: resumePresetId, projectId: saved.projectId || null, muted: !!saved.muted, resumed: true, lastPreview: saved.lastPreview || '' });
304
283
  }
305
284
 
@@ -386,7 +365,7 @@ function restart(msg, ws, cfg) {
386
365
  }
387
366
 
388
367
  const savedToken = s.sessionToken;
389
- const { name, cwd, commandId, projectId, roleName, muted, lastPreview, lastActivityAt } = s;
368
+ const { name, cwd, commandId, projectId, muted, lastPreview, lastActivityAt } = s;
390
369
 
391
370
  activity.clear(id);
392
371
  telemetry.clear(id);
@@ -405,7 +384,6 @@ function restart(msg, ws, cfg) {
405
384
 
406
385
  const next = sessions.get(id);
407
386
  if (next) {
408
- next.roleName = roleName || null;
409
387
  next.muted = !!muted;
410
388
  next.lastPreview = lastPreview || '';
411
389
  next.lastActivityAt = lastActivityAt || null;
@@ -417,7 +395,6 @@ function restart(msg, ws, cfg) {
417
395
  function list() {
418
396
  return [...sessions].map(([id, s]) => ({
419
397
  id, name: s.name, themeId: s.themeId, commandId: s.commandId, presetId: s.presetId || 'shell', projectId: s.projectId, muted: !!s.muted,
420
- roleName: s.roleName || null,
421
398
  // Last preview text for sidebar display on reconnect
422
399
  lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
423
400
  menu: s._menuKey ? JSON.parse(s._menuKey) : undefined,
@@ -445,13 +422,18 @@ function getResumable(cfg) {
445
422
  if (s.presetId) return s;
446
423
  const cmd = (cfg.commands || []).find(c => c.id === s.commandId);
447
424
  if (!cmd) return { ...s, presetId: 'shell' };
448
- const preset = PRESETS.find(p => binName(p.command) === binName(cmd.command));
425
+ const preset = presetForCommand(cmd);
449
426
  return { ...s, presetId: preset?.presetId || 'shell' };
450
427
  });
451
428
  }
452
429
 
453
430
  function sendBuffers(ws) {
454
431
  for (const [id, s] of sessions) {
432
+ if (s.chunks.length) {
433
+ const data = s.chunks.join('');
434
+ ws.send(JSON.stringify({ type: 'output', id, data, replay: true }));
435
+ continue;
436
+ }
455
437
  if (['claude-code', 'codex', 'gemini-cli', 'opencode', 'clideck-agent'].includes(s.presetId) && !s.working) {
456
438
  const text = transcript.getReplayText(id, s.presetId);
457
439
  if (text) {
@@ -459,10 +441,6 @@ function sendBuffers(ws) {
459
441
  continue;
460
442
  }
461
443
  }
462
- if (s.chunks.length) {
463
- const data = s.chunks.join('');
464
- ws.send(JSON.stringify({ type: 'output', id, data, replay: true }));
465
- }
466
444
  }
467
445
  }
468
446
 
@@ -486,7 +464,6 @@ function saveSessions(cfg) {
486
464
  .map(([id, s]) => ({
487
465
  id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
488
466
  themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
489
- roleName: s.roleName || null,
490
467
  lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
491
468
  savedAt: new Date().toISOString(),
492
469
  }));
package/transcript.js CHANGED
@@ -226,9 +226,13 @@ function getTurns(id, n, order) {
226
226
 
227
227
  function getCache() { return { ...cache }; }
228
228
 
229
+ function hasSettledReplay(entries) {
230
+ return entries?.some(e => e.role === 'agent') && entries[entries.length - 1]?.role === 'agent';
231
+ }
232
+
229
233
  function getReplayText(id, presetId) {
230
234
  const entries = builder.compactEntries(entriesById[id], presetId);
231
- if (!entries?.length) return '';
235
+ if (!hasSettledReplay(entries)) return '';
232
236
  const marks = {
233
237
  'claude-code': { user: '❯', agent: '⏺' },
234
238
  codex: { user: '›', agent: '•' },
@@ -1,112 +0,0 @@
1
- // Roles panel — manage worker role definitions (core feature, not plugin-specific)
2
- import { state, send } from './state.js';
3
- import { esc } from './utils.js';
4
-
5
- const panel = document.getElementById('panel-roles');
6
-
7
- function getRoles() { return state.cfg.roles || []; }
8
-
9
- function save() { send({ type: 'config.update', config: state.cfg }); }
10
-
11
- export function renderRoles() {
12
- const roles = getRoles();
13
- panel.innerHTML = `
14
- <div class="flex items-center justify-between px-3 pt-3 pb-2">
15
- <span class="text-sm font-bold text-slate-200 tracking-tight" style="font-family:'JetBrains Mono',monospace">Roles</span>
16
- <button id="btn-add-role" class="icon-btn w-7 h-7 flex items-center justify-center rounded-md border border-slate-600 text-slate-400 hover:bg-slate-700 hover:text-slate-200 transition-colors text-sm" title="New role">+</button>
17
- </div>
18
- <div id="roles-list" class="tmx-scroll flex-1 overflow-y-auto border-t border-slate-700/50"></div>`;
19
-
20
- panel.querySelector('#btn-add-role').addEventListener('click', () => openEditor());
21
-
22
- const list = panel.querySelector('#roles-list');
23
- list.addEventListener('click', (e) => {
24
- if (e.target.closest('.role-edit')) {
25
- const idx = +e.target.closest('.role-row').dataset.idx;
26
- openEditor(idx);
27
- return;
28
- }
29
- if (e.target.closest('.role-del')) {
30
- const idx = +e.target.closest('.role-row').dataset.idx;
31
- state.cfg.roles.splice(idx, 1);
32
- save();
33
- renderRoles();
34
- return;
35
- }
36
- });
37
-
38
- renderRoleList(roles);
39
- }
40
-
41
- function renderRoleList(roles) {
42
- const list = panel.querySelector('#roles-list');
43
- if (!roles.length) {
44
- list.innerHTML = `<div class="flex flex-col items-center justify-center h-full px-6 text-center">
45
- <p class="text-sm text-slate-400 mb-1">No roles defined</p>
46
- <p class="text-xs text-slate-600 leading-relaxed">Define agent identities with a name and instructions.<br>Roles are sent to the agent when a session starts<br>and can be used by plugins like Autopilot.</p>
47
- </div>`;
48
- return;
49
- }
50
- list.innerHTML = roles.map((r, idx) => `
51
- <div class="role-row group flex items-start gap-2 px-3 py-2.5 cursor-default hover:bg-slate-800/40 transition-colors ${idx > 0 ? 'border-t border-slate-700/30' : ''}" data-idx="${idx}">
52
- <div class="flex-1 min-w-0">
53
- <div class="text-[13px] font-medium text-slate-200 truncate">${esc(r.name)}</div>
54
- <div class="text-[11px] text-slate-500 mt-0.5 line-clamp-2 leading-relaxed">${esc(r.instructions)}</div>
55
- </div>
56
- <div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 mt-0.5">
57
- <button class="role-edit w-6 h-6 flex items-center justify-center rounded text-slate-500 hover:text-slate-300 hover:bg-slate-700/60 transition-colors" title="Edit">
58
- <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
59
- </button>
60
- <button class="role-del w-6 h-6 flex items-center justify-center rounded text-slate-500 hover:text-red-400 hover:bg-slate-700/60 transition-colors" title="Delete">
61
- <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
62
- </button>
63
- </div>
64
- </div>
65
- `).join('');
66
- }
67
-
68
- function closeEditor() { document.getElementById('role-editor')?.remove(); }
69
-
70
- function openEditor(idx) {
71
- if (document.getElementById('role-editor')) { closeEditor(); if (idx == null) return; }
72
- const existing = idx != null ? getRoles()[idx] : null;
73
-
74
- const card = document.createElement('div');
75
- card.id = 'role-editor';
76
- card.className = 'p-3 border-b border-slate-700/50 bg-slate-800/30';
77
- card.innerHTML = `
78
- <input id="re-name" type="text" maxlength="40" placeholder="Role name" value="${esc(existing?.name || '')}"
79
- class="w-full px-3 py-2 text-sm bg-slate-900 border border-slate-700 rounded-md text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500 transition-colors mb-2">
80
- <textarea id="re-instructions" rows="4" placeholder="Who am I? e.g. You are a senior software architect. You break down goals into clear, actionable tasks with acceptance criteria."
81
- class="w-full max-w-full px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-200 placeholder-slate-600 outline-none focus:border-blue-500 transition-colors resize-y leading-relaxed font-mono mb-2" style="min-height:5lh">${esc(existing?.instructions || '')}</textarea>
82
- <div class="flex items-center gap-2">
83
- <button id="re-save" class="px-4 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-md transition-colors">${existing ? 'Save' : 'Add'}</button>
84
- <button id="re-cancel" class="px-3 py-1.5 text-xs text-slate-500 hover:text-slate-300 transition-colors">Cancel</button>
85
- </div>`;
86
-
87
- const list = panel.querySelector('#roles-list');
88
- list.parentElement.insertBefore(card, list);
89
-
90
- const nameEl = card.querySelector('#re-name');
91
- const instrEl = card.querySelector('#re-instructions');
92
- nameEl.focus();
93
-
94
- const doSave = () => {
95
- const name = nameEl.value.trim();
96
- const instructions = instrEl.value.trim();
97
- if (!name || !instructions) return;
98
- if (!state.cfg.roles) state.cfg.roles = [];
99
- if (idx != null) {
100
- state.cfg.roles[idx] = { ...state.cfg.roles[idx], name, instructions };
101
- } else {
102
- state.cfg.roles.push({ id: crypto.randomUUID(), name, instructions });
103
- }
104
- save();
105
- closeEditor();
106
- renderRoles();
107
- };
108
-
109
- card.querySelector('#re-save').addEventListener('click', doSave);
110
- card.querySelector('#re-cancel').addEventListener('click', closeEditor);
111
- nameEl.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeEditor(); });
112
- }