@d3lm/pr-stats 0.2.0 → 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 +8 -6
  2. package/dist/tui-app.mjs +673 -276
  3. package/package.json +29 -21
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
- import { useReducer, useRef as useRef5, useState as useState4 } from "react";
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
  }
@@ -307,6 +307,9 @@ function loadSettings() {
307
307
  if (settings.noCache !== void 0 && typeof settings.noCache !== "boolean") {
308
308
  throw new CliError(`"noCache" in ${settingsFile()} must be true or false`);
309
309
  }
310
+ if (settings.copyLinks !== void 0 && typeof settings.copyLinks !== "boolean") {
311
+ throw new CliError(`"copyLinks" in ${settingsFile()} must be true or false`);
312
+ }
310
313
  current = settings;
311
314
  return current;
312
315
  }
@@ -319,6 +322,15 @@ function saveNoCache(on) {
319
322
  `);
320
323
  return true;
321
324
  }
325
+ function saveCopyLinks(on) {
326
+ current = { ...current, copyLinks: on };
327
+ if (!cacheEnabled()) {
328
+ return false;
329
+ }
330
+ writeFileAtomic(settingsFile(), `${JSON.stringify(current, null, 2)}
331
+ `);
332
+ return true;
333
+ }
322
334
  function saveTheme(value2) {
323
335
  current = { ...current };
324
336
  if (value2 === void 0) {
@@ -349,7 +361,7 @@ function applySettings(values, explicit) {
349
361
  return settings;
350
362
  }
351
363
 
352
- // tui/theme.ts
364
+ // src/tui/theme.ts
353
365
  var BASE = {
354
366
  bg: "#1e1e1e",
355
367
  border: "#363636",
@@ -362,6 +374,7 @@ var BASE = {
362
374
  inputFocusedBg: "#303030",
363
375
  warn: "#f0b689",
364
376
  error: "#ff8080",
377
+ success: "#89f0ab",
365
378
  chartBar: "#b98d63",
366
379
  chartLine: "#c99a6f",
367
380
  chartDim: "#6e563f",
@@ -534,17 +547,24 @@ function themeColorText(key) {
534
547
  return Array.isArray(value2) ? value2.join(" ") : value2;
535
548
  }
536
549
 
537
- // tui/components/Footer.tsx
550
+ // src/tui/components/Footer.tsx
538
551
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
539
552
  function Footer({
540
553
  width,
541
554
  modal,
542
555
  editing,
543
556
  tab,
557
+ authoredTab,
544
558
  views,
559
+ copyLinks: copyLinks2,
545
560
  openError,
561
+ copyNotice,
546
562
  stale
547
563
  }) {
564
+ const notice = openError ?? copyNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
565
+ const check = openError === null && copyNotice !== null;
566
+ const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
567
+ const hints = truncated(hintsFor(modal, editing, tab, authoredTab, views, copyLinks2), width - 2 - noticeWidth);
548
568
  return /* @__PURE__ */ jsxs(Fragment, { children: [
549
569
  /* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
550
570
  /* @__PURE__ */ jsxs(
@@ -557,14 +577,23 @@ function Footer({
557
577
  paddingRight: 1,
558
578
  justifyContent: "space-between",
559
579
  children: [
560
- /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hintsFor(modal, editing, tab, views) }),
561
- /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: openError !== null ? theme.error : theme.warn, children: openError ?? (stale ? "options changed \xB7 press r to reload" : "") })
580
+ /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hints }),
581
+ /* @__PURE__ */ jsxs("text", { wrapMode: "none", children: [
582
+ check && /* @__PURE__ */ jsx("span", { fg: theme.success, children: "\u2714 " }),
583
+ /* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : copyNotice !== null ? theme.muted : theme.warn, children: notice })
584
+ ] })
562
585
  ]
563
586
  }
564
587
  )
565
588
  ] });
566
589
  }
567
- function hintsFor(modal, editing, tab, views) {
590
+ function truncated(text, limit2) {
591
+ if (text.length <= limit2) {
592
+ return text;
593
+ }
594
+ return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
595
+ }
596
+ function hintsFor(modal, editing, tab, authoredTab, views, copyLinks2) {
568
597
  if (modal === "options") {
569
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";
570
599
  }
@@ -574,29 +603,31 @@ function hintsFor(modal, editing, tab, views) {
574
603
  if (modal === "theme") {
575
604
  return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit hex \xB7 esc back \xB7 q quit";
576
605
  }
577
- 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") {
578
608
  const scope2 = views === null ? null : tab === 0 ? views.pendingScope : views.openScope;
579
609
  const repos2 = views === null ? [] : tab === 0 ? views.pendingRepos : views.openRepos;
580
610
  if (scope2?.view === "list") {
581
- 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`;
582
612
  }
613
+ const action = copyLinks2 ? "enter copy link" : "enter open";
583
614
  if (scope2 !== null && repos2.length > 0) {
584
- return scope2.repo === null ? "\u2191/\u2193 select \xB7 enter open \xB7 g group by repo \xB7 esc back \xB7 o options \xB7 s settings \xB7 r reload \xB7 q quit" : "\u2191/\u2193 select \xB7 enter open \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`;
585
616
  }
586
- return "\u2191/\u2193 select \xB7 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`;
587
618
  }
588
- const scope = views === null ? null : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
589
- 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;
590
621
  if (scope?.view === "list") {
591
- 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`;
592
623
  }
593
624
  if (scope !== null && repos.length > 0) {
594
- 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`;
595
626
  }
