Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/project-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ function attachConnection(ctx) {
}
sendTo(ws, { type: "history_meta", total: total, from: fromIndex });
for (var i = fromIndex; i < total; i++) {
sendTo(ws, hydrateImageRefs(active.history[i]));
sendTo(ws, Object.assign({}, hydrateImageRefs(active.history[i]), { _historyIndex: i }));
}
var _lastUsage = null, _lastModelUsage = null, _lastCost = null, _lastStreamInputTokens = null;
for (var _ri = total - 1; _ri >= 0; _ri--) {
Expand Down
4 changes: 3 additions & 1 deletion lib/project-sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,9 @@ function attachSessions(ctx) {
var targetFrom = typeof msg.target === "number" ? msg.target : before - sm.HISTORY_PAGE_SIZE;
var from = sm.findTurnBoundary(session.history, Math.max(0, targetFrom));
var to = before;
var items = session.history.slice(from, to).map(hydrateImageRefs);
var items = session.history.slice(from, to).map(function (item, index) {
return Object.assign({}, hydrateImageRefs(item), { _historyIndex: from + index });
});
sendTo(ws, {
type: "history_prepend",
items: items,
Expand Down
2 changes: 2 additions & 0 deletions lib/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
historyTotal: 0,
replayingHistory: false,
historyReplayBatch: null,
pendingSessionEntryNavigation: null,
sessionNavigationRequestId: null,
projectName: projectName,
cwd: "",
currentSlug: currentSlug,
Expand Down
45 changes: 12 additions & 33 deletions lib/public/css/command-palette.css
Original file line number Diff line number Diff line change
Expand Up @@ -550,41 +550,20 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
.clayos-issue-link { --record-color: var(--link, var(--accent)); }
.clayos-log-link { --record-color: var(--accent); }

.clayos-session-link {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 26px;
margin: 2px 1px;
padding: 3px 8px 3px 6px;
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
border-radius: 7px;
background: color-mix(in srgb, var(--accent) 7%, var(--bg-alt));
color: var(--text);
font: inherit;
font-size: 11px;
line-height: 1;
vertical-align: middle;
cursor: pointer;
.clayos-session-link { --record-color: var(--accent); }

#messages .session-entry-target {
outline: 2px solid color-mix(in srgb, var(--accent) 72%, transparent);
outline-offset: 3px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent) 7%, transparent);
animation: sessionEntryTargetFade 1800ms ease-out forwards;
}
.clayos-session-link::before {
content: "\2197";
display: grid;
place-items: center;
width: 17px;
height: 17px;
border-radius: 5px;
background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--accent);
font-size: 12px;

@keyframes sessionEntryTargetFade {
0%, 72% { background: color-mix(in srgb, var(--accent) 10%, transparent); }
100% { background: transparent; }
}
.clayos-session-link > span { font-weight: 620; }
.clayos-session-link > small { color: var(--text-muted); font-size: 10px; }
.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)); }
.clayos-session-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.clayos-session-link.is-loading { opacity: 0.62; cursor: progress; }
.clayos-session-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
.clayos-session-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }

.search-clay-pending {
align-self: flex-start;
Expand Down
5 changes: 3 additions & 2 deletions lib/public/modules/app-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ export function prependOlderHistory(items, meta) {

// Save current rendering state
var savedMsgEl = store.get('currentMsgEl');
var savedReplayingHistory = store.get('replayingHistory');
var savedActivity = getActivityEl();
var savedFullText = store.get('currentFullText');
var savedTurnCounter = getTurnCounter();
Expand All @@ -359,7 +360,7 @@ export function prependOlderHistory(items, meta) {
var savedUsage = JSON.parse(JSON.stringify(getSessionUsage()));

// Reset to initial values for clean rendering
store.set({ currentMsgEl: null, currentFullText: "" });
store.set({ currentMsgEl: null, currentFullText: "", replayingHistory: true });
setActivityEl(null);
setTurnCounter(0);
resetToolState();
Expand Down Expand Up @@ -387,7 +388,7 @@ export function prependOlderHistory(items, meta) {
setPrependAnchor(null);

// Restore saved state
store.set({ currentMsgEl: savedMsgEl, currentFullText: savedFullText });
store.set({ currentMsgEl: savedMsgEl, currentFullText: savedFullText, replayingHistory: savedReplayingHistory });
setActivityEl(savedActivity);
setTurnCounter(savedTurnCounter);
restoreToolState(savedToolsState);
Expand Down
16 changes: 12 additions & 4 deletions lib/public/modules/app-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { handleHomeMateMemoryState, handleHomeMateKnowledgeState, syncHomeMateSe
import { renderKnowledgeList, handleKnowledgeContent, handleKnowledgeMutation } from './mate-knowledge.js';
import { handleKnowledgeWorkbenchMessage } from './mate-knowledge-workbench.js';
import { renderMemoryList } from './mate-memory.js';
import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
import { handlePaletteSessionSwitch, handlePaletteHistoryDone, handlePaletteHistoryPrepended, setPaletteVersion } from './command-palette.js';
import { handleFindInSessionResults } from './session-search.js';
import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
Expand Down Expand Up @@ -224,6 +224,7 @@ export function processMessage(msg) {
}
}

store.set({ currentHistoryIndex: Number.isSafeInteger(msg._historyIndex) && msg._historyIndex >= 0 ? msg._historyIndex : null });
switch (msg.type) {
case "message_ack":
var acknowledgedEntry = acknowledgeMessage(msg.clientMessageId);
Expand All @@ -250,6 +251,7 @@ export function processMessage(msg) {

case "history_prepend":
prependOlderHistory(msg.items, msg.meta);
handlePaletteHistoryPrepended();
break;

case "history_done":
Expand Down Expand Up @@ -311,11 +313,16 @@ export function processMessage(msg) {
// re-pins on every height change while armed. Disarms early on
// any real user scroll input.
// Skip arming when we have a pending in-conversation navigate
// target (file-edit deeplink) — the navigate block below scrolls
// that element into view, and sticky-bottom would fight it.
// target (file-edit or Session-entry deeplink) — the navigate block
// below scrolls that element into view, and sticky-bottom would
// fight it.
var nav = getPendingNavigate();
var hasNavTarget = nav && (nav.toolId || nav.assistantUuid);
if (hasNavTarget) {
var sessionEntryNav = store.get('pendingSessionEntryNavigation');
var hasSessionEntryNav = sessionEntryNav
&& String(sessionEntryNav.projectSlug) === String(store.get('currentSlug'))
&& String(sessionEntryNav.sessionId) === String(store.get('activeSessionId'));
if (hasNavTarget || hasSessionEntryNav) {
// Navigate block below will scrollIntoView on the target — don't
// arm sticky-bottom or it would fight that scroll.
scrollToBottom();
Expand Down Expand Up @@ -343,6 +350,7 @@ export function processMessage(msg) {
}
});
}
requestAnimationFrame(function () { handlePaletteHistoryDone(); });
break;

case "restore_mate_dm":
Expand Down
7 changes: 4 additions & 3 deletions lib/public/modules/app-rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { refreshIcons } from './icons.js';
import { closeToolGroup } from './tools.js';
import { sendMessage, hasSendableContent } from './input.js';
import { getChatLayout } from './theme.js';
import { addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
import { addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
import { resolveProjectAssistantIdentity, applyProjectAssistantIdentity } from './project-chat-identity.js';
export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
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';
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';
export { addUserMessage, addSystemMessage, addConflictMessage, addContextOverflowMessage } from './app-message-cards.js';

// --- Module-owned state (not in store) ---
Expand Down Expand Up @@ -44,7 +44,8 @@ export function addCopyHandler(msgEl, rawText) {
}

export function appendDelta(text) {
ensureAssistantBlock();
var assistantEl = ensureAssistantBlock();
if (store.get('replayingHistory')) addHistoryIndex(assistantEl, store.get('currentHistoryIndex'));
streamBuffer += text;
if (store.get('replayingHistory')) {
store.set({ currentFullText: store.get('currentFullText') + streamBuffer });
Expand Down
8 changes: 8 additions & 0 deletions lib/public/modules/chat-render-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,19 @@ export function setIsUserScrolledUp(value) { isUserScrolledUp = value; }

export function addToMessages(element) {
var messagesEl = getMessagesEl();
addHistoryIndex(element, store.get('currentHistoryIndex'));
if (prependAnchor) messagesEl.insertBefore(element, prependAnchor);
else messagesEl.appendChild(element);
keepActiveThinkingAtTail();
}

export function addHistoryIndex(element, historyIndex) {
if (!element || !Number.isSafeInteger(historyIndex) || historyIndex < 0) return;
var existing = element.dataset.historyIndexes || "";
if (existing.split(",").indexOf(String(historyIndex)) !== -1) return;
element.dataset.historyIndexes = existing ? existing + "," + historyIndex : String(historyIndex);
}

// Thinking is a single accumulating record for the active turn. Other live
// transcript entries may arrive between reasoning segments, so keep only the
// currently active record after the tail activity indicator.
Expand Down
23 changes: 23 additions & 0 deletions lib/public/modules/clay-log-links.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { enhanceClayIssueLinks, isExactClayIssueReference } from './clay-issue-links.js';
import { enhanceGithubLinks } from './github-links.js';
import { escapeHtml } from './utils.js';

export function renderClayRecordLink(href, text) {
if (isExactClaySessionReference(href)) return renderClaySessionLink(href.trim(), text);
if (!isExactClayLogReference(href) && !isExactClayIssueReference(href)) return null;
return '<a href="#" data-clay-record-ref="' + href.trim() + '">' + text + '</a>';
}

export function isExactClaySessionReference(text) {
return parseClaySessionReference(text) !== null;
}

export function renderClaySessionLink(ref, text) {
var parsed = parseClaySessionReference(ref);
if (!parsed) return null;
var label = text ? String(text).trim() : "";
var safeLabel = escapeHtml(label);
var entryAttr = parsed.entryIndex === null ? "" : ' data-session-entry-index="' + parsed.entryIndex + '"';
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>';
}

export function parseClaySessionReference(text) {
var match = /^((?:session:[A-Za-z0-9_-]{24}))(?:#entry-([0-9]+))?$/.exec((text || "").trim());
if (!match) return null;
var entryIndex = match[2] === undefined ? null : Number(match[2]);
if (entryIndex !== null && (!Number.isSafeInteger(entryIndex) || entryIndex < 0)) return null;
return { ref: match[1], entryIndex: entryIndex };
}
export function parseClayLogReferences(text) {
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;
var results = [];
Expand Down
99 changes: 94 additions & 5 deletions lib/public/modules/command-palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { openSearch as openSessionSearch } from './session-search.js';
import { startSearchClayChat, attachSearchClayChat, detachSearchClayChat, getSearchClayChatSummary } from './search-clay-chat.js';
import { getWs } from './ws-ref.js';
import { openProjectLog } from './project-logs.js';
import { store } from './store.js';
import { disarmStickyBottom } from './chat-render-runtime.js';

function formatRelativeDate(ts) {
if (!ts) return "";
Expand Down Expand Up @@ -70,6 +72,10 @@ function handleClaySessionLinkClick(event) {
var requestId = "clayos-" + (isLog ? "log-" : "session-") + Date.now() + "-" + Math.random().toString(36).slice(2, 9);
if (isLog) link.dataset.logRequest = requestId;
else link.dataset.sessionRequest = requestId;
if (!isLog) store.set({ sessionNavigationRequestId: requestId, sessionNavigationRequestContext: {
projectSlug: store.get('currentSlug'),
sessionId: store.get('activeSessionId')
} });
link.classList.remove("is-error");
link.classList.add("is-loading");
var request = { requestId: requestId, surface: link.closest(".search-clay-chat") ? "search" : "home" };
Expand All @@ -79,6 +85,7 @@ function handleClaySessionLinkClick(event) {
} else {
request.type = "home_clay_session_resolve";
request.sessionRef = link.dataset.sessionRef;
if (link.dataset.sessionEntryIndex !== undefined) request.entryIndex = Number(link.dataset.sessionEntryIndex);
}
ws.send(JSON.stringify(request));
}
Expand All @@ -91,12 +98,31 @@ export function handleClaySessionTarget(msg) {
for (var i = 0; i < links.length; i++) {
if ((isLog ? links[i].dataset.logRequest : links[i].dataset.sessionRequest) === msg.requestId) link = links[i];
}
if (!isLog && store.get('sessionNavigationRequestId') !== msg.requestId) {
if (link) {
link.classList.remove("is-loading");
delete link.dataset.sessionRequest;
}
return true;
}
if (!isLog) {
var requestContext = store.get('sessionNavigationRequestContext');
if (requestContext && (String(store.get('currentSlug')) !== String(requestContext.projectSlug) || String(store.get('activeSessionId')) !== String(requestContext.sessionId))) {
if (link) {
link.classList.remove("is-loading");
delete link.dataset.sessionRequest;
}
store.set({ pendingSessionEntryNavigation: null, sessionNavigationRequestContext: null });
return true;
}
}
if (link) {
link.classList.remove("is-loading");
if (isLog) delete link.dataset.logRequest;
else delete link.dataset.sessionRequest;
}
if (msg.status !== "ready" || !msg.target) {
if (!isLog) store.set({ pendingSessionEntryNavigation: null });
if (link) {
link.classList.add("is-error");
link.title = msg.error || "Item not available";
Expand All @@ -108,7 +134,7 @@ export function handleClaySessionTarget(msg) {
navigateToLog(msg.target);
return true;
}
navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null);
navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null, msg.target.entryIndex);
return true;
}

Expand Down Expand Up @@ -151,9 +177,61 @@ export function handlePaletteSessionSwitch() {
var nav = pendingNav;
pendingNav = null;
if (ctx.currentSlug && ctx.currentSlug() === nav.slug) {
ctx.selectSession(nav.sessionId);
if (nav.query) setTimeout(function () { openSessionSearch(nav.query); }, 400);
navigateToSession({ projectSlug: nav.slug, sessionId: nav.sessionId }, nav.query, nav.entryIndex);
}
}

export function handlePaletteHistoryDone() {
var pending = store.get('pendingSessionEntryNavigation');
if (!pending) return;
if (!isActiveEntryNavigation(pending)) {
store.set({ pendingSessionEntryNavigation: null });
return;
}
var historyFrom = store.get('historyFrom') || 0;
if (pending.entryIndex < historyFrom) {
var ws = getWs();
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom, target: pending.entryIndex }));
return;
}
jumpToSessionEntry(pending.entryIndex);
}

export function handlePaletteHistoryPrepended() {
var pending = store.get('pendingSessionEntryNavigation');
if (!pending) return;
if (!isActiveEntryNavigation(pending)) {
store.set({ pendingSessionEntryNavigation: null });
return;
}
requestAnimationFrame(function () {
if (store.get('pendingSessionEntryNavigation') === pending) jumpToSessionEntry(pending.entryIndex, pending);
});
}

function isActiveEntryNavigation(pending) {
return String(store.get('currentSlug') || (ctx && ctx.currentSlug ? ctx.currentSlug() : "")) === String(pending.projectSlug) && String(store.get('activeSessionId')) === String(pending.sessionId);
}

function jumpToSessionEntry(entryIndex, pending) {
if (pending && !isActiveEntryNavigation(pending)) return;
var messages = document.querySelectorAll("#messages [data-history-index], #messages [data-history-indexes]");
var target = null;
for (var i = 0; i < messages.length; i++) {
var indexes = (messages[i].dataset.historyIndexes || messages[i].dataset.historyIndex || "").split(",");
if (indexes.indexOf(String(entryIndex)) !== -1) { target = messages[i]; break; }
}
store.set({ pendingSessionEntryNavigation: null });
if (!target) return;
disarmStickyBottom();
var oldTargets = document.querySelectorAll("#messages .session-entry-target");
for (var oldIndex = 0; oldIndex < oldTargets.length; oldIndex++) oldTargets[oldIndex].classList.remove("session-entry-target");
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
target.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "center" });
target.classList.remove("session-entry-target");
void target.offsetWidth;
target.classList.add("session-entry-target");
setTimeout(function () { target.classList.remove("session-entry-target"); }, reducedMotion ? 700 : 1800);
}

function navigateToLog(target) {
Expand Down Expand Up @@ -357,13 +435,24 @@ function activateItem(entry) {
if (entry.type === "mate-session" || entry.type === "session") navigateToSession(entry.data, entry.query);
}

function navigateToSession(item, query) {
function navigateToSession(item, query, entryIndex) {
closeCommandPalette();
pendingNav = null;
var hasEntry = entryIndex !== null && entryIndex !== undefined && Number.isSafeInteger(Number(entryIndex)) && Number(entryIndex) >= 0;
store.set({ pendingSessionEntryNavigation: hasEntry ? { projectSlug: item.projectSlug, sessionId: item.sessionId, entryIndex: Number(entryIndex) } : null });
if (ctx.currentSlug && ctx.currentSlug() === item.projectSlug) {
var alreadyActive = String(store.get('activeSessionId')) === String(item.sessionId);
if (alreadyActive) {
if (query) setTimeout(function () { openSessionSearch(query); }, 400);
if (hasEntry) requestAnimationFrame(function () {
if (!store.get('replayingHistory')) handlePaletteHistoryDone();
});
return;
}
ctx.selectSession(item.sessionId);
if (query) setTimeout(function () { openSessionSearch(query); }, 400);
} else {
pendingNav = { slug: item.projectSlug, sessionId: item.sessionId, query: query };
pendingNav = { slug: item.projectSlug, sessionId: item.sessionId, query: query, entryIndex: hasEntry ? Number(entryIndex) : null };
ctx.switchProject(item.projectSlug);
}
}
Loading
Loading