clay-server 5.2.0-beta.1 → 5.2.0-beta.2

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.
@@ -301,7 +301,7 @@ function attachConnection(ctx) {
301
301
  }
302
302
  sendTo(ws, { type: "history_meta", total: total, from: fromIndex });
303
303
  for (var i = fromIndex; i < total; i++) {
304
- sendTo(ws, hydrateImageRefs(active.history[i]));
304
+ sendTo(ws, Object.assign({}, hydrateImageRefs(active.history[i]), { _historyIndex: i }));
305
305
  }
306
306
  var _lastUsage = null, _lastModelUsage = null, _lastCost = null, _lastStreamInputTokens = null;
307
307
  for (var _ri = total - 1; _ri >= 0; _ri--) {
@@ -588,7 +588,9 @@ function attachSessions(ctx) {
588
588
  var targetFrom = typeof msg.target === "number" ? msg.target : before - sm.HISTORY_PAGE_SIZE;
589
589
  var from = sm.findTurnBoundary(session.history, Math.max(0, targetFrom));
590
590
  var to = before;
591
- var items = session.history.slice(from, to).map(hydrateImageRefs);
591
+ var items = session.history.slice(from, to).map(function (item, index) {
592
+ return Object.assign({}, hydrateImageRefs(item), { _historyIndex: from + index });
593
+ });
592
594
  sendTo(ws, {
593
595
  type: "history_prepend",
594
596
  items: items,
package/lib/public/app.js CHANGED
@@ -315,6 +315,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
315
315
  historyTotal: 0,
316
316
  replayingHistory: false,
317
317
  historyReplayBatch: null,
318
+ pendingSessionEntryNavigation: null,
319
+ sessionNavigationRequestId: null,
318
320
  projectName: projectName,
319
321
  cwd: "",
320
322
  currentSlug: currentSlug,
@@ -550,41 +550,20 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
550
550
  .clayos-issue-link { --record-color: var(--link, var(--accent)); }
551
551
  .clayos-log-link { --record-color: var(--accent); }
552
552
 
553
- .clayos-session-link {
554
- display: inline-flex;
555
- align-items: center;
556
- gap: 6px;
557
- min-height: 26px;
558
- margin: 2px 1px;
559
- padding: 3px 8px 3px 6px;
560
- border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
561
- border-radius: 7px;
562
- background: color-mix(in srgb, var(--accent) 7%, var(--bg-alt));
563
- color: var(--text);
564
- font: inherit;
565
- font-size: 11px;
566
- line-height: 1;
567
- vertical-align: middle;
568
- cursor: pointer;
553
+ .clayos-session-link { --record-color: var(--accent); }
554
+
555
+ #messages .session-entry-target {
556
+ outline: 2px solid color-mix(in srgb, var(--accent) 72%, transparent);
557
+ outline-offset: 3px;
558
+ border-radius: 8px;
559
+ background: color-mix(in srgb, var(--accent) 7%, transparent);
560
+ animation: sessionEntryTargetFade 1800ms ease-out forwards;
569
561
  }
570
- .clayos-session-link::before {
571
- content: "\2197";
572
- display: grid;
573
- place-items: center;
574
- width: 17px;
575
- height: 17px;
576
- border-radius: 5px;
577
- background: color-mix(in srgb, var(--accent) 14%, transparent);
578
- color: var(--accent);
579
- font-size: 12px;
562
+
563
+ @keyframes sessionEntryTargetFade {
564
+ 0%, 72% { background: color-mix(in srgb, var(--accent) 10%, transparent); }
565
+ 100% { background: transparent; }
580
566
  }
581
- .clayos-session-link > span { font-weight: 620; }
582
- .clayos-session-link > small { color: var(--text-muted); font-size: 10px; }
583
- .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)); }
584
- .clayos-session-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
585
- .clayos-session-link.is-loading { opacity: 0.62; cursor: progress; }
586
- .clayos-session-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
587
- .clayos-session-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
588
567
 
589
568
  .search-clay-pending {
590
569
  align-self: flex-start;
@@ -350,6 +350,7 @@ export function prependOlderHistory(items, meta) {
350
350
 
351
351
  // Save current rendering state
352
352
  var savedMsgEl = store.get('currentMsgEl');
353
+ var savedReplayingHistory = store.get('replayingHistory');
353
354
  var savedActivity = getActivityEl();
354
355
  var savedFullText = store.get('currentFullText');
355
356
  var savedTurnCounter = getTurnCounter();
@@ -359,7 +360,7 @@ export function prependOlderHistory(items, meta) {
359
360
  var savedUsage = JSON.parse(JSON.stringify(getSessionUsage()));
360
361
 
361
362
  // Reset to initial values for clean rendering
362
- store.set({ currentMsgEl: null, currentFullText: "" });
363
+ store.set({ currentMsgEl: null, currentFullText: "", replayingHistory: true });
363
364
  setActivityEl(null);
364
365
  setTurnCounter(0);
365
366
  resetToolState();
@@ -387,7 +388,7 @@ export function prependOlderHistory(items, meta) {
387
388
  setPrependAnchor(null);
388
389
 
389
390
  // Restore saved state
390
- store.set({ currentMsgEl: savedMsgEl, currentFullText: savedFullText });
391
+ store.set({ currentMsgEl: savedMsgEl, currentFullText: savedFullText, replayingHistory: savedReplayingHistory });
391
392
  setActivityEl(savedActivity);
392
393
  setTurnCounter(savedTurnCounter);
393
394
  restoreToolState(savedToolsState);
@@ -40,7 +40,7 @@ import { handleHomeMateMemoryState, handleHomeMateKnowledgeState, syncHomeMateSe
40
40
  import { renderKnowledgeList, handleKnowledgeContent, handleKnowledgeMutation } from './mate-knowledge.js';
41
41
  import { handleKnowledgeWorkbenchMessage } from './mate-knowledge-workbench.js';
42
42
  import { renderMemoryList } from './mate-memory.js';
43
- import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
43
+ import { handlePaletteSessionSwitch, handlePaletteHistoryDone, handlePaletteHistoryPrepended, setPaletteVersion } from './command-palette.js';
44
44
  import { handleFindInSessionResults } from './session-search.js';
45
45
  import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
46
46
  import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
@@ -224,6 +224,7 @@ export function processMessage(msg) {
224
224
  }
225
225
  }
226
226
 
227
+ store.set({ currentHistoryIndex: Number.isSafeInteger(msg._historyIndex) && msg._historyIndex >= 0 ? msg._historyIndex : null });
227
228
  switch (msg.type) {
228
229
  case "message_ack":
229
230
  var acknowledgedEntry = acknowledgeMessage(msg.clientMessageId);
@@ -250,6 +251,7 @@ export function processMessage(msg) {
250
251
 
251
252
  case "history_prepend":
252
253
  prependOlderHistory(msg.items, msg.meta);
254
+ handlePaletteHistoryPrepended();
253
255
  break;
254
256
 
255
257
  case "history_done":
@@ -311,11 +313,16 @@ export function processMessage(msg) {
311
313
  // re-pins on every height change while armed. Disarms early on
312
314
  // any real user scroll input.
313
315
  // Skip arming when we have a pending in-conversation navigate
314
- // target (file-edit deeplink) — the navigate block below scrolls
315
- // that element into view, and sticky-bottom would fight it.
316
+ // target (file-edit or Session-entry deeplink) — the navigate block
317
+ // below scrolls that element into view, and sticky-bottom would
318
+ // fight it.
316
319
  var nav = getPendingNavigate();
317
320
  var hasNavTarget = nav && (nav.toolId || nav.assistantUuid);
318
- if (hasNavTarget) {
321
+ var sessionEntryNav = store.get('pendingSessionEntryNavigation');
322
+ var hasSessionEntryNav = sessionEntryNav
323
+ && String(sessionEntryNav.projectSlug) === String(store.get('currentSlug'))
324
+ && String(sessionEntryNav.sessionId) === String(store.get('activeSessionId'));
325
+ if (hasNavTarget || hasSessionEntryNav) {
319
326
  // Navigate block below will scrollIntoView on the target — don't
320
327
  // arm sticky-bottom or it would fight that scroll.
321
328
  scrollToBottom();
@@ -343,6 +350,7 @@ export function processMessage(msg) {
343
350
  }
344
351
  });
345
352
  }
353
+ requestAnimationFrame(function () { handlePaletteHistoryDone(); });
346
354
  break;
347
355
 
348
356
  case "restore_mate_dm":
@@ -8,10 +8,10 @@ import { refreshIcons } from './icons.js';
8
8
  import { closeToolGroup } from './tools.js';
9
9
  import { sendMessage, hasSendableContent } from './input.js';
10
10
  import { getChatLayout } from './theme.js';
11
- import { addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
11
+ import { addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
12
12
  import { resolveProjectAssistantIdentity, applyProjectAssistantIdentity } from './project-chat-identity.js';
13
13
  export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
14
- export { initRendering, addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, getPrependAnchor, setPrependAnchor, getActivityEl, setActivityEl, getIsUserScrolledUp, setIsUserScrolledUp, getStickyBottom, armStickyBottom, disarmStickyBottom, VENDOR_AVATARS, VENDOR_NAMES, VENDOR_HOMEPAGES } from './chat-render-runtime.js';
14
+ export { initRendering, addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, getPrependAnchor, setPrependAnchor, getActivityEl, setActivityEl, getIsUserScrolledUp, setIsUserScrolledUp, getStickyBottom, armStickyBottom, disarmStickyBottom, VENDOR_AVATARS, VENDOR_NAMES, VENDOR_HOMEPAGES } from './chat-render-runtime.js';
15
15
  export { addUserMessage, addSystemMessage, addConflictMessage, addContextOverflowMessage } from './app-message-cards.js';
16
16
 
17
17
  // --- Module-owned state (not in store) ---
@@ -44,7 +44,8 @@ export function addCopyHandler(msgEl, rawText) {
44
44
  }
45
45
 
46
46
  export function appendDelta(text) {
47
- ensureAssistantBlock();
47
+ var assistantEl = ensureAssistantBlock();
48
+ if (store.get('replayingHistory')) addHistoryIndex(assistantEl, store.get('currentHistoryIndex'));
48
49
  streamBuffer += text;
49
50
  if (store.get('replayingHistory')) {
50
51
  store.set({ currentFullText: store.get('currentFullText') + streamBuffer });
@@ -155,11 +155,19 @@ export function setIsUserScrolledUp(value) { isUserScrolledUp = value; }
155
155
 
156
156
  export function addToMessages(element) {
157
157
  var messagesEl = getMessagesEl();
158
+ addHistoryIndex(element, store.get('currentHistoryIndex'));
158
159
  if (prependAnchor) messagesEl.insertBefore(element, prependAnchor);
159
160
  else messagesEl.appendChild(element);
160
161
  keepActiveThinkingAtTail();
161
162
  }
162
163
 
164
+ export function addHistoryIndex(element, historyIndex) {
165
+ if (!element || !Number.isSafeInteger(historyIndex) || historyIndex < 0) return;
166
+ var existing = element.dataset.historyIndexes || "";
167
+ if (existing.split(",").indexOf(String(historyIndex)) !== -1) return;
168
+ element.dataset.historyIndexes = existing ? existing + "," + historyIndex : String(historyIndex);
169
+ }
170
+
163
171
  // Thinking is a single accumulating record for the active turn. Other live
164
172
  // transcript entries may arrive between reasoning segments, so keep only the
165
173
  // currently active record after the tail activity indicator.
@@ -1,10 +1,33 @@
1
1
  import { enhanceClayIssueLinks, isExactClayIssueReference } from './clay-issue-links.js';
2
2
  import { enhanceGithubLinks } from './github-links.js';
3
+ import { escapeHtml } from './utils.js';
3
4
 
4
5
  export function renderClayRecordLink(href, text) {
6
+ if (isExactClaySessionReference(href)) return renderClaySessionLink(href.trim(), text);
5
7
  if (!isExactClayLogReference(href) && !isExactClayIssueReference(href)) return null;
6
8
  return '<a href="#" data-clay-record-ref="' + href.trim() + '">' + text + '</a>';
7
9
  }
10
+
11
+ export function isExactClaySessionReference(text) {
12
+ return parseClaySessionReference(text) !== null;
13
+ }
14
+
15
+ export function renderClaySessionLink(ref, text) {
16
+ var parsed = parseClaySessionReference(ref);
17
+ if (!parsed) return null;
18
+ var label = text ? String(text).trim() : "";
19
+ var safeLabel = escapeHtml(label);
20
+ var entryAttr = parsed.entryIndex === null ? "" : ' data-session-entry-index="' + parsed.entryIndex + '"';
21
+ return '<button type="button" class="clayos-session-link clayos-record-link" data-session-ref="' + parsed.ref + '"' + entryAttr + ' aria-label="Open session' + (safeLabel ? " from " + safeLabel : "") + '"><span>Session</span>' + (safeLabel ? '<small>' + safeLabel + '</small>' : '') + '</button>';
22
+ }
23
+
24
+ export function parseClaySessionReference(text) {
25
+ var match = /^((?:session:[A-Za-z0-9_-]{24}))(?:#entry-([0-9]+))?$/.exec((text || "").trim());
26
+ if (!match) return null;
27
+ var entryIndex = match[2] === undefined ? null : Number(match[2]);
28
+ if (entryIndex !== null && (!Number.isSafeInteger(entryIndex) || entryIndex < 0)) return null;
29
+ return { ref: match[1], entryIndex: entryIndex };
30
+ }
8
31
  export function parseClayLogReferences(text) {
9
32
  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;
10
33
  var results = [];
@@ -5,6 +5,8 @@ import { openSearch as openSessionSearch } from './session-search.js';
5
5
  import { startSearchClayChat, attachSearchClayChat, detachSearchClayChat, getSearchClayChatSummary } from './search-clay-chat.js';
6
6
  import { getWs } from './ws-ref.js';
7
7
  import { openProjectLog } from './project-logs.js';
8
+ import { store } from './store.js';
9
+ import { disarmStickyBottom } from './chat-render-runtime.js';
8
10
 
9
11
  function formatRelativeDate(ts) {
10
12
  if (!ts) return "";
@@ -70,6 +72,10 @@ function handleClaySessionLinkClick(event) {
70
72
  var requestId = "clayos-" + (isLog ? "log-" : "session-") + Date.now() + "-" + Math.random().toString(36).slice(2, 9);
71
73
  if (isLog) link.dataset.logRequest = requestId;
72
74
  else link.dataset.sessionRequest = requestId;
75
+ if (!isLog) store.set({ sessionNavigationRequestId: requestId, sessionNavigationRequestContext: {
76
+ projectSlug: store.get('currentSlug'),
77
+ sessionId: store.get('activeSessionId')
78
+ } });
73
79
  link.classList.remove("is-error");
74
80
  link.classList.add("is-loading");
75
81
  var request = { requestId: requestId, surface: link.closest(".search-clay-chat") ? "search" : "home" };
@@ -79,6 +85,7 @@ function handleClaySessionLinkClick(event) {
79
85
  } else {
80
86
  request.type = "home_clay_session_resolve";
81
87
  request.sessionRef = link.dataset.sessionRef;
88
+ if (link.dataset.sessionEntryIndex !== undefined) request.entryIndex = Number(link.dataset.sessionEntryIndex);
82
89
  }
83
90
  ws.send(JSON.stringify(request));
84
91
  }
@@ -91,12 +98,31 @@ export function handleClaySessionTarget(msg) {
91
98
  for (var i = 0; i < links.length; i++) {
92
99
  if ((isLog ? links[i].dataset.logRequest : links[i].dataset.sessionRequest) === msg.requestId) link = links[i];
93
100
  }
101
+ if (!isLog && store.get('sessionNavigationRequestId') !== msg.requestId) {
102
+ if (link) {
103
+ link.classList.remove("is-loading");
104
+ delete link.dataset.sessionRequest;
105
+ }
106
+ return true;
107
+ }
108
+ if (!isLog) {
109
+ var requestContext = store.get('sessionNavigationRequestContext');
110
+ if (requestContext && (String(store.get('currentSlug')) !== String(requestContext.projectSlug) || String(store.get('activeSessionId')) !== String(requestContext.sessionId))) {
111
+ if (link) {
112
+ link.classList.remove("is-loading");
113
+ delete link.dataset.sessionRequest;
114
+ }
115
+ store.set({ pendingSessionEntryNavigation: null, sessionNavigationRequestContext: null });
116
+ return true;
117
+ }
118
+ }
94
119
  if (link) {
95
120
  link.classList.remove("is-loading");
96
121
  if (isLog) delete link.dataset.logRequest;
97
122
  else delete link.dataset.sessionRequest;
98
123
  }
99
124
  if (msg.status !== "ready" || !msg.target) {
125
+ if (!isLog) store.set({ pendingSessionEntryNavigation: null });
100
126
  if (link) {
101
127
  link.classList.add("is-error");
102
128
  link.title = msg.error || "Item not available";
@@ -108,7 +134,7 @@ export function handleClaySessionTarget(msg) {
108
134
  navigateToLog(msg.target);
109
135
  return true;
110
136
  }
111
- navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null);
137
+ navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null, msg.target.entryIndex);
112
138
  return true;
113
139
  }
114
140
 
@@ -151,9 +177,61 @@ export function handlePaletteSessionSwitch() {
151
177
  var nav = pendingNav;
152
178
  pendingNav = null;
153
179
  if (ctx.currentSlug && ctx.currentSlug() === nav.slug) {
154
- ctx.selectSession(nav.sessionId);
155
- if (nav.query) setTimeout(function () { openSessionSearch(nav.query); }, 400);
180
+ navigateToSession({ projectSlug: nav.slug, sessionId: nav.sessionId }, nav.query, nav.entryIndex);
181
+ }
182
+ }
183
+
184
+ export function handlePaletteHistoryDone() {
185
+ var pending = store.get('pendingSessionEntryNavigation');
186
+ if (!pending) return;
187
+ if (!isActiveEntryNavigation(pending)) {
188
+ store.set({ pendingSessionEntryNavigation: null });
189
+ return;
190
+ }
191
+ var historyFrom = store.get('historyFrom') || 0;
192
+ if (pending.entryIndex < historyFrom) {
193
+ var ws = getWs();
194
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom, target: pending.entryIndex }));
195
+ return;
156
196
  }
197
+ jumpToSessionEntry(pending.entryIndex);
198
+ }
199
+
200
+ export function handlePaletteHistoryPrepended() {
201
+ var pending = store.get('pendingSessionEntryNavigation');
202
+ if (!pending) return;
203
+ if (!isActiveEntryNavigation(pending)) {
204
+ store.set({ pendingSessionEntryNavigation: null });
205
+ return;
206
+ }
207
+ requestAnimationFrame(function () {
208
+ if (store.get('pendingSessionEntryNavigation') === pending) jumpToSessionEntry(pending.entryIndex, pending);
209
+ });
210
+ }
211
+
212
+ function isActiveEntryNavigation(pending) {
213
+ return String(store.get('currentSlug') || (ctx && ctx.currentSlug ? ctx.currentSlug() : "")) === String(pending.projectSlug) && String(store.get('activeSessionId')) === String(pending.sessionId);
214
+ }
215
+
216
+ function jumpToSessionEntry(entryIndex, pending) {
217
+ if (pending && !isActiveEntryNavigation(pending)) return;
218
+ var messages = document.querySelectorAll("#messages [data-history-index], #messages [data-history-indexes]");
219
+ var target = null;
220
+ for (var i = 0; i < messages.length; i++) {
221
+ var indexes = (messages[i].dataset.historyIndexes || messages[i].dataset.historyIndex || "").split(",");
222
+ if (indexes.indexOf(String(entryIndex)) !== -1) { target = messages[i]; break; }
223
+ }
224
+ store.set({ pendingSessionEntryNavigation: null });
225
+ if (!target) return;
226
+ disarmStickyBottom();
227
+ var oldTargets = document.querySelectorAll("#messages .session-entry-target");
228
+ for (var oldIndex = 0; oldIndex < oldTargets.length; oldIndex++) oldTargets[oldIndex].classList.remove("session-entry-target");
229
+ var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
230
+ target.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "center" });
231
+ target.classList.remove("session-entry-target");
232
+ void target.offsetWidth;
233
+ target.classList.add("session-entry-target");
234
+ setTimeout(function () { target.classList.remove("session-entry-target"); }, reducedMotion ? 700 : 1800);
157
235
  }
158
236
 
159
237
  function navigateToLog(target) {
@@ -357,13 +435,24 @@ function activateItem(entry) {
357
435
  if (entry.type === "mate-session" || entry.type === "session") navigateToSession(entry.data, entry.query);
358
436
  }
359
437
 
360
- function navigateToSession(item, query) {
438
+ function navigateToSession(item, query, entryIndex) {
361
439
  closeCommandPalette();
440
+ pendingNav = null;
441
+ var hasEntry = entryIndex !== null && entryIndex !== undefined && Number.isSafeInteger(Number(entryIndex)) && Number(entryIndex) >= 0;
442
+ store.set({ pendingSessionEntryNavigation: hasEntry ? { projectSlug: item.projectSlug, sessionId: item.sessionId, entryIndex: Number(entryIndex) } : null });
362
443
  if (ctx.currentSlug && ctx.currentSlug() === item.projectSlug) {
444
+ var alreadyActive = String(store.get('activeSessionId')) === String(item.sessionId);
445
+ if (alreadyActive) {
446
+ if (query) setTimeout(function () { openSessionSearch(query); }, 400);
447
+ if (hasEntry) requestAnimationFrame(function () {
448
+ if (!store.get('replayingHistory')) handlePaletteHistoryDone();
449
+ });
450
+ return;
451
+ }
363
452
  ctx.selectSession(item.sessionId);
364
453
  if (query) setTimeout(function () { openSessionSearch(query); }, 400);
365
454
  } else {
366
- pendingNav = { slug: item.projectSlug, sessionId: item.sessionId, query: query };
455
+ pendingNav = { slug: item.projectSlug, sessionId: item.sessionId, query: query, entryIndex: hasEntry ? Number(entryIndex) : null };
367
456
  ctx.switchProject(item.projectSlug);
368
457
  }
369
458
  }
@@ -84,7 +84,7 @@ export function enhanceClaySessionLinks(root) {
84
84
  fragment.appendChild(document.createTextNode(text.slice(offset, match.index)));
85
85
  var button = document.createElement("button");
86
86
  button.type = "button";
87
- button.className = "clayos-session-link";
87
+ button.className = "clayos-session-link clayos-record-link";
88
88
  button.dataset.sessionRef = match[1];
89
89
  button.setAttribute("aria-label", "Open session" + (match[2] ? " from " + match[2].trim() : ""));
90
90
  var label = document.createElement("span");
@@ -418,7 +418,7 @@ function attachHomeChat(deps) {
418
418
  return sessionReferenceValue;
419
419
  }
420
420
 
421
- function handleMessage(ws, msg) {
421
+ function handleMessage(ws, msg, routedProjectSlug) {
422
422
  if (!msg || typeof msg.type !== "string") return false;
423
423
  if (msg.type === "home_mate_stop" || msg.type === "home_mate_permission_response") {
424
424
  var controlUserId = ws._clayUser ? ws._clayUser.id : null;
@@ -477,8 +477,8 @@ function attachHomeChat(deps) {
477
477
  if (msg.type === "issue_reference_resolve") { resolveIssueLink(ws, msg); return true; }
478
478
  if (homeDebates.handle(ws, userId, msg)) return true;
479
479
  if (msg.type === "home_clay_ask") { homeClayEntry.start(ws, userId, msg); return true; }
480
- if (msg.type === "home_clay_session_resolve") { homeClaySessionLinks.resolve(ws, msg); return true; }
481
- if (msg.type === "home_clay_log_resolve") { homeClaySessionLinks.resolveLog(ws, msg); return true; }
480
+ if (msg.type === "home_clay_session_resolve") { homeClaySessionLinks.resolve(ws, msg, routedProjectSlug); return true; }
481
+ if (msg.type === "home_clay_log_resolve") { homeClaySessionLinks.resolveLog(ws, msg, routedProjectSlug); return true; }
482
482
 
483
483
  if (msg.type === "home_mate_debate_plan") {
484
484
  homeDebate.start(ws, userId, msg);
@@ -2,23 +2,31 @@
2
2
  // owner-bound source session.
3
3
 
4
4
  function attachHomeClaySessionLinks(ctx) {
5
- function source(ws, msg) {
5
+ function source(ws, msg, routedProjectSlug) {
6
6
  var tap = msg.surface === "search" ? ws._searchClayTap : ws._homeChatTap;
7
+ if (!tap && routedProjectSlug) {
8
+ var routedProject = ctx.projects.get(routedProjectSlug);
9
+ var routedManager = routedProject && routedProject.getSessionManager ? routedProject.getSessionManager() : null;
10
+ var routedSession = routedManager && ws._clayActiveSession ? routedManager.sessions.get(ws._clayActiveSession) : null;
11
+ return { tap: { mateSlug: routedProjectSlug, sessionId: ws._clayActiveSession }, project: routedProject, session: routedSession };
12
+ }
7
13
  var sourceProject = tap && tap.mateSlug ? ctx.projects.get(tap.mateSlug) : null;
8
14
  var sourceManager = sourceProject && sourceProject.getSessionManager ? sourceProject.getSessionManager() : null;
9
15
  var sourceSession = sourceManager && tap ? sourceManager.sessions.get(tap.sessionId) : null;
10
16
  return { tap: tap, project: sourceProject, session: sourceSession };
11
17
  }
12
18
 
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 })
17
- : null;
19
+ function resolve(ws, msg, routedProjectSlug) {
20
+ var selected = source(ws, msg, routedProjectSlug);
18
21
  var payload = { type: "home_clay_session_target", requestId: msg.requestId || null, sessionRef: msg.sessionRef || null, status: "error" };
19
22
  try {
23
+ var binding = selected.session ? { projectSlug: selected.tap.mateSlug, session: selected.session } : null;
24
+ if (binding && ws && ws._clayUser) binding.viewerId = ws._clayUser.id;
25
+ var bound = ctx.workspaceQueryService && binding
26
+ ? ctx.workspaceQueryService.bindProjectSession(binding)
27
+ : null;
20
28
  if (!bound) throw new Error("The source conversation is no longer available.");
21
- payload.target = bound.resolveSessionNavigation({ sessionRef: msg.sessionRef });
29
+ payload.target = bound.resolveSessionNavigation({ sessionRef: msg.sessionRef, entryIndex: msg.entryIndex });
22
30
  payload.status = "ready";
23
31
  } catch (error) {
24
32
  payload.error = error.message || "Session not available.";
@@ -26,8 +34,8 @@ function attachHomeClaySessionLinks(ctx) {
26
34
  ctx.sendMessage(ws, payload);
27
35
  }
28
36
 
29
- function resolveLog(ws, msg) {
30
- var selected = source(ws, msg);
37
+ function resolveLog(ws, msg, routedProjectSlug) {
38
+ var selected = source(ws, msg, routedProjectSlug);
31
39
  var status = selected.project && selected.project.getStatus ? selected.project.getStatus() : null;
32
40
  var bound = ctx.projectLogsService && selected.session && status
33
41
  ? ctx.projectLogsService.bindMate({
package/lib/server.js CHANGED
@@ -1630,7 +1630,7 @@ function createServer(opts) {
1630
1630
  if (dmHandler.handleMessage(ws, msg)) return true;
1631
1631
  if (mateHandler && mateHandler.handleMessage(ws, msg)) return true;
1632
1632
  if (emailHandler.handleMessage(ws, msg)) return true;
1633
- if (homeChatHandler.handleMessage(ws, msg)) return true;
1633
+ if (homeChatHandler.handleMessage(ws, msg, routedProjectSlug)) return true;
1634
1634
  if (homePreferencesHandler.handleMessage(ws, msg)) return true;
1635
1635
  if (toolsHandler.handleMessage(ws, msg)) return true;
1636
1636
  return false;
package/lib/sessions.js CHANGED
@@ -864,7 +864,9 @@ function createSessionManager(opts) {
864
864
  if (_item && (_item.type === "mention_user" || _item.type === "mention_response")) {
865
865
  console.log("[DEBUG replayHistory] sending mention at index=" + i + " from=" + fromIndex + " total=" + total + " type=" + _item.type + " mate=" + (_item.mateName || ""));
866
866
  }
867
- _send(transform ? transform(_item) : _item);
867
+ var replayItem = transform ? transform(_item) : _item;
868
+ if (replayItem && typeof replayItem === "object") replayItem = Object.assign({}, replayItem, { _historyIndex: i });
869
+ _send(replayItem);
868
870
  }
869
871
 
870
872
  // Find the last result message in the full history for accurate context data
@@ -168,6 +168,13 @@ function attachWorkspaceQueryService(ctx) {
168
168
  } else if (source.session.ownerId) {
169
169
  return null;
170
170
  }
171
+ if (source.viewerId !== undefined) {
172
+ var viewerPrincipal = {
173
+ userId: isMultiUser() ? source.viewerId : null,
174
+ singleUserOwnerId: isMultiUser() ? null : source.viewerId,
175
+ };
176
+ if (!ownsSession(viewerPrincipal, source.session)) return null;
177
+ }
171
178
  return createBound({
172
179
  userId: isMultiUser() ? source.session.ownerId : null,
173
180
  sourceProjectSlug: source.projectSlug,
@@ -366,11 +373,13 @@ function attachWorkspaceQueryService(ctx) {
366
373
 
367
374
  function resolveSessionNavigation(principal, ref) {
368
375
  if (typeof ref !== "string" || !/^session:[A-Za-z0-9_-]{24}$/.test(ref)) throw new Error("Invalid session reference.");
376
+ var entryIndex = arguments.length > 2 ? arguments[2] : null;
377
+ if (entryIndex !== null && entryIndex !== undefined && (!Number.isSafeInteger(entryIndex) || entryIndex < 0)) throw new Error("Invalid session entry reference.");
369
378
  var projects = collect(principal);
370
379
  for (var p = 0; p < projects.length; p++) {
371
380
  var session = findSession(projects[p], ref);
372
381
  if (!session) continue;
373
- return {
382
+ var target = {
374
383
  projectSlug: projects[p].projectSlug,
375
384
  projectTitle: projects[p].projectTitle,
376
385
  isMate: projects[p].isMate,
@@ -379,6 +388,8 @@ function attachWorkspaceQueryService(ctx) {
379
388
  homeSessionId: session.cliSessionId || "local:" + session.localId,
380
389
  title: cleanMetadata(session.title || "New Session", 160),
381
390
  };
391
+ if (entryIndex !== null && entryIndex !== undefined) target.entryIndex = entryIndex;
392
+ return target;
382
393
  }
383
394
  throw new Error("Session not found in the owned workspace.");
384
395
  }
@@ -450,7 +461,7 @@ function attachWorkspaceQueryService(ctx) {
450
461
  if (!principal.isClay) throw new Error("Workspace-wide activity is available only to builtin Clay.");
451
462
  return workspaceActivity(principal, args || {});
452
463
  },
453
- resolveSessionNavigation: function (args) { return resolveSessionNavigation(principal, args && args.sessionRef); },
464
+ resolveSessionNavigation: function (args) { return resolveSessionNavigation(principal, args && args.sessionRef, args && args.entryIndex); },
454
465
  listRecentDecisions: function (args) {
455
466
  if (!principal.isClay) throw new Error("Workspace decisions are available only to builtin Clay.");
456
467
  return recentDecisions(principal, args || {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "5.2.0-beta.1",
3
+ "version": "5.2.0-beta.2",
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"