clay-server 2.5.1 → 2.6.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.
package/lib/public/app.js CHANGED
@@ -1,17 +1,19 @@
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, populateCliSessionList, renderIconStrip, initIconStrip } from './modules/sidebar.js';
4
+ import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, initIconStrip, getEmojiCategories } from './modules/sidebar.js';
5
5
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
6
6
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
7
- import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands } from './modules/input.js';
7
+ import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } 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
- import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed } from './modules/terminal.js';
10
+ import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
11
11
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted } from './modules/sticky-notes.js';
12
12
  import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
13
13
  import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
14
- import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleShutdownResult } from './modules/server-settings.js';
14
+ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
15
+ import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved } from './modules/project-settings.js';
16
+ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
15
17
 
16
18
  // --- Base path for multi-project routing ---
17
19
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -24,10 +26,7 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
24
26
  var inputEl = $("input");
25
27
  var sendBtn = $("send-btn");
26
28
  function getStatusDot() {
27
- var active = document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status");
28
- if (active) return active;
29
- // Fallback: home icon status dot
30
- return document.querySelector(".icon-strip-home .icon-strip-status");
29
+ return document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status");
31
30
  }
32
31
  var headerTitleEl = $("header-title");
33
32
  var headerRenameBtn = $("header-rename-btn");
@@ -73,15 +72,42 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
73
72
  function renderProjectList() {
74
73
  // Render icon strip projects
75
74
  var iconStripProjects = cachedProjects.map(function (p) {
76
- return { slug: p.slug, name: p.title || p.project, isProcessing: p.isProcessing };
75
+ return { slug: p.slug, name: p.title || p.project, icon: p.icon || null, isProcessing: p.isProcessing };
77
76
  });
78
77
  renderIconStrip(iconStripProjects, currentSlug);
78
+ // Update title bar project name and icon if it changed
79
+ for (var pi = 0; pi < cachedProjects.length; pi++) {
80
+ if (cachedProjects[pi].slug === currentSlug) {
81
+ var updatedName = cachedProjects[pi].title || cachedProjects[pi].project;
82
+ var tbName = document.getElementById("title-bar-project-name");
83
+ if (tbName && updatedName) tbName.textContent = updatedName;
84
+ var tbIcon = document.getElementById("title-bar-project-icon");
85
+ if (tbIcon) {
86
+ var pIcon = cachedProjects[pi].icon || null;
87
+ if (pIcon) {
88
+ tbIcon.textContent = pIcon;
89
+ if (typeof twemoji !== "undefined") {
90
+ twemoji.parse(tbIcon, { folder: "svg", ext: ".svg" });
91
+ }
92
+ tbIcon.classList.add("has-icon");
93
+ try { localStorage.setItem("clay-project-icon-" + (currentSlug || "default"), pIcon); } catch (e) {}
94
+ } else {
95
+ tbIcon.textContent = "";
96
+ tbIcon.classList.remove("has-icon");
97
+ try { localStorage.removeItem("clay-project-icon-" + (currentSlug || "default")); } catch (e) {}
98
+ }
99
+ }
100
+ break;
101
+ }
102
+ }
79
103
  // Re-apply current socket status to the active icon's dot
80
104
  var dot = getStatusDot();
81
105
  if (dot) {
82
106
  if (connected && processing) { dot.classList.add("connected"); dot.classList.add("processing"); }
83
107
  else if (connected) { dot.classList.add("connected"); }
84
108
  }
109
+ // Start/stop cross-project IO blink for non-active processing projects
110
+ updateCrossProjectBlink();
85
111
  }
86
112
 
