effective-progress 0.5.3 → 0.6.0

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/dist/index.mjs CHANGED
@@ -1,9 +1,454 @@
1
- import { Brand, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Ref, Schema } from "effect";
1
+ import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Schema } from "effect";
2
2
  import { dual } from "effect/Function";
3
- import { Writable } from "node:stream";
4
3
  import { Box, Text, render } from "ink";
5
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { useEffect, useState, useSyncExternalStore } from "react";
5
+ import stringWidth from "string-width";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
 
8
+ //#region src/types.ts
9
+ const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
10
+ const TaskId = Brand.nominal();
11
+ const TaskStatusSchema = Schema.Literal("running", "done", "failed");
12
+ const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
13
+ var DeterminateTaskUnits = class extends Schema.TaggedClass()("DeterminateTaskUnits", {
14
+ succeeded: Schema.Number,
15
+ failed: Schema.Number,
16
+ processed: Schema.Number,
17
+ total: Schema.Number
18
+ }) {};
19
+ var IndeterminateTaskUnits = class extends Schema.TaggedClass()("IndeterminateTaskUnits", {}) {};
20
+ const TaskUnitsSchema = Schema.Union(DeterminateTaskUnits, IndeterminateTaskUnits);
21
+ var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
22
+ id: TaskIdSchema,
23
+ parentId: Schema.NullOr(TaskIdSchema),
24
+ description: Schema.String,
25
+ status: TaskStatusSchema,
26
+ countDisplay: TaskCountDisplaySchema,
27
+ transient: Schema.Boolean,
28
+ units: TaskUnitsSchema,
29
+ startedAt: Schema.Number,
30
+ completedAt: Schema.NullOr(Schema.Number)
31
+ }) {};
32
+ var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
33
+ var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
34
+ taskId: TaskIdSchema,
35
+ parentId: Schema.NullOr(TaskIdSchema),
36
+ description: Schema.String,
37
+ total: Schema.optional(Schema.Number),
38
+ transient: Schema.Boolean,
39
+ countDisplay: TaskCountDisplaySchema
40
+ }) {};
41
+ var TaskUpdatedEvent = class extends Schema.TaggedClass()("TaskUpdated", {
42
+ taskId: TaskIdSchema,
43
+ description: Schema.optional(Schema.String),
44
+ succeeded: Schema.optional(Schema.Number),
45
+ failed: Schema.optional(Schema.Number),
46
+ processed: Schema.optional(Schema.Number),
47
+ total: Schema.optional(Schema.Number),
48
+ transient: Schema.optional(Schema.Boolean),
49
+ countDisplay: Schema.optional(TaskCountDisplaySchema)
50
+ }) {};
51
+ var TaskAdvancedEvent = class extends Schema.TaggedClass()("TaskAdvanced", {
52
+ taskId: TaskIdSchema,
53
+ amount: Schema.Number,
54
+ kind: Schema.Literal("succeeded", "failed")
55
+ }) {};
56
+ var TaskCompletedEvent = class extends Schema.TaggedClass()("TaskCompleted", { taskId: TaskIdSchema }) {};
57
+ var TaskFailedEvent = class extends Schema.TaggedClass()("TaskFailed", { taskId: TaskIdSchema }) {};
58
+ var TaskRemovedEvent = class extends Schema.TaggedClass()("TaskRemoved", { taskId: TaskIdSchema }) {};
59
+ const ProgressTaskEventSchema = Schema.Union(TaskAddedEvent, TaskUpdatedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskFailedEvent, TaskRemovedEvent);
60
+ const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
61
+
62
+ //#endregion
63
+ //#region src/ink-renderer/snapshot/render-snapshot.ts
64
+ const orderedVisibleTasks = (store) => store.renderOrder.flatMap((row) => {
65
+ const snapshot = store.tasks.get(row.id);
66
+ if (!snapshot || snapshot.transient && snapshot.status !== "running") return [];
67
+ return [{
68
+ snapshot,
69
+ depth: row.depth
70
+ }];
71
+ });
72
+ const computeTreeInfo = (ordered) => {
73
+ const hasNextSiblingByIndex = Array.from({ length: ordered.length }, () => false);
74
+ for (let i = 0; i < ordered.length; i++) {
75
+ const depth = ordered[i].depth;
76
+ for (let j = i + 1; j < ordered.length; j++) {
77
+ const candidateDepth = ordered[j].depth;
78
+ if (candidateDepth < depth) break;
79
+ if (candidateDepth === depth) {
80
+ hasNextSiblingByIndex[i] = true;
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ const ancestorStateByDepth = [];
86
+ return ordered.map((entry, index) => {
87
+ const depth = entry.depth;
88
+ ancestorStateByDepth.length = depth;
89
+ const hasChildren = index + 1 < ordered.length && ordered[index + 1] !== void 0 && ordered[index + 1].depth > depth;
90
+ const tree = {
91
+ depth,
92
+ hasNextSibling: hasNextSiblingByIndex[index] ?? false,
93
+ hasChildren,
94
+ ancestorHasNextSibling: [...ancestorStateByDepth]
95
+ };
96
+ ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
97
+ return {
98
+ ...entry,
99
+ tree
100
+ };
101
+ });
102
+ };
103
+ const toRenderSnapshot = (store) => {
104
+ const visibleTasks = orderedVisibleTasks(store);
105
+ const hasRunningTasks = visibleTasks.some((entry) => entry.snapshot.status === "running");
106
+ return {
107
+ rows: computeTreeInfo(visibleTasks).map((entry) => ({
108
+ task: entry.snapshot,
109
+ tree: entry.tree
110
+ })),
111
+ hasRunningTasks
112
+ };
113
+ };
114
+
115
+ //#endregion
116
+ //#region src/ink-renderer/store.ts
117
+ const normalizeDeterminateCounts = (counts) => {
118
+ const total = Math.max(0, counts.total);
119
+ const failed = Math.min(total, Math.max(0, counts.failed));
120
+ const succeeded = Math.min(total - failed, Math.max(0, counts.succeeded));
121
+ return new DeterminateTaskUnits({
122
+ succeeded,
123
+ failed,
124
+ processed: succeeded + failed,
125
+ total
126
+ });
127
+ };
128
+ const updateDeterminateCounts = (units, options) => normalizeDeterminateCounts({
129
+ succeeded: options.succeeded ?? units.succeeded,
130
+ failed: options.failed ?? units.failed,
131
+ total: options.total ?? units.total
132
+ });
133
+ const updatedSnapshot = (snapshot, options) => {
134
+ const currentUnits = snapshot.units;
135
+ const units = (() => {
136
+ if (options.total !== void 0) {
137
+ if (options.total <= 0) return new IndeterminateTaskUnits({});
138
+ if (currentUnits._tag === "DeterminateTaskUnits") return updateDeterminateCounts(currentUnits, options);
139
+ return normalizeDeterminateCounts({
140
+ succeeded: options.succeeded ?? 0,
141
+ failed: options.failed ?? 0,
142
+ total: options.total
143
+ });
144
+ }
145
+ if (currentUnits._tag === "DeterminateTaskUnits") {
146
+ if (options.succeeded === void 0 && options.failed === void 0) return currentUnits;
147
+ return updateDeterminateCounts(currentUnits, options);
148
+ }
149
+ return currentUnits;
150
+ })();
151
+ return new TaskSnapshot({
152
+ id: snapshot.id,
153
+ parentId: snapshot.parentId,
154
+ description: options.description ?? snapshot.description,
155
+ status: snapshot.status,
156
+ countDisplay: options.countDisplay ?? snapshot.countDisplay,
157
+ transient: options.transient ?? snapshot.transient,
158
+ units,
159
+ startedAt: snapshot.startedAt,
160
+ completedAt: snapshot.completedAt
161
+ });
162
+ };
163
+ const withTransient = (snapshot, transient) => new TaskSnapshot({
164
+ id: snapshot.id,
165
+ parentId: snapshot.parentId,
166
+ description: snapshot.description,
167
+ status: snapshot.status,
168
+ countDisplay: snapshot.countDisplay,
169
+ transient,
170
+ units: snapshot.units,
171
+ startedAt: snapshot.startedAt,
172
+ completedAt: snapshot.completedAt
173
+ });
174
+ const findInsertionIndex = (renderOrder, parentId) => {
175
+ if (parentId === null) return {
176
+ index: renderOrder.length,
177
+ depth: 0
178
+ };
179
+ const parentIdx = renderOrder.findIndex((row) => row.id === parentId);
180
+ if (parentIdx === -1) return {
181
+ index: renderOrder.length,
182
+ depth: 0
183
+ };
184
+ const parentDepth = renderOrder[parentIdx].depth;
185
+ let i = parentIdx + 1;
186
+ while (i < renderOrder.length && renderOrder[i].depth > parentDepth) i++;
187
+ return {
188
+ index: i,
189
+ depth: parentDepth + 1
190
+ };
191
+ };
192
+ const removeFromRenderOrder = (renderOrder, taskId) => {
193
+ const idx = renderOrder.findIndex((row) => row.id === taskId);
194
+ if (idx === -1) return renderOrder;
195
+ const taskDepth = renderOrder[idx].depth;
196
+ let end = idx + 1;
197
+ while (end < renderOrder.length && renderOrder[end].depth > taskDepth) end++;
198
+ const next = [...renderOrder];
199
+ next.splice(idx, end - idx);
200
+ return next;
201
+ };
202
+ const SNAPSHOT_PUBLISH_INTERVAL_MILLIS = 50;
203
+ const makeProgressRenderStore = () => {
204
+ let nextTaskId = 0;
205
+ let state = {
206
+ tasks: /* @__PURE__ */ new Map(),
207
+ renderOrder: []
208
+ };
209
+ let publishedSnapshot = toRenderSnapshot(state);
210
+ let hasPendingPublish = false;
211
+ let lastPublishAt = 0;
212
+ let publishTimeout;
213
+ const listeners = /* @__PURE__ */ new Set();
214
+ const notifyListeners = () => {
215
+ for (const listener of listeners) listener();
216
+ };
217
+ const publishNow = () => {
218
+ hasPendingPublish = false;
219
+ lastPublishAt = Date.now();
220
+ publishedSnapshot = toRenderSnapshot(state);
221
+ notifyListeners();
222
+ };
223
+ const clearScheduledPublish = () => {
224
+ if (publishTimeout === void 0) return;
225
+ clearTimeout(publishTimeout);
226
+ publishTimeout = void 0;
227
+ };
228
+ const schedulePublish = () => {
229
+ if (!hasPendingPublish) return;
230
+ const now = Date.now();
231
+ const waitMillis = Math.max(0, SNAPSHOT_PUBLISH_INTERVAL_MILLIS - (now - lastPublishAt));
232
+ if (waitMillis === 0) {
233
+ clearScheduledPublish();
234
+ publishNow();
235
+ return;
236
+ }
237
+ if (publishTimeout !== void 0) return;
238
+ publishTimeout = setTimeout(() => {
239
+ publishTimeout = void 0;
240
+ if (!hasPendingPublish) return;
241
+ publishNow();
242
+ }, waitMillis);
243
+ };
244
+ const publish = (nextState) => {
245
+ if (nextState === state) return;
246
+ state = nextState;
247
+ hasPendingPublish = true;
248
+ schedulePublish();
249
+ };
250
+ const updateState = (transform) => {
251
+ publish(transform(state));
252
+ };
253
+ return {
254
+ getSnapshot: () => publishedSnapshot,
255
+ subscribe: (listener) => {
256
+ listeners.add(listener);
257
+ return () => {
258
+ listeners.delete(listener);
259
+ };
260
+ },
261
+ flush: () => {
262
+ if (!hasPendingPublish) return;
263
+ clearScheduledPublish();
264
+ publishNow();
265
+ },
266
+ addTask: (options) => Effect.gen(function* () {
267
+ const taskId = TaskId(++nextTaskId);
268
+ const units = options.total === void 0 || options.total <= 0 ? new IndeterminateTaskUnits({}) : normalizeDeterminateCounts({
269
+ succeeded: 0,
270
+ failed: 0,
271
+ total: options.total
272
+ });
273
+ const parentSnapshot = options.parentId === void 0 ? void 0 : state.tasks.get(options.parentId);
274
+ const now = yield* Clock.currentTimeMillis;
275
+ const parentId = options.parentId ?? null;
276
+ const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
277
+ const task = new TaskSnapshot({
278
+ id: taskId,
279
+ parentId,
280
+ description: options.description,
281
+ status: "running",
282
+ countDisplay,
283
+ transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
284
+ units,
285
+ startedAt: now,
286
+ completedAt: null
287
+ });
288
+ updateState((current) => {
289
+ const nextTasks = new Map(current.tasks);
290
+ nextTasks.set(taskId, task);
291
+ const { index, depth } = findInsertionIndex(current.renderOrder, parentId);
292
+ const nextRenderOrder = [...current.renderOrder];
293
+ nextRenderOrder.splice(index, 0, {
294
+ id: taskId,
295
+ depth
296
+ });
297
+ return {
298
+ tasks: nextTasks,
299
+ renderOrder: nextRenderOrder
300
+ };
301
+ });
302
+ return taskId;
303
+ }),
304
+ updateTask: (taskId, options) => Effect.sync(() => {
305
+ updateState((current) => {
306
+ const currentTask = current.tasks.get(taskId);
307
+ if (!currentTask) return current;
308
+ const nextTasks = new Map(current.tasks);
309
+ const nextTask = updatedSnapshot(currentTask, options);
310
+ nextTasks.set(taskId, nextTask);
311
+ if (options.transient !== void 0) for (const [candidateId, candidate] of current.tasks.entries()) {
312
+ if (candidateId === taskId) continue;
313
+ let parentId = candidate.parentId;
314
+ let isDescendant = false;
315
+ while (parentId !== null) {
316
+ if (parentId === taskId) {
317
+ isDescendant = true;
318
+ break;
319
+ }
320
+ parentId = current.tasks.get(parentId)?.parentId ?? null;
321
+ }
322
+ if (isDescendant) nextTasks.set(candidateId, withTransient(candidate, nextTask.transient));
323
+ }
324
+ return {
325
+ tasks: nextTasks,
326
+ renderOrder: current.renderOrder
327
+ };
328
+ });
329
+ }),
330
+ addSuccess: (taskId, amount = 1) => Effect.sync(() => {
331
+ updateState((current) => {
332
+ const currentTask = current.tasks.get(taskId);
333
+ if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
334
+ const nextTasks = new Map(current.tasks);
335
+ nextTasks.set(taskId, new TaskSnapshot({
336
+ id: currentTask.id,
337
+ parentId: currentTask.parentId,
338
+ description: currentTask.description,
339
+ status: currentTask.status,
340
+ countDisplay: currentTask.countDisplay,
341
+ transient: currentTask.transient,
342
+ units: normalizeDeterminateCounts({
343
+ succeeded: currentTask.units.succeeded + amount,
344
+ failed: currentTask.units.failed,
345
+ total: currentTask.units.total
346
+ }),
347
+ startedAt: currentTask.startedAt,
348
+ completedAt: currentTask.completedAt
349
+ }));
350
+ return {
351
+ tasks: nextTasks,
352
+ renderOrder: current.renderOrder
353
+ };
354
+ });
355
+ }),
356
+ addFailure: (taskId, amount = 1) => Effect.sync(() => {
357
+ updateState((current) => {
358
+ const currentTask = current.tasks.get(taskId);
359
+ if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
360
+ const nextTasks = new Map(current.tasks);
361
+ nextTasks.set(taskId, new TaskSnapshot({
362
+ id: currentTask.id,
363
+ parentId: currentTask.parentId,
364
+ description: currentTask.description,
365
+ status: currentTask.status,
366
+ countDisplay: currentTask.countDisplay,
367
+ transient: currentTask.transient,
368
+ units: normalizeDeterminateCounts({
369
+ succeeded: currentTask.units.succeeded,
370
+ failed: currentTask.units.failed + amount,
371
+ total: currentTask.units.total
372
+ }),
373
+ startedAt: currentTask.startedAt,
374
+ completedAt: currentTask.completedAt
375
+ }));
376
+ return {
377
+ tasks: nextTasks,
378
+ renderOrder: current.renderOrder
379
+ };
380
+ });
381
+ }),
382
+ completeTask: (taskId) => Effect.gen(function* () {
383
+ const now = yield* Clock.currentTimeMillis;
384
+ updateState((current) => {
385
+ const currentTask = current.tasks.get(taskId);
386
+ if (!currentTask) return current;
387
+ const nextTasks = new Map(current.tasks);
388
+ if (currentTask.transient) {
389
+ nextTasks.delete(taskId);
390
+ return {
391
+ tasks: nextTasks,
392
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
393
+ };
394
+ }
395
+ nextTasks.set(taskId, new TaskSnapshot({
396
+ id: currentTask.id,
397
+ parentId: currentTask.parentId,
398
+ description: currentTask.description,
399
+ status: "done",
400
+ countDisplay: currentTask.countDisplay,
401
+ transient: currentTask.transient,
402
+ units: currentTask.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
403
+ succeeded: currentTask.units.total - currentTask.units.failed,
404
+ failed: currentTask.units.failed,
405
+ total: currentTask.units.total
406
+ }) : currentTask.units,
407
+ startedAt: currentTask.startedAt,
408
+ completedAt: now
409
+ }));
410
+ return {
411
+ tasks: nextTasks,
412
+ renderOrder: current.renderOrder
413
+ };
414
+ });
415
+ }),
416
+ failTask: (taskId) => Effect.gen(function* () {
417
+ const now = yield* Clock.currentTimeMillis;
418
+ updateState((current) => {
419
+ const currentTask = current.tasks.get(taskId);
420
+ if (!currentTask) return current;
421
+ const nextTasks = new Map(current.tasks);
422
+ if (currentTask.transient) {
423
+ nextTasks.delete(taskId);
424
+ return {
425
+ tasks: nextTasks,
426
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
427
+ };
428
+ }
429
+ nextTasks.set(taskId, new TaskSnapshot({
430
+ id: currentTask.id,
431
+ parentId: currentTask.parentId,
432
+ description: currentTask.description,
433
+ status: "failed",
434
+ countDisplay: currentTask.countDisplay,
435
+ transient: currentTask.transient,
436
+ units: currentTask.units,
437
+ startedAt: currentTask.startedAt,
438
+ completedAt: now
439
+ }));
440
+ return {
441
+ tasks: nextTasks,
442
+ renderOrder: current.renderOrder
443
+ };
444
+ });
445
+ }),
446
+ getTask: (taskId) => Effect.sync(() => Option.fromNullable(state.tasks.get(taskId))),
447
+ listTasks: Effect.sync(() => Array.from(state.tasks.values()))
448
+ };
449
+ };
450
+
451
+ //#endregion
7
452
  //#region src/ink-renderer/format.ts
8
453
  const SPINNER_FRAMES = [
9
454
  "⠋",
@@ -34,669 +479,999 @@ const formatElapsed = (task, now) => {
34
479
  };
35
480
  const formatEta = (task, now) => {
36
481
  if (task.status !== "running" || task.units._tag !== "DeterminateTaskUnits") return "";
37
- const { completed, total } = task.units;
38
- const remaining = total - completed;
39
- if (completed <= 0 || remaining <= 0) return "";
482
+ const { processed, total } = task.units;
483
+ const remaining = total - processed;
484
+ if (processed <= 0 || remaining <= 0) return "";
40
485
  const elapsedMillis = Math.max(1, now - task.startedAt);
41
- return formatDurationSeconds(Math.max(0, Math.floor(elapsedMillis / completed * remaining)) / 1e3);
486
+ return formatDurationSeconds(Math.max(0, Math.floor(elapsedMillis / processed * remaining)) / 1e3);
42
487
  };
43
- const formatAmount = (task, tick) => {
488
+ const getTaskIndicator = (task, tick) => {
489
+ if (task.status === "running") return {
490
+ symbol: SPINNER_FRAMES[tick % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0],
491
+ color: "yellow"
492
+ };
493
+ if (task.status === "failed") return {
494
+ symbol: "✗",
495
+ color: "red"
496
+ };
497
+ if (task.units._tag !== "DeterminateTaskUnits") return {
498
+ symbol: "✓",
499
+ color: "green"
500
+ };
501
+ const { succeeded, failed, processed, total } = task.units;
502
+ if (failed === 0 && processed === total) return {
503
+ symbol: "✓",
504
+ color: "green"
505
+ };
506
+ if (failed > 0 && succeeded > 0) return {
507
+ symbol: "~",
508
+ color: "yellow"
509
+ };
510
+ if (failed > 0 && succeeded === 0) return {
511
+ symbol: "✗",
512
+ color: "red"
513
+ };
514
+ return {
515
+ symbol: "✓",
516
+ color: "green"
517
+ };
518
+ };
519
+ const formatDeterminateAmountParts = (task) => {
520
+ if (task.units._tag !== "DeterminateTaskUnits") return;
521
+ const totalText = `${task.units.total}`;
522
+ const width = totalText.length;
523
+ const processedText = `${task.units.processed}`;
524
+ return {
525
+ succeeded: task.countDisplay === "detailed" ? `${task.units.succeeded}`.padStart(width, " ") : "",
526
+ failed: task.countDisplay === "detailed" ? `${task.units.failed}`.padStart(width, " ") : "",
527
+ processed: processedText,
528
+ total: totalText
529
+ };
530
+ };
531
+ const formatAmount = (task, _tick) => {
44
532
  if (task.units._tag === "DeterminateTaskUnits") {
45
- const totalText = `${task.units.total}`;
46
- return `${`${task.units.completed}`.padStart(totalText.length, " ")}/${totalText}`;
533
+ const parts = formatDeterminateAmountParts(task);
534
+ if (parts === void 0) return "";
535
+ if (task.countDisplay === "detailed") return `${parts.succeeded} ${parts.failed} ${parts.processed}/${parts.total}`;
536
+ return `${parts.processed}/${parts.total}`;
47
537
  }
48
- if (task.status === "running") return SPINNER_FRAMES[(task.units.spinnerFrame + tick) % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0];
49
- return task.status === "done" ? "" : "";
538
+ if (task.status === "running" && task.units._tag === "IndeterminateTaskUnits") return "";
539
+ return task.status === "failed" ? "" : "";
50
540
  };
51
541
 
52
542
  //#endregion
53
- //#region src/ink-renderer/tree.ts
54
- const treeAncestorPrefix = (tree) => tree.ancestorHasNextSibling.slice(1).map((hasNext) => hasNext ? "" : " ").join("");
55
- const renderTreePrefix = (tree) => {
56
- if (tree.depth <= 0) return "";
57
- return `${treeAncestorPrefix(tree)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
543
+ //#region src/ink-renderer/columns/determinate.ts
544
+ const isDeterminate = (task) => task.units._tag === "DeterminateTaskUnits";
545
+ const hasDeterminateRows = (rows) => rows.some((row) => isDeterminate(row.task));
546
+
547
+ //#endregion
548
+ //#region src/ink-renderer/columns/spec.ts
549
+ const WIDTH_CACHE_LIMIT = 4096;
550
+ const widthCache = /* @__PURE__ */ new Map();
551
+ const textWidth = (text) => {
552
+ const cached = widthCache.get(text);
553
+ if (cached !== void 0) return cached;
554
+ const width = stringWidth(text);
555
+ if (widthCache.size >= WIDTH_CACHE_LIMIT) widthCache.clear();
556
+ widthCache.set(text, width);
557
+ return width;
58
558
  };
59
- const computeTreeInfo = (ordered) => {
60
- const hasNextSiblingByIndex = Array.from({ length: ordered.length }, () => false);
61
- for (let i = 0; i < ordered.length; i++) {
62
- const depth = ordered[i].depth;
63
- for (let j = i + 1; j < ordered.length; j++) {
64
- const candidateDepth = ordered[j].depth;
65
- if (candidateDepth < depth) break;
66
- if (candidateDepth === depth) {
67
- hasNextSiblingByIndex[i] = true;
68
- break;
69
- }
70
- }
559
+ const resolveColumnSpec = (spec, resistance) => ({
560
+ id: spec.id,
561
+ grow: spec.grow,
562
+ canHide: spec.canHide,
563
+ variants: spec.variants.map((variant) => ({
564
+ id: variant.id,
565
+ minWidth: variant.minWidth,
566
+ idealWidth: variant.idealWidth,
567
+ maxWidth: variant.maxWidth,
568
+ shrinkResistance: resistance.shrink(spec.id, variant.id),
569
+ demoteResistance: resistance.demote(spec.id, variant.id),
570
+ hideResistance: resistance.hide(spec.id, variant.id),
571
+ renderCell: variant.renderCell
572
+ }))
573
+ });
574
+ const resolveColumnSpecs = (specs, resistance) => specs.flatMap((spec) => spec.variants.length > 0 ? [resolveColumnSpec(spec, resistance)] : []);
575
+
576
+ //#endregion
577
+ //#region src/ink-renderer/columns/amount-column.tsx
578
+ const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
579
+ const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
580
+ const blank = (width) => " ".repeat(Math.max(0, width));
581
+ const shouldShowDetailedCounts = (task) => task.units._tag === "DeterminateTaskUnits" && task.countDisplay === "detailed";
582
+ const SucceededCountColumn = ({ task, width }) => {
583
+ if (width <= 0) return null;
584
+ if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
585
+ if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
586
+ return /* @__PURE__ */ jsx(Text, {
587
+ color: "green",
588
+ children: padLeft(`${task.units.succeeded}`, width)
589
+ });
590
+ };
591
+ const FailedCountColumn = ({ task, width }) => {
592
+ if (width <= 0) return null;
593
+ if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
594
+ if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
595
+ return /* @__PURE__ */ jsx(Text, {
596
+ color: "red",
597
+ children: padLeft(`${task.units.failed}`, width)
598
+ });
599
+ };
600
+ const ProcessedCountColumn = ({ task, width }) => {
601
+ if (width <= 0) return null;
602
+ if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
603
+ return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
604
+ };
605
+ const AmountSeparatorColumn = ({ task, tick }) => {
606
+ if (task.units._tag === "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: "/" });
607
+ const symbol = formatAmount(task, tick);
608
+ if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
609
+ color: "red",
610
+ children: symbol
611
+ });
612
+ if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
613
+ color: "yellow",
614
+ children: symbol
615
+ });
616
+ return /* @__PURE__ */ jsx(Text, { children: symbol });
617
+ };
618
+ const TotalCountColumn = ({ task, width }) => {
619
+ if (width <= 0) return null;
620
+ if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
621
+ return /* @__PURE__ */ jsx(Text, { children: padRight(`${task.units.total}`, width) });
622
+ };
623
+ const AmountColumn = ({ task, tick, layout }) => {
624
+ if (layout.kind === "text") {
625
+ const text = formatAmount(task, tick);
626
+ if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
627
+ wrap: "truncate-end",
628
+ color: "red",
629
+ children: text
630
+ });
631
+ if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
632
+ wrap: "truncate-end",
633
+ color: "yellow",
634
+ children: text
635
+ });
636
+ return /* @__PURE__ */ jsx(Text, {
637
+ wrap: "truncate-end",
638
+ children: text
639
+ });
71
640
  }
72
- const ancestorStateByDepth = [];
73
- return ordered.map((entry, index) => {
74
- const depth = entry.depth;
75
- ancestorStateByDepth.length = depth;
76
- const hasChildren = index + 1 < ordered.length && ordered[index + 1] !== void 0 && ordered[index + 1].depth > depth;
77
- const tree = {
78
- depth,
79
- hasNextSibling: hasNextSiblingByIndex[index] ?? false,
80
- hasChildren,
81
- ancestorHasNextSibling: [...ancestorStateByDepth]
82
- };
83
- ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
84
- return {
85
- ...entry,
86
- tree
87
- };
641
+ if (layout.kind === "processed") return /* @__PURE__ */ jsxs(Box, {
642
+ flexDirection: "row",
643
+ children: [
644
+ /* @__PURE__ */ jsx(ProcessedCountColumn, {
645
+ task,
646
+ width: layout.processedWidth
647
+ }),
648
+ /* @__PURE__ */ jsx(AmountSeparatorColumn, {
649
+ task,
650
+ tick
651
+ }),
652
+ /* @__PURE__ */ jsx(TotalCountColumn, {
653
+ task,
654
+ width: layout.totalWidth
655
+ })
656
+ ]
657
+ });
658
+ return /* @__PURE__ */ jsxs(Box, {
659
+ flexDirection: "row",
660
+ children: [
661
+ layout.succeededWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SucceededCountColumn, {
662
+ task,
663
+ width: layout.succeededWidth
664
+ }), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
665
+ layout.failedWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FailedCountColumn, {
666
+ task,
667
+ width: layout.failedWidth
668
+ }), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
669
+ /* @__PURE__ */ jsx(ProcessedCountColumn, {
670
+ task,
671
+ width: layout.processedWidth
672
+ }),
673
+ /* @__PURE__ */ jsx(AmountSeparatorColumn, {
674
+ task,
675
+ tick
676
+ }),
677
+ /* @__PURE__ */ jsx(TotalCountColumn, {
678
+ task,
679
+ width: layout.totalWidth
680
+ })
681
+ ]
88
682
  });
89
683
  };
90
-
91
- //#endregion
92
- //#region src/ink-renderer/layout.ts
93
- const DEFAULT_BAR_WIDTH = 20;
94
- const MIN_DESCRIPTION_WIDTH = 8;
95
- const MIN_BAR_WIDTH = 8;
96
- const MIN_ELAPSED_WIDTH = 3;
97
- const MIN_AMOUNT_WIDTH = 1;
98
- const BASELINE_ROW_WIDTH = 100;
99
- const MIN_DESCRIPTION_COLUMNS_FOR_TREE = 24;
100
- const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
101
- const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
102
- const textWidth = (text) => Array.from(text).length;
103
- const computeWidths = (rows, now, tick, terminalColumns, includeTree = true) => {
684
+ const computeAmountMetrics = (rows, tick) => {
104
685
  let hasDeterminate = false;
105
- let description = MIN_DESCRIPTION_WIDTH;
106
- let amount = 1;
107
- let elapsed = RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR;
108
- let eta = RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR;
686
+ let hasDetailed = false;
687
+ let totalDigits = 0;
688
+ let simpleTextWidth = 0;
109
689
  for (const row of rows) {
110
- const { task, tree } = row;
111
- const treePrefix = includeTree ? renderTreePrefix(tree) : "";
112
- description = Math.max(description, textWidth(`${treePrefix}${task.description}`));
113
- if (task.units._tag === "DeterminateTaskUnits") hasDeterminate = true;
114
- amount = Math.max(amount, textWidth(formatAmount(task, tick)));
115
- elapsed = Math.max(elapsed, textWidth(formatElapsed(task, now)));
116
- if (task.status === "running" && task.units._tag === "DeterminateTaskUnits") {
117
- const etaValue = formatEta(task, now);
118
- const etaText = `ETA: ${etaValue.length > 0 ? etaValue : "--"}`;
119
- eta = Math.max(eta, textWidth(etaText));
690
+ const { task } = row;
691
+ if (isDeterminate(task)) {
692
+ hasDeterminate = true;
693
+ totalDigits = Math.max(totalDigits, textWidth(`${task.units.total}`));
694
+ if (task.countDisplay === "detailed") hasDetailed = true;
695
+ continue;
120
696
  }
121
- }
122
- let widths = {
123
- description,
124
- bar: hasDeterminate ? DEFAULT_BAR_WIDTH : 0,
125
- amount,
126
- elapsed,
127
- eta
128
- };
129
- const visible = (w) => [
130
- w.description,
131
- w.bar,
132
- w.amount,
133
- w.elapsed,
134
- w.eta
135
- ].filter((width) => width > 0);
136
- const total = (w) => {
137
- const cols = visible(w);
138
- return cols.reduce((sum, width) => sum + width, 0) + Math.max(0, cols.length - 1);
139
- };
140
- const baselineTarget = Math.max(BASELINE_ROW_WIDTH, total(widths));
141
- const target = terminalColumns === void 0 ? baselineTarget : Math.max(1, Math.min(Math.max(1, Math.floor(terminalColumns)), baselineTarget));
142
- if (total(widths) < target) widths.description += target - total(widths);
143
- else if (total(widths) > target) {
144
- let overflow = total(widths) - target;
145
- const reduceBy = (key, min) => {
146
- if (overflow <= 0) return;
147
- const current = widths[key];
148
- if (current <= min) return;
149
- const reducible = current - min;
150
- const delta = Math.min(reducible, overflow);
151
- widths = {
152
- ...widths,
153
- [key]: current - delta
154
- };
155
- overflow -= delta;
156
- };
157
- reduceBy("description", MIN_DESCRIPTION_WIDTH);
158
- reduceBy("eta", 0);
159
- reduceBy("bar", MIN_BAR_WIDTH);
160
- reduceBy("bar", 0);
161
- reduceBy("elapsed", MIN_ELAPSED_WIDTH);
162
- reduceBy("amount", MIN_AMOUNT_WIDTH);
163
- reduceBy("description", 0);
164
- if (total(widths) < target) widths.description += target - total(widths);
697
+ simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, tick)));
165
698
  }
166
699
  return {
167
- row: total(widths),
168
- description: widths.description,
169
- bar: widths.bar,
170
- amount: widths.amount,
171
- elapsed: widths.elapsed,
172
- eta: widths.eta
700
+ hasDeterminate,
701
+ hasDetailed,
702
+ totalDigits: Math.max(1, totalDigits),
703
+ simpleTextWidth
173
704
  };
174
705
  };
175
- const computeSharedColumnWidths = (rows, now, tick, terminalColumns) => {
176
- const withTree = computeWidths(rows, now, tick, terminalColumns, true);
177
- if (withTree.description >= MIN_DESCRIPTION_COLUMNS_FOR_TREE) return {
178
- ...withTree,
179
- showTree: true
180
- };
706
+ const detailedAmountLayout = (metrics) => ({
707
+ kind: "detailed",
708
+ succeededWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
709
+ failedWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
710
+ processedWidth: metrics.totalDigits,
711
+ totalWidth: metrics.totalDigits
712
+ });
713
+ const processedAmountLayout = (metrics) => ({
714
+ kind: "processed",
715
+ processedWidth: metrics.totalDigits,
716
+ totalWidth: metrics.totalDigits
717
+ });
718
+ const detailedAmountWidth = (metrics) => metrics.totalDigits + 1 + metrics.totalDigits + (metrics.hasDetailed ? metrics.totalDigits + 1 + metrics.totalDigits + 1 : 0);
719
+ const processedAmountWidth = (metrics) => metrics.totalDigits + 1 + metrics.totalDigits;
720
+ const createAmountColumnSpec = (context) => {
721
+ const metrics = computeAmountMetrics(context.rows, context.tick);
722
+ if (!metrics.hasDeterminate && metrics.simpleTextWidth <= 0) return;
723
+ const detailedLayout = detailedAmountLayout(metrics);
724
+ const processedLayout = processedAmountLayout(metrics);
725
+ const detailedWidth = detailedAmountWidth(metrics);
726
+ const processedWidth = processedAmountWidth(metrics);
727
+ return {
728
+ id: "amount",
729
+ grow: 0,
730
+ canHide: true,
731
+ variants: metrics.hasDeterminate && metrics.hasDetailed ? [{
732
+ id: "detailed",
733
+ minWidth: detailedWidth,
734
+ idealWidth: detailedWidth,
735
+ renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
736
+ task: row.task,
737
+ tick: context.tick,
738
+ layout: detailedLayout
739
+ })
740
+ }, {
741
+ id: "processed",
742
+ minWidth: processedWidth,
743
+ idealWidth: processedWidth,
744
+ renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
745
+ task: row.task,
746
+ tick: context.tick,
747
+ layout: processedLayout
748
+ })
749
+ }] : metrics.hasDeterminate ? [{
750
+ id: "processed",
751
+ minWidth: processedWidth,
752
+ idealWidth: processedWidth,
753
+ renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
754
+ task: row.task,
755
+ tick: context.tick,
756
+ layout: processedLayout
757
+ })
758
+ }] : [{
759
+ id: "text",
760
+ minWidth: 0,
761
+ idealWidth: metrics.simpleTextWidth,
762
+ renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
763
+ task: row.task,
764
+ tick: context.tick,
765
+ layout: { kind: "text" }
766
+ })
767
+ }]
768
+ };
769
+ };
770
+
771
+ //#endregion
772
+ //#region src/ink-renderer/columns/bar-column.tsx
773
+ const DEFAULT_BAR_WIDTH = 30;
774
+ const MIN_BAR_WIDTH = 8;
775
+ const segmentLengths = (width, total, succeeded, failed) => {
776
+ if (total <= 0) return {
777
+ succeeded: 0,
778
+ failed: 0,
779
+ remaining: width
780
+ };
781
+ const succeededEnd = Math.round(succeeded / total * width);
782
+ const failedEnd = Math.round((succeeded + failed) / total * width);
783
+ const succeededLength = Math.max(0, Math.min(width, succeededEnd));
784
+ const failedLength = Math.max(0, Math.min(width, failedEnd) - succeededLength);
785
+ return {
786
+ succeeded: succeededLength,
787
+ failed: failedLength,
788
+ remaining: Math.max(0, width - succeededLength - failedLength)
789
+ };
790
+ };
791
+ const BarColumn = ({ task, width }) => {
792
+ if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, {});
793
+ const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
794
+ return /* @__PURE__ */ jsxs(Text, {
795
+ wrap: "truncate-end",
796
+ children: [
797
+ /* @__PURE__ */ jsx(Text, {
798
+ color: "green",
799
+ children: "━".repeat(lengths.succeeded)
800
+ }),
801
+ /* @__PURE__ */ jsx(Text, {
802
+ color: "red",
803
+ children: "━".repeat(lengths.failed)
804
+ }),
805
+ /* @__PURE__ */ jsx(Text, {
806
+ color: "gray",
807
+ children: "─".repeat(lengths.remaining)
808
+ })
809
+ ]
810
+ });
811
+ };
812
+ const createBarColumnSpec = (context, isTTY) => {
813
+ if (!hasDeterminateRows(context.rows)) return;
181
814
  return {
182
- ...computeWidths(rows, now, tick, terminalColumns, false),
183
- showTree: false
815
+ id: "bar",
816
+ grow: 0,
817
+ canHide: true,
818
+ variants: [{
819
+ id: "full",
820
+ minWidth: MIN_BAR_WIDTH,
821
+ idealWidth: DEFAULT_BAR_WIDTH,
822
+ maxWidth: DEFAULT_BAR_WIDTH,
823
+ renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
824
+ task: row.task,
825
+ tree: row.tree,
826
+ now: context.now,
827
+ tick: context.tick,
828
+ isTTY,
829
+ width: Math.max(1, Math.min(width, DEFAULT_BAR_WIDTH))
830
+ })
831
+ }, {
832
+ id: "compact",
833
+ minWidth: 1,
834
+ idealWidth: MIN_BAR_WIDTH,
835
+ maxWidth: MIN_BAR_WIDTH,
836
+ renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
837
+ task: row.task,
838
+ tree: row.tree,
839
+ now: context.now,
840
+ tick: context.tick,
841
+ isTTY,
842
+ width: Math.max(1, width)
843
+ })
844
+ }]
184
845
  };
185
846
  };
186
847
 
187
848
  //#endregion
188
- //#region src/ink-renderer/columns/amount-column.tsx
189
- const AmountColumn = ({ task, tick }) => {
190
- const text = formatAmount(task, tick);
191
- return /* @__PURE__ */ jsx(Text, {
192
- color: task.status === "failed" ? "red" : task.status === "done" ? "green" : task.units._tag === "DeterminateTaskUnits" ? "whiteBright" : "yellow",
193
- children: text
194
- });
849
+ //#region src/ink-renderer/view/tree-prefix.ts
850
+ const treeAncestorPrefix = (tree) => tree.ancestorHasNextSibling.slice(1).map((hasNext) => hasNext ? "│ " : " ").join("");
851
+ const renderTreePrefix = (tree) => {
852
+ if (tree.depth <= 0) return "";
853
+ return `${treeAncestorPrefix(tree)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
195
854
  };
196
855
 
197
856
  //#endregion
198
- //#region src/ink-renderer/columns/bar-column.tsx
199
- const clamp = (value, minimum, maximum) => Math.min(Math.max(value, minimum), maximum);
200
- const BarColumn = ({ task, width }) => {
201
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, {});
202
- const barWidth = Math.max(1, Math.floor(width));
203
- const safeTotal = Math.max(1, task.units.total);
204
- const ratio = task.status === "done" ? 1 : clamp(task.units.completed / safeTotal, 0, 1);
205
- const filled = Math.round(barWidth * ratio);
206
- const empty = Math.max(0, barWidth - filled);
207
- const bar = `${"━".repeat(filled)}${"─".repeat(empty)}`;
208
- return /* @__PURE__ */ jsx(Text, {
857
+ //#region src/ink-renderer/columns/description-column.tsx
858
+ const DescriptionColumn = ({ task, tree, showTree, tick }) => {
859
+ const treePrefix = showTree ? renderTreePrefix(tree) : "";
860
+ const indicator = getTaskIndicator(task, tick);
861
+ return /* @__PURE__ */ jsxs(Text, {
209
862
  wrap: "truncate-end",
210
- color: task.status === "failed" ? "red" : task.status === "done" ? "green" : "blue",
211
- children: bar
863
+ children: [
864
+ treePrefix,
865
+ /* @__PURE__ */ jsx(Text, {
866
+ color: indicator.color,
867
+ children: indicator.symbol
868
+ }),
869
+ ` ${task.description}`
870
+ ]
212
871
  });
213
872
  };
214
-
215
- //#endregion
216
- //#region src/ink-renderer/columns/description-column.tsx
217
- const DescriptionColumn = ({ task, tree, showTree }) => /* @__PURE__ */ jsx(Text, {
218
- wrap: "truncate-end",
219
- children: `${showTree ? renderTreePrefix(tree) : ""}${task.description}`
220
- });
873
+ const MIN_DESCRIPTION_WIDTH = 8;
874
+ const MIN_DESCRIPTION_WITH_TREE_WIDTH = 20;
875
+ const DESCRIPTION_PADDING_GROWTH_LIMIT = 20;
876
+ const maxDescriptionWidth = (rows, showTree) => rows.reduce((max, row) => {
877
+ const treePrefix = showTree ? renderTreePrefix(row.tree) : "";
878
+ return Math.max(max, textWidth(`${treePrefix}${row.task.description}`) + 2);
879
+ }, MIN_DESCRIPTION_WIDTH);
880
+ const createDescriptionColumnSpec = (context, isTTY) => {
881
+ const treeIdeal = maxDescriptionWidth(context.rows, true);
882
+ const plainIdeal = maxDescriptionWidth(context.rows, false);
883
+ const treeVariantIdeal = Math.max(MIN_DESCRIPTION_WITH_TREE_WIDTH, treeIdeal);
884
+ const plainVariantIdeal = Math.max(MIN_DESCRIPTION_WIDTH, plainIdeal);
885
+ return {
886
+ id: "description",
887
+ grow: 1,
888
+ canHide: false,
889
+ variants: [{
890
+ id: "tree",
891
+ minWidth: MIN_DESCRIPTION_WITH_TREE_WIDTH,
892
+ idealWidth: treeVariantIdeal,
893
+ maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, treeVariantIdeal),
894
+ renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
895
+ task: row.task,
896
+ tree: row.tree,
897
+ now: context.now,
898
+ tick: context.tick,
899
+ isTTY,
900
+ showTree: true
901
+ })
902
+ }, {
903
+ id: "plain",
904
+ minWidth: MIN_DESCRIPTION_WIDTH,
905
+ idealWidth: plainVariantIdeal,
906
+ maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, plainVariantIdeal),
907
+ renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
908
+ task: row.task,
909
+ tree: row.tree,
910
+ now: context.now,
911
+ tick: context.tick,
912
+ isTTY,
913
+ showTree: false
914
+ })
915
+ }]
916
+ };
917
+ };
221
918
 
