Restore active run state on session replay
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { getCurrentAccount } from "@/lib/claw-auth";
|
||||
|
||||
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const account = await getCurrentAccount();
|
||||
if (!account)
|
||||
return Response.json({ error: "请先登录账号" }, { status: 401 });
|
||||
|
||||
const incoming = new URL(req.url);
|
||||
const sessionId = incoming.searchParams.get("session_id")?.trim();
|
||||
if (!sessionId)
|
||||
return Response.json({ error: "Missing session_id" }, { status: 400 });
|
||||
|
||||
const url = new URL(`${CLAW_API_URL}/api/runs/latest`);
|
||||
url.searchParams.set("account_id", account.id);
|
||||
url.searchParams.set("session_id", sessionId);
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
const payload = await response.text();
|
||||
return new Response(payload, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"content-type":
|
||||
response.headers.get("content-type") ?? "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -45,6 +45,13 @@ type ClawStoredSession = {
|
||||
messages?: ClawStoredMessage[];
|
||||
};
|
||||
|
||||
export type ClawRunStatus = {
|
||||
run_id?: string;
|
||||
session_id?: string;
|
||||
status?: "idle" | "queued" | "running" | "completed" | "failed" | "cancelled";
|
||||
current_stage?: string;
|
||||
};
|
||||
|
||||
type ClawStoredToolCall = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@@ -222,9 +229,16 @@ const ClawSessionList: FC = () => {
|
||||
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),
|
||||
toReplayRepository(
|
||||
payload,
|
||||
session.session_id,
|
||||
runStatus,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
@@ -323,6 +337,18 @@ function dedupeSessions(payload: unknown[]) {
|
||||
return [...byId.values()].sort((a, b) => b.modified_at - a.modified_at);
|
||||
}
|
||||
|
||||
export async function fetchLatestRunStatus(sessionId: string) {
|
||||
try {
|
||||
const url = new URL("/api/claw/runs/latest", window.location.origin);
|
||||
url.searchParams.set("session_id", sessionId);
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
if (!response.ok) return null;
|
||||
return (await response.json()) as ClawRunStatus;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isClawSession(value: unknown): value is ClawSession {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const item = value as Partial<ClawSession>;
|
||||
@@ -334,6 +360,7 @@ function isClawSession(value: unknown): value is ClawSession {
|
||||
export function toReplayRepository(
|
||||
session: ClawStoredSession,
|
||||
fallbackSessionId: string,
|
||||
runStatus?: ClawRunStatus | null,
|
||||
): ExportedMessageRepository {
|
||||
const messages: ExportedMessageRepository["messages"] = [];
|
||||
const toolResults = collectToolResults(session.messages ?? []);
|
||||
@@ -393,12 +420,49 @@ export function toReplayRepository(
|
||||
messages.push({ message: threadMessage, parentId: previousId });
|
||||
previousId = id;
|
||||
}
|
||||
if (isActiveRunStatus(runStatus)) {
|
||||
const id = `${fallbackSessionId}-run-${runStatus.run_id ?? "active"}`;
|
||||
const text =
|
||||
runStatus.status === "queued"
|
||||
? "当前会话已有任务在执行,本轮正在排队。"
|
||||
: runStatus.current_stage || "后端正在执行这个会话。";
|
||||
const parts = [{ type: "reasoning", text }] as UIMessage["parts"];
|
||||
const uiMessage: UIMessage = {
|
||||
id,
|
||||
role: "assistant",
|
||||
parts,
|
||||
metadata: { sessionId: session.session_id ?? fallbackSessionId },
|
||||
} as UIMessage;
|
||||
const threadMessage = {
|
||||
id,
|
||||
role: "assistant",
|
||||
createdAt: new Date(),
|
||||
content: toThreadMessageContent(parts),
|
||||
status: { type: "running" },
|
||||
metadata: {
|
||||
unstable_state: null,
|
||||
unstable_annotations: [],
|
||||
unstable_data: [],
|
||||
steps: [],
|
||||
custom: { sessionId: session.session_id ?? fallbackSessionId },
|
||||
},
|
||||
} as ThreadMessage;
|
||||
bindExternalStoreMessage(threadMessage, uiMessage);
|
||||
messages.push({ message: threadMessage, parentId: previousId });
|
||||
previousId = id;
|
||||
}
|
||||
return {
|
||||
headId: previousId,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
function isActiveRunStatus(
|
||||
runStatus?: ClawRunStatus | null,
|
||||
): runStatus is ClawRunStatus {
|
||||
return runStatus?.status === "queued" || runStatus?.status === "running";
|
||||
}
|
||||
|
||||
function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) {
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
} from "@/components/assistant-ui/attachment";
|
||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||
import { Reasoning } from "@/components/assistant-ui/reasoning";
|
||||
import {
|
||||
fetchLatestRunStatus,
|
||||
toReplayRepository,
|
||||
} from "@/components/assistant-ui/thread-list";
|
||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -58,6 +62,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useClawSessionReplay } from "@/lib/claw-session-replay";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ComposerInsertEvent = CustomEvent<{ text: string }>;
|
||||
@@ -110,6 +115,7 @@ type ContextBudget = {
|
||||
};
|
||||
|
||||
export const Thread: FC = () => {
|
||||
useRefreshReplayedRun();
|
||||
return (
|
||||
<ThreadPrimitive.Root
|
||||
className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
|
||||
@@ -149,6 +155,52 @@ export const Thread: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function useRefreshReplayedRun() {
|
||||
const { replaySession } = useClawSessionReplay();
|
||||
const isRunning = useAuiState((s) => s.thread.isRunning);
|
||||
const hasReplayedRunMessage = useAuiState((s) =>
|
||||
s.thread.messages.some((message) => message.id.includes("-run-")),
|
||||
);
|
||||
const latestSessionId = useAuiState((s) =>
|
||||
findLatestMessageMetadataValue(s.thread.messages, "sessionId"),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRunning || !hasReplayedRunMessage) return;
|
||||
const sessionId = normalizeSessionId(
|
||||
latestSessionId ?? window.localStorage.getItem("claw.activeSessionId"),
|
||||
);
|
||||
if (!sessionId) return;
|
||||
const activeSessionId = sessionId;
|
||||
let cancelled = false;
|
||||
|
||||
async function refreshIfFinished() {
|
||||
const runStatus = await fetchLatestRunStatus(activeSessionId);
|
||||
if (cancelled) return;
|
||||
if (runStatus?.status === "queued" || runStatus?.status === "running") {
|
||||
return;
|
||||
}
|
||||
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),
|
||||
);
|
||||
window.dispatchEvent(new Event("claw-sessions-changed"));
|
||||
}
|
||||
|
||||
refreshIfFinished();
|
||||
const interval = window.setInterval(refreshIfFinished, 3000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(interval);
|
||||
};
|
||||
}, [hasReplayedRunMessage, isRunning, latestSessionId, replaySession]);
|
||||
}
|
||||
|
||||
const ChatTopActions: FC = () => {
|
||||
const { openFiles } = useActivityPanel();
|
||||
const latestSessionId = useAuiState((s) =>
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { ClawAccount } from "@/app/claw-account-gate";
|
||||
import { ClawLlmSettings } from "@/app/claw-llm-settings";
|
||||
import { ClawThemeSwitcher } from "@/app/claw-theme-switcher";
|
||||
import {
|
||||
fetchLatestRunStatus,
|
||||
ThreadList,
|
||||
toReplayRepository,
|
||||
} from "@/components/assistant-ui/thread-list";
|
||||
@@ -367,9 +368,10 @@ function useSidebarSessions(open: boolean) {
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const payload = (await response.json()) as ReplaySessionPayload;
|
||||
const runStatus = await fetchLatestRunStatus(cleanSessionId);
|
||||
replaySession(
|
||||
cleanSessionId,
|
||||
toReplayRepository(payload, cleanSessionId),
|
||||
toReplayRepository(payload, cleanSessionId, runStatus),
|
||||
);
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
|
||||
Reference in New Issue
Block a user