@kud/gh-ink 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,13 @@
1
- import React2, { useState } from 'react';
1
+ import React2, { useState, useRef, useEffect } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
- import { colors, ScrollView, TextInput } from '@kud/ink-ui';
3
+ import { colors, ScrollView, TextInput, LoadingScreen, Switch, useListCursor, Tabs, FooterHints } from '@kud/ink-ui';
4
4
  import { isPassCheck, isFailCheck, resolveThread, unresolveThread, replyToThread, rerunFailedRun, mergePr, reRequestReviewer } from '@kud/gh';
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
+ import { $ } from 'zx';
7
+ import { spawn } from 'child_process';
8
+ import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
6
11
 
7
12
  // src/components/comments-panel.tsx
8
13
  var ENTITIES = {
@@ -599,5 +604,1779 @@ var healthLegend = [
599
604
  ["merged", "Merged"],
600
605
  ["closed", "Closed"]
601
606
  ];
607
+ var cacheDir = () => join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "ambre");
608
+ var cacheFile = (key) => join(cacheDir(), `${key.replace(/[^a-z0-9._-]/gi, "-")}.json`);
609
+ var readCache = (key) => {
610
+ try {
611
+ const raw = JSON.parse(readFileSync(cacheFile(key), "utf8"));
612
+ if (!Array.isArray(raw?.sections)) return null;
613
+ return { sections: raw.sections, login: raw.login ?? "", at: raw.at ?? 0 };
614
+ } catch {
615
+ return null;
616
+ }
617
+ };
618
+ var writeCache = (key, data) => {
619
+ try {
620
+ mkdirSync(cacheDir(), { recursive: true });
621
+ writeFileSync(cacheFile(key), JSON.stringify({ ...data, at: Date.now() }));
622
+ } catch {
623
+ }
624
+ };
625
+ var relativeTime = (iso) => {
626
+ const diff = (Date.now() - new Date(iso).getTime()) / 1e3;
627
+ if (diff < 3600) return `${Math.floor(diff / 60)}m`;
628
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
629
+ if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
630
+ return `${Math.floor(diff / 604800)}w`;
631
+ };
632
+ var healthSentence = (item) => {
633
+ const glyph = healthDisplay[item.health].glyph.trim();
634
+ const d = item.detail;
635
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
636
+ switch (item.health) {
637
+ case "merged":
638
+ return `Merged (${glyph}). Nothing left to do.`;
639
+ case "closed":
640
+ return `Closed (${glyph}) without merging.`;
641
+ case "draft":
642
+ return `Still a draft (${glyph}), so no review is being asked for yet.`;
643
+ case "ci-fail":
644
+ return `CI is failing (${glyph}) \u2014 ${plural(d?.checksFail ?? 0, "check")} red.`;
645
+ case "conflict":
646
+ return `It conflicts with the base branch (${glyph}) and cannot merge until that is resolved.`;
647
+ case "changes-req":
648
+ return `Changes were requested (${glyph}).`;
649
+ case "threads":
650
+ return `${plural(item.unresolved, "review thread")} still open (${glyph}).`;
651
+ case "pending":
652
+ return `${plural(d?.checksPending ?? 0, "check")} still running (${glyph}).`;
653
+ case "approved":
654
+ return `Approved (${glyph}) and ready to merge.`;
655
+ case "waiting":
656
+ return `Nobody has reviewed it yet (${glyph}).`;
657
+ default:
658
+ return "An open issue \u2014 no review state applies.";
659
+ }
660
+ };
661
+ var checksSentence = (d) => {
662
+ if (!d) return null;
663
+ const total = d.checksPass + d.checksFail + d.checksPending;
664
+ if (total === 0) return "No CI checks run on it.";
665
+ const parts = [];
666
+ if (d.checksPass) parts.push(`${d.checksPass} passing`);
667
+ if (d.checksFail) parts.push(`${d.checksFail} failing`);
668
+ if (d.checksPending) parts.push(`${d.checksPending} running`);
669
+ return `Checks: ${parts.join(", ")}.`;
670
+ };
671
+ var turnSentences = (item, login) => {
672
+ if (!item.lastActor) return ["Nothing has been said on it yet."];
673
+ const d = item.detail;
674
+ const when = d?.lastEventAt ? `${relativeTime(d.lastEventAt)} ago` : "earlier";
675
+ if (item.lastActor !== login)
676
+ return [`${item.lastActor} spoke last (\u2190), ${when}. Your reply is owed.`];
677
+ const them = item.author && item.author !== login ? item.author : null;
678
+ if (!them)
679
+ return [
680
+ `You spoke last (\u2192), ${when}. It is waiting on a reviewer, not on you.`
681
+ ];
682
+ const lines = [`You spoke last (\u2192), ${when}. The ball is with ${them}.`];
683
+ if (d?.lastCommitAt && d.lastEventAt && d.lastCommitAt < d.lastEventAt)
684
+ lines.push(
685
+ `Nothing has been pushed since ${relativeTime(d.lastCommitAt)} ago, so it is stalled on ${them}, not on you.`
686
+ );
687
+ return lines;
688
+ };
689
+ var explainItem = (item, login) => {
690
+ const author = item.author && item.author !== login ? item.author : "you";
691
+ const kind = item.kind === "pr" ? "pull request" : "issue";
692
+ const stands = [healthSentence(item)];
693
+ const checks = item.kind === "pr" ? checksSentence(item.detail) : null;
694
+ if (checks) stands.push(checks);
695
+ if (item.conversation > 0)
696
+ stands.push(
697
+ `${item.conversation} comment${item.conversation === 1 ? "" : "s"} across the conversation and its threads.`
698
+ );
699
+ return [
700
+ {
701
+ heading: "What it is",
702
+ lines: [
703
+ `A ${kind} on ${item.repo}, opened by ${author} ${item.age} ago.`
704
+ ]
705
+ },
706
+ { heading: "Where it stands", lines: stands },
707
+ { heading: "Whose turn", lines: turnSentences(item, login) }
708
+ ];
709
+ };
710
+ var repoPriority = (repo) => {
711
+ const profile = process.env.OS_PROFILE ?? "";
712
+ if (profile === "work") {
713
+ if (repo === "theorchard/orchardgo") return 0;
714
+ if (repo.startsWith("theorchard/")) return 1;
715
+ if (repo.startsWith("kud/")) return 2;
716
+ return 3;
717
+ }
718
+ return repo.startsWith("kud/") ? 0 : 1;
719
+ };
720
+ var sortItems = (items) => [...items].sort((a, b) => {
721
+ const pd = repoPriority(a.repo) - repoPriority(b.repo);
722
+ return pd !== 0 ? pd : a.repo.localeCompare(b.repo);
723
+ });
724
+ var sortByRecency = (items) => [...items].sort((a, b) => b.ts - a.ts);
725
+ var insertRepoHeaders = (items) => {
726
+ const result = [];
727
+ let lastRepo = "";
728
+ for (const item of items) {
729
+ if (!item.indent && item.repo !== lastRepo) {
730
+ lastRepo = item.repo;
731
+ result.push({
732
+ kind: "repo-header",
733
+ repo: item.repo,
734
+ age: "",
735
+ indent: false
736
+ });
737
+ }
738
+ result.push(item);
739
+ }
740
+ return result;
741
+ };
742
+ var layoutGHItems = (items, sectionId) => insertRepoHeaders(
743
+ sectionId === "done" ? sortByRecency(items) : sortItems(items)
744
+ );
745
+ var filterByOrigin = (sections, keep, isWorkRepo) => sections.map((s) => {
746
+ const kept = s.items.filter(
747
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && (i.kind === "pr" || i.kind === "issue" ? keep === "work" ? isWorkRepo(i.repo) : !isWorkRepo(i.repo) : true)
748
+ );
749
+ const gh = kept.filter(
750
+ (i) => i.kind === "pr" || i.kind === "issue"
751
+ );
752
+ const other = kept.filter((i) => i.kind !== "pr" && i.kind !== "issue");
753
+ return { ...s, items: [...layoutGHItems(gh, s.id), ...other] };
754
+ }).filter(
755
+ (s) => s.items.some(
756
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
757
+ )
758
+ );
759
+ var searchText = (i) => i.kind === "pr" || i.kind === "issue" ? `${i.title} ${i.repo} #${i.number}` : i.kind === "jira" ? `${i.summary} ${i.key}` : "";
760
+ var filterBySearch = (sections, query) => {
761
+ const q = query.trim().toLowerCase();
762
+ if (!q) return sections;
763
+ return sections.map((s) => {
764
+ const kept = s.items.filter(
765
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && searchText(i).toLowerCase().includes(q)
766
+ );
767
+ const gh = kept.filter(
768
+ (i) => i.kind === "pr" || i.kind === "issue"
769
+ );
770
+ const other = kept.filter((i) => i.kind !== "pr" && i.kind !== "issue");
771
+ return { ...s, items: [...layoutGHItems(gh, s.id), ...other] };
772
+ }).filter(
773
+ (s) => s.items.some(
774
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
775
+ )
776
+ );
777
+ };
778
+ var filterByRepos = (sections, repos) => {
779
+ if (repos.size === 0) return sections;
780
+ return sections.map((s) => {
781
+ const kept = s.items.filter(
782
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && (i.kind === "pr" || i.kind === "issue" ? repos.has(i.repo) : true)
783
+ );
784
+ const gh = kept.filter(
785
+ (i) => i.kind === "pr" || i.kind === "issue"
786
+ );
787
+ const other = kept.filter((i) => i.kind !== "pr" && i.kind !== "issue");
788
+ return { ...s, items: [...layoutGHItems(gh, s.id), ...other] };
789
+ }).filter(
790
+ (s) => s.items.some(
791
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
792
+ )
793
+ );
794
+ };
795
+ var isHeader = (i) => i.kind === "repo-header" || i.kind === "subgroup-header";
796
+ var headerOwnsContent = (items, idx) => {
797
+ const kind = items[idx].kind;
798
+ for (let i = idx + 1; i < items.length; i++) {
799
+ const next = items[i];
800
+ if (!isHeader(next)) return true;
801
+ if (kind === "repo-header" || next.kind === "subgroup-header") return false;
802
+ }
803
+ return false;
804
+ };
805
+ var withoutItem = (sections, target) => sections.map((s) => {
806
+ const kept = s.items.filter(
807
+ (i) => isHeader(i) || !(i.kind === target.kind && i.number === target.number && i.repo === target.repo)
808
+ );
809
+ return {
810
+ ...s,
811
+ items: kept.filter(
812
+ (item, idx) => !isHeader(item) || headerOwnsContent(kept, idx)
813
+ )
814
+ };
815
+ }).filter((s) => s.items.some((i) => !isHeader(i)));
816
+ var reposInSections = (sections) => [
817
+ ...new Set(
818
+ sections.flatMap((s) => s.items).filter((i) => i.kind === "pr" || i.kind === "issue").map((i) => i.repo)
819
+ )
820
+ ].sort();
821
+ var moveCursor = (items, current, dir) => {
822
+ let next = current + dir;
823
+ while (next >= 0 && next < items.length && (items[next].kind === "repo-header" || items[next].kind === "subgroup-header"))
824
+ next += dir;
825
+ if (next < 0 || next >= items.length) return current;
826
+ return next;
827
+ };
828
+ var itemLines = (item, isFirst) => (item.kind === "repo-header" || item.kind === "subgroup-header") && !isFirst ? 2 : 1;
829
+ var fitCount = (items, start, budget) => {
830
+ let lines = 0;
831
+ let count = 0;
832
+ for (let i = start; i < items.length; i++) {
833
+ const cost = itemLines(items[i], i === start);
834
+ if (lines + cost > budget) break;
835
+ lines += cost;
836
+ count++;
837
+ }
838
+ return count;
839
+ };
840
+ var windowCount = (items, start, budget) => {
841
+ const raw = fitCount(items, start, budget);
842
+ return start + raw < items.length ? fitCount(items, start, budget - 1) : raw;
843
+ };
844
+ var maxViewStart = (items, budget) => {
845
+ let start = 0;
846
+ while (start < items.length - 1 && start + windowCount(items, start, budget) < items.length)
847
+ start++;
848
+ return start;
849
+ };
850
+ var firstSelectable = (section) => Math.max(
851
+ 0,
852
+ section.items.findIndex(
853
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
854
+ )
855
+ );
856
+ var withHeaders = (items, idx) => {
857
+ let start = idx;
858
+ while (start > 0 && (items[start - 1].kind === "repo-header" || items[start - 1].kind === "subgroup-header"))
859
+ start--;
860
+ return start;
861
+ };
862
+ var truncate = (str, max) => {
863
+ if (str.length <= max) return str;
864
+ const half = Math.floor((max - 1) / 2);
865
+ return `${str.slice(0, half)}\u2026${str.slice(-half)}`;
866
+ };
867
+ var clipboard = (text) => {
868
+ const p = spawn("pbcopy", [], { stdio: "pipe" });
869
+ p.stdin.write(text);
870
+ p.stdin.end();
871
+ };
872
+ var buildCheckoutCmd = async (repoFull, branch, login) => {
873
+ const [repoOwner, repoName] = repoFull.split("/");
874
+ const projects = process.env.PROJECTS_DIR ?? `${process.env.HOME}/Projects`;
875
+ const profile = process.env.OS_PROFILE ?? "";
876
+ const isWorkRepo = profile === "work" && (repoFull.startsWith("theorchard/") || repoFull.startsWith("kud/") && (repoName ?? "").startsWith("theorchard-"));
877
+ const cloneBase = profile === "work" ? `${projects}/${isWorkRepo ? "work" : "home"}` : projects;
878
+ const searchDirs = profile === "work" ? [`${projects}/work`, `${projects}/home`] : [projects];
879
+ let repoPath = "";
880
+ outer: for (const searchDir of searchDirs) {
881
+ if (!existsSync(searchDir)) continue;
882
+ let entries;
883
+ try {
884
+ entries = readdirSync(searchDir);
885
+ } catch {
886
+ continue;
887
+ }
888
+ for (const entry of entries) {
889
+ const fullPath = join(searchDir, entry);
890
+ try {
891
+ if (!statSync(fullPath).isDirectory()) continue;
892
+ } catch {
893
+ continue;
894
+ }
895
+ for (const remote of ["origin", "upstream"]) {
896
+ const r = await $({
897
+ nothrow: true,
898
+ quiet: true
899
+ })`git -C ${fullPath} remote get-url ${remote}`;
900
+ if (r.exitCode === 0 && r.stdout.includes(repoFull)) {
901
+ repoPath = fullPath;
902
+ break outer;
903
+ }
904
+ }
905
+ }
906
+ }
907
+ if (!repoPath) {
908
+ const candidate = `${cloneBase}/${repoOwner}-${repoName}`;
909
+ if (existsSync(candidate)) repoPath = candidate;
910
+ }
911
+ let cmd;
912
+ if (repoPath) {
913
+ cmd = `cd ${repoPath}`;
914
+ } else if (repoOwner === login) {
915
+ cmd = `cd ${cloneBase} && gh repo clone ${repoFull} && cd ${repoName}`;
916
+ } else {
917
+ const r = await $({
918
+ nothrow: true,
919
+ quiet: true
920
+ })`gh repo list ${login} --fork --limit 200 --json name,parent --jq ${`.[] | select(.parent.nameWithOwner == "${repoFull}") | .name`}`;
921
+ const forkName = r.stdout.trim();
922
+ if (forkName) {
923
+ cmd = `cd ${cloneBase} && git clone git@github.com:${login}/${forkName}.git && cd ${forkName} && git remote add upstream git@github.com:${repoFull}.git`;
924
+ } else {
925
+ cmd = `cd ${cloneBase} && gh repo fork ${repoFull} --clone && cd $(ls -td -- */ | head -1)`;
926
+ }
927
+ }
928
+ if (branch)
929
+ cmd += ` && git fetch origin ${branch} 2>/dev/null; git switch ${branch}`;
930
+ return cmd;
931
+ };
932
+ var resolveRepoPath = async (repoFull) => {
933
+ const projects = process.env.PROJECTS_DIR ?? `${process.env.HOME}/Projects`;
934
+ const profile = process.env.OS_PROFILE ?? "";
935
+ const searchDirs = profile === "work" ? [`${projects}/work`, `${projects}/home`] : [projects];
936
+ for (const searchDir of searchDirs) {
937
+ if (!existsSync(searchDir)) continue;
938
+ let entries;
939
+ try {
940
+ entries = readdirSync(searchDir);
941
+ } catch {
942
+ continue;
943
+ }
944
+ for (const entry of entries) {
945
+ const fullPath = join(searchDir, entry);
946
+ try {
947
+ if (!statSync(fullPath).isDirectory()) continue;
948
+ } catch {
949
+ continue;
950
+ }
951
+ for (const remote of ["origin", "upstream"]) {
952
+ const r = await $({
953
+ nothrow: true,
954
+ quiet: true
955
+ })`git -C ${fullPath} remote get-url ${remote}`;
956
+ if (r.exitCode === 0 && r.stdout.includes(repoFull)) return fullPath;
957
+ }
958
+ }
959
+ }
960
+ return null;
961
+ };
962
+ var itermRun = async (cmd, appleScript) => {
963
+ await $({
964
+ nothrow: true,
965
+ quiet: true,
966
+ env: { ...process.env, ITERM_CMD: cmd }
967
+ })`osascript -e ${appleScript}`;
968
+ };
969
+ var jumpToRepo = async (repoFull, branch, login) => {
970
+ const cmd = await buildCheckoutCmd(repoFull, branch, login);
971
+ await itermRun(
972
+ cmd,
973
+ `tell application "iTerm2"
974
+ tell current window
975
+ create tab with default profile
976
+ tell current session of current tab
977
+ write text (system attribute "ITERM_CMD")
978
+ end tell
979
+ end tell
980
+ end tell`
981
+ );
982
+ };
983
+ var runInPane = async (cmd) => {
984
+ await itermRun(
985
+ cmd,
986
+ `tell application "iTerm2"
987
+ tell current window
988
+ tell current session of current tab
989
+ set newPane to (split vertically with default profile)
990
+ tell newPane
991
+ write text (system attribute "ITERM_CMD")
992
+ end tell
993
+ end tell
994
+ end tell
995
+ end tell`
996
+ );
997
+ };
998
+ var jumpToRepoPane = async (repoFull, branch, login) => {
999
+ const cmd = await buildCheckoutCmd(repoFull, branch, login);
1000
+ await runInPane(cmd);
1001
+ };
1002
+ var openInTab = async (cmd) => {
1003
+ await itermRun(
1004
+ cmd,
1005
+ `tell application "iTerm2"
1006
+ tell current window
1007
+ create tab with default profile
1008
+ tell current session of current tab
1009
+ write text (system attribute "ITERM_CMD")
1010
+ end tell
1011
+ end tell
1012
+ end tell`
1013
+ );
1014
+ };
1015
+ var runInPaneHorizontal = async (cmd) => {
1016
+ await itermRun(
1017
+ cmd,
1018
+ `tell application "iTerm2"
1019
+ tell current window
1020
+ tell current session of current tab
1021
+ set newPane to (split horizontally with default profile)
1022
+ tell newPane
1023
+ write text (system attribute "ITERM_CMD")
1024
+ end tell
1025
+ end tell
1026
+ end tell
1027
+ end tell`
1028
+ );
1029
+ };
1030
+ var runHere = (cmd) => {
1031
+ const proc = spawn(
1032
+ "osascript",
1033
+ [
1034
+ "-e",
1035
+ `delay 0.5
1036
+ tell application "iTerm2"
1037
+ tell current session of current window
1038
+ write text (system attribute "ITERM_CMD")
1039
+ end tell
1040
+ end tell`
1041
+ ],
1042
+ {
1043
+ detached: true,
1044
+ stdio: "ignore",
1045
+ env: { ...process.env, ITERM_CMD: cmd }
1046
+ }
1047
+ );
1048
+ proc.unref();
1049
+ };
1050
+ var FRAME_COLOR = "gray";
1051
+ var FRAME_PAD_X = 1;
1052
+ var FRAME_CHROME_COLS = 2 + FRAME_PAD_X * 2;
1053
+ var COLS = (process.stdout.columns ?? 120) - FRAME_CHROME_COLS;
1054
+ var topLevelCount = (s) => s.items.filter(
1055
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && !i.indent
1056
+ ).length;
1057
+ var drillCmd = (item) => {
1058
+ if (item.kind === "jira") return `jira issue view ${item.key}`;
1059
+ return null;
1060
+ };
1061
+ var drillLabel = (item) => {
1062
+ if (item.kind === "jira") return "View ticket";
1063
+ if (item.kind === "issue") return "View issue";
1064
+ if (item.kind === "pr") return "Open PR";
1065
+ return "Drill in";
1066
+ };
1067
+ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions, onRefresh, onRemove, onOpenView) => {
1068
+ if (item.kind === "repo-header" || item.kind === "subgroup-header" || item.kind === "show-more" || item.kind === "show-less")
1069
+ return [];
1070
+ const open = {
1071
+ label: "Open in browser",
1072
+ hint: "o",
1073
+ run: () => {
1074
+ $`open ${item.url}`.catch(() => {
1075
+ });
1076
+ showFlash("\u2197 Opened in browser");
1077
+ }
1078
+ };
1079
+ const copyUrl = {
1080
+ label: "Copy URL",
1081
+ hint: "c",
1082
+ run: () => {
1083
+ clipboard(item.url);
1084
+ const label = item.kind === "jira" ? item.key : `#${item.number}`;
1085
+ showFlash(`\u2713 Copied URL for ${label}`);
1086
+ }
1087
+ };
1088
+ const drill = drillCmd(item);
1089
+ const mountable = item.kind === "pr" || item.kind === "issue";
1090
+ const drillAction = drill || mountable && onOpenView ? {
1091
+ label: drillLabel(item),
1092
+ hint: "d",
1093
+ run: () => {
1094
+ if (onOpenView?.(item)) return;
1095
+ if (drill) {
1096
+ void runInPane(drill).catch(() => {
1097
+ });
1098
+ showFlash(`\u2197 ${drillLabel(item)}`);
1099
+ }
1100
+ }
1101
+ } : null;
1102
+ if (item.kind === "jira") {
1103
+ const base = drillAction ? [drillAction, open, copyUrl] : [open, copyUrl];
1104
+ if (jiraTransitions && jiraTransitions.length > 0) {
1105
+ base.push({
1106
+ label: "Move status",
1107
+ hint: "t",
1108
+ run: () => {
1109
+ },
1110
+ subActions: jiraTransitions.map(
1111
+ ({ label, state, resolutions }) => resolutions && resolutions.length > 0 ? {
1112
+ label,
1113
+ hint: "",
1114
+ run: () => {
1115
+ },
1116
+ subActions: resolutions.map((resolution) => ({
1117
+ label: resolution,
1118
+ hint: "",
1119
+ run: () => {
1120
+ showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
1121
+ void $`jira issue move ${item.key} ${state} --resolution ${resolution}`.then(() => {
1122
+ showFlash(`\u2713 ${label} \xB7 ${resolution}`);
1123
+ setTimeout(() => onRefresh?.(), 1500);
1124
+ }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
1125
+ }
1126
+ }))
1127
+ } : {
1128
+ label,
1129
+ hint: "",
1130
+ run: () => {
1131
+ showFlash(`\u22EF Moving to ${label}\u2026`);
1132
+ void $`jira issue move ${item.key} ${state}`.then(() => {
1133
+ showFlash(`\u2713 Moved to ${label}`);
1134
+ setTimeout(() => onRefresh?.(), 1500);
1135
+ }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
1136
+ }
1137
+ }
1138
+ )
1139
+ });
1140
+ }
1141
+ return base;
1142
+ }
1143
+ const actions = drillAction ? [drillAction, open, copyUrl] : [open, copyUrl];
1144
+ actions.push({
1145
+ label: "Copy repo name",
1146
+ hint: "r",
1147
+ run: () => {
1148
+ clipboard(item.repo);
1149
+ showFlash(`\u2713 Copied ${item.repo}`);
1150
+ }
1151
+ });
1152
+ if (item.kind === "pr" && item.branch) {
1153
+ actions.push({
1154
+ label: "Copy branch name",
1155
+ hint: "b",
1156
+ run: () => {
1157
+ clipboard(item.branch);
1158
+ showFlash(`\u2713 Copied ${item.branch}`);
1159
+ }
1160
+ });
1161
+ }
1162
+ if (item.kind === "pr" && item.branch) {
1163
+ actions.push({
1164
+ label: "Switch here",
1165
+ hint: "s",
1166
+ run: () => {
1167
+ const script = [
1168
+ "delay 0.5",
1169
+ 'tell application "iTerm2"',
1170
+ " tell current session of current window",
1171
+ ` write text "git switch ${item.branch}"`,
1172
+ " end tell",
1173
+ "end tell"
1174
+ ].join("\n");
1175
+ const proc = spawn("osascript", ["-e", script], {
1176
+ detached: true,
1177
+ stdio: "ignore"
1178
+ });
1179
+ proc.unref();
1180
+ process.exit(0);
1181
+ }
1182
+ });
1183
+ }
1184
+ if (item.kind === "issue") {
1185
+ actions.push({
1186
+ label: "Open project in new tab",
1187
+ hint: "j",
1188
+ run: () => {
1189
+ showFlash(`\u22EF Opening ${item.repo}\u2026`);
1190
+ void jumpToRepo(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
1191
+ }
1192
+ });
1193
+ actions.push({
1194
+ label: "Open project in new pane",
1195
+ hint: "p",
1196
+ run: () => {
1197
+ showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
1198
+ void jumpToRepoPane(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
1199
+ }
1200
+ });
1201
+ actions.push({
1202
+ label: "Close issue",
1203
+ hint: "",
1204
+ run: () => {
1205
+ },
1206
+ subActions: [
1207
+ {
1208
+ label: `Close #${item.number}`,
1209
+ hint: "",
1210
+ run: () => {
1211
+ onRemove?.(item);
1212
+ showFlash(`\u2713 Closed #${item.number}`);
1213
+ void $`gh issue close ${item.number} --repo ${item.repo}`.catch(
1214
+ () => {
1215
+ showFlash(`\u2717 Close failed \u2014 restoring #${item.number}`);
1216
+ onRefresh?.();
1217
+ }
1218
+ );
1219
+ }
1220
+ },
1221
+ { label: "Cancel", hint: "", run: () => {
1222
+ } }
1223
+ ]
1224
+ });
1225
+ }
1226
+ if (item.kind === "pr") {
1227
+ actions.push({
1228
+ label: "Switch in new tab",
1229
+ hint: "j",
1230
+ run: () => {
1231
+ showFlash(`\u22EF Jumping to ${item.repo}\u2026`);
1232
+ void jumpToRepo(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
1233
+ }
1234
+ });
1235
+ actions.push({
1236
+ label: "Switch in new pane",
1237
+ hint: "p",
1238
+ run: () => {
1239
+ showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
1240
+ void jumpToRepoPane(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
1241
+ }
1242
+ });
1243
+ if (jiraBase && jiraKeyRe) {
1244
+ const jiraKey = !item.indent ? item.title.match(jiraKeyRe)?.[0] : null;
1245
+ if (jiraKey) {
1246
+ actions.push({
1247
+ label: `Open ${jiraKey} in Jira`,
1248
+ hint: "t",
1249
+ run: () => {
1250
+ $`open ${jiraBase}/${jiraKey}`.catch(() => {
1251
+ });
1252
+ showFlash(`\u2197 Opened ${jiraKey} in Jira`);
1253
+ }
1254
+ });
1255
+ }
1256
+ }
1257
+ actions.push({
1258
+ label: "Close PR",
1259
+ hint: "",
1260
+ run: () => {
1261
+ },
1262
+ subActions: [
1263
+ {
1264
+ label: `Close #${item.number}`,
1265
+ hint: "",
1266
+ run: () => {
1267
+ onRemove?.(item);
1268
+ showFlash(`\u2713 Closed #${item.number}`);
1269
+ void $`gh pr close ${item.number} --repo ${item.repo}`.catch(() => {
1270
+ showFlash(`\u2717 Close failed \u2014 restoring #${item.number}`);
1271
+ onRefresh?.();
1272
+ });
1273
+ }
1274
+ },
1275
+ { label: "Cancel", hint: "", run: () => {
1276
+ } }
1277
+ ]
1278
+ });
1279
+ if (item.branch) {
1280
+ actions.push({
1281
+ label: "Close PR + Delete branch",
1282
+ hint: "",
1283
+ run: () => {
1284
+ },
1285
+ subActions: [
1286
+ {
1287
+ label: `Close #${item.number} + delete ${item.branch}`,
1288
+ hint: "",
1289
+ run: () => {
1290
+ onRemove?.(item);
1291
+ showFlash(`\u2713 Closed #${item.number} and deleted ${item.branch}`);
1292
+ void $`gh pr close ${item.number} --repo ${item.repo}`.then(
1293
+ () => $`gh api -X DELETE ${`repos/${item.repo}/git/refs/heads/${item.branch}`}`
1294
+ ).catch(() => {
1295
+ showFlash(`\u2717 Close + delete failed \u2014 restoring`);
1296
+ onRefresh?.();
1297
+ });
1298
+ }
1299
+ },
1300
+ { label: "Cancel", hint: "", run: () => {
1301
+ } }
1302
+ ]
1303
+ });
1304
+ }
1305
+ }
1306
+ return actions;
1307
+ };
1308
+ var agoText = (ms) => {
1309
+ const d = (Date.now() - ms) / 1e3;
1310
+ if (d < 60) return "just now";
1311
+ if (d < 3600) return `${Math.floor(d / 60)}m ago`;
1312
+ if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
1313
+ return `${Math.floor(d / 86400)}d ago`;
1314
+ };
1315
+ var brandOf = (title) => `\u{1F680} ${title[0].toUpperCase()}${title.slice(1)}`;
1316
+ var InboxHeader = ({
1317
+ sections,
1318
+ login,
1319
+ brand,
1320
+ work,
1321
+ loading,
1322
+ refreshing,
1323
+ hasPending,
1324
+ fetchedAt
1325
+ }) => {
1326
+ const total = sections.reduce((n, s) => n + topLevelCount(s), 0);
1327
+ const countSeg = loading ? " loading\u2026 " : ` ${String(total).padStart(3)} item${total !== 1 ? "s" : ""} \xB7 `;
1328
+ const userSeg = loading ? "" : `@${login} `;
1329
+ const workLabel = work === void 0 ? "" : " w work \u25CF\u2500\u25CB home ";
1330
+ const [statusText, statusColor] = hasPending ? ["\u25CF new \xB7 r apply", "#FF8700"] : refreshing ? ["\u21BB refreshing\u2026", "cyan"] : fetchedAt ? [`updated ${agoText(fetchedAt)}`, void 0] : ["", void 0];
1331
+ const statusSeg = statusText ? statusText + " " : "";
1332
+ const fill = Math.max(
1333
+ 4,
1334
+ COLS - brand.length - countSeg.length - userSeg.length - workLabel.length - statusSeg.length
1335
+ );
1336
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1337
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", bold: true, children: brand }),
1338
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: countSeg }),
1339
+ userSeg ? /* @__PURE__ */ jsx(Text, { children: userSeg }) : null,
1340
+ work !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1341
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " w " }),
1342
+ /* @__PURE__ */ jsx(Switch, { left: "work", right: "home", value: work ? "left" : "right" }),
1343
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " })
1344
+ ] }) : null,
1345
+ statusText ? /* @__PURE__ */ jsx(
1346
+ Text,
1347
+ {
1348
+ color: statusColor,
1349
+ dimColor: !statusColor,
1350
+ bold: hasPending,
1351
+ children: statusSeg
1352
+ }
1353
+ ) : null,
1354
+ /* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: "\u254C".repeat(fill) })
1355
+ ] });
1356
+ };
1357
+ var toCiStatusState = (status) => status ? { kind: "ready", status } : { kind: "error" };
1358
+ var sameCiStatusState = (a, b) => {
1359
+ if (a.kind !== b.kind) return false;
1360
+ if (a.kind !== "ready" || b.kind !== "ready") return true;
1361
+ return a.status.job === b.status.job && a.status.buildNumber === b.status.buildNumber && a.status.result === b.status.result && a.status.building === b.status.building;
1362
+ };
1363
+ var jenkinsResultDisplay = (result, building) => {
1364
+ if (building) return ["*", "yellow"];
1365
+ if (result === "SUCCESS") return ["\u2713", "green"];
1366
+ if (result === "FAILURE" || result === "ABORTED") return ["\u2717", "red"];
1367
+ if (result === "UNSTABLE") return ["\xB1", "yellow"];
1368
+ return ["\xB7", "#888888"];
1369
+ };
1370
+ var CiStatusLine = ({
1371
+ state,
1372
+ job
1373
+ }) => {
1374
+ const name = job ?? "ci";
1375
+ if (state.kind === "loading")
1376
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1377
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1378
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${name} loading\u2026` })
1379
+ ] });
1380
+ if (state.kind === "error")
1381
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1382
+ /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: " \u2717 " }),
1383
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${name} no build / not configured` })
1384
+ ] });
1385
+ const { status } = state;
1386
+ const [, color] = jenkinsResultDisplay(status.result, status.building);
1387
+ const label = status.building ? "BUILDING" : status.result;
1388
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1389
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1390
+ /* @__PURE__ */ jsx(Text, { color, bold: true, children: "\u25CF " }),
1391
+ /* @__PURE__ */ jsx(Text, { bold: true, children: status.job }),
1392
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " + label }),
1393
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", children: " #" + status.buildNumber }),
1394
+ status.age ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " + status.age }) : null
1395
+ ] });
1396
+ };
1397
+ var RepoHeaderRow = ({ repo, gap }) => {
1398
+ const label = `\u2500\u2500 ${repo} `;
1399
+ const fill = Math.max(4, 46 - label.length);
1400
+ return /* @__PURE__ */ jsx(Box, { marginTop: gap ? 1 : 0, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " + label + "\u2500".repeat(fill) }) });
1401
+ };
1402
+ var ItemRow = ({
1403
+ item,
1404
+ active,
1405
+ gap,
1406
+ login
1407
+ }) => {
1408
+ if (item.kind === "repo-header")
1409
+ return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false });
1410
+ if (item.kind === "subgroup-header")
1411
+ return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1412
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", bold: true, children: " \xBB " }),
1413
+ /* @__PURE__ */ jsx(Text, { bold: true, children: item.label })
1414
+ ] });
1415
+ if (item.kind === "show-more")
1416
+ return /* @__PURE__ */ jsxs(Box, { children: [
1417
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
1418
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2514\u2500 " }),
1419
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: active ? "\u21B5 " : " " }),
1420
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `+${item.hidden.length} more` })
1421
+ ] });
1422
+ if (item.kind === "show-less")
1423
+ return /* @__PURE__ */ jsxs(Box, { children: [
1424
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
1425
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2514\u2500 " }),
1426
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: active ? "\u21B5 " : " " }),
1427
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "show less" })
1428
+ ] });
1429
+ if (item.kind === "jira") {
1430
+ const titleMax2 = Math.max(20, COLS - item.key.length - 10);
1431
+ return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1432
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
1433
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", bold: active, children: item.key + " " }),
1434
+ /* @__PURE__ */ jsx(Text, { bold: active, children: truncate(item.summary, titleMax2) })
1435
+ ] });
1436
+ }
1437
+ const { glyph: icon, color } = healthDisplay[item.health];
1438
+ const [turnIcon, turnColor] = !login || !item.lastActor ? [" ", "white"] : item.lastActor === login ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
1439
+ const numStr = `#${item.number}`.padEnd(7);
1440
+ const showAuthor = !!item.author && item.author !== login;
1441
+ const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
1442
+ const suffix = [
1443
+ item.age || "",
1444
+ unresolvedLabel,
1445
+ showAuthor ? `by ${item.author}` : ""
1446
+ ].filter(Boolean).join(" ");
1447
+ const repoLabel = item.indent ? item.repo : "";
1448
+ const fixedWidth = 2 + (item.indent ? 3 : 0) + 2 + 2 + 7 + repoLabel.length + suffix.length + 6;
1449
+ const titleMax = Math.max(20, COLS - fixedWidth);
1450
+ return /* @__PURE__ */ jsxs(Box, { children: [
1451
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
1452
+ item.indent ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2514\u2500 " }) : null,
1453
+ /* @__PURE__ */ jsx(Text, { color, bold: true, children: icon + " " }),
1454
+ /* @__PURE__ */ jsx(Text, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
1455
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", children: numStr }),
1456
+ /* @__PURE__ */ jsx(Text, { bold: active, children: truncate(item.title, titleMax) + " " }),
1457
+ repoLabel ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: repoLabel }) : null,
1458
+ unresolvedLabel ? /* @__PURE__ */ jsx(Text, { bold: true, color: "#FF8700", children: " " + unresolvedLabel }) : null,
1459
+ showAuthor ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: " by " + item.author }) : null,
1460
+ item.age ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " + item.age }) : null
1461
+ ] });
1462
+ };
1463
+ var useActionMenu = () => {
1464
+ const [actions, setActions] = useState(null);
1465
+ const [cursor, setCursor] = useState(0);
1466
+ const open = (next) => {
1467
+ if (next.length === 0) return;
1468
+ setCursor(0);
1469
+ setActions(next);
1470
+ };
1471
+ const handleKey = (key) => {
1472
+ if (!actions) return false;
1473
+ if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
1474
+ if (key.downArrow) setCursor((c) => Math.min(actions.length - 1, c + 1));
1475
+ if (key.return) {
1476
+ const action = actions[cursor];
1477
+ if (action?.subActions) {
1478
+ setCursor(0);
1479
+ setActions(action.subActions);
1480
+ } else {
1481
+ setActions(null);
1482
+ action?.run();
1483
+ }
1484
+ }
1485
+ if (key.escape) setActions(null);
1486
+ return true;
1487
+ };
1488
+ return { actions, cursor, open, close: () => setActions(null), handleKey };
1489
+ };
1490
+ var ActionMenu = ({
1491
+ item,
1492
+ actions,
1493
+ cursor
1494
+ }) => {
1495
+ const title = item.kind === "jira" ? item.key : item.kind === "pr" || item.kind === "issue" ? `#${item.number}` : "";
1496
+ return /* @__PURE__ */ jsxs(
1497
+ Box,
1498
+ {
1499
+ flexDirection: "column",
1500
+ borderStyle: "round",
1501
+ borderColor: "cyan",
1502
+ paddingX: 1,
1503
+ marginTop: 1,
1504
+ children: [
1505
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: title }),
1506
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(32) }),
1507
+ actions.map((a, i) => /* @__PURE__ */ jsxs(Box, { children: [
1508
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: i === cursor ? "\u276F " : " " }),
1509
+ /* @__PURE__ */ jsx(Text, { bold: i === cursor, children: a.label }),
1510
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " + a.hint })
1511
+ ] }, a.label)),
1512
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(32) }),
1513
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u21B5 confirm esc cancel" })
1514
+ ]
1515
+ }
1516
+ );
1517
+ };
1518
+ var TURN_LEGEND = [
1519
+ ["\u2190", "#FF8700", "They spoke last \xB7 your turn"],
1520
+ ["\u2192", "#888888", "You spoke last \xB7 waiting on them"]
1521
+ ];
1522
+ var HelpModal = ({
1523
+ workToggle,
1524
+ hasCi,
1525
+ hasJira,
1526
+ tabHelp
1527
+ }) => {
1528
+ const keys = [
1529
+ ["\u2191 \u2193", "navigate"],
1530
+ ["\u2190 \u2192 \xB7 tab", "switch tab"],
1531
+ ["\u21B5 \xB7 d", "open / drill in"],
1532
+ ["m", "actions \xB7 close"],
1533
+ ["e", "explain this row"],
1534
+ ["o", "open in browser"],
1535
+ ["c", "copy URL"],
1536
+ ["b", "copy branch"],
1537
+ ["s", "switch to branch here"],
1538
+ ["j", "open repo in new tab"],
1539
+ ["p", "open repo in new pane"],
1540
+ ...hasJira ? [["t", "Jira: move / open ticket"]] : [],
1541
+ ["/", "search"],
1542
+ ["f", "filter by repo"],
1543
+ ["r", "refresh"],
1544
+ ...workToggle ? [["w", "toggle work / home"]] : [],
1545
+ ...hasCi ? [["J", "Jenkins explorer"]] : [],
1546
+ ["?", "this help"],
1547
+ ["q", "quit"]
1548
+ ];
1549
+ return /* @__PURE__ */ jsxs(
1550
+ Box,
1551
+ {
1552
+ flexDirection: "column",
1553
+ borderStyle: "round",
1554
+ borderColor: "cyan",
1555
+ paddingX: 1,
1556
+ children: [
1557
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "Legend" }),
1558
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1559
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginRight: 3, minWidth: 26, children: [
1560
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Status" }),
1561
+ healthLegend.map(([health, label]) => {
1562
+ const { glyph: icon, color } = healthDisplay[health];
1563
+ return /* @__PURE__ */ jsxs(Box, { children: [
1564
+ /* @__PURE__ */ jsx(Text, { color, bold: true, children: icon + " " }),
1565
+ /* @__PURE__ */ jsx(Text, { children: " " + label })
1566
+ ] }, health);
1567
+ }),
1568
+ TURN_LEGEND.map(([icon, color, label]) => /* @__PURE__ */ jsxs(Box, { children: [
1569
+ /* @__PURE__ */ jsx(Text, { color, bold: true, children: icon + " " }),
1570
+ /* @__PURE__ */ jsx(Text, { children: " " + label })
1571
+ ] }, icon)),
1572
+ /* @__PURE__ */ jsxs(Box, { children: [
1573
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "#FF8700", children: "\uF086 " }),
1574
+ /* @__PURE__ */ jsx(Text, { children: " Open-thread count" })
1575
+ ] })
1576
+ ] }),
1577
+ tabHelp ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginRight: 3, minWidth: 30, children: [
1578
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Tabs" }),
1579
+ tabHelp.map(([tab, meaning]) => /* @__PURE__ */ jsxs(Box, { children: [
1580
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", children: tab.padEnd(10) }),
1581
+ /* @__PURE__ */ jsx(Text, { children: meaning })
1582
+ ] }, tab))
1583
+ ] }) : null,
1584
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1585
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Keys" }),
1586
+ keys.map(([k, label]) => /* @__PURE__ */ jsxs(Box, { children: [
1587
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: k.padEnd(11) }),
1588
+ /* @__PURE__ */ jsx(Text, { children: label })
1589
+ ] }, k))
1590
+ ] })
1591
+ ] }),
1592
+ tabHelp ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Each item lands in the FIRST tab that claims it, so counts are residuals rather than totals." }) : null,
1593
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "esc \xB7 ? close" })
1594
+ ]
1595
+ }
1596
+ );
1597
+ };
1598
+ var ExplainModal = ({ item, login }) => /* @__PURE__ */ jsxs(
1599
+ Box,
1600
+ {
1601
+ flexDirection: "column",
1602
+ borderStyle: "round",
1603
+ borderColor: "cyan",
1604
+ paddingX: 1,
1605
+ width: Math.min(COLS, 78),
1606
+ children: [
1607
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", bold: true, children: `#${item.number} \xB7 ${item.repo}` }),
1608
+ /* @__PURE__ */ jsx(Text, { children: item.title }),
1609
+ explainItem(item, login).map((section) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1610
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: section.heading }),
1611
+ section.lines.map((line, i) => /* @__PURE__ */ jsx(Text, { children: " " + line }, i))
1612
+ ] }, section.heading)),
1613
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "esc \xB7 e close" }) })
1614
+ ]
1615
+ }
1616
+ );
1617
+ var RepoPicker = ({
1618
+ repos,
1619
+ selected,
1620
+ cursor
1621
+ }) => {
1622
+ const { rows } = useWindowSize();
1623
+ const budget = Math.max(6, rows - 12);
1624
+ const start = Math.max(
1625
+ 0,
1626
+ Math.min(cursor - Math.floor(budget / 2), repos.length - budget)
1627
+ );
1628
+ const visible = repos.slice(start, start + budget);
1629
+ return /* @__PURE__ */ jsxs(
1630
+ Box,
1631
+ {
1632
+ flexDirection: "column",
1633
+ borderStyle: "round",
1634
+ borderColor: "cyan",
1635
+ paddingX: 1,
1636
+ minWidth: 42,
1637
+ children: [
1638
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: `Filter by repo (${selected.size} on)` }),
1639
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(36) }),
1640
+ visible.map((repo, i) => {
1641
+ const idx = start + i;
1642
+ const on = selected.has(repo);
1643
+ const active = idx === cursor;
1644
+ return /* @__PURE__ */ jsxs(Box, { children: [
1645
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
1646
+ /* @__PURE__ */ jsx(Text, { color: on ? "green" : void 0, children: on ? "\u25C9 " : "\u25CB " }),
1647
+ /* @__PURE__ */ jsx(Text, { bold: active, children: repo })
1648
+ ] }, repo);
1649
+ }),
1650
+ repos.length > budget ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 ${repos.length} repos total` }) : null,
1651
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(36) }),
1652
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "space toggle \xB7 a clear all \xB7 \u21B5/esc done" })
1653
+ ]
1654
+ }
1655
+ );
1656
+ };
1657
+ var BrowseScreen = ({
1658
+ sections,
1659
+ login,
1660
+ jiraBase,
1661
+ jiraKeyRe,
1662
+ jiraTransitions,
1663
+ onRefresh,
1664
+ refreshing,
1665
+ hasPending,
1666
+ fetchedAt,
1667
+ workToggle,
1668
+ hidden,
1669
+ onOpenPr,
1670
+ onOpenIssue,
1671
+ onOpenExt,
1672
+ ciStatusState,
1673
+ ciJob,
1674
+ tabHelp,
1675
+ isWorkRepo,
1676
+ initialIncludeWork,
1677
+ brand
1678
+ }) => {
1679
+ const { rows } = useWindowSize();
1680
+ const [includeWork, setIncludeWork] = useState(
1681
+ () => workToggle ? initialIncludeWork ?? true : true
1682
+ );
1683
+ const applyWork = (secs, include) => !workToggle || !isWorkRepo ? secs : filterByOrigin(secs, include ? "work" : "home", isWorkRepo);
1684
+ const initialSections = applyWork(sections, includeWork);
1685
+ const [localSections, setLocalSections] = useState(initialSections);
1686
+ const [tabIdx, setTabIdx] = useState(0);
1687
+ const [cursors, setCursors] = useState(
1688
+ () => Object.fromEntries(initialSections.map((s) => [s.id, firstSelectable(s)]))
1689
+ );
1690
+ const [viewStarts, setViewStarts] = useState({});
1691
+ const safeTabIdx = Math.min(tabIdx, Math.max(0, localSections.length - 1));
1692
+ const activeId = localSections[safeTabIdx]?.id ?? "";
1693
+ const [flash, setFlash] = useState(null);
1694
+ const menu = useActionMenu();
1695
+ const [search, setSearch] = useState(null);
1696
+ const [searchInput, setSearchInput] = useState(false);
1697
+ const [repoFilter, setRepoFilter] = useState(/* @__PURE__ */ new Set());
1698
+ const [repoPicker, setRepoPicker] = useState(false);
1699
+ const [help, setHelp] = useState(false);
1700
+ const [explain, setExplain] = useState(false);
1701
+ const filterActive = search != null || repoFilter.size > 0;
1702
+ const reserveCiRow = ciStatusState != null;
1703
+ const ciStatus = ciStatusState?.kind === "ready" ? ciStatusState.status : null;
1704
+ const listHeight = Math.max(
1705
+ 5,
1706
+ // -10 rather than -8: the extra 2 are the frame's top and bottom border
1707
+ // rows, so the tree never grows taller than the terminal inside the frame.
1708
+ rows - 10 - (filterActive ? 2 : 0) - (reserveCiRow ? 2 : 0)
1709
+ );
1710
+ useEffect(() => {
1711
+ setCursors((p) => ({ ...p, [activeId]: 0 }));
1712
+ setViewStarts((p) => ({ ...p, [activeId]: 0 }));
1713
+ }, [search]);
1714
+ useEffect(() => {
1715
+ setLocalSections(applyWork(sections, includeWork));
1716
+ }, [sections]);
1717
+ useEffect(() => {
1718
+ setTabIdx((prev) => Math.min(prev, Math.max(0, localSections.length - 1)));
1719
+ setCursors(
1720
+ (prev) => Object.fromEntries(
1721
+ localSections.map((s) => {
1722
+ const c = Math.min(
1723
+ prev[s.id] ?? firstSelectable(s),
1724
+ s.items.length - 1
1725
+ );
1726
+ if (c < 0) return [s.id, 0];
1727
+ return [
1728
+ s.id,
1729
+ s.items[c]?.kind === "repo-header" || s.items[c]?.kind === "subgroup-header" ? moveCursor(s.items, c, 1) : c
1730
+ ];
1731
+ })
1732
+ )
1733
+ );
1734
+ setViewStarts(
1735
+ (prev) => Object.fromEntries(
1736
+ localSections.map((s) => [
1737
+ s.id,
1738
+ Math.min(prev[s.id] ?? 0, maxViewStart(s.items, listHeight))
1739
+ ])
1740
+ )
1741
+ );
1742
+ }, [localSections, listHeight]);
1743
+ const removeItemFromSections = (target) => setLocalSections((prev) => withoutItem(prev, target));
1744
+ const rawSection = localSections[safeTabIdx] ?? {
1745
+ id: "empty",
1746
+ label: "",
1747
+ items: []
1748
+ };
1749
+ const searched = search != null ? filterBySearch([rawSection], search) : [rawSection];
1750
+ const filtered = repoFilter.size > 0 ? filterByRepos(searched, repoFilter) : searched;
1751
+ const section = filterActive ? { ...rawSection, items: filtered[0]?.items ?? [] } : rawSection;
1752
+ const allRepos = reposInSections(localSections);
1753
+ const { cursor: repoCursor, setCursor: setRepoCursor } = useListCursor(
1754
+ allRepos.length,
1755
+ { vimKeys: false, isActive: repoPicker }
1756
+ );
1757
+ const cursor = cursors[activeId] ?? 0;
1758
+ const viewStart = viewStarts[activeId] ?? 0;
1759
+ const visibleCount = windowCount(section.items, viewStart, listHeight);
1760
+ const visibleItems = section.items.slice(viewStart, viewStart + visibleCount);
1761
+ const hasMore = viewStart + visibleCount < section.items.length;
1762
+ const activeItem = section.items[cursor];
1763
+ const showFlash = (msg) => {
1764
+ setFlash(msg);
1765
+ setTimeout(() => setFlash(null), 2e3);
1766
+ };
1767
+ const openDrillView = (item) => {
1768
+ const open = (fn, i) => {
1769
+ menu.close();
1770
+ fn(i);
1771
+ return true;
1772
+ };
1773
+ if (item.kind === "pr" && onOpenPr) return open(onOpenPr, item);
1774
+ if (item.kind === "issue" && onOpenIssue) return open(onOpenIssue, item);
1775
+ return false;
1776
+ };
1777
+ const openMenu = () => {
1778
+ if (!activeItem || activeItem.kind === "repo-header" || activeItem.kind === "subgroup-header")
1779
+ return;
1780
+ const actions = buildActions(
1781
+ activeItem,
1782
+ login,
1783
+ (msg) => {
1784
+ menu.close();
1785
+ showFlash(msg);
1786
+ },
1787
+ jiraBase,
1788
+ jiraKeyRe,
1789
+ jiraTransitions,
1790
+ onRefresh,
1791
+ removeItemFromSections,
1792
+ openDrillView
1793
+ );
1794
+ menu.open(actions);
1795
+ };
1796
+ useInput((input, key) => {
1797
+ if (hidden) return;
1798
+ if (key.ctrl || key.meta) return;
1799
+ if (help) {
1800
+ setHelp(false);
1801
+ return;
1802
+ }
1803
+ if (input === "?") {
1804
+ setHelp(true);
1805
+ return;
1806
+ }
1807
+ if (explain) {
1808
+ setExplain(false);
1809
+ return;
1810
+ }
1811
+ if (repoPicker) {
1812
+ if (key.escape || key.return) return setRepoPicker(false);
1813
+ if (input === "a") return setRepoFilter(/* @__PURE__ */ new Set());
1814
+ if (input === " ") {
1815
+ const repo = allRepos[repoCursor];
1816
+ if (repo)
1817
+ setRepoFilter((prev) => {
1818
+ const next = new Set(prev);
1819
+ if (next.has(repo)) next.delete(repo);
1820
+ else next.add(repo);
1821
+ return next;
1822
+ });
1823
+ }
1824
+ return;
1825
+ }
1826
+ if (searchInput) {
1827
+ if (key.return) return setSearchInput(false);
1828
+ if (key.escape) {
1829
+ setSearch(null);
1830
+ return setSearchInput(false);
1831
+ }
1832
+ if (key.backspace || key.delete)
1833
+ return setSearch((s) => (s ?? "").slice(0, -1));
1834
+ if (input && !key.ctrl && !key.meta && !key.tab)
1835
+ return setSearch((s) => (s ?? "") + input);
1836
+ return;
1837
+ }
1838
+ if (input === "/") {
1839
+ setSearch("");
1840
+ setSearchInput(true);
1841
+ return;
1842
+ }
1843
+ if (input === "f" && allRepos.length > 0) {
1844
+ setRepoCursor(0);
1845
+ setRepoPicker(true);
1846
+ return;
1847
+ }
1848
+ if (key.escape && search != null) {
1849
+ setSearch(null);
1850
+ return;
1851
+ }
1852
+ if (key.escape && repoFilter.size > 0) {
1853
+ setRepoFilter(/* @__PURE__ */ new Set());
1854
+ return;
1855
+ }
1856
+ if (menu.handleKey(key)) return;
1857
+ if (key.upArrow) {
1858
+ const next = moveCursor(section.items, cursor, -1);
1859
+ const newVs = Math.min(viewStart, withHeaders(section.items, next));
1860
+ setCursors((p) => ({ ...p, [activeId]: next }));
1861
+ setViewStarts((p) => ({ ...p, [activeId]: newVs }));
1862
+ }
1863
+ if (key.downArrow) {
1864
+ const next = moveCursor(section.items, cursor, 1);
1865
+ let newVs = viewStart;
1866
+ while (next >= newVs + windowCount(section.items, newVs, listHeight))
1867
+ newVs++;
1868
+ setCursors((p) => ({ ...p, [activeId]: next }));
1869
+ setViewStarts((p) => ({ ...p, [activeId]: newVs }));
1870
+ }
1871
+ if (key.leftArrow) setTabIdx((i) => Math.max(0, i - 1));
1872
+ if (key.rightArrow)
1873
+ setTabIdx((i) => Math.min(localSections.length - 1, i + 1));
1874
+ if (key.tab)
1875
+ setTabIdx(
1876
+ (i) => (i + (key.shift ? -1 : 1) + localSections.length) % localSections.length
1877
+ );
1878
+ if (input === "q") process.exit(0);
1879
+ if (input === "r") {
1880
+ onRefresh?.();
1881
+ return;
1882
+ }
1883
+ if (workToggle && input === "w") {
1884
+ const next = !includeWork;
1885
+ const nextSections = applyWork(sections, next);
1886
+ const keptIdx = nextSections.findIndex((s) => s.id === activeId);
1887
+ setIncludeWork(next);
1888
+ setLocalSections(nextSections);
1889
+ if (keptIdx >= 0) setTabIdx(keptIdx);
1890
+ return;
1891
+ }
1892
+ if (input === "J" && ciStatus) {
1893
+ onOpenExt?.("jenkins", ciStatus.job);
1894
+ return;
1895
+ }
1896
+ if (!activeItem || activeItem.kind === "repo-header" || activeItem.kind === "subgroup-header")
1897
+ return;
1898
+ if (key.return && activeItem.kind === "show-more") {
1899
+ setLocalSections(
1900
+ (prev) => prev.map((s) => ({
1901
+ ...s,
1902
+ items: s.items.flatMap(
1903
+ (i) => i === activeItem ? [
1904
+ ...activeItem.hidden,
1905
+ {
1906
+ kind: "show-less",
1907
+ toHide: activeItem.hidden,
1908
+ indent: true
1909
+ }
1910
+ ] : [i]
1911
+ )
1912
+ }))
1913
+ );
1914
+ return;
1915
+ }
1916
+ if (key.return && activeItem.kind === "show-less") {
1917
+ setLocalSections(
1918
+ (prev) => prev.map((s) => ({
1919
+ ...s,
1920
+ items: s.items.filter((i) => !activeItem.toHide.includes(i)).flatMap(
1921
+ (i) => i === activeItem ? [
1922
+ {
1923
+ kind: "show-more",
1924
+ hidden: activeItem.toHide,
1925
+ indent: true
1926
+ }
1927
+ ] : [i]
1928
+ )
1929
+ }))
1930
+ );
1931
+ return;
1932
+ }
1933
+ if (activeItem.kind === "show-more" || activeItem.kind === "show-less")
1934
+ return;
1935
+ if (key.return) {
1936
+ if (openDrillView(activeItem)) return;
1937
+ openMenu();
1938
+ return;
1939
+ }
1940
+ if (input === "m") {
1941
+ openMenu();
1942
+ return;
1943
+ }
1944
+ if (input === "e" && activeItem && (activeItem.kind === "pr" || activeItem.kind === "issue")) {
1945
+ setExplain(true);
1946
+ return;
1947
+ }
1948
+ if (input === "o") {
1949
+ $`open ${activeItem.url}`.catch(() => {
1950
+ });
1951
+ const label = activeItem.kind === "jira" ? activeItem.key : `#${activeItem.number}`;
1952
+ showFlash(`\u2197 Opened ${label}`);
1953
+ return;
1954
+ }
1955
+ if (input === "c") {
1956
+ clipboard(activeItem.url);
1957
+ const label = activeItem.kind === "jira" ? activeItem.key : `#${activeItem.number}`;
1958
+ showFlash(`\u2713 Copied URL for ${label}`);
1959
+ return;
1960
+ }
1961
+ if (input === "d") {
1962
+ if (openDrillView(activeItem)) return;
1963
+ const cmd = drillCmd(activeItem);
1964
+ if (cmd) {
1965
+ void runInPane(cmd).catch(() => {
1966
+ });
1967
+ showFlash(`\u2197 ${drillLabel(activeItem)}`);
1968
+ }
1969
+ return;
1970
+ }
1971
+ if (input === "b" && activeItem.kind === "pr" && activeItem.branch) {
1972
+ clipboard(activeItem.branch);
1973
+ showFlash(`\u2713 Copied ${activeItem.branch}`);
1974
+ return;
1975
+ }
1976
+ if (activeItem.kind !== "jira") {
1977
+ if (input === "s" && activeItem.kind === "pr" && activeItem.branch) {
1978
+ const script = [
1979
+ "delay 0.5",
1980
+ 'tell application "iTerm2"',
1981
+ " tell current session of current window",
1982
+ ` write text "git switch ${activeItem.branch}"`,
1983
+ " end tell",
1984
+ "end tell"
1985
+ ].join("\n");
1986
+ const proc = spawn("osascript", ["-e", script], {
1987
+ detached: true,
1988
+ stdio: "ignore"
1989
+ });
1990
+ proc.unref();
1991
+ process.exit(0);
1992
+ }
1993
+ if (input === "j" && (activeItem.kind === "pr" || activeItem.kind === "issue")) {
1994
+ const { repo } = activeItem;
1995
+ const branch = activeItem.kind === "pr" ? activeItem.branch ?? "" : "";
1996
+ showFlash(`\u22EF Opening ${repo}\u2026`);
1997
+ void jumpToRepo(repo, branch, login).then(() => showFlash(`\u2197 Opened ${repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
1998
+ return;
1999
+ }
2000
+ }
2001
+ if (input === "t" && activeItem && activeItem.kind === "jira" && jiraTransitions && jiraTransitions.length > 0) {
2002
+ const jiraKey = activeItem.key;
2003
+ const actions = jiraTransitions.map(
2004
+ ({ label, state, resolutions }) => resolutions && resolutions.length > 0 ? {
2005
+ label,
2006
+ hint: "",
2007
+ run: () => {
2008
+ },
2009
+ subActions: resolutions.map((resolution) => ({
2010
+ label: resolution,
2011
+ hint: "",
2012
+ run: () => {
2013
+ menu.close();
2014
+ showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
2015
+ $`jira issue move ${jiraKey} ${state} --resolution ${resolution}`.then(() => {
2016
+ showFlash(`\u2713 ${label} \xB7 ${resolution}`);
2017
+ setTimeout(() => onRefresh?.(), 1500);
2018
+ }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
2019
+ }
2020
+ }))
2021
+ } : {
2022
+ label,
2023
+ hint: "",
2024
+ run: () => {
2025
+ menu.close();
2026
+ showFlash(`\u22EF Moving to ${label}\u2026`);
2027
+ $`jira issue move ${jiraKey} ${state}`.then(() => {
2028
+ showFlash(`\u2713 Moved to ${label}`);
2029
+ setTimeout(() => onRefresh?.(), 1500);
2030
+ }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
2031
+ }
2032
+ }
2033
+ );
2034
+ menu.open(actions);
2035
+ }
2036
+ });
2037
+ const hints = [
2038
+ ["\u2191\u2193", "nav"],
2039
+ ["\u2190\u2192", "tab"],
2040
+ ["\u21B5/d", "open"],
2041
+ ["m", "actions"],
2042
+ ["e", "explain"],
2043
+ ["/", "search"],
2044
+ ["f", "filter"],
2045
+ ["r", "refresh"],
2046
+ ...ciStatus ? [["J", "jenkins"]] : [],
2047
+ ["?", "help"],
2048
+ ["q", "quit"]
2049
+ ];
2050
+ const matchCount = section.items.filter(
2051
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
2052
+ ).length;
2053
+ if (hidden) return null;
2054
+ return /* @__PURE__ */ jsxs(
2055
+ Box,
2056
+ {
2057
+ flexDirection: "column",
2058
+ marginTop: 1,
2059
+ borderStyle: "round",
2060
+ borderColor: FRAME_COLOR,
2061
+ borderDimColor: true,
2062
+ paddingX: FRAME_PAD_X,
2063
+ children: [
2064
+ /* @__PURE__ */ jsx(
2065
+ InboxHeader,
2066
+ {
2067
+ brand,
2068
+ sections: localSections,
2069
+ login,
2070
+ work: workToggle ? includeWork : void 0,
2071
+ refreshing,
2072
+ hasPending,
2073
+ fetchedAt
2074
+ }
2075
+ ),
2076
+ ciStatusState ? /* @__PURE__ */ jsx(CiStatusLine, { state: ciStatusState, job: ciJob }) : null,
2077
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
2078
+ Tabs,
2079
+ {
2080
+ active: section.id,
2081
+ items: localSections.map((s) => ({
2082
+ value: s.id,
2083
+ label: s.label,
2084
+ count: topLevelCount(s)
2085
+ }))
2086
+ }
2087
+ ) }),
2088
+ search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
2089
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " / " }),
2090
+ /* @__PURE__ */ jsx(Text, { children: search }),
2091
+ searchInput ? /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u258F" }) : null,
2092
+ /* @__PURE__ */ jsx(
2093
+ Text,
2094
+ {
2095
+ dimColor: true,
2096
+ children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}`
2097
+ }
2098
+ )
2099
+ ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
2100
+ /* @__PURE__ */ jsx(Text, { color: "#FF8700", children: " \u25C9 " }),
2101
+ /* @__PURE__ */ jsx(Text, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
2102
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " f edit \xB7 esc clear" })
2103
+ ] }) : null,
2104
+ help ? /* @__PURE__ */ jsx(
2105
+ Box,
2106
+ {
2107
+ minHeight: listHeight,
2108
+ flexDirection: "column",
2109
+ justifyContent: "center",
2110
+ alignItems: "center",
2111
+ children: /* @__PURE__ */ jsx(
2112
+ HelpModal,
2113
+ {
2114
+ workToggle,
2115
+ hasCi: ciStatus != null,
2116
+ hasJira: !!jiraBase,
2117
+ tabHelp
2118
+ }
2119
+ )
2120
+ }
2121
+ ) : explain && activeItem && (activeItem.kind === "pr" || activeItem.kind === "issue") ? /* @__PURE__ */ jsx(
2122
+ Box,
2123
+ {
2124
+ minHeight: listHeight,
2125
+ flexDirection: "column",
2126
+ justifyContent: "center",
2127
+ alignItems: "center",
2128
+ children: /* @__PURE__ */ jsx(ExplainModal, { item: activeItem, login })
2129
+ }
2130
+ ) : repoPicker ? /* @__PURE__ */ jsx(
2131
+ Box,
2132
+ {
2133
+ minHeight: listHeight,
2134
+ flexDirection: "column",
2135
+ justifyContent: "center",
2136
+ alignItems: "center",
2137
+ children: /* @__PURE__ */ jsx(
2138
+ RepoPicker,
2139
+ {
2140
+ repos: allRepos,
2141
+ selected: repoFilter,
2142
+ cursor: Math.min(repoCursor, Math.max(0, allRepos.length - 1))
2143
+ }
2144
+ )
2145
+ }
2146
+ ) : menu.actions && activeItem && activeItem.kind !== "repo-header" ? /* @__PURE__ */ jsx(
2147
+ Box,
2148
+ {
2149
+ minHeight: listHeight,
2150
+ flexDirection: "column",
2151
+ justifyContent: "center",
2152
+ alignItems: "center",
2153
+ children: /* @__PURE__ */ jsx(
2154
+ ActionMenu,
2155
+ {
2156
+ item: activeItem,
2157
+ actions: menu.actions,
2158
+ cursor: menu.cursor
2159
+ }
2160
+ )
2161
+ }
2162
+ ) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", minHeight: listHeight, children: [
2163
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, children: visibleItems.map((item, i) => /* @__PURE__ */ jsx(
2164
+ ItemRow,
2165
+ {
2166
+ item,
2167
+ active: viewStart + i === cursor,
2168
+ login,
2169
+ gap: viewStart + i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2170
+ // first child — in Other PRs that's "free" because the
2171
+ // child is itself a repo-header (gap above). A jira row
2172
+ // has no such stand-in, so it needs this explicitly. Never
2173
+ // applies between two tickets/PRs — only right after a
2174
+ // header.
2175
+ item.kind === "jira" && ["repo-header", "subgroup-header"].includes(
2176
+ section.items[viewStart + i - 1]?.kind
2177
+ ))
2178
+ },
2179
+ `${viewStart + i}:${item.kind === "jira" ? item.instanceKey ?? item.key : item.kind === "repo-header" ? `header:${item.repo}` : item.kind === "subgroup-header" ? `subgroup:${item.label}` : item.kind === "show-more" ? `show-more:${item.hidden[0]?.repo ?? i}` : item.kind === "show-less" ? `show-less:${item.toHide[0]?.repo ?? i}` : `${item.repo}/${item.number}`}`
2180
+ )) }),
2181
+ hasMore && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2182
+ " ",
2183
+ "\u2193 ",
2184
+ section.items.length - viewStart - visibleCount,
2185
+ " more"
2186
+ ] })
2187
+ ] }),
2188
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text, { color: "green", children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
2189
+ ]
2190
+ }
2191
+ );
2192
+ };
2193
+ var signatureOf = (sections) => JSON.stringify(sections, (k, v) => k === "age" ? void 0 : v);
2194
+ var App = ({
2195
+ fetcher,
2196
+ cacheKey,
2197
+ title = "inbox",
2198
+ detailFor,
2199
+ isWorkRepo,
2200
+ initialIncludeWork,
2201
+ jiraBase,
2202
+ jiraKeyRe,
2203
+ jiraTransitions,
2204
+ workToggle,
2205
+ hasCiStatus,
2206
+ ciJob,
2207
+ ciFetcher,
2208
+ ciPollMs = 6e4,
2209
+ extensions,
2210
+ tabHelp
2211
+ }) => {
2212
+ const [state, setState] = useState({ phase: "loading" });
2213
+ const [pending, setPending] = useState(null);
2214
+ const [refreshing, setRefreshing] = useState(false);
2215
+ const [fetchedAt, setFetchedAt] = useState(null);
2216
+ const [ciStatusState, setCiStatusState] = useState({
2217
+ kind: "loading"
2218
+ });
2219
+ const applyCiStatus = (status) => setCiStatusState((prev) => {
2220
+ const next = toCiStatusState(status);
2221
+ return sameCiStatusState(prev, next) ? prev : next;
2222
+ });
2223
+ const displayedKey = useRef("");
2224
+ const showData = (sections, login) => {
2225
+ displayedKey.current = signatureOf(sections);
2226
+ setPending(null);
2227
+ setFetchedAt(Date.now());
2228
+ setState({ phase: "browse", sections, login });
2229
+ };
2230
+ const revalidate = (manual = false) => {
2231
+ if (manual) setRefreshing(true);
2232
+ fetcher().then((fresh) => {
2233
+ if (cacheKey) writeCache(cacheKey, fresh);
2234
+ setRefreshing(false);
2235
+ setFetchedAt(Date.now());
2236
+ if (hasCiStatus) applyCiStatus(fresh.ciStatus ?? null);
2237
+ const freshKey = signatureOf(fresh.sections);
2238
+ if (!displayedKey.current) {
2239
+ if (fresh.sections.length === 0) {
2240
+ console.log(`${title[0].toUpperCase()}${title.slice(1)} empty.`);
2241
+ process.exit(0);
2242
+ }
2243
+ showData(fresh.sections, fresh.login);
2244
+ } else if (freshKey !== displayedKey.current) {
2245
+ setPending(fresh);
2246
+ } else {
2247
+ setPending(null);
2248
+ }
2249
+ }).catch((err) => {
2250
+ setRefreshing(false);
2251
+ if (!displayedKey.current) {
2252
+ console.error("Error:", err.message);
2253
+ process.exit(1);
2254
+ }
2255
+ });
2256
+ };
2257
+ const applyOrRefresh = () => {
2258
+ if (pending) showData(pending.sections, pending.login);
2259
+ else revalidate(true);
2260
+ };
2261
+ useEffect(() => {
2262
+ const cached = cacheKey ? readCache(cacheKey) : null;
2263
+ if (cached && cached.sections.length > 0) {
2264
+ displayedKey.current = signatureOf(cached.sections);
2265
+ setFetchedAt(cached.at);
2266
+ setState({
2267
+ phase: "browse",
2268
+ sections: cached.sections,
2269
+ login: cached.login
2270
+ });
2271
+ }
2272
+ revalidate();
2273
+ }, []);
2274
+ useEffect(() => {
2275
+ if (!hasCiStatus || !ciFetcher) return;
2276
+ let live = true;
2277
+ const poll = () => {
2278
+ ciFetcher().then((status) => {
2279
+ if (!live) return;
2280
+ applyCiStatus(status);
2281
+ }).catch(() => {
2282
+ });
2283
+ };
2284
+ poll();
2285
+ const id = setInterval(poll, ciPollMs);
2286
+ return () => {
2287
+ live = false;
2288
+ clearInterval(id);
2289
+ };
2290
+ }, [hasCiStatus, ciFetcher, ciPollMs]);
2291
+ if (state.phase === "loading")
2292
+ return /* @__PURE__ */ jsxs(
2293
+ Box,
2294
+ {
2295
+ flexDirection: "column",
2296
+ marginTop: 1,
2297
+ borderStyle: "round",
2298
+ borderColor: FRAME_COLOR,
2299
+ borderDimColor: true,
2300
+ paddingX: FRAME_PAD_X,
2301
+ children: [
2302
+ /* @__PURE__ */ jsx(
2303
+ InboxHeader,
2304
+ {
2305
+ brand: brandOf(title),
2306
+ sections: [],
2307
+ login: "",
2308
+ work: workToggle ? initialIncludeWork ?? true : void 0,
2309
+ loading: true
2310
+ }
2311
+ ),
2312
+ hasCiStatus ? /* @__PURE__ */ jsx(CiStatusLine, { state: ciStatusState, job: ciJob }) : null,
2313
+ /* @__PURE__ */ jsx(LoadingScreen, { label: `Fetching ${title}\u2026` })
2314
+ ]
2315
+ }
2316
+ );
2317
+ const overlay = state.phase === "pr" ? { kind: "pr", item: state.item } : state.phase === "issue" ? { kind: "issue", item: state.item } : state.phase === "ext" ? {
2318
+ kind: "ext",
2319
+ extId: state.extId,
2320
+ target: state.target
2321
+ } : null;
2322
+ const toBrowse = () => setState({ phase: "browse", sections: state.sections, login: state.login });
2323
+ const removeAndReturn = (target) => setState({
2324
+ phase: "browse",
2325
+ sections: withoutItem(state.sections, target),
2326
+ login: state.login
2327
+ });
2328
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2329
+ /* @__PURE__ */ jsx(
2330
+ BrowseScreen,
2331
+ {
2332
+ brand: brandOf(title),
2333
+ sections: state.sections,
2334
+ login: state.login,
2335
+ jiraBase,
2336
+ jiraKeyRe,
2337
+ jiraTransitions,
2338
+ onRefresh: applyOrRefresh,
2339
+ refreshing,
2340
+ hasPending: pending !== null,
2341
+ fetchedAt,
2342
+ workToggle,
2343
+ isWorkRepo,
2344
+ initialIncludeWork,
2345
+ hidden: overlay !== null,
2346
+ ciStatusState: hasCiStatus ? ciStatusState : void 0,
2347
+ ciJob,
2348
+ tabHelp,
2349
+ onOpenPr: (item) => setState({
2350
+ phase: "pr",
2351
+ item,
2352
+ sections: state.sections,
2353
+ login: state.login
2354
+ }),
2355
+ onOpenIssue: (item) => setState({
2356
+ phase: "issue",
2357
+ item,
2358
+ sections: state.sections,
2359
+ login: state.login
2360
+ }),
2361
+ onOpenExt: (id, target) => setState({
2362
+ phase: "ext",
2363
+ extId: id,
2364
+ target,
2365
+ sections: state.sections,
2366
+ login: state.login
2367
+ })
2368
+ }
2369
+ ),
2370
+ (overlay?.kind === "pr" || overlay?.kind === "issue") && detailFor?.({
2371
+ item: overlay.item,
2372
+ kind: overlay.kind,
2373
+ login: state.login,
2374
+ onBack: toBrowse,
2375
+ onRefresh: applyOrRefresh,
2376
+ onRemove: removeAndReturn
2377
+ }),
2378
+ overlay?.kind === "ext" && extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target)
2379
+ ] });
2380
+ };
602
2381
 
603
- export { CommentsPanel, HealthPanel, healthColor, healthDisplay, healthGlyph, healthLegend, renderMarkdown };
2382
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, clipboard, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, maxViewStart, moveCursor, openInTab, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, windowCount, withHeaders, withoutItem, writeCache };