niceeval 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +2 -2
  4. package/dist/i18n/zh-CN.d.ts +3 -3
  5. package/dist/report/aggregate.d.ts +32 -26
  6. package/dist/report/aggregate.js +157 -76
  7. package/dist/report/built-in/index.d.ts +2 -0
  8. package/dist/report/built-in/index.js +8 -0
  9. package/dist/report/components.d.ts +91 -164
  10. package/dist/report/components.js +377 -114
  11. package/dist/report/compute.d.ts +86 -73
  12. package/dist/report/compute.js +592 -432
  13. package/dist/report/flag.d.ts +28 -17
  14. package/dist/report/flag.js +86 -16
  15. package/dist/report/format.d.ts +11 -11
  16. package/dist/report/format.js +17 -15
  17. package/dist/report/index.d.ts +16 -17
  18. package/dist/report/index.js +20 -22
  19. package/dist/report/load.js +3 -2
  20. package/dist/report/locale.d.ts +49 -34
  21. package/dist/report/locale.js +106 -58
  22. package/dist/report/metrics.d.ts +10 -3
  23. package/dist/report/metrics.js +46 -12
  24. package/dist/report/primitives.d.ts +42 -15
  25. package/dist/report/primitives.js +135 -26
  26. package/dist/report/react/AttemptList.d.ts +10 -8
  27. package/dist/report/react/AttemptList.js +18 -10
  28. package/dist/report/react/DeltaTable.js +19 -18
  29. package/dist/report/react/EvalList.d.ts +3 -3
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +4 -2
  32. package/dist/report/react/ExperimentComparison.js +5 -4
  33. package/dist/report/react/ExperimentList.d.ts +3 -3
  34. package/dist/report/react/ExperimentList.js +16 -15
  35. package/dist/report/react/MetricBars.js +5 -4
  36. package/dist/report/react/MetricLine.js +12 -5
  37. package/dist/report/react/MetricMatrix.js +1 -1
  38. package/dist/report/react/MetricScatter.js +54 -17
  39. package/dist/report/react/MetricTable.js +2 -12
  40. package/dist/report/react/ScopeSummary.d.ts +10 -0
  41. package/dist/report/react/ScopeSummary.js +17 -0
  42. package/dist/report/react/Scoreboard.js +6 -6
  43. package/dist/report/react/cell.js +2 -2
  44. package/dist/report/react/fixtures.d.ts +5 -9
  45. package/dist/report/react/fixtures.js +105 -149
  46. package/dist/report/react/index.d.ts +15 -5
  47. package/dist/report/react/index.js +18 -7
  48. package/dist/report/report.d.ts +137 -20
  49. package/dist/report/report.js +261 -34
  50. package/dist/report/text/faces.d.ts +17 -19
  51. package/dist/report/text/faces.js +225 -157
  52. package/dist/report/text/plot.js +1 -1
  53. package/dist/report/text/table.js +2 -2
  54. package/dist/report/tree.d.ts +90 -40
  55. package/dist/report/tree.js +252 -94
  56. package/dist/report/types.d.ts +245 -300
  57. package/dist/report/types.js +4 -3
  58. package/dist/report/web.d.ts +21 -5
  59. package/dist/report/web.js +42 -16
  60. package/dist/results/select.d.ts +38 -16
  61. package/dist/results/select.js +73 -25
  62. package/dist/results/types.d.ts +38 -14
  63. package/dist/shared/aggregate.d.ts +3 -2
  64. package/dist/shared/aggregate.js +5 -4
  65. package/docs-site/zh/README.md +44 -0
  66. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  67. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  68. package/docs-site/zh/examples/index.mdx +50 -0
  69. package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
  70. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  71. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  72. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  73. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  74. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  75. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  76. package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
  77. package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
  78. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  79. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
  80. package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
  81. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  82. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
  83. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
  84. package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
  85. package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
  86. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
  87. package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
  88. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
  89. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
  90. package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
  91. package/docs-site/zh/index.mdx +23 -25
  92. package/docs-site/zh/introduction.mdx +8 -8
  93. package/docs-site/zh/reference/builtin-agents.mdx +5 -5
  94. package/docs-site/zh/reference/capabilities.mdx +6 -6
  95. package/docs-site/zh/reference/cli.mdx +9 -7
  96. package/docs-site/zh/reference/define-agent.mdx +1 -1
  97. package/docs-site/zh/reference/events.mdx +3 -3
  98. package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
  99. package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
  100. package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
  101. package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
  102. package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
  103. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  104. package/package.json +6 -2
  105. package/src/agents/index.ts +2 -2
  106. package/src/agents/openai-compat.ts +1 -1
  107. package/src/agents/streaming.ts +2 -2
  108. package/src/agents/types.ts +3 -3
  109. package/src/cli.ts +42 -23
  110. package/src/context/context.ts +1 -1
  111. package/src/context/session.test.ts +1 -1
  112. package/src/context/session.ts +1 -1
  113. package/src/i18n/en.ts +18 -16
  114. package/src/i18n/zh-CN.ts +16 -15
  115. package/src/report/aggregate.ts +175 -87
  116. package/src/report/built-in/index.tsx +9 -0
  117. package/src/report/components.tsx +625 -285
  118. package/src/report/compute.ts +717 -515
  119. package/src/report/dual-render.test.tsx +738 -1148
  120. package/src/report/flag.ts +97 -33
  121. package/src/report/format.ts +18 -22
  122. package/src/report/index.ts +113 -58
  123. package/src/report/load.ts +3 -2
  124. package/src/report/locale.ts +120 -69
  125. package/src/report/metrics.ts +42 -12
  126. package/src/report/primitives.tsx +190 -45
  127. package/src/report/react/AttemptList.tsx +32 -20
  128. package/src/report/react/DeltaTable.tsx +63 -45
  129. package/src/report/react/EvalList.tsx +0 -0
  130. package/src/report/react/ExperimentComparison.tsx +12 -7
  131. package/src/report/react/ExperimentList.tsx +38 -26
  132. package/src/report/react/MetricBars.tsx +5 -4
  133. package/src/report/react/MetricLine.tsx +13 -8
  134. package/src/report/react/MetricMatrix.tsx +2 -2
  135. package/src/report/react/MetricScatter.tsx +74 -20
  136. package/src/report/react/MetricTable.tsx +4 -76
  137. package/src/report/react/ScopeSummary.tsx +86 -0
  138. package/src/report/react/Scoreboard.tsx +28 -10
  139. package/src/report/react/cell.tsx +2 -2
  140. package/src/report/react/enhance.js +57 -5
  141. package/src/report/react/fixtures.ts +109 -156
  142. package/src/report/react/index.tsx +24 -39
  143. package/src/report/react/render.test.tsx +139 -104
  144. package/src/report/react/styles.css +181 -91
  145. package/src/report/report.test.ts +761 -1031
  146. package/src/report/report.ts +425 -47
  147. package/src/report/text/faces.ts +257 -164
  148. package/src/report/text/plot.ts +1 -1
  149. package/src/report/text/table.ts +2 -2
  150. package/src/report/tree.ts +362 -104
  151. package/src/report/types.ts +257 -287
  152. package/src/report/web.ts +63 -20
  153. package/src/results/attempt-evidence.test.ts +4 -4
  154. package/src/results/attempt-evidence.ts +5 -5
  155. package/src/results/copy.ts +6 -6
  156. package/src/results/host-equivalence.test.ts +26 -14
  157. package/src/results/index.ts +10 -4
  158. package/src/results/open.ts +8 -4
  159. package/src/results/results.test.ts +4 -3
  160. package/src/results/select.ts +104 -34
  161. package/src/results/types.ts +36 -14
  162. package/src/runner/feedback/human.test.ts +1 -1
  163. package/src/runner/run.ts +1 -1
  164. package/src/sandbox/cli-commands.ts +2 -2
  165. package/src/scoring/judge.test.ts +1 -1
  166. package/src/shared/aggregate.ts +5 -4
  167. package/src/show/compose.ts +50 -67
  168. package/src/show/index.ts +107 -56
  169. package/src/show/render.ts +43 -27
  170. package/src/show/report-host.test.ts +188 -0
  171. package/src/show/report-host.ts +375 -0
  172. package/src/show/show.test.ts +86 -36
  173. package/src/view/app/App.test.tsx +69 -0
  174. package/src/view/app/App.tsx +144 -48
  175. package/src/view/app/components/AttemptModal.tsx +324 -63
  176. package/src/view/app/components/CodeView.tsx +10 -4
  177. package/src/view/app/i18n.ts +31 -17
  178. package/src/view/app/lib/artifact-url.ts +14 -3
  179. package/src/view/app/main.tsx +13 -8
  180. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  181. package/src/view/app/types.ts +4 -1
  182. package/src/view/artifact-serving.test.ts +21 -1
  183. package/src/view/client-dist/app.css +1 -1
  184. package/src/view/client-dist/app.js +6 -6
  185. package/src/view/data.test.ts +9 -3
  186. package/src/view/data.ts +145 -49
  187. package/src/view/index.ts +48 -44
  188. package/src/view/server.ts +35 -15
  189. package/src/view/shared/types.ts +34 -5
  190. package/src/view/styles.css +224 -0
  191. package/src/view/view-report.test.ts +161 -57
  192. package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
  193. package/dist/report/built-ins/experiment-comparison.js +0 -119
  194. package/dist/report/built-ins/index.d.ts +0 -2
  195. package/dist/report/built-ins/index.js +0 -2
  196. package/dist/report/react/GroupSummary.d.ts +0 -8
  197. package/dist/report/react/GroupSummary.js +0 -8
  198. package/dist/report/react/RunOverview.d.ts +0 -8
  199. package/dist/report/react/RunOverview.js +0 -12
  200. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  201. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  202. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  203. package/docs-site/zh/example/showcase.mdx +0 -39
  204. package/src/report/built-in-user-parity.test.tsx +0 -597
  205. package/src/report/built-ins/experiment-comparison.tsx +0 -179
  206. package/src/report/built-ins/index.ts +0 -7
  207. package/src/report/react/GroupSummary.tsx +0 -66
  208. package/src/report/react/RunOverview.tsx +0 -109
  209. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  210. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  211. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  212. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  213. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  214. /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
  215. /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
  216. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
  217. /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
  218. /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
