clideck 1.29.1 → 1.29.3

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 CHANGED
@@ -1,101 +1,87 @@
1
- <img src="public/img/clideck-logo-icon.png" width="48" alt="clideck logo">
1
+ <p align="center">
2
+ <img src="public/img/clideck-logo-icon.png" width="64" alt="clideck logo">
3
+ </p>
2
4
 
3
- # clideck
5
+ <h1 align="center">clideck</h1>
4
6
 
5
- > **Formerly `termix-cli`** — if you arrived here from an old link, you're in the right place. The project has been renamed to **CliDeck**. Update your install: `npm install -g clideck`
7
+ <p align="center">
8
+ one screen for AI coding agents.
9
+ <br><br>
10
+ <a href="https://clideck.dev">Website</a> · <a href="https://docs.clideck.dev">Docs</a> · <a href="https://youtu.be/hICrtjGAeDk">Demo</a>
11
+ </p>
6
12
 
7
- Manage your AI agents like WhatsApp chats. Assign roles, let Autopilot route work between them, check in from your phone.
13
+ <p align="center">
14
+ <a href="https://github.com/rustykuntz/clideck/stargazers">
15
+ <img src="https://img.shields.io/github/stars/rustykuntz/clideck?style=social" alt="stars">
16
+ </a>
17
+ <a href="https://www.npmjs.com/package/clideck">
18
+ <img src="https://img.shields.io/npm/v/clideck" alt="npm version">
19
+ </a>
20
+ </p>
8
21
 
