@kud/gh-ink 0.76.3 → 0.78.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
@@ -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. */
@@ -902,6 +930,19 @@ declare const COLS: number;
902
930
  * disagrees with the sum of its own tabs is worse than either number alone.
903
931
  */
904
932
  declare const workCount: (s: Section) => number;
933
+ /**
934
+ * How many rows in a tab carry a `warning`: what the tab draws as `⚠ N` after
935
+ * its count.
936
+ *
937
+ * Counted off the rows rather than handed in by the host, so the tab can only
938
+ * ever count what a row in it actually says. A separate host figure is a second
939
+ * implementation of the same rule, free to warn on a tab whose rows are silent.
940
+ *
941
+ * Distinct keys, because a ticket can head more than one block in the same tab,
942
+ * and one wrong ticket drawn twice is still one thing to fix. Read off the
943
+ * same sections the badge reads, so a search narrows both together.
944
+ */
945
+ declare const warningCount: (s: Section) => number;
905
946
  declare const drillCmd: (item: AnyItem) => string | null;
906
947
  declare const explainGhAction: (e: unknown) => string;
907
948
  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?: {
@@ -961,7 +1002,12 @@ type StripStatusState = {
961
1002
  kind: "ready";
962
1003
  strip: StatusStrip | null;
963
1004
  };
964
- /** What the strip SAYS, ignoring when it said it. */
1005
+ /**
1006
+ * What the strip SAYS, ignoring when it said it. Every field the strip draws
1007
+ * is in it — the cause word and the shared note included — or a cause moving
1008
+ * from `slow` to `errors` would bail out of the repaint and leave the old
1009
+ * word on screen.
1010
+ */
965
1011
  declare const stripSignature: (strip: StatusStrip | null) => string;
966
1012
  declare const toCiStatusState: (status: CiStatus | null) => CiStatusState;
967
1013
  declare const sameCiStatusState: (a: CiStatusState, b: CiStatusState) => boolean;
@@ -1137,5 +1183,5 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
1137
1183
  */
1138
1184
  declare const parsePatterns: (value: string) => string[];
1139
1185
 
1140
- 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 };
1141
- 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, StripState, StripStatusState, TabForStatus, TrailingColumns, TrailingShed };
1186
+ 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, warningCount, windowCount, withHeaders, withoutItem, workCount, writeCache };
1187
+ 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";
@@ -2385,6 +2510,13 @@ var workCount = (s) => {
2385
2510
  }
2386
2511
  return n;
2387
2512
  };
2513
+ var [WARNING_MARK, WARNING_INK] = stripGlyph("warn");
2514
+ var warningCount = (s) => {
2515
+ const keys = /* @__PURE__ */ new Set();
2516
+ for (const item of s.items)
2517
+ if (item.kind === "task" && item.warning) keys.add(item.key);
2518
+ return keys.size;
2519
+ };
2388
2520
  var countedSections = (sections, departing) => {
2389
2521
  if (!departing?.size) return sections;
2390
2522
  const sent = new Set(sections.map((s) => s.id));
@@ -2885,7 +3017,12 @@ var InboxHeader = ({
2885
3017
  /* @__PURE__ */ jsx(Text3, { color: colors.info, dimColor: true, children: "\u254C".repeat(fill) })
2886
3018
  ] });
2887
3019
  };
2888
- var stripSignature = (strip) => strip === null ? "\u2205" : strip.items.map((i) => `${i.key}=${i.state}${i.alarm ? "\u25B2" : ""}`).join(",");
3020
+ var stripSignature = (strip) => strip === null ? "\u2205" : [
3021
+ strip.note ?? "",
3022
+ ...strip.items.map(
3023
+ (i) => `${i.key}=${i.state}:${i.cause ?? ""}${i.alarm ? ":build" : ""}`
3024
+ )
3025
+ ].join(",");
2889
3026
  var toCiStatusState = (status) => status ? { kind: "ready", status } : { kind: "error" };
2890
3027
  var sameCiStatusState = (a, b) => {
2891
3028
  if (a.kind !== b.kind) return false;
@@ -3051,10 +3188,11 @@ var ItemRow = ({
3051
3188
  ] });
