返回 DeepSeek-Reasonix
ChatToolBody.tsx
根目录 / desktop / frontend / src / components / ChatToolBody.tsx
1 import { useEffect, useRef, useState } from "react";
2 import type { ChatNode } from "../lib/chatViewSource";
3 import type { ChatContentLoader } from "../lib/chatContentLoader";
4 import { useT } from "../lib/i18n";
5 import { diffsFor, subjectOf } from "../lib/tools";
6 import { TerminalBlock } from "./harness-chat/TerminalBlock";
7 import { DiffBlock } from "./harness-chat/DiffBlock";
8 import { WebBlock } from "./harness-chat/WebBlock";
9 import { normalizeSearchSources } from "../lib/searchSourcesPresentation";
10 import { parseSearchSources, searchOutputMetadata } from "../lib/searchSources";
11 import toolCss from "./harness-chat/ToolRow.styles";
12 import { classifyTool, toolPresentation } from "../lib/chatToolPresentation";
13 import { boundedPayloadSections, utf8Prefix } from "../lib/toolPayloadPreview";
14 import "./harness-chat/TerminalBlock.css";
15 import "./harness-chat/DiffBlock.css";
16 import "./harness-chat/Pill.css";
17 import "./harness-chat/ToolBody.css";
18 import "./harness-chat/WebBlock.css";
19
20 export default function ChatToolBody({ item, loader }: { item: Extract<ChatNode, { kind: "tool" }>["item"]; loader: ChatContentLoader }) {
21 const t = useT();
22 const [loaded, setLoaded] = useState<{ item: typeof item; text: string }>();
23 const [pendingItem, setPendingItem] = useState<typeof item>();
24 const [errorItem, setErrorItem] = useState<typeof item>();
25 const busy = pendingItem === item;
26 const error = errorItem === item;
27 const epoch = useRef(0);
28 useEffect(() => { return () => { epoch.current++; }; }, [item]);
29 const full = loaded && loaded.item.id === item.id && loaded.item.args === item.args &&
30 loaded.item.output === item.output && loaded.item.error === item.error && loaded.item.dataArchived === item.dataArchived
31 ? loaded.text : undefined;
32 const preview = JSON.stringify({ args: item.args, output: item.output, error: item.error, diff: item.fileDiff });
33 const limited = full === undefined && (loader.needsFullContent(item, "tool") || preview.length > 8000);
34 const load = async () => {
35 const ticket = ++epoch.current; setPendingItem(item); setErrorItem(undefined);
36 try {
37 const text = await loader.load(item, "tool");
38 if (ticket !== epoch.current) throw new Error("Tool changed or closed");
39 const payload: unknown = JSON.parse(text);
40 if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("Invalid tool content");
41 for (const key of ["args", "output", "error"] as const) {
42 if (key in payload && (payload as Record<string, unknown>)[key] != null && typeof (payload as Record<string, unknown>)[key] !== "string") throw new Error("Invalid tool content");
43 }
44 setLoaded({ item, text });
45 } catch { if (ticket === epoch.current) setErrorItem(item); }
46 finally { if (ticket === epoch.current) setPendingItem(undefined); }
47 };
48 let value: { args?: string; output?: string; error?: string } = item;
49 if (full !== undefined) { try { value = JSON.parse(full); } catch { /* Keep the available preview. */ } }
50 let args: Record<string, unknown> = {};
51 try { args = JSON.parse(value.args || "{}") || {}; } catch { /* Streaming arguments. */ }
52 const labels = { copy: t("msg.copy"), copied: t("msg.copied"), collapse: t("common.collapse"), collapseAria: t("common.collapse"),
53 expand: (hidden: number) => t("chat.expandLines", { count: hidden }), expandAria: (hidden: number) => t("chat.expandLines", { count: hidden }) };
54 const diffs = !limited ? diffsFor(item.name, value.args || "{}") : [];
55 const kind = classifyTool(item);
56 const presentation = toolPresentation(item);
57 const search = kind === "search" && !limited ? normalizeSearchSources(item.searchSources ?? parseSearchSources(value.output || "")) : undefined;
58 const searchMeta = searchOutputMetadata(value.output);
59 return <>
60 {!limited && kind === "shell" && (item.execution || item.status === "running" || item.status === "stopped") ? <TerminalBlock command={typeof args.command === "string" ? args.command : value.args || ""}
61 output={value.error || value.output} running={presentation.state === "running"} exitCode={presentation.exitCode}
62 presentation={{ state: presentation.dot, label: t(presentation.label) }}
63 maxLines={200} className={toolCss.terminalBody}
64 labels={{ ...labels, signal: signal => signal, exitCode: code => `${code}`, running: t("chat.running"), failed: t("chat.failed"), done: t("chat.done"), noOutput: t("chat.noOutput") }} />
65 : search && item.status === "done" ? <WebBlock kind="search" answer={searchMeta.summary ?? item.searchSummary}
66 sources={search.visible.map(source => ({ url: source.href, title: source.title }))} truncated={search.hiddenCount > 0}
67 labels={{ noResults: t("sources.notProvided"), sourcesTruncated: t("sources.hidden", { n: search.hiddenCount }), http: "HTTP", contentTruncated: t("chat.loadFull") }} className={toolCss.webBody} />
68 : diffs.length ? <DiffBlock diffs={diffs.map(diff => ({ path: subjectOf(item.name, value.args || "{}"), oldText: diff.original, newText: diff.modified }))}
69 maxLines={200} labels={{ ...labels, files: count => t("chat.files", { count }) }} className={toolCss.diffBody} />
70 : <div className={toolCss.ioCard}><ToolPayload text={full ?? preview} preview={limited} /></div>}
71 {limited && <button className="btn" disabled={busy} onClick={() => void load()}>{t(error ? "chat.loadFailed" : busy ? "chat.loading" : "chat.loadFull")}</button>}
72 </>;
73 }
74
75 export function ToolPayload({ text, preview }: { text: string; preview: boolean }) {
76 const t = useT();
77 let value: unknown;
78 try { value = JSON.parse(text); } catch { return <pre>{preview ? utf8Prefix(text, 16 * 1024) : text}</pre>; }
79 if (!value || typeof value !== "object" || Array.isArray(value)) return <pre>{preview ? utf8Prefix(text, 16 * 1024) : text}</pre>;
80 const entries = preview ? boundedPayloadSections(value as Record<string, unknown>)
81 : Object.entries(value).filter(([, content]) => content != null).map(([key, content]) => ({ key, body: typeof content === "string" ? content : JSON.stringify(content, null, 2) }));
82 return <>{entries.map(({ key, body }) => {
83 const label = key === "args" ? t("chat.tool.input") : key === "output" ? t("chat.tool.output") : key;
84 return <section key={key} className={toolCss.ioSection}><span className={toolCss.ioLabel}>{label}</span><pre className={toolCss.ioText} data-error={key === "error" || undefined}>{body}</pre></section>;
85 })}</>;
86 }
87
87 lines Plain Text