@kud/gh-ink 0.52.1 → 0.54.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
@@ -66,11 +66,26 @@ type SidebarRow = {
66
66
  */
67
67
  url?: string;
68
68
  };
69
+ /**
70
+ * Plain data, deliberately: a host caches its last fetch to disk and paints it
71
+ * on the next launch, and this rides along in that file. A function on it would
72
+ * not survive the round trip, so the host's words for `live` travel as a prop
73
+ * on `SidePanel` instead — vocabulary is configuration, not data that goes
74
+ * stale.
75
+ */
69
76
  type Sidebar = {
70
77
  /** Heading for the rail. The host's vocabulary, never ours. */
71
78
  title: string;
72
79
  rows: SidebarRow[];
73
80
  };
81
+ /**
82
+ * What to call `live` on screen, in the host's words — `4 on board` and `off
83
+ * board` on a ticket cockpit, say. Takes the number rather than a word pair
84
+ * because the two states need not share a sentence shape: "off board" has no
85
+ * N in it. `live` itself stays a number, so the absent-draws-nothing contract
86
+ * and the host's sort are untouched.
87
+ */
88
+ type LiveLabel = (live: number) => string;
74
89
  /**
75
90
  * The rail's width, INCLUDING its rule and padding. Exported because the list
76
91
  * beside it has to shrink by exactly this much: every row in that list truncates
@@ -83,18 +98,26 @@ type Sidebar = {
83
98
  * word and left the rail listing keys with an ellipsis after them. The key line
84
99
  * was never the constraint — the label is what makes a key readable, and a rail
85
100
  * you cannot read is forty columns spent on nothing.
101
+ *
102
+ * Widened again once the label became the row's first line: at 45 usable
103
+ * columns a real epic title still lost its last two words. But a 200-column
104
+ * terminal has room to give where a 120-column one does not — at 120 a rail
105
+ * this wide leaves the list 52 columns, which elides a PR title past its own
106
+ * key. So this is the CEILING, and `railWidth` sizes the rail to the frame:
107
+ * a third of it, never below the 52 the grid was laid out for, never above.
86
108
  */
87
- declare const SIDEBAR_COLS = 52;
109
+ declare const SIDEBAR_COLS = 64;
110
+ declare const MIN_SIDEBAR_COLS = 52;
111
+ /** How wide the rail is in a frame of `cols` — pass the same number to `width`. */
112
+ declare const railWidth: (cols: number) => number;
88
113
  /**
89
- * The dim figures after the key: how far through, and how much is moving.
90
- *
91
- * Either half may be missing and the other still worth saying, so this is a join
92
- * of what is known rather than one of three fixed shapes. `done`/`total` are
93
- * treated as a pair — a numerator with no denominator is not progress, it is a
94
- * number — and a zero is printed wherever it was counted, because "nothing is
95
- * moving" is exactly the thing a roadmap is read to notice.
114
+ * The plain-text facts of a row, for a host or a test that wants them as one
115
+ * string: how far through, then how much is moving. Either half may be missing
116
+ * and the other still worth saying, and a zero is printed wherever it was
117
+ * counted, because "nothing is moving" is exactly the thing a roadmap is read
118
+ * to notice.
96
119
  */
97
- declare const counts: (row: SidebarRow) => string;
120
+ declare const counts: (row: SidebarRow, liveLabel?: LiveLabel) => string;
98
121
  /**
99
122
  * How many rows fit in `height` — one fewer than would physically fit whenever
100
123
  * that means anything is left over, to buy the line that says so.
@@ -108,18 +131,23 @@ declare const railCapacity: (height: number, rows: number) => number;
108
131
  /**
109
132
  * A right-hand rail of initiatives, standing beside the list.
110
133
  *
111
- * Two lines per row rather than one, because both halves are load-bearing and
112
- * neither survives the other being cut: a key with no words cannot be read at a
113
- * glance, and words with no key cannot be opened. Forty columns is not enough for
114
- * both on one line, so they stack.
134
+ * Label first, facts beneath, because a roadmap is read by name: nobody thinks
135
+ * "ACC-11312", they think "the OpenSearch migration". So the label is the
136
+ * bright line and the key drops to the facts line as what it is — a reference
137
+ * you follow, in the secondary tier. Two lines rather than one because both are
138
+ * load-bearing and neither survives the other being cut: words with no key
139
+ * cannot be opened, and a key with no words cannot be read at a glance.
115
140
  *
116
141
  * Presentational, like every other row renderer here: it DRAWS a cursor but does
117
142
  * not own one, and it never calls `useInput`. Where the cursor is, and whether
118
143
  * the arrows are pointed at this rail at all, are the host's state — which is
119
144
  * what lets a screen with two focus regions have exactly one of them lit.
120
145
  */