3052
3189
  if (item.kind === "task") {
3053
3190
  const note = item.note ?? "";
3191
+ const warning = item.warning ?? "";
3054
3192
  const transitIcon = pending ? PENDING_GLYPH : !transient ? " " : isDeparture(transient) ? TRANSIT_OUT_FRAMES[rampAt(TRANSIT_OUT_FRAMES.length)] : isArrival(transient) ? TRANSIT_IN_FRAMES[rampAt(TRANSIT_IN_FRAMES.length)] : "\u25C9";
3055
3193
  const titleMax = Math.max(
3056
3194
  1,
3057
- cols - item.key.length - note.length - (item.pill ? pillWidth(item.pill) + 1 : 0) - (farewellLabel ? pillWidth(farewellLabel) + 2 : 0) - prefix.length - (markerCols > 0 ? markerCols + 1 : 0) - // The focus gutter, priced only when it is drawn: two columns on every
3195
+ cols - item.key.length - note.length - (item.pill ? pillWidth(item.pill) + 1 : 0) - (warning ? warning.length + 3 : 0) - (farewellLabel ? pillWidth(farewellLabel) + 2 : 0) - prefix.length - (markerCols > 0 ? markerCols + 1 : 0) - // The focus gutter, priced only when it is drawn: two columns on every
3058
3196
  // url-bearing row while a focus is on screen, none at all without one.
3059
3197
  (focus !== void 0 ? 2 : 0) - 12
3060
3198
  );
@@ -3088,6 +3226,11 @@ var ItemRow = ({
3088
3226
  }
3089
3227
  )
3090
3228
  ] }) : null,
