| 1 | import React, { useState } from "react"; |
| 2 | import { createRoot } from "react-dom/client"; |
| 3 | import { Transcript } from "../src/components/Transcript"; |
| 4 | import { WorkspaceTurnResult } from "../src/components/WorkspaceTurnResult"; |
| 5 | import { initialState, reducer, type Item } from "../src/lib/useController"; |
| 6 | import { historicalResultNotice } from "../src/lib/completionResultState"; |
| 7 | import { app, type AppBindings } from "../src/lib/bridge"; |
| 8 | import { installDesktopHostStub } from "../src/__tests__/desktopHostStub"; |
| 9 | import { LocaleProvider, useI18n } from "../src/lib/i18n"; |
| 10 | import type { TurnChanges, WireCompletionSummary, WireEvent } from "../src/lib/types"; |
| 11 | import "../src/styles.css"; |
| 12 | |
| 13 | const path = "desktop/frontend/src/components/deeply/nested/very-long-component-name/WorkspaceTurnResult.tsx"; |
| 14 | const diff: TurnChanges = { id: "0:42", turn: 0, coverage: "complete", added: 18, removed: 6, reasons: [], files: [ |
| 15 | { path, kind: "modify", added: 15, removed: 6 }, |
| 16 | { path: "internal/checkpoint/turn_changes.go", kind: "create", added: 3, removed: 0 }, |
| 17 | ] }; |
| 18 | let unavailable = false; |
| 19 | const calls: string[] = []; |
| 20 | const fallback = Object.fromEntries(["ReportFrontendDiagnostic", "ToolResultForTab", "ListDirForTab", "WorkspaceChanges"].map(k => [k, app[k as keyof AppBindings]])); |
| 21 | installDesktopHostStub(new Proxy({ |
| 22 | ...fallback, |
| 23 | WorkspaceTurnChanges: async (_tab: string, _session: string, _turn: number, id: string) => { calls.push("summary:" + id); return unavailable ? { ...diff, id: undefined, coverage: "unknown", files: [] } : { ...diff, id }; }, |
| 24 | WorkspaceTurnChangeDetail: async (_tab: string, _session: string, _turn: number, id: string, file: string) => { calls.push("detail:" + id); return unavailable ? null : { ...diff.files.find(f => f.path === file), patch: "@@ -1,2 +1,3 @@\n-old result\n+frozen result for " + id + "\n+confirmed changes\n context" }; }, |
| 25 | TurnCheckLog: async (_tab: string, session: string, id: string) => { calls.push("log:" + session + ":" + id); return unavailable ? null : { output: "go test ./...\n--- FAIL: TestFrozenResult\nexpected 0, got 1\nFAIL\nexit status 1", truncated: false }; }, |
| 26 | }, { get(target, key) { return target[key as keyof typeof target] ?? (async () => undefined); } }) as unknown as AppBindings); |
| 27 | (window as unknown as { turnResultCalls: string[] }).turnResultCalls = calls; |
| 28 | |
| 29 | function snapshot(scenario: string) { |
| 30 | const event = (s: typeof initialState, e: WireEvent) => reducer(s, { type: "event", e }); |
| 31 | let state = reducer(initialState, { type: "user", text: "完善本轮结果展示,保留历史差异和检查日志。" }); |
| 32 | state = event(state, { kind: "turn_started", turnId: "fixture-turn", checkpointTurn: 0 }); |
| 33 | state = event(state, { kind: "tool_dispatch", turnId: "fixture-turn", tool: { id: "check-1", name: "exec_command", args: '{"command":"go test ./..."}', readOnly: false } }); |
| 34 | state = event(state, { kind: "tool_progress", turnId: "fixture-turn", tool: { id: "check-1", name: "exec_command", verifying: true, output: "running checkpoint tests..." } }); |
| 35 | if (scenario === "running") return state; |
| 36 | const failed = scenario === "failed"; |
| 37 | const checks = scenario === "none" ? [] : [{ command: "go test ./...", passed: !failed && scenario !== "interrupted", stale: scenario === "stale", interrupted: scenario === "interrupted", exitCode: failed ? 1 : 0, toolCallId: "check-1", toolResultId: "log-entry-1" }]; |
| 38 | const receipt = { verdict: "partial", interrupted: scenario === "interrupted", diff: scenario === "legacy" ? undefined : { ...diff, coverage: scenario === "partial" ? "partial" as const : "complete" as const, reasons: scenario === "partial" ? ["external_change"] : [] }, verifications: checks }; |
| 39 | state = event(state, { kind: "tool_result", turnId: "fixture-turn", tool: { id: "check-1", name: "exec_command", output: "check completed" } }); |
| 40 | state = event(state, { kind: "message", text: "已补齐本轮差异与检查记录。文件统计只计算已确认的净变更。", turnId: "fixture-turn" }); |
| 41 | state = event(state, { kind: "completion_summary", turnId: "fixture-turn", completion: { preset: "balanced", verdict: "partial", mutations: 30, checks_passed: checks.length && !failed ? 1 : 0, checks_failed: failed ? 1 : 0, checks_suppressed: 0, review: "none", attention: failed } }); |
| 42 | state = event(state, { kind: "turn_done", turnId: "fixture-turn", checkpointTurn: 0, receipt: scenario === "legacy" ? undefined : receipt }); |
| 43 | if (scenario === "history") { |
| 44 | const saved = historicalResultNotice({ role: "notice", content: "", completionReceipt: { ...receipt, diff: { ...diff, id: "0:historical" } }, checkpointTurn: 0, turnId: "historical" }, "old-result")!; |
| 45 | state.items = [{ kind: "user", id: "old-user", text: "这是历史轮次" }, saved, { kind: "assistant", id: "old-answer", text: "历史修改已完成。", reasoning: "", streaming: false }, { kind: "user", id: "middle-user", text: "继续说明" }, { kind: "assistant", id: "middle-answer", text: "这一轮没有文件修改。", reasoning: "", streaming: false }, ...state.items] as Item[]; |
| 46 | } |
| 47 | return state; |
| 48 | } |
| 49 | |
| 50 | function Fixture() { |
| 51 | const locale = useI18n(); |
| 52 | const [scenario, setScenario] = useState("failed"); |
| 53 | const [dark, setDark] = useState(true); |
| 54 | const [width, setWidth] = useState(410); |
| 55 | const [selection, setSelection] = useState<{ summary: WireCompletionSummary; view: "changes" | "checks"; key: number }>(); |
| 56 | const [missing, setMissing] = useState(false); |
| 57 | const state = React.useMemo(() => snapshot(scenario), [scenario]); |
| 58 | React.useEffect(() => { document.documentElement.dataset.theme = dark ? "dark" : "light"; document.documentElement.dataset.themeStyle = "graphite"; }, [dark]); |
| 59 | React.useEffect(() => { locale.setPref("zh"); }, [locale.setPref]); |
| 60 | const show = (summary: WireCompletionSummary | undefined, view: "changes" | "checks") => summary && setSelection({ summary, view, key: Date.now() }); |
| 61 | return <div style={{ height: "100vh", display: "grid", gridTemplateRows: "auto 1fr", background: "var(--bg)", color: "var(--fg)" }}> |
| 62 | <nav style={{ display: "flex", gap: 8, padding: 12, flexWrap: "wrap", borderBottom: "1px solid var(--border-soft)" }} aria-label="Browser fixtures"> |
| 63 | <label>场景 <select aria-label="Scenario" value={scenario} onChange={e => { setScenario(e.target.value); setSelection(undefined); }}>{["failed","passed","none","stale","interrupted","running","partial","legacy","history"].map(s => <option key={s}>{s}</option>)}</select></label> |
| 64 | <button onClick={() => setDark(v => !v)}>深浅色</button> |
| 65 | <button onClick={() => setWidth(v => v === 410 ? 280 : 410)}>面板宽度 {width}</button> |
| 66 | <button aria-pressed={missing} onClick={() => { unavailable = !missing; setMissing(!missing); setSelection(undefined); }}>模拟数据已清理</button> |
| 67 | </nav> |
| 68 | <main style={{ minHeight: 0, display: "grid", gridTemplateColumns: selection ? "minmax(0,1fr) " + width + "px" : "minmax(0,1fr)" }}> |
| 69 | <div style={{ minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" }}><Transcript items={state.items} running={state.running} turnStartAt={state.turnStartAt} tabId="fixture" geometrySessionKey="fixture" onPrompt={() => {}} onOpenChanges={s => show(s, "changes")} onOpenVerification={s => show(s, "checks")} /></div> |
| 70 | {selection && <aside style={{ overflow: "auto", minWidth: 0, borderLeft: "1px solid var(--border-soft)" }}><WorkspaceTurnResult key={selection.key} summary={selection.summary} tabId="fixture" sessionPath="/fixture-session.json" initialView={selection.view} onAllChanges={() => setSelection(undefined)} /></aside>} |
| 71 | </main> |
| 72 | </div>; |
| 73 | } |
| 74 | createRoot(document.getElementById("root")!).render(<LocaleProvider><Fixture /></LocaleProvider>); |
| 75 |