@kud/gh-ink 0.32.2 → 0.34.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
@@ -140,7 +140,23 @@ type GHItem = {
140
140
  * handing back a `lastActor` that no longer claims a turn.
141
141
  */
142
142
  pinned?: boolean;
143
- indent: boolean;
143
+ /**
144
+ * How deep this row hangs in the tree: 0 top level, 1 a child, 2 a
145
+ * grandchild. Absent means 0.
146
+ *
147
+ * Always read it through `depthOf`, never off the row — `indent` is still a
148
+ * legal way to spell "depth 1", and that helper is the only place that knows
149
+ * both spellings. A site that reads `item.indent` directly prices a `depth: 2`
150
+ * row as top level, which is the bug this field exists to end rather than a
151
+ * new one to introduce.
152
+ */
153
+ depth?: number;
154
+ /**
155
+ * @deprecated Legacy spelling of `depth: 1`. Write `depth`; this is kept so
156
+ * every existing producer still typechecks, and it is read in exactly one
157
+ * place (`depthOf`). Due to be removed at the next major.
158
+ */
159
+ indent?: boolean;
144
160
  };
145
161
  type TaskRow = {
146
162
  kind: "task";
@@ -149,7 +165,10 @@ type TaskRow = {
149
165
  url: string;
150
166
  status: string;
151
167
  age: string;
152
- indent: boolean;
168
+ /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
169
+ depth?: number;
170
+ /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
171
+ indent?: boolean;
153
172
  instanceKey?: string;
154
173
  /**
155
174
  * Trailing annotation, rendered dim after the summary — a recurrence marker,
@@ -176,23 +195,35 @@ type RepoHeader = {
176
195
  kind: "repo-header";
177
196
  repo: string;
178
197
  age: string;
179
- indent: boolean;
198
+ /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
199
+ depth?: number;
200
+ /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
201
+ indent?: boolean;
180
202
  };
181
203
  type ShowMore = {
182
204
  kind: "show-more";
183
205
  hidden: GHItem[];
184
- indent: boolean;
206
+ /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
207
+ depth?: number;
208
+ /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
209
+ indent?: boolean;
185
210
  };
186
211
  type ShowLess = {
187
212
  kind: "show-less";
188
213
  toHide: GHItem[];
189
- indent: boolean;
214
+ /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
215
+ depth?: number;
216
+ /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
217
+ indent?: boolean;
190
218
  };
191
219
  type SubgroupHeader = {
192
220
  kind: "subgroup-header";
193
221
  label: string;
194
222
  age: string;
195
- indent: boolean;
223
+ /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
224
+ depth?: number;
225
+ /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
226
+ indent?: boolean;
196
227
  };
197
228
  type AnyItem = GHItem | TaskRow | RepoHeader | SubgroupHeader | ShowMore | ShowLess;
198
229
  type CiStatus = {
@@ -295,6 +326,22 @@ declare const filterBySearch: (sections: Section[], query: string) => Section[];
295
326
  declare const filterByRepos: (sections: Section[], repos: Set<string>) => Section[];
296
327
  declare const withoutItem: (sections: Section[], target: GHItem) => Section[];
297
328
  declare const reposInSections: (sections: Section[]) => string[];
329
+ /**
330
+ * The next selectable row in `dir`, or `current` when there is none.
331
+ *
332
+ * A repo-header is a STOP, and a subgroup-header is not. The two look alike and
333
+ * are not: the fence names a thing you can act on — open the checkout, copy every
334
+ * URL under it — where a band label ("Your move") names an arrangement of rows
335
+ * and has nothing behind it to open. Landing on the second would be a keystroke
336
+ * spent on a row whose every key is inert.
337
+ *
338
+ * Unconditional in both directions, deliberately. Skipping the fence going down
339
+ * and landing on it going up would make ↓ then ↑ end somewhere other than where
340
+ * it started, and an arrow pair that is not its own inverse reads as the list
341
+ * drifting rather than as a shortcut. The cost is one press per repo group; the
342
+ * fence is also the "you have crossed into another repo" beat the eye takes
343
+ * anyway, so the press buys orientation as well as position.
344
+ */
298
345
  declare const moveCursor: (items: AnyItem[], current: number, dir: 1 | -1) => number;
299
346
  /**
300
347
  * What to say about the remaining budget, or nothing at all.
package/dist/index.js CHANGED
@@ -780,6 +780,11 @@ var tabsOfMarks = (sections, marks) => {
780
780
  return out;
781
781
  };
782
782
  var quietly = $({ quiet: true });
783
+ var depthOf = (item) => {
784
+ if (!item) return 0;
785
+ if (typeof item.depth === "number") return Math.max(0, Math.floor(item.depth));
786
+ return item.indent === true ? 1 : 0;
787
+ };
783
788
  var relativeTime = (iso) => {
784
789
  const diff = (Date.now() - new Date(iso).getTime()) / 1e3;
785
790
  if (diff < 3600) return `${Math.floor(diff / 60)}m`;
@@ -902,7 +907,7 @@ var insertRepoHeaders = (items) => {
902
907
  const result = [];
903
908
  let lastRepo = "";
904
909
  for (const item of items) {
905
- if (!item.indent && item.repo !== lastRepo) {
910
+ if (depthOf(item) === 0 && item.repo !== lastRepo) {
906
911
  lastRepo = item.repo;
907
912
  result.push({
908
913
  kind: "repo-header",
@@ -1057,7 +1062,7 @@ var reposInSections = (sections) => [
1057
1062
  ].sort();
1058
1063
  var moveCursor = (items, current2, dir) => {
1059
1064
  let next = current2 + dir;
1060
- while (next >= 0 && next < items.length && (items[next].kind === "repo-header" || items[next].kind === "subgroup-header"))
1065
+ while (next >= 0 && next < items.length && items[next].kind === "subgroup-header")
1061
1066
  next += dir;
1062
1067
  if (next < 0 || next >= items.length) return current2;
1063
1068
  return next;
@@ -1077,14 +1082,54 @@ var budgetNotice = (budget, now = Date.now()) => {
1077
1082
  critical: false
1078
1083
  };
1079
1084
  };
1080
- var isChildRow = (item) => !!item && (item.kind === "show-more" || item.kind === "show-less" || "indent" in item && item.indent === true);
1085
+ var isLastAtDepth = (items, i) => {
1086
+ const depth = depthOf(items[i]);
1087
+ for (let j = i + 1; j < items.length; j += 1) {
1088
+ const d = depthOf(items[j]);
1089
+ if (d > depth) continue;
1090
+ return d < depth;
1091
+ }
1092
+ return true;
1093
+ };
1094
+ var treePrefix = (items, i) => {
1095
+ const depth = depthOf(items[i]);
1096
+ if (depth === 0) return "";
1097
+ const ancestorLast = [];
1098
+ let level = depth - 1;
1099
+ for (let j = i - 1; j >= 0 && level >= 1; j -= 1) {
1100
+ if (depthOf(items[j]) === level) {
1101
+ ancestorLast[level] = isLastAtDepth(items, j);
1102
+ level -= 1;
1103
+ }
1104
+ }
1105
+ let prefix = "";
1106
+ for (let k = 1; k < depth; k += 1) prefix += ancestorLast[k] ? " " : "\u2502 ";
1107
+ return prefix + (isLastAtDepth(items, i) ? "\u2514\u2500 " : "\u251C\u2500 ");
1108
+ };
1109
+ var groupUrls = (items, i) => {
1110
+ const urls = [];
1111
+ for (let j = i + 1; j < items.length; j += 1) {
1112
+ const item = items[j];
1113
+ if (item.kind === "repo-header" || item.kind === "subgroup-header") break;
1114
+ if (item.kind === "show-more") {
1115
+ for (const child of item.hidden) if (child.url) urls.push(child.url);
1116
+ continue;
1117
+ }
1118
+ const url = item.url;
1119
+ if (url) urls.push(url);
1120
+ }
1121
+ return urls;
1122
+ };
1081
1123
  var treeUrls = (items, i) => {
1124
+ if (items[i]?.kind === "repo-header") return groupUrls(items, i);
1082
1125
  let root = i;
1083
- while (root > 0 && isChildRow(items[root])) root -= 1;
1126
+ while (root > 0 && depthOf(items[root]) > 0 && items[root]?.kind !== "task")
1127
+ root -= 1;
1128
+ const rootDepth = depthOf(items[root]);
1084
1129
  const urls = [];
1085
1130
  for (let j = root; j < items.length; j += 1) {
1086
1131
  const item = items[j];
1087
- if (j > root && !isChildRow(item)) break;
1132
+ if (j > root && depthOf(item) <= rootDepth) break;
1088
1133
  if (item.kind === "show-more") {
1089
1134
  for (const child of item.hidden) if (child.url) urls.push(child.url);
1090
1135
  continue;
@@ -1100,7 +1145,8 @@ var gapsAbove = (items, i) => {
1100
1145
  if (item.kind === "repo-header" || item.kind === "subgroup-header")
1101
1146
  return true;
1102
1147
  if (item.kind !== "task") return false;
1103
- return isChildRow(items[i + 1]) || isChildRow(items[i - 1]);
1148
+ if (depthOf(item) > 0) return false;
1149
+ return depthOf(items[i + 1]) > 0 || depthOf(items[i - 1]) > 0;
1104
1150
  };
1105
1151
  var fitCount = (items, start, budget) => {
1106
1152
  let lines = 0;
@@ -1345,7 +1391,7 @@ var Backdrop = ({
1345
1391
  }
1346
1392
  ) : children });
1347
1393
  var topLevelCount = (s) => s.items.filter(
1348
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && !i.indent
1394
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && depthOf(i) === 0
1349
1395
  ).length;
1350
1396
  var drillCmd = (item) => {
1351
1397
  if (item.kind === "task")
@@ -1593,7 +1639,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1593
1639
  }
1594
1640
  });
1595
1641
  if (jiraBase && jiraKeyRe) {
1596
- const jiraKey = !item.indent ? item.title.match(jiraKeyRe)?.[0] : null;
1642
+ const jiraKey = depthOf(item) === 0 ? item.title.match(jiraKeyRe)?.[0] : null;
1597
1643
  if (jiraKey) {
1598
1644
  actions.push({
1599
1645
  label: `Open ${jiraKey} in Jira`,
@@ -1761,10 +1807,21 @@ var CiStatusLine = ({
1761
1807
  status.age ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + status.age }) : null
1762
1808
  ] });
1763
1809
  };
1764
- var RepoHeaderRow = ({ repo, gap }) => {
1810
+ var FENCE_COLS = 46;
1811
+ var RepoHeaderRow = ({
1812
+ repo,
1813
+ gap,
1814
+ active
1815
+ }) => {
1765
1816
  const label = `\u2500\u2500 ${repo} `;
1766
- const fill = Math.max(4, 46 - label.length);
1767
- return /* @__PURE__ */ jsx(Box, { marginTop: gap ? 1 : 0, children: /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + label + "\u2500".repeat(fill) }) });
1817
+ const fill = Math.max(4, FENCE_COLS - label.length);
1818
+ const rule = "\u2500".repeat(fill);
1819
+ return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1820
+ /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1821
+ /* @__PURE__ */ jsx(Text2, { dimColor: !active, children: "\u2500\u2500 " }),
1822
+ /* @__PURE__ */ jsx(Text2, { dimColor: !active, bold: active, children: repo }),
1823
+ /* @__PURE__ */ jsx(Text2, { dimColor: !active, children: " " + rule })
1824
+ ] });
1768
1825
  };
