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
@@ -111,6 +111,7 @@ export function finalizeAssistantBlock() {
111
111
 
112
112
  export function showClaudePreThinking() {
113
113
  if (getChatLayout() !== "channel") return;
114
+ removeMatePreThinking();
114
115
  var vendor = store.get('currentVendor') || "claude";
115
116
  var vendorAvatar = VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude;
116
117
  var vendorName = VENDOR_NAMES[vendor] || VENDOR_NAMES.claude;
@@ -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";
@@ -3,6 +3,7 @@ import { setRewindMode, isRewindMode } from './rewind.js';
3
3
  import { renderPicker as renderContextPicker } from './context-sources.js';
4
4
  import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible, mentionMenuKeydown, setMentionAtIdx, parseMentionFromInput, clearMentionState, stickyReapplyMention, sendMention, sendUserMention, renderMentionUser, renderUserMention, removeMentionChip } from './mention.js';
5
5
  import { store } from './store.js';
6
+ import { sendAcknowledgedMessage } from './message-delivery.js';
6
7
  import { mateAvatarUrl } from './avatar.js';
7
8
  import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
8
9
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
@@ -316,7 +317,11 @@ export function sendMessage() {
316
317
  // Include selected vendor for session binding (server uses on first message)
317
318
  var _selVendor = store.get("currentVendor") || null;
318
319
  if (_selVendor) payload.vendor = _selVendor;
319
- ctx.ws.send(JSON.stringify(payload));
320
+ var clientMessageId = sendAcknowledgedMessage(payload);
321
+ var optimisticUsers = ctx.messagesEl.querySelectorAll(".msg-user");
322
+ if (optimisticUsers.length > 0) {
323
+ optimisticUsers[optimisticUsers.length - 1].dataset.clientMessageId = clientMessageId;
324
+ }
320
325
 
321
326
  // A turn blocked on an unanswered tool-permission request looks like a
322
327
  // black hole: the new message queues silently while the dots keep
@@ -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
  /**
@@ -0,0 +1,97 @@
1
+ // Reliable delivery for ordinary project chat messages within a page lifetime.
2
+
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+
6
+ var ACK_TIMEOUT_MS = 5000;
7
+ var ackTimers = {};
8
+
9
+ function createClientMessageId() {
10
+ if (window.crypto && typeof window.crypto.randomUUID === "function") {
11
+ return "cm-" + window.crypto.randomUUID();
12
+ }
13
+ return "cm-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2);
14
+ }
15
+
16
+ function pendingMessages() {
17
+ return store.get('pendingOutboundMessages') || [];
18
+ }
19
+
20
+ function replacePending(messages) {
21
+ store.set({ pendingOutboundMessages: messages });
22
+ }
23
+
24
+ function clearAckTimer(clientMessageId) {
25
+ if (!ackTimers[clientMessageId]) return;
26
+ clearTimeout(ackTimers[clientMessageId]);
27
+ delete ackTimers[clientMessageId];
28
+ }
29
+
30
+ function isPending(clientMessageId) {
31
+ var pending = pendingMessages();
32
+ for (var i = 0; i < pending.length; i++) {
33
+ if (pending[i].payload.clientMessageId === clientMessageId) return true;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ function armAckTimer(clientMessageId, socket) {
39
+ clearAckTimer(clientMessageId);
40
+ ackTimers[clientMessageId] = setTimeout(function () {
41
+ delete ackTimers[clientMessageId];
42
+ if (!isPending(clientMessageId) || getWs() !== socket) return;
43
+ window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
44
+ detail: { socket: socket, clientMessageId: clientMessageId },
45
+ }));
46
+ }, ACK_TIMEOUT_MS);
47
+ }
48
+
49
+ function transmit(entry) {
50
+ var socket = getWs();
51
+ if (!socket || socket.readyState !== 1) return false;
52
+ try {
53
+ socket.send(JSON.stringify(entry.payload));
54
+ armAckTimer(entry.payload.clientMessageId, socket);
55
+ return true;
56
+ } catch (e) {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ export function sendAcknowledgedMessage(payload) {
62
+ var nextPayload = Object.assign({}, payload, { clientMessageId: createClientMessageId() });
63
+ var entry = {
64
+ projectSlug: store.get('currentSlug'),
65
+ sessionId: store.get('activeSessionId'),
66
+ payload: nextPayload,
67
+ };
68
+ replacePending(pendingMessages().concat([entry]));
69
+ if (!transmit(entry)) {
70
+ window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
71
+ detail: { socket: getWs(), clientMessageId: nextPayload.clientMessageId },
72
+ }));
73
+ }
74
+ return nextPayload.clientMessageId;
75
+ }
76
+
77
+ export function acknowledgeMessage(clientMessageId) {
78
+ if (typeof clientMessageId !== "string") return false;
79
+ var pending = pendingMessages();
80
+ var next = pending.filter(function (entry) {
81
+ return entry.payload.clientMessageId !== clientMessageId;
82
+ });
83
+ if (next.length === pending.length) return false;
84
+ clearAckTimer(clientMessageId);
85
+ replacePending(next);
86
+ return true;
87
+ }
88
+
89
+ export function replayPendingMessages(sessionId) {
90
+ var pending = pendingMessages();
91
+ var projectSlug = store.get('currentSlug');
92
+ for (var i = 0; i < pending.length; i++) {
93
+ if (pending[i].projectSlug === projectSlug && pending[i].sessionId === sessionId) {
94
+ transmit(pending[i]);
95
+ }
96
+ }
97
+ }
@@ -106,6 +106,34 @@ export function renderFilter(container, categories, selected, onChange) {
106
106
  container.appendChild(select);
107
107
  }
108
108
 
109
+ export function renderContextFilter(container, selected, isWorktree, onChange) {
110
+ container.innerHTML = "";
111
+ var select = document.createElement("select");
112
+ select.id = "project-logs-context";
113
+ select.className = "project-logs-filter project-logs-context-filter";
114
+ select.setAttribute("aria-label", "Filter by change scope");
115
+ var options = isWorktree
116
+ ? [["current", "Current worktree"], ["project", "Project"], ["all", "All changes"]]
117
+ : [["project", "Project"], ["all", "All changes"]];
118
+ for (var i = 0; i < options.length; i++) {
119
+ var option = document.createElement("option");
120
+ option.value = options[i][0];
121
+ option.textContent = options[i][1];
122
+ select.appendChild(option);
123
+ }
124
+ select.value = selected || (isWorktree ? "current" : "project");
125
+ select.addEventListener("change", function () { onChange(select.value); });
126
+ container.appendChild(select);
127
+ }
128
+
129
+ function contextLabel(context) {
130
+ if (!context || !context.changeSetId) return "";
131
+ var label = context.branch || "Worktree change";
132
+ if (context.status === "merged") return label + " · Merged";
133
+ if (context.status === "archived") return label + " · Archived";
134
+ return label + " · Active";
135
+ }
136
+
109
137
  // One ledger row. Reveals what happened without opening the record: category,
110
138
  // priority, title, summary, author, time, revision and comment counts.
111
139
  function renderRow(entry, onOpen) {
@@ -152,6 +180,8 @@ function renderRow(entry, onOpen) {
152
180
  if (entry.commentCount > 0) {
153
181
  parts.push(entry.commentCount + (entry.commentCount === 1 ? " comment" : " comments"));
154
182
  }
183
+ var context = contextLabel(entry.context);
184
+ if (context) parts.push(context);
155
185
  meta.textContent = parts.filter(Boolean).join(" · ");
156
186
  row.appendChild(meta);
157
187
 
@@ -289,8 +319,8 @@ function historyVerb(revision) {
289
319
  return "Revised";
290
320
  }
291
321
 
292
- // The entry, full pane. People read here and may only comment: there is no
293
- // edit affordance because canonical authorship belongs to agent sessions.
322
+ // The entry, full pane. People may comment and the project owner may delete;
323
+ // there is no edit affordance because canonical authorship belongs to agents.
294
324
  export function renderDetail(detailEl, entry, handlers) {
295
325
  detailEl.innerHTML = "";
296
326
  if (!entry) return;
@@ -317,6 +347,14 @@ export function renderDetail(detailEl, entry, handlers) {
317
347
  flag.textContent = PRIORITY_LABELS[entry.priority] || entry.priority;
318
348
  chips.appendChild(flag);
319
349
  }
350
+ var context = contextLabel(entry.context);
351
+ if (context) {
352
+ var contextChip = document.createElement("span");
353
+ contextChip.className = "project-log-context-chip";
354
+ contextChip.dataset.status = entry.context.status || "active";
355
+ contextChip.textContent = context;
356
+ chips.appendChild(contextChip);
357
+ }
320
358
  setText(header, "h1", entry.title || "Untitled log");
321
359
  setText(header, ".project-log-doc-summary", entry.summary || "");
322
360
  var byline = [
@@ -325,6 +363,14 @@ export function renderDetail(detailEl, entry, handlers) {
325
363
  formatTime(entry.updatedAt || entry.createdAt),
326
364
  ].filter(Boolean).join(" · ");
327
365
  setText(header, ".project-log-byline", byline);
366
+ if (handlers && handlers.canDelete) {
367
+ var deleteButton = document.createElement("button");
368
+ deleteButton.type = "button";
369
+ deleteButton.className = "project-log-delete-button";
370
+ deleteButton.innerHTML = '<i data-lucide="trash-2"></i><span>Delete log</span>';
371
+ deleteButton.addEventListener("click", function () { handlers.onDelete(entry); });
372
+ header.appendChild(deleteButton);
373
+ }
328
374
  article.appendChild(header);
329
375
 
330
376
  var markdown = document.createElement("div");