clay-server 4.0.0-beta.9 → 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 -7
  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
@@ -0,0 +1,326 @@
1
+ // Sticky Notes browser: the bounded right workbench window over the board.
2
+ //
3
+ // This is the index/viewer surface, not the floating cards. The cards stay on
4
+ // the canvas over the conversation; this pane lists them, splits them into Open
5
+ // and Closed, and is where a note is completed or brought back.
6
+ //
7
+ // Completion here is Close, never delete. A closed note leaves the active
8
+ // canvas and moves to the Closed tab, where it stays readable and reversible.
9
+ // There is deliberately no destructive control on this surface at all.
10
+
11
+ import { store } from './store.js';
12
+ import { iconHtml, refreshIcons } from './icons.js';
13
+ import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
14
+ import { enhanceClayLogLinks } from './clay-log-links.js';
15
+ import { send } from './sticky-notes-shared.js';
16
+ import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
17
+
18
+ var panel = null;
19
+ var gridEl = null;
20
+ var tabsEl = null;
21
+
22
+ // Registered by app.js so this module never imports the other right-pane tools
23
+ // and cannot form an import cycle with them.
24
+ var exclusiveClosers = [];
25
+
26
+ export function registerExclusiveClosers(fns) {
27
+ exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
28
+ }
29
+
30
+ function closeOtherRightTools() {
31
+ for (var i = 0; i < exclusiveClosers.length; i++) {
32
+ try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
33
+ }
34
+ }
35
+
36
+ function isClosed(data) {
37
+ if (!data) return false;
38
+ if (data.state === "closed") return true;
39
+ if (data.state === "open") return false;
40
+ // Legacy notes predate the state field; `hidden` was the old spelling.
41
+ return data.hidden === true;
42
+ }
43
+
44
+ export function openNotes(all) {
45
+ return (all || []).filter(function (data) { return !isClosed(data); });
46
+ }
47
+
48
+ export function closedNotes(all) {
49
+ return (all || []).filter(isClosed);
50
+ }
51
+
52
+ function currentTab() {
53
+ return store.get('notesBrowserTab') === "closed" ? "closed" : "open";
54
+ }
55
+
56
+ // --- Panel ---------------------------------------------------------------
57
+
58
+ function ensurePanel() {
59
+ if (panel) return;
60
+ var panels = document.getElementById("main-panels");
61
+ if (!panels) return;
62
+ panel = document.createElement("section");
63
+ panel.id = "notes-browser";
64
+ panel.className = "notes-browser hidden";
65
+ panel.setAttribute("aria-label", "Sticky Notes");
66
+ panel.innerHTML =
67
+ '<header class="notes-browser-topbar">' +
68
+ '<span class="notes-browser-title">' + iconHtml("sticky-note") + 'Sticky Notes</span>' +
69
+ '<span class="notes-browser-subtitle">Open items on the board, and what has been closed</span>' +
70
+ '<div class="notes-browser-window-actions" aria-label="Sticky Notes window controls">' +
71
+ '<button id="notes-browser-wide" class="notes-browser-icon-btn" type="button" title="Widen panel" aria-label="Widen Sticky Notes panel" aria-pressed="false">' + iconHtml("chevrons-left-right") + '</button>' +
72
+ '<button id="notes-browser-fullscreen" class="notes-browser-icon-btn" type="button" title="Toggle fullscreen" aria-label="Toggle Sticky Notes fullscreen" aria-pressed="false">' + iconHtml("maximize-2") + '</button>' +
73
+ '<button id="notes-browser-close" class="notes-browser-icon-btn" type="button" title="Close Sticky Notes" aria-label="Close Sticky Notes">' + iconHtml("x") + '</button>' +
74
+ '</div>' +
75
+ '</header>' +
76
+ '<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
77
+ '<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>';
78
+ panels.appendChild(panel);
79
+
80
+ gridEl = panel.querySelector("#notes-browser-grid");
81
+ tabsEl = panel.querySelector("#notes-browser-tabs");
82
+
83
+ panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
84
+ panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
85
+ applyWindowState(!store.get('notesBrowserWide'), store.get('notesBrowserFullscreen'));
86
+ });
87
+ panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
88
+ applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
89
+ });
90
+ refreshIcons();
91
+ }
92
+
93
+ // Width and fullscreen are transient view state, so they live in the store.
94
+ // `panel-fullscreen` is the shared class the document viewer, terminal, and
95
+ // Logs already use, so the geometry is the established one.
96
+ function applyWindowState(wide, fullscreen) {
97
+ store.set({ notesBrowserWide: !!wide, notesBrowserFullscreen: !!fullscreen });
98
+ if (!panel) return;
99
+ panel.classList.toggle("notes-browser-wide", !!wide && !fullscreen);
100
+ panel.classList.toggle("panel-fullscreen", !!fullscreen);
101
+ var wideBtn = panel.querySelector("#notes-browser-wide");
102
+ var fullBtn = panel.querySelector("#notes-browser-fullscreen");
103
+ if (wideBtn) {
104
+ wideBtn.setAttribute("aria-pressed", wide ? "true" : "false");
105
+ wideBtn.disabled = !!fullscreen;
106
+ }
107
+ if (fullBtn) fullBtn.setAttribute("aria-pressed", fullscreen ? "true" : "false");
108
+ }
109
+
110
+ // --- Tabs ----------------------------------------------------------------
111
+
112
+ function renderTabs(openCount, closedCount) {
113
+ if (!tabsEl) return;
114
+ var active = currentTab();
115
+ tabsEl.innerHTML = "";
116
+ var specs = [
117
+ { key: "open", label: "Open", count: openCount },
118
+ { key: "closed", label: "Closed", count: closedCount },
119
+ ];
120
+ for (var i = 0; i < specs.length; i++) {
121
+ (function (spec) {
122
+ var tab = document.createElement("button");
123
+ tab.type = "button";
124
+ tab.className = "notes-browser-tab" + (spec.key === active ? " active" : "");
125
+ tab.setAttribute("role", "tab");
126
+ tab.setAttribute("aria-selected", spec.key === active ? "true" : "false");
127
+ tab.tabIndex = spec.key === active ? 0 : -1;
128
+ tab.dataset.tab = spec.key;
129
+ tab.innerHTML = '<span>' + spec.label + '</span><span class="notes-browser-tab-count">' + spec.count + '</span>';
130
+ tab.addEventListener("click", function () { selectTab(spec.key); });
131
+ tab.addEventListener("keydown", function (e) {
132
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
133
+ e.preventDefault();
134
+ selectTab(spec.key === "open" ? "closed" : "open");
135
+ var next = tabsEl.querySelector('.notes-browser-tab.active');
136
+ if (next) next.focus();
137
+ });
138
+ tabsEl.appendChild(tab);
139
+ })(specs[i]);
140
+ }
141
+ }
142
+
143
+ function selectTab(key) {
144
+ store.set({ notesBrowserTab: key === "closed" ? "closed" : "open" });
145
+ renderNotesBrowser();
146
+ }
147
+
148
+ // --- Cards ---------------------------------------------------------------
149
+
150
+ function closedMeta(data) {
151
+ var parts = [];
152
+ if (data.closedAt) {
153
+ var when = new Date(data.closedAt);
154
+ if (!isNaN(when.getTime())) parts.push("Closed " + when.toLocaleDateString());
155
+ } else {
156
+ parts.push("Closed");
157
+ }
158
+ var by = data.closedBy;
159
+ if (by && by.type === "user" && by.displayName) parts.push("by " + by.displayName);
160
+ else if (by && by.type === "session") parts.push("by an agent session");
161
+ return parts.join(" · ");
162
+ }
163
+
164
+ function buildCard(data) {
165
+ var card = document.createElement("article");
166
+ var closed = isClosed(data);
167
+ card.className = "notes-browser-card" + (closed ? " closed" : "");
168
+ card.setAttribute("role", "listitem");
169
+ card.dataset.color = data.color || "purple";
170
+ card.dataset.noteId = data.id;
171
+
172
+ var header = document.createElement("div");
173
+ header.className = "notes-browser-card-header";
174
+
175
+ var title = document.createElement("h3");
176
+ title.className = "notes-browser-card-title";
177
+ title.textContent = getTitle(data.text) || "Untitled";
178
+ header.appendChild(title);
179
+
180
+ // Exactly one lifecycle action per card, and it is never destructive.
181
+ var action = document.createElement("button");
182
+ action.type = "button";
183
+ action.className = "notes-browser-card-action";
184
+ if (closed) {
185
+ action.innerHTML = iconHtml("rotate-ccw") + "<span>Reopen</span>";
186
+ action.title = "Reopen this note and return it to the board";
187
+ action.setAttribute("aria-label", "Reopen note: " + title.textContent);
188
+ action.addEventListener("click", function (e) {
189
+ e.stopPropagation();
190
+ send({ type: "note_reopen", id: data.id });
191
+ });
192
+ } else {
193
+ action.innerHTML = iconHtml("check") + "<span>Close</span>";
194
+ action.title = "Close this note; it stays on record and can be reopened";
195
+ action.setAttribute("aria-label", "Close note: " + title.textContent);
196
+ action.addEventListener("click", function (e) {
197
+ e.stopPropagation();
198
+ send({ type: "note_close", id: data.id });
199
+ });
200
+ }
201
+ header.appendChild(action);
202
+ card.appendChild(header);
203
+
204
+ var body = document.createElement("div");
205
+ body.className = "notes-browser-card-body";
206
+ var bodyLines = (data.text || "").split("\n").slice(1).join("\n").trim();
207
+ if (bodyLines) {
208
+ body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
209
+ enhanceClayLogLinks(body);
210
+ }
211
+ card.appendChild(body);
212
+
213
+ if (closed) {
214
+ var meta = document.createElement("p");
215
+ meta.className = "notes-browser-card-meta";
216
+ meta.textContent = closedMeta(data);
217
+ card.appendChild(meta);
218
+ }
219
+
220
+ var strip = document.createElement("div");
221
+ strip.className = "notes-browser-card-color";
222
+ card.appendChild(strip);
223
+
224
+ // An open note can be jumped to on the canvas. A closed note has no card on
225
+ // the canvas, so reopening is the only way back and the body is read-only.
226
+ if (!closed) {
227
+ card.tabIndex = 0;
228
+ card.addEventListener("click", function () { focusNoteOnCanvas(data.id); });
229
+ card.addEventListener("keydown", function (e) {
230
+ if (e.key !== "Enter" && e.key !== " ") return;
231
+ e.preventDefault();
232
+ focusNoteOnCanvas(data.id);
233
+ });
234
+ }
235
+ return card;
236
+ }
237
+
238
+ function emptyState(tab) {
239
+ var empty = document.createElement("div");
240
+ empty.className = "notes-browser-empty";
241
+ if (tab === "closed") {
242
+ empty.innerHTML = iconHtml("check") +
243
+ "<p>Nothing closed yet</p>" +
244
+ "<p class=\"notes-browser-empty-sub\">Closing a note takes it off the board without deleting it. Anything you close shows up here and can be reopened.</p>";
245
+ } else {
246
+ empty.innerHTML = iconHtml("sticky-note") +
247
+ "<p>Keep what matters across sessions</p>" +
248
+ "<p class=\"notes-browser-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>";
249
+ }
250
+ return empty;
251
+ }
252
+
253
+ export function renderNotesBrowser() {
254
+ if (!gridEl) return;
255
+ var all = listNoteData();
256
+ var open = openNotes(all);
257
+ var closed = closedNotes(all);
258
+ renderTabs(open.length, closed.length);
259
+
260
+ var tab = currentTab();
261
+ var shown = tab === "closed" ? closed : open;
262
+ // Newest first. The id is timestamp-prefixed, so it orders by creation.
263
+ shown = shown.slice().sort(function (a, b) {
264
+ var left = String(a.id || "");
265
+ var right = String(b.id || "");
266
+ return left < right ? 1 : left > right ? -1 : 0;
267
+ });
268
+
269
+ gridEl.innerHTML = "";
270
+ if (shown.length === 0) {
271
+ gridEl.appendChild(emptyState(tab));
272
+ refreshIcons();
273
+ return;
274
+ }
275
+ for (var i = 0; i < shown.length; i++) gridEl.appendChild(buildCard(shown[i]));
276
+ refreshIcons();
277
+ }
278
+
279
+ // --- Lifecycle -----------------------------------------------------------
280
+
281
+ // The conversation, composer, and title bar stay mounted and visible: the
282
+ // browser opens beside them in the single right workbench slot.
283
+ export function openNotesBrowser() {
284
+ ensurePanel();
285
+ if (!panel) return;
286
+ closeOtherRightTools();
287
+ // The floating canvas would otherwise sit on top of the pane.
288
+ hideNotes();
289
+ panel.classList.remove("hidden");
290
+ applyWindowState(store.get('notesBrowserWide'), false);
291
+ store.set({ notesBrowserOpen: true });
292
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
293
+ if (sidebarBtn) sidebarBtn.classList.add("active");
294
+ renderNotesBrowser();
295
+ }
296
+
297
+ export function closeNotesBrowser() {
298
+ if (!store.get('notesBrowserOpen')) return;
299
+ if (panel) panel.classList.add("hidden");
300
+ // Fullscreen is always dropped on close so the next open is a bounded pane.
301
+ applyWindowState(store.get('notesBrowserWide'), false);
302
+ store.set({ notesBrowserOpen: false });
303
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
304
+ if (sidebarBtn) sidebarBtn.classList.remove("active");
305
+ // The canvas comes back so the open notes are reachable again.
306
+ showNotes();
307
+ }
308
+
309
+ export function isNotesBrowserOpen() {
310
+ return !!store.get('notesBrowserOpen');
311
+ }
312
+
313
+ export function initNotesBrowser() {
314
+ document.addEventListener("keydown", function (e) {
315
+ if (e.key !== "Escape") return;
316
+ if (!store.get('notesBrowserOpen')) return;
317
+ closeNotesBrowser();
318
+ });
319
+ store.subscribe(function (state, previous) {
320
+ if (state.currentSlug === previous.currentSlug) return;
321
+ // A different project starts from a closed, bounded, Open-tab pane.
322
+ closeNotesBrowser();
323
+ applyWindowState(false, false);
324
+ store.set({ notesBrowserTab: "open" });
325
+ });
326
+ }
@@ -0,0 +1,360 @@
1
+ // One floating sticky-note card: its markup, its drag and resize behaviour, and
2
+ // its header controls (colour, minimize, close, bring to front).
3
+ //
4
+ // The card knows how to build and wire itself. It does not own the board, so it
5
+ // never touches the notes map or the badges; the canvas module holds those and
6
+ // calls renderNote for each note.
7
+
8
+ import { refreshIcons, iconHtml } from './icons.js';
9
+ import { VENDOR_AVATARS } from './app-rendering.js';
10
+ import { getTitle, renderMiniMarkdown } from './sticky-note-markdown.js';
11
+ import { enhanceClayLogLinks } from './clay-log-links.js';
12
+ import {
13
+ NOTE_COLORS,
14
+ isClosedNote,
15
+ send,
16
+ clampPos,
17
+ clampSize,
18
+ debouncedUpdate,
19
+ } from './sticky-notes-shared.js';
20
+ import { setupTextEdit } from './sticky-notes-editor.js';
21
+
22
+ var colorPickerEl = null;
23
+
24
+ // --- Note rendering ---
25
+
26
+ export function renderNote(data) {
27
+ var el = document.createElement("div");
28
+ el.className = "sticky-note";
29
+ el.dataset.noteId = data.id;
30
+ var clamped = clampPos(data.x, data.y, data.w, data.h);
31
+ el.style.left = clamped.x + "px";
32
+ el.style.top = clamped.y + "px";
33
+ el.style.width = data.w + "px";
34
+ el.style.height = data.h + "px";
35
+ el.style.zIndex = 100 + (data.zIndex || 0);
36
+ el.dataset.color = data.color || "purple";
37
+
38
+ if (data.minimized) el.classList.add("minimized");
39
+ if (isClosedNote(data)) el.classList.add("hidden");
40
+
41
+ // Header
42
+ var header = document.createElement("div");
43
+ header.className = "sticky-note-header";
44
+
45
+ if (data.origin && VENDOR_AVATARS[data.origin.vendor]) {
46
+ var originBadge = document.createElement("img");
47
+ originBadge.className = "sticky-note-agent-badge";
48
+ originBadge.src = VENDOR_AVATARS[data.origin.vendor];
49
+ originBadge.alt = "";
50
+ originBadge.title = "Created by " + data.origin.vendor;
51
+ header.appendChild(originBadge);
52
+ }
53
+
54
+ var closeBtn = document.createElement("button");
55
+ closeBtn.className = "sticky-note-btn sticky-note-close";
56
+ closeBtn.title = "Close";
57
+ closeBtn.innerHTML = iconHtml("x");
58
+ header.appendChild(closeBtn);
59
+
60
+ var minBtn = document.createElement("button");
61
+ minBtn.className = "sticky-note-btn sticky-note-min-btn";
62
+ minBtn.title = data.minimized ? "Expand" : "Minimize";
63
+ minBtn.innerHTML = data.minimized ? iconHtml("maximize-2") : iconHtml("minus");
64
+ header.appendChild(minBtn);
65
+
66
+ var spacer = document.createElement("div");
67
+ spacer.className = "sticky-note-spacer";
68
+ spacer.textContent = getTitle(data.text) || "Untitled";
69
+ header.appendChild(spacer);
70
+
71
+ var addBtn = document.createElement("button");
72
+ addBtn.className = "sticky-note-btn";
73
+ addBtn.title = "New note";
74
+ addBtn.innerHTML = iconHtml("plus");
75
+ addBtn.addEventListener("click", function (e) {
76
+ e.stopPropagation();
77
+ createNote();
78
+ });
79
+ header.appendChild(addBtn);
80
+
81
+ var colorBtn = document.createElement("button");
82
+ colorBtn.className = "sticky-note-color-btn";
83
+ colorBtn.title = "Change color";
84
+ colorBtn.innerHTML = iconHtml("palette");
85
+ header.appendChild(colorBtn);
86
+
87
+ var mdBtn = document.createElement("button");
88
+ mdBtn.className = "sticky-note-btn sticky-note-md-btn";
89
+ mdBtn.title = "Edit markdown";
90
+ mdBtn.innerHTML = "<span class='sn-md-label'>MD</span>";
91
+ header.appendChild(mdBtn);
92
+
93
+ var opacityWrap = document.createElement("div");
94
+ opacityWrap.className = "sticky-note-opacity";
95
+ var opacitySlider = document.createElement("input");
96
+ opacitySlider.type = "range";
97
+ opacitySlider.min = "20";
98
+ opacitySlider.max = "100";
99
+ opacitySlider.value = String(Math.round((typeof data.opacity === "number" ? data.opacity : 0.64) * 100));
100
+ opacitySlider.className = "sticky-note-opacity-slider";
101
+ opacitySlider.title = "Opacity";
102
+ opacityWrap.appendChild(opacitySlider);
103
+ header.appendChild(opacityWrap);
104
+
105
+ // Apply saved opacity via CSS variable (not element opacity, so header stays visible on hover)
106
+ if (typeof data.opacity === "number") {
107
+ el.style.setProperty("--note-opacity", data.opacity);
108
+ }
109
+
110
+ opacitySlider.addEventListener("input", function (e) {
111
+ e.stopPropagation();
112
+ var val = parseInt(opacitySlider.value, 10) / 100;
113
+ el.style.setProperty("--note-opacity", val);
114
+ });
115
+ opacitySlider.addEventListener("change", function (e) {
116
+ e.stopPropagation();
117
+ var val = parseInt(opacitySlider.value, 10) / 100;
118
+ el.style.setProperty("--note-opacity", val);
119
+ debouncedUpdate(data.id, { opacity: val }, 300);
120
+ });
121
+ opacitySlider.addEventListener("mousedown", function (e) { e.stopPropagation(); });
122
+
123
+ el.appendChild(header);
124
+
125
+ // Body
126
+ var body = document.createElement("div");
127
+ body.className = "sticky-note-body";
128
+
129
+ // Hidden textarea as markdown data store
130
+ var textarea = document.createElement("textarea");
131
+ textarea.className = "sticky-note-text";
132
+ textarea.value = data.text || "";
133
+ textarea.style.display = "none";
134
+ body.appendChild(textarea);
135
+
136
+ // Contenteditable rendered view (primary editing surface)
137
+ var rendered = document.createElement("div");
138
+ rendered.className = "sticky-note-rendered";
139
+ rendered.contentEditable = "true";
140
+ rendered.spellcheck = true;
141
+ if (data.text && data.text.trim()) {
142
+ rendered.innerHTML = renderMiniMarkdown(data.text);
143
+ enhanceClayLogLinks(rendered);
144
+ } else {
145
+ rendered.classList.add("is-empty");
146
+ }
147
+ body.appendChild(rendered);
148
+
149
+ el.appendChild(body);
150
+
151
+ // Resize handle
152
+ var resizeHandle = document.createElement("div");
153
+ resizeHandle.className = "sticky-note-resize";
154
+ el.appendChild(resizeHandle);
155
+
156
+ // --- Event handlers ---
157
+ setupDrag(el, spacer, data.id);
158
+ setupResize(el, resizeHandle, data.id);
159
+ setupTextEdit(textarea, rendered, data.id, mdBtn);
160
+ setupColorPicker(colorBtn, el, data.id);
161
+ setupMinimize(minBtn, el, data.id);
162
+ setupClose(closeBtn, el, data.id);
163
+ setupBringToFront(el, data.id);
164
+
165
+ refreshIcons();
166
+ return el;
167
+ }
168
+ // --- Drag ---
169
+
170
+ function setupDrag(noteEl, spacerEl, noteId) {
171
+ var dragging = false;
172
+ var pointerId = null;
173
+ var startX, startY, origX, origY;
174
+
175
+ spacerEl.addEventListener("pointerdown", function (e) {
176
+ if (e.pointerType === "mouse" && e.button !== 0) return;
177
+ e.preventDefault();
178
+ dragging = true;
179
+ pointerId = e.pointerId;
180
+ startX = e.clientX;
181
+ startY = e.clientY;
182
+ origX = parseInt(noteEl.style.left) || 0;
183
+ origY = parseInt(noteEl.style.top) || 0;
184
+ noteEl.classList.add("dragging");
185
+ document.body.classList.add("sticky-note-interacting");
186
+ spacerEl.setPointerCapture(pointerId);
187
+ });
188
+
189
+ function onMove(e) {
190
+ if (!dragging || e.pointerId !== pointerId) return;
191
+ var dx = e.clientX - startX;
192
+ var dy = e.clientY - startY;
193
+ var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
194
+ noteEl.style.left = c.x + "px";
195
+ noteEl.style.top = c.y + "px";
196
+ }
197
+
198
+ function onUp(e) {
199
+ if (!dragging || (e && e.pointerId !== pointerId)) return;
200
+ dragging = false;
201
+ noteEl.classList.remove("dragging");
202
+ document.body.classList.remove("sticky-note-interacting");
203
+ if (pointerId !== null && spacerEl.hasPointerCapture(pointerId)) spacerEl.releasePointerCapture(pointerId);
204
+ pointerId = null;
205
+ debouncedUpdate(noteId, {
206
+ x: parseInt(noteEl.style.left),
207
+ y: parseInt(noteEl.style.top),
208
+ }, 200);
209
+ }
210
+
211
+ spacerEl.addEventListener("pointermove", onMove);
212
+ spacerEl.addEventListener("pointerup", onUp);
213
+ spacerEl.addEventListener("pointercancel", onUp);
214
+ spacerEl.addEventListener("lostpointercapture", onUp);
215
+ }
216
+
217
+ // --- Resize ---
218
+
219
+ function setupResize(noteEl, handle, noteId) {
220
+ var resizing = false;
221
+ var pointerId = null;
222
+ var startX, startY, origW, origH;
223
+ var MIN_W = 160;
224
+ var MIN_H = 80;
225
+
226
+ handle.addEventListener("pointerdown", function (e) {
227
+ if (e.pointerType === "mouse" && e.button !== 0) return;
228
+ e.preventDefault();
229
+ e.stopPropagation();
230
+ resizing = true;
231
+ pointerId = e.pointerId;
232
+ startX = e.clientX;
233
+ startY = e.clientY;
234
+ origW = noteEl.offsetWidth;
235
+ origH = noteEl.offsetHeight;
236
+ noteEl.classList.add("resizing");
237
+ document.body.classList.add("sticky-note-interacting");
238
+ handle.setPointerCapture(pointerId);
239
+ });
240
+
241
+ function onMove(e) {
242
+ if (!resizing || e.pointerId !== pointerId) return;
243
+ var rawW = Math.max(MIN_W, origW + (e.clientX - startX));
244
+ var rawH = Math.max(MIN_H, origH + (e.clientY - startY));
245
+ var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
246
+ noteEl.style.width = Math.max(MIN_W, cs.w) + "px";
247
+ noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
248
+ }
249
+
250
+ function onUp(e) {
251
+ if (!resizing || (e && e.pointerId !== pointerId)) return;
252
+ resizing = false;
253
+ noteEl.classList.remove("resizing");
254
+ document.body.classList.remove("sticky-note-interacting");
255
+ if (pointerId !== null && handle.hasPointerCapture(pointerId)) handle.releasePointerCapture(pointerId);
256
+ pointerId = null;
257
+ debouncedUpdate(noteId, {
258
+ w: noteEl.offsetWidth,
259
+ h: noteEl.offsetHeight,
260
+ }, 200);
261
+ }
262
+
263
+ handle.addEventListener("pointermove", onMove);
264
+ handle.addEventListener("pointerup", onUp);
265
+ handle.addEventListener("pointercancel", onUp);
266
+ handle.addEventListener("lostpointercapture", onUp);
267
+ }
268
+
269
+ // --- Color picker ---
270
+
271
+ export function closeColorPicker() {
272
+ if (colorPickerEl) {
273
+ colorPickerEl.remove();
274
+ colorPickerEl = null;
275
+ }
276
+ }
277
+
278
+ function setupColorPicker(btn, noteEl, noteId) {
279
+ btn.addEventListener("click", function (e) {
280
+ e.stopPropagation();
281
+ showColorPicker(btn, noteEl, noteId);
282
+ });
283
+ }
284
+
285
+ function showColorPicker(anchor, noteEl, noteId) {
286
+ closeColorPicker();
287
+
288
+ var picker = document.createElement("div");
289
+ picker.className = "sticky-note-color-picker";
290
+
291
+ for (var i = 0; i < NOTE_COLORS.length; i++) {
292
+ (function (color) {
293
+ var dot = document.createElement("button");
294
+ dot.className = "sticky-note-color-dot";
295
+ dot.dataset.color = color;
296
+ if (noteEl.dataset.color === color) dot.classList.add("active");
297
+ dot.addEventListener("click", function (e) {
298
+ e.stopPropagation();
299
+ noteEl.dataset.color = color;
300
+ send({ type: "note_update", id: noteId, color: color });
301
+ closeColorPicker();
302
+ });
303
+ picker.appendChild(dot);
304
+ })(NOTE_COLORS[i]);
305
+ }
306
+
307
+ document.body.appendChild(picker);
308
+ colorPickerEl = picker;
309
+
310
+ // Position relative to anchor
311
+ var rect = anchor.getBoundingClientRect();
312
+ picker.style.left = rect.left + "px";
313
+ picker.style.top = (rect.bottom + 4) + "px";
314
+
315
+ // Close on outside click
316
+ setTimeout(function () {
317
+ document.addEventListener("click", function closeHandler() {
318
+ closeColorPicker();
319
+ document.removeEventListener("click", closeHandler);
320
+ });
321
+ }, 0);
322
+ }
323
+
324
+ // --- Minimize ---
325
+
326
+ function setupMinimize(btn, noteEl, noteId) {
327
+ btn.addEventListener("click", function (e) {
328
+ e.stopPropagation();
329
+ var isMinimized = noteEl.classList.toggle("minimized");
330
+ btn.innerHTML = isMinimized ? iconHtml("maximize-2") : iconHtml("minus");
331
+ btn.title = isMinimized ? "Expand" : "Minimize";
332
+ refreshIcons();
333
+ send({ type: "note_update", id: noteId, minimized: isMinimized });
334
+ });
335
+ }
336
+
337
+ // --- Close note ---
338
+ //
339
+ // Completing a note closes it. The record is kept and stays reachable in the
340
+ // browser's Closed tab; nothing is ever deleted from this control.
341
+
342
+ function setupClose(btn, noteEl, noteId) {
343
+ btn.addEventListener("click", function (e) {
344
+ e.stopPropagation();
345
+ noteEl.classList.add("hidden");
346
+ send({ type: "note_close", id: noteId });
347
+ });
348
+ }
349
+
350
+ // --- Bring to front ---
351
+
352
+ function setupBringToFront(noteEl, noteId) {
353
+ noteEl.addEventListener("mousedown", function (e) {
354
+ // Skip bring-to-front when clicking header buttons to avoid
355
+ // race condition where server response replaces innerHTML
356
+ // between mousedown and click, causing the click event to be lost
357
+ if (e.target.closest("button")) return;
358
+ send({ type: "note_bring_front", id: noteId });
359
+ });
360
+ }