claude-relay 2.3.0 → 2.4.0

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.
Files changed (54) hide show
  1. package/README.md +21 -5
  2. package/bin/cli.js +214 -9
  3. package/lib/cli-sessions.js +270 -0
  4. package/lib/config.js +3 -2
  5. package/lib/daemon.js +45 -1
  6. package/lib/pages.js +8 -1
  7. package/lib/project.js +121 -12
  8. package/lib/public/app.js +411 -87
  9. package/lib/public/css/base.css +41 -7
  10. package/lib/public/css/diff.css +6 -6
  11. package/lib/public/css/filebrowser.css +62 -52
  12. package/lib/public/css/highlight.css +144 -0
  13. package/lib/public/css/input.css +11 -9
  14. package/lib/public/css/menus.css +82 -23
  15. package/lib/public/css/messages.css +183 -35
  16. package/lib/public/css/overlays.css +166 -50
  17. package/lib/public/css/rewind.css +17 -17
  18. package/lib/public/css/sidebar.css +210 -137
  19. package/lib/public/index.html +75 -42
  20. package/lib/public/modules/filebrowser.js +2 -1
  21. package/lib/public/modules/markdown.js +10 -10
  22. package/lib/public/modules/notifications.js +38 -1
  23. package/lib/public/modules/sidebar.js +109 -31
  24. package/lib/public/modules/terminal.js +84 -23
  25. package/lib/public/modules/theme.js +622 -0
  26. package/lib/public/modules/tools.js +247 -4
  27. package/lib/public/modules/utils.js +21 -5
  28. package/lib/public/style.css +1 -0
  29. package/lib/sdk-bridge.js +95 -0
  30. package/lib/server.js +45 -3
  31. package/lib/sessions.js +16 -3
  32. package/lib/themes/ayu-light.json +9 -0
  33. package/lib/themes/catppuccin-latte.json +9 -0
  34. package/lib/themes/catppuccin-mocha.json +9 -0
  35. package/lib/themes/claude-light.json +9 -0
  36. package/lib/themes/claude.json +9 -0
  37. package/lib/themes/dracula.json +9 -0
  38. package/lib/themes/everforest-light.json +9 -0
  39. package/lib/themes/everforest.json +9 -0
  40. package/lib/themes/github-light.json +9 -0
  41. package/lib/themes/gruvbox-dark.json +9 -0
  42. package/lib/themes/gruvbox-light.json +9 -0
  43. package/lib/themes/monokai.json +9 -0
  44. package/lib/themes/nord-light.json +9 -0
  45. package/lib/themes/nord.json +9 -0
  46. package/lib/themes/one-dark.json +9 -0
  47. package/lib/themes/one-light.json +9 -0
  48. package/lib/themes/rose-pine-dawn.json +9 -0
  49. package/lib/themes/rose-pine.json +9 -0
  50. package/lib/themes/solarized-dark.json +9 -0
  51. package/lib/themes/solarized-light.json +9 -0
  52. package/lib/themes/tokyo-night-light.json +9 -0
  53. package/lib/themes/tokyo-night.json +9 -0
  54. package/package.json +2 -1
package/lib/public/app.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
2
  import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal } from './modules/markdown.js';
4
- import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline } from './modules/sidebar.js';
4
+ import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList } from './modules/sidebar.js';
5
5
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid } from './modules/rewind.js';
6
6
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
7
7
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands } from './modules/input.js';
8
8
  import { initQrCode } from './modules/qrcode.js';
9
9
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer } from './modules/filebrowser.js';
10
10
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed } from './modules/terminal.js';
11
- import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools } from './modules/tools.js';
11
+ import { initTheme, getThemeColor, getComputedVar, onThemeChange } from './modules/theme.js';
12
+ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
12
13
 
13
14
  // --- Base path for multi-project routing ---
14
15
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -21,7 +22,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
21
22
  var inputEl = $("input");
22
23
  var sendBtn = $("send-btn");
23
24
  var statusDot = $("status-dot");
24
- var projectNameEl = $("project-name");
25
+ var headerTitleEl = $("header-title");
26
+ var headerRenameBtn = $("header-rename-btn");
25
27
  var slashMenu = $("slash-menu");
26
28
  var sidebar = $("sidebar");
27
29
  var sidebarOverlay = $("sidebar-overlay");
@@ -36,25 +38,21 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
36
38
  var connectVerbEl = $("connect-verb");
37
39
  var connectStatusEl = $("connect-status");
38
40
 
