@kud/gh-ink 0.58.0 → 0.59.1

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 +116 -16
  2. package/dist/index.js +356 -160
  3. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -136,32 +136,112 @@ declare const counts: (row: SidebarRow, liveLabel?: LiveLabel) => string;
136
136
  */
137
137
  declare const railCapacity: (height: number, rows: number) => number;
138
138
  /**
139
- * A right-hand rail of initiatives, standing beside the list.
140
- *
141
- * Label first, facts beneath, because a roadmap is read by name: nobody thinks
142
- * "ACC-11312", they think "the OpenSearch migration". So the label is the
143
- * bright line and the key drops to the facts line as what it is — a reference
144
- * you follow, in the secondary tier. Two lines rather than one because both are
145
- * load-bearing and neither survives the other being cut: words with no key
146
- * cannot be opened, and a key with no words cannot be read at a glance.
139
+ * One rail, or several stacked in one column. A host with one thing to rail —
140
+ * initiatives — passes the one; a host with two kinds of container passes both,
141
+ * and the panel draws them in order under one rule, each with its own heading.
142
+ * A stack rather than a second prop because the second rail needs everything
143
+ * the first has — a heading, a window, a cursor — and nothing more.
144
+ */
145
+ type Rails = Sidebar | Sidebar[];
146
+ /** The stack as an array, whichever shape the host chose. */
147
+ declare const railsOf: (rails: Rails | undefined) => Sidebar[];
148
+ /** Every row of every section in order — the space the rail's cursor moves in. */
149
+ declare const railRowsOf: (rails: Rails | undefined) => SidebarRow[];
150
+ /**
151
+ * How the rail's height is shared out down a stack. Each section takes what it
152
+ * needs while that fits inside an even share of what is left, so a short
153
+ * section hands its surplus down rather than sitting beside a stripe of blank
154
+ * lines while the section under it says `+4 more`. Even shares rather than
155
+ * first-come, because the FIRST section is the one most likely to be long, and
156
+ * letting it fill the column would leave the second with a heading and nothing
157
+ * under it — a section that is visibly there and unreadable.
158
+ */
159
+ declare const railHeights: (height: number, rowCounts: number[]) => number[];
160
+ /**
161
+ * A right-hand rail standing beside the list: one section, or a stack of them.
147
162
  *
148
163
  * Presentational, like every other row renderer here: it DRAWS a cursor but does
149
164
  * not own one, and it never calls `useInput`. Where the cursor is, and whether
150
165
  * the arrows are pointed at this rail at all, are the host's state — which is
151
- * what lets a screen with two focus regions have exactly one of them lit.
166
+ * what lets a screen with two focus regions have exactly one of them lit. The
167
+ * cursor counts across the whole stack, in `railRowsOf` order, so the host
168
+ * moves one number and never learns where a section boundary falls.
152
169
  */
153
170
  declare const SidePanel: ({ sidebar, liveLabel, width, height, focused, cursor, }: {
154
- sidebar: Sidebar;
171
+ sidebar: Rails;
155
172
  liveLabel?: LiveLabel;
156
173
  /** From `railWidth`, so the list beside it can subtract the same number. */
157
174
  width?: number;
158
175
  height?: number;
159
176
  /** The arrows are pointed here, so this rail draws the cursor. */
160
177
  focused?: boolean;
161
- /** Which row the cursor is on. Only drawn while `focused`. */
178
+ /** Which row the cursor is on, counted across every section. Only drawn while `focused`. */
162
179
  cursor?: number;
163
180
  }) => React.JSX.Element;
164
181
 
182
+ /**
183
+ * One state per thing, from a closed set the strip knows how to draw. The host
184
+ * decides what "warn" means — an error-rate edge, a stale feed, a flaky build —
185
+ * and the strip only ever says it with a shape.
186
+ *
187
+ * `unknown` is a state and not the absence of one: "nothing heard from it" is
188
+ * a fact about the feed that a green tick would paper over, and a strip that
189
+ * draws an unmeasured thing as fine is the one failure it cannot afford.
190
+ */
191
+ type StripState = "ok" | "warn" | "fail" | "unknown";
192
+ type StripItem = {
193
+ /** The name on screen. Short — it is drawn once per item across one line. */
194
+ key: string;
195
+ state: StripState;
196
+ /**
197
+ * A second, independent alarm beside the state — a build on fire next to a
198
+ * service that is serving fine, say. Drawn as its own mark rather than folded
199
+ * into `state`, because the two answer different questions and a reader
200
+ * needs to see when they disagree.
201
+ */
202
+ alarm?: boolean;
203
+ /** Where `o` goes when this item is opened from a rail row. Unused here. */
204
+ url?: string;
205
+ };
206
+ type StatusStrip = {
207
+ /** What the row is a strip OF, in the host's words: `services`, say. */
208
+ title: string;
209
+ items: StripItem[];
210
+ /** When this snapshot was taken (ms). Drawn as an age at the end of the row. */
211
+ at?: number;
212
+ };
213
+ declare const stripGlyph: (state: StripState) => [string, string];
214
+ declare const ageLabel: (at: number | undefined, now?: number) => string;
215
+ /**
216
+ * The strip as plain text, at the width it has to fit. Exported so a test — or a
217
+ * host laying out its own row — can ask the same question the renderer asks.
218
+ *
219
+ * Wide: every item named with its mark, so the row reads as a roll call. Narrow:
220
+ * counts for what is fine, and names ONLY for what is not — the reader of a
221
+ * narrow terminal loses the roll call, never the failures. When even the
222
+ * failures do not fit, the worst few are named and the rest counted, so the
223
+ * row still says "and N more" rather than silently dropping the fourth fire.
224
+ */
225
+ declare const stripLayout: (strip: StatusStrip, width: number, now?: number) => {
226
+ parts: StripItem[] | null;
227
+ summary: string;
228
+ age: string;
229
+ };
230
+ /**
231
+ * A standing single-line row, the same height across loading / absent / ready
232
+ * so the content below it never jumps — the same contract `CiStatusLine` keeps,
233
+ * and drawn in the same place, because a reader learns one row of glances.
234
+ */
235
+ declare const StatusStripLine: ({ strip, label, width, now, }: {
236
+ /** `undefined` while the first poll is out; `null` when the host has nothing. */
237
+ strip: StatusStrip | null | undefined;
238
+ /** What to call the row before the data arrives with its own title. */
239
+ label?: string;
240
+ /** Columns the row may use. */
241
+ width: number;
242
+ now?: number;
243
+ }) => React.JSX.Element;
244
+
165
245
  declare const renderMarkdown: (raw: string, width: number, fileLink?: (path: string, line?: number) => string) => StyledLine[];
166
246
 
