import { bindExternalStoreMessage, type ExportedMessageRepository, type ThreadMessage, } from "@assistant-ui/core"; import { ThreadListPrimitive } from "@assistant-ui/react"; import type { UIMessage } from "ai"; import { Loader2Icon, MoreHorizontalIcon, PencilIcon, PlusIcon, ServerIcon, Trash2Icon, XIcon, } from "lucide-react"; import { type FC, type KeyboardEvent, useEffect, useRef, useState, } from "react"; import { Button } from "@/components/ui/button"; import { clearActiveSessionId, readActiveSessionId, writeActiveSessionId, writePendingWorkspaceSessionId, } from "@/lib/claw-active-session"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url"; type ClawSession = { session_id: string; turns: number; tool_calls: number; preview: string; modified_at: number; }; type ClawStoredMessage = { role?: string; content?: string; name?: string; tool_call_id?: string; tool_calls?: ClawStoredToolCall[]; state?: string; stop_reason?: string; metadata?: { elapsed_ms?: unknown; kind?: unknown; placeholder?: unknown; phase?: unknown; status?: unknown; }; }; type ClawStoredSession = { session_id: string; messages?: ClawStoredMessage[]; }; export type ClawRunStatus = { run_id?: string; session_id?: string; status?: | "idle" | "queued" | "running" | "completed" | "failed" | "cancelled" | "interrupted"; current_stage?: string; started_at?: number; updated_at?: number; finished_at?: number; elapsed_ms?: number; cancellable?: boolean; pending_prompt?: string; events?: ClawRunEvent[]; }; type ClawActiveRunStatus = ClawRunStatus & { status: "queued" | "running"; }; type ClawRunEvent = { type?: string; started_at?: unknown; recorded_at?: unknown; elapsed_ms?: unknown; tool_name?: string; tool_call_id?: string; arguments?: unknown; assistant_content?: string; delta?: string; ok?: boolean; metadata?: { output_preview?: unknown; }; }; type ClawStoredToolCall = { id?: string; name?: string; arguments?: unknown; function?: { name?: string; arguments?: unknown; }; }; export const ThreadList: FC = () => { return ( ); }; const ThreadListNew: FC = () => { return (
{ clearActiveSessionId(); pushHomeUrl(); window.dispatchEvent(new Event("claw-active-session-cleared")); }} >
); }; type JupyterWorkspaceEntry = { id: string; label?: string; base_url?: string; workspace_root?: string; created_at?: number; last_used_at?: number; }; function ensureSidebarWorkspaceSessionId(): { sessionId: string; created: boolean; } { const existing = readActiveSessionId(); if (existing) return { sessionId: existing, created: false }; const generated = typeof crypto !== "undefined" && "randomUUID" in crypto ? `__LOCALID_${crypto.randomUUID().replace(/-/g, "").slice(0, 8)}` : `__LOCALID_${Math.random().toString(36).slice(2, 10)}`; writeActiveSessionId(generated); return { sessionId: generated, created: true }; } function workspaceDisplayLabel(entry: JupyterWorkspaceEntry): string { if (entry.label && entry.label.trim()) return entry.label.trim(); if (entry.base_url) { try { return new URL(entry.base_url).host; } catch { return entry.base_url; } } return entry.id; } const JupyterWorkspaceList: FC = () => { const [entries, setEntries] = useState([]); const [loaded, setLoaded] = useState(false); const [bindingId, setBindingId] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; const refresh = () => { fetch("/api/claw/jupyter/workspaces", { cache: "no-store" }) .then((res) => (res.ok ? res.json() : [])) .then((payload) => { if (cancelled) return; if (Array.isArray(payload)) { setEntries(payload as JupyterWorkspaceEntry[]); } else { setEntries([]); } setLoaded(true); }) .catch(() => { if (cancelled) return; setEntries([]); setLoaded(true); }); }; refresh(); const onChanged = () => refresh(); window.addEventListener("claw-workspaces-changed", onChanged); window.addEventListener("focus", refresh); return () => { cancelled = true; window.removeEventListener("claw-workspaces-changed", onChanged); window.removeEventListener("focus", refresh); }; }, []); if (!loaded || entries.length === 0) return null; const bind = async (entry: JupyterWorkspaceEntry) => { setError(null); setBindingId(entry.id); const { sessionId, created } = ensureSidebarWorkspaceSessionId(); try { const response = await fetch( `/api/claw/jupyter/workspaces/${encodeURIComponent(entry.id)}/bind`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ session_id: sessionId }), }, ); if (!response.ok) { const payload = (await response.json().catch(() => ({}))) as { detail?: string; error?: string; }; setError(payload.detail ?? payload.error ?? "切换工作区失败"); return; } if (created) writePendingWorkspaceSessionId(sessionId); window.dispatchEvent(new Event("claw-sessions-changed")); window.dispatchEvent(new Event("claw-workspaces-changed")); } catch (err) { setError( err instanceof Error ? err.message : "切换工作区时网络异常", ); } finally { setBindingId(null); } }; const remove = async (entry: JupyterWorkspaceEntry) => { if (!window.confirm(`移除工作区「${workspaceDisplayLabel(entry)}」?`)) return; try { const response = await fetch( `/api/claw/jupyter/workspaces/${encodeURIComponent(entry.id)}`, { method: "DELETE" }, ); if (!response.ok) return; setEntries((current) => current.filter((e) => e.id !== entry.id)); } catch { // best effort; next refresh will reconcile } }; return (
工作区
{entries.map((entry) => { const label = workspaceDisplayLabel(entry); const subtitle = entry.workspace_root ?? ""; const isBinding = bindingId === entry.id; return (
); })}
{error ? (
{error}
) : null}
); }; const ClawSessionList: FC = () => { const { replaySession } = useClawSessionReplay(); const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); const [loadingSessionId, setLoadingSessionId] = useState(null); const [deletingSessionId, setDeletingSessionId] = useState( null, ); const [renamingSessionId, setRenamingSessionId] = useState( null, ); const [draftTitle, setDraftTitle] = useState(""); const [openMenuSessionId, setOpenMenuSessionId] = useState( null, ); const renameInputRef = useRef(null); useEffect(() => { setActiveSessionId(readActiveSessionId()); const refreshSessions = () => { fetch("/api/claw/sessions") .then((res) => (res.ok ? res.json() : [])) .then((payload) => { if (Array.isArray(payload)) setSessions(dedupeSessions(payload)); }) .catch(() => setSessions([])); }; const clearActiveSession = () => { setActiveSessionId(null); refreshSessions(); }; window.addEventListener("claw-active-session-cleared", clearActiveSession); window.addEventListener("claw-sessions-changed", refreshSessions); window.addEventListener("focus", refreshSessions); refreshSessions(); return () => { window.removeEventListener( "claw-active-session-cleared", clearActiveSession, ); window.removeEventListener("claw-sessions-changed", refreshSessions); window.removeEventListener("focus", refreshSessions); }; }, []); useEffect(() => { if (!renamingSessionId) return; window.requestAnimationFrame(() => { renameInputRef.current?.focus(); renameInputRef.current?.select(); }); }, [renamingSessionId]); if (!sessions.length) return null; const saveTitle = async (sessionId: string, fallbackTitle: string) => { const normalized = draftTitle.trim(); if (!normalized || normalized === fallbackTitle) { setRenamingSessionId(null); setDraftTitle(""); return; } const response = await fetch(`/api/claw/sessions/${sessionId}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ title: normalized }), }); if (!response.ok) return; const payload = (await response.json()) as { title?: string }; const nextTitle = payload.title || normalized; setSessions((current) => current.map((item) => item.session_id === sessionId ? { ...item, preview: nextTitle } : item, ), ); setRenamingSessionId(null); setDraftTitle(""); }; return (
{sessions.map((session) => { const active = activeSessionId === session.session_id; const loading = loadingSessionId === session.session_id; return (
{renamingSessionId === session.session_id ? (
setDraftTitle(event.target.value)} onBlur={() => { saveTitle( session.session_id, session.preview || session.session_id, ); }} onKeyDown={(event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); event.currentTarget.blur(); } if (event.key === "Escape") { event.preventDefault(); setRenamingSessionId(null); setDraftTitle(""); } }} />
) : ( )} {openMenuSessionId === session.session_id ? (
) : null}
); })}
); }; function dedupeSessions(payload: unknown[]) { const byId = new Map(); for (const item of payload) { if (!isClawSession(item)) continue; const previous = byId.get(item.session_id); if (!previous || item.modified_at >= previous.modified_at) { byId.set(item.session_id, item); } } 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; return ( typeof item.session_id === "string" && typeof item.modified_at === "number" ); } export function toReplayRepository( session: ClawStoredSession, fallbackSessionId: string, runStatus?: ClawRunStatus | null, ): ExportedMessageRepository { const messages: ExportedMessageRepository["messages"] = []; const replayMessages = getReplayMessages(session.messages ?? [], runStatus); const toolResults = collectToolResults(replayMessages); const lastAssistantContentIndex = findLastAssistantContentIndex(replayMessages); let previousId: string | null = null; for (const [index, message] of replayMessages.entries()) { if (message.role === "tool") continue; if (message.role !== "user" && message.role !== "assistant") continue; const content = cleanStoredContent(message.content ?? ""); const toolParts = message.role === "assistant" ? toToolParts(message.tool_calls ?? [], toolResults, content) : []; if (!content && !toolParts.length) continue; if (content.trimStart().startsWith("")) continue; const id = `${fallbackSessionId}-${index}`; const shouldShowAssistantText = message.role !== "assistant" || !toolParts.length || index === lastAssistantContentIndex; const elapsedMs = message.role === "assistant" ? resolveReplayElapsedMs(replayMessages, index) : undefined; const parts = ( message.role === "assistant" && toolParts.length ? shouldShowAssistantText ? [...toolParts, { type: "text", text: content }] : toolParts : message.role === "assistant" && elapsedMs !== undefined ? [ { type: "reasoning", text: "思考并执行完成。", elapsedMs }, { type: "text", text: content }, ] : [{ type: "text", text: content }] ) as UIMessage["parts"]; const replayParts = withReplayElapsedMs(parts, elapsedMs); const uiMessage: UIMessage = { id, role: message.role, parts: replayParts, ...(message.role === "assistant" ? { metadata: { sessionId: session.session_id ?? fallbackSessionId, elapsedMs, }, } : {}), } as UIMessage; const threadMessage = { id, role: message.role, createdAt: new Date(), content: toThreadMessageContent(replayParts, elapsedMs), ...(message.role === "assistant" ? { status: { type: "complete", reason: "stop" }, metadata: { unstable_state: null, unstable_annotations: [], unstable_data: [], steps: [], custom: { sessionId: session.session_id ?? fallbackSessionId, elapsedMs, }, }, } : { metadata: { custom: {} } }), } as ThreadMessage; bindExternalStoreMessage(threadMessage, uiMessage); messages.push({ message: threadMessage, parentId: previousId }); previousId = id; } if (shouldShowTerminalPendingPrompt(replayMessages, runStatus)) { const pendingPrompt = resolvePendingPrompt(replayMessages, runStatus); if (pendingPrompt) { const id = `${fallbackSessionId}-pending-${runStatus?.run_id ?? "terminal"}`; const parts = [ { type: "text", text: pendingPrompt }, ] as UIMessage["parts"]; const uiMessage: UIMessage = { id, role: "user", parts, metadata: { sessionId: session.session_id ?? fallbackSessionId }, } as UIMessage; const threadMessage = { id, role: "user", createdAt: new Date(), content: toThreadMessageContent(parts), attachments: [], metadata: { custom: { sessionId: session.session_id ?? fallbackSessionId }, }, } as ThreadMessage; bindExternalStoreMessage(threadMessage, uiMessage); messages.push({ message: threadMessage, parentId: previousId }); previousId = id; } } if (shouldShowInterruptedNotice(session.messages ?? [], runStatus)) { const id = `${fallbackSessionId}-interrupted`; const text = runStatus?.status === "cancelled" ? "上一次任务已取消,后台没有正在执行的进程。你可以继续回复,或重新发起任务。" : "上一次任务已中断,后台没有正在执行的进程。你可以继续回复,或重新发起任务。"; const parts = [{ type: "text", 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: "complete", reason: "stop" }, 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; } if (isActiveRunStatus(runStatus)) { const pendingPrompt = resolvePendingPrompt(replayMessages, runStatus); if (pendingPrompt) { const id = `${fallbackSessionId}-pending-${runStatus.run_id ?? "active"}`; const parts = [ { type: "text", text: pendingPrompt }, ] as UIMessage["parts"]; const uiMessage: UIMessage = { id, role: "user", parts, metadata: { sessionId: session.session_id ?? fallbackSessionId }, } as UIMessage; const threadMessage = { id, role: "user", createdAt: new Date(), content: toThreadMessageContent(parts), attachments: [], metadata: { custom: { sessionId: session.session_id ?? fallbackSessionId }, }, } as ThreadMessage; bindExternalStoreMessage(threadMessage, uiMessage); messages.push({ message: threadMessage, parentId: previousId }); previousId = id; } const id = `${fallbackSessionId}-run-${runStatus.run_id ?? "active"}`; const runStartedAtMs = resolveRunStartedAtMs(runStatus); const parts = buildActiveRunParts(runStatus) as UIMessage["parts"]; const uiMessage: UIMessage = { id, role: "assistant", parts, metadata: { sessionId: session.session_id ?? fallbackSessionId, runId: runStatus.run_id, runStartedAtMs, }, } 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, runId: runStatus.run_id, runStartedAtMs, }, }, } as ThreadMessage; bindExternalStoreMessage(threadMessage, uiMessage); messages.push({ message: threadMessage, parentId: previousId }); previousId = id; } return { headId: previousId, messages, }; } function isActiveRunStatus( runStatus?: ClawRunStatus | null, ): runStatus is ClawActiveRunStatus { return runStatus?.status === "queued" || runStatus?.status === "running"; } function shouldShowTerminalPendingPrompt( messages: readonly ClawStoredMessage[], runStatus?: ClawRunStatus | null, ) { return ( !isActiveRunStatus(runStatus) && Boolean(resolvePendingPrompt(messages, runStatus)) && (runStatus?.status === "interrupted" || runStatus?.status === "cancelled" || runStatus?.status === "failed") ); } function buildActiveRunParts(runStatus: ClawActiveRunStatus) { const eventLines = summarizeRunEvents(runStatus); const fallback = runStatus.status === "queued" ? "当前会话已有任务在执行,本轮正在排队。" : runStatus.current_stage || "后端正在执行这个会话。"; const reasoningText = eventLines.length ? eventLines.join("\n") : fallback; const runStartedAtMs = resolveRunStartedAtMs(runStatus); const elapsedMs = normalizeElapsedMs(runStatus.elapsed_ms); return [ { type: "reasoning", text: reasoningText, runStartedAtMs, elapsedMs }, ...buildRunToolParts(runStatus.events ?? []), ]; } function summarizeRunEvents(runStatus: ClawActiveRunStatus) { const lines: string[] = []; for (const event of runStatus.events ?? []) { if (event.type === "run_queued") { lines.push("当前会话已有任务在执行,本轮正在排队。"); } if (event.type === "run_started") { lines.push("后端已开始执行本轮任务。"); } if (event.type === "tool_start") { const stageNote = typeof event.assistant_content === "string" ? event.assistant_content.trim() : ""; lines.push( stageNote || (event.tool_name ? `调用工具 ${event.tool_name}` : "正在调用工具"), ); } if (event.type === "tool_result") { lines.push( event.tool_name ? `工具完成 ${event.tool_name}` : "工具调用完成", ); } if (event.type === "tool_delta") { lines.push(event.tool_name ? `${event.tool_name} 输出中` : "工具输出中"); } if (event.type === "final_text_start") { lines.push("正在整理回复"); } if (event.type === "user_review_required") { lines.push("等待用户 review"); } } if (runStatus.current_stage && lines.at(-1) !== runStatus.current_stage) { lines.push(runStatus.current_stage); } return lines.slice(-40); } function buildRunToolParts(events: readonly ClawRunEvent[]) { const resultById = new Map(); for (const event of events) { if (event.type === "tool_result" && event.tool_call_id) { resultById.set(event.tool_call_id, event); } } const seen = new Set(); return events.flatMap((event) => { if ( event.type !== "tool_start" || !event.tool_call_id || !event.tool_name || seen.has(event.tool_call_id) ) { return []; } seen.add(event.tool_call_id); const result = resultById.get(event.tool_call_id); const outputPreview = result?.metadata?.output_preview; const output = typeof outputPreview === "string" ? outputPreview : result ? result.ok === false ? "工具执行失败,等待最终结果汇总。" : "工具调用完成,等待最终结果汇总。" : undefined; return [ { type: "dynamic-tool", toolName: event.tool_name, toolCallId: event.tool_call_id, state: output === undefined ? "input-available" : "output-available", input: attachStageNote(event.arguments ?? {}, event.assistant_content), ...(output === undefined ? {} : { output }), }, ]; }); } function getReplayMessages( messages: readonly ClawStoredMessage[], runStatus?: ClawRunStatus | null, ) { const visibleMessages = isActiveRunStatus(runStatus) ? messages.filter((message) => !isRunStatusMessage(message)) : [...messages]; return trimIncompleteReplayTail(visibleMessages); } function shouldShowInterruptedNotice( messages: readonly ClawStoredMessage[], runStatus?: ClawRunStatus | null, ) { if (isActiveRunStatus(runStatus)) return false; if (hasRunStatusMessage(messages, runStatus?.status)) return false; return ( runStatus?.status === "interrupted" || runStatus?.status === "cancelled" || trimIncompleteReplayTail(messages).length !== messages.length ); } function trimIncompleteReplayTail(messages: readonly ClawStoredMessage[]) { const trimmed = [...messages]; while (trimmed.length && isIncompleteReplayMessage(trimmed.at(-1))) { trimmed.pop(); } while (trimmed.length && hasTrailingAssistantToolCall(trimmed.at(-1))) { trimmed.pop(); } return trimmed; } function isIncompleteReplayMessage(message?: ClawStoredMessage) { if (!message) return false; if (message.state && message.state !== "final") return true; if ( message.role === "tool" && (message.metadata?.phase === "starting" || message.metadata?.phase === "running") ) { return true; } if ( message.role !== "user" && message.metadata?.placeholder === true && message.metadata?.status === "running" ) { return true; } return false; } function hasTrailingAssistantToolCall(message?: ClawStoredMessage) { return message?.role === "assistant" && Boolean(message.tool_calls?.length); } function hasRunStatusMessage( messages: readonly ClawStoredMessage[], status?: string, ) { return messages.some( (message) => isRunStatusMessage(message) && (!status || message.metadata?.status === status), ); } function isRunStatusMessage(message?: ClawStoredMessage) { return message?.metadata?.kind === "run_status"; } function resolvePendingPrompt( messages: readonly ClawStoredMessage[], runStatus?: ClawRunStatus | null, ) { const pendingPrompt = typeof runStatus?.pending_prompt === "string" ? runStatus.pending_prompt.trim() : ""; if (!pendingPrompt) return null; for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (!message || message.role === "system" || message.role === "tool") continue; const content = cleanStoredContent(message.content ?? "").trim(); if (!content || content.trimStart().startsWith("")) continue; return message.role === "user" && content === pendingPrompt ? null : pendingPrompt; } return pendingPrompt; } function normalizeRunTimestampMs(value: unknown) { if (typeof value !== "number" || !Number.isFinite(value)) return undefined; return value < 10_000_000_000 ? Math.round(value * 1000) : Math.round(value); } function normalizeElapsedMs(value: unknown) { if (typeof value !== "number" || !Number.isFinite(value)) return undefined; return Math.max(0, Math.round(value)); } function resolveRunStartedAtMs(runStatus: ClawRunStatus) { const direct = normalizeRunTimestampMs(runStatus.started_at); if (direct !== undefined) return direct; const updatedAt = normalizeRunTimestampMs(runStatus.updated_at); const elapsedMs = normalizeElapsedMs(runStatus.elapsed_ms); if (updatedAt !== undefined && elapsedMs !== undefined) { return Math.max(0, updatedAt - elapsedMs); } for (const event of runStatus.events ?? []) { if (event.type !== "run_started") continue; const eventStarted = normalizeRunTimestampMs(event.started_at); if (eventStarted !== undefined) return eventStarted; const recordedAt = normalizeRunTimestampMs(event.recorded_at); if (recordedAt !== undefined) return recordedAt; } for (const event of runStatus.events ?? []) { const recordedAt = normalizeRunTimestampMs(event.recorded_at); const elapsedMs = normalizeElapsedMs(event.elapsed_ms); if (recordedAt !== undefined && elapsedMs !== undefined) { return Math.max(0, recordedAt - elapsedMs); } } for (const event of runStatus.events ?? []) { const recordedAt = normalizeRunTimestampMs(event.recorded_at); if (recordedAt !== undefined) return recordedAt; } return undefined; } function resolveReplayElapsedMs( messages: readonly ClawStoredMessage[], index: number, ) { const current = normalizeElapsedMs(messages[index]?.metadata?.elapsed_ms); if (current !== undefined) return current; for (let i = index + 1; i < messages.length; i += 1) { const message = messages[i]; if (message?.role === "user") return undefined; if (message?.role !== "assistant") continue; const elapsedMs = normalizeElapsedMs(message.metadata?.elapsed_ms); if (elapsedMs !== undefined) return elapsedMs; } return undefined; } function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message?.role !== "assistant") continue; const content = cleanStoredContent(message.content ?? ""); if (!content) continue; return index; } return -1; } function cleanStoredContent(content: string) { const markers = ["\n\n[当前会话目录]", "\n[当前会话目录]"]; for (const marker of markers) { if (content.includes(marker)) return content.split(marker, 1)[0].trim(); } return content.trim(); } function collectToolResults(messages: readonly ClawStoredMessage[]) { const results = new Map(); for (const message of messages) { if ( message.role === "tool" && message.tool_call_id && !isIncompleteReplayMessage(message) ) { results.set(message.tool_call_id, message.content ?? ""); } } return results; } function toToolParts( toolCalls: readonly ClawStoredToolCall[], toolResults: Map, stageNote?: string, ) { return toolCalls.flatMap((call) => { const toolCallId = call.id; const toolName = call.function?.name ?? call.name; if (!toolCallId || !toolName) return []; const input = attachStageNote( parseToolInput(call.function?.arguments ?? call.arguments), stageNote, ); const output = toolResults.get(toolCallId); return [ { type: "dynamic-tool", toolName, toolCallId, state: output === undefined ? "input-available" : "output-available", input, ...(output === undefined ? {} : { output }), }, ]; }); } function attachStageNote(input: unknown, stageNote?: string) { const note = stageNote?.trim(); if (!note) return input; if (input && typeof input === "object" && !Array.isArray(input)) { return { ...input, __claw_stage_note: note }; } return { value: input, __claw_stage_note: note }; } function withReplayElapsedMs(parts: UIMessage["parts"], elapsedMs?: number) { if (elapsedMs === undefined) return parts; return parts.map((part) => { if (part.type === "reasoning") return { ...part, elapsedMs }; if (part.type === "dynamic-tool") { return { ...part, elapsedMs, input: attachReplayElapsed(part.input ?? {}, elapsedMs), }; } return part; }) as UIMessage["parts"]; } function attachReplayElapsed(input: unknown, elapsedMs: number) { if (input && typeof input === "object" && !Array.isArray(input)) { return { ...input, __claw_elapsed_ms: elapsedMs }; } return { value: input, __claw_elapsed_ms: elapsedMs }; } function toThreadMessageContent(parts: UIMessage["parts"], elapsedMs?: number) { return parts.map((part) => { const elapsedPayload = elapsedMs === undefined ? {} : { elapsedMs }; if (part.type === "text") return part; if (part.type === "reasoning") return { ...part, ...elapsedPayload }; if (part.type === "dynamic-tool") { return { type: "tool-call", toolName: part.toolName, toolCallId: part.toolCallId, args: part.input ?? {}, argsText: JSON.stringify(stripReplayElapsed(part.input ?? {})), ...elapsedPayload, ...(part.state === "output-available" ? { result: part.output } : {}), }; } return part; }); } function stripReplayElapsed(value: unknown): unknown { if (!value || typeof value !== "object" || Array.isArray(value)) return value; const { __claw_elapsed_ms: _elapsedMs, ...rest } = value as Record< string, unknown >; return rest; } function parseToolInput(value: unknown) { if (typeof value !== "string") return value ?? {}; try { return JSON.parse(value); } catch { return value; } }