@iloveagents/foundry-web-ui 0.29.1 → 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.
- package/dist/components/ag-ui-runtime-provider.d.ts +8 -3
- package/dist/components/ag-ui-runtime-provider.js +76 -33
- package/dist/components/assistant-chat.d.ts +1 -15
- package/dist/components/assistant-chat.js +3 -4
- package/dist/components/chat-bubble.js +3 -4
- package/dist/components/chat-context-items.d.ts +6 -0
- package/dist/components/chat-context-items.js +25 -0
- package/dist/components/chat-context.js +10 -2
- package/dist/components/chat-empty-state.js +1 -1
- package/dist/components/chat-header.js +2 -1
- package/dist/components/composer-add-menu.d.ts +0 -19
- package/dist/components/composer-add-menu.js +2 -10
- package/dist/components/context-badges.d.ts +0 -14
- package/dist/components/context-badges.js +2 -29
- package/dist/components/context-bar.d.ts +1 -21
- package/dist/components/context-bar.js +3 -74
- package/dist/components/focus-chat-pane.js +1 -1
- package/dist/components/sidebar.js +39 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/lib/ag-ui-adapter.d.ts +3 -0
- package/dist/lib/ag-ui-adapter.js +75 -11
- package/dist/lib/chat-runs-store.d.ts +40 -0
- package/dist/lib/chat-runs-store.js +173 -0
- package/dist/lib/merge-chat-state.d.ts +3 -0
- package/dist/lib/merge-chat-state.js +21 -0
- package/dist/lib/nav-config.js +47 -20
- package/dist/lib/use-new-conversation.js +4 -8
- package/dist/workbench/chat-header.js +4 -3
- package/dist/workbench/conversation-history.d.ts +1 -1
- package/dist/workbench/conversation-history.js +75 -19
- package/dist/workbench/running-chats-menu.d.ts +3 -0
- package/dist/workbench/running-chats-menu.js +20 -0
- package/dist/workbench/use-workbench-state.d.ts +1 -2
- package/dist/workbench/use-workbench-state.js +15 -18
- package/dist/workbench/welcome-intro.js +1 -1
- package/dist/workbench/workbench-styles.js +54 -5
- package/dist/workbench/workbench.js +1 -1
- 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
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 {
|
|
23
|
-
export {
|
|
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 {
|
|
21
|
-
export {
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
145
|
+
state: requestState,
|
|
114
146
|
context,
|
|
115
|
-
registry:
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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;
|