effective-progress 0.10.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,11 +1,11 @@
1
+ import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
1
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 stringWidth from "fast-string-width";
4
+ import { Box, Text, render, useBoxMetrics } from "ink";
5
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
  import cliSpinners from "cli-spinners";
7
- import { createContext, memo, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
- import { VirtualList } from "ink-virtual-list";
8
+ import stringWidth from "fast-string-width";
9
9
 
10
10
  //#region src/types.ts
11
11
  const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
@@ -27,7 +27,8 @@ const TaskSnapshotSchema = Schema.Struct({
27
27
  transient: Schema.Boolean,
28
28
  units: TaskUnitsSchema,
29
29
  startedAt: Schema.Number,
30
- completedAt: Schema.NullOr(Schema.Number)
30
+ completedAt: Schema.NullOr(Schema.Number),
31
+ metadata: Schema.Unknown
31
32
  });
32
33
  const TaskSnapshot = (snapshot) => snapshot;
33
34
  var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
@@ -101,7 +102,8 @@ const updatedSnapshot = (snapshot, options) => {
101
102
  transient: options.transient ?? snapshot.transient,
102
103
  units,
103
104
  startedAt: snapshot.startedAt,
104
- completedAt: snapshot.completedAt
105
+ completedAt: snapshot.completedAt,
106
+ metadata: snapshot.metadata
105
107
  });
106
108
  };
