@kud/gh-ink 0.24.0 → 0.25.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
@@ -270,6 +270,7 @@ declare const runHere: (cmd: string) => void;
270
270
  declare const COLS: number;
271
271
  declare const topLevelCount: (s: Section) => number;
272
272
  declare const drillCmd: (item: AnyItem) => string | null;
273
+ declare const explainGhAction: (e: unknown) => string;
273
274
  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?: {
274
275
  extensions?: InboxExtension[];
275
276
  onOpenExt?: (id: string, target: ExtensionTarget) => void;
@@ -314,7 +315,7 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
314
315
  cursor: number;
315
316
  }) => React.JSX.Element;
316
317
  declare const signatureOf: (sections: Section[]) => string;
317
- declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, extensions, tabHelp, emptyHint, }: {
318
+ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, }: {
318
319
  fetcher: () => Promise<{
319
320
  sections: Section[];
320
321
  login: string;
@@ -342,6 +343,15 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
342
343
  watchPath?: string;
343
344
  /** Bursts of writes to collapse into one refetch. */
344
345
  watchDebounceMs?: number;
346
+ /**
347
+ * Random spread added to `watchDebounceMs`, so several cockpits woken by one
348
+ * signal take turns instead of stampeding. The first to wake fetches and
349
+ * writes the shared cache; the others adopt it and pay nothing.
350
+ *
351
+ * Defaults to 0 — no randomness unless a host asks for it. Set it to comfortably
352
+ * more than one round trip when you expect several instances to be open.
353
+ */
354
+ watchJitterMs?: number;
345
355
  extensions?: InboxExtension[];
346
356
  }) => React.JSX.Element;
347
357
 
@@ -443,4 +453,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
443
453
  */
444
454
  declare const parsePatterns: (value: string) => string[];
445
455
 
446
- 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 InboxConfig, type InboxExtension, type JiraTransition, type OriginSplit, type RepoFilter, type RepoHeader, type RepoProfile, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
456
+ 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 InboxConfig, type InboxExtension, type JiraTransition, type OriginSplit, type RepoFilter, type RepoHeader, type RepoProfile, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/dist/index.js CHANGED
@@ -1317,9 +1317,22 @@ var UNSUBSCRIBE_MUTATION = `
1317
1317
  }
1318
1318
  }
1319
1319
  `;
