@kud/gh-ink 0.23.2 → 0.24.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
@@ -199,8 +199,8 @@ declare const sortItems: (items: GHItem[]) => GHItem[];
199
199
  declare const sortByRecency: (items: GHItem[]) => GHItem[];
200
200
  declare const insertRepoHeaders: (items: GHItem[]) => AnyItem[];
201
201
  type Standing = "authored" | "queued" | "spoken";
202
- declare const whoseMove: (health: Health, sectionId: string, standing?: Standing) => "you" | "them";
203
- declare const layoutGHItems: (items: GHItem[], sectionId: string) => AnyItem[];
202
+ declare const whoseMove: (health: Health, sectionId: string, standing?: Standing, theySpokeLast?: boolean) => "you" | "them";
203
+ declare const layoutGHItems: (items: GHItem[], sectionId: string, login?: string) => AnyItem[];
204
204
  /**
205
205
  * A two-way repo split, when a host has one. `undefined` — the ordinary case —
206
206
  * means no split at all: every row stands.
@@ -238,6 +238,20 @@ declare const filterByRepos: (sections: Section[], repos: Set<string>) => Sectio
238
238
  declare const withoutItem: (sections: Section[], target: GHItem) => Section[];
239
239
  declare const reposInSections: (sections: Section[]) => string[];
240
240
  declare const moveCursor: (items: AnyItem[], current: number, dir: 1 | -1) => number;
241
+ /**
242
+ * Whether this row draws a blank line above it.
243
+ *
244
+ * ONE definition, because two readers need the same answer: the renderer draws
245
+ * the gap, and fitCount pays for it out of the window budget. They disagreed
246
+ * until 2026-08-27 — fitCount priced only headers at two lines while the
247
+ * renderer ALSO gapped a task row following a header — so a tab with that shape
248
+ * drew one line more than the window had bought. The frame is sized to fill the
249
+ * terminal exactly, so the overflow scrolls the whole panel instead of clipping.
250
+ *
251
+ * Index-based rather than item-based: whether a row gaps depends on what sits
252
+ * above it, which an item alone cannot answer.
253
+ */
254
+ declare const gapsAbove: (items: readonly AnyItem[], i: number) => boolean;
241
255
  declare const fitCount: (items: AnyItem[], start: number, budget: number) => number;
242
256
  declare const windowCount: (items: AnyItem[], start: number, budget: number) => number;
243
257
  declare const maxViewStart: (items: AnyItem[], budget: number) => number;
@@ -282,21 +296,6 @@ declare const CiStatusLine: ({ state, job, }: {
282
296
  state: CiStatusState;
283
297
  job?: string;
284
298
  }) => React.JSX.Element;
285
- /**
286
- * Indexes of task rows whose row has already appeared higher up the same list.
287
- *
288
- * A ticket with one PR needing you and another in review lands in two bands,
289
- * which is right — the band reads each PR, not the ticket. What was wrong was
290
- * drawing the second occurrence identically to the first, so one ticket read as
291
- * two. The repeat keeps its key and drops its summary.
292
- *
293
- * Identity is the URL, never `key`. `key` is a LABEL, and on a task surface that
294
- * is not a tracker (`life`, whose keys are categories like "Flat") one label
295
- * legitimately heads several unrelated rows — keying on it hid the summary of
296
- * every row but the first. A row with no url is never a repeat, because nothing
297
- * identifies it.
298
- */
299
- declare const repeatedTaskRows: (items: readonly AnyItem[]) => Set<number>;
300
299
  declare const useActionMenu: () => {
301
300
  actions: Action[] | null;
302
301
  cursor: number;
@@ -444,4 +443,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
444
443
  */
445
444
  declare const parsePatterns: (value: string) => string[];
446
445
 
447
- 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, repeatedTaskRows, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
446
+ 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, gapsAbove, 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
@@ -919,18 +919,38 @@ var YOURS = {
919
919
  queued: ["waiting", "pending", "threads", "approved"],
920
920
  spoken: ["threads"]
921
921
  };
922
- var whoseMove = (health, sectionId, standing) => YOURS[standing ?? STANDING[sectionId] ?? "queued"].includes(health) ? "you" : "them";
922
+ var whoseMove = (health, sectionId, standing, theySpokeLast) => (
923
+ // Somebody else having the last word outranks every review state below. It is
924
+ // the plainest claim on you there is — a question asked, an objection raised,
925
+ // a "can you rebase" — and none of it shows up as a health, because a bare
926
+ // comment approves nothing, fails nothing and opens no thread.
927
+ //
928
+ // The row already SAID this and the band disagreed with it: the turn arrow
929
+ // reads lastActor and drew `←`, and the explain panel spelled out "X spoke
930
+ // last, your reply is owed", while the band filed it under Their move. Two
931
+ // signals on one row, pointing opposite ways, until 2026-08-27.
932
+ //
933
+ // Only in that direction. YOU having spoken last does not hand the row over —
934
+ // red CI on your own PR is yours whether or not you commented after it — so
935
+ // that case falls through to the table.
936
+ theySpokeLast ? "you" : YOURS[standing ?? STANDING[sectionId] ?? "queued"].includes(health) ? "you" : "them"
937
+ );
923
938
  var BAND_LABEL = {
924
939
  you: "Your move",
925
940
  them: "Their move"
926
941
  };
927
- var layoutGHItems = (items, sectionId) => {
942
+ var layoutGHItems = (items, sectionId, login) => {
928
943
  if (sectionId === "done") return insertRepoHeaders(sortByRecency(items));
929
944
  const sorted = sortItems(items);
930
945
  const bands = ["you", "them"].map((side) => ({
931
946
  side,
932
947
  rows: sorted.filter(
933
- (i) => whoseMove(i.health, sectionId, i.standing) === side
948
+ (i) => whoseMove(
949
+ i.health,
950
+ sectionId,
951
+ i.standing,
952
+ !!login && !!i.lastActor && i.lastActor !== login
953
+ ) === side
934
954
  )
935
955
  }));
936
956
  const filled = bands.filter((b) => b.rows.length > 0);
@@ -1028,12 +1048,17 @@ var moveCursor = (items, current2, dir) => {
1028
1048
  if (next < 0 || next >= items.length) return current2;
1029
1049
  return next;
1030
1050
  };
1031
- var itemLines = (item, isFirst) => (item.kind === "repo-header" || item.kind === "subgroup-header") && !isFirst ? 2 : 1;
1051
+ var gapsAbove = (items, i) => {
1052
+ const item = items[i];
1053
+ if (!item || i === 0) return false;
1054
+ if (item.kind === "repo-header" || item.kind === "subgroup-header") return true;
1055
+ return item.kind === "task";
1056
+ };
1032
1057
  var fitCount = (items, start, budget) => {
1033
1058
  let lines = 0;
1034
1059
  let count = 0;
1035
1060
  for (let i = start; i < items.length; i++) {
1036
- const cost = itemLines(items[i], i === start);
1061
+ const cost = i !== start && gapsAbove(items, i) ? 2 : 1;
1037
1062
  if (lines + cost > budget) break;
1038
1063
  lines += cost;
1039
1064
  count++;
@@ -1685,16 +1710,6 @@ var TRANSIT_LABEL = {
1685
1710
  };
1686
1711
  var TAB_MARK = "\u25CF";
1687
1712
  var tabLabel = (label, marked, id) => marked.size === 0 ? label : `${marked.has(id) ? TAB_MARK : " "} ${label}`;
1688
- var repeatedTaskRows = (items) => {
1689
- const seen = /* @__PURE__ */ new Set();
1690
- const repeats = /* @__PURE__ */ new Set();
1691
- items.forEach((row, i) => {
1692
- if (row.kind !== "task" || !row.url) return;
1693
- if (seen.has(row.url)) repeats.add(i);
1694
- else seen.add(row.url);
1695
- });
1696
- return repeats;
1697
- };
1698
1713
  var isChildRow = (item) => !!item && (item.kind === "show-more" || item.kind === "show-less" || "indent" in item && item.indent === true);
1699
1714
  var ItemRow = ({
1700
1715
  item,
@@ -1705,7 +1720,6 @@ var ItemRow = ({
1705
1720
  transient,
1706
1721
  lastChild,
1707
1722
  parent,
1708
- repeat,
1709
1723
  sparkFrame = 0
1710
1724
  }) => {
1711
1725
  if (item.kind === "repo-header")
@@ -1741,8 +1755,8 @@ var ItemRow = ({
1741
1755
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
1742
1756
  /* @__PURE__ */ jsx(Text2, { dimColor: true, children: parent ? "\u252C " : " " }),
1743
1757
  /* @__PURE__ */ jsx(Text2, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
1744
- /* @__PURE__ */ jsx(Text2, { color: repeat ? void 0 : "#FF8700", dimColor: repeat, bold: active, children: item.key + " " }),
1745
- repeat ? null : /* @__PURE__ */ jsx(
1758
+ /* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: active, children: item.key + " " }),
1759
+ /* @__PURE__ */ jsx(
1746
1760
  Text2,
1747
1761
  {
1748
1762
  bold: active || transient === "in",
@@ -1751,7 +1765,7 @@ var ItemRow = ({
1751
1765
  children: truncate(item.summary, titleMax2)
1752
1766
  }
1753
1767
  ),
1754
- note && !repeat ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: ` ${note}` }) : null,
1768
+ note ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: ` ${note}` }) : null,
1755
1769
  transitLabel2 ? /* @__PURE__ */ jsx(Text2, { bold: true, color: TRANSIT_COLOUR[transient], children: " " + transitLabel2 }) : null
1756
1770
  ] });
1757
1771
  }
@@ -2209,10 +2223,6 @@ var BrowseScreen = ({
2209
2223
  { vimKeys: false, isActive: repoPicker }
2210
2224
  );
2211
2225
  const cursor = cursors[activeId] ?? 0;
2212
- const repeatedTasks = useMemo(
2213
- () => repeatedTaskRows(section.items),
2214
- [section.items]
2215
- );
2216
2226
  const viewStart = viewStarts[activeId] ?? 0;
2217
2227
  const visibleCount = windowCount(section.items, viewStart, listHeight);
2218
2228
  const visibleItems = section.items.slice(viewStart, viewStart + visibleCount);
@@ -2624,24 +2634,8 @@ var BrowseScreen = ({
2624
2634
  transient: transientOf(transients, item),
2625
2635
  lastChild: "indent" in item && item.indent ? !isChildRow(section.items[viewStart + i + 1]) : void 0,
2626
2636
  parent: item.kind === "task" && isChildRow(section.items[viewStart + i + 1]),
2627
- repeat: repeatedTasks.has(viewStart + i),
2628
2637
  sparkFrame,
2629
- gap: (
2630
- // Window-relative, never `viewStart + i`. fitCount prices the
2631
- // window's FIRST row at one line (isFirst), so gapping it when
2632
- // scrolled draws a row the budget never bought — and the frame
2633
- // is sized to fill the terminal exactly, so the overflow scrolls
2634
- // the whole panel up a line instead of clipping.
2635
- i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2636
- // first child — in Other PRs that's "free" because the
2637
- // child is itself a repo-header (gap above). A task row
2638
- // has no such stand-in, so it needs this explicitly. Never
2639
- // applies between two tickets/PRs — only right after a
2640
- // header.
2641
- item.kind === "task" && ["repo-header", "subgroup-header"].includes(
2642
- section.items[viewStart + i - 1]?.kind
2643
- ))
2644
- )
2638
+ gap: i > 0 && gapsAbove(section.items, viewStart + i)
2645
2639
  },
2646
2640
  `${viewStart + i}:${item.kind === "task" ? item.instanceKey ?? item.key : item.kind === "repo-header" ? `header:${item.repo}` : item.kind === "subgroup-header" ? `subgroup:${item.label}` : item.kind === "show-more" ? `show-more:${item.hidden[0]?.repo ?? i}` : item.kind === "show-less" ? `show-less:${item.toHide[0]?.repo ?? i}` : `${item.repo}/${item.number}`}`
2647
2641
  )) }),
@@ -3035,4 +3029,4 @@ var matchesFilter = (repo, filter) => {
3035
3029
  };
3036
3030
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
3037
3031
 
3038
- 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, repeatedTaskRows, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
3032
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, 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.23.2",
3
+ "version": "0.24.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",