107
109
  const withTransient = (snapshot, transient) => TaskSnapshot({
@@ -113,7 +115,8 @@ const withTransient = (snapshot, transient) => TaskSnapshot({
113
115
  transient,
114
116
  units: snapshot.units,
115
117
  startedAt: snapshot.startedAt,
116
- completedAt: snapshot.completedAt
118
+ completedAt: snapshot.completedAt,
119
+ metadata: snapshot.metadata
117
120
  });
118
121
  const findInsertionIndex = (renderOrder, parentId) => {
119
122
  if (parentId === null) return {
@@ -156,7 +159,8 @@ const makeProgressRenderStore = () => {
156
159
  let nextTaskId = 0;
157
160
  let state = {
158
161
  tasks: /* @__PURE__ */ new Map(),
159
- renderOrder: []
162
+ renderOrder: [],
163
+ columns: /* @__PURE__ */ new Map()
160
164
  };
161
165
  let pendingEvents = [];
162
166
  let publishedPublication = {
@@ -245,7 +249,8 @@ const makeProgressRenderStore = () => {
245
249
  transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
246
250
  units,
247
251
  startedAt: now,
248
- completedAt: null
252
+ completedAt: null,
253
+ metadata: options.metadata
249
254
  });
250
255
  updateState((current) => {
251
256
  const nextTasks = new Map(current.tasks);
@@ -259,7 +264,8 @@ const makeProgressRenderStore = () => {
259
264
  return {
260
265
  state: {
261
266
  tasks: nextTasks,
262
- renderOrder: nextRenderOrder
267
+ renderOrder: nextRenderOrder,
268
+ columns: options.columns ? new Map(current.columns).set(taskId, options.columns) : current.columns
263
269
  },
264
270
  events: [new TaskAddedEvent({
265
271
  taskId,
@@ -306,7 +312,8 @@ const makeProgressRenderStore = () => {
306
312
  return {
307
313
  state: {
308
314
  tasks: nextTasks,
309
- renderOrder: current.renderOrder
315
+ renderOrder: current.renderOrder,
316
+ columns: current.columns
310
317
  },
311
318
  events
312
319
  };
@@ -333,12 +340,14 @@ const makeProgressRenderStore = () => {
333
340
  total: currentTask.units.total
334
341
  }),
335
342
  startedAt: currentTask.startedAt,
336
- completedAt: currentTask.completedAt
343
+ completedAt: currentTask.completedAt,
344
+ metadata: currentTask.metadata
337
345
  }));
338
346
  return {
339
347
  state: {
340
348
  tasks: nextTasks,
341
- renderOrder: current.renderOrder
349
+ renderOrder: current.renderOrder,
350
+ columns: current.columns
342
351
  },
343
352
  events: [new TaskAdvancedEvent({
344
353
  taskId,
@@ -369,12 +378,14 @@ const makeProgressRenderStore = () => {
369
378
  total: currentTask.units.total
370
379
  }),
371
380
  startedAt: currentTask.startedAt,
372
- completedAt: currentTask.completedAt
381
+ completedAt: currentTask.completedAt,
382
+ metadata: currentTask.metadata
373
383
  }));
374
384
  return {
375
385
  state: {
376
386
  tasks: nextTasks,
377
- renderOrder: current.renderOrder
387
+ renderOrder: current.renderOrder,
388
+ columns: current.columns
378
389
  },
379
390
  events: [new TaskAdvancedEvent({
380
391
  taskId,
@@ -392,14 +403,21 @@ const makeProgressRenderStore = () => {
392
403
  state: current,
393
404
  events: []
394
405
  };
406
+ if (currentTask.status !== "running") return {
407
+ state: current,
408
+ events: []
409
+ };
395
410
  const nextTasks = new Map(current.tasks);
396
411
  if (currentTask.transient) {
397
412
  const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
398
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);
399
416
  return {
400
417
  state: {
401
418
  tasks: nextTasks,
402
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
419
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId),
420
+ columns: nextColumns
403
421
  },
404
422
  events: [new TaskCompletedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
405
423
  };
@@ -421,12 +439,14 @@ const makeProgressRenderStore = () => {
421
439
  total: currentTask.units.processed
422
440
  }) : currentTask.units,
423
441
  startedAt: currentTask.startedAt,
424
- completedAt: now
442
+ completedAt: now,
443
+ metadata: currentTask.metadata
425
444
  }));
426
445
  return {
427
446
  state: {
428
447
  tasks: nextTasks,
429
- renderOrder: current.renderOrder
448
+ renderOrder: current.renderOrder,
449
+ columns: current.columns
430
450
  },
431
451
  events: [new TaskCompletedEvent({ taskId })]
432
452
  };
@@ -440,14 +460,21 @@ const makeProgressRenderStore = () => {
440
460
  state: current,
441
461
  events: []
442
462
  };
463
+ if (currentTask.status !== "running") return {
464
+ state: current,
465
+ events: []
466
+ };
443
467
  const nextTasks = new Map(current.tasks);
444
468
  if (currentTask.transient) {
445
469
  const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
446
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);
447
473
  return {
448
474
  state: {
449
475
  tasks: nextTasks,
450
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
476
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId),
477
+ columns: nextColumns
451
478
  },
452
479
  events: [new TaskFailedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
453
480
  };
@@ -461,22 +488,133 @@ const makeProgressRenderStore = () => {
461
488
  transient: currentTask.transient,
462
489
  units: currentTask.units,
463
490
  startedAt: currentTask.startedAt,
464
- completedAt: now
491
+ completedAt: now,
492
+ metadata: currentTask.metadata
465
493
  }));
466
494
  return {
467
495
  state: {
468
496
  tasks: nextTasks,
469
- renderOrder: current.renderOrder
497
+ renderOrder: current.renderOrder,
498
+ columns: current.columns
470
499
  },
471
500
  events: [new TaskFailedEvent({ taskId })]
472
501
  };
473
502
  });
474
503
  }),
475
504
  getTask: (taskId) => Effect.sync(() => Option.fromNullable(state.tasks.get(taskId))),
476
- 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
+ })
477
539
  };
478
540
  };
479
541
 
542
+ //#endregion
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;
557
+ };
558
+
559
+ //#endregion
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
+
480
618
  //#endregion
481
619
  //#region src/renderer/shared/determinate.ts
482
620
  const isDeterminate$2 = (task) => task.units.total !== void 0;
@@ -606,38 +744,17 @@ const renderAmount = (task, layout) => {
606
744
  /* @__PURE__ */ jsx(Text, { children: ` ${processed}/${total}` })
607
745
  ] });
608
746
  };
609
- const defaultAmountColumnConfig = {
610
- minWidth: 4,
611
- sticky: true
612
- };
613
- const createAmountColumn = (config) => {
614
- const resolvedConfig = {
615
- ...defaultAmountColumnConfig,
616
- ...config
617
- };
618
- let amountLayout = emptyAmountLayout;
619
- return {
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
- };
631
- },
632
- noWrap: false,
633
- sticky: resolvedConfig.sticky
634
- };
635
- };
747
+ const AmountCell = ({ task, layout }) => /* @__PURE__ */ jsx(Text, {
748
+ wrap: "truncate-end",
749
+ children: renderAmount(task, layout)
750
+ });
636
751
 
