clay-server 4.0.0-beta.17 → 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.
@@ -22,6 +22,11 @@ var LOGS_CONTRACT =
22
22
  "Do not paste raw conversation transcripts, log command-by-command narration, trivial confirmations, or speculation. Repository history may show the code change but usually does not preserve the user's intent, constraints, verification, or unfinished state, so it is not a substitute for the work log. " +
23
23
  "Every write is attributed and permanently revision-tracked, so keep entries concise, concrete, and true while retaining enough context for a clean Driver handoff.";
24
24
 
25
+ var CLAY_READ_CONTRACT =
26
+ "When the user asks Clay about a project's prior work, decisions, defects, status, rationale, or unfinished work, identify the relevant project and search its Project Logs before answering. " +
27
+ "Use conversation history as additional evidence when useful, but do not substitute it for the ledger's durable project record. Do not search Logs for unrelated general questions. " +
28
+ "When citing a returned log in user-visible text, write [clayos/<ref> — short label] so Clay Studio renders an owner-validated Log control.";
29
+
25
30
  // User learning moments are a durable project asset, so capturing them is a
26
31
  // default rather than an option. This category is about a change in the user's
27
32
  // conceptual model, never knowledge the Driver acquired while doing the work.
@@ -236,7 +241,7 @@ function globalTools(bound) {
236
241
  return [
237
242
  {
238
243
  name: "list_project_logs",
239
- description: LOGS_CONTRACT + " List logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
244
+ description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " List logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
240
245
  inputSchema: buildShape({
241
246
  projectSlug: { type: "string", description: "Exact project slug." },
242
247
  kind: { type: "string", description: "Optional category filter, matched exactly against this project's vocabulary. The response lists the categories currently in use." },
@@ -250,7 +255,7 @@ function globalTools(bound) {
250
255
  },
251
256
  {
252
257
  name: "search_project_logs",
253
- description: LOGS_CONTRACT + " Search logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
258
+ description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " Search logs for one project the current user is authorized to see. Read-only, and available only to authoritative builtin Clay.",
254
259
  inputSchema: buildShape({
255
260
  projectSlug: { type: "string", description: "Exact project slug." },
256
261
  query: { type: "string", description: "Search query." },
@@ -264,7 +269,7 @@ function globalTools(bound) {
264
269
  },
265
270
  {
266
271
  name: "read_project_log_revision",
267
- description: LOGS_CONTRACT + " Read one entry as of a given revision. Read-only, and available only to authoritative builtin Clay.",
272
+ description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " Read one entry as of a given revision. Read-only, and available only to authoritative builtin Clay.",
268
273
  inputSchema: buildShape({
269
274
  projectSlug: { type: "string", description: "Exact project slug." },
270
275
  ref: { type: "string", description: REF_DESCRIPTION },
@@ -274,7 +279,7 @@ function globalTools(bound) {
274
279
  },
275
280
  {
276
281
  name: "read_project_log",
277
- description: LOGS_CONTRACT + " Read one log entry from an authorized project, including its summary and any comments people have added. Read-only, and available only to authoritative builtin Clay.",
282
+ description: LOGS_CONTRACT + " " + CLAY_READ_CONTRACT + " Read one log entry from an authorized project, including its summary and any comments people have added. Read-only, and available only to authoritative builtin Clay.",
278
283
  inputSchema: buildShape({
279
284
  projectSlug: { type: "string", description: "Exact project slug." },
280
285
  ref: { type: "string", description: REF_DESCRIPTION },
@@ -312,6 +317,7 @@ function createMcpServer(adapter, bound, includeGlobal) {
312
317
 
313
318
  module.exports = {
314
319
  LOGS_CONTRACT: LOGS_CONTRACT,
320
+ CLAY_READ_CONTRACT: CLAY_READ_CONTRACT,
315
321
  LEARNING_CONTRACT: LEARNING_CONTRACT,
316
322
  ATTENTION_CONTRACT: ATTENTION_CONTRACT,
317
323
  REVIEW_CONTRACT: REVIEW_CONTRACT,
@@ -21,6 +21,7 @@
21
21
 
22
22
  var builtinMates = require("./builtin-mates");
23
23
  var logsStore = require("./project-logs-store");
24
+ var LOG_REF_PATTERN = /^log:[A-Za-z0-9_-]{24}$/;
24
25
 
25
26
  function readOnly() {
26
27
  throw new Error("Project Logs are read-only for builtin Clay.");
@@ -396,6 +397,33 @@ function attachProjectLogsService(ctx) {
396
397
  return storeForStatus(statusFor(principal, slug));
397
398
  }
398
399
 
400
+ function resolveLogNavigation(args) {
401
+ var principal = revalidate();
402
+ var ref = args && args.ref;
403
+ if (!LOG_REF_PATTERN.test(ref || "")) throw new Error("Invalid log reference.");
404
+ var projects = getProjects();
405
+ var seenScopes = new Set();
406
+ var match = null;
407
+ if (!projects || typeof projects.forEach !== "function") throw new Error("Project Logs are unavailable.");
408
+ projects.forEach(function (project, slug) {
409
+ if (!project || typeof project.getStatus !== "function") return;
410
+ var status = project.getStatus();
411
+ if (!authorizedProject(principal, status)) return;
412
+ var governing = effectiveStatus(status);
413
+ var scope = governing && (governing.projectKnowledgeId || governing.path);
414
+ if (!scope || seenScopes.has(scope)) return;
415
+ seenScopes.add(scope);
416
+ var entry = null;
417
+ try { entry = storeForStatus(status).read(ref, false); } catch (e) { return; }
418
+ if (!entry) return;
419
+ var targetSlug = status.isWorktree && status.parentSlug && projectFor(status.parentSlug) ? status.parentSlug : slug;
420
+ if (match && match.projectSlug !== targetSlug) throw new Error("Log reference is ambiguous.");
421
+ match = { projectSlug: targetSlug, ref: ref };
422
+ });
423
+ if (!match) throw new Error("Log entry not found.");
424
+ return match;
425
+ }
426
+
399
427
  return {
400
428
  isClay: true,
401
429
  canWrite: false,
@@ -413,6 +441,7 @@ function attachProjectLogsService(ctx) {
413
441
  logHistory: function (args) { return target(args).history(args && args.ref, args || {}); },
414
442
  commentLog: readOnly,
415
443
  readLogRevision: function (args) { return target(args).readRevision(args && args.ref, args && args.revision); },
444
+ resolveLogNavigation: resolveLogNavigation,
416
445
  listLogFeedback: readOnly,
417
446
  reviewLogComment: readOnly,
418
447
  revertLog: readOnly,
@@ -366,7 +366,11 @@ function attachProjectLogs(ctx) {
366
366
  }
367
367
 
368
368
  function getSystemPrompt(session) {
369
- if (isMate || !service) return "";
369
+ if (!service) return "";
370
+ if (isMate) {
371
+ if (!includeGlobal() || (session && !sessionBinding(session))) return "";
372
+ return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.CLAY_READ_CONTRACT;
373
+ }
370
374
  if (session && !sessionBinding(session)) return "";
371
375
  return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.LOGS_CONTRACT +
372
376
  "\n" + logsMcp.LEARNING_CONTRACT +
package/lib/project.js CHANGED
@@ -1319,7 +1319,7 @@ function createProjectContext(opts) {
1319
1319
  if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg);
1320
1320
  return;
1321
1321
  }
1322
- if (msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve") {
1322
+ if (msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve" || msg.type === "home_clay_log_resolve") {
1323
1323
  if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
1324
1324
  return;
1325
1325
  }
@@ -486,7 +486,8 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
486
486
  .search-clay-activity-item small { color: var(--text-dimmer); font-size: 10px; line-height: 1.35; }
487
487
  @keyframes searchClaySpin { to { transform: rotate(360deg); } }
488
488
 
489
- .clayos-session-link {
489
+ .clayos-session-link,
490
+ .clayos-log-link {
490
491
  display: inline-flex;
491
492
  align-items: center;
492
493
  gap: 6px;
@@ -503,7 +504,8 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
503
504
  vertical-align: middle;
504
505
  cursor: pointer;
505
506
  }
506
- .clayos-session-link::before {
507
+ .clayos-session-link::before,
508
+ .clayos-log-link::before {
507
509
  content: "\2197";
508
510
  display: grid;
509
511
  place-items: center;
@@ -514,13 +516,21 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
514
516
  color: var(--accent);
515
517
  font-size: 12px;
516
518
  }
517
- .clayos-session-link > span { font-weight: 620; }
518
- .clayos-session-link > small { color: var(--text-muted); font-size: 10px; }
519
- .clayos-session-link:hover { border-color: color-mix(in srgb, var(--accent) 52%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--bg-alt)); }
520
- .clayos-session-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
521
- .clayos-session-link.is-loading { opacity: 0.62; cursor: progress; }
522
- .clayos-session-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
523
- .clayos-session-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
519
+ .clayos-log-link::before { content: "\2261"; }
520
+ .clayos-session-link > span,
521
+ .clayos-log-link > span { font-weight: 620; }
522
+ .clayos-session-link > small,
523
+ .clayos-log-link > small { color: var(--text-muted); font-size: 10px; }
524
+ .clayos-session-link:hover,
525
+ .clayos-log-link:hover { border-color: color-mix(in srgb, var(--accent) 52%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--bg-alt)); }
526
+ .clayos-session-link:focus-visible,
527
+ .clayos-log-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
528
+ .clayos-session-link.is-loading,
529
+ .clayos-log-link.is-loading { opacity: 0.62; cursor: progress; }
530
+ .clayos-session-link.is-loading::before,
531
+ .clayos-log-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
532
+ .clayos-session-link.is-error,
533
+ .clayos-log-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
524
534
 
525
535
  .search-clay-pending {
526
536
  align-self: flex-start;
@@ -85,9 +85,13 @@
85
85
  min-height: 80px;
86
86
  border: 1px solid color-mix(in srgb, var(--note-border) calc(var(--note-opacity, 0.64) * 100%), transparent);
87
87
  background: color-mix(in srgb, var(--note-surface) calc(var(--note-opacity, 0.64) * 100%), transparent);
88
+ -webkit-backdrop-filter: blur(7px) saturate(0.92) contrast(1.02);
89
+ backdrop-filter: blur(7px) saturate(0.92) contrast(1.02);
88
90
  color: var(--note-ink);
89
91
  transition: box-shadow 0.15s;
90
92
  overflow: hidden;
93
+ contain: layout style;
94
+ will-change: transform, backdrop-filter;
91
95
  }
92
96
 
93
97
  .sticky-note:hover {
@@ -798,8 +802,11 @@
798
802
 
799
803
  @media (prefers-reduced-transparency: reduce) {
800
804
  .sticky-note {
805
+ -webkit-backdrop-filter: none;
806
+ backdrop-filter: none;
801
807
  border-color: var(--note-border);
802
808
  background: var(--note-surface);
809
+ will-change: transform;
803
810
  }
804
811
  }
805
812
 
@@ -31,7 +31,7 @@
31
31
  (function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
32
32
  </script>
33
33
  <script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
34
- <link rel="stylesheet" href="style.css?v=20260901-clay-chat6">
34
+ <link rel="stylesheet" href="style.css?v=20260908-log-links2">
35
35
  <style>
36
36
  @media(max-width:768px){
37
37
  /* User messages: vertical stack, avatar on top, right-aligned */
@@ -2337,7 +2337,7 @@
2337
2337
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2338
2338
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2339
2339
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2340
- <script type="module" src="app.js?v=20260902-markdown-boundary1"></script>
2340
+ <script type="module" src="app.js?v=20260908-log-links2"></script>
2341
2341
  <div id="pwa-install-modal" class="pwa-modal hidden">
2342
2342
  <div class="pwa-modal-backdrop"></div>
2343
2343
  <div class="pwa-modal-card">
@@ -2,15 +2,15 @@
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { showToast, copyToClipboard, escapeHtml } from './utils.js';
4
4
 
5
- function showConfirmDialog(message, onConfirm) {
5
+ function showConfirmDialog(message, actionLabel, onConfirm) {
6
6
  var modal = document.createElement("div");
7
7
  modal.className = "admin-modal-overlay";
8
- var html = '<div class="admin-modal">' +
8
+ var html = '<div class="admin-modal" role="dialog" aria-modal="true" aria-label="Confirm action">' +
9
9
  '<div class="admin-modal-body" style="padding:20px 16px 16px">' +
10
10
  '<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">' + escapeHtml(message) + '</p>' +
11
11
  '</div>' +
12
12
  '<div class="admin-modal-footer">' +
13
- '<button class="admin-modal-save admin-modal-confirm-danger">Revoke</button>' +
13
+ '<button class="admin-modal-save admin-modal-confirm-danger">' + escapeHtml(actionLabel) + '</button>' +
14
14
  '<button class="admin-modal-cancel">Cancel</button>' +
15
15
  '</div></div>';
16
16
  modal.innerHTML = html;
@@ -198,9 +198,9 @@ function renderUsersTab(body) {
198
198
  var userId = this.dataset.userId;
199
199
  var user = cachedUsers.find(function (u) { return u.id === userId; });
200
200
  var name = user ? (user.displayName || user.username) : "this user";
201
- if (confirm("Remove " + name + "? This cannot be undone.")) {
201
+ showConfirmDialog("Remove " + name + "? This cannot be undone.", "Remove user", function () {
202
202
  removeUser(userId, body);
203
- }
203
+ });
204
204
  });
205
205
  }
206
206
  }
@@ -509,7 +509,7 @@ function renderInvitesTab(body) {
509
509
  revokeBtns[k].addEventListener("click", function () {
510
510
  var code = this.dataset.code;
511
511
  var btn = this;
512
- showConfirmDialog("Revoke this invite? The link will no longer work.", function () {
512
+ showConfirmDialog("Revoke this invite? The link will no longer work.", "Revoke", function () {
513
513
  btn.disabled = true;
514
514
  fetch("/api/admin/invites/" + encodeURIComponent(code), { method: "DELETE" })
515
515
  .then(function (r) { return r.json(); })
@@ -751,12 +751,12 @@ function renderSmtpTab(body, cfg) {
751
751
  var removeBtn = body.querySelector("#smtp-remove");
752
752
  if (removeBtn) {
753
753
  removeBtn.addEventListener("click", function () {
754
- if (confirm("Remove SMTP configuration? Users will need to use PIN login.")) {
754
+ showConfirmDialog("Remove SMTP configuration? Users will need to use PIN login.", "Remove SMTP", function () {
755
755
  apiPost("/api/admin/smtp", { host: "", user: "", pass: "", from: "" }).then(function () {
756
756
  showToast("SMTP configuration removed");
757
757
  loadSmtpTab(body);
758
758
  });
759
- }
759
+ });
760
760
  });
761
761
  }
762
762
  }
@@ -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) {
@@ -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 };
package/lib/server.js CHANGED
@@ -996,17 +996,21 @@ function createServer(opts) {
996
996
 
997
997
  // --- WebSocket ---
998
998
  var wss = new WebSocketServer({ noServer: true });
999
+ var homePreferencesHandler = null;
999
1000
 
1000
1001
  // Slug-less /ws handler: lets a user with no projects yet load the regular
1001
1002
  // app shell and create/add/clone their first one. Only knows about the
1002
- // small set of bootstrap messages (browse_dir, add_project, create_project,
1003
- // clone_project, ping).
1003
+ // small set of bootstrap messages, including the Home preferences required
1004
+ // to dismiss the workspace restoration screen.
1004
1005
  var globalWs = serverGlobalWs.attachGlobalWs({
1005
1006
  osUsers: osUsers,
1006
1007
  usersModule: users,
1007
1008
  onAddProject: onAddProject,
1008
1009
  onCreateProject: onCreateProject,
1009
1010
  onCloneProject: onCloneProject,
1011
+ onHomePreferenceMessage: function (ws, msg) {
1012
+ return homePreferencesHandler ? homePreferencesHandler.handleMessage(ws, msg) : false;
1013
+ },
1010
1014
  });
1011
1015
 
1012
1016
  server.on("upgrade", function (req, socket, head) {
@@ -1447,9 +1451,10 @@ function createServer(opts) {
1447
1451
  projects: projects,
1448
1452
  addProject: addProject,
1449
1453
  workspaceQueryService: workspaceQueryService,
1454
+ projectLogsService: projectLogsService,
1450
1455
  });
1451
1456
 
1452
- var homePreferencesHandler = serverHomePreferences.attachHomePreferences({
1457
+ homePreferencesHandler = serverHomePreferences.attachHomePreferences({
1453
1458
  users: users,
1454
1459
  projects: projects,
1455
1460
  });
package/lib/ws-schema.js CHANGED
@@ -44,6 +44,8 @@ var schema = {
44
44
  "project_assignment_response": { direction: "c2s", handler: "lib/workspace-assignment-service.js", description: "Approve or cancel an exact-session cross-project assignment proposal" },
45
45
  "home_clay_session_resolve": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Resolve an opaque Mate session reference through the exact bound source session" },
46
46
  "home_clay_session_target": { direction: "s2c", handler: "lib/public/modules/command-palette.js", description: "Return an owner-validated navigation target for a rendered Mate session reference" },
47
+ "home_clay_log_resolve": { direction: "c2s", handler: "lib/server-home-chat.js", description: "Resolve an opaque Project Log reference through the exact bound Clay session" },
48
+ "home_clay_log_target": { direction: "s2c", handler: "lib/public/modules/command-palette.js", description: "Return an owner-validated project target for a rendered Log reference" },
47
49
 
48
50
  "session_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of sessions for the sidebar" },
49
51
  "session_switched": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Confirmation that active session changed" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.0.0-beta.17",
3
+ "version": "4.0.0-beta.18",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",