@vzn/vx 0.0.649 → 0.0.651

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 (122) hide show
  1. package/README.md +5 -20
  2. package/package.json +5 -5
  3. package/schemas/last.json +15 -0
  4. package/src/bin.ts +9 -31
  5. package/src/cache/cache.ts +225 -51
  6. package/src/cache/file-hashes.ts +3 -0
  7. package/src/cache/fold-memo.ts +8 -1
  8. package/src/cache/git-inputs.ts +188 -39
  9. package/src/cache/index.ts +12 -0
  10. package/src/cache/inputs.ts +3 -0
  11. package/src/cache/layer.ts +11 -0
  12. package/src/cache/layered-cache.ts +4 -0
  13. package/src/cache/linked-inputs.ts +3 -0
  14. package/src/cache/lockfile-memo.ts +120 -0
  15. package/src/cache/output-index.ts +5 -2
  16. package/src/cache/remote-hits.ts +9 -1
  17. package/src/cache/remote-lane.ts +2 -1
  18. package/src/cache/run-history.ts +11 -1
  19. package/src/cache/run-notes.ts +123 -0
  20. package/src/cache/schema.ts +54 -1
  21. package/src/cli/cache.ts +26 -16
  22. package/src/cli/completions.ts +4 -2
  23. package/src/cli/docs.ts +10 -11
  24. package/src/cli/help.ts +12 -23
  25. package/src/cli/index.ts +4 -9
  26. package/src/cli/info.ts +15 -7
  27. package/src/cli/init.ts +23 -15
  28. package/src/cli/last.ts +224 -223
  29. package/src/cli/lock.ts +55 -5
  30. package/src/cli/plan-format.ts +100 -118
  31. package/src/cli/plugin-commands.ts +10 -3
  32. package/src/cli/plugin-templates.ts +2 -2
  33. package/src/cli/run.ts +46 -27
  34. package/src/cli/select.ts +43 -5
  35. package/src/cli/show.ts +38 -42
  36. package/src/cli/task-verb.ts +22 -0
  37. package/src/cli/upgrade.ts +3 -1
  38. package/src/cli/watch-fs.ts +6 -1
  39. package/src/cli/watch-judge.ts +2 -1
  40. package/src/cli/watch.ts +83 -44
  41. package/src/cli/why.ts +126 -92
  42. package/src/cli/workspace-config.ts +2 -1
  43. package/src/exec/executor.ts +2 -1
  44. package/src/exec/kill-tree.ts +58 -5
  45. package/src/exec/local-executor.ts +1 -1
  46. package/src/exec/pipes.ts +152 -0
  47. package/src/exec/runner.ts +199 -182
  48. package/src/exec/sandbox-binds.ts +1 -1
  49. package/src/exec/sandbox-runtime.ts +11 -2
  50. package/src/exec/spawn-off.ts +154 -0
  51. package/src/exec/warm-shell.ts +146 -0
  52. package/src/graph/critical-path.ts +40 -28
  53. package/src/graph/priorities.ts +98 -44
  54. package/src/graph/scheduler.ts +12 -2
  55. package/src/graph/task-graph.ts +27 -1
  56. package/src/handoff.ts +10 -7
  57. package/src/history/history.ts +39 -2
  58. package/src/history/index.ts +3 -0
  59. package/src/history/metrics.ts +9 -1
  60. package/src/index.ts +12 -3
  61. package/src/orchestrator/doctor.ts +3 -3
  62. package/src/orchestrator/execute-task.ts +16 -2
  63. package/src/orchestrator/graph-memo.ts +50 -1
  64. package/src/orchestrator/graph-stage.ts +6 -0
  65. package/src/orchestrator/hit-restore.ts +3 -0
  66. package/src/orchestrator/index.ts +3 -1
  67. package/src/orchestrator/live-usage.ts +98 -0
  68. package/src/orchestrator/locked-digests.ts +63 -0
  69. package/src/orchestrator/lockfile-claim.ts +176 -102
  70. package/src/orchestrator/miss-save.ts +31 -3
  71. package/src/orchestrator/persistent-task.ts +3 -1
  72. package/src/orchestrator/plan.ts +5 -1
  73. package/src/orchestrator/plugin-host.ts +61 -0
  74. package/src/orchestrator/plugin.ts +49 -4
  75. package/src/orchestrator/prepare.ts +18 -2
  76. package/src/orchestrator/projects.ts +4 -1
  77. package/src/orchestrator/run-context.ts +29 -10
  78. package/src/orchestrator/run-failures.ts +35 -114
  79. package/src/orchestrator/run-records.ts +10 -0
  80. package/src/orchestrator/run.ts +89 -26
  81. package/src/orchestrator/sandbox-request.ts +2 -2
  82. package/src/orchestrator/signals.ts +8 -5
  83. package/src/orchestrator/steps.ts +2 -2
  84. package/src/orchestrator/task-hash.ts +17 -3
  85. package/src/report/forecast.ts +12 -33
  86. package/src/report/framed-output.ts +388 -161
  87. package/src/report/index.ts +8 -2
  88. package/src/report/logger.ts +100 -36
  89. package/src/report/status-line.ts +81 -365
  90. package/src/report/summary.ts +104 -131
  91. package/src/ui/ansi.ts +48 -0
  92. package/src/ui/components.ts +506 -0
  93. package/src/ui/index.ts +79 -0
  94. package/src/ui/kit.ts +37 -0
  95. package/src/ui/live-region.ts +363 -0
  96. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  97. package/src/ui/terminal.ts +51 -0
  98. package/src/ui/theme.ts +224 -0
  99. package/src/ui/version-line.ts +28 -0
  100. package/src/util/config-frame.ts +65 -10
  101. package/src/util/edit-distance.ts +6 -1
  102. package/src/util/hidden-prompt.ts +8 -2
  103. package/src/util/index.ts +2 -15
  104. package/src/util/paths.ts +20 -2
  105. package/src/util/secret-mask.ts +39 -3
  106. package/src/util/secrets.ts +3 -0
  107. package/src/util/settle.ts +31 -0
  108. package/src/util/timing.ts +4 -16
  109. package/src/workspace/affected.ts +89 -30
  110. package/src/workspace/config-schema.ts +4 -2
  111. package/src/workspace/dependencies-lock.ts +191 -0
  112. package/src/workspace/filter.ts +28 -0
  113. package/src/workspace/index.ts +9 -0
  114. package/src/workspace/lockfile.ts +159 -6
  115. package/src/workspace/migration.ts +4 -3
  116. package/src/workspace/workspace.ts +3 -0
  117. package/src/orchestrator/run-steps.ts +0 -93
  118. package/src/util/colors.ts +0 -130
  119. package/src/util/report.ts +0 -44
  120. package/src/util/ui.ts +0 -258
  121. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  122. /package/src/{report → ui}/terminal-signals.ts +0 -0
