effective-progress 0.9.0 → 0.10.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 { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Schema } from "effect";
2
2
  import { dual } from "effect/Function";
3
3
  import { Box, Text, render } from "ink";
4
- import { useEffect, useRef, useState, useSyncExternalStore } from "react";
5
4
  import stringWidth from "fast-string-width";
6
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import cliSpinners from "cli-spinners";
7
+ import { createContext, memo, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
+ import { VirtualList } from "ink-virtual-list";
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,
@@ -26,7 +28,8 @@ var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
26
28
  units: TaskUnitsSchema,
27
29
  startedAt: Schema.Number,
28
30
  completedAt: Schema.NullOr(Schema.Number)
29
- }) {};
31
+ });
32
+ const TaskSnapshot = (snapshot) => snapshot;
30
33
  var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
31
34
  var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
32
35
  taskId: TaskIdSchema,
@@ -58,60 +61,7 @@ const ProgressTaskEventSchema = Schema.Union(TaskAddedEvent, TaskUpdatedEvent, T
58
61
  const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
59
62
 
60
63
  //#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
64
+ //#region src/renderer/store.ts
115
65
  const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
116
66
  const sanitizeTotalOnAdd = (total) => {
117
67
  if (total === void 0) return;
@@ -142,7 +92,7 @@ const updatedSnapshot = (snapshot, options) => {
142
92
  failed: options.failed ?? currentUnits.failed,
143
93
  total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(options.total) : currentUnits.total
144
94
  });
145
- return new TaskSnapshot({
95
+ return TaskSnapshot({
146
96
  id: snapshot.id,
147
97
  parentId: snapshot.parentId,
148
98
  description: options.description ?? snapshot.description,
@@ -154,7 +104,7 @@ const updatedSnapshot = (snapshot, options) => {
154
104
  completedAt: snapshot.completedAt
155
105
  });
156
106
  };
157
- const withTransient = (snapshot, transient) => new TaskSnapshot({
107
+ const withTransient = (snapshot, transient) => TaskSnapshot({
158
108
  id: snapshot.id,
159
109
  parentId: snapshot.parentId,
160
110
  description: snapshot.description,
@@ -193,14 +143,26 @@ const removeFromRenderOrder = (renderOrder, taskId) => {
193
143
  next.splice(idx, end - idx);
194
144
  return next;
195
145
  };
196
- const SNAPSHOT_PUBLISH_INTERVAL_MILLIS = 50;
146
+ const subtreeTaskIds = (renderOrder, taskId) => {
147
+ const idx = renderOrder.findIndex((row) => row.id === taskId);
148
+ if (idx === -1) return [];
149
+ const taskDepth = renderOrder[idx].depth;
150
+ let end = idx + 1;
151
+ while (end < renderOrder.length && renderOrder[end].depth > taskDepth) end++;
152
+ return renderOrder.slice(idx, end).map((row) => row.id);
153
+ };
154
+ const SNAPSHOT_PUBLISH_INTERVAL_MILLIS = 100;
197
155
  const makeProgressRenderStore = () => {
198
156
  let nextTaskId = 0;
199
157
  let state = {
200
158
  tasks: /* @__PURE__ */ new Map(),
201
159
  renderOrder: []
202
160
  };
203
- let publishedSnapshot = toRenderSnapshot(state);
161
+ let pendingEvents = [];
162
+ let publishedPublication = {
163
+ snapshot: state,
164
+ events: []
165
+ };
204
166
  let hasPendingPublish = false;
205
167
  let lastPublishAt = 0;
206
168
  let publishTimeout;
@@ -209,10 +171,15 @@ const makeProgressRenderStore = () => {
209
171
  for (const listener of listeners) listener();
210
172
  };
211
173
  const publishNow = () => {
174
+ const nextPublication = {
175
+ snapshot: state,
176
+ events: [...pendingEvents]
177
+ };
212
178
  hasPendingPublish = false;
213
- lastPublishAt = Date.now();
214
- publishedSnapshot = toRenderSnapshot(state);
179
+ publishedPublication = nextPublication;
215
180
  notifyListeners();
181
+ pendingEvents = [];
182
+ lastPublishAt = Date.now();
216
183
  };
217
184
  const clearScheduledPublish = () => {
218
185
  if (publishTimeout === void 0) return;
@@ -235,9 +202,10 @@ const makeProgressRenderStore = () => {
235
202
  publishNow();
236
203
  }, waitMillis);
237
204
  };
238
- const publish = (nextState) => {
239
- if (nextState === state) return;
240
- state = nextState;
205
+ const publish = (update) => {
206
+ if (update.state === state) return;
207
+ state = update.state;
208
+ if (update.events.length > 0) pendingEvents.push(...update.events);
241
209
  hasPendingPublish = true;
242
210
  schedulePublish();
243
211
  };
@@ -245,7 +213,7 @@ const makeProgressRenderStore = () => {
245
213
  publish(transform(state));
246
214
  };
247
215
  return {
248
- getSnapshot: () => publishedSnapshot,
216
+ getSnapshot: () => publishedPublication,
249
217
  subscribe: (listener) => {
250
218
  listeners.add(listener);
251
219
  return () => {
@@ -268,7 +236,7 @@ const makeProgressRenderStore = () => {
268
236
  const now = yield* Clock.currentTimeMillis;
269
237
  const parentId = options.parentId ?? null;
270
238
  const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
271
- const task = new TaskSnapshot({
239
+ const task = TaskSnapshot({
272
240
  id: taskId,
273
241
  parentId,
274
242
  description: options.description,
@@ -289,45 +257,70 @@ const makeProgressRenderStore = () => {
289
257
  depth
290
258
  });
291
259
  return {
292
- tasks: nextTasks,
293
- renderOrder: nextRenderOrder
260
+ state: {
261
+ tasks: nextTasks,
262
+ renderOrder: nextRenderOrder
263
+ },
264
+ events: [new TaskAddedEvent({
265
+ taskId,
266
+ parentId,
267
+ description: task.description,
268
+ total: task.units.total,
269
+ transient: task.transient,
270
+ countDisplay: task.countDisplay
271
+ })]
294
272
  };
295
273
  });
296
274
  return taskId;
297
275
  }),
298
- updateTask: (taskId, options) => Effect.gen(function* () {
299
- const currentTask = state.tasks.get(taskId);
300
- if (!currentTask) return;
301
- const nextTask = updatedSnapshot(currentTask, options);
276
+ updateTask: (taskId, options) => Effect.sync(() => {
302
277
  updateState((current) => {
303
- if (!current.tasks.get(taskId)) return current;
278
+ const currentTask = current.tasks.get(taskId);
279
+ if (!currentTask) return {
280
+ state: current,
281
+ events: []
282
+ };
283
+ const nextTask = updatedSnapshot(currentTask, options);
304
284
  const nextTasks = new Map(current.tasks);
305
285
  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));
286
+ const events = [new TaskUpdatedEvent({
287
+ taskId,
288
+ description: options.description ?? void 0,
289
+ succeeded: options.succeeded ?? void 0,
290
+ failed: options.failed ?? void 0,
291
+ processed: options.succeeded !== void 0 || options.failed !== void 0 ? nextTask.units.processed : void 0,
292
+ total: hasExplicitTotal(options) ? nextTask.units.total : void 0,
293
+ transient: options.transient ?? void 0,
294
+ countDisplay: options.countDisplay ?? void 0
295
+ })];
296
+ if (options.transient !== void 0) for (const candidateId of subtreeTaskIds(current.renderOrder, taskId).slice(1)) {
297
+ const candidate = current.tasks.get(candidateId);
298
+ if (!candidate) continue;
299
+ const nextCandidate = withTransient(candidate, nextTask.transient);
300
+ nextTasks.set(candidateId, nextCandidate);
301
+ events.push(new TaskUpdatedEvent({
302
+ taskId: candidateId,
303
+ transient: nextCandidate.transient
304
+ }));
318
305
  }
319
306
  return {
320
- tasks: nextTasks,
321
- renderOrder: current.renderOrder
307
+ state: {
308
+ tasks: nextTasks,
309
+ renderOrder: current.renderOrder
310
+ },
311
+ events
322
312
  };
323
313
  });
324
314
  }),
325
315
  incrementSucceeded: (taskId, amount = 1) => Effect.sync(() => {
326
316
  updateState((current) => {
327
317
  const currentTask = current.tasks.get(taskId);
328
- if (!currentTask) return current;
318
+ if (!currentTask) return {
319
+ state: current,
320
+ events: []
321
+ };
329
322
  const nextTasks = new Map(current.tasks);
330
- nextTasks.set(taskId, new TaskSnapshot({
323
+ nextTasks.set(taskId, TaskSnapshot({
331
324
  id: currentTask.id,
332
325
  parentId: currentTask.parentId,
333
326
  description: currentTask.description,
@@ -343,17 +336,27 @@ const makeProgressRenderStore = () => {
343
336
  completedAt: currentTask.completedAt
344
337
  }));
345
338
  return {
346
- tasks: nextTasks,
347
- renderOrder: current.renderOrder
339
+ state: {
340
+ tasks: nextTasks,
341
+ renderOrder: current.renderOrder
342
+ },
343
+ events: [new TaskAdvancedEvent({
344
+ taskId,
345
+ amount,
346
+ kind: "succeeded"
347
+ })]
348
348
  };
349
349
  });
350
350
  }),
351
351
  incrementFailed: (taskId, amount = 1) => Effect.sync(() => {
352
352
  updateState((current) => {
353
353
  const currentTask = current.tasks.get(taskId);
354
- if (!currentTask) return current;
354
+ if (!currentTask) return {
355
+ state: current,
356
+ events: []
357
+ };
355
358
  const nextTasks = new Map(current.tasks);
356
- nextTasks.set(taskId, new TaskSnapshot({
359
+ nextTasks.set(taskId, TaskSnapshot({
357
360
  id: currentTask.id,
358
361
  parentId: currentTask.parentId,
359
362
  description: currentTask.description,
@@ -369,8 +372,15 @@ const makeProgressRenderStore = () => {
369
372
  completedAt: currentTask.completedAt
370
373
  }));
371
374
  return {
372
- tasks: nextTasks,
373
- renderOrder: current.renderOrder
375
+ state: {
376
+ tasks: nextTasks,
377
+ renderOrder: current.renderOrder
378
+ },
379
+ events: [new TaskAdvancedEvent({
380
+ taskId,
381
+ amount,
382
+ kind: "failed"
383
+ })]
374
384
  };
375
385
  });
376
386
  }),
@@ -378,16 +388,23 @@ const makeProgressRenderStore = () => {
378
388
  const now = yield* Clock.currentTimeMillis;
379
389
  updateState((current) => {
380
390
  const currentTask = current.tasks.get(taskId);
381
- if (!currentTask) return current;
391
+ if (!currentTask) return {
392
+ state: current,
393
+ events: []
394
+ };
382
395
  const nextTasks = new Map(current.tasks);
383
396
  if (currentTask.transient) {
384
- nextTasks.delete(taskId);
397
+ const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
398
+ for (const removedTaskId of removedTaskIds) nextTasks.delete(removedTaskId);
385
399
  return {
386
- tasks: nextTasks,
387
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
400
+ state: {
401
+ tasks: nextTasks,
402
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
403
+ },
404
+ events: [new TaskCompletedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
388
405
  };
389
406
  }
390
- nextTasks.set(taskId, new TaskSnapshot({
407
+ nextTasks.set(taskId, TaskSnapshot({
391
408
  id: currentTask.id,
392
409
  parentId: currentTask.parentId,
393
410
  description: currentTask.description,
@@ -407,8 +424,11 @@ const makeProgressRenderStore = () => {
407
424
  completedAt: now
408
425
  }));
409
426
  return {
410
- tasks: nextTasks,
411
- renderOrder: current.renderOrder
427
+ state: {
428
+ tasks: nextTasks,
429
+ renderOrder: current.renderOrder
430
+ },
431
+ events: [new TaskCompletedEvent({ taskId })]
412
432
  };
413
433
  });
414
434
  }),
@@ -416,16 +436,23 @@ const makeProgressRenderStore = () => {
416
436
  const now = yield* Clock.currentTimeMillis;
417
437
  updateState((current) => {
418
438
  const currentTask = current.tasks.get(taskId);
419
- if (!currentTask) return current;
439
+ if (!currentTask) return {
440
+ state: current,
441
+ events: []
442
+ };
420
443
  const nextTasks = new Map(current.tasks);
421
444
  if (currentTask.transient) {
422
- nextTasks.delete(taskId);
445
+ const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
446
+ for (const removedTaskId of removedTaskIds) nextTasks.delete(removedTaskId);
423
447
  return {
424
- tasks: nextTasks,
425
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
448
+ state: {
449
+ tasks: nextTasks,
450
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
451
+ },
452
+ events: [new TaskFailedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
426
453
  };
427
454
  }
428
- nextTasks.set(taskId, new TaskSnapshot({
455
+ nextTasks.set(taskId, TaskSnapshot({
429
456
  id: currentTask.id,
430
457
  parentId: currentTask.parentId,
431
458
  description: currentTask.description,
@@ -437,8 +464,11 @@ const makeProgressRenderStore = () => {
437
464
  completedAt: now
438
465
  }));
439
466
  return {
440
- tasks: nextTasks,
441
- renderOrder: current.renderOrder
467
+ state: {
468
+ tasks: nextTasks,
469
+ renderOrder: current.renderOrder
470
+ },
471
+ events: [new TaskFailedEvent({ taskId })]
442
472
  };
443
473
  });
444
474
  }),
@@ -448,46 +478,11 @@ const makeProgressRenderStore = () => {
448
478
  };
449
479
 
450
480
  //#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
- };
475
- };
481
+ //#region src/renderer/shared/determinate.ts
482
+ const isDeterminate$2 = (task) => task.units.total !== void 0;
476
483
 
477
484
  //#endregion
478
- //#region src/ink-renderer/format.ts
479
- const SPINNER_FRAMES = [
480
- "⠋",
481
- "⠙",
482
- "⠹",
483
- "⠸",
484
- "⠼",
485
- "⠴",
486
- "⠦",
487
- "⠧",
488
- "⠇",
489
- "⠏"
490
- ];
485
+ //#region src/renderer/shared/format.ts
491
486
  const isDeterminate$1 = (task) => task.units.total !== void 0;
492
487
  const showsUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
493
488
  const formatDurationSeconds = (seconds) => {
@@ -513,37 +508,6 @@ const formatEta = (task, now) => {
513
508
  const elapsedMillis = Math.max(1, now - task.startedAt);
514
509
  return formatDurationSeconds(Math.max(0, Math.floor(elapsedMillis / processed * remaining)) / 1e3);
515
510
  };
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
511
  const formatDeterminateAmountParts = (task) => {
548
512
  if (!isDeterminate$1(task)) return;
549
513
  const totalText = `${task.units.total}`;
@@ -556,16 +520,6 @@ const formatDeterminateAmountParts = (task) => {
556
520
  total: totalText
557
521
  };
558
522
  };
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
523
  const formatAmount = (task, _tick) => {
570
524
  if (isDeterminate$1(task)) {
571
525
  const parts = formatDeterminateAmountParts(task);
@@ -582,780 +536,808 @@ const formatAmount = (task, _tick) => {
582
536
  };
583
537
 
584
538
  //#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
- };
539
+ //#region src/renderer/shared/text-width.ts
540
+ const WIDTH_CACHE_LIMIT = 8192;
541
+ const widthCache = /* @__PURE__ */ new Map();
542
+ const textWidth = (text) => {
543
+ const cached = widthCache.get(text);
544
+ if (cached !== void 0) return cached;
545
+ const width = stringWidth(text);
546
+ if (widthCache.size >= WIDTH_CACHE_LIMIT) widthCache.clear();
547
+ widthCache.set(text, width);
548
+ return width;
591
549
  };
592
550
 
593
551
  //#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);
552
+ //#region src/renderer/columns/amount-column.tsx
553
+ const hasUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
554
+ const hasCountedAmount = (task) => isDeterminate$2(task) || hasUnknownTotalCounts(task);
555
+ const totalTextFor = (task) => task.units.total === void 0 ? "?" : `${task.units.total}`;
556
+ const emptyAmountLayout = {
557
+ hasDetailedRows: false,
558
+ countWidth: 0,
559
+ processedWidth: 0,
560
+ totalWidth: 0,
561
+ preferredWidth: 0
562
+ };
563
+ const measureAmountLayout = (rows) => {
564
+ const countedTasks = rows.flatMap((row) => hasCountedAmount(row.task) ? [row.task] : []);
565
+ const hasDetailedRows = countedTasks.some((task) => task.countDisplay === "detailed");
566
+ if (countedTasks.length === 0) {
567
+ const preferredWidth = rows.reduce((max, row) => Math.max(max, textWidth(formatAmount(row.task, 0))), 0);
568
+ return {
569
+ ...emptyAmountLayout,
570
+ preferredWidth
571
+ };
572
+ }
573
+ const processedWidth = countedTasks.reduce((max, task) => Math.max(max, `${task.units.processed}`.length), 1);
574
+ const totalWidth = countedTasks.reduce((max, task) => Math.max(max, totalTextFor(task).length), 1);
575
+ const countWidth = hasDetailedRows ? countedTasks.reduce((max, task) => Math.max(max, processedWidth, totalWidth, `${task.units.succeeded}`.length, `${task.units.failed}`.length), 1) : 0;
576
+ const countedWidth = (hasDetailedRows ? countWidth + 1 + countWidth + 1 : 0) + processedWidth + 1 + totalWidth;
598
577
  return {
599
- ...measure,
600
- preferred,
601
- max
578
+ hasDetailedRows,
579
+ countWidth,
580
+ processedWidth,
581
+ totalWidth,
582
+ preferredWidth: rows.reduce((max, row) => {
583
+ if (hasCountedAmount(row.task)) return Math.max(max, countedWidth);
584
+ return Math.max(max, textWidth(formatAmount(row.task, 0)));
585
+ }, countedWidth)
602
586
  };
603
587
  };
604
- const commitStickyWidth = ({ key, measure, stickyWidths }) => {
605
- stickyWidths.set(key, measure.preferred);
588
+ const renderAmount = (task, layout) => {
589
+ if (!hasCountedAmount(task)) return formatAmount(task, 0);
590
+ const processed = `${task.units.processed}`.padStart(layout.processedWidth, " ");
591
+ const total = totalTextFor(task).padStart(layout.totalWidth, " ");
592
+ if (!layout.hasDetailedRows) return `${processed}/${total}`;
593
+ if (task.countDisplay !== "detailed") return `${" ".repeat(layout.countWidth)} ${" ".repeat(layout.countWidth)} ${processed}/${total}`;
594
+ const succeeded = `${task.units.succeeded}`.padStart(layout.countWidth, " ");
595
+ const failed = `${task.units.failed}`.padStart(layout.countWidth, " ");
596
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
597
+ /* @__PURE__ */ jsx(Text, {
598
+ color: "green",
599
+ children: succeeded
600
+ }),
601
+ /* @__PURE__ */ jsx(Text, { children: ` ` }),
602
+ /* @__PURE__ */ jsx(Text, {
603
+ color: "red",
604
+ children: failed
605
+ }),
606
+ /* @__PURE__ */ jsx(Text, { children: ` ${processed}/${total}` })
607
+ ] });
608
+ };
609
+ const defaultAmountColumnConfig = {
610
+ minWidth: 4,
611
+ sticky: true
606
612
  };
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
+ const createAmountColumn = (config) => {
614
+ const resolvedConfig = {
615
+ ...defaultAmountColumnConfig,
616
+ ...config
617
+ };
618
+ let amountLayout = emptyAmountLayout;
613
619
  return {
614
- measure,
615
- commitStickyWidth: stickyKey === void 0 ? void 0 : () => {
616
- commitStickyWidth({
617
- key: stickyKey,
618
- measure,
619
- stickyWidths: frame.stickyWidths
620
- });
620
+ Component: ({ row }) => /* @__PURE__ */ jsx(Text, {
621
+ wrap: "truncate-end",
622
+ children: renderAmount(row.task, amountLayout)
623
+ }),
624
+ measure: ({ rows }) => {
625
+ amountLayout = measureAmountLayout(rows);
626
+ return {
627
+ minWidth: amountLayout.preferredWidth > 0 ? Math.min(resolvedConfig.minWidth, amountLayout.preferredWidth) : 0,
628
+ preferredWidth: amountLayout.preferredWidth,
629
+ maxWidth: amountLayout.preferredWidth
630
+ };
621
631
  },
622
- render
632
+ noWrap: false,
633
+ sticky: resolvedConfig.sticky
623
634
  };
624
635
  };
625
636
 
626
637
  //#endregion
627
- //#region src/ink-renderer/columns/text-width.ts
628
- const WIDTH_CACHE_LIMIT = 4096;
629
- const widthCache = /* @__PURE__ */ new Map();
630
- const textWidth = (text) => {
631
- const cached = widthCache.get(text);
632
- if (cached !== void 0) return cached;
633
- const width = stringWidth(text);
634
- if (widthCache.size >= WIDTH_CACHE_LIMIT) widthCache.clear();
635
- widthCache.set(text, width);
636
- return width;
638
+ //#region src/renderer/columns/bar-column.tsx
639
+ const DEFAULT_BAR_WIDTH = 30;
640
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
641
+ const renderProgressBar = (task, width) => {
642
+ if (!isDeterminate$2(task)) return /* @__PURE__ */ jsx(Text, { children: ` `.repeat(Math.max(0, width)) });
643
+ const displayTotal = Math.max(task.units.total, task.units.succeeded + task.units.failed);
644
+ const succeededEnd = displayTotal === 0 ? width : Math.round(task.units.succeeded / displayTotal * width);
645
+ const failedEnd = displayTotal === 0 ? width : Math.round((task.units.succeeded + task.units.failed) / displayTotal * width);
646
+ const succeededLength = clamp(succeededEnd, 0, width);
647
+ const failedLength = clamp(failedEnd, succeededLength, width) - succeededLength;
648
+ const remainingLength = Math.max(0, width - succeededLength - failedLength);
649
+ return /* @__PURE__ */ jsxs(Text, {
650
+ wrap: "truncate-end",
651
+ children: [
652
+ /* @__PURE__ */ jsx(Text, {
653
+ color: "green",
654
+ children: "━".repeat(succeededLength)
655
+ }),
656
+ /* @__PURE__ */ jsx(Text, {
657
+ color: "red",
658
+ children: "━".repeat(failedLength)
659
+ }),
660
+ /* @__PURE__ */ jsx(Text, {
661
+ color: "gray",
662
+ children: "─".repeat(remainingLength)
663
+ })
664
+ ]
665
+ });
637
666
  };
638
-
639
- //#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 ? "├─ " : "└─ "}`;
667
+ const defaultBarColumnConfig = {
668
+ minWidth: 4,
669
+ barWidth: DEFAULT_BAR_WIDTH,
670
+ sticky: true
652
671
  };
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
672
+ const createBarColumn = (config) => {
673
+ const resolvedConfig = {
674
+ ...defaultBarColumnConfig,
675
+ ...config
676
+ };
677
+ return {
678
+ Component: ({ row, width }) => /* @__PURE__ */ jsx(Text, {
679
+ wrap: "truncate-end",
680
+ children: renderProgressBar(row.task, width)
681
+ }),
682
+ measure: ({ rows }) => {
683
+ const hasDeterminateRows = rows.some((row) => row.derived.isDeterminate);
684
+ const preferredWidth = hasDeterminateRows ? resolvedConfig.barWidth : 0;
685
+ return {
686
+ minWidth: hasDeterminateRows ? Math.min(resolvedConfig.minWidth, preferredWidth) : 0,
687
+ preferredWidth,
688
+ maxWidth: preferredWidth
689
+ };
673
690
  },
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
- });
691
+ noWrap: false,
692
+ sticky: resolvedConfig.sticky
693
+ };
697
694
  };
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");
706
695
 
707
696
  //#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
- }
733
- });
697
+ //#region src/renderer/hooks/use-spinner-clock.ts
698
+ const normalizeIntervalMillis = (intervalMillis) => Math.max(1, intervalMillis);
699
+ const getSpinnerTickAtTime = (baseTick, startedAt, now, intervalMillis) => {
700
+ const elapsedMillis = Math.max(0, now - startedAt);
701
+ return baseTick + Math.floor(elapsedMillis / normalizeIntervalMillis(intervalMillis));
702
+ };
703
+ const useSpinnerClock = (active, intervalMillis) => {
704
+ const [tick, setTick] = useState(0);
705
+ const tickRef = useRef(tick);
706
+ useEffect(() => {
707
+ tickRef.current = tick;
708
+ }, [tick]);
709
+ useEffect(() => {
710
+ if (!active) return;
711
+ const baseTick = tickRef.current;
712
+ const startedAt = Date.now();
713
+ const updateTick = () => {
714
+ setTick(getSpinnerTickAtTime(baseTick, startedAt, Date.now(), intervalMillis));
715
+ };
716
+ updateTick();
717
+ const interval = setInterval(() => {
718
+ updateTick();
719
+ }, normalizeIntervalMillis(intervalMillis));
720
+ return () => {
721
+ clearInterval(interval);
722
+ };
723
+ }, [active, intervalMillis]);
724
+ return tick;
734
725
  };
735
- const ElapsedRootColumn = createColumnDefinition({ _tag: "elapsed" }, (frame) => ElapsedColumn(frame));
736
726
 
737
727
  //#endregion
738
- //#region src/ink-renderer/columns/determinate.ts
739
- const isDeterminate = (task) => task.units.total !== void 0;
728
+ //#region src/renderer/context/spinner-context.tsx
729
+ const DEFAULT_SPINNER_INTERVAL_MILLIS = cliSpinners.dots.interval;
730
+ const SpinnerContext = createContext(0);
731
+ const SpinnerProvider = ({ active, children, intervalMillis = DEFAULT_SPINNER_INTERVAL_MILLIS, tickOverride }) => {
732
+ const liveTick = useSpinnerClock(active, intervalMillis);
733
+ const tick = tickOverride ?? liveTick;
734
+ return /* @__PURE__ */ jsx(SpinnerContext.Provider, {
735
+ value: tick,
736
+ children
737
+ });
738
+ };
739
+ const useSpinnerTick = () => useContext(SpinnerContext);
740
740
 
741
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 : "--";
742
+ //#region src/renderer/columns/description-column.tsx
743
+ const MIN_TREE_DESCRIPTION_TEXT_WIDTH = 6;
744
+ const defaultDescriptionColumnConfig = {
745
+ minWidth: 1,
746
+ spinnerType: "dots",
747
+ sticky: true
749
748
  };
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);
749
+ const isDeterminate = (task) => task.units.total !== void 0;
750
+ const getSpinnerFrame = (tick, spinnerType) => {
751
+ const frames = cliSpinners[spinnerType].frames;
752
+ return frames[(tick % frames.length + frames.length) % frames.length] ?? frames[0] ?? "";
757
753
  };
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
- }
754
+ const getTaskIndicator = (task, tick, spinnerType = defaultDescriptionColumnConfig.spinnerType) => {
755
+ if (task.status === "running") return {
756
+ symbol: getSpinnerFrame(tick, spinnerType),
757
+ color: "yellow"
758
+ };
759
+ if (task.status === "failed") return {
760
+ symbol: "✗",
761
+ color: "red"
762
+ };
763
+ if (!isDeterminate(task)) return {
764
+ symbol: "✓",
765
+ color: "green"
766
+ };
767
+ const { succeeded, failed, processed, total } = task.units;
768
+ if (failed === 0 && processed === total) return {
769
+ symbol: "✓",
770
+ color: "green"
771
+ };
772
+ if (failed > 0 && succeeded > 0) return {
773
+ symbol: "~",
774
+ color: "yellow"
775
+ };
776
+ if (failed > 0 && succeeded === 0) return {
777
+ symbol: "✗",
778
+ color: "red"
779
+ };
773
780
  return {
774
- hasEta,
775
- prefixedWidth,
776
- durationWidth: Math.max(2, durationWidth),
777
- primaryUnitWidth: Math.max(2, primaryUnitWidth)
781
+ symbol: "✓",
782
+ color: "green"
778
783
  };
779
784
  };
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
- })
785
+ const TaskIndicatorGlyph = ({ task, spinnerType = defaultDescriptionColumnConfig.spinnerType }) => {
786
+ const indicator = getTaskIndicator(task, useSpinnerTick(), spinnerType);
787
+ return /* @__PURE__ */ jsx(Text, {
788
+ color: indicator.color,
789
+ children: indicator.symbol
796
790
  });
797
791
  };
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
- }
792
+ const createDescriptionColumn = (config) => {
793
+ const resolvedConfig = {
794
+ ...defaultDescriptionColumnConfig,
795
+ ...config,
796
+ spinnerType: config?.spinnerType ?? defaultDescriptionColumnConfig.spinnerType
797
+ };
798
+ let minTreeWidth = MIN_TREE_DESCRIPTION_TEXT_WIDTH + 2;
799
+ const Component = ({ row, width }) => {
800
+ const showTree = width >= minTreeWidth;
801
+ const treePrefix = showTree ? row.derived.treePrefix : "";
802
+ if (!showTree && width <= 1) return /* @__PURE__ */ jsx(TaskIndicatorGlyph, {
803
+ task: row.task,
804
+ spinnerType: resolvedConfig.spinnerType
805
+ });
806
+ if (!showTree && width === 2) return /* @__PURE__ */ jsxs(Text, {
807
+ wrap: "truncate-end",
808
+ children: [/* @__PURE__ */ jsx(TaskIndicatorGlyph, {
809
+ task: row.task,
810
+ spinnerType: resolvedConfig.spinnerType
811
+ }), "…"]
812
+ });
813
+ return /* @__PURE__ */ jsxs(Text, {
814
+ wrap: "truncate-end",
815
+ children: [
816
+ treePrefix,
817
+ /* @__PURE__ */ jsx(TaskIndicatorGlyph, {
818
+ task: row.task,
819
+ spinnerType: resolvedConfig.spinnerType
820
+ }),
821
+ ` ${row.task.description}`
822
+ ]
823
+ });
824
+ };
828
825
  return {
829
- hasStructuredCounts,
830
- hasDetailed,
831
- hasDeterminate,
832
- countDigits: Math.max(1, countDigits),
833
- totalWidth: Math.max(1, totalWidth),
834
- simpleTextWidth
826
+ Component,
827
+ measure: ({ rows }) => {
828
+ const hasNestedRows = rows.some((row) => row.tree.depth > 0);
829
+ minTreeWidth = rows.reduce((max, row) => Math.max(max, row.derived.treePrefixWidth + 2 + MIN_TREE_DESCRIPTION_TEXT_WIDTH), MIN_TREE_DESCRIPTION_TEXT_WIDTH + 2);
830
+ return {
831
+ minWidth: resolvedConfig.minWidth,
832
+ preferredWidth: Math.max(rows.reduce((max, row) => Math.max(max, row.derived.treePrefixedDescriptionWidth + 2), resolvedConfig.minWidth), hasNestedRows ? minTreeWidth : resolvedConfig.minWidth),
833
+ maxWidth: void 0
834
+ };
835
+ },
836
+ noWrap: false,
837
+ sticky: resolvedConfig.sticky,
838
+ stickyMaxWidth: resolvedConfig.stickyMaxWidth
835
839
  };
836
840
  };
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
841
 
846
842
  //#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) });
850
- return /* @__PURE__ */ jsx(Text, {
851
- color: "green",
852
- children: padLeft(`${task.units.succeeded}`, width)
853
- });
843
+ //#region src/renderer/hooks/use-now-clock.ts
844
+ const useNowClock = (active, intervalMillis) => {
845
+ const [now, setNow] = useState(() => Date.now());
846
+ useEffect(() => {
847
+ if (!active) return;
848
+ setNow(Date.now());
849
+ const interval = setInterval(() => {
850
+ setNow(Date.now());
851
+ }, intervalMillis);
852
+ return () => {
853
+ clearInterval(interval);
854
+ };
855
+ }, [active, intervalMillis]);
856
+ return now;
854
857
  };
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)
858
+
859
+ //#endregion
860
+ //#region src/renderer/context/now-context.tsx
861
+ const NOW_INTERVAL_MILLIS = 1e3;
862
+ const NowContext = createContext(Date.now());
863
+ const NowProvider = ({ active, children, nowOverride }) => {
864
+ const liveNow = useNowClock(active, NOW_INTERVAL_MILLIS);
865
+ const now = nowOverride ?? liveNow;
866
+ return /* @__PURE__ */ jsx(NowContext.Provider, {
867
+ value: now,
868
+ children
860
869
  });
861
870
  };
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) });
871
+ const useNow = () => useContext(NowContext);
872
+
873
+ //#endregion
874
+ //#region src/renderer/columns/elapsed-column.tsx
875
+ const defaultElapsedColumnConfig = {
876
+ minWidth: 2,
877
+ justify: "right",
878
+ sticky: true
869
879
  };
870
- const structuredAmount = (task, tick, width, layout) => {
871
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, {
880
+ const ElapsedText = ({ task, width }) => {
881
+ return /* @__PURE__ */ jsx(Text, {
872
882
  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
- ]
883
- });
884
- return /* @__PURE__ */ jsxs(Box, {
885
- flexDirection: "row",
886
- width,
887
- 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)
893
- ]
883
+ color: "gray",
884
+ children: formatElapsed(task, useNow()).slice(0, Math.max(0, width))
894
885
  });
895
886
  };
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)
887
+ const createElapsedColumn = (config) => {
888
+ const resolvedConfig = {
889
+ ...defaultElapsedColumnConfig,
890
+ ...config
911
891
  };
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
- }
892
+ const Component = ({ row, width }) => /* @__PURE__ */ jsx(ElapsedText, {
893
+ task: row.task,
894
+ width
939
895
  });
896
+ return {
897
+ Component,
898
+ measure: ({ rows, now }) => {
899
+ const width = rows.reduce((max, row) => Math.max(max, formatElapsed(row.task, now).length), resolvedConfig.minWidth);
900
+ return {
901
+ minWidth: resolvedConfig.minWidth,
902
+ preferredWidth: width,
903
+ maxWidth: width
904
+ };
905
+ },
906
+ getLayoutDependency: ({ rows, now }) => rows.reduce((max, row) => Math.max(max, formatElapsed(row.task, now).length), 0),
907
+ justify: resolvedConfig.justify,
908
+ noWrap: true,
909
+ sticky: resolvedConfig.sticky
910
+ };
940
911
  };
941
912
 
942
913
  //#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
914
+ //#region src/renderer/columns/eta-column.tsx
915
+ const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
916
+ const cropTextToWidth = (text, width) => {
917
+ if (width <= 0) return "";
918
+ return text.slice(0, width);
919
+ };
920
+ const etaDurationText = (row, now) => {
921
+ const eta = formatEta(row.task, now);
922
+ return eta === "" ? void 0 : eta;
923
+ };
924
+ const renderEtaText = (row, now, width) => {
925
+ const duration = etaDurationText(row, now);
926
+ if (duration === void 0) return {
927
+ mode: "compact",
928
+ text: ""
929
+ };
930
+ const prefixed = `ETA: ${duration}`;
931
+ if (width >= prefixed.length) return {
932
+ mode: "prefixed",
933
+ text: prefixed
934
+ };
935
+ if (width >= duration.length) return {
936
+ mode: "duration",
937
+ text: duration
949
938
  };
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
939
  return {
956
- succeeded: succeededLength,
957
- failed: failedLength,
958
- remaining: Math.max(0, width - succeededLength - failedLength)
940
+ mode: "compact",
941
+ text: cropTextToWidth(primaryUnit(duration), width)
959
942
  };
960
943
  };
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
- }
992
- });
944
+ const defaultEtaColumnConfig = {
945
+ minWidth: 3,
946
+ justify: "right",
947
+ sticky: true
993
948
  };
994
-
995
- //#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, {
949
+ const EtaText = ({ row, width, justify }) => {
950
+ const rendered = renderEtaText(row, useNow(), width);
951
+ return /* @__PURE__ */ jsx(Box, {
952
+ width,
953
+ justifyContent: rendered.mode === "prefixed" ? justify === "right" ? "flex-end" : "flex-start" : "flex-start",
954
+ children: /* @__PURE__ */ jsx(Text, {
1007
955
  wrap: "truncate-end",
1008
- color: determinateColor === "green" || determinateColor === "yellow" || determinateColor === "red" ? determinateColor : void 0,
1009
- children: percentText(task)
1010
- });
1011
- }
1012
- });
1013
-
1014
- //#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
- };
956
+ color: "gray",
957
+ children: rendered.text
958
+ })
959
+ });
1039
960
  };
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
961
+ const createEtaColumn = (config) => {
962
+ const resolvedConfig = {
963
+ ...defaultEtaColumnConfig,
964
+ ...config
1052
965
  };
1053
- };
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;
966
+ const Component = ({ row, width }) => /* @__PURE__ */ jsx(EtaText, {
967
+ row,
968
+ width,
969
+ justify: resolvedConfig.justify
970
+ });
1064
971
  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?.();
972
+ Component,
973
+ measure: ({ rows, now }) => {
974
+ const width = rows.reduce((max, row) => {
975
+ const duration = etaDurationText(row, now);
976
+ if (duration === void 0) return max;
977
+ return Math.max(max, `ETA: ${duration}`.length);
978
+ }, resolvedConfig.minWidth);
979
+ return {
980
+ minWidth: resolvedConfig.minWidth,
981
+ preferredWidth: width,
982
+ maxWidth: width
983
+ };
1073
984
  },
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
- }
985
+ getLayoutDependency: ({ rows, now }) => rows.reduce((max, row) => {
986
+ const duration = etaDurationText(row, now);
987
+ if (duration === void 0) return max;
988
+ return Math.max(max, `ETA: ${duration}`.length);
989
+ }, 0),
990
+ justify: resolvedConfig.justify,
991
+ noWrap: true,
992
+ sticky: resolvedConfig.sticky
1094
993
  };
