@kud/gh-ink 0.66.1 → 0.67.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/README.md CHANGED
@@ -32,6 +32,35 @@ the same panel drops into a full-screen CLI or a single pane of a dashboard. The
32
32
  core (`@kud/gh`) decides the semantic token; this layer maps it to a colour. Same
33
33
  seam as `@kud/jenkins` → `@kud/jenkins-ink`.
34
34
 
35
+ ## Inbox extensions
36
+
37
+ A host contributes verbs and screens to the inbox through `InboxExtension`:
38
+
39
+ | Field | What |
40
+ | ----------- | ----------------------------------------------------------------------------------------------------------- |
41
+ | `key` | The keypress that opens it, matched after the inbox's own bindings, so an extension never shadows them. |
42
+ | `scope` | `"item"` acts on the selected row and earns a place in its action menu; `"global"` does not. |
43
+ | `menuGroup` | `"act"` (does something to the row) or `"open"` (opens something); absent or `"other"` joins the quiet end. |
44
+ | `hint` | Short footer label. The action menu capitalises it for the row ("submit" → "Submit"); the legend keeps `title`. |
45
+ | `icon` | Nerd-font glyph for the menu row, as a `"\uF4FA"` escape. Shown only in nerd icon mode (see below). |
46
+ | `body` | `(onExit, target) => ReactNode` — rendered inside the inbox frame; claims header/footer via `useChrome`. |
47
+
48
+ Nerd-font icons render only when the host calls `setIconMode("nerd")` from
49
+ `@kud/ink-ui` before the first render. In the default text mode the menu's icon
50
+ column is dropped entirely — no stand-in, nothing to align — so a terminal
51
+ without a Nerd Font loses nothing but decoration. The built-ins use these
52
+ codepoints; sibling host verbs should match them rather than invent neighbours:
53
+
54
+ | Verb | Codepoint |
55
+ | ----------------------------- | --------- |
56
+ | Submit / Resubmit | U+F4FA / U+F46A |
57
+ | Land | U+F419 |
58
+ | Open PR / in browser / ticket | U+F440 / U+F465 / U+F41B |
59
+ | Switch here / tab / pane | U+EBCB / U+EAE4 / U+EB56 |
60
+ | Copy URL / repo / branch | U+F44C / U+F401 / U+F418 |
61
+ | Unsubscribe / Mute / Remove reviewer | U+F478 / U+F466 / U+F468 |
62
+ | Close PR / and delete / issue | U+F4DC / U+F48E / U+F41D |
63
+
35
64
  ## Development
36
65
 
37
66
  ```sh
package/dist/index.d.ts CHANGED
@@ -417,6 +417,25 @@ declare const healthLegend: [Health, string][];
417
417
  */
418
418
  declare const truncate: (str: string, max: number) => string;
419
419
 
420
+ type ChromeHints = [string, string][];
421
+ type ChromeSpec = {
422
+ scope?: string;
423
+ hints?: ChromeHints;
424
+ };
425
+ type ChromeState = ChromeSpec & {
426
+ setChrome: (spec: ChromeSpec) => void;
427
+ };
428
+ /**
429
+ * Claim a word in the inbox chrome while mounted: `scope` becomes the header
430
+ * breadcrumb (e.g. `Resubmit · acme/api-gateway#1234`), `hints` the footer's
431
+ * verbs. The shell appends its own back/quit tail, so a body passes only its
432
+ * own keys — never `esc`, `q` or `?`.
433
+ *
434
+ * Returns whether a shell claimed it: a view mounted outside the inbox (where
435
+ * the context is null) draws its own chrome instead, so the same component
436
+ * renders framed standalone and chromeless inside the shell.
437
+ */
438
+ declare const useChrome: (spec: ChromeSpec) => boolean;
420
439
  type ExtensionTarget = {
421
440
  item?: AnyItem;
422
441
  ciJob?: string;
@@ -438,6 +457,8 @@ interface InboxExtension {
438
457
  key: string;
439
458
  hint?: string;
440
459
  scope?: "item" | "global";
460
+ menuGroup?: "act" | "open" | "other";
461
+ icon?: string;
441
462
  drills?: AnyItem["kind"][];
442
463
  body: (onExit: () => void, target?: ExtensionTarget) => ReactNode;
443
464
  commands?: (target: ExtensionTarget) => Command[];
@@ -525,12 +546,20 @@ type SettledResult = {
525
546
  ok: false;
526
547
  reason: string;
527
548
  };
549
+ type ActionGroup = "act" | "open" | "switch" | "copy" | "quiet" | "close" | "confirm";
528
550
  type Action = {
529
551
  label: string;
530
552
  hint: string;
531
553
  run: () => void;
532
554
  subActions?: Action[];
555
+ group?: ActionGroup;
556
+ icon?: string;
533
557
  };
558
+ type MenuGap = {
559
+ gap: true;
560
+ };
561
+ type MenuRow = Action | MenuGap;
562
+ declare const isMenuGap: (row: MenuRow) => row is MenuGap;
534
563
  type JiraTransition = {
535
564
  /** What the action menu shows. Yours to word; nothing matches on it. */
536
565
  label: string;
@@ -701,8 +730,16 @@ declare const CiStatusLine: ({ state, job, }: {
701
730
  state: CiStatusState;
702
731
  job?: string;
703
732
  }) => React.JSX.Element;
733
+ declare const menuRowsOf: (actions: Action[]) => MenuRow[];
734
+ /**
735
+ * What tone a menu row wears: `"error"` for the close rows (top-level and
736
+ * confirm alike, never `Cancel`), nothing for everything else. The menu's one
737
+ * meaningful colour, and the only one — every other distinction on the menu
738
+ * is glyph, word or weight, never hue alone.
739
+ */
740
+ declare const menuTone: (row: Action) => "error" | undefined;
704
741
  declare const useActionMenu: () => {
705
- actions: Action[] | null;
742
+ actions: MenuRow[] | null;
706
743
  cursor: number;
707
744
  open: (next: Action[]) => void;
708
745
  close: () => void;
@@ -713,10 +750,17 @@ declare const useActionMenu: () => {
713
750
  escape?: boolean;
714
751
  }) => boolean;
715
752
  };
716
- declare const ActionMenu: ({ item, actions, cursor, }: {
753
+ declare const ActionMenu: ({ item, actions, cursor, cols, }: {
717
754
  item: AnyItem;
718
- actions: Action[];
755
+ actions: MenuRow[];
719
756
  cursor: number;
757
+ /**
758
+ * Content columns the menu may take. The width is fixed from the longest
759
+ * row — clamp(longest + 4, 40, 56), capped at `cols - 4` — so it never
760
+ * resizes as the cursor moves. A host beside a rail passes what the list
761
+ * has; everywhere else the frame's own width is the right answer.
762
+ */
763
+ cols?: number;
720
764
  }) => React.JSX.Element;
721
765
  declare const signatureOf: (sections: Section[]) => string;
722
766
  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, onSettled, }: {
@@ -827,4 +871,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
827
871
  */
828
872
  declare const parsePatterns: (value: string) => string[];
829
873
 
830
- 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, PrRow, type PrRowProps, type Rails, type RepoFilter, type RowAnnouncement, type RowMotion, SIDEBAR_COLS, type SettledResult, SidePanel, type Sidebar, type SidebarRow, type StatusStrip, StatusStripLine, type StripItem, type StripState, type StripStatusState, type TrailingColumns, type TrailingShed, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
874
+ export { type Action, type ActionGroup, ActionMenu, App, COLS, type ChromeHints, type ChromeSpec, type ChromeState, 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 MenuGap, type MenuRow, PrRow, type PrRowProps, type Rails, type RepoFilter, type RowAnnouncement, type RowMotion, SIDEBAR_COLS, type SettledResult, SidePanel, type Sidebar, type SidebarRow, type StatusStrip, StatusStripLine, type StripItem, type StripState, type StripStatusState, type TrailingColumns, type TrailingShed, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import React2, { createContext, useState, useRef, useEffect, useMemo, useContext } from 'react';
1
+ import React2, { createContext, useState, useRef, useContext, useEffect, useCallback, useMemo } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
- import { colors, ScrollView, TextInput, Pill, useAppKeys, LoadingScreen, StatusMessage, FooterHints, useFilterMode, useListCursor, CommandPalette, Tabs, ProgressBar, pillWidth } from '@kud/ink-ui';
3
+ import { colors, ScrollView, TextInput, Pill, getIconMode, FooterHints, useAppKeys, LoadingScreen, StatusMessage, useFilterMode, useListCursor, CommandPalette, Tabs, ProgressBar, pillWidth } 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 { PIN_MARK, sizeOf, sizePartsOf, depthOf, impliedLabels, labelPriority, checkoutDirs, profileOf, inboxConfig, relativeTime, filterBySearch, filterByRepos, filterByOrigin } from '@kud/gh-workflow';
@@ -1161,6 +1161,18 @@ var StatusStripLine = ({
1161
1161
  age ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${age}` }) : null
1162
1162
  ] });
1163
1163
  };
1164
+ var InboxChromeContext = createContext(null);
1165
+ var useChrome = (spec) => {
1166
+ const chrome = useContext(InboxChromeContext);
1167
+ const setChrome = chrome?.setChrome;
1168
+ const scope = spec.scope;
1169
+ const hintsKey = JSON.stringify(spec.hints ?? null);
1170
+ useEffect(() => {
1171
+ if (!setChrome) return;
1172
+ setChrome({ scope, hints: spec.hints });
1173
+ }, [setChrome, scope, hintsKey]);
1174
+ return chrome !== null;
1175
+ };
1164
1176
 
1165
1177
  // src/inbox/cache.ts
1166
1178
  var cacheTtlMs = () => inboxConfig().cacheTtlMs;
@@ -1412,6 +1424,7 @@ var reconcile = (prev, fresh, now, holdMs = HOLD_MS) => {
1412
1424
  return out;
1413
1425
  };
1414
1426
  var quietly = $({ quiet: true });
1427
+ var isMenuGap = (row) => "gap" in row;
1415
1428
  var healthSentence = (item) => {
1416
1429
  const glyph = displayFor(item.health).glyph.trim();
1417
1430
  const d = item.detail;
@@ -1898,6 +1911,14 @@ var drillLabel = (item) => {
1898
1911
  if (item.kind === "pr") return "Open PR";
1899
1912
  return "Drill in";
1900
1913
  };
1914
+ var sameChrome = (a, b) => {
1915
+ if (a.scope !== b.scope) return false;
1916
+ if (a.hints === b.hints) return true;
1917
+ if (!a.hints || !b.hints || a.hints.length !== b.hints.length) return false;
1918
+ return a.hints.every(
1919
+ ([key, label], i) => b.hints[i]?.[0] === key && b.hints[i]?.[1] === label
1920
+ );
1921
+ };
1901
1922
  var UNSUBSCRIBE_MUTATION = `
1902
1923
  mutation($id: ID!) {
1903
1924
  updateSubscription(input: { subscribableId: $id, state: UNSUBSCRIBED }) {
@@ -1948,6 +1969,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1948
1969
  const open = {
1949
1970
  label: "Open in browser",
1950
1971
  hint: "o",
1972
+ group: "open",
1973
+ icon: "\uF465",
1951
1974
  run: () => {
1952
1975
  quietly`open ${item.url}`.catch(() => {
1953
1976
  });
@@ -1957,6 +1980,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1957
1980
  const copyUrl = {
1958
1981
  label: "Copy URL",
1959
1982
  hint: "c",
1983
+ group: "copy",
1984
+ icon: "\uF44C",
1960
1985
  run: () => {
1961
1986
  clipboard(item.url);
1962
1987
  const label = item.kind === "task" ? item.key : `#${item.number}`;
@@ -1968,6 +1993,11 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1968
1993
  const drillAction = drill || mountable && onOpenView ? {
1969
1994
  label: drillLabel(item),
1970
1995
  hint: "d",
1996
+ group: "open",
1997
+ // Only the PR drill names its glyph: "View issue" and "View ticket"
1998
+ // have no nerd-font verb of their own, and an icon that says nothing
1999
+ // the label does not already say is decoration, not information.
2000
+ icon: item.kind === "pr" ? "\uF440" : void 0,
1971
2001
  run: () => {
1972
2002
  if (onOpenView?.(item)) return;
1973
2003
  if (drill) {
@@ -1983,6 +2013,10 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1983
2013
  base.push({
1984
2014
  label: "Move status",
1985
2015
  hint: "t",
2016
+ // The task's act: changing its state, the one mutation this menu owns.
2017
+ // Guarded by the caller's own condition above — without configured
2018
+ // transitions there is no such row, and none is invented here.
2019
+ group: "act",
1986
2020
  run: () => {
1987
2021
  },
1988
2022
  subActions: jiraTransitions.map(
@@ -2016,22 +2050,40 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2016
2050
  )
2017
2051
  });
2018
2052
  }
2019
- return base;
2053
+ return byGroup(base);
2020
2054
  }
2021
2055
  const actions = drillAction ? [drillAction, open, copyUrl] : [open, copyUrl];
2022
2056
  const closing = [];
2023
2057
  actions.push({
2024
2058
  label: "Copy repo name",
2025
2059
  hint: "r",
2060
+ group: "copy",
2061
+ icon: "\uF401",
2026
2062
  run: () => {
2027
2063
  clipboard(item.repo);
2028
2064
  showFlash(`\u2713 Copied ${item.repo}`);
2029
2065
  }
2030
2066
  });
2067
+ actions.push({
2068
+ label: "Unsubscribe",
2069
+ hint: "u",
2070
+ group: "quiet",
2071
+ icon: "\uF478",
2072
+ run: () => {
2073
+ showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
2074
+ void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch((e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`));
2075
+ }
2076
+ });
2031
2077
  if (item.kind === "pr" && item.standing === "queued" && login) {
2032
2078
  actions.push({
2033
2079
  label: "Remove me as reviewer",
2034
2080
  hint: "x",
2081
+ // Quiet, beside Unsubscribe: both are "less of this PR for me" — no more
2082
+ // review requests, no more notifications. Guarded by the caller's own
2083
+ // condition above, like "Move status" is by its: without a standing
2084
+ // review request there is no such row, and none is invented here.
2085
+ group: "quiet",
2086
+ icon: "\uF468",
2035
2087
  run: () => {
2036
2088
  onRemove?.(item);
2037
2089
  showFlash(`\u22EF Removing you from #${item.number}\u2026`);
@@ -2053,18 +2105,12 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2053
2105
  }
2054
2106
  });
2055
2107
  }
2056
- actions.push({
2057
- label: "Unsubscribe",
2058
- hint: "u",
2059
- run: () => {
2060
- showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
2061
- void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch((e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`));
2062
- }
2063
- });
2064
2108
  if (item.kind === "pr" && item.branch) {
2065
2109
  actions.push({
2066
2110
  label: "Copy branch name",
2067
2111
  hint: "b",
2112
+ group: "copy",
2113
+ icon: "\uF418",
2068
2114
  run: () => {
2069
2115
  clipboard(item.branch);
2070
2116
  showFlash(`\u2713 Copied ${item.branch}`);
@@ -2075,6 +2121,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2075
2121
  actions.push({
2076
2122
  label: "Switch here",
2077
2123
  hint: "s",
2124
+ group: "switch",
2125
+ icon: "\uEBCB",
2078
2126
  run: () => {
2079
2127
  const script = [
2080
2128
  "delay 0.5",
@@ -2095,16 +2143,20 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2095
2143
  }
2096
2144
  if (item.kind === "issue") {
2097
2145
  actions.push({
2098
- label: "Open project in new tab",
2146
+ label: "Open in new tab",
2099
2147
  hint: "j",
2148
+ group: "switch",
2149
+ icon: "\uEAE4",
2100
2150
  run: () => {
2101
2151
  showFlash(`\u22EF Opening ${item.repo}\u2026`);
2102
2152
  void jumpToRepo(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
2103
2153
  }
2104
2154
  });
2105
2155
  actions.push({
2106
- label: "Open project in new pane",
2156
+ label: "Open in new pane",
2107
2157
  hint: "p",
2158
+ group: "switch",
2159
+ icon: "\uEB56",
2108
2160
  run: () => {
2109
2161
  showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
2110
2162
  void jumpToRepoPane(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
@@ -2113,12 +2165,15 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2113
2165
  actions.push({
2114
2166
  label: "Close issue",
2115
2167
  hint: "",
2168
+ group: "close",
2169
+ icon: "\uF41D",
2116
2170
  run: () => {
2117
2171
  },
2118
2172
  subActions: [
2119
2173
  {
2120
2174
  label: `Close #${item.number}`,
2121
2175
  hint: "",
2176
+ group: "confirm",
2122
2177
  run: () => {
2123
2178
  onRemove?.(item);
2124
2179
  showFlash(`\u2713 Closed #${item.number}`);
@@ -2137,7 +2192,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2137
2192
  );
2138
2193
  }
2139
2194
  },
2140
- { label: "Cancel", hint: "", run: () => {
2195
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2141
2196
  } }
2142
2197
  ]
2143
2198
  });
@@ -2146,6 +2201,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2146
2201
  actions.push({
2147
2202
  label: "Switch in new tab",
2148
2203
  hint: "j",
2204
+ group: "switch",
2205
+ icon: "\uEAE4",
2149
2206
  run: () => {
2150
2207
  showFlash(`\u22EF Jumping to ${item.repo}\u2026`);
2151
2208
  void jumpToRepo(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
@@ -2154,6 +2211,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2154
2211
  actions.push({
2155
2212
  label: "Switch in new pane",
2156
2213
  hint: "p",
2214
+ group: "switch",
2215
+ icon: "\uEB56",
2157
2216
  run: () => {
2158
2217
  showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
2159
2218
  void jumpToRepoPane(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
@@ -2165,6 +2224,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2165
2224
  actions.push({
2166
2225
  label: `Open ${jiraKey} in Jira`,
2167
2226
  hint: "t",
2227
+ group: "open",
2168
2228
  run: () => openInJira(jiraBase, jiraKey, showFlash)
2169
2229
  });
2170
2230
  }
@@ -2172,12 +2232,15 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2172
2232
  closing.push({
2173
2233
  label: "Close PR",
2174
2234
  hint: "",
2235
+ group: "close",
2236
+ icon: "\uF4DC",
2175
2237
  run: () => {
2176
2238
  },
2177
2239
  subActions: [
2178
2240
  {
2179
2241
  label: `Close #${item.number}`,
2180
2242
  hint: "",
2243
+ group: "confirm",
2181
2244
  run: () => {
2182
2245
  onRemove?.(item);
2183
2246
  showFlash(`\u2713 Closed #${item.number}`);
@@ -2196,20 +2259,25 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2196
2259
  );
2197
2260
  }
2198
2261
  },
2199
- { label: "Cancel", hint: "", run: () => {
2262
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2200
2263
  } }
2201
2264
  ]
2202
2265
  });
2203
2266
  if (item.branch) {
2204
2267
  closing.push({
2205
- label: "Close PR + Delete branch",
2268
+ // "and", not "+": the row is a sentence like every other, and the key
2269
+ // column carries the hint — symbols are not shortcuts here.
2270
+ label: "Close PR and delete branch",
2206
2271
  hint: "",
2272
+ group: "close",
2273
+ icon: "\uF48E",
2207
2274
  run: () => {
2208
2275
  },
2209
2276
  subActions: [
2210
2277
  {
2211
- label: `Close #${item.number} + delete ${item.branch}`,
2278
+ label: `Close #${item.number} and delete ${item.branch}`,
2212
2279
  hint: "",
2280
+ group: "confirm",
2213
2281
  run: () => {
2214
2282
  onRemove?.(item);
2215
2283
  showFlash(`\u2713 Closed #${item.number} and deleted ${item.branch}`);
@@ -2221,16 +2289,20 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2221
2289
  });
2222
2290
  }
2223
2291
  },
2224
- { label: "Cancel", hint: "", run: () => {
2292
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2225
2293
  } }
2226
2294
  ]
2227
2295
  });
2228
2296
  }
2229
2297
  }
2298
+ const extensionLabel = (e) => e.hint ? e.hint.charAt(0).toUpperCase() + e.hint.slice(1) : e.title;
2299
+ const extensionGroup = (e) => e.menuGroup === "act" ? "act" : e.menuGroup === "open" ? "open" : "quiet";
2230
2300
  for (const e of itemExtensions(ext?.extensions))
2231
2301
  actions.push({
2232
- label: e.title,
2302
+ label: extensionLabel(e),
2233
2303
  hint: e.key,
2304
+ group: extensionGroup(e),
2305
+ icon: e.icon,
2234
2306
  run: () => ext?.onOpenExt?.(e.id, {
2235
2307
  item,
2236
2308
  login,
@@ -2239,7 +2311,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2239
2311
  })
2240
2312
  });
2241
2313
  actions.push(...closing);
2242
- return actions;
2314
+ return byGroup(actions);
2243
2315
  };
2244
2316
  var agoText = (ms) => {
2245
2317
  const d = (Date.now() - ms) / 1e3;
@@ -2526,26 +2598,60 @@ var ItemRow = ({
2526
2598
  }
2527
2599
  );
2528
2600
  };
2601
+ var GROUP_RANK = {
2602
+ act: 0,
2603
+ open: 1,
2604
+ switch: 2,
2605
+ copy: 3,
2606
+ quiet: 4,
2607
+ close: 5,
2608
+ confirm: 6
2609
+ };
2610
+ var groupRank = (action) => action.group === void 0 ? 7 : GROUP_RANK[action.group];
2611
+ var menuRowsOf = (actions) => {
2612
+ const sorted = byGroup(actions);
2613
+ const rows = [];
2614
+ sorted.forEach((action, i) => {
2615
+ if (i > 0 && sorted[i - 1].group !== action.group) rows.push({ gap: true });
2616
+ rows.push(action);
2617
+ });
2618
+ return rows;
2619
+ };
2620
+ var byGroup = (actions) => [...actions].sort((a, b) => groupRank(a) - groupRank(b));
2621
+ var isConfirmRows = (rows) => rows.length > 0 && rows.every((row) => isMenuGap(row) || row.group === "confirm");
2622
+ var menuTone = (row) => row.group === "close" || row.group === "confirm" && row.label !== "Cancel" ? "error" : void 0;
2529
2623
  var useActionMenu = () => {
2530
2624
  const [actions, setActions] = useState(null);
2531
2625
  const [cursor, setCursor] = useState(0);
2532
2626
  const open = (next) => {
2533
2627
  if (next.length === 0) return;
2534
2628
  setCursor(0);
2535
- setActions(next);
2629
+ setActions(menuRowsOf(next));
2536
2630
  };
2537
2631
  const handleKey = (key) => {
2538
2632
  if (!actions) return false;
2539
- if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
2540
- if (key.downArrow) setCursor((c) => Math.min(actions.length - 1, c + 1));
2633
+ if (key.upArrow)
2634
+ setCursor((c) => {
2635
+ let n = c - 1;
2636
+ while (n > 0 && isMenuGap(actions[n])) n -= 1;
2637
+ return isMenuGap(actions[n]) ? c : n;
2638
+ });
2639
+ if (key.downArrow)
2640
+ setCursor((c) => {
2641
+ let n = c + 1;
2642
+ while (n < actions.length - 1 && isMenuGap(actions[n])) n += 1;
2643
+ return isMenuGap(actions[n]) ? c : n;
2644
+ });
2541
2645
  if (key.return) {
2542
- const action = actions[cursor];
2543
- if (action?.subActions) {
2544
- setCursor(0);
2545
- setActions(action.subActions);
2646
+ const row = actions[cursor];
2647
+ if (!row || isMenuGap(row)) return true;
2648
+ if (row.subActions) {
2649
+ const rows = menuRowsOf(row.subActions);
2650
+ setCursor(isConfirmRows(rows) ? rows.length - 1 : 0);
2651
+ setActions(rows);
2546
2652
  } else {
2547
2653
  setActions(null);
2548
- action?.run();
2654
+ row.run();
2549
2655
  }
2550
2656
  }
2551
2657
  if (key.escape) setActions(null);
@@ -2556,29 +2662,81 @@ var useActionMenu = () => {
2556
2662
  var ActionMenu = ({
2557
2663
  item,
2558
2664
  actions,
2559
- cursor
2665
+ cursor,
2666
+ cols = COLS
2560
2667
  }) => {
2561
- const title = item.kind === "task" ? item.key : item.kind === "pr" || item.kind === "issue" ? `#${item.number}` : "";
2668
+ const showIcons = getIconMode() === "nerd" && actions.some((row) => !isMenuGap(row) && row.icon !== void 0);
2669
+ const keyWidth = Math.max(
2670
+ 0,
2671
+ ...actions.flatMap(
2672
+ (row) => isMenuGap(row) ? [] : [[...row.hint ?? ""].length]
2673
+ )
2674
+ );
2675
+ const contentWidth = Math.max(
2676
+ 0,
2677
+ ...actions.flatMap(
2678
+ (row) => isMenuGap(row) ? [] : [
2679
+ 2 + (showIcons ? 2 : 0) + [...row.label].length + (keyWidth > 0 ? 1 + keyWidth : 0)
2680
+ ]
2681
+ )
2682
+ );
2683
+ const width = Math.min(
2684
+ Math.max(contentWidth + 4, 40),
2685
+ 56,
2686
+ Math.max(20, cols - 4)
2687
+ );
2688
+ const inner = Math.max(1, width - 4);
2689
+ const confirming = isConfirmRows(actions);
2690
+ const isCloseRow = (row) => menuTone(row) === "error";
2691
+ const number = item.kind === "pr" || item.kind === "issue" ? item.number : null;
2692
+ const leadNum = item.kind === "task" ? item.key : number !== null ? `#${number}` : "";
2693
+ const lead = leadNum ? `${leadNum} \xB7 ` : "";
2694
+ const rest = confirming ? `Close #${number}?` : item.kind === "task" ? item.summary : item.kind === "pr" || item.kind === "issue" ? item.title : "";
2695
+ const restText = [...lead].length + [...rest].length <= inner ? rest : [...rest].slice(0, Math.max(0, inner - [...lead].length - 1)).join("") + "\u2026";
2562
2696
  return /* @__PURE__ */ jsxs(
2563
2697
  Box,
2564
2698
  {
2565
2699
  flexDirection: "column",
2566
2700
  flexShrink: 0,
2701
+ width,
2567
2702
  borderStyle: "round",
2568
2703
  borderColor: colors.info,
2569
2704
  backgroundColor: OVERLAY_BG,
2570
2705
  paddingX: 1,
2571
2706
  marginTop: 1,
2572
2707
  children: [
2573
- /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: title }),
2574
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(32) }),
2575
- actions.map((a, i) => /* @__PURE__ */ jsxs(Box, { children: [
2576
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: i === cursor ? "\u276F " : " " }),
2577
- /* @__PURE__ */ jsx(Text3, { bold: i === cursor, children: a.label }),
2578
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + a.hint })
2579
- ] }, a.label)),
2580
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(32) }),
2581
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2191\u2193 navigate \u21B5 confirm esc cancel" })
2708
+ confirming ? /* @__PURE__ */ jsx(Text3, { color: colors.error, bold: true, children: restText }) : /* @__PURE__ */ jsxs(Text3, { children: [
2709
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: leadNum }),
2710
+ leadNum ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " \xB7 " }) : null,
2711
+ /* @__PURE__ */ jsx(Text3, { children: restText })
2712
+ ] }),
2713
+ /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(inner) }),
2714
+ actions.map(
2715
+ (row, i) => isMenuGap(row) ? /* @__PURE__ */ jsx(Text3, { children: " " }, `gap-${i}`) : /* @__PURE__ */ jsxs(Box, { width: inner, children: [
2716
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text3, { color: colors.info, children: i === cursor ? "\u276F " : " " }) }),
2717
+ showIcons ? /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: row.icon ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: row.icon }) : /* @__PURE__ */ jsx(Text3, { children: " " }) }) : null,
2718
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(
2719
+ Text3,
2720
+ {
2721
+ bold: i === cursor,
2722
+ color: isCloseRow(row) ? colors.error : void 0,
2723
+ children: row.label
2724
+ }
2725
+ ) }),
2726
+ keyWidth > 0 ? /* @__PURE__ */ jsx(Box, { width: keyWidth, flexShrink: 0, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: row.hint ?? "" }) }) : null
2727
+ ] }, `${i}:${row.label}`)
2728
+ ),
2729
+ /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(inner) }),
2730
+ /* @__PURE__ */ jsx(
2731
+ FooterHints,
2732
+ {
2733
+ hints: [
2734
+ ["\u2191\u2193", "move"],
2735
+ ["\u21B5", "run"],
2736
+ ["esc", "close"]
2737
+ ]
2738
+ }
2739
+ )
2582
2740
  ]
2583
2741
  }
2584
2742
  );
@@ -2854,6 +3012,7 @@ var BrowseScreen = ({
2854
3012
  fetchedAt,
2855
3013
  refreshError,
2856
3014
  hidden,
3015
+ overlayBody,
2857
3016
  peelRef,
2858
3017
  onTyping,
2859
3018
  onOpenPr,
@@ -3603,7 +3762,8 @@ var BrowseScreen = ({
3603
3762
  }));
3604
3763
  return { items, run, message };
3605
3764
  }, [palette, activeItem, extensions, jiraBase, jiraKeyRe, jiraSetupHint]);
3606
- if (hidden) return null;
3765
+ const chrome = useContext(InboxChromeContext);
3766
+ const overlayOpen = overlayBody != null;
3607
3767
  const overlay = palette !== null ? /* @__PURE__ */ jsx(
3608
3768
  CommandPalette,
3609
3769
  {
@@ -3648,7 +3808,8 @@ var BrowseScreen = ({
3648
3808
  {
3649
3809
  item: activeItem,
3650
3810
  actions: menu.actions,
3651
- cursor: menu.cursor
3811
+ cursor: menu.cursor,
3812
+ cols: listCols
3652
3813
  }
3653
3814
  ) : null;
3654
3815
  return /* @__PURE__ */ jsxs(
@@ -3667,7 +3828,7 @@ var BrowseScreen = ({
3667
3828
  brand,
3668
3829
  sections: localSections,
3669
3830
  login,
3670
- scopeLabel: origin?.label,
3831
+ scopeLabel: overlayOpen ? chrome?.scope ?? origin?.label : origin?.label,
3671
3832
  budgetLabel: budgetNotice(budget)?.label,
3672
3833
  budgetCritical: budgetNotice(budget)?.critical,
3673
3834
  refreshing,
@@ -3690,7 +3851,7 @@ var BrowseScreen = ({
3690
3851
  /* @__PURE__ */ jsx(Text3, { bold: true, color: colors.accent, children: " \u26A1 auto-refresh paused to save API budget" }),
3691
3852
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " r refreshes anyway" })
3692
3853
  ] }) : null,
3693
- /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
3854
+ overlayOpen ? null : /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
3694
3855
  Tabs,
3695
3856
  {
3696
3857
  active: section.id,
@@ -3723,18 +3884,24 @@ var BrowseScreen = ({
3723
3884
  }))
3724
3885
  }
3725
3886
  ) }),
3726
- search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3887
+ !overlayOpen && search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3727
3888
  /* @__PURE__ */ jsx(Text3, { color: colors.info, children: " / " }),
3728
3889
  /* @__PURE__ */ jsx(Text3, { children: search }),
3729
3890
  searchInput ? /* @__PURE__ */ jsx(Text3, { color: colors.info, children: "\u258F" }) : null,
3730
3891
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""} ${filter.hints.map(([k, label]) => `${k} ${label}`).join(" \xB7 ")}` })
3731
- ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3892
+ ] }) : !overlayOpen && repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3732
3893
  /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " \u25C9 " }),
3733
3894
  /* @__PURE__ */ jsx(Text3, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
3734
3895
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " f edit \xB7 esc clear" })
3735
3896
  ] }) : null,
3736
3897
  /* @__PURE__ */ jsxs(Box, { children: [
3737
- /* @__PURE__ */ jsxs(
3898
+ overlayOpen ? (
3899
+ // The overlay owns this band: the same frame, the same header above
3900
+ // and footer below, the body where the list was. Fixed to the list's
3901
+ // own height so the frame holds still between browse and drill, and
3902
+ // clipped rather than spilling past the footer on a long body.
3903
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", height: listHeight, overflow: "hidden", children: overlayBody })
3904
+ ) : /* @__PURE__ */ jsxs(
3738
3905
  Box,
3739
3906
  {
3740
3907
  flexDirection: "column",
@@ -3794,7 +3961,7 @@ var BrowseScreen = ({
3794
3961
  ]
3795
3962
  }
3796
3963
  ),
3797
- showRail ? /* @__PURE__ */ jsx(
3964
+ showRail && !overlayOpen ? /* @__PURE__ */ jsx(
3798
3965
  SidePanel,
3799
3966
  {
3800
3967
  sidebar: rails,
@@ -3806,7 +3973,20 @@ var BrowseScreen = ({
3806
3973
  }
3807
3974
  ) : null
3808
3975
  ] }),
3809
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3976
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : overlayOpen ? (
3977
+ // The overlay's own verbs, plus the way back. `page="nested"` is what
3978
+ // appends `⌫ back` and `q quit`, so a body passes only its own keys
3979
+ // through `useChrome` — the same contract ink-ui's `Page` offers its
3980
+ // nested pages, without re-laying-out the browse frame as one.
3981
+ /* @__PURE__ */ jsx(
3982
+ FooterHints,
3983
+ {
3984
+ hints: chrome?.hints ?? [],
3985
+ page: "nested",
3986
+ help: false
3987
+ }
3988
+ )
3989
+ ) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3810
3990
  ]
3811
3991
  }
3812
3992
  );
@@ -3873,6 +4053,15 @@ var App = ({
3873
4053
  const browsePeel = useRef(null);
3874
4054
  const drillPeel = useRef(null);
3875
4055
  const [typing, setTyping] = useState(false);
4056
+ const [chromeSpec, setChromeSpec] = useState({});
4057
+ const setChrome = useCallback(
4058
+ (next) => setChromeSpec((prev) => sameChrome(prev, next) ? prev : next),
4059
+ []
4060
+ );
4061
+ const chromeValue = useMemo(
4062
+ () => ({ scope: chromeSpec.scope, hints: chromeSpec.hints, setChrome }),
4063
+ [chromeSpec, setChrome]
4064
+ );
3876
4065
  useAppKeys({
3877
4066
  isActive: !typing,
3878
4067
  onBack: () => {
@@ -4236,7 +4425,10 @@ var App = ({
4236
4425
  extId: state.extId,
4237
4426
  target: state.target
4238
4427
  } : null;
4239
- const toBrowse = () => setState({ phase: "browse", sections: state.sections, login: state.login });
4428
+ const toBrowse = () => {
4429
+ setChromeSpec({});
4430
+ setState({ phase: "browse", sections: state.sections, login: state.login });
4431
+ };
4240
4432
  const markLeaving = (target) => {
4241
4433
  if (state.phase !== "browse") toBrowse();
4242
4434
  if (!leavingSince.current.has(target.url))
@@ -4264,82 +4456,89 @@ var App = ({
4264
4456
  );
4265
4457
  }, MERGED_HOLD_MS);
4266
4458
  };
4267
- return /* @__PURE__ */ jsxs(Fragment, { children: [
4268
- /* @__PURE__ */ jsx(
4269
- BrowseScreen,
4270
- {
4271
- brand: brandOf(title),
4272
- sections: state.sections,
4273
- login: state.login,
4274
- jiraBase,
4275
- jiraKeyRe,
4276
- jiraSetupHint,
4277
- jiraTransitions,
4278
- onRefresh: applyOrRefresh,
4279
- onActed,
4280
- onSettled,
4281
- refreshing,
4282
- hasPending: pending !== null,
4283
- pendingSummary,
4284
- appliedSummary: transients.size > 0 ? appliedSummary : "",
4285
- fetchedAt,
4286
- refreshError: refreshError ?? void 0,
4287
- origin,
4288
- budget,
4289
- skippedForBudget,
4290
- hidden: overlay !== null,
4291
- peelRef: browsePeel,
4292
- onTyping: setTyping,
4293
- mergedUrls,
4294
- leavingUrls,
4295
- transitSince,
4296
- onLeave: markLeaving,
4297
- transients,
4298
- onTabChange: setVisibleTab,
4299
- sidebar,
4300
- liveLabel,
4301
- ciStatusState: hasCiStatus ? ciStatusState : void 0,
4302
- ciJob,
4303
- stripState: hasStrip ? stripState : void 0,
4304
- stripLabel,
4305
- tabHelp,
4306
- onOpenPr: (item) => setState({
4459
+ const overlayBody = overlay?.kind === "pr" || overlay?.kind === "issue" ? detailFor?.({
4460
+ item: overlay.item,
4461
+ kind: overlay.kind,
4462
+ login: state.login,
4463
+ registerPeel: (peel) => {
4464
+ drillPeel.current = peel;
4465
+ },
4466
+ onTyping: setTyping,
4467
+ onBack: toBrowse,
4468
+ onRefresh: applyOrRefresh,
4469
+ onRemove: markLeaving,
4470
+ onMerged: markMerged
4471
+ }) : overlay?.kind === "ext" ? extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target) : null;
4472
+ return /* @__PURE__ */ jsx(InboxChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx(
4473
+ BrowseScreen,
4474
+ {
4475
+ brand: brandOf(title),
4476
+ sections: state.sections,
4477
+ login: state.login,
4478
+ jiraBase,
4479
+ jiraKeyRe,
4480
+ jiraSetupHint,
4481
+ jiraTransitions,
4482
+ onRefresh: applyOrRefresh,
4483
+ onActed,
4484
+ onSettled,
4485
+ refreshing,
4486
+ hasPending: pending !== null,
4487
+ pendingSummary,
4488
+ appliedSummary: transients.size > 0 ? appliedSummary : "",
4489
+ fetchedAt,
4490
+ refreshError: refreshError ?? void 0,
4491
+ origin,
4492
+ budget,
4493
+ skippedForBudget,
4494
+ hidden: overlay !== null,
4495
+ overlayBody: overlayBody ?? void 0,
4496
+ peelRef: browsePeel,
4497
+ onTyping: setTyping,
4498
+ mergedUrls,
4499
+ leavingUrls,
4500
+ transitSince,
4501
+ onLeave: markLeaving,
4502
+ transients,
4503
+ onTabChange: setVisibleTab,
4504
+ sidebar,
4505
+ liveLabel,
4506
+ ciStatusState: hasCiStatus ? ciStatusState : void 0,
4507
+ ciJob,
4508
+ stripState: hasStrip ? stripState : void 0,
4509
+ stripLabel,
4510
+ tabHelp,
4511
+ onOpenPr: (item) => {
4512
+ setChromeSpec({});
4513
+ setState({
4307
4514
  phase: "pr",
4308
4515
  item,
4309
4516
  sections: state.sections,
4310
4517
  login: state.login
4311
- }),
4312
- onOpenIssue: (item) => setState({
4518
+ });
4519
+ },
4520
+ onOpenIssue: (item) => {
4521
+ setChromeSpec({});
4522
+ setState({
4313
4523
  phase: "issue",
4314
4524
  item,
4315
4525
  sections: state.sections,
4316
4526
  login: state.login
4317
- }),
4318
- onOpenExt: (id, target) => setState({
4527
+ });
4528
+ },
4529
+ onOpenExt: (id, target) => {
4530
+ setChromeSpec({});
4531
+ setState({
4319
4532
  phase: "ext",
4320
4533
  extId: id,
4321
4534
  target,
4322
4535
  sections: state.sections,
4323
4536
  login: state.login
4324
- }),
4325
- extensions
4326
- }
4327
- ),
4328
- (overlay?.kind === "pr" || overlay?.kind === "issue") && detailFor?.({
4329
- item: overlay.item,
4330
- kind: overlay.kind,
4331
- login: state.login,
4332
- registerPeel: (peel) => {
4333
- drillPeel.current = peel;
4537
+ });
4334
4538
  },
4335
- onTyping: setTyping,
4336
- onBack: toBrowse,
4337
- onRefresh: applyOrRefresh,
4338
- onRemove: markLeaving,
4339
- onMerged: markMerged
4340
- }),
4341
- overlay?.kind === "ext" && extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target)
4342
- ] });
4539
+ extensions
4540
+ }
4541
+ ) });
4343
4542
  };
4344
4543
 
4345
4544
  // src/inbox/filter.ts
@@ -4356,4 +4555,4 @@ var matchesFilter = (repo, filter) => {
4356
4555
  };
4357
4556
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
4358
4557
 
4359
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, PrRow, 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, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, windowCount, withHeaders, withoutItem, workCount, writeCache };
4558
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.66.1",
3
+ "version": "0.67.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.19.0",
52
- "@kud/gh-workflow": "0.14.0",
51
+ "@kud/gh": "0.20.0",
52
+ "@kud/gh-workflow": "0.14.1",
53
53
  "@kud/ink-ui": "0.33.0",
54
54
  "zx": "8.8.5"
55
55
  },