niceeval 0.6.1 → 0.6.2

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 (296) hide show
  1. package/dist/agents/types.d.ts +67 -5
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
  9. package/dist/report/built-ins/experiment-comparison.js +116 -10
  10. package/dist/report/built-ins/index.d.ts +1 -0
  11. package/dist/report/built-ins/index.js +1 -1
  12. package/dist/report/components.d.ts +8 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +54 -34
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -8
  19. package/dist/report/format.js +27 -12
  20. package/dist/report/index.d.ts +4 -3
  21. package/dist/report/index.js +5 -4
  22. package/dist/report/locale.d.ts +11 -2
  23. package/dist/report/locale.js +23 -5
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +65 -14
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +2 -2
  28. package/dist/report/react/AttemptList.js +5 -6
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +2 -1
  34. package/dist/report/react/ExperimentList.js +8 -10
  35. package/dist/report/react/MetricScatter.js +5 -11
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +21 -14
  40. package/dist/report/report.d.ts +5 -1
  41. package/dist/report/report.js +6 -2
  42. package/dist/report/text/faces.d.ts +1 -1
  43. package/dist/report/text/faces.js +42 -41
  44. package/dist/report/text/table.js +36 -5
  45. package/dist/report/types.d.ts +39 -21
  46. package/dist/results/types.d.ts +11 -0
  47. package/dist/runner/feedback/sink.d.ts +110 -0
  48. package/dist/runner/types.d.ts +513 -22
  49. package/dist/sandbox/docker.d.ts +23 -2
  50. package/dist/sandbox/e2b.d.ts +15 -1
  51. package/dist/sandbox/errors.d.ts +30 -3
  52. package/dist/sandbox/io-retry.d.ts +17 -0
  53. package/dist/sandbox/registry.d.ts +2 -0
  54. package/dist/sandbox/resolve.d.ts +18 -5
  55. package/dist/sandbox/retry.d.ts +11 -1
  56. package/dist/sandbox/types.d.ts +39 -5
  57. package/dist/sandbox/vercel.d.ts +7 -1
  58. package/dist/scoring/coverage.d.ts +30 -0
  59. package/dist/scoring/display.d.ts +21 -0
  60. package/dist/scoring/display.js +120 -0
  61. package/dist/scoring/types.d.ts +103 -20
  62. package/dist/shared/aggregate.d.ts +1 -0
  63. package/dist/shared/aggregate.js +3 -3
  64. package/dist/shared/types.d.ts +28 -0
  65. package/dist/tty-line.d.ts +0 -4
  66. package/dist/util.d.ts +23 -0
  67. package/docs-site/zh/concepts/adapter.mdx +22 -4
  68. package/docs-site/zh/concepts/experiment.mdx +1 -1
  69. package/docs-site/zh/concepts/overview.mdx +6 -6
  70. package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
  71. package/docs-site/zh/guides/authoring.mdx +33 -0
  72. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  73. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  74. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  75. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  76. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  77. package/docs-site/zh/guides/debugging.mdx +210 -0
  78. package/docs-site/zh/guides/experiments.mdx +10 -3
  79. package/docs-site/zh/guides/official-adapters.mdx +26 -2
  80. package/docs-site/zh/guides/publish-report.mdx +30 -16
  81. package/docs-site/zh/guides/report-components.mdx +42 -30
  82. package/docs-site/zh/guides/reporters.mdx +2 -2
  83. package/docs-site/zh/guides/results-data.mdx +17 -9
  84. package/docs-site/zh/guides/runner.mdx +17 -7
  85. package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
  86. package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
  87. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  88. package/docs-site/zh/guides/viewing-results.mdx +79 -36
  89. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  90. package/docs-site/zh/guides/write-send.mdx +17 -1
  91. package/docs-site/zh/index.mdx +1 -1
  92. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  93. package/docs-site/zh/reference/capabilities.mdx +2 -2
  94. package/docs-site/zh/reference/cli.mdx +33 -7
  95. package/docs-site/zh/reference/define-agent.mdx +57 -4
  96. package/docs-site/zh/reference/define-config.mdx +1 -1
  97. package/docs-site/zh/reference/define-eval.mdx +42 -9
  98. package/docs-site/zh/reference/expect.mdx +26 -1
  99. package/package.json +5 -1
  100. package/src/agents/ai-sdk-otel.test.ts +1 -0
  101. package/src/agents/ai-sdk.test.ts +3 -0
  102. package/src/agents/ai-sdk.ts +3 -0
  103. package/src/agents/bub-install-spec.test.ts +34 -0
  104. package/src/agents/bub-install-spec.ts +32 -0
  105. package/src/agents/bub.ts +31 -32
  106. package/src/agents/claude-code.test.ts +130 -9
  107. package/src/agents/claude-code.ts +76 -4
  108. package/src/agents/codex.test.ts +189 -40
  109. package/src/agents/codex.ts +155 -14
  110. package/src/agents/coding-cli-versions.test.ts +15 -0
  111. package/src/agents/coding-cli-versions.ts +3 -0
  112. package/src/agents/index.ts +11 -0
  113. package/src/agents/langgraph.test.ts +204 -0
  114. package/src/agents/langgraph.ts +495 -0
  115. package/src/agents/marketplace.ts +85 -0
  116. package/src/agents/native-config.test.ts +179 -0
  117. package/src/agents/native-config.ts +267 -0
  118. package/src/agents/openai-compat.test.ts +1 -0
  119. package/src/agents/openclaw.test.ts +31 -0
  120. package/src/agents/openclaw.ts +171 -0
  121. package/src/agents/plugin-config.test.ts +1 -0
  122. package/src/agents/sdk-streams.test.ts +79 -0
  123. package/src/agents/sdk-streams.ts +55 -10
  124. package/src/agents/skills.test.ts +1 -0
  125. package/src/agents/streaming.test.ts +3 -9
  126. package/src/agents/types.ts +68 -5
  127. package/src/agents/ui-message-stream.test.ts +3 -0
  128. package/src/cli.ts +411 -108
  129. package/src/context/context.test.ts +51 -12
  130. package/src/context/context.ts +161 -29
  131. package/src/context/session.test.ts +1 -0
  132. package/src/context/session.ts +114 -6
  133. package/src/context/types.ts +30 -12
  134. package/src/define.test.ts +13 -8
  135. package/src/define.ts +25 -4
  136. package/src/expect/index.ts +53 -23
  137. package/src/i18n/en.ts +64 -2
  138. package/src/i18n/zh-CN.ts +65 -3
  139. package/src/o11y/cost.test.ts +1 -0
  140. package/src/o11y/execution-tree.test.ts +1 -20
  141. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  142. package/src/o11y/otlp/parse.test.ts +1 -0
  143. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  144. package/src/o11y/parsers/bub.test.ts +1 -0
  145. package/src/o11y/parsers/claude-code.test.ts +1 -34
  146. package/src/o11y/parsers/openclaw.test.ts +154 -0
  147. package/src/o11y/parsers/openclaw.ts +310 -0
  148. package/src/o11y/prices.json +746 -311
  149. package/src/o11y/tool-names.test.ts +1 -0
  150. package/src/o11y/types.ts +16 -2
  151. package/src/report/aggregate.ts +34 -5
  152. package/src/report/built-in-user-parity.test.tsx +110 -153
  153. package/src/report/built-ins/experiment-comparison.tsx +173 -13
  154. package/src/report/built-ins/index.ts +6 -1
  155. package/src/report/components.tsx +9 -3
  156. package/src/report/compute.ts +70 -40
  157. package/src/report/dual-render.test.tsx +194 -67
  158. package/src/report/flag.ts +30 -2
  159. package/src/report/format.ts +35 -11
  160. package/src/report/index.ts +22 -4
  161. package/src/report/locale.ts +25 -5
  162. package/src/report/metrics.ts +67 -14
  163. package/src/report/primitives.tsx +6 -0
  164. package/src/report/react/AttemptList.tsx +6 -31
  165. package/src/report/react/EvalList.tsx +0 -0
  166. package/src/report/react/ExperimentComparison.tsx +68 -0
  167. package/src/report/react/ExperimentList.tsx +15 -9
  168. package/src/report/react/MetricScatter.tsx +12 -14
  169. package/src/report/react/chart-math.test.ts +85 -0
  170. package/src/report/react/chart-math.ts +101 -22
  171. package/src/report/react/enhance.js +33 -1
  172. package/src/report/react/fixtures.ts +24 -17
  173. package/src/report/react/render.test.tsx +9 -64
  174. package/src/report/react/styles.css +73 -2
  175. package/src/report/report.test.ts +306 -98
  176. package/src/report/report.ts +6 -2
  177. package/src/report/text/faces.ts +47 -43
  178. package/src/report/text/table.ts +42 -5
  179. package/src/report/types.ts +41 -21
  180. package/src/results/annotated-source.test.ts +62 -9
  181. package/src/results/annotated-source.ts +64 -6
  182. package/src/results/attempt-evidence.test.ts +9 -7
  183. package/src/results/attempt-evidence.ts +15 -8
  184. package/src/results/attempt-source.ts +6 -3
  185. package/src/results/copy.ts +145 -55
  186. package/src/results/host-equivalence.test.ts +8 -6
  187. package/src/results/index.ts +2 -0
  188. package/src/results/locator.test.ts +1 -22
  189. package/src/results/open.ts +7 -1
  190. package/src/results/publish.ts +149 -0
  191. package/src/results/results.test.ts +85 -51
  192. package/src/results/truncate.ts +90 -0
  193. package/src/results/types.ts +7 -0
  194. package/src/results/writer.ts +31 -13
  195. package/src/runner/attempt.test.ts +138 -7
  196. package/src/runner/attempt.ts +603 -104
  197. package/src/runner/discover.test.ts +47 -0
  198. package/src/runner/discover.ts +36 -2
  199. package/src/runner/eval-source.test.ts +1 -27
  200. package/src/runner/feedback/agent.test.ts +504 -0
  201. package/src/runner/feedback/agent.ts +409 -0
  202. package/src/runner/feedback/ci.test.ts +562 -0
  203. package/src/runner/feedback/ci.ts +401 -0
  204. package/src/runner/feedback/coordinator.test.ts +317 -0
  205. package/src/runner/feedback/coordinator.ts +397 -0
  206. package/src/runner/feedback/failure.ts +40 -0
  207. package/src/runner/feedback/human.test.ts +616 -0
  208. package/src/runner/feedback/human.ts +535 -0
  209. package/src/runner/feedback/index.ts +66 -0
  210. package/src/runner/feedback/io.ts +78 -0
  211. package/src/runner/feedback/profile.test.ts +50 -0
  212. package/src/runner/feedback/profile.ts +58 -0
  213. package/src/runner/feedback/reducer.test.ts +395 -0
  214. package/src/runner/feedback/reducer.ts +260 -0
  215. package/src/runner/feedback/renderer.ts +82 -0
  216. package/src/runner/feedback/sink.ts +203 -0
  217. package/src/runner/feedback/testing.ts +106 -0
  218. package/src/runner/ledger.test.ts +230 -0
  219. package/src/runner/ledger.ts +329 -0
  220. package/src/runner/report.test.ts +128 -3
  221. package/src/runner/report.ts +33 -9
  222. package/src/runner/reporters/artifacts.ts +8 -2
  223. package/src/runner/reporters/braintrust.test.ts +8 -7
  224. package/src/runner/reporters/braintrust.ts +9 -2
  225. package/src/runner/reporters/index.ts +2 -2
  226. package/src/runner/reporters/json.test.ts +162 -0
  227. package/src/runner/reporters/json.ts +35 -8
  228. package/src/runner/reporters/shared.ts +1 -5
  229. package/src/runner/run.test.ts +760 -3
  230. package/src/runner/run.ts +242 -36
  231. package/src/runner/sandbox-prep.ts +3 -42
  232. package/src/runner/timing.ts +158 -0
  233. package/src/runner/types.ts +518 -22
  234. package/src/sandbox/checkpoint.test.ts +55 -0
  235. package/src/sandbox/checkpoint.ts +29 -8
  236. package/src/sandbox/cli-commands.ts +407 -0
  237. package/src/sandbox/docker.ts +115 -16
  238. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  239. package/src/sandbox/e2b-agent-template.ts +94 -0
  240. package/src/sandbox/e2b.ts +74 -9
  241. package/src/sandbox/errors.ts +111 -4
  242. package/src/sandbox/index.ts +2 -0
  243. package/src/sandbox/io-retry.test.ts +58 -0
  244. package/src/sandbox/io-retry.ts +45 -0
  245. package/src/sandbox/keep-registry.test.ts +86 -0
  246. package/src/sandbox/keep-registry.ts +142 -0
  247. package/src/sandbox/keep.ts +178 -0
  248. package/src/sandbox/paths.test.ts +1 -0
  249. package/src/sandbox/paths.ts +19 -8
  250. package/src/sandbox/registry.ts +20 -3
  251. package/src/sandbox/resolve.ts +76 -11
  252. package/src/sandbox/retry.test.ts +70 -0
  253. package/src/sandbox/retry.ts +46 -4
  254. package/src/sandbox/types.ts +44 -6
  255. package/src/sandbox/vercel.ts +43 -20
  256. package/src/scoring/collector.ts +60 -17
  257. package/src/scoring/coverage.ts +95 -0
  258. package/src/scoring/diff.ts +81 -0
  259. package/src/scoring/display.test.ts +121 -0
  260. package/src/scoring/display.ts +133 -0
  261. package/src/scoring/evidence.test.ts +189 -0
  262. package/src/scoring/judge.test.ts +142 -0
  263. package/src/scoring/judge.ts +15 -18
  264. package/src/scoring/scoped.ts +217 -50
  265. package/src/scoring/types.ts +117 -20
  266. package/src/scoring/verdict.ts +16 -4
  267. package/src/shared/aggregate.ts +3 -2
  268. package/src/shared/types.ts +31 -0
  269. package/src/show/compose.ts +2 -2
  270. package/src/show/index.ts +21 -1
  271. package/src/show/render.ts +619 -104
  272. package/src/show/show.test.ts +235 -19
  273. package/src/tty-line.ts +8 -26
  274. package/src/util.test.ts +1 -0
  275. package/src/util.ts +41 -0
  276. package/src/view/app/components/AttemptModal.tsx +153 -2
  277. package/src/view/app/components/CodeView.tsx +32 -11
  278. package/src/view/app/components/CopyControls.tsx +2 -2
  279. package/src/view/app/i18n.ts +6 -0
  280. package/src/view/app/lib/attempt-route.test.ts +1 -0
  281. package/src/view/app/lib/verdict.ts +7 -9
  282. package/src/view/artifact-serving.test.ts +2 -1
  283. package/src/view/client-dist/app.css +1 -1
  284. package/src/view/client-dist/app.js +17 -17
  285. package/src/view/data.test.ts +1 -0
  286. package/src/view/data.ts +11 -1
  287. package/src/view/index.ts +11 -0
  288. package/src/view/server.ts +2 -0
  289. package/src/view/styles.css +3 -0
  290. package/src/view/view-report.test.ts +6 -5
  291. package/src/runner/reporters/console.ts +0 -70
  292. package/src/runner/reporters/live.test.ts +0 -56
  293. package/src/runner/reporters/live.ts +0 -247
  294. package/src/runner/reporters/quiet.test.ts +0 -66
  295. package/src/runner/reporters/quiet.ts +0 -49
  296. package/src/runner/reporters/table.ts +0 -277
