clideck 1.27.1 → 1.29.1
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 +184 -58
- package/package.json +1 -1
- package/plugin-loader.js +36 -14
- package/plugins/autopilot/clideck-plugin.json +3 -3
- package/plugins/autopilot/index.js +24 -30
- package/public/index.html +2 -2
- package/public/js/app.js +21 -5
- package/public/js/creator.js +14 -2
- package/public/js/settings.js +8 -6
- package/public/js/terminals.js +123 -24
- package/public/js/toast.js +2 -17
- package/public/js/utils.js +9 -0
- package/public/tailwind.css +1 -1
- package/server.js +87 -23
- package/sessions.js +69 -19
- 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/package.json
CHANGED
package/plugin-loader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } = require('fs');
|
|
2
|
+
const { createHash } = require('crypto');
|
|
2
3
|
const { join, sep } = require('path');
|
|
3
4
|
const { execFile: _execFile } = require('child_process');
|
|
4
5
|
// Windows needs shell:true for npm (it's npm.cmd, not a binary)
|
|
@@ -11,7 +12,7 @@ mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
|
11
12
|
|
|
12
13
|
// Seed bundled plugins — copy if missing, update if bundled version is newer
|
|
13
14
|
const BUNDLED_DIR = join(__dirname, 'plugins');
|
|
14
|
-
const
|
|
15
|
+
const depsChanged = new Set(); // plugins whose install inputs changed — need reinstall
|
|
15
16
|
if (existsSync(BUNDLED_DIR)) {
|
|
16
17
|
for (const entry of readdirSync(BUNDLED_DIR, { withFileTypes: true })) {
|
|
17
18
|
if (!entry.isDirectory()) continue;
|
|
@@ -25,8 +26,20 @@ if (existsSync(BUNDLED_DIR)) {
|
|
|
25
26
|
const installedManifestFile = existsSync(join(target, 'clideck-plugin.json')) ? join(target, 'clideck-plugin.json') : join(target, 'termix-plugin.json');
|
|
26
27
|
const installedManifest = JSON.parse(readFileSync(installedManifestFile, 'utf8'));
|
|
27
28
|
if (bundledManifest.version !== installedManifest.version) {
|
|
29
|
+
// Check if install inputs changed before copying
|
|
30
|
+
let needsReinstall = false;
|
|
31
|
+
if (bundledManifest.install) {
|
|
32
|
+
const installHash = (dir) => {
|
|
33
|
+
const h = createHash('sha256');
|
|
34
|
+
for (const f of ['package.json', 'package-lock.json']) {
|
|
35
|
+
try { h.update(readFileSync(join(dir, f))); } catch {}
|
|
36
|
+
}
|
|
37
|
+
return h.digest('hex');
|
|
38
|
+
};
|
|
39
|
+
needsReinstall = installHash(target) !== installHash(join(BUNDLED_DIR, entry.name));
|
|
40
|
+
}
|
|
28
41
|
cpSync(join(BUNDLED_DIR, entry.name), target, { recursive: true });
|
|
29
|
-
if (
|
|
42
|
+
if (needsReinstall) depsChanged.add(bundledManifest.id || entry.name);
|
|
30
43
|
console.log(`[plugin] updated ${entry.name} ${installedManifest.version} → ${bundledManifest.version}`);
|
|
31
44
|
}
|
|
32
45
|
} catch {}
|
|
@@ -41,6 +54,7 @@ const outputHooks = [];
|
|
|
41
54
|
const statusHooks = [];
|
|
42
55
|
const transcriptHooks = [];
|
|
43
56
|
const menuHooks = [];
|
|
57
|
+
const configHooks = [];
|
|
44
58
|
const sessionStatus = new Map(); // sessionId → boolean (dedup multi-client reports)
|
|
45
59
|
const autoApproveMenus = new Set(); // sessionIds where menus should be auto-approved
|
|
46
60
|
const frontendHandlers = new Map();
|
|
@@ -55,7 +69,7 @@ const settingsChangeHandlers = new Map(); // pluginId → [fn]
|
|
|
55
69
|
const sessionPills = new Map(); // pillId → { pluginId, id, title, projectId, working, statusText, icon, logs[] }
|
|
56
70
|
|
|
57
71
|
function removeHooks(pluginId) {
|
|
58
|
-
for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks, menuHooks]) {
|
|
72
|
+
for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks, menuHooks, configHooks]) {
|
|
59
73
|
for (let i = arr.length - 1; i >= 0; i--) {
|
|
60
74
|
if (arr[i].pluginId === pluginId) arr.splice(i, 1);
|
|
61
75
|
}
|
|
@@ -132,14 +146,14 @@ function init(broadcast, getSessions, getConfig, saveConfig, sessionInput, creat
|
|
|
132
146
|
createSessionFn = createProgrammatic;
|
|
133
147
|
closeSessionFn = closeSession;
|
|
134
148
|
|
|
135
|
-
// Clear install state for bundled plugins
|
|
136
|
-
if (
|
|
149
|
+
// Clear install state only for bundled plugins whose dependencies changed
|
|
150
|
+
if (depsChanged.size) {
|
|
137
151
|
const cfg = getConfig();
|
|
138
152
|
if (cfg?.pluginInstalled) {
|
|
139
|
-
for (const id of
|
|
153
|
+
for (const id of depsChanged) {
|
|
140
154
|
if (cfg.pluginInstalled[id]) {
|
|
141
155
|
delete cfg.pluginInstalled[id];
|
|
142
|
-
console.log(`[plugin] cleared install state for
|
|
156
|
+
console.log(`[plugin] install inputs changed, cleared install state for ${id}`);
|
|
143
157
|
}
|
|
144
158
|
}
|
|
145
159
|
saveConfig(cfg);
|
|
@@ -180,6 +194,7 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
180
194
|
onStatusChange(fn) { statusHooks.push({ pluginId, fn }); },
|
|
181
195
|
onTranscriptEntry(fn) { transcriptHooks.push({ pluginId, fn }); },
|
|
182
196
|
onMenuDetected(fn) { menuHooks.push({ pluginId, fn }); },
|
|
197
|
+
onConfigChange(fn) { configHooks.push({ pluginId, fn }); },
|
|
183
198
|
|
|
184
199
|
sendToFrontend(event, data = {}) {
|
|
185
200
|
broadcastFn?.({ ...data, type: `plugin.${pluginId}.${event}` });
|
|
@@ -191,13 +206,14 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
191
206
|
getSession(id) {
|
|
192
207
|
const s = sessionsFn?.()?.get(id);
|
|
193
208
|
if (!s) return null;
|
|
194
|
-
|
|
209
|
+
const state = sessionStatus.get(id) || '';
|
|
210
|
+
return { id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId, roleName: s.roleName || null, working: state.startsWith('1:') };
|
|
195
211
|
},
|
|
196
212
|
getSessions() {
|
|
197
213
|
const sessions = sessionsFn?.();
|
|
198
214
|
if (!sessions) return [];
|
|
199
215
|
return [...sessions].map(([id, s]) => ({
|
|
200
|
-
id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId, roleName: s.roleName || null, working:
|
|
216
|
+
id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId, roleName: s.roleName || null, working: (sessionStatus.get(id) || '').startsWith('1:'),
|
|
201
217
|
}));
|
|
202
218
|
},
|
|
203
219
|
|
|
@@ -219,8 +235,6 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
219
235
|
getRoles() { return JSON.parse(JSON.stringify(getConfigFn?.()?.roles || [])); },
|
|
220
236
|
getProjects() { return JSON.parse(JSON.stringify(getConfigFn?.()?.projects || [])); },
|
|
221
237
|
getTranscript(id, n) { return transcript.getLastTurns(id, n || 20); },
|
|
222
|
-
getScreenTurns(id, agent, opts) { return transcript.getScreenTurns(id, agent, opts); },
|
|
223
|
-
getScreen(id) { return transcript.getScreen(id); },
|
|
224
238
|
detectMenu(lines, presetId) { return transcript.detectMenu(lines, presetId); },
|
|
225
239
|
|
|
226
240
|
addToolbarAction(opts) { state.actions.push({ ...opts, pluginId, slot: 'toolbar' }); },
|
|
@@ -321,8 +335,9 @@ function notifyOutput(id, data) {
|
|
|
321
335
|
}
|
|
322
336
|
|
|
323
337
|
function notifyStatus(id, working, source) {
|
|
324
|
-
|
|
325
|
-
sessionStatus.
|
|
338
|
+
const next = `${working ? 1 : 0}:${source || ''}`;
|
|
339
|
+
if (sessionStatus.get(id) === next) return;
|
|
340
|
+
sessionStatus.set(id, next);
|
|
326
341
|
for (const h of statusHooks) {
|
|
327
342
|
try { h.fn(id, working, source); }
|
|
328
343
|
catch (e) { console.error(`[plugin:${h.pluginId}] status error: ${e.message}`); }
|
|
@@ -343,6 +358,13 @@ function notifyMenu(id, choices) {
|
|
|
343
358
|
}
|
|
344
359
|
}
|
|
345
360
|
|
|
361
|
+
function notifyConfig(cfg) {
|
|
362
|
+
for (const h of configHooks) {
|
|
363
|
+
try { h.fn(cfg); }
|
|
364
|
+
catch (e) { console.error(`[plugin:${h.pluginId}] config error: ${e.message}`); }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
346
368
|
|
|
347
369
|
function updateSetting(pluginId, key, value) {
|
|
348
370
|
// Validate plugin exists (also prevents __proto__ pollution — Map lookup returns undefined)
|
|
@@ -539,7 +561,7 @@ function removePlugin(pluginId) {
|
|
|
539
561
|
module.exports = {
|
|
540
562
|
PLUGINS_DIR, BUNDLED_IDS,
|
|
541
563
|
init, shutdown,
|
|
542
|
-
transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, clearStatus, isWorking, shouldAutoApproveMenu,
|
|
564
|
+
transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, notifyConfig, clearStatus, isWorking, shouldAutoApproveMenu,
|
|
543
565
|
handleMessage, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
|
|
544
566
|
getPills, getPillLogs,
|
|
545
567
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "autopilot",
|
|
3
3
|
"name": "Autopilot",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"author": "CliDeck",
|
|
6
|
-
"description": "Multi-agent orchestration — routes output between agents automatically",
|
|
6
|
+
"description": "Multi-agent orchestration — routes output between agents automatically. Uses ~50 output tokens per routing decision.",
|
|
7
7
|
"icon": "<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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/></svg>",
|
|
8
8
|
"install": "npm",
|
|
9
9
|
"settings": [
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"key": "model",
|
|
34
34
|
"label": "Model",
|
|
35
35
|
"type": "dynamic-select",
|
|
36
|
-
"default": "claude-
|
|
36
|
+
"default": "claude-opus-4-6"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"key": "apiKey",
|
|
@@ -75,6 +75,12 @@ function addTokens(pid, usage) {
|
|
|
75
75
|
api.sendToFrontend('tokens', { projectId: pid, ...u });
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function latestAgentOutput(id) {
|
|
79
|
+
const turns = api.getTranscript(id, 20);
|
|
80
|
+
const last = [...turns].reverse().find(t => t.role === 'agent');
|
|
81
|
+
return last?.text?.trim().slice(0, 8000) || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
// --- Consumed state (persisted per-project: role → boolean) ---
|
|
79
85
|
|
|
80
86
|
function captureIdleOutput(id, pid, proj) {
|
|
@@ -82,9 +88,8 @@ function captureIdleOutput(id, pid, proj) {
|
|
|
82
88
|
if (proj.status.get(id)) return;
|
|
83
89
|
const w = proj.workers.get(id);
|
|
84
90
|
if (w) {
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
const out = turns[turns.length - 1].text.trim().slice(0, 8000);
|
|
91
|
+
const out = latestAgentOutput(id);
|
|
92
|
+
if (out) {
|
|
88
93
|
const oid = outputId(out);
|
|
89
94
|
const prev = proj.lastOutput.get(id);
|
|
90
95
|
const isNew = !prev || prev.outputId !== oid;
|
|
@@ -200,13 +205,9 @@ function refreshWorkers(pid, proj) {
|
|
|
200
205
|
proj.workers.set(sid, w);
|
|
201
206
|
proj.status.set(sid, liveStatus.get(sid));
|
|
202
207
|
api.setAutoApproveMenu(sid, true);
|
|
203
|
-
// Seed output for idle new workers from .screen
|
|
204
208
|
if (!liveStatus.get(sid)) {
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
const text = turns[turns.length - 1].text.trim().slice(0, 8000);
|
|
208
|
-
proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
|
|
209
|
-
}
|
|
209
|
+
const text = latestAgentOutput(sid);
|
|
210
|
+
if (text) proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
213
|
}
|
|
@@ -398,7 +399,7 @@ async function consult(pid, proj) {
|
|
|
398
399
|
}
|
|
399
400
|
|
|
400
401
|
const provider = api.getSetting('provider') || 'anthropic';
|
|
401
|
-
const modelId = api.getSetting('model') || 'claude-
|
|
402
|
+
const modelId = api.getSetting('model') || 'claude-opus-4-6';
|
|
402
403
|
const apiKey = api.getSetting('apiKey') || m.getEnvApiKey(provider) || '';
|
|
403
404
|
|
|
404
405
|
if (!apiKey) {
|
|
@@ -430,11 +431,8 @@ async function consult(pid, proj) {
|
|
|
430
431
|
let capturedAt = stored?.capturedAt || 0;
|
|
431
432
|
let oid = stored?.outputId || null;
|
|
432
433
|
if (!text) {
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
const last = [...turns].reverse().find(t => t.role === 'agent');
|
|
436
|
-
if (last) { text = last.text.trim().slice(0, 8000); capturedAt = capturedAt || Date.now(); oid = outputId(text); }
|
|
437
|
-
}
|
|
434
|
+
text = latestAgentOutput(sid);
|
|
435
|
+
if (text) { capturedAt = capturedAt || Date.now(); oid = outputId(text); }
|
|
438
436
|
}
|
|
439
437
|
entries.push({ sid, role: w.role, text, capturedAt, outputId: oid });
|
|
440
438
|
}
|
|
@@ -554,14 +552,8 @@ function executeAction(pid, proj, action, args, pillId) {
|
|
|
554
552
|
let out = stored?.text || null;
|
|
555
553
|
let oid = stored?.outputId || null;
|
|
556
554
|
if (!out && src) {
|
|
557
|
-
|
|
558
|
-
if (
|
|
559
|
-
const turns = api.getScreenTurns(src, w.presetId, { raw: true });
|
|
560
|
-
if (turns?.length && turns[turns.length - 1].role === 'agent') {
|
|
561
|
-
out = turns[turns.length - 1].text.trim().slice(0, 8000);
|
|
562
|
-
oid = outputId(out);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
555
|
+
out = latestAgentOutput(src);
|
|
556
|
+
if (out) oid = outputId(out);
|
|
565
557
|
}
|
|
566
558
|
if (!out) return `"${args.from}" has no output to route`;
|
|
567
559
|
|
|
@@ -616,10 +608,14 @@ function executeAction(pid, proj, action, args, pillId) {
|
|
|
616
608
|
|
|
617
609
|
// --- Lifecycle ---
|
|
618
610
|
|
|
619
|
-
function start(pid) {
|
|
611
|
+
async function start(pid) {
|
|
620
612
|
if (projects.has(pid)) return { error: 'Already running' };
|
|
621
613
|
if (!enabled()) return { error: 'Autopilot disabled' };
|
|
622
614
|
|
|
615
|
+
const provider = api.getSetting('provider') || 'anthropic';
|
|
616
|
+
const apiKey = api.getSetting('apiKey') || (await ai()).getEnvApiKey(provider) || '';
|
|
617
|
+
if (!apiKey) return { error: 'Set the API key in Autopilot settings (Plugins panel)' };
|
|
618
|
+
|
|
623
619
|
const { workers, status } = discoverWorkers(pid);
|
|
624
620
|
if (workers.size < 1) return { error: 'No agents with roles in this project' };
|
|
625
621
|
|
|
@@ -649,12 +645,10 @@ function start(pid) {
|
|
|
649
645
|
// Flag all workers for core menu auto-approve
|
|
650
646
|
for (const [sid] of workers) api.setAutoApproveMenu(sid, true);
|
|
651
647
|
|
|
652
|
-
// Seed lastOutput from .screen for idle workers
|
|
653
648
|
for (const [sid, w] of workers) {
|
|
654
649
|
if (status.get(sid)) continue;
|
|
655
|
-
const
|
|
656
|
-
if (!
|
|
657
|
-
const text = turns[turns.length - 1].text.trim().slice(0, 8000);
|
|
650
|
+
const text = latestAgentOutput(sid);
|
|
651
|
+
if (!text) continue;
|
|
658
652
|
proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
|
|
659
653
|
}
|
|
660
654
|
|
|
@@ -700,13 +694,13 @@ module.exports.init = function (pluginApi) {
|
|
|
700
694
|
});
|
|
701
695
|
|
|
702
696
|
// Toggle on/off
|
|
703
|
-
api.onFrontendMessage('autopilot-toggle', (msg) => {
|
|
697
|
+
api.onFrontendMessage('autopilot-toggle', async (msg) => {
|
|
704
698
|
if (projects.has(msg.projectId)) {
|
|
705
699
|
stop(msg.projectId);
|
|
706
700
|
} else {
|
|
707
701
|
// Remove lingering pill from a previous finished run
|
|
708
702
|
api.removeSessionPill(`autopilot-${msg.projectId}`);
|
|
709
|
-
const r = start(msg.projectId);
|
|
703
|
+
const r = await start(msg.projectId);
|
|
710
704
|
if (r.error) api.sendToFrontend('error', { msg: r.error });
|
|
711
705
|
}
|
|
712
706
|
});
|
package/public/index.html
CHANGED
|
@@ -324,8 +324,8 @@
|
|
|
324
324
|
<div class="mb-5">
|
|
325
325
|
<label class="block text-xs text-slate-400 mb-1.5 ml-6">Minimum working time before notifying</label>
|
|
326
326
|
<select id="cfg-notify-min-work" class="ml-6 px-3 py-1.5 text-sm bg-slate-800 border border-slate-600 rounded-md text-slate-200 outline-none focus:border-blue-500 transition-colors cursor-pointer">
|
|
327
|
-
<option value="0">Always</option>
|
|
328
|
-
<option value="10"
|
|
327
|
+
<option value="0" selected>Always</option>
|
|
328
|
+
<option value="10">10 seconds</option>
|
|
329
329
|
<option value="30">30 seconds</option>
|
|
330
330
|
</select>
|
|
331
331
|
</div>
|
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>
|
|
@@ -433,7 +449,7 @@ function showTelemetrySetup(commandId, sessionId) {
|
|
|
433
449
|
|
|
434
450
|
const toast = document.createElement('div');
|
|
435
451
|
toast.dataset.setupPreset = preset.presetId;
|
|
436
|
-
toast.dataset.sessionId = sessionId;
|
|
452
|
+
if (sessionId) toast.dataset.sessionId = sessionId;
|
|
437
453
|
toast.dataset.commandId = commandId;
|
|
438
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';
|
|
439
455
|
toast.style.opacity = '0';
|
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,7 +585,7 @@ function setStatus(id, working) {
|
|
|
526
585
|
|
|
527
586
|
// Notify on working → idle transition
|
|
528
587
|
if (wasWorking && !working && !entry.muted) {
|
|
529
|
-
const minWork = state.cfg.notifyMinWork ??
|
|
588
|
+
const minWork = state.cfg.notifyMinWork ?? 0;
|
|
530
589
|
const workDuration = (Date.now() - (entry.workStartedAt || 0)) / 1000;
|
|
531
590
|
if (workDuration >= minWork) {
|
|
532
591
|
entry.workStartedAt = null;
|
|
@@ -546,11 +605,15 @@ function setStatus(id, working) {
|
|
|
546
605
|
}
|
|
547
606
|
}
|
|
548
607
|
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
608
|
+
// Save once shortly after idle unless the agent resumes first.
|
|
609
|
+
if (wasWorking && !working) {
|
|
610
|
+
entry.scheduleIdleCapture?.();
|
|
611
|
+
}
|
|
552
612
|
|
|
553
|
-
if (working
|
|
613
|
+
if (working) {
|
|
614
|
+
entry.cancelIdleCapture?.();
|
|
615
|
+
if (!entry.workStartedAt) entry.workStartedAt = Date.now();
|
|
616
|
+
}
|
|
554
617
|
|
|
555
618
|
const el = document.querySelector(`.group[data-id="${id}"] .session-status`);
|
|
556
619
|
if (!el) return;
|
|
@@ -1112,7 +1175,7 @@ function openPillLog(id) {
|
|
|
1112
1175
|
<span class="flex-1"></span>
|
|
1113
1176
|
<button class="pill-log-clear text-[11px] text-slate-600 hover:text-slate-400 transition-colors">Clear</button>
|
|
1114
1177
|
</div>
|
|
1115
|
-
<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>
|
|
1116
1179
|
</div>`;
|
|
1117
1180
|
document.getElementById('terminals').appendChild(panel);
|
|
1118
1181
|
panel.querySelector('.pill-log-clear').addEventListener('click', () => {
|
|
@@ -1147,9 +1210,45 @@ function appendLogLine(entry) {
|
|
|
1147
1210
|
const body = document.querySelector('#pill-log-panel .pill-log-body');
|
|
1148
1211
|
if (!body) return;
|
|
1149
1212
|
const line = document.createElement('div');
|
|
1150
|
-
line.className = 'flex gap-3 py-0.5';
|
|
1151
1213
|
const time = new Date(entry.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
1152
|
-
|
|
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>`;
|
|
1153
1252
|
body.appendChild(line);
|
|
1154
1253
|
body.scrollTop = body.scrollHeight;
|
|
1155
1254
|
}
|