121
- declare const SidePanel: ({ sidebar, height, focused, cursor, }: {
146
+ declare const SidePanel: ({ sidebar, liveLabel, width, height, focused, cursor, }: {
122
147
  sidebar: Sidebar;
148
+ liveLabel?: LiveLabel;
149
+ /** From `railWidth`, so the list beside it can subtract the same number. */
150
+ width?: number;
123
151
  height?: number;
124
152
  /** The arrows are pointed here, so this rail draws the cursor. */
125
153
  focused?: boolean;
@@ -184,12 +212,20 @@ type CachedCockpit = {
184
212
  login: string;
185
213
  at: number;
186
214
  budget?: InboxBudget;
215
+ /**
216
+ * The rail, when the host supplied one. Cached beside the rows because it is
217
+ * painted beside them: a launch that trusts the cache never refetches, so a
218
+ * rail left out of the file is a rail that does not exist until the next
219
+ * refresh — `i` did nothing and the footer did not even offer it.
220
+ */
221
+ sidebar?: Sidebar;
187
222
  };
188
223
  declare const readCache: (key: string) => CachedCockpit | null;
189
224
  declare const writeCache: (key: string, data: {
190
225
  sections: Section[];
191
226
  login: string;
192
227
  budget?: InboxBudget;
228
+ sidebar?: Sidebar;
193
229
  }) => void;
194
230
 
195
231
  type DetailContext = {
@@ -378,7 +414,7 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
378
414
  cursor: number;
379
415
  }) => React.JSX.Element;
380
416
  declare const signatureOf: (sections: Section[]) => string;
381
- declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, }: {
417
+ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, liveLabel, }: {
382
418
  fetcher: () => Promise<{
383
419
  sections: Section[];
384
420
  login: string;
@@ -401,6 +437,13 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
401
437
  sidebar?: Sidebar;
402
438
  }>;
403
439
  cacheKey?: string;
440
+ /**
441
+ * What the rail calls a row's `live` count, in this host's words — `4 on
442
+ * board` / `off board` on a ticket cockpit. A prop rather than part of the
443
+ * fetch result because it is vocabulary, not data: it never goes stale, and
444
+ * a function would not survive the cache the sidebar is painted from.
445
+ */
446
+ liveLabel?: LiveLabel;
404
447
  title?: string;
405
448
  detailFor?: (ctx: DetailContext) => React.ReactNode;
406
449
  /** A two-way repo split, and what to call the side on screen. See OriginSplit. */
@@ -453,4 +496,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
453
496
  */
454
497
  declare const parsePatterns: (value: string) => string[];
455
498
 
456
- export { type Action, ActionMenu, App, COLS, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxExtension, type JiraTransition, 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, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, toCiStatusState, truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
499
+ export { type Action, ActionMenu, App, COLS, CiStatusLine, type CiStatusState, 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 };
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, useApp } from 'ink';
3
- import { colors, ScrollView, TextInput, LoadingScreen, StatusMessage, FooterHints, useListCursor, Tabs, pillWidth, Pill } from '@kud/ink-ui';
3
+ import { colors, ScrollView, TextInput, LoadingScreen, ProgressBar, StatusMessage, FooterHints, useListCursor, Tabs, 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';
@@ -585,73 +585,115 @@ var HealthPanel = ({
585
585
  ] }) }) : note ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.success, children: " " + note }) }) : null
586
586
  ] });
587
587
  };
588
- var ACCENT = "#FF8700";
589
- var SIDEBAR_COLS = 52;
588
+ var SIDEBAR_COLS = 64;
589
+ var MIN_SIDEBAR_COLS = 52;
590
+ var railWidth = (cols) => Math.max(MIN_SIDEBAR_COLS, Math.min(SIDEBAR_COLS, Math.round(cols / 3)));
590
591
  var RULE = 1;
591
592
  var PAD = 2;