@@ -1,5 +1,4 @@
1
- // 通用图表数值工具:round-number 刻度生成与标签防撞布局,零依赖纯函数。
2
- // 从 src/view/app/lib/chart.ts 迁来(view 侧从这里 re-export),报告组件与 view 共用同一份。
1
+ // 通用图表数值工具:round-number 刻度生成与点标签布局,零依赖纯函数。
3
2
  function niceNumber(range, round) {
4
3
  if (range <= 0)
5
4
  return 1;
@@ -43,26 +42,79 @@ export function niceTicks(min, max, count = 5) {
43
42
  ticks.push(Number(v.toFixed(10)));
44
43
  return ticks;
45
44
  }
45
+ /** 标签框高度(基线上方 10px、下方 2px,对应 11px 字号)。 */
46
+ const LABEL_ASCENT = 10;
47
+ const LABEL_DESCENT = 2;
48
+ /** 数据点视为边长 2×DOT_R 的方形障碍(点半径 4.5 + 命中余量)。 */
49
+ const DOT_R = 6;
50
+ /** 紧邻位与点的水平间距;候选环按它逐环外扩。 */
51
+ const NEAR = 10;
52
+ const RINGS = [NEAR, NEAR + 12, NEAR + 26];
53
+ function labelBoxAt(x, y, anchor, width) {
54
+ const x0 = anchor === "start" ? x : anchor === "end" ? x - width : x - width / 2;
55
+ return { x0, y0: y - LABEL_ASCENT, x1: x0 + width, y1: y + LABEL_DESCENT };
56
+ }
57
+ function overlapArea(a, b) {
58
+ const w = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
59
+ const h = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
60
+ return w > 0 && h > 0 ? w * h : 0;
61
+ }
62
+ function outOfBoundsArea(box, bounds) {
63
+ const total = (box.x1 - box.x0) * (box.y1 - box.y0);
64
+ const w = Math.min(box.x1, bounds.x1) - Math.max(box.x0, bounds.x0);
65
+ const h = Math.min(box.y1, bounds.y1) - Math.max(box.y0, bounds.y0);
66
+ const inside = w > 0 && h > 0 ? w * h : 0;
67
+ return total - inside;
68
+ }
69
+ /** 一个点在距离 d 的候选环:左右紧邻、四个斜角、正上正下。顺序即同代价时的偏好序。 */
70
+ function ringCandidates(p, d) {
71
+ const diag = d * 0.75;
72
+ return [
73
+ { x: p.cx + d, y: p.cy + 4, anchor: "start" },
74
+ { x: p.cx - d, y: p.cy + 4, anchor: "end" },
75
+ { x: p.cx + diag, y: p.cy - diag - 2, anchor: "start" },
76
+ { x: p.cx - diag, y: p.cy - diag - 2, anchor: "end" },
77
+ { x: p.cx, y: p.cy - d - LABEL_DESCENT - 2, anchor: "middle" },
78
+ { x: p.cx + diag, y: p.cy + diag + LABEL_ASCENT, anchor: "start" },
79
+ { x: p.cx - diag, y: p.cy + diag + LABEL_ASCENT, anchor: "end" },
80
+ { x: p.cx, y: p.cy + d + LABEL_ASCENT + 2, anchor: "middle" },
81
+ ];
82
+ }
46
83
  /**
47
- * 贪心地把互相重叠的直接标签往下推开(按 y 排序、逐个查重叠再下移),点本身位置不变。
48
- * 散点常见同分不同价的聚簇,这一步避免标签叠成一团糊字。
84
+ * 散点直接标签的候选位择优布局(docs/feature/reports/library.md「MetricScatter」):
85
+ * 每个标签在点四周由近及远的候选环上打分——与已放置标签的重叠、与任何数据点的重叠、
86
+ * 越出画布的面积、离点距离逐项累加,取代价最小的候选。存在无冲突候选时标签必不遮点、
87
+ * 不叠标签、不越界;全候选冲突时取重叠最小者,绝不丢标签。重合点簇因此向不同方向散开,
88
+ * 而不是单向堆叠压到下方的数据点上。
49
89
  */
50
- export function layoutLabelOffsets(items, lineHeight = 14, padX = 4) {
51
- const offsets = new Array(items.length).fill(0);
52
- const order = [...items.keys()].sort((a, b) => items[a].cy - items[b].cy || items[a].cx - items[b].cx);
53
- const placed = [];
90
+ export function placePointLabels(points, bounds) {
91
+ const dots = points.map((p) => ({ x0: p.cx - DOT_R, y0: p.cy - DOT_R, x1: p.cx + DOT_R, y1: p.cy + DOT_R }));
92
+ const placedBoxes = [];
93
+ const result = new Array(points.length);
94
+ // 由上到下、由左到右逐个放置,布局确定可复现
95
+ const order = [...points.keys()].sort((a, b) => points[a].cy - points[b].cy || points[a].cx - points[b].cx);
54
96
  for (const i of order) {
55
- const it = items[i];
56
- const x0 = it.anchorLeft ? it.cx - it.width : it.cx;
57
- const x1 = it.anchorLeft ? it.cx : it.cx + it.width;
58
- let y = it.cy;
59
- let guard = 0;
60
- while (guard < 40 && placed.some((p) => x0 < p.x1 + padX && x1 > p.x0 - padX && Math.abs(y - p.y) < lineHeight)) {
61
- y += lineHeight;
62
- guard++;
97
+ const p = points[i];
98
+ let best = null;
99
+ for (let ring = 0; ring < RINGS.length; ring++) {
100
+ const candidates = ringCandidates(p, RINGS[ring]);
101
+ for (let c = 0; c < candidates.length; c++) {
102
+ const candidate = candidates[c];
103
+ const box = labelBoxAt(candidate.x, candidate.y, candidate.anchor, p.width);
104
+ let cost = 0;
105
+ for (const placed of placedBoxes)
106
+ cost += overlapArea(box, placed) * 4;
107
+ for (const dot of dots)
108
+ cost += overlapArea(box, dot) * 3;
109
+ cost += outOfBoundsArea(box, bounds) * 6;
110
+ cost += ring * 8 + c * 0.5; // 近环、偏好序靠前者优先,只作同冲突程度下的排序
111
+ if (best === null || cost < best.cost) {
112
+ best = { candidate, box, cost, leader: !(ring === 0 && c <= 1) };
113
+ }
114
+ }
63
115
  }
64
- placed.push({ x0, x1, y });
65
- offsets[i] = y - it.cy;
116
+ placedBoxes.push(best.box);
117
+ result[i] = { ...best.candidate, leader: best.leader };
66
118
  }
67
- return offsets;
119
+ return result;
68
120
  }
@@ -6,14 +6,14 @@ export declare const overviewData: OverviewData;
6
6
  export declare const overviewWithCost: OverviewData;
7
7
  /** GroupSummary.data 的产物形态:eval 级折叠计票 + 旧 GroupSelector 口径的通过率(包成 MetricCell)。 */
8
8
  export declare const groupSummaryData: GroupSummaryData;
9
- export declare const tableData: TableData<"pass-rate" | "code-lines">;
9
+ export declare const tableData: TableData<"task-pass-rate" | "code-lines">;
10
10
  /** rows: "experiment" 的榜单形态:行携带 agent/model 元信息与 eval 级折叠计票。 */
11
- export declare const tableDataWithMeta: TableData<"pass-rate">;
11
+ export declare const tableDataWithMeta: TableData<"task-pass-rate">;
12
12
  export declare const matrixData: MatrixData;
13
13
  export declare const scoreboardData: ScoreboardData;
14
14
  export declare const scatterData: ScatterData;
15
15
  export declare const lineData: LineData;
16
- export declare const deltaData: DeltaData<"pass-rate" | "cost">;
16
+ export declare const deltaData: DeltaData<"task-pass-rate" | "cost">;
17
17
  export declare const attemptListItems: AttemptListItem[];
18
18
  export declare const evalListItems: EvalListItem[];
19
19
  export declare const experimentListItems: ExperimentListItem[];
@@ -3,7 +3,7 @@
3
3
  // 数字刻意覆盖诚实细节:coverage 角标(samples<total)、全 null 格子、
4
4
  // 稀疏矩阵、缺数据的散点、delta 的 null 不硬算、truncated 计数。
5
5
  const locator = (s) => s;
6
- export const passRateColumn = { key: "pass-rate", label: "pass rate", unit: "%", better: "higher" };
6
+ export const passRateColumn = { key: "task-pass-rate", label: "pass rate", unit: "%", better: "higher" };
7
7
  export const codeLinesColumn = { key: "code-lines", label: "code lines", unit: "lines", better: "lower" };
8
8
  export const costColumn = { key: "cost", label: "cost", unit: "$", better: "lower" };
9
9
  export const overviewData = {
@@ -59,7 +59,7 @@ export const tableData = {
59
59
  {
60
60
  key: "codex",
61
61
  cells: {
62
- "pass-rate": { value: 0.5, display: "50%", samples: 6, total: 6, refs: [] },
62
+ "task-pass-rate": { value: 0.5, display: "50%", samples: 6, total: 6, refs: [] },
63
63
  // 全 null:一个有效样本都没有 → 缺数据文案,绝不画 0
64
64
  "code-lines": { value: null, display: "—", samples: 0, total: 6, refs: [] },
65
65
  },
@@ -67,7 +67,7 @@ export const tableData = {
67
67
  {
68
68
  key: "bub",
69
69
  cells: {
70
- "pass-rate": {
70
+ "task-pass-rate": {
71
71
  value: 0.87,
72
72
  display: "87%",
73
73
  samples: 6,
@@ -87,7 +87,7 @@ export const tableDataWithMeta = {
87
87
  rows: [
88
88
  {
89
89
  key: "compare/bub",
90
- cells: { "pass-rate": { value: 0.5, display: "50%", samples: 2, total: 2, refs: [] } },
90
+ cells: { "task-pass-rate": { value: 0.5, display: "50%", samples: 2, total: 2, refs: [] } },
91
91
  meta: {
92
92
  agent: "bub",
93
93
  model: "gpt-5.4",
@@ -96,7 +96,7 @@ export const tableDataWithMeta = {
96
96
  },
97
97
  {
98
98
  key: "compare/codex",
99
- cells: { "pass-rate": { value: 1, display: "100%", samples: 2, total: 2, refs: [] } },
99
+ cells: { "task-pass-rate": { value: 1, display: "100%", samples: 2, total: 2, refs: [] } },
100
100
  meta: {
101
101
  agent: "codex",
102
102
  verdicts: { passed: 2, failed: 0, errored: 0, skipped: 0 },
@@ -242,7 +242,7 @@ export const deltaData = {
242
242
  b: { experimentId: "compare/bub--agents-md" },
243
243
  cells: {
244
244
  // 通过率 +12pp:better higher → 好(绿)
245
- "pass-rate": {
245
+ "task-pass-rate": {
246
246
  a: { value: 0.5, display: "50%", samples: 6, total: 6, refs: [] },
247
247
  b: { value: 0.62, display: "62%", samples: 6, total: 6, refs: [] },
248
248
  delta: 0.12,
@@ -262,7 +262,7 @@ export const deltaData = {
262
262
  a: { experimentId: "compare/codex" },
263
263
  b: { experimentId: "compare/codex--agents-md" },
264
264
  cells: {
265
- "pass-rate": {
265
+ "task-pass-rate": {
266
266
  a: { value: 0.4, display: "40%", samples: 6, total: 6, refs: [] },
267
267
  b: { value: 0.4, display: "40%", samples: 6, total: 6, refs: [] },
268
268
  delta: 0,
@@ -292,8 +292,9 @@ const failedAttempt = {
292
292
  name: "roots-correct",
293
293
  severity: "gate",
294
294
  score: 0,
295
- passed: false,
296
- detail: "expected x=2, got x=3",
295
+ outcome: "failed",
296
+ expected: "x=2",
297
+ received: "x=3",
297
298
  evidence: "judge: sign flipped when substituting into the quadratic formula",
298
299
  },
299
300
  ],
@@ -307,7 +308,17 @@ const erroredAttempt = {
307
308
  attempt: 0,
308
309
  agent: "codex",
309
310
  verdict: "errored",
310
- error: "TypeError: cannot read properties of undefined (reading 'foo')",
311
+ // 结构化 error:列表只渲染 message 一层摘要;cause/stack diagnostics 是下钻详情,随数据携带
312
+ error: {
313
+ code: "unexpected-error",
314
+ message: "TypeError: cannot read properties of undefined (reading 'foo')",
315
+ phase: "eval.run",
316
+ stack: "TypeError: cannot read properties of undefined (reading 'foo')\n at run (adapter.ts:42:7)",
317
+ cause: { name: "TypeError", message: "cannot read properties of undefined (reading 'foo')" },
318
+ },
319
+ diagnostics: [
320
+ { code: "sandbox-teardown-failed", level: "warning", message: "sandbox teardown timed out", phase: "sandbox.teardown" },
321
+ ],
311
322
  assertions: [],
312
323
  durationMs: 4_500,
313
324
  locator: locator("@1c1c1c1"),
@@ -324,7 +335,6 @@ export const evalListItems = [
324
335
  evalId: "algebra/quadratic",
325
336
  experimentId: "compare/bub",
326
337
  verdict: "failed",
327
- reason: "roots-correct: expected x=2, got x=3",
328
338
  score: { value: 0, display: "0%", samples: 1, total: 1, refs: [failedAttempt.locator] },
329
339
  duration: { value: 32_000, display: "32.0s", samples: 1, total: 1, refs: [failedAttempt.locator] },
330
340
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 1, refs: [failedAttempt.locator] },
@@ -334,7 +344,6 @@ export const evalListItems = [
334
344
  evalId: "geometry/angles",
335
345
  experimentId: "compare/codex",
336
346
  verdict: "errored",
337
- reason: erroredAttempt.error,
338
347
  score: { value: 0, display: "0%", samples: 1, total: 1, refs: [erroredAttempt.locator] },
339
348
  duration: { value: 4_500, display: "4.5s", samples: 1, total: 1, refs: [erroredAttempt.locator] },
340
349
  cost: { value: null, display: "—", samples: 0, total: 1, refs: [] },
@@ -359,7 +368,6 @@ export const experimentListItems = [
359
368
  {
360
369
  evalId: "algebra/quadratic",
361
370
  verdict: "failed",
362
- reason: "roots-correct: expected x=2, got x=3",
363
371
  duration: { value: 32_000, display: "32.0s", samples: 1, total: 1, refs: [failedAttempt.locator] },
364
372
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 1, refs: [failedAttempt.locator] },
365
373
  attempts: [failedAttempt, failedRetryAttempt],
@@ -388,7 +396,6 @@ export const experimentListItems = [
388
396
  {
389
397
  evalId: "geometry/angles",
390
398
  verdict: "errored",
391
- reason: erroredAttempt.error,
392
399
  duration: { value: 4_500, display: "4.5s", samples: 1, total: 1, refs: [erroredAttempt.locator] },
393
400
  cost: { value: null, display: "—", samples: 0, total: 1, refs: [] },
394
401
  attempts: [erroredAttempt],
@@ -13,7 +13,11 @@ export interface ReportDefinition {
13
13
  [REPORT_DEFINITION]: true;
14
14
  }
15
15
  export declare function defineReport(build: (ctx: ReportContext) => ReportNode | Promise<ReportNode>): ReportDefinition;
16
- /** 宿主装载报告文件时用:默认导出是不是 defineReport 的产物。 */
16
+ /**
17
+ * 宿主装载报告文件时用:默认导出是不是 ReportDefinition。defineReport 的产物是普通对象;
18
+ * 「组件兼报告」(把 defineReport 产物 Object.assign 到双面组件上)是挂了 build 面的
19
+ * 可调用函数,同样算数——判据只看 build 面与标记,不看宿主形态。
20
+ */
17
21
  export declare function isReportDefinition(value: unknown): value is ReportDefinition;
18
22
  /**
19
23
  * text 宿主的装载语义:build → 渲染前解析数据组件(唯一的 await 边界)→ 树校验 → 遍历渲染
@@ -14,9 +14,13 @@ export function defineReport(build) {
14
14
  }
15
15
  return { build, [REPORT_DEFINITION]: true };
16
16
  }
17
- /** 宿主装载报告文件时用:默认导出是不是 defineReport 的产物。 */
17
+ /**
18
+ * 宿主装载报告文件时用:默认导出是不是 ReportDefinition。defineReport 的产物是普通对象;
19
+ * 「组件兼报告」(把 defineReport 产物 Object.assign 到双面组件上)是挂了 build 面的
20
+ * 可调用函数,同样算数——判据只看 build 面与标记,不看宿主形态。
21
+ */
18
22
  export function isReportDefinition(value) {
19
- return (typeof value === "object" &&
23
+ return ((typeof value === "object" || typeof value === "function") &&
20
24
  value !== null &&
21
25
  value[REPORT_DEFINITION] === true &&
22
26
  typeof value.build === "function");
@@ -24,6 +24,6 @@ export declare function scoreboardText(data: ScoreboardData, ctx: TextContext):
24
24
  export declare function scatterText(data: ScatterData, ctx: TextContext): string;
25
25
  export declare function lineText(data: LineData, ctx: TextContext): string;
26
26
  export declare function deltaText(data: DeltaData, ctx: TextContext): string;
27
- export declare function experimentListText(items: ExperimentListItem[], ctx: TextContext): string;
27
+ export declare function experimentListText(items: ExperimentListItem[], ctx: TextContext, relativeTo?: string): string;
28
28
  export declare function evalListText(items: EvalListItem[], ctx: TextContext): string;
29
29
  export declare function attemptListText(items: AttemptListItem[], total: number | undefined, ctx: TextContext): string;
@@ -4,9 +4,9 @@
4
4
  // 零 react、零 IO、纯同步 —— 这是 text 宿主不需要 react-dom 的那一半。
5
5
  // chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale 字典,
6
6
  // 默认 en 与历史输出逐字一致;数据(display、键、warnings message)不本地化。
7
- import { attemptItemReason, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD, verdictMark, } from "../format.js";
7
+ import { attemptItemReason, experimentDisplayName, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD, verdictMark, } from "../format.js";
8
8
  import { countText, localeText, resolveMetricLabel } from "../locale.js";
9
- import { indentBlock, padDisplay, textBar, wrapDisplay } from "./layout.js";
9
+ import { padDisplay, stringWidth, textBar, wrapDisplay } from "./layout.js";
10
10
  import { renderTableText } from "./table.js";
11
11
  import { renderCharPlot, renderCoordinateTable } from "./plot.js";
12
12
  const MISSING_MARK = "—";
@@ -399,12 +399,12 @@ export function deltaText(data, ctx) {
399
399
  // 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
400
400
  // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
401
401
  // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
402
- // 命令模板只在 AttemptList(叶子层)展示完整断言明细时才值得,不在中间层重复。
402
+ // 比较列表都不内联完整断言或命令模板。
403
403
  function locatorBadge(item) {
404
404
  return `${item.locator}${verdictMark(item.verdict)}`;
405
405
  }
406
406
  // ── ExperimentList ──
407
- function experimentSummaryTable(items, ctx) {
407
+ function experimentSummaryTable(items, ctx, relativeTo) {
408
408
  const locale = ctx.locale;
409
409
  const compact = ctx.width < 100;
410
410
  const columns = [
@@ -420,7 +420,7 @@ function experimentSummaryTable(items, ctx) {
420
420
  const rows = items.map((item) => ({
421
421
  key: item.experimentId,
422
422
  cells: {
423
- experiment: item.experimentId,
423
+ experiment: experimentDisplayName(item.experimentId, relativeTo),
424
424
  model: item.model ?? localeText(locale, "experimentList.defaultModel"),
425
425
  agent: item.agent,
426
426
  duration: cellText(item.duration),
@@ -430,18 +430,25 @@ function experimentSummaryTable(items, ctx) {
430
430
  cost: cellText(item.cost),
431
431
  },
432
432
  }));
433
- const metadata = items.flatMap((item) => wrapDisplay(`${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}`));
433
+ 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}`));
434
434
  return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
435
435
  }
436
- function experimentDetailTable(item, ctx) {
436
+ function experimentDetailTable(item, ctx, relativeTo) {
437
437
  const locale = ctx.locale;
438
438
  const columns = [
439
439
  { key: "status", header: localeText(locale, "experimentList.status") },
440
440
  { key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
441
- { key: "result", header: localeText(locale, "experimentList.result") },
441
+ // Result 是可扫读的失败预览,不是证据面:两行放不下的以 收口,完整值走 locator 下钻。
442
+ { key: "result", header: localeText(locale, "experimentList.result"), maxLines: 2 },
442
443
  { key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
443
444
  { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
444
445
  ];
446
+ // Result 的字符预算 ≈ 两行 × 它能分到的列宽(总宽减其它列的自然宽与列距)。这里只做
447
+ // 优先级让位(标题先截、received 最后截)的粗预算;精确的按宽度收口由列的 maxLines 兜底。
448
+ const statusWidth = Math.max(stringWidth(localeText(locale, "experimentList.status")), ...item.evalRows.map((row) => stringWidth(`${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`)));
449
+ const entityWidth = Math.max(stringWidth(localeText(locale, "experimentList.evalAttempt")), ...item.evalRows.flatMap((row) => [stringWidth(row.evalId), ...row.attempts.map((a) => stringWidth(a.locator) + 3)]));
450
+ const fixedWidth = statusWidth + entityWidth + 8 /* duration */ + 6 /* cost */ + 3 * 4; /* 4 段列距 */
451
+ const resultBudget = Math.max(24, (ctx.width - fixedWidth) * 2);
445
452
  const rows = item.evalRows.flatMap((row) => {
446
453
  const parent = {
447
454
  key: row.evalId,
@@ -449,8 +456,8 @@ function experimentDetailTable(item, ctx) {
449
456
  status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
450
457
  entity: row.evalId,
451
458
  result: "",
452
- duration: "",
453
- cost: "",
459
+ duration: localeText(locale, "entityList.average", { value: cellText(row.duration) }),
460
+ cost: localeText(locale, "entityList.average", { value: cellText(row.cost) }),
454
461
  },
455
462
  };
456
463
  const attempts = row.attempts.map((attempt, index) => ({
@@ -458,7 +465,7 @@ function experimentDetailTable(item, ctx) {
458
465
  cells: {
459
466
  status: ` ${verdictMark(attempt.verdict)}`,
460
467
  entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
461
- result: attemptItemReason(attempt) ?? MISSING_MARK,
468
+ result: attemptItemReason(attempt, resultBudget) ?? MISSING_MARK,
462
469
  duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
463
470
  cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
464
471
  },
@@ -468,16 +475,22 @@ function experimentDetailTable(item, ctx) {
468
475
  const flags = item.flags && Object.keys(item.flags).length > 0
469
476
  ? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags).map(([key, value]) => `${key}=${String(value)}`).join(" · ")}`
470
477
  : undefined;
471
- return [item.experimentId, flags, renderTableText({ columns, rows, locale }, ctx)].filter(Boolean).join("\n");
478
+ return [experimentDisplayName(item.experimentId, relativeTo), flags, renderTableText({ columns, rows, locale }, ctx)]
479
+ .filter(Boolean)
480
+ .join("\n");
472
481
  }
473
- export function experimentListText(items, ctx) {
482
+ export function experimentListText(items, ctx, relativeTo) {
474
483
  if (items.length === 0)
475
484
  return localeText(ctx.locale, "attemptList.empty");
476
- return [experimentSummaryTable(items, ctx), ...items.map((item) => experimentDetailTable(item, ctx))].join("\n\n");
485
+ return [
486
+ experimentSummaryTable(items, ctx, relativeTo),
487
+ ...items.map((item) => experimentDetailTable(item, ctx, relativeTo)),
488
+ ].join("\n\n");
477
489
  }
478
490
  // ── EvalList ──
479
- function evalListAttemptLine(item) {
480
- const reason = attemptItemReason(item);
491
+ function evalListAttemptLine(item, ctx) {
492
+ // 行式列表同守「Result 最多两行」:预算 = 两行终端宽,超出由 fit 投影按优先级让位。
493
+ const reason = attemptItemReason(item, Math.max(24, ctx.width * 2 - stringWidth(locatorBadge(item)) - 6));
481
494
  return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
482
495
  }
483
496
  export function evalListText(items, ctx) {
@@ -489,18 +502,21 @@ export function evalListText(items, ctx) {
489
502
  const summary = [
490
503
  localeText(locale, "attemptList.score", { score: cellText(item.score) }),
491
504
  localeText(locale, "overview.attemptsCount", { n: item.attempts.length }),
492
- `${formatDurationMs(item.duration.value ?? 0)} avg`,
493
- item.cost.value === null ? `${missingText(locale)} avg` : `${formatUSD(item.cost.value)} avg`,
505
+ localeText(locale, "entityList.average", {
506
+ value: item.duration.value === null ? missingText(locale) : formatDurationMs(item.duration.value),
507
+ }),
508
+ localeText(locale, "entityList.average", {
509
+ value: item.cost.value === null ? missingText(locale) : formatUSD(item.cost.value),
510
+ }),
494
511
  ].join(" · ");
495
- const attemptLines = item.attempts.map(evalListAttemptLine);
512
+ const attemptLines = item.attempts.map((attempt) => evalListAttemptLine(attempt, ctx));
496
513
  return [identity, ` ${summary}`, ...attemptLines].join("\n");
497
514
  });
498
515
  return blocks.join("\n\n");
499
516
  }
500
517
  // ── AttemptList ──
501
- /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本,
502
- * 然后逐条断言(gate 与 soft 都列,与 web 面的 AttemptRow 同一份材料) */
503
- function attemptListItemText(item, ctx, locale) {
518
+ /** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions locator 下钻。 */
519
+ function attemptListItemText(item, ctx) {
504
520
  const head = [
505
521
  `${verdictMark(item.verdict)} ${item.locator}`,
506
522
  item.evalId,
@@ -509,31 +525,16 @@ function attemptListItemText(item, ctx, locale) {
509
525
  ...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
510
526
  ].join(" · ");
511
527
  const lines = [head];
512
- if (item.error) {
513
- lines.push(indentBlock(wrapDisplay(item.error, ctx.width - 4).join("\n"), " "));
514
- }
515
- for (const assertion of item.assertions) {
516
- const scoreText = assertion.threshold !== undefined
517
- ? `${formatPlainNumber(assertion.score)}/${formatPlainNumber(assertion.threshold)}`
518
- : formatPlainNumber(assertion.score);
519
- lines.push(` ${assertion.severity} ${assertion.name} · ${localeText(locale, `verdict.${assertion.passed ? "passed" : "failed"}`)}${assertion.severity === "soft" ? ` ${scoreText}` : ""}`);
520
- if (assertion.detail)
521
- lines.push(indentBlock(wrapDisplay(assertion.detail, ctx.width - 4).join("\n"), " "));
522
- if (assertion.evidence) {
523
- const limit = Math.max(240, ctx.width * 5);
524
- const evidence = assertion.evidence.length <= limit
525
- ? assertion.evidence
526
- : `${assertion.evidence.slice(0, limit)}… (${assertion.evidence.length - limit} more chars; open ${item.locator} for full evidence)`;
527
- lines.push(indentBlock(wrapDisplay(evidence, ctx.width - 6).join("\n"), " "));
528
- }
529
- }
528
+ const reason = attemptItemReason(item, Math.max(24, ctx.width * 2 - 4));
529
+ if (reason)
530
+ lines.push(` ${reason}`);
530
531
  return lines.join("\n");
531
532
  }
532
533
  export function attemptListText(items, total, ctx) {
533
534
  const locale = ctx.locale;
534
535
  if (items.length === 0)
535
536
  return localeText(locale, "attemptList.empty");
536
- const blocks = items.map((item) => attemptListItemText(item, ctx, locale));
537
+ const blocks = items.map((item) => attemptListItemText(item, ctx));
537
538
  const remaining = (total ?? items.length) - items.length;
538
539
  if (remaining > 0)
539
540
  blocks.push(localeText(locale, "attemptList.truncatedText", { n: remaining }));
@@ -6,7 +6,7 @@
6
6
  // 列宽按显示宽度算(CJK 记 2 列);null 渲染 —,不补 0;超宽先折最宽的左对齐列,
7
7
  // 压到下限仍放不下就从右侧丢列并如实报数(「截断报剩余」是既有契约,不在这里破例)。
8
8
  import { countText, localeText } from "../locale.js";
9
- import { renderAlignedRows, stringWidth, wrapDisplay } from "./layout.js";
9
+ import { charDisplayWidth, renderAlignedRows, stringWidth, wrapDisplay } from "./layout.js";
10
10
  const MISSING_MARK = "—";
11
11
  /** 列间距,与 renderAlignedRows 的 join(" ") 一致。 */
12
12
  const COLUMN_GAP = 3;
@@ -47,12 +47,36 @@ function fitWidths(natural, align, available) {
47
47
  }
48
48
  return { widths, hidden };
49
49
  }
50
- /** 逻辑行 物理行:每格折到自己的列宽,行高取最高的那格,矮格补空串。 */
51
- function toPhysicalRows(cells, widths) {
50
+ /** 按显示宽度截到 width(不够放 `…` 时原样返回)。 */
51
+ function truncateDisplay(text, width) {
52
+ if (stringWidth(text) <= width)
53
+ return text;
54
+ let out = "";
55
+ let used = 0;
56
+ for (const ch of text) {
57
+ const w = charDisplayWidth(ch.codePointAt(0));
58
+ if (used + w > width)
59
+ break;
60
+ out += ch;
61
+ used += w;
62
+ }
63
+ return out;
64
+ }
65
+ /** 列的 maxLines 收口:超出的行丢弃,最后一行以 `…` 如实标注被收口。 */
66
+ function clampCellLines(lines, maxLines, width) {
67
+ if (maxLines === undefined || lines.length <= maxLines)
68
+ return lines;
69
+ const kept = lines.slice(0, Math.max(1, maxLines));
70
+ const last = kept[kept.length - 1];
71
+ kept[kept.length - 1] = `${truncateDisplay(last, Math.max(1, width - 1))}…`;
72
+ return kept;
73
+ }
74
+ /** 逻辑行 → 物理行:每格折到自己的列宽,列带 maxLines 就收口,行高取最高的那格,矮格补空串。 */
75
+ function toPhysicalRows(cells, widths, maxLines) {
52
76
  const wrapped = cells.map((cell, c) =>
53
77
  // 放得下就原样保留:wrapDisplay 会把连续空格折成一个,而 DeltaTable 的
54
78
  // "50% → 62% +12pp" 这类格子内含固定间距 —— 不折行时一个字节都不能动。
55
- !cell.includes("\n") && stringWidth(cell) <= widths[c] ? [cell] : wrapDisplay(cell, widths[c]));
79
+ clampCellLines(!cell.includes("\n") && stringWidth(cell) <= widths[c] ? [cell] : wrapDisplay(cell, widths[c]), maxLines[c], widths[c]));
56
80
  const height = Math.max(...wrapped.map((lines) => lines.length), 1);
57
81
  const out = [];
58
82
  for (let i = 0; i < height; i++)
@@ -80,7 +104,14 @@ export function renderTableText(props, ctx) {
80
104
  const matrix = [header, ...body];
81
105
  const natural = header.map((_, c) => Math.max(...matrix.map((row) => stringWidth(row[c] ?? ""))));
82
106
  const { widths, hidden } = fitWidths(natural, align, ctx.width);
83
- const physical = matrix.flatMap((row) => toPhysicalRows(row.slice(0, widths.length), widths));
107
+ const maxLines = props.columns.map((column) => column.maxLines);
108
+ if (hasLocator)
109
+ maxLines.push(undefined);
110
+ // 表头不参与 maxLines 收口:表头是自己写的短词,收口只服务数据格。
111
+ const physical = [
112
+ ...toPhysicalRows(header.slice(0, widths.length), widths, widths.map(() => undefined)),
113
+ ...body.flatMap((row) => toPhysicalRows(row.slice(0, widths.length), widths, maxLines)),
114
+ ];
84
115
  const table = renderAlignedRows(physical, align);
85
116
  return hidden > 0 ? `${table}\n${countText(locale, "table.columnsHidden", hidden)}` : table;
86
117
  }