39
- // --- Project Switcher ---
40
- var projectSwitcher = $("project-switcher");
41
- var projectSwitcherBtn = $("project-switcher-btn");
42
- var projectDropdown = $("project-dropdown");
43
- var projectDropdownList = $("project-dropdown-list");
44
- var projectSwitcherName = $("project-switcher-name");
45
- var projectSwitcherCount = $("project-switcher-count");
41
+ // --- Project List ---
42
+ var projectListSection = $("project-list-section");
43
+ var projectListEl = $("project-list");
44
+ var projectListAddBtn = $("project-list-add");
46
45
  var projectHint = $("project-hint");
47
46
  var projectHintDismiss = $("project-hint-dismiss");
48
47
  var cachedProjects = [];
49
48
  var cachedProjectCount = 0;
50
49
  var currentSlug = slugMatch ? slugMatch[1] : null;
51
50
 
52
- function updateProjectSwitcher(msg) {
51
+ function updateProjectList(msg) {
53
52
  if (typeof msg.projectCount === "number") cachedProjectCount = msg.projectCount;
54
53
  if (msg.projects) cachedProjects = msg.projects;
55
54
  var count = cachedProjectCount || 0;
56
- projectSwitcherName.textContent = projectName || "Project";
57
- projectSwitcherCount.textContent = count ? count + (count === 1 ? " project" : " projects") : "";
55
+ renderProjectList();
58
56
  if (count === 1 && projectHint) {
59
57
  try {
60
58
  if (!localStorage.getItem("claude-relay-project-hint-dismissed")) {
@@ -66,14 +64,15 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
66
64
  }
67
65
  }
68
66
 
69
- function renderProjectDropdown() {
70
- projectDropdownList.innerHTML = "";
67
+ function renderProjectList() {
68
+ if (!projectListEl) return;
69
+ projectListEl.innerHTML = "";
71
70
  for (var i = 0; i < cachedProjects.length; i++) {
72
71
  var p = cachedProjects[i];
73
72
  var isCurrent = p.slug === currentSlug;
74
73
  var displayName = p.title || p.project;
75
74
  var item = document.createElement("a");
76
- item.className = "project-dropdown-item" + (isCurrent ? " current" : "");
75
+ item.className = "project-list-item" + (isCurrent ? " current" : "");
77
76
  item.href = "/p/" + p.slug + "/";
78
77
 
79
78
  var indicator = document.createElement("span");
@@ -85,60 +84,35 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
85
84
  name.textContent = displayName;
86
85
  item.appendChild(name);
87
86
 
88
- var count = document.createElement("span");
89
- count.className = "pd-count";
90
- count.textContent = p.sessions;
91
- item.appendChild(count);
92
-
93
- projectDropdownList.appendChild(item);
94
- }
87
+ var removeBtn = document.createElement("button");
88
+ removeBtn.className = "pd-remove";
89
+ removeBtn.type = "button";
90
+ removeBtn.title = "Remove project";
91
+ removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
92
+ removeBtn.dataset.slug = p.slug;
93
+ removeBtn.dataset.name = displayName;
94
+ removeBtn.addEventListener("click", function (e) {
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ var s = this.dataset.slug;
98
+ var n = this.dataset.name;
99
+ confirmRemoveProject(s, n);
100
+ });
101
+ item.appendChild(removeBtn);
95
102
 
96
- // Add project button logic
97
- var addBtn = document.getElementById("project-dropdown-add");
98
- if (addBtn) {
99
- addBtn.onclick = function () {
100
- closeProjectDropdown();
101
- var hint = document.getElementById("project-hint");
102
- if (hint) {
103
- hint.classList.remove("hidden");
104
- try { localStorage.removeItem("claude-relay-project-hint-dismissed"); } catch (e) {}
105
- }
106
- };
103
+ projectListEl.appendChild(item);
107
104
  }
108
105
  refreshIcons();
109
106
  }
110
107
 
111
- function toggleProjectDropdown() {
112
- if (projectDropdown.classList.contains("hidden")) {
113
- renderProjectDropdown();
114
- projectDropdown.classList.remove("hidden");
115
- projectSwitcher.classList.add("open");
116
- } else {
117
- closeProjectDropdown();
118
- }
119
- }
120
-
121
- function closeProjectDropdown() {
122
- projectDropdown.classList.add("hidden");
123
- projectSwitcher.classList.remove("open");
124
- }
125
-
126
- if (projectSwitcherBtn) {
127
- projectSwitcherBtn.addEventListener("click", function (e) {
128
- e.stopPropagation();
129
- toggleProjectDropdown();
108
+ if (projectListAddBtn) {
109
+ projectListAddBtn.addEventListener("click", function () {
110
+ openAddProjectModal();
130
111
  });
131
112
  }
132
113
 
133
- document.addEventListener("click", function (e) {
134
- if (projectSwitcher && !projectSwitcher.contains(e.target)) {
135
- closeProjectDropdown();
136
- }
137
- });
138
-
139
114
  document.addEventListener("keydown", function (e) {
140
115
  if (e.key === "Escape") {
141
- closeProjectDropdown();
142
116
  closeImageModal();
143
117
  }
144
118
  });
@@ -185,6 +159,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
185
159
  // isComposing -> modules/input.js
186
160
  var reconnectTimer = null;
187
161
  var reconnectDelay = 1000;
162
+ var disconnectNotifTimer = null;
163
+ var disconnectNotifShown = false;
188
164
  var activityEl = null;
189
165
  var currentMsgEl = null;
190
166
  var currentFullText = "";
@@ -215,6 +191,45 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
215
191
 
216
192
  // builtinCommands -> modules/input.js
217
193
 
194
+ // --- Header session rename ---
195
+ if (headerRenameBtn) {
196
+ headerRenameBtn.addEventListener("click", function () {
197
+ if (!activeSessionId) return;
198
+ var currentText = headerTitleEl.textContent;
199
+ var input = document.createElement("input");
200
+ input.type = "text";
201
+ input.className = "header-rename-input";
202
+ input.value = currentText;
203
+ headerTitleEl.style.display = "none";
204
+ headerRenameBtn.style.display = "none";
205
+ headerTitleEl.parentNode.insertBefore(input, headerTitleEl.nextSibling);
206
+ input.focus();
207
+ input.select();
208
+
209
+ function commit() {
210
+ var newTitle = input.value.trim();
211
+ if (newTitle && newTitle !== currentText && ws && ws.readyState === 1) {
212
+ ws.send(JSON.stringify({ type: "rename_session", id: activeSessionId, title: newTitle }));
213
+ headerTitleEl.textContent = newTitle;
214
+ }
215
+ input.remove();
216
+ headerTitleEl.style.display = "";
217
+ headerRenameBtn.style.display = "";
218
+ }
219
+
220
+ input.addEventListener("keydown", function (e) {
221
+ if (e.key === "Enter") { e.preventDefault(); commit(); }
222
+ if (e.key === "Escape") {
223
+ e.preventDefault();
224
+ input.remove();
225
+ headerTitleEl.style.display = "";
226
+ headerRenameBtn.style.display = "";
227
+ }
228
+ });
229
+ input.addEventListener("blur", commit);
230
+ });
231
+ }
232
+
218
233
  // --- Confirm modal ---
219
234
  var confirmModal = $("confirm-modal");
220
235
  var confirmText = $("confirm-text");
@@ -265,6 +280,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
265
280
  addSystemMessage: addSystemMessage,
266
281
  });
267
282
 
283
+ // --- Theme (module) ---
284
+ initTheme();
285
+
268
286
  // --- Sidebar (module) ---
269
287
  initSidebar({
270
288
  $: $,
@@ -280,6 +298,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
280
298
  hamburgerBtn: hamburgerBtn,
281
299
  newSessionBtn: newSessionBtn,
282
300
  resumeSessionBtn: resumeSessionBtn,
301
+ headerTitleEl: headerTitleEl,
283
302
  showConfirm: showConfirm,
284
303
  onFilesTabOpen: function () { loadRootDirectory(); },
285
304
  });
@@ -337,9 +356,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
337
356
  ];
338
357
 
339
358
  var CELL = 12;
340
- var accent = "#DA7756";
341
- var eye = "#2F2E2B";
342
- var antenna = "#E8E5DE";
359
+ var accent = getThemeColor("base09");
360
+ var eye = getThemeColor("base00");
361
+ var antenna = getThemeColor("base06");
343
362
 
344
363
  for (var r = 0; r < grid.length; r++) {
345
364
  for (var c = 0; c < grid[r].length; c++) {
@@ -357,6 +376,23 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
357
376
  }
358
377
  })();
359
378
 
379
+ // Update pixel mascot colors when theme changes
380
+ onThemeChange(function () {
381
+ var newAccent = getThemeColor("base09");
382
+ var newEye = getThemeColor("base00");
383
+ var newAntenna = getThemeColor("base06");
384
+ for (var i = 0; i < pixelBlocks.length; i++) {
385
+ var el = pixelBlocks[i];
386
+ var bg = el.style.background;
387
+ if (bg === accent || bg === newAccent) el.style.background = newAccent;
388
+ else if (bg === eye || bg === newEye) el.style.background = newEye;
389
+ else if (bg === antenna || bg === newAntenna) el.style.background = newAntenna;
390
+ }
391
+ accent = newAccent;
392
+ eye = newEye;
393
+ antenna = newAntenna;
394
+ });
395
+
360
396
  function pixelScatter() {
361
397
  stopSpark();
362
398
  for (var i = 0; i < pixelBlocks.length; i++) {
@@ -396,7 +432,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
396
432
  antennaBlocks[i].style.background = "#FFF";
397
433
  antennaBlocks[i].style.boxShadow = "0 0 6px 2px rgba(255,255,255,0.6)";
398
434
  } else {
399
- antennaBlocks[i].style.background = "#E8E5DE";
435
+ antennaBlocks[i].style.background = antenna;
400
436
  antennaBlocks[i].style.boxShadow = "none";
401
437
  }
402
438
  }
@@ -411,7 +447,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
411
447
  sparkTimer = null;
412
448
  }
413
449
  for (var i = 0; i < antennaBlocks.length; i++) {
414
- antennaBlocks[i].style.background = "#E8E5DE";
450
+ antennaBlocks[i].style.background = antenna;
415
451
  antennaBlocks[i].style.boxShadow = "none";
416
452
  }
417
453
  }
@@ -456,7 +492,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
456
492
  if (xhr.status === 200) faviconSvg = xhr.responseText;
457
493
  else return;
458
494
  }
