clideck 1.31.12 → 1.31.14
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 +2 -4
- package/assets/clideck-ask.png +0 -0
- package/handlers.js +37 -4
- package/package.json +1 -1
- package/plugin-loader.js +29 -1
- package/plugins/voice-input/clideck-plugin.json +1 -1
- package/plugins/voice-input/index.js +28 -18
- package/public/index.html +1 -1
- package/public/js/app.js +57 -23
package/README.md
CHANGED
|
@@ -51,14 +51,12 @@ clideck --port 4001
|
|
|
51
51
|
|
|
52
52
|
**Session resume** - close the lid, reopen tomorrow, pick up where things left off. each agent's session ID is captured automatically.
|
|
53
53
|
|
|
54
|
-
**
|
|
54
|
+
**Ask another session** - from inside any CliDeck session, an agent can consult another session and get the answer back as command output:
|
|
55
55
|
|
|
56
56
|
<p align="center">
|
|
57
|
-
<img src="assets/
|
|
57
|
+
<img src="assets/clideck-ask.png" width="720" alt="One agent asking another session and getting findings back">
|
|
58
58
|
</p>
|
|
59
59
|
|
|
60
|
-
**Ask another session** - from inside any CliDeck session, an agent can consult another session and get the answer back as command output:
|
|
61
|
-
|
|
62
60
|
```bash
|
|
63
61
|
clideck agents
|
|
64
62
|
clideck ask --session "Reviewer" --message "Review this output and return findings." --timeout 10m
|
|
Binary file
|
package/handlers.js
CHANGED
|
@@ -100,9 +100,9 @@ function configRootFor(preset, cmd) {
|
|
|
100
100
|
return os.homedir();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function checkRemoteUpdate(ws) {
|
|
103
|
+
function checkRemoteUpdate(ws, force = false) {
|
|
104
104
|
const now = Date.now();
|
|
105
|
-
if (remoteUpdateCache && now - remoteUpdateCheckedAt < REMOTE_UPDATE_INTERVAL) {
|
|
105
|
+
if (!force && remoteUpdateCache && now - remoteUpdateCheckedAt < REMOTE_UPDATE_INTERVAL) {
|
|
106
106
|
ws.send(JSON.stringify({ type: 'remote.update', checked: true, ...remoteUpdateCache }));
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
@@ -573,7 +573,7 @@ function onConnection(ws) {
|
|
|
573
573
|
try { ws.send(JSON.stringify({ type: 'remote.status', installed: true, ...JSON.parse(stdout) })); }
|
|
574
574
|
catch { ws.send(JSON.stringify({ type: 'remote.status', installed: true })); }
|
|
575
575
|
});
|
|
576
|
-
checkRemoteUpdate(ws);
|
|
576
|
+
checkRemoteUpdate(ws, !!msg.forceUpdate);
|
|
577
577
|
break;
|
|
578
578
|
}
|
|
579
579
|
|
|
@@ -602,7 +602,28 @@ function onConnection(ws) {
|
|
|
602
602
|
break;
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
case 'remote.voice.transcribe': {
|
|
606
|
+
const requestId = String(msg.requestId || '');
|
|
607
|
+
const replyError = (error) => ws.send(JSON.stringify({ type: 'remote.voice.error', requestId, error }));
|
|
608
|
+
if (!plugins.hasCapability('voice-input', 'transcribeAudio')) {
|
|
609
|
+
const voicePlugin = plugins.getInfo().find(p => p.id === 'voice-input' && p.installed);
|
|
610
|
+
replyError(voicePlugin
|
|
611
|
+
? 'Restart CliDeck so the Voice Input plugin update can finish loading.'
|
|
612
|
+
: 'Install the Voice Input plugin in CliDeck first.');
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
if (typeof msg.audio !== 'string' || !msg.audio) {
|
|
616
|
+
replyError('No audio received.');
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
plugins.invoke('voice-input', 'transcribeAudio', { audio: msg.audio })
|
|
620
|
+
.then(result => ws.send(JSON.stringify({ type: 'remote.voice.result', requestId, ...result })))
|
|
621
|
+
.catch(e => replyError(e.message || 'Voice transcription failed.'));
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
|
|
605
625
|
case 'remote.install': {
|
|
626
|
+
const update = !!msg.update;
|
|
606
627
|
const proc = require('child_process').spawn('npm', ['install', '-g', 'clideck-remote'], {
|
|
607
628
|
shell: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
608
629
|
});
|
|
@@ -610,7 +631,19 @@ function onConnection(ws) {
|
|
|
610
631
|
proc.stderr.on('data', d => ws.send(JSON.stringify({ type: 'remote.install.progress', text: d.toString() })));
|
|
611
632
|
proc.on('close', code => {
|
|
612
633
|
remoteUpdateCache = null;
|
|
613
|
-
|
|
634
|
+
if (code !== 0 || !update) {
|
|
635
|
+
ws.send(JSON.stringify({ type: 'remote.install.done', success: code === 0, update }));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
require('child_process').execFile('clideck-remote', ['restart', '--json'], { timeout: 10000, shell: process.platform === 'win32', env: remoteCliEnv() }, (err, stdout) => {
|
|
639
|
+
if (err) {
|
|
640
|
+
ws.send(JSON.stringify({ type: 'remote.install.done', success: false, update, error: err.message }));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
let restart = null;
|
|
644
|
+
try { restart = JSON.parse(stdout); } catch {}
|
|
645
|
+
ws.send(JSON.stringify({ type: 'remote.install.done', success: true, update, restart }));
|
|
646
|
+
});
|
|
614
647
|
});
|
|
615
648
|
break;
|
|
616
649
|
}
|
package/package.json
CHANGED
package/plugin-loader.js
CHANGED
|
@@ -67,6 +67,7 @@ let createSessionFn = null;
|
|
|
67
67
|
let closeSessionFn = null;
|
|
68
68
|
const settingsChangeHandlers = new Map(); // pluginId → [fn]
|
|
69
69
|
const sessionPills = new Map(); // pillId → { pluginId, id, title, projectId, working, statusText, icon, logs[] }
|
|
70
|
+
const backendHandlers = new Map(); // pluginId.name → fn
|
|
70
71
|
|
|
71
72
|
function removeHooks(pluginId) {
|
|
72
73
|
for (const arr of [inputHooks, outputHooks, statusHooks, transcriptHooks, menuHooks, configHooks]) {
|
|
@@ -77,6 +78,9 @@ function removeHooks(pluginId) {
|
|
|
77
78
|
for (const key of frontendHandlers.keys()) {
|
|
78
79
|
if (key.startsWith(`plugin.${pluginId}.`)) frontendHandlers.delete(key);
|
|
79
80
|
}
|
|
81
|
+
for (const key of backendHandlers.keys()) {
|
|
82
|
+
if (key.startsWith(`${pluginId}.`)) backendHandlers.delete(key);
|
|
83
|
+
}
|
|
80
84
|
settingsChangeHandlers.delete(pluginId);
|
|
81
85
|
for (const [id, pill] of sessionPills) {
|
|
82
86
|
if (pill.pluginId === pluginId) {
|
|
@@ -202,6 +206,11 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
202
206
|
onFrontendMessage(event, fn) {
|
|
203
207
|
frontendHandlers.set(`plugin.${pluginId}.${event}`, fn);
|
|
204
208
|
},
|
|
209
|
+
expose(name, fn) {
|
|
210
|
+
if (typeof name === 'string' && name && typeof fn === 'function') {
|
|
211
|
+
backendHandlers.set(`${pluginId}.${name}`, fn);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
205
214
|
|
|
206
215
|
getSession(id) {
|
|
207
216
|
const s = sessionsFn?.()?.get(id);
|
|
@@ -423,8 +432,25 @@ function handleMessage(msg) {
|
|
|
423
432
|
return true;
|
|
424
433
|
}
|
|
425
434
|
|
|
435
|
+
function hasCapability(pluginId, name) {
|
|
436
|
+
return backendHandlers.has(`${pluginId}.${name}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function invoke(pluginId, name, data = {}) {
|
|
440
|
+
const key = `${pluginId}.${name}`;
|
|
441
|
+
const fn = backendHandlers.get(key);
|
|
442
|
+
if (!fn) throw new Error(`Plugin capability not available: ${key}`);
|
|
443
|
+
return await fn(data);
|
|
444
|
+
}
|
|
445
|
+
|
|
426
446
|
function getInfo() {
|
|
427
447
|
const cfg = getConfigFn?.();
|
|
448
|
+
const capabilitiesFor = (pluginId) => {
|
|
449
|
+
const prefix = `${pluginId}.`;
|
|
450
|
+
return [...backendHandlers.keys()]
|
|
451
|
+
.filter(k => k.startsWith(prefix))
|
|
452
|
+
.map(k => k.slice(prefix.length));
|
|
453
|
+
};
|
|
428
454
|
const installed = [...plugins.values()].map(p => ({
|
|
429
455
|
id: p.manifest.id,
|
|
430
456
|
name: p.manifest.name,
|
|
@@ -436,6 +462,7 @@ function getInfo() {
|
|
|
436
462
|
settingValues: cfg?.pluginSettings?.[p.manifest.id] || {},
|
|
437
463
|
dynamicOptions: p.dynamicOptions || {},
|
|
438
464
|
actions: p.actions,
|
|
465
|
+
capabilities: capabilitiesFor(p.manifest.id),
|
|
439
466
|
hasClient: existsSync(join(p.dir, 'client.js')),
|
|
440
467
|
bundled: BUNDLED_IDS.has(p.manifest.id),
|
|
441
468
|
installed: true,
|
|
@@ -451,6 +478,7 @@ function getInfo() {
|
|
|
451
478
|
settingValues: {},
|
|
452
479
|
dynamicOptions: {},
|
|
453
480
|
actions: [],
|
|
481
|
+
capabilities: [],
|
|
454
482
|
hasClient: false,
|
|
455
483
|
bundled: BUNDLED_IDS.has(u.manifest.id),
|
|
456
484
|
installed: false,
|
|
@@ -561,6 +589,6 @@ module.exports = {
|
|
|
561
589
|
PLUGINS_DIR, BUNDLED_IDS,
|
|
562
590
|
init, shutdown,
|
|
563
591
|
transformInput, notifyOutput, notifyStatus, notifyTranscript, notifyMenu, notifyConfig, clearStatus, isWorking, shouldAutoApproveMenu,
|
|
564
|
-
handleMessage, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
|
|
592
|
+
handleMessage, hasCapability, invoke, updateSetting, getInfo, resolveFile, installPlugin, removePlugin,
|
|
565
593
|
getPills, getPillLogs,
|
|
566
594
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "voice-input",
|
|
3
3
|
"name": "Voice Input",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.1",
|
|
5
5
|
"author": "CliDeck",
|
|
6
6
|
"description": "Dictate prompts with your voice using Whisper speech-to-text",
|
|
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\"><path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\"/><line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\"/></svg>",
|
|
@@ -244,28 +244,38 @@ module.exports = {
|
|
|
244
244
|
return { text: data.text || '', language: data.language || 'unknown', avg_logprob: null };
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// --- Transcription API ---
|
|
248
|
+
|
|
249
|
+
async function transcribeAudio(audio) {
|
|
250
|
+
if (!api.getSetting('enabled')) {
|
|
251
|
+
throw new Error('Enable the Voice Input plugin in CliDeck first.');
|
|
252
|
+
}
|
|
253
|
+
const backend = api.getSetting('backend');
|
|
254
|
+
let result;
|
|
255
|
+
if (backend === 'local') {
|
|
256
|
+
if (!worker) await startLocal();
|
|
257
|
+
if (!worker) throw new Error('Local model not running. Enable plugin with local backend to start.');
|
|
258
|
+
result = await workerCmd('transcribe', {
|
|
259
|
+
audio,
|
|
260
|
+
lang: api.getSetting('language') || 'auto',
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
result = await transcribeOpenAI(audio);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const text = processText(result.text || '');
|
|
267
|
+
if (!text) return { text: '', skipped: true };
|
|
268
|
+
return { text, language: result.language, inferenceTime: result.inference_time };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
api.expose('transcribeAudio', ({ audio }) => transcribeAudio(audio));
|
|
272
|
+
|
|
247
273
|
// --- Message handlers ---
|
|
248
274
|
|
|
249
275
|
api.onFrontendMessage('transcribe', async (msg) => {
|
|
250
|
-
const backend = api.getSetting('backend');
|
|
251
276
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!worker) { api.sendToFrontend('error', { error: 'Local model not running. Enable plugin with local backend to start.' }); return; }
|
|
255
|
-
result = await workerCmd('transcribe', {
|
|
256
|
-
audio: msg.audio,
|
|
257
|
-
lang: api.getSetting('language') || 'auto',
|
|
258
|
-
});
|
|
259
|
-
} else {
|
|
260
|
-
result = await transcribeOpenAI(msg.audio);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const text = processText(result.text || '');
|
|
264
|
-
if (!text) {
|
|
265
|
-
api.sendToFrontend('result', { text: '', skipped: true, sessionId: msg.sessionId });
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
api.sendToFrontend('result', { text, language: result.language, inferenceTime: result.inference_time, sessionId: msg.sessionId });
|
|
277
|
+
const result = await transcribeAudio(msg.audio);
|
|
278
|
+
api.sendToFrontend('result', { ...result, sessionId: msg.sessionId });
|
|
269
279
|
} catch (e) {
|
|
270
280
|
api.log(`transcribe: ${e.message}`);
|
|
271
281
|
api.sendToFrontend('error', { error: e.message });
|
package/public/index.html
CHANGED
|
@@ -355,7 +355,7 @@
|
|
|
355
355
|
<div id="remote-modal" class="absolute inset-0 z-[260] bg-black/60 backdrop-blur-sm hidden items-center justify-center">
|
|
356
356
|
<div style="background:var(--color-dialog);border:1px solid color-mix(in srgb, var(--color-muted) 40%, transparent);box-shadow:0 25px 60px -12px var(--color-shadow)" class="rounded-2xl w-[340px] flex flex-col overflow-hidden">
|
|
357
357
|
<div class="px-5 py-3.5 flex items-center justify-between">
|
|
358
|
-
<span class="text-[13px] font-semibold text-slate-200">Mobile Remote</span>
|
|
358
|
+
<span id="remote-modal-title" class="text-[13px] font-semibold text-slate-200">Mobile Remote</span>
|
|
359
359
|
<button id="remote-close" class="w-6 h-6 flex items-center justify-center rounded-md text-slate-500 hover:text-slate-300 hover:bg-slate-700/50 transition-colors">
|
|
360
360
|
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
361
361
|
</button>
|
package/public/js/app.js
CHANGED
|
@@ -45,7 +45,7 @@ function connect() {
|
|
|
45
45
|
reconnectReplaySkip = new Set(state.terms.keys());
|
|
46
46
|
setServerConnectionState(true);
|
|
47
47
|
flushQueuedSends();
|
|
48
|
-
send({ type: 'remote.status' });
|
|
48
|
+
send({ type: 'remote.status', forceUpdate: true });
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
state.ws.onmessage = ({ data }) => {
|
|
@@ -364,10 +364,13 @@ function connect() {
|
|
|
364
364
|
appendInstallLog(msg.text);
|
|
365
365
|
break;
|
|
366
366
|
case 'remote.install.done':
|
|
367
|
-
handleInstallDone(msg
|
|
367
|
+
handleInstallDone(msg);
|
|
368
368
|
break;
|
|
369
369
|
case 'remote.update':
|
|
370
370
|
remoteUpdateInfo = msg?.available ? msg : null;
|
|
371
|
+
if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
|
|
372
|
+
startRemoteInstall({ update: true, auto: true });
|
|
373
|
+
}
|
|
371
374
|
if (remotePreflight?.pending) {
|
|
372
375
|
remotePreflight.updateSeen = true;
|
|
373
376
|
finishRemotePreflight();
|
|
@@ -1161,6 +1164,7 @@ let remoteStatsTimer = null;
|
|
|
1161
1164
|
let remoteUpdateInfo = null;
|
|
1162
1165
|
let remotePreflight = null;
|
|
1163
1166
|
let remoteLastStatus = null;
|
|
1167
|
+
let remoteInstallMode = null;
|
|
1164
1168
|
|
|
1165
1169
|
function startRemotePoll() {
|
|
1166
1170
|
stopRemotePoll();
|
|
@@ -1180,27 +1184,34 @@ function setRemotePane(pane) {
|
|
|
1180
1184
|
}
|
|
1181
1185
|
}
|
|
1182
1186
|
|
|
1187
|
+
function remoteTitle() {
|
|
1188
|
+
const version = state.remoteVersion && state.remoteVersion !== 'not installed'
|
|
1189
|
+
? ` v${state.remoteVersion}`
|
|
1190
|
+
: '';
|
|
1191
|
+
return `Mobile Remote${version}`;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function updateRemoteTitle() {
|
|
1195
|
+
const title = remoteTitle();
|
|
1196
|
+
const modalTitle = document.getElementById('remote-modal-title');
|
|
1197
|
+
const introTitle = document.getElementById('remote-intro-title');
|
|
1198
|
+
if (modalTitle) modalTitle.textContent = title;
|
|
1199
|
+
if (introTitle) introTitle.textContent = `CliDeck ${title}`;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1183
1202
|
function showRemoteIntro(opts = {}) {
|
|
1184
1203
|
const title = document.getElementById('remote-intro-title');
|
|
1185
1204
|
const text = document.getElementById('remote-intro-text');
|
|
1186
1205
|
const foot = document.getElementById('remote-intro-foot');
|
|
1187
1206
|
const btn = document.getElementById('remote-add');
|
|
1188
|
-
|
|
1207
|
+
updateRemoteTitle();
|
|
1208
|
+
if (opts.title) title.textContent = opts.title;
|
|
1189
1209
|
text.textContent = opts.text || 'Control your AI agents from your phone. See live status, send messages, and get notifications — all end-to-end encrypted.';
|
|
1190
1210
|
foot.innerHTML = opts.foot || 'Installs the <code class="text-slate-500">clideck-remote</code> package via npm';
|
|
1191
1211
|
btn.textContent = opts.button || 'Add to CliDeck';
|
|
1192
1212
|
setRemotePane('intro');
|
|
1193
1213
|
}
|
|
1194
1214
|
|
|
1195
|
-
function showRemoteUpdateRequired() {
|
|
1196
|
-
showRemoteIntro({
|
|
1197
|
-
title: 'Update Required',
|
|
1198
|
-
text: `Version ${remoteUpdateInfo.latest} is available. Update CliDeck Remote to continue with mobile pairing on this machine.`,
|
|
1199
|
-
foot: `Installed: <code class="text-slate-500">${esc(remoteUpdateInfo.installed)}</code> · Latest: <code class="text-slate-500">${esc(remoteUpdateInfo.latest)}</code>`,
|
|
1200
|
-
button: 'Update to Continue',
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
1215
|
function finishRemotePreflight() {
|
|
1205
1216
|
if (!remotePreflight?.pending || !remotePreflight.statusSeen || !remotePreflight.updateSeen) return;
|
|
1206
1217
|
remotePreflight = null;
|
|
@@ -1209,7 +1220,7 @@ function finishRemotePreflight() {
|
|
|
1209
1220
|
return;
|
|
1210
1221
|
}
|
|
1211
1222
|
if (remoteUpdateInfo?.available) {
|
|
1212
|
-
|
|
1223
|
+
startRemoteInstall({ update: true, auto: true });
|
|
1213
1224
|
return;
|
|
1214
1225
|
}
|
|
1215
1226
|
if (remoteState === 'idle') {
|
|
@@ -1241,6 +1252,7 @@ function finishRemotePreflight() {
|
|
|
1241
1252
|
}
|
|
1242
1253
|
|
|
1243
1254
|
function openRemoteModal() {
|
|
1255
|
+
updateRemoteTitle();
|
|
1244
1256
|
remoteModalOpen = true;
|
|
1245
1257
|
remoteModal.classList.remove('hidden');
|
|
1246
1258
|
remoteModal.style.display = 'flex';
|
|
@@ -1331,6 +1343,7 @@ function handleRemoteStatus(msg) {
|
|
|
1331
1343
|
remoteLastStatus = msg;
|
|
1332
1344
|
remoteInstalled = !!msg.installed;
|
|
1333
1345
|
state.remoteVersion = msg.version || (msg.installed ? null : 'not installed');
|
|
1346
|
+
updateRemoteTitle();
|
|
1334
1347
|
updateVersionFooter();
|
|
1335
1348
|
const wasPaired = remoteState === 'paired';
|
|
1336
1349
|
const preflighting = !!remotePreflight?.pending;
|
|
@@ -1368,8 +1381,9 @@ function handleRemoteStatus(msg) {
|
|
|
1368
1381
|
stopRemotePoll();
|
|
1369
1382
|
if (wasPaired) { stopRemoteStats(); setRemoteLock(false); }
|
|
1370
1383
|
}
|
|
1371
|
-
if (remoteUpdateInfo?.available &&
|
|
1372
|
-
|
|
1384
|
+
if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
|
|
1385
|
+
startRemoteInstall({ update: true, auto: true });
|
|
1386
|
+
return;
|
|
1373
1387
|
}
|
|
1374
1388
|
updateRemoteButton();
|
|
1375
1389
|
if (remotePreflight?.pending) {
|
|
@@ -1387,8 +1401,8 @@ function handleRemotePaired(msg) {
|
|
|
1387
1401
|
else qrImg.classList.add('hidden');
|
|
1388
1402
|
updateRemoteButton();
|
|
1389
1403
|
startRemotePoll();
|
|
1390
|
-
if (remoteUpdateInfo?.available &&
|
|
1391
|
-
|
|
1404
|
+
if (remoteUpdateInfo?.available && remoteInstalled && !remoteInstallMode) {
|
|
1405
|
+
startRemoteInstall({ update: true, auto: true });
|
|
1392
1406
|
return;
|
|
1393
1407
|
}
|
|
1394
1408
|
if (remotePreflight?.pending) {
|
|
@@ -1422,17 +1436,39 @@ function appendInstallLog(text) {
|
|
|
1422
1436
|
log.scrollTop = log.scrollHeight;
|
|
1423
1437
|
}
|
|
1424
1438
|
|
|
1425
|
-
function
|
|
1439
|
+
function startRemoteInstall(opts = {}) {
|
|
1440
|
+
remoteInstallMode = { update: !!opts.update, auto: !!opts.auto };
|
|
1441
|
+
const log = document.getElementById('remote-install-log');
|
|
1442
|
+
log.textContent = '';
|
|
1443
|
+
if (remoteInstallMode.update) {
|
|
1444
|
+
appendInstallLog(`Updating clideck-remote to ${remoteUpdateInfo?.latest || 'latest'}...\n`);
|
|
1445
|
+
}
|
|
1446
|
+
setRemotePane('installing');
|
|
1447
|
+
if (!remoteModalOpen) openRemoteModal();
|
|
1448
|
+
send({ type: 'remote.install', update: remoteInstallMode.update });
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function handleInstallDone(msg) {
|
|
1452
|
+
const success = !!msg?.success;
|
|
1453
|
+
const wasUpdate = !!msg?.update || !!remoteInstallMode?.update;
|
|
1454
|
+
remoteInstallMode = null;
|
|
1426
1455
|
if (success) {
|
|
1427
1456
|
remoteInstalled = true;
|
|
1428
1457
|
remoteUpdateInfo = null;
|
|
1458
|
+
if (wasUpdate) {
|
|
1459
|
+
remoteState = 'connecting';
|
|
1460
|
+
setRemotePane('connecting');
|
|
1461
|
+
send({ type: 'remote.status', forceUpdate: true });
|
|
1462
|
+
startRemotePoll();
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1429
1465
|
// Installed — go straight to pairing
|
|
1430
1466
|
remoteState = 'connecting';
|
|
1431
1467
|
setRemotePane('connecting');
|
|
1432
1468
|
send({ type: 'remote.pair' });
|
|
1433
1469
|
} else {
|
|
1434
1470
|
const log = document.getElementById('remote-install-log');
|
|
1435
|
-
log.textContent +=
|
|
1471
|
+
log.textContent += `\n— ${msg?.error || 'Install failed'}. Check permissions or run manually:\n npm install -g clideck-remote\n`;
|
|
1436
1472
|
log.scrollTop = log.scrollHeight;
|
|
1437
1473
|
}
|
|
1438
1474
|
}
|
|
@@ -1450,14 +1486,12 @@ btnRemote.addEventListener('click', () => {
|
|
|
1450
1486
|
remotePreflight = { pending: true, statusSeen: false, updateSeen: false };
|
|
1451
1487
|
setRemotePane('connecting');
|
|
1452
1488
|
openRemoteModal();
|
|
1453
|
-
send({ type: 'remote.status' });
|
|
1489
|
+
send({ type: 'remote.status', forceUpdate: true });
|
|
1454
1490
|
});
|
|
1455
1491
|
|
|
1456
1492
|
// Install button
|
|
1457
1493
|
document.getElementById('remote-add').addEventListener('click', () => {
|
|
1458
|
-
|
|
1459
|
-
setRemotePane('installing');
|
|
1460
|
-
send({ type: 'remote.install' });
|
|
1494
|
+
startRemoteInstall({ update: !!(remoteInstalled && remoteUpdateInfo?.available) });
|
|
1461
1495
|
});
|
|
1462
1496
|
|
|
1463
1497
|
// Close / disconnect
|