claude-relay 2.3.1 → 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 (52) hide show
  1. package/README.md +20 -5
  2. package/bin/cli.js +206 -8
  3. package/lib/cli-sessions.js +270 -0
  4. package/lib/daemon.js +40 -0
  5. package/lib/project.js +121 -1
  6. package/lib/public/app.js +385 -76
  7. package/lib/public/css/base.css +41 -7
  8. package/lib/public/css/diff.css +6 -6
  9. package/lib/public/css/filebrowser.css +34 -54
  10. package/lib/public/css/highlight.css +144 -0
  11. package/lib/public/css/input.css +9 -9
  12. package/lib/public/css/menus.css +82 -23
  13. package/lib/public/css/messages.css +178 -34
  14. package/lib/public/css/overlays.css +166 -50
  15. package/lib/public/css/rewind.css +17 -17
  16. package/lib/public/css/sidebar.css +210 -137
  17. package/lib/public/index.html +73 -40
  18. package/lib/public/modules/filebrowser.js +2 -1
  19. package/lib/public/modules/markdown.js +10 -10
  20. package/lib/public/modules/notifications.js +38 -1
  21. package/lib/public/modules/sidebar.js +109 -31
  22. package/lib/public/modules/terminal.js +11 -23
  23. package/lib/public/modules/theme.js +622 -0
  24. package/lib/public/modules/tools.js +245 -4
  25. package/lib/public/modules/utils.js +21 -5
  26. package/lib/public/style.css +1 -0
  27. package/lib/sdk-bridge.js +95 -0
  28. package/lib/server.js +41 -0
  29. package/lib/sessions.js +16 -3
  30. package/lib/themes/ayu-light.json +9 -0
  31. package/lib/themes/catppuccin-latte.json +9 -0
  32. package/lib/themes/catppuccin-mocha.json +9 -0
  33. package/lib/themes/claude-light.json +9 -0
  34. package/lib/themes/claude.json +9 -0
  35. package/lib/themes/dracula.json +9 -0
  36. package/lib/themes/everforest-light.json +9 -0
  37. package/lib/themes/everforest.json +9 -0
  38. package/lib/themes/github-light.json +9 -0
  39. package/lib/themes/gruvbox-dark.json +9 -0
  40. package/lib/themes/gruvbox-light.json +9 -0
  41. package/lib/themes/monokai.json +9 -0
  42. package/lib/themes/nord-light.json +9 -0
  43. package/lib/themes/nord.json +9 -0
  44. package/lib/themes/one-dark.json +9 -0
  45. package/lib/themes/one-light.json +9 -0
  46. package/lib/themes/rose-pine-dawn.json +9 -0
  47. package/lib/themes/rose-pine.json +9 -0
  48. package/lib/themes/solarized-dark.json +9 -0
  49. package/lib/themes/solarized-light.json +9 -0
  50. package/lib/themes/tokyo-night-light.json +9 -0
  51. package/lib/themes/tokyo-night.json +9 -0
  52. 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
  });
@@ -217,6 +191,45 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
217
191
 
218
192
  // builtinCommands -> modules/input.js
219
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
+
220
233
  // --- Confirm modal ---
221
234
  var confirmModal = $("confirm-modal");
222
235
  var confirmText = $("confirm-text");
@@ -267,6 +280,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
267
280
  addSystemMessage: addSystemMessage,
268
281
  });
269
282
 
283
+ // --- Theme (module) ---
284
+ initTheme();
285
+
270
286
  // --- Sidebar (module) ---
271
287
  initSidebar({
272
288
  $: $,
@@ -282,6 +298,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
282
298
  hamburgerBtn: hamburgerBtn,
283
299
  newSessionBtn: newSessionBtn,
284
300
  resumeSessionBtn: resumeSessionBtn,
301
+ headerTitleEl: headerTitleEl,
285
302
  showConfirm: showConfirm,
286
303
  onFilesTabOpen: function () { loadRootDirectory(); },
287
304
  });
@@ -339,9 +356,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
339
356
  ];
340
357
 
341
358
  var CELL = 12;
342
- var accent = "#DA7756";
343
- var eye = "#2F2E2B";
344
- var antenna = "#E8E5DE";
359
+ var accent = getThemeColor("base09");
360
+ var eye = getThemeColor("base00");
361
+ var antenna = getThemeColor("base06");
345
362
 
