niceeval 0.10.0 → 0.10.3-canary.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 (71) hide show
  1. package/INDEX.md +4 -1
  2. package/README.md +1 -1
  3. package/README.zh.md +1 -1
  4. package/dist/report/components/attempt-detail/AttemptAssertions.js +2 -1
  5. package/dist/report/components/attempt-detail/AttemptConversation.d.ts +5 -1
  6. package/dist/report/components/attempt-detail/AttemptConversation.js +42 -2
  7. package/dist/report/components/attempt-detail/AttemptSource.js +84 -13
  8. package/dist/report/components/attempt-detail/compute.js +44 -1
  9. package/dist/report/components/attempt-detail/faces.js +4 -0
  10. package/dist/report/components/attempt-detail/index.d.ts +2 -2
  11. package/dist/report/components/attempt-detail/index.js +24 -4
  12. package/dist/report/components/entity-lists/ExperimentList.d.ts +1 -2
  13. package/dist/report/components/entity-lists/ExperimentList.js +6 -5
  14. package/dist/report/components/entity-lists/faces.d.ts +1 -1
  15. package/dist/report/components/entity-lists/faces.js +10 -9
  16. package/dist/report/components/entity-lists/index.d.ts +5 -7
  17. package/dist/report/components/entity-lists/index.js +7 -3
  18. package/dist/report/components/metric-views/MetricScatter.js +2 -38
  19. package/dist/report/index.d.ts +1 -1
  20. package/dist/report/model/format.d.ts +5 -5
  21. package/dist/report/model/format.js +34 -8
  22. package/dist/report/model/types.d.ts +16 -2
  23. package/dist/report/react/index.d.ts +1 -1
  24. package/dist/runner/types.d.ts +2 -1
  25. package/dist/sandbox/e2b.d.ts +0 -1
  26. package/dist/scoring/display.d.ts +7 -1
  27. package/dist/scoring/display.js +22 -2
  28. package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +5 -5
  29. package/docs-site/zh/explanation/runner.mdx +10 -2
  30. package/docs-site/zh/index.mdx +1 -1
  31. package/docs-site/zh/reference/cli.mdx +2 -2
  32. package/docs-site/zh/reference/expect.mdx +21 -1
  33. package/docs-site/zh/reference/report-components.mdx +3 -1
  34. package/docs-site/zh/troubleshooting/debugging.mdx +8 -8
  35. package/docs-site/zh/troubleshooting/recover-after-kill.mdx +62 -0
  36. package/docs-site/zh/tutorials/agent-onboarding.mdx +14 -2
  37. package/docs-site/zh/tutorials/experiments.mdx +1 -2
  38. package/docs-site/zh/tutorials/quickstart.mdx +2 -2
  39. package/docs-site/zh/tutorials/sandbox-providers.mdx +27 -0
  40. package/docs-site/zh/tutorials/viewing-results.mdx +13 -10
  41. package/docs-site/zh/tutorials/write-experiment.mdx +1 -3
  42. package/package.json +3 -4
  43. package/src/cli.ts +10 -2
  44. package/src/expect/index.test.ts +39 -0
  45. package/src/expect/index.ts +33 -0
  46. package/src/o11y/prices.json +176 -99
  47. package/src/report/assets/styles.css +822 -0
  48. package/src/report/components/attempt-detail/AttemptAssertions.tsx +4 -3
  49. package/src/report/components/attempt-detail/AttemptConversation.tsx +55 -7
  50. package/src/report/components/attempt-detail/AttemptSource.tsx +187 -42
  51. package/src/report/components/attempt-detail/attempt-components.test.tsx +82 -4
  52. package/src/report/components/attempt-detail/compute.ts +50 -1
  53. package/src/report/components/attempt-detail/faces.ts +3 -0
  54. package/src/report/components/attempt-detail/index.tsx +33 -16
  55. package/src/report/components/attempt-detail/validate.test.ts +19 -2
  56. package/src/report/components/entity-lists/ExperimentList.tsx +13 -10
  57. package/src/report/components/entity-lists/faces.ts +10 -9
  58. package/src/report/components/entity-lists/index.tsx +7 -10
  59. package/src/report/components/metric-views/MetricScatter.tsx +2 -38
  60. package/src/report/components/render.test.tsx +19 -9
  61. package/src/report/index.ts +2 -0
  62. package/src/report/model/format.ts +33 -8
  63. package/src/report/model/types.ts +18 -2
  64. package/src/report/react/index.tsx +2 -0
  65. package/src/report/runtime/dual-render.test.tsx +43 -10
  66. package/src/runner/types.ts +2 -1
  67. package/src/sandbox/e2b-reconcile.test.ts +126 -0
  68. package/src/sandbox/e2b.ts +38 -2
  69. package/src/scoring/display.test.ts +26 -0
  70. package/src/scoring/display.ts +24 -2
  71. package/src/view/view-report.test.ts +3 -1
package/INDEX.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  按当前任务从树里挑 1–3 页读取(通常是一页 tutorial 或 explanation 搭一页 reference);页面再引用其它概念或参考时,继续读取包内文件。
10
10
 
11
+ 随包文档目前只有中文。不要为了找英文版而去抓官网或 GitHub——那里的页面可能对应另一个版本,「另一个版本的英文页」比「当前版本的中文页」更危险。中文页读起来没有障碍就直接读;与用户交流仍用用户的语言。
12
+
11
13
  <!-- GENERATED:BEGIN bundled-docs-tree -->
12
14
 
13
15
  <!-- 本文件是构建产物(pnpm run build:index),勿手改:树区文案改对应页面的 frontmatter title/description,导语改 INDEX.template.md(生成逻辑见 scripts/generate-reference.ts) -->
@@ -56,7 +58,7 @@
56
58
  - `docs-site/zh/reference/define-config.mdx` — defineConfig:项目默认配置:defineConfig 参考:judge、reporters、并发、超时和 sandbox 默认值。
57
59
  - `docs-site/zh/reference/define-eval.mdx` — defineEval:声明、配置并运行 NiceEval 评估用例:defineEval 参考:选项、test context t、Turn 返回值、Sandbox helper,以及数组和 keyed record 数据集导出。
58
60
  - `docs-site/zh/reference/events.mdx` — 标准事件流参考:StreamEvent 的十种事件:每种的字段、什么时候吐、哪些断言消费它。adapter 的核心工作就是产出这条流。