592
- var CONTENT = SIDEBAR_COLS - RULE - PAD;
593
- var LABEL_INDENT = 2;
594
- var RULE_COLOR = "gray";
595
- var counts = (row) => [
596
- row.done !== void 0 && row.total !== void 0 ? `${row.done}/${row.total}` : "",
597
- row.live !== void 0 ? `${row.live} live` : ""
598
- ].filter(Boolean).join(" \xB7 ");
599
- var truncate = (text, max) => [...text].length <= max ? text : [...text].slice(0, Math.max(0, max - 1)).join("") + "\u2026";
593
+ var contentOf = (width) => width - RULE - PAD;
594
+ var MARKS = 4;
595
+ var FACTS_INDENT = 6;
596
+ var KEY_COLS = 10;
597
+ var BAR_COLS = 10;
598
+ var FRACTION_COLS = 5;
599
+ var GUTTER = " ";
600
+ var FACTS_FIXED = FACTS_INDENT + KEY_COLS + BAR_COLS + FRACTION_COLS + GUTTER.length * 3;
601
+ var defaultLiveLabel = (live) => live === 0 ? "nothing live" : `${live} live`;
602
+ var fractionOf = (row) => row.done !== void 0 && row.total !== void 0 ? `${row.done}/${row.total}` : "";
603
+ var counts = (row, liveLabel = defaultLiveLabel) => [fractionOf(row), row.live !== void 0 ? liveLabel(row.live) : ""].filter(Boolean).join(" \xB7 ");
604
+ var DANGLING = /[\s:;,.\-—–(\[{]+$/u;
605
+ var truncateWords = (text, max) => {
606
+ const chars = [...text];
607
+ if (chars.length <= max) return text;
608
+ const room = Math.max(0, max - 1);
609
+ const hard = chars.slice(0, room).join("");
610
+ const boundary = chars[room] === " " ? room : hard.lastIndexOf(" ");
611
+ const soft = boundary >= Math.floor(room * 0.6) ? hard.slice(0, boundary) : hard;
612
+ return soft.replace(DANGLING, "") + "\u2026";
613
+ };
600
614
  var ROW_LINES = 3;
601
615
  var HEADING_LINES = 2;
602
616
  var railCapacity = (height, rows) => {
603
617
  const fits = Math.max(0, Math.floor((height - HEADING_LINES) / ROW_LINES));
604
618
  return fits >= rows ? rows : Math.max(0, fits - 1);
605
619
  };
620
+ var percentOf = (done, total) => total === 0 ? 0 : Math.round(done / total * 100);
621
+ var Progress = ({ row }) => {
622
+ const fraction = fractionOf(row);
623
+ const bar = row.done !== void 0 && row.total !== void 0 && row.total > 0 ? /* @__PURE__ */ jsx(
624
+ ProgressBar,
625
+ {
626
+ value: percentOf(row.done, row.total),
627
+ width: BAR_COLS,
628
+ color: colors.secondary
629
+ }
630
+ ) : /* @__PURE__ */ jsx(Text, { children: " ".repeat(BAR_COLS) });
631
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
632
+ bar,
633
+ /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: GUTTER + fraction.padStart(FRACTION_COLS) })
634
+ ] });
635
+ };
606
636
  var SidePanel = ({
607
637
  sidebar,
638
+ liveLabel = defaultLiveLabel,
639
+ width = SIDEBAR_COLS,
608
640
  height,
609
641
  focused = false,
610
642
  cursor = 0
611
643
  }) => {
644
+ const content = contentOf(width);
645
+ const labelCols = content - MARKS;
646
+ const liveCols = content - FACTS_FIXED;
612
647
  const capacity = height === void 0 ? sidebar.rows.length : railCapacity(height, sidebar.rows.length);
613
648
  const start = focused && cursor >= capacity ? Math.min(cursor - capacity + 1, sidebar.rows.length - capacity) : 0;
614
649
  const shown = sidebar.rows.slice(start, start + capacity);
615
650
  const hidden = sidebar.rows.length - shown.length;
651
+ const focus = focused ? " \u25CF focus" : "";
652
+ const rule = "\u254C".repeat(
653
+ Math.max(0, content - [...sidebar.title].length - focus.length - 1)
654
+ );
616
655
  return (
617
656
  // A rule down the left rather than a full box: the rail's other three edges
618
657
  // already have the frame's border a column or two away, and a second
619
658
  // rectangle inside the first reads as a nested panel — something you could
620
659
  // focus and act on, which this cannot be. One line is the whole claim: what
621
- // is left of it is the list, what is right of it is not.
660
+ // is left of it is the list, what is right of it is not. In the frame's own
661
+ // colour and not dimmed, so the two rules read as one hierarchy.
622
662
  /* @__PURE__ */ jsxs(
623
663
  Box,
624
664
  {
625
665
  flexDirection: "column",
626
- width: SIDEBAR_COLS,
666
+ width,
627
667
  flexShrink: 0,
628
668
  height,
629
669
  borderStyle: "single",
630
- borderColor: RULE_COLOR,
631
- borderDimColor: true,
670
+ borderColor: colors.muted,
632
671
  borderTop: false,
633
672
  borderRight: false,
634
673
  borderBottom: false,
635
674
  paddingLeft: PAD,
636
675
  children: [
637
676
  /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
638
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: "\xBB " }),
639
677
  /* @__PURE__ */ jsx(Text, { bold: true, children: sidebar.title }),
640
- focused ? /* @__PURE__ */ jsx(Text, { color: ACCENT, children: " \u25CF focus" }) : null
678
+ focused ? /* @__PURE__ */ jsx(Text, { color: colors.accent, children: focus }) : null,
679
+ /* @__PURE__ */ jsx(Text, { color: colors.info, dimColor: true, children: ` ${rule}` })
641
680
  ] }),
642
- sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "nothing open" }) }) : shown.map((row, i) => {
681
+ sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: MARKS, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: "nothing open" }) }) : shown.map((row, i) => {
643
682
  const active = focused && start + i === cursor;
644
683
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
645
684
  /* @__PURE__ */ jsxs(Box, { children: [
646
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
647
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
648
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: active, children: row.key }),
649
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${counts(row)}` })
685
+ /* @__PURE__ */ jsx(Text, { color: colors.info, children: active ? "\u276F " : " " }),
686
+ /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
687
+ /* @__PURE__ */ jsx(Text, { bold: active, children: truncateWords(row.label, labelCols) })
650
688
  ] }),
651
- /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: !active, children: truncate(row.label, CONTENT - LABEL_INDENT - 2) }) })
689
+ /* @__PURE__ */ jsxs(Box, { paddingLeft: FACTS_INDENT, children: [
690
+ /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: row.key.padEnd(KEY_COLS) + GUTTER }),
691
+ /* @__PURE__ */ jsx(Progress, { row }),
692
+ /* @__PURE__ */ jsx(Text, { children: GUTTER + (row.live !== void 0 ? liveLabel(row.live).slice(0, liveCols) : "") })
693
+ ] })
652
694
  ] }, row.key);
653
695
  }),
654
- hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: `+${hidden} more` }) }) : null
696
+ hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: MARKS, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: `+${hidden} more` }) }) : null
655
697
  ]
656
698
  }
657
699
  )
@@ -720,7 +762,8 @@ var readCache = (key) => {
720
762
  sections: raw.sections,
721
763
  login: raw.login ?? "",
722
764
  at: raw.at ?? 0,
723
- budget: raw.budget
765
+ budget: raw.budget,
766
+ ...Array.isArray(raw.sidebar?.rows) ? { sidebar: raw.sidebar } : {}
724
767
  };
725
768
  } catch {
726
769
  return null;
@@ -1135,7 +1178,7 @@ var withHeaders = (items, idx) => {
1135
1178
  start--;
1136
1179
  return start;
1137
1180
  };
1138
- var truncate2 = (str, max) => {
1181
+ var truncate = (str, max) => {
1139
1182
  if (str.length <= max) return str;
1140
1183
  const half = Math.floor((max - 1) / 2);
1141
1184
  return `${str.slice(0, half)}\u2026${str.slice(-half)}`;
@@ -1723,18 +1766,18 @@ var InboxHeader = ({
1723
1766
  const userSeg = loading || quiet ? "" : `@${login} `;
1724
1767
  const workLabel = scopeLabel ? ` ${scopeLabel} ` : "";
1725
1768
  const budgetSeg = budgetLabel ? budgetLabel + " " : "";
1726
- const [statusText, statusColor] = hasPending ? [`\u25CF ${pendingSummary || "new"} \xB7 r apply`, "#FF8700"] : refreshing ? ["\u21BB refreshing\u2026", "cyan"] : appliedSummary ? [`\u25C9 ${appliedSummary}`, "#FF8700"] : fetchedAt ? [`updated ${agoText(fetchedAt)}`, void 0] : ["", void 0];
1769
+ const [statusText, statusColor] = hasPending ? [`\u25CF ${pendingSummary || "new"} \xB7 r apply`, colors.accent] : refreshing ? ["\u21BB refreshing\u2026", "cyan"] : appliedSummary ? [`\u25C9 ${appliedSummary}`, colors.accent] : fetchedAt ? [`updated ${agoText(fetchedAt)}`, void 0] : ["", void 0];
1727
1770
  const statusSeg = statusText ? statusText + " " : "";
1728
1771
  const fill = Math.max(
1729
1772
  4,
1730
1773
  COLS - brand.length - countSeg.length - userSeg.length - workLabel.length - budgetSeg.length - statusSeg.length
1731
1774
  );
1732
1775
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1733
- /* @__PURE__ */ jsx(Text3, { color: "#FF8700", bold: true, children: brand }),
1776
+ /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: true, children: brand }),
1734
1777
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: countSeg }),
1735
1778
  userSeg ? /* @__PURE__ */ jsx(Text3, { children: userSeg }) : null,
1736
1779
  scopeLabel ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: workLabel }) : null,
1737
- budgetLabel ? /* @__PURE__ */ jsx(Text3, { bold: true, color: budgetCritical ? "#FF5F5F" : "#FF8700", children: budgetSeg }) : null,
1780
+ budgetLabel ? /* @__PURE__ */ jsx(Text3, { bold: true, color: budgetCritical ? "#FF5F5F" : colors.accent, children: budgetSeg }) : null,
1738
1781
  statusText ? /* @__PURE__ */ jsx(
1739
1782
  Text3,
1740
1783
  {
@@ -1783,7 +1826,7 @@ var CiStatusLine = ({
1783
1826
  /* @__PURE__ */ jsx(Text3, { color, bold: true, children: "\u25CF " }),
1784
1827
  /* @__PURE__ */ jsx(Text3, { bold: true, children: status.job }),
1785
1828
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + label }),
1786
- /* @__PURE__ */ jsx(Text3, { color: "#FF8700", children: " #" + status.buildNumber }),
1829
+ /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " #" + status.buildNumber }),
1787
1830
  status.age ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + status.age }) : null
1788
1831
  ] });
1789
1832
  };
@@ -1825,13 +1868,13 @@ var TRANSIT_IN_FRAMES = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
1825
1868
  var TRANSIT_COLOUR = {
1826
1869
  in: "#3FB950",
1827
1870
  out: "#8B949E",
1828
- changed: "#FF8700",
1871
+ changed: colors.accent,
1829
1872
  // A move is neither an arrival nor a departure, so it takes the colour of the
1830
1873
  // third thing that can happen to a row you already had: it changed. Both ends
1831
1874
  // of one move wear the same colour and the same word, which is what lets you
1832
1875
  // recognise the row you just watched leave when you land on the tab it went to.
1833
- "moved-in": "#FF8700",
1834
- "moved-out": "#FF8700"
1876
+ "moved-in": colors.accent,
1877
+ "moved-out": colors.accent
1835
1878
  };
1836
1879
  var TRANSIT_LABEL = {
1837
1880
  out: "gone"};
@@ -1863,7 +1906,7 @@ var ItemRow = ({
1863
1906
  return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false, active });
1864
1907
  if (item.kind === "subgroup-header")
1865
1908
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1866
- /* @__PURE__ */ jsx(Text3, { color: "#FF8700", bold: true, children: " \xBB " }),
1909
+ /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: true, children: " \xBB " }),
1867
1910
  /* @__PURE__ */ jsx(Text3, { bold: true, children: item.label })
1868
1911
  ] });
1869
1912
  if (item.kind === "show-more")
@@ -1892,14 +1935,14 @@ var ItemRow = ({
1892
1935
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
1893
1936
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: parent ? "\u252C " : " " }),
1894
1937
  /* @__PURE__ */ jsx(Text3, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
1895
- /* @__PURE__ */ jsx(Text3, { color: "#FF8700", bold: active, children: item.key + " " }),
1938
+ /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: active, children: item.key + " " }),
1896
1939
  /* @__PURE__ */ jsx(
1897
1940
  Text3,
1898
1941
  {
1899
1942
  bold: active || !!transient && isArrival(transient),
1900
1943
  dimColor: !!transient && isDeparture(transient),
1901
1944
  strikethrough: transient === "out",
1902
- children: truncate2(item.summary, titleMax2)
1945
+ children: truncate(item.summary, titleMax2)
1903
1946
  }
1904
1947
  ),
1905
1948
  item.pill && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1925,7 +1968,7 @@ var ItemRow = ({
1925
1968
  const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : transient && isDeparture(transient) ? TRANSIT_OUT_FRAMES[rampAt(TRANSIT_OUT_FRAMES.length)] : transient && isArrival(transient) ? TRANSIT_IN_FRAMES[rampAt(TRANSIT_IN_FRAMES.length)] : healthIcon;
1926
1969
  const color = merged ? MERGED_COLOUR : transient ? TRANSIT_COLOUR[transient] : healthColor2;
1927
1970
  const spokeLast = !!login && !!item.lastActor && item.lastActor === login;
1928
- const [turnIcon, turnColor] = item.pinned ? [PIN_MARK, "#FF8700"] : !login || !item.lastActor ? [" ", "white"] : spokeLast ? ["\u2192", colors.secondary] : ["\u2190", "#FF8700"];
1971
+ const [turnIcon, turnColor] = item.pinned ? [PIN_MARK, colors.accent] : !login || !item.lastActor ? [" ", "white"] : spokeLast ? ["\u2192", colors.secondary] : ["\u2190", colors.accent];
1929
1972
  const numStr = `#${item.number}`.padEnd(7);
1930
1973
  const showAuthor = !!item.author && item.author !== login;
1931
1974
  const sizeLabel = sizeOf(item);
@@ -1958,7 +2001,7 @@ var ItemRow = ({
1958
2001
  if (next.length <= LABEL_CELL_MAX) fitted.push(name);
1959
2002
  }
1960
2003
  if (fitted.length === 0) {
1961
- return `\uF02B ${truncate2(shown[0], LABEL_CELL_MAX)}`;
2004
+ return `\uF02B ${truncate(shown[0], LABEL_CELL_MAX)}`;
1962
2005
  }
1963
2006
  return `\uF02B ${fitted.join(", ")}`;
1964
2007
  };
@@ -1994,14 +2037,14 @@ var ItemRow = ({
1994
2037
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
1995
2038
  /* @__PURE__ */ jsx(Text3, { color, bold: true, children: icon + " " }),
1996
2039
  /* @__PURE__ */ jsx(Text3, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
1997
- /* @__PURE__ */ jsx(Text3, { color: "#FF8700", children: numStr }),
2040
+ /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: numStr }),
1998
2041
  /* @__PURE__ */ jsx(
1999
2042
  Text3,
2000
2043
  {
2001
2044
  bold: active || !!transient && isArrival(transient),
2002
2045
  dimColor: !!transient && isDeparture(transient),
2003
2046
  strikethrough: transient === "out",
2004
- children: truncate2(item.title, titleMax) + " "
2047
+ children: truncate(item.title, titleMax) + " "
2005
2048
  }
2006
2049
  ),
2007
2050
  labelLabel ? /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: labelLabel + " " }) : null,
@@ -2012,7 +2055,7 @@ var ItemRow = ({
2012
2055
  " ",
2013
2056
  /* @__PURE__ */ jsx(Text3, { color: colors.error, children: sizeParts.removed })
2014
2057
  ] }) : null,
2015
- unresolvedLabel && !givingUp.threads ? /* @__PURE__ */ jsx(Text3, { bold: !spokeLast, color: spokeLast ? colors.secondary : "#FF8700", children: " " + unresolvedLabel }) : null,
2058
+ unresolvedLabel && !givingUp.threads ? /* @__PURE__ */ jsx(Text3, { bold: !spokeLast, color: spokeLast ? colors.secondary : colors.accent, children: " " + unresolvedLabel }) : null,
2016
2059
  showAuthor && !givingUp.author ? /* @__PURE__ */ jsx(Text3, { dimColor: true, italic: true, children: " by " + item.author }) : null,
2017
2060
  ageLabel && !givingUp.age ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + ageLabel }) : null,
