Cache session replay on client
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Popover as PopoverPrimitive } from "radix-ui";
|
||||
import type * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ClawAccount } from "@/app/claw-account-gate";
|
||||
import { ClawLlmSettings } from "@/app/claw-llm-settings";
|
||||
import { ClawThemeSwitcher } from "@/app/claw-theme-switcher";
|
||||
import {
|
||||
fetchLatestRunStatus,
|
||||
type ClawRunStatus,
|
||||
ThreadList,
|
||||
toReplayRepository,
|
||||
} from "@/components/assistant-ui/thread-list";
|
||||
@@ -49,6 +49,10 @@ import {
|
||||
readActiveSessionId,
|
||||
writeActiveSessionId,
|
||||
} from "@/lib/claw-active-session";
|
||||
import {
|
||||
fetchSessionReplaySnapshot,
|
||||
readCachedSessionReplay,
|
||||
} from "@/lib/claw-session-cache";
|
||||
import { useClawSessionReplay } from "@/lib/claw-session-replay";
|
||||
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";
|
||||
|
||||
@@ -384,6 +388,8 @@ function SessionResultButton({
|
||||
function useSidebarSessions(open: boolean) {
|
||||
const [sessions, setSessions] = useState<SidebarSession[]>([]);
|
||||
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
||||
const openRequestRef = useRef(0);
|
||||
const openAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -417,22 +423,54 @@ function useSidebarSessions(open: boolean) {
|
||||
) {
|
||||
const cleanSessionId = normalizeSessionId(sessionId);
|
||||
if (!cleanSessionId) return;
|
||||
const requestId = openRequestRef.current + 1;
|
||||
openRequestRef.current = requestId;
|
||||
openAbortRef.current?.abort();
|
||||
const abortController = new AbortController();
|
||||
openAbortRef.current = abortController;
|
||||
const cached = readCachedSessionReplay<ReplaySessionPayload, ClawRunStatus>(
|
||||
cleanSessionId,
|
||||
);
|
||||
writeActiveSessionId(cleanSessionId);
|
||||
pushSessionUrl(cleanSessionId);
|
||||
setLoadingSessionId(sessionId);
|
||||
try {
|
||||
const response = await fetch(`/api/claw/sessions/${cleanSessionId}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const payload = (await response.json()) as ReplaySessionPayload;
|
||||
const runStatus = await fetchLatestRunStatus(cleanSessionId);
|
||||
if (cached) {
|
||||
replaySession(
|
||||
cleanSessionId,
|
||||
toReplayRepository(payload, cleanSessionId, runStatus),
|
||||
toReplayRepository(cached.session, cleanSessionId, cached.runStatus),
|
||||
);
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
} else {
|
||||
setLoadingSessionId(sessionId);
|
||||
}
|
||||
try {
|
||||
const snapshot = await fetchSessionReplaySnapshot<
|
||||
ReplaySessionPayload,
|
||||
ClawRunStatus
|
||||
>(cleanSessionId, { signal: abortController.signal });
|
||||
if (requestId !== openRequestRef.current) return;
|
||||
if (readActiveSessionId() !== cleanSessionId) return;
|
||||
if (!snapshot) return;
|
||||
if (!cached || snapshot.signature !== cached.signature) {
|
||||
replaySession(
|
||||
cleanSessionId,
|
||||
toReplayRepository(
|
||||
snapshot.session,
|
||||
cleanSessionId,
|
||||
snapshot.runStatus,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
||||
console.warn("[sidebar] failed to open session", error);
|
||||
}
|
||||
} finally {
|
||||
if (requestId === openRequestRef.current) {
|
||||
setLoadingSessionId(null);
|
||||
}
|
||||
if (openAbortRef.current === abortController) {
|
||||
openAbortRef.current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user