@kud/gh-ink 0.11.0 → 0.12.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
@@ -225,7 +225,7 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
225
225
  actions: Action[];
226
226
  cursor: number;
227
227
  }) => React.JSX.Element;
228
- declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, initialIncludeWork, jiraBase, jiraKeyRe, jiraTransitions, workToggle, hasCiStatus, ciJob, ciFetcher, ciPollMs, extensions, tabHelp, emptyHint, }: {
228
+ declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, initialIncludeWork, jiraBase, jiraKeyRe, jiraTransitions, workToggle, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, extensions, tabHelp, emptyHint, }: {
229
229
  fetcher: () => Promise<{
230
230
  sections: Section[];
231
231
  login: string;
@@ -246,6 +246,14 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, initialIn
246
246
  ciJob?: string;
247
247
  ciFetcher?: () => Promise<CiStatus | null>;
248
248
  ciPollMs?: number;
249
+ /**
250
+ * A file something else touches when it has changed GitHub on your behalf —
251
+ * a Claude session closing an issue, a script merging a PR. Touching it makes
252
+ * the inbox refetch; it never repaints on its own.
253
+ */
254
+ watchPath?: string;
255
+ /** Bursts of writes to collapse into one refetch. */
256
+ watchDebounceMs?: number;
249
257
  extensions?: InboxExtension[];
250
258
  }) => React.JSX.Element;
251
259
 
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")
@@ -1490,19 +1613,21 @@ var ItemRow = ({
1490
1613
  ] });
1491
1614
  }
1492
1615
  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;
1616
+ 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;
1617
+ const color = merged ? MERGED_COLOUR : transient ? TRANSIT_COLOUR[transient] : healthColor2;
1495
1618
  const [turnIcon, turnColor] = !login || !item.lastActor ? [" ", "white"] : item.lastActor === login ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
1496
1619
  const numStr = `#${item.number}`.padEnd(7);
1497
1620
  const showAuthor = !!item.author && item.author !== login;
1498
1621
  const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
1499
1622
  const ageLabel = item.activityAge && item.activityAge !== item.age ? `${item.activityAge} \xB7 ${item.age}` : item.age;
1500
1623
  const mergedLabel = merged ? "MERGED" : "";
1624
+ const transitLabel = merged || !transient ? "" : TRANSIT_LABEL[transient];
1501
1625
  const suffix = [
1502
1626
  ageLabel || "",
1503
1627
  unresolvedLabel,
1504
1628
  showAuthor ? `by ${item.author}` : "",
1505
- mergedLabel
1629
+ mergedLabel,
1630
+ transitLabel
1506
1631
  ].filter(Boolean).join(" ");
1507
1632
  const repoLabel = item.indent ? item.repo : "";
1508
1633
  const fixedWidth = 2 + (item.indent ? 3 : 0) + 2 + 2 + 7 + repoLabel.length + suffix.length + 6;
@@ -1513,12 +1638,21 @@ var ItemRow = ({
1513
1638
  /* @__PURE__ */ jsx(Text2, { color, bold: true, children: icon + " " }),
1514
1639
  /* @__PURE__ */ jsx(Text2, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
1515
1640
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", children: numStr }),
1516
- /* @__PURE__ */ jsx(Text2, { bold: active, children: truncate(item.title, titleMax) + " " }),
1641
+ /* @__PURE__ */ jsx(
1642
+ Text2,
1643
+ {
1644
+ bold: active || transient === "in",
1645
+ dimColor: transient === "out",
1646
+ strikethrough: transient === "out",
1647
+ children: truncate(item.title, titleMax) + " "
1648
+ }
1649
+ ),
1517
1650
  repoLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: repoLabel }) : null,
1518
1651
  unresolvedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: "#FF8700", children: " " + unresolvedLabel }) : null,
1519
1652
  showAuthor ? /* @__PURE__ */ jsx(Text2, { dimColor: true, italic: true, children: " by " + item.author }) : null,
1520
1653
  ageLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + ageLabel }) : null,
1521
- mergedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: MERGED_COLOUR, children: " " + mergedLabel }) : null
1654
+ mergedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: MERGED_COLOUR, children: " " + mergedLabel }) : null,
1655
+ transitLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: TRANSIT_COLOUR[transient], children: " " + transitLabel }) : null
1522
1656
  ] });
1523
1657
  };
