@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.d.ts +202 -2
- package/dist/index.js +1782 -3
- package/package.json +3 -2
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 };
|