@kud/gh-ink 0.30.0 → 0.32.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
@@ -124,6 +124,22 @@ type GHItem = {
124
124
  * never about the PR. Left unset, the section id decides as before.
125
125
  */
126
126
  standing?: Standing;
127
+ /**
128
+ * A turn the viewer has claimed BY HAND, outranking everything inferred.
129
+ *
130
+ * The inference below is good and still gets things wrong in one direction it
131
+ * cannot see: a row can be genuinely yours while every signal says otherwise —
132
+ * nothing red, no unresolved thread, nobody waiting on a word from you — and
133
+ * no amount of reading the PR harder will discover it. That knowledge lives
134
+ * with the viewer, so this is the field where they put it.
135
+ *
136
+ * It is a PIN, not a correction: it applies whether or not the row was already
137
+ * yours, which is why it does not pair with an opposite. The other direction —
138
+ * "no reply is owed for this particular comment" — is a fact about one event
139
+ * and belongs where the events are read, not here; a host expresses it by
140
+ * handing back a `lastActor` that no longer claims a turn.
141
+ */
142
+ pinned?: boolean;
127
143
  indent: boolean;
128
144
  };
129
145
  type TaskRow = {
@@ -240,7 +256,7 @@ declare const sortItems: (items: GHItem[]) => GHItem[];
240
256
  declare const sortByRecency: (items: GHItem[]) => GHItem[];
241
257
  declare const insertRepoHeaders: (items: GHItem[]) => AnyItem[];
242
258
  type Standing = "authored" | "queued" | "spoken";
243
- declare const whoseMove: (health: Health, sectionId: string, standing?: Standing, theySpokeLast?: boolean) => "you" | "them";
259
+ declare const whoseMove: (health: Health, sectionId: string, standing?: Standing, theySpokeLast?: boolean, pinned?: boolean) => "you" | "them";
244
260
  declare const layoutGHItems: (items: GHItem[], sectionId: string, login?: string) => AnyItem[];
245
261
  /**
246
262
  * A two-way repo split, when a host has one. `undefined` — the ordinary case —
package/dist/index.js CHANGED
@@ -838,6 +838,10 @@ var checksSentence = (d) => {
838
838
  return `Checks: ${parts.join(", ")}.`;
839
839
  };
840
840
  var turnSentences = (item, login) => {
841
+ if (item.pinned)
842
+ return [
843
+ "You pinned this (!). It stays under Your move until you unpin it, whatever else the row says."
844
+ ];
841
845
  if (!item.lastActor) return ["Nothing has been said on it yet."];
842
846
  const d = item.detail;
843
847
  const when = d?.lastEventAt ? `${relativeTime(d.lastEventAt)} ago` : "earlier";
@@ -937,8 +941,9 @@ var YOURS = {
937
941
  queued: ["waiting", "pending", "threads", "approved"],
938
942
  spoken: ["threads"]
939
943
  };
940
- var whoseMove = (health, sectionId, standing, theySpokeLast) => {
944
+ var whoseMove = (health, sectionId, standing, theySpokeLast, pinned) => {
941
945
  const position = standing ?? STANDING[sectionId] ?? "queued";
946
+ if (pinned) return "you";
942
947
  if (theySpokeLast && position === "authored") return "you";
943
948
  return YOURS[position].includes(health) ? "you" : "them";
944
949
  };
@@ -956,7 +961,8 @@ var layoutGHItems = (items, sectionId, login) => {
956
961
  i.health,
957
962
  sectionId,
958
963
  i.standing,
959
- !!login && !!i.lastActor && i.lastActor !== login
964
+ !!login && !!i.lastActor && i.lastActor !== login,
965
+ i.pinned
960
966
  ) === side
961
967
  )
962
968
  }));
@@ -1778,6 +1784,7 @@ var TRANSIT_LABEL = {
1778
1784
  out: "GONE",
1779
1785
  changed: "UPDATED"
1780
1786
  };
1787
+ var LEAVING_HOLD_MS = 2500;
1781
1788
  var TAB_MARK = "\u25CF";
1782
1789
  var tabLabel = (label, marked, id) => marked.size === 0 ? label : `${marked.has(id) ? TAB_MARK : " "} ${label}`;
1783
1790
  var ItemRow = ({
@@ -1786,11 +1793,13 @@ var ItemRow = ({
1786
1793
  gap,
1787
1794
  login,
1788
1795
  merged,
1789
- transient,
1796
+ transient: refreshMark,
1797
+ leaving,
1790
1798
  lastChild,
1791
1799
  parent,
1792
1800
  sparkFrame = 0
1793
1801
  }) => {
1802
+ const transient = leaving ? "out" : refreshMark;
1794
1803
  if (item.kind === "repo-header")
1795
1804
  return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false });
1796
1805
  if (item.kind === "subgroup-header")
@@ -1842,7 +1851,7 @@ var ItemRow = ({
1842
1851
  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;
1843
1852
  const color = merged ? MERGED_COLOUR : transient ? TRANSIT_COLOUR[transient] : healthColor2;
1844
1853
  const spokeLast = !!login && !!item.lastActor && item.lastActor === login;
1845
- const [turnIcon, turnColor] = !login || !item.lastActor ? [" ", "white"] : spokeLast ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
1854
+ const [turnIcon, turnColor] = item.pinned ? ["!", "#FF8700"] : !login || !item.lastActor ? [" ", "white"] : spokeLast ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
1846
1855
  const numStr = `#${item.number}`.padEnd(7);
1847
1856
  const showAuthor = !!item.author && item.author !== login;
1848
1857
  const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
@@ -2212,6 +2221,8 @@ var BrowseScreen = ({
2212
2221
  skippedForBudget,
2213
2222
  brand,
2214
2223
  mergedUrls,
2224
+ leavingUrls,
2225
+ onLeave,
2215
2226
  transients,
2216
2227
  onTabChange
2217
2228
  }) => {
@@ -2281,7 +2292,7 @@ var BrowseScreen = ({
2281
2292
  )
2282
2293
  );
2283
2294
  }, [localSections, listHeight]);
2284
- const removeItemFromSections = (target) => setLocalSections((prev) => withoutItem(prev, target));
2295
+ const dismissItem = (target) => onLeave ? onLeave(target) : setLocalSections((prev) => withoutItem(prev, target));
2285
2296
  const rawSection = localSections[safeTabIdx] ?? {
2286
2297
  id: "empty",
2287
2298
  label: "",
@@ -2313,7 +2324,7 @@ var BrowseScreen = ({
2313
2324
  return out;
2314
2325
  }, [localSections, transients]);
2315
2326
  const [sparkFrame, setSparkFrame] = useState(0);
2316
- const sparkling = (mergedUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item));
2327
+ const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item));
2317
2328
  useEffect(() => {
2318
2329
  if (!sparkling) return;
2319
2330
  const id = setInterval(() => setSparkFrame((f) => f + 1), MERGED_FRAME_MS);
@@ -2346,7 +2357,7 @@ var BrowseScreen = ({
2346
2357
  jiraKeyRe,
2347
2358
  jiraTransitions,
2348
2359
  onRefresh,
2349
- removeItemFromSections,
2360
+ dismissItem,
2350
2361
  openDrillView,
2351
2362
  { extensions, onOpenExt, onActed }
2352
2363
  );
@@ -2453,7 +2464,7 @@ var BrowseScreen = ({
2453
2464
  item: activeItem ?? void 0,
2454
2465
  ciJob: ciStatus?.job,
2455
2466
  login,
2456
- onRemove: (row) => removeItemFromSections(row),
2467
+ onRemove: (row) => dismissItem(row),
2457
2468
  showFlash
2458
2469
  });
2459
2470
  return;
@@ -2562,7 +2573,7 @@ var BrowseScreen = ({
2562
2573
  return;
2563
2574
  }
2564
2575
  if (input === "x" && activeItem.kind === "pr" && activeItem.standing === "queued" && login) {
2565
- removeItemFromSections(activeItem);
2576
+ dismissItem(activeItem);
2566
2577
  showFlash(`\u22EF Removing you from #${activeItem.number}\u2026`);
2567
2578
  void quietly`gh pr edit ${activeItem.number} --repo ${activeItem.repo} --remove-reviewer ${login}`.then(() => {
2568
2579
  showFlash(`\u2713 Removed you as reviewer on #${activeItem.number}`);
@@ -2729,6 +2740,7 @@ var BrowseScreen = ({
2729
2740
  active: viewStart + i === cursor,
2730
2741
  login,
2731
2742
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2743
+ leaving: (item.kind === "pr" || item.kind === "issue") && !!leavingUrls?.includes(item.url),
2732
2744
  transient: transientOf(transients, item),
2733
2745
  lastChild: "indent" in item && item.indent ? !isChildRow(section.items[viewStart + i + 1]) : void 0,
2734
2746
  parent: item.kind === "task" && isChildRow(section.items[viewStart + i + 1]),
@@ -3043,8 +3055,9 @@ var App = ({
3043
3055
  );
3044
3056
  const [skippedForBudget, setSkippedForBudget] = useState(false);
3045
3057
  const [mergedUrls, setMergedUrls] = useState([]);
3046
- const mergeTimers = useRef([]);
3047
- useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
3058
+ const [leavingUrls, setLeavingUrls] = useState([]);
3059
+ const holdTimers = useRef([]);
3060
+ useEffect(() => () => holdTimers.current.forEach(clearTimeout), []);
3048
3061
  if (state.phase === "loading" || state.phase === "empty" || state.phase === "failed")
3049
3062
  return /* @__PURE__ */ jsxs(
3050
3063
  Box,
@@ -3092,17 +3105,26 @@ var App = ({
3092
3105
  target: state.target
3093
3106
  } : null;
3094
3107
  const toBrowse = () => setState({ phase: "browse", sections: state.sections, login: state.login });
3095
- const removeAndReturn = (target) => setState({
3096
- phase: "browse",
3097
- sections: withoutItem(state.sections, target),
3098
- login: state.login
3099
- });
3108
+ const markLeaving = (target) => {
3109
+ if (state.phase !== "browse") toBrowse();
3110
+ setLeavingUrls(
3111
+ (prev) => prev.includes(target.url) ? prev : [...prev, target.url]
3112
+ );
3113
+ holdTimers.current.push(
3114
+ setTimeout(() => {
3115
+ setLeavingUrls((prev) => prev.filter((u) => u !== target.url));
3116
+ setState(
3117
+ (s) => s.phase === "browse" ? { ...s, sections: withoutItem(s.sections, target) } : s
3118
+ );
3119
+ }, LEAVING_HOLD_MS)
3120
+ );
3121
+ };
3100
3122
  const markMerged = (target) => {
3101
3123
  toBrowse();
3102
3124
  setMergedUrls(
3103
3125
  (prev) => prev.includes(target.url) ? prev : [...prev, target.url]
3104
3126
  );
3105
- mergeTimers.current.push(
3127
+ holdTimers.current.push(
3106
3128
  setTimeout(() => {
3107
3129
  setMergedUrls((prev) => prev.filter((u) => u !== target.url));
3108
3130
  setState(
@@ -3133,6 +3155,8 @@ var App = ({
3133
3155
  skippedForBudget,
3134
3156
  hidden: overlay !== null,
3135
3157
  mergedUrls,
3158
+ leavingUrls,
3159
+ onLeave: markLeaving,
3136
3160
  transients,
3137
3161
  onTabChange: setVisibleTab,
3138
3162
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
@@ -3166,7 +3190,7 @@ var App = ({
3166
3190
  login: state.login,
3167
3191
  onBack: toBrowse,
3168
3192
  onRefresh: applyOrRefresh,
3169
- onRemove: removeAndReturn,
3193
+ onRemove: markLeaving,
3170
3194
  onMerged: markMerged
3171
3195
  }),
3172
3196
  overlay?.kind === "ext" && extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.30.0",
3
+ "version": "0.32.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,7 +48,7 @@
48
48
  "react": ">=19"
49
49
  },
50
50
  "dependencies": {
51
- "@kud/gh": "0.8.0",
51
+ "@kud/gh": "0.9.0",
52
52
  "@kud/ink-ui": "0.14.0",
53
53
  "zx": "8.8.5"
54
54
  },