effective-progress 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -7
  2. package/dist/index.mjs +649 -737
  3. package/package.json +2 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Schema } from "effect";
1
+ import { Brand, Cause, Clock, Context, Data, Effect, Exit, FiberRef, Hash, Layer, Option, Schema } from "effect";
2
2
  import { dual } from "effect/Function";
3
3
  import { Box, Text, render } from "ink";
4
- import { useEffect, useState, useSyncExternalStore } from "react";
4
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
5
5
  import stringWidth from "fast-string-width";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
 
@@ -113,14 +113,13 @@ const toRenderSnapshot = (store) => {
113
113
  //#endregion
114
114
  //#region src/ink-renderer/store.ts
115
115
  const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
116
- const DEFAULT_TOTAL = 100;
117
116
  const sanitizeTotalOnAdd = (total) => {
118
117
  if (total === void 0) return;
119
- return total < 0 ? DEFAULT_TOTAL : total;
118
+ return total < 0 ? void 0 : total;
120
119
  };
121
- const sanitizeTotalOnUpdate = (currentTotal, nextTotal) => {
120
+ const sanitizeTotalOnUpdate = (nextTotal) => {
122
121
  if (nextTotal === void 0) return;
123
- return nextTotal < 0 ? currentTotal : nextTotal;
122
+ return nextTotal < 0 ? void 0 : nextTotal;
124
123
  };
125
124
  const normalizeUnits = (counts) => {
126
125
  const succeeded = Math.max(0, counts.succeeded);
@@ -141,7 +140,7 @@ const updatedSnapshot = (snapshot, options) => {
141
140
  const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? currentUnits : normalizeUnits({
142
141
  succeeded: options.succeeded ?? currentUnits.succeeded,
143
142
  failed: options.failed ?? currentUnits.failed,
144
- total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(currentUnits.total, options.total) : currentUnits.total
143
+ total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(options.total) : currentUnits.total
145
144
  });
146
145
  return new TaskSnapshot({
147
146
  id: snapshot.id,
@@ -448,6 +447,33 @@ const makeProgressRenderStore = () => {
448
447
  };
449
448
  };
450
449
 
450
+ //#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
+ };
476
+
451
477
  //#endregion
452
478
  //#region src/ink-renderer/format.ts
453
479
  const SPINNER_FRAMES = [
@@ -530,6 +556,16 @@ const formatDeterminateAmountParts = (task) => {
530
556
  total: totalText
531
557
  };
532
558
  };
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
+ };
533
569
  const formatAmount = (task, _tick) => {
534
570
  if (isDeterminate$1(task)) {
535
571
  const parts = formatDeterminateAmountParts(task);
@@ -546,12 +582,49 @@ const formatAmount = (task, _tick) => {
546
582
  };
547
583
 
548
584
  //#endregion
549
- //#region src/ink-renderer/columns/determinate.ts
550
- const isDeterminate = (task) => task.units.total !== void 0;
551
- const hasDeterminateRows = (rows) => rows.some((row) => isDeterminate(row.task));
585
+ //#region src/ink-renderer/columns/node.ts
586
+ const createColumnDefinition = (config, create) => {
587
+ return {
588
+ id: Hash.hash(Data.struct(config)).toString(36),
589
+ build: (frame) => create(frame, config)
590
+ };
591
+ };
552
592
 
553
593
  //#endregion
554
- //#region src/ink-renderer/columns/spec.ts
594
+ //#region src/ink-renderer/columns/sticky-width.ts
595
+ const applyStickyWidth = ({ key, measure, stickyWidths }) => {
596
+ const preferred = Math.max(measure.preferred, stickyWidths.get(key) ?? 0);
597
+ const max = measure.max === void 0 ? void 0 : Math.max(measure.max, preferred);
598
+ return {
599
+ ...measure,
600
+ preferred,
601
+ max
602
+ };
603
+ };
604
+ const commitStickyWidth = ({ key, measure, stickyWidths }) => {
605
+ stickyWidths.set(key, measure.preferred);
606
+ };
607
+ const createStickyColumn = ({ frame, measure: baseMeasure, render, stickyKey }) => {
608
+ const measure = stickyKey === void 0 ? baseMeasure : applyStickyWidth({
609
+ key: stickyKey,
610
+ measure: baseMeasure,
611
+ stickyWidths: frame.stickyWidths
612
+ });
613
+ return {
614
+ measure,
615
+ commitStickyWidth: stickyKey === void 0 ? void 0 : () => {
616
+ commitStickyWidth({
617
+ key: stickyKey,
618
+ measure,
619
+ stickyWidths: frame.stickyWidths
620
+ });
621
+ },
622
+ render
623
+ };
624
+ };
625
+
626
+ //#endregion
627
+ //#region src/ink-renderer/columns/text-width.ts
555
628
  const WIDTH_CACHE_LIMIT = 4096;
556
629
  const widthCache = /* @__PURE__ */ new Map();
557
630
  const textWidth = (text) => {
@@ -562,140 +635,187 @@ const textWidth = (text) => {
562
635
  widthCache.set(text, width);
563
636
  return width;
564
637
  };
565
- const resolveColumnSpec = (spec, resistance) => ({
566
- id: spec.id,
567
- grow: spec.grow,
568
- canHide: spec.canHide,
569
- variants: spec.variants.map((variant) => ({
570
- id: variant.id,
571
- minWidth: variant.minWidth,
572
- idealWidth: variant.idealWidth,
573
- maxWidth: variant.maxWidth,
574
- shrinkResistance: resistance.shrink(spec.id, variant.id),
575
- demoteResistance: resistance.demote(spec.id, variant.id),
576
- hideResistance: resistance.hide(spec.id, variant.id),
577
- renderCell: variant.renderCell
578
- }))
579
- });
580
- const resolveColumnSpecs = (specs, resistance) => specs.flatMap((spec) => spec.variants.length > 0 ? [resolveColumnSpec(spec, resistance)] : []);
581
638
 
582
639
  //#endregion
583
- //#region src/ink-renderer/columns/amount-column.tsx
584
- const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
585
- const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
586
- const blank = (width) => " ".repeat(Math.max(0, width));
587
- const shouldShowCountAmount = (task) => task.units.total !== void 0 || task.units.processed > 0;
588
- const shouldShowDetailedCounts = (task) => shouldShowCountAmount(task) && task.countDisplay === "detailed";
589
- const SucceededCountColumn = ({ task, width }) => {
590
- if (width <= 0) return null;
591
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
592
- if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
593
- return /* @__PURE__ */ jsx(Text, {
594
- color: "green",
595
- children: padLeft(`${task.units.succeeded}`, width)
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 ? "├─ " : "└─ "}`;
652
+ };
653
+ const maxDescriptionWidth = (frame, showTree) => frame.taskIds.reduce((max, taskId) => {
654
+ const treePrefix = showTree ? renderTreePrefix(frame.getTree(taskId)) : "";
655
+ return Math.max(max, textWidth(`${treePrefix}${frame.getTask(taskId).description}`) + 2);
656
+ }, MIN_PLAIN_DESCRIPTION_WIDTH);
657
+ const minTreeDescriptionWidth = (frame) => frame.taskIds.reduce((max, taskId) => {
658
+ const treePrefixWidth = textWidth(renderTreePrefix(frame.getTree(taskId)));
659
+ return Math.max(max, treePrefixWidth + 2 + MIN_TREE_DESCRIPTION_TEXT_WIDTH);
660
+ }, MIN_PLAIN_DESCRIPTION_WIDTH);
661
+ const DescriptionColumn = (frame, config = {}) => {
662
+ const variant = config.variant ?? "plain";
663
+ const showTree = variant === "tree";
664
+ const stickyKey = variant === "tree" ? DESCRIPTION_TREE_STICKY_KEY : variant === "plain" ? DESCRIPTION_PLAIN_STICKY_KEY : variant === "compact" ? DESCRIPTION_COMPACT_STICKY_KEY : void 0;
665
+ const min = variant === "spinner" ? MIN_SPINNER_WIDTH : variant === "compact" ? MIN_COMPACT_DESCRIPTION_WIDTH : variant === "tree" ? minTreeDescriptionWidth(frame) : MIN_PLAIN_DESCRIPTION_WIDTH;
666
+ const preferred = variant === "spinner" ? MIN_SPINNER_WIDTH : Math.max(min, maxDescriptionWidth(frame, showTree));
667
+ return createStickyColumn({
668
+ frame,
669
+ measure: {
670
+ min,
671
+ preferred,
672
+ max: preferred
673
+ },
674
+ stickyKey,
675
+ render: (taskId) => {
676
+ const task = frame.getTask(taskId);
677
+ const tree = frame.getTree(taskId);
678
+ const treePrefix = showTree ? renderTreePrefix(tree) : "";
679
+ const indicator = getTaskIndicator(task, frame.tick);
680
+ if (variant === "spinner") return /* @__PURE__ */ jsx(Text, {
681
+ color: indicator.color,
682
+ children: indicator.symbol
683
+ });
684
+ return /* @__PURE__ */ jsxs(Text, {
685
+ wrap: "truncate-end",
686
+ children: [
687
+ treePrefix,
688
+ /* @__PURE__ */ jsx(Text, {
689
+ color: indicator.color,
690
+ children: indicator.symbol
691
+ }),
692
+ ` ${task.description}`
693
+ ]
694
+ });
695
+ }
596
696
  });
597
697
  };
598
- const FailedCountColumn = ({ task, width }) => {
599
- if (width <= 0) return null;
600
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
601
- if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
602
- return /* @__PURE__ */ jsx(Text, {
603
- color: "red",
604
- children: padLeft(`${task.units.failed}`, width)
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
+
707
+ //#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
+ }
605
733
  });
606
734
  };
607
- const ProcessedCountColumn = ({ task, width }) => {
608
- if (width <= 0) return null;
609
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
610
- return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
735
+ const ElapsedRootColumn = createColumnDefinition({ _tag: "elapsed" }, (frame) => ElapsedColumn(frame));
736
+
737
+ //#endregion
738
+ //#region src/ink-renderer/columns/determinate.ts
739
+ const isDeterminate = (task) => task.units.total !== void 0;
740
+
741
+ //#endregion
742
+ //#region src/ink-renderer/columns/eta-column.tsx
743
+ const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
744
+ const ETA_STICKY_KEY = Symbol("eta");
745
+ const etaDurationText = (task, now) => {
746
+ if (task.status !== "running" || !isDeterminate(task)) return;
747
+ const eta = formatEta(task, now);
748
+ return eta.length > 0 ? eta : "--";
611
749
  };
612
- const AmountSeparatorColumn = ({ task, tick }) => {
613
- if (shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: "/" });
614
- const symbol = formatAmount(task, tick);
615
- if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
616
- color: "red",
617
- children: symbol
618
- });
619
- if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
620
- color: "yellow",
621
- children: symbol
622
- });
623
- return /* @__PURE__ */ jsx(Text, { children: symbol });
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);
624
757
  };
625
- const TotalCountColumn = ({ task, width }) => {
626
- if (width <= 0) return null;
627
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
628
- return /* @__PURE__ */ jsx(Text, { children: padRight(isDeterminate(task) ? `${task.units.total}` : "?", width) });
758
+ const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
759
+ const computeEtaMetrics = (frame) => {
760
+ let hasEta = false;
761
+ let prefixedWidth = 0;
762
+ let durationWidth = 0;
763
+ let primaryUnitWidth = 0;
764
+ for (const taskId of frame.taskIds) {
765
+ const duration = etaDurationText(frame.getTask(taskId), frame.now);
766
+ if (duration === void 0) continue;
767
+ hasEta = true;
768
+ const prefixed = `ETA: ${duration}`;
769
+ prefixedWidth = Math.max(prefixedWidth, textWidth(prefixed));
770
+ durationWidth = Math.max(durationWidth, textWidth(duration));
771
+ primaryUnitWidth = Math.max(primaryUnitWidth, textWidth(primaryUnit(duration)));
772
+ }
773
+ return {
774
+ hasEta,
775
+ prefixedWidth,
776
+ durationWidth: Math.max(2, durationWidth),
777
+ primaryUnitWidth: Math.max(2, primaryUnitWidth)
778
+ };
629
779
  };
630
- const AmountColumn = ({ task, tick, layout }) => {
631
- if (layout.kind === "text") {
632
- const text = formatAmount(task, tick);
633
- if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
634
- wrap: "truncate-end",
635
- color: "red",
636
- children: text
637
- });
638
- if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
639
- wrap: "truncate-end",
640
- color: "yellow",
641
- children: text
642
- });
643
- return /* @__PURE__ */ jsx(Text, {
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, {
644
792
  wrap: "truncate-end",
645
- children: text
646
- });
647
- }
648
- if (layout.kind === "processed") return /* @__PURE__ */ jsxs(Box, {
649
- flexDirection: "row",
650
- children: [
651
- /* @__PURE__ */ jsx(ProcessedCountColumn, {
652
- task,
653
- width: layout.processedWidth
654
- }),
655
- /* @__PURE__ */ jsx(AmountSeparatorColumn, {
656
- task,
657
- tick
658
- }),
659
- /* @__PURE__ */ jsx(TotalCountColumn, {
660
- task,
661
- width: layout.totalWidth
662
- })
663
- ]
664
- });
665
- return /* @__PURE__ */ jsxs(Box, {
666
- flexDirection: "row",
667
- children: [
668
- layout.succeededWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SucceededCountColumn, {
669
- task,
670
- width: layout.succeededWidth
671
- }), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
672
- layout.failedWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FailedCountColumn, {
673
- task,
674
- width: layout.failedWidth
675
- }), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
676
- /* @__PURE__ */ jsx(ProcessedCountColumn, {
677
- task,
678
- width: layout.processedWidth
679
- }),
680
- /* @__PURE__ */ jsx(AmountSeparatorColumn, {
681
- task,
682
- tick
683
- }),
684
- /* @__PURE__ */ jsx(TotalCountColumn, {
685
- task,
686
- width: layout.totalWidth
687
- })
688
- ]
793
+ color: "gray",
794
+ children: renderEtaText(frame.getTask(taskId), frame.now, width)
795
+ })
689
796
  });
690
797
  };
691
- const computeAmountMetrics = (rows, tick) => {
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) => {
692
810
  let hasStructuredCounts = false;
693
811
  let hasDetailed = false;
812
+ let hasDeterminate = false;
694
813
  let countDigits = 0;
695
814
  let totalWidth = 0;
696
815
  let simpleTextWidth = 0;
697
- for (const row of rows) {
698
- const { task } = row;
816
+ for (const taskId of frame.taskIds) {
817
+ const task = frame.getTask(taskId);
818
+ hasDeterminate ||= isDeterminate(task);
699
819
  if (shouldShowCountAmount(task)) {
700
820
  hasStructuredCounts = true;
701
821
  countDigits = Math.max(countDigits, textWidth(`${task.units.succeeded}`), textWidth(`${task.units.failed}`), textWidth(`${task.units.processed}`));
@@ -703,85 +823,124 @@ const computeAmountMetrics = (rows, tick) => {
703
823
  if (task.countDisplay === "detailed") hasDetailed = true;
704
824
  continue;
705
825
  }
706
- simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, tick)));
826
+ simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, frame.tick)));
707
827
  }
708
828
  return {
709
829
  hasStructuredCounts,
710
830
  hasDetailed,
831
+ hasDeterminate,
711
832
  countDigits: Math.max(1, countDigits),
712
833
  totalWidth: Math.max(1, totalWidth),
713
834
  simpleTextWidth
714
835
  };
715
836
  };
716
- const detailedAmountLayout = (metrics) => ({
717
- kind: "detailed",
718
- succeededWidth: metrics.hasDetailed ? metrics.countDigits : 0,
719
- failedWidth: metrics.hasDetailed ? metrics.countDigits : 0,
720
- processedWidth: metrics.countDigits,
721
- totalWidth: metrics.totalWidth
722
- });
723
- const processedAmountLayout = (metrics) => ({
724
- kind: "processed",
725
- processedWidth: metrics.countDigits,
726
- totalWidth: metrics.totalWidth
727
- });
728
- const detailedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth + (metrics.hasDetailed ? metrics.countDigits + 1 + metrics.countDigits + 1 : 0);
729
837
  const processedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth;
730
- const createAmountColumnSpec = (context) => {
731
- const metrics = computeAmountMetrics(context.rows, context.tick);
732
- if (!metrics.hasStructuredCounts && metrics.simpleTextWidth <= 0) return;
733
- const detailedLayout = detailedAmountLayout(metrics);
734
- const processedLayout = processedAmountLayout(metrics);
838
+ const detailedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth + (metrics.hasDetailed ? metrics.countDigits + 1 + metrics.countDigits + 1 : 0);
839
+ const percentText = (task) => {
840
+ if (!isDeterminate(task)) return formatAmount(task, 0);
841
+ if (task.units.total === 0) return "100%";
842
+ const displayTotal = Math.max(task.units.total, task.units.processed);
843
+ return `${Math.max(0, Math.min(100, Math.round(task.units.processed / displayTotal * 100)))}%`;
844
+ };
845
+
846
+ //#endregion
847
+ //#region src/ink-renderer/columns/progress/amount-column.tsx
848
+ const succeededCount = (task, width) => {
849
+ if (width <= 0 || !shouldShowCountAmount(task) || !shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
850
+ return /* @__PURE__ */ jsx(Text, {
851
+ color: "green",
852
+ children: padLeft(`${task.units.succeeded}`, width)
853
+ });
854
+ };
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)
860
+ });
861
+ };
862
+ const processedCount = (task, width) => {
863
+ if (width <= 0 || !shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
864
+ return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
865
+ };
866
+ const totalCount = (task, width) => {
867
+ if (width <= 0 || !shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
868
+ return /* @__PURE__ */ jsx(Text, { children: padRight(isDeterminate(task) ? `${task.units.total}` : "?", width) });
869
+ };
870
+ const structuredAmount = (task, tick, width, layout) => {
871
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, {
872
+ 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
+ ]
894
+ });
895
+ };
896
+ const AmountColumn = (frame, config) => {
897
+ const metrics = config.metrics ?? computeProgressMetrics(frame);
735
898
  const detailedWidth = detailedAmountWidth(metrics);
736
899
  const processedWidth = processedAmountWidth(metrics);
737
- return {
738
- id: "amount",
739
- grow: 0,
740
- canHide: true,
741
- variants: metrics.hasStructuredCounts && metrics.hasDetailed ? [{
742
- id: "detailed",
743
- minWidth: detailedWidth,
744
- idealWidth: detailedWidth,
745
- renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
746
- task: row.task,
747
- tick: context.tick,
748
- layout: detailedLayout
749
- })
750
- }, {
751
- id: "processed",
752
- minWidth: processedWidth,
753
- idealWidth: processedWidth,
754
- renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
755
- task: row.task,
756
- tick: context.tick,
757
- layout: processedLayout
758
- })
759
- }] : metrics.hasStructuredCounts ? [{
760
- id: "processed",
761
- minWidth: processedWidth,
762
- idealWidth: processedWidth,
763
- renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
764
- task: row.task,
765
- tick: context.tick,
766
- layout: processedLayout
767
- })
768
- }] : [{
769
- id: "text",
770
- minWidth: 0,
771
- idealWidth: metrics.simpleTextWidth,
772
- renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
773
- task: row.task,
774
- tick: context.tick,
775
- layout: { kind: "text" }
776
- })
777
- }]
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)
778
911
  };
912
+ return createStickyColumn({
913
+ frame,
914
+ measure: {
915
+ min: summary.minWidth,
916
+ preferred: summary.preferredWidth,
917
+ max: summary.preferredWidth
918
+ },
919
+ stickyKey: config.stickyWidth === true ? config.key : void 0,
920
+ render: (taskId, width) => {
921
+ const task = frame.getTask(taskId);
922
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, {
923
+ wrap: "truncate-end",
924
+ children: formatAmount(task, frame.tick)
925
+ });
926
+ if (summary.hasDetailed && width >= summary.detailedWidth) return structuredAmount(task, frame.tick, width, {
927
+ kind: "detailed",
928
+ succeededWidth: summary.countDigits,
929
+ failedWidth: summary.countDigits,
930
+ processedWidth: summary.countDigits,
931
+ totalWidth: summary.totalWidth
932
+ });
933
+ return structuredAmount(task, frame.tick, width, {
934
+ kind: "processed",
935
+ processedWidth: summary.countDigits,
936
+ totalWidth: summary.totalWidth
937
+ });
938
+ }
939
+ });
779
940
  };
780
941
 
781
942
  //#endregion
782
- //#region src/ink-renderer/columns/bar-column.tsx
783
- const DEFAULT_BAR_WIDTH = 30;
784
- const MIN_BAR_WIDTH = 8;
943
+ //#region src/ink-renderer/columns/progress/bar-column.tsx
785
944
  const segmentLengths = (width, total, succeeded, failed) => {
786
945
  if (total === 0) return {
787
946
  succeeded: width,
@@ -799,576 +958,335 @@ const segmentLengths = (width, total, succeeded, failed) => {
799
958
  remaining: Math.max(0, width - succeededLength - failedLength)
800
959
  };
801
960
  };
802
- const BarColumn = ({ task, width }) => {
803
- if (!isDeterminate(task)) return /* @__PURE__ */ jsx(Text, {});
804
- const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
805
- return /* @__PURE__ */ jsxs(Text, {
806
- wrap: "truncate-end",
807
- children: [
808
- /* @__PURE__ */ jsx(Text, {
809
- color: "green",
810
- children: "━".repeat(lengths.succeeded)
811
- }),
812
- /* @__PURE__ */ jsx(Text, {
813
- color: "red",
814
- children: "━".repeat(lengths.failed)
815
- }),
816
- /* @__PURE__ */ jsx(Text, {
817
- color: "gray",
818
- children: "─".repeat(lengths.remaining)
819
- })
820
- ]
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
+ }
821
992
  });
822
993
  };
823
- const createBarColumnSpec = (context, isTTY) => {
824
- if (!hasDeterminateRows(context.rows)) return;
825
- return {
826
- id: "bar",
827
- grow: 0,
828
- canHide: true,
829
- variants: [{
830
- id: "full",
831
- minWidth: MIN_BAR_WIDTH,
832
- idealWidth: DEFAULT_BAR_WIDTH,
833
- maxWidth: DEFAULT_BAR_WIDTH,
834
- renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
835
- task: row.task,
836
- tree: row.tree,
837
- now: context.now,
838
- tick: context.tick,
839
- isTTY,
840
- width: Math.max(1, Math.min(width, DEFAULT_BAR_WIDTH))
841
- })
842
- }, {
843
- id: "compact",
844
- minWidth: 1,
845
- idealWidth: MIN_BAR_WIDTH,
846
- maxWidth: MIN_BAR_WIDTH,
847
- renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
848
- task: row.task,
849
- tree: row.tree,
850
- now: context.now,
851
- tick: context.tick,
852
- isTTY,
853
- width: Math.max(1, width)
854
- })
855
- }]
856
- };
857
- };
858
994
 
859
995
  //#endregion
860
- //#region src/ink-renderer/view/tree-prefix.ts
861
- const treeAncestorPrefix = (tree) => tree.ancestorHasNextSibling.slice(1).map((hasNext) => hasNext ? "│ " : " ").join("");
862
- const renderTreePrefix = (tree) => {
863
- if (tree.depth <= 0) return "";
864
- return `${treeAncestorPrefix(tree)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
865
- };
996
+ //#region src/ink-renderer/columns/progress/percent-column.tsx
997
+ const PercentColumn = (frame) => ({
998
+ measure: {
999
+ min: 4,
1000
+ preferred: 4,
1001
+ max: 4
1002
+ },
1003
+ render: (taskId, _width) => {
1004
+ const task = frame.getTask(taskId);
1005
+ const determinateColor = isDeterminate(task) ? getDeterminateProcessedColor(task) : void 0;
1006
+ return /* @__PURE__ */ jsx(Text, {
1007
+ wrap: "truncate-end",
1008
+ color: determinateColor === "green" || determinateColor === "yellow" || determinateColor === "red" ? determinateColor : void 0,
1009
+ children: percentText(task)
1010
+ });
1011
+ }
1012
+ });
866
1013
 
867
1014
  //#endregion
868
- //#region src/ink-renderer/columns/description-column.tsx
869
- const DescriptionColumn = ({ task, tree, showTree, tick }) => {
870
- const treePrefix = showTree ? renderTreePrefix(tree) : "";
871
- const indicator = getTaskIndicator(task, tick);
872
- return /* @__PURE__ */ jsxs(Text, {
873
- wrap: "truncate-end",
874
- children: [
875
- treePrefix,
876
- /* @__PURE__ */ jsx(Text, {
877
- color: indicator.color,
878
- children: indicator.symbol
879
- }),
880
- ` ${task.description}`
881
- ]
882
- });
883
- };
884
- const MIN_DESCRIPTION_WIDTH = 8;
885
- const MIN_DESCRIPTION_WITH_TREE_WIDTH = 20;
886
- const DESCRIPTION_PADDING_GROWTH_LIMIT = 20;
887
- const maxDescriptionWidth = (rows, showTree) => rows.reduce((max, row) => {
888
- const treePrefix = showTree ? renderTreePrefix(row.tree) : "";
889
- return Math.max(max, textWidth(`${treePrefix}${row.task.description}`) + 2);
890
- }, MIN_DESCRIPTION_WIDTH);
891
- const createDescriptionColumnSpec = (context, isTTY) => {
892
- const treeIdeal = maxDescriptionWidth(context.rows, true);
893
- const plainIdeal = maxDescriptionWidth(context.rows, false);
894
- const treeVariantIdeal = Math.max(MIN_DESCRIPTION_WITH_TREE_WIDTH, treeIdeal);
895
- const plainVariantIdeal = Math.max(MIN_DESCRIPTION_WIDTH, plainIdeal);
896
- return {
897
- id: "description",
898
- grow: 1,
899
- canHide: false,
900
- variants: [{
901
- id: "tree",
902
- minWidth: MIN_DESCRIPTION_WITH_TREE_WIDTH,
903
- idealWidth: treeVariantIdeal,
904
- maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, treeVariantIdeal),
905
- renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
906
- task: row.task,
907
- tree: row.tree,
908
- now: context.now,
909
- tick: context.tick,
910
- isTTY,
911
- showTree: true
912
- })
913
- }, {
914
- id: "plain",
915
- minWidth: MIN_DESCRIPTION_WIDTH,
916
- idealWidth: plainVariantIdeal,
917
- maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, plainVariantIdeal),
918
- renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
919
- task: row.task,
920
- tree: row.tree,
921
- now: context.now,
922
- tick: context.tick,
923
- isTTY,
924
- showTree: false
925
- })
926
- }]
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
927
1024
  };
928
- };
929
-
930
- //#endregion
931
- //#region src/ink-renderer/columns/elapsed-column.tsx
932
- const ElapsedColumn = ({ task, now }) => /* @__PURE__ */ jsx(Text, {
933
- wrap: "truncate-end",
934
- color: "gray",
935
- children: formatElapsed(task, now)
936
- });
937
- const MIN_ELAPSED_WIDTH = 2;
938
- const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
939
- const maxElapsedWidth = (rows, now) => rows.reduce((max, row) => Math.max(max, textWidth(formatElapsed(row.task, now))), MIN_ELAPSED_WIDTH);
940
- const createElapsedColumnSpec = (context, isTTY) => {
941
- const elapsedContentWidth = maxElapsedWidth(context.rows, context.now);
942
1025
  return {
943
- id: "elapsed",
944
- grow: 0,
945
- canHide: false,
946
- variants: [{
947
- id: "stable",
948
- minWidth: elapsedContentWidth,
949
- idealWidth: hasDeterminateRows(context.rows) ? Math.max(elapsedContentWidth, RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR) : elapsedContentWidth,
950
- renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
951
- task: row.task,
952
- tree: row.tree,
953
- now: context.now,
954
- tick: context.tick,
955
- isTTY
956
- })
957
- }, {
958
- id: "compact",
959
- minWidth: MIN_ELAPSED_WIDTH,
960
- idealWidth: elapsedContentWidth,
961
- renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
962
- task: row.task,
963
- tree: row.tree,
964
- now: context.now,
965
- tick: context.tick,
966
- isTTY
967
- })
968
- }]
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
969
1038
  };
970
1039
  };
971
-
972
- //#endregion
973
- //#region src/ink-renderer/columns/eta-column.tsx
974
- const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
975
- const etaDurationText = (task, now) => {
976
- if (task.status !== "running" || !isDeterminate(task)) return;
977
- const eta = formatEta(task, now);
978
- return eta.length > 0 ? eta : "--";
979
- };
980
- const EtaColumn = ({ task, now, mode }) => {
981
- const duration = etaDurationText(task, now);
982
- if (duration === void 0) return /* @__PURE__ */ jsx(Text, {});
983
- return /* @__PURE__ */ jsx(Text, {
984
- wrap: "truncate-end",
985
- color: "gray",
986
- children: mode === "prefixed" ? `ETA: ${duration}` : mode === "primary" ? primaryUnit(duration) : duration
987
- });
988
- };
989
- const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
990
- const computeEtaMetrics = (rows, now) => {
991
- let hasEta = false;
992
- let prefixedWidth = 0;
993
- let durationWidth = 0;
994
- let primaryUnitWidth = 0;
995
- for (const row of rows) {
996
- const duration = etaDurationText(row.task, now);
997
- if (duration === void 0) continue;
998
- hasEta = true;
999
- const prefixed = `ETA: ${duration}`;
1000
- prefixedWidth = Math.max(prefixedWidth, textWidth(prefixed));
1001
- durationWidth = Math.max(durationWidth, textWidth(duration));
1002
- primaryUnitWidth = Math.max(primaryUnitWidth, textWidth(primaryUnit(duration)));
1003
- }
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));
1004
1047
  return {
1005
- hasEta,
1006
- prefixedWidth,
1007
- durationWidth: Math.max(2, durationWidth),
1008
- primaryUnitWidth: Math.max(2, primaryUnitWidth)
1048
+ kind: "bar-amount",
1049
+ bar,
1050
+ barWidth: Math.max(bar.measure.min, available - amountWidth),
1051
+ amountWidth
1009
1052
  };
1010
1053
  };
1011
- const createEtaColumnSpec = (context, isTTY) => {
1012
- const metrics = computeEtaMetrics(context.rows, context.now);
1013
- if (!metrics.hasEta) return;
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;
1014
1064
  return {
1015
- id: "eta",
1016
- grow: 0,
1017
- canHide: true,
1018
- variants: [
1019
- {
1020
- id: "prefixed",
1021
- minWidth: metrics.prefixedWidth,
1022
- idealWidth: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR),
1023
- renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1024
- task: row.task,
1025
- tree: row.tree,
1026
- now: context.now,
1027
- tick: context.tick,
1028
- isTTY,
1029
- mode: "prefixed"
1030
- })
1031
- },
1032
- {
1033
- id: "duration",
1034
- minWidth: metrics.durationWidth,
1035
- idealWidth: metrics.durationWidth,
1036
- renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1037
- task: row.task,
1038
- tree: row.tree,
1039
- now: context.now,
1040
- tick: context.tick,
1041
- isTTY,
1042
- mode: "duration"
1043
- })
1044
- },
1045
- {
1046
- id: "primary",
1047
- minWidth: metrics.primaryUnitWidth,
1048
- idealWidth: metrics.primaryUnitWidth,
1049
- renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1050
- task: row.task,
1051
- tree: row.tree,
1052
- now: context.now,
1053
- tick: context.tick,
1054
- isTTY,
1055
- mode: "primary"
1056
- })
1057
- }
1058
- ]
1065
+ measure: {
1066
+ min: !model.metrics.hasDeterminate ? amount.measure.min : fullMin,
1067
+ preferred,
1068
+ max
1069
+ },
1070
+ commitStickyWidth: () => {
1071
+ model.bar?.commitStickyWidth?.();
1072
+ amount.commitStickyWidth?.();
1073
+ },
1074
+ render: (taskId, width) => {
1075
+ const layout = layoutForWidth(width, model);
1076
+ if (layout.kind === "percent") return model.percent.render(taskId, width);
1077
+ if (layout.kind === "amount-only") return amount.render(taskId, width);
1078
+ return /* @__PURE__ */ jsxs(Box, {
1079
+ flexDirection: "row",
1080
+ width,
1081
+ children: [
1082
+ /* @__PURE__ */ jsx(Box, {
1083
+ width: layout.barWidth,
1084
+ children: layout.bar.render(taskId, layout.barWidth)
1085
+ }),
1086
+ /* @__PURE__ */ jsx(Box, { marginRight: 1 }),
1087
+ /* @__PURE__ */ jsx(Box, {
1088
+ width: layout.amountWidth,
1089
+ children: amount.render(taskId, layout.amountWidth)
1090
+ })
1091
+ ]
1092
+ });
1093
+ }
1059
1094
  };
1060
1095
  };
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");
1061
1102
 
1062
1103
  //#endregion
1063
- //#region src/ink-renderer/columns/planner.ts
1064
- const clampInt = (value, min, max) => Math.max(min, Math.min(max, Math.floor(value)));
1065
- const activeColumns = (columns) => columns.filter((column) => !column.hidden);
1066
- const visibleColumns = (columns) => columns.filter((column) => !column.hidden && column.width > 0);
1067
- const visibleGapWidth = (columns) => Math.max(0, visibleColumns(columns).length - 1);
1068
- const totalWidth = (columns) => visibleColumns(columns).reduce((sum, column) => sum + column.width, 0) + visibleGapWidth(columns);
1069
- const currentVariant = (column) => column.variants[column.variantIndex];
1070
- const variantMaxWidth = (column) => {
1071
- const variant = currentVariant(column);
1072
- return variant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(variant.minWidth, variant.maxWidth);
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;
1073
1109
  };
1074
- const reduceOverflowByShrink = (columns, overflow) => {
1075
- let remaining = overflow;
1076
- const candidates = activeColumns(columns).filter((column) => column.width > currentVariant(column).minWidth).sort((a, b) => currentVariant(a).shrinkResistance - currentVariant(b).shrinkResistance);
1077
- for (const column of candidates) {
1078
- if (remaining <= 0) break;
1079
- const variant = currentVariant(column);
1080
- const reducible = column.width - variant.minWidth;
1081
- if (reducible <= 0) continue;
1082
- const delta = Math.min(reducible, remaining);
1083
- column.width -= delta;
1084
- remaining -= delta;
1085
- }
1086
- return remaining;
1110
+ const ROOT_LAYOUTS = [
1111
+ [
1112
+ DescriptionTreeRootColumn,
1113
+ ProgressRootColumn,
1114
+ ElapsedRootColumn,
1115
+ EtaRootColumn
1116
+ ],
1117
+ [
1118
+ DescriptionPlainRootColumn,
1119
+ ProgressRootColumn,
1120
+ ElapsedRootColumn,
1121
+ EtaRootColumn
1122
+ ],
1123
+ [
1124
+ DescriptionPlainRootColumn,
1125
+ ProgressPercentRootColumn,
1126
+ ElapsedRootColumn,
1127
+ EtaRootColumn
1128
+ ],
1129
+ [
1130
+ DescriptionPlainRootColumn,
1131
+ ProgressPercentRootColumn,
1132
+ ElapsedRootColumn
1133
+ ],
1134
+ [DescriptionPlainRootColumn, ProgressPercentRootColumn],
1135
+ [DescriptionCompactRootColumn, ProgressPercentRootColumn],
1136
+ [DescriptionCompactRootColumn],
1137
+ [DescriptionSpinnerRootColumn]
1138
+ ];
1139
+ const ROOT_COLUMNS = [...new Map(ROOT_LAYOUTS.flat().map((column) => [column.id, column])).values()];
1140
+ const measureColumns = (frame) => new Map(ROOT_COLUMNS.flatMap((definition) => {
1141
+ const column = definition.build(frame);
1142
+ if (column === void 0) return [];
1143
+ const measure = column.measure;
1144
+ return [[definition.id, {
1145
+ definition,
1146
+ id: definition.id,
1147
+ measure,
1148
+ preferredWidth: measure.preferred,
1149
+ commitStickyWidth: column.commitStickyWidth,
1150
+ render: column.render
1151
+ }]];
1152
+ }));
1153
+ const candidateRootLayouts = (columnsById) => {
1154
+ const hasProgress = columnsById.has(ProgressRootColumn.id);
1155
+ const hasPercentProgress = columnsById.has(ProgressPercentRootColumn.id);
1156
+ const hasEta = columnsById.has(EtaRootColumn.id);
1157
+ return [
1158
+ ...hasProgress && hasEta ? [[
1159
+ DescriptionTreeRootColumn,
1160
+ ProgressRootColumn,
1161
+ ElapsedRootColumn,
1162
+ EtaRootColumn
1163
+ ], [
1164
+ DescriptionPlainRootColumn,
1165
+ ProgressRootColumn,
1166
+ ElapsedRootColumn,
1167
+ EtaRootColumn
1168
+ ]] : [],
1169
+ ...hasProgress && !hasEta ? [[
1170
+ DescriptionTreeRootColumn,
1171
+ ProgressRootColumn,
1172
+ ElapsedRootColumn
1173
+ ], [
1174
+ DescriptionPlainRootColumn,
1175
+ ProgressRootColumn,
1176
+ ElapsedRootColumn
1177
+ ]] : [],
1178
+ ...hasPercentProgress && hasEta ? [[
1179
+ DescriptionPlainRootColumn,
1180
+ ProgressPercentRootColumn,
1181
+ ElapsedRootColumn,
1182
+ EtaRootColumn
1183
+ ]] : [],
1184
+ ...hasPercentProgress ? [
1185
+ [
1186
+ DescriptionPlainRootColumn,
1187
+ ProgressPercentRootColumn,
1188
+ ElapsedRootColumn
1189
+ ],
1190
+ [DescriptionPlainRootColumn, ProgressPercentRootColumn],
1191
+ [DescriptionCompactRootColumn, ProgressPercentRootColumn]
1192
+ ] : [],
1193
+ ...!hasProgress && !hasPercentProgress ? [
1194
+ [DescriptionTreeRootColumn, ElapsedRootColumn],
1195
+ [DescriptionPlainRootColumn, ElapsedRootColumn],
1196
+ [DescriptionPlainRootColumn]
1197
+ ] : [],
1198
+ [DescriptionCompactRootColumn],
1199
+ [DescriptionSpinnerRootColumn]
1200
+ ];
1087
1201
  };
1088
- const nextDemoteCandidate = (columns) => activeColumns(columns).filter((column) => column.variantIndex + 1 < column.variants.length).sort((a, b) => currentVariant(a).demoteResistance - currentVariant(b).demoteResistance)[0];
1089
- const applyDemote = (column) => {
1090
- const nextVariant = column.variants[column.variantIndex + 1];
1091
- column.variantIndex += 1;
1092
- const nextMaxWidth = nextVariant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(nextVariant.minWidth, nextVariant.maxWidth);
1093
- column.width = Math.max(nextVariant.minWidth, Math.min(column.width, nextVariant.idealWidth, nextMaxWidth));
1202
+ const resolveRootLayouts = (columnsById) => {
1203
+ const resolveLayout = (definitions) => {
1204
+ const columns = [];
1205
+ for (const definition of definitions) {
1206
+ const column = columnsById.get(definition.id);
1207
+ if (column === void 0) return;
1208
+ columns.push(column);
1209
+ }
1210
+ return columns;
1211
+ };
1212
+ return candidateRootLayouts(columnsById).map(resolveLayout).filter((columns) => columns !== void 0);
1094
1213
  };
1095
- const nextHideCandidate = (columns) => activeColumns(columns).filter((column) => column.spec.canHide).sort((a, b) => currentVariant(a).hideResistance - currentVariant(b).hideResistance)[0];
1096
- const applyHide = (column) => {
1097
- column.hidden = true;
1098
- column.width = 0;
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) ?? [];
1099
1219
  };
1100
- const distributeGrowth = (columns, extra) => {
1101
- if (extra <= 0) return;
1102
- let remaining = extra;
1103
- while (remaining > 0) {
1104
- const candidates = activeColumns(columns).filter((column) => column.spec.grow > 0 && column.width < variantMaxWidth(column));
1105
- if (candidates.length === 0) break;
1106
- const growSum = candidates.reduce((sum, column) => sum + Math.max(0, column.spec.grow), 0);
1107
- if (growSum <= 0) break;
1108
- let distributed = 0;
1109
- for (const column of candidates) {
1110
- const weight = Math.max(0, column.spec.grow);
1111
- if (weight <= 0) continue;
1112
- const headroom = Math.max(0, variantMaxWidth(column) - column.width);
1113
- if (headroom <= 0) continue;
1114
- const share = Math.min(headroom, Math.floor(remaining * weight / growSum));
1115
- if (share <= 0) continue;
1116
- column.width += share;
1117
- distributed += share;
1118
- }
1119
- if (distributed === 0) {
1120
- for (const column of candidates) {
1121
- if (remaining <= 0) break;
1122
- if (Math.max(0, variantMaxWidth(column) - column.width) <= 0) continue;
1123
- column.width += 1;
1124
- distributed += 1;
1125
- remaining -= 1;
1126
- }
1127
- if (distributed === 0) break;
1220
+ const preferredWidthsForSet = (columns) => columns.map((column) => Math.max(column.measure.min, column.preferredWidth));
1221
+ const nextDistinctWidth = (entries, widest) => entries.find((entry) => entry.width < widest)?.width;
1222
+ const reduceOverflowRichStyle = (widths, minimums, targetWidth) => {
1223
+ let overflow = visibleWidth(widths, ROOT_GAP) - targetWidth;
1224
+ while (overflow > 0) {
1225
+ const shrinkable = widths.map((width, index) => ({
1226
+ width,
1227
+ index,
1228
+ minimum: minimums[index] ?? width
1229
+ })).filter(({ width, minimum }) => width > minimum).sort((left, right) => right.width - left.width || left.index - right.index);
1230
+ if (shrinkable.length === 0) break;
1231
+ const widest = shrinkable[0].width;
1232
+ const cohort = shrinkable.filter(({ width }) => width === widest);
1233
+ const nextWidth = nextDistinctWidth(shrinkable, widest);
1234
+ const maxUniformDrop = widest - Math.max(nextWidth ?? 0, ...cohort.map(({ minimum }) => minimum));
1235
+ const uniformDrop = Math.min(maxUniformDrop, Math.floor(overflow / cohort.length));
1236
+ if (uniformDrop > 0) {
1237
+ for (const { index } of cohort) widths[index] = widths[index] - uniformDrop;
1238
+ overflow -= uniformDrop * cohort.length;
1128
1239
  continue;
1129
1240
  }
1130
- remaining -= distributed;
1131
- }
1132
- };
1133
- const planColumns = ({ context, baselineWidth, columns }) => {
1134
- const mutable = columns.flatMap((column) => {
1135
- if (column.variants.length === 0) return [];
1136
- const first = column.variants[0];
1137
- return [{
1138
- spec: column,
1139
- variants: column.variants,
1140
- variantIndex: 0,
1141
- width: Math.max(first.minWidth, Math.min(first.idealWidth, first.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(first.minWidth, first.maxWidth))),
1142
- hidden: false
1143
- }];
1144
- });
1145
- if (mutable.length === 0) return {
1146
- rowWidth: 0,
1147
- columns: []
1148
- };
1149
- const naturalWidth = totalWidth(mutable);
1150
- const clampedTerminal = context.terminalColumns === void 0 ? void 0 : clampInt(context.terminalColumns, 1, Number.POSITIVE_INFINITY);
1151
- const baselineTarget = Math.max(1, Math.max(baselineWidth, naturalWidth));
1152
- const target = clampedTerminal === void 0 ? baselineTarget : Math.min(baselineTarget, clampedTerminal);
1153
- if (naturalWidth < target) distributeGrowth(mutable, target - naturalWidth);
1154
- else if (naturalWidth > target) {
1155
- let overflow = naturalWidth - target;
1156
- let progressed = true;
1157
- while (overflow > 0 && progressed) {
1158
- const before = overflow;
1159
- overflow = reduceOverflowByShrink(mutable, overflow);
1241
+ let changed = false;
1242
+ for (const { index, minimum } of cohort) {
1160
1243
  if (overflow <= 0) break;
1161
- const demoteCandidate = nextDemoteCandidate(mutable);
1162
- const hideCandidate = nextHideCandidate(mutable);
1163
- if (demoteCandidate === void 0 && hideCandidate === void 0) {
1164
- progressed = before !== overflow;
1165
- continue;
1166
- }
1167
- if ((demoteCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(demoteCandidate).demoteResistance) <= (hideCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(hideCandidate).hideResistance) && demoteCandidate !== void 0) applyDemote(demoteCandidate);
1168
- else if (hideCandidate !== void 0) applyHide(hideCandidate);
1169
- overflow = Math.max(0, totalWidth(mutable) - target);
1170
- progressed = true;
1244
+ if (widths[index] <= minimum) continue;
1245
+ widths[index] = widths[index] - 1;
1246
+ overflow -= 1;
1247
+ changed = true;
1171
1248
  }
1172
- const compactedWidth = totalWidth(mutable);
1173
- if (compactedWidth < target) distributeGrowth(mutable, target - compactedWidth);
1249
+ if (!changed) break;
1174
1250
  }
1175
- const visible = visibleColumns(mutable);
1176
- return {
1177
- rowWidth: totalWidth(mutable),
1178
- columns: visible.map((column) => {
1179
- const variant = currentVariant(column);
1180
- return {
1181
- id: column.spec.id,
1182
- variantId: variant.id,
1183
- width: column.width,
1184
- renderCell: variant.renderCell
1185
- };
1186
- })
1187
- };
1251
+ return widths;
1188
1252
  };
1189
-
1190
- //#endregion
1191
- //#region src/ink-renderer/columns/layout.tsx
1192
- /**
1193
- * Frame planning algorithm (per render tick):
1194
- *
1195
- * 1. Ask each column module to produce a logical column spec (variants + measured widths).
1196
- * 2. Apply shared resistance rules (shrink / demote / hide) to those variants.
1197
- * 3. Feed resolved columns into `planColumns`.
1198
- * 4. `planColumns` shrinks, demotes, and hides until the row fits target width.
1199
- * 5. Render rows with the selected variant and width for each visible column.
1200
- */
1201
- const BASELINE_ROW_WIDTH = 150;
1202
- const DEFAULT_RESISTANCE = 1e3;
1203
- const variantOrderKey = ({ columnId, variantId }) => `${columnId}:${variantId}`;
1204
- const SHRINK_ORDER = [
1205
- {
1206
- columnId: "eta",
1207
- variantId: "prefixed"
1208
- },
1209
- {
1210
- columnId: "elapsed",
1211
- variantId: "stable"
1212
- },
1213
- {
1214
- columnId: "eta",
1215
- variantId: "duration"
1216
- },
1217
- {
1218
- columnId: "eta",
1219
- variantId: "primary"
1220
- },
1221
- {
1222
- columnId: "bar",
1223
- variantId: "compact"
1224
- },
1225
- {
1226
- columnId: "bar",
1227
- variantId: "full"
1228
- },
1229
- {
1230
- columnId: "amount",
1231
- variantId: "text"
1232
- },
1233
- {
1234
- columnId: "elapsed",
1235
- variantId: "compact"
1236
- },
1237
- {
1238
- columnId: "description",
1239
- variantId: "tree"
1240
- },
1241
- {
1242
- columnId: "amount",
1243
- variantId: "detailed"
1244
- },
1245
- {
1246
- columnId: "amount",
1247
- variantId: "processed"
1248
- },
1249
- {
1250
- columnId: "description",
1251
- variantId: "plain"
1252
- }
1253
- ];
1254
- const DEMOTE_ORDER = [
1255
- {
1256
- columnId: "description",
1257
- variantId: "tree"
1258
- },
1259
- {
1260
- columnId: "eta",
1261
- variantId: "prefixed"
1262
- },
1263
- {
1264
- columnId: "eta",
1265
- variantId: "duration"
1266
- },
1267
- {
1268
- columnId: "elapsed",
1269
- variantId: "stable"
1270
- },
1271
- {
1272
- columnId: "bar",
1273
- variantId: "full"
1274
- },
1275
- {
1276
- columnId: "amount",
1277
- variantId: "detailed"
1278
- }
1279
- ];
1280
- const HIDE_ORDER = [
1281
- {
1282
- columnId: "eta",
1283
- variantId: "primary"
1284
- },
1285
- {
1286
- columnId: "eta",
1287
- variantId: "prefixed"
1288
- },
1289
- {
1290
- columnId: "eta",
1291
- variantId: "duration"
1292
- },
1293
- {
1294
- columnId: "bar",
1295
- variantId: "full"
1296
- },
1297
- {
1298
- columnId: "bar",
1299
- variantId: "compact"
1300
- },
1301
- {
1302
- columnId: "amount",
1303
- variantId: "text"
1304
- },
1305
- {
1306
- columnId: "amount",
1307
- variantId: "detailed"
1308
- },
1309
- {
1310
- columnId: "amount",
1311
- variantId: "processed"
1312
- }
1313
- ];
1314
- const toResistanceLookup = (order) => new Map(order.map((item, index) => [variantOrderKey(item), index + 1]));
1315
- const shrinkResistanceLookup = toResistanceLookup(SHRINK_ORDER);
1316
- const demoteResistanceLookup = toResistanceLookup(DEMOTE_ORDER);
1317
- const hideResistanceLookup = toResistanceLookup(HIDE_ORDER);
1318
- const resistanceFor = (lookup, columnId, variantId) => lookup.get(`${columnId}:${variantId}`) ?? DEFAULT_RESISTANCE;
1319
- const buildColumns = (context, isTTY) => {
1320
- return resolveColumnSpecs([
1321
- createDescriptionColumnSpec(context, isTTY),
1322
- createBarColumnSpec(context, isTTY),
1323
- createAmountColumnSpec(context),
1324
- createElapsedColumnSpec(context, isTTY),
1325
- createEtaColumnSpec(context, isTTY)
1326
- ].filter((spec) => spec !== void 0), {
1327
- shrink: (columnId, variantId) => resistanceFor(shrinkResistanceLookup, columnId, variantId),
1328
- demote: (columnId, variantId) => resistanceFor(demoteResistanceLookup, columnId, variantId),
1329
- hide: (columnId, variantId) => resistanceFor(hideResistanceLookup, columnId, variantId)
1330
- });
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);
1331
1260
  };
1332
- const computeFrameLayout = (rows, now, tick, terminalColumns, isTTY) => {
1333
- const context = {
1334
- rows,
1335
- now,
1336
- tick,
1337
- terminalColumns
1338
- };
1339
- return planColumns({
1340
- context,
1341
- columns: buildColumns(context, isTTY),
1342
- baselineWidth: hasDeterminateRows(rows) ? BASELINE_ROW_WIDTH : 1
1343
- });
1261
+ const emptyRootColumn = (stickyWidths) => {
1262
+ stickyWidths.clear();
1263
+ return { render: () => null };
1344
1264
  };
1345
-
1346
- //#endregion
1347
- //#region src/ink-renderer/view/task-row.tsx
1348
- const TaskRow = ({ row, layout }) => {
1349
- return /* @__PURE__ */ jsx(Box, {
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, {
1350
1277
  flexDirection: "row",
1351
- minWidth: layout.rowWidth,
1352
- children: layout.columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
1278
+ minWidth: rowWidth,
1279
+ children: columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
1280
+ flexDirection: "column",
1353
1281
  width: column.width,
1354
- flexShrink: column.id === "description" ? 1 : 0,
1355
- marginRight: index < layout.columns.length - 1 ? 1 : 0,
1356
- children: column.renderCell(row, 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))
1357
1288
  }, column.id))
1358
- });
1359
- };
1360
-
1361
- //#endregion
1362
- //#region src/ink-renderer/view/progress-view.tsx
1363
- const ProgressView = ({ rows, now, tick, isTTY, terminalColumns }) => {
1364
- const layout = computeFrameLayout(rows, now, tick, terminalColumns, isTTY);
1365
- return /* @__PURE__ */ jsx(Box, {
1366
- flexDirection: "column",
1367
- children: rows.map((row) => /* @__PURE__ */ jsx(TaskRow, {
1368
- row,
1369
- layout
1370
- }, row.task.id))
1371
- });
1289
+ }) };
1372
1290
  };
1373
1291
 
1374
1292
  //#endregion
@@ -1408,17 +1326,12 @@ const useSpinnerClock = (active, intervalMillis) => {
1408
1326
  //#region src/ink-renderer/view/render-root.tsx
1409
1327
  const SPINNER_INTERVAL_MILLIS = 100;
1410
1328
  const NOW_INTERVAL_MILLIS = 1e3;
1411
- const ProgressRoot = ({ store, isTTY, getTerminalColumns }) => {
1329
+ const ProgressRoot = ({ store, getTerminalColumns }) => {
1412
1330
  const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1413
1331
  const tick = useSpinnerClock(snapshot.hasRunningTasks, SPINNER_INTERVAL_MILLIS);
1414
1332
  const now = useNowClock(snapshot.hasRunningTasks, NOW_INTERVAL_MILLIS);
1415
- return /* @__PURE__ */ jsx(ProgressView, {
1416
- rows: snapshot.rows,
1417
- now,
1418
- tick,
1419
- isTTY,
1420
- terminalColumns: getTerminalColumns()
1421
- });
1333
+ const stickyWidths = useRef(/* @__PURE__ */ new Map());
1334
+ return RootColumn(snapshot.rows, now, tick, getTerminalColumns(), stickyWidths.current).render();
1422
1335
  };
1423
1336
 
1424
1337
  //#endregion
@@ -1426,7 +1339,6 @@ const ProgressRoot = ({ store, isTTY, getTerminalColumns }) => {
1426
1339
  const MAX_FPS = 12;
1427
1340
  const makeDefaultInkRenderer = () => ({ run: (store, stdio, isTTY) => Effect.sync(() => render(/* @__PURE__ */ jsx(ProgressRoot, {
1428
1341
  store,
1429
- isTTY,
1430
1342
  getTerminalColumns: () => isTTY ? stdio.stderr.columns : void 0
1431
1343
  }), {
1432
1344
  stdout: stdio.stdout,