@d3lm/pr-stats 0.2.18 → 0.2.20

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`;
@@ -1226,122 +1849,14 @@ var applyStyle = (self, string) => {
1226
1849
  string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
1227
1850
  }
1228
1851
  return openAll + string + closeAll;
1229
- };
1230
- 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
- }
1852
+ };
1853
+ Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
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;
@@ -1495,611 +2010,271 @@ function renderHelp() {
1495
2010
  const body = wrapMarkup(option.help, HELP_WIDTH - column).map((line) => colorizeHelp(line));
1496
2011
  lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
1497
2012
  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
- ] });
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
  }
@@ -3377,6 +3553,9 @@ async function searchPrsViaApi({ user, sinceIso, repos, includeDrafts, mode }) {
3377
3553
  authored: "author"
3378
3554
  }[mode];
3379
3555
  const terms = ["type:pr", `${qualifier}:${user}`, `created:>=${sinceIso}`];
3556
+ if (mode !== "authored") {
3557
+ terms.push(`-author:${user}`);
3558
+ }
3380
3559
  if (!includeDrafts) {
3381
3560
  terms.push("draft:false");
3382
3561
  }
@@ -3433,6 +3612,9 @@ async function searchPrs({ user, sinceIso, repos, includeDrafts, mode }) {
3433
3612
  for (const repo of repos) {
3434
3613
  args.push("--repo", repo);
3435
3614
  }
3615
+ if (mode !== "authored") {
3616
+ args.push("--", `-author:${user}`);
3617
+ }
3436
3618
  return JSON.parse(await gh(args));
3437
3619
  }
3438
3620
  async function fetchPrDetails(prs) {
@@ -4021,7 +4203,7 @@ function buildStatsReport(raw, options) {
4021
4203
  };
4022
4204
  }
4023
4205
  function exportFile() {
4024
- return join4(process.cwd(), "pr-stats.json");
4206
+ return join5(process.cwd(), "pr-stats.json");
4025
4207
  }
4026
4208
  function exportStatsFile(raw, options) {
4027
4209
  writeFileAtomic(exportFile(), `${JSON.stringify(buildStatsReport(raw, options), null, 2)}
