niceeval 0.6.1 → 0.7.0

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 (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
@@ -1,12 +1,13 @@
1
- import { 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,22 +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]">
57
- {result.error ? <div className="modal-error">{result.error}</div> : null}
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}
62
+ {result.error ? (
63
+ <div className="modal-error">
64
+ <ErrorDetailBlock error={result.error} />
65
+ </div>
66
+ ) : null}
67
+ {result.phases && result.phases.length > 0 ? <PhaseTimingBlock phases={result.phases} trace={trace} t={t} /> : null}
68
+ {result.diagnostics && result.diagnostics.length > 0 ? (
69
+ <AttemptDiagnostics diagnostics={result.diagnostics} t={t} />
70
+ ) : null}
71
+ <UsageDiffLine result={result} />
58
72
  {data.status === "loading" ? <div className="conv-loading">{t("trace.loading")}</div> : null}
59
73
  {hasCode ? (
60
74
  <CodeView sources={data.sources ?? []} events={data.events || []} assertions={allAssertions} t={t} />
61
75
  ) : data.status !== "loading" ? (
62
76
  // hasSources 为真却取不到 → 源码捕获过,是 artifact 文件在当前托管里缺失;和「从未捕获」分开提示。
77
+ // 断言明细此时由断言区(detailMode)内嵌呈现,这里只保留说明与会话流,不再重复 checks。
63
78
  <NoSourceBody
64
- assertions={allAssertions}
79
+ assertions={[]}
65
80
  events={data.events || []}
66
81
  message={t(result.hasSources && base ? "code.sourceUnavailable" : "code.noSource")}
67
82
  t={t}
68
83
  />
69
84
  ) : null}