59
- - `docs-site/zh/reference/expect.mdx` — niceeval/expect matchers 与自定义断言参考:niceeval/expect 参考:includes、equals、matches、similarity、satisfies。可链式调用 .gate() 或 .atLeast(0.7),也可以用 makeAssertion 构建自定义 matcher。
61
+ - `docs-site/zh/reference/expect.mdx` — niceeval/expect matchers 与自定义断言参考:niceeval/expect 参考:includes、equals、matches、similarity、satisfies,以及形状断言 includesUrl、hasSections。可链式调用 .gate() 或 .atLeast(0.7),也可以用 makeAssertion 构建自定义 matcher。
60
62
  - `docs-site/zh/reference/official-adapters.mdx` — 官方适配器一览:NiceEval 内置的 Sandbox 和非 Sandbox 适配器分别是什么、怎么鉴权,Sandbox 型里怎么装 MCP server、Skill、插件,怎么使用 Agent 官方配置文件。
61
63
  - `docs-site/zh/reference/report-components.mdx` — 报告组件一览:报告文件里能摆的全部官方双面组件:每个组件回答什么问题、怎么调用、网页面长什么样、终端字符输出长什么样。
62
64
  - `docs-site/zh/reference/results-data.mdx` — 用 niceeval/results 直接读写结果数据:报告积木脚下的数据层:openResults 把 .niceeval/ 的落盘 artifact parse 成「实验 → 结果快照 → 评估 → attempt」的类型化层次,createResultsWriter 把别家结果写成 NiceEval 格式,copySnapshots 负责发布瘦身。
@@ -65,6 +67,7 @@
65
67
 
66
68
  - `docs-site/zh/troubleshooting/debug-sandbox.mdx` — 保留 Sandbox 现场排查问题:用 --keep-sandbox 把失败 Attempt 的 Sandbox 保留成可随时唤醒的现场,用 niceeval sandbox enter 进去手动排查,用 sandbox list / stop 查看和清理。
67
69
  - `docs-site/zh/troubleshooting/debugging.mdx` — 排查失败与复盘历史运行:一份按场景组织的排查手册:断言失败怎么定位、环境错误怎么进 Sandbox、agent 改了什么怎么看、旧的运行怎么翻出来复盘——每一步都有命令顺序和输出示例。
70
+ - `docs-site/zh/troubleshooting/recover-after-kill.mdx` — 运行被强杀后恢复:进程被 kill -9、CI 超时或断电杀掉后,重跑同一条命令续跑没跑完的部分,用 niceeval sandbox prune 收回没清理的容器,用 --teardown 补齐实验收尾。
68
71
 
69
72
  ## `docs-site/zh/examples/`
70
73
 
package/README.md CHANGED
@@ -129,7 +129,7 @@ pnpm exec niceeval view // view eval results
129
129
  ## Quick Start
130
130
 
131
131
  ```text
132
- READ https://niceeval.com/INIT.md and install niceeval for this repo.
132
+ READ https://niceeval.com/INIT.md and set up niceeval for this repo: install it, integrate it with this project, and run the first eval end to end.
133
133
  ```
134
134
 
135
135
  Start from the scenario that matches what you need to evaluate:
package/README.zh.md CHANGED
@@ -129,7 +129,7 @@ pnpm exec niceeval view // 查看评估结果
129
129
  ## 快速开始
130
130
 
131
131
  ```text
132
- READ https://niceeval.com/INIT.md and install niceeval for this repo.
132
+ READ https://niceeval.com/INIT.md and set up niceeval for this repo: install it, integrate it with this project, and run the first eval end to end.
133
133
  ```
134
134
 
135
135
  从你的场景开始:
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { stripControl } from "../../../scoring/display.js";
2
3
  import { cx } from "../shared.js";
3
4
  function assertTone(a) {
4
5
  if (a.outcome === "unavailable")
@@ -8,7 +9,7 @@ function assertTone(a) {
8
9
  return a.severity === "soft" ? "warn" : "bad";
9
10
  }
10
11
  function AssertionRow({ a }) {
11
- return (_jsxs("details", { className: "nre-assertion-row", open: true, children: [_jsxs("summary", { children: [_jsx("span", { className: `nre-assertion-badge nre-tone-${assertTone(a)}`, children: a.outcome === "unavailable" ? "unavailable" : a.outcome }), _jsxs("span", { className: "nre-assertion-name", children: [a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : "", a.name] }), a.detail && a.detail !== a.name ? _jsx("span", { className: "nre-assertion-detail", children: a.detail }) : null] }), _jsxs("div", { className: "nre-assertion-body", children: [a.outcome === "unavailable" ? _jsx("div", { children: a.reason }) : null, a.outcome !== "unavailable" && a.expected !== undefined ? _jsxs("div", { children: ["expected: ", a.expected] }) : null, a.outcome !== "unavailable" && a.received !== undefined ? _jsxs("div", { children: ["received: ", a.received] }) : null] })] }));
12
+ return (_jsxs("details", { className: "nre-assertion-row", open: true, children: [_jsxs("summary", { children: [_jsx("span", { className: `nre-assertion-badge nre-tone-${assertTone(a)}`, children: a.outcome === "unavailable" ? "unavailable" : a.outcome }), _jsxs("span", { className: "nre-assertion-name", children: [a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : "", a.name] }), a.detail && a.detail !== a.name ? _jsx("span", { className: "nre-assertion-detail", children: a.detail }) : null] }), _jsxs("div", { className: "nre-assertion-body", children: [a.outcome === "unavailable" ? _jsx("div", { children: a.reason === undefined ? undefined : stripControl(a.reason) }) : null, a.outcome !== "unavailable" && a.expected !== undefined ? _jsxs("div", { children: ["expected: ", stripControl(a.expected)] }) : null, a.outcome !== "unavailable" && a.received !== undefined ? _jsxs("div", { children: ["received: ", stripControl(a.received)] }) : null] })] }));
12
13
  }
13
14
  export function AttemptAssertions({ data, className, }) {
14
15
  if (data === null)
@@ -1,5 +1,9 @@
1
1
  import type { ReactElement } from "react";
2
- import type { AttemptConversationData } from "../../model/types.ts";
2
+ import type { AttemptConversationData, AttemptConversationReply } from "../../model/types.ts";
3
+ /** AttemptSource 复用同一份回复 renderer,把一轮执行挂回对应的 send 源码行。 */
4
+ export declare function ConversationReplies({ replies }: {
5
+ replies: AttemptConversationReply[];
6
+ }): ReactElement;
3
7
  export declare function AttemptConversation({ data, className, }: {
4
8
  data: AttemptConversationData | null;
5
9
  className?: string;
@@ -1,5 +1,38 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../shared.js";
3
+ const TOOL_VERB = {
4
+ shell: "Bash",
5
+ file_read: "Read",
6
+ file_write: "Write",
7
+ file_edit: "Edit",
8
+ web_fetch: "Fetch",
9
+ web_search: "Search",
10
+ glob: "Glob",
11
+ grep: "Grep",
12
+ list_dir: "List",
13
+ agent_task: "Task",
14
+ };
15
+ function compact(value, max = 140) {
16
+ if (value === undefined || value === null)
17
+ return "";
18
+ const text = typeof value === "string" ? value : JSON.stringify(value);
19
+ const oneLine = text.replace(/\s+/g, " ").trim();
20
+ return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
21
+ }
22
+ function toolPrimaryArg(input) {
23
+ if (typeof input === "string")
24
+ return compact(input, 80);
25
+ if (input === null || Array.isArray(input) || typeof input !== "object")
26
+ return "";
27
+ for (const key of ["command", "cmd", "path", "file", "file_path", "pattern", "query", "url", "prompt", "description"]) {
28
+ const value = input[key];
29
+ if (typeof value === "string" && value)
30
+ return compact(value, 80);
31
+ if (key === "command" && Array.isArray(value))
32
+ return compact(value.filter((item) => typeof item === "string").join(" "), 80);
33
+ }
34
+ return compact(input, 80);
35
+ }
3
36
  function ReplyRow({ reply }) {
4
37
  switch (reply.kind) {
5
38
  case "assistant":
@@ -13,7 +46,10 @@ function ReplyRow({ reply }) {
13
46
  case "skill":
14
47
  return (_jsxs("div", { className: "nre-conv-skill", children: [_jsx("span", { className: "nre-conv-role", children: "skill loaded" }), " ", reply.skill] }));
15
48
  case "tool":
16
- return (_jsxs("details", { className: "nre-conv-tool", children: [_jsxs("summary", { children: [reply.name, reply.status ? ` · ${reply.status}` : ""] }), _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.input, null, 2) }), reply.output !== undefined ? _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.output, null, 2) }) : null] }));
49
+ const verb = (reply.tool ? TOOL_VERB[reply.tool] : undefined) ?? reply.name;
50
+ const arg = toolPrimaryArg(reply.input);
51
+ const preview = compact(reply.output);
52
+ return (_jsxs("details", { className: "nre-conv-tool", children: [_jsxs("summary", { children: [_jsx("span", { className: cx("nre-conv-tool-dot", reply.status ? `nre-conv-tool-${reply.status}` : "nre-conv-tool-pending") }), _jsx("span", { className: "nre-conv-tool-name", title: arg ? `${verb}(${arg})` : verb, children: arg ? `${verb}(${arg})` : verb }), _jsxs("span", { className: "nre-conv-tool-preview", children: [reply.status ?? "pending", preview ? ` · ${preview}` : ""] })] }), _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.input, null, 2) }), reply.output !== undefined ? _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.output, null, 2) }) : null] }));
17
53
  case "subagent":
