@kud/gh-ink 0.76.2 → 0.77.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +58 -24
  2. package/dist/index.js +230 -96
  3. package/package.json +10 -10
package/dist/index.d.ts CHANGED
@@ -467,10 +467,17 @@ type StripItem = {
467
467
  key: string;
468
468
  state: StripState;
469
469
  /**
470
- * A second, independent alarm beside the state — a build on fire next to a
471
- * service that is serving fine, say. Drawn as its own mark rather than folded
472
- * into `state`, because the two answer different questions and a reader
473
- * needs to see when they disagree.
470
+ * Why the state is what it is, as a short word the host chooses: `slow`,
471
+ * `errors`, `errors · slow`, `alerting`. Drawn after the mark in the mark's
472
+ * colour. Ignored for `ok`; `unknown` draws `no data` itself.
473
+ */
474
+ cause?: string;
475
+ /**
476
+ * The thing's latest trunk build is red. Independent of `state` and drawn
477
+ * as its own `build ✕` beside it rather than folded in, because the two
478
+ * answer different questions and a reader needs to see when they disagree.
479
+ * Folding them — a red build downgrading a tick to a warning — was ruled
480
+ * against on 2026-10-23: it loses which system is broken.
474
481
  */
475
482
  alarm?: boolean;
476
483
  /** Where `o` goes when this item is opened from a rail row. Unused here. */
@@ -480,30 +487,51 @@ type StatusStrip = {
480
487
  /** What the row is a strip OF, in the host's words: `services`, say. */
481
488
  title: string;
482
489
  items: StripItem[];
483
- /** When this snapshot was taken (ms). Drawn as an age at the end of the row. */
490
+ /** When this snapshot was taken (ms). Drawn as an age at the end of row 1. */
484
491
  at?: number;
492
+ /**
493
+ * Why every item is unknown, when it is one shared reason (`monitoring
494
+ * unreachable`). Turns row 1 into one sentence instead of one `? no data`
495
+ * per item — thirteen identical items say less than the sentence does.
496
+ */
497
+ note?: string;
485
498
  };
486
499
  declare const stripGlyph: (state: StripState) => [string, string];
487
- declare const ageLabel: (at: number | undefined, now?: number) => string;
488
- /**
489
- * The strip as plain text, at the width it has to fit. Exported so a test — or a
490
- * host laying out its own row — can ask the same question the renderer asks.
491
- *
492
- * Wide: every item named with its mark, so the row reads as a roll call. Narrow:
493
- * counts for what is fine, and names ONLY for what is not — the reader of a
494
- * narrow terminal loses the roll call, never the failures. When even the
495
- * failures do not fit, the worst few are named and the rest counted, so the
496
- * row still says "and N more" rather than silently dropping the fourth fire.
497
- */
498
- declare const stripLayout: (strip: StatusStrip, width: number, now?: number) => {
499
- parts: StripItem[] | null;
500
- summary: string;
500
+ /** One run of text in one style — the unit both the budget and the renderer read. */
501
+ type StripSpan = {
502
+ text: string;
503
+ color?: string;
504
+ bold?: boolean;
505
+ dim?: boolean;
506
+ };
507
+ type StripRows = {
508
+ /** Row 1 as plain text, without its title or age. */
509
+ issues: string;
510
+ /** Row 2 as plain text, without its title. */
511
+ success: string;
512
+ /** The age as drawn (`8m ago`), or `""` when it was shed or never stamped. */
501
513
  age: string;
514
+ /** The same rows as styled runs — exactly what the renderer draws. */
515
+ spans: {
516
+ issues: StripSpan[];
517
+ success: StripSpan[];
518
+ };
519
+ /** Both titles, padded to one width so the rows' content lines up. */
520
+ titles: [string, string];
502
521
  };
503
522
  /**
504
- * A standing single-line row, the same height across loading / absent / ready
505
- * so the content below it never jumps — the same contract `CiStatusLine` keeps,
506
- * and drawn in the same place, because a reader learns one row of glances.
523
+ * Both rows of the strip at the width they have to fit. Exported so a test —
524
+ * or a host laying out its own row — can ask the same question the renderer
525
+ * asks.
526
+ */
527
+ declare const stripRows: (strip: StatusStrip, width: number, now?: number) => StripRows;
528
+ declare const ageLabel: (at: number | undefined, now?: number) => string;
529
+ /**
530
+ * A standing two-line row, the same height across loading / absent / ready
531
+ * — two lines and one margin — so the content below it never jumps: the same
532
+ * contract `CiStatusLine` keeps, and drawn in the same place, because a reader
533
+ * learns one row of glances. The host reserves those three rows out of its
534
+ * own budget; a fourth here would clip the frame from the top.
507
535
  */
508
536
  declare const StatusStripLine: ({ strip, label, width, now, }: {
509
537
  /** `undefined` while the first poll is out; `null` when the host has nothing. */
@@ -961,7 +989,12 @@ type StripStatusState = {
961
989
  kind: "ready";
962
990
  strip: StatusStrip | null;
963
991
  };
964
- /** What the strip SAYS, ignoring when it said it. */
992
+ /**
993
+ * What the strip SAYS, ignoring when it said it. Every field the strip draws
994
+ * is in it — the cause word and the shared note included — or a cause moving
995
+ * from `slow` to `errors` would bail out of the repaint and leave the old
996
+ * word on screen.
997
+ */
965
998
  declare const stripSignature: (strip: StatusStrip | null) => string;
966
999
  declare const toCiStatusState: (status: CiStatus | null) => CiStatusState;
967
1000
  declare const sameCiStatusState: (a: CiStatusState, b: CiStatusState) => boolean;
@@ -1137,4 +1170,5 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
1137
1170
  */
1138
1171
  declare const parsePatterns: (value: string) => string[];
1139
1172
 
1140
- export { type Action, type ActionGroup, ActionMenu, App, COLS, type ChromeHints, type ChromeSpec, type ChromeState, CiStatusLine, type CiStatusState, type Command, CommentsPanel, type CommentsPanelProps, DEFAULT_FOCUS_EMPTY, DEFAULT_FOCUS_LABEL, type DetailContext, type ExplainSection, type ExtensionTarget, FOCUS_JUMP_KEY, FOCUS_MARK, type FocusGutter, type FocusSlot, FocusSlotLine, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxExtension, type JiraAvailableTransition, type JiraTransition, type JiraTransitionsFor, type LiveLabel, LookupMiss, MIN_SIDEBAR_COLS, type MenuGap, type MenuRow, PrRow, type PrRowProps, type Rails, type RepoFilter, type RowAnnouncement, type RowMotion, SIDEBAR_COLS, type SettledResult, SidePanel, type Sidebar, type SidebarRow, type StatusStrip, StatusStripLine, type StripItem, type StripState, type StripStatusState, type TabForStatus, type TrailingColumns, type TrailingShed, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, focusCellFor, focusRefColor, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
1173
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, DEFAULT_FOCUS_EMPTY, DEFAULT_FOCUS_LABEL, FOCUS_JUMP_KEY, FOCUS_MARK, FocusSlotLine, HealthPanel, LookupMiss, MIN_SIDEBAR_COLS, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, focusCellFor, focusRefColor, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripRows, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
1174
+ export type { Action, ActionGroup, ChromeHints, ChromeSpec, ChromeState, CiStatusState, Command, CommentsPanelProps, DetailContext, ExplainSection, ExtensionTarget, FocusGutter, FocusSlot, HealthPanelProps, InboxBudget, InboxExtension, JiraAvailableTransition, JiraTransition, JiraTransitionsFor, LiveLabel, MenuGap, MenuRow, PrRowProps, Rails, RepoFilter, RowAnnouncement, RowMotion, SettledResult, Sidebar, SidebarRow, StatusStrip, StripItem, StripRows, StripSpan, StripState, StripStatusState, TabForStatus, TrailingColumns, TrailingShed };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import React5, { createContext, useState, useRef, useContext, useEffect, useCallback, useMemo } from 'react';
1
+ import React2, { createContext, useState, useRef, useContext, useEffect, useCallback, useMemo } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
- import { colors, ScrollView, TextInput, Pill, getIconMode, FooterHints, useAppKeys, LoadingScreen, pillWidth, StatusMessage, useFilterMode, useListCursor, CommandPalette, Tabs, ProgressBar } from '@kud/ink-ui';
3
+ import { colors, ScrollView, TextInput, Pill, Spinner, getIconMode, FooterHints, useAppKeys, LoadingScreen, pillWidth, StatusMessage, useFilterMode, useListCursor, CommandPalette, Tabs, ProgressBar } from '@kud/ink-ui';
4
4
  import { isPassCheck, isFailCheck, isInconclusiveCheck, resolveThread, unresolveThread, replyToThread, rerunFailedRun, mergePr, reRequestReviewer, parseRef, formatRef, fetchItemNode } from '@kud/gh';
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
6
  import { PIN_MARK, sizeOf, sizePartsOf, depthOf, impliedLabels, labelPriority, checkoutDirs, profileOf, inboxConfig, relativeTime, filterBySearch, filterByRepos, filterByOrigin, refCandidates, resolveRef } from '@kud/gh-workflow';
@@ -444,7 +444,7 @@ var Summary = ({
444
444
  /* @__PURE__ */ jsx(Text, { children: " " }),
445
445
  /* @__PURE__ */ jsx(Text, { bold: true, children: label.padEnd(10) }),
446
446
  /* @__PURE__ */ jsx(Text, { children: icon + " " }),
447
- parts.map(([text, color], i) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
447
+ parts.map(([text, color], i) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
448
448
  i > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
449
449
  /* @__PURE__ */ jsx(Text, { color, children: text })
450
450
  ] }, text + i))
@@ -930,7 +930,7 @@ var layoutOf = ({
930
930
  }
931
931
  return `\uF02B ${fitted.join(", ")}`;
932
932
  };
933
- const widthOf2 = () => {
933
+ const widthOf3 = () => {
934
934
  const block = TRAILING.reduce(
935
935
  (w, k) => givingUp[k] || own[k] === 0 ? w : w + 2 + own[k],
936
936
  0
@@ -949,11 +949,11 @@ var layoutOf = ({
949
949
  () => givingUp.repo = true,
950
950
  () => givingUp.age = true
951
951
  ]) {
952
- if (cols - widthOf2() >= MIN_TITLE) break;
952
+ if (cols - widthOf3() >= MIN_TITLE) break;
953
953
  give();
954
954
  }
955
955
  const labelLabel = labelCell();
956
- const titleMax = Math.max(1, cols - widthOf2());
956
+ const titleMax = Math.max(1, cols - widthOf3());
957
957
  return {
958
958
  numStr,
959
959
  showAuthor,
@@ -1081,7 +1081,7 @@ var PrRow = ({
1081
1081
  /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: agePair.activity }),
1082
1082
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` (${agePair.lifetime})` })
1083
1083
  ] }) : ageLabel2 ? /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: ageLabel2 }) : null }) : null,