1769
1826
  var MERGED_HOLD_MS = 5e3;
1770
1827
  var MERGED_FRAME_MS = 150;
@@ -1796,13 +1853,13 @@ var ItemRow = ({
1796
1853
  merged,
1797
1854
  transient: refreshMark,
1798
1855
  leaving,
1799
- lastChild,
1856
+ prefix = "",
1800
1857
  parent,
1801
1858
  sparkFrame = 0
1802
1859
  }) => {
1803
1860
  const transient = leaving ? "out" : refreshMark;
1804
1861
  if (item.kind === "repo-header")
1805
- return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false });
1862
+ return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false, active });
1806
1863
  if (item.kind === "subgroup-header")
1807
1864
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1808
1865
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: true, children: " \xBB " }),
@@ -1811,14 +1868,14 @@ var ItemRow = ({
1811
1868
  if (item.kind === "show-more")
1812
1869
  return /* @__PURE__ */ jsxs(Box, { children: [
1813
1870
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1814
- /* @__PURE__ */ jsx(Text2, { dimColor: true, children: "\u2514\u2500 " }),
1871
+ /* @__PURE__ */ jsx(Text2, { dimColor: true, children: prefix }),
1815
1872
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: active ? "\u21B5 " : " " }),
1816
1873
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: `+${item.hidden.length} more` })
1817
1874
  ] });