167
247
  declare const healthDisplay: Record<Health, {
@@ -234,14 +314,14 @@ type CachedCockpit = {
234
314
  * rail left out of the file is a rail that does not exist until the next
235
315
  * refresh — `i` did nothing and the footer did not even offer it.
236
316
  */
237
- sidebar?: Sidebar;
317
+ sidebar?: Rails;
238
318
  };
239
319
  declare const readCache: (key: string) => CachedCockpit | null;
240
320
  declare const writeCache: (key: string, data: {
241
321
  sections: Section[];
242
322
  login: string;
243
323
  budget?: InboxBudget;
244
- sidebar?: Sidebar;
324
+ sidebar?: Rails;
245
325
  }) => void;
246
326
 
247
327
  type DetailContext = {
@@ -422,6 +502,14 @@ type CiStatusState = {
422
502
  kind: "ready";
423
503
  status: CiStatus;
424
504
  };
505
+ type StripStatusState = {
506
+ kind: "loading";
507
+ } | {
508
+ kind: "ready";
509
+ strip: StatusStrip | null;
510
+ };
511
+ /** What the strip SAYS, ignoring when it said it. */
512
+ declare const stripSignature: (strip: StatusStrip | null) => string;
425
513
  declare const toCiStatusState: (status: CiStatus | null) => CiStatusState;
426
514
  declare const sameCiStatusState: (a: CiStatusState, b: CiStatusState) => boolean;
427
515
  declare const CiStatusLine: ({ state, job, }: {
@@ -446,7 +534,7 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
446
534
  cursor: number;
447
535
  }) => React.JSX.Element;
448
536
  declare const signatureOf: (sections: Section[]) => string;
449
- declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraSetupHint, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, liveLabel, }: {
537
+ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraSetupHint, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, stripFetcher, stripLabel, stripPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, liveLabel, }: {
450
538
  fetcher: () => Promise<{
451
539
  sections: Section[];
452
540
  login: string;
@@ -466,7 +554,7 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
466
554
  * Part of the fetch result rather than a static prop because it is data, and
467
555
  * data that goes stale the same way the rows do.
468
556
  */
469
- sidebar?: Sidebar;
557
+ sidebar?: Rails;
470
558
  }>;
471
559
  cacheKey?: string;
472
560
  /**
@@ -490,6 +578,18 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
490
578
  ciJob?: string;
491
579
  ciFetcher?: () => Promise<CiStatus | null>;
492
580
  ciPollMs?: number;
581
+ /**
582
+ * A standing status strip under the CI line — one row of things and the
583
+ * state each is in, polled on its own timer like the CI line and for the
584
+ * same reason: it is a glance at something that moves on its own schedule.
585
+ * Wiring a fetcher reserves the row; leaving it out draws nothing. `null`
586
+ * from the fetcher means "nothing configured" and is drawn as such, while a
587
+ * rejected poll keeps the last strip up.
588
+ */
589
+ stripFetcher?: () => Promise<StatusStrip | null>;
590
+ /** What to call the row before its first answer arrives with a title. */
591
+ stripLabel?: string;
592
+ stripPollMs?: number;
493
593
  /**
494
594
  * A file something else touches when it has changed GitHub on your behalf —
495
595
  * a Claude session closing an issue, a script merging a PR. Touching it makes
@@ -529,4 +629,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
529
629
  */
530
630
  declare const parsePatterns: (value: string) => string[];
531
631
 
532
- export { type Action, ActionMenu, App, COLS, CiStatusLine, type CiStatusState, type Command, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxExtension, type JiraTransition, type LiveLabel, MIN_SIDEBAR_COLS, type RepoFilter, SIDEBAR_COLS, SidePanel, type Sidebar, type SidebarRow, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, railCapacity, railWidth, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, toCiStatusState, truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
632
+ export { type Action, ActionMenu, App, COLS, CiStatusLine, type CiStatusState, type Command, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxExtension, type JiraTransition, type LiveLabel, MIN_SIDEBAR_COLS, type Rails, type RepoFilter, SIDEBAR_COLS, SidePanel, type Sidebar, type SidebarRow, type StatusStrip, StatusStripLine, type StripItem, type StripState, type StripStatusState, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React2, { createContext, useState, useRef, useEffect, useMemo, useContext } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
- import { colors, ScrollView, TextInput, useAppKeys, LoadingScreen, ProgressBar, StatusMessage, FooterHints, useListCursor, CommandPalette, Tabs, pillWidth, Pill } from '@kud/ink-ui';
3
+ import { colors, ScrollView, TextInput, useAppKeys, LoadingScreen, StatusMessage, FooterHints, useListCursor, CommandPalette, Tabs, ProgressBar, pillWidth, Pill } from '@kud/ink-ui';
4
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';
@@ -634,15 +634,28 @@ var Progress = ({ row }) => {
634
634
  /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: GUTTER + fraction.padStart(FRACTION_COLS) })
635
635
  ] });
636
636
  };
637
- var SidePanel = ({
637
+ var railsOf = (rails) => rails === void 0 ? [] : Array.isArray(rails) ? rails : [rails];
638
+ var railRowsOf = (rails) => railsOf(rails).flatMap((s) => s.rows);
639
+ var linesFor = (rows) => HEADING_LINES + Math.max(1, rows) * ROW_LINES;
640
+ var railHeights = (height, rowCounts) => {
641
+ const out = [];
642
+ let remaining = height;
643
+ rowCounts.forEach((rows, i) => {
644
+ const share = Math.floor(remaining / (rowCounts.length - i));
645
+ const h = Math.min(share, linesFor(rows));
646
+ out.push(h);
647
+ remaining -= h;
648
+ });
649
+ return out;
650
+ };
651
+ var RailSection = ({
638
652
  sidebar,
639
- liveLabel = defaultLiveLabel,
640
- width = SIDEBAR_COLS,
653
+ liveLabel,
654
+ content,
641
655
  height,
642
- focused = false,
643
- cursor = 0
656
+ focused,
657
+ cursor
644
658
  }) => {
645
- const content = contentOf(width);
646
659
  const labelCols = content - MARKS;
647
660
  const liveCols = content - FACTS_FIXED;
648
661
  const capacity = height === void 0 ? sidebar.rows.length : railCapacity(height, sidebar.rows.length);
@@ -653,6 +666,46 @@ var SidePanel = ({
653
666
  const rule = "\u254C".repeat(
654
667
  Math.max(0, content - [...sidebar.title].length - focus.length - 1)
655
668
  );
669
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height, children: [
670
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
671
+ /* @__PURE__ */ jsx(Text, { bold: true, children: sidebar.title }),
672
+ focused ? /* @__PURE__ */ jsx(Text, { color: colors.accent, children: focus }) : null,
673
+ /* @__PURE__ */ jsx(Text, { color: colors.info, dimColor: true, children: ` ${rule}` })
674
+ ] }),
675
+ sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: MARKS, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: "nothing open" }) }) : shown.map((row, i) => {
676
+ const active = focused && start + i === cursor;
677
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
678
+ /* @__PURE__ */ jsxs(Box, { children: [
679
+ /* @__PURE__ */ jsx(Text, { color: colors.info, children: active ? "\u276F " : " " }),
680
+ /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
681
+ /* @__PURE__ */ jsx(Text, { bold: active, children: truncateWords(row.label, labelCols) })
682
+ ] }),
683
+ /* @__PURE__ */ jsxs(Box, { children: [
684
+ /* @__PURE__ */ jsx(Text, { color: row.markerColor, children: " ".repeat(FACTS_INDENT - 2) + (row.marker ?? " ") + " " }),
685
+ /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: row.key.padEnd(KEY_COLS) + GUTTER }),
686
+ /* @__PURE__ */ jsx(Progress, { row }),
687
+ /* @__PURE__ */ jsx(Text, { children: GUTTER + (row.live !== void 0 ? liveLabel(row.live).slice(0, liveCols) : "") })
688
+ ] })
689
+ ] }, row.key);
690
+ }),
691
+ hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: MARKS, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: `+${hidden} more` }) }) : null
692
+ ] });
693
+ };
694
+ var SidePanel = ({
695
+ sidebar,
696
+ liveLabel = defaultLiveLabel,
697
+ width = SIDEBAR_COLS,
698
+ height,
699
+ focused = false,
700
+ cursor = 0
701
+ }) => {
702
+ const content = contentOf(width);
703
+ const sections = railsOf(sidebar);
704
+ const heights = height === void 0 ? sections.map(() => void 0) : railHeights(
705
+ height,
706
+ sections.map((s) => s.rows.length)
707
+ );
708
+ let offset = 0;
656
709
  return (
657
710
  // A rule down the left rather than a full box: the rail's other three edges
658
711
  // already have the frame's border a column or two away, and a second
@@ -660,7 +713,7 @@ var SidePanel = ({
660
713
  // focus and act on, which this cannot be. One line is the whole claim: what
661
714
  // is left of it is the list, what is right of it is not. In the frame's own
662
715
  // colour and not dimmed, so the two rules read as one hierarchy.
663
- /* @__PURE__ */ jsxs(
716
+ /* @__PURE__ */ jsx(
664
717
  Box,
665
718
  {
666
719
  flexDirection: "column",
@@ -673,34 +726,113 @@ var SidePanel = ({
673
726
  borderRight: false,
674
727
  borderBottom: false,
675
728
  paddingLeft: PAD,
676
- children: [
677
- /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
678
- /* @__PURE__ */ jsx(Text, { bold: true, children: sidebar.title }),
679
- focused ? /* @__PURE__ */ jsx(Text, { color: colors.accent, children: focus }) : null,
680
- /* @__PURE__ */ jsx(Text, { color: colors.info, dimColor: true, children: ` ${rule}` })
681
- ] }),
682
- sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: MARKS, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: "nothing open" }) }) : shown.map((row, i) => {
683
- const active = focused && start + i === cursor;
684
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
685
- /* @__PURE__ */ jsxs(Box, { children: [
686
- /* @__PURE__ */ jsx(Text, { color: colors.info, children: active ? "\u276F " : " " }),
687
- /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
688
- /* @__PURE__ */ jsx(Text, { bold: active, children: truncateWords(row.label, labelCols) })
689
- ] }),
690
- /* @__PURE__ */ jsxs(Box, { children: [
691
- /* @__PURE__ */ jsx(Text, { color: row.markerColor, children: " ".repeat(FACTS_INDENT - 2) + (row.marker ?? " ") + " " }),
692
- /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: row.key.padEnd(KEY_COLS) + GUTTER }),
693
- /* @__PURE__ */ jsx(Progress, { row }),
694
- /* @__PURE__ */ jsx(Text, { children: GUTTER + (row.live !== void 0 ? liveLabel(row.live).slice(0, liveCols) : "") })
695
- ] })
696
- ] }, row.key);
697
- }),
698
- hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: MARKS, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: `+${hidden} more` }) }) : null
699
- ]
729
+ children: sections.map((section, i) => {
730
+ const first = offset;
731
+ offset += section.rows.length;
732
+ const inside = focused && cursor >= first && cursor < offset;
733
+ const owns = inside || focused && sections.length === 1;
734
+ return /* @__PURE__ */ jsx(
735
+ RailSection,
736
+ {
737
+ sidebar: section,
738
+ liveLabel,
739
+ content,
740
+ height: heights[i],
741
+ focused: owns,
742
+ cursor: cursor - first
743
+ },
744
+ section.title
745
+ );
746
+ })
700
747
  }