1095
994
  };
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
995
 
1103
996
  //#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]
997
+ //#region src/renderer/default-columns.tsx
998
+ const defaultRendererv2Columns = [
999
+ createDescriptionColumn(),
1000
+ createBarColumn(),
1001
+ createAmountColumn(),
1002
+ createElapsedColumn(),
1003
+ createEtaColumn()
1138
1004
  ];
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, {
1005
+
1006
+ //#endregion
1007
+ //#region src/renderer/width-allocator.ts
1008
+ const RENDERER_COLUMN_GAP = 1;
1009
+ const clampWidth = (width, minWidth, maxWidth) => maxWidth === void 0 ? Math.max(minWidth, width) : Math.max(minWidth, Math.min(maxWidth, width));
1010
+ const normalizeMeasurement = (definition, context) => {
1011
+ if (definition.fixedWidth !== void 0) {
1012
+ const width = Math.max(0, definition.fixedWidth);
1013
+ return {
1014
+ definition,
1015
+ minWidth: width,
1016
+ maxWidth: width,
1017
+ preferredWidth: width,
1018
+ sticky: definition.sticky === true,
1019
+ stickyLimit: width
1020
+ };
1021
+ }
1022
+ const measured = definition.measure(context);
1023
+ const minWidth = Math.max(0, measured.minWidth);
1024
+ const maxWidth = measured.maxWidth === void 0 ? void 0 : Math.max(minWidth, measured.maxWidth);
1025
+ const preferredWidth = clampWidth(measured.preferredWidth, minWidth, maxWidth);
1026
+ const stickyLimit = definition.sticky !== true ? void 0 : definition.stickyMaxWidth !== void 0 ? Math.max(minWidth, definition.stickyMaxWidth) : maxWidth;
1027
+ return {
1145
1028
  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;
1029
+ minWidth,
1030
+ maxWidth,
1031
+ preferredWidth,
1032
+ sticky: definition.sticky === true,
1033
+ stickyLimit
1211
1034
  };
1212
- return candidateRootLayouts(columnsById).map(resolveLayout).filter((columns) => columns !== void 0);
1213
1035
  };
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) ?? [];
1036
+ const visibleIndices = (widths) => widths.flatMap((width, index) => width > 0 ? [index] : []);
1037
+ const totalVisibleWidth = (widths) => {
1038
+ const indices = visibleIndices(widths);
1039
+ if (indices.length === 0) return 0;
1040
+ return indices.reduce((sum, index, visibleIndex) => {
1041
+ const padding = visibleIndex < indices.length - 1 ? RENDERER_COLUMN_GAP : 0;
1042
+ return sum + widths[index] + padding;
1043
+ }, 0);
1219
1044
  };