346
363
  for (var r = 0; r < grid.length; r++) {
347
364
  for (var c = 0; c < grid[r].length; c++) {
@@ -359,6 +376,23 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
359
376
  }
360
377
  })();
361
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
+
362
396
  function pixelScatter() {
363
397
  stopSpark();
364
398
  for (var i = 0; i < pixelBlocks.length; i++) {
@@ -398,7 +432,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
398
432
  antennaBlocks[i].style.background = "#FFF";
399
433
  antennaBlocks[i].style.boxShadow = "0 0 6px 2px rgba(255,255,255,0.6)";
400
434
  } else {
401
- antennaBlocks[i].style.background = "#E8E5DE";
435
+ antennaBlocks[i].style.background = antenna;
402
436
  antennaBlocks[i].style.boxShadow = "none";
403
437
  }
404
438
  }
@@ -413,7 +447,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
413
447
  sparkTimer = null;
414
448
  }
415
449
  for (var i = 0; i < antennaBlocks.length; i++) {
416
- antennaBlocks[i].style.background = "#E8E5DE";
450
+ antennaBlocks[i].style.background = antenna;
417
451
  antennaBlocks[i].style.boxShadow = "none";
418
452
  }
419
453
  }
@@ -458,7 +492,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
458
492
  if (xhr.status === 200) faviconSvg = xhr.responseText;
459
493
  else return;
460
494
  }
461
- var svg = faviconSvg.replace(/fill="#57AB5A"/, 'fill="' + bgColor + '"');
495
+ var svg = faviconSvg.replace(/fill="#57AB5A"/g, 'fill="' + bgColor + '"');
462
496
  faviconLink.href = "data:image/svg+xml," + encodeURIComponent(svg);
463
497
  }
464
498
 
@@ -484,9 +518,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
484
518
  ioTimer = setTimeout(function () { statusDot.classList.remove("io"); }, 60);
485
519
 
486
520
  // Blink favicon: dim then restore
487
- updateFavicon("#3D6B3E");
521
+ updateFavicon(getComputedVar("--sidebar-bg"));
488
522
  clearTimeout(faviconIoTimer);
489
- faviconIoTimer = setTimeout(function () { updateFavicon("#57AB5A"); }, 60);
523
+ faviconIoTimer = setTimeout(function () { updateFavicon(getComputedVar("--success")); }, 60);
490
524
  }
491
525
 
492
526
  // --- Urgent favicon blink (permission / ask user) ---
@@ -495,7 +529,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
495
529
  function startUrgentBlink() {
496
530
  if (urgentBlinkTimer) return;
497
531
  savedTitle = document.title;
498
- var colors = ["#DA7756", "#57AB5A", "#DA7756", "#E8E5DE", "#DA7756", "#57AB5A"];
532
+ var colors = [getComputedVar("--accent"), getComputedVar("--success"), getComputedVar("--accent"), getComputedVar("--text"), getComputedVar("--accent"), getComputedVar("--success")];
499
533
  var tick = 0;
500
534
  urgentBlinkTimer = setInterval(function () {
501
535
  updateFavicon(colors[tick % colors.length]);
@@ -507,7 +541,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
507
541
  if (!urgentBlinkTimer) return;
508
542
  clearInterval(urgentBlinkTimer);
509
543
  urgentBlinkTimer = null;
510
- updateFavicon("#57AB5A");
544
+ updateFavicon(getComputedVar("--success"));
511
545
  if (savedTitle) document.title = savedTitle;
512
546
  savedTitle = null;
513
547
  }
@@ -522,12 +556,12 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
522
556
  setSendBtnMode("send");
523
557
  connectOverlay.classList.add("hidden");
524
558
  stopVerbCycle();
525
- updateFavicon("#57AB5A");
559
+ updateFavicon(getComputedVar("--success"));
526
560
  } else if (status === "processing") {
527
561
  statusDot.classList.add("processing");
528
562
  processing = true;
529
563
  setSendBtnMode("stop");
530
- updateFavicon("#57AB5A");
564
+ updateFavicon(getComputedVar("--success"));
531
565
  } else {
532
566
  connected = false;
533
567
  sendBtn.disabled = true;
@@ -535,7 +569,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
535
569
  connectStatusEl.textContent = "Reconnecting...";
536
570
  startVerbCycle();
537
571
  startPixelAnim();
538
- updateFavicon("#E5534B");
572
+ updateFavicon(getComputedVar("--error"));
539
573
  }
540
574
  }
