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 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
- if (remoteUpdateCache.available) ws.send(JSON.stringify({ type: 'remote.update', ...remoteUpdateCache }));
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; } catch { return; }
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) return;
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
- if (remoteUpdateCache.available) ws.send(JSON.stringify({ type: 'remote.update', ...remoteUpdateCache }));
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
- const has = (arr, path) => arr?.some(h => h.hooks?.some(x => x.command?.includes('claude-hook.js') && x.command?.includes(` ${path}`)));
120
- detected = has(hooks.UserPromptSubmit, 'start') && has(hooks.Stop, 'stop') && has(hooks.StopFailure, 'stop')
121
- && has(hooks.PreToolUse, 'menu')
122
- && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.command?.includes('claude-hook.js') && x.command?.includes(' idle')));
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 = content.includes('[otel]') && /^\s*notify\s*=.*notify-helper/m.test(content) && content.includes(`localhost:${port}`);
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
- const has = (arr, route) => arr?.some(h => h.hooks?.some(x => x.command?.includes('gemini-hook.js') && x.command?.includes(` ${route}`)));
136
- detected = has(hooks.BeforeAgent, 'start') && has(hooks.AfterAgent, 'stop') && has(hooks.SessionEnd, 'stop') && has(hooks.BeforeTool, 'menu');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.29.0",
3
+ "version": "1.29.2",
4
4
  "description": "One screen for all your AI coding agents — run, monitor, and manage multiple CLI agents from a single browser tab",
5
5
  "main": "server.js",
6
6
  "bin": {
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
- 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: !!sessionStatus.get(id) };
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: !!sessionStatus.get(id),
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
- showRemoteUpdateToast(msg);
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
- setRemotePane('intro');
1337
+ showRemoteIntro();
1301
1338
  document.getElementById('remote-install-log').textContent = '';
1302
1339
  openRemoteModal();
1303
1340
  return;
1304
1341
  }
1305
- if (remoteState === 'idle') {
1306
- remoteState = 'connecting';
1307
- setRemotePane('connecting');
1308
- openRemoteModal();
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
- // Inject role prompt once after agent starts producing output
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._rolePromptTimer = setTimeout(() => {
108
- if (session.pendingRolePrompt) {
109
- transcript.recordInjectedInput(id, session.pendingRolePrompt);
110
- term.write(session.pendingRolePrompt);
111
- setTimeout(() => term.write('\r'), 150);
112
- console.log(`Session ${id.slice(0, 8)}: injected role prompt`);
113
- delete session.pendingRolePrompt;
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
 
@@ -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+[^\n]*$/, '').trim();
17
+ out = out.replace(/\n\n\s*[✻✢✣✤✥✦✧][\s\S]*$/, '').trim();
18
18
  }
19
19
  return out;
20
20
  }
@@ -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;