637
752
  //#endregion
638
753
  //#region src/renderer/columns/bar-column.tsx
639
- const DEFAULT_BAR_WIDTH = 30;
640
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
+ };
641
758
  const renderProgressBar = (task, width) => {
642
759
  if (!isDeterminate$2(task)) return /* @__PURE__ */ jsx(Text, { children: ` `.repeat(Math.max(0, width)) });
643
760
  const displayTotal = Math.max(task.units.total, task.units.succeeded + task.units.failed);
@@ -664,94 +781,21 @@ const renderProgressBar = (task, width) => {
664
781
  ]
665
782
  });
666
783
  };
667
- const defaultBarColumnConfig = {
668
- minWidth: 4,
669
- barWidth: DEFAULT_BAR_WIDTH,
670
- sticky: true
671
- };
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
- };
690
- },
691
- noWrap: false,
692
- sticky: resolvedConfig.sticky
693
- };
694
- };
695
-
696
- //#endregion
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;
725
- };
726
-
727
- //#endregion
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);
784
+ const BarCell = ({ task, width }) => /* @__PURE__ */ jsx(Text, {
785
+ wrap: "truncate-end",
786
+ children: renderProgressBar(task, width ?? 0)
787
+ });
740
788
 
741
789
  //#endregion
742
790
  //#region src/renderer/columns/description-column.tsx
743
791
  const MIN_TREE_DESCRIPTION_TEXT_WIDTH = 6;
744
- const defaultDescriptionColumnConfig = {
745
- minWidth: 1,
746
- spinnerType: "dots",
747
- sticky: true
748
- };
792
+ const DEFAULT_SPINNER_TYPE = "dots";
749
793
  const isDeterminate = (task) => task.units.total !== void 0;
750
794
  const getSpinnerFrame = (tick, spinnerType) => {
751
795
  const frames = cliSpinners[spinnerType].frames;
752
796
  return frames[(tick % frames.length + frames.length) % frames.length] ?? frames[0] ?? "";
753
797
  };
754
- const getTaskIndicator = (task, tick, spinnerType = defaultDescriptionColumnConfig.spinnerType) => {
798
+ const getTaskIndicator = (task, tick, spinnerType = DEFAULT_SPINNER_TYPE) => {
755
799
  if (task.status === "running") return {
756
800
  symbol: getSpinnerFrame(tick, spinnerType),
757
801
  color: "yellow"
@@ -782,412 +826,246 @@ const getTaskIndicator = (task, tick, spinnerType = defaultDescriptionColumnConf
782
826
  color: "green"
783
827
  };
784
828
  };
785
- const TaskIndicatorGlyph = ({ task, spinnerType = defaultDescriptionColumnConfig.spinnerType }) => {
786
- const indicator = getTaskIndicator(task, useSpinnerTick(), spinnerType);
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);
787
832
  return /* @__PURE__ */ jsx(Text, {
788
833
  color: indicator.color,
789
834
  children: indicator.symbol
790
835
  });
791
836
  };
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
- };
825
- return {
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
839
- };
840
- };
841
-
842
- //#endregion
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;
857
- };
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
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
843
+ });
844
+ if (!showTree && width !== void 0 && width === 2) return /* @__PURE__ */ jsxs(Text, {
845
+ wrap: "truncate-end",
846
+ children: [/* @__PURE__ */ jsx(TaskIndicatorGlyph, {
847
+ task: cell.task,
848
+ tick: spinnerTick
849
+ }), "…"]
850
+ });
851
+ return /* @__PURE__ */ jsxs(Text, {
852
+ wrap: "truncate-end",
853
+ children: [
854
+ treePrefix,
855
+ /* @__PURE__ */ jsx(TaskIndicatorGlyph, {
856
+ task: cell.task,
857
+ tick: spinnerTick
858
+ }),
859
+ ` ${cell.task.description}`
860
+ ]
869
861
  });
870
862
  };
871
- const useNow = () => useContext(NowContext);
872
863
 
873
864
  //#endregion
874
865
  //#region src/renderer/columns/elapsed-column.tsx