1084
- backdropped ? null : announcements.map((a) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
1084
+ backdropped ? null : announcements.map((a) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
1085
1085
  /* @__PURE__ */ jsx(Text3, { children: " " }),
1086
1086
  /* @__PURE__ */ jsx(Pill, { color: a.color, children: a.label })
1087
1087
  ] }, a.label))
@@ -1100,12 +1100,183 @@ var stripGlyph = (state) => {
1100
1100
  return ["?", colors.muted];
1101
1101
  }
1102
1102
  };
1103
- var ALARM = "\uF06D";
1103
+ var OK = stripGlyph("ok")[0];
1104
+ var WARN = stripGlyph("warn")[0];
1105
+ var FAIL = stripGlyph("fail")[0];
1106
+ var SUCCESS_TITLE = "success";
1107
+ var ITEM_GAP = " ";
1108
+ var dim = (text) => ({ text, dim: true });
1109
+ var widthOf = (spans) => spans.reduce((n, span) => n + span.text.length, 0);
1110
+ var textOf = (spans) => spans.map((span) => span.text).join("");
1111
+ var joinWith = (groups, sep) => groups.flatMap((group, i) => i === 0 ? group : [sep, ...group]);
1104
1112
  var SEVERITY = {
1105
1113
  fail: 0,
1106
1114
  warn: 1,
1107
- unknown: 2,
1108
- ok: 3
1115
+ build: 2,
1116
+ unknown: 3
1117
+ };
1118
+ var rankOf = (item) => {
1119
+ if (item.state === "ok") return item.alarm ? "build" : null;
1120
+ return item.state;
1121
+ };
1122
+ var buildSpans = () => [
1123
+ { text: "build ", color: colors.error },
1124
+ { text: FAIL, color: colors.error, bold: true }
1125
+ ];
1126
+ var nameSpans = (item) => [
1127
+ { text: item.key, bold: true },
1128
+ dim(":"),
1129
+ { text: " " }
1130
+ ];
1131
+ var itemSpans = (item) => {
1132
+ const spans = nameSpans(item);
1133
+ switch (item.state) {
1134
+ case "fail":
1135
+ spans.push({ text: FAIL, color: colors.error, bold: true });
1136
+ if (item.cause)
1137
+ spans.push({ text: ` ${item.cause}`, color: colors.error, bold: true });
1138
+ break;
1139
+ case "warn":
1140
+ spans.push({ text: WARN, color: colors.warning, bold: true });
1141
+ if (item.cause)
1142
+ spans.push({ text: ` ${item.cause}`, color: colors.warning });
1143
+ break;
1144
+ case "unknown":
1145
+ spans.push(dim("? no data"));
1146
+ break;
1147
+ case "ok":
1148
+ spans.push({ text: OK, color: colors.success });
1149
+ }
1150
+ if (item.alarm)
1151
+ spans.push({ text: " " }, dim("\xB7"), { text: " " }, ...buildSpans());
1152
+ return spans;
1153
+ };
1154
+ var countSpans = (rank, n) => {
1155
+ switch (rank) {
1156
+ case "fail":
1157
+ return [{ text: `${FAIL} ${n}`, color: colors.error, bold: true }];
1158
+ case "warn":
1159
+ return [{ text: `${WARN} ${n}`, color: colors.warning, bold: true }];
1160
+ case "build":
1161
+ return [...buildSpans(), { text: ` ${n}`, color: colors.error }];
1162
+ case "unknown":
1163
+ return [dim(`? ${n}`)];
1164
+ }
1165
+ };
1166
+ var foldedIssues = (bad, folded, failsNamed) => {
1167
+ let fails = 0;
1168
+ const named = [];
1169
+ const rest = [];
1170
+ for (const item of bad) {
1171
+ const rank = rankOf(item);
1172
+ const keep = rank === "fail" ? fails++ < failsNamed : !folded.has(rank);
1173
+ (keep ? named : rest).push(item);
1174
+ }
1175
+ const tallies = [
1176
+ ["fail", rest.filter((i) => i.state === "fail").length],
1177
+ ["warn", rest.filter((i) => i.state === "warn").length],
1178
+ ["build", rest.filter((i) => i.alarm).length],
1179
+ ["unknown", rest.filter((i) => i.state === "unknown").length]
1180
+ ];
1181
+ return joinWith(
1182
+ [
1183
+ ...named.map(itemSpans),
1184
+ ...tallies.filter(([, n]) => n > 0).map(([rank, n]) => countSpans(rank, n))
1185
+ ],
1186
+ { text: ITEM_GAP }
1187
+ );
1188
+ };
1189
+ var issueCandidates = (strip) => {
1190
+ const { items, note } = strip;
1191
+ if (note !== void 0) {
1192
+ const sentence = dim(`? no data for all ${items.length} \xB7 ${note}`);
1193
+ const builds = items.filter((i) => i.alarm);
1194
+ const named = joinWith(
1195
+ [[sentence], ...builds.map((i) => [...nameSpans(i), ...buildSpans()])],
1196
+ { text: ITEM_GAP }
1197
+ );
1198
+ const counted = joinWith(
1199
+ [
1200
+ [sentence],
1201
+ ...builds.length ? [countSpans("build", builds.length)] : []
1202
+ ],
1203
+ { text: ITEM_GAP }
1204
+ );
1205
+ return [
1206
+ { spans: named, age: true },
1207
+ { spans: counted, age: true },
1208
+ { spans: counted, age: false }
1209
+ ];
1210
+ }
1211
+ const bad = items.filter((i) => rankOf(i) !== null).sort((a, b) => SEVERITY[rankOf(a)] - SEVERITY[rankOf(b)]);
1212
+ if (bad.length === 0) {
1213
+ const calm = items.length > 0 ? [
1214
+ { text: OK, color: colors.success },
1215
+ { text: ` all ${items.length} fine`, bold: true }
1216
+ ] : [dim("? no data")];
1217
+ return [
1218
+ { spans: calm, age: true },
1219
+ { spans: calm, age: false }
1220
+ ];
1221
+ }
1222
+ const fails = bad.filter((i) => i.state === "fail").length;
1223
+ const levels = [
1224
+ [],
1225
+ ["unknown"],
1226
+ ["unknown", "build"],
1227
+ ["unknown", "build", "warn"]
1228
+ ];
1229
+ const everything = new Set(levels[levels.length - 1]);
1230
+ const candidates = levels.map((level) => ({
1231
+ spans: foldedIssues(bad, new Set(level), fails),
1232
+ age: true
1233
+ }));
1234
+ for (let named = fails; named >= 0; named--)
1235
+ candidates.push({ spans: foldedIssues(bad, everything, named), age: false });
1236
+ return candidates;
1237
+ };
1238
+ var successSpans = (strip, budget) => {
1239
+ const fine = strip.items.filter((i) => rankOf(i) === null);
1240
+ if (fine.length === 0)
1241
+ return [
1242
+ dim(
1243
+ strip.items.every((i) => i.state === "unknown") ? "nothing measured" : "none"
1244
+ )
1245
+ ];
1246
+ const tick = { text: OK, color: colors.success };
1247
+ const full = [
1248
+ tick,
1249
+ { text: " " },
1250
+ ...joinWith(
1251
+ fine.map((i) => [{ text: i.key, bold: true }]),
1252
+ dim(" \xB7 ")
1253
+ )
1254
+ ];
1255
+ if (widthOf(full) <= budget) return full;
1256
+ return [tick, { text: ` ${fine.length} ${strip.title}` }];
1257
+ };
1258
+ var stripRows = (strip, width, now = Date.now()) => {
1259
+ const titleWidth = Math.max(strip.title.length, SUCCESS_TITLE.length);
1260
+ const titles = [
1261
+ ` ${strip.title.padEnd(titleWidth)} `,
1262
+ ` ${SUCCESS_TITLE.padEnd(titleWidth)} `
1263
+ ];
1264
+ const label = ageLabel(strip.at, now);
1265
+ const ageText = label ? `${label} ago` : "";
1266
+ const ageWidth = ageText ? 3 + ageText.length : 0;
1267
+ const budget = width - titles[0].length;
1268
+ const candidates = issueCandidates(strip);
1269
+ const chosen = candidates.find(
1270
+ (c) => widthOf(c.spans) + (c.age ? ageWidth : 0) <= budget
1271
+ ) ?? candidates[candidates.length - 1];
1272
+ const success = successSpans(strip, budget);
1273
+ return {
1274
+ issues: textOf(chosen.spans),
1275
+ success: textOf(success),
1276
+ age: chosen.age ? ageText : "",
1277
+ spans: { issues: chosen.spans, success },
1278
+ titles
1279
+ };
1109
1280
  };
1110
1281
  var ageLabel = (at, now = Date.now()) => {
1111
1282
  if (at === void 0) return "";
@@ -1115,38 +1286,7 @@ var ageLabel = (at, now = Date.now()) => {
1115
1286
  if (m < 60) return `${m}m`;
1116
1287
  return `${Math.round(m / 60)}h`;
1117
1288
  };
1118
- var itemText = (item) => item.state === "fail" ? `${stripGlyph("fail")[0]} ${item.key}` : `${stripGlyph(item.state)[0]}${item.alarm ? ` ${ALARM}` : ""} ${item.key}`;
1119
- var itemWidth = (item) => item.state === "fail" ? pillWidth(itemText(item)) : itemText(item).length;
1120
- var stripLayout = (strip, width, now = Date.now()) => {
1121
- const age = ageLabel(strip.at, now);
1122
- const fixed = 2 + strip.title.length + 2 + (age ? age.length + 3 : 0);
1123
- const full = strip.items.map(itemText).join(" ");
1124
- const fullWidth = strip.items.reduce((n, item) => n + itemWidth(item), 0) + Math.max(0, strip.items.length - 1) * 2;
1125
- if (fixed + fullWidth <= width)
1126
- return { parts: strip.items, summary: full, age };
1127
- const sorted = [...strip.items].sort(
1128
- (a, b) => SEVERITY[a.state] - SEVERITY[b.state]
1129
- );
1130
- const bad = sorted.filter((i) => i.state !== "ok" || i.alarm);
1131
- const ok = strip.items.length - bad.length;
1132
- const tally = (n, s) => n > 0 ? `${n} ${stripGlyph(s)[0]}` : "";
1133
- let named = [];
1134
- let text = "";
1135
- for (let n = bad.length; n >= 0; n--) {
1136
- named = bad.slice(0, n);
1137
- const rest = bad.slice(n);
1138
- const counts2 = ["fail", "warn", "unknown"].map((s) => tally(rest.filter((i) => i.state === s).length, s)).filter(Boolean);
1139
- text = [tally(ok, "ok"), ...counts2, ...named.map(itemText)].filter(Boolean).join(" \xB7 ");
1140
- const textWidth = text.length + 2 * named.filter((i) => i.state === "fail").length;
1141
- if (fixed + textWidth <= width) break;
1142
- }
1143
- return { parts: null, summary: text, age };
1144
- };
1145
- var nameStyle = (item) => {
1146
- if (item.alarm) return { color: colors.error, bold: true };
1147
- if (item.state === "unknown") return { dimColor: true };
1148
- return {};
1149
- };
1289
+ var Spans = ({ spans }) => /* @__PURE__ */ jsx(Fragment, { children: spans.map((span, i) => /* @__PURE__ */ jsx(Text, { color: span.color, bold: span.bold, dimColor: span.dim, children: span.text }, i)) });
1150
1290
  var StatusStripLine = ({
1151
1291
  strip,
1152
1292
  label = "status",
@@ -1154,45 +1294,30 @@ var StatusStripLine = ({
1154
1294
  now
1155
1295
  }) => {
1156
1296
  if (strip === void 0)
1157
- return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1158
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1159
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${label} loading\u2026` })
1297
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1298
+ /* @__PURE__ */ jsxs(Box, { children: [
1299
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1300
+ /* @__PURE__ */ jsx(Spinner, {}),
1301
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${label} loading\u2026` })
1302
+ ] }),
1303
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${SUCCESS_TITLE}` })
1160
1304
  ] });
1161
1305
  if (strip === null)
1162
- return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1163
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${stripGlyph("unknown")[0]} ` }),
1164
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${label} no signal / not configured` })
1306
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1307
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${stripGlyph("unknown")[0]} ${label} no signal / not configured` }),
1308
+ /* @__PURE__ */ jsx(Text, { children: " " })
1165
1309
  ] });
