@iloveagents/foundry-web-ui 0.29.1 → 0.31.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.
Files changed (48) hide show
  1. package/dist/components/ag-ui-runtime-provider.d.ts +8 -3
  2. package/dist/components/ag-ui-runtime-provider.js +76 -33
  3. package/dist/components/assistant-chat.d.ts +1 -15
  4. package/dist/components/assistant-chat.js +4 -5
  5. package/dist/components/chat-bubble.js +3 -4
  6. package/dist/components/chat-context-items.d.ts +6 -0
  7. package/dist/components/chat-context-items.js +25 -0
  8. package/dist/components/chat-context.js +10 -2
  9. package/dist/components/chat-empty-state.js +1 -1
  10. package/dist/components/chat-header.js +2 -1
  11. package/dist/components/composer-add-menu.d.ts +0 -19
  12. package/dist/components/composer-add-menu.js +2 -10
  13. package/dist/components/context-badges.d.ts +0 -14
  14. package/dist/components/context-badges.js +2 -29
  15. package/dist/components/context-bar.d.ts +1 -21
  16. package/dist/components/context-bar.js +3 -74
  17. package/dist/components/focus-chat-pane.js +1 -1
  18. package/dist/components/markdown-text.js +6 -4
  19. package/dist/components/sidebar.js +51 -17
  20. package/dist/components/tool-panel.js +1 -1
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.js +5 -4
  23. package/dist/lib/ag-ui-adapter.d.ts +3 -0
  24. package/dist/lib/ag-ui-adapter.js +75 -11
  25. package/dist/lib/auth-provider.js +1 -1
  26. package/dist/lib/chat-runs-store.d.ts +40 -0
  27. package/dist/lib/chat-runs-store.js +173 -0
  28. package/dist/lib/merge-chat-state.d.ts +3 -0
  29. package/dist/lib/merge-chat-state.js +21 -0
  30. package/dist/lib/nav-config.js +47 -20
  31. package/dist/lib/theme-runtime.d.ts +23 -0
  32. package/dist/lib/theme-runtime.js +210 -30
  33. package/dist/lib/theme-store.d.ts +3 -1
  34. package/dist/lib/theme-store.js +3 -2
  35. package/dist/lib/use-new-conversation.js +4 -8
  36. package/dist/styles.css +14 -0
  37. package/dist/workbench/chat-header.js +4 -3
  38. package/dist/workbench/conversation-history.d.ts +1 -1
  39. package/dist/workbench/conversation-history.js +75 -19
  40. package/dist/workbench/running-chats-menu.d.ts +3 -0
  41. package/dist/workbench/running-chats-menu.js +20 -0
  42. package/dist/workbench/selection-bridge.js +2 -5
  43. package/dist/workbench/use-workbench-state.d.ts +1 -2
  44. package/dist/workbench/use-workbench-state.js +15 -18
  45. package/dist/workbench/welcome-intro.js +1 -1
  46. package/dist/workbench/workbench-styles.js +54 -6
  47. package/dist/workbench/workbench.js +1 -1
  48. package/package.json +3 -3
@@ -1,23 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PinStateIcon } from "./pin-state-icon.js";
3
- /**
4
- * Context Pins — pin button with count badge and hover flyout.
5
- *
6
- * Click: toggles pin/unpin current page.
7
- * Hover (when pins exist): shows flyout to view, navigate, and remove pins.
8
- */
9
- import { useState, useRef, useEffect, createElement } from "react";
10
- import { useNavigate } from "react-router";
11
- import { X, TextSelect, FileText, Link2, StickyNote } from "lucide-react";
12
- import { cn } from "@iloveagents/foundry-web-primitives";
13
- import { TooltipIconButton } from "./tooltip-icon-button.js";
14
1
  import { useAppStore } from "../lib/app-store.js";
15
- const CONTEXT_ICONS = {
16
- selection: TextSelect,
17
- page: FileText,
18
- ref: Link2,
19
- note: StickyNote,
20
- };
21
2
  /** Resolve the URL a context item navigates to when clicked.
22
3
  *
23
4
  * Two shapes the framework knows about:
@@ -59,15 +40,14 @@ export function getNavigablePath(item) {
59
40
  return null;
60
41
  }
61
42
  /**
62
- * The "is this page pinned, and how do I flip that" logic, shared by the
63
- * pin badge here and the composer's add-menu. Two call sites deciding
64
- * independently what "pinned" means is how they end up disagreeing.
43
+ * Pin the current navigation context from the With menu.
65
44
  */
66
45
  export function usePagePin() {
67
46
  const items = useAppStore((s) => s.contextItems);
68
47
  const removeItem = useAppStore((s) => s.removeContextItem);
69
48
  const addContextItem = useAppStore((s) => s.addContextItem);
70
- const currentPage = useAppStore((s) => s.currentPage);
49
+ const page = useAppStore((s) => s.currentPage);
50
+ const currentPage = page;
71
51
  const pageLabel = useAppStore((s) => s.navContext.label);
72
52
  const navMeta = useAppStore((s) => s.navContext.meta);
73
53
  const currentPagePin = items.find((i) => i.persistence === "persistent" &&
@@ -91,54 +71,3 @@ export function usePagePin() {
91
71
  },
92
72
  };
93
73
  }
