effective-progress 0.9.0 → 0.11.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,11 @@
1
- import { Brand, Cause, Clock, Context, Data, Effect, Exit, FiberRef, Hash, Layer, Option, Schema } from "effect";
1
+ import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
2
+ import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Schema } from "effect";
2
3
  import { dual } from "effect/Function";
3
- import { Box, Text, render } from "ink";
4
- import { useEffect, useRef, useState, useSyncExternalStore } from "react";
5
- import stringWidth from "fast-string-width";
4
+ import { Box, Text, render, useBoxMetrics } from "ink";
5
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import cliSpinners from "cli-spinners";
8
+ import stringWidth from "fast-string-width";
7
9
 
8
10
  //#region src/types.ts
9
11
  const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
@@ -16,7 +18,7 @@ const TaskUnitsSchema = Schema.Struct({
16
18
  processed: Schema.Number,
17
19
  total: Schema.optional(Schema.Number)
18
20
  });
19
- var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
21
+ const TaskSnapshotSchema = Schema.Struct({
20
22
  id: TaskIdSchema,
21
23
  parentId: Schema.NullOr(TaskIdSchema),
22
24
  description: Schema.String,
@@ -25,8 +27,10 @@ var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
25
27
  transient: Schema.Boolean,
26
28
  units: TaskUnitsSchema,
27
29
  startedAt: Schema.Number,
28
- completedAt: Schema.NullOr(Schema.Number)
29
- }) {};
30
+ completedAt: Schema.NullOr(Schema.Number),
31
+ metadata: Schema.Unknown
32
+ });
33
+ const TaskSnapshot = (snapshot) => snapshot;
30
34
  var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
31
35
  var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
32
36
  taskId: TaskIdSchema,