@@ -4109,6 +4291,12 @@ var SETTINGS = [
4109
4291
  label: "Copy instead of open",
4110
4292
  hint: "enter and a click on a PR reference copy its link to the clipboard instead of opening the browser"
4111
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
+ },
4112
4300
  {
4113
4301
  key: "themePreset",
4114
4302
  section: "Theme",
@@ -4370,6 +4558,7 @@ function SettingsModal({
4370
4558
  notifications: notifications2,
4371
4559
  notifyChannel: notifyChannel2,
4372
4560
  copyLinks: copyLinks2,
4561
+ snoozeDuration: snoozeDuration2,
4373
4562
  preset,
4374
4563
  onDraft,
4375
4564
  onSubmit
@@ -4399,6 +4588,7 @@ function SettingsModal({
4399
4588
  channelValue,
4400
4589
  deliveryValue,
4401
4590
  copyLinks: copyLinks2,
4591
+ snoozeDuration: snoozeDuration2,
4402
4592
  preset,
4403
4593
  onDraft,
4404
4594
  onSubmit
@@ -4421,6 +4611,7 @@ function SettingValue({
4421
4611
  channelValue,
4422
4612
  deliveryValue,
4423
4613
  copyLinks: copyLinks2,
4614
+ snoozeDuration: snoozeDuration2,
4424
4615
  preset,
4425
4616
  onDraft,
4426
4617
  onSubmit
@@ -4453,6 +4644,12 @@ function SettingValue({
4453
4644
  case "copyLinks": {
4454
4645
  return /* @__PURE__ */ jsx12(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
4455
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
+ }
4456
4653
  case "themePreset": {
4457
4654
  return /* @__PURE__ */ jsx12(ToggleValue, { value: preset, isSelected });
4458
4655
  }
@@ -4496,8 +4693,42 @@ function PathValue({ path, confirming, isSelected }) {
4496
4693
  return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
4497
4694
  }
4498
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
+
4499
4730
  // src/tui/components/ThemeModal.tsx
4500
- 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";
4501
4732
  function ThemeModal({
4502
4733
  selected,
4503
4734
  editing,
@@ -4510,8 +4741,8 @@ function ThemeModal({
4510
4741
  const spec = THEME_COLORS[selected];
4511
4742
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
4512
4743
  const hint = spec.key in overrides ? `${spec.hint} \xB7 custom color, an empty value restores the theme` : spec.hint;
4513
- return /* @__PURE__ */ jsxs12(ModalFrame, { title: "Theme colors", children: [
4514
- /* @__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(
4515
4746
  ColorRow,
4516
4747
  {
4517
4748
  color,
@@ -4523,7 +4754,7 @@ function ThemeModal({
4523
4754
  },
4524
4755
  color.key
4525
4756
  )) }),
4526
- /* @__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 })
4527
4758
  ] });
4528
4759
  }
4529
4760
  function ColorRow({
@@ -4536,20 +4767,20 @@ function ColorRow({
4536
4767
  }) {
4537
4768
  const value2 = themeColorText(color.key);
4538
4769
  const swatch = theme[color.key];
4539
- 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: [
4540
- Array.isArray(swatch) ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
4541
- /* @__PURE__ */ jsx13("span", { fg: swatch[0], children: "\u2588" }),
4542
- /* @__PURE__ */ jsx13("span", { fg: swatch[1], children: "\u2588" }),
4543
- /* @__PURE__ */ jsx13("span", { fg: swatch[2], children: "\u2588" }),
4544
- /* @__PURE__ */ jsx13("span", { fg: swatch[3], children: "\u2588" })
4545
- ] }) : /* @__PURE__ */ jsx13("span", { fg: swatch, children: "\u2588\u2588" }),
4546
- /* @__PURE__ */ jsx13("span", { children: " " }),
4547
- 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 })
4548
4779
  ] }) });
4549
4780
  }
4550
4781
 
4551
4782
  // src/tui/components/Modals.tsx
4552
- import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
4783
+ import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
4553
4784
  function Modals({
4554
4785
  ui,
4555
4786
  options,
@@ -4560,16 +4791,18 @@ function Modals({
4560
4791
  notifications: notifications2,
4561
4792
  notifyChannel: notifyChannel2,
4562
4793
  copyLinks: copyLinks2,
4794
+ snoozeDuration: snoozeDuration2,
4563
4795
  themeState,
4564
4796
  onDraft,
4565
4797
  onSubmitField,
4566
- onSubmitReloadInterval,
4798
+ onSubmitSetting,
4567
4799
  onSubmitThemeColor,
4800
+ onSubmitSnooze,
4568
4801
  onToggleReviewType,
4569
4802
  onToggleWorkDay
4570
4803
  }) {
4571
4804
  if (ui.modal === "options") {
4572
- return /* @__PURE__ */ jsx14(
4805
+ return /* @__PURE__ */ jsx15(
4573
4806
  OptionsModal,
4574
4807
  {
4575
4808
  options,
@@ -4585,7 +4818,7 @@ function Modals({
4585
4818
  );
4586
4819
  }
4587
4820
  if (ui.modal === "settings") {
4588
- return /* @__PURE__ */ jsx14(
4821
+ return /* @__PURE__ */ jsx15(
4589
4822
  SettingsModal,
4590
4823
  {
4591
4824
  selected: ui.selectedSetting,
@@ -4598,14 +4831,27 @@ function Modals({
4598
4831
  notifications: notifications2,
4599
4832
  notifyChannel: notifyChannel2,
4600
4833
  copyLinks: copyLinks2,
4834
+ snoozeDuration: snoozeDuration2,
4601
4835
  preset: themeState.preset,
4602
4836
  onDraft,
4603
- 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
4604
4850
  }
4605
4851
  );
4606
4852
  }
4607
4853
  if (ui.modal === "theme") {
4608
- return /* @__PURE__ */ jsx14(
4854
+ return /* @__PURE__ */ jsx15(
4609
4855
  ThemeModal,
4610
4856
  {
4611
4857
  selected: ui.selectedThemeColor,
@@ -4621,6 +4867,72 @@ function Modals({
4621
4867
  return null;
4622
4868
  }
4623
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
+
4624
4936
  // src/tui/hooks/useAutoReload.ts
4625
4937
  import { useEffect as useEffect4, useEffectEvent } from "react";
4626
4938
  function useAutoReload(intervalMs, loading, reload) {
@@ -4746,68 +5058,6 @@ function useLoader(options, noCache2, callbacks = {}) {
4746
5058
 
4747
5059
  // src/tui/hooks/useReviewNotifications.ts
4748
5060
  import { useRef as useRef5 } from "react";
4749
-
4750
- // src/tui/data/notifications.ts
4751
- function diffReviewRequests(previous, results) {
4752
- const baseline = /* @__PURE__ */ new Map();
4753
- const reviewedKeys = /* @__PURE__ */ new Set();
4754
- const pending = [];
4755
- for (const result of results) {
4756
- const key = `${result.pr.repo}#${result.pr.number}`;
4757
- if (result.kind === "reviewed") {
4758
- reviewedKeys.add(key);
4759
- } else if (result.kind === "pending" && result.pr.state === "open") {
4760
- const requestedAt = result.requestedAt.getTime();
4761
- baseline.set(key, requestedAt);
4762
- pending.push({ key, pr: result.pr, requestedAt });
4763
- }
4764
- }
4765
- if (previous === null) {
4766
- return { baseline, newRequests: [], reRequests: [] };
4767
- }
4768
- const newRequests = [];
4769
- const reRequests = [];
4770
- for (const { key, pr, requestedAt } of pending) {
4771
- const before = previous.get(key);
4772
- if (before !== void 0 && before >= requestedAt) {
4773
- continue;
4774
- }
4775
- (reviewedKeys.has(key) ? reRequests : newRequests).push(pr);
4776
- }
4777
- return { baseline, newRequests, reRequests };
4778
- }
4779
- var MAX_LISTED = 3;
4780
- var TEST_NOTIFICATION = {
4781
- title: "pr-stats",
4782
- body: "Desktop notifications are working. New review requests show up like this."
4783
- };
4784
- function describeReviewRequests(changes) {
4785
- const notifications2 = [];
4786
- if (changes.newRequests.length > 0) {
4787
- notifications2.push(describe(changes.newRequests, "Review requested on", "new PRs awaiting your review"));
4788
- }
4789
- if (changes.reRequests.length > 0) {
4790
- notifications2.push(describe(changes.reRequests, "Review re-requested on", "PRs came back for review"));
4791
- }
4792
- return notifications2;
4793
- }
4794
- function describe(prs, singleTitle, pluralTitle) {
4795
- if (prs.length === 1) {
4796
- const [pr] = prs;
4797
- return { title: `${singleTitle} ${refOf(pr)}`, body: pr.title };
4798
- }
4799
- const lines = prs.slice(0, MAX_LISTED).map((pr) => `${refOf(pr)} ${pr.title}`);
4800
- const rest = prs.length - lines.length;
4801
- if (rest > 0) {
4802
- lines.push(`and ${rest} more`);
4803
- }
4804
- return { title: `${prs.length} ${pluralTitle}`, body: lines.join("\n") };
4805
- }
4806
- function refOf(pr) {
4807
- return `${pr.repo}#${pr.number}`;
4808
- }
4809
-
4810
- // src/tui/hooks/useReviewNotifications.ts
4811
5061
  function useReviewNotifications(enabled2, notify, onError) {
4812
5062
  const baselineRef = useRef5(null);
4813
5063
  return (key, results) => {
@@ -4823,6 +5073,53 @@ function useReviewNotifications(enabled2, notify, onError) {
4823
5073
  };
4824
5074
  }
4825
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
+
4826
5123
  // src/tui/hooks/useViewModel.ts
4827
5124
  import { useMemo } from "react";
4828
5125
 
@@ -4880,12 +5177,12 @@ function buildSizeRepoOptions(raw) {
4880
5177
  }
4881
5178
  function pendingDetail(counts) {
4882
5179
  const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
4883
- return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
5180
+ return awaiting + (counts.snoozed > 0 ? `, ${counts.snoozed} snoozed` : "") + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
4884
5181
  }
4885
- function buildPendingRepoOptions(raw) {
5182
+ function buildPendingRepoOptions(raw, snoozes2 = [], now = Date.now()) {
4886
5183
  const countsByRepo = /* @__PURE__ */ new Map();
4887
5184
  const countsOf = (repo) => {
4888
- const counts = countsByRepo.get(repo) ?? { awaiting: 0, reviewing: 0 };
5185
+ const counts = countsByRepo.get(repo) ?? { awaiting: 0, snoozed: 0, reviewing: 0 };
4889
5186
  countsByRepo.set(repo, counts);
4890
5187
  return counts;
4891
5188
  };
@@ -4896,18 +5193,23 @@ function buildPendingRepoOptions(raw) {
4896
5193
  return [];
4897
5194
  }
4898
5195
  const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
4899
- for (const entry of stats.pending) {
5196
+ const { awaiting, snoozed } = splitSnoozed(stats.pending, snoozes2, now);
5197
+ for (const entry of awaiting) {
4900
5198
  countsOf(entry.pr.repo).awaiting += 1;
4901
5199
  }
5200
+ for (const entry of snoozed) {
5201
+ countsOf(entry.pr.repo).snoozed += 1;
5202
+ }
4902
5203
  for (const entry of stats.reviewing) {
4903
5204
  countsOf(entry.pr.repo).reviewing += 1;
4904
5205
  }
4905
5206
  const entries = [...countsByRepo.entries()].toSorted(
4906
- (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])
4907
5208
  );
4908
- const totals = { awaiting: 0, reviewing: 0 };
5209
+ const totals = { awaiting: 0, snoozed: 0, reviewing: 0 };
4909
5210
  for (const [, counts] of entries) {
4910
5211
  totals.awaiting += counts.awaiting;
5212
+ totals.snoozed += counts.snoozed;
4911
5213
  totals.reviewing += counts.reviewing;
4912
5214
  }
4913
5215
  return [
@@ -5081,10 +5383,10 @@ function buildBarsCard({ title, subtitle, rows, format, expanded = false }) {
5081
5383
  }
5082
5384
 
5083
5385
  // src/tui/views/charts/weeks.ts
5084
- var DAY_MS = 864e5;
5085
- var WEEK_MS = 7 * DAY_MS;
5386
+ var DAY_MS2 = 864e5;
5387
+ var WEEK_MS2 = 7 * DAY_MS2;
5086
5388
  function mondayOf(dayUtcMs) {
5087
- return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 * DAY_MS;
5389
+ return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 * DAY_MS2;
5088
5390
  }
5089
5391
  function dateLabel(ms) {
5090
5392
  return new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
@@ -5118,14 +5420,14 @@ function buildCumulativeCard({ title, series, legend }) {
5118
5420
  subtitle.push({ text: `, ${legend}`, fg: theme.muted });
5119
5421
  const mondays = series.flatMap((entry) => entry.dates.map((date) => weekOf(date)));
5120
5422
  const first = Math.min(...mondays);
5121
- const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
5423
+ const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
5122
5424
  if (weekCount < 2) {
5123
5425
  return { title, subtitle, lines: [[{ text: "not enough weeks to draw a trend", fg: theme.muted }]] };
5124
5426
  }
5125
5427
  const totals = series.map((entry) => {
5126
5428
  const weekly = Array.from({ length: weekCount }, () => 0);
5127
5429
  for (const date of entry.dates) {
5128
- weekly[(weekOf(date) - first) / WEEK_MS] += 1;
5430
+ weekly[(weekOf(date) - first) / WEEK_MS2] += 1;
5129
5431
  }
5130
5432
  let running = 0;
5131
5433
  return weekly.map((count2) => running += count2);
@@ -5178,7 +5480,7 @@ function buildCumulativeCard({ title, series, legend }) {
5178
5480
  yWidth + 2,
5179
5481
  weekCount,
5180
5482
  (week) => Math.round(week / (weekCount - 1) * (CUM_WIDTH - 1)),
5181
- (week) => first + week * WEEK_MS
5483
+ (week) => first + week * WEEK_MS2
5182
5484
  )
5183
5485
  );
5184
5486
  return { title, subtitle, lines };
@@ -5527,7 +5829,7 @@ function buildTrendCard({
5527
5829
  }
5528
5830
  const mondays = [...byWeek.keys()];
5529
5831
  const first = Math.min(...mondays);
5530
- const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
5832
+ const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
5531
5833
  const scaleSuffix = scale === "log" ? ", log scale" : "";
5532
5834
  if (weekCount < 2) {
5533
5835
  return {
@@ -5538,7 +5840,7 @@ function buildTrendCard({
5538
5840
  }
5539
5841
  const medians = [];
5540
5842
  for (let week = 0; week < weekCount; week++) {
5541
- const values = byWeek.get(first + week * WEEK_MS);
5843
+ const values = byWeek.get(first + week * WEEK_MS2);
5542
5844
  if (values !== void 0) {
5543
5845
  medians.push(
5544
5846
  percentile(
@@ -5600,7 +5902,7 @@ function buildTrendCard({
5600
5902
  prefix,
5601
5903
  points.length,
5602
5904
  (point) => point * stretch,
5603
- (point) => first + point * chunk * WEEK_MS
5905
+ (point) => first + point * chunk * WEEK_MS2
5604
5906
  )
5605
5907
  );
5606
5908
  const subtitle = chunk === 1 ? `weekly ${valueLabel}${scaleSuffix}` : `${valueLabel} per ${chunk} weeks${scaleSuffix}`;
@@ -5614,10 +5916,10 @@ var V_PARTIALS = [" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586
5614
5916
  function buildVolumeCard(title, dates, weights) {
5615
5917
  const mondays = dates.map((date) => mondayOf(zonedStamp(date).dayUtcMs));
5616
5918
  const first = Math.min(...mondays);
5617
- const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
5919
+ const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
5618
5920
  const weekly = Array.from({ length: weekCount }, () => 0);
5619
5921
  for (const [i, monday] of mondays.entries()) {
5620
- weekly[(monday - first) / WEEK_MS] += weights?.[i] ?? 1;
5922
+ weekly[(monday - first) / WEEK_MS2] += weights?.[i] ?? 1;
5621
5923
  }
5622
5924
  const chunk = Math.ceil(weekCount / VOLUME_MAX_BARS);
5623
5925
  const bars = [];
@@ -5655,7 +5957,7 @@ function buildVolumeCard(title, dates, weights) {
5655
5957
  yWidth + 2,
5656
5958
  bars.length,
5657
5959
  (bar) => bar * 3,
5658
- (bar) => first + bar * chunk * WEEK_MS
5960
+ (bar) => first + bar * chunk * WEEK_MS2
5659
5961
  )
5660
5962
  );
5661
5963
  const total = weights === void 0 ? dates.length : weights.reduce((sum, weight) => sum + weight, 0);
@@ -5684,7 +5986,7 @@ function weeklySums(entries) {
5684
5986
  const first = Math.min(...mondays);
5685
5987
  const last = Math.max(...mondays);
5686
5988
  const result = [];
5687
- for (let monday = first; monday <= last; monday += WEEK_MS) {
5989
+ for (let monday = first; monday <= last; monday += WEEK_MS2) {
5688
5990
  result.push({ date: mondayNoon(monday), value: byWeek.get(monday) ?? 0 });
5689
5991
  }
5690
5992
  return result;
@@ -6339,7 +6641,7 @@ function resolveScope(scope, repos) {
6339
6641
  }
6340
6642
  return dropVanishedRepo(scope, repos);
6341
6643
  }
6342
- function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoch) {
6644
+ function useViewModel(raw, options, width, scopes, grouping, expanded, snoozes2, themeEpoch) {
6343
6645
  return useMemo(() => {
6344
6646
  void themeEpoch;
6345
6647
  configureTimeMode({
@@ -6359,7 +6661,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
6359
6661
  percentile: options.targetPercentile === "" ? DEFAULT_TARGET_PERCENTILE : parseTargetPercentile(options.targetPercentile)
6360
6662
  };
6361
6663
  const sizeTarget = options.sizeTarget === "" ? void 0 : parseSizeTarget(options.sizeTarget);
6362
- const pendingRepos = buildPendingRepoOptions(raw);
6664
+ const pendingRepos = buildPendingRepoOptions(raw, snoozes2);
6363
6665
  const openRepos = buildOpenRepoOptions(raw);
6364
6666
  const mergedRepos = buildMergedRepoOptions(raw);
6365
6667
  const reviewRepos = buildReviewRepoOptions(raw);
@@ -6385,7 +6687,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
6385
6687
  reviewScope,
6386
6688
  sizeScope,
6387
6689
  commentScope,
6388
- pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending) : null,
6690
+ pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending, snoozes2) : null,
6389
6691
  open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
6390
6692
  merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width, expanded.merged) : null,
6391
6693
  review,
@@ -6412,6 +6714,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
6412
6714
  grouping.open,
6413
6715
  expanded.review,
6414
6716
  expanded.merged,
6717
+ snoozes2,
6415
6718
  themeEpoch
6416
6719
  ]);
6417
6720
  }
@@ -6542,6 +6845,13 @@ function handleSettingsModalKey(key, context) {
6542
6845
  context.dispatchUi({ type: "cacheActionReported", action: saveCopyLinks(next) ? "saved" : "notSaved" });
6543
6846
  break;
6544
6847
  }
6848
+ case "snoozeDuration": {
6849
+ if (key.name !== "return") {
6850
+ break;
6851
+ }
6852
+ context.beginEdit(context.snoozeDuration);
6853
+ break;
6854
+ }
6545
6855
  case "themePreset": {
6546
6856
  const next = cycleTheme(context.themeState, key.name === "left" ? -1 : 1);
6547
6857
  applyThemeState(next);
@@ -6690,6 +7000,23 @@ function handleQueueKey(key, context) {
6690
7000
  }
6691
7001
  break;
6692
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
+ }
6693
7020
  }
6694
7021
  }
6695
7022
  function statsTabOf(context) {
@@ -6785,7 +7112,7 @@ function handleAppKey(key, context) {
6785
7112
  }
6786
7113
  if (key.name === "o") {
6787
7114
  context.dispatchUi({ type: "modalOpened", modal: "options" });
6788
- } else if (key.name === "s") {
7115
+ } else if (key.name === "s" && key.shift) {
6789
7116
  context.dispatchUi({ type: "modalOpened", modal: "settings" });
6790
7117
  } else if (["1", "2", "3", "4", "5"].includes(key.name)) {
6791
7118
  context.dispatchBrowse({ type: "tabSelected", tab: Number(key.name) - 1 });
@@ -6812,6 +7139,8 @@ var initialUiState = {
6812
7139
  fieldError: null,
6813
7140
  settingError: null,
6814
7141
  themeColorError: null,
7142
+ snoozeTarget: null,
7143
+ snoozeError: null,
6815
7144
  cacheAction: null,
6816
7145
  openError: null,
6817
7146
  successNotice: null
@@ -6830,7 +7159,7 @@ function uiReducer(state, action) {
6830
7159
  case "openErrorReported": {
6831
7160
  return { ...state, openError: action.message };
6832
7161
  }
6833
- case "copyReported": {
7162
+ case "successReported": {
6834
7163
  return { ...state, openError: null, successNotice: { text: action.message } };
6835
7164
  }
6836
7165
  case "successNoticeExpired": {
@@ -6858,6 +7187,23 @@ function uiReducer(state, action) {
6858
7187
  case "themeModalClosed": {
6859
7188
  return { ...state, modal: "settings", themeColorError: null, cacheAction: null };
6860
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
+ }
6861
7207
  case "fieldSelectionMoved": {
6862
7208
  return { ...state, selectedField: cycled2(state.selectedField, action.delta, FIELDS.length), fieldError: null };
6863
7209
  }
@@ -6881,6 +7227,9 @@ function uiReducer(state, action) {
6881
7227
  return { ...state, editing: true, fieldError: null, settingError: null, themeColorError: null };
6882
7228
  }
6883
7229
  case "editCancelled": {
7230
+ if (state.modal === "snooze") {
7231
+ return { ...state, modal: null, editing: false, snoozeTarget: null, snoozeError: null };
7232
+ }
6884
7233
  return { ...state, editing: false, fieldError: null, settingError: null, themeColorError: null };
6885
7234
  }
6886
7235
  case "fieldCommitted": {
@@ -6973,7 +7322,7 @@ function createClipboardCopier(renderer2) {
6973
7322
  }
6974
7323
 
6975
7324
  // src/tui/App.tsx
6976
- 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";
6977
7326
  function App({
6978
7327
  initial: initial2,
6979
7328
  initialSaved = null,
@@ -6983,6 +7332,8 @@ function App({
6983
7332
  initialNotifications = false,
6984
7333
  initialNotifyChannel = "auto",
6985
7334
  initialCopyLinks = false,
7335
+ initialSnoozeDuration = DEFAULT_SNOOZE_DURATION,
7336
+ initialSnoozes = [],
6986
7337
  initialTheme = defaultThemeState(),
6987
7338
  openUrl = openInBrowser,
6988
7339
  copyUrl,
@@ -6995,15 +7346,17 @@ function App({
6995
7346
  const copyLink = copyUrl ?? defaultCopyUrl;
6996
7347
  const [ui, dispatchUi] = useReducer(uiReducer, initialUiState);
6997
7348
  const [browse, dispatchBrowse] = useReducer(browseReducer, initialBrowseState);
6998
- const [options, setOptions] = useState4(initial2);
6999
- const [saved2, setSaved] = useState4(initialSaved);
7000
- const [noCache2, setNoCache] = useState4(initialNoCache);
7001
- const [autoReload2, setAutoReload] = useState4(initialAutoReload);
7002
- const [reloadInterval2, setReloadInterval] = useState4(initialReloadInterval);
7003
- const [notifications2, setNotifications] = useState4(initialNotifications);
7004
- const [notifyChannel2, setNotifyChannel] = useState4(initialNotifyChannel);
7005
- const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
7006
- 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);
7007
7360
  const defaultNotify = useMemo2(
7008
7361
  () => createNotifier(notificationBoundary(renderer2), notifyChannel2),
7009
7362
  [renderer2, notifyChannel2]
@@ -7013,9 +7366,13 @@ function App({
7013
7366
  copyLink(row.url, (message) => {
7014
7367
  dispatchUi({ type: "openErrorReported", message });
7015
7368
  });
7016
- dispatchUi({ type: "copyReported", message: `copied ${row.ref} to the clipboard` });
7369
+ dispatchUi({ type: "successReported", message: `copied ${row.ref} to the clipboard` });
7017
7370
  };
7018
- useEffect7(() => {
7371
+ const unsnooze = (ref) => {
7372
+ snoozeStore.remove([ref]);
7373
+ dispatchUi({ type: "successReported", message: `unsnoozed ${ref}` });
7374
+ };
7375
+ useEffect8(() => {
7019
7376
  if (ui.successNotice === null) {
7020
7377
  return void 0;
7021
7378
  }
@@ -7053,7 +7410,35 @@ function App({
7053
7410
  }
7054
7411
  });
7055
7412
  useAutoReload(autoReload2 ? reloadIntervalMs(reloadInterval2) : null, loading, reload);
7056
- 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
+ );
7057
7442
  const showLoad = useDeferredLoading(loading, isSnapshot ? { showDelay: 0 } : void 0);
7058
7443
  const draftRef = useRef6("");
7059
7444
  const commitField = () => {
@@ -7087,6 +7472,49 @@ function App({
7087
7472
  throw error2;
7088
7473
  }
7089
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
+ };
7090
7518
  const commitThemeColor = () => {
7091
7519
  try {
7092
7520
  const next = withColorOverride(themeState, THEME_COLORS[ui.selectedThemeColor].key, draftRef.current.trim());
@@ -7114,6 +7542,7 @@ function App({
7114
7542
  notifications: notifications2,
7115
7543
  notifyChannel: notifyChannel2,
7116
7544
  copyLinks: copyLinks2,
7545
+ snoozeDuration: snoozeDuration2,
7117
7546
  themeState,
7118
7547
  options,
7119
7548
  views,
@@ -7133,6 +7562,7 @@ function App({
7133
7562
  openUrl,
7134
7563
  notify: notifier,
7135
7564
  copyRow,
7565
+ unsnooze,
7136
7566
  beginEdit: (value2) => {
7137
7567
  draftRef.current = value2;
7138
7568
  dispatchUi({ type: "editStarted" });
@@ -7144,8 +7574,8 @@ function App({
7144
7574
  });
7145
7575
  });
7146
7576
  const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
7147
- return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
7148
- /* @__PURE__ */ jsx15(
7577
+ return /* @__PURE__ */ jsxs14("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
7578
+ /* @__PURE__ */ jsx16(
7149
7579
  Header,
7150
7580
  {
7151
7581
  options,
@@ -7155,8 +7585,8 @@ function App({
7155
7585
  reloadEvery: autoReload2 ? reloadInterval2 : null
7156
7586
  }
7157
7587
  ),
7158
- /* @__PURE__ */ jsx15(TabBar, { tab: browse.tab }),
7159
- /* @__PURE__ */ jsx15(
7588
+ /* @__PURE__ */ jsx16(TabBar, { tab: browse.tab }),
7589
+ /* @__PURE__ */ jsx16(
7160
7590
  MainPanel,
7161
7591
  {
7162
7592
  views,
@@ -7175,7 +7605,7 @@ function App({
7175
7605
  onRefClick: copyLinks2 ? copyRow : null
7176
7606
  }
7177
7607
  ),
7178
- /* @__PURE__ */ jsx15(
7608
+ /* @__PURE__ */ jsx16(
7179
7609
  Footer,
7180
7610
  {
7181
7611
  width,
@@ -7184,13 +7614,14 @@ function App({
7184
7614
  tab: browse.tab,
7185
7615
  authoredTab: browse.authoredTab,
7186
7616
  views,
7617
+ pendingCursor: browse.rowCursors.pending,
7187
7618
  copyLinks: copyLinks2,
7188
7619
  openError: ui.openError,
7189
7620
  successNotice: ui.successNotice === null ? null : ui.successNotice.text,
7190
7621
  stale
7191
7622
  }
7192
7623
  ),
7193
- /* @__PURE__ */ jsx15(
7624
+ /* @__PURE__ */ jsx16(
7194
7625
  Modals,
7195
7626
  {
7196
7627
  ui,
@@ -7202,13 +7633,15 @@ function App({
7202
7633
  notifications: notifications2,
7203
7634
  notifyChannel: notifyChannel2,
7204
7635
  copyLinks: copyLinks2,
7636
+ snoozeDuration: snoozeDuration2,
7205
7637
  themeState,
7206
7638
  onDraft: (value2) => {
7207
7639
  draftRef.current = value2;
7208
7640
  },
7209
7641
  onSubmitField: commitField,
7210
- onSubmitReloadInterval: commitReloadInterval,
7642
+ onSubmitSetting: commitSetting,
7211
7643
  onSubmitThemeColor: commitThemeColor,
7644
+ onSubmitSnooze: commitSnooze,
7212
7645
  onToggleReviewType: (type) => {
7213
7646
  setOptions((previous) => {
7214
7647
  return { ...previous, reviewTypes: toggleReviewType(previous.reviewTypes, type) };
@@ -7266,6 +7699,8 @@ function bootstrap() {
7266
7699
  notifications: settings.notifications === true,
7267
7700
  notifyChannel: settings.notifyChannel ?? "auto",
7268
7701
  copyLinks: settings.copyLinks === true,
7702
+ snoozeDuration: settings.snoozeDuration ?? DEFAULT_SNOOZE_DURATION,
7703
+ snoozes: readSnoozes(),
7269
7704
  theme: theme3,
7270
7705
  json: values.json
7271
7706
  };
@@ -7278,8 +7713,21 @@ function bootstrap() {
7278
7713
  }
7279
7714
 
7280
7715
  // src/tui/main.tsx
7281
- import { jsx as jsx16 } from "@opentui/react/jsx-runtime";
7282
- 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();
7283
7731
  if (json) {
7284
7732
  await runJsonStats(initial, noCache);
7285
7733
  }
@@ -7292,7 +7740,7 @@ for (const signal of ["SIGTERM", "SIGHUP"]) {
7292
7740
  });
7293
7741
  }
7294
7742
  createRoot(renderer).render(
7295
- /* @__PURE__ */ jsx16(
7743
+ /* @__PURE__ */ jsx17(
7296
7744
  App,
7297
7745
  {
7298
7746
  initial,
@@ -7303,6 +7751,8 @@ createRoot(renderer).render(
7303
7751
  initialNotifications: notifications,
7304
7752
  initialNotifyChannel: notifyChannel,
7305
7753
  initialCopyLinks: copyLinks,
7754
+ initialSnoozeDuration: snoozeDuration,
7755
+ initialSnoozes: snoozes,
7306
7756
  initialTheme: theme2,
7307
7757
  onQuit: () => {
7308
7758
  renderer.destroy();