701
748
  )
702
749
  );
703
750
  };
751
+ var stripGlyph = (state) => {
752
+ switch (state) {
753
+ case "ok":
754
+ return ["\u2713", colors.success];
755
+ case "warn":
756
+ return ["!", colors.warning];
757
+ case "fail":
758
+ return ["\u2717", colors.error];
759
+ case "unknown":
760
+ return ["\u25CB", colors.muted];
761
+ }
762
+ };
763
+ var ALARM = "\u25B2";
764
+ var SEVERITY = {
765
+ fail: 0,
766
+ warn: 1,
767
+ unknown: 2,
768
+ ok: 3
769
+ };
770
+ var ageLabel = (at, now = Date.now()) => {
771
+ if (at === void 0) return "";
772
+ const s = Math.max(0, Math.round((now - at) / 1e3));
773
+ if (s < 60) return `${s}s`;
774
+ const m = Math.round(s / 60);
775
+ if (m < 60) return `${m}m`;
776
+ return `${Math.round(m / 60)}h`;
777
+ };
778
+ var itemText = (item) => `${stripGlyph(item.state)[0]} ${item.key}${item.alarm ? ALARM : ""}`;
779
+ var stripLayout = (strip, width, now = Date.now()) => {
780
+ const age = ageLabel(strip.at, now);
781
+ const fixed = 2 + strip.title.length + 2 + (age ? age.length + 3 : 0);
782
+ const full = strip.items.map(itemText).join(" ");
783
+ if (fixed + full.length <= width)
784
+ return { parts: strip.items, summary: full, age };
785
+ const sorted = [...strip.items].sort(
786
+ (a, b) => SEVERITY[a.state] - SEVERITY[b.state]
787
+ );
788
+ const bad = sorted.filter((i) => i.state !== "ok" || i.alarm);
789
+ const ok = strip.items.length - bad.length;
790
+ const tally = (n, s) => n > 0 ? `${n} ${stripGlyph(s)[0]}` : "";
791
+ let named = [];
792
+ let text = "";
793
+ for (let n = bad.length; n >= 0; n--) {
794
+ named = bad.slice(0, n);
795
+ const rest = bad.slice(n);
796
+ const counts2 = ["fail", "warn", "unknown"].map((s) => tally(rest.filter((i) => i.state === s).length, s)).filter(Boolean);
797
+ text = [tally(ok, "ok"), ...counts2, ...named.map(itemText)].filter(Boolean).join(" \xB7 ");
798
+ if (fixed + text.length <= width) break;
799
+ }
800
+ return { parts: null, summary: text, age };
801
+ };
802
+ var StatusStripLine = ({
803
+ strip,
804
+ label = "status",
805
+ width,
806
+ now
807
+ }) => {
808
+ if (strip === void 0)
809
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
810
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
811
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${label} loading\u2026` })
812
+ ] });
813
+ if (strip === null)
814
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
815
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u25CB " }),
816
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${label} no signal / not configured` })
817
+ ] });
818
+ const { parts, summary, age } = stripLayout(strip, width, now);
819
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
820
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${strip.title} ` }),
821
+ parts ? parts.map((item, i) => {
822
+ const [glyph, color] = stripGlyph(item.state);
823
+ return /* @__PURE__ */ jsxs(React2.Fragment, { children: [
824
+ i > 0 ? /* @__PURE__ */ jsx(Text, { children: " " }) : null,
825
+ /* @__PURE__ */ jsx(Text, { color, bold: true, children: glyph }),
826
+ /* @__PURE__ */ jsxs(Text, { children: [
827
+ " ",
828
+ item.key
829
+ ] }),
830
+ item.alarm ? /* @__PURE__ */ jsx(Text, { color: colors.error, bold: true, children: ALARM }) : null
831
+ ] }, item.key);
832
+ }) : /* @__PURE__ */ jsx(Text, { children: summary }),
833
+ age ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${age}` }) : null
834
+ ] });
835
+ };
704
836
  var healthDisplay = {
705
837
  none: { glyph: " ", color: colors.muted },
706
838
  draft: { glyph: "~", color: colors.muted },
@@ -749,7 +881,7 @@ var displayFor = (h) => h ? healthDisplay[h] : UNREAD_DISPLAY;
749
881
 
750
882
  // src/inbox/cache.ts
751
883
  var cacheTtlMs = () => inboxConfig().cacheTtlMs;
752
- var CACHE_VERSION = 4;
884
+ var CACHE_VERSION = 5;
753
885
  var cacheDir = () => join(
754
886
  process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
755
887
  inboxConfig().cacheNamespace
@@ -765,7 +897,7 @@ var readCache = (key) => {
765
897
  login: raw.login ?? "",
766
898
  at: raw.at ?? 0,
767
899
  budget: raw.budget,
768
- ...Array.isArray(raw.sidebar?.rows) ? { sidebar: raw.sidebar } : {}
900
+ ...railsOf(raw.sidebar).every((s) => Array.isArray(s?.rows)) && raw.sidebar !== void 0 ? { sidebar: raw.sidebar } : {}
769
901
  };
770
902
  } catch {
771
903
  return null;
@@ -1059,9 +1191,7 @@ var turnSentences = (item, login) => {
1059
1191
  return [`${item.lastActor} spoke last (\u2190), ${when}. Your reply is owed.`];
1060
1192
  const them = item.author && item.author !== login ? item.author : null;
1061
1193
  if (!them)
1062
- return [
1063
- `You spoke last, ${when}. It is waiting on a reviewer, not on you.`
1064
- ];
1194
+ return [`You spoke last, ${when}. It is waiting on a reviewer, not on you.`];
1065
1195
  const lines = [`You spoke last, ${when}. The ball is with ${them}.`];
1066
1196
  if (d?.lastCommitAt && d.lastEventAt && d.lastCommitAt < d.lastEventAt)
1067
1197
  lines.push(
@@ -1440,7 +1570,7 @@ var backdropStyle = (props) => ({
1440
1570
  inverse: false,
1441
1571
  backgroundColor: void 0
1442
1572
  });
1443
- var Text3 = ({ children, ...props }) => {
1573
+ var Text4 = ({ children, ...props }) => {
1444
1574
  const dimmed = useContext(DimContext);
1445
1575
  return dimmed ? /* @__PURE__ */ jsx(Text, { ...props, ...backdropStyle(props), children }) : /* @__PURE__ */ jsx(Text, { ...props, children });
1446
1576
  };
@@ -1836,13 +1966,13 @@ var InboxHeader = ({
1836
1966
  COLS - brand.length - countSeg.length - userSeg.length - workLabel.length - budgetSeg.length - statusSeg.length
1837
1967
  );
1838
1968
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1839
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: true, children: brand }),
1840
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: countSeg }),
1841
- userSeg ? /* @__PURE__ */ jsx(Text3, { children: userSeg }) : null,
1842
- scopeLabel ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: workLabel }) : null,
1843
- budgetLabel ? /* @__PURE__ */ jsx(Text3, { bold: true, color: budgetCritical ? "#FF5F5F" : colors.accent, children: budgetSeg }) : null,
1969
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, bold: true, children: brand }),
1970
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: countSeg }),
1971
+ userSeg ? /* @__PURE__ */ jsx(Text4, { children: userSeg }) : null,
1972
+ scopeLabel ? /* @__PURE__ */ jsx(Text4, { dimColor: true, children: workLabel }) : null,
1973
+ budgetLabel ? /* @__PURE__ */ jsx(Text4, { bold: true, color: budgetCritical ? "#FF5F5F" : colors.accent, children: budgetSeg }) : null,
1844
1974
  statusText ? /* @__PURE__ */ jsx(
1845
- Text3,
1975
+ Text4,
1846
1976
  {
1847
1977
  color: statusColor,
1848
1978
  dimColor: !statusColor,
@@ -1850,9 +1980,10 @@ var InboxHeader = ({
1850
1980
  children: statusSeg
1851
1981
  }
1852
1982
  ) : null,
1853
- /* @__PURE__ */ jsx(Text3, { color: colors.info, dimColor: true, children: "\u254C".repeat(fill) })
1983
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, dimColor: true, children: "\u254C".repeat(fill) })
1854
1984
  ] });
1855
1985
  };
1986
+ var stripSignature = (strip) => strip === null ? "\u2205" : strip.items.map((i) => `${i.key}=${i.state}${i.alarm ? "\u25B2" : ""}`).join(",");
1856
1987
  var toCiStatusState = (status) => status ? { kind: "ready", status } : { kind: "error" };
1857
1988
  var sameCiStatusState = (a, b) => {
1858
1989
  if (a.kind !== b.kind) return false;
@@ -1873,24 +2004,24 @@ var CiStatusLine = ({
1873
2004
  const name = job ?? "ci";
1874
2005
  if (state.kind === "loading")
1875
2006
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1876
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " \xB7 " }),
1877
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: `${name} loading\u2026` })
2007
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: " \xB7 " }),
2008
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: `${name} loading\u2026` })
1878
2009
  ] });
1879
2010
  if (state.kind === "error")
1880
2011
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1881
- /* @__PURE__ */ jsx(Text3, { color: colors.error, bold: true, children: " \u2717 " }),
1882
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: `${name} no build / not configured` })
2012
+ /* @__PURE__ */ jsx(Text4, { color: colors.error, bold: true, children: " \u2717 " }),
2013
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: `${name} no build / not configured` })
1883
2014
  ] });
1884
2015
  const { status } = state;
1885
2016
  const [, color] = jenkinsResultDisplay(status.result, status.building);
1886
2017
  const label = status.building ? "BUILDING" : status.result;
1887
2018
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1888
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " }),
1889
- /* @__PURE__ */ jsx(Text3, { color, bold: true, children: "\u25CF " }),
1890
- /* @__PURE__ */ jsx(Text3, { bold: true, children: status.job }),
1891
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + label }),
1892
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " #" + status.buildNumber }),
1893
- status.age ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + status.age }) : null
2019
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: " " }),
2020
+ /* @__PURE__ */ jsx(Text4, { color, bold: true, children: "\u25CF " }),
2021
+ /* @__PURE__ */ jsx(Text4, { bold: true, children: status.job }),
2022
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: " " + label }),
2023
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, children: " #" + status.buildNumber }),
2024
+ status.age ? /* @__PURE__ */ jsx(Text4, { dimColor: true, children: " " + status.age }) : null
1894
2025
  ] });
1895
2026
  };
1896
2027
  var FENCE_COLS = 46;
@@ -1903,10 +2034,10 @@ var RepoHeaderRow = ({
1903
2034
  const fill = Math.max(4, FENCE_COLS - label.length);
1904
2035
  const rule = "\u2500".repeat(fill);
1905
2036
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1906
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
1907
- /* @__PURE__ */ jsx(Text3, { dimColor: !active, children: "\u2500\u2500 " }),
2037
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: active ? "\u276F " : " " }),
2038
+ /* @__PURE__ */ jsx(Text4, { dimColor: !active, children: "\u2500\u2500 " }),
1908
2039
  /* @__PURE__ */ jsx(
1909
- Text3,
2040
+ Text4,
1910
2041
  {
1911
2042
  dimColor: false,
1912
2043
  color: active ? void 0 : colors.secondary,
@@ -1914,7 +2045,7 @@ var RepoHeaderRow = ({
1914
2045
  children: repo
1915
2046
  }
1916
2047
  ),
1917
- /* @__PURE__ */ jsx(Text3, { dimColor: !active, children: " " + rule })
2048
+ /* @__PURE__ */ jsx(Text4, { dimColor: !active, children: " " + rule })
1918
2049
  ] });
1919
2050
  };
1920
2051
  var MERGED_HOLD_MS = 3e3;
@@ -1979,22 +2110,22 @@ var ItemRow = ({
1979
2110
  return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false, active });
1980
2111
  if (item.kind === "subgroup-header")
1981
2112
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1982
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: true, children: " \xBB " }),
1983
- /* @__PURE__ */ jsx(Text3, { bold: true, children: item.label })
2113
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, bold: true, children: " \xBB " }),
2114
+ /* @__PURE__ */ jsx(Text4, { bold: true, children: item.label })
1984
2115
  ] });
1985
2116
  if (item.kind === "show-more")
1986
2117
  return /* @__PURE__ */ jsxs(Box, { children: [
1987
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
1988
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
1989
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: active ? "\u21B5 " : " " }),
1990
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: `+${item.hidden.length} more` })
2118
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: active ? "\u276F " : " " }),
2119
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: prefix }),
2120
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: active ? "\u21B5 " : " " }),
2121
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: `+${item.hidden.length} more` })
1991
2122
  ] });
1992
2123
  if (item.kind === "show-less")
1993
2124
  return /* @__PURE__ */ jsxs(Box, { children: [
1994
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
1995
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
1996
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: active ? "\u21B5 " : " " }),
1997
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "show less" })
2125
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: active ? "\u276F " : " " }),
2126
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: prefix }),
2127
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: active ? "\u21B5 " : " " }),
2128
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "show less" })
1998
2129
  ] });
1999
2130
  if (item.kind === "task") {
2000
2131
  const note = item.note ?? "";
@@ -2004,14 +2135,14 @@ var ItemRow = ({
2004
2135
  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) - 12
2005
2136
  );
2006
2137
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
2007
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
2008
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
2009
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: parent ? "\u252C " : " " }),
2010
- /* @__PURE__ */ jsx(Text3, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
2011
- markerCols > 0 ? /* @__PURE__ */ jsx(Text3, { color: item.markerColor, children: (item.marker ?? "").padEnd(markerCols) + " " }) : null,
2012
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: active, children: item.key + " " }),
2138
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: active ? "\u276F " : " " }),
2139
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: prefix }),
2140
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: parent ? "\u252C " : " " }),
2141
+ /* @__PURE__ */ jsx(Text4, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
2142
+ markerCols > 0 ? /* @__PURE__ */ jsx(Text4, { color: item.markerColor, children: (item.marker ?? "").padEnd(markerCols) + " " }) : null,
2143
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, bold: active, children: item.key + " " }),
2013
2144
  /* @__PURE__ */ jsx(
2014
- Text3,
2145
+ Text4,
2015
2146
  {
2016
2147
  bold: active || !!transient && isArrival(transient),
2017
2148
  dimColor: !!transient && isDeparture(transient),
@@ -2020,7 +2151,7 @@ var ItemRow = ({
2020
2151
  }
2021
2152
  ),
2022
2153
  item.pill && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2023
- /* @__PURE__ */ jsx(Text3, { children: " " }),
2154
+ /* @__PURE__ */ jsx(Text4, { children: " " }),
2024
2155
  /* @__PURE__ */ jsx(
2025
2156
  Pill,
2026
2157
  {
@@ -2031,9 +2162,9 @@ var ItemRow = ({
2031
2162
  }
2032
2163
  )
2033
2164
  ] }) : null,
2034
- note ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${note}` }) : null,
2165
+ note ? /* @__PURE__ */ jsx(Text4, { dimColor: true, children: ` ${note}` }) : null,
2035
2166
  farewellLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2036
- /* @__PURE__ */ jsx(Text3, { children: " " }),
2167
+ /* @__PURE__ */ jsx(Text4, { children: " " }),
2037
2168
  /* @__PURE__ */ jsx(Pill, { color: TRANSIT_COLOUR.out, children: farewellLabel })
2038
2169
  ] }) : null
2039
2170
  ] });
@@ -2049,7 +2180,7 @@ var ItemRow = ({
2049
2180
  const sizeParts = sizePartsOf(item);
2050
2181
  const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
2051
2182
  const agePair = item.activityAge && item.activityAge !== item.age ? { activity: item.activityAge, lifetime: item.age } : null;
2052
- const ageLabel = agePair ? `${agePair.activity} (${agePair.lifetime})` : item.age;
2183
+ const ageLabel2 = agePair ? `${agePair.activity} (${agePair.lifetime})` : item.age;
2053
2184
  const mergedLabel = merged ? "merged" : "";
2054
2185
  const pillCaps = [mergedLabel, farewellLabel].filter(Boolean).length * 2;
2055
2186
  const repoLabel = depthOf(item) > 0 ? item.repo : "";
@@ -2082,7 +2213,7 @@ var ItemRow = ({
2082
2213
  };
2083
2214
  const widthOf2 = () => {
2084
2215
  const suffix = [
2085
- givingUp.age ? "" : ageLabel || "",
2216
+ givingUp.age ? "" : ageLabel2 || "",
2086
2217
  // ASCII only, so no PUA double-width correction — see the label cell.
2087
2218
  givingUp.size ? "" : sizeLabel || "",
2088
2219
  givingUp.threads ? "" : unresolvedLabel,
@@ -2108,13 +2239,13 @@ var ItemRow = ({
2108
2239
  const labelLabel = labelCell();
2109
2240
  const titleMax = Math.max(1, cols - widthOf2());
2110
2241
  return /* @__PURE__ */ jsxs(Box, { children: [
2111
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
2112
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
2113
- /* @__PURE__ */ jsx(Text3, { color, bold: true, children: icon + " " }),
2114
- /* @__PURE__ */ jsx(Text3, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
2115
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: numStr }),
2242
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: active ? "\u276F " : " " }),
2243
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: prefix }),
2244
+ /* @__PURE__ */ jsx(Text4, { color, bold: true, children: icon + " " }),
2245
+ /* @__PURE__ */ jsx(Text4, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
2246
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, children: numStr }),
2116
2247
  /* @__PURE__ */ jsx(
2117
- Text3,
2248
+ Text4,
2118
2249
  {
2119
2250
  bold: active || !!transient && isArrival(transient),
2120
2251
  dimColor: !!transient && isDeparture(transient),
@@ -2122,33 +2253,33 @@ var ItemRow = ({
2122
2253
  children: truncate(item.title, titleMax) + " "
2123
2254
  }
2124
2255
  ),
2125
- labelLabel ? /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: labelLabel + " " }) : null,
2126
- repoLabel && !givingUp.repo ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: repoLabel }) : null,
2127
- sizeParts && !givingUp.size ? /* @__PURE__ */ jsxs(Text3, { children: [
2256
+ labelLabel ? /* @__PURE__ */ jsx(Text4, { color: colors.secondary, children: labelLabel + " " }) : null,
2257
+ repoLabel && !givingUp.repo ? /* @__PURE__ */ jsx(Text4, { dimColor: true, children: repoLabel }) : null,
2258
+ sizeParts && !givingUp.size ? /* @__PURE__ */ jsxs(Text4, { children: [
2128
2259
  " ",
2129
- /* @__PURE__ */ jsx(Text3, { color: colors.success, children: sizeParts.added }),
2260
+ /* @__PURE__ */ jsx(Text4, { color: colors.success, children: sizeParts.added }),
2130
2261
  " ",
2131
- /* @__PURE__ */ jsx(Text3, { color: colors.error, children: sizeParts.removed })
2262
+ /* @__PURE__ */ jsx(Text4, { color: colors.error, children: sizeParts.removed })
2132
2263
  ] }) : null,
2133
2264
  unresolvedLabel && !givingUp.threads ? /* @__PURE__ */ jsx(
2134
- Text3,
2265
+ Text4,
2135
2266
  {
2136
2267
  bold: !spokeLast,
2137
2268
  color: spokeLast ? colors.secondary : colors.accent,
2138
2269
  children: " " + unresolvedLabel
2139
2270
  }
2140
2271
  ) : null,
2141
- showAuthor && !givingUp.author ? /* @__PURE__ */ jsx(Text3, { dimColor: true, italic: true, children: " by " + item.author }) : null,
2142
- ageLabel && !givingUp.age ? agePair ? /* @__PURE__ */ jsxs(Fragment, { children: [
2143
- /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: " " + agePair.activity }),
2144
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` (${agePair.lifetime})` })
2145
- ] }) : /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: " " + ageLabel }) : null,
2272
+ showAuthor && !givingUp.author ? /* @__PURE__ */ jsx(Text4, { dimColor: true, italic: true, children: " by " + item.author }) : null,
2273
+ ageLabel2 && !givingUp.age ? agePair ? /* @__PURE__ */ jsxs(Fragment, { children: [
2274
+ /* @__PURE__ */ jsx(Text4, { color: colors.secondary, children: " " + agePair.activity }),
2275
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: ` (${agePair.lifetime})` })
2276
+ ] }) : /* @__PURE__ */ jsx(Text4, { color: colors.secondary, children: " " + ageLabel2 }) : null,
2146
2277
  mergedLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2147
- /* @__PURE__ */ jsx(Text3, { children: " " }),
2278
+ /* @__PURE__ */ jsx(Text4, { children: " " }),
2148
2279
  /* @__PURE__ */ jsx(Pill, { color: MERGED_COLOUR, children: mergedLabel })
2149
2280
  ] }) : null,
2150
2281
  farewellLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2151
- /* @__PURE__ */ jsx(Text3, { children: " " }),
2282
+ /* @__PURE__ */ jsx(Text4, { children: " " }),
2152
2283
  /* @__PURE__ */ jsx(Pill, { color: TRANSIT_COLOUR.out, children: farewellLabel })
2153
2284
  ] }) : null
2154
2285
  ] });
@@ -2196,15 +2327,15 @@ var ActionMenu = ({
2196
2327
  paddingX: 1,
2197
2328
  marginTop: 1,
2198
2329
  children: [
2199
- /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: title }),
2200
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(32) }),
2330
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, bold: true, children: title }),
2331
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "\u2500".repeat(32) }),
2201
2332
  actions.map((a, i) => /* @__PURE__ */ jsxs(Box, { children: [
2202
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: i === cursor ? "\u276F " : " " }),
2203
- /* @__PURE__ */ jsx(Text3, { bold: i === cursor, children: a.label }),
2204
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + a.hint })
2333
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: i === cursor ? "\u276F " : " " }),
2334
+ /* @__PURE__ */ jsx(Text4, { bold: i === cursor, children: a.label }),
2335
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: " " + a.hint })
2205
2336
  ] }, a.label)),
2206
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(32) }),
2207
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2191\u2193 navigate \u21B5 confirm esc cancel" })
2337
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "\u2500".repeat(32) }),
2338
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "\u2191\u2193 navigate \u21B5 confirm esc cancel" })
2208
2339
  ]
2209
2340
  }
2210
2341
  );
@@ -2252,11 +2383,11 @@ var LegendGroup = ({
2252
2383
  width,
2253
2384
  marginRight
2254
2385
  }) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, marginRight, children: [
2255
- pinned !== void 0 ? /* @__PURE__ */ jsx(Text3, { bold: true, dimColor: true, children: pinned ?? " " }) : null,
2386
+ pinned !== void 0 ? /* @__PURE__ */ jsx(Text4, { bold: true, dimColor: true, children: pinned ?? " " }) : null,
2256
2387
  lines.map(
2257
- (line, index) => line.kind === "gap" ? /* @__PURE__ */ jsx(Text3, { children: " " }, index) : line.kind === "title" ? /* @__PURE__ */ jsx(Text3, { bold: true, dimColor: true, children: line.text }, index) : /* @__PURE__ */ jsxs(Box, { children: [
2258
- /* @__PURE__ */ jsx(Text3, { color: line.row.color, bold: line.row.bold, children: line.row.cell.padEnd(line.cellWidth) }),
2259
- /* @__PURE__ */ jsx(Text3, { wrap: "truncate-end", children: line.row.label })
2388
+ (line, index) => line.kind === "gap" ? /* @__PURE__ */ jsx(Text4, { children: " " }, index) : line.kind === "title" ? /* @__PURE__ */ jsx(Text4, { bold: true, dimColor: true, children: line.text }, index) : /* @__PURE__ */ jsxs(Box, { children: [
2389
+ /* @__PURE__ */ jsx(Text4, { color: line.row.color, bold: line.row.bold, children: line.row.cell.padEnd(line.cellWidth) }),
2390
+ /* @__PURE__ */ jsx(Text4, { wrap: "truncate-end", children: line.row.label })
2260
2391
  ] }, index)
2261
2392
  )
2262
2393
  ] });
@@ -2377,8 +2508,8 @@ var HelpModal = ({
2377
2508
  width: contentWidth + MODAL_CHROME_COLS,
2378
2509
  children: [
2379
2510
  /* @__PURE__ */ jsxs(Box, { children: [
2380
- /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: "Legend" }),
2381
- scrollable ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${offset + 1}\u2013${offset + shown} of ${totalRows}` }) : null
2511
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, bold: true, children: "Legend" }),
2512
+ scrollable ? /* @__PURE__ */ jsx(Text4, { dimColor: true, children: ` ${offset + 1}\u2013${offset + shown} of ${totalRows}` }) : null
2382
2513
  ] }),
2383
2514
  /* @__PURE__ */ jsx(Box, { marginTop: 1, width: contentWidth, children: groups.map((group, index) => /* @__PURE__ */ jsx(
2384
2515
  LegendGroup,
@@ -2390,8 +2521,8 @@ var HelpModal = ({
2390
2521
  },
2391
2522
  group[0]?.title ?? index
2392
2523
  )) }),
2393
- tabHelp && !scrollable ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: LEGEND_FOOTNOTE }) : null,
2394
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: scrollable ? "\u2191\u2193 scroll \xB7 esc \xB7 ? close" : "esc \xB7 ? close" })
2524
+ tabHelp && !scrollable ? /* @__PURE__ */ jsx(Text4, { dimColor: true, children: LEGEND_FOOTNOTE }) : null,
2525
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: scrollable ? "\u2191\u2193 scroll \xB7 esc \xB7 ? close" : "esc \xB7 ? close" })
2395
2526
  ]
2396
2527
  }
2397
2528
  );
@@ -2406,13 +2537,13 @@ var ExplainModal = ({ item, login }) => /* @__PURE__ */ jsxs(
2406
2537
  paddingX: 1,
2407
2538
  width: Math.min(COLS, 78),
2408
2539
  children: [
2409
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: true, children: `#${item.number} \xB7 ${item.repo}` }),
2410
- /* @__PURE__ */ jsx(Text3, { children: item.title }),
2540
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, bold: true, children: `#${item.number} \xB7 ${item.repo}` }),
2541
+ /* @__PURE__ */ jsx(Text4, { children: item.title }),
2411
2542
  explainItem(item, login).map((section) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
2412
- /* @__PURE__ */ jsx(Text3, { bold: true, dimColor: true, children: section.heading }),
2413
- section.lines.map((line, i) => /* @__PURE__ */ jsx(Text3, { children: " " + line }, i))
2543
+ /* @__PURE__ */ jsx(Text4, { bold: true, dimColor: true, children: section.heading }),
2544
+ section.lines.map((line, i) => /* @__PURE__ */ jsx(Text4, { children: " " + line }, i))
2414
2545
  ] }, section.heading)),
2415
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "esc \xB7 e close" }) })
2546
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "esc \xB7 e close" }) })
2416
2547
  ]
2417
2548
  }
2418
2549
  );
@@ -2438,21 +2569,21 @@ var RepoPicker = ({
2438
2569
  paddingX: 1,
2439
2570
  minWidth: 42,
2440
2571
  children: [
2441
- /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: `Filter by repo (${selected.size} on)` }),
2442
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(36) }),
2572
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, bold: true, children: `Filter by repo (${selected.size} on)` }),
2573
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "\u2500".repeat(36) }),
2443
2574
  visible.map((repo, i) => {
2444
2575
  const idx = start + i;
2445
2576
  const on = selected.has(repo);
2446
2577
  const active = idx === cursor;
2447
2578
  return /* @__PURE__ */ jsxs(Box, { children: [
2448
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
2449
- /* @__PURE__ */ jsx(Text3, { color: on ? "green" : void 0, children: on ? "\u25C9 " : "\u25CB " }),
2450
- /* @__PURE__ */ jsx(Text3, { bold: active, children: repo })
2579
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: active ? "\u276F " : " " }),
2580
+ /* @__PURE__ */ jsx(Text4, { color: on ? "green" : void 0, children: on ? "\u25C9 " : "\u25CB " }),
2581
+ /* @__PURE__ */ jsx(Text4, { bold: active, children: repo })
2451
2582
  ] }, repo);
2452
2583
  }),
2453
- repos.length > budget ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` \u2026 ${repos.length} repos total` }) : null,
2454
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(36) }),
2455
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "space toggle \xB7 a clear all \xB7 \u21B5/esc done" })
2584
+ repos.length > budget ? /* @__PURE__ */ jsx(Text4, { dimColor: true, children: ` \u2026 ${repos.length} repos total` }) : null,
2585
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "\u2500".repeat(36) }),
2586
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: "space toggle \xB7 a clear all \xB7 \u21B5/esc done" })
2456
2587
  ]
