@kud/gh-ink 0.55.1 → 0.57.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
@@ -65,6 +65,13 @@ type SidebarRow = {
65
65
  * an error about a host decision the reader cannot change.
66
66
  */
67
67
  url?: string;
68
+ /**
69
+ * A one-cell mark drawn before the key on the facts line — a priority
70
+ * arrow — and its colour: the same pair `TaskRow` takes, so a host fills
71
+ * both from one source. Absent draws the blank the cell keeps anyway.
72
+ */
73
+ marker?: string;
74
+ markerColor?: string;
68
75
  };
69
76
  /**
70
77
  * Plain data, deliberately: a host caches its last fetch to disk and paints it
@@ -241,6 +248,22 @@ type DetailContext = {
241
248
  * once. Optional so a host that cannot merge need not implement it.
242
249
  */
243
250
  onMerged?: (item: GHItem) => void;
251
+ /**
252
+ * Where a drill publishes its PEEL — close the topmost layer it owns and say
253
+ * whether there was one. `esc` and `backspace` are bound once at the app root
254
+ * and routed here; a drill view binds neither itself, per `@kud/ink-ui`'s
255
+ * contract that a view exported from a `*-ink` package takes `onBack` and
256
+ * leaves the app's keys to the app.
257
+ *
258
+ * Returning `false` means "no layer of mine was open", at which point the root
259
+ * closes the drill itself.
260
+ */
261
+ registerPeel?: (peel: (() => boolean) | null) => void;
262
+ /**
263
+ * True while a text field inside the drill owns the keyboard — a reply box —
264
+ * so the root stands `q`, `esc` and `backspace` down and the field keeps them.
265
+ */
266
+ onTyping?: (typing: boolean) => void;
244
267
  };
245
268
  type Action = {
246
269
  label: string;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React2, { createContext, useState, useRef, useEffect, useMemo, useContext } from 'react';
2
- import { useWindowSize, useInput, Text, Box, useApp } from 'ink';
3
- import { colors, ScrollView, TextInput, LoadingScreen, ProgressBar, StatusMessage, FooterHints, useListCursor, Tabs, pillWidth, Pill } from '@kud/ink-ui';
2
+ import { useWindowSize, useInput, Text, Box } from 'ink';
3
+ import { colors, ScrollView, TextInput, useAppKeys, 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';
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
8
8
  import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, watch, rmSync } from 'fs';
9
9
  import { join, dirname, basename } from 'path';
10
10
  import { homedir } from 'os';
11
- import { depthOf, checkoutDirs, profileOf, inboxConfig, relativeTime, filterBySearch, filterByRepos, filterByOrigin, PIN_MARK, sizeOf, sizePartsOf, impliedLabels, labelPriority } from '@kud/gh-workflow';
11
+ import { PIN_MARK, depthOf, checkoutDirs, profileOf, inboxConfig, relativeTime, filterBySearch, filterByRepos, filterByOrigin, sizeOf, sizePartsOf, impliedLabels, labelPriority } from '@kud/gh-workflow';
12
12
  export { PIN_MARK, bandOf, checkoutDirs, configureInbox, filterByOrigin, filterByRepos, filterBySearch, impliedLabels, inboxConfig, insertRepoHeaders, labelPriority, layoutGHItems, profileOf, relativeTime, repoPriority, resetInboxConfig, sortByRecency, sortItems, whoseMove } from '@kud/gh-workflow';
13
13
 
14
14
  // src/components/comments-panel.tsx
@@ -687,7 +687,8 @@ var SidePanel = ({
687
687
  /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
688
688
  /* @__PURE__ */ jsx(Text, { bold: active, children: truncateWords(row.label, labelCols) })
689
689
  ] }),