@@ -1,12 +1,13 @@
1
- import { Fragment, useEffect, useState } from "react";
1
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import type { ArtifactLoadState, T } from "../shared.ts";
3
- import type { ViewResult } from "../types.ts";
3
+ import type { Assertion, Span, ViewResult } from "../types.ts";
4
4
  import { artifactUrl } from "../lib/artifact-url.ts";
5
- import { asEvents, asSources } from "../lib/guards.ts";
5
+ import { asEvents, asSources, asSpans } from "../lib/guards.ts";
6
6
  import { verdictClass, verdictLabel } from "../lib/verdict.ts";
7
- import { CodeView, NoSourceBody } from "./CodeView.tsx";
7
+ import { AssertDetail, assertTone, CodeView, NoSourceBody } from "./CodeView.tsx";
8
+ import { formatScore } from "../lib/format.ts";
8
9
  import { CopyAttemptPrompt } from "./CopyControls.tsx";
9
- import { LazyArtifact } from "./LazyArtifact.tsx";
10
+ import { Trace } from "./Trace.tsx";
10
11
  import { Dialog, DialogClose, DialogContent, DialogTitle } from "./ui/dialog.tsx";
11
12
  import { Badge } from "./ui/badge.tsx";
12
13
 
@@ -14,6 +15,7 @@ export function AttemptModal({ result, onClose, t }: { result: ViewResult; onClo
14
15
  const allAssertions = result.assertions || [];
15
16
  const base = result.artifactBase;
16
17
  const [data, setData] = useState<ArtifactLoadState>({ sources: null, events: null, status: "loading" });
18
+ const trace = useTraceSpans(base, result.hasTrace);
17
19
 
18
20
  // Esc / 焦点陷阱 / 背景滚动锁 / 点遮罩关闭 都交给 Radix Dialog;这里只保留 artifact 拉取。
19
21
  useEffect(() => {
@@ -34,10 +36,12 @@ export function AttemptModal({ result, onClose, t }: { result: ViewResult; onClo
34
36
  const verdict = result.verdict;
35
37
  const hasCode = Boolean(data.sources?.length);
36
38
 
39
+ // 区块顺序对齐 docs/feature/reports/view.md「Attempt 详情」:判定与断言(errored 的结构化错误
40
+ // 就是判定原因,归这一区)→ 紧凑时间树 → diagnostics → usage → 源码/对话 → trace 入口。
37
41
  return (
38
42
  <Dialog open onOpenChange={(o) => { if (!o) onClose(); }}>
39
43
  <DialogContent aria-describedby={undefined}>
40
- <div className="flex min-w-0 shrink-0 items-center justify-between gap-3 border-b border-line px-[18px] pb-[11px] pt-[13px]">
44
+ <div className="flex min-w-0 shrink-0 items-center justify-between gap-3 border-b border-line px-7 pb-3 pt-4">
41
45
  <div className="flex min-w-0 flex-col gap-[3px]">
42
46
  <Badge tone={verdictClass(verdict)}>{verdictLabel(verdict, t)}</Badge>
43
47
  <DialogTitle asChild>
@@ -53,31 +57,33 @@ export function AttemptModal({ result, onClose, t }: { result: ViewResult; onClo
53
57
  x
54
58
  </DialogClose>
55
59
  </div>
56
- <div className="flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
60
+ <div className="flex-1 overflow-y-auto px-7 pb-7 pt-5">
61
+ {data.status !== "loading" && !hasCode ? <AssertionSection assertions={allAssertions} t={t} /> : null}
57
62
  {result.error ? (
58
63
  <div className="modal-error">
59
64
  <ErrorDetailBlock error={result.error} />
60
65
  </div>
61
66
  ) : null}
67
+ {result.phases && result.phases.length > 0 ? <PhaseTimingBlock phases={result.phases} trace={trace} t={t} /> : null}
62
68
  {result.diagnostics && result.diagnostics.length > 0 ? (
63
- <AttemptDiagnostics diagnostics={result.diagnostics} />
69
+ <AttemptDiagnostics diagnostics={result.diagnostics} t={t} />
64
70
  ) : null}
65
- {result.phases && result.phases.length > 0 ? <PhaseTimingBlock phases={result.phases} /> : null}
66
71
  <UsageDiffLine result={result} />
67
72
  {data.status === "loading" ? <div className="conv-loading">{t("trace.loading")}</div> : null}
68
73
  {hasCode ? (
69
74
  <CodeView sources={data.sources ?? []} events={data.events || []} assertions={allAssertions} t={t} />
70
75
  ) : data.status !== "loading" ? (
71
76
  // hasSources 为真却取不到 → 源码捕获过,是 artifact 文件在当前托管里缺失;和「从未捕获」分开提示。
77
+ // 断言明细此时由断言区(detailMode)内嵌呈现,这里只保留说明与会话流,不再重复 checks。
72
78
  <NoSourceBody
73
- assertions={allAssertions}
79
+ assertions={[]}
74
80
  events={data.events || []}
75
81
  message={t(result.hasSources && base ? "code.sourceUnavailable" : "code.noSource")}
76
82
  t={t}
77
83
  />
78
84
  ) : null}
79
- {result.hasTrace && base ? (
80
- <LazyArtifact type="trace" src={`${base}/trace.json`} t={t} />
85
+ {trace.available ? (
86
+ <FullTraceEntry trace={trace} t={t} />
81
87
  ) : data.status !== "loading" ? (
82
88
  <div className="mt-3 text-xs text-muted">
83
89
  {t("trace.enableHint")}
@@ -92,6 +98,113 @@ export function AttemptModal({ result, onClose, t }: { result: ViewResult; onClo
92
98
  );
93
99
  }
94
100
 
101
+ /* ───────────────────────── 判定与断言 ───────────────────────── */
102
+
103
+ /** 断言的源码位置标注(fallback 下无代码可跳,纯文本)。 */
104
+ function locLabel(a: Assertion): React.ReactNode {
105
+ const loc = a.loc;
106
+ if (!loc) return null;
107
+ const short = loc.file.split("/").pop() ?? loc.file;
108
+ return (
109
+ <span className="assert-loc" title={`${loc.file}:${loc.line}`}>
110
+ {short}:{loc.line}
111
+ </span>
112
+ );
113
+ }
114
+
115
+ /**
116
+ * 断言区 = 源码视图的 fallback(见 docs/feature/reports/view.md「Attempt 详情」):断言明细
117
+ * 单点住在源码行里,只有源码不可用(未捕获或 artifact 缺失)时这里才出现,呈现同一份事实——
118
+ * failed / unavailable 与没过阈值的 soft 每条一行、一次展开明细,第一条 failed 默认展开;
119
+ * passed 按 group 收进默认折叠区,只显示数量。
120
+ */
121
+ function AssertionSection({ assertions, t }: { assertions: Assertion[]; t: T }) {
122
+ if (!assertions.length) return null;
123
+ const attention = assertions.filter((a) => a.outcome !== "passed");
124
+ const passed = assertions.filter((a) => a.outcome === "passed");
125
+ const firstFailed = attention.findIndex((a) => a.outcome === "failed");
126
+ const openIdx = firstFailed === -1 ? 0 : firstFailed;
127
+ const groups = new Map<string, Assertion[]>();
128
+ for (const a of passed) {
129
+ const g = a.groupPath?.join(" > ") ?? "";
130
+ groups.set(g, [...(groups.get(g) ?? []), a]);
131
+ }
132
+ return (
133
+ <div className="attempt-asserts">
134
+ <div className="modal-sect">{t("code.checks")}</div>
135
+ {attention.length ? (
136
+ <div className="attempt-asserts-open">
137
+ {attention.map((a, i) => (
138
+ <FailureRow key={i} a={a} defaultOpen={i === openIdx} t={t} />
139
+ ))}
140
+ </div>
141
+ ) : null}
142
+ {passed.length ? (
143
+ <details className="asserts-passed">
144
+ <summary>{t("assert.passedCollapsed", { count: passed.length })}</summary>
145
+ {[...groups.entries()].map(([g, list]) => (
146
+ <details key={g || "·"} className="asserts-passed-group">
147
+ <summary>
148
+ {g || "—"} · {list.length}
149
+ </summary>
150
+ <AssertDetail asserts={list.map(stripGroupPath)} t={t} anchor={locLabel} />
151
+ </details>
152
+ ))}
153
+ </details>
154
+ ) : null}
155
+ </div>
156
+ );
157
+ }
158
+
159
+ /** 折叠区已按 group 分组,行内不再重复组前缀。 */
160
+ function stripGroupPath(a: Assertion): Assertion {
161
+ const { groupPath: _gp, ...rest } = a;
162
+ return rest as Assertion;
163
+ }
164
+
165
+ /** fallback 断言区里的一行:标题(徽章/名字/matcher/分数/位置),明细一次展开可见。 */
166
+ function FailureRow({ a, defaultOpen, t }: { a: Assertion; defaultOpen: boolean; t: T }) {
167
+ return (
168
+ <details className="fail-row" open={defaultOpen || undefined}>
169
+ <summary className="fail-sum">
170
+ <span className={`abadge ${assertTone(a)}`}>
171
+ {a.outcome === "unavailable" ? t("assert.unavailable") : t("assert.fail")}
172
+ </span>
173
+ <span className="fail-name">
174
+ {a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : ""}
175
+ {a.name}
176
+ </span>
177
+ {a.detail && a.detail !== a.name ? <span className="fail-matcher">{a.detail}</span> : null}
178
+ {a.optional ? <span className="assert-sev">{t("assert.optional")}</span> : null}
179
+ {a.severity === "soft" ? <span className="assert-sev">{t("assert.soft")}</span> : null}
180
+ {a.outcome !== "unavailable" && a.threshold !== undefined ? (
181
+ <span className="assert-score">
182
+ {formatScore(a.score)} / {formatScore(a.threshold)}
183
+ </span>
184
+ ) : null}
185
+ {locLabel(a)}
186
+ </summary>
187
+ <div className="fail-body">
188
+ {a.outcome === "unavailable" ? <div className="assert-reason">{a.reason}</div> : null}
189
+ {a.outcome !== "unavailable" && a.expected !== undefined ? (
190
+ <div className="assert-reason">expected: {a.expected}</div>
191
+ ) : null}
192
+ {a.outcome !== "unavailable" && a.received !== undefined ? (
193
+ <div className="assert-reason">received: {a.received}</div>
194
+ ) : null}
195
+ {a.outcome !== "unavailable" && a.evidence ? (
196
+ <details className="assert-evidence">
197
+ <summary>{t("assert.evidence")}</summary>
198
+ <pre className="assert-evidence-pre">{a.evidence}</pre>
199
+ </details>
200
+ ) : null}
201
+ </div>
202
+ </details>
203
+ );
204
+ }
205
+
206
+ /* ───────────────────────── 结构化错误与 diagnostics ───────────────────────── */
207
+
95
208
  /** errored attempt 的结构化 error 明细(见 docs/feature/reports/view.md「结构化错误」)。结构化
96
209
  * `AttemptError`:operation / code / message + 可选 cause / stack。字段标签是低层技术标识,与终端
97
210
  * `niceeval show` 一样保持英文,不进 view 的 i18n 词典。 */
@@ -122,7 +235,7 @@ function ErrorDetailBlock({ error }: { error: NonNullable<ViewResult["error"]> }
122
235
 
123
236
  /** attempt 级诊断,按 lifecycle 阶段分组(teardown/cleanup 等,与 verdict 独立;
124
237
  * 见 docs/feature/reports/view.md「Attempt 详情」)。 */
125
- function AttemptDiagnostics({ diagnostics }: { diagnostics: NonNullable<ViewResult["diagnostics"]> }) {
238
+ function AttemptDiagnostics({ diagnostics, t }: { diagnostics: NonNullable<ViewResult["diagnostics"]>; t: T }) {
126
239
  const groups = new Map<string, typeof diagnostics>();
127
240
  for (const d of diagnostics) {
128
241
  const list = groups.get(d.phase) ?? [];
@@ -130,7 +243,7 @@ function AttemptDiagnostics({ diagnostics }: { diagnostics: NonNullable<ViewResu
130
243
  }
131
244
  return (
132
245
  <div className="mt-3">
133
- <div className="text-xs font-[640] text-text">diagnostics</div>
246
+ <div className="modal-sect">{t("attempt.diagnostics")}</div>
134
247
  {[...groups.entries()].map(([phase, list]) => (
135
248
  <div key={phase} className="mt-1">
136
249
  <div className="text-[11px] text-muted">{phase}</div>
@@ -153,6 +266,8 @@ function AttemptDiagnostics({ diagnostics }: { diagnostics: NonNullable<ViewResu
153
266
  );
154
267
  }
155
268
 
269
+ /* ───────────────────────── 统一时间树 ───────────────────────── */
270
+
156
271
  const CLOSING_PHASES = new Set(["eval.teardown", "agent.teardown", "sandbox.teardown", "sandbox.suspend", "sandbox.stop"]);
157
272
 
158
273
  function fmtMs(ms: number): string {
@@ -161,75 +276,221 @@ function fmtMs(ms: number): string {
161
276
  return `${ms}ms`;
162
277
  }
163
278
 
164
- /** 统一时间树(见 docs/feature/reports/view.md「Attempt 详情」):`result.json.phases` 的主链
165
- * 分解与收尾段;每个 phase 可展开 runner 直接观察到的 hook / 命令 / turn(嵌套 children)。 */
166
- function PhaseTimingBlock({ phases }: { phases: NonNullable<ViewResult["phases"]> }) {
279
+ type PhaseEntry = NonNullable<ViewResult["phases"]>[number];
280
+ type TimingChild = NonNullable<PhaseEntry["children"]>[number];
281
+
282
+ /** trace.json 的共享按需装载:时间树的 turn 挂接与底部 trace 入口用同一份,只 fetch 一次。 */
283
+ interface TraceHook {
284
+ available: boolean;
285
+ status: "idle" | "loading" | "ready" | "failed";
286
+ spans: Span[] | null;
287
+ load: () => void;
288
+ }
289
+
290
+ function useTraceSpans(base: string | undefined, hasTrace: boolean | undefined): TraceHook {
291
+ const [state, setState] = useState<{ spans: Span[] | null; status: TraceHook["status"] }>({ spans: null, status: "idle" });
292
+ const started = useRef(false);
293
+ const available = Boolean(base && hasTrace);
294
+ const load = useCallback(() => {
295
+ if (started.current || !base) return;
296
+ started.current = true;
297
+ setState({ spans: null, status: "loading" });
298
+ fetch(artifactUrl(`${base}/trace.json`))
299
+ .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
300
+ .then((body) => setState({ spans: asSpans(body) ?? [], status: "ready" }))
301
+ .catch(() => setState({ spans: null, status: "failed" }));
302
+ }, [base]);
303
+ return useMemo(() => ({ available, load, ...state }), [available, load, state]);
304
+ }
305
+
306
+ const seriesClass = (i: number, failed?: true): string => (failed ? "phase-seg-bad" : `phase-seg-${(i % 6) + 1}`);
307
+
308
+ /**
309
+ * 统一时间树(见 docs/feature/reports/view.md「Attempt 详情」):整区一个折叠块,收合时只占
310
+ * 一行(标题 + 总耗时),有失败 phase 的 attempt 默认展开;展开后主链先画一条分解条,列表里
311
+ * 每个 phase 的 children(hook / 沙箱命令 / session/turn)默认收合、可逐个展开。
312
+ * 收尾段单列,不计入总耗时。
313
+ */
314
+ function PhaseTimingBlock({ phases, trace, t }: { phases: NonNullable<ViewResult["phases"]>; trace: TraceHook; t: T }) {
167
315
  const main = phases.filter((p) => !CLOSING_PHASES.has(p.name));
168
316
  const closing = phases.filter((p) => CLOSING_PHASES.has(p.name));
317
+ const total = main.reduce((sum, p) => sum + p.durationMs, 0);
318
+ const anyFailed = phases.some((p) => p.failed);
319
+ const [open, setOpen] = useState(anyFailed);
169
320
  return (
170
- <div className="mt-3">
171
- <div className="text-xs font-[640] text-text">timing</div>
172
- <ul className="mt-1 space-y-0.5">
173
- {main.map((p, i) => (
174
- <TimingRow key={i} name={p.name} durationMs={p.durationMs} failed={p.failed} children_={p.children} />
175
- ))}
176
- </ul>
177
- {closing.length > 0 ? (
178
- <div className="mt-1">
179
- <div className="text-[11px] text-muted">teardown (not counted in total)</div>
180
- <ul className="mt-0.5 space-y-0.5">
181
- {closing.map((p, i) => (
182
- <TimingRow key={i} name={p.name} durationMs={p.durationMs} failed={p.failed} children_={p.children} />
321
+ <details className="attempt-timing" open={open} onToggle={(e) => setOpen(e.currentTarget.open)}>
322
+ <summary className="timing-sum">
323
+ <span className="modal-sect">{t("attempt.timing")}</span>
324
+ <span className="phase-dur num">{fmtMs(total)}</span>
325
+ {anyFailed ? <span className="phase-fail">✗</span> : null}
326
+ </summary>
327
+ {open ? (
328
+ <>
329
+ {total > 0 ? (
330
+ <div className="phase-bar" aria-hidden="true">
331
+ {main.map((p, i) => (
332
+ <div
333
+ key={i}
334
+ className={`phase-seg ${seriesClass(i, p.failed)}`}
335
+ style={{ flexGrow: Math.max(p.durationMs, total / 200) }}
336
+ title={`${p.name} ${fmtMs(p.durationMs)}`}
337
+ />
338
+ ))}
339
+ </div>
340
+ ) : null}
341
+ <ul className="phase-list">
342
+ {main.map((p, i) => (
343
+ <PhaseRow key={i} phase={p} dotClass={seriesClass(i, p.failed)} trace={trace} t={t} />
183
344
  ))}
184
345
  </ul>
185
- </div>
346
+ {closing.length > 0 ? (
347
+ <div className="phase-closing">
348
+ <div className="phase-closing-head">{t("attempt.teardown")}</div>
349
+ <ul className="phase-list">
350
+ {closing.map((p, i) => (
351
+ <PhaseRow key={i} phase={p} trace={trace} t={t} />
352
+ ))}
353
+ </ul>
354
+ </div>
355
+ ) : null}
356
+ </>
186
357
  ) : null}
187
- </div>
358
+ </details>
188
359
  );
189
360
  }
190
361
 
191
- type TimingChild = NonNullable<NonNullable<ViewResult["phases"]>[number]["children"]>[number];
192
-
193
- function TimingRow({
194
- name,
195
- durationMs,
196
- failed,
197
- children_,
198
- }: {
199
- name: string;
200
- durationMs: number;
201
- failed?: true;
202
- children_?: TimingChild[];
203
- }) {
204
- const kids = children_ ?? [];
205
- const label = (node: TimingChild): string =>
206
- node.kind === "command" && node.command ? `shell · ${node.command.display}` : node.kind === "turn" ? `turn ${node.label}` : node.label;
207
- const renderChild = (node: TimingChild, depth: number): React.ReactNode => (
208
- <li key={node.id} className="text-[11px]" style={{ paddingLeft: `${depth * 12}px` }}>
209
- <span className="text-muted">{label(node)}</span> {fmtMs(node.durationMs)}
210
- {node.failed ? <span className="text-bad"> ✗</span> : null}
211
- {(node.children ?? []).map((c) => renderChild(c, depth + 1))}
362
+ function PhaseRow({ phase, dotClass, trace, t }: { phase: PhaseEntry; dotClass?: string; trace: TraceHook; t: T }) {
363
+ const kids = phase.children ?? [];
364
+ // 失败 phase 默认展开:失败线索直接给出,其余保持收合(契约:children 默认收合)。
365
+ const [open, setOpen] = useState(Boolean(phase.failed));
366
+ const head = (
367
+ <>
368
+ {dotClass ? <span className={`phase-dot ${dotClass}`} /> : <span className="phase-dot phase-dot-none" />}
369
+ <span className="phase-name">{phase.name}</span>
370
+ <span className="phase-dur num">{fmtMs(phase.durationMs)}</span>
371
+ {phase.failed ? <span className="phase-fail">✗</span> : null}
372
+ {kids.length > 0 ? <span className="phase-kids num">{countNodes(kids)}</span> : null}
373
+ </>
374
+ );
375
+ if (kids.length === 0) return <li className="phase-row">{head}</li>;
376
+ return (
377
+ <li>
378
+ <details className="phase-details" open={open} onToggle={(e) => setOpen(e.currentTarget.open)}>
379
+ <summary className="phase-row">{head}</summary>
380
+ {open ? (
381
+ <ul className="timing-kids">
382
+ {kids.map((node) => (
383
+ <TimingNodeRow key={node.id} node={node} trace={trace} t={t} />
384
+ ))}
385
+ </ul>
386
+ ) : null}
387
+ </details>
212
388
  </li>
213
389
  );
390
+ }
391
+
392
+ function countNodes(nodes: TimingChild[]): number {
393
+ return nodes.reduce((sum, n) => sum + 1 + countNodes(n.children ?? []), 0);
394
+ }
395
+
396
+ function TimingNodeRow({ node, trace, t }: { node: TimingChild; trace: TraceHook; t: T }) {
397
+ const kids = node.children ?? [];
398
+ // turn 带 traceId 且这次运行有 trace.json 时,展开即挂接同一轮的 agent/model/tool spans。
399
+ const hooksTrace = node.kind === "turn" && Boolean(node.traceId) && trace.available;
400
+ const [open, setOpen] = useState(Boolean(node.failed) && (kids.length > 0 || hooksTrace));
401
+ const label =
402
+ node.kind === "command" && node.command ? `shell · ${node.command.display}` : node.kind === "turn" ? `turn ${node.label}` : node.label;
403
+ const head = (
404
+ <>
405
+ <span className="tnode-label" title={label}>{label}</span>
406
+ <span className="phase-dur num">{fmtMs(node.durationMs)}</span>
407
+ {node.failed ? <span className="phase-fail">✗</span> : null}
408
+ </>
409
+ );
410
+ if (kids.length === 0 && !hooksTrace) return <li className="tnode-row">{head}</li>;
214
411
  return (
215
- <li className="text-xs">
216
- <span>{name}</span> <span className="text-muted">{fmtMs(durationMs)}</span>
217
- {failed ? <span className="text-bad"> ✗</span> : null}
218
- {kids.length > 0 ? <ul className="mt-0.5 space-y-0.5 pl-3">{kids.map((c) => renderChild(c, 0))}</ul> : null}
412
+ <li>
413
+ <details className="phase-details" open={open} onToggle={(e) => setOpen(e.currentTarget.open)}>
414
+ <summary className="tnode-row">{head}</summary>
415
+ {open ? (
416
+ <>
417
+ {kids.length > 0 ? (
418
+ <ul className="timing-kids">
419
+ {kids.map((child) => (
420
+ <TimingNodeRow key={child.id} node={child} trace={trace} t={t} />
421
+ ))}
422
+ </ul>
423
+ ) : null}
424
+ {hooksTrace ? <TurnSpans traceId={node.traceId!} trace={trace} t={t} /> : null}
425
+ </>
426
+ ) : null}
427
+ </details>
219
428
  </li>
220
429
  );
221
430
  }
222
431
 
223
- /** usage diff 入口行(见 docs/feature/reports/view.md「Attempt 详情」) */
432
+ function TurnSpans({ traceId, trace, t }: { traceId: string; trace: TraceHook; t: T }) {
433
+ useEffect(() => { trace.load(); }, [trace]);
434
+ if (trace.status === "idle" || trace.status === "loading") return <div className="tnode-meta">{t("trace.loading")}</div>;
435
+ if (trace.status === "failed") return <div className="tnode-meta">{t("trace.loadFailed")}</div>;
436
+ const spans = (trace.spans ?? []).filter((s) => s.traceId === traceId);
437
+ if (!spans.length) return <div className="tnode-meta">{t("trace.noSpans")}</div>;
438
+ return (
439
+ <div className="tnode-trace">
440
+ <Trace spans={spans} t={t} />
441
+ </div>
442
+ );
443
+ }
444
+
445
+ /** 底部 trace 入口:被测 agent 的完整原始 span 瀑布,与时间树共享同一次 trace.json 拉取。 */
446
+ function FullTraceEntry({ trace, t }: { trace: TraceHook; t: T }) {
447
+ const [open, setOpen] = useState(false);
448
+ return (
449
+ <details
450
+ className="trace-details"
451
+ open={open}
452
+ onToggle={(e) => {
453
+ const isOpen = e.currentTarget.open;
454
+ setOpen(isOpen);
455
+ if (isOpen) trace.load();
456
+ }}
457
+ >
458
+ <summary>{t("trace.timing")}</summary>
459
+ <div className="trace-slot">
460
+ {trace.status === "failed" ? (
461
+ <div className="trace-span-meta">{t("trace.loadFailed")}</div>
462
+ ) : trace.status !== "ready" ? (
463
+ <div className="trace-span-meta">{t("trace.loading")}</div>
464
+ ) : (
465
+ <Trace spans={trace.spans ?? []} t={t} />
466
+ )}
467
+ </div>
468
+ </details>
469
+ );
470
+ }
471
+
472
+ /* ───────────────────────── usage 行 ───────────────────────── */
473
+
474
+ /** usage / sandbox 元信息 chips(见 docs/feature/reports/view.md「Attempt 详情」)。 */
224
475
  function UsageDiffLine({ result }: { result: ViewResult }) {
225
- const parts: string[] = [];
476
+ const chips: React.ReactNode[] = [];
226
477
  if (result.usage) {
227
478
  const tok = result.usage.inputTokens + result.usage.outputTokens;
228
- parts.push(`usage: ${tok.toLocaleString()} tok${result.usage.costUSD !== undefined ? ` · $${result.usage.costUSD.toFixed(4)}` : ""}`);
479
+ chips.push(
480
+ <span className="meta-chip" key="usage">
481
+ usage <b>{tok.toLocaleString()} tok</b>
482
+ {result.usage.costUSD !== undefined ? <> · ${result.usage.costUSD.toFixed(4)}</> : null}
483
+ </span>,
484
+ );
229
485
  }
230
486
  if (result.sandbox) {
231
- parts.push(`sandbox: ${result.sandbox.provider} · ${result.sandbox.sandboxId}${result.sandbox.kept ? " · kept" : ""}`);
487
+ chips.push(
488
+ <span className="meta-chip" key="sandbox">
489
+ sandbox <b>{result.sandbox.provider}</b> · {result.sandbox.sandboxId}
490
+ {result.sandbox.kept ? " · kept" : ""}
491
+ </span>,
492
+ );
232
493
  }
233
- if (parts.length === 0) return null;
234
- return <div className="mt-2 text-xs text-muted">{parts.join(" ")}</div>;
494
+ if (chips.length === 0) return null;
495
+ return <div className="meta-chips">{chips}</div>;
235
496
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo, useState } from "react";
1
+ import { useCallback, useMemo, useState, type ReactNode } from "react";
2
2
  import { AlertCircle, CheckCircle2, ChevronRight, MessageCircle, XCircle } from "lucide-react";
3
3
  import type { T } from "../shared.ts";
4
4
  import type { Assertion, CodeSource, SourceTurn, TranscriptEvent } from "../types.ts";
@@ -8,7 +8,7 @@ import { InputBlock, ToolBlock, Transcript } from "./Transcript.tsx";
8
8
 
9
9
  /** soft 断言没过阈值不影响 verdict,颜色上跟 gate 失败(红)区分开,用 warn(黄);
10
10
  * unavailable 用独立第三态(非红非绿)。 */
11
- function assertTone(a: Assertion): "good" | "warn" | "bad" | "na" {
11
+ export function assertTone(a: Assertion): "good" | "warn" | "bad" | "na" {
12
12
  if (a.outcome === "unavailable") return "na";
13
13
  if (a.outcome === "passed") return "good";
14
14
  return a.severity === "soft" ? "warn" : "bad";
@@ -17,7 +17,12 @@ function assertTone(a: Assertion): "good" | "warn" | "bad" | "na" {
17
17
  export function CodeView({ sources, events, assertions, t }: { sources: CodeSource[]; events: TranscriptEvent[]; assertions: Assertion[]; t: T }) {
18
18
  const turns = useMemo(() => indexTurns(events), [events]);
19
19
  const asserts = useMemo(() => indexAsserts(assertions), [assertions]);
20
- const [open, setOpen] = useState<Set<string>>(() => new Set());
20
+ // 断言明细单点住在源码行(契约 docs/feature/reports/view.md「Attempt 详情」):
21
+ // 第一条失败行默认展开,打开页面即见「为什么失败」,不用逐行找红行点开。
22
+ const [open, setOpen] = useState<Set<string>>(() => {
23
+ const first = assertions.find((a) => a.outcome === "failed" && a.loc);
24
+ return new Set(first?.loc ? [locKey(first.loc.file, first.loc.line)] : []);
25
+ });
21
26
  const toggle = useCallback((k: string) => {
22
27
  setOpen((prev) => {
23
28
  const next = new Set(prev);
@@ -223,7 +228,7 @@ export function ReplyPanel({ turn, t }: { turn: SourceTurn; t: T }) {
223
228
  );
224
229
  }
225
230
 
226
- export function AssertDetail({ asserts, t }: { asserts: Assertion[]; t: T }) {
231
+ export function AssertDetail({ asserts, t, anchor }: { asserts: Assertion[]; t: T; anchor?: (a: Assertion) => ReactNode }) {
227
232
  return (
228
233
  <div className="line-detail assert-detail">
229
234
  {asserts.map((a: Assertion, i: number) => (
@@ -234,6 +239,7 @@ export function AssertDetail({ asserts, t }: { asserts: Assertion[]; t: T }) {
234
239
  <span className="assert-name">
235
240
  {a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : ""}
236
241
  {a.name}
242
+ {anchor?.(a)}
237
243
  </span>
238
244
  {a.optional ? <span className="assert-sev">{t("assert.optional")}</span> : null}
239
245
  {a.severity === "soft" ? <span className="assert-sev">{t("assert.soft")}</span> : null}