@kud/gh-ink 0.34.0 → 0.36.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
@@ -140,6 +140,31 @@ type GHItem = {
140
140
  * handing back a `lastActor` that no longer claims a turn.
141
141
  */
142
142
  pinned?: boolean;
143
+ /**
144
+ * What this row STANDS FOR, when that differs from what it is. Absent means
145
+ * the row is a unit of work and is counted as one.
146
+ *
147
+ * `"container"` marks a row that exists to carry context rather than to be
148
+ * done: the initiative a story hangs under, drawn as a real row because it is
149
+ * still selectable, openable, and worth seeing until it closes — but not
150
+ * itself a thing on anyone's plate. Four stories under one container is four
151
+ * items, not five.
152
+ *
153
+ * Deliberately not `uncounted`. A host knows what a row IS; it should not have
154
+ * to know what the tab badge does with that, and a field named after one
155
+ * consumer starts lying the moment a second one reads it.
156
+ *
157
+ * Orthogonal to `depth`, which says where the row hangs rather than what it
158
+ * stands for. A container is a genuine depth-0 row with genuine depth-1
159
+ * children; spelling it as a depth would mean lying about the tree to fix a
160
+ * number, and every site that draws indentation reads that lie as truth.
161
+ *
162
+ * A union with one member rather than a boolean. `indent` was a boolean that
163
+ * turned out to need a scalar, and widening it cost a deprecation that is
164
+ * still in this file. Adding a second role here is additive; turning a boolean
165
+ * into a union is not.
166
+ */
167
+ role?: "container";
143
168
  /**
144
169
  * How deep this row hangs in the tree: 0 top level, 1 a child, 2 a
145
170
  * grandchild. Absent means 0.
@@ -165,6 +190,8 @@ type TaskRow = {
165
190
  url: string;
166
191
  status: string;
167
192
  age: string;
193
+ /** What this row stands for. See `GHItem.role`. */
194
+ role?: "container";
168
195
  /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
169
196
  depth?: number;
170
197
  /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
@@ -375,6 +402,24 @@ declare const openInTab: (cmd: string) => Promise<void>;
375
402
  declare const runInPaneHorizontal: (cmd: string) => Promise<void>;
376
403
  declare const runHere: (cmd: string) => void;
377
404
  declare const COLS: number;
405
+ /**
406
+ * What the tab badge and the header total both count: rows that are somebody's
407
+ * work, at the top level.
408
+ *
409
+ * The exclusions are two different kinds of thing. Headers and the
410
+ * show-more/show-less affordances are furniture — not entities at all, which is
411
+ * why the search and repo filters drop them too. This function used to exclude
412
+ * only the two header kinds while its three siblings excluded all four, so a
413
+ * collapsed tail sitting at the top level counted as an item.
414
+ * `role: "container"` rows ARE entities, selectable and openable; they simply
415
+ * are not work. An epic is the case that forced the distinction: real, owned,
416
+ * and on screen until it closes, but the work is the stories under it, and
417
+ * counting it as well reported five things to do where there were four.
418
+ *
419
+ * One function, both consumers, on purpose. The badge and the whole-board total
420
+ * are the same claim at two scales; computed apart they drift, and a header that
421
+ * disagrees with the sum of its own tabs is worse than either number alone.
422
+ */
378
423
  declare const topLevelCount: (s: Section) => number;
379
424
  declare const drillCmd: (item: AnyItem) => string | null;
380
425
  declare const explainGhAction: (e: unknown) => string;
package/dist/index.js CHANGED
@@ -676,6 +676,9 @@ var invalidateCache = (key) => {
676
676
  };
677
677
 
678
678
  // src/inbox/diff.ts
679
+ var MARK_SEP = "\0";
680
+ var markKey = (sectionId, rowKey) => `${sectionId}${MARK_SEP}${rowKey}`;
681
+ var rowKeyOfMark = (mark) => mark.slice(mark.indexOf(MARK_SEP) + 1);
679
682
  var keyOf = (item) => {
680
683
  if (item.kind === "pr" || item.kind === "issue") return item.url;
681
684
  if (item.kind === "task") {
@@ -707,11 +710,27 @@ var indexRows = (sections) => {
707
710
  for (const section of sections) {
708
711
  section.items.forEach((item, index) => {
709
712
  const key = keyOf(item);
710
- if (key) out.set(key, { item, sectionId: section.id, index });
713
+ if (key)
714
+ out.set(markKey(section.id, key), {
715
+ item,
716
+ sectionId: section.id,
717
+ index
718
+ });
711
719
  });
712
720
  }
713
721
  return out;
714
722
  };
723
+ var rowsOf = (index) => {
724
+ const out = /* @__PURE__ */ new Map();
725
+ for (const [mark, placed] of index) {
726
+ const key = rowKeyOfMark(mark);
727
+ const seen = out.get(key);
728
+ if (seen) seen.add(placed.sectionId);
729
+ else out.set(key, /* @__PURE__ */ new Set([placed.sectionId]));
730
+ }
731
+ return out;
732
+ };
733
+ var sameSections = (a, b) => a.size === b.size && [...a].every((id) => b.has(id));
715
734
  var spliceBack = (next, leaving) => {
716
735
  if (leaving.length === 0) return next;
717
736
  const bySection = /* @__PURE__ */ new Map();
@@ -732,30 +751,43 @@ var spliceBack = (next, leaving) => {
732
751
  var diffSections = (prev, next) => {
733
752
  const before = indexRows(prev);
734
753
  const after = indexRows(next);
754
+ const boardBefore = rowsOf(before);
755
+ const boardAfter = rowsOf(after);
735
756
  const transients = /* @__PURE__ */ new Map();
736
757
  const leaving = [];
737
- for (const [key] of after) if (!before.has(key)) transients.set(key, "in");
738
- for (const [key, was] of before) {
739
- const now = after.get(key);
758
+ for (const [mark] of after)
759
+ if (!before.has(mark))
760
+ transients.set(
761
+ mark,
762
+ boardBefore.has(rowKeyOfMark(mark)) ? "moved-in" : "in"
763
+ );
764
+ for (const [mark, was] of before) {
765
+ const now = after.get(mark);
740
766
  if (!now) {
741
- transients.set(key, "out");
767
+ transients.set(
768
+ mark,
769
+ boardAfter.has(rowKeyOfMark(mark)) ? "moved-out" : "out"
770
+ );
742
771
  leaving.push(was);
743
772
  } else if (renderedState(was.item) !== renderedState(now.item)) {
744
- transients.set(key, "changed");
773
+ transients.set(mark, "changed");
745
774
  }
746
775
  }
747
776
  let added = 0;
748
777
  let removed = 0;
749
- let changed = 0;
750
- for (const [, kind] of transients) {
751
- if (kind === "in") added++;
752
- else if (kind === "out") removed++;
753
- else changed++;
778
+ const changedRows = /* @__PURE__ */ new Set();
779
+ for (const [mark, kind] of transients)
780
+ if (kind === "changed") changedRows.add(rowKeyOfMark(mark));
781
+ for (const [key, sections] of boardAfter) {
782
+ const had = boardBefore.get(key);
783
+ if (!had) added += 1;
784
+ else if (!sameSections(had, sections)) changedRows.add(key);
754
785
  }
786
+ for (const key of boardBefore.keys()) if (!boardAfter.has(key)) removed += 1;
755
787
  return {
756
788
  transients,
757
789
  union: spliceBack(next, leaving),
758
- counts: { added, removed, changed }
790
+ counts: { added, removed, changed: changedRows.size }
759
791
  };
760
792
  };
761
793
  var summariseDiff = (counts) => {
@@ -765,17 +797,19 @@ var summariseDiff = (counts) => {
765
797
  if (counts.changed) parts.push(`${counts.changed} moved`);
766
798
  return parts.join(" \xB7 ");
767
799
  };
768
- var transientOf = (transients, item) => {
800
+ var transientOf = (transients, item, sectionId) => {
769
801
  if (!transients?.size) return void 0;
770
802
  const key = keyOf(item);
771
- return key ? transients.get(key) : void 0;
803
+ return key ? transients.get(markKey(sectionId, key)) : void 0;
772
804
  };
773
805
  var tabsOfMarks = (sections, marks) => {
774
806
  const out = /* @__PURE__ */ new Map();
775
807
  for (const section of sections)
776
808
  for (const item of section.items) {
777
809
  const key = keyOf(item);
778
- if (key && marks.has(key)) out.set(key, section.id);
810
+ if (!key) continue;
811
+ const mark = markKey(section.id, key);
812
+ if (marks.has(mark)) out.set(mark, section.id);
779
813
  }
780
814
  return out;
781
815
  };
@@ -1391,7 +1425,7 @@ var Backdrop = ({
1391
1425
  }
1392
1426
  ) : children });
1393
1427
  var topLevelCount = (s) => s.items.filter(
1394
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && depthOf(i) === 0
1428
+ (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && !("role" in i && i.role === "container") && depthOf(i) === 0
1395
1429
  ).length;
1396
1430
  var drillCmd = (item) => {
1397
1431
  if (item.kind === "task")
@@ -1835,13 +1869,26 @@ var TRANSIT_IN_FRAMES = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
1835
1869
  var TRANSIT_COLOUR = {
1836
1870
  in: "#3FB950",
1837
1871
  out: "#8B949E",
1838
- changed: "#FF8700"
1872
+ changed: "#FF8700",
1873
+ // A move is neither an arrival nor a departure, so it takes the colour of the
1874
+ // third thing that can happen to a row you already had: it changed. Both ends
1875
+ // of one move wear the same colour and the same word, which is what lets you
1876
+ // recognise the row you just watched leave when you land on the tab it went to.
1877
+ "moved-in": "#FF8700",
1878
+ "moved-out": "#FF8700"
1839
1879
  };
1840
1880
  var TRANSIT_LABEL = {
1841
1881
  in: "NEW",
1842
1882
  out: "GONE",
1843
- changed: "UPDATED"
1844
- };
1883
+ changed: "UPDATED",
1884
+ // Never GONE. The row is still on the board, one tab over, and telling you it
1885
+ // has gone is the marker lying about work you still own — which was the whole
1886
+ // failure: an epic moving tab said nothing at all and simply vanished.
1887
+ "moved-in": "MOVED",
1888
+ "moved-out": "MOVED"
1889
+ };
1890
+ var isDeparture = (t) => t === "out" || t === "moved-out";
1891
+ var isArrival = (t) => t === "in" || t === "moved-in";
1845
1892
  var LEAVING_HOLD_MS = 2500;
1846
1893
  var TAB_MARK = "\u25CF";
1847
1894
  var tabLabel = (label, marked, id) => marked.size === 0 ? label : `${marked.has(id) ? TAB_MARK : " "} ${label}`;
@@ -1881,7 +1928,7 @@ var ItemRow = ({
1881
1928
  ] });
1882
1929
  if (item.kind === "task") {
1883
1930
  const note = item.note ?? "";
1884
- const transitIcon = !transient ? " " : transient === "out" ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : transient === "in" ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : "\u25C9";
1931
+ const transitIcon = !transient ? " " : isDeparture(transient) ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : isArrival(transient) ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : "\u25C9";
1885
1932
  const transitLabel2 = transient ? TRANSIT_LABEL[transient] : "";
1886
1933
  const titleMax2 = Math.max(
1887
1934
  20,
@@ -1896,8 +1943,8 @@ var ItemRow = ({
1896
1943
  /* @__PURE__ */ jsx(
1897
1944
  Text2,
1898
1945
  {
1899
- bold: active || transient === "in",
1900
- dimColor: transient === "out",
1946
+ bold: active || !!transient && isArrival(transient),
1947
+ dimColor: !!transient && isDeparture(transient),
1901
1948
  strikethrough: transient === "out",
1902
1949
  children: truncate(item.summary, titleMax2)
1903
1950
  }
@@ -1907,7 +1954,7 @@ var ItemRow = ({
1907
1954
  ] });
1908
1955
  }
1909
1956
  const { glyph: healthIcon, color: healthColor2 } = healthDisplay[item.health];
1910
- const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : transient === "out" ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : transient === "in" ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : healthIcon;
1957
+ const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : transient && isDeparture(transient) ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : transient && isArrival(transient) ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : healthIcon;
1911
1958
  const color = merged ? MERGED_COLOUR : transient ? TRANSIT_COLOUR[transient] : healthColor2;
1912
1959
  const spokeLast = !!login && !!item.lastActor && item.lastActor === login;
1913
1960
  const [turnIcon, turnColor] = item.pinned ? [PIN_MARK, "#FF8700"] : !login || !item.lastActor ? [" ", "white"] : spokeLast ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
@@ -1936,8 +1983,8 @@ var ItemRow = ({
1936
1983
  /* @__PURE__ */ jsx(
1937
1984
  Text2,
1938
1985
  {
1939
- bold: active || transient === "in",
1940
- dimColor: transient === "out",
1986
+ bold: active || !!transient && isArrival(transient),
1987
+ dimColor: !!transient && isDeparture(transient),
1941
1988
  strikethrough: transient === "out",
1942
1989
  children: truncate(item.title, titleMax) + " "
1943
1990
  }
@@ -2379,11 +2426,12 @@ var BrowseScreen = ({
2379
2426
  const out = /* @__PURE__ */ new Set();
2380
2427
  if (!transients?.size) return out;
2381
2428
  for (const s of localSections)
2382
- if (s.items.some((item) => transientOf(transients, item))) out.add(s.id);
2429
+ if (s.items.some((item) => transientOf(transients, item, s.id)))
2430
+ out.add(s.id);
2383
2431
  return out;
2384
2432
  }, [localSections, transients]);
2385
2433
  const [sparkFrame, setSparkFrame] = useState(0);
2386
- const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item));
2434
+ const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item, section.id));
2387
2435
  useEffect(() => {
2388
2436
  if (!sparkling) return;
2389
2437
  const id = setInterval(() => setSparkFrame((f) => f + 1), MERGED_FRAME_MS);
@@ -2853,7 +2901,7 @@ var BrowseScreen = ({
2853
2901
  login,
2854
2902
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2855
2903
  leaving: (item.kind === "pr" || item.kind === "issue") && !!leavingUrls?.includes(item.url),
2856
- transient: transientOf(transients, item),
2904
+ transient: transientOf(transients, item, section.id),
2857
2905
  prefix: treePrefix(section.items, viewStart + i),
2858
2906
  parent: item.kind === "task" && depthOf(section.items[viewStart + i + 1]) > depthOf(item),
2859
2907
  sparkFrame,
@@ -2986,7 +3034,12 @@ var App = ({
2986
3034
  const settleTab = (tabId, marks) => {
2987
3035
  const keys = [...transitTabs.current].filter(([, tab]) => tab === tabId).map(([key]) => key);
2988
3036
  for (const key of keys) transitTabs.current.delete(key);
2989
- const gone = new Set(keys.filter((key) => marks.get(key) === "out"));
3037
+ const gone = new Set(
3038
+ keys.filter((key) => {
3039
+ const mark = marks.get(key);
3040
+ return !!mark && isDeparture(mark);
3041
+ }).map(rowKeyOfMark)
3042
+ );
2990
3043
  setTransients((prev) => {
2991
3044
  const next = new Map(prev);
2992
3045
  for (const key of keys) next.delete(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.34.0",
3
+ "version": "0.36.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",