@kud/gh-ink 0.11.1 → 0.13.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
@@ -85,6 +85,13 @@ type JiraRow = {
85
85
  age: string;
86
86
  indent: boolean;
87
87
  instanceKey?: string;
88
+ /**
89
+ * Trailing annotation, rendered dim after the summary — a recurrence marker,
90
+ * a source hint, anything secondary to the title. Its own node rather than
91
+ * part of `summary` so it can be dimmed, and so its width is measured
92
+ * separately instead of being smuggled past the truncation maths.
93
+ */
94
+ note?: string;
88
95
  };
89
96
  type RepoHeader = {
90
97
  kind: "repo-header";
@@ -225,7 +232,7 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
225
232
  actions: Action[];
226
233
  cursor: number;
227
234
  }) => React.JSX.Element;
228
- declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, initialIncludeWork, jiraBase, jiraKeyRe, jiraTransitions, workToggle, hasCiStatus, ciJob, ciFetcher, ciPollMs, extensions, tabHelp, emptyHint, }: {
235
+ declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, initialIncludeWork, jiraBase, jiraKeyRe, jiraTransitions, workToggle, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, extensions, tabHelp, emptyHint, }: {
229
236
  fetcher: () => Promise<{
230
237
  sections: Section[];
231
238
  login: string;
@@ -246,6 +253,14 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, initialIn
246
253
  ciJob?: string;
247
254
  ciFetcher?: () => Promise<CiStatus | null>;
248
255
  ciPollMs?: number;
256
+ /**
257
+ * A file something else touches when it has changed GitHub on your behalf —
258
+ * a Claude session closing an issue, a script merging a PR. Touching it makes
259
+ * the inbox refetch; it never repaints on its own.
260
+ */
261
+ watchPath?: string;
262
+ /** Bursts of writes to collapse into one refetch. */
263
+ watchDebounceMs?: number;
249
264
  extensions?: InboxExtension[];
250
265
  }) => React.JSX.Element;
251
266
 
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import React2, { createContext, useState, useRef, useEffect, useContext } from 'react';
1
+ import React2, { createContext, useState, useRef, useEffect, useMemo, useContext } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
3
  import { colors, ScrollView, TextInput, LoadingScreen, Switch, StatusMessage, FooterHints, useListCursor, Tabs } 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';
7
7
  import { spawn } from 'child_process';
8
- import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, rmSync } from 'fs';
9
- import { join } from 'path';
8
+ import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, watch, rmSync } from 'fs';
9
+ import { join, dirname, basename } from 'path';
10
10
  import { homedir } from 'os';
11
11
 
12
12
  // src/components/comments-panel.tsx
@@ -637,6 +637,102 @@ var invalidateCache = (key) => {
637
637
  } catch {
638
638
  }
639
639
  };