459
- var svg = faviconSvg.replace(/fill="#57AB5A"/, 'fill="' + bgColor + '"');
495
+ var svg = faviconSvg.replace(/fill="#57AB5A"/g, 'fill="' + bgColor + '"');
460
496
  faviconLink.href = "data:image/svg+xml," + encodeURIComponent(svg);
461
497
  }
462
498
 
@@ -482,9 +518,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
482
518
  ioTimer = setTimeout(function () { statusDot.classList.remove("io"); }, 60);
483
519
 
484
520
  // Blink favicon: dim then restore
485
- updateFavicon("#3D6B3E");
521
+ updateFavicon(getComputedVar("--sidebar-bg"));
486
522
  clearTimeout(faviconIoTimer);
487
- faviconIoTimer = setTimeout(function () { updateFavicon("#57AB5A"); }, 60);
523
+ faviconIoTimer = setTimeout(function () { updateFavicon(getComputedVar("--success")); }, 60);
488
524
  }
489
525
 
490
526
  // --- Urgent favicon blink (permission / ask user) ---
@@ -493,7 +529,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
493
529
  function startUrgentBlink() {
494
530
  if (urgentBlinkTimer) return;
495
531
  savedTitle = document.title;
496
- var colors = ["#DA7756", "#57AB5A", "#DA7756", "#E8E5DE", "#DA7756", "#57AB5A"];
532
+ var colors = [getComputedVar("--accent"), getComputedVar("--success"), getComputedVar("--accent"), getComputedVar("--text"), getComputedVar("--accent"), getComputedVar("--success")];
497
533
  var tick = 0;
498
534
  urgentBlinkTimer = setInterval(function () {
499
535
  updateFavicon(colors[tick % colors.length]);
@@ -505,7 +541,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
505
541
  if (!urgentBlinkTimer) return;
506
542
  clearInterval(urgentBlinkTimer);
507
543
  urgentBlinkTimer = null;
508
- updateFavicon("#57AB5A");
544
+ updateFavicon(getComputedVar("--success"));
509
545
  if (savedTitle) document.title = savedTitle;
510
546
  savedTitle = null;
511
547
  }
@@ -520,12 +556,12 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
520
556
  setSendBtnMode("send");
521
557
  connectOverlay.classList.add("hidden");
522
558
  stopVerbCycle();
523
- updateFavicon("#57AB5A");
559
+ updateFavicon(getComputedVar("--success"));
524
560
  } else if (status === "processing") {
525
561
  statusDot.classList.add("processing");
526
562
  processing = true;
527
563
  setSendBtnMode("stop");
528
- updateFavicon("#57AB5A");
564
+ updateFavicon(getComputedVar("--success"));
529
565
  } else {
530
566
  connected = false;
531
567
  sendBtn.disabled = true;
@@ -533,7 +569,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
533
569
  connectStatusEl.textContent = "Reconnecting...";
534
570
  startVerbCycle();
535
571
  startPixelAnim();
536
- updateFavicon("#E5534B");
572
+ updateFavicon(getComputedVar("--error"));
537
573
  }
538
574
  }