@@ -1,332 +1,33 @@
1
- // Dynamic status display for interactive runs. NOT a TUI: a
2
- // fixed-height worker region redrawn in place with clear-line /
3
- // cursor-up — no alternate screen.
4
- // The writer is the serialization point for ALL logger stdout: every
5
- // ordinary write erases the status display first, writes its content,
6
- // then redraws, so the display can never interleave with task output.
1
+ // The live region's panels: Warnings, Running and the live Summary,
2
+ // redrawn in place under the log by the ui module's writer.
7
3
 
8
- import { dumbTerminal, paint, type ColorSupport } from '../util/index.js'
4
+ import { paint, panelTop, type ColorSupport } from '../ui/index.js'
9
5
  import {
10
- formatListHeader,
6
+ formatExpected,
7
+ formatRunningHeader,
11
8
  formatTaskRow,
12
9
  paintIdParts,
13
10
  pathNote,
14
- placeNote,
11
+ type RowCells,
12
+ unknownUsual,
15
13
  } from './framed-output.js'
16
14
  import type { BlockedTask } from './forecast.js'
17
15
 
18
16
  const NO_COLOR: ColorSupport = { enabled: false }
19
- // Same palette as framed-output.ts so glyphs and stats agree.
20
- const ACCENT = '#06b6d4'
21
- const ERROR = '#ef4444'
22
- const WARN = '#eab308' // yellow-500, as the summary's
23
- const SUCCESS = '#22c55e'
24
-
25
- /** Erase the current line and return the cursor to column 0. */
26
- const CLEAR = '\x1b[2K\r'
27
-
28
- export interface StatusStream {
29
- write(chunk: string): unknown
30
- isTTY?: boolean
31
- /**
32
- * Terminal width in columns. Read fresh on every draw — `process.stdout`
33
- * updates it on SIGWINCH, so a mid-run resize is picked up. Absent (not a
34
- * TTY) means the region is never drawn anyway.
35
- */
36
- columns?: number
37
- /** Terminal height in rows, read per draw as `columns` is. */
38
- rows?: number
39
- }
40
-
41
- /** A region's lines, or what builds them when it is drawn. */
42
- export type Region = readonly string[] | (() => readonly string[])
43
-
44
- export interface OutputWriter {
45
- /** True only when the stream is a TTY and the run isn't CI. */
46
- readonly enabled: boolean
47
- /** Serialized content write: erase status → content → redraw. */
48
- write(chunk: string): void
49
- /**
50
- * The same, to another stream on the same terminal (stderr): the region
51
- * on this one is erased first and redrawn after.
52
- */
53
- writeTo(target: StatusStream, chunk: string): void
54
- /**
55
- * Replace the status line's content. Redraws are throttled to
56
- * `minRedrawMs` unless `force` (task start/finish) is set.
57
- */
58
- setStatus(line: string, opts?: { force?: boolean }): void
59
- /**
60
- * Replace the multi-line status region. Same throttle contract as
61
- * `setStatus`; the two share one display slot. A function is called
62
- * when the region is next drawn, once: a set the throttle coalesces
63
- * builds nothing.
64
- */
65
- setRegion(lines: Region, opts?: { force?: boolean }): void
66
- /** Hand over what `coalesce` holds and write straight through from now on. */
67
- settle(): void
68
- /** Permanently remove the status display; later set* is a no-op. */
69
- clearStatus(): void
70
- /**
71
- * Erase the status display and draw none until the returned release:
72
- * a prompt on the terminal (a secret's) waits for its answer there.
73
- */
74
- hold(): () => void
75
- }
76
-
77
- export interface OutputWriterOptions {
78
- /** Master switch on top of the TTY check (false in CI). */
79
- enabled?: boolean
80
- /** Minimum interval between unforced redraws. */
81
- minRedrawMs?: number
82
- /**
83
- * Floor between FORCED redraws. Task events force a redraw each; a
84
- * 3,270-task warm run produced 6,540 forced redraws ≈ 6.7 MB of
85
- * ANSI to the terminal. A forced set inside the floor marks the
86
- * content dirty and ONE trailing draw lands the final state when
87
- * the floor expires. 0 disables (tests asserting synchronously).
88
- */
89
- forceFloorMs?: number
90
- /** Injectable clock for tests. */
91
- now?: () => number
92
- /**
93
- * Off a TTY, hold writes and hand them to the stream once per turn of
94
- * the event loop until `settle()`. A warm 476-package run wrote each of
95
- * its 952 task lines as its own syscall (item 753). Off by default: a
96
- * caller reading its stream right after a write sees it.
97
- */
98
- coalesce?: boolean
99
- }
100
-
101
- export function createOutputWriter(
102
- stream: StatusStream,
103
- opts: OutputWriterOptions = {},
104
- ): OutputWriter {
105
- const enabled = (opts.enabled ?? true) && stream.isTTY === true && !dumbTerminal()
106
- const minRedrawMs = opts.minRedrawMs ?? 100
107
- const forceFloorMs = opts.forceFloorMs ?? 30
108
- const now = opts.now ?? Date.now
109
- // The coalesced writes not yet handed over; null once settled or when
110
- // not coalescing.
111
- let held: string[] | null = !enabled && opts.coalesce === true ? [] : null
112
- let flushQueued = false
113
- const flushHeld = (): void => {
114
- flushQueued = false
115
- if (held === null || held.length === 0) return
116
- const text = held.join('')
117
- held.length = 0
118
- stream.write(text)
119
- }
120
-
121
- let current: Region | null = null
122
- let shown = false
123
- // PHYSICAL rows the shown region occupies, not logical lines — see
124
- // `regionRows`.
125
- let shownRows = 0
126
- let dead = false
127
- let holds = 0
128
- let lastDraw = -Infinity
129
- // Trailing draw scheduled when a forced set lands inside the floor.
130
- let trailing: ReturnType<typeof setTimeout> | null = null
131
- // Streamed task output (focused mode) can end mid-line; redrawing
132
- // the status display would wipe the partial line, so we hold off
133
- // until a write restores column 0.
134
- let atLineStart = true
135
-
136
- const cancelTrailing = (): void => {
137
- if (trailing !== null) {
138
- clearTimeout(trailing)
139
- trailing = null
140
- }
141
- }
142
-
143
- /**
144
- * How many PHYSICAL terminal rows `lines` occupies. The terminal wraps
145
- * any line wider than the viewport, so a region of N logical lines can
146
- * cover more than N rows — and erasing by the logical count moves the
147
- * cursor up too few rows, leaving the top of the region on screen. It
148
- * accumulates, because every redraw is short by the same amount.
149
- *
150
- * Both triggers are ordinary: the summary section is a fixed 62 visible
151
- * columns (narrower terminal ⇒ every bar row wraps), and a task id is
152
- * deliberately never truncated (long id ⇒ the worker row wraps at any
153
- * width). Width is read per draw so a mid-run resize is handled. Width
154
- * must be VISIBLE width — these lines carry ANSI, which occupies no
155
- * column.
156
- *
157
- * Unknown width falls back to the logical count, which is a KNOWN
158
- * residual rather than a guarantee: a wrapped line then under-erases
159
- * exactly as it did before this function existed. It is deliberate,
160
- * because the alternatives are worse — guessing a width over-erases on a
161
- * wider terminal, and `ESC[J` clears to end of SCREEN, so over-erasing
162
- * destroys output above the region instead of merely leaving junk below
163
- * it. Reachable only on a TTY whose winsize ioctl fails, since a non-TTY
164
- * disables the region outright.
165
- */
166
- const lineRows = (line: string): number => {
167
- const cols = stream.columns
168
- if (cols === undefined || cols <= 0) return 1
169
- return Math.max(1, Math.ceil(Bun.stringWidth(line) / cols))
170
- }
171
- const regionRows = (lines: readonly string[]): number => {
172
- let rows = 0
173
- for (const line of lines) rows += lineRows(line)
174
- return rows
175
- }
176
-
177
- /**
178
- * The leading lines that fit the terminal's height, one row spared.
179
- * Cursor-up stops at the top of the screen, and a row that scrolled into
180
- * the scrollback is out of the erase's reach: a region taller than the
181
- * terminal left its top behind on every redraw, one stacked block per tick
182
- * (VS Code's short panel, 2026-10-14). The first line always shows.
183
- */
184
- const fitted = (lines: readonly string[]): readonly string[] => {
185
- const height = stream.rows
186
- if (height === undefined || height <= 0) return lines
187
- let rows = 0
188
- let n = 0
189
- for (const line of lines) {
190
- rows += lineRows(line)
191
- if (n > 0 && rows > height - 1) break
192
- n++
193
- }
194
- return n === lines.length ? lines : lines.slice(0, n)
195
- }
196
-
197
- // Erase sequence for whatever is currently shown. A single row keeps
198
- // the exact legacy bytes (ESC[2K\r); anything taller moves to its
199
- // top row and clears to end of screen.
200
- const eraseSeq = (): string => (shownRows > 1 ? `\r\x1b[${shownRows - 1}A\x1b[J` : CLEAR)
201
-
202
- const draw = (): void => {
203
- // Whatever was pending is now painted — the trailing draw would
204
- // only repeat these bytes.
205
- cancelTrailing()
206
- const erase = shown ? eraseSeq() : CLEAR
207
- // Built here, not at the set: a warm run's task events set the
208
- // region hundreds of times inside one floor, and each build was a
209
- // forecast pass and a summary render no frame showed (200 hits on a
210
- // terminal: run graph 62 → 9 ms). Kept built: a write redraws it.
211
- if (typeof current === 'function') current = current()
212
- const lines = fitted(current!)
213
- stream.write(erase + lines.join('\n'))
214
- shown = true
215
- shownRows = regionRows(lines)
216
- lastDraw = now()
217
- }
218
- const set = (lines: Region, o: { force?: boolean }): void => {
219
- if (!enabled || dead) return
220
- current = lines
221
- if (!atLineStart || holds > 0) return
222
- const since = now() - lastDraw
223
- if (o.force) {
224
- if (since >= forceFloorMs) {
225
- draw()
226
- return
227
- }
228
- // Coalesce the burst: `current` is already the latest state, so
229
- // one trailing draw at floor expiry lands it. unref — a stray
230
- // timer must never hold the process open.
231
- if (trailing === null) {
232
- trailing = setTimeout(() => {
233
- trailing = null
234
- if (dead || current === null || !atLineStart || holds > 0) return
235
- draw()
236
- }, forceFloorMs - since)
237
- trailing.unref?.()
238
- }
239
- return
240
- }
241
- if (since >= minRedrawMs) draw()
242
- }
243
-
244
- return {
245
- enabled,
246
- write(chunk) {
247
- if (!enabled) {
248
- if (held === null) {
249
- stream.write(chunk)
250
- return
251
- }
252
- held.push(chunk)
253
- if (!flushQueued) {
254
- flushQueued = true
255
- setImmediate(flushHeld)
256
- }
257
- return
258
- }
259
- if (shown) {
260
- stream.write(eraseSeq())
261
- shown = false
262
- shownRows = 0
263
- }
264
- stream.write(chunk)
265
- if (chunk.length > 0) atLineStart = chunk.endsWith('\n')
266
- if (!dead && current !== null && atLineStart && holds === 0) draw()
267
- },
268
- writeTo(target, chunk) {
269
- if (target === stream) return this.write(chunk)
270
- // Held stdout is older than this chunk.
271
- flushHeld()
272
- if (!enabled) {
273
- target.write(chunk)
274
- return
275
- }
276
- if (shown) {
277
- stream.write(eraseSeq())
278
- shown = false
279
- shownRows = 0
280
- }
281
- target.write(chunk)
282
- if (!dead && current !== null && atLineStart && holds === 0) draw()
283
- },
284
- setStatus(line, o = {}) {
285
- set([line], o)
286
- },
287
- setRegion(lines, o = {}) {
288
- set(lines, o)
289
- },
290
- settle() {
291
- flushHeld()
292
- held = null
293
- },
294
- clearStatus() {
295
- if (!enabled || dead) return
296
- dead = true
297
- current = null
298
- cancelTrailing()
299
- if (shown) {
300
- stream.write(eraseSeq())
301
- shown = false
302
- shownRows = 0
303
- }
304
- },
305
- hold() {
306
- if (!enabled || dead) return () => undefined
307
- holds++
308
- cancelTrailing()
309
- if (shown) {
310
- stream.write(eraseSeq())
311
- shown = false
312
- shownRows = 0
313
- }
314
- let released = false
315
- return () => {
316
- if (released) return
317
- released = true
318
- holds--
319
- if (holds === 0 && !dead && current !== null && atLineStart) draw()
320
- }
321
- },
322
- }
323
- }
324
17
 