94
- export const ContextPins = ({ allowPageToggle = true, showPinButton = false, compact = false, flyoutDirection = "up", }) => {
95
- const items = useAppStore((s) => s.contextItems);
96
- const removeItem = useAppStore((s) => s.removeContextItem);
97
- const currentPage = useAppStore((s) => s.currentPage);
98
- const { isPinned: isCurrentPagePinned, toggle: handleTogglePin } = usePagePin();
99
- const navigate = useNavigate();
100
- const [flyoutOpen, setFlyoutOpen] = useState(false);
101
- const containerRef = useRef(null);
102
- const hoverTimeout = useRef(undefined);
103
- const pinned = items.filter((i) => i.persistence === "persistent");
104
- // Close flyout on outside click
105
- useEffect(() => {
106
- if (!flyoutOpen)
107
- return;
108
- const handleClick = (e) => {
109
- if (containerRef.current && !containerRef.current.contains(e.target)) {
110
- setFlyoutOpen(false);
111
- }
112
- };
113
- document.addEventListener("mousedown", handleClick);
114
- return () => document.removeEventListener("mousedown", handleClick);
115
- }, [flyoutOpen]);
116
- const handleMouseEnter = () => {
117
- clearTimeout(hoverTimeout.current);
118
- if (pinned.length > 0)
119
- setFlyoutOpen(true);
120
- };
121
- const handleMouseLeave = () => {
122
- hoverTimeout.current = setTimeout(() => setFlyoutOpen(false), 200);
123
- };
124
- if (pinned.length === 0 && !showPinButton)
125
- return null;
126
- return (_jsxs("div", { className: "relative", ref: containerRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleMouseEnter, onBlur: (event) => {
127
- if (!event.currentTarget.contains(event.relatedTarget))
128
- setFlyoutOpen(false);
129
- }, children: [_jsxs(TooltipIconButton, { tooltip: allowPageToggle
130
- ? isCurrentPagePinned
131
- ? "Unpin this page"
132
- : "Pin this page"
133
- : "Manage pinned context", size: "icon", className: cn("shrink-0 relative", compact ? "size-7" : "size-8", isCurrentPagePinned ? "text-foreground" : "text-muted-foreground"), "aria-pressed": allowPageToggle ? isCurrentPagePinned : undefined, "aria-expanded": !allowPageToggle ? flyoutOpen : undefined, onClick: allowPageToggle ? handleTogglePin : () => setFlyoutOpen(true), children: [_jsx(PinStateIcon, { pinned: isCurrentPagePinned, className: compact ? "size-3.5" : "size-4" }), pinned.length > 0 && (_jsx("span", { className: cn("absolute rounded-full bg-primary font-medium text-primary-foreground flex items-center justify-center", compact
134
- ? "-top-0.5 -right-0.5 size-3.5 text-[9px]"
135
- : "top-0 right-0 size-3.5 text-[9px]"), children: pinned.length }))] }), flyoutOpen && pinned.length > 0 && (_jsxs("div", { className: cn("absolute z-50 w-64 rounded-lg border border-border bg-background shadow-xl p-2 space-y-1", flyoutDirection === "up" ? "bottom-full mb-2 left-0" : "top-9 right-0"), children: [_jsx("div", { className: "px-1 pb-1 border-b border-border/50", children: _jsx("span", { className: "text-[10px] font-medium text-muted-foreground uppercase tracking-wide", children: "Pinned Context" }) }), pinned.map((item) => {
136
- const navPath = getNavigablePath(item);
137
- return (_jsxs("div", { className: "flex items-center gap-1.5 px-1 py-1 rounded hover:bg-muted group/pin", children: [createElement(CONTEXT_ICONS[item.type] ?? TextSelect, {
138
- className: "size-3 text-primary shrink-0",
139
- }), navPath ? (_jsx("button", { type: "button", onClick: () => {
140
- navigate(navPath);
141
- setFlyoutOpen(false);
142
- }, className: "text-xs text-foreground truncate flex-1 text-left hover:underline", children: item.label })) : (_jsx("span", { className: "text-xs text-foreground truncate flex-1", children: item.label })), _jsx("button", { type: "button", onClick: () => removeItem(item.id), className: cn("p-0.5 rounded hover:bg-primary/10 transition-opacity shrink-0", "opacity-0 group-hover/pin:opacity-100"), "aria-label": `Remove ${item.label}`, children: _jsx(X, { className: "size-3 text-muted-foreground" }) })] }, item.id));
143
- })] }))] }));
144
- };
@@ -8,5 +8,5 @@ import { FocusPaneResize } from "./data-table/focus-pane-resize.js";
8
8
  import { ChatContent } from "./assistant-chat.js";
9
9
  export function FocusChatPane({ chatOpen, setChatOpen, chatWidth, setChatWidth, source, onClose, }) {
10
10
  const pageLabel = useAppStore((state) => state.navContext.label);
11
- return (_jsxs("aside", { "aria-label": "Focused content chat", style: { "--focus-chat-width": `${chatWidth ?? 384}px` }, className: cn("relative flex min-h-0 shrink-0 flex-col border-r border-border bg-background", chatOpen ? "w-full md:w-[var(--focus-chat-width)]" : "w-11", source && "hidden xl:flex"), children: [_jsx("div", { className: cn("flex h-12 shrink-0 items-center", chatOpen ? "gap-1 border-b border-border px-2" : "justify-center"), children: chatOpen ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1 px-2 text-sm font-semibold", children: "Chat" }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", "aria-label": "Collapse chat", className: "size-8", onClick: () => setChatOpen(false), children: _jsx(PanelLeft, { size: 18 }) }), _jsx(Button, { variant: "ghost", size: "icon", title: "Close", "aria-label": "Close", className: "size-8 md:hidden", onClick: () => onClose(), children: _jsx(X, { size: 18 }) })] })) : (_jsx(TooltipIconButton, { tooltip: "Expand chat", "aria-label": "Expand chat", "aria-expanded": false, className: "size-8", onClick: () => setChatOpen(true), children: _jsx(MessageSquare, { size: 18 }) })) }), chatOpen && (_jsx(FocusPaneResize, { label: "chat", side: "right", width: chatWidth, onResize: setChatWidth })), chatOpen && (_jsx(ChatContent, { emptyState: _jsx(ChatEmptyState, { pageLabel: pageLabel, homeStarterPriority: 20 }), compactComposer: true, showPinButton: false, hideCurrentPageBadge: true, starterSuggestions: [] }))] }));
11
+ return (_jsxs("aside", { "aria-label": "Focused content chat", style: { "--focus-chat-width": `${chatWidth ?? 384}px` }, className: cn("relative flex min-h-0 shrink-0 flex-col border-r border-border bg-background", chatOpen ? "w-full md:w-[var(--focus-chat-width)]" : "w-11", source && "hidden xl:flex"), children: [_jsx("div", { className: cn("flex h-12 shrink-0 items-center", chatOpen ? "gap-1 border-b border-border px-2" : "justify-center"), children: chatOpen ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1 px-2 text-sm font-semibold", children: "Chat" }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", "aria-label": "Collapse chat", className: "size-8", onClick: () => setChatOpen(false), children: _jsx(PanelLeft, { size: 18 }) }), _jsx(Button, { variant: "ghost", size: "icon", title: "Close", "aria-label": "Close", className: "size-8 md:hidden", onClick: () => onClose(), children: _jsx(X, { size: 18 }) })] })) : (_jsx(TooltipIconButton, { tooltip: "Expand chat", "aria-label": "Expand chat", "aria-expanded": false, className: "size-8", onClick: () => setChatOpen(true), children: _jsx(MessageSquare, { size: 18 }) })) }), chatOpen && (_jsx(FocusPaneResize, { label: "chat", side: "right", width: chatWidth, onResize: setChatWidth })), chatOpen && (_jsx(ChatContent, { emptyState: _jsx(ChatEmptyState, { pageLabel: pageLabel, homeStarterPriority: 20 }), compactComposer: true, starterSuggestions: [] }))] }));
12
12
  }