541
575
 
@@ -1104,6 +1138,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1104
1138
  if (currentFullText) {
1105
1139
  addCopyHandler(currentMsgEl, currentFullText);
1106
1140
  }
1141
+ // Assistant text appeared, so break the current tool group
1142
+ closeToolGroup();
1107
1143
  }
1108
1144
  currentMsgEl = null;
1109
1145
  currentFullText = "";
@@ -1257,6 +1293,16 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1257
1293
  break;
1258
1294
 
1259
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();
1260
1306
  var pendingQuery = getActiveSearchQuery();
1261
1307
  if (pendingQuery) {
1262
1308
  requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
@@ -1271,6 +1317,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1271
1317
  target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
1272
1318
  }
1273
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");
1274
1323
  target.scrollIntoView({ behavior: "smooth", block: "center" });
1275
1324
  target.classList.add("message-blink");
1276
1325
  setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
@@ -1282,7 +1331,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1282
1331
  case "info":
1283
1332
  projectName = msg.project || msg.cwd;
1284
1333
  if (msg.slug) currentSlug = msg.slug;
1285
- projectNameEl.textContent = projectName;
1334
+ headerTitleEl.textContent = projectName;
1286
1335
  updatePageTitle();
1287
1336
  if (msg.version) {
1288
1337
  var vEl = $("footer-version");
@@ -1297,7 +1346,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1297
1346
  var spBanner = $("skip-perms-banner");
1298
1347
  if (spBanner) spBanner.classList.remove("hidden");
1299
1348
  }
1300
- updateProjectSwitcher(msg);
1349
+ updateProjectList(msg);
1301
1350
  break;
1302
1351
 
1303
1352
  case "update_available":
@@ -1366,6 +1415,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1366
1415
  handleSearchResults(msg);
1367
1416
  break;
1368
1417
 
1418
+ case "cli_session_list":
1419
+ populateCliSessionList(msg.sessions || []);
1420
+ break;
1421
+
1369
1422
  case "session_switched":
1370
1423
  // Save draft from outgoing session
1371
1424
  if (activeSessionId && inputEl.value) {
@@ -1468,6 +1521,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1468
1521
  if (askTool) {
1469
1522
  if (askTool.el) askTool.el.style.display = "none";
1470
1523
  askTool.done = true;
1524
+ removeToolFromGroup(msg.id);
1471
1525
  }
1472
1526
  renderAskUserQuestion(msg.id, msg.input);
1473
1527
  startUrgentBlink();
@@ -1543,10 +1597,23 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1543
1597
  scrollToBottom();
1544
1598
  break;
1545
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
+
1546
1612
  case "result":
1547
1613
  setActivity(null);
1548
1614
  stopThinking();
1549
1615
  markAllToolsDone();
1616
+ closeToolGroup();
1550
1617
  finalizeAssistantBlock();
1551
1618
  addTurnMeta(msg.cost, msg.duration);
1552
1619
  accumulateUsage(msg.cost, msg.usage);
@@ -1557,6 +1624,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1557
1624
  setActivity(null);
1558
1625
  stopThinking();
1559
1626
  markAllToolsDone();
1627
+ closeToolGroup();
1560
1628
  finalizeAssistantBlock();
1561
1629
  processing = false;
1562
1630
  setStatus("connected");
@@ -1579,7 +1647,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1579
1647
  case "error":
1580
1648
  setActivity(null);
1581
1649
  addSystemMessage(msg.text, true);
1582
- updateFavicon("#E5534B");
1650
+ updateFavicon(getComputedVar("--error"));
1583
1651
  break;
1584
1652
 
1585
1653
  case "rewind_preview_result":
@@ -1650,6 +1718,22 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1650
1718
  case "process_stats":
1651
1719
  updateStatusPanel(msg);
1652
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;
1653
1737
  }
1654
1738
  }
1655
1739
 
@@ -1822,6 +1906,231 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1822
1906
  fileViewerEl: $("file-viewer"),
1823
1907
  });
1824
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
+
1825
2134
  // --- Init ---
1826
2135
  lucide.createIcons();
1827
2136
  startVerbCycle();