clay-server 3.8.1 → 4.0.0-beta.10

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 (222) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/background-task-timing.js +105 -0
  3. package/lib/builtin-mates.js +6 -6
  4. package/lib/claude-hook-installer.js +15 -0
  5. package/lib/clay-history-mcp-server.js +62 -297
  6. package/lib/daemon.js +23 -1
  7. package/lib/debate-model-selection.js +119 -0
  8. package/lib/home-debate-tool-policy.js +164 -0
  9. package/lib/home-response-notifications.js +26 -0
  10. package/lib/knowledge-import.js +436 -0
  11. package/lib/knowledge-record-store.js +188 -0
  12. package/lib/knowledge-search.js +224 -0
  13. package/lib/mate-creation-mcp-server.js +55 -0
  14. package/lib/mate-knowledge-mcp-server.js +151 -0
  15. package/lib/mate-knowledge-migration.js +283 -0
  16. package/lib/mate-knowledge-service.js +488 -0
  17. package/lib/mate-knowledge-sync.js +482 -0
  18. package/lib/mate-ready-creation.js +95 -0
  19. package/lib/mates.js +2 -0
  20. package/lib/notes.js +2 -1
  21. package/lib/project-ask-user.js +64 -0
  22. package/lib/project-capsule-catalog.js +101 -0
  23. package/lib/project-connection.js +17 -0
  24. package/lib/project-debate-proposal.js +133 -17
  25. package/lib/project-debate.js +214 -110
  26. package/lib/project-delegated-follow-up.js +84 -0
  27. package/lib/project-delegated-session.js +53 -0
  28. package/lib/project-filesystem.js +7 -9
  29. package/lib/project-knowledge.js +23 -0
  30. package/lib/project-logs-comments.js +137 -0
  31. package/lib/project-logs-mcp-server.js +290 -0
  32. package/lib/project-logs-query.js +167 -0
  33. package/lib/project-logs-root.js +67 -0
  34. package/lib/project-logs-schema.js +259 -0
  35. package/lib/project-logs-service.js +357 -0
  36. package/lib/project-logs-snapshot.js +202 -0
  37. package/lib/project-logs-store.js +498 -0
  38. package/lib/project-logs-versioning.js +128 -0
  39. package/lib/project-logs.js +347 -0
  40. package/lib/project-mate-creation-proposal.js +120 -0
  41. package/lib/project-mate-interaction.js +14 -0
  42. package/lib/project-mate-knowledge.js +110 -0
  43. package/lib/project-memory.js +36 -7
  44. package/lib/project-models.js +157 -32
  45. package/lib/project-session-notes.js +2 -2
  46. package/lib/project-session-pair.js +8 -8
  47. package/lib/project-sessions.js +66 -51
  48. package/lib/project-vendor-login.js +414 -0
  49. package/lib/project-worker-proposal.js +25 -25
  50. package/lib/project-workspace-query.js +144 -0
  51. package/lib/project.js +433 -102
  52. package/lib/public/app.js +109 -48
  53. package/lib/public/css/avatar-imprints.css +0 -1
  54. package/lib/public/css/base.css +4 -0
  55. package/lib/public/css/capsule-ui.css +389 -0
  56. package/lib/public/css/command-palette.css +306 -6
  57. package/lib/public/css/home-capsule-library.css +243 -0
  58. package/lib/public/css/home-capsule-source.css +192 -0
  59. package/lib/public/css/home-debate-live.css +146 -0
  60. package/lib/public/css/home-debate-planning.css +125 -0
  61. package/lib/public/css/home-debates-archive.css +281 -0
  62. package/lib/public/css/home-hub.css +896 -405
  63. package/lib/public/css/home-mate-creation.css +34 -0
  64. package/lib/public/css/home-mate-model-picker.css +131 -0
  65. package/lib/public/css/home-mate-settings.css +344 -0
  66. package/lib/public/css/home-session-actions.css +265 -0
  67. package/lib/public/css/home-sidebar.css +667 -0
  68. package/lib/public/css/icon-strip.css +1 -2
  69. package/lib/public/css/input.css +79 -25
  70. package/lib/public/css/mates.css +7 -34
  71. package/lib/public/css/pane.css +1 -1
  72. package/lib/public/css/playbook.css +0 -80
  73. package/lib/public/css/project-logs.css +234 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/sticky-notes.css +53 -117
  76. package/lib/public/css/tui-attention.css +0 -44
  77. package/lib/public/css/workspace-assignment.css +45 -0
  78. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  79. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  80. package/lib/public/index.html +96 -74
  81. package/lib/public/modules/app-connection.js +100 -5
  82. package/lib/public/modules/app-dm.js +43 -160
  83. package/lib/public/modules/app-home-hub.js +328 -585
  84. package/lib/public/modules/app-message-cards.js +242 -0
  85. package/lib/public/modules/app-message-router.js +84 -0
  86. package/lib/public/modules/app-messages.js +124 -38
  87. package/lib/public/modules/app-notifications.js +8 -119
  88. package/lib/public/modules/app-panels.js +43 -2
  89. package/lib/public/modules/app-projects.js +9 -1
  90. package/lib/public/modules/app-rendering.js +21 -602
  91. package/lib/public/modules/avatar-imprint.js +11 -8
  92. package/lib/public/modules/background-tasks-ui.js +139 -16
  93. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  94. package/lib/public/modules/chat-render-runtime.js +198 -0
  95. package/lib/public/modules/command-palette.js +225 -436
  96. package/lib/public/modules/dm-render.js +82 -0
  97. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  98. package/lib/public/modules/home-capsule-library.js +146 -0
  99. package/lib/public/modules/home-capsule-source.js +383 -0
  100. package/lib/public/modules/home-chat-identity.js +35 -0
  101. package/lib/public/modules/home-chat-scroll.js +77 -0
  102. package/lib/public/modules/home-chat-stream-state.js +34 -0
  103. package/lib/public/modules/home-composer-focus.js +25 -0
  104. package/lib/public/modules/home-conversations-sheet.js +179 -0
  105. package/lib/public/modules/home-debate-activity.js +48 -0
  106. package/lib/public/modules/home-debate-controls.js +295 -0
  107. package/lib/public/modules/home-debate-launch.js +41 -0
  108. package/lib/public/modules/home-debate-live.js +284 -0
  109. package/lib/public/modules/home-debate-models.js +73 -0
  110. package/lib/public/modules/home-debate-planning.js +335 -0
  111. package/lib/public/modules/home-debates-archive.js +279 -0
  112. package/lib/public/modules/home-dock-resize.js +74 -0
  113. package/lib/public/modules/home-dock.js +446 -0
  114. package/lib/public/modules/home-mate-chat.js +496 -0
  115. package/lib/public/modules/home-mate-creation.js +109 -0
  116. package/lib/public/modules/home-mate-model-picker.js +281 -0
  117. package/lib/public/modules/home-mate-selection.js +38 -0
  118. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  119. package/lib/public/modules/home-mate-settings.js +383 -0
  120. package/lib/public/modules/home-session-actions.js +373 -0
  121. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  122. package/lib/public/modules/home-shell.js +9 -0
  123. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  124. package/lib/public/modules/home-sidebar.js +165 -0
  125. package/lib/public/modules/home-sub-surface.js +19 -0
  126. package/lib/public/modules/home-surface-boot.js +68 -0
  127. package/lib/public/modules/home-surface.js +141 -0
  128. package/lib/public/modules/home-tools.js +271 -0
  129. package/lib/public/modules/markdown.js +50 -2
  130. package/lib/public/modules/mate-management.js +86 -0
  131. package/lib/public/modules/mate-sidebar.js +0 -8
  132. package/lib/public/modules/mate-wizard.js +0 -7
  133. package/lib/public/modules/mention.js +17 -19
  134. package/lib/public/modules/pane-bridge.js +9 -0
  135. package/lib/public/modules/profile.js +4 -5
  136. package/lib/public/modules/project-logs-ambient.js +238 -0
  137. package/lib/public/modules/project-logs-render.js +359 -0
  138. package/lib/public/modules/project-logs.js +328 -0
  139. package/lib/public/modules/project-settings.js +34 -28
  140. package/lib/public/modules/project-switcher.js +11 -5
  141. package/lib/public/modules/project-workspace-assignment.js +23 -0
  142. package/lib/public/modules/scheduler.js +17 -3
  143. package/lib/public/modules/search-clay-chat.js +402 -0
  144. package/lib/public/modules/server-settings.js +14 -12
  145. package/lib/public/modules/session-actions.js +1 -1
  146. package/lib/public/modules/sidebar-mates.js +56 -552
  147. package/lib/public/modules/sidebar-mobile.js +12 -73
  148. package/lib/public/modules/sidebar-presence.js +37 -0
  149. package/lib/public/modules/sidebar-sessions.js +25 -1
  150. package/lib/public/modules/split-pair-ui.js +13 -13
  151. package/lib/public/modules/split-view.js +7 -0
  152. package/lib/public/modules/sticky-notes.js +106 -53
  153. package/lib/public/modules/tool-input-composition.js +48 -0
  154. package/lib/public/modules/tool-llm-status.js +167 -0
  155. package/lib/public/modules/tool-palette.js +76 -4
  156. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  157. package/lib/public/modules/tool-renderer-chart.js +158 -0
  158. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  159. package/lib/public/modules/tool-renderer.js +464 -0
  160. package/lib/public/modules/tool-runtime.js +309 -0
  161. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  162. package/lib/public/modules/tool-ui-tree.js +21 -0
  163. package/lib/public/modules/tui-attention.js +10 -0
  164. package/lib/public/modules/user-settings.js +16 -23
  165. package/lib/public/modules/vendor-login.js +287 -0
  166. package/lib/public/modules/worker-proposal.js +2 -2
  167. package/lib/public/modules/workspace-assignment-card.js +149 -0
  168. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  169. package/lib/public/style.css +14 -2
  170. package/lib/runtime-env.js +65 -0
  171. package/lib/sdk-bridge.js +120 -32
  172. package/lib/sdk-message-processor.js +34 -13
  173. package/lib/server-home-capsule-creation.js +36 -0
  174. package/lib/server-home-chat-events.js +287 -0
  175. package/lib/server-home-chat.js +518 -0
  176. package/lib/server-home-clay-entry.js +65 -0
  177. package/lib/server-home-clay-session-links.js +26 -0
  178. package/lib/server-home-debate-planning.js +214 -0
  179. package/lib/server-home-debates.js +137 -0
  180. package/lib/server-home-mate-creation.js +261 -0
  181. package/lib/server-home-models.js +241 -0
  182. package/lib/server-home-preferences.js +61 -0
  183. package/lib/server-mates.js +37 -0
  184. package/lib/server-tools.js +377 -0
  185. package/lib/server.js +166 -59
  186. package/lib/session-pair-mcp-server.js +6 -6
  187. package/lib/sessions.js +22 -4
  188. package/lib/tool-capsule-source.js +142 -0
  189. package/lib/tool-control-mcp-server.js +126 -0
  190. package/lib/tool-llm.js +48 -0
  191. package/lib/tool-storage.js +77 -0
  192. package/lib/tool-ui-spec-advanced.js +82 -0
  193. package/lib/tool-ui-spec.js +261 -0
  194. package/lib/tools-registry.js +286 -0
  195. package/lib/users-home-dock-preferences.js +77 -0
  196. package/lib/users-home-surface-preferences.js +99 -0
  197. package/lib/users-preferences.js +4 -7
  198. package/lib/users.js +12 -0
  199. package/lib/whats-new-content.js +3 -3
  200. package/lib/workspace-assignment-service.js +420 -0
  201. package/lib/workspace-query-mcp-server.js +154 -0
  202. package/lib/workspace-query-service.js +492 -0
  203. package/lib/ws-schema.js +107 -12
  204. package/lib/yoke/adapters/acp.js +2 -1
  205. package/lib/yoke/adapters/antigravity.js +2 -1
  206. package/lib/yoke/adapters/claude-worker.js +11 -4
  207. package/lib/yoke/adapters/claude.js +141 -46
  208. package/lib/yoke/adapters/codex.js +79 -11
  209. package/lib/yoke/adapters/kiro.js +2 -1
  210. package/lib/yoke/claude-user-input.js +38 -0
  211. package/lib/yoke/codex-app-server.js +20 -2
  212. package/lib/yoke/codex-background-tasks.js +8 -2
  213. package/lib/yoke/codex-user-input.js +64 -0
  214. package/lib/yoke/complete-once.js +164 -0
  215. package/lib/yoke/index.js +5 -1
  216. package/lib/yoke/interface.js +19 -0
  217. package/lib/yoke/mcp-bridge-server.js +2 -6
  218. package/lib/yoke/user-input.js +254 -0
  219. package/package.json +4 -3
  220. package/lib/public/css/home-chat.css +0 -380
  221. package/lib/public/modules/home-chat.js +0 -438
  222. package/lib/server-clay-home.js +0 -245
