@iloveagents/foundry-web-ui 0.29.0 → 0.30.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 (39) 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 +3 -4
  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/sidebar.js +39 -10
  19. package/dist/index.d.ts +4 -3
  20. package/dist/index.js +4 -3
  21. package/dist/lib/ag-ui-adapter.d.ts +3 -0
  22. package/dist/lib/ag-ui-adapter.js +75 -11
  23. package/dist/lib/chat-runs-store.d.ts +40 -0
  24. package/dist/lib/chat-runs-store.js +173 -0
  25. package/dist/lib/merge-chat-state.d.ts +3 -0
  26. package/dist/lib/merge-chat-state.js +21 -0
  27. package/dist/lib/nav-config.js +47 -20
  28. package/dist/lib/use-new-conversation.js +4 -8
  29. package/dist/workbench/chat-header.js +4 -3
  30. package/dist/workbench/conversation-history.d.ts +1 -1
  31. package/dist/workbench/conversation-history.js +75 -19
  32. package/dist/workbench/running-chats-menu.d.ts +3 -0
  33. package/dist/workbench/running-chats-menu.js +20 -0
  34. package/dist/workbench/use-workbench-state.d.ts +1 -2
  35. package/dist/workbench/use-workbench-state.js +15 -18
  36. package/dist/workbench/welcome-intro.js +1 -1
  37. package/dist/workbench/workbench-styles.js +63 -11
  38. package/dist/workbench/workbench.js +1 -1
  39. 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
  }
@@ -506,10 +506,11 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
506
506
  }
507
507
  function SubNavItems({ items, parentPath, depth = 1, }) {
508
508
  const navigate = useNavigate();
509
- const currentPath = useLocation().pathname;
509
+ const location = useLocation();
510
+ const currentPath = location.pathname + location.search;
510
511
  const setNavContext = useAppStore((s) => s.setNavContext);
511
512
  const navContext = useAppStore((s) => s.navContext);
512
- const isOnParent = currentPath === parentPath;
513
+ const isOnParent = matchesNavLocation(currentPath, parentPath);
513
514
  const [activeLabel, setActiveLabel] = useState(items[0]?.label ?? "");
514
515
  // Children with unique paths (e.g., SPACES documents) use URL matching;
515
516
  // children sharing the parent path (e.g., Documents sub-filters) use label matching.
@@ -543,19 +544,40 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
543
544
  }
544
545
  /** Check if current page is inside a nav item's subtree. */
545
546
  function isInSubtree(currentPage, item) {
546
- if (currentPage === item.to)
547
+ if (matchesNavLocation(currentPage, item.to))
547
548
  return true;
548
549
  return item.children?.some((c) => isInSubtree(currentPage, c)) ?? false;
549
550
  }
551
+ /** A navigation target may declare stable scope parameters while the page
552
+ * adds transient state such as a selected row or table sort. */
553
+ function matchesNavLocation(currentPage, target) {
554
+ const current = new URL(currentPage, "http://navigation.local");
555
+ const destination = new URL(target, current.origin);
556
+ if (current.pathname.replace(/\/+$/, "") !== destination.pathname.replace(/\/+$/, "")) {
557
+ return false;
558
+ }
559
+ if (destination.searchParams.size === 0)
560
+ return true;
561
+ return [...destination.searchParams].every(([key, value]) => current.searchParams.getAll(key).includes(value));
562
+ }
563
+ function hasMoreSpecificNavMatch(currentPage, target) {
564
+ const count = new URL(target, "http://navigation.local").searchParams.size;
565
+ const walk = (items) => items.some((item) => (matchesNavLocation(currentPage, item.to) &&
566
+ new URL(item.to, "http://navigation.local").searchParams.size > count) ||
567
+ walk(item.children ?? []));
568
+ return useNavStore.getState().config.some((group) => walk(group.items));
569
+ }
550
570
  /** A list owns the sidebar selection of descendants it deliberately does not draw.
551
571
  * The route lookup still resolves the actual descendant for chat and page actions. */