18
54
  return (_jsxs("details", { className: "nre-conv-subagent", children: [_jsxs("summary", { children: ["subagent ", reply.name, reply.status ? ` · ${reply.status}` : ""] }), reply.output !== undefined ? _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.output, null, 2) }) : null] }));
19
55
  case "input":
@@ -24,8 +60,12 @@ function ReplyRow({ reply }) {
24
60
  return (_jsxs("details", { className: "nre-conv-raw", children: [_jsx("summary", { children: "unrecognized event" }), _jsx("pre", { className: "nre-conv-tool-io", children: JSON.stringify(reply.raw, null, 2) })] }));
25
61
  }
26
62
  }
63
+ /** AttemptSource 复用同一份回复 renderer,把一轮执行挂回对应的 send 源码行。 */
64
+ export function ConversationReplies({ replies }) {
65
+ return (_jsx("div", { className: "nre-conv-replies", children: replies.map((reply, i) => (_jsx(ReplyRow, { reply: reply }, i))) }));
66
+ }
27
67
  function RoundCard({ round, index }) {
28
- return (_jsxs("div", { className: "nre-conv-round", children: [_jsxs("div", { className: "nre-conv-round-head", children: ["round ", index + 1, round.loc ? (_jsxs("span", { className: "nre-conv-round-loc", title: `${round.loc.file}:${round.loc.line}`, children: [round.loc.file.split("/").pop(), ":", round.loc.line] })) : null] }), round.sentText ? _jsx("div", { className: "nre-conv-sent", children: round.sentText }) : null, _jsx("div", { className: "nre-conv-replies", children: round.replies.map((reply, i) => (_jsx(ReplyRow, { reply: reply }, i))) })] }));
68
+ return (_jsxs("div", { className: "nre-conv-round", children: [_jsxs("div", { className: "nre-conv-round-head", children: ["round ", index + 1, round.loc ? (_jsxs("span", { className: "nre-conv-round-loc", title: `${round.loc.file}:${round.loc.line}`, children: [round.loc.file.split("/").pop(), ":", round.loc.line] })) : null] }), round.sentText ? _jsx("div", { className: "nre-conv-sent", children: round.sentText }) : null, _jsx(ConversationReplies, { replies: round.replies })] }));
29
69
  }
