fm-bench 0.4.2 → 0.4.3

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.
package/README.md CHANGED
@@ -161,6 +161,8 @@ fm-bench legend --json
161
161
  fm-bench legend --csv
162
162
  ```
163
163
 
164
+ The legend wraps long definitions and rules to fit the terminal width instead of hiding text behind ellipses.
165
+
164
166
  ## Live Progress
165
167
 
166
168
  Interactive terminal runs show a single-line status indicator on stderr while prompts are loaded, models are inspected, tokens are counted, warmups run, and benchmark jobs complete. The final report still prints to stdout, so `--json`, `--csv`, and `--out` remain automation-friendly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fm-bench",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Dynamic benchmark CLI for Apple's fm command on macOS 27+.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/table.js CHANGED
@@ -109,16 +109,10 @@ export function renderLegend(options = {}) {
109
109
  ]);
110
110
 
111
111
  if (options.compact || width < 88) {
112
- return legendEntries().map((item) => {
113
- const suffix = item.rule ? ` Rule: ${item.rule}` : '';
114
- return truncate(`${item.table.toUpperCase()} ${item.column}: ${item.definition}${suffix}`, width);
115
- }).join('\n');
112
+ return renderCompactLegend(width);
116
113
  }
117
114
 
118
- return renderTable(['table', 'column', 'definition', 'rule'], rows, {
119
- ...options,
120
- maxCellWidth: Math.max(20, Math.floor((width - 48) / 2))
121
- });
115
+ return renderWrappedTable(['table', 'column', 'definition', 'rule'], rows, legendColumnWidths(width), options);
122
116
  }
123
117
 
124
118
  export function formatMs(value) {
@@ -358,6 +352,26 @@ function rule(left, join, right, horizontal, widths) {
358
352
  return `${left}${widths.map((width) => horizontal.repeat(width + 2)).join(join)}${right}`;
359
353
  }
360
354
 
355
+ function renderWrappedTable(headers, rows, widths, options = {}) {
356
+ const ascii = Boolean(options.ascii);
357
+ const style = ascii ? ASCII_TABLE : UNICODE_TABLE;
358
+ const top = rule(style.topLeft, style.topJoin, style.topRight, style.horizontal, widths);
359
+ const middle = rule(style.midLeft, style.midJoin, style.midRight, style.horizontal, widths);
360
+ const bottom = rule(style.bottomLeft, style.bottomJoin, style.bottomRight, style.horizontal, widths);
361
+ const headerLine = rowLine(headers.map((header) => truncate(header, widths[headers.indexOf(header)])), widths, style, true, options);
362
+ const bodyLines = [];
363
+
364
+ for (const row of rows) {
365
+ const wrappedCells = row.map((value, index) => wrapText(formatCell(value), widths[index]));
366
+ const height = Math.max(...wrappedCells.map((lines) => lines.length));
367
+ for (let lineIndex = 0; lineIndex < height; lineIndex += 1) {
368
+ bodyLines.push(rowLine(wrappedCells.map((lines) => lines[lineIndex] ?? ''), widths, style, false, options));
369
+ }
370
+ }
371
+
372
+ return [top, headerLine, middle, ...bodyLines, bottom].join('\n');
373
+ }
374
+
361
375
  function rowLine(row, widths, style, header = false, options = {}) {
362
376
  return `${style.vertical}${row.map((cell, index) => {
363
377
  const normalized = header ? { text: String(cell).toUpperCase(), tone: 'header' } : normalizeCell(cell);
@@ -409,6 +423,63 @@ function compactReason(value) {
409
423
  return `${clean.slice(0, 55)}...`;
410
424
  }
411
425
 
426
+ function renderCompactLegend(width) {
427
+ const lines = [];
428
+ for (const item of legendEntries()) {
429
+ lines.push(...wrapText(`${item.table.toUpperCase()} ${item.column}`, width));
430
+ lines.push(...wrapText(` Definition: ${item.definition}`, width));
431
+ if (item.rule) {
432
+ lines.push(...wrapText(` Rule: ${item.rule}`, width));
433
+ }
434
+ lines.push('');
435
+ }
436
+ if (lines.at(-1) === '') lines.pop();
437
+ return lines.join('\n');
438
+ }
439
+
440
+ function legendColumnWidths(width) {
441
+ const available = Math.max(40, width - 13);
442
+ const tableWidth = 7;
443
+ const columnWidth = Math.min(25, Math.max(18, Math.floor(available * 0.28)));
444
+ const remaining = Math.max(36, available - tableWidth - columnWidth);
445
+ const definitionWidth = Math.max(18, Math.floor(remaining * 0.52));
446
+ const ruleWidth = Math.max(18, remaining - definitionWidth);
447
+ return [tableWidth, columnWidth, definitionWidth, ruleWidth];
448
+ }
449
+
450
+ function wrapText(value, width) {
451
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
452
+ if (!text) return [''];
453
+ if (width <= 1) return [text];
454
+
455
+ const lines = [];
456
+ let current = '';
457
+ for (const rawWord of text.split(' ')) {
458
+ const pieces = splitLongWord(rawWord, width);
459
+ for (const word of pieces) {
460
+ if (!current) {
461
+ current = word;
462
+ } else if (visibleLength(`${current} ${word}`) <= width) {
463
+ current = `${current} ${word}`;
464
+ } else {
465
+ lines.push(current);
466
+ current = word;
467
+ }
468
+ }
469
+ }
470
+ if (current) lines.push(current);
471
+ return lines.length ? lines : [''];
472
+ }
473
+
474
+ function splitLongWord(word, width) {
475
+ if (visibleLength(word) <= width) return [word];
476
+ const pieces = [];
477
+ for (let index = 0; index < word.length; index += width) {
478
+ pieces.push(word.slice(index, index + width));
479
+ }
480
+ return pieces;
481
+ }
482
+
412
483
  function truncate(value, width) {
413
484
  const text = String(value ?? '');
414
485
  if (visibleLength(text) <= width) return text;