clideck 1.22.3 → 1.22.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.
@@ -4,6 +4,7 @@
4
4
  "name": "Claude Code",
5
5
  "icon": "/img/claude-code.png",
6
6
  "command": "claude",
7
+ "installCmd": "npm install -g @anthropic-ai/claude-code",
7
8
  "isAgent": true,
8
9
  "canResume": true,
9
10
  "resumeCommand": "claude --resume {{sessionId}}",
@@ -23,6 +24,7 @@
23
24
  "name": "Codex",
24
25
  "icon": "/img/codex.png",
25
26
  "command": "codex --no-alt-screen",
27
+ "installCmd": "npm install -g @openai/codex",
26
28
  "isAgent": true,
27
29
  "canResume": true,
28
30
  "resumeCommand": "codex resume {{sessionId}} --no-alt-screen",
@@ -45,6 +47,7 @@
45
47
  "name": "Gemini CLI",
46
48
  "icon": "/img/gemini.png",
47
49
  "command": "gemini",
50
+ "installCmd": "npm install -g @google/gemini-cli",
48
51
  "isAgent": true,
49
52
  "canResume": true,
50
53
  "resumeCommand": "gemini --resume {{sessionId}}",
@@ -67,6 +70,7 @@
67
70
  "name": "OpenCode",
68
71
  "icon": "/img/opencode.png",
69
72
  "command": "opencode",
73
+ "installCmd": "npm install -g opencode-ai",
70
74
  "isAgent": true,
71
75
  "canResume": true,
72
76
  "resumeCommand": "opencode --session {{sessionId}}",
package/handlers.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, unlinkSync } = require('fs');
2
2
  const { join, dirname } = require('path');
3
+ const { execFileSync } = require('child_process');
3
4
  const os = require('os');
4
5
  const config = require('./config');
5
6
  const sessions = require('./sessions');
@@ -10,6 +11,17 @@ for (const p of presets) if (p.presetId === 'shell') p.command = defaultShell;
10
11
  const transcript = require('./transcript');
11
12
  const plugins = require('./plugin-loader');
12
13
 
14
+ // Check which agent binaries are available on PATH
15
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
16
+ function checkAvailability() {
17
+ for (const p of presets) {
18
+ if (p.presetId === 'shell') { p.available = true; continue; }
19
+ try { execFileSync(whichCmd, [binName(p.command)], { stdio: 'ignore' }); p.available = true; }
20
+ catch { p.available = false; }
21
+ }
22
+ }
23
+ checkAvailability();
24
+
13
25
  let cfg = config.load();
14
26
  if (detectTelemetryConfig(cfg)) config.save(cfg);
15
27
 
