clideck 1.27.0 → 1.29.0
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 +7 -0
- package/agent-presets.json +7 -9
- package/bin/claude-hook.js +31 -0
- package/bin/gemini-hook.js +31 -0
- package/bin/notify-helper.js +9 -2
- package/codex-config.js +77 -0
- package/config.js +2 -0
- package/handlers.js +164 -56
- package/package.json +1 -2
- package/plugin-loader.js +176 -50
- package/plugins/autopilot/clideck-plugin.json +5 -3
- package/plugins/autopilot/index.js +24 -30
- package/plugins/autopilot/package.json +7 -0
- package/plugins/trim-clip/clideck-plugin.json +1 -0
- package/plugins/voice-input/clideck-plugin.json +1 -0
- package/public/index.html +2 -4
- package/public/js/app.js +62 -5
- package/public/js/creator.js +14 -2
- package/public/js/settings.js +8 -6
- package/public/js/terminals.js +126 -26
- package/public/js/toast.js +2 -17
- package/public/js/utils.js +9 -0
- package/public/tailwind.css +1 -1
- package/server.js +87 -21
- package/sessions.js +41 -8
- package/telemetry-receiver.js +83 -80
- package/tools/merge-jsonl-roles.mjs +182 -0
- package/transcript-builder.js +53 -0
- package/transcript-parser.js +135 -0
- package/transcript.js +115 -181
package/public/js/app.js
CHANGED
|
@@ -14,6 +14,8 @@ import { registerHotkey, unregisterHotkey, unregisterAllForPlugin } from './hotk
|
|
|
14
14
|
import { renderPrompts } from './prompts.js';
|
|
15
15
|
import { renderRoles } from './roles.js';
|
|
16
16
|
|
|
17
|
+
const shownAgentHealthToasts = new Set();
|
|
18
|
+
|
|
17
19
|
function connect() {
|
|
18
20
|
state.ws = new WebSocket(`ws://${location.host}`);
|
|
19
21
|
|
|
@@ -50,6 +52,12 @@ function connect() {
|
|
|
50
52
|
state.presets = msg.presets;
|
|
51
53
|
renderSettings();
|
|
52
54
|
refreshCreator();
|
|
55
|
+
for (const p of state.presets) {
|
|
56
|
+
if (p.available && p.health && !p.health.ok && p.health.reason !== 'Not installed' && !shownAgentHealthToasts.has(p.presetId)) {
|
|
57
|
+
shownAgentHealthToasts.add(p.presetId);
|
|
58
|
+
showToast(`${p.name}: ${p.health.reason}`, { id: `agent-health-${p.presetId}`, type: p.versionOk === false ? 'error' : 'warn', duration: 0, title: 'Agent Attention' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
53
61
|
break;
|
|
54
62
|
case 'sessions.resumable':
|
|
55
63
|
state.resumable = msg.list;
|
|
@@ -83,17 +91,23 @@ function connect() {
|
|
|
83
91
|
case 'session.status':
|
|
84
92
|
setStatus(msg.id, msg.working);
|
|
85
93
|
break;
|
|
86
|
-
// Server requests
|
|
87
|
-
case '
|
|
94
|
+
// Server requests terminal capture (e.g. after PermissionRequest hook)
|
|
95
|
+
case 'terminal.capture': {
|
|
88
96
|
const ce = state.terms.get(msg.id);
|
|
89
97
|
if (ce?.term) {
|
|
90
98
|
const buf = ce.term.buffer.active;
|
|
91
99
|
const lines = [];
|
|
92
100
|
for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); }
|
|
93
|
-
send({ type: 'terminal.buffer', id: msg.id, lines });
|
|
101
|
+
send({ type: 'terminal.buffer', id: msg.id, lines, menuVersion: msg.menuVersion });
|
|
94
102
|
}
|
|
95
103
|
break;
|
|
96
104
|
}
|
|
105
|
+
case 'session.history': {
|
|
106
|
+
const entry = state.terms.get(msg.id);
|
|
107
|
+
if (entry && !entry.queue(msg.text + '\n')) entry.term.write(msg.text + '\n');
|
|
108
|
+
updatePreview(msg.id);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
97
111
|
// Bridge preview text (OpenCode plugin)
|
|
98
112
|
case 'session.preview': {
|
|
99
113
|
const pe = state.terms.get(msg.id);
|
|
@@ -193,7 +207,9 @@ function connect() {
|
|
|
193
207
|
if (!toast) break;
|
|
194
208
|
const actionsEl = toast.querySelector('.setup-actions');
|
|
195
209
|
if (msg.success) {
|
|
196
|
-
const sid = toast.dataset.sessionId
|
|
210
|
+
const sid = (toast.dataset.sessionId && toast.dataset.sessionId !== 'null' && toast.dataset.sessionId !== 'undefined')
|
|
211
|
+
? toast.dataset.sessionId
|
|
212
|
+
: '';
|
|
197
213
|
actionsEl.innerHTML = `
|
|
198
214
|
<div class="flex-1 flex items-center gap-1.5 text-xs text-emerald-400">
|
|
199
215
|
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path d="M5 13l4 4L19 7"/></svg>
|
|
@@ -221,6 +237,19 @@ function connect() {
|
|
|
221
237
|
case 'plugins':
|
|
222
238
|
loadPlugins(msg.list);
|
|
223
239
|
break;
|
|
240
|
+
case 'plugin.install.result': {
|
|
241
|
+
const btn = document.querySelector(`.plugin-install-btn[data-plugin-id="${msg.pluginId}"]`);
|
|
242
|
+
if (!btn) break;
|
|
243
|
+
if (msg.success) {
|
|
244
|
+
btn.textContent = 'Installed';
|
|
245
|
+
btn.className = btn.className.replace('bg-blue-600 hover:bg-blue-500 text-white', 'bg-emerald-600/20 text-emerald-400 cursor-default');
|
|
246
|
+
} else {
|
|
247
|
+
btn.textContent = 'Failed';
|
|
248
|
+
btn.className = btn.className.replace('bg-blue-600 hover:bg-blue-500', 'bg-red-600/20 text-red-400 cursor-default');
|
|
249
|
+
btn.disabled = false;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
224
253
|
case 'pills':
|
|
225
254
|
state.pills.clear();
|
|
226
255
|
for (const p of msg.list) addPill(p);
|
|
@@ -420,7 +449,7 @@ function showTelemetrySetup(commandId, sessionId) {
|
|
|
420
449
|
|
|
421
450
|
const toast = document.createElement('div');
|
|
422
451
|
toast.dataset.setupPreset = preset.presetId;
|
|
423
|
-
toast.dataset.sessionId = sessionId;
|
|
452
|
+
if (sessionId) toast.dataset.sessionId = sessionId;
|
|
424
453
|
toast.dataset.commandId = commandId;
|
|
425
454
|
toast.className = 'fixed bottom-5 right-5 z-[500] w-[360px] bg-slate-800/95 backdrop-blur-sm border border-slate-700/60 rounded-xl shadow-2xl shadow-black/60';
|
|
426
455
|
toast.style.opacity = '0';
|
|
@@ -746,15 +775,34 @@ function renderPluginsPanel(list) {
|
|
|
746
775
|
}
|
|
747
776
|
const expanded = getPluginExpanded();
|
|
748
777
|
const trashSvg = `<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>`;
|
|
778
|
+
const defaultIcon = `<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v2m6.36 1.64l-1.42 1.42M21 12h-2M17.94 17.94l-1.42-1.42M12 19v2M6.06 17.94l1.42-1.42M3 12h2M6.06 6.06l1.42 1.42"/><circle cx="12" cy="12" r="4"/></svg>`;
|
|
749
779
|
|
|
750
780
|
container.innerHTML = list.map((p, i) => {
|
|
751
781
|
const open = !!expanded[p.id];
|
|
782
|
+
const icon = p.icon || defaultIcon;
|
|
752
783
|
const deleteBtn = p.bundled ? '' : `<div class="plugin-delete flex items-center justify-center w-6 h-6 rounded text-slate-600 hover:text-red-400 hover:bg-slate-700/50 cursor-pointer transition-colors flex-shrink-0" data-plugin-id="${esc(p.id)}" data-plugin-name="${esc(p.name)}" title="Remove plugin">${trashSvg}</div>`;
|
|
753
784
|
const hasFooter = p.author || !p.bundled;
|
|
785
|
+
|
|
786
|
+
if (!p.installed) {
|
|
787
|
+
return `
|
|
788
|
+
<div class="plugin-card ${i > 0 ? 'border-t border-slate-700/50' : ''}">
|
|
789
|
+
<div class="px-4 py-3">
|
|
790
|
+
<div class="flex items-center gap-2">
|
|
791
|
+
<span class="text-slate-500 flex-shrink-0">${icon}</span>
|
|
792
|
+
<span class="flex-1 text-sm font-medium text-slate-400 truncate">${esc(p.name)}</span>
|
|
793
|
+
<span class="text-[10px] text-slate-600 flex-shrink-0">v${esc(p.version)}</span>
|
|
794
|
+
<button class="plugin-install-btn px-2.5 py-1 text-[11px] font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-md transition-colors flex-shrink-0" data-plugin-id="${esc(p.id)}">Install</button>
|
|
795
|
+
</div>
|
|
796
|
+
${p.description ? `<p class="text-[11px] text-slate-600 mt-0.5 leading-snug">${esc(p.description)}</p>` : ''}
|
|
797
|
+
</div>
|
|
798
|
+
</div>`;
|
|
799
|
+
}
|
|
800
|
+
|
|
754
801
|
return `
|
|
755
802
|
<div class="plugin-card ${i > 0 ? 'border-t border-slate-700/50' : ''}">
|
|
756
803
|
<div class="plugin-toggle px-4 py-3 hover:bg-slate-800/50 transition-colors cursor-pointer" data-plugin-id="${esc(p.id)}">
|
|
757
804
|
<div class="flex items-center gap-2">
|
|
805
|
+
<span class="text-slate-400 flex-shrink-0">${icon}</span>
|
|
758
806
|
<span class="flex-1 text-sm font-medium text-slate-200 truncate">${esc(p.name)}</span>
|
|
759
807
|
<span class="text-[10px] text-slate-500 flex-shrink-0">v${esc(p.version)}</span>
|
|
760
808
|
<svg class="plugin-chevron w-4 h-4 text-slate-500 transition-transform duration-200 flex-shrink-0 ${open ? '' : 'collapsed'}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M19 9l-7 7-7-7"/></svg>
|
|
@@ -793,6 +841,15 @@ function renderPluginsPanel(list) {
|
|
|
793
841
|
});
|
|
794
842
|
});
|
|
795
843
|
|
|
844
|
+
container.querySelectorAll('.plugin-install-btn').forEach(el => {
|
|
845
|
+
el.addEventListener('click', () => {
|
|
846
|
+
el.disabled = true;
|
|
847
|
+
el.textContent = 'Installing...';
|
|
848
|
+
el.className = el.className.replace('bg-blue-600 hover:bg-blue-500', 'bg-slate-700 cursor-wait');
|
|
849
|
+
send({ type: 'plugin.install', pluginId: el.dataset.pluginId });
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
|
|
796
853
|
container.querySelectorAll('[data-setting]').forEach(el => {
|
|
797
854
|
const pluginId = el.dataset.plugin;
|
|
798
855
|
const key = el.dataset.setting;
|
package/public/js/creator.js
CHANGED
|
@@ -35,11 +35,15 @@ function isPresetMissing(p) {
|
|
|
35
35
|
return cmd.command === p.command;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function isPresetOutdated(p) {
|
|
39
|
+
return p.available !== false && p.versionOk === false;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
// True if preset binary exists but telemetry/hooks are not configured yet
|
|
39
43
|
function isPresetUnpatched(p) {
|
|
40
|
-
if (p.available === false || !p.telemetryAutoSetup) return false;
|
|
44
|
+
if (p.available === false || p.versionOk === false || !p.telemetryAutoSetup) return false;
|
|
41
45
|
const cmd = findCommandForPreset(p);
|
|
42
|
-
return !cmd || !cmd.
|
|
46
|
+
return !cmd || !cmd.telemetryStatus?.ok;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
function renderPresetButtons() {
|
|
@@ -52,6 +56,14 @@ function renderPresetButtons() {
|
|
|
52
56
|
<button class="install-btn px-2.5 py-1 text-[11px] font-medium text-blue-400 hover:text-blue-300 bg-blue-500/10 hover:bg-blue-500/20 rounded-md transition-colors" data-preset="${p.presetId}">Add</button>
|
|
53
57
|
</div>`;
|
|
54
58
|
}
|
|
59
|
+
if (isPresetOutdated(p)) {
|
|
60
|
+
return `
|
|
61
|
+
<div class="w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-left text-slate-500">
|
|
62
|
+
<span class="opacity-40">${agentIcon(p.icon, 24)}</span>
|
|
63
|
+
<span class="flex-1 min-w-0">${esc(p.name)}</span>
|
|
64
|
+
<button class="install-btn px-2.5 py-1 text-[11px] font-medium text-rose-400 hover:text-rose-300 bg-rose-500/10 hover:bg-rose-500/20 rounded-md transition-colors" data-preset="${p.presetId}">Update</button>
|
|
65
|
+
</div>`;
|
|
66
|
+
}
|
|
55
67
|
if (isPresetUnpatched(p)) {
|
|
56
68
|
return `
|
|
57
69
|
<div class="w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-left text-slate-500">
|
package/public/js/settings.js
CHANGED
|
@@ -111,10 +111,12 @@ function integrationSection(c) {
|
|
|
111
111
|
const preset = telemetryPreset(c);
|
|
112
112
|
if (!preset) return '';
|
|
113
113
|
if (!preset.telemetryAutoSetup && !preset.bridge) return '';
|
|
114
|
-
const configured = !!c.
|
|
115
|
-
const detail =
|
|
116
|
-
? `<span class="text-
|
|
117
|
-
:
|
|
114
|
+
const configured = !!c.telemetryStatus?.ok;
|
|
115
|
+
const detail = preset.versionOk === false
|
|
116
|
+
? `<span class="text-rose-400/80">Update required</span> — need ${esc(preset.minVersion)}+ (found ${esc(preset.version || 'unknown')})`
|
|
117
|
+
: configured
|
|
118
|
+
? `<span class="text-emerald-400/80">Configured</span> — ${esc(preset.telemetryConfigPath || '')}`
|
|
119
|
+
: `<span class="text-amber-400/80">${esc(c.telemetryStatus?.error || 'Needs setup')}</span> — ${esc(preset.telemetryConfigPath || '')}`;
|
|
118
120
|
return `
|
|
119
121
|
<div class="mt-3 pt-3 border-t border-slate-700/50">
|
|
120
122
|
<div class="text-[11px] text-slate-500">${detail}</div>
|
|
@@ -383,7 +385,7 @@ function renderThemeSection() {
|
|
|
383
385
|
function renderNotifications() {
|
|
384
386
|
const enabled = !!state.cfg.notifyIdle;
|
|
385
387
|
document.getElementById('cfg-notify-idle').checked = enabled;
|
|
386
|
-
document.getElementById('cfg-notify-min-work').value = state.cfg.notifyMinWork
|
|
388
|
+
document.getElementById('cfg-notify-min-work').value = state.cfg.notifyMinWork ?? 0;
|
|
387
389
|
|
|
388
390
|
const permStatus = document.getElementById('notify-permission-status');
|
|
389
391
|
if (enabled && 'Notification' in window) {
|
|
@@ -461,7 +463,7 @@ function saveConfig() {
|
|
|
461
463
|
state.cfg.defaultPath = document.getElementById('cfg-default-path').value.trim();
|
|
462
464
|
state.cfg.confirmClose = document.getElementById('cfg-confirm-close').checked;
|
|
463
465
|
state.cfg.notifyIdle = document.getElementById('cfg-notify-idle').checked;
|
|
464
|
-
state.cfg.notifyMinWork = parseInt(document.getElementById('cfg-notify-min-work').value, 10) ||
|
|
466
|
+
state.cfg.notifyMinWork = parseInt(document.getElementById('cfg-notify-min-work').value, 10) || 0;
|
|
465
467
|
state.cfg.notifySoundEnabled = document.getElementById('cfg-notify-sound').checked;
|
|
466
468
|
state.cfg.notifySound = document.getElementById('cfg-notify-sound-pick').value;
|
|
467
469
|
// Preserve fields not managed by this form
|
package/public/js/terminals.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { state, send } from './state.js';
|
|
2
|
-
import { esc, resolveIconPath } from './utils.js';
|
|
2
|
+
import { esc, miniMarkdown, resolveIconPath } from './utils.js';
|
|
3
3
|
import { resolveTheme, resolveAccent, applyTheme } from './profiles.js';
|
|
4
4
|
import { attachToTerminal, registerHotkey } from './hotkeys.js';
|
|
5
5
|
import { closeDropdown } from './prompts.js';
|
|
@@ -338,25 +338,56 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
|
|
|
338
338
|
term.loadAddon(fit);
|
|
339
339
|
term.onData(data => send({ type: 'input', id, data }));
|
|
340
340
|
|
|
341
|
-
// [
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
let _screenTimer = null, _renderSilent = false;
|
|
345
|
-
function _tryScreenCapture() {
|
|
341
|
+
// [TRANSCRIPT-CAPTURE] initial settled capture plus one delayed idle save
|
|
342
|
+
let _captureTimer = null, _renderSilent = false, _lastTyping = 0, _initialCaptureDone = false, _idleSaveTimer = null;
|
|
343
|
+
function _sendCapture() {
|
|
346
344
|
const entry = state.terms.get(id);
|
|
347
|
-
if (!entry?.
|
|
348
|
-
entry.pendingScreenCapture = false;
|
|
345
|
+
if (!entry?.term) return;
|
|
349
346
|
const buf = entry.term.buffer.active;
|
|
350
347
|
const lines = [];
|
|
351
348
|
for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); }
|
|
352
349
|
send({ type: 'terminal.buffer', id, lines });
|
|
353
350
|
}
|
|
354
|
-
|
|
355
|
-
|
|
351
|
+
function _isChrome(t) {
|
|
352
|
+
return !t
|
|
353
|
+
|| /^[─━═\u2500-\u257f]+$/.test(t)
|
|
354
|
+
|| /^[▀▄█▌▐░▒▓╭╮╰╯│╔╗╚╝║]+$/.test(t)
|
|
355
|
+
|| (/[█▀▄▌▐░▒▓]/.test(t) && /^[█▀▄▌▐░▒▓\s]+$/.test(t))
|
|
356
|
+
|| /^[❯>$%#]\s*$/.test(t)
|
|
357
|
+
|| /^(esc to interrupt|\? for shortcuts)$/i.test(t);
|
|
358
|
+
}
|
|
359
|
+
function _hasContent() {
|
|
360
|
+
const entry = state.terms.get(id);
|
|
361
|
+
if (!entry?.term) return false;
|
|
362
|
+
const buf = entry.term.buffer.active;
|
|
363
|
+
for (let i = 0; i < buf.length; i++) {
|
|
364
|
+
const text = buf.getLine(i)?.translateToString(true).trim();
|
|
365
|
+
if (!_isChrome(text)) return true;
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
function _tryCapture() {
|
|
370
|
+
const entry = state.terms.get(id);
|
|
371
|
+
if (!_renderSilent || Date.now() - _lastTyping < 2000) return;
|
|
372
|
+
// Initial capture: first time render settles with real content, capture regardless of working/idle
|
|
373
|
+
if (!_initialCaptureDone) {
|
|
374
|
+
if (!_hasContent()) return; // retry on next silence
|
|
375
|
+
_initialCaptureDone = true;
|
|
376
|
+
_sendCapture();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
term.onData(() => {
|
|
381
|
+
_lastTyping = Date.now();
|
|
382
|
+
// User typing invalidates pending capture — will re-try after silence
|
|
383
|
+
_renderSilent = false;
|
|
384
|
+
clearTimeout(_captureTimer);
|
|
385
|
+
_captureTimer = setTimeout(() => { _renderSilent = true; _tryCapture(); }, 2000);
|
|
386
|
+
});
|
|
356
387
|
term.onRender(() => {
|
|
357
388
|
_renderSilent = false;
|
|
358
|
-
clearTimeout(
|
|
359
|
-
|
|
389
|
+
clearTimeout(_captureTimer);
|
|
390
|
+
_captureTimer = setTimeout(() => { _renderSilent = true; _tryCapture(); }, 2000);
|
|
360
391
|
});
|
|
361
392
|
term.onWriteParsed(() => {
|
|
362
393
|
if (Date.now() - _lastTyping < 500) return;
|
|
@@ -364,8 +395,23 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
|
|
|
364
395
|
if (entry) entry.lastRenderAt = Date.now();
|
|
365
396
|
});
|
|
366
397
|
|
|
367
|
-
// Expose capture function so setStatus can
|
|
368
|
-
setTimeout(() => {
|
|
398
|
+
// Expose capture function so setStatus can schedule a retry
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
const e = state.terms.get(id);
|
|
401
|
+
if (e) {
|
|
402
|
+
e.tryCapture = _tryCapture;
|
|
403
|
+
e.sendCaptureNow = _sendCapture;
|
|
404
|
+
e.scheduleIdleCapture = () => {
|
|
405
|
+
clearTimeout(_idleSaveTimer);
|
|
406
|
+
_idleSaveTimer = setTimeout(() => {
|
|
407
|
+
const entry = state.terms.get(id);
|
|
408
|
+
if (!entry || entry.working) return;
|
|
409
|
+
_sendCapture();
|
|
410
|
+
}, 300);
|
|
411
|
+
};
|
|
412
|
+
e.cancelIdleCapture = () => clearTimeout(_idleSaveTimer);
|
|
413
|
+
}
|
|
414
|
+
}, 0);
|
|
369
415
|
|
|
370
416
|
term.open(el);
|
|
371
417
|
attachToTerminal(term);
|
|
@@ -394,8 +440,21 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
|
|
|
394
440
|
fitRaf = requestAnimationFrame(() => { fitRaf = 0; doFit(); });
|
|
395
441
|
});
|
|
396
442
|
ro.observe(el);
|
|
397
|
-
// Safety: if RO hasn't fired within 500ms, flush anyway to avoid unbounded queue
|
|
398
|
-
|
|
443
|
+
// Safety: if RO hasn't fired within 500ms, flush anyway to avoid unbounded queue.
|
|
444
|
+
// If the element is hidden (background tab), force a reasonable default size so the PTY
|
|
445
|
+
// doesn't stay at a tiny default and produce garbled output.
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
if (!fitted) {
|
|
448
|
+
fitted = true;
|
|
449
|
+
if (!el.offsetWidth) {
|
|
450
|
+
term.resize(120, 30);
|
|
451
|
+
send({ type: 'resize', id, cols: 120, rows: 30 });
|
|
452
|
+
}
|
|
453
|
+
for (const chunk of pending) term.write(chunk);
|
|
454
|
+
pending = null;
|
|
455
|
+
updatePreview(id);
|
|
456
|
+
}
|
|
457
|
+
}, 500);
|
|
399
458
|
const cancelFitRaf = () => { if (fitRaf) { cancelAnimationFrame(fitRaf); fitRaf = 0; } };
|
|
400
459
|
state.terms.set(id, { term, fit, el, ro, cancelFitRaf, themeId, commandId, projectId: projectId || null, muted: !!muted, working: false, workStartedAt: null, stopBounce, queue: (data) => { if (!fitted) { pending.push(data); return true; } return false; }, lastActivityAt: Date.now(), unread: false, lastPreviewText: lastPreview || '', searchText: '' });
|
|
401
460
|
document.getElementById('empty').style.display = 'none';
|
|
@@ -526,11 +585,12 @@ function setStatus(id, working) {
|
|
|
526
585
|
|
|
527
586
|
// Notify on working → idle transition
|
|
528
587
|
if (wasWorking && !working && !entry.muted) {
|
|
588
|
+
const minWork = state.cfg.notifyMinWork ?? 0;
|
|
529
589
|
const workDuration = (Date.now() - (entry.workStartedAt || 0)) / 1000;
|
|
530
|
-
const minWork = state.cfg.notifyMinWork || 10;
|
|
531
590
|
if (workDuration >= minWork) {
|
|
532
|
-
|
|
533
|
-
|
|
591
|
+
entry.workStartedAt = null;
|
|
592
|
+
// Sound: all sessions when tab unfocused, all except active when focused
|
|
593
|
+
if (state.cfg.notifySoundEnabled !== false && (!document.hasFocus() || state.active !== id)) {
|
|
534
594
|
new Audio(`/fx/${(state.cfg.notifySound || 'default-beep')}.mp3`).play().catch(() => {});
|
|
535
595
|
}
|
|
536
596
|
// Browser notification: plays when the CliDeck tab is not focused
|
|
@@ -545,11 +605,15 @@ function setStatus(id, working) {
|
|
|
545
605
|
}
|
|
546
606
|
}
|
|
547
607
|
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
608
|
+
// Save once shortly after idle unless the agent resumes first.
|
|
609
|
+
if (wasWorking && !working) {
|
|
610
|
+
entry.scheduleIdleCapture?.();
|
|
611
|
+
}
|
|
551
612
|
|
|
552
|
-
if (working)
|
|
613
|
+
if (working) {
|
|
614
|
+
entry.cancelIdleCapture?.();
|
|
615
|
+
if (!entry.workStartedAt) entry.workStartedAt = Date.now();
|
|
616
|
+
}
|
|
553
617
|
|
|
554
618
|
const el = document.querySelector(`.group[data-id="${id}"] .session-status`);
|
|
555
619
|
if (!el) return;
|
|
@@ -1111,7 +1175,7 @@ function openPillLog(id) {
|
|
|
1111
1175
|
<span class="flex-1"></span>
|
|
1112
1176
|
<button class="pill-log-clear text-[11px] text-slate-600 hover:text-slate-400 transition-colors">Clear</button>
|
|
1113
1177
|
</div>
|
|
1114
|
-
<div class="pill-log-body flex-1 overflow-y-auto p-4
|
|
1178
|
+
<div class="pill-log-body flex-1 overflow-y-auto p-4 text-xs leading-relaxed tmx-scroll"></div>
|
|
1115
1179
|
</div>`;
|
|
1116
1180
|
document.getElementById('terminals').appendChild(panel);
|
|
1117
1181
|
panel.querySelector('.pill-log-clear').addEventListener('click', () => {
|
|
@@ -1146,9 +1210,45 @@ function appendLogLine(entry) {
|
|
|
1146
1210
|
const body = document.querySelector('#pill-log-panel .pill-log-body');
|
|
1147
1211
|
if (!body) return;
|
|
1148
1212
|
const line = document.createElement('div');
|
|
1149
|
-
line.className = 'flex gap-3 py-0.5';
|
|
1150
1213
|
const time = new Date(entry.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
1151
|
-
|
|
1214
|
+
const t = entry.text;
|
|
1215
|
+
|
|
1216
|
+
// Categorize log entries for visual treatment
|
|
1217
|
+
let color = 'text-slate-400';
|
|
1218
|
+
let icon = '';
|
|
1219
|
+
let content = esc(t);
|
|
1220
|
+
if (/^Started with/.test(t)) {
|
|
1221
|
+
color = 'text-emerald-400';
|
|
1222
|
+
icon = '<span class="text-emerald-500">▶</span>';
|
|
1223
|
+
} else if (/^Routed /.test(t)) {
|
|
1224
|
+
color = 'text-indigo-400';
|
|
1225
|
+
icon = '<span class="text-indigo-500">→</span>';
|
|
1226
|
+
} else if (/^Notify:/.test(t)) {
|
|
1227
|
+
color = 'text-amber-300';
|
|
1228
|
+
icon = '<span class="text-amber-500">●</span>';
|
|
1229
|
+
content = '<strong class="text-amber-300">Notify:</strong> ' + miniMarkdown(t.replace(/^Notify:\s*/, ''));
|
|
1230
|
+
} else if (/^Consulting /.test(t)) {
|
|
1231
|
+
color = 'text-slate-500';
|
|
1232
|
+
icon = '<span class="text-slate-600">…</span>';
|
|
1233
|
+
} else if (/→ working$/.test(t)) {
|
|
1234
|
+
color = 'text-blue-400';
|
|
1235
|
+
icon = '<span class="text-blue-500">●</span>';
|
|
1236
|
+
} else if (/→ idle$/.test(t)) {
|
|
1237
|
+
color = 'text-slate-500';
|
|
1238
|
+
icon = '<span class="text-slate-600">○</span>';
|
|
1239
|
+
} else if (/^Completed$/.test(t)) {
|
|
1240
|
+
color = 'text-emerald-400';
|
|
1241
|
+
icon = '<span class="text-emerald-500">✓</span>';
|
|
1242
|
+
} else if (/^Stopped$/.test(t)) {
|
|
1243
|
+
color = 'text-slate-500';
|
|
1244
|
+
icon = '<span class="text-slate-600">■</span>';
|
|
1245
|
+
} else if (/^Paused/.test(t)) {
|
|
1246
|
+
color = 'text-amber-400';
|
|
1247
|
+
icon = '<span class="text-amber-500">▮▮</span>';
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
line.className = 'flex gap-3 py-1 items-start';
|
|
1251
|
+
line.innerHTML = `<span class="text-slate-600 flex-shrink-0 tabular-nums">${time}</span><span class="w-4 flex-shrink-0 text-center">${icon}</span><span class="${color} leading-relaxed">${content}</span>`;
|
|
1152
1252
|
body.appendChild(line);
|
|
1153
1253
|
body.scrollTop = body.scrollHeight;
|
|
1154
1254
|
}
|
package/public/js/toast.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { esc, miniMarkdown } from './utils.js';
|
|
2
|
+
|
|
1
3
|
const ICONS = {
|
|
2
4
|
info: '<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"/>',
|
|
3
5
|
success: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
|
@@ -19,18 +21,6 @@ function getContainer() {
|
|
|
19
21
|
return c;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
* Minimal markdown: **bold**, `code`, - bullet lists, line breaks.
|
|
24
|
-
*/
|
|
25
|
-
function miniMarkdown(text) {
|
|
26
|
-
return esc(text)
|
|
27
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-slate-200 font-semibold">$1</strong>')
|
|
28
|
-
.replace(/`(.+?)`/g, '<code class="px-1 py-0.5 rounded bg-slate-700/60 text-slate-300 text-[11px]">$1</code>')
|
|
29
|
-
.replace(/^[-•]\s+(.+)$/gm, '<li class="ml-3">$1</li>')
|
|
30
|
-
.replace(/(<li.*<\/li>\n?)+/g, '<ul class="list-disc pl-2 space-y-0.5">$&</ul>')
|
|
31
|
-
.replace(/\n/g, '<br>');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
24
|
/**
|
|
35
25
|
* @param {string} message — plain text, markdown (if markdown option), or raw HTML (if html option)
|
|
36
26
|
* @param {{ type?: string, duration?: number, id?: string, html?: boolean, markdown?: boolean, title?: string }} opts
|
|
@@ -74,8 +64,3 @@ export function showToast(message, opts = {}) {
|
|
|
74
64
|
return { dismiss };
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
function esc(s) {
|
|
78
|
-
const d = document.createElement('div');
|
|
79
|
-
d.textContent = s;
|
|
80
|
-
return d.innerHTML;
|
|
81
|
-
}
|
package/public/js/utils.js
CHANGED
|
@@ -8,6 +8,15 @@ export function esc(s) {
|
|
|
8
8
|
return s.replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function miniMarkdown(text) {
|
|
12
|
+
return esc(text)
|
|
13
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-slate-200 font-semibold">$1</strong>')
|
|
14
|
+
.replace(/`(.+?)`/g, '<code class="px-1 py-0.5 rounded bg-slate-700/60 text-slate-300 text-[11px]">$1</code>')
|
|
15
|
+
.replace(/^[-•]\s+(.+)$/gm, '<li class="ml-3">$1</li>')
|
|
16
|
+
.replace(/(<li.*<\/li>\n?)+/g, '<ul class="list-disc pl-2 space-y-0.5">$&</ul>')
|
|
17
|
+
.replace(/\n/g, '<br>');
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export function debounce(fn, ms) {
|
|
12
21
|
let t;
|
|
13
22
|
return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
|