@@ -7,7 +7,6 @@ import { iconHtml, refreshIcons } from './icons.js';
7
7
  import { parseEmojis } from './markdown.js';
8
8
  import { getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
9
9
  import { openCommandPalette } from './command-palette.js';
10
- import { getMateSessions } from './mate-sidebar.js';
11
10
  import { openProjectSettings } from './project-settings.js';
12
11
  import {
13
12
  getCachedSessions,
@@ -33,8 +32,8 @@ import { store } from './store.js';
33
32
  import { getWs } from './ws-ref.js';
34
33
  import { dismissOverlayPanels, closeSidebar } from './sidebar.js';
35
34
  import { switchProject, getCachedProjects } from './app-projects.js';
36
- import { openDm } from './app-dm.js';
37
35
  import { showHomeHub } from './app-home-hub.js';
36
+ import { openHomeChat } from './home-mate-chat.js';
38
37
  import { openTerminal } from './terminal.js';
39
38
  import { requestKnowledgeList } from './mate-knowledge.js';
40
39
  import { loadRootDirectory } from './filebrowser.js';
@@ -319,9 +318,9 @@ function renderSheetSessions(listEl) {
319
318
  if (type === "project") {
320
319
  renderMobileSessionsInto(sessionListEl);
321
320
  } else if (type === "mate") {
322
- // Mate DM: open the DM and show mate actions
323
- openDm(mateId);
324
- renderMateMobileActions(sessionListEl);
321
+ closeMobileSheet();
322
+ showHomeHub();
323
+ openHomeChat(mateId);
325
324
  }
326
325
 
327
326
  refreshIcons();
@@ -370,12 +369,8 @@ function renderSheetSessions(listEl) {
370
369
  // Track that chat sheet is open
371
370
  mobileChatSheetOpen = true;
372
371
 
373
- // --- Initial render: show mate actions if DM active, otherwise project sessions ---
374
- if (getCurrentDmUserId()) {
375
- renderSessionsForContext("mate", null, getCurrentDmUserId());
376
- } else {
377
- renderSessionsForContext("project", getCachedCurrentSlug(), null);
378
- }
372
+ // Mate chips navigate away immediately, so the sheet always renders project sessions.
373
+ renderSessionsForContext("project", getCachedCurrentSlug(), null);
379
374
  }
380
375
 
381
376
  // Helper: create a mobile session item element
@@ -659,54 +654,6 @@ function createMobileLoopGroup(loopId, children, groupKey) {
659
654
  return wrapper;
660
655
  }
661
656
 
662
- function renderMateMobileActions(container) {
663
- var newSessionBtn = document.createElement("button");
664
- newSessionBtn.className = "mobile-session-new";
665
- newSessionBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
666
- newSessionBtn.addEventListener("click", function () {
667
- if (getWs() && store.get('connected')) {
668
- getWs().send(JSON.stringify({ type: "new_session" }));
669
- }
670
- closeMobileSheet();
671
- });
672
- container.appendChild(newSessionBtn);
673
-
674
- var debateBtn = document.createElement("button");
675
- debateBtn.className = "mobile-session-new";
676
- debateBtn.innerHTML = '<i data-lucide="mic" style="width:16px;height:16px"></i> New debate';
677
- debateBtn.addEventListener("click", function () {
678
- closeMobileSheet();
679
- var targetBtn = document.getElementById("mate-debate-btn");
680
- if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
681
- });
682
- container.appendChild(debateBtn);
683
-
684
- // Render mate session list
685
- var mateSessions = getMateSessions();
686
- if (mateSessions.length > 0) {
687
- var sorted = mateSessions.slice().sort(function (a, b) {
688
- return (b.lastActivity || 0) - (a.lastActivity || 0);
689
- });
690
-
691
- var currentGroup = "";
692
- for (var i = 0; i < sorted.length; i++) {
693
- var s = sorted[i];
694
- var group = getDateGroup(s.lastActivity || 0);
695
- if (group !== currentGroup) {
696
- currentGroup = group;
697
- var header = document.createElement("div");
698
- header.className = "mobile-sheet-group";
699
- header.textContent = group;
700
- container.appendChild(header);
701
- }
702
- var mateItem = createMobileSessionItem(s);
703
- container.appendChild(mateItem);
704
- }
705
- }
706
-
707
- refreshIcons();
708
- }
709
-
710
657
  // Helper: render sorted sessions into a container with date groups (with loop session grouping)
711
658
  function renderMobileSessionsInto(container) {
712
659
  // Mirrors the desktop sidebar's split button: one "New session" action on
@@ -902,11 +849,7 @@ export function refreshMobileChatSheet() {
902
849
 
903
850
  // Re-render sessions for current context
904
851
  sessionListEl.innerHTML = "";
905
- if (getCurrentDmUserId()) {
906
- renderMateMobileActions(sessionListEl);
907
- } else {
908
- renderMobileSessionsInto(sessionListEl);
909
- }
852
+ renderMobileSessionsInto(sessionListEl);
910
853
 
911
854
  refreshIcons();
912
855
  }
@@ -944,8 +887,7 @@ function renderSheetMateProfile(listEl) {
944
887
  var actions = [
945
888
  { icon: "book-open", label: "Knowledge", btnId: "mate-knowledge-btn", countId: "mate-knowledge-count" },
946
889
  { icon: "sticky-note", label: "Sticky Notes", btnId: "sticky-notes-sidebar-btn", countId: "sticky-notes-sidebar-count" },
947
- { icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" },
948
- { icon: "calendar", label: "Scheduled Tasks", btnId: "mate-scheduler-btn" }
890
+ { icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" }
949
891
  ];
950
892
 
951
893
  for (var i = 0; i < actions.length; i++) {
@@ -1062,12 +1004,11 @@ function renderSheetTools(listEl) {
1062
1004
  { icon: "brain", label: "Memory", action: "mate-memory" },
1063
1005
  { icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
1064
1006
  { icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
1065
- { icon: "puzzle", label: "Skills", action: "mate-skills" },
1066
- { icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" }
1007
+ { icon: "puzzle", label: "Skills", action: "mate-skills" }
1067
1008
  ] : [
1068
1009
  { icon: "folder-tree", label: "Files", action: "files" },
1069
1010
  { icon: "square-terminal", label: "Terminal", action: "terminal" },
1070
- { icon: "calendar-clock", label: "Scheduled Tasks", action: "scheduler" }
1011
+ { icon: "notebook-tabs", label: "Logs", action: "project-logs" }
1071
1012
  ];
1072
1013
 
1073
1014
  for (var i = 0; i < items.length; i++) {
@@ -1082,8 +1023,8 @@ function renderSheetTools(listEl) {
1082
1023
  setTimeout(function () { openMobileSheet("files"); }, 250);
1083
1024
  } else if (item.action === "terminal") {
1084
1025
  openTerminal();
1085
- } else if (item.action === "scheduler") {
1086
- targetId = "scheduler-btn";
1026
+ } else if (item.action === "project-logs") {
1027
+ targetId = "project-logs-btn";
1087
1028
  } else if (item.action === "mate-knowledge") {
1088
1029
  setTimeout(function () { openMobileSheet("mate-knowledge"); }, 250);
1089
1030
  return;
@@ -1093,8 +1034,6 @@ function renderSheetTools(listEl) {
1093
1034
  targetId = "mate-skills-btn";
1094
1035
  } else if (item.action === "mate-memory") {
1095
1036
  targetId = "mate-memory-btn";
1096
- } else if (item.action === "mate-scheduler") {
1097
- targetId = "mate-scheduler-btn";
1098
1037
  } else if (item.action === "mate-debate") {
1099
1038
  targetId = "mate-debate-btn";
1100
1039
  }
@@ -0,0 +1,37 @@
1
+ // Compact online-user presence renderer for the project sidebar.
2
+
3
+ import { userAvatarUrl } from './avatar.js';
4
+
5
+ var lastSidebarPresenceIds = [];
6
+
7
+ export function renderSidebarPresence(onlineUsers) {
8
+ var container = document.getElementById("sidebar-presence");
9
+ if (!container) return;
10
+ if (!onlineUsers || onlineUsers.length < 2) {
11
+ if (lastSidebarPresenceIds.length > 0) {
12
+ lastSidebarPresenceIds = [];
13
+ container.innerHTML = "";
14
+ }
15
+ return;
16
+ }
17
+ var newIds = onlineUsers.map(function (user) { return user.id; }).sort();
18
+ if (newIds.length === lastSidebarPresenceIds.length && newIds.every(function (id, index) { return id === lastSidebarPresenceIds[index]; })) return;
19
+ lastSidebarPresenceIds = newIds;
20
+ container.innerHTML = "";
21
+ var maxShow = 4;
22
+ for (var i = 0; i < Math.min(onlineUsers.length, maxShow); i++) {
23
+ var user = onlineUsers[i];
24
+ var image = document.createElement("img");
25
+ image.className = "sidebar-presence-avatar";
26
+ image.src = userAvatarUrl(user, 24);
27
+ image.alt = user.displayName;
28
+ image.dataset.tip = user.displayName + " (@" + user.username + ")";
29
+ container.appendChild(image);
30
+ }
31
+ if (onlineUsers.length > maxShow) {
32
+ var more = document.createElement("span");
33
+ more.className = "sidebar-presence-more";
34
+ more.textContent = "+" + (onlineUsers.length - maxShow);
35
+ container.appendChild(more);
36
+ }
37
+ }
@@ -1376,8 +1376,29 @@ function renderSplitGroupItem(group, members) {
1376
1376
 
1377
1377
  // --- Main session list ---
1378
1378
 
1379
+ // Fingerprint of the last completed render: the whole session payload plus the
1380
+ // two store values this render also reads. Nothing is cherry-picked, so the
1381
+ // guard can only skip a frame that is byte-identical to what is already on
1382
+ // screen. A re-render triggered by other state (sessions === null) always
1383
+ // runs and resets the fingerprint afterwards.
1384
+ var lastRenderFingerprint = null;
1385
+
1386
+ function sessionRenderFingerprint(sessions) {
1387
+ try {
1388
+ return JSON.stringify([sessions, store.get('splitGroups') || [], store.get('splitPanes') || null]);
1389
+ } catch (e) {
1390
+ return null;
1391
+ }
1392
+ }
1393
+
1379
1394
  export function renderSessionList(sessions) {
1380
- if (sessions) cachedSessions = sessions;
1395
+ if (sessions) {
1396
+ // A reconnect and a streaming turn both re-send this list unchanged;
1397
+ // rebuilding the sidebar for an identical frame is what made it flicker.
1398
+ var incoming = sessionRenderFingerprint(sessions);
1399
+ if (incoming !== null && incoming === lastRenderFingerprint) return;
1400
+ cachedSessions = sessions;
1401
+ }
1381
1402
 
1382
1403
  // If mobile chat sheet is open, refresh it
1383
1404
  if (refreshMobileChatSheet) refreshMobileChatSheet();
@@ -1516,6 +1537,9 @@ export function renderSessionList(sessions) {
1516
1537
  refreshIcons();
1517
1538
  if (updatePageTitle) updatePageTitle();
1518
1539
  syncHeaderSearchUi();
1540
+ // Recorded from the sessions actually rendered, so a null-argument re-render
1541
+ // driven by other state refreshes the fingerprint rather than stranding it.
1542
+ lastRenderFingerprint = sessionRenderFingerprint(cachedSessions);
1519
1543
  }
1520
1544
 
1521
1545
  // --- Search results ---
@@ -5,8 +5,8 @@ import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAg
5
5
  import { showToast } from './utils.js';
6
6
 
7
7
  var pairDialog = null;
8
- // Set when the dialog was opened via "Add Worker" on an existing session:
9
- // that session becomes the Driver and the dialog only configures the Worker.
8
+ // Set when the dialog was opened via "Add Split Worker" on an existing session:
9
+ // that session becomes the Driver and the dialog only configures the Split Worker.
10
10
  var pendingDriver = null;
11
11
 
12
12
  function closePairDialog() {
@@ -35,7 +35,7 @@ function roleColumn(role, description, vendorSelect, modelSelect, effortSelect,
35
35
  col.className = "pair-modal-role";
36
36
  var heading = document.createElement("div");
37
37
  heading.className = "pair-modal-role-heading";
38
- heading.innerHTML = '<span class="pair-role-dot pair-role-' + role.toLowerCase() + '"></span><div><strong>' + role + '</strong><span>' + description + '</span></div>';
38
+ heading.innerHTML = '<span class="pair-role-dot pair-role-' + role.toLowerCase().replace(/ /g, "-") + '"></span><div><strong>' + role + '</strong><span>' + description + '</span></div>';
39
39
  col.appendChild(heading);
40
40
  var vendorField = fieldLabel("Vendor", vendorSelect);
41
41
  col.appendChild(vendorField.fragment);
@@ -82,13 +82,13 @@ export function showPairDialog(options) {
82
82
 
83
83
  var title = document.createElement("div");
84
84
  title.className = "wt-modal-title";
85
- title.textContent = driverInfo ? "Add Worker" : "Pair session";
85
+ title.textContent = driverInfo ? "Add Split Worker" : "Pair session";
86
86
  modal.appendChild(title);
87
87
  var desc = document.createElement("div");
88
88
  desc.className = "pair-modal-desc";
89
89
  desc.textContent = driverInfo
90
- ? "This session becomes the Driver; a new Worker opens beside it."
91
- : "One Driver plans and directs a visible Worker session.";
90
+ ? "This session becomes the Driver; a new Split Worker opens beside it."
91
+ : "One Driver plans and directs a visible Split Worker session.";
92
92
  modal.appendChild(desc);
93
93
 
94
94
  var driverVendor = buildAgentVendorSelect(installed, installed.indexOf("claude") !== -1 ? "claude" : installed[0]);
@@ -137,7 +137,7 @@ export function showPairDialog(options) {
137
137
  } else {
138
138
  roles.appendChild(roleColumn("Driver", "Plans, delegates, and integrates", driverVendor, driverModel, driverEffort, options));
139
139
  }
140
- roles.appendChild(roleColumn("Worker", "Executes delegated tasks", workerVendor, workerModel, workerEffort, options));
140
+ roles.appendChild(roleColumn("Split Worker", "Executes delegated tasks", workerVendor, workerModel, workerEffort, options));
141
141
  modal.appendChild(roles);
142
142
 
143
143
  var actions = document.createElement("div");
@@ -149,7 +149,7 @@ export function showPairDialog(options) {
149
149
  var createBtn = document.createElement("button");
150
150
  createBtn.type = "button";
151
151
  createBtn.className = "wt-modal-btn primary";
152
- createBtn.textContent = driverInfo ? "Add Worker" : "Create pair";
152
+ createBtn.textContent = driverInfo ? "Add Split Worker" : "Create pair";
153
153
  actions.appendChild(cancelBtn);
154
154
  actions.appendChild(createBtn);
155
155
  modal.appendChild(actions);
@@ -218,7 +218,7 @@ export function handleSplitDelegation(msg) {
218
218
  if (msg.active) states[msg.groupId] = msg;
219
219
  else delete states[msg.groupId];
220
220
  store.set({ splitDelegations: states });
221
- // Inside the worker's own pane, mirror the delegation into the composer
221
+ // Inside the Split Worker's own pane, mirror the delegation into the composer
222
222
  // notice (the delegated user_message shows it too; this covers the end).
223
223
  if (store.get('paneMode') && msg.to === store.get('activeSessionId')) {
224
224
  if (msg.active) showWorkerDelegationNotice(null);
@@ -252,7 +252,7 @@ export function syncPairChrome(host, split) {
252
252
  setBtn.type = "button";
253
253
  setBtn.className = "split-pair-set-driver";
254
254
  setBtn.textContent = "Set Driver";
255
- setBtn.title = "Make this session the Driver; the other pane becomes its Worker";
255
+ setBtn.title = "Make this session the Driver; the other pane becomes its Split Worker";
256
256
  setBtn.addEventListener("click", function () { sendSetPair(group.id, paneSessionId); });
257
257
  var titleEl = header.querySelector(".split-pane-title");
258
258
  var accessEl = header.querySelector(".split-pane-full-access");
@@ -264,11 +264,11 @@ export function syncPairChrome(host, split) {
264
264
  for (var pi = 0; pi < split.panes.length && pi < headers.length; pi++) {
265
265
  (function (sessionId, header) {
266
266
  var isDriver = sessionId === group.pair.driverId;
267
- var role = isDriver ? "Driver" : "Worker";
267
+ var role = isDriver ? "Driver" : "Split Worker";
268
268
  var badge = document.createElement("span");
269
- badge.className = "split-pair-role split-pair-role-" + role.toLowerCase();
269
+ badge.className = "split-pair-role split-pair-role-" + role.toLowerCase().replace(/ /g, "-");
270
270
  badge.textContent = role;
271
- badge.title = isDriver ? "Driver controls this Worker" : "Worker controlled by the Driver";
271
+ badge.title = isDriver ? "Driver controls this Split Worker" : "Split Worker controlled by the Driver";
272
272
  var title = header.querySelector(".split-pane-title");
273
273
  var access = header.querySelector(".split-pane-full-access");
274
274
  header.insertBefore(badge, access ? access.nextSibling : (title ? title.nextSibling : null));
@@ -11,6 +11,7 @@ import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
11
11
  import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
13
  import { presentMarkdownEdit } from './filebrowser.js';
14
+ import { autoStartLoginIfNeeded } from './vendor-login.js';
14
15
 
15
16
  var host = null;
16
17
  var nativeApp = null;
@@ -276,6 +277,12 @@ function handlePaneMessage(event) {
276
277
  if (present) presentMarkdownEdit(present);
277
278
  return;
278
279
  }
280
+ // Both panes can report auth_required for their own sessions. The shell runs
281
+ // one login flow for the project; repeat events are no-ops there.
282
+ if (msg.type === "clay-pane-auth-required") {
283
+ if (msg.message) autoStartLoginIfNeeded(msg.message);
284
+ return;
285
+ }
279
286
  if (msg.type !== "clay-pane-context") return;
280
287
  var frames = host.querySelectorAll(".split-pane-frame");
281
288
  for (var i = 0; i < frames.length; i++) {
@@ -11,7 +11,7 @@ var textTimers = {};
11
11
  var colorPickerEl = null;
12
12
  var formatToolbarEl = null;
13
13
 
14
- var NOTE_COLORS = ["yellow", "blue", "green", "pink", "orange", "purple"];
14
+ var NOTE_COLORS = ["purple", "green", "yellow", "blue", "pink", "orange"];
15
15
 
16
16
 
17
17
  function getContainerBounds() {
@@ -98,7 +98,8 @@ export function createNote() {
98
98
  type: "note_create",
99
99
  x: 60 + offset,
100
100
  y: 60 + offset,
101
- color: "yellow",
101
+ color: "purple",
102
+ opacity: 0.64,
102
103
  });
103
104
  }
104
105
 
@@ -154,7 +155,7 @@ function renderNote(data) {
154
155
  el.style.width = data.w + "px";
155
156
  el.style.height = data.h + "px";
156
157
  el.style.zIndex = 100 + (data.zIndex || 0);
157
- el.dataset.color = data.color || "yellow";
158
+ el.dataset.color = data.color || "purple";
158
159
 
159
160
  if (data.minimized) el.classList.add("minimized");
160
161
  if (data.hidden) el.classList.add("hidden");
@@ -217,7 +218,7 @@ function renderNote(data) {
217
218
  opacitySlider.type = "range";
218
219
  opacitySlider.min = "20";
219
220
  opacitySlider.max = "100";
220
- opacitySlider.value = String(Math.round((data.opacity || 1) * 100));
221
+ opacitySlider.value = String(Math.round((typeof data.opacity === "number" ? data.opacity : 0.64) * 100));
221
222
  opacitySlider.className = "sticky-note-opacity-slider";
222
223
  opacitySlider.title = "Opacity";
223
224
  opacityWrap.appendChild(opacitySlider);
@@ -754,17 +755,44 @@ export function handleNotesList(msg) {
754
755
  var container = document.getElementById("sticky-notes-container");
755
756
  if (!container) return;
756
757
 
757
- // Clear existing
758
- container.innerHTML = "";
759
- notes.clear();
760
-
758
+ // Keyed reconciliation, never a clear-and-rebuild. A reconnect re-sends this
759
+ // list unchanged; tearing every note down and recreating it made all the
760
+ // floating cards vanish and reappear, and destroyed any open editor.
761
761
  var list = msg.notes || [];
762
- for (var i = 0; i < list.length; i++) {
763
- var el = renderNote(list[i]);
764
- notes.set(list[i].id, { data: list[i], el: el });
762
+ var seen = {};
763
+ var created = false;
764
+ var i;
765
+
766
+ for (i = 0; i < list.length; i++) {
767
+ var data = list[i];
768
+ if (!data || !data.id) continue;
769
+ seen[data.id] = true;
770
+ var entry = notes.get(data.id);
771
+ if (entry && entry.el && entry.el.parentNode === container) {
772
+ patchNote(entry, data);
773
+ continue;
774
+ }
775
+ var el = renderNote(data);
776
+ notes.set(data.id, { data: data, el: el });
765
777
  container.appendChild(el);
778
+ created = true;
779
+ }
780
+
781
+ // Remove notes the server no longer lists.
782
+ var staleIds = [];
783
+ notes.forEach(function (value, id) {
784
+ if (!seen[id]) staleIds.push(id);
785
+ });
786
+ for (i = 0; i < staleIds.length; i++) {
787
+ var stale = notes.get(staleIds[i]);
788
+ if (stale && stale.el && stale.el.parentNode) stale.el.parentNode.removeChild(stale.el);
789
+ notes.delete(staleIds[i]);
766
790
  }
767
791
 
792
+ // renderNote already refreshes icons for the nodes it builds; an all-patch
793
+ // pass introduces no new Lucide markup and must not trigger a document scan.
794
+ if (created) refreshIcons();
795
+
768
796
  updateBadge();
769
797
  updateSidebarBadge();
770
798
 
@@ -800,59 +828,84 @@ export function handleNoteCreated(msg) {
800
828
  }
801
829
  }
802
830
 
803
- export function handleNoteUpdated(msg) {
804
- if (!msg.note) return;
805
- var entry = notes.get(msg.note.id);
806
- if (!entry) return;
807
-
808
- entry.data = msg.note;
809
-
810
- // Update DOM
811
- entry.el.style.left = msg.note.x + "px";
812
- entry.el.style.top = msg.note.y + "px";
813
- entry.el.style.width = msg.note.w + "px";
814
- entry.el.style.height = msg.note.h + "px";
815
- entry.el.style.zIndex = 100 + (msg.note.zIndex || 0);
816
- entry.el.dataset.color = msg.note.color || "yellow";
817
-
818
- // Update text only if not actively editing (check rendered or textarea focus)
819
- var textarea = entry.el.querySelector(".sticky-note-text");
820
- var rendered = entry.el.querySelector(".sticky-note-rendered");
821
- if (rendered && rendered !== document.activeElement && textarea !== document.activeElement) {
822
- if (textarea) textarea.value = msg.note.text || "";
823
- if (msg.note.text && msg.note.text.trim()) {
824
- rendered.innerHTML = renderMiniMarkdown(msg.note.text);
831
+ // Patch one existing note element in place from a server payload.
832
+ //
833
+ // Geometry, z-order, colour, and opacity are plain style writes and are always
834
+ // applied. The rendered markdown is only replaced when the text actually
835
+ // changed and the note is not being edited, so a geometry-only or z-only
836
+ // update never destroys the user's caret, selection, or in-progress draft.
837
+ // Returns true when icon markup was replaced, so the caller can decide whether
838
+ // a Lucide pass is needed at all.
839
+ function patchNote(entry, next) {
840
+ if (!entry || !next) return false;
841
+ var previous = entry.data || {};
842
+ entry.data = next;
843
+ var el = entry.el;
844
+
845
+ el.style.left = next.x + "px";
846
+ el.style.top = next.y + "px";
847
+ el.style.width = next.w + "px";
848
+ el.style.height = next.h + "px";
849
+ el.style.zIndex = 100 + (next.zIndex || 0);
850
+ el.dataset.color = next.color || "purple";
851
+
852
+ var textarea = el.querySelector(".sticky-note-text");
853
+ var rendered = el.querySelector(".sticky-note-rendered");
854
+ var nextText = next.text || "";
855
+ var editing = rendered === document.activeElement || textarea === document.activeElement;
856
+ // A draft the user typed but has not committed must survive a redraw.
857
+ var hasDraft = !!(textarea && textarea.value !== (previous.text || ""));
858
+ var textChanged = nextText !== (previous.text || "");
859
+ if (rendered && !editing && !hasDraft && textChanged) {
860
+ if (textarea) textarea.value = nextText;
861
+ if (nextText.trim()) {
862
+ rendered.innerHTML = renderMiniMarkdown(nextText);
825
863
  rendered.classList.remove("is-empty");
826
864
  } else {
827
865
  rendered.innerHTML = "";
828
866
  rendered.classList.add("is-empty");
829
867
  }
830
- syncTitle(entry.el, msg.note.text);
868
+ syncTitle(el, nextText);
831
869
  }
832
870
 
833
- // Handle opacity
834
- if (typeof msg.note.opacity === "number") {
835
- entry.el.style.setProperty("--note-opacity", msg.note.opacity);
836
- var slider = entry.el.querySelector(".sticky-note-opacity-slider");
837
- if (slider) slider.value = String(Math.round(msg.note.opacity * 100));
871
+ if (typeof next.opacity === "number") {
872
+ el.style.setProperty("--note-opacity", next.opacity);
873
+ var slider = el.querySelector(".sticky-note-opacity-slider");
874
+ if (slider) slider.value = String(Math.round(next.opacity * 100));
838
875
  }
839
876
 
840
- // Handle hidden state
841
- if (msg.note.hidden) {
842
- entry.el.classList.add("hidden");
877
+ if (next.hidden) el.classList.add("hidden");
878
+ else el.classList.remove("hidden");
879
+
880
+ // The minimize button swaps its icon, which is the only path here that can
881
+ // introduce fresh Lucide markup.
882
+ var iconsReplaced = false;
883
+ var wasMinimized = el.classList.contains("minimized");
884
+ var minBtn = el.querySelector(".sticky-note-min-btn");
885
+ if (next.minimized) {
886
+ el.classList.add("minimized");
887
+ if (minBtn && !wasMinimized) {
888
+ minBtn.innerHTML = iconHtml("maximize-2");
889
+ minBtn.title = "Expand";
890
+ iconsReplaced = true;
891
+ }
843
892
  } else {
844
- entry.el.classList.remove("hidden");
893
+ el.classList.remove("minimized");
894
+ if (minBtn && wasMinimized) {
895
+ minBtn.innerHTML = iconHtml("minus");
896
+ minBtn.title = "Minimize";
897
+ iconsReplaced = true;
898
+ }
845
899
  }
900
+ return iconsReplaced;
901
+ }
846
902
 
847
- var minBtn = entry.el.querySelector(".sticky-note-min-btn");
848
- if (msg.note.minimized) {
849
- entry.el.classList.add("minimized");
850
- if (minBtn) { minBtn.innerHTML = iconHtml("maximize-2"); minBtn.title = "Expand"; }
851
- } else {
852
- entry.el.classList.remove("minimized");
853
- if (minBtn) { minBtn.innerHTML = iconHtml("minus"); minBtn.title = "Minimize"; }
854
- }
855
- refreshIcons();
903
+ export function handleNoteUpdated(msg) {
904
+ if (!msg.note) return;
905
+ var entry = notes.get(msg.note.id);
906
+ if (!entry) return;
907
+
908
+ if (patchNote(entry, msg.note)) refreshIcons();
856
909
 
857
910
  // Re-render archive if open (text or color may have changed)
858
911
  if (archiveOpen) renderArchiveCards();
@@ -926,7 +979,7 @@ function renderArchiveCards() {
926
979
  (function (noteData) {
927
980
  var card = document.createElement("div");
928
981
  card.className = "notes-archive-card" + (noteData.data.hidden ? " archived" : "");
929
- card.dataset.color = noteData.data.color || "yellow";
982
+ card.dataset.color = noteData.data.color || "purple";
930
983
 
931
984
  // Header with title + delete
932
985
  var header = document.createElement("div");
@@ -0,0 +1,48 @@
1
+ // Composition-safe action binding for declarative Capsule text controls.
2
+
3
+ var inputStates = new WeakMap();
4
+
5
+ export function isToolTextInputComposing(input) {
6
+ var state = inputStates.get(input);
7
+ return !!state && state.composing;
8
+ }
9
+
10
+ export function bindToolTextInput(input, commit, options) {
11
+ options = options || {};
12
+ var state = { composing: false, trailingValue: null };
13
+ inputStates.set(input, state);
14
+
15
+ function isAlive() {
16
+ return typeof options.isAlive !== "function" || options.isAlive(input);
17
+ }
18
+
19
+ input.addEventListener("compositionstart", function () {
20
+ state.composing = true;
21
+ state.trailingValue = null;
22
+ });
23
+
24
+ input.addEventListener("beforeinput", function (event) {
25
+ if (state.trailingValue === null && event && (event.isComposing || event.inputType === "insertCompositionText")) {
26
+ state.composing = true;
27
+ }
28
+ });
29
+
30
+ input.addEventListener("compositionend", function () {
31
+ state.composing = false;
32
+ state.trailingValue = input.value;
33
+ if (!isAlive()) return;
34
+ commit(input.value);
35
+ if (typeof options.onCompositionEnd === "function") options.onCompositionEnd(input);
36
+ });
37
+
38
+ input.addEventListener("input", function (event) {
39
+ if (state.composing || (event && event.isComposing)) return;
40
+ if (state.trailingValue !== null && input.value === state.trailingValue) {
41
+ state.trailingValue = null;
42
+ return;
43
+ }
44
+ state.trailingValue = null;
45
+ if (!isAlive()) return;
46
+ commit(input.value);
47
+ });
48
+ }