70
- {result.hasTrace && base ? (
71
- <LazyArtifact type="trace" src={`${base}/trace.json`} t={t} />
85
+ {trace.available ? (
86
+ <FullTraceEntry trace={trace} t={t} />
72
87
  ) : data.status !== "loading" ? (
73
88
  <div className="mt-3 text-xs text-muted">
74
89
  {t("trace.enableHint")}
@@ -82,3 +97,400 @@ export function AttemptModal({ result, onClose, t }: { result: ViewResult; onClo
82
97
  </Dialog>
83
98
  );
84
99
  }
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
+
208
+ /** errored attempt 的结构化 error 明细(见 docs/feature/reports/view.md「结构化错误」)。结构化
209
+ * `AttemptError`:operation / code / message + 可选 cause / stack。字段标签是低层技术标识,与终端
210
+ * `niceeval show` 一样保持英文,不进 view 的 i18n 词典。 */
211
+ function ErrorDetailBlock({ error }: { error: NonNullable<ViewResult["error"]> }) {
212
+ const rows: [string, string][] = [
213
+ ["phase", error.phase],
214
+ ["code", error.code],
215
+ ["message", error.message],
216
+ ];
217
+ if (error.cause) rows.push(["cause", error.cause.name ? `${error.cause.name} · ${error.cause.message}` : error.cause.message]);
218
+ const stack = error.stack?.replace(/\n+$/, "") ?? "";
219
+ return (
220
+ <div>
221
+ <dl className="grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-xs">
222
+ {rows.map(([k, v]) => (
223
+ <Fragment key={k}>
224
+ <dt className="text-muted">{k}</dt>
225
+ <dd className="min-w-0 break-words">{v}</dd>
226
+ </Fragment>
227
+ ))}
228
+ </dl>
229
+ {stack ? (
230
+ <pre className="mt-2 overflow-x-auto whitespace-pre-wrap text-[11px] text-muted">{stack}</pre>
231
+ ) : null}
232
+ </div>
233
+ );
234
+ }
235
+
236
+ /** attempt 级诊断,按 lifecycle 阶段分组(teardown/cleanup 等,与 verdict 独立;
237
+ * 见 docs/feature/reports/view.md「Attempt 详情」)。 */
238
+ function AttemptDiagnostics({ diagnostics, t }: { diagnostics: NonNullable<ViewResult["diagnostics"]>; t: T }) {
239
+ const groups = new Map<string, typeof diagnostics>();
240
+ for (const d of diagnostics) {
241
+ const list = groups.get(d.phase) ?? [];
242
+ groups.set(d.phase, [...list, d]);
243
+ }
244
+ return (
245
+ <div className="mt-3">
246
+ <div className="modal-sect">{t("attempt.diagnostics")}</div>
247
+ {[...groups.entries()].map(([phase, list]) => (
248
+ <div key={phase} className="mt-1">
249
+ <div className="text-[11px] text-muted">{phase}</div>
250
+ <ul className="mt-0.5 space-y-1 pl-3">
251
+ {list.map((d, i) => (
252
+ <li key={i} className="text-xs">
253
+ <span className="text-muted">
254
+ {d.level} · {d.code}
255
+ </span>
256
+ <div className="min-w-0 break-words">
257
+ {d.message}
258
+ {d.count && d.count > 1 ? ` (${d.count} occurrences)` : ""}
259
+ </div>
260
+ </li>
261
+ ))}
262
+ </ul>
263
+ </div>
264
+ ))}
265
+ </div>
266
+ );
267
+ }
268
+
269
+ /* ───────────────────────── 统一时间树 ───────────────────────── */
270
+
271
+ const CLOSING_PHASES = new Set(["eval.teardown", "agent.teardown", "sandbox.teardown", "sandbox.suspend", "sandbox.stop"]);
272
+
273
+ function fmtMs(ms: number): string {
274
+ if (ms >= 60_000) return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`;
275
+ if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
276
+ return `${ms}ms`;
277
+ }
278
+
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 }) {
315
+ const main = phases.filter((p) => !CLOSING_PHASES.has(p.name));
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);
320
+ return (
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} />
344
+ ))}
345
+ </ul>
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
+ </>
357
+ ) : null}
358
+ </details>
359
+ );
360
+ }
361
+
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>
388
+ </li>
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>;
411
+ return (
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>
428
+ </li>
429
+ );
430
+ }
431
+
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 详情」)。 */
475
+ function UsageDiffLine({ result }: { result: ViewResult }) {
476
+ const chips: React.ReactNode[] = [];
477
+ if (result.usage) {
478
+ const tok = result.usage.inputTokens + result.usage.outputTokens;
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
+ );
485
+ }
486
+ if (result.sandbox) {
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
+ );
493
+ }
494
+ if (chips.length === 0) return null;
495
+ return <div className="meta-chips">{chips}</div>;
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";
@@ -6,16 +6,23 @@ import { highlightTs, indexAsserts, indexTurns, locKey } from "../lib/transcript
6
6
  import { formatScore } from "../lib/format.ts";
7
7
  import { InputBlock, ToolBlock, Transcript } from "./Transcript.tsx";
8
8
 
9
- /** soft 断言没过阈值不影响 verdict,颜色上跟 gate 失败(红)区分开,用 warn(黄)。 */
10
- function assertTone(a: Assertion): "good" | "warn" | "bad" {
11
- if (a.passed) return "good";
9
+ /** soft 断言没过阈值不影响 verdict,颜色上跟 gate 失败(红)区分开,用 warn(黄);
10
+ * unavailable 用独立第三态(非红非绿) */
11
+ export function assertTone(a: Assertion): "good" | "warn" | "bad" | "na" {
12
+ if (a.outcome === "unavailable") return "na";
13
+ if (a.outcome === "passed") return "good";
12
14
  return a.severity === "soft" ? "warn" : "bad";
13
15
  }
14
16
 
15
17
  export function CodeView({ sources, events, assertions, t }: { sources: CodeSource[]; events: TranscriptEvent[]; assertions: Assertion[]; t: T }) {
16
18
  const turns = useMemo(() => indexTurns(events), [events]);
17
19
  const asserts = useMemo(() => indexAsserts(assertions), [assertions]);
18
- 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
+ });
19
26
  const toggle = useCallback((k: string) => {
20
27
  setOpen((prev) => {
21
28
  const next = new Set(prev);
@@ -118,13 +125,16 @@ export function CodeLine({
118
125
  }) {
119
126
  const hasReply = !!turn;
120
127
  const hasAsserts = !!(asserts && asserts.length);
121
- // 只有 gate 断言没过才算这一行真的"fail";只剩 soft 断言没过阈值时是"warn"(不影响 verdict)
128
+ // 只有 gate 断言没过才算这一行真的"fail";只剩 soft 断言没过阈值时是"warn"(不影响 verdict);
129
+ // 只剩 unavailable 时是第三态 "na"(非红非绿)。
122
130
  const status = hasAsserts
123
- ? asserts?.every((a: Assertion) => a.passed)
131
+ ? asserts?.every((a: Assertion) => a.outcome === "passed")
124
132
  ? "pass"
125
- : asserts?.some((a: Assertion) => !a.passed && a.severity === "gate")
133
+ : asserts?.some((a: Assertion) => a.outcome === "failed" && a.severity === "gate")
126
134
  ? "fail"
127
- : "warn"
135
+ : asserts?.some((a: Assertion) => a.outcome === "failed")
136
+ ? "warn"
137
+ : "na"
128
138
  : null;
129
139
  const clickable = hasReply || hasAsserts;
130
140
  const rowCls =
@@ -149,6 +159,8 @@ export function CodeLine({
149
159
  <CheckCircle2 className="gstat good" aria-hidden="true" />
150
160
  ) : status === "warn" ? (
151
161
  <AlertCircle className="gstat warn" aria-hidden="true" />
162
+ ) : status === "na" ? (
163
+ <AlertCircle className="gstat na" aria-hidden="true" />
152
164
  ) : (
153
165
  <XCircle className="gstat bad" aria-hidden="true" />
154
166
  )
@@ -174,6 +186,7 @@ export function CodeLine({
174
186
 
175
187
  /** 行尾分数徽章:judge / 带阈值的断言显示分数(过绿不过红);纯 gate 断言靠行色 + gutter 勾叉。 */
176
188
  export function AssertBadge({ a }: { a: Assertion }) {
189
+ if (a.outcome === "unavailable") return <span className="abadge na">n/a</span>;
177
190
  const showPct = a.threshold !== undefined || (a.score > 0 && a.score < 1);
178
191
  if (!showPct) return null;
179
192
  return (
@@ -215,21 +228,35 @@ export function ReplyPanel({ turn, t }: { turn: SourceTurn; t: T }) {
215
228
  );
216
229
  }
217
230
 
218
- export function AssertDetail({ asserts, t }: { asserts: Assertion[]; t: T }) {
231
+ export function AssertDetail({ asserts, t, anchor }: { asserts: Assertion[]; t: T; anchor?: (a: Assertion) => ReactNode }) {
219
232
  return (
220
233
  <div className="line-detail assert-detail">
221
234
  {asserts.map((a: Assertion, i: number) => (
222
235
  <div key={i} className="assert-row">
223
- <span className={`abadge ${assertTone(a)}`}>{a.passed ? t("assert.pass") : t("assert.fail")}</span>
224
- <span className="assert-name">{a.name}</span>
236
+ <span className={`abadge ${assertTone(a)}`}>
237
+ {a.outcome === "unavailable" ? t("assert.unavailable") : a.outcome === "passed" ? t("assert.pass") : t("assert.fail")}
238
+ </span>
239
+ <span className="assert-name">
240
+ {a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : ""}
241
+ {a.name}
242
+ {anchor?.(a)}
243
+ </span>
244
+ {a.optional ? <span className="assert-sev">{t("assert.optional")}</span> : null}
225
245
  {a.severity === "soft" ? <span className="assert-sev">{t("assert.soft")}</span> : null}
226
- {a.threshold !== undefined ? (
246
+ {a.outcome !== "unavailable" && a.threshold !== undefined ? (
227
247
  <span className="assert-score">
228
248
  {formatScore(a.score)} / {formatScore(a.threshold)}
229
249
  </span>
230
250
  ) : null}
251
+ {a.outcome === "unavailable" ? <div className="assert-reason">{a.reason}</div> : null}
231
252
  {a.detail ? <div className="assert-reason">{a.detail}</div> : null}
232
- {a.evidence ? (
253
+ {a.outcome !== "unavailable" && a.expected !== undefined ? (
254
+ <div className="assert-reason">expected: {a.expected}</div>
255
+ ) : null}
256
+ {a.outcome !== "unavailable" && a.received !== undefined ? (
257
+ <div className="assert-reason">received: {a.received}</div>
258
+ ) : null}
259
+ {a.outcome !== "unavailable" && a.evidence ? (
233
260
  <details className="assert-evidence">
234
261
  <summary>{t("assert.evidence")}</summary>
235
262
  <pre className="assert-evidence-pre">{a.evidence}</pre>
@@ -3,7 +3,7 @@ import { Check, Copy } from "lucide-react";
3
3
  import type { T } from "../shared.ts";
4
4
  import type { ViewResult, ViewSnapshot } from "../types.ts";
5
5
  import { snapshotLabel } from "../lib/rows.ts";
6
- import { failingAssertions, reasonFor } from "../lib/verdict.ts";
6
+ import { reasonFor } from "../lib/verdict.ts";
7
7
 
8
8
  /** 修复 prompt 的一条失败条目;路径均相对 view 输入根(默认 `.niceeval/`)。 */
9
9
  export interface FixPromptEntry {
@@ -20,7 +20,7 @@ export function toFixPromptEntry(r: ViewResult, experimentLabel: string): FixPro
20
20
  experiment: r.experimentId ?? experimentLabel,
21
21
  evalId: r.id,
22
22
  verdict: r.verdict,
23
- reason: reasonFor(r, failingAssertions(r)),
23
+ reason: reasonFor(r),
24
24
  artifactBase: r.artifactBase,
25
25
  // result.json 与其它 artifact(events.json 等)同目录落盘,resultPath 因此复用同一个
26
26
  // artifactBase(不再靠 attemptRef 的 {snapshot, attempt} 两段拼——locator 是不透明的,