@d3lm/pr-stats 0.2.7 → 0.2.8
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 +132 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,12 +28,12 @@ pr-stats
|
|
|
28
28
|
Without flags, it looks at PRs from the last 90 days across all repositories you can access. The tabs hold these views.
|
|
29
29
|
|
|
30
30
|
- The queue tab, which it opens on, shows two lists. The open PRs awaiting your review come first with how long each has been waiting, and below them sit 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.
|
|
31
|
-
- The Your PRs tab 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 reports how your authored PRs got created, merged, and closed, telling a merge apart from a close without one. It charts time-to-merge percentiles, a histogram and trend, a merge-time heatmap, and a scatter of merge time against PR size. It also plots a merge-rate trend over the concluded PRs, cumulative created and merged lines whose gap shows the backlog, weekly created and merged volumes, an outcome gauge, and the most recently merged and closed PRs.
|
|
31
|
+
- The Your PRs tab 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 reports how your authored PRs got created, merged, and closed, telling a merge apart from a close without one. It charts time-to-merge percentiles, a histogram and trend, a merge-time heatmap, and a scatter of merge time against PR size. It also plots a merge-rate trend over the concluded PRs, cumulative created and merged lines whose gap shows the backlog, weekly created and merged volumes, an outcome gauge, and the most recently merged and closed PRs. A reviewer leaderboard ranks who reviews your PRs by distinct PRs reviewed, and a review-coverage gauge counts the merged PRs that never received a review. Your own replies to review threads never count as a review for either of them.
|
|
32
32
|
- The time-to-review report pairs its histogram, trend, heatmap, and weekly volume with the completed review cycles per PR and a verdict gauge splitting approvals from change requests. It also shows the age of the requests still waiting on you, how old PRs already were when the request reached you, and an off-hours gauge that splits weekdays into work hours and after hours once `--work-hours` is set. On the aggregate view it additionally compares median review times by repo.
|
|
33
33
|
- The PR size report carries the same histogram, trend, heatmap, and weekly volume for PR sizes and adds a net-lines trend that sums additions minus deletions per week.
|
|
34
34
|
- The comments report holds a histogram of comments per PR, a scatter of comments against PR size, and the most commented PRs.
|
|
35
35
|
|
|
36
|
-
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.
|
|
36
|
+
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. Comparison cards like the reviewer leaderboard cap themselves at eight rows and fold the rest into an overflow line, and the `x` key lifts the cap on the open stats tab and restores it. The footer names the key whenever the tab has a capped card.
|
|
37
37
|
|
|
38
38
|
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.
|
|
39
39
|
|
package/dist/tui-app.mjs
CHANGED
|
@@ -146,7 +146,7 @@ import { join as join2 } from "node:path";
|
|
|
146
146
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
147
147
|
import { homedir } from "node:os";
|
|
148
148
|
import { dirname, join } from "node:path";
|
|
149
|
-
var VERSION =
|
|
149
|
+
var VERSION = 3;
|
|
150
150
|
var enabled = false;
|
|
151
151
|
function configureCache(on) {
|
|
152
152
|
enabled = on;
|
|
@@ -621,10 +621,12 @@ function hintsFor(modal, editing, tab, authoredTab, views, copyLinks2) {
|
|
|
621
621
|
if (scope?.view === "list") {
|
|
622
622
|
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`;
|
|
623
623
|
}
|
|
624
|
+
const view = views === null ? null : tab === 1 ? views.merged : tab === 2 ? views.review : tab === 3 ? views.size : views.comments;
|
|
625
|
+
const expand = view?.expandable ? view.expanded ? "x collapse \xB7 " : "x expand \xB7 " : "";
|
|
624
626
|
if (scope !== null && repos.length > 0) {
|
|
625
|
-
return `${toggle}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`;
|
|
627
|
+
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`;
|
|
626
628
|
}
|
|
627
|
-
return `${toggle}1-5 tabs \xB7 j/k scroll \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
629
|
+
return `${toggle}${expand}1-5 tabs \xB7 j/k scroll \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
628
630
|
}
|
|
629
631
|
|
|
630
632
|
// src/tui/components/Spinner.tsx
|
|
@@ -883,6 +885,37 @@ function computeMergeStats(sizes) {
|
|
|
883
885
|
closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
|
|
884
886
|
return { merged, closed, open, allHours: merged.map((result) => result.hours) };
|
|
885
887
|
}
|
|
888
|
+
function computeReviewerStats(sizes, author) {
|
|
889
|
+
const byLogin = /* @__PURE__ */ new Map();
|
|
890
|
+
let mergedReviewed = 0;
|
|
891
|
+
let mergedUnreviewed = 0;
|
|
892
|
+
for (const entry of sizes) {
|
|
893
|
+
const others = entry.reviewers.filter((login) => login !== author);
|
|
894
|
+
for (const login of others) {
|
|
895
|
+
const counts = byLogin.get(login) ?? { prs: 0, reviews: 0 };
|
|
896
|
+
counts.reviews += 1;
|
|
897
|
+
byLogin.set(login, counts);
|
|
898
|
+
}
|
|
899
|
+
const distinct = new Set(others);
|
|
900
|
+
for (const login of distinct) {
|
|
901
|
+
const counts = byLogin.get(login);
|
|
902
|
+
if (counts !== void 0) {
|
|
903
|
+
counts.prs += 1;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (entry.mergedAt !== null) {
|
|
907
|
+
if (others.length > 0) {
|
|
908
|
+
mergedReviewed += 1;
|
|
909
|
+
} else {
|
|
910
|
+
mergedUnreviewed += 1;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const leaderboard = [...byLogin.entries()].map(([login, counts]) => {
|
|
915
|
+
return { login, ...counts };
|
|
916
|
+
}).toSorted((a, b) => b.prs - a.prs || b.reviews - a.reviews || a.login.localeCompare(b.login));
|
|
917
|
+
return { leaderboard, mergedReviewed, mergedUnreviewed };
|
|
918
|
+
}
|
|
886
919
|
function computeCommentStats(sizes) {
|
|
887
920
|
const metrics = [
|
|
888
921
|
{ label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
|
|
@@ -1086,9 +1119,24 @@ function useScrollbarSettle(scrollRef, mounted = true) {
|
|
|
1086
1119
|
renderer2.off(CliRenderEvents.FRAME, release);
|
|
1087
1120
|
scrollRef.current?.verticalScrollBar.resetVisibilityControl();
|
|
1088
1121
|
};
|
|
1122
|
+
const resyncThumb = () => {
|
|
1123
|
+
const bar = scrollRef.current?.verticalScrollBar;
|
|
1124
|
+
if (!bar) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const height = bar.viewportSize;
|
|
1128
|
+
const wanted = Math.max(1, height);
|
|
1129
|
+
if (bar.scrollSize < wanted || bar.slider.viewPortSize === wanted) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
bar.viewportSize = 0;
|
|
1133
|
+
bar.viewportSize = height;
|
|
1134
|
+
};
|
|
1089
1135
|
renderer2.on(CliRenderEvents.FRAME, release);
|
|
1136
|
+
renderer2.on(CliRenderEvents.FRAME, resyncThumb);
|
|
1090
1137
|
return () => {
|
|
1091
1138
|
renderer2.off(CliRenderEvents.FRAME, release);
|
|
1139
|
+
renderer2.off(CliRenderEvents.FRAME, resyncThumb);
|
|
1092
1140
|
};
|
|
1093
1141
|
}, [scrollRef, renderer2, mounted]);
|
|
1094
1142
|
}
|
|
@@ -1428,7 +1476,8 @@ var initialBrowseState = {
|
|
|
1428
1476
|
},
|
|
1429
1477
|
repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0, merged: 0 },
|
|
1430
1478
|
rowCursors: { pending: 0, open: 0 },
|
|
1431
|
-
grouped: { pending: false, open: false }
|
|
1479
|
+
grouped: { pending: false, open: false },
|
|
1480
|
+
expanded: { review: false, size: false, comment: false, merged: false }
|
|
1432
1481
|
};
|
|
1433
1482
|
function dropVanishedRepo(scope, repos) {
|
|
1434
1483
|
if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
|
|
@@ -1477,6 +1526,9 @@ function browseReducer(state, action) {
|
|
|
1477
1526
|
case "groupingToggled": {
|
|
1478
1527
|
return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
|
|
1479
1528
|
}
|
|
1529
|
+
case "expandToggled": {
|
|
1530
|
+
return { ...state, expanded: { ...state.expanded, [action.tab]: !state.expanded[action.tab] } };
|
|
1531
|
+
}
|
|
1480
1532
|
case "dataLoaded": {
|
|
1481
1533
|
return {
|
|
1482
1534
|
...state,
|
|
@@ -3402,6 +3454,7 @@ async function fetchPrSizes(prs) {
|
|
|
3402
3454
|
}
|
|
3403
3455
|
reviews(first: 100) {
|
|
3404
3456
|
nodes {
|
|
3457
|
+
author { login }
|
|
3405
3458
|
comments {
|
|
3406
3459
|
totalCount
|
|
3407
3460
|
}
|
|
@@ -3596,6 +3649,7 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
|
|
|
3596
3649
|
if (details) {
|
|
3597
3650
|
const discussion = details.comments.totalCount;
|
|
3598
3651
|
const review = details.reviews.nodes.reduce((sum, node) => sum + (node?.comments.totalCount ?? 0), 0);
|
|
3652
|
+
const reviewers = details.reviews.nodes.flatMap((node) => node?.author == null ? [] : [node.author.login]);
|
|
3599
3653
|
sizes.push({
|
|
3600
3654
|
pr,
|
|
3601
3655
|
files: details.changedFiles,
|
|
@@ -3604,7 +3658,8 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
|
|
|
3604
3658
|
total: details.additions + details.deletions,
|
|
3605
3659
|
mergedAt: details.mergedAt === null ? null : new Date(details.mergedAt),
|
|
3606
3660
|
closedAt: details.closedAt === null ? null : new Date(details.closedAt),
|
|
3607
|
-
comments: { discussion, review, total: discussion + review }
|
|
3661
|
+
comments: { discussion, review, total: discussion + review },
|
|
3662
|
+
reviewers
|
|
3608
3663
|
});
|
|
3609
3664
|
}
|
|
3610
3665
|
}
|
|
@@ -4048,8 +4103,8 @@ function hbar(fraction, width, color) {
|
|
|
4048
4103
|
// src/tui/views/charts/bars.ts
|
|
4049
4104
|
var BAR_WIDTH2 = 24;
|
|
4050
4105
|
var MAX_BARS = 8;
|
|
4051
|
-
function buildBarsCard({ title, subtitle, rows, format }) {
|
|
4052
|
-
const shown = rows.slice(0, MAX_BARS);
|
|
4106
|
+
function buildBarsCard({ title, subtitle, rows, format, expanded = false }) {
|
|
4107
|
+
const shown = expanded ? rows : rows.slice(0, MAX_BARS);
|
|
4053
4108
|
const max = Math.max(...shown.map((row) => row.value), 0);
|
|
4054
4109
|
const labelWidth = Math.max(...shown.map((row) => row.label.length));
|
|
4055
4110
|
const valueWidth = Math.max(...shown.map((row) => format(row.value).length));
|
|
@@ -4067,7 +4122,7 @@ function buildBarsCard({ title, subtitle, rows, format }) {
|
|
|
4067
4122
|
return line;
|
|
4068
4123
|
});
|
|
4069
4124
|
if (rows.length > shown.length) {
|
|
4070
|
-
lines.push([{ text: `+ ${rows.length - shown.length} more`, fg: theme.dim }]);
|
|
4125
|
+
lines.push([{ text: `+ ${rows.length - shown.length} more \xB7 x expands`, fg: theme.dim }]);
|
|
4071
4126
|
}
|
|
4072
4127
|
return { title, subtitle, lines };
|
|
4073
4128
|
}
|
|
@@ -4736,7 +4791,7 @@ function buildVerdictCard(reviewed) {
|
|
|
4736
4791
|
})
|
|
4737
4792
|
});
|
|
4738
4793
|
}
|
|
4739
|
-
function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100) {
|
|
4794
|
+
function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100, expanded = false) {
|
|
4740
4795
|
const results = repo === null ? raw.reviewResults : raw.reviewResults.filter((result) => result.pr.repo === repo);
|
|
4741
4796
|
const stats = computeReviewStats(results, { targetHours, now: raw.fetchedAt });
|
|
4742
4797
|
const strip = [
|
|
@@ -4751,7 +4806,9 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4751
4806
|
distributionTitle: "Review time distribution",
|
|
4752
4807
|
noCharts: "No completed reviews to chart.",
|
|
4753
4808
|
cards: [],
|
|
4754
|
-
distribution: null
|
|
4809
|
+
distribution: null,
|
|
4810
|
+
expandable: false,
|
|
4811
|
+
expanded
|
|
4755
4812
|
};
|
|
4756
4813
|
if (results.length === 0) {
|
|
4757
4814
|
return { empty: "No reviewed or review-requested PRs found.", ...base, lists: [] };
|
|
@@ -4814,7 +4871,8 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4814
4871
|
detail: `n=${hours.length}`
|
|
4815
4872
|
};
|
|
4816
4873
|
}).toSorted((a, b) => b.value - a.value),
|
|
4817
|
-
format: formatDuration
|
|
4874
|
+
format: formatDuration,
|
|
4875
|
+
expanded
|
|
4818
4876
|
}) : null;
|
|
4819
4877
|
const cards = [
|
|
4820
4878
|
buildHistogramCard({
|
|
@@ -4870,7 +4928,15 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4870
4928
|
ticks: DURATION_TICKS,
|
|
4871
4929
|
flat: (count2, value2) => `all ${count2} ${count2 === 1 ? "review" : "reviews"} took ${value2}`
|
|
4872
4930
|
});
|
|
4873
|
-
return {
|
|
4931
|
+
return {
|
|
4932
|
+
empty: null,
|
|
4933
|
+
...base,
|
|
4934
|
+
headline,
|
|
4935
|
+
cards,
|
|
4936
|
+
distribution,
|
|
4937
|
+
lists,
|
|
4938
|
+
expandable: byRepoCard !== null && stats.byRepo.length > MAX_BARS
|
|
4939
|
+
};
|
|
4874
4940
|
}
|
|
4875
4941
|
function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
4876
4942
|
const base = {
|
|
@@ -4880,7 +4946,9 @@ function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
|
4880
4946
|
noCharts: "No authored PRs to chart.",
|
|
4881
4947
|
cards: [],
|
|
4882
4948
|
distribution: null,
|
|
4883
|
-
lists: []
|
|
4949
|
+
lists: [],
|
|
4950
|
+
expandable: false,
|
|
4951
|
+
expanded: false
|
|
4884
4952
|
};
|
|
4885
4953
|
if (raw.authoredTotal === 0) {
|
|
4886
4954
|
return { empty: "No authored PRs found.", ...base };
|
|
@@ -4989,7 +5057,9 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
4989
5057
|
noCharts: "No authored PRs to chart.",
|
|
4990
5058
|
cards: [],
|
|
4991
5059
|
distribution: null,
|
|
4992
|
-
lists: []
|
|
5060
|
+
lists: [],
|
|
5061
|
+
expandable: false,
|
|
5062
|
+
expanded: false
|
|
4993
5063
|
};
|
|
4994
5064
|
if (raw.authoredTotal === 0) {
|
|
4995
5065
|
return { empty: "No authored PRs found.", ...base };
|
|
@@ -5075,7 +5145,7 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
5075
5145
|
}
|
|
5076
5146
|
return { empty: null, ...base, strip, headline, cards, distribution, lists };
|
|
5077
5147
|
}
|
|
5078
|
-
function buildMergedView(raw, repo = null, width = 100) {
|
|
5148
|
+
function buildMergedView(raw, repo = null, width = 100, expanded = false) {
|
|
5079
5149
|
const base = {
|
|
5080
5150
|
strip: [],
|
|
5081
5151
|
headline: null,
|
|
@@ -5083,7 +5153,9 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
5083
5153
|
noCharts: "No merged PRs to chart.",
|
|
5084
5154
|
cards: [],
|
|
5085
5155
|
distribution: null,
|
|
5086
|
-
lists: []
|
|
5156
|
+
lists: [],
|
|
5157
|
+
expandable: false,
|
|
5158
|
+
expanded
|
|
5087
5159
|
};
|
|
5088
5160
|
if (raw.authoredTotal === 0) {
|
|
5089
5161
|
return { empty: "No authored PRs found.", ...base };
|
|
@@ -5093,6 +5165,7 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
5093
5165
|
return { empty: "No accessible authored PRs to analyze.", ...base };
|
|
5094
5166
|
}
|
|
5095
5167
|
const stats = computeMergeStats(sizes);
|
|
5168
|
+
const reviewers = computeReviewerStats(sizes, raw.user);
|
|
5096
5169
|
const strip = [
|
|
5097
5170
|
countCell(sizes.length, "PRs created"),
|
|
5098
5171
|
countCell(stats.merged.length, "merged"),
|
|
@@ -5123,8 +5196,22 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
5123
5196
|
)
|
|
5124
5197
|
});
|
|
5125
5198
|
}
|
|
5199
|
+
const reviewerCard = reviewers.leaderboard.length === 0 ? null : buildBarsCard({
|
|
5200
|
+
title: "Who reviews your PRs",
|
|
5201
|
+
subtitle: "distinct PRs reviewed per person",
|
|
5202
|
+
rows: reviewers.leaderboard.map((row) => {
|
|
5203
|
+
return {
|
|
5204
|
+
label: row.login,
|
|
5205
|
+
value: row.prs,
|
|
5206
|
+
detail: row.reviews === 1 ? "1 review" : `${row.reviews} reviews`
|
|
5207
|
+
};
|
|
5208
|
+
}),
|
|
5209
|
+
format: count,
|
|
5210
|
+
expanded
|
|
5211
|
+
});
|
|
5212
|
+
const expandable = reviewers.leaderboard.length > MAX_BARS;
|
|
5126
5213
|
if (stats.merged.length === 0) {
|
|
5127
|
-
return { empty: null, ...base, strip, lists };
|
|
5214
|
+
return { empty: null, ...base, strip, cards: reviewerCard === null ? [] : [reviewerCard], lists, expandable };
|
|
5128
5215
|
}
|
|
5129
5216
|
const sorted = [...stats.allHours].toSorted((a, b) => a - b);
|
|
5130
5217
|
const headline = [
|
|
@@ -5202,7 +5289,16 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
5202
5289
|
{ label: "closed unmerged", count: stats.closed.length, color: theme.warn },
|
|
5203
5290
|
...stats.open.length > 0 ? [{ label: "still open", count: stats.open.length, color: theme.chartDim }] : []
|
|
5204
5291
|
]
|
|
5205
|
-
})
|
|
5292
|
+
}),
|
|
5293
|
+
buildGaugeCard({
|
|
5294
|
+
title: "Review coverage",
|
|
5295
|
+
subtitle: "merged PRs that received a review",
|
|
5296
|
+
rows: [
|
|
5297
|
+
{ label: "reviewed", count: reviewers.mergedReviewed, color: theme.accent },
|
|
5298
|
+
{ label: "merged unreviewed", count: reviewers.mergedUnreviewed, color: theme.warn }
|
|
5299
|
+
]
|
|
5300
|
+
}),
|
|
5301
|
+
...reviewerCard === null ? [] : [reviewerCard]
|
|
5206
5302
|
];
|
|
5207
5303
|
const distribution = buildDistribution({
|
|
5208
5304
|
values: stats.allHours,
|
|
@@ -5211,7 +5307,7 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
5211
5307
|
ticks: DURATION_TICKS,
|
|
5212
5308
|
flat: (n, value2) => `all ${n} merged ${n === 1 ? "PR" : "PRs"} took ${value2}`
|
|
5213
5309
|
});
|
|
5214
|
-
return { empty: null, ...base, strip, headline, cards, distribution, lists };
|
|
5310
|
+
return { empty: null, ...base, strip, headline, cards, distribution, lists, expandable };
|
|
5215
5311
|
}
|
|
5216
5312
|
function count(value2) {
|
|
5217
5313
|
return formatCount(Math.round(value2));
|
|
@@ -5227,7 +5323,7 @@ function resolveScope(scope, repos) {
|
|
|
5227
5323
|
}
|
|
5228
5324
|
return dropVanishedRepo(scope, repos);
|
|
5229
5325
|
}
|
|
5230
|
-
function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
5326
|
+
function useViewModel(raw, options, width, scopes, grouping, expanded, themeEpoch) {
|
|
5231
5327
|
return useMemo(() => {
|
|
5232
5328
|
void themeEpoch;
|
|
5233
5329
|
configureTimeMode({
|
|
@@ -5253,7 +5349,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
|
5253
5349
|
const reviewScope = resolveScope(scopes.review, reviewRepos);
|
|
5254
5350
|
const sizeScope = resolveScope(scopes.size, sizeRepos);
|
|
5255
5351
|
const commentScope = resolveScope(scopes.comment, commentRepos);
|
|
5256
|
-
const review = reviewScope.view === "detail" ? buildReviewView(raw, targetHours, targetLabelOf(options.target), reviewScope.repo, width) : null;
|
|
5352
|
+
const review = reviewScope.view === "detail" ? buildReviewView(raw, targetHours, targetLabelOf(options.target), reviewScope.repo, width, expanded.review) : null;
|
|
5257
5353
|
return {
|
|
5258
5354
|
pendingRepos,
|
|
5259
5355
|
openRepos,
|
|
@@ -5269,7 +5365,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
|
5269
5365
|
commentScope,
|
|
5270
5366
|
pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending) : null,
|
|
5271
5367
|
open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
|
|
5272
|
-
merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width) : null,
|
|
5368
|
+
merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width, expanded.merged) : null,
|
|
5273
5369
|
review,
|
|
5274
5370
|
size: sizeScope.view === "detail" ? buildSizeView(raw, sizeTarget, sizeScope.repo, width) : null,
|
|
5275
5371
|
comments: commentScope.view === "detail" ? buildCommentView(raw, commentScope.repo, width) : null
|
|
@@ -5290,6 +5386,8 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
|
5290
5386
|
scopes.comment,
|
|
5291
5387
|
grouping.pending,
|
|
5292
5388
|
grouping.open,
|
|
5389
|
+
expanded.review,
|
|
5390
|
+
expanded.merged,
|
|
5293
5391
|
themeEpoch
|
|
5294
5392
|
]);
|
|
5295
5393
|
}
|
|
@@ -5519,31 +5617,35 @@ function statsTabOf(context) {
|
|
|
5519
5617
|
return {
|
|
5520
5618
|
key: "merged",
|
|
5521
5619
|
repos: views?.mergedRepos ?? [],
|
|
5522
|
-
scope: views?.mergedScope ?? null
|
|
5620
|
+
scope: views?.mergedScope ?? null,
|
|
5621
|
+
view: views?.merged ?? null
|
|
5523
5622
|
};
|
|
5524
5623
|
}
|
|
5525
5624
|
if (context.browse.tab === 2) {
|
|
5526
5625
|
return {
|
|
5527
5626
|
key: "review",
|
|
5528
5627
|
repos: views?.reviewRepos ?? [],
|
|
5529
|
-
scope: views?.reviewScope ?? null
|
|
5628
|
+
scope: views?.reviewScope ?? null,
|
|
5629
|
+
view: views?.review ?? null
|
|
5530
5630
|
};
|
|
5531
5631
|
}
|
|
5532
5632
|
if (context.browse.tab === 3) {
|
|
5533
5633
|
return {
|
|
5534
5634
|
key: "size",
|
|
5535
5635
|
repos: views?.sizeRepos ?? [],
|
|
5536
|
-
scope: views?.sizeScope ?? null
|
|
5636
|
+
scope: views?.sizeScope ?? null,
|
|
5637
|
+
view: views?.size ?? null
|
|
5537
5638
|
};
|
|
5538
5639
|
}
|
|
5539
5640
|
return {
|
|
5540
5641
|
key: "comment",
|
|
5541
5642
|
repos: views?.commentRepos ?? [],
|
|
5542
|
-
scope: views?.commentScope ?? null
|
|
5643
|
+
scope: views?.commentScope ?? null,
|
|
5644
|
+
view: views?.comments ?? null
|
|
5543
5645
|
};
|
|
5544
5646
|
}
|
|
5545
5647
|
function handleStatsKey(key, context) {
|
|
5546
|
-
const { key: tab, repos, scope } = statsTabOf(context);
|
|
5648
|
+
const { key: tab, repos, scope, view } = statsTabOf(context);
|
|
5547
5649
|
if (scope !== null && scope.view === "list") {
|
|
5548
5650
|
switch (key.name) {
|
|
5549
5651
|
case "up":
|
|
@@ -5564,6 +5666,8 @@ function handleStatsKey(key, context) {
|
|
|
5564
5666
|
}
|
|
5565
5667
|
} else if ((key.name === "escape" || key.name === "backspace") && repos.length > 0) {
|
|
5566
5668
|
context.dispatchBrowse({ type: "pickerReturned", tab });
|
|
5669
|
+
} else if (key.name === "x" && view?.expandable === true) {
|
|
5670
|
+
context.dispatchBrowse({ type: "expandToggled", tab });
|
|
5567
5671
|
} else if (key.name === "j") {
|
|
5568
5672
|
context.scrollBy(context.browse.tab, 2);
|
|
5569
5673
|
} else if (key.name === "k") {
|
|
@@ -5829,7 +5933,7 @@ function App({
|
|
|
5829
5933
|
}
|
|
5830
5934
|
});
|
|
5831
5935
|
});
|
|
5832
|
-
const views = useViewModel(raw, options, width, browse.scopes, browse.grouped, themeState);
|
|
5936
|
+
const views = useViewModel(raw, options, width, browse.scopes, browse.grouped, browse.expanded, themeState);
|
|
5833
5937
|
const showLoad = useDeferredLoading(loading, isSnapshot ? { showDelay: 0 } : void 0);
|
|
5834
5938
|
const draftRef = useRef5("");
|
|
5835
5939
|
const commitField = () => {
|