@vzn/vx 0.0.650 → 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 (115) 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 +202 -25
  6. package/src/cache/file-hashes.ts +3 -0
  7. package/src/cache/git-inputs.ts +188 -39
  8. package/src/cache/index.ts +1 -0
  9. package/src/cache/inputs.ts +3 -0
  10. package/src/cache/layer.ts +11 -0
  11. package/src/cache/layered-cache.ts +4 -0
  12. package/src/cache/linked-inputs.ts +3 -0
  13. package/src/cache/lockfile-memo.ts +3 -2
  14. package/src/cache/output-index.ts +5 -2
  15. package/src/cache/remote-lane.ts +2 -1
  16. package/src/cache/run-history.ts +11 -1
  17. package/src/cache/schema.ts +11 -1
  18. package/src/cli/cache.ts +26 -16
  19. package/src/cli/completions.ts +4 -2
  20. package/src/cli/docs.ts +10 -11
  21. package/src/cli/help.ts +12 -23
  22. package/src/cli/index.ts +4 -9
  23. package/src/cli/info.ts +15 -7
  24. package/src/cli/init.ts +23 -15
  25. package/src/cli/last.ts +213 -203
  26. package/src/cli/lock.ts +55 -5
  27. package/src/cli/plan-format.ts +100 -118
  28. package/src/cli/plugin-commands.ts +8 -2
  29. package/src/cli/plugin-templates.ts +2 -2
  30. package/src/cli/run.ts +46 -27
  31. package/src/cli/select.ts +43 -5
  32. package/src/cli/show.ts +38 -42
  33. package/src/cli/task-verb.ts +22 -0
  34. package/src/cli/upgrade.ts +3 -1
  35. package/src/cli/watch-fs.ts +6 -1
  36. package/src/cli/watch-judge.ts +2 -1
  37. package/src/cli/watch.ts +83 -44
  38. package/src/cli/why.ts +123 -89
  39. package/src/cli/workspace-config.ts +2 -1
  40. package/src/exec/executor.ts +2 -1
  41. package/src/exec/kill-tree.ts +58 -5
  42. package/src/exec/local-executor.ts +1 -1
  43. package/src/exec/pipes.ts +152 -0
  44. package/src/exec/runner.ts +199 -182
  45. package/src/exec/sandbox-binds.ts +1 -1
  46. package/src/exec/sandbox-runtime.ts +11 -2
  47. package/src/exec/spawn-off.ts +154 -0
  48. package/src/exec/warm-shell.ts +146 -0
  49. package/src/graph/critical-path.ts +40 -28
  50. package/src/graph/priorities.ts +98 -44
  51. package/src/graph/scheduler.ts +14 -10
  52. package/src/graph/task-graph.ts +27 -1
  53. package/src/handoff.ts +10 -7
  54. package/src/history/history.ts +5 -0
  55. package/src/history/metrics.ts +9 -1
  56. package/src/index.ts +3 -1
  57. package/src/orchestrator/doctor.ts +1 -1
  58. package/src/orchestrator/execute-task.ts +16 -2
  59. package/src/orchestrator/graph-memo.ts +50 -1
  60. package/src/orchestrator/graph-stage.ts +6 -0
  61. package/src/orchestrator/index.ts +3 -0
  62. package/src/orchestrator/live-usage.ts +16 -11
  63. package/src/orchestrator/locked-digests.ts +63 -0
  64. package/src/orchestrator/lockfile-claim.ts +108 -29
  65. package/src/orchestrator/miss-save.ts +31 -3
  66. package/src/orchestrator/persistent-task.ts +3 -1
  67. package/src/orchestrator/plan.ts +5 -1
  68. package/src/orchestrator/plugin-host.ts +61 -0
  69. package/src/orchestrator/plugin.ts +47 -2
  70. package/src/orchestrator/prepare.ts +16 -1
  71. package/src/orchestrator/projects.ts +4 -1
  72. package/src/orchestrator/run-context.ts +29 -10
  73. package/src/orchestrator/run-failures.ts +1 -1
  74. package/src/orchestrator/run-records.ts +10 -0
  75. package/src/orchestrator/run.ts +53 -22
  76. package/src/orchestrator/sandbox-request.ts +2 -2
  77. package/src/orchestrator/signals.ts +8 -5
  78. package/src/orchestrator/task-hash.ts +17 -3
  79. package/src/report/events.ts +3 -11
  80. package/src/report/forecast.ts +10 -32
  81. package/src/report/framed-output.ts +272 -173
  82. package/src/report/index.ts +8 -3
  83. package/src/report/logger.ts +73 -38
  84. package/src/report/plain-output.ts +1 -1
  85. package/src/report/status-line.ts +49 -357
  86. package/src/report/summary.ts +100 -131
  87. package/src/ui/ansi.ts +48 -0
  88. package/src/ui/components.ts +506 -0
  89. package/src/ui/index.ts +79 -0
  90. package/src/ui/kit.ts +37 -0
  91. package/src/ui/live-region.ts +363 -0
  92. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  93. package/src/ui/terminal.ts +51 -0
  94. package/src/ui/theme.ts +224 -0
  95. package/src/ui/version-line.ts +28 -0
  96. package/src/util/config-frame.ts +65 -10
  97. package/src/util/edit-distance.ts +6 -1
  98. package/src/util/index.ts +2 -15
  99. package/src/util/paths.ts +20 -2
  100. package/src/util/secret-mask.ts +39 -3
  101. package/src/util/settle.ts +31 -0
  102. package/src/util/timing.ts +4 -16
  103. package/src/workspace/affected.ts +89 -30
  104. package/src/workspace/config-schema.ts +4 -2
  105. package/src/workspace/dependencies-lock.ts +191 -0
  106. package/src/workspace/filter.ts +28 -0
  107. package/src/workspace/index.ts +9 -0
  108. package/src/workspace/lockfile.ts +159 -6
  109. package/src/workspace/migration.ts +4 -3
  110. package/src/workspace/workspace.ts +3 -0
  111. package/src/util/colors.ts +0 -133
  112. package/src/util/report.ts +0 -44
  113. package/src/util/ui.ts +0 -258
  114. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  115. /package/src/{report → ui}/terminal-signals.ts +0 -0