1220
- const preferredWidthsForSet = (columns) => columns.map((column) => Math.max(column.measure.min, column.preferredWidth));
1221
1045
  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) {
1046
+ const shrinkWidestFirst = (widths, columns, targetWidth, canShrink) => {
1047
+ while (totalVisibleWidth(widths) > targetWidth) {
1225
1048
  const shrinkable = widths.map((width, index) => ({
1226
- width,
1227
1049
  index,
1228
- minimum: minimums[index] ?? width
1229
- })).filter(({ width, minimum }) => width > minimum).sort((left, right) => right.width - left.width || left.index - right.index);
1050
+ width,
1051
+ minWidth: columns[index].minWidth
1052
+ })).filter(({ index, width, minWidth }) => width > minWidth && canShrink(index)).sort((left, right) => right.width - left.width || right.index - left.index);
1230
1053
  if (shrinkable.length === 0) break;
1054
+ const overflow = totalVisibleWidth(widths) - targetWidth;
1231
1055
  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));
1056
+ const cohort = shrinkable.filter((entry) => entry.width === widest);
1057
+ const maxUniformDrop = widest - Math.max(nextDistinctWidth(shrinkable, widest) ?? 0, ...cohort.map((entry) => entry.minWidth));
1235
1058
  const uniformDrop = Math.min(maxUniformDrop, Math.floor(overflow / cohort.length));