@@ -80,6 +92,11 @@ function onConnection(ws) {
80
92
  ws.send(JSON.stringify({ type: 'config', config: cfg }));
81
93
  break;
82
94
 
95
+ case 'checkAvailability':
96
+ checkAvailability();
97
+ ws.send(JSON.stringify({ type: 'presets', presets }));
98
+ break;
99
+
83
100
  case 'config.update':
84
101
  cfg = { ...cfg, ...msg.config };
85
102
  detectTelemetryConfig(cfg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.22.3",
3
+ "version": "1.22.5",
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": {
Binary file
Binary file
Binary file
Binary file
Binary file
package/public/index.html CHANGED
@@ -6,6 +6,45 @@
6
6
  <link rel="icon" type="image/png" href="/img/clideck-logo-icon.png">
7
7
  <link rel="stylesheet" href="/xterm.css">
8
8
  <link rel="stylesheet" href="/tailwind.css">
9
+ <style>
10
+ :root {
11
+ --color-project-header-bg: #191c21;
12
+ --color-unread-pill-bg: rgba(59, 130, 246, 0.15);
13
+ --color-filter-unread-bg: transparent;
14
+ --color-list-row-hover: color-mix(in srgb, var(--color-chat-hover) 55%, transparent);
15
+ --color-list-row-active: var(--color-chat-active);
16
+ --color-session-icon-bg: #242626;
17
+ }
18
+ .light {
19
+ --color-project-header-bg: #f6f5f5;
20
+ --color-unread-pill-bg: #f6f5f5;
21
+ --color-filter-unread-bg: #f6f5f5;
22
+ --color-session-icon-bg: #f7f5f3;
23
+ }
24
+ .session-row,
25
+ .resumable-row {
26
+ margin: 2px 8px;
27
+ border-radius: 10px;
28
+ transition: background-color 0.18s ease;
29
+ }
30
+ .session-row:hover,
31
+ .resumable-row:hover {
32
+ background: var(--color-list-row-hover) !important;
33
+ }
34
+ .session-row.active-session {
35
+ background: var(--color-list-row-active) !important;
36
+ }
37
+ .project-group {
38
+ margin: 8px 0 4px;
39
+ }
40
+ .project-group:first-child {
41
+ margin-top: 4px;
42
+ }
43
+ .project-header {
44
+ margin: 0 8px 4px;
45
+ border-radius: 10px;
46
+ }
47
+ </style>
9
48
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
49
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
50
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@700&display=swap" rel="stylesheet">
@@ -58,9 +97,9 @@
58
97
  </div>
59
98
  <div class="flex bg-slate-800/30 rounded-lg p-[3px]">
60
99
  <button class="filter-tab flex-1 text-[11px] font-medium py-[5px] rounded-md transition-all bg-slate-700/60 text-slate-200" data-tab="all">All</button>
61
- <button class="filter-tab flex-1 text-[11px] font-medium py-[5px] rounded-md transition-all text-slate-500 hover:text-slate-400 flex items-center justify-center gap-1" data-tab="unread">
100
+ <button class="filter-tab flex-1 text-[11px] font-medium py-[5px] rounded-md transition-all text-slate-500 hover:text-slate-400 flex items-center justify-center gap-1" data-tab="unread" style="background:var(--color-filter-unread-bg)">
62
101
  Unread
63
- <span id="unread-badge" class="hidden min-w-[16px] h-4 px-1 rounded-full bg-blue-500/15 text-blue-400 text-[10px] font-semibold inline-flex items-center justify-center">0</span>
102
+ <span id="unread-badge" class="hidden min-w-[16px] h-4 px-1 rounded-full text-blue-400 text-[10px] font-semibold inline-flex items-center justify-center" style="background:var(--color-unread-pill-bg)">0</span>
64
103
  </button>
65
104
  </div>
66
105
  </div>
package/public/js/app.js CHANGED
@@ -2,7 +2,7 @@ import { state, send } from './state.js';
2
2
  import { esc, binName } from './utils.js';
3
3
  import { addTerminal, removeTerminal, select, startRename, startProjectRename, setSessionTheme, openMenu, closeMenu, setStatus, updateMuteIndicator, updatePreview, markUnread, applyFilter, setTab, renderResumable, regroupSessions, toggleProjectCollapse, setSessionProject, estimateSize, restartComplete, positionMenu } from './terminals.js';
4
4
  import { renderSettings } from './settings.js';
5
- import { openCreator, closeCreator } from './creator.js';
5
+ import { openCreator, closeCreator, refreshCreator } from './creator.js';
6
6
  import { handleDirsResponse, openFolderPicker } from './folder-picker.js';
7
7
  import { confirmClose } from './confirm.js';
8
8
  import { applyTheme } from './profiles.js';
@@ -42,6 +42,7 @@ function connect() {
42
42
  case 'presets':
43
43
  state.presets = msg.presets;
44
44
  renderSettings();
45
+ refreshCreator();
45
46
  break;
46
47
  case 'sessions.resumable':
47
48
  state.resumable = msg.list;
@@ -295,7 +296,10 @@ function showModeToast() {
295
296
  });
296
297
  }
297
298
 
298
- document.getElementById('btn-new').addEventListener('click', openCreator);
299
+ document.getElementById('btn-new').addEventListener('click', () => {
300
+ send({ type: 'checkAvailability' });
301
+ openCreator();
302
+ });
299
303
  document.getElementById('btn-new-project').addEventListener('click', () => {
300
304
  closeCreator();
301
305
  openProjectCreator();
@@ -385,6 +389,18 @@ function showTelemetrySetup(commandId, sessionId) {
385
389
 
386
390
  // --- Project context menu ---
387
391
  let projectMenuCleanup = null;
392
+
393
+ function resumeDormantSessions(ids, label) {
394
+ const uniqueIds = [...new Set(ids)].filter(Boolean);
395
+ if (!uniqueIds.length) return;
396
+ showToast(`Starting ${uniqueIds.length} dormant session${uniqueIds.length > 1 ? 's' : ''}${label ? ` from ${label}` : ''}…`, { duration: 3000 });
397
+ uniqueIds.forEach((id, index) => {
398
+ setTimeout(() => {
399
+ if (state.resumable.some(s => s.id === id)) send({ type: 'session.resume', id });
400
+ }, index * 1000);
401
+ });
402
+ }
403
+
388
404
  function openProjectMenu(projectId, anchorEl) {
389
405
  if (projectMenuCleanup) projectMenuCleanup();
390
406
  const proj = (state.cfg.projects || []).find(p => p.id === projectId);
@@ -407,6 +423,10 @@ function openProjectMenu(projectId, anchorEl) {
407
423
  <svg class="w-4 h-4 flex-shrink-0 text-slate-400" 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>
408
424
  Rename
409
425
  </button>
426
+ <button class="pm-action flex items-center gap-2 w-full px-3 py-2 text-sm ${hasDormant ? 'text-slate-300 hover:bg-slate-700 cursor-pointer' : 'text-slate-600 cursor-default'} transition-colors text-left" data-action="start-dormant" ${hasDormant ? '' : 'disabled'}>
427
+ <svg class="w-4 h-4 flex-shrink-0 ${hasDormant ? 'text-slate-400' : 'text-slate-600'}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="7 5 19 12 7 19 7 5"/></svg>
428
+ Start all dormant sessions
429
+ </button>
410
430
  <button class="pm-action flex items-center gap-2 w-full px-3 py-2 text-sm ${hasDormant ? 'text-slate-300 hover:bg-slate-700 cursor-pointer' : 'text-slate-600 cursor-default'} transition-colors text-left" data-action="clear-dormant" ${hasDormant ? '' : 'disabled'}>
411
431
  <svg class="w-4 h-4 flex-shrink-0 ${hasDormant ? 'text-slate-400' : 'text-slate-600'}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/><line x1="18" y1="9" x2="12" y2="15"/><line x1="12" y1="9" x2="18" y2="15"/></svg>
412
432
  Clear dormant sessions
@@ -433,6 +453,13 @@ function openProjectMenu(projectId, anchorEl) {
433
453
  startProjectRename(projectId);
434
454
  return;
435
455
  }
456
+ if (btn.dataset.action === 'start-dormant') {
457
+ const ids = [...document.querySelectorAll(`.project-group[data-project-id="${projectId}"] .project-sessions [data-resumable-id]`)]
458
+ .map(el => el.dataset.resumableId);
459
+ if (!ids.length) return;
460
+ resumeDormantSessions(ids, `"${proj?.name || 'project'}"`);
461
+ return;
462
+ }
436
463
  if (btn.dataset.action === 'clear-dormant') {
437
464
  const ids = state.resumable.filter(s => s.projectId === projectId).map(s => s.id);
438
465
  if (!ids.length) return;
@@ -2,6 +2,7 @@ import { state, send } from './state.js';
2
2
  import { esc, agentIcon, binName } from './utils.js';
3
3
  import { openFolderPicker } from './folder-picker.js';
4
4
  import { estimateSize } from './terminals.js';
5
+ import { showToast } from './toast.js';
5
6
 
6
7
  const ADJECTIVES = [
7
8
  'Blue', 'Red', 'Green', 'Purple', 'Golden', 'Silver', 'Coral', 'Amber',
@@ -101,11 +102,18 @@ export function openCreator() {
101
102
  </button>
102
103
  </div>
103
104
  <div class="space-y-0.5">
104
- ${presets.map(p => `
105
- <button class="preset-btn w-full flex items-center gap-2.5 px-3 py-2 rounded-md hover:bg-slate-700/70 text-sm text-slate-300 transition-colors text-left" data-preset="${p.presetId}">
106
- ${agentIcon(p.icon, 24)}
107
- <span>${esc(p.name)}</span>
108
- </button>`).join('')}
105
+ ${presets.map(p => {
106
+ const hasConfigured = state.cfg.commands.some(c => binName(c.command) === binName(p.command) && c.enabled !== false);
107
+ const missing = p.available === false && !hasConfigured;
108
+ return `
109
+ <button class="preset-btn w-full flex items-center gap-2.5 px-3 py-2 rounded-md hover:bg-slate-700/70 text-sm transition-colors text-left ${missing ? 'text-slate-500' : 'text-slate-300'}" data-preset="${p.presetId}">
110
+ <span class="${missing ? 'opacity-40' : ''}">${agentIcon(p.icon, 24)}</span>
111
+ <span class="flex-1 min-w-0">
112
+ <span>${esc(p.name)}</span>
113
+ ${missing ? `<span class="block text-[10px] text-slate-600 truncate">${esc(p.installCmd || 'Not installed')}</span>` : ''}
114
+ </span>
115
+ </button>`;
116
+ }).join('')}
109
117
  </div>`;
110
118
 
111
119
  const list = document.getElementById('session-list');
@@ -187,6 +195,14 @@ export function openCreator() {
187
195
  if (!btn) return;
188
196
  const preset = state.presets.find(p => p.presetId === btn.dataset.preset);
189
197
  if (!preset) return;
198
+ const hasConfigured = state.cfg.commands.some(c => binName(c.command) === binName(preset.command) && c.enabled !== false);
199
+ if (preset.available === false && !hasConfigured && preset.installCmd) {
200
+ navigator.clipboard.writeText(preset.installCmd).then(
201
+ () => showToast(`Install command copied: <code class="text-slate-200">${esc(preset.installCmd)}</code>`, { html: true, duration: 4000 }),
202
+ () => showToast(`Run: ${preset.installCmd}`, { duration: 4000 }),
203
+ );
204
+ return;
205
+ }
190
206
  const name = nameInput.value.trim() || fallbackName;
191
207
  const cwd = cwdInput.value.trim() || undefined;
192
208
  const projectSelect = card.querySelector('#creator-project');
@@ -196,6 +212,13 @@ export function openCreator() {
196
212
  });
197
213
  }
198
214
 
215
+ export function refreshCreator() {
216
+ if (document.getElementById('session-creator')) {
217
+ closeCreator();
218
+ openCreator();
219
+ }
220
+ }
221
+
199
222
  export function closeCreator() {
200
223
  document.getElementById('session-creator')?.remove();
201
224
  }
@@ -1,5 +1,5 @@
1
1
  import { state, send } from './state.js';
2
- import { esc } from './utils.js';
2
+ import { esc, resolveIconPath } from './utils.js';
3
3
  import { resolveTheme, resolveAccent, applyTheme } from './profiles.js';
4
4
  import { attachToTerminal } from './hotkeys.js';
5
5
  import { closeDropdown } from './prompts.js';
@@ -97,7 +97,7 @@ function startBounce(container) {
97
97
  function iconHtml(commandId) {
98
98
  const icon = state.cfg.commands.find(c => c.id === commandId)?.icon || 'terminal';
99
99
  if (icon.startsWith('/'))
100
- return `<img src="${esc(icon)}" class="w-5 h-5 object-contain" draggable="false">`;
100
+ return `<img src="${esc(resolveIconPath(icon))}" class="w-5 h-5 object-contain" draggable="false">`;
101
101
  return TERMINAL_SVG;
102
102
  }
103
103
 
@@ -290,10 +290,10 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
290
290
  themeId = themeId || state.cfg.defaultTheme || 'default';
291
291
 
292
292
  const item = document.createElement('div');
293
- item.className = 'group flex items-center gap-2 px-2.5 py-2 cursor-pointer transition-colors select-none';
293
+ item.className = 'group session-row flex items-center gap-2 px-2.5 py-2 cursor-pointer transition-colors select-none';
294
294
  item.dataset.id = id;
295
295
  item.innerHTML = `
296
- <div class="session-icon w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 overflow-hidden pointer-events-none">
296
+ <div class="session-icon w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden pointer-events-none" style="background:var(--color-session-icon-bg)">
297
297
  ${iconHtml(commandId)}
298
298
  </div>
299
299
  <div class="flex-1 min-w-0 pointer-events-none">
@@ -698,7 +698,7 @@ export function regroupSessions() {
698
698
 
699
699
  const collapsed = proj.collapsed;
700
700
  header.innerHTML = `
701
- <div class="group project-header flex items-center gap-1.5 px-2.5 py-1.5 cursor-pointer hover:bg-slate-800/30 transition-colors select-none" data-project-id="${proj.id}">
701
+ <div class="group project-header flex items-center gap-1.5 px-2.5 py-1.5 cursor-pointer hover:bg-slate-800/30 transition-colors select-none" data-project-id="${proj.id}" style="background:var(--color-project-header-bg)">
702
702
  <span class="project-chevron ${collapsed ? 'collapsed' : ''} text-slate-500">${CHEVRON_SVG}</span>
703
703
  <span class="w-2 h-2 rounded-full flex-shrink-0" style="background:${projectColor(proj)}"></span>
704
704
  <span class="project-name flex-1 text-[11px] font-semibold uppercase tracking-wider text-slate-500 truncate">${esc(proj.name)}</span>
@@ -797,7 +797,7 @@ function buildResumableRow(s) {
797
797
  row.className = 'group resumable-row flex items-center gap-2 px-2.5 py-2 cursor-pointer hover:bg-slate-800/30 transition-colors';
798
798
  row.dataset.resumableId = s.id;
799
799
  row.innerHTML = `
800
- <div class="w-8 h-8 rounded-full bg-slate-800/50 flex items-center justify-center flex-shrink-0 overflow-hidden opacity-40">
800
+ <div class="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden opacity-40" style="background:var(--color-session-icon-bg)">
801
801
  ${iconHtml(s.commandId)}
802
802
  </div>
803
803
  <div class="flex-1 min-w-0">
@@ -807,7 +807,7 @@ function buildResumableRow(s) {
807
807
  </div>
808
808
  <div class="flex items-center gap-1 mt-0.5">
809
809
  <span class="flex-1 text-xs text-slate-600 truncate">${s.lastPreview ? esc(s.lastPreview) : esc(label) + (path ? ' · ' + esc(path) : '')}</span>
810
- <button class="resume-btn opacity-0 group-hover:opacity-100 text-slate-600 hover:text-emerald-400 flex-shrink-0 transition-all flex items-center gap-0.5 text-[11px] font-medium" title="Resume session">
810
+ <button class="resume-btn opacity-60 group-hover:opacity-100 text-slate-500 hover:text-emerald-400 flex-shrink-0 transition-all flex items-center gap-0.5 text-[11px] font-medium" title="Resume session">
811
811
  Resume${RESUME_SVG}
812
812
  </button>
813
813
  </div>
@@ -875,6 +875,7 @@ export function setTab(tab) {
875
875
  const base = 'filter-tab flex-1 text-[11px] font-medium py-[5px] rounded-md transition-all';
876
876
  const extra = btn.dataset.tab === 'unread' ? ' flex items-center justify-center gap-1' : '';
877
877
  btn.className = base + extra + (active ? ' bg-slate-700/60 text-slate-200' : ' text-slate-500 hover:text-slate-400');
878
+ btn.style.background = !active && btn.dataset.tab === 'unread' ? 'var(--color-filter-unread-bg)' : '';
878
879
  });
879
880
  applyFilter();
880
881
  }
@@ -15,10 +15,26 @@ export function debounce(fn, ms) {
15
15
 
16
16
  const TERMINAL_SVG = `<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`;
17
17
 
18
+ const ICON_VARIANTS = {
19
+ '/img/claude-code.png': { all: '/img/claude-all.png' },
20
+ '/img/codex.png': { dark: '/img/codex-dark.png', light: '/img/codex-light.png' },
21
+ '/img/gemini.png': { all: '/img/gemini-all.png' },
22
+ '/img/opencode.png': { all: '/img/opencode-all.png' },
23
+ };
24
+
25
+ export function resolveIconPath(icon) {
26
+ if (!icon || !icon.startsWith('/')) return icon;
27
+ const canonical = icon.replace(/-(light|dark|all)(?=\.[a-z]+$)/, '');
28
+ const variants = ICON_VARIANTS[canonical];
29
+ if (!variants) return icon;
30
+ const isLight = document.documentElement.classList.contains('light');
31
+ return (isLight ? variants.light : variants.dark) || variants.all || icon;
32
+ }
33
+
18
34
  export function agentIcon(icon, px = 32) {
19
35
  const s = `width:${px}px;height:${px}px`;
20
36
  if (icon && icon.startsWith('/')) {
21
- return `<img src="${esc(icon)}" style="${s}" class="rounded object-cover flex-shrink-0" alt="">`;
37
+ return `<img src="${esc(resolveIconPath(icon))}" style="${s}" class="rounded object-cover flex-shrink-0" alt="">`;
22
38
  }
23
39
  if (icon === 'terminal') {
24
40
  return `<div style="${s}" class="rounded bg-slate-700 flex items-center justify-center text-slate-400 flex-shrink-0">${TERMINAL_SVG}</div>`;
Binary file
Binary file
Binary file