返回 DeepSeek-Reasonix
WorkspaceTurnResult.tsx
根目录 / desktop / frontend / src / components / WorkspaceTurnResult.tsx
1 import { forwardRef, useEffect, useState } from "react";
2 import { app } from "../lib/bridge";
3 import { useT } from "../lib/i18n";
4 import { normalizeTurnChanges } from "../lib/turnResult";
5 import type { TurnChanges, TurnFileChange, WireCompletionSummary } from "../lib/types";
6 import { DiffView } from "./DiffView";
7 import { TurnResultSummary } from "./TurnResultSummary";
8 import { WorkspaceTurnVerification } from "./WorkspaceTurnVerification";
9
10 function FrozenFileDiff({ tabId, sessionPath, turn, resultId, file }: { tabId: string; sessionPath: string; turn: number; resultId: string; file: TurnFileChange }) {
11 const t = useT();
12 const key = `${tabId}\u0000${sessionPath}\u0000${resultId}\u0000${file.path}`;
13 const [resource, setResource] = useState<{ key: string; file?: TurnFileChange | null }>();
14 useEffect(() => {
15 let active = true;
16 void app.WorkspaceTurnChangeDetail(tabId, sessionPath, turn, resultId, file.path)
17 .then(value => { if (active) setResource({ key, file: value }); })
18 .catch(() => { if (active) setResource({ key, file: null }); });
19 return () => { active = false; };
20 }, [file.path, key, resultId, sessionPath, tabId, turn]);
21 const detail = resource?.key === key ? resource.file : undefined;
22 return <section className="turn-file-diff" aria-label={file.path}>
23 <h4>{file.path}</h4>
24 {detail === undefined ? <p role="status">{t("completion.loadingResult")}</p>
25 : !detail ? <p>{t("completion.diffUnavailable")}</p>
26 : detail.binary ? <p>{t("completion.binary")}</p>
27 : detail.modeOnly ? <p>{t("completion.modeOnly")}</p>
28 : detail.unavailable ? <p>{t("completion.detailLimited")}</p>
29 : detail.patch ? <DiffView diff={detail.patch} /> : <p>{t("completion.noNetChanges")}</p>}
30 </section>;
31 }
32
33 export const WorkspaceTurnResult = forwardRef<HTMLElement, {
34 summary: WireCompletionSummary;
35 tabId: string;
36 sessionPath: string;
37 initialView?: "changes" | "checks";
38 onAllChanges(): void;
39 }>(function WorkspaceTurnResult({ summary, tabId, sessionPath, initialView = "checks", onAllChanges }, ref) {
40 const t = useT();
41 const recorded = summary.receipt?.diff;
42 const resultId = recorded?.id ?? "";
43 const turn = recorded?.turn ?? summary.checkpointTurn;
44 const key = `${tabId}\u0000${sessionPath}\u0000${resultId}`;
45 const [selected, setSelected] = useState<{ key: string; path: string }>();
46 const [view, setView] = useState(initialView);
47 const [resource, setResource] = useState<{ key: string; result?: TurnChanges }>();
48 useEffect(() => {
49 if (!resultId || turn === undefined || !sessionPath) return;
50 let active = true;
51 void app.WorkspaceTurnChanges(tabId, sessionPath, turn, resultId)
52 .then(result => { if (active) setResource({ key, result: normalizeTurnChanges(result) }); })
53 .catch(() => { if (active) setResource({ key }); });
54 return () => { active = false; };
55 }, [key, resultId, sessionPath, tabId, turn]);
56 const current = resource?.key === key ? resource : undefined;
57 const available = current?.result?.id === resultId && current?.result?.coverage !== "unknown";
58 const files = recorded?.files ?? [];
59 const file = selected?.key === key ? files.find(file => file.path === selected.path) : undefined;
60 return <section ref={ref} className="workspace-turn-result" aria-label={t("notice.completionChangesTitle")}>
61 <header className="workspace-turn-result__head">
62 <div className="workspace-turn-result__tabs" role="group" aria-label={t("notice.completionChangesTitle")}>
63 <button className="btn btn--small" aria-pressed={view === "changes"} onClick={() => setView("changes")}>{t("completion.turnDiff")}</button>
64 <button className="btn btn--small" aria-pressed={view === "checks"} onClick={() => setView("checks")}>{t("completion.panelTitle")}</button>
65 </div>
66 <button className="btn btn--small" onClick={onAllChanges}>{t("completion.allChanges")}</button>
67 </header>
68 {turn !== undefined && <p className="workspace-turn-result__label">{t("completion.turnLabel", { count: turn + 1 })}</p>}
69 {view === "checks" ? <WorkspaceTurnVerification summary={summary} tabId={tabId} sessionPath={sessionPath} /> : <>
70 <TurnResultSummary summary={summary} />
71 {(!resultId || !sessionPath || current && !available) && <p className="workspace-note">{t("completion.diffUnavailable")}</p>}
72 {resultId && sessionPath && !current && <p role="status">{t("completion.loadingResult")}</p>}
73 <div className="turn-file-list">
74 {files.map(entry => <button className="turn-file-list__entry" key={entry.path} disabled={!available} aria-pressed={file?.path === entry.path} onClick={() => setSelected({ key, path: entry.path })}>
75 <span>{entry.path}</span><span>{entry.binary ? t("completion.binary") : entry.modeOnly ? t("completion.modeOnly") : entry.uncounted ? t("completion.linesUnknown") : <><span className="turn-result-added">+{entry.added}</span>{" "}<span className="turn-result-removed">−{entry.removed}</span></>}</span>
76 </button>)}
77 </div>
78 {file && available && turn !== undefined && <FrozenFileDiff tabId={tabId} sessionPath={sessionPath} turn={turn} resultId={resultId} file={file} />}
79 </>}
80 </section>;
81 });
82
82 lines Plain Text