1236
1059
  if (uniformDrop > 0) {
1237
- for (const { index } of cohort) widths[index] = widths[index] - uniformDrop;
1238
- overflow -= uniformDrop * cohort.length;
1060
+ for (const { index } of cohort) widths[index] = Math.max(columns[index].minWidth, widths[index] - uniformDrop);
1239
1061
  continue;
1240
1062
  }
1241
1063
  let changed = false;
1242
- for (const { index, minimum } of cohort) {
1243
- if (overflow <= 0) break;
1244
- if (widths[index] <= minimum) continue;
1064
+ for (const { index, minWidth } of cohort) {
1065
+ if (totalVisibleWidth(widths) <= targetWidth) break;
1066
+ if (widths[index] <= minWidth) continue;
1245
1067
  widths[index] = widths[index] - 1;
1246
- overflow -= 1;
1247
1068
  changed = true;
1248
1069
  }
1249
1070
  if (!changed) break;
1250
1071
  }
1251
1072
  return widths;
1252
1073
  };
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);
1074
+ const forceFitFromRight = (widths, columns, targetWidth, canShrink) => {
1075
+ while (totalVisibleWidth(widths) > targetWidth) {
1076
+ const rightmostVisibleIndex = widths.findLastIndex((width, index) => width > 0 && canShrink(index));
1077
+ if (rightmostVisibleIndex < 0) break;
1078
+ widths[rightmostVisibleIndex] = widths[rightmostVisibleIndex] - 1;
1079
+ }
1080
+ return widths;
1081
+ };
1082
+ const applyStickyGrowth = (widths, columns, targetWidth, previousStickyWidths) => {
1083
+ const stickyTargets = columns.map((column, index) => {
1084
+ if (!column.sticky) return;
1085
+ return clampWidth(Math.max(previousStickyWidths.get(index) ?? 0, column.preferredWidth), column.minWidth, column.stickyLimit);
1086
+ });
1087
+ if (targetWidth === void 0) stickyTargets.forEach((target, index) => {
1088
+ if (target !== void 0) widths[index] = Math.max(widths[index], target);
1089
+ });
1090
+ else {
1091
+ let remaining = targetWidth - totalVisibleWidth(widths);
1092
+ if (remaining > 0) stickyTargets.forEach((target, index) => {
1093
+ if (target === void 0 || remaining <= 0) return;
1094
+ const deficit = Math.max(0, target - widths[index]);
1095
+ const growth = Math.min(deficit, remaining);
1096
+ widths[index] = widths[index] + growth;
1097
+ remaining -= growth;
1098
+ });
1099
+ }
1100
+ const nextStickyWidths = /* @__PURE__ */ new Map();
1101
+ stickyTargets.forEach((target, index) => {
1102
+ if (target === void 0) return;
1103
+ nextStickyWidths.set(index, clampWidth(Math.max(target, widths[index]), columns[index].minWidth, columns[index].stickyLimit));
1104
+ });
1105
+ return {
1106
+ widths,
1107
+ nextStickyWidths
1108
+ };
1260
1109
  };