2457
2588
  }
2458
2589
  );
@@ -2487,6 +2618,8 @@ var BrowseScreen = ({
2487
2618
  extensions,
2488
2619
  ciStatusState,
2489
2620
  ciJob,
2621
+ stripState,
2622
+ stripLabel,
2490
2623
  tabHelp,
2491
2624
  origin,
2492
2625
  budget,
@@ -2528,12 +2661,13 @@ var BrowseScreen = ({
2528
2661
  const [palette, setPalette] = useState(null);
2529
2662
  const filterActive = search != null || repoFilter.size > 0;
2530
2663
  const reserveCiRow = ciStatusState != null;
2664
+ const reserveStripRow = stripState != null;
2531
2665
  const ciStatus = ciStatusState?.kind === "ready" ? ciStatusState.status : null;
2532
2666
  const listHeight = Math.max(
2533
2667
  5,
2534
2668
  // -10 rather than -8: the extra 2 are the frame's top and bottom border
2535
2669
  // rows, so the tree never grows taller than the terminal inside the frame.
2536
- rows - 10 - (filterActive ? 2 : 0) - (reserveCiRow ? 2 : 0)
2670
+ rows - 10 - (filterActive ? 2 : 0) - (reserveCiRow ? 2 : 0) - (reserveStripRow ? 2 : 0)
2537
2671
  );
2538
2672
  useEffect(() => {
2539
2673
  setCursors((p) => ({ ...p, [activeId]: 0 }));
@@ -2627,10 +2761,13 @@ var BrowseScreen = ({
2627
2761
  return out;
2628
2762
  }, [localSections, transients]);
2629
2763
  const [railOpen, setRailOpen] = useState(false);
2630
- const showRail = railOpen && !!sidebar;
2764
+ const rails = railsOf(sidebar);
2765
+ const hasRail = rails.length > 0;
2766
+ const railTitle = rails.map((s) => s.title.toLowerCase()).join(" \xB7 ");
2767
+ const showRail = railOpen && hasRail;
2631
2768
  const [railFocus, setRailFocus] = useState(false);
2632
2769
  const [railCursor, setRailCursor] = useState(0);
2633
- const railRows = sidebar?.rows ?? [];
2770
+ const railRows = railRowsOf(sidebar);
2634
2771
  const railActive = showRail && railFocus && railRows.length > 0;
2635
2772
  const railAt = Math.min(railCursor, Math.max(0, railRows.length - 1));
2636
2773
  const peel = () => {
@@ -2804,7 +2941,7 @@ var BrowseScreen = ({
2804
2941
  setRepoPicker(true);
2805
2942
  return;
2806
2943
  }
2807
- if (input === "i" && sidebar) {
2944
+ if (input === "i" && hasRail) {
2808
2945
  setRailOpen((open) => {
2809
2946
  if (open) setRailFocus(false);
2810
2947
  return !open;
@@ -2845,6 +2982,13 @@ var BrowseScreen = ({
2845
2982
  }
2846
2983
  return;
2847
2984
  }
2985
+ if (input === "c") {
2986
+ const row = railRows[railAt];
2987
+ if (!row?.url) return;
2988
+ clipboard(row.url);
2989
+ showFlash(`\u2713 Copied URL for ${row.key}`);
2990
+ return;
2991
+ }
2848
2992
  if (input !== "r" && input !== "q" && !key.leftArrow && !key.rightArrow)
2849
2993
  return;
2850
2994
  }
@@ -3103,9 +3247,10 @@ var BrowseScreen = ({
3103
3247
  // reach a row is worse than a short footer: it names a key that does
3104
3248
  // nothing where you are standing.
3105
3249
  [
3106
- ["\u2191\u2193", "initiative"],
3250
+ ["\u2191\u2193", "row"],
3107
3251
  ["\u21B5", "open"],
3108
3252
  ["o", "browser"],
3253
+ ["c", "copy url"],
3109
3254
  ["\u21E5/esc", "back to list"],
3110
3255
  ["?", "help"],
3111
3256
  ["q", "quit"]
@@ -3128,7 +3273,7 @@ var BrowseScreen = ({
3128
3273
  ...jiraBase ? [["\u2303K", "launch"]] : [],
3129
3274
  // Advertised only where it does something, and named for what it
3130
3275
  // shows rather than for the furniture: nobody wants "a sidebar".
3131
- ...showRail ? [["\u21E5", sidebar.title.toLowerCase()]] : sidebar ? [["i", sidebar.title.toLowerCase()]] : [],
3276
+ ...showRail ? [["\u21E5", railTitle]] : hasRail ? [["i", railTitle]] : [],
3132
3277
  ["?", "help"],
3133
3278
  ["q", "quit"]
3134
3279
  ];
@@ -3142,11 +3287,11 @@ var BrowseScreen = ({
3142
3287
  const query = palette.trim();
3143
3288
  const ticketKey = jiraKeyRe ? query.toUpperCase().match(jiraKeyRe)?.[0] : void 0;
3144
3289
  const commands = [];
3145
- const rich = (key, where) => /* @__PURE__ */ jsxs(Text3, { children: [
3290
+ const rich = (key, where) => /* @__PURE__ */ jsxs(Text4, { children: [
3146
3291
  "Open ",
3147
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: key }),
3292
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, children: key }),
3148
3293
  " ",
3149
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: where })
3294
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: where })
3150
3295
  ] });
3151
3296
  const labels = /* @__PURE__ */ new Map();
3152
3297
  if (ticketKey && jiraBase) {
@@ -3234,7 +3379,14 @@ var BrowseScreen = ({
3234
3379
  selected: repoFilter,
3235
3380
  cursor: Math.min(repoCursor, Math.max(0, allRepos.length - 1))
3236
3381
  }
3237
- ) : menu.actions && activeItem && activeItem.kind !== "repo-header" ? /* @__PURE__ */ jsx(ActionMenu, { item: activeItem, actions: menu.actions, cursor: menu.cursor }) : null;
3382
+ ) : menu.actions && activeItem && activeItem.kind !== "repo-header" ? /* @__PURE__ */ jsx(
3383
+ ActionMenu,
3384
+ {
3385
+ item: activeItem,
3386
+ actions: menu.actions,
3387
+ cursor: menu.cursor
3388
+ }
3389
+ ) : null;
3238
3390
  return /* @__PURE__ */ jsxs(
3239
3391
  Box,
3240
3392
  {
@@ -3262,9 +3414,17 @@ var BrowseScreen = ({
3262
3414
  }
3263
3415
  ),
3264
3416
  ciStatusState ? /* @__PURE__ */ jsx(CiStatusLine, { state: ciStatusState, job: ciJob }) : null,
3417
+ stripState ? /* @__PURE__ */ jsx(
3418
+ StatusStripLine,
3419
+ {
3420
+ strip: stripState.kind === "ready" ? stripState.strip : void 0,
3421
+ label: stripLabel,
3422
+ width: COLS
3423
+ }
3424
+ ) : null,
3265
3425
  skippedForBudget ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3266
- /* @__PURE__ */ jsx(Text3, { bold: true, color: colors.accent, children: " \u26A1 auto-refresh paused to save API budget" }),
3267
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " r refreshes anyway" })
3426
+ /* @__PURE__ */ jsx(Text4, { bold: true, color: colors.accent, children: " \u26A1 auto-refresh paused to save API budget" }),
3427
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: " r refreshes anyway" })
3268
3428
  ] }) : null,
3269
3429
  /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
3270
3430
  Tabs,
@@ -3300,14 +3460,14 @@ var BrowseScreen = ({
3300
3460
  }
3301
3461
  ) }),
3302
3462
  search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3303
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: " / " }),
3304
- /* @__PURE__ */ jsx(Text3, { children: search }),
3305
- searchInput ? /* @__PURE__ */ jsx(Text3, { color: colors.info, children: "\u258F" }) : null,
3306
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}` })
3463
+ /* @__PURE__ */ jsx(Text4, { color: colors.info, children: " / " }),
3464
+ /* @__PURE__ */ jsx(Text4, { children: search }),
3465
+ searchInput ? /* @__PURE__ */ jsx(Text4, { color: colors.info, children: "\u258F" }) : null,
3466
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}` })
3307
3467
  ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3308