@@ -132,10 +132,12 @@ function extractCodeInfo(children) {
132
132
  * Shared markdown component overrides used by both chat messages and the tool panel.
133
133
  */
134
134
  export const markdownComponents = {
135
- h1: ({ children, ...props }) => (_jsx("h1", { className: "text-2xl font-bold mt-6 mb-4 text-foreground", ...props, children: children })),
136
- h2: ({ children, ...props }) => (_jsx("h2", { className: "text-xl font-bold mt-5 mb-3 text-foreground", ...props, children: children })),
137
- h3: ({ children, ...props }) => (_jsx("h3", { className: "text-lg font-semibold mt-4 mb-2 text-foreground", ...props, children: children })),
138
- h4: ({ children, ...props }) => (_jsx("h4", { className: "text-base font-semibold mt-3 mb-1.5 text-foreground", ...props, children: children })),
135
+ h1: ({ children, ...props }) => (_jsx("h1", { className: "text-2xl font-bold mt-6 mb-4 text-content-heading-1", ...props, children: children })),
136
+ h2: ({ children, ...props }) => (_jsx("h2", { className: "text-xl font-bold mt-5 mb-3 text-content-heading-2", ...props, children: children })),
137
+ h3: ({ children, ...props }) => (_jsx("h3", { className: "text-lg font-semibold mt-4 mb-2 text-content-heading-3", ...props, children: children })),
138
+ h4: ({ children, ...props }) => (_jsx("h4", { className: "text-base font-semibold mt-3 mb-1.5 text-content-heading-4", ...props, children: children })),
139
+ h5: ({ children, ...props }) => (_jsx("h5", { className: "mt-3 mb-1.5 text-sm font-semibold text-content-heading-5", ...props, children: children })),
140
+ h6: ({ children, ...props }) => (_jsx("h6", { className: "mt-3 mb-1.5 text-xs font-semibold text-content-heading-6", ...props, children: children })),
139
141
  p: ({ children, ...props }) => (_jsx("p", { className: "mb-2 leading-relaxed text-foreground", ...props, children: injectCitations(children) })),
140
142
  ul: ({ children, ...props }) => (_jsx("ul", { className: "list-disc list-outside pl-6 mb-4 space-y-1 text-foreground", ...props, children: children })),
141
143
  ol: ({ children, ...props }) => (_jsx("ol", { className: "list-decimal list-outside pl-6 mb-4 space-y-1 text-foreground", ...props, children: children })),
@@ -13,9 +13,9 @@ 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";
15
15
  import { AppBrand } from "./app-brand.js";
16
- const ACTIVE_NAV_ROW_CLASS = "border border-transparent bg-sidebar-selected shadow-none";
17
- const ACTIVE_NAV_TEXT_CLASS = "font-semibold text-sidebar-foreground";
18
- const ACTIVE_NAV_ICON_CLASS = "text-primary";
16
+ const ACTIVE_NAV_ROW_CLASS = "border border-transparent bg-sidebar-selected text-sidebar-selected-foreground shadow-none";
17
+ const ACTIVE_NAV_TEXT_CLASS = "font-semibold text-sidebar-selected-foreground";
18
+ const ACTIVE_NAV_ICON_CLASS = "text-sidebar-selected-foreground";
19
19
  const ACTIVE_NAV_BADGE_CLASS = "border border-sidebar-border bg-background/60 text-sidebar-foreground";
20
20
  // Indent ladder for the nav tree, indexed by depth (0 = top-level item).
21
21
  //
@@ -437,13 +437,15 @@ function ContainerDropZone({ dnd }) {
437
437
  function NavTwisty({ hasChildren, childrenUnloaded = false, isExpanded, onToggle, label, isActive, depth, inTree = true, }) {
438
438
  const cell = cn(NAV_TWISTY_CLASS, getNavDepthOffsets(depth).column);
439
439
  if (!hasChildren) {
440
- return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("block size-1 shrink-0 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
440
+ return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("block size-1 shrink-0 rounded-full", isActive ? "bg-sidebar-selected-foreground/70" : "bg-sidebar-foreground/30") })) }));
441
441
  }
442
442
  return (_jsx("span", { "data-nav-twisty-cell": true, className: cell, children: _jsx("button", { type: "button", "data-nav-twisty": true, "aria-expanded": isExpanded, "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`, onClick: (e) => {
443
443
  e.preventDefault();
444
444
  e.stopPropagation();
445
445
  onToggle?.();
446
- }, className: cn("flex items-center justify-center rounded", NAV_TWISTY_HIT_CLASS, isActive ? "text-primary" : "text-sidebar-foreground/50 hover:text-sidebar-foreground"), children: _jsx(ChevronRight, { className: cn("size-3.5 transition-transform duration-150", isExpanded && "rotate-90") }) }) }));
446
+ }, className: cn("flex items-center justify-center rounded", NAV_TWISTY_HIT_CLASS, isActive
447
+ ? "text-sidebar-selected-foreground"
448
+ : "text-sidebar-foreground/50 hover:text-sidebar-foreground"), children: _jsx(ChevronRight, { className: cn("size-3.5 transition-transform duration-150", isExpanded && "rotate-90") }) }) }));
447
449
  }
448
450
  function NavItemActions({ actions }) {
449
451
  return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7 rounded-md text-muted-foreground hover:bg-muted/80 hover:text-foreground", onClick: (e) => {
@@ -506,10 +508,11 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
506
508
  }
507
509
  function SubNavItems({ items, parentPath, depth = 1, }) {
508
510
  const navigate = useNavigate();
509
- const currentPath = useLocation().pathname;
511
+ const location = useLocation();
512
+ const currentPath = location.pathname + location.search;
510
513
  const setNavContext = useAppStore((s) => s.setNavContext);
511
514
  const navContext = useAppStore((s) => s.navContext);
512
- const isOnParent = currentPath === parentPath;
515
+ const isOnParent = matchesNavLocation(currentPath, parentPath);
513
516
  const [activeLabel, setActiveLabel] = useState(items[0]?.label ?? "");
514
517
  // Children with unique paths (e.g., SPACES documents) use URL matching;
515
518
  // children sharing the parent path (e.g., Documents sub-filters) use label matching.
@@ -543,19 +546,40 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
543
546
  }
544
547
  /** Check if current page is inside a nav item's subtree. */
545
548
  function isInSubtree(currentPage, item) {
546
- if (currentPage === item.to)
549
+ if (matchesNavLocation(currentPage, item.to))
547
550
  return true;
548
551
  return item.children?.some((c) => isInSubtree(currentPage, c)) ?? false;
549
552
  }