640
+
641
+ // src/inbox/diff.ts
642
+ var keyOf = (item) => {
643
+ if (item.kind === "pr" || item.kind === "issue") return item.url;
644
+ if (item.kind === "jira") {
645
+ const row = item;
646
+ return row.url || `jira:${row.key}`;
647
+ }
648
+ return null;
649
+ };
650
+ var renderedState = (item) => {
651
+ if (item.kind === "pr" || item.kind === "issue") {
652
+ const i = item;
653
+ return [
654
+ i.title,
655
+ i.health,
656
+ i.unresolved,
657
+ i.conversation,
658
+ i.lastActor ?? "",
659
+ i.author ?? ""
660
+ ].join("|");
661
+ }
662
+ if (item.kind === "jira") {
663
+ const j = item;
664
+ return [j.summary, j.jiraStatus].join("|");
665
+ }
666
+ return "";
667
+ };
668
+ var indexRows = (sections) => {
669
+ const out = /* @__PURE__ */ new Map();
670
+ for (const section of sections) {
671
+ section.items.forEach((item, index) => {
672
+ const key = keyOf(item);
673
+ if (key) out.set(key, { item, sectionId: section.id, index });
674
+ });
675
+ }
676
+ return out;
677
+ };
678
+ var spliceBack = (next, leaving) => {
679
+ if (leaving.length === 0) return next;
680
+ const bySection = /* @__PURE__ */ new Map();
681
+ for (const row of leaving) {
682
+ const list = bySection.get(row.sectionId);
683
+ if (list) list.push(row);
684
+ else bySection.set(row.sectionId, [row]);
685
+ }
686
+ return next.map((section) => {
687
+ const rows = bySection.get(section.id);
688
+ if (!rows) return section;
689
+ const items = [...section.items];
690
+ for (const row of [...rows].sort((a, b) => a.index - b.index))
691
+ items.splice(Math.min(row.index, items.length), 0, row.item);
692
+ return { ...section, items };
693
+ });
694
+ };
695
+ var diffSections = (prev, next) => {
696
+ const before = indexRows(prev);
697
+ const after = indexRows(next);
698
+ const transients = /* @__PURE__ */ new Map();
699
+ const leaving = [];
700
+ for (const [key] of after) if (!before.has(key)) transients.set(key, "in");
701
+ for (const [key, was] of before) {
702
+ const now = after.get(key);
703
+ if (!now) {
704
+ transients.set(key, "out");
705
+ leaving.push(was);
706
+ } else if (renderedState(was.item) !== renderedState(now.item)) {
707
+ transients.set(key, "changed");
708
+ }
709
+ }
710
+ let added = 0;
711
+ let removed = 0;
712
+ let changed = 0;
713
+ for (const [, kind] of transients) {
714
+ if (kind === "in") added++;
715
+ else if (kind === "out") removed++;
716
+ else changed++;
717
+ }
718
+ return {
719
+ transients,
720
+ union: spliceBack(next, leaving),
721
+ counts: { added, removed, changed }
722
+ };
723
+ };
724
+ var summariseDiff = (counts) => {
725
+ const parts = [];
726
+ if (counts.added) parts.push(`${counts.added} new`);
727
+ if (counts.removed) parts.push(`${counts.removed} gone`);
728
+ if (counts.changed) parts.push(`${counts.changed} moved`);
729
+ return parts.join(" \xB7 ");
730
+ };
731
+ var transientOf = (transients, item) => {
732
+ if (!transients?.size) return void 0;
733
+ const key = keyOf(item);
734
+ return key ? transients.get(key) : void 0;
735
+ };
640
736
  var quietly = $({ quiet: true });