87
113
  if (projectListAddBtn) {
@@ -153,7 +179,7 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
153
179
  var projectName = "";
154
180
  var turnCounter = 0;
155
181
 
156
- // Restore cached project name for instant display (before WS connects)
182
+ // Restore cached project name and icon for instant display (before WS connects)
157
183
  try {
158
184
  var _cachedProjectName = localStorage.getItem("clay-project-name-" + (currentSlug || "default"));
159
185
  if (_cachedProjectName) {
@@ -162,6 +188,17 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
162
188
  var _tbp = $("title-bar-project-name");
163
189
  if (_tbp) _tbp.textContent = _cachedProjectName;
164
190
  }
191
+ var _cachedProjectIcon = localStorage.getItem("clay-project-icon-" + (currentSlug || "default"));
192
+ if (_cachedProjectIcon) {
193
+ var _tbi = $("title-bar-project-icon");
194
+ if (_tbi) {
195
+ _tbi.textContent = _cachedProjectIcon;
196
+ if (typeof twemoji !== "undefined") {
197
+ twemoji.parse(_tbi, { folder: "svg", ext: ".svg" });
198
+ }
199
+ _tbi.classList.add("has-icon");
200
+ }
201
+ }
165
202
  } catch (e) {}
166
203
  var messageUuidMap = [];
167
204
  // pendingRewindUuid is now in modules/rewind.js
@@ -435,8 +472,45 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
435
472
  if (!connected) return;
436
473
  var dot = getStatusDot();
437
474
  if (dot) dot.classList.add("io");
475
+ // Also blink the active session's processing dot in sidebar
476
+ var sessionDot = document.querySelector(".session-item.active .session-processing");
477
+ if (sessionDot) sessionDot.classList.add("io");
438
478
  clearTimeout(ioTimer);
439
- ioTimer = setTimeout(function () { var d = getStatusDot(); if (d) d.classList.remove("io"); }, 80);
479
+ ioTimer = setTimeout(function () {
480
+ var d = getStatusDot();
481
+ if (d) d.classList.remove("io");
482
+ var sd = document.querySelector(".session-item.active .session-processing.io");
483
+ if (sd) sd.classList.remove("io");
484
+ }, 80);
485
+ }
486
+
487
+ // --- Per-session IO blink for non-active sessions ---
488
+ var sessionIoTimers = {};
489
+ function blinkSessionDot(sessionId) {
490
+ var el = document.querySelector('.session-item[data-session-id="' + sessionId + '"] .session-processing');
491
+ if (!el) return;
492
+ el.classList.add("io");
493
+ clearTimeout(sessionIoTimers[sessionId]);
494
+ sessionIoTimers[sessionId] = setTimeout(function () {
495
+ el.classList.remove("io");
496
+ delete sessionIoTimers[sessionId];
497
+ }, 80);
498
+ }
499
+
500
+ // --- Cross-project IO blink for non-active processing projects ---
501
+ var crossProjectBlinkTimer = null;
502
+ function updateCrossProjectBlink() {
503
+ if (crossProjectBlinkTimer) { clearTimeout(crossProjectBlinkTimer); crossProjectBlinkTimer = null; }
504
+ function doBlink() {
505
+ var dots = document.querySelectorAll("#icon-strip-projects .icon-strip-item:not(.active) .icon-strip-status.processing");
506
+ if (dots.length === 0) { crossProjectBlinkTimer = null; return; }
507
+ for (var i = 0; i < dots.length; i++) { dots[i].classList.add("io"); }
508
+ setTimeout(function () {
509
+ for (var j = 0; j < dots.length; j++) { dots[j].classList.remove("io"); }
510
+ crossProjectBlinkTimer = setTimeout(doBlink, 150 + Math.random() * 350);
511
+ }, 80);
512
+ }
513
+ crossProjectBlinkTimer = setTimeout(doBlink, 50);
440
514
  }
441
515
 
442
516
  // --- Urgent favicon blink (permission / ask user) ---
@@ -521,7 +595,7 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
521
595
  var currentModels = [];
522
596
  var currentModel = "";
523
597
  var currentMode = "default";
524
- var currentEffort = "high";
598
+ var currentEffort = "medium";
525
599
  var currentBetas = [];
526
600
  var skipPermsEnabled = false;
527
601
 
@@ -1586,8 +1660,17 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
1586
1660
  suggestionChipsEl.innerHTML = "";
1587
1661
  var chip = document.createElement("button");
1588
1662
  chip.className = "suggestion-chip";