3229
+ warning ? /* @__PURE__ */ jsxs(Text3, { color: WARNING_INK, children: [
3230
+ " ",
3231
+ /* @__PURE__ */ jsx(Text3, { bold: true, children: WARNING_MARK }),
3232
+ ` ${warning}`
3233
+ ] }) : null,
3091
3234
  note ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${note}` }) : null,
3092
3235
  farewellLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
3093
3236
  /* @__PURE__ */ jsx(Text3, { children: " " }),
@@ -3123,10 +3266,7 @@ var ItemRow = ({
3123
3266
  icon,
3124
3267
  motion,
3125
3268
  announcements,
3126
- focusCell: focusCellFor(
3127
- focus,
3128
- "url" in item ? item.url : void 0
3129
- )
3269
+ focusCell: focusCellFor(focus, "url" in item ? item.url : void 0)
3130
3270
  }
3131
3271
  );
3132
3272
  };
@@ -3287,11 +3427,11 @@ var TURN_LEGEND = [
3287
3427
  var CELL_GUTTER = 2;
3288
3428
  var COL_GAP = 3;
3289
3429
  var cellWidthOf = (col) => Math.max(0, ...col.rows.map((r) => r.cell.length)) + CELL_GUTTER;
3290
- var widthOf = (col) => Math.max(
3430
+ var widthOf2 = (col) => Math.max(
3291
3431
  col.title.length,
3292
3432
  ...col.rows.map((r) => cellWidthOf(col) + r.label.length)
3293
3433
  );
3294
- var groupWidth = (group) => Math.max(0, ...group.map(widthOf));
3434
+ var groupWidth = (group) => Math.max(0, ...group.map(widthOf2));
3295
3435
  var layoutWidth = (groups) => groups.reduce((total, g) => total + groupWidth(g), 0) + COL_GAP * Math.max(0, groups.length - 1);
3296
3436
  var legendLayout = (cols, available) => {
3297
3437
  const across = cols.map((c) => [c]);
@@ -3362,9 +3502,7 @@ var HelpModal = ({
3362
3502
  ["p", "open repo in new pane"],
3363
3503
  ["u", "unsubscribe from this item"],
3364
3504
  ["x", "remove yourself as reviewer"],
3365
- ...hasJira ? [
3366
- ["t", "Jira: move / open ticket"]
3367
- ] : [],
3505
+ ...hasJira ? [["t", "Jira: move / open ticket"]] : [],
3368
3506
  ["\u2303K", "launch \xB7 open a ticket or PR by name"],
3369
3507
  ["/", "search"],
3370
3508
  ["f", "filter by repo"],
@@ -3630,7 +3768,7 @@ var BrowseScreen = ({
3630
3768
  5,
3631
3769
  // -10 rather than -8: the extra 2 are the frame's top and bottom border
3632
3770
  // 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)
3771
+ rows - 10 - (filterActive ? 2 : 0) - (reserveCiRow ? 2 : 0) - (reserveStripRow ? 3 : 0) - (reserveFocusRow ? 2 : 0)
3634
3772
  );
3635
3773
  useEffect(() => {
3636
3774
  setLocalSections(applyWork(sections));
@@ -3723,6 +3861,10 @@ var BrowseScreen = ({
3723
3861
  const s = counted.find((c) => c.id === id);
3724
3862
  return s ? workCount(s) : 0;
3725
3863
  };
3864
+ const warningOf = (id) => {
3865
+ const s = counted.find((c) => c.id === id);
3866
+ return s ? warningCount(s) : 0;
3867
+ };
3726
3868
  const markedTabs = useMemo(() => {
3727
3869
  const out = /* @__PURE__ */ new Set();
3728
3870
  if (!transients?.size) return out;
@@ -4330,7 +4472,13 @@ var BrowseScreen = ({
4330
4472
  headerHint,
4331
4473
  ["?", "help"],
4332
4474
  ["q", "quit"]
4333
- ] : activeItem?.kind === "subgroup-header" ? [["\u2191\u2193", "nav"], ["\u2190\u2192", "tab"], headerHint, ["?", "help"], ["q", "quit"]] : [
4475
+ ] : activeItem?.kind === "subgroup-header" ? [
4476
+ ["\u2191\u2193", "nav"],
4477
+ ["\u2190\u2192", "tab"],
4478
+ headerHint,
4479
+ ["?", "help"],
4480
+ ["q", "quit"]
4481
+ ] : [
4334
4482
  ["\u2191\u2193", "nav"],
4335
4483
  ["\u2190\u2192", "tab"],
4336
4484
  ["\u21B5/d", "open"],
@@ -4565,6 +4713,7 @@ var BrowseScreen = ({
4565
4713
  label: s.label,
4566
4714
  icon: s.icon,
4567
4715
  count: countOf(s.id),
4716
+ warning: warningOf(s.id),
4568
4717
  /*
4569
4718
  * The denominator, and only where it can still be true.
4570
4719
  *
@@ -4923,10 +5072,13 @@ var App = ({
4923
5072
  const running = [...heldSince].filter(([tab]) => held.has(tab));
4924
5073
  if (running.length === 0) return;
4925
5074
  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)))));
5075
+ const timer = setTimeout(
5076
+ () => {
5077
+ for (const [tab, at] of running)
5078
+ if (remaining(at) <= 0) settleTab(tab, transients);
5079
+ },
5080
+ Math.max(0, Math.min(...running.map(([, at]) => remaining(at))))
5081
+ );
4930
5082
  return () => clearTimeout(timer);
4931
5083
  }, [transients, heldSince, state.phase]);
4932
5084
  const pendingSummary = useMemo(
@@ -5254,7 +5406,7 @@ var App = ({
5254
5406
  flexDirection: "column",
5255
5407
  minHeight: Math.max(
5256
5408
  5,
5257
- rows - 5 - (hasCiStatus ? 2 : 0) - (hasStrip ? 2 : 0) - (hasFocusSlot ? 2 : 0)
5409
+ rows - 5 - (hasCiStatus ? 2 : 0) - (hasStrip ? 3 : 0) - (hasFocusSlot ? 2 : 0)
5258
5410
  ),
5259
5411
  children: state.phase === "loading" ? /* @__PURE__ */ jsx(LoadingScreen, { label: `Fetching ${title}\u2026` }) : /* @__PURE__ */ jsx(
5260
5412
  NoRowsScreen,
@@ -5414,4 +5566,4 @@ var matchesFilter = (repo, filter) => {
5414
5566
  };
5415
5567
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
5416
5568
 
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 };
5569
+ 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, warningCount, 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.3",
3
+ "version": "0.78.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",
@@ -44,17 +44,17 @@
44
44
  "access": "public"
45
45
  },
46
46
  "peerDependencies": {
47
- "@kud/ink-ui": ">=0.41.0 <1",
47
+ "@kud/ink-ui": ">=0.42.0 <1",
48
48
  "ink": ">=7",
49
49
  "react": ">=19"
50
50
  },
51
51
  "dependencies": {
52
52
  "@kud/gh": "0.21.1",
53
- "@kud/gh-workflow": "0.17.1",
53
+ "@kud/gh-workflow": "0.18.0",
54
54
  "zx": "8.8.5"
55
55
  },
56
56
  "devDependencies": {
57
- "@kud/ink-ui": "0.41.0",
57
+ "@kud/ink-ui": "0.42.0",
58
58
  "@types/node": "26.6.5",
59
59
  "@types/react": "19.3.0",
60
60
  "ink": "8.0.0",