@d3lm/pr-stats 0.2.5 → 0.2.6
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/README.md +2 -2
- package/dist/tui-app.mjs +210 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pr-stats
|
|
2
2
|
|
|
3
|
-
An interactive terminal UI, built with [OpenTUI](https://github.com/anomalyco/opentui) and React, that shows statistics for a GitHub user, e.g., time to review, size of authored PRs, or comments received on them. It works with any repository your GitHub login can see. It has a queue tab for the open PRs awaiting your review, a Your PRs tab that splits into your open PRs and a merged-and-closed report, chart tabs for the review-time, size, and comments reports, and a live options modal.
|
|
3
|
+
An interactive terminal UI, built with [OpenTUI](https://github.com/anomalyco/opentui) and React, that shows statistics for a GitHub user, e.g., time to review, size of authored PRs, or comments received on them. It works with any repository your GitHub login can see. It has a queue tab for the open PRs on your reviewing plate, split into those awaiting your review and those you already reviewed, a Your PRs tab that splits into your open PRs and a merged-and-closed report, chart tabs for the review-time, size, and comments reports, and a live options modal.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ You can also run it without installing through `npx @d3lm/pr-stats`. The bunx eq
|
|
|
25
25
|
pr-stats
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Without flags, it looks at PRs from the last 90 days across all repositories you can access. It opens on a tab
|
|
28
|
+
Without flags, it looks at PRs from the last 90 days across all repositories you can access. It opens on a tab with two lists, the open PRs awaiting your review with how long each has been waiting, and below them the open PRs you already reviewed or commented on with how long ago that was, so a PR stays visible until it merges or closes. A fresh review request moves a PR from the reviewing list back into the awaiting one. The Your PRs tab next to it has two sub-tabs, which the `t` key switches. The first lists your own authored PRs that are still open with their age and size. The second charts how your authored PRs got created, merged, and closed, telling a merge apart from a close without one, with time-to-merge percentiles, a histogram and trend, a merge-time heatmap, weekly created and merged volumes, an outcome gauge, and the most recently merged and closed PRs. The remaining tabs hold the time-to-review report, the PR size report, and the comments report with a histogram of comments per PR, a scatter of comments against PR size, and the most commented PRs. When the data spans multiple repos, every tab opens on a repo picker that drills into one repo or the aggregate across all of them, and on the two queue lists the `g` key groups the aggregate list by repo.
|
|
29
29
|
|
|
30
30
|
Node gates the FFI that OpenTUI renders through behind the `--experimental-ffi` flag, and the launcher re-executes itself with that flag when it is missing, so a plain `pr-stats` works without extra flags on both runtimes.
|
|
31
31
|
|
package/dist/tui-app.mjs
CHANGED
|
@@ -787,6 +787,24 @@ function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Da
|
|
|
787
787
|
}
|
|
788
788
|
}
|
|
789
789
|
pending.sort((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
|
|
790
|
+
const pendingKeys = new Set(pending.map((entry) => `${entry.pr.repo}#${entry.pr.number}`));
|
|
791
|
+
const latestReviews = /* @__PURE__ */ new Map();
|
|
792
|
+
for (const result of results) {
|
|
793
|
+
if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const key = `${result.pr.repo}#${result.pr.number}`;
|
|
797
|
+
if (pendingKeys.has(key)) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const latest = latestReviews.get(key);
|
|
801
|
+
if (latest === void 0 || result.reviewedAt > latest.reviewedAt) {
|
|
802
|
+
latestReviews.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const reviewing = [...latestReviews.values()].map(({ pr, reviewedAt }) => {
|
|
806
|
+
return { pr, reviewedAt, hours: durationHours(reviewedAt, now) };
|
|
807
|
+
}).toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
|
|
790
808
|
const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
|
|
791
809
|
const unrequested = results.filter((result) => result.kind === "unrequested");
|
|
792
810
|
const allHours = reviewed.map((result) => result.hours);
|
|
@@ -798,7 +816,7 @@ function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Da
|
|
|
798
816
|
}
|
|
799
817
|
const byRepo = [...hoursByRepo.entries()].toSorted((a, b) => b[1].length - a[1].length);
|
|
800
818
|
const misses = targetHours === void 0 ? [] : reviewed.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
|
|
801
|
-
return { reviewed, pending, expired, unrequested, allHours, byRepo, misses };
|
|
819
|
+
return { reviewed, pending, reviewing, expired, unrequested, allHours, byRepo, misses };
|
|
802
820
|
}
|
|
803
821
|
function computeSizeStats(sizes, { sizeTarget } = {}) {
|
|
804
822
|
const metrics = [
|
|
@@ -948,7 +966,7 @@ function toPrRows(entries, leads) {
|
|
|
948
966
|
|
|
949
967
|
// src/tui/views/queue.ts
|
|
950
968
|
function queueRows(view) {
|
|
951
|
-
return view.lists.flatMap((list) => list.rows);
|
|
969
|
+
return view.sections.flatMap((section) => [...section.rows, ...section.lists.flatMap((list) => list.rows)]);
|
|
952
970
|
}
|
|
953
971
|
function groupedLists(entries, rowsOf2) {
|
|
954
972
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -962,23 +980,26 @@ function groupedLists(entries, rowsOf2) {
|
|
|
962
980
|
});
|
|
963
981
|
}
|
|
964
982
|
function buildPendingReviewView(raw, repo = null, grouped = false) {
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
983
|
+
const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
|
|
984
|
+
const awaiting = repo === null ? stats.pending : stats.pending.filter((entry) => entry.pr.repo === repo);
|
|
985
|
+
const reviewing = repo === null ? stats.reviewing : stats.reviewing.filter((entry) => entry.pr.repo === repo);
|
|
986
|
+
if (awaiting.length === 0 && reviewing.length === 0) {
|
|
987
|
+
return { empty: "No PRs are awaiting your review, and none you reviewed are still open.", sections: [] };
|
|
988
|
+
}
|
|
989
|
+
const split = repo === null && grouped;
|
|
990
|
+
const sectionOf = (title, entries) => split ? { title, rows: [], lists: groupedLists(entries, rowsOf) } : { title, rows: rowsOf(entries), lists: [] };
|
|
973
991
|
return {
|
|
974
992
|
empty: null,
|
|
975
|
-
|
|
993
|
+
sections: [
|
|
994
|
+
...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting)],
|
|
995
|
+
...reviewing.length === 0 ? [] : [sectionOf(`Reviewing (n=${reviewing.length})`, reviewing)]
|
|
996
|
+
]
|
|
976
997
|
};
|
|
977
998
|
}
|
|
978
999
|
function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
979
1000
|
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());
|
|
980
1001
|
if (open.length === 0) {
|
|
981
|
-
return { empty: "No open authored PRs found.",
|
|
1002
|
+
return { empty: "No open authored PRs found.", sections: [] };
|
|
982
1003
|
}
|
|
983
1004
|
const rowsOf2 = (group) => {
|
|
984
1005
|
const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
|
|
@@ -990,13 +1011,11 @@ function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
|
990
1011
|
)
|
|
991
1012
|
);
|
|
992
1013
|
};
|
|
1014
|
+
const title = `Your open authored PRs (n=${open.length})`;
|
|
993
1015
|
if (repo === null && grouped) {
|
|
994
|
-
return { empty: null, lists: groupedLists(open, rowsOf2) };
|
|
1016
|
+
return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf2) }] };
|
|
995
1017
|
}
|
|
996
|
-
return {
|
|
997
|
-
empty: null,
|
|
998
|
-
lists: [{ title: `Your open authored PRs (n=${open.length})`, rows: rowsOf2(open) }]
|
|
999
|
-
};
|
|
1018
|
+
return { empty: null, sections: [{ title, rows: rowsOf2(open), lists: [] }] };
|
|
1000
1019
|
}
|
|
1001
1020
|
function rowsOf(group) {
|
|
1002
1021
|
return toPrRows(
|
|
@@ -1210,7 +1229,7 @@ function QueuePanel({
|
|
|
1210
1229
|
heading,
|
|
1211
1230
|
warning,
|
|
1212
1231
|
empty,
|
|
1213
|
-
|
|
1232
|
+
sections,
|
|
1214
1233
|
cursor,
|
|
1215
1234
|
onRefClick
|
|
1216
1235
|
}) {
|
|
@@ -1237,10 +1256,57 @@ function QueuePanel({
|
|
|
1237
1256
|
}
|
|
1238
1257
|
const offsets = [];
|
|
1239
1258
|
let total = 0;
|
|
1240
|
-
for (const
|
|
1241
|
-
|
|
1242
|
-
total +=
|
|
1243
|
-
|
|
1259
|
+
for (const section of sections) {
|
|
1260
|
+
const entry = { rows: total, lists: [] };
|
|
1261
|
+
total += section.rows.length;
|
|
1262
|
+
for (const list of section.lists) {
|
|
1263
|
+
entry.lists.push(total);
|
|
1264
|
+
total += list.rows.length;
|
|
1265
|
+
}
|
|
1266
|
+
offsets.push(entry);
|
|
1267
|
+
}
|
|
1268
|
+
const renderRow = (row, index, indent) => {
|
|
1269
|
+
const isSelected = index === cursor;
|
|
1270
|
+
const bg = isSelected ? theme.selectedBg : void 0;
|
|
1271
|
+
const refStart = indent.length + 2 + row.lead.length + 2;
|
|
1272
|
+
return /* @__PURE__ */ jsxs5(
|
|
1273
|
+
"text",
|
|
1274
|
+
{
|
|
1275
|
+
id: `queue-row-${index}`,
|
|
1276
|
+
wrapMode: "none",
|
|
1277
|
+
onMouseDown: onRefClick === null ? void 0 : (event) => {
|
|
1278
|
+
lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
|
|
1279
|
+
},
|
|
1280
|
+
onMouseUp: onRefClick === null ? void 0 : (event) => {
|
|
1281
|
+
const down = lastDown.current;
|
|
1282
|
+
lastDown.current = null;
|
|
1283
|
+
if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
|
|
1287
|
+
if (local >= refStart && local < refStart + row.ref.length) {
|
|
1288
|
+
onRefClick(row);
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
children: [
|
|
1292
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.accent, children: [
|
|
1293
|
+
indent,
|
|
1294
|
+
isSelected ? "\u25B8 " : " "
|
|
1295
|
+
] }),
|
|
1296
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1297
|
+
row.lead,
|
|
1298
|
+
" "
|
|
1299
|
+
] }),
|
|
1300
|
+
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 }),
|
|
1301
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1302
|
+
" ",
|
|
1303
|
+
row.title
|
|
1304
|
+
] })
|
|
1305
|
+
]
|
|
1306
|
+
},
|
|
1307
|
+
row.url
|
|
1308
|
+
);
|
|
1309
|
+
};
|
|
1244
1310
|
return /* @__PURE__ */ jsxs5("box", { flexGrow: 1, flexDirection: "column", children: [
|
|
1245
1311
|
header,
|
|
1246
1312
|
/* @__PURE__ */ jsxs5(
|
|
@@ -1253,49 +1319,19 @@ function QueuePanel({
|
|
|
1253
1319
|
verticalScrollbarOptions: overlayScrollbar,
|
|
1254
1320
|
children: [
|
|
1255
1321
|
warning !== null && /* @__PURE__ */ jsx6("text", { wrapMode: "word", fg: theme.warn, marginTop: 1, children: warning }),
|
|
1256
|
-
|
|
1257
|
-
/* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.accent, children:
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
|
|
1270
|
-
},
|
|
1271
|
-
onMouseUp: onRefClick === null ? void 0 : (event) => {
|
|
1272
|
-
const down = lastDown.current;
|
|
1273
|
-
lastDown.current = null;
|
|
1274
|
-
if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
|
|
1278
|
-
if (local >= refStart && local < refStart + row.ref.length) {
|
|
1279
|
-
onRefClick(row);
|
|
1280
|
-
}
|
|
1281
|
-
},
|
|
1282
|
-
children: [
|
|
1283
|
-
/* @__PURE__ */ jsx6("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
|
|
1284
|
-
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1285
|
-
row.lead,
|
|
1286
|
-
" "
|
|
1287
|
-
] }),
|
|
1288
|
-
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 }),
|
|
1289
|
-
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1290
|
-
" ",
|
|
1291
|
-
row.title
|
|
1292
|
-
] })
|
|
1293
|
-
]
|
|
1294
|
-
},
|
|
1295
|
-
row.url
|
|
1296
|
-
);
|
|
1297
|
-
})
|
|
1298
|
-
] }, list.title))
|
|
1322
|
+
sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: 1, children: [
|
|
1323
|
+
/* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.accent, children: section.title }),
|
|
1324
|
+
section.rows.map((row, rowIndex) => renderRow(row, offsets[sectionIndex].rows + rowIndex, "")),
|
|
1325
|
+
section.lists.map((list, listIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: listIndex > 0 ? 1 : 0, children: [
|
|
1326
|
+
/* @__PURE__ */ jsxs5("text", { wrapMode: "none", fg: theme.accent, children: [
|
|
1327
|
+
" ",
|
|
1328
|
+
list.title
|
|
1329
|
+
] }),
|
|
1330
|
+
list.rows.map(
|
|
1331
|
+
(row, rowIndex) => renderRow(row, offsets[sectionIndex].lists[listIndex] + rowIndex, " ")
|
|
1332
|
+
)
|
|
1333
|
+
] }, list.title))
|
|
1334
|
+
] }, section.title))
|
|
1299
1335
|
]
|
|
1300
1336
|
}
|
|
1301
1337
|
)
|
|
@@ -1482,7 +1518,7 @@ function QueueTab({
|
|
|
1482
1518
|
heading: repos.length > 0 ? scope.repo ?? (grouped ? "All repos \xB7 grouped by repo" : "All repos") : null,
|
|
1483
1519
|
warning,
|
|
1484
1520
|
empty: view.empty,
|
|
1485
|
-
|
|
1521
|
+
sections: view.sections,
|
|
1486
1522
|
cursor: Math.min(rowCursor, queueRows(view).length - 1),
|
|
1487
1523
|
onRefClick
|
|
1488
1524
|
}
|
|
@@ -2993,6 +3029,62 @@ function ColorRow({
|
|
|
2993
3029
|
] }) });
|
|
2994
3030
|
}
|
|
2995
3031
|
|
|
3032
|
+
// src/tui/components/Modals.tsx
|
|
3033
|
+
import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
|
|
3034
|
+
function Modals({
|
|
3035
|
+
ui,
|
|
3036
|
+
options,
|
|
3037
|
+
saved: saved2,
|
|
3038
|
+
noCache: noCache2,
|
|
3039
|
+
copyLinks: copyLinks2,
|
|
3040
|
+
themeState,
|
|
3041
|
+
onDraft,
|
|
3042
|
+
onSubmitField,
|
|
3043
|
+
onSubmitThemeColor
|
|
3044
|
+
}) {
|
|
3045
|
+
if (ui.modal === "options") {
|
|
3046
|
+
return /* @__PURE__ */ jsx14(
|
|
3047
|
+
OptionsModal,
|
|
3048
|
+
{
|
|
3049
|
+
options,
|
|
3050
|
+
saved: saved2,
|
|
3051
|
+
selected: ui.selectedField,
|
|
3052
|
+
editing: ui.editing,
|
|
3053
|
+
fieldError: ui.fieldError,
|
|
3054
|
+
onDraft,
|
|
3055
|
+
onSubmit: onSubmitField
|
|
3056
|
+
}
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
if (ui.modal === "settings") {
|
|
3060
|
+
return /* @__PURE__ */ jsx14(
|
|
3061
|
+
SettingsModal,
|
|
3062
|
+
{
|
|
3063
|
+
selected: ui.selectedSetting,
|
|
3064
|
+
cacheAction: ui.cacheAction,
|
|
3065
|
+
noCache: noCache2,
|
|
3066
|
+
copyLinks: copyLinks2,
|
|
3067
|
+
preset: themeState.preset
|
|
3068
|
+
}
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
if (ui.modal === "theme") {
|
|
3072
|
+
return /* @__PURE__ */ jsx14(
|
|
3073
|
+
ThemeModal,
|
|
3074
|
+
{
|
|
3075
|
+
selected: ui.selectedThemeColor,
|
|
3076
|
+
editing: ui.editing,
|
|
3077
|
+
error: ui.themeColorError,
|
|
3078
|
+
cacheAction: ui.cacheAction,
|
|
3079
|
+
overrides: themeState.preset === "custom" ? themeState.overrides : {},
|
|
3080
|
+
onDraft,
|
|
3081
|
+
onSubmit: onSubmitThemeColor
|
|
3082
|
+
}
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
return null;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
2996
3088
|
// src/tui/hooks/useDeferredLoading.ts
|
|
2997
3089
|
import { useEffect as useEffect4, useRef as useRef3, useState as useState2 } from "react";
|
|
2998
3090
|
function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } = {}) {
|
|
@@ -3379,7 +3471,10 @@ function classifyPr(pr, details, user) {
|
|
|
3379
3471
|
(node) => node?.author?.login === user && node.submittedAt ? [new Date(node.submittedAt)] : []
|
|
3380
3472
|
);
|
|
3381
3473
|
if (requests.length === 0) {
|
|
3382
|
-
|
|
3474
|
+
if (reviews.length === 0) {
|
|
3475
|
+
return [{ kind: "inaccessible", pr }];
|
|
3476
|
+
}
|
|
3477
|
+
return [{ kind: "unrequested", pr, reviewedAt: new Date(Math.max(...reviews.map((review) => review.getTime()))) }];
|
|
3383
3478
|
}
|
|
3384
3479
|
const events = [
|
|
3385
3480
|
...requests.map((at) => {
|
|
@@ -3492,6 +3587,9 @@ function reviveRawData(data) {
|
|
|
3492
3587
|
if (result.kind === "reviewed") {
|
|
3493
3588
|
return { ...result, pr, requestedAt: new Date(result.requestedAt), reviewedAt: new Date(result.reviewedAt) };
|
|
3494
3589
|
}
|
|
3590
|
+
if (result.kind === "unrequested") {
|
|
3591
|
+
return { ...result, pr, reviewedAt: new Date(result.reviewedAt) };
|
|
3592
|
+
}
|
|
3495
3593
|
return { ...result, pr };
|
|
3496
3594
|
}),
|
|
3497
3595
|
sizes: data.sizes.map((entry) => {
|
|
@@ -3518,6 +3616,9 @@ function loadSnapshot(options) {
|
|
|
3518
3616
|
return null;
|
|
3519
3617
|
}
|
|
3520
3618
|
const data = reviveRawData(stored.data);
|
|
3619
|
+
if (data.reviewResults.some((result) => result.kind === "unrequested" && Number.isNaN(result.reviewedAt.getTime()))) {
|
|
3620
|
+
return null;
|
|
3621
|
+
}
|
|
3521
3622
|
if (sinceIso === data.sinceIso) {
|
|
3522
3623
|
return data;
|
|
3523
3624
|
}
|
|
@@ -3726,24 +3827,42 @@ function buildSizeRepoOptions(raw) {
|
|
|
3726
3827
|
})
|
|
3727
3828
|
];
|
|
3728
3829
|
}
|
|
3729
|
-
function pendingDetail(
|
|
3730
|
-
|
|
3830
|
+
function pendingDetail(counts) {
|
|
3831
|
+
const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
|
|
3832
|
+
return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewing` : "");
|
|
3731
3833
|
}
|
|
3732
3834
|
function buildPendingRepoOptions(raw) {
|
|
3733
|
-
const
|
|
3835
|
+
const countsByRepo = /* @__PURE__ */ new Map();
|
|
3836
|
+
const countsOf = (repo) => {
|
|
3837
|
+
const counts = countsByRepo.get(repo) ?? { awaiting: 0, reviewing: 0 };
|
|
3838
|
+
countsByRepo.set(repo, counts);
|
|
3839
|
+
return counts;
|
|
3840
|
+
};
|
|
3734
3841
|
for (const result of raw.reviewResults) {
|
|
3735
|
-
|
|
3736
|
-
countByRepo.set(result.pr.repo, (countByRepo.get(result.pr.repo) ?? 0) + pending);
|
|
3842
|
+
countsOf(result.pr.repo);
|
|
3737
3843
|
}
|
|
3738
|
-
if (
|
|
3844
|
+
if (countsByRepo.size < 2) {
|
|
3739
3845
|
return [];
|
|
3740
3846
|
}
|
|
3741
|
-
const
|
|
3742
|
-
const
|
|
3847
|
+
const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
|
|
3848
|
+
for (const entry of stats.pending) {
|
|
3849
|
+
countsOf(entry.pr.repo).awaiting += 1;
|
|
3850
|
+
}
|
|
3851
|
+
for (const entry of stats.reviewing) {
|
|
3852
|
+
countsOf(entry.pr.repo).reviewing += 1;
|
|
3853
|
+
}
|
|
3854
|
+
const entries = [...countsByRepo.entries()].toSorted(
|
|
3855
|
+
(a, b) => b[1].awaiting - a[1].awaiting || b[1].reviewing - a[1].reviewing || a[0].localeCompare(b[0])
|
|
3856
|
+
);
|
|
3857
|
+
const totals = { awaiting: 0, reviewing: 0 };
|
|
3858
|
+
for (const [, counts] of entries) {
|
|
3859
|
+
totals.awaiting += counts.awaiting;
|
|
3860
|
+
totals.reviewing += counts.reviewing;
|
|
3861
|
+
}
|
|
3743
3862
|
return [
|
|
3744
|
-
{ repo: null, label: "All repos", detail: pendingDetail(
|
|
3745
|
-
...entries.map(([repo,
|
|
3746
|
-
return { repo, label: repo, detail: pendingDetail(
|
|
3863
|
+
{ repo: null, label: "All repos", detail: pendingDetail(totals) },
|
|
3864
|
+
...entries.map(([repo, counts]) => {
|
|
3865
|
+
return { repo, label: repo, detail: pendingDetail(counts) };
|
|
3747
3866
|
})
|
|
3748
3867
|
];
|
|
3749
3868
|
}
|
|
@@ -5351,7 +5470,7 @@ function createClipboardCopier(renderer2) {
|
|
|
5351
5470
|
}
|
|
5352
5471
|
|
|
5353
5472
|
// src/tui/App.tsx
|
|
5354
|
-
import { jsx as
|
|
5473
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
|
|
5355
5474
|
function App({
|
|
5356
5475
|
initial: initial2,
|
|
5357
5476
|
initialSaved = null,
|
|
@@ -5473,9 +5592,9 @@ function App({
|
|
|
5473
5592
|
});
|
|
5474
5593
|
const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
|
|
5475
5594
|
return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
|
|
5476
|
-
/* @__PURE__ */
|
|
5477
|
-
/* @__PURE__ */
|
|
5478
|
-
/* @__PURE__ */
|
|
5595
|
+
/* @__PURE__ */ jsx15(Header, { options, raw, error, spinning: showLoad }),
|
|
5596
|
+
/* @__PURE__ */ jsx15(TabBar, { tab: browse.tab }),
|
|
5597
|
+
/* @__PURE__ */ jsx15(
|
|
5479
5598
|
MainPanel,
|
|
5480
5599
|
{
|
|
5481
5600
|
views,
|
|
@@ -5494,7 +5613,7 @@ function App({
|
|
|
5494
5613
|
onRefClick: copyLinks2 ? copyRow : null
|
|
5495
5614
|
}
|
|
5496
5615
|
),
|
|
5497
|
-
/* @__PURE__ */
|
|
5616
|
+
/* @__PURE__ */ jsx15(
|
|
5498
5617
|
Footer,
|
|
5499
5618
|
{
|
|
5500
5619
|
width,
|
|
@@ -5509,42 +5628,20 @@ function App({
|
|
|
5509
5628
|
stale
|
|
5510
5629
|
}
|
|
5511
5630
|
),
|
|
5512
|
-
|
|
5513
|
-
|
|
5631
|
+
/* @__PURE__ */ jsx15(
|
|
5632
|
+
Modals,
|
|
5514
5633
|
{
|
|
5634
|
+
ui,
|
|
5515
5635
|
options,
|
|
5516
5636
|
saved: saved2,
|
|
5517
|
-
selected: ui.selectedField,
|
|
5518
|
-
editing: ui.editing,
|
|
5519
|
-
fieldError: ui.fieldError,
|
|
5520
|
-
onDraft: (value2) => {
|
|
5521
|
-
draftRef.current = value2;
|
|
5522
|
-
},
|
|
5523
|
-
onSubmit: commitField
|
|
5524
|
-
}
|
|
5525
|
-
),
|
|
5526
|
-
ui.modal === "settings" && /* @__PURE__ */ jsx14(
|
|
5527
|
-
SettingsModal,
|
|
5528
|
-
{
|
|
5529
|
-
selected: ui.selectedSetting,
|
|
5530
|
-
cacheAction: ui.cacheAction,
|
|
5531
5637
|
noCache: noCache2,
|
|
5532
5638
|
copyLinks: copyLinks2,
|
|
5533
|
-
|
|
5534
|
-
}
|
|
5535
|
-
),
|
|
5536
|
-
ui.modal === "theme" && /* @__PURE__ */ jsx14(
|
|
5537
|
-
ThemeModal,
|
|
5538
|
-
{
|
|
5539
|
-
selected: ui.selectedThemeColor,
|
|
5540
|
-
editing: ui.editing,
|
|
5541
|
-
error: ui.themeColorError,
|
|
5542
|
-
cacheAction: ui.cacheAction,
|
|
5543
|
-
overrides: themeState.preset === "custom" ? themeState.overrides : {},
|
|
5639
|
+
themeState,
|
|
5544
5640
|
onDraft: (value2) => {
|
|
5545
5641
|
draftRef.current = value2;
|
|
5546
5642
|
},
|
|
5547
|
-
|
|
5643
|
+
onSubmitField: commitField,
|
|
5644
|
+
onSubmitThemeColor: commitThemeColor
|
|
5548
5645
|
}
|
|
5549
5646
|
)
|
|
5550
5647
|
] });
|
|
@@ -5590,7 +5687,7 @@ function bootstrap() {
|
|
|
5590
5687
|
}
|
|
5591
5688
|
|
|
5592
5689
|
// src/tui/main.tsx
|
|
5593
|
-
import { jsx as
|
|
5690
|
+
import { jsx as jsx16 } from "@opentui/react/jsx-runtime";
|
|
5594
5691
|
var { initial, saved, noCache, copyLinks, theme: theme2 } = bootstrap();
|
|
5595
5692
|
var exitSignals = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT", "SIGHUP", "SIGBREAK", "SIGBUS"];
|
|
5596
5693
|
var renderer = await createCliRenderer({ exitOnCtrlC: true, exitSignals });
|
|
@@ -5601,7 +5698,7 @@ for (const signal of ["SIGTERM", "SIGHUP"]) {
|
|
|
5601
5698
|
});
|
|
5602
5699
|
}
|
|
5603
5700
|
createRoot(renderer).render(
|
|
5604
|
-
/* @__PURE__ */
|
|
5701
|
+
/* @__PURE__ */ jsx16(
|
|
5605
5702
|
App,
|
|
5606
5703
|
{
|
|
5607
5704
|
initial,
|