539
575
 
@@ -974,6 +1010,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
974
1010
  div.dataset.turn = ++turnCounter;
975
1011
  var bubble = document.createElement("div");
976
1012
  bubble.className = "bubble";
1013
+ bubble.dir = "auto";
977
1014
 
978
1015
  if (images && images.length > 0) {
979
1016
  var imgRow = document.createElement("div");
@@ -1024,7 +1061,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1024
1061
  currentMsgEl = document.createElement("div");
1025
1062
  currentMsgEl.className = "msg-assistant";
1026
1063
  currentMsgEl.dataset.turn = turnCounter;
1027
- currentMsgEl.innerHTML = '<div class="md-content"></div>';
1064
+ currentMsgEl.innerHTML = '<div class="md-content" dir="auto"></div>';
1028
1065
  addToMessages(currentMsgEl);
1029
1066
  currentFullText = "";
1030
1067
  }
@@ -1101,6 +1138,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1101
1138
  if (currentFullText) {
1102
1139
  addCopyHandler(currentMsgEl, currentFullText);
1103
1140
  }
1141
+ // Assistant text appeared, so break the current tool group
1142
+ closeToolGroup();
1104
1143
  }
1105
1144
  currentMsgEl = null;
1106
1145
  currentFullText = "";
@@ -1164,8 +1203,13 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1164
1203
 