9
- [Documentation](https://docs.clideck.dev/) | [Video Demo](https://youtu.be/hICrtjGAeDk) | [Website](https://clideck.dev/)
22
+ <!-- TODO: Replace with a ~10 second GIF showing: open clideck,
23
+ sidebar with multiple agents across projects, click between them,
24
+ one working one idle. No narration needed. -->
10
25
 
11
- ![clideck dashboard](assets/clideck-themes.jpg)
26
+ <p align="center">
27
+ <img src="assets/clideck-themes.jpg" width="720" alt="clideck dashboard">
28
+ </p>
12
29
 
13
- You run Claude Code, Codex, Gemini CLI in separate terminals. You alt-tab between them, forget which one finished, lose sessions when you close the lid.
30
+ clideck is a local app for running multiple AI coding agents without juggling terminals. Claude Code, Codex, Gemini CLI, and OpenCode all live in one browser window with a chat-style sidebar, live status, message previews, session resume, and projects to keep things organized. an autopilot routes work between agents automatically, and an E2E encrypted mobile relay gives full control over all agents from a phone.
14
31
 
15
- clideck puts all your agents in one screen a sidebar with every session, live status, last message preview, and timestamps. Click a session, you're in its terminal. Exactly like switching between chats.
32
+ the main problem with using multiple agents is not starting them. it is managing them. terminals pile up, finished work gets missed, good sessions disappear after a restart. clideck does not sit in the middle rewriting prompts or output - it only watches lightweight status signals (OpenTelemetry) so it can tell which agent is working, which is idle, and which is waiting. everything runs locally, no data leaves your machine.
16
33
 
17
- Give each agent a role (Programmer, Reviewer, Product Manager), turn on Autopilot, and walk away — it routes output between agents automatically until the task is done or it needs you. Check progress from your phone with a QR scan.
34
+ ## Why this exists
18
35
 
19
- Native terminals. Your keystrokes go straight to the agent, nothing in between. clideck never reads your prompts or output.
36
+ Terminal multiplexers are great at panes. clideck is about conversations.
20
37
 
21
- ## Quick Start
38
+ A pane grid is flat. agent work usually is not. projects, roles, previews, timestamps, notifications, resume, and sometimes a bit of routing between specialists all fit more naturally into a chat app layout. it also maps naturally to mobile, so the same mental model works on desktop and phone.
22
39
 
23
- ```bash
24
- npx clideck
25
- ```
26
-
27
- Open [http://localhost:4000](http://localhost:4000). Click **+**, pick an agent and optionally a project and role, start working.
28
-
29
- New users get 3 built-in roles (Programmer, Reviewer, Product Manager) and 3 starter prompts in the prompt library.
30
-
31
- Or install globally:
40
+ ## Quick start
32
41
 
33
42
  ```bash
34
43
  npm install -g clideck
35
44
  clideck
36
45
  ```
37
46
 
38
- ## What You Get
39
-
40
- - **Roles** — define reusable agent identities (Programmer, Reviewer, PM) and assign them when creating sessions. Instructions are injected into the agent automatically.
41
- - **Autopilot** — project-level workflow routing. Watches your role-assigned agents, waits for them to finish, forwards output to the next specialist. Fingerprints each output, tracks handoff history, and guards against repeat loops. Supports 8 LLM providers (Anthropic, OpenAI, Google, Groq, xAI, Mistral, OpenRouter, Cerebras). Notifies you when work is complete or blocked.
42
- - **Mobile access** — check on your agents from your phone with a QR scan. E2E encrypted.
43
- - **Live working/idle status** — see which agent is thinking and which is waiting for you, without checking each terminal
44
- - **Session resume** — close clideck, reopen it tomorrow, pick up where you left off
45
- - **Notifications** — browser and sound alerts when an agent finishes or needs input
46
- - **Message previews** — latest output from each agent, right in the sidebar
47
- - **Projects** — group sessions by project with drag-and-drop
48
- - **Search** — find any session by name or scroll back through transcript content
49
- - **Prompt Library** — save reusable prompts, type `//` in any terminal to paste them
50
- - **Plugins** — full server + client API with hooks for input, output, status, transcript, and menus. Programmatic session control, toolbar and project actions, session pills, and a settings UI. Ships with Voice Input, Trim Clip, and Autopilot — or build your own.
51
- - **15 themes** — dark and light, plus custom theme support
52
-
53
- ## Mobile Access
54
-
55
- Start a task on your laptop, walk away, check progress from your phone. See who's working, who's idle, who needs input. Send messages, answer choice menus, browse conversation history, and resume sessions — all from the browser on your phone.
56
-
57
- Pair with one QR scan, no account needed. End-to-end encrypted with AES-256-GCM — the relay sees only opaque blobs. Your code never leaves your machines.
58
-
59
- Mobile access is provided by [`clideck-remote`](https://www.npmjs.com/package/clideck-remote), a separate optional package. Install it with `npm install -g clideck-remote`.
47
+ Open [localhost:4000](http://localhost:4000). Click **+**, pick an agent, start working.
60
48
 
61
- ## Supported Agents
49
+ Or just run it once with `npx clideck`. Works on macOS and Windows. Node 18+. Linux: untested - if you try it, [open an issue](https://github.com/rustykuntz/clideck/issues).
62
50
 
63
- clideck auto-detects whether each agent is working or idle:
51
+ ## What makes it useful
64
52
 
65
- | Agent | Status detection | Setup |
66
- |-------|-----------------|-------|
67
- | **Claude Code** | Automatic | Nothing to configure |
68
- | **Codex** | Automatic | One-click setup in clideck |
69
- | **Gemini CLI** | Automatic | One-click setup in clideck |
70
- | **OpenCode** | Via plugin bridge | One-click setup in clideck |
71
- | **Shell** | I/O activity only | None |
53
+ **Live status** - see which agent is working and which is waiting. Status detection for Claude Code, Codex, Gemini CLI, and OpenCode.
72
54
 
73
- Claude Code works out of the box. Other agents need a one-time setup that clideck walks you through.
55
+ **Session resume** - close the lid, reopen tomorrow, pick up where things left off. each agent's session ID is captured automatically.
74
56
 
75
- Minimum supported agent versions:
57
+ **Roles** - give agents reusable identities like programmer, reviewer, or product manager. prompts are injected automatically when a session starts.
76
58
 
77
- - Gemini CLI `v0.36.0+`
78
- - OpenAI Codex `v0.118.0+`
79
- - Claude Code `v2.1.90+`
80
- - OpenCode `v1.2.26+`
59
+ **Autopilot** - enable autopilot on a project, walk away. it watches for one agent to finish, hands the output to the next one, and keeps going until the work is done or blocked. this is the part that makes sleep possible. routes content verbatim, no rewriting or summarizing. fingerprints each output and tracks handoff history to guard against repeat loops. ~50 output tokens per routing decision. supports Anthropic, OpenAI, Google, Groq, xAI, Mistral, OpenRouter, Cerebras.
81
60
 
82
- ## How It Works
61
+ <p align="center">
62
+ <img src="assets/autopilot.gif" width="720" alt="Autopilot routing work between agents">
63
+ </p>
83
64
 
84
- Each agent runs in a real terminal (PTY) on your machine. clideck receives lightweight status signals via OpenTelemetry it knows *that* an agent is working, not *what* it's working on.
65
+ **Mobile remote** - the agents keep running on the local machine. status, prompts, history, and replies stay available from a phone while away. E2E encrypted, no account needed.
85
66
 
86
- Autopilot routes existing agent output between agents verbatim it does not rewrite or summarize the routed content.
67
+ **Native terminals** - each session opens into its real terminal. keys go straight to the agent, nothing sits in the middle.
87
68
 
88
- Everything runs locally. No data is collected, transmitted, or stored outside your machine.
69
+ ## Supported agents
89
70
 
90
- ## Platform Support
71
+ Claude Code, Codex, Gemini CLI, OpenCode, Shell, and any other terminal tool.
91
72
 
92
- Tested on **macOS** and **Windows**. Works in any modern browser. Linux: untested — if you try it, open an issue.
73
+ ## Also
93
74
 
94
- ## Documentation
75
+ - **Projects** - group sessions, drag and drop
76
+ - **Prompt library** - save reusable prompts, type `//` to paste
77
+ - **Search** - find sessions or scroll through transcripts
78
+ - **Plugins** - server + client API. ships with Voice Input, Trim Clip, and Autopilot. build your own
79
+ - **15 themes** - dark, light, or make your own
80
+ - **Notifications** - browser + sound alerts when agents finish
95
81
 
96
- Full setup guides, agent configuration, and plugin development:
82
+ ## Docs
97
83
 
98
- **[docs.clideck.dev](https://docs.clideck.dev/)**
84
+ Guides, agent setup, plugin development: **[docs.clideck.dev](https://docs.clideck.dev)**
99
85
 
100
86
  ## Acknowledgments
101
87
 
@@ -103,4 +89,4 @@ Built with [xterm.js](https://xtermjs.org/).
103
89
 
104
90
  ## License
105
91
 
106
- MIT see [LICENSE](LICENSE).
92
+ MIT - see [LICENSE](LICENSE).
Binary file
Binary file
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.29.1",
3
+ "version": "1.29.3",
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": {
@@ -9,7 +9,11 @@ export function init(pluginApi) {
9
9
  api.onMessage('started', (msg) => {
10
10
  activeProjects.add(msg.projectId);
11
11
  updateToggle(msg.projectId, true);
12
- api.toast('Autopilot started');
12
+ api.toast('Keep this browser tab active and prevent sleep while Autopilot runs.', {
13
+ title: 'Autopilot started',
14
+ type: 'warn',
15
+ duration: 5000,
16
+ });
13
17
  });
14
18
 
15
19
  api.onMessage('stopped', (msg) => {
@@ -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);
@@ -607,7 +611,12 @@ function openPrevSessionsMenu(anchorEl) {
607
611
  const menu = document.createElement('div');
608
612
  menu.className = 'fixed z-[400] min-w-[160px] bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 py-1';
609
613
 
610
- const dormantIds = state.resumable.filter(s => !s.projectId).map(s => s.id);
614
+ // Clear exactly the dormant sessions currently rendered in "Previous Sessions".
615
+ // This keeps the action aligned with the UI even if a session has a stale projectId
616
+ // that no longer resolves to a real project group.
617
+ const dormantIds = [...document.querySelectorAll('#resumable-section [data-resumable-id]')]
618
+ .map(el => el.dataset.resumableId)
619
+ .filter(Boolean);
611
620
 
612
621
  menu.innerHTML = `
613
622
  <button class="pv-action flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 transition-colors text-left" data-action="clear-dormant">
@@ -1046,6 +1055,9 @@ let remoteModalOpen = false;
1046
1055
  let remoteStatusPoll = null;
1047
1056
  let remoteConnectedAt = null;
1048
1057
  let remoteStatsTimer = null;
1058
+ let remoteUpdateInfo = null;
1059
+ let remotePreflight = null;
1060
+ let remoteLastStatus = null;
1049
1061
 
1050
1062
  function startRemotePoll() {
1051
1063
  stopRemotePoll();
@@ -1065,6 +1077,66 @@ function setRemotePane(pane) {
1065
1077
  }
1066
1078
  }
1067
1079
 
1080
+ function showRemoteIntro(opts = {}) {
1081
+ const title = document.getElementById('remote-intro-title');
1082
+ const text = document.getElementById('remote-intro-text');
1083
+ const foot = document.getElementById('remote-intro-foot');
1084
+ const btn = document.getElementById('remote-add');
1085
+ title.textContent = opts.title || 'CliDeck Mobile Remote';
1086
+ text.textContent = opts.text || 'Control your AI agents from your phone. See live status, send messages, and get notifications — all end-to-end encrypted.';
1087
+ foot.innerHTML = opts.foot || 'Installs the <code class="text-slate-500">clideck-remote</code> package via npm';
1088
+ btn.textContent = opts.button || 'Add to CliDeck';
1089
+ setRemotePane('intro');
1090
+ }
1091
+
1092
+ function showRemoteUpdateRequired() {
1093
+ showRemoteIntro({
1094
+ title: 'Update Required',
1095
+ text: `Version ${remoteUpdateInfo.latest} is available. Update CliDeck Remote to continue with mobile pairing on this machine.`,
1096
+ foot: `Installed: <code class="text-slate-500">${esc(remoteUpdateInfo.installed)}</code> · Latest: <code class="text-slate-500">${esc(remoteUpdateInfo.latest)}</code>`,
1097
+ button: 'Update to Continue',
1098
+ });
1099
+ }
1100
+
1101
+ function finishRemotePreflight() {
1102
+ if (!remotePreflight?.pending || !remotePreflight.statusSeen || !remotePreflight.updateSeen) return;
1103
+ remotePreflight = null;
1104
+ if (!remoteInstalled) {
1105
+ showRemoteIntro();
1106
+ return;
1107
+ }
1108
+ if (remoteUpdateInfo?.available) {
1109
+ showRemoteUpdateRequired();
1110
+ return;
1111
+ }
1112
+ if (remoteState === 'idle') {
1113
+ remoteState = 'connecting';
1114
+ setRemotePane('connecting');
1115
+ send({ type: 'remote.pair' });
1116
+ return;
1117
+ }
1118
+ if (remoteState === 'paired' && remoteLastStatus?.paired) {
1119
+ setRemotePane('active');
1120
+ setRemoteLock(true);
1121
+ startRemoteStats(remoteLastStatus.pairedAt);
1122
+ const deviceEl = document.getElementById('remote-device-info');
1123
+ if (deviceEl) {
1124
+ const parts = [remoteLastStatus.deviceName, remoteLastStatus.location].filter(Boolean);
1125
+ deviceEl.textContent = parts.length ? parts.join(' · ') : '';
1126
+ }
1127
+ return;
1128
+ }
1129
+ if (remoteState === 'waiting' && remoteLastStatus?.connected && remoteLastStatus?.url) {
1130
+ document.getElementById('remote-url-box').textContent = remoteLastStatus.url;
1131
+ const qrImg = document.getElementById('remote-qr-img');
1132
+ if (remoteLastStatus.qr && remoteLastStatus.qr.startsWith('data:')) { qrImg.src = remoteLastStatus.qr; qrImg.classList.remove('hidden'); }
1133
+ else qrImg.classList.add('hidden');
1134
+ setRemotePane('qr');
1135
+ return;
1136
+ }
1137
+ setRemotePane(remoteState === 'paired' ? 'active' : remoteState === 'waiting' ? 'qr' : 'connecting');
1138
+ }
1139
+
1068
1140
  function openRemoteModal() {
1069
1141
  remoteModalOpen = true;
1070
1142
  remoteModal.classList.remove('hidden');
@@ -1153,10 +1225,12 @@ function updateRemoteButton() {
1153
1225
  }
1154
1226
 
1155
1227
  function handleRemoteStatus(msg) {
1228
+ remoteLastStatus = msg;
1156
1229
  remoteInstalled = !!msg.installed;
1157
1230
  state.remoteVersion = msg.version || (msg.installed ? null : 'not installed');
1158
1231
  updateVersionFooter();
1159
1232
  const wasPaired = remoteState === 'paired';
1233
+ const preflighting = !!remotePreflight?.pending;
1160
1234
  if (!msg.installed) {
1161
1235
  remoteState = 'idle';
1162
1236
  stopRemotePoll();
@@ -1165,7 +1239,7 @@ function handleRemoteStatus(msg) {
1165
1239
  const wasFresh = remoteState !== 'paired';
1166
1240
  remoteState = 'paired';
1167
1241
  if (!remoteStatusPoll) startRemotePoll();
1168
- if (wasFresh) {
1242
+ if (wasFresh && !preflighting) {
1169
1243
 
1170
1244
  setRemotePane('active');
1171
1245
  setRemoteLock(true);
@@ -1185,13 +1259,20 @@ function handleRemoteStatus(msg) {
1185
1259
  if (msg.qr && msg.qr.startsWith('data:')) { qrImg.src = msg.qr; qrImg.classList.remove('hidden'); }
1186
1260
  else qrImg.classList.add('hidden');
1187
1261
  startRemotePoll();
1188
- if (remoteModalOpen) setRemotePane('qr');
1262
+ if (!preflighting && remoteModalOpen) setRemotePane('qr');
1189
1263
  } else {
1190
1264
  remoteState = 'idle';
1191
1265
  stopRemotePoll();
1192
1266
  if (wasPaired) { stopRemoteStats(); setRemoteLock(false); }
1193
1267
  }
1268
+ if (remoteUpdateInfo?.available && remoteModalOpen) {
1269
+ showRemoteUpdateRequired();
1270
+ }
1194
1271
  updateRemoteButton();
1272
+ if (remotePreflight?.pending) {
1273
+ remotePreflight.statusSeen = true;
1274
+ finishRemotePreflight();
1275
+ }
1195
1276
  }
1196
1277
 
1197
1278
  function handleRemotePaired(msg) {
@@ -1201,9 +1282,18 @@ function handleRemotePaired(msg) {
1201
1282
  const qrImg = document.getElementById('remote-qr-img');
1202
1283
  if (msg.qr && msg.qr.startsWith('data:')) { qrImg.src = msg.qr; qrImg.classList.remove('hidden'); }
1203
1284
  else qrImg.classList.add('hidden');
1204
- setRemotePane('qr');
1205
1285
  updateRemoteButton();
1206
1286
  startRemotePoll();
1287
+ if (remoteUpdateInfo?.available && remoteModalOpen) {
1288
+ showRemoteUpdateRequired();
1289
+ return;
1290
+ }
1291
+ if (remotePreflight?.pending) {
1292
+ remotePreflight.statusSeen = true;
1293
+ finishRemotePreflight();
1294
+ return;
1295
+ }
1296
+ setRemotePane('qr');
1207
1297
  }
1208
1298
 
1209
1299
  function handleRemoteUnpaired() {
@@ -1232,6 +1322,7 @@ function appendInstallLog(text) {
1232
1322
  function handleInstallDone(success) {
1233
1323
  if (success) {
1234
1324
  remoteInstalled = true;
1325
+ remoteUpdateInfo = null;
1235
1326
  // Installed — go straight to pairing
1236
1327
  remoteState = 'connecting';
1237
1328
  setRemotePane('connecting');
@@ -1243,74 +1334,20 @@ function handleInstallDone(success) {
1243
1334
  }
1244
1335
  }
1245
1336
 
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
1337
  // Button click
1296
1338
  btnRemote.addEventListener('click', () => {
1297
1339
  if (remoteModalOpen && remoteState !== 'paired') { closeRemoteModal(); return; }
1298
1340
  if (remoteModalOpen) return; // paired — can't dismiss
1299
1341
  if (!remoteInstalled) {
1300
- setRemotePane('intro');
1342
+ showRemoteIntro();
1301
1343
  document.getElementById('remote-install-log').textContent = '';
1302
1344
  openRemoteModal();
1303
1345
  return;
1304
1346
  }
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
- }
1347
+ remotePreflight = { pending: true, statusSeen: false, updateSeen: false };
1348
+ setRemotePane('connecting');
1349
+ openRemoteModal();
1350
+ send({ type: 'remote.status' });
1314
1351
  });
1315
1352
 
1316
1353
  // Install button
@@ -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;