641
737
  var relativeTime = (iso) => {
642
738
  const diff = (Date.now() - new Date(iso).getTime()) / 1e3;
@@ -657,15 +753,24 @@ var healthSentence = (item) => {
657
753
  case "draft":
658
754
  return `Still a draft (${glyph}), so no review is being asked for yet.`;
659
755
  case "ci-fail":
660
- return `CI is failing (${glyph}) \u2014 ${plural(d?.checksFail ?? 0, "check")} red.`;
756
+ return `CI is failing (${glyph}) \u2014 ${plural(
757
+ d?.checksFail ?? 0,
758
+ "check"
759
+ )} red.`;
661
760
  case "conflict":
662
761
  return `It conflicts with the base branch (${glyph}) and cannot merge until that is resolved.`;
663
762
  case "changes-req":
664
763
  return `Changes were requested (${glyph}).`;
665
764
  case "threads":
666
- return `${plural(item.unresolved, "review thread")} still open (${glyph}).`;
765
+ return `${plural(
766
+ item.unresolved,
767
+ "review thread"
768
+ )} still open (${glyph}).`;
667
769
  case "pending":
668
- return `${plural(d?.checksPending ?? 0, "check")} still running (${glyph}).`;
770
+ return `${plural(
771
+ d?.checksPending ?? 0,
772
+ "check"
773
+ )} still running (${glyph}).`;
669
774
  case "approved":
670
775
  return `Approved (${glyph}) and ready to merge.`;
671
776
  case "waiting":
@@ -698,7 +803,9 @@ var turnSentences = (item, login) => {
698
803
  const lines = [`You spoke last (\u2192), ${when}. The ball is with ${them}.`];
699
804
  if (d?.lastCommitAt && d.lastEventAt && d.lastCommitAt < d.lastEventAt)
700
805
  lines.push(
701
- `Nothing has been pushed since ${relativeTime(d.lastCommitAt)} ago, so it is stalled on ${them}, not on you.`
806
+ `Nothing has been pushed since ${relativeTime(
807
+ d.lastCommitAt
808
+ )} ago, so it is stalled on ${them}, not on you.`
702
809
  );
703
810
  return lines;
704
811
  };
@@ -1370,13 +1477,14 @@ var InboxHeader = ({
1370
1477
  quiet,
1371
1478
  refreshing,
1372
1479
  hasPending,
1480
+ pendingSummary,
1373
1481
  fetchedAt
1374
1482
  }) => {
1375
1483
  const total = sections.reduce((n, s) => n + topLevelCount(s), 0);
1376
1484
  const countSeg = loading ? " loading\u2026 " : quiet ? " " : ` ${String(total).padStart(3)} item${total !== 1 ? "s" : ""} \xB7 `;
1377
1485
  const userSeg = loading || quiet ? "" : `@${login} `;
1378
1486
  const workLabel = work === void 0 ? "" : " w work \u25CF\u2500\u25CB home ";
1379
- const [statusText, statusColor] = hasPending ? ["\u25CF new \xB7 r apply", "#FF8700"] : refreshing ? ["\u21BB refreshing\u2026", "cyan"] : fetchedAt ? [`updated ${agoText(fetchedAt)}`, void 0] : ["", void 0];
1487
+ const [statusText, statusColor] = hasPending ? [`\u25CF ${pendingSummary || "new"} \xB7 r apply`, "#FF8700"] : refreshing ? ["\u21BB refreshing\u2026", "cyan"] : fetchedAt ? [`updated ${agoText(fetchedAt)}`, void 0] : ["", void 0];
1380
1488
  const statusSeg = statusText ? statusText + " " : "";
1381
1489
  const fill = Math.max(
1382
1490
  4,
@@ -1452,12 +1560,27 @@ var MERGED_HOLD_MS = 3e3;
1452
1560
  var MERGED_FRAME_MS = 150;
1453
1561
  var MERGED_FRAMES = ["\u2726", "\u2727", "\u2736", "\u2727"];
1454
1562
  var MERGED_COLOUR = "#A371F7";
1563
+ var TRANSIT_HOLD_MS = 2500;
1564
+ var NO_TRANSIENTS = /* @__PURE__ */ new Map();
1565
+ var TRANSIT_OUT_FRAMES = ["\u25C9", "\u25CE", "\u25CB", "\xB7"];
1566
+ var TRANSIT_IN_FRAMES = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
1567
+ var TRANSIT_COLOUR = {
1568
+ in: "#3FB950",
1569
+ out: "#8B949E",
1570
+ changed: "#FF8700"
1571
+ };
1572
+ var TRANSIT_LABEL = {
1573
+ in: "NEW",
1574
+ out: "GONE",
1575
+ changed: "UPDATED"
1576
+ };
1455
1577
  var ItemRow = ({
1456
1578
  item,
1457
1579
  active,
1458
1580
  gap,
1459
1581
  login,
1460
1582
  merged,
1583
+ transient,
1461
1584
  sparkFrame = 0
1462
1585
  }) => {
1463
1586
  if (item.kind === "repo-header")
@@ -1482,27 +1605,31 @@ var ItemRow = ({
1482
1605
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: "show less" })
1483
1606
  ] });
1484
1607
  if (item.kind === "jira") {
1485
- const titleMax2 = Math.max(20, COLS - item.key.length - 10);
1608
+ const note = item.note ?? "";
1609
+ const titleMax2 = Math.max(20, COLS - item.key.length - note.length - 10);
1486
1610
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1487
1611
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1488
1612
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: active, children: item.key + " " }),
1489
- /* @__PURE__ */ jsx(Text2, { bold: active, children: truncate(item.summary, titleMax2) })
1613
+ /* @__PURE__ */ jsx(Text2, { bold: active, children: truncate(item.summary, titleMax2) }),
1614
+ note ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: ` ${note}` }) : null
1490
1615
  ] });
1491
1616
  }
1492
1617
  const { glyph: healthIcon, color: healthColor2 } = healthDisplay[item.health];
1493
- const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : healthIcon;
1494
- const color = merged ? MERGED_COLOUR : healthColor2;
1618
+ const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : transient === "out" ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : transient === "in" ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : healthIcon;
1619
+ const color = merged ? MERGED_COLOUR : transient ? TRANSIT_COLOUR[transient] : healthColor2;
1495
1620
  const [turnIcon, turnColor] = !login || !item.lastActor ? [" ", "white"] : item.lastActor === login ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
1496
1621
  const numStr = `#${item.number}`.padEnd(7);
1497
1622
  const showAuthor = !!item.author && item.author !== login;
1498
1623
  const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
1499
1624
  const ageLabel = item.activityAge && item.activityAge !== item.age ? `${item.activityAge} \xB7 ${item.age}` : item.age;
1500
1625
  const mergedLabel = merged ? "MERGED" : "";
1626
+ const transitLabel = merged || !transient ? "" : TRANSIT_LABEL[transient];
1501
1627
  const suffix = [
1502
1628
  ageLabel || "",
1503
1629
  unresolvedLabel,
1504
1630
  showAuthor ? `by ${item.author}` : "",
1505
- mergedLabel
1631
+ mergedLabel,
1632
+ transitLabel
1506
1633
  ].filter(Boolean).join(" ");
