@kud/gh-ink 0.73.0 → 0.74.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
@@ -526,6 +526,27 @@ declare const writeCache: (key: string, data: {
526
526
  sidebar?: Rails;
527
527
  }) => void;
528
528
 
529
+ /**
530
+ * What an action did to a row, said before the server has agreed to it.
531
+ *
532
+ * Three shapes cover every action the inbox takes on a row: it leaves the list
533
+ * (close, done), it changes tab (a Jira move), or it changes in place (labels,
534
+ * assignee, read). A patch names the END STATE rather than the step that gets
535
+ * there, which is what lets it be reapplied on top of every fetch: "SHOP-1234
536
+ * belongs in review, wearing In Review" is as true on the third fetch as on the
537
+ * first, where "move it one tab right" would walk it off the board.
538
+ */
539
+ type Patch = {
540
+ kind: "remove";
541
+ } | {
542
+ kind: "move";
543
+ toSection: string;
544
+ fields?: Record<string, unknown>;
545
+ } | {
546
+ kind: "mutate";
547
+ fields: Record<string, unknown>;
548
+ };
549
+
529
550
  type DetailContext = {
530
551
  item: GHItem;
531
552
  kind: "pr" | "issue";
@@ -629,6 +650,25 @@ type TabForStatus = (to: {
629
650
  name: string;
630
651
  category?: string;
631
652
  }) => string | null;
653
+ /**
654
+ * What a move hands back when the screen took it optimistically: the request's
655
+ * two possible answers, reported so the pending patch can stop showing `◌` or
656
+ * be marked failed. Absent when the move was not taken optimistically, and the
657
+ * caller falls back to its own flashes.
658
+ */
659
+ type OptimisticHandle = {
660
+ settled: () => void;
661
+ failed: (err: unknown) => void;
662
+ };
663
+ /**
664
+ * A row action said ahead of the server: the end state it leaves the row in,
665
+ * and the verb the failure line names it by — `close` reads back as
666
+ * "Couldn't close #412".
667
+ */
668
+ type OptimisticChange = {
669
+ patch: Patch;
670
+ action: string;
671
+ };
632
672
  /**
633
673
  * Resolve the live transitions for a ticket, called lazily when its move
634
674
  * submenu opens — never at board fetch, where one call per visible ticket
@@ -766,6 +806,15 @@ declare const buildActions: (item: AnyItem, login: string, showFlash: (msg: stri
766
806
  * the settlement signal.
767
807
  */
768
808
  onSettled?: (item: GHItem, result: SettledResult) => void;
809
+ /**
810
+ * Take the row's action ahead of the server — see `OptimisticChange`. A
811
+ * handle back means the row is already where the action puts it, so the
812
+ * request's answer goes to the handle and a failure goes ONLY there: the
813
+ * failed patch has its own sticky line with `w` beside it, and a flash
814
+ * naming the same failure would be a second voice saying it less well.
815
+ * Absent, every action keeps the pessimistic shape it had.
816
+ */
817
+ optimistic?: (item: AnyItem, change: OptimisticChange) => OptimisticHandle | undefined;
769
818
  /**
770
819
  * Open the ticket's move submenu through the live-transitions hook. Passed
771
820
  * by the browse screen (which owns the menu the rows open into) only when
package/dist/index.js CHANGED
@@ -1054,9 +1054,9 @@ var PrRow = ({
1054
1054
  justifyContent: "flex-end",
1055
1055
  flexShrink: 0,
1056
1056
  children: sizeParts ? /* @__PURE__ */ jsxs(Text3, { children: [
1057
- /* @__PURE__ */ jsx(Text3, { color: colors.success, children: sizeParts.added }),
1057
+ /* @__PURE__ */ jsx(Text3, { color: colors.added, children: sizeParts.added }),
1058
1058
  " ",
1059
- /* @__PURE__ */ jsx(Text3, { color: colors.error, children: sizeParts.removed })
1059
+ /* @__PURE__ */ jsx(Text3, { color: colors.removed, children: sizeParts.removed })
1060
1060
  ] }) : null
1061
1061
  }
1062
1062
  ) : null,
@@ -1335,20 +1335,12 @@ var lookupRows = (claim, lookup, query, start, openUrl) => {
1335
1335
  if (lookup?.phase === "failed")
1336
1336
  return {
1337
1337
  exclusive: true,
1338
- rows: [
1339
- {
1340
- id: "launcher:retry",
1341
- title: `Couldn't look up ${claim.label}: ${lookup.message}`,
1342
- label: /* @__PURE__ */ jsxs(Text3, { color: colors.error, children: [
1343
- "\u2717 couldn't look up ",
1344
- claim.label,
1345
- " \xB7 ",
1346
- lookup.message
1347
- ] }),
1348
- run: () => start(query, claim),
1349
- keepOpen: true
1350
- }
1351
- ]
1338
+ rows: [],
1339
+ message: {
1340
+ text: `couldn't look up ${claim.label} \xB7 ${lookup.message} \xB7 \u21B5 retry`,
1341
+ tone: "error",
1342
+ onSubmit: () => start(query, claim)
1343
+ }
1352
1344
  };
1353
1345
  const rows = [
1354
1346
  {
@@ -1696,6 +1688,7 @@ var failureNotice = (failed) => {
1696
1688
  };
1697
1689
  var quietly = $({ quiet: true });
1698
1690
  var isMenuGap = (row) => "gap" in row;
1691
+ var rowLabelOf = (item) => item.kind === "task" ? item.ticket ?? item.key : item.kind === "pr" || item.kind === "issue" ? `#${item.number}` : "";
1699
1692
  var jiraMovesCache = /* @__PURE__ */ new Map();
1700
1693
  var cachedJiraMoves = (ticket, status) => {
1701
1694
  const entry = jiraMovesCache.get(ticket);
@@ -2297,6 +2290,19 @@ var workCount = (s) => {
2297
2290
  }
2298
2291
  return n;
2299
2292
  };
2293
+ var countedSections = (sections, departing) => {
2294
+ if (!departing?.size) return sections;
2295
+ const sent = new Set(sections.map((s) => s.id));
2296
+ return sections.map((s) => {
2297
+ const items = s.items.filter((item) => {
2298
+ const key = keyOf(item);
2299
+ if (!key || !departing.has(key)) return true;
2300
+ const to = departing.get(key);
2301
+ return to != null && (to === s.id || !sent.has(to));
2302
+ });
2303
+ return items.length === s.items.length ? s : { ...s, items };
2304
+ });
2305
+ };
2300
2306
  var drillCmd = (item) => {
2301
2307
  if (item.kind === "task")
2302
2308
  return item.ticket ? `jira issue view ${item.ticket}` : null;
@@ -2360,6 +2366,7 @@ var openInJira = (jiraBase, key, showFlash) => {
2360
2366
  });
2361
2367
  showFlash(`\u2197 Opened ${key} in Jira`);
2362
2368
  };
2369
+ var REMOVE_ME_AS_REVIEWER = "Remove me as reviewer";
2363
2370
  var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions, onRefresh, onRemove, onOpenView, ext) => {
2364
2371
  if (item.kind === "repo-header" || item.kind === "subgroup-header" || item.kind === "show-more" || item.kind === "show-less")
2365
2372
  return [];
@@ -2464,6 +2471,32 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2464
2471
  }
2465
2472
  const actions = drillAction ? [drillAction, open, copyUrl] : [open, copyUrl];
2466
2473
  const closing = [];
2474
+ const removeAhead = (opts) => {
2475
+ const row = item;
2476
+ const held = ext?.optimistic?.(row, {
2477
+ patch: { kind: "remove" },
2478
+ action: opts.action
2479
+ });
2480
+ onRemove?.(row);
2481
+ showFlash(opts.taking);
2482
+ void opts.request().then(
2483
+ () => {
2484
+ held?.settled();
2485
+ if (opts.done) showFlash(opts.done);
2486
+ if (held || opts.refreshes) ext?.onActed?.();
2487
+ if (opts.settles) ext?.onSettled?.(row, { ok: true });
2488
+ },
2489
+ (e) => {
2490
+ if (held) held.failed(e);
2491
+ else {
2492
+ showFlash(opts.refused(e));
2493
+ onRefresh?.();
2494
+ }
2495
+ if (opts.settles)
2496
+ ext?.onSettled?.(row, { ok: false, reason: firstLine(e) });
2497
+ }
2498
+ );
2499
+ };
2467
2500
  actions.push({
2468
2501
  label: "Copy repo name",
2469
2502
  hint: "r",
@@ -2486,7 +2519,7 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2486
2519
  });
2487
2520
  if (item.kind === "pr" && item.standing === "queued" && login) {
2488
2521
  actions.push({
2489
- label: "Remove me as reviewer",
2522
+ label: REMOVE_ME_AS_REVIEWER,
2490
2523
  hint: "x",
2491
2524
  // Quiet, beside Unsubscribe: both are "less of this PR for me" — no more
2492
2525
  // review requests, no more notifications. Guarded by the caller's own
@@ -2494,25 +2527,17 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2494
2527
  // review request there is no such row, and none is invented here.
2495
2528
  group: "quiet",
2496
2529
  icon: "\uF468",
2497
- run: () => {
2498
- onRemove?.(item);
2499
- showFlash(`\u22EF Removing you from #${item.number}\u2026`);
2500
- void quietly`gh pr edit ${item.number} --repo ${item.repo} --remove-reviewer ${login}`.then(
2501
- () => {
2502
- showFlash(`\u2713 Removed you as reviewer on #${item.number}`);
2503
- ext?.onActed?.();
2504
- ext?.onSettled?.(item, { ok: true });
2505
- },
2506
- (e) => {
2507
- showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`);
2508
- onRefresh?.();
2509
- ext?.onSettled?.(item, {
2510
- ok: false,
2511
- reason: firstLine(e)
2512
- });
2513
- }
2514
- );
2515
- }
2530
+ // Optimistic, the same shape Close uses. A row that lingers for a round
2531
+ // trip reads as a broken keypress.
2532
+ run: () => removeAhead({
2533
+ action: "remove you from",
2534
+ request: () => quietly`gh pr edit ${item.number} --repo ${item.repo} --remove-reviewer ${login}`,
2535
+ taking: `\u22EF Removing you from #${item.number}\u2026`,
2536
+ done: `\u2713 Removed you as reviewer on #${item.number}`,
2537
+ refused: (e) => `\u2717 #${item.number}: ${explainGhAction(e)}`,
2538
+ settles: true,
2539
+ refreshes: true
2540
+ })
2516
2541
  });
2517
2542
  }
2518
2543
  if (item.kind === "pr" && item.branch) {
@@ -2584,23 +2609,13 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2584
2609
  label: `Close #${item.number}`,
2585
2610
  hint: "",
2586
2611
  group: "confirm",
2587
- run: () => {
2588
- onRemove?.(item);
2589
- showFlash(`\u2713 Closed #${item.number}`);
2590
- void quietly`gh issue close ${item.number} --repo ${item.repo}`.then(
2591
- () => {
2592
- ext?.onSettled?.(item, { ok: true });
2593
- },
2594
- (e) => {
2595
- showFlash(`\u2717 Close failed \u2014 restoring #${item.number}`);
2596
- onRefresh?.();
2597
- ext?.onSettled?.(item, {
2598
- ok: false,
2599
- reason: firstLine(e)
2600
- });
2601
- }
2602
- );
2603
- }
2612
+ run: () => removeAhead({
2613
+ action: "close",
2614
+ request: () => quietly`gh issue close ${item.number} --repo ${item.repo}`,
2615
+ taking: `\u2713 Closed #${item.number}`,
2616
+ refused: () => `\u2717 Close failed \u2014 restoring #${item.number}`,
2617
+ settles: true
2618
+ })
2604
2619
  },
2605
2620
  { label: "Cancel", hint: "", group: "confirm", run: () => {
2606
2621
  } }
@@ -2651,23 +2666,13 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2651
2666
  label: `Close #${item.number}`,
2652
2667
  hint: "",
2653
2668
  group: "confirm",
2654
- run: () => {
2655
- onRemove?.(item);
2656
- showFlash(`\u2713 Closed #${item.number}`);
2657
- void quietly`gh pr close ${item.number} --repo ${item.repo}`.then(
2658
- () => {
2659
- ext?.onSettled?.(item, { ok: true });
2660
- },
2661
- (e) => {
2662
- showFlash(`\u2717 Close failed \u2014 restoring #${item.number}`);
2663
- onRefresh?.();
2664
- ext?.onSettled?.(item, {
2665
- ok: false,
2666
- reason: firstLine(e)
2667
- });
2668
- }
2669
- );
2670
- }
2669
+ run: () => removeAhead({
2670
+ action: "close",
2671
+ request: () => quietly`gh pr close ${item.number} --repo ${item.repo}`,
2672
+ taking: `\u2713 Closed #${item.number}`,
2673
+ refused: () => `\u2717 Close failed \u2014 restoring #${item.number}`,
2674
+ settles: true
2675
+ })
2671
2676
  },
2672
2677
  { label: "Cancel", hint: "", group: "confirm", run: () => {
2673
2678
  } }
@@ -2688,16 +2693,23 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
2688
2693
  label: `Close #${item.number} and delete ${item.branch}`,
2689
2694
  hint: "",
2690
2695
  group: "confirm",
2691
- run: () => {
2692
- onRemove?.(item);
2693
- showFlash(`\u2713 Closed #${item.number} and deleted ${item.branch}`);
2694
- void quietly`gh pr close ${item.number} --repo ${item.repo}`.then(
2695
- () => quietly`gh api -X DELETE ${`repos/${item.repo}/git/refs/heads/${item.branch}`}`
2696
- ).catch(() => {
2697
- showFlash(`\u2717 Close + delete failed \u2014 restoring`);
2698
- onRefresh?.();
2699
- });
2700
- }
2696
+ // The patch covers the close and nothing else. A branch that will
2697
+ // not delete leaves a PR that IS closed, so reporting "Couldn't
2698
+ // close #412" and offering `w` would restore a row the server no
2699
+ // longer has; the branch gets its own flash instead.
2700
+ run: () => removeAhead({
2701
+ action: "close",
2702
+ request: async () => {
2703
+ await quietly`gh pr close ${item.number} --repo ${item.repo}`;
2704
+ void quietly`gh api -X DELETE ${`repos/${item.repo}/git/refs/heads/${item.branch}`}`.catch(
2705
+ () => showFlash(
2706
+ `\u2717 Closed #${item.number}, but couldn't delete ${item.branch}`
2707
+ )
2708
+ );
2709
+ },
2710
+ taking: `\u2713 Closed #${item.number} and deleted ${item.branch}`,
2711
+ refused: () => `\u2717 Close + delete failed \u2014 restoring`
2712
+ })
2701
2713
  },