1589
- chip.innerHTML = iconHtml("sparkles") + " " + escapeHtml(suggestion);
1663
+ chip.innerHTML =
1664
+ '<span class="suggestion-chip-send">' + iconHtml("sparkles") +
1665
+ '<span class="suggestion-chip-text">' + escapeHtml(suggestion) + '</span></span>' +
1666
+ '<span class="suggestion-chip-edit">' + iconHtml("pencil") + '</span>';
1590
1667
  chip.addEventListener("click", function () {
1668
+ inputEl.value = suggestion;
1669
+ hideSuggestionChips();
1670
+ sendMessage();
1671
+ });
1672
+ chip.querySelector(".suggestion-chip-edit").addEventListener("click", function (e) {
1673
+ e.stopPropagation();
1591
1674
  inputEl.value = suggestion;
1592
1675
  inputEl.focus();
1593
1676
  inputEl.select();
@@ -1604,14 +1687,6 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
1604
1687
  suggestionChipsEl.classList.add("hidden");
1605
1688
  }
1606
1689
 
1607
- function acceptSuggestionChip() {
1608
- if (suggestionChipsEl.classList.contains("hidden")) return false;
1609
- var chip = suggestionChipsEl.querySelector(".suggestion-chip");
1610
- if (!chip) return false;
1611
- chip.click();
1612
- return true;
1613
- }
1614
-
1615
1690
  function resetClientState() {
1616
1691
  messagesEl.innerHTML = "";
1617
1692
  currentMsgEl = null;
@@ -1921,7 +1996,7 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
1921
1996
  for (var ei = 0; ei < levels.length; ei++) {
1922
1997
  if (levels[ei] === currentEffort) { effortValid = true; break; }
1923
1998
  }
1924
- if (!effortValid) currentEffort = "high";
1999
+ if (!effortValid) currentEffort = "medium";
1925
2000
  }
1926
2001
  updateConfigChip();
1927
2002
  break;
@@ -1943,6 +2018,14 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
1943
2018
  showToast(msg.message, msg.level, msg.detail);
1944
2019
  break;
1945
2020
 
2021
+ case "skill_installed":
2022
+ handleSkillInstalled(msg);
2023
+ break;
2024
+
2025
+ case "skill_uninstalled":
2026
+ handleSkillUninstalled(msg);
2027
+ break;
2028
+
1946
2029
  case "input_sync":
1947
2030
  handleInputSync(msg.text);
1948
2031
  break;
@@ -1951,6 +2034,10 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
1951
2034
  renderSessionList(msg.sessions || []);
1952
2035
  break;
1953
2036
 
2037
+ case "session_io":
2038
+ blinkSessionDot(msg.id);
2039
+ break;
2040
+
1954
2041
  case "search_results":
1955
2042
  handleSearchResults(msg);
1956
2043
  break;
@@ -2265,7 +2352,41 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
2265
2352
  break;
2266
2353
 
2267
2354
  case "fs_read_result":
2268
- handleFsRead(msg);
2355
+ if (msg.path === "CLAUDE.md" && isProjectSettingsOpen()) {
2356
+ handleInstructionsRead(msg);
2357
+ } else {
2358
+ handleFsRead(msg);
2359
+ }
2360
+ break;
2361
+
2362
+ case "fs_write_result":
2363
+ handleInstructionsWrite(msg);
2364
+ break;
2365
+
2366
+ case "project_env_result":
2367
+ handleProjectEnv(msg);
2368
+ break;
2369
+
2370
+ case "set_project_env_result":
2371
+ handleProjectEnvSaved(msg);
2372
+ break;
2373
+
2374
+ case "global_claude_md_result":
2375
+ handleGlobalClaudeMdRead(msg);
2376
+ break;
2377
+
2378
+ case "write_global_claude_md_result":
2379
+ handleGlobalClaudeMdWrite(msg);
2380
+ break;
2381
+
2382
+ case "shared_env_result":
2383
+ handleSharedEnv(msg);
2384
+ handleProjectSharedEnv(msg);
2385
+ break;
2386
+
2387
+ case "set_shared_env_result":
2388
+ handleSharedEnvSaved(msg);
2389
+ handleProjectSharedEnvSaved(msg);
2269
2390
  break;
2270
2391
 
2271
2392
  case "fs_file_changed":
@@ -2341,6 +2462,24 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
2341
2462
  handleRemoveProjectResult(msg);
2342
2463
  break;
2343
2464
 
2465
+ case "reorder_projects_result":
2466
+ if (!msg.ok) {
2467
+ showToast(msg.error || "Failed to reorder projects", "error");
2468
+ }
2469
+ break;
2470
+
2471
+ case "set_project_title_result":
2472
+ if (!msg.ok) {
2473
+ showToast(msg.error || "Failed to rename project", "error");
2474
+ }
2475
+ break;
2476
+
2477
+ case "set_project_icon_result":
2478
+ if (!msg.ok) {
2479
+ showToast(msg.error || "Failed to set icon", "error");
2480
+ }
2481
+ break;
2482
+
2344
2483
  case "projects_updated":
2345
2484
  updateProjectList(msg);
2346
2485
  break;
@@ -2493,6 +2632,7 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
2493
2632
  get ws() { return ws; },
2494
2633
  get connected() { return connected; },
2495
2634
  get processing() { return processing; },
2635
+ get basePath() { return basePath; },
2496
2636
  inputEl: inputEl,
2497
2637
  sendBtn: sendBtn,
2498
2638
  slashMenu: slashMenu,
@@ -2508,7 +2648,6 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
2508
2648
  resetContextData: resetContextData,
2509
2649
  showImageModal: showImageModal,
2510
2650
  hideSuggestionChips: hideSuggestionChips,
2511
- acceptSuggestionChip: acceptSuggestionChip,
2512
2651
  });