2018
2061
  mergedLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -2082,7 +2125,7 @@ var ActionMenu = ({
2082
2125
  );
2083
2126
  };
2084
2127
  var TURN_LEGEND = [
2085
- ["\u2190", "#FF8700", "They spoke last \xB7 your turn"],
2128
+ ["\u2190", colors.accent, "They spoke last \xB7 your turn"],
2086
2129
  ["\u2192", colors.secondary, "You spoke last \xB7 waiting on them"]
2087
2130
  ];
2088
2131
  var CELL_GUTTER = 2;
@@ -2187,7 +2230,7 @@ var HelpModal = ({
2187
2230
  })),
2188
2231
  {
2189
2232
  cell: "\uF086",
2190
- color: "#FF8700",
2233
+ color: colors.accent,
2191
2234
  bold: true,
2192
2235
  label: "Open-thread count"
2193
2236
  }
@@ -2200,7 +2243,7 @@ var HelpModal = ({
2200
2243
  title: "Tabs",
2201
2244
  rows: tabHelp.map(([tab, meaning]) => ({
2202
2245
  cell: tab,
2203
- color: "#FF8700",
2246
+ color: colors.accent,
2204
2247
  label: meaning
2205
2248
  }))
2206
2249
  }
@@ -2275,7 +2318,7 @@ var ExplainModal = ({ item, login }) => /* @__PURE__ */ jsxs(
2275
2318
  paddingX: 1,
2276
2319
  width: Math.min(COLS, 78),
2277
2320
  children: [
2278
- /* @__PURE__ */ jsx(Text3, { color: "#FF8700", bold: true, children: `#${item.number} \xB7 ${item.repo}` }),
2321
+ /* @__PURE__ */ jsx(Text3, { color: colors.accent, bold: true, children: `#${item.number} \xB7 ${item.repo}` }),
2279
2322
  /* @__PURE__ */ jsx(Text3, { children: item.title }),
2280
2323
  explainItem(item, login).map((section) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
2281
2324
  /* @__PURE__ */ jsx(Text3, { bold: true, dimColor: true, children: section.heading }),
@@ -2364,7 +2407,8 @@ var BrowseScreen = ({
2364
2407
  onLeave,
2365
2408
  transients,
2366
2409
  onTabChange,
2367
- sidebar
2410
+ sidebar,
2411
+ liveLabel
2368
2412
  }) => {
2369
2413
  const { rows } = useWindowSize();
2370
2414
  const { exit } = useApp();
@@ -2488,7 +2532,8 @@ var BrowseScreen = ({
2488
2532
  const railRows = sidebar?.rows ?? [];
2489
2533
  const railActive = showRail && railFocus && railRows.length > 0;
2490
2534
  const railAt = Math.min(railCursor, Math.max(0, railRows.length - 1));
2491
- const listCols = showRail ? COLS - SIDEBAR_COLS : COLS;
2535
+ const railCols = railWidth(COLS);
2536
+ const listCols = showRail ? COLS - railCols : COLS;
2492
2537
  const [sparkFrame, setSparkFrame] = useState(0);
2493
2538
  const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || // Any MARKED TAB, not just rows on screen. The scope used to be this tab's
2494
2539
  // own items, which was right while the animation lived on the rows and is
@@ -2577,11 +2622,11 @@ var BrowseScreen = ({
2577
2622
  if (key.ctrl || key.meta) return;
2578
2623
  if (help) {
2579
2624
  if (helpRange.max > 0) {
2580
- const step = (n) => setHelpScroll((s) => Math.min(helpRange.max, Math.max(0, s + n)));
2581
- if (key.upArrow) return step(-1);
2582
- if (key.downArrow) return step(1);
2583
- if (key.pageUp) return step(-helpRange.page);
2584
- if (key.pageDown) return step(helpRange.page);
2625
+ const step2 = (n) => setHelpScroll((s) => Math.min(helpRange.max, Math.max(0, s + n)));
2626
+ if (key.upArrow) return step2(-1);
2627
+ if (key.downArrow) return step2(1);
2628
+ if (key.pageUp) return step2(-helpRange.page);
2629
+ if (key.pageDown) return step2(helpRange.page);
2585
2630
  }
2586
2631
  setHelp(false);
2587
2632
  return;
@@ -2691,12 +2736,10 @@ var BrowseScreen = ({
2691
2736
  setCursors((p) => ({ ...p, [activeId]: next }));
2692
2737
  setViewStarts((p) => ({ ...p, [activeId]: newVs }));
2693
2738
  }
2694
- if (key.leftArrow) setTabIdx((i) => Math.max(0, i - 1));
2695
- if (key.rightArrow)
2696
- setTabIdx((i) => Math.min(localSections.length - 1, i + 1));
2697
- if (key.tab)
2739
+ const step = key.leftArrow || key.tab && key.shift ? -1 : key.rightArrow || key.tab ? 1 : 0;
2740
+ if (step !== 0)
2698
2741
  setTabIdx(
2699
- (i) => (i + (key.shift ? -1 : 1) + localSections.length) % localSections.length
2742
+ (i) => (i + step + localSections.length) % localSections.length
2700
2743
  );
2701
2744
  if (input === "q") exit();
2702
2745
  if (input === "r") {
@@ -3011,7 +3054,7 @@ var BrowseScreen = ({
3011
3054
  ),
3012
3055
  ciStatusState ? /* @__PURE__ */ jsx(CiStatusLine, { state: ciStatusState, job: ciJob }) : null,
3013
3056
  skippedForBudget ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3014
- /* @__PURE__ */ jsx(Text3, { bold: true, color: "#FF8700", children: " \u26A1 auto-refresh paused to save API budget" }),
3057
+ /* @__PURE__ */ jsx(Text3, { bold: true, color: colors.accent, children: " \u26A1 auto-refresh paused to save API budget" }),
3015
3058
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " r refreshes anyway" })
3016
3059
  ] }) : null,
3017
3060
  /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
@@ -3043,7 +3086,7 @@ var BrowseScreen = ({
3043
3086
  s.id,
3044
3087
  pulseSettled ? PULSE_SETTLED_FRAME : sparkFrame
3045
3088
  ),
3046
- markerColor: "#FF8700"
3089
+ markerColor: colors.accent
3047
3090
  }))
3048
3091
  }
3049
3092
  ) }),
@@ -3053,7 +3096,7 @@ var BrowseScreen = ({
3053
3096
  searchInput ? /* @__PURE__ */ jsx(Text3, { color: "cyan", children: "\u258F" }) : null,
3054
3097
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}` })
3055
3098
  ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3056
- /* @__PURE__ */ jsx(Text3, { color: "#FF8700", children: " \u25C9 " }),
3099
+ /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " \u25C9 " }),
3057
3100
  /* @__PURE__ */ jsx(Text3, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
3058
3101
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " f edit \xB7 esc clear" })
3059
3102
  ] }) : null,
@@ -3117,6 +3160,8 @@ var BrowseScreen = ({
3117
3160
  SidePanel,
3118
3161
  {
3119
3162
  sidebar,
3163
+ liveLabel,
3164
+ width: railCols,
3120
3165
  height: listHeight,
3121
3166
  focused: railActive,
3122
3167
  cursor: railAt
@@ -3179,7 +3224,8 @@ var App = ({
3179
3224
  watchJitterMs = 0,
3180
3225
  extensions,
3181
3226
  tabHelp,
3182
- emptyHint
3227
+ emptyHint,
3228
+ liveLabel
3183
3229
  }) => {
3184
3230
  const { rows } = useWindowSize();
3185
3231
  const [state, setState] = useState({ phase: "loading" });
@@ -3356,6 +3402,7 @@ var App = ({
3356
3402
  displayedKey.current = signatureOf(cached.sections);
3357
3403
  displayedSections.current = cached.sections;
3358
3404
  setFetchedAt(cached.at);
3405
+ if (cached.sidebar) setSidebar(cached.sidebar);
3359
3406
  setState({
3360
3407
  phase: "browse",
3361
3408
  sections: cached.sections,
@@ -3400,6 +3447,7 @@ var App = ({
3400
3447
  const shared = cacheKey ? readCache(cacheKey) : null;
3401
3448
  if (shared && shared.at > firedAt) {
3402
3449
  receive({ sections: shared.sections, login: shared.login });
3450
+ if (shared.sidebar) setSidebar(shared.sidebar);
3403
3451
  setFetchedAt(shared.at);
3404
3452
  return;
3405
3453
  }
@@ -3544,6 +3592,7 @@ var App = ({
3544
3592
  transients,
3545
3593
  onTabChange: setVisibleTab,
3546
3594
  sidebar,
3595
+ liveLabel,
3547
3596
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
3548
3597
  ciJob,
3549
3598
  tabHelp,
@@ -3596,4 +3645,4 @@ var matchesFilter = (repo, filter) => {
3596
3645
  };
3597
3646
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
3598
3647
 
3599
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, 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, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, toCiStatusState, truncate2 as truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
3648
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.52.1",
3
+ "version": "0.54.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",
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@kud/gh": "0.16.0",
52
52
  "@kud/gh-workflow": "0.11.0",
53
- "@kud/ink-ui": "0.28.2",
53
+ "@kud/ink-ui": "0.29.0",
54
54
  "zx": "8.8.5"
55
55
  },
56
56
  "devDependencies": {