@kud/gh-ink 0.66.0 → 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
@@ -296,14 +296,15 @@ type TrailingShed = Partial<Record<(typeof TRAILING)[number], boolean>>;
296
296
  declare const trailingColumnsOf: (items: readonly GHItem[]) => TrailingColumns;
297
297
  /**
298
298
  * Width of the `#n` cell across these rows: the widest number plus a two-cell
299
- * gutter, left-aligned. Measured once by the list over the whole section, the
300
- * same move as `markerCols` and `trailingColumnsOf`, so the titles start on one
301
- * column and that column does not move as you scroll.
299
+ * gutter, left-aligned. The inbox measures it once over the rows of EVERY tab
300
+ * (`listColumnsOf`), the same move as `markerCols` and `trailingColumnsOf`, so
301
+ * the titles start on one column and that column moves neither as you scroll
302
+ * nor as you switch tabs. Measured per tab, it put `#1234`'s titles a column
303
+ * right of `#157`'s in the next tab, and every switch shifted the list.
302
304
  *
303
305
  * It replaced `padEnd(7)`, a constant that fitted the repos it was written
304
306
  * against and nothing else: `#31805` left a one-cell gutter where `#172` left
305
- * three, and a six-digit number would have run straight into its title. A
306
- * section of short numbers no longer pays for a long one it does not hold.
307
+ * three, and a six-digit number would have run straight into its title.
307
308
  */
308
309
  declare const numberColumnsOf: (items: readonly GHItem[]) => number;
309
310
  type LayoutInput = Pick<PrRowProps, "item" | "login" | "cols" | "prefix" | "uniformLabels" | "announcements" | "columns" | "shed" | "numberCols" | "gutter">;
@@ -416,6 +417,25 @@ declare const healthLegend: [Health, string][];
416
417
  */
417
418
  declare const truncate: (str: string, max: number) => string;
418
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;
419
439
  type ExtensionTarget = {
420
440
  item?: AnyItem;
421
441
  ciJob?: string;
@@ -437,6 +457,8 @@ interface InboxExtension {
437
457
  key: string;
438
458
  hint?: string;
439
459
  scope?: "item" | "global";
460
+ menuGroup?: "act" | "open" | "other";
461
+ icon?: string;
440
462
  drills?: AnyItem["kind"][];
441
463
  body: (onExit: () => void, target?: ExtensionTarget) => ReactNode;
442
464
  commands?: (target: ExtensionTarget) => Command[];
@@ -524,12 +546,20 @@ type SettledResult = {
524
546
  ok: false;
525
547
  reason: string;
526
548
  };
549
+ type ActionGroup = "act" | "open" | "switch" | "copy" | "quiet" | "close" | "confirm";
527
550
  type Action = {
528
551
  label: string;
529
552
  hint: string;
530
553
  run: () => void;
531
554
  subActions?: Action[];
555
+ group?: ActionGroup;
556
+ icon?: string;
532
557
  };
558
+ type MenuGap = {
559
+ gap: true;
560
+ };
561
+ type MenuRow = Action | MenuGap;
562
+ declare const isMenuGap: (row: MenuRow) => row is MenuGap;
533
563
  type JiraTransition = {
534
564
  /** What the action menu shows. Yours to word; nothing matches on it. */
535
565
  label: string;
@@ -700,8 +730,16 @@ declare const CiStatusLine: ({ state, job, }: {
700
730
  state: CiStatusState;
701
731
  job?: string;
702
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;
703
741
  declare const useActionMenu: () => {
704
- actions: Action[] | null;
742
+ actions: MenuRow[] | null;
705
743
  cursor: number;
706
744
  open: (next: Action[]) => void;
707
745
  close: () => void;
@@ -712,10 +750,17 @@ declare const useActionMenu: () => {
712
750
  escape?: boolean;
713
751
  }) => boolean;
714
752
  };
715
- declare const ActionMenu: ({ item, actions, cursor, }: {
753
+ declare const ActionMenu: ({ item, actions, cursor, cols, }: {
716
754
  item: AnyItem;
717
- actions: Action[];
755
+ actions: MenuRow[];
718
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;
719
764
  }) => React.JSX.Element;
720
765
  declare const signatureOf: (sections: Section[]) => string;
721
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, }: {
@@ -826,4 +871,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
826
871
  */
827
872
  declare const parsePatterns: (value: string) => string[];
828
873
 
829
- 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;
@@ -1531,6 +1544,13 @@ var reposInSections = (sections) => [
1531
1544
  sections.flatMap((s) => s.items).filter((i) => i.kind === "pr" || i.kind === "issue").map((i) => i.repo)
1532
1545
  )
1533
1546
  ].sort();
1547
+ var listColumnsOf = (sections) => {
1548
+ const rows = sections.flatMap((s) => s.items).filter((i) => i.kind === "pr" || i.kind === "issue");
1549
+ return {
1550
+ numberCols: numberColumnsOf(rows),
1551
+ trailingColumns: trailingColumnsOf(rows)
1552
+ };
1553
+ };
1534
1554
  var moveCursor = (items, current, dir) => {
1535
1555
  const next = current + dir;
1536
1556
  if (next < 0 || next >= items.length) return current;
@@ -1891,6 +1911,14 @@ var drillLabel = (item) => {
1891
1911
  if (item.kind === "pr") return "Open PR";
1892
1912
  return "Drill in";
1893
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
+ };
1894
1922
  var UNSUBSCRIBE_MUTATION = `
1895
1923
  mutation($id: ID!) {
1896
1924
  updateSubscription(input: { subscribableId: $id, state: UNSUBSCRIBED }) {
@@ -1941,6 +1969,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1941
1969
  const open = {
1942
1970
  label: "Open in browser",
1943
1971
  hint: "o",
1972
+ group: "open",
1973
+ icon: "\uF465",
1944
1974
  run: () => {
1945
1975
  quietly`open ${item.url}`.catch(() => {
1946
1976
  });
@@ -1950,6 +1980,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1950
1980
  const copyUrl = {
1951
1981
  label: "Copy URL",
1952
1982
  hint: "c",
1983
+ group: "copy",
1984
+ icon: "\uF44C",
1953
1985
  run: () => {
1954
1986
  clipboard(item.url);
1955
1987
  const label = item.kind === "task" ? item.key : `#${item.number}`;
@@ -1961,6 +1993,11 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1961
1993
  const drillAction = drill || mountable && onOpenView ? {
1962
1994
  label: drillLabel(item),
1963
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,
1964
2001
  run: () => {
1965
2002
  if (onOpenView?.(item)) return;
1966
2003
  if (drill) {
@@ -1976,6 +2013,10 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1976
2013
  base.push({
1977
2014
  label: "Move status",
1978
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",
1979
2020
  run: () => {
1980
2021
  },
1981
2022
  subActions: jiraTransitions.map(
@@ -2009,21 +2050,40 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2009
2050
  )
2010
2051
  });
2011
2052
  }
2012
- return base;
2053
+ return byGroup(base);
2013
2054
  }
2014
2055
  const actions = drillAction ? [drillAction, open, copyUrl] : [open, copyUrl];
2056
+ const closing = [];
2015
2057
  actions.push({
2016
2058
  label: "Copy repo name",
2017
2059
  hint: "r",
2060
+ group: "copy",
2061
+ icon: "\uF401",
2018
2062
  run: () => {
2019
2063
  clipboard(item.repo);
2020
2064
  showFlash(`\u2713 Copied ${item.repo}`);
2021
2065
  }
2022
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
+ });
2023
2077
  if (item.kind === "pr" && item.standing === "queued" && login) {
2024
2078
  actions.push({
2025
2079
  label: "Remove me as reviewer",
2026
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",
2027
2087
  run: () => {
2028
2088
  onRemove?.(item);
2029
2089
  showFlash(`\u22EF Removing you from #${item.number}\u2026`);
@@ -2045,18 +2105,12 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2045
2105
  }
2046
2106
  });
2047
2107
  }
2048
- actions.push({
2049
- label: "Unsubscribe",
2050
- hint: "u",
2051
- run: () => {
2052
- showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
2053
- void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch((e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`));
2054
- }
2055
- });
2056
2108
  if (item.kind === "pr" && item.branch) {
2057
2109
  actions.push({
2058
2110
  label: "Copy branch name",
2059
2111
  hint: "b",
2112
+ group: "copy",
2113
+ icon: "\uF418",
2060
2114
  run: () => {
2061
2115
  clipboard(item.branch);
2062
2116
  showFlash(`\u2713 Copied ${item.branch}`);
@@ -2067,6 +2121,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2067
2121
  actions.push({
2068
2122
  label: "Switch here",
2069
2123
  hint: "s",
2124
+ group: "switch",
2125
+ icon: "\uEBCB",
2070
2126
  run: () => {
2071
2127
  const script = [
2072
2128
  "delay 0.5",
@@ -2087,16 +2143,20 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2087
2143
  }
2088
2144
  if (item.kind === "issue") {
2089
2145
  actions.push({
2090
- label: "Open project in new tab",
2146
+ label: "Open in new tab",
2091
2147
  hint: "j",
2148
+ group: "switch",
2149
+ icon: "\uEAE4",
2092
2150
  run: () => {
2093
2151
  showFlash(`\u22EF Opening ${item.repo}\u2026`);
2094
2152
  void jumpToRepo(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
2095
2153
  }
2096
2154
  });
2097
2155
  actions.push({
2098
- label: "Open project in new pane",
2156
+ label: "Open in new pane",
2099
2157
  hint: "p",
2158
+ group: "switch",
2159
+ icon: "\uEB56",
2100
2160
  run: () => {
2101
2161
  showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
2102
2162
  void jumpToRepoPane(item.repo, "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
@@ -2105,12 +2165,15 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2105
2165
  actions.push({
2106
2166
  label: "Close issue",
2107
2167
  hint: "",
2168
+ group: "close",
2169
+ icon: "\uF41D",
2108
2170
  run: () => {
2109
2171
  },
2110
2172
  subActions: [
2111
2173
  {
2112
2174
  label: `Close #${item.number}`,
2113
2175
  hint: "",
2176
+ group: "confirm",
2114
2177
  run: () => {
2115
2178
  onRemove?.(item);
2116
2179
  showFlash(`\u2713 Closed #${item.number}`);
@@ -2129,7 +2192,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2129
2192
  );
2130
2193
  }
2131
2194
  },
2132
- { label: "Cancel", hint: "", run: () => {
2195
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2133
2196
  } }
2134
2197
  ]
2135
2198
  });
@@ -2138,6 +2201,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2138
2201
  actions.push({
2139
2202
  label: "Switch in new tab",
2140
2203
  hint: "j",
2204
+ group: "switch",
2205
+ icon: "\uEAE4",
2141
2206
  run: () => {
2142
2207
  showFlash(`\u22EF Jumping to ${item.repo}\u2026`);
2143
2208
  void jumpToRepo(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new tab`)).catch(() => showFlash("\u2717 Jump failed"));
@@ -2146,6 +2211,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2146
2211
  actions.push({
2147
2212
  label: "Switch in new pane",
2148
2213
  hint: "p",
2214
+ group: "switch",
2215
+ icon: "\uEB56",
2149
2216
  run: () => {
2150
2217
  showFlash(`\u22EF Opening pane for ${item.repo}\u2026`);
2151
2218
  void jumpToRepoPane(item.repo, item.branch ?? "", login).then(() => showFlash(`\u2197 Opened ${item.repo} in new pane`)).catch(() => showFlash("\u2717 Pane failed"));
@@ -2157,19 +2224,23 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2157
2224
  actions.push({
2158
2225
  label: `Open ${jiraKey} in Jira`,
2159
2226
  hint: "t",
2227
+ group: "open",
2160
2228
  run: () => openInJira(jiraBase, jiraKey, showFlash)
2161
2229
  });
2162
2230
  }
2163
2231
  }
2164
- actions.push({
2232
+ closing.push({
2165
2233
  label: "Close PR",
2166
2234
  hint: "",
2235
+ group: "close",
2236
+ icon: "\uF4DC",
2167
2237
  run: () => {
2168
2238
  },
2169
2239
  subActions: [
2170
2240
  {
2171
2241
  label: `Close #${item.number}`,
2172
2242
  hint: "",
2243
+ group: "confirm",
2173
2244
  run: () => {
2174
2245
  onRemove?.(item);
2175
2246
  showFlash(`\u2713 Closed #${item.number}`);
@@ -2188,20 +2259,25 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2188
2259
  );
2189
2260
  }
2190
2261
  },
2191
- { label: "Cancel", hint: "", run: () => {
2262
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2192
2263
  } }
2193
2264
  ]
2194
2265
  });
2195
2266
  if (item.branch) {
2196
- actions.push({
2197
- label: "Close PR + Delete branch",
2267
+ closing.push({
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",
2198
2271
  hint: "",
2272
+ group: "close",
2273
+ icon: "\uF48E",
2199
2274
  run: () => {
2200
2275
  },
2201
2276
  subActions: [
2202
2277
  {
2203
- label: `Close #${item.number} + delete ${item.branch}`,
2278
+ label: `Close #${item.number} and delete ${item.branch}`,
2204
2279
  hint: "",
2280
+ group: "confirm",
2205
2281
  run: () => {
2206
2282
  onRemove?.(item);
2207
2283
  showFlash(`\u2713 Closed #${item.number} and deleted ${item.branch}`);
@@ -2213,16 +2289,20 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2213
2289
  });
2214
2290
  }
2215
2291
  },
2216
- { label: "Cancel", hint: "", run: () => {
2292
+ { label: "Cancel", hint: "", group: "confirm", run: () => {
2217
2293
  } }
2218
2294
  ]
2219
2295
  });
2220
2296
  }
2221
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";
2222
2300
  for (const e of itemExtensions(ext?.extensions))
2223
2301
  actions.push({
2224
- label: e.title,
2302
+ label: extensionLabel(e),
2225
2303
  hint: e.key,
2304
+ group: extensionGroup(e),
2305
+ icon: e.icon,
2226
2306
  run: () => ext?.onOpenExt?.(e.id, {
2227
2307
  item,
2228
2308
  login,
@@ -2230,7 +2310,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2230
2310
  showFlash
2231
2311
  })
2232
2312
  });
2233
- return actions;
2313
+ actions.push(...closing);
2314
+ return byGroup(actions);
2234
2315
  };
2235
2316
  var agoText = (ms) => {
2236
2317
  const d = (Date.now() - ms) / 1e3;
@@ -2517,26 +2598,60 @@ var ItemRow = ({
2517
2598
  }
2518
2599
  );
2519
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;
2520
2623
  var useActionMenu = () => {
2521
2624
  const [actions, setActions] = useState(null);
2522
2625
  const [cursor, setCursor] = useState(0);
2523
2626
  const open = (next) => {
2524
2627
  if (next.length === 0) return;
2525
2628
  setCursor(0);
2526
- setActions(next);
2629
+ setActions(menuRowsOf(next));
2527
2630
  };
2528
2631
  const handleKey = (key) => {
2529
2632
  if (!actions) return false;
2530
- if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
2531
- 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
+ });
2532
2645
  if (key.return) {
2533
- const action = actions[cursor];
2534
- if (action?.subActions) {
2535
- setCursor(0);
2536
- 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);
2537
2652
  } else {
2538
2653
  setActions(null);
2539
- action?.run();
2654
+ row.run();
2540
2655
  }
2541
2656
  }
2542
2657
  if (key.escape) setActions(null);
@@ -2547,29 +2662,81 @@ var useActionMenu = () => {
2547
2662
  var ActionMenu = ({
2548
2663
  item,
2549
2664
  actions,
2550
- cursor
2665
+ cursor,
2666
+ cols = COLS
2551
2667
  }) => {
2552
- 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";
2553
2696
  return /* @__PURE__ */ jsxs(
2554
2697
  Box,
2555
2698
  {
2556
2699
  flexDirection: "column",
2557
2700
  flexShrink: 0,
2701
+ width,
2558
2702
  borderStyle: "round",
2559
2703
  borderColor: colors.info,
2560
2704
  backgroundColor: OVERLAY_BG,
2561
2705
  paddingX: 1,
2562
2706
  marginTop: 1,
2563
2707
  children: [
2564
- /* @__PURE__ */ jsx(Text3, { color: colors.info, bold: true, children: title }),
2565
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(32) }),
2566
- actions.map((a, i) => /* @__PURE__ */ jsxs(Box, { children: [
2567
- /* @__PURE__ */ jsx(Text3, { color: colors.info, children: i === cursor ? "\u276F " : " " }),
2568
- /* @__PURE__ */ jsx(Text3, { bold: i === cursor, children: a.label }),
2569
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + a.hint })
2570
- ] }, a.label)),
2571
- /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "\u2500".repeat(32) }),
2572
- /* @__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
+ )
2573
2740
  ]
2574
2741
  }
2575
2742
  );
@@ -2845,6 +3012,7 @@ var BrowseScreen = ({
2845
3012
  fetchedAt,
2846
3013
  refreshError,
2847
3014
  hidden,
3015
+ overlayBody,
2848
3016
  peelRef,
2849
3017
  onTyping,
2850
3018
  onOpenPr,
@@ -3023,8 +3191,7 @@ var BrowseScreen = ({
3023
3191
  }, [searchInput, palette, onTyping]);
3024
3192
  const railCols = railWidth(COLS);
3025
3193
  const listCols = showRail ? COLS - railCols : COLS;
3026
- const trailingColumns = trailingColumnsOf(labelBearing);
3027
- const numberCols = numberColumnsOf(labelBearing);
3194
+ const { trailingColumns, numberCols } = listColumnsOf(localSections);
3028
3195
  const rowCols = Math.min(listCols, ROW_MAX_COLS);
3029
3196
  const trailingShed = sectionShedOf(
3030
3197
  section.items.flatMap(
@@ -3595,7 +3762,8 @@ var BrowseScreen = ({
3595
3762
  }));
3596
3763
  return { items, run, message };
3597
3764
  }, [palette, activeItem, extensions, jiraBase, jiraKeyRe, jiraSetupHint]);
3598
- if (hidden) return null;
3765
+ const chrome = useContext(InboxChromeContext);
3766
+ const overlayOpen = overlayBody != null;
3599
3767
  const overlay = palette !== null ? /* @__PURE__ */ jsx(
3600
3768
  CommandPalette,
3601
3769
  {
@@ -3640,7 +3808,8 @@ var BrowseScreen = ({
3640
3808
  {
3641
3809
  item: activeItem,
3642
3810
  actions: menu.actions,
3643
- cursor: menu.cursor
3811
+ cursor: menu.cursor,
3812
+ cols: listCols
3644
3813
  }
3645
3814
  ) : null;
3646
3815
  return /* @__PURE__ */ jsxs(
@@ -3659,7 +3828,7 @@ var BrowseScreen = ({
3659
3828
  brand,
3660
3829
  sections: localSections,
3661
3830
  login,
3662
- scopeLabel: origin?.label,
3831
+ scopeLabel: overlayOpen ? chrome?.scope ?? origin?.label : origin?.label,
3663
3832
  budgetLabel: budgetNotice(budget)?.label,
3664
3833
  budgetCritical: budgetNotice(budget)?.critical,
3665
3834
  refreshing,
@@ -3682,7 +3851,7 @@ var BrowseScreen = ({
3682
3851
  /* @__PURE__ */ jsx(Text3, { bold: true, color: colors.accent, children: " \u26A1 auto-refresh paused to save API budget" }),
3683
3852
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " r refreshes anyway" })
3684
3853
  ] }) : null,
3685
- /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
3854
+ overlayOpen ? null : /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
3686
3855
  Tabs,
3687
3856
  {
3688
3857
  active: section.id,
@@ -3715,18 +3884,24 @@ var BrowseScreen = ({
3715
3884
  }))
3716
3885
  }
3717
3886
  ) }),
3718
- search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3887
+ !overlayOpen && search != null ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3719
3888
  /* @__PURE__ */ jsx(Text3, { color: colors.info, children: " / " }),
3720
3889
  /* @__PURE__ */ jsx(Text3, { children: search }),
3721
3890
  searchInput ? /* @__PURE__ */ jsx(Text3, { color: colors.info, children: "\u258F" }) : null,
3722
3891
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${matchCount} match${matchCount !== 1 ? "es" : ""} ${filter.hints.map(([k, label]) => `${k} ${label}`).join(" \xB7 ")}` })
3723
- ] }) : repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3892
+ ] }) : !overlayOpen && repoFilter.size > 0 ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
3724
3893
  /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: " \u25C9 " }),
3725
3894
  /* @__PURE__ */ jsx(Text3, { children: `${repoFilter.size} repo${repoFilter.size !== 1 ? "s" : ""}` }),
3726
3895
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " f edit \xB7 esc clear" })
3727
3896
  ] }) : null,
3728
3897
  /* @__PURE__ */ jsxs(Box, { children: [
3729
- /* @__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(
3730
3905
  Box,
3731
3906
  {
3732
3907
  flexDirection: "column",
@@ -3786,7 +3961,7 @@ var BrowseScreen = ({
3786
3961
  ]
3787
3962
  }
3788
3963
  ),
3789
- showRail ? /* @__PURE__ */ jsx(
3964
+ showRail && !overlayOpen ? /* @__PURE__ */ jsx(
3790
3965
  SidePanel,
3791
3966
  {
3792
3967
  sidebar: rails,
@@ -3798,7 +3973,20 @@ var BrowseScreen = ({
3798
3973
  }
3799
3974
  ) : null
3800
3975
  ] }),
3801
- /* @__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 }) })
3802
3990
  ]
3803
3991
  }
3804
3992
  );
@@ -3865,6 +4053,15 @@ var App = ({
3865
4053
  const browsePeel = useRef(null);
3866
4054
  const drillPeel = useRef(null);
3867
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
+ );
3868
4065
  useAppKeys({
3869
4066
  isActive: !typing,
3870
4067
  onBack: () => {
@@ -4228,7 +4425,10 @@ var App = ({
4228
4425
  extId: state.extId,
4229
4426
  target: state.target
4230
4427
  } : null;
4231
- 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
+ };
4232
4432
  const markLeaving = (target) => {
4233
4433
  if (state.phase !== "browse") toBrowse();
4234
4434
  if (!leavingSince.current.has(target.url))
@@ -4256,82 +4456,89 @@ var App = ({
4256
4456
  );
4257
4457
  }, MERGED_HOLD_MS);
4258
4458
  };
4259
- return /* @__PURE__ */ jsxs(Fragment, { children: [
4260
- /* @__PURE__ */ jsx(
4261
- BrowseScreen,
4262
- {
4263
- brand: brandOf(title),
4264
- sections: state.sections,
4265
- login: state.login,
4266
- jiraBase,
4267
- jiraKeyRe,
4268
- jiraSetupHint,
4269
- jiraTransitions,
4270
- onRefresh: applyOrRefresh,
4271
- onActed,
4272
- onSettled,
4273
- refreshing,
4274
- hasPending: pending !== null,
4275
- pendingSummary,
4276
- appliedSummary: transients.size > 0 ? appliedSummary : "",
4277
- fetchedAt,
4278
- refreshError: refreshError ?? void 0,
4279
- origin,
4280
- budget,
4281
- skippedForBudget,
4282
- hidden: overlay !== null,
4283
- peelRef: browsePeel,
4284
- onTyping: setTyping,
4285
- mergedUrls,
4286
- leavingUrls,
4287
- transitSince,
4288
- onLeave: markLeaving,
4289
- transients,
4290
- onTabChange: setVisibleTab,
4291
- sidebar,
4292
- liveLabel,
4293
- ciStatusState: hasCiStatus ? ciStatusState : void 0,
4294
- ciJob,
4295
- stripState: hasStrip ? stripState : void 0,
4296
- stripLabel,
4297
- tabHelp,
4298
- 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({
4299
4514
  phase: "pr",
4300
4515
  item,
4301
4516
  sections: state.sections,
4302
4517
  login: state.login
4303
- }),
4304
- onOpenIssue: (item) => setState({
4518
+ });
4519
+ },
4520
+ onOpenIssue: (item) => {
4521
+ setChromeSpec({});
4522
+ setState({
4305
4523
  phase: "issue",
4306
4524
  item,
4307
4525
  sections: state.sections,
4308
4526
  login: state.login
4309
- }),
4310
- onOpenExt: (id, target) => setState({
4527
+ });
4528
+ },
4529
+ onOpenExt: (id, target) => {
4530
+ setChromeSpec({});
4531
+ setState({
4311
4532
  phase: "ext",
4312
4533
  extId: id,
4313
4534
  target,
4314
4535
  sections: state.sections,
4315
4536
  login: state.login
4316
- }),
4317
- extensions
4318
- }
4319
- ),
4320
- (overlay?.kind === "pr" || overlay?.kind === "issue") && detailFor?.({
4321
- item: overlay.item,
4322
- kind: overlay.kind,
4323
- login: state.login,
4324
- registerPeel: (peel) => {
4325
- drillPeel.current = peel;
4537
+ });
4326
4538
  },
4327
- onTyping: setTyping,
4328
- onBack: toBrowse,
4329
- onRefresh: applyOrRefresh,
4330
- onRemove: markLeaving,
4331
- onMerged: markMerged
4332
- }),
4333
- overlay?.kind === "ext" && extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target)
4334
- ] });
4539
+ extensions
4540
+ }
4541
+ ) });
4335
4542
  };
4336
4543
 
4337
4544
  // src/inbox/filter.ts
@@ -4348,4 +4555,4 @@ var matchesFilter = (repo, filter) => {
4348
4555
  };
4349
4556
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
4350
4557
 
4351
- 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.0",
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
  },