690
- /* @__PURE__ */ jsxs(Box, { paddingLeft: FACTS_INDENT, children: [
690
+ /* @__PURE__ */ jsxs(Box, { children: [
691
+ /* @__PURE__ */ jsx(Text, { color: row.markerColor, children: " ".repeat(FACTS_INDENT - 2) + (row.marker ?? " ") + " " }),
691
692
  /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: row.key.padEnd(KEY_COLS) + GUTTER }),
692
693
  /* @__PURE__ */ jsx(Progress, { row }),
693
694
  /* @__PURE__ */ jsx(Text, { children: GUTTER + (row.live !== void 0 ? liveLabel(row.live).slice(0, liveCols) : "") })
@@ -1049,7 +1050,7 @@ var checksSentence = (d) => {
1049
1050
  var turnSentences = (item, login) => {
1050
1051
  if (item.pinned)
1051
1052
  return [
1052
- "You pinned this (!). It stays under Your move until you unpin it, whatever else the row says."
1053
+ "You pinned this (+). It stays under Your move until you unpin it, whatever else the row says."
1053
1054
  ];
1054
1055
  if (!item.lastActor) return ["Nothing has been said on it yet."];
1055
1056
  const d = item.detail;
@@ -1059,9 +1060,9 @@ var turnSentences = (item, login) => {
1059
1060
  const them = item.author && item.author !== login ? item.author : null;
1060
1061
  if (!them)
1061
1062
  return [
1062
- `You spoke last (\u2192), ${when}. It is waiting on a reviewer, not on you.`
1063
+ `You spoke last, ${when}. It is waiting on a reviewer, not on you.`
1063
1064
  ];
1064
- const lines = [`You spoke last (\u2192), ${when}. The ball is with ${them}.`];
1065
+ const lines = [`You spoke last, ${when}. The ball is with ${them}.`];
1065
1066
  if (d?.lastCommitAt && d.lastEventAt && d.lastCommitAt < d.lastEventAt)
1066
1067
  lines.push(
1067
1068
  `Nothing has been pushed since ${relativeTime(
@@ -1848,7 +1849,7 @@ var InboxHeader = ({
1848
1849
  children: statusSeg
1849
1850
  }
1850
1851
  ) : null,
1851
- /* @__PURE__ */ jsx(Text3, { color: "cyan", dimColor: true, children: "\u254C".repeat(fill) })
1852
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, dimColor: true, children: "\u254C".repeat(fill) })
1852
1853
  ] });
1853
1854
  };
1854
1855
  var toCiStatusState = (status) => status ? { kind: "ready", status } : { kind: "error" };
@@ -1876,7 +1877,7 @@ var CiStatusLine = ({
1876
1877
  ] });
1877
1878
  if (state.kind === "error")
1878
1879
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
1879
- /* @__PURE__ */ jsx(Text3, { color: "red", bold: true, children: " \u2717 " }),
1880
+ /* @__PURE__ */ jsx(Text3, { color: colors.error, bold: true, children: " \u2717 " }),
1880
1881
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: `${name} no build / not configured` })
1881
1882
  ] });
1882
1883
  const { status } = state;
@@ -1901,9 +1902,17 @@ var RepoHeaderRow = ({
1901
1902
  const fill = Math.max(4, FENCE_COLS - label.length);
1902
1903
  const rule = "\u2500".repeat(fill);
1903
1904
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1904
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: active ? "\u276F " : " " }),
1905
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
1905
1906
  /* @__PURE__ */ jsx(Text3, { dimColor: !active, children: "\u2500\u2500 " }),
1906
- /* @__PURE__ */ jsx(Text3, { dimColor: !active, bold: active, children: repo }),
1907
+ /* @__PURE__ */ jsx(
1908
+ Text3,
1909
+ {
1910
+ dimColor: false,
1911
+ color: active ? void 0 : colors.secondary,
1912
+ bold: active,
1913
+ children: repo
1914
+ }
1915
+ ),
1907
1916
  /* @__PURE__ */ jsx(Text3, { dimColor: !active, children: " " + rule })
1908
1917
  ] });
1909
1918
  };
@@ -1957,7 +1966,8 @@ var ItemRow = ({
1957
1966
  parent,
1958
1967
  sparkFrame = 0,
1959
1968
  cols = COLS,
1960
- markerCols = 0
1969
+ markerCols = 0,
1970
+ uniformLabels = []
1961
1971
  }) => {
1962
1972
  const backdropped = useBackdropped();
1963
1973
  const transient = leaving ? "out" : refreshMark;
@@ -1973,14 +1983,14 @@ var ItemRow = ({
1973
1983
  ] });
1974
1984
  if (item.kind === "show-more")
1975
1985
  return /* @__PURE__ */ jsxs(Box, { children: [
1976
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: active ? "\u276F " : " " }),
1986
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
1977
1987
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
1978
1988
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: active ? "\u21B5 " : " " }),
1979
1989
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: `+${item.hidden.length} more` })
1980
1990
  ] });
1981
1991
  if (item.kind === "show-less")
1982
1992
  return /* @__PURE__ */ jsxs(Box, { children: [
1983
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: active ? "\u276F " : " " }),
1993
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
1984
1994
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
1985
1995
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: active ? "\u21B5 " : " " }),
1986
1996
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "show less" })
@@ -1993,7 +2003,7 @@ var ItemRow = ({
1993
2003
  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
1994
2004
  );
1995
2005
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1996
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: active ? "\u276F " : " " }),
2006
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
1997
2007
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
1998
2008
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: parent ? "\u252C " : " " }),
1999
2009
  /* @__PURE__ */ jsx(Text3, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
@@ -2031,17 +2041,18 @@ var ItemRow = ({
2031
2041
  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;
2032
2042
  const color = merged ? MERGED_COLOUR : transient ? TRANSIT_COLOUR[transient] : healthColor2;
2033
2043
  const spokeLast = !!login && !!item.lastActor && item.lastActor === login;
2034
- const [turnIcon, turnColor] = item.pinned ? [PIN_MARK, colors.accent] : !login || !item.lastActor ? [" ", "white"] : spokeLast ? ["\u2192", colors.secondary] : ["\u2190", colors.accent];
2044
+ const [turnIcon, turnColor] = item.pinned ? [PIN_MARK, colors.accent] : !login || !item.lastActor || spokeLast ? [" ", colors.muted] : ["\u2190", colors.accent];
2035
2045
  const numStr = `#${item.number}`.padEnd(7);
2036
2046
  const showAuthor = !!item.author && item.author !== login;
2037
2047
  const sizeLabel = sizeOf(item);
2038
2048
  const sizeParts = sizePartsOf(item);
2039
2049
  const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
2040
- const ageLabel = item.activityAge && item.activityAge !== item.age ? `${item.activityAge} \xB7 ${item.age}` : item.age;
2050
+ const agePair = item.activityAge && item.activityAge !== item.age ? { activity: item.activityAge, lifetime: item.age } : null;
2051
+ const ageLabel = agePair ? `${agePair.activity} (${agePair.lifetime})` : item.age;
2041
2052
  const mergedLabel = merged ? "merged" : "";
2042
2053
  const pillCaps = [mergedLabel, farewellLabel].filter(Boolean).length * 2;
2043
2054
  const repoLabel = depthOf(item) > 0 ? item.repo : "";
2044
- const implied = impliedLabels(item.repo);
2055
+ const implied = [...impliedLabels(item.repo), ...uniformLabels];
2045
2056
  const labelNames = (item.labels ?? []).filter((l) => !implied.includes(l)).sort((a, b) => labelPriority(a) - labelPriority(b) || a.localeCompare(b)).slice(0, 2);
2046
2057
  const givingUp = {
2047
2058
  author: false,
@@ -2096,7 +2107,7 @@ var ItemRow = ({
2096
2107
  const labelLabel = labelCell();
2097
2108
  const titleMax = Math.max(1, cols - widthOf2());
2098
2109
  return /* @__PURE__ */ jsxs(Box, { children: [
2099
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: active ? "\u276F " : " " }),
2110
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
2100
2111
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
2101
2112
  /* @__PURE__ */ jsx(Text3, { color, bold: true, children: icon + " " }),
2102
2113
  /* @__PURE__ */ jsx(Text3, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
@@ -2118,9 +2129,19 @@ var ItemRow = ({
2118
2129
  " ",
2119
2130
  /* @__PURE__ */ jsx(Text3, { color: colors.error, children: sizeParts.removed })
2120
2131
  ] }) : null,
2121
- unresolvedLabel && !givingUp.threads ? /* @__PURE__ */ jsx(Text3, { bold: !spokeLast, color: spokeLast ? colors.secondary : colors.accent, children: " " + unresolvedLabel }) : null,
2132
+ unresolvedLabel && !givingUp.threads ? /* @__PURE__ */ jsx(
2133
+ Text3,
2134
+ {
2135
+ bold: !spokeLast,
2136
+ color: spokeLast ? colors.secondary : colors.accent,
2137
+ children: " " + unresolvedLabel
2138
+ }
2139
+ ) : null,
2122
2140
  showAuthor && !givingUp.author ? /* @__PURE__ */ jsx(Text3, { dimColor: true, italic: true, children: " by " + item.author }) : null,
2123
- ageLabel && !givingUp.age ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + ageLabel }) : null,
2141
+ ageLabel && !givingUp.age ? agePair ? /* @__PURE__ */ jsxs(Fragment, { children: [
2142
+ /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: " " + agePair.activity }),
2143
+ /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` (${agePair.lifetime})` })
2144
+ ] }) : /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: " " + ageLabel }) : null,
2124
2145
  mergedLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2125
2146
  /* @__PURE__ */ jsx(Text3, { children: " " }),
2126
2147
  /* @__PURE__ */ jsx(Pill, { color: MERGED_COLOUR, children: mergedLabel })
@@ -2169,15 +2190,15 @@ var ActionMenu = ({
2169
2190
  {
2170
2191
  flexDirection: "column",
2171
2192
  borderStyle: "round",
2172
- borderColor: "cyan",
2193
+ borderColor: colors.info,
2173
2194
  backgroundColor: OVERLAY_BG,
2174
2195
  paddingX: 1,
2175
2196
  marginTop: 1,
2176
2197
  children: [
2177
- /* @__PURE__ */ jsx(Text3, { color: "cyan", bold: true, children: title }),
2198
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: title }),
2178
2199
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(32) }),
2179
2200
  actions.map((a, i) => /* @__PURE__ */ jsxs(Box, { children: [
2180
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: i === cursor ? "\u276F " : " " }),
2201
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: i === cursor ? "\u276F " : " " }),
2181
2202
  /* @__PURE__ */ jsx(Text3, { bold: i === cursor, children: a.label }),
2182
2203
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + a.hint })
2183
2204
  ] }, a.label)),
@@ -2188,8 +2209,8 @@ var ActionMenu = ({
2188
2209
  );
2189
2210
  };
2190
2211
  var TURN_LEGEND = [
2191
- ["\u2190", colors.accent, "They spoke last \xB7 your turn"],
2192
- ["\u2192", colors.secondary, "You spoke last \xB7 waiting on them"]
2212
+ ["\u2190", colors.accent, "They spoke last \xB7 reply owed"],
2213
+ [PIN_MARK, colors.accent, "Pinned \xB7 stays under Your move"]
2193
2214
  ];
2194
2215
  var CELL_GUTTER = 2;
2195
2216
  var COL_GAP = 3;
@@ -2346,13 +2367,13 @@ var HelpModal = ({
2346
2367
  {
2347
2368
  flexDirection: "column",
2348
2369
  borderStyle: "round",
2349
- borderColor: "cyan",
2370
+ borderColor: colors.info,
2350
2371
  backgroundColor: OVERLAY_BG,
2351
2372
  paddingX: 1,
2352
2373
  width: contentWidth + MODAL_CHROME_COLS,
2353
2374
  children: [
2354
2375
  /* @__PURE__ */ jsxs(Box, { children: [
2355
- /* @__PURE__ */ jsx(Text3, { color: "cyan", bold: true, children: "Legend" }),
2376
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: "Legend" }),
2356
2377
  scrollable ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${offset + 1}\u2013${offset + shown} of ${totalRows}` }) : null
2357
2378
  ] }),
2358
2379
  /* @__PURE__ */ jsx(Box, { marginTop: 1, width: contentWidth, children: groups.map((group, index) => /* @__PURE__ */ jsx(
@@ -2376,7 +2397,7 @@ var ExplainModal = ({ item, login }) => /* @__PURE__ */ jsxs(
2376
2397
  {
2377
2398
  flexDirection: "column",
2378
2399
  borderStyle: "round",
2379
- borderColor: "cyan",
2400
+ borderColor: colors.info,
2380
2401
  backgroundColor: OVERLAY_BG,
2381
2402
  paddingX: 1,
2382
2403
  width: Math.min(COLS, 78),
@@ -2408,19 +2429,19 @@ var RepoPicker = ({
2408
2429
  {
2409
2430
  flexDirection: "column",
2410
2431
  borderStyle: "round",
2411
- borderColor: "cyan",
2432
+ borderColor: colors.info,
2412
2433
  backgroundColor: OVERLAY_BG,
2413
2434
  paddingX: 1,
2414
2435
  minWidth: 42,
2415
2436
  children: [
2416
- /* @__PURE__ */ jsx(Text3, { color: "cyan", bold: true, children: `Filter by repo (${selected.size} on)` }),
2437
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: `Filter by repo (${selected.size} on)` }),
2417
2438
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(36) }),
2418
2439
  visible.map((repo, i) => {
2419
2440
  const idx = start + i;
2420
2441
  const on = selected.has(repo);
2421
2442
  const active = idx === cursor;
2422
2443
  return /* @__PURE__ */ jsxs(Box, { children: [
2423
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: active ? "\u276F " : " " }),
2444
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
2424
2445
  /* @__PURE__ */ jsx(Text3, { color: on ? "green" : void 0, children: on ? "\u25C9 " : "\u25CB " }),
2425
2446
  /* @__PURE__ */ jsx(Text3, { bold: active, children: repo })
2426
2447
  ] }, repo);
@@ -2453,6 +2474,8 @@ var BrowseScreen = ({
2453
2474
  fetchedAt,
2454
2475
  refreshError,
2455
2476
  hidden,
2477
+ peelRef,
2478
+ onTyping,
2456
2479
  onOpenPr,
2457
2480
  onOpenIssue,
2458
2481
  onOpenExt,
@@ -2474,7 +2497,6 @@ var BrowseScreen = ({
2474
2497
  liveLabel
2475
2498
  }) => {
2476
2499
  const { rows } = useWindowSize();
2477
- const { exit } = useApp();
2478
2500
  const applyWork = (secs) => origin ? filterByOrigin(secs, origin.show, origin.match, login) : secs;
2479
2501
  const initialSections = applyWork(sections);
2480
2502
  const [localSections, setLocalSections] = useState(initialSections);
@@ -2578,6 +2600,12 @@ var BrowseScreen = ({
2578
2600
  (w, i) => i.kind === "task" ? Math.max(w, [...i.marker ?? ""].length) : w,
2579
2601
  0
2580
2602
  );
2603
+ const labelBearing = section.items.filter(
2604
+ (i) => i.kind === "pr" || i.kind === "issue"
2605
+ );
2606
+ const uniformLabels = labelBearing.length > 1 ? (labelBearing[0]?.labels ?? []).filter(
2607
+ (name) => labelBearing.every((i) => (i.labels ?? []).includes(name))
2608
+ ) : [];
2581
2609
  const hasMore = viewStart + visibleCount < section.items.length;
2582
2610
  const activeItem = section.items[cursor];
2583
2611
  const showFlash = (msg) => {
@@ -2599,6 +2627,26 @@ var BrowseScreen = ({
2599
2627
  const railRows = sidebar?.rows ?? [];
2600
2628
  const railActive = showRail && railFocus && railRows.length > 0;
2601
2629
  const railAt = Math.min(railCursor, Math.max(0, railRows.length - 1));
2630
+ const peel = () => {
2631
+ if (help) return setHelp(false), true;
2632
+ if (explain) return setExplain(false), true;
2633
+ if (repoPicker) return setRepoPicker(false), true;
2634
+ if (railActive) return setRailFocus(false), true;
2635
+ if (menu.actions !== null) return menu.close(), true;
2636
+ if (search != null) return setSearch(null), true;
2637
+ if (repoFilter.size > 0) return setRepoFilter(/* @__PURE__ */ new Set()), true;
2638
+ return false;
2639
+ };
2640
+ useEffect(() => {
2641
+ if (!peelRef) return;
2642
+ peelRef.current = peel;
2643
+ return () => {
2644
+ peelRef.current = null;
2645
+ };
2646
+ });
2647
+ useEffect(() => {
2648
+ onTyping?.(searchInput);
2649
+ }, [searchInput, onTyping]);
2602
2650
  const railCols = railWidth(COLS);
2603
2651
  const listCols = showRail ? COLS - railCols : COLS;
2604
2652
  const [sparkFrame, setSparkFrame] = useState(0);
@@ -2708,7 +2756,7 @@ var BrowseScreen = ({
2708
2756
  return;
2709
2757
  }
2710
2758
  if (repoPicker) {
2711
- if (key.escape || key.return) return setRepoPicker(false);
2759
+ if (key.return) return setRepoPicker(false);
2712
2760
  if (input === "a") return setRepoFilter(/* @__PURE__ */ new Set());
2713
2761
  if (input === " ") {
2714
2762
  const repo = allRepos[repoCursor];
@@ -2764,30 +2812,30 @@ var BrowseScreen = ({
2764
2812
  setRailCursor((c) => Math.min(railRows.length - 1, c + 1));
2765
2813
  return;
2766
2814
  }
2767
- if (key.escape) {
2768
- setRailFocus(false);
2769
- return;
2770
- }
2771
2815
  if (key.return || input === "o") {
2772
2816
  const row = railRows[railAt];
2773
- if (row?.url) {
2774
- quietly`open ${row.url}`.catch(() => {
2817
+ if (!row) return;
2818
+ if (key.return && openDrillView({
2819
+ kind: "task",
2820
+ key: row.key,
2821
+ ticket: row.key,
2822
+ summary: row.label,
2823
+ url: row.url ?? "",
2824
+ status: "",
2825
+ age: "",
2826
+ indent: false
2827
+ }))
2828
+ return;
2829
+ if (row.url) {
2830
+ quietly`open `.catch(() => {
2775
2831
  });
2776
- showFlash(`\u2197 Opened ${row.key}`);
2832
+ showFlash(`\u2197 Opened `);
2777
2833
  }
2778
2834
  return;
2779
2835
  }
2780
2836
  if (input !== "r" && input !== "q" && !key.leftArrow && !key.rightArrow)
2781
2837
  return;
2782
2838
  }
2783
- if (key.escape && search != null) {
2784
- setSearch(null);
2785
- return;
2786
- }
2787
- if (key.escape && repoFilter.size > 0) {
2788
- setRepoFilter(/* @__PURE__ */ new Set());
2789
- return;
2790
- }
2791
2839
  if (menu.handleKey(key)) return;
2792
2840
  if (key.upArrow) {
2793
2841
  const next = moveCursor(section.items, cursor, -1);
@@ -2805,10 +2853,7 @@ var BrowseScreen = ({
2805
2853
  }
2806
2854
  const step = key.leftArrow || key.tab && key.shift ? -1 : key.rightArrow || key.tab ? 1 : 0;
2807
2855
  if (step !== 0)
2808
- setTabIdx(
2809
- (i) => (i + step + localSections.length) % localSections.length
2810
- );
2811
- if (input === "q") exit();
2856
+ setTabIdx((i) => (i + step + localSections.length) % localSections.length);
2812
2857
  if (input === "r") {
2813
2858
  onRefresh?.();
2814
2859
  return;
@@ -3048,6 +3093,7 @@ var BrowseScreen = ({
3048
3093
  [
3049
3094
  ["\u2191\u2193", "initiative"],
3050
3095
  ["\u21B5", "open"],
3096
+ ["o", "browser"],
3051
3097
  ["\u21E5/esc", "back to list"],
3052
3098
  ["?", "help"],
3053
3099
  ["q", "quit"]
@@ -3158,9 +3204,9 @@ var BrowseScreen = ({
3158
3204
  }
3159
3205
  ) }),
3160
3206
  search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3161
- /* @__PURE__ */ jsx(Text3, { color: "cyan", children: " / " }),
3207
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: " / " }),
3162
3208
  /* @__PURE__ */ jsx(Text3, { children: search }),
3163
- searchInput ? /* @__PURE__ */ jsx(Text3, { color: "cyan", children: "\u258F" }) : null,
3209
+ searchInput ? /* @__PURE__ */ jsx(Text3, { color: colors.info, children: "\u258F" }) : null,
3164
3210
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""}${searchInput ? " \u21B5 accept \xB7 esc clear" : " esc clear"}` })
3165
3211
  ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3166
3212
  /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " \u25C9 " }),
@@ -3198,6 +3244,7 @@ var BrowseScreen = ({
3198
3244
  sparkFrame,
3199
3245
  cols: listCols,
3200
3246
  markerCols,
3247
+ uniformLabels,
3201
3248
  gap: i > 0 && gapsAbove(section.items, viewStart + i)
3202
3249
  },
3203
3250
  `${viewStart + i}:${rowKey(item)}`
@@ -3236,7 +3283,7 @@ var BrowseScreen = ({
3236
3283
  }
3237
3284
  ) : null
3238
3285
  ] }),