@@ -58,60 +62,7 @@ const ProgressTaskEventSchema = Schema.Union(TaskAddedEvent, TaskUpdatedEvent, T
58
62
  const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
59
63
 
60
64
  //#endregion
61
- //#region src/ink-renderer/snapshot/render-snapshot.ts
62
- const orderedVisibleTasks = (store) => store.renderOrder.flatMap((row) => {
63
- const snapshot = store.tasks.get(row.id);
64
- if (!snapshot || snapshot.transient && snapshot.status !== "running") return [];
65
- return [{
66
- snapshot,
67
- depth: row.depth
68
- }];
69
- });
70
- const computeTreeInfo = (ordered) => {
71
- const hasNextSiblingByIndex = Array.from({ length: ordered.length }, () => false);
72
- for (let i = 0; i < ordered.length; i++) {
73
- const depth = ordered[i].depth;
74
- for (let j = i + 1; j < ordered.length; j++) {
75
- const candidateDepth = ordered[j].depth;
76
- if (candidateDepth < depth) break;
77
- if (candidateDepth === depth) {
78
- hasNextSiblingByIndex[i] = true;
79
- break;
80
- }
81
- }
82
- }
83
- const ancestorStateByDepth = [];
84
- return ordered.map((entry, index) => {
85
- const depth = entry.depth;
86
- ancestorStateByDepth.length = depth;
87
- const hasChildren = index + 1 < ordered.length && ordered[index + 1] !== void 0 && ordered[index + 1].depth > depth;
88
- const tree = {
89
- depth,
90
- hasNextSibling: hasNextSiblingByIndex[index] ?? false,
91
- hasChildren,
92
- ancestorHasNextSibling: [...ancestorStateByDepth]
93
- };
94
- ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
95
- return {
96
- ...entry,
97
- tree
98
- };
99
- });
100
- };
101
- const toRenderSnapshot = (store) => {
102
- const visibleTasks = orderedVisibleTasks(store);
103
- const hasRunningTasks = visibleTasks.some((entry) => entry.snapshot.status === "running");
104
- return {
105
- rows: computeTreeInfo(visibleTasks).map((entry) => ({
106
- task: entry.snapshot,
107
- tree: entry.tree
108
- })),
109
- hasRunningTasks
110
- };
111
- };
112
-
113
- //#endregion
114
- //#region src/ink-renderer/store.ts
65
+ //#region src/renderer/store.ts
115
66
  const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
116
67
  const sanitizeTotalOnAdd = (total) => {
117
68
  if (total === void 0) return;
@@ -142,7 +93,7 @@ const updatedSnapshot = (snapshot, options) => {
142
93
  failed: options.failed ?? currentUnits.failed,
143
94
  total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(options.total) : currentUnits.total
144
95
  });
145
- return new TaskSnapshot({
96
+ return TaskSnapshot({
146
97
  id: snapshot.id,
147
98
  parentId: snapshot.parentId,
148
99
  description: options.description ?? snapshot.description,
@@ -151,10 +102,11 @@ const updatedSnapshot = (snapshot, options) => {
151
102
  transient: options.transient ?? snapshot.transient,
152
103
  units,
153
104
  startedAt: snapshot.startedAt,
154
- completedAt: snapshot.completedAt
105
+ completedAt: snapshot.completedAt,
106
+ metadata: snapshot.metadata
155
107
  });
156
108
  };
157
- const withTransient = (snapshot, transient) => new TaskSnapshot({
109
+ const withTransient = (snapshot, transient) => TaskSnapshot({
158
110
  id: snapshot.id,
159
111
  parentId: snapshot.parentId,
160
112
  description: snapshot.description,
@@ -163,7 +115,8 @@ const withTransient = (snapshot, transient) => new TaskSnapshot({
163
115
  transient,
164
116
  units: snapshot.units,
165
117
  startedAt: snapshot.startedAt,
166
- completedAt: snapshot.completedAt
118
+ completedAt: snapshot.completedAt,
119
+ metadata: snapshot.metadata
167
120
  });
168
121
  const findInsertionIndex = (renderOrder, parentId) => {
169
122
  if (parentId === null) return {
@@ -193,14 +146,27 @@ const removeFromRenderOrder = (renderOrder, taskId) => {
193
146
  next.splice(idx, end - idx);
194
147
  return next;
195
148
  };
196
- const SNAPSHOT_PUBLISH_INTERVAL_MILLIS = 50;
149
+ const subtreeTaskIds = (renderOrder, taskId) => {
150
+ const idx = renderOrder.findIndex((row) => row.id === taskId);
151
+ if (idx === -1) return [];
152
+ const taskDepth = renderOrder[idx].depth;
153
+ let end = idx + 1;
154
+ while (end < renderOrder.length && renderOrder[end].depth > taskDepth) end++;
155
+ return renderOrder.slice(idx, end).map((row) => row.id);
156
+ };
157
+ const SNAPSHOT_PUBLISH_INTERVAL_MILLIS = 100;
197
158
  const makeProgressRenderStore = () => {
198
159
  let nextTaskId = 0;
199
160
  let state = {
200
161
  tasks: /* @__PURE__ */ new Map(),
201
- renderOrder: []
162
+ renderOrder: [],
163
+ columns: /* @__PURE__ */ new Map()
164
+ };
165
+ let pendingEvents = [];
166
+ let publishedPublication = {
167
+ snapshot: state,
168
+ events: []
202
169
  };
203
- let publishedSnapshot = toRenderSnapshot(state);
204
170
  let hasPendingPublish = false;
205
171
  let lastPublishAt = 0;
206
172
  let publishTimeout;
@@ -209,10 +175,15 @@ const makeProgressRenderStore = () => {
209
175
  for (const listener of listeners) listener();
210
176
  };
211
177
  const publishNow = () => {
178
+ const nextPublication = {
179
+ snapshot: state,
180
+ events: [...pendingEvents]
181
+ };
212
182
  hasPendingPublish = false;
213
- lastPublishAt = Date.now();
214
- publishedSnapshot = toRenderSnapshot(state);
183
+ publishedPublication = nextPublication;
215
184
  notifyListeners();
185
+ pendingEvents = [];
186
+ lastPublishAt = Date.now();
216
187
  };
217
188
  const clearScheduledPublish = () => {
218
189
  if (publishTimeout === void 0) return;
@@ -235,9 +206,10 @@ const makeProgressRenderStore = () => {
235
206
  publishNow();
236
207
  }, waitMillis);
237
208
  };
238
- const publish = (nextState) => {
239
- if (nextState === state) return;
240
- state = nextState;
209
+ const publish = (update) => {
210
+ if (update.state === state) return;
211
+ state = update.state;
212
+ if (update.events.length > 0) pendingEvents.push(...update.events);
241
213
  hasPendingPublish = true;
242
214
  schedulePublish();
243
215
  };
@@ -245,7 +217,7 @@ const makeProgressRenderStore = () => {
245
217
  publish(transform(state));
246
218
  };
247
219
  return {
248
- getSnapshot: () => publishedSnapshot,
220
+ getSnapshot: () => publishedPublication,
249
221
  subscribe: (listener) => {
250
222
  listeners.add(listener);
251
223
  return () => {
@@ -268,7 +240,7 @@ const makeProgressRenderStore = () => {
268
240
  const now = yield* Clock.currentTimeMillis;
269
241
  const parentId = options.parentId ?? null;
270
242
  const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
271
- const task = new TaskSnapshot({
243
+ const task = TaskSnapshot({
272
244
  id: taskId,
273
245
  parentId,
274
246
  description: options.description,
@@ -277,7 +249,8 @@ const makeProgressRenderStore = () => {
277
249
  transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
278
250
  units,
279
251
  startedAt: now,
280
- completedAt: null
252
+ completedAt: null,
253
+ metadata: options.metadata
281
254
  });
282
255
  updateState((current) => {
283
256
  const nextTasks = new Map(current.tasks);
@@ -289,45 +262,72 @@ const makeProgressRenderStore = () => {
289
262
  depth
290
263
  });
291
264
  return {
292
- tasks: nextTasks,
293
- renderOrder: nextRenderOrder
265
+ state: {
266
+ tasks: nextTasks,
267
+ renderOrder: nextRenderOrder,
268
+ columns: options.columns ? new Map(current.columns).set(taskId, options.columns) : current.columns
269
+ },
270
+ events: [new TaskAddedEvent({
271
+ taskId,
272
+ parentId,
273
+ description: task.description,
274
+ total: task.units.total,
275
+ transient: task.transient,
276
+ countDisplay: task.countDisplay
277
+ })]
294
278
  };
295
279
  });
296
280
  return taskId;
297
281
  }),
298
- updateTask: (taskId, options) => Effect.gen(function* () {
299
- const currentTask = state.tasks.get(taskId);
300
- if (!currentTask) return;
301
- const nextTask = updatedSnapshot(currentTask, options);
282
+ updateTask: (taskId, options) => Effect.sync(() => {
302
283
  updateState((current) => {
303
- if (!current.tasks.get(taskId)) return current;
284
+ const currentTask = current.tasks.get(taskId);
285
+ if (!currentTask) return {
286
+ state: current,
287
+ events: []
288
+ };
289
+ const nextTask = updatedSnapshot(currentTask, options);
304
290
  const nextTasks = new Map(current.tasks);
305
291
  nextTasks.set(taskId, nextTask);
306
- if (options.transient !== void 0) for (const [candidateId, candidate] of current.tasks.entries()) {
307
- if (candidateId === taskId) continue;
308
- let parentId = candidate.parentId;
309
- let isDescendant = false;
310
- while (parentId !== null) {
311
- if (parentId === taskId) {
312
- isDescendant = true;
313
- break;
314
- }
315
- parentId = current.tasks.get(parentId)?.parentId ?? null;
316
- }
317
- if (isDescendant) nextTasks.set(candidateId, withTransient(candidate, nextTask.transient));
292
+ const events = [new TaskUpdatedEvent({
293
+ taskId,
294
+ description: options.description ?? void 0,
295
+ succeeded: options.succeeded ?? void 0,
296
+ failed: options.failed ?? void 0,
297
+ processed: options.succeeded !== void 0 || options.failed !== void 0 ? nextTask.units.processed : void 0,
298
+ total: hasExplicitTotal(options) ? nextTask.units.total : void 0,
299
+ transient: options.transient ?? void 0,
300
+ countDisplay: options.countDisplay ?? void 0
301
+ })];
302
+ if (options.transient !== void 0) for (const candidateId of subtreeTaskIds(current.renderOrder, taskId).slice(1)) {
303
+ const candidate = current.tasks.get(candidateId);
304
+ if (!candidate) continue;
305
+ const nextCandidate = withTransient(candidate, nextTask.transient);
306
+ nextTasks.set(candidateId, nextCandidate);
307
+ events.push(new TaskUpdatedEvent({
308
+ taskId: candidateId,
309
+ transient: nextCandidate.transient
310
+ }));
318
311
  }
319
312
  return {
320
- tasks: nextTasks,
321
- renderOrder: current.renderOrder
313
+ state: {
314
+ tasks: nextTasks,
315
+ renderOrder: current.renderOrder,
316
+ columns: current.columns
317
+ },
318
+ events
322
319
  };
323
320
  });
324
321
  }),
325
322
  incrementSucceeded: (taskId, amount = 1) => Effect.sync(() => {
326
323
  updateState((current) => {
327
324
  const currentTask = current.tasks.get(taskId);
328
- if (!currentTask) return current;
325
+ if (!currentTask) return {
326
+ state: current,
327
+ events: []
328
+ };
329
329
  const nextTasks = new Map(current.tasks);
330
- nextTasks.set(taskId, new TaskSnapshot({
330
+ nextTasks.set(taskId, TaskSnapshot({
331
331
  id: currentTask.id,
332
332
  parentId: currentTask.parentId,
333
333
  description: currentTask.description,
@@ -340,20 +340,32 @@ const makeProgressRenderStore = () => {
340
340
  total: currentTask.units.total
341
341
  }),
342
342
  startedAt: currentTask.startedAt,
343
- completedAt: currentTask.completedAt
343
+ completedAt: currentTask.completedAt,
344
+ metadata: currentTask.metadata
344
345
  }));
345
346
  return {
346
- tasks: nextTasks,
347
- renderOrder: current.renderOrder
347
+ state: {
348
+ tasks: nextTasks,
349
+ renderOrder: current.renderOrder,
350
+ columns: current.columns
351
+ },
352
+ events: [new TaskAdvancedEvent({
353
+ taskId,
354
+ amount,
355
+ kind: "succeeded"
356
+ })]
348
357
  };
349
358
  });
350
359
  }),
351
360
  incrementFailed: (taskId, amount = 1) => Effect.sync(() => {
352
361
  updateState((current) => {
353
362
  const currentTask = current.tasks.get(taskId);
354
- if (!currentTask) return current;
363
+ if (!currentTask) return {
364
+ state: current,
365
+ events: []
366
+ };
355
367
  const nextTasks = new Map(current.tasks);
356
- nextTasks.set(taskId, new TaskSnapshot({
368
+ nextTasks.set(taskId, TaskSnapshot({
357
369
  id: currentTask.id,
358
370
  parentId: currentTask.parentId,
359
371
  description: currentTask.description,
@@ -366,11 +378,20 @@ const makeProgressRenderStore = () => {
366
378
  total: currentTask.units.total
367
379
  }),
368
380
  startedAt: currentTask.startedAt,
369
- completedAt: currentTask.completedAt
381
+ completedAt: currentTask.completedAt,
382
+ metadata: currentTask.metadata
370
383
  }));
371
384
  return {
372
- tasks: nextTasks,
373
- renderOrder: current.renderOrder
385
+ state: {
386
+ tasks: nextTasks,
387
+ renderOrder: current.renderOrder,
388
+ columns: current.columns
389
+ },
390
+ events: [new TaskAdvancedEvent({
391
+ taskId,
392
+ amount,
393
+ kind: "failed"
394
+ })]
374
395
  };
375
396
  });
376
397
  }),
@@ -378,16 +399,30 @@ const makeProgressRenderStore = () => {
378
399
  const now = yield* Clock.currentTimeMillis;
379
400
  updateState((current) => {
380
401
  const currentTask = current.tasks.get(taskId);
381
- if (!currentTask) return current;
402
+ if (!currentTask) return {
403
+ state: current,
404
+ events: []
405
+ };
406
+ if (currentTask.status !== "running") return {
407
+ state: current,
408
+ events: []
409
+ };
382
410
  const nextTasks = new Map(current.tasks);
383
411
  if (currentTask.transient) {
384
- nextTasks.delete(taskId);
412
+ const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
413
+ for (const removedTaskId of removedTaskIds) nextTasks.delete(removedTaskId);
414
+ const nextColumns = new Map(current.columns);
415
+ for (const removedTaskId of removedTaskIds) nextColumns.delete(removedTaskId);
385
416
  return {
386
- tasks: nextTasks,
387
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
417
+ state: {
418
+ tasks: nextTasks,
419
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId),
420
+ columns: nextColumns
421
+ },
422
+ events: [new TaskCompletedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
388
423
  };
389
424
  }
390
- nextTasks.set(taskId, new TaskSnapshot({
425
+ nextTasks.set(taskId, TaskSnapshot({
391
426
  id: currentTask.id,
392
427
  parentId: currentTask.parentId,
393
428
  description: currentTask.description,
@@ -404,11 +439,16 @@ const makeProgressRenderStore = () => {
404
439
  total: currentTask.units.processed
405
440
  }) : currentTask.units,
406
441
  startedAt: currentTask.startedAt,
407
- completedAt: now
442
+ completedAt: now,
443
+ metadata: currentTask.metadata
408
444
  }));
409
445
  return {
410
- tasks: nextTasks,
411
- renderOrder: current.renderOrder
446
+ state: {
447
+ tasks: nextTasks,
448
+ renderOrder: current.renderOrder,
449
+ columns: current.columns
450
+ },
451
+ events: [new TaskCompletedEvent({ taskId })]
412
452
  };
413
453
  });
414
454
  }),
@@ -416,16 +456,30 @@ const makeProgressRenderStore = () => {
416
456
  const now = yield* Clock.currentTimeMillis;
417
457
  updateState((current) => {
418
458
  const currentTask = current.tasks.get(taskId);
419
- if (!currentTask) return current;
459
+ if (!currentTask) return {
460
+ state: current,
461
+ events: []
462
+ };
463
+ if (currentTask.status !== "running") return {
464
+ state: current,
465
+ events: []
466
+ };
420
467
  const nextTasks = new Map(current.tasks);
421
468
  if (currentTask.transient) {
422
- nextTasks.delete(taskId);
469
+ const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
470
+ for (const removedTaskId of removedTaskIds) nextTasks.delete(removedTaskId);
471
+ const nextColumns = new Map(current.columns);
472
+ for (const removedTaskId of removedTaskIds) nextColumns.delete(removedTaskId);
423
473
  return {
424
- tasks: nextTasks,
425
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
474
+ state: {
475
+ tasks: nextTasks,
476
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId),
477
+ columns: nextColumns
478
+ },
479
+ events: [new TaskFailedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
426
480
  };
427
481
  }
428
- nextTasks.set(taskId, new TaskSnapshot({
482
+ nextTasks.set(taskId, TaskSnapshot({
429
483
  id: currentTask.id,
430
484
  parentId: currentTask.parentId,
431
485
  description: currentTask.description,
@@ -434,60 +488,139 @@ const makeProgressRenderStore = () => {
434
488
  transient: currentTask.transient,
435
489
  units: currentTask.units,
436
490
  startedAt: currentTask.startedAt,
437
- completedAt: now
491
+ completedAt: now,
492
+ metadata: currentTask.metadata
438
493
  }));
439
494
  return {
440
- tasks: nextTasks,
441
- renderOrder: current.renderOrder
495
+ state: {
496
+ tasks: nextTasks,
497
+ renderOrder: current.renderOrder,
498
+ columns: current.columns
499
+ },
500
+ events: [new TaskFailedEvent({ taskId })]
442
501
  };
443
502
  });
444
503
  }),
445
504
  getTask: (taskId) => Effect.sync(() => Option.fromNullable(state.tasks.get(taskId))),
446
- listTasks: Effect.sync(() => Array.from(state.tasks.values()))
505
+ listTasks: Effect.sync(() => Array.from(state.tasks.values())),
506
+ setMetadata: (taskId, metadata) => Effect.sync(() => {
507
+ updateState((current) => {
508
+ const currentTask = current.tasks.get(taskId);
509
+ if (!currentTask) return {
510
+ state: current,
511
+ events: []
512
+ };
513
+ const nextTasks = new Map(current.tasks);
514
+ nextTasks.set(taskId, TaskSnapshot({
515
+ id: currentTask.id,
516
+ parentId: currentTask.parentId,
517
+ description: currentTask.description,
518
+ status: currentTask.status,
519
+ countDisplay: currentTask.countDisplay,
520
+ transient: currentTask.transient,
521
+ units: currentTask.units,
522
+ startedAt: currentTask.startedAt,
523
+ completedAt: currentTask.completedAt,
524
+ metadata
525
+ }));
526
+ return {
527
+ state: {
528
+ tasks: nextTasks,
529
+ renderOrder: current.renderOrder,
530
+ columns: current.columns
531
+ },
532
+ events: []
533
+ };
534
+ });
535
+ }),
536
+ getMetadata: (taskId) => Effect.sync(() => {
537
+ return state.tasks.get(taskId)?.metadata;
538
+ })
447
539
  };
448
540
  };
449
541
 
450
542
  //#endregion
451
- //#region src/ink-renderer/columns/frame.ts
452
- const createRenderFrame = (rows, now, tick, stickyWidths) => {
453
- const tasks = /* @__PURE__ */ new Map();
454
- const trees = /* @__PURE__ */ new Map();
455
- for (const row of rows) {
456
- tasks.set(row.task.id, row.task);
457
- trees.set(row.task.id, row.tree);
458
- }
459
- return {
460
- taskIds: rows.map((row) => row.task.id),
461
- now,
462
- tick,
463
- stickyWidths,
464
- getTask: (taskId) => {
465
- const task = tasks.get(taskId);
466
- if (task === void 0) throw new Error(`Unknown task id: ${taskId}`);
467
- return task;
468
- },
469
- getTree: (taskId) => {
470
- const tree = trees.get(taskId);
471
- if (tree === void 0) throw new Error(`Unknown task tree: ${taskId}`);
472
- return tree;
473
- }
474
- };
543
+ //#region src/renderer/hooks/use-now-clock.ts
544
+ const useNowClock = (active, intervalMillis) => {
545
+ const [now, setNow] = useState(() => Date.now());
546
+ useEffect(() => {
547
+ if (!active) return;
548
+ setNow(Date.now());
549
+ const interval = setInterval(() => {
550
+ setNow(Date.now());
551
+ }, intervalMillis);
552
+ return () => {
553
+ clearInterval(interval);
554
+ };
555
+ }, [active, intervalMillis]);
556
+ return now;
475
557
  };
476
558
 
477
559
  //#endregion
478
- //#region src/ink-renderer/format.ts
479
- const SPINNER_FRAMES = [
480
- "⠋",
481
- "⠙",
482
- "⠹",
483
- "⠸",
484
- "⠼",
485
- "⠴",
486
- "⠦",
487
- "⠧",
488
- "⠇",
489
- "⠏"
490
- ];
560
+ //#region src/renderer/context/now-context.tsx
561
+ const NOW_INTERVAL_MILLIS = 1e3;
562
+ const NowContext = createContext(Date.now());
563
+ const NowProvider = ({ active, children, nowOverride }) => {
564
+ const liveNow = useNowClock(active, NOW_INTERVAL_MILLIS);
565
+ const now = nowOverride ?? liveNow;
566
+ return /* @__PURE__ */ jsx(NowContext.Provider, {
567
+ value: now,
568
+ children
569
+ });
570
+ };
571
+ const useNow = () => useContext(NowContext);
572
+
573
+ //#endregion
574
+ //#region src/renderer/hooks/use-spinner-clock.ts
575
+ const normalizeIntervalMillis = (intervalMillis) => Math.max(1, intervalMillis);
576
+ const getSpinnerTickAtTime = (baseTick, startedAt, now, intervalMillis) => {
577
+ const elapsedMillis = Math.max(0, now - startedAt);
578
+ return baseTick + Math.floor(elapsedMillis / normalizeIntervalMillis(intervalMillis));
579
+ };
580
+ const useSpinnerClock = (active, intervalMillis) => {
581
+ const [tick, setTick] = useState(0);
582
+ const tickRef = useRef(tick);
583
+ useEffect(() => {
584
+ tickRef.current = tick;
585
+ }, [tick]);
586
+ useEffect(() => {
587
+ if (!active) return;
588
+ const baseTick = tickRef.current;
589
+ const startedAt = Date.now();
590
+ const updateTick = () => {
591
+ setTick(getSpinnerTickAtTime(baseTick, startedAt, Date.now(), intervalMillis));
592
+ };
593
+ updateTick();
594
+ const interval = setInterval(() => {
595
+ updateTick();
596
+ }, normalizeIntervalMillis(intervalMillis));
597
+ return () => {
598
+ clearInterval(interval);
599
+ };
600
+ }, [active, intervalMillis]);
601
+ return tick;
602
+ };
603
+
604
+ //#endregion
605
+ //#region src/renderer/context/spinner-context.tsx
606
+ const DEFAULT_SPINNER_INTERVAL_MILLIS = cliSpinners.dots.interval;
607
+ const SpinnerContext = createContext(0);
608
+ const SpinnerProvider = ({ active, children, intervalMillis = DEFAULT_SPINNER_INTERVAL_MILLIS, tickOverride }) => {
609
+ const liveTick = useSpinnerClock(active, intervalMillis);
610
+ const tick = tickOverride ?? liveTick;
611
+ return /* @__PURE__ */ jsx(SpinnerContext.Provider, {
612
+ value: tick,
613
+ children
614
+ });
615
+ };
616
+ const useSpinnerTick = () => useContext(SpinnerContext);
617
+
618
+ //#endregion
619
+ //#region src/renderer/shared/determinate.ts
620
+ const isDeterminate$2 = (task) => task.units.total !== void 0;
621
+
622
+ //#endregion
623
+ //#region src/renderer/shared/format.ts
491
624
  const isDeterminate$1 = (task) => task.units.total !== void 0;
492
625
  const showsUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
493
626
  const formatDurationSeconds = (seconds) => {
@@ -513,37 +646,6 @@ const formatEta = (task, now) => {
513
646
  const elapsedMillis = Math.max(1, now - task.startedAt);
514
647
  return formatDurationSeconds(Math.max(0, Math.floor(elapsedMillis / processed * remaining)) / 1e3);
515
648
  };
516
- const getTaskIndicator = (task, tick) => {
517
- if (task.status === "running") return {
518
- symbol: SPINNER_FRAMES[tick % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0],
519
- color: "yellow"
520
- };
521
- if (task.status === "failed") return {
522
- symbol: "✗",
523
- color: "red"
524
- };
525
- if (!isDeterminate$1(task)) return {
526
- symbol: "✓",
527
- color: "green"
528
- };
529
- const { succeeded, failed, processed, total } = task.units;
530
- if (failed === 0 && processed === total) return {
531
- symbol: "✓",
532
- color: "green"
533
- };
534
- if (failed > 0 && succeeded > 0) return {
535
- symbol: "~",
536
- color: "yellow"
537
- };
538
- if (failed > 0 && succeeded === 0) return {
539
- symbol: "✗",
540
- color: "red"
541
- };
542
- return {
543
- symbol: "✓",
544
- color: "green"
545
- };
546
- };
547
649
  const formatDeterminateAmountParts = (task) => {
548
650
  if (!isDeterminate$1(task)) return;
549
651
  const totalText = `${task.units.total}`;
@@ -556,16 +658,6 @@ const formatDeterminateAmountParts = (task) => {
556
658
  total: totalText
557
659
  };
558
660
  };
559
- const getDeterminateProcessedColor = (task) => {
560
- if (!isDeterminate$1(task)) return "whiteBright";
561
- const { succeeded, failed, processed, total } = task.units;
562
- if (task.status === "failed" && processed < total) return "red";
563
- if (processed >= total && failed === 0) return "green";
564
- if (processed >= total && failed > 0 && succeeded === 0) return "red";
565
- if (succeeded > 0 && failed > 0) return "yellow";
566
- if (failed > 0 && succeeded === 0) return "red";
567
- return "whiteBright";
568
- };
569
661
  const formatAmount = (task, _tick) => {
570
662
  if (isDeterminate$1(task)) {
571
663
  const parts = formatDeterminateAmountParts(task);
@@ -582,50 +674,8 @@ const formatAmount = (task, _tick) => {
582
674
  };
583
675
 
584
676
  //#endregion
585
- //#region src/ink-renderer/columns/node.ts
586
- const createColumnDefinition = (config, create) => {
587
- return {
588
- id: Hash.hash(Data.struct(config)).toString(36),
589
- build: (frame) => create(frame, config)
590
- };
591
- };
592
-
593
- //#endregion
594
- //#region src/ink-renderer/columns/sticky-width.ts
595
- const applyStickyWidth = ({ key, measure, stickyWidths }) => {
596
- const preferred = Math.max(measure.preferred, stickyWidths.get(key) ?? 0);
597
- const max = measure.max === void 0 ? void 0 : Math.max(measure.max, preferred);
598
- return {
599
- ...measure,
600
- preferred,
601
- max
602
- };
603
- };
604
- const commitStickyWidth = ({ key, measure, stickyWidths }) => {
605
- stickyWidths.set(key, measure.preferred);
606
- };
607
- const createStickyColumn = ({ frame, measure: baseMeasure, render, stickyKey }) => {
608
- const measure = stickyKey === void 0 ? baseMeasure : applyStickyWidth({
609
- key: stickyKey,
610
- measure: baseMeasure,
611
- stickyWidths: frame.stickyWidths
612
- });
613
- return {
614
- measure,
615
- commitStickyWidth: stickyKey === void 0 ? void 0 : () => {
616
- commitStickyWidth({
617
- key: stickyKey,
618
- measure,
619
- stickyWidths: frame.stickyWidths
620
- });
621
- },
622
- render
623
- };
624
- };
625
-
626
- //#endregion
627
- //#region src/ink-renderer/columns/text-width.ts
628
- const WIDTH_CACHE_LIMIT = 4096;
677
+ //#region src/renderer/shared/text-width.ts
678
+ const WIDTH_CACHE_LIMIT = 8192;
629
679
  const widthCache = /* @__PURE__ */ new Map();
630
680
  const textWidth = (text) => {
631
681
  const cached = widthCache.get(text);
@@ -637,725 +687,526 @@ const textWidth = (text) => {
637
687
  };
638
688
 
639
689
  //#endregion
640
- //#region src/ink-renderer/columns/description-column.tsx
641
- const MIN_PLAIN_DESCRIPTION_WIDTH = 8;
642
- const MIN_COMPACT_DESCRIPTION_WIDTH = 3;
643
- const MIN_SPINNER_WIDTH = 1;
644
- const MIN_TREE_DESCRIPTION_TEXT_WIDTH = 6;
645
- const DESCRIPTION_TREE_STICKY_KEY = Symbol("description.tree");
646
- const DESCRIPTION_PLAIN_STICKY_KEY = Symbol("description.plain");
647
- const DESCRIPTION_COMPACT_STICKY_KEY = Symbol("description.compact");
648
- const treeAncestorPrefix = (tree) => tree.ancestorHasNextSibling.slice(1).map((hasNext) => hasNext ? "│ " : " ").join("");
649
- const renderTreePrefix = (tree) => {
650
- if (tree.depth <= 0) return "";
651
- return `${treeAncestorPrefix(tree)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
690
+ //#region src/renderer/columns/amount-column.tsx
691
+ const hasUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
692
+ const hasCountedAmount = (task) => isDeterminate$2(task) || hasUnknownTotalCounts(task);
693
+ const totalTextFor = (task) => task.units.total === void 0 ? "?" : `${task.units.total}`;
694
+ const emptyAmountLayout = {
695
+ hasDetailedRows: false,
696
+ countWidth: 0,
697
+ processedWidth: 0,
698
+ totalWidth: 0,
699
+ preferredWidth: 0
652
700
  };
653
- const maxDescriptionWidth = (frame, showTree) => frame.taskIds.reduce((max, taskId) => {
654
- const treePrefix = showTree ? renderTreePrefix(frame.getTree(taskId)) : "";
655
- return Math.max(max, textWidth(`${treePrefix}${frame.getTask(taskId).description}`) + 2);
656
- }, MIN_PLAIN_DESCRIPTION_WIDTH);
657
- const minTreeDescriptionWidth = (frame) => frame.taskIds.reduce((max, taskId) => {
658
- const treePrefixWidth = textWidth(renderTreePrefix(frame.getTree(taskId)));
659
- return Math.max(max, treePrefixWidth + 2 + MIN_TREE_DESCRIPTION_TEXT_WIDTH);
660
- }, MIN_PLAIN_DESCRIPTION_WIDTH);
661
- const DescriptionColumn = (frame, config = {}) => {
662
- const variant = config.variant ?? "plain";
663
- const showTree = variant === "tree";
664
- const stickyKey = variant === "tree" ? DESCRIPTION_TREE_STICKY_KEY : variant === "plain" ? DESCRIPTION_PLAIN_STICKY_KEY : variant === "compact" ? DESCRIPTION_COMPACT_STICKY_KEY : void 0;
665
- const min = variant === "spinner" ? MIN_SPINNER_WIDTH : variant === "compact" ? MIN_COMPACT_DESCRIPTION_WIDTH : variant === "tree" ? minTreeDescriptionWidth(frame) : MIN_PLAIN_DESCRIPTION_WIDTH;
666
- const preferred = variant === "spinner" ? MIN_SPINNER_WIDTH : Math.max(min, maxDescriptionWidth(frame, showTree));
667
- return createStickyColumn({
668
- frame,
669
- measure: {
670
- min,
671
- preferred,
672
- max: preferred
673
- },
674
- stickyKey,
675
- render: (taskId) => {
676
- const task = frame.getTask(taskId);
677
- const tree = frame.getTree(taskId);
678
- const treePrefix = showTree ? renderTreePrefix(tree) : "";
679
- const indicator = getTaskIndicator(task, frame.tick);
680
- if (variant === "spinner") return /* @__PURE__ */ jsx(Text, {
681
- color: indicator.color,
682
- children: indicator.symbol
683
- });
684
- return /* @__PURE__ */ jsxs(Text, {
685
- wrap: "truncate-end",
686
- children: [
687
- treePrefix,
688
- /* @__PURE__ */ jsx(Text, {
689
- color: indicator.color,
690
- children: indicator.symbol
691
- }),
692
- ` ${task.description}`
693
- ]
694
- });
695
- }
696
- });
701
+ const measureAmountLayout = (rows) => {
702
+ const countedTasks = rows.flatMap((row) => hasCountedAmount(row.task) ? [row.task] : []);
703
+ const hasDetailedRows = countedTasks.some((task) => task.countDisplay === "detailed");
704
+ if (countedTasks.length === 0) {
705
+ const preferredWidth = rows.reduce((max, row) => Math.max(max, textWidth(formatAmount(row.task, 0))), 0);
706
+ return {
707
+ ...emptyAmountLayout,
708
+ preferredWidth
709
+ };
710
+ }
711
+ const processedWidth = countedTasks.reduce((max, task) => Math.max(max, `${task.units.processed}`.length), 1);
712
+ const totalWidth = countedTasks.reduce((max, task) => Math.max(max, totalTextFor(task).length), 1);
713
+ const countWidth = hasDetailedRows ? countedTasks.reduce((max, task) => Math.max(max, processedWidth, totalWidth, `${task.units.succeeded}`.length, `${task.units.failed}`.length), 1) : 0;
714
+ const countedWidth = (hasDetailedRows ? countWidth + 1 + countWidth + 1 : 0) + processedWidth + 1 + totalWidth;
715
+ return {
716
+ hasDetailedRows,
717
+ countWidth,
718
+ processedWidth,
719
+ totalWidth,
720
+ preferredWidth: rows.reduce((max, row) => {
721
+ if (hasCountedAmount(row.task)) return Math.max(max, countedWidth);
722
+ return Math.max(max, textWidth(formatAmount(row.task, 0)));
723
+ }, countedWidth)
724
+ };
697
725
  };
698
- const createDescriptionRootColumn = (variant) => createColumnDefinition({
699
- _tag: "description",
700
- variant
701
- }, (frame, config) => DescriptionColumn(frame, config));
702
- const DescriptionTreeRootColumn = createDescriptionRootColumn("tree");
703
- const DescriptionPlainRootColumn = createDescriptionRootColumn("plain");
704
- const DescriptionCompactRootColumn = createDescriptionRootColumn("compact");
705
- const DescriptionSpinnerRootColumn = createDescriptionRootColumn("spinner");
726
+ const renderAmount = (task, layout) => {
727
+ if (!hasCountedAmount(task)) return formatAmount(task, 0);
728
+ const processed = `${task.units.processed}`.padStart(layout.processedWidth, " ");
729
+ const total = totalTextFor(task).padStart(layout.totalWidth, " ");
730
+ if (!layout.hasDetailedRows) return `${processed}/${total}`;
731
+ if (task.countDisplay !== "detailed") return `${" ".repeat(layout.countWidth)} ${" ".repeat(layout.countWidth)} ${processed}/${total}`;
732
+ const succeeded = `${task.units.succeeded}`.padStart(layout.countWidth, " ");
733
+ const failed = `${task.units.failed}`.padStart(layout.countWidth, " ");
734
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
735
+ /* @__PURE__ */ jsx(Text, {
736
+ color: "green",
737
+ children: succeeded
738
+ }),
739
+ /* @__PURE__ */ jsx(Text, { children: ` ` }),
740
+ /* @__PURE__ */ jsx(Text, {
741
+ color: "red",
742
+ children: failed
743
+ }),
744
+ /* @__PURE__ */ jsx(Text, { children: ` ${processed}/${total}` })
745
+ ] });
746
+ };
747
+ const AmountCell = ({ task, layout }) => /* @__PURE__ */ jsx(Text, {
748
+ wrap: "truncate-end",
749
+ children: renderAmount(task, layout)
750
+ });
706
751
 
707
752
  //#endregion
708
- //#region src/ink-renderer/columns/elapsed-column.tsx
709
- const MIN_ELAPSED_WIDTH = Array.from("10s").length;
710
- const ELAPSED_STICKY_KEY = Symbol("elapsed");
711
- const maxElapsedWidth = (frame) => frame.taskIds.reduce((max, taskId) => Math.max(max, textWidth(formatElapsed(frame.getTask(taskId), frame.now))), MIN_ELAPSED_WIDTH);
712
- const ElapsedColumn = (frame) => {
713
- const elapsedContentWidth = maxElapsedWidth(frame);
714
- return createStickyColumn({
715
- frame,
716
- stickyKey: ELAPSED_STICKY_KEY,
717
- measure: {
718
- min: MIN_ELAPSED_WIDTH,
719
- preferred: elapsedContentWidth,
720
- max: elapsedContentWidth
721
- },
722
- render: (taskId, width) => {
723
- const formatted = formatElapsed(frame.getTask(taskId), frame.now);
724
- return /* @__PURE__ */ jsx(Box, {
725
- width,
726
- justifyContent: "flex-end",
727
- children: /* @__PURE__ */ jsx(Text, {
728
- color: "gray",
729
- children: textWidth(formatted) <= width ? formatted : formatted.slice(0, width)
730
- })
731
- });
732
- }
753
+ //#region src/renderer/columns/bar-column.tsx
754
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
755
+ const prepareBar = (rows) => {
756
+ return { hasDeterminateRows: rows.some((row) => row.derived.isDeterminate) };
757
+ };
758
+ const renderProgressBar = (task, width) => {
759
+ if (!isDeterminate$2(task)) return /* @__PURE__ */ jsx(Text, { children: ` `.repeat(Math.max(0, width)) });
760
+ const displayTotal = Math.max(task.units.total, task.units.succeeded + task.units.failed);
761
+ const succeededEnd = displayTotal === 0 ? width : Math.round(task.units.succeeded / displayTotal * width);
762
+ const failedEnd = displayTotal === 0 ? width : Math.round((task.units.succeeded + task.units.failed) / displayTotal * width);
763
+ const succeededLength = clamp(succeededEnd, 0, width);
764
+ const failedLength = clamp(failedEnd, succeededLength, width) - succeededLength;
765
+ const remainingLength = Math.max(0, width - succeededLength - failedLength);
766
+ return /* @__PURE__ */ jsxs(Text, {
767
+ wrap: "truncate-end",
768
+ children: [
769
+ /* @__PURE__ */ jsx(Text, {
770
+ color: "green",
771
+ children: "".repeat(succeededLength)
772
+ }),
773
+ /* @__PURE__ */ jsx(Text, {
774
+ color: "red",
775
+ children: "━".repeat(failedLength)
776
+ }),
777
+ /* @__PURE__ */ jsx(Text, {
778
+ color: "gray",
779
+ children: "─".repeat(remainingLength)
780
+ })
781
+ ]
733
782
  });
734
783
  };
735
- const ElapsedRootColumn = createColumnDefinition({ _tag: "elapsed" }, (frame) => ElapsedColumn(frame));
784
+ const BarCell = ({ task, width }) => /* @__PURE__ */ jsx(Text, {
785
+ wrap: "truncate-end",
786
+ children: renderProgressBar(task, width ?? 0)
787
+ });
736
788
 
737
789
  //#endregion
738
- //#region src/ink-renderer/columns/determinate.ts
790
+ //#region src/renderer/columns/description-column.tsx
791
+ const MIN_TREE_DESCRIPTION_TEXT_WIDTH = 6;
792
+ const DEFAULT_SPINNER_TYPE = "dots";
739
793
  const isDeterminate = (task) => task.units.total !== void 0;
740
-
741
- //#endregion
742
- //#region src/ink-renderer/columns/eta-column.tsx
743
- const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
744
- const ETA_STICKY_KEY = Symbol("eta");
745
- const etaDurationText = (task, now) => {
746
- if (task.status !== "running" || !isDeterminate(task)) return;
747
- const eta = formatEta(task, now);
748
- return eta.length > 0 ? eta : "--";
749
- };
750
- const renderEtaText = (task, now, width) => {
751
- const duration = etaDurationText(task, now);
752
- if (duration === void 0) return "";
753
- const prefixed = `ETA: ${duration}`;
754
- if (width >= textWidth(prefixed)) return prefixed;
755
- if (width >= textWidth(duration)) return duration;
756
- return primaryUnit(duration);
794
+ const getSpinnerFrame = (tick, spinnerType) => {
795
+ const frames = cliSpinners[spinnerType].frames;
796
+ return frames[(tick % frames.length + frames.length) % frames.length] ?? frames[0] ?? "";
757
797
  };
758
- const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
759
- const computeEtaMetrics = (frame) => {
760
- let hasEta = false;
761
- let prefixedWidth = 0;
762
- let durationWidth = 0;
763
- let primaryUnitWidth = 0;
764
- for (const taskId of frame.taskIds) {
765
- const duration = etaDurationText(frame.getTask(taskId), frame.now);
766
- if (duration === void 0) continue;
767
- hasEta = true;
768
- const prefixed = `ETA: ${duration}`;
769
- prefixedWidth = Math.max(prefixedWidth, textWidth(prefixed));
770
- durationWidth = Math.max(durationWidth, textWidth(duration));
771
- primaryUnitWidth = Math.max(primaryUnitWidth, textWidth(primaryUnit(duration)));
772
- }
773
- return {
774
- hasEta,
775
- prefixedWidth,
776
- durationWidth: Math.max(2, durationWidth),
777
- primaryUnitWidth: Math.max(2, primaryUnitWidth)
798
+ const getTaskIndicator = (task, tick, spinnerType = DEFAULT_SPINNER_TYPE) => {
799
+ if (task.status === "running") return {
800
+ symbol: getSpinnerFrame(tick, spinnerType),
801
+ color: "yellow"
802
+ };
803
+ if (task.status === "failed") return {
804
+ symbol: "✗",
805
+ color: "red"
806
+ };
807
+ if (!isDeterminate(task)) return {
808
+ symbol: "✓",
809
+ color: "green"
810
+ };
811
+ const { succeeded, failed, processed, total } = task.units;
812
+ if (failed === 0 && processed === total) return {
813
+ symbol: "✓",
814
+ color: "green"
815
+ };
816
+ if (failed > 0 && succeeded > 0) return {
817
+ symbol: "~",
818
+ color: "yellow"
819
+ };
820
+ if (failed > 0 && succeeded === 0) return {
821
+ symbol: "✗",
822
+ color: "red"
778
823
  };
779
- };
780
- const EtaColumn = (frame) => {
781
- const metrics = computeEtaMetrics(frame);
782
- if (!metrics.hasEta) return;
783
- return createStickyColumn({
784
- frame,
785
- stickyKey: ETA_STICKY_KEY,
786
- measure: {
787
- min: metrics.primaryUnitWidth,
788
- preferred: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR),
789
- max: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR)
790
- },
791
- render: (taskId, width) => /* @__PURE__ */ jsx(Text, {
792
- wrap: "truncate-end",
793
- color: "gray",
794
- children: renderEtaText(frame.getTask(taskId), frame.now, width)
795
- })
796
- });
797
- };
798
- const EtaRootColumn = createColumnDefinition({ _tag: "eta" }, (frame) => EtaColumn(frame));
799
-
800
- //#endregion
801
- //#region src/ink-renderer/columns/progress/shared.ts
802
- const DEFAULT_BAR_WIDTH = 30;
803
- const PERCENT_FALLBACK_WIDTH = 10;
804
- const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
805
- const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
806
- const blank = (width) => " ".repeat(Math.max(0, width));
807
- const shouldShowCountAmount = (task) => task.units.total !== void 0 || task.units.processed > 0;
808
- const shouldShowDetailedCounts = (task) => shouldShowCountAmount(task) && task.countDisplay === "detailed";
809
- const computeProgressMetrics = (frame) => {
810
- let hasStructuredCounts = false;
811
- let hasDetailed = false;
812
- let hasDeterminate = false;
813
- let countDigits = 0;
814
- let totalWidth = 0;
815
- let simpleTextWidth = 0;
816
- for (const taskId of frame.taskIds) {
817
- const task = frame.getTask(taskId);
818
- hasDeterminate ||= isDeterminate(task);
819
- if (shouldShowCountAmount(task)) {
820
- hasStructuredCounts = true;
821
- countDigits = Math.max(countDigits, textWidth(`${task.units.succeeded}`), textWidth(`${task.units.failed}`), textWidth(`${task.units.processed}`));
822
- totalWidth = Math.max(totalWidth, textWidth(isDeterminate(task) ? `${task.units.total}` : "?"));
823
- if (task.countDisplay === "detailed") hasDetailed = true;
824
- continue;
825
- }
826
- simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, frame.tick)));
827
- }
828
824
  return {
829
- hasStructuredCounts,
830
- hasDetailed,
831
- hasDeterminate,
832
- countDigits: Math.max(1, countDigits),
833
- totalWidth: Math.max(1, totalWidth),
834
- simpleTextWidth
825
+ symbol: "✓",
826
+ color: "green"
835
827
  };
836
828
  };
837
- const processedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth;
838
- const detailedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth + (metrics.hasDetailed ? metrics.countDigits + 1 + metrics.countDigits + 1 : 0);
839
- const percentText = (task) => {
840
- if (!isDeterminate(task)) return formatAmount(task, 0);
841
- if (task.units.total === 0) return "100%";
842
- const displayTotal = Math.max(task.units.total, task.units.processed);
843
- return `${Math.max(0, Math.min(100, Math.round(task.units.processed / displayTotal * 100)))}%`;
844
- };
845
-
846
- //#endregion
847
- //#region src/ink-renderer/columns/progress/amount-column.tsx
848
- const succeededCount = (task, width) => {
849
- if (width <= 0 || !shouldShowCountAmount(task) || !shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
829
+ const prepareDescription = (rows) => ({ minTreeWidth: rows.reduce((max, row) => Math.max(max, row.derived.treePrefixWidth + 2 + MIN_TREE_DESCRIPTION_TEXT_WIDTH), MIN_TREE_DESCRIPTION_TEXT_WIDTH + 2) });
830
+ const TaskIndicatorGlyph = ({ task, tick, spinnerType = DEFAULT_SPINNER_TYPE }) => {
831
+ const indicator = getTaskIndicator(task, tick, spinnerType);
850
832
  return /* @__PURE__ */ jsx(Text, {
851
- color: "green",
852
- children: padLeft(`${task.units.succeeded}`, width)
833
+ color: indicator.color,
834
+ children: indicator.symbol
853
835
  });
854
836
  };
855
- const failedCount = (task, width) => {
856
- if (width <= 0 || !shouldShowCountAmount(task) || !shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
857
- return /* @__PURE__ */ jsx(Text, {
858
- color: "red",
859
- children: padLeft(`${task.units.failed}`, width)
837
+ const DescriptionCell = ({ cell, width, minTreeWidth, spinnerTick }) => {
838
+ const showTree = width === void 0 || width >= minTreeWidth;
839
+ const treePrefix = showTree ? cell.derived.treePrefix : "";
840
+ if (!showTree && width !== void 0 && width <= 1) return /* @__PURE__ */ jsx(TaskIndicatorGlyph, {
841
+ task: cell.task,
842
+ tick: spinnerTick
860
843
  });
861
- };
862
- const processedCount = (task, width) => {
863
- if (width <= 0 || !shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
864
- return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
865
- };
866
- const totalCount = (task, width) => {
867
- if (width <= 0 || !shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
868
- return /* @__PURE__ */ jsx(Text, { children: padRight(isDeterminate(task) ? `${task.units.total}` : "?", width) });
869
- };
870
- const structuredAmount = (task, tick, width, layout) => {
871
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, {
844
+ if (!showTree && width !== void 0 && width === 2) return /* @__PURE__ */ jsxs(Text, {
872
845
  wrap: "truncate-end",
873
- children: formatAmount(task, tick)
874
- });
875
- if (layout.kind === "processed") return /* @__PURE__ */ jsxs(Box, {
876
- flexDirection: "row",
877
- width,
878
- children: [
879
- processedCount(task, layout.processedWidth),
880
- /* @__PURE__ */ jsx(Text, { children: "/" }),
881
- totalCount(task, layout.totalWidth)
882
- ]
846
+ children: [/* @__PURE__ */ jsx(TaskIndicatorGlyph, {
847
+ task: cell.task,
848
+ tick: spinnerTick
849
+ }), ""]
883
850
  });
884
- return /* @__PURE__ */ jsxs(Box, {
885
- flexDirection: "row",
886
- width,
851
+ return /* @__PURE__ */ jsxs(Text, {
852
+ wrap: "truncate-end",
887
853
  children: [
888
- layout.succeededWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [succeededCount(task, layout.succeededWidth), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
889
- layout.failedWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [failedCount(task, layout.failedWidth), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
890
- processedCount(task, layout.processedWidth),
891
- /* @__PURE__ */ jsx(Text, { children: "/" }),
892
- totalCount(task, layout.totalWidth)
854
+ treePrefix,
855
+ /* @__PURE__ */ jsx(TaskIndicatorGlyph, {
856
+ task: cell.task,
857
+ tick: spinnerTick
858
+ }),
859
+ ` ${cell.task.description}`
893
860
  ]
894
861
  });
895
862
  };
896
- const AmountColumn = (frame, config) => {
897
- const metrics = config.metrics ?? computeProgressMetrics(frame);
898
- const detailedWidth = detailedAmountWidth(metrics);
899
- const processedWidth = processedAmountWidth(metrics);
900
- const preferredWidth = metrics.hasStructuredCounts ? metrics.hasDetailed ? detailedWidth : processedWidth : Math.max(1, metrics.simpleTextWidth);
901
- const minWidth = metrics.hasStructuredCounts ? processedWidth : Math.max(1, metrics.simpleTextWidth);
902
- const summary = {
903
- hasDetailed: metrics.hasDetailed,
904
- countDigits: metrics.countDigits,
905
- totalWidth: metrics.totalWidth,
906
- detailedWidth,
907
- processedWidth,
908
- preferredWidth,
909
- minWidth,
910
- simpleTextWidth: Math.max(1, metrics.simpleTextWidth)
911
- };
912
- return createStickyColumn({
913
- frame,
914
- measure: {
915
- min: summary.minWidth,
916
- preferred: summary.preferredWidth,
917
- max: summary.preferredWidth
918
- },
919
- stickyKey: config.stickyWidth === true ? config.key : void 0,
920
- render: (taskId, width) => {
921
- const task = frame.getTask(taskId);
922
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, {
923
- wrap: "truncate-end",
924
- children: formatAmount(task, frame.tick)
925
- });
926
- if (summary.hasDetailed && width >= summary.detailedWidth) return structuredAmount(task, frame.tick, width, {
927
- kind: "detailed",
928
- succeededWidth: summary.countDigits,
929
- failedWidth: summary.countDigits,
930
- processedWidth: summary.countDigits,
931
- totalWidth: summary.totalWidth
932
- });
933
- return structuredAmount(task, frame.tick, width, {
934
- kind: "processed",
935
- processedWidth: summary.countDigits,
936
- totalWidth: summary.totalWidth
937
- });
938
- }
863
+
864
+ //#endregion
865
+ //#region src/renderer/columns/elapsed-column.tsx
866
+ const ElapsedCell = ({ task, now }) => {
867
+ return /* @__PURE__ */ jsx(Text, {
868
+ wrap: "truncate-end",
869
+ color: "gray",
870
+ children: formatElapsed(task, now)
939
871
  });
940
872
  };
941
873
 
942
874
  //#endregion
943
- //#region src/ink-renderer/columns/progress/bar-column.tsx
944
- const segmentLengths = (width, total, succeeded, failed) => {
945
- if (total === 0) return {
946
- succeeded: width,
947
- failed: 0,
948
- remaining: 0
949
- };
950
- const displayTotal = Math.max(total, succeeded + failed);
951
- const succeededEnd = Math.round(succeeded / displayTotal * width);
952
- const failedEnd = Math.round((succeeded + failed) / displayTotal * width);
953
- const succeededLength = Math.max(0, Math.min(width, succeededEnd));
954
- const failedLength = Math.max(0, Math.min(width, failedEnd) - succeededLength);
955
- return {
956
- succeeded: succeededLength,
957
- failed: failedLength,
958
- remaining: Math.max(0, width - succeededLength - failedLength)
959
- };
960
- };
961
- const BarColumn = (frame, config) => {
962
- return createStickyColumn({
963
- frame,
964
- measure: {
965
- min: 4,
966
- preferred: DEFAULT_BAR_WIDTH,
967
- max: config.fullWidth ? void 0 : DEFAULT_BAR_WIDTH
968
- },
969
- stickyKey: config.stickyWidth === true ? config.key : void 0,
970
- render: (taskId, width) => {
971
- const task = frame.getTask(taskId);
972
- if (!isDeterminate(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
973
- const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
974
- return /* @__PURE__ */ jsxs(Text, {
975
- wrap: "truncate-end",
976
- children: [
977
- /* @__PURE__ */ jsx(Text, {
978
- color: "green",
979
- children: "━".repeat(lengths.succeeded)
980
- }),
981
- /* @__PURE__ */ jsx(Text, {
982
- color: "red",
983
- children: "━".repeat(lengths.failed)
984
- }),
985
- /* @__PURE__ */ jsx(Text, {
986
- color: "gray",
987
- children: "─".repeat(lengths.remaining)
988
- })
989
- ]
990
- });
991
- }
875
+ //#region src/renderer/columns/eta-column.tsx
876
+ const EtaCell = ({ task, now }) => {
877
+ const eta = formatEta(task, now);
878
+ if (eta === "") return null;
879
+ return /* @__PURE__ */ jsx(Text, {
880
+ wrap: "truncate-end",
881
+ color: "gray",
882
+ children: `ETA: ${eta}`
992
883
  });
993
884
  };
994
885
 
995
886
  //#endregion
996
- //#region src/ink-renderer/columns/progress/percent-column.tsx
997
- const PercentColumn = (frame) => ({
998
- measure: {
999
- min: 4,
1000
- preferred: 4,
1001
- max: 4
1002
- },
1003
- render: (taskId, _width) => {
1004
- const task = frame.getTask(taskId);
1005
- const determinateColor = isDeterminate(task) ? getDeterminateProcessedColor(task) : void 0;
1006
- return /* @__PURE__ */ jsx(Text, {
1007
- wrap: "truncate-end",
1008
- color: determinateColor === "green" || determinateColor === "yellow" || determinateColor === "red" ? determinateColor : void 0,
1009
- children: percentText(task)
1010
- });
1011
- }
887
+ //#region src/columns.tsx
888
+ var columns_exports = /* @__PURE__ */ __exportAll({
889
+ amount: () => amount,
890
+ bar: () => bar,
891
+ defaults: () => defaults,
892
+ description: () => description,
893
+ elapsed: () => elapsed,
894
+ eta: () => eta,
895
+ resolveColumnSizeValue: () => resolveColumnSizeValue,
896
+ spacer: () => spacer
897
+ });
898
+ const spacer = ({ flexGrow, flexShrink, flexBasis, minWidth } = {}) => ({
899
+ render: () => null,
900
+ flexGrow,
901
+ flexShrink,
902
+ flexBasis,
903
+ minWidth
904
+ });
905
+ const description = () => ({
906
+ prepare: prepareDescription,
907
+ flexGrow: 1,
908
+ flexShrink: 1,
909
+ minWidth: 1,
910
+ render: (cell, ctx) => /* @__PURE__ */ jsx(DescriptionCell, {
911
+ cell,
912
+ width: ctx.width,
913
+ minTreeWidth: ctx.prepared.minTreeWidth,
914
+ spinnerTick: ctx.spinnerTick
915
+ })
916
+ });
917
+ const bar = () => ({
918
+ prepare: prepareBar,
919
+ flexShrink: (prepared) => prepared.hasDeterminateRows ? 1 : 0,
920
+ flexBasis: (prepared) => prepared.hasDeterminateRows ? 30 : 0,
921
+ minWidth: (prepared) => prepared.hasDeterminateRows ? 4 : 0,
922
+ render: ({ task }, ctx) => /* @__PURE__ */ jsx(BarCell, {
923
+ task,
924
+ width: ctx.width
925
+ })
926
+ });
927
+ const amount = () => ({
928
+ prepare: measureAmountLayout,
929
+ align: "right",
930
+ render: ({ task }, ctx) => /* @__PURE__ */ jsx(AmountCell, {
931
+ task,
932
+ layout: ctx.prepared
933
+ })
934
+ });
935
+ const elapsed = () => ({
936
+ align: "right",
937
+ flexShrink: 0,
938
+ render: ({ task }, ctx) => /* @__PURE__ */ jsx(ElapsedCell, {
939
+ task,
940
+ now: ctx.now
941
+ })
1012
942
  });
943
+ const eta = () => ({
944
+ align: "right",
945
+ flexShrink: 0,
946
+ render: ({ task }, ctx) => /* @__PURE__ */ jsx(EtaCell, {
947
+ task,
948
+ now: ctx.now
949
+ })
950
+ });
951
+ const defaults = () => [
952
+ description(),
953
+ bar(),
954
+ amount(),
955
+ elapsed(),
956
+ eta()
957
+ ];
958
+ const resolveColumnSizeValue = (value, prepared) => {
959
+ if (typeof value === "function") return value(prepared);
960
+ return value;
961
+ };
1013
962
 
1014
963
  //#endregion
1015
- //#region src/ink-renderer/columns/progress-metrics-column.tsx
1016
- const PROGRESS_BAR_STICKY_KEY = Symbol("progress.bar");
1017
- const PROGRESS_AMOUNT_STICKY_KEY = Symbol("progress.amount");
1018
- const createProgressColumnModel = (frame, mode) => {
1019
- const metrics = computeProgressMetrics(frame);
1020
- const percent = PercentColumn(frame);
1021
- if (mode === "percent" && metrics.hasDeterminate) return {
1022
- metrics,
1023
- percent
1024
- };
1025
- return {
1026
- metrics,
1027
- percent,
1028
- amount: AmountColumn(frame, {
1029
- key: PROGRESS_AMOUNT_STICKY_KEY,
1030
- metrics,
1031
- stickyWidth: true
1032
- }),
1033
- bar: metrics.hasDeterminate ? BarColumn(frame, {
1034
- key: PROGRESS_BAR_STICKY_KEY,
1035
- fullWidth: false,
1036
- stickyWidth: true
1037
- }) : void 0
1038
- };
964
+ //#region src/renderer/column-resolver.ts
965
+ const NO_PREPARE = Symbol("no-prepare");
966
+ const getColumnsForRow = (row, columns) => columns.get(row.task.id) ?? defaults();
967
+ const toCellInfo = (row) => row;
968
+ const maxDefined = (values) => values.reduce((maxValue, value) => value === void 0 ? maxValue : Math.max(maxValue ?? Number.NEGATIVE_INFINITY, value), void 0);
969
+ const resolvePreparedGroups = (defs, cellInfos) => {
970
+ const groupedRows = /* @__PURE__ */ new Map();
971
+ defs.forEach((def, rowIndex) => {
972
+ if (!def?.prepare) return;
973
+ const key = def.prepare;
974
+ const rows = groupedRows.get(key);
975
+ const cell = cellInfos[rowIndex];
976
+ if (!cell) return;
977
+ if (rows) rows.push(cell);
978
+ else groupedRows.set(key, [cell]);
979
+ });
980
+ const groups = [{
981
+ key: NO_PREPARE,
982
+ prepared: void 0
983
+ }];
984
+ for (const [key, rows] of groupedRows) groups.push({
985
+ key,
986
+ prepared: key(rows)
987
+ });
988
+ return groups;
1039
989
  };
1040
- const layoutForWidth = (width, model) => {
1041
- if (!model.metrics.hasDeterminate || model.amount === void 0) return { kind: "amount-only" };
1042
- const bar = model.bar;
1043
- if (bar === void 0) return { kind: "percent" };
1044
- if (width < bar.measure.min + 1 + model.amount.measure.min || width < PERCENT_FALLBACK_WIDTH) return { kind: "percent" };
1045
- const available = Math.max(0, width - 1);
1046
- const amountWidth = Math.min(model.amount.measure.preferred, Math.max(model.amount.measure.min, available - bar.measure.min));
1047
- return {
1048
- kind: "bar-amount",
1049
- bar,
1050
- barWidth: Math.max(bar.measure.min, available - amountWidth),
1051
- amountWidth
1052
- };
990
+ const getPreparedFor = (def, groups) => {
991
+ if (!def?.prepare) return;
992
+ return groups.find((group) => group.key === def.prepare)?.prepared;
1053
993
  };
1054
- const ProgressMetricsColumn = (frame, config = {}) => {
1055
- const mode = config.mode ?? "full";
1056
- const model = createProgressColumnModel(frame, mode);
1057
- if (!model.metrics.hasStructuredCounts && !model.metrics.hasDeterminate) return;
1058
- if (mode === "percent" && model.metrics.hasDeterminate) return model.percent;
1059
- const amount = model.amount;
1060
- if (amount === void 0) return;
1061
- const preferred = model.metrics.hasDeterminate && model.bar !== void 0 ? model.bar.measure.preferred + 1 + amount.measure.preferred : amount.measure.preferred;
1062
- const fullMin = model.metrics.hasDeterminate && model.bar !== void 0 ? model.bar.measure.min + 1 + amount.measure.min : amount.measure.min;
1063
- const max = !model.metrics.hasDeterminate || model.bar === void 0 || model.bar.measure.max === void 0 ? amount.measure.max : model.bar.measure.max + 1 + amount.measure.preferred;
1064
- return {
1065
- measure: {
1066
- min: !model.metrics.hasDeterminate ? amount.measure.min : fullMin,
1067
- preferred,
1068
- max
1069
- },
1070
- commitStickyWidth: () => {
1071
- model.bar?.commitStickyWidth?.();
1072
- amount.commitStickyWidth?.();
1073
- },
1074
- render: (taskId, width) => {
1075
- const layout = layoutForWidth(width, model);
1076
- if (layout.kind === "percent") return model.percent.render(taskId, width);
1077
- if (layout.kind === "amount-only") return amount.render(taskId, width);
1078
- return /* @__PURE__ */ jsxs(Box, {
1079
- flexDirection: "row",
1080
- width,
1081
- children: [
1082
- /* @__PURE__ */ jsx(Box, {
1083
- width: layout.barWidth,
1084
- children: layout.bar.render(taskId, layout.barWidth)
1085
- }),
1086
- /* @__PURE__ */ jsx(Box, { marginRight: 1 }),
1087
- /* @__PURE__ */ jsx(Box, {
1088
- width: layout.amountWidth,
1089
- children: amount.render(taskId, layout.amountWidth)
1090
- })
1091
- ]
1092
- });
1093
- }
1094
- };
994
+ const resolveColumns = (rows, columns) => {
995
+ const columnsByRow = rows.map((row) => getColumnsForRow(row, columns));
996
+ const cellInfos = rows.map(toCellInfo);
997
+ const maxColumnCount = columnsByRow.reduce((max, defs) => Math.max(max, defs.length), 0);
998
+ return Array.from({ length: maxColumnCount }, (_, index) => {
999
+ const defsAtIndex = columnsByRow.map((defs) => defs[index]);
1000
+ const preparedGroups = resolvePreparedGroups(defsAtIndex, cellInfos);
1001
+ const entries = defsAtIndex.map((column) => column === void 0 ? void 0 : {
1002
+ column,
1003
+ prepared: getPreparedFor(column, preparedGroups)
1004
+ });
1005
+ return {
1006
+ index,
1007
+ rows,
1008
+ entries,
1009
+ flexGrow: maxDefined(entries.map((entry) => entry === void 0 ? void 0 : resolveColumnSizeValue(entry.column.flexGrow, entry.prepared))),
1010
+ flexShrink: maxDefined(entries.map((entry) => entry === void 0 ? void 0 : resolveColumnSizeValue(entry.column.flexShrink, entry.prepared))),
1011
+ flexBasis: maxDefined(entries.map((entry) => entry === void 0 ? void 0 : resolveColumnSizeValue(entry.column.flexBasis, entry.prepared))),
1012
+ minWidth: maxDefined(entries.map((entry) => entry === void 0 ? void 0 : resolveColumnSizeValue(entry.column.minWidth, entry.prepared)))
1013
+ };
1014
+ });
1095
1015
  };
1096
- const createProgressRootColumn = (mode) => createColumnDefinition({
1097
- _tag: "progress",
1098
- mode
1099
- }, (frame, config) => ProgressMetricsColumn(frame, config));
1100
- const ProgressRootColumn = createProgressRootColumn("full");
1101
- const ProgressPercentRootColumn = createProgressRootColumn("percent");
1102
1016
 
1103
1017
  //#endregion
1104
- //#region src/ink-renderer/columns/root-column.tsx
1105
- const ROOT_GAP = 1;
1106
- const visibleWidth = (widths, gap) => {
1107
- const visible = widths.filter((width) => width > 0);
1108
- return visible.reduce((sum, width) => sum + width, 0) + Math.max(0, visible.length - 1) * gap;
1109
- };
1110
- const ROOT_LAYOUTS = [
1111
- [
1112
- DescriptionTreeRootColumn,
1113
- ProgressRootColumn,
1114
- ElapsedRootColumn,
1115
- EtaRootColumn
1116
- ],
1117
- [
1118
- DescriptionPlainRootColumn,
1119
- ProgressRootColumn,
1120
- ElapsedRootColumn,
1121
- EtaRootColumn
1122
- ],
1123
- [
1124
- DescriptionPlainRootColumn,
1125
- ProgressPercentRootColumn,
1126
- ElapsedRootColumn,
1127
- EtaRootColumn
1128
- ],
1129
- [
1130
- DescriptionPlainRootColumn,
1131
- ProgressPercentRootColumn,
1132
- ElapsedRootColumn
1133
- ],
1134
- [DescriptionPlainRootColumn, ProgressPercentRootColumn],
1135
- [DescriptionCompactRootColumn, ProgressPercentRootColumn],
1136
- [DescriptionCompactRootColumn],
1137
- [DescriptionSpinnerRootColumn]
1138
- ];
1139
- const ROOT_COLUMNS = [...new Map(ROOT_LAYOUTS.flat().map((column) => [column.id, column])).values()];
1140
- const measureColumns = (frame) => new Map(ROOT_COLUMNS.flatMap((definition) => {
1141
- const column = definition.build(frame);
1142
- if (column === void 0) return [];
1143
- const measure = column.measure;
1144
- return [[definition.id, {
1145
- definition,
1146
- id: definition.id,
1147
- measure,
1148
- preferredWidth: measure.preferred,
1149
- commitStickyWidth: column.commitStickyWidth,
1150
- render: column.render
1151
- }]];
1152
- }));
1153
- const candidateRootLayouts = (columnsById) => {
1154
- const hasProgress = columnsById.has(ProgressRootColumn.id);
1155
- const hasPercentProgress = columnsById.has(ProgressPercentRootColumn.id);
1156
- const hasEta = columnsById.has(EtaRootColumn.id);
1157
- return [
1158
- ...hasProgress && hasEta ? [[
1159
- DescriptionTreeRootColumn,
1160
- ProgressRootColumn,
1161
- ElapsedRootColumn,
1162
- EtaRootColumn
1163
- ], [
1164
- DescriptionPlainRootColumn,
1165
- ProgressRootColumn,
1166
- ElapsedRootColumn,
1167
- EtaRootColumn
1168
- ]] : [],
1169
- ...hasProgress && !hasEta ? [[
1170
- DescriptionTreeRootColumn,
1171
- ProgressRootColumn,
1172
- ElapsedRootColumn
1173
- ], [
1174
- DescriptionPlainRootColumn,
1175
- ProgressRootColumn,
1176
- ElapsedRootColumn
1177
- ]] : [],
1178
- ...hasPercentProgress && hasEta ? [[
1179
- DescriptionPlainRootColumn,
1180
- ProgressPercentRootColumn,
1181
- ElapsedRootColumn,
1182
- EtaRootColumn
1183
- ]] : [],
1184
- ...hasPercentProgress ? [
1185
- [
1186
- DescriptionPlainRootColumn,
1187
- ProgressPercentRootColumn,
1188
- ElapsedRootColumn
1189
- ],
1190
- [DescriptionPlainRootColumn, ProgressPercentRootColumn],
1191
- [DescriptionCompactRootColumn, ProgressPercentRootColumn]
1192
- ] : [],
1193
- ...!hasProgress && !hasPercentProgress ? [
1194
- [DescriptionTreeRootColumn, ElapsedRootColumn],
1195
- [DescriptionPlainRootColumn, ElapsedRootColumn],
1196
- [DescriptionPlainRootColumn]
1197
- ] : [],
1198
- [DescriptionCompactRootColumn],
1199
- [DescriptionSpinnerRootColumn]
1200
- ];
1201
- };
1202
- const resolveRootLayouts = (columnsById) => {
1203
- const resolveLayout = (definitions) => {
1204
- const columns = [];
1205
- for (const definition of definitions) {
1206
- const column = columnsById.get(definition.id);
1207
- if (column === void 0) return;
1208
- columns.push(column);
1209
- }
1210
- return columns;
1211
- };
1212
- return candidateRootLayouts(columnsById).map(resolveLayout).filter((columns) => columns !== void 0);
1213
- };
1214
- const minimumWidthForSet = (columns) => visibleWidth(columns.map((column) => column.measure.min), ROOT_GAP);
1215
- const selectColumnSet = (columnSets, terminalColumns) => {
1216
- if (columnSets.length === 0) return [];
1217
- if (terminalColumns === void 0) return columnSets[0] ?? [];
1218
- return columnSets.find((columns) => minimumWidthForSet(columns) <= terminalColumns) ?? columnSets.at(-1) ?? [];
1219
- };
1220
- const preferredWidthsForSet = (columns) => columns.map((column) => Math.max(column.measure.min, column.preferredWidth));
1221
- const nextDistinctWidth = (entries, widest) => entries.find((entry) => entry.width < widest)?.width;
1222
- const reduceOverflowRichStyle = (widths, minimums, targetWidth) => {
1223
- let overflow = visibleWidth(widths, ROOT_GAP) - targetWidth;
1224
- while (overflow > 0) {
1225
- const shrinkable = widths.map((width, index) => ({
1226
- width,
1227
- index,
1228
- minimum: minimums[index] ?? width
1229
- })).filter(({ width, minimum }) => width > minimum).sort((left, right) => right.width - left.width || left.index - right.index);
1230
- if (shrinkable.length === 0) break;
1231
- const widest = shrinkable[0].width;
1232
- const cohort = shrinkable.filter(({ width }) => width === widest);
1233
- const nextWidth = nextDistinctWidth(shrinkable, widest);
1234
- const maxUniformDrop = widest - Math.max(nextWidth ?? 0, ...cohort.map(({ minimum }) => minimum));
1235
- const uniformDrop = Math.min(maxUniformDrop, Math.floor(overflow / cohort.length));
1236
- if (uniformDrop > 0) {
1237
- for (const { index } of cohort) widths[index] = widths[index] - uniformDrop;
1238
- overflow -= uniformDrop * cohort.length;
1239
- continue;
1240
- }
1241
- let changed = false;
1242
- for (const { index, minimum } of cohort) {
1243
- if (overflow <= 0) break;
1244
- if (widths[index] <= minimum) continue;
1245
- widths[index] = widths[index] - 1;
1246
- overflow -= 1;
1247
- changed = true;
1248
- }
1249
- if (!changed) break;
1250
- }
1251
- return widths;
1018
+ //#region src/renderer/public-api.tsx
1019
+ const justifyContentForAlign = (align) => {
1020
+ if (align === "right") return "flex-end";
1021
+ if (align === "center") return "center";
1022
+ return "flex-start";
1252
1023
  };
1253
- const widthForSelectedSet = (columns, targetWidth) => {
1254
- if (columns.length === 0) return [];
1255
- if (targetWidth === void 0) return preferredWidthsForSet(columns);
1256
- const minimums = columns.map((column) => column.measure.min);
1257
- const widths = preferredWidthsForSet(columns);
1258
- if (visibleWidth(widths, ROOT_GAP) <= targetWidth) return widths;
1259
- return reduceOverflowRichStyle(widths, minimums, targetWidth);
1024
+ const renderNode = (node) => {
1025
+ if (typeof node === "string" || typeof node === "number") return /* @__PURE__ */ jsx(Text, {
1026
+ wrap: "truncate-end",
1027
+ children: node
1028
+ });
1029
+ return node;
1260
1030
  };
1261
- const emptyRootColumn = (stickyWidths) => {
1262
- stickyWidths.clear();
1263
- return { render: () => null };
1031
+ const ColumnPosition = ({ position }) => {
1032
+ const ref = useRef(null);
1033
+ const { width, hasMeasured } = useBoxMetrics(ref);
1034
+ const now = useNow();
1035
+ const spinnerTick = useSpinnerTick();
1036
+ return /* @__PURE__ */ jsx(Box, {
1037
+ ref,
1038
+ flexDirection: "column",
1039
+ flexGrow: position.flexGrow,
1040
+ flexShrink: position.flexShrink ?? 0,
1041
+ flexBasis: position.flexBasis,
1042
+ minWidth: position.minWidth,
1043
+ children: position.rows.map((row, rowIndex) => {
1044
+ const entry = position.entries[rowIndex];
1045
+ const column = entry?.column;
1046
+ const cell = row;
1047
+ const output = column?.render(cell, {
1048
+ width: hasMeasured ? width : void 0,
1049
+ now,
1050
+ spinnerTick,
1051
+ prepared: entry?.prepared
1052
+ }) ?? null;
1053
+ return /* @__PURE__ */ jsx(Box, {
1054
+ height: 1,
1055
+ justifyContent: justifyContentForAlign(column?.align),
1056
+ children: renderNode(output)
1057
+ }, row.task.id);
1058
+ })
1059
+ });
1264
1060
  };
1265
- const RootColumn = (rows, now, tick, terminalColumns, stickyWidths = /* @__PURE__ */ new Map()) => {
1266
- if (rows.length === 0) return emptyRootColumn(stickyWidths);
1267
- const selectedColumns = selectColumnSet(resolveRootLayouts(measureColumns(createRenderFrame(rows, now, tick, stickyWidths))), terminalColumns);
1268
- for (const column of selectedColumns) column.commitStickyWidth?.();
1269
- const columns = widthForSelectedSet(selectedColumns, terminalColumns === void 0 ? visibleWidth(selectedColumns.map((column) => column.preferredWidth), ROOT_GAP) : terminalColumns).map((width, index) => ({
1270
- id: selectedColumns[index].id,
1271
- width,
1272
- render: selectedColumns[index].render
1273
- })).filter((column) => column.width > 0);
1274
- const taskIds = rows.map((row) => row.task.id);
1275
- const rowWidth = visibleWidth(columns.map((column) => column.width), ROOT_GAP);
1276
- return { render: () => /* @__PURE__ */ jsx(Box, {
1061
+ const ProgressRenderer = ({ rows, columns }) => {
1062
+ if (rows.length === 0) return null;
1063
+ return /* @__PURE__ */ jsx(Box, {
1277
1064
  flexDirection: "row",
1278
- minWidth: rowWidth,
1279
- children: columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
1280
- flexDirection: "column",
1281
- width: column.width,
1282
- marginRight: index < columns.length - 1 ? ROOT_GAP : 0,
1283
- children: taskIds.map((taskId) => /* @__PURE__ */ jsx(Box, {
1284
- width: column.width,
1285
- height: 1,
1286
- children: column.render(taskId, column.width)
1287
- }, taskId))
1288
- }, column.id))
1289
- }) };
1065
+ columnGap: 1,
1066
+ overflow: "hidden",
1067
+ children: resolveColumns(rows, columns).map((position) => /* @__PURE__ */ jsx(ColumnPosition, { position }, position.index))
1068
+ });
1290
1069
  };
1291
1070
 
1292
1071
  //#endregion
1293
- //#region src/ink-renderer/view/hooks/use-now-clock.ts
1294
- const useNowClock = (active, intervalMillis) => {
1295
- const [now, setNow] = useState(() => Date.now());
1296
- useEffect(() => {
1297
- if (!active) return;
1298
- setNow(Date.now());
1299
- const interval = setInterval(() => {
1300
- setNow(Date.now());
1301
- }, intervalMillis);
1302
- return () => {
1303
- clearInterval(interval);
1072
+ //#region src/renderer/store/render-snapshot.ts
1073
+ const orderedVisibleTasks = (store) => store.renderOrder.flatMap((row) => {
1074
+ const snapshot = store.tasks.get(row.id);
1075
+ if (!snapshot || snapshot.transient && snapshot.status !== "running") return [];
1076
+ return [{
1077
+ snapshot,
1078
+ depth: row.depth
1079
+ }];
1080
+ });
1081
+ const treeAncestorPrefix = (ancestorHasNextSibling) => ancestorHasNextSibling.slice(1).map((hasNextSibling) => hasNextSibling ? "│ " : " ").join("");
1082
+ const renderTreePrefix = (tree) => {
1083
+ if (tree.depth <= 0) return "";
1084
+ return `${treeAncestorPrefix(tree.ancestorHasNextSibling)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
1085
+ };
1086
+ const arraysEqual = (left, right) => {
1087
+ if (left.length !== right.length) return false;
1088
+ for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return false;
1089
+ return true;
1090
+ };
1091
+ const sameTreePrefixInputs = (left, right) => left.depth === right.depth && left.hasNextSibling === right.hasNextSibling && arraysEqual(left.ancestorHasNextSibling, right.ancestorHasNextSibling);
1092
+ const sameTree = (left, right) => sameTreePrefixInputs(left, right) && left.hasChildren === right.hasChildren;
1093
+ const deriveRow = (task, tree, previousRow) => {
1094
+ if (previousRow !== void 0 && previousRow.task === task && sameTree(previousRow.tree, tree)) return previousRow.derived;
1095
+ const treePrefix = previousRow !== void 0 && sameTreePrefixInputs(previousRow.tree, tree) ? previousRow.derived.treePrefix : renderTreePrefix(tree);
1096
+ const treePrefixWidth = previousRow !== void 0 && sameTreePrefixInputs(previousRow.tree, tree) ? previousRow.derived.treePrefixWidth : treePrefix.length;
1097
+ const descriptionWidth = previousRow !== void 0 && previousRow.task.description === task.description ? previousRow.derived.descriptionWidth : textWidth(task.description);
1098
+ const isDeterminate = previousRow !== void 0 && previousRow.task.units.total === task.units.total ? previousRow.derived.isDeterminate : task.units.total !== void 0;
1099
+ const hasRenderableProgress = previousRow !== void 0 && previousRow.task.units.total === task.units.total && previousRow.task.units.processed === task.units.processed ? previousRow.derived.hasRenderableProgress : task.units.total !== void 0 || task.units.processed > 0;
1100
+ return {
1101
+ treePrefix,
1102
+ treePrefixWidth,
1103
+ descriptionWidth,
1104
+ treePrefixedDescriptionWidth: treePrefixWidth + descriptionWidth,
1105
+ hasRenderableProgress,
1106
+ isDeterminate
1107
+ };
1108
+ };
1109
+ const computeTreeInfo = (ordered, previousRows) => {
1110
+ const hasNextSiblingByIndex = Array.from({ length: ordered.length }, () => false);
1111
+ const seenByDepth = [];
1112
+ for (let i = ordered.length - 1; i >= 0; i--) {
1113
+ const depth = ordered[i].depth;
1114
+ hasNextSiblingByIndex[i] = seenByDepth[depth] ?? false;
1115
+ seenByDepth[depth] = true;
1116
+ seenByDepth.length = depth + 1;
1117
+ }
1118
+ const ancestorStateByDepth = [];
1119
+ const previousRowsByTaskId = new Map(previousRows.map((row) => [row.task.id, row]));
1120
+ return ordered.map((entry, index) => {
1121
+ const depth = entry.depth;
1122
+ ancestorStateByDepth.length = depth;
1123
+ const hasChildren = index + 1 < ordered.length && ordered[index + 1] !== void 0 && ordered[index + 1].depth > depth;
1124
+ const tree = {
1125
+ depth,
1126
+ hasNextSibling: hasNextSiblingByIndex[index] ?? false,
1127
+ hasChildren,
1128
+ ancestorHasNextSibling: [...ancestorStateByDepth]
1304
1129
  };
1305
- }, [active, intervalMillis]);
1306
- return now;
1130
+ const previousRow = previousRowsByTaskId.get(entry.snapshot.id);
1131
+ ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
1132
+ if (previousRow !== void 0 && previousRow.task === entry.snapshot && sameTree(previousRow.tree, tree)) return previousRow;
1133
+ const derived = deriveRow(entry.snapshot, tree, previousRow);
1134
+ return {
1135
+ task: entry.snapshot,
1136
+ tree: previousRow !== void 0 && sameTree(previousRow.tree, tree) ? previousRow.tree : tree,
1137
+ derived
1138
+ };
1139
+ });
1140
+ };
1141
+ const toRenderSnapshot = (store, previousSnapshot) => {
1142
+ const visibleTasks = orderedVisibleTasks(store);
1143
+ const hasRunningTasks = visibleTasks.some((entry) => entry.snapshot.status === "running");
1144
+ return {
1145
+ rows: computeTreeInfo(visibleTasks, previousSnapshot?.rows ?? []),
1146
+ hasRunningTasks
1147
+ };
1307
1148
  };
1308
1149
 
1309
1150
  //#endregion
1310
- //#region src/ink-renderer/view/hooks/use-spinner-clock.ts
1311
- const useSpinnerClock = (active, intervalMillis) => {
1312
- const [tick, setTick] = useState(0);
1151
+ //#region src/renderer/store/use-progress-render-view.ts
1152
+ const useRenderSnapshot = (storeSnapshot) => {
1153
+ const previousSnapshotRef = useRef(void 0);
1154
+ const renderSnapshot = useMemo(() => toRenderSnapshot(storeSnapshot, previousSnapshotRef.current), [storeSnapshot]);
1313
1155
  useEffect(() => {
1314
- if (!active) return;
1315
- const interval = setInterval(() => {
1316
- setTick((current) => current + 1);
1317
- }, intervalMillis);
1318
- return () => {
1319
- clearInterval(interval);
1320
- };
1321
- }, [active, intervalMillis]);
1322
- return tick;
1156
+ previousSnapshotRef.current = renderSnapshot;
1157
+ }, [renderSnapshot]);
1158
+ return renderSnapshot;
1159
+ };
1160
+ const useProgressRenderView = (store) => {
1161
+ const publication = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1162
+ const renderSnapshot = useRenderSnapshot(publication.snapshot);
1163
+ return {
1164
+ publication,
1165
+ renderSnapshot,
1166
+ hasRunningTasks: renderSnapshot.rows.some((row) => row.task.status === "running")
1167
+ };
1323
1168
  };
1324
1169
 
1325
1170
  //#endregion
1326
- //#region src/ink-renderer/view/render-root.tsx
1327
- const SPINNER_INTERVAL_MILLIS = 100;
1328
- const NOW_INTERVAL_MILLIS = 1e3;
1329
- const ProgressRoot = ({ store, getTerminalColumns }) => {
1330
- const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1331
- const tick = useSpinnerClock(snapshot.hasRunningTasks, SPINNER_INTERVAL_MILLIS);
1332
- const now = useNowClock(snapshot.hasRunningTasks, NOW_INTERVAL_MILLIS);
1333
- const stickyWidths = useRef(/* @__PURE__ */ new Map());
1334
- return RootColumn(snapshot.rows, now, tick, getTerminalColumns(), stickyWidths.current).render();
1171
+ //#region src/renderer/renderer-service.tsx
1172
+ const MAX_FPS = 24;
1173
+ const ProgressRoot = ({ store }) => {
1174
+ const { renderSnapshot, hasRunningTasks, publication } = useProgressRenderView(store);
1175
+ return /* @__PURE__ */ jsx(SpinnerProvider, {
1176
+ active: hasRunningTasks,
1177
+ children: /* @__PURE__ */ jsx(NowProvider, {
1178
+ active: hasRunningTasks,
1179
+ children: /* @__PURE__ */ jsx(ProgressRenderer, {
1180
+ rows: renderSnapshot.rows,
1181
+ columns: publication.snapshot.columns
1182
+ })
1183
+ })
1184
+ });
1185
+ };
1186
+ const makeRendererv2InkRendererService = () => {
1187
+ return { run: (store, stdio) => {
1188
+ const proot = /* @__PURE__ */ jsx(ProgressRoot, { store });
1189
+ return Effect.sync(() => render(proot, {
1190
+ stdout: stdio.stdout,
1191
+ stderr: stdio.stderr,
1192
+ patchConsole: true,
1193
+ exitOnCtrlC: false,
1194
+ debug: false,
1195
+ maxFps: MAX_FPS
1196
+ })).pipe(Effect.flatMap((instance) => Effect.never.pipe(Effect.ensuring(Effect.gen(function* () {
1197
+ store.flush();
1198
+ instance.rerender(proot);
1199
+ yield* Effect.sync(() => {
1200
+ instance.unmount();
1201
+ });
1202
+ })))));
1203
+ } };
1335
1204
  };
1336
1205
 
1337
1206
  //#endregion
1338
1207
  //#region src/services/ink-renderer.tsx
1339
- const MAX_FPS = 12;
1340
- const makeDefaultInkRenderer = () => ({ run: (store, stdio, isTTY) => Effect.sync(() => render(/* @__PURE__ */ jsx(ProgressRoot, {
1341
- store,
1342
- getTerminalColumns: () => isTTY ? stdio.stderr.columns : void 0
1343
- }), {
1344
- stdout: stdio.stdout,
1345
- stderr: stdio.stderr,
1346
- patchConsole: true,
1347
- exitOnCtrlC: false,
1348
- debug: false,
1349
- maxFps: MAX_FPS
1350
- })).pipe(Effect.flatMap((instance) => Effect.never.pipe(Effect.ensuring(Effect.gen(function* () {
1351
- store.flush();
1352
- yield* Effect.sleep("0 millis");
1353
- yield* Effect.sync(() => {
1354
- instance.unmount();
1355
- });
1356
- }))))) });
1357
1208
  var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")() {
1358
- static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeDefaultInkRenderer()));
1209
+ static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeRendererv2InkRendererService()));
1359
1210
  };
1360
1211
 
1361
1212
  //#endregion
@@ -1370,16 +1221,16 @@ var ProgressStdio = class ProgressStdio extends Context.Tag("stromseng.dev/effec
1370
1221
 
1371
1222
  //#endregion
1372
1223
  //#region src/services/progress.ts
1224
+ /** Builds the scoped implementation used by `ProgressService.task(...)` without auto-providing services. */
1373
1225
  const makeProgressService = Effect.gen(function* () {
1374
1226
  const stdio = yield* ProgressStdio;
1375
1227
  const inkRenderer = yield* InkRenderer;
1376
1228
  const outerConsole = yield* Effect.console;
1377
- const isTTY = Boolean(stdio.stderr.isTTY);
1378
1229
  const store = makeProgressRenderStore();
1379
1230
  const currentParentRef = yield* FiberRef.make(Option.none());
1380
1231
  const scope = yield* Effect.scope;
1381
1232
  const log = (...args) => args.length === 0 ? Effect.void : outerConsole.log(...args);
1382
- yield* Effect.forkIn(inkRenderer.run(store, stdio, isTTY), scope);
1233
+ yield* Effect.forkIn(inkRenderer.run(store, stdio), scope);
1383
1234
  yield* Effect.sleep("0 millis");
1384
1235
  const addTask = (options) => Effect.gen(function* () {
1385
1236
  const resolvedParentId = options.parentId === void 0 ? yield* FiberRef.get(currentParentRef) : Option.some(options.parentId);
@@ -1395,7 +1246,27 @@ const makeProgressService = Effect.gen(function* () {
1395
1246
  const failTask = store.failTask;
1396
1247
  const getTask = store.getTask;
1397
1248
  const listTasks = store.listTasks;
1398
- const runTask = dual(2, (effect, options) => Effect.gen(function* () {
1249
+ const setMetadata = store.setMetadata;
1250
+ const getMetadata = store.getMetadata;
1251
+ const makeTaskHandle = (taskId) => ({
1252
+ id: taskId,
1253
+ getMetadata: getMetadata(taskId),
1254
+ setMetadata: (metadata) => setMetadata(taskId, metadata),
1255
+ updateMetadata: (f) => Effect.flatMap(getMetadata(taskId), (current) => setMetadata(taskId, f(current))),
1256
+ incrementSucceeded: (amount) => incrementSucceeded(taskId, amount),
1257
+ incrementFailed: (amount) => incrementFailed(taskId, amount),
1258
+ update: (options) => updateTask(taskId, options),
1259
+ complete: completeTask(taskId),
1260
+ fail: failTask(taskId),
1261
+ getSnapshot: getTask(taskId).pipe(Effect.map(Option.getOrThrow))
1262
+ });
1263
+ const autoFinalizeIfRunning = (taskId, exit) => Effect.gen(function* () {
1264
+ const task = yield* getTask(taskId);
1265
+ if (Option.isNone(task) || task.value.status !== "running") return;
1266
+ if (Exit.isSuccess(exit)) yield* completeTask(taskId);
1267
+ else yield* failTask(taskId);
1268
+ });
1269
+ const scopedTask = dual(2, (effect, options) => Effect.gen(function* () {
1399
1270
  const inheritedParentId = yield* FiberRef.get(currentParentRef);
1400
1271
  const resolvedParentId = options.parentId === void 0 ? inheritedParentId : Option.some(options.parentId);
1401
1272
  const taskId = yield* addTask({
@@ -1415,17 +1286,29 @@ const makeProgressService = Effect.gen(function* () {
1415
1286
  log,
1416
1287
  getTask,
1417
1288
  listTasks,
1418
- runTask,
1419
- withTask: dual(2, (effect, options) => runTask(Effect.gen(function* () {
1420
- const taskId = yield* Task;
1421
- const exit = yield* Effect.exit(effect);
1422
- if (Exit.isSuccess(exit)) yield* completeTask(taskId);
1423
- else yield* failTask(taskId);
1424
- return yield* Exit.match(exit, {
1425
- onFailure: Effect.failCause,
1426
- onSuccess: Effect.succeed
1427
- });
1428
- }), options))
1289
+ setMetadata,
1290
+ getMetadata,
1291
+ task: dual(2, (effectOrCallback, options) => {
1292
+ if (typeof effectOrCallback === "function") return scopedTask(Effect.gen(function* () {
1293
+ const taskId = yield* Task;
1294
+ const handle = makeTaskHandle(taskId);
1295
+ const exit = yield* Effect.exit(effectOrCallback(handle));
1296
+ yield* autoFinalizeIfRunning(taskId, exit);
1297
+ return yield* Exit.match(exit, {
1298
+ onFailure: Effect.failCause,
1299
+ onSuccess: Effect.succeed
1300
+ });
1301
+ }), options);
1302
+ return scopedTask(Effect.gen(function* () {
1303
+ const taskId = yield* Task;
1304
+ const exit = yield* Effect.exit(effectOrCallback);
1305
+ yield* autoFinalizeIfRunning(taskId, exit);
1306
+ return yield* Exit.match(exit, {
1307
+ onFailure: Effect.failCause,
1308
+ onSuccess: Effect.succeed
1309
+ });
1310
+ }), options);
1311
+ })
1429
1312
  };
1430
1313
  return Progress.of(service);
1431
1314
  });
@@ -1457,9 +1340,17 @@ const provideProgress = (effect) => Effect.gen(function* () {
1457
1340
  if (Option.isSome(existing)) return yield* Effect.provideService(effect, Progress, existing.value);
1458
1341
  return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
1459
1342
  });
1460
- const task = dual(2, (effect, options) => {
1343
+ /**
1344
+ * Runs an effect inside a task, creating and providing a `Progress` service automatically when one
1345
+ * is not already present in the environment.
1346
+ *
1347
+ * The effect form tracks success and failure from the effect exit. The callback form exposes a
1348
+ * typed `TaskHandle` for task-local updates, typed metadata, and explicit completion or failure,
1349
+ * and otherwise auto-finalizes from the callback exit if the task is still `running`.
1350
+ */
1351
+ const task = dual(2, (effectOrCallback, options) => {
1461
1352
  return provideProgress(Effect.gen(function* () {
1462
- return yield* (yield* Progress).withTask(effect, options);
1353
+ return yield* (yield* Progress).task(effectOrCallback, options);
1463
1354
  }));
1464
1355
  });
1465
1356
  const wrapEffects = (effects, tap) => Array.isArray(effects) ? effects.map(tap) : Object.fromEntries(Object.entries(effects).map(([k, effect]) => [k, tap(effect)]));
@@ -1482,10 +1373,14 @@ const isTaskFullyProcessed = (progress, taskId) => Effect.gen(function* () {
1482
1373
  const { processed, total } = taskOption.value.units;
1483
1374
  return total !== void 0 && processed >= total;
1484
1375
  });
1376
+ /**
1377
+ * Runs multiple effects under a single parent task and keeps the task counters in sync with the
1378
+ * child effect outcomes.
1379
+ */
1485
1380
  const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* () {
1486
1381
  const progress = yield* Progress;
1487
- return yield* progress.runTask(Effect.gen(function* () {
1488
- const taskId = yield* Task;
1382
+ return yield* progress.task((handle) => Effect.gen(function* () {
1383
+ const taskId = handle.id;
1489
1384
  const exit = yield* Effect.exit(Effect.all(wrapEffects(effects, (effect) => wrapTrackedEffect(progress, taskId, effect)), {
1490
1385
  concurrency: options.concurrency,
1491
1386
  batching: options.batching,
@@ -1508,10 +1403,13 @@ const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* (
1508
1403
  countDisplay: allCountDisplay(options.mode)
1509
1404
  });
1510
1405
  })));
1406
+ /**
1407
+ * Runs `Effect.forEach` under a single parent task and advances the task counters as items finish.
1408
+ */
1511
1409
  const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(function* () {
1512
1410
  const progress = yield* Progress;
1513
- return yield* progress.runTask(Effect.gen(function* () {
1514
- const taskId = yield* Task;
1411
+ return yield* progress.task((handle) => Effect.gen(function* () {
1412
+ const taskId = handle.id;
1515
1413
  const exit = yield* Effect.exit(Effect.forEach(iterable, (item, index) => wrapTrackedEffect(progress, taskId, f(item, index)), {
1516
1414
  concurrency: options.concurrency,
1517
1415
  batching: options.batching,
@@ -1533,4 +1431,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1533
1431
  })));
1534
1432
 
1535
1433
  //#endregion
1536
- export { Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1434
+ export { columns_exports as Columns, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskSnapshotSchema, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };