@kud/gh-ink 0.7.1 → 0.8.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 CHANGED
@@ -177,6 +177,14 @@ declare const drillCmd: (item: AnyItem) => string | null;
177
177
  declare const buildActions: (item: AnyItem, login: string, showFlash: (msg: string) => void, jiraBase?: string, jiraKeyRe?: RegExp, jiraTransitions?: JiraTransition[], onRefresh?: () => void, onRemove?: (item: GHItem) => void, onOpenView?: (item: AnyItem) => boolean, ext?: {
178
178
  extensions?: InboxExtension[];
179
179
  onOpenExt?: (id: string, target: ExtensionTarget) => void;
180
+ /**
181
+ * A mutation landed. Drops the cached glance now and schedules the refresh,
182
+ * replacing a bare `setTimeout(onRefresh, 1500)` at each call site.
183
+ *
184
+ * Both halves matter and only one is visible: the delay lets GitHub settle,
185
+ * the drop stops the pre-action cache outliving a quit inside that window.
186
+ */
187
+ onActed?: () => void;
180
188
  }) => Action[];
181
189
  type CiStatusState = {
182
190
  kind: "loading";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { isPassCheck, isFailCheck, resolveThread, unresolveThread, replyToThread
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
6
  import { $ } from 'zx';
7
7
  import { spawn } from 'child_process';
8
- import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
8
+ import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, rmSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { homedir } from 'os';
11
11
 
@@ -604,21 +604,34 @@ var healthLegend = [
604
604
  ["merged", "Merged"],
605
605
  ["closed", "Closed"]
606
606
  ];
607
+ var CACHE_TTL_MS = 12e4;
608
+ var CACHE_VERSION = 1;
607
609
  var cacheDir = () => join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "ambre");
608
610
  var cacheFile = (key) => join(cacheDir(), `${key.replace(/[^a-z0-9._-]/gi, "-")}.json`);
609
611
  var readCache = (key) => {
610
612
  try {
611
613
  const raw = JSON.parse(readFileSync(cacheFile(key), "utf8"));
614
+ if (raw?.version !== CACHE_VERSION) return null;
612
615
  if (!Array.isArray(raw?.sections)) return null;
613
616
  return { sections: raw.sections, login: raw.login ?? "", at: raw.at ?? 0 };
614
617
  } catch {
615
618
  return null;
616
619
  }
617
620
  };
621
+ var isFresh = (cached, now = Date.now()) => !!cached && now - cached.at < CACHE_TTL_MS;
618
622
  var writeCache = (key, data) => {
619
623
  try {
620
624
  mkdirSync(cacheDir(), { recursive: true });
621
- writeFileSync(cacheFile(key), JSON.stringify({ ...data, at: Date.now() }));
625
+ writeFileSync(
626
+ cacheFile(key),
627
+ JSON.stringify({ version: CACHE_VERSION, ...data, at: Date.now() })
628
+ );
629
+ } catch {
630
+ }
631
+ };
632
+ var invalidateCache = (key) => {
633
+ try {
634
+ rmSync(cacheFile(key), { force: true });
622
635
  } catch {
623
636
  }
624
637
  };
@@ -1052,6 +1065,7 @@ var FRAME_COLOR = "gray";
1052
1065
  var FRAME_PAD_X = 1;
1053
1066
  var FRAME_CHROME_COLS = 2 + FRAME_PAD_X * 2;
1054
1067
  var COLS = (process.stdout.columns ?? 120) - FRAME_CHROME_COLS;
1068
+ var MODAL_CHROME_COLS = 2 + 1 * 2;
1055
1069
  var topLevelCount = (s) => s.items.filter(
1056
1070
  (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && !i.indent
1057
1071
  ).length;
@@ -1121,7 +1135,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1121
1135
  showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
1122
1136
  void quietly`jira issue move ${item.key} ${state} --resolution ${resolution}`.then(() => {
1123
1137
  showFlash(`\u2713 ${label} \xB7 ${resolution}`);
1124
- setTimeout(() => onRefresh?.(), 1500);
1138
+ ext?.onActed?.();
1125
1139
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
1126
1140
  }
1127
1141
  }))
@@ -1132,7 +1146,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1132
1146
  showFlash(`\u22EF Moving to ${label}\u2026`);
1133
1147
  void quietly`jira issue move ${item.key} ${state}`.then(() => {
1134
1148
  showFlash(`\u2713 Moved to ${label}`);
1135
- setTimeout(() => onRefresh?.(), 1500);
1149
+ ext?.onActed?.();
1136
1150
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
1137
1151
  }
1138
1152
  }
@@ -1528,12 +1542,55 @@ var TURN_LEGEND = [
1528
1542
  ["\u2190", "#FF8700", "They spoke last \xB7 your turn"],
1529
1543
  ["\u2192", "#888888", "You spoke last \xB7 waiting on them"]
1530
1544
  ];
1545
+ var CELL_GUTTER = 2;
1546
+ var COL_GAP = 3;
1547
+ var cellWidthOf = (col) => Math.max(0, ...col.rows.map((r) => r.cell.length)) + CELL_GUTTER;
1548
+ var widthOf = (col) => Math.max(
1549
+ col.title.length,
1550
+ ...col.rows.map((r) => cellWidthOf(col) + r.label.length)
1551
+ );
1552
+ var groupWidth = (group) => Math.max(0, ...group.map(widthOf));
1553
+ var layoutWidth = (groups) => groups.reduce((total, g) => total + groupWidth(g), 0) + COL_GAP * Math.max(0, groups.length - 1);
1554
+ var legendLayout = (cols, available) => {
1555
+ const across = cols.map((c) => [c]);
1556
+ if (layoutWidth(across) <= available) return across;
1557
+ const stacked = [cols.slice(0, -1), cols.slice(-1)];
1558
+ if (layoutWidth(stacked) <= available) return stacked;
1559
+ return [cols];
1560
+ };
1561
+ var LegendGroup = ({
1562
+ group,
1563
+ width,
1564
+ marginRight
1565
+ }) => /* @__PURE__ */ jsx(Box, { flexDirection: "column", width, marginRight, children: group.map((col, index) => {
1566
+ const cellWidth = cellWidthOf(col);
1567
+ return /* @__PURE__ */ jsxs(
1568
+ Box,
1569
+ {
1570
+ flexDirection: "column",
1571
+ marginTop: index === 0 ? 0 : 1,
1572
+ children: [
1573
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: col.title }),
1574
+ col.rows.map((row) => /* @__PURE__ */ jsxs(Box, { children: [
1575
+ /* @__PURE__ */ jsx(Text, { color: row.color, bold: row.bold, children: row.cell.padEnd(cellWidth) }),
1576
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: row.label })
1577
+ ] }, col.title + row.cell + row.label))
1578
+ ]
1579
+ },
1580
+ col.title
1581
+ );
1582
+ }) });
1531
1583
  var HelpModal = ({
1532
1584
  workToggle,
1533
1585
  extensions,
1534
1586
  hasJira,
1535
1587
  tabHelp
1536
1588
  }) => {
1589
+ const { columns } = useWindowSize();
1590
+ const available = Math.max(
1591
+ 20,
1592
+ columns - FRAME_CHROME_COLS - MODAL_CHROME_COLS
1593
+ );
1537
1594
  const keys = [
1538
1595
  ["\u2191 \u2193", "navigate"],
1539
1596
  ["\u2190 \u2192 \xB7 tab", "switch tab"],
@@ -1555,6 +1612,49 @@ var HelpModal = ({
1555
1612
  ["?", "this help"],
1556
1613
  ["q", "quit"]
1557
1614
  ];
1615
+ const statusColumn = {
1616
+ title: "Status",
1617
+ rows: [
1618
+ ...healthLegend.map(([health, label]) => ({
1619
+ cell: healthDisplay[health].glyph,
1620
+ color: healthDisplay[health].color,
1621
+ bold: true,
1622
+ label
1623
+ })),
1624
+ ...TURN_LEGEND.map(([cell, color, label]) => ({
1625
+ cell,
1626
+ color,
1627
+ bold: true,
1628
+ label
1629
+ })),
1630
+ {
1631
+ cell: "\uF086",
1632
+ color: "#FF8700",
1633
+ bold: true,
1634
+ label: "Open-thread count"
1635
+ }
1636
+ ]
1637
+ };
1638
+ const cols = [
1639
+ statusColumn,
1640
+ ...tabHelp ? [
1641
+ {
1642
+ title: "Tabs",
1643
+ rows: tabHelp.map(([tab, meaning]) => ({
1644
+ cell: tab,
1645
+ color: "#FF8700",
1646
+ label: meaning
1647
+ }))
1648
+ }
1649
+ ] : [],
1650
+ {
1651
+ title: "Keys",
1652
+ rows: keys.map(([key, label]) => ({ cell: key, color: "cyan", label }))
1653
+ }
1654
+ ];
1655
+ const groups = legendLayout(cols, available);
1656
+ const widths = groups.map((g) => Math.min(groupWidth(g), available));
1657
+ const contentWidth = Math.min(available, layoutWidth(groups));
1558
1658
  return /* @__PURE__ */ jsxs(
1559
1659
  Box,
1560
1660
  {
@@ -1562,42 +1662,18 @@ var HelpModal = ({
1562
1662
  borderStyle: "round",
1563
1663
  borderColor: "cyan",
1564
1664
  paddingX: 1,
1665
+ width: contentWidth + MODAL_CHROME_COLS,
1565
1666
  children: [
1566
1667
  /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "Legend" }),
1567
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1568
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginRight: 3, minWidth: 26, children: [
1569
- /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Status" }),
1570
- healthLegend.map(([health, label]) => {
1571
- const { glyph: icon, color } = healthDisplay[health];
1572
- return /* @__PURE__ */ jsxs(Box, { children: [
1573
- /* @__PURE__ */ jsx(Text, { color, bold: true, children: icon + " " }),
1574
- /* @__PURE__ */ jsx(Text, { children: " " + label })
1575
- ] }, health);
1576
- }),
1577
- TURN_LEGEND.map(([icon, color, label]) => /* @__PURE__ */ jsxs(Box, { children: [
1578
- /* @__PURE__ */ jsx(Text, { color, bold: true, children: icon + " " }),
1579
- /* @__PURE__ */ jsx(Text, { children: " " + label })
1580
- ] }, icon)),
1581
- /* @__PURE__ */ jsxs(Box, { children: [
1582
- /* @__PURE__ */ jsx(Text, { bold: true, color: "#FF8700", children: "\uF086 " }),
1583
- /* @__PURE__ */ jsx(Text, { children: " Open-thread count" })
1584
- ] })
1585
- ] }),
1586
- tabHelp ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginRight: 3, minWidth: 30, children: [
1587
- /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Tabs" }),
1588
- tabHelp.map(([tab, meaning]) => /* @__PURE__ */ jsxs(Box, { children: [
1589
- /* @__PURE__ */ jsx(Text, { color: "#FF8700", children: tab.padEnd(10) }),
1590
- /* @__PURE__ */ jsx(Text, { children: meaning })
1591
- ] }, tab))
1592
- ] }) : null,
1593
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1594
- /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Keys" }),
1595
- keys.map(([k, label]) => /* @__PURE__ */ jsxs(Box, { children: [
1596
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: k.padEnd(11) }),
1597
- /* @__PURE__ */ jsx(Text, { children: label })
1598
- ] }, k))
1599
- ] })
1600
- ] }),
1668
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, width: contentWidth, children: groups.map((group, index) => /* @__PURE__ */ jsx(
1669
+ LegendGroup,
1670
+ {
1671
+ group,
1672
+ width: widths[index],
1673
+ marginRight: index === groups.length - 1 ? 0 : COL_GAP
1674
+ },
1675
+ group[0]?.title ?? index
1676
+ )) }),
1601
1677
  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,