3239
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: "green", children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3286
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3240
3287
  ]
3241
3288
  }
3242
3289
  );
@@ -3246,9 +3293,7 @@ var NoRowsScreen = ({
3246
3293
  detail,
3247
3294
  onRetry
3248
3295
  }) => {
3249
- const { exit } = useApp();
3250
3296
  useInput((input) => {
3251
- if (input === "q") exit();
3252
3297
  if (input === "r") onRetry();
3253
3298
  });
3254
3299
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
@@ -3297,6 +3342,24 @@ var App = ({
3297
3342
  }) => {
3298
3343
  const { rows } = useWindowSize();
3299
3344
  const [state, setState] = useState({ phase: "loading" });
3345
+ const browsePeel = useRef(null);
3346
+ const drillPeel = useRef(null);
3347
+ const [typing, setTyping] = useState(false);
3348
+ useAppKeys({
3349
+ isActive: !typing,
3350
+ onBack: () => {
3351
+ if (state.phase === "pr" || state.phase === "issue" || state.phase === "ext") {
3352
+ if (drillPeel.current?.()) return true;
3353
+ setState({
3354
+ phase: "browse",
3355
+ sections: state.sections,
3356
+ login: state.login
3357
+ });
3358
+ return true;
3359
+ }
3360
+ return browsePeel.current?.() ?? false;
3361
+ }
3362
+ });
3300
3363
  const [pending, setPending] = useState(null);
3301
3364
  const [refreshing, setRefreshing] = useState(false);
3302
3365
  const [fetchedAt, setFetchedAt] = useState(null);
@@ -3661,6 +3724,8 @@ var App = ({
3661
3724
  budget,
3662
3725
  skippedForBudget,
3663
3726
  hidden: overlay !== null,
3727
+ peelRef: browsePeel,
3728
+ onTyping: setTyping,
3664
3729
  mergedUrls,
3665
3730
  leavingUrls,
3666
3731
  transitSince,
@@ -3698,6 +3763,10 @@ var App = ({
3698
3763
  item: overlay.item,
3699
3764
  kind: overlay.kind,
3700
3765
  login: state.login,
3766
+ registerPeel: (peel) => {
3767
+ drillPeel.current = peel;
3768
+ },
3769
+ onTyping: setTyping,
3701
3770
  onBack: toBrowse,
3702
3771
  onRefresh: applyOrRefresh,
3703
3772
  onRemove: markLeaving,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.55.1",
3
+ "version": "0.57.0",
4
4
  "description": "Ink components for rendering GitHub PR review comments and health — controlled, presentation-only, built on @kud/ink-ui and fed by @kud/gh.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -48,8 +48,8 @@
48
48
  "react": ">=19"
49
49
  },
50
50
  "dependencies": {
51
- "@kud/gh": "0.16.0",
52
- "@kud/gh-workflow": "0.12.0",
51
+ "@kud/gh": "0.17.0",
52
+ "@kud/gh-workflow": "0.12.1",
53
53
  "@kud/ink-ui": "0.29.0",
54
54
  "zx": "8.8.5"
55
55
  },