1166
- const { parts, summary, age } = stripLayout(strip, width, now);
1167
- return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1168
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${strip.title} ` }),
1169
- parts ? parts.map((item, i) => {
1170
- if (item.state === "fail")
1171
- return /* @__PURE__ */ jsxs(React5.Fragment, { children: [
1172
- i > 0 ? /* @__PURE__ */ jsx(Text, { children: " " }) : null,
1173
- /* @__PURE__ */ jsx(Pill, { variant: "error", tone: "solid", children: itemText(item) })
1174
- ] }, item.key);
1175
- const [glyph, color] = stripGlyph(item.state);
1176
- const asking = item.state === "warn";
1177
- return /* @__PURE__ */ jsxs(React5.Fragment, { children: [
1178
- i > 0 ? /* @__PURE__ */ jsx(Text, { children: " " }) : null,
1179
- /* @__PURE__ */ jsx(
1180
- Text,
1181
- {
1182
- color: item.state === "unknown" ? void 0 : color,
1183
- dimColor: item.state === "unknown",
1184
- bold: asking,
1185
- children: glyph
1186
- }
1187
- ),
1188
- item.alarm ? /* @__PURE__ */ jsx(Text, { color: colors.error, bold: true, children: ` ${ALARM}` }) : null,
1189
- /* @__PURE__ */ jsxs(Text, { ...nameStyle(item), children: [
1190
- " ",
1191
- item.key
1192
- ] })
1193
- ] }, item.key);
1194
- }) : /* @__PURE__ */ jsx(Text, { children: summary }),
1195
- age ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${age}` }) : null
1310
+ const { spans, age, titles } = stripRows(strip, width, now);
1311
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1312
+ /* @__PURE__ */ jsxs(Box, { children: [
1313
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: titles[0] }),
1314
+ /* @__PURE__ */ jsx(Spans, { spans: spans.issues }),
1315
+ age ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${age}` }) : null
1316
+ ] }),
1317
+ /* @__PURE__ */ jsxs(Box, { children: [
1318
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: titles[1] }),
1319
+ /* @__PURE__ */ jsx(Spans, { spans: spans.success })
1320
+ ] })
1196
1321
  ] });
1197
1322
  };
1198
1323
  var SEARCH_GLYPH_NERD = "\uF422";
@@ -2885,7 +3010,12 @@ var InboxHeader = ({
2885
3010
  /* @__PURE__ */ jsx(Text3, { color: colors.info, dimColor: true, children: "\u254C".repeat(fill) })
2886
3011
  ] });
2887
3012
  };
2888
- var stripSignature = (strip) => strip === null ? "\u2205" : strip.items.map((i) => `${i.key}=${i.state}${i.alarm ? "\u25B2" : ""}`).join(",");
3013
+ var stripSignature = (strip) => strip === null ? "\u2205" : [
3014
+ strip.note ?? "",
3015
+ ...strip.items.map(
3016
+ (i) => `${i.key}=${i.state}:${i.cause ?? ""}${i.alarm ? ":build" : ""}`
3017
+ )
3018
+ ].join(",");
2889
3019
  var toCiStatusState = (status) => status ? { kind: "ready", status } : { kind: "error" };
2890
3020
  var sameCiStatusState = (a, b) => {
2891
3021
  if (a.kind !== b.kind) return false;
@@ -3123,10 +3253,7 @@ var ItemRow = ({
3123
3253
  icon,
3124
3254
  motion,
3125
3255
  announcements,
3126
- focusCell: focusCellFor(
3127
- focus,
3128
- "url" in item ? item.url : void 0
3129
- )
3256
+ focusCell: focusCellFor(focus, "url" in item ? item.url : void 0)
3130
3257
  }
3131
3258
  );
3132
3259
  };
@@ -3287,11 +3414,11 @@ var TURN_LEGEND = [
3287
3414
  var CELL_GUTTER = 2;
3288
3415
  var COL_GAP = 3;
3289
3416
  var cellWidthOf = (col) => Math.max(0, ...col.rows.map((r) => r.cell.length)) + CELL_GUTTER;
3290
- var widthOf = (col) => Math.max(
3417
+ var widthOf2 = (col) => Math.max(
3291
3418
  col.title.length,
3292
3419
  ...col.rows.map((r) => cellWidthOf(col) + r.label.length)
3293
3420
  );
3294
- var groupWidth = (group) => Math.max(0, ...group.map(widthOf));
3421
+ var groupWidth = (group) => Math.max(0, ...group.map(widthOf2));
3295
3422
  var layoutWidth = (groups) => groups.reduce((total, g) => total + groupWidth(g), 0) + COL_GAP * Math.max(0, groups.length - 1);
3296
3423
  var legendLayout = (cols, available) => {
3297
3424
  const across = cols.map((c) => [c]);
@@ -3362,9 +3489,7 @@ var HelpModal = ({
3362
3489
  ["p", "open repo in new pane"],
3363
3490
  ["u", "unsubscribe from this item"],
3364
3491
  ["x", "remove yourself as reviewer"],
3365
- ...hasJira ? [
3366
- ["t", "Jira: move / open ticket"]
3367
- ] : [],
3492
+ ...hasJira ? [["t", "Jira: move / open ticket"]] : [],
3368
3493
  ["\u2303K", "launch \xB7 open a ticket or PR by name"],
3369
3494
  ["/", "search"],
3370
3495
  ["f", "filter by repo"],
@@ -3630,7 +3755,7 @@ var BrowseScreen = ({
3630
3755
  5,
3631
3756
  // -10 rather than -8: the extra 2 are the frame's top and bottom border
3632
3757
  // rows, so the tree never grows taller than the terminal inside the frame.
3633
- rows - 10 - (filterActive ? 2 : 0) - (reserveCiRow ? 2 : 0) - (reserveStripRow ? 2 : 0) - (reserveFocusRow ? 2 : 0)
3758
+ rows - 10 - (filterActive ? 2 : 0) - (reserveCiRow ? 2 : 0) - (reserveStripRow ? 3 : 0) - (reserveFocusRow ? 2 : 0)
3634
3759
  );
3635
3760
  useEffect(() => {
3636
3761
  setLocalSections(applyWork(sections));
@@ -4330,7 +4455,13 @@ var BrowseScreen = ({
4330
4455
  headerHint,
4331
4456
  ["?", "help"],
4332
4457
  ["q", "quit"]
4333
- ] : activeItem?.kind === "subgroup-header" ? [["\u2191\u2193", "nav"], ["\u2190\u2192", "tab"], headerHint, ["?", "help"], ["q", "quit"]] : [
4458
+ ] : activeItem?.kind === "subgroup-header" ? [
4459
+ ["\u2191\u2193", "nav"],
4460
+ ["\u2190\u2192", "tab"],
4461
+ headerHint,
4462
+ ["?", "help"],
4463
+ ["q", "quit"]
4464
+ ] : [
4334
4465
  ["\u2191\u2193", "nav"],
4335
4466
  ["\u2190\u2192", "tab"],
4336
4467
  ["\u21B5/d", "open"],
@@ -4923,10 +5054,13 @@ var App = ({
4923
5054
  const running = [...heldSince].filter(([tab]) => held.has(tab));
4924
5055
  if (running.length === 0) return;
4925
5056
  const remaining = (at) => at + TRANSIT_HOLD_MS - Date.now();
4926
- const timer = setTimeout(() => {
4927
- for (const [tab, at] of running)
4928
- if (remaining(at) <= 0) settleTab(tab, transients);
4929
- }, Math.max(0, Math.min(...running.map(([, at]) => remaining(at)))));
5057
+ const timer = setTimeout(
5058
+ () => {
5059
+ for (const [tab, at] of running)
5060
+ if (remaining(at) <= 0) settleTab(tab, transients);
5061
+ },
5062
+ Math.max(0, Math.min(...running.map(([, at]) => remaining(at))))
5063
+ );
4930
5064
  return () => clearTimeout(timer);
4931
5065
  }, [transients, heldSince, state.phase]);
4932
5066
  const pendingSummary = useMemo(
@@ -5254,7 +5388,7 @@ var App = ({
5254
5388
  flexDirection: "column",
5255
5389
  minHeight: Math.max(
5256
5390
  5,
5257
- rows - 5 - (hasCiStatus ? 2 : 0) - (hasStrip ? 2 : 0) - (hasFocusSlot ? 2 : 0)
5391
+ rows - 5 - (hasCiStatus ? 2 : 0) - (hasStrip ? 3 : 0) - (hasFocusSlot ? 2 : 0)
5258
5392
  ),
5259
5393
  children: state.phase === "loading" ? /* @__PURE__ */ jsx(LoadingScreen, { label: `Fetching ${title}\u2026` }) : /* @__PURE__ */ jsx(
5260
5394
  NoRowsScreen,
@@ -5414,4 +5548,4 @@ var matchesFilter = (repo, filter) => {
5414
5548
  };
5415
5549
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
5416
5550
 
5417
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, DEFAULT_FOCUS_EMPTY, DEFAULT_FOCUS_LABEL, FOCUS_JUMP_KEY, FOCUS_MARK, FocusSlotLine, HealthPanel, LookupMiss, MIN_SIDEBAR_COLS, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, focusCellFor, focusRefColor, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
5551
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, DEFAULT_FOCUS_EMPTY, DEFAULT_FOCUS_LABEL, FOCUS_JUMP_KEY, FOCUS_MARK, FocusSlotLine, HealthPanel, LookupMiss, MIN_SIDEBAR_COLS, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, focusCellFor, focusRefColor, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripRows, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.76.2",
3
+ "version": "0.77.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",
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "engines": {
18
- "node": ">=20"
18
+ "node": ">=22"
19
19
  },
20
20
  "scripts": {
21
21
  "build": "tsup",
@@ -49,19 +49,19 @@
49
49
  "react": ">=19"
50
50
  },
51
51
  "dependencies": {
52
- "@kud/gh": "0.21.0",
53
- "@kud/gh-workflow": "0.17.0",
52
+ "@kud/gh": "0.21.1",
53
+ "@kud/gh-workflow": "0.17.1",
54
54
  "zx": "8.8.5"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@kud/ink-ui": "0.41.0",
58
- "@types/node": "22.20.1",
59
- "@types/react": "19.2.17",
60
- "ink": "7.1.0",
58
+ "@types/node": "26.6.5",
59
+ "@types/react": "19.3.0",
60
+ "ink": "8.0.0",
61
61
  "ink-testing-library": "4.0.0",
62
- "react": "19.2.7",
63
- "tsup": "8.5.1",
62
+ "react": "19.3.1",
63
+ "tsup": "9.0.0",
64
64
  "typescript": "5.9.3",
65
- "vitest": "4.1.10"
65
+ "vitest": "5.0.5"
66
66
  }
67
67
  }