@d3lm/pr-stats 0.2.11 → 0.2.13

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -45
  3. package/dist/tui-app.mjs +2006 -1872
  4. package/package.json +1 -1
package/dist/tui-app.mjs CHANGED
@@ -558,11 +558,11 @@ function Footer({
558
558
  views,
559
559
  copyLinks: copyLinks2,
560
560
  openError,
561
- copyNotice,
561
+ successNotice,
562
562
  stale
563
563
  }) {
564
- const notice = openError ?? copyNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
565
- const check = openError === null && copyNotice !== null;
564
+ const notice = openError ?? successNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
565
+ const check = openError === null && successNotice !== null;
566
566
  const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
567
567
  const hints = truncated(hintsFor(modal, editing, tab, authoredTab, views, copyLinks2), width - 2 - noticeWidth);
568
568
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -580,7 +580,7 @@ function Footer({
580
580
  /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hints }),
581
581
  /* @__PURE__ */ jsxs("text", { wrapMode: "none", children: [
582
582
  check && /* @__PURE__ */ jsx("span", { fg: theme.success, children: "\u2714 " }),
583
- /* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : copyNotice !== null ? theme.muted : theme.warn, children: notice })
583
+ /* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : successNotice !== null ? theme.muted : theme.warn, children: notice })
584
584
  ] })
585
585
  ]
586
586
  }
@@ -629,1997 +629,2067 @@ function hintsFor(modal, editing, tab, authoredTab, views, copyLinks2) {
629
629
  return `${toggle}${expand}1-5 tabs \xB7 j/k scroll \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
630
630
  }
631
631
 
632
- // src/tui/components/Spinner.tsx
633
- import { useEffect, useState } from "react";
634
- import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
635
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
636
- var SPINNER_INTERVAL_MS = 80;
637
- function Spinner() {
638
- const [frame, setFrame] = useState(0);
639
- useEffect(() => {
640
- const timer = setInterval(() => {
641
- setFrame((previous) => (previous + 1) % SPINNER_FRAMES.length);
642
- }, SPINNER_INTERVAL_MS);
643
- return () => {
644
- clearInterval(timer);
645
- };
646
- }, []);
647
- return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
648
- }
649
-
650
- // src/tui/components/Header.tsx
651
- import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
652
- function Header({
653
- options,
654
- raw,
655
- error,
656
- spinning
657
- }) {
658
- const context = [
659
- raw ? `@${raw.user}` : options.user !== "" ? `@${options.user}` : "@...",
660
- `since ${options.since}`,
661
- raw && raw.repos.length > 0 ? raw.repos.join(", ") : options.repos !== "" ? options.repos : "all repos",
662
- timeModeLabel(options)
663
- ].join(" \xB7 ");
664
- const rightStatus = raw ? error !== null ? "reload failed \xB7 press r to retry" : `refreshed ${raw.fetchedAt.toLocaleTimeString()}` : "";
665
- return /* @__PURE__ */ jsxs2("box", { flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [
666
- /* @__PURE__ */ jsxs2("text", { wrapMode: "none", children: [
667
- /* @__PURE__ */ jsx3("b", { fg: theme.accent, children: "pr-stats" }),
668
- /* @__PURE__ */ jsxs2("span", { fg: theme.muted, children: [
669
- " \xB7 ",
670
- context
671
- ] })
672
- ] }),
673
- spinning ? /* @__PURE__ */ jsx3(Spinner, {}) : /* @__PURE__ */ jsx3("text", { wrapMode: "none", fg: raw !== null && error !== null ? theme.error : theme.muted, children: rightStatus })
674
- ] });
675
- }
676
- function timeModeLabel(options) {
677
- if (options.wallClock) {
678
- return "wall-clock time";
632
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
633
+ function stringReplaceAll(string, substring, postfix) {
634
+ let index = string.indexOf(substring);
635
+ if (index === -1) {
636
+ return string;
679
637
  }
680
- const tz = options.tz === "" ? Intl.DateTimeFormat().resolvedOptions().timeZone : options.tz;
681
- return options.workHours === "0-24" ? `Mon-Fri all hours ${tz}` : `Mon-Fri ${options.workHours} ${tz}`;
682
- }
683
-
684
- // src/time.ts
685
- function wallFormatter(tz) {
686
- return new Intl.DateTimeFormat("en-US", {
687
- timeZone: tz,
688
- hourCycle: "h23",
689
- year: "numeric",
690
- month: "2-digit",
691
- day: "2-digit",
692
- hour: "2-digit",
693
- minute: "2-digit",
694
- second: "2-digit"
695
- });
696
- }
697
- var timeMode = {
698
- business: true,
699
- workWindows: [{ startMin: 0, endMin: 24 * 60 }],
700
- dayHours: 24,
701
- formatter: wallFormatter("UTC")
702
- };
703
- function configureTimeMode({
704
- business,
705
- workWindows,
706
- tz
707
- }) {
708
- const workMinutesPerDay = workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
709
- timeMode.business = business;
710
- timeMode.workWindows = workWindows;
711
- timeMode.dayHours = business ? workMinutesPerDay / 60 : 24;
712
- timeMode.formatter = wallFormatter(tz);
713
- }
714
- function isFullDayMode() {
715
- return timeMode.business && timeMode.dayHours === 24;
716
- }
717
- function wallParts(instantMs) {
718
- const parts = Object.fromEntries(timeMode.formatter.formatToParts(instantMs).map((part) => [part.type, part.value]));
719
- return {
720
- year: Number(parts.year),
721
- month: Number(parts.month),
722
- day: Number(parts.day),
723
- hour: Number(parts.hour),
724
- minute: Number(parts.minute),
725
- second: Number(parts.second)
726
- };
727
- }
728
- function zonedStamp(date) {
729
- const parts = wallParts(date.getTime());
730
- const dayUtcMs = Date.UTC(parts.year, parts.month - 1, parts.day);
731
- return { dayUtcMs, weekday: new Date(dayUtcMs).getUTCDay(), hour: parts.hour, minute: parts.minute };
638
+ const substringLength = substring.length;
639
+ let endIndex = 0;
640
+ let returnValue = "";
641
+ do {
642
+ returnValue += string.slice(endIndex, index) + substring + postfix;
643
+ endIndex = index + substringLength;
644
+ index = string.indexOf(substring, endIndex);
645
+ } while (index !== -1);
646
+ returnValue += string.slice(endIndex);
647
+ return returnValue;
732
648
  }
733
- function hasWorkWindows() {
734
- const covered = timeMode.workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
735
- return covered < 24 * 60;
649
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
650
+ let endIndex = 0;
651
+ let returnValue = "";
652
+ do {
653
+ const isGotCR = string[index - 1] === "\r";
654
+ returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? "\r\n" : "\n") + postfix;
655
+ endIndex = index + 1;
656
+ index = string.indexOf("\n", endIndex);
657
+ } while (index !== -1);
658
+ returnValue += string.slice(endIndex);
659
+ return returnValue;
736
660
  }
737
- function classifyInstant(date) {
738
- const { weekday, hour, minute } = zonedStamp(date);
739
- if (weekday === 0 || weekday === 6) {
740
- return "weekend";
661
+
662
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
663
+ var ANSI_BACKGROUND_OFFSET = 10;
664
+ var ANSI_UNDERLINE_OFFSET = 20;
665
+ var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
666
+ var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
667
+ var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
668
+ var wrapUnderlineAnsi = (code) => `\x1B[58;5;${code < 90 ? code - 30 : code - 90 + 8}m`;
669
+ var styles = {
670
+ modifier: {
671
+ reset: [0, 0],
672
+ // 21 isn't widely supported and 22 does the same thing
673
+ bold: [1, 22],
674
+ dim: [2, 22],
675
+ italic: [3, 23],
676
+ underline: [4, 24],
677
+ // Extended underline styles (`SGR 4:x` sub-parameters). Not in upstream `ansi-styles`.
678
+ underlineDouble: ["4:2", 24],
679
+ underlineCurly: ["4:3", 24],
680
+ underlineDotted: ["4:4", 24],
681
+ underlineDashed: ["4:5", 24],
682
+ overline: [53, 55],
683
+ inverse: [7, 27],
684
+ hidden: [8, 28],
685
+ strikethrough: [9, 29]
686
+ },
687
+ color: {
688
+ black: [30, 39],
689
+ red: [31, 39],
690
+ green: [32, 39],
691
+ yellow: [33, 39],
692
+ blue: [34, 39],
693
+ magenta: [35, 39],
694
+ cyan: [36, 39],
695
+ white: [37, 39],
696
+ // Bright color
697
+ blackBright: [90, 39],
698
+ gray: [90, 39],
699
+ // Alias of `blackBright`
700
+ grey: [90, 39],
701
+ // Alias of `blackBright`
702
+ redBright: [91, 39],
703
+ greenBright: [92, 39],
704
+ yellowBright: [93, 39],
705
+ blueBright: [94, 39],
706
+ magentaBright: [95, 39],
707
+ cyanBright: [96, 39],
708
+ whiteBright: [97, 39]
709
+ },
710
+ bgColor: {
711
+ bgBlack: [40, 49],
712
+ bgRed: [41, 49],
713
+ bgGreen: [42, 49],
714
+ bgYellow: [43, 49],
715
+ bgBlue: [44, 49],
716
+ bgMagenta: [45, 49],
717
+ bgCyan: [46, 49],
718
+ bgWhite: [47, 49],
719
+ // Bright color
720
+ bgBlackBright: [100, 49],
721
+ bgGray: [100, 49],
722
+ // Alias of `bgBlackBright`
723
+ bgGrey: [100, 49],
724
+ // Alias of `bgBlackBright`
725
+ bgRedBright: [101, 49],
726
+ bgGreenBright: [102, 49],
727
+ bgYellowBright: [103, 49],
728
+ bgBlueBright: [104, 49],
729
+ bgMagentaBright: [105, 49],
730
+ bgCyanBright: [106, 49],
731
+ bgWhiteBright: [107, 49]
732
+ },
733
+ // Underline color (`SGR 58`/`59`). Not in upstream `ansi-styles`.
734
+ underlineColor: {
735
+ underlineBlack: ["58;5;0", 59],
736
+ underlineRed: ["58;5;1", 59],
737
+ underlineGreen: ["58;5;2", 59],
738
+ underlineYellow: ["58;5;3", 59],
739
+ underlineBlue: ["58;5;4", 59],
740
+ underlineMagenta: ["58;5;5", 59],
741
+ underlineCyan: ["58;5;6", 59],
742
+ underlineWhite: ["58;5;7", 59],
743
+ // Bright color
744
+ underlineBlackBright: ["58;5;8", 59],
745
+ underlineGray: ["58;5;8", 59],
746
+ // Alias of `underlineBlackBright`
747
+ underlineGrey: ["58;5;8", 59],
748
+ // Alias of `underlineBlackBright`
749
+ underlineRedBright: ["58;5;9", 59],
750
+ underlineGreenBright: ["58;5;10", 59],
751
+ underlineYellowBright: ["58;5;11", 59],
752
+ underlineBlueBright: ["58;5;12", 59],
753
+ underlineMagentaBright: ["58;5;13", 59],
754
+ underlineCyanBright: ["58;5;14", 59],
755
+ underlineWhiteBright: ["58;5;15", 59]
741
756
  }
742
- const minuteOfDay = hour * 60 + minute;
743
- return timeMode.workWindows.some((window) => minuteOfDay >= window.startMin && minuteOfDay < window.endMin) ? "work" : "after";
744
- }
745
- function utcFromWall(wallTargetMs) {
746
- let guess = wallTargetMs;
747
- for (let i = 0; i < 2; i++) {
748
- const parts = wallParts(guess);
749
- const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
750
- guess = wallTargetMs - (wall - guess);
757
+ };
758
+ var modifierNames = Object.keys(styles.modifier);
759
+ var foregroundColorNames = Object.keys(styles.color);
760
+ var backgroundColorNames = Object.keys(styles.bgColor);
761
+ var underlineColorNames = Object.keys(styles.underlineColor);
762
+ var colorNames = [...foregroundColorNames, ...backgroundColorNames];
763
+ function assembleStyles() {
764
+ const codes = /* @__PURE__ */ new Map();
765
+ for (const [groupName, group] of Object.entries(styles)) {
766
+ for (const [styleName, style] of Object.entries(group)) {
767
+ styles[styleName] = {
768
+ open: `\x1B[${style[0]}m`,
769
+ close: `\x1B[${style[1]}m`
770
+ };
771
+ group[styleName] = styles[styleName];
772
+ codes.set(Number.parseInt(style[0], 10), style[1]);
773
+ }
774
+ Object.defineProperty(styles, groupName, {
775
+ value: group,
776
+ enumerable: false
777
+ });
751
778
  }
752
- return guess;
779
+ Object.defineProperty(styles, "codes", {
780
+ value: codes,
781
+ enumerable: false
782
+ });
783
+ styles.color.close = "\x1B[39m";
784
+ styles.bgColor.close = "\x1B[49m";
785
+ styles.underlineColor.close = "\x1B[59m";
786
+ styles.color.ansi = wrapAnsi16();
787
+ styles.color.ansi256 = wrapAnsi256();
788
+ styles.color.ansi16m = wrapAnsi16m();
789
+ styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
790
+ styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
791
+ styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
792
+ styles.underlineColor.ansi = wrapUnderlineAnsi;
793
+ styles.underlineColor.ansi256 = wrapAnsi256(ANSI_UNDERLINE_OFFSET);
794
+ styles.underlineColor.ansi16m = wrapAnsi16m(ANSI_UNDERLINE_OFFSET);
795
+ Object.defineProperties(styles, {
796
+ rgbToAnsi256: {
797
+ value(red, green, blue) {
798
+ if (red === green && green === blue) {
799
+ if (red < 8) {
800
+ return 16;
801
+ }
802
+ if (red > 248) {
803
+ return 231;
804
+ }
805
+ return Math.round((red - 8) / 247 * 24) + 232;
806
+ }
807
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
808
+ },
809
+ enumerable: false
810
+ },
811
+ hexToRgb: {
812
+ value(hex) {
813
+ const matches = /[\da-f]{6}|[\da-f]{3}/i.exec(hex.toString(16));
814
+ if (!matches) {
815
+ return [0, 0, 0];
816
+ }
817
+ let [colorString] = matches;
818
+ if (colorString.length === 3) {
819
+ colorString = [...colorString].map((character) => character + character).join("");
820
+ }
821
+ const integer = Number.parseInt(colorString, 16);
822
+ return [
823
+ /* eslint-disable no-bitwise -- We need the speed */
824
+ integer >> 16 & 255,
825
+ integer >> 8 & 255,
826
+ integer & 255
827
+ /* eslint-enable no-bitwise */
828
+ ];
829
+ },
830
+ enumerable: false
831
+ },
832
+ hexToAnsi256: {
833
+ value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
834
+ enumerable: false
835
+ },
836
+ ansi256ToAnsi: {
837
+ value(code) {
838
+ if (code < 8) {
839
+ return 30 + code;
840
+ }
841
+ if (code < 16) {
842
+ return 90 + (code - 8);
843
+ }
844
+ let red;
845
+ let green;
846
+ let blue;
847
+ if (code >= 232) {
848
+ red = ((code - 232) * 10 + 8) / 255;
849
+ green = red;
850
+ blue = red;
851
+ } else {
852
+ code -= 16;
853
+ const remainder = code % 36;
854
+ red = Math.floor(code / 36) / 5;
855
+ green = Math.floor(remainder / 6) / 5;
856
+ blue = remainder % 6 / 5;
857
+ }
858
+ const value2 = Math.max(red, green, blue) * 2;
859
+ if (value2 === 0) {
860
+ return 30;
861
+ }
862
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
863
+ if (value2 === 2) {
864
+ result += 60;
865
+ }
866
+ return result;
867
+ },
868
+ enumerable: false
869
+ },
870
+ rgbToAnsi: {
871
+ value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
872
+ enumerable: false
873
+ },
874
+ hexToAnsi: {
875
+ value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
876
+ enumerable: false
877
+ }
878
+ });
879
+ return styles;
753
880
  }
754
- function businessMsBetween(start, end) {
755
- const startMs = start.getTime();
756
- const endMs = end.getTime();
757
- if (endMs <= startMs) {
881
+ var ansiStyles = assembleStyles();
882
+ var ansi_styles_default = ansiStyles;
883
+
884
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/supports-color/index.js
885
+ import process2 from "node:process";
886
+ import os from "node:os";
887
+ import tty from "node:tty";
888
+ function hasFlag(flag2, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
889
+ const prefix = flag2.startsWith("-") ? "" : flag2.length === 1 ? "-" : "--";
890
+ const position = argv.indexOf(prefix + flag2);
891
+ const terminatorPosition = argv.indexOf("--");
892
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
893
+ }
894
+ var { env } = process2;
895
+ var flagForceColor;
896
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
897
+ flagForceColor = 0;
898
+ } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
899
+ flagForceColor = 1;
900
+ }
901
+ function hasNumericForceColor() {
902
+ return /^\d+$/.test(env.FORCE_COLOR);
903
+ }
904
+ function envForceColor() {
905
+ if (!("FORCE_COLOR" in env)) {
906
+ return;
907
+ }
908
+ if (env.FORCE_COLOR === "false") {
758
909
  return 0;
759
910
  }
760
- let total = 0;
761
- const parts = wallParts(startMs);
762
- let localDay = Date.UTC(parts.year, parts.month - 1, parts.day);
763
- while (utcFromWall(localDay) <= endMs) {
764
- const weekday = new Date(localDay).getUTCDay();
765
- if (weekday !== 0 && weekday !== 6) {
766
- for (const window of timeMode.workWindows) {
767
- const windowStart = utcFromWall(localDay + window.startMin * 6e4);
768
- const windowEnd = utcFromWall(localDay + window.endMin * 6e4);
769
- const overlapStart = Math.max(windowStart, startMs);
770
- const overlapEnd = Math.min(windowEnd, endMs);
771
- if (overlapEnd > overlapStart) {
772
- total += overlapEnd - overlapStart;
773
- }
774
- }
775
- }
776
- localDay += 864e5;
911
+ if (env.FORCE_COLOR === "true" || env.FORCE_COLOR.length === 0) {
912
+ return 1;
777
913
  }
778
- return total;
914
+ if (!hasNumericForceColor()) {
915
+ return;
916
+ }
917
+ return Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
779
918
  }
780
- function durationHours(start, end) {
781
- if (!timeMode.business) {
782
- return (end.getTime() - start.getTime()) / 36e5;
919
+ function translateLevel(level) {
920
+ if (level === 0) {
921
+ return false;
783
922
  }
784
- return businessMsBetween(start, end) / 36e5;
923
+ return {
924
+ level,
925
+ hasBasic: true,
926
+ has256: level >= 2,
927
+ has16m: level >= 3
928
+ };
785
929
  }
786
-
787
- // src/compute.ts
788
- function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
789
- const reviewed = [];
790
- const pending = [];
791
- for (const result of results) {
792
- if (result.kind === "reviewed") {
793
- reviewed.push({
794
- pr: result.pr,
795
- requestedAt: result.requestedAt,
796
- reviewedAt: result.reviewedAt,
797
- hours: durationHours(result.requestedAt, result.reviewedAt),
798
- verdict: result.verdict,
799
- lines: result.lines
800
- });
801
- } else if (result.kind === "pending" && result.pr.state === "open") {
802
- pending.push({ pr: result.pr, requestedAt: result.requestedAt, hours: durationHours(result.requestedAt, now) });
930
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
931
+ const noFlagForceColor = envForceColor();
932
+ if (noFlagForceColor !== void 0) {
933
+ flagForceColor = noFlagForceColor;
934
+ }
935
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
936
+ if (forceColor === 0) {
937
+ return 0;
938
+ }
939
+ if (sniffFlags) {
940
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
941
+ return 3;
942
+ }
943
+ if (hasFlag("color=256")) {
944
+ return 2;
803
945
  }
804
946
  }
805
- pending.sort((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
806
- const pendingKeys = new Set(pending.map((entry) => `${entry.pr.repo}#${entry.pr.number}`));
807
- const latestReviews = /* @__PURE__ */ new Map();
808
- for (const result of results) {
809
- if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
810
- continue;
947
+ if (forceColor !== void 0 && hasNumericForceColor()) {
948
+ return forceColor;
949
+ }
950
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) {
951
+ return 1;
952
+ }
953
+ if (haveStream && !streamIsTTY && forceColor === void 0) {
954
+ return 0;
955
+ }
956
+ const min = forceColor || 0;
957
+ if (env.TERM === "dumb") {
958
+ return min;
959
+ }
960
+ if (process2.platform === "win32") {
961
+ const osRelease = os.release().split(".");
962
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
963
+ return Number(osRelease[2]) >= 14931 ? 3 : 2;
811
964
  }
812
- const key = `${result.pr.repo}#${result.pr.number}`;
813
- if (pendingKeys.has(key)) {
814
- continue;
965
+ return 1;
966
+ }
967
+ if ("CI" in env) {
968
+ if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
969
+ return 3;
815
970
  }
816
- const latest = latestReviews.get(key);
817
- if (latest === void 0 || result.reviewedAt > latest.reviewedAt) {
818
- latestReviews.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
971
+ if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
972
+ return 1;
819
973
  }
974
+ return min;
820
975
  }
821
- const reviewing = [...latestReviews.values()].map(({ pr, reviewedAt }) => {
822
- return { pr, reviewedAt, hours: durationHours(reviewedAt, now) };
823
- }).toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
824
- const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
825
- const unrequested = results.filter((result) => result.kind === "unrequested");
826
- const allHours = reviewed.map((result) => result.hours);
827
- const hoursByRepo = /* @__PURE__ */ new Map();
828
- for (const result of reviewed) {
829
- const hours = hoursByRepo.get(result.pr.repo) ?? [];
830
- hours.push(result.hours);
831
- hoursByRepo.set(result.pr.repo, hours);
976
+ if ("TEAMCITY_VERSION" in env) {
977
+ return /^(?:9\.0*[1-9]\d*\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
832
978
  }
833
- const byRepo = [...hoursByRepo.entries()].toSorted((a, b) => b[1].length - a[1].length);
834
- const misses = targetHours === void 0 ? [] : reviewed.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
835
- const cyclesByPr = /* @__PURE__ */ new Map();
836
- for (const result of reviewed) {
837
- const key = `${result.pr.repo}#${result.pr.number}`;
838
- cyclesByPr.set(key, (cyclesByPr.get(key) ?? 0) + 1);
979
+ if (env.COLORTERM === "truecolor") {
980
+ return 3;
839
981
  }
840
- return {
841
- reviewed,
842
- pending,
843
- reviewing,
844
- expired,
845
- unrequested,
846
- allHours,
847
- byRepo,
848
- misses,
849
- cycles: [...cyclesByPr.values()]
850
- };
851
- }
852
- function computeSizeStats(sizes, { sizeTarget } = {}) {
853
- const metrics = [
854
- { label: "files changed", values: sizes.map((size) => size.files) },
855
- { label: "lines added", values: sizes.map((size) => size.additions) },
856
- { label: "lines removed", values: sizes.map((size) => size.deletions) },
857
- { label: "lines total", values: sizes.map((size) => size.total) }
858
- ];
859
- const timelineTotals = [...sizes].toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime()).map((size) => size.total);
860
- if (sizeTarget === void 0) {
861
- return { metrics, timelineTotals, met: void 0, misses: [], targetLabel: void 0 };
982
+ if (env.TERM === "xterm-kitty") {
983
+ return 3;
862
984
  }
863
- const meetsTarget = (size) => (sizeTarget.lines === void 0 || size.total <= sizeTarget.lines) && (sizeTarget.files === void 0 || size.files <= sizeTarget.files);
864
- const targetLabel = [
865
- ...sizeTarget.lines === void 0 ? [] : [`<= ${sizeTarget.lines} lines`],
866
- ...sizeTarget.files === void 0 ? [] : [`<= ${sizeTarget.files} files`]
867
- ].join(", ");
868
- const met = sizes.filter((size) => meetsTarget(size)).length;
869
- const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
870
- return { metrics, timelineTotals, met, misses, targetLabel };
871
- }
872
- function computeMergeStats(sizes) {
873
- const merged = [];
874
- const closed = [];
875
- const open = [];
876
- for (const entry of sizes) {
877
- if (entry.mergedAt !== null) {
878
- merged.push({ entry, mergedAt: entry.mergedAt, hours: durationHours(entry.pr.createdAt, entry.mergedAt) });
879
- } else if (entry.pr.state === "open") {
880
- open.push(entry);
881
- } else if (entry.closedAt !== null) {
882
- closed.push({ entry, closedAt: entry.closedAt, hours: durationHours(entry.pr.createdAt, entry.closedAt) });
883
- }
985
+ if (env.TERM === "xterm-ghostty") {
986
+ return 3;
884
987
  }
885
- merged.sort((a, b) => b.mergedAt.getTime() - a.mergedAt.getTime());
886
- closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
887
- return { merged, closed, open, allHours: merged.map((result) => result.hours) };
888
- }
889
- function computeReviewerStats(sizes, author) {
890
- const byLogin = /* @__PURE__ */ new Map();
891
- let mergedReviewed = 0;
892
- let mergedUnreviewed = 0;
893
- for (const entry of sizes) {
894
- const others = entry.reviews.flatMap(
895
- (review) => review.login === null || review.login === author ? [] : [review.login]
896
- );
897
- for (const login of others) {
898
- const counts = byLogin.get(login) ?? { prs: 0, reviews: 0 };
899
- counts.reviews += 1;
900
- byLogin.set(login, counts);
901
- }
902
- const distinct = new Set(others);
903
- for (const login of distinct) {
904
- const counts = byLogin.get(login);
905
- if (counts !== void 0) {
906
- counts.prs += 1;
988
+ if (env.TERM === "wezterm") {
989
+ return 3;
990
+ }
991
+ if ("TERM_PROGRAM" in env) {
992
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".", 1)[0], 10);
993
+ switch (env.TERM_PROGRAM) {
994
+ case "iTerm.app": {
995
+ return version >= 3 ? 3 : 2;
907
996
  }
908
- }
909
- if (entry.mergedAt !== null) {
910
- if (others.length > 0) {
911
- mergedReviewed += 1;
912
- } else {
913
- mergedUnreviewed += 1;
997
+ case "Apple_Terminal": {
998
+ return 2;
914
999
  }
915
1000
  }
916
1001
  }
917
- const leaderboard = [...byLogin.entries()].map(([login, counts]) => {
918
- return { login, ...counts };
919
- }).toSorted((a, b) => b.prs - a.prs || b.reviews - a.reviews || a.login.localeCompare(b.login));
920
- return { leaderboard, mergedReviewed, mergedUnreviewed };
921
- }
922
- function firstReviewOf(entry, author) {
923
- let earliest = null;
924
- for (const review of entry.reviews) {
925
- if (review.login === null || review.login === author || review.submittedAt === null) {
926
- continue;
927
- }
928
- if (earliest === null || review.submittedAt < earliest) {
929
- earliest = review.submittedAt;
930
- }
1002
+ if (/-256(?:color)?$/i.test(env.TERM)) {
1003
+ return 2;
931
1004
  }
932
- if (earliest === null) {
933
- return null;
1005
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
1006
+ return 1;
934
1007
  }
935
- return { reviewedAt: earliest, hours: durationHours(entry.pr.createdAt, earliest) };
936
- }
937
- function computeFirstReviewStats(sizes, author, { now = /* @__PURE__ */ new Date() } = {}) {
938
- const received = [];
939
- const awaiting = [];
940
- for (const entry of sizes) {
941
- const first = firstReviewOf(entry, author);
942
- if (first !== null) {
943
- received.push({ entry, ...first });
944
- } else if (entry.pr.state === "open") {
945
- awaiting.push({ entry, hours: durationHours(entry.pr.createdAt, now) });
946
- }
1008
+ if ("COLORTERM" in env) {
1009
+ return 1;
947
1010
  }
948
- awaiting.sort((a, b) => a.entry.pr.createdAt.getTime() - b.entry.pr.createdAt.getTime());
949
- return { received, awaiting, allHours: received.map((result) => result.hours) };
1011
+ return min;
950
1012
  }
951
- function computeCommentStats(sizes) {
952
- const metrics = [
953
- { label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
954
- { label: "review comments", values: sizes.map((size) => size.comments.review) },
955
- { label: "all comments", values: sizes.map((size) => size.comments.total) }
956
- ];
957
- const totals = sizes.map((size) => size.comments.total);
958
- const uncommented = totals.filter((total) => total === 0).length;
959
- const top = sizes.filter((size) => size.comments.total > 0).toSorted((a, b) => b.comments.total - a.comments.total);
960
- return { metrics, totals, uncommented, top };
1013
+ function createSupportsColor(stream, options = {}) {
1014
+ const level = _supportsColor(stream, {
1015
+ streamIsTTY: stream && stream.isTTY,
1016
+ ...options
1017
+ });
1018
+ return translateLevel(level);
961
1019
  }
1020
+ var supportsColor = {
1021
+ stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
1022
+ stderr: createSupportsColor({ isTTY: tty.isatty(2) })
1023
+ };
1024
+ var supports_color_default = supportsColor;
962
1025
 
963
- // src/report.ts
964
- var BUCKETS = [];
965
- function initBuckets() {
966
- BUCKETS = makeBuckets();
967
- }
968
- function currentBuckets() {
969
- return BUCKETS;
970
- }
971
- function makeBuckets() {
972
- if (!timeMode.business || isFullDayMode()) {
973
- return [
974
- { label: "< 1h", max: 1 },
975
- { label: "1-4h", max: 4 },
976
- { label: "4-8h", max: 8 },
977
- { label: "8-24h", max: 24 },
978
- { label: "1-2d", max: 48 },
979
- { label: "2-4d", max: 96 },
980
- { label: "4-7d", max: 168 },
981
- { label: "> 7d", max: Infinity }
982
- ];
1026
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/index.js
1027
+ var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
1028
+ var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
1029
+ var STYLER = /* @__PURE__ */ Symbol("STYLER");
1030
+ var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
1031
+ var LEVEL = /* @__PURE__ */ Symbol("LEVEL");
1032
+ var styles2 = /* @__PURE__ */ Object.create(null);
1033
+ var assertValidLevel = (level) => {
1034
+ if (!Number.isSafeInteger(level) || level < 0 || level > 3) {
1035
+ throw new Error("The `level` should be an integer from 0 to 3");
983
1036
  }
984
- const wd = timeMode.dayHours;
985
- return [
986
- { label: "< 1h", max: 1 },
987
- { label: "1-4h", max: 4 },
988
- { label: "4h-1wd", max: wd },
989
- { label: "1-2wd", max: 2 * wd },
990
- { label: "2-3wd", max: 3 * wd },
991
- { label: "3-5wd", max: 5 * wd },
992
- { label: "5-10wd", max: 10 * wd },
993
- { label: "> 10wd", max: Infinity }
994
- ];
995
- }
996
- var LINE_BUCKETS = [
997
- { label: "< 50", max: 50 },
998
- { label: "50-100", max: 100 },
999
- { label: "100-250", max: 250 },
1000
- { label: "250-500", max: 500 },
1001
- { label: "500-1k", max: 1e3 },
1002
- { label: "1k-2.5k", max: 2500 },
1003
- { label: "2.5k-5k", max: 5e3 },
1004
- { label: "> 5k", max: Infinity }
1005
- ];
1006
- var FILE_BUCKETS = [
1007
- { label: "1-2", max: 3 },
1008
- { label: "3-5", max: 6 },
1009
- { label: "6-10", max: 11 },
1010
- { label: "11-20", max: 21 },
1011
- { label: "21-50", max: 51 },
1012
- { label: "> 50", max: Infinity }
1013
- ];
1014
- var CYCLE_BUCKETS = [
1015
- { label: "1", max: 2 },
1016
- { label: "2", max: 3 },
1017
- { label: "3", max: 4 },
1018
- { label: "4-5", max: 6 },
1019
- { label: "> 5", max: Infinity }
1020
- ];
1021
- var COMMENT_BUCKETS = [
1022
- { label: "0", max: 1 },
1023
- { label: "1-2", max: 3 },
1024
- { label: "3-5", max: 6 },
1025
- { label: "6-10", max: 11 },
1026
- { label: "11-20", max: 21 },
1027
- { label: "21-50", max: 51 },
1028
- { label: "> 50", max: Infinity }
1029
- ];
1030
- function formatHoursOnly(hours) {
1031
- if (hours < 1) {
1032
- return `${Math.round(hours * 60)}m`;
1037
+ };
1038
+ var levelDescriptor = {
1039
+ enumerable: true,
1040
+ get() {
1041
+ return this[LEVEL];
1042
+ },
1043
+ set(level) {
1044
+ assertValidLevel(level);
1045
+ this[LEVEL] = level;
1033
1046
  }
1034
- return `${hours.toFixed(1)}h`;
1035
- }
1036
- function weeksSuffix(hours) {
1037
- const weekHours = timeMode.business ? 5 * timeMode.dayHours : 7 * 24;
1038
- if (hours < weekHours) {
1039
- return "";
1047
+ };
1048
+ var applyOptions = (object, options = {}) => {
1049
+ if (options.level !== void 0) {
1050
+ assertValidLevel(options.level);
1040
1051
  }
1041
- return ` (${(hours / weekHours).toFixed(1)} weeks)`;
1052
+ const colorLevel = stdoutColor ? stdoutColor.level : 0;
1053
+ object[LEVEL] = options.level === void 0 ? colorLevel : options.level;
1054
+ };
1055
+ var chalkFactory = (options) => {
1056
+ const chalk2 = (...strings) => strings.join(" ");
1057
+ applyOptions(chalk2, options);
1058
+ Object.setPrototypeOf(chalk2, createChalk.prototype);
1059
+ return chalk2;
1060
+ };
1061
+ function createChalk(options) {
1062
+ return chalkFactory(options);
1042
1063
  }
1043
- function formatCount(value2) {
1044
- if (value2 < 1e3) {
1045
- return String(value2);
1064
+ Object.setPrototypeOf(createChalk.prototype, Function.prototype);
1065
+ for (const [styleName, style] of Object.entries(ansi_styles_default)) {
1066
+ styles2[styleName] = {
1067
+ get() {
1068
+ const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
1069
+ Object.defineProperty(this, styleName, { value: builder });
1070
+ return builder;
1071
+ }
1072
+ };
1073
+ }
1074
+ styles2.visible = {
1075
+ get() {
1076
+ const builder = createBuilder(this, this[STYLER], true);
1077
+ Object.defineProperty(this, "visible", { value: builder });
1078
+ return builder;
1079
+ }
1080
+ };
1081
+ var createModelConverters = (model, type) => {
1082
+ const style = ansi_styles_default[type];
1083
+ if (model === "rgb") {
1084
+ const ansi2 = (red, green, blue) => style.ansi(ansi_styles_default.rgbToAnsi(red, green, blue));
1085
+ const ansi256 = (red, green, blue) => style.ansi256(ansi_styles_default.rgbToAnsi256(red, green, blue));
1086
+ return [ansi2, ansi2, ansi256, style.ansi16m];
1087
+ }
1088
+ if (model === "hex") {
1089
+ const ansi2 = (hex) => style.ansi(ansi_styles_default.hexToAnsi(hex));
1090
+ const ansi256 = (hex) => style.ansi256(ansi_styles_default.hexToAnsi256(hex));
1091
+ return [ansi2, ansi2, ansi256, (hex) => style.ansi16m(...ansi_styles_default.hexToRgb(hex))];
1092
+ }
1093
+ const ansi = (code) => style.ansi(ansi_styles_default.ansi256ToAnsi(code));
1094
+ return [ansi, ansi, style.ansi256, style.ansi256];
1095
+ };
1096
+ var usedModels = ["rgb", "hex", "ansi256"];
1097
+ for (const model of usedModels) {
1098
+ const capitalizedModel = model[0].toUpperCase() + model.slice(1);
1099
+ for (const [styleName, type] of [
1100
+ [model, "color"],
1101
+ ["bg" + capitalizedModel, "bgColor"],
1102
+ ["underline" + capitalizedModel, "underlineColor"]
1103
+ ]) {
1104
+ const { close } = ansi_styles_default[type];
1105
+ const converters = createModelConverters(model, type);
1106
+ styles2[styleName] = {
1107
+ get() {
1108
+ const styleFunction = function(first, second, third) {
1109
+ const open = converters[this.level](first, second, third);
1110
+ return createBuilder(this, createStyler(open, close, this[STYLER]), this[IS_EMPTY]);
1111
+ };
1112
+ Object.defineProperty(this, styleName, { value: styleFunction });
1113
+ return styleFunction;
1114
+ }
1115
+ };
1046
1116
  }
1047
- const scaled = value2 / 1e3;
1048
- return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
1049
1117
  }
1118
+ var proto = Object.defineProperties(
1119
+ () => {
1120
+ },
1121
+ {
1122
+ ...styles2,
1123
+ level: {
1124
+ enumerable: true,
1125
+ get() {
1126
+ return this[GENERATOR].level;
1127
+ },
1128
+ set(level) {
1129
+ this[GENERATOR].level = level;
1130
+ }
1131
+ }
1132
+ }
1133
+ );
1134
+ var createStyler = (open, close, parent) => {
1135
+ let openAll;
1136
+ let closeAll;
1137
+ if (parent === void 0) {
1138
+ openAll = open;
1139
+ closeAll = close;
1140
+ } else {
1141
+ openAll = parent.openAll + open;
1142
+ closeAll = close + parent.closeAll;
1143
+ }
1144
+ return {
1145
+ open,
1146
+ close,
1147
+ openAll,
1148
+ closeAll,
1149
+ parent
1150
+ };
1151
+ };
1152
+ var createBuilder = (self, _styler, _isEmpty) => {
1153
+ const builder = (...arguments_) => {
1154
+ if (arguments_.length === 1) {
1155
+ return applyStyle(builder, "" + arguments_[0]);
1156
+ }
1157
+ if (arguments_.length === 2) {
1158
+ return applyStyle(builder, arguments_[0] + " " + arguments_[1]);
1159
+ }
1160
+ return applyStyle(builder, arguments_.join(" "));
1161
+ };
1162
+ Object.setPrototypeOf(builder, proto);
1163
+ builder[GENERATOR] = self[GENERATOR] ?? self;
1164
+ builder[STYLER] = _styler;
1165
+ builder[IS_EMPTY] = _isEmpty;
1166
+ return builder;
1167
+ };
1168
+ var applyStyle = (self, string) => {
1169
+ if (self[GENERATOR][LEVEL] <= 0 || !string) {
1170
+ return self[IS_EMPTY] ? "" : string;
1171
+ }
1172
+ let styler = self[STYLER];
1173
+ if (styler === void 0) {
1174
+ return string;
1175
+ }
1176
+ const { openAll, closeAll } = styler;
1177
+ if (string.includes("\x1B")) {
1178
+ while (styler !== void 0) {
1179
+ string = stringReplaceAll(string, styler.close, styler.open);
1180
+ styler = styler.parent;
1181
+ }
1182
+ }
1183
+ const lfIndex = string.indexOf("\n");
1184
+ if (lfIndex !== -1) {
1185
+ string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
1186
+ }
1187
+ return openAll + string + closeAll;
1188
+ };
1189
+ Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
1190
+ var chalk = createChalk();
1191
+ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
1192
+ var source_default = chalk;
1050
1193
 
1051
- // src/tui/views/rows.ts
1052
- function durationLead(result) {
1053
- return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
1054
- }
1055
- function toPrRows(entries, leads) {
1056
- const width = Math.max(...leads.map((lead) => lead.length));
1057
- return entries.map((entry, i) => {
1058
- return {
1059
- lead: leads[i].padEnd(width),
1060
- ref: `${entry.pr.repo}#${entry.pr.number}`,
1061
- url: entry.pr.url,
1062
- title: entry.pr.title
1063
- };
1194
+ // src/flags.ts
1195
+ import { parseArgs } from "node:util";
1196
+
1197
+ // src/time.ts
1198
+ function wallFormatter(tz) {
1199
+ return new Intl.DateTimeFormat("en-US", {
1200
+ timeZone: tz,
1201
+ hourCycle: "h23",
1202
+ year: "numeric",
1203
+ month: "2-digit",
1204
+ day: "2-digit",
1205
+ hour: "2-digit",
1206
+ minute: "2-digit",
1207
+ second: "2-digit"
1064
1208
  });
1065
1209
  }
1066
-
1067
- // src/tui/views/queue.ts
1068
- function queueRows(view) {
1069
- return view.sections.flatMap((section) => [...section.rows, ...section.lists.flatMap((list) => list.rows)]);
1210
+ var timeMode = {
1211
+ business: true,
1212
+ workWindows: [{ startMin: 0, endMin: 24 * 60 }],
1213
+ workDays: /* @__PURE__ */ new Set([1, 2, 3, 4, 5]),
1214
+ dayHours: 24,
1215
+ formatter: wallFormatter("UTC")
1216
+ };
1217
+ function configureTimeMode({
1218
+ business,
1219
+ workWindows,
1220
+ workDays,
1221
+ tz
1222
+ }) {
1223
+ const workMinutesPerDay = workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
1224
+ timeMode.business = business;
1225
+ timeMode.workWindows = workWindows;
1226
+ timeMode.workDays = workDays;
1227
+ timeMode.dayHours = business ? workMinutesPerDay / 60 : 24;
1228
+ timeMode.formatter = wallFormatter(tz);
1070
1229
  }
1071
- function groupedLists(entries, rowsOf2) {
1072
- const groups = /* @__PURE__ */ new Map();
1073
- for (const entry of entries) {
1074
- const group = groups.get(entry.pr.repo) ?? [];
1075
- group.push(entry);
1076
- groups.set(entry.pr.repo, group);
1077
- }
1078
- return [...groups.entries()].toSorted((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])).map(([repo, group]) => {
1079
- return { title: `${repo} (n=${group.length})`, rows: rowsOf2(group) };
1080
- });
1230
+ function isFullDayMode() {
1231
+ return timeMode.business && timeMode.dayHours === 24;
1081
1232
  }
1082
- function buildPendingReviewView(raw, repo = null, grouped = false) {
1083
- const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
1084
- const awaiting = repo === null ? stats.pending : stats.pending.filter((entry) => entry.pr.repo === repo);
1085
- const reviewing = repo === null ? stats.reviewing : stats.reviewing.filter((entry) => entry.pr.repo === repo);
1086
- if (awaiting.length === 0 && reviewing.length === 0) {
1087
- return { empty: "No PRs are awaiting your review, and none you reviewed are still open.", sections: [] };
1088
- }
1089
- const split = repo === null && grouped;
1090
- const sectionOf = (title, entries) => split ? { title, rows: [], lists: groupedLists(entries, rowsOf) } : { title, rows: rowsOf(entries), lists: [] };
1233
+ function wallParts(instantMs) {
1234
+ const parts = Object.fromEntries(timeMode.formatter.formatToParts(instantMs).map((part) => [part.type, part.value]));
1091
1235
  return {
1092
- empty: null,
1093
- sections: [
1094
- ...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting)],
1095
- ...reviewing.length === 0 ? [] : [sectionOf(`Reviewing (n=${reviewing.length})`, reviewing)]
1096
- ]
1236
+ year: Number(parts.year),
1237
+ month: Number(parts.month),
1238
+ day: Number(parts.day),
1239
+ hour: Number(parts.hour),
1240
+ minute: Number(parts.minute),
1241
+ second: Number(parts.second)
1097
1242
  };
1098
1243
  }
1099
- function buildOpenAuthoredView(raw, repo = null, grouped = false) {
1100
- const open = raw.sizes.filter((entry) => entry.pr.state === "open" && (repo === null || entry.pr.repo === repo)).toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime());
1101
- if (open.length === 0) {
1102
- return { empty: "No open authored PRs found.", sections: [] };
1244
+ function zonedStamp(date) {
1245
+ const parts = wallParts(date.getTime());
1246
+ const dayUtcMs = Date.UTC(parts.year, parts.month - 1, parts.day);
1247
+ return { dayUtcMs, weekday: new Date(dayUtcMs).getUTCDay(), hour: parts.hour, minute: parts.minute };
1248
+ }
1249
+ function hasWorkWindows() {
1250
+ const covered = timeMode.workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
1251
+ return covered < 24 * 60;
1252
+ }
1253
+ function classifyInstant(date) {
1254
+ const { weekday, hour, minute } = zonedStamp(date);
1255
+ if (!timeMode.workDays.has(weekday)) {
1256
+ return "weekend";
1103
1257
  }
1104
- const rowsOf2 = (group) => {
1105
- const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
1106
- const ageWidth = Math.max(...ages.map((age) => age.length));
1107
- return toPrRows(
1108
- group,
1109
- group.map(
1110
- (entry, i) => `${ages[i].padEnd(ageWidth)} +${entry.additions}/-${entry.deletions}, ${entry.files} files`
1111
- )
1112
- );
1113
- };
1114
- const title = `Your open authored PRs (n=${open.length})`;
1115
- if (repo === null && grouped) {
1116
- return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf2) }] };
1258
+ const minuteOfDay = hour * 60 + minute;
1259
+ return timeMode.workWindows.some((window) => minuteOfDay >= window.startMin && minuteOfDay < window.endMin) ? "work" : "after";
1260
+ }
1261
+ function utcFromWall(wallTargetMs) {
1262
+ let guess = wallTargetMs;
1263
+ for (let i = 0; i < 2; i++) {
1264
+ const parts = wallParts(guess);
1265
+ const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
1266
+ guess = wallTargetMs - (wall - guess);
1117
1267
  }
1118
- return { empty: null, sections: [{ title, rows: rowsOf2(open), lists: [] }] };
1268
+ return guess;
1119
1269
  }
1120
- function rowsOf(group) {
1121
- return toPrRows(
1122
- group,
1123
- group.map((entry) => durationLead(entry))
1124
- );
1125
- }
1126
-
1127
- // src/tui/components/ChartsPanel.tsx
1128
- import { useTerminalDimensions } from "@opentui/react";
1129
-
1130
- // src/tui/hooks/scrollbar.ts
1131
- import { CliRenderEvents } from "@opentui/core";
1132
- import { useRenderer } from "@opentui/react";
1133
- import { useLayoutEffect } from "react";
1134
- var overlayScrollbar = {
1135
- position: "absolute",
1136
- top: 0,
1137
- right: 0,
1138
- bottom: 0
1139
- };
1140
- function useScrollbarSettle(scrollRef, mounted = true) {
1141
- const renderer2 = useRenderer();
1142
- useLayoutEffect(() => {
1143
- if (!mounted) {
1144
- return void 0;
1145
- }
1146
- if (!scrollRef.current?.verticalScrollBar) {
1147
- return void 0;
1148
- }
1149
- scrollRef.current.verticalScrollBar.visible = false;
1150
- const release = () => {
1151
- renderer2.off(CliRenderEvents.FRAME, release);
1152
- scrollRef.current?.verticalScrollBar.resetVisibilityControl();
1153
- };
1154
- const resyncThumb = () => {
1155
- const bar = scrollRef.current?.verticalScrollBar;
1156
- if (!bar) {
1157
- return;
1158
- }
1159
- const height = bar.viewportSize;
1160
- const wanted = Math.max(1, height);
1161
- if (bar.scrollSize < wanted || bar.slider.viewPortSize === wanted) {
1162
- return;
1270
+ function businessMsBetween(start, end) {
1271
+ const startMs = start.getTime();
1272
+ const endMs = end.getTime();
1273
+ if (endMs <= startMs) {
1274
+ return 0;
1275
+ }
1276
+ let total = 0;
1277
+ const parts = wallParts(startMs);
1278
+ let localDay = Date.UTC(parts.year, parts.month - 1, parts.day);
1279
+ while (utcFromWall(localDay) <= endMs) {
1280
+ const weekday = new Date(localDay).getUTCDay();
1281
+ if (timeMode.workDays.has(weekday)) {
1282
+ for (const window of timeMode.workWindows) {
1283
+ const windowStart = utcFromWall(localDay + window.startMin * 6e4);
1284
+ const windowEnd = utcFromWall(localDay + window.endMin * 6e4);
1285
+ const overlapStart = Math.max(windowStart, startMs);
1286
+ const overlapEnd = Math.min(windowEnd, endMs);
1287
+ if (overlapEnd > overlapStart) {
1288
+ total += overlapEnd - overlapStart;
1289
+ }
1163
1290
  }
1164
- bar.viewportSize = 0;
1165
- bar.viewportSize = height;
1166
- };
1167
- renderer2.on(CliRenderEvents.FRAME, release);
1168
- renderer2.on(CliRenderEvents.FRAME, resyncThumb);
1169
- return () => {
1170
- renderer2.off(CliRenderEvents.FRAME, release);
1171
- renderer2.off(CliRenderEvents.FRAME, resyncThumb);
1172
- };
1173
- }, [scrollRef, renderer2, mounted]);
1291
+ }
1292
+ localDay += 864e5;
1293
+ }
1294
+ return total;
1174
1295
  }
1175
-
1176
- // src/tui/views/charts/model.ts
1177
- function lineLength(line) {
1178
- return line.reduce((sum, span) => sum + span.text.length, 0);
1296
+ function durationHours(start, end) {
1297
+ if (!timeMode.business) {
1298
+ return (end.getTime() - start.getTime()) / 36e5;
1299
+ }
1300
+ return businessMsBetween(start, end) / 36e5;
1179
1301
  }
1180
- function cardWidth(card) {
1181
- const subtitleLength = typeof card.subtitle === "string" ? card.subtitle.length : lineLength(card.subtitle);
1182
- return Math.max(card.title.length + 2 + subtitleLength, ...card.lines.map(lineLength));
1302
+
1303
+ // src/flags.ts
1304
+ var flag = source_default.green;
1305
+ var value = source_default.cyan;
1306
+ var dim = source_default.dim;
1307
+ var OPTIONS = [
1308
+ {
1309
+ name: "since",
1310
+ short: "s",
1311
+ type: "string",
1312
+ default: "90d",
1313
+ placeholder: "<value>",
1314
+ help: "Only include PRs created after this point. Accepts an ISO date (`2026-01-01`) or a relative value (`30d`, `8w`, `6m`, `1y`). The default is `90d`."
1315
+ },
1316
+ {
1317
+ name: "repo",
1318
+ short: "r",
1319
+ type: "string",
1320
+ multiple: true,
1321
+ default: [],
1322
+ placeholder: "<name>",
1323
+ help: 'Restrict the search to a repository. Repeat the flag for multiple repositories. Accepts "owner/name" or a bare name, which resolves against the owner of the repo in the current directory. Without this flag, all repositories you have access to are searched.'
1324
+ },
1325
+ {
1326
+ name: "user",
1327
+ short: "u",
1328
+ type: "string",
1329
+ placeholder: "<login>",
1330
+ help: "Compute stats for this user. Defaults to the authenticated user."
1331
+ },
1332
+ {
1333
+ name: "token",
1334
+ type: "string",
1335
+ placeholder: "<token>",
1336
+ help: "GitHub access token. When set, the tool calls the GitHub API directly instead of going through the gh CLI. The `GITHUB_TOKEN` and `GH_TOKEN` environment variables work as well."
1337
+ },
1338
+ {
1339
+ name: "target",
1340
+ short: "t",
1341
+ type: "string",
1342
+ placeholder: "<value>",
1343
+ help: "Report how many reviews finished within this time. Accepts hours (`24h` or plain `24`), minutes (`90m`), or days (`2d`). A day means 24 counted hours, or one working day when --work-hours is set. Open PRs that have already waited longer than the target count as misses."
1344
+ },
1345
+ {
1346
+ name: "target-percentile",
1347
+ type: "string",
1348
+ placeholder: "<p>",
1349
+ help: "Check the --target against this percentile of your review times. Accepts a whole percentile from 1 to 100, with an optional p prefix (`90` or `p90`). The default is `90`, so the review headline reports whether your p90 review time meets the target."
1350
+ },
1351
+ {
1352
+ name: "size-target",
1353
+ type: "string",
1354
+ placeholder: "<v>",
1355
+ help: "Report how many authored PRs fit within this size. Accepts a line budget (`400` or `400l`), a file budget (`20f`), or both (`400l,20f`). Lines count additions plus deletions."
1356
+ },
1357
+ {
1358
+ name: "tz",
1359
+ type: "string",
1360
+ placeholder: "<zone>",
1361
+ help: "IANA timezone for the weekend and working-hours math, for example `Europe/Berlin`. Defaults to your system timezone."
1362
+ },
1363
+ {
1364
+ name: "work-days",
1365
+ type: "string",
1366
+ default: "mon-fri",
1367
+ placeholder: "<days>",
1368
+ help: "Count only these days as working days. Accepts comma-separated weekday names and ranges like `mon-fri`, `sun-thu`, or `mon,wed,fri`. A range may wrap around the end of the week, so `sat-wed` works too. Days outside the set count as weekend."
1369
+ },
1370
+ {
1371
+ name: "work-hours",
1372
+ short: "w",
1373
+ type: "string",
1374
+ default: "0-24",
1375
+ placeholder: "<v>",
1376
+ help: "Count only these working hours instead of full working days. Accepts 24-hour ranges like `9-17` or `8:30-16:30`, 12-hour ranges like `9am-6pm` or `8:30am-4:30pm`, or several comma-separated ranges like `9-18,19:30-20:30`. Without this flag, every hour on a working day counts."
1377
+ },
1378
+ {
1379
+ name: "wall-clock",
1380
+ type: "boolean",
1381
+ default: false,
1382
+ help: "Measure raw elapsed time, including weekends."
1383
+ },
1384
+ {
1385
+ name: "include-drafts",
1386
+ type: "boolean",
1387
+ default: false,
1388
+ help: "Include PRs that are currently drafts. Excluded by default."
1389
+ },
1390
+ {
1391
+ name: "review-types",
1392
+ type: "string",
1393
+ placeholder: "<list>",
1394
+ help: "Count only these review types as a review. Accepts a comma-separated list of `approve`, `comment`, and `request-changes`, for example `approve,request-changes`. A review of another type never answers a request, so the PR stays in the awaiting queue until a counted review lands. Without this flag, every submitted review counts."
1395
+ },
1396
+ {
1397
+ name: "no-cache",
1398
+ type: "boolean",
1399
+ default: false,
1400
+ help: "Refetch every PR instead of reading the local disk cache. Closed PRs are normally served from a per-PR cache because their timelines and sizes no longer change. Fresh results still update the cache. The TUI settings dialog can save this behavior for every run, and the flag wins over the saved setting."
1401
+ },
1402
+ {
1403
+ name: "json",
1404
+ type: "boolean",
1405
+ default: false,
1406
+ help: "Print every stat as JSON to stdout instead of starting the TUI, so the output can be piped into jq or redirected to a file. The report holds the review, size, merge, reviewer, and comment stats with one entry per PR, and every other flag applies to it the same way. Progress renders on stderr, so a piped stdout stays pure JSON."
1407
+ },
1408
+ {
1409
+ name: "debug",
1410
+ type: "string",
1411
+ placeholder: "<path>",
1412
+ help: "Serve canned data from a fake gh binary instead of fetching from GitHub. Accepts a path to a testdata directory that contains a `gh` executable, for example `tui/testdata`, or a path to the executable itself. Tokens are ignored while this flag is set. This is useful for testing and local development."
1413
+ },
1414
+ {
1415
+ name: "help",
1416
+ short: "h",
1417
+ type: "boolean",
1418
+ default: false,
1419
+ help: "Show this help."
1420
+ }
1421
+ ];
1422
+ var HELP_WIDTH = Math.min(process.stdout.isTTY ? process.stdout.columns : 80, 120);
1423
+ function colorizeHelp(text) {
1424
+ return text.replaceAll(/`([^`]+)`/g, (match, span) => value(span)).replaceAll(/"[^"]*"/g, (span) => source_default.yellow(span)).replaceAll(/--[a-z][a-z-]*/g, (span) => flag(span));
1183
1425
  }
1184
- function formatDuration(hours) {
1185
- if (hours < 1) {
1186
- return `${Math.round(hours * 60)}m`;
1426
+ function wrapMarkup(text, width) {
1427
+ const lines = [];
1428
+ let line = "";
1429
+ for (const word of text.split(" ")) {
1430
+ const candidate = line === "" ? word : `${line} ${word}`;
1431
+ if (line !== "" && candidate.replaceAll("`", "").length > width) {
1432
+ lines.push(line);
1433
+ line = word;
1434
+ } else {
1435
+ line = candidate;
1436
+ }
1187
1437
  }
1188
- if (hours < 2 * timeMode.dayHours) {
1189
- return `${compact(hours)}h`;
1438
+ if (line !== "") {
1439
+ lines.push(line);
1190
1440
  }
1191
- return `${compact(hours / timeMode.dayHours)}${timeMode.business && !isFullDayMode() ? "wd" : "d"}`;
1192
- }
1193
- function compact(value2) {
1194
- return String(Math.round(value2 * 10) / 10);
1441
+ return lines;
1195
1442
  }
1196
-
1197
- // src/tui/components/ChartsPanel.tsx
1198
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
1199
- var COLUMN_GAP = 4;
1200
- function ChartsPanel({
1201
- scrollRef,
1202
- focused,
1203
- heading,
1204
- warning,
1205
- view
1206
- }) {
1207
- useScrollbarSettle(scrollRef, view.empty === null);
1208
- const { width } = useTerminalDimensions();
1209
- if (view.empty !== null) {
1210
- return /* @__PURE__ */ jsx4("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx4("text", { fg: theme.muted, children: view.empty }) });
1211
- }
1212
- const left = view.cards.filter((_, i) => i % 2 === 0);
1213
- const right = view.cards.filter((_, i) => i % 2 === 1);
1214
- const leftWidth = Math.max(0, ...left.map((card) => cardWidth(card)));
1215
- const rightWidth = Math.max(0, ...right.map((card) => cardWidth(card)));
1216
- const twoColumns = right.length > 0 && width - 4 >= leftWidth + COLUMN_GAP + rightWidth;
1217
- const rows = left.map((card, i) => {
1218
- return { key: card.title, left: card, right: right.at(i) };
1443
+ function renderHelp() {
1444
+ const labels = OPTIONS.map((option) => {
1445
+ const short = option.short ? `-${option.short}, ` : "";
1446
+ const long = option.placeholder ? `--${option.name} ${option.placeholder}` : `--${option.name}`;
1447
+ return short + long;
1219
1448
  });
1220
- const cards = twoColumns ? /* @__PURE__ */ jsx4("box", { flexDirection: "column", rowGap: 1, children: rows.map((row) => /* @__PURE__ */ jsxs3("box", { flexDirection: "row", alignItems: "flex-start", columnGap: COLUMN_GAP, children: [
1221
- /* @__PURE__ */ jsx4("box", { flexDirection: "column", width: leftWidth, flexShrink: 0, children: /* @__PURE__ */ jsx4(ChartCard, { card: row.left }) }),
1222
- /* @__PURE__ */ jsx4("box", { flexDirection: "column", width: rightWidth, flexShrink: 0, children: row.right !== void 0 && /* @__PURE__ */ jsx4(ChartCard, { card: row.right }) })
1223
- ] }, row.key)) }) : /* @__PURE__ */ jsx4("box", { flexDirection: "column", rowGap: 1, children: view.cards.map((card) => /* @__PURE__ */ jsx4(ChartCard, { card }, card.title)) });
1224
- return /* @__PURE__ */ jsxs3("box", { flexGrow: 1, flexDirection: "column", children: [
1225
- /* @__PURE__ */ jsx4("box", { height: 1, children: /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
1226
- /* @__PURE__ */ jsx4("box", { height: 1, paddingLeft: 1, paddingRight: 2, flexDirection: "row", justifyContent: "space-between", children: keyed(view.strip, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) }),
1227
- (heading !== null || view.headline !== null) && /* @__PURE__ */ jsxs3("box", { height: 1, paddingLeft: 1, paddingRight: 2, flexDirection: "row", justifyContent: "space-between", children: [
1228
- /* @__PURE__ */ jsx4("text", { wrapMode: "none", children: heading !== null && /* @__PURE__ */ jsxs3(Fragment2, { children: [
1229
- /* @__PURE__ */ jsx4("span", { fg: theme.accent, children: "\u25B8 " }),
1230
- /* @__PURE__ */ jsx4("b", { fg: theme.text, children: heading })
1231
- ] }) }),
1232
- view.headline !== null && /* @__PURE__ */ jsx4(ChartLine, { line: view.headline })
1233
- ] }),
1234
- /* @__PURE__ */ jsx4("box", { height: 1, children: /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
1235
- /* @__PURE__ */ jsxs3(
1236
- "scrollbox",
1237
- {
1238
- ref: scrollRef,
1239
- focused,
1240
- flexGrow: 1,
1241
- paddingLeft: 1,
1242
- paddingRight: 2,
1243
- verticalScrollbarOptions: overlayScrollbar,
1244
- children: [
1245
- warning !== null && /* @__PURE__ */ jsx4("text", { wrapMode: "word", fg: theme.warn, marginTop: 1, children: warning }),
1246
- view.lists.map((list) => /* @__PURE__ */ jsxs3("box", { flexDirection: "column", marginTop: 1, children: [
1247
- /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.accent, children: list.title }),
1248
- list.rows.map((row) => /* @__PURE__ */ jsxs3("text", { wrapMode: "none", fg: theme.text, children: [
1249
- " ",
1250
- row.lead,
1251
- " ",
1252
- /* @__PURE__ */ jsx4("a", { href: row.url, fg: theme.accent, children: row.ref }),
1253
- " ",
1254
- row.title
1255
- ] }, row.url))
1256
- ] }, list.title)),
1257
- view.distribution !== null && /* @__PURE__ */ jsxs3("box", { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [
1258
- /* @__PURE__ */ jsxs3("box", { flexDirection: "row", justifyContent: "space-between", children: [
1259
- /* @__PURE__ */ jsx4("text", { wrapMode: "none", children: /* @__PURE__ */ jsx4("b", { fg: theme.text, children: view.distributionTitle }) }),
1260
- /* @__PURE__ */ jsx4(ChartLine, { line: view.distribution.stats })
1261
- ] }),
1262
- /* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(view.distribution.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
1263
- ] }),
1264
- view.cards.length === 0 ? /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.muted, marginTop: 1, children: view.noCharts }) : cards
1265
- ]
1266
- }
1267
- )
1268
- ] });
1449
+ const column = Math.max(...labels.map((label) => label.length)) + 4;
1450
+ const lines = [`${source_default.bold("Usage:")} ${source_default.bold("pr-stats")} ${dim("[options]")}`, "", source_default.bold("Options:")];
1451
+ for (const [index, option] of OPTIONS.entries()) {
1452
+ const short = option.short ? `${flag(`-${option.short}`)}, ` : "";
1453
+ const long = option.placeholder ? `${flag(`--${option.name}`)} ${dim(option.placeholder)}` : flag(`--${option.name}`);
1454
+ const body = wrapMarkup(option.help, HELP_WIDTH - column).map((line) => colorizeHelp(line));
1455
+ lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
1456
+ for (const overflow of body.slice(1)) {
1457
+ lines.push(" ".repeat(column) + overflow);
1458
+ }
1459
+ }
1460
+ return lines.join("\n");
1269
1461
  }
1270
- function keyed(items, contentOf) {
1271
- const seen = /* @__PURE__ */ new Map();
1272
- return items.map((item) => {
1273
- const content = contentOf(item);
1274
- const count2 = seen.get(content) ?? 0;
1275
- seen.set(content, count2 + 1);
1276
- return { key: `${count2}:${content}`, item };
1277
- });
1462
+ var HELP = renderHelp();
1463
+ function parseCliArgs(args) {
1464
+ const options = {};
1465
+ for (const option of OPTIONS) {
1466
+ options[option.name] = { type: option.type };
1467
+ if (option.short) {
1468
+ options[option.name].short = option.short;
1469
+ }
1470
+ if (option.multiple) {
1471
+ options[option.name].multiple = true;
1472
+ }
1473
+ }
1474
+ const { values } = parseArgs({ options, args });
1475
+ const explicit = new Set(Object.keys(values));
1476
+ for (const option of OPTIONS) {
1477
+ if (option.default !== void 0 && values[option.name] === void 0) {
1478
+ values[option.name] = option.default;
1479
+ }
1480
+ }
1481
+ return { values, explicit };
1278
1482
  }
1279
- function spanKey(span) {
1280
- return `${span.fg ?? ""}|${span.bg ?? ""}|${span.bold === true ? "b" : ""}|${span.text}`;
1483
+ function parseSince(input) {
1484
+ const relative = /^(\d+)([dwmy])$/.exec(input);
1485
+ if (relative) {
1486
+ const amount = Number(relative[1]);
1487
+ const date2 = /* @__PURE__ */ new Date();
1488
+ if (relative[2] === "d") {
1489
+ date2.setDate(date2.getDate() - amount);
1490
+ }
1491
+ if (relative[2] === "w") {
1492
+ date2.setDate(date2.getDate() - amount * 7);
1493
+ }
1494
+ if (relative[2] === "m") {
1495
+ date2.setMonth(date2.getMonth() - amount);
1496
+ }
1497
+ if (relative[2] === "y") {
1498
+ date2.setFullYear(date2.getFullYear() - amount);
1499
+ }
1500
+ return date2;
1501
+ }
1502
+ const date = new Date(input);
1503
+ if (Number.isNaN(date.getTime())) {
1504
+ throw new CliError(`invalid --since value "${input}", use an ISO date or 30d/8w/6m/1y`);
1505
+ }
1506
+ return date;
1281
1507
  }
1282
- function lineKey(line) {
1283
- return line.map((span) => spanKey(span)).join("&");
1508
+ function parseTarget(input) {
1509
+ const match = /^(\d+(?:\.\d+)?)([hdm]?)$/.exec(input);
1510
+ if (!match) {
1511
+ throw new CliError(`invalid --target value "${input}", use 24h, 2d, or 90m`);
1512
+ }
1513
+ const amount = Number(match[1]);
1514
+ if (match[2] === "d") {
1515
+ return amount * timeMode.dayHours;
1516
+ }
1517
+ if (match[2] === "m") {
1518
+ return amount / 60;
1519
+ }
1520
+ return amount;
1284
1521
  }
1285
- function ChartLine({ line }) {
1286
- return /* @__PURE__ */ jsx4("text", { wrapMode: "none", children: keyed(line, spanKey).map(
1287
- ({ key, item }) => item.bold === true ? /* @__PURE__ */ jsx4("b", { fg: item.fg ?? theme.text, bg: item.bg, children: item.text }, key) : /* @__PURE__ */ jsx4("span", { fg: item.fg ?? theme.text, bg: item.bg, children: item.text }, key)
1288
- ) });
1522
+ var DEFAULT_TARGET_PERCENTILE = 90;
1523
+ function parseTargetPercentile(input) {
1524
+ const match = /^p?(\d{1,3})$/i.exec(input);
1525
+ const value2 = match === null ? Number.NaN : Number(match[1]);
1526
+ if (!Number.isInteger(value2) || value2 < 1 || value2 > 100) {
1527
+ throw new CliError(`invalid --target-percentile value "${input}", use a percentile from 1 to 100 like 90 or p90`);
1528
+ }
1529
+ return value2;
1289
1530
  }
1290
- function ChartCard({ card }) {
1291
- const subtitle = typeof card.subtitle === "string" ? [{ text: card.subtitle, fg: theme.muted }] : card.subtitle;
1292
- return /* @__PURE__ */ jsxs3("box", { flexDirection: "column", children: [
1293
- /* @__PURE__ */ jsxs3("text", { wrapMode: "none", children: [
1294
- /* @__PURE__ */ jsx4("b", { fg: theme.text, children: card.title }),
1295
- /* @__PURE__ */ jsx4("span", { fg: theme.muted, children: " " }),
1296
- keyed(subtitle, spanKey).map(({ key, item }) => /* @__PURE__ */ jsx4("span", { fg: item.fg ?? theme.muted, children: item.text }, key))
1297
- ] }),
1298
- /* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(card.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
1299
- ] });
1531
+ function parseSizeTarget(input) {
1532
+ const target = {};
1533
+ for (const part of input.split(",")) {
1534
+ const match = /^(\d+)([lf]?)$/.exec(part.trim());
1535
+ if (!match) {
1536
+ throw new CliError(`invalid --size-target value "${input}", use 400, 400l, 20f, or 400l,20f`);
1537
+ }
1538
+ const key = match[2] === "f" ? "files" : "lines";
1539
+ if (target[key] !== void 0) {
1540
+ throw new CliError(`--size-target sets the ${key} budget twice`);
1541
+ }
1542
+ target[key] = Number(match[1]);
1543
+ }
1544
+ return target;
1300
1545
  }
1301
-
1302
- // src/tui/components/Placeholder.tsx
1303
- import { jsx as jsx5, jsxs as jsxs4 } from "@opentui/react/jsx-runtime";
1304
- var BAR_WIDTH = 40;
1305
- var PARTIAL_BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
1306
- function Placeholder({
1307
- error,
1308
- loading,
1309
- load
1310
- }) {
1311
- return /* @__PURE__ */ jsx5("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", flexDirection: "column", children: error !== null ? /* @__PURE__ */ jsx5("text", { fg: theme.error, children: error }) : loading && load !== null ? /* @__PURE__ */ jsx5(LoadProgress, { load }) : /* @__PURE__ */ jsx5("text", { fg: theme.muted, children: "no data yet, press r to load" }) });
1546
+ var REVIEW_TYPES = /* @__PURE__ */ new Map([
1547
+ ["approve", "APPROVED"],
1548
+ ["comment", "COMMENTED"],
1549
+ ["request-changes", "CHANGES_REQUESTED"]
1550
+ ]);
1551
+ function parseReviewTypes(input) {
1552
+ const states = /* @__PURE__ */ new Set();
1553
+ for (const part of input.split(",")) {
1554
+ const state = REVIEW_TYPES.get(part.trim().toLowerCase());
1555
+ if (state === void 0) {
1556
+ throw new CliError(`invalid --review-types value "${part.trim()}", use approve, comment, or request-changes`);
1557
+ }
1558
+ states.add(state);
1559
+ }
1560
+ return states;
1312
1561
  }
1313
- function LoadProgress({ load }) {
1314
- if (!load.total) {
1315
- return /* @__PURE__ */ jsxs4("box", { flexDirection: "row", columnGap: 1, children: [
1316
- /* @__PURE__ */ jsx5("text", { fg: theme.muted, children: loadLabel(load) }),
1317
- /* @__PURE__ */ jsx5(Spinner, {})
1318
- ] });
1562
+ function toMinutesOfDay(hourText, minuteText, meridiem) {
1563
+ const minute = Number(minuteText ?? 0);
1564
+ if (minute > 59) {
1565
+ return null;
1319
1566
  }
1320
- const done = load.done ?? 0;
1321
- const cells = Math.min(done / load.total, 1) * BAR_WIDTH;
1322
- const whole = Math.floor(cells);
1323
- const partial = whole < BAR_WIDTH ? PARTIAL_BLOCKS[Math.floor((cells - whole) * 8)] : "";
1324
- const counter = `${String(done).padStart(String(load.total).length)}/${load.total}`;
1325
- return /* @__PURE__ */ jsxs4("box", { flexDirection: "column", alignItems: "center", rowGap: 1, children: [
1326
- /* @__PURE__ */ jsx5("text", { fg: theme.muted, children: loadLabel(load) }),
1327
- /* @__PURE__ */ jsxs4("text", { wrapMode: "none", children: [
1328
- /* @__PURE__ */ jsx5("span", { bg: theme.accent, children: " ".repeat(whole) }),
1329
- /* @__PURE__ */ jsx5("span", { fg: theme.accent, bg: theme.selectedBg, children: partial }),
1330
- /* @__PURE__ */ jsx5("span", { bg: theme.selectedBg, children: " ".repeat(BAR_WIDTH - whole - partial.length) }),
1331
- /* @__PURE__ */ jsx5("span", { fg: theme.muted, children: ` ${counter}` })
1332
- ] })
1333
- ] });
1567
+ let hour = Number(hourText);
1568
+ if (meridiem !== void 0) {
1569
+ if (hour < 1 || hour > 12) {
1570
+ return null;
1571
+ }
1572
+ hour %= 12;
1573
+ if (meridiem.toLowerCase() === "pm") {
1574
+ hour += 12;
1575
+ }
1576
+ }
1577
+ return hour * 60 + minute;
1334
1578
  }
1335
- function loadLabel(load) {
1336
- return load.phase === "search" ? "searching PRs" : "fetching PR details";
1579
+ var WEEKDAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
1580
+ var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1581
+ function parseWorkDays(input) {
1582
+ const days = /* @__PURE__ */ new Set();
1583
+ for (const part of input.split(",")) {
1584
+ const match = /^([a-z]{3})(?:-([a-z]{3}))?$/i.exec(part.trim());
1585
+ const endName = match?.[2];
1586
+ const start = match === null ? -1 : WEEKDAY_NAMES.indexOf(match[1].toLowerCase());
1587
+ const end = match === null ? -1 : endName === void 0 ? start : WEEKDAY_NAMES.indexOf(endName.toLowerCase());
1588
+ if (start === -1 || end === -1) {
1589
+ throw new CliError(
1590
+ `invalid --work-days value "${part.trim()}", use weekday names and ranges like mon-fri, sun-thu, or mon,wed,fri`
1591
+ );
1592
+ }
1593
+ for (let day = start; ; day = (day + 1) % 7) {
1594
+ days.add(day);
1595
+ if (day === end) {
1596
+ break;
1597
+ }
1598
+ }
1599
+ }
1600
+ return days;
1337
1601
  }
1338
-
1339
- // src/tui/components/QueuePanel.tsx
1340
- import { useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
1341
- import { useEffect as useEffect2, useRef } from "react";
1342
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "@opentui/react/jsx-runtime";
1343
- function QueuePanel({
1344
- heading,
1345
- warning,
1346
- empty,
1347
- sections,
1348
- cursor,
1349
- onRefClick
1350
- }) {
1351
- const scrollRef = useRef(null);
1352
- const { width } = useTerminalDimensions2();
1353
- const lastDown = useRef(null);
1354
- useScrollbarSettle(scrollRef, empty === null);
1355
- useEffect2(() => {
1356
- scrollRef.current?.scrollChildIntoView(`queue-row-${cursor}`);
1357
- }, [cursor]);
1358
- const header = heading === null ? null : /* @__PURE__ */ jsxs5(Fragment3, { children: [
1359
- /* @__PURE__ */ jsx6("box", { height: 1, children: /* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
1360
- /* @__PURE__ */ jsx6("box", { height: 1, paddingLeft: 1, paddingRight: 2, children: /* @__PURE__ */ jsxs5("text", { wrapMode: "none", children: [
1361
- /* @__PURE__ */ jsx6("span", { fg: theme.accent, children: "\u25B8 " }),
1362
- /* @__PURE__ */ jsx6("b", { fg: theme.text, children: heading })
1363
- ] }) }),
1364
- /* @__PURE__ */ jsx6("box", { height: 1, children: /* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) })
1365
- ] });
1366
- if (empty !== null) {
1367
- return /* @__PURE__ */ jsxs5("box", { flexGrow: 1, flexDirection: "column", children: [
1368
- header,
1369
- /* @__PURE__ */ jsx6("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx6("text", { fg: theme.muted, children: empty }) })
1370
- ] });
1602
+ function workDayRunLabel(start, end) {
1603
+ return start === end ? WEEKDAY_LABELS[start] : `${WEEKDAY_LABELS[start]}-${WEEKDAY_LABELS[end]}`;
1604
+ }
1605
+ function formatWorkDays(days) {
1606
+ if (days.size === 7) {
1607
+ return "Mon-Sun";
1371
1608
  }
1372
- const offsets = [];
1373
- let total = 0;
1374
- for (const section of sections) {
1375
- const entry = { rows: total, lists: [] };
1376
- total += section.rows.length;
1377
- for (const list of section.lists) {
1378
- entry.lists.push(total);
1379
- total += list.rows.length;
1609
+ const monFirst = [1, 2, 3, 4, 5, 6, 0];
1610
+ const anchor = monFirst.find((day) => days.has(day) && !days.has((day + 6) % 7)) ?? 1;
1611
+ const runs = [];
1612
+ let runStart = -1;
1613
+ let runEnd = -1;
1614
+ for (let step = 0; step < 7; step++) {
1615
+ const day = (anchor + step) % 7;
1616
+ if (days.has(day)) {
1617
+ runStart = runStart === -1 ? day : runStart;
1618
+ runEnd = day;
1619
+ } else if (runStart !== -1) {
1620
+ runs.push(workDayRunLabel(runStart, runEnd));
1621
+ runStart = -1;
1380
1622
  }
1381
- offsets.push(entry);
1382
1623
  }
1383
- const renderRow = (row, index, indent) => {
1384
- const isSelected = index === cursor;
1385
- const bg = isSelected ? theme.selectedBg : void 0;
1386
- const refStart = indent.length + 2 + row.lead.length + 2;
1387
- return /* @__PURE__ */ jsxs5(
1388
- "text",
1389
- {
1390
- id: `queue-row-${index}`,
1391
- wrapMode: "none",
1392
- onMouseDown: onRefClick === null ? void 0 : (event) => {
1393
- lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
1394
- },
1395
- onMouseUp: onRefClick === null ? void 0 : (event) => {
1396
- const down = lastDown.current;
1397
- lastDown.current = null;
1398
- if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
1399
- return;
1400
- }
1401
- const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
1402
- if (local >= refStart && local < refStart + row.ref.length) {
1403
- onRefClick(row);
1404
- }
1405
- },
1406
- children: [
1407
- /* @__PURE__ */ jsxs5("span", { fg: theme.accent, children: [
1408
- indent,
1409
- isSelected ? "\u25B8 " : " "
1410
- ] }),
1411
- /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
1412
- row.lead,
1413
- " "
1414
- ] }),
1415
- onRefClick === null ? /* @__PURE__ */ jsx6("a", { href: row.url, fg: theme.accent, bg, children: row.ref }) : /* @__PURE__ */ jsx6("span", { fg: theme.accent, bg, children: row.ref }),
1416
- /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
1417
- " ",
1418
- row.title
1419
- ] })
1420
- ]
1421
- },
1422
- row.url
1423
- );
1424
- };
1425
- return /* @__PURE__ */ jsxs5("box", { flexGrow: 1, flexDirection: "column", children: [
1426
- header,
1427
- /* @__PURE__ */ jsxs5(
1428
- "scrollbox",
1429
- {
1430
- ref: scrollRef,
1431
- flexGrow: 1,
1432
- paddingLeft: 1,
1433
- paddingRight: 2,
1434
- verticalScrollbarOptions: overlayScrollbar,
1435
- children: [
1436
- warning !== null && /* @__PURE__ */ jsx6("text", { wrapMode: "word", fg: theme.warn, marginTop: 1, children: warning }),
1437
- sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: 1, children: [
1438
- /* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.accent, children: section.title }),
1439
- section.rows.map((row, rowIndex) => renderRow(row, offsets[sectionIndex].rows + rowIndex, "")),
1440
- section.lists.map((list, listIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: listIndex > 0 ? 1 : 0, children: [
1441
- /* @__PURE__ */ jsxs5("text", { wrapMode: "none", fg: theme.accent, children: [
1442
- " ",
1443
- list.title
1444
- ] }),
1445
- list.rows.map(
1446
- (row, rowIndex) => renderRow(row, offsets[sectionIndex].lists[listIndex] + rowIndex, " ")
1447
- )
1448
- ] }, list.title))
1449
- ] }, section.title))
1450
- ]
1451
- }
1452
- )
1453
- ] });
1624
+ return runs.join(",");
1625
+ }
1626
+ function canonicalWorkDays(input) {
1627
+ return formatWorkDays(parseWorkDays(input));
1628
+ }
1629
+ function parseWorkHours(input) {
1630
+ const windows = input.split(",").map((range) => {
1631
+ const match = /^(\d{1,2})(?::(\d{2}))?(am|pm)?-(\d{1,2})(?::(\d{2}))?(am|pm)?$/i.exec(range.trim());
1632
+ if (!match) {
1633
+ throw new CliError(
1634
+ `invalid --work-hours range "${range}", use ranges like 9-17, 9am-6pm, 8:30-16:30, or 9-18,19:30-20:30`
1635
+ );
1636
+ }
1637
+ const startMin = toMinutesOfDay(match[1], match[2], match[3]);
1638
+ const end = toMinutesOfDay(match[4], match[5], match[6]);
1639
+ const endMin = end === 0 ? 24 * 60 : end;
1640
+ if (startMin === null || endMin === null || endMin <= startMin || endMin > 24 * 60) {
1641
+ throw new CliError(`invalid --work-hours range "${range}"`);
1642
+ }
1643
+ return { startMin, endMin };
1644
+ });
1645
+ windows.sort((a, b) => a.startMin - b.startMin);
1646
+ for (let i = 1; i < windows.length; i++) {
1647
+ if (windows[i].startMin < windows[i - 1].endMin) {
1648
+ throw new CliError(`--work-hours ranges overlap around ${formatMinutesOfDay(windows[i].startMin)}`);
1649
+ }
1650
+ }
1651
+ return windows;
1652
+ }
1653
+ function resolveTimezone(input) {
1654
+ const tz = input ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
1655
+ try {
1656
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
1657
+ } catch {
1658
+ throw new CliError(`invalid --tz value "${tz}", use an IANA zone like Europe/Berlin`);
1659
+ }
1660
+ return tz;
1454
1661
  }
1455
1662
 
1456
- // src/tui/components/RepoList.tsx
1457
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
1458
- import { jsx as jsx7, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
1459
- function RepoList({
1663
+ // src/tui/components/Spinner.tsx
1664
+ import { useEffect, useState } from "react";
1665
+ import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
1666
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1667
+ var SPINNER_INTERVAL_MS = 80;
1668
+ function Spinner() {
1669
+ const [frame, setFrame] = useState(0);
1670
+ useEffect(() => {
1671
+ const timer = setInterval(() => {
1672
+ setFrame((previous) => (previous + 1) % SPINNER_FRAMES.length);
1673
+ }, SPINNER_INTERVAL_MS);
1674
+ return () => {
1675
+ clearInterval(timer);
1676
+ };
1677
+ }, []);
1678
+ return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
1679
+ }
1680
+
1681
+ // src/tui/components/Header.tsx
1682
+ import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
1683
+ function Header({
1460
1684
  options,
1461
- cursor,
1462
- prompt = "Select a repository and press enter to open its charts."
1685
+ raw,
1686
+ error,
1687
+ spinning
1463
1688
  }) {
1464
- const scrollRef = useRef2(null);
1465
- useScrollbarSettle(scrollRef);
1466
- useEffect3(() => {
1467
- scrollRef.current?.scrollChildIntoView(`repo-row-${cursor}`);
1468
- }, [cursor]);
1469
- const labelWidth = Math.max(...options.map((option) => option.label.length));
1470
- return /* @__PURE__ */ jsxs6(
1471
- "scrollbox",
1472
- {
1473
- ref: scrollRef,
1474
- flexGrow: 1,
1475
- paddingLeft: 1,
1476
- paddingRight: 2,
1477
- verticalScrollbarOptions: overlayScrollbar,
1478
- children: [
1479
- /* @__PURE__ */ jsx7("text", { wrapMode: "none", fg: theme.muted, children: prompt }),
1480
- /* @__PURE__ */ jsx7("box", { flexDirection: "column", marginTop: 1, children: options.map((option, i) => {
1481
- const isSelected = i === cursor;
1482
- return /* @__PURE__ */ jsxs6("text", { id: `repo-row-${i}`, wrapMode: "none", children: [
1483
- /* @__PURE__ */ jsx7("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
1484
- /* @__PURE__ */ jsx7("span", { fg: isSelected ? theme.text : theme.muted, bg: isSelected ? theme.selectedBg : void 0, children: ` ${option.label.padEnd(labelWidth)} ` }),
1485
- /* @__PURE__ */ jsxs6("span", { fg: theme.muted, children: [
1486
- " ",
1487
- option.detail
1488
- ] })
1489
- ] }, option.label);
1490
- }) })
1491
- ]
1492
- }
1493
- );
1689
+ const context = [
1690
+ raw ? `@${raw.user}` : options.user !== "" ? `@${options.user}` : "@...",
1691
+ `since ${options.since}`,
1692
+ raw && raw.repos.length > 0 ? raw.repos.join(", ") : options.repos !== "" ? options.repos : "all repos",
1693
+ timeModeLabel(options)
1694
+ ].join(" \xB7 ");
1695
+ const rightStatus = raw ? error !== null ? "reload failed \xB7 press r to retry" : `refreshed ${raw.fetchedAt.toLocaleTimeString()}` : "";
1696
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [
1697
+ /* @__PURE__ */ jsxs2("text", { wrapMode: "none", children: [
1698
+ /* @__PURE__ */ jsx3("b", { fg: theme.accent, children: "pr-stats" }),
1699
+ /* @__PURE__ */ jsxs2("span", { fg: theme.muted, children: [
1700
+ " \xB7 ",
1701
+ context
1702
+ ] })
1703
+ ] }),
1704
+ spinning ? /* @__PURE__ */ jsx3(Spinner, {}) : /* @__PURE__ */ jsx3("text", { wrapMode: "none", fg: raw !== null && error !== null ? theme.error : theme.muted, children: rightStatus })
1705
+ ] });
1494
1706
  }
1495
-
1496
- // src/tui/state/browse.ts
1497
- var TABS = ["1 Awaiting you", "2 Your PRs", "3 Review time", "4 PR size", "5 Comments"];
1498
- var initialBrowseState = {
1499
- tab: 0,
1500
- authoredTab: "open",
1501
- scopes: {
1502
- pending: { view: "list" },
1503
- open: { view: "list" },
1504
- review: { view: "list" },
1505
- size: { view: "list" },
1506
- comment: { view: "list" },
1507
- merged: { view: "list" }
1508
- },
1509
- repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0, merged: 0 },
1510
- rowCursors: { pending: 0, open: 0 },
1511
- grouped: { pending: false, open: false },
1512
- expanded: { review: false, size: false, comment: false, merged: false }
1513
- };
1514
- function dropVanishedRepo(scope, repos) {
1515
- if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
1516
- return { view: "list" };
1707
+ function timeModeLabel(options) {
1708
+ if (options.wallClock) {
1709
+ return "wall-clock time";
1517
1710
  }
1518
- return scope;
1711
+ const tz = options.tz === "" ? Intl.DateTimeFormat().resolvedOptions().timeZone : options.tz;
1712
+ if (options.workHours === "0-24") {
1713
+ return `${options.workDays} all hours ${tz}`;
1714
+ }
1715
+ return `${options.workDays} ${options.workHours} (${dailyHoursLabel(options.workHours)}) ${tz}`;
1519
1716
  }
1520
- function cycled(previous, delta, count2) {
1521
- return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
1717
+ function dailyHoursLabel(workHours) {
1718
+ const minutes = parseWorkHours(workHours).reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
1719
+ const hours = Math.round(minutes / 60 * 100) / 100;
1720
+ return `${hours} ${hours === 1 ? "hour" : "hours"}`;
1522
1721
  }
1523
- function browseReducer(state, action) {
1524
- switch (action.type) {
1525
- case "tabSelected": {
1526
- return { ...state, tab: action.tab };
1722
+
1723
+ // src/compute.ts
1724
+ function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
1725
+ const reviewed = [];
1726
+ const pending = [];
1727
+ for (const result of results) {
1728
+ if (result.kind === "reviewed") {
1729
+ reviewed.push({
1730
+ pr: result.pr,
1731
+ requestedAt: result.requestedAt,
1732
+ reviewedAt: result.reviewedAt,
1733
+ hours: durationHours(result.requestedAt, result.reviewedAt),
1734
+ verdict: result.verdict,
1735
+ lines: result.lines
1736
+ });
1737
+ } else if (result.kind === "pending" && result.pr.state === "open") {
1738
+ pending.push({ pr: result.pr, requestedAt: result.requestedAt, hours: durationHours(result.requestedAt, now) });
1527
1739
  }
1528
- case "tabCycled": {
1529
- return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
1740
+ }
1741
+ pending.sort((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
1742
+ const pendingKeys = new Set(pending.map((entry) => `${entry.pr.repo}#${entry.pr.number}`));
1743
+ const latestReviews = /* @__PURE__ */ new Map();
1744
+ for (const result of results) {
1745
+ if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
1746
+ continue;
1530
1747
  }
1531
- case "subTabToggled": {
1532
- return { ...state, authoredTab: state.authoredTab === "open" ? "merged" : "open" };
1748
+ const key = `${result.pr.repo}#${result.pr.number}`;
1749
+ if (pendingKeys.has(key)) {
1750
+ continue;
1533
1751
  }
1534
- case "repoCursorMoved": {
1535
- return {
1536
- ...state,
1537
- repoCursors: {
1538
- ...state.repoCursors,
1539
- [action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
1540
- }
1541
- };
1752
+ const latest = latestReviews.get(key);
1753
+ if (latest === void 0 || result.reviewedAt > latest.reviewedAt) {
1754
+ latestReviews.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
1542
1755
  }
1543
- case "rowCursorMoved": {
1544
- return {
1545
- ...state,
1546
- rowCursors: {
1547
- ...state.rowCursors,
1548
- [action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
1549
- }
1550
- };
1756
+ }
1757
+ const reviewing = [...latestReviews.values()].map(({ pr, reviewedAt }) => {
1758
+ return { pr, reviewedAt, hours: durationHours(reviewedAt, now) };
1759
+ }).toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
1760
+ const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
1761
+ const unrequested = results.filter((result) => result.kind === "unrequested");
1762
+ const allHours = reviewed.map((result) => result.hours);
1763
+ const hoursByRepo = /* @__PURE__ */ new Map();
1764
+ for (const result of reviewed) {
1765
+ const hours = hoursByRepo.get(result.pr.repo) ?? [];
1766
+ hours.push(result.hours);
1767
+ hoursByRepo.set(result.pr.repo, hours);
1768
+ }
1769
+ const byRepo = [...hoursByRepo.entries()].toSorted((a, b) => b[1].length - a[1].length);
1770
+ const misses = targetHours === void 0 ? [] : reviewed.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
1771
+ const cyclesByPr = /* @__PURE__ */ new Map();
1772
+ for (const result of reviewed) {
1773
+ const key = `${result.pr.repo}#${result.pr.number}`;
1774
+ cyclesByPr.set(key, (cyclesByPr.get(key) ?? 0) + 1);
1775
+ }
1776
+ return {
1777
+ reviewed,
1778
+ pending,
1779
+ reviewing,
1780
+ expired,
1781
+ unrequested,
1782
+ allHours,
1783
+ byRepo,
1784
+ misses,
1785
+ cycles: [...cyclesByPr.values()]
1786
+ };
1787
+ }
1788
+ function computeSizeStats(sizes, { sizeTarget } = {}) {
1789
+ const metrics = [
1790
+ { label: "files changed", values: sizes.map((size) => size.files) },
1791
+ { label: "lines added", values: sizes.map((size) => size.additions) },
1792
+ { label: "lines removed", values: sizes.map((size) => size.deletions) },
1793
+ { label: "lines total", values: sizes.map((size) => size.total) }
1794
+ ];
1795
+ const timelineTotals = [...sizes].toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime()).map((size) => size.total);
1796
+ if (sizeTarget === void 0) {
1797
+ return { metrics, timelineTotals, met: void 0, misses: [], targetLabel: void 0 };
1798
+ }
1799
+ const meetsTarget = (size) => (sizeTarget.lines === void 0 || size.total <= sizeTarget.lines) && (sizeTarget.files === void 0 || size.files <= sizeTarget.files);
1800
+ const targetLabel = [
1801
+ ...sizeTarget.lines === void 0 ? [] : [`<= ${sizeTarget.lines} lines`],
1802
+ ...sizeTarget.files === void 0 ? [] : [`<= ${sizeTarget.files} files`]
1803
+ ].join(", ");
1804
+ const met = sizes.filter((size) => meetsTarget(size)).length;
1805
+ const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
1806
+ return { metrics, timelineTotals, met, misses, targetLabel };
1807
+ }
1808
+ function computeMergeStats(sizes) {
1809
+ const merged = [];
1810
+ const closed = [];
1811
+ const open = [];
1812
+ for (const entry of sizes) {
1813
+ if (entry.mergedAt !== null) {
1814
+ merged.push({ entry, mergedAt: entry.mergedAt, hours: durationHours(entry.pr.createdAt, entry.mergedAt) });
1815
+ } else if (entry.pr.state === "open") {
1816
+ open.push(entry);
1817
+ } else if (entry.closedAt !== null) {
1818
+ closed.push({ entry, closedAt: entry.closedAt, hours: durationHours(entry.pr.createdAt, entry.closedAt) });
1551
1819
  }
1552
- case "repoOpened": {
1553
- return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
1820
+ }
1821
+ merged.sort((a, b) => b.mergedAt.getTime() - a.mergedAt.getTime());
1822
+ closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
1823
+ return { merged, closed, open, allHours: merged.map((result) => result.hours) };
1824
+ }
1825
+ function computeReviewerStats(sizes, author) {
1826
+ const byLogin = /* @__PURE__ */ new Map();
1827
+ let mergedReviewed = 0;
1828
+ let mergedUnreviewed = 0;
1829
+ for (const entry of sizes) {
1830
+ const others = entry.reviews.flatMap(
1831
+ (review) => review.login === null || review.login === author ? [] : [review.login]
1832
+ );
1833
+ for (const login of others) {
1834
+ const counts = byLogin.get(login) ?? { prs: 0, reviews: 0 };
1835
+ counts.reviews += 1;
1836
+ byLogin.set(login, counts);
1554
1837
  }
1555
- case "pickerReturned": {
1556
- return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
1838
+ const distinct = new Set(others);
1839
+ for (const login of distinct) {
1840
+ const counts = byLogin.get(login);
1841
+ if (counts !== void 0) {
1842
+ counts.prs += 1;
1843
+ }
1557
1844
  }
1558
- case "groupingToggled": {
1559
- return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
1845
+ if (entry.mergedAt !== null) {
1846
+ if (others.length > 0) {
1847
+ mergedReviewed += 1;
1848
+ } else {
1849
+ mergedUnreviewed += 1;
1850
+ }
1560
1851
  }
1561
- case "expandToggled": {
1562
- return { ...state, expanded: { ...state.expanded, [action.tab]: !state.expanded[action.tab] } };
1852
+ }
1853
+ const leaderboard = [...byLogin.entries()].map(([login, counts]) => {
1854
+ return { login, ...counts };
1855
+ }).toSorted((a, b) => b.prs - a.prs || b.reviews - a.reviews || a.login.localeCompare(b.login));
1856
+ return { leaderboard, mergedReviewed, mergedUnreviewed };
1857
+ }
1858
+ function firstReviewOf(entry, author) {
1859
+ let earliest = null;
1860
+ for (const review of entry.reviews) {
1861
+ if (review.login === null || review.login === author || review.submittedAt === null) {
1862
+ continue;
1563
1863
  }
1564
- case "dataLoaded": {
1565
- return {
1566
- ...state,
1567
- scopes: {
1568
- pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
1569
- open: dropVanishedRepo(state.scopes.open, action.repos.open),
1570
- review: dropVanishedRepo(state.scopes.review, action.repos.review),
1571
- size: dropVanishedRepo(state.scopes.size, action.repos.size),
1572
- comment: dropVanishedRepo(state.scopes.comment, action.repos.comment),
1573
- merged: dropVanishedRepo(state.scopes.merged, action.repos.merged)
1574
- }
1575
- };
1864
+ if (earliest === null || review.submittedAt < earliest) {
1865
+ earliest = review.submittedAt;
1576
1866
  }
1577
1867
  }
1578
- return state;
1868
+ if (earliest === null) {
1869
+ return null;
1870
+ }
1871
+ return { reviewedAt: earliest, hours: durationHours(entry.pr.createdAt, earliest) };
1579
1872
  }
1580
-
1581
- // src/tui/components/TabBar.tsx
1582
- import { jsx as jsx8, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
1583
- function TabBar({ tab }) {
1584
- return /* @__PURE__ */ jsx8("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginTop: 1, columnGap: 1, children: TABS.map((label, i) => /* @__PURE__ */ jsx8(
1585
- "text",
1586
- {
1587
- wrapMode: "none",
1588
- fg: i === tab ? theme.accent : theme.muted,
1589
- bg: i === tab ? theme.selectedBg : void 0,
1590
- children: ` ${label} `
1591
- },
1592
- label
1593
- )) });
1873
+ function computeFirstReviewStats(sizes, author, { now = /* @__PURE__ */ new Date() } = {}) {
1874
+ const received = [];
1875
+ const awaiting = [];
1876
+ for (const entry of sizes) {
1877
+ const first = firstReviewOf(entry, author);
1878
+ if (first !== null) {
1879
+ received.push({ entry, ...first });
1880
+ } else if (entry.pr.state === "open") {
1881
+ awaiting.push({ entry, hours: durationHours(entry.pr.createdAt, now) });
1882
+ }
1883
+ }
1884
+ awaiting.sort((a, b) => a.entry.pr.createdAt.getTime() - b.entry.pr.createdAt.getTime());
1885
+ return { received, awaiting, allHours: received.map((result) => result.hours) };
1594
1886
  }
1595
- var SUB_TABS = [
1596
- { key: "open", label: "Open" },
1597
- { key: "merged", label: "Merged & closed" }
1598
- ];
1599
- function SubTabBar({ active }) {
1600
- return /* @__PURE__ */ jsxs7("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginBottom: 1, columnGap: 1, children: [
1601
- SUB_TABS.map(({ key, label }) => /* @__PURE__ */ jsx8(
1602
- "text",
1603
- {
1604
- wrapMode: "none",
1605
- fg: key === active ? theme.accent : theme.muted,
1606
- bg: key === active ? theme.selectedBg : void 0,
1607
- children: ` ${label} `
1608
- },
1609
- key
1610
- )),
1611
- /* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t switches" })
1612
- ] });
1887
+ function computeCommentStats(sizes) {
1888
+ const metrics = [
1889
+ { label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
1890
+ { label: "review comments", values: sizes.map((size) => size.comments.review) },
1891
+ { label: "all comments", values: sizes.map((size) => size.comments.total) }
1892
+ ];
1893
+ const totals = sizes.map((size) => size.comments.total);
1894
+ const uncommented = totals.filter((total) => total === 0).length;
1895
+ const top = sizes.filter((size) => size.comments.total > 0).toSorted((a, b) => b.comments.total - a.comments.total);
1896
+ return { metrics, totals, uncommented, top };
1613
1897
  }
1614
1898
 
1615
- // src/tui/components/MainPanel.tsx
1616
- import { jsx as jsx9, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
1617
- function QueueTab({
1618
- prompt,
1619
- repos,
1620
- scope,
1621
- view,
1622
- repoCursor,
1623
- rowCursor,
1624
- grouped,
1625
- warning,
1626
- onRefClick
1627
- }) {
1628
- if (scope.view === "list") {
1629
- return /* @__PURE__ */ jsx9(RepoList, { prompt, options: repos, cursor: Math.min(repoCursor, repos.length - 1) });
1630
- }
1631
- if (view === null) {
1632
- return null;
1633
- }
1634
- return /* @__PURE__ */ jsx9(
1635
- QueuePanel,
1636
- {
1637
- heading: repos.length > 0 ? scope.repo ?? (grouped ? "All repos \xB7 grouped by repo" : "All repos") : null,
1638
- warning,
1639
- empty: view.empty,
1640
- sections: view.sections,
1641
- cursor: Math.min(rowCursor, queueRows(view).length - 1),
1642
- onRefClick
1643
- }
1644
- );
1899
+ // src/report.ts
1900
+ var BUCKETS = [];
1901
+ function initBuckets() {
1902
+ BUCKETS = makeBuckets();
1645
1903
  }
1646
- function StatsTab({
1647
- repos,
1648
- scope,
1649
- view,
1650
- cursor,
1651
- scrollRef,
1652
- focused,
1653
- warning
1654
- }) {
1655
- if (scope.view === "list") {
1656
- return /* @__PURE__ */ jsx9(RepoList, { options: repos, cursor: Math.min(cursor, repos.length - 1) });
1904
+ function currentBuckets() {
1905
+ return BUCKETS;
1906
+ }
1907
+ function makeBuckets() {
1908
+ if (!timeMode.business || isFullDayMode()) {
1909
+ return [
1910
+ { label: "< 1h", max: 1 },
1911
+ { label: "1-4h", max: 4 },
1912
+ { label: "4-8h", max: 8 },
1913
+ { label: "8-24h", max: 24 },
1914
+ { label: "1-2d", max: 48 },
1915
+ { label: "2-4d", max: 96 },
1916
+ { label: "4-7d", max: 168 },
1917
+ { label: "> 7d", max: Infinity }
1918
+ ];
1657
1919
  }
1658
- if (view === null) {
1659
- return null;
1920
+ const wd = timeMode.dayHours;
1921
+ return [
1922
+ { label: "< 1h", max: 1 },
1923
+ { label: "1-4h", max: 4 },
1924
+ { label: "4h-1wd", max: wd },
1925
+ { label: "1-2wd", max: 2 * wd },
1926
+ { label: "2-3wd", max: 3 * wd },
1927
+ { label: "3-5wd", max: 5 * wd },
1928
+ { label: "5-10wd", max: 10 * wd },
1929
+ { label: "> 10wd", max: Infinity }
1930
+ ];
1931
+ }
1932
+ var LINE_BUCKETS = [
1933
+ { label: "< 50", max: 50 },
1934
+ { label: "50-100", max: 100 },
1935
+ { label: "100-250", max: 250 },
1936
+ { label: "250-500", max: 500 },
1937
+ { label: "500-1k", max: 1e3 },
1938
+ { label: "1k-2.5k", max: 2500 },
1939
+ { label: "2.5k-5k", max: 5e3 },
1940
+ { label: "> 5k", max: Infinity }
1941
+ ];
1942
+ var FILE_BUCKETS = [
1943
+ { label: "1-2", max: 3 },
1944
+ { label: "3-5", max: 6 },
1945
+ { label: "6-10", max: 11 },
1946
+ { label: "11-20", max: 21 },
1947
+ { label: "21-50", max: 51 },
1948
+ { label: "> 50", max: Infinity }
1949
+ ];
1950
+ var CYCLE_BUCKETS = [
1951
+ { label: "1", max: 2 },
1952
+ { label: "2", max: 3 },
1953
+ { label: "3", max: 4 },
1954
+ { label: "4-5", max: 6 },
1955
+ { label: "> 5", max: Infinity }
1956
+ ];
1957
+ var COMMENT_BUCKETS = [
1958
+ { label: "0", max: 1 },
1959
+ { label: "1-2", max: 3 },
1960
+ { label: "3-5", max: 6 },
1961
+ { label: "6-10", max: 11 },
1962
+ { label: "11-20", max: 21 },
1963
+ { label: "21-50", max: 51 },
1964
+ { label: "> 50", max: Infinity }
1965
+ ];
1966
+ function formatHoursOnly(hours) {
1967
+ if (hours < 1) {
1968
+ return `${Math.round(hours * 60)}m`;
1660
1969
  }
1661
- return /* @__PURE__ */ jsx9(
1662
- ChartsPanel,
1663
- {
1664
- scrollRef,
1665
- focused,
1666
- heading: repos.length > 0 ? scope.repo ?? "All repos" : null,
1667
- warning,
1668
- view
1669
- }
1670
- );
1970
+ return `${hours.toFixed(1)}h`;
1671
1971
  }
1672
- function MainPanel({
1673
- views,
1674
- browse,
1675
- warning,
1676
- focused,
1677
- scrollRefs,
1678
- error,
1679
- loading,
1680
- load,
1681
- onRefClick
1682
- }) {
1683
- return /* @__PURE__ */ jsx9("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx9(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */ jsx9(
1684
- QueueTab,
1685
- {
1686
- prompt: "Select a repository and press enter to open its review queue.",
1687
- repos: views.pendingRepos,
1688
- scope: views.pendingScope,
1689
- view: views.pending,
1690
- repoCursor: browse.repoCursors.pending,
1691
- rowCursor: browse.rowCursors.pending,
1692
- grouped: browse.grouped.pending,
1693
- warning,
1694
- onRefClick
1695
- }
1696
- ) : browse.tab === 1 ? /* @__PURE__ */ jsxs8("box", { flexGrow: 1, flexDirection: "column", children: [
1697
- /* @__PURE__ */ jsx9(SubTabBar, { active: browse.authoredTab }),
1698
- browse.authoredTab === "open" ? /* @__PURE__ */ jsx9(
1699
- QueueTab,
1700
- {
1701
- prompt: "Select a repository and press enter to list its open PRs.",
1702
- repos: views.openRepos,
1703
- scope: views.openScope,
1704
- view: views.open,
1705
- repoCursor: browse.repoCursors.open,
1706
- rowCursor: browse.rowCursors.open,
1707
- grouped: browse.grouped.open,
1708
- warning,
1709
- onRefClick
1710
- }
1711
- ) : /* @__PURE__ */ jsx9(
1712
- StatsTab,
1713
- {
1714
- repos: views.mergedRepos,
1715
- scope: views.mergedScope,
1716
- view: views.merged,
1717
- cursor: browse.repoCursors.merged,
1718
- scrollRef: scrollRefs.merged,
1719
- focused,
1720
- warning
1721
- }
1722
- )
1723
- ] }) : browse.tab === 2 ? /* @__PURE__ */ jsx9(
1724
- StatsTab,
1725
- {
1726
- repos: views.reviewRepos,
1727
- scope: views.reviewScope,
1728
- view: views.review,
1729
- cursor: browse.repoCursors.review,
1730
- scrollRef: scrollRefs.review,
1731
- focused,
1732
- warning
1733
- }
1734
- ) : browse.tab === 3 ? /* @__PURE__ */ jsx9(
1735
- StatsTab,
1736
- {
1737
- repos: views.sizeRepos,
1738
- scope: views.sizeScope,
1739
- view: views.size,
1740
- cursor: browse.repoCursors.size,
1741
- scrollRef: scrollRefs.size,
1742
- focused,
1743
- warning
1744
- }
1745
- ) : /* @__PURE__ */ jsx9(
1746
- StatsTab,
1747
- {
1748
- repos: views.commentRepos,
1749
- scope: views.commentScope,
1750
- view: views.comments,
1751
- cursor: browse.repoCursors.comment,
1752
- scrollRef: scrollRefs.comment,
1753
- focused,
1754
- warning
1755
- }
1756
- ) });
1972
+ function weeksSuffix(hours) {
1973
+ const weekHours = timeMode.business ? 5 * timeMode.dayHours : 7 * 24;
1974
+ if (hours < weekHours) {
1975
+ return "";
1976
+ }
1977
+ return ` (${(hours / weekHours).toFixed(1)} weeks)`;
1757
1978
  }
1758
-
1759
- // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
1760
- function stringReplaceAll(string, substring, postfix) {
1761
- let index = string.indexOf(substring);
1762
- if (index === -1) {
1763
- return string;
1979
+ function formatCount(value2) {
1980
+ if (value2 < 1e3) {
1981
+ return String(value2);
1764
1982
  }
1765
- const substringLength = substring.length;
1766
- let endIndex = 0;
1767
- let returnValue = "";
1768
- do {
1769
- returnValue += string.slice(endIndex, index) + substring + postfix;
1770
- endIndex = index + substringLength;
1771
- index = string.indexOf(substring, endIndex);
1772
- } while (index !== -1);
1773
- returnValue += string.slice(endIndex);
1774
- return returnValue;
1983
+ const scaled = value2 / 1e3;
1984
+ return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
1775
1985
  }
1776
- function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
1777
- let endIndex = 0;
1778
- let returnValue = "";
1779
- do {
1780
- const isGotCR = string[index - 1] === "\r";
1781
- returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? "\r\n" : "\n") + postfix;
1782
- endIndex = index + 1;
1783
- index = string.indexOf("\n", endIndex);
1784
- } while (index !== -1);
1785
- returnValue += string.slice(endIndex);
1786
- return returnValue;
1986
+
1987
+ // src/tui/views/rows.ts
1988
+ function durationLead(result) {
1989
+ return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
1990
+ }
1991
+ function toPrRows(entries, leads) {
1992
+ const width = Math.max(...leads.map((lead) => lead.length));
1993
+ return entries.map((entry, i) => {
1994
+ return {
1995
+ lead: leads[i].padEnd(width),
1996
+ ref: `${entry.pr.repo}#${entry.pr.number}`,
1997
+ url: entry.pr.url,
1998
+ title: entry.pr.title
1999
+ };
2000
+ });
1787
2001
  }
1788
2002
 
1789
- // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
1790
- var ANSI_BACKGROUND_OFFSET = 10;
1791
- var ANSI_UNDERLINE_OFFSET = 20;
1792
- var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
1793
- var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
1794
- var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
1795
- var wrapUnderlineAnsi = (code) => `\x1B[58;5;${code < 90 ? code - 30 : code - 90 + 8}m`;
1796
- var styles = {
1797
- modifier: {
1798
- reset: [0, 0],
1799
- // 21 isn't widely supported and 22 does the same thing
1800
- bold: [1, 22],
1801
- dim: [2, 22],
1802
- italic: [3, 23],
1803
- underline: [4, 24],
1804
- // Extended underline styles (`SGR 4:x` sub-parameters). Not in upstream `ansi-styles`.
1805
- underlineDouble: ["4:2", 24],
1806
- underlineCurly: ["4:3", 24],
1807
- underlineDotted: ["4:4", 24],
1808
- underlineDashed: ["4:5", 24],
1809
- overline: [53, 55],
1810
- inverse: [7, 27],
1811
- hidden: [8, 28],
1812
- strikethrough: [9, 29]
1813
- },
1814
- color: {
1815
- black: [30, 39],
1816
- red: [31, 39],
1817
- green: [32, 39],
1818
- yellow: [33, 39],
1819
- blue: [34, 39],
1820
- magenta: [35, 39],
1821
- cyan: [36, 39],
1822
- white: [37, 39],
1823
- // Bright color
1824
- blackBright: [90, 39],
1825
- gray: [90, 39],
1826
- // Alias of `blackBright`
1827
- grey: [90, 39],
1828
- // Alias of `blackBright`
1829
- redBright: [91, 39],
1830
- greenBright: [92, 39],
1831
- yellowBright: [93, 39],
1832
- blueBright: [94, 39],
1833
- magentaBright: [95, 39],
1834
- cyanBright: [96, 39],
1835
- whiteBright: [97, 39]
1836
- },
1837
- bgColor: {
1838
- bgBlack: [40, 49],
1839
- bgRed: [41, 49],
1840
- bgGreen: [42, 49],
1841
- bgYellow: [43, 49],
1842
- bgBlue: [44, 49],
1843
- bgMagenta: [45, 49],
1844
- bgCyan: [46, 49],
1845
- bgWhite: [47, 49],
1846
- // Bright color
1847
- bgBlackBright: [100, 49],
1848
- bgGray: [100, 49],
1849
- // Alias of `bgBlackBright`
1850
- bgGrey: [100, 49],
1851
- // Alias of `bgBlackBright`
1852
- bgRedBright: [101, 49],
1853
- bgGreenBright: [102, 49],
1854
- bgYellowBright: [103, 49],
1855
- bgBlueBright: [104, 49],
1856
- bgMagentaBright: [105, 49],
1857
- bgCyanBright: [106, 49],
1858
- bgWhiteBright: [107, 49]
1859
- },
1860
- // Underline color (`SGR 58`/`59`). Not in upstream `ansi-styles`.
1861
- underlineColor: {
1862
- underlineBlack: ["58;5;0", 59],
1863
- underlineRed: ["58;5;1", 59],
1864
- underlineGreen: ["58;5;2", 59],
1865
- underlineYellow: ["58;5;3", 59],
1866
- underlineBlue: ["58;5;4", 59],
1867
- underlineMagenta: ["58;5;5", 59],
1868
- underlineCyan: ["58;5;6", 59],
1869
- underlineWhite: ["58;5;7", 59],
1870
- // Bright color
1871
- underlineBlackBright: ["58;5;8", 59],
1872
- underlineGray: ["58;5;8", 59],
1873
- // Alias of `underlineBlackBright`
1874
- underlineGrey: ["58;5;8", 59],
1875
- // Alias of `underlineBlackBright`
1876
- underlineRedBright: ["58;5;9", 59],
1877
- underlineGreenBright: ["58;5;10", 59],
1878
- underlineYellowBright: ["58;5;11", 59],
1879
- underlineBlueBright: ["58;5;12", 59],
1880
- underlineMagentaBright: ["58;5;13", 59],
1881
- underlineCyanBright: ["58;5;14", 59],
1882
- underlineWhiteBright: ["58;5;15", 59]
1883
- }
1884
- };
1885
- var modifierNames = Object.keys(styles.modifier);
1886
- var foregroundColorNames = Object.keys(styles.color);
1887
- var backgroundColorNames = Object.keys(styles.bgColor);
1888
- var underlineColorNames = Object.keys(styles.underlineColor);
1889
- var colorNames = [...foregroundColorNames, ...backgroundColorNames];
1890
- function assembleStyles() {
1891
- const codes = /* @__PURE__ */ new Map();
1892
- for (const [groupName, group] of Object.entries(styles)) {
1893
- for (const [styleName, style] of Object.entries(group)) {
1894
- styles[styleName] = {
1895
- open: `\x1B[${style[0]}m`,
1896
- close: `\x1B[${style[1]}m`
1897
- };
1898
- group[styleName] = styles[styleName];
1899
- codes.set(Number.parseInt(style[0], 10), style[1]);
1900
- }
1901
- Object.defineProperty(styles, groupName, {
1902
- value: group,
1903
- enumerable: false
1904
- });
1905
- }
1906
- Object.defineProperty(styles, "codes", {
1907
- value: codes,
1908
- enumerable: false
1909
- });
1910
- styles.color.close = "\x1B[39m";
1911
- styles.bgColor.close = "\x1B[49m";
1912
- styles.underlineColor.close = "\x1B[59m";
1913
- styles.color.ansi = wrapAnsi16();
1914
- styles.color.ansi256 = wrapAnsi256();
1915
- styles.color.ansi16m = wrapAnsi16m();
1916
- styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
1917
- styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
1918
- styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
1919
- styles.underlineColor.ansi = wrapUnderlineAnsi;
1920
- styles.underlineColor.ansi256 = wrapAnsi256(ANSI_UNDERLINE_OFFSET);
1921
- styles.underlineColor.ansi16m = wrapAnsi16m(ANSI_UNDERLINE_OFFSET);
1922
- Object.defineProperties(styles, {
1923
- rgbToAnsi256: {
1924
- value(red, green, blue) {
1925
- if (red === green && green === blue) {
1926
- if (red < 8) {
1927
- return 16;
1928
- }
1929
- if (red > 248) {
1930
- return 231;
1931
- }
1932
- return Math.round((red - 8) / 247 * 24) + 232;
1933
- }
1934
- return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
1935
- },
1936
- enumerable: false
1937
- },
1938
- hexToRgb: {
1939
- value(hex) {
1940
- const matches = /[\da-f]{6}|[\da-f]{3}/i.exec(hex.toString(16));
1941
- if (!matches) {
1942
- return [0, 0, 0];
1943
- }
1944
- let [colorString] = matches;
1945
- if (colorString.length === 3) {
1946
- colorString = [...colorString].map((character) => character + character).join("");
1947
- }
1948
- const integer = Number.parseInt(colorString, 16);
1949
- return [
1950
- /* eslint-disable no-bitwise -- We need the speed */
1951
- integer >> 16 & 255,
1952
- integer >> 8 & 255,
1953
- integer & 255
1954
- /* eslint-enable no-bitwise */
1955
- ];
1956
- },
1957
- enumerable: false
1958
- },
1959
- hexToAnsi256: {
1960
- value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
1961
- enumerable: false
1962
- },
1963
- ansi256ToAnsi: {
1964
- value(code) {
1965
- if (code < 8) {
1966
- return 30 + code;
1967
- }
1968
- if (code < 16) {
1969
- return 90 + (code - 8);
1970
- }
1971
- let red;
1972
- let green;
1973
- let blue;
1974
- if (code >= 232) {
1975
- red = ((code - 232) * 10 + 8) / 255;
1976
- green = red;
1977
- blue = red;
1978
- } else {
1979
- code -= 16;
1980
- const remainder = code % 36;
1981
- red = Math.floor(code / 36) / 5;
1982
- green = Math.floor(remainder / 6) / 5;
1983
- blue = remainder % 6 / 5;
1984
- }
1985
- const value2 = Math.max(red, green, blue) * 2;
1986
- if (value2 === 0) {
1987
- return 30;
1988
- }
1989
- let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
1990
- if (value2 === 2) {
1991
- result += 60;
1992
- }
1993
- return result;
1994
- },
1995
- enumerable: false
1996
- },
1997
- rgbToAnsi: {
1998
- value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
1999
- enumerable: false
2000
- },
2001
- hexToAnsi: {
2002
- value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
2003
- enumerable: false
2004
- }
2005
- });
2006
- return styles;
2007
- }
2008
- var ansiStyles = assembleStyles();
2009
- var ansi_styles_default = ansiStyles;
2010
-
2011
- // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/supports-color/index.js
2012
- import process2 from "node:process";
2013
- import os from "node:os";
2014
- import tty from "node:tty";
2015
- function hasFlag(flag2, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
2016
- const prefix = flag2.startsWith("-") ? "" : flag2.length === 1 ? "-" : "--";
2017
- const position = argv.indexOf(prefix + flag2);
2018
- const terminatorPosition = argv.indexOf("--");
2019
- return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
2020
- }
2021
- var { env } = process2;
2022
- var flagForceColor;
2023
- if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
2024
- flagForceColor = 0;
2025
- } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
2026
- flagForceColor = 1;
2027
- }
2028
- function hasNumericForceColor() {
2029
- return /^\d+$/.test(env.FORCE_COLOR);
2003
+ // src/tui/views/queue.ts
2004
+ function queueRows(view) {
2005
+ return view.sections.flatMap((section) => [...section.rows, ...section.lists.flatMap((list) => list.rows)]);
2030
2006
  }
2031
- function envForceColor() {
2032
- if (!("FORCE_COLOR" in env)) {
2033
- return;
2034
- }
2035
- if (env.FORCE_COLOR === "false") {
2036
- return 0;
2037
- }
2038
- if (env.FORCE_COLOR === "true" || env.FORCE_COLOR.length === 0) {
2039
- return 1;
2040
- }
2041
- if (!hasNumericForceColor()) {
2042
- return;
2007
+ function groupedLists(entries, rowsOf2) {
2008
+ const groups = /* @__PURE__ */ new Map();
2009
+ for (const entry of entries) {
2010
+ const group = groups.get(entry.pr.repo) ?? [];
2011
+ group.push(entry);
2012
+ groups.set(entry.pr.repo, group);
2043
2013
  }
2044
- return Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
2014
+ return [...groups.entries()].toSorted((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])).map(([repo, group]) => {
2015
+ return { title: `${repo} (n=${group.length})`, rows: rowsOf2(group) };
2016
+ });
2045
2017
  }
2046
- function translateLevel(level) {
2047
- if (level === 0) {
2048
- return false;
2018
+ function buildPendingReviewView(raw, repo = null, grouped = false) {
2019
+ const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
2020
+ const awaiting = repo === null ? stats.pending : stats.pending.filter((entry) => entry.pr.repo === repo);
2021
+ const reviewing = repo === null ? stats.reviewing : stats.reviewing.filter((entry) => entry.pr.repo === repo);
2022
+ if (awaiting.length === 0 && reviewing.length === 0) {
2023
+ return { empty: "No PRs are awaiting your review, and none you reviewed are still open.", sections: [] };
2049
2024
  }
2025
+ const split = repo === null && grouped;
2026
+ const sectionOf = (title, entries) => split ? { title, rows: [], lists: groupedLists(entries, rowsOf) } : { title, rows: rowsOf(entries), lists: [] };
2050
2027
  return {
2051
- level,
2052
- hasBasic: true,
2053
- has256: level >= 2,
2054
- has16m: level >= 3
2028
+ empty: null,
2029
+ sections: [
2030
+ ...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting)],
2031
+ ...reviewing.length === 0 ? [] : [sectionOf(`Reviewing (n=${reviewing.length})`, reviewing)]
2032
+ ]
2055
2033
  };
2056
2034
  }
2057
- function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
2058
- const noFlagForceColor = envForceColor();
2059
- if (noFlagForceColor !== void 0) {
2060
- flagForceColor = noFlagForceColor;
2061
- }
2062
- const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
2063
- if (forceColor === 0) {
2064
- return 0;
2065
- }
2066
- if (sniffFlags) {
2067
- if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
2068
- return 3;
2069
- }
2070
- if (hasFlag("color=256")) {
2071
- return 2;
2072
- }
2073
- }
2074
- if (forceColor !== void 0 && hasNumericForceColor()) {
2075
- return forceColor;
2076
- }
2077
- if ("TF_BUILD" in env && "AGENT_NAME" in env) {
2078
- return 1;
2079
- }
2080
- if (haveStream && !streamIsTTY && forceColor === void 0) {
2081
- return 0;
2082
- }
2083
- const min = forceColor || 0;
2084
- if (env.TERM === "dumb") {
2085
- return min;
2035
+ function buildOpenAuthoredView(raw, repo = null, grouped = false) {
2036
+ const open = raw.sizes.filter((entry) => entry.pr.state === "open" && (repo === null || entry.pr.repo === repo)).toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime());
2037
+ if (open.length === 0) {
2038
+ return { empty: "No open authored PRs found.", sections: [] };
2086
2039
  }
2087
- if (process2.platform === "win32") {
2088
- const osRelease = os.release().split(".");
2089
- if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
2090
- return Number(osRelease[2]) >= 14931 ? 3 : 2;
2091
- }
2092
- return 1;
2040
+ const rowsOf2 = (group) => {
2041
+ const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
2042
+ const ageWidth = Math.max(...ages.map((age) => age.length));
2043
+ return toPrRows(
2044
+ group,
2045
+ group.map(
2046
+ (entry, i) => `${ages[i].padEnd(ageWidth)} +${entry.additions}/-${entry.deletions}, ${entry.files} files`
2047
+ )
2048
+ );
2049
+ };
2050
+ const title = `Your open authored PRs (n=${open.length})`;
2051
+ if (repo === null && grouped) {
2052
+ return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf2) }] };
2093
2053
  }
2094
- if ("CI" in env) {
2095
- if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
2096
- return 3;
2054
+ return { empty: null, sections: [{ title, rows: rowsOf2(open), lists: [] }] };
2055
+ }
2056
+ function rowsOf(group) {
2057
+ return toPrRows(
2058
+ group,
2059
+ group.map((entry) => durationLead(entry))
2060
+ );
2061
+ }
2062
+
2063
+ // src/tui/components/ChartsPanel.tsx
2064
+ import { useTerminalDimensions } from "@opentui/react";
2065
+
2066
+ // src/tui/hooks/scrollbar.ts
2067
+ import { CliRenderEvents } from "@opentui/core";
2068
+ import { useRenderer } from "@opentui/react";
2069
+ import { useLayoutEffect } from "react";
2070
+ var overlayScrollbar = {
2071
+ position: "absolute",
2072
+ top: 0,
2073
+ right: 0,
2074
+ bottom: 0
2075
+ };
2076
+ function useScrollbarSettle(scrollRef, mounted = true) {
2077
+ const renderer2 = useRenderer();
2078
+ useLayoutEffect(() => {
2079
+ if (!mounted) {
2080
+ return void 0;
2097
2081
  }
2098
- if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
2099
- return 1;
2082
+ if (!scrollRef.current?.verticalScrollBar) {
2083
+ return void 0;
2100
2084
  }
2101
- return min;
2102
- }
2103
- if ("TEAMCITY_VERSION" in env) {
2104
- return /^(?:9\.0*[1-9]\d*\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
2105
- }
2106
- if (env.COLORTERM === "truecolor") {
2107
- return 3;
2108
- }
2109
- if (env.TERM === "xterm-kitty") {
2110
- return 3;
2111
- }
2112
- if (env.TERM === "xterm-ghostty") {
2113
- return 3;
2114
- }
2115
- if (env.TERM === "wezterm") {
2116
- return 3;
2117
- }
2118
- if ("TERM_PROGRAM" in env) {
2119
- const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".", 1)[0], 10);
2120
- switch (env.TERM_PROGRAM) {
2121
- case "iTerm.app": {
2122
- return version >= 3 ? 3 : 2;
2085
+ scrollRef.current.verticalScrollBar.visible = false;
2086
+ const release = () => {
2087
+ renderer2.off(CliRenderEvents.FRAME, release);
2088
+ scrollRef.current?.verticalScrollBar.resetVisibilityControl();
2089
+ };
2090
+ const resyncThumb = () => {
2091
+ const bar = scrollRef.current?.verticalScrollBar;
2092
+ if (!bar) {
2093
+ return;
2123
2094
  }
2124
- case "Apple_Terminal": {
2125
- return 2;
2095
+ const height = bar.viewportSize;
2096
+ const wanted = Math.max(1, height);
2097
+ if (bar.scrollSize < wanted || bar.slider.viewPortSize === wanted) {
2098
+ return;
2126
2099
  }
2127
- }
2128
- }
2129
- if (/-256(?:color)?$/i.test(env.TERM)) {
2130
- return 2;
2131
- }
2132
- if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
2133
- return 1;
2134
- }
2135
- if ("COLORTERM" in env) {
2136
- return 1;
2137
- }
2138
- return min;
2139
- }
2140
- function createSupportsColor(stream, options = {}) {
2141
- const level = _supportsColor(stream, {
2142
- streamIsTTY: stream && stream.isTTY,
2143
- ...options
2144
- });
2145
- return translateLevel(level);
2100
+ bar.viewportSize = 0;
2101
+ bar.viewportSize = height;
2102
+ };
2103
+ renderer2.on(CliRenderEvents.FRAME, release);
2104
+ renderer2.on(CliRenderEvents.FRAME, resyncThumb);
2105
+ return () => {
2106
+ renderer2.off(CliRenderEvents.FRAME, release);
2107
+ renderer2.off(CliRenderEvents.FRAME, resyncThumb);
2108
+ };
2109
+ }, [scrollRef, renderer2, mounted]);
2146
2110
  }
2147
- var supportsColor = {
2148
- stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
2149
- stderr: createSupportsColor({ isTTY: tty.isatty(2) })
2150
- };
2151
- var supports_color_default = supportsColor;
2152
2111
 
2153
- // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/index.js
2154
- var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
2155
- var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
2156
- var STYLER = /* @__PURE__ */ Symbol("STYLER");
2157
- var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
2158
- var LEVEL = /* @__PURE__ */ Symbol("LEVEL");
2159
- var styles2 = /* @__PURE__ */ Object.create(null);
2160
- var assertValidLevel = (level) => {
2161
- if (!Number.isSafeInteger(level) || level < 0 || level > 3) {
2162
- throw new Error("The `level` should be an integer from 0 to 3");
2163
- }
2164
- };
2165
- var levelDescriptor = {
2166
- enumerable: true,
2167
- get() {
2168
- return this[LEVEL];
2169
- },
2170
- set(level) {
2171
- assertValidLevel(level);
2172
- this[LEVEL] = level;
2173
- }
2174
- };
2175
- var applyOptions = (object, options = {}) => {
2176
- if (options.level !== void 0) {
2177
- assertValidLevel(options.level);
2178
- }
2179
- const colorLevel = stdoutColor ? stdoutColor.level : 0;
2180
- object[LEVEL] = options.level === void 0 ? colorLevel : options.level;
2181
- };
2182
- var chalkFactory = (options) => {
2183
- const chalk2 = (...strings) => strings.join(" ");
2184
- applyOptions(chalk2, options);
2185
- Object.setPrototypeOf(chalk2, createChalk.prototype);
2186
- return chalk2;
2187
- };
2188
- function createChalk(options) {
2189
- return chalkFactory(options);
2112
+ // src/tui/views/charts/model.ts
2113
+ function lineLength(line) {
2114
+ return line.reduce((sum, span) => sum + span.text.length, 0);
2190
2115
  }
2191
- Object.setPrototypeOf(createChalk.prototype, Function.prototype);
2192
- for (const [styleName, style] of Object.entries(ansi_styles_default)) {
2193
- styles2[styleName] = {
2194
- get() {
2195
- const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
2196
- Object.defineProperty(this, styleName, { value: builder });
2197
- return builder;
2198
- }
2199
- };
2116
+ function cardWidth(card) {
2117
+ const subtitleLength = typeof card.subtitle === "string" ? card.subtitle.length : lineLength(card.subtitle);
2118
+ return Math.max(card.title.length + 2 + subtitleLength, ...card.lines.map(lineLength));
2200
2119
  }
2201
- styles2.visible = {
2202
- get() {
2203
- const builder = createBuilder(this, this[STYLER], true);
2204
- Object.defineProperty(this, "visible", { value: builder });
2205
- return builder;
2206
- }
2207
- };
2208
- var createModelConverters = (model, type) => {
2209
- const style = ansi_styles_default[type];
2210
- if (model === "rgb") {
2211
- const ansi2 = (red, green, blue) => style.ansi(ansi_styles_default.rgbToAnsi(red, green, blue));
2212
- const ansi256 = (red, green, blue) => style.ansi256(ansi_styles_default.rgbToAnsi256(red, green, blue));
2213
- return [ansi2, ansi2, ansi256, style.ansi16m];
2214
- }
2215
- if (model === "hex") {
2216
- const ansi2 = (hex) => style.ansi(ansi_styles_default.hexToAnsi(hex));
2217
- const ansi256 = (hex) => style.ansi256(ansi_styles_default.hexToAnsi256(hex));
2218
- return [ansi2, ansi2, ansi256, (hex) => style.ansi16m(...ansi_styles_default.hexToRgb(hex))];
2120
+ function formatDuration(hours) {
2121
+ if (hours < 1) {
2122
+ return `${Math.round(hours * 60)}m`;
2219
2123
  }
2220
- const ansi = (code) => style.ansi(ansi_styles_default.ansi256ToAnsi(code));
2221
- return [ansi, ansi, style.ansi256, style.ansi256];
2222
- };
2223
- var usedModels = ["rgb", "hex", "ansi256"];
2224
- for (const model of usedModels) {
2225
- const capitalizedModel = model[0].toUpperCase() + model.slice(1);
2226
- for (const [styleName, type] of [
2227
- [model, "color"],
2228
- ["bg" + capitalizedModel, "bgColor"],
2229
- ["underline" + capitalizedModel, "underlineColor"]
2230
- ]) {
2231
- const { close } = ansi_styles_default[type];
2232
- const converters = createModelConverters(model, type);
2233
- styles2[styleName] = {
2234
- get() {
2235
- const styleFunction = function(first, second, third) {
2236
- const open = converters[this.level](first, second, third);
2237
- return createBuilder(this, createStyler(open, close, this[STYLER]), this[IS_EMPTY]);
2238
- };
2239
- Object.defineProperty(this, styleName, { value: styleFunction });
2240
- return styleFunction;
2241
- }
2242
- };
2124
+ if (hours < 2 * timeMode.dayHours) {
2125
+ return `${compact(hours)}h`;
2243
2126
  }
2127
+ return `${compact(hours / timeMode.dayHours)}${timeMode.business && !isFullDayMode() ? "wd" : "d"}`;
2244
2128
  }
2245
- var proto = Object.defineProperties(
2246
- () => {
2247
- },
2248
- {
2249
- ...styles2,
2250
- level: {
2251
- enumerable: true,
2252
- get() {
2253
- return this[GENERATOR].level;
2254
- },
2255
- set(level) {
2256
- this[GENERATOR].level = level;
2257
- }
2258
- }
2259
- }
2260
- );
2261
- var createStyler = (open, close, parent) => {
2262
- let openAll;
2263
- let closeAll;
2264
- if (parent === void 0) {
2265
- openAll = open;
2266
- closeAll = close;
2267
- } else {
2268
- openAll = parent.openAll + open;
2269
- closeAll = close + parent.closeAll;
2129
+ function compact(value2) {
2130
+ return String(Math.round(value2 * 10) / 10);
2131
+ }
2132
+
2133
+ // src/tui/components/ChartsPanel.tsx
2134
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
2135
+ var COLUMN_GAP = 4;
2136
+ function ChartsPanel({
2137
+ scrollRef,
2138
+ focused,
2139
+ heading,
2140
+ warning,
2141
+ view
2142
+ }) {
2143
+ useScrollbarSettle(scrollRef, view.empty === null);
2144
+ const { width } = useTerminalDimensions();
2145
+ if (view.empty !== null) {
2146
+ return /* @__PURE__ */ jsx4("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx4("text", { fg: theme.muted, children: view.empty }) });
2270
2147
  }
2271
- return {
2272
- open,
2273
- close,
2274
- openAll,
2275
- closeAll,
2276
- parent
2277
- };
2278
- };
2279
- var createBuilder = (self, _styler, _isEmpty) => {
2280
- const builder = (...arguments_) => {
2281
- if (arguments_.length === 1) {
2282
- return applyStyle(builder, "" + arguments_[0]);
2283
- }
2284
- if (arguments_.length === 2) {
2285
- return applyStyle(builder, arguments_[0] + " " + arguments_[1]);
2286
- }
2287
- return applyStyle(builder, arguments_.join(" "));
2288
- };
2289
- Object.setPrototypeOf(builder, proto);
2290
- builder[GENERATOR] = self[GENERATOR] ?? self;
2291
- builder[STYLER] = _styler;
2292
- builder[IS_EMPTY] = _isEmpty;
2293
- return builder;
2294
- };
2295
- var applyStyle = (self, string) => {
2296
- if (self[GENERATOR][LEVEL] <= 0 || !string) {
2297
- return self[IS_EMPTY] ? "" : string;
2148
+ const left = view.cards.filter((_, i) => i % 2 === 0);
2149
+ const right = view.cards.filter((_, i) => i % 2 === 1);
2150
+ const leftWidth = Math.max(0, ...left.map((card) => cardWidth(card)));
2151
+ const rightWidth = Math.max(0, ...right.map((card) => cardWidth(card)));
2152
+ const twoColumns = right.length > 0 && width - 4 >= leftWidth + COLUMN_GAP + rightWidth;
2153
+ const rows = left.map((card, i) => {
2154
+ return { key: card.title, left: card, right: right.at(i) };
2155
+ });
2156
+ const cards = twoColumns ? /* @__PURE__ */ jsx4("box", { flexDirection: "column", rowGap: 1, children: rows.map((row) => /* @__PURE__ */ jsxs3("box", { flexDirection: "row", alignItems: "flex-start", columnGap: COLUMN_GAP, children: [
2157
+ /* @__PURE__ */ jsx4("box", { flexDirection: "column", width: leftWidth, flexShrink: 0, children: /* @__PURE__ */ jsx4(ChartCard, { card: row.left }) }),
2158
+ /* @__PURE__ */ jsx4("box", { flexDirection: "column", width: rightWidth, flexShrink: 0, children: row.right !== void 0 && /* @__PURE__ */ jsx4(ChartCard, { card: row.right }) })
2159
+ ] }, row.key)) }) : /* @__PURE__ */ jsx4("box", { flexDirection: "column", rowGap: 1, children: view.cards.map((card) => /* @__PURE__ */ jsx4(ChartCard, { card }, card.title)) });
2160
+ return /* @__PURE__ */ jsxs3("box", { flexGrow: 1, flexDirection: "column", children: [
2161
+ /* @__PURE__ */ jsx4("box", { height: 1, children: /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
2162
+ /* @__PURE__ */ jsx4("box", { height: 1, paddingLeft: 1, paddingRight: 2, flexDirection: "row", justifyContent: "space-between", children: keyed(view.strip, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) }),
2163
+ (heading !== null || view.headline !== null) && /* @__PURE__ */ jsxs3("box", { height: 1, paddingLeft: 1, paddingRight: 2, flexDirection: "row", justifyContent: "space-between", children: [
2164
+ /* @__PURE__ */ jsx4("text", { wrapMode: "none", children: heading !== null && /* @__PURE__ */ jsxs3(Fragment2, { children: [
2165
+ /* @__PURE__ */ jsx4("span", { fg: theme.accent, children: "\u25B8 " }),
2166
+ /* @__PURE__ */ jsx4("b", { fg: theme.text, children: heading })
2167
+ ] }) }),
2168
+ view.headline !== null && /* @__PURE__ */ jsx4(ChartLine, { line: view.headline })
2169
+ ] }),
2170
+ /* @__PURE__ */ jsx4("box", { height: 1, children: /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
2171
+ /* @__PURE__ */ jsxs3(
2172
+ "scrollbox",
2173
+ {
2174
+ ref: scrollRef,
2175
+ focused,
2176
+ flexGrow: 1,
2177
+ paddingLeft: 1,
2178
+ paddingRight: 2,
2179
+ verticalScrollbarOptions: overlayScrollbar,
2180
+ children: [
2181
+ warning !== null && /* @__PURE__ */ jsx4("text", { wrapMode: "word", fg: theme.warn, marginTop: 1, children: warning }),
2182
+ view.lists.map((list) => /* @__PURE__ */ jsxs3("box", { flexDirection: "column", marginTop: 1, children: [
2183
+ /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.accent, children: list.title }),
2184
+ list.rows.map((row) => /* @__PURE__ */ jsxs3("text", { wrapMode: "none", fg: theme.text, children: [
2185
+ " ",
2186
+ row.lead,
2187
+ " ",
2188
+ /* @__PURE__ */ jsx4("a", { href: row.url, fg: theme.accent, children: row.ref }),
2189
+ " ",
2190
+ row.title
2191
+ ] }, row.url))
2192
+ ] }, list.title)),
2193
+ view.distribution !== null && /* @__PURE__ */ jsxs3("box", { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [
2194
+ /* @__PURE__ */ jsxs3("box", { flexDirection: "row", justifyContent: "space-between", children: [
2195
+ /* @__PURE__ */ jsx4("text", { wrapMode: "none", children: /* @__PURE__ */ jsx4("b", { fg: theme.text, children: view.distributionTitle }) }),
2196
+ /* @__PURE__ */ jsx4(ChartLine, { line: view.distribution.stats })
2197
+ ] }),
2198
+ /* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(view.distribution.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
2199
+ ] }),
2200
+ view.cards.length === 0 ? /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.muted, marginTop: 1, children: view.noCharts }) : cards
2201
+ ]
2202
+ }
2203
+ )
2204
+ ] });
2205
+ }
2206
+ function keyed(items, contentOf) {
2207
+ const seen = /* @__PURE__ */ new Map();
2208
+ return items.map((item) => {
2209
+ const content = contentOf(item);
2210
+ const count2 = seen.get(content) ?? 0;
2211
+ seen.set(content, count2 + 1);
2212
+ return { key: `${count2}:${content}`, item };
2213
+ });
2214
+ }
2215
+ function spanKey(span) {
2216
+ return `${span.fg ?? ""}|${span.bg ?? ""}|${span.bold === true ? "b" : ""}|${span.text}`;
2217
+ }
2218
+ function lineKey(line) {
2219
+ return line.map((span) => spanKey(span)).join("&");
2220
+ }
2221
+ function ChartLine({ line }) {
2222
+ return /* @__PURE__ */ jsx4("text", { wrapMode: "none", children: keyed(line, spanKey).map(
2223
+ ({ key, item }) => item.bold === true ? /* @__PURE__ */ jsx4("b", { fg: item.fg ?? theme.text, bg: item.bg, children: item.text }, key) : /* @__PURE__ */ jsx4("span", { fg: item.fg ?? theme.text, bg: item.bg, children: item.text }, key)
2224
+ ) });
2225
+ }
2226
+ function ChartCard({ card }) {
2227
+ const subtitle = typeof card.subtitle === "string" ? [{ text: card.subtitle, fg: theme.muted }] : card.subtitle;
2228
+ return /* @__PURE__ */ jsxs3("box", { flexDirection: "column", children: [
2229
+ /* @__PURE__ */ jsxs3("text", { wrapMode: "none", children: [
2230
+ /* @__PURE__ */ jsx4("b", { fg: theme.text, children: card.title }),
2231
+ /* @__PURE__ */ jsx4("span", { fg: theme.muted, children: " " }),
2232
+ keyed(subtitle, spanKey).map(({ key, item }) => /* @__PURE__ */ jsx4("span", { fg: item.fg ?? theme.muted, children: item.text }, key))
2233
+ ] }),
2234
+ /* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(card.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
2235
+ ] });
2236
+ }
2237
+
2238
+ // src/tui/components/Placeholder.tsx
2239
+ import { jsx as jsx5, jsxs as jsxs4 } from "@opentui/react/jsx-runtime";
2240
+ var BAR_WIDTH = 40;
2241
+ var PARTIAL_BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
2242
+ function Placeholder({
2243
+ error,
2244
+ loading,
2245
+ load
2246
+ }) {
2247
+ return /* @__PURE__ */ jsx5("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", flexDirection: "column", children: error !== null ? /* @__PURE__ */ jsx5("text", { fg: theme.error, children: error }) : loading && load !== null ? /* @__PURE__ */ jsx5(LoadProgress, { load }) : /* @__PURE__ */ jsx5("text", { fg: theme.muted, children: "no data yet, press r to load" }) });
2248
+ }
2249
+ function LoadProgress({ load }) {
2250
+ if (!load.total) {
2251
+ return /* @__PURE__ */ jsxs4("box", { flexDirection: "row", columnGap: 1, children: [
2252
+ /* @__PURE__ */ jsx5("text", { fg: theme.muted, children: loadLabel(load) }),
2253
+ /* @__PURE__ */ jsx5(Spinner, {})
2254
+ ] });
2298
2255
  }
2299
- let styler = self[STYLER];
2300
- if (styler === void 0) {
2301
- return string;
2256
+ const done = load.done ?? 0;
2257
+ const cells = Math.min(done / load.total, 1) * BAR_WIDTH;
2258
+ const whole = Math.floor(cells);
2259
+ const partial = whole < BAR_WIDTH ? PARTIAL_BLOCKS[Math.floor((cells - whole) * 8)] : "";
2260
+ const counter = `${String(done).padStart(String(load.total).length)}/${load.total}`;
2261
+ return /* @__PURE__ */ jsxs4("box", { flexDirection: "column", alignItems: "center", rowGap: 1, children: [
2262
+ /* @__PURE__ */ jsx5("text", { fg: theme.muted, children: loadLabel(load) }),
2263
+ /* @__PURE__ */ jsxs4("text", { wrapMode: "none", children: [
2264
+ /* @__PURE__ */ jsx5("span", { bg: theme.accent, children: " ".repeat(whole) }),
2265
+ /* @__PURE__ */ jsx5("span", { fg: theme.accent, bg: theme.selectedBg, children: partial }),
2266
+ /* @__PURE__ */ jsx5("span", { bg: theme.selectedBg, children: " ".repeat(BAR_WIDTH - whole - partial.length) }),
2267
+ /* @__PURE__ */ jsx5("span", { fg: theme.muted, children: ` ${counter}` })
2268
+ ] })
2269
+ ] });
2270
+ }
2271
+ function loadLabel(load) {
2272
+ return load.phase === "search" ? "searching PRs" : "fetching PR details";
2273
+ }
2274
+
2275
+ // src/tui/components/QueuePanel.tsx
2276
+ import { useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
2277
+ import { useEffect as useEffect2, useRef } from "react";
2278
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "@opentui/react/jsx-runtime";
2279
+ function QueuePanel({
2280
+ heading,
2281
+ warning,
2282
+ empty,
2283
+ sections,
2284
+ cursor,
2285
+ onRefClick
2286
+ }) {
2287
+ const scrollRef = useRef(null);
2288
+ const { width } = useTerminalDimensions2();
2289
+ const lastDown = useRef(null);
2290
+ useScrollbarSettle(scrollRef, empty === null);
2291
+ useEffect2(() => {
2292
+ scrollRef.current?.scrollChildIntoView(`queue-row-${cursor}`);
2293
+ }, [cursor]);
2294
+ const header = heading === null ? null : /* @__PURE__ */ jsxs5(Fragment3, { children: [
2295
+ /* @__PURE__ */ jsx6("box", { height: 1, children: /* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
2296
+ /* @__PURE__ */ jsx6("box", { height: 1, paddingLeft: 1, paddingRight: 2, children: /* @__PURE__ */ jsxs5("text", { wrapMode: "none", children: [
2297
+ /* @__PURE__ */ jsx6("span", { fg: theme.accent, children: "\u25B8 " }),
2298
+ /* @__PURE__ */ jsx6("b", { fg: theme.text, children: heading })
2299
+ ] }) }),
2300
+ /* @__PURE__ */ jsx6("box", { height: 1, children: /* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) })
2301
+ ] });
2302
+ if (empty !== null) {
2303
+ return /* @__PURE__ */ jsxs5("box", { flexGrow: 1, flexDirection: "column", children: [
2304
+ header,
2305
+ /* @__PURE__ */ jsx6("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx6("text", { fg: theme.muted, children: empty }) })
2306
+ ] });
2302
2307
  }
2303
- const { openAll, closeAll } = styler;
2304
- if (string.includes("\x1B")) {
2305
- while (styler !== void 0) {
2306
- string = stringReplaceAll(string, styler.close, styler.open);
2307
- styler = styler.parent;
2308
+ const offsets = [];
2309
+ let total = 0;
2310
+ for (const section of sections) {
2311
+ const entry = { rows: total, lists: [] };
2312
+ total += section.rows.length;
2313
+ for (const list of section.lists) {
2314
+ entry.lists.push(total);
2315
+ total += list.rows.length;
2308
2316
  }
2317
+ offsets.push(entry);
2309
2318
  }
2310
- const lfIndex = string.indexOf("\n");
2311
- if (lfIndex !== -1) {
2312
- string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
2313
- }
2314
- return openAll + string + closeAll;
2315
- };
2316
- Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
2317
- var chalk = createChalk();
2318
- var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
2319
- var source_default = chalk;
2320
-
2321
- // src/flags.ts
2322
- import { parseArgs } from "node:util";
2323
- var flag = source_default.green;
2324
- var value = source_default.cyan;
2325
- var dim = source_default.dim;
2326
- var OPTIONS = [
2327
- {
2328
- name: "since",
2329
- short: "s",
2330
- type: "string",
2331
- default: "90d",
2332
- placeholder: "<value>",
2333
- help: "Only include PRs created after this point. Accepts an ISO date (`2026-01-01`) or a relative value (`30d`, `8w`, `6m`, `1y`). The default is `90d`."
2334
- },
2335
- {
2336
- name: "repo",
2337
- short: "r",
2338
- type: "string",
2339
- multiple: true,
2340
- default: [],
2341
- placeholder: "<name>",
2342
- help: 'Restrict the search to a repository. Repeat the flag for multiple repositories. Accepts "owner/name" or a bare name, which resolves against the owner of the repo in the current directory. Without this flag, all repositories you have access to are searched.'
2343
- },
2344
- {
2345
- name: "user",
2346
- short: "u",
2347
- type: "string",
2348
- placeholder: "<login>",
2349
- help: "Compute stats for this user. Defaults to the authenticated user."
2350
- },
2351
- {
2352
- name: "token",
2353
- type: "string",
2354
- placeholder: "<token>",
2355
- help: "GitHub access token. When set, the tool calls the GitHub API directly instead of going through the gh CLI. The `GITHUB_TOKEN` and `GH_TOKEN` environment variables work as well."
2356
- },
2357
- {
2358
- name: "target",
2359
- short: "t",
2360
- type: "string",
2361
- placeholder: "<value>",
2362
- help: "Report how many reviews finished within this time. Accepts hours (`24h` or plain `24`), minutes (`90m`), or days (`2d`). A day means 24 counted hours, or one working day when --work-hours is set. Open PRs that have already waited longer than the target count as misses."
2363
- },
2364
- {
2365
- name: "target-percentile",
2366
- type: "string",
2367
- placeholder: "<p>",
2368
- help: "Check the --target against this percentile of your review times. Accepts a whole percentile from 1 to 100, with an optional p prefix (`90` or `p90`). The default is `90`, so the review headline reports whether your p90 review time meets the target."
2369
- },
2370
- {
2371
- name: "size-target",
2372
- type: "string",
2373
- placeholder: "<v>",
2374
- help: "Report how many authored PRs fit within this size. Accepts a line budget (`400` or `400l`), a file budget (`20f`), or both (`400l,20f`). Lines count additions plus deletions."
2375
- },
2376
- {
2377
- name: "tz",
2378
- type: "string",
2379
- placeholder: "<zone>",
2380
- help: "IANA timezone for the weekend and working-hours math, for example `Europe/Berlin`. Defaults to your system timezone."
2381
- },
2382
- {
2383
- name: "work-hours",
2384
- short: "w",
2385
- type: "string",
2386
- default: "0-24",
2387
- placeholder: "<v>",
2388
- help: "Count only these working hours instead of full weekdays. Accepts 24-hour ranges like `9-17` or `8:30-16:30`, 12-hour ranges like `9am-6pm` or `8:30am-4:30pm`, or several comma-separated ranges like `9-18,19:30-20:30`. Without this flag, every hour Mon-Fri counts."
2389
- },
2390
- {
2391
- name: "wall-clock",
2392
- type: "boolean",
2393
- default: false,
2394
- help: "Measure raw elapsed time, including weekends."
2395
- },
2396
- {
2397
- name: "include-drafts",
2398
- type: "boolean",
2399
- default: false,
2400
- help: "Include PRs that are currently drafts. Excluded by default."
2401
- },
2402
- {
2403
- name: "review-types",
2404
- type: "string",
2405
- placeholder: "<list>",
2406
- help: "Count only these review types as a review. Accepts a comma-separated list of `approve`, `comment`, and `request-changes`, for example `approve,request-changes`. A review of another type never answers a request, so the PR stays in the awaiting queue until a counted review lands. Without this flag, every submitted review counts."
2407
- },
2408
- {
2409
- name: "no-cache",
2410
- type: "boolean",
2411
- default: false,
2412
- help: "Refetch every PR instead of reading the local disk cache. Closed PRs are normally served from a per-PR cache because their timelines and sizes no longer change. Fresh results still update the cache. The TUI settings dialog can save this behavior for every run, and the flag wins over the saved setting."
2413
- },
2414
- {
2415
- name: "json",
2416
- type: "boolean",
2417
- default: false,
2418
- help: "Print every stat as JSON to stdout instead of starting the TUI, so the output can be piped into jq or redirected to a file. The report holds the review, size, merge, reviewer, and comment stats with one entry per PR, and every other flag applies to it the same way. Progress renders on stderr, so a piped stdout stays pure JSON."
2419
- },
2420
- {
2421
- name: "debug",
2422
- type: "string",
2423
- placeholder: "<path>",
2424
- help: "Serve canned data from a fake gh binary instead of fetching from GitHub. Accepts a path to a testdata directory that contains a `gh` executable, for example `tui/testdata`, or a path to the executable itself. Tokens are ignored while this flag is set. This is useful for testing and local development."
2425
- },
2426
- {
2427
- name: "help",
2428
- short: "h",
2429
- type: "boolean",
2430
- default: false,
2431
- help: "Show this help."
2432
- }
2433
- ];
2434
- var HELP_WIDTH = Math.min(process.stdout.isTTY ? process.stdout.columns : 80, 120);
2435
- function colorizeHelp(text) {
2436
- return text.replaceAll(/`([^`]+)`/g, (match, span) => value(span)).replaceAll(/"[^"]*"/g, (span) => source_default.yellow(span)).replaceAll(/--[a-z][a-z-]*/g, (span) => flag(span));
2319
+ const renderRow = (row, index, indent) => {
2320
+ const isSelected = index === cursor;
2321
+ const bg = isSelected ? theme.selectedBg : void 0;
2322
+ const refStart = indent.length + 2 + row.lead.length + 2;
2323
+ return /* @__PURE__ */ jsxs5(
2324
+ "text",
2325
+ {
2326
+ id: `queue-row-${index}`,
2327
+ wrapMode: "none",
2328
+ onMouseDown: onRefClick === null ? void 0 : (event) => {
2329
+ lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
2330
+ },
2331
+ onMouseUp: onRefClick === null ? void 0 : (event) => {
2332
+ const down = lastDown.current;
2333
+ lastDown.current = null;
2334
+ if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
2335
+ return;
2336
+ }
2337
+ const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
2338
+ if (local >= refStart && local < refStart + row.ref.length) {
2339
+ onRefClick(row);
2340
+ }
2341
+ },
2342
+ children: [
2343
+ /* @__PURE__ */ jsxs5("span", { fg: theme.accent, children: [
2344
+ indent,
2345
+ isSelected ? "\u25B8 " : " "
2346
+ ] }),
2347
+ /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
2348
+ row.lead,
2349
+ " "
2350
+ ] }),
2351
+ onRefClick === null ? /* @__PURE__ */ jsx6("a", { href: row.url, fg: theme.accent, bg, children: row.ref }) : /* @__PURE__ */ jsx6("span", { fg: theme.accent, bg, children: row.ref }),
2352
+ /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
2353
+ " ",
2354
+ row.title
2355
+ ] })
2356
+ ]
2357
+ },
2358
+ row.url
2359
+ );
2360
+ };
2361
+ return /* @__PURE__ */ jsxs5("box", { flexGrow: 1, flexDirection: "column", children: [
2362
+ header,
2363
+ /* @__PURE__ */ jsxs5(
2364
+ "scrollbox",
2365
+ {
2366
+ ref: scrollRef,
2367
+ flexGrow: 1,
2368
+ paddingLeft: 1,
2369
+ paddingRight: 2,
2370
+ verticalScrollbarOptions: overlayScrollbar,
2371
+ children: [
2372
+ warning !== null && /* @__PURE__ */ jsx6("text", { wrapMode: "word", fg: theme.warn, marginTop: 1, children: warning }),
2373
+ sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: 1, children: [
2374
+ /* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.accent, children: section.title }),
2375
+ section.rows.map((row, rowIndex) => renderRow(row, offsets[sectionIndex].rows + rowIndex, "")),
2376
+ section.lists.map((list, listIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: listIndex > 0 ? 1 : 0, children: [
2377
+ /* @__PURE__ */ jsxs5("text", { wrapMode: "none", fg: theme.accent, children: [
2378
+ " ",
2379
+ list.title
2380
+ ] }),
2381
+ list.rows.map(
2382
+ (row, rowIndex) => renderRow(row, offsets[sectionIndex].lists[listIndex] + rowIndex, " ")
2383
+ )
2384
+ ] }, list.title))
2385
+ ] }, section.title))
2386
+ ]
2387
+ }
2388
+ )
2389
+ ] });
2437
2390
  }
2438
- function wrapMarkup(text, width) {
2439
- const lines = [];
2440
- let line = "";
2441
- for (const word of text.split(" ")) {
2442
- const candidate = line === "" ? word : `${line} ${word}`;
2443
- if (line !== "" && candidate.replaceAll("`", "").length > width) {
2444
- lines.push(line);
2445
- line = word;
2446
- } else {
2447
- line = candidate;
2391
+
2392
+ // src/tui/components/RepoList.tsx
2393
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
2394
+ import { jsx as jsx7, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
2395
+ function RepoList({
2396
+ options,
2397
+ cursor,
2398
+ prompt = "Select a repository and press enter to open its charts."
2399
+ }) {
2400
+ const scrollRef = useRef2(null);
2401
+ useScrollbarSettle(scrollRef);
2402
+ useEffect3(() => {
2403
+ scrollRef.current?.scrollChildIntoView(`repo-row-${cursor}`);
2404
+ }, [cursor]);
2405
+ const labelWidth = Math.max(...options.map((option) => option.label.length));
2406
+ return /* @__PURE__ */ jsxs6(
2407
+ "scrollbox",
2408
+ {
2409
+ ref: scrollRef,
2410
+ flexGrow: 1,
2411
+ paddingLeft: 1,
2412
+ paddingRight: 2,
2413
+ verticalScrollbarOptions: overlayScrollbar,
2414
+ children: [
2415
+ /* @__PURE__ */ jsx7("text", { wrapMode: "none", fg: theme.muted, children: prompt }),
2416
+ /* @__PURE__ */ jsx7("box", { flexDirection: "column", marginTop: 1, children: options.map((option, i) => {
2417
+ const isSelected = i === cursor;
2418
+ return /* @__PURE__ */ jsxs6("text", { id: `repo-row-${i}`, wrapMode: "none", children: [
2419
+ /* @__PURE__ */ jsx7("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
2420
+ /* @__PURE__ */ jsx7("span", { fg: isSelected ? theme.text : theme.muted, bg: isSelected ? theme.selectedBg : void 0, children: ` ${option.label.padEnd(labelWidth)} ` }),
2421
+ /* @__PURE__ */ jsxs6("span", { fg: theme.muted, children: [
2422
+ " ",
2423
+ option.detail
2424
+ ] })
2425
+ ] }, option.label);
2426
+ }) })
2427
+ ]
2448
2428
  }
2449
- }
2450
- if (line !== "") {
2451
- lines.push(line);
2452
- }
2453
- return lines;
2429
+ );
2454
2430
  }
2455
- function renderHelp() {
2456
- const labels = OPTIONS.map((option) => {
2457
- const short = option.short ? `-${option.short}, ` : "";
2458
- const long = option.placeholder ? `--${option.name} ${option.placeholder}` : `--${option.name}`;
2459
- return short + long;
2460
- });
2461
- const column = Math.max(...labels.map((label) => label.length)) + 4;
2462
- const lines = [`${source_default.bold("Usage:")} ${source_default.bold("pr-stats")} ${dim("[options]")}`, "", source_default.bold("Options:")];
2463
- for (const [index, option] of OPTIONS.entries()) {
2464
- const short = option.short ? `${flag(`-${option.short}`)}, ` : "";
2465
- const long = option.placeholder ? `${flag(`--${option.name}`)} ${dim(option.placeholder)}` : flag(`--${option.name}`);
2466
- const body = wrapMarkup(option.help, HELP_WIDTH - column).map((line) => colorizeHelp(line));
2467
- lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
2468
- for (const overflow of body.slice(1)) {
2469
- lines.push(" ".repeat(column) + overflow);
2470
- }
2431
+
2432
+ // src/tui/state/browse.ts
2433
+ var TABS = ["1 Awaiting you", "2 Your PRs", "3 Review time", "4 PR size", "5 Comments"];
2434
+ var initialBrowseState = {
2435
+ tab: 0,
2436
+ authoredTab: "open",
2437
+ scopes: {
2438
+ pending: { view: "list" },
2439
+ open: { view: "list" },
2440
+ review: { view: "list" },
2441
+ size: { view: "list" },
2442
+ comment: { view: "list" },
2443
+ merged: { view: "list" }
2444
+ },
2445
+ repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0, merged: 0 },
2446
+ rowCursors: { pending: 0, open: 0 },
2447
+ grouped: { pending: false, open: false },
2448
+ expanded: { review: false, size: false, comment: false, merged: false }
2449
+ };
2450
+ function dropVanishedRepo(scope, repos) {
2451
+ if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
2452
+ return { view: "list" };
2471
2453
  }
2472
- return lines.join("\n");
2454
+ return scope;
2473
2455
  }
2474
- var HELP = renderHelp();
2475
- function parseCliArgs(args) {
2476
- const options = {};
2477
- for (const option of OPTIONS) {
2478
- options[option.name] = { type: option.type };
2479
- if (option.short) {
2480
- options[option.name].short = option.short;
2456
+ function cycled(previous, delta, count2) {
2457
+ return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
2458
+ }
2459
+ function browseReducer(state, action) {
2460
+ switch (action.type) {
2461
+ case "tabSelected": {
2462
+ return { ...state, tab: action.tab };
2481
2463
  }
2482
- if (option.multiple) {
2483
- options[option.name].multiple = true;
2464
+ case "tabCycled": {
2465
+ return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
2484
2466
  }
2485
- }
2486
- const { values } = parseArgs({ options, args });
2487
- const explicit = new Set(Object.keys(values));
2488
- for (const option of OPTIONS) {
2489
- if (option.default !== void 0 && values[option.name] === void 0) {
2490
- values[option.name] = option.default;
2467
+ case "subTabToggled": {
2468
+ return { ...state, authoredTab: state.authoredTab === "open" ? "merged" : "open" };
2491
2469
  }
2492
- }
2493
- return { values, explicit };
2494
- }
2495
- function parseSince(input) {
2496
- const relative = /^(\d+)([dwmy])$/.exec(input);
2497
- if (relative) {
2498
- const amount = Number(relative[1]);
2499
- const date2 = /* @__PURE__ */ new Date();
2500
- if (relative[2] === "d") {
2501
- date2.setDate(date2.getDate() - amount);
2470
+ case "repoCursorMoved": {
2471
+ return {
2472
+ ...state,
2473
+ repoCursors: {
2474
+ ...state.repoCursors,
2475
+ [action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
2476
+ }
2477
+ };
2502
2478
  }
2503
- if (relative[2] === "w") {
2504
- date2.setDate(date2.getDate() - amount * 7);
2479
+ case "rowCursorMoved": {
2480
+ return {
2481
+ ...state,
2482
+ rowCursors: {
2483
+ ...state.rowCursors,
2484
+ [action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
2485
+ }
2486
+ };
2505
2487
  }
2506
- if (relative[2] === "m") {
2507
- date2.setMonth(date2.getMonth() - amount);
2488
+ case "repoOpened": {
2489
+ return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
2508
2490
  }
2509
- if (relative[2] === "y") {
2510
- date2.setFullYear(date2.getFullYear() - amount);
2491
+ case "pickerReturned": {
2492
+ return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
2493
+ }
2494
+ case "groupingToggled": {
2495
+ return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
2496
+ }
2497
+ case "expandToggled": {
2498
+ return { ...state, expanded: { ...state.expanded, [action.tab]: !state.expanded[action.tab] } };
2499
+ }
2500
+ case "dataLoaded": {
2501
+ return {
2502
+ ...state,
2503
+ scopes: {
2504
+ pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
2505
+ open: dropVanishedRepo(state.scopes.open, action.repos.open),
2506
+ review: dropVanishedRepo(state.scopes.review, action.repos.review),
2507
+ size: dropVanishedRepo(state.scopes.size, action.repos.size),
2508
+ comment: dropVanishedRepo(state.scopes.comment, action.repos.comment),
2509
+ merged: dropVanishedRepo(state.scopes.merged, action.repos.merged)
2510
+ }
2511
+ };
2511
2512
  }
2512
- return date2;
2513
- }
2514
- const date = new Date(input);
2515
- if (Number.isNaN(date.getTime())) {
2516
- throw new CliError(`invalid --since value "${input}", use an ISO date or 30d/8w/6m/1y`);
2517
2513
  }
2518
- return date;
2514
+ return state;
2515
+ }
2516
+
2517
+ // src/tui/components/TabBar.tsx
2518
+ import { jsx as jsx8, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
2519
+ function TabBar({ tab }) {
2520
+ return /* @__PURE__ */ jsx8("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginTop: 1, columnGap: 1, children: TABS.map((label, i) => /* @__PURE__ */ jsx8(
2521
+ "text",
2522
+ {
2523
+ wrapMode: "none",
2524
+ fg: i === tab ? theme.accent : theme.muted,
2525
+ bg: i === tab ? theme.selectedBg : void 0,
2526
+ children: ` ${label} `
2527
+ },
2528
+ label
2529
+ )) });
2530
+ }
2531
+ var SUB_TABS = [
2532
+ { key: "open", label: "Open" },
2533
+ { key: "merged", label: "Merged & closed" }
2534
+ ];
2535
+ function SubTabBar({ active }) {
2536
+ return /* @__PURE__ */ jsxs7("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginBottom: 1, columnGap: 1, children: [
2537
+ SUB_TABS.map(({ key, label }) => /* @__PURE__ */ jsx8(
2538
+ "text",
2539
+ {
2540
+ wrapMode: "none",
2541
+ fg: key === active ? theme.accent : theme.muted,
2542
+ bg: key === active ? theme.selectedBg : void 0,
2543
+ children: ` ${label} `
2544
+ },
2545
+ key
2546
+ )),
2547
+ /* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t switches" })
2548
+ ] });
2519
2549
  }
2520
- function parseTarget(input) {
2521
- const match = /^(\d+(?:\.\d+)?)([hdm]?)$/.exec(input);
2522
- if (!match) {
2523
- throw new CliError(`invalid --target value "${input}", use 24h, 2d, or 90m`);
2524
- }
2525
- const amount = Number(match[1]);
2526
- if (match[2] === "d") {
2527
- return amount * timeMode.dayHours;
2528
- }
2529
- if (match[2] === "m") {
2530
- return amount / 60;
2550
+
2551
+ // src/tui/components/MainPanel.tsx
2552
+ import { jsx as jsx9, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
2553
+ function QueueTab({
2554
+ prompt,
2555
+ repos,
2556
+ scope,
2557
+ view,
2558
+ repoCursor,
2559
+ rowCursor,
2560
+ grouped,
2561
+ warning,
2562
+ onRefClick
2563
+ }) {
2564
+ if (scope.view === "list") {
2565
+ return /* @__PURE__ */ jsx9(RepoList, { prompt, options: repos, cursor: Math.min(repoCursor, repos.length - 1) });
2531
2566
  }
2532
- return amount;
2533
- }
2534
- var DEFAULT_TARGET_PERCENTILE = 90;
2535
- function parseTargetPercentile(input) {
2536
- const match = /^p?(\d{1,3})$/i.exec(input);
2537
- const value2 = match === null ? Number.NaN : Number(match[1]);
2538
- if (!Number.isInteger(value2) || value2 < 1 || value2 > 100) {
2539
- throw new CliError(`invalid --target-percentile value "${input}", use a percentile from 1 to 100 like 90 or p90`);
2567
+ if (view === null) {
2568
+ return null;
2540
2569
  }
2541
- return value2;
2542
- }
2543
- function parseSizeTarget(input) {
2544
- const target = {};
2545
- for (const part of input.split(",")) {
2546
- const match = /^(\d+)([lf]?)$/.exec(part.trim());
2547
- if (!match) {
2548
- throw new CliError(`invalid --size-target value "${input}", use 400, 400l, 20f, or 400l,20f`);
2549
- }
2550
- const key = match[2] === "f" ? "files" : "lines";
2551
- if (target[key] !== void 0) {
2552
- throw new CliError(`--size-target sets the ${key} budget twice`);
2570
+ return /* @__PURE__ */ jsx9(
2571
+ QueuePanel,
2572
+ {
2573
+ heading: repos.length > 0 ? scope.repo ?? (grouped ? "All repos \xB7 grouped by repo" : "All repos") : null,
2574
+ warning,
2575
+ empty: view.empty,
2576
+ sections: view.sections,
2577
+ cursor: Math.min(rowCursor, queueRows(view).length - 1),
2578
+ onRefClick
2553
2579
  }
2554
- target[key] = Number(match[1]);
2555
- }
2556
- return target;
2580
+ );
2557
2581
  }
2558
- var REVIEW_TYPES = /* @__PURE__ */ new Map([
2559
- ["approve", "APPROVED"],
2560
- ["comment", "COMMENTED"],
2561
- ["request-changes", "CHANGES_REQUESTED"]
2562
- ]);
2563
- function parseReviewTypes(input) {
2564
- const states = /* @__PURE__ */ new Set();
2565
- for (const part of input.split(",")) {
2566
- const state = REVIEW_TYPES.get(part.trim().toLowerCase());
2567
- if (state === void 0) {
2568
- throw new CliError(`invalid --review-types value "${part.trim()}", use approve, comment, or request-changes`);
2569
- }
2570
- states.add(state);
2582
+ function StatsTab({
2583
+ repos,
2584
+ scope,
2585
+ view,
2586
+ cursor,
2587
+ scrollRef,
2588
+ focused,
2589
+ warning
2590
+ }) {
2591
+ if (scope.view === "list") {
2592
+ return /* @__PURE__ */ jsx9(RepoList, { options: repos, cursor: Math.min(cursor, repos.length - 1) });
2571
2593
  }
2572
- return states;
2573
- }
2574
- function toMinutesOfDay(hourText, minuteText, meridiem) {
2575
- const minute = Number(minuteText ?? 0);
2576
- if (minute > 59) {
2594
+ if (view === null) {
2577
2595
  return null;
2578
2596
  }
2579
- let hour = Number(hourText);
2580
- if (meridiem !== void 0) {
2581
- if (hour < 1 || hour > 12) {
2582
- return null;
2583
- }
2584
- hour %= 12;
2585
- if (meridiem.toLowerCase() === "pm") {
2586
- hour += 12;
2597
+ return /* @__PURE__ */ jsx9(
2598
+ ChartsPanel,
2599
+ {
2600
+ scrollRef,
2601
+ focused,
2602
+ heading: repos.length > 0 ? scope.repo ?? "All repos" : null,
2603
+ warning,
2604
+ view
2587
2605
  }
2588
- }
2589
- return hour * 60 + minute;
2606
+ );
2590
2607
  }
2591
- function parseWorkHours(input) {
2592
- const windows = input.split(",").map((range) => {
2593
- const match = /^(\d{1,2})(?::(\d{2}))?(am|pm)?-(\d{1,2})(?::(\d{2}))?(am|pm)?$/i.exec(range.trim());
2594
- if (!match) {
2595
- throw new CliError(
2596
- `invalid --work-hours range "${range}", use ranges like 9-17, 9am-6pm, 8:30-16:30, or 9-18,19:30-20:30`
2597
- );
2608
+ function MainPanel({
2609
+ views,
2610
+ browse,
2611
+ warning,
2612
+ focused,
2613
+ scrollRefs,
2614
+ error,
2615
+ loading,
2616
+ load,
2617
+ onRefClick
2618
+ }) {
2619
+ return /* @__PURE__ */ jsx9("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx9(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */ jsx9(
2620
+ QueueTab,
2621
+ {
2622
+ prompt: "Select a repository and press enter to open its review queue.",
2623
+ repos: views.pendingRepos,
2624
+ scope: views.pendingScope,
2625
+ view: views.pending,
2626
+ repoCursor: browse.repoCursors.pending,
2627
+ rowCursor: browse.rowCursors.pending,
2628
+ grouped: browse.grouped.pending,
2629
+ warning,
2630
+ onRefClick
2598
2631
  }
2599
- const startMin = toMinutesOfDay(match[1], match[2], match[3]);
2600
- const end = toMinutesOfDay(match[4], match[5], match[6]);
2601
- const endMin = end === 0 ? 24 * 60 : end;
2602
- if (startMin === null || endMin === null || endMin <= startMin || endMin > 24 * 60) {
2603
- throw new CliError(`invalid --work-hours range "${range}"`);
2632
+ ) : browse.tab === 1 ? /* @__PURE__ */ jsxs8("box", { flexGrow: 1, flexDirection: "column", children: [
2633
+ /* @__PURE__ */ jsx9(SubTabBar, { active: browse.authoredTab }),
2634
+ browse.authoredTab === "open" ? /* @__PURE__ */ jsx9(
2635
+ QueueTab,
2636
+ {
2637
+ prompt: "Select a repository and press enter to list its open PRs.",
2638
+ repos: views.openRepos,
2639
+ scope: views.openScope,
2640
+ view: views.open,
2641
+ repoCursor: browse.repoCursors.open,
2642
+ rowCursor: browse.rowCursors.open,
2643
+ grouped: browse.grouped.open,
2644
+ warning,
2645
+ onRefClick
2646
+ }
2647
+ ) : /* @__PURE__ */ jsx9(
2648
+ StatsTab,
2649
+ {
2650
+ repos: views.mergedRepos,
2651
+ scope: views.mergedScope,
2652
+ view: views.merged,
2653
+ cursor: browse.repoCursors.merged,
2654
+ scrollRef: scrollRefs.merged,
2655
+ focused,
2656
+ warning
2657
+ }
2658
+ )
2659
+ ] }) : browse.tab === 2 ? /* @__PURE__ */ jsx9(
2660
+ StatsTab,
2661
+ {
2662
+ repos: views.reviewRepos,
2663
+ scope: views.reviewScope,
2664
+ view: views.review,
2665
+ cursor: browse.repoCursors.review,
2666
+ scrollRef: scrollRefs.review,
2667
+ focused,
2668
+ warning
2604
2669
  }
2605
- return { startMin, endMin };
2606
- });
2607
- windows.sort((a, b) => a.startMin - b.startMin);
2608
- for (let i = 1; i < windows.length; i++) {
2609
- if (windows[i].startMin < windows[i - 1].endMin) {
2610
- throw new CliError(`--work-hours ranges overlap around ${formatMinutesOfDay(windows[i].startMin)}`);
2670
+ ) : browse.tab === 3 ? /* @__PURE__ */ jsx9(
2671
+ StatsTab,
2672
+ {
2673
+ repos: views.sizeRepos,
2674
+ scope: views.sizeScope,
2675
+ view: views.size,
2676
+ cursor: browse.repoCursors.size,
2677
+ scrollRef: scrollRefs.size,
2678
+ focused,
2679
+ warning
2611
2680
  }
2612
- }
2613
- return windows;
2614
- }
2615
- function resolveTimezone(input) {
2616
- const tz = input ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
2617
- try {
2618
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
2619
- } catch {
2620
- throw new CliError(`invalid --tz value "${tz}", use an IANA zone like Europe/Berlin`);
2621
- }
2622
- return tz;
2681
+ ) : /* @__PURE__ */ jsx9(
2682
+ StatsTab,
2683
+ {
2684
+ repos: views.commentRepos,
2685
+ scope: views.commentScope,
2686
+ view: views.comments,
2687
+ cursor: browse.repoCursors.comment,
2688
+ scrollRef: scrollRefs.comment,
2689
+ focused,
2690
+ warning
2691
+ }
2692
+ ) });
2623
2693
  }
2624
2694
 
2625
2695
  // src/tui/state/options.ts
@@ -2680,6 +2750,13 @@ var FIELDS = [
2680
2750
  kind: "text",
2681
2751
  fetch: false
2682
2752
  },
2753
+ {
2754
+ key: "workDays",
2755
+ label: "Work days",
2756
+ hint: "enter opens the day list, checked days count as working days",
2757
+ kind: "multi",
2758
+ fetch: false
2759
+ },
2683
2760
  {
2684
2761
  key: "workHours",
2685
2762
  label: "Work hours",
@@ -2703,6 +2780,23 @@ var FIELDS = [
2703
2780
  }
2704
2781
  ];
2705
2782
  var REVIEW_TYPE_CHOICES = ["approve", "comment", "request-changes"];
2783
+ var WORK_DAY_CHOICES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
2784
+ function checkedWorkDays(value2) {
2785
+ return new Set([...parseWorkDays(value2)].map((day) => WORK_DAY_CHOICES[(day + 6) % 7]));
2786
+ }
2787
+ function toggleWorkDay(value2, day) {
2788
+ const days = parseWorkDays(value2);
2789
+ const number = (WORK_DAY_CHOICES.indexOf(day) + 1) % 7;
2790
+ if (days.has(number)) {
2791
+ days.delete(number);
2792
+ } else {
2793
+ days.add(number);
2794
+ }
2795
+ if (days.size === 0) {
2796
+ return value2;
2797
+ }
2798
+ return formatWorkDays(days);
2799
+ }
2706
2800
  function checkedReviewTypes(value2) {
2707
2801
  if (value2.trim() === "") {
2708
2802
  return new Set(REVIEW_TYPE_CHOICES);
@@ -2723,7 +2817,7 @@ function toggleReviewType(value2, type) {
2723
2817
  return next.length === REVIEW_TYPE_CHOICES.length ? "" : next.join(",");
2724
2818
  }
2725
2819
  function validateField(key, value2) {
2726
- if (value2 === "" && key !== "workHours" && key !== "since") {
2820
+ if (value2 === "" && key !== "workHours" && key !== "workDays" && key !== "since") {
2727
2821
  return;
2728
2822
  }
2729
2823
  switch (key) {
@@ -2743,6 +2837,10 @@ function validateField(key, value2) {
2743
2837
  parseSizeTarget(value2);
2744
2838
  break;
2745
2839
  }
2840
+ case "workDays": {
2841
+ parseWorkDays(value2);
2842
+ break;
2843
+ }
2746
2844
  case "workHours": {
2747
2845
  parseWorkHours(value2);
2748
2846
  break;
@@ -2768,6 +2866,7 @@ function readSavedOptions() {
2768
2866
  const record = value2;
2769
2867
  record.reviewTypes ??= "";
2770
2868
  record.targetPercentile ??= "";
2869
+ record.workDays ??= "Mon-Fri";
2771
2870
  const options = {};
2772
2871
  for (const field of FIELDS) {
2773
2872
  const raw = record[field.key];
@@ -2817,6 +2916,9 @@ function applySavedOptions(values, explicit) {
2817
2916
  if (!explicit.has("size-target") && saved2.sizeTarget !== "") {
2818
2917
  values["size-target"] = saved2.sizeTarget;
2819
2918
  }
2919
+ if (!explicit.has("work-days")) {
2920
+ values["work-days"] = saved2.workDays;
2921
+ }
2820
2922
  if (!explicit.has("work-hours")) {
2821
2923
  values["work-hours"] = saved2.workHours;
2822
2924
  }
@@ -2927,7 +3029,8 @@ function OptionsModal({
2927
3029
  fieldError,
2928
3030
  onDraft,
2929
3031
  onSubmit,
2930
- onToggleReviewType
3032
+ onToggleReviewType,
3033
+ onToggleWorkDay
2931
3034
  }) {
2932
3035
  const savedState = savedLine(options, saved2);
2933
3036
  return /* @__PURE__ */ jsxs10(ModalFrame, { title: "Options", children: [
@@ -2944,7 +3047,8 @@ function OptionsModal({
2944
3047
  isEditing: index === selected && editing && field.kind !== "toggle",
2945
3048
  onDraft,
2946
3049
  onSubmit,
2947
- onToggleType: onToggleReviewType
3050
+ onToggleType: onToggleReviewType,
3051
+ onToggleWorkDay
2948
3052
  },
2949
3053
  field.key
2950
3054
  );
@@ -2989,14 +3093,29 @@ function FieldRow({
2989
3093
  isEditing,
2990
3094
  onDraft,
2991
3095
  onSubmit,
2992
- onToggleType
3096
+ onToggleType,
3097
+ onToggleWorkDay
2993
3098
  }) {
2994
3099
  const value2 = displayValue(field.key, options[field.key]);
2995
3100
  const valueColor = value2.isPlaceholder ? isSelected ? theme.muted : theme.dim : theme.muted;
2996
3101
  if (isEditing && field.kind === "multi") {
2997
3102
  return /* @__PURE__ */ jsxs10("box", { flexDirection: "column", children: [
2998
3103
  /* @__PURE__ */ jsx11(ModalRow, { label: field.label, isSelected, children: /* @__PURE__ */ jsx11("text", { wrapMode: "none", children: /* @__PURE__ */ jsx11("b", { fg: value2.isPlaceholder ? theme.muted : theme.text, children: value2.text }) }) }),
2999
- /* @__PURE__ */ jsx11(TypeChecklist, { value: String(options[field.key]), onToggle: onToggleType })
3104
+ field.key === "reviewTypes" ? /* @__PURE__ */ jsx11(
3105
+ Checklist,
3106
+ {
3107
+ choices: REVIEW_TYPE_CHOICES,
3108
+ checked: checkedReviewTypes(options[field.key]),
3109
+ onToggle: onToggleType
3110
+ }
3111
+ ) : /* @__PURE__ */ jsx11(
3112
+ Checklist,
3113
+ {
3114
+ choices: WORK_DAY_CHOICES,
3115
+ checked: checkedWorkDays(String(options[field.key])),
3116
+ onToggle: onToggleWorkDay
3117
+ }
3118
+ )
3000
3119
  ] });
3001
3120
  }
3002
3121
  return /* @__PURE__ */ jsx11(ModalRow, { label: field.label, isSelected, children: isEditing ? /* @__PURE__ */ jsx11(
@@ -3022,15 +3141,18 @@ function FieldRow({
3022
3141
  /* @__PURE__ */ jsx11("span", { fg: theme.muted, children: " \u203A" })
3023
3142
  ] }) : isSelected ? /* @__PURE__ */ jsx11("text", { wrapMode: "none", children: /* @__PURE__ */ jsx11("b", { fg: value2.isPlaceholder ? theme.muted : theme.text, children: value2.text }) }) : /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: valueColor, children: value2.text }) });
3024
3143
  }
3025
- function TypeChecklist({ value: value2, onToggle }) {
3026
- const checked = checkedReviewTypes(value2);
3027
- return /* @__PURE__ */ jsx11("box", { alignSelf: "flex-end", width: 30, height: REVIEW_TYPE_CHOICES.length, marginRight: 2, children: /* @__PURE__ */ jsx11(
3144
+ function Checklist({
3145
+ choices,
3146
+ checked,
3147
+ onToggle
3148
+ }) {
3149
+ return /* @__PURE__ */ jsx11("box", { alignSelf: "flex-end", width: 30, height: choices.length, marginRight: 2, children: /* @__PURE__ */ jsx11(
3028
3150
  "select",
3029
3151
  {
3030
3152
  focused: true,
3031
3153
  width: "100%",
3032
- height: REVIEW_TYPE_CHOICES.length,
3033
- options: REVIEW_TYPE_CHOICES.map((choice) => {
3154
+ height: choices.length,
3155
+ options: choices.map((choice) => {
3034
3156
  return { name: `[${checked.has(choice) ? "x" : " "}] ${choice}`, description: "", value: choice };
3035
3157
  }),
3036
3158
  showDescription: false,
@@ -3711,6 +3833,7 @@ function buildStatsReport(raw, options) {
3711
3833
  configureTimeMode({
3712
3834
  business: !options.wallClock,
3713
3835
  workWindows: parseWorkHours(options.workHours),
3836
+ workDays: parseWorkDays(options.workDays),
3714
3837
  tz: timezone
3715
3838
  });
3716
3839
  const targetHours = options.target === "" ? void 0 : parseTarget(options.target);
@@ -3745,6 +3868,7 @@ function buildStatsReport(raw, options) {
3745
3868
  repos: raw.repos,
3746
3869
  searchCapped: raw.searchCapped,
3747
3870
  options: {
3871
+ workDays: options.workDays,
3748
3872
  workHours: options.workHours,
3749
3873
  timezone,
3750
3874
  wallClock: options.wallClock,
@@ -4134,7 +4258,8 @@ function Modals({
4134
4258
  onDraft,
4135
4259
  onSubmitField,
4136
4260
  onSubmitThemeColor,
4137
- onToggleReviewType
4261
+ onToggleReviewType,
4262
+ onToggleWorkDay
4138
4263
  }) {
4139
4264
  if (ui.modal === "options") {
4140
4265
  return /* @__PURE__ */ jsx14(
@@ -4147,7 +4272,8 @@ function Modals({
4147
4272
  fieldError: ui.fieldError,
4148
4273
  onDraft,
4149
4274
  onSubmit: onSubmitField,
4150
- onToggleReviewType
4275
+ onToggleReviewType,
4276
+ onToggleWorkDay
4151
4277
  }
4152
4278
  );
4153
4279
  }
@@ -4744,7 +4870,7 @@ function buildGaugeCard({ title, subtitle, rows }) {
4744
4870
  }
4745
4871
 
4746
4872
  // src/tui/views/charts/heatmap.ts
4747
- var WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
4873
+ var WEEKDAY_LABELS2 = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
4748
4874
  function heatColor(count2) {
4749
4875
  if (count2 === 1) {
4750
4876
  return theme.heat[0];
@@ -4755,13 +4881,13 @@ function heatColor(count2) {
4755
4881
  return count2 <= 4 ? theme.heat[2] : theme.heat[3];
4756
4882
  }
4757
4883
  function buildHeatmapCard({ title, subtitle, grid, columns, legend }) {
4758
- const cells = WEEKDAY_LABELS.map(() => Array.from({ length: 24 }, () => 0));
4884
+ const cells = WEEKDAY_LABELS2.map(() => Array.from({ length: 24 }, () => 0));
4759
4885
  for (const date of grid) {
4760
4886
  const { weekday, hour } = zonedStamp(date);
4761
4887
  cells[(weekday + 6) % 7][hour] += 1;
4762
4888
  }
4763
4889
  const counts = columns.map((column) => {
4764
- const byDay = WEEKDAY_LABELS.map(() => 0);
4890
+ const byDay = WEEKDAY_LABELS2.map(() => 0);
4765
4891
  for (const date of column.dates) {
4766
4892
  byDay[(zonedStamp(date).weekday + 6) % 7] += 1;
4767
4893
  }
@@ -4776,7 +4902,7 @@ function buildHeatmapCard({ title, subtitle, grid, columns, legend }) {
4776
4902
  { text: columns.map((column, i) => column.label.padStart(columnWidths[i])).join(""), fg: theme.dim }
4777
4903
  ];
4778
4904
  const lines = [header];
4779
- for (const [day, label] of WEEKDAY_LABELS.entries()) {
4905
+ for (const [day, label] of WEEKDAY_LABELS2.entries()) {
4780
4906
  const line = [{ text: `${label} `, fg: theme.muted }];
4781
4907
  for (let hour = 0; hour < 24; hour++) {
4782
4908
  const count2 = cells[day][hour];
@@ -5801,6 +5927,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
5801
5927
  configureTimeMode({
5802
5928
  business: !options.wallClock,
5803
5929
  workWindows: parseWorkHours(options.workHours),
5930
+ workDays: parseWorkDays(options.workDays),
5804
5931
  tz: resolveTimezone(options.tz === "" ? void 0 : options.tz)
5805
5932
  });
5806
5933
  initBuckets();
@@ -5849,6 +5976,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
5849
5976
  };
5850
5977
  }, [
5851
5978
  raw,
5979
+ options.workDays,
5852
5980
  options.workHours,
5853
5981
  options.tz,
5854
5982
  options.wallClock,
@@ -6230,7 +6358,7 @@ var initialUiState = {
6230
6358
  themeColorError: null,
6231
6359
  cacheAction: null,
6232
6360
  openError: null,
6233
- copyNotice: null
6361
+ successNotice: null
6234
6362
  };
6235
6363
  function cycled2(previous, delta, count2) {
6236
6364
  return (previous + count2 + delta) % count2;
@@ -6238,19 +6366,19 @@ function cycled2(previous, delta, count2) {
6238
6366
  function uiReducer(state, action) {
6239
6367
  switch (action.type) {
6240
6368
  case "noticesDismissed": {
6241
- if (state.openError === null && state.copyNotice === null) {
6369
+ if (state.openError === null && state.successNotice === null) {
6242
6370
  return state;
6243
6371
  }
6244
- return { ...state, openError: null, copyNotice: null };
6372
+ return { ...state, openError: null, successNotice: null };
6245
6373
  }
6246
6374
  case "openErrorReported": {
6247
6375
  return { ...state, openError: action.message };
6248
6376
  }
6249
6377
  case "copyReported": {
6250
- return { ...state, openError: null, copyNotice: { text: action.message } };
6378
+ return { ...state, openError: null, successNotice: { text: action.message } };
6251
6379
  }
6252
- case "copyNoticeExpired": {
6253
- return { ...state, copyNotice: null };
6380
+ case "successNoticeExpired": {
6381
+ return { ...state, successNotice: null };
6254
6382
  }
6255
6383
  case "modalOpened": {
6256
6384
  return { ...state, modal: action.modal, fieldError: null, themeColorError: null, cacheAction: null };
@@ -6298,10 +6426,10 @@ function uiReducer(state, action) {
6298
6426
  return { ...state, fieldError: action.message };
6299
6427
  }
6300
6428
  case "optionsSaveReported": {
6301
- return {
6302
- ...state,
6303
- fieldError: action.saved ? null : "the cache is disabled for this session \xB7 options not saved"
6304
- };
6429
+ if (action.saved) {
6430
+ return { ...state, fieldError: null, successNotice: { text: "options saved" } };
6431
+ }
6432
+ return { ...state, fieldError: "the cache is disabled for this session \xB7 options not saved" };
6305
6433
  }
6306
6434
  case "themeColorCommitted": {
6307
6435
  return { ...state, editing: false, themeColorError: null, cacheAction: action.action };
@@ -6404,16 +6532,16 @@ function App({
6404
6532
  dispatchUi({ type: "copyReported", message: `copied ${row.ref} to the clipboard` });
6405
6533
  };
6406
6534
  useEffect6(() => {
6407
- if (ui.copyNotice === null) {
6535
+ if (ui.successNotice === null) {
6408
6536
  return void 0;
6409
6537
  }
6410
6538
  const timer = setTimeout(() => {
6411
- dispatchUi({ type: "copyNoticeExpired" });
6539
+ dispatchUi({ type: "successNoticeExpired" });
6412
6540
  }, 2500);
6413
6541
  return () => {
6414
6542
  clearTimeout(timer);
6415
6543
  };
6416
- }, [ui.copyNotice]);
6544
+ }, [ui.successNotice]);
6417
6545
  const reviewScrollRef = useRef5(null);
6418
6546
  const sizeScrollRef = useRef5(null);
6419
6547
  const commentScrollRef = useRef5(null);
@@ -6530,7 +6658,7 @@ function App({
6530
6658
  views,
6531
6659
  copyLinks: copyLinks2,
6532
6660
  openError: ui.openError,
6533
- copyNotice: ui.copyNotice === null ? null : ui.copyNotice.text,
6661
+ successNotice: ui.successNotice === null ? null : ui.successNotice.text,
6534
6662
  stale
6535
6663
  }
6536
6664
  ),
@@ -6552,6 +6680,11 @@ function App({
6552
6680
  setOptions((previous) => {
6553
6681
  return { ...previous, reviewTypes: toggleReviewType(previous.reviewTypes, type) };
6554
6682
  });
6683
+ },
6684
+ onToggleWorkDay: (day) => {
6685
+ setOptions((previous) => {
6686
+ return { ...previous, workDays: toggleWorkDay(previous.workDays, day) };
6687
+ });
6555
6688
  }
6556
6689
  }
6557
6690
  )
@@ -6578,6 +6711,7 @@ function bootstrap() {
6578
6711
  target: values.target ?? "",
6579
6712
  targetPercentile: values["target-percentile"] ?? "",
6580
6713
  sizeTarget: values["size-target"] ?? "",
6714
+ workDays: canonicalWorkDays(values["work-days"]),
6581
6715
  workHours: values["work-hours"],
6582
6716
  tz: values.tz ?? "",
6583
6717
  wallClock: values["wall-clock"],