875
- const defaultElapsedColumnConfig = {
876
- minWidth: 2,
877
- justify: "right",
878
- sticky: true
879
- };
880
- const ElapsedText = ({ task, width }) => {
866
+ const ElapsedCell = ({ task, now }) => {
881
867
  return /* @__PURE__ */ jsx(Text, {
882
868
  wrap: "truncate-end",
883
869
  color: "gray",
884
- children: formatElapsed(task, useNow()).slice(0, Math.max(0, width))
870
+ children: formatElapsed(task, now)
885
871
  });
886
872
  };
887
- const createElapsedColumn = (config) => {
888
- const resolvedConfig = {
889
- ...defaultElapsedColumnConfig,
890
- ...config
891
- };
892
- const Component = ({ row, width }) => /* @__PURE__ */ jsx(ElapsedText, {
893
- task: row.task,
894
- width
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
- };
911
- };
912
873
 
913
874
  //#endregion
914
875
  //#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
938
- };
939
- return {
940
- mode: "compact",
941
- text: cropTextToWidth(primaryUnit(duration), width)
942
- };
943
- };
944
- const defaultEtaColumnConfig = {
945
- minWidth: 3,
946
- justify: "right",
947
- sticky: true
948
- };
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, {
955
- wrap: "truncate-end",
956
- color: "gray",
957
- children: rendered.text
958
- })
959
- });
960
- };
961
- const createEtaColumn = (config) => {
962
- const resolvedConfig = {
963
- ...defaultEtaColumnConfig,
964
- ...config
965
- };
966
- const Component = ({ row, width }) => /* @__PURE__ */ jsx(EtaText, {
967
- row,
968
- width,
969
- justify: resolvedConfig.justify
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}`
970
883
  });
971
- return {
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
- };
984
- },
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
993
- };
994
884
  };
995
885
 
996
886
  //#endregion
997
- //#region src/renderer/default-columns.tsx
998
- const defaultRendererv2Columns = [
999
- createDescriptionColumn(),
1000
- createBarColumn(),
1001
- createAmountColumn(),
1002
- createElapsedColumn(),
1003
- createEtaColumn()
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
+ })
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()
1004
957
  ];
958
+ const resolveColumnSizeValue = (value, prepared) => {
959
+ if (typeof value === "function") return value(prepared);
960
+ return value;
961
+ };
1005
962
 
1006
963
  //#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 {
1028
- definition,
1029
- minWidth,
1030
- maxWidth,
1031
- preferredWidth,
1032
- sticky: definition.sticky === true,
1033
- stickyLimit
1034
- };
1035
- };
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);
1044
- };
1045
- const nextDistinctWidth = (entries, widest) => entries.find((entry) => entry.width < widest)?.width;
1046
- const shrinkWidestFirst = (widths, columns, targetWidth, canShrink) => {
1047
- while (totalVisibleWidth(widths) > targetWidth) {
1048
- const shrinkable = widths.map((width, index) => ({
1049
- 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);
1053
- if (shrinkable.length === 0) break;
1054
- const overflow = totalVisibleWidth(widths) - targetWidth;
1055
- const widest = shrinkable[0].width;
1056
- const cohort = shrinkable.filter((entry) => entry.width === widest);
1057
- const maxUniformDrop = widest - Math.max(nextDistinctWidth(shrinkable, widest) ?? 0, ...cohort.map((entry) => entry.minWidth));
1058
- const uniformDrop = Math.min(maxUniformDrop, Math.floor(overflow / cohort.length));
1059
- if (uniformDrop > 0) {
1060
- for (const { index } of cohort) widths[index] = Math.max(columns[index].minWidth, widths[index] - uniformDrop);
1061
- continue;
1062
- }
1063
- let changed = false;
1064
- for (const { index, minWidth } of cohort) {
1065
- if (totalVisibleWidth(widths) <= targetWidth) break;
1066
- if (widths[index] <= minWidth) continue;
1067
- widths[index] = widths[index] - 1;
1068
- changed = true;
1069
- }
1070
- if (!changed) break;
1071
- }
1072
- return widths;
1073
- };
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);
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]);
1086
979
  });
1087
- if (targetWidth === void 0) stickyTargets.forEach((target, index) => {
1088
- if (target !== void 0) widths[index] = Math.max(widths[index], target);
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)
1089
987
  });
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;
988
+ return groups;
989
+ };
990
+ const getPreparedFor = (def, groups) => {
991
+ if (!def?.prepare) return;
992
+ return groups.find((group) => group.key === def.prepare)?.prepared;
993
+ };
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)
1098
1004
  });
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));
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
+ };
1104
1014
  });
1105
- return {
1106
- widths,
1107
- nextStickyWidths
1108
- };
1109
- };
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
- };
1129
1015
  };
1130
1016
 
1131
1017
  //#endregion
1132
1018
  //#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,
1141
- width: column.width,
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
- };
1019
+ const justifyContentForAlign = (align) => {
1020
+ if (align === "right") return "flex-end";
1021
+ if (align === "center") return "center";
1022
+ return "flex-start";
1023
+ };
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;
1030
+ };
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
+ });
1060
+ };
1061
+ const ProgressRenderer = ({ rows, columns }) => {
1062
+ if (rows.length === 0) return null;
1063
+ return /* @__PURE__ */ jsx(Box, {
1064
+ flexDirection: "row",
1065
+ columnGap: 1,
1066
+ overflow: "hidden",
1067
+ children: resolveColumns(rows, columns).map((position) => /* @__PURE__ */ jsx(ColumnPosition, { position }, position.index))
1068
+ });
1191
1069
  };
1192
1070
 
1193
1071
  //#endregion
@@ -1292,31 +1170,22 @@ const useProgressRenderView = (store) => {
1292
1170
  //#endregion
1293
1171
  //#region src/renderer/renderer-service.tsx
1294
1172
  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, {
1173
+ const ProgressRoot = ({ store }) => {
1174
+ const { renderSnapshot, hasRunningTasks, publication } = useProgressRenderView(store);
1175
+ return /* @__PURE__ */ jsx(SpinnerProvider, {
1176
+ active: hasRunningTasks,
1177
+ children: /* @__PURE__ */ jsx(NowProvider, {
1300
1178
  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
- })
1179
+ children: /* @__PURE__ */ jsx(ProgressRenderer, {
1180
+ rows: renderSnapshot.rows,
1181
+ columns: publication.snapshot.columns
1308
1182
  })
1309
- });
1310
- };
1183
+ })
1184
+ });
1311
1185
  };
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
- });
1186
+ const makeRendererv2InkRendererService = () => {
1187
+ return { run: (store, stdio) => {
1188
+ const proot = /* @__PURE__ */ jsx(ProgressRoot, { store });
1320
1189
  return Effect.sync(() => render(proot, {
1321
1190
  stdout: stdio.stdout,
1322
1191
  stderr: stdio.stderr,
@@ -1337,7 +1206,7 @@ const makeRendererv2InkRendererService = (columns) => {
1337
1206
  //#endregion
1338
1207
  //#region src/services/ink-renderer.tsx
1339
1208
  var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")() {
1340
- static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeRendererv2InkRendererService(defaultRendererv2Columns)));
1209
+ static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeRendererv2InkRendererService()));
1341
1210
  };
1342
1211
 
1343
1212
  //#endregion
@@ -1352,16 +1221,16 @@ var ProgressStdio = class ProgressStdio extends Context.Tag("stromseng.dev/effec
1352
1221
 
1353
1222
  //#endregion
1354
1223
  //#region src/services/progress.ts
1224
+ /** Builds the scoped implementation used by `ProgressService.task(...)` without auto-providing services. */
1355
1225
  const makeProgressService = Effect.gen(function* () {
1356
1226
  const stdio = yield* ProgressStdio;
1357
1227
  const inkRenderer = yield* InkRenderer;
1358
1228
  const outerConsole = yield* Effect.console;
1359
- const isTTY = Boolean(stdio.stderr.isTTY);
1360
1229
  const store = makeProgressRenderStore();
1361
1230
  const currentParentRef = yield* FiberRef.make(Option.none());
1362
1231
  const scope = yield* Effect.scope;
1363
1232
  const log = (...args) => args.length === 0 ? Effect.void : outerConsole.log(...args);
1364
- yield* Effect.forkIn(inkRenderer.run(store, stdio, isTTY), scope);
1233
+ yield* Effect.forkIn(inkRenderer.run(store, stdio), scope);
1365
1234
  yield* Effect.sleep("0 millis");
1366
1235
  const addTask = (options) => Effect.gen(function* () {
1367
1236
  const resolvedParentId = options.parentId === void 0 ? yield* FiberRef.get(currentParentRef) : Option.some(options.parentId);
@@ -1377,7 +1246,27 @@ const makeProgressService = Effect.gen(function* () {
1377
1246
  const failTask = store.failTask;
1378
1247
  const getTask = store.getTask;
1379
1248
  const listTasks = store.listTasks;
1380
- 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* () {
1381
1270
  const inheritedParentId = yield* FiberRef.get(currentParentRef);
1382
1271
  const resolvedParentId = options.parentId === void 0 ? inheritedParentId : Option.some(options.parentId);
1383
1272
  const taskId = yield* addTask({
@@ -1397,17 +1286,29 @@ const makeProgressService = Effect.gen(function* () {
1397
1286
  log,
1398
1287
  getTask,
1399
1288
  listTasks,
1400
- runTask,
1401
- withTask: dual(2, (effect, options) => runTask(Effect.gen(function* () {
1402
- const taskId = yield* Task;
1403
- const exit = yield* Effect.exit(effect);
1404
- if (Exit.isSuccess(exit)) yield* completeTask(taskId);
1405
- else yield* failTask(taskId);
1406
- return yield* Exit.match(exit, {
1407
- onFailure: Effect.failCause,
1408
- onSuccess: Effect.succeed
1409
- });
1410
- }), 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
+ })
1411
1312
  };
1412
1313
  return Progress.of(service);
1413
1314
  });
@@ -1439,9 +1340,17 @@ const provideProgress = (effect) => Effect.gen(function* () {
1439
1340
  if (Option.isSome(existing)) return yield* Effect.provideService(effect, Progress, existing.value);
1440
1341
  return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
1441
1342
  });
1442
- 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) => {
1443
1352
  return provideProgress(Effect.gen(function* () {
1444
- return yield* (yield* Progress).withTask(effect, options);
1353
+ return yield* (yield* Progress).task(effectOrCallback, options);
1445
1354
  }));
1446
1355
  });
1447
1356
  const wrapEffects = (effects, tap) => Array.isArray(effects) ? effects.map(tap) : Object.fromEntries(Object.entries(effects).map(([k, effect]) => [k, tap(effect)]));
@@ -1464,10 +1373,14 @@ const isTaskFullyProcessed = (progress, taskId) => Effect.gen(function* () {
1464
1373
  const { processed, total } = taskOption.value.units;
1465
1374
  return total !== void 0 && processed >= total;
1466
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
+ */
1467
1380
  const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* () {
1468
1381
  const progress = yield* Progress;
1469
- return yield* progress.runTask(Effect.gen(function* () {
1470
- const taskId = yield* Task;
1382
+ return yield* progress.task((handle) => Effect.gen(function* () {
1383
+ const taskId = handle.id;
1471
1384
  const exit = yield* Effect.exit(Effect.all(wrapEffects(effects, (effect) => wrapTrackedEffect(progress, taskId, effect)), {
1472
1385
  concurrency: options.concurrency,
1473
1386
  batching: options.batching,
@@ -1490,10 +1403,13 @@ const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* (
1490
1403
  countDisplay: allCountDisplay(options.mode)
1491
1404
  });
1492
1405
  })));
1406
+ /**
1407
+ * Runs `Effect.forEach` under a single parent task and advances the task counters as items finish.
1408
+ */
1493
1409
  const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(function* () {
1494
1410
  const progress = yield* Progress;
1495
- return yield* progress.runTask(Effect.gen(function* () {
1496
- const taskId = yield* Task;
1411
+ return yield* progress.task((handle) => Effect.gen(function* () {
1412
+ const taskId = handle.id;
1497
1413
  const exit = yield* Effect.exit(Effect.forEach(iterable, (item, index) => wrapTrackedEffect(progress, taskId, f(item, index)), {
1498
1414
  concurrency: options.concurrency,
1499
1415
  batching: options.batching,
@@ -1515,4 +1431,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1515
1431
  })));
1516
1432
 
1517
1433
  //#endregion
1518
- export { Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskSnapshotSchema, 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 };