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

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.
@@ -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.3",
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"