@kud/gh-ink 0.29.1 → 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) => {
@@ -1074,7 +1090,8 @@ var treeUrls = (items, i) => {
1074
1090
  var gapsAbove = (items, i) => {
1075
1091
  const item = items[i];
1076
1092
  if (!item || i === 0) return false;
1077
- if (item.kind === "repo-header" || item.kind === "subgroup-header") return true;
1093
+ if (item.kind === "repo-header" || item.kind === "subgroup-header")
1094
+ return true;
1078
1095
  if (item.kind !== "task") return false;
1079
1096
  return isChildRow(items[i + 1]) || isChildRow(items[i - 1]);
1080
1097
  };
@@ -1342,9 +1359,15 @@ var UNSUBSCRIBE_MUTATION = `
1342
1359
  }
1343
1360
  `;
1344
1361
  var GH_ACTION_FAILURES = [
1345
- [/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
+ ],
1346
1366
  [/\b401\b|not logged in|authentication/i, "gh is not authenticated"],
1347
- [/\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
+ ],
1348
1371
  [/\b50[0234]\b/, "GitHub's API is failing (5xx)"],
1349
1372
  [/ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNREFUSED/i, "no route to GitHub"]
1350
1373
  ];
@@ -1468,9 +1491,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1468
1491
  hint: "u",
1469
1492
  run: () => {
1470
1493
  showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
1471
- void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(
1472
- (e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`)
1473
- );
1494
+ void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch((e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`));
1474
1495
  }
1475
1496
  });
1476
1497
  if (item.kind === "pr" && item.branch) {
@@ -1744,6 +1765,7 @@ var MERGED_FRAMES = ["\u2726", "\u2727", "\u2736", "\u2727"];
1744
1765
  var MERGED_COLOUR = "#A371F7";
1745
1766
  var TRANSIT_HOLD_MS = 7e3;
1746
1767
  var NO_TRANSIENTS = /* @__PURE__ */ new Map();
1768
+ var NO_HOLDS = /* @__PURE__ */ new Map();
1747
1769
  var TRANSIT_OUT_FRAMES = ["\u25C9", "\u25CE", "\u25CB", "\xB7"];
1748
1770
  var TRANSIT_IN_FRAMES = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
1749
1771
  var TRANSIT_COLOUR = {
@@ -2807,6 +2829,7 @@ var App = ({
2807
2829
  const displayedSections = useRef([]);
2808
2830
  const [transients, setTransients] = useState(NO_TRANSIENTS);
2809
2831
  const transitTabs = useRef(/* @__PURE__ */ new Map());
2832
+ const [heldSince, setHeldSince] = useState(NO_HOLDS);
2810
2833
  const [visibleTab, setVisibleTab] = useState("");
2811
2834
  const showData = (sections, login) => {
2812
2835
  const before = displayedSections.current;
@@ -2861,15 +2884,31 @@ var App = ({
2861
2884
  );
2862
2885
  };
2863
2886
  useEffect(() => {
2864
- if (transients.size === 0) return;
2865
- if (state.phase !== "browse" || !visibleTab) return;
2866
- 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();
2867
2903
  const timer = setTimeout(
2868
- () => settleTab(visibleTab, transients),
2869
- 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))))
2870
2909
  );
2871
2910
  return () => clearTimeout(timer);
2872
- }, [transients, visibleTab, state.phase]);
2911
+ }, [transients, heldSince, state.phase]);
2873
2912
  const pendingSummary = useMemo(
2874
2913
  () => pending ? summariseDiff(
2875
2914
  diffSections(displayedSections.current, pending.sections).counts
@@ -2917,10 +2956,7 @@ var App = ({
2917
2956
  }
2918
2957
  showData(fresh.sections, fresh.login);
2919
2958
  } else if (freshKey !== displayedKey.current) {
2920
- const { counts } = diffSections(
2921
- displayedSections.current,
2922
- fresh.sections
2923
- );
2959
+ const { counts } = diffSections(displayedSections.current, fresh.sections);
2924
2960
  if (counts.added + counts.removed + counts.changed === 0)
2925
2961
  showData(fresh.sections, fresh.login);
2926
2962
  else setPending(fresh);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.29.1",
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
  },