1320
+ var GH_ACTION_FAILURES = [
1321
+ [/rate limit|RATE_LIMIT/i, "GitHub rate limit spent \u2014 it resets within the hour"],
1322
+ [/\b401\b|not logged in|authentication/i, "gh is not authenticated"],
1323
+ [/\b403\b|must have admin|not authorized|permission/i, "not permitted on this repo"],
1324
+ [/\b50[0234]\b/, "GitHub's API is failing (5xx)"],
1325
+ [/ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNREFUSED/i, "no route to GitHub"]
1326
+ ];
1327
+ var explainGhAction = (e) => {
1328
+ const err = e;
1329
+ const raw = (err.stderr ?? "").trim().split("\n").filter(Boolean).pop() ?? err.message ?? "no reason reported";
1330
+ const human = GH_ACTION_FAILURES.find(([re]) => re.test(raw))?.[1];
1331
+ return human ? `${human} (${raw})` : raw;
1332
+ };
1320
1333
  var unsubscribeFrom = async (item) => {
1321
- const view = item.kind === "pr" ? "pr" : "issue";
1322
- const found = await quietly`gh ${view} view ${item.number} --repo ${item.repo} --json id --jq .id`;
1334
+ const route = item.kind === "pr" ? "pulls" : "issues";
1335
+ const found = await quietly`gh api repos/${item.repo}/${route}/${item.number} --jq .node_id`;
1323
1336
  const id = found.stdout.trim();
1324
1337
  if (!id) throw new Error(`no node id for ${item.repo}#${item.number}`);
1325
1338
  await quietly`gh api graphql -f query=${UNSUBSCRIBE_MUTATION} -f id=${id}`;
@@ -1419,8 +1432,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1419
1432
  void quietly`gh pr edit ${item.number} --repo ${item.repo} --remove-reviewer ${login}`.then(() => {
1420
1433
  showFlash(`\u2713 Removed you as reviewer on #${item.number}`);
1421
1434
  ext?.onActed?.();
1422
- }).catch(() => {
1423
- showFlash(`\u2717 Could not remove you from #${item.number}`);
1435
+ }).catch((e) => {
1436
+ showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`);
1424
1437
  onRefresh?.();
1425
1438
  });
1426
1439
  }
@@ -1431,7 +1444,9 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1431
1444
  hint: "u",
1432
1445
  run: () => {
1433
1446
  showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
1434
- void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(() => showFlash(`\u2717 Unsubscribe failed for #${item.number}`));
1447
+ void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(
1448
+ (e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`)
1449
+ );
1435
1450
  }
1436
1451
  });
1437
1452
  if (item.kind === "pr" && item.branch) {
@@ -2466,7 +2481,9 @@ var BrowseScreen = ({
2466
2481
  }
2467
2482
  if (input === "u" && (activeItem.kind === "pr" || activeItem.kind === "issue")) {
2468
2483
  showFlash(`\u22EF Unsubscribing from #${activeItem.number}\u2026`);
2469
- void unsubscribeFrom(activeItem).then(() => showFlash(`\u2713 Unsubscribed from #${activeItem.number}`)).catch(() => showFlash(`\u2717 Unsubscribe failed for #${activeItem.number}`));
2484
+ void unsubscribeFrom(activeItem).then(() => showFlash(`\u2713 Unsubscribed from #${activeItem.number}`)).catch(
2485
+ (e) => showFlash(`\u2717 #${activeItem.number}: ${explainGhAction(e)}`)
2486
+ );
2470
2487
  return;
2471
2488
  }
2472
2489
  if (input === "x" && activeItem.kind === "pr" && activeItem.standing === "queued" && login) {
@@ -2475,8 +2492,8 @@ var BrowseScreen = ({
2475
2492
  void quietly`gh pr edit ${activeItem.number} --repo ${activeItem.repo} --remove-reviewer ${login}`.then(() => {
2476
2493
  showFlash(`\u2713 Removed you as reviewer on #${activeItem.number}`);
2477
2494
  onActed?.();
2478
- }).catch(() => {
2479
- showFlash(`\u2717 Could not remove you from #${activeItem.number}`);
2495
+ }).catch((e) => {
2496
+ showFlash(`\u2717 #${activeItem.number}: ${explainGhAction(e)}`);
2480
2497
  onRefresh?.();
2481
2498
  });
2482
2499
  return;
@@ -2705,6 +2722,11 @@ var App = ({
2705
2722
  ciPollMs = 6e4,
2706
2723
  watchPath,
2707
2724
  watchDebounceMs = 400,
2725
+ // Zero by default, deliberately. A package that adds randomness to its own
2726
+ // timing makes every host's tests flaky — this one's went green locally on a
2727
+ // lucky draw and red in CI. It is also a host concern: only somebody running
2728
+ // several cockpits at once needs the stagger, and only they know how many.
2729
+ watchJitterMs = 0,
2708
2730
  extensions,
2709
2731
  tabHelp,
2710
2732
  emptyHint
@@ -2802,24 +2824,7 @@ var App = ({
2802
2824
  setFetchedAt(Date.now());
2803
2825
  setRefreshError(null);
2804
2826
  if (hasCiStatus) applyCiStatus(fresh.ciStatus ?? null);
2805
- const freshKey = signatureOf(fresh.sections);
2806
- if (!displayedKey.current) {
2807
- if (fresh.sections.length === 0) {
2808
- setState({ phase: "empty" });
2809
- return;
2810
- }
2811
- showData(fresh.sections, fresh.login);
2812
- } else if (freshKey !== displayedKey.current) {
2813
- const { counts } = diffSections(
2814
- displayedSections.current,
2815
- fresh.sections
2816
- );
2817
- if (counts.added + counts.removed + counts.changed === 0)
2818
- showData(fresh.sections, fresh.login);
2819
- else setPending(fresh);
2820
- } else {
2821
- setPending(null);
2822
- }
2827
+ receive(fresh);
2823
2828
  }).catch((err) => {
2824
2829
  setRefreshing(false);
2825
2830
  const message = err.message;
@@ -2830,6 +2835,26 @@ var App = ({
2830
2835
  setRefreshError({ message, at: Date.now() });
2831
2836
  });
2832
2837
  };
2838
+ const receive = (fresh) => {
2839
+ const freshKey = signatureOf(fresh.sections);
2840
+ if (!displayedKey.current) {
2841
+ if (fresh.sections.length === 0) {
2842
+ setState({ phase: "empty" });
2843
+ return;
2844
+ }
2845
+ showData(fresh.sections, fresh.login);
2846
+ } else if (freshKey !== displayedKey.current) {
2847
+ const { counts } = diffSections(
2848
+ displayedSections.current,
2849
+ fresh.sections
2850
+ );
2851
+ if (counts.added + counts.removed + counts.changed === 0)
2852
+ showData(fresh.sections, fresh.login);
2853
+ else setPending(fresh);
2854
+ } else {
2855
+ setPending(null);
2856
+ }
2857
+ };
2833
2858
  const applyOrRefresh = () => {
2834
2859
  if (pending) showData(pending.sections, pending.login);
2835
2860
  else revalidate(true);
@@ -2882,7 +2907,18 @@ var App = ({
2882
2907
  watcher = watch(dir, (_event, changed) => {
2883
2908
  if (!live || changed && changed !== name) return;
2884
2909
  if (timer) clearTimeout(timer);
2885
- timer = setTimeout(() => live && revalidate(), watchDebounceMs);
2910
+ const firedAt = Date.now();
2911
+ const delay = watchDebounceMs + Math.floor(Math.random() * watchJitterMs);
2912
+ timer = setTimeout(() => {
2913
+ if (!live) return;
2914
+ const shared = cacheKey ? readCache(cacheKey) : null;
2915
+ if (shared && shared.at > firedAt) {
2916
+ receive({ sections: shared.sections, login: shared.login });
2917
+ setFetchedAt(shared.at);
2918
+ return;
2919
+ }
2920
+ revalidate();
2921
+ }, delay);
2886
2922
  });
2887
2923
  } catch {
2888
2924
  return;
@@ -3029,4 +3065,4 @@ var matchesFilter = (repo, filter) => {
3029
3065
  };
3030
3066
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
3031
3067
 
3032
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
3068
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.24.0",
3
+ "version": "0.25.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",