@d3lm/pr-stats 0.2.19 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui-app.mjs CHANGED
@@ -136,11 +136,11 @@ import { createRoot } from "@opentui/react";
136
136
 
137
137
  // src/tui/App.tsx
138
138
  import { useKeyboard, useRenderer as useRenderer3, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
139
- import { useEffect as useEffect7, useMemo as useMemo2, useReducer, useRef as useRef6, useState as useState4 } from "react";
139
+ import { useEffect as useEffect8, useMemo as useMemo2, useReducer, useRef as useRef6, useState as useState5 } from "react";
140
140
 
141
141
  // src/settings.ts
142
- import { readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
143
- import { join as join2 } from "node:path";
142
+ import { readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
143
+ import { join as join3 } from "node:path";
144
144
 
145
145
  // src/cache.ts
146
146
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -263,6 +263,10 @@ var PrCache = class {
263
263
  }
264
264
  };
265
265
 
266
+ // src/snooze.ts
267
+ import { readFileSync as readFileSync2 } from "node:fs";
268
+ import { join as join2 } from "node:path";
269
+
266
270
  // src/utils.ts
267
271
  var CliError = class extends Error {
268
272
  };
@@ -278,6 +282,135 @@ function percentile(sorted, percent) {
278
282
  return sorted[Math.max(0, index)];
279
283
  }
280
284
 
285
+ // src/snooze.ts
286
+ var DEFAULT_SNOOZE_DURATION = "30m";
287
+ var MINUTE_MS = 6e4;
288
+ var HOUR_MS = 60 * MINUTE_MS;
289
+ var DAY_MS = 24 * HOUR_MS;
290
+ var WEEK_MS = 7 * DAY_MS;
291
+ var MAX_SNOOZE_MS = 4 * WEEK_MS;
292
+ var UNIT_MS = { m: MINUTE_MS, h: HOUR_MS, d: DAY_MS, w: WEEK_MS };
293
+ function snoozeDurationMs(input) {
294
+ const match = /^(\d+)([mhdw])$/.exec(input);
295
+ if (match === null) {
296
+ return null;
297
+ }
298
+ const ms = Number(match[1]) * UNIT_MS[match[2]];
299
+ return ms >= MINUTE_MS && ms <= MAX_SNOOZE_MS ? ms : null;
300
+ }
301
+ function parseSnoozeDuration(input) {
302
+ const ms = snoozeDurationMs(input);
303
+ if (ms === null) {
304
+ throw new CliError(`invalid snooze duration "${input}", use a value from 1m to 4w like 30m, 2h, or 1d`);
305
+ }
306
+ return ms;
307
+ }
308
+ function formatWakeTime(until, now = Date.now()) {
309
+ const wake = new Date(until);
310
+ const today = new Date(now);
311
+ const time = wake.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
312
+ const sameDay = wake.getFullYear() === today.getFullYear() && wake.getMonth() === today.getMonth() && wake.getDate() === today.getDate();
313
+ return sameDay ? time : `${wake.toLocaleDateString(void 0, { month: "short", day: "numeric" })} ${time}`;
314
+ }
315
+ function snoozesFile() {
316
+ return join2(cacheDir(), "snoozes.json");
317
+ }
318
+ function reviveSnooze(ref, stored) {
319
+ if (typeof stored !== "object" || stored === null) {
320
+ return null;
321
+ }
322
+ const { until, requestedAt } = stored;
323
+ if (typeof until !== "string" || typeof requestedAt !== "string") {
324
+ return null;
325
+ }
326
+ const untilMs = Date.parse(until);
327
+ const requestedAtMs = Date.parse(requestedAt);
328
+ if (Number.isNaN(untilMs) || Number.isNaN(requestedAtMs)) {
329
+ return null;
330
+ }
331
+ return { ref, until: untilMs, requestedAt: requestedAtMs };
332
+ }
333
+ function readSnoozes() {
334
+ if (!cacheEnabled()) {
335
+ return [];
336
+ }
337
+ let parsed;
338
+ try {
339
+ parsed = JSON.parse(readFileSync2(snoozesFile(), "utf8"));
340
+ } catch {
341
+ return [];
342
+ }
343
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
344
+ return [];
345
+ }
346
+ const snoozes2 = [];
347
+ for (const [ref, stored] of Object.entries(parsed)) {
348
+ const snooze = reviveSnooze(ref, stored);
349
+ if (snooze !== null) {
350
+ snoozes2.push(snooze);
351
+ }
352
+ }
353
+ return snoozes2.toSorted((a, b) => a.until - b.until);
354
+ }
355
+ function writeSnoozes(snoozes2) {
356
+ if (!cacheEnabled()) {
357
+ return false;
358
+ }
359
+ const stored = {};
360
+ for (const snooze of snoozes2) {
361
+ stored[snooze.ref] = {
362
+ until: new Date(snooze.until).toISOString(),
363
+ requestedAt: new Date(snooze.requestedAt).toISOString()
364
+ };
365
+ }
366
+ writeFileAtomic(snoozesFile(), `${JSON.stringify(stored, null, 2)}
367
+ `);
368
+ return true;
369
+ }
370
+ function activeSnooze(snoozes2, ref, requestedAt, now) {
371
+ return snoozes2.find(
372
+ (snooze) => snooze.ref === ref && snooze.until > now && requestedAt.getTime() <= snooze.requestedAt
373
+ );
374
+ }
375
+ function splitSnoozed(entries, snoozes2, now) {
376
+ const awaiting = [];
377
+ const snoozed = [];
378
+ for (const entry of entries) {
379
+ const snooze = activeSnooze(snoozes2, prKey(entry.pr.repo, entry.pr.number), entry.requestedAt, now);
380
+ if (snooze === void 0) {
381
+ awaiting.push(entry);
382
+ } else {
383
+ snoozed.push({ ...entry, until: snooze.until });
384
+ }
385
+ }
386
+ snoozed.sort((a, b) => a.until - b.until);
387
+ return { awaiting, snoozed };
388
+ }
389
+ function nextWakeUp(snoozes2) {
390
+ if (snoozes2.length === 0) {
391
+ return null;
392
+ }
393
+ return Math.min(...snoozes2.map((snooze) => snooze.until));
394
+ }
395
+ function dueSnoozes(snoozes2, now) {
396
+ return snoozes2.filter((snooze) => snooze.until <= now);
397
+ }
398
+ function wokenPrs(due, results) {
399
+ const pending = /* @__PURE__ */ new Map();
400
+ for (const result of results) {
401
+ if (result.kind === "pending" && result.pr.state === "open") {
402
+ pending.set(prKey(result.pr.repo, result.pr.number), {
403
+ pr: result.pr,
404
+ requestedAt: result.requestedAt.getTime()
405
+ });
406
+ }
407
+ }
408
+ return due.flatMap((snooze) => {
409
+ const entry = pending.get(snooze.ref);
410
+ return entry !== void 0 && entry.requestedAt <= snooze.requestedAt ? [entry.pr] : [];
411
+ });
412
+ }
413
+
281
414
  // src/settings.ts
282
415
  var NOTIFY_CHANNELS = ["auto", "terminal", "command", "bell"];
283
416
  var DEFAULT_RELOAD_INTERVAL = "10m";
@@ -298,7 +431,7 @@ function parseReloadInterval(input) {
298
431
  return ms;
299
432
  }
300
433
  function settingsFile() {
301
- return join2(cacheDir(), "settings.json");
434
+ return join3(cacheDir(), "settings.json");
302
435
  }
303
436
  var current = {};
304
437
  function writeCurrent() {
@@ -316,7 +449,7 @@ function loadSettings() {
316
449
  }
317
450
  let text;
318
451
  try {
319
- text = readFileSync2(settingsFile(), "utf8");
452
+ text = readFileSync3(settingsFile(), "utf8");
320
453
  } catch {
321
454
  return current;
322
455
  }
@@ -350,6 +483,11 @@ function loadSettings() {
350
483
  if (settings.notifyChannel !== void 0 && !NOTIFY_CHANNELS.includes(settings.notifyChannel)) {
351
484
  throw new CliError(`"notifyChannel" in ${settingsFile()} must be "auto", "terminal", "command", or "bell"`);
352
485
  }
486
+ if (settings.snoozeDuration !== void 0 && (typeof settings.snoozeDuration !== "string" || snoozeDurationMs(settings.snoozeDuration) === null)) {
487
+ throw new CliError(
488
+ `"snoozeDuration" in ${settingsFile()} must be a duration from 1m to 4w like "30m", "2h", or "1d"`
489
+ );
490
+ }
353
491
  current = settings;
354
492
  return current;
355
493
  }
@@ -377,6 +515,10 @@ function saveNotifyChannel(channel) {
377
515
  current = { ...current, notifyChannel: channel };
378
516
  return writeCurrent();
379
517
  }
518
+ function saveSnoozeDuration(value2) {
519
+ current = { ...current, snoozeDuration: value2 };
520
+ return writeCurrent();
521
+ }
380
522
  function saveTheme(value2) {
381
523
  current = { ...current };
382
524
  if (value2 === void 0) {
@@ -588,120 +730,601 @@ function themeColorText(key) {
588
730
  return Array.isArray(value2) ? value2.join(" ") : value2;
589
731
  }
590
732
 
591
- // src/tui/components/Footer.tsx
592
- import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
593
- function Footer({
594
- width,
595
- modal,
596
- editing,
597
- tab,
598
- authoredTab,
599
- views,
600
- copyLinks: copyLinks2,
601
- openError,
602
- successNotice,
603
- stale
733
+ // src/time.ts
734
+ function wallFormatter(tz) {
735
+ return new Intl.DateTimeFormat("en-US", {
736
+ timeZone: tz,
737
+ hourCycle: "h23",
738
+ year: "numeric",
739
+ month: "2-digit",
740
+ day: "2-digit",
741
+ hour: "2-digit",
742
+ minute: "2-digit",
743
+ second: "2-digit"
744
+ });
745
+ }
746
+ var timeMode = {
747
+ business: true,
748
+ workWindows: [{ startMin: 0, endMin: 24 * 60 }],
749
+ workDays: /* @__PURE__ */ new Set([1, 2, 3, 4, 5]),
750
+ dayHours: 24,
751
+ formatter: wallFormatter("UTC")
752
+ };
753
+ function configureTimeMode({
754
+ business,
755
+ workWindows,
756
+ workDays,
757
+ tz
604
758
  }) {
605
- const notice = openError ?? successNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
606
- const check = openError === null && successNotice !== null;
607
- const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
608
- const hints = truncated(hintsFor(modal, editing, tab, authoredTab, views, copyLinks2), width - 2 - noticeWidth);
609
- return /* @__PURE__ */ jsxs(Fragment, { children: [
610
- /* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
611
- /* @__PURE__ */ jsxs(
612
- "box",
613
- {
614
- flexDirection: "row",
615
- height: 1,
616
- marginBottom: 1,
617
- paddingLeft: 1,
618
- paddingRight: 1,
619
- justifyContent: "space-between",
620
- children: [
621
- /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hints }),
622
- /* @__PURE__ */ jsxs("text", { wrapMode: "none", children: [
623
- check && /* @__PURE__ */ jsx("span", { fg: theme.success, children: "\u2714 " }),
624
- /* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : successNotice !== null ? theme.muted : theme.warn, children: notice })
625
- ] })
626
- ]
627
- }
628
- )
629
- ] });
759
+ const workMinutesPerDay = workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
760
+ timeMode.business = business;
761
+ timeMode.workWindows = workWindows;
762
+ timeMode.workDays = workDays;
763
+ timeMode.dayHours = business ? workMinutesPerDay / 60 : 24;
764
+ timeMode.formatter = wallFormatter(tz);
630
765
  }
631
- function truncated(text, limit2) {
632
- if (text.length <= limit2) {
633
- return text;
634
- }
635
- return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
766
+ function isFullDayMode() {
767
+ return timeMode.business && timeMode.dayHours === 24;
636
768
  }
637
- function hintsFor(modal, editing, tab, authoredTab, views, copyLinks2) {
638
- if (modal === "options") {
639
- return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit \xB7 \u2190/\u2192 toggle \xB7 s save \xB7 esc close \xB7 q quit";
769
+ function wallParts(instantMs) {
770
+ const parts = Object.fromEntries(timeMode.formatter.formatToParts(instantMs).map((part) => [part.type, part.value]));
771
+ return {
772
+ year: Number(parts.year),
773
+ month: Number(parts.month),
774
+ day: Number(parts.day),
775
+ hour: Number(parts.hour),
776
+ minute: Number(parts.minute),
777
+ second: Number(parts.second)
778
+ };
779
+ }
780
+ function zonedStamp(date) {
781
+ const parts = wallParts(date.getTime());
782
+ const dayUtcMs = Date.UTC(parts.year, parts.month - 1, parts.day);
783
+ return { dayUtcMs, weekday: new Date(dayUtcMs).getUTCDay(), hour: parts.hour, minute: parts.minute };
784
+ }
785
+ function hasWorkWindows() {
786
+ const covered = timeMode.workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
787
+ return covered < 24 * 60;
788
+ }
789
+ function classifyInstant(date) {
790
+ const { weekday, hour, minute } = zonedStamp(date);
791
+ if (!timeMode.workDays.has(weekday)) {
792
+ return "weekend";
640
793
  }
641
- if (modal === "settings") {
642
- return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter apply \xB7 \u2190/\u2192 toggle \xB7 esc close \xB7 q quit";
794
+ const minuteOfDay = hour * 60 + minute;
795
+ return timeMode.workWindows.some((window) => minuteOfDay >= window.startMin && minuteOfDay < window.endMin) ? "work" : "after";
796
+ }
797
+ function utcFromWall(wallTargetMs) {
798
+ let guess = wallTargetMs;
799
+ for (let i = 0; i < 2; i++) {
800
+ const parts = wallParts(guess);
801
+ const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
802
+ guess = wallTargetMs - (wall - guess);
643
803
  }
644
- if (modal === "theme") {
645
- return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit hex \xB7 esc back \xB7 q quit";
804
+ return guess;
805
+ }
806
+ function businessMsBetween(start, end) {
807
+ const startMs = start.getTime();
808
+ const endMs = end.getTime();
809
+ if (endMs <= startMs) {
810
+ return 0;
646
811
  }
647
- const toggle = tab === 1 ? authoredTab === "open" ? "t merged stats \xB7 " : "t open PRs \xB7 " : "";
648
- if (tab === 0 || tab === 1 && authoredTab === "open") {
649
- const scope2 = views === null ? null : tab === 0 ? views.pendingScope : views.openScope;
650
- const repos2 = views === null ? [] : tab === 0 ? views.pendingRepos : views.openRepos;
651
- if (scope2?.view === "list") {
652
- return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
653
- }
654
- const action = copyLinks2 ? "enter copy link" : "enter open";
655
- if (scope2 !== null && repos2.length > 0) {
656
- return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 ${toggle}g group by repo \xB7 esc back \xB7 o options \xB7 s settings \xB7 r reload \xB7 q quit` : `\u2191/\u2193 select \xB7 ${action} \xB7 ${toggle}esc back \xB7 1-5 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
812
+ let total = 0;
813
+ const parts = wallParts(startMs);
814
+ let localDay = Date.UTC(parts.year, parts.month - 1, parts.day);
815
+ while (utcFromWall(localDay) <= endMs) {
816
+ const weekday = new Date(localDay).getUTCDay();
817
+ if (timeMode.workDays.has(weekday)) {
818
+ for (const window of timeMode.workWindows) {
819
+ const windowStart = utcFromWall(localDay + window.startMin * 6e4);
820
+ const windowEnd = utcFromWall(localDay + window.endMin * 6e4);
821
+ const overlapStart = Math.max(windowStart, startMs);
822
+ const overlapEnd = Math.min(windowEnd, endMs);
823
+ if (overlapEnd > overlapStart) {
824
+ total += overlapEnd - overlapStart;
825
+ }
826
+ }
657
827
  }
658
- return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
659
- }
660
- const scope = views === null ? null : tab === 1 ? views.mergedScope : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
661
- const repos = views === null ? [] : tab === 1 ? views.mergedRepos : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
662
- if (scope?.view === "list") {
663
- return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
664
- }
665
- const view = views === null ? null : tab === 1 ? views.merged : tab === 2 ? views.review : tab === 3 ? views.size : views.comments;
666
- const expand = view?.expandable ? view.expanded ? "x collapse \xB7 " : "x expand \xB7 " : "";
667
- if (scope !== null && repos.length > 0) {
668
- return `${toggle}${expand}esc back \xB7 j/k scroll \xB7 1-5 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
828
+ localDay += 864e5;
669
829
  }
670
- 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`;
830
+ return total;
671
831
  }
672
-
673
- // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
674
- function stringReplaceAll(string, substring, postfix) {
675
- let index = string.indexOf(substring);
676
- if (index === -1) {
677
- return string;
832
+ function durationHours(start, end) {
833
+ if (!timeMode.business) {
834
+ return (end.getTime() - start.getTime()) / 36e5;
678
835
  }
679
- const substringLength = substring.length;
680
- let endIndex = 0;
681
- let returnValue = "";
682
- do {
683
- returnValue += string.slice(endIndex, index) + substring + postfix;
684
- endIndex = index + substringLength;
685
- index = string.indexOf(substring, endIndex);
686
- } while (index !== -1);
687
- returnValue += string.slice(endIndex);
688
- return returnValue;
689
- }
690
- function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
691
- let endIndex = 0;
692
- let returnValue = "";
693
- do {
694
- const isGotCR = string[index - 1] === "\r";
695
- returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? "\r\n" : "\n") + postfix;
696
- endIndex = index + 1;
697
- index = string.indexOf("\n", endIndex);
698
- } while (index !== -1);
699
- returnValue += string.slice(endIndex);
700
- return returnValue;
836
+ return businessMsBetween(start, end) / 36e5;
701
837
  }
702
838
 
703
- // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
704
- var ANSI_BACKGROUND_OFFSET = 10;
839
+ // src/compute.ts
840
+ function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
841
+ const reviewed = [];
842
+ const pending = [];
843
+ for (const result of results) {
844
+ if (result.kind === "reviewed") {
845
+ reviewed.push({
846
+ pr: result.pr,
847
+ requestedAt: result.requestedAt,
848
+ reviewedAt: result.reviewedAt,
849
+ hours: durationHours(result.requestedAt, result.reviewedAt),
850
+ verdict: result.verdict,
851
+ lines: result.lines
852
+ });
853
+ } else if (result.kind === "pending" && result.pr.state === "open") {
854
+ pending.push({ pr: result.pr, requestedAt: result.requestedAt, hours: durationHours(result.requestedAt, now) });
855
+ }
856
+ }
857
+ pending.sort((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
858
+ const pendingKeys = new Set(pending.map((entry) => `${entry.pr.repo}#${entry.pr.number}`));
859
+ const latestReviews = /* @__PURE__ */ new Map();
860
+ for (const result of results) {
861
+ if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
862
+ continue;
863
+ }
864
+ const key = `${result.pr.repo}#${result.pr.number}`;
865
+ if (pendingKeys.has(key)) {
866
+ continue;
867
+ }
868
+ const latest = latestReviews.get(key);
869
+ if (latest === void 0 || result.reviewedAt > latest.reviewedAt) {
870
+ latestReviews.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
871
+ }
872
+ }
873
+ const reviewing = [...latestReviews.values()].map(({ pr, reviewedAt }) => {
874
+ return { pr, reviewedAt, hours: durationHours(reviewedAt, now) };
875
+ }).toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
876
+ const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
877
+ const unrequested = results.filter((result) => result.kind === "unrequested");
878
+ const allHours = reviewed.map((result) => result.hours);
879
+ const hoursByRepo = /* @__PURE__ */ new Map();
880
+ for (const result of reviewed) {
881
+ const hours = hoursByRepo.get(result.pr.repo) ?? [];
882
+ hours.push(result.hours);
883
+ hoursByRepo.set(result.pr.repo, hours);
884
+ }
885
+ const byRepo = [...hoursByRepo.entries()].toSorted((a, b) => b[1].length - a[1].length);
886
+ const misses = targetHours === void 0 ? [] : reviewed.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
887
+ const cyclesByPr = /* @__PURE__ */ new Map();
888
+ for (const result of reviewed) {
889
+ const key = `${result.pr.repo}#${result.pr.number}`;
890
+ cyclesByPr.set(key, (cyclesByPr.get(key) ?? 0) + 1);
891
+ }
892
+ return {
893
+ reviewed,
894
+ pending,
895
+ reviewing,
896
+ expired,
897
+ unrequested,
898
+ allHours,
899
+ byRepo,
900
+ misses,
901
+ cycles: [...cyclesByPr.values()]
902
+ };
903
+ }
904
+ function computeSizeStats(sizes, { sizeTarget } = {}) {
905
+ const metrics = [
906
+ { label: "files changed", values: sizes.map((size) => size.files) },
907
+ { label: "lines added", values: sizes.map((size) => size.additions) },
908
+ { label: "lines removed", values: sizes.map((size) => size.deletions) },
909
+ { label: "lines total", values: sizes.map((size) => size.total) }
910
+ ];
911
+ const timelineTotals = [...sizes].toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime()).map((size) => size.total);
912
+ if (sizeTarget === void 0) {
913
+ return { metrics, timelineTotals, met: void 0, misses: [], targetLabel: void 0 };
914
+ }
915
+ const meetsTarget = (size) => (sizeTarget.lines === void 0 || size.total <= sizeTarget.lines) && (sizeTarget.files === void 0 || size.files <= sizeTarget.files);
916
+ const targetLabel = [
917
+ ...sizeTarget.lines === void 0 ? [] : [`<= ${sizeTarget.lines} lines`],
918
+ ...sizeTarget.files === void 0 ? [] : [`<= ${sizeTarget.files} files`]
919
+ ].join(", ");
920
+ const met = sizes.filter((size) => meetsTarget(size)).length;
921
+ const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
922
+ return { metrics, timelineTotals, met, misses, targetLabel };
923
+ }
924
+ function computeMergeStats(sizes) {
925
+ const merged = [];
926
+ const closed = [];
927
+ const open = [];
928
+ for (const entry of sizes) {
929
+ if (entry.mergedAt !== null) {
930
+ merged.push({ entry, mergedAt: entry.mergedAt, hours: durationHours(entry.pr.createdAt, entry.mergedAt) });
931
+ } else if (entry.pr.state === "open") {
932
+ open.push(entry);
933
+ } else if (entry.closedAt !== null) {
934
+ closed.push({ entry, closedAt: entry.closedAt, hours: durationHours(entry.pr.createdAt, entry.closedAt) });
935
+ }
936
+ }
937
+ merged.sort((a, b) => b.mergedAt.getTime() - a.mergedAt.getTime());
938
+ closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
939
+ return { merged, closed, open, allHours: merged.map((result) => result.hours) };
940
+ }
941
+ function computeReviewerStats(sizes, author) {
942
+ const byLogin = /* @__PURE__ */ new Map();
943
+ let mergedReviewed = 0;
944
+ let mergedUnreviewed = 0;
945
+ for (const entry of sizes) {
946
+ const others = entry.reviews.flatMap(
947
+ (review) => review.login === null || review.login === author ? [] : [review.login]
948
+ );
949
+ for (const login of others) {
950
+ const counts = byLogin.get(login) ?? { prs: 0, reviews: 0 };
951
+ counts.reviews += 1;
952
+ byLogin.set(login, counts);
953
+ }
954
+ const distinct = new Set(others);
955
+ for (const login of distinct) {
956
+ const counts = byLogin.get(login);
957
+ if (counts !== void 0) {
958
+ counts.prs += 1;
959
+ }
960
+ }
961
+ if (entry.mergedAt !== null) {
962
+ if (others.length > 0) {
963
+ mergedReviewed += 1;
964
+ } else {
965
+ mergedUnreviewed += 1;
966
+ }
967
+ }
968
+ }
969
+ const leaderboard = [...byLogin.entries()].map(([login, counts]) => {
970
+ return { login, ...counts };
971
+ }).toSorted((a, b) => b.prs - a.prs || b.reviews - a.reviews || a.login.localeCompare(b.login));
972
+ return { leaderboard, mergedReviewed, mergedUnreviewed };
973
+ }
974
+ function firstReviewOf(entry, author) {
975
+ let earliest = null;
976
+ for (const review of entry.reviews) {
977
+ if (review.login === null || review.login === author || review.submittedAt === null) {
978
+ continue;
979
+ }
980
+ if (earliest === null || review.submittedAt < earliest) {
981
+ earliest = review.submittedAt;
982
+ }
983
+ }
984
+ if (earliest === null) {
985
+ return null;
986
+ }
987
+ return { reviewedAt: earliest, hours: durationHours(entry.pr.createdAt, earliest) };
988
+ }
989
+ function computeFirstReviewStats(sizes, author, { now = /* @__PURE__ */ new Date() } = {}) {
990
+ const received = [];
991
+ const awaiting = [];
992
+ for (const entry of sizes) {
993
+ const first = firstReviewOf(entry, author);
994
+ if (first !== null) {
995
+ received.push({ entry, ...first });
996
+ } else if (entry.pr.state === "open") {
997
+ awaiting.push({ entry, hours: durationHours(entry.pr.createdAt, now) });
998
+ }
999
+ }
1000
+ awaiting.sort((a, b) => a.entry.pr.createdAt.getTime() - b.entry.pr.createdAt.getTime());
1001
+ return { received, awaiting, allHours: received.map((result) => result.hours) };
1002
+ }
1003
+ function computeCommentStats(sizes) {
1004
+ const metrics = [
1005
+ { label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
1006
+ { label: "review comments", values: sizes.map((size) => size.comments.review) },
1007
+ { label: "all comments", values: sizes.map((size) => size.comments.total) }
1008
+ ];
1009
+ const totals = sizes.map((size) => size.comments.total);
1010
+ const uncommented = totals.filter((total) => total === 0).length;
1011
+ const top = sizes.filter((size) => size.comments.total > 0).toSorted((a, b) => b.comments.total - a.comments.total);
1012
+ return { metrics, totals, uncommented, top };
1013
+ }
1014
+
1015
+ // src/report.ts
1016
+ var BUCKETS = [];
1017
+ function initBuckets() {
1018
+ BUCKETS = makeBuckets();
1019
+ }
1020
+ function currentBuckets() {
1021
+ return BUCKETS;
1022
+ }
1023
+ function makeBuckets() {
1024
+ if (!timeMode.business || isFullDayMode()) {
1025
+ return [
1026
+ { label: "< 1h", max: 1 },
1027
+ { label: "1-4h", max: 4 },
1028
+ { label: "4-8h", max: 8 },
1029
+ { label: "8-24h", max: 24 },
1030
+ { label: "1-2d", max: 48 },
1031
+ { label: "2-4d", max: 96 },
1032
+ { label: "4-7d", max: 168 },
1033
+ { label: "> 7d", max: Infinity }
1034
+ ];
1035
+ }
1036
+ const wd = timeMode.dayHours;
1037
+ return [
1038
+ { label: "< 1h", max: 1 },
1039
+ { label: "1-4h", max: 4 },
1040
+ { label: "4h-1wd", max: wd },
1041
+ { label: "1-2wd", max: 2 * wd },
1042
+ { label: "2-3wd", max: 3 * wd },
1043
+ { label: "3-5wd", max: 5 * wd },
1044
+ { label: "5-10wd", max: 10 * wd },
1045
+ { label: "> 10wd", max: Infinity }
1046
+ ];
1047
+ }
1048
+ var LINE_BUCKETS = [
1049
+ { label: "< 50", max: 50 },
1050
+ { label: "50-100", max: 100 },
1051
+ { label: "100-250", max: 250 },
1052
+ { label: "250-500", max: 500 },
1053
+ { label: "500-1k", max: 1e3 },
1054
+ { label: "1k-2.5k", max: 2500 },
1055
+ { label: "2.5k-5k", max: 5e3 },
1056
+ { label: "> 5k", max: Infinity }
1057
+ ];
1058
+ var FILE_BUCKETS = [
1059
+ { label: "1-2", max: 3 },
1060
+ { label: "3-5", max: 6 },
1061
+ { label: "6-10", max: 11 },
1062
+ { label: "11-20", max: 21 },
1063
+ { label: "21-50", max: 51 },
1064
+ { label: "> 50", max: Infinity }
1065
+ ];
1066
+ var CYCLE_BUCKETS = [
1067
+ { label: "1", max: 2 },
1068
+ { label: "2", max: 3 },
1069
+ { label: "3", max: 4 },
1070
+ { label: "4-5", max: 6 },
1071
+ { label: "> 5", max: Infinity }
1072
+ ];
1073
+ var COMMENT_BUCKETS = [
1074
+ { label: "0", max: 1 },
1075
+ { label: "1-2", max: 3 },
1076
+ { label: "3-5", max: 6 },
1077
+ { label: "6-10", max: 11 },
1078
+ { label: "11-20", max: 21 },
1079
+ { label: "21-50", max: 51 },
1080
+ { label: "> 50", max: Infinity }
1081
+ ];
1082
+ function formatHoursOnly(hours) {
1083
+ if (hours < 1) {
1084
+ return `${Math.round(hours * 60)}m`;
1085
+ }
1086
+ return `${hours.toFixed(1)}h`;
1087
+ }
1088
+ function weeksSuffix(hours) {
1089
+ const weekHours = timeMode.business ? 5 * timeMode.dayHours : 7 * 24;
1090
+ if (hours < weekHours) {
1091
+ return "";
1092
+ }
1093
+ return ` (${(hours / weekHours).toFixed(1)} weeks)`;
1094
+ }
1095
+ function formatCount(value2) {
1096
+ if (value2 < 1e3) {
1097
+ return String(value2);
1098
+ }
1099
+ const scaled = value2 / 1e3;
1100
+ return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
1101
+ }
1102
+
1103
+ // src/tui/views/rows.ts
1104
+ function durationLead(result) {
1105
+ return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
1106
+ }
1107
+ function toPrRows(entries, leads) {
1108
+ const width = Math.max(...leads.map((lead) => lead.length));
1109
+ return entries.map((entry, i) => {
1110
+ return {
1111
+ lead: leads[i].padEnd(width),
1112
+ ref: `${entry.pr.repo}#${entry.pr.number}`,
1113
+ url: entry.pr.url,
1114
+ title: entry.pr.title
1115
+ };
1116
+ });
1117
+ }
1118
+
1119
+ // src/tui/views/queue.ts
1120
+ function queueRows(view) {
1121
+ return view.sections.flatMap((section) => [...section.rows, ...section.lists.flatMap((list) => list.rows)]);
1122
+ }
1123
+ function queueRowAt(view, cursor) {
1124
+ const rows = view === null ? [] : queueRows(view);
1125
+ return rows[Math.min(cursor, rows.length - 1)];
1126
+ }
1127
+ function snoozeActionOf(row) {
1128
+ if (row?.pending === void 0) {
1129
+ return null;
1130
+ }
1131
+ return row.pending.snoozed ? "unsnooze" : "snooze";
1132
+ }
1133
+ function groupedLists(entries, rowsOf2) {
1134
+ const groups = /* @__PURE__ */ new Map();
1135
+ for (const entry of entries) {
1136
+ const group = groups.get(entry.pr.repo) ?? [];
1137
+ group.push(entry);
1138
+ groups.set(entry.pr.repo, group);
1139
+ }
1140
+ return [...groups.entries()].toSorted((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])).map(([repo, group]) => {
1141
+ return { title: `${repo} (n=${group.length})`, rows: rowsOf2(group) };
1142
+ });
1143
+ }
1144
+ function buildPendingReviewView(raw, repo = null, grouped = false, snoozes2 = [], now = Date.now()) {
1145
+ const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
1146
+ const inScope = (entries) => repo === null ? entries : entries.filter((entry) => entry.pr.repo === repo);
1147
+ const { awaiting, snoozed } = splitSnoozed(inScope(stats.pending), snoozes2, now);
1148
+ const reviewing = inScope(stats.reviewing);
1149
+ if (awaiting.length === 0 && snoozed.length === 0 && reviewing.length === 0) {
1150
+ return { empty: "No PRs are awaiting your review, and none you reviewed are still open.", sections: [] };
1151
+ }
1152
+ const split = repo === null && grouped;
1153
+ const sectionOf = (title, entries, rowsOf2) => split ? { title, rows: [], lists: groupedLists(entries, rowsOf2) } : { title, rows: rowsOf2(entries), lists: [] };
1154
+ const snoozedRowsOf = (group) => snoozedRows(group, now);
1155
+ return {
1156
+ empty: null,
1157
+ sections: [
1158
+ ...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting, awaitingRows)],
1159
+ ...snoozed.length === 0 ? [] : [sectionOf(`Snoozed (n=${snoozed.length})`, snoozed, snoozedRowsOf)],
1160
+ ...reviewing.length === 0 ? [] : [sectionOf(`Reviewed (n=${reviewing.length})`, reviewing, rowsOf)]
1161
+ ]
1162
+ };
1163
+ }
1164
+ function awaitingRows(group) {
1165
+ return rowsOf(group).map((row, i) => {
1166
+ return { ...row, pending: { requestedAt: group[i].requestedAt.getTime(), snoozed: false } };
1167
+ });
1168
+ }
1169
+ function snoozedRows(group, now) {
1170
+ return toPrRows(
1171
+ group,
1172
+ group.map((entry) => `until ${formatWakeTime(entry.until, now)}`)
1173
+ ).map((row, i) => {
1174
+ return { ...row, pending: { requestedAt: group[i].requestedAt.getTime(), snoozed: true } };
1175
+ });
1176
+ }
1177
+ function buildOpenAuthoredView(raw, repo = null, grouped = false) {
1178
+ 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());
1179
+ if (open.length === 0) {
1180
+ return { empty: "No open authored PRs found.", sections: [] };
1181
+ }
1182
+ const rowsOf2 = (group) => {
1183
+ const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
1184
+ const ageWidth = Math.max(...ages.map((age) => age.length));
1185
+ return toPrRows(
1186
+ group,
1187
+ group.map(
1188
+ (entry, i) => `${ages[i].padEnd(ageWidth)} +${entry.additions}/-${entry.deletions}, ${entry.files} files`
1189
+ )
1190
+ );
1191
+ };
1192
+ const title = `Your open authored PRs (n=${open.length})`;
1193
+ if (repo === null && grouped) {
1194
+ return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf2) }] };
1195
+ }
1196
+ return { empty: null, sections: [{ title, rows: rowsOf2(open), lists: [] }] };
1197
+ }
1198
+ function rowsOf(group) {
1199
+ return toPrRows(
1200
+ group,
1201
+ group.map((entry) => durationLead(entry))
1202
+ );
1203
+ }
1204
+
1205
+ // src/tui/components/Footer.tsx
1206
+ import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
1207
+ function Footer({
1208
+ width,
1209
+ modal,
1210
+ editing,
1211
+ tab,
1212
+ authoredTab,
1213
+ views,
1214
+ pendingCursor,
1215
+ copyLinks: copyLinks2,
1216
+ openError,
1217
+ successNotice,
1218
+ stale
1219
+ }) {
1220
+ const notice = openError ?? successNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
1221
+ const check = openError === null && successNotice !== null;
1222
+ const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
1223
+ const hints = truncated(
1224
+ hintsFor(modal, editing, tab, authoredTab, views, pendingCursor, copyLinks2),
1225
+ width - 2 - noticeWidth
1226
+ );
1227
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1228
+ /* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
1229
+ /* @__PURE__ */ jsxs(
1230
+ "box",
1231
+ {
1232
+ flexDirection: "row",
1233
+ height: 1,
1234
+ marginBottom: 1,
1235
+ paddingLeft: 1,
1236
+ paddingRight: 1,
1237
+ justifyContent: "space-between",
1238
+ children: [
1239
+ /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hints }),
1240
+ /* @__PURE__ */ jsxs("text", { wrapMode: "none", children: [
1241
+ check && /* @__PURE__ */ jsx("span", { fg: theme.success, children: "\u2714 " }),
1242
+ /* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : successNotice !== null ? theme.muted : theme.warn, children: notice })
1243
+ ] })
1244
+ ]
1245
+ }
1246
+ )
1247
+ ] });
1248
+ }
1249
+ function truncated(text, limit2) {
1250
+ if (text.length <= limit2) {
1251
+ return text;
1252
+ }
1253
+ return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
1254
+ }
1255
+ function hintsFor(modal, editing, tab, authoredTab, views, pendingCursor, copyLinks2) {
1256
+ if (modal === "options") {
1257
+ return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit \xB7 \u2190/\u2192 toggle \xB7 s save \xB7 esc close \xB7 q quit";
1258
+ }
1259
+ if (modal === "settings") {
1260
+ return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter apply \xB7 \u2190/\u2192 toggle \xB7 esc close \xB7 q quit";
1261
+ }
1262
+ if (modal === "theme") {
1263
+ return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit hex \xB7 esc back \xB7 q quit";
1264
+ }
1265
+ if (modal === "snooze") {
1266
+ return "enter snooze \xB7 esc cancel";
1267
+ }
1268
+ const toggle = tab === 1 ? authoredTab === "open" ? "t merged stats \xB7 " : "t open PRs \xB7 " : "";
1269
+ if (tab === 0 || tab === 1 && authoredTab === "open") {
1270
+ const scope2 = views === null ? null : tab === 0 ? views.pendingScope : views.openScope;
1271
+ const repos2 = views === null ? [] : tab === 0 ? views.pendingRepos : views.openRepos;
1272
+ if (scope2?.view === "list") {
1273
+ return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
1274
+ }
1275
+ const snoozeAction = tab === 0 && views !== null ? snoozeActionOf(queueRowAt(views.pending, pendingCursor)) : null;
1276
+ const snooze = snoozeAction === null ? "" : `s ${snoozeAction} \xB7 `;
1277
+ const action = copyLinks2 ? "enter copy link" : "enter open";
1278
+ if (scope2 !== null && repos2.length > 0) {
1279
+ return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 ${snooze}${toggle}g group by repo \xB7 esc back \xB7 o options \xB7 S settings \xB7 r reload \xB7 q quit` : `\u2191/\u2193 select \xB7 ${action} \xB7 ${snooze}${toggle}esc back \xB7 1-5 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
1280
+ }
1281
+ return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 ${snooze}${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
1282
+ }
1283
+ const scope = views === null ? null : tab === 1 ? views.mergedScope : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
1284
+ const repos = views === null ? [] : tab === 1 ? views.mergedRepos : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
1285
+ if (scope?.view === "list") {
1286
+ return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
1287
+ }
1288
+ const view = views === null ? null : tab === 1 ? views.merged : tab === 2 ? views.review : tab === 3 ? views.size : views.comments;
1289
+ const expand = view?.expandable ? view.expanded ? "x collapse \xB7 " : "x expand \xB7 " : "";
1290
+ if (scope !== null && repos.length > 0) {
1291
+ return `${toggle}${expand}esc back \xB7 j/k scroll \xB7 1-5 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
1292
+ }
1293
+ 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`;
1294
+ }
1295
+
1296
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
1297
+ function stringReplaceAll(string, substring, postfix) {
1298
+ let index = string.indexOf(substring);
1299
+ if (index === -1) {
1300
+ return string;
1301
+ }
1302
+ const substringLength = substring.length;
1303
+ let endIndex = 0;
1304
+ let returnValue = "";
1305
+ do {
1306
+ returnValue += string.slice(endIndex, index) + substring + postfix;
1307
+ endIndex = index + substringLength;
1308
+ index = string.indexOf(substring, endIndex);
1309
+ } while (index !== -1);
1310
+ returnValue += string.slice(endIndex);
1311
+ return returnValue;
1312
+ }
1313
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
1314
+ let endIndex = 0;
1315
+ let returnValue = "";
1316
+ do {
1317
+ const isGotCR = string[index - 1] === "\r";
1318
+ returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? "\r\n" : "\n") + postfix;
1319
+ endIndex = index + 1;
1320
+ index = string.indexOf("\n", endIndex);
1321
+ } while (index !== -1);
1322
+ returnValue += string.slice(endIndex);
1323
+ return returnValue;
1324
+ }
1325
+
1326
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
1327
+ var ANSI_BACKGROUND_OFFSET = 10;
705
1328
  var ANSI_UNDERLINE_OFFSET = 20;
706
1329
  var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
707
1330
  var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
@@ -1228,120 +1851,12 @@ var applyStyle = (self, string) => {
1228
1851
  return openAll + string + closeAll;
1229
1852
  };
1230
1853
  Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
1231
- var chalk = createChalk();
1232
- var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
1233
- var source_default = chalk;
1234
-
1235
- // src/flags.ts
1236
- import { parseArgs } from "node:util";
1237
-
1238
- // src/time.ts
1239
- function wallFormatter(tz) {
1240
- return new Intl.DateTimeFormat("en-US", {
1241
- timeZone: tz,
1242
- hourCycle: "h23",
1243
- year: "numeric",
1244
- month: "2-digit",
1245
- day: "2-digit",
1246
- hour: "2-digit",
1247
- minute: "2-digit",
1248
- second: "2-digit"
1249
- });
1250
- }
1251
- var timeMode = {
1252
- business: true,
1253
- workWindows: [{ startMin: 0, endMin: 24 * 60 }],
1254
- workDays: /* @__PURE__ */ new Set([1, 2, 3, 4, 5]),
1255
- dayHours: 24,
1256
- formatter: wallFormatter("UTC")
1257
- };
1258
- function configureTimeMode({
1259
- business,
1260
- workWindows,
1261
- workDays,
1262
- tz
1263
- }) {
1264
- const workMinutesPerDay = workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
1265
- timeMode.business = business;
1266
- timeMode.workWindows = workWindows;
1267
- timeMode.workDays = workDays;
1268
- timeMode.dayHours = business ? workMinutesPerDay / 60 : 24;
1269
- timeMode.formatter = wallFormatter(tz);
1270
- }
1271
- function isFullDayMode() {
1272
- return timeMode.business && timeMode.dayHours === 24;
1273
- }
1274
- function wallParts(instantMs) {
1275
- const parts = Object.fromEntries(timeMode.formatter.formatToParts(instantMs).map((part) => [part.type, part.value]));
1276
- return {
1277
- year: Number(parts.year),
1278
- month: Number(parts.month),
1279
- day: Number(parts.day),
1280
- hour: Number(parts.hour),
1281
- minute: Number(parts.minute),
1282
- second: Number(parts.second)
1283
- };
1284
- }
1285
- function zonedStamp(date) {
1286
- const parts = wallParts(date.getTime());
1287
- const dayUtcMs = Date.UTC(parts.year, parts.month - 1, parts.day);
1288
- return { dayUtcMs, weekday: new Date(dayUtcMs).getUTCDay(), hour: parts.hour, minute: parts.minute };
1289
- }
1290
- function hasWorkWindows() {
1291
- const covered = timeMode.workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
1292
- return covered < 24 * 60;
1293
- }
1294
- function classifyInstant(date) {
1295
- const { weekday, hour, minute } = zonedStamp(date);
1296
- if (!timeMode.workDays.has(weekday)) {
1297
- return "weekend";
1298
- }
1299
- const minuteOfDay = hour * 60 + minute;
1300
- return timeMode.workWindows.some((window) => minuteOfDay >= window.startMin && minuteOfDay < window.endMin) ? "work" : "after";
1301
- }
1302
- function utcFromWall(wallTargetMs) {
1303
- let guess = wallTargetMs;
1304
- for (let i = 0; i < 2; i++) {
1305
- const parts = wallParts(guess);
1306
- const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
1307
- guess = wallTargetMs - (wall - guess);
1308
- }
1309
- return guess;
1310
- }
1311
- function businessMsBetween(start, end) {
1312
- const startMs = start.getTime();
1313
- const endMs = end.getTime();
1314
- if (endMs <= startMs) {
1315
- return 0;
1316
- }
1317
- let total = 0;
1318
- const parts = wallParts(startMs);
1319
- let localDay = Date.UTC(parts.year, parts.month - 1, parts.day);
1320
- while (utcFromWall(localDay) <= endMs) {
1321
- const weekday = new Date(localDay).getUTCDay();
1322
- if (timeMode.workDays.has(weekday)) {
1323
- for (const window of timeMode.workWindows) {
1324
- const windowStart = utcFromWall(localDay + window.startMin * 6e4);
1325
- const windowEnd = utcFromWall(localDay + window.endMin * 6e4);
1326
- const overlapStart = Math.max(windowStart, startMs);
1327
- const overlapEnd = Math.min(windowEnd, endMs);
1328
- if (overlapEnd > overlapStart) {
1329
- total += overlapEnd - overlapStart;
1330
- }
1331
- }
1332
- }
1333
- localDay += 864e5;
1334
- }
1335
- return total;
1336
- }
1337
- function durationHours(start, end) {
1338
- if (!timeMode.business) {
1339
- return (end.getTime() - start.getTime()) / 36e5;
1340
- }
1341
- return businessMsBetween(start, end) / 36e5;
1342
- }
1854
+ var chalk = createChalk();
1855
+ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
1856
+ var source_default = chalk;
1343
1857
 
1344
1858
  // src/flags.ts
1859
+ import { parseArgs } from "node:util";
1345
1860
  var flag = source_default.green;
1346
1861
  var value = source_default.cyan;
1347
1862
  var dim = source_default.dim;
@@ -1493,613 +2008,273 @@ function renderHelp() {
1493
2008
  const short = option.short ? `${flag(`-${option.short}`)}, ` : "";
1494
2009
  const long = option.placeholder ? `${flag(`--${option.name}`)} ${dim(option.placeholder)}` : flag(`--${option.name}`);
1495
2010
  const body = wrapMarkup(option.help, HELP_WIDTH - column).map((line) => colorizeHelp(line));
1496
- lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
1497
- for (const overflow of body.slice(1)) {
1498
- lines.push(" ".repeat(column) + overflow);
1499
- }
1500
- }
1501
- return lines.join("\n");
1502
- }
1503
- var HELP = renderHelp();
1504
- function parseCliArgs(args) {
1505
- const options = {};
1506
- for (const option of OPTIONS) {
1507
- options[option.name] = { type: option.type };
1508
- if (option.short) {
1509
- options[option.name].short = option.short;
1510
- }
1511
- if (option.multiple) {
1512
- options[option.name].multiple = true;
1513
- }
1514
- }
1515
- const { values } = parseArgs({ options, args });
1516
- const explicit = new Set(Object.keys(values));
1517
- for (const option of OPTIONS) {
1518
- if (option.default !== void 0 && values[option.name] === void 0) {
1519
- values[option.name] = option.default;
1520
- }
1521
- }
1522
- return { values, explicit };
1523
- }
1524
- function parseSince(input) {
1525
- const relative = /^(\d+)([dwmy])$/.exec(input);
1526
- if (relative) {
1527
- const amount = Number(relative[1]);
1528
- const date2 = /* @__PURE__ */ new Date();
1529
- if (relative[2] === "d") {
1530
- date2.setDate(date2.getDate() - amount);
1531
- }
1532
- if (relative[2] === "w") {
1533
- date2.setDate(date2.getDate() - amount * 7);
1534
- }
1535
- if (relative[2] === "m") {
1536
- date2.setMonth(date2.getMonth() - amount);
1537
- }
1538
- if (relative[2] === "y") {
1539
- date2.setFullYear(date2.getFullYear() - amount);
1540
- }
1541
- return date2;
1542
- }
1543
- const date = new Date(input);
1544
- if (Number.isNaN(date.getTime())) {
1545
- throw new CliError(`invalid --since value "${input}", use an ISO date or 30d/8w/6m/1y`);
1546
- }
1547
- return date;
1548
- }
1549
- function parseTarget(input) {
1550
- const match = /^(\d+(?:\.\d+)?)([hdm]?)$/.exec(input);
1551
- if (!match) {
1552
- throw new CliError(`invalid --target value "${input}", use 24h, 2d, or 90m`);
1553
- }
1554
- const amount = Number(match[1]);
1555
- if (match[2] === "d") {
1556
- return amount * timeMode.dayHours;
1557
- }
1558
- if (match[2] === "m") {
1559
- return amount / 60;
1560
- }
1561
- return amount;
1562
- }
1563
- var DEFAULT_TARGET_PERCENTILE = 90;
1564
- function parseTargetPercentile(input) {
1565
- const match = /^p?(\d{1,3})$/i.exec(input);
1566
- const value2 = match === null ? Number.NaN : Number(match[1]);
1567
- if (!Number.isInteger(value2) || value2 < 1 || value2 > 100) {
1568
- throw new CliError(`invalid --target-percentile value "${input}", use a percentile from 1 to 100 like 90 or p90`);
1569
- }
1570
- return value2;
1571
- }
1572
- function parseSizeTarget(input) {
1573
- const target = {};
1574
- for (const part of input.split(",")) {
1575
- const match = /^(\d+)([lf]?)$/.exec(part.trim());
1576
- if (!match) {
1577
- throw new CliError(`invalid --size-target value "${input}", use 400, 400l, 20f, or 400l,20f`);
1578
- }
1579
- const key = match[2] === "f" ? "files" : "lines";
1580
- if (target[key] !== void 0) {
1581
- throw new CliError(`--size-target sets the ${key} budget twice`);
1582
- }
1583
- target[key] = Number(match[1]);
1584
- }
1585
- return target;
1586
- }
1587
- var REVIEW_TYPES = /* @__PURE__ */ new Map([
1588
- ["approve", "APPROVED"],
1589
- ["comment", "COMMENTED"],
1590
- ["request-changes", "CHANGES_REQUESTED"]
1591
- ]);
1592
- function parseReviewTypes(input) {
1593
- const states = /* @__PURE__ */ new Set();
1594
- for (const part of input.split(",")) {
1595
- const state = REVIEW_TYPES.get(part.trim().toLowerCase());
1596
- if (state === void 0) {
1597
- throw new CliError(`invalid --review-types value "${part.trim()}", use approve, comment, or request-changes`);
1598
- }
1599
- states.add(state);
1600
- }
1601
- return states;
1602
- }
1603
- function toMinutesOfDay(hourText, minuteText, meridiem) {
1604
- const minute = Number(minuteText ?? 0);
1605
- if (minute > 59) {
1606
- return null;
1607
- }
1608
- let hour = Number(hourText);
1609
- if (meridiem !== void 0) {
1610
- if (hour < 1 || hour > 12) {
1611
- return null;
1612
- }
1613
- hour %= 12;
1614
- if (meridiem.toLowerCase() === "pm") {
1615
- hour += 12;
1616
- }
1617
- }
1618
- return hour * 60 + minute;
1619
- }
1620
- var WEEKDAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
1621
- var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1622
- function parseWorkDays(input) {
1623
- const days = /* @__PURE__ */ new Set();
1624
- for (const part of input.split(",")) {
1625
- const match = /^([a-z]{3})(?:-([a-z]{3}))?$/i.exec(part.trim());
1626
- const endName = match?.[2];
1627
- const start = match === null ? -1 : WEEKDAY_NAMES.indexOf(match[1].toLowerCase());
1628
- const end = match === null ? -1 : endName === void 0 ? start : WEEKDAY_NAMES.indexOf(endName.toLowerCase());
1629
- if (start === -1 || end === -1) {
1630
- throw new CliError(
1631
- `invalid --work-days value "${part.trim()}", use weekday names and ranges like mon-fri, sun-thu, or mon,wed,fri`
1632
- );
1633
- }
1634
- for (let day = start; ; day = (day + 1) % 7) {
1635
- days.add(day);
1636
- if (day === end) {
1637
- break;
1638
- }
1639
- }
1640
- }
1641
- return days;
1642
- }
1643
- function workDayRunLabel(start, end) {
1644
- return start === end ? WEEKDAY_LABELS[start] : `${WEEKDAY_LABELS[start]}-${WEEKDAY_LABELS[end]}`;
1645
- }
1646
- function formatWorkDays(days) {
1647
- if (days.size === 7) {
1648
- return "Mon-Sun";
1649
- }
1650
- const monFirst = [1, 2, 3, 4, 5, 6, 0];
1651
- const anchor = monFirst.find((day) => days.has(day) && !days.has((day + 6) % 7)) ?? 1;
1652
- const runs = [];
1653
- let runStart = -1;
1654
- let runEnd = -1;
1655
- for (let step = 0; step < 7; step++) {
1656
- const day = (anchor + step) % 7;
1657
- if (days.has(day)) {
1658
- runStart = runStart === -1 ? day : runStart;
1659
- runEnd = day;
1660
- } else if (runStart !== -1) {
1661
- runs.push(workDayRunLabel(runStart, runEnd));
1662
- runStart = -1;
1663
- }
1664
- }
1665
- return runs.join(",");
1666
- }
1667
- function canonicalWorkDays(input) {
1668
- return formatWorkDays(parseWorkDays(input));
1669
- }
1670
- function parseWorkHours(input) {
1671
- const windows = input.split(",").map((range) => {
1672
- const match = /^(\d{1,2})(?::(\d{2}))?(am|pm)?-(\d{1,2})(?::(\d{2}))?(am|pm)?$/i.exec(range.trim());
1673
- if (!match) {
1674
- throw new CliError(
1675
- `invalid --work-hours range "${range}", use ranges like 9-17, 9am-6pm, 8:30-16:30, or 9-18,19:30-20:30`
1676
- );
1677
- }
1678
- const startMin = toMinutesOfDay(match[1], match[2], match[3]);
1679
- const end = toMinutesOfDay(match[4], match[5], match[6]);
1680
- const endMin = end === 0 ? 24 * 60 : end;
1681
- if (startMin === null || endMin === null || endMin <= startMin || endMin > 24 * 60) {
1682
- throw new CliError(`invalid --work-hours range "${range}"`);
1683
- }
1684
- return { startMin, endMin };
1685
- });
1686
- windows.sort((a, b) => a.startMin - b.startMin);
1687
- for (let i = 1; i < windows.length; i++) {
1688
- if (windows[i].startMin < windows[i - 1].endMin) {
1689
- throw new CliError(`--work-hours ranges overlap around ${formatMinutesOfDay(windows[i].startMin)}`);
1690
- }
1691
- }
1692
- return windows;
1693
- }
1694
- function resolveTimezone(input) {
1695
- const tz = input ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
1696
- try {
1697
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
1698
- } catch {
1699
- throw new CliError(`invalid --tz value "${tz}", use an IANA zone like Europe/Berlin`);
1700
- }
1701
- return tz;
1702
- }
1703
-
1704
- // src/tui/components/Spinner.tsx
1705
- import { useEffect, useState } from "react";
1706
- import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
1707
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1708
- var SPINNER_INTERVAL_MS = 80;
1709
- function Spinner() {
1710
- const [frame, setFrame] = useState(0);
1711
- useEffect(() => {
1712
- const timer = setInterval(() => {
1713
- setFrame((previous) => (previous + 1) % SPINNER_FRAMES.length);
1714
- }, SPINNER_INTERVAL_MS);
1715
- return () => {
1716
- clearInterval(timer);
1717
- };
1718
- }, []);
1719
- return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
1720
- }
1721
-
1722
- // src/tui/components/Header.tsx
1723
- import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
1724
- function Header({
1725
- options,
1726
- raw,
1727
- error,
1728
- spinning,
1729
- reloadEvery
1730
- }) {
1731
- const context = [
1732
- raw ? `@${raw.user}` : options.user !== "" ? `@${options.user}` : "@...",
1733
- `since ${options.since}`,
1734
- raw && raw.repos.length > 0 ? raw.repos.join(", ") : options.repos !== "" ? options.repos : "all repos",
1735
- timeModeLabel(options)
1736
- ].join(" \xB7 ");
1737
- const rightStatus = raw ? error !== null ? "reload failed \xB7 press r to retry" : `refreshed ${raw.fetchedAt.toLocaleTimeString()}${reloadEvery === null ? "" : ` \xB7 every ${reloadEvery}`}` : "";
1738
- return /* @__PURE__ */ jsxs2("box", { flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [
1739
- /* @__PURE__ */ jsxs2("text", { wrapMode: "none", children: [
1740
- /* @__PURE__ */ jsx3("b", { fg: theme.accent, children: "pr-stats" }),
1741
- /* @__PURE__ */ jsxs2("span", { fg: theme.muted, children: [
1742
- " \xB7 ",
1743
- context
1744
- ] })
1745
- ] }),
1746
- spinning ? /* @__PURE__ */ jsx3(Spinner, {}) : /* @__PURE__ */ jsx3("text", { wrapMode: "none", fg: raw !== null && error !== null ? theme.error : theme.muted, children: rightStatus })
1747
- ] });
2011
+ lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
2012
+ for (const overflow of body.slice(1)) {
2013
+ lines.push(" ".repeat(column) + overflow);
2014
+ }
2015
+ }
2016
+ return lines.join("\n");
1748
2017
  }
1749
- function timeModeLabel(options) {
1750
- if (options.wallClock) {
1751
- return "wall-clock time";
2018
+ var HELP = renderHelp();
2019
+ function parseCliArgs(args) {
2020
+ const options = {};
2021
+ for (const option of OPTIONS) {
2022
+ options[option.name] = { type: option.type };
2023
+ if (option.short) {
2024
+ options[option.name].short = option.short;
2025
+ }
2026
+ if (option.multiple) {
2027
+ options[option.name].multiple = true;
2028
+ }
1752
2029
  }
1753
- const tz = options.tz === "" ? Intl.DateTimeFormat().resolvedOptions().timeZone : options.tz;
1754
- if (options.workHours === "0-24") {
1755
- return `${options.workDays} all hours ${tz}`;
2030
+ const { values } = parseArgs({ options, args });
2031
+ const explicit = new Set(Object.keys(values));
2032
+ for (const option of OPTIONS) {
2033
+ if (option.default !== void 0 && values[option.name] === void 0) {
2034
+ values[option.name] = option.default;
2035
+ }
1756
2036
  }
1757
- return `${options.workDays} ${options.workHours} (${dailyHoursLabel(options.workHours)}) ${tz}`;
1758
- }
1759
- function dailyHoursLabel(workHours) {
1760
- const minutes = parseWorkHours(workHours).reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
1761
- const hours = Math.round(minutes / 60 * 100) / 100;
1762
- return `${hours} ${hours === 1 ? "hour" : "hours"}`;
2037
+ return { values, explicit };
1763
2038
  }
1764
-
1765
- // src/compute.ts
1766
- function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
1767
- const reviewed = [];
1768
- const pending = [];
1769
- for (const result of results) {
1770
- if (result.kind === "reviewed") {
1771
- reviewed.push({
1772
- pr: result.pr,
1773
- requestedAt: result.requestedAt,
1774
- reviewedAt: result.reviewedAt,
1775
- hours: durationHours(result.requestedAt, result.reviewedAt),
1776
- verdict: result.verdict,
1777
- lines: result.lines
1778
- });
1779
- } else if (result.kind === "pending" && result.pr.state === "open") {
1780
- pending.push({ pr: result.pr, requestedAt: result.requestedAt, hours: durationHours(result.requestedAt, now) });
2039
+ function parseSince(input) {
2040
+ const relative = /^(\d+)([dwmy])$/.exec(input);
2041
+ if (relative) {
2042
+ const amount = Number(relative[1]);
2043
+ const date2 = /* @__PURE__ */ new Date();
2044
+ if (relative[2] === "d") {
2045
+ date2.setDate(date2.getDate() - amount);
1781
2046
  }
1782
- }
1783
- pending.sort((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
1784
- const pendingKeys = new Set(pending.map((entry) => `${entry.pr.repo}#${entry.pr.number}`));
1785
- const latestReviews = /* @__PURE__ */ new Map();
1786
- for (const result of results) {
1787
- if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
1788
- continue;
2047
+ if (relative[2] === "w") {
2048
+ date2.setDate(date2.getDate() - amount * 7);
1789
2049
  }
1790
- const key = `${result.pr.repo}#${result.pr.number}`;
1791
- if (pendingKeys.has(key)) {
1792
- continue;
2050
+ if (relative[2] === "m") {
2051
+ date2.setMonth(date2.getMonth() - amount);
1793
2052
  }
1794
- const latest = latestReviews.get(key);
1795
- if (latest === void 0 || result.reviewedAt > latest.reviewedAt) {
1796
- latestReviews.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
2053
+ if (relative[2] === "y") {
2054
+ date2.setFullYear(date2.getFullYear() - amount);
1797
2055
  }
2056
+ return date2;
1798
2057
  }
1799
- const reviewing = [...latestReviews.values()].map(({ pr, reviewedAt }) => {
1800
- return { pr, reviewedAt, hours: durationHours(reviewedAt, now) };
1801
- }).toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
1802
- const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
1803
- const unrequested = results.filter((result) => result.kind === "unrequested");
1804
- const allHours = reviewed.map((result) => result.hours);
1805
- const hoursByRepo = /* @__PURE__ */ new Map();
1806
- for (const result of reviewed) {
1807
- const hours = hoursByRepo.get(result.pr.repo) ?? [];
1808
- hours.push(result.hours);
1809
- hoursByRepo.set(result.pr.repo, hours);
1810
- }
1811
- const byRepo = [...hoursByRepo.entries()].toSorted((a, b) => b[1].length - a[1].length);
1812
- const misses = targetHours === void 0 ? [] : reviewed.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
1813
- const cyclesByPr = /* @__PURE__ */ new Map();
1814
- for (const result of reviewed) {
1815
- const key = `${result.pr.repo}#${result.pr.number}`;
1816
- cyclesByPr.set(key, (cyclesByPr.get(key) ?? 0) + 1);
2058
+ const date = new Date(input);
2059
+ if (Number.isNaN(date.getTime())) {
2060
+ throw new CliError(`invalid --since value "${input}", use an ISO date or 30d/8w/6m/1y`);
1817
2061
  }
1818
- return {
1819
- reviewed,
1820
- pending,
1821
- reviewing,
1822
- expired,
1823
- unrequested,
1824
- allHours,
1825
- byRepo,
1826
- misses,
1827
- cycles: [...cyclesByPr.values()]
1828
- };
2062
+ return date;
1829
2063
  }
1830
- function computeSizeStats(sizes, { sizeTarget } = {}) {
1831
- const metrics = [
1832
- { label: "files changed", values: sizes.map((size) => size.files) },
1833
- { label: "lines added", values: sizes.map((size) => size.additions) },
1834
- { label: "lines removed", values: sizes.map((size) => size.deletions) },
1835
- { label: "lines total", values: sizes.map((size) => size.total) }
1836
- ];
1837
- const timelineTotals = [...sizes].toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime()).map((size) => size.total);
1838
- if (sizeTarget === void 0) {
1839
- return { metrics, timelineTotals, met: void 0, misses: [], targetLabel: void 0 };
2064
+ function parseTarget(input) {
2065
+ const match = /^(\d+(?:\.\d+)?)([hdm]?)$/.exec(input);
2066
+ if (!match) {
2067
+ throw new CliError(`invalid --target value "${input}", use 24h, 2d, or 90m`);
1840
2068
  }
1841
- const meetsTarget = (size) => (sizeTarget.lines === void 0 || size.total <= sizeTarget.lines) && (sizeTarget.files === void 0 || size.files <= sizeTarget.files);
1842
- const targetLabel = [
1843
- ...sizeTarget.lines === void 0 ? [] : [`<= ${sizeTarget.lines} lines`],
1844
- ...sizeTarget.files === void 0 ? [] : [`<= ${sizeTarget.files} files`]
1845
- ].join(", ");
1846
- const met = sizes.filter((size) => meetsTarget(size)).length;
1847
- const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
1848
- return { metrics, timelineTotals, met, misses, targetLabel };
2069
+ const amount = Number(match[1]);
2070
+ if (match[2] === "d") {
2071
+ return amount * timeMode.dayHours;
2072
+ }
2073
+ if (match[2] === "m") {
2074
+ return amount / 60;
2075
+ }
2076
+ return amount;
1849
2077
  }
1850
- function computeMergeStats(sizes) {
1851
- const merged = [];
1852
- const closed = [];
1853
- const open = [];
1854
- for (const entry of sizes) {
1855
- if (entry.mergedAt !== null) {
1856
- merged.push({ entry, mergedAt: entry.mergedAt, hours: durationHours(entry.pr.createdAt, entry.mergedAt) });
1857
- } else if (entry.pr.state === "open") {
1858
- open.push(entry);
1859
- } else if (entry.closedAt !== null) {
1860
- closed.push({ entry, closedAt: entry.closedAt, hours: durationHours(entry.pr.createdAt, entry.closedAt) });
1861
- }
2078
+ var DEFAULT_TARGET_PERCENTILE = 90;
2079
+ function parseTargetPercentile(input) {
2080
+ const match = /^p?(\d{1,3})$/i.exec(input);
2081
+ const value2 = match === null ? Number.NaN : Number(match[1]);
2082
+ if (!Number.isInteger(value2) || value2 < 1 || value2 > 100) {
2083
+ throw new CliError(`invalid --target-percentile value "${input}", use a percentile from 1 to 100 like 90 or p90`);
1862
2084
  }
1863
- merged.sort((a, b) => b.mergedAt.getTime() - a.mergedAt.getTime());
1864
- closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
1865
- return { merged, closed, open, allHours: merged.map((result) => result.hours) };
2085
+ return value2;
1866
2086
  }
1867
- function computeReviewerStats(sizes, author) {
1868
- const byLogin = /* @__PURE__ */ new Map();
1869
- let mergedReviewed = 0;
1870
- let mergedUnreviewed = 0;
1871
- for (const entry of sizes) {
1872
- const others = entry.reviews.flatMap(
1873
- (review) => review.login === null || review.login === author ? [] : [review.login]
1874
- );
1875
- for (const login of others) {
1876
- const counts = byLogin.get(login) ?? { prs: 0, reviews: 0 };
1877
- counts.reviews += 1;
1878
- byLogin.set(login, counts);
1879
- }
1880
- const distinct = new Set(others);
1881
- for (const login of distinct) {
1882
- const counts = byLogin.get(login);
1883
- if (counts !== void 0) {
1884
- counts.prs += 1;
1885
- }
2087
+ function parseSizeTarget(input) {
2088
+ const target = {};
2089
+ for (const part of input.split(",")) {
2090
+ const match = /^(\d+)([lf]?)$/.exec(part.trim());
2091
+ if (!match) {
2092
+ throw new CliError(`invalid --size-target value "${input}", use 400, 400l, 20f, or 400l,20f`);
1886
2093
  }
1887
- if (entry.mergedAt !== null) {
1888
- if (others.length > 0) {
1889
- mergedReviewed += 1;
1890
- } else {
1891
- mergedUnreviewed += 1;
1892
- }
2094
+ const key = match[2] === "f" ? "files" : "lines";
2095
+ if (target[key] !== void 0) {
2096
+ throw new CliError(`--size-target sets the ${key} budget twice`);
1893
2097
  }
2098
+ target[key] = Number(match[1]);
1894
2099
  }
1895
- const leaderboard = [...byLogin.entries()].map(([login, counts]) => {
1896
- return { login, ...counts };
1897
- }).toSorted((a, b) => b.prs - a.prs || b.reviews - a.reviews || a.login.localeCompare(b.login));
1898
- return { leaderboard, mergedReviewed, mergedUnreviewed };
2100
+ return target;
1899
2101
  }
1900
- function firstReviewOf(entry, author) {
1901
- let earliest = null;
1902
- for (const review of entry.reviews) {
1903
- if (review.login === null || review.login === author || review.submittedAt === null) {
1904
- continue;
1905
- }
1906
- if (earliest === null || review.submittedAt < earliest) {
1907
- earliest = review.submittedAt;
2102
+ var REVIEW_TYPES = /* @__PURE__ */ new Map([
2103
+ ["approve", "APPROVED"],
2104
+ ["comment", "COMMENTED"],
2105
+ ["request-changes", "CHANGES_REQUESTED"]
2106
+ ]);
2107
+ function parseReviewTypes(input) {
2108
+ const states = /* @__PURE__ */ new Set();
2109
+ for (const part of input.split(",")) {
2110
+ const state = REVIEW_TYPES.get(part.trim().toLowerCase());
2111
+ if (state === void 0) {
2112
+ throw new CliError(`invalid --review-types value "${part.trim()}", use approve, comment, or request-changes`);
1908
2113
  }
2114
+ states.add(state);
1909
2115
  }
1910
- if (earliest === null) {
2116
+ return states;
2117
+ }
2118
+ function toMinutesOfDay(hourText, minuteText, meridiem) {
2119
+ const minute = Number(minuteText ?? 0);
2120
+ if (minute > 59) {
1911
2121
  return null;
1912
2122
  }
1913
- return { reviewedAt: earliest, hours: durationHours(entry.pr.createdAt, earliest) };
2123
+ let hour = Number(hourText);
2124
+ if (meridiem !== void 0) {
2125
+ if (hour < 1 || hour > 12) {
2126
+ return null;
2127
+ }
2128
+ hour %= 12;
2129
+ if (meridiem.toLowerCase() === "pm") {
2130
+ hour += 12;
2131
+ }
2132
+ }
2133
+ return hour * 60 + minute;
1914
2134
  }
1915
- function computeFirstReviewStats(sizes, author, { now = /* @__PURE__ */ new Date() } = {}) {
1916
- const received = [];
1917
- const awaiting = [];
1918
- for (const entry of sizes) {
1919
- const first = firstReviewOf(entry, author);
1920
- if (first !== null) {
1921
- received.push({ entry, ...first });
1922
- } else if (entry.pr.state === "open") {
1923
- awaiting.push({ entry, hours: durationHours(entry.pr.createdAt, now) });
2135
+ var WEEKDAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
2136
+ var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2137
+ function parseWorkDays(input) {
2138
+ const days = /* @__PURE__ */ new Set();
2139
+ for (const part of input.split(",")) {
2140
+ const match = /^([a-z]{3})(?:-([a-z]{3}))?$/i.exec(part.trim());
2141
+ const endName = match?.[2];
2142
+ const start = match === null ? -1 : WEEKDAY_NAMES.indexOf(match[1].toLowerCase());
2143
+ const end = match === null ? -1 : endName === void 0 ? start : WEEKDAY_NAMES.indexOf(endName.toLowerCase());
2144
+ if (start === -1 || end === -1) {
2145
+ throw new CliError(
2146
+ `invalid --work-days value "${part.trim()}", use weekday names and ranges like mon-fri, sun-thu, or mon,wed,fri`
2147
+ );
2148
+ }
2149
+ for (let day = start; ; day = (day + 1) % 7) {
2150
+ days.add(day);
2151
+ if (day === end) {
2152
+ break;
2153
+ }
1924
2154
  }
1925
2155
  }
1926
- awaiting.sort((a, b) => a.entry.pr.createdAt.getTime() - b.entry.pr.createdAt.getTime());
1927
- return { received, awaiting, allHours: received.map((result) => result.hours) };
1928
- }
1929
- function computeCommentStats(sizes) {
1930
- const metrics = [
1931
- { label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
1932
- { label: "review comments", values: sizes.map((size) => size.comments.review) },
1933
- { label: "all comments", values: sizes.map((size) => size.comments.total) }
1934
- ];
1935
- const totals = sizes.map((size) => size.comments.total);
1936
- const uncommented = totals.filter((total) => total === 0).length;
1937
- const top = sizes.filter((size) => size.comments.total > 0).toSorted((a, b) => b.comments.total - a.comments.total);
1938
- return { metrics, totals, uncommented, top };
1939
- }
1940
-
1941
- // src/report.ts
1942
- var BUCKETS = [];
1943
- function initBuckets() {
1944
- BUCKETS = makeBuckets();
2156
+ return days;
1945
2157
  }
1946
- function currentBuckets() {
1947
- return BUCKETS;
2158
+ function workDayRunLabel(start, end) {
2159
+ return start === end ? WEEKDAY_LABELS[start] : `${WEEKDAY_LABELS[start]}-${WEEKDAY_LABELS[end]}`;
1948
2160
  }
1949
- function makeBuckets() {
1950
- if (!timeMode.business || isFullDayMode()) {
1951
- return [
1952
- { label: "< 1h", max: 1 },
1953
- { label: "1-4h", max: 4 },
1954
- { label: "4-8h", max: 8 },
1955
- { label: "8-24h", max: 24 },
1956
- { label: "1-2d", max: 48 },
1957
- { label: "2-4d", max: 96 },
1958
- { label: "4-7d", max: 168 },
1959
- { label: "> 7d", max: Infinity }
1960
- ];
2161
+ function formatWorkDays(days) {
2162
+ if (days.size === 7) {
2163
+ return "Mon-Sun";
1961
2164
  }
1962
- const wd = timeMode.dayHours;
1963
- return [
1964
- { label: "< 1h", max: 1 },
1965
- { label: "1-4h", max: 4 },
1966
- { label: "4h-1wd", max: wd },
1967
- { label: "1-2wd", max: 2 * wd },
1968
- { label: "2-3wd", max: 3 * wd },
1969
- { label: "3-5wd", max: 5 * wd },
1970
- { label: "5-10wd", max: 10 * wd },
1971
- { label: "> 10wd", max: Infinity }
1972
- ];
1973
- }
1974
- var LINE_BUCKETS = [
1975
- { label: "< 50", max: 50 },
1976
- { label: "50-100", max: 100 },
1977
- { label: "100-250", max: 250 },
1978
- { label: "250-500", max: 500 },
1979
- { label: "500-1k", max: 1e3 },
1980
- { label: "1k-2.5k", max: 2500 },
1981
- { label: "2.5k-5k", max: 5e3 },
1982
- { label: "> 5k", max: Infinity }
1983
- ];
1984
- var FILE_BUCKETS = [
1985
- { label: "1-2", max: 3 },
1986
- { label: "3-5", max: 6 },
1987
- { label: "6-10", max: 11 },
1988
- { label: "11-20", max: 21 },
1989
- { label: "21-50", max: 51 },
1990
- { label: "> 50", max: Infinity }
1991
- ];
1992
- var CYCLE_BUCKETS = [
1993
- { label: "1", max: 2 },
1994
- { label: "2", max: 3 },
1995
- { label: "3", max: 4 },
1996
- { label: "4-5", max: 6 },
1997
- { label: "> 5", max: Infinity }
1998
- ];
1999
- var COMMENT_BUCKETS = [
2000
- { label: "0", max: 1 },
2001
- { label: "1-2", max: 3 },
2002
- { label: "3-5", max: 6 },
2003
- { label: "6-10", max: 11 },
2004
- { label: "11-20", max: 21 },
2005
- { label: "21-50", max: 51 },
2006
- { label: "> 50", max: Infinity }
2007
- ];
2008
- function formatHoursOnly(hours) {
2009
- if (hours < 1) {
2010
- return `${Math.round(hours * 60)}m`;
2165
+ const monFirst = [1, 2, 3, 4, 5, 6, 0];
2166
+ const anchor = monFirst.find((day) => days.has(day) && !days.has((day + 6) % 7)) ?? 1;
2167
+ const runs = [];
2168
+ let runStart = -1;
2169
+ let runEnd = -1;
2170
+ for (let step = 0; step < 7; step++) {
2171
+ const day = (anchor + step) % 7;
2172
+ if (days.has(day)) {
2173
+ runStart = runStart === -1 ? day : runStart;
2174
+ runEnd = day;
2175
+ } else if (runStart !== -1) {
2176
+ runs.push(workDayRunLabel(runStart, runEnd));
2177
+ runStart = -1;
2178
+ }
2011
2179
  }
2012
- return `${hours.toFixed(1)}h`;
2180
+ return runs.join(",");
2013
2181
  }
2014
- function weeksSuffix(hours) {
2015
- const weekHours = timeMode.business ? 5 * timeMode.dayHours : 7 * 24;
2016
- if (hours < weekHours) {
2017
- return "";
2182
+ function canonicalWorkDays(input) {
2183
+ return formatWorkDays(parseWorkDays(input));
2184
+ }
2185
+ function parseWorkHours(input) {
2186
+ const windows = input.split(",").map((range) => {
2187
+ const match = /^(\d{1,2})(?::(\d{2}))?(am|pm)?-(\d{1,2})(?::(\d{2}))?(am|pm)?$/i.exec(range.trim());
2188
+ if (!match) {
2189
+ throw new CliError(
2190
+ `invalid --work-hours range "${range}", use ranges like 9-17, 9am-6pm, 8:30-16:30, or 9-18,19:30-20:30`
2191
+ );
2192
+ }
2193
+ const startMin = toMinutesOfDay(match[1], match[2], match[3]);
2194
+ const end = toMinutesOfDay(match[4], match[5], match[6]);
2195
+ const endMin = end === 0 ? 24 * 60 : end;
2196
+ if (startMin === null || endMin === null || endMin <= startMin || endMin > 24 * 60) {
2197
+ throw new CliError(`invalid --work-hours range "${range}"`);
2198
+ }
2199
+ return { startMin, endMin };
2200
+ });
2201
+ windows.sort((a, b) => a.startMin - b.startMin);
2202
+ for (let i = 1; i < windows.length; i++) {
2203
+ if (windows[i].startMin < windows[i - 1].endMin) {
2204
+ throw new CliError(`--work-hours ranges overlap around ${formatMinutesOfDay(windows[i].startMin)}`);
2205
+ }
2018
2206
  }
2019
- return ` (${(hours / weekHours).toFixed(1)} weeks)`;
2207
+ return windows;
2020
2208
  }
2021
- function formatCount(value2) {
2022
- if (value2 < 1e3) {
2023
- return String(value2);
2209
+ function resolveTimezone(input) {
2210
+ const tz = input ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
2211
+ try {
2212
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
2213
+ } catch {
2214
+ throw new CliError(`invalid --tz value "${tz}", use an IANA zone like Europe/Berlin`);
2024
2215
  }
2025
- const scaled = value2 / 1e3;
2026
- return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
2216
+ return tz;
2027
2217
  }
2028
2218
 
2029
- // src/tui/views/rows.ts
2030
- function durationLead(result) {
2031
- return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
2032
- }
2033
- function toPrRows(entries, leads) {
2034
- const width = Math.max(...leads.map((lead) => lead.length));
2035
- return entries.map((entry, i) => {
2036
- return {
2037
- lead: leads[i].padEnd(width),
2038
- ref: `${entry.pr.repo}#${entry.pr.number}`,
2039
- url: entry.pr.url,
2040
- title: entry.pr.title
2219
+ // src/tui/components/Spinner.tsx
2220
+ import { useEffect, useState } from "react";
2221
+ import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
2222
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2223
+ var SPINNER_INTERVAL_MS = 80;
2224
+ function Spinner() {
2225
+ const [frame, setFrame] = useState(0);
2226
+ useEffect(() => {
2227
+ const timer = setInterval(() => {
2228
+ setFrame((previous) => (previous + 1) % SPINNER_FRAMES.length);
2229
+ }, SPINNER_INTERVAL_MS);
2230
+ return () => {
2231
+ clearInterval(timer);
2041
2232
  };
2042
- });
2233
+ }, []);
2234
+ return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
2043
2235
  }
2044
2236
 
2045
- // src/tui/views/queue.ts
2046
- function queueRows(view) {
2047
- return view.sections.flatMap((section) => [...section.rows, ...section.lists.flatMap((list) => list.rows)]);
2048
- }
2049
- function groupedLists(entries, rowsOf2) {
2050
- const groups = /* @__PURE__ */ new Map();
2051
- for (const entry of entries) {
2052
- const group = groups.get(entry.pr.repo) ?? [];
2053
- group.push(entry);
2054
- groups.set(entry.pr.repo, group);
2055
- }
2056
- return [...groups.entries()].toSorted((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])).map(([repo, group]) => {
2057
- return { title: `${repo} (n=${group.length})`, rows: rowsOf2(group) };
2058
- });
2059
- }
2060
- function buildPendingReviewView(raw, repo = null, grouped = false) {
2061
- const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
2062
- const awaiting = repo === null ? stats.pending : stats.pending.filter((entry) => entry.pr.repo === repo);
2063
- const reviewing = repo === null ? stats.reviewing : stats.reviewing.filter((entry) => entry.pr.repo === repo);
2064
- if (awaiting.length === 0 && reviewing.length === 0) {
2065
- return { empty: "No PRs are awaiting your review, and none you reviewed are still open.", sections: [] };
2066
- }
2067
- const split = repo === null && grouped;
2068
- const sectionOf = (title, entries) => split ? { title, rows: [], lists: groupedLists(entries, rowsOf) } : { title, rows: rowsOf(entries), lists: [] };
2069
- return {
2070
- empty: null,
2071
- sections: [
2072
- ...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting)],
2073
- ...reviewing.length === 0 ? [] : [sectionOf(`Reviewed (n=${reviewing.length})`, reviewing)]
2074
- ]
2075
- };
2076
- }
2077
- function buildOpenAuthoredView(raw, repo = null, grouped = false) {
2078
- 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());
2079
- if (open.length === 0) {
2080
- return { empty: "No open authored PRs found.", sections: [] };
2081
- }
2082
- const rowsOf2 = (group) => {
2083
- const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
2084
- const ageWidth = Math.max(...ages.map((age) => age.length));
2085
- return toPrRows(
2086
- group,
2087
- group.map(
2088
- (entry, i) => `${ages[i].padEnd(ageWidth)} +${entry.additions}/-${entry.deletions}, ${entry.files} files`
2089
- )
2090
- );
2091
- };
2092
- const title = `Your open authored PRs (n=${open.length})`;
2093
- if (repo === null && grouped) {
2094
- return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf2) }] };
2237
+ // src/tui/components/Header.tsx
2238
+ import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
2239
+ function Header({
2240
+ options,
2241
+ raw,
2242
+ error,
2243
+ spinning,
2244
+ reloadEvery
2245
+ }) {
2246
+ const context = [
2247
+ raw ? `@${raw.user}` : options.user !== "" ? `@${options.user}` : "@...",
2248
+ `since ${options.since}`,
2249
+ raw && raw.repos.length > 0 ? raw.repos.join(", ") : options.repos !== "" ? options.repos : "all repos",
2250
+ timeModeLabel(options)
2251
+ ].join(" \xB7 ");
2252
+ const rightStatus = raw ? error !== null ? "reload failed \xB7 press r to retry" : `refreshed ${raw.fetchedAt.toLocaleTimeString()}${reloadEvery === null ? "" : ` \xB7 every ${reloadEvery}`}` : "";
2253
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [
2254
+ /* @__PURE__ */ jsxs2("text", { wrapMode: "none", children: [
2255
+ /* @__PURE__ */ jsx3("b", { fg: theme.accent, children: "pr-stats" }),
2256
+ /* @__PURE__ */ jsxs2("span", { fg: theme.muted, children: [
2257
+ " \xB7 ",
2258
+ context
2259
+ ] })
2260
+ ] }),
2261
+ spinning ? /* @__PURE__ */ jsx3(Spinner, {}) : /* @__PURE__ */ jsx3("text", { wrapMode: "none", fg: raw !== null && error !== null ? theme.error : theme.muted, children: rightStatus })
2262
+ ] });
2263
+ }
2264
+ function timeModeLabel(options) {
2265
+ if (options.wallClock) {
2266
+ return "wall-clock time";
2095
2267
  }
2096
- return { empty: null, sections: [{ title, rows: rowsOf2(open), lists: [] }] };
2268
+ const tz = options.tz === "" ? Intl.DateTimeFormat().resolvedOptions().timeZone : options.tz;
2269
+ if (options.workHours === "0-24") {
2270
+ return `${options.workDays} all hours ${tz}`;
2271
+ }
2272
+ return `${options.workDays} ${options.workHours} (${dailyHoursLabel(options.workHours)}) ${tz}`;
2097
2273
  }
2098
- function rowsOf(group) {
2099
- return toPrRows(
2100
- group,
2101
- group.map((entry) => durationLead(entry))
2102
- );
2274
+ function dailyHoursLabel(workHours) {
2275
+ const minutes = parseWorkHours(workHours).reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
2276
+ const hours = Math.round(minutes / 60 * 100) / 100;
2277
+ return `${hours} ${hours === 1 ? "hour" : "hours"}`;
2103
2278
  }
2104
2279
 
2105
2280
  // src/tui/components/ChartsPanel.tsx
@@ -2361,6 +2536,7 @@ function QueuePanel({
2361
2536
  const renderRow = (row, index, indent) => {
2362
2537
  const isSelected = index === cursor;
2363
2538
  const bg = isSelected ? theme.selectedBg : void 0;
2539
+ const fg = row.pending?.snoozed === true ? theme.muted : theme.text;
2364
2540
  const refStart = indent.length + 2 + row.lead.length + 2;
2365
2541
  return /* @__PURE__ */ jsxs5(
2366
2542
  "text",
@@ -2386,12 +2562,12 @@ function QueuePanel({
2386
2562
  indent,
2387
2563
  isSelected ? "\u25B8 " : " "
2388
2564
  ] }),
2389
- /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
2565
+ /* @__PURE__ */ jsxs5("span", { fg, bg, children: [
2390
2566
  row.lead,
2391
2567
  " "
2392
2568
  ] }),
2393
2569
  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 }),
2394
- /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
2570
+ /* @__PURE__ */ jsxs5("span", { fg, bg, children: [
2395
2571
  " ",
2396
2572
  row.title
2397
2573
  ] })
@@ -3229,13 +3405,13 @@ import { useRenderer as useRenderer2 } from "@opentui/react";
3229
3405
  import { homedir as homedir2 } from "node:os";
3230
3406
 
3231
3407
  // src/tui/data/export.ts
3232
- import { join as join4 } from "node:path";
3408
+ import { join as join5 } from "node:path";
3233
3409
 
3234
3410
  // src/github.ts
3235
3411
  import { execFile } from "node:child_process";
3236
3412
  import { createHash } from "node:crypto";
3237
3413
  import { statSync } from "node:fs";
3238
- import { join as join3, resolve } from "node:path";
3414
+ import { join as join4, resolve } from "node:path";
3239
3415
  import { promisify } from "node:util";
3240
3416
  var execFileAsync = promisify(execFile);
3241
3417
  var API_BASE = "https://api.github.com";
@@ -3245,7 +3421,7 @@ function resolveDebugBinary(input) {
3245
3421
  const resolved = resolve(input);
3246
3422
  const stats = statSync(resolved, { throwIfNoEntry: false });
3247
3423
  if (stats?.isDirectory()) {
3248
- const binary = join3(resolved, "gh");
3424
+ const binary = join4(resolved, "gh");
3249
3425
  if (!statSync(binary, { throwIfNoEntry: false })?.isFile()) {
3250
3426
  throw new CliError(`--debug directory "${input}" does not contain a gh executable`);
3251
3427
  }
@@ -4027,7 +4203,7 @@ function buildStatsReport(raw, options) {
4027
4203
  };
4028
4204
  }
4029
4205
  function exportFile() {
4030
- return join4(process.cwd(), "pr-stats.json");
4206
+ return join5(process.cwd(), "pr-stats.json");
4031
4207
  }
4032
4208
  function exportStatsFile(raw, options) {
4033
4209
  writeFileAtomic(exportFile(), `${JSON.stringify(buildStatsReport(raw, options), null, 2)}
@@ -4115,6 +4291,12 @@ var SETTINGS = [
4115
4291
  label: "Copy instead of open",
4116
4292
  hint: "enter and a click on a PR reference copy its link to the clipboard instead of opening the browser"
4117
4293
  },
4294
+ {
4295
+ key: "snoozeDuration",
4296
+ section: "Snooze",
4297
+ label: "Default snooze",
4298
+ hint: "the duration the snooze dialog starts with when s snoozes a PR, like 30m, 2h, or 1d \xB7 enter edits the value"
4299
+ },
4118
4300
  {
4119
4301
  key: "themePreset",
4120
4302
  section: "Theme",
@@ -4376,6 +4558,7 @@ function SettingsModal({
4376
4558
  notifications: notifications2,
4377
4559
  notifyChannel: notifyChannel2,
4378
4560
  copyLinks: copyLinks2,
4561
+ snoozeDuration: snoozeDuration2,
4379
4562
  preset,
4380
4563
  onDraft,
4381
4564
  onSubmit
@@ -4405,6 +4588,7 @@ function SettingsModal({
4405
4588
  channelValue,
4406
4589
  deliveryValue,
4407
4590
  copyLinks: copyLinks2,
4591
+ snoozeDuration: snoozeDuration2,
4408
4592
  preset,
4409
4593
  onDraft,
4410
4594
  onSubmit
@@ -4427,6 +4611,7 @@ function SettingValue({
4427
4611
  channelValue,
4428
4612
  deliveryValue,
4429
4613
  copyLinks: copyLinks2,
4614
+ snoozeDuration: snoozeDuration2,
4430
4615
  preset,
4431
4616
  onDraft,
4432
4617
  onSubmit
@@ -4459,6 +4644,12 @@ function SettingValue({
4459
4644
  case "copyLinks": {
4460
4645
  return /* @__PURE__ */ jsx12(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
4461
4646
  }
4647
+ case "snoozeDuration": {
4648
+ if (isEditing) {
4649
+ return /* @__PURE__ */ jsx12(ModalInput, { width: 16, value: snoozeDuration2, onDraft, onSubmit });
4650
+ }
4651
+ return /* @__PURE__ */ jsx12(IntervalValue, { value: snoozeDuration2, active: true, isSelected });
4652
+ }
4462
4653
  case "themePreset": {
4463
4654
  return /* @__PURE__ */ jsx12(ToggleValue, { value: preset, isSelected });
4464
4655
  }
@@ -4502,8 +4693,42 @@ function PathValue({ path, confirming, isSelected }) {
4502
4693
  return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
4503
4694
  }
4504
4695
 
4696
+ // src/tui/components/SnoozeModal.tsx
4697
+ import { jsx as jsx13, jsxs as jsxs12 } from "@opentui/react/jsx-runtime";
4698
+ var HINT = "how long to park the PR, like 30m, 2h, or 1d \xB7 enter snoozes \xB7 esc cancels";
4699
+ function SnoozeModal({
4700
+ target,
4701
+ snoozeDuration: snoozeDuration2,
4702
+ error,
4703
+ onDraft,
4704
+ onSubmit
4705
+ }) {
4706
+ return /* @__PURE__ */ jsxs12(ModalFrame, { title: "Snooze", children: [
4707
+ /* @__PURE__ */ jsxs12("text", { wrapMode: "word", height: 2, marginLeft: 2, marginRight: 2, marginBottom: 1, children: [
4708
+ /* @__PURE__ */ jsx13("b", { fg: theme.accent, children: target.ref }),
4709
+ /* @__PURE__ */ jsxs12("span", { fg: theme.muted, children: [
4710
+ " ",
4711
+ target.title
4712
+ ] })
4713
+ ] }),
4714
+ /* @__PURE__ */ jsx13(ModalRow, { label: "Snooze for", isSelected: true, children: /* @__PURE__ */ jsx13(ModalInput, { width: 16, value: snoozeDuration2, onDraft, onSubmit }) }),
4715
+ /* @__PURE__ */ jsx13(
4716
+ "text",
4717
+ {
4718
+ wrapMode: "word",
4719
+ height: 2,
4720
+ fg: error !== null ? theme.error : theme.muted,
4721
+ marginTop: 1,
4722
+ marginLeft: 2,
4723
+ marginRight: 2,
4724
+ children: error ?? HINT
4725
+ }
4726
+ )
4727
+ ] });
4728
+ }
4729
+
4505
4730
  // src/tui/components/ThemeModal.tsx
4506
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs12 } from "@opentui/react/jsx-runtime";
4731
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
4507
4732
  function ThemeModal({
4508
4733
  selected,
4509
4734
  editing,
@@ -4516,8 +4741,8 @@ function ThemeModal({
4516
4741
  const spec = THEME_COLORS[selected];
4517
4742
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
4518
4743
  const hint = spec.key in overrides ? `${spec.hint} \xB7 custom color, an empty value restores the theme` : spec.hint;
4519
- return /* @__PURE__ */ jsxs12(ModalFrame, { title: "Theme colors", children: [
4520
- /* @__PURE__ */ jsx13("box", { flexDirection: "column", marginBottom: 1, children: THEME_COLORS.map((color, index) => /* @__PURE__ */ jsx13(
4744
+ return /* @__PURE__ */ jsxs13(ModalFrame, { title: "Theme colors", children: [
4745
+ /* @__PURE__ */ jsx14("box", { flexDirection: "column", marginBottom: 1, children: THEME_COLORS.map((color, index) => /* @__PURE__ */ jsx14(
4521
4746
  ColorRow,
4522
4747
  {
4523
4748
  color,
@@ -4529,7 +4754,7 @@ function ThemeModal({
4529
4754
  },
4530
4755
  color.key
4531
4756
  )) }),
4532
- /* @__PURE__ */ jsx13("text", { wrapMode: "word", height: 2, fg: error !== null ? theme.error : theme.muted, marginLeft: 2, marginRight: 2, children: error ?? message?.text ?? hint })
4757
+ /* @__PURE__ */ jsx14("text", { wrapMode: "word", height: 2, fg: error !== null ? theme.error : theme.muted, marginLeft: 2, marginRight: 2, children: error ?? message?.text ?? hint })
4533
4758
  ] });
4534
4759
  }
4535
4760
  function ColorRow({
@@ -4542,20 +4767,20 @@ function ColorRow({
4542
4767
  }) {
4543
4768
  const value2 = themeColorText(color.key);
4544
4769
  const swatch = theme[color.key];
4545
- return /* @__PURE__ */ jsx13(ModalRow, { label: color.key, isSelected, children: isEditing ? /* @__PURE__ */ jsx13(ModalInput, { width: 36, value: value2, onDraft, onSubmit }) : /* @__PURE__ */ jsxs12("text", { wrapMode: "none", children: [
4546
- Array.isArray(swatch) ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
4547
- /* @__PURE__ */ jsx13("span", { fg: swatch[0], children: "\u2588" }),
4548
- /* @__PURE__ */ jsx13("span", { fg: swatch[1], children: "\u2588" }),
4549
- /* @__PURE__ */ jsx13("span", { fg: swatch[2], children: "\u2588" }),
4550
- /* @__PURE__ */ jsx13("span", { fg: swatch[3], children: "\u2588" })
4551
- ] }) : /* @__PURE__ */ jsx13("span", { fg: swatch, children: "\u2588\u2588" }),
4552
- /* @__PURE__ */ jsx13("span", { children: " " }),
4553
- isSelected ? /* @__PURE__ */ jsx13("b", { fg: theme.text, children: value2 }) : /* @__PURE__ */ jsx13("span", { fg: isCustom ? theme.text : theme.muted, children: value2 })
4770
+ return /* @__PURE__ */ jsx14(ModalRow, { label: color.key, isSelected, children: isEditing ? /* @__PURE__ */ jsx14(ModalInput, { width: 36, value: value2, onDraft, onSubmit }) : /* @__PURE__ */ jsxs13("text", { wrapMode: "none", children: [
4771
+ Array.isArray(swatch) ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
4772
+ /* @__PURE__ */ jsx14("span", { fg: swatch[0], children: "\u2588" }),
4773
+ /* @__PURE__ */ jsx14("span", { fg: swatch[1], children: "\u2588" }),
4774
+ /* @__PURE__ */ jsx14("span", { fg: swatch[2], children: "\u2588" }),
4775
+ /* @__PURE__ */ jsx14("span", { fg: swatch[3], children: "\u2588" })
4776
+ ] }) : /* @__PURE__ */ jsx14("span", { fg: swatch, children: "\u2588\u2588" }),
4777
+ /* @__PURE__ */ jsx14("span", { children: " " }),
4778
+ isSelected ? /* @__PURE__ */ jsx14("b", { fg: theme.text, children: value2 }) : /* @__PURE__ */ jsx14("span", { fg: isCustom ? theme.text : theme.muted, children: value2 })
4554
4779
  ] }) });
4555
4780
  }
4556
4781
 
4557
4782
  // src/tui/components/Modals.tsx
4558
- import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
4783
+ import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
4559
4784
  function Modals({
4560
4785
  ui,
4561
4786
  options,
@@ -4566,16 +4791,18 @@ function Modals({
4566
4791
  notifications: notifications2,
4567
4792
  notifyChannel: notifyChannel2,
4568
4793
  copyLinks: copyLinks2,
4794
+ snoozeDuration: snoozeDuration2,
4569
4795
  themeState,
4570
4796
  onDraft,
4571
4797
  onSubmitField,
4572
- onSubmitReloadInterval,
4798
+ onSubmitSetting,
4573
4799
  onSubmitThemeColor,
4800
+ onSubmitSnooze,
4574
4801
  onToggleReviewType,
4575
4802
  onToggleWorkDay
4576
4803
  }) {
4577
4804
  if (ui.modal === "options") {
4578
- return /* @__PURE__ */ jsx14(
4805
+ return /* @__PURE__ */ jsx15(
4579
4806
  OptionsModal,
4580
4807
  {
4581
4808
  options,
@@ -4591,7 +4818,7 @@ function Modals({
4591
4818
  );
4592
4819
  }
4593
4820
  if (ui.modal === "settings") {
4594
- return /* @__PURE__ */ jsx14(
4821
+ return /* @__PURE__ */ jsx15(
4595
4822
  SettingsModal,
4596
4823
  {
4597
4824
  selected: ui.selectedSetting,
@@ -4604,14 +4831,27 @@ function Modals({
4604
4831
  notifications: notifications2,
4605
4832
  notifyChannel: notifyChannel2,
4606
4833
  copyLinks: copyLinks2,
4834
+ snoozeDuration: snoozeDuration2,
4607
4835
  preset: themeState.preset,
4608
4836
  onDraft,
4609
- onSubmit: onSubmitReloadInterval
4837
+ onSubmit: onSubmitSetting
4838
+ }
4839
+ );
4840
+ }
4841
+ if (ui.modal === "snooze" && ui.snoozeTarget !== null) {
4842
+ return /* @__PURE__ */ jsx15(
4843
+ SnoozeModal,
4844
+ {
4845
+ target: ui.snoozeTarget,
4846
+ snoozeDuration: snoozeDuration2,
4847
+ error: ui.snoozeError,
4848
+ onDraft,
4849
+ onSubmit: onSubmitSnooze
4610
4850
  }
4611
4851
  );
4612
4852
  }
4613
4853
  if (ui.modal === "theme") {
4614
- return /* @__PURE__ */ jsx14(
4854
+ return /* @__PURE__ */ jsx15(
4615
4855
  ThemeModal,
4616
4856
  {
4617
4857
  selected: ui.selectedThemeColor,
@@ -4627,6 +4867,72 @@ function Modals({
4627
4867
  return null;
4628
4868
  }
4629
4869
 
4870
+ // src/tui/data/notifications.ts
4871
+ function diffReviewRequests(previous, results) {
4872
+ const baseline = /* @__PURE__ */ new Map();
4873
+ const reviewedKeys = /* @__PURE__ */ new Set();
4874
+ const pending = [];
4875
+ for (const result of results) {
4876
+ const key = `${result.pr.repo}#${result.pr.number}`;
4877
+ if (result.kind === "reviewed") {
4878
+ reviewedKeys.add(key);
4879
+ } else if (result.kind === "pending" && result.pr.state === "open") {
4880
+ const requestedAt = result.requestedAt.getTime();
4881
+ baseline.set(key, requestedAt);
4882
+ pending.push({ key, pr: result.pr, requestedAt });
4883
+ }
4884
+ }
4885
+ if (previous === null) {
4886
+ return { baseline, newRequests: [], reRequests: [] };
4887
+ }
4888
+ const newRequests = [];
4889
+ const reRequests = [];
4890
+ for (const { key, pr, requestedAt } of pending) {
4891
+ const before = previous.get(key);
4892
+ if (before !== void 0 && before >= requestedAt) {
4893
+ continue;
4894
+ }
4895
+ (reviewedKeys.has(key) ? reRequests : newRequests).push(pr);
4896
+ }
4897
+ return { baseline, newRequests, reRequests };
4898
+ }
4899
+ var MAX_LISTED = 3;
4900
+ var TEST_NOTIFICATION = {
4901
+ title: "pr-stats",
4902
+ body: "Desktop notifications are working. New review requests show up like this."
4903
+ };
4904
+ function describeReviewRequests(changes) {
4905
+ const notifications2 = [];
4906
+ if (changes.newRequests.length > 0) {
4907
+ notifications2.push(describe(changes.newRequests, "Review requested on", "new PRs awaiting your review"));
4908
+ }
4909
+ if (changes.reRequests.length > 0) {
4910
+ notifications2.push(describe(changes.reRequests, "Review re-requested on", "PRs came back for review"));
4911
+ }
4912
+ return notifications2;
4913
+ }
4914
+ function describeSnoozeWakeUps(prs) {
4915
+ if (prs.length === 0) {
4916
+ return [];
4917
+ }
4918
+ return [describe(prs, "Snooze ended on", "snoozed PRs are back in your queue")];
4919
+ }
4920
+ function describe(prs, singleTitle, pluralTitle) {
4921
+ if (prs.length === 1) {
4922
+ const [pr] = prs;
4923
+ return { title: `${singleTitle} ${refOf(pr)}`, body: pr.title };
4924
+ }
4925
+ const lines = prs.slice(0, MAX_LISTED).map((pr) => `${refOf(pr)} ${pr.title}`);
4926
+ const rest = prs.length - lines.length;
4927
+ if (rest > 0) {
4928
+ lines.push(`and ${rest} more`);
4929
+ }
4930
+ return { title: `${prs.length} ${pluralTitle}`, body: lines.join("\n") };
4931
+ }
4932
+ function refOf(pr) {
4933
+ return `${pr.repo}#${pr.number}`;
4934
+ }
4935
+
4630
4936
  // src/tui/hooks/useAutoReload.ts
4631
4937
  import { useEffect as useEffect4, useEffectEvent } from "react";
4632
4938
  function useAutoReload(intervalMs, loading, reload) {
@@ -4752,68 +5058,6 @@ function useLoader(options, noCache2, callbacks = {}) {
4752
5058
 
4753
5059
  // src/tui/hooks/useReviewNotifications.ts
4754
5060
  import { useRef as useRef5 } from "react";
4755
-
4756
- // src/tui/data/notifications.ts
4757
- function diffReviewRequests(previous, results) {
4758
- const baseline = /* @__PURE__ */ new Map();
4759
- const reviewedKeys = /* @__PURE__ */ new Set();
4760
- const pending = [];
4761
- for (const result of results) {
4762
- const key = `${result.pr.repo}#${result.pr.number}`;
4763
- if (result.kind === "reviewed") {
4764
- reviewedKeys.add(key);
4765
- } else if (result.kind === "pending" && result.pr.state === "open") {
4766
- const requestedAt = result.requestedAt.getTime();
4767
- baseline.set(key, requestedAt);
4768
- pending.push({ key, pr: result.pr, requestedAt });
4769
- }
4770
- }
4771
- if (previous === null) {
4772
- return { baseline, newRequests: [], reRequests: [] };
4773
- }
4774
- const newRequests = [];
4775
- const reRequests = [];
4776
- for (const { key, pr, requestedAt } of pending) {
4777
- const before = previous.get(key);
4778
- if (before !== void 0 && before >= requestedAt) {
4779
- continue;
4780
- }
4781
- (reviewedKeys.has(key) ? reRequests : newRequests).push(pr);
4782
- }
4783
- return { baseline, newRequests, reRequests };
4784
- }
4785
- var MAX_LISTED = 3;
4786
- var TEST_NOTIFICATION = {
4787
- title: "pr-stats",
4788
- body: "Desktop notifications are working. New review requests show up like this."
4789
- };
4790
- function describeReviewRequests(changes) {
4791
- const notifications2 = [];
4792
- if (changes.newRequests.length > 0) {
4793
- notifications2.push(describe(changes.newRequests, "Review requested on", "new PRs awaiting your review"));
4794
- }
4795
- if (changes.reRequests.length > 0) {
4796
- notifications2.push(describe(changes.reRequests, "Review re-requested on", "PRs came back for review"));
4797
- }
4798
- return notifications2;
4799
- }
4800
- function describe(prs, singleTitle, pluralTitle) {
4801
- if (prs.length === 1) {
4802
- const [pr] = prs;
4803
- return { title: `${singleTitle} ${refOf(pr)}`, body: pr.title };
4804
- }
4805
- const lines = prs.slice(0, MAX_LISTED).map((pr) => `${refOf(pr)} ${pr.title}`);
4806
- const rest = prs.length - lines.length;
4807
- if (rest > 0) {
4808
- lines.push(`and ${rest} more`);
4809
- }
4810
- return { title: `${prs.length} ${pluralTitle}`, body: lines.join("\n") };
4811
- }
4812
- function refOf(pr) {
4813
- return `${pr.repo}#${pr.number}`;
4814
- }
4815
-
4816
- // src/tui/hooks/useReviewNotifications.ts
4817
5061
  function useReviewNotifications(enabled2, notify, onError) {
4818
5062
  const baselineRef = useRef5(null);
4819
5063
  return (key, results) => {
@@ -4829,6 +5073,53 @@ function useReviewNotifications(enabled2, notify, onError) {
4829
5073
  };
4830
5074
  }
4831
5075
 
5076
+ // src/tui/hooks/useSnoozes.ts
5077
+ import { useState as useState4 } from "react";
5078
+ function useSnoozes(initial2) {
5079
+ const [snoozes2, setSnoozes] = useState4(initial2);
5080
+ const commit = (next) => {
5081
+ setSnoozes(next);
5082
+ return writeSnoozes(next);
5083
+ };
5084
+ return {
5085
+ snoozes: snoozes2,
5086
+ add: (snooze) => commit([...snoozes2.filter((entry) => entry.ref !== snooze.ref), snooze]),
5087
+ remove: (refs) => commit(snoozes2.filter((entry) => !refs.includes(entry.ref)))
5088
+ };
5089
+ }
5090
+
5091
+ // src/tui/hooks/useSnoozeWakeups.ts
5092
+ import { useEffect as useEffect7, useEffectEvent as useEffectEvent2 } from "react";
5093
+ var MAX_TIMER_MS = 2 ** 31 - 1;
5094
+ function useSnoozeWakeups(snoozes2, ready, onWakeUp) {
5095
+ const wake = useEffectEvent2(() => {
5096
+ onWakeUp();
5097
+ });
5098
+ const next = nextWakeUp(snoozes2);
5099
+ useEffect7(() => {
5100
+ if (!ready || next === null) {
5101
+ return void 0;
5102
+ }
5103
+ let timer;
5104
+ const arm = () => {
5105
+ timer = setTimeout(
5106
+ () => {
5107
+ if (Date.now() < next) {
5108
+ arm();
5109
+ return;
5110
+ }
5111
+ wake();
5112
+ },
5113
+ Math.min(Math.max(0, next - Date.now()), MAX_TIMER_MS)
5114
+ );
5115
+ };
5116
+ arm();
5117
+ return () => {
5118
+ clearTimeout(timer);
5119
+ };
5120
+ }, [ready, next]);
5121
+ }
5122
+
4832
5123
  // src/tui/hooks/useViewModel.ts
4833
5124
  import { useMemo } from "react";
4834
5125
 
@@ -4886,12 +5177,12 @@ function buildSizeRepoOptions(raw) {
4886
5177
  }
4887
5178
  function pendingDetail(counts) {
4888
5179
  const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
4889
- return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
5180
+ return awaiting + (counts.snoozed > 0 ? `, ${counts.snoozed} snoozed` : "") + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
4890
5181
  }
4891
- function buildPendingRepoOptions(raw) {
5182
+ function buildPendingRepoOptions(raw, snoozes2 = [], now = Date.now()) {
4892
5183
  const countsByRepo = /* @__PURE__ */ new Map();
4893
5184
  const countsOf = (repo) => {
4894
- const counts = countsByRepo.get(repo) ?? { awaiting: 0, reviewing: 0 };
5185
+ const counts = countsByRepo.get(repo) ?? { awaiting: 0, snoozed: 0, reviewing: 0 };
4895
5186
  countsByRepo.set(repo, counts);
4896
5187
  return counts;
4897
5188
  };
@@ -4902,18 +5193,23 @@ function buildPendingRepoOptions(raw) {
4902
5193
  return [];
4903
5194
  }
4904
5195
  const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
4905
- for (const entry of stats.pending) {
5196
+ const { awaiting, snoozed } = splitSnoozed(stats.pending, snoozes2, now);
5197
+ for (const entry of awaiting) {
4906
5198
  countsOf(entry.pr.repo).awaiting += 1;
4907
5199
  }
5200
+ for (const entry of snoozed) {
5201
+ countsOf(entry.pr.repo).snoozed += 1;
5202
+ }
4908
5203
  for (const entry of stats.reviewing) {
4909
5204
  countsOf(entry.pr.repo).reviewing += 1;
4910
5205
  }
4911
5206
  const entries = [...countsByRepo.entries()].toSorted(
4912
- (a, b) => b[1].awaiting - a[1].awaiting || b[1].reviewing - a[1].reviewing || a[0].localeCompare(b[0])
5207
+ (a, b) => b[1].awaiting - a[1].awaiting || b[1].snoozed - a[1].snoozed || b[1].reviewing - a[1].reviewing || a[0].localeCompare(b[0])
4913
5208
  );
4914
- const totals = { awaiting: 0, reviewing: 0 };
5209
+ const totals = { awaiting: 0, snoozed: 0, reviewing: 0 };
4915
5210
  for (const [, counts] of entries) {
4916
5211
  totals.awaiting += counts.awaiting;
5212
+ totals.snoozed += counts.snoozed;
4917
5213
  totals.reviewing += counts.reviewing;
4918
5214
  }
4919
5215
  return [
@@ -5087,10 +5383,10 @@ function buildBarsCard({ title, subtitle, rows, format, expanded = false }) {
5087
5383
  }
5088
5384
 
5089
5385
  // src/tui/views/charts/weeks.ts
5090
- var DAY_MS = 864e5;
5091
- var WEEK_MS = 7 * DAY_MS;
5386
+ var DAY_MS2 = 864e5;
5387
+ var WEEK_MS2 = 7 * DAY_MS2;
5092
5388
  function mondayOf(dayUtcMs) {
5093
- return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 * DAY_MS;
5389
+ return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 * DAY_MS2;
5094
5390
  }
5095
5391
  function dateLabel(ms) {
5096
5392
  return new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
@@ -5124,14 +5420,14 @@ function buildCumulativeCard({ title, series, legend }) {
5124
5420
  subtitle.push({ text: `, ${legend}`, fg: theme.muted });
5125
5421
  const mondays = series.flatMap((entry) => entry.dates.map((date) => weekOf(date)));
5126
5422
  const first = Math.min(...mondays);
5127
- const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
5423
+ const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
5128
5424
  if (weekCount < 2) {
5129
5425
  return { title, subtitle, lines: [[{ text: "not enough weeks to draw a trend", fg: theme.muted }]] };
5130
5426
  }
5131
5427
  const totals = series.map((entry) => {
5132
5428
  const weekly = Array.from({ length: weekCount }, () => 0);
5133
5429
  for (const date of entry.dates) {
5134
- weekly[(weekOf(date) - first) / WEEK_MS] += 1;
5430
+ weekly[(weekOf(date) - first) / WEEK_MS2] += 1;
5135
5431
  }
5136
5432
  let running = 0;
5137
5433
  return weekly.map((count2) => running += count2);
@@ -5184,7 +5480,7 @@ function buildCumulativeCard({ title, series, legend }) {
5184
5480
  yWidth + 2,
5185
5481
  weekCount,
5186
5482
  (week) => Math.round(week / (weekCount - 1) * (CUM_WIDTH - 1)),
5187
- (week) => first + week * WEEK_MS
5483
+ (week) => first + week * WEEK_MS2
5188
5484
  )
5189
5485
  );
5190
5486
  return { title, subtitle, lines };
@@ -5533,7 +5829,7 @@ function buildTrendCard({
5533
5829
  }
5534
5830
  const mondays = [...byWeek.keys()];
5535
5831
  const first = Math.min(...mondays);
5536
- const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
5832
+ const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
5537
5833
  const scaleSuffix = scale === "log" ? ", log scale" : "";
5538
5834
  if (weekCount < 2) {
5539
5835
  return {
@@ -5544,7 +5840,7 @@ function buildTrendCard({
5544
5840
  }
5545
5841
  const medians = [];
5546
5842
  for (let week = 0; week < weekCount; week++) {
5547
- const values = byWeek.get(first + week * WEEK_MS);
5843
+ const values = byWeek.get(first + week * WEEK_MS2);
5548
5844
  if (values !== void 0) {
5549
5845
  medians.push(
5550
5846
  percentile(
@@ -5606,7 +5902,7 @@ function buildTrendCard({
5606
5902
  prefix,
5607
5903
  points.length,
5608
5904
  (point) => point * stretch,
5609
- (point) => first + point * chunk * WEEK_MS
5905
+ (point) => first + point * chunk * WEEK_MS2
5610
5906
  )
5611
5907
  );
5612
5908
  const subtitle = chunk === 1 ? `weekly ${valueLabel}${scaleSuffix}` : `${valueLabel} per ${chunk} weeks${scaleSuffix}`;
@@ -5620,10 +5916,10 @@ var V_PARTIALS = [" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586
5620
5916
  function buildVolumeCard(title, dates, weights) {
5621
5917
  const mondays = dates.map((date) => mondayOf(zonedStamp(date).dayUtcMs));
5622
5918
  const first = Math.min(...mondays);
5623
- const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
5919
+ const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
5624
5920
  const weekly = Array.from({ length: weekCount }, () => 0);
5625
5921
  for (const [i, monday] of mondays.entries()) {
5626
- weekly[(monday - first) / WEEK_MS] += weights?.[i] ?? 1;
5922
+ weekly[(monday - first) / WEEK_MS2] += weights?.[i] ?? 1;
5627
5923
  }
5628
5924
  const chunk = Math.ceil(weekCount / VOLUME_MAX_BARS);
5629
5925
  const bars = [];
@@ -5661,7 +5957,7 @@ function buildVolumeCard(title, dates, weights) {
5661
5957
  yWidth + 2,
5662
5958
  bars.length,
5663
5959
  (bar) => bar * 3,
5664
- (bar) => first + bar * chunk * WEEK_MS
5960
+ (bar) => first + bar * chunk * WEEK_MS2
5665
5961
  )
5666
5962
  );
5667
5963
  const total = weights === void 0 ? dates.length : weights.reduce((sum, weight) => sum + weight, 0);
@@ -5690,7 +5986,7 @@ function weeklySums(entries) {
5690
5986
  const first = Math.min(...mondays);
5691
5987
  const last = Math.max(...mondays);
5692
5988
  const result = [];
5693
- for (let monday = first; monday <= last; monday += WEEK_MS) {
5989
+ for (let monday = first; monday <= last; monday += WEEK_MS2) {
5694
5990
  result.push({ date: mondayNoon(monday), value: byWeek.get(monday) ?? 0 });
5695
5991
  }
5696
5992
  return result;
@@ -6345,7 +6641,7 @@ function resolveScope(scope, repos) {
6345
6641
  }
6346
6642
  return dropVanishedRepo(scope, repos);
6347
6643
  }
6348
- function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoch) {
6644
+ function useViewModel(raw, options, width, scopes, grouping, expanded, snoozes2, themeEpoch) {
6349
6645
  return useMemo(() => {
6350
6646
  void themeEpoch;
6351
6647
  configureTimeMode({
@@ -6365,7 +6661,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
6365
6661
  percentile: options.targetPercentile === "" ? DEFAULT_TARGET_PERCENTILE : parseTargetPercentile(options.targetPercentile)
6366
6662
  };
6367
6663
  const sizeTarget = options.sizeTarget === "" ? void 0 : parseSizeTarget(options.sizeTarget);
6368
- const pendingRepos = buildPendingRepoOptions(raw);
6664
+ const pendingRepos = buildPendingRepoOptions(raw, snoozes2);
6369
6665
  const openRepos = buildOpenRepoOptions(raw);
6370
6666
  const mergedRepos = buildMergedRepoOptions(raw);
6371
6667
  const reviewRepos = buildReviewRepoOptions(raw);
@@ -6391,7 +6687,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
6391
6687
  reviewScope,
6392
6688
  sizeScope,
6393
6689
  commentScope,
6394
- pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending) : null,
6690
+ pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending, snoozes2) : null,
6395
6691
  open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
6396
6692
  merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width, expanded.merged) : null,
6397
6693
  review,
@@ -6418,6 +6714,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
6418
6714
  grouping.open,
6419
6715
  expanded.review,
6420
6716
  expanded.merged,
6717
+ snoozes2,
6421
6718
  themeEpoch
6422
6719
  ]);
6423
6720
  }
@@ -6548,6 +6845,13 @@ function handleSettingsModalKey(key, context) {
6548
6845
  context.dispatchUi({ type: "cacheActionReported", action: saveCopyLinks(next) ? "saved" : "notSaved" });
6549
6846
  break;
6550
6847
  }
6848
+ case "snoozeDuration": {
6849
+ if (key.name !== "return") {
6850
+ break;
6851
+ }
6852
+ context.beginEdit(context.snoozeDuration);
6853
+ break;
6854
+ }
6551
6855
  case "themePreset": {
6552
6856
  const next = cycleTheme(context.themeState, key.name === "left" ? -1 : 1);
6553
6857
  applyThemeState(next);
@@ -6696,6 +7000,23 @@ function handleQueueKey(key, context) {
6696
7000
  }
6697
7001
  break;
6698
7002
  }
7003
+ case "s": {
7004
+ const cursor = context.browse.rowCursors[tab];
7005
+ const row = rows[Math.min(cursor, rows.length - 1)];
7006
+ if (row.pending === void 0) {
7007
+ break;
7008
+ }
7009
+ if (row.pending.snoozed) {
7010
+ context.unsnooze(row.ref);
7011
+ break;
7012
+ }
7013
+ context.dispatchUi({
7014
+ type: "snoozeModalOpened",
7015
+ target: { ref: row.ref, title: row.title, requestedAt: row.pending.requestedAt }
7016
+ });
7017
+ context.beginEdit(context.snoozeDuration);
7018
+ break;
7019
+ }
6699
7020
  }
6700
7021
  }
6701
7022
  function statsTabOf(context) {
@@ -6791,7 +7112,7 @@ function handleAppKey(key, context) {
6791
7112
  }
6792
7113
  if (key.name === "o") {
6793
7114
  context.dispatchUi({ type: "modalOpened", modal: "options" });
6794
- } else if (key.name === "s") {
7115
+ } else if (key.name === "s" && key.shift) {
6795
7116
  context.dispatchUi({ type: "modalOpened", modal: "settings" });
6796
7117
  } else if (["1", "2", "3", "4", "5"].includes(key.name)) {
6797
7118
  context.dispatchBrowse({ type: "tabSelected", tab: Number(key.name) - 1 });
@@ -6818,6 +7139,8 @@ var initialUiState = {
6818
7139
  fieldError: null,
6819
7140
  settingError: null,
6820
7141
  themeColorError: null,
7142
+ snoozeTarget: null,
7143
+ snoozeError: null,
6821
7144
  cacheAction: null,
6822
7145
  openError: null,
6823
7146
  successNotice: null
@@ -6836,7 +7159,7 @@ function uiReducer(state, action) {
6836
7159
  case "openErrorReported": {
6837
7160
  return { ...state, openError: action.message };
6838
7161
  }
6839
- case "copyReported": {
7162
+ case "successReported": {
6840
7163
  return { ...state, openError: null, successNotice: { text: action.message } };
6841
7164
  }
6842
7165
  case "successNoticeExpired": {
@@ -6864,6 +7187,23 @@ function uiReducer(state, action) {
6864
7187
  case "themeModalClosed": {
6865
7188
  return { ...state, modal: "settings", themeColorError: null, cacheAction: null };
6866
7189
  }
7190
+ case "snoozeModalOpened": {
7191
+ return { ...state, modal: "snooze", editing: true, snoozeTarget: action.target, snoozeError: null };
7192
+ }
7193
+ case "snoozeCommitted": {
7194
+ const closed = { ...state, modal: null, editing: false, snoozeTarget: null, snoozeError: null };
7195
+ if (action.saved) {
7196
+ return { ...closed, openError: null, successNotice: { text: action.message } };
7197
+ }
7198
+ return {
7199
+ ...closed,
7200
+ openError: `${action.message} \xB7 the cache is disabled for this session, so the snooze is not saved`,
7201
+ successNotice: null
7202
+ };
7203
+ }
7204
+ case "snoozeErrorReported": {
7205
+ return { ...state, snoozeError: action.message };
7206
+ }
6867
7207
  case "fieldSelectionMoved": {
6868
7208
  return { ...state, selectedField: cycled2(state.selectedField, action.delta, FIELDS.length), fieldError: null };
6869
7209
  }
@@ -6887,6 +7227,9 @@ function uiReducer(state, action) {
6887
7227
  return { ...state, editing: true, fieldError: null, settingError: null, themeColorError: null };
6888
7228
  }
6889
7229
  case "editCancelled": {
7230
+ if (state.modal === "snooze") {
7231
+ return { ...state, modal: null, editing: false, snoozeTarget: null, snoozeError: null };
7232
+ }
6890
7233
  return { ...state, editing: false, fieldError: null, settingError: null, themeColorError: null };
6891
7234
  }
6892
7235
  case "fieldCommitted": {
@@ -6979,7 +7322,7 @@ function createClipboardCopier(renderer2) {
6979
7322
  }
6980
7323
 
6981
7324
  // src/tui/App.tsx
6982
- import { jsx as jsx15, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
7325
+ import { jsx as jsx16, jsxs as jsxs14 } from "@opentui/react/jsx-runtime";
6983
7326
  function App({
6984
7327
  initial: initial2,
6985
7328
  initialSaved = null,
@@ -6989,6 +7332,8 @@ function App({
6989
7332
  initialNotifications = false,
6990
7333
  initialNotifyChannel = "auto",
6991
7334
  initialCopyLinks = false,
7335
+ initialSnoozeDuration = DEFAULT_SNOOZE_DURATION,
7336
+ initialSnoozes = [],
6992
7337
  initialTheme = defaultThemeState(),
6993
7338
  openUrl = openInBrowser,
6994
7339
  copyUrl,
@@ -7001,15 +7346,17 @@ function App({
7001
7346
  const copyLink = copyUrl ?? defaultCopyUrl;
7002
7347
  const [ui, dispatchUi] = useReducer(uiReducer, initialUiState);
7003
7348
  const [browse, dispatchBrowse] = useReducer(browseReducer, initialBrowseState);
7004
- const [options, setOptions] = useState4(initial2);
7005
- const [saved2, setSaved] = useState4(initialSaved);
7006
- const [noCache2, setNoCache] = useState4(initialNoCache);
7007
- const [autoReload2, setAutoReload] = useState4(initialAutoReload);
7008
- const [reloadInterval2, setReloadInterval] = useState4(initialReloadInterval);
7009
- const [notifications2, setNotifications] = useState4(initialNotifications);
7010
- const [notifyChannel2, setNotifyChannel] = useState4(initialNotifyChannel);
7011
- const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
7012
- const [themeState, setThemeState] = useState4(initialTheme);
7349
+ const [options, setOptions] = useState5(initial2);
7350
+ const [saved2, setSaved] = useState5(initialSaved);
7351
+ const [noCache2, setNoCache] = useState5(initialNoCache);
7352
+ const [autoReload2, setAutoReload] = useState5(initialAutoReload);
7353
+ const [reloadInterval2, setReloadInterval] = useState5(initialReloadInterval);
7354
+ const [notifications2, setNotifications] = useState5(initialNotifications);
7355
+ const [notifyChannel2, setNotifyChannel] = useState5(initialNotifyChannel);
7356
+ const [copyLinks2, setCopyLinks] = useState5(initialCopyLinks);
7357
+ const [snoozeDuration2, setSnoozeDuration] = useState5(initialSnoozeDuration);
7358
+ const [themeState, setThemeState] = useState5(initialTheme);
7359
+ const snoozeStore = useSnoozes(initialSnoozes);
7013
7360
  const defaultNotify = useMemo2(
7014
7361
  () => createNotifier(notificationBoundary(renderer2), notifyChannel2),
7015
7362
  [renderer2, notifyChannel2]
@@ -7019,9 +7366,13 @@ function App({
7019
7366
  copyLink(row.url, (message) => {
7020
7367
  dispatchUi({ type: "openErrorReported", message });
7021
7368
  });
7022
- dispatchUi({ type: "copyReported", message: `copied ${row.ref} to the clipboard` });
7369
+ dispatchUi({ type: "successReported", message: `copied ${row.ref} to the clipboard` });
7023
7370
  };
7024
- useEffect7(() => {
7371
+ const unsnooze = (ref) => {
7372
+ snoozeStore.remove([ref]);
7373
+ dispatchUi({ type: "successReported", message: `unsnoozed ${ref}` });
7374
+ };
7375
+ useEffect8(() => {
7025
7376
  if (ui.successNotice === null) {
7026
7377
  return void 0;
7027
7378
  }
@@ -7059,7 +7410,35 @@ function App({
7059
7410
  }
7060
7411
  });
7061
7412
  useAutoReload(autoReload2 ? reloadIntervalMs(reloadInterval2) : null, loading, reload);
7062
- const views = useViewModel(raw, options, width, browse.scopes, browse.grouped, browse.expanded, themeState);
7413
+ useSnoozeWakeups(snoozeStore.snoozes, raw !== null, () => {
7414
+ if (raw === null) {
7415
+ return;
7416
+ }
7417
+ const due = dueSnoozes(snoozeStore.snoozes, Date.now());
7418
+ if (due.length === 0) {
7419
+ return;
7420
+ }
7421
+ const woken = wokenPrs(due, raw.reviewResults);
7422
+ snoozeStore.remove(due.map((snooze) => snooze.ref));
7423
+ if (!notifications2) {
7424
+ return;
7425
+ }
7426
+ for (const notification of describeSnoozeWakeUps(woken)) {
7427
+ notifier(notification.title, notification.body, (message) => {
7428
+ dispatchUi({ type: "openErrorReported", message });
7429
+ });
7430
+ }
7431
+ });
7432
+ const views = useViewModel(
7433
+ raw,
7434
+ options,
7435
+ width,
7436
+ browse.scopes,
7437
+ browse.grouped,
7438
+ browse.expanded,
7439
+ snoozeStore.snoozes,
7440
+ themeState
7441
+ );
7063
7442
  const showLoad = useDeferredLoading(loading, isSnapshot ? { showDelay: 0 } : void 0);
7064
7443
  const draftRef = useRef6("");
7065
7444
  const commitField = () => {
@@ -7093,6 +7472,49 @@ function App({
7093
7472
  throw error2;
7094
7473
  }
7095
7474
  };
7475
+ const commitSnoozeDuration = () => {
7476
+ const value2 = draftRef.current.trim();
7477
+ try {
7478
+ parseSnoozeDuration(value2);
7479
+ setSnoozeDuration(value2);
7480
+ dispatchUi({ type: "settingCommitted", action: saveSnoozeDuration(value2) ? "saved" : "notSaved" });
7481
+ } catch (error2) {
7482
+ if (error2 instanceof CliError) {
7483
+ dispatchUi({ type: "settingErrorReported", message: error2.message });
7484
+ return;
7485
+ }
7486
+ throw error2;
7487
+ }
7488
+ };
7489
+ const commitSetting = () => {
7490
+ if (SETTINGS[ui.selectedSetting].key === "snoozeDuration") {
7491
+ commitSnoozeDuration();
7492
+ } else {
7493
+ commitReloadInterval();
7494
+ }
7495
+ };
7496
+ const commitSnooze = () => {
7497
+ const target = ui.snoozeTarget;
7498
+ if (target === null) {
7499
+ return;
7500
+ }
7501
+ const value2 = draftRef.current.trim();
7502
+ try {
7503
+ const until = Date.now() + parseSnoozeDuration(value2);
7504
+ const saved3 = snoozeStore.add({ ref: target.ref, until, requestedAt: target.requestedAt });
7505
+ dispatchUi({
7506
+ type: "snoozeCommitted",
7507
+ message: `snoozed ${target.ref} until ${formatWakeTime(until)}`,
7508
+ saved: saved3
7509
+ });
7510
+ } catch (error2) {
7511
+ if (error2 instanceof CliError) {
7512
+ dispatchUi({ type: "snoozeErrorReported", message: error2.message });
7513
+ return;
7514
+ }
7515
+ throw error2;
7516
+ }
7517
+ };
7096
7518
  const commitThemeColor = () => {
7097
7519
  try {
7098
7520
  const next = withColorOverride(themeState, THEME_COLORS[ui.selectedThemeColor].key, draftRef.current.trim());
@@ -7120,6 +7542,7 @@ function App({
7120
7542
  notifications: notifications2,
7121
7543
  notifyChannel: notifyChannel2,
7122
7544
  copyLinks: copyLinks2,
7545
+ snoozeDuration: snoozeDuration2,
7123
7546
  themeState,
7124
7547
  options,
7125
7548
  views,
@@ -7139,6 +7562,7 @@ function App({
7139
7562
  openUrl,
7140
7563
  notify: notifier,
7141
7564
  copyRow,
7565
+ unsnooze,
7142
7566
  beginEdit: (value2) => {
7143
7567
  draftRef.current = value2;
7144
7568
  dispatchUi({ type: "editStarted" });
@@ -7150,8 +7574,8 @@ function App({
7150
7574
  });
7151
7575
  });
7152
7576
  const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
7153
- return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
7154
- /* @__PURE__ */ jsx15(
7577
+ return /* @__PURE__ */ jsxs14("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
7578
+ /* @__PURE__ */ jsx16(
7155
7579
  Header,
7156
7580
  {
7157
7581
  options,
@@ -7161,8 +7585,8 @@ function App({
7161
7585
  reloadEvery: autoReload2 ? reloadInterval2 : null
7162
7586
  }
7163
7587
  ),
7164
- /* @__PURE__ */ jsx15(TabBar, { tab: browse.tab }),
7165
- /* @__PURE__ */ jsx15(
7588
+ /* @__PURE__ */ jsx16(TabBar, { tab: browse.tab }),
7589
+ /* @__PURE__ */ jsx16(
7166
7590
  MainPanel,
7167
7591
  {
7168
7592
  views,
@@ -7181,7 +7605,7 @@ function App({
7181
7605
  onRefClick: copyLinks2 ? copyRow : null
7182
7606
  }
7183
7607
  ),
7184
- /* @__PURE__ */ jsx15(
7608
+ /* @__PURE__ */ jsx16(
7185
7609
  Footer,
7186
7610
  {
7187
7611
  width,
@@ -7190,13 +7614,14 @@ function App({
7190
7614
  tab: browse.tab,
7191
7615
  authoredTab: browse.authoredTab,
7192
7616
  views,
7617
+ pendingCursor: browse.rowCursors.pending,
7193
7618
  copyLinks: copyLinks2,
7194
7619
  openError: ui.openError,
7195
7620
  successNotice: ui.successNotice === null ? null : ui.successNotice.text,
7196
7621
  stale
7197
7622
  }
7198
7623
  ),
7199
- /* @__PURE__ */ jsx15(
7624
+ /* @__PURE__ */ jsx16(
7200
7625
  Modals,
7201
7626
  {
7202
7627
  ui,
@@ -7208,13 +7633,15 @@ function App({
7208
7633
  notifications: notifications2,
7209
7634
  notifyChannel: notifyChannel2,
7210
7635
  copyLinks: copyLinks2,
7636
+ snoozeDuration: snoozeDuration2,
7211
7637
  themeState,
7212
7638
  onDraft: (value2) => {
7213
7639
  draftRef.current = value2;
7214
7640
  },
7215
7641
  onSubmitField: commitField,
7216
- onSubmitReloadInterval: commitReloadInterval,
7642
+ onSubmitSetting: commitSetting,
7217
7643
  onSubmitThemeColor: commitThemeColor,
7644
+ onSubmitSnooze: commitSnooze,
7218
7645
  onToggleReviewType: (type) => {
7219
7646
  setOptions((previous) => {
7220
7647
  return { ...previous, reviewTypes: toggleReviewType(previous.reviewTypes, type) };
@@ -7272,6 +7699,8 @@ function bootstrap() {
7272
7699
  notifications: settings.notifications === true,
7273
7700
  notifyChannel: settings.notifyChannel ?? "auto",
7274
7701
  copyLinks: settings.copyLinks === true,
7702
+ snoozeDuration: settings.snoozeDuration ?? DEFAULT_SNOOZE_DURATION,
7703
+ snoozes: readSnoozes(),
7275
7704
  theme: theme3,
7276
7705
  json: values.json
7277
7706
  };
@@ -7284,8 +7713,21 @@ function bootstrap() {
7284
7713
  }
7285
7714
 
7286
7715
  // src/tui/main.tsx
7287
- import { jsx as jsx16 } from "@opentui/react/jsx-runtime";
7288
- var { initial, saved, noCache, autoReload, reloadInterval, notifications, notifyChannel, copyLinks, theme: theme2, json } = bootstrap();
7716
+ import { jsx as jsx17 } from "@opentui/react/jsx-runtime";
7717
+ var {
7718
+ initial,
7719
+ saved,
7720
+ noCache,
7721
+ autoReload,
7722
+ reloadInterval,
7723
+ notifications,
7724
+ notifyChannel,
7725
+ copyLinks,
7726
+ snoozeDuration,
7727
+ snoozes,
7728
+ theme: theme2,
7729
+ json
7730
+ } = bootstrap();
7289
7731
  if (json) {
7290
7732
  await runJsonStats(initial, noCache);
7291
7733
  }
@@ -7298,7 +7740,7 @@ for (const signal of ["SIGTERM", "SIGHUP"]) {
7298
7740
  });
7299
7741
  }
7300
7742
  createRoot(renderer).render(
7301
- /* @__PURE__ */ jsx16(
7743
+ /* @__PURE__ */ jsx17(
7302
7744
  App,
7303
7745
  {
7304
7746
  initial,
@@ -7309,6 +7751,8 @@ createRoot(renderer).render(
7309
7751
  initialNotifications: notifications,
7310
7752
  initialNotifyChannel: notifyChannel,
7311
7753
  initialCopyLinks: copyLinks,
7754
+ initialSnoozeDuration: snoozeDuration,
7755
+ initialSnoozes: snoozes,
7312
7756
  initialTheme: theme2,
7313
7757
  onQuit: () => {
7314
7758
  renderer.destroy();