@kud/gh-ink 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -201,7 +201,38 @@ declare const insertRepoHeaders: (items: GHItem[]) => AnyItem[];
201
201
  type Standing = "authored" | "queued" | "spoken";
202
202
  declare const whoseMove: (health: Health, sectionId: string, standing?: Standing) => "you" | "them";
203
203
  declare const layoutGHItems: (items: GHItem[], sectionId: string) => AnyItem[];
204
- declare const filterByOrigin: (sections: Section[], keep: "work" | "home", isWorkRepo: (repo: string) => boolean) => Section[];
204
+ /**
205
+ * A two-way repo split, when a host has one. `undefined` — the ordinary case —
206
+ * means no split at all: every row stands.
207
+ *
208
+ * The package deliberately does not NAME the sides. It hardcoded "work" and
209
+ * "home" until 2026-08-27: one reader's two lives compiled into a library
210
+ * anyone can install, printed in a header where no other host would recognise
211
+ * the word. The predicate, the side, and the word are all yours.
212
+ *
213
+ * Chosen once, by the host, before the first paint. There is no in-app toggle,
214
+ * because a key that flipped it could only ever put the inbox out of step with
215
+ * the scope the command was started in, with nothing on screen to explain the
216
+ * disagreement.
217
+ */
218
+ type OriginSplit = {
219
+ /** Rows whose repo this matches are one side; everything else is the other. */
220
+ match: (repo: string) => boolean;
221
+ /** Which side to show. */
222
+ show: "matched" | "rest";
223
+ /** What to call the visible side in the header. Omitted, nothing is shown. */
224
+ label?: string;
225
+ };
226
+ /**
227
+ * Keep only one side of a two-way repo split, leaving non-GH rows (task)
228
+ * untouched. Mirrors filterByRepos' gh/other split.
229
+ *
230
+ * `keep` is positional — "the ones that matched" or "the rest" — and never a
231
+ * name. This took the literal strings "work" and "home" until 2026-08-27, which
232
+ * is one reader's life compiled into a published library: no other host has
233
+ * those two categories, and several have none at all.
234
+ */
235
+ declare const filterByOrigin: (sections: Section[], keep: "matched" | "rest", match: (repo: string) => boolean) => Section[];
205
236
  declare const filterBySearch: (sections: Section[], query: string) => Section[];
206
237
  declare const filterByRepos: (sections: Section[], repos: Set<string>) => Section[];
207
238
  declare const withoutItem: (sections: Section[], target: GHItem) => Section[];
@@ -268,7 +299,8 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
268
299
  actions: Action[];
269
300
  cursor: number;
270
301
  }) => React.JSX.Element;
271
- declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, includeWork, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, extensions, tabHelp, emptyHint, }: {
302
+ declare const signatureOf: (sections: Section[]) => string;
303
+ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, extensions, tabHelp, emptyHint, }: {
272
304
  fetcher: () => Promise<{
273
305
  sections: Section[];
274
306
  login: string;
@@ -277,17 +309,8 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, isWorkRepo, includeWo
277
309
  cacheKey?: string;
278
310
  title?: string;
279
311
  detailFor?: (ctx: DetailContext) => React.ReactNode;
280
- isWorkRepo?: (repo: string) => boolean;
281
- /**
282
- * Which side of the split to show, chosen ONCE by the host at launch — there
283
- * is no in-app toggle. `undefined` means no split: every row stands.
284
- *
285
- * The host decides from where the command was run, so the answer is settled
286
- * before the first paint and cannot change under the reader. A `w` key used to
287
- * flip it mid-session, which meant the inbox could disagree with the scope the
288
- * command had been started in, with nothing on screen explaining why.
289
- */
290
- includeWork?: boolean;
312
+ /** A two-way repo split, and what to call the side on screen. See OriginSplit. */
313
+ origin?: OriginSplit;
291
314
  tabHelp?: [string, string][];
292
315
  emptyHint?: string;
293
316
  jiraBase?: string;
@@ -406,4 +429,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
406
429
  */
407
430
  declare const parsePatterns: (value: string) => string[];
408
431
 
409
- export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxConfig, type InboxExtension, type JiraTransition, type RepoFilter, type RepoHeader, type RepoProfile, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
432
+ export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxConfig, type InboxExtension, type JiraTransition, type OriginSplit, type RepoFilter, type RepoHeader, type RepoProfile, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/dist/index.js CHANGED
@@ -915,7 +915,7 @@ var STANDING = {
915
915
  reviewed: "spoken"
916
916
  };
917
917
  var YOURS = {
918
- authored: ["ci-fail", "conflict", "changes-req", "threads", "approved"],
918
+ authored: ["ci-fail", "conflict", "changes-req", "threads", "approved", "draft"],
919
919
  queued: ["waiting", "pending", "threads", "approved"],
920
920
  spoken: ["threads"]
921
921
  };
@@ -945,9 +945,9 @@ var layoutGHItems = (items, sectionId) => {
945
945
  ...insertRepoHeaders(rows)
946
946
  ]);
947
947
  };
948
- var filterByOrigin = (sections, keep, isWorkRepo) => sections.map((s) => {
948
+ var filterByOrigin = (sections, keep, match) => sections.map((s) => {
949
949
  const kept = s.items.filter(
950
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && (i.kind === "pr" || i.kind === "issue" ? keep === "work" ? isWorkRepo(i.repo) : !isWorkRepo(i.repo) : true)
950
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && (i.kind === "pr" || i.kind === "issue" ? keep === "matched" ? match(i.repo) : !match(i.repo) : true)
951
951
  );
952
952
  const gh = kept.filter(
953
953
  (i) => i.kind === "pr" || i.kind === "issue"
@@ -1384,6 +1384,23 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
1384
1384
  showFlash(`\u2713 Copied ${item.repo}`);
1385
1385
  }
1386
1386
  });
1387
+ if (item.kind === "pr" && item.standing === "queued" && login) {
1388
+ actions.push({
1389
+ label: "Remove me as reviewer",
1390
+ hint: "x",
1391
+ run: () => {
1392
+ onRemove?.(item);
1393
+ showFlash(`\u22EF Removing you from #${item.number}\u2026`);
1394
+ void quietly`gh pr edit ${item.number} --repo ${item.repo} --remove-reviewer ${login}`.then(() => {
1395
+ showFlash(`\u2713 Removed you as reviewer on #${item.number}`);
1396
+ ext?.onActed?.();
1397
+ }).catch(() => {
1398
+ showFlash(`\u2717 Could not remove you from #${item.number}`);
1399
+ onRefresh?.();
1400
+ });
1401
+ }
1402
+ });
1403
+ }
1387
1404
  actions.push({
1388
1405
  label: "Unsubscribe",
1389
1406
  hint: "u",
@@ -1568,7 +1585,7 @@ var InboxHeader = ({
1568
1585
  sections,
1569
1586
  login,
1570
1587
  brand,
1571
- work,
1588
+ scopeLabel,
1572
1589
  loading,
1573
1590
  quiet,
1574
1591
  refreshing,
@@ -1579,7 +1596,7 @@ var InboxHeader = ({
1579
1596
  const total = sections.reduce((n, s) => n + topLevelCount(s), 0);
1580
1597
  const countSeg = loading ? " loading\u2026 " : quiet ? " " : ` ${String(total).padStart(3)} item${total !== 1 ? "s" : ""} \xB7 `;
1581
1598
  const userSeg = loading || quiet ? "" : `@${login} `;
1582
- const workLabel = work === void 0 ? "" : ` ${work ? "work" : "home"} `;
1599
+ const workLabel = scopeLabel ? ` ${scopeLabel} ` : "";
1583
1600
  const [statusText, statusColor] = hasPending ? [`\u25CF ${pendingSummary || "new"} \xB7 r apply`, "#FF8700"] : refreshing ? ["\u21BB refreshing\u2026", "cyan"] : fetchedAt ? [`updated ${agoText(fetchedAt)}`, void 0] : ["", void 0];
1584
1601
  const statusSeg = statusText ? statusText + " " : "";
1585
1602
  const fill = Math.max(
@@ -1590,7 +1607,7 @@ var InboxHeader = ({
1590
1607
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: true, children: brand }),
1591
1608
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: countSeg }),
1592
1609
  userSeg ? /* @__PURE__ */ jsx(Text2, { children: userSeg }) : null,
1593
- work !== void 0 ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: workLabel }) : null,
1610
+ scopeLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: workLabel }) : null,
1594
1611
  statusText ? /* @__PURE__ */ jsx(
1595
1612
  Text2,
1596
1613
  {
@@ -1668,6 +1685,7 @@ var TRANSIT_LABEL = {
1668
1685
  };
1669
1686
  var TAB_MARK = "\u25CF";
1670
1687
  var tabLabel = (label, marked, id) => marked.size === 0 ? label : `${marked.has(id) ? TAB_MARK : " "} ${label}`;
1688
+ var isChildRow = (item) => !!item && (item.kind === "show-more" || item.kind === "show-less" || "indent" in item && item.indent === true);
1671
1689
  var ItemRow = ({
1672
1690
  item,
1673
1691
  active,
@@ -1675,6 +1693,8 @@ var ItemRow = ({
1675
1693
  login,
1676
1694
  merged,
1677
1695
  transient,
1696
+ lastChild,
1697
+ parent,
1678
1698
  sparkFrame = 0
1679
1699
  }) => {
1680
1700
  if (item.kind === "repo-header")
@@ -1709,6 +1729,7 @@ var ItemRow = ({
1709
1729
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1710
1730
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1711
1731
  /* @__PURE__ */ jsx(Text2, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
1732
+ /* @__PURE__ */ jsx(Text2, { dimColor: true, children: parent ? "\u252C " : " " }),
1712
1733
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: active, children: item.key + " " }),
1713
1734
  /* @__PURE__ */ jsx(
1714
1735
  Text2,
@@ -1746,7 +1767,7 @@ var ItemRow = ({
1746
1767
  const titleMax = Math.max(20, COLS - fixedWidth);
1747
1768
  return /* @__PURE__ */ jsxs(Box, { children: [
1748
1769
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1749
- item.indent ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: "\u2514\u2500 " }) : null,
1770
+ item.indent ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: lastChild === false ? "\u251C\u2500 " : "\u2514\u2500 " }) : null,
1750
1771
  /* @__PURE__ */ jsx(Text2, { color, bold: true, children: icon + " " }),
1751
1772
  /* @__PURE__ */ jsx(Text2, { color: turnColor, bold: turnIcon === "\u2190", children: turnIcon + " " }),
1752
1773
  /* @__PURE__ */ jsx(Text2, { color: "#FF8700", children: numStr }),
@@ -1901,6 +1922,7 @@ var HelpModal = ({
1901
1922
  ["j", "open repo in new tab"],
1902
1923
  ["p", "open repo in new pane"],
1903
1924
  ["u", "unsubscribe from this item"],
1925
+ ["x", "remove yourself as reviewer"],
1904
1926
  ...hasJira ? [["t", "Jira: move / open ticket"]] : [],
1905
1927
  ["/", "search"],
1906
1928
  ["f", "filter by repo"],
@@ -2089,15 +2111,14 @@ var BrowseScreen = ({
2089
2111
  ciStatusState,
2090
2112
  ciJob,
2091
2113
  tabHelp,
2092
- isWorkRepo,
2093
- includeWork,
2114
+ origin,
2094
2115
  brand,
2095
2116
  mergedUrls,
2096
2117
  transients,
2097
2118
  onTabChange
2098
2119
  }) => {
2099
2120
  const { rows } = useWindowSize();
2100
- const applyWork = (secs) => includeWork === void 0 || !isWorkRepo ? secs : filterByOrigin(secs, includeWork ? "work" : "home", isWorkRepo);
2121
+ const applyWork = (secs) => origin ? filterByOrigin(secs, origin.show, origin.match) : secs;
2101
2122
  const initialSections = applyWork(sections);
2102
2123
  const [localSections, setLocalSections] = useState(initialSections);
2103
2124
  const [tabIdx, setTabIdx] = useState(0);
@@ -2423,6 +2444,18 @@ var BrowseScreen = ({
2423
2444
  void unsubscribeFrom(activeItem).then(() => showFlash(`\u2713 Unsubscribed from #${activeItem.number}`)).catch(() => showFlash(`\u2717 Unsubscribe failed for #${activeItem.number}`));
2424
2445
  return;
2425
2446
  }
2447
+ if (input === "x" && activeItem.kind === "pr" && activeItem.standing === "queued" && login) {
2448
+ removeItemFromSections(activeItem);
2449
+ showFlash(`\u22EF Removing you from #${activeItem.number}\u2026`);
2450
+ void quietly`gh pr edit ${activeItem.number} --repo ${activeItem.repo} --remove-reviewer ${login}`.then(() => {
2451
+ showFlash(`\u2713 Removed you as reviewer on #${activeItem.number}`);
2452
+ onActed?.();
2453
+ }).catch(() => {
2454
+ showFlash(`\u2717 Could not remove you from #${activeItem.number}`);
2455
+ onRefresh?.();
2456
+ });
2457
+ return;
2458
+ }
2426
2459
  if (input === "b" && activeItem.kind === "pr" && activeItem.branch) {
2427
2460
  clipboard(activeItem.branch);
2428
2461
  showFlash(`\u2713 Copied ${activeItem.branch}`);
@@ -2535,7 +2568,7 @@ var BrowseScreen = ({
2535
2568
  brand,
2536
2569
  sections: localSections,
2537
2570
  login,
2538
- work: includeWork,
2571
+ scopeLabel: origin?.label,
2539
2572
  refreshing,
2540
2573
  hasPending,
2541
2574
  pendingSummary,
@@ -2574,6 +2607,8 @@ var BrowseScreen = ({
2574
2607
  login,
2575
2608
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2576
2609
  transient: transientOf(transients, item),
2610
+ lastChild: "indent" in item && item.indent ? !isChildRow(section.items[viewStart + i + 1]) : void 0,
2611
+ parent: item.kind === "task" && isChildRow(section.items[viewStart + i + 1]),
2577
2612
  sparkFrame,
2578
2613
  gap: (
2579
2614
  // Window-relative, never `viewStart + i`. fitCount prices the
@@ -2640,14 +2675,17 @@ var NoRowsScreen = ({
2640
2675
  ) })
2641
2676
  ] });
2642
2677
  };
2643
- var signatureOf = (sections) => JSON.stringify(sections, (k, v) => k === "age" ? void 0 : v);
2678
+ var RELATIVE_TIME_KEYS = /* @__PURE__ */ new Set(["age", "activityAge"]);
2679
+ var signatureOf = (sections) => JSON.stringify(
2680
+ sections,
2681
+ (k, v) => RELATIVE_TIME_KEYS.has(k) ? void 0 : v
2682
+ );
2644
2683
  var App = ({
2645
2684
  fetcher,
2646
2685
  cacheKey,
2647
2686
  title = "inbox",
2648
2687
  detailFor,
2649
- isWorkRepo,
2650
- includeWork,
2688
+ origin,
2651
2689
  jiraBase,
2652
2690
  jiraKeyRe,
2653
2691
  jiraTransitions,
@@ -2762,7 +2800,13 @@ var App = ({
2762
2800
  }
2763
2801
  showData(fresh.sections, fresh.login);
2764
2802
  } else if (freshKey !== displayedKey.current) {
2765
- setPending(fresh);
2803
+ const { counts } = diffSections(
2804
+ displayedSections.current,
2805
+ fresh.sections
2806
+ );
2807
+ if (counts.added + counts.removed + counts.changed === 0)
2808
+ showData(fresh.sections, fresh.login);
2809
+ else setPending(fresh);
2766
2810
  } else {
2767
2811
  setPending(null);
2768
2812
  }
@@ -2859,7 +2903,7 @@ var App = ({
2859
2903
  brand: brandOf(title),
2860
2904
  sections: [],
2861
2905
  login: "",
2862
- work: state.phase === "loading" ? includeWork : void 0,
2906
+ scopeLabel: state.phase === "loading" ? origin?.label : void 0,
2863
2907
  loading: state.phase === "loading",
2864
2908
  quiet: state.phase !== "loading"
2865
2909
  }
@@ -2918,8 +2962,7 @@ var App = ({
2918
2962
  pendingSummary,
2919
2963
  fetchedAt,
2920
2964
  refreshError: refreshError ?? void 0,
2921
- isWorkRepo,
2922
- includeWork,
2965
+ origin,
2923
2966
  hidden: overlay !== null,
2924
2967
  mergedUrls,
2925
2968
  transients,
@@ -2976,4 +3019,4 @@ var matchesFilter = (repo, filter) => {
2976
3019
  };
2977
3020
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
2978
3021
 
2979
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
3022
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.22.0",
3
+ "version": "0.23.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",