1165
1204
  ws.onopen = function () {
1166
1205
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
1167
- // Local notification on reconnect (only if not focused)
1168
- if (wasConnected && !document.hasFocus() && "serviceWorker" in navigator) {
1206
+ // Cancel pending "connection lost" notification if reconnected quickly
1207
+ if (disconnectNotifTimer) {
1208
+ clearTimeout(disconnectNotifTimer);
1209
+ disconnectNotifTimer = null;
1210
+ }
1211
+ // Only show "restored" notification if "lost" was actually shown
1212
+ if (wasConnected && disconnectNotifShown && !document.hasFocus() && "serviceWorker" in navigator) {
1169
1213
  navigator.serviceWorker.ready.then(function (reg) {
1170
1214
  reg.showNotification("Claude Relay", {
1171
1215
  body: "Server connection restored",
@@ -1173,6 +1217,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1173
1217
  });
1174
1218
  }).catch(function () {});
1175
1219
  }
1220
+ disconnectNotifShown = false;
1176
1221
  wasConnected = true;
1177
1222
  setStatus("connected");
1178
1223
  reconnectDelay = 1000;
@@ -1198,14 +1243,20 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1198
1243
  setStatus("disconnected");
1199
1244
  processing = false;
1200
1245
  setActivity(null);
1201
- // Local notification when connection drops (only if not focused)
1202
- if (!document.hasFocus() && "serviceWorker" in navigator) {
1203
- navigator.serviceWorker.ready.then(function (reg) {
1204
- reg.showNotification("Claude Relay", {
1205
- body: "Server connection lost",
1206
- tag: "claude-disconnect",
1207
- });
1208
- }).catch(function () {});
1246
+ // Delay "connection lost" notification by 5s to suppress brief disconnects
1247
+ if (!disconnectNotifTimer) {
1248
+ disconnectNotifTimer = setTimeout(function () {
1249
+ disconnectNotifTimer = null;
1250
+ disconnectNotifShown = true;
1251
+ if (!document.hasFocus() && "serviceWorker" in navigator) {
1252
+ navigator.serviceWorker.ready.then(function (reg) {
1253
+ reg.showNotification("Claude Relay", {
1254
+ body: "Server connection lost",
1255
+ tag: "claude-disconnect",
1256
+ });
1257
+ }).catch(function () {});
1258
+ }
1259
+ }, 5000);
1209
1260
  }
1210
1261
  scheduleReconnect();
1211
1262
  };
@@ -1242,6 +1293,16 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1242
1293
  break;
1243
1294
 
1244
1295
  case "history_done":
1296
+ // Render + finalize any incomplete turn from the replayed history
1297
+ if (currentMsgEl && currentFullText) {
1298
+ var replayContentEl = currentMsgEl.querySelector(".md-content");
1299
+ if (replayContentEl) {
1300
+ replayContentEl.innerHTML = renderMarkdown(currentFullText);
1301
+ }
1302
+ }
1303
+ markAllToolsDone();
1304
+ finalizeAssistantBlock();
1305
+ scrollToBottom();
1245
1306
  var pendingQuery = getActiveSearchQuery();
1246
1307
  if (pendingQuery) {
1247
1308
  requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
@@ -1256,6 +1317,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1256
1317
  target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
1257
1318
  }
1258
1319
  if (target) {
1320
+ // Auto-expand parent tool group if collapsed
1321
+ var parentGroup = target.closest(".tool-group");
1322
+ if (parentGroup) parentGroup.classList.remove("collapsed");
1259
1323
  target.scrollIntoView({ behavior: "smooth", block: "center" });
1260
1324
  target.classList.add("message-blink");
1261
1325
  setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
@@ -1267,7 +1331,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1267
1331
  case "info":
1268
1332
  projectName = msg.project || msg.cwd;
1269
1333
  if (msg.slug) currentSlug = msg.slug;
1270
- projectNameEl.textContent = projectName;
1334
+ headerTitleEl.textContent = projectName;
1271
1335
  updatePageTitle();
1272
1336
  if (msg.version) {
1273
1337
  var vEl = $("footer-version");
@@ -1282,7 +1346,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1282
1346
  var spBanner = $("skip-perms-banner");
1283
1347
  if (spBanner) spBanner.classList.remove("hidden");
1284
1348
  }
1285
- updateProjectSwitcher(msg);
1349
+ updateProjectList(msg);
1286
1350
  break;
1287
1351
 
1288
1352
  case "update_available":
@@ -1351,6 +1415,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1351
1415
  handleSearchResults(msg);
1352
1416
  break;
1353
1417
 
1418
+ case "cli_session_list":
1419
+ populateCliSessionList(msg.sessions || []);
1420
+ break;
1421
+
1354
1422
  case "session_switched":
1355
1423
  // Save draft from outgoing session
1356
1424
  if (activeSessionId && inputEl.value) {
@@ -1453,6 +1521,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1453
1521
  if (askTool) {
1454
1522
  if (askTool.el) askTool.el.style.display = "none";
1455
1523
  askTool.done = true;
1524
+ removeToolFromGroup(msg.id);
1456
1525
  }
1457
1526
  renderAskUserQuestion(msg.id, msg.input);
1458
1527
  startUrgentBlink();
@@ -1528,10 +1597,23 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1528
1597
  scrollToBottom();
1529
1598
  break;
1530
1599
 
1600
+ case "subagent_activity":
1601
+ updateSubagentActivity(msg.parentToolId, msg.text);
1602
+ break;
1603
+
1604
+ case "subagent_tool":
1605
+ addSubagentToolEntry(msg.parentToolId, msg.toolName, msg.toolId, msg.text);
1606
+ break;
1607
+
1608
+ case "subagent_done":
1609
+ markSubagentDone(msg.parentToolId);
1610
+ break;
1611
+
1531
1612
  case "result":
1532
1613
  setActivity(null);
1533
1614
  stopThinking();
1534
1615
  markAllToolsDone();
1616
+ closeToolGroup();
1535
1617
  finalizeAssistantBlock();
1536
1618
  addTurnMeta(msg.cost, msg.duration);
1537
1619
  accumulateUsage(msg.cost, msg.usage);
@@ -1542,6 +1624,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1542
1624
  setActivity(null);
1543
1625
  stopThinking();
1544
1626
  markAllToolsDone();
1627
+ closeToolGroup();
1545
1628
  finalizeAssistantBlock();
1546
1629
  processing = false;
1547
1630
  setStatus("connected");
@@ -1564,7 +1647,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1564
1647
  case "error":
1565
1648
  setActivity(null);
1566
1649
  addSystemMessage(msg.text, true);
1567
- updateFavicon("#E5534B");
1650
+ updateFavicon(getComputedVar("--error"));
1568
1651
  break;
1569
1652
 
1570
1653
  case "rewind_preview_result":
@@ -1635,6 +1718,22 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1635
1718
  case "process_stats":
1636
1719
  updateStatusPanel(msg);
1637
1720
  break;
1721
+
1722
+ case "browse_dir_result":
1723
+ handleBrowseDirResult(msg);
1724
+ break;
1725
+
1726
+ case "add_project_result":
1727
+ handleAddProjectResult(msg);
1728
+ break;
1729
+
1730
+ case "remove_project_result":
1731
+ handleRemoveProjectResult(msg);
1732
+ break;
1733
+
1734
+ case "projects_updated":
1735
+ updateProjectList(msg);
1736
+ break;
1638
1737
  }
1639
1738
  }
1640
1739
 
@@ -1807,6 +1906,231 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1807
1906
  fileViewerEl: $("file-viewer"),
1808
1907
  });