- /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " \u25C9 " }),
3309
- /* @__PURE__ */ jsx(Text3, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
3310
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " f edit \xB7 esc clear" })
3468
+ /* @__PURE__ */ jsx(Text4, { color: colors.accent, children: " \u25C9 " }),
3469
+ /* @__PURE__ */ jsx(Text4, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
3470
+ /* @__PURE__ */ jsx(Text4, { dimColor: true, children: " f edit \xB7 esc clear" })
3311
3471
  ] }) : null,
3312
3472
  /* @__PURE__ */ jsxs(Box, { children: [
3313
3473
  /* @__PURE__ */ jsxs(
@@ -3345,7 +3505,7 @@ var BrowseScreen = ({
3345
3505
  },
3346
3506
  `${viewStart + i}:${rowKey(item)}`
3347
3507
  )) }),
3348
- hasMore && /* @__PURE__ */ jsxs(Text3, { dimColor: true, children: [
3508
+ hasMore && /* @__PURE__ */ jsxs(Text4, { dimColor: true, children: [
3349
3509
  " ",
3350
3510
  "\u2193 ",
3351
3511
  section.items.length - viewStart - visibleCount,
@@ -3367,10 +3527,10 @@ var BrowseScreen = ({
3367
3527
  ]
3368
3528
  }
3369
3529
  ),
3370
- showRail && sidebar ? /* @__PURE__ */ jsx(
3530
+ showRail ? /* @__PURE__ */ jsx(
3371
3531
  SidePanel,
3372
3532
  {
3373
- sidebar,
3533
+ sidebar: rails,
3374
3534
  liveLabel,
3375
3535
  width: railCols,
3376
3536
  height: listHeight,
@@ -3379,7 +3539,7 @@ var BrowseScreen = ({
3379
3539
  }
3380
3540
  ) : null
3381
3541
  ] }),
3382
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3542
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text4, { color: colors.success, children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3383
3543
  ]
3384
3544
  }
3385
3545
  );
@@ -3394,7 +3554,7 @@ var NoRowsScreen = ({
3394
3554
  });
3395
3555
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
3396
3556
  /* @__PURE__ */ jsx(StatusMessage, { variant: reason === "empty" ? "info" : "error", children: reason === "empty" ? "Nothing open." : `Fetch failed \u2014 ${detail ?? "no reason reported"}` }),
3397
- reason === "empty" && detail ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: detail }) }) : null,
3557
+ reason === "empty" && detail ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text4, { dimColor: true, children: detail }) }) : null,
3398
3558
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
3399
3559
  FooterHints,
3400
3560
  {
@@ -3425,6 +3585,9 @@ var App = ({
3425
3585
  ciJob,
3426
3586
  ciFetcher,
3427
3587
  ciPollMs = 6e4,
3588
+ stripFetcher,
3589
+ stripLabel,
3590
+ stripPollMs = 3e5,
3428
3591
  watchPath,
3429
3592
  watchDebounceMs = 400,
3430
3593
  // Zero by default, deliberately. A package that adds randomness to its own
@@ -3468,6 +3631,15 @@ var App = ({
3468
3631
  const next = toCiStatusState(status);
3469
3632
  return sameCiStatusState(prev, next) ? prev : next;
3470
3633
  });
3634
+ const hasStrip = !!stripFetcher;
3635
+ const [stripState, setStripState] = useState({
3636
+ kind: "loading"
3637
+ });
3638
+ const applyStrip = (strip) => setStripState((prev) => {
3639
+ const next = { kind: "ready", strip };
3640
+ if (prev.kind !== "ready") return next;
3641
+ return stripSignature(prev.strip) === stripSignature(strip) ? prev : next;
3642
+ });
3471
3643
  const displayedKey = useRef("");
3472
3644
  const displayedSections = useRef([]);
3473
3645
  const [transients, setTransients] = useState(NO_TRANSIENTS);
@@ -3550,13 +3722,10 @@ var App = ({
3550
3722
  const running = [...heldSince].filter(([tab]) => held.has(tab));
3551
3723
  if (running.length === 0) return;
3552
3724
  const remaining = (at) => at + TRANSIT_HOLD_MS - Date.now();
3553
- const timer = setTimeout(
3554
- () => {
3555
- for (const [tab, at] of running)
3556
- if (remaining(at) <= 0) settleTab(tab, transients);
3557
- },
3558
- Math.max(0, Math.min(...running.map(([, at]) => remaining(at))))
3559
- );
3725
+ const timer = setTimeout(() => {
3726
+ for (const [tab, at] of running)
3727
+ if (remaining(at) <= 0) settleTab(tab, transients);
3728
+ }, Math.max(0, Math.min(...running.map(([, at]) => remaining(at)))));
3560
3729
  return () => clearTimeout(timer);
3561
3730
  }, [transients, heldSince, state.phase]);
3562
3731
  const pendingSummary = useMemo(
@@ -3664,6 +3833,23 @@ var App = ({
3664
3833
  clearInterval(id);
3665
3834
  };
3666
3835
  }, [hasCiStatus, ciFetcher, ciPollMs]);
3836
+ useEffect(() => {
3837
+ if (!stripFetcher) return;
3838
+ let live = true;
3839
+ const poll = () => {
3840
+ stripFetcher().then((strip) => {
3841
+ if (!live) return;
3842
+ applyStrip(strip);
3843
+ }).catch(() => {
3844
+ });
3845
+ };
3846
+ poll();
3847
+ const id = setInterval(poll, stripPollMs);
3848
+ return () => {
3849
+ live = false;
3850
+ clearInterval(id);
3851
+ };
3852
+ }, [stripFetcher, stripPollMs]);
3667
3853
  useEffect(() => {
3668
3854
  if (!watchPath) return;
3669
3855
  const dir = dirname(watchPath);
@@ -3748,6 +3934,14 @@ var App = ({
3748
3934
  }
3749
3935
  ),
3750
3936
  hasCiStatus ? /* @__PURE__ */ jsx(CiStatusLine, { state: ciStatusState, job: ciJob }) : null,
3937
+ hasStrip ? /* @__PURE__ */ jsx(
3938
+ StatusStripLine,
3939
+ {
3940
+ strip: stripState.kind === "ready" ? stripState.strip : void 0,
3941
+ label: stripLabel,
3942
+ width: COLS
3943
+ }
3944
+ ) : null,
3751
3945
  /* @__PURE__ */ jsx(
3752
3946
  Box,
3753
3947
  {
@@ -3834,6 +4028,8 @@ var App = ({
3834
4028
  liveLabel,
3835
4029
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
3836
4030
  ciJob,
4031
+ stripState: hasStrip ? stripState : void 0,
4032
+ stripLabel,
3837
4033
  tabHelp,
3838
4034
  onOpenPr: (item) => setState({
3839
4035
  phase: "pr",
@@ -3888,4 +4084,4 @@ var matchesFilter = (repo, filter) => {
3888
4084
  };
3889
4085
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
3890
4086
 
3891
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, SIDEBAR_COLS, SidePanel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, railCapacity, railWidth, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, toCiStatusState, truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
4087
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.58.0",
3
+ "version": "0.59.1",
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",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@kud/gh": "0.17.0",
52
- "@kud/gh-workflow": "0.12.1",
52
+ "@kud/gh-workflow": "0.13.0",
53
53
  "@kud/ink-ui": "0.30.0",
54
54
  "zx": "8.8.5"
55
55
  },