222
919
  //#endregion
223
920
  //#region src/ink-renderer/columns/elapsed-column.tsx
224
921
  const ElapsedColumn = ({ task, now }) => /* @__PURE__ */ jsx(Text, {
922
+ wrap: "truncate-end",
225
923
  color: "gray",
226
924
  children: formatElapsed(task, now)
227
925
  });
926
+ const MIN_ELAPSED_WIDTH = 2;
927
+ const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
928
+ const maxElapsedWidth = (rows, now) => rows.reduce((max, row) => Math.max(max, textWidth(formatElapsed(row.task, now))), MIN_ELAPSED_WIDTH);
929
+ const createElapsedColumnSpec = (context, isTTY) => {
930
+ const elapsedContentWidth = maxElapsedWidth(context.rows, context.now);
931
+ return {
932
+ id: "elapsed",
933
+ grow: 0,
934
+ canHide: false,
935
+ variants: [{
936
+ id: "stable",
937
+ minWidth: elapsedContentWidth,
938
+ idealWidth: hasDeterminateRows(context.rows) ? Math.max(elapsedContentWidth, RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR) : elapsedContentWidth,
939
+ renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
940
+ task: row.task,
941
+ tree: row.tree,
942
+ now: context.now,
943
+ tick: context.tick,
944
+ isTTY
945
+ })
946
+ }, {
947
+ id: "compact",
948
+ minWidth: MIN_ELAPSED_WIDTH,
949
+ idealWidth: elapsedContentWidth,
950
+ renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
951
+ task: row.task,
952
+ tree: row.tree,
953
+ now: context.now,
954
+ tick: context.tick,
955
+ isTTY
956
+ })
957
+ }]
958
+ };
959
+ };
228
960
 
