clay-server 4.0.0-beta.8 → 4.0.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 (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -1,43 +1,62 @@
1
+ // The sticky-note canvas: the board of floating cards over the conversation.
2
+ //
3
+ // This module owns the notes map, visibility, the badges, and the WebSocket
4
+ // handlers. Card markup and interactions live in sticky-notes-card.js, text
5
+ // editing in sticky-notes-editor.js, and the leaf helpers in
6
+ // sticky-notes-shared.js. The browse/index pane is sticky-notes-browser.js.
7
+
1
8
  import { refreshIcons, iconHtml } from './icons.js';
2
- import { VENDOR_AVATARS } from './app-rendering.js';
3
- import { extractMarkdown, getTitle, renderMiniMarkdown } from './sticky-note-markdown.js';
9
+ import { renderMiniMarkdown } from './sticky-note-markdown.js';
10
+ import { enhanceClayLogLinks } from './clay-log-links.js';
11
+ import { isClosedNote, send, clampPos, clearNoteTimers, syncTitle } from './sticky-notes-shared.js';
12
+ import { renderNote, closeColorPicker } from './sticky-notes-card.js';
13
+ import { closeFormatToolbar, isFormatToolbarOpen } from './sticky-notes-editor.js';
4
14
 
5
- var ctx;
6
15
  var notes = new Map(); // id -> { data, el }
7
16
  var notesVisible = false;
8
- var archiveOpen = false;
9
- var updateTimers = {};
10
- var textTimers = {};
11
- var colorPickerEl = null;
12
- var formatToolbarEl = null;
13
17
 
14
- var NOTE_COLORS = ["purple", "green", "yellow", "blue", "pink", "orange"];
18
+ // The canvas shows open notes only. Closed notes leave the canvas and live in
19
+ // the browser's Closed tab until they are reopened.
20
+ function openCount() {
21
+ var count = 0;
22
+ notes.forEach(function (entry) {
23
+ if (entry && !isClosedNote(entry.data)) count++;
24
+ });
25
+ return count;
26
+ }
27
+
28
+ // The browser pane registers here rather than being imported, so the canvas
29
+ // module and the browser module never form an import cycle.
30
+ var browserRefresh = null;
15
31
 
32
+ export function setBrowserRefresh(fn) {
33
+ browserRefresh = typeof fn === "function" ? fn : null;
34
+ }
16
35
 
17
- function getContainerBounds() {
18
- var c = document.getElementById("sticky-notes-container");
19
- if (!c || c.clientWidth === 0 || c.clientHeight === 0) return null;
20
- return { w: c.clientWidth, h: c.clientHeight };
36
+ function refreshBrowser() {
37
+ if (browserRefresh) browserRefresh();
21
38
  }
22
39
 
23
- function clampPos(x, y, noteW, noteH) {
24
- var b = getContainerBounds();
25
- if (!b) return { x: x, y: y };
26
- return {
27
- x: Math.max(0, Math.min(x, b.w - noteW)),
28
- y: Math.max(0, Math.min(y, b.h - noteH)),
29
- };
40
+ // Read-only view of every note the client knows about, for the browser pane.
41
+ export function listNoteData() {
42
+ var out = [];
43
+ notes.forEach(function (entry) { if (entry && entry.data) out.push(entry.data); });
44
+ return out;
30
45
  }
31
46
 
32
- function clampSize(x, y, w, h) {
33
- var b = getContainerBounds();
34
- if (!b) return { w: w, h: h };
35
- return {
36
- w: Math.min(w, b.w - x),
37
- h: Math.min(h, b.h - y),
38
- };
47
+ // Bring one open note to the front of the canvas and flash it.
48
+ export function focusNoteOnCanvas(id) {
49
+ var entry = notes.get(id);
50
+ if (!entry || isClosedNote(entry.data)) return;
51
+ showNotes();
52
+ send({ type: "note_bring_front", id: id });
53
+ if (entry.data.minimized) send({ type: "note_update", id: id, minimized: false });
54
+ if (!entry.el) return;
55
+ entry.el.classList.add("note-flash");
56
+ setTimeout(function () { if (entry.el) entry.el.classList.remove("note-flash"); }, 600);
39
57
  }
40
58
 
59
+
41
60
  function reclampAllNotes() {
42
61
  notes.forEach(function (entry) {
43
62
  var el = entry.el;
@@ -51,12 +70,10 @@ function reclampAllNotes() {
51
70
  });
52
71
  }
53
72
 
54
- export function initStickyNotes(_ctx) {
55
- ctx = _ctx;
56
-
73
+ export function initStickyNotes() {
57
74
  // Close format toolbar on outside click
58
75
  document.addEventListener("mousedown", function (e) {
59
- if (formatToolbarEl && !e.target.closest(".sn-format-toolbar") && !e.target.closest(".sticky-note-text") && !e.target.closest(".sticky-note-rendered")) {
76
+ if (isFormatToolbarOpen() && !e.target.closest(".sn-format-toolbar") && !e.target.closest(".sticky-note-text") && !e.target.closest(".sticky-note-rendered")) {
60
77
  closeFormatToolbar();
61
78
  }
62
79
  });
@@ -94,7 +111,7 @@ export function createNote() {
94
111
  if (!container) return;
95
112
  // Scatter position so notes don't stack exactly
96
113
  var offset = (notes.size % 5) * 30;
97
- wsSend({
114
+ send({
98
115
  type: "note_create",
99
116
  x: 60 + offset,
100
117
  y: 60 + offset,
@@ -106,673 +123,66 @@ export function createNote() {
106
123
  function updateBadge() {
107
124
  var badge = document.querySelector(".sticky-notes-count");
108
125
  if (!badge) return;
109
- if (notes.size > 0) {
110
- badge.textContent = notes.size;
126
+ var count = openCount();
127
+ if (count > 0) {
128
+ badge.textContent = count;
111
129
  badge.classList.remove("hidden");
112
130
  } else {
113
131
  badge.classList.add("hidden");
114
132
  }
115
133
  }
116
134
 
117
- // --- WS send helpers ---
118
-
119
- function wsSend(obj) {
120
- if (ctx && ctx.ws && ctx.connected) {
121
- ctx.ws.send(JSON.stringify(obj));
122
- }
123
- }
124
-
125
- function debouncedUpdate(id, changes, delay) {
126
- clearTimeout(updateTimers[id]);
127
- updateTimers[id] = setTimeout(function () {
128
- changes.type = "note_update";
129
- changes.id = id;
130
- wsSend(changes);
131
- }, delay || 300);
132
- }
133
-
134
- function debouncedTextUpdate(id, text) {
135
- clearTimeout(textTimers[id]);
136
- textTimers[id] = setTimeout(function () {
137
- wsSend({ type: "note_update", id: id, text: text });
138
- }, 500);
139
- }
140
-
141
- function syncTitle(noteEl, text) {
142
- var spacer = noteEl.querySelector(".sticky-note-spacer");
143
- if (spacer) spacer.textContent = getTitle(text) || "Untitled";
144
- }
145
-
146
- // --- Note rendering ---
147
-
148
- function renderNote(data) {
149
- var el = document.createElement("div");
150
- el.className = "sticky-note";
151
- el.dataset.noteId = data.id;
152
- var clamped = clampPos(data.x, data.y, data.w, data.h);
153
- el.style.left = clamped.x + "px";
154
- el.style.top = clamped.y + "px";
155
- el.style.width = data.w + "px";
156
- el.style.height = data.h + "px";
157
- el.style.zIndex = 100 + (data.zIndex || 0);
158
- el.dataset.color = data.color || "purple";
159
-
160
- if (data.minimized) el.classList.add("minimized");
161
- if (data.hidden) el.classList.add("hidden");
162
-
163
- // Header
164
- var header = document.createElement("div");
165
- header.className = "sticky-note-header";
166
-
167
- if (data.origin && VENDOR_AVATARS[data.origin.vendor]) {
168
- var originBadge = document.createElement("img");
169
- originBadge.className = "sticky-note-agent-badge";
170
- originBadge.src = VENDOR_AVATARS[data.origin.vendor];
171
- originBadge.alt = "";
172
- originBadge.title = "Created by " + data.origin.vendor;
173
- header.appendChild(originBadge);
174
- }
175
-
176
- var closeBtn = document.createElement("button");
177
- closeBtn.className = "sticky-note-btn sticky-note-close";
178
- closeBtn.title = "Close";
179
- closeBtn.innerHTML = iconHtml("x");
180
- header.appendChild(closeBtn);
181
-
182
- var minBtn = document.createElement("button");
183
- minBtn.className = "sticky-note-btn sticky-note-min-btn";
184
- minBtn.title = data.minimized ? "Expand" : "Minimize";
185
- minBtn.innerHTML = data.minimized ? iconHtml("maximize-2") : iconHtml("minus");
186
- header.appendChild(minBtn);
187
-
188
- var spacer = document.createElement("div");
189
- spacer.className = "sticky-note-spacer";
190
- spacer.textContent = getTitle(data.text) || "Untitled";
191
- header.appendChild(spacer);
192
-
193
- var addBtn = document.createElement("button");
194
- addBtn.className = "sticky-note-btn";
195
- addBtn.title = "New note";
196
- addBtn.innerHTML = iconHtml("plus");
197
- addBtn.addEventListener("click", function (e) {
198
- e.stopPropagation();
199
- createNote();
200
- });
201
- header.appendChild(addBtn);
202
-
203
- var colorBtn = document.createElement("button");
204
- colorBtn.className = "sticky-note-color-btn";
205
- colorBtn.title = "Change color";
206
- colorBtn.innerHTML = iconHtml("palette");
207
- header.appendChild(colorBtn);
208
-
209
- var mdBtn = document.createElement("button");
210
- mdBtn.className = "sticky-note-btn sticky-note-md-btn";
211
- mdBtn.title = "Edit markdown";
212
- mdBtn.innerHTML = "<span class='sn-md-label'>MD</span>";
213
- header.appendChild(mdBtn);
214
-
215
- var opacityWrap = document.createElement("div");
216
- opacityWrap.className = "sticky-note-opacity";
217
- var opacitySlider = document.createElement("input");
218
- opacitySlider.type = "range";
219
- opacitySlider.min = "20";
220
- opacitySlider.max = "100";
221
- opacitySlider.value = String(Math.round((typeof data.opacity === "number" ? data.opacity : 0.64) * 100));
222
- opacitySlider.className = "sticky-note-opacity-slider";
223
- opacitySlider.title = "Opacity";
224
- opacityWrap.appendChild(opacitySlider);
225
- header.appendChild(opacityWrap);
226
-
227
- // Apply saved opacity via CSS variable (not element opacity, so header stays visible on hover)
228
- if (typeof data.opacity === "number") {
229
- el.style.setProperty("--note-opacity", data.opacity);
230
- }
231
-
232
- opacitySlider.addEventListener("input", function (e) {
233
- e.stopPropagation();
234
- var val = parseInt(opacitySlider.value, 10) / 100;
235
- el.style.setProperty("--note-opacity", val);
236
- });
237
- opacitySlider.addEventListener("change", function (e) {
238
- e.stopPropagation();
239
- var val = parseInt(opacitySlider.value, 10) / 100;
240
- el.style.setProperty("--note-opacity", val);
241
- debouncedUpdate(data.id, { opacity: val }, 300);
242
- });
243
- opacitySlider.addEventListener("mousedown", function (e) { e.stopPropagation(); });
244
-
245
- el.appendChild(header);
246
-
247
- // Body
248
- var body = document.createElement("div");
249
- body.className = "sticky-note-body";
250
-
251
- // Hidden textarea as markdown data store
252
- var textarea = document.createElement("textarea");
253
- textarea.className = "sticky-note-text";
254
- textarea.value = data.text || "";
255
- textarea.style.display = "none";
256
- body.appendChild(textarea);
257
-
258
- // Contenteditable rendered view (primary editing surface)
259
- var rendered = document.createElement("div");
260
- rendered.className = "sticky-note-rendered";
261
- rendered.contentEditable = "true";
262
- rendered.spellcheck = true;
263
- if (data.text && data.text.trim()) {
264
- rendered.innerHTML = renderMiniMarkdown(data.text);
265
- } else {
266
- rendered.classList.add("is-empty");
267
- }
268
- body.appendChild(rendered);
269
-
270
- el.appendChild(body);
271
-
272
- // Resize handle
273
- var resizeHandle = document.createElement("div");
274
- resizeHandle.className = "sticky-note-resize";
275
- el.appendChild(resizeHandle);
276
-
277
- // --- Event handlers ---
278
- setupDrag(el, spacer, data.id);
279
- setupResize(el, resizeHandle, data.id);
280
- setupTextEdit(textarea, rendered, data.id, mdBtn);
281
- setupColorPicker(colorBtn, el, data.id);
282
- setupMinimize(minBtn, el, data.id);
283
- setupClose(closeBtn, el, data.id);
284
- setupBringToFront(el, data.id);
285
-
286
- refreshIcons();
287
- return el;
288
- }
289
-
290
- // --- Drag ---
291
-
292
- function setupDrag(noteEl, spacerEl, noteId) {
293
- var dragging = false;
294
- var pointerId = null;
295
- var startX, startY, origX, origY;
296
-
297
- spacerEl.addEventListener("pointerdown", function (e) {
298
- if (e.pointerType === "mouse" && e.button !== 0) return;
299
- e.preventDefault();
300
- dragging = true;
301
- pointerId = e.pointerId;
302
- startX = e.clientX;
303
- startY = e.clientY;
304
- origX = parseInt(noteEl.style.left) || 0;
305
- origY = parseInt(noteEl.style.top) || 0;
306
- noteEl.classList.add("dragging");
307
- document.body.classList.add("sticky-note-interacting");
308
- spacerEl.setPointerCapture(pointerId);
309
- });
310
-
311
- function onMove(e) {
312
- if (!dragging || e.pointerId !== pointerId) return;
313
- var dx = e.clientX - startX;
314
- var dy = e.clientY - startY;
315
- var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
316
- noteEl.style.left = c.x + "px";
317
- noteEl.style.top = c.y + "px";
318
- }
319
-
320
- function onUp(e) {
321
- if (!dragging || (e && e.pointerId !== pointerId)) return;
322
- dragging = false;
323
- noteEl.classList.remove("dragging");
324
- document.body.classList.remove("sticky-note-interacting");
325
- if (pointerId !== null && spacerEl.hasPointerCapture(pointerId)) spacerEl.releasePointerCapture(pointerId);
326
- pointerId = null;
327
- debouncedUpdate(noteId, {
328
- x: parseInt(noteEl.style.left),
329
- y: parseInt(noteEl.style.top),
330
- }, 200);
331
- }
332
-
333
- spacerEl.addEventListener("pointermove", onMove);
334
- spacerEl.addEventListener("pointerup", onUp);
335
- spacerEl.addEventListener("pointercancel", onUp);
336
- spacerEl.addEventListener("lostpointercapture", onUp);
337
- }
338
-
339
- // --- Resize ---
340
-
341
- function setupResize(noteEl, handle, noteId) {
342
- var resizing = false;
343
- var pointerId = null;
344
- var startX, startY, origW, origH;
345
- var MIN_W = 160;
346
- var MIN_H = 80;
347
-
348
- handle.addEventListener("pointerdown", function (e) {
349
- if (e.pointerType === "mouse" && e.button !== 0) return;
350
- e.preventDefault();
351
- e.stopPropagation();
352
- resizing = true;
353
- pointerId = e.pointerId;
354
- startX = e.clientX;
355
- startY = e.clientY;
356
- origW = noteEl.offsetWidth;
357
- origH = noteEl.offsetHeight;
358
- noteEl.classList.add("resizing");
359
- document.body.classList.add("sticky-note-interacting");
360
- handle.setPointerCapture(pointerId);
361
- });
362
-
363
- function onMove(e) {
364
- if (!resizing || e.pointerId !== pointerId) return;
365
- var rawW = Math.max(MIN_W, origW + (e.clientX - startX));
366
- var rawH = Math.max(MIN_H, origH + (e.clientY - startY));
367
- var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
368
- noteEl.style.width = Math.max(MIN_W, cs.w) + "px";
369
- noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
370
- }
371
-
372
- function onUp(e) {
373
- if (!resizing || (e && e.pointerId !== pointerId)) return;
374
- resizing = false;
375
- noteEl.classList.remove("resizing");
376
- document.body.classList.remove("sticky-note-interacting");
377
- if (pointerId !== null && handle.hasPointerCapture(pointerId)) handle.releasePointerCapture(pointerId);
378
- pointerId = null;
379
- debouncedUpdate(noteId, {
380
- w: noteEl.offsetWidth,
381
- h: noteEl.offsetHeight,
382
- }, 200);
383
- }
384
-
385
- handle.addEventListener("pointermove", onMove);
386
- handle.addEventListener("pointerup", onUp);
387
- handle.addEventListener("pointercancel", onUp);
388
- handle.addEventListener("lostpointercapture", onUp);
389
- }
390
-
391
- // --- Text edit (contenteditable) ---
392
-
393
- // --- Format toolbar (WYSIWYG) ---
394
-
395
- var FORMAT_BUTTONS = [
396
- { label: "B", title: "Bold", cls: "sn-fmt-bold", command: "bold" },
397
- { label: "I", title: "Italic", cls: "sn-fmt-italic", command: "italic" },
398
- { label: "S", title: "Strikethrough", cls: "sn-fmt-strike", command: "strikethrough" },
399
- { label: "code-2", title: "Code", cls: "sn-fmt-code", command: "code", isIcon: true },
400
- ];
401
-
402
- function closeFormatToolbar() {
403
- if (formatToolbarEl) {
404
- formatToolbarEl.remove();
405
- formatToolbarEl = null;
406
- }
407
- }
408
-
409
- function applyFormat(command, rendered) {
410
- if (command === "code") {
411
- var sel = window.getSelection();
412
- if (!sel.rangeCount || sel.isCollapsed) return;
413
- var range = sel.getRangeAt(0);
414
- var ancestor = range.commonAncestorContainer;
415
- var codeParent = (ancestor.nodeType === 3 ? ancestor.parentElement : ancestor);
416
- if (codeParent && codeParent.closest && codeParent.closest("code")) {
417
- // Unwrap: replace <code> with its text content
418
- var codeEl = codeParent.closest("code");
419
- var textNode = document.createTextNode(codeEl.textContent);
420
- codeEl.parentNode.replaceChild(textNode, codeEl);
421
- var newRange = document.createRange();
422
- newRange.selectNodeContents(textNode);
423
- sel.removeAllRanges();
424
- sel.addRange(newRange);
425
- } else {
426
- // Wrap selection in <code>
427
- var code = document.createElement("code");
428
- try { range.surroundContents(code); } catch (e) {
429
- var frag = range.extractContents();
430
- code.appendChild(frag);
431
- range.insertNode(code);
432
- }
433
- }
434
- } else {
435
- document.execCommand(command, false, null);
436
- }
437
- // Trigger sync
438
- rendered.dispatchEvent(new Event("input", { bubbles: true }));
439
- }
440
-
441
- function showFormatToolbar(rendered) {
442
- closeFormatToolbar();
443
-
444
- var sel = window.getSelection();
445
- if (!sel || !sel.rangeCount || sel.isCollapsed) return;
446
- if (!sel.toString().trim()) return;
447
-
448
- var range = sel.getRangeAt(0);
449
- // When dragging outside the note, commonAncestorContainer may be a parent
450
- // of rendered. Clamp the range to the rendered element so the toolbar shows.
451
- if (!rendered.contains(range.commonAncestorContainer)) {
452
- try {
453
- var clampedRange = range.cloneRange();
454
- if (range.startContainer === rendered || rendered.contains(range.startContainer)) {
455
- clampedRange.selectNodeContents(rendered);
456
- clampedRange.setStart(range.startContainer, range.startOffset);
457
- } else if (range.endContainer === rendered || rendered.contains(range.endContainer)) {
458
- clampedRange.selectNodeContents(rendered);
459
- clampedRange.setEnd(range.endContainer, range.endOffset);
460
- } else {
461
- return;
462
- }
463
- range = clampedRange;
464
- } catch (e) {
465
- return;
466
- }
467
- }
468
-
469
- var toolbar = document.createElement("div");
470
- toolbar.className = "sn-format-toolbar";
471
-
472
- for (var i = 0; i < FORMAT_BUTTONS.length; i++) {
473
- (function (cfg) {
474
- var btn = document.createElement("button");
475
- btn.className = "sn-fmt-btn " + cfg.cls;
476
- btn.title = cfg.title;
477
- btn.innerHTML = cfg.isIcon ? iconHtml(cfg.label) : cfg.label;
478
- btn.addEventListener("mousedown", function (e) {
479
- e.preventDefault();
480
- e.stopPropagation();
481
- applyFormat(cfg.command, rendered);
482
- setTimeout(function () {
483
- var s = window.getSelection();
484
- if (s && !s.isCollapsed) {
485
- showFormatToolbar(rendered);
486
- } else {
487
- closeFormatToolbar();
488
- }
489
- }, 0);
490
- });
491
- toolbar.appendChild(btn);
492
- })(FORMAT_BUTTONS[i]);
493
- }
494
-
495
- refreshIcons();
496
- document.body.appendChild(toolbar);
497
- formatToolbarEl = toolbar;
498
- positionToolbarAtRange(toolbar, range);
499
- }
500
-
501
- function positionToolbarAtRange(toolbar, range) {
502
- var rect = range.getBoundingClientRect();
503
- var toolbarX = rect.left + rect.width / 2;
504
- var toolbarY = rect.top - 4;
505
-
506
- toolbar.style.left = toolbarX + "px";
507
- toolbar.style.top = toolbarY + "px";
508
-
509
- requestAnimationFrame(function () {
510
- var tw = toolbar.offsetWidth;
511
- var th = toolbar.offsetHeight;
512
- var x = Math.max(8, Math.min(toolbarX - tw / 2, window.innerWidth - tw - 8));
513
- var y = Math.max(8, toolbarY - th);
514
- toolbar.style.left = x + "px";
515
- toolbar.style.top = y + "px";
516
- });
517
- }
518
-
519
- function setupTextEdit(textarea, rendered, noteId, mdBtn) {
520
- var noteEl = textarea.closest(".sticky-note");
521
- var mdMode = false;
522
-
523
- function saveRenderedMarkdown() {
524
- var markdown = extractMarkdown(rendered);
525
- textarea.value = markdown;
526
- debouncedTextUpdate(noteId, markdown);
527
- syncTitle(noteEl, markdown);
528
- rendered.classList.toggle("is-empty", !markdown.trim());
529
- }
530
-
531
- function toggleChecklistItem(check) {
532
- var checked = !check.classList.contains("checked");
533
- check.classList.toggle("checked", checked);
534
- check.textContent = checked ? "✓" : "☐";
535
- check.setAttribute("aria-checked", checked ? "true" : "false");
536
- saveRenderedMarkdown();
537
- }
538
-
539
- // MD button: toggle between contenteditable rendered view and raw textarea
540
- mdBtn.addEventListener("click", function (e) {
541
- e.stopPropagation();
542
- mdMode = !mdMode;
543
- if (mdMode) {
544
- // Switch to raw markdown editing
545
- var md = extractMarkdown(rendered);
546
- textarea.value = md;
547
- textarea.style.display = "";
548
- rendered.style.display = "none";
549
- mdBtn.classList.add("active");
550
- textarea.focus();
551
- } else {
552
- // Switch back to contenteditable rendered view
553
- var md = textarea.value;
554
- debouncedTextUpdate(noteId, md);
555
- syncTitle(noteEl, md);
556
- if (md.trim()) {
557
- rendered.innerHTML = renderMiniMarkdown(md);
558
- rendered.classList.remove("is-empty");
559
- } else {
560
- rendered.innerHTML = "";
561
- rendered.classList.add("is-empty");
562
- }
563
- textarea.style.display = "none";
564
- rendered.style.display = "";
565
- mdBtn.classList.remove("active");
566
- rendered.focus();
567
- }
568
- });
569
-
570
- // Sync contenteditable changes to textarea (data store) and save
571
- rendered.addEventListener("input", function () {
572
- saveRenderedMarkdown();
573
- });
574
-
575
- rendered.addEventListener("click", function (e) {
576
- var check = e.target.closest && e.target.closest(".sn-check");
577
- if (!check || !rendered.contains(check)) return;
578
- e.preventDefault();
579
- e.stopPropagation();
580
- toggleChecklistItem(check);
581
- });
582
-
583
- // On blur, re-render to normalize HTML structure
584
- rendered.addEventListener("blur", function (e) {
585
- // Don't re-render if clicking format toolbar (it prevents default, but just in case)
586
- if (e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".sn-format-toolbar")) return;
587
- closeFormatToolbar();
588
- var md = extractMarkdown(rendered);
589
- textarea.value = md;
590
- if (md.trim()) {
591
- rendered.innerHTML = renderMiniMarkdown(md);
592
- rendered.classList.remove("is-empty");
593
- } else {
594
- rendered.innerHTML = "";
595
- rendered.classList.add("is-empty");
596
- }
597
- });
598
-
599
- // Show format toolbar when selecting text
600
- rendered.addEventListener("mouseup", function (e) {
601
- if (e.target.tagName === "A") return;
602
- setTimeout(function () {
603
- var sel = window.getSelection();
604
- if (sel && !sel.isCollapsed && sel.toString().trim()) {
605
- showFormatToolbar(rendered);
606
- } else {
607
- closeFormatToolbar();
608
- }
609
- }, 10);
610
- });
611
-
612
- // Keyboard selection
613
- rendered.addEventListener("keyup", function (e) {
614
- if (e.shiftKey || e.key === "Shift") {
615
- var sel = window.getSelection();
616
- if (sel && !sel.isCollapsed) {
617
- showFormatToolbar(rendered);
618
- } else {
619
- closeFormatToolbar();
620
- }
621
- }
622
- });
623
-
624
- // Insert <br> on Enter instead of <div>
625
- rendered.addEventListener("keydown", function (e) {
626
- if (e.target.classList && e.target.classList.contains("sn-check") &&
627
- (e.key === "Enter" || e.key === " ")) {
628
- e.preventDefault();
629
- toggleChecklistItem(e.target);
630
- return;
631
- }
632
- if (e.key === "Enter" && !e.shiftKey) {
633
- e.preventDefault();
634
- document.execCommand("insertLineBreak");
635
- }
636
- });
637
-
638
- // Paste as plain text to avoid importing HTML formatting
639
- rendered.addEventListener("paste", function (e) {
640
- e.preventDefault();
641
- var text = (e.clipboardData || window.clipboardData).getData("text/plain");
642
- document.execCommand("insertText", false, text);
643
- });
644
-
645
- // Prevent drag when clicking body
646
- rendered.addEventListener("mousedown", function (e) {
647
- e.stopPropagation();
648
- });
649
-
650
- // Sync textarea edits when in MD mode
651
- textarea.addEventListener("input", function () {
652
- if (!mdMode) return;
653
- debouncedTextUpdate(noteId, textarea.value);
654
- syncTitle(noteEl, textarea.value);
655
- });
656
-
657
- textarea.addEventListener("mousedown", function (e) {
658
- e.stopPropagation();
659
- });
660
- }
661
-
662
- // --- Color picker ---
663
-
664
- function closeColorPicker() {
665
- if (colorPickerEl) {
666
- colorPickerEl.remove();
667
- colorPickerEl = null;
668
- }
669
- }
670
-
671
- function setupColorPicker(btn, noteEl, noteId) {
672
- btn.addEventListener("click", function (e) {
673
- e.stopPropagation();
674
- showColorPicker(btn, noteEl, noteId);
675
- });
676
- }
677
-
678
- function showColorPicker(anchor, noteEl, noteId) {
679
- closeColorPicker();
680
-
681
- var picker = document.createElement("div");
682
- picker.className = "sticky-note-color-picker";
683
-
684
- for (var i = 0; i < NOTE_COLORS.length; i++) {
685
- (function (color) {
686
- var dot = document.createElement("button");
687
- dot.className = "sticky-note-color-dot";
688
- dot.dataset.color = color;
689
- if (noteEl.dataset.color === color) dot.classList.add("active");
690
- dot.addEventListener("click", function (e) {
691
- e.stopPropagation();
692
- noteEl.dataset.color = color;
693
- wsSend({ type: "note_update", id: noteId, color: color });
694
- closeColorPicker();
695
- });
696
- picker.appendChild(dot);
697
- })(NOTE_COLORS[i]);
698
- }
699
-
700
- document.body.appendChild(picker);
701
- colorPickerEl = picker;
702
-
703
- // Position relative to anchor
704
- var rect = anchor.getBoundingClientRect();
705
- picker.style.left = rect.left + "px";
706
- picker.style.top = (rect.bottom + 4) + "px";
707
-
708
- // Close on outside click
709
- setTimeout(function () {
710
- document.addEventListener("click", function closeHandler() {
711
- closeColorPicker();
712
- document.removeEventListener("click", closeHandler);
713
- });
714
- }, 0);
715
- }
716
-
717
- // --- Minimize ---
718
-
719
- function setupMinimize(btn, noteEl, noteId) {
720
- btn.addEventListener("click", function (e) {
721
- e.stopPropagation();
722
- var isMinimized = noteEl.classList.toggle("minimized");
723
- btn.innerHTML = isMinimized ? iconHtml("maximize-2") : iconHtml("minus");
724
- btn.title = isMinimized ? "Expand" : "Minimize";
725
- refreshIcons();
726
- wsSend({ type: "note_update", id: noteId, minimized: isMinimized });
727
- });
728
- }
729
-
730
- // --- Close (hide) note ---
731
-
732
- function setupClose(btn, noteEl, noteId) {
733
- btn.addEventListener("click", function (e) {
734
- e.stopPropagation();
735
- noteEl.classList.add("hidden");
736
- wsSend({ type: "note_update", id: noteId, hidden: true });
737
- });
738
- }
739
-
740
- // --- Bring to front ---
741
-
742
- function setupBringToFront(noteEl, noteId) {
743
- noteEl.addEventListener("mousedown", function (e) {
744
- // Skip bring-to-front when clicking header buttons to avoid
745
- // race condition where server response replaces innerHTML
746
- // between mousedown and click, causing the click event to be lost
747
- if (e.target.closest("button")) return;
748
- wsSend({ type: "note_bring_front", id: noteId });
749
- });
750
- }
751
-
752
135
  // --- WS message handlers ---
753
136
 
754
137
  export function handleNotesList(msg) {
755
138
  var container = document.getElementById("sticky-notes-container");
756
139
  if (!container) return;
757
140
 
758
- // Clear existing
759
- container.innerHTML = "";
760
- notes.clear();
761
-
141
+ // Keyed reconciliation, never a clear-and-rebuild. A reconnect re-sends this
142
+ // list unchanged; tearing every note down and recreating it made all the
143
+ // floating cards vanish and reappear, and destroyed any open editor.
762
144
  var list = msg.notes || [];
763
- for (var i = 0; i < list.length; i++) {
764
- var el = renderNote(list[i]);
765
- notes.set(list[i].id, { data: list[i], el: el });
145
+ var seen = {};
146
+ var created = false;
147
+ var i;
148
+
149
+ for (i = 0; i < list.length; i++) {
150
+ var data = list[i];
151
+ if (!data || !data.id) continue;
152
+ seen[data.id] = true;
153
+ var entry = notes.get(data.id);
154
+ if (entry && entry.el && entry.el.parentNode === container) {
155
+ patchNote(entry, data);
156
+ continue;
157
+ }
158
+ var el = renderNote(data);
159
+ notes.set(data.id, { data: data, el: el });
766
160
  container.appendChild(el);
161
+ created = true;
162
+ }
163
+
164
+ // Remove notes the server no longer lists.
165
+ var staleIds = [];
166
+ notes.forEach(function (value, id) {
167
+ if (!seen[id]) staleIds.push(id);
168
+ });
169
+ for (i = 0; i < staleIds.length; i++) {
170
+ var stale = notes.get(staleIds[i]);
171
+ if (stale && stale.el && stale.el.parentNode) stale.el.parentNode.removeChild(stale.el);
172
+ notes.delete(staleIds[i]);
767
173
  }
768
174
 
175
+ // renderNote already refreshes icons for the nodes it builds; an all-patch
176
+ // pass introduces no new Lucide markup and must not trigger a document scan.
177
+ if (created) refreshIcons();
178
+
769
179
  updateBadge();
770
180
  updateSidebarBadge();
181
+ refreshBrowser();
771
182
 
772
- // If user already has notes, they know the feature (no onboarding needed)
773
-
774
- // Auto-show if there are notes
775
- if (list.length > 0 && !notesVisible) {
183
+ // Auto-show only when something is actually asking for attention. A board of
184
+ // nothing but closed notes must not pop the canvas open.
185
+ if (openCount() > 0 && !notesVisible) {
776
186
  notesVisible = true;
777
187
  container.classList.remove("hidden");
778
188
  }
@@ -790,9 +200,7 @@ export function handleNoteCreated(msg) {
790
200
  container.appendChild(el);
791
201
  updateBadge();
792
202
  updateSidebarBadge();
793
-
794
- // Re-render archive if open
795
- if (archiveOpen) renderArchiveCards();
203
+ refreshBrowser();
796
204
 
797
205
  // Show container if hidden
798
206
  if (!notesVisible) {
@@ -801,62 +209,91 @@ export function handleNoteCreated(msg) {
801
209
  }
802
210
  }
803
211
 
804
- export function handleNoteUpdated(msg) {
805
- if (!msg.note) return;
806
- var entry = notes.get(msg.note.id);
807
- if (!entry) return;
808
-
809
- entry.data = msg.note;
810
-
811
- // Update DOM
812
- entry.el.style.left = msg.note.x + "px";
813
- entry.el.style.top = msg.note.y + "px";
814
- entry.el.style.width = msg.note.w + "px";
815
- entry.el.style.height = msg.note.h + "px";
816
- entry.el.style.zIndex = 100 + (msg.note.zIndex || 0);
817
- entry.el.dataset.color = msg.note.color || "purple";
818
-
819
- // Update text only if not actively editing (check rendered or textarea focus)
820
- var textarea = entry.el.querySelector(".sticky-note-text");
821
- var rendered = entry.el.querySelector(".sticky-note-rendered");
822
- if (rendered && rendered !== document.activeElement && textarea !== document.activeElement) {
823
- if (textarea) textarea.value = msg.note.text || "";
824
- if (msg.note.text && msg.note.text.trim()) {
825
- rendered.innerHTML = renderMiniMarkdown(msg.note.text);
212
+ // Patch one existing note element in place from a server payload.
213
+ //
214
+ // Geometry, z-order, colour, and opacity are plain style writes and are always
215
+ // applied. The rendered markdown is only replaced when the text actually
216
+ // changed and the note is not being edited, so a geometry-only or z-only
217
+ // update never destroys the user's caret, selection, or in-progress draft.
218
+ // Returns true when icon markup was replaced, so the caller can decide whether
219
+ // a Lucide pass is needed at all.
220
+ function patchNote(entry, next) {
221
+ if (!entry || !next) return false;
222
+ var previous = entry.data || {};
223
+ entry.data = next;
224
+ var el = entry.el;
225
+
226
+ el.style.left = next.x + "px";
227
+ el.style.top = next.y + "px";
228
+ el.style.width = next.w + "px";
229
+ el.style.height = next.h + "px";
230
+ el.style.zIndex = 100 + (next.zIndex || 0);
231
+ el.dataset.color = next.color || "purple";
232
+
233
+ var textarea = el.querySelector(".sticky-note-text");
234
+ var rendered = el.querySelector(".sticky-note-rendered");
235
+ var nextText = next.text || "";
236
+ var editing = rendered === document.activeElement || textarea === document.activeElement;
237
+ // A draft the user typed but has not committed must survive a redraw.
238
+ var hasDraft = !!(textarea && textarea.value !== (previous.text || ""));
239
+ var textChanged = nextText !== (previous.text || "");
240
+ if (rendered && !editing && !hasDraft && textChanged) {
241
+ if (textarea) textarea.value = nextText;
242
+ if (nextText.trim()) {
243
+ rendered.innerHTML = renderMiniMarkdown(nextText);
244
+ enhanceClayLogLinks(rendered);
826
245
  rendered.classList.remove("is-empty");
827
246
  } else {
828
247
  rendered.innerHTML = "";
829
248
  rendered.classList.add("is-empty");
830
249
  }
831
- syncTitle(entry.el, msg.note.text);
250
+ syncTitle(el, nextText);
832
251
  }
833
252
 
834
- // Handle opacity
835
- if (typeof msg.note.opacity === "number") {
836
- entry.el.style.setProperty("--note-opacity", msg.note.opacity);
837
- var slider = entry.el.querySelector(".sticky-note-opacity-slider");
838
- if (slider) slider.value = String(Math.round(msg.note.opacity * 100));
253
+ if (typeof next.opacity === "number") {
254
+ el.style.setProperty("--note-opacity", next.opacity);
255
+ var slider = el.querySelector(".sticky-note-opacity-slider");
256
+ if (slider) slider.value = String(Math.round(next.opacity * 100));
839
257
  }
840
258
 
841
- // Handle hidden state
842
- if (msg.note.hidden) {
843
- entry.el.classList.add("hidden");
259
+ if (isClosedNote(next)) el.classList.add("hidden");
260
+ else el.classList.remove("hidden");
261
+
262
+ // The minimize button swaps its icon, which is the only path here that can
263
+ // introduce fresh Lucide markup.
264
+ var iconsReplaced = false;
265
+ var wasMinimized = el.classList.contains("minimized");
266
+ var minBtn = el.querySelector(".sticky-note-min-btn");
267
+ if (next.minimized) {
268
+ el.classList.add("minimized");
269
+ if (minBtn && !wasMinimized) {
270
+ minBtn.innerHTML = iconHtml("maximize-2");
271
+ minBtn.title = "Expand";
272
+ iconsReplaced = true;
273
+ }
844
274
  } else {
845
- entry.el.classList.remove("hidden");
275
+ el.classList.remove("minimized");
276
+ if (minBtn && wasMinimized) {
277
+ minBtn.innerHTML = iconHtml("minus");
278
+ minBtn.title = "Minimize";
279
+ iconsReplaced = true;
280
+ }
846
281
  }
282
+ return iconsReplaced;
283
+ }
847
284
 
848
- var minBtn = entry.el.querySelector(".sticky-note-min-btn");
849
- if (msg.note.minimized) {
850
- entry.el.classList.add("minimized");
851
- if (minBtn) { minBtn.innerHTML = iconHtml("maximize-2"); minBtn.title = "Expand"; }
852
- } else {
853
- entry.el.classList.remove("minimized");
854
- if (minBtn) { minBtn.innerHTML = iconHtml("minus"); minBtn.title = "Minimize"; }
855
- }
856
- refreshIcons();
285
+ export function handleNoteUpdated(msg) {
286
+ if (!msg.note) return;
287
+ var entry = notes.get(msg.note.id);
288
+ if (!entry) return;
857
289
 
858
- // Re-render archive if open (text or color may have changed)
859
- if (archiveOpen) renderArchiveCards();
290
+ if (patchNote(entry, msg.note)) refreshIcons();
291
+
292
+ // A close or reopen arrives as an ordinary update, so the canvas count and
293
+ // the browser both follow from the same message.
294
+ updateBadge();
295
+ updateSidebarBadge();
296
+ refreshBrowser();
860
297
  }
861
298
 
862
299
  export function handleNoteWritten(msg) {
@@ -870,6 +307,8 @@ export function handleNoteWritten(msg) {
870
307
  }, 2100);
871
308
  }
872
309
 
310
+ // Defensive only. The server no longer deletes notes and never sends this, but
311
+ // an older daemon on the other end of the socket still must not corrupt state.
873
312
  export function handleNoteDeleted(msg) {
874
313
  var entry = notes.get(msg.id);
875
314
  if (!entry) return;
@@ -878,14 +317,8 @@ export function handleNoteDeleted(msg) {
878
317
  updateBadge();
879
318
  updateSidebarBadge();
880
319
 
881
- // Clear debounce timers
882
- clearTimeout(updateTimers[msg.id]);
883
- clearTimeout(textTimers[msg.id]);
884
- delete updateTimers[msg.id];
885
- delete textTimers[msg.id];
886
-
887
- // Re-render archive if open
888
- if (archiveOpen) renderArchiveCards();
320
+ clearNoteTimers(msg.id);
321
+ refreshBrowser();
889
322
  }
890
323
 
891
324
  // --- Sidebar badge ---
@@ -893,224 +326,15 @@ export function handleNoteDeleted(msg) {
893
326
  function updateSidebarBadge() {
894
327
  var badge = document.getElementById("sticky-notes-sidebar-count");
895
328
  if (!badge) return;
896
- if (notes.size > 0) {
897
- badge.textContent = notes.size;
329
+ var count = openCount();
330
+ if (count > 0) {
331
+ badge.textContent = count;
898
332
  badge.classList.remove("hidden");
899
333
  } else {
900
334
  badge.classList.add("hidden");
901
335
  }
902
336
  }
903
337
 
904
- // --- Notes Archive View ---
905
-
906
- function renderArchiveCards() {
907
- var grid = document.getElementById("notes-archive-grid");
908
- if (!grid) return;
909
-
910
- grid.innerHTML = "";
911
-
912
- if (notes.size === 0) {
913
- var empty = document.createElement("div");
914
- empty.className = "notes-archive-empty";
915
- empty.innerHTML = iconHtml("sticky-note") + "<p>Keep what matters across sessions</p><p class=\"notes-archive-empty-sub\">Use sticky notes for checklists, goals, handoffs, and durable project knowledge. You and every Clay agent can find them in future sessions.</p><p class=\"notes-archive-empty-sub\">Create one with the " + iconHtml("sticky-note") + " button in the title bar</p>";
916
- grid.appendChild(empty);
917
- refreshIcons();
918
- return;
919
- }
920
-
921
- // Sort by creation time (id is timestamp-based) — newest first
922
- var sorted = Array.from(notes.values()).sort(function (a, b) {
923
- return (b.data.id || "").localeCompare(a.data.id || "");
924
- });
925
-
926
- for (var i = 0; i < sorted.length; i++) {
927
- (function (noteData) {
928
- var card = document.createElement("div");
929
- card.className = "notes-archive-card" + (noteData.data.hidden ? " archived" : "");
930
- card.dataset.color = noteData.data.color || "purple";
931
-
932
- // Header with title + delete
933
- var header = document.createElement("div");
934
- header.className = "notes-archive-card-header";
935
-
936
- var title = document.createElement("div");
937
- title.className = "notes-archive-card-title";
938
- title.textContent = getTitle(noteData.data.text) || "Untitled";
939
- header.appendChild(title);
940
-
941
- var deleteBtn = document.createElement("button");
942
- deleteBtn.className = "notes-archive-card-delete";
943
- deleteBtn.title = "Delete permanently";
944
- deleteBtn.innerHTML = iconHtml("trash-2");
945
- deleteBtn.addEventListener("click", function (e) {
946
- e.stopPropagation();
947
- // Confirm before permanent delete
948
- if (card.classList.contains("confirm-delete")) {
949
- wsSend({ type: "note_delete", id: noteData.data.id });
950
- card.classList.add("deleting");
951
- return;
952
- }
953
- card.classList.add("confirm-delete");
954
- deleteBtn.title = "Click again to confirm";
955
- setTimeout(function () {
956
- card.classList.remove("confirm-delete");
957
- deleteBtn.title = "Delete permanently";
958
- }, 2000);
959
- });
960
- // Restore button (only for archived/hidden notes)
961
- if (noteData.data.hidden) {
962
- var restoreBtn = document.createElement("button");
963
- restoreBtn.className = "notes-archive-card-restore";
964
- restoreBtn.title = "Restore to canvas";
965
- restoreBtn.innerHTML = iconHtml("rotate-ccw") + "<span>Restore</span>";
966
- restoreBtn.addEventListener("click", function (e) {
967
- e.stopPropagation();
968
- wsSend({ type: "note_update", id: noteData.data.id, hidden: false });
969
- noteData.el.classList.remove("hidden");
970
- noteData.data.hidden = false;
971
- renderArchiveCards();
972
- });
973
- header.appendChild(restoreBtn);
974
- }
975
-
976
- header.appendChild(deleteBtn);
977
- card.appendChild(header);
978
-
979
- // Body (rendered markdown)
980
- var body = document.createElement("div");
981
- body.className = "notes-archive-card-body";
982
- var bodyLines = (noteData.data.text || "").split("\n").slice(1).join("\n").trim();
983
- if (bodyLines) {
984
- body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
985
- }
986
- card.appendChild(body);
987
-
988
- // Color strip at bottom
989
- var colorStrip = document.createElement("div");
990
- colorStrip.className = "notes-archive-card-color";
991
- card.appendChild(colorStrip);
992
-
993
- // Click card → close archive, jump to note on canvas
994
- card.addEventListener("click", function () {
995
- closeArchive();
996
- showNotes();
997
- // Un-hide if needed
998
- if (noteData.data.hidden) {
999
- wsSend({ type: "note_update", id: noteData.data.id, hidden: false });
1000
- noteData.el.classList.remove("hidden");
1001
- }
1002
- // Bring note to front
1003
- wsSend({ type: "note_bring_front", id: noteData.data.id });
1004
- // Un-minimize if needed
1005
- if (noteData.data.minimized) {
1006
- wsSend({ type: "note_update", id: noteData.data.id, minimized: false });
1007
- }
1008
- // Flash the note
1009
- var noteEl = noteData.el;
1010
- if (noteEl) {
1011
- noteEl.classList.add("note-flash");
1012
- setTimeout(function () { noteEl.classList.remove("note-flash"); }, 600);
1013
- }
1014
- });
1015
-
1016
- grid.appendChild(card);
1017
- })(sorted[i]);
1018
- }
1019
-
1020
- refreshIcons();
1021
- }
1022
-
1023
- export function openArchive() {
1024
- if (archiveOpen) return;
1025
- archiveOpen = true;
1026
-
1027
- var messagesEl = document.getElementById("messages");
1028
- var appEl = document.getElementById("app");
1029
- var inputArea = document.getElementById("input-area");
1030
- var titleBar = document.querySelector("#main-column > .title-bar-content");
1031
- var notesContainer = document.getElementById("sticky-notes-container");
1032
-
1033
- // Hide messages, input area, session title bar, and floating notes
1034
- if (messagesEl) messagesEl.classList.add("hidden");
1035
- if (inputArea) inputArea.classList.add("hidden");
1036
- if (titleBar) titleBar.classList.add("hidden");
1037
- if (notesContainer) notesContainer.classList.add("hidden");
1038
-
1039
- // Create or show archive container
1040
- var archive = document.getElementById("notes-archive");
1041
- if (!archive) {
1042
- archive = document.createElement("div");
1043
- archive.id = "notes-archive";
1044
-
1045
- var header = document.createElement("div");
1046
- header.className = "notes-archive-header";
1047
-
1048
- var titleWrap = document.createElement("div");
1049
- titleWrap.className = "notes-archive-title-wrap";
1050
- titleWrap.innerHTML = iconHtml("sticky-note") + "<h2>Sticky Notes</h2><span class=\"notes-archive-count\"></span>";
1051
- header.appendChild(titleWrap);
1052
-
1053
- var closeBtn = document.createElement("button");
1054
- closeBtn.className = "notes-archive-close";
1055
- closeBtn.title = "Back to chat";
1056
- closeBtn.innerHTML = iconHtml("x");
1057
- closeBtn.addEventListener("click", function () {
1058
- closeArchive();
1059
- });
1060
- header.appendChild(closeBtn);
1061
-
1062
- archive.appendChild(header);
1063
-
1064
- var grid = document.createElement("div");
1065
- grid.id = "notes-archive-grid";
1066
- grid.className = "notes-archive-grid";
1067
- archive.appendChild(grid);
1068
-
1069
- if (appEl) appEl.appendChild(archive);
1070
- }
1071
-
1072
- archive.classList.remove("hidden");
1073
-
1074
- // Update count
1075
- var countEl = archive.querySelector(".notes-archive-count");
1076
- if (countEl) countEl.textContent = notes.size + " note" + (notes.size !== 1 ? "s" : "");
1077
-
1078
- renderArchiveCards();
1079
-
1080
- // Mark sidebar button active
1081
- var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
1082
- if (sidebarBtn) sidebarBtn.classList.add("active");
1083
-
1084
- refreshIcons();
1085
- }
1086
-
1087
- export function closeArchive() {
1088
- if (!archiveOpen) return;
1089
- archiveOpen = false;
1090
-
1091
- var archive = document.getElementById("notes-archive");
1092
- var messagesEl = document.getElementById("messages");
1093
- var inputArea = document.getElementById("input-area");
1094
- var titleBar = document.querySelector("#main-column > .title-bar-content");
1095
- var notesContainer = document.getElementById("sticky-notes-container");
1096
-
1097
- if (archive) archive.classList.add("hidden");
1098
- if (messagesEl) messagesEl.classList.remove("hidden");
1099
- if (inputArea) inputArea.classList.remove("hidden");
1100
- if (titleBar) titleBar.classList.remove("hidden");
1101
-
1102
- // Restore floating notes if they were visible before
1103
- if (notesContainer && notesVisible) notesContainer.classList.remove("hidden");
1104
-
1105
- // Un-mark sidebar button
1106
- var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
1107
- if (sidebarBtn) sidebarBtn.classList.remove("active");
1108
- }
1109
-
1110
- export function isArchiveOpen() {
1111
- return archiveOpen;
1112
- }
1113
-
1114
338
  export function isNotesVisible() {
1115
339
  return notesVisible;
1116
340
  }