@kud/gh-ink 0.29.0 → 0.30.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
@@ -94,6 +94,7 @@ type GHDetail = {
94
94
  checksPass: number;
95
95
  checksFail: number;
96
96
  checksPending: number;
97
+ checksStale?: number;
97
98
  threadsTotal: number;
98
99
  lastCommitAt?: string;
99
100
  lastEventAt?: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
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, StatusMessage, FooterHints, useListCursor, Tabs } from '@kud/ink-ui';
4
- import { isPassCheck, isFailCheck, resolveThread, unresolveThread, replyToThread, rerunFailedRun, mergePr, reRequestReviewer } from '@kud/gh';
4
+ import { isPassCheck, isFailCheck, isInconclusiveCheck, 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';
@@ -417,7 +417,7 @@ var CommentsPanel = ({
417
417
  ) : null
418
418
  ] });
419
419
  };
420
- var checkIcon = (c) => isPassCheck(c) ? ["\u2713", colors.success] : isFailCheck(c) ? ["\u2717", colors.error] : ["\xB7", colors.warning];
420
+ var checkIcon = (c) => isPassCheck(c) ? ["\u2713", colors.success] : isFailCheck(c) ? ["\u2717", colors.error] : isInconclusiveCheck(c) ? ["-", colors.warning] : ["\xB7", colors.warning];
421
421
  var checkLabel = (c) => {
422
422
  const name = c.context ?? c.name ?? "";
423
423
  return c.workflowName ? `${c.workflowName} / ${name}` : name;
@@ -491,9 +491,11 @@ var HealthPanel = ({
491
491
  }
492
492
  };
493
493
  const retrigger = () => {
494
- const failing = checks.find((c) => isFailCheck(c) && runIdOf(checkUrl(c)));
495
- const runId = failing && runIdOf(checkUrl(failing));
496
- if (!runId) return flash("No failed Actions run to retrigger");
494
+ const stuck = checks.find(
495
+ (c) => (isFailCheck(c) || isInconclusiveCheck(c)) && runIdOf(checkUrl(c))
496
+ );
497
+ const runId = stuck && runIdOf(checkUrl(stuck));
498
+ if (!runId) return flash("No Actions run to retrigger");
497
499
  void act("retrigger CI", () => rerunFailedRun(repo, runId));
498
500
  };
499
501
  useInput((input, key) => {
@@ -528,7 +530,8 @@ var HealthPanel = ({
528
530
  const reviewerMap = Object.fromEntries(reviewers);
529
531
  const passed = checks.filter(isPassCheck).length;
530
532
  const failed = checks.filter(isFailCheck).length;
531
- const pending = checks.length - passed - failed;
533
+ const stale = checks.filter(isInconclusiveCheck).length;
534
+ const pending = checks.length - passed - failed - stale;
532
535
  const approved = Object.values(reviewerMap).filter(
533
536
  (s) => s === "APPROVED"
534
537
  ).length;
@@ -537,6 +540,7 @@ var HealthPanel = ({
537
540
  ).length;
538
541
  const checkParts = [[`${passed} passed`, colors.success]];
539
542
  if (failed > 0) checkParts.push([`${failed} failed`, colors.error]);
543
+ if (stale > 0) checkParts.push([`${stale} cancelled`, colors.warning]);
540
544
  if (pending > 0) checkParts.push([`${pending} pending`, colors.warning]);
541
545
  const reviewParts = [
542
546
  approved > 0 ? [`${approved} approved`, colors.success] : ["0 approved", "white"]
@@ -823,11 +827,13 @@ var healthSentence = (item) => {
823
827
  };
824
828
  var checksSentence = (d) => {
825
829
  if (!d) return null;
826
- const total = d.checksPass + d.checksFail + d.checksPending;
830
+ const stale = d.checksStale ?? 0;
831
+ const total = d.checksPass + d.checksFail + d.checksPending + stale;
827
832
  if (total === 0) return "No CI checks run on it.";
828
833
  const parts = [];
829
834
  if (d.checksPass) parts.push(`${d.checksPass} passing`);
830
835
  if (d.checksFail) parts.push(`${d.checksFail} failing`);
836
+ if (stale) parts.push(`${stale} cancelled`);
831
837
  if (d.checksPending) parts.push(`${d.checksPending} running`);
832
838
  return `Checks: ${parts.join(", ")}.`;
833
839
  };
@@ -920,7 +926,14 @@ var STANDING = {
920
926
  reviewed: "spoken"
921
927
  };
922
928
  var YOURS = {
923
- authored: ["ci-fail", "conflict", "changes-req", "threads", "approved", "draft"],
929
+ authored: [
930
+ "ci-fail",
931
+ "conflict",
932
+ "changes-req",
933
+ "threads",
934
+ "approved",
935
+ "draft"
936
+ ],
924
937
  queued: ["waiting", "pending", "threads", "approved"],
925
938
  spoken: ["threads"]
926
939
  };
@@ -1052,7 +1065,10 @@ var budgetNotice = (budget, now = Date.now()) => {
1052
1065
  1,
1053
1066
  Math.round((new Date(budget.resetAt).getTime() - now) / 6e4)
1054
1067
  );
1055
- return fetches <= 0 ? { label: `\u26A1 API budget spent \xB7 ${mins}m`, critical: true } : { label: `\u26A1 ${fetches} fetch${fetches === 1 ? "" : "es"} left`, critical: false };
1068
+ return fetches <= 0 ? { label: `\u26A1 API budget spent \xB7 ${mins}m`, critical: true } : {
1069
+ label: `\u26A1 ${fetches} fetch${fetches === 1 ? "" : "es"} left`,
1070
+ critical: false
1071
+ };
1056
1072
  };
1057
1073
  var isChildRow = (item) => !!item && (item.kind === "show-more" || item.kind === "show-less" || "indent" in item && item.indent === true);
1058
1074
  var treeUrls = (items, i) => {
@@ -1060,8 +1076,13 @@ var treeUrls = (items, i) => {
1060
1076
  while (root > 0 && isChildRow(items[root])) root -= 1;
1061
1077
  const urls = [];
1062
1078
  for (let j = root; j < items.length; j += 1) {
1063
- if (j > root && !isChildRow(items[j])) break;
1064
- const url = items[j].url;
1079
+ const item = items[j];
1080
+ if (j > root && !isChildRow(item)) break;
1081
+ if (item.kind === "show-more") {
1082
+ for (const child of item.hidden) if (child.url) urls.push(child.url);
1083
+ continue;
1084
+ }
1085
+ const url = item.url;
1065
1086
  if (url) urls.push(url);
1066
1087
  }
1067
1088
  return urls;
@@ -1069,7 +1090,8 @@ var treeUrls = (items, i) => {
1069
1090
  var gapsAbove = (items, i) => {
1070
1091
  const item = items[i];
1071
1092
  if (!item || i === 0) return false;
1072
- if (item.kind === "repo-header" || item.kind === "subgroup-header") return true;
1093
+ if (item.kind === "repo-header" || item.kind === "subgroup-header")
1094
+ return true;
1073
1095
  if (item.kind !== "task") return false;
1074
1096
  return isChildRow(items[i + 1]) || isChildRow(items[i - 1]);
1075
1097
  };
@@ -1337,9 +1359,15 @@ var UNSUBSCRIBE_MUTATION = `
1337
1359
  }
1338
1360
  `;
1339
1361
  var GH_ACTION_FAILURES = [
1340
- [/rate limit|RATE_LIMIT/i, "GitHub rate limit spent \u2014 it resets within the hour"],
1362
+ [
1363
+ /rate limit|RATE_LIMIT/i,
1364
+ "GitHub rate limit spent \u2014 it resets within the hour"
1365
+ ],
1341
1366
  [/\b401\b|not logged in|authentication/i, "gh is not authenticated"],
1342
- [/\b403\b|must have admin|not authorized|permission/i, "not permitted on this repo"],
1367
+ [
1368
+ /\b403\b|must have admin|not authorized|permission/i,
1369
+ "not permitted on this repo"
1370
+ ],
1343
1371
  [/\b50[0234]\b/, "GitHub's API is failing (5xx)"],
1344
1372
  [/ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNREFUSED/i, "no route to GitHub"]
1345
1373
  ];
@@ -1463,9 +1491,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1463
1491
  hint: "u",
1464
1492
  run: () => {
1465
1493
  showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
1466
- void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(
1467
- (e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`)
1468
- );
1494
+ void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch((e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`));
1469
1495
  }
1470
1496
  });
1471
1497
  if (item.kind === "pr" && item.branch) {
@@ -1739,6 +1765,7 @@ var MERGED_FRAMES = ["\u2726", "\u2727", "\u2736", "\u2727"];
1739
1765
  var MERGED_COLOUR = "#A371F7";
1740
1766
  var TRANSIT_HOLD_MS = 7e3;
1741
1767
  var NO_TRANSIENTS = /* @__PURE__ */ new Map();
1768
+ var NO_HOLDS = /* @__PURE__ */ new Map();
1742
1769
  var TRANSIT_OUT_FRAMES = ["\u25C9", "\u25CE", "\u25CB", "\xB7"];
1743
1770
  var TRANSIT_IN_FRAMES = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
1744
1771
  var TRANSIT_COLOUR = {
@@ -2802,6 +2829,7 @@ var App = ({
2802
2829
  const displayedSections = useRef([]);
2803
2830
  const [transients, setTransients] = useState(NO_TRANSIENTS);
2804
2831
  const transitTabs = useRef(/* @__PURE__ */ new Map());
2832
+ const [heldSince, setHeldSince] = useState(NO_HOLDS);
2805
2833
  const [visibleTab, setVisibleTab] = useState("");
2806
2834
  const showData = (sections, login) => {
2807
2835
  const before = displayedSections.current;
@@ -2856,15 +2884,31 @@ var App = ({
2856
2884
  );
2857
2885
  };
2858
2886
  useEffect(() => {
2859
- if (transients.size === 0) return;
2860
- if (state.phase !== "browse" || !visibleTab) return;
2861
- if (![...transitTabs.current.values()].includes(visibleTab)) return;
2887
+ const held = new Set(transitTabs.current.values());
2888
+ const arrived = state.phase === "browse" && !!visibleTab && held.has(visibleTab);
2889
+ setHeldSince((prev) => {
2890
+ const next = new Map([...prev].filter(([tab]) => held.has(tab)));
2891
+ if (arrived && !next.has(visibleTab)) next.set(visibleTab, Date.now());
2892
+ const same = next.size === prev.size && [...next.keys()].every((t) => prev.has(t));
2893
+ return same ? prev : next.size === 0 ? NO_HOLDS : next;
2894
+ });
2895
+ }, [transients, visibleTab, state.phase]);
2896
+ useEffect(() => {
2897
+ if (transients.size === 0 || heldSince.size === 0) return;
2898
+ if (state.phase !== "browse") return;
2899
+ const held = new Set(transitTabs.current.values());
2900
+ const running = [...heldSince].filter(([tab]) => held.has(tab));
2901
+ if (running.length === 0) return;
2902
+ const remaining = (at) => at + TRANSIT_HOLD_MS - Date.now();
2862
2903
  const timer = setTimeout(
2863
- () => settleTab(visibleTab, transients),
2864
- TRANSIT_HOLD_MS
2904
+ () => {
2905
+ for (const [tab, at] of running)
2906
+ if (remaining(at) <= 0) settleTab(tab, transients);
2907
+ },
2908
+ Math.max(0, Math.min(...running.map(([, at]) => remaining(at))))
2865
2909
  );
2866
2910
  return () => clearTimeout(timer);
2867
- }, [transients, visibleTab, state.phase]);
2911
+ }, [transients, heldSince, state.phase]);
2868
2912
  const pendingSummary = useMemo(
2869
2913
  () => pending ? summariseDiff(
2870
2914
  diffSections(displayedSections.current, pending.sections).counts
@@ -2912,10 +2956,7 @@ var App = ({
2912
2956
  }
2913
2957
  showData(fresh.sections, fresh.login);
2914
2958
  } else if (freshKey !== displayedKey.current) {
2915
- const { counts } = diffSections(
2916
- displayedSections.current,
2917
- fresh.sections
2918
- );
2959
+ const { counts } = diffSections(displayedSections.current, fresh.sections);
2919
2960
  if (counts.added + counts.removed + counts.changed === 0)
2920
2961
  showData(fresh.sections, fresh.login);
2921
2962
  else setPending(fresh);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.29.0",
3
+ "version": "0.30.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",
@@ -48,7 +48,7 @@
48
48
  "react": ">=19"
49
49
  },
50
50
  "dependencies": {
51
- "@kud/gh": "0.7.1",
51
+ "@kud/gh": "0.8.0",
52
52
  "@kud/ink-ui": "0.14.0",
53
53
  "zx": "8.8.5"
54
54
  },