229
961
  //#endregion
230
962
  //#region src/ink-renderer/columns/eta-column.tsx
231
- const EtaColumn = ({ task, now }) => {
232
- if (task.status !== "running" || task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, {});
963
+ const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
964
+ const etaDurationText = (task, now) => {
965
+ if (task.status !== "running" || !isDeterminate(task)) return;
233
966
  const eta = formatEta(task, now);
967
+ return eta.length > 0 ? eta : "--";
968
+ };
969
+ const EtaColumn = ({ task, now, mode }) => {
970
+ const duration = etaDurationText(task, now);
971
+ if (duration === void 0) return /* @__PURE__ */ jsx(Text, {});
234
972
  return /* @__PURE__ */ jsx(Text, {
235
973
  wrap: "truncate-end",
236
974
  color: "gray",
237
- children: eta.length > 0 ? `ETA: ${eta}` : "ETA: --"
975
+ children: mode === "prefixed" ? `ETA: ${duration}` : mode === "primary" ? primaryUnit(duration) : duration
238
976
  });
239
977
  };
978
+ const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
979
+ const computeEtaMetrics = (rows, now) => {
980
+ let hasEta = false;
981
+ let prefixedWidth = 0;
982
+ let durationWidth = 0;
983
+ let primaryUnitWidth = 0;
984
+ for (const row of rows) {
985
+ const duration = etaDurationText(row.task, now);
986
+ if (duration === void 0) continue;
987
+ hasEta = true;
988
+ const prefixed = `ETA: ${duration}`;
989
+ prefixedWidth = Math.max(prefixedWidth, textWidth(prefixed));
990
+ durationWidth = Math.max(durationWidth, textWidth(duration));
991
+ primaryUnitWidth = Math.max(primaryUnitWidth, textWidth(primaryUnit(duration)));
992
+ }
993
+ return {
994
+ hasEta,
995
+ prefixedWidth,
996
+ durationWidth: Math.max(2, durationWidth),
997
+ primaryUnitWidth: Math.max(2, primaryUnitWidth)
998
+ };
999
+ };
1000
+ const createEtaColumnSpec = (context, isTTY) => {
1001
+ const metrics = computeEtaMetrics(context.rows, context.now);
1002
+ if (!metrics.hasEta) return;
1003
+ return {
1004
+ id: "eta",
1005
+ grow: 0,
1006
+ canHide: true,
1007
+ variants: [
1008
+ {
1009
+ id: "prefixed",
1010
+ minWidth: metrics.prefixedWidth,
1011
+ idealWidth: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR),
1012
+ renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1013
+ task: row.task,
1014
+ tree: row.tree,
1015
+ now: context.now,
1016
+ tick: context.tick,
1017
+ isTTY,
1018
+ mode: "prefixed"
1019
+ })
1020
+ },
1021
+ {
1022
+ id: "duration",
1023
+ minWidth: metrics.durationWidth,
1024
+ idealWidth: metrics.durationWidth,
1025
+ renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1026
+ task: row.task,
1027
+ tree: row.tree,
1028
+ now: context.now,
1029
+ tick: context.tick,
1030
+ isTTY,
1031
+ mode: "duration"
1032
+ })
1033
+ },
1034
+ {
1035
+ id: "primary",
1036
+ minWidth: metrics.primaryUnitWidth,
1037
+ idealWidth: metrics.primaryUnitWidth,
1038
+ renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1039
+ task: row.task,
1040
+ tree: row.tree,
1041
+ now: context.now,
1042
+ tick: context.tick,
1043
+ isTTY,
1044
+ mode: "primary"
1045
+ })
1046
+ }
1047
+ ]
1048
+ };
1049
+ };
1050
+
1051
+ //#endregion
1052
+ //#region src/ink-renderer/columns/planner.ts
1053
+ const clampInt = (value, min, max) => Math.max(min, Math.min(max, Math.floor(value)));
1054
+ const activeColumns = (columns) => columns.filter((column) => !column.hidden);
1055
+ const visibleColumns = (columns) => columns.filter((column) => !column.hidden && column.width > 0);
1056
+ const visibleGapWidth = (columns) => Math.max(0, visibleColumns(columns).length - 1);
1057
+ const totalWidth = (columns) => visibleColumns(columns).reduce((sum, column) => sum + column.width, 0) + visibleGapWidth(columns);
1058
+ const currentVariant = (column) => column.variants[column.variantIndex];
1059
+ const variantMaxWidth = (column) => {
1060
+ const variant = currentVariant(column);
1061
+ return variant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(variant.minWidth, variant.maxWidth);
1062
+ };
1063
+ const reduceOverflowByShrink = (columns, overflow) => {
1064
+ let remaining = overflow;
1065
+ const candidates = activeColumns(columns).filter((column) => column.width > currentVariant(column).minWidth).sort((a, b) => currentVariant(a).shrinkResistance - currentVariant(b).shrinkResistance);
1066
+ for (const column of candidates) {
1067
+ if (remaining <= 0) break;
1068
+ const variant = currentVariant(column);
1069
+ const reducible = column.width - variant.minWidth;
1070
+ if (reducible <= 0) continue;
1071
+ const delta = Math.min(reducible, remaining);
1072
+ column.width -= delta;
1073
+ remaining -= delta;
1074
+ }
1075
+ return remaining;
1076
+ };
1077
+ const nextDemoteCandidate = (columns) => activeColumns(columns).filter((column) => column.variantIndex + 1 < column.variants.length).sort((a, b) => currentVariant(a).demoteResistance - currentVariant(b).demoteResistance)[0];
1078
+ const applyDemote = (column) => {
1079
+ const nextVariant = column.variants[column.variantIndex + 1];
1080
+ column.variantIndex += 1;
1081
+ const nextMaxWidth = nextVariant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(nextVariant.minWidth, nextVariant.maxWidth);
1082
+ column.width = Math.max(nextVariant.minWidth, Math.min(column.width, nextVariant.idealWidth, nextMaxWidth));
1083
+ };
1084
+ const nextHideCandidate = (columns) => activeColumns(columns).filter((column) => column.spec.canHide).sort((a, b) => currentVariant(a).hideResistance - currentVariant(b).hideResistance)[0];
1085
+ const applyHide = (column) => {
1086
+ column.hidden = true;
1087
+ column.width = 0;
1088
+ };
1089
+ const distributeGrowth = (columns, extra) => {
1090
+ if (extra <= 0) return;
1091
+ let remaining = extra;
1092
+ while (remaining > 0) {
1093
+ const candidates = activeColumns(columns).filter((column) => column.spec.grow > 0 && column.width < variantMaxWidth(column));
1094
+ if (candidates.length === 0) break;
1095
+ const growSum = candidates.reduce((sum, column) => sum + Math.max(0, column.spec.grow), 0);
1096
+ if (growSum <= 0) break;
1097
+ let distributed = 0;
1098
+ for (const column of candidates) {
1099
+ const weight = Math.max(0, column.spec.grow);
1100
+ if (weight <= 0) continue;
1101
+ const headroom = Math.max(0, variantMaxWidth(column) - column.width);
1102
+ if (headroom <= 0) continue;
1103
+ const share = Math.min(headroom, Math.floor(remaining * weight / growSum));
1104
+ if (share <= 0) continue;
1105
+ column.width += share;
1106
+ distributed += share;
1107
+ }
1108
+ if (distributed === 0) {
1109
+ for (const column of candidates) {
1110
+ if (remaining <= 0) break;
1111
+ if (Math.max(0, variantMaxWidth(column) - column.width) <= 0) continue;
1112
+ column.width += 1;
1113
+ distributed += 1;
1114
+ remaining -= 1;
1115
+ }
1116
+ if (distributed === 0) break;
1117
+ continue;
1118
+ }
1119
+ remaining -= distributed;
1120
+ }
1121
+ };
1122
+ const planColumns = ({ context, baselineWidth, columns }) => {
1123
+ const mutable = columns.flatMap((column) => {
1124
+ if (column.variants.length === 0) return [];
1125
+ const first = column.variants[0];
1126
+ return [{
1127
+ spec: column,
1128
+ variants: column.variants,
1129
+ variantIndex: 0,
1130
+ width: Math.max(first.minWidth, Math.min(first.idealWidth, first.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(first.minWidth, first.maxWidth))),
1131
+ hidden: false
1132
+ }];
1133
+ });
1134
+ if (mutable.length === 0) return {
1135
+ rowWidth: 0,
1136
+ columns: []
1137
+ };
1138
+ const naturalWidth = totalWidth(mutable);
1139
+ const clampedTerminal = context.terminalColumns === void 0 ? void 0 : clampInt(context.terminalColumns, 1, Number.POSITIVE_INFINITY);
1140
+ const baselineTarget = Math.max(1, Math.max(baselineWidth, naturalWidth));
1141
+ const target = clampedTerminal === void 0 ? baselineTarget : Math.min(baselineTarget, clampedTerminal);
1142
+ if (naturalWidth < target) distributeGrowth(mutable, target - naturalWidth);
1143
+ else if (naturalWidth > target) {
1144
+ let overflow = naturalWidth - target;
1145
+ let progressed = true;
1146
+ while (overflow > 0 && progressed) {
1147
+ const before = overflow;
1148
+ overflow = reduceOverflowByShrink(mutable, overflow);
1149
+ if (overflow <= 0) break;
1150
+ const demoteCandidate = nextDemoteCandidate(mutable);
1151
+ const hideCandidate = nextHideCandidate(mutable);
1152
+ if (demoteCandidate === void 0 && hideCandidate === void 0) {
1153
+ progressed = before !== overflow;
1154
+ continue;
1155
+ }
1156
+ if ((demoteCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(demoteCandidate).demoteResistance) <= (hideCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(hideCandidate).hideResistance) && demoteCandidate !== void 0) applyDemote(demoteCandidate);
1157
+ else if (hideCandidate !== void 0) applyHide(hideCandidate);
1158
+ overflow = Math.max(0, totalWidth(mutable) - target);
1159
+ progressed = true;
1160
+ }
1161
+ const compactedWidth = totalWidth(mutable);
1162
+ if (compactedWidth < target) distributeGrowth(mutable, target - compactedWidth);
1163
+ }
1164
+ const visible = visibleColumns(mutable);
1165
+ return {
1166
+ rowWidth: totalWidth(mutable),
1167
+ columns: visible.map((column) => {
1168
+ const variant = currentVariant(column);
1169
+ return {
1170
+ id: column.spec.id,
1171
+ variantId: variant.id,
1172
+ width: column.width,
1173
+ renderCell: variant.renderCell
1174
+ };
1175
+ })
1176
+ };
1177
+ };
240
1178
 