@@ -1,334 +1,25 @@
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 {
6
+ formatExpected,
10
7
  formatRunningHeader,
11
8
  formatTaskRow,
12
9
  paintIdParts,
13
10
  pathNote,
14
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
330
- /** How long it waited ready for a slot; absent when it started at once. */
331
- waitedMs?: number
332
23
  /** Its cache cell: `no-cache` for a task that caches nothing, else `miss` (the default). */
333
24
  cache?: 'miss' | 'no-cache'
334
25
  }
@@ -344,8 +35,11 @@ export interface StatusRegionState {
344
35
  critical?: string
345
36
  /** Where a running task runs, when not here (`TaskExecutor.running`); its row shows it. */
346
37
  where?: (taskId: string) => string | null | undefined
347
- /** A running task's usual duration from history; its row shows it. */
348
- 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
349
43
  /** A running task's live CPU and memory; its row's `cpu` and `mem`. */
350
44
  usage?: (taskId: string) => LiveUsage | undefined
351
45
  /**
@@ -410,10 +104,10 @@ export function formatFailureLine(
410
104
  cells: RowCells = {},
411
105
  ): string {
412
106
  return formatTaskRow(
413
- paint(ERROR, '◼\uFE0E', colors),
107
+ paint('error', '\u25fc\uFE0E', colors),
414
108
  durationMs,
415
109
  'failed',
416
- ERROR,
110
+ 'error',
417
111
  cacheWord,
418
112
  '',
419
113
  paintPinnedId(id, colors),
@@ -437,10 +131,10 @@ export function formatStepLine(
437
131
  colors: ColorSupport = NO_COLOR,
438
132
  ): string {
439
133
  return formatTaskRow(
440
- ok ? paint(SUCCESS, '\u23fa\uFE0E', colors) : paint(ERROR, '\u25fc\uFE0E', colors),
134
+ ok ? paint('ok', '\u23fa\uFE0E', colors) : paint('error', '\u25fc\uFE0E', colors),
441
135
  durationMs,
442
136
  ok ? 'success' : 'failed',
443
- ok ? SUCCESS : ERROR,
137
+ ok ? 'ok' : 'error',
444
138
  INTERNAL,
445
139
  '',
446
140
  paintPinnedId(id, colors),
@@ -449,6 +143,11 @@ export function formatStepLine(
449
143
  )
450
144
  }
451
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
+
452
151
  /**
453
152
  * Worker region: pinned ready persistent tasks, one row per worker
454
153
  * slot — a task stays in its slot for its whole life so names never
@@ -461,25 +160,26 @@ export function formatStatusRegion(
461
160
  s: StatusRegionState,
462
161
  colors: ColorSupport = NO_COLOR,
463
162
  ): string[] {
464
- const dim = (t: string) => paint('', t, colors, { dim: true })
163
+ const dim = (t: string) => paint('muted', t, colors)
465
164
  // A task's usual duration, the live list's `usual` cell: `~18s`.
466
165
  const usualOf = (id: string): { text: string; painted: string } => {
467
- const ms = s.expected?.get(id)
468
- const text = ms === undefined ? '' : formatExpected(ms)
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)
469
170
  return { text, painted: dim(text) }
470
171
  }
471
- // Leading blank separates the live running region from the completed
472
- // task list scrolling above it (owner: "separate running from list").
473
- const lines: string[] = ['']
172
+ // The Running panel's rows; its box and header go round them below.
173
+ const lines: string[] = []
474
174
  // Persistent (dev-server) rows: ▸ glyph, no elapsed, `running`, no
475
175
  // cache state (it's still alive).
476
176
  for (const id of s.pinnedPersistent) {
477
177
  lines.push(
478
178
  formatTaskRow(
479
- paint(ACCENT, '▸', colors),
179
+ paint('info', '▸', colors),
480
180
  null,
481
181
  'running',
482
- ACCENT,
182
+ 'info',
483
183
  '',
484
184
  '',
485
185
  paintPinnedId(id, colors),
@@ -493,8 +193,9 @@ export function formatStatusRegion(
493
193
  // the ticking time IS the motion), then `running`, the usual duration,
494
194
  // how many tasks wait on it, and the full id (never truncated — name is
495
195
  // the last column). The rows no task runs in hold the tasks planned
496
- // next, each nested under what it waits on; the rest stay blank, so the
497
- // 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).
498
199
  const running = s.slots.filter((x): x is WorkerSlot => x !== null && x.waiting !== true)
499
200
  // Placed by an executor (DTE), the list is what runs: blocked tasks are
500
201
  // the tasks line's count alone, and its size is no machine's.
@@ -513,16 +214,14 @@ export function formatStatusRegion(
513
214
  n++
514
215
  }
515
216
  }
516
- let rows = 0
517
217
  for (const slot of running) {
518
- rows++
519
218
  if (s.stopping === true) {
520
219
  lines.push(
521
220
  formatTaskRow(
522
221
  ' ',
523
222
  Math.max(0, s.nowMs - slot.startedMs),
524
223
  'aborting',
525
- WARN,
224
+ 'warn',
526
225
  '',
527
226
  '',
528
227
  paintPinnedId(slot.id, colors),
@@ -539,14 +238,13 @@ export function formatStatusRegion(
539
238
  ' ',
540
239
  Math.max(0, s.nowMs - slot.startedMs),
541
240
  'running',
542
- ACCENT,
241
+ 'info',
543
242
  slot.cache ?? 'miss',
544
243
  '',
545
244
  paintPinnedId(slot.id, colors),
546
245
  colors,
547
246
  {
548
247
  usual: usualOf(slot.id),
549
- ...(slot.waitedMs !== undefined ? { waitedMs: slot.waitedMs } : {}),
550
248
  ...(use !== undefined ? { cpu: use.cores, memBytes: use.rssBytes } : {}),
551
249
  blocks: String(s.blockedBy?.get(slot.id) ?? 0),
552
250
  worker: at ?? 'local',
@@ -554,7 +252,6 @@ export function formatStatusRegion(
554
252
  ) + (s.critical === slot.id ? pathNote(colors) : ''),
555
253
  )
556
254
  for (const b of nested.get(slot.id) ?? []) {
557
- rows++
558
255
  lines.push(
559
256
  formatTaskRow(
560
257
  ' ',
@@ -570,14 +267,13 @@ export function formatStatusRegion(
570
267
  )
571
268
  }
572
269
  }
573
- if (!placed) for (; rows < (s.size ?? 0); rows++) lines.push('')
574
270
  for (const step of s.steps ?? []) {
575
271
  lines.push(
576
272
  formatTaskRow(
577
273
  ' ',
578
274
  Math.max(0, s.nowMs - step.startedMs),
579
275
  'running',
580
- ACCENT,
276
+ 'info',
581
277
  INTERNAL,
582
278
  '',
583
279
  paintPinnedId(step.id, colors),
@@ -586,23 +282,19 @@ export function formatStatusRegion(
586
282
  ),
587
283
  )
588
284
  }
589
- if (lines.length > 1) lines.splice(1, 0, formatRunningHeader(colors))
590
- // The live summary section — identical shape to the final printout,
591
- // so the region visually BECOMES the summary when the run ends.
592
- lines.push(...s.summaryLines)
593
- return lines
594
- }
595
-
596
- /**
597
- * A running task's usual duration, fixed while the elapsed time counts up
598
- * toward it (2026-10-14): `~5.2s`, `~42s`, `~2m 05s`. Fits the cache
599
- * column below 100 minutes.
600
- */
601
- function formatExpected(ms: number): string {
602
- // The tier by the rounded value: 999.6 ms printed `~1000ms` (item 1034's class).
603
- if (Math.round(ms) < 1000) return `~${Math.round(ms)}ms`
604
- if (ms < 9950) return `~${(ms / 1000).toFixed(1)}s`
605
- const s = Math.round(ms / 1000)
606
- if (s < 60) return `~${s}s`
607
- 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
608
300
  }