@kud/gh-ink 0.13.0 → 0.15.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
@@ -76,12 +76,12 @@ type GHItem = {
76
76
  detail?: GHDetail;
77
77
  indent: boolean;
78
78
  };
79
- type JiraRow = {
80
- kind: "jira";
79
+ type TaskRow = {
80
+ kind: "task";
81
81
  key: string;
82
82
  summary: string;
83
83
  url: string;
84
- jiraStatus: string;
84
+ status: string;
85
85
  age: string;
86
86
  indent: boolean;
87
87
  instanceKey?: string;
@@ -92,6 +92,19 @@ type JiraRow = {
92
92
  * separately instead of being smuggled past the truncation maths.
93
93
  */
94
94
  note?: string;
95
+ /**
96
+ * The Jira issue key behind this row, when one exists. Its PRESENCE is what
97
+ * turns on the ticket affordances — ↵ opens a menu led by `jira issue view`,
98
+ * and `t` transitions the issue. A row without it is just a task: ↵ opens its
99
+ * URL and nothing here shells out to `jira`.
100
+ *
101
+ * Separate from `key` because the two are not the same thing. `key` is the
102
+ * left column, and a caller is free to put anything legible there: cockpit
103
+ * puts the ticket key, `life` puts a Todoist project name. Reading the drill
104
+ * off `key` is what made `life` run `jira issue view "Maison "` and
105
+ * label it "View ticket" on a surface that has never touched Jira.
106
+ */
107
+ ticket?: string;
95
108
  };
96
109
  type RepoHeader = {
97
110
  kind: "repo-header";
@@ -115,7 +128,7 @@ type SubgroupHeader = {
115
128
  age: string;
116
129
  indent: boolean;
117
130
  };
118
- type AnyItem = GHItem | JiraRow | RepoHeader | SubgroupHeader | ShowMore | ShowLess;
131
+ type AnyItem = GHItem | TaskRow | RepoHeader | SubgroupHeader | ShowMore | ShowLess;
119
132
  type CiStatus = {
120
133
  job: string;
121
134
  buildNumber: number;
@@ -275,4 +288,4 @@ declare const writeCache: (key: string, data: {
275
288
  login: string;
276
289
  }) => void;
277
290
 
278
- 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 };
291
+ 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 JiraTransition, type RepoHeader, type Section, type ShowLess, type ShowMore, type SubgroupHeader, type TaskRow, 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
@@ -607,7 +607,7 @@ var healthLegend = [
607
607
  ["closed", "Closed"]
608
608
  ];
609
609
  var CACHE_TTL_MS = 12e4;
610
- var CACHE_VERSION = 1;
610
+ var CACHE_VERSION = 2;
611
611
  var cacheDir = () => join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "ambre");
612
612
  var cacheFile = (key) => join(cacheDir(), `${key.replace(/[^a-z0-9._-]/gi, "-")}.json`);
613
613
  var readCache = (key) => {
@@ -641,9 +641,9 @@ var invalidateCache = (key) => {
641
641
  // src/inbox/diff.ts
642
642
  var keyOf = (item) => {
643
643
  if (item.kind === "pr" || item.kind === "issue") return item.url;
644
- if (item.kind === "jira") {
644
+ if (item.kind === "task") {
645
645
  const row = item;
646
- return row.url || `jira:${row.key}`;
646
+ return row.url || `task:${row.key}`;
647
647
  }
648
648
  return null;
649
649
  };
@@ -659,9 +659,9 @@ var renderedState = (item) => {
659
659
  i.author ?? ""
660
660
  ].join("|");
661
661
  }
662
- if (item.kind === "jira") {
662
+ if (item.kind === "task") {
663
663
  const j = item;
664
- return [j.summary, j.jiraStatus].join("|");
664
+ return [j.summary, j.status].join("|");
665
665
  }
666
666
  return "";
667
667
  };
@@ -733,6 +733,15 @@ var transientOf = (transients, item) => {
733
733
  const key = keyOf(item);
734
734
  return key ? transients.get(key) : void 0;
735
735
  };
736
+ var tabsOfMarks = (sections, marks) => {
737
+ const out = /* @__PURE__ */ new Map();
738
+ for (const section of sections)
739
+ for (const item of section.items) {
740
+ const key = keyOf(item);
741
+ if (key && marks.has(key)) out.set(key, section.id);
742
+ }
743
+ return out;
744
+ };
736
745
  var quietly = $({ quiet: true });
737
746
  var relativeTime = (iso) => {
738
747
  const diff = (Date.now() - new Date(iso).getTime()) / 1e3;
@@ -881,7 +890,7 @@ var filterByOrigin = (sections, keep, isWorkRepo) => sections.map((s) => {
881
890
  (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
882
891
  )
883
892
  );
884
- var searchText = (i) => i.kind === "pr" || i.kind === "issue" ? `${i.title} ${i.repo} #${i.number}` : i.kind === "jira" ? `${i.summary} ${i.key}` : "";
893
+ var searchText = (i) => i.kind === "pr" || i.kind === "issue" ? `${i.title} ${i.repo} #${i.number}` : i.kind === "task" ? `${i.summary} ${i.key}` : "";
885
894
  var filterBySearch = (sections, query) => {
886
895
  const q = query.trim().toLowerCase();
887
896
  if (!q) return sections;
@@ -1202,11 +1211,12 @@ var topLevelCount = (s) => s.items.filter(
1202
1211
  (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && !i.indent
1203
1212
  ).length;
1204
1213
  var drillCmd = (item) => {
1205
- if (item.kind === "jira") return `jira issue view ${item.key}`;
1214
+ if (item.kind === "task")
1215
+ return item.ticket ? `jira issue view ${item.ticket}` : null;
1206
1216
  return null;
1207
1217
  };
1208
1218
  var drillLabel = (item) => {
1209
- if (item.kind === "jira") return "View ticket";
1219
+ if (item.kind === "task") return "View ticket";
1210
1220
  if (item.kind === "issue") return "View issue";
1211
1221
  if (item.kind === "pr") return "Open PR";
1212
1222
  return "Drill in";
@@ -1228,7 +1238,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1228
1238
  hint: "c",
1229
1239
  run: () => {
1230
1240
  clipboard(item.url);
1231
- const label = item.kind === "jira" ? item.key : `#${item.number}`;
1241
+ const label = item.kind === "task" ? item.key : `#${item.number}`;
1232
1242
  showFlash(`\u2713 Copied URL for ${label}`);
1233
1243
  }
1234
1244
  };
@@ -1246,9 +1256,9 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1246
1256
  }
1247
1257
  }
1248
1258
  } : null;
1249
- if (item.kind === "jira") {
1259
+ if (item.kind === "task") {
1250
1260
  const base = drillAction ? [drillAction, open, copyUrl] : [open, copyUrl];
1251
- if (jiraTransitions && jiraTransitions.length > 0) {
1261
+ if (item.ticket && jiraTransitions && jiraTransitions.length > 0) {
1252
1262
  base.push({
1253
1263
  label: "Move status",
1254
1264
  hint: "t",
@@ -1265,7 +1275,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1265
1275
  hint: "",
1266
1276
  run: () => {
1267
1277
  showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
1268
- void quietly`jira issue move ${item.key} ${state} --resolution ${resolution}`.then(() => {
1278
+ void quietly`jira issue move ${item.ticket} ${state} --resolution ${resolution}`.then(() => {
1269
1279
  showFlash(`\u2713 ${label} \xB7 ${resolution}`);
1270
1280
  ext?.onActed?.();
1271
1281
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
@@ -1276,7 +1286,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1276
1286
  hint: "",
1277
1287
  run: () => {
1278
1288
  showFlash(`\u22EF Moving to ${label}\u2026`);
1279
- void quietly`jira issue move ${item.key} ${state}`.then(() => {
1289
+ void quietly`jira issue move ${item.ticket} ${state}`.then(() => {
1280
1290
  showFlash(`\u2713 Moved to ${label}`);
1281
1291
  ext?.onActed?.();
1282
1292
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
@@ -1604,7 +1614,7 @@ var ItemRow = ({
1604
1614
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: active ? "\u21B5 " : " " }),
1605
1615
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: "show less" })
1606
1616
  ] });
1607
- if (item.kind === "jira") {
1617
+ if (item.kind === "task") {
1608
1618
  const note = item.note ?? "";
1609
1619
  const titleMax2 = Math.max(20, COLS - item.key.length - note.length - 10);
1610
1620
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
@@ -1689,7 +1699,7 @@ var ActionMenu = ({
1689
1699
  actions,
1690
1700
  cursor
1691
1701
  }) => {
1692
- const title = item.kind === "jira" ? item.key : item.kind === "pr" || item.kind === "issue" ? `#${item.number}` : "";
1702
+ const title = item.kind === "task" ? item.key : item.kind === "pr" || item.kind === "issue" ? `#${item.number}` : "";
1693
1703
  return /* @__PURE__ */ jsxs(
1694
1704
  Box,
1695
1705
  {
@@ -1946,7 +1956,8 @@ var BrowseScreen = ({
1946
1956
  initialIncludeWork,
1947
1957
  brand,
1948
1958
  mergedUrls,
1949
- transients
1959
+ transients,
1960
+ onTabChange
1950
1961
  }) => {
1951
1962
  const { rows } = useWindowSize();
1952
1963
  const [includeWork, setIncludeWork] = useState(
@@ -1962,6 +1973,9 @@ var BrowseScreen = ({
1962
1973
  const [viewStarts, setViewStarts] = useState({});
1963
1974
  const safeTabIdx = Math.min(tabIdx, Math.max(0, localSections.length - 1));
1964
1975
  const activeId = localSections[safeTabIdx]?.id ?? "";
1976
+ useEffect(() => {
1977
+ onTabChange?.(activeId);
1978
+ }, [activeId]);
1965
1979
  const [flash, setFlash] = useState(null);
1966
1980
  const menu = useActionMenu();
1967
1981
  const [search, setSearch] = useState(null);
@@ -2037,7 +2051,7 @@ var BrowseScreen = ({
2037
2051
  setTimeout(() => setFlash(null), 2e3);
2038
2052
  };
2039
2053
  const [sparkFrame, setSparkFrame] = useState(0);
2040
- const sparkling = (mergedUrls?.length ?? 0) > 0 || (transients?.size ?? 0) > 0;
2054
+ const sparkling = (mergedUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item));
2041
2055
  useEffect(() => {
2042
2056
  if (!sparkling) return;
2043
2057
  const id = setInterval(() => setSparkFrame((f) => f + 1), MERGED_FRAME_MS);
@@ -2222,6 +2236,12 @@ var BrowseScreen = ({
2222
2236
  return;
2223
2237
  if (key.return) {
2224
2238
  if (openDrillView(activeItem)) return;
2239
+ if (activeItem.kind === "task" && !activeItem.ticket) {
2240
+ void quietly`open ${activeItem.url}`.catch(() => {
2241
+ });
2242
+ showFlash("\u2197 Opened in browser");
2243
+ return;
2244
+ }
2225
2245
  openMenu();
2226
2246
  return;
2227
2247
  }
@@ -2236,13 +2256,13 @@ var BrowseScreen = ({
2236
2256
  if (input === "o") {
2237
2257
  quietly`open ${activeItem.url}`.catch(() => {
2238
2258
  });
2239
- const label = activeItem.kind === "jira" ? activeItem.key : `#${activeItem.number}`;
2259
+ const label = activeItem.kind === "task" ? activeItem.key : `#${activeItem.number}`;
2240
2260
  showFlash(`\u2197 Opened ${label}`);
2241
2261
  return;
2242
2262
  }
2243
2263
  if (input === "c") {
2244
2264
  clipboard(activeItem.url);
2245
- const label = activeItem.kind === "jira" ? activeItem.key : `#${activeItem.number}`;
2265
+ const label = activeItem.kind === "task" ? activeItem.key : `#${activeItem.number}`;
2246
2266
  showFlash(`\u2713 Copied URL for ${label}`);
2247
2267
  return;
2248
2268
  }
@@ -2261,7 +2281,7 @@ var BrowseScreen = ({
2261
2281
  showFlash(`\u2713 Copied ${activeItem.branch}`);
2262
2282
  return;
2263
2283
  }
2264
- if (activeItem.kind !== "jira") {
2284
+ if (activeItem.kind !== "task") {
2265
2285
  if (input === "s" && activeItem.kind === "pr" && activeItem.branch) {
2266
2286
  const script = [
2267
2287
  "delay 0.5",
@@ -2286,8 +2306,8 @@ var BrowseScreen = ({
2286
2306
  return;
2287
2307
  }
2288
2308
  }
2289
- if (input === "t" && activeItem && activeItem.kind === "jira" && jiraTransitions && jiraTransitions.length > 0) {
2290
- const jiraKey = activeItem.key;
2309
+ if (input === "t" && activeItem && activeItem.kind === "task" && activeItem.ticket && jiraTransitions && jiraTransitions.length > 0) {
2310
+ const jiraKey = activeItem.ticket;
2291
2311
  const actions = jiraTransitions.map(
2292
2312
  ({ label, state, resolutions }) => resolutions && resolutions.length > 0 ? {
2293
2313
  label,
@@ -2414,16 +2434,16 @@ var BrowseScreen = ({
2414
2434
  // the whole panel up a line instead of clipping.
2415
2435
  i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2416
2436
  // first child — in Other PRs that's "free" because the
2417
- // child is itself a repo-header (gap above). A jira row
2437
+ // child is itself a repo-header (gap above). A task row
2418
2438
  // has no such stand-in, so it needs this explicitly. Never
2419
2439
  // applies between two tickets/PRs — only right after a
2420
2440
  // header.
2421
- item.kind === "jira" && ["repo-header", "subgroup-header"].includes(
2441
+ item.kind === "task" && ["repo-header", "subgroup-header"].includes(
2422
2442
  section.items[viewStart + i - 1]?.kind
2423
2443
  ))
2424
2444
  )
2425
2445
  },
2426
- `${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}`}`
2446
+ `${viewStart + i}:${item.kind === "task" ? 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}`}`
2427
2447
  )) }),
2428
2448
  hasMore && /* @__PURE__ */ jsxs(Text2, { dimColor: true, children: [
2429
2449
  " ",
@@ -2508,13 +2528,8 @@ var App = ({
2508
2528
  const displayedKey = useRef("");
2509
2529
  const displayedSections = useRef([]);
2510
2530
  const [transients, setTransients] = useState(NO_TRANSIENTS);
2511
- const transitTimer = useRef(null);
2512
- useEffect(
2513
- () => () => {
2514
- if (transitTimer.current) clearTimeout(transitTimer.current);
2515
- },
2516
- []
2517
- );
2531
+ const transitTabs = useRef(/* @__PURE__ */ new Map());
2532
+ const [visibleTab, setVisibleTab] = useState("");
2518
2533
  const showData = (sections, login) => {
2519
2534
  const before = displayedSections.current;
2520
2535
  displayedSections.current = sections;
@@ -2522,25 +2537,61 @@ var App = ({
2522
2537
  setPending(null);
2523
2538
  setFetchedAt(Date.now());
2524
2539
  if (before.length === 0) {
2540
+ transitTabs.current = /* @__PURE__ */ new Map();
2525
2541
  setTransients(NO_TRANSIENTS);
2526
2542
  setState({ phase: "browse", sections, login });
2527
2543
  return;
2528
2544
  }
2529
- const { transients: marks, union } = diffSections(before, sections);
2545
+ const { transients: fresh, union } = diffSections(before, sections);
2546
+ const marks = new Map(fresh);
2547
+ for (const [key, mark] of transients)
2548
+ if (!marks.has(key)) marks.set(key, mark);
2549
+ const tabs = tabsOfMarks(union, marks);
2550
+ for (const key of [...marks.keys()]) if (!tabs.has(key)) marks.delete(key);
2530
2551
  if (marks.size === 0) {
2552
+ transitTabs.current = /* @__PURE__ */ new Map();
2553
+ setTransients(NO_TRANSIENTS);
2531
2554
  setState({ phase: "browse", sections, login });
2532
2555
  return;
2533
2556
  }
2557
+ transitTabs.current = tabs;
2534
2558
  setTransients(marks);
2535
2559
  setState({ phase: "browse", sections: union, login });
2536
- if (transitTimer.current) clearTimeout(transitTimer.current);
2537
- transitTimer.current = setTimeout(() => {
2538
- setTransients(NO_TRANSIENTS);
2539
- setState(
2540
- (prev) => prev.phase === "browse" && prev.sections === union ? { ...prev, sections } : prev
2541
- );
2542
- }, TRANSIT_HOLD_MS);
2543
2560
  };
2561
+ const settleTab = (tabId, marks) => {
2562
+ const keys = [...transitTabs.current].filter(([, tab]) => tab === tabId).map(([key]) => key);
2563
+ for (const key of keys) transitTabs.current.delete(key);
2564
+ const gone = new Set(keys.filter((key) => marks.get(key) === "out"));
2565
+ setTransients((prev) => {
2566
+ const next = new Map(prev);
2567
+ for (const key of keys) next.delete(key);
2568
+ return next.size === 0 ? NO_TRANSIENTS : next;
2569
+ });
2570
+ setState(
2571
+ (prev) => prev.phase !== "browse" ? prev : {
2572
+ ...prev,
2573
+ sections: prev.sections.map(
2574
+ (section) => section.id !== tabId ? section : {
2575
+ ...section,
2576
+ items: section.items.filter((item) => {
2577
+ const key = keyOf(item);
2578
+ return !(key && gone.has(key));
2579
+ })
2580
+ }
2581
+ )
2582
+ }
2583
+ );
2584
+ };
2585
+ useEffect(() => {
2586
+ if (transients.size === 0) return;
2587
+ if (state.phase !== "browse" || !visibleTab) return;
2588
+ if (![...transitTabs.current.values()].includes(visibleTab)) return;
2589
+ const timer = setTimeout(
2590
+ () => settleTab(visibleTab, transients),
2591
+ TRANSIT_HOLD_MS
2592
+ );
2593
+ return () => clearTimeout(timer);
2594
+ }, [transients, visibleTab, state.phase]);
2544
2595
  const pendingSummary = useMemo(
2545
2596
  () => pending ? summariseDiff(
2546
2597
  diffSections(displayedSections.current, pending.sections).counts
@@ -2725,6 +2776,7 @@ var App = ({
2725
2776
  hidden: overlay !== null,
2726
2777
  mergedUrls,
2727
2778
  transients,
2779
+ onTabChange: setVisibleTab,
2728
2780
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
2729
2781
  ciJob,
2730
2782
  tabHelp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.13.0",
3
+ "version": "0.15.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",