1602
1678
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "esc \xB7 ? close" })
1603
1679
  ]
@@ -1673,6 +1749,7 @@ var BrowseScreen = ({
1673
1749
  jiraKeyRe,
1674
1750
  jiraTransitions,
1675
1751
  onRefresh,
1752
+ onActed,
1676
1753
  refreshing,
1677
1754
  hasPending,
1678
1755
  fetchedAt,
@@ -1807,7 +1884,7 @@ var BrowseScreen = ({
1807
1884
  onRefresh,
1808
1885
  removeItemFromSections,
1809
1886
  openDrillView,
1810
- { extensions, onOpenExt }
1887
+ { extensions, onOpenExt, onActed }
1811
1888
  );
1812
1889
  menu.open(actions);
1813
1890
  };
@@ -2037,7 +2114,7 @@ var BrowseScreen = ({
2037
2114
  showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
2038
2115
  quietly`jira issue move ${jiraKey} ${state} --resolution ${resolution}`.then(() => {
2039
2116
  showFlash(`\u2713 ${label} \xB7 ${resolution}`);
2040
- setTimeout(() => onRefresh?.(), 1500);
2117
+ onActed?.();
2041
2118
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
2042
2119
  }
2043
2120
  }))
@@ -2049,7 +2126,7 @@ var BrowseScreen = ({
2049
2126
  showFlash(`\u22EF Moving to ${label}\u2026`);
2050
2127
  quietly`jira issue move ${jiraKey} ${state}`.then(() => {
2051
2128
  showFlash(`\u2713 Moved to ${label}`);
2052
- setTimeout(() => onRefresh?.(), 1500);
2129
+ onActed?.();
2053
2130
  }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
2054
2131
  }
2055
2132
  }
@@ -2280,9 +2357,14 @@ var App = ({
2280
2357
  if (pending) showData(pending.sections, pending.login);
2281
2358
  else revalidate(true);
2282
2359
  };
2360
+ const onActed = () => {
2361
+ if (cacheKey) invalidateCache(cacheKey);
2362
+ setTimeout(() => applyOrRefresh(), 1500);
2363
+ };
2283
2364
  useEffect(() => {
2284
2365
  const cached = cacheKey ? readCache(cacheKey) : null;
2285
- if (cached && cached.sections.length > 0) {
2366
+ const painted = !!cached && cached.sections.length > 0;
2367
+ if (painted && cached) {
2286
2368
  displayedKey.current = signatureOf(cached.sections);
2287
2369
  setFetchedAt(cached.at);
2288
2370
  setState({
@@ -2291,7 +2373,7 @@ var App = ({
2291
2373
  login: cached.login
2292
2374
  });
2293
2375
  }
2294
- revalidate();
2376
+ if (!painted || !isFresh(cached)) revalidate();
2295
2377
  }, []);
2296
2378
  useEffect(() => {
2297
2379
  if (!hasCiStatus || !ciFetcher) return;
@@ -2358,6 +2440,7 @@ var App = ({
2358
2440
  jiraKeyRe,
2359
2441
  jiraTransitions,
2360
2442
  onRefresh: applyOrRefresh,
2443
+ onActed,
2361
2444
  refreshing,
2362
2445
  hasPending: pending !== null,
2363
2446
  fetchedAt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
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",
@@ -48,7 +48,7 @@
48
48
  "react": ">=19"
49
49
  },
50
50
  "dependencies": {
51
- "@kud/gh": "0.5.0",
51
+ "@kud/gh": "0.5.1",
52
52
  "@kud/ink-ui": "0.14.0",
53
53
  "zx": "8.8.5"
54
54
  },