241
1179
  //#endregion
242
- //#region src/ink-renderer/task-row.tsx
243
- const TaskRow = ({ row, now, tick, isTTY, widths }) => {
244
- const props = {
245
- task: row.task,
246
- tree: row.tree,
1180
+ //#region src/ink-renderer/columns/layout.tsx
1181
+ /**
1182
+ * Frame planning algorithm (per render tick):
1183
+ *
1184
+ * 1. Ask each column module to produce a logical column spec (variants + measured widths).
1185
+ * 2. Apply shared resistance rules (shrink / demote / hide) to those variants.
1186
+ * 3. Feed resolved columns into `planColumns`.
1187
+ * 4. `planColumns` shrinks, demotes, and hides until the row fits target width.
1188
+ * 5. Render rows with the selected variant and width for each visible column.
1189
+ */
1190
+ const BASELINE_ROW_WIDTH = 150;
1191
+ const DEFAULT_RESISTANCE = 1e3;
1192
+ const variantOrderKey = ({ columnId, variantId }) => `${columnId}:${variantId}`;
1193
+ const SHRINK_ORDER = [
1194
+ {
1195
+ columnId: "eta",
1196
+ variantId: "prefixed"
1197
+ },
1198
+ {
1199
+ columnId: "elapsed",
1200
+ variantId: "stable"
1201
+ },
1202
+ {
1203
+ columnId: "eta",
1204
+ variantId: "duration"
1205
+ },
1206
+ {
1207
+ columnId: "eta",
1208
+ variantId: "primary"
1209
+ },
1210
+ {
1211
+ columnId: "bar",
1212
+ variantId: "compact"
1213
+ },
1214
+ {
1215
+ columnId: "bar",
1216
+ variantId: "full"
1217
+ },
1218
+ {
1219
+ columnId: "amount",
1220
+ variantId: "text"
1221
+ },
1222
+ {
1223
+ columnId: "elapsed",
1224
+ variantId: "compact"
1225
+ },
1226
+ {
1227
+ columnId: "description",
1228
+ variantId: "tree"
1229
+ },
1230
+ {
1231
+ columnId: "amount",
1232
+ variantId: "detailed"
1233
+ },
1234
+ {
1235
+ columnId: "amount",
1236
+ variantId: "processed"
1237
+ },
1238
+ {
1239
+ columnId: "description",
1240
+ variantId: "plain"
1241
+ }
1242
+ ];
1243
+ const DEMOTE_ORDER = [
1244
+ {
1245
+ columnId: "description",
1246
+ variantId: "tree"
1247
+ },
1248
+ {
1249
+ columnId: "eta",
1250
+ variantId: "prefixed"
1251
+ },
1252
+ {
1253
+ columnId: "eta",
1254
+ variantId: "duration"
1255
+ },
1256
+ {
1257
+ columnId: "elapsed",
1258
+ variantId: "stable"
1259
+ },
1260
+ {
1261
+ columnId: "bar",
1262
+ variantId: "full"
1263
+ },
1264
+ {
1265
+ columnId: "amount",
1266
+ variantId: "detailed"
1267
+ }
1268
+ ];
1269
+ const HIDE_ORDER = [
1270
+ {
1271
+ columnId: "eta",
1272
+ variantId: "primary"
1273
+ },
1274
+ {
1275
+ columnId: "eta",
1276
+ variantId: "prefixed"
1277
+ },
1278
+ {
1279
+ columnId: "eta",
1280
+ variantId: "duration"
1281
+ },
1282
+ {
1283
+ columnId: "bar",
1284
+ variantId: "full"
1285
+ },
1286
+ {
1287
+ columnId: "bar",
1288
+ variantId: "compact"
1289
+ },
1290
+ {
1291
+ columnId: "amount",
1292
+ variantId: "text"
1293
+ },
1294
+ {
1295
+ columnId: "amount",
1296
+ variantId: "detailed"
1297
+ },
1298
+ {
1299
+ columnId: "amount",
1300
+ variantId: "processed"
1301
+ }
1302
+ ];
1303
+ const toResistanceLookup = (order) => new Map(order.map((item, index) => [variantOrderKey(item), index + 1]));
1304
+ const shrinkResistanceLookup = toResistanceLookup(SHRINK_ORDER);
1305
+ const demoteResistanceLookup = toResistanceLookup(DEMOTE_ORDER);
1306
+ const hideResistanceLookup = toResistanceLookup(HIDE_ORDER);
1307
+ const resistanceFor = (lookup, columnId, variantId) => lookup.get(`${columnId}:${variantId}`) ?? DEFAULT_RESISTANCE;
1308
+ const buildColumns = (context, isTTY) => {
1309
+ return resolveColumnSpecs([
1310
+ createDescriptionColumnSpec(context, isTTY),
1311
+ createBarColumnSpec(context, isTTY),
1312
+ createAmountColumnSpec(context),
1313
+ createElapsedColumnSpec(context, isTTY),
1314
+ createEtaColumnSpec(context, isTTY)
1315
+ ].filter((spec) => spec !== void 0), {
1316
+ shrink: (columnId, variantId) => resistanceFor(shrinkResistanceLookup, columnId, variantId),
1317
+ demote: (columnId, variantId) => resistanceFor(demoteResistanceLookup, columnId, variantId),
1318
+ hide: (columnId, variantId) => resistanceFor(hideResistanceLookup, columnId, variantId)
1319
+ });
1320
+ };
1321
+ const computeFrameLayout = (rows, now, tick, terminalColumns, isTTY) => {
1322
+ const context = {
1323
+ rows,
247
1324
  now,
248
1325
  tick,
249
- isTTY,
250
- showTree: widths.showTree
1326
+ terminalColumns
251
1327
  };
252
- return /* @__PURE__ */ jsxs(Box, {
1328
+ return planColumns({
1329
+ context,
1330
+ columns: buildColumns(context, isTTY),
1331
+ baselineWidth: hasDeterminateRows(rows) ? BASELINE_ROW_WIDTH : 1
1332
+ });
1333
+ };
1334
+
1335
+ //#endregion
1336
+ //#region src/ink-renderer/view/task-row.tsx
1337
+ const TaskRow = ({ row, layout }) => {
1338
+ return /* @__PURE__ */ jsx(Box, {
253
1339
  flexDirection: "row",
254
- minWidth: widths.row,
255
- children: [
256
- /* @__PURE__ */ jsx(Box, {
257
- width: widths.description,
258
- flexShrink: 1,
259
- marginRight: 1,
260
- children: /* @__PURE__ */ jsx(DescriptionColumn, { ...props })
261
- }),
262
- widths.bar > 0 ? /* @__PURE__ */ jsx(Box, {
263
- width: widths.bar,
264
- flexShrink: 0,
265
- marginRight: 1,
266
- children: /* @__PURE__ */ jsx(BarColumn, {
267
- ...props,
268
- width: Math.max(1, Math.min(widths.bar, DEFAULT_BAR_WIDTH))
269
- })
270
- }) : null,
271
- /* @__PURE__ */ jsx(Box, {
272
- width: widths.amount,
273
- flexShrink: 0,
274
- marginRight: 1,
275
- children: /* @__PURE__ */ jsx(AmountColumn, { ...props })
276
- }),
277
- /* @__PURE__ */ jsx(Box, {
278
- width: widths.elapsed,
279
- flexShrink: 0,
280
- marginRight: 1,
281
- children: /* @__PURE__ */ jsx(ElapsedColumn, { ...props })
282
- }),
283
- widths.eta > 0 ? /* @__PURE__ */ jsx(Box, {
284
- width: widths.eta,
285
- flexShrink: 0,
286
- children: /* @__PURE__ */ jsx(EtaColumn, { ...props })
287
- }) : null
288
- ]
1340
+ minWidth: layout.rowWidth,
1341
+ children: layout.columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
1342
+ width: column.width,
1343
+ flexShrink: column.id === "description" ? 1 : 0,
1344
+ marginRight: index < layout.columns.length - 1 ? 1 : 0,
1345
+ children: column.renderCell(row, column.width)
1346
+ }, column.id))
289
1347
  });
290
1348
  };
291
1349
 
292
1350
  //#endregion
293
- //#region src/ink-renderer/app.tsx
294
- const ProgressApp = ({ rows, now, tick, isTTY, terminalColumns }) => {
295
- const widths = computeSharedColumnWidths(rows, now, tick, terminalColumns);
1351
+ //#region src/ink-renderer/view/progress-view.tsx
1352
+ const ProgressView = ({ rows, now, tick, isTTY, terminalColumns }) => {
1353
+ const layout = computeFrameLayout(rows, now, tick, terminalColumns, isTTY);
296
1354
  return /* @__PURE__ */ jsx(Box, {
297
1355
  flexDirection: "column",
298
1356
  children: rows.map((row) => /* @__PURE__ */ jsx(TaskRow, {
299
1357
  row,
300
- now,
301
- tick,
302
- isTTY,
303
- widths
1358
+ layout
304
1359
  }, row.task.id))
305
1360
  });
306
1361
  };
307
1362
 
308
1363
  //#endregion
309
- //#region src/ink-renderer/model.ts
310
- const orderedVisibleTasks = (store) => store.renderOrder.flatMap((row) => {
311
- const snapshot = store.tasks.get(row.id);
312
- if (!snapshot || snapshot.transient && snapshot.status !== "running") return [];
313
- return [{
314
- snapshot,
315
- depth: row.depth
316
- }];
317
- });
318
- const toTaskRows = (store) => computeTreeInfo(orderedVisibleTasks(store)).map((entry) => ({
319
- task: entry.snapshot,
320
- tree: entry.tree
321
- }));
322
-
323
- //#endregion
324
- //#region src/ink-renderer/service.tsx
325
- const RENDER_INTERVAL_MILLIS = 100;
326
- const hasRunningSpinners = (tasks) => tasks.some((task) => task.status === "running" && task.units._tag === "IndeterminateTaskUnits");
327
- const createInkWritable = (terminal) => new Writable({ write(chunk, _encoding, callback) {
328
- try {
329
- const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : `${chunk}`;
330
- Effect.runSync(terminal.writeStderr(text));
331
- callback();
332
- } catch (error) {
333
- callback(error);
334
- }
335
- } });
336
- const makeDefaultInkRenderer = () => ({ run: (storeRef, dirtyRef, terminal, isTTY) => Effect.gen(function* () {
337
- const output = createInkWritable(terminal);
338
- let instance;
339
- let tick = 0;
340
- let rendererActive = false;
341
- const renderStore = (store, now, terminalColumns) => Effect.sync(() => {
342
- const app = /* @__PURE__ */ jsx(ProgressApp, {
343
- rows: toTaskRows(store),
344
- now,
345
- tick,
346
- isTTY,
347
- terminalColumns
348
- });
349
- if (instance === void 0) {
350
- instance = render(app, {
351
- stdout: output,
352
- stderr: output,
353
- patchConsole: true,
354
- exitOnCtrlC: false,
355
- debug: false
356
- });
357
- return;
358
- }
359
- instance.rerender(app);
360
- });
361
- return yield* Effect.gen(function* () {
362
- rendererActive = true;
363
- while (true) {
364
- const dirty = yield* Ref.getAndSet(dirtyRef, false);
365
- const store = yield* Ref.get(storeRef);
366
- const tasks = Array.from(store.tasks.values()).filter((task) => !(task.transient && task.status !== "running"));
367
- if (dirty || hasRunningSpinners(tasks)) yield* renderStore(store, yield* Clock.currentTimeMillis, isTTY ? yield* terminal.stderrColumns : void 0);
368
- tick += 1;
369
- yield* Effect.sleep(RENDER_INTERVAL_MILLIS);
370
- }
371
- }).pipe(Effect.ensuring(Effect.gen(function* () {
372
- if (rendererActive) yield* renderStore(yield* Ref.get(storeRef), yield* Clock.currentTimeMillis, isTTY ? yield* terminal.stderrColumns : void 0);
373
- yield* Effect.sync(() => {
374
- instance?.unmount();
375
- });
376
- })));
377
- }) });
378
- var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")() {
379
- static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeDefaultInkRenderer()));
1364
+ //#region src/ink-renderer/view/hooks/use-now-clock.ts
1365
+ const useNowClock = (active, intervalMillis) => {
1366
+ const [now, setNow] = useState(() => Date.now());
1367
+ useEffect(() => {
1368
+ if (!active) return;
1369
+ setNow(Date.now());
1370
+ const interval = setInterval(() => {
1371
+ setNow(Date.now());
1372
+ }, intervalMillis);
1373
+ return () => {
1374
+ clearInterval(interval);
1375
+ };
1376
+ }, [active, intervalMillis]);
1377
+ return now;
380
1378
  };
