@d3lm/pr-stats 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui-app.mjs
CHANGED
|
@@ -136,11 +136,11 @@ import { createRoot } from "@opentui/react";
|
|
|
136
136
|
|
|
137
137
|
// src/tui/App.tsx
|
|
138
138
|
import { useKeyboard, useRenderer as useRenderer3, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
|
|
139
|
-
import { useEffect as
|
|
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
|
|
143
|
-
import { join as
|
|
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
|
|
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 =
|
|
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/
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
|
632
|
-
|
|
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
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
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
|
|
830
|
+
return total;
|
|
671
831
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
704
|
-
|
|
839
|
+
// src/compute.ts
|
|
840
|
+
function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
|
|
841
|
+
const reviewed = [];
|
|
842
|
+
const pending = [];
|
|
843
|
+
for (const result of results) {
|
|
844
|
+
if (result.kind === "reviewed") {
|
|
845
|
+
reviewed.push({
|
|
846
|
+
pr: result.pr,
|
|
847
|
+
requestedAt: result.requestedAt,
|
|
848
|
+
reviewedAt: result.reviewedAt,
|
|
849
|
+
hours: durationHours(result.requestedAt, result.reviewedAt),
|
|
850
|
+
verdict: result.verdict,
|
|
851
|
+
lines: result.lines
|
|
852
|
+
});
|
|
853
|
+
} else if (result.kind === "pending" && result.pr.state === "open") {
|
|
854
|
+
pending.push({ pr: result.pr, requestedAt: result.requestedAt, hours: durationHours(result.requestedAt, now) });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
pending.sort((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
|
|
858
|
+
const pendingKeys = new Set(pending.map((entry) => `${entry.pr.repo}#${entry.pr.number}`));
|
|
859
|
+
const latestReviews = /* @__PURE__ */ new Map();
|
|
860
|
+
for (const result of results) {
|
|
861
|
+
if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const key = `${result.pr.repo}#${result.pr.number}`;
|
|
865
|
+
if (pendingKeys.has(key)) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const latest = latestReviews.get(key);
|
|
869
|
+
if (latest === void 0 || result.reviewedAt > latest.reviewedAt) {
|
|
870
|
+
latestReviews.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const reviewing = [...latestReviews.values()].map(({ pr, reviewedAt }) => {
|
|
874
|
+
return { pr, reviewedAt, hours: durationHours(reviewedAt, now) };
|
|
875
|
+
}).toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
|
|
876
|
+
const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
|
|
877
|
+
const unrequested = results.filter((result) => result.kind === "unrequested");
|
|
878
|
+
const allHours = reviewed.map((result) => result.hours);
|
|
879
|
+
const hoursByRepo = /* @__PURE__ */ new Map();
|
|
880
|
+
for (const result of reviewed) {
|
|
881
|
+
const hours = hoursByRepo.get(result.pr.repo) ?? [];
|
|
882
|
+
hours.push(result.hours);
|
|
883
|
+
hoursByRepo.set(result.pr.repo, hours);
|
|
884
|
+
}
|
|
885
|
+
const byRepo = [...hoursByRepo.entries()].toSorted((a, b) => b[1].length - a[1].length);
|
|
886
|
+
const misses = targetHours === void 0 ? [] : reviewed.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
|
|
887
|
+
const cyclesByPr = /* @__PURE__ */ new Map();
|
|
888
|
+
for (const result of reviewed) {
|
|
889
|
+
const key = `${result.pr.repo}#${result.pr.number}`;
|
|
890
|
+
cyclesByPr.set(key, (cyclesByPr.get(key) ?? 0) + 1);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
reviewed,
|
|
894
|
+
pending,
|
|
895
|
+
reviewing,
|
|
896
|
+
expired,
|
|
897
|
+
unrequested,
|
|
898
|
+
allHours,
|
|
899
|
+
byRepo,
|
|
900
|
+
misses,
|
|
901
|
+
cycles: [...cyclesByPr.values()]
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function computeSizeStats(sizes, { sizeTarget } = {}) {
|
|
905
|
+
const metrics = [
|
|
906
|
+
{ label: "files changed", values: sizes.map((size) => size.files) },
|
|
907
|
+
{ label: "lines added", values: sizes.map((size) => size.additions) },
|
|
908
|
+
{ label: "lines removed", values: sizes.map((size) => size.deletions) },
|
|
909
|
+
{ label: "lines total", values: sizes.map((size) => size.total) }
|
|
910
|
+
];
|
|
911
|
+
const timelineTotals = [...sizes].toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime()).map((size) => size.total);
|
|
912
|
+
if (sizeTarget === void 0) {
|
|
913
|
+
return { metrics, timelineTotals, met: void 0, misses: [], targetLabel: void 0 };
|
|
914
|
+
}
|
|
915
|
+
const meetsTarget = (size) => (sizeTarget.lines === void 0 || size.total <= sizeTarget.lines) && (sizeTarget.files === void 0 || size.files <= sizeTarget.files);
|
|
916
|
+
const targetLabel = [
|
|
917
|
+
...sizeTarget.lines === void 0 ? [] : [`<= ${sizeTarget.lines} lines`],
|
|
918
|
+
...sizeTarget.files === void 0 ? [] : [`<= ${sizeTarget.files} files`]
|
|
919
|
+
].join(", ");
|
|
920
|
+
const met = sizes.filter((size) => meetsTarget(size)).length;
|
|
921
|
+
const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
|
|
922
|
+
return { metrics, timelineTotals, met, misses, targetLabel };
|
|
923
|
+
}
|
|
924
|
+
function computeMergeStats(sizes) {
|
|
925
|
+
const merged = [];
|
|
926
|
+
const closed = [];
|
|
927
|
+
const open = [];
|
|
928
|
+
for (const entry of sizes) {
|
|
929
|
+
if (entry.mergedAt !== null) {
|
|
930
|
+
merged.push({ entry, mergedAt: entry.mergedAt, hours: durationHours(entry.pr.createdAt, entry.mergedAt) });
|
|
931
|
+
} else if (entry.pr.state === "open") {
|
|
932
|
+
open.push(entry);
|
|
933
|
+
} else if (entry.closedAt !== null) {
|
|
934
|
+
closed.push({ entry, closedAt: entry.closedAt, hours: durationHours(entry.pr.createdAt, entry.closedAt) });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
merged.sort((a, b) => b.mergedAt.getTime() - a.mergedAt.getTime());
|
|
938
|
+
closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
|
|
939
|
+
return { merged, closed, open, allHours: merged.map((result) => result.hours) };
|
|
940
|
+
}
|
|
941
|
+
function computeReviewerStats(sizes, author) {
|
|
942
|
+
const byLogin = /* @__PURE__ */ new Map();
|
|
943
|
+
let mergedReviewed = 0;
|
|
944
|
+
let mergedUnreviewed = 0;
|
|
945
|
+
for (const entry of sizes) {
|
|
946
|
+
const others = entry.reviews.flatMap(
|
|
947
|
+
(review) => review.login === null || review.login === author ? [] : [review.login]
|
|
948
|
+
);
|
|
949
|
+
for (const login of others) {
|
|
950
|
+
const counts = byLogin.get(login) ?? { prs: 0, reviews: 0 };
|
|
951
|
+
counts.reviews += 1;
|
|
952
|
+
byLogin.set(login, counts);
|
|
953
|
+
}
|
|
954
|
+
const distinct = new Set(others);
|
|
955
|
+
for (const login of distinct) {
|
|
956
|
+
const counts = byLogin.get(login);
|
|
957
|
+
if (counts !== void 0) {
|
|
958
|
+
counts.prs += 1;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (entry.mergedAt !== null) {
|
|
962
|
+
if (others.length > 0) {
|
|
963
|
+
mergedReviewed += 1;
|
|
964
|
+
} else {
|
|
965
|
+
mergedUnreviewed += 1;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const leaderboard = [...byLogin.entries()].map(([login, counts]) => {
|
|
970
|
+
return { login, ...counts };
|
|
971
|
+
}).toSorted((a, b) => b.prs - a.prs || b.reviews - a.reviews || a.login.localeCompare(b.login));
|
|
972
|
+
return { leaderboard, mergedReviewed, mergedUnreviewed };
|
|
973
|
+
}
|
|
974
|
+
function firstReviewOf(entry, author) {
|
|
975
|
+
let earliest = null;
|
|
976
|
+
for (const review of entry.reviews) {
|
|
977
|
+
if (review.login === null || review.login === author || review.submittedAt === null) {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (earliest === null || review.submittedAt < earliest) {
|
|
981
|
+
earliest = review.submittedAt;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (earliest === null) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
return { reviewedAt: earliest, hours: durationHours(entry.pr.createdAt, earliest) };
|
|
988
|
+
}
|
|
989
|
+
function computeFirstReviewStats(sizes, author, { now = /* @__PURE__ */ new Date() } = {}) {
|
|
990
|
+
const received = [];
|
|
991
|
+
const awaiting = [];
|
|
992
|
+
for (const entry of sizes) {
|
|
993
|
+
const first = firstReviewOf(entry, author);
|
|
994
|
+
if (first !== null) {
|
|
995
|
+
received.push({ entry, ...first });
|
|
996
|
+
} else if (entry.pr.state === "open") {
|
|
997
|
+
awaiting.push({ entry, hours: durationHours(entry.pr.createdAt, now) });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
awaiting.sort((a, b) => a.entry.pr.createdAt.getTime() - b.entry.pr.createdAt.getTime());
|
|
1001
|
+
return { received, awaiting, allHours: received.map((result) => result.hours) };
|
|
1002
|
+
}
|
|
1003
|
+
function computeCommentStats(sizes) {
|
|
1004
|
+
const metrics = [
|
|
1005
|
+
{ label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
|
|
1006
|
+
{ label: "review comments", values: sizes.map((size) => size.comments.review) },
|
|
1007
|
+
{ label: "all comments", values: sizes.map((size) => size.comments.total) }
|
|
1008
|
+
];
|
|
1009
|
+
const totals = sizes.map((size) => size.comments.total);
|
|
1010
|
+
const uncommented = totals.filter((total) => total === 0).length;
|
|
1011
|
+
const top = sizes.filter((size) => size.comments.total > 0).toSorted((a, b) => b.comments.total - a.comments.total);
|
|
1012
|
+
return { metrics, totals, uncommented, top };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/report.ts
|
|
1016
|
+
var BUCKETS = [];
|
|
1017
|
+
function initBuckets() {
|
|
1018
|
+
BUCKETS = makeBuckets();
|
|
1019
|
+
}
|
|
1020
|
+
function currentBuckets() {
|
|
1021
|
+
return BUCKETS;
|
|
1022
|
+
}
|
|
1023
|
+
function makeBuckets() {
|
|
1024
|
+
if (!timeMode.business || isFullDayMode()) {
|
|
1025
|
+
return [
|
|
1026
|
+
{ label: "< 1h", max: 1 },
|
|
1027
|
+
{ label: "1-4h", max: 4 },
|
|
1028
|
+
{ label: "4-8h", max: 8 },
|
|
1029
|
+
{ label: "8-24h", max: 24 },
|
|
1030
|
+
{ label: "1-2d", max: 48 },
|
|
1031
|
+
{ label: "2-4d", max: 96 },
|
|
1032
|
+
{ label: "4-7d", max: 168 },
|
|
1033
|
+
{ label: "> 7d", max: Infinity }
|
|
1034
|
+
];
|
|
1035
|
+
}
|
|
1036
|
+
const wd = timeMode.dayHours;
|
|
1037
|
+
return [
|
|
1038
|
+
{ label: "< 1h", max: 1 },
|
|
1039
|
+
{ label: "1-4h", max: 4 },
|
|
1040
|
+
{ label: "4h-1wd", max: wd },
|
|
1041
|
+
{ label: "1-2wd", max: 2 * wd },
|
|
1042
|
+
{ label: "2-3wd", max: 3 * wd },
|
|
1043
|
+
{ label: "3-5wd", max: 5 * wd },
|
|
1044
|
+
{ label: "5-10wd", max: 10 * wd },
|
|
1045
|
+
{ label: "> 10wd", max: Infinity }
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
1048
|
+
var LINE_BUCKETS = [
|
|
1049
|
+
{ label: "< 50", max: 50 },
|
|
1050
|
+
{ label: "50-100", max: 100 },
|
|
1051
|
+
{ label: "100-250", max: 250 },
|
|
1052
|
+
{ label: "250-500", max: 500 },
|
|
1053
|
+
{ label: "500-1k", max: 1e3 },
|
|
1054
|
+
{ label: "1k-2.5k", max: 2500 },
|
|
1055
|
+
{ label: "2.5k-5k", max: 5e3 },
|
|
1056
|
+
{ label: "> 5k", max: Infinity }
|
|
1057
|
+
];
|
|
1058
|
+
var FILE_BUCKETS = [
|
|
1059
|
+
{ label: "1-2", max: 3 },
|
|
1060
|
+
{ label: "3-5", max: 6 },
|
|
1061
|
+
{ label: "6-10", max: 11 },
|
|
1062
|
+
{ label: "11-20", max: 21 },
|
|
1063
|
+
{ label: "21-50", max: 51 },
|
|
1064
|
+
{ label: "> 50", max: Infinity }
|
|
1065
|
+
];
|
|
1066
|
+
var CYCLE_BUCKETS = [
|
|
1067
|
+
{ label: "1", max: 2 },
|
|
1068
|
+
{ label: "2", max: 3 },
|
|
1069
|
+
{ label: "3", max: 4 },
|
|
1070
|
+
{ label: "4-5", max: 6 },
|
|
1071
|
+
{ label: "> 5", max: Infinity }
|
|
1072
|
+
];
|
|
1073
|
+
var COMMENT_BUCKETS = [
|
|
1074
|
+
{ label: "0", max: 1 },
|
|
1075
|
+
{ label: "1-2", max: 3 },
|
|
1076
|
+
{ label: "3-5", max: 6 },
|
|
1077
|
+
{ label: "6-10", max: 11 },
|
|
1078
|
+
{ label: "11-20", max: 21 },
|
|
1079
|
+
{ label: "21-50", max: 51 },
|
|
1080
|
+
{ label: "> 50", max: Infinity }
|
|
1081
|
+
];
|
|
1082
|
+
function formatHoursOnly(hours) {
|
|
1083
|
+
if (hours < 1) {
|
|
1084
|
+
return `${Math.round(hours * 60)}m`;
|
|
1085
|
+
}
|
|
1086
|
+
return `${hours.toFixed(1)}h`;
|
|
1087
|
+
}
|
|
1088
|
+
function weeksSuffix(hours) {
|
|
1089
|
+
const weekHours = timeMode.business ? 5 * timeMode.dayHours : 7 * 24;
|
|
1090
|
+
if (hours < weekHours) {
|
|
1091
|
+
return "";
|
|
1092
|
+
}
|
|
1093
|
+
return ` (${(hours / weekHours).toFixed(1)} weeks)`;
|
|
1094
|
+
}
|
|
1095
|
+
function formatCount(value2) {
|
|
1096
|
+
if (value2 < 1e3) {
|
|
1097
|
+
return String(value2);
|
|
1098
|
+
}
|
|
1099
|
+
const scaled = value2 / 1e3;
|
|
1100
|
+
return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/tui/views/rows.ts
|
|
1104
|
+
function durationLead(result) {
|
|
1105
|
+
return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
|
|
1106
|
+
}
|
|
1107
|
+
function toPrRows(entries, leads) {
|
|
1108
|
+
const width = Math.max(...leads.map((lead) => lead.length));
|
|
1109
|
+
return entries.map((entry, i) => {
|
|
1110
|
+
return {
|
|
1111
|
+
lead: leads[i].padEnd(width),
|
|
1112
|
+
ref: `${entry.pr.repo}#${entry.pr.number}`,
|
|
1113
|
+
url: entry.pr.url,
|
|
1114
|
+
title: entry.pr.title
|
|
1115
|
+
};
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/tui/views/queue.ts
|
|
1120
|
+
function queueRows(view) {
|
|
1121
|
+
return view.sections.flatMap((section) => [...section.rows, ...section.lists.flatMap((list) => list.rows)]);
|
|
1122
|
+
}
|
|
1123
|
+
function queueRowAt(view, cursor) {
|
|
1124
|
+
const rows = view === null ? [] : queueRows(view);
|
|
1125
|
+
return rows[Math.min(cursor, rows.length - 1)];
|
|
1126
|
+
}
|
|
1127
|
+
function snoozeActionOf(row) {
|
|
1128
|
+
if (row?.pending === void 0) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
return row.pending.snoozed ? "unsnooze" : "snooze";
|
|
1132
|
+
}
|
|
1133
|
+
function groupedLists(entries, rowsOf2) {
|
|
1134
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const entry of entries) {
|
|
1136
|
+
const group = groups.get(entry.pr.repo) ?? [];
|
|
1137
|
+
group.push(entry);
|
|
1138
|
+
groups.set(entry.pr.repo, group);
|
|
1139
|
+
}
|
|
1140
|
+
return [...groups.entries()].toSorted((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])).map(([repo, group]) => {
|
|
1141
|
+
return { title: `${repo} (n=${group.length})`, rows: rowsOf2(group) };
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
function buildPendingReviewView(raw, repo = null, grouped = false, snoozes2 = [], now = Date.now()) {
|
|
1145
|
+
const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
|
|
1146
|
+
const inScope = (entries) => repo === null ? entries : entries.filter((entry) => entry.pr.repo === repo);
|
|
1147
|
+
const { awaiting, snoozed } = splitSnoozed(inScope(stats.pending), snoozes2, now);
|
|
1148
|
+
const reviewing = inScope(stats.reviewing);
|
|
1149
|
+
if (awaiting.length === 0 && snoozed.length === 0 && reviewing.length === 0) {
|
|
1150
|
+
return { empty: "No PRs are awaiting your review, and none you reviewed are still open.", sections: [] };
|
|
1151
|
+
}
|
|
1152
|
+
const split = repo === null && grouped;
|
|
1153
|
+
const sectionOf = (title, entries, rowsOf2) => split ? { title, rows: [], lists: groupedLists(entries, rowsOf2) } : { title, rows: rowsOf2(entries), lists: [] };
|
|
1154
|
+
const snoozedRowsOf = (group) => snoozedRows(group, now);
|
|
1155
|
+
return {
|
|
1156
|
+
empty: null,
|
|
1157
|
+
sections: [
|
|
1158
|
+
...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting, awaitingRows)],
|
|
1159
|
+
...snoozed.length === 0 ? [] : [sectionOf(`Snoozed (n=${snoozed.length})`, snoozed, snoozedRowsOf)],
|
|
1160
|
+
...reviewing.length === 0 ? [] : [sectionOf(`Reviewed (n=${reviewing.length})`, reviewing, rowsOf)]
|
|
1161
|
+
]
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function awaitingRows(group) {
|
|
1165
|
+
return rowsOf(group).map((row, i) => {
|
|
1166
|
+
return { ...row, pending: { requestedAt: group[i].requestedAt.getTime(), snoozed: false } };
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
function snoozedRows(group, now) {
|
|
1170
|
+
return toPrRows(
|
|
1171
|
+
group,
|
|
1172
|
+
group.map((entry) => `until ${formatWakeTime(entry.until, now)}`)
|
|
1173
|
+
).map((row, i) => {
|
|
1174
|
+
return { ...row, pending: { requestedAt: group[i].requestedAt.getTime(), snoozed: true } };
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
1178
|
+
const open = raw.sizes.filter((entry) => entry.pr.state === "open" && (repo === null || entry.pr.repo === repo)).toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime());
|
|
1179
|
+
if (open.length === 0) {
|
|
1180
|
+
return { empty: "No open authored PRs found.", sections: [] };
|
|
1181
|
+
}
|
|
1182
|
+
const rowsOf2 = (group) => {
|
|
1183
|
+
const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
|
|
1184
|
+
const ageWidth = Math.max(...ages.map((age) => age.length));
|
|
1185
|
+
return toPrRows(
|
|
1186
|
+
group,
|
|
1187
|
+
group.map(
|
|
1188
|
+
(entry, i) => `${ages[i].padEnd(ageWidth)} +${entry.additions}/-${entry.deletions}, ${entry.files} files`
|
|
1189
|
+
)
|
|
1190
|
+
);
|
|
1191
|
+
};
|
|
1192
|
+
const title = `Your open authored PRs (n=${open.length})`;
|
|
1193
|
+
if (repo === null && grouped) {
|
|
1194
|
+
return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf2) }] };
|
|
1195
|
+
}
|
|
1196
|
+
return { empty: null, sections: [{ title, rows: rowsOf2(open), lists: [] }] };
|
|
1197
|
+
}
|
|
1198
|
+
function rowsOf(group) {
|
|
1199
|
+
return toPrRows(
|
|
1200
|
+
group,
|
|
1201
|
+
group.map((entry) => durationLead(entry))
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/tui/components/Footer.tsx
|
|
1206
|
+
import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
|
|
1207
|
+
function Footer({
|
|
1208
|
+
width,
|
|
1209
|
+
modal,
|
|
1210
|
+
editing,
|
|
1211
|
+
tab,
|
|
1212
|
+
authoredTab,
|
|
1213
|
+
views,
|
|
1214
|
+
pendingCursor,
|
|
1215
|
+
copyLinks: copyLinks2,
|
|
1216
|
+
openError,
|
|
1217
|
+
successNotice,
|
|
1218
|
+
stale
|
|
1219
|
+
}) {
|
|
1220
|
+
const notice = openError ?? successNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
|
|
1221
|
+
const check = openError === null && successNotice !== null;
|
|
1222
|
+
const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
|
|
1223
|
+
const hints = truncated(
|
|
1224
|
+
hintsFor(modal, editing, tab, authoredTab, views, pendingCursor, copyLinks2),
|
|
1225
|
+
width - 2 - noticeWidth
|
|
1226
|
+
);
|
|
1227
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1228
|
+
/* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
|
|
1229
|
+
/* @__PURE__ */ jsxs(
|
|
1230
|
+
"box",
|
|
1231
|
+
{
|
|
1232
|
+
flexDirection: "row",
|
|
1233
|
+
height: 1,
|
|
1234
|
+
marginBottom: 1,
|
|
1235
|
+
paddingLeft: 1,
|
|
1236
|
+
paddingRight: 1,
|
|
1237
|
+
justifyContent: "space-between",
|
|
1238
|
+
children: [
|
|
1239
|
+
/* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hints }),
|
|
1240
|
+
/* @__PURE__ */ jsxs("text", { wrapMode: "none", children: [
|
|
1241
|
+
check && /* @__PURE__ */ jsx("span", { fg: theme.success, children: "\u2714 " }),
|
|
1242
|
+
/* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : successNotice !== null ? theme.muted : theme.warn, children: notice })
|
|
1243
|
+
] })
|
|
1244
|
+
]
|
|
1245
|
+
}
|
|
1246
|
+
)
|
|
1247
|
+
] });
|
|
1248
|
+
}
|
|
1249
|
+
function truncated(text, limit2) {
|
|
1250
|
+
if (text.length <= limit2) {
|
|
1251
|
+
return text;
|
|
1252
|
+
}
|
|
1253
|
+
return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
|
|
1254
|
+
}
|
|
1255
|
+
function hintsFor(modal, editing, tab, authoredTab, views, pendingCursor, copyLinks2) {
|
|
1256
|
+
if (modal === "options") {
|
|
1257
|
+
return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit \xB7 \u2190/\u2192 toggle \xB7 s save \xB7 esc close \xB7 q quit";
|
|
1258
|
+
}
|
|
1259
|
+
if (modal === "settings") {
|
|
1260
|
+
return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter apply \xB7 \u2190/\u2192 toggle \xB7 esc close \xB7 q quit";
|
|
1261
|
+
}
|
|
1262
|
+
if (modal === "theme") {
|
|
1263
|
+
return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit hex \xB7 esc back \xB7 q quit";
|
|
1264
|
+
}
|
|
1265
|
+
if (modal === "snooze") {
|
|
1266
|
+
return "enter snooze \xB7 esc cancel";
|
|
1267
|
+
}
|
|
1268
|
+
const toggle = tab === 1 ? authoredTab === "open" ? "t merged stats \xB7 " : "t open PRs \xB7 " : "";
|
|
1269
|
+
if (tab === 0 || tab === 1 && authoredTab === "open") {
|
|
1270
|
+
const scope2 = views === null ? null : tab === 0 ? views.pendingScope : views.openScope;
|
|
1271
|
+
const repos2 = views === null ? [] : tab === 0 ? views.pendingRepos : views.openRepos;
|
|
1272
|
+
if (scope2?.view === "list") {
|
|
1273
|
+
return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1274
|
+
}
|
|
1275
|
+
const snoozeAction = tab === 0 && views !== null ? snoozeActionOf(queueRowAt(views.pending, pendingCursor)) : null;
|
|
1276
|
+
const snooze = snoozeAction === null ? "" : `s ${snoozeAction} \xB7 `;
|
|
1277
|
+
const action = copyLinks2 ? "enter copy link" : "enter open";
|
|
1278
|
+
if (scope2 !== null && repos2.length > 0) {
|
|
1279
|
+
return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 ${snooze}${toggle}g group by repo \xB7 esc back \xB7 o options \xB7 S settings \xB7 r reload \xB7 q quit` : `\u2191/\u2193 select \xB7 ${action} \xB7 ${snooze}${toggle}esc back \xB7 1-5 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1280
|
+
}
|
|
1281
|
+
return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 ${snooze}${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1282
|
+
}
|
|
1283
|
+
const scope = views === null ? null : tab === 1 ? views.mergedScope : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
|
|
1284
|
+
const repos = views === null ? [] : tab === 1 ? views.mergedRepos : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
|
|
1285
|
+
if (scope?.view === "list") {
|
|
1286
|
+
return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1287
|
+
}
|
|
1288
|
+
const view = views === null ? null : tab === 1 ? views.merged : tab === 2 ? views.review : tab === 3 ? views.size : views.comments;
|
|
1289
|
+
const expand = view?.expandable ? view.expanded ? "x collapse \xB7 " : "x expand \xB7 " : "";
|
|
1290
|
+
if (scope !== null && repos.length > 0) {
|
|
1291
|
+
return `${toggle}${expand}esc back \xB7 j/k scroll \xB7 1-5 tabs \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1292
|
+
}
|
|
1293
|
+
return `${toggle}${expand}1-5 tabs \xB7 j/k scroll \xB7 o options \xB7 S settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
|
|
1297
|
+
function stringReplaceAll(string, substring, postfix) {
|
|
1298
|
+
let index = string.indexOf(substring);
|
|
1299
|
+
if (index === -1) {
|
|
1300
|
+
return string;
|
|
1301
|
+
}
|
|
1302
|
+
const substringLength = substring.length;
|
|
1303
|
+
let endIndex = 0;
|
|
1304
|
+
let returnValue = "";
|
|
1305
|
+
do {
|
|
1306
|
+
returnValue += string.slice(endIndex, index) + substring + postfix;
|
|
1307
|
+
endIndex = index + substringLength;
|
|
1308
|
+
index = string.indexOf(substring, endIndex);
|
|
1309
|
+
} while (index !== -1);
|
|
1310
|
+
returnValue += string.slice(endIndex);
|
|
1311
|
+
return returnValue;
|
|
1312
|
+
}
|
|
1313
|
+
function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|
1314
|
+
let endIndex = 0;
|
|
1315
|
+
let returnValue = "";
|
|
1316
|
+
do {
|
|
1317
|
+
const isGotCR = string[index - 1] === "\r";
|
|
1318
|
+
returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? "\r\n" : "\n") + postfix;
|
|
1319
|
+
endIndex = index + 1;
|
|
1320
|
+
index = string.indexOf("\n", endIndex);
|
|
1321
|
+
} while (index !== -1);
|
|
1322
|
+
returnValue += string.slice(endIndex);
|
|
1323
|
+
return returnValue;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
1327
|
+
var ANSI_BACKGROUND_OFFSET = 10;
|
|
705
1328
|
var ANSI_UNDERLINE_OFFSET = 20;
|
|
706
1329
|
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
707
1330
|
var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
|
|
@@ -1228,120 +1851,12 @@ var applyStyle = (self, string) => {
|
|
|
1228
1851
|
return openAll + string + closeAll;
|
|
1229
1852
|
};
|
|
1230
1853
|
Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
|
|
1231
|
-
var chalk = createChalk();
|
|
1232
|
-
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
1233
|
-
var source_default = chalk;
|
|
1234
|
-
|
|
1235
|
-
// src/flags.ts
|
|
1236
|
-
import { parseArgs } from "node:util";
|
|
1237
|
-
|
|
1238
|
-
// src/time.ts
|
|
1239
|
-
function wallFormatter(tz) {
|
|
1240
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
1241
|
-
timeZone: tz,
|
|
1242
|
-
hourCycle: "h23",
|
|
1243
|
-
year: "numeric",
|
|
1244
|
-
month: "2-digit",
|
|
1245
|
-
day: "2-digit",
|
|
1246
|
-
hour: "2-digit",
|
|
1247
|
-
minute: "2-digit",
|
|
1248
|
-
second: "2-digit"
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
var timeMode = {
|
|
1252
|
-
business: true,
|
|
1253
|
-
workWindows: [{ startMin: 0, endMin: 24 * 60 }],
|
|
1254
|
-
workDays: /* @__PURE__ */ new Set([1, 2, 3, 4, 5]),
|
|
1255
|
-
dayHours: 24,
|
|
1256
|
-
formatter: wallFormatter("UTC")
|
|
1257
|
-
};
|
|
1258
|
-
function configureTimeMode({
|
|
1259
|
-
business,
|
|
1260
|
-
workWindows,
|
|
1261
|
-
workDays,
|
|
1262
|
-
tz
|
|
1263
|
-
}) {
|
|
1264
|
-
const workMinutesPerDay = workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
|
|
1265
|
-
timeMode.business = business;
|
|
1266
|
-
timeMode.workWindows = workWindows;
|
|
1267
|
-
timeMode.workDays = workDays;
|
|
1268
|
-
timeMode.dayHours = business ? workMinutesPerDay / 60 : 24;
|
|
1269
|
-
timeMode.formatter = wallFormatter(tz);
|
|
1270
|
-
}
|
|
1271
|
-
function isFullDayMode() {
|
|
1272
|
-
return timeMode.business && timeMode.dayHours === 24;
|
|
1273
|
-
}
|
|
1274
|
-
function wallParts(instantMs) {
|
|
1275
|
-
const parts = Object.fromEntries(timeMode.formatter.formatToParts(instantMs).map((part) => [part.type, part.value]));
|
|
1276
|
-
return {
|
|
1277
|
-
year: Number(parts.year),
|
|
1278
|
-
month: Number(parts.month),
|
|
1279
|
-
day: Number(parts.day),
|
|
1280
|
-
hour: Number(parts.hour),
|
|
1281
|
-
minute: Number(parts.minute),
|
|
1282
|
-
second: Number(parts.second)
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
function zonedStamp(date) {
|
|
1286
|
-
const parts = wallParts(date.getTime());
|
|
1287
|
-
const dayUtcMs = Date.UTC(parts.year, parts.month - 1, parts.day);
|
|
1288
|
-
return { dayUtcMs, weekday: new Date(dayUtcMs).getUTCDay(), hour: parts.hour, minute: parts.minute };
|
|
1289
|
-
}
|
|
1290
|
-
function hasWorkWindows() {
|
|
1291
|
-
const covered = timeMode.workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
|
|
1292
|
-
return covered < 24 * 60;
|
|
1293
|
-
}
|
|
1294
|
-
function classifyInstant(date) {
|
|
1295
|
-
const { weekday, hour, minute } = zonedStamp(date);
|
|
1296
|
-
if (!timeMode.workDays.has(weekday)) {
|
|
1297
|
-
return "weekend";
|
|
1298
|
-
}
|
|
1299
|
-
const minuteOfDay = hour * 60 + minute;
|
|
1300
|
-
return timeMode.workWindows.some((window) => minuteOfDay >= window.startMin && minuteOfDay < window.endMin) ? "work" : "after";
|
|
1301
|
-
}
|
|
1302
|
-
function utcFromWall(wallTargetMs) {
|
|
1303
|
-
let guess = wallTargetMs;
|
|
1304
|
-
for (let i = 0; i < 2; i++) {
|
|
1305
|
-
const parts = wallParts(guess);
|
|
1306
|
-
const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
1307
|
-
guess = wallTargetMs - (wall - guess);
|
|
1308
|
-
}
|
|
1309
|
-
return guess;
|
|
1310
|
-
}
|
|
1311
|
-
function businessMsBetween(start, end) {
|
|
1312
|
-
const startMs = start.getTime();
|
|
1313
|
-
const endMs = end.getTime();
|
|
1314
|
-
if (endMs <= startMs) {
|
|
1315
|
-
return 0;
|
|
1316
|
-
}
|
|
1317
|
-
let total = 0;
|
|
1318
|
-
const parts = wallParts(startMs);
|
|
1319
|
-
let localDay = Date.UTC(parts.year, parts.month - 1, parts.day);
|
|
1320
|
-
while (utcFromWall(localDay) <= endMs) {
|
|
1321
|
-
const weekday = new Date(localDay).getUTCDay();
|
|
1322
|
-
if (timeMode.workDays.has(weekday)) {
|
|
1323
|
-
for (const window of timeMode.workWindows) {
|
|
1324
|
-
const windowStart = utcFromWall(localDay + window.startMin * 6e4);
|
|
1325
|
-
const windowEnd = utcFromWall(localDay + window.endMin * 6e4);
|
|
1326
|
-
const overlapStart = Math.max(windowStart, startMs);
|
|
1327
|
-
const overlapEnd = Math.min(windowEnd, endMs);
|
|
1328
|
-
if (overlapEnd > overlapStart) {
|
|
1329
|
-
total += overlapEnd - overlapStart;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
localDay += 864e5;
|
|
1334
|
-
}
|
|
1335
|
-
return total;
|
|
1336
|
-
}
|
|
1337
|
-
function durationHours(start, end) {
|
|
1338
|
-
if (!timeMode.business) {
|
|
1339
|
-
return (end.getTime() - start.getTime()) / 36e5;
|
|
1340
|
-
}
|
|
1341
|
-
return businessMsBetween(start, end) / 36e5;
|
|
1342
|
-
}
|
|
1854
|
+
var chalk = createChalk();
|
|
1855
|
+
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
1856
|
+
var source_default = chalk;
|
|
1343
1857
|
|
|
1344
1858
|
// src/flags.ts
|
|
1859
|
+
import { parseArgs } from "node:util";
|
|
1345
1860
|
var flag = source_default.green;
|
|
1346
1861
|
var value = source_default.cyan;
|
|
1347
1862
|
var dim = source_default.dim;
|
|
@@ -1493,613 +2008,273 @@ function renderHelp() {
|
|
|
1493
2008
|
const short = option.short ? `${flag(`-${option.short}`)}, ` : "";
|
|
1494
2009
|
const long = option.placeholder ? `${flag(`--${option.name}`)} ${dim(option.placeholder)}` : flag(`--${option.name}`);
|
|
1495
2010
|
const body = wrapMarkup(option.help, HELP_WIDTH - column).map((line) => colorizeHelp(line));
|
|
1496
|
-
lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
|
|
1497
|
-
for (const overflow of body.slice(1)) {
|
|
1498
|
-
lines.push(" ".repeat(column) + overflow);
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
return lines.join("\n");
|
|
1502
|
-
}
|
|
1503
|
-
var HELP = renderHelp();
|
|
1504
|
-
function parseCliArgs(args) {
|
|
1505
|
-
const options = {};
|
|
1506
|
-
for (const option of OPTIONS) {
|
|
1507
|
-
options[option.name] = { type: option.type };
|
|
1508
|
-
if (option.short) {
|
|
1509
|
-
options[option.name].short = option.short;
|
|
1510
|
-
}
|
|
1511
|
-
if (option.multiple) {
|
|
1512
|
-
options[option.name].multiple = true;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
const { values } = parseArgs({ options, args });
|
|
1516
|
-
const explicit = new Set(Object.keys(values));
|
|
1517
|
-
for (const option of OPTIONS) {
|
|
1518
|
-
if (option.default !== void 0 && values[option.name] === void 0) {
|
|
1519
|
-
values[option.name] = option.default;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
return { values, explicit };
|
|
1523
|
-
}
|
|
1524
|
-
function parseSince(input) {
|
|
1525
|
-
const relative = /^(\d+)([dwmy])$/.exec(input);
|
|
1526
|
-
if (relative) {
|
|
1527
|
-
const amount = Number(relative[1]);
|
|
1528
|
-
const date2 = /* @__PURE__ */ new Date();
|
|
1529
|
-
if (relative[2] === "d") {
|
|
1530
|
-
date2.setDate(date2.getDate() - amount);
|
|
1531
|
-
}
|
|
1532
|
-
if (relative[2] === "w") {
|
|
1533
|
-
date2.setDate(date2.getDate() - amount * 7);
|
|
1534
|
-
}
|
|
1535
|
-
if (relative[2] === "m") {
|
|
1536
|
-
date2.setMonth(date2.getMonth() - amount);
|
|
1537
|
-
}
|
|
1538
|
-
if (relative[2] === "y") {
|
|
1539
|
-
date2.setFullYear(date2.getFullYear() - amount);
|
|
1540
|
-
}
|
|
1541
|
-
return date2;
|
|
1542
|
-
}
|
|
1543
|
-
const date = new Date(input);
|
|
1544
|
-
if (Number.isNaN(date.getTime())) {
|
|
1545
|
-
throw new CliError(`invalid --since value "${input}", use an ISO date or 30d/8w/6m/1y`);
|
|
1546
|
-
}
|
|
1547
|
-
return date;
|
|
1548
|
-
}
|
|
1549
|
-
function parseTarget(input) {
|
|
1550
|
-
const match = /^(\d+(?:\.\d+)?)([hdm]?)$/.exec(input);
|
|
1551
|
-
if (!match) {
|
|
1552
|
-
throw new CliError(`invalid --target value "${input}", use 24h, 2d, or 90m`);
|
|
1553
|
-
}
|
|
1554
|
-
const amount = Number(match[1]);
|
|
1555
|
-
if (match[2] === "d") {
|
|
1556
|
-
return amount * timeMode.dayHours;
|
|
1557
|
-
}
|
|
1558
|
-
if (match[2] === "m") {
|
|
1559
|
-
return amount / 60;
|
|
1560
|
-
}
|
|
1561
|
-
return amount;
|
|
1562
|
-
}
|
|
1563
|
-
var DEFAULT_TARGET_PERCENTILE = 90;
|
|
1564
|
-
function parseTargetPercentile(input) {
|
|
1565
|
-
const match = /^p?(\d{1,3})$/i.exec(input);
|
|
1566
|
-
const value2 = match === null ? Number.NaN : Number(match[1]);
|
|
1567
|
-
if (!Number.isInteger(value2) || value2 < 1 || value2 > 100) {
|
|
1568
|
-
throw new CliError(`invalid --target-percentile value "${input}", use a percentile from 1 to 100 like 90 or p90`);
|
|
1569
|
-
}
|
|
1570
|
-
return value2;
|
|
1571
|
-
}
|
|
1572
|
-
function parseSizeTarget(input) {
|
|
1573
|
-
const target = {};
|
|
1574
|
-
for (const part of input.split(",")) {
|
|
1575
|
-
const match = /^(\d+)([lf]?)$/.exec(part.trim());
|
|
1576
|
-
if (!match) {
|
|
1577
|
-
throw new CliError(`invalid --size-target value "${input}", use 400, 400l, 20f, or 400l,20f`);
|
|
1578
|
-
}
|
|
1579
|
-
const key = match[2] === "f" ? "files" : "lines";
|
|
1580
|
-
if (target[key] !== void 0) {
|
|
1581
|
-
throw new CliError(`--size-target sets the ${key} budget twice`);
|
|
1582
|
-
}
|
|
1583
|
-
target[key] = Number(match[1]);
|
|
1584
|
-
}
|
|
1585
|
-
return target;
|
|
1586
|
-
}
|
|
1587
|
-
var REVIEW_TYPES = /* @__PURE__ */ new Map([
|
|
1588
|
-
["approve", "APPROVED"],
|
|
1589
|
-
["comment", "COMMENTED"],
|
|
1590
|
-
["request-changes", "CHANGES_REQUESTED"]
|
|
1591
|
-
]);
|
|
1592
|
-
function parseReviewTypes(input) {
|
|
1593
|
-
const states = /* @__PURE__ */ new Set();
|
|
1594
|
-
for (const part of input.split(",")) {
|
|
1595
|
-
const state = REVIEW_TYPES.get(part.trim().toLowerCase());
|
|
1596
|
-
if (state === void 0) {
|
|
1597
|
-
throw new CliError(`invalid --review-types value "${part.trim()}", use approve, comment, or request-changes`);
|
|
1598
|
-
}
|
|
1599
|
-
states.add(state);
|
|
1600
|
-
}
|
|
1601
|
-
return states;
|
|
1602
|
-
}
|
|
1603
|
-
function toMinutesOfDay(hourText, minuteText, meridiem) {
|
|
1604
|
-
const minute = Number(minuteText ?? 0);
|
|
1605
|
-
if (minute > 59) {
|
|
1606
|
-
return null;
|
|
1607
|
-
}
|
|
1608
|
-
let hour = Number(hourText);
|
|
1609
|
-
if (meridiem !== void 0) {
|
|
1610
|
-
if (hour < 1 || hour > 12) {
|
|
1611
|
-
return null;
|
|
1612
|
-
}
|
|
1613
|
-
hour %= 12;
|
|
1614
|
-
if (meridiem.toLowerCase() === "pm") {
|
|
1615
|
-
hour += 12;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return hour * 60 + minute;
|
|
1619
|
-
}
|
|
1620
|
-
var WEEKDAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
1621
|
-
var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1622
|
-
function parseWorkDays(input) {
|
|
1623
|
-
const days = /* @__PURE__ */ new Set();
|
|
1624
|
-
for (const part of input.split(",")) {
|
|
1625
|
-
const match = /^([a-z]{3})(?:-([a-z]{3}))?$/i.exec(part.trim());
|
|
1626
|
-
const endName = match?.[2];
|
|
1627
|
-
const start = match === null ? -1 : WEEKDAY_NAMES.indexOf(match[1].toLowerCase());
|
|
1628
|
-
const end = match === null ? -1 : endName === void 0 ? start : WEEKDAY_NAMES.indexOf(endName.toLowerCase());
|
|
1629
|
-
if (start === -1 || end === -1) {
|
|
1630
|
-
throw new CliError(
|
|
1631
|
-
`invalid --work-days value "${part.trim()}", use weekday names and ranges like mon-fri, sun-thu, or mon,wed,fri`
|
|
1632
|
-
);
|
|
1633
|
-
}
|
|
1634
|
-
for (let day = start; ; day = (day + 1) % 7) {
|
|
1635
|
-
days.add(day);
|
|
1636
|
-
if (day === end) {
|
|
1637
|
-
break;
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
return days;
|
|
1642
|
-
}
|
|
1643
|
-
function workDayRunLabel(start, end) {
|
|
1644
|
-
return start === end ? WEEKDAY_LABELS[start] : `${WEEKDAY_LABELS[start]}-${WEEKDAY_LABELS[end]}`;
|
|
1645
|
-
}
|
|
1646
|
-
function formatWorkDays(days) {
|
|
1647
|
-
if (days.size === 7) {
|
|
1648
|
-
return "Mon-Sun";
|
|
1649
|
-
}
|
|
1650
|
-
const monFirst = [1, 2, 3, 4, 5, 6, 0];
|
|
1651
|
-
const anchor = monFirst.find((day) => days.has(day) && !days.has((day + 6) % 7)) ?? 1;
|
|
1652
|
-
const runs = [];
|
|
1653
|
-
let runStart = -1;
|
|
1654
|
-
let runEnd = -1;
|
|
1655
|
-
for (let step = 0; step < 7; step++) {
|
|
1656
|
-
const day = (anchor + step) % 7;
|
|
1657
|
-
if (days.has(day)) {
|
|
1658
|
-
runStart = runStart === -1 ? day : runStart;
|
|
1659
|
-
runEnd = day;
|
|
1660
|
-
} else if (runStart !== -1) {
|
|
1661
|
-
runs.push(workDayRunLabel(runStart, runEnd));
|
|
1662
|
-
runStart = -1;
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
return runs.join(",");
|
|
1666
|
-
}
|
|
1667
|
-
function canonicalWorkDays(input) {
|
|
1668
|
-
return formatWorkDays(parseWorkDays(input));
|
|
1669
|
-
}
|
|
1670
|
-
function parseWorkHours(input) {
|
|
1671
|
-
const windows = input.split(",").map((range) => {
|
|
1672
|
-
const match = /^(\d{1,2})(?::(\d{2}))?(am|pm)?-(\d{1,2})(?::(\d{2}))?(am|pm)?$/i.exec(range.trim());
|
|
1673
|
-
if (!match) {
|
|
1674
|
-
throw new CliError(
|
|
1675
|
-
`invalid --work-hours range "${range}", use ranges like 9-17, 9am-6pm, 8:30-16:30, or 9-18,19:30-20:30`
|
|
1676
|
-
);
|
|
1677
|
-
}
|
|
1678
|
-
const startMin = toMinutesOfDay(match[1], match[2], match[3]);
|
|
1679
|
-
const end = toMinutesOfDay(match[4], match[5], match[6]);
|
|
1680
|
-
const endMin = end === 0 ? 24 * 60 : end;
|
|
1681
|
-
if (startMin === null || endMin === null || endMin <= startMin || endMin > 24 * 60) {
|
|
1682
|
-
throw new CliError(`invalid --work-hours range "${range}"`);
|
|
1683
|
-
}
|
|
1684
|
-
return { startMin, endMin };
|
|
1685
|
-
});
|
|
1686
|
-
windows.sort((a, b) => a.startMin - b.startMin);
|
|
1687
|
-
for (let i = 1; i < windows.length; i++) {
|
|
1688
|
-
if (windows[i].startMin < windows[i - 1].endMin) {
|
|
1689
|
-
throw new CliError(`--work-hours ranges overlap around ${formatMinutesOfDay(windows[i].startMin)}`);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
return windows;
|
|
1693
|
-
}
|
|
1694
|
-
function resolveTimezone(input) {
|
|
1695
|
-
const tz = input ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1696
|
-
try {
|
|
1697
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
1698
|
-
} catch {
|
|
1699
|
-
throw new CliError(`invalid --tz value "${tz}", use an IANA zone like Europe/Berlin`);
|
|
1700
|
-
}
|
|
1701
|
-
return tz;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// src/tui/components/Spinner.tsx
|
|
1705
|
-
import { useEffect, useState } from "react";
|
|
1706
|
-
import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
|
|
1707
|
-
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1708
|
-
var SPINNER_INTERVAL_MS = 80;
|
|
1709
|
-
function Spinner() {
|
|
1710
|
-
const [frame, setFrame] = useState(0);
|
|
1711
|
-
useEffect(() => {
|
|
1712
|
-
const timer = setInterval(() => {
|
|
1713
|
-
setFrame((previous) => (previous + 1) % SPINNER_FRAMES.length);
|
|
1714
|
-
}, SPINNER_INTERVAL_MS);
|
|
1715
|
-
return () => {
|
|
1716
|
-
clearInterval(timer);
|
|
1717
|
-
};
|
|
1718
|
-
}, []);
|
|
1719
|
-
return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// src/tui/components/Header.tsx
|
|
1723
|
-
import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
|
|
1724
|
-
function Header({
|
|
1725
|
-
options,
|
|
1726
|
-
raw,
|
|
1727
|
-
error,
|
|
1728
|
-
spinning,
|
|
1729
|
-
reloadEvery
|
|
1730
|
-
}) {
|
|
1731
|
-
const context = [
|
|
1732
|
-
raw ? `@${raw.user}` : options.user !== "" ? `@${options.user}` : "@...",
|
|
1733
|
-
`since ${options.since}`,
|
|
1734
|
-
raw && raw.repos.length > 0 ? raw.repos.join(", ") : options.repos !== "" ? options.repos : "all repos",
|
|
1735
|
-
timeModeLabel(options)
|
|
1736
|
-
].join(" \xB7 ");
|
|
1737
|
-
const rightStatus = raw ? error !== null ? "reload failed \xB7 press r to retry" : `refreshed ${raw.fetchedAt.toLocaleTimeString()}${reloadEvery === null ? "" : ` \xB7 every ${reloadEvery}`}` : "";
|
|
1738
|
-
return /* @__PURE__ */ jsxs2("box", { flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [
|
|
1739
|
-
/* @__PURE__ */ jsxs2("text", { wrapMode: "none", children: [
|
|
1740
|
-
/* @__PURE__ */ jsx3("b", { fg: theme.accent, children: "pr-stats" }),
|
|
1741
|
-
/* @__PURE__ */ jsxs2("span", { fg: theme.muted, children: [
|
|
1742
|
-
" \xB7 ",
|
|
1743
|
-
context
|
|
1744
|
-
] })
|
|
1745
|
-
] }),
|
|
1746
|
-
spinning ? /* @__PURE__ */ jsx3(Spinner, {}) : /* @__PURE__ */ jsx3("text", { wrapMode: "none", fg: raw !== null && error !== null ? theme.error : theme.muted, children: rightStatus })
|
|
1747
|
-
] });
|
|
2011
|
+
lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
|
|
2012
|
+
for (const overflow of body.slice(1)) {
|
|
2013
|
+
lines.push(" ".repeat(column) + overflow);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return lines.join("\n");
|
|
1748
2017
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
|
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
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1791
|
-
|
|
1792
|
-
continue;
|
|
2050
|
+
if (relative[2] === "m") {
|
|
2051
|
+
date2.setMonth(date2.getMonth() - amount);
|
|
1793
2052
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
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
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
]
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
return
|
|
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
|
-
|
|
1851
|
-
|
|
1852
|
-
const
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
-
|
|
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
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
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
|
-
|
|
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
|
|
1947
|
-
return
|
|
2158
|
+
function workDayRunLabel(start, end) {
|
|
2159
|
+
return start === end ? WEEKDAY_LABELS[start] : `${WEEKDAY_LABELS[start]}-${WEEKDAY_LABELS[end]}`;
|
|
1948
2160
|
}
|
|
1949
|
-
function
|
|
1950
|
-
if (
|
|
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
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
|
2180
|
+
return runs.join(",");
|
|
2013
2181
|
}
|
|
2014
|
-
function
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
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
|
|
2207
|
+
return windows;
|
|
2020
2208
|
}
|
|
2021
|
-
function
|
|
2022
|
-
|
|
2023
|
-
|
|
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
|
-
|
|
2026
|
-
return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
|
|
2216
|
+
return tz;
|
|
2027
2217
|
}
|
|
2028
2218
|
|
|
2029
|
-
// src/tui/
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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/
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
-
|
|
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
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
3424
|
+
const binary = join4(resolved, "gh");
|
|
3249
3425
|
if (!statSync(binary, { throwIfNoEntry: false })?.isFile()) {
|
|
3250
3426
|
throw new CliError(`--debug directory "${input}" does not contain a gh executable`);
|
|
3251
3427
|
}
|
|
@@ -4027,7 +4203,7 @@ function buildStatsReport(raw, options) {
|
|
|
4027
4203
|
};
|
|
4028
4204
|
}
|
|
4029
4205
|
function exportFile() {
|
|
4030
|
-
return
|
|
4206
|
+
return join5(process.cwd(), "pr-stats.json");
|
|
4031
4207
|
}
|
|
4032
4208
|
function exportStatsFile(raw, options) {
|
|
4033
4209
|
writeFileAtomic(exportFile(), `${JSON.stringify(buildStatsReport(raw, options), null, 2)}
|
|
@@ -4115,6 +4291,12 @@ var SETTINGS = [
|
|
|
4115
4291
|
label: "Copy instead of open",
|
|
4116
4292
|
hint: "enter and a click on a PR reference copy its link to the clipboard instead of opening the browser"
|
|
4117
4293
|
},
|
|
4294
|
+
{
|
|
4295
|
+
key: "snoozeDuration",
|
|
4296
|
+
section: "Snooze",
|
|
4297
|
+
label: "Default snooze",
|
|
4298
|
+
hint: "the duration the snooze dialog starts with when s snoozes a PR, like 30m, 2h, or 1d \xB7 enter edits the value"
|
|
4299
|
+
},
|
|
4118
4300
|
{
|
|
4119
4301
|
key: "themePreset",
|
|
4120
4302
|
section: "Theme",
|
|
@@ -4376,6 +4558,7 @@ function SettingsModal({
|
|
|
4376
4558
|
notifications: notifications2,
|
|
4377
4559
|
notifyChannel: notifyChannel2,
|
|
4378
4560
|
copyLinks: copyLinks2,
|
|
4561
|
+
snoozeDuration: snoozeDuration2,
|
|
4379
4562
|
preset,
|
|
4380
4563
|
onDraft,
|
|
4381
4564
|
onSubmit
|
|
@@ -4405,6 +4588,7 @@ function SettingsModal({
|
|
|
4405
4588
|
channelValue,
|
|
4406
4589
|
deliveryValue,
|
|
4407
4590
|
copyLinks: copyLinks2,
|
|
4591
|
+
snoozeDuration: snoozeDuration2,
|
|
4408
4592
|
preset,
|
|
4409
4593
|
onDraft,
|
|
4410
4594
|
onSubmit
|
|
@@ -4427,6 +4611,7 @@ function SettingValue({
|
|
|
4427
4611
|
channelValue,
|
|
4428
4612
|
deliveryValue,
|
|
4429
4613
|
copyLinks: copyLinks2,
|
|
4614
|
+
snoozeDuration: snoozeDuration2,
|
|
4430
4615
|
preset,
|
|
4431
4616
|
onDraft,
|
|
4432
4617
|
onSubmit
|
|
@@ -4459,6 +4644,12 @@ function SettingValue({
|
|
|
4459
4644
|
case "copyLinks": {
|
|
4460
4645
|
return /* @__PURE__ */ jsx12(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
|
|
4461
4646
|
}
|
|
4647
|
+
case "snoozeDuration": {
|
|
4648
|
+
if (isEditing) {
|
|
4649
|
+
return /* @__PURE__ */ jsx12(ModalInput, { width: 16, value: snoozeDuration2, onDraft, onSubmit });
|
|
4650
|
+
}
|
|
4651
|
+
return /* @__PURE__ */ jsx12(IntervalValue, { value: snoozeDuration2, active: true, isSelected });
|
|
4652
|
+
}
|
|
4462
4653
|
case "themePreset": {
|
|
4463
4654
|
return /* @__PURE__ */ jsx12(ToggleValue, { value: preset, isSelected });
|
|
4464
4655
|
}
|
|
@@ -4502,8 +4693,42 @@ function PathValue({ path, confirming, isSelected }) {
|
|
|
4502
4693
|
return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
|
|
4503
4694
|
}
|
|
4504
4695
|
|
|
4696
|
+
// src/tui/components/SnoozeModal.tsx
|
|
4697
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "@opentui/react/jsx-runtime";
|
|
4698
|
+
var HINT = "how long to park the PR, like 30m, 2h, or 1d \xB7 enter snoozes \xB7 esc cancels";
|
|
4699
|
+
function SnoozeModal({
|
|
4700
|
+
target,
|
|
4701
|
+
snoozeDuration: snoozeDuration2,
|
|
4702
|
+
error,
|
|
4703
|
+
onDraft,
|
|
4704
|
+
onSubmit
|
|
4705
|
+
}) {
|
|
4706
|
+
return /* @__PURE__ */ jsxs12(ModalFrame, { title: "Snooze", children: [
|
|
4707
|
+
/* @__PURE__ */ jsxs12("text", { wrapMode: "word", height: 2, marginLeft: 2, marginRight: 2, marginBottom: 1, children: [
|
|
4708
|
+
/* @__PURE__ */ jsx13("b", { fg: theme.accent, children: target.ref }),
|
|
4709
|
+
/* @__PURE__ */ jsxs12("span", { fg: theme.muted, children: [
|
|
4710
|
+
" ",
|
|
4711
|
+
target.title
|
|
4712
|
+
] })
|
|
4713
|
+
] }),
|
|
4714
|
+
/* @__PURE__ */ jsx13(ModalRow, { label: "Snooze for", isSelected: true, children: /* @__PURE__ */ jsx13(ModalInput, { width: 16, value: snoozeDuration2, onDraft, onSubmit }) }),
|
|
4715
|
+
/* @__PURE__ */ jsx13(
|
|
4716
|
+
"text",
|
|
4717
|
+
{
|
|
4718
|
+
wrapMode: "word",
|
|
4719
|
+
height: 2,
|
|
4720
|
+
fg: error !== null ? theme.error : theme.muted,
|
|
4721
|
+
marginTop: 1,
|
|
4722
|
+
marginLeft: 2,
|
|
4723
|
+
marginRight: 2,
|
|
4724
|
+
children: error ?? HINT
|
|
4725
|
+
}
|
|
4726
|
+
)
|
|
4727
|
+
] });
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4505
4730
|
// src/tui/components/ThemeModal.tsx
|
|
4506
|
-
import { Fragment as Fragment4, jsx as
|
|
4731
|
+
import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
|
|
4507
4732
|
function ThemeModal({
|
|
4508
4733
|
selected,
|
|
4509
4734
|
editing,
|
|
@@ -4516,8 +4741,8 @@ function ThemeModal({
|
|
|
4516
4741
|
const spec = THEME_COLORS[selected];
|
|
4517
4742
|
const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
|
|
4518
4743
|
const hint = spec.key in overrides ? `${spec.hint} \xB7 custom color, an empty value restores the theme` : spec.hint;
|
|
4519
|
-
return /* @__PURE__ */
|
|
4520
|
-
/* @__PURE__ */
|
|
4744
|
+
return /* @__PURE__ */ jsxs13(ModalFrame, { title: "Theme colors", children: [
|
|
4745
|
+
/* @__PURE__ */ jsx14("box", { flexDirection: "column", marginBottom: 1, children: THEME_COLORS.map((color, index) => /* @__PURE__ */ jsx14(
|
|
4521
4746
|
ColorRow,
|
|
4522
4747
|
{
|
|
4523
4748
|
color,
|
|
@@ -4529,7 +4754,7 @@ function ThemeModal({
|
|
|
4529
4754
|
},
|
|
4530
4755
|
color.key
|
|
4531
4756
|
)) }),
|
|
4532
|
-
/* @__PURE__ */
|
|
4757
|
+
/* @__PURE__ */ jsx14("text", { wrapMode: "word", height: 2, fg: error !== null ? theme.error : theme.muted, marginLeft: 2, marginRight: 2, children: error ?? message?.text ?? hint })
|
|
4533
4758
|
] });
|
|
4534
4759
|
}
|
|
4535
4760
|
function ColorRow({
|
|
@@ -4542,20 +4767,20 @@ function ColorRow({
|
|
|
4542
4767
|
}) {
|
|
4543
4768
|
const value2 = themeColorText(color.key);
|
|
4544
4769
|
const swatch = theme[color.key];
|
|
4545
|
-
return /* @__PURE__ */
|
|
4546
|
-
Array.isArray(swatch) ? /* @__PURE__ */
|
|
4547
|
-
/* @__PURE__ */
|
|
4548
|
-
/* @__PURE__ */
|
|
4549
|
-
/* @__PURE__ */
|
|
4550
|
-
/* @__PURE__ */
|
|
4551
|
-
] }) : /* @__PURE__ */
|
|
4552
|
-
/* @__PURE__ */
|
|
4553
|
-
isSelected ? /* @__PURE__ */
|
|
4770
|
+
return /* @__PURE__ */ jsx14(ModalRow, { label: color.key, isSelected, children: isEditing ? /* @__PURE__ */ jsx14(ModalInput, { width: 36, value: value2, onDraft, onSubmit }) : /* @__PURE__ */ jsxs13("text", { wrapMode: "none", children: [
|
|
4771
|
+
Array.isArray(swatch) ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
|
|
4772
|
+
/* @__PURE__ */ jsx14("span", { fg: swatch[0], children: "\u2588" }),
|
|
4773
|
+
/* @__PURE__ */ jsx14("span", { fg: swatch[1], children: "\u2588" }),
|
|
4774
|
+
/* @__PURE__ */ jsx14("span", { fg: swatch[2], children: "\u2588" }),
|
|
4775
|
+
/* @__PURE__ */ jsx14("span", { fg: swatch[3], children: "\u2588" })
|
|
4776
|
+
] }) : /* @__PURE__ */ jsx14("span", { fg: swatch, children: "\u2588\u2588" }),
|
|
4777
|
+
/* @__PURE__ */ jsx14("span", { children: " " }),
|
|
4778
|
+
isSelected ? /* @__PURE__ */ jsx14("b", { fg: theme.text, children: value2 }) : /* @__PURE__ */ jsx14("span", { fg: isCustom ? theme.text : theme.muted, children: value2 })
|
|
4554
4779
|
] }) });
|
|
4555
4780
|
}
|
|
4556
4781
|
|
|
4557
4782
|
// src/tui/components/Modals.tsx
|
|
4558
|
-
import { jsx as
|
|
4783
|
+
import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
|
|
4559
4784
|
function Modals({
|
|
4560
4785
|
ui,
|
|
4561
4786
|
options,
|
|
@@ -4566,16 +4791,18 @@ function Modals({
|
|
|
4566
4791
|
notifications: notifications2,
|
|
4567
4792
|
notifyChannel: notifyChannel2,
|
|
4568
4793
|
copyLinks: copyLinks2,
|
|
4794
|
+
snoozeDuration: snoozeDuration2,
|
|
4569
4795
|
themeState,
|
|
4570
4796
|
onDraft,
|
|
4571
4797
|
onSubmitField,
|
|
4572
|
-
|
|
4798
|
+
onSubmitSetting,
|
|
4573
4799
|
onSubmitThemeColor,
|
|
4800
|
+
onSubmitSnooze,
|
|
4574
4801
|
onToggleReviewType,
|
|
4575
4802
|
onToggleWorkDay
|
|
4576
4803
|
}) {
|
|
4577
4804
|
if (ui.modal === "options") {
|
|
4578
|
-
return /* @__PURE__ */
|
|
4805
|
+
return /* @__PURE__ */ jsx15(
|
|
4579
4806
|
OptionsModal,
|
|
4580
4807
|
{
|
|
4581
4808
|
options,
|
|
@@ -4591,7 +4818,7 @@ function Modals({
|
|
|
4591
4818
|
);
|
|
4592
4819
|
}
|
|
4593
4820
|
if (ui.modal === "settings") {
|
|
4594
|
-
return /* @__PURE__ */
|
|
4821
|
+
return /* @__PURE__ */ jsx15(
|
|
4595
4822
|
SettingsModal,
|
|
4596
4823
|
{
|
|
4597
4824
|
selected: ui.selectedSetting,
|
|
@@ -4604,14 +4831,27 @@ function Modals({
|
|
|
4604
4831
|
notifications: notifications2,
|
|
4605
4832
|
notifyChannel: notifyChannel2,
|
|
4606
4833
|
copyLinks: copyLinks2,
|
|
4834
|
+
snoozeDuration: snoozeDuration2,
|
|
4607
4835
|
preset: themeState.preset,
|
|
4608
4836
|
onDraft,
|
|
4609
|
-
onSubmit:
|
|
4837
|
+
onSubmit: onSubmitSetting
|
|
4838
|
+
}
|
|
4839
|
+
);
|
|
4840
|
+
}
|
|
4841
|
+
if (ui.modal === "snooze" && ui.snoozeTarget !== null) {
|
|
4842
|
+
return /* @__PURE__ */ jsx15(
|
|
4843
|
+
SnoozeModal,
|
|
4844
|
+
{
|
|
4845
|
+
target: ui.snoozeTarget,
|
|
4846
|
+
snoozeDuration: snoozeDuration2,
|
|
4847
|
+
error: ui.snoozeError,
|
|
4848
|
+
onDraft,
|
|
4849
|
+
onSubmit: onSubmitSnooze
|
|
4610
4850
|
}
|
|
4611
4851
|
);
|
|
4612
4852
|
}
|
|
4613
4853
|
if (ui.modal === "theme") {
|
|
4614
|
-
return /* @__PURE__ */
|
|
4854
|
+
return /* @__PURE__ */ jsx15(
|
|
4615
4855
|
ThemeModal,
|
|
4616
4856
|
{
|
|
4617
4857
|
selected: ui.selectedThemeColor,
|
|
@@ -4627,6 +4867,72 @@ function Modals({
|
|
|
4627
4867
|
return null;
|
|
4628
4868
|
}
|
|
4629
4869
|
|
|
4870
|
+
// src/tui/data/notifications.ts
|
|
4871
|
+
function diffReviewRequests(previous, results) {
|
|
4872
|
+
const baseline = /* @__PURE__ */ new Map();
|
|
4873
|
+
const reviewedKeys = /* @__PURE__ */ new Set();
|
|
4874
|
+
const pending = [];
|
|
4875
|
+
for (const result of results) {
|
|
4876
|
+
const key = `${result.pr.repo}#${result.pr.number}`;
|
|
4877
|
+
if (result.kind === "reviewed") {
|
|
4878
|
+
reviewedKeys.add(key);
|
|
4879
|
+
} else if (result.kind === "pending" && result.pr.state === "open") {
|
|
4880
|
+
const requestedAt = result.requestedAt.getTime();
|
|
4881
|
+
baseline.set(key, requestedAt);
|
|
4882
|
+
pending.push({ key, pr: result.pr, requestedAt });
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
if (previous === null) {
|
|
4886
|
+
return { baseline, newRequests: [], reRequests: [] };
|
|
4887
|
+
}
|
|
4888
|
+
const newRequests = [];
|
|
4889
|
+
const reRequests = [];
|
|
4890
|
+
for (const { key, pr, requestedAt } of pending) {
|
|
4891
|
+
const before = previous.get(key);
|
|
4892
|
+
if (before !== void 0 && before >= requestedAt) {
|
|
4893
|
+
continue;
|
|
4894
|
+
}
|
|
4895
|
+
(reviewedKeys.has(key) ? reRequests : newRequests).push(pr);
|
|
4896
|
+
}
|
|
4897
|
+
return { baseline, newRequests, reRequests };
|
|
4898
|
+
}
|
|
4899
|
+
var MAX_LISTED = 3;
|
|
4900
|
+
var TEST_NOTIFICATION = {
|
|
4901
|
+
title: "pr-stats",
|
|
4902
|
+
body: "Desktop notifications are working. New review requests show up like this."
|
|
4903
|
+
};
|
|
4904
|
+
function describeReviewRequests(changes) {
|
|
4905
|
+
const notifications2 = [];
|
|
4906
|
+
if (changes.newRequests.length > 0) {
|
|
4907
|
+
notifications2.push(describe(changes.newRequests, "Review requested on", "new PRs awaiting your review"));
|
|
4908
|
+
}
|
|
4909
|
+
if (changes.reRequests.length > 0) {
|
|
4910
|
+
notifications2.push(describe(changes.reRequests, "Review re-requested on", "PRs came back for review"));
|
|
4911
|
+
}
|
|
4912
|
+
return notifications2;
|
|
4913
|
+
}
|
|
4914
|
+
function describeSnoozeWakeUps(prs) {
|
|
4915
|
+
if (prs.length === 0) {
|
|
4916
|
+
return [];
|
|
4917
|
+
}
|
|
4918
|
+
return [describe(prs, "Snooze ended on", "snoozed PRs are back in your queue")];
|
|
4919
|
+
}
|
|
4920
|
+
function describe(prs, singleTitle, pluralTitle) {
|
|
4921
|
+
if (prs.length === 1) {
|
|
4922
|
+
const [pr] = prs;
|
|
4923
|
+
return { title: `${singleTitle} ${refOf(pr)}`, body: pr.title };
|
|
4924
|
+
}
|
|
4925
|
+
const lines = prs.slice(0, MAX_LISTED).map((pr) => `${refOf(pr)} ${pr.title}`);
|
|
4926
|
+
const rest = prs.length - lines.length;
|
|
4927
|
+
if (rest > 0) {
|
|
4928
|
+
lines.push(`and ${rest} more`);
|
|
4929
|
+
}
|
|
4930
|
+
return { title: `${prs.length} ${pluralTitle}`, body: lines.join("\n") };
|
|
4931
|
+
}
|
|
4932
|
+
function refOf(pr) {
|
|
4933
|
+
return `${pr.repo}#${pr.number}`;
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4630
4936
|
// src/tui/hooks/useAutoReload.ts
|
|
4631
4937
|
import { useEffect as useEffect4, useEffectEvent } from "react";
|
|
4632
4938
|
function useAutoReload(intervalMs, loading, reload) {
|
|
@@ -4752,68 +5058,6 @@ function useLoader(options, noCache2, callbacks = {}) {
|
|
|
4752
5058
|
|
|
4753
5059
|
// src/tui/hooks/useReviewNotifications.ts
|
|
4754
5060
|
import { useRef as useRef5 } from "react";
|
|
4755
|
-
|
|
4756
|
-
// src/tui/data/notifications.ts
|
|
4757
|
-
function diffReviewRequests(previous, results) {
|
|
4758
|
-
const baseline = /* @__PURE__ */ new Map();
|
|
4759
|
-
const reviewedKeys = /* @__PURE__ */ new Set();
|
|
4760
|
-
const pending = [];
|
|
4761
|
-
for (const result of results) {
|
|
4762
|
-
const key = `${result.pr.repo}#${result.pr.number}`;
|
|
4763
|
-
if (result.kind === "reviewed") {
|
|
4764
|
-
reviewedKeys.add(key);
|
|
4765
|
-
} else if (result.kind === "pending" && result.pr.state === "open") {
|
|
4766
|
-
const requestedAt = result.requestedAt.getTime();
|
|
4767
|
-
baseline.set(key, requestedAt);
|
|
4768
|
-
pending.push({ key, pr: result.pr, requestedAt });
|
|
4769
|
-
}
|
|
4770
|
-
}
|
|
4771
|
-
if (previous === null) {
|
|
4772
|
-
return { baseline, newRequests: [], reRequests: [] };
|
|
4773
|
-
}
|
|
4774
|
-
const newRequests = [];
|
|
4775
|
-
const reRequests = [];
|
|
4776
|
-
for (const { key, pr, requestedAt } of pending) {
|
|
4777
|
-
const before = previous.get(key);
|
|
4778
|
-
if (before !== void 0 && before >= requestedAt) {
|
|
4779
|
-
continue;
|
|
4780
|
-
}
|
|
4781
|
-
(reviewedKeys.has(key) ? reRequests : newRequests).push(pr);
|
|
4782
|
-
}
|
|
4783
|
-
return { baseline, newRequests, reRequests };
|
|
4784
|
-
}
|
|
4785
|
-
var MAX_LISTED = 3;
|
|
4786
|
-
var TEST_NOTIFICATION = {
|
|
4787
|
-
title: "pr-stats",
|
|
4788
|
-
body: "Desktop notifications are working. New review requests show up like this."
|
|
4789
|
-
};
|
|
4790
|
-
function describeReviewRequests(changes) {
|
|
4791
|
-
const notifications2 = [];
|
|
4792
|
-
if (changes.newRequests.length > 0) {
|
|
4793
|
-
notifications2.push(describe(changes.newRequests, "Review requested on", "new PRs awaiting your review"));
|
|
4794
|
-
}
|
|
4795
|
-
if (changes.reRequests.length > 0) {
|
|
4796
|
-
notifications2.push(describe(changes.reRequests, "Review re-requested on", "PRs came back for review"));
|
|
4797
|
-
}
|
|
4798
|
-
return notifications2;
|
|
4799
|
-
}
|
|
4800
|
-
function describe(prs, singleTitle, pluralTitle) {
|
|
4801
|
-
if (prs.length === 1) {
|
|
4802
|
-
const [pr] = prs;
|
|
4803
|
-
return { title: `${singleTitle} ${refOf(pr)}`, body: pr.title };
|
|
4804
|
-
}
|
|
4805
|
-
const lines = prs.slice(0, MAX_LISTED).map((pr) => `${refOf(pr)} ${pr.title}`);
|
|
4806
|
-
const rest = prs.length - lines.length;
|
|
4807
|
-
if (rest > 0) {
|
|
4808
|
-
lines.push(`and ${rest} more`);
|
|
4809
|
-
}
|
|
4810
|
-
return { title: `${prs.length} ${pluralTitle}`, body: lines.join("\n") };
|
|
4811
|
-
}
|
|
4812
|
-
function refOf(pr) {
|
|
4813
|
-
return `${pr.repo}#${pr.number}`;
|
|
4814
|
-
}
|
|
4815
|
-
|
|
4816
|
-
// src/tui/hooks/useReviewNotifications.ts
|
|
4817
5061
|
function useReviewNotifications(enabled2, notify, onError) {
|
|
4818
5062
|
const baselineRef = useRef5(null);
|
|
4819
5063
|
return (key, results) => {
|
|
@@ -4829,6 +5073,53 @@ function useReviewNotifications(enabled2, notify, onError) {
|
|
|
4829
5073
|
};
|
|
4830
5074
|
}
|
|
4831
5075
|
|
|
5076
|
+
// src/tui/hooks/useSnoozes.ts
|
|
5077
|
+
import { useState as useState4 } from "react";
|
|
5078
|
+
function useSnoozes(initial2) {
|
|
5079
|
+
const [snoozes2, setSnoozes] = useState4(initial2);
|
|
5080
|
+
const commit = (next) => {
|
|
5081
|
+
setSnoozes(next);
|
|
5082
|
+
return writeSnoozes(next);
|
|
5083
|
+
};
|
|
5084
|
+
return {
|
|
5085
|
+
snoozes: snoozes2,
|
|
5086
|
+
add: (snooze) => commit([...snoozes2.filter((entry) => entry.ref !== snooze.ref), snooze]),
|
|
5087
|
+
remove: (refs) => commit(snoozes2.filter((entry) => !refs.includes(entry.ref)))
|
|
5088
|
+
};
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
// src/tui/hooks/useSnoozeWakeups.ts
|
|
5092
|
+
import { useEffect as useEffect7, useEffectEvent as useEffectEvent2 } from "react";
|
|
5093
|
+
var MAX_TIMER_MS = 2 ** 31 - 1;
|
|
5094
|
+
function useSnoozeWakeups(snoozes2, ready, onWakeUp) {
|
|
5095
|
+
const wake = useEffectEvent2(() => {
|
|
5096
|
+
onWakeUp();
|
|
5097
|
+
});
|
|
5098
|
+
const next = nextWakeUp(snoozes2);
|
|
5099
|
+
useEffect7(() => {
|
|
5100
|
+
if (!ready || next === null) {
|
|
5101
|
+
return void 0;
|
|
5102
|
+
}
|
|
5103
|
+
let timer;
|
|
5104
|
+
const arm = () => {
|
|
5105
|
+
timer = setTimeout(
|
|
5106
|
+
() => {
|
|
5107
|
+
if (Date.now() < next) {
|
|
5108
|
+
arm();
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5111
|
+
wake();
|
|
5112
|
+
},
|
|
5113
|
+
Math.min(Math.max(0, next - Date.now()), MAX_TIMER_MS)
|
|
5114
|
+
);
|
|
5115
|
+
};
|
|
5116
|
+
arm();
|
|
5117
|
+
return () => {
|
|
5118
|
+
clearTimeout(timer);
|
|
5119
|
+
};
|
|
5120
|
+
}, [ready, next]);
|
|
5121
|
+
}
|
|
5122
|
+
|
|
4832
5123
|
// src/tui/hooks/useViewModel.ts
|
|
4833
5124
|
import { useMemo } from "react";
|
|
4834
5125
|
|
|
@@ -4886,12 +5177,12 @@ function buildSizeRepoOptions(raw) {
|
|
|
4886
5177
|
}
|
|
4887
5178
|
function pendingDetail(counts) {
|
|
4888
5179
|
const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
|
|
4889
|
-
return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
|
|
5180
|
+
return awaiting + (counts.snoozed > 0 ? `, ${counts.snoozed} snoozed` : "") + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
|
|
4890
5181
|
}
|
|
4891
|
-
function buildPendingRepoOptions(raw) {
|
|
5182
|
+
function buildPendingRepoOptions(raw, snoozes2 = [], now = Date.now()) {
|
|
4892
5183
|
const countsByRepo = /* @__PURE__ */ new Map();
|
|
4893
5184
|
const countsOf = (repo) => {
|
|
4894
|
-
const counts = countsByRepo.get(repo) ?? { awaiting: 0, reviewing: 0 };
|
|
5185
|
+
const counts = countsByRepo.get(repo) ?? { awaiting: 0, snoozed: 0, reviewing: 0 };
|
|
4895
5186
|
countsByRepo.set(repo, counts);
|
|
4896
5187
|
return counts;
|
|
4897
5188
|
};
|
|
@@ -4902,18 +5193,23 @@ function buildPendingRepoOptions(raw) {
|
|
|
4902
5193
|
return [];
|
|
4903
5194
|
}
|
|
4904
5195
|
const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
|
|
4905
|
-
|
|
5196
|
+
const { awaiting, snoozed } = splitSnoozed(stats.pending, snoozes2, now);
|
|
5197
|
+
for (const entry of awaiting) {
|
|
4906
5198
|
countsOf(entry.pr.repo).awaiting += 1;
|
|
4907
5199
|
}
|
|
5200
|
+
for (const entry of snoozed) {
|
|
5201
|
+
countsOf(entry.pr.repo).snoozed += 1;
|
|
5202
|
+
}
|
|
4908
5203
|
for (const entry of stats.reviewing) {
|
|
4909
5204
|
countsOf(entry.pr.repo).reviewing += 1;
|
|
4910
5205
|
}
|
|
4911
5206
|
const entries = [...countsByRepo.entries()].toSorted(
|
|
4912
|
-
(a, b) => b[1].awaiting - a[1].awaiting || b[1].reviewing - a[1].reviewing || a[0].localeCompare(b[0])
|
|
5207
|
+
(a, b) => b[1].awaiting - a[1].awaiting || b[1].snoozed - a[1].snoozed || b[1].reviewing - a[1].reviewing || a[0].localeCompare(b[0])
|
|
4913
5208
|
);
|
|
4914
|
-
const totals = { awaiting: 0, reviewing: 0 };
|
|
5209
|
+
const totals = { awaiting: 0, snoozed: 0, reviewing: 0 };
|
|
4915
5210
|
for (const [, counts] of entries) {
|
|
4916
5211
|
totals.awaiting += counts.awaiting;
|
|
5212
|
+
totals.snoozed += counts.snoozed;
|
|
4917
5213
|
totals.reviewing += counts.reviewing;
|
|
4918
5214
|
}
|
|
4919
5215
|
return [
|
|
@@ -5087,10 +5383,10 @@ function buildBarsCard({ title, subtitle, rows, format, expanded = false }) {
|
|
|
5087
5383
|
}
|
|
5088
5384
|
|
|
5089
5385
|
// src/tui/views/charts/weeks.ts
|
|
5090
|
-
var
|
|
5091
|
-
var
|
|
5386
|
+
var DAY_MS2 = 864e5;
|
|
5387
|
+
var WEEK_MS2 = 7 * DAY_MS2;
|
|
5092
5388
|
function mondayOf(dayUtcMs) {
|
|
5093
|
-
return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 *
|
|
5389
|
+
return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 * DAY_MS2;
|
|
5094
5390
|
}
|
|
5095
5391
|
function dateLabel(ms) {
|
|
5096
5392
|
return new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
@@ -5124,14 +5420,14 @@ function buildCumulativeCard({ title, series, legend }) {
|
|
|
5124
5420
|
subtitle.push({ text: `, ${legend}`, fg: theme.muted });
|
|
5125
5421
|
const mondays = series.flatMap((entry) => entry.dates.map((date) => weekOf(date)));
|
|
5126
5422
|
const first = Math.min(...mondays);
|
|
5127
|
-
const weekCount = (Math.max(...mondays) - first) /
|
|
5423
|
+
const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
|
|
5128
5424
|
if (weekCount < 2) {
|
|
5129
5425
|
return { title, subtitle, lines: [[{ text: "not enough weeks to draw a trend", fg: theme.muted }]] };
|
|
5130
5426
|
}
|
|
5131
5427
|
const totals = series.map((entry) => {
|
|
5132
5428
|
const weekly = Array.from({ length: weekCount }, () => 0);
|
|
5133
5429
|
for (const date of entry.dates) {
|
|
5134
|
-
weekly[(weekOf(date) - first) /
|
|
5430
|
+
weekly[(weekOf(date) - first) / WEEK_MS2] += 1;
|
|
5135
5431
|
}
|
|
5136
5432
|
let running = 0;
|
|
5137
5433
|
return weekly.map((count2) => running += count2);
|
|
@@ -5184,7 +5480,7 @@ function buildCumulativeCard({ title, series, legend }) {
|
|
|
5184
5480
|
yWidth + 2,
|
|
5185
5481
|
weekCount,
|
|
5186
5482
|
(week) => Math.round(week / (weekCount - 1) * (CUM_WIDTH - 1)),
|
|
5187
|
-
(week) => first + week *
|
|
5483
|
+
(week) => first + week * WEEK_MS2
|
|
5188
5484
|
)
|
|
5189
5485
|
);
|
|
5190
5486
|
return { title, subtitle, lines };
|
|
@@ -5533,7 +5829,7 @@ function buildTrendCard({
|
|
|
5533
5829
|
}
|
|
5534
5830
|
const mondays = [...byWeek.keys()];
|
|
5535
5831
|
const first = Math.min(...mondays);
|
|
5536
|
-
const weekCount = (Math.max(...mondays) - first) /
|
|
5832
|
+
const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
|
|
5537
5833
|
const scaleSuffix = scale === "log" ? ", log scale" : "";
|
|
5538
5834
|
if (weekCount < 2) {
|
|
5539
5835
|
return {
|
|
@@ -5544,7 +5840,7 @@ function buildTrendCard({
|
|
|
5544
5840
|
}
|
|
5545
5841
|
const medians = [];
|
|
5546
5842
|
for (let week = 0; week < weekCount; week++) {
|
|
5547
|
-
const values = byWeek.get(first + week *
|
|
5843
|
+
const values = byWeek.get(first + week * WEEK_MS2);
|
|
5548
5844
|
if (values !== void 0) {
|
|
5549
5845
|
medians.push(
|
|
5550
5846
|
percentile(
|
|
@@ -5606,7 +5902,7 @@ function buildTrendCard({
|
|
|
5606
5902
|
prefix,
|
|
5607
5903
|
points.length,
|
|
5608
5904
|
(point) => point * stretch,
|
|
5609
|
-
(point) => first + point * chunk *
|
|
5905
|
+
(point) => first + point * chunk * WEEK_MS2
|
|
5610
5906
|
)
|
|
5611
5907
|
);
|
|
5612
5908
|
const subtitle = chunk === 1 ? `weekly ${valueLabel}${scaleSuffix}` : `${valueLabel} per ${chunk} weeks${scaleSuffix}`;
|
|
@@ -5620,10 +5916,10 @@ var V_PARTIALS = [" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586
|
|
|
5620
5916
|
function buildVolumeCard(title, dates, weights) {
|
|
5621
5917
|
const mondays = dates.map((date) => mondayOf(zonedStamp(date).dayUtcMs));
|
|
5622
5918
|
const first = Math.min(...mondays);
|
|
5623
|
-
const weekCount = (Math.max(...mondays) - first) /
|
|
5919
|
+
const weekCount = (Math.max(...mondays) - first) / WEEK_MS2 + 1;
|
|
5624
5920
|
const weekly = Array.from({ length: weekCount }, () => 0);
|
|
5625
5921
|
for (const [i, monday] of mondays.entries()) {
|
|
5626
|
-
weekly[(monday - first) /
|
|
5922
|
+
weekly[(monday - first) / WEEK_MS2] += weights?.[i] ?? 1;
|
|
5627
5923
|
}
|
|
5628
5924
|
const chunk = Math.ceil(weekCount / VOLUME_MAX_BARS);
|
|
5629
5925
|
const bars = [];
|
|
@@ -5661,7 +5957,7 @@ function buildVolumeCard(title, dates, weights) {
|
|
|
5661
5957
|
yWidth + 2,
|
|
5662
5958
|
bars.length,
|
|
5663
5959
|
(bar) => bar * 3,
|
|
5664
|
-
(bar) => first + bar * chunk *
|
|
5960
|
+
(bar) => first + bar * chunk * WEEK_MS2
|
|
5665
5961
|
)
|
|
5666
5962
|
);
|
|
5667
5963
|
const total = weights === void 0 ? dates.length : weights.reduce((sum, weight) => sum + weight, 0);
|
|
@@ -5690,7 +5986,7 @@ function weeklySums(entries) {
|
|
|
5690
5986
|
const first = Math.min(...mondays);
|
|
5691
5987
|
const last = Math.max(...mondays);
|
|
5692
5988
|
const result = [];
|
|
5693
|
-
for (let monday = first; monday <= last; monday +=
|
|
5989
|
+
for (let monday = first; monday <= last; monday += WEEK_MS2) {
|
|
5694
5990
|
result.push({ date: mondayNoon(monday), value: byWeek.get(monday) ?? 0 });
|
|
5695
5991
|
}
|
|
5696
5992
|
return result;
|
|
@@ -6345,7 +6641,7 @@ function resolveScope(scope, repos) {
|
|
|
6345
6641
|
}
|
|
6346
6642
|
return dropVanishedRepo(scope, repos);
|
|
6347
6643
|
}
|
|
6348
|
-
function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoch) {
|
|
6644
|
+
function useViewModel(raw, options, width, scopes, grouping, expanded, snoozes2, themeEpoch) {
|
|
6349
6645
|
return useMemo(() => {
|
|
6350
6646
|
void themeEpoch;
|
|
6351
6647
|
configureTimeMode({
|
|
@@ -6365,7 +6661,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
|
|
|
6365
6661
|
percentile: options.targetPercentile === "" ? DEFAULT_TARGET_PERCENTILE : parseTargetPercentile(options.targetPercentile)
|
|
6366
6662
|
};
|
|
6367
6663
|
const sizeTarget = options.sizeTarget === "" ? void 0 : parseSizeTarget(options.sizeTarget);
|
|
6368
|
-
const pendingRepos = buildPendingRepoOptions(raw);
|
|
6664
|
+
const pendingRepos = buildPendingRepoOptions(raw, snoozes2);
|
|
6369
6665
|
const openRepos = buildOpenRepoOptions(raw);
|
|
6370
6666
|
const mergedRepos = buildMergedRepoOptions(raw);
|
|
6371
6667
|
const reviewRepos = buildReviewRepoOptions(raw);
|
|
@@ -6391,7 +6687,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
|
|
|
6391
6687
|
reviewScope,
|
|
6392
6688
|
sizeScope,
|
|
6393
6689
|
commentScope,
|
|
6394
|
-
pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending) : null,
|
|
6690
|
+
pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending, snoozes2) : null,
|
|
6395
6691
|
open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
|
|
6396
6692
|
merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width, expanded.merged) : null,
|
|
6397
6693
|
review,
|
|
@@ -6418,6 +6714,7 @@ function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoc
|
|
|
6418
6714
|
grouping.open,
|
|
6419
6715
|
expanded.review,
|
|
6420
6716
|
expanded.merged,
|
|
6717
|
+
snoozes2,
|
|
6421
6718
|
themeEpoch
|
|
6422
6719
|
]);
|
|
6423
6720
|
}
|
|
@@ -6548,6 +6845,13 @@ function handleSettingsModalKey(key, context) {
|
|
|
6548
6845
|
context.dispatchUi({ type: "cacheActionReported", action: saveCopyLinks(next) ? "saved" : "notSaved" });
|
|
6549
6846
|
break;
|
|
6550
6847
|
}
|
|
6848
|
+
case "snoozeDuration": {
|
|
6849
|
+
if (key.name !== "return") {
|
|
6850
|
+
break;
|
|
6851
|
+
}
|
|
6852
|
+
context.beginEdit(context.snoozeDuration);
|
|
6853
|
+
break;
|
|
6854
|
+
}
|
|
6551
6855
|
case "themePreset": {
|
|
6552
6856
|
const next = cycleTheme(context.themeState, key.name === "left" ? -1 : 1);
|
|
6553
6857
|
applyThemeState(next);
|
|
@@ -6696,6 +7000,23 @@ function handleQueueKey(key, context) {
|
|
|
6696
7000
|
}
|
|
6697
7001
|
break;
|
|
6698
7002
|
}
|
|
7003
|
+
case "s": {
|
|
7004
|
+
const cursor = context.browse.rowCursors[tab];
|
|
7005
|
+
const row = rows[Math.min(cursor, rows.length - 1)];
|
|
7006
|
+
if (row.pending === void 0) {
|
|
7007
|
+
break;
|
|
7008
|
+
}
|
|
7009
|
+
if (row.pending.snoozed) {
|
|
7010
|
+
context.unsnooze(row.ref);
|
|
7011
|
+
break;
|
|
7012
|
+
}
|
|
7013
|
+
context.dispatchUi({
|
|
7014
|
+
type: "snoozeModalOpened",
|
|
7015
|
+
target: { ref: row.ref, title: row.title, requestedAt: row.pending.requestedAt }
|
|
7016
|
+
});
|
|
7017
|
+
context.beginEdit(context.snoozeDuration);
|
|
7018
|
+
break;
|
|
7019
|
+
}
|
|
6699
7020
|
}
|
|
6700
7021
|
}
|
|
6701
7022
|
function statsTabOf(context) {
|
|
@@ -6791,7 +7112,7 @@ function handleAppKey(key, context) {
|
|
|
6791
7112
|
}
|
|
6792
7113
|
if (key.name === "o") {
|
|
6793
7114
|
context.dispatchUi({ type: "modalOpened", modal: "options" });
|
|
6794
|
-
} else if (key.name === "s") {
|
|
7115
|
+
} else if (key.name === "s" && key.shift) {
|
|
6795
7116
|
context.dispatchUi({ type: "modalOpened", modal: "settings" });
|
|
6796
7117
|
} else if (["1", "2", "3", "4", "5"].includes(key.name)) {
|
|
6797
7118
|
context.dispatchBrowse({ type: "tabSelected", tab: Number(key.name) - 1 });
|
|
@@ -6818,6 +7139,8 @@ var initialUiState = {
|
|
|
6818
7139
|
fieldError: null,
|
|
6819
7140
|
settingError: null,
|
|
6820
7141
|
themeColorError: null,
|
|
7142
|
+
snoozeTarget: null,
|
|
7143
|
+
snoozeError: null,
|
|
6821
7144
|
cacheAction: null,
|
|
6822
7145
|
openError: null,
|
|
6823
7146
|
successNotice: null
|
|
@@ -6836,7 +7159,7 @@ function uiReducer(state, action) {
|
|
|
6836
7159
|
case "openErrorReported": {
|
|
6837
7160
|
return { ...state, openError: action.message };
|
|
6838
7161
|
}
|
|
6839
|
-
case "
|
|
7162
|
+
case "successReported": {
|
|
6840
7163
|
return { ...state, openError: null, successNotice: { text: action.message } };
|
|
6841
7164
|
}
|
|
6842
7165
|
case "successNoticeExpired": {
|
|
@@ -6864,6 +7187,23 @@ function uiReducer(state, action) {
|
|
|
6864
7187
|
case "themeModalClosed": {
|
|
6865
7188
|
return { ...state, modal: "settings", themeColorError: null, cacheAction: null };
|
|
6866
7189
|
}
|
|
7190
|
+
case "snoozeModalOpened": {
|
|
7191
|
+
return { ...state, modal: "snooze", editing: true, snoozeTarget: action.target, snoozeError: null };
|
|
7192
|
+
}
|
|
7193
|
+
case "snoozeCommitted": {
|
|
7194
|
+
const closed = { ...state, modal: null, editing: false, snoozeTarget: null, snoozeError: null };
|
|
7195
|
+
if (action.saved) {
|
|
7196
|
+
return { ...closed, openError: null, successNotice: { text: action.message } };
|
|
7197
|
+
}
|
|
7198
|
+
return {
|
|
7199
|
+
...closed,
|
|
7200
|
+
openError: `${action.message} \xB7 the cache is disabled for this session, so the snooze is not saved`,
|
|
7201
|
+
successNotice: null
|
|
7202
|
+
};
|
|
7203
|
+
}
|
|
7204
|
+
case "snoozeErrorReported": {
|
|
7205
|
+
return { ...state, snoozeError: action.message };
|
|
7206
|
+
}
|
|
6867
7207
|
case "fieldSelectionMoved": {
|
|
6868
7208
|
return { ...state, selectedField: cycled2(state.selectedField, action.delta, FIELDS.length), fieldError: null };
|
|
6869
7209
|
}
|
|
@@ -6887,6 +7227,9 @@ function uiReducer(state, action) {
|
|
|
6887
7227
|
return { ...state, editing: true, fieldError: null, settingError: null, themeColorError: null };
|
|
6888
7228
|
}
|
|
6889
7229
|
case "editCancelled": {
|
|
7230
|
+
if (state.modal === "snooze") {
|
|
7231
|
+
return { ...state, modal: null, editing: false, snoozeTarget: null, snoozeError: null };
|
|
7232
|
+
}
|
|
6890
7233
|
return { ...state, editing: false, fieldError: null, settingError: null, themeColorError: null };
|
|
6891
7234
|
}
|
|
6892
7235
|
case "fieldCommitted": {
|
|
@@ -6979,7 +7322,7 @@ function createClipboardCopier(renderer2) {
|
|
|
6979
7322
|
}
|
|
6980
7323
|
|
|
6981
7324
|
// src/tui/App.tsx
|
|
6982
|
-
import { jsx as
|
|
7325
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "@opentui/react/jsx-runtime";
|
|
6983
7326
|
function App({
|
|
6984
7327
|
initial: initial2,
|
|
6985
7328
|
initialSaved = null,
|
|
@@ -6989,6 +7332,8 @@ function App({
|
|
|
6989
7332
|
initialNotifications = false,
|
|
6990
7333
|
initialNotifyChannel = "auto",
|
|
6991
7334
|
initialCopyLinks = false,
|
|
7335
|
+
initialSnoozeDuration = DEFAULT_SNOOZE_DURATION,
|
|
7336
|
+
initialSnoozes = [],
|
|
6992
7337
|
initialTheme = defaultThemeState(),
|
|
6993
7338
|
openUrl = openInBrowser,
|
|
6994
7339
|
copyUrl,
|
|
@@ -7001,15 +7346,17 @@ function App({
|
|
|
7001
7346
|
const copyLink = copyUrl ?? defaultCopyUrl;
|
|
7002
7347
|
const [ui, dispatchUi] = useReducer(uiReducer, initialUiState);
|
|
7003
7348
|
const [browse, dispatchBrowse] = useReducer(browseReducer, initialBrowseState);
|
|
7004
|
-
const [options, setOptions] =
|
|
7005
|
-
const [saved2, setSaved] =
|
|
7006
|
-
const [noCache2, setNoCache] =
|
|
7007
|
-
const [autoReload2, setAutoReload] =
|
|
7008
|
-
const [reloadInterval2, setReloadInterval] =
|
|
7009
|
-
const [notifications2, setNotifications] =
|
|
7010
|
-
const [notifyChannel2, setNotifyChannel] =
|
|
7011
|
-
const [copyLinks2, setCopyLinks] =
|
|
7012
|
-
const [
|
|
7349
|
+
const [options, setOptions] = useState5(initial2);
|
|
7350
|
+
const [saved2, setSaved] = useState5(initialSaved);
|
|
7351
|
+
const [noCache2, setNoCache] = useState5(initialNoCache);
|
|
7352
|
+
const [autoReload2, setAutoReload] = useState5(initialAutoReload);
|
|
7353
|
+
const [reloadInterval2, setReloadInterval] = useState5(initialReloadInterval);
|
|
7354
|
+
const [notifications2, setNotifications] = useState5(initialNotifications);
|
|
7355
|
+
const [notifyChannel2, setNotifyChannel] = useState5(initialNotifyChannel);
|
|
7356
|
+
const [copyLinks2, setCopyLinks] = useState5(initialCopyLinks);
|
|
7357
|
+
const [snoozeDuration2, setSnoozeDuration] = useState5(initialSnoozeDuration);
|
|
7358
|
+
const [themeState, setThemeState] = useState5(initialTheme);
|
|
7359
|
+
const snoozeStore = useSnoozes(initialSnoozes);
|
|
7013
7360
|
const defaultNotify = useMemo2(
|
|
7014
7361
|
() => createNotifier(notificationBoundary(renderer2), notifyChannel2),
|
|
7015
7362
|
[renderer2, notifyChannel2]
|
|
@@ -7019,9 +7366,13 @@ function App({
|
|
|
7019
7366
|
copyLink(row.url, (message) => {
|
|
7020
7367
|
dispatchUi({ type: "openErrorReported", message });
|
|
7021
7368
|
});
|
|
7022
|
-
dispatchUi({ type: "
|
|
7369
|
+
dispatchUi({ type: "successReported", message: `copied ${row.ref} to the clipboard` });
|
|
7023
7370
|
};
|
|
7024
|
-
|
|
7371
|
+
const unsnooze = (ref) => {
|
|
7372
|
+
snoozeStore.remove([ref]);
|
|
7373
|
+
dispatchUi({ type: "successReported", message: `unsnoozed ${ref}` });
|
|
7374
|
+
};
|
|
7375
|
+
useEffect8(() => {
|
|
7025
7376
|
if (ui.successNotice === null) {
|
|
7026
7377
|
return void 0;
|
|
7027
7378
|
}
|
|
@@ -7059,7 +7410,35 @@ function App({
|
|
|
7059
7410
|
}
|
|
7060
7411
|
});
|
|
7061
7412
|
useAutoReload(autoReload2 ? reloadIntervalMs(reloadInterval2) : null, loading, reload);
|
|
7062
|
-
|
|
7413
|
+
useSnoozeWakeups(snoozeStore.snoozes, raw !== null, () => {
|
|
7414
|
+
if (raw === null) {
|
|
7415
|
+
return;
|
|
7416
|
+
}
|
|
7417
|
+
const due = dueSnoozes(snoozeStore.snoozes, Date.now());
|
|
7418
|
+
if (due.length === 0) {
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
const woken = wokenPrs(due, raw.reviewResults);
|
|
7422
|
+
snoozeStore.remove(due.map((snooze) => snooze.ref));
|
|
7423
|
+
if (!notifications2) {
|
|
7424
|
+
return;
|
|
7425
|
+
}
|
|
7426
|
+
for (const notification of describeSnoozeWakeUps(woken)) {
|
|
7427
|
+
notifier(notification.title, notification.body, (message) => {
|
|
7428
|
+
dispatchUi({ type: "openErrorReported", message });
|
|
7429
|
+
});
|
|
7430
|
+
}
|
|
7431
|
+
});
|
|
7432
|
+
const views = useViewModel(
|
|
7433
|
+
raw,
|
|
7434
|
+
options,
|
|
7435
|
+
width,
|
|
7436
|
+
browse.scopes,
|
|
7437
|
+
browse.grouped,
|
|
7438
|
+
browse.expanded,
|
|
7439
|
+
snoozeStore.snoozes,
|
|
7440
|
+
themeState
|
|
7441
|
+
);
|
|
7063
7442
|
const showLoad = useDeferredLoading(loading, isSnapshot ? { showDelay: 0 } : void 0);
|
|
7064
7443
|
const draftRef = useRef6("");
|
|
7065
7444
|
const commitField = () => {
|
|
@@ -7093,6 +7472,49 @@ function App({
|
|
|
7093
7472
|
throw error2;
|
|
7094
7473
|
}
|
|
7095
7474
|
};
|
|
7475
|
+
const commitSnoozeDuration = () => {
|
|
7476
|
+
const value2 = draftRef.current.trim();
|
|
7477
|
+
try {
|
|
7478
|
+
parseSnoozeDuration(value2);
|
|
7479
|
+
setSnoozeDuration(value2);
|
|
7480
|
+
dispatchUi({ type: "settingCommitted", action: saveSnoozeDuration(value2) ? "saved" : "notSaved" });
|
|
7481
|
+
} catch (error2) {
|
|
7482
|
+
if (error2 instanceof CliError) {
|
|
7483
|
+
dispatchUi({ type: "settingErrorReported", message: error2.message });
|
|
7484
|
+
return;
|
|
7485
|
+
}
|
|
7486
|
+
throw error2;
|
|
7487
|
+
}
|
|
7488
|
+
};
|
|
7489
|
+
const commitSetting = () => {
|
|
7490
|
+
if (SETTINGS[ui.selectedSetting].key === "snoozeDuration") {
|
|
7491
|
+
commitSnoozeDuration();
|
|
7492
|
+
} else {
|
|
7493
|
+
commitReloadInterval();
|
|
7494
|
+
}
|
|
7495
|
+
};
|
|
7496
|
+
const commitSnooze = () => {
|
|
7497
|
+
const target = ui.snoozeTarget;
|
|
7498
|
+
if (target === null) {
|
|
7499
|
+
return;
|
|
7500
|
+
}
|
|
7501
|
+
const value2 = draftRef.current.trim();
|
|
7502
|
+
try {
|
|
7503
|
+
const until = Date.now() + parseSnoozeDuration(value2);
|
|
7504
|
+
const saved3 = snoozeStore.add({ ref: target.ref, until, requestedAt: target.requestedAt });
|
|
7505
|
+
dispatchUi({
|
|
7506
|
+
type: "snoozeCommitted",
|
|
7507
|
+
message: `snoozed ${target.ref} until ${formatWakeTime(until)}`,
|
|
7508
|
+
saved: saved3
|
|
7509
|
+
});
|
|
7510
|
+
} catch (error2) {
|
|
7511
|
+
if (error2 instanceof CliError) {
|
|
7512
|
+
dispatchUi({ type: "snoozeErrorReported", message: error2.message });
|
|
7513
|
+
return;
|
|
7514
|
+
}
|
|
7515
|
+
throw error2;
|
|
7516
|
+
}
|
|
7517
|
+
};
|
|
7096
7518
|
const commitThemeColor = () => {
|
|
7097
7519
|
try {
|
|
7098
7520
|
const next = withColorOverride(themeState, THEME_COLORS[ui.selectedThemeColor].key, draftRef.current.trim());
|
|
@@ -7120,6 +7542,7 @@ function App({
|
|
|
7120
7542
|
notifications: notifications2,
|
|
7121
7543
|
notifyChannel: notifyChannel2,
|
|
7122
7544
|
copyLinks: copyLinks2,
|
|
7545
|
+
snoozeDuration: snoozeDuration2,
|
|
7123
7546
|
themeState,
|
|
7124
7547
|
options,
|
|
7125
7548
|
views,
|
|
@@ -7139,6 +7562,7 @@ function App({
|
|
|
7139
7562
|
openUrl,
|
|
7140
7563
|
notify: notifier,
|
|
7141
7564
|
copyRow,
|
|
7565
|
+
unsnooze,
|
|
7142
7566
|
beginEdit: (value2) => {
|
|
7143
7567
|
draftRef.current = value2;
|
|
7144
7568
|
dispatchUi({ type: "editStarted" });
|
|
@@ -7150,8 +7574,8 @@ function App({
|
|
|
7150
7574
|
});
|
|
7151
7575
|
});
|
|
7152
7576
|
const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
|
|
7153
|
-
return /* @__PURE__ */
|
|
7154
|
-
/* @__PURE__ */
|
|
7577
|
+
return /* @__PURE__ */ jsxs14("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
|
|
7578
|
+
/* @__PURE__ */ jsx16(
|
|
7155
7579
|
Header,
|
|
7156
7580
|
{
|
|
7157
7581
|
options,
|
|
@@ -7161,8 +7585,8 @@ function App({
|
|
|
7161
7585
|
reloadEvery: autoReload2 ? reloadInterval2 : null
|
|
7162
7586
|
}
|
|
7163
7587
|
),
|
|
7164
|
-
/* @__PURE__ */
|
|
7165
|
-
/* @__PURE__ */
|
|
7588
|
+
/* @__PURE__ */ jsx16(TabBar, { tab: browse.tab }),
|
|
7589
|
+
/* @__PURE__ */ jsx16(
|
|
7166
7590
|
MainPanel,
|
|
7167
7591
|
{
|
|
7168
7592
|
views,
|
|
@@ -7181,7 +7605,7 @@ function App({
|
|
|
7181
7605
|
onRefClick: copyLinks2 ? copyRow : null
|
|
7182
7606
|
}
|
|
7183
7607
|
),
|
|
7184
|
-
/* @__PURE__ */
|
|
7608
|
+
/* @__PURE__ */ jsx16(
|
|
7185
7609
|
Footer,
|
|
7186
7610
|
{
|
|
7187
7611
|
width,
|
|
@@ -7190,13 +7614,14 @@ function App({
|
|
|
7190
7614
|
tab: browse.tab,
|
|
7191
7615
|
authoredTab: browse.authoredTab,
|
|
7192
7616
|
views,
|
|
7617
|
+
pendingCursor: browse.rowCursors.pending,
|
|
7193
7618
|
copyLinks: copyLinks2,
|
|
7194
7619
|
openError: ui.openError,
|
|
7195
7620
|
successNotice: ui.successNotice === null ? null : ui.successNotice.text,
|
|
7196
7621
|
stale
|
|
7197
7622
|
}
|
|
7198
7623
|
),
|
|
7199
|
-
/* @__PURE__ */
|
|
7624
|
+
/* @__PURE__ */ jsx16(
|
|
7200
7625
|
Modals,
|
|
7201
7626
|
{
|
|
7202
7627
|
ui,
|
|
@@ -7208,13 +7633,15 @@ function App({
|
|
|
7208
7633
|
notifications: notifications2,
|
|
7209
7634
|
notifyChannel: notifyChannel2,
|
|
7210
7635
|
copyLinks: copyLinks2,
|
|
7636
|
+
snoozeDuration: snoozeDuration2,
|
|
7211
7637
|
themeState,
|
|
7212
7638
|
onDraft: (value2) => {
|
|
7213
7639
|
draftRef.current = value2;
|
|
7214
7640
|
},
|
|
7215
7641
|
onSubmitField: commitField,
|
|
7216
|
-
|
|
7642
|
+
onSubmitSetting: commitSetting,
|
|
7217
7643
|
onSubmitThemeColor: commitThemeColor,
|
|
7644
|
+
onSubmitSnooze: commitSnooze,
|
|
7218
7645
|
onToggleReviewType: (type) => {
|
|
7219
7646
|
setOptions((previous) => {
|
|
7220
7647
|
return { ...previous, reviewTypes: toggleReviewType(previous.reviewTypes, type) };
|
|
@@ -7272,6 +7699,8 @@ function bootstrap() {
|
|
|
7272
7699
|
notifications: settings.notifications === true,
|
|
7273
7700
|
notifyChannel: settings.notifyChannel ?? "auto",
|
|
7274
7701
|
copyLinks: settings.copyLinks === true,
|
|
7702
|
+
snoozeDuration: settings.snoozeDuration ?? DEFAULT_SNOOZE_DURATION,
|
|
7703
|
+
snoozes: readSnoozes(),
|
|
7275
7704
|
theme: theme3,
|
|
7276
7705
|
json: values.json
|
|
7277
7706
|
};
|
|
@@ -7284,8 +7713,21 @@ function bootstrap() {
|
|
|
7284
7713
|
}
|
|
7285
7714
|
|
|
7286
7715
|
// src/tui/main.tsx
|
|
7287
|
-
import { jsx as
|
|
7288
|
-
var {
|
|
7716
|
+
import { jsx as jsx17 } from "@opentui/react/jsx-runtime";
|
|
7717
|
+
var {
|
|
7718
|
+
initial,
|
|
7719
|
+
saved,
|
|
7720
|
+
noCache,
|
|
7721
|
+
autoReload,
|
|
7722
|
+
reloadInterval,
|
|
7723
|
+
notifications,
|
|
7724
|
+
notifyChannel,
|
|
7725
|
+
copyLinks,
|
|
7726
|
+
snoozeDuration,
|
|
7727
|
+
snoozes,
|
|
7728
|
+
theme: theme2,
|
|
7729
|
+
json
|
|
7730
|
+
} = bootstrap();
|
|
7289
7731
|
if (json) {
|
|
7290
7732
|
await runJsonStats(initial, noCache);
|
|
7291
7733
|
}
|
|
@@ -7298,7 +7740,7 @@ for (const signal of ["SIGTERM", "SIGHUP"]) {
|
|
|
7298
7740
|
});
|
|
7299
7741
|
}
|
|
7300
7742
|
createRoot(renderer).render(
|
|
7301
|
-
/* @__PURE__ */
|
|
7743
|
+
/* @__PURE__ */ jsx17(
|
|
7302
7744
|
App,
|
|
7303
7745
|
{
|
|
7304
7746
|
initial,
|
|
@@ -7309,6 +7751,8 @@ createRoot(renderer).render(
|
|
|
7309
7751
|
initialNotifications: notifications,
|
|
7310
7752
|
initialNotifyChannel: notifyChannel,
|
|
7311
7753
|
initialCopyLinks: copyLinks,
|
|
7754
|
+
initialSnoozeDuration: snoozeDuration,
|
|
7755
|
+
initialSnoozes: snoozes,
|
|
7312
7756
|
initialTheme: theme2,
|
|
7313
7757
|
onQuit: () => {
|
|
7314
7758
|
renderer.destroy();
|