1818
1875
  if (item.kind === "show-less")
1819
1876
  return /* @__PURE__ */ jsxs(Box, { children: [
1820
1877
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1821
- /* @__PURE__ */ jsx(Text2, { dimColor: true, children: "\u2514\u2500 " }),
1878
+ /* @__PURE__ */ jsx(Text2, { dimColor: true, children: prefix }),
1822
1879
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: active ? "\u21B5 " : " " }),
1823
1880
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: "show less" })
1824
1881
  ] });
@@ -1828,10 +1885,11 @@ var ItemRow = ({
1828
1885
  const transitLabel2 = transient ? TRANSIT_LABEL[transient] : "";
1829
1886
  const titleMax2 = Math.max(
1830
1887
  20,
1831
- COLS - item.key.length - note.length - transitLabel2.length - 12
1888
+ COLS - item.key.length - note.length - transitLabel2.length - prefix.length - 12
1832
1889
  );
1833
1890
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1834
1891
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1892
+ /* @__PURE__ */ jsx(Text2, { dimColor: true, children: prefix }),
1835
1893
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: parent ? "\u252C " : " " }),
1836
1894
  /* @__PURE__ */ jsx(Text2, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
1837
1895
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: active, children: item.key + " " }),
@@ -1866,12 +1924,12 @@ var ItemRow = ({
1866
1924
  mergedLabel,
1867
1925
  transitLabel
1868
1926
  ].filter(Boolean).join(" ");
1869
- const repoLabel = item.indent ? item.repo : "";
1870
- const fixedWidth = 2 + (item.indent ? 3 : 0) + 2 + 2 + 7 + repoLabel.length + suffix.length + 6;
1927
+ const repoLabel = depthOf(item) > 0 ? item.repo : "";
1928
+ const fixedWidth = 2 + prefix.length + 2 + 2 + 7 + repoLabel.length + suffix.length + 6;
1871
1929
  const titleMax = Math.max(20, COLS - fixedWidth);
1872
1930
  return /* @__PURE__ */ jsxs(Box, { children: [
1873
1931
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1874
- item.indent ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: lastChild === false ? "\u251C\u2500 " : "\u2514\u2500 " }) : null,
1932
+ /* @__PURE__ */ jsx(Text2, { dimColor: true, children: prefix }),
1875
1933
  /* @__PURE__ */ jsx(Text2, { color, bold: true, children: icon + " " }),
1876
1934
  /* @__PURE__ */ jsx(Text2, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
1877
1935
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", children: numStr }),
@@ -2016,13 +2074,13 @@ var HelpModal = ({
2016
2074
  const keys = [
2017
2075
  ["\u2191 \u2193", "navigate"],
2018
2076
  ["\u2190 \u2192 \xB7 tab", "switch tab"],
2019
- ["\u21B5 \xB7 d", "open / drill in"],
2077
+ ["\u21B5 \xB7 d", "open / drill in \xB7 repo: checkout"],
2020
2078
  ["m", "actions \xB7 close"],
2021
2079
  ["e", "explain this row"],
2022
2080
  ["o", "open in browser"],
2023
- ["O", "open ticket + its PRs"],
2081
+ ["O", "open tree \xB7 whole repo group"],
2024
2082
  ["c", "copy URL"],
2025
- ["C", "copy ticket + its PRs"],
2083
+ ["C", "copy tree \xB7 whole repo group"],
2026
2084
  ["b", "copy branch"],
2027
2085
  ["s", "switch to branch here"],
2028
2086
  ["j", "open repo in new tab"],
@@ -2462,7 +2520,11 @@ var BrowseScreen = ({
2462
2520
  const ext = extensionFor(input, extensions);
2463
2521
  if (ext) {
2464
2522
  onOpenExt?.(ext.id, {
2465
- item: activeItem ?? void 0,
2523
+ // ExtensionTarget.item has always been documented as absent on a header
2524
+ // row, and until the fence became selectable that was true for free.
2525
+ // Now it has to be said: every extension body narrows on `kind` for a
2526
+ // pr / issue / task and none of them expect a repo-header.
2527
+ item: activeItem && !isHeader(activeItem) ? activeItem : void 0,
2466
2528
  ciJob: ciStatus?.job,
2467
2529
  login,
2468
2530
  onRemove: (row) => dismissItem(row),
@@ -2470,8 +2532,42 @@ var BrowseScreen = ({
2470
2532
  });
2471
2533
  return;
2472
2534
  }
2473
- if (!activeItem || activeItem.kind === "repo-header" || activeItem.kind === "subgroup-header")
2535
+ if (activeItem?.kind === "repo-header") {
2536
+ const { repo } = activeItem;
2537
+ const repoUrl = `https://github.com/${repo}`;
2538
+ if (key.return || input === "d" || input === "j") {
2539
+ showFlash(`\u22EF Opening ${repo}\u2026`);
2540
+ void jumpToRepo(repo, "", login).then(() => showFlash(`\u2197 Opened ${repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
2541
+ return;
2542
+ }
2543
+ if (input === "o") {
2544
+ quietly`open ${repoUrl}`.catch(() => {
2545
+ });
2546
+ showFlash(`\u2197 Opened ${repo}`);
2547
+ return;
2548
+ }
2549
+ if (input === "c") {
2550
+ clipboard(repoUrl);
2551
+ showFlash(`\u2713 Copied URL for ${repo}`);
2552
+ return;
2553
+ }
2554
+ if (input === "C" || input === "O") {
2555
+ const urls = treeUrls(section.items, cursor);
2556
+ if (urls.length === 0) return;
2557
+ const n = `${urls.length} URL${urls.length === 1 ? "" : "s"}`;
2558
+ if (input === "C") {
2559
+ clipboard(urls.join("\n"));
2560
+ showFlash(`\u2713 Copied ${n} from ${repo}`);
2561
+ } else {
2562
+ quietly`open ${urls}`.catch(() => {
2563
+ });
2564
+ showFlash(`\u2197 Opened ${n} from ${repo}`);
2565
+ }
2566
+ return;
2567
+ }
2474
2568
  return;
2569
+ }
2570
+ if (!activeItem || activeItem.kind === "subgroup-header") return;
2475
2571
  if (key.return && activeItem.kind === "show-more") {
2476
2572
  setLocalSections(
2477
2573
  (prev) => prev.map((s) => ({
@@ -2482,7 +2578,12 @@ var BrowseScreen = ({
2482
2578
  {
2483
2579
  kind: "show-less",
2484
2580
  toHide: activeItem.hidden,
2485
- indent: true
2581
+ // The depth of the row it replaces, never a constant. A
2582
+ // collapsed row sitting under a story is at depth 2, and
2583
+ // pinning it to 1 makes the walk in `treeUrls` stop one
2584
+ // level early and drop every row it holds — silently, since
2585
+ // nothing on screen reports what the clipboard missed.
2586
+ depth: depthOf(activeItem)
2486
2587
  }
2487
2588
  ] : [i]
2488
2589
  )
@@ -2499,7 +2600,9 @@ var BrowseScreen = ({
2499
2600
  {
2500
2601
  kind: "show-more",
2501
2602
  hidden: activeItem.toHide,
2502
- indent: true
2603
+ // Same reasoning as the show-less above: carry the depth
2604
+ // of the row being replaced, not a constant.
2605
+ depth: depthOf(activeItem)
2503
2606
  }
2504
2607
  ] : [i]
2505
2608
  )
@@ -2651,7 +2754,15 @@ var BrowseScreen = ({
2651
2754
  menu.open(actions);
2652
2755
  }
2653
2756
  });
2654
- const hints = [
2757
+ const hints = activeItem?.kind === "repo-header" ? [
2758
+ ["\u2191\u2193", "nav"],
2759
+ ["\u2190\u2192", "tab"],
2760
+ ["\u21B5", "open repo"],
2761
+ ["o", "browser"],
2762
+ ["C", "copy group"],
2763
+ ["?", "help"],
2764
+ ["q", "quit"]
2765
+ ] : [
2655
2766
  ["\u2191\u2193", "nav"],
2656
2767
  ["\u2190\u2192", "tab"],
2657
2768
  ["\u21B5/d", "open"],
@@ -2743,8 +2854,8 @@ var BrowseScreen = ({
2743
2854
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2744
2855
  leaving: (item.kind === "pr" || item.kind === "issue") && !!leavingUrls?.includes(item.url),
2745
2856
  transient: transientOf(transients, item),
2746
- lastChild: "indent" in item && item.indent ? !isChildRow(section.items[viewStart + i + 1]) : void 0,
2747
- parent: item.kind === "task" && isChildRow(section.items[viewStart + i + 1]),
2857
+ prefix: treePrefix(section.items, viewStart + i),
2858
+ parent: item.kind === "task" && depthOf(section.items[viewStart + i + 1]) > depthOf(item),
2748
2859
  sparkFrame,
2749
2860
  gap: i > 0 && gapsAbove(section.items, viewStart + i)
2750
2861
  },
@@ -2952,7 +3063,7 @@ var App = ({
2952
3063
  receive(fresh);
2953
3064
  }).catch((err) => {
2954
3065
  setRefreshing(false);
2955
- const message = err.message;
3066
+ const message = explainGhAction(err);
2956
3067
  if (!displayedKey.current) {
2957
3068
  setState({ phase: "failed", message });
2958
3069
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.32.2",
3
+ "version": "0.34.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",