381
1379
 
382
1380
  //#endregion
383
- //#region src/terminal.ts
384
- const withRawInputCapture = (effect) => Effect.suspend(() => {
385
- if (!process.stdin.isTTY) return effect;
386
- const stdin = process.stdin;
387
- const wasRaw = Boolean(stdin.isRaw);
388
- const onData = (chunk) => {
389
- if (chunk.length === 1 && chunk[0] === 3) process.kill(process.pid, "SIGINT");
390
- };
391
- return Effect.acquireUseRelease(Effect.sync(() => {
392
- stdin.resume();
393
- stdin.setRawMode?.(true);
394
- stdin.on("data", onData);
395
- }), () => effect, () => Effect.sync(() => {
396
- try {
397
- stdin.off("data", onData);
398
- stdin.setRawMode?.(wasRaw);
399
- stdin.pause();
400
- } catch {}
401
- }));
402
- });
403
- const defaultTerminalService = {
404
- isTTY: Effect.sync(() => Boolean(process.stderr.isTTY)),
405
- stderrRows: Effect.sync(() => process.stderr.rows),
406
- stderrColumns: Effect.sync(() => process.stderr.columns),
407
- writeStderr: (text) => Effect.sync(() => {
408
- process.stderr.write(text);
409
- }),
410
- withRawInputCapture
411
- };
412
- var ProgressTerminal = class ProgressTerminal extends Context.Tag("stromseng.dev/ProgressTerminal")() {
413
- static Default = Layer.succeed(ProgressTerminal, defaultTerminalService);
1381
+ //#region src/ink-renderer/view/hooks/use-spinner-clock.ts
1382
+ const useSpinnerClock = (active, intervalMillis) => {
1383
+ const [tick, setTick] = useState(0);
1384
+ useEffect(() => {
1385
+ if (!active) return;
1386
+ const interval = setInterval(() => {
1387
+ setTick((current) => current + 1);
1388
+ }, intervalMillis);
1389
+ return () => {
1390
+ clearInterval(interval);
1391
+ };
1392
+ }, [active, intervalMillis]);
1393
+ return tick;
414
1394
  };
415
1395
 
416
1396
  //#endregion
417
- //#region src/types.ts
418
- const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
419
- const TaskId = Brand.nominal();
420
- const TaskStatusSchema = Schema.Literal("running", "done", "failed");
421
- var DeterminateTaskUnits = class extends Schema.TaggedClass()("DeterminateTaskUnits", {
422
- completed: Schema.Number,
423
- total: Schema.Number
424
- }) {};
425
- var IndeterminateTaskUnits = class extends Schema.TaggedClass()("IndeterminateTaskUnits", { spinnerFrame: Schema.Number }) {};
426
- const TaskUnitsSchema = Schema.Union(DeterminateTaskUnits, IndeterminateTaskUnits);
427
- var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
428
- id: TaskIdSchema,
429
- parentId: Schema.NullOr(TaskIdSchema),
430
- description: Schema.String,
431
- status: TaskStatusSchema,
432
- transient: Schema.Boolean,
433
- units: TaskUnitsSchema,
434
- startedAt: Schema.Number,
435
- completedAt: Schema.NullOr(Schema.Number)
436
- }) {};
437
- var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
438
- var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
439
- taskId: TaskIdSchema,
440
- parentId: Schema.NullOr(TaskIdSchema),
441
- description: Schema.String,
442
- total: Schema.optional(Schema.Number),
443
- transient: Schema.Boolean
444
- }) {};
445
- var TaskUpdatedEvent = class extends Schema.TaggedClass()("TaskUpdated", {
446
- taskId: TaskIdSchema,
447
- description: Schema.optional(Schema.String),
448
- completed: Schema.optional(Schema.Number),
449
- total: Schema.optional(Schema.Number),
450
- transient: Schema.optional(Schema.Boolean)
451
- }) {};
452
- var TaskAdvancedEvent = class extends Schema.TaggedClass()("TaskAdvanced", {
453
- taskId: TaskIdSchema,
454
- amount: Schema.Number
455
- }) {};
456
- var TaskCompletedEvent = class extends Schema.TaggedClass()("TaskCompleted", { taskId: TaskIdSchema }) {};
457
- var TaskFailedEvent = class extends Schema.TaggedClass()("TaskFailed", { taskId: TaskIdSchema }) {};
458
- var TaskRemovedEvent = class extends Schema.TaggedClass()("TaskRemoved", { taskId: TaskIdSchema }) {};
459
- const ProgressTaskEventSchema = Schema.Union(TaskAddedEvent, TaskUpdatedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskFailedEvent, TaskRemovedEvent);
460
- const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
1397
+ //#region src/ink-renderer/view/render-root.tsx
1398
+ const SPINNER_INTERVAL_MILLIS = 100;
1399
+ const NOW_INTERVAL_MILLIS = 1e3;
1400
+ const ProgressRoot = ({ store, isTTY, getTerminalColumns }) => {
1401
+ const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1402
+ const tick = useSpinnerClock(snapshot.hasRunningTasks, SPINNER_INTERVAL_MILLIS);
1403
+ const now = useNowClock(snapshot.hasRunningTasks, NOW_INTERVAL_MILLIS);
1404
+ return /* @__PURE__ */ jsx(ProgressView, {
1405
+ rows: snapshot.rows,
1406
+ now,
1407
+ tick,
1408
+ isTTY,
1409
+ terminalColumns: getTerminalColumns()
1410
+ });
1411
+ };
461
1412
 