2513
2652
 
2514
2653
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
@@ -2532,10 +2671,25 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
2532
2671
  wsPath: wsPath,
2533
2672
  get currentModels() { return currentModels; },
2534
2673
  set currentModels(v) { currentModels = v; updateConfigChip(); },
2674
+ get currentModel() { return currentModel; },
2675
+ get currentMode() { return currentMode; },
2676
+ get currentEffort() { return currentEffort; },
2677
+ get currentBetas() { return currentBetas; },
2535
2678
  setContextView: setContextView,
2536
2679
  applyContextView: applyContextView,
2537
2680
  });
2538
2681
 
2682
+ // --- Project Settings ---
2683
+ initProjectSettings({
2684
+ get ws() { return ws; },
2685
+ get connected() { return connected; },
2686
+ get currentModels() { return currentModels; },
2687
+ get currentModel() { return currentModel; },
2688
+ get currentMode() { return currentMode; },
2689
+ get currentEffort() { return currentEffort; },
2690
+ get currentBetas() { return currentBetas; },
2691
+ }, getEmojiCategories());
2692
+
2539
2693
  // --- QR code ---
2540
2694
  initQrCode();
2541
2695
 
@@ -2564,6 +2718,15 @@ import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDa
2564
2718
  get connected() { return connected; },
2565
2719
  });
2566
2720
 
2721
+ // --- Skills ---
2722
+ initSkills({
2723
+ get ws() { return ws; },
2724
+ get connected() { return connected; },
2725
+ basePath: basePath,
2726
+ openTerminal: function () { openTerminal(); },
2727
+ sendTerminalCommand: function (cmd) { sendTerminalCommand(cmd); },
2728
+ });
2729
+
2567
2730
  // --- Remove project ---
2568
2731
  function confirmRemoveProject(slug, name) {
2569
2732
  showConfirm("Remove project \"" + name + "\"?", function () {
@@ -12,8 +12,8 @@
12
12
 
13
13
  .diff-table {
14
14
  border-collapse: collapse;
15
- width: 100%;
16
- table-layout: fixed;
15
+ min-width: 100%;
16
+ table-layout: auto;
17
17
  }
18
18
 
19
19
  /* --- Line numbers --- */
@@ -80,8 +80,7 @@
80
80
  /* --- Split diff specifics --- */
81
81
  .diff-table-split .diff-code-old,
82
82
  .diff-table-split .diff-code-new {
83
- max-width: 0;
84
- overflow: hidden;
83
+ width: 50%;
85
84
  }
86
85
 
87
86
  /* Split: divider between left and right */