596
- 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`;
597
628
  }
598
629
 
599
- // tui/components/Spinner.tsx
630
+ // src/tui/components/Spinner.tsx
600
631
  import { useEffect, useState } from "react";
601
632
  import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
602
633
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -614,7 +645,7 @@ function Spinner() {
614
645
  return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
615
646
  }
616
647
 
617
- // tui/components/Header.tsx
648
+ // src/tui/components/Header.tsx
618
649
  import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
619
650
  function Header({
620
651
  options,
@@ -648,7 +679,7 @@ function timeModeLabel(options) {
648
679
  return options.workHours === "0-24" ? `Mon-Fri all hours ${tz}` : `Mon-Fri ${options.workHours} ${tz}`;
649
680
  }
650
681
 
651
- // time.ts
682
+ // src/time.ts
652
683
  function wallFormatter(tz) {
653
684
  return new Intl.DateTimeFormat("en-US", {
654
685
  timeZone: tz,
@@ -739,7 +770,7 @@ function durationHours(start, end) {
739
770
  return businessMsBetween(start, end) / 36e5;
740
771
  }
741
772
 
742
- // compute.ts
773
+ // src/compute.ts
743
774
  function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
744
775
  const reviewed = [];
745
776
  const pending = [];
@@ -789,6 +820,23 @@ function computeSizeStats(sizes, { sizeTarget } = {}) {
789
820
  const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
790
821
  return { metrics, timelineTotals, met, misses, targetLabel };
791
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
+ }
792
840
  function computeCommentStats(sizes) {
793
841
  const metrics = [
794
842
  { label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
@@ -801,7 +849,7 @@ function computeCommentStats(sizes) {
801
849
  return { metrics, totals, uncommented, top };
802
850
  }
803
851
 
804
- // report.ts
852
+ // src/report.ts
805
853
  var BUCKETS = [];
806
854
  function initBuckets() {
807
855
  BUCKETS = makeBuckets();
@@ -882,7 +930,7 @@ function formatCount(value2) {
882
930
  return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
883
931
  }
884
932
 
885
- // tui/views/rows.ts
933
+ // src/tui/views/rows.ts
886
934
  function durationLead(result) {
887
935
  return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
888
936
  }
@@ -898,7 +946,7 @@ function toPrRows(entries, leads) {
898
946
  });
899
947
  }
900
948
 
901
- // tui/views/queue.ts
949
+ // src/tui/views/queue.ts
902
950
  function queueRows(view) {
903
951
  return view.lists.flatMap((list) => list.rows);
904
952
  }
@@ -957,10 +1005,10 @@ function rowsOf(group) {
957
1005
  );
958
1006
  }
959
1007
 
960
- // tui/components/ChartsPanel.tsx
1008
+ // src/tui/components/ChartsPanel.tsx
961
1009
  import { useTerminalDimensions } from "@opentui/react";
962
1010
 
963
- // tui/hooks/scrollbar.ts
1011
+ // src/tui/hooks/scrollbar.ts
964
1012
  import { CliRenderEvents } from "@opentui/core";
965
1013
  import { useRenderer } from "@opentui/react";
966
1014
  import { useLayoutEffect } from "react";
@@ -991,12 +1039,13 @@ function useScrollbarSettle(scrollRef, mounted = true) {
991
1039
  }, [scrollRef, renderer2, mounted]);
992
1040
  }
993
1041
 
994
- // tui/views/charts/model.ts
1042
+ // src/tui/views/charts/model.ts
995
1043
  function lineLength(line) {
996
1044
  return line.reduce((sum, span) => sum + span.text.length, 0);
997
1045
  }
998
1046
  function cardWidth(card) {
999
- return Math.max(card.title.length + 2 + card.subtitle.length, ...card.lines.map(lineLength));
1047
+ const subtitleLength = typeof card.subtitle === "string" ? card.subtitle.length : lineLength(card.subtitle);
1048
+ return Math.max(card.title.length + 2 + subtitleLength, ...card.lines.map(lineLength));
1000
1049
  }
1001
1050
  function formatDuration(hours) {
1002
1051
  if (hours < 1) {
@@ -1011,7 +1060,7 @@ function compact(value2) {
1011
1060
  return String(Math.round(value2 * 10) / 10);
1012
1061
  }
1013
1062
 
1014
- // tui/components/ChartsPanel.tsx
1063
+ // src/tui/components/ChartsPanel.tsx
1015
1064
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
1016
1065
  var COLUMN_GAP = 4;
1017
1066
  function ChartsPanel({
@@ -1105,16 +1154,18 @@ function ChartLine({ line }) {
1105
1154
  ) });
1106
1155
  }
1107
1156
  function ChartCard({ card }) {
1157
+ const subtitle = typeof card.subtitle === "string" ? [{ text: card.subtitle, fg: theme.muted }] : card.subtitle;
1108
1158
  return /* @__PURE__ */ jsxs3("box", { flexDirection: "column", children: [
1109
1159
  /* @__PURE__ */ jsxs3("text", { wrapMode: "none", children: [
1110
1160
  /* @__PURE__ */ jsx4("b", { fg: theme.text, children: card.title }),
1111
- /* @__PURE__ */ jsx4("span", { fg: theme.muted, children: ` ${card.subtitle}` })
1161
+ /* @__PURE__ */ jsx4("span", { fg: theme.muted, children: " " }),
1162
+ keyed(subtitle, spanKey).map(({ key, item }) => /* @__PURE__ */ jsx4("span", { fg: item.fg ?? theme.muted, children: item.text }, key))
1112
1163
  ] }),
1113
1164
  /* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(card.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
1114
1165
  ] });
1115
1166
  }
1116
1167
 
1117
- // tui/components/Placeholder.tsx
1168
+ // src/tui/components/Placeholder.tsx
1118
1169
  import { jsx as jsx5, jsxs as jsxs4 } from "@opentui/react/jsx-runtime";
1119
1170
  var BAR_WIDTH = 40;
1120
1171
  var PARTIAL_BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
@@ -1148,7 +1199,7 @@ function loadLabel(load) {
1148
1199
  return load.phase === "search" ? "searching PRs..." : "fetching PR details";
1149
1200
  }
1150
1201
 
1151
- // tui/components/QueuePanel.tsx
1202
+ // src/tui/components/QueuePanel.tsx
1152
1203
  import { useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
1153
1204
  import { useEffect as useEffect2, useRef } from "react";
1154
1205
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "@opentui/react/jsx-runtime";
@@ -1157,10 +1208,12 @@ function QueuePanel({
1157
1208
  warning,
1158
1209
  empty,
1159
1210
  lists,
1160
- cursor
1211
+ cursor,
1212
+ onRefClick
1161
1213
  }) {
1162
1214
  const scrollRef = useRef(null);
1163
1215
  const { width } = useTerminalDimensions2();
1216
+ const lastDown = useRef(null);
1164
1217
  useScrollbarSettle(scrollRef, empty === null);
1165
1218
  useEffect2(() => {
1166
1219
  scrollRef.current?.scrollChildIntoView(`queue-row-${cursor}`);
@@ -1203,18 +1256,41 @@ function QueuePanel({
1203
1256
  const index = offsets[listIndex] + rowIndex;
1204
1257
  const isSelected = index === cursor;
1205
1258
  const bg = isSelected ? theme.selectedBg : void 0;
1206
- return /* @__PURE__ */ jsxs5("text", { id: `queue-row-${index}`, wrapMode: "none", children: [
1207
- /* @__PURE__ */ jsx6("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
1208
- /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
1209
- row.lead,
1210
- " "
1211
- ] }),
1212
- /* @__PURE__ */ jsx6("a", { href: row.url, fg: theme.accent, bg, children: row.ref }),
1213
- /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
1214
- " ",
1215
- row.title
1216
- ] })
1217
- ] }, row.url);
1259
+ const refStart = 2 + row.lead.length + 2;
1260
+ return /* @__PURE__ */ jsxs5(
1261
+ "text",
1262
+ {
1263
+ id: `queue-row-${index}`,
1264
+ wrapMode: "none",
1265
+ onMouseDown: onRefClick === null ? void 0 : (event) => {
1266
+ lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
1267
+ },
1268
+ onMouseUp: onRefClick === null ? void 0 : (event) => {
1269
+ const down = lastDown.current;
1270
+ lastDown.current = null;
1271
+ if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
1272
+ return;
1273
+ }
1274
+ const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
1275
+ if (local >= refStart && local < refStart + row.ref.length) {
1276
+ onRefClick(row);
1277
+ }
1278
+ },
1279
+ children: [
1280
+ /* @__PURE__ */ jsx6("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
1281
+ /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
1282
+ row.lead,
1283
+ " "
1284
+ ] }),
1285
+ 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 }),
1286
+ /* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
1287
+ " ",
1288
+ row.title
1289
+ ] })
1290
+ ]
1291
+ },
1292
+ row.url
1293
+ );
1218
1294
  })
1219
1295
  ] }, list.title))
1220
1296
  ]
@@ -1223,7 +1299,7 @@ function QueuePanel({
1223
1299
  ] });
1224
1300
  }
1225
1301
 
1226
- // tui/components/RepoList.tsx
1302
+ // src/tui/components/RepoList.tsx
1227
1303
  import { useEffect as useEffect3, useRef as useRef2 } from "react";
1228
1304
  import { jsx as jsx7, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
1229
1305
  function RepoList({
@@ -1263,8 +1339,123 @@ function RepoList({
1263
1339
  );
1264
1340
  }
1265
1341
 
1266
- // tui/components/MainPanel.tsx
1267
- 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";
1268
1459
  function QueueTab({
1269
1460
  prompt,
1270
1461
  repos,
@@ -1273,22 +1464,24 @@ function QueueTab({
1273
1464
  repoCursor,
1274
1465
  rowCursor,
1275
1466
  grouped,
1276
- warning
1467
+ warning,
1468
+ onRefClick
1277
1469
  }) {
1278
1470
  if (scope.view === "list") {
1279
- 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) });
1280
1472
  }
1281
1473
  if (view === null) {
1282
1474
  return null;
1283
1475
  }
1284
- return /* @__PURE__ */ jsx8(
1476
+ return /* @__PURE__ */ jsx9(
1285
1477
  QueuePanel,
1286
1478
  {
1287
1479
  heading: repos.length > 0 ? scope.repo ?? (grouped ? "All repos \xB7 grouped by repo" : "All repos") : null,
1288
1480
  warning,
1289
1481
  empty: view.empty,
1290
1482
  lists: view.lists,
1291
- cursor: Math.min(rowCursor, queueRows(view).length - 1)
1483
+ cursor: Math.min(rowCursor, queueRows(view).length - 1),
1484
+ onRefClick
1292
1485
  }
1293
1486
  );
1294
1487
  }
@@ -1302,12 +1495,12 @@ function StatsTab({
1302
1495
  warning
1303
1496
  }) {
1304
1497
  if (scope.view === "list") {
1305
- 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) });
1306
1499
  }
1307
1500
  if (view === null) {
1308
1501
  return null;
1309
1502
  }
1310
- return /* @__PURE__ */ jsx8(
1503
+ return /* @__PURE__ */ jsx9(
1311
1504
  ChartsPanel,
1312
1505
  {
1313
1506
  scrollRef,
@@ -1326,9 +1519,10 @@ function MainPanel({
1326
1519
  scrollRefs,
1327
1520
  error,
1328
1521
  loading,
1329
- load
1522
+ load,
1523
+ onRefClick
1330
1524
  }) {
1331
- 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(
1332
1526
  QueueTab,
1333
1527
  {
1334
1528
  prompt: "Select a repository and press enter to open its review queue.",
@@ -1338,21 +1532,37 @@ function MainPanel({
1338
1532
  repoCursor: browse.repoCursors.pending,
1339
1533
  rowCursor: browse.rowCursors.pending,
1340
1534
  grouped: browse.grouped.pending,
1341
- warning
1342
- }
1343
- ) : browse.tab === 1 ? /* @__PURE__ */ jsx8(
1344
- QueueTab,
1345
- {
1346
- prompt: "Select a repository and press enter to list its open PRs.",
1347
- repos: views.openRepos,
1348
- scope: views.openScope,
1349
- view: views.open,
1350
- repoCursor: browse.repoCursors.open,
1351
- rowCursor: browse.rowCursors.open,
1352
- grouped: browse.grouped.open,
1353
- warning
1535
+ warning,
1536
+ onRefClick
1354
1537
  }
1355
- ) : 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(
1356
1566
  StatsTab,
1357
1567
  {
1358
1568
  repos: views.reviewRepos,
@@ -1363,7 +1573,7 @@ function MainPanel({
1363
1573
  focused,
1364
1574
  warning
1365
1575
  }
1366
- ) : browse.tab === 3 ? /* @__PURE__ */ jsx8(
1576
+ ) : browse.tab === 3 ? /* @__PURE__ */ jsx9(
1367
1577
  StatsTab,
1368
1578
  {
1369
1579
  repos: views.sizeRepos,
@@ -1374,7 +1584,7 @@ function MainPanel({
1374
1584
  focused,
1375
1585
  warning
1376
1586
  }
1377
- ) : /* @__PURE__ */ jsx8(
1587
+ ) : /* @__PURE__ */ jsx9(
1378
1588
  StatsTab,
1379
1589
  {
1380
1590
  repos: views.commentRepos,
@@ -1388,7 +1598,7 @@ function MainPanel({
1388
1598
  ) });
1389
1599
  }
1390
1600
 
1391
- // ../../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
1392
1602
  function stringReplaceAll(string, substring, postfix) {
1393
1603
  let index = string.indexOf(substring);
1394
1604
  if (index === -1) {
@@ -1418,7 +1628,7 @@ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
1418
1628
  return returnValue;
1419
1629
  }
1420
1630
 
1421
- // ../../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
1422
1632
  var ANSI_BACKGROUND_OFFSET = 10;
1423
1633
  var ANSI_UNDERLINE_OFFSET = 20;
1424
1634
  var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
@@ -1640,7 +1850,7 @@ function assembleStyles() {
1640
1850
  var ansiStyles = assembleStyles();
1641
1851
  var ansi_styles_default = ansiStyles;
1642
1852
 
1643
- // ../../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
1644
1854
  import process2 from "node:process";
1645
1855
  import os from "node:os";
1646
1856
  import tty from "node:tty";
@@ -1782,7 +1992,7 @@ var supportsColor = {
1782
1992
  };
1783
1993
  var supports_color_default = supportsColor;
1784
1994
 
1785
- // ../../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
1786
1996
  var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
1787
1997
  var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
1788
1998
  var STYLER = /* @__PURE__ */ Symbol("STYLER");
@@ -1950,7 +2160,7 @@ var chalk = createChalk();
1950
2160
  var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
1951
2161
  var source_default = chalk;
1952
2162
 
1953
- // flags.ts
2163
+ // src/flags.ts
1954
2164
  import { parseArgs } from "node:util";
1955
2165
  var flag = source_default.green;
1956
2166
  var value = source_default.cyan;
@@ -2211,7 +2421,7 @@ function resolveTimezone(input) {
2211
2421
  return tz;
2212
2422
  }
2213
2423
 
2214
- // tui/state/options.ts
2424
+ // src/tui/state/options.ts
2215
2425
  var FIELDS = [
2216
2426
  {
2217
2427
  key: "since",
@@ -2383,14 +2593,14 @@ function targetLabelOf(target) {
2383
2593
  return target + (/^[\d.]+$/.test(target) ? "h" : "");
2384
2594
  }
2385
2595
 
2386
- // tui/components/ModalFrame.tsx
2387
- 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";
2388
2598
  var MODAL_WIDTH = 64;
2389
2599
  function ModalFrame({ title, children }) {
2390
2600
  const tabWidth = title.length + 4;
2391
2601
  const tabTopRow = `\u250C${"\u2500".repeat(tabWidth - 2)}\u2510`;
2392
2602
  const tabTitleSuffix = ` \u2514${"\u2500".repeat(MODAL_WIDTH - tabWidth - 1)}\u2510`;
2393
- return /* @__PURE__ */ jsx9(
2603
+ return /* @__PURE__ */ jsx10(
2394
2604
  "box",
2395
2605
  {
2396
2606
  position: "absolute",
@@ -2401,14 +2611,14 @@ function ModalFrame({ title, children }) {
2401
2611
  alignItems: "center",
2402
2612
  justifyContent: "center",
2403
2613
  zIndex: 10,
2404
- children: /* @__PURE__ */ jsxs7("box", { flexDirection: "column", width: MODAL_WIDTH, children: [
2405
- /* @__PURE__ */ jsx9("text", { alignSelf: "flex-start", wrapMode: "none", fg: theme.border, bg: theme.bg, children: tabTopRow }),
2406
- /* @__PURE__ */ jsxs7("text", { wrapMode: "none", bg: theme.bg, children: [
2407
- /* @__PURE__ */ jsx9("span", { fg: theme.border, children: "\u2502 " }),
2408
- /* @__PURE__ */ jsx9("span", { fg: theme.muted, children: title }),
2409
- /* @__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 })
2410
2620
  ] }),
2411
- /* @__PURE__ */ jsx9(
2621
+ /* @__PURE__ */ jsx10(
2412
2622
  "box",
2413
2623
  {
2414
2624
  border: ["left", "right", "bottom"],
@@ -2425,7 +2635,7 @@ function ModalFrame({ title, children }) {
2425
2635
  );
2426
2636
  }
2427
2637
  function ModalRow({ label, isSelected, children }) {
2428
- return /* @__PURE__ */ jsxs7(
2638
+ return /* @__PURE__ */ jsxs9(
2429
2639
  "box",
2430
2640
  {
2431
2641
  flexDirection: "row",
@@ -2434,17 +2644,17 @@ function ModalRow({ label, isSelected, children }) {
2434
2644
  paddingRight: 2,
2435
2645
  backgroundColor: isSelected ? theme.selectedBg : void 0,
2436
2646
  children: [
2437
- isSelected ? /* @__PURE__ */ jsx9("text", { position: "absolute", left: -1, wrapMode: "none", fg: theme.accent, bg: theme.bg, children: "\u2503" }) : null,
2438
- /* @__PURE__ */ jsx9("text", { wrapMode: "none", fg: theme.text, children: label }),
2439
- /* @__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 }),
2440
2650
  children
2441
2651
  ]
2442
2652
  }
2443
2653
  );
2444
2654
  }
2445
2655
 
2446
- // tui/components/OptionsModal.tsx
2447
- 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";
2448
2658
  var EMPTY_PLACEHOLDERS = {
2449
2659
  repos: "(all accessible)",
2450
2660
  user: "(authenticated user)",
@@ -2466,12 +2676,12 @@ function OptionsModal({
2466
2676
  onSubmit
2467
2677
  }) {
2468
2678
  const savedState = savedLine(options, saved2);
2469
- return /* @__PURE__ */ jsxs8(ModalFrame, { title: "Options", children: [
2470
- SECTIONS.map((section) => /* @__PURE__ */ jsxs8("box", { flexDirection: "column", marginBottom: 1, children: [
2471
- /* @__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 }),
2472
2682
  section.fields.map((field) => {
2473
2683
  const index = FIELDS.indexOf(field);
2474
- return /* @__PURE__ */ jsx10(
2684
+ return /* @__PURE__ */ jsx11(
2475
2685
  FieldRow,
2476
2686
  {
2477
2687
  field,
@@ -2485,8 +2695,8 @@ function OptionsModal({
2485
2695
  );
2486
2696
  })
2487
2697
  ] }, section.title)),
2488
- /* @__PURE__ */ jsx10("text", { wrapMode: "none", fg: savedState.color, marginLeft: 2, marginRight: 2, marginBottom: 1, children: savedState.text }),
2489
- /* @__PURE__ */ jsx10(
2698
+ /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: savedState.color, marginLeft: 2, marginRight: 2, marginBottom: 1, children: savedState.text }),
2699
+ /* @__PURE__ */ jsx11(
2490
2700
  "text",
2491
2701
  {
2492
2702
  wrapMode: "word",
@@ -2527,7 +2737,7 @@ function FieldRow({
2527
2737
  }) {
2528
2738
  const value2 = displayValue(field.key, options[field.key]);
2529
2739
  const valueColor = value2.isPlaceholder ? isSelected ? theme.muted : theme.dim : theme.muted;
2530
- 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(
2531
2741
  "input",
2532
2742
  {
2533
2743
  width: 32,
@@ -2544,17 +2754,17 @@ function FieldRow({
2544
2754
  textColor: theme.text,
2545
2755
  cursorColor: theme.accent
2546
2756
  }
2547
- ) : isSelected && field.kind === "toggle" ? /* @__PURE__ */ jsxs8("text", { wrapMode: "none", children: [
2548
- /* @__PURE__ */ jsx10("span", { fg: theme.muted, children: "\u2039 " }),
2549
- /* @__PURE__ */ jsx10("b", { fg: theme.text, children: value2.text }),
2550
- /* @__PURE__ */ jsx10("span", { fg: theme.muted, children: " \u203A" })
2551
- ] }) : 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 }) });
2552
2762
  }
2553
2763
 
2554
- // tui/components/SettingsModal.tsx
2764
+ // src/tui/components/SettingsModal.tsx
2555
2765
  import { homedir as homedir2 } from "node:os";
2556
2766
 
2557
- // tui/state/settings.ts
2767
+ // src/tui/state/settings.ts
2558
2768
  var SETTINGS = [
2559
2769
  {
2560
2770
  key: "noCache",
@@ -2568,6 +2778,12 @@ var SETTINGS = [
2568
2778
  label: "Clear cache",
2569
2779
  hint: "deletes the cached PR data at this path, so the next reload refetches everything"
2570
2780
  },
2781
+ {
2782
+ key: "copyLinks",
2783
+ section: "Links",
2784
+ label: "Copy instead of open",
2785
+ hint: "enter and a click on a PR reference copy its link to the clipboard instead of opening the browser"
2786
+ },
2571
2787
  {
2572
2788
  key: "themePreset",
2573
2789
  section: "Theme",
@@ -2599,6 +2815,7 @@ var THEME_COLORS = [
2599
2815
  { key: "inputFocusedBg", hint: "background of the focused text input" },
2600
2816
  { key: "warn", hint: "notices like the reload reminder and confirm prompts" },
2601
2817
  { key: "error", hint: "error messages and failed loads" },
2818
+ { key: "success", hint: "the checkmark on the copied-link notice" },
2602
2819
  { key: "chartBar", hint: "histogram and volume bars" },
2603
2820
  { key: "chartLine", hint: "trend lines and the scatter dots" },
2604
2821
  { key: "chartDim", hint: "de-emphasized chart parts like the over-target share" },
@@ -2615,8 +2832,8 @@ var CACHE_MESSAGES = {
2615
2832
  resetDisabled: { text: "the cache is disabled for this session \xB7 nothing to reset" }
2616
2833
  };
2617
2834
 
2618
- // tui/components/SettingsModal.tsx
2619
- 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";
2620
2837
  var SECTIONS2 = [];
2621
2838
  for (const setting of SETTINGS) {
2622
2839
  const last = SECTIONS2.at(-1);
@@ -2630,27 +2847,29 @@ function SettingsModal({
2630
2847
  selected,
2631
2848
  cacheAction,
2632
2849
  noCache: noCache2,
2850
+ copyLinks: copyLinks2,
2633
2851
  preset
2634
2852
  }) {
2635
2853
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
2636
- return /* @__PURE__ */ jsxs9(ModalFrame, { title: "Settings", children: [
2637
- SECTIONS2.map((section) => /* @__PURE__ */ jsxs9("box", { flexDirection: "column", marginBottom: 1, children: [
2638
- /* @__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 }),
2639
2857
  section.settings.map((setting) => {
2640
2858
  const isSelected = SETTINGS.indexOf(setting) === selected;
2641
- return /* @__PURE__ */ jsx11(ModalRow, { label: setting.label, isSelected, children: /* @__PURE__ */ jsx11(
2859
+ return /* @__PURE__ */ jsx12(ModalRow, { label: setting.label, isSelected, children: /* @__PURE__ */ jsx12(
2642
2860
  SettingValue,
2643
2861
  {
2644
2862
  setting,
2645
2863
  isSelected,
2646
2864
  cacheAction,
2647
2865
  noCache: noCache2,
2866
+ copyLinks: copyLinks2,
2648
2867
  preset
2649
2868
  }
2650
2869
  ) }, setting.key);
2651
2870
  })
2652
2871
  ] }, section.title)),
2653
- /* @__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 })
2654
2873
  ] });
2655
2874
  }
2656
2875
  function SettingValue({
@@ -2658,23 +2877,27 @@ function SettingValue({
2658
2877
  isSelected,
2659
2878
  cacheAction,
2660
2879
  noCache: noCache2,
2880
+ copyLinks: copyLinks2,
2661
2881
  preset
2662
2882
  }) {
2663
2883
  switch (setting.key) {
2664
2884
  case "noCache": {
2665
- return /* @__PURE__ */ jsx11(ToggleValue, { value: noCache2 ? "yes" : "no", isSelected });
2885
+ return /* @__PURE__ */ jsx12(ToggleValue, { value: noCache2 ? "yes" : "no", isSelected });
2666
2886
  }
2667
2887
  case "clearCache": {
2668
- return /* @__PURE__ */ jsx11(PathValue, { path: cacheDir(), confirming: cacheAction === "confirm", isSelected });
2888
+ return /* @__PURE__ */ jsx12(PathValue, { path: cacheDir(), confirming: cacheAction === "confirm", isSelected });
2889
+ }
2890
+ case "copyLinks": {
2891
+ return /* @__PURE__ */ jsx12(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
2669
2892
  }
2670
2893
  case "themePreset": {
2671
- return /* @__PURE__ */ jsx11(ToggleValue, { value: preset, isSelected });
2894
+ return /* @__PURE__ */ jsx12(ToggleValue, { value: preset, isSelected });
2672
2895
  }
2673
2896
  case "themeColors": {
2674
- 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)) });
2675
2898
  }
2676
2899
  case "resetSettings": {
2677
- return /* @__PURE__ */ jsx11(PathValue, { path: settingsFile(), confirming: cacheAction === "resetConfirm", isSelected });
2900
+ return /* @__PURE__ */ jsx12(PathValue, { path: settingsFile(), confirming: cacheAction === "resetConfirm", isSelected });
2678
2901
  }
2679
2902
  default: {
2680
2903
  return null;
@@ -2683,113 +2906,23 @@ function SettingValue({
2683
2906
  }
2684
2907
  function ToggleValue({ value: value2, isSelected }) {
2685
2908
  if (isSelected) {
2686
- return /* @__PURE__ */ jsxs9("text", { wrapMode: "none", children: [
2687
- /* @__PURE__ */ jsx11("span", { fg: theme.muted, children: "\u2039 " }),
2688
- /* @__PURE__ */ jsx11("b", { fg: theme.text, children: value2 }),
2689
- /* @__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" })
2690
2913
  ] });
2691
2914
  }
2692
- return /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: theme.muted, children: value2 });
2915
+ return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.muted, children: value2 });
2693
2916
  }
2694
2917
  function PathValue({ path, confirming, isSelected }) {
2695
2918
  if (confirming) {
2696
- return /* @__PURE__ */ jsx11("text", { wrapMode: "none", children: /* @__PURE__ */ jsx11("b", { fg: theme.warn, children: "enter to confirm" }) });
2697
- }
2698
- return /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
2699
- }
2700
-
2701
- // tui/state/browse.ts
2702
- var TABS = ["1 Awaiting you", "2 Your open PRs", "3 Review time", "4 PR size", "5 Comments"];
2703
- var initialBrowseState = {
2704
- tab: 0,
2705
- scopes: {
2706
- pending: { view: "list" },
2707
- open: { view: "list" },
2708
- review: { view: "list" },
2709
- size: { view: "list" },
2710
- comment: { view: "list" }
2711
- },
2712
- repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0 },
2713
- rowCursors: { pending: 0, open: 0 },
2714
- grouped: { pending: false, open: false }
2715
- };
2716
- function dropVanishedRepo(scope, repos) {
2717
- if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
2718
- return { view: "list" };
2719
- }
2720
- return scope;
2721
- }
2722
- function cycled(previous, delta, count2) {
2723
- return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
2724
- }
2725
- function browseReducer(state, action) {
2726
- switch (action.type) {
2727
- case "tabSelected": {
2728
- return { ...state, tab: action.tab };
2729
- }
2730
- case "tabCycled": {
2731
- return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
2732
- }
2733
- case "repoCursorMoved": {
2734
- return {
2735
- ...state,
2736
- repoCursors: {
2737
- ...state.repoCursors,
2738
- [action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
2739
- }
2740
- };
2741
- }
2742
- case "rowCursorMoved": {
2743
- return {
2744
- ...state,
2745
- rowCursors: {
2746
- ...state.rowCursors,
2747
- [action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
2748
- }
2749
- };
2750
- }
2751
- case "repoOpened": {
2752
- return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
2753
- }
2754
- case "pickerReturned": {
2755
- return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
2756
- }
2757
- case "groupingToggled": {
2758
- return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
2759
- }
2760
- case "dataLoaded": {
2761
- return {
2762
- ...state,
2763
- scopes: {
2764
- pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
2765
- open: dropVanishedRepo(state.scopes.open, action.repos.open),
2766
- review: dropVanishedRepo(state.scopes.review, action.repos.review),
2767
- size: dropVanishedRepo(state.scopes.size, action.repos.size),
2768
- comment: dropVanishedRepo(state.scopes.comment, action.repos.comment)
2769
- }
2770
- };
2771
- }
2919
+ return /* @__PURE__ */ jsx12("text", { wrapMode: "none", children: /* @__PURE__ */ jsx12("b", { fg: theme.warn, children: "enter to confirm" }) });
2772
2920
  }
2773
- return state;
2921
+ return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
2774
2922
  }
2775
2923
 
2776
- // tui/components/TabBar.tsx
2777
- import { jsx as jsx12 } from "@opentui/react/jsx-runtime";
2778
- function TabBar({ tab }) {
2779
- return /* @__PURE__ */ jsx12("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginTop: 1, columnGap: 1, children: TABS.map((label, i) => /* @__PURE__ */ jsx12(
2780
- "text",
2781
- {
2782
- wrapMode: "none",
2783
- fg: i === tab ? theme.accent : theme.muted,
2784
- bg: i === tab ? theme.selectedBg : void 0,
2785
- children: ` ${label} `
2786
- },
2787
- label
2788
- )) });
2789
- }
2790
-
2791
- // tui/components/ThemeModal.tsx
2792
- 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";
2793
2926
  function ThemeModal({
2794
2927
  selected,
2795
2928
  editing,
@@ -2802,7 +2935,7 @@ function ThemeModal({
2802
2935
  const spec = THEME_COLORS[selected];
2803
2936
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
2804
2937
  const hint = spec.key in overrides ? `${spec.hint} \xB7 custom color, an empty value restores the theme` : spec.hint;
2805
- return /* @__PURE__ */ jsxs10(ModalFrame, { title: "Theme colors", children: [
2938
+ return /* @__PURE__ */ jsxs12(ModalFrame, { title: "Theme colors", children: [
2806
2939
  /* @__PURE__ */ jsx13("box", { flexDirection: "column", marginBottom: 1, children: THEME_COLORS.map((color, index) => /* @__PURE__ */ jsx13(
2807
2940
  ColorRow,
2808
2941
  {
@@ -2845,8 +2978,8 @@ function ColorRow({
2845
2978
  textColor: theme.text,
2846
2979
  cursorColor: theme.accent
2847
2980
  }
2848
- ) : /* @__PURE__ */ jsxs10("text", { wrapMode: "none", children: [
2849
- Array.isArray(swatch) ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
2981
+ ) : /* @__PURE__ */ jsxs12("text", { wrapMode: "none", children: [
2982
+ Array.isArray(swatch) ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
2850
2983
  /* @__PURE__ */ jsx13("span", { fg: swatch[0], children: "\u2588" }),
2851
2984
  /* @__PURE__ */ jsx13("span", { fg: swatch[1], children: "\u2588" }),
2852
2985
  /* @__PURE__ */ jsx13("span", { fg: swatch[2], children: "\u2588" }),
@@ -2857,7 +2990,7 @@ function ColorRow({
2857
2990
  ] }) });
2858
2991
  }
2859
2992
 
2860
- // tui/hooks/useDeferredLoading.ts
2993
+ // src/tui/hooks/useDeferredLoading.ts
2861
2994
  import { useEffect as useEffect4, useRef as useRef3, useState as useState2 } from "react";
2862
2995
  function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } = {}) {
2863
2996
  const [visible, setVisible] = useState2(isLoading && showDelay === 0);
@@ -2885,10 +3018,10 @@ function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } =
2885
3018
  return visible;
2886
3019
  }
2887
3020
 
2888
- // tui/hooks/useLoader.ts
3021
+ // src/tui/hooks/useLoader.ts
2889
3022
  import { useEffect as useEffect5, useRef as useRef4, useState as useState3 } from "react";
2890
3023
 
2891
- // github.ts
3024
+ // src/github.ts
2892
3025
  import { execFile } from "node:child_process";
2893
3026
  import { createHash } from "node:crypto";
2894
3027
  import { statSync } from "node:fs";
@@ -3132,6 +3265,8 @@ async function fetchPrSizes(prs) {
3132
3265
  additions
3133
3266
  deletions
3134
3267
  changedFiles
3268
+ mergedAt
3269
+ closedAt
3135
3270
  comments {
3136
3271
  totalCount
3137
3272
  }
@@ -3150,7 +3285,7 @@ async function fetchPrSizes(prs) {
3150
3285
  return prs.map((pr, i) => data[`pr${i}`]?.pullRequest ?? null);
3151
3286
  }
3152
3287
 
3153
- // data.ts
3288
+ // src/data.ts
3154
3289
  var BATCH_SIZE = 25;
3155
3290
  var MAX_CONCURRENT_BATCHES = 4;
3156
3291
  function createLimiter(maxConcurrent) {
@@ -3332,6 +3467,8 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
3332
3467
  additions: details.additions,
3333
3468
  deletions: details.deletions,
3334
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),
3335
3472
  comments: { discussion, review, total: discussion + review }
3336
3473
  });
3337
3474
  }
@@ -3339,7 +3476,7 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
3339
3476
  return { sizes, cacheHits };
3340
3477
  }
3341
3478
 
3342
- // tui/data/load.ts
3479
+ // src/tui/data/load.ts
3343
3480
  function reviveRawData(data) {
3344
3481
  return {
3345
3482
  ...data,
@@ -3355,7 +3492,12 @@ function reviveRawData(data) {
3355
3492
  return { ...result, pr };
3356
3493
  }),
3357
3494
  sizes: data.sizes.map((entry) => {
3358
- 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
+ };
3359
3501
  })
3360
3502
  };
3361
3503
  }
@@ -3455,7 +3597,7 @@ async function loadData(options, onPhase, { bypassCache = false } = {}) {
3455
3597
  return data;
3456
3598
  }
3457
3599
 
3458
- // tui/hooks/useLoader.ts
3600
+ // src/tui/hooks/useLoader.ts
3459
3601
  function useLoader(options, noCache2, onLoaded) {
3460
3602
  const [startupSnapshot] = useState3(() => noCache2 ? null : loadSnapshot(options));
3461
3603
  const [raw, setRaw] = useState3(startupSnapshot);
@@ -3526,10 +3668,10 @@ function useLoader(options, noCache2, onLoaded) {
3526
3668
  };
3527
3669
  }
3528
3670
 
3529
- // tui/hooks/useViewModel.ts
3671
+ // src/tui/hooks/useViewModel.ts
3530
3672
  import { useMemo } from "react";
3531
3673
 
3532
- // tui/views/repos.ts
3674
+ // src/tui/views/repos.ts
3533
3675
  function reviewDetail(counts) {
3534
3676
  return `${counts.reviewed} reviewed` + (counts.pending > 0 ? `, ${counts.pending} pending` : "");
3535
3677
  }
@@ -3623,6 +3765,38 @@ function buildOpenRepoOptions(raw) {
3623
3765
  })
3624
3766
  ];
3625
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
+ }
3626
3800
  function commentDetail(comments, prs) {
3627
3801
  return `${comments} ${comments === 1 ? "comment" : "comments"} on ${prs} ${prs === 1 ? "PR" : "PRs"}`;
3628
3802
  }
@@ -3653,7 +3827,7 @@ function buildCommentRepoOptions(raw) {
3653
3827
  ];
3654
3828
  }
3655
3829
 
3656
- // tui/views/charts/draw.ts
3830
+ // src/tui/views/charts/draw.ts
3657
3831
  function blankCells(width) {
3658
3832
  return Array.from({ length: width }, () => {
3659
3833
  return { ch: " " };
@@ -3706,7 +3880,7 @@ function hbar(fraction, width, color) {
3706
3880
  return line;
3707
3881
  }
3708
3882
 
3709
- // tui/views/charts/distribution.ts
3883
+ // src/tui/views/charts/distribution.ts
3710
3884
  var DURATION_TICKS = [5 / 60, 0.25, 0.5, 1, 2, 4, 8, 24, 48, 96, 168, 336, 720, 2160];
3711
3885
  var COUNT_TICKS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4, 25e3, 5e4, 1e5];
3712
3886
  function buildDistribution({ values, width, format, ticks, flat }) {
@@ -3785,7 +3959,7 @@ function buildDistribution({ values, width, format, ticks, flat }) {
3785
3959
  };
3786
3960
  }
3787
3961
 
3788
- // tui/views/charts/gauge.ts
3962
+ // src/tui/views/charts/gauge.ts
3789
3963
  var GAUGE_BAR_WIDTH = 24;
3790
3964
  function buildGaugeCard({ title, subtitle, rows }) {
3791
3965
  const total = rows.reduce((sum, row) => sum + row.count, 0);
@@ -3808,7 +3982,7 @@ function buildGaugeCard({ title, subtitle, rows }) {
3808
3982
  return { title, subtitle, lines };
3809
3983
  }
3810
3984
 
3811
- // tui/views/charts/heatmap.ts
3985
+ // src/tui/views/charts/heatmap.ts
3812
3986
  var WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
3813
3987
  function heatColor(count2) {
3814
3988
  if (count2 === 1) {
@@ -3874,7 +4048,7 @@ function buildHeatmapCard({ title, subtitle, grid, columns, legend }) {
3874
4048
  return { title, subtitle, lines };
3875
4049
  }
3876
4050
 
3877
- // tui/views/charts/histogram.ts
4051
+ // src/tui/views/charts/histogram.ts
3878
4052
  var HIST_BAR_WIDTH = 36;
3879
4053
  function axisStep(maxCount) {
3880
4054
  for (const step of [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1e3, 2e3, 5e3]) {
@@ -3926,7 +4100,7 @@ function buildHistogramCard({ title, subtitle, values, buckets, format }) {
3926
4100
  return { title, subtitle, lines };
3927
4101
  }
3928
4102
 
3929
- // tui/views/charts/scatter.ts
4103
+ // src/tui/views/charts/scatter.ts
3930
4104
  var SCATTER_WIDTH = 36;
3931
4105
  var SCATTER_HEIGHT = 8;
3932
4106
  var DOT_BITS = [
@@ -3976,7 +4150,7 @@ function buildScatterCard({ title, subtitle, points, formatX, formatY }) {
3976
4150
  return { title, subtitle, lines };
3977
4151
  }
3978
4152
 
3979
- // tui/views/charts/spread.ts
4153
+ // src/tui/views/charts/spread.ts
3980
4154
  var SPREAD_STRIP_WIDTH = 32;
3981
4155
  function buildSpreadCard(title, metrics, format) {
3982
4156
  const rows = metrics.map(({ label, values }) => {
@@ -3986,9 +4160,9 @@ function buildSpreadCard(title, metrics, format) {
3986
4160
  const span = Math.log1p(max) - Math.log1p(min);
3987
4161
  const pos = (value2) => {
3988
4162
  if (span === 0) {
3989
- return 0;
4163
+ return 1;
3990
4164
  }
3991
- return Math.round((Math.log1p(value2) - Math.log1p(min)) / span * (SPREAD_STRIP_WIDTH - 1));
4165
+ return 1 + Math.round((Math.log1p(value2) - Math.log1p(min)) / span * (SPREAD_STRIP_WIDTH - 3));
3992
4166
  };
3993
4167
  const cells = blankCells(SPREAD_STRIP_WIDTH);
3994
4168
  for (let i = 0; i < SPREAD_STRIP_WIDTH; i++) {
@@ -4012,13 +4186,19 @@ function buildSpreadCard(title, metrics, format) {
4012
4186
  { text: ` ${row.max}`, fg: theme.text }
4013
4187
  ];
4014
4188
  });
4015
- return { title, subtitle: "\u2592 p25-p75, \u2588 p50, log scale", lines };
4189
+ const subtitle = [
4190
+ { text: "\u2592", fg: theme.chartBar },
4191
+ { text: " p25-p75, ", fg: theme.muted },
4192
+ { text: "\u2588", fg: theme.accent },
4193
+ { text: " p50, log scale", fg: theme.muted }
4194
+ ];
4195
+ return { title, subtitle, lines };
4016
4196
  }
4017
4197
 
4018
- // tui/views/charts/trend.ts
4198
+ // src/tui/views/charts/trend.ts
4019
4199
  var asciichart = __toESM(require_asciichart(), 1);
4020
4200
 
4021
- // tui/views/charts/weeks.ts
4201
+ // src/tui/views/charts/weeks.ts
4022
4202
  var DAY_MS = 864e5;
4023
4203
  var WEEK_MS = 7 * DAY_MS;
4024
4204
  function mondayOf(dayUtcMs) {
@@ -4042,7 +4222,7 @@ function weekAxisRow(width, prefix, points, columnOf, mondayOfPoint) {
4042
4222
  return mergeCells(cells);
4043
4223
  }
4044
4224
 
4045
- // tui/views/charts/trend.ts
4225
+ // src/tui/views/charts/trend.ts
4046
4226
  var TREND_POINTS = 40;
4047
4227
  var TREND_HEIGHT = 6;
4048
4228
  function axisSplit(line) {
@@ -4136,7 +4316,7 @@ function buildTrendCard({ title, entries, format, floor }) {
4136
4316
  return { title, subtitle, lines };
4137
4317
  }
4138
4318
 
4139
- // tui/views/charts/volume.ts
4319
+ // src/tui/views/charts/volume.ts
4140
4320
  var VOLUME_ROWS = 4;
4141
4321
  var VOLUME_MAX_BARS = 18;
4142
4322
  var V_PARTIALS = [" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
@@ -4192,7 +4372,7 @@ function buildVolumeCard(title, dates, weights) {
4192
4372
  return { title, subtitle, lines };
4193
4373
  }
4194
4374
 
4195
- // tui/views/stats.ts
4375
+ // src/tui/views/stats.ts
4196
4376
  function countCell(count2, label, dimWhenZero = false) {
4197
4377
  const dim2 = dimWhenZero && count2 === 0;
4198
4378
  return [
@@ -4497,11 +4677,122 @@ function buildCommentView(raw, repo = null, width = 100) {
4497
4677
  }
4498
4678
  return { empty: null, ...base, strip, headline, left, right, distribution, lists };
4499
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
+ }
4500
4791
  function count(value2) {
4501
4792
  return formatCount(Math.round(value2));
4502
4793
  }
4503
4794
 
4504
- // tui/hooks/useViewModel.ts
4795
+ // src/tui/hooks/useViewModel.ts
4505
4796
  function resolveScope(scope, repos) {
4506
4797
  if (repos.length === 0) {
4507
4798
  return { view: "detail", repo: null };
@@ -4524,11 +4815,13 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4524
4815
  const sizeTarget = options.sizeTarget === "" ? void 0 : parseSizeTarget(options.sizeTarget);
4525
4816
  const pendingRepos = buildPendingRepoOptions(raw);
4526
4817
  const openRepos = buildOpenRepoOptions(raw);
4818
+ const mergedRepos = buildMergedRepoOptions(raw);
4527
4819
  const reviewRepos = buildReviewRepoOptions(raw);
4528
4820
  const sizeRepos = buildSizeRepoOptions(raw);
4529
4821
  const commentRepos = buildCommentRepoOptions(raw);
4530
4822
  const pendingScope = resolveScope(scopes.pending, pendingRepos);
4531
4823
  const openScope = resolveScope(scopes.open, openRepos);
4824
+ const mergedScope = resolveScope(scopes.merged, mergedRepos);
4532
4825
  const reviewScope = resolveScope(scopes.review, reviewRepos);
4533
4826
  const sizeScope = resolveScope(scopes.size, sizeRepos);
4534
4827
  const commentScope = resolveScope(scopes.comment, commentRepos);
@@ -4536,16 +4829,19 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4536
4829
  return {
4537
4830
  pendingRepos,
4538
4831
  openRepos,
4832
+ mergedRepos,
4539
4833
  reviewRepos,
4540
4834
  sizeRepos,
4541
4835
  commentRepos,
4542
4836
  pendingScope,
4543
4837
  openScope,
4838
+ mergedScope,
4544
4839
  reviewScope,
4545
4840
  sizeScope,
4546
4841
  commentScope,
4547
4842
  pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending) : null,
4548
4843
  open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
4844
+ merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width) : null,
4549
4845
  review,
4550
4846
  size: sizeScope.view === "detail" ? buildSizeView(raw, sizeTarget, sizeScope.repo, width) : null,
4551
4847
  comments: commentScope.view === "detail" ? buildCommentView(raw, commentScope.repo, width) : null
@@ -4560,6 +4856,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4560
4856
  width,
4561
4857
  scopes.pending,
4562
4858
  scopes.open,
4859
+ scopes.merged,
4563
4860
  scopes.review,
4564
4861
  scopes.size,
4565
4862
  scopes.comment,
@@ -4569,7 +4866,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
4569
4866
  ]);
4570
4867
  }
4571
4868
 
4572
- // tui/keymap.ts
4869
+ // src/tui/keymap.ts
4573
4870
  function handleOptionsModalKey(key, context) {
4574
4871
  switch (key.name) {
4575
4872
  case "escape": {
@@ -4648,6 +4945,12 @@ function handleSettingsModalKey(key, context) {
4648
4945
  }
4649
4946
  break;
4650
4947
  }
4948
+ case "copyLinks": {
4949
+ const next = !context.copyLinks;
4950
+ context.setCopyLinks(next);
4951
+ context.dispatchUi({ type: "cacheActionReported", action: saveCopyLinks(next) ? "saved" : "notSaved" });
4952
+ break;
4953
+ }
4651
4954
  case "themePreset": {
4652
4955
  const next = cycleTheme(context.themeState, key.name === "left" ? -1 : 1);
4653
4956
  applyThemeState(next);
@@ -4770,15 +5073,27 @@ function handleQueueKey(key, context) {
4770
5073
  }
4771
5074
  case "return": {
4772
5075
  const cursor = context.browse.rowCursors[tab];
4773
- context.openUrl(rows[Math.min(cursor, rows.length - 1)].url, (message) => {
4774
- context.dispatchUi({ type: "openErrorReported", message });
4775
- });
5076
+ const row = rows[Math.min(cursor, rows.length - 1)];
5077
+ if (context.copyLinks) {
5078
+ context.copyRow(row);
5079
+ } else {
5080
+ context.openUrl(row.url, (message) => {
5081
+ context.dispatchUi({ type: "openErrorReported", message });
5082
+ });
5083
+ }
4776
5084
  break;
4777
5085
  }
4778
5086
  }
4779
5087
  }
4780
5088
  function statsTabOf(context) {
4781
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
+ }
4782
5097
  if (context.browse.tab === 2) {
4783
5098
  return {
4784
5099
  key: "review",
@@ -4828,7 +5143,7 @@ function handleStatsKey(key, context) {
4828
5143
  }
4829
5144
  }
4830
5145
  function handleAppKey(key, context) {
4831
- context.dispatchUi({ type: "openErrorDismissed" });
5146
+ context.dispatchUi({ type: "noticesDismissed" });
4832
5147
  if (context.ui.editing) {
4833
5148
  if (key.name === "escape") {
4834
5149
  context.dispatchUi({ type: "editCancelled" });
@@ -4865,14 +5180,16 @@ function handleAppKey(key, context) {
4865
5180
  context.dispatchBrowse({ type: "tabCycled", delta: -1 });
4866
5181
  } else if (key.name === "right" || key.name === "tab") {
4867
5182
  context.dispatchBrowse({ type: "tabCycled", delta: 1 });
4868
- } 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") {
4869
5186
  handleQueueKey(key, context);
4870
5187
  } else {
4871
5188
  handleStatsKey(key, context);
4872
5189
  }
4873
5190
  }
4874
5191
 
4875
- // tui/state/ui.ts
5192
+ // src/tui/state/ui.ts
4876
5193
  var initialUiState = {
4877
5194
  modal: null,
4878
5195
  editing: false,
@@ -4882,19 +5199,29 @@ var initialUiState = {
4882
5199
  fieldError: null,
4883
5200
  themeColorError: null,
4884
5201
  cacheAction: null,
4885
- openError: null
5202
+ openError: null,
5203
+ copyNotice: null
4886
5204
  };
4887
5205
  function cycled2(previous, delta, count2) {
4888
5206
  return (previous + count2 + delta) % count2;
4889
5207
  }
4890
5208
  function uiReducer(state, action) {
4891
5209
  switch (action.type) {
4892
- case "openErrorDismissed": {
4893
- return state.openError === null ? state : { ...state, openError: null };
5210
+ case "noticesDismissed": {
5211
+ if (state.openError === null && state.copyNotice === null) {
5212
+ return state;
5213
+ }
5214
+ return { ...state, openError: null, copyNotice: null };
4894
5215
  }
4895
5216
  case "openErrorReported": {
4896
5217
  return { ...state, openError: action.message };
4897
5218
  }
5219
+ case "copyReported": {
5220
+ return { ...state, openError: null, copyNotice: { text: action.message } };
5221
+ }
5222
+ case "copyNoticeExpired": {
5223
+ return { ...state, copyNotice: null };
5224
+ }
4898
5225
  case "modalOpened": {
4899
5226
  return { ...state, modal: action.modal, fieldError: null, themeColorError: null, cacheAction: null };
4900
5227
  }
@@ -4959,7 +5286,7 @@ function uiReducer(state, action) {
4959
5286
  return state;
4960
5287
  }
4961
5288
 
4962
- // tui/utils/browser.ts
5289
+ // src/tui/utils/browser.ts
4963
5290
  import { spawn } from "node:child_process";
4964
5291
  function openCommand(url) {
4965
5292
  switch (process.platform) {
@@ -4988,14 +5315,50 @@ function openInBrowser(url, onError) {
4988
5315
  child.unref();
4989
5316
  }
4990
5317
 
4991
- // tui/App.tsx
4992
- import { jsx as jsx14, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
5318
+ // src/tui/utils/clipboard.ts
5319
+ import { spawn as spawn2 } from "node:child_process";
5320
+ function copyCommand() {
5321
+ switch (process.platform) {
5322
+ case "darwin": {
5323
+ return { command: "pbcopy", args: [] };
5324
+ }
5325
+ case "win32": {
5326
+ return { command: "clip", args: [] };
5327
+ }
5328
+ default: {
5329
+ if (process.env.WAYLAND_DISPLAY !== void 0 && process.env.WAYLAND_DISPLAY !== "") {
5330
+ return { command: "wl-copy", args: [] };
5331
+ }
5332
+ return { command: "xclip", args: ["-selection", "clipboard"] };
5333
+ }
5334
+ }
5335
+ }
5336
+ function copyToClipboard(text, onError) {
5337
+ const { command, args } = copyCommand();
5338
+ const child = spawn2(command, args, { stdio: ["pipe", "ignore", "ignore"] });
5339
+ child.on("error", (error) => {
5340
+ onError(`could not copy the link (${error.message})`);
5341
+ });
5342
+ child.on("exit", (code) => {
5343
+ if (code !== null && code !== 0) {
5344
+ onError(`could not copy the link (${command} exited with ${code})`);
5345
+ }
5346
+ });
5347
+ child.stdin.on("error", () => {
5348
+ });
5349
+ child.stdin.end(text);
5350
+ }
5351
+
5352
+ // src/tui/App.tsx
5353
+ import { jsx as jsx14, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
4993
5354
  function App({
4994
5355
  initial: initial2,
4995
5356
  initialSaved = null,
4996
5357
  initialNoCache = false,
5358
+ initialCopyLinks = false,
4997
5359
  initialTheme = defaultThemeState(),
4998
5360
  openUrl = openInBrowser,
5361
+ copyUrl = copyToClipboard,
4999
5362
  onQuit
5000
5363
  }) {
5001
5364
  const { width } = useTerminalDimensions3();
@@ -5004,10 +5367,29 @@ function App({
5004
5367
  const [options, setOptions] = useState4(initial2);
5005
5368
  const [saved2, setSaved] = useState4(initialSaved);
5006
5369
  const [noCache2, setNoCache] = useState4(initialNoCache);
5370
+ const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
5007
5371
  const [themeState, setThemeState] = useState4(initialTheme);
5372
+ const copyRow = (row) => {
5373
+ copyUrl(row.url, (message) => {
5374
+ dispatchUi({ type: "openErrorReported", message });
5375
+ });
5376
+ dispatchUi({ type: "copyReported", message: `copied ${row.ref} to the clipboard` });
5377
+ };
5378
+ useEffect6(() => {
5379
+ if (ui.copyNotice === null) {
5380
+ return void 0;
5381
+ }
5382
+ const timer = setTimeout(() => {
5383
+ dispatchUi({ type: "copyNoticeExpired" });
5384
+ }, 2500);
5385
+ return () => {
5386
+ clearTimeout(timer);
5387
+ };
5388
+ }, [ui.copyNotice]);
5008
5389
  const reviewScrollRef = useRef5(null);
5009
5390
  const sizeScrollRef = useRef5(null);
5010
5391
  const commentScrollRef = useRef5(null);
5392
+ const mergedScrollRef = useRef5(null);
5011
5393
  const { raw, isSnapshot, loading, load, error, stale, reload } = useLoader(options, noCache2, (data) => {
5012
5394
  dispatchBrowse({
5013
5395
  type: "dataLoaded",
@@ -5016,7 +5398,8 @@ function App({
5016
5398
  open: buildOpenRepoOptions(data),
5017
5399
  review: buildReviewRepoOptions(data),
5018
5400
  size: buildSizeRepoOptions(data),
5019
- comment: buildCommentRepoOptions(data)
5401
+ comment: buildCommentRepoOptions(data),
5402
+ merged: buildMergedRepoOptions(data)
5020
5403
  }
5021
5404
  });
5022
5405
  });
@@ -5059,6 +5442,7 @@ function App({
5059
5442
  ui,
5060
5443
  browse,
5061
5444
  noCache: noCache2,
5445
+ copyLinks: copyLinks2,
5062
5446
  themeState,
5063
5447
  options,
5064
5448
  views,
@@ -5067,22 +5451,24 @@ function App({
5067
5451
  setOptions,
5068
5452
  setSaved,
5069
5453
  setNoCache,
5454
+ setCopyLinks,
5070
5455
  setThemeState,
5071
5456
  quit: onQuit,
5072
5457
  reload,
5073
5458
  openUrl,
5459
+ copyRow,
5074
5460
  beginEdit: (value2) => {
5075
5461
  draftRef.current = value2;
5076
5462
  dispatchUi({ type: "editStarted" });
5077
5463
  },
5078
5464
  scrollBy: (forTab, delta) => {
5079
- const ref = forTab === 2 ? reviewScrollRef : forTab === 3 ? sizeScrollRef : commentScrollRef;
5465
+ const ref = forTab === 1 ? mergedScrollRef : forTab === 2 ? reviewScrollRef : forTab === 3 ? sizeScrollRef : commentScrollRef;
5080
5466
  ref.current?.scrollBy(delta);
5081
5467
  }
5082
5468
  });
5083
5469
  });
5084
5470
  const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
5085
- 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: [
5086
5472
  /* @__PURE__ */ jsx14(Header, { options, raw, error, spinning: showLoad }),
5087
5473
  /* @__PURE__ */ jsx14(TabBar, { tab: browse.tab }),
5088
5474
  /* @__PURE__ */ jsx14(
@@ -5092,10 +5478,16 @@ function App({
5092
5478
  browse,
5093
5479
  warning: capWarning,
5094
5480
  focused: ui.modal === null,
5095
- scrollRefs: { review: reviewScrollRef, size: sizeScrollRef, comment: commentScrollRef },
5481
+ scrollRefs: {
5482
+ review: reviewScrollRef,
5483
+ size: sizeScrollRef,
5484
+ comment: commentScrollRef,
5485
+ merged: mergedScrollRef
5486
+ },
5096
5487
  error,
5097
5488
  loading,
5098
- load
5489
+ load,
5490
+ onRefClick: copyLinks2 ? copyRow : null
5099
5491
  }
5100
5492
  ),
5101
5493
  /* @__PURE__ */ jsx14(
@@ -5105,8 +5497,11 @@ function App({
5105
5497
  modal: ui.modal,
5106
5498
  editing: ui.editing,
5107
5499
  tab: browse.tab,
5500
+ authoredTab: browse.authoredTab,
5108
5501
  views,
5502
+ copyLinks: copyLinks2,
5109
5503
  openError: ui.openError,
5504
+ copyNotice: ui.copyNotice === null ? null : ui.copyNotice.text,
5110
5505
  stale
5111
5506
  }
5112
5507
  ),
@@ -5130,6 +5525,7 @@ function App({
5130
5525
  selected: ui.selectedSetting,
5131
5526
  cacheAction: ui.cacheAction,
5132
5527
  noCache: noCache2,
5528
+ copyLinks: copyLinks2,
5133
5529
  preset: themeState.preset
5134
5530
  }
5135
5531
  ),
@@ -5150,7 +5546,7 @@ function App({
5150
5546
  ] });
5151
5547
  }
5152
5548
 
5153
- // tui/bootstrap.ts
5549
+ // src/tui/bootstrap.ts
5154
5550
  function bootstrap() {
5155
5551
  const { values, explicit } = parseCliArgs();
5156
5552
  if (values.help) {
@@ -5180,7 +5576,7 @@ function bootstrap() {
5180
5576
  validateField(field.key, value2);
5181
5577
  }
5182
5578
  }
5183
- return { initial: initial2, saved: saved2, noCache: values["no-cache"], theme: theme3 };
5579
+ return { initial: initial2, saved: saved2, noCache: values["no-cache"], copyLinks: settings.copyLinks === true, theme: theme3 };
5184
5580
  } catch (error) {
5185
5581
  if (error instanceof CliError) {
5186
5582
  fail(error.message);
@@ -5189,9 +5585,9 @@ function bootstrap() {
5189
5585
  }
5190
5586
  }
5191
5587
 
5192
- // tui/main.tsx
5588
+ // src/tui/main.tsx
5193
5589
  import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
5194
- var { initial, saved, noCache, theme: theme2 } = bootstrap();
5590
+ var { initial, saved, noCache, copyLinks, theme: theme2 } = bootstrap();
5195
5591
  var renderer = await createCliRenderer({ exitOnCtrlC: true });
5196
5592
  for (const signal of ["SIGTERM", "SIGHUP"]) {
5197
5593
  process.on(signal, () => {
@@ -5206,6 +5602,7 @@ createRoot(renderer).render(
5206
5602
  initial,
5207
5603
  initialSaved: saved,
5208
5604
  initialNoCache: noCache,
5605
+ initialCopyLinks: copyLinks,
5209
5606
  initialTheme: theme2,
5210
5607
  onQuit: () => {
5211
5608
  renderer.destroy();