462
1413
  //#endregion
463
- //#region src/runtime.ts
464
- const updatedSnapshot = (snapshot, options) => {
465
- const currentUnits = snapshot.units;
466
- const units = (() => {
467
- if (options.total !== void 0) {
468
- if (options.total <= 0) return new IndeterminateTaskUnits({ spinnerFrame: 0 });
469
- const completed = options.completed ?? (currentUnits._tag === "DeterminateTaskUnits" ? currentUnits.completed : 0);
470
- return new DeterminateTaskUnits({
471
- completed: Math.max(0, completed),
472
- total: Math.max(0, options.total)
473
- });
474
- }
475
- if (currentUnits._tag === "DeterminateTaskUnits") {
476
- if (options.completed === void 0) return currentUnits;
477
- return new DeterminateTaskUnits({
478
- completed: Math.max(0, options.completed),
479
- total: currentUnits.total
480
- });
481
- }
482
- return currentUnits;
483
- })();
484
- return new TaskSnapshot({
485
- id: snapshot.id,
486
- parentId: snapshot.parentId,
487
- description: options.description ?? snapshot.description,
488
- status: snapshot.status,
489
- transient: options.transient ?? snapshot.transient,
490
- units,
491
- startedAt: snapshot.startedAt,
492
- completedAt: snapshot.completedAt
1414
+ //#region src/services/ink-renderer.tsx
1415
+ const MAX_FPS = 12;
1416
+ const makeDefaultInkRenderer = () => ({ run: (store, stdio, isTTY) => Effect.sync(() => render(/* @__PURE__ */ jsx(ProgressRoot, {
1417
+ store,
1418
+ isTTY,
1419
+ getTerminalColumns: () => isTTY ? stdio.stderr.columns : void 0
1420
+ }), {
1421
+ stdout: stdio.stdout,
1422
+ stderr: stdio.stderr,
1423
+ patchConsole: true,
1424
+ exitOnCtrlC: false,
1425
+ debug: false,
1426
+ maxFps: MAX_FPS
1427
+ })).pipe(Effect.flatMap((instance) => Effect.never.pipe(Effect.ensuring(Effect.gen(function* () {
1428
+ store.flush();
1429
+ yield* Effect.sleep("0 millis");
1430
+ yield* Effect.sync(() => {
1431
+ instance.unmount();
493
1432
  });
1433
+ }))))) });
1434
+ var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")() {
1435
+ static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeDefaultInkRenderer()));
494
1436
  };
