diff --git a/frontend/app/app/assistant.tsx b/frontend/app/app/assistant.tsx index a5addcd..c94e2c0 100644 --- a/frontend/app/app/assistant.tsx +++ b/frontend/app/app/assistant.tsx @@ -19,6 +19,10 @@ import { SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; +import { + readActiveSessionId, + writeActiveSessionId, +} from "@/lib/claw-active-session"; import { ClawSessionReplayProvider } from "@/lib/claw-session-replay"; import { ClawAccountGate, useClawAccount } from "./claw-account-gate"; @@ -31,21 +35,13 @@ export const Assistant = () => { prepareSendMessagesRequest: async (options) => { const body = options.body as Record; const messages = options.messages as UIMessage[]; - const selectedSessionId = - typeof window !== "undefined" - ? window.localStorage.getItem("claw.activeSessionId") - : null; + const selectedSessionId = readActiveSessionId(); const lastSessionId = getLastSessionId(messages); const selectedResumeSessionId = messages.length > 1 ? selectedSessionId : null; const outgoingSessionId = lastSessionId ?? selectedResumeSessionId ?? options.id; - if (typeof window !== "undefined" && outgoingSessionId) { - window.localStorage.setItem( - "claw.activeSessionId", - outgoingSessionId, - ); - } + writeActiveSessionId(outgoingSessionId); return { body: { @@ -65,7 +61,7 @@ export const Assistant = () => { }); const replaySession = useCallback( (sessionId: string, repository: ExportedMessageRepository) => { - window.localStorage.setItem("claw.activeSessionId", sessionId); + writeActiveSessionId(sessionId); runtime.thread.import(repository); }, [runtime], diff --git a/frontend/app/app/claw-account-gate.tsx b/frontend/app/app/claw-account-gate.tsx index cc281cc..502c68d 100644 --- a/frontend/app/app/claw-account-gate.tsx +++ b/frontend/app/app/claw-account-gate.tsx @@ -5,6 +5,7 @@ import type { ReactNode } from "react"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { clearActiveSessionId } from "@/lib/claw-active-session"; export type ClawAccount = { id: string; @@ -131,7 +132,7 @@ export function ClawAccountBar({ }) { async function logout() { await fetch("/api/claw/auth/logout", { method: "POST" }); - window.localStorage.removeItem("claw.activeSessionId"); + clearActiveSessionId(); onLogout(); } diff --git a/frontend/app/components/assistant-ui/activity-panel.tsx b/frontend/app/components/assistant-ui/activity-panel.tsx index 1c065b2..2da6c7d 100644 --- a/frontend/app/components/assistant-ui/activity-panel.tsx +++ b/frontend/app/components/assistant-ui/activity-panel.tsx @@ -37,6 +37,10 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; +import { + readActiveSessionId, + writeActiveSessionId, +} from "@/lib/claw-active-session"; import { cn } from "@/lib/utils"; type ActivityContextValue = { @@ -71,12 +75,7 @@ export function ActivityProvider({ children }: { children: ReactNode }) { }, []); const openFiles = useCallback((nextSessionId?: string | null) => { setMode("files"); - setSessionId( - nextSessionId ?? - (typeof window !== "undefined" - ? window.localStorage.getItem("claw.activeSessionId") - : null), - ); + setSessionId(nextSessionId ?? readActiveSessionId()); setFilesRefreshToken((value) => value + 1); setOpen(true); }, []); @@ -257,7 +256,7 @@ function SessionFilesPanel({ let cancelled = false; async function loadFiles() { const activeSessionId = normalizeSessionId( - sessionId ?? window.localStorage.getItem("claw.activeSessionId"), + sessionId ?? readActiveSessionId(), ); setStatus("正在读取聊天中的文件..."); try { @@ -285,10 +284,7 @@ function SessionFilesPanel({ return; } if (nextPayload.session_id) { - window.localStorage.setItem( - "claw.activeSessionId", - nextPayload.session_id, - ); + writeActiveSessionId(nextPayload.session_id); } setPayload(nextPayload); setStatus(""); diff --git a/frontend/app/components/assistant-ui/thread-list.tsx b/frontend/app/components/assistant-ui/thread-list.tsx index 20b6677..80ce7b2 100644 --- a/frontend/app/components/assistant-ui/thread-list.tsx +++ b/frontend/app/components/assistant-ui/thread-list.tsx @@ -19,6 +19,11 @@ import { useState, } from "react"; import { Button } from "@/components/ui/button"; +import { + clearActiveSessionId, + readActiveSessionId, + writeActiveSessionId, +} from "@/lib/claw-active-session"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; type ClawSession = { @@ -106,7 +111,7 @@ const ThreadListNew: FC = () => { return (
{ - window.localStorage.removeItem("claw.activeSessionId"); + clearActiveSessionId(); window.dispatchEvent(new Event("claw-active-session-cleared")); }} > @@ -141,7 +146,7 @@ const ClawSessionList: FC = () => { const renameInputRef = useRef(null); useEffect(() => { - setActiveSessionId(window.localStorage.getItem("claw.activeSessionId")); + setActiveSessionId(readActiveSessionId()); const refreshSessions = () => { fetch("/api/claw/sessions") .then((res) => (res.ok ? res.json() : [])) @@ -247,10 +252,7 @@ const ClawSessionList: FC = () => { className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center truncate px-3 text-start text-sm" onClick={async () => { setOpenMenuSessionId(null); - window.localStorage.setItem( - "claw.activeSessionId", - session.session_id, - ); + writeActiveSessionId(session.session_id); setActiveSessionId(session.session_id); setLoadingSessionId(session.session_id); try { @@ -333,9 +335,7 @@ const ClawSessionList: FC = () => { ); setOpenMenuSessionId(null); if (activeSessionId === session.session_id) { - window.localStorage.removeItem( - "claw.activeSessionId", - ); + clearActiveSessionId(); setActiveSessionId(null); window.dispatchEvent( new Event("claw-active-session-cleared"), diff --git a/frontend/app/components/assistant-ui/thread.tsx b/frontend/app/components/assistant-ui/thread.tsx index f91115e..7f2e4c6 100644 --- a/frontend/app/components/assistant-ui/thread.tsx +++ b/frontend/app/components/assistant-ui/thread.tsx @@ -62,6 +62,11 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + clearActiveSessionId, + readActiveSessionId, + writeActiveSessionId, +} from "@/lib/claw-active-session"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; import { cn } from "@/lib/utils"; @@ -193,6 +198,15 @@ function useRefreshReplayedRun() { const runStatus = await fetchLatestRunStatus(activeSessionId); if (cancelled) return; if (runStatus?.status === "queued" || runStatus?.status === "running") { + const response = await fetch(`/api/claw/sessions/${activeSessionId}`, { + cache: "no-store", + }); + if (!response.ok || cancelled) return; + const payload = await response.json(); + replaySession( + activeSessionId, + toReplayRepository(payload, activeSessionId, runStatus), + ); return; } const response = await fetch(`/api/claw/sessions/${activeSessionId}`, { @@ -243,7 +257,7 @@ function useReplayedRunState() { }); const localSessionId = typeof window !== "undefined" - ? normalizeSessionId(window.localStorage.getItem("claw.activeSessionId")) + ? normalizeSessionId(readActiveSessionId()) : null; const sessionId = replaySessionId ?? latestSessionId ?? localSessionId; return useMemo( @@ -722,7 +736,7 @@ function useComposerContextStatus() { useEffect(() => { const cleanLatestSessionId = normalizeSessionId(latestSessionId); if (cleanLatestSessionId) { - window.localStorage.setItem("claw.activeSessionId", cleanLatestSessionId); + writeActiveSessionId(cleanLatestSessionId); window.setTimeout(() => { window.dispatchEvent(new Event("claw-sessions-changed")); }, 0); @@ -752,13 +766,10 @@ function useComposerContextStatus() { : {}; let sessionId = normalizeSessionId( normalizeSessionId(latestSessionId) || - normalizeSessionId( - window.localStorage.getItem("claw.activeSessionId"), - ) || + normalizeSessionId(readActiveSessionId()) || normalizeSessionId(statePayload.active_session_id), ); - if (sessionId) - window.localStorage.setItem("claw.activeSessionId", sessionId); + if (sessionId) writeActiveSessionId(sessionId); let sessionPayload: ClawStoredSession | null = null; let contextBudget: ContextBudget | undefined; if (sessionId) { @@ -772,7 +783,7 @@ function useComposerContextStatus() { sessionPayload = (await sessionResponse.json()) as ClawStoredSession; } else if (sessionResponse.status === 404) { - window.localStorage.removeItem("claw.activeSessionId"); + clearActiveSessionId(); sessionId = null; } } diff --git a/frontend/app/components/assistant-ui/threadlist-sidebar.tsx b/frontend/app/components/assistant-ui/threadlist-sidebar.tsx index b0b8277..65f5d42 100644 --- a/frontend/app/components/assistant-ui/threadlist-sidebar.tsx +++ b/frontend/app/components/assistant-ui/threadlist-sidebar.tsx @@ -43,6 +43,11 @@ import { SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; +import { + clearActiveSessionId, + readActiveSessionId, + writeActiveSessionId, +} from "@/lib/claw-active-session"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; type ThreadListSidebarProps = React.ComponentProps & { @@ -157,7 +162,7 @@ function CollapsedSidebarRail({
{ - window.localStorage.removeItem("claw.activeSessionId"); + clearActiveSessionId(); window.dispatchEvent(new Event("claw-active-session-cleared")); }} > @@ -360,7 +365,7 @@ function useSidebarSessions(open: boolean) { ) { const cleanSessionId = normalizeSessionId(sessionId); if (!cleanSessionId) return; - window.localStorage.setItem("claw.activeSessionId", cleanSessionId); + writeActiveSessionId(cleanSessionId); setLoadingSessionId(sessionId); try { const response = await fetch(`/api/claw/sessions/${cleanSessionId}`, { @@ -438,14 +443,13 @@ function AccountMenu({ setState(nextState); setSessions(nextSessions); const activeSessionId = normalizeSessionId( - window.localStorage.getItem("claw.activeSessionId") ?? - nextState?.active_session_id, + readActiveSessionId() ?? nextState?.active_session_id, ); if (!activeSessionId) { setActiveSession(null); return; } - window.localStorage.setItem("claw.activeSessionId", activeSessionId); + writeActiveSessionId(activeSessionId); const response = await fetch(`/api/claw/sessions/${activeSessionId}`, { cache: "no-store", }); @@ -460,7 +464,7 @@ function AccountMenu({ async function logout() { await fetch("/api/claw/auth/logout", { method: "POST" }); - window.localStorage.removeItem("claw.activeSessionId"); + clearActiveSessionId(); onLogout(); } diff --git a/frontend/app/lib/claw-active-session.ts b/frontend/app/lib/claw-active-session.ts new file mode 100644 index 0000000..0995a71 --- /dev/null +++ b/frontend/app/lib/claw-active-session.ts @@ -0,0 +1,34 @@ +"use client"; + +const ACTIVE_SESSION_ID_KEY = "claw.activeSessionId"; + +export function readActiveSessionId() { + if (typeof window === "undefined") return null; + return ( + normalizeSessionId(window.sessionStorage.getItem(ACTIVE_SESSION_ID_KEY)) ?? + normalizeSessionId(window.localStorage.getItem(ACTIVE_SESSION_ID_KEY)) + ); +} + +export function writeActiveSessionId(sessionId: string | null | undefined) { + if (typeof window === "undefined") return; + const normalized = normalizeSessionId(sessionId); + if (!normalized) { + clearActiveSessionId(); + return; + } + window.sessionStorage.setItem(ACTIVE_SESSION_ID_KEY, normalized); + window.localStorage.setItem(ACTIVE_SESSION_ID_KEY, normalized); +} + +export function clearActiveSessionId() { + if (typeof window === "undefined") return; + window.sessionStorage.removeItem(ACTIVE_SESSION_ID_KEY); + window.localStorage.removeItem(ACTIVE_SESSION_ID_KEY); +} + +function normalizeSessionId(value: unknown) { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed || null; +}