import { ActionBarMorePrimitive, ActionBarPrimitive, AuiIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, SuggestionPrimitive, ThreadPrimitive, useAui, useAuiState, } from "@assistant-ui/react"; import { ArrowDownIcon, ArrowUpIcon, BookOpenIcon, BrainIcon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, FileTextIcon, ListIcon, MoreHorizontalIcon, PencilIcon, RefreshCwIcon, SearchIcon, SquareIcon, WrenchIcon, } from "lucide-react"; import { Popover as PopoverPrimitive } from "radix-ui"; import type { ComponentProps, FC, KeyboardEvent, ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import TextareaAutosize from "react-textarea-autosize"; import { useActivityPanel } from "@/components/assistant-ui/activity-panel"; import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments, } 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"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; import { cn } from "@/lib/utils"; type ComposerInsertEvent = CustomEvent<{ text: string }>; type SkillSummary = { name: string; description?: string; when_to_use?: string; enabled?: boolean; configurable?: boolean; }; type SkillSyncResponse = { skills?: SkillSummary[]; updated?: boolean; before?: string; after?: string; changed_files?: string[]; message?: string; error?: string; detail?: string; }; type SlashCommandSummary = { names: string[]; primary: string; description?: string; webui_supported?: boolean; }; type ClawState = { model?: string; active_session_id?: string | null; }; type ClawStoredSession = { session_id?: string; model?: string; }; type ModelOption = { id: string; provider?: string; model_type?: string; }; type ContextBudget = { model?: string; context_window_tokens?: number; projected_input_tokens?: number; message_tokens?: number; chat_overhead_tokens?: number; reserved_output_tokens?: number; reserved_compaction_buffer_tokens?: number; reserved_schema_tokens?: number; hard_input_limit_tokens?: number; soft_input_limit_tokens?: number; overflow_tokens?: number; soft_overflow_tokens?: number; exceeds_hard_limit?: boolean; exceeds_soft_limit?: boolean; token_counter_backend?: string; token_counter_source?: string; token_counter_accurate?: boolean; }; function scheduleSessionListRefresh() { if (typeof window === "undefined") return; window.dispatchEvent(new Event("claw-sessions-changed")); for (const delay of [300, 1000, 2500]) { window.setTimeout(() => { window.dispatchEvent(new Event("claw-sessions-changed")); }, delay); } } export const Thread: FC = () => { useRefreshReplayedRun(); return (
s.thread.isEmpty}>
{() => }
); }; function useRefreshReplayedRun() { const { replaySession } = useClawSessionReplay(); const replayedRun = useReplayedRunState(); useEffect(() => { if (!replayedRun.active || !replayedRun.sessionId) return; const activeSessionId = replayedRun.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); }; }, [replayedRun.active, replayedRun.sessionId, replaySession]); } function useReplayedRunState() { const active = useAuiState((s) => s.thread.messages.some((message) => isReplayRunMessageId(message.id)), ); const replaySessionId = useAuiState((s) => { const replayRunMessage = s.thread.messages.find((message) => isReplayRunMessageId(message.id), ); return normalizeSessionId( readMetadataValue(replayRunMessage?.metadata, "sessionId"), ); }); const latestSessionId = useAuiState((s) => normalizeSessionId( findLatestMessageMetadataValue(s.thread.messages, "sessionId"), ), ); const runId = useAuiState((s) => { const replayRunMessage = s.thread.messages.find((message) => isReplayRunMessageId(message.id), ); return normalizeSessionId( readMetadataValue(replayRunMessage?.metadata, "runId"), ); }); const localSessionId = typeof window !== "undefined" ? normalizeSessionId(window.localStorage.getItem("claw.activeSessionId")) : null; const sessionId = replaySessionId ?? latestSessionId ?? localSessionId; return useMemo( () => ({ active, sessionId, runId }), [active, sessionId, runId], ); } async function cancelLatestRun(sessionId: string, runId?: string | null) { await fetch("/api/claw/runs/cancel", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ session_id: sessionId, ...(runId ? { run_id: runId } : {}), }), }); } const ChatTopActions: FC = () => { const { openFiles } = useActivityPanel(); const latestSessionId = useAuiState((s) => findLatestMessageMetadataValue(s.thread.messages, "sessionId"), ); return (
); }; const ThreadMessage: FC = () => { const role = useAuiState((s) => s.message.role); const isEditing = useAuiState((s) => s.message.composer.isEditing); if (isEditing) return ; if (role === "user") return ; return ; }; const ThreadScrollToBottom: FC = () => { return ( ); }; const ThreadWelcome: FC = () => { return (

ZK Data Agent

中控数据开发平台

); }; const ThreadSuggestions: FC = () => { return (
{() => }
); }; const ThreadSuggestionItem: FC = () => { return (
); }; const Composer: FC = () => { const replayedRun = useReplayedRunState(); return (
); }; const ComposerAction: FC = () => { const runtimeRunning = useAuiState((s) => s.thread.isRunning); const replayedRun = useReplayedRunState(); const cancelSessionId = replayedRun.sessionId; return (
{!runtimeRunning && !replayedRun.active ? ( ) : null} {runtimeRunning ? ( ) : null} {!runtimeRunning && replayedRun.active ? ( ) : null}
); }; const ReplayedRunCancelButton: FC<{ sessionId: string | null; runId: string | null; }> = ({ sessionId, runId }) => { const { replaySession } = useClawSessionReplay(); const [cancelling, setCancelling] = useState(false); return ( ); }; const ComposerContextStatus: FC = () => { const status = useComposerContextStatus(); const totalTokens = status.contextBudget?.projected_input_tokens ?? 0; const limit = status.contextBudget?.context_window_tokens ?? 0; const percent = limit ? Math.min(100, Math.round((totalTokens / limit) * 100)) : 0; return (
); }; function ContextWindowButton({ percent, totalTokens, limit, contextBudget, }: { percent: number; totalTokens: number; limit: number; contextBudget?: ContextBudget; }) { const ringStyle = { background: `conic-gradient(var(--primary) ${percent * 3.6}deg, var(--muted) 0deg)`, }; return (
背景信息窗口:
{percent}% 已用(剩余 {Math.max(0, 100 - percent)}%)
已用 {formatCompactTokenCount(totalTokens)} 标记,共{" "} {formatCompactTokenCount(limit)}
背景信息窗口:
{percent}% 已用(剩余 {Math.max(0, 100 - percent)}%)
已用 {formatCompactTokenCount(totalTokens)} 标记,共{" "} {formatCompactTokenCount(limit)}
软阈值: {formatCompactTokenCount( contextBudget?.soft_input_limit_tokens ?? 0, )}
硬阈值: {formatCompactTokenCount( contextBudget?.hard_input_limit_tokens ?? 0, )}
保留输出: {formatCompactTokenCount( contextBudget?.reserved_output_tokens ?? 0, )} ;压缩缓冲: {formatCompactTokenCount( contextBudget?.reserved_compaction_buffer_tokens ?? 0, )}
计数器:{contextBudget?.token_counter_backend ?? "unknown"} {contextBudget?.token_counter_accurate ? "(精确)" : "(估算)"}
); } function ModelPickerButton({ status, }: { status: { model?: string; models: ModelOption[]; setModel: (model: string) => Promise; }; }) { const [query, setQuery] = useState(""); const normalizedQuery = query.trim().toLowerCase(); const filteredModels = useMemo(() => { if (!normalizedQuery) return status.models; return status.models.filter((model) => { const provider = model.provider ?? modelProvider(model.id); return ( model.id.toLowerCase().includes(normalizedQuery) || provider.toLowerCase().includes(normalizedQuery) ); }); }, [normalizedQuery, status.models]); return (
切换模型
setQuery(event.target.value)} placeholder="搜索模型或来源" className="h-8 w-full rounded-lg border bg-background pr-2.5 pl-8 text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-ring" />
{status.models.length ? (
{filteredModels.length ? ( filteredModels.map((model) => ( )) ) : (
没有匹配的模型
)}
) : (
模型列表读取中
)}
); } function useComposerContextStatus() { const isRunning = useAuiState((s) => s.thread.isRunning); const latestSessionId = useAuiState((s) => findLatestMessageMetadataValue(s.thread.messages, "sessionId"), ); const wasRunningRef = useRef(false); const [status, setStatus] = useState<{ model?: string; contextBudget?: ContextBudget; sessionId?: string | null; models: ModelOption[]; setModel: (model: string) => Promise; }>({ models: [], setModel: async () => {}, }); useEffect(() => { const cleanLatestSessionId = normalizeSessionId(latestSessionId); if (cleanLatestSessionId) { window.localStorage.setItem("claw.activeSessionId", cleanLatestSessionId); window.setTimeout(() => { window.dispatchEvent(new Event("claw-sessions-changed")); }, 0); } }, [latestSessionId]); useEffect(() => { if (wasRunningRef.current && !isRunning) { window.dispatchEvent(new Event("claw-sessions-changed")); window.setTimeout(() => { window.dispatchEvent(new Event("claw-sessions-changed")); }, 1500); } wasRunningRef.current = isRunning; }, [isRunning]); useEffect(() => { let cancelled = false; async function refresh() { try { const stateResponse = await fetch("/api/claw/state", { cache: "no-store", }); const statePayload = stateResponse.ok ? ((await stateResponse.json()) as ClawState) : {}; let sessionId = normalizeSessionId( normalizeSessionId(latestSessionId) || normalizeSessionId( window.localStorage.getItem("claw.activeSessionId"), ) || normalizeSessionId(statePayload.active_session_id), ); if (sessionId) window.localStorage.setItem("claw.activeSessionId", sessionId); let sessionPayload: ClawStoredSession | null = null; let contextBudget: ContextBudget | undefined; if (sessionId) { const sessionResponse = await fetch( `/api/claw/sessions/${sessionId}`, { cache: "no-store", }, ); if (sessionResponse.ok) { sessionPayload = (await sessionResponse.json()) as ClawStoredSession; } else if (sessionResponse.status === 404) { window.localStorage.removeItem("claw.activeSessionId"); sessionId = null; } } const budgetUrl = new URL( "/api/claw/context-budget", window.location.origin, ); if (sessionId) budgetUrl.searchParams.set("session_id", sessionId); const budgetResponse = await fetch(budgetUrl, { cache: "no-store" }); contextBudget = budgetResponse.ok ? ((await budgetResponse.json()) as ContextBudget) : undefined; if (cancelled) return; setStatus((current) => ({ model: statePayload.model ?? contextBudget?.model ?? sessionPayload?.model, contextBudget, sessionId, models: current.models, setModel: current.setModel, })); } catch { if (!cancelled) { setStatus((current) => ({ ...current, })); } } } refresh(); const onRefresh = () => refresh(); window.addEventListener("focus", onRefresh); window.addEventListener("claw-active-session-cleared", onRefresh); const interval = window.setInterval(refresh, isRunning ? 2000 : 8000); return () => { cancelled = true; window.removeEventListener("focus", onRefresh); window.removeEventListener("claw-active-session-cleared", onRefresh); window.clearInterval(interval); }; }, [isRunning, latestSessionId]); useEffect(() => { let cancelled = false; async function refreshModels() { try { const response = await fetch("/api/claw/models", { cache: "no-store", }); const payload = (await response.json()) as { models?: ModelOption[] }; if (!cancelled && response.ok) { setStatus((current) => ({ ...current, models: Array.isArray(payload.models) ? payload.models : [], })); } } catch { if (!cancelled) { setStatus((current) => ({ ...current, models: [] })); } } } refreshModels(); return () => { cancelled = true; }; }, []); const setModel = useCallback(async (model: string) => { const response = await fetch("/api/claw/state", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model }), }); const payload = (await response.json()) as ClawState; if (!response.ok) return; setStatus((current) => ({ ...current, model: payload.model ?? model, contextBudget: undefined, })); }, []); useEffect(() => { setStatus((current) => ({ ...current, setModel })); }, [setModel]); return status; } function findLatestMessageMetadataValue( messages: readonly unknown[], key: string, ) { for (const message of [...messages].reverse()) { if (!message || typeof message !== "object") continue; const metadata = (message as { metadata?: unknown }).metadata; const value = readMetadataValue(metadata, key); if (value !== undefined) return value; } return undefined; } function readMetadataValue(metadata: unknown, key: string): unknown { if (!metadata || typeof metadata !== "object") return undefined; const record = metadata as Record; if (record[key] !== undefined) return record[key]; const custom = record.custom; if (custom && typeof custom === "object") { return (custom as Record)[key]; } return undefined; } function normalizeSessionId(value: unknown) { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed || null; } function formatCompactTokenCount(value: number) { if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`; return `${value}`; } function compactModelLabel(model?: string) { if (!model) return "模型"; const value = model.includes("/") ? model.split("/").at(-1) || model : model; if (value.toLowerCase().startsWith("mimo-v2.5")) return "MiMo 2.5"; if (value.toLowerCase().startsWith("mimo-v2")) return "MiMo 2"; if (value.toLowerCase().startsWith("deepseek")) return "DeepSeek"; if (value.toLowerCase().startsWith("qwen")) return "Qwen"; if (value.toLowerCase().startsWith("minimax")) return "MiniMax"; return value; } function modelProvider(model: string) { if (model.includes("/")) return model.split("/", 1)[0] || "unknown"; return "default"; } const ComposerAssistButtons: FC = () => { return (
} triggerLabel="" items={TOOL_PROMPTS} />
); }; const TOOL_PROMPTS = [ { title: "data_agent_profile_router_sessions", description: "查看 parquet 线上 session 数据字段、规模和样例。", prompt: "请优先使用 data_agent_profile_router_sessions 了解我指定的 router_session_parquet 数据目录,再给出可用字段和初步挖掘建议。", }, { title: "data_agent_search_router_sessions", description: "用关键词、正则、domain、device 等条件筛选候选。", prompt: "请优先使用 data_agent_search_router_sessions 按我的条件筛选线上候选,并说明筛选策略、命中数量和样例质量。", }, { title: "data_agent_normalize_dataset_draft", description: "把自然语言 draft 转换为 canonical records。", prompt: "请在我确认数据内容后,使用 data_agent_normalize_dataset_draft 把 draft 转换成 canonical records。", }, { title: "data_agent_validate_dataset_records", description: "校验 canonical records 是否符合结构约束。", prompt: "请使用 data_agent_validate_dataset_records 校验当前 canonical records,并解释每类错误应该如何修复。", }, ]; function PromptInsertDialog({ title, triggerIcon, triggerLabel, items, }: { title: string; triggerIcon: ReactNode; triggerLabel: string; items: { title: string; description: string; prompt: string }[]; }) { const [open, setOpen] = useState(false); return ( {title}
{items.map((item) => ( ))}
); } function SlashCommandDialog() { const [open, setOpen] = useState(false); const [commands, setCommands] = useState([]); const [status, setStatus] = useState( "点击后读取当前后端 slash command 列表。", ); async function loadCommands() { setStatus("正在读取 slash command 列表..."); try { const response = await fetch("/api/claw/slash-commands", { cache: "no-store", }); const payload = (await response.json()) as | SlashCommandSummary[] | { error?: string }; if (!response.ok || !Array.isArray(payload)) { throw new Error( Array.isArray(payload) ? "读取失败" : (payload.error ?? "读取失败"), ); } setCommands(payload); setStatus(payload.length ? "" : "当前没有可用 slash command。"); } catch (err) { setStatus(err instanceof Error ? err.message : "读取 slash command 失败"); } } return ( { setOpen(open); if (open) loadCommands(); }} > 快捷入口 {status ? (

{status}

) : null}
{commands.map((command) => { const commandText = `/${command.primary}`; const aliases = command.names .filter((name) => name !== command.primary) .map((name) => `/${name}`) .join(" "); return ( ); })}
); } function SkillInsertDialog() { const [open, setOpen] = useState(false); const [skills, setSkills] = useState([]); const [status, setStatus] = useState("点击后读取当前后端 Skill 列表。"); const [updatingSkill, setUpdatingSkill] = useState(null); const [syncingSkills, setSyncingSkills] = useState(false); const [bulkUpdatingSkills, setBulkUpdatingSkills] = useState(false); const configurableSkills = skills.filter( (skill) => skill.configurable !== false, ); const allSkillsEnabled = configurableSkills.length > 0 && configurableSkills.every((skill) => skill.enabled !== false); async function loadSkills() { setStatus("正在读取 Skill 列表..."); try { const response = await fetch("/api/claw/skills", { cache: "no-store" }); const payload = (await response.json()) as | SkillSummary[] | { error?: string }; if (!response.ok || !Array.isArray(payload)) { throw new Error( Array.isArray(payload) ? "读取失败" : (payload.error ?? "读取失败"), ); } setSkills(payload); setStatus(payload.length ? "" : "当前没有可用 Skill。"); } catch (err) { setStatus(err instanceof Error ? err.message : "读取 Skill 失败"); } } async function syncSkills() { setSyncingSkills(true); setStatus("正在从 git 同步 Skill..."); try { const response = await fetch("/api/claw/skills", { method: "POST", cache: "no-store", }); const payload = (await response.json()) as SkillSyncResponse; if (!response.ok || !Array.isArray(payload.skills)) { throw new Error(payload.detail ?? payload.error ?? "同步 Skill 失败"); } setSkills(payload.skills); const changedCount = payload.changed_files?.length ?? 0; if (payload.updated) { const shortHash = payload.after ? payload.after.slice(0, 7) : "最新提交"; setStatus(`Skill 已更新到 ${shortHash},变更 ${changedCount} 个文件。`); } else { setStatus("Skill 已是最新。"); } } catch (err) { setStatus(err instanceof Error ? err.message : "同步 Skill 失败"); } finally { setSyncingSkills(false); } } async function setSkillEnabled(skill: SkillSummary, enabled: boolean) { if (skill.configurable === false) return; const previous = skills; setUpdatingSkill(skill.name); setSkills((items) => items.map((item) => item.name === skill.name ? { ...item, enabled } : item, ), ); try { const response = await fetch("/api/claw/skills", { method: "PATCH", headers: { "content-type": "application/json", }, body: JSON.stringify({ skill: skill.name, enabled }), }); const payload = (await response.json()) as | SkillSummary[] | { error?: string; detail?: string }; if (!response.ok || !Array.isArray(payload)) { throw new Error( Array.isArray(payload) ? "更新失败" : (payload.detail ?? payload.error ?? "更新失败"), ); } setSkills(payload); setStatus(enabled ? `已启用 ${skill.name}` : `已停用 ${skill.name}`); } catch (err) { setSkills(previous); setStatus(err instanceof Error ? err.message : "更新 Skill 失败"); } finally { setUpdatingSkill(null); } } async function setAllSkillsEnabled(enabled: boolean) { if (!configurableSkills.length) return; const previous = skills; setBulkUpdatingSkills(true); setSkills((items) => items.map((item) => item.configurable === false ? item : { ...item, enabled }, ), ); try { const response = await fetch("/api/claw/skills", { method: "PATCH", headers: { "content-type": "application/json", }, body: JSON.stringify({ apply_all: true, enabled }), }); const payload = (await response.json()) as | SkillSummary[] | { error?: string; detail?: string }; if (!response.ok || !Array.isArray(payload)) { throw new Error( Array.isArray(payload) ? "更新失败" : (payload.detail ?? payload.error ?? "更新失败"), ); } setSkills(payload); setStatus(enabled ? "已启用全部 Skill。" : "已停用全部可选 Skill。"); } catch (err) { setSkills(previous); setStatus(err instanceof Error ? err.message : "批量更新 Skill 失败"); } finally { setBulkUpdatingSkills(false); } } return ( { setOpen(open); if (open) loadSkills(); }} > Skills
{status ? (

{status}

) : null}
{skills.map((skill) => (
setSkillEnabled(skill, event.currentTarget.checked) } />
))}
); } function insertComposerText(text: string) { window.dispatchEvent( new CustomEvent("claw-composer-insert", { detail: { text }, }), ); } const MessageError: FC = () => { return ( ); }; const AssistantMessage: FC = () => { const ACTION_BAR_PT = "pt-1.5"; const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`; const messageId = useAuiState((s) => s.message.id); return (
{ if (part.type === "reasoning" || part.type === "tool-call") return ["group-chain"]; return null; }} > {({ part }) => { switch (part.type) { case "group-chain": return ( ); case "text": return ( ); case "reasoning": return ; case "tool-call": return part.toolUI ?? ; default: return null; } }}
); }; const ActivityChainSummary: FC<{ messageId: string; partIndex: number | undefined; }> = ({ messageId, partIndex }) => { const { openItem } = useActivityPanel(); const startedAtRef = useRef(Date.now()); const metadataRunStartedAtMs = useAuiState((s) => { const value = readMetadataValue(s.message.metadata, "runStartedAtMs"); return typeof value === "number" && Number.isFinite(value) ? value : null; }); const contentRunStartedAtMs = useAuiState((s) => findNumberInActivityParts(s.message.content, "runStartedAtMs"), ); const storedElapsedMs = useAuiState((s) => { const value = readMetadataValue(s.message.metadata, "elapsedMs"); const contentValue = findElapsedMsInActivityParts(s.message.content); if (typeof value !== "number" || !Number.isFinite(value)) { return contentValue; } return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.round(value)) : null; }); const [elapsedMs, setElapsedMs] = useState(storedElapsedMs); const label = useAuiState((s) => { const message = s.message; const status = message.status?.type === "running" || isReplayRunMessageId(messageId) ? "running" : message.status?.type; return summarizeActivityChainLabel(message.content, status, elapsedMs); }); const active = useAuiState((s) => { const message = s.message; return ( message.status?.type === "running" || isReplayRunMessageId(messageId) ); }); useEffect(() => { if (!active) { setElapsedMs(storedElapsedMs); return; } const startedAt = metadataRunStartedAtMs ?? contentRunStartedAtMs ?? startedAtRef.current; setElapsedMs(Math.max(0, Date.now() - startedAt)); const timer = window.setInterval(() => { setElapsedMs(Math.max(0, Date.now() - startedAt)); }, 1000); return () => window.clearInterval(timer); }, [active, metadataRunStartedAtMs, contentRunStartedAtMs, storedElapsedMs]); return ( ); }; function isReplayRunMessageId(messageId: string) { return messageId.includes("-run-"); } type ActivitySummaryPart = { type: string; text?: string; result?: unknown; args?: unknown; toolName?: string; runStartedAtMs?: unknown; elapsedMs?: unknown; }; function findNumberInActivityParts( content: readonly ActivitySummaryPart[], key: keyof ActivitySummaryPart, ) { for (const part of content) { const value = part[key]; if (typeof value === "number" && Number.isFinite(value)) return value; } return null; } function findElapsedMsInActivityParts(content: readonly ActivitySummaryPart[]) { for (const part of content) { if (typeof part.elapsedMs === "number" && Number.isFinite(part.elapsedMs)) { return Math.max(0, Math.round(part.elapsedMs)); } const args = part.args; if (!args || typeof args !== "object" || Array.isArray(args)) continue; const value = (args as Record).__claw_elapsed_ms; if (typeof value === "number" && Number.isFinite(value)) { return Math.max(0, Math.round(value)); } } return null; } function summarizeActivityChainLabel( content: readonly ActivitySummaryPart[], status: string | undefined, elapsedMs: number | null = null, ) { const toolParts = content.filter((part) => part.type === "tool-call"); const runningTool = toolParts.findLast((part) => part.result === undefined); const activeTool = runningTool ?? toolParts.at(-1); const reasoningText = content.find((part) => part.type === "reasoning")?.text ?? ""; const lastReasoningLine = reasoningText .trim() .split("\n") .map((line) => line.trim()) .filter(Boolean) .at(-1); const stageNote = activeTool ? getToolStageNote(activeTool.args) : ""; const activeDetail = stageNote || (activeTool?.toolName ? `调用 ${activeTool.toolName}` : "") || (isGenericActivityLine(lastReasoningLine) ? "" : lastReasoningLine); if (status === "running") { const elapsed = `用时 ${formatActivityDuration(elapsedMs ?? 0)}`; if (activeDetail) { return `${compactActivityLabel(activeDetail, 15)},${elapsed}`; } return `思考中,${elapsed}`; } const label = compactActivityLabel( activeDetail || lastReasoningLine || "思考完成", 15, ); const finalDuration = extractDurationLabel(lastReasoningLine); const elapsed = finalDuration || (elapsedMs === null ? "" : formatActivityDuration(elapsedMs)); return elapsed ? `${label},用时 ${elapsed}` : label; } function getToolStageNote(value: unknown) { if (!value || typeof value !== "object" || Array.isArray(value)) return ""; const note = (value as Record).__claw_stage_note; return typeof note === "string" ? note.trim() : ""; } function isGenericActivityLine(value?: string) { if (!value) return true; return ( value.startsWith("思考中") || value.startsWith("思考并执行完成") || value.endsWith("输出中...") ); } function extractDurationLabel(value?: string) { if (!value) return ""; const match = value.match(/用时\s*([^。,.,\s]+)/); return match?.[1] ?? ""; } function formatActivityDuration(ms: number) { const safeMs = Math.max(0, Math.round(ms)); if (safeMs < 1000) return `${safeMs}ms`; const totalSeconds = Math.round(safeMs / 1000); if (totalSeconds < 60) return `${totalSeconds}s`; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return seconds > 0 ? `${minutes}min${seconds}s` : `${minutes}min`; } function compactActivityLabel(label: string, maxLength = 15) { const normalized = label.replace(/\s+/g, " ").trim(); const progress = normalized.match( /^进度[::]\s*批\s*(\d+)\s*\/\s*(\d+)(?:[,,]\s*已生成\s*(\d+)\s*\/\s*(\d+))?/, ); if (progress) { const batch = `批${progress[1]}/${progress[2]}`; const count = progress[3] && progress[4] ? ` 已${progress[3]}/${progress[4]}` : ""; const compactProgress = `${batch}${count}`; if (compactProgress.length <= maxLength) return compactProgress; return batch; } if (normalized.length <= maxLength) return normalized; return `${normalized.slice(0, Math.max(1, maxLength - 1))}…`; } const AssistantActionBar: FC = () => { return ( s.message.isCopied}> !s.message.isCopied}> Export as Markdown ); }; const UserMessage: FC = () => { return (
); }; const UserActionBar: FC = () => { return ( ); }; const EditComposer: FC = () => { return (
); }; const ImeComposerInput: FC> = ({ autoFocus, onChange, onCompositionEnd, onCompositionStart, onKeyDown, disabled, ...props }) => { const aui = useAui(); const storeText = useAuiState((s) => s.composer.isEditing ? s.composer.text : "", ); const runtimeDisabled = useAuiState( (s) => s.thread.isDisabled || s.composer.dictation?.inputDisabled, ); const [localText, setLocalText] = useState(storeText); const textareaRef = useRef(null); const isComposingRef = useRef(false); const justSubmittedRef = useRef(false); const isDisabled = runtimeDisabled || disabled; useEffect(() => { if (!isComposingRef.current) { if (justSubmittedRef.current && storeText) return; if (!storeText) justSubmittedRef.current = false; setLocalText(storeText); } }, [storeText]); useEffect(() => { if ( !isComposingRef.current && localText !== storeText && aui.composer().getState().isEditing ) { aui.composer().setText(localText); } }, [aui, localText, storeText]); useEffect(() => { if (!autoFocus || isDisabled) return; const textarea = textareaRef.current; if (!textarea) return; textarea.focus({ preventScroll: true }); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }, [autoFocus, isDisabled]); useEffect(() => { const clearAfterSubmit = () => { justSubmittedRef.current = true; setLocalText(""); scheduleSessionListRefresh(); }; const unsubscribeComposer = aui.on("composer.send", clearAfterSubmit); const unsubscribeRun = aui.on("thread.runStart", clearAfterSubmit); return () => { unsubscribeComposer(); unsubscribeRun(); }; }, [aui]); const syncComposerText = useCallback( (value: string) => { if (!aui.composer().getState().isEditing) return; aui.composer().setText(value); }, [aui], ); useEffect(() => { const textarea = textareaRef.current; if (!textarea) return; const handleInsert = (event: Event) => { const insertText = (event as ComposerInsertEvent).detail?.text; if (!insertText) return; const nextText = [localText.trimEnd(), insertText.trim()] .filter(Boolean) .join("\n\n"); setLocalText(nextText); syncComposerText(nextText); requestAnimationFrame(() => { textarea.focus({ preventScroll: true }); textarea.setSelectionRange(nextText.length, nextText.length); }); }; window.addEventListener("claw-composer-insert", handleInsert); return () => window.removeEventListener("claw-composer-insert", handleInsert); }, [localText, syncComposerText]); const handleKeyDown = (event: KeyboardEvent) => { onKeyDown?.(event); if (event.defaultPrevented || isDisabled) return; if ( event.nativeEvent.isComposing || isComposingRef.current || event.key !== "Enter" || event.shiftKey ) { return; } const threadState = aui.thread().getState(); if (threadState.isRunning && !threadState.capabilities.queue) return; event.preventDefault(); syncComposerText(localText); aui.composer().send(); justSubmittedRef.current = true; setLocalText(""); }; return ( { onChange?.(event); const nextText = event.currentTarget.value; justSubmittedRef.current = false; setLocalText(nextText); const isNativeComposing = (event.nativeEvent as { isComposing?: boolean }).isComposing === true; if (!isNativeComposing && !isComposingRef.current) { syncComposerText(nextText); } }} onCompositionStart={(event) => { onCompositionStart?.(event); isComposingRef.current = true; }} onCompositionEnd={(event) => { onCompositionEnd?.(event); isComposingRef.current = false; const target = event.currentTarget; queueMicrotask(() => { const nextText = target.value; setLocalText(nextText); syncComposerText(nextText); }); }} onKeyDown={handleKeyDown} /> ); }; const BranchPicker: FC = ({ className, ...rest }) => { return ( / ); };