495
- const withTransient = (snapshot, transient) => new TaskSnapshot({
496
- id: snapshot.id,
497
- parentId: snapshot.parentId,
498
- description: snapshot.description,
499
- status: snapshot.status,
500
- transient,
501
- units: snapshot.units,
502
- startedAt: snapshot.startedAt,
503
- completedAt: snapshot.completedAt
504
- });
505
- const findInsertionIndex = (renderOrder, parentId) => {
506
- if (parentId === null) return {
507
- index: renderOrder.length,
508
- depth: 0
509
- };
510
- const parentIdx = renderOrder.findIndex((row) => row.id === parentId);
511
- if (parentIdx === -1) return {
512
- index: renderOrder.length,
513
- depth: 0
514
- };
515
- const parentDepth = renderOrder[parentIdx].depth;
516
- let i = parentIdx + 1;
517
- while (i < renderOrder.length && renderOrder[i].depth > parentDepth) i++;
518
- return {
519
- index: i,
520
- depth: parentDepth + 1
521
- };
1437
+
1438
+ //#endregion
1439
+ //#region src/services/stdio.ts
1440
+ const defaultStdioService = {
1441
+ stdout: process.stdout,
1442
+ stderr: process.stderr
522
1443
  };
523
- const removeFromRenderOrder = (renderOrder, taskId) => {
524
- const idx = renderOrder.findIndex((row) => row.id === taskId);
525
- if (idx === -1) return renderOrder;
526
- const taskDepth = renderOrder[idx].depth;
527
- let end = idx + 1;
528
- while (end < renderOrder.length && renderOrder[end].depth > taskDepth) end++;
529
- const next = [...renderOrder];
530
- next.splice(idx, end - idx);
531
- return next;
1444
+ var ProgressStdio = class ProgressStdio extends Context.Tag("stromseng.dev/effective-progress/ProgressStdio")() {
1445
+ static Default = Layer.succeed(ProgressStdio, defaultStdioService);
532
1446
  };
1447
+
1448
+ //#endregion
1449
+ //#region src/services/progress.ts
533
1450
  const makeProgressService = Effect.gen(function* () {
534
- const terminal = yield* ProgressTerminal;
1451
+ const stdio = yield* ProgressStdio;
535
1452
  const inkRenderer = yield* InkRenderer;
536
1453
  const outerConsole = yield* Effect.console;
537
- const isTTY = yield* terminal.isTTY;
538
- const nextTaskIdRef = yield* Ref.make(0);
539
- const storeRef = yield* Ref.make({
540
- tasks: /* @__PURE__ */ new Map(),
541
- renderOrder: []
542
- });
543
- const dirtyRef = yield* Ref.make(true);
1454
+ const isTTY = Boolean(stdio.stderr.isTTY);
1455
+ const store = makeProgressRenderStore();
544
1456
  const currentParentRef = yield* FiberRef.make(Option.none());
545
1457
  const scope = yield* Effect.scope;
546
- const markDirty = Ref.set(dirtyRef, true);
547
1458
  const log = (...args) => args.length === 0 ? Effect.void : outerConsole.log(...args);
548
- yield* Effect.forkIn(inkRenderer.run(storeRef, dirtyRef, terminal, isTTY), scope);
1459
+ yield* Effect.forkIn(inkRenderer.run(store, stdio, isTTY), scope);
549
1460
  yield* Effect.sleep("0 millis");
550
1461
  const addTask = (options) => Effect.gen(function* () {
551
1462
  const resolvedParentId = options.parentId === void 0 ? yield* FiberRef.get(currentParentRef) : Option.some(options.parentId);
552
- const taskId = TaskId(yield* Ref.updateAndGet(nextTaskIdRef, (id) => id + 1));
553
- const units = options.total === void 0 || options.total <= 0 ? new IndeterminateTaskUnits({ spinnerFrame: 0 }) : new DeterminateTaskUnits({
554
- completed: 0,
555
- total: Math.max(0, options.total)
556
- });
557
- const store = yield* Ref.get(storeRef);
558
- const parentSnapshot = Option.isSome(resolvedParentId) ? store.tasks.get(resolvedParentId.value) : void 0;
559
- const now = yield* Clock.currentTimeMillis;
560
- const parentIdValue = Option.getOrNull(resolvedParentId);
561
- const snapshot = new TaskSnapshot({
562
- id: taskId,
563
- parentId: parentIdValue,
564
- description: options.description,
565
- status: "running",
566
- transient: parentSnapshot?.transient ?? options.transient ?? false,
567
- units,
568
- startedAt: now,
569
- completedAt: null
570
- });
571
- yield* Ref.update(storeRef, (s) => {
572
- const nextTasks = new Map(s.tasks);
573
- nextTasks.set(taskId, snapshot);
574
- const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
575
- const nextOrder = [...s.renderOrder];
576
- nextOrder.splice(index, 0, {
577
- id: taskId,
578
- depth
579
- });
580
- return {
581
- tasks: nextTasks,
582
- renderOrder: nextOrder
583
- };
584
- });
585
- yield* markDirty;
586
- return taskId;
587
- });
588
- const updateTask = (taskId, options) => Ref.update(storeRef, (store) => {
589
- const snapshot = store.tasks.get(taskId);
590
- if (!snapshot) return store;
591
- const nextTasks = new Map(store.tasks);
592
- const nextSnapshot = updatedSnapshot(snapshot, options);
593
- nextTasks.set(taskId, nextSnapshot);
594
- if (options.transient !== void 0) for (const [candidateId, candidate] of store.tasks.entries()) {
595
- if (candidateId === taskId) continue;
596
- let parentId = candidate.parentId;
597
- let isDescendant = false;
598
- while (parentId !== null) {
599
- if (parentId === taskId) {
600
- isDescendant = true;
601
- break;
602
- }
603
- parentId = store.tasks.get(parentId)?.parentId ?? null;
604
- }
605
- if (isDescendant) nextTasks.set(candidateId, withTransient(candidate, nextSnapshot.transient));
606
- }
607
- return {
608
- tasks: nextTasks,
609
- renderOrder: store.renderOrder
610
- };
611
- }).pipe(Effect.zipRight(markDirty));
612
- const advanceTask = (taskId, amount = 1) => Ref.update(storeRef, (store) => {
613
- const snapshot = store.tasks.get(taskId);
614
- if (!snapshot) return store;
615
- const units = snapshot.units._tag === "DeterminateTaskUnits" ? new DeterminateTaskUnits({
616
- completed: Math.min(snapshot.units.total, snapshot.units.completed + amount),
617
- total: snapshot.units.total
618
- }) : new IndeterminateTaskUnits({ spinnerFrame: Math.max(0, snapshot.units.spinnerFrame + amount) });
619
- const nextTasks = new Map(store.tasks);
620
- nextTasks.set(taskId, new TaskSnapshot({
621
- id: snapshot.id,
622
- parentId: snapshot.parentId,
623
- description: snapshot.description,
624
- status: snapshot.status,
625
- transient: snapshot.transient,
626
- units,
627
- startedAt: snapshot.startedAt,
628
- completedAt: snapshot.completedAt
629
- }));
630
- return {
631
- tasks: nextTasks,
632
- renderOrder: store.renderOrder
633
- };
634
- }).pipe(Effect.zipRight(markDirty));
635
- const completeTask = (taskId) => Effect.gen(function* () {
636
- const now = yield* Clock.currentTimeMillis;
637
- yield* Ref.update(storeRef, (store) => {
638
- const snapshot = store.tasks.get(taskId);
639
- if (!snapshot) return store;
640
- const nextTasks = new Map(store.tasks);
641
- if (snapshot.transient) {
642
- nextTasks.delete(taskId);
643
- return {
644
- tasks: nextTasks,
645
- renderOrder: removeFromRenderOrder(store.renderOrder, taskId)
646
- };
647
- }
648
- nextTasks.set(taskId, new TaskSnapshot({
649
- id: snapshot.id,
650
- parentId: snapshot.parentId,
651
- description: snapshot.description,
652
- status: "done",
653
- transient: snapshot.transient,
654
- units: snapshot.units._tag === "DeterminateTaskUnits" ? new DeterminateTaskUnits({
655
- completed: snapshot.units.total,
656
- total: snapshot.units.total
657
- }) : snapshot.units,
658
- startedAt: snapshot.startedAt,
659
- completedAt: now
660
- }));
661
- return {
662
- tasks: nextTasks,
663
- renderOrder: store.renderOrder
664
- };
665
- });
666
- yield* markDirty;
667
- });
668
- const failTask = (taskId) => Effect.gen(function* () {
669
- const now = yield* Clock.currentTimeMillis;
670
- yield* Ref.update(storeRef, (store) => {
671
- const snapshot = store.tasks.get(taskId);
672
- if (!snapshot) return store;
673
- const nextTasks = new Map(store.tasks);
674
- if (snapshot.transient) {
675
- nextTasks.delete(taskId);
676
- return {
677
- tasks: nextTasks,
678
- renderOrder: removeFromRenderOrder(store.renderOrder, taskId)
679
- };
680
- }
681
- nextTasks.set(taskId, new TaskSnapshot({
682
- id: snapshot.id,
683
- parentId: snapshot.parentId,
684
- description: snapshot.description,
685
- status: "failed",
686
- transient: snapshot.transient,
687
- units: snapshot.units,
688
- startedAt: snapshot.startedAt,
689
- completedAt: now
690
- }));
691
- return {
692
- tasks: nextTasks,
693
- renderOrder: store.renderOrder
694
- };
1463
+ return yield* store.addTask({
1464
+ ...options,
1465
+ parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : void 0
695
1466
  });
696
- yield* markDirty;
697
1467
  });
698
- const getTask = (taskId) => Ref.get(storeRef).pipe(Effect.map((store) => Option.fromNullable(store.tasks.get(taskId))));
699
- const listTasks = Ref.get(storeRef).pipe(Effect.map((store) => Array.from(store.tasks.values())));
1468
+ const updateTask = store.updateTask;
1469
+ const advanceTask = store.addSuccess;
1470
+ const advanceTaskFailed = store.addFailure;
1471
+ const completeTask = store.completeTask;
1472
+ const failTask = store.failTask;
1473
+ const getTask = store.getTask;
1474
+ const listTasks = store.listTasks;
700
1475
  const runTask = dual(2, (effect, options) => Effect.gen(function* () {
701
1476
  const inheritedParentId = yield* FiberRef.get(currentParentRef);
702
1477
  const resolvedParentId = options.parentId === void 0 ? inheritedParentId : Option.some(options.parentId);
@@ -711,6 +1486,7 @@ const makeProgressService = Effect.gen(function* () {
711
1486
  addTask,
712
1487
  updateTask,
713
1488
  advanceTask,
1489
+ advanceTaskFailed,
714
1490
  completeTask,
715
1491
  failTask,
716
1492
  log,
@@ -732,11 +1508,11 @@ const makeProgressService = Effect.gen(function* () {
732
1508
  });
733
1509
  var Progress = class Progress extends Context.Tag("stromseng.dev/effective-progress/Progress")() {
734
1510
  static Default = Layer.unwrapEffect(Effect.gen(function* () {
735
- const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
1511
+ const stdioOption = yield* Effect.serviceOption(ProgressStdio);
736
1512
  const inkRendererOption = yield* Effect.serviceOption(InkRenderer);
737
1513
  let layer = Layer.scoped(Progress, makeProgressService);
738
1514
  if (Option.isNone(inkRendererOption)) layer = layer.pipe(Layer.provide(InkRenderer.Default));
739
- if (Option.isNone(terminalOption)) layer = layer.pipe(Layer.provide(ProgressTerminal.Default));
1515
+ if (Option.isNone(stdioOption)) layer = layer.pipe(Layer.provide(ProgressStdio.Default));
740
1516
  return layer;
741
1517
  }));
742
1518
  };
@@ -765,11 +1541,29 @@ const task = dual(2, (effect, options) => {
765
1541
  });
766
1542
  const wrapEffects = (effects, tap) => Array.isArray(effects) ? effects.map(tap) : Object.fromEntries(Object.entries(effects).map(([k, effect]) => [k, tap(effect)]));
767
1543
  const countEffects = (effects) => Array.isArray(effects) ? effects.length : Object.keys(effects).length;
1544
+ const isCollectAllMode = (mode) => mode === "either" || mode === "validate";
1545
+ const allCountDisplay = (mode) => isCollectAllMode(mode) ? "detailed" : "processedOnly";
1546
+ const wrapTrackedEffect = (progress, taskId, effect) => Effect.gen(function* () {
1547
+ const exit = yield* Effect.exit(effect);
1548
+ if (Exit.isSuccess(exit)) {
1549
+ yield* progress.advanceTask(taskId, 1);
1550
+ return exit.value;
1551
+ }
1552
+ if (Cause.isInterruptedOnly(exit.cause)) return yield* Effect.failCause(exit.cause);
1553
+ yield* progress.advanceTaskFailed(taskId, 1);
1554
+ return yield* Effect.failCause(exit.cause);
1555
+ });
1556
+ const isTaskFullyProcessed = (progress, taskId) => Effect.gen(function* () {
1557
+ const taskOption = yield* progress.getTask(taskId);
1558
+ if (Option.isNone(taskOption) || taskOption.value.units._tag !== "DeterminateTaskUnits") return false;
1559
+ const { processed, total } = taskOption.value.units;
1560
+ return processed >= total;
1561
+ });
768
1562
  const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* () {
769
1563
  const progress = yield* Progress;
770
1564
  return yield* progress.runTask(Effect.gen(function* () {
771
1565
  const taskId = yield* Task;
772
- const exit = yield* Effect.exit(Effect.all(wrapEffects(effects, (effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))), {
1566
+ const exit = yield* Effect.exit(Effect.all(wrapEffects(effects, (effect) => wrapTrackedEffect(progress, taskId, effect)), {
773
1567
  concurrency: options.concurrency,
774
1568
  batching: options.batching,
775
1569
  discard: options.discard,
@@ -777,6 +1571,8 @@ const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* (
777
1571
  concurrentFinalizers: options.concurrentFinalizers
778
1572
  }));
779
1573
  if (Exit.isSuccess(exit)) yield* progress.completeTask(taskId);
1574
+ else if (!isCollectAllMode(options.mode)) yield* progress.failTask(taskId);
1575
+ else if (yield* isTaskFullyProcessed(progress, taskId)) yield* progress.completeTask(taskId);
780
1576
  else yield* progress.failTask(taskId);
781
1577
  return yield* Exit.match(exit, {
782
1578
  onFailure: Effect.failCause,
@@ -785,14 +1581,15 @@ const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* (
785
1581
  }), {
786
1582
  description: options.description,
787
1583
  total: countEffects(effects),
788
- transient: options.transient
1584
+ transient: options.transient,
1585
+ countDisplay: allCountDisplay(options.mode)
789
1586
  });
790
1587
  })));
791
1588
  const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(function* () {
792
1589
  const progress = yield* Progress;
793
1590
  return yield* progress.runTask(Effect.gen(function* () {
794
1591
  const taskId = yield* Task;
795
- const exit = yield* Effect.exit(Effect.forEach(iterable, (item, index) => Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)), {
1592
+ const exit = yield* Effect.exit(Effect.forEach(iterable, (item, index) => wrapTrackedEffect(progress, taskId, f(item, index)), {
796
1593
  concurrency: options.concurrency,
797
1594
  batching: options.batching,
798
1595
  discard: options.discard,
@@ -807,9 +1604,10 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
807
1604
  }), {
808
1605
  description: options.description,
809
1606
  total: options.total ?? inferTotal(iterable),
810
- transient: options.transient
1607
+ transient: options.transient,
1608
+ countDisplay: "processedOnly"
811
1609
  });
812
1610
  })));
813
1611
 
814
1612
  //#endregion
815
- export { DeterminateTaskUnits, IndeterminateTaskUnits, Progress, ProgressTaskEventSchema, ProgressTerminal, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1613
+ export { DeterminateTaskUnits, IndeterminateTaskUnits, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };