clideck 1.30.3 → 1.30.4

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.
@@ -10,7 +10,7 @@
10
10
  "canResume": true,
11
11
  "resumeCommand": "claude --resume {{sessionId}}",
12
12
  "sessionIdPattern": "Session ID:\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
13
- "outputMarker": "\u23fa",
13
+ "outputMarker": "",
14
14
  "telemetryConfigPath": "~/.claude/settings.json",
15
15
  "telemetrySetup": "Required for working/idle status, Autopilot, notifications, and mobile remote.\n\nCliDeck will add start/stop hooks to ~/.claude/settings.json. Claude will ask for one-time approval on next launch.",
16
16
  "telemetryAutoSetup": {
@@ -35,7 +35,7 @@
35
35
  "canResume": true,
36
36
  "resumeCommand": "codex resume {{sessionId}}",
37
37
  "sessionIdPattern": "Session:\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
38
- "outputMarker": "\u2022",
38
+ "outputMarker": "",
39
39
  "telemetryConfigPath": "~/.codex/config.toml",
40
40
  "telemetryEnv": {
41
41
  "OTEL_LOGS_EXPORTER": "otlp",
@@ -59,7 +59,7 @@
59
59
  "canResume": true,
60
60
  "resumeCommand": "gemini --resume {{sessionId}}",
61
61
  "sessionIdPattern": "Session ID:\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
62
- "outputMarker": "\u2726",
62
+ "outputMarker": "",
63
63
  "telemetryConfigPath": "~/.gemini/settings.json",
64
64
  "telemetrySetup": "Required for working/idle status, resume, Autopilot, notifications, and mobile remote.\n\nCliDeck will add BeforeAgent/AfterAgent/SessionEnd/BeforeTool hooks to ~/.gemini/settings.json.",
65
65
  "telemetryAutoSetup": {
@@ -77,7 +77,7 @@
77
77
  "canResume": true,
78
78
  "resumeCommand": "opencode --session {{sessionId}}",
79
79
  "sessionIdPattern": "(ses_[a-zA-Z0-9]{10,})",
80
- "outputMarker": "\u2502",
80
+ "outputMarker": "",
81
81
  "bridge": "opencode",
82
82
  "pluginPath": "~/.config/opencode/plugins/clideck-bridge.js",
83
83
  "pluginSetup": "Install the CliDeck bridge plugin to enable real-time status and resume.\n\ncp opencode-plugin/clideck-bridge.js ~/.config/opencode/plugins/",
@@ -85,6 +85,24 @@
85
85
  "label": "Install plugin"
86
86
  }
87
87
  },
88
+ {
89
+ "presetId": "clideck-agent",
90
+ "name": "Clideck Agent",
91
+ "icon": "/img/clideck-agent.svg",
92
+ "command": "clideck-agent",
93
+ "isAgent": true,
94
+ "canResume": true,
95
+ "resumeCommand": "clideck-agent --resume {{sessionId}}",
96
+ "sessionIdPattern": null,
97
+ "outputMarker": "•",
98
+ "telemetryEnabled": true,
99
+ "telemetryEnv": {
100
+ "OTEL_LOGS_EXPORTER": "otlp",
101
+ "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
102
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:{{port}}",
103
+ "OTEL_LOGS_EXPORT_INTERVAL": "2000"
104
+ }
105
+ },
88
106
  {
89
107
  "presetId": "shell",
90
108
  "name": "Shell",
package/config.js CHANGED
@@ -124,7 +124,8 @@ function migrate(cfg) {
124
124
  if (cmd.sessionIdPattern === undefined) cmd.sessionIdPattern = preset?.sessionIdPattern || null;
125
125
  if (cmd.outputMarker === undefined) cmd.outputMarker = preset?.outputMarker || null;
126
126
  // Claude Code telemetry is built-in, always on
127
- if (preset?.presetId === 'claude-code') cmd.telemetryEnabled = true;
127
+ if (preset?.telemetryEnabled === true) cmd.telemetryEnabled = true;
128
+ else if (preset?.presetId === 'claude-code') cmd.telemetryEnabled = true;
128
129
  else if (cmd.telemetryEnabled === undefined) cmd.telemetryEnabled = false;
129
130
  if (cmd.telemetryStatus === undefined) cmd.telemetryStatus = null;
130
131
  // Sync bridge config from preset
package/handlers.js CHANGED
@@ -139,64 +139,69 @@ function detectTelemetryConfig(c) {
139
139
  const home = os.homedir();
140
140
  const port = '4000';
141
141
  let changed = false;
142
- let repairedAny = false;
143
- for (const cmd of c.commands || []) {
144
- const bin = binName(cmd.command);
145
- const preset = presets.find(p => binName(p.command) === bin);
146
- if (!preset) continue;
147
- let detected = false;
148
- let reason = '';
149
- if (preset.presetId === 'claude-code') {
150
- try {
151
- const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
152
- const hooks = s.hooks || {};
153
- detected = hasExistingHook(hooks.UserPromptSubmit, 'claude-hook.js', 'start')
154
- && hasExistingHook(hooks.Stop, 'claude-hook.js', 'stop')
155
- && hasExistingHook(hooks.StopFailure, 'claude-hook.js', 'stop')
156
- && hasExistingHook(hooks.PreToolUse, 'claude-hook.js', 'menu')
157
- && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && hasExistingHook([h], 'claude-hook.js', 'idle'));
158
- if (!detected) reason = 'Needs re-patch';
159
- } catch {}
160
- } else if (preset.presetId === 'codex') {
161
- try {
162
- const content = readFileSync(join(home, '.codex', 'config.toml'), 'utf8');
163
- detected = codexConfigLooksHealthy(content, port);
164
- if (!detected) reason = 'Needs re-patch';
165
- } catch {}
166
- } else if (preset.presetId === 'gemini-cli') {
167
- try {
168
- const s = JSON.parse(readFileSync(join(home, '.gemini', 'settings.json'), 'utf8'));
169
- const hooks = s.hooks || {};
170
- detected = hasExistingHook(hooks.BeforeAgent, 'gemini-hook.js', 'start')
171
- && hasExistingHook(hooks.AfterAgent, 'gemini-hook.js', 'stop')
172
- && hasExistingHook(hooks.SessionEnd, 'gemini-hook.js', 'stop')
173
- && hasExistingHook(hooks.BeforeTool, 'gemini-hook.js', 'menu');
142
+ const attemptedRepairs = new Set();
143
+
144
+ for (let pass = 0; pass < 2; pass++) {
145
+ let repairedAny = false;
146
+ for (const cmd of c.commands || []) {
147
+ const bin = binName(cmd.command);
148
+ const preset = presets.find(p => binName(p.command) === bin);
149
+ if (!preset) continue;
150
+ let detected = false;
151
+ let reason = '';
152
+ if (preset.presetId === 'claude-code') {
153
+ try {
154
+ const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
155
+ const hooks = s.hooks || {};
156
+ detected = hasExistingHook(hooks.UserPromptSubmit, 'claude-hook.js', 'start')
157
+ && hasExistingHook(hooks.Stop, 'claude-hook.js', 'stop')
158
+ && hasExistingHook(hooks.StopFailure, 'claude-hook.js', 'stop')
159
+ && hasExistingHook(hooks.PreToolUse, 'claude-hook.js', 'menu')
160
+ && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && hasExistingHook([h], 'claude-hook.js', 'idle'));
161
+ if (!detected) reason = 'Needs re-patch';
162
+ } catch {}
163
+ } else if (preset.presetId === 'codex') {
164
+ try {
165
+ const content = readFileSync(join(home, '.codex', 'config.toml'), 'utf8');
166
+ detected = codexConfigLooksHealthy(content, port);
167
+ if (!detected) reason = 'Needs re-patch';
168
+ } catch {}
169
+ } else if (preset.presetId === 'gemini-cli') {
170
+ try {
171
+ const s = JSON.parse(readFileSync(join(home, '.gemini', 'settings.json'), 'utf8'));
172
+ const hooks = s.hooks || {};
173
+ detected = hasExistingHook(hooks.BeforeAgent, 'gemini-hook.js', 'start')
174
+ && hasExistingHook(hooks.AfterAgent, 'gemini-hook.js', 'stop')
175
+ && hasExistingHook(hooks.SessionEnd, 'gemini-hook.js', 'stop')
176
+ && hasExistingHook(hooks.BeforeTool, 'gemini-hook.js', 'menu');
177
+ if (!detected) reason = 'Needs re-patch';
178
+ } catch {}
179
+ } else if (preset.presetId === 'opencode') {
180
+ detected = existsSync(join(opencodePluginDir, 'clideck-bridge.js')) || existsSync(join(opencodePluginDir, 'termix-bridge.js'));
174
181
  if (!detected) reason = 'Needs re-patch';
175
- } catch {}
176
- } else if (preset.presetId === 'opencode') {
177
- detected = existsSync(join(opencodePluginDir, 'clideck-bridge.js')) || existsSync(join(opencodePluginDir, 'termix-bridge.js'));
178
- if (!detected) reason = 'Needs re-patch';
179
- } else { continue; }
180
- if (preset.available && preset.minVersion && !preset.versionOk) {
181
- detected = false;
182
- reason = `Update required (${preset.minVersion}+)`;
183
- } else if (!detected && cmd.telemetryEnabled && preset.telemetryAutoSetup && preset.available && preset.versionOk) {
184
- const repaired = applyTelemetryConfig(preset);
185
- if (repaired.success) {
186
- repairedAny = true;
187
- continue;
182
+ } else { continue; }
183
+ if (preset.available && preset.minVersion && !preset.versionOk) {
184
+ detected = false;
185
+ reason = `Update required (${preset.minVersion}+)`;
186
+ } else if (!detected && cmd.telemetryEnabled && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(preset.presetId)) {
187
+ attemptedRepairs.add(preset.presetId);
188
+ const repaired = applyTelemetryConfig(preset);
189
+ if (repaired.success) {
190
+ repairedAny = true;
191
+ continue;
192
+ }
188
193
  }
194
+ const nextEnabled = detected || (!!cmd.telemetryEnabled && !reason.startsWith('Update required'));
195
+ const nextStatus = detected ? { ok: true } : { ok: false, error: reason || 'Needs setup' };
196
+ if (cmd.telemetryEnabled !== nextEnabled || JSON.stringify(cmd.telemetryStatus || null) !== JSON.stringify(nextStatus)) {
197
+ cmd.telemetryEnabled = nextEnabled;
198
+ cmd.telemetryStatus = nextStatus;
199
+ changed = true;
200
+ }
201
+ preset.health = detected ? { ok: true } : { ok: false, reason: reason || 'Needs setup' };
189
202
  }
190
- const nextEnabled = detected || (!!cmd.telemetryEnabled && !reason.startsWith('Update required'));
191
- const nextStatus = detected ? { ok: true } : { ok: false, error: reason || 'Needs setup' };
192
- if (cmd.telemetryEnabled !== nextEnabled || JSON.stringify(cmd.telemetryStatus || null) !== JSON.stringify(nextStatus)) {
193
- cmd.telemetryEnabled = nextEnabled;
194
- cmd.telemetryStatus = nextStatus;
195
- changed = true;
196
- }
197
- preset.health = detected ? { ok: true } : { ok: false, reason: reason || 'Needs setup' };
203
+ if (!repairedAny) break;
198
204
  }
199
- if (repairedAny) return detectTelemetryConfig(c) || true;
200
205
  if (changed) console.log('Config: synced telemetry/plugin state from detected config files');
201
206
  return changed;
202
207
  }
@@ -251,10 +256,10 @@ function onConnection(ws) {
251
256
  if (choices && sess.presetId === 'codex') {
252
257
  const last = require('./telemetry-receiver').getLastEvent(msg.id);
253
258
  if (!last.startsWith('codex.sse_event:response.completed')) {
254
- console.log(`[codex] menu rejected — lastEvent=${last} session=${msg.id.slice(0,8)}`);
259
+ // console.log(`[codex] menu rejected — lastEvent=${last} session=${msg.id.slice(0,8)}`);
255
260
  choices = null;
256
261
  } else {
257
- console.log(`[codex] menu accepted session=${msg.id.slice(0,8)}`);
262
+ // console.log(`[codex] menu accepted session=${msg.id.slice(0,8)}`);
258
263
  }
259
264
  }
260
265
  if (choices && sess.presetId === 'claude-code' && msg.menuVersion && (sess._menuConsumedVersion || 0) >= msg.menuVersion) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.30.3",
3
+ "version": "1.30.4",
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": {
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
2
+ <title>Clideck Agent</title>
3
+ <defs>
4
+ <linearGradient id="bubbleGradient" x1="64" y1="72" x2="448" y2="408" gradientUnits="userSpaceOnUse">
5
+ <stop offset="0" stop-color="#2C2F38"/>
6
+ <stop offset="1" stop-color="#171A20"/>
7
+ </linearGradient>
8
+ <linearGradient id="glyphGradient" x1="122" y1="150" x2="395" y2="355" gradientUnits="userSpaceOnUse">
9
+ <stop offset="0" stop-color="#FFFFFF"/>
10
+ <stop offset="1" stop-color="#E7EAF0"/>
11
+ </linearGradient>
12
+ <filter id="glyphShadow" x="82" y="126" width="350" height="254" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
13
+ <feOffset dx="0" dy="10"/>
14
+ <feGaussianBlur stdDeviation="10"/>
15
+ <feColorMatrix type="matrix" values="0 0 0 0 0.03 0 0 0 0 0.04 0 0 0 0 0.07 0 0 0 0.35 0"/>
16
+ <feBlend in2="SourceGraphic" result="shape"/>
17
+ </filter>
18
+ </defs>
19
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" fill="url(#bubbleGradient)"/>
20
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" stroke="#3E4350" stroke-width="2"/>
21
+ <g filter="url(#glyphShadow)" stroke="url(#glyphGradient)" stroke-linecap="round" stroke-linejoin="round">
22
+ <path d="M134 168L245 256L134 344" stroke-width="40"/>
23
+ <path d="M292 344H390" stroke-width="38"/>
24
+ </g>
25
+ </svg>
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
2
+ <title>Clideck Agent</title>
3
+ <defs>
4
+ <linearGradient id="bubbleGradient" x1="64" y1="72" x2="448" y2="408" gradientUnits="userSpaceOnUse">
5
+ <stop offset="0" stop-color="#F7F8FB"/>
6
+ <stop offset="1" stop-color="#E5E9F1"/>
7
+ </linearGradient>
8
+ <linearGradient id="glyphGradient" x1="122" y1="150" x2="395" y2="355" gradientUnits="userSpaceOnUse">
9
+ <stop offset="0" stop-color="#1D212B"/>
10
+ <stop offset="1" stop-color="#3D4656"/>
11
+ </linearGradient>
12
+ <filter id="glyphShadow" x="82" y="126" width="350" height="254" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
13
+ <feOffset dx="0" dy="6"/>
14
+ <feGaussianBlur stdDeviation="7"/>
15
+ <feColorMatrix type="matrix" values="0 0 0 0 0.47 0 0 0 0 0.53 0 0 0 0 0.63 0 0 0 0.18 0"/>
16
+ <feBlend in2="SourceGraphic" result="shape"/>
17
+ </filter>
18
+ </defs>
19
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" fill="url(#bubbleGradient)"/>
20
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" stroke="#C6CEDA" stroke-width="2"/>
21
+ <g filter="url(#glyphShadow)" stroke="url(#glyphGradient)" stroke-linecap="round" stroke-linejoin="round">
22
+ <path d="M134 168L245 256L134 344" stroke-width="40"/>
23
+ <path d="M292 344H390" stroke-width="38"/>
24
+ </g>
25
+ </svg>
@@ -27,6 +27,11 @@ function findCommandForPreset(p) {
27
27
  || state.cfg.commands.find(c => binName(c.command) === binName(p.command));
28
28
  }
29
29
 
30
+ function telemetryEnabledForPreset(preset, existing) {
31
+ if (preset?.telemetryEnabled === true) return true;
32
+ return !!existing?.telemetryEnabled;
33
+ }
34
+
30
35
  // True if preset binary is missing and the configured command is unchanged from the preset default
31
36
  function isPresetMissing(p) {
32
37
  if (p.available !== false) return false;
@@ -115,7 +120,7 @@ function createFromPreset(preset, sessionName, cwd, projectId) {
115
120
  resumeCommand: preset.resumeCommand,
116
121
  sessionIdPattern: preset.sessionIdPattern,
117
122
  outputMarker: preset.outputMarker || null,
118
- telemetryEnabled: false,
123
+ telemetryEnabled: telemetryEnabledForPreset(preset),
119
124
  telemetryStatus: null,
120
125
  bridge: preset.bridge,
121
126
  };
@@ -270,7 +275,7 @@ export function openCreator() {
270
275
  if (!preset) return;
271
276
  let cmd = findCommandForPreset(preset);
272
277
  if (!cmd) {
273
- cmd = { id: crypto.randomUUID(), presetId: preset.presetId, label: preset.name, icon: preset.icon, command: preset.command, enabled: true, defaultPath: '', isAgent: preset.isAgent, canResume: preset.canResume, resumeCommand: preset.resumeCommand, sessionIdPattern: preset.sessionIdPattern, outputMarker: preset.outputMarker || null, telemetryEnabled: false, telemetryStatus: null, bridge: preset.bridge };
278
+ cmd = { id: crypto.randomUUID(), presetId: preset.presetId, label: preset.name, icon: preset.icon, command: preset.command, enabled: true, defaultPath: '', isAgent: preset.isAgent, canResume: preset.canResume, resumeCommand: preset.resumeCommand, sessionIdPattern: preset.sessionIdPattern, outputMarker: preset.outputMarker || null, telemetryEnabled: telemetryEnabledForPreset(preset), telemetryStatus: null, bridge: preset.bridge };
274
279
  state.cfg.commands.push(cmd);
275
280
  send({ type: 'config.update', config: state.cfg });
276
281
  }
@@ -24,15 +24,69 @@ document.getElementById('settings-nav').addEventListener('click', (e) => {
24
24
  if (btn) switchCategory(btn.dataset.cat);
25
25
  });
26
26
 
27
+ function captureSettingsFocus() {
28
+ const active = document.activeElement;
29
+ if (!active || !(active instanceof HTMLElement)) return null;
30
+ if (!active.closest('#panel-settings')) return null;
31
+
32
+ const snapshot = {
33
+ id: active.id || null,
34
+ selector: null,
35
+ cardIdx: null,
36
+ selectionStart: null,
37
+ selectionEnd: null,
38
+ };
39
+
40
+ const card = active.closest('.agent-card');
41
+ if (card) {
42
+ snapshot.cardIdx = card.dataset.idx || null;
43
+ for (const cls of ['agent-name', 'agent-command', 'agent-enabled', 'agent-is-agent', 'agent-can-resume', 'agent-resume-cmd']) {
44
+ if (active.classList.contains(cls)) {
45
+ snapshot.selector = `.${cls}`;
46
+ break;
47
+ }
48
+ }
49
+ }
50
+
51
+ if (typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') {
52
+ snapshot.selectionStart = active.selectionStart;
53
+ snapshot.selectionEnd = active.selectionEnd;
54
+ }
55
+
56
+ return snapshot;
57
+ }
58
+
59
+ function restoreSettingsFocus(snapshot) {
60
+ if (!snapshot) return;
61
+
62
+ let target = null;
63
+ if (snapshot.id) target = document.getElementById(snapshot.id);
64
+ if (!target && snapshot.cardIdx != null && snapshot.selector) {
65
+ target = document.querySelector(`.agent-card[data-idx="${snapshot.cardIdx}"] ${snapshot.selector}`);
66
+ }
67
+ if (!(target instanceof HTMLElement)) return;
68
+
69
+ target.focus({ preventScroll: true });
70
+ if (
71
+ typeof snapshot.selectionStart === 'number' &&
72
+ typeof snapshot.selectionEnd === 'number' &&
73
+ typeof target.setSelectionRange === 'function'
74
+ ) {
75
+ target.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd);
76
+ }
77
+ }
78
+
27
79
  // ── Render all ──
28
80
 
29
81
  export function renderSettings() {
82
+ const focusSnapshot = captureSettingsFocus();
30
83
  document.getElementById('cfg-default-path').value = state.cfg.defaultPath || '';
31
84
  document.getElementById('cfg-confirm-close').checked = state.cfg.confirmClose !== false;
32
85
  renderAgentList();
33
86
  renderThemeSection();
34
87
  renderNotifications();
35
88
  updateVersionFooter();
89
+ restoreSettingsFocus(focusSnapshot);
36
90
  }
37
91
 
38
92
  export function updateVersionFooter() {
@@ -107,6 +161,22 @@ function telemetryPreset(cmd) {
107
161
  return (state.presets || []).find(p => binName(p.command) === bin);
108
162
  }
109
163
 
164
+ function presetForCommand(existing, command) {
165
+ const presets = state.presets || [];
166
+ if (existing?.presetId) {
167
+ const byId = presets.find(p => p.presetId === existing.presetId);
168
+ if (byId) return byId;
169
+ }
170
+ const bin = binName(command || existing?.command || '');
171
+ return presets.find(p => binName(p.command) === bin) || null;
172
+ }
173
+
174
+ function telemetryEnabledForCommand(existing, command) {
175
+ const preset = presetForCommand(existing, command);
176
+ if (preset?.telemetryEnabled === true) return true;
177
+ return !!existing?.telemetryEnabled;
178
+ }
179
+
110
180
  function integrationSection(c) {
111
181
  const preset = telemetryPreset(c);
112
182
  if (!preset) return '';
@@ -217,7 +287,7 @@ function openPresetMenu(anchorEl) {
217
287
  enabled: true, defaultPath: '', isAgent: p.isAgent, canResume: p.canResume,
218
288
  resumeCommand: p.resumeCommand, sessionIdPattern: p.sessionIdPattern,
219
289
  outputMarker: p.outputMarker || null,
220
- telemetryEnabled: false,
290
+ telemetryEnabled: telemetryEnabledForCommand({ presetId: p.presetId, command: p.command }, p.command),
221
291
  telemetryStatus: null,
222
292
  bridge: p.bridge,
223
293
  });
@@ -453,7 +523,7 @@ function saveConfig() {
453
523
  resumeCommand: card.querySelector('.agent-resume-cmd')?.value.trim() || null,
454
524
  sessionIdPattern: existing.sessionIdPattern || null,
455
525
  outputMarker: existing.outputMarker || null,
456
- telemetryEnabled: existing.telemetryEnabled || false,
526
+ telemetryEnabled: telemetryEnabledForCommand(existing, command),
457
527
  telemetryStatus: existing.telemetryStatus || null,
458
528
  bridge: existing.bridge,
459
529
  };
@@ -185,6 +185,7 @@ function openMenu(sessionId, anchor) {
185
185
  // Project submenu items
186
186
  if (projects.length) {
187
187
  html += `<div class="px-3 py-1 text-[10px] font-semibold uppercase tracking-wider text-slate-600">Move to project</div>`;
188
+ html += `<div class="tmx-scroll py-0.5" style="max-height:10rem;overflow-y:auto">`;
188
189
  for (const p of projects) {
189
190
  const active = entry?.projectId === p.id;
190
191
  html += `<button class="menu-action flex items-center gap-2 w-full px-3 py-1.5 text-sm ${active ? 'text-blue-400' : 'text-slate-300'} hover:bg-slate-700 transition-colors text-left" data-action="project" data-project-id="${p.id}">
@@ -192,6 +193,7 @@ function openMenu(sessionId, anchor) {
192
193
  ${esc(p.name)}${active ? ' ✓' : ''}
193
194
  </button>`;
194
195
  }
196
+ html += `</div>`;
195
197
  if (entry?.projectId) {
196
198
  html += `<button class="menu-action flex items-center gap-2 w-full px-3 py-1.5 text-sm text-slate-500 hover:bg-slate-700 transition-colors text-left" data-action="unproject">
197
199
  <span class="w-2 h-2 rounded-full flex-shrink-0 border border-slate-600"></span>
@@ -29,6 +29,7 @@ const ICON_VARIANTS = {
29
29
  '/img/codex.png': { dark: '/img/codex-dark.png', light: '/img/codex-light.png' },
30
30
  '/img/gemini.png': { all: '/img/gemini-all.png' },
31
31
  '/img/opencode.png': { all: '/img/opencode-all.png' },
32
+ '/img/clideck-agent.svg': { dark: '/img/clideck-agent-dark.svg', light: '/img/clideck-agent-light.svg' },
32
33
  };
33
34
 
34
35
  export function resolveIconPath(icon) {
package/sessions.js CHANGED
@@ -93,7 +93,7 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
93
93
  const preset = PRESETS.find(p => binName(p.command) === bin);
94
94
  const session = { name, themeId, commandId, cwd, pty: term, chunks: [], chunksSize: 0, sessionToken: savedToken || null, projectId: projectId || null, presetId: preset?.presetId || 'shell', working: undefined };
95
95
  sessions.set(id, session);
96
- transcript.setFinalizeOnIdle(id, ['claude-code', 'codex', 'gemini-cli', 'opencode'].includes(session.presetId) ? session.presetId : null);
96
+ transcript.setFinalizeOnIdle(id, ['claude-code', 'codex', 'gemini-cli', 'opencode', 'clideck-agent'].includes(session.presetId) ? session.presetId : null);
97
97
 
98
98
  // Always watch telemetry-backed agents so OTLP fallback matching can attach
99
99
  // early events to this session even when the agent omits clideck.session_id.
@@ -445,7 +445,7 @@ function getResumable(cfg) {
445
445
 
446
446
  function sendBuffers(ws) {
447
447
  for (const [id, s] of sessions) {
448
- if (['claude-code', 'codex', 'gemini-cli', 'opencode'].includes(s.presetId) && !s.working) {
448
+ if (['claude-code', 'codex', 'gemini-cli', 'opencode', 'clideck-agent'].includes(s.presetId) && !s.working) {
449
449
  const text = transcript.getReplayText(id, s.presetId);
450
450
  if (text) {
451
451
  ws.send(JSON.stringify({ type: 'session.history', id, text, replay: true }));
@@ -71,7 +71,7 @@ function handleLogs(req, res) {
71
71
  const resAttrs = parseAttrs(rl.resource?.attributes);
72
72
  const sessionId = resAttrs['clideck.session_id'];
73
73
 
74
- // service.name values: claude-code, codex_cli_rs, gemini-cli
74
+ // service.name values: claude-code, codex_cli_rs, gemini-cli, clideck-agent
75
75
  const serviceName = resAttrs['service.name'] || 'unknown';
76
76
  let resolvedId = sessionId;
77
77
 
@@ -135,6 +135,13 @@ function handleLogs(req, res) {
135
135
  broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
136
136
  }
137
137
 
138
+ if (serviceName === 'clideck-agent' && eventName === 'clideck.turn_start') {
139
+ broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
140
+ }
141
+ if (serviceName === 'clideck-agent' && eventName === 'clideck.agent_idle') {
142
+ broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
143
+ }
144
+
138
145
  // Codex can announce a function-call phase before the later tool_decision
139
146
  // event carries a call_id. Block idle as soon as the tool phase is known,
140
147
  // then refine it to call-specific tracking when tool_decision arrives.