30
70
  export function AttemptConversation({ data, className, }) {
31
71
  if (data === null)
@@ -1,5 +1,28 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { stripControl } from "../../../scoring/display.js";
2
3
  import { cx } from "../shared.js";
4
+ import { ConversationReplies } from "./AttemptConversation.js";
5
+ const TS_HL_RE = /(\/\/[^\n]*)|(\/\*[^]*?\*\/)|(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(import|from|export|default|const|let|var|async|await|function|return|if|else|for|of|in|new|class|extends|typeof|void|true|false|null|undefined)\b|\b(\d[\d_.]*)\b|([A-Za-z_$][\w$]*)(?=\s*\()/g;
6
+ /** 逐行零依赖 TS 高亮;token class 是稳定的 web 展示语义,不改源码文本。 */
7
+ function highlightTs(line) {
8
+ const out = [];
9
+ let last = 0;
10
+ let i = 0;
11
+ let match;
12
+ TS_HL_RE.lastIndex = 0;
13
+ while ((match = TS_HL_RE.exec(line))) {
14
+ if (match.index > last)
15
+ out.push(line.slice(last, match.index));
16
+ const tokenClass = match[1] || match[2] ? "tok-comment" : match[3] ? "tok-str" : match[4] ? "tok-kw" : match[5] ? "tok-num" : "tok-fn";
17
+ out.push(_jsx("span", { className: tokenClass, children: match[0] }, i++));
18
+ last = match.index + match[0].length;
19
+ if (match[0].length === 0)
20
+ TS_HL_RE.lastIndex++;
21
+ }
22
+ if (last < line.length)
23
+ out.push(line.slice(last));
24
+ return out;
25
+ }
3
26
  function assertTone(a) {
4
27
  if (a.outcome === "unavailable")
5
28
  return "na";
@@ -7,21 +30,69 @@ function assertTone(a) {
7
30
  return "good";
8
31
  return a.severity === "soft" ? "warn" : "bad";
9
32
  }
33
+ function lineTone(line) {
34
+ if (line.assertions.length === 0)
35
+ return undefined;
36
+ if (line.assertions.some((a) => assertTone(a) === "bad"))
37
+ return "bad";
38
+ if (line.assertions.some((a) => assertTone(a) === "warn"))
39
+ return "warn";
40
+ if (line.assertions.some((a) => assertTone(a) === "na"))
41
+ return "na";
42
+ return "good";
43
+ }
44
+ function formatDuration(ms) {
45
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
46
+ }
47
+ function thresholdScores(assertions) {
48
+ const scores = [];
49
+ for (const assertion of assertions) {
50
+ if (assertion.outcome !== "unavailable" && assertion.threshold !== undefined) {
51
+ scores.push(`${assertion.score}/${assertion.threshold}`);
52
+ }
53
+ }
54
+ return scores;
55
+ }
56
+ /** 行号位标记(与产品站示例卡同语言:图标顶替行号,不加独立状态列);内联 SVG,零图标依赖。 */
57
+ const MARK_ICONS = {
58
+ send: _jsx("path", { d: "M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z" }),
59
+ good: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "m9 12 2 2 4-4" })] })),
60
+ bad: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "m15 9-6 6" }), _jsx("path", { d: "m9 9 6 6" })] })),
61
+ warn: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), _jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })] })),
62
+ na: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }), _jsx("path", { d: "M12 17h.01" })] })),
63
+ };
64
+ function LineNo({ line, tone, send }) {
65
+ const kind = tone ?? (send ? "send" : null);
66
+ if (kind === null)
67
+ return _jsx("span", { className: "nre-source-ln", children: line });
68
+ const label = kind === "bad" ? "failed" : kind === "warn" ? "soft failed" : kind === "good" ? "passed" : kind === "na" ? "unavailable" : "send";
69
+ return (_jsx("span", { className: "nre-source-ln nre-source-ln-mark", role: "img", "aria-label": label, title: label, children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: MARK_ICONS[kind] }) }));
70
+ }
71
+ function LineSummary({ line, tone }) {
72
+ const interactive = line.assertions.length > 0 || line.sends.length > 0 || line.turns.length > 0;
73
+ const scores = thresholdScores(line.assertions);
74
+ return (_jsxs("span", { className: "nre-source-line-summary", children: [_jsx(LineNo, { line: line.line, tone: tone, send: line.sends.length > 0 || line.turns.length > 0 }), _jsx("code", { className: "nre-source-text", children: highlightTs(line.text) }), _jsxs("span", { className: "nre-source-line-meta", children: [scores.length > 0 ? _jsx("span", { className: "nre-source-score-badge", children: scores.join(", ") }) : null, interactive ? _jsx("span", { className: "nre-source-chevron", children: "\u203A" }) : null] })] }));
75
+ }
76
+ function TurnDetail({ turn, showMeta = false, showSent = false }) {
77
+ return (_jsxs("div", { className: cx("nre-source-turn", `nre-source-turn-${turn.status}`), children: [showMeta ? (_jsxs("div", { className: "nre-source-turn-head", children: [_jsx("span", { children: turn.label }), _jsx("span", { children: turn.status }), turn.durationMs === undefined ? null : _jsx("span", { children: formatDuration(turn.durationMs) })] })) : null, showSent && turn.sentText ? _jsx("div", { className: "nre-conv-sent", children: turn.sentText }) : null, _jsx(ConversationReplies, { replies: turn.replies })] }));
78
+ }
79
+ function AssertionDetail({ assertion }) {
80
+ return (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(assertion)}`, children: [_jsxs("div", { className: "nre-source-assertion-head", children: [_jsx("span", { className: "nre-assertion-badge", children: assertion.outcome }), _jsx("span", { className: "nre-assertion-name", children: assertion.name }), assertion.outcome !== "unavailable" ? _jsx("span", { className: "nre-source-assertion-score", children: assertion.score }) : null] }), assertion.detail ? _jsx("div", { className: "nre-assertion-detail", children: assertion.detail }) : null, assertion.outcome === "unavailable" ? (_jsxs("div", { className: "nre-assertion-body", children: ["reason: ", assertion.reason === undefined ? undefined : stripControl(assertion.reason)] })) : assertion.expected !== undefined || assertion.received !== undefined || assertion.evidence !== undefined ? (_jsxs("div", { className: "nre-assertion-body", children: [assertion.expected !== undefined ? _jsxs("span", { children: ["expected: ", stripControl(assertion.expected)] }) : null, assertion.received !== undefined ? _jsxs("span", { children: ["received: ", stripControl(assertion.received)] }) : null, assertion.evidence !== undefined ? _jsxs("span", { children: ["evidence: ", stripControl(assertion.evidence)] }) : null] })) : null] }));
81
+ }
10
82
  export function AttemptSource({ data, className }) {
11
83
  if (data === null)
12
84
  return null;
85
+ const firstAttentionLine = data.lines.find((line) => {
86
+ const tone = lineTone(line);
87
+ return tone === "bad" || tone === "warn" || tone === "na";
88
+ })?.line;
13
89
  return (_jsxs("div", { className: cx("nre", "nre-attempt-source", className), children: [_jsx("div", { className: "nre-attempt-source-head", children: data.sourcePath }), _jsx("div", { className: "nre-attempt-source-lines", children: data.lines.map((line) => {
14
- const hasAsserts = line.assertions.length > 0;
15
- const hasSends = line.sends.length > 0;
16
- const worstTone = hasAsserts
17
- ? line.assertions.some((a) => assertTone(a) === "bad")
18
- ? "bad"
19
- : line.assertions.some((a) => assertTone(a) === "warn")
20
- ? "warn"
21
- : line.assertions.some((a) => assertTone(a) === "na")
22
- ? "na"
23
- : "good"
24
- : undefined;
25
- return (_jsxs("details", { className: cx("nre-source-line", worstTone ? `nre-tone-${worstTone}` : undefined, hasSends ? "nre-source-line-send" : undefined), open: hasAsserts && (worstTone === "bad" || worstTone === "warn"), children: [_jsxs("summary", { children: [_jsx("span", { className: "nre-source-ln", children: line.line }), _jsx("code", { className: "nre-source-text", children: line.text }), hasSends ? _jsxs("span", { className: "nre-source-send-mark", children: ["\u21B3 ", line.sends.map((s) => s.label).join(", ")] }) : null] }), hasAsserts ? (_jsx("div", { className: "nre-source-line-detail", children: line.assertions.map((a, i) => (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(a)}`, children: [_jsx("span", { className: "nre-assertion-name", children: a.name }), a.outcome === "unavailable" ? (_jsx("span", { children: a.reason })) : (_jsxs(_Fragment, { children: [a.expected !== undefined ? _jsxs("span", { children: ["expected: ", a.expected] }) : null, a.received !== undefined ? _jsxs("span", { children: ["received: ", a.received] }) : null] }))] }, i))) })) : null] }, line.line));
26
- }) }), data.unmapped.length > 0 ? (_jsxs("div", { className: "nre-attempt-source-unmapped", children: [_jsx("div", { className: "nre-attempt-source-unmapped-head", children: "Other assertions" }), data.unmapped.map((a, i) => (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(a)}`, children: [a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : "", a.name] }, i)))] })) : null] }));
90
+ const tone = lineTone(line);
91
+ const interactive = line.assertions.length > 0 || line.sends.length > 0 || line.turns.length > 0;
92
+ const lineClass = cx("nre-source-line", tone ? `nre-tone-${tone}` : undefined, line.sends.length > 0 || line.turns.length > 0 ? "nre-source-line-send" : undefined);
93
+ if (!interactive) {
94
+ return (_jsx("div", { className: lineClass, children: _jsx(LineSummary, { line: line, tone: tone }) }, line.line));
95
+ }
96
+ return (_jsxs("details", { className: lineClass, open: line.line === firstAttentionLine, children: [_jsx("summary", { children: _jsx(LineSummary, { line: line, tone: tone }) }), _jsxs("div", { className: "nre-source-line-detail", children: [line.turns.map((turn, i) => (_jsx(TurnDetail, { turn: turn }, `${turn.label}-${i}`))), line.assertions.map((assertion, i) => (_jsx(AssertionDetail, { assertion: assertion }, i)))] })] }, line.line));
97
+ }) }), data.unmapped.length > 0 ? (_jsxs("div", { className: "nre-attempt-source-unmapped", children: [_jsx("div", { className: "nre-attempt-source-unmapped-head", children: "Other assertions" }), data.unmapped.map((a, i) => (_jsxs("div", { className: `nre-assertion-row nre-tone-${assertTone(a)}`, children: [a.groupPath?.length ? `${a.groupPath.join(" > ")} · ` : "", a.name] }, i)))] })) : null, data.unlocatedTurns.length > 0 ? (_jsxs("div", { className: "nre-attempt-source-unlocated", children: [_jsx("div", { className: "nre-attempt-source-unmapped-head", children: "Other conversation" }), data.unlocatedTurns.map((turn, i) => (_jsx(TurnDetail, { turn: turn, showMeta: true, showSent: true }, `${turn.label}-${i}`)))] })) : null] }));
27
98
  }
@@ -48,7 +48,50 @@ export function attemptSourceData(evidence) {
48
48
  if (!evidence.capabilities.source || evidence.evalSource === null)
49
49
  return null;
50
50
  const { sourcePath, lines, unmapped, summary } = evidence.evalSource;
51
- return { locator: evidence.locator, sourcePath, lines, unmapped, summary };
51
+ const projectedLines = lines.map((line) => ({
52
+ ...line,
53
+ turns: line.sends.map((send) => ({
54
+ label: send.label,
55
+ status: send.status,
56
+ ...(send.durationMs === undefined ? {} : { durationMs: send.durationMs }),
57
+ sentText: "",
58
+ replies: [],
59
+ })),
60
+ }));
61
+ const usedTurns = new Map();
62
+ const unlocatedTurns = [];
63
+ const conversation = attemptConversationData(evidence);
64
+ for (const [roundIndex, round] of (conversation?.rounds ?? []).entries()) {
65
+ const status = round.replies.some((reply) => reply.kind === "error" ||
66
+ ((reply.kind === "tool" || reply.kind === "subagent") && reply.status === "failed"))
67
+ ? "failed"
68
+ : round.replies.some((reply) => reply.kind === "input")
69
+ ? "waiting"
70
+ : "completed";
71
+ const fallback = {
72
+ label: `t${roundIndex + 1}`,
73
+ status,
74
+ sentText: round.sentText,
75
+ replies: round.replies,
76
+ };
77
+ const loc = round.loc;
78
+ if (!loc || loc.file !== sourcePath || loc.line < 1 || loc.line > projectedLines.length) {
79
+ unlocatedTurns.push(fallback);
80
+ continue;
81
+ }
82
+ const line = projectedLines[loc.line - 1];
83
+ const turnIndex = usedTurns.get(loc.line) ?? 0;
84
+ usedTurns.set(loc.line, turnIndex + 1);
85
+ const annotated = line.turns[turnIndex];
86
+ if (annotated) {
87
+ annotated.sentText = round.sentText;
88
+ annotated.replies = round.replies;
89
+ }
90
+ else {
91
+ line.turns.push(fallback);
92
+ }
93
+ }
94
+ return { locator: evidence.locator, sourcePath, lines: projectedLines, unmapped, unlocatedTurns, summary };
52
95
  }
53
96
  // ───────────────────────── AttemptFixPrompt ─────────────────────────
54
97
  /** 单条 attempt 版的批量修复 prompt(与 CopyFixPrompt 的多条版本同一份步骤文案)。 */
@@ -85,6 +85,10 @@ export function attemptSourceText(data, ctx) {
85
85
  const headerParts = [`${data.sourcePath} · ${data.summary.annotatedLines}/${data.summary.totalLines} lines annotated`];
86
86
  if (command)
87
87
  headerParts.push(command);
88
+ const hasConversation = data.unlocatedTurns.length > 0 || data.lines.some((line) => line.turns.length > 0);
89
+ const executionCommand = hasConversation ? evidenceCommand(ctx, data.locator, "--execution") : null;
90
+ if (executionCommand)
91
+ headerParts.push(executionCommand);
88
92
  // 源码锚由 assertionLine 自己按 a.loc 拼(与 AttemptAssertions 共用同一份逻辑);这里只负责
89
93
  // 挑出非 passed 的条目,不重复算锚点。
90
94
  const failed = data.lines.flatMap((line) => line.assertions.filter((a) => a.outcome !== "passed"));
@@ -34,5 +34,5 @@ export declare function validateDiffData(data: unknown): string | null;
34
34
  export declare const AttemptDiff: ReportComponent<AttemptSectionProps<AttemptDiffData>>;
35
35
  /** source / assertions fallback:有 source 放 AttemptSource,否则放 AttemptAssertions。 */
36
36
  export declare const AttemptAssessment: ReportComponent<Record<string, never>>;
37
- /** 内建排列顺序;不产生新的 data 或渲染面,用户可以在自己的 attempt-input page 里重排这些叶子。 */
38
- export declare const AttemptDetail: ReportComponent<unknown>;
37
+ /** 内建排列顺序;有 source 时回复已按 loc 展开在 AttemptSource 行内,不再重复一份 round 卡。 */
38
+ export declare const AttemptDetail: ReportComponent<Record<string, never>>;
@@ -205,7 +205,7 @@ export const AttemptAssertions = makeAttemptComponent({
205
205
  /** AnnotatedSourceLine(src/results/annotated-source.ts):一行源码 + 映射到这一行的断言 / send 标注。 */
206
206
  function annotatedSourceLineProblem(value, path) {
207
207
  if (!isObject(value))
208
- return `"${path}" must be an AnnotatedSourceLine { line, text, assertions, sends }`;
208
+ return `"${path}" must be an AttemptSourceLineData { line, text, assertions, sends, turns }`;
209
209
  if (typeof value.line !== "number")
210
210
  return `"${path}.line" must be a number`;
211
211
  if (typeof value.text !== "string")
@@ -215,7 +215,21 @@ function annotatedSourceLineProblem(value, path) {
215
215
  return assertionsProblem;
216
216
  if (!Array.isArray(value.sends))
217
217
  return `"${path}.sends" must be an array`;
218
- return null;
218
+ return arrayProblem(value.turns, `${path}.turns`, sourceTurnProblem);
219
+ }
220
+ function sourceTurnProblem(value, path) {
221
+ if (!isObject(value))
222
+ return `"${path}" must be an AttemptSourceTurn`;
223
+ if (typeof value.label !== "string")
224
+ return `"${path}.label" must be a string`;
225
+ if (value.status !== "completed" && value.status !== "failed" && value.status !== "waiting") {
226
+ return `"${path}.status" must be "completed", "failed", or "waiting"`;
227
+ }
228
+ if (value.durationMs !== undefined && typeof value.durationMs !== "number")
229
+ return `"${path}.durationMs" must be a number`;
230
+ if (typeof value.sentText !== "string")
231
+ return `"${path}.sentText" must be a string`;
232
+ return arrayProblem(value.replies, `${path}.replies`, conversationReplyProblem);
219
233
  }
220
234
  /** AnnotatedEvalSourceSummary(src/results/annotated-source.ts):全是计数字段。 */
221
235
  function sourceSummaryProblem(value, path) {
@@ -250,6 +264,9 @@ export function validateSourceData(data) {
250
264
  const unmappedProblem = arrayProblem(data.unmapped, "unmapped", assertionResultProblem);
251
265
  if (unmappedProblem !== null)
252
266
  return unmappedProblem;
267
+ const turnsProblem = arrayProblem(data.unlocatedTurns, "unlocatedTurns", sourceTurnProblem);
268
+ if (turnsProblem !== null)
269
+ return turnsProblem;
253
270
  return sourceSummaryProblem(data.summary, "summary");
254
271
  }
255
272
  export const AttemptSource = makeAttemptComponent({
@@ -522,6 +539,9 @@ export const AttemptAssessment = defineComponent((_props, ctx) => {
522
539
  return (_jsxs(Col, { children: [_jsx(AttemptError, {}), ctx.page.evidence.capabilities.source ? _jsx(AttemptSource, {}) : _jsx(AttemptAssertions, {})] }));
523
540
  });
524
541
  AttemptAssessment.displayName = "AttemptAssessment";
525
- /** 内建排列顺序;不产生新的 data 或渲染面,用户可以在自己的 attempt-input page 里重排这些叶子。 */
526
- export const AttemptDetail = defineComponent(() => (_jsxs(Col, { children: [_jsx(AttemptSummary, {}), _jsx(AttemptAssessment, {}), _jsx(AttemptFixPrompt, {}), _jsx(AttemptTimeline, {}), _jsx(AttemptDiagnostics, {}), _jsx(AttemptUsage, {}), _jsx(AttemptConversation, {}), _jsx(AttemptTrace, {}), _jsx(AttemptDiff, {})] })));
542
+ /** 内建排列顺序;有 source 时回复已按 loc 展开在 AttemptSource 行内,不再重复一份 round 卡。 */
543
+ export const AttemptDetail = defineComponent((_props, ctx) => {
544
+ const conversationLivesInSource = ctx.page.input === "attempt" && ctx.page.evidence.capabilities.source && ctx.page.evidence.evalSource !== null;
545
+ return (_jsxs(Col, { children: [_jsx(AttemptSummary, {}), _jsx(AttemptAssessment, {}), _jsx(AttemptFixPrompt, {}), _jsx(AttemptTimeline, {}), _jsx(AttemptDiagnostics, {}), _jsx(AttemptUsage, {}), conversationLivesInSource ? null : _jsx(AttemptConversation, {}), _jsx(AttemptTrace, {}), _jsx(AttemptDiff, {})] }));
546
+ });
527
547
  AttemptDetail.displayName = "AttemptDetail";
@@ -2,11 +2,10 @@ import type { ReactElement } from "react";
2
2
  import type { AttemptLocator } from "../../../results/locator.ts";
3
3
  import type { ExperimentListItem } from "../../model/types.ts";
4
4
  import { type ReportLocale } from "../../model/locale.ts";
5
- export declare function ExperimentList({ data, attemptHref, filter, className, locale, relativeTo, }: {
5
+ export declare function ExperimentList({ data, attemptHref, filter, className, locale, }: {
6
6
  data: readonly ExperimentListItem[];
7
7
  attemptHref?: (locator: AttemptLocator) => string;
8
8
  filter?: boolean;
9
9
  className?: string;
10
10
  locale?: ReportLocale;
11
- relativeTo?: string;
12
11
  }): ReactElement;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { experimentDisplayName } from "../../model/format.js";
2
+ import { shortestUniqueLabels } from "../../model/format.js";
3
3
  import { DEFAULT_REPORT_LOCALE, localeText } from "../../model/locale.js";
4
4
  import { AttemptLocatorBadge, failureSummaryText } from "./AttemptList.js";
5
5
  import { MetricCellView } from "../cell.js";
@@ -47,10 +47,11 @@ function Flags({ flags, locale }) {
47
47
  return null;
48
48
  return (_jsxs("div", { className: "nre-experiment-flags", children: [_jsx("span", { children: localeText(locale, "experimentList.flags") }), Object.entries(flags).map(([key, value]) => (_jsxs("b", { children: [key, "=", typeof value === "string" ? value : JSON.stringify(value)] }, key)))] }));
49
49
  }
50
- function ExperimentRow({ item, attemptHref, locale, relativeTo, }) {
51
- return (_jsxs("details", { className: "nre-experiment-entry", children: [_jsxs("summary", { className: "nre-experiment-summary", children: [_jsxs("span", { className: "nre-experiment-name", "data-sort-value": item.experimentId, children: [_jsx("b", { className: cx("nre-experiment-id", "nre-key"), children: experimentDisplayName(item.experimentId, relativeTo) }), _jsxs("small", { children: [localeText(locale, "overview.evalsCount", { n: item.evals }), item.attempts > item.evals ? ` · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })}` : "", ` · ${formatDate(item.lastRunAt, locale)}`] })] }), _jsx("span", { "data-sort-value": item.model ?? "", children: item.model ?? localeText(locale, "experimentList.defaultModel") }), _jsx("span", { "data-sort-value": item.agent, children: _jsx("span", { className: cx("nre-experiment-agent", "nre-key", colorClassForKey(item.agent)), children: item.agent }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.durationMs.value ?? "", children: _jsx(MetricCellView, { cell: item.durationMs, locale: locale }) }), _jsx("span", { className: cx("nre-num", passRateTone(item.endToEndPassRate.value)), "data-sort-value": item.endToEndPassRate.value ?? "", children: _jsx(MetricCellView, { cell: item.endToEndPassRate, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.tokens.value ?? "", children: _jsx(MetricCellView, { cell: item.tokens, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.costUSD.value ?? "", children: _jsx(MetricCellView, { cell: item.costUSD, locale: locale }) }), _jsx("span", { "data-sort-value": item.evalVerdicts.passed, children: _jsx(VerdictSummary, { item: item, locale: locale }) })] }), _jsxs("div", { className: "nre-experiment-detail", children: [_jsx(Flags, { flags: item.flags, locale: locale }), _jsx("ul", { className: "nre-experiment-evals", children: item.evalRows.map((row) => (_jsx(EvalAttempts, { row: row, attemptHref: attemptHref, locale: locale }, row.evalId))) })] })] }));
50
+ function ExperimentRow({ item, label, attemptHref, locale, }) {
51
+ return (_jsxs("details", { className: "nre-experiment-entry", children: [_jsxs("summary", { className: "nre-experiment-summary", children: [_jsxs("span", { className: "nre-experiment-name", "data-sort-value": item.experimentId, children: [_jsx("b", { className: cx("nre-experiment-id", "nre-key"), children: label }), _jsxs("small", { children: [localeText(locale, "overview.evalsCount", { n: item.evals }), item.attempts > item.evals ? ` · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })}` : "", ` · ${formatDate(item.lastRunAt, locale)}`] })] }), _jsx("span", { "data-sort-value": item.model ?? "", children: item.model ?? localeText(locale, "experimentList.defaultModel") }), _jsx("span", { "data-sort-value": item.agent, children: _jsx("span", { className: cx("nre-experiment-agent", "nre-key", colorClassForKey(item.agent)), children: item.agent }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.durationMs.value ?? "", children: _jsx(MetricCellView, { cell: item.durationMs, locale: locale }) }), _jsx("span", { className: cx("nre-num", passRateTone(item.endToEndPassRate.value)), "data-sort-value": item.endToEndPassRate.value ?? "", children: _jsx(MetricCellView, { cell: item.endToEndPassRate, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.tokens.value ?? "", children: _jsx(MetricCellView, { cell: item.tokens, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.costUSD.value ?? "", children: _jsx(MetricCellView, { cell: item.costUSD, locale: locale }) }), _jsx("span", { "data-sort-value": item.evalVerdicts.passed, children: _jsx(VerdictSummary, { item: item, locale: locale }) })] }), _jsxs("div", { className: "nre-experiment-detail", children: [_jsx(Flags, { flags: item.flags, locale: locale }), _jsx("ul", { className: "nre-experiment-evals", children: item.evalRows.map((row) => (_jsx(EvalAttempts, { row: row, attemptHref: attemptHref, locale: locale }, row.evalId))) })] })] }));
52
52
  }
53
- export function ExperimentList({ data, attemptHref, filter = false, className, locale = DEFAULT_REPORT_LOCALE, relativeTo, }) {
53
+ export function ExperimentList({ data, attemptHref, filter = false, className, locale = DEFAULT_REPORT_LOCALE, }) {
54
+ const experimentLabels = shortestUniqueLabels(data.map((item) => item.experimentId));
54
55
  const labels = [
55
56
  localeText(locale, "experimentList.experiment"),
56
57
  localeText(locale, "table.model"),
@@ -61,6 +62,6 @@ export function ExperimentList({ data, attemptHref, filter = false, className, l
61
62
  localeText(locale, "experimentList.cost"),
62
63
  localeText(locale, "experimentList.result"),
63
64
  ];
64
- const board = (_jsxs("div", { className: "nre-experiment-table", children: [_jsx("div", { className: "nre-experiment-head", children: labels.map((label, index) => (_jsxs("button", { type: "button", "data-nre-experiment-sort": index, className: cx(index >= 3 && index <= 6 && "nre-num-head", index === 4 && "nre-sort-desc"), title: index === 4 ? localeText(locale, "experimentList.passRateDescription") : undefined, children: [_jsx("span", { className: "nre-sort-label", children: label }), _jsx("span", { className: "nre-sort-icon", "aria-hidden": "true" })] }, label))) }), data.length === 0 && _jsx("p", { className: "nre-experiment-list-empty", children: localeText(locale, "attemptList.empty") }), data.map((item) => (_jsx(ExperimentRow, { item: item, attemptHref: attemptHref, locale: locale, relativeTo: relativeTo }, item.experimentId)))] }));
65
+ const board = (_jsxs("div", { className: "nre-experiment-table", children: [_jsx("div", { className: "nre-experiment-head", children: labels.map((label, index) => (_jsxs("button", { type: "button", "data-nre-experiment-sort": index, className: cx(index >= 3 && index <= 6 && "nre-num-head", index === 4 && "nre-sort-desc"), title: index === 4 ? localeText(locale, "experimentList.passRateDescription") : undefined, children: [_jsx("span", { className: "nre-sort-label", children: label }), _jsx("span", { className: "nre-sort-icon", "aria-hidden": "true" })] }, label))) }), data.length === 0 && _jsx("p", { className: "nre-experiment-list-empty", children: localeText(locale, "attemptList.empty") }), data.map((item) => (_jsx(ExperimentRow, { item: item, label: experimentLabels.get(item.experimentId) ?? item.experimentId, attemptHref: attemptHref, locale: locale }, item.experimentId)))] }));
65
66
  return (_jsxs("div", { className: cx("nre", "nre-experiment-list", filter && "nre-experiment-table-wrap", className), children: [filter && (_jsx("input", { className: "nre-filter", "data-nre-experiment-filter": "", type: "search", placeholder: localeText(locale, "experimentList.filterPlaceholder") })), board] }));
66
67
  }
@@ -1,5 +1,5 @@
1
1
  import type { AttemptListItem, EvalListItem, ExperimentListItem } from "../../model/types.ts";
2
2
  import type { TextContext } from "../../definition/tree.ts";
3
- export declare function experimentListText(items: readonly ExperimentListItem[], ctx: TextContext, relativeTo?: string): string;
3
+ export declare function experimentListText(items: readonly ExperimentListItem[], ctx: TextContext): string;
4
4
  export declare function evalListText(items: readonly EvalListItem[], ctx: TextContext): string;
5
5
  export declare function attemptListText(items: readonly AttemptListItem[], total: number | undefined, ctx: TextContext): string;
@@ -3,7 +3,7 @@
3
3
  // locator,中间不留空格)。ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的
4
4
  // 摘要,不重复整段 niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼
5
5
  // `niceeval show <locator>`。零 react、零 IO、纯同步。
6
- import { experimentDisplayName, fitFailureSummary, formatDurationMs, formatUSD, verdictMark, } from "../../model/format.js";
6
+ import { fitFailureSummary, formatDurationMs, formatUSD, shortestUniqueLabels, verdictMark, } from "../../model/format.js";
7
7
  import { countText, localeText } from "../../model/locale.js";
8
8
  import { stringWidth, wrapDisplay } from "../../model/text-layout.js";
9
9
  import { renderTableText } from "../../definition/table-text.js";
@@ -29,7 +29,7 @@ function attemptReasonText(item, locale, maxChars) {
29
29
  return fitFailureSummary(withMore, Math.max(24, maxChars));
30
30
  }
31
31
  // ── ExperimentList ──
32
- function experimentSummaryTable(items, ctx, relativeTo) {
32
+ function experimentSummaryTable(items, ctx, labels) {
33
33
  const locale = ctx.locale;
34
34
  const compact = ctx.width < 100;
35
35
  const columns = [
@@ -45,7 +45,7 @@ function experimentSummaryTable(items, ctx, relativeTo) {
45
45
  const rows = items.map((item) => ({
46
46
  key: item.experimentId,
47
47
  cells: {
48
- experiment: experimentDisplayName(item.experimentId, relativeTo),
48
+ experiment: labels.get(item.experimentId) ?? item.experimentId,
49
49
  model: item.model ?? localeText(locale, "experimentList.defaultModel"),
50
50
  agent: item.agent,
51
51
  duration: cellText(item.durationMs, locale),
@@ -55,10 +55,10 @@ function experimentSummaryTable(items, ctx, relativeTo) {
55
55
  cost: cellText(item.costUSD, locale),
56
56
  },
57
57
  }));
58
- const metadata = items.flatMap((item) => wrapDisplay(`${experimentDisplayName(item.experimentId, relativeTo)}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`, Math.max(8, ctx.width - 2)).map((line) => ` ${line}`));
58
+ const metadata = items.flatMap((item) => wrapDisplay(`${labels.get(item.experimentId) ?? item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`, Math.max(8, ctx.width - 2)).map((line) => ` ${line}`));
59
59
  return [renderTableText({ columns: columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
60
60
  }
61
- function experimentDetailTable(item, ctx, relativeTo) {
61
+ function experimentDetailTable(item, ctx, label) {
62
62
  const locale = ctx.locale;
63
63
  const columns = [
64
64
  { key: "status", header: localeText(locale, "experimentList.status") },
@@ -104,19 +104,20 @@ function experimentDetailTable(item, ctx, relativeTo) {
104
104
  .join(" · ")}`
105
105
  : undefined;
106
106
  return [
107
- experimentDisplayName(item.experimentId, relativeTo),
107
+ label,
108
108
  flags,
109
109
  renderTableText({ columns: columns, rows, locale }, ctx),
110
110
  ]
111
111
  .filter(Boolean)
112
112
  .join("\n");
113
113
  }
