Cache session replay on client

This commit is contained in:
wuyang6
2026-06-12 17:28:15 +08:00
parent ddf06c727d
commit 00a83ba4c3
4 changed files with 286 additions and 56 deletions
@@ -29,6 +29,12 @@ import {
writeActiveSessionId,
writePendingWorkspaceSessionId,
} from "@/lib/claw-active-session";
import {
fetchSessionReplaySnapshot,
invalidateCachedSessionReplay,
readCachedSessionReplay,
type SessionReplaySnapshot,
} from "@/lib/claw-session-cache";
import { useClawSessionReplay } from "@/lib/claw-session-replay";
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";
@@ -113,6 +119,27 @@ type ClawStoredToolCall = {
};
};
type ReplaySessionFn = (
sessionId: string,
repository: ExportedMessageRepository,
) => void;
type ClawReplaySnapshot = SessionReplaySnapshot<
ClawStoredSession,
ClawRunStatus
>;
function replaySessionSnapshot(
replaySession: ReplaySessionFn,
sessionId: string,
snapshot: ClawReplaySnapshot,
) {
replaySession(
sessionId,
toReplayRepository(snapshot.session, sessionId, snapshot.runStatus),
);
}
export const ThreadList: FC = () => {
return (
<ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex min-h-0 flex-1 flex-col gap-1">
@@ -168,7 +195,7 @@ function ensureSidebarWorkspaceSessionId(): {
}
function workspaceDisplayLabel(entry: JupyterWorkspaceEntry): string {
if (entry.label && entry.label.trim()) return entry.label.trim();
if (entry.label?.trim()) return entry.label.trim();
if (entry.base_url) {
try {
return new URL(entry.base_url).host;
@@ -243,16 +270,15 @@ const JupyterWorkspaceList: FC = () => {
window.dispatchEvent(new Event("claw-sessions-changed"));
window.dispatchEvent(new Event("claw-workspaces-changed"));
} catch (err) {
setError(
err instanceof Error ? err.message : "切换工作区时网络异常",
);
setError(err instanceof Error ? err.message : "切换工作区时网络异常");
} finally {
setBindingId(null);
}
};
const remove = async (entry: JupyterWorkspaceEntry) => {
if (!window.confirm(`移除工作区「${workspaceDisplayLabel(entry)}」?`)) return;
if (!window.confirm(`移除工作区「${workspaceDisplayLabel(entry)}」?`))
return;
try {
const response = await fetch(
`/api/claw/jupyter/workspaces/${encodeURIComponent(entry.id)}`,
@@ -331,6 +357,8 @@ const ClawSessionList: FC = () => {
null,
);
const renameInputRef = useRef<HTMLInputElement | null>(null);
const openRequestRef = useRef(0);
const openAbortRef = useRef<AbortController | null>(null);
useEffect(() => {
setActiveSessionId(readActiveSessionId());
@@ -394,6 +422,51 @@ const ClawSessionList: FC = () => {
setDraftTitle("");
};
const openSession = async (sessionId: string) => {
const requestId = openRequestRef.current + 1;
openRequestRef.current = requestId;
openAbortRef.current?.abort();
const abortController = new AbortController();
openAbortRef.current = abortController;
const cached = readCachedSessionReplay<ClawStoredSession, ClawRunStatus>(
sessionId,
);
writeActiveSessionId(sessionId);
pushSessionUrl(sessionId);
setActiveSessionId(sessionId);
if (cached) {
replaySessionSnapshot(replaySession, sessionId, cached);
setLoadingSessionId(null);
} else {
setLoadingSessionId(sessionId);
}
try {
const snapshot = await fetchSessionReplaySnapshot<
ClawStoredSession,
ClawRunStatus
>(sessionId, { signal: abortController.signal });
if (requestId !== openRequestRef.current) return;
if (readActiveSessionId() !== sessionId) return;
if (!snapshot) return;
if (!cached || snapshot.signature !== cached.signature) {
replaySessionSnapshot(replaySession, sessionId, snapshot);
}
} catch (error) {
if (!(error instanceof DOMException && error.name === "AbortError")) {
console.warn("[session-list] failed to open session", error);
}
} finally {
if (requestId === openRequestRef.current) {
setLoadingSessionId(null);
}
if (openAbortRef.current === abortController) {
openAbortRef.current = null;
}
}
};
return (
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
<div className="flex flex-col gap-1">
@@ -443,31 +516,7 @@ const ClawSessionList: FC = () => {
preview: session.preview,
});
setOpenMenuSessionId(null);
writeActiveSessionId(session.session_id);
pushSessionUrl(session.session_id);
setActiveSessionId(session.session_id);
setLoadingSessionId(session.session_id);
try {
const response = await fetch(
`/api/claw/sessions/${session.session_id}`,
);
if (!response.ok) return;
const payload =
(await response.json()) as ClawStoredSession;
const runStatus = await fetchLatestRunStatus(
session.session_id,
);
replaySession(
session.session_id,
toReplayRepository(
payload,
session.session_id,
runStatus,
),
);
} finally {
setLoadingSessionId(null);
}
await openSession(session.session_id);
}}
>
<span className="truncate">
@@ -525,6 +574,7 @@ const ClawSessionList: FC = () => {
(item) => item.session_id !== session.session_id,
),
);
invalidateCachedSessionReplay(session.session_id);
setOpenMenuSessionId(null);
if (activeSessionId === session.session_id) {
clearActiveSessionId();