clay-server 4.0.0-beta.16 → 4.0.0-beta.18

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 (50) hide show
  1. package/lib/daemon-projects.js +10 -5
  2. package/lib/daemon.js +61 -10
  3. package/lib/project-log-context.js +171 -0
  4. package/lib/project-log-feedback-delivery.js +47 -5
  5. package/lib/project-logs-context-state.js +42 -0
  6. package/lib/project-logs-mcp-server.js +15 -4
  7. package/lib/project-logs-query.js +18 -0
  8. package/lib/project-logs-root.js +8 -0
  9. package/lib/project-logs-service.js +121 -15
  10. package/lib/project-logs-snapshot.js +3 -0
  11. package/lib/project-logs-store.js +29 -32
  12. package/lib/project-logs-versioning.js +6 -2
  13. package/lib/project-logs.js +61 -13
  14. package/lib/project-message-delivery.js +70 -0
  15. package/lib/project-user-message.js +11 -0
  16. package/lib/project.js +6 -1
  17. package/lib/public/app.js +5 -0
  18. package/lib/public/css/command-palette.css +19 -9
  19. package/lib/public/css/project-logs.css +20 -2
  20. package/lib/public/css/sticky-notes.css +7 -0
  21. package/lib/public/index.html +2 -2
  22. package/lib/public/modules/admin.js +8 -8
  23. package/lib/public/modules/app-connection.js +22 -3
  24. package/lib/public/modules/app-messages.js +29 -1
  25. package/lib/public/modules/app-rendering.js +1 -0
  26. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  27. package/lib/public/modules/clay-log-links.js +70 -0
  28. package/lib/public/modules/command-palette.js +52 -15
  29. package/lib/public/modules/input.js +6 -1
  30. package/lib/public/modules/markdown.js +2 -0
  31. package/lib/public/modules/message-delivery.js +97 -0
  32. package/lib/public/modules/project-logs-render.js +48 -2
  33. package/lib/public/modules/project-logs.js +140 -11
  34. package/lib/public/modules/sidebar-session-hierarchy.js +16 -7
  35. package/lib/public/modules/sidebar-sessions.js +3 -1
  36. package/lib/public/modules/sticky-note-markdown.js +5 -0
  37. package/lib/public/modules/sticky-notes-browser.js +2 -0
  38. package/lib/public/modules/sticky-notes-card.js +2 -0
  39. package/lib/public/modules/sticky-notes-editor.js +3 -0
  40. package/lib/public/modules/sticky-notes.js +2 -0
  41. package/lib/public/style.css +1 -1
  42. package/lib/sdk-bridge.js +20 -10
  43. package/lib/server-global-ws.js +4 -0
  44. package/lib/server-home-chat-events.js +3 -0
  45. package/lib/server-home-chat.js +9 -2
  46. package/lib/server-home-clay-entry.js +1 -1
  47. package/lib/server-home-clay-session-links.js +34 -5
  48. package/lib/server.js +14 -3
  49. package/lib/ws-schema.js +8 -2
  50. package/package.json +1 -1
@@ -6,9 +6,9 @@
6
6
  // record, and Back returns to the list with the query, filter, and scroll
7
7
  // position intact.
8
8
  //
9
- // People read and comment. Canonical entries are created and revised only by
10
- // this project's agent sessions through the clay-logs MCP tools, so this
11
- // module has no create or edit affordance at all.
9
+ // People read and comment, and project owners may delete. Canonical entries
10
+ // are created and revised only by this project's agent sessions through the
11
+ // clay-logs MCP tools, so this module has no create or edit affordance.
12
12
 
13
13
  import { store } from './store.js';
14
14
  import { getWs } from './ws-ref.js';
@@ -18,14 +18,17 @@ import { closeScheduler } from './scheduler.js';
18
18
  import { closeNotesBrowser } from './sticky-notes-browser.js';
19
19
  import { closeFileViewer } from './filebrowser.js';
20
20
  import { closeTerminal } from './terminal.js';
21
- import { renderFilter, renderList, renderDetail } from './project-logs-render.js';
21
+ import { renderFilter, renderContextFilter, renderList, renderDetail } from './project-logs-render.js';
22
22
 
23
23
  var panel = null;
24
24
  var listEl = null;
25
25
  var detailEl = null;
26
26
  var searchEl = null;
27
27
  var filterEl = null;
28
+ var contextFilterEl = null;
28
29
  var backBtn = null;
30
+ var deleteDialog = null;
31
+ var pendingDeleteEntry = null;
29
32
  var searchTimer = null;
30
33
  var requestCounter = 0;
31
34
 
@@ -93,17 +96,32 @@ function ensurePanel() {
93
96
  '</header>' +
94
97
  '<div id="project-logs-toolbar" class="project-logs-toolbar">' +
95
98
  '<label class="project-logs-search"><i data-lucide="search"></i><input id="project-logs-search" type="search" placeholder="Search logs" autocomplete="off"></label>' +
99
+ '<div id="project-logs-context-filter-slot"></div>' +
96
100
  '<div id="project-logs-filter-slot"></div>' +
97
101
  '</div>' +
98
102
  '<div id="project-logs-list" class="project-logs-list" role="list"></div>' +
99
- '<main id="project-logs-detail" class="project-logs-detail hidden"></main>';
103
+ '<main id="project-logs-detail" class="project-logs-detail hidden"></main>' +
104
+ '<div id="project-log-delete-dialog" class="project-log-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="project-log-delete-heading">' +
105
+ '<div class="project-log-delete-dialog-card">' +
106
+ '<h2 id="project-log-delete-heading">Delete this log?</h2>' +
107
+ '<p>This removes the entry from the project ledger. It cannot be restored in the app.</p>' +
108
+ '<p class="project-log-delete-dialog-title"></p>' +
109
+ '<p class="project-log-delete-dialog-status" role="status"></p>' +
110
+ '<div class="project-log-delete-dialog-actions">' +
111
+ '<button type="button" data-action="cancel">Cancel</button>' +
112
+ '<button type="button" class="danger" data-action="delete">Delete log</button>' +
113
+ '</div>' +
114
+ '</div>' +
115
+ '</div>';
100
116
  panels.appendChild(panel);
101
117
 
102
118
  listEl = panel.querySelector("#project-logs-list");
103
119
  detailEl = panel.querySelector("#project-logs-detail");
104
120
  searchEl = panel.querySelector("#project-logs-search");
105
121
  filterEl = panel.querySelector("#project-logs-filter-slot");
122
+ contextFilterEl = panel.querySelector("#project-logs-context-filter-slot");
106
123
  backBtn = panel.querySelector("#project-logs-back");
124
+ deleteDialog = panel.querySelector("#project-log-delete-dialog");
107
125
 
108
126
  panel.querySelector("#project-logs-close").addEventListener("click", closeProjectLogs);
109
127
  backBtn.addEventListener("click", showList);
@@ -117,6 +135,14 @@ function ensurePanel() {
117
135
  if (searchTimer) clearTimeout(searchTimer);
118
136
  searchTimer = setTimeout(requestList, 180);
119
137
  });
138
+ deleteDialog.querySelector('[data-action="cancel"]').addEventListener("click", closeDeleteDialog);
139
+ deleteDialog.querySelector('[data-action="delete"]').addEventListener("click", confirmDelete);
140
+ deleteDialog.addEventListener("click", function (event) {
141
+ if (event.target === deleteDialog) closeDeleteDialog();
142
+ });
143
+ deleteDialog.addEventListener("keydown", function (event) {
144
+ if (event.key === "Escape") closeDeleteDialog();
145
+ });
120
146
  refreshIcons();
121
147
  }
122
148
 
@@ -159,7 +185,48 @@ function showDetail(entry) {
159
185
  detailEl.classList.remove("hidden");
160
186
  detailEl.scrollTop = 0;
161
187
  backBtn.classList.remove("hidden");
162
- renderDetail(detailEl, entry, { onComment: submitComment });
188
+ renderDetail(detailEl, entry, {
189
+ onComment: submitComment,
190
+ onDelete: openDeleteDialog,
191
+ canDelete: store.get('projectLogsCanDelete') === true,
192
+ });
193
+ }
194
+
195
+ function openDeleteDialog(entry) {
196
+ if (!deleteDialog || !entry || store.get('projectLogsCanDelete') !== true) return;
197
+ pendingDeleteEntry = entry;
198
+ deleteDialog.querySelector(".project-log-delete-dialog-title").textContent = entry.title || "Untitled log";
199
+ deleteDialog.querySelector(".project-log-delete-dialog-status").textContent = "";
200
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
201
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
202
+ deleteDialog.classList.remove("hidden");
203
+ deleteDialog.querySelector('[data-action="cancel"]').focus();
204
+ }
205
+
206
+ function closeDeleteDialog(force) {
207
+ if (!deleteDialog) return;
208
+ if (!force && store.get('projectLogsDeleteRequestId')) return;
209
+ deleteDialog.classList.add("hidden");
210
+ pendingDeleteEntry = null;
211
+ store.set({ projectLogsDeleteRequestId: null });
212
+ }
213
+
214
+ function confirmDelete() {
215
+ if (!pendingDeleteEntry || !deleteDialog) return;
216
+ var requestId = nextRequestId("delete-log");
217
+ var status = deleteDialog.querySelector(".project-log-delete-dialog-status");
218
+ var button = deleteDialog.querySelector('[data-action="delete"]');
219
+ var cancelButton = deleteDialog.querySelector('[data-action="cancel"]');
220
+ store.set({ projectLogsDeleteRequestId: requestId });
221
+ button.disabled = true;
222
+ cancelButton.disabled = true;
223
+ status.textContent = "Deleting...";
224
+ if (!send({ type: "project_log_delete", requestId: requestId, ref: pendingDeleteEntry.ref })) {
225
+ button.disabled = false;
226
+ cancelButton.disabled = false;
227
+ store.set({ projectLogsDeleteRequestId: null });
228
+ status.textContent = "Logs are unavailable while disconnected.";
229
+ }
163
230
  }
164
231
 
165
232
  // --- Requests ------------------------------------------------------------
@@ -176,6 +243,7 @@ function requestList() {
176
243
  requestId: requestId,
177
244
  query: searchEl ? searchEl.value.trim() : "",
178
245
  category: store.get('projectLogsCategory') || "",
246
+ contextMode: store.get('projectLogsContextMode') || "",
179
247
  })) {
180
248
  if (listEl) {
181
249
  listEl.removeAttribute("aria-busy");
@@ -206,6 +274,11 @@ function applyFilter(category) {
206
274
  requestList();
207
275
  }
208
276
 
277
+ function applyContextFilter(contextMode) {
278
+ store.set({ projectLogsContextMode: contextMode || "project" });
279
+ requestList();
280
+ }
281
+
209
282
  // --- Lifecycle -----------------------------------------------------------
210
283
 
211
284
  export function initProjectLogs() {
@@ -227,10 +300,14 @@ export function initProjectLogs() {
227
300
  projectLogsSelectedRef: null,
228
301
  projectLogsView: "list",
229
302
  projectLogsCategory: "",
303
+ projectLogsContextMode: "",
304
+ projectLogsIsWorktree: false,
230
305
  projectLogsListScroll: 0,
231
306
  projectLogsListRequestId: null,
232
307
  projectLogsReadRequestId: null,
233
308
  projectLogsCommentRequestId: null,
309
+ projectLogsDeleteRequestId: null,
310
+ projectLogsCanDelete: false,
234
311
  projectLogsUnread: 0,
235
312
  projectLogsSeenRevisions: {}
236
313
  });
@@ -259,11 +336,19 @@ export function openProjectLogs() {
259
336
  requestList();
260
337
  }
261
338
 
339
+ export function openProjectLog(ref) {
340
+ if (typeof ref !== "string" || !/^log:[A-Za-z0-9_-]{24}$/.test(ref)) return false;
341
+ openProjectLogs();
342
+ requestEntry(ref);
343
+ return true;
344
+ }
345
+
262
346
  export function closeProjectLogs() {
263
347
  if (!store.get('projectLogsOpen')) return;
264
348
  if (panel) {
265
349
  panel.classList.add("hidden");
266
350
  }
351
+ closeDeleteDialog(true);
267
352
  // Fullscreen is always dropped on close so the next open is a bounded pane
268
353
  // and never silently hides the conversation.
269
354
  applyWindowState(store.get('projectLogsWide'), false);
@@ -276,21 +361,31 @@ export function closeProjectLogs() {
276
361
 
277
362
  export function handleProjectLogsState(msg) {
278
363
  if (msg.requestId && msg.requestId !== store.get('projectLogsListRequestId')) return;
279
- store.set({ projectLogsEntries: msg.entries || [] });
364
+ var contextMode = msg.contextMode || store.get('projectLogsContextMode') || (msg.isWorktree ? "current" : "project");
365
+ store.set({
366
+ projectLogsEntries: msg.entries || [],
367
+ projectLogsCanDelete: msg.canDelete === true,
368
+ projectLogsContextMode: contextMode,
369
+ projectLogsIsWorktree: msg.isWorktree === true,
370
+ });
280
371
  if (!listEl) return;
281
372
  listEl.removeAttribute("aria-busy");
282
373
  if (filterEl && Array.isArray(msg.categories)) {
283
374
  renderFilter(filterEl, msg.categories, store.get('projectLogsCategory'), applyFilter);
284
375
  }
285
- var isFiltered = !!((searchEl && searchEl.value.trim()) || store.get('projectLogsCategory'));
376
+ if (contextFilterEl) {
377
+ renderContextFilter(contextFilterEl, contextMode, msg.isWorktree === true, applyContextFilter);
378
+ }
379
+ var isFiltered = !!((searchEl && searchEl.value.trim()) || store.get('projectLogsCategory') || contextMode !== "project");
286
380
  renderList(listEl, msg.entries || [], requestEntry,
287
- isFiltered ? "No entries match this search or category." : "No logs yet. This project's agent sessions record decisions and work here.");
381
+ isFiltered ? "No entries match the current scope, search, or category." : "No logs yet. This project's agent sessions record decisions and work here.");
288
382
  if (store.get('projectLogsView') !== "detail") listEl.scrollTop = store.get('projectLogsListScroll') || 0;
289
383
  }
290
384
 
291
385
  export function handleProjectLogEntry(msg) {
292
386
  if (msg.requestId && msg.requestId !== store.get('projectLogsReadRequestId')) return;
293
387
  if (!msg.entry) return;
388
+ store.set({ projectLogsCanDelete: msg.canDelete === true });
294
389
  showDetail(msg.entry);
295
390
  }
296
391
 
@@ -300,18 +395,45 @@ export function handleProjectLogCommented(msg) {
300
395
  // The comment is a proposal, not a change, so say so plainly rather than
301
396
  // leaving the composer reading "Posting..." forever.
302
397
  var pendingStatus = store.get('projectLogsCommentStatusEl');
303
- if (pendingStatus) pendingStatus.textContent = "Awaiting Project Driver review";
398
+ if (pendingStatus) pendingStatus.textContent = msg.reviewQueued
399
+ ? "Project Driver is reviewing..."
400
+ : "Awaiting Project Driver review";
304
401
  store.set({ projectLogsCommentStatusEl: null });
305
402
  // Re-render the open entry so the new comment and its attribution appear,
306
403
  // then refresh the ledger so the comment count is current.
307
404
  showDetail(msg.entry);
405
+ if (msg.reviewQueued && detailEl) {
406
+ var badges = detailEl.querySelectorAll(".project-log-comment-status-badge");
407
+ if (badges.length > 0) badges[badges.length - 1].textContent = "Project Driver is reviewing";
408
+ }
409
+ requestList();
410
+ }
411
+
412
+ export function handleProjectLogCommentReviewed(msg) {
413
+ if (!msg || !msg.ref || !store.get('projectLogsOpen')) return;
414
+ if (store.get('projectLogsView') === "detail" && msg.ref === store.get('projectLogsSelectedRef')) {
415
+ requestEntry(msg.ref);
416
+ return;
417
+ }
418
+ requestList();
419
+ }
420
+
421
+ export function handleProjectLogDeleted(msg) {
422
+ if (msg.requestId && msg.requestId !== store.get('projectLogsDeleteRequestId')) return;
423
+ closeDeleteDialog(true);
424
+ showList();
308
425
  requestList();
426
+ showToast("Log deleted.");
309
427
  }
310
428
 
311
429
  // A canonical revision landed. This marks the explicit button only; it never
312
430
  // opens, reveals, focuses, or scrolls the pane.
313
431
  export function handleProjectLogUpdated(msg) {
314
432
  noteCanonicalUpdate(msg);
433
+ if (msg.op === "delete" && msg.ref === store.get('projectLogsSelectedRef')) {
434
+ closeDeleteDialog(true);
435
+ showList();
436
+ }
315
437
  // Refresh the ledger only when it is already on screen, so the row moves to
316
438
  // the top the moment its canonical update arrives.
317
439
  if (store.get('projectLogsOpen') && store.get('projectLogsView') !== "detail") requestList();
@@ -321,12 +443,19 @@ export function handleProjectLogsError(msg) {
321
443
  var pending = [
322
444
  store.get('projectLogsListRequestId'),
323
445
  store.get('projectLogsReadRequestId'),
324
- store.get('projectLogsCommentRequestId')
446
+ store.get('projectLogsCommentRequestId'),
447
+ store.get('projectLogsDeleteRequestId')
325
448
  ];
326
449
  if (msg.requestId && pending.indexOf(msg.requestId) === -1) return;
327
450
  if (msg.requestId && msg.requestId === store.get('projectLogsListRequestId') && listEl) {
328
451
  listEl.removeAttribute("aria-busy");
329
452
  renderList(listEl, [], requestEntry, "The ledger could not be loaded. Try opening Logs again.");
330
453
  }
454
+ if (msg.requestId && msg.requestId === store.get('projectLogsDeleteRequestId') && deleteDialog) {
455
+ store.set({ projectLogsDeleteRequestId: null });
456
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
457
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
458
+ deleteDialog.querySelector(".project-log-delete-dialog-status").textContent = msg.message || "The log could not be deleted.";
459
+ }
331
460
  showToast(msg.message || "Project Logs could not complete the request.", "error");
332
461
  }
@@ -55,6 +55,10 @@ export function hierarchyItemMatches(item, matchIds) {
55
55
  return true;
56
56
  }
57
57
 
58
+ export function isDesktopHierarchyToggleEvent(event) {
59
+ return !!(event && event.target && typeof event.target.closest === "function" && event.target.closest(".session-driver-toggle"));
60
+ }
61
+
58
62
  function expanded(surface, key, workers, matchIds) {
59
63
  var state = expansionBySurface[surface];
60
64
  if (state.has(key)) return state.get(key);
@@ -93,20 +97,25 @@ export function renderDesktopDriverHierarchy(root, renderSession, rerender, matc
93
97
  control.setAttribute("aria-controls", childrenId);
94
98
  control.setAttribute("aria-label", (isExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
95
99
  control.innerHTML = iconHtml("chevron-right");
100
+ var children = document.createElement("div");
101
+ children.id = childrenId;
102
+ children.className = "session-worker-children";
103
+ children.setAttribute("role", "group");
104
+ children.setAttribute("aria-label", "Workers for " + (root.driver.title || "New Session"));
105
+ children.hidden = !isExpanded;
96
106
  control.addEventListener("click", function (event) {
97
107
  event.preventDefault();
98
108
  event.stopPropagation();
99
- toggle("desktop", key, isExpanded, rerender);
109
+ var nextExpanded = !isExpanded;
110
+ expansionBySurface.desktop.set(key, nextExpanded);
111
+ control.setAttribute("aria-expanded", String(nextExpanded));
112
+ control.setAttribute("aria-label", (nextExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
113
+ children.hidden = !nextExpanded;
114
+ isExpanded = nextExpanded;
100
115
  });
101
116
  row.insertBefore(control, row.firstChild);
102
117
  wrapper.appendChild(row);
103
118
 
104
- var children = document.createElement("div");
105
- children.id = childrenId;
106
- children.className = "session-worker-children";
107
- children.setAttribute("role", "group");
108
- children.setAttribute("aria-label", "Workers for " + (root.driver.title || "New Session"));
109
- children.hidden = !isExpanded;
110
119
  var current = currentWorkerIds();
111
120
  for (var i = 0; i < root.workers.length; i++) {
112
121
  if (!visibleWorker(root.workers[i], root.driver, matchIds)) continue;
@@ -18,6 +18,7 @@ import { groupedSessionIds } from './split-group-helpers.js';
18
18
  import { openPairDialog } from './split-pair-ui.js';
19
19
  import {
20
20
  hierarchyItemMatches,
21
+ isDesktopHierarchyToggleEvent,
21
22
  prepareSidebarHierarchy,
22
23
  renderDesktopDriverHierarchy,
23
24
  renderDesktopOrphanHierarchy
@@ -1333,7 +1334,8 @@ function renderSessionItem(s, options) {
1333
1334
  appendSessionCloseButton(el, s);
1334
1335
 
1335
1336
  el.addEventListener("click", (function (id) {
1336
- return function () {
1337
+ return function (event) {
1338
+ if (isDesktopHierarchyToggleEvent(event)) return;
1337
1339
  if (getWs() && store.get('connected')) {
1338
1340
  var pendingQuery = searchQuery || "";
1339
1341
  getWs().send(JSON.stringify({ type: "switch_session", id: id }));
@@ -58,6 +58,11 @@ function nodeToMarkdown(node) {
58
58
  return "\n" + inner;
59
59
  case "P": return "\n" + inner;
60
60
  case "A": return node.getAttribute("href") || inner;
61
+ case "BUTTON":
62
+ if (node.classList.contains("clayos-log-link") && node.dataset && node.dataset.logRef) {
63
+ return "`" + node.dataset.logRef + "`";
64
+ }
65
+ return inner;
61
66
  case "SPAN":
62
67
  if (node.classList.contains("sn-check")) {
63
68
  return node.classList.contains("checked") ? "- [x]" : "- [ ]";
@@ -11,6 +11,7 @@
11
11
  import { store } from './store.js';
12
12
  import { iconHtml, refreshIcons } from './icons.js';
13
13
  import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
14
+ import { enhanceClayLogLinks } from './clay-log-links.js';
14
15
  import { send } from './sticky-notes-shared.js';
15
16
  import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
16
17
 
@@ -205,6 +206,7 @@ function buildCard(data) {
205
206
  var bodyLines = (data.text || "").split("\n").slice(1).join("\n").trim();
206
207
  if (bodyLines) {
207
208
  body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
209
+ enhanceClayLogLinks(body);
208
210
  }
209
211
  card.appendChild(body);
210
212
 
@@ -8,6 +8,7 @@
8
8
  import { refreshIcons, iconHtml } from './icons.js';
9
9
  import { VENDOR_AVATARS } from './app-rendering.js';
10
10
  import { getTitle, renderMiniMarkdown } from './sticky-note-markdown.js';
11
+ import { enhanceClayLogLinks } from './clay-log-links.js';
11
12
  import {
12
13
  NOTE_COLORS,
13
14
  isClosedNote,
@@ -139,6 +140,7 @@ export function renderNote(data) {
139
140
  rendered.spellcheck = true;
140
141
  if (data.text && data.text.trim()) {
141
142
  rendered.innerHTML = renderMiniMarkdown(data.text);
143
+ enhanceClayLogLinks(rendered);
142
144
  } else {
143
145
  rendered.classList.add("is-empty");
144
146
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { refreshIcons, iconHtml } from './icons.js';
9
9
  import { extractMarkdown, renderMiniMarkdown } from './sticky-note-markdown.js';
10
+ import { enhanceClayLogLinks } from './clay-log-links.js';
10
11
  import { debouncedTextUpdate, syncTitle } from './sticky-notes-shared.js';
11
12
 
12
13
  var formatToolbarEl = null;
@@ -183,6 +184,7 @@ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
183
184
  syncTitle(noteEl, md);
184
185
  if (md.trim()) {
185
186
  rendered.innerHTML = renderMiniMarkdown(md);
187
+ enhanceClayLogLinks(rendered);
186
188
  rendered.classList.remove("is-empty");
187
189
  } else {
188
190
  rendered.innerHTML = "";
@@ -217,6 +219,7 @@ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
217
219
  textarea.value = md;
218
220
  if (md.trim()) {
219
221
  rendered.innerHTML = renderMiniMarkdown(md);
222
+ enhanceClayLogLinks(rendered);
220
223
  rendered.classList.remove("is-empty");
221
224
  } else {
222
225
  rendered.innerHTML = "";
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { refreshIcons, iconHtml } from './icons.js';
9
9
  import { renderMiniMarkdown } from './sticky-note-markdown.js';
10
+ import { enhanceClayLogLinks } from './clay-log-links.js';
10
11
  import { isClosedNote, send, clampPos, clearNoteTimers, syncTitle } from './sticky-notes-shared.js';
11
12
  import { renderNote, closeColorPicker } from './sticky-notes-card.js';
12
13
  import { closeFormatToolbar, isFormatToolbarOpen } from './sticky-notes-editor.js';
@@ -240,6 +241,7 @@ function patchNote(entry, next) {
240
241
  if (textarea) textarea.value = nextText;
241
242
  if (nextText.trim()) {
242
243
  rendered.innerHTML = renderMiniMarkdown(nextText);
244
+ enhanceClayLogLinks(rendered);
243
245
  rendered.classList.remove("is-empty");
244
246
  } else {
245
247
  rendered.innerHTML = "";
@@ -41,7 +41,7 @@
41
41
  @import url("css/session-search.css");
42
42
  @import url("css/tooltip.css");
43
43
  @import url("css/mates.css?v=20260407");
44
- @import url("css/command-palette.css?v=20260901-clay-chat2");
44
+ @import url("css/command-palette.css?v=20260908-log-links2");
45
45
  @import url("css/mention.css");
46
46
  @import url("css/debate.css");
47
47
  @import url("css/home-debate-planning.css");
package/lib/sdk-bridge.js CHANGED
@@ -1623,6 +1623,21 @@ function createSDKBridge(opts) {
1623
1623
  if (selectedUserInputMode === "fallback") {
1624
1624
  sessionToolDefs = sessionToolDefs.concat(yoke.userInput.fallbackToolDefs(sessionUserInputHandler));
1625
1625
  }
1626
+ function findSessionToolDef(toolName) {
1627
+ // Codex restores a thread's original dynamic-tool catalog on resume, but
1628
+ // pair state can change after this query starts. Resolve live session
1629
+ // handlers first so the dispatcher cannot lag behind that catalog. Keep
1630
+ // the query snapshot as a fallback for query-bound tools such as the
1631
+ // structured-input fallback, whose handler exists only for this turn.
1632
+ var liveToolDefs = getSessionToolDefs(session) || [];
1633
+ for (var liveIndex = 0; liveIndex < liveToolDefs.length; liveIndex++) {
1634
+ if (liveToolDefs[liveIndex].name === toolName) return liveToolDefs[liveIndex];
1635
+ }
1636
+ for (var snapshotIndex = 0; snapshotIndex < sessionToolDefs.length; snapshotIndex++) {
1637
+ if (sessionToolDefs[snapshotIndex].name === toolName) return sessionToolDefs[snapshotIndex];
1638
+ }
1639
+ return null;
1640
+ }
1626
1641
 
1627
1642
  // Give fresh queries an immediate provisional label. The visible user
1628
1643
  // message in history wins over `text`, which may contain an internal
@@ -1663,10 +1678,9 @@ function createSDKBridge(opts) {
1663
1678
  toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
1664
1679
  dynamicTools: sessionToolDefs,
1665
1680
  callDynamicTool: function (toolName, args) {
1666
- for (var dti = 0; dti < sessionToolDefs.length; dti++) {
1667
- if (sessionToolDefs[dti].name === toolName && typeof sessionToolDefs[dti].handler === "function") {
1668
- return sessionToolDefs[dti].handler(args || {});
1669
- }
1681
+ var toolDef = findSessionToolDef(toolName);
1682
+ if (toolDef && typeof toolDef.handler === "function") {
1683
+ return toolDef.handler(args || {});
1670
1684
  }
1671
1685
  return Promise.reject(new Error("Session tool not found: " + toolName));
1672
1686
  },
@@ -1674,12 +1688,8 @@ function createSDKBridge(opts) {
1674
1688
  abortController: linuxUser ? undefined : session.abortController,
1675
1689
  canUseTool: function(toolName, input, toolOpts) {
1676
1690
  var permissionName = toolName;
1677
- for (var pti = 0; pti < sessionToolDefs.length; pti++) {
1678
- if (sessionToolDefs[pti].name === toolName && sessionToolDefs[pti].permissionName) {
1679
- permissionName = sessionToolDefs[pti].permissionName;
1680
- break;
1681
- }
1682
- }
1691
+ var permissionToolDef = findSessionToolDef(toolName);
1692
+ if (permissionToolDef && permissionToolDef.permissionName) permissionName = permissionToolDef.permissionName;
1683
1693
  return handleCanUseTool(session, permissionName, input, toolOpts);
1684
1694
  },
1685
1695
  onUserInputRequest: function(request, respond) {
@@ -7,6 +7,7 @@
7
7
  // Bound to the slug-less `/ws` endpoint by lib/server.js. Only handles the
8
8
  // small set of messages needed to bootstrap into a project context:
9
9
  // - ping -> pong keep-alive
10
+ // - home_* -> user-scoped Home preferences
10
11
  // - browse_dir -> directory picker for the add-project modal
11
12
  // - add_project -> register an existing directory
12
13
  // - create_project -> make a new empty project
@@ -37,6 +38,7 @@ function attachGlobalWs(opts) {
37
38
  var onAddProject = opts.onAddProject;
38
39
  var onCreateProject = opts.onCreateProject;
39
40
  var onCloneProject = opts.onCloneProject;
41
+ var onHomePreferenceMessage = opts.onHomePreferenceMessage;
40
42
 
41
43
  function handleMessage(ws, msg) {
42
44
  if (!msg || typeof msg !== "object") return;
@@ -46,6 +48,8 @@ function attachGlobalWs(opts) {
46
48
  return;
47
49
  }
48
50
 
51
+ if (typeof onHomePreferenceMessage === "function" && onHomePreferenceMessage(ws, msg)) return;
52
+
49
53
  // --- Directory picker for the add-project modal ---
50
54
  if (msg.type === "browse_dir") {
51
55
  var rawPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
@@ -228,6 +228,9 @@ function searchToolLabel(name, input) {
228
228
  if (name === "list_project_sessions") return "Reviewing project conversations";
229
229
  if (name === "list_projects") return "Reviewing your projects";
230
230
  if (name === "search_project_history") return query ? "Searching a project for \u201c" + query + "\u201d" : "Searching a project";
231
+ if (name === "search_project_logs") return query ? "Searching project logs for \u201c" + query + "\u201d" : "Searching project logs";
232
+ if (name === "list_project_logs") return "Reviewing project logs";
233
+ if (name === "read_project_log" || name === "read_project_log_revision" || name === "project_log_history") return "Reading a project log";
231
234
  return "Using " + (cleanActivityText(name, 64) || "a workspace tool");
232
235
  }
233
236
 
@@ -204,7 +204,12 @@ function attachHomeChat(deps) {
204
204
 
205
205
  var homeModels = attachHomeModels({ users: users, mates: mates, projects: projects, sendMessage: sendMessage });
206
206
  var homeDebates = attachHomeDebates({ mates: mates, findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, sendMessage: sendMessage });
207
- var homeClaySessionLinks = attachHomeClaySessionLinks({ projects: projects, workspaceQueryService: deps.workspaceQueryService, sendMessage: sendMessage });
207
+ var homeClaySessionLinks = attachHomeClaySessionLinks({
208
+ projects: projects,
209
+ workspaceQueryService: deps.workspaceQueryService,
210
+ projectLogsService: deps.projectLogsService,
211
+ sendMessage: sendMessage,
212
+ });
208
213
 
209
214
  function sendHistory(ws, found, session, requestId) {
210
215
  if (ws.readyState !== 1) return;
@@ -383,7 +388,7 @@ function attachHomeChat(deps) {
383
388
 
384
389
  function handleMessage(ws, msg) {
385
390
  if (!msg || typeof msg.type !== "string") return false;
386
- if (msg.type !== "home_debates_list" && msg.type !== "home_clay_ask" && msg.type !== "home_clay_session_resolve" && msg.type !== "home_mate_present" && msg.type !== "home_mate_open" && msg.type !== "home_mate_sessions_list" && msg.type !== "home_mate_session_open" && msg.type !== "home_mate_send" && msg.type !== "home_mate_new_session" && msg.type !== "home_mate_debate_plan" && msg.type !== "home_debate_proposal_response" && msg.type !== "home_debate_question_response" && msg.type !== "home_debate_control" && msg.type !== "home_mate_creation_plan" && msg.type !== "home_mate_creation_question_response" && msg.type !== "home_mate_creation_proposal_response" && msg.type !== "home_mate_close" && msg.type !== "home_mate_memory_list" && msg.type !== "home_mate_knowledge_list" && msg.type !== "home_mate_models_get" && msg.type !== "home_mate_model_set") {
391
+ if (msg.type !== "home_debates_list" && msg.type !== "home_clay_ask" && msg.type !== "home_clay_session_resolve" && msg.type !== "home_clay_log_resolve" && msg.type !== "home_mate_present" && msg.type !== "home_mate_open" && msg.type !== "home_mate_sessions_list" && msg.type !== "home_mate_session_open" && msg.type !== "home_mate_send" && msg.type !== "home_mate_new_session" && msg.type !== "home_mate_debate_plan" && msg.type !== "home_debate_proposal_response" && msg.type !== "home_debate_question_response" && msg.type !== "home_debate_control" && msg.type !== "home_mate_creation_plan" && msg.type !== "home_mate_creation_question_response" && msg.type !== "home_mate_creation_proposal_response" && msg.type !== "home_mate_close" && msg.type !== "home_mate_memory_list" && msg.type !== "home_mate_knowledge_list" && msg.type !== "home_mate_models_get" && msg.type !== "home_mate_model_set") {
387
392
  return false;
388
393
  }
389
394
 
@@ -402,6 +407,7 @@ function attachHomeChat(deps) {
402
407
  if (users.isMultiUser() && !userId) {
403
408
  if (msg.type === "home_debates_list") sendMessage(ws, { type: "home_debates_state", requestId: msg.requestId || null, status: "error", debates: [], error: "Sign in to load your debates." });
404
409
  else if (msg.type === "home_clay_session_resolve") sendMessage(ws, { type: "home_clay_session_target", requestId: msg.requestId || null, sessionRef: msg.sessionRef || null, status: "error", error: "Sign in to open this session." });
410
+ else if (msg.type === "home_clay_log_resolve") sendMessage(ws, { type: "home_clay_log_target", requestId: msg.requestId || null, ref: msg.ref || null, status: "error", error: "Sign in to open this log." });
405
411
  else if (msg.type === "home_mate_models_get" || msg.type === "home_mate_model_set") homeModels.sendAccessError(ws, msg, "Not authenticated.");
406
412
  else sendError(ws, msg.mateId || null, "Not authenticated.", msg.requestId || null, msg.sessionId || null);
407
413
  return true;
@@ -410,6 +416,7 @@ function attachHomeChat(deps) {
410
416
  if (homeDebates.handle(ws, userId, msg)) return true;
411
417
  if (msg.type === "home_clay_ask") { homeClayEntry.start(ws, userId, msg); return true; }
412
418
  if (msg.type === "home_clay_session_resolve") { homeClaySessionLinks.resolve(ws, msg); return true; }
419
+ if (msg.type === "home_clay_log_resolve") { homeClaySessionLinks.resolveLog(ws, msg); return true; }
413
420
 
414
421
  if (msg.type === "home_mate_debate_plan") {
415
422
  homeDebate.start(ws, userId, msg);
@@ -2,7 +2,7 @@
2
2
 
3
3
  function attachHomeClayEntry(ctx) {
4
4
  function providerPrompt(text) {
5
- return "This conversation began in Clay Studio's global search. Respond as Clay. When the user is trying to locate prior work, use search_workspace_history before any other investigation. Search by meaning, related terms, and context, using at most three focused search passes. Do not use Bash, filesystem scans, or generic agents to search Clay conversation history. Give the user useful findings promptly, and do not mention tools or internal search mechanics. Do not force a workspace search when the request is a general question or task.\n\nUser request:\n" + text;
5
+ return "This conversation began in Clay Studio's global search. Respond as Clay. When the user is trying to locate a conversation, use search_workspace_history before any other investigation. When the user asks about a project's prior work, decisions, defects, status, rationale, or unfinished work, identify the project and search_project_logs before answering; use conversation history as additional evidence when useful. Search by meaning, related terms, and context, using at most three focused search passes. Do not use Bash, filesystem scans, or generic agents to search Clay history. Give the user useful findings promptly, and do not mention tools or internal search mechanics. Do not force a workspace or Logs search when the request is a general question or task.\n\nUser request:\n" + text;
6
6
  }
7
7
 
8
8
  function findExisting(manager, userId, requestId) {
@@ -1,13 +1,19 @@
1
- // Resolve rendered Mate session references through an exact owner-bound source session.
1
+ // Resolve rendered Mate session and Project Log references through an exact
2
+ // owner-bound source session.
2
3
 
3
4
  function attachHomeClaySessionLinks(ctx) {
4
- function resolve(ws, msg) {
5
+ function source(ws, msg) {
5
6
  var tap = msg.surface === "search" ? ws._searchClayTap : ws._homeChatTap;
6
7
  var sourceProject = tap && tap.mateSlug ? ctx.projects.get(tap.mateSlug) : null;
7
8
  var sourceManager = sourceProject && sourceProject.getSessionManager ? sourceProject.getSessionManager() : null;
8
9
  var sourceSession = sourceManager && tap ? sourceManager.sessions.get(tap.sessionId) : null;
9
- var bound = ctx.workspaceQueryService && sourceSession
10
- ? ctx.workspaceQueryService.bindProjectSession({ projectSlug: tap.mateSlug, session: sourceSession })
10
+ return { tap: tap, project: sourceProject, session: sourceSession };
11
+ }
12
+
13
+ function resolve(ws, msg) {
14
+ var selected = source(ws, msg);
15
+ var bound = ctx.workspaceQueryService && selected.session
16
+ ? ctx.workspaceQueryService.bindProjectSession({ projectSlug: selected.tap.mateSlug, session: selected.session })
11
17
  : null;
12
18
  var payload = { type: "home_clay_session_target", requestId: msg.requestId || null, sessionRef: msg.sessionRef || null, status: "error" };
13
19
  try {
@@ -20,7 +26,30 @@ function attachHomeClaySessionLinks(ctx) {
20
26
  ctx.sendMessage(ws, payload);
21
27
  }
22
28
 
23
- return { resolve: resolve };
29
+ function resolveLog(ws, msg) {
30
+ var selected = source(ws, msg);
31
+ var status = selected.project && selected.project.getStatus ? selected.project.getStatus() : null;
32
+ var bound = ctx.projectLogsService && selected.session && status
33
+ ? ctx.projectLogsService.bindMate({
34
+ projectSlug: selected.tap.mateSlug,
35
+ projectOwnerId: status.projectOwnerId || null,
36
+ isMate: true,
37
+ mateId: selected.tap.mateId,
38
+ session: selected.session,
39
+ })
40
+ : null;
41
+ var payload = { type: "home_clay_log_target", requestId: msg.requestId || null, ref: msg.ref || null, status: "error" };
42
+ try {
43
+ if (!bound) throw new Error("The source conversation is no longer available.");
44
+ payload.target = bound.resolveLogNavigation({ ref: msg.ref });
45
+ payload.status = "ready";
46
+ } catch (error) {
47
+ payload.error = error.message || "Log not available.";
48
+ }
49
+ ctx.sendMessage(ws, payload);
50
+ }
51
+
52
+ return { resolve: resolve, resolveLog: resolveLog };
24
53
  }
25
54
 
26
55
  module.exports = { attachHomeClaySessionLinks: attachHomeClaySessionLinks };