clay-server 4.0.0-beta.1 → 4.0.0-beta.11

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 (99) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/config.js +25 -1
  3. package/lib/daemon.js +22 -0
  4. package/lib/git-cli.js +94 -1
  5. package/lib/knowledge-import.js +436 -0
  6. package/lib/knowledge-record-store.js +188 -0
  7. package/lib/knowledge-search.js +240 -0
  8. package/lib/mate-knowledge-mcp-server.js +151 -0
  9. package/lib/mate-knowledge-migration.js +283 -0
  10. package/lib/mate-knowledge-service.js +488 -0
  11. package/lib/mate-knowledge-sync.js +482 -0
  12. package/lib/notes-lifecycle.js +149 -0
  13. package/lib/notes.js +60 -3
  14. package/lib/project-connection.js +17 -0
  15. package/lib/project-http.js +15 -1
  16. package/lib/project-knowledge.js +22 -0
  17. package/lib/project-logs-comments.js +137 -0
  18. package/lib/project-logs-mcp-server.js +314 -0
  19. package/lib/project-logs-query.js +167 -0
  20. package/lib/project-logs-root.js +67 -0
  21. package/lib/project-logs-schema.js +259 -0
  22. package/lib/project-logs-service.js +357 -0
  23. package/lib/project-logs-snapshot.js +202 -0
  24. package/lib/project-logs-store.js +498 -0
  25. package/lib/project-logs-versioning.js +128 -0
  26. package/lib/project-logs.js +348 -0
  27. package/lib/project-mate-interaction.js +14 -0
  28. package/lib/project-mate-knowledge.js +110 -0
  29. package/lib/project-memory.js +22 -0
  30. package/lib/project-pair-lifecycle.js +461 -0
  31. package/lib/project-session-notes.js +64 -13
  32. package/lib/project-session-pair.js +124 -111
  33. package/lib/project-sessions.js +46 -6
  34. package/lib/project-user-message.js +41 -4
  35. package/lib/project-vendor-login.js +414 -0
  36. package/lib/project-worker-permission.js +429 -0
  37. package/lib/project-worker-proposal.js +16 -1
  38. package/lib/project.js +164 -4
  39. package/lib/public/app.js +49 -20
  40. package/lib/public/css/git-placard.css +154 -0
  41. package/lib/public/css/input.css +79 -25
  42. package/lib/public/css/notifications-center.css +67 -0
  43. package/lib/public/css/pane.css +52 -0
  44. package/lib/public/css/project-logs.css +234 -0
  45. package/lib/public/css/scheduler.css +1 -1
  46. package/lib/public/css/sticky-notes.css +231 -226
  47. package/lib/public/index.html +12 -0
  48. package/lib/public/modules/app-connection.js +90 -4
  49. package/lib/public/modules/app-messages.js +54 -19
  50. package/lib/public/modules/app-notifications.js +15 -124
  51. package/lib/public/modules/app-panels.js +43 -2
  52. package/lib/public/modules/app-projects.js +2 -2
  53. package/lib/public/modules/background-tasks-ui.js +139 -16
  54. package/lib/public/modules/git-agent-sessions.js +85 -0
  55. package/lib/public/modules/git-panel.js +111 -84
  56. package/lib/public/modules/git-placard.js +161 -0
  57. package/lib/public/modules/home-sidebar.js +7 -0
  58. package/lib/public/modules/mate-sidebar.js +0 -8
  59. package/lib/public/modules/pane-bridge.js +9 -0
  60. package/lib/public/modules/project-logs-ambient.js +238 -0
  61. package/lib/public/modules/project-logs-render.js +359 -0
  62. package/lib/public/modules/project-logs.js +329 -0
  63. package/lib/public/modules/scheduler.js +17 -3
  64. package/lib/public/modules/sidebar-mobile.js +5 -9
  65. package/lib/public/modules/sidebar-sessions.js +25 -1
  66. package/lib/public/modules/sidebar.js +17 -5
  67. package/lib/public/modules/split-view.js +7 -0
  68. package/lib/public/modules/sticky-notes-browser.js +324 -0
  69. package/lib/public/modules/sticky-notes-card.js +358 -0
  70. package/lib/public/modules/sticky-notes-editor.js +288 -0
  71. package/lib/public/modules/sticky-notes-shared.js +90 -0
  72. package/lib/public/modules/sticky-notes.js +168 -946
  73. package/lib/public/modules/tool-palette-order.js +143 -0
  74. package/lib/public/modules/tool-palette-overlays.js +159 -0
  75. package/lib/public/modules/tool-palette.js +21 -188
  76. package/lib/public/modules/tui-attention.js +10 -0
  77. package/lib/public/modules/update-snooze.js +169 -0
  78. package/lib/public/modules/vendor-login.js +287 -0
  79. package/lib/public/modules/worker-pane-lock.js +174 -0
  80. package/lib/public/style.css +2 -0
  81. package/lib/sdk-bridge.js +77 -10
  82. package/lib/sdk-message-processor.js +24 -7
  83. package/lib/server.js +61 -6
  84. package/lib/session-driver-eligibility.js +183 -0
  85. package/lib/session-notes-mcp-server.js +25 -5
  86. package/lib/session-pair-factory.js +233 -0
  87. package/lib/session-pair-mcp-server.js +45 -5
  88. package/lib/session-pair-prompts.js +60 -0
  89. package/lib/sessions.js +4 -1
  90. package/lib/tools-registry.js +11 -1
  91. package/lib/update-snooze.js +437 -0
  92. package/lib/workspace-query-access.js +102 -0
  93. package/lib/workspace-query-service.js +24 -17
  94. package/lib/ws-schema.js +28 -3
  95. package/lib/yoke/adapters/claude.js +69 -4
  96. package/lib/yoke/adapters/codex.js +42 -5
  97. package/lib/yoke/codex-app-server.js +20 -2
  98. package/lib/yoke/codex-background-tasks.js +8 -2
  99. package/package.json +3 -3
@@ -125,8 +125,8 @@ export function initScheduler(_ctx) {
125
125
  function ensurePanel() {
126
126
  if (panel) return;
127
127
 
128
- var appEl = document.getElementById("app");
129
- if (!appEl) return;
128
+ var hostEl = document.getElementById("main-area");
129
+ if (!hostEl) return;
130
130
 
131
131
  panel = document.createElement("div");
132
132
  panel.id = "scheduler-panel";
@@ -256,7 +256,7 @@ function ensurePanel() {
256
256
  bodyRow.appendChild(content);
257
257
  panel.appendChild(bodyRow);
258
258
 
259
- appEl.appendChild(panel);
259
+ hostEl.appendChild(panel);
260
260
 
261
261
  // --- Close button (in top bar) ---
262
262
  panel.querySelector("#scheduler-panel-close").addEventListener("click", function () {
@@ -383,6 +383,8 @@ function openScheduler() {
383
383
 
384
384
  var sidebarBtn = document.getElementById("scheduler-btn");
385
385
  if (sidebarBtn) sidebarBtn.classList.add("active");
386
+ var homeBtn = document.getElementById("home-scheduler-btn");
387
+ if (homeBtn) homeBtn.classList.add("active");
386
388
 
387
389
  // Persist scheduler state in URL hash
388
390
  if (location.hash !== "#scheduler") {
@@ -423,6 +425,8 @@ export function closeScheduler() {
423
425
  // Un-mark sidebar button
424
426
  var sidebarBtn = document.getElementById("scheduler-btn");
425
427
  if (sidebarBtn) sidebarBtn.classList.remove("active");
428
+ var homeBtn = document.getElementById("home-scheduler-btn");
429
+ if (homeBtn) homeBtn.classList.remove("active");
426
430
 
427
431
  // Remove scheduler hash from URL
428
432
  if (location.hash === "#scheduler") {
@@ -1470,6 +1474,16 @@ export function openSchedulerToTab(tab) {
1470
1474
  }
1471
1475
  }
1472
1476
 
1477
+ export function openHomeScheduler() {
1478
+ if (!ctx) return;
1479
+ ctx.requireClayRalph(function () {
1480
+ showAllProjects = true;
1481
+ openSchedulerToTab("calendar");
1482
+ renderSidebar();
1483
+ render();
1484
+ });
1485
+ }
1486
+
1473
1487
  export function isSchedulerOpen() {
1474
1488
  return panelOpen;
1475
1489
  }
@@ -887,8 +887,7 @@ function renderSheetMateProfile(listEl) {
887
887
  var actions = [
888
888
  { icon: "book-open", label: "Knowledge", btnId: "mate-knowledge-btn", countId: "mate-knowledge-count" },
889
889
  { icon: "sticky-note", label: "Sticky Notes", btnId: "sticky-notes-sidebar-btn", countId: "sticky-notes-sidebar-count" },
890
- { icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" },
891
- { icon: "calendar", label: "Scheduled Tasks", btnId: "mate-scheduler-btn" }
890
+ { icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" }
892
891
  ];
893
892
 
894
893
  for (var i = 0; i < actions.length; i++) {
@@ -1005,12 +1004,11 @@ function renderSheetTools(listEl) {
1005
1004
  { icon: "brain", label: "Memory", action: "mate-memory" },
1006
1005
  { icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
1007
1006
  { icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
1008
- { icon: "puzzle", label: "Skills", action: "mate-skills" },
1009
- { icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" }
1007
+ { icon: "puzzle", label: "Skills", action: "mate-skills" }
1010
1008
  ] : [
1011
1009
  { icon: "folder-tree", label: "Files", action: "files" },
1012
1010
  { icon: "square-terminal", label: "Terminal", action: "terminal" },
1013
- { icon: "calendar-clock", label: "Scheduled Tasks", action: "scheduler" }
1011
+ { icon: "notebook-tabs", label: "Logs", action: "project-logs" }
1014
1012
  ];
1015
1013
 
1016
1014
  for (var i = 0; i < items.length; i++) {
@@ -1025,8 +1023,8 @@ function renderSheetTools(listEl) {
1025
1023
  setTimeout(function () { openMobileSheet("files"); }, 250);
1026
1024
  } else if (item.action === "terminal") {
1027
1025
  openTerminal();
1028
- } else if (item.action === "scheduler") {
1029
- targetId = "scheduler-btn";
1026
+ } else if (item.action === "project-logs") {
1027
+ targetId = "project-logs-btn";
1030
1028
  } else if (item.action === "mate-knowledge") {
1031
1029
  setTimeout(function () { openMobileSheet("mate-knowledge"); }, 250);
1032
1030
  return;
@@ -1036,8 +1034,6 @@ function renderSheetTools(listEl) {
1036
1034
  targetId = "mate-skills-btn";
1037
1035
  } else if (item.action === "mate-memory") {
1038
1036
  targetId = "mate-memory-btn";
1039
- } else if (item.action === "mate-scheduler") {
1040
- targetId = "mate-scheduler-btn";
1041
1037
  } else if (item.action === "mate-debate") {
1042
1038
  targetId = "mate-debate-btn";
1043
1039
  }
@@ -1376,8 +1376,29 @@ function renderSplitGroupItem(group, members) {
1376
1376
 
1377
1377
  // --- Main session list ---
1378
1378
 
1379
+ // Fingerprint of the last completed render: the whole session payload plus the
1380
+ // two store values this render also reads. Nothing is cherry-picked, so the
1381
+ // guard can only skip a frame that is byte-identical to what is already on
1382
+ // screen. A re-render triggered by other state (sessions === null) always
1383
+ // runs and resets the fingerprint afterwards.
1384
+ var lastRenderFingerprint = null;
1385
+
1386
+ function sessionRenderFingerprint(sessions) {
1387
+ try {
1388
+ return JSON.stringify([sessions, store.get('splitGroups') || [], store.get('splitPanes') || null]);
1389
+ } catch (e) {
1390
+ return null;
1391
+ }
1392
+ }
1393
+
1379
1394
  export function renderSessionList(sessions) {
1380
- if (sessions) cachedSessions = sessions;
1395
+ if (sessions) {
1396
+ // A reconnect and a streaming turn both re-send this list unchanged;
1397
+ // rebuilding the sidebar for an identical frame is what made it flicker.
1398
+ var incoming = sessionRenderFingerprint(sessions);
1399
+ if (incoming !== null && incoming === lastRenderFingerprint) return;
1400
+ cachedSessions = sessions;
1401
+ }
1381
1402
 
1382
1403
  // If mobile chat sheet is open, refresh it
1383
1404
  if (refreshMobileChatSheet) refreshMobileChatSheet();
@@ -1516,6 +1537,9 @@ export function renderSessionList(sessions) {
1516
1537
  refreshIcons();
1517
1538
  if (updatePageTitle) updatePageTitle();
1518
1539
  syncHeaderSearchUi();
1540
+ // Recorded from the sessions actually rendered, so a null-argument re-render
1541
+ // driven by other state refreshes the fingerprint rather than stranding it.
1542
+ lastRenderFingerprint = sessionRenderFingerprint(cachedSessions);
1519
1543
  }
1520
1544
 
1521
1545
  // --- Search results ---
@@ -1,4 +1,4 @@
1
- import { closeArchive } from './sticky-notes.js';
1
+ import { closeNotesBrowser } from './sticky-notes-browser.js';
2
2
  import { closeScheduler } from './scheduler.js';
3
3
  import { initSidebarSessions } from './sidebar-sessions.js';
4
4
  import { focusFileTree } from './filebrowser.js';
@@ -23,7 +23,7 @@ export function syncResizeHandles() {
23
23
  }
24
24
 
25
25
  export function dismissOverlayPanels() {
26
- closeArchive();
26
+ closeNotesBrowser();
27
27
  closeScheduler();
28
28
  }
29
29
 
@@ -119,7 +119,10 @@ export function initSidebar(_ctx) {
119
119
 
120
120
  // --- Panel switch (sessions / files / projects) ---
121
121
  var fileBrowserBtn = ctx.$("file-browser-btn");
122
- var gitBtn = ctx.$("git-sidebar-btn");
122
+ // Git has no tool-palette tile. The repository placard's More control is
123
+ // the entry point into the same panel, with the same lifecycle.
124
+ var gitPlacard = ctx.$("git-placard");
125
+ var gitPlacardMore = ctx.$("git-placard-more");
123
126
  var projectsPanel = ctx.$("sidebar-panel-projects");
124
127
  var sessionsPanel = ctx.$("sidebar-panel-sessions");
125
128
  var filesPanel = ctx.$("sidebar-panel-files");
@@ -216,14 +219,23 @@ export function initSidebar(_ctx) {
216
219
  });
217
220
  });
218
221
  }
219
- if (gitBtn) {
220
- gitBtn.addEventListener("click", function () {
222
+ if (gitPlacardMore) {
223
+ gitPlacardMore.addEventListener("click", function () {
221
224
  if (gitPanel && !gitPanel.classList.contains("hidden")) {
222
225
  hideGitPanel(function () { showSessionsPanel(); });
223
226
  }
224
227
  else showGitPanel();
225
228
  });
226
229
  }
230
+ if (gitPlacard && gitPlacardMore) {
231
+ // Clicking the block itself is a pointer convenience only; it forwards to
232
+ // the More control so every listener bound there runs exactly once, and
233
+ // the keyboard path stays the single focusable button.
234
+ gitPlacard.addEventListener("click", function (event) {
235
+ if (event.target.closest("#git-placard-more")) return;
236
+ gitPlacardMore.click();
237
+ });
238
+ }
227
239
  if (gitPanelClose) {
228
240
  gitPanelClose.addEventListener("click", function () {
229
241
  hideGitPanel(function () { showSessionsPanel(); });
@@ -11,6 +11,7 @@ import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
11
11
  import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
13
  import { presentMarkdownEdit } from './filebrowser.js';
14
+ import { autoStartLoginIfNeeded } from './vendor-login.js';
14
15
 
15
16
  var host = null;
16
17
  var nativeApp = null;
@@ -276,6 +277,12 @@ function handlePaneMessage(event) {
276
277
  if (present) presentMarkdownEdit(present);
277
278
  return;
278
279
  }
280
+ // Both panes can report auth_required for their own sessions. The shell runs
281
+ // one login flow for the project; repeat events are no-ops there.
282
+ if (msg.type === "clay-pane-auth-required") {
283
+ if (msg.message) autoStartLoginIfNeeded(msg.message);
284
+ return;
285
+ }
279
286
  if (msg.type !== "clay-pane-context") return;
280
287
  var frames = host.querySelectorAll(".split-pane-frame");
281
288
  for (var i = 0; i < frames.length; i++) {
@@ -0,0 +1,324 @@
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 { send } from './sticky-notes-shared.js';
15
+ import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
16
+
17
+ var panel = null;
18
+ var gridEl = null;
19
+ var tabsEl = null;
20
+
21
+ // Registered by app.js so this module never imports the other right-pane tools
22
+ // and cannot form an import cycle with them.
23
+ var exclusiveClosers = [];
24
+
25
+ export function registerExclusiveClosers(fns) {
26
+ exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
27
+ }
28
+
29
+ function closeOtherRightTools() {
30
+ for (var i = 0; i < exclusiveClosers.length; i++) {
31
+ try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
32
+ }
33
+ }
34
+
35
+ function isClosed(data) {
36
+ if (!data) return false;
37
+ if (data.state === "closed") return true;
38
+ if (data.state === "open") return false;
39
+ // Legacy notes predate the state field; `hidden` was the old spelling.
40
+ return data.hidden === true;
41
+ }
42
+
43
+ export function openNotes(all) {
44
+ return (all || []).filter(function (data) { return !isClosed(data); });
45
+ }
46
+
47
+ export function closedNotes(all) {
48
+ return (all || []).filter(isClosed);
49
+ }
50
+
51
+ function currentTab() {
52
+ return store.get('notesBrowserTab') === "closed" ? "closed" : "open";
53
+ }
54
+
55
+ // --- Panel ---------------------------------------------------------------
56
+
57
+ function ensurePanel() {
58
+ if (panel) return;
59
+ var panels = document.getElementById("main-panels");
60
+ if (!panels) return;
61
+ panel = document.createElement("section");
62
+ panel.id = "notes-browser";
63
+ panel.className = "notes-browser hidden";
64
+ panel.setAttribute("aria-label", "Sticky Notes");
65
+ panel.innerHTML =
66
+ '<header class="notes-browser-topbar">' +
67
+ '<span class="notes-browser-title">' + iconHtml("sticky-note") + 'Sticky Notes</span>' +
68
+ '<span class="notes-browser-subtitle">Open items on the board, and what has been closed</span>' +
69
+ '<div class="notes-browser-window-actions" aria-label="Sticky Notes window controls">' +
70
+ '<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>' +
71
+ '<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>' +
72
+ '<button id="notes-browser-close" class="notes-browser-icon-btn" type="button" title="Close Sticky Notes" aria-label="Close Sticky Notes">' + iconHtml("x") + '</button>' +
73
+ '</div>' +
74
+ '</header>' +
75
+ '<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
76
+ '<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>';
77
+ panels.appendChild(panel);
78
+
79
+ gridEl = panel.querySelector("#notes-browser-grid");
80
+ tabsEl = panel.querySelector("#notes-browser-tabs");
81
+
82
+ panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
83
+ panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
84
+ applyWindowState(!store.get('notesBrowserWide'), store.get('notesBrowserFullscreen'));
85
+ });
86
+ panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
87
+ applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
88
+ });
89
+ refreshIcons();
90
+ }
91
+
92
+ // Width and fullscreen are transient view state, so they live in the store.
93
+ // `panel-fullscreen` is the shared class the document viewer, terminal, and
94
+ // Logs already use, so the geometry is the established one.
95
+ function applyWindowState(wide, fullscreen) {
96
+ store.set({ notesBrowserWide: !!wide, notesBrowserFullscreen: !!fullscreen });
97
+ if (!panel) return;
98
+ panel.classList.toggle("notes-browser-wide", !!wide && !fullscreen);
99
+ panel.classList.toggle("panel-fullscreen", !!fullscreen);
100
+ var wideBtn = panel.querySelector("#notes-browser-wide");
101
+ var fullBtn = panel.querySelector("#notes-browser-fullscreen");
102
+ if (wideBtn) {
103
+ wideBtn.setAttribute("aria-pressed", wide ? "true" : "false");
104
+ wideBtn.disabled = !!fullscreen;
105
+ }
106
+ if (fullBtn) fullBtn.setAttribute("aria-pressed", fullscreen ? "true" : "false");
107
+ }
108
+
109
+ // --- Tabs ----------------------------------------------------------------
110
+
111
+ function renderTabs(openCount, closedCount) {
112
+ if (!tabsEl) return;
113
+ var active = currentTab();
114
+ tabsEl.innerHTML = "";
115
+ var specs = [
116
+ { key: "open", label: "Open", count: openCount },
117
+ { key: "closed", label: "Closed", count: closedCount },
118
+ ];
119
+ for (var i = 0; i < specs.length; i++) {
120
+ (function (spec) {
121
+ var tab = document.createElement("button");
122
+ tab.type = "button";
123
+ tab.className = "notes-browser-tab" + (spec.key === active ? " active" : "");
124
+ tab.setAttribute("role", "tab");
125
+ tab.setAttribute("aria-selected", spec.key === active ? "true" : "false");
126
+ tab.tabIndex = spec.key === active ? 0 : -1;
127
+ tab.dataset.tab = spec.key;
128
+ tab.innerHTML = '<span>' + spec.label + '</span><span class="notes-browser-tab-count">' + spec.count + '</span>';
129
+ tab.addEventListener("click", function () { selectTab(spec.key); });
130
+ tab.addEventListener("keydown", function (e) {
131
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
132
+ e.preventDefault();
133
+ selectTab(spec.key === "open" ? "closed" : "open");
134
+ var next = tabsEl.querySelector('.notes-browser-tab.active');
135
+ if (next) next.focus();
136
+ });
137
+ tabsEl.appendChild(tab);
138
+ })(specs[i]);
139
+ }
140
+ }
141
+
142
+ function selectTab(key) {
143
+ store.set({ notesBrowserTab: key === "closed" ? "closed" : "open" });
144
+ renderNotesBrowser();
145
+ }
146
+
147
+ // --- Cards ---------------------------------------------------------------
148
+
149
+ function closedMeta(data) {
150
+ var parts = [];
151
+ if (data.closedAt) {
152
+ var when = new Date(data.closedAt);
153
+ if (!isNaN(when.getTime())) parts.push("Closed " + when.toLocaleDateString());
154
+ } else {
155
+ parts.push("Closed");
156
+ }
157
+ var by = data.closedBy;
158
+ if (by && by.type === "user" && by.displayName) parts.push("by " + by.displayName);
159
+ else if (by && by.type === "session") parts.push("by an agent session");
160
+ return parts.join(" · ");
161
+ }
162
+
163
+ function buildCard(data) {
164
+ var card = document.createElement("article");
165
+ var closed = isClosed(data);
166
+ card.className = "notes-browser-card" + (closed ? " closed" : "");
167
+ card.setAttribute("role", "listitem");
168
+ card.dataset.color = data.color || "purple";
169
+ card.dataset.noteId = data.id;
170
+
171
+ var header = document.createElement("div");
172
+ header.className = "notes-browser-card-header";
173
+
174
+ var title = document.createElement("h3");
175
+ title.className = "notes-browser-card-title";
176
+ title.textContent = getTitle(data.text) || "Untitled";
177
+ header.appendChild(title);
178
+
179
+ // Exactly one lifecycle action per card, and it is never destructive.
180
+ var action = document.createElement("button");
181
+ action.type = "button";
182
+ action.className = "notes-browser-card-action";
183
+ if (closed) {
184
+ action.innerHTML = iconHtml("rotate-ccw") + "<span>Reopen</span>";
185
+ action.title = "Reopen this note and return it to the board";
186
+ action.setAttribute("aria-label", "Reopen note: " + title.textContent);
187
+ action.addEventListener("click", function (e) {
188
+ e.stopPropagation();
189
+ send({ type: "note_reopen", id: data.id });
190
+ });
191
+ } else {
192
+ action.innerHTML = iconHtml("check") + "<span>Close</span>";
193
+ action.title = "Close this note; it stays on record and can be reopened";
194
+ action.setAttribute("aria-label", "Close note: " + title.textContent);
195
+ action.addEventListener("click", function (e) {
196
+ e.stopPropagation();
197
+ send({ type: "note_close", id: data.id });
198
+ });
199
+ }
200
+ header.appendChild(action);
201
+ card.appendChild(header);
202
+
203
+ var body = document.createElement("div");
204
+ body.className = "notes-browser-card-body";
205
+ var bodyLines = (data.text || "").split("\n").slice(1).join("\n").trim();
206
+ if (bodyLines) {
207
+ body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
208
+ }
209
+ card.appendChild(body);
210
+
211
+ if (closed) {
212
+ var meta = document.createElement("p");
213
+ meta.className = "notes-browser-card-meta";
214
+ meta.textContent = closedMeta(data);
215
+ card.appendChild(meta);
216
+ }
217
+
218
+ var strip = document.createElement("div");
219
+ strip.className = "notes-browser-card-color";
220
+ card.appendChild(strip);
221
+
222
+ // An open note can be jumped to on the canvas. A closed note has no card on
223
+ // the canvas, so reopening is the only way back and the body is read-only.
224
+ if (!closed) {
225
+ card.tabIndex = 0;
226
+ card.addEventListener("click", function () { focusNoteOnCanvas(data.id); });
227
+ card.addEventListener("keydown", function (e) {
228
+ if (e.key !== "Enter" && e.key !== " ") return;
229
+ e.preventDefault();
230
+ focusNoteOnCanvas(data.id);
231
+ });
232
+ }
233
+ return card;
234
+ }
235
+
236
+ function emptyState(tab) {
237
+ var empty = document.createElement("div");
238
+ empty.className = "notes-browser-empty";
239
+ if (tab === "closed") {
240
+ empty.innerHTML = iconHtml("check") +
241
+ "<p>Nothing closed yet</p>" +
242
+ "<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>";
243
+ } else {
244
+ empty.innerHTML = iconHtml("sticky-note") +
245
+ "<p>Keep what matters across sessions</p>" +
246
+ "<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>";
247
+ }
248
+ return empty;
249
+ }
250
+
251
+ export function renderNotesBrowser() {
252
+ if (!gridEl) return;
253
+ var all = listNoteData();
254
+ var open = openNotes(all);
255
+ var closed = closedNotes(all);
256
+ renderTabs(open.length, closed.length);
257
+
258
+ var tab = currentTab();
259
+ var shown = tab === "closed" ? closed : open;
260
+ // Newest first. The id is timestamp-prefixed, so it orders by creation.
261
+ shown = shown.slice().sort(function (a, b) {
262
+ var left = String(a.id || "");
263
+ var right = String(b.id || "");
264
+ return left < right ? 1 : left > right ? -1 : 0;
265
+ });
266
+
267
+ gridEl.innerHTML = "";
268
+ if (shown.length === 0) {
269
+ gridEl.appendChild(emptyState(tab));
270
+ refreshIcons();
271
+ return;
272
+ }
273
+ for (var i = 0; i < shown.length; i++) gridEl.appendChild(buildCard(shown[i]));
274
+ refreshIcons();
275
+ }
276
+
277
+ // --- Lifecycle -----------------------------------------------------------
278
+
279
+ // The conversation, composer, and title bar stay mounted and visible: the
280
+ // browser opens beside them in the single right workbench slot.
281
+ export function openNotesBrowser() {
282
+ ensurePanel();
283
+ if (!panel) return;
284
+ closeOtherRightTools();
285
+ // The floating canvas would otherwise sit on top of the pane.
286
+ hideNotes();
287
+ panel.classList.remove("hidden");
288
+ applyWindowState(store.get('notesBrowserWide'), false);
289
+ store.set({ notesBrowserOpen: true });
290
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
291
+ if (sidebarBtn) sidebarBtn.classList.add("active");
292
+ renderNotesBrowser();
293
+ }
294
+
295
+ export function closeNotesBrowser() {
296
+ if (!store.get('notesBrowserOpen')) return;
297
+ if (panel) panel.classList.add("hidden");
298
+ // Fullscreen is always dropped on close so the next open is a bounded pane.
299
+ applyWindowState(store.get('notesBrowserWide'), false);
300
+ store.set({ notesBrowserOpen: false });
301
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
302
+ if (sidebarBtn) sidebarBtn.classList.remove("active");
303
+ // The canvas comes back so the open notes are reachable again.
304
+ showNotes();
305
+ }
306
+
307
+ export function isNotesBrowserOpen() {
308
+ return !!store.get('notesBrowserOpen');
309
+ }
310
+
311
+ export function initNotesBrowser() {
312
+ document.addEventListener("keydown", function (e) {
313
+ if (e.key !== "Escape") return;
314
+ if (!store.get('notesBrowserOpen')) return;
315
+ closeNotesBrowser();
316
+ });
317
+ store.subscribe(function (state, previous) {
318
+ if (state.currentSlug === previous.currentSlug) return;
319
+ // A different project starts from a closed, bounded, Open-tab pane.
320
+ closeNotesBrowser();
321
+ applyWindowState(false, false);
322
+ store.set({ notesBrowserTab: "open" });
323
+ });
324
+ }