@vzn/vx 0.0.649 → 0.0.650

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.
@@ -292,9 +292,14 @@ function paintTaskId(node: TaskNode, colors: ColorSupport, opts: { bold?: boolea
292
292
  export const TIME_COL = 7 // "0ms" … "999.99s", then "16m 40s" … "99m 59s"
293
293
  const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run"
294
294
  const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
295
- const BLOCKED_COL = 7 // the live list's count of tasks a running one holds back
295
+ const BLOCKED_COL = 7 // `blocks`: how many tasks are downstream of the row's
296
+ const USUAL_COL = 7 // "~18s" live, "+6.0s" / "−1m 05s" after
297
+ const WAITED_COL = 7 // the time cell's width: "1.20s"
298
+ const CPU_COL = 5 // "180%", "1200%"
299
+ const MEM_COL = 6 // "412MB", "1.1GB", "1023MB"
300
+ const OUT_COL = 7 // `outputs`: "12MB", "900B"
301
+ const WORKER_COL = 12 // `local`, or a plugin's name for the machine, cut at 12
296
302
  /** The list header's colour: violet-400, the summary rule's first stop. */
297
- const HEADER = '#a78bfa'
298
303
 
299
304
  /** Right-align the duration in a TIME_COL cell (pad on the left). */
300
305
  function timeCell(ms: number | null, colors: ColorSupport): string {
@@ -375,10 +380,11 @@ function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
375
380
  }
376
381
 
377
382
  /**
378
- * One reported task row: `<glyph> <time> <status> <cache> <name>`.
379
- * `glyph` is pre-painted; status/cache are raw text + color, padded to
380
- * their columns; `paintedId` carries identity coloring. `ms = null`
381
- * blanks the time (e.g. a skip never ran); `cache = ''` blanks it.
383
+ * One reported task row on the list grid: `<glyph> <time> <usual> <waited>
384
+ * <status> <cache> <cpu> <mem> <blocks> <worker> <name>`. `glyph` is
385
+ * pre-painted; status/cache are raw text + color, padded to their
386
+ * columns; `paintedId` carries identity coloring. `ms = null` blanks the
387
+ * time (a skip never ran); `cache = ''` and an absent cell blank theirs.
382
388
  */
383
389
  export function formatTaskRow(
384
390
  glyph: string,
@@ -389,32 +395,118 @@ export function formatTaskRow(
389
395
  cacheColor: string,
390
396
  paintedId: string,
391
397
  colors: ColorSupport = NO_COLOR,
392
- /** The live list's `blocked` cell, right-aligned before the id; absent off that list. */
393
- blocked?: string,
398
+ cells: RowCells = {},
394
399
  ): string {
395
400
  const st = cell(status, statusColor, STATUS_COL, colors)
396
401
  // `aborting` runs one into the cache cell, which is blank beside it.
397
402
  const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
398
- const bl =
399
- blocked === undefined
400
- ? ''
401
- : `${' '.repeat(Math.max(0, BLOCKED_COL - blocked.length))}${paint(WARN, blocked, colors)} `
402
- return ` ${glyph} ${timeCell(ms, colors)} ${st} ${ca} ${bl}${paintedId}`
403
+ const usual = cells.usual ?? { text: '', painted: '' }
404
+ const us = `${' '.repeat(Math.max(0, USUAL_COL - usual.text.length))}${usual.painted}`
405
+ const waited = cells.waitedMs === undefined ? '' : formatElapsed(cells.waitedMs)
406
+ const cpu = cells.cpu === undefined ? '' : `${Math.round(cells.cpu * 100)}%`
407
+ const mem = cells.memBytes === undefined ? '' : formatSize(cells.memBytes)
408
+ const out = cells.outputBytes === undefined ? '' : formatSize(cells.outputBytes)
409
+ const blocks = cells.blocks ?? ''
410
+ const worker = workerName(cells.worker ?? '')
411
+ return (
412
+ ` ${glyph} ${timeCell(ms, colors)} ${us} ${right(waited, WAITED_COL, colors)} ${st} ${ca} ` +
413
+ `${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
414
+ `${' '.repeat(Math.max(0, BLOCKED_COL - blocks.length))}${paint(WARN, blocks, colors)} ` +
415
+ `${worker === '' ? '' : ui.inline(ui.place(worker), colors)}${' '.repeat(WORKER_COL - worker.length)} ${paintedId}`
416
+ )
417
+ }
418
+
419
+ /** A row's cells past time, status and cache; each blank when absent. */
420
+ export interface RowCells {
421
+ /** After the time, pre-painted: `~18s` live, `+6.0s` / `−1.4s` after. */
422
+ usual?: { text: string; painted: string }
423
+ /** How long the task waited ready for a slot. */
424
+ waitedMs?: number
425
+ /** Cores in use (live) or on average (after): 1.8 prints `180%`. */
426
+ cpu?: number
427
+ /** Resident memory now (live) or at its peak (after). */
428
+ memBytes?: number
429
+ /** The bytes of the task's outputs: what its save stored, or its hit restored. */
430
+ outputBytes?: number
431
+ /** How many tasks are downstream. */
432
+ blocks?: string
433
+ /** The machine it runs or ran on: `local`, or the executor's name for it. */
434
+ worker?: string
435
+ }
436
+
437
+ function right(text: string, width: number, colors: ColorSupport): string {
438
+ return ' '.repeat(Math.max(0, width - text.length)) + paint('', text, colors, { dim: true })
439
+ }
440
+
441
+ /** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
442
+ export function formatSize(bytes: number): string {
443
+ if (bytes < 1024) return `${bytes}B`
444
+ const mb = bytes / 1024 / 1024
445
+ if (mb < 1) return `${Math.round(bytes / 1024)}KB`
446
+ if (mb < 9.95) return `${mb.toFixed(1)}MB`
447
+ if (mb < 1000) return `${Math.round(mb)}MB`
448
+ const gb = mb / 1024
449
+ return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
450
+ }
451
+
452
+ /** A worker's name in its 12 columns: a longer one ends in `…`. */
453
+ function workerName(name: string): string {
454
+ return name.length > WORKER_COL ? `${name.slice(0, WORKER_COL - 1)}\u2026` : name
403
455
  }
404
456
 
405
457
  /**
406
- * The header over a task list, on the row grid: bold and coloured, so it
407
- * reads apart from the rows (dim, it was lost among them). `worker` when an
408
- * executor places tasks: the name after the id's dot. `blocked` on the live
409
- * list, whose rows count the tasks each running one holds back.
458
+ * A wait worth a cell: under 50 ms the task started at once, the
459
+ * scheduler's own tick and a sibling's spawn, not a full pool.
410
460
  */
411
- export function formatListHeader(
412
- worker: boolean,
461
+ export function waited(ms: number | undefined): boolean {
462
+ return ms !== undefined && ms >= 50
463
+ }
464
+
465
+ /** A task's average cores over its run: CPU time over wall time. */
466
+ function coresOf(o: TaskOutcome): number | undefined {
467
+ return o.cpuMs === undefined || o.durationMs <= 0 ? undefined : o.cpuMs / o.durationMs
468
+ }
469
+
470
+ /**
471
+ * The results list's `±usual` cell: how far an executed task ran from its
472
+ * usual time, `+6.0s` red or `−1.4s` green, only past a fifth of the usual
473
+ * either way; blank with no usual time to compare.
474
+ */
475
+ function usualDelta(
476
+ ms: number,
477
+ usualMs: number | undefined,
413
478
  colors: ColorSupport = NO_COLOR,
414
- blocked = false,
415
- ): string {
416
- const head = ` ${'time'.padStart(TIME_COL)} ${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${blocked ? `${'blocked'.padStart(BLOCKED_COL)} ` : ''}task${worker ? ' \u00b7 worker' : ''}`
417
- return paint(HEADER, head, colors, { bold: true })
479
+ ): { text: string; painted: string } {
480
+ if (usualMs === undefined || usualMs <= 0 || Math.abs(ms - usualMs) <= usualMs * 0.2)
481
+ return { text: '', painted: '' }
482
+ const d = ms - usualMs
483
+ const text = `${d > 0 ? '+' : '\u2212'}${formatElapsed(Math.abs(d), true)}`
484
+ return { text, painted: paint(d > 0 ? ERROR : SUCCESS, text, colors) }
485
+ }
486
+
487
+ /**
488
+ * The header over a task list, on the row grid: dim and underlined across
489
+ * the whole line, gaps included, straight over its rows (bold in a hue, it
490
+ * blended into rows of the same hues).
491
+ */
492
+ export function formatListHeader(colors: ColorSupport = NO_COLOR): string {
493
+ return header('\u00b1usual', colors)
494
+ }
495
+
496
+ /**
497
+ * The live list's header: the usual duration where the results list has
498
+ * how far a task ran from it; the other columns are the results'.
499
+ */
500
+ export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
501
+ return header('usual', colors)
502
+ }
503
+
504
+ function header(usual: string, colors: ColorSupport): string {
505
+ const head =
506
+ ` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)} ${'waited'.padStart(WAITED_COL)} ` +
507
+ `${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${'cpu'.padStart(CPU_COL)} ` +
508
+ `${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)} task`
509
+ return paint('', head, colors, { dim: true, underline: true })
418
510
  }
419
511
 
420
512
  /** Compact one-liner for a cache hit with nothing to replay. */
@@ -423,8 +515,11 @@ export function formatTaskHitLine(
423
515
  o: TaskOutcome,
424
516
  colors: ColorSupport = NO_COLOR,
425
517
  onPath = false,
518
+ blocks?: number,
426
519
  ): string {
427
- return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath)
520
+ return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
521
+ ...ranCells(o, blocks),
522
+ })
428
523
  }
429
524
 
430
525
  /**
@@ -448,8 +543,34 @@ export function formatTaskExecutedLine(
448
543
  o: TaskOutcome,
449
544
  colors: ColorSupport = NO_COLOR,
450
545
  onPath = false,
546
+ blocks?: number,
547
+ usualMs?: number,
451
548
  ): string {
452
- return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath)
549
+ return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
550
+ ...executedCells(o, blocks),
551
+ usual: usualDelta(o.durationMs, usualMs, colors),
552
+ })
553
+ }
554
+
555
+ /** An executed task's cells: where and how long it waited, its average cores and peak memory. */
556
+ export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
557
+ const cores = coresOf(o)
558
+ return {
559
+ ...ranCells(o, blocks),
560
+ ...(cores !== undefined ? { cpu: cores } : {}),
561
+ ...(o.peakRssBytes !== undefined ? { memBytes: o.peakRssBytes } : {}),
562
+ }
563
+ }
564
+
565
+ /** The cells every row of a task that reached a machine carries. */
566
+ function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
567
+ return {
568
+ // A skip reached no machine.
569
+ ...(o.status === 'skipped' ? {} : { worker: o.where ?? 'local' }),
570
+ ...(blocks !== undefined ? { blocks: String(blocks) } : {}),
571
+ ...(waited(o.queuedMs) ? { waitedMs: o.queuedMs! } : {}),
572
+ ...(o.outputBytes !== undefined ? { outputBytes: o.outputBytes } : {}),
573
+ }
453
574
  }
454
575
 
455
576
  /** Shared: render any outcome on the grid. */
@@ -459,6 +580,7 @@ function formatOutcomeRow(
459
580
  ms: number | null,
460
581
  colors: ColorSupport,
461
582
  onPath: boolean,
583
+ cells: RowCells,
462
584
  ): string {
463
585
  const st = statusOf(o)
464
586
  const ca = cacheOf(o)
@@ -472,9 +594,9 @@ function formatOutcomeRow(
472
594
  ca.color,
473
595
  paintedId,
474
596
  colors,
597
+ cells,
475
598
  ) +
476
599
  (onPath ? pathNote(colors) : '') +
477
- whereNote(o, colors) +
478
600
  flakyNote(o, colors)
479
601
  )
480
602
  }
@@ -485,7 +607,7 @@ export function pathNote(colors: ColorSupport = NO_COLOR): string {
485
607
  }
486
608
 
487
609
  /** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
488
- export function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
610
+ function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
489
611
  return o.where === undefined ? '' : placeNote(o.where, colors)
490
612
  }
491
613
 
@@ -493,7 +615,7 @@ export function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
493
615
  * ` · <where>` after the id: the dot dim, the place in its own hue. Last
494
616
  * on the row, as a worker's name has no width to pad to (2026-10-21).
495
617
  */
496
- export function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
618
+ function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
497
619
  return `${paint('', ' \u00b7', colors, { dim: true })} ${ui.inline(ui.place(where), colors)}`
498
620
  }
499
621
 
@@ -524,6 +646,7 @@ export function formatTaskSkippedLine(
524
646
  node: TaskNode,
525
647
  colors: ColorSupport = NO_COLOR,
526
648
  skip?: Parameters<typeof skippedReason>[0],
649
+ blocks?: number,
527
650
  ): string {
528
651
  const why = skippedReason(skip)
529
652
  const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors, { dim: true })}`
@@ -537,6 +660,7 @@ export function formatTaskSkippedLine(
537
660
  '',
538
661
  paintTaskId(node, colors),
539
662
  colors,
663
+ blocks === undefined ? {} : { blocks: String(blocks) },
540
664
  ) + suffix
541
665
  )
542
666
  }
@@ -550,8 +674,10 @@ export function formatTaskAbortedLine(
550
674
  node: TaskNode,
551
675
  o: TaskOutcome,
552
676
  colors: ColorSupport = NO_COLOR,
677
+ blocks?: number,
553
678
  ): string {
554
679
  const id = paintTaskId(node, colors)
680
+ const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
555
681
  if (neverStarted(o)) {
556
682
  return formatTaskRow(
557
683
  paint('', '\u25cc', colors, { dim: true }),
@@ -562,6 +688,7 @@ export function formatTaskAbortedLine(
562
688
  '',
563
689
  id,
564
690
  colors,
691
+ bl,
565
692
  )
566
693
  }
567
694
  return formatTaskRow(
@@ -573,6 +700,7 @@ export function formatTaskAbortedLine(
573
700
  '',
574
701
  id,
575
702
  colors,
703
+ ranCells(o, blocks),
576
704
  )
577
705
  }
578
706
 
@@ -31,7 +31,8 @@ export {
31
31
  warnOf,
32
32
  } from './logger.js'
33
33
  export { formatPersistentList } from './framed-output.js'
34
- export { createForecast } from './forecast.js'
34
+ export { blocksCounts, createForecast } from './forecast.js'
35
35
  export { fileLocations, fileMemo, type OutputLocation } from './path-links.js'
36
36
  export { plainOutput } from './plain-output.js'
37
+ export type { LiveUsage } from './status-line.js'
37
38
  export { formatOutcomeTable, formatRunSummary } from './summary.js'
@@ -8,10 +8,11 @@ import {
8
8
  formatListHeader,
9
9
  formatTaskAbortedLine,
10
10
  formatTaskBlock,
11
+ executedCells,
12
+ waited,
11
13
  formatTaskExecutedLine,
12
14
  flakyNote,
13
15
  pathNote,
14
- whereNote,
15
16
  formatTaskHashLine,
16
17
  formatTaskHitLine,
17
18
  formatTaskSkippedLine,
@@ -91,7 +92,8 @@ export interface Logger {
91
92
  /** The run's canonical start (epoch ms) — equals the final summary's `startedAt`. */
92
93
  startedAtMs?: number
93
94
  }): void
94
- taskStart?(node: TaskNode): void
95
+ /** `waitedMs`: how long it waited ready for a slot, when not started at once. */
96
+ taskStart?(node: TaskNode, info?: { waitedMs?: number }): void
95
97
  /** The task graph finished (any outcome); vx's own closing steps follow. Idempotent. */
96
98
  runEnd?(): void
97
99
  /**
@@ -354,6 +356,8 @@ export function defaultLogger(
354
356
  const onPath = new Set<string>()
355
357
  let blockedCount = 0
356
358
  let expected: ReadonlyMap<string, number> | undefined
359
+ // Each task's usual time once history is read: the results' `±usual`.
360
+ let usualOf: ReadonlyMap<string, number> | undefined
357
361
  let blocked: readonly BlockedTask[] | undefined
358
362
  let blockedBy: ReadonlyMap<string, number> | undefined
359
363
  let etaAtMs = 0
@@ -521,6 +525,7 @@ export function defaultLogger(
521
525
  expected = f.expected
522
526
  blocked = f.blocked
523
527
  blockedBy = f.blockedBy
528
+ usualOf = f.usual
524
529
  }
525
530
  const etaMs = deadlineMs === undefined ? undefined : deadlineMs - now
526
531
  if (signals.progress && total > 0) {
@@ -604,6 +609,7 @@ export function defaultLogger(
604
609
  nowMs: Date.now(),
605
610
  summaryLines,
606
611
  ...(runContext?.whereRunning !== undefined ? { where: runContext.whereRunning } : {}),
612
+ ...(runContext?.usage !== undefined ? { usage: runContext.usage } : {}),
607
613
  ...(expected !== undefined ? { expected } : {}),
608
614
  ...(critical !== undefined ? { critical } : {}),
609
615
  },
@@ -688,7 +694,7 @@ export function defaultLogger(
688
694
  const emitRow = (line: string): void => {
689
695
  if (!headed) {
690
696
  headed = true
691
- emitLine(formatListHeader(runContext?.whereRunning !== undefined, colors))
697
+ emitLine(formatListHeader(colors))
692
698
  }
693
699
  emitLine(line)
694
700
  }
@@ -731,10 +737,17 @@ export function defaultLogger(
731
737
  (framesTerminal && holdsTerminal(node)) ||
732
738
  (view.mode === 'focused' && isPrimary(node) && !isGroupTask(node) && requestedCount <= 1)
733
739
 
740
+ // How many tasks are downstream of each: every results row's `blocks`.
741
+ const blocksOf = (id: string): number | undefined => runContext?.blocks?.().get(id)
734
742
  const failureRow = (node: TaskNode, outcome: TaskOutcome): string =>
735
- formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(outcome)) +
743
+ formatFailureLine(
744
+ node.id,
745
+ outcome.durationMs,
746
+ colors,
747
+ cacheWordOf(outcome),
748
+ executedCells(outcome, blocksOf(node.id)),
749
+ ) +
736
750
  (onPath.has(node.id) ? pathNote(colors) : '') +
737
- whereNote(outcome, colors) +
738
751
  flakyNote(outcome, colors)
739
752
 
740
753
  return {
@@ -798,7 +811,7 @@ export function defaultLogger(
798
811
  tail = false
799
812
  killStatus()
800
813
  },
801
- taskStart(node) {
814
+ taskStart(node, info) {
802
815
  if (isGroupTask(node)) return
803
816
  if (holdsTerminal(node)) {
804
817
  killStatus()
@@ -820,7 +833,12 @@ export function defaultLogger(
820
833
  }
821
834
  // The rows are the status region's; with none, nothing reads them.
822
835
  if (statusDead) return
823
- const slot: WorkerSlot = { id: node.id, startedMs: Date.now() }
836
+ const slot: WorkerSlot = {
837
+ id: node.id,
838
+ startedMs: Date.now(),
839
+ cache: ranNoCache({ node }) ? 'no-cache' : 'miss',
840
+ ...(waited(info?.waitedMs) ? { waitedMs: info!.waitedMs! } : {}),
841
+ }
824
842
  const free = slots.indexOf(null)
825
843
  if (free >= 0) slots[free] = slot
826
844
  else slotQueue.push(slot)
@@ -1016,7 +1034,7 @@ export function defaultLogger(
1016
1034
  // Otherwise a row in the task list, in every view that lists a
1017
1035
  // failure: the run exits red, and the row names why.
1018
1036
  if (view.mode !== 'none' && view.mode !== 'hash-only') {
1019
- emitRow(formatTaskAbortedLine(node, outcome, colors))
1037
+ emitRow(formatTaskAbortedLine(node, outcome, colors, blocksOf(node.id)))
1020
1038
  }
1021
1039
  return
1022
1040
  }
@@ -1099,7 +1117,7 @@ export function defaultLogger(
1099
1117
  }
1100
1118
  case 'errors-only':
1101
1119
  if (outcome.status === 'skipped') {
1102
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1120
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1103
1121
  return
1104
1122
  }
1105
1123
  if (outcome.status !== 'failed') return
@@ -1136,9 +1154,18 @@ export function defaultLogger(
1136
1154
  ),
1137
1155
  )
1138
1156
  } else if (outcome.status === 'success') {
1139
- emitRow(formatTaskExecutedLine(node, outcome, colors, onPath.has(node.id)))
1157
+ emitRow(
1158
+ formatTaskExecutedLine(
1159
+ node,
1160
+ outcome,
1161
+ colors,
1162
+ onPath.has(node.id),
1163
+ blocksOf(node.id),
1164
+ usualOf?.get(node.id),
1165
+ ),
1166
+ )
1140
1167
  } else if (outcome.status === 'skipped') {
1141
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1168
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1142
1169
  }
1143
1170
  return
1144
1171
  case 'focused':
@@ -1147,7 +1174,7 @@ export function defaultLogger(
1147
1174
  // stopped), so no frame-open fired — and a skip has no output,
1148
1175
  // so a one-liner carries everything a frame would.
1149
1176
  if (outcome.status === 'skipped') {
1150
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1177
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1151
1178
  return
1152
1179
  }
1153
1180
  if (streamsLive(node)) {
@@ -1198,7 +1225,7 @@ export function defaultLogger(
1198
1225
  // Dependency-pulled nodes: silent on success; a skip gets its
1199
1226
  // row, failures the ◼ row now and their frame replayed at runEnd.
1200
1227
  if (outcome.status === 'skipped') {
1201
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1228
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1202
1229
  } else if (outcome.status === 'failed') {
1203
1230
  emitRow(failureRow(node, outcome))
1204
1231
  deferredFailures.push(
@@ -1222,11 +1249,13 @@ export function defaultLogger(
1222
1249
  // point); misses/failures are always framed. One-liners
1223
1250
  // stay outside ::group:: — there's nothing to collapse.
1224
1251
  if (isHit && output.every((c) => c.text.trim().length === 0)) {
1225
- emitRow(formatTaskHitLine(node, outcome, colors, onPath.has(node.id)))
1252
+ emitRow(
1253
+ formatTaskHitLine(node, outcome, colors, onPath.has(node.id), blocksOf(node.id)),
1254
+ )
1226
1255
  return
1227
1256
  }
1228
1257
  if (outcome.status === 'skipped') {
1229
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1258
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1230
1259
  return
1231
1260
  }
1232
1261
  const block = formatTaskBlock(
@@ -42,7 +42,7 @@ export function plainOutput(sink: Logger): Logger {
42
42
  status: (line) => sink.status(line),
43
43
  ...(sink.warn && { warn: (line) => sink.warn!(line) }),
44
44
  ...(sink.runStart && { runStart: (info) => sink.runStart!(info) }),
45
- ...(sink.taskStart && { taskStart: (node) => sink.taskStart!(node) }),
45
+ ...(sink.taskStart && { taskStart: (node, info) => sink.taskStart!(node, info) }),
46
46
  ...(sink.runEnd && { runEnd: () => sink.runEnd!() }),
47
47
  ...(sink.stepStart && { stepStart: (id) => sink.stepStart!(id) }),
48
48
  ...(sink.stepEnd && { stepEnd: (id, info) => sink.stepEnd!(id, info) }),
@@ -7,11 +7,11 @@
7
7
 
8
8
  import { dumbTerminal, paint, type ColorSupport } from '../util/index.js'
9
9
  import {
10
- formatListHeader,
10
+ formatRunningHeader,
11
11
  formatTaskRow,
12
12
  paintIdParts,
13
13
  pathNote,
14
- placeNote,
14
+ type RowCells,
15
15
  } from './framed-output.js'
16
16
  import type { BlockedTask } from './forecast.js'
17
17
 
@@ -327,6 +327,16 @@ export interface WorkerSlot {
327
327
  startedMs: number
328
328
  /** Its executor holds it for a machine to begin it (`TaskExecutor.running` is `null`). */
329
329
  waiting?: boolean
330
+ /** How long it waited ready for a slot; absent when it started at once. */
331
+ waitedMs?: number
332
+ /** Its cache cell: `no-cache` for a task that caches nothing, else `miss` (the default). */
333
+ cache?: 'miss' | 'no-cache'
334
+ }
335
+
336
+ /** What a running task's process tree uses now. */
337
+ export interface LiveUsage {
338
+ cores: number
339
+ rssBytes: number
330
340
  }
331
341
 
332
342
  export interface StatusRegionState {
@@ -336,6 +346,8 @@ export interface StatusRegionState {
336
346
  where?: (taskId: string) => string | null | undefined
337
347
  /** A running task's usual duration from history; its row shows it. */
338
348
  expected?: ReadonlyMap<string, number>
349
+ /** A running task's live CPU and memory; its row's `cpu` and `mem`. */
350
+ usage?: (taskId: string) => LiveUsage | undefined
339
351
  /**
340
352
  * Ready persistent tasks (dev servers …) whose children keep
341
353
  * running after their outcome lands. Pinned until runEnd — the
@@ -395,6 +407,7 @@ export function formatFailureLine(
395
407
  durationMs: number | null,
396
408
  colors: ColorSupport = NO_COLOR,
397
409
  cacheWord: 'miss' | 'no-cache' = 'miss',
410
+ cells: RowCells = {},
398
411
  ): string {
399
412
  return formatTaskRow(
400
413
  paint(ERROR, '◼\uFE0E', colors),
@@ -405,6 +418,7 @@ export function formatFailureLine(
405
418
  '',
406
419
  paintPinnedId(id, colors),
407
420
  colors,
421
+ cells,
408
422
  )
409
423
  }
410
424
 
@@ -431,6 +445,7 @@ export function formatStepLine(
431
445
  '',
432
446
  paintPinnedId(id, colors),
433
447
  colors,
448
+ { worker: 'local' },
434
449
  )
435
450
  }
436
451
 
@@ -447,6 +462,12 @@ export function formatStatusRegion(
447
462
  colors: ColorSupport = NO_COLOR,
448
463
  ): string[] {
449
464
  const dim = (t: string) => paint('', t, colors, { dim: true })
465
+ // A task's usual duration, the live list's `usual` cell: `~18s`.
466
+ const usualOf = (id: string): { text: string; painted: string } => {
467
+ const ms = s.expected?.get(id)
468
+ const text = ms === undefined ? '' : formatExpected(ms)
469
+ return { text, painted: dim(text) }
470
+ }
450
471
  // Leading blank separates the live running region from the completed
451
472
  // task list scrolling above it (owner: "separate running from list").
452
473
  const lines: string[] = ['']
@@ -463,8 +484,7 @@ export function formatStatusRegion(
463
484
  '',
464
485
  paintPinnedId(id, colors),
465
486
  colors,
466
- // A blank `blocked` cell, so the id sits under the header's `task`.
467
- '',
487
+ { worker: 'local' },
468
488
  ),
469
489
  )
470
490
  }
@@ -507,27 +527,31 @@ export function formatStatusRegion(
507
527
  '',
508
528
  paintPinnedId(slot.id, colors),
509
529
  colors,
510
- '',
530
+ { worker: s.where?.(slot.id) ?? 'local' },
511
531
  ),
512
532
  )
513
533
  continue
514
534
  }
515
535
  const at = s.where?.(slot.id)
516
- const usual = s.expected?.get(slot.id)
536
+ const use = s.usage?.(slot.id)
517
537
  lines.push(
518
538
  formatTaskRow(
519
539
  ' ',
520
540
  Math.max(0, s.nowMs - slot.startedMs),
521
541
  'running',
522
542
  ACCENT,
523
- usual === undefined ? '' : formatExpected(usual),
543
+ slot.cache ?? 'miss',
524
544
  '',
525
545
  paintPinnedId(slot.id, colors),
526
546
  colors,
527
- String(s.blockedBy?.get(slot.id) ?? 0),
528
- ) +
529
- (s.critical === slot.id ? pathNote(colors) : '') +
530
- (at != null ? placeNote(at, colors) : ''),
547
+ {
548
+ usual: usualOf(slot.id),
549
+ ...(slot.waitedMs !== undefined ? { waitedMs: slot.waitedMs } : {}),
550
+ ...(use !== undefined ? { cpu: use.cores, memBytes: use.rssBytes } : {}),
551
+ blocks: String(s.blockedBy?.get(slot.id) ?? 0),
552
+ worker: at ?? 'local',
553
+ },
554
+ ) + (s.critical === slot.id ? pathNote(colors) : ''),
531
555
  )
532
556
  for (const b of nested.get(slot.id) ?? []) {
533
557
  rows++
@@ -541,7 +565,7 @@ export function formatStatusRegion(
541
565
  '',
542
566
  `${dim('\u2514')} ${paintPinnedId(b.id, colors)}`,
543
567
  colors,
544
- '',
568
+ { usual: usualOf(b.id) },
545
569
  ),
546
570
  )
547
571
  }
@@ -558,11 +582,11 @@ export function formatStatusRegion(
558
582
  '',
559
583
  paintPinnedId(step.id, colors),
560
584
  colors,
561
- '',
585
+ { worker: 'local' },
562
586
  ),
563
587
  )
564
588
  }
565
- if (lines.length > 1) lines.splice(1, 0, formatListHeader(placed, colors, true))
589
+ if (lines.length > 1) lines.splice(1, 0, formatRunningHeader(colors))
566
590
  // The live summary section — identical shape to the final printout,
567
591
  // so the region visually BECOMES the summary when the run ends.
568
592
  lines.push(...s.summaryLines)
@@ -223,6 +223,10 @@ export interface RunContext {
223
223
  executorRows?: () => readonly (readonly [string, ui.Inline])[]
224
224
  /** Where a running task runs, `null` while it waits for a machine (`TaskExecutor.running`): its live row. */
225
225
  whereRunning?: (taskId: string) => string | null | undefined
226
+ /** A running task's live CPU and memory: its live row's `cpu` and `mem`. */
227
+ usage?: (taskId: string) => { cores: number; rssBytes: number } | undefined
228
+ /** Each task's count of the tasks downstream of it in the run, built on first ask: the rows' `blocks`. */
229
+ blocks?: () => ReadonlyMap<string, number>
226
230
  /** Worker-pool size for this run; shown on the `info` row. */
227
231
  concurrency?: number
228
232
  remoteCacheEnabled: boolean
@@ -12,6 +12,7 @@
12
12
  const RESET = '\x1b[0m'
13
13
  const BOLD = '\x1b[1m'
14
14
  const DIM = '\x1b[2m'
15
+ const UNDERLINE = '\x1b[4m'
15
16
 
16
17
  // State hues every report shares with the run summary.
17
18
  export const SUCCESS = '#22c55e'
@@ -27,6 +28,7 @@ export interface ColorSupport {
27
28
  export interface PaintOptions {
28
29
  bold?: boolean
29
30
  dim?: boolean
31
+ underline?: boolean
30
32
  }
31
33
 
32
34
  /**
@@ -102,6 +104,7 @@ export function paint(
102
104
  let prefix = ''
103
105
  if (opts.bold) prefix += BOLD
104
106
  if (opts.dim) prefix += DIM
107
+ if (opts.underline) prefix += UNDERLINE
105
108
  if (color) prefix += ansiFor(color)
106
109
  if (prefix.length === 0) return text
107
110
  return `${prefix}${text}${RESET}`