1261
- const emptyRootColumn = (stickyWidths) => {
1262
- stickyWidths.clear();
1263
- return { render: () => null };
1110
+ const planColumnLayout = (definitions, rows, now, terminalColumns, previousStickyWidths) => {
1111
+ const measureContext = {
1112
+ rows,
1113
+ now
1114
+ };
1115
+ const columns = definitions.map((definition) => normalizeMeasurement(definition, measureContext));
1116
+ let widths = columns.map((column) => column.preferredWidth);
1117
+ if (terminalColumns !== void 0) {
1118
+ widths = shrinkWidestFirst([...widths], columns, terminalColumns, (index) => columns[index].definition.fixedWidth === void 0);
1119
+ if (totalVisibleWidth(widths) > terminalColumns) widths = forceFitFromRight([...widths], columns, terminalColumns, (index) => columns[index].definition.fixedWidth === void 0);
1120
+ }
1121
+ const stickyResult = applyStickyGrowth(widths, columns, terminalColumns, previousStickyWidths);
1122
+ return {
1123
+ columns: stickyResult.widths.map((width, index) => ({
1124
+ definition: columns[index].definition,
1125
+ width
1126
+ })).filter(({ width }) => width > 0),
1127
+ nextStickyWidths: stickyResult.nextStickyWidths
1128
+ };
1264
1129
  };
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, {
1277
- flexDirection: "row",
1278
- minWidth: rowWidth,
1279
- children: columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
1280
- flexDirection: "column",
1130
+
1131
+ //#endregion
1132
+ //#region src/renderer/public-api.tsx
1133
+ const ProgressRow = memo(({ row, rowIndex, terminalColumns, columns }) => /* @__PURE__ */ jsx(Box, {
1134
+ flexDirection: "row",
1135
+ width: terminalColumns,
1136
+ columnGap: RENDERER_COLUMN_GAP,
1137
+ children: columns.map((column, columnIndex) => {
1138
+ const Component = column.definition.Component;
1139
+ return /* @__PURE__ */ jsx(Box, {
1140
+ height: 1,
1281
1141
  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
- }) };
1142
+ minWidth: column.width,
1143
+ flexBasis: column.width,
1144
+ flexGrow: 0,
1145
+ flexShrink: 0,
1146
+ justifyContent: column.definition.justify === "right" ? "flex-end" : "flex-start",
1147
+ children: /* @__PURE__ */ jsx(Component, {
1148
+ row,
1149
+ rowIndex,
1150
+ width: column.width
1151
+ })
1152
+ }, columnIndex);
1153
+ })
1154
+ }), (previousProps, nextProps) => previousProps.row === nextProps.row && previousProps.rowIndex === nextProps.rowIndex && previousProps.terminalColumns === nextProps.terminalColumns && previousProps.columns === nextProps.columns);
1155
+ const layoutDependencyKeyFor = (columns, context) => columns.map((column, index) => `${index}:${column.getLayoutDependency?.(context) ?? "static"}`).join("|");
1156
+ const CreateProgressRenderer = (columns) => {
1157
+ return ({ rows, terminalColumns, terminalRows }) => {
1158
+ const stickyWidthsRef = useRef(/* @__PURE__ */ new Map());
1159
+ const now = useNow();
1160
+ const layout = useMemo(() => rows.length === 0 ? {
1161
+ columns: [],
1162
+ nextStickyWidths: /* @__PURE__ */ new Map()
1163
+ } : planColumnLayout(columns, rows, now, terminalColumns, stickyWidthsRef.current), [
1164
+ columns,
1165
+ layoutDependencyKeyFor(columns, {
1166
+ rows,
1167
+ now
1168
+ }),
1169
+ rows,
1170
+ terminalColumns
1171
+ ]);
1172
+ useEffect(() => {
1173
+ stickyWidthsRef.current = rows.length === 0 ? /* @__PURE__ */ new Map() : layout.nextStickyWidths;
1174
+ }, [layout.nextStickyWidths, rows.length]);
1175
+ if (layout.columns.length === 0) return null;
1176
+ return /* @__PURE__ */ jsx(VirtualList, {
1177
+ items: rows,
1178
+ keyExtractor: (row) => `${row.task.id}`,
1179
+ selectedIndex: Math.max(0, rows.length - 1),
1180
+ height: terminalRows ?? "auto",
1181
+ itemHeight: 1,
1182
+ showOverflowIndicators: true,
1183
+ renderItem: ({ item: row, index: rowIndex }) => /* @__PURE__ */ jsx(ProgressRow, {
1184
+ row,
1185
+ rowIndex,
1186
+ terminalColumns,
1187
+ columns: layout.columns
1188
+ })
1189
+ });
1190
+ };
1290
1191
  };