1507
1634
  const repoLabel = item.indent ? item.repo : "";
1508
1635
  const fixedWidth = 2 + (item.indent ? 3 : 0) + 2 + 2 + 7 + repoLabel.length + suffix.length + 6;
@@ -1513,12 +1640,21 @@ var ItemRow = ({
1513
1640
  /* @__PURE__ */ jsx(Text2, { color, bold: true, children: icon + " " }),
1514
1641
  /* @__PURE__ */ jsx(Text2, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
1515
1642
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", children: numStr }),
1516
- /* @__PURE__ */ jsx(Text2, { bold: active, children: truncate(item.title, titleMax) + " " }),
1643
+ /* @__PURE__ */ jsx(
1644
+ Text2,
1645
+ {
1646
+ bold: active || transient === "in",
1647
+ dimColor: transient === "out",
1648
+ strikethrough: transient === "out",
1649
+ children: truncate(item.title, titleMax) + " "
1650
+ }
1651
+ ),
1517
1652
  repoLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: repoLabel }) : null,
1518
1653
  unresolvedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: "#FF8700", children: " " + unresolvedLabel }) : null,
1519
1654
  showAuthor ? /* @__PURE__ */ jsx(Text2, { dimColor: true, italic: true, children: " by " + item.author }) : null,
1520
1655
  ageLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + ageLabel }) : null,
1521
- mergedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: MERGED_COLOUR, children: " " + mergedLabel }) : null
1656
+ mergedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: MERGED_COLOUR, children: " " + mergedLabel }) : null,
1657
+ transitLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: TRANSIT_COLOUR[transient], children: " " + transitLabel }) : null
1522
1658
  ] });
1523
1659
  };
1524
1660
  var useActionMenu = () => {
@@ -1794,6 +1930,7 @@ var BrowseScreen = ({
1794
1930
  onActed,
1795
1931
  refreshing,
1796
1932
  hasPending,
1933
+ pendingSummary,
1797
1934
  fetchedAt,
1798
1935
  refreshError,
1799
1936
  workToggle,
@@ -1808,7 +1945,8 @@ var BrowseScreen = ({
1808
1945
  isWorkRepo,
1809
1946
  initialIncludeWork,
1810
1947
  brand,
1811
- mergedUrls
1948
+ mergedUrls,
1949
+ transients
1812
1950
  }) => {
1813
1951
  const { rows } = useWindowSize();
1814
1952
  const [includeWork, setIncludeWork] = useState(
@@ -1899,7 +2037,7 @@ var BrowseScreen = ({
1899
2037
  setTimeout(() => setFlash(null), 2e3);
1900
2038
  };
1901
2039
  const [sparkFrame, setSparkFrame] = useState(0);
1902
- const sparkling = (mergedUrls?.length ?? 0) > 0;
2040
+ const sparkling = (mergedUrls?.length ?? 0) > 0 || (transients?.size ?? 0) > 0;
1903
2041
  useEffect(() => {
1904
2042
  if (!sparkling) return;
1905
2043
  const id = setInterval(() => setSparkFrame((f) => f + 1), MERGED_FRAME_MS);
@@ -2231,6 +2369,7 @@ var BrowseScreen = ({
2231
2369
  work: workToggle ? includeWork : void 0,
2232
2370
  refreshing,
2233
2371
  hasPending,
2372
+ pendingSummary,
2234
2373
  fetchedAt
2235
2374
  }
2236
2375
  ),
@@ -2250,13 +2389,7 @@ var BrowseScreen = ({
2250
2389
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: " / " }),
2251
2390
  /* @__PURE__ */ jsx(Text2, { children: search }),
2252
2391
  searchInput ? /* @__PURE__ */ jsx(Text2, { color: "cyan", children: "\u258F" }) : null,
2253
- /* @__PURE__ */ jsx(
2254
- Text2,
2255
- {
2256
- dimColor: true,
2257
- children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}`
2258
- }
2259
- )
2392
+ /* @__PURE__ */ jsx(Text2, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}` })
2260
2393
  ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
2261
2394
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", children: " \u25C9 " }),
2262
2395
  /* @__PURE__ */ jsx(Text2, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
@@ -2271,6 +2404,7 @@ var BrowseScreen = ({
2271
2404
  active: viewStart + i === cursor,
2272
2405
  login,
2273
2406
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2407
+ transient: transientOf(transients, item),
2274
2408
  sparkFrame,
2275
2409
  gap: (
2276
2410
  // Window-relative, never `viewStart + i`. fitCount prices the
@@ -2353,6 +2487,8 @@ var App = ({
2353
2487
  ciJob,
2354
2488
  ciFetcher,
2355
2489
  ciPollMs = 6e4,
2490
+ watchPath,
2491
+ watchDebounceMs = 400,
2356
2492
  extensions,
2357
2493
  tabHelp,
2358
2494
  emptyHint
@@ -2370,12 +2506,47 @@ var App = ({
2370
2506
  return sameCiStatusState(prev, next) ? prev : next;
2371
2507
  });
2372
2508
  const displayedKey = useRef("");
2509
+ const displayedSections = useRef([]);
2510
+ const [transients, setTransients] = useState(NO_TRANSIENTS);
2511
+ const transitTimer = useRef(null);
2512
+ useEffect(
2513
+ () => () => {
2514
+ if (transitTimer.current) clearTimeout(transitTimer.current);
2515
+ },
2516
+ []
2517
+ );
2373
2518
  const showData = (sections, login) => {
2519
+ const before = displayedSections.current;
2520
+ displayedSections.current = sections;
2374
2521
  displayedKey.current = signatureOf(sections);
2375
2522
  setPending(null);
2376
2523
  setFetchedAt(Date.now());
2377
- setState({ phase: "browse", sections, login });
2524
+ if (before.length === 0) {
2525
+ setTransients(NO_TRANSIENTS);
2526
+ setState({ phase: "browse", sections, login });
2527
+ return;
2528
+ }
2529
+ const { transients: marks, union } = diffSections(before, sections);
2530
+ if (marks.size === 0) {
2531
+ setState({ phase: "browse", sections, login });
2532
+ return;
2533
+ }
2534
+ setTransients(marks);
2535
+ 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);
2378
2543
  };
2544
+ const pendingSummary = useMemo(
2545
+ () => pending ? summariseDiff(
2546
+ diffSections(displayedSections.current, pending.sections).counts
2547
+ ) : "",
2548
+ [pending]
2549
+ );
2379
2550
  const revalidate = (manual = false) => {
2380
2551
  if (manual) setRefreshing(true);
2381
2552
  fetcher().then((fresh) => {
@@ -2419,6 +2590,7 @@ var App = ({
2419
2590
  const painted = !!cached && cached.sections.length > 0;
2420
2591
  if (painted && cached) {
2421
2592
  displayedKey.current = signatureOf(cached.sections);
2593
+ displayedSections.current = cached.sections;
2422
2594
  setFetchedAt(cached.at);
2423
2595
  setState({
2424
2596
  phase: "browse",
@@ -2445,6 +2617,29 @@ var App = ({
2445
2617
  clearInterval(id);
2446
2618
  };
2447
2619
  }, [hasCiStatus, ciFetcher, ciPollMs]);
2620
+ useEffect(() => {
2621
+ if (!watchPath) return;
2622
+ const dir = dirname(watchPath);
2623
+ const name = basename(watchPath);
2624
+ if (!existsSync(dir)) return;
2625
+ let timer = null;
2626
+ let live = true;
2627
+ let watcher;
2628
+ try {
2629
+ watcher = watch(dir, (_event, changed) => {
2630
+ if (!live || changed && changed !== name) return;
2631
+ if (timer) clearTimeout(timer);
2632
+ timer = setTimeout(() => live && revalidate(), watchDebounceMs);
2633
+ });
2634
+ } catch {
2635
+ return;
2636
+ }
2637
+ return () => {
2638
+ live = false;
2639
+ if (timer) clearTimeout(timer);
2640
+ watcher.close();
2641
+ };
2642
+ }, [watchPath, watchDebounceMs]);
2448
2643
  const [mergedUrls, setMergedUrls] = useState([]);
2449
2644
  const mergeTimers = useRef([]);
2450
2645
  useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
@@ -2521,6 +2716,7 @@ var App = ({
2521
2716
  onActed,
2522
2717
  refreshing,
2523
2718
  hasPending: pending !== null,
2719
+ pendingSummary,
2524
2720
  fetchedAt,
2525
2721
  refreshError: refreshError ?? void 0,
2526
2722
  workToggle,
@@ -2528,6 +2724,7 @@ var App = ({
2528
2724
  initialIncludeWork,
2529
2725
  hidden: overlay !== null,
2530
2726
  mergedUrls,
2727
+ transients,
2531
2728
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
2532
2729
  ciJob,
2533
2730
  tabHelp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.11.1",
3
+ "version": "0.13.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",