1524
1658
  var useActionMenu = () => {
@@ -1794,6 +1928,7 @@ var BrowseScreen = ({
1794
1928
  onActed,
1795
1929
  refreshing,
1796
1930
  hasPending,
1931
+ pendingSummary,
1797
1932
  fetchedAt,
1798
1933
  refreshError,
1799
1934
  workToggle,
@@ -1808,7 +1943,8 @@ var BrowseScreen = ({
1808
1943
  isWorkRepo,
1809
1944
  initialIncludeWork,
1810
1945
  brand,
1811
- mergedUrls
1946
+ mergedUrls,
1947
+ transients
1812
1948
  }) => {
1813
1949
  const { rows } = useWindowSize();
1814
1950
  const [includeWork, setIncludeWork] = useState(
@@ -1899,7 +2035,7 @@ var BrowseScreen = ({
1899
2035
  setTimeout(() => setFlash(null), 2e3);
1900
2036
  };
1901
2037
  const [sparkFrame, setSparkFrame] = useState(0);
1902
- const sparkling = (mergedUrls?.length ?? 0) > 0;
2038
+ const sparkling = (mergedUrls?.length ?? 0) > 0 || (transients?.size ?? 0) > 0;
1903
2039
  useEffect(() => {
1904
2040
  if (!sparkling) return;
1905
2041
  const id = setInterval(() => setSparkFrame((f) => f + 1), MERGED_FRAME_MS);
@@ -2231,6 +2367,7 @@ var BrowseScreen = ({
2231
2367
  work: workToggle ? includeWork : void 0,
2232
2368
  refreshing,
2233
2369
  hasPending,
2370
+ pendingSummary,
2234
2371
  fetchedAt
2235
2372
  }
2236
2373
  ),
@@ -2250,13 +2387,7 @@ var BrowseScreen = ({
2250
2387
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: " / " }),
2251
2388
  /* @__PURE__ */ jsx(Text2, { children: search }),
2252
2389
  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
- )
2390
+ /* @__PURE__ */ jsx(Text2, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}` })
2260
2391
  ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
2261
2392
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", children: " \u25C9 " }),
2262
2393
  /* @__PURE__ */ jsx(Text2, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
@@ -2271,16 +2402,24 @@ var BrowseScreen = ({
2271
2402
  active: viewStart + i === cursor,
2272
2403
  login,
2273
2404
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2405
+ transient: transientOf(transients, item),
2274
2406
  sparkFrame,
2275
- gap: viewStart + i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2276
- // first child in Other PRs that's "free" because the
2277
- // child is itself a repo-header (gap above). A jira row
2278
- // has no such stand-in, so it needs this explicitly. Never
2279
- // applies between two tickets/PRs only right after a
2280
- // header.
2281
- item.kind === "jira" && ["repo-header", "subgroup-header"].includes(
2282
- section.items[viewStart + i - 1]?.kind
2283
- ))
2407
+ gap: (
2408
+ // Window-relative, never `viewStart + i`. fitCount prices the
2409
+ // window's FIRST row at one line (isFirst), so gapping it when
2410
+ // scrolled draws a row the budget never bought and the frame
2411
+ // is sized to fill the terminal exactly, so the overflow scrolls
2412
+ // the whole panel up a line instead of clipping.
2413
+ i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2414
+ // first child in Other PRs that's "free" because the
2415
+ // child is itself a repo-header (gap above). A jira row
2416
+ // has no such stand-in, so it needs this explicitly. Never
2417
+ // applies between two tickets/PRs — only right after a
2418
+ // header.
2419
+ item.kind === "jira" && ["repo-header", "subgroup-header"].includes(
2420
+ section.items[viewStart + i - 1]?.kind
2421
+ ))
2422
+ )
2284
2423
  },
2285
2424
  `${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}`}`
2286
2425
  )) }),
@@ -2346,6 +2485,8 @@ var App = ({
2346
2485
  ciJob,
2347
2486
  ciFetcher,
2348
2487
  ciPollMs = 6e4,
2488
+ watchPath,
2489
+ watchDebounceMs = 400,
2349
2490
  extensions,
2350
2491
  tabHelp,
2351
2492
  emptyHint
@@ -2363,12 +2504,47 @@ var App = ({
2363
2504
  return sameCiStatusState(prev, next) ? prev : next;
2364
2505
  });
2365
2506
  const displayedKey = useRef("");
2507
+ const displayedSections = useRef([]);
2508
+ const [transients, setTransients] = useState(NO_TRANSIENTS);
2509
+ const transitTimer = useRef(null);
2510
+ useEffect(
2511
+ () => () => {
2512
+ if (transitTimer.current) clearTimeout(transitTimer.current);
2513
+ },
2514
+ []
2515
+ );
2366
2516
  const showData = (sections, login) => {
2517
+ const before = displayedSections.current;
2518
+ displayedSections.current = sections;
2367
2519
  displayedKey.current = signatureOf(sections);
2368
2520
  setPending(null);
2369
2521
  setFetchedAt(Date.now());
2370
- setState({ phase: "browse", sections, login });
2522
+ if (before.length === 0) {
2523
+ setTransients(NO_TRANSIENTS);
2524
+ setState({ phase: "browse", sections, login });
2525
+ return;
2526
+ }
2527
+ const { transients: marks, union } = diffSections(before, sections);
2528
+ if (marks.size === 0) {
2529
+ setState({ phase: "browse", sections, login });
2530
+ return;
2531
+ }
2532
+ setTransients(marks);
2533
+ setState({ phase: "browse", sections: union, login });
2534
+ if (transitTimer.current) clearTimeout(transitTimer.current);
2535
+ transitTimer.current = setTimeout(() => {
2536
+ setTransients(NO_TRANSIENTS);
2537
+ setState(
2538
+ (prev) => prev.phase === "browse" && prev.sections === union ? { ...prev, sections } : prev
2539
+ );
2540
+ }, TRANSIT_HOLD_MS);
2371
2541
  };
2542
+ const pendingSummary = useMemo(
2543
+ () => pending ? summariseDiff(
2544
+ diffSections(displayedSections.current, pending.sections).counts
2545
+ ) : "",
2546
+ [pending]
2547
+ );
2372
2548
  const revalidate = (manual = false) => {
2373
2549
  if (manual) setRefreshing(true);
2374
2550
  fetcher().then((fresh) => {
@@ -2412,6 +2588,7 @@ var App = ({
2412
2588
  const painted = !!cached && cached.sections.length > 0;
2413
2589
  if (painted && cached) {
2414
2590
  displayedKey.current = signatureOf(cached.sections);
2591
+ displayedSections.current = cached.sections;
2415
2592
  setFetchedAt(cached.at);
2416
2593
  setState({
2417
2594
  phase: "browse",
@@ -2438,6 +2615,29 @@ var App = ({
2438
2615
  clearInterval(id);
2439
2616
  };
2440
2617
  }, [hasCiStatus, ciFetcher, ciPollMs]);
2618
+ useEffect(() => {
2619
+ if (!watchPath) return;
2620
+ const dir = dirname(watchPath);
2621
+ const name = basename(watchPath);
2622
+ if (!existsSync(dir)) return;
2623
+ let timer = null;
2624
+ let live = true;
2625
+ let watcher;
2626
+ try {
2627
+ watcher = watch(dir, (_event, changed) => {
2628
+ if (!live || changed && changed !== name) return;
2629
+ if (timer) clearTimeout(timer);
2630
+ timer = setTimeout(() => live && revalidate(), watchDebounceMs);
2631
+ });
2632
+ } catch {
2633
+ return;
2634
+ }
2635
+ return () => {
2636
+ live = false;
2637
+ if (timer) clearTimeout(timer);
2638
+ watcher.close();
2639
+ };
2640
+ }, [watchPath, watchDebounceMs]);
2441
2641
  const [mergedUrls, setMergedUrls] = useState([]);
2442
2642
  const mergeTimers = useRef([]);
2443
2643
  useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
@@ -2514,6 +2714,7 @@ var App = ({
2514
2714
  onActed,
2515
2715
  refreshing,
2516
2716
  hasPending: pending !== null,
2717
+ pendingSummary,
2517
2718
  fetchedAt,
2518
2719
  refreshError: refreshError ?? void 0,
2519
2720
  workToggle,
@@ -2521,6 +2722,7 @@ var App = ({
2521
2722
  initialIncludeWork,
2522
2723
  hidden: overlay !== null,
2523
2724
  mergedUrls,
2725
+ transients,
2524
2726
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
2525
2727
  ciJob,
2526
2728
  tabHelp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",