1291
1192
 
1292
1193
  //#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);
1194
+ //#region src/renderer/store/render-snapshot.ts
1195
+ const orderedVisibleTasks = (store) => store.renderOrder.flatMap((row) => {
1196
+ const snapshot = store.tasks.get(row.id);
1197
+ if (!snapshot || snapshot.transient && snapshot.status !== "running") return [];
1198
+ return [{
1199
+ snapshot,
1200
+ depth: row.depth
1201
+ }];
1202
+ });
1203
+ const treeAncestorPrefix = (ancestorHasNextSibling) => ancestorHasNextSibling.slice(1).map((hasNextSibling) => hasNextSibling ? "│ " : " ").join("");
1204
+ const renderTreePrefix = (tree) => {
1205
+ if (tree.depth <= 0) return "";
1206
+ return `${treeAncestorPrefix(tree.ancestorHasNextSibling)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
1207
+ };
1208
+ const arraysEqual = (left, right) => {
1209
+ if (left.length !== right.length) return false;
1210
+ for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return false;
1211
+ return true;
1212
+ };
1213
+ const sameTreePrefixInputs = (left, right) => left.depth === right.depth && left.hasNextSibling === right.hasNextSibling && arraysEqual(left.ancestorHasNextSibling, right.ancestorHasNextSibling);
1214
+ const sameTree = (left, right) => sameTreePrefixInputs(left, right) && left.hasChildren === right.hasChildren;
1215
+ const deriveRow = (task, tree, previousRow) => {
1216
+ if (previousRow !== void 0 && previousRow.task === task && sameTree(previousRow.tree, tree)) return previousRow.derived;
1217
+ const treePrefix = previousRow !== void 0 && sameTreePrefixInputs(previousRow.tree, tree) ? previousRow.derived.treePrefix : renderTreePrefix(tree);
1218
+ const treePrefixWidth = previousRow !== void 0 && sameTreePrefixInputs(previousRow.tree, tree) ? previousRow.derived.treePrefixWidth : treePrefix.length;
1219
+ const descriptionWidth = previousRow !== void 0 && previousRow.task.description === task.description ? previousRow.derived.descriptionWidth : textWidth(task.description);
1220
+ const isDeterminate = previousRow !== void 0 && previousRow.task.units.total === task.units.total ? previousRow.derived.isDeterminate : task.units.total !== void 0;
1221
+ 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;
1222
+ return {
1223
+ treePrefix,
1224
+ treePrefixWidth,
1225
+ descriptionWidth,
1226
+ treePrefixedDescriptionWidth: treePrefixWidth + descriptionWidth,
1227
+ hasRenderableProgress,
1228
+ isDeterminate
1229
+ };
1230
+ };
1231
+ const computeTreeInfo = (ordered, previousRows) => {
1232
+ const hasNextSiblingByIndex = Array.from({ length: ordered.length }, () => false);
1233
+ const seenByDepth = [];
1234
+ for (let i = ordered.length - 1; i >= 0; i--) {
1235
+ const depth = ordered[i].depth;
1236
+ hasNextSiblingByIndex[i] = seenByDepth[depth] ?? false;
1237
+ seenByDepth[depth] = true;
1238
+ seenByDepth.length = depth + 1;
1239
+ }
1240
+ const ancestorStateByDepth = [];
1241
+ const previousRowsByTaskId = new Map(previousRows.map((row) => [row.task.id, row]));
1242
+ return ordered.map((entry, index) => {
1243
+ const depth = entry.depth;
1244
+ ancestorStateByDepth.length = depth;
1245
+ const hasChildren = index + 1 < ordered.length && ordered[index + 1] !== void 0 && ordered[index + 1].depth > depth;
1246
+ const tree = {
1247
+ depth,
1248
+ hasNextSibling: hasNextSiblingByIndex[index] ?? false,
1249
+ hasChildren,
1250
+ ancestorHasNextSibling: [...ancestorStateByDepth]
1304
1251
  };
1305
- }, [active, intervalMillis]);
1306
- return now;
1252
+ const previousRow = previousRowsByTaskId.get(entry.snapshot.id);
1253
+ ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
1254
+ if (previousRow !== void 0 && previousRow.task === entry.snapshot && sameTree(previousRow.tree, tree)) return previousRow;
1255
+ const derived = deriveRow(entry.snapshot, tree, previousRow);
1256
+ return {
1257
+ task: entry.snapshot,
1258
+ tree: previousRow !== void 0 && sameTree(previousRow.tree, tree) ? previousRow.tree : tree,
1259
+ derived
1260
+ };
1261
+ });
1262
+ };
1263
+ const toRenderSnapshot = (store, previousSnapshot) => {
1264
+ const visibleTasks = orderedVisibleTasks(store);
1265
+ const hasRunningTasks = visibleTasks.some((entry) => entry.snapshot.status === "running");
1266
+ return {
1267
+ rows: computeTreeInfo(visibleTasks, previousSnapshot?.rows ?? []),
1268
+ hasRunningTasks
1269
+ };
1307
1270
  };
1308
1271
 
1309
1272
  //#endregion
1310
- //#region src/ink-renderer/view/hooks/use-spinner-clock.ts
1311
- const useSpinnerClock = (active, intervalMillis) => {
1312
- const [tick, setTick] = useState(0);
1273
+ //#region src/renderer/store/use-progress-render-view.ts
1274
+ const useRenderSnapshot = (storeSnapshot) => {
1275
+ const previousSnapshotRef = useRef(void 0);
1276
+ const renderSnapshot = useMemo(() => toRenderSnapshot(storeSnapshot, previousSnapshotRef.current), [storeSnapshot]);
1313
1277
  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;
1278
+ previousSnapshotRef.current = renderSnapshot;
1279
+ }, [renderSnapshot]);
1280
+ return renderSnapshot;
1281
+ };
1282
+ const useProgressRenderView = (store) => {
1283
+ const publication = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1284
+ const renderSnapshot = useRenderSnapshot(publication.snapshot);
1285
+ return {
1286
+ publication,
1287
+ renderSnapshot,
1288
+ hasRunningTasks: renderSnapshot.rows.some((row) => row.task.status === "running")
1289
+ };
1323
1290
  };
1324
1291
 
1325
1292
  //#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();
1293
+ //#region src/renderer/renderer-service.tsx
1294
+ const MAX_FPS = 24;
1295
+ const CreateProgressRoot = (columns) => {
1296
+ const ProgressRenderer = CreateProgressRenderer(columns);
1297
+ return ({ store, getTerminalColumns, getTerminalRows }) => {
1298
+ const { renderSnapshot, hasRunningTasks } = useProgressRenderView(store);
1299
+ return /* @__PURE__ */ jsx(SpinnerProvider, {
1300
+ active: hasRunningTasks,
1301
+ children: /* @__PURE__ */ jsx(NowProvider, {
1302
+ active: hasRunningTasks,
1303
+ children: /* @__PURE__ */ jsx(ProgressRenderer, {
1304
+ rows: renderSnapshot.rows,
1305
+ terminalColumns: getTerminalColumns(),
1306
+ terminalRows: getTerminalRows()
1307
+ })
1308
+ })
1309
+ });
1310
+ };
1311
+ };
1312
+ const makeRendererv2InkRendererService = (columns) => {
1313
+ const ProgressRoot = CreateProgressRoot(columns);
1314
+ return { run: (store, stdio, isTTY) => {
1315
+ const proot = /* @__PURE__ */ jsx(ProgressRoot, {
1316
+ store,
1317
+ getTerminalColumns: () => isTTY ? stdio.stderr.columns : void 0,
1318
+ getTerminalRows: () => isTTY ? stdio.stderr.rows : void 0
1319
+ });
1320
+ return Effect.sync(() => render(proot, {
1321
+ stdout: stdio.stdout,
1322
+ stderr: stdio.stderr,
1323
+ patchConsole: true,
1324
+ exitOnCtrlC: false,
1325
+ debug: false,
1326
+ maxFps: MAX_FPS
1327
+ })).pipe(Effect.flatMap((instance) => Effect.never.pipe(Effect.ensuring(Effect.gen(function* () {
1328
+ store.flush();
1329
+ instance.rerender(proot);
1330
+ yield* Effect.sync(() => {
1331
+ instance.unmount();
1332
+ });
1333
+ })))));
1334
+ } };
1335
1335
  };
1336
1336
 
1337
1337
  //#endregion
1338
1338
  //#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
1339
  var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")() {
1358
- static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeDefaultInkRenderer()));
1340
+ static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeRendererv2InkRendererService(defaultRendererv2Columns)));
1359
1341
  };
1360
1342
 
1361
1343
  //#endregion
@@ -1533,4 +1515,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1533
1515
  })));
1534
1516
 
1535
1517
  //#endregion
1536
- export { Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1518
+ export { Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskSnapshotSchema, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };