clay-server 4.0.0-beta.17 → 4.0.0-beta.19

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 (41) hide show
  1. package/lib/daemon-projects.js +8 -8
  2. package/lib/daemon.js +42 -10
  3. package/lib/git-cli.js +1 -0
  4. package/lib/os-users.js +3 -1
  5. package/lib/project-log-context.js +24 -23
  6. package/lib/project-logs-mcp-server.js +10 -4
  7. package/lib/project-logs-service.js +29 -0
  8. package/lib/project-logs.js +5 -1
  9. package/lib/project-sessions.js +5 -1
  10. package/lib/project.js +1 -1
  11. package/lib/public/css/command-palette.css +19 -9
  12. package/lib/public/css/sidebar.css +11 -0
  13. package/lib/public/css/sticky-notes.css +7 -0
  14. package/lib/public/index.html +2 -2
  15. package/lib/public/modules/admin.js +8 -8
  16. package/lib/public/modules/app-home-hub.js +19 -7
  17. package/lib/public/modules/app-projects.js +5 -3
  18. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  19. package/lib/public/modules/clay-log-links.js +70 -0
  20. package/lib/public/modules/command-palette.js +52 -15
  21. package/lib/public/modules/markdown.js +2 -0
  22. package/lib/public/modules/project-logs.js +7 -0
  23. package/lib/public/modules/sidebar-session-hierarchy.js +16 -8
  24. package/lib/public/modules/sticky-note-markdown.js +5 -0
  25. package/lib/public/modules/sticky-notes-browser.js +2 -0
  26. package/lib/public/modules/sticky-notes-card.js +2 -0
  27. package/lib/public/modules/sticky-notes-editor.js +3 -0
  28. package/lib/public/modules/sticky-notes.js +2 -0
  29. package/lib/public/style.css +1 -1
  30. package/lib/sdk-bridge.js +20 -10
  31. package/lib/server-global-ws.js +11 -0
  32. package/lib/server-home-chat-events.js +3 -0
  33. package/lib/server-home-chat.js +9 -2
  34. package/lib/server-home-clay-entry.js +1 -1
  35. package/lib/server-home-clay-session-links.js +34 -5
  36. package/lib/server.js +19 -10
  37. package/lib/session-hygiene.js +3 -0
  38. package/lib/users.js +16 -16
  39. package/lib/worktree.js +25 -38
  40. package/lib/ws-schema.js +2 -0
  41. package/package.json +1 -1
@@ -417,13 +417,15 @@ export function switchProject(slug) {
417
417
  if (!slug) return;
418
418
  document.cookie = "clay_last_project=" + encodeURIComponent(slug) + "; Path=/; SameSite=Strict; Max-Age=31536000";
419
419
  var st = store.snap();
420
+ var targetWsPath = "/p/" + slug + "/ws";
421
+ var alreadyInProject = slug === st.currentSlug && st.wsPath === targetWsPath;
420
422
  var wasDm = st.dmMode;
421
423
  var wasMate = st.dmMode && st.dmTargetUser && st.dmTargetUser.isMate;
422
424
  if (st.dmMode) exitDmMode(wasMate);
423
425
  closeWhatsNewArticle();
424
426
  if (isHomeHubVisible()) {
425
427
  hideHomeHub();
426
- if (slug === store.get('currentSlug')) {
428
+ if (alreadyInProject) {
427
429
  if (document.documentElement.classList.contains("pwa-standalone")) {
428
430
  history.replaceState(null, "", "/p/" + slug + "/");
429
431
  } else {
@@ -432,7 +434,7 @@ export function switchProject(slug) {
432
434
  return;
433
435
  }
434
436
  }
435
- if (slug === store.get('currentSlug')) {
437
+ if (alreadyInProject) {
436
438
  var ws = getWs();
437
439
  if (wasDm && ws && ws.readyState === 1) {
438
440
  ws.send(JSON.stringify({ type: "switch_session", id: store.get('activeSessionId') }));
@@ -446,7 +448,7 @@ export function switchProject(slug) {
446
448
  resetScheduler(slug);
447
449
  store.set({ currentSlug: slug });
448
450
  store.set({ basePath: "/p/" + slug + "/" });
449
- store.set({ wsPath: "/p/" + slug + "/ws" });
451
+ store.set({ wsPath: targetWsPath });
450
452
  if (document.documentElement.classList.contains("pwa-standalone")) {
451
453
  history.replaceState(null, "", "/p/" + slug + "/");
452
454
  } else {
@@ -105,7 +105,7 @@ export function addAssistantCopyHandler(row, rawText) {
105
105
  }
106
106
 
107
107
  row.addEventListener("click", function (event) {
108
- if (event.target.closest("a, pre, code")) return;
108
+ if (event.target.closest("a, button, pre, code")) return;
109
109
  var selection = window.getSelection();
110
110
  if (selection && selection.toString().length > 0) return;
111
111
  if (!primed) {
@@ -0,0 +1,70 @@
1
+ export function parseClayLogReferences(text) {
2
+ var pattern = /\[clayos\/(log:[A-Za-z0-9_-]{24})(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]|(^|[^A-Za-z0-9_/:])(log:[A-Za-z0-9_-]{24})(?![A-Za-z0-9_-])/g;
3
+ var results = [];
4
+ var match;
5
+ while ((match = pattern.exec(text || ""))) {
6
+ results.push({
7
+ start: match.index,
8
+ end: pattern.lastIndex,
9
+ prefix: match[3] || "",
10
+ ref: match[1] || match[4],
11
+ label: match[2] ? match[2].trim() : "",
12
+ });
13
+ }
14
+ return results;
15
+ }
16
+
17
+ export function isExactClayLogReference(text) {
18
+ return /^log:[A-Za-z0-9_-]{24}$/.test((text || "").trim());
19
+ }
20
+
21
+ function createLogLink(ref, labelText) {
22
+ var button = document.createElement("button");
23
+ button.type = "button";
24
+ button.className = "clayos-log-link";
25
+ button.contentEditable = "false";
26
+ button.dataset.logRef = ref;
27
+ button.setAttribute("aria-label", "Open Project Log" + (labelText ? " " + labelText : ""));
28
+ var label = document.createElement("span");
29
+ label.textContent = "Log";
30
+ button.appendChild(label);
31
+ if (labelText) {
32
+ var meta = document.createElement("small");
33
+ meta.textContent = labelText;
34
+ button.appendChild(meta);
35
+ }
36
+ return button;
37
+ }
38
+
39
+ export function enhanceClayLogLinks(root) {
40
+ var codeNodes = root.querySelectorAll("code");
41
+ for (var codeIndex = 0; codeIndex < codeNodes.length; codeIndex++) {
42
+ var code = codeNodes[codeIndex];
43
+ var codeText = (code.textContent || "").trim();
44
+ if (code.closest("pre") || !isExactClayLogReference(codeText)) continue;
45
+ code.parentNode.replaceChild(createLogLink(codeText, ""), code);
46
+ }
47
+
48
+ var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
49
+ var nodes = [];
50
+ var node;
51
+ while ((node = walker.nextNode())) {
52
+ if (!node.parentElement || node.parentElement.closest("code, pre, a, button")) continue;
53
+ if (parseClayLogReferences(node.nodeValue).length) nodes.push(node);
54
+ }
55
+ for (var i = 0; i < nodes.length; i++) {
56
+ var text = nodes[i].nodeValue || "";
57
+ var matches = parseClayLogReferences(text);
58
+ var fragment = document.createDocumentFragment();
59
+ var offset = 0;
60
+ for (var j = 0; j < matches.length; j++) {
61
+ var match = matches[j];
62
+ fragment.appendChild(document.createTextNode(text.slice(offset, match.start)));
63
+ if (match.prefix) fragment.appendChild(document.createTextNode(match.prefix));
64
+ fragment.appendChild(createLogLink(match.ref, match.label));
65
+ offset = match.end;
66
+ }
67
+ fragment.appendChild(document.createTextNode(text.slice(offset)));
68
+ nodes[i].parentNode.replaceChild(fragment, nodes[i]);
69
+ }
70
+ }
@@ -6,6 +6,7 @@ import { showHomeHub, minimizeHomeHub } from './app-home-hub.js';
6
6
  import { openHomeConversation } from './home-mate-chat.js';
7
7
  import { startSearchClayChat, attachSearchClayChat, detachSearchClayChat, getSearchClayChatSummary } from './search-clay-chat.js';
8
8
  import { getWs } from './ws-ref.js';
9
+ import { openProjectLog } from './project-logs.js';
9
10
 
10
11
  function formatRelativeDate(ts) {
11
12
  if (!ts) return "";
@@ -27,6 +28,7 @@ var ctx;
27
28
  var paletteEl = null, inputEl = null, resultsEl = null, footerEl = null;
28
29
  var activeIndex = -1, items = [], debounceTimer = null, abortCtrl = null;
29
30
  var pendingNav = null, cachedHomeData = [], cachedVersion = null;
31
+ var pendingLogNav = null;
30
32
  var searchResults = [], searchQuery = "", searchPending = false, chatMode = false;
31
33
 
32
34
  export function initCommandPalette(value) {
@@ -52,43 +54,62 @@ export function initCommandPalette(value) {
52
54
  }
53
55
 
54
56
  function handleClaySessionLinkClick(event) {
55
- var link = event.target.closest(".clayos-session-link");
57
+ var link = event.target.closest(".clayos-session-link, .clayos-log-link");
56
58
  if (!link || link.classList.contains("is-loading")) return;
59
+ event.preventDefault();
60
+ event.stopPropagation();
61
+ if (link.classList.contains("clayos-log-link") && !link.closest(".search-clay-chat, #home-mate-chat")) {
62
+ openProjectLog(link.dataset.logRef);
63
+ return;
64
+ }
57
65
  var ws = getWs();
58
66
  if (!ws || ws.readyState !== 1) {
59
67
  link.classList.add("is-error");
60
- link.title = "Reconnect to open this session";
68
+ link.title = "Reconnect to open this item";
61
69
  return;
62
70
  }
63
- var requestId = "clayos-session-" + Date.now() + "-" + Math.random().toString(36).slice(2, 9);
64
- link.dataset.sessionRequest = requestId;
71
+ var isLog = link.classList.contains("clayos-log-link");
72
+ var requestId = "clayos-" + (isLog ? "log-" : "session-") + Date.now() + "-" + Math.random().toString(36).slice(2, 9);
73
+ if (isLog) link.dataset.logRequest = requestId;
74
+ else link.dataset.sessionRequest = requestId;
65
75
  link.classList.remove("is-error");
66
76
  link.classList.add("is-loading");
67
- ws.send(JSON.stringify({
68
- type: "home_clay_session_resolve",
69
- requestId: requestId,
70
- sessionRef: link.dataset.sessionRef,
71
- surface: link.closest(".search-clay-chat") ? "search" : "home",
72
- }));
77
+ var request = { requestId: requestId, surface: link.closest(".search-clay-chat") ? "search" : "home" };
78
+ if (isLog) {
79
+ request.type = "home_clay_log_resolve";
80
+ request.ref = link.dataset.logRef;
81
+ } else {
82
+ request.type = "home_clay_session_resolve";
83
+ request.sessionRef = link.dataset.sessionRef;
84
+ }
85
+ ws.send(JSON.stringify(request));
73
86
  }
74
87
 
75
88
  export function handleClaySessionTarget(msg) {
76
- if (!msg || msg.type !== "home_clay_session_target") return false;
77
- var links = document.querySelectorAll(".clayos-session-link[data-session-request]");
89
+ if (!msg || (msg.type !== "home_clay_session_target" && msg.type !== "home_clay_log_target")) return false;
90
+ var isLog = msg.type === "home_clay_log_target";
91
+ var links = document.querySelectorAll(isLog ? ".clayos-log-link[data-log-request]" : ".clayos-session-link[data-session-request]");
78
92
  var link = null;
79
- for (var i = 0; i < links.length; i++) if (links[i].dataset.sessionRequest === msg.requestId) link = links[i];
93
+ for (var i = 0; i < links.length; i++) {
94
+ if ((isLog ? links[i].dataset.logRequest : links[i].dataset.sessionRequest) === msg.requestId) link = links[i];
95
+ }
80
96
  if (link) {
81
97
  link.classList.remove("is-loading");
82
- delete link.dataset.sessionRequest;
98
+ if (isLog) delete link.dataset.logRequest;
99
+ else delete link.dataset.sessionRequest;
83
100
  }
84
101
  if (msg.status !== "ready" || !msg.target) {
85
102
  if (link) {
86
103
  link.classList.add("is-error");
87
- link.title = msg.error || "Session not available";
104
+ link.title = msg.error || "Item not available";
88
105
  }
89
106
  return true;
90
107
  }
91
108
  closeCommandPalette();
109
+ if (isLog) {
110
+ navigateToLog(msg.target);
111
+ return true;
112
+ }
92
113
  if (msg.target.isMate && msg.target.mateId && msg.target.homeSessionId) {
93
114
  showHomeHub();
94
115
  openHomeConversation(msg.target.mateId, msg.target.homeSessionId);
@@ -129,6 +150,11 @@ export function closeCommandPalette() {
129
150
  export function setPaletteVersion(version) { cachedVersion = version; }
130
151
 
131
152
  export function handlePaletteSessionSwitch() {
153
+ if (pendingLogNav && ctx.currentSlug && ctx.currentSlug() === pendingLogNav.projectSlug) {
154
+ var logNav = pendingLogNav;
155
+ pendingLogNav = null;
156
+ openProjectLog(logNav.ref);
157
+ }
132
158
  if (!pendingNav) return;
133
159
  var nav = pendingNav;
134
160
  pendingNav = null;
@@ -138,6 +164,17 @@ export function handlePaletteSessionSwitch() {
138
164
  }
139
165
  }
140
166
 
167
+ function navigateToLog(target) {
168
+ closeCommandPalette();
169
+ minimizeHomeHub();
170
+ if (ctx.currentSlug && ctx.currentSlug() === target.projectSlug) {
171
+ openProjectLog(target.ref);
172
+ } else {
173
+ pendingLogNav = { projectSlug: target.projectSlug, ref: target.ref };
174
+ ctx.switchProject(target.projectSlug);
175
+ }
176
+ }
177
+
141
178
  function buildDOM() {
142
179
  paletteEl = document.createElement("div");
143
180
  paletteEl.className = "cmd-palette hidden";
@@ -2,6 +2,7 @@ import { copyToClipboard, escapeHtml } from './utils.js';
2
2
  import { refreshIcons } from './icons.js';
3
3
  import { getMermaidThemeVars } from './theme.js';
4
4
  import { store } from './store.js';
5
+ import { enhanceClayLogLinks } from './clay-log-links.js';
5
6
 
6
7
  // Initialize markdown parser
7
8
  marked.use({ gfm: true, breaks: false });
@@ -89,6 +90,7 @@ export function enhanceClaySessionLinks(root) {
89
90
  fragment.appendChild(document.createTextNode(text.slice(offset)));
90
91
  nodes[i].parentNode.replaceChild(fragment, nodes[i]);
91
92
  }
93
+ enhanceClayLogLinks(root);
92
94
  }
93
95
 
94
96
  /**
@@ -336,6 +336,13 @@ export function openProjectLogs() {
336
336
  requestList();
337
337
  }
338
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
+
339
346
  export function closeProjectLogs() {
340
347
  if (!store.get('projectLogsOpen')) return;
341
348
  if (panel) {
@@ -84,6 +84,8 @@ export function renderDesktopDriverHierarchy(root, renderSession, rerender, matc
84
84
  var key = "driver:" + root.driver.id;
85
85
  var isExpanded = expanded("desktop", key, root.workers, matchIds);
86
86
  var childrenId = "session-workers-" + root.driver.id;
87
+ var header = document.createElement("div");
88
+ header.className = "session-driver-header";
87
89
  var row = renderSession(root.driver);
88
90
  row.classList.add("session-driver-item");
89
91
  var control = document.createElement("button");
@@ -93,20 +95,26 @@ export function renderDesktopDriverHierarchy(root, renderSession, rerender, matc
93
95
  control.setAttribute("aria-controls", childrenId);
94
96
  control.setAttribute("aria-label", (isExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
95
97
  control.innerHTML = iconHtml("chevron-right");
96
- control.addEventListener("click", function (event) {
97
- event.preventDefault();
98
- event.stopPropagation();
99
- toggle("desktop", key, isExpanded, rerender);
100
- });
101
- row.insertBefore(control, row.firstChild);
102
- wrapper.appendChild(row);
103
-
104
98
  var children = document.createElement("div");
105
99
  children.id = childrenId;
106
100
  children.className = "session-worker-children";
107
101
  children.setAttribute("role", "group");
108
102
  children.setAttribute("aria-label", "Workers for " + (root.driver.title || "New Session"));
109
103
  children.hidden = !isExpanded;
104
+ control.addEventListener("click", function (event) {
105
+ event.preventDefault();
106
+ event.stopPropagation();
107
+ var nextExpanded = !isExpanded;
108
+ expansionBySurface.desktop.set(key, nextExpanded);
109
+ control.setAttribute("aria-expanded", String(nextExpanded));
110
+ control.setAttribute("aria-label", (nextExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
111
+ children.hidden = !nextExpanded;
112
+ isExpanded = nextExpanded;
113
+ });
114
+ header.appendChild(control);
115
+ header.appendChild(row);
116
+ wrapper.appendChild(header);
117
+
110
118
  var current = currentWorkerIds();
111
119
  for (var i = 0; i < root.workers.length; i++) {
112
120
  if (!visibleWorker(root.workers[i], root.driver, matchIds)) continue;
@@ -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 and Mate -> the same user-scoped handlers as project sockets
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,8 @@ 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;
42
+ var onAppMessage = opts.onAppMessage;
40
43
 
41
44
  function handleMessage(ws, msg) {
42
45
  if (!msg || typeof msg !== "object") return;
@@ -46,6 +49,14 @@ function attachGlobalWs(opts) {
46
49
  return;
47
50
  }
48
51
 
52
+ // A user does not need an ordinary project before Home becomes useful.
53
+ // Route Mate discovery, Home chat/debates, and tools through the same
54
+ // handlers used by project sockets. This also lets mate_list repair an
55
+ // existing account whose built-ins were missing from its final storage.
56
+ if (typeof onAppMessage === "function" && onAppMessage(ws, msg)) return;
57
+
58
+ if (typeof onHomePreferenceMessage === "function" && onHomePreferenceMessage(ws, msg)) return;
59
+
49
60
  // --- Directory picker for the add-project modal ---
50
61
  if (msg.type === "browse_dir") {
51
62
  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 };