clideck 1.22.2 → 1.22.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.
- package/README.md +1 -1
- package/agent-presets.json +4 -0
- package/handlers.js +17 -0
- package/package.json +1 -1
- package/public/img/claude-all.png +0 -0
- package/public/img/clideck-logo-terminal-panel.png +0 -0
- package/public/img/codex-dark.png +0 -0
- package/public/img/codex-light.png +0 -0
- package/public/img/gemini-all.png +0 -0
- package/public/img/opencode-all.png +0 -0
- package/public/index.html +42 -3
- package/public/js/app.js +25 -1
- package/public/js/creator.js +30 -5
- package/public/js/terminals.js +8 -7
- package/public/js/utils.js +17 -1
- package/server.js +8 -8
- package/public/img/codex.png +0 -0
- package/public/img/gemini.png +0 -0
- package/public/img/opencode.png +0 -0
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Tested on **macOS** and **Windows**. Works in any modern browser. Linux: unteste
|
|
|
70
70
|
|
|
71
71
|
Full setup guides, agent configuration, and plugin development:
|
|
72
72
|
|
|
73
|
-
**[Documentation](https://clideck.dev/)**
|
|
73
|
+
**[Documentation](https://docs.clideck.dev/)**
|
|
74
74
|
|
|
75
75
|
## License
|
|
76
76
|
|
package/agent-presets.json
CHANGED
|
@@ -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
|
Binary file
|
|
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">
|
|
@@ -39,7 +78,7 @@
|
|
|
39
78
|
<!-- Chats panel -->
|
|
40
79
|
<div id="panel-chats" class="flex flex-col flex-1 min-h-0">
|
|
41
80
|
<div class="flex items-center justify-between px-3 pt-3 pb-2">
|
|
42
|
-
<span class="text-sm font-bold
|
|
81
|
+
<span class="text-sm font-bold tracking-tight" style="font-family:'JetBrains Mono',monospace"><span style="color:var(--color-text)">cli</span><span style="color:var(--color-dim)">deck</span></span>
|
|
43
82
|
<div class="flex items-center gap-1">
|
|
44
83
|
<span id="save-indicator" class="save-indicator" title="Sessions saved">
|
|
45
84
|
<svg class="save-tick" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
@@ -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
|
|
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;
|
|
@@ -385,6 +386,18 @@ function showTelemetrySetup(commandId, sessionId) {
|
|
|
385
386
|
|
|
386
387
|
// --- Project context menu ---
|
|
387
388
|
let projectMenuCleanup = null;
|
|
389
|
+
|
|
390
|
+
function resumeDormantSessions(ids, label) {
|
|
391
|
+
const uniqueIds = [...new Set(ids)].filter(Boolean);
|
|
392
|
+
if (!uniqueIds.length) return;
|
|
393
|
+
showToast(`Starting ${uniqueIds.length} dormant session${uniqueIds.length > 1 ? 's' : ''}${label ? ` from ${label}` : ''}…`, { duration: 3000 });
|
|
394
|
+
uniqueIds.forEach((id, index) => {
|
|
395
|
+
setTimeout(() => {
|
|
396
|
+
if (state.resumable.some(s => s.id === id)) send({ type: 'session.resume', id });
|
|
397
|
+
}, index * 1000);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
388
401
|
function openProjectMenu(projectId, anchorEl) {
|
|
389
402
|
if (projectMenuCleanup) projectMenuCleanup();
|
|
390
403
|
const proj = (state.cfg.projects || []).find(p => p.id === projectId);
|
|
@@ -407,6 +420,10 @@ function openProjectMenu(projectId, anchorEl) {
|
|
|
407
420
|
<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
421
|
Rename
|
|
409
422
|
</button>
|
|
423
|
+
<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'}>
|
|
424
|
+
<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>
|
|
425
|
+
Start all dormant sessions
|
|
426
|
+
</button>
|
|
410
427
|
<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
428
|
<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
429
|
Clear dormant sessions
|
|
@@ -433,6 +450,13 @@ function openProjectMenu(projectId, anchorEl) {
|
|
|
433
450
|
startProjectRename(projectId);
|
|
434
451
|
return;
|
|
435
452
|
}
|
|
453
|
+
if (btn.dataset.action === 'start-dormant') {
|
|
454
|
+
const ids = [...document.querySelectorAll(`.project-group[data-project-id="${projectId}"] .project-sessions [data-resumable-id]`)]
|
|
455
|
+
.map(el => el.dataset.resumableId);
|
|
456
|
+
if (!ids.length) return;
|
|
457
|
+
resumeDormantSessions(ids, `"${proj?.name || 'project'}"`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
436
460
|
if (btn.dataset.action === 'clear-dormant') {
|
|
437
461
|
const ids = state.resumable.filter(s => s.projectId === projectId).map(s => s.id);
|
|
438
462
|
if (!ids.length) return;
|
package/public/js/creator.js
CHANGED
|
@@ -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',
|
|
@@ -76,6 +77,8 @@ export function openCreator() {
|
|
|
76
77
|
// Close project creator if open
|
|
77
78
|
document.getElementById('project-creator')?.remove();
|
|
78
79
|
if (!state.presets.length) return;
|
|
80
|
+
// Recheck binary availability on the server
|
|
81
|
+
send({ type: 'checkAvailability' });
|
|
79
82
|
|
|
80
83
|
const fallbackName = randomName();
|
|
81
84
|
const presets = sortedPresets();
|
|
@@ -101,11 +104,18 @@ export function openCreator() {
|
|
|
101
104
|
</button>
|
|
102
105
|
</div>
|
|
103
106
|
<div class="space-y-0.5">
|
|
104
|
-
${presets.map(p =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
${presets.map(p => {
|
|
108
|
+
const hasConfigured = state.cfg.commands.some(c => binName(c.command) === binName(p.command) && c.enabled !== false);
|
|
109
|
+
const missing = p.available === false && !hasConfigured;
|
|
110
|
+
return `
|
|
111
|
+
<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}">
|
|
112
|
+
<span class="${missing ? 'opacity-40' : ''}">${agentIcon(p.icon, 24)}</span>
|
|
113
|
+
<span class="flex-1 min-w-0">
|
|
114
|
+
<span>${esc(p.name)}</span>
|
|
115
|
+
${missing ? `<span class="block text-[10px] text-slate-600 truncate">${esc(p.installCmd || 'Not installed')}</span>` : ''}
|
|
116
|
+
</span>
|
|
117
|
+
</button>`;
|
|
118
|
+
}).join('')}
|
|
109
119
|
</div>`;
|
|
110
120
|
|
|
111
121
|
const list = document.getElementById('session-list');
|
|
@@ -187,6 +197,14 @@ export function openCreator() {
|
|
|
187
197
|
if (!btn) return;
|
|
188
198
|
const preset = state.presets.find(p => p.presetId === btn.dataset.preset);
|
|
189
199
|
if (!preset) return;
|
|
200
|
+
const hasConfigured = state.cfg.commands.some(c => binName(c.command) === binName(preset.command) && c.enabled !== false);
|
|
201
|
+
if (preset.available === false && !hasConfigured && preset.installCmd) {
|
|
202
|
+
navigator.clipboard.writeText(preset.installCmd).then(
|
|
203
|
+
() => showToast(`Install command copied: <code class="text-slate-200">${esc(preset.installCmd)}</code>`, { html: true, duration: 4000 }),
|
|
204
|
+
() => showToast(`Run: ${preset.installCmd}`, { duration: 4000 }),
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
190
208
|
const name = nameInput.value.trim() || fallbackName;
|
|
191
209
|
const cwd = cwdInput.value.trim() || undefined;
|
|
192
210
|
const projectSelect = card.querySelector('#creator-project');
|
|
@@ -196,6 +214,13 @@ export function openCreator() {
|
|
|
196
214
|
});
|
|
197
215
|
}
|
|
198
216
|
|
|
217
|
+
export function refreshCreator() {
|
|
218
|
+
if (document.getElementById('session-creator')) {
|
|
219
|
+
closeCreator();
|
|
220
|
+
openCreator();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
export function closeCreator() {
|
|
200
225
|
document.getElementById('session-creator')?.remove();
|
|
201
226
|
}
|
package/public/js/terminals.js
CHANGED
|
@@ -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
|
|
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
|
|
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-
|
|
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
|
}
|
package/public/js/utils.js
CHANGED
|
@@ -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>`;
|
package/server.js
CHANGED
|
@@ -107,16 +107,16 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
107
107
|
const v = require('./package.json').version;
|
|
108
108
|
const url = `http://localhost:${PORT}`;
|
|
109
109
|
console.log(`
|
|
110
|
-
\x1b[38;5;
|
|
110
|
+
\x1b[38;5;105m ╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸\x1b[0m
|
|
111
111
|
|
|
112
|
-
\x1b[38;5;
|
|
113
|
-
\x1b[38;5;
|
|
114
|
-
\x1b[38;5;
|
|
115
|
-
\x1b[38;5;
|
|
116
|
-
\x1b[38;5;
|
|
117
|
-
\x1b[38;5;
|
|
112
|
+
\x1b[38;5;239m ██████╗\x1b[38;5;242m██╗ \x1b[38;5;245m██╗\x1b[38;5;105m██████╗ \x1b[38;5;141m███████╗\x1b[38;5;147m ██████╗\x1b[38;5;183m██╗ ██╗\x1b[0m
|
|
113
|
+
\x1b[38;5;239m ██╔════╝\x1b[38;5;242m██║ \x1b[38;5;245m██║\x1b[38;5;105m██╔══██╗\x1b[38;5;141m██╔════╝\x1b[38;5;147m██╔════╝\x1b[38;5;183m██║ ██╔╝\x1b[0m
|
|
114
|
+
\x1b[38;5;239m ██║ \x1b[38;5;242m██║ \x1b[38;5;245m██║\x1b[38;5;105m██║ ██║\x1b[38;5;141m█████╗ \x1b[38;5;147m██║ \x1b[38;5;183m█████╔╝ \x1b[0m
|
|
115
|
+
\x1b[38;5;239m ██║ \x1b[38;5;242m██║ \x1b[38;5;245m██║\x1b[38;5;105m██║ ██║\x1b[38;5;141m██╔══╝ \x1b[38;5;147m██║ \x1b[38;5;183m██╔═██╗ \x1b[0m
|
|
116
|
+
\x1b[38;5;239m ╚██████╗\x1b[38;5;242m███████╗\x1b[38;5;245m██║\x1b[38;5;105m██████╔╝\x1b[38;5;141m███████╗\x1b[38;5;147m╚██████╗\x1b[38;5;183m██║ ██╗\x1b[0m
|
|
117
|
+
\x1b[38;5;239m ╚═════╝\x1b[38;5;242m╚══════╝\x1b[38;5;245m╚═╝\x1b[38;5;105m╚═════╝ \x1b[38;5;141m╚══════╝\x1b[38;5;147m ╚═════╝\x1b[38;5;183m╚═╝ ╚═╝\x1b[0m
|
|
118
118
|
|
|
119
|
-
\x1b[38;5;
|
|
119
|
+
\x1b[38;5;105m ╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸\x1b[0m
|
|
120
120
|
|
|
121
121
|
\x1b[38;5;245m v${v}\x1b[0m
|
|
122
122
|
|
package/public/img/codex.png
DELETED
|
Binary file
|
package/public/img/gemini.png
DELETED
|
Binary file
|
package/public/img/opencode.png
DELETED
|
Binary file
|