325
18
  export interface WorkerSlot {
326
19
  id: string
327
20
  startedMs: number
328
21
  /** Its executor holds it for a machine to begin it (`TaskExecutor.running` is `null`). */
329
22
  waiting?: boolean
23
+ /** Its cache cell: `no-cache` for a task that caches nothing, else `miss` (the default). */
24
+ cache?: 'miss' | 'no-cache'
25
+ }
26
+
27
+ /** What a running task's process tree uses now. */
28
+ export interface LiveUsage {
29
+ cores: number
30
+ rssBytes: number
330
31
  }
331
32
 
332
33
  export interface StatusRegionState {
@@ -334,8 +35,13 @@ export interface StatusRegionState {
334
35
  critical?: string
335
36
  /** Where a running task runs, when not here (`TaskExecutor.running`); its row shows it. */
336
37
  where?: (taskId: string) => string | null | undefined
337
- /** A running task's usual duration from history; its row shows it. */
338
- expected?: ReadonlyMap<string, number>
38
+ /**
39
+ * A task's usual duration from history, undefined for one it never
40
+ * timed; its row shows it, `unknown` for none. Absent: no history to ask.
41
+ */
42
+ usual?: (taskId: string) => number | undefined
43
+ /** A running task's live CPU and memory; its row's `cpu` and `mem`. */
44
+ usage?: (taskId: string) => LiveUsage | undefined
339
45
  /**
340
46
  * Ready persistent tasks (dev servers …) whose children keep
341
47
  * running after their outcome lands. Pinned until runEnd — the
@@ -395,16 +101,18 @@ export function formatFailureLine(
395
101
  durationMs: number | null,
396
102
  colors: ColorSupport = NO_COLOR,
397
103
  cacheWord: 'miss' | 'no-cache' = 'miss',
104
+ cells: RowCells = {},
398
105
  ): string {
399
106
  return formatTaskRow(
400
- paint(ERROR, '◼\uFE0E', colors),
107
+ paint('error', '\u25fc\uFE0E', colors),
401
108
  durationMs,
402
109
  'failed',
403
- ERROR,
110
+ 'error',
404
111
  cacheWord,
405
112
  '',
406
113
  paintPinnedId(id, colors),
407
114
  colors,
115
+ cells,
408
116
  )
409
117
  }
410
118
 
@@ -423,17 +131,23 @@ export function formatStepLine(
423
131
  colors: ColorSupport = NO_COLOR,
424
132
  ): string {
425
133
  return formatTaskRow(
426
- ok ? paint(SUCCESS, '\u23fa\uFE0E', colors) : paint(ERROR, '\u25fc\uFE0E', colors),
134
+ ok ? paint('ok', '\u23fa\uFE0E', colors) : paint('error', '\u25fc\uFE0E', colors),
427
135
  durationMs,
428
136
  ok ? 'success' : 'failed',
429
- ok ? SUCCESS : ERROR,
137
+ ok ? 'ok' : 'error',
430
138
  INTERNAL,
431
139
  '',
432
140
  paintPinnedId(id, colors),
433
141
  colors,
142
+ { worker: 'local' },
434
143
  )
435
144
  }
436
145
 
146
+ /** `rows` in a panel titled `title`, counted. */
147
+ function panel(title: string, count: string, rows: readonly string[], c: ColorSupport): string[] {
148
+ return [panelTop(title, c, count), ...rows]
149
+ }
150
+
437
151
  /**
438
152
  * Worker region: pinned ready persistent tasks, one row per worker
439
153
  * slot — a task stays in its slot for its whole life so names never
@@ -446,25 +160,31 @@ export function formatStatusRegion(
446
160
  s: StatusRegionState,
447
161
  colors: ColorSupport = NO_COLOR,
448
162
  ): string[] {
449
- const dim = (t: string) => paint('', t, colors, { dim: true })
450
- // Leading blank separates the live running region from the completed
451
- // task list scrolling above it (owner: "separate running from list").
452
- const lines: string[] = ['']
163
+ const dim = (t: string) => paint('muted', t, colors)
164
+ // A task's usual duration, the live list's `usual` cell: `~18s`.
165
+ const usualOf = (id: string): { text: string; painted: string } => {
166
+ if (s.usual === undefined) return { text: '', painted: '' }
167
+ const ms = s.usual(id)
168
+ if (ms === undefined) return unknownUsual(colors)
169
+ const text = formatExpected(ms)
170
+ return { text, painted: dim(text) }
171
+ }
172
+ // The Running panel's rows; its box and header go round them below.
173
+ const lines: string[] = []
453
174
  // Persistent (dev-server) rows: ▸ glyph, no elapsed, `running`, no
454
175
  // cache state (it's still alive).
455
176
  for (const id of s.pinnedPersistent) {
456
177
  lines.push(
457
178
  formatTaskRow(
458
- paint(ACCENT, '▸', colors),
179
+ paint('info', '▸', colors),
459
180
  null,
460
181
  'running',
461
- ACCENT,
182
+ 'info',
462
183
  '',
463
184
  '',
464
185
  paintPinnedId(id, colors),
465
186
  colors,
466
- // A blank `blocked` cell, so the id sits under the header's `task`.
467
- '',
187
+ { worker: 'local' },
468
188
  ),
469
189
  )
470
190
  }
@@ -473,8 +193,9 @@ export function formatStatusRegion(
473
193
  // the ticking time IS the motion), then `running`, the usual duration,
474
194
  // how many tasks wait on it, and the full id (never truncated — name is
475
195
  // the last column). The rows no task runs in hold the tasks planned
476
- // next, each nested under what it waits on; the rest stay blank, so the
477
- // list keeps its height.
196
+ // next, each nested under what it waits on. No blank rows pad the list
197
+ // to the worker count: a run using 2 of 12 workers left 10 empty lines
198
+ // between its rows and the footer (2026-10-29).
478
199
  const running = s.slots.filter((x): x is WorkerSlot => x !== null && x.waiting !== true)
479
200
  // Placed by an executor (DTE), the list is what runs: blocked tasks are
480
201
  // the tasks line's count alone, and its size is no machine's.
@@ -493,44 +214,44 @@ export function formatStatusRegion(
493
214
  n++
494
215
  }
495
216
  }
496
- let rows = 0
497
217
  for (const slot of running) {
498
- rows++
499
218
  if (s.stopping === true) {
500
219
  lines.push(
501
220
  formatTaskRow(
502
221
  ' ',
503
222
  Math.max(0, s.nowMs - slot.startedMs),
504
223
  'aborting',
505
- WARN,
224
+ 'warn',
506
225
  '',
507
226
  '',
508
227
  paintPinnedId(slot.id, colors),
509
228
  colors,
510
- '',
229
+ { worker: s.where?.(slot.id) ?? 'local' },
511
230
  ),
512
231
  )
513
232
  continue
514
233
  }
515
234
  const at = s.where?.(slot.id)
516
- const usual = s.expected?.get(slot.id)
235
+ const use = s.usage?.(slot.id)
517
236
  lines.push(
518
237
  formatTaskRow(
519
238
  ' ',
520
239
  Math.max(0, s.nowMs - slot.startedMs),
521
240
  'running',
522
- ACCENT,
523
- usual === undefined ? '' : formatExpected(usual),
241
+ 'info',
242
+ slot.cache ?? 'miss',
524
243
  '',
525
244
  paintPinnedId(slot.id, colors),
526
245
  colors,
527
- String(s.blockedBy?.get(slot.id) ?? 0),
528
- ) +
529
- (s.critical === slot.id ? pathNote(colors) : '') +
530
- (at != null ? placeNote(at, colors) : ''),
246
+ {
247
+ usual: usualOf(slot.id),
248
+ ...(use !== undefined ? { cpu: use.cores, memBytes: use.rssBytes } : {}),
249
+ blocks: String(s.blockedBy?.get(slot.id) ?? 0),
250
+ worker: at ?? 'local',
251
+ },
252
+ ) + (s.critical === slot.id ? pathNote(colors) : ''),
531
253
  )
532
254
  for (const b of nested.get(slot.id) ?? []) {
533
- rows++
534
255
  lines.push(
535
256
  formatTaskRow(
536
257
  ' ',
@@ -541,44 +262,39 @@ export function formatStatusRegion(
541
262
  '',
542
263
  `${dim('\u2514')} ${paintPinnedId(b.id, colors)}`,
543
264
  colors,
544
- '',
265
+ { usual: usualOf(b.id) },
545
266
  ),
546
267
  )
547
268
  }
548
269
  }
549
- if (!placed) for (; rows < (s.size ?? 0); rows++) lines.push('')
550
270
  for (const step of s.steps ?? []) {
551
271
  lines.push(
552
272
  formatTaskRow(
553
273
  ' ',
554
274
  Math.max(0, s.nowMs - step.startedMs),
555
275
  'running',
556
- ACCENT,
276
+ 'info',
557
277
  INTERNAL,
558
278
  '',
559
279
  paintPinnedId(step.id, colors),
560
280
  colors,
561
- '',
281
+ { worker: 'local' },
562
282
  ),
563
283
  )
564
284
  }
565
- if (lines.length > 1) lines.splice(1, 0, formatListHeader(placed, colors, true))
566
- // The live summary section — identical shape to the final printout,
567
- // so the region visually BECOMES the summary when the run ends.
568
- lines.push(...s.summaryLines)
569
- return lines
570
- }
571
-
572
- /**
573
- * A running task's usual duration, fixed while the elapsed time counts up
574
- * toward it (2026-10-14): `~5.2s`, `~42s`, `~2m 05s`. Fits the cache
575
- * column below 100 minutes.
576
- */
577
- function formatExpected(ms: number): string {
578
- // The tier by the rounded value: 999.6 ms printed `~1000ms` (item 1034's class).
579
- if (Math.round(ms) < 1000) return `~${Math.round(ms)}ms`
580
- if (ms < 9950) return `~${(ms / 1000).toFixed(1)}s`
581
- const s = Math.round(ms / 1000)
582
- if (s < 60) return `~${s}s`
583
- return `~${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
285
+ // Panels top to bottom: what runs now, then the live Summary, the same panel the final printout is, so the region
286
+ // BECOMES the summary when the run ends.
287
+ const out: string[] = []
288
+ if (lines.length > 0)
289
+ out.push(
290
+ '',
291
+ ...panel(
292
+ 'Running',
293
+ String(s.pinnedPersistent.length + running.length + (s.steps?.length ?? 0)),
294
+ [formatRunningHeader(colors), ...lines],
295
+ colors,
296
+ ),
297
+ )
298
+ out.push(...s.summaryLines)
299
+ return out
584
300
  }