@iloveagents/foundry-web-ui 0.33.0 → 0.33.2

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.
@@ -1,7 +1,39 @@
1
1
  import { type ReactNode } from "react";
2
- import type { NavGroup } from "../lib/nav-config.js";
2
+ import type { NavGroup, NavItemDnd } from "../lib/nav-config.js";
3
3
  declare function scrollNavRowIntoView(nav: HTMLElement, row: HTMLElement): void;
4
4
  export declare const _scrollNavRowIntoViewForTest: typeof scrollNavRowIntoView;
5
+ /** Drop zone rendered inside expanded containers so users can move items back to the container root.
6
+ *
7
+ * The strip stays IN FLOW and reserves its full height for the whole drag:
8
+ * 4px at rest, 28px from the moment a drag begins until it ends, with the
9
+ * band filling exactly what was reserved. Growing on hover instead — the
10
+ * obvious design — fails in both of its available shapes:
11
+ *
12
+ * - in flow, every row beneath jumps 24px at the exact moment the pointer
13
+ * crosses the strip, which is the moment the user is aiming at one of
14
+ * those rows. Dragging a file from a nested folder down to a sibling
15
+ * crosses one strip per open container, so the destination walks away
16
+ * from the cursor mid-drag (measured: 28px per crossing) and the drop
17
+ * lands on whatever slid underneath. From the user's seat the gesture
18
+ * simply does nothing, with no request made and nothing to read.
19
+ * - out of flow, nothing moves, but the expanded band overlaps ~12px of
20
+ * the rows on either side and, being positioned, takes their drops — a
21
+ * moving target traded for a wrong one.
22
+ *
23
+ * Reserving for the whole drag settles the layout once, before the user has
24
+ * aimed at anything. The opening is deliberately NOT animated: a 150ms
25
+ * height transition is a moving target for 150ms, which is inside the time
26
+ * a short drag to a nearby row takes. Only the collapse animates, and by
27
+ * then nothing is being aimed at.
28
+ *
29
+ * Exported for its test only — it is not in the package barrel, and the
30
+ * invariants (in flow, reserved by the drag rather than the pointer, band
31
+ * == strip) are ones jsdom can check from the class names and nothing can
32
+ * check from the outside.
33
+ */
34
+ export declare function ContainerDropZone({ dnd }: {
35
+ dnd: NavItemDnd;
36
+ }): import("react/jsx-runtime").JSX.Element;
5
37
  interface SidebarViewProps {
6
38
  /** Display-only navigation override; the navigation registry remains intact. */
7
39
  groups?: NavGroup[];
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
3
3
  import { NavLink, useLocation, useNavigate } from "react-router";
4
4
  import { Ellipsis, Layers, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, ChevronLeft, } from "lucide-react";
@@ -8,7 +8,7 @@ import { useToolPanelStore } from "../lib/tool-panel-store.js";
8
8
  import { useNewConversation } from "../lib/use-new-conversation.js";
9
9
  import { useNavStore } from "../lib/nav-store.js";
10
10
  import { useAppStore } from "../lib/app-store.js";
11
- import { getContainerDropZoneLabel, hasAnyDropTarget, hasExternalFilesData, } from "../lib/nav-dnd.js";
11
+ import { beginNavDrag, subscribeNavDrag, getNavDragVersion, navDragAccepts, hasAnyDropTarget, hasExternalFilesData, } from "../lib/nav-dnd.js";
12
12
  import { TooltipIconButton } from "./tooltip-icon-button.js";
13
13
  import { Button } from "@iloveagents/foundry-web-primitives";
14
14
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
@@ -262,6 +262,12 @@ function useNavItemDnd(dnd) {
262
262
  return;
263
263
  e.dataTransfer.setData(dnd.type, data);
264
264
  e.dataTransfer.effectAllowed = "move";
265
+ // Container drop zones reserve their space for the whole drag rather
266
+ // than growing under the pointer — see `beginNavDrag`. The MIME type
267
+ // goes with it so only zones that would take THIS drag open, and the
268
+ // source element so its `dragend` is still heard if a nav refresh
269
+ // detaches this row mid-drag.
270
+ beginNavDrag(dnd.type, e.currentTarget);
265
271
  }, [dnd]);