114
- export function experimentListText(items, ctx, relativeTo) {
114
+ export function experimentListText(items, ctx) {
115
115
  if (items.length === 0)
116
116
  return localeText(ctx.locale, "attemptList.empty");
117
+ const labels = shortestUniqueLabels(items.map((item) => item.experimentId));
117
118
  return [
118
- experimentSummaryTable(items, ctx, relativeTo),
119
- ...items.map((item) => experimentDetailTable(item, ctx, relativeTo)),
119
+ experimentSummaryTable(items, ctx, labels),
120
+ ...items.map((item) => experimentDetailTable(item, ctx, labels.get(item.experimentId) ?? item.experimentId)),
120
121
  ].join("\n\n");
121
122
  }
122
123
  // ── EvalList ──
@@ -12,14 +12,12 @@ interface EntityListChrome extends ChromeProps {
12
12
  export type ExperimentListProps = DataProps<readonly ExperimentListItem[], Record<never, never>, EntityListChrome & {
13
13
  /** web 面在比较表前显示实验过滤框;text 面忽略。 */
14
14
  filter?: boolean;
15
- /**
16
- * 可选父路径:两面的行标签去掉与它相同的前缀,只显示 experiment id 末段。自定义报告
17
- * 显式传入使用;默认 `ExperimentComparison` 不传,完整 id 始终可见。完整 id 仍是
18
- * 排序 / 着色 / 过滤 / 折叠的键。
19
- */
20
- relativeTo?: string;
21
15
  }>;
22
- /** 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。 */
16
+ /**
17
+ * 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。行标签是
18
+ * experiment id 在当前列表里的最短唯一后缀(与 MetricScatter 点标签同一算法,重名逐步
19
+ * 加长到能区分为止);完整 id 不受影响,仍是排序 / 过滤 / 折叠的键。
20
+ */
23
21
  export declare const ExperimentList: ReportComponent<ExperimentListProps>;
24
22
  export type EvalListProps = DataProps<readonly EvalListItem[], Record<never, never>, EntityListChrome>;
25
23
  /** Eval 列表:每项一个 experimentId + evalId,展开到这道题的 Attempt。 */
@@ -103,7 +103,11 @@ export const validateEvalListData = (data) => arrayProblem(data, "data", (item,
103
103
  return arrayProblem(item.attempts, `${path}.attempts`, attemptListItemProblem);
104
104
  });
105
105
  export const validateAttemptListData = (data) => arrayProblem(data, "data", attemptListItemProblem);
106
- /** 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。 */
106
+ /**
107
+ * 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。行标签是
108
+ * experiment id 在当前列表里的最短唯一后缀(与 MetricScatter 点标签同一算法,重名逐步
109
+ * 加长到能区分为止);完整 id 不受影响,仍是排序 / 过滤 / 折叠的键。
110
+ */
107
111
  export const ExperimentList = makeDataComponent({
108
112
  name: "ExperimentList",
109
113
  dataFnName: "experimentListData",
@@ -111,8 +115,8 @@ export const ExperimentList = makeDataComponent({
111
115
  dataFn: (input) => experimentListData(input),
112
116
  specKeys: [],
113
117
  validate: validateExperimentListData,
114
- web: (props, ctx) => (_jsx(ExperimentListWeb, { data: props.data, filter: props.filter, relativeTo: props.relativeTo, locale: props.locale ?? ctx.locale, attemptHref: hrefOf(props, ctx), className: props.className })),
115
- text: (props, ctx) => experimentListText(props.data, ctx, props.relativeTo),
118
+ web: (props, ctx) => (_jsx(ExperimentListWeb, { data: props.data, filter: props.filter, locale: props.locale ?? ctx.locale, attemptHref: hrefOf(props, ctx), className: props.className })),
119
+ text: (props, ctx) => experimentListText(props.data, ctx),
116
120
  });
117
121
  /** Eval 列表:每项一个 experimentId + evalId,展开到这道题的 Attempt。 */
118
122
  export const EvalList = makeDataComponent({