2702
2714
  { label: "Cancel", hint: "", group: "confirm", run: () => {
2703
2715
  } }
@@ -2983,7 +2995,7 @@ var ItemRow = ({
2983
2995
  const icon = merged ? {
2984
2996
  glyph: MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length],
2985
2997
  color: MERGED_COLOUR
2986
- } : transient ? {
2998
+ } : pending ? { glyph: PENDING_GLYPH, color: colors.muted } : transient ? {
2987
2999
  glyph: isDeparture(transient) ? TRANSIT_OUT_FRAMES[rampAt(TRANSIT_OUT_FRAMES.length)] : isArrival(transient) ? TRANSIT_IN_FRAMES[rampAt(TRANSIT_IN_FRAMES.length)] : displayFor(item.health).glyph,
2988
3000
  color: TRANSIT_COLOUR[transient]
2989
3001
  } : void 0;
@@ -3043,17 +3055,18 @@ var useActionMenu = () => {
3043
3055
  };
3044
3056
  const handleKey = (key) => {
3045
3057
  if (!actions) return false;
3058
+ const live = (n) => n >= 0 && n < actions.length && !isMenuGap(actions[n]);
3046
3059
  if (key.upArrow)
3047
3060
  setCursor((c) => {
3048
3061
  let n = c - 1;
3049
3062
  while (n > 0 && isMenuGap(actions[n])) n -= 1;
3050
- return isMenuGap(actions[n]) ? c : n;
3063
+ return live(n) ? n : c;
3051
3064
  });
3052
3065
  if (key.downArrow)
3053
3066
  setCursor((c) => {
3054
3067
  let n = c + 1;
3055
3068
  while (n < actions.length - 1 && isMenuGap(actions[n])) n += 1;
3056
- return isMenuGap(actions[n]) ? c : n;
3069
+ return live(n) ? n : c;
3057
3070
  });
3058
3071
  if (key.return) {
3059
3072
  const row = actions[cursor];
@@ -3453,6 +3466,8 @@ var BrowseScreen = ({
3453
3466
  sidebar,
3454
3467
  liveLabel,
3455
3468
  optimisticMove,
3469
+ optimistic,
3470
+ departing,
3456
3471
  pendingKeys,
3457
3472
  failure,
3458
3473
  onRestore
@@ -3578,6 +3593,14 @@ var BrowseScreen = ({
3578
3593
  const timer = setTimeout(() => setDismissedSeq(seq), FAILURE_NOTICE_MS);
3579
3594
  return () => clearTimeout(timer);
3580
3595
  }, [failure?.seq]);
3596
+ const counted = useMemo(
3597
+ () => countedSections(localSections, departing),
3598
+ [localSections, departing]
3599
+ );
3600
+ const countOf = (id) => {
3601
+ const s = counted.find((c) => c.id === id);
3602
+ return s ? workCount(s) : 0;
3603
+ };
3581
3604
  const markedTabs = useMemo(() => {
3582
3605
  const out = /* @__PURE__ */ new Set();
3583
3606
  if (!transients?.size) return out;
@@ -3713,31 +3736,34 @@ var BrowseScreen = ({
3713
3736
  optimistic: optimisticMove ? (to) => optimisticMove(task, to) : void 0
3714
3737
  });
3715
3738
  };
3739
+ const actionsFor = (item, flash2) => buildActions(
3740
+ item,
3741
+ login,
3742
+ flash2,
3743
+ jiraBase,
3744
+ jiraKeyRe,
3745
+ jiraTransitions,
3746
+ onRefresh,
3747
+ dismissItem,
3748
+ openDrillView,
3749
+ {
3750
+ extensions,
3751
+ onOpenExt,
3752
+ onActed,
3753
+ onSettled,
3754
+ optimistic,
3755
+ onOpenMove: jiraTransitionsFor ? openMoveFor : void 0
3756
+ }
3757
+ );
3716
3758
  const openMenu = () => {
3717
3759
  if (!activeItem || activeItem.kind === "repo-header" || activeItem.kind === "subgroup-header")
3718
3760
  return;
3719
- const actions = buildActions(
3720
- activeItem,
3721
- login,
3722
- (msg) => {
3761
+ menu.open(
3762
+ actionsFor(activeItem, (msg) => {
3723
3763
  menu.close();
3724
3764
  showFlash(msg);
3725
- },
3726
- jiraBase,
3727
- jiraKeyRe,
3728
- jiraTransitions,
3729
- onRefresh,
3730
- dismissItem,
3731
- openDrillView,
3732
- {
3733
- extensions,
3734
- onOpenExt,
3735
- onActed,
3736
- onSettled,
3737
- onOpenMove: jiraTransitionsFor ? openMoveFor : void 0
3738
- }
3765
+ })
3739
3766
  );
3740
- menu.open(actions);
3741
3767
  };
3742
3768
  useInput((input, key) => {
3743
3769
  const idleFor = Date.now() - lastKeyAt.current;
@@ -4029,20 +4055,7 @@ var BrowseScreen = ({
4029
4055
  return;
4030
4056
  }
4031
4057
  if (input === "x" && activeItem.kind === "pr" && activeItem.standing === "queued" && login) {
4032
- dismissItem(activeItem);
4033
- showFlash(`\u22EF Removing you from #${activeItem.number}\u2026`);
4034
- void quietly`gh pr edit ${activeItem.number} --repo ${activeItem.repo} --remove-reviewer ${login}`.then(
4035
- () => {
4036
- showFlash(`\u2713 Removed you as reviewer on #${activeItem.number}`);
4037
- onActed?.();
4038
- onSettled?.(activeItem, { ok: true });
4039
- },
4040
- (e) => {
4041
- showFlash(`\u2717 #${activeItem.number}: ${explainGhAction(e)}`);
4042
- onRefresh?.();
4043
- onSettled?.(activeItem, { ok: false, reason: firstLine(e) });
4044
- }
4045
- );
4058
+ actionsFor(activeItem, showFlash).find((a) => a.label === REMOVE_ME_AS_REVIEWER)?.run();
4046
4059
  return;
4047
4060
  }
4048
4061
  if (input === "b" && activeItem.kind === "pr" && activeItem.branch) {
@@ -4331,7 +4344,7 @@ var BrowseScreen = ({
4331
4344
  InboxHeader,
4332
4345
  {
4333
4346
  brand,
4334
- sections: localSections,
4347
+ sections: counted,
4335
4348
  login,
4336
4349
  scopeLabel: overlayOpen ? chrome?.scope ?? origin?.label : origin?.label,
4337
4350
  budgetLabel: budgetNotice(budget)?.label,
@@ -4363,7 +4376,7 @@ var BrowseScreen = ({
4363
4376
  items: localSections.map((s) => ({
4364
4377
  value: s.id,
4365
4378
  label: s.label,
4366
- count: workCount(s),
4379
+ count: countOf(s.id),
4367
4380
  /*
4368
4381
  * The denominator, and only where it can still be true.
4369
4382
  *
@@ -4835,6 +4848,26 @@ var App = ({
4835
4848
  phase: "inflight"
4836
4849
  });
4837
4850
  } : void 0;
4851
+ const optimistic = (item, change) => {
4852
+ const key = keyOf(item);
4853
+ if (!key) return void 0;
4854
+ return hold(key, {
4855
+ patch: change.patch,
4856
+ action: change.action,
4857
+ label: rowLabelOf(item),
4858
+ since: Date.now(),
4859
+ phase: "inflight"
4860
+ });
4861
+ };
4862
+ const departing = useMemo(
4863
+ () => new Map(
4864
+ [...overrides.current].flatMap(
4865
+ ([key, { patch }]) => patch.kind === "remove" ? [[key, null]] : patch.kind === "move" ? [[key, patch.toSection]] : []
4866
+ )
4867
+ ),
4868
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4869
+ [overrideRev]
4870
+ );
4838
4871
  const restore = () => {
4839
4872
  const newest = failedOverrides(overrides.current)[0];
4840
4873
  if (!newest) return;
@@ -5070,6 +5103,8 @@ var App = ({
5070
5103
  jiraTransitions,
5071
5104
  jiraTransitionsFor,
5072
5105
  optimisticMove,
5106
+ optimistic,
5107
+ departing,
5073
5108
  pendingKeys,
5074
5109
  failure,
5075
5110
  onRestore: restore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.73.0",
3
+ "version": "0.74.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",
@@ -44,7 +44,7 @@
44
44
  "access": "public"
45
45
  },
46
46
  "peerDependencies": {
47
- "@kud/ink-ui": ">=0.36.0 <1",
47
+ "@kud/ink-ui": ">=0.38.0 <1",
48
48
  "ink": ">=7",
49
49
  "react": ">=19"
50
50
  },
@@ -54,7 +54,7 @@
54
54
  "zx": "8.8.5"
55
55
  },
56
56
  "devDependencies": {
57
- "@kud/ink-ui": "0.36.0",
57
+ "@kud/ink-ui": "0.38.0",
58
58
  "@types/node": "22.20.1",
59
59
  "@types/react": "19.2.17",
60
60
  "ink": "7.1.0",