266
272
  const onDragEnter = useCallback((e) => {
267
273
  if (!hasMatchingDragData(e))
@@ -359,57 +365,101 @@ function useNavItemDnd(dnd) {
359
365
  : {},
360
366
  };
361
367
  }
362
- /** Drop zone rendered inside expanded containers so users can move items back to the container root. */
363
- function ContainerDropZone({ dnd }) {
368
+ /** Drop zone rendered inside expanded containers so users can move items back to the container root.
369
+ *
370
+ * The strip stays IN FLOW and reserves its full height for the whole drag:
371
+ * 4px at rest, 28px from the moment a drag begins until it ends, with the
372
+ * band filling exactly what was reserved. Growing on hover instead — the
373
+ * obvious design — fails in both of its available shapes:
374
+ *
375
+ * - in flow, every row beneath jumps 24px at the exact moment the pointer
376
+ * crosses the strip, which is the moment the user is aiming at one of
377
+ * those rows. Dragging a file from a nested folder down to a sibling
378
+ * crosses one strip per open container, so the destination walks away
379
+ * from the cursor mid-drag (measured: 28px per crossing) and the drop
380
+ * lands on whatever slid underneath. From the user's seat the gesture
381
+ * simply does nothing, with no request made and nothing to read.
382
+ * - out of flow, nothing moves, but the expanded band overlaps ~12px of
383
+ * the rows on either side and, being positioned, takes their drops — a
384
+ * moving target traded for a wrong one.
385
+ *
386
+ * Reserving for the whole drag settles the layout once, before the user has
387
+ * aimed at anything. The opening is deliberately NOT animated: a 150ms
388
+ * height transition is a moving target for 150ms, which is inside the time
389
+ * a short drag to a nearby row takes. Only the collapse animates, and by
390
+ * then nothing is being aimed at.
391
+ *
392
+ * Exported for its test only — it is not in the package barrel, and the
393
+ * invariants (in flow, reserved by the drag rather than the pointer, band
394
+ * == strip) are ones jsdom can check from the class names and nothing can
395
+ * check from the outside.
396
+ */
397
+ export function ContainerDropZone({ dnd }) {
364
398
  const [isDragOver, setIsDragOver] = useState(false);
365
- const [isFileDragOver, setIsFileDragOver] = useState(false);
366
399
  const dragCounter = useRef(0);
367
400
  const acceptsFiles = dnd.canDropFiles?.() ?? false;
368
401
  const acceptsEntities = dnd.canDrop();
402
+ // Whether this zone is open is the DRAG's business, not the pointer's:
403
+ // the band is reserved before the pointer reaches it, so hover can no
404
+ // longer say anything about what is being dragged. And reserving space
405
+ // and painting a label is a promise that a drop will land here — so a
406
+ // zone opens only for a drag it would accept, which the drag's own MIME
407
+ // types decide.
408
+ //
409
+ // The subscribed snapshot is a VERSION, not the types themselves: a MIME
410
+ // token may legally contain whatever character one joins them with, and a
411
+ // split on it turns one type into two. The predicate reads the set.
412
+ useSyncExternalStore(subscribeNavDrag, getNavDragVersion, () => 0);
413
+ const dragActive = navDragAccepts({ type: dnd.type, acceptsEntities });
369
414
  const hasExternalFiles = (e) => hasExternalFilesData(e.dataTransfer);
370
415
  const hasMatchingDragData = (e) => Boolean((acceptsEntities && e.dataTransfer.types.includes(dnd.type)) ||
371
416
  (acceptsFiles && hasExternalFiles(e)));
372
- return (_jsx("div", { onDragEnter: (e) => {
373
- if (!hasMatchingDragData(e))
374
- return;
375
- e.preventDefault();
376
- dragCounter.current++;
377
- setIsFileDragOver(hasExternalFiles(e));
378
- setIsDragOver(true);
379
- }, onDragLeave: () => {
380
- dragCounter.current--;
381
- if (dragCounter.current <= 0) {
417
+ return (_jsx("div", { className: cn("relative mx-2",
418
+ // Reserved for the whole drag, so the rows below settle once —
419
+ // before the user has aimed — and never while they are aiming.
420
+ // The transition rides on the RESTING class, so the reservation
421
+ // opens instantly and only the collapse animates: an animated
422
+ // opening is a moving target for as long as it runs.
423
+ dragActive ? "mt-0 h-7" : "-mt-1 h-1 transition-[height,margin] duration-150"), children: _jsx("div", { onDragEnter: (e) => {
424
+ if (!hasMatchingDragData(e))
425
+ return;
426
+ e.preventDefault();
427
+ // Hover state only — the band's size is the drag's business, not
428
+ // the pointer's. Opening it from here is the original bug: the row
429
+ // the user is aiming at slides away as they reach it.
430
+ dragCounter.current++;
431
+ setIsDragOver(true);
432
+ }, onDragLeave: () => {
433
+ dragCounter.current--;
434
+ if (dragCounter.current <= 0) {
435
+ dragCounter.current = 0;
436
+ setIsDragOver(false);
437
+ }
438
+ }, onDragOver: (e) => {
439
+ if (!hasMatchingDragData(e))
440
+ return;
441
+ e.preventDefault();
442
+ e.dataTransfer.dropEffect = hasExternalFiles(e) ? "copy" : "move";
443
+ }, onDrop: async (e) => {
444
+ const files = Array.from(e.dataTransfer.files ?? []);
445
+ const draggedData = acceptsEntities ? e.dataTransfer.getData(dnd.type) : "";
446
+ const hasFiles = acceptsFiles && files.length > 0 && dnd.onFileDrop;
447
+ const hasEntityDrop = Boolean(acceptsEntities && draggedData);
448
+ if (!hasFiles && !hasEntityDrop)
449
+ return;
450
+ e.preventDefault();
451
+ e.stopPropagation();
382
452
  dragCounter.current = 0;
383
- setIsFileDragOver(false);
384
453
  setIsDragOver(false);
385
- }
386
- }, onDragOver: (e) => {
387
- if (!hasMatchingDragData(e))
388
- return;
389
- e.preventDefault();
390
- e.dataTransfer.dropEffect = hasExternalFiles(e) ? "copy" : "move";
391
- }, onDrop: async (e) => {
392
- const files = Array.from(e.dataTransfer.files ?? []);
393
- const draggedData = acceptsEntities ? e.dataTransfer.getData(dnd.type) : "";
394
- const hasFiles = acceptsFiles && files.length > 0 && dnd.onFileDrop;
395
- const hasEntityDrop = Boolean(acceptsEntities && draggedData);
396
- if (!hasFiles && !hasEntityDrop)
397
- return;
398
- e.preventDefault();
399
- e.stopPropagation();
400
- dragCounter.current = 0;
401
- setIsFileDragOver(false);
402
- setIsDragOver(false);
403
- if (hasFiles) {
404
- await dnd.onFileDrop(files);
405
- return;
406
- }
407
- await dnd.onDrop(draggedData);
408
- }, className: cn("relative mx-2 rounded-md overflow-hidden", isDragOver
409
- ? cn("h-7 flex items-center justify-center gap-1", isFileDragOver
410
- ? "bg-primary/10 border border-dashed border-primary"
411
- : "bg-primary/10 border border-dashed border-primary/30")
412
- : "-mt-1 h-1"), children: isDragOver && (_jsxs(_Fragment, { children: [isFileDragOver && _jsx(Upload, { className: "size-3 text-primary" }), _jsx("span", { className: "text-[10px] text-primary/70", children: getContainerDropZoneLabel(isFileDragOver) })] })) }));
454
+ if (hasFiles) {
455
+ await dnd.onFileDrop(files);
456
+ return;
457
+ }
458
+ await dnd.onDrop(draggedData);
459
+ }, className: cn(
460
+ // Fills the reserved strip exactly: what is painted is what
461
+ // accepts a drop, and nothing extends over a neighbouring row.
462
+ "h-full w-full rounded-md overflow-hidden", dragActive && "flex items-center justify-center", isDragOver && "bg-primary/10 border border-dashed border-primary/30"), children: dragActive && (_jsx("span", { className: cn("text-[10px] transition-colors", isDragOver ? "text-primary/70" : "text-sidebar-foreground/35"), children: "Move to root" })) }) }));
413
463
  }
414
464
  /**
415
465
  * Leading disclosure cell on every nav row — the Confluence page-tree
@@ -5,4 +5,112 @@ export declare function hasExternalFilesData(dataTransfer: {
5
5
  } | null;
6
6
  }): boolean;
7
7
  export declare function hasAnyDropTarget(isDropTarget: boolean, isFileDropTarget: boolean): boolean;
8
- export declare function getContainerDropZoneLabel(hasExternalFiles: boolean): string;
8
+ /**
9
+ * Is a drag that the nav can accept in flight, anywhere — and what is it?
10
+ *
11
+ * Container drop zones are a 4px strip at rest and a labelled band while a
12
+ * drag is in flight, and WHEN they grow decides whether the sidebar is
13
+ * usable. Growing on hover is the trap, in both available shapes:
14
+ *
15
+ * - in flow, every row below the strip jumps 24px at the moment the pointer
16
+ * crosses it — which is the moment the user is aiming at one of them, so
17
+ * the destination walks away from the cursor;
18
+ * - out of flow, nothing moves, but the expanded band then covers ~12px of
19
+ * the rows on either side and takes their drops.
20
+ *
21
+ * So the space is reserved for the whole drag instead. The layout settles
22
+ * once, before the user has aimed at anything, and from then on every zone's
23
+ * hit area is exactly the band that is painted — no moving target, and no
24
+ * neighbour quietly answering for another.
25
+ *
26
+ * "Before the user has aimed" is the whole value, and it is also the whole
27
+ * PRECONDITION. Only a drag that starts in this document has a moment before
28
+ * the aim: `dragstart` fires before any movement, so the layout can settle
29
+ * while the pointer is still on the row being picked up.
30
+ *
31
+ * A drag arriving from OUTSIDE — the desktop, or another window — has no such
32
+ * moment. The browser hit-tests whatever is under the pointer before any
33
+ * script runs, so the first `dragenter` this document sees is already aimed
34
+ * at something; opening the zones from it moves that something out from under
35
+ * a stationary pointer. That is the very failure this whole change exists to
36
+ * remove, arriving by a different road.
37
+ *
38
+ * So outside drags get no reservation: the strips stay 4px and nothing moves.
39
+ * They are not left without a target — container rows accept and highlight
40
+ * file drops themselves, which is the path a file drag was always going to
41
+ * take. The root-drop affordance is a convenience with a redundant route; the
42
+ * mis-aim was a correctness failure with none.
43
+ *
44
+ * WHAT is tracked is the gesture's set of MIME types, not just "a drag is
45
+ * happening". Reserving space and painting a label is a promise that a drop
46
+ * will land, so only a zone that would take this particular drag may make it.
47
+ * A set rather than one type because `dragstart` BUBBLES: a draggable row
48
+ * sits inside a draggable container, both handlers run, and tracking one
49
+ * would track whichever ran last — the ancestor's — leaving zones that match
50
+ * the child's payload shut.
51
+ *
52
+ * The set is read directly and never serialised. It was briefly published as
53
+ * a joined string, for `useSyncExternalStore`'s identity comparison — but a
54
+ * MIME token may legally contain the separator (`|` is not one of RFC 2045's
55
+ * tspecials), and then one type splits into two. The snapshot is a version
56
+ * counter instead, so there is nothing to encode.
57
+ */
58
+ type NavDragListener = () => void;
59
+ /**
60
+ * Ends whatever drag is in flight.
61
+ *
62
+ * Bound to the window for `dragend` (a drop or a cancel) and `drop` (a
63
+ * source outside this document, which has no `dragend` here at all), and to
64
+ * the source element itself, whose own `dragend` is the only one left once a
65
+ * nav refresh has detached it.
66
+ *
67
+ * There is deliberately no `dragleave` ending. A relatedTarget-less
68
+ * `dragleave` is the only signal a drag leaving the window gives, and it is
69
+ * the same signal whether the drag is over or merely outside — so it ended
70
+ * live in-page drags, collapsing every zone for the rest of the gesture. It
71
+ * existed for drags with no `dragend` in this document, and none of those
72
+ * opens a reservation now.
73
+ */
74
+ export declare function endNavDrag(): void;
75
+ /**
76
+ * Called from `dragstart` on a nav row, with the MIME type that row wrote its
77
+ * payload under and the element it started from.
78
+ *
79
+ * The type ADDS rather than replaces: `dragstart` bubbles through every
80
+ * draggable ancestor, and each one's type belongs to the same gesture.
81
+ *
82
+ * The source is held so its `dragend` can be heard even after a nav refresh
83
+ * has removed the row mid-drag. A detached node still receives its own
84
+ * `dragend`, but the event reaches no window listener from there — so
85
+ * without this the reservation would stay open, every zone expanded, until
86
+ * some later drag happened to finish elsewhere.
87
+ */
88
+ export declare function beginNavDrag(type: string, source?: Element | null): void;
89
+ export declare function subscribeNavDrag(listener: NavDragListener): () => void;
90
+ /**
91
+ * The snapshot `useSyncExternalStore` compares: a version, bumped on every
92
+ * change. A primitive, so identity comparison is meaningful, and carrying no
93
+ * encoding of its own — see the note about `|` above.
94
+ */
95
+ export declare function getNavDragVersion(): number;
96
+ /**
97
+ * Would a zone with this `dnd.type` accept the live drag?
98
+ *
99
+ * Reserving space and painting a label is a promise that a drop will land,
100
+ * so only a zone that would actually take this drag may do either. "A drag
101
+ * is happening" was not enough: two modules with different `dnd.type`s
102
+ * opened each other's zones.
103
+ *
104
+ * Files need no case here. Only a nav row's `dragstart` opens a reservation
105
+ * and those carry an entity, so no zone ever opens for a file drag whatever
106
+ * it is configured to accept. It still HANDLES one — the resting strip takes
107
+ * a file drop like any other target — and the container's own row advertises
108
+ * that same drop at full size.
109
+ */
110
+ export declare function navDragAccepts(zone: {
111
+ type: string;
112
+ acceptsEntities: boolean;
113
+ }): boolean;
114
+ /** Test seam — module-level state outlives a test file otherwise. */
115
+ export declare function __resetNavDragForTest(): void;
116
+ export {};
@@ -4,6 +4,106 @@ export function hasExternalFilesData(dataTransfer) {
4
4
  export function hasAnyDropTarget(isDropTarget, isFileDropTarget) {
5
5
  return isDropTarget || isFileDropTarget;
6
6
  }
7
- export function getContainerDropZoneLabel(hasExternalFiles) {
8
- return hasExternalFiles ? "Upload to root" : "Move to root";
7
+ const navDragListeners = new Set();
8
+ /** The MIME types this gesture carries. Empty means no drag is in flight. */
9
+ const navDragTypes = new Set();
10
+ /** Bumped on every change — the `useSyncExternalStore` snapshot. */
11
+ let navDragVersion = 0;
12
+ /** The element the drag started from, when we were given one — so its
13
+ * `dragend` is heard even after a nav refresh detaches it mid-drag. */
14
+ let navDragSource = null;
15
+ let navDragWatchBound = false;
16
+ function publish() {
17
+ navDragVersion += 1;
18
+ for (const listener of navDragListeners)
19
+ listener();
20
+ }
21
+ /**
22
+ * Ends whatever drag is in flight.
23
+ *
24
+ * Bound to the window for `dragend` (a drop or a cancel) and `drop` (a
25
+ * source outside this document, which has no `dragend` here at all), and to
26
+ * the source element itself, whose own `dragend` is the only one left once a
27
+ * nav refresh has detached it.
28
+ *
29
+ * There is deliberately no `dragleave` ending. A relatedTarget-less
30
+ * `dragleave` is the only signal a drag leaving the window gives, and it is
31
+ * the same signal whether the drag is over or merely outside — so it ended
32
+ * live in-page drags, collapsing every zone for the rest of the gesture. It
33
+ * existed for drags with no `dragend` in this document, and none of those
34
+ * opens a reservation now.
35
+ */
36
+ export function endNavDrag() {
37
+ navDragSource = null;
38
+ if (navDragTypes.size === 0)
39
+ return;
40
+ navDragTypes.clear();
41
+ publish();
42
+ }
43
+ function bindNavDragWatch() {
44
+ if (navDragWatchBound || typeof window === "undefined")
45
+ return;
46
+ navDragWatchBound = true;
47
+ window.addEventListener("dragend", endNavDrag, true);
48
+ window.addEventListener("drop", endNavDrag, true);
49
+ }
50
+ /**
51
+ * Called from `dragstart` on a nav row, with the MIME type that row wrote its
52
+ * payload under and the element it started from.
53
+ *
54
+ * The type ADDS rather than replaces: `dragstart` bubbles through every
55
+ * draggable ancestor, and each one's type belongs to the same gesture.
56
+ *
57
+ * The source is held so its `dragend` can be heard even after a nav refresh
58
+ * has removed the row mid-drag. A detached node still receives its own
59
+ * `dragend`, but the event reaches no window listener from there — so
60
+ * without this the reservation would stay open, every zone expanded, until
61
+ * some later drag happened to finish elsewhere.
62
+ */
63
+ export function beginNavDrag(type, source) {
64
+ bindNavDragWatch();
65
+ if (source && source !== navDragSource) {
66
+ navDragSource = source;
67
+ source.addEventListener("dragend", endNavDrag, { once: true });
68
+ }
69
+ if (navDragTypes.has(type))
70
+ return;
71
+ navDragTypes.add(type);
72
+ publish();
73
+ }
74
+ export function subscribeNavDrag(listener) {
75
+ bindNavDragWatch();
76
+ navDragListeners.add(listener);
77
+ return () => navDragListeners.delete(listener);
78
+ }
79
+ /**
80
+ * The snapshot `useSyncExternalStore` compares: a version, bumped on every
81
+ * change. A primitive, so identity comparison is meaningful, and carrying no
82
+ * encoding of its own — see the note about `|` above.
83
+ */
84
+ export function getNavDragVersion() {
85
+ return navDragVersion;
86
+ }
87
+ /**
88
+ * Would a zone with this `dnd.type` accept the live drag?
89
+ *
90
+ * Reserving space and painting a label is a promise that a drop will land,
91
+ * so only a zone that would actually take this drag may do either. "A drag
92
+ * is happening" was not enough: two modules with different `dnd.type`s
93
+ * opened each other's zones.
94
+ *
95
+ * Files need no case here. Only a nav row's `dragstart` opens a reservation
96
+ * and those carry an entity, so no zone ever opens for a file drag whatever
97
+ * it is configured to accept. It still HANDLES one — the resting strip takes
98
+ * a file drop like any other target — and the container's own row advertises
99
+ * that same drop at full size.
100
+ */
101
+ export function navDragAccepts(zone) {
102
+ return zone.acceptsEntities && navDragTypes.has(zone.type);
103
+ }
104
+ /** Test seam — module-level state outlives a test file otherwise. */
105
+ export function __resetNavDragForTest() {
106
+ navDragTypes.clear();
107
+ navDragSource = null;
108
+ navDragVersion = 0;
9
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.33.0",
3
+ "version": "0.33.2",
4
4
  "license": "MIT",
5
5
  "description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
6
6
  "keywords": [
@@ -80,8 +80,8 @@
80
80
  "recharts": "^3.10.1",
81
81
  "remark-gfm": "^4.0.0",
82
82
  "tailwind-merge": "^3.5.0",
83
- "@iloveagents/foundry-agent": "^0.33.0",
84
- "@iloveagents/foundry-web-primitives": "^0.33.0"
83
+ "@iloveagents/foundry-agent": "^0.33.2",
84
+ "@iloveagents/foundry-web-primitives": "^0.33.2"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@ag-ui/client": "^0.0.52",