552
572
  function isSelectedNavItem(currentPage, item) {
553
- return currentPage === item.to || Boolean(item.childrenHidden && isInSubtree(currentPage, item));
573
+ return ((matchesNavLocation(currentPage, item.to) && !hasMoreSpecificNavMatch(currentPage, item.to)) ||
574
+ Boolean(item.childrenHidden && isInSubtree(currentPage, item)));
554
575
  }
555
576
  /** Nested folder with collapsible children (used inside SubNavItems). */
556
577
  function NestedFolderItem({ item, depth }) {
557
578
  const navigate = useNavigate();
558
- const currentPath = useLocation().pathname;
579
+ const location = useLocation();
580
+ const currentPath = location.pathname + location.search;
559
581
  const setNavContext = useAppStore((s) => s.setNavContext);
560
582
  const navContext = useAppStore((s) => s.navContext);
561
583
  const isInsideSubtree = isInSubtree(currentPath, item);
@@ -624,7 +646,8 @@ function NestedFolderItem({ item, depth }) {
624
646
  }
625
647
  function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader = false, }) {
626
648
  const navigate = useNavigate();
627
- const currentPath = useLocation().pathname;
649
+ const location = useLocation();
650
+ const currentPath = location.pathname + location.search;
628
651
  const setNavContext = useAppStore((s) => s.setNavContext);
629
652
  const navContext = useAppStore((s) => s.navContext);
630
653
  const isInsideSubtree = isInSubtree(currentPath, item);
@@ -693,7 +716,12 @@ function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader
693
716
  // Transparent border for box-model parity — see the NavLink variant.
694
717
  "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
718
  }
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,
719
+ return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": 0, to: item.to, onClick: (event) => {
720
+ if (!isActive || !matchesNavLocation(currentPath, item.to))
721
+ return;
722
+ event.preventDefault();
723
+ useSidebarStore.getState().closeMobile();
724
+ }, ...draggableProps, ...dropTargetProps, className: () => cn("relative flex items-center gap-2 w-full", bodyClass,
697
725
  // The transparent border is NOT decoration: it is what makes this
698
726
  // row's box model match every other variant. An absolutely
699
727
  // positioned twisty resolves against the PADDING box, so a row
@@ -953,7 +981,7 @@ function NavGroupSection({ group }) {
953
981
  // collapsible headers and item labels.
954
982
  _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
983
  ? "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 &&
984
+ : "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
985
  "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
986
  // A leaf dot distinguishes "nothing under this" from a sibling
959
987
  // that branches. Decided per GROUP: Workspaces has branches, so
@@ -996,7 +1024,8 @@ function SidebarContent({ collapsed, groups, onNewConversation, hideFooterAction
996
1024
  // still resolving, so the skeleton never flashes over a populated tree.
997
1025
  const hasWorkspacesGroup = navConfig.some((group) => group.label === "Workspaces" || group.meta?.type === "spaces-workspaces");
998
1026
  const showNavSkeleton = navLoading && !hasWorkspacesGroup;
999
- const currentPage = useLocation().pathname;
1027
+ const location = useLocation();
1028
+ const currentPage = location.pathname + location.search;
1000
1029
  const navContext = useAppStore((s) => s.navContext);
1001
1030
  useScrollActiveNavRow(currentPage, navConfig, collapsed);
1002
1031
  const preventNativeFileDrop = useCallback((e) => {
@@ -1050,7 +1079,7 @@ function SidebarContent({ collapsed, groups, onNewConversation, hideFooterAction
1050
1079
  to: it.to,
1051
1080
  label: it.label,
1052
1081
  icon: it.icon,
1053
- isActive: currentPage === it.to,
1082
+ isActive: isSelectedNavItem(currentPage, it),
1054
1083
  }));
1055
1084
  return { group, groupIndex, items };
1056
1085
  })
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";
@@ -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";
@@ -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
  }
@@ -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;