1809
1908
 
1909
+ // --- Remove project ---
1910
+ function confirmRemoveProject(slug, name) {
1911
+ showConfirm("Remove project \"" + name + "\"?", function () {
1912
+ if (ws && ws.readyState === 1) {
1913
+ ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
1914
+ }
1915
+ });
1916
+ }
1917
+
1918
+ function handleRemoveProjectResult(msg) {
1919
+ if (msg.ok) {
1920
+ showToast("Project removed", "success");
1921
+ // If we removed the current project, navigate to first available
1922
+ if (msg.slug === currentSlug) {
1923
+ window.location.href = "/";
1924
+ }
1925
+ } else {
1926
+ showToast(msg.error || "Failed to remove project", "error");
1927
+ }
1928
+ }
1929
+
1930
+ // --- Add project modal ---
1931
+ var addProjectModal = document.getElementById("add-project-modal");
1932
+ var addProjectInput = document.getElementById("add-project-input");
1933
+ var addProjectSuggestions = document.getElementById("add-project-suggestions");
1934
+ var addProjectError = document.getElementById("add-project-error");
1935
+ var addProjectOk = document.getElementById("add-project-ok");
1936
+ var addProjectCancel = document.getElementById("add-project-cancel");
1937
+ var addProjectDebounce = null;
1938
+ var addProjectActiveIdx = -1;
1939
+
1940
+ function openAddProjectModal() {
1941
+ addProjectModal.classList.remove("hidden");
1942
+ addProjectInput.value = "/";
1943
+ addProjectError.classList.add("hidden");
1944
+ addProjectError.textContent = "";
1945
+ addProjectSuggestions.classList.add("hidden");
1946
+ addProjectSuggestions.innerHTML = "";
1947
+ addProjectActiveIdx = -1;
1948
+ addProjectOk.disabled = false;
1949
+ setTimeout(function () {
1950
+ addProjectInput.focus();
1951
+ addProjectInput.setSelectionRange(1, 1);
1952
+ }, 50);
1953
+ }
1954
+
1955
+ function closeAddProjectModal() {
1956
+ addProjectModal.classList.add("hidden");
1957
+ addProjectInput.value = "";
1958
+ addProjectSuggestions.classList.add("hidden");
1959
+ addProjectSuggestions.innerHTML = "";
1960
+ addProjectError.classList.add("hidden");
1961
+ addProjectActiveIdx = -1;
1962
+ if (addProjectDebounce) { clearTimeout(addProjectDebounce); addProjectDebounce = null; }
1963
+ }
1964
+
1965
+ function requestBrowseDir(val) {
1966
+ if (!ws || ws.readyState !== 1) return;
1967
+ ws.send(JSON.stringify({ type: "browse_dir", path: val }));
1968
+ }
1969
+
1970
+ function handleBrowseDirResult(msg) {
1971
+ addProjectSuggestions.innerHTML = "";
1972
+ addProjectActiveIdx = -1;
1973
+ if (msg.error) {
1974
+ addProjectSuggestions.classList.add("hidden");
1975
+ return;
1976
+ }
1977
+ var entries = msg.entries || [];
1978
+ if (entries.length === 0) {
1979
+ addProjectSuggestions.classList.add("hidden");
1980
+ return;
1981
+ }
1982
+ for (var si = 0; si < entries.length; si++) {
1983
+ var entry = entries[si];
1984
+ var item = document.createElement("div");
1985
+ item.className = "add-project-suggestion-item";
1986
+ item.dataset.path = entry.path;
1987
+ item.innerHTML = '<i data-lucide="folder"></i><span class="add-project-suggestion-name">' +
1988
+ escapeHtml(entry.name) + '</span>';
1989
+ item.addEventListener("click", function (e) {
1990
+ var p = this.dataset.path + "/";
1991
+ addProjectInput.value = p;
1992
+ addProjectInput.focus();
1993
+ addProjectError.classList.add("hidden");
1994
+ requestBrowseDir(p);
1995
+ });
1996
+ addProjectSuggestions.appendChild(item);
1997
+ }
1998
+ addProjectSuggestions.classList.remove("hidden");
1999
+ refreshIcons();
2000
+ }
2001
+
2002
+ function handleAddProjectResult(msg) {
2003
+ if (msg.ok) {
2004
+ closeAddProjectModal();
2005
+ if (msg.existing) {
2006
+ showToast("Project already registered", "info");
2007
+ } else {
2008
+ showToast("Project added", "success");
2009
+ // Navigate to the new project
2010
+ if (msg.slug) {
2011
+ window.location.href = "/p/" + msg.slug + "/";
2012
+ }
2013
+ }
2014
+ } else {
2015
+ addProjectError.textContent = msg.error || "Failed to add project";
2016
+ addProjectError.classList.remove("hidden");
2017
+ addProjectOk.disabled = false;
2018
+ }
2019
+ }
2020
+
2021
+ function setActiveIdx(idx) {
2022
+ var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
2023
+ addProjectActiveIdx = idx;
2024
+ for (var ai = 0; ai < items.length; ai++) {
2025
+ if (ai === idx) {
2026
+ items[ai].classList.add("active");
2027
+ items[ai].scrollIntoView({ block: "nearest" });
2028
+ } else {
2029
+ items[ai].classList.remove("active");
2030
+ }
2031
+ }
2032
+ }
2033
+
2034
+ addProjectInput.addEventListener("focus", function () {
2035
+ var val = addProjectInput.value;
2036
+ if (val && addProjectSuggestions.children.length === 0) {
2037
+ requestBrowseDir(val);
2038
+ } else if (addProjectSuggestions.children.length > 0) {
2039
+ addProjectSuggestions.classList.remove("hidden");
2040
+ }
2041
+ });
2042
+
2043
+ addProjectModal.querySelector(".confirm-dialog").addEventListener("click", function (e) {
2044
+ if (e.target === addProjectInput || addProjectInput.contains(e.target)) return;
2045
+ if (e.target === addProjectSuggestions || addProjectSuggestions.contains(e.target)) return;
2046
+ addProjectSuggestions.classList.add("hidden");
2047
+ addProjectActiveIdx = -1;
2048
+ });
2049
+
2050
+ addProjectInput.addEventListener("input", function () {
2051
+ var val = addProjectInput.value;
2052
+ addProjectError.classList.add("hidden");
2053
+ if (addProjectDebounce) clearTimeout(addProjectDebounce);
2054
+ addProjectDebounce = setTimeout(function () {
2055
+ requestBrowseDir(val);
2056
+ }, 200);
2057
+ });
2058
+
2059
+ addProjectInput.addEventListener("keydown", function (e) {
2060
+ var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
2061
+
2062
+ if (e.key === "ArrowDown") {
2063
+ e.preventDefault();
2064
+ if (items.length > 0) {
2065
+ var next = addProjectActiveIdx < items.length - 1 ? addProjectActiveIdx + 1 : 0;
2066
+ setActiveIdx(next);
2067
+ }
2068
+ return;
2069
+ }
2070
+
2071
+ if (e.key === "ArrowUp") {
2072
+ e.preventDefault();
2073
+ if (items.length > 0) {
2074
+ var prev = addProjectActiveIdx > 0 ? addProjectActiveIdx - 1 : items.length - 1;
2075
+ setActiveIdx(prev);
2076
+ }
2077
+ return;
2078
+ }
2079
+
2080
+ if (e.key === "Tab") {
2081
+ e.preventDefault();
2082
+ var target = addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length
2083
+ ? items[addProjectActiveIdx]
2084
+ : items.length > 0 ? items[0] : null;
2085
+ if (target) {
2086
+ var p = target.dataset.path + "/";
2087
+ addProjectInput.value = p;
2088
+ addProjectError.classList.add("hidden");
2089
+ requestBrowseDir(p);
2090
+ }
2091
+ return;
2092
+ }
2093
+
2094
+ if (e.key === "Enter") {
2095
+ e.preventDefault();
2096
+ // If a suggestion is highlighted, pick it first
2097
+ if (addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length) {
2098
+ var picked = items[addProjectActiveIdx].dataset.path + "/";
2099
+ addProjectInput.value = picked;
2100
+ addProjectError.classList.add("hidden");
2101
+ requestBrowseDir(picked);
2102
+ return;
2103
+ }
2104
+ // Otherwise submit
2105
+ submitAddProject();
2106
+ return;
2107
+ }
2108
+
2109
+ if (e.key === "Escape") {
2110
+ e.preventDefault();
2111
+ closeAddProjectModal();
2112
+ return;
2113
+ }
2114
+ });
2115
+
2116
+ function submitAddProject() {
2117
+ var val = addProjectInput.value.replace(/\/+$/, "");
2118
+ if (!val) return;
2119
+ addProjectOk.disabled = true;
2120
+ addProjectError.classList.add("hidden");
2121
+ if (ws && ws.readyState === 1) {
2122
+ ws.send(JSON.stringify({ type: "add_project", path: val }));
2123
+ }
2124
+ }
2125
+
2126
+ addProjectOk.addEventListener("click", function () { submitAddProject(); });
2127
+ addProjectCancel.addEventListener("click", function () { closeAddProjectModal(); });
2128
+
2129
+ // Close on backdrop click
2130
+ addProjectModal.querySelector(".confirm-backdrop").addEventListener("click", function () {
2131
+ closeAddProjectModal();
2132
+ });
2133
+
1810
2134
  // --- Init ---
1811
2135
  lucide.createIcons();
1812
2136
  startVerbCycle();