553
+ /** A navigation target may declare stable scope parameters while the page
554
+ * adds transient state such as a selected row or table sort. */
555
+ function matchesNavLocation(currentPage, target) {
556
+ const current = new URL(currentPage, "http://navigation.local");
557
+ const destination = new URL(target, current.origin);
558
+ if (current.pathname.replace(/\/+$/, "") !== destination.pathname.replace(/\/+$/, "")) {
559
+ return false;
560
+ }
561
+ if (destination.searchParams.size === 0)
562
+ return true;
563
+ return [...destination.searchParams].every(([key, value]) => current.searchParams.getAll(key).includes(value));
564
+ }
565
+ function hasMoreSpecificNavMatch(currentPage, target) {
566
+ const count = new URL(target, "http://navigation.local").searchParams.size;
567
+ const walk = (items) => items.some((item) => (matchesNavLocation(currentPage, item.to) &&
568
+ new URL(item.to, "http://navigation.local").searchParams.size > count) ||
569
+ walk(item.children ?? []));
570
+ return useNavStore.getState().config.some((group) => walk(group.items));
571
+ }
550
572
  /** A list owns the sidebar selection of descendants it deliberately does not draw.
551
573
  * The route lookup still resolves the actual descendant for chat and page actions. */
552
574
  function isSelectedNavItem(currentPage, item) {
553
- return currentPage === item.to || Boolean(item.childrenHidden && isInSubtree(currentPage, item));
575
+ return ((matchesNavLocation(currentPage, item.to) && !hasMoreSpecificNavMatch(currentPage, item.to)) ||
576
+ Boolean(item.childrenHidden && isInSubtree(currentPage, item)));
554
577
  }
555
578
  /** Nested folder with collapsible children (used inside SubNavItems). */
556
579
  function NestedFolderItem({ item, depth }) {
557
580
  const navigate = useNavigate();
558
- const currentPath = useLocation().pathname;
581
+ const location = useLocation();
582
+ const currentPath = location.pathname + location.search;
559
583
  const setNavContext = useAppStore((s) => s.setNavContext);
560
584
  const navContext = useAppStore((s) => s.navContext);
561
585
  const isInsideSubtree = isInSubtree(currentPath, item);
@@ -624,7 +648,8 @@ function NestedFolderItem({ item, depth }) {
624
648
  }
625
649
  function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader = false, }) {
626
650
  const navigate = useNavigate();
627
- const currentPath = useLocation().pathname;
651
+ const location = useLocation();
652
+ const currentPath = location.pathname + location.search;
628
653
  const setNavContext = useAppStore((s) => s.setNavContext);
629
654
  const navContext = useAppStore((s) => s.navContext);
630
655
  const isInsideSubtree = isInSubtree(currentPath, item);
@@ -643,7 +668,9 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
643
668
  : heading || !underHeader
644
669
  ? NAV_FLAT_BODY_CLASS
645
670
  : getNavDepthOffsets(0).body;
646
- const headingClass = heading && !isDisabled && "font-semibold text-sidebar-foreground";
671
+ const headingClass = heading &&
672
+ !isDisabled &&
673
+ `font-semibold ${isActive ? "text-sidebar-selected-foreground" : "text-sidebar-primary"}`;
647
674
  const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
648
675
  const rendersRow = Boolean(hasChildren || hasActions);
649
676
  const activeRowRef = useActiveNavRowScroll(isActive && rendersRow);
@@ -693,7 +720,12 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
693
720
  // Transparent border for box-model parity — see the NavLink variant.
694
721
  "rounded-lg border border-transparent pr-3 py-1.5 text-left text-sm text-sidebar-foreground/45"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, label: item.label, depth: 0 }), _jsx(item.icon, { className: "size-4 shrink-0 text-sidebar-foreground/45" }), _jsx("span", { className: "truncate", children: item.label })] }));
695
722
  }
