@kud/gh-ink 0.66.1 → 0.68.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,29 @@ var StatusStripLine = ({
1161
1161
  age ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${age}` }) : null
1162
1162
  ] });
1163
1163
  };
1164
+ var SEARCH_GLYPH_NERD = "\uF422";
1165
+ var searchGlyph = (nerd) => nerd ? SEARCH_GLYPH_NERD : "/";
1166
+ var searchBarState = ({
1167
+ typing,
1168
+ nerd
1169
+ }) => ({
1170
+ glyph: searchGlyph(nerd),
1171
+ glyphColor: typing ? colors.info : colors.muted,
1172
+ caret: typing,
1173
+ hints: typing ? "\u2191\u2193 move \xB7 \u21B5 keep \xB7 esc clear" : "filtered \xB7 keys active \xB7 esc clear"
1174
+ });
1175
+ var InboxChromeContext = createContext(null);
1176
+ var useChrome = (spec) => {
1177
+ const chrome = useContext(InboxChromeContext);
1178
+ const setChrome = chrome?.setChrome;
1179
+ const scope = spec.scope;
1180
+ const hintsKey = JSON.stringify(spec.hints ?? null);
1181
+ useEffect(() => {
1182
+ if (!setChrome) return;
1183
+ setChrome({ scope, hints: spec.hints });
1184
+ }, [setChrome, scope, hintsKey]);
1185
+ return chrome !== null;
1186
+ };
1164
1187
 
1165
1188
  // src/inbox/cache.ts
1166
1189
  var cacheTtlMs = () => inboxConfig().cacheTtlMs;
@@ -1412,6 +1435,7 @@ var reconcile = (prev, fresh, now, holdMs = HOLD_MS) => {
1412
1435
  return out;
1413
1436
  };
1414
1437
  var quietly = $({ quiet: true });
1438
+ var isMenuGap = (row) => "gap" in row;
1415
1439
  var healthSentence = (item) => {
1416
1440
  const glyph = displayFor(item.health).glyph.trim();
1417
1441
  const d = item.detail;
@@ -1898,6 +1922,14 @@ var drillLabel = (item) => {
1898
1922
  if (item.kind === "pr") return "Open PR";
1899
1923
  return "Drill in";
1900
1924
  };
1925
+ var sameChrome = (a, b) => {
1926
+ if (a.scope !== b.scope) return false;
1927
+ if (a.hints === b.hints) return true;
1928
+ if (!a.hints || !b.hints || a.hints.length !== b.hints.length) return false;
1929
+ return a.hints.every(
1930
+ ([key, label], i) => b.hints[i]?.[0] === key && b.hints[i]?.[1] === label
1931
+ );
1932
+ };
1901
1933
  var UNSUBSCRIBE_MUTATION = `
1902
1934
  mutation($id: ID!) {
1903
1935
  updateSubscription(input: { subscribableId: $id, state: UNSUBSCRIBED }) {
@@ -1948,6 +1980,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1948
1980
  const open = {
1949
1981
  label: "Open in browser",
1950
1982
  hint: "o",
1983
+ group: "open",
1984
+ icon: "\uF465",
1951
1985
  run: () => {
1952
1986
  quietly`open ${item.url}`.catch(() => {
1953
1987
  });
@@ -1957,6 +1991,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1957
1991
  const copyUrl = {
1958
1992
  label: "Copy URL",
1959
1993
  hint: "c",
1994
+ group: "copy",
1995
+ icon: "\uF44C",
1960
1996
  run: () => {
1961
1997
  clipboard(item.url);
1962
1998
  const label = item.kind === "task" ? item.key : `#${item.number}`;
@@ -1968,6 +2004,11 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1968
2004
  const drillAction = drill || mountable && onOpenView ? {
1969
2005
  label: drillLabel(item),
1970
2006
  hint: "d",
2007
+ group: "open",
2008
+ // Only the PR drill names its glyph: "View issue" and "View ticket"
2009
+ // have no nerd-font verb of their own, and an icon that says nothing
2010
+ // the label does not already say is decoration, not information.
2011
+ icon: item.kind === "pr" ? "\uF440" : void 0,
1971
2012
  run: () => {
1972
2013
  if (onOpenView?.(item)) return;
1973
2014
  if (drill) {
@@ -1983,6 +2024,10 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1983
2024
  base.push({
1984
2025
  label: "Move status",
1985
2026
  hint: "t",
2027
+ // The task's act: changing its state, the one mutation this menu owns.
2028
+ // Guarded by the caller's own condition above — without configured
2029
+ // transitions there is no such row, and none is invented here.
2030
+ group: "act",
1986
2031
  run: () => {
1987
2032
  },
1988
2033
  subActions: jiraTransitions.map(
@@ -2016,22 +2061,40 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2016
2061
  )
2017
2062
  });
2018
2063
  }
2019
- return base;
2064
+ return byGroup(base);
2020
2065
  }
2021
2066
  const actions = drillAction ? [drillAction, open, copyUrl] : [open, copyUrl];
2022
2067
  const closing = [];
2023
2068
  actions.push({
2024
2069
  label: "Copy repo name",
2025
2070
  hint: "r",
2071
+ group: "copy",
2072
+ icon: "\uF401",
2026
2073
  run: () => {
2027
2074
  clipboard(item.repo);
2028
2075
  showFlash(`\u2713 Copied ${item.repo}`);
2029
2076
  }
2030
2077
  });
2078
+ actions.push({
2079
+ label: "Unsubscribe",
2080
+ hint: "u",
2081
+ group: "quiet",
2082
+ icon: "\uF478",
2083
+ run: () => {
2084
+ showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
2085
+ void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch((e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`));
2086
+ }
2087
+ });
2031
2088
  if (item.kind === "pr" && item.standing === "queued" && login) {
2032
2089
  actions.push({
2033
2090
  label: "Remove me as reviewer",
2034
2091
  hint: "x",
2092
+ // Quiet, beside Unsubscribe: both are "less of this PR for me" — no more
2093
+ // review requests, no more notifications. Guarded by the caller's own
2094
+ // condition above, like "Move status" is by its: without a standing
2095
+ // review request there is no such row, and none is invented here.
2096
+ group: "quiet",
2097
+ icon: "\uF468",
2035
2098
  run: () => {
2036
2099
  onRemove?.(item);
2037
2100
  showFlash(`\u22EF Removing you from #${item.number}\u2026`);
@@ -2053,18 +2116,12 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2053
2116
  }
2054
2117
  });
2055
2118
  }
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
2119
  if (item.kind === "pr" && item.branch) {
2065
2120
  actions.push({
2066
2121
  label: "Copy branch name",
2067
2122
  hint: "b",
2123
+ group: "copy",
2124
+ icon: "\uF418",
2068
2125
  run: () => {
2069
2126
  clipboard(item.branch);
2070
2127
  showFlash(`\u2713 Copied ${item.branch}`);
@@ -2075,6 +2132,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2075
2132
  actions.push({
2076
2133
  label: "Switch here",
2077
2134
  hint: "s",
2135
+ group: "switch",
2136
+ icon: "\uEBCB",
2078
2137
  run: () => {
2079
2138
  const script = [
2080
2139
  "delay 0.5",
@@ -2095,16 +2154,20 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2095
2154
  }
2096
2155
  if (item.kind === "issue") {
2097
2156
  actions.push({
2098
- label: "Open project in new tab",
2157
+ label: "Open in new tab",
2099
2158
  hint: "j",
2159
+ group: "switch",
2160
+ icon: "\uEAE4",
2100
2161
  run: () => {
2101
2162
  showFlash(`\u22EF Opening ${item.repo}\u2026`);
2102
2163
  void jumpToRepo(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
2103
2164
  }
2104
2165
  });
2105
2166
  actions.push({
2106
- label: "Open project in new pane",
2167
+ label: "Open in new pane",
2107
2168
  hint: "p",
2169
+ group: "switch",
2170
+ icon: "\uEB56",
2108
2171
  run: () => {
2109
2172
  showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
2110
2173
  void jumpToRepoPane(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
@@ -2113,12 +2176,15 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2113
2176
  actions.push({
2114
2177
  label: "Close issue",
2115
2178
  hint: "",
2179
+ group: "close",
2180
+ icon: "\uF41D",
2116
2181
  run: () => {
2117
2182
  },
2118
2183
  subActions: [
2119
2184
  {
2120
2185
  label: `Close #${item.number}`,
2121
2186
  hint: "",
2187
+ group: "confirm",
2122
2188
  run: () => {
2123
2189
  onRemove?.(item);
2124
2190
  showFlash(`\u2713 Closed #${item.number}`);
@@ -2137,7 +2203,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2137
2203
  );
2138
2204
  }
2139
2205
  },
2140
- { label: "Cancel", hint: "", run: () => {
2206
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2141
2207
  } }
2142
2208
  ]
2143
2209
  });
@@ -2146,6 +2212,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2146
2212
  actions.push({
2147
2213
  label: "Switch in new tab",
2148
2214
  hint: "j",
2215
+ group: "switch",
2216
+ icon: "\uEAE4",
2149
2217
  run: () => {
2150
2218
  showFlash(`\u22EF Jumping to ${item.repo}\u2026`);
2151
2219
  void jumpToRepo(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
@@ -2154,6 +2222,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2154
2222
  actions.push({
2155
2223
  label: "Switch in new pane",
2156
2224
  hint: "p",
2225
+ group: "switch",
2226
+ icon: "\uEB56",
2157
2227
  run: () => {
2158
2228
  showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
2159
2229
  void jumpToRepoPane(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
@@ -2165,6 +2235,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2165
2235
  actions.push({
2166
2236
  label: `Open ${jiraKey} in Jira`,
2167
2237
  hint: "t",
2238
+ group: "open",
2168
2239
  run: () => openInJira(jiraBase, jiraKey, showFlash)
2169
2240
  });
2170
2241
  }
@@ -2172,12 +2243,15 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2172
2243
  closing.push({
2173
2244
  label: "Close PR",
2174
2245
  hint: "",
2246
+ group: "close",
2247
+ icon: "\uF4DC",
2175
2248
  run: () => {
2176
2249
  },
2177
2250
  subActions: [
2178
2251
  {
2179
2252
  label: `Close #${item.number}`,
2180
2253
  hint: "",
2254
+ group: "confirm",
2181
2255
  run: () => {
2182
2256
  onRemove?.(item);
2183
2257
  showFlash(`\u2713 Closed #${item.number}`);
@@ -2196,20 +2270,25 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2196
2270
  );
2197
2271
  }
2198
2272
  },
2199
- { label: "Cancel", hint: "", run: () => {
2273
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2200
2274
  } }
2201
2275
  ]
2202
2276
  });
2203
2277
  if (item.branch) {
2204
2278
  closing.push({
2205
- label: "Close PR + Delete branch",
2279
+ // "and", not "+": the row is a sentence like every other, and the key
2280
+ // column carries the hint — symbols are not shortcuts here.
2281
+ label: "Close PR and delete branch",
2206
2282
  hint: "",
2283
+ group: "close",
2284
+ icon: "\uF48E",
2207
2285
  run: () => {
2208
2286
  },
2209
2287
  subActions: [
2210
2288
  {
2211
- label: `Close #${item.number} + delete ${item.branch}`,
2289
+ label: `Close #${item.number} and delete ${item.branch}`,
2212
2290
  hint: "",
2291
+ group: "confirm",
2213
2292
  run: () => {
2214
2293
  onRemove?.(item);
2215
2294
  showFlash(`\u2713 Closed #${item.number} and deleted ${item.branch}`);
@@ -2221,16 +2300,20 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2221
2300
  });
2222
2301
  }
2223
2302
  },
2224
- { label: "Cancel", hint: "", run: () => {
2303
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2225
2304
  } }
2226
2305
  ]
2227
2306
  });
2228
2307
  }
2229
2308
  }
2309
+ const extensionLabel = (e) => e.hint ? e.hint.charAt(0).toUpperCase() + e.hint.slice(1) : e.title;
2310
+ const extensionGroup = (e) => e.menuGroup === "act" ? "act" : e.menuGroup === "open" ? "open" : "quiet";
2230
2311
  for (const e of itemExtensions(ext?.extensions))
2231
2312
  actions.push({
2232
- label: e.title,
2313
+ label: extensionLabel(e),
2233
2314
  hint: e.key,
2315
+ group: extensionGroup(e),
2316
+ icon: e.icon,
2234
2317
  run: () => ext?.onOpenExt?.(e.id, {
2235
2318
  item,
2236
2319
  login,
@@ -2239,7 +2322,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2239
2322
  })
2240
2323
  });
2241
2324
  actions.push(...closing);
2242
- return actions;
2325
+ return byGroup(actions);
2243
2326
  };
2244
2327
  var agoText = (ms) => {
2245
2328
  const d = (Date.now() - ms) / 1e3;
@@ -2526,26 +2609,60 @@ var ItemRow = ({
2526
2609
  }
2527
2610
  );
2528
2611
  };
2612
+ var GROUP_RANK = {
2613
+ act: 0,
2614
+ open: 1,
2615
+ switch: 2,
2616
+ copy: 3,
2617
+ quiet: 4,
2618
+ close: 5,
2619
+ confirm: 6
2620
+ };
2621
+ var groupRank = (action) => action.group === void 0 ? 7 : GROUP_RANK[action.group];
2622
+ var menuRowsOf = (actions) => {
2623
+ const sorted = byGroup(actions);
2624
+ const rows = [];
2625
+ sorted.forEach((action, i) => {
2626
+ if (i > 0 && sorted[i - 1].group !== action.group) rows.push({ gap: true });
2627
+ rows.push(action);
2628
+ });
2629
+ return rows;
2630
+ };
2631
+ var byGroup = (actions) => [...actions].sort((a, b) => groupRank(a) - groupRank(b));
2632
+ var isConfirmRows = (rows) => rows.length > 0 && rows.every((row) => isMenuGap(row) || row.group === "confirm");
2633
+ var menuTone = (row) => row.group === "close" || row.group === "confirm" && row.label !== "Cancel" ? "error" : void 0;
2529
2634
  var useActionMenu = () => {
2530
2635
  const [actions, setActions] = useState(null);
2531
2636
  const [cursor, setCursor] = useState(0);
2532
2637
  const open = (next) => {
2533
2638
  if (next.length === 0) return;
2534
2639
  setCursor(0);
2535
- setActions(next);
2640
+ setActions(menuRowsOf(next));
2536
2641
  };
2537
2642
  const handleKey = (key) => {
2538
2643
  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));
2644
+ if (key.upArrow)
2645
+ setCursor((c) => {
2646
+ let n = c - 1;
2647
+ while (n > 0 && isMenuGap(actions[n])) n -= 1;
2648
+ return isMenuGap(actions[n]) ? c : n;
2649
+ });
2650
+ if (key.downArrow)
2651
+ setCursor((c) => {
2652
+ let n = c + 1;
2653
+ while (n < actions.length - 1 && isMenuGap(actions[n])) n += 1;
2654
+ return isMenuGap(actions[n]) ? c : n;
2655
+ });
2541
2656
  if (key.return) {
2542
- const action = actions[cursor];
2543
- if (action?.subActions) {
2544
- setCursor(0);
2545
- setActions(action.subActions);
2657
+ const row = actions[cursor];
2658
+ if (!row || isMenuGap(row)) return true;
2659
+ if (row.subActions) {
2660
+ const rows = menuRowsOf(row.subActions);
2661
+ setCursor(isConfirmRows(rows) ? rows.length - 1 : 0);
2662
+ setActions(rows);
2546
2663
  } else {
2547
2664
  setActions(null);
2548
- action?.run();
2665
+ row.run();
2549
2666
  }
2550
2667
  }
2551
2668
  if (key.escape) setActions(null);
@@ -2556,29 +2673,81 @@ var useActionMenu = () => {
2556
2673
  var ActionMenu = ({
2557
2674
  item,
2558
2675
  actions,
2559
- cursor
2676
+ cursor,
2677
+ cols = COLS
2560
2678
  }) => {
2561
- const title = item.kind === "task" ? item.key : item.kind === "pr" || item.kind === "issue" ? `#${item.number}` : "";
2679
+ const showIcons = getIconMode() === "nerd" && actions.some((row) => !isMenuGap(row) && row.icon !== void 0);
2680
+ const keyWidth = Math.max(
2681
+ 0,
2682
+ ...actions.flatMap(
2683
+ (row) => isMenuGap(row) ? [] : [[...row.hint ?? ""].length]
2684
+ )
2685
+ );
2686
+ const contentWidth = Math.max(
2687
+ 0,
2688
+ ...actions.flatMap(
2689
+ (row) => isMenuGap(row) ? [] : [
2690
+ 2 + (showIcons ? 2 : 0) + [...row.label].length + (keyWidth > 0 ? 1 + keyWidth : 0)
2691
+ ]
2692
+ )
2693
+ );
2694
+ const width = Math.min(
2695
+ Math.max(contentWidth + 4, 40),
2696
+ 56,
2697
+ Math.max(20, cols - 4)
2698
+ );
2699
+ const inner = Math.max(1, width - 4);
2700
+ const confirming = isConfirmRows(actions);
2701
+ const isCloseRow = (row) => menuTone(row) === "error";
2702
+ const number = item.kind === "pr" || item.kind === "issue" ? item.number : null;
2703
+ const leadNum = item.kind === "task" ? item.key : number !== null ? `#${number}` : "";
2704
+ const lead = leadNum ? `${leadNum} \xB7 ` : "";
2705
+ const rest = confirming ? `Close #${number}?` : item.kind === "task" ? item.summary : item.kind === "pr" || item.kind === "issue" ? item.title : "";
2706
+ const restText = [...lead].length + [...rest].length <= inner ? rest : [...rest].slice(0, Math.max(0, inner - [...lead].length - 1)).join("") + "\u2026";
2562
2707
  return /* @__PURE__ */ jsxs(
2563
2708
  Box,
2564
2709
  {
2565
2710
  flexDirection: "column",
2566
2711
  flexShrink: 0,
2712
+ width,
2567
2713
  borderStyle: "round",
2568
2714
  borderColor: colors.info,
2569
2715
  backgroundColor: OVERLAY_BG,
2570
2716
  paddingX: 1,
2571
2717
  marginTop: 1,
2572
2718
  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" })
2719
+ confirming ? /* @__PURE__ */ jsx(Text3, { color: colors.error, bold: true, children: restText }) : /* @__PURE__ */ jsxs(Text3, { children: [
2720
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: leadNum }),
2721
+ leadNum ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " \xB7 " }) : null,
2722
+ /* @__PURE__ */ jsx(Text3, { children: restText })
2723
+ ] }),
2724
+ /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(inner) }),
2725
+ actions.map(
2726
+ (row, i) => isMenuGap(row) ? /* @__PURE__ */ jsx(Text3, { children: " " }, `gap-${i}`) : /* @__PURE__ */ jsxs(Box, { width: inner, children: [
2727
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text3, { color: colors.info, children: i === cursor ? "\u276F " : " " }) }),
2728
+ showIcons ? /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: row.icon ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: row.icon }) : /* @__PURE__ */ jsx(Text3, { children: " " }) }) : null,
2729
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(
2730
+ Text3,
2731
+ {
2732
+ bold: i === cursor,
2733
+ color: isCloseRow(row) ? colors.error : void 0,
2734
+ children: row.label
2735
+ }
2736
+ ) }),
2737
+ keyWidth > 0 ? /* @__PURE__ */ jsx(Box, { width: keyWidth, flexShrink: 0, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text3, { color: colors.secondary, children: row.hint ?? "" }) }) : null
2738
+ ] }, `${i}:${row.label}`)
2739
+ ),
2740
+ /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(inner) }),
2741
+ /* @__PURE__ */ jsx(
2742
+ FooterHints,
2743
+ {
2744
+ hints: [
2745
+ ["\u2191\u2193", "move"],
2746
+ ["\u21B5", "run"],
2747
+ ["esc", "close"]
2748
+ ]
2749
+ }
2750
+ )
2582
2751
  ]
2583
2752
  }
2584
2753
  );
@@ -2854,6 +3023,7 @@ var BrowseScreen = ({
2854
3023
  fetchedAt,
2855
3024
  refreshError,
2856
3025
  hidden,
3026
+ overlayBody,
2857
3027
  peelRef,
2858
3028
  onTyping,
2859
3029
  onOpenPr,
@@ -3544,6 +3714,10 @@ var BrowseScreen = ({
3544
3714
  const matchCount = section.items.filter(
3545
3715
  (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
3546
3716
  ).length;
3717
+ const searchBar = searchBarState({
3718
+ typing: searchInput,
3719
+ nerd: getIconMode() === "nerd"
3720
+ });
3547
3721
  const launcher = useMemo(() => {
3548
3722
  const run = /* @__PURE__ */ new Map();
3549
3723
  if (palette === null)
@@ -3603,7 +3777,8 @@ var BrowseScreen = ({
3603
3777
  }));
3604
3778
  return { items, run, message };
3605
3779
  }, [palette, activeItem, extensions, jiraBase, jiraKeyRe, jiraSetupHint]);
3606
- if (hidden) return null;
3780
+ const chrome = useContext(InboxChromeContext);
3781
+ const overlayOpen = overlayBody != null;
3607
3782
  const overlay = palette !== null ? /* @__PURE__ */ jsx(
3608
3783
  CommandPalette,
3609
3784
  {
@@ -3648,7 +3823,8 @@ var BrowseScreen = ({
3648
3823
  {
3649
3824
  item: activeItem,
3650
3825
  actions: menu.actions,
3651
- cursor: menu.cursor
3826
+ cursor: menu.cursor,
3827
+ cols: listCols
3652
3828
  }
3653
3829
  ) : null;
3654
3830
  return /* @__PURE__ */ jsxs(
@@ -3667,7 +3843,7 @@ var BrowseScreen = ({
3667
3843
  brand,
3668
3844
  sections: localSections,
3669
3845
  login,
3670
- scopeLabel: origin?.label,
3846
+ scopeLabel: overlayOpen ? chrome?.scope ?? origin?.label : origin?.label,
3671
3847
  budgetLabel: budgetNotice(budget)?.label,
3672
3848
  budgetCritical: budgetNotice(budget)?.critical,
3673
3849
  refreshing,
@@ -3690,7 +3866,7 @@ var BrowseScreen = ({
3690
3866
  /* @__PURE__ */ jsx(Text3, { bold: true, color: colors.accent, children: " \u26A1 auto-refresh paused to save API budget" }),
3691
3867
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " r refreshes anyway" })
3692
3868
  ] }) : null,
3693
- /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
3869
+ overlayOpen ? null : /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
3694
3870
  Tabs,
3695
3871
  {
3696
3872
  active: section.id,
@@ -3723,18 +3899,24 @@ var BrowseScreen = ({
3723
3899
  }))
3724
3900
  }
3725
3901
  ) }),
3726
- search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3727
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: " / " }),
3902
+ !overlayOpen && search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3903
+ /* @__PURE__ */ jsx(Text3, { color: searchBar.glyphColor, children: ` ${searchBar.glyph} ` }),
3728
3904
  /* @__PURE__ */ jsx(Text3, { children: search }),
3729
- searchInput ? /* @__PURE__ */ jsx(Text3, { color: colors.info, children: "\u258F" }) : null,
3730
- /* @__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: [
3905
+ searchBar.caret ? /* @__PURE__ */ jsx(Text3, { color: colors.info, children: "\u258F" }) : null,
3906
+ /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""} ${searchInput ? filter.hints.map(([k, label]) => `${k} ${label}`).join(" \xB7 ") : searchBar.hints}` })
3907
+ ] }) : !overlayOpen && repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3732
3908
  /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " \u25C9 " }),
3733
3909
  /* @__PURE__ */ jsx(Text3, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
3734
3910
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " f edit \xB7 esc clear" })
3735
3911
  ] }) : null,
3736
3912
  /* @__PURE__ */ jsxs(Box, { children: [
3737
- /* @__PURE__ */ jsxs(
3913
+ overlayOpen ? (
3914
+ // The overlay owns this band: the same frame, the same header above
3915
+ // and footer below, the body where the list was. Fixed to the list's
3916
+ // own height so the frame holds still between browse and drill, and
3917
+ // clipped rather than spilling past the footer on a long body.
3918
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", height: listHeight, overflow: "hidden", children: overlayBody })
3919
+ ) : /* @__PURE__ */ jsxs(
3738
3920
  Box,
3739
3921
  {
3740
3922
  flexDirection: "column",
@@ -3794,7 +3976,7 @@ var BrowseScreen = ({
3794
3976
  ]
3795
3977
  }
3796
3978
  ),
3797
- showRail ? /* @__PURE__ */ jsx(
3979
+ showRail && !overlayOpen ? /* @__PURE__ */ jsx(
3798
3980
  SidePanel,
3799
3981
  {
3800
3982
  sidebar: rails,
@@ -3806,7 +3988,13 @@ var BrowseScreen = ({
3806
3988
  }
3807
3989
  ) : null
3808
3990
  ] }),
3809
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3991
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : overlayOpen ? (
3992
+ // The overlay's own verbs, plus the way back. `page="nested"` is what
3993
+ // appends `⌫ back` and `q quit`, so a body passes only its own keys
3994
+ // through `useChrome` — the same contract ink-ui's `Page` offers its
3995
+ // nested pages, without re-laying-out the browse frame as one.
3996
+ /* @__PURE__ */ jsx(FooterHints, { hints: chrome?.hints ?? [], page: "nested", help: false })
3997
+ ) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3810
3998
  ]
3811
3999
  }
3812
4000
  );
@@ -3873,6 +4061,15 @@ var App = ({
3873
4061
  const browsePeel = useRef(null);
3874
4062
  const drillPeel = useRef(null);
3875
4063
  const [typing, setTyping] = useState(false);
4064
+ const [chromeSpec, setChromeSpec] = useState({});
4065
+ const setChrome = useCallback(
4066
+ (next) => setChromeSpec((prev) => sameChrome(prev, next) ? prev : next),
4067
+ []
4068
+ );
4069
+ const chromeValue = useMemo(
4070
+ () => ({ scope: chromeSpec.scope, hints: chromeSpec.hints, setChrome }),
4071
+ [chromeSpec, setChrome]
4072
+ );
3876
4073
  useAppKeys({
3877
4074
  isActive: !typing,
3878
4075
  onBack: () => {
@@ -4236,7 +4433,10 @@ var App = ({
4236
4433
  extId: state.extId,
4237
4434
  target: state.target
4238
4435
  } : null;
4239
- const toBrowse = () => setState({ phase: "browse", sections: state.sections, login: state.login });
4436
+ const toBrowse = () => {
4437
+ setChromeSpec({});
4438
+ setState({ phase: "browse", sections: state.sections, login: state.login });
4439
+ };
4240
4440
  const markLeaving = (target) => {
4241
4441
  if (state.phase !== "browse") toBrowse();
4242
4442
  if (!leavingSince.current.has(target.url))
@@ -4264,82 +4464,89 @@ var App = ({
4264
4464
  );
4265
4465
  }, MERGED_HOLD_MS);
4266
4466
  };
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({
4467
+ const overlayBody = overlay?.kind === "pr" || overlay?.kind === "issue" ? detailFor?.({
4468
+ item: overlay.item,
4469
+ kind: overlay.kind,
4470
+ login: state.login,
4471
+ registerPeel: (peel) => {
4472
+ drillPeel.current = peel;
4473
+ },
4474
+ onTyping: setTyping,
4475
+ onBack: toBrowse,
4476
+ onRefresh: applyOrRefresh,
4477
+ onRemove: markLeaving,
4478
+ onMerged: markMerged
4479
+ }) : overlay?.kind === "ext" ? extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target) : null;
4480
+ return /* @__PURE__ */ jsx(InboxChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx(
4481
+ BrowseScreen,
4482
+ {
4483
+ brand: brandOf(title),
4484
+ sections: state.sections,
4485
+ login: state.login,
4486
+ jiraBase,
4487
+ jiraKeyRe,
4488
+ jiraSetupHint,
4489
+ jiraTransitions,
4490
+ onRefresh: applyOrRefresh,
4491
+ onActed,
4492
+ onSettled,
4493
+ refreshing,
4494
+ hasPending: pending !== null,
4495
+ pendingSummary,
4496
+ appliedSummary: transients.size > 0 ? appliedSummary : "",
4497
+ fetchedAt,
4498
+ refreshError: refreshError ?? void 0,
4499
+ origin,
4500
+ budget,
4501
+ skippedForBudget,
4502
+ hidden: overlay !== null,
4503
+ overlayBody: overlayBody ?? void 0,
4504
+ peelRef: browsePeel,
4505
+ onTyping: setTyping,
4506
+ mergedUrls,
4507
+ leavingUrls,
4508
+ transitSince,
4509
+ onLeave: markLeaving,
4510
+ transients,
4511
+ onTabChange: setVisibleTab,
4512
+ sidebar,
4513
+ liveLabel,
4514
+ ciStatusState: hasCiStatus ? ciStatusState : void 0,
4515
+ ciJob,
4516
+ stripState: hasStrip ? stripState : void 0,
4517
+ stripLabel,
4518
+ tabHelp,
4519
+ onOpenPr: (item) => {
4520
+ setChromeSpec({});
4521
+ setState({
4307
4522
  phase: "pr",
4308
4523
  item,
4309
4524
  sections: state.sections,
4310
4525
  login: state.login
4311
- }),
4312
- onOpenIssue: (item) => setState({
4526
+ });
4527
+ },
4528
+ onOpenIssue: (item) => {
4529
+ setChromeSpec({});
4530
+ setState({
4313
4531
  phase: "issue",
4314
4532
  item,
4315
4533
  sections: state.sections,
4316
4534
  login: state.login
4317
- }),
4318
- onOpenExt: (id, target) => setState({
4535
+ });
4536
+ },
4537
+ onOpenExt: (id, target) => {
4538
+ setChromeSpec({});
4539
+ setState({
4319
4540
  phase: "ext",
4320
4541
  extId: id,
4321
4542
  target,
4322
4543
  sections: state.sections,
4323
4544
  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;
4545
+ });
4334
4546
  },
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
- ] });
4547
+ extensions
4548
+ }
4549
+ ) });
4343
4550
  };
4344
4551
 
4345
4552
  // src/inbox/filter.ts
@@ -4356,4 +4563,4 @@ var matchesFilter = (repo, filter) => {
4356
4563
  };
4357
4564
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
4358
4565
 
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 };
4566
+ 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.68.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
  },