@kud/gh-ink 0.3.0 → 0.4.0

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 CHANGED
@@ -33,11 +33,16 @@ declare const healthGlyph: (h: Health) => string;
33
33
  declare const healthColor: (h: Health) => string;
34
34
  declare const healthLegend: [Health, string][];
35
35
 
36
+ type ExtensionTarget = {
37
+ item?: AnyItem;
38
+ ciJob?: string;
39
+ login: string;
40
+ };
36
41
  interface InboxExtension {
37
42
  id: string;
38
43
  title: string;
39
44
  key: string;
40
- body: (onExit: () => void, target?: string) => ReactNode;
45
+ body: (onExit: () => void, target?: ExtensionTarget) => ReactNode;
41
46
  }
42
47
 
43
48
  type GHDetail = {
@@ -233,4 +238,4 @@ declare const writeCache: (key: string, data: {
233
238
  login: string;
234
239
  }) => void;
235
240
 
236
- export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxExtension, type JiraRow, type JiraTransition, type RepoHeader, type Section, type ShowLess, type ShowMore, type SubgroupHeader, 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 };
241
+ export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxExtension, type JiraRow, type JiraTransition, type RepoHeader, type Section, type ShowLess, type ShowMore, type SubgroupHeader, 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 };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React2, { useState, useRef, useEffect } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
- import { colors, ScrollView, TextInput, LoadingScreen, Switch, Tabs, FooterHints } 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
6
  import { $ } from 'zx';
@@ -622,6 +622,7 @@ var writeCache = (key, data) => {
622
622
  } catch {
623
623
  }
624
624
  };
625
+ var quietly = $({ quiet: true });
625
626
  var relativeTime = (iso) => {
626
627
  const diff = (Date.now() - new Date(iso).getTime()) / 1e3;
627
628
  if (diff < 3600) return `${Math.floor(diff / 60)}m`;
@@ -847,6 +848,12 @@ var maxViewStart = (items, budget) => {
847
848
  start++;
848
849
  return start;
849
850
  };
851
+ var firstSelectable = (section) => Math.max(
852
+ 0,
853
+ section.items.findIndex(
854
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
855
+ )
856
+ );
850
857
  var withHeaders = (items, idx) => {
851
858
  let start = idx;
852
859
  while (start > 0 && (items[start - 1].kind === "repo-header" || items[start - 1].kind === "subgroup-header"))
@@ -1065,7 +1072,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1065
1072
  label: "Open in browser",
1066
1073
  hint: "o",
1067
1074
  run: () => {
1068
- $`open ${item.url}`.catch(() => {
1075
+ quietly`open ${item.url}`.catch(() => {
1069
1076
  });
1070
1077
  showFlash("\u2197 Opened in browser");
1071
1078
  }
@@ -1112,7 +1119,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1112
1119
  hint: "",
1113
1120
  run: () => {
1114
1121
  showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
1115
- void $`jira issue move ${item.key} ${state} --resolution ${resolution}`.then(() => {
1122
+ void quietly`jira issue move ${item.key} ${state} --resolution ${resolution}`.then(() => {
1116
1123
  showFlash(`\u2713 ${label} \xB7 ${resolution}`);
1117
1124
  setTimeout(() => onRefresh?.(), 1500);
1118
1125
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
@@ -1123,7 +1130,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1123
1130
  hint: "",
1124
1131
  run: () => {
1125
1132
  showFlash(`\u22EF Moving to ${label}\u2026`);
1126
- void $`jira issue move ${item.key} ${state}`.then(() => {
1133
+ void quietly`jira issue move ${item.key} ${state}`.then(() => {
1127
1134
  showFlash(`\u2713 Moved to ${label}`);
1128
1135
  setTimeout(() => onRefresh?.(), 1500);
1129
1136
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
@@ -1204,7 +1211,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1204
1211
  run: () => {
1205
1212
  onRemove?.(item);
1206
1213
  showFlash(`\u2713 Closed #${item.number}`);
1207
- void $`gh issue close ${item.number} --repo ${item.repo}`.catch(
1214
+ void quietly`gh issue close ${item.number} --repo ${item.repo}`.catch(
1208
1215
  () => {
1209
1216
  showFlash(`\u2717 Close failed \u2014 restoring #${item.number}`);
1210
1217
  onRefresh?.();
@@ -1241,7 +1248,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1241
1248
  label: `Open ${jiraKey} in Jira`,
1242
1249
  hint: "t",
1243
1250
  run: () => {
1244
- $`open ${jiraBase}/${jiraKey}`.catch(() => {
1251
+ quietly`open ${jiraBase}/${jiraKey}`.catch(() => {
1245
1252
  });
1246
1253
  showFlash(`\u2197 Opened ${jiraKey} in Jira`);
1247
1254
  }
@@ -1260,10 +1267,12 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1260
1267
  run: () => {
1261
1268
  onRemove?.(item);
1262
1269
  showFlash(`\u2713 Closed #${item.number}`);
1263
- void $`gh pr close ${item.number} --repo ${item.repo}`.catch(() => {
1264
- showFlash(`\u2717 Close failed \u2014 restoring #${item.number}`);
1265
- onRefresh?.();
1266
- });
1270
+ void quietly`gh pr close ${item.number} --repo ${item.repo}`.catch(
1271
+ () => {
1272
+ showFlash(`\u2717 Close failed \u2014 restoring #${item.number}`);
1273
+ onRefresh?.();
1274
+ }
1275
+ );
1267
1276
  }
1268
1277
  },
1269
1278
  { label: "Cancel", hint: "", run: () => {
@@ -1283,8 +1292,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1283
1292
  run: () => {
1284
1293
  onRemove?.(item);
1285
1294
  showFlash(`\u2713 Closed #${item.number} and deleted ${item.branch}`);
1286
- void $`gh pr close ${item.number} --repo ${item.repo}`.then(
1287
- () => $`gh api -X DELETE ${`repos/${item.repo}/git/refs/heads/${item.branch}`}`
1295
+ void quietly`gh pr close ${item.number} --repo ${item.repo}`.then(
1296
+ () => quietly`gh api -X DELETE ${`repos/${item.repo}/git/refs/heads/${item.branch}`}`
1288
1297
  ).catch(() => {
1289
1298
  showFlash(`\u2717 Close + delete failed \u2014 restoring`);
1290
1299
  onRefresh?.();
@@ -1648,6 +1657,7 @@ var RepoPicker = ({
1648
1657
  }
1649
1658
  );
1650
1659
  };
1660
+ var extensionFor = (input, extensions) => input ? extensions?.find((e) => e.key === input) : void 0;
1651
1661
  var BrowseScreen = ({
1652
1662
  sections,
1653
1663
  login,
@@ -1663,6 +1673,7 @@ var BrowseScreen = ({
1663
1673
  onOpenPr,
1664
1674
  onOpenIssue,
1665
1675
  onOpenExt,
1676
+ extensions,
1666
1677
  ciStatusState,
1667
1678
  ciJob,
1668
1679
  tabHelp,
@@ -1679,25 +1690,17 @@ var BrowseScreen = ({
1679
1690
  const [localSections, setLocalSections] = useState(initialSections);
1680
1691
  const [tabIdx, setTabIdx] = useState(0);
1681
1692
  const [cursors, setCursors] = useState(
1682
- initialSections.map(
1683
- (s) => Math.max(
1684
- 0,
1685
- s.items.findIndex(
1686
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
1687
- )
1688
- )
1689
- )
1690
- );
1691
- const [viewStarts, setViewStarts] = useState(
1692
- initialSections.map(() => 0)
1693
+ () => Object.fromEntries(initialSections.map((s) => [s.id, firstSelectable(s)]))
1693
1694
  );
1695
+ const [viewStarts, setViewStarts] = useState({});
1696
+ const safeTabIdx = Math.min(tabIdx, Math.max(0, localSections.length - 1));
1697
+ const activeId = localSections[safeTabIdx]?.id ?? "";
1694
1698
  const [flash, setFlash] = useState(null);
1695
1699
  const menu = useActionMenu();
1696
1700
  const [search, setSearch] = useState(null);
1697
1701
  const [searchInput, setSearchInput] = useState(false);
1698
1702
  const [repoFilter, setRepoFilter] = useState(/* @__PURE__ */ new Set());
1699
1703
  const [repoPicker, setRepoPicker] = useState(false);
1700
- const [repoCursor, setRepoCursor] = useState(0);
1701
1704
  const [help, setHelp] = useState(false);
1702
1705
  const [explain, setExplain] = useState(false);
1703
1706
  const filterActive = search != null || repoFilter.size > 0;
@@ -1710,8 +1713,8 @@ var BrowseScreen = ({
1710
1713
  rows - 10 - (filterActive ? 2 : 0) - (reserveCiRow ? 2 : 0)
1711
1714
  );
1712
1715
  useEffect(() => {
1713
- setCursors((p) => p.map((c, i) => i === tabIdx ? 0 : c));
1714
- setViewStarts((p) => p.map((v, i) => i === tabIdx ? 0 : v));
1716
+ setCursors((p) => ({ ...p, [activeId]: 0 }));
1717
+ setViewStarts((p) => ({ ...p, [activeId]: 0 }));
1715
1718
  }, [search]);
1716
1719
  useEffect(() => {
1717
1720
  setLocalSections(applyWork(sections, includeWork));
@@ -1719,20 +1722,30 @@ var BrowseScreen = ({
1719
1722
  useEffect(() => {
1720
1723
  setTabIdx((prev) => Math.min(prev, Math.max(0, localSections.length - 1)));
1721
1724
  setCursors(
1722
- (prev) => localSections.map((s, i) => {
1723
- const c = Math.min(prev[i] ?? 0, s.items.length - 1);
1724
- if (c < 0) return 0;
1725
- return s.items[c]?.kind === "repo-header" || s.items[c]?.kind === "subgroup-header" ? moveCursor(s.items, c, 1) : c;
1726
- })
1725
+ (prev) => Object.fromEntries(
1726
+ localSections.map((s) => {
1727
+ const c = Math.min(
1728
+ prev[s.id] ?? firstSelectable(s),
1729
+ s.items.length - 1
1730
+ );
1731
+ if (c < 0) return [s.id, 0];
1732
+ return [
1733
+ s.id,
1734
+ s.items[c]?.kind === "repo-header" || s.items[c]?.kind === "subgroup-header" ? moveCursor(s.items, c, 1) : c
1735
+ ];
1736
+ })
1737
+ )
1727
1738
  );
1728
1739
  setViewStarts(
1729
- (prev) => localSections.map(
1730
- (s, i) => Math.min(prev[i] ?? 0, maxViewStart(s.items, listHeight))
1740
+ (prev) => Object.fromEntries(
1741
+ localSections.map((s) => [
1742
+ s.id,
1743
+ Math.min(prev[s.id] ?? 0, maxViewStart(s.items, listHeight))
1744
+ ])
1731
1745
  )
1732
1746
  );
1733
1747
  }, [localSections, listHeight]);
1734
1748
  const removeItemFromSections = (target) => setLocalSections((prev) => withoutItem(prev, target));
1735
- const safeTabIdx = Math.min(tabIdx, Math.max(0, localSections.length - 1));
1736
1749
  const rawSection = localSections[safeTabIdx] ?? {
1737
1750
  id: "empty",
1738
1751
  label: "",
@@ -1742,8 +1755,12 @@ var BrowseScreen = ({
1742
1755
  const filtered = repoFilter.size > 0 ? filterByRepos(searched, repoFilter) : searched;
1743
1756
  const section = filterActive ? { ...rawSection, items: filtered[0]?.items ?? [] } : rawSection;
1744
1757
  const allRepos = reposInSections(localSections);
1745
- const cursor = cursors[safeTabIdx] ?? 0;
1746
- const viewStart = viewStarts[safeTabIdx] ?? 0;
1758
+ const { cursor: repoCursor, setCursor: setRepoCursor } = useListCursor(
1759
+ allRepos.length,
1760
+ { vimKeys: false, isActive: repoPicker }
1761
+ );
1762
+ const cursor = cursors[activeId] ?? 0;
1763
+ const viewStart = viewStarts[activeId] ?? 0;
1747
1764
  const visibleCount = windowCount(section.items, viewStart, listHeight);
1748
1765
  const visibleItems = section.items.slice(viewStart, viewStart + visibleCount);
1749
1766
  const hasMore = viewStart + visibleCount < section.items.length;
@@ -1798,9 +1815,6 @@ var BrowseScreen = ({
1798
1815
  }
1799
1816
  if (repoPicker) {
1800
1817
  if (key.escape || key.return) return setRepoPicker(false);
1801
- if (key.upArrow) return setRepoCursor((c) => Math.max(0, c - 1));
1802
- if (key.downArrow)
1803
- return setRepoCursor((c) => Math.min(allRepos.length - 1, c + 1));
1804
1818
  if (input === "a") return setRepoFilter(/* @__PURE__ */ new Set());
1805
1819
  if (input === " ") {
1806
1820
  const repo = allRepos[repoCursor];
@@ -1848,16 +1862,16 @@ var BrowseScreen = ({
1848
1862
  if (key.upArrow) {
1849
1863
  const next = moveCursor(section.items, cursor, -1);
1850
1864
  const newVs = Math.min(viewStart, withHeaders(section.items, next));
1851
- setCursors((p) => p.map((c, i) => i === tabIdx ? next : c));
1852
- setViewStarts((p) => p.map((v, i) => i === tabIdx ? newVs : v));
1865
+ setCursors((p) => ({ ...p, [activeId]: next }));
1866
+ setViewStarts((p) => ({ ...p, [activeId]: newVs }));
1853
1867
  }
1854
1868
  if (key.downArrow) {
1855
1869
  const next = moveCursor(section.items, cursor, 1);
1856
1870
  let newVs = viewStart;
1857
1871
  while (next >= newVs + windowCount(section.items, newVs, listHeight))
1858
1872
  newVs++;
1859
- setCursors((p) => p.map((c, i) => i === tabIdx ? next : c));
1860
- setViewStarts((p) => p.map((v, i) => i === tabIdx ? newVs : v));
1873
+ setCursors((p) => ({ ...p, [activeId]: next }));
1874
+ setViewStarts((p) => ({ ...p, [activeId]: newVs }));
1861
1875
  }
1862
1876
  if (key.leftArrow) setTabIdx((i) => Math.max(0, i - 1));
1863
1877
  if (key.rightArrow)
@@ -1873,12 +1887,20 @@ var BrowseScreen = ({
1873
1887
  }
1874
1888
  if (workToggle && input === "w") {
1875
1889
  const next = !includeWork;
1890
+ const nextSections = applyWork(sections, next);
1891
+ const keptIdx = nextSections.findIndex((s) => s.id === activeId);
1876
1892
  setIncludeWork(next);
1877
- setLocalSections(applyWork(sections, next));
1893
+ setLocalSections(nextSections);
1894
+ if (keptIdx >= 0) setTabIdx(keptIdx);
1878
1895
  return;
1879
1896
  }
1880
- if (input === "J" && ciStatus) {
1881
- onOpenExt?.("jenkins", ciStatus.job);
1897
+ const ext = extensionFor(input, extensions);
1898
+ if (ext) {
1899
+ onOpenExt?.(ext.id, {
1900
+ item: activeItem ?? void 0,
1901
+ ciJob: ciStatus?.job,
1902
+ login
1903
+ });
1882
1904
  return;
1883
1905
  }
1884
1906
  if (!activeItem || activeItem.kind === "repo-header" || activeItem.kind === "subgroup-header")
@@ -1934,7 +1956,7 @@ var BrowseScreen = ({
1934
1956
  return;
1935
1957
  }
1936
1958
  if (input === "o") {
1937
- $`open ${activeItem.url}`.catch(() => {
1959
+ quietly`open ${activeItem.url}`.catch(() => {
1938
1960
  });
1939
1961
  const label = activeItem.kind === "jira" ? activeItem.key : `#${activeItem.number}`;
1940
1962
  showFlash(`\u2197 Opened ${label}`);
@@ -2000,7 +2022,7 @@ var BrowseScreen = ({
2000
2022
  run: () => {
2001
2023
  menu.close();
2002
2024
  showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
2003
- $`jira issue move ${jiraKey} ${state} --resolution ${resolution}`.then(() => {
2025
+ quietly`jira issue move ${jiraKey} ${state} --resolution ${resolution}`.then(() => {
2004
2026
  showFlash(`\u2713 ${label} \xB7 ${resolution}`);
2005
2027
  setTimeout(() => onRefresh?.(), 1500);
2006
2028
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
@@ -2012,7 +2034,7 @@ var BrowseScreen = ({
2012
2034
  run: () => {
2013
2035
  menu.close();
2014
2036
  showFlash(`\u22EF Moving to ${label}\u2026`);
2015
- $`jira issue move ${jiraKey} ${state}`.then(() => {
2037
+ quietly`jira issue move ${jiraKey} ${state}`.then(() => {
2016
2038
  showFlash(`\u2713 Moved to ${label}`);
2017
2039
  setTimeout(() => onRefresh?.(), 1500);
2018
2040
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
@@ -2352,7 +2374,8 @@ var App = ({
2352
2374
  target,
2353
2375
  sections: state.sections,
2354
2376
  login: state.login
2355
- })
2377
+ }),
2378
+ extensions
2356
2379
  }
2357
2380
  ),
2358
2381
  (overlay?.kind === "pr" || overlay?.kind === "issue") && detailFor?.({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Ink components for rendering GitHub PR review comments and health — controlled, presentation-only, built on @kud/ink-ui and fed by @kud/gh.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",