696
- return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: () => cn("relative flex items-center gap-2 w-full", bodyClass,
723
+ return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": 0, to: item.to, onClick: (event) => {
724
+ if (!isActive || !matchesNavLocation(currentPath, item.to))
725
+ return;
726
+ event.preventDefault();
727
+ useSidebarStore.getState().closeMobile();
728
+ }, ...draggableProps, ...dropTargetProps, className: () => cn("relative flex items-center gap-2 w-full", bodyClass,
697
729
  // The transparent border is NOT decoration: it is what makes this
698
730
  // row's box model match every other variant. An absolutely
699
731
  // positioned twisty resolves against the PADDING box, so a row
@@ -766,7 +798,8 @@ function SidebarResizeHandle() {
766
798
  // group is now the source of truth for "you're in a chat right now".
767
799
  // Kept as a comment so future spelunkers know it was intentional.
768
800
  function CollapsedNavShortcut({ to, label, icon: Icon, isActive, }) {
769
- return (_jsx(NavLink, { to: to, children: _jsx(TooltipIconButton, { tooltip: label, side: "right", className: cn(isActive && "border border-transparent bg-sidebar-selected text-sidebar-foreground"), children: _jsx(Icon, { className: cn("size-4", isActive ? "text-primary" : "text-sidebar-icon") }) }) }, to));
801
+ return (_jsx(NavLink, { to: to, children: _jsx(TooltipIconButton, { tooltip: label, side: "right", className: cn(isActive &&
802
+ "border border-transparent bg-sidebar-selected text-sidebar-selected-foreground"), children: _jsx(Icon, { className: cn("size-4", isActive ? "text-sidebar-selected-foreground" : "text-sidebar-icon") }) }) }, to));
770
803
  }
771
804
  // ---------------------------------------------------------------------------
772
805
  // Collapsible nav-group state — persisted per-label in localStorage so the
@@ -953,7 +986,7 @@ function NavGroupSection({ group }) {
953
986
  // collapsible headers and item labels.
954
987
  _jsxs("div", { "data-nav-group-header": true, className: cn("flex items-center justify-between py-1.5 transition-colors hover:bg-sidebar-accent/70", SIDEBAR_GROUP_HEADER_BOX, SIDEBAR_ITEM_GAP_CLASS), children: [_jsx("span", { "aria-hidden": "true", className: SIDEBAR_ITEM_ICON_CLASS }), _jsx(NavLink, { to: group.to, end: true, className: "min-w-0 flex-1", children: ({ isActive }) => (_jsx("span", { className: cn("block", SIDEBAR_GROUP_HEADER_LABEL_CLASS, isActive
955
988
  ? "text-sidebar-selected-foreground"
956
- : "text-sidebar-foreground/75 hover:text-sidebar-foreground"), children: group.label })) }), group.createActions && group.createActions.length > 0 && (_jsx(CreateActionsMenu, { group: group }))] })) : (_jsxs("div", { "data-nav-group-header": true, className: cn("flex items-center", SIDEBAR_GROUP_HEADER_BOX, SIDEBAR_ITEM_GAP_CLASS), children: [_jsx("span", { "aria-hidden": "true", className: SIDEBAR_ITEM_ICON_CLASS }), _jsx("span", { className: cn("text-sidebar-foreground/75", SIDEBAR_GROUP_HEADER_LABEL_CLASS), children: group.label })] })) })), !collapsed && (_jsx("div", { className: cn(animateRows &&
989
+ : "text-sidebar-foreground/75 hover:text-sidebar-foreground"), children: group.label })) }), group.createActions && group.createActions.length > 0 && (_jsx(CreateActionsMenu, { group: group }))] })) : (_jsxs("div", { "data-nav-group-header": true, className: cn("flex items-center", SIDEBAR_GROUP_HEADER_BOX, SIDEBAR_ITEM_GAP_CLASS), children: [_jsx("span", { "aria-hidden": "true", className: SIDEBAR_ITEM_ICON_CLASS }), _jsx("span", { className: cn("text-sidebar-foreground/75", SIDEBAR_GROUP_HEADER_LABEL_CLASS), children: group.label })] })) })), !collapsed && (_jsx("div", { "data-nav-group-items": true, className: cn(animateRows &&
957
990
  "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-top-1 motion-safe:duration-200"), children: (focused ? [focusedItem] : group.items).map((item) => (_jsx(CollapsibleNavItem, { item: item,
958
991
  // A leaf dot distinguishes "nothing under this" from a sibling
959
992
  // that branches. Decided per GROUP: Workspaces has branches, so
@@ -996,7 +1029,8 @@ function SidebarContent({ collapsed, groups, onNewConversation, hideFooterAction
996
1029
  // still resolving, so the skeleton never flashes over a populated tree.
997
1030
  const hasWorkspacesGroup = navConfig.some((group) => group.label === "Workspaces" || group.meta?.type === "spaces-workspaces");
998
1031
  const showNavSkeleton = navLoading && !hasWorkspacesGroup;
999
- const currentPage = useLocation().pathname;
1032
+ const location = useLocation();
1033
+ const currentPage = location.pathname + location.search;
1000
1034
  const navContext = useAppStore((s) => s.navContext);
1001
1035
  useScrollActiveNavRow(currentPage, navConfig, collapsed);
1002
1036
  const preventNativeFileDrop = useCallback((e) => {
@@ -1050,7 +1084,7 @@ function SidebarContent({ collapsed, groups, onNewConversation, hideFooterAction
1050
1084
  to: it.to,
1051
1085
  label: it.label,
1052
1086
  icon: it.icon,
1053
- isActive: currentPage === it.to,
1087
+ isActive: isSelectedNavItem(currentPage, it),
1054
1088
  }));
1055
1089
  return { group, groupIndex, items };
1056
1090
  })
@@ -141,5 +141,5 @@ export function ToolPanel({ contained = false }) {
141
141
  else
142
142
  return;
143
143
  event.preventDefault();
144
- }, className: cn("absolute left-0 top-0 z-10 h-full w-2 -translate-x-1/2", "cursor-col-resize touch-none", "before:absolute before:inset-y-0 before:left-1/2 before:w-px before:-translate-x-1/2", "before:bg-transparent hover:before:bg-primary/40 focus-visible:before:bg-primary", "focus:outline-none") })) : null, _jsx("div", { className: "flex-shrink-0 border-b border-border px-6 py-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold text-foreground truncate pr-4", children: content.title }), _jsxs("div", { className: "flex items-center gap-1", children: [entityId && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: handleToggleEntityPin, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", isEntityPinned ? "text-foreground" : "text-muted-foreground"), "aria-pressed": isEntityPinned, "aria-label": isEntityPinned ? "Unpin document" : "Pin document", title: isEntityPinned ? "Unpin document" : "Pin document", children: _jsx(PinStateIcon, { pinned: isEntityPinned }) }), _jsx("button", { type: "button", onClick: handleNavigateToEntity, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Open in navigation", title: "Open in navigation", children: _jsx(FolderOpen, { className: "size-4 text-muted-foreground" }) })] })), _jsx("button", { type: "button", onClick: handleCopyAll, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Copy content", children: copied ? (_jsx(Check, { className: "size-4 text-success" })) : (_jsx(Copy, { className: "size-4 text-muted-foreground" })) }), !contained && (_jsx("button", { type: "button", onClick: toggleFullscreen, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": isFullscreen ? "Exit fullscreen" : "Fullscreen", children: isFullscreen ? (_jsx(Minimize2, { className: "size-4 text-muted-foreground" })) : (_jsx(Maximize2, { className: "size-4 text-muted-foreground" })) })), _jsx("button", { type: "button", onClick: closePanel, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Close panel", children: _jsx(X, { className: "size-5 text-muted-foreground" }) })] })] }) }), _jsxs("div", { className: "relative flex-1 overflow-y-auto", ref: contentRef, "data-testid": "tool-panel-content", children: [_jsx(PanelContentRenderer, { content: content }), _jsx(SelectionPopover, { containerRef: contentRef })] })] }));
144
+ }, className: cn("absolute left-0 top-0 z-10 h-full w-2 -translate-x-1/2", "cursor-col-resize touch-none", "before:absolute before:inset-y-0 before:left-1/2 before:w-px before:-translate-x-1/2", "before:bg-transparent hover:before:bg-primary/40 focus-visible:before:bg-primary", "focus:outline-none") })) : null, _jsx("div", { className: "flex-shrink-0 border-b border-border px-6 py-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold text-heading truncate pr-4", children: content.title }), _jsxs("div", { className: "flex items-center gap-1", children: [entityId && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: handleToggleEntityPin, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", isEntityPinned ? "text-foreground" : "text-muted-foreground"), "aria-pressed": isEntityPinned, "aria-label": isEntityPinned ? "Unpin document" : "Pin document", title: isEntityPinned ? "Unpin document" : "Pin document", children: _jsx(PinStateIcon, { pinned: isEntityPinned }) }), _jsx("button", { type: "button", onClick: handleNavigateToEntity, className: cn("flex items-center justify-center", "size-8 rounded-md shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Open in navigation", title: "Open in navigation", children: _jsx(FolderOpen, { className: "size-4 text-muted-foreground" }) })] })), _jsx("button", { type: "button", onClick: handleCopyAll, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Copy content", children: copied ? (_jsx(Check, { className: "size-4 text-success" })) : (_jsx(Copy, { className: "size-4 text-muted-foreground" })) }), !contained && (_jsx("button", { type: "button", onClick: toggleFullscreen, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": isFullscreen ? "Exit fullscreen" : "Fullscreen", children: isFullscreen ? (_jsx(Minimize2, { className: "size-4 text-muted-foreground" })) : (_jsx(Maximize2, { className: "size-4 text-muted-foreground" })) })), _jsx("button", { type: "button", onClick: closePanel, className: cn("flex items-center justify-center", "size-8 rounded-md flex-shrink-0", "hover:bg-muted transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"), "aria-label": "Close panel", children: _jsx(X, { className: "size-5 text-muted-foreground" }) })] })] }) }), _jsxs("div", { className: "relative flex-1 overflow-y-auto", ref: contentRef, "data-testid": "tool-panel-content", children: [_jsx(PanelContentRenderer, { content: content }), _jsx(SelectionPopover, { containerRef: contentRef })] })] }));
145
145
  }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { ToolPanel } from "./components/tool-panel.js";
4
4
  export { ToolPanelLayout } from "./components/tool-panel-layout.js";
5
5
  export { MarkdownText, markdownComponents } from "./components/markdown-text.js";
6
6
  export { ClientToolExecutor } from "./components/client-tool-executor.js";
7
- export { AGUIRuntimeProvider, useAGUIAdapter } from "./components/ag-ui-runtime-provider.js";
7
+ export { AGUIRuntimeProvider, useAGUIAdapter, useIsForegroundChat, } from "./components/ag-ui-runtime-provider.js";
8
8
  export type { AGUIChatConversationFactoryArgs, AGUIHistoryAdapterFactory, } from "./components/ag-ui-runtime-provider.js";
9
9
  export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER, } from "./components/assistant-chat.js";
10
10
  export { ComposerAddMenu, type ComposerAddMenuProps } from "./components/composer-add-menu.js";
@@ -19,8 +19,8 @@ export type { ToolCallStatus, ToolCallCardProps } from "./components/tool-call-c
19
19
  export { ToolFallback } from "./components/tool-fallback.js";
20
20
  export { ConfirmationCard } from "./components/confirmation-card.js";
21
21
  export { TooltipIconButton } from "./components/tooltip-icon-button.js";
22
- export { ComposerContextBadges, SentContextBadges } from "./components/context-badges.js";
23
- export { ContextPins, usePagePin } from "./components/context-bar.js";
22
+ export { SentContextBadges } from "./components/context-badges.js";
23
+ export { usePagePin } from "./components/context-bar.js";
24
24
  export { SelectionPopover } from "./components/selection-popover.js";
25
25
  export { GlobalSelectionPopover } from "./components/global-selection-popover.js";
26
26
  export { LoadingIndicator } from "./components/loading-indicator.js";
@@ -76,8 +76,8 @@ export { submitComposerText } from "./lib/composer-submit-store.js";
76
76
  export { registerSelectionContextResolver, resolveSelectionContext, type SelectionContextInput, type SelectionContextItem, type SelectionContextResolver, type SelectionContextResult, } from "./lib/selection-context.js";
77
77
  export { useThemeStore } from "./lib/theme-store.js";
78
78
  export type { ThemeMode } from "./lib/theme-store.js";
79
- export { mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
80
- export type { ThemeDefinition, ThemeLayer, ResolvedThemeRuntime, ThemeColorSlots, ThemeSidebarSlots, ThemeStatusSlots, ThemeChartSlots, ThemeTypographySlots, ThemeRadiusSlots, ThemeAppSlots, ThemeBranding, } from "./lib/theme-runtime.js";
79
+ export { THEME_SLOTS, mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
80
+ export type { ThemeSlot, ThemeDefinition, ThemeLayer, ResolvedThemeRuntime, ThemeColorSlots, ThemeSidebarSlots, ThemeStatusSlots, ThemeChartSlots, ThemeTypographySlots, ThemeRadiusSlots, ThemeAppSlots, ThemeBranding, } from "./lib/theme-runtime.js";
81
81
  export { useDevStore } from "./lib/dev-store.js";
82
82
  export { DEFAULT_NAV_CONFIG, findNavItem } from "./lib/nav-config.js";
83
83
  export type { NavItem, NavItemAction, NavItemDnd, NavGroup, NavGroupCreateAction, NavMatch, CollapsedRailItem, CollapsedRailContext, } from "./lib/nav-config.js";
@@ -91,3 +91,4 @@ export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reaso
91
91
  export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
92
92
  export { AuthProvider } from "./lib/auth-provider.js";
93
93
  export { createUiId } from "./lib/create-ui-id.js";
94
+ export { useChatRuns, isChatRunActive, removeChatRun, type ChatRun, } from "./lib/chat-runs-store.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { ToolPanel } from "./components/tool-panel.js";
5
5
  export { ToolPanelLayout } from "./components/tool-panel-layout.js";
6
6
  export { MarkdownText, markdownComponents } from "./components/markdown-text.js";
7
7
  export { ClientToolExecutor } from "./components/client-tool-executor.js";
8
- export { AGUIRuntimeProvider, useAGUIAdapter } from "./components/ag-ui-runtime-provider.js";
8
+ export { AGUIRuntimeProvider, useAGUIAdapter, useIsForegroundChat, } from "./components/ag-ui-runtime-provider.js";
9
9
  export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER, } from "./components/assistant-chat.js";
10
10
  export { ComposerAddMenu } from "./components/composer-add-menu.js";
11
11
  export { ChatBubble } from "./components/chat-bubble.js";
@@ -17,8 +17,8 @@ export { ToolCallCard } from "./components/tool-call-card.js";
17
17
  export { ToolFallback } from "./components/tool-fallback.js";
18
18
  export { ConfirmationCard } from "./components/confirmation-card.js";
19
19
  export { TooltipIconButton } from "./components/tooltip-icon-button.js";
20
- export { ComposerContextBadges, SentContextBadges } from "./components/context-badges.js";
21
- export { ContextPins, usePagePin } from "./components/context-bar.js";
20
+ export { SentContextBadges } from "./components/context-badges.js";
21
+ export { usePagePin } from "./components/context-bar.js";
22
22
  export { SelectionPopover } from "./components/selection-popover.js";
23
23
  export { GlobalSelectionPopover } from "./components/global-selection-popover.js";
24
24
  export { LoadingIndicator } from "./components/loading-indicator.js";
@@ -79,7 +79,7 @@ export { useChatLifecycleStore } from "./lib/chat-lifecycle-store.js";
79
79
  export { submitComposerText } from "./lib/composer-submit-store.js";
80
80
  export { registerSelectionContextResolver, resolveSelectionContext, } from "./lib/selection-context.js";
81
81
  export { useThemeStore } from "./lib/theme-store.js";
82
- export { mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
82
+ export { THEME_SLOTS, mergeThemeDefinitions, resolveThemeRuntime, getEffectiveThemeMode, getThemePreset, } from "./lib/theme-runtime.js";
83
83
  // Stores moved to @iloveagents/foundry-agent (#95): authStore, citationStore,
84
84
  // streamingStatusStore, clientToolRegistry. Consumers import directly from
85
85
  // `@iloveagents/foundry-agent` / `@iloveagents/foundry-agent/msal` — no re-exports here per
@@ -106,3 +106,4 @@ export { AuthProvider } from "./lib/auth-provider.js";
106
106
  // Re-exporting here would create two canonical sources for the same
107
107
  // utility — banned per the "no shims" pre-1.0 rule (#90).
108
108
  export { createUiId } from "./lib/create-ui-id.js";
109
+ export { useChatRuns, isChatRunActive, removeChatRun, } from "./lib/chat-runs-store.js";
@@ -19,9 +19,12 @@
19
19
  import type { ChatModelAdapter, ChatModelRunOptions, ChatModelRunResult } from "@assistant-ui/react";
20
20
  export declare class AGUIAdapterSDK implements ChatModelAdapter {
21
21
  private readonly runner;
22
+ private previousAppState?;
23
+ private readonly conversationUrl?;
22
24
  constructor(url?: string, options?: {
23
25
  threadId?: string;
24
26
  fetchFn?: typeof fetch;
27
+ conversationUrl?: string;
25
28
  });
26
29
  get threadId(): string;
27
30
  get state(): unknown;
@@ -16,12 +16,15 @@
16
16
  * @see https://docs.ag-ui.com/sdk/js/client/http-agent
17
17
  * @see https://www.assistant-ui.com/docs/runtimes/custom/local
18
18
  */
19
- import { AGUIRunner, agentStateStore, clientToolRegistry, streamingStatusStore, } from "@iloveagents/foundry-agent";
19
+ import { mergeChatState } from "./merge-chat-state.js";
20
+ import { AGUIRunner, clientToolRegistry } from "@iloveagents/foundry-agent";
20
21
  import { tokenFetch } from "@iloveagents/foundry-agent/msal";
21
22
  import { useAppStore } from "./app-store.js";
22
23
  import { useDevStore } from "./dev-store.js";
24
+ import { publishChatAgentState, beginChatRun, touchChatRun, updateChatRun, getChatStateSnapshot, useChatRuns, retainChatProjection, waitForForegroundChat, } from "./chat-runs-store.js";
23
25
  export class AGUIAdapterSDK {
24
26
  constructor(url = "/api/agent", options) {
27
+ this.conversationUrl = options?.conversationUrl;
25
28
  this.runner = new AGUIRunner({
26
29
  url,
27
30
  threadId: options?.threadId,
@@ -35,11 +38,22 @@ export class AGUIAdapterSDK {
35
38
  return this.runner.state;
36
39
  }
37
40
  async *run({ messages, abortSignal }) {
41
+ if (useChatRuns.getState().deletedThreads[this.threadId]) {
42
+ throw new Error("This conversation was deleted. Start a new chat to continue.");
43
+ }
38
44
  const aguiMessages = convertMessagesToAGUI(messages);
39
45
  // App-level lifecycle: mark thread active, snapshot context, consume
40
46
  // ephemeral context off the last user message.
41
47
  useAppStore.getState().markThreadActive();
42
48
  const appState = useAppStore.getState().getAgentState();
49
+ const projection = appState;
50
+ const retained = getChatStateSnapshot(this.threadId);
51
+ const serverState = retained.state ?? this.runner.state;
52
+ const requestState = mergeChatState(serverState && typeof serverState === "object"
53
+ ? serverState
54
+ : {}, this.previousAppState ?? retained.projection, projection);
55
+ this.previousAppState = structuredClone(projection);
56
+ retainChatProjection(this.threadId, projection);
43
57
  const context = buildAGUIContext(appState);
44
58
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
45
59
  if (lastUserMsg) {
@@ -55,7 +69,25 @@ export class AGUIAdapterSDK {
55
69
  // Liveness clock for the working indicator: every runner event (incl.
56
70
  // server heartbeats) proves the run is alive; the indicator derives
57
71
  // elapsed time from runStartedAt and staleness from lastSignalAt.
58
- streamingStatusStore.getState().markRunStarted();
72
+ const stopController = new AbortController();
73
+ const runSignal = AbortSignal.any([abortSignal, stopController.signal]);
74
+ beginChatRun({
75
+ threadId: this.threadId,
76
+ conversationUrl: this.conversationUrl,
77
+ title: lastUserMsg?.content
78
+ .filter((p) => p.type === "text")
79
+ .map((p) => p.text)
80
+ .join(" ")
81
+ .slice(0, 120) || "Conversation",
82
+ pageLabel: useAppStore.getState().navContext.label ?? undefined,
83
+ contextMeta: { ...useAppStore.getState().navContext.meta },
84
+ stop: () => stopController.abort(),
85
+ });
86
+ const registry = clientToolRegistry.getState();
87
+ const pageOwners = new Map(registry.listPageTools().map((tool) => [tool.name, tool]));
88
+ const ownerLocation = typeof window === "undefined" ? "" : window.location.href;
89
+ const schemas = registry.getActiveSchemas();
90
+ const advertised = new Set(schemas.map((tool) => tool.name));
59
91
  /**
60
92
  * Build a `ChatModelRunResult` from the accumulated state.
61
93
  *
@@ -110,13 +142,35 @@ export class AGUIAdapterSDK {
110
142
  try {
111
143
  for await (const event of this.runner.run({
112
144
  messages: aguiMessages,
113
- state: appState,
145
+ state: requestState,
114
146
  context,
115
- registry: clientToolRegistry.getState(),
116
- abortSignal,
147
+ registry: {
148
+ ...registry,
149
+ getActiveSchemas: () => schemas,
150
+ isRegistered: (name) => advertised.has(name),
151
+ executeTool: async (name, args) => {
152
+ await waitForForegroundChat(this.threadId, runSignal);
153
+ runSignal.throwIfAborted();
154
+ const owner = pageOwners.get(name);
155
+ if (owner) {
156
+ const current = clientToolRegistry
157
+ .getState()
158
+ .listPageTools()
159
+ .find((tool) => tool.name === name);
160
+ if (current !== owner ||
161
+ (typeof window !== "undefined" && window.location.href !== ownerLocation)) {
162
+ return JSON.stringify({
163
+ error: "The page changed since this action was requested. Open the original page and request the action again.",
164
+ });
165
+ }
166
+ }
167
+ return clientToolRegistry.getState().executeTool(name, args);
168
+ },
169
+ },
170
+ abortSignal: runSignal,
117
171
  })) {
118
172
  let shouldYield = false;
119
- streamingStatusStore.getState().touchSignal();
173
+ touchChatRun(this.threadId);
120
174
  switch (event.type) {
121
175
  case "heartbeat":
122
176
  // Server liveness ping — already recorded via touchSignal above.
@@ -146,7 +200,7 @@ export class AGUIAdapterSDK {
146
200
  shouldYield = true;
147
201
  break;
148
202
  case "streaming-status":
149
- streamingStatusStore.getState().setStreamingStatus(event.status);
203
+ touchChatRun(this.threadId, event.status);
150
204
  break;
151
205
  case "text-delta":
152
206
  currentText += event.delta;
@@ -162,7 +216,7 @@ export class AGUIAdapterSDK {
162
216
  case "agent-state":
163
217
  // Publish server state so pages can react to it. Not a chat
164
218
  // message, so no yield.
165
- agentStateStore.getState().setAgentState(event.state, event.patch);
219
+ publishChatAgentState(this.threadId, event.state, event.patch);
166
220
  break;
167
221
  case "messages-snapshot":
168
222
  case "activity":
@@ -256,7 +310,10 @@ export class AGUIAdapterSDK {
256
310
  // not a silent empty bubble. We yield the terminal status rather
257
311
  // than throwing; an unhandled throw re-introduces the stuck-running
258
312
  // bug (the runner generator has already returned here).
259
- if (runError !== null) {
313
+ if (runSignal.aborted) {
314
+ yield buildResult({ type: "incomplete", reason: "cancelled" });
315
+ }
316
+ else if (runError !== null) {
260
317
  yield buildResult({
261
318
  type: "incomplete",
262
319
  reason: "error",
@@ -267,9 +324,16 @@ export class AGUIAdapterSDK {
267
324
  yield buildResult({ type: "complete", reason: "stop" });
268
325
  }
269
326
  }
327
+ catch (error) {
328
+ if (runSignal.aborted)
329
+ yield buildResult({ type: "incomplete", reason: "cancelled" });
330
+ else {
331
+ runError = error instanceof Error ? error.message : String(error);
332
+ throw error;
333
+ }
334
+ }
270
335
  finally {
271
- streamingStatusStore.getState().setStreamingStatus({ status: "idle" });
272
- streamingStatusStore.getState().markRunEnded();
336
+ updateChatRun(this.threadId, runSignal.aborted ? "cancelled" : runError !== null ? "failed" : "completed", runError ?? undefined);
273
337
  }
274
338
  }
275
339
  }
@@ -42,7 +42,7 @@ export const AuthProvider = ({ config, children }) => {
42
42
  if (!ready)
43
43
  return null;
44
44
  if (initError) {
45
- return (_jsx("div", { className: "flex min-h-dvh items-center justify-center bg-background px-6 text-center", children: _jsxs("div", { className: "max-w-md rounded-2xl border border-border bg-card px-6 py-5 shadow-sm", children: [_jsx("h1", { className: "text-lg font-semibold text-foreground", children: "Authentication unavailable" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: initError })] }) }));
45
+ return (_jsx("div", { className: "flex min-h-dvh items-center justify-center bg-background px-6 text-center", children: _jsxs("div", { className: "max-w-md rounded-2xl border border-border bg-card px-6 py-5 shadow-sm", children: [_jsx("h1", { className: "text-lg font-semibold text-heading", children: "Authentication unavailable" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: initError })] }) }));
46
46
  }
47
47
  if (!MsalProvider || !useMsalHook) {
48
48
  return null;
@@ -0,0 +1,40 @@
1
+ import { type StreamingStatus } from "@iloveagents/foundry-agent";
2
+ export type ChatRunStatus = "running" | "waiting" | "completed" | "failed" | "cancelled";
3
+ export interface ChatRun {
4
+ threadId: string;
5
+ conversationUrl?: string;
6
+ title: string;
7
+ pageLabel?: string;
8
+ /** Navigation snapshot at run start, interpreted by the owning feature module. */
9
+ contextMeta?: Record<string, unknown>;
10
+ status: ChatRunStatus;
11
+ startedAt: number;
12
+ finishedAt?: number;
13
+ error?: string;
14
+ stop: () => void;
15
+ }
16
+ export declare const isChatRunActive: (run: ChatRun) => boolean;
17
+ /** Browser-session activity. Message persistence remains owned by the history adapter. */
18
+ export declare const useChatRuns: import("zustand").UseBoundStore<import("zustand").StoreApi<{
19
+ foregroundId: string | null;
20
+ runs: Record<string, ChatRun>;
21
+ deletedThreads: Record<string, true>;
22
+ }>>;
23
+ export declare function getChatStateSnapshot(threadId: string): {
24
+ state: unknown;
25
+ projection: Record<string, unknown> | undefined;
26
+ };
27
+ export declare function retainChatProjection(threadId: string, projection: Record<string, unknown>): void;
28
+ export declare function publishChatAgentState(threadId: string, state: unknown, patch?: unknown[]): void;
29
+ export declare function isForegroundChat(threadId: string): boolean;
30
+ export declare function selectChatRun(threadId: string): void;
31
+ /** Call after a successful conversation deletion so session activity cannot restore it. */
32
+ export declare function removeChatRun(threadId: string): void;
33
+ export declare function beginChatRun(run: Omit<ChatRun, "status" | "startedAt">): void;
34
+ export declare function touchChatRun(threadId: string, status?: StreamingStatus): void;
35
+ export declare function updateChatRun(threadId: string, status: ChatRunStatus, error?: string): void;
36
+ /** Browser tools need the conversation in front, so they cannot act on another chat's page. */
37
+ export declare function waitForForegroundChat(threadId: string, signal: AbortSignal): Promise<void>;
38
+ export declare function warnBeforeChatUnload(event: BeforeUnloadEvent): void;
39
+ /** Dispose browser-owned runs when the authenticated runtime is torn down. */
40
+ export declare function disposeChatRuns(): void;