clideck 1.29.0 → 1.29.2
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/handlers.js +48 -11
- package/package.json +1 -1
- package/plugin-loader.js +3 -2
- package/plugins/autopilot/prompt.md +2 -1
- package/public/index.html +3 -3
- package/public/js/app.js +95 -63
- package/sessions.js +29 -12
- package/transcript-builder.js +1 -1
- package/transcript-parser.js +27 -0
package/handlers.js
CHANGED
|
@@ -59,19 +59,26 @@ function getInstalledVersion(bin) {
|
|
|
59
59
|
function checkRemoteUpdate(ws) {
|
|
60
60
|
const now = Date.now();
|
|
61
61
|
if (remoteUpdateCache && now - remoteUpdateCheckedAt < REMOTE_UPDATE_INTERVAL) {
|
|
62
|
-
|
|
62
|
+
ws.send(JSON.stringify({ type: 'remote.update', checked: true, ...remoteUpdateCache }));
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
const shellOpt = process.platform === 'win32';
|
|
66
66
|
require('child_process').execFile('npm', ['list', '-g', 'clideck-remote', '--json', '--depth=0'], { shell: shellOpt, timeout: 10000 }, (err, stdout) => {
|
|
67
67
|
let installed;
|
|
68
|
-
try { installed = JSON.parse(stdout).dependencies['clideck-remote'].version; }
|
|
68
|
+
try { installed = JSON.parse(stdout).dependencies['clideck-remote'].version; }
|
|
69
|
+
catch {
|
|
70
|
+
ws.send(JSON.stringify({ type: 'remote.update', available: false, checked: false }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
69
73
|
require('child_process').execFile('npm', ['view', 'clideck-remote', 'version'], { shell: shellOpt, timeout: 10000 }, (err2, stdout2) => {
|
|
70
|
-
if (err2)
|
|
74
|
+
if (err2) {
|
|
75
|
+
ws.send(JSON.stringify({ type: 'remote.update', installed, available: false, checked: false }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
71
78
|
const latest = stdout2.trim();
|
|
72
79
|
remoteUpdateCache = { installed, latest, available: compareVersions(latest, installed) > 0 };
|
|
73
80
|
remoteUpdateCheckedAt = now;
|
|
74
|
-
|
|
81
|
+
ws.send(JSON.stringify({ type: 'remote.update', checked: true, ...remoteUpdateCache }));
|
|
75
82
|
});
|
|
76
83
|
});
|
|
77
84
|
}
|
|
@@ -101,6 +108,33 @@ checkAvailability();
|
|
|
101
108
|
let cfg = config.load();
|
|
102
109
|
if (detectTelemetryConfig(cfg)) config.save(cfg);
|
|
103
110
|
|
|
111
|
+
function extractQuotedPath(command, needle) {
|
|
112
|
+
if (!command || !needle) return '';
|
|
113
|
+
const parts = String(command).match(/"([^"]+)"/g) || [];
|
|
114
|
+
for (const part of parts) {
|
|
115
|
+
const value = part.slice(1, -1);
|
|
116
|
+
if (value.includes(needle)) return value;
|
|
117
|
+
}
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hasExistingHook(arr, hookFile, route) {
|
|
122
|
+
return !!arr?.some(h => h.hooks?.some(x => {
|
|
123
|
+
if (!x.command?.includes(hookFile) || !x.command?.includes(` ${route}`)) return false;
|
|
124
|
+
const hookPath = extractQuotedPath(x.command, hookFile);
|
|
125
|
+
return !!hookPath && existsSync(hookPath);
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function codexConfigLooksHealthy(content, port) {
|
|
130
|
+
if (!content.includes('[otel]') || !content.includes(`localhost:${port}`)) return false;
|
|
131
|
+
const notifyLine = content.match(/^\s*notify\s*=\s*\[(.+)\]\s*$/m)?.[1] || '';
|
|
132
|
+
if (!notifyLine.includes('notify-helper')) return false;
|
|
133
|
+
const quoted = [...notifyLine.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
|
134
|
+
const helperPath = quoted.find(v => v.includes('notify-helper'));
|
|
135
|
+
return !!helperPath && existsSync(helperPath);
|
|
136
|
+
}
|
|
137
|
+
|
|
104
138
|
function detectTelemetryConfig(c) {
|
|
105
139
|
const home = os.homedir();
|
|
106
140
|
const port = '4000';
|
|
@@ -116,24 +150,27 @@ function detectTelemetryConfig(c) {
|
|
|
116
150
|
try {
|
|
117
151
|
const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
|
|
118
152
|
const hooks = s.hooks || {};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
&&
|
|
122
|
-
&& hooks.
|
|
153
|
+
detected = hasExistingHook(hooks.UserPromptSubmit, 'claude-hook.js', 'start')
|
|
154
|
+
&& hasExistingHook(hooks.Stop, 'claude-hook.js', 'stop')
|
|
155
|
+
&& hasExistingHook(hooks.StopFailure, 'claude-hook.js', 'stop')
|
|
156
|
+
&& hasExistingHook(hooks.PreToolUse, 'claude-hook.js', 'menu')
|
|
157
|
+
&& hooks.Notification?.some(h => h.matcher === 'idle_prompt' && hasExistingHook([h], 'claude-hook.js', 'idle'));
|
|
123
158
|
if (!detected) reason = 'Needs re-patch';
|
|
124
159
|
} catch {}
|
|
125
160
|
} else if (preset.presetId === 'codex') {
|
|
126
161
|
try {
|
|
127
162
|
const content = readFileSync(join(home, '.codex', 'config.toml'), 'utf8');
|
|
128
|
-
detected =
|
|
163
|
+
detected = codexConfigLooksHealthy(content, port);
|
|
129
164
|
if (!detected) reason = 'Needs re-patch';
|
|
130
165
|
} catch {}
|
|
131
166
|
} else if (preset.presetId === 'gemini-cli') {
|
|
132
167
|
try {
|
|
133
168
|
const s = JSON.parse(readFileSync(join(home, '.gemini', 'settings.json'), 'utf8'));
|
|
134
169
|
const hooks = s.hooks || {};
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
detected = hasExistingHook(hooks.BeforeAgent, 'gemini-hook.js', 'start')
|
|
171
|
+
&& hasExistingHook(hooks.AfterAgent, 'gemini-hook.js', 'stop')
|
|
172
|
+
&& hasExistingHook(hooks.SessionEnd, 'gemini-hook.js', 'stop')
|
|
173
|
+
&& hasExistingHook(hooks.BeforeTool, 'gemini-hook.js', 'menu');
|
|
137
174
|
if (!detected) reason = 'Needs re-patch';
|
|
138
175
|
} catch {}
|
|
139
176
|
} else if (preset.presetId === 'opencode') {
|
package/package.json
CHANGED
package/plugin-loader.js
CHANGED
|
@@ -206,13 +206,14 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
206
206
|
getSession(id) {
|
|
207
207
|
const s = sessionsFn?.()?.get(id);
|
|
208
208
|
if (!s) return null;
|
|
209
|
-
|
|
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:') };
|
|
210
211
|
},
|
|
211
212
|
getSessions() {
|
|
212
213
|
const sessions = sessionsFn?.();
|
|
213
214
|
if (!sessions) return [];
|
|
214
215
|
return [...sessions].map(([id, s]) => ({
|
|
215
|
-
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:'),
|
|
216
217
|
}));
|
|
217
218
|
},
|
|
218
219
|
|
|
@@ -51,10 +51,11 @@ RULES
|
|
|
51
51
|
|
|
52
52
|
DO NOT USE notify_user UNLESS ABSOLUTELY NECESSARY
|
|
53
53
|
- Do NOT ask the user if you should continue. Do NOT notify them with requests like "Please resume agent X" or "Should I keep going?" or "Is this a good stopping point?"
|
|
54
|
+
- Do NOT alert the user that some agent asking for the user input before proceeding, this is not an execuse to stop and ask the user what to do. You should route the work to the next best agent until the workflow is truly blocked and cannot proceed without user input. (e.g. if the programmer ask for the user input, first make sure the reivewer or QA agent has not already reviewed the code, if not route it to them first)
|
|
54
55
|
- The user may be away from the computer and expects the agents to keep working until the task is naturally complete.
|
|
55
56
|
- You are autonomous. If you are unsure how to proceed, re-read the workflow state and the latest agent outputs, think differently, and route again.
|
|
56
57
|
- Repeat agents with the same output if needed, unless the routing state shows that the same handoff is being repeated without progress.
|
|
57
|
-
- You steer between agents until the task is complete or the user interrupts you, period.
|
|
58
|
+
- You steer between agents until the task is complete or the user interrupts you, period.
|
|
58
59
|
|
|
59
60
|
HOW TO THINK
|
|
60
61
|
For each decision, reason in this order:
|
package/public/index.html
CHANGED
|
@@ -368,10 +368,10 @@
|
|
|
368
368
|
<!-- Intro (not installed) -->
|
|
369
369
|
<div id="remote-intro" class="hidden px-6 py-6 flex flex-col items-center gap-4">
|
|
370
370
|
<svg class="w-12 h-12 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
371
|
-
<h3 class="text-[13px] font-semibold text-slate-200">CliDeck Mobile Remote</h3>
|
|
372
|
-
<p class="text-xs text-slate-400 text-center leading-relaxed">Control your AI agents from your phone. See live status, send messages, and get notifications — all end-to-end encrypted.</p>
|
|
371
|
+
<h3 id="remote-intro-title" class="text-[13px] font-semibold text-slate-200">CliDeck Mobile Remote</h3>
|
|
372
|
+
<p id="remote-intro-text" class="text-xs text-slate-400 text-center leading-relaxed">Control your AI agents from your phone. See live status, send messages, and get notifications — all end-to-end encrypted.</p>
|
|
373
373
|
<button id="remote-add" class="mt-1 w-full px-4 py-2.5 text-[13px] font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors">Add to CliDeck</button>
|
|
374
|
-
<p class="text-[11px] text-slate-600 text-center">Installs the <code class="text-slate-500">clideck-remote</code> package via npm</p>
|
|
374
|
+
<p id="remote-intro-foot" class="text-[11px] text-slate-600 text-center">Installs the <code class="text-slate-500">clideck-remote</code> package via npm</p>
|
|
375
375
|
</div>
|
|
376
376
|
|
|
377
377
|
<!-- Installing -->
|
package/public/js/app.js
CHANGED
|
@@ -291,7 +291,11 @@ function connect() {
|
|
|
291
291
|
handleInstallDone(msg.success);
|
|
292
292
|
break;
|
|
293
293
|
case 'remote.update':
|
|
294
|
-
|
|
294
|
+
remoteUpdateInfo = msg?.available ? msg : null;
|
|
295
|
+
if (remotePreflight?.pending) {
|
|
296
|
+
remotePreflight.updateSeen = true;
|
|
297
|
+
finishRemotePreflight();
|
|
298
|
+
}
|
|
295
299
|
break;
|
|
296
300
|
default:
|
|
297
301
|
if (msg.type?.startsWith('plugin.')) dispatchPluginMessage(msg);
|
|
@@ -1046,6 +1050,9 @@ let remoteModalOpen = false;
|
|
|
1046
1050
|
let remoteStatusPoll = null;
|
|
1047
1051
|
let remoteConnectedAt = null;
|
|
1048
1052
|
let remoteStatsTimer = null;
|
|
1053
|
+
let remoteUpdateInfo = null;
|
|
1054
|
+
let remotePreflight = null;
|
|
1055
|
+
let remoteLastStatus = null;
|
|
1049
1056
|
|
|
1050
1057
|
function startRemotePoll() {
|
|
1051
1058
|
stopRemotePoll();
|
|
@@ -1065,6 +1072,66 @@ function setRemotePane(pane) {
|
|
|
1065
1072
|
}
|
|
1066
1073
|
}
|
|
1067
1074
|
|
|
1075
|
+
function showRemoteIntro(opts = {}) {
|
|
1076
|
+
const title = document.getElementById('remote-intro-title');
|
|
1077
|
+
const text = document.getElementById('remote-intro-text');
|
|
1078
|
+
const foot = document.getElementById('remote-intro-foot');
|
|
1079
|
+
const btn = document.getElementById('remote-add');
|
|
1080
|
+
title.textContent = opts.title || 'CliDeck Mobile Remote';
|
|
1081
|
+
text.textContent = opts.text || 'Control your AI agents from your phone. See live status, send messages, and get notifications — all end-to-end encrypted.';
|
|
1082
|
+
foot.innerHTML = opts.foot || 'Installs the <code class="text-slate-500">clideck-remote</code> package via npm';
|
|
1083
|
+
btn.textContent = opts.button || 'Add to CliDeck';
|
|
1084
|
+
setRemotePane('intro');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function showRemoteUpdateRequired() {
|
|
1088
|
+
showRemoteIntro({
|
|
1089
|
+
title: 'Update Required',
|
|
1090
|
+
text: `Version ${remoteUpdateInfo.latest} is available. Update CliDeck Remote to continue with mobile pairing on this machine.`,
|
|
1091
|
+
foot: `Installed: <code class="text-slate-500">${esc(remoteUpdateInfo.installed)}</code> · Latest: <code class="text-slate-500">${esc(remoteUpdateInfo.latest)}</code>`,
|
|
1092
|
+
button: 'Update to Continue',
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function finishRemotePreflight() {
|
|
1097
|
+
if (!remotePreflight?.pending || !remotePreflight.statusSeen || !remotePreflight.updateSeen) return;
|
|
1098
|
+
remotePreflight = null;
|
|
1099
|
+
if (!remoteInstalled) {
|
|
1100
|
+
showRemoteIntro();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (remoteUpdateInfo?.available) {
|
|
1104
|
+
showRemoteUpdateRequired();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (remoteState === 'idle') {
|
|
1108
|
+
remoteState = 'connecting';
|
|
1109
|
+
setRemotePane('connecting');
|
|
1110
|
+
send({ type: 'remote.pair' });
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (remoteState === 'paired' && remoteLastStatus?.paired) {
|
|
1114
|
+
setRemotePane('active');
|
|
1115
|
+
setRemoteLock(true);
|
|
1116
|
+
startRemoteStats(remoteLastStatus.pairedAt);
|
|
1117
|
+
const deviceEl = document.getElementById('remote-device-info');
|
|
1118
|
+
if (deviceEl) {
|
|
1119
|
+
const parts = [remoteLastStatus.deviceName, remoteLastStatus.location].filter(Boolean);
|
|
1120
|
+
deviceEl.textContent = parts.length ? parts.join(' · ') : '';
|
|
1121
|
+
}
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (remoteState === 'waiting' && remoteLastStatus?.connected && remoteLastStatus?.url) {
|
|
1125
|
+
document.getElementById('remote-url-box').textContent = remoteLastStatus.url;
|
|
1126
|
+
const qrImg = document.getElementById('remote-qr-img');
|
|
1127
|
+
if (remoteLastStatus.qr && remoteLastStatus.qr.startsWith('data:')) { qrImg.src = remoteLastStatus.qr; qrImg.classList.remove('hidden'); }
|
|
1128
|
+
else qrImg.classList.add('hidden');
|
|
1129
|
+
setRemotePane('qr');
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
setRemotePane(remoteState === 'paired' ? 'active' : remoteState === 'waiting' ? 'qr' : 'connecting');
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1068
1135
|
function openRemoteModal() {
|
|
1069
1136
|
remoteModalOpen = true;
|
|
1070
1137
|
remoteModal.classList.remove('hidden');
|
|
@@ -1153,10 +1220,12 @@ function updateRemoteButton() {
|
|
|
1153
1220
|
}
|
|
1154
1221
|
|
|
1155
1222
|
function handleRemoteStatus(msg) {
|
|
1223
|
+
remoteLastStatus = msg;
|
|
1156
1224
|
remoteInstalled = !!msg.installed;
|
|
1157
1225
|
state.remoteVersion = msg.version || (msg.installed ? null : 'not installed');
|
|
1158
1226
|
updateVersionFooter();
|
|
1159
1227
|
const wasPaired = remoteState === 'paired';
|
|
1228
|
+
const preflighting = !!remotePreflight?.pending;
|
|
1160
1229
|
if (!msg.installed) {
|
|
1161
1230
|
remoteState = 'idle';
|
|
1162
1231
|
stopRemotePoll();
|
|
@@ -1165,7 +1234,7 @@ function handleRemoteStatus(msg) {
|
|
|
1165
1234
|
const wasFresh = remoteState !== 'paired';
|
|
1166
1235
|
remoteState = 'paired';
|
|
1167
1236
|
if (!remoteStatusPoll) startRemotePoll();
|
|
1168
|
-
if (wasFresh) {
|
|
1237
|
+
if (wasFresh && !preflighting) {
|
|
1169
1238
|
|
|
1170
1239
|
setRemotePane('active');
|
|
1171
1240
|
setRemoteLock(true);
|
|
@@ -1185,13 +1254,20 @@ function handleRemoteStatus(msg) {
|
|
|
1185
1254
|
if (msg.qr && msg.qr.startsWith('data:')) { qrImg.src = msg.qr; qrImg.classList.remove('hidden'); }
|
|
1186
1255
|
else qrImg.classList.add('hidden');
|
|
1187
1256
|
startRemotePoll();
|
|
1188
|
-
if (remoteModalOpen) setRemotePane('qr');
|
|
1257
|
+
if (!preflighting && remoteModalOpen) setRemotePane('qr');
|
|
1189
1258
|
} else {
|
|
1190
1259
|
remoteState = 'idle';
|
|
1191
1260
|
stopRemotePoll();
|
|
1192
1261
|
if (wasPaired) { stopRemoteStats(); setRemoteLock(false); }
|
|
1193
1262
|
}
|
|
1263
|
+
if (remoteUpdateInfo?.available && remoteModalOpen) {
|
|
1264
|
+
showRemoteUpdateRequired();
|
|
1265
|
+
}
|
|
1194
1266
|
updateRemoteButton();
|
|
1267
|
+
if (remotePreflight?.pending) {
|
|
1268
|
+
remotePreflight.statusSeen = true;
|
|
1269
|
+
finishRemotePreflight();
|
|
1270
|
+
}
|
|
1195
1271
|
}
|
|
1196
1272
|
|
|
1197
1273
|
function handleRemotePaired(msg) {
|
|
@@ -1201,9 +1277,18 @@ function handleRemotePaired(msg) {
|
|
|
1201
1277
|
const qrImg = document.getElementById('remote-qr-img');
|
|
1202
1278
|
if (msg.qr && msg.qr.startsWith('data:')) { qrImg.src = msg.qr; qrImg.classList.remove('hidden'); }
|
|
1203
1279
|
else qrImg.classList.add('hidden');
|
|
1204
|
-
setRemotePane('qr');
|
|
1205
1280
|
updateRemoteButton();
|
|
1206
1281
|
startRemotePoll();
|
|
1282
|
+
if (remoteUpdateInfo?.available && remoteModalOpen) {
|
|
1283
|
+
showRemoteUpdateRequired();
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (remotePreflight?.pending) {
|
|
1287
|
+
remotePreflight.statusSeen = true;
|
|
1288
|
+
finishRemotePreflight();
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
setRemotePane('qr');
|
|
1207
1292
|
}
|
|
1208
1293
|
|
|
1209
1294
|
function handleRemoteUnpaired() {
|
|
@@ -1232,6 +1317,7 @@ function appendInstallLog(text) {
|
|
|
1232
1317
|
function handleInstallDone(success) {
|
|
1233
1318
|
if (success) {
|
|
1234
1319
|
remoteInstalled = true;
|
|
1320
|
+
remoteUpdateInfo = null;
|
|
1235
1321
|
// Installed — go straight to pairing
|
|
1236
1322
|
remoteState = 'connecting';
|
|
1237
1323
|
setRemotePane('connecting');
|
|
@@ -1243,74 +1329,20 @@ function handleInstallDone(success) {
|
|
|
1243
1329
|
}
|
|
1244
1330
|
}
|
|
1245
1331
|
|
|
1246
|
-
let remoteUpdateShown = false;
|
|
1247
|
-
|
|
1248
|
-
function showRemoteUpdateToast(msg) {
|
|
1249
|
-
if (remoteUpdateShown) return;
|
|
1250
|
-
remoteUpdateShown = true;
|
|
1251
|
-
|
|
1252
|
-
const toast = document.createElement('div');
|
|
1253
|
-
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';
|
|
1254
|
-
toast.style.cssText = 'opacity:0;transform:translateY(12px);transition:opacity 0.3s ease,transform 0.3s ease';
|
|
1255
|
-
|
|
1256
|
-
toast.innerHTML = `
|
|
1257
|
-
<div class="flex items-center gap-2.5 px-4 pt-3.5 pb-1">
|
|
1258
|
-
<svg class="w-5 h-5 flex-shrink-0 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
1259
|
-
<span class="text-[13px] font-semibold text-slate-200">CliDeck Remote Update</span>
|
|
1260
|
-
<button class="dismiss-btn ml-auto 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">
|
|
1261
|
-
<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>
|
|
1262
|
-
</button>
|
|
1263
|
-
</div>
|
|
1264
|
-
<p class="px-4 pt-1 pb-2.5 text-xs text-slate-400 leading-relaxed">
|
|
1265
|
-
Version <span class="text-slate-300">${esc(msg.latest)}</span> is available (installed: ${esc(msg.installed)}).
|
|
1266
|
-
</p>
|
|
1267
|
-
<div class="px-4 pb-3.5 flex items-center gap-2">
|
|
1268
|
-
<button class="update-btn flex-1 px-3 py-2 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors">Update</button>
|
|
1269
|
-
<button class="dismiss-btn px-3 py-2 text-xs text-slate-500 hover:text-slate-300 transition-colors">Later</button>
|
|
1270
|
-
</div>`;
|
|
1271
|
-
|
|
1272
|
-
const dismiss = () => {
|
|
1273
|
-
toast.style.opacity = '0';
|
|
1274
|
-
toast.style.transform = 'translateY(12px)';
|
|
1275
|
-
setTimeout(() => toast.remove(), 300);
|
|
1276
|
-
};
|
|
1277
|
-
|
|
1278
|
-
toast.querySelectorAll('.dismiss-btn').forEach(b => {
|
|
1279
|
-
b.onclick = () => { dismiss(); setTimeout(() => { remoteUpdateShown = false; }, 600000); };
|
|
1280
|
-
});
|
|
1281
|
-
|
|
1282
|
-
toast.querySelector('.update-btn').onclick = () => {
|
|
1283
|
-
dismiss();
|
|
1284
|
-
remoteUpdateShown = false;
|
|
1285
|
-
document.getElementById('remote-install-log').textContent = '';
|
|
1286
|
-
setRemotePane('installing');
|
|
1287
|
-
openRemoteModal();
|
|
1288
|
-
send({ type: 'remote.install' });
|
|
1289
|
-
};
|
|
1290
|
-
|
|
1291
|
-
document.body.appendChild(toast);
|
|
1292
|
-
requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; });
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
1332
|
// Button click
|
|
1296
1333
|
btnRemote.addEventListener('click', () => {
|
|
1297
1334
|
if (remoteModalOpen && remoteState !== 'paired') { closeRemoteModal(); return; }
|
|
1298
1335
|
if (remoteModalOpen) return; // paired — can't dismiss
|
|
1299
1336
|
if (!remoteInstalled) {
|
|
1300
|
-
|
|
1337
|
+
showRemoteIntro();
|
|
1301
1338
|
document.getElementById('remote-install-log').textContent = '';
|
|
1302
1339
|
openRemoteModal();
|
|
1303
1340
|
return;
|
|
1304
1341
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
send({ type: 'remote.pair' });
|
|
1310
|
-
} else {
|
|
1311
|
-
setRemotePane(remoteState === 'paired' ? 'active' : remoteState === 'waiting' ? 'qr' : 'connecting');
|
|
1312
|
-
openRemoteModal();
|
|
1313
|
-
}
|
|
1342
|
+
remotePreflight = { pending: true, statusSeen: false, updateSeen: false };
|
|
1343
|
+
setRemotePane('connecting');
|
|
1344
|
+
openRemoteModal();
|
|
1345
|
+
send({ type: 'remote.status' });
|
|
1314
1346
|
});
|
|
1315
1347
|
|
|
1316
1348
|
// Install button
|
package/sessions.js
CHANGED
|
@@ -101,19 +101,28 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
101
101
|
if (preset?.telemetryEnv) telemetry.watchSession(id, bin);
|
|
102
102
|
if (preset?.bridge === 'opencode') opencodeBridge.watchSession(id, cwd);
|
|
103
103
|
|
|
104
|
+
function injectRolePrompt() {
|
|
105
|
+
if (!session.pendingRolePrompt) return;
|
|
106
|
+
transcript.recordInjectedInput(id, session.pendingRolePrompt);
|
|
107
|
+
term.write(session.pendingRolePrompt);
|
|
108
|
+
setTimeout(() => term.write('\r'), 150);
|
|
109
|
+
console.log(`Session ${id.slice(0, 8)}: injected role prompt`);
|
|
110
|
+
delete session.pendingRolePrompt;
|
|
111
|
+
delete session._rolePromptTimer;
|
|
112
|
+
}
|
|
113
|
+
|
|
104
114
|
term.onData((data) => {
|
|
105
|
-
//
|
|
115
|
+
// Role prompts should be injected only when the agent is likely ready for
|
|
116
|
+
// input. For Codex, use the first OTLP startup event instead of a blind
|
|
117
|
+
// fixed startup delay; other agents keep the existing delayed path.
|
|
106
118
|
if (session.pendingRolePrompt && !session._rolePromptTimer) {
|
|
107
|
-
session.
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
delete session._rolePromptTimer;
|
|
115
|
-
}
|
|
116
|
-
}, 3000);
|
|
119
|
+
if (session.presetId === 'codex') {
|
|
120
|
+
if (telemetry.hasEvents(id)) injectRolePrompt();
|
|
121
|
+
} else {
|
|
122
|
+
session._rolePromptTimer = setTimeout(() => {
|
|
123
|
+
if (session.pendingRolePrompt) injectRolePrompt();
|
|
124
|
+
}, 3000);
|
|
125
|
+
}
|
|
117
126
|
}
|
|
118
127
|
session.chunks.push(data);
|
|
119
128
|
session.chunksSize += data.length;
|
|
@@ -363,7 +372,7 @@ function restart(msg, ws, cfg) {
|
|
|
363
372
|
}
|
|
364
373
|
|
|
365
374
|
const savedToken = s.sessionToken;
|
|
366
|
-
const { name, cwd, commandId, projectId } = s;
|
|
375
|
+
const { name, cwd, commandId, projectId, roleName, muted, lastPreview, lastActivityAt } = s;
|
|
367
376
|
|
|
368
377
|
activity.clear(id);
|
|
369
378
|
telemetry.clear(id);
|
|
@@ -380,6 +389,14 @@ function restart(msg, ws, cfg) {
|
|
|
380
389
|
return;
|
|
381
390
|
}
|
|
382
391
|
|
|
392
|
+
const next = sessions.get(id);
|
|
393
|
+
if (next) {
|
|
394
|
+
next.roleName = roleName || null;
|
|
395
|
+
next.muted = !!muted;
|
|
396
|
+
next.lastPreview = lastPreview || '';
|
|
397
|
+
next.lastActivityAt = lastActivityAt || null;
|
|
398
|
+
}
|
|
399
|
+
|
|
383
400
|
broadcast({ type: 'session.restarted', id, resumed: !!canResume });
|
|
384
401
|
}
|
|
385
402
|
|
package/transcript-builder.js
CHANGED
|
@@ -14,7 +14,7 @@ function cleanAgentText(presetId, text) {
|
|
|
14
14
|
out = out.replace(/\n\s*.*\(running stop hook\)[\s\S]*$/, '').trim();
|
|
15
15
|
out = out.replace(/\n\s*\?\s*for shortcuts[\s\S]*$/, '').trim();
|
|
16
16
|
out = out.replace(/\n\s*esc to interrupt[\s\S]*$/, '').trim();
|
|
17
|
-
out = out.replace(/\n\s*[✻✢✣✤✥✦✧]\s
|
|
17
|
+
out = out.replace(/\n\n\s*[✻✢✣✤✥✦✧][\s\S]*$/, '').trim();
|
|
18
18
|
}
|
|
19
19
|
return out;
|
|
20
20
|
}
|
package/transcript-parser.js
CHANGED
|
@@ -4,12 +4,39 @@ function parseTurns(presetId, lines, users) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
function parseLastAgentOnly(presetId, lines) {
|
|
7
|
+
if (presetId === 'claude-code') return parseLastClaudeAgentOnly(lines);
|
|
7
8
|
const turns = collapseAgentTurns((parsers[presetId] || (() => null))(lines, null));
|
|
8
9
|
if (!turns?.length) return null;
|
|
9
10
|
const last = [...turns].reverse().find(t => t.role === 'agent');
|
|
10
11
|
return last || null;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
function parseLastClaudeAgentOnly(lines) {
|
|
15
|
+
const userPromptRe = /^(?:[│ ]\s*)?[❯›]\s(.*)$/;
|
|
16
|
+
const agentRe = /^(?:[│ ]\s*)?[⏺•●]\s(.*)$/;
|
|
17
|
+
let promptIdx = -1;
|
|
18
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19
|
+
if (userPromptRe.test(lines[i])) { promptIdx = i; break; }
|
|
20
|
+
}
|
|
21
|
+
const upperBound = promptIdx >= 0 ? promptIdx : lines.length;
|
|
22
|
+
let start = -1;
|
|
23
|
+
for (let i = upperBound - 1; i >= 0; i--) {
|
|
24
|
+
if (agentRe.test(lines[i])) { start = i; break; }
|
|
25
|
+
}
|
|
26
|
+
if (start < 0) return null;
|
|
27
|
+
const first = lines[start].match(agentRe);
|
|
28
|
+
const turn = { role: 'agent', text: first[1] };
|
|
29
|
+
for (let i = start + 1; i < upperBound; i++) {
|
|
30
|
+
if (userPromptRe.test(lines[i]) || agentRe.test(lines[i])) break;
|
|
31
|
+
let cont = lines[i];
|
|
32
|
+
if (cont.startsWith('│ ')) cont = cont.slice(2);
|
|
33
|
+
else if (cont.startsWith(' ')) cont = cont.slice(2);
|
|
34
|
+
turn.text += '\n' + cont;
|
|
35
|
+
}
|
|
36
|
+
turn.text = turn.text.replace(/\n+$/, '');
|
|
37
|
+
return turn;
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
const parsers = {
|
|
14
41
|
'claude-code': (lines, users) => {
|
|
15
42
|
const known = users?.length ? new Set(users) : null;
|