@d3lm/pr-stats 0.2.1 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +7 -5
  2. package/dist/tui-app.mjs +478 -245
  3. package/package.json +28 -20
package/dist/tui-app.mjs CHANGED
@@ -28,9 +28,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  mod
29
29
  ));
30
30
 
31
- // ../../node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js
31
+ // node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js
32
32
  var require_asciichart = __commonJS({
33
- "../../node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js"(exports) {
33
+ "node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js"(exports) {
34
34
  "use strict";
35
35
  (function(exports2) {
36
36
  exports2.black = "\x1B[30m";
@@ -130,23 +130,23 @@ var require_asciichart = __commonJS({
130
130
  }
131
131
  });
132
132
 
133
- // tui/main.tsx
133
+ // src/tui/main.tsx
134
134
  import { createCliRenderer } from "@opentui/core";
135
135
  import { createRoot } from "@opentui/react";
136
136
 
137
- // tui/App.tsx
137
+ // src/tui/App.tsx
138
138
  import { useKeyboard, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
139
139
  import { useEffect as useEffect6, useReducer, useRef as useRef5, useState as useState4 } from "react";
140
140
 
141
- // settings.ts
141
+ // src/settings.ts
142
142
  import { readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
143
143
  import { join as join2 } from "node:path";
144
144
 
145
- // cache.ts
145
+ // src/cache.ts
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 = 1;
149
+ var VERSION = 2;
150
150
  var enabled = false;
151
151
  function configureCache(on) {
152
152
  enabled = on;
@@ -263,7 +263,7 @@ var PrCache = class {
263
263
  }
264
264
  };
265
265
 
266
- // utils.ts
266
+ // src/utils.ts
267
267
  var CliError = class extends Error {
268
268
  };
269
269
  function fail(message) {
@@ -278,7 +278,7 @@ function percentile(sorted, percent) {
278
278
  return sorted[Math.max(0, index)];
279
279
  }
280
280
 
281
- // settings.ts
281
+ // src/settings.ts
282
282
  function settingsFile() {
283
283
  return join2(cacheDir(), "settings.json");
284
284
  }
@@ -361,7 +361,7 @@ function applySettings(values, explicit) {
361
361
  return settings;
362
362
  }
363
363
 
364
- // tui/theme.ts
364
+ // src/tui/theme.ts
365
365
  var BASE = {
366
366
  bg: "#1e1e1e",
367
367
  border: "#363636",
@@ -547,13 +547,14 @@ function themeColorText(key) {
547
547
  return Array.isArray(value2) ? value2.join(" ") : value2;
548
548
  }
549
549
 
550
- // tui/components/Footer.tsx
550
+ // src/tui/components/Footer.tsx
551
551
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
552
552
  function Footer({
553
553
  width,
554
554
  modal,
555
555
  editing,
556
556
  tab,
557
+ authoredTab,
557
558
  views,
558
559
  copyLinks: copyLinks2,
559
560
  openError,
@@ -563,7 +564,7 @@ function Footer({
563
564
  const notice = openError ?? copyNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
564
565
  const check = openError === null && copyNotice !== null;
565
566
  const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
566
- const hints = truncated(hintsFor(modal, editing, tab, views, copyLinks2), width - 2 - noticeWidth);
567
+ const hints = truncated(hintsFor(modal, editing, tab, authoredTab, views, copyLinks2), width - 2 - noticeWidth);
567
568
  return /* @__PURE__ */ jsxs(Fragment, { children: [
568
569
  /* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
569
570
  /* @__PURE__ */ jsxs(
@@ -592,7 +593,7 @@ function truncated(text, limit2) {
592
593
  }
593
594
  return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
594
595
  }
595
- function hintsFor(modal, editing, tab, views, copyLinks2) {
596
+ function hintsFor(modal, editing, tab, authoredTab, views, copyLinks2) {
596
597
  if (modal === "options") {
597
598
  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";
598
599
  }
@@ -602,30 +603,31 @@ function hintsFor(modal, editing, tab, views, copyLinks2) {
602
603
  if (modal === "theme") {
603
604
  return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit hex \xB7 esc back \xB7 q quit";
604
605
  }
605
- if (tab <= 1) {
606
+ const toggle = tab === 1 ? authoredTab === "open" ? "t merged stats \xB7 " : "t open PRs \xB7 " : "";
607
+ if (tab === 0 || tab === 1 && authoredTab === "open") {
606
608
  const scope2 = views === null ? null : tab === 0 ? views.pendingScope : views.openScope;
607
609
  const repos2 = views === null ? [] : tab === 0 ? views.pendingRepos : views.openRepos;
608
610
  if (scope2?.view === "list") {
609
- return "\u2191/\u2193 select \xB7 enter open \xB7 \u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit";
611
+ 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`;
610
612
  }
611
613
  const action = copyLinks2 ? "enter copy link" : "enter open";
612
614
  if (scope2 !== null && repos2.length > 0) {
613
- return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 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 esc back \xB7 1-5 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
615
+ return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 ${toggle}g group by repo \xB7 esc back \xB7 o options \xB7 s settings \xB7 r reload \xB7 q quit` : `\u2191/\u2193 select \xB7 ${action} \xB7 ${toggle}esc back \xB7 1-5 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
614
616
  }
615
- return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 \u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
617
+ return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
616
618
  }
617
- const scope = views === null ? null : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
618
- const repos = views === null ? [] : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
619
+ const scope = views === null ? null : tab === 1 ? views.mergedScope : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
620
+ const repos = views === null ? [] : tab === 1 ? views.mergedRepos : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
619
621
  if (scope?.view === "list") {
620
- return "\u2191/\u2193 select \xB7 enter open \xB7 \u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit";
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`;
621
623
  }
622
624
  if (scope !== null && repos.length > 0) {
623
- return "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";
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`;
624
626
  }
625
- return "1-5 tabs \xB7 j/k scroll \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit";
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`;
626
628
  }
627
629
 
628
- // tui/components/Spinner.tsx
630
+ // src/tui/components/Spinner.tsx
629
631
  import { useEffect, useState } from "react";
630
632
  import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
631
633
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -643,7 +645,7 @@ function Spinner() {
643
645
  return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
644
646
  }
645
647
 
646
- // tui/components/Header.tsx
648
+ // src/tui/components/Header.tsx
647
649
  import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
648
650
  function Header({
649
651
  options,
@@ -677,7 +679,7 @@ function timeModeLabel(options) {
677
679
  return options.workHours === "0-24" ? `Mon-Fri all hours ${tz}` : `Mon-Fri ${options.workHours} ${tz}`;
678
680
  }
679
681
 
680
- // time.ts
682
+ // src/time.ts
681
683
  function wallFormatter(tz) {
682
684
  return new Intl.DateTimeFormat("en-US", {
683
685
  timeZone: tz,
@@ -768,7 +770,7 @@ function durationHours(start, end) {
768
770
  return businessMsBetween(start, end) / 36e5;
769
771
  }
770
772
 
771
- // compute.ts
773
+ // src/compute.ts
772
774
  function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
773
775
  const reviewed = [];
774
776
  const pending = [];
@@ -818,6 +820,23 @@ function computeSizeStats(sizes, { sizeTarget } = {}) {
818
820
  const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
819
821
  return { metrics, timelineTotals, met, misses, targetLabel };
820
822
  }
823
+ function computeMergeStats(sizes) {
824
+ const merged = [];
825
+ const closed = [];
826
+ const open = [];
827
+ for (const entry of sizes) {
828
+ if (entry.mergedAt !== null) {
829
+ merged.push({ entry, mergedAt: entry.mergedAt, hours: durationHours(entry.pr.createdAt, entry.mergedAt) });
830
+ } else if (entry.pr.state === "open") {
831
+ open.push(entry);
832
+ } else if (entry.closedAt !== null) {
833
+ closed.push({ entry, closedAt: entry.closedAt, hours: durationHours(entry.pr.createdAt, entry.closedAt) });
834
+ }
835
+ }
836
+ merged.sort((a, b) => b.mergedAt.getTime() - a.mergedAt.getTime());
837
+ closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
838
+ return { merged, closed, open, allHours: merged.map((result) => result.hours) };
839
+ }
821
840
  function computeCommentStats(sizes) {
822
841
  const metrics = [
823
842
  { label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
@@ -830,7 +849,7 @@ function computeCommentStats(sizes) {
830
849
  return { metrics, totals, uncommented, top };
831
850
  }
832
851
 
833
- // report.ts
852
+ // src/report.ts
834
853
  var BUCKETS = [];
835
854
  function initBuckets() {
836
855
  BUCKETS = makeBuckets();
@@ -911,7 +930,7 @@ function formatCount(value2) {
911
930
  return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
912
931
  }
913
932
 
914
- // tui/views/rows.ts
933
+ // src/tui/views/rows.ts
915
934
  function durationLead(result) {
916
935
  return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
917
936
  }
@@ -927,7 +946,7 @@ function toPrRows(entries, leads) {
927
946
  });
928
947
  }
929
948
 
930
- // tui/views/queue.ts
949
+ // src/tui/views/queue.ts
931
950
  function queueRows(view) {
932
951
  return view.lists.flatMap((list) => list.rows);
933
952
  }
@@ -986,10 +1005,10 @@ function rowsOf(group) {
986
1005
  );
987
1006
  }
988
1007
 
989
- // tui/components/ChartsPanel.tsx
1008
+ // src/tui/components/ChartsPanel.tsx
990
1009
  import { useTerminalDimensions } from "@opentui/react";
991
1010
 
992
- // tui/hooks/scrollbar.ts
1011
+ // src/tui/hooks/scrollbar.ts
993
1012
  import { CliRenderEvents } from "@opentui/core";
994
1013
  import { useRenderer } from "@opentui/react";
995
1014
  import { useLayoutEffect } from "react";
@@ -1020,7 +1039,7 @@ function useScrollbarSettle(scrollRef, mounted = true) {
1020
1039
  }, [scrollRef, renderer2, mounted]);
1021
1040
  }
1022
1041
 
1023
- // tui/views/charts/model.ts
1042
+ // src/tui/views/charts/model.ts
1024
1043
  function lineLength(line) {
1025
1044
  return line.reduce((sum, span) => sum + span.text.length, 0);
1026
1045
  }
@@ -1041,7 +1060,7 @@ function compact(value2) {
1041
1060
  return String(Math.round(value2 * 10) / 10);
1042
1061
  }
1043
1062
 
1044
- // tui/components/ChartsPanel.tsx
1063
+ // src/tui/components/ChartsPanel.tsx
1045
1064
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
1046
1065
  var COLUMN_GAP = 4;
1047
1066
  function ChartsPanel({
@@ -1146,7 +1165,7 @@ function ChartCard({ card }) {
1146
1165
  ] });
1147
1166
  }
1148
1167
 
1149
- // tui/components/Placeholder.tsx
1168
+ // src/tui/components/Placeholder.tsx
1150
1169
  import { jsx as jsx5, jsxs as jsxs4 } from "@opentui/react/jsx-runtime";
1151
1170
  var BAR_WIDTH = 40;
1152
1171
  var PARTIAL_BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
@@ -1180,7 +1199,7 @@ function loadLabel(load) {
1180
1199
  return load.phase === "search" ? "searching PRs..." : "fetching PR details";
1181
1200
  }
1182
1201
 
1183
- // tui/components/QueuePanel.tsx
1202
+ // src/tui/components/QueuePanel.tsx
1184
1203
  import { useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
1185
1204
  import { useEffect as useEffect2, useRef } from "react";
1186
1205
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "@opentui/react/jsx-runtime";
@@ -1280,7 +1299,7 @@ function QueuePanel({
1280
1299
  ] });
1281
1300
  }
1282
1301
 
1283
- // tui/components/RepoList.tsx
1302
+ // src/tui/components/RepoList.tsx
1284
1303
  import { useEffect as useEffect3, useRef as useRef2 } from "react";
1285
1304
  import { jsx as jsx7, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
1286
1305
  function RepoList({
@@ -1320,8 +1339,123 @@ function RepoList({
1320
1339
  );
1321
1340
  }
1322
1341
 
1323
- // tui/components/MainPanel.tsx
1324
- import { jsx as jsx8 } from "@opentui/react/jsx-runtime";
1342
+ // src/tui/state/browse.ts
1343
+ var TABS = ["1 Awaiting you", "2 Your PRs", "3 Review time", "4 PR size", "5 Comments"];
1344
+ var initialBrowseState = {
1345
+ tab: 0,
1346
+ authoredTab: "open",
1347
+ scopes: {
1348
+ pending: { view: "list" },
1349
+ open: { view: "list" },
1350
+ review: { view: "list" },
1351
+ size: { view: "list" },
1352
+ comment: { view: "list" },
1353
+ merged: { view: "list" }
1354
+ },
1355
+ repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0, merged: 0 },
1356
+ rowCursors: { pending: 0, open: 0 },
1357
+ grouped: { pending: false, open: false }
1358
+ };
1359
+ function dropVanishedRepo(scope, repos) {
1360
+ if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
1361
+ return { view: "list" };
1362
+ }
1363
+ return scope;
1364
+ }
1365
+ function cycled(previous, delta, count2) {
1366
+ return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
1367
+ }
1368
+ function browseReducer(state, action) {
1369
+ switch (action.type) {
1370
+ case "tabSelected": {
1371
+ return { ...state, tab: action.tab };
1372
+ }
1373
+ case "tabCycled": {
1374
+ return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
1375
+ }
1376
+ case "subTabToggled": {
1377
+ return { ...state, authoredTab: state.authoredTab === "open" ? "merged" : "open" };
1378
+ }
1379
+ case "repoCursorMoved": {
1380
+ return {
1381
+ ...state,
1382
+ repoCursors: {
1383
+ ...state.repoCursors,
1384
+ [action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
1385
+ }
1386
+ };
1387
+ }
1388
+ case "rowCursorMoved": {
1389
+ return {
1390
+ ...state,
1391
+ rowCursors: {
1392
+ ...state.rowCursors,
1393
+ [action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
1394
+ }
1395
+ };
1396
+ }
1397
+ case "repoOpened": {
1398
+ return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
1399
+ }
1400
+ case "pickerReturned": {
1401
+ return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
1402
+ }
1403
+ case "groupingToggled": {
1404
+ return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
1405
+ }
1406
+ case "dataLoaded": {
1407
+ return {
1408
+ ...state,
1409
+ scopes: {
1410
+ pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
1411
+ open: dropVanishedRepo(state.scopes.open, action.repos.open),
1412
+ review: dropVanishedRepo(state.scopes.review, action.repos.review),
1413
+ size: dropVanishedRepo(state.scopes.size, action.repos.size),
1414
+ comment: dropVanishedRepo(state.scopes.comment, action.repos.comment),
1415
+ merged: dropVanishedRepo(state.scopes.merged, action.repos.merged)
1416
+ }
1417
+ };
1418
+ }
1419
+ }
1420
+ return state;
1421
+ }
1422
+
1423
+ // src/tui/components/TabBar.tsx
1424
+ import { jsx as jsx8, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
1425
+ function TabBar({ tab }) {
1426
+ return /* @__PURE__ */ jsx8("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginTop: 1, columnGap: 1, children: TABS.map((label, i) => /* @__PURE__ */ jsx8(
1427
+ "text",
1428
+ {
1429
+ wrapMode: "none",
1430
+ fg: i === tab ? theme.accent : theme.muted,
1431
+ bg: i === tab ? theme.selectedBg : void 0,
1432
+ children: ` ${label} `
1433
+ },
1434
+ label
1435
+ )) });
1436
+ }
1437
+ var SUB_TABS = [
1438
+ { key: "open", label: "Open" },
1439
+ { key: "merged", label: "Merged & closed" }
1440
+ ];
1441
+ function SubTabBar({ active }) {
1442
+ return /* @__PURE__ */ jsxs7("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginBottom: 1, columnGap: 1, children: [
1443
+ SUB_TABS.map(({ key, label }) => /* @__PURE__ */ jsx8(
1444
+ "text",
1445
+ {
1446
+ wrapMode: "none",
1447
+ fg: key === active ? theme.accent : theme.muted,
1448
+ bg: key === active ? theme.selectedBg : void 0,
1449
+ children: ` ${label} `
1450
+ },
1451
+ key
1452
+ )),
1453
+ /* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t switches" })
1454
+ ] });
1455
+ }
1456
+
1457
+ // src/tui/components/MainPanel.tsx
1458
+ import { jsx as jsx9, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
1325
1459
  function QueueTab({
1326
1460
  prompt,
1327
1461
  repos,
@@ -1334,12 +1468,12 @@ function QueueTab({
1334
1468
  onRefClick
1335
1469
  }) {
1336
1470
  if (scope.view === "list") {
1337
- return /* @__PURE__ */ jsx8(RepoList, { prompt, options: repos, cursor: Math.min(repoCursor, repos.length - 1) });
1471
+ return /* @__PURE__ */ jsx9(RepoList, { prompt, options: repos, cursor: Math.min(repoCursor, repos.length - 1) });
1338
1472
  }
1339
1473
  if (view === null) {
1340
1474
  return null;
1341
1475
  }
1342
- return /* @__PURE__ */ jsx8(
1476
+ return /* @__PURE__ */ jsx9(
1343
1477
  QueuePanel,
1344
1478
  {
1345
1479
  heading: repos.length > 0 ? scope.repo ?? (grouped ? "All repos \xB7 grouped by repo" : "All repos") : null,
@@ -1361,12 +1495,12 @@ function StatsTab({
1361
1495
  warning
1362
1496
  }) {
1363
1497
  if (scope.view === "list") {
1364
- return /* @__PURE__ */ jsx8(RepoList, { options: repos, cursor: Math.min(cursor, repos.length - 1) });
1498
+ return /* @__PURE__ */ jsx9(RepoList, { options: repos, cursor: Math.min(cursor, repos.length - 1) });
1365
1499
  }
1366
1500
  if (view === null) {
1367
1501
  return null;
1368
1502
  }
1369
- return /* @__PURE__ */ jsx8(
1503
+ return /* @__PURE__ */ jsx9(
1370
1504
  ChartsPanel,
1371
1505
  {
1372
1506
  scrollRef,
@@ -1388,7 +1522,7 @@ function MainPanel({
1388
1522
  load,
1389
1523
  onRefClick
1390
1524
  }) {
1391
- return /* @__PURE__ */ jsx8("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx8(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */ jsx8(
1525
+ return /* @__PURE__ */ jsx9("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx9(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */ jsx9(
1392
1526
  QueueTab,
1393
1527
  {
1394
1528
  prompt: "Select a repository and press enter to open its review queue.",
@@ -1401,20 +1535,34 @@ function MainPanel({
1401
1535
  warning,
1402
1536
  onRefClick
1403
1537
  }
1404
- ) : browse.tab === 1 ? /* @__PURE__ */ jsx8(
1405
- QueueTab,
1406
- {
1407
- prompt: "Select a repository and press enter to list its open PRs.",
1408
- repos: views.openRepos,
1409
- scope: views.openScope,
1410
- view: views.open,
1411
- repoCursor: browse.repoCursors.open,
1412
- rowCursor: browse.rowCursors.open,
1413
- grouped: browse.grouped.open,
1414
- warning,
1415
- onRefClick
1416
- }
1417
- ) : browse.tab === 2 ? /* @__PURE__ */ jsx8(
1538
+ ) : browse.tab === 1 ? /* @__PURE__ */ jsxs8("box", { flexGrow: 1, flexDirection: "column", children: [
1539
+ /* @__PURE__ */ jsx9(SubTabBar, { active: browse.authoredTab }),
1540
+ browse.authoredTab === "open" ? /* @__PURE__ */ jsx9(
1541
+ QueueTab,
1542
+ {
1543
+ prompt: "Select a repository and press enter to list its open PRs.",
1544
+ repos: views.openRepos,
1545
+ scope: views.openScope,
1546
+ view: views.open,
1547
+ repoCursor: browse.repoCursors.open,
1548
+ rowCursor: browse.rowCursors.open,
1549
+ grouped: browse.grouped.open,
1550
+ warning,
1551
+ onRefClick
1552
+ }
1553
+ ) : /* @__PURE__ */ jsx9(
1554
+ StatsTab,
1555
+ {
1556
+ repos: views.mergedRepos,
1557
+ scope: views.mergedScope,
1558
+ view: views.merged,
1559
+ cursor: browse.repoCursors.merged,
1560
+ scrollRef: scrollRefs.merged,
1561
+ focused,
1562
+ warning
1563
+ }
1564
+ )
1565
+ ] }) : browse.tab === 2 ? /* @__PURE__ */ jsx9(
1418
1566
  StatsTab,
1419
1567
  {
1420
1568
  repos: views.reviewRepos,
@@ -1425,7 +1573,7 @@ function MainPanel({
1425
1573
  focused,
1426
1574
  warning
1427
1575
  }
1428
- ) : browse.tab === 3 ? /* @__PURE__ */ jsx8(
1576
+ ) : browse.tab === 3 ? /* @__PURE__ */ jsx9(
1429
1577
  StatsTab,
1430
1578
  {
1431
1579
  repos: views.sizeRepos,
@@ -1436,7 +1584,7 @@ function MainPanel({
1436
1584
  focused,
1437
1585
  warning
1438
1586
  }
1439
- ) : /* @__PURE__ */ jsx8(
1587
+ ) : /* @__PURE__ */ jsx9(
1440
1588
  StatsTab,
1441
1589
  {
1442
1590
  repos: views.commentRepos,
@@ -1450,7 +1598,7 @@ function MainPanel({
1450
1598
  ) });
1451
1599
  }
1452
1600
 
1453
- // ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
1601
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
1454
1602
  function stringReplaceAll(string, substring, postfix) {
1455
1603
  let index = string.indexOf(substring);
1456
1604
  if (index === -1) {
@@ -1480,7 +1628,7 @@ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
1480
1628
  return returnValue;
1481
1629
  }
1482
1630
 
1483
- // ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
1631
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
1484
1632
  var ANSI_BACKGROUND_OFFSET = 10;
1485
1633
  var ANSI_UNDERLINE_OFFSET = 20;
1486
1634
  var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
@@ -1702,7 +1850,7 @@ function assembleStyles() {
1702
1850
  var ansiStyles = assembleStyles();
1703
1851
  var ansi_styles_default = ansiStyles;
1704
1852
 
1705
- // ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/supports-color/index.js
1853
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/supports-color/index.js
1706
1854
  import process2 from "node:process";
1707
1855
  import os from "node:os";
1708
1856
  import tty from "node:tty";
@@ -1844,7 +1992,7 @@ var supportsColor = {
1844
1992
  };
1845
1993
  var supports_color_default = supportsColor;
1846
1994
 
1847
- // ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/index.js
1995
+ // node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/index.js
1848
1996
  var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
1849
1997
  var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
1850
1998
  var STYLER = /* @__PURE__ */ Symbol("STYLER");
@@ -2012,7 +2160,7 @@ var chalk = createChalk();
2012
2160
  var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
2013
2161
  var source_default = chalk;
2014
2162
 
2015
- // flags.ts
2163
+ // src/flags.ts
2016
2164
  import { parseArgs } from "node:util";
2017
2165
  var flag = source_default.green;
2018
2166
  var value = source_default.cyan;
@@ -2273,7 +2421,7 @@ function resolveTimezone(input) {
2273
2421
  return tz;
2274
2422
  }
2275
2423
 
2276
- // tui/state/options.ts
2424
+ // src/tui/state/options.ts
2277
2425
  var FIELDS = [
2278
2426
  {
2279
2427
  key: "since",
@@ -2445,14 +2593,14 @@ function targetLabelOf(target) {
2445
2593
  return target + (/^[\d.]+$/.test(target) ? "h" : "");
2446
2594
  }
2447
2595
 
2448
- // tui/components/ModalFrame.tsx
2449
- import { jsx as jsx9, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
2596
+ // src/tui/components/ModalFrame.tsx
2597
+ import { jsx as jsx10, jsxs as jsxs9 } from "@opentui/react/jsx-runtime";
2450
2598
  var MODAL_WIDTH = 64;
2451
2599
  function ModalFrame({ title, children }) {
2452
2600
  const tabWidth = title.length + 4;
2453
2601
  const tabTopRow = `\u250C${"\u2500".repeat(tabWidth - 2)}\u2510`;
2454
2602
  const tabTitleSuffix = ` \u2514${"\u2500".repeat(MODAL_WIDTH - tabWidth - 1)}\u2510`;
2455
- return /* @__PURE__ */ jsx9(
2603
+ return /* @__PURE__ */ jsx10(
2456
2604
  "box",
2457
2605
  {
2458
2606
  position: "absolute",
@@ -2463,14 +2611,14 @@ function ModalFrame({ title, children }) {
2463
2611
  alignItems: "center",
2464
2612
  justifyContent: "center",
2465
2613
  zIndex: 10,
2466
- children: /* @__PURE__ */ jsxs7("box", { flexDirection: "column", width: MODAL_WIDTH, children: [
2467
- /* @__PURE__ */ jsx9("text", { alignSelf: "flex-start", wrapMode: "none", fg: theme.border, bg: theme.bg, children: tabTopRow }),
2468
- /* @__PURE__ */ jsxs7("text", { wrapMode: "none", bg: theme.bg, children: [
2469
- /* @__PURE__ */ jsx9("span", { fg: theme.border, children: "\u2502 " }),
2470
- /* @__PURE__ */ jsx9("span", { fg: theme.muted, children: title }),
2471
- /* @__PURE__ */ jsx9("span", { fg: theme.border, children: tabTitleSuffix })
2614
+ children: /* @__PURE__ */ jsxs9("box", { flexDirection: "column", width: MODAL_WIDTH, children: [
2615
+ /* @__PURE__ */ jsx10("text", { alignSelf: "flex-start", wrapMode: "none", fg: theme.border, bg: theme.bg, children: tabTopRow }),
2616
+ /* @__PURE__ */ jsxs9("text", { wrapMode: "none", bg: theme.bg, children: [
2617
+ /* @__PURE__ */ jsx10("span", { fg: theme.border, children: "\u2502 " }),
2618
+ /* @__PURE__ */ jsx10("span", { fg: theme.muted, children: title }),
2619
+ /* @__PURE__ */ jsx10("span", { fg: theme.border, children: tabTitleSuffix })
2472
2620
  ] }),
2473
- /* @__PURE__ */ jsx9(
2621
+ /* @__PURE__ */ jsx10(
2474
2622
  "box",
2475
2623
  {
2476
2624
  border: ["left", "right", "bottom"],
@@ -2487,7 +2635,7 @@ function ModalFrame({ title, children }) {
2487
2635
  );
2488
2636
  }
2489
2637
  function ModalRow({ label, isSelected, children }) {
2490
- return /* @__PURE__ */ jsxs7(
2638
+ return /* @__PURE__ */ jsxs9(
2491
2639
  "box",
2492
2640
  {
2493
2641
  flexDirection: "row",
@@ -2496,17 +2644,17 @@ function ModalRow({ label, isSelected, children }) {
2496
2644
  paddingRight: 2,
2497
2645
  backgroundColor: isSelected ? theme.selectedBg : void 0,
2498
2646
  children: [
2499
- isSelected ? /* @__PURE__ */ jsx9("text", { position: "absolute", left: -1, wrapMode: "none", fg: theme.accent, bg: theme.bg, children: "\u2503" }) : null,
2500
- /* @__PURE__ */ jsx9("text", { wrapMode: "none", fg: theme.text, children: label }),
2501
- /* @__PURE__ */ jsx9("box", { flexGrow: 1 }),
2647
+ isSelected ? /* @__PURE__ */ jsx10("text", { position: "absolute", left: -1, wrapMode: "none", fg: theme.accent, bg: theme.bg, children: "\u2503" }) : null,
2648
+ /* @__PURE__ */ jsx10("text", { wrapMode: "none", fg: theme.text, children: label }),
2649
+ /* @__PURE__ */ jsx10("box", { flexGrow: 1 }),
2502
2650
  children
2503
2651
  ]
2504
2652
  }
2505
2653
  );
2506
2654
  }
2507
2655
 
2508
- // tui/components/OptionsModal.tsx
2509
- import { jsx as jsx10, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
2656
+ // src/tui/components/OptionsModal.tsx
2657
+ import { jsx as jsx11, jsxs as jsxs10 } from "@opentui/react/jsx-runtime";
2510
2658
  var EMPTY_PLACEHOLDERS = {
2511
2659
  repos: "(all accessible)",
2512
2660
  user: "(authenticated user)",
@@ -2528,12 +2676,12 @@ function OptionsModal({
2528
2676
  onSubmit
2529
2677
  }) {
2530
2678
  const savedState = savedLine(options, saved2);
2531
- return /* @__PURE__ */ jsxs8(ModalFrame, { title: "Options", children: [
2532
- SECTIONS.map((section) => /* @__PURE__ */ jsxs8("box", { flexDirection: "column", marginBottom: 1, children: [
2533
- /* @__PURE__ */ jsx10("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
2679
+ return /* @__PURE__ */ jsxs10(ModalFrame, { title: "Options", children: [
2680
+ SECTIONS.map((section) => /* @__PURE__ */ jsxs10("box", { flexDirection: "column", marginBottom: 1, children: [
2681
+ /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
2534
2682
  section.fields.map((field) => {
2535
2683
  const index = FIELDS.indexOf(field);
2536
- return /* @__PURE__ */ jsx10(
2684
+ return /* @__PURE__ */ jsx11(
2537
2685
  FieldRow,
2538
2686
  {
2539
2687
  field,
@@ -2547,8 +2695,8 @@ function OptionsModal({
2547
2695
  );
2548
2696
  })
2549
2697
  ] }, section.title)),
2550
- /* @__PURE__ */ jsx10("text", { wrapMode: "none", fg: savedState.color, marginLeft: 2, marginRight: 2, marginBottom: 1, children: savedState.text }),
2551
- /* @__PURE__ */ jsx10(
2698
+ /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: savedState.color, marginLeft: 2, marginRight: 2, marginBottom: 1, children: savedState.text }),
2699
+ /* @__PURE__ */ jsx11(
2552
2700
  "text",
2553
2701
  {
2554
2702
  wrapMode: "word",
@@ -2589,7 +2737,7 @@ function FieldRow({
2589
2737
  }) {
2590
2738
  const value2 = displayValue(field.key, options[field.key]);
2591
2739
  const valueColor = value2.isPlaceholder ? isSelected ? theme.muted : theme.dim : theme.muted;
2592
- return /* @__PURE__ */ jsx10(ModalRow, { label: field.label, isSelected, children: isEditing ? /* @__PURE__ */ jsx10(
2740
+ return /* @__PURE__ */ jsx11(ModalRow, { label: field.label, isSelected, children: isEditing ? /* @__PURE__ */ jsx11(
2593
2741
  "input",
2594
2742
  {
2595
2743
  width: 32,
@@ -2606,17 +2754,17 @@ function FieldRow({
2606
2754
  textColor: theme.text,
2607
2755
  cursorColor: theme.accent
2608
2756
  }
2609
- ) : isSelected && field.kind === "toggle" ? /* @__PURE__ */ jsxs8("text", { wrapMode: "none", children: [
2610
- /* @__PURE__ */ jsx10("span", { fg: theme.muted, children: "\u2039 " }),
2611
- /* @__PURE__ */ jsx10("b", { fg: theme.text, children: value2.text }),
2612
- /* @__PURE__ */ jsx10("span", { fg: theme.muted, children: " \u203A" })
2613
- ] }) : isSelected ? /* @__PURE__ */ jsx10("text", { wrapMode: "none", children: /* @__PURE__ */ jsx10("b", { fg: value2.isPlaceholder ? theme.muted : theme.text, children: value2.text }) }) : /* @__PURE__ */ jsx10("text", { wrapMode: "none", fg: valueColor, children: value2.text }) });
2757
+ ) : isSelected && field.kind === "toggle" ? /* @__PURE__ */ jsxs10("text", { wrapMode: "none", children: [
2758
+ /* @__PURE__ */ jsx11("span", { fg: theme.muted, children: "\u2039 " }),
2759
+ /* @__PURE__ */ jsx11("b", { fg: theme.text, children: value2.text }),
2760
+ /* @__PURE__ */ jsx11("span", { fg: theme.muted, children: " \u203A" })
2761
+ ] }) : isSelected ? /* @__PURE__ */ jsx11("text", { wrapMode: "none", children: /* @__PURE__ */ jsx11("b", { fg: value2.isPlaceholder ? theme.muted : theme.text, children: value2.text }) }) : /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: valueColor, children: value2.text }) });
2614
2762
  }
2615
2763
 
2616
- // tui/components/SettingsModal.tsx
2764
+ // src/tui/components/SettingsModal.tsx
2617
2765
  import { homedir as homedir2 } from "node:os";
2618
2766
 
2619
- // tui/state/settings.ts
2767
+ // src/tui/state/settings.ts
2620
2768
  var SETTINGS = [
2621
2769
  {
2622
2770
  key: "noCache",
@@ -2684,8 +2832,8 @@ var CACHE_MESSAGES = {
2684
2832
  resetDisabled: { text: "the cache is disabled for this session \xB7 nothing to reset" }
2685
2833
  };
2686
2834
 
2687
- // tui/components/SettingsModal.tsx
2688
- import { jsx as jsx11, jsxs as jsxs9 } from "@opentui/react/jsx-runtime";
2835
+ // src/tui/components/SettingsModal.tsx
2836
+ import { jsx as jsx12, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
2689
2837
  var SECTIONS2 = [];
2690
2838
  for (const setting of SETTINGS) {
2691
2839
  const last = SECTIONS2.at(-1);
@@ -2703,12 +2851,12 @@ function SettingsModal({
2703
2851
  preset
2704
2852
  }) {
2705
2853
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
2706
- return /* @__PURE__ */ jsxs9(ModalFrame, { title: "Settings", children: [
2707
- SECTIONS2.map((section) => /* @__PURE__ */ jsxs9("box", { flexDirection: "column", marginBottom: 1, children: [
2708
- /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
2854
+ return /* @__PURE__ */ jsxs11(ModalFrame, { title: "Settings", children: [
2855
+ SECTIONS2.map((section) => /* @__PURE__ */ jsxs11("box", { flexDirection: "column", marginBottom: 1, children: [
2856
+ /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
2709
2857
  section.settings.map((setting) => {
2710
2858
  const isSelected = SETTINGS.indexOf(setting) === selected;
2711
- return /* @__PURE__ */ jsx11(ModalRow, { label: setting.label, isSelected, children: /* @__PURE__ */ jsx11(
2859
+ return /* @__PURE__ */ jsx12(ModalRow, { label: setting.label, isSelected, children: /* @__PURE__ */ jsx12(
2712
2860
  SettingValue,
2713
2861
  {
2714
2862
  setting,
@@ -2721,7 +2869,7 @@ function SettingsModal({
2721
2869
  ) }, setting.key);
2722
2870
  })
2723
2871
  ] }, section.title)),
2724
- /* @__PURE__ */ jsx11("text", { wrapMode: "word", height: 2, fg: message?.warn ? theme.warn : theme.muted, marginLeft: 2, marginRight: 2, children: message?.text ?? SETTINGS[selected].hint })
2872
+ /* @__PURE__ */ jsx12("text", { wrapMode: "word", height: 2, fg: message?.warn ? theme.warn : theme.muted, marginLeft: 2, marginRight: 2, children: message?.text ?? SETTINGS[selected].hint })
2725
2873
  ] });
2726
2874
  }
2727
2875
  function SettingValue({
@@ -2734,22 +2882,22 @@ function SettingValue({
2734
2882
  }) {
2735
2883
  switch (setting.key) {
2736
2884
  case "noCache": {
2737
- return /* @__PURE__ */ jsx11(ToggleValue, { value: noCache2 ? "yes" : "no", isSelected });
2885
+ return /* @__PURE__ */ jsx12(ToggleValue, { value: noCache2 ? "yes" : "no", isSelected });
2738
2886
  }
2739
2887
  case "clearCache": {
2740
- return /* @__PURE__ */ jsx11(PathValue, { path: cacheDir(), confirming: cacheAction === "confirm", isSelected });
2888
+ return /* @__PURE__ */ jsx12(PathValue, { path: cacheDir(), confirming: cacheAction === "confirm", isSelected });
2741
2889
  }
2742
2890
  case "copyLinks": {
2743
- return /* @__PURE__ */ jsx11(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
2891
+ return /* @__PURE__ */ jsx12(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
2744
2892
  }
2745
2893
  case "themePreset": {
2746
- return /* @__PURE__ */ jsx11(ToggleValue, { value: preset, isSelected });
2894
+ return /* @__PURE__ */ jsx12(ToggleValue, { value: preset, isSelected });
2747
2895
  }
2748
2896
  case "themeColors": {
2749
- return /* @__PURE__ */ jsx11("text", { wrapMode: "none", children: ["chartDim", "chartBar", "chartLine", "accent"].map((key) => /* @__PURE__ */ jsx11("span", { fg: theme[key], children: "\u2588\u2588" }, key)) });
2897
+ return /* @__PURE__ */ jsx12("text", { wrapMode: "none", children: ["chartDim", "chartBar", "chartLine", "accent"].map((key) => /* @__PURE__ */ jsx12("span", { fg: theme[key], children: "\u2588\u2588" }, key)) });
2750
2898
  }
2751
2899
  case "resetSettings": {
2752
- return /* @__PURE__ */ jsx11(PathValue, { path: settingsFile(), confirming: cacheAction === "resetConfirm", isSelected });
2900
+ return /* @__PURE__ */ jsx12(PathValue, { path: settingsFile(), confirming: cacheAction === "resetConfirm", isSelected });
2753
2901
  }
2754
2902
  default: {
2755
2903
  return null;
@@ -2758,113 +2906,23 @@ function SettingValue({
2758
2906
  }
2759
2907
  function ToggleValue({ value: value2, isSelected }) {
2760
2908
  if (isSelected) {
2761
- return /* @__PURE__ */ jsxs9("text", { wrapMode: "none", children: [
2762
- /* @__PURE__ */ jsx11("span", { fg: theme.muted, children: "\u2039 " }),
2763
- /* @__PURE__ */ jsx11("b", { fg: theme.text, children: value2 }),
2764
- /* @__PURE__ */ jsx11("span", { fg: theme.muted, children: " \u203A" })
2909
+ return /* @__PURE__ */ jsxs11("text", { wrapMode: "none", children: [
2910
+ /* @__PURE__ */ jsx12("span", { fg: theme.muted, children: "\u2039 " }),
2911
+ /* @__PURE__ */ jsx12("b", { fg: theme.text, children: value2 }),
2912
+ /* @__PURE__ */ jsx12("span", { fg: theme.muted, children: " \u203A" })
2765
2913
  ] });
2766
2914
  }
2767
- return /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: theme.muted, children: value2 });
2915
+ return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.muted, children: value2 });
2768
2916
  }
2769
2917
  function PathValue({ path, confirming, isSelected }) {
2770
2918
  if (confirming) {
2771
- return /* @__PURE__ */ jsx11("text", { wrapMode: "none", children: /* @__PURE__ */ jsx11("b", { fg: theme.warn, children: "enter to confirm" }) });
2772
- }
2773
- return /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
2774
- }
2775
-
2776
- // tui/state/browse.ts
2777
- var TABS = ["1 Awaiting you", "2 Your open PRs", "3 Review time", "4 PR size", "5 Comments"];
2778
- var initialBrowseState = {
2779
- tab: 0,
2780
- scopes: {
2781
- pending: { view: "list" },
2782
- open: { view: "list" },
2783
- review: { view: "list" },
2784
- size: { view: "list" },
2785
- comment: { view: "list" }
2786
- },
2787
- repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0 },
2788
- rowCursors: { pending: 0, open: 0 },
2789
- grouped: { pending: false, open: false }
2790
- };
2791
- function dropVanishedRepo(scope, repos) {
2792
- if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
2793
- return { view: "list" };
2794
- }
2795
- return scope;
2796
- }
2797
- function cycled(previous, delta, count2) {
2798
- return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
2799
- }
2800
- function browseReducer(state, action) {
2801
- switch (action.type) {
2802
- case "tabSelected": {
2803
- return { ...state, tab: action.tab };
2804
- }
2805
- case "tabCycled": {
2806
- return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
2807
- }
2808
- case "repoCursorMoved": {
2809
- return {
2810
- ...state,
2811
- repoCursors: {
2812
- ...state.repoCursors,
2813
- [action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
2814
- }
2815
- };
2816
- }
2817
- case "rowCursorMoved": {
2818
- return {
2819
- ...state,
2820
- rowCursors: {
2821
- ...state.rowCursors,
2822
- [action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
2823
- }
2824
- };
2825
- }
2826
- case "repoOpened": {
2827
- return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
2828
- }
2829
- case "pickerReturned": {
2830
- return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
2831
- }
2832
- case "groupingToggled": {
2833
- return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
2834
- }
2835
- case "dataLoaded": {
2836
- return {
2837
- ...state,
2838
- scopes: {
2839
- pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
2840
- open: dropVanishedRepo(state.scopes.open, action.repos.open),
2841
- review: dropVanishedRepo(state.scopes.review, action.repos.review),
2842
- size: dropVanishedRepo(state.scopes.size, action.repos.size),
2843
- comment: dropVanishedRepo(state.scopes.comment, action.repos.comment)
2844
- }
2845
- };
2846
- }
2919
+ return /* @__PURE__ */ jsx12("text", { wrapMode: "none", children: /* @__PURE__ */ jsx12("b", { fg: theme.warn, children: "enter to confirm" }) });
2847
2920
  }
2848
- return state;
2921
+ return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
2849
2922
  }
2850
2923
 
2851
- // tui/components/TabBar.tsx
2852
- import { jsx as jsx12 } from "@opentui/react/jsx-runtime";
2853
- function TabBar({ tab }) {
2854
- return /* @__PURE__ */ jsx12("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginTop: 1, columnGap: 1, children: TABS.map((label, i) => /* @__PURE__ */ jsx12(
2855
- "text",
2856
- {
2857
- wrapMode: "none",
2858
- fg: i === tab ? theme.accent : theme.muted,
2859
- bg: i === tab ? theme.selectedBg : void 0,
2860
- children: ` ${label} `
2861
- },
2862
- label
2863
- )) });
2864
- }
2865
-
2866
- // tui/components/ThemeModal.tsx
2867
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "@opentui/react/jsx-runtime";
2924
+ // src/tui/components/ThemeModal.tsx
2925
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs12 } from "@opentui/react/jsx-runtime";
2868
2926
  function ThemeModal({
2869
2927
  selected,
2870
2928
  editing,
@@ -2877,7 +2935,7 @@ function ThemeModal({
2877
2935
  const spec = THEME_COLORS[selected];
2878
2936
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
2879
2937
  const hint = spec.key in overrides ? `${spec.hint} \xB7 custom color, an empty value restores the theme` : spec.hint;
2880
- return /* @__PURE__ */ jsxs10(ModalFrame, { title: "Theme colors", children: [
2938
+ return /* @__PURE__ */ jsxs12(ModalFrame, { title: "Theme colors", children: [
2881
2939
  /* @__PURE__ */ jsx13("box", { flexDirection: "column", marginBottom: 1, children: THEME_COLORS.map((color, index) => /* @__PURE__ */ jsx13(
2882
2940
  ColorRow,
2883
2941
  {
@@ -2920,8 +2978,8 @@ function ColorRow({
2920
2978
  textColor: theme.text,
2921
2979
  cursorColor: theme.accent
2922
2980
  }
2923
- ) : /* @__PURE__ */ jsxs10("text", { wrapMode: "none", children: [
2924
- Array.isArray(swatch) ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
2981
+ ) : /* @__PURE__ */ jsxs12("text", { wrapMode: "none", children: [
2982
+ Array.isArray(swatch) ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
2925
2983
  /* @__PURE__ */ jsx13("span", { fg: swatch[0], children: "\u2588" }),
2926
2984
  /* @__PURE__ */ jsx13("span", { fg: swatch[1], children: "\u2588" }),
2927
2985
  /* @__PURE__ */ jsx13("span", { fg: swatch[2], children: "\u2588" }),
@@ -2932,7 +2990,7 @@ function ColorRow({
2932
2990
  ] }) });
2933
2991
  }
2934
2992
 
2935
- // tui/hooks/useDeferredLoading.ts
2993
+ // src/tui/hooks/useDeferredLoading.ts
2936
2994
  import { useEffect as useEffect4, useRef as useRef3, useState as useState2 } from "react";
2937
2995
  function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } = {}) {
2938
2996
  const [visible, setVisible] = useState2(isLoading && showDelay === 0);
@@ -2960,10 +3018,10 @@ function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } =
2960
3018
  return visible;
2961
3019
  }
2962
3020
 
2963
- // tui/hooks/useLoader.ts
3021
+ // src/tui/hooks/useLoader.ts
2964
3022
  import { useEffect as useEffect5, useRef as useRef4, useState as useState3 } from "react";
2965
3023
 
2966
- // github.ts
3024
+ // src/github.ts
2967
3025
  import { execFile } from "node:child_process";
2968
3026
  import { createHash } from "node:crypto";
2969
3027
  import { statSync } from "node:fs";
@@ -3207,6 +3265,8 @@ async function fetchPrSizes(prs) {
3207
3265
  additions
3208
3266
  deletions
3209
3267
  changedFiles
3268
+ mergedAt
3269
+ closedAt
3210
3270
  comments {
3211
3271
  totalCount
3212
3272
  }
@@ -3225,7 +3285,7 @@ async function fetchPrSizes(prs) {
3225
3285
  return prs.map((pr, i) => data[`pr${i}`]?.pullRequest ?? null);
3226
3286
  }
3227
3287
 
3228
- // data.ts
3288
+ // src/data.ts
3229
3289
  var BATCH_SIZE = 25;
3230
3290
  var MAX_CONCURRENT_BATCHES = 4;
3231
3291
  function createLimiter(maxConcurrent) {
@@ -3407,6 +3467,8 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
3407
3467
  additions: details.additions,
3408
3468
  deletions: details.deletions,
3409
3469
  total: details.additions + details.deletions,
3470
+ mergedAt: details.mergedAt === null ? null : new Date(details.mergedAt),
3471
+ closedAt: details.closedAt === null ? null : new Date(details.closedAt),
3410
3472
  comments: { discussion, review, total: discussion + review }
3411
3473
  });
3412
3474
  }
@@ -3414,7 +3476,7 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
3414
3476
  return { sizes, cacheHits };
3415
3477
  }
3416
3478
 
3417
- // tui/data/load.ts
3479
+ // src/tui/data/load.ts
3418
3480
  function reviveRawData(data) {
3419
3481
  return {
3420
3482
  ...data,
@@ -3430,7 +3492,12 @@ function reviveRawData(data) {
3430
3492
  return { ...result, pr };
3431
3493
  }),
3432
3494
  sizes: data.sizes.map((entry) => {
3433
- return { ...entry, pr: { ...entry.pr, createdAt: new Date(entry.pr.createdAt) } };
3495
+ return {
3496
+ ...entry,
3497
+ pr: { ...entry.pr, createdAt: new Date(entry.pr.createdAt) },
3498
+ mergedAt: entry.mergedAt === null ? null : new Date(entry.mergedAt),
3499
+ closedAt: entry.closedAt === null ? null : new Date(entry.closedAt)
3500
+ };
3434
3501
  })
3435
3502
  };
3436
3503
  }
@@ -3530,7 +3597,7 @@ async function loadData(options, onPhase, { bypassCache = false } = {}) {
3530
3597
  return data;
3531
3598
  }
3532
3599
 
3533
- // tui/hooks/useLoader.ts
3600
+ // src/tui/hooks/useLoader.ts
3534
3601
  function useLoader(options, noCache2, onLoaded) {
3535
3602
  const [startupSnapshot] = useState3(() => noCache2 ? null : loadSnapshot(options));
3536
3603
  const [raw, setRaw] = useState3(startupSnapshot);
@@ -3601,10 +3668,10 @@ function useLoader(options, noCache2, onLoaded) {
3601
3668
  };
3602
3669
  }
3603
3670
 
3604
- // tui/hooks/useViewModel.ts
3671
+ // src/tui/hooks/useViewModel.ts
3605
3672
  import { useMemo } from "react";
3606
3673
 
3607
- // tui/views/repos.ts
3674
+ // src/tui/views/repos.ts
3608
3675
  function reviewDetail(counts) {
3609
3676
  return `${counts.reviewed} reviewed` + (counts.pending > 0 ? `, ${counts.pending} pending` : "");
3610
3677
  }
@@ -3698,6 +3765,38 @@ function buildOpenRepoOptions(raw) {
3698
3765
  })
3699
3766
  ];
3700
3767
  }
3768
+ function mergedDetail(counts) {
3769
+ return `${counts.merged} merged` + (counts.closed > 0 ? `, ${counts.closed} closed unmerged` : "");
3770
+ }
3771
+ function buildMergedRepoOptions(raw) {
3772
+ const countsByRepo = /* @__PURE__ */ new Map();
3773
+ for (const size of raw.sizes) {
3774
+ const counts = countsByRepo.get(size.pr.repo) ?? { merged: 0, closed: 0 };
3775
+ if (size.mergedAt !== null) {
3776
+ counts.merged += 1;
3777
+ } else if (size.pr.state !== "open") {
3778
+ counts.closed += 1;
3779
+ }
3780
+ countsByRepo.set(size.pr.repo, counts);
3781
+ }
3782
+ if (countsByRepo.size < 2) {
3783
+ return [];
3784
+ }
3785
+ const entries = [...countsByRepo.entries()].toSorted(
3786
+ (a, b) => b[1].merged - a[1].merged || b[1].closed - a[1].closed || a[0].localeCompare(b[0])
3787
+ );
3788
+ const totals = { merged: 0, closed: 0 };
3789
+ for (const [, counts] of entries) {
3790
+ totals.merged += counts.merged;
3791
+ totals.closed += counts.closed;
3792
+ }
3793
+ return [
3794
+ { repo: null, label: "All repos", detail: mergedDetail(totals) },
3795
+ ...entries.map(([repo, counts]) => {
3796
+ return { repo, label: repo, detail: mergedDetail(counts) };
3797
+ })
3798
+ ];
3799
+ }
3701
3800
  function commentDetail(comments, prs) {
3702
3801
  return `${comments} ${comments === 1 ? "comment" : "comments"} on ${prs} ${prs === 1 ? "PR" : "PRs"}`;
3703
3802
  }
@@ -3728,7 +3827,7 @@ function buildCommentRepoOptions(raw) {
3728
3827
  ];
3729
3828
  }
3730
3829
 
3731
- // tui/views/charts/draw.ts
3830
+ // src/tui/views/charts/draw.ts
3732
3831
  function blankCells(width) {
3733
3832
  return Array.from({ length: width }, () => {
3734
3833
  return { ch: " " };
@@ -3781,7 +3880,7 @@ function hbar(fraction, width, color) {
3781
3880
  return line;
3782
3881
  }
3783
3882
 
3784
- // tui/views/charts/distribution.ts
3883
+ // src/tui/views/charts/distribution.ts
3785
3884
  var DURATION_TICKS = [5 / 60, 0.25, 0.5, 1, 2, 4, 8, 24, 48, 96, 168, 336, 720, 2160];
3786
3885
  var COUNT_TICKS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4, 25e3, 5e4, 1e5];
3787
3886
  function buildDistribution({ values, width, format, ticks, flat }) {
@@ -3860,7 +3959,7 @@ function buildDistribution({ values, width, format, ticks, flat }) {
3860
3959
  };
3861
3960
  }
3862
3961
 
3863
- // tui/views/charts/gauge.ts
3962
+ // src/tui/views/charts/gauge.ts
3864
3963
  var GAUGE_BAR_WIDTH = 24;
3865
3964
  function buildGaugeCard({ title, subtitle, rows }) {
3866
3965
  const total = rows.reduce((sum, row) => sum + row.count, 0);
@@ -3883,7 +3982,7 @@ function buildGaugeCard({ title, subtitle, rows }) {
3883
3982
  return { title, subtitle, lines };
3884
3983
  }
3885
3984
 
3886
- // tui/views/charts/heatmap.ts
3985
+ // src/tui/views/charts/heatmap.ts
3887
3986
  var WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
3888
3987
  function heatColor(count2) {
3889
3988
  if (count2 === 1) {
@@ -3949,7 +4048,7 @@ function buildHeatmapCard({ title, subtitle, grid, columns, legend }) {
3949
4048
  return { title, subtitle, lines };
3950
4049
  }
3951
4050
 
3952
- // tui/views/charts/histogram.ts
4051
+ // src/tui/views/charts/histogram.ts
3953
4052
  var HIST_BAR_WIDTH = 36;
3954
4053
  function axisStep(maxCount) {
3955
4054
  for (const step of [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1e3, 2e3, 5e3]) {
@@ -4001,7 +4100,7 @@ function buildHistogramCard({ title, subtitle, values, buckets, format }) {
4001
4100
  return { title, subtitle, lines };
4002
4101
  }
4003
4102
 
4004
- // tui/views/charts/scatter.ts
4103
+ // src/tui/views/charts/scatter.ts
4005
4104
  var SCATTER_WIDTH = 36;
4006
4105
  var SCATTER_HEIGHT = 8;
4007
4106
  var DOT_BITS = [
@@ -4051,7 +4150,7 @@ function buildScatterCard({ title, subtitle, points, formatX, formatY }) {
4051
4150
  return { title, subtitle, lines };
4052
4151
  }
4053
4152
 
4054
- // tui/views/charts/spread.ts
4153
+ // src/tui/views/charts/spread.ts
4055
4154
  var SPREAD_STRIP_WIDTH = 32;
4056
4155
  function buildSpreadCard(title, metrics, format) {
4057
4156
  const rows = metrics.map(({ label, values }) => {
@@ -4096,10 +4195,10 @@ function buildSpreadCard(title, metrics, format) {
4096
4195
  return { title, subtitle, lines };
4097
4196
  }
4098
4197
 
4099
- // tui/views/charts/trend.ts
4198
+ // src/tui/views/charts/trend.ts
4100
4199
  var asciichart = __toESM(require_asciichart(), 1);
4101
4200
 
4102
- // tui/views/charts/weeks.ts
4201
+ // src/tui/views/charts/weeks.ts
4103
4202
  var DAY_MS = 864e5;
4104
4203
  var WEEK_MS = 7 * DAY_MS;
4105
4204
  function mondayOf(dayUtcMs) {
@@ -4123,7 +4222,7 @@ function weekAxisRow(width, prefix, points, columnOf, mondayOfPoint) {
4123
4222
  return mergeCells(cells);
4124
4223
  }
4125
4224
 
4126
- // tui/views/charts/trend.ts
4225
+ // src/tui/views/charts/trend.ts
4127
4226
  var TREND_POINTS = 40;
4128
4227
  var TREND_HEIGHT = 6;
4129
4228
  function axisSplit(line) {
@@ -4217,7 +4316,7 @@ function buildTrendCard({ title, entries, format, floor }) {
4217
4316
  return { title, subtitle, lines };
4218
4317
  }
4219
4318
 
4220
- // tui/views/charts/volume.ts
4319
+ // src/tui/views/charts/volume.ts
4221
4320
  var VOLUME_ROWS = 4;
4222
4321
  var VOLUME_MAX_BARS = 18;
4223
4322
  var V_PARTIALS = [" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
@@ -4273,7 +4372,7 @@ function buildVolumeCard(title, dates, weights) {
4273
4372
  return { title, subtitle, lines };
4274
4373
  }
4275
4374
 
4276
- // tui/views/stats.ts
4375
+ // src/tui/views/stats.ts
4277
4376
  function countCell(count2, label, dimWhenZero = false) {
4278
4377
  const dim2 = dimWhenZero && count2 === 0;
4279
4378
  return [
@@ -4578,11 +4677,122 @@ function buildCommentView(raw, repo = null, width = 100) {
4578
4677
  }
4579
4678
  return { empty: null, ...base, strip, headline, left, right, distribution, lists };
4580
4679
  }
4680
+ function buildMergedView(raw, repo = null, width = 100) {
4681
+ const base = {
4682
+ strip: [],
4683
+ headline: null,
4684
+ distributionTitle: "Time to merge distribution",
4685
+ noCharts: "No merged PRs to chart.",
4686
+ left: [],
4687
+ right: [],
4688
+ distribution: null,
4689
+ lists: []
4690
+ };
4691
+ if (raw.authoredTotal === 0) {
4692
+ return { empty: "No authored PRs found.", ...base };
4693
+ }
4694
+ const sizes = repo === null ? raw.sizes : raw.sizes.filter((size) => size.pr.repo === repo);
4695
+ if (sizes.length === 0) {
4696
+ return { empty: "No accessible authored PRs to analyze.", ...base };
4697
+ }
4698
+ const stats = computeMergeStats(sizes);
4699
+ const strip = [
4700
+ countCell(sizes.length, "PRs created"),
4701
+ countCell(stats.merged.length, "merged"),
4702
+ countCell(stats.closed.length, "closed unmerged", true),
4703
+ countCell(stats.open.length, "still open", true)
4704
+ ];
4705
+ if (repo === null) {
4706
+ strip.push([{ text: `${raw.authoredTotal - raw.sizes.length} inaccessible (excluded)`, fg: theme.dim }]);
4707
+ }
4708
+ const lists = [];
4709
+ if (stats.merged.length > 0) {
4710
+ const top = stats.merged.slice(0, 5);
4711
+ lists.push({
4712
+ title: "Recently merged PRs",
4713
+ rows: toPrRows(
4714
+ top.map((result) => result.entry),
4715
+ top.map((result) => `${durationLead(result)} to merge`)
4716
+ )
4717
+ });
4718
+ }
4719
+ if (stats.closed.length > 0) {
4720
+ const top = stats.closed.slice(0, 5);
4721
+ lists.push({
4722
+ title: "Closed without merging",
4723
+ rows: toPrRows(
4724
+ top.map((result) => result.entry),
4725
+ top.map((result) => `${durationLead(result)} to close`)
4726
+ )
4727
+ });
4728
+ }
4729
+ if (stats.merged.length === 0) {
4730
+ return { empty: null, ...base, strip, lists };
4731
+ }
4732
+ const sorted = [...stats.allHours].toSorted((a, b) => a - b);
4733
+ const headline = [
4734
+ { text: "p50 ", fg: theme.muted },
4735
+ { text: formatDuration(percentile(sorted, 50)), fg: theme.accent },
4736
+ { text: " p90 ", fg: theme.muted },
4737
+ { text: formatDuration(percentile(sorted, 90)), fg: theme.accent },
4738
+ { text: ` ${stats.merged.length} of ${sizes.length} PRs merged`, fg: theme.muted }
4739
+ ];
4740
+ const mergeDates = stats.merged.map((result) => result.mergedAt);
4741
+ const created = sizes.map((size) => size.pr.createdAt);
4742
+ const left = [
4743
+ buildHistogramCard({
4744
+ title: "Time to merge",
4745
+ subtitle: "elapsed time, created \u2192 merged",
4746
+ values: stats.allHours,
4747
+ buckets: currentBuckets(),
4748
+ format: formatDuration
4749
+ }),
4750
+ buildHeatmapCard({
4751
+ title: "When your PRs merge",
4752
+ subtitle: "PRs merged, weekday \xD7 hour, local time",
4753
+ grid: mergeDates,
4754
+ columns: [
4755
+ { label: "merged", dates: mergeDates },
4756
+ { label: "created", dates: created, muted: true }
4757
+ ],
4758
+ legend: "PRs merged in that hour"
4759
+ }),
4760
+ buildGaugeCard({
4761
+ title: "Outcomes",
4762
+ subtitle: "where your authored PRs ended up",
4763
+ rows: [
4764
+ { label: "merged", count: stats.merged.length, color: theme.accent },
4765
+ { label: "closed unmerged", count: stats.closed.length, color: theme.warn },
4766
+ ...stats.open.length > 0 ? [{ label: "still open", count: stats.open.length, color: theme.chartDim }] : []
4767
+ ]
4768
+ })
4769
+ ];
4770
+ const right = [
4771
+ buildTrendCard({
4772
+ title: "Time to merge trend",
4773
+ entries: stats.merged.map((result) => {
4774
+ return { date: result.mergedAt, value: result.hours };
4775
+ }),
4776
+ format: formatDuration,
4777
+ floor: 1 / 60
4778
+ }),
4779
+ buildVolumeCard("PRs merged per week", mergeDates),
4780
+ buildVolumeCard("PRs created per week", created)
4781
+ ];
4782
+ const distribution = buildDistribution({
4783
+ values: stats.allHours,
4784
+ width: Math.max(width - 3, 40),
4785
+ format: formatDuration,
4786
+ ticks: DURATION_TICKS,
4787
+ flat: (n, value2) => `all ${n} merged ${n === 1 ? "PR" : "PRs"} took ${value2}`
4788
+ });
4789
+ return { empty: null, ...base, strip, headline, left, right, distribution, lists };
4790
+ }
4581
4791
  function count(value2) {
4582
4792
  return formatCount(Math.round(value2));
4583
4793
  }
4584
4794
 
4585
- // tui/hooks/useViewModel.ts
4795
+ // src/tui/hooks/useViewModel.ts
4586
4796
  function resolveScope(scope, repos) {
4587
4797
  if (repos.length === 0) {
4588
4798
  return { view: "detail", repo: null };
@@ -4605,11 +4815,13 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4605
4815
  const sizeTarget = options.sizeTarget === "" ? void 0 : parseSizeTarget(options.sizeTarget);
4606
4816
  const pendingRepos = buildPendingRepoOptions(raw);
4607
4817
  const openRepos = buildOpenRepoOptions(raw);
4818
+ const mergedRepos = buildMergedRepoOptions(raw);
4608
4819
  const reviewRepos = buildReviewRepoOptions(raw);
4609
4820
  const sizeRepos = buildSizeRepoOptions(raw);
4610
4821
  const commentRepos = buildCommentRepoOptions(raw);
4611
4822
  const pendingScope = resolveScope(scopes.pending, pendingRepos);
4612
4823
  const openScope = resolveScope(scopes.open, openRepos);
4824
+ const mergedScope = resolveScope(scopes.merged, mergedRepos);
4613
4825
  const reviewScope = resolveScope(scopes.review, reviewRepos);
4614
4826
  const sizeScope = resolveScope(scopes.size, sizeRepos);
4615
4827
  const commentScope = resolveScope(scopes.comment, commentRepos);
@@ -4617,16 +4829,19 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4617
4829
  return {
4618
4830
  pendingRepos,
4619
4831
  openRepos,
4832
+ mergedRepos,
4620
4833
  reviewRepos,
4621
4834
  sizeRepos,
4622
4835
  commentRepos,
4623
4836
  pendingScope,
4624
4837
  openScope,
4838
+ mergedScope,
4625
4839
  reviewScope,
4626
4840
  sizeScope,
4627
4841
  commentScope,
4628
4842
  pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending) : null,
4629
4843
  open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
4844
+ merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width) : null,
4630
4845
  review,
4631
4846
  size: sizeScope.view === "detail" ? buildSizeView(raw, sizeTarget, sizeScope.repo, width) : null,
4632
4847
  comments: commentScope.view === "detail" ? buildCommentView(raw, commentScope.repo, width) : null
@@ -4641,6 +4856,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4641
4856
  width,
4642
4857
  scopes.pending,
4643
4858
  scopes.open,
4859
+ scopes.merged,
4644
4860
  scopes.review,
4645
4861
  scopes.size,
4646
4862
  scopes.comment,
@@ -4650,7 +4866,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4650
4866
  ]);
4651
4867
  }
4652
4868
 
4653
- // tui/keymap.ts
4869
+ // src/tui/keymap.ts
4654
4870
  function handleOptionsModalKey(key, context) {
4655
4871
  switch (key.name) {
4656
4872
  case "escape": {
@@ -4871,6 +5087,13 @@ function handleQueueKey(key, context) {
4871
5087
  }
4872
5088
  function statsTabOf(context) {
4873
5089
  const { views } = context;
5090
+ if (context.browse.tab === 1) {
5091
+ return {
5092
+ key: "merged",
5093
+ repos: views?.mergedRepos ?? [],
5094
+ scope: views?.mergedScope ?? null
5095
+ };
5096
+ }
4874
5097
  if (context.browse.tab === 2) {
4875
5098
  return {
4876
5099
  key: "review",
@@ -4957,14 +5180,16 @@ function handleAppKey(key, context) {
4957
5180
  context.dispatchBrowse({ type: "tabCycled", delta: -1 });
4958
5181
  } else if (key.name === "right" || key.name === "tab") {
4959
5182
  context.dispatchBrowse({ type: "tabCycled", delta: 1 });
4960
- } else if (context.browse.tab <= 1) {
5183
+ } else if (context.browse.tab === 1 && key.name === "t") {
5184
+ context.dispatchBrowse({ type: "subTabToggled" });
5185
+ } else if (context.browse.tab === 0 || context.browse.tab === 1 && context.browse.authoredTab === "open") {
4961
5186
  handleQueueKey(key, context);
4962
5187
  } else {
4963
5188
  handleStatsKey(key, context);
4964
5189
  }
4965
5190
  }
4966
5191
 
4967
- // tui/state/ui.ts
5192
+ // src/tui/state/ui.ts
4968
5193
  var initialUiState = {
4969
5194
  modal: null,
4970
5195
  editing: false,
@@ -5061,7 +5286,7 @@ function uiReducer(state, action) {
5061
5286
  return state;
5062
5287
  }
5063
5288
 
5064
- // tui/utils/browser.ts
5289
+ // src/tui/utils/browser.ts
5065
5290
  import { spawn } from "node:child_process";
5066
5291
  function openCommand(url) {
5067
5292
  switch (process.platform) {
@@ -5090,7 +5315,7 @@ function openInBrowser(url, onError) {
5090
5315
  child.unref();
5091
5316
  }
5092
5317
 
5093
- // tui/utils/clipboard.ts
5318
+ // src/tui/utils/clipboard.ts
5094
5319
  import { spawn as spawn2 } from "node:child_process";
5095
5320
  function copyCommand() {
5096
5321
  switch (process.platform) {
@@ -5124,8 +5349,8 @@ function copyToClipboard(text, onError) {
5124
5349
  child.stdin.end(text);
5125
5350
  }
5126
5351
 
5127
- // tui/App.tsx
5128
- import { jsx as jsx14, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
5352
+ // src/tui/App.tsx
5353
+ import { jsx as jsx14, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
5129
5354
  function App({
5130
5355
  initial: initial2,
5131
5356
  initialSaved = null,
@@ -5164,6 +5389,7 @@ function App({
5164
5389
  const reviewScrollRef = useRef5(null);
5165
5390
  const sizeScrollRef = useRef5(null);
5166
5391
  const commentScrollRef = useRef5(null);
5392
+ const mergedScrollRef = useRef5(null);
5167
5393
  const { raw, isSnapshot, loading, load, error, stale, reload } = useLoader(options, noCache2, (data) => {
5168
5394
  dispatchBrowse({
5169
5395
  type: "dataLoaded",
@@ -5172,7 +5398,8 @@ function App({
5172
5398
  open: buildOpenRepoOptions(data),
5173
5399
  review: buildReviewRepoOptions(data),
5174
5400
  size: buildSizeRepoOptions(data),
5175
- comment: buildCommentRepoOptions(data)
5401
+ comment: buildCommentRepoOptions(data),
5402
+ merged: buildMergedRepoOptions(data)
5176
5403
  }
5177
5404
  });
5178
5405
  });
@@ -5235,13 +5462,13 @@ function App({
5235
5462
  dispatchUi({ type: "editStarted" });
5236
5463
  },
5237
5464
  scrollBy: (forTab, delta) => {
5238
- const ref = forTab === 2 ? reviewScrollRef : forTab === 3 ? sizeScrollRef : commentScrollRef;
5465
+ const ref = forTab === 1 ? mergedScrollRef : forTab === 2 ? reviewScrollRef : forTab === 3 ? sizeScrollRef : commentScrollRef;
5239
5466
  ref.current?.scrollBy(delta);
5240
5467
  }
5241
5468
  });
5242
5469
  });
5243
5470
  const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
5244
- return /* @__PURE__ */ jsxs11("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
5471
+ return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
5245
5472
  /* @__PURE__ */ jsx14(Header, { options, raw, error, spinning: showLoad }),
5246
5473
  /* @__PURE__ */ jsx14(TabBar, { tab: browse.tab }),
5247
5474
  /* @__PURE__ */ jsx14(
@@ -5251,7 +5478,12 @@ function App({
5251
5478
  browse,
5252
5479
  warning: capWarning,
5253
5480
  focused: ui.modal === null,
5254
- scrollRefs: { review: reviewScrollRef, size: sizeScrollRef, comment: commentScrollRef },
5481
+ scrollRefs: {
5482
+ review: reviewScrollRef,
5483
+ size: sizeScrollRef,
5484
+ comment: commentScrollRef,
5485
+ merged: mergedScrollRef
5486
+ },
5255
5487
  error,
5256
5488
  loading,
5257
5489
  load,
@@ -5265,6 +5497,7 @@ function App({
5265
5497
  modal: ui.modal,
5266
5498
  editing: ui.editing,
5267
5499
  tab: browse.tab,
5500
+ authoredTab: browse.authoredTab,
5268
5501
  views,
5269
5502
  copyLinks: copyLinks2,
5270
5503
  openError: ui.openError,
@@ -5313,7 +5546,7 @@ function App({
5313
5546
  ] });
5314
5547
  }
5315
5548
 
5316
- // tui/bootstrap.ts
5549
+ // src/tui/bootstrap.ts
5317
5550
  function bootstrap() {
5318
5551
  const { values, explicit } = parseCliArgs();
5319
5552
  if (values.help) {
@@ -5352,7 +5585,7 @@ function bootstrap() {
5352
5585
  }
5353
5586
  }
5354
5587
 
5355
- // tui/main.tsx
5588
+ // src/tui/main.tsx
5356
5589
  import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
5357
5590
  var { initial, saved, noCache, copyLinks, theme: theme2 } = bootstrap();
5358
5591
  var renderer = await createCliRenderer({ exitOnCtrlC: true });