@iloveagents/foundry-web-ui 0.32.3 → 0.33.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-adapter-context.d.ts +16 -0
- package/dist/components/ag-ui-adapter-context.js +6 -0
- package/dist/components/ag-ui-runtime-provider.d.ts +1 -7
- package/dist/components/ag-ui-runtime-provider.js +2 -2
- package/dist/components/assistant-chat.js +4 -4
- package/dist/components/chat-bubble.js +1 -1
- package/dist/components/chat-context.d.ts +3 -1
- package/dist/components/chat-context.js +5 -3
- package/dist/components/chat-empty-state.js +1 -1
- package/dist/components/chat-header.js +1 -1
- package/dist/components/conversation-memory.d.ts +25 -0
- package/dist/components/conversation-memory.js +66 -0
- package/dist/components/loading-indicator.js +3 -0
- package/dist/components/sidebar.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/ag-ui-adapter.js +11 -3
- package/dist/lib/agent-data.d.ts +20 -0
- package/dist/lib/agent-data.js +127 -0
- package/dist/lib/app-store.js +8 -8
- package/dist/lib/chat-runs-store.js +3 -1
- package/dist/workbench/chat-header.js +1 -1
- package/dist/workbench/conversation-history.d.ts +3 -1
- package/dist/workbench/conversation-history.js +61 -18
- package/dist/workbench/running-chats-menu.js +1 -1
- package/dist/workbench/use-workbench-state.js +1 -1
- package/dist/workbench/welcome-intro.js +1 -1
- package/dist/workbench/workbench.js +2 -1
- package/package.json +3 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
|
|
2
|
+
/**
|
|
3
|
+
* The AG-UI runtime a component is rendered in.
|
|
4
|
+
*
|
|
5
|
+
* Its own module, importing nothing at runtime but React, so a composer
|
|
6
|
+
* control can ask which thread it belongs to without pulling in the
|
|
7
|
+
* provider — and with it the tool UIs, adapters and theme it mounts.
|
|
8
|
+
*/
|
|
9
|
+
export interface AGUIContextValue {
|
|
10
|
+
adapter: AGUIAdapterSDK;
|
|
11
|
+
resetThread: () => void;
|
|
12
|
+
selectRetainedThread: (threadId: string) => boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const AGUIAdapterContext: import("react").Context<AGUIContextValue | null>;
|
|
15
|
+
/** The thread this component is rendered for, or `null` outside a runtime. */
|
|
16
|
+
export declare function useCurrentThreadId(): string | null;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
export const AGUIAdapterContext = createContext(null);
|
|
3
|
+
/** The thread this component is rendered for, or `null` outside a runtime. */
|
|
4
|
+
export function useCurrentThreadId() {
|
|
5
|
+
return useContext(AGUIAdapterContext)?.adapter.threadId ?? null;
|
|
6
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ThreadHistoryAdapter } from "@assistant-ui/react";
|
|
2
|
-
import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
|
|
3
2
|
/**
|
|
4
3
|
* Per-mount inputs the host shell passes when it wants a chat
|
|
5
4
|
* conversation history wired to the runtime. Mirrors
|
|
@@ -57,12 +56,7 @@ interface AGUIRuntimeProviderProps {
|
|
|
57
56
|
*/
|
|
58
57
|
historyAdapterFactory?: AGUIHistoryAdapterFactory;
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
adapter: AGUIAdapterSDK;
|
|
62
|
-
resetThread: () => void;
|
|
63
|
-
selectRetainedThread: (threadId: string) => boolean;
|
|
64
|
-
}
|
|
65
|
-
export declare function useAGUIAdapter(): AGUIContextValue;
|
|
59
|
+
export declare function useAGUIAdapter(): import("./ag-ui-adapter-context.js").AGUIContextValue;
|
|
66
60
|
/** Whether this runtime owns visible UI effects; standalone consumers remain active. */
|
|
67
61
|
export declare function useIsForegroundChat(): boolean;
|
|
68
62
|
export declare function AGUIRuntimeProvider(props: AGUIRuntimeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useContext, useState, useCallback, useMemo, useRef, useLayoutEffect, useEffect, } from "react";
|
|
3
3
|
import { AssistantRuntimeProvider, useLocalRuntime } from "@assistant-ui/react";
|
|
4
4
|
import { TooltipProvider } from "../ui/tooltip.js";
|
|
5
5
|
import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
|
|
6
6
|
import { FileAttachmentAdapter } from "../lib/attachment-adapter.js";
|
|
7
7
|
import { useAttachmentAdapterStore } from "../lib/attachment-registry.js";
|
|
8
8
|
import { useVoiceAdapterStore } from "../lib/voice-adapter-registry.js";
|
|
9
|
+
import { AGUIAdapterContext } from "./ag-ui-adapter-context.js";
|
|
9
10
|
import { ShowDocumentToolUI } from "./show-document-tool-ui.js";
|
|
10
11
|
import { ClientToolExecutor } from "./client-tool-executor.js";
|
|
11
12
|
import { useChatRuns, isChatRunActive, selectChatRun, warnBeforeChatUnload, disposeChatRuns, } from "../lib/chat-runs-store.js";
|
|
12
|
-
const AGUIAdapterContext = createContext(null);
|
|
13
13
|
export function useAGUIAdapter() {
|
|
14
14
|
const context = useContext(AGUIAdapterContext);
|
|
15
15
|
if (!context) {
|
|
@@ -115,7 +115,7 @@ const DropZone = ({ children, centered, }) => {
|
|
|
115
115
|
: `Couldn't attach ${rejected.length} files (${rejected.join("; ")})`);
|
|
116
116
|
}
|
|
117
117
|
}, [aui, showDropError]);
|
|
118
|
-
return (_jsxs("div", { className: cn("relative flex flex-col flex-1", centered ? "
|
|
118
|
+
return (_jsxs("div", { className: cn("relative flex flex-col flex-1", centered ? "overflow-y-auto" : "overflow-hidden"), onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [children, isDragging && (_jsx("div", { className: cn("absolute inset-0 z-50 flex items-center justify-center pointer-events-none", "bg-background/80 backdrop-blur-sm", "border-2 border-dashed border-primary rounded-xl"), children: _jsx("p", { className: "text-lg text-primary font-medium", children: "Drop files here" }) })), dropError && (_jsx("div", { role: "alert", className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-50 max-w-[90%]", "rounded-md border border-destructive/50 bg-background px-3 py-2", "text-sm text-destructive shadow-md pointer-events-none truncate"), children: dropError }))] }));
|
|
119
119
|
};
|
|
120
120
|
/**
|
|
121
121
|
* "Message Assistant..." named the machine and told the user nothing. The
|
|
@@ -142,11 +142,11 @@ export function ChatContent({ centerComposer = false, composerIntro, composerFoo
|
|
|
142
142
|
UserMessage,
|
|
143
143
|
EditComposer,
|
|
144
144
|
AssistantMessage,
|
|
145
|
-
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), !ThreadSurface && (_jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) }))] }), _jsxs("div", { className: "shrink-0 bg-background", hidden: hideComposer, children: [composerIntro && !ThreadSurface && (_jsx("div", { className: "mx-auto max-w-3xl px-4 pt-4", children: composerIntro })), _jsx(ChatComposerBannerSlot, {}), _jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { "data-chat-composer-divider": true, className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-4", children: [_jsx(ChatContext, {}), _jsxs(ComposerPrimitive.Root, { style: compactComposer ? { borderRadius: 16 } : undefined, className: cn(
|
|
145
|
+
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), !ThreadSurface && (_jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) }))] }), _jsxs("div", { className: cn("shrink-0 bg-background", centerComposer && !ThreadSurface && "my-auto"), hidden: hideComposer, children: [composerIntro && !ThreadSurface && (_jsx("div", { className: "mx-auto max-w-3xl px-4 pt-4", children: composerIntro })), _jsx(ChatComposerBannerSlot, {}), _jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { "data-chat-composer-divider": true, className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-4", children: [_jsx(ChatContext, { centered: centerComposer && !ThreadSurface }), _jsxs(ComposerPrimitive.Root, { style: compactComposer ? { borderRadius: 16 } : undefined, className: cn(
|
|
146
146
|
// Query container for the controls inside, so they size
|
|
147
147
|
// against the composer rather than the viewport. This one
|
|
148
148
|
// is wide, so the effort picker keeps its full label.
|
|
149
|
-
"@container", "flex flex-col gap-2", "rounded-3xl border border-input", "bg-background shadow-sm", "px-4 py-2", "focus-within:border-primary/50 focus-within:shadow-md", "transition-all duration-300"), children: [_jsx(ComposerSubmitBridge, {}), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: cn("flex items-center gap-2", compactComposer ? "flex-wrap" : "flex-wrap @md:flex-nowrap"), children: [_jsx("div", { className: cn("flex shrink-0 items-center gap-1", compactComposer ? "order-2" : "order-2 @md:order-1"), children: _jsx(ComposerAddMenu, {}) }), _jsx(ComposerInput, { autoFocus: true, placeholder: placeholder, rows: 1, className: cn("bg-transparent", compactComposer
|
|
149
|
+
"@container", "flex flex-col gap-2", "rounded-3xl border border-input", "bg-background shadow-sm", "px-4 py-2", "focus-within:border-primary/50 focus-within:shadow-md", centerComposer && "shadow-md", "transition-all duration-300"), children: [_jsx(ComposerSubmitBridge, {}), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: cn("flex items-center gap-2", compactComposer ? "flex-wrap" : "flex-wrap @md:flex-nowrap"), children: [_jsx("div", { className: cn("flex shrink-0 items-center gap-1", compactComposer ? "order-2" : "order-2 @md:order-1"), children: _jsx(ComposerAddMenu, {}) }), _jsx(ComposerInput, { autoFocus: true, placeholder: placeholder, rows: 1, className: cn("bg-transparent", compactComposer
|
|
150
150
|
? "order-1 w-full"
|
|
151
|
-
: "order-1 w-full @md:order-2 @md:w-auto @md:flex-1", "py-2 text-base sm:text-sm leading-relaxed", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-32") }), _jsx("div", { className: cn("order-3 ml-auto flex shrink-0 items-center gap-2", !compactComposer && "@md:ml-0"), children: _jsx(ComposerActionButton, {}) })] })] }), composerFooter] })] })] })] }));
|
|
151
|
+
: "order-1 w-full @md:order-2 @md:w-auto @md:flex-1", "py-2 text-base sm:text-sm leading-relaxed", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-32") }), _jsx("div", { className: cn("order-3 ml-auto flex shrink-0 items-center gap-2", !compactComposer && "@md:ml-0"), children: _jsx(ComposerActionButton, {}) })] })] }), !ThreadSurface && composerFooter] })] })] })] }));
|
|
152
152
|
}
|
|
@@ -106,7 +106,7 @@ export function ChatBubble() {
|
|
|
106
106
|
height: 64,
|
|
107
107
|
} })), !isOpen && (_jsxs("div", { className: "fixed z-50 group", style: { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsx("button", { type: "button", onClick: openBubble, className: cn("size-10 rounded-full", "bg-primary text-primary-foreground shadow-lg", "flex items-center justify-center", "hover:bg-primary/90 transition-all duration-200", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), "aria-label": "Ask the AI", children: _jsx(Sparkles, { className: "size-4" }) }), _jsx(ChatLauncherBadgeSlot, {})] })), isOpen && (_jsxs("div", { ref: panelRef, "data-chat-surface": "bubble", className: cn("fixed z-50", "rounded-2xl border border-border", "bg-background shadow-2xl", "flex flex-col overflow-hidden", "w-95 h-150"), style: dragPos
|
|
108
108
|
? { left: dragPos.x, top: dragPos.y }
|
|
109
|
-
: { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsxs("div", { className: cn("flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0", "cursor-grab active:cursor-grabbing select-none"), style: { touchAction: "none" }, onPointerDown: onHeaderPointerDown, onPointerMove: onHeaderPointerMove, onPointerUp: onHeaderPointerUp, onLostPointerCapture: onHeaderLostPointerCapture, children: [_jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-sm", children: [_jsx(Sparkles, { className: "size-3.5 text-primary shrink-0" }), _jsx("span", { className: "font-semibold text-foreground truncate", children: pageLabel })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: threadActive ? "Active
|
|
109
|
+
: { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsxs("div", { className: cn("flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0", "cursor-grab active:cursor-grabbing select-none"), style: { touchAction: "none" }, onPointerDown: onHeaderPointerDown, onPointerMove: onHeaderPointerMove, onPointerUp: onHeaderPointerUp, onLostPointerCapture: onHeaderLostPointerCapture, children: [_jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-sm", children: [_jsx(Sparkles, { className: "size-3.5 text-primary shrink-0" }), _jsx("span", { className: "font-semibold text-foreground truncate", children: pageLabel })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: threadActive ? "Active chat" : "New chat" })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(TooltipIconButton, { tooltip: "New chat", size: "icon", className: "size-7", onClick: startNewThread, children: _jsx(SquarePen, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Expand chat", size: "icon", className: "size-7", onClick: expandChat, children: _jsx(Maximize2, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Close", size: "icon", className: "size-7", onClick: handleClose, children: _jsx(X, { className: "size-4" }) })] })] }), _jsxs(ThreadPrimitive.Root, { className: "flex flex-col flex-1 overflow-hidden", children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [ThreadSurface && (_jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: "flex min-h-full flex-col items-center justify-center gap-4 p-4", children: [_jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Ask the agent about this page" }), _jsx(ChatHomeStartersSlot, {})] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "px-4 pt-4 pb-3", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
110
110
|
UserMessage: BubbleUserMessage,
|
|
111
111
|
AssistantMessage: BubbleAssistantMessage,
|
|
112
112
|
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: !ThreadSurface && _jsx(BubbleScrollToBottomButton, {}) })] }), _jsxs("div", { className: "shrink-0 border-t border-border", hidden: hideComposer, children: [_jsx(ChatComposerBannerSlot, {}), _jsxs("div", { className: "px-3 py-3", children: [_jsx(ChatContext, {}), _jsx(ComposerPrimitive.Root, { className: cn(
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
/** The displayed context and the agent request use the same projection. */
|
|
2
|
-
export declare function ChatContext(
|
|
2
|
+
export declare function ChatContext({ centered }: {
|
|
3
|
+
centered?: boolean;
|
|
4
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -6,14 +6,16 @@ import { summarizeChatContext } from "../lib/chat-context-summary.js";
|
|
|
6
6
|
import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover.js";
|
|
7
7
|
import { ChatContextDetailsSlot } from "./chat-slots.js";
|
|
8
8
|
import { ChatContextItems } from "./chat-context-items.js";
|
|
9
|
+
import { ConversationMemory, useConversationMemory } from "./conversation-memory.js";
|
|
9
10
|
import { usePagePin } from "./context-bar.js";
|
|
10
11
|
import { PinStateIcon } from "./pin-state-icon.js";
|
|
11
12
|
function readContext() {
|
|
12
13
|
return summarizeChatContext(useAppStore.getState().getAgentState());
|
|
13
14
|
}
|
|
14
15
|
/** The displayed context and the agent request use the same projection. */
|
|
15
|
-
export function ChatContext() {
|
|
16
|
+
export function ChatContext({ centered = false }) {
|
|
16
17
|
const [open, setOpen] = useState(false);
|
|
18
|
+
const memory = useConversationMemory();
|
|
17
19
|
const [context, setContext] = useState(readContext);
|
|
18
20
|
const items = useAppStore((s) => s.contextItems);
|
|
19
21
|
const { isPinned, toggle } = usePagePin();
|
|
@@ -74,7 +76,7 @@ export function ChatContext() {
|
|
|
74
76
|
};
|
|
75
77
|
}, []);
|
|
76
78
|
const Icon = context.field ? TextCursorInput : context.focused ? Maximize2 : FileText;
|
|
77
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-chat-context": true, "aria-label": `Message context: ${context.label}${detail ? `, ${detail}` : ""}${additionalCount ? `, ${additionalCount} more` : ""}`, className:
|
|
79
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-chat-context": true, "aria-label": `Message context: ${context.label}${detail ? `, ${detail}` : ""}${additionalCount ? `, ${additionalCount} more` : ""}${memory?.full ? ", conversation memory full" : memory?.nearlyFull ? ", conversation memory nearly full" : ""}`, className: `mb-2 flex min-h-9 min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary [@media(pointer:coarse)]:min-h-11 ${centered ? "mx-auto w-fit max-w-full" : "w-full"}`, children: [_jsx(Icon, { className: "size-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 flex-1 truncate", "aria-live": "polite", children: ["With ", _jsx("span", { className: "font-medium text-foreground", children: context.label }), detail && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "aria-hidden": true, children: "\u203A" }), " ", _jsx("span", { className: "font-medium text-foreground", children: detail })] }))] }), additionalCount > 0 && (_jsxs("span", { className: "shrink-0 tabular-nums", "aria-hidden": true, children: ["+", additionalCount] })), memory?.nearlyFull && (_jsx("span", { "data-memory-dot": true, title: "Conversation memory nearly full", className: `size-1.5 shrink-0 rounded-full ${memory.full ? "bg-destructive" : "bg-warning"}` })), _jsx(ChevronDown, { className: "size-3.5 shrink-0" })] }) }), _jsxs(PopoverContent, { align: centered ? "center" : "start", side: "top", collisionPadding: 12, "aria-label": "Context and actions", className: "w-[22rem] max-w-[calc(100vw-1.5rem)] max-h-[min(36rem,var(--radix-popover-content-available-height))] overflow-y-auto overscroll-contain space-y-3 p-3 text-sm", children: [_jsxs("div", { className: "px-2", children: [_jsx("h2", { className: "text-xs font-medium text-muted-foreground", children: "Current context" }), _jsxs("p", { className: "mt-1 break-words font-medium", children: [context.label, detail && ` › ${detail}`] })] }), _jsx("p", { className: "px-2 text-xs leading-relaxed text-muted-foreground", children: detail
|
|
78
80
|
? "This selection stays available when you move into chat."
|
|
79
|
-
: "The current page follows your navigation. Select text or a field to work more specifically." }), context.selectedText && (_jsx("blockquote", { className: "max-h-32 overflow-y-auto border-l-2 border-border pl-3 text-xs", children: context.selectedText })), !context.focused && context.label !== "Your spaces" && (_jsxs("button", { type: "button", "aria-pressed": isPinned, onClick: toggle, className: "flex min-h-9 w-full items-center gap-2 rounded-lg px-2 text-left text-sm hover:bg-muted", children: [_jsx(PinStateIcon, { pinned: isPinned, className: "size-4 shrink-0 text-muted-foreground" }), isPinned ? "Unpin this page" : "Keep this page in context"] })), _jsx(ChatContextItems, { items: items, onNavigate: () => setOpen(false) }), _jsx(ChatContextDetailsSlot, { onDismiss: () => setOpen(false) }), context.references.length > 0 && (_jsxs("div", { className: "border-t border-border px-2 pt-3 text-xs", children: [_jsx("h3", { className: "font-medium", children: "Also open" }), _jsx("ul", { className: "mt-1 space-y-1 text-muted-foreground", children: context.references.map((label, index) => (_jsx("li", { className: "truncate", children: label }, `${label}-${index}`))) })] }))] })] }));
|
|
81
|
+
: "The current page follows your navigation. Select text or a field to work more specifically." }), context.selectedText && (_jsx("blockquote", { className: "max-h-32 overflow-y-auto border-l-2 border-border pl-3 text-xs", children: context.selectedText })), !context.focused && context.label !== "Your spaces" && (_jsxs("button", { type: "button", "aria-pressed": isPinned, onClick: toggle, className: "flex min-h-9 w-full items-center gap-2 rounded-lg px-2 text-left text-sm hover:bg-muted", children: [_jsx(PinStateIcon, { pinned: isPinned, className: "size-4 shrink-0 text-muted-foreground" }), isPinned ? "Unpin this page" : "Keep this page in context"] })), _jsx(ChatContextItems, { items: items, onNavigate: () => setOpen(false) }), _jsx(ChatContextDetailsSlot, { onDismiss: () => setOpen(false) }), memory && _jsx(ConversationMemory, { usage: memory }), context.references.length > 0 && (_jsxs("div", { className: "border-t border-border px-2 pt-3 text-xs", children: [_jsx("h3", { className: "font-medium", children: "Also open" }), _jsx("ul", { className: "mt-1 space-y-1 text-muted-foreground", children: context.references.map((label, index) => (_jsx("li", { className: "truncate", children: label }, `${label}-${index}`))) })] }))] })] }));
|
|
80
82
|
}
|
|
@@ -2,5 +2,5 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { ChatHomeStartersSlot } from "./chat-slots.js";
|
|
3
3
|
/** One compact conversation home for workbench, focus and embedded chat panes. */
|
|
4
4
|
export function ChatEmptyState({ pageLabel, children, showHomeStarters = true, homeStarterPriority = 0, }) {
|
|
5
|
-
return (_jsxs("div", { "data-chat-empty-state": true, className: "flex min-h-full flex-col justify-start gap-3 px-4 pb-8 pt-3", children: [_jsx("h1", { className: "text-base leading-6 font-semibold", children: "What would you like to get done?" }), _jsx("p", { className: "max-w-sm text-sm leading-relaxed text-muted-foreground", children: pageLabel && !pageLabel.startsWith("/") ? (_jsxs(_Fragment, { children: ["Work with ", _jsx("span", { className: "font-medium text-foreground", children: pageLabel }), " or across your spaces: find information, create or edit content, and run actions."] })) : ("Find information, create or edit content, and run actions across your spaces.") }), _jsx("p", { className: "max-w-sm text-xs leading-relaxed text-muted-foreground", children: "Your
|
|
5
|
+
return (_jsxs("div", { "data-chat-empty-state": true, className: "flex min-h-full flex-col justify-start gap-3 px-4 pb-8 pt-3", children: [_jsx("h1", { className: "text-base leading-6 font-semibold", children: "What would you like to get done?" }), _jsx("p", { className: "max-w-sm text-sm leading-relaxed text-muted-foreground", children: pageLabel && !pageLabel.startsWith("/") ? (_jsxs(_Fragment, { children: ["Work with ", _jsx("span", { className: "font-medium text-foreground", children: pageLabel }), " or across your spaces: find information, create or edit content, and run actions."] })) : ("Find information, create or edit content, and run actions across your spaces.") }), _jsx("p", { className: "max-w-sm text-xs leading-relaxed text-muted-foreground", children: "Your chat continues as you browse. Select text or a field to work more specifically." }), showHomeStarters && _jsx(ChatHomeStartersSlot, { priority: homeStarterPriority }), children] }));
|
|
6
6
|
}
|
|
@@ -20,5 +20,5 @@ export const ChatHeader = () => {
|
|
|
20
20
|
const handleNewConversation = useNewConversation(isExpanded ? { navigateToChat: false, preserveNavigation: true } : undefined);
|
|
21
21
|
const toggleMobile = useSidebarStore((s) => s.toggleMobile);
|
|
22
22
|
const isSidebarOpen = useSidebarStore((s) => s.isOpen);
|
|
23
|
-
return (_jsx("div", { className: cn("shrink-0", "bg-background border-b border-border"), children: _jsxs("div", { className: "px-4 py-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TooltipIconButton, { tooltip: "Open sidebar", onClick: toggleMobile, className: "md:hidden", children: _jsx(Menu, { className: "size-5" }) }), _jsx(Link, { to: "/", className: cn("flex items-center gap-2.5 hover:opacity-80 transition-opacity", isSidebarOpen && "md:hidden"), children: _jsx(AppBrand, { labelClassName: "text-sm font-semibold text-foreground" }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(RunningChatsMenu, {}), _jsx(ThemeToggle, {}), _jsx(TooltipIconButton, { tooltip: "New
|
|
23
|
+
return (_jsx("div", { className: cn("shrink-0", "bg-background border-b border-border"), children: _jsxs("div", { className: "px-4 py-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TooltipIconButton, { tooltip: "Open sidebar", onClick: toggleMobile, className: "md:hidden", children: _jsx(Menu, { className: "size-5" }) }), _jsx(Link, { to: "/", className: cn("flex items-center gap-2.5 hover:opacity-80 transition-opacity", isSidebarOpen && "md:hidden"), children: _jsx(AppBrand, { labelClassName: "text-sm font-semibold text-foreground" }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(RunningChatsMenu, {}), _jsx(ThemeToggle, {}), _jsx(TooltipIconButton, { tooltip: "New chat", size: "icon", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-primary" }) }), _jsx(UserMenu, {})] })] }) }));
|
|
24
24
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ConversationMemoryUsage {
|
|
2
|
+
used: number;
|
|
3
|
+
limit: number;
|
|
4
|
+
ratio: number;
|
|
5
|
+
nearlyFull: boolean;
|
|
6
|
+
full: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the server compacts at this ceiling. Only a server that states
|
|
9
|
+
* its own fill and ceiling with each call does; a window the host merely
|
|
10
|
+
* stated says how much fits, not what happens when it is full.
|
|
11
|
+
*/
|
|
12
|
+
compacts: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* How much of this conversation's memory is in use, or `null` while unknown.
|
|
16
|
+
*
|
|
17
|
+
* Known only when a fill and its ceiling are both stated in the same units:
|
|
18
|
+
* preferably by the server with each model call's usage, else the model's
|
|
19
|
+
* input count against a window the host stated.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useConversationMemory(): ConversationMemoryUsage | null;
|
|
22
|
+
/** The "Conversation memory" section of the context card. */
|
|
23
|
+
export declare function ConversationMemory({ usage }: {
|
|
24
|
+
usage: ConversationMemoryUsage;
|
|
25
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from "zustand";
|
|
3
|
+
import { contextUsageStore } from "@iloveagents/foundry-agent";
|
|
4
|
+
import { Info } from "lucide-react";
|
|
5
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
|
+
import { useCurrentThreadId } from "./ag-ui-adapter-context.js";
|
|
8
|
+
/** From here the chat is close to the point where older content is summarized. */
|
|
9
|
+
const NEARLY_FULL = 0.9;
|
|
10
|
+
/** At the ceiling: the next message compacts the chat. */
|
|
11
|
+
const FULL = 0.98;
|
|
12
|
+
function formatTokens(n) {
|
|
13
|
+
if (n >= 10000)
|
|
14
|
+
return `${Math.round(n / 1000)}k`;
|
|
15
|
+
if (n >= 1000)
|
|
16
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
17
|
+
return String(n);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* How much of this conversation's memory is in use, or `null` while unknown.
|
|
21
|
+
*
|
|
22
|
+
* Known only when a fill and its ceiling are both stated in the same units:
|
|
23
|
+
* preferably by the server with each model call's usage, else the model's
|
|
24
|
+
* input count against a window the host stated.
|
|
25
|
+
*/
|
|
26
|
+
export function useConversationMemory() {
|
|
27
|
+
const threadId = useCurrentThreadId();
|
|
28
|
+
const windowTokens = useStore(contextUsageStore, (s) => s.windowTokens);
|
|
29
|
+
const usage = useStore(contextUsageStore, (s) => (threadId ? s.byThread[threadId] : undefined));
|
|
30
|
+
if (!usage)
|
|
31
|
+
return null;
|
|
32
|
+
// Without the server's own figure: what the model was sent plus what it
|
|
33
|
+
// answered, which is now part of the conversation too.
|
|
34
|
+
const used = usage.contextLimit
|
|
35
|
+
? (usage.contextUsed ?? 0)
|
|
36
|
+
: usage.inputTokens + (usage.outputTokens ?? 0);
|
|
37
|
+
const limit = usage.contextLimit ?? windowTokens;
|
|
38
|
+
if (!limit)
|
|
39
|
+
return null;
|
|
40
|
+
const ratio = Math.min(used / limit, 1);
|
|
41
|
+
return {
|
|
42
|
+
used: Math.min(used, limit),
|
|
43
|
+
limit,
|
|
44
|
+
ratio,
|
|
45
|
+
nearlyFull: ratio >= NEARLY_FULL,
|
|
46
|
+
full: ratio >= FULL,
|
|
47
|
+
compacts: usage.contextLimit != null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function explanation(usage) {
|
|
51
|
+
if (!usage.compacts)
|
|
52
|
+
return usage.full
|
|
53
|
+
? "This chat is full: the model cannot take in more of it. Start a new chat to continue."
|
|
54
|
+
: "How much of what the model can take in this chat uses. For a fresh topic, start a new chat.";
|
|
55
|
+
if (usage.full)
|
|
56
|
+
return "This chat is full. It will be compacted with your next message: earlier parts are summarized to make room.";
|
|
57
|
+
if (usage.nearlyFull)
|
|
58
|
+
return "This chat is nearly full. When it fills, earlier parts are summarized to make room; for a fresh topic, start a new chat.";
|
|
59
|
+
return "When this fills, the chat is compacted: earlier parts are summarized to make room.";
|
|
60
|
+
}
|
|
61
|
+
/** The "Conversation memory" section of the context card. */
|
|
62
|
+
export function ConversationMemory({ usage }) {
|
|
63
|
+
const percent = Math.round(usage.ratio * 100);
|
|
64
|
+
const about = explanation(usage);
|
|
65
|
+
return (_jsxs("section", { "aria-label": "Conversation memory", "data-conversation-memory": usage.full ? "full" : usage.nearlyFull ? "nearly-full" : "ok", className: "border-t border-border px-2 pt-3 text-xs", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("h3", { className: "font-medium", children: "Conversation memory" }), _jsx(TooltipProvider, { delayDuration: 150, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": about, className: "inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(Info, { className: "size-3", "aria-hidden": true }) }) }), _jsx(TooltipContent, { side: "top", className: "max-w-60 leading-relaxed", children: about })] }) })] }), _jsxs("span", { className: "tabular-nums text-muted-foreground", children: [formatTokens(usage.used), " of ", formatTokens(usage.limit)] })] }), _jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": percent, "aria-label": `${percent}% of conversation memory used`, className: "mt-2 h-1.5 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: cn("h-full rounded-full transition-[width]", usage.full ? "bg-destructive" : usage.nearlyFull ? "bg-warning" : "bg-primary"), style: { width: `${percent}%` } }) })] }));
|
|
66
|
+
}
|
|
@@ -31,6 +31,9 @@ export function LoadingIndicator() {
|
|
|
31
31
|
else if (streaming.status === "streaming") {
|
|
32
32
|
label = "Generating response";
|
|
33
33
|
}
|
|
34
|
+
else if (streaming.status === "working" && streaming.label) {
|
|
35
|
+
label = streaming.label;
|
|
36
|
+
}
|
|
34
37
|
return (
|
|
35
38
|
// `px-4` keeps the label on the same left edge as the message parts
|
|
36
39
|
// above it — measured, not guessed: the rows sit at x=469 and so does
|
|
@@ -1008,7 +1008,7 @@ function CreateActionsMenu({ group }) {
|
|
|
1008
1008
|
action.handler();
|
|
1009
1009
|
}, children: action.label }, action.id))) })] }));
|
|
1010
1010
|
}
|
|
1011
|
-
function SidebarContent({ collapsed, groups, onNewConversation, hideFooterActions, hideNewConversation, collapsedGroupsOnly, newConversationAfterGroup, newConversationLabel = "New
|
|
1011
|
+
function SidebarContent({ collapsed, groups, onNewConversation, hideFooterActions, hideNewConversation, collapsedGroupsOnly, newConversationAfterGroup, newConversationLabel = "New chat", }) {
|
|
1012
1012
|
const toggle = useSidebarStore((s) => s.toggle);
|
|
1013
1013
|
const defaultNewConversation = useNewConversation();
|
|
1014
1014
|
const handleNewConversation = onNewConversation ?? defaultNewConversation;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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
7
|
export { AGUIRuntimeProvider, useAGUIAdapter, useIsForegroundChat, } from "./components/ag-ui-runtime-provider.js";
|
|
8
|
+
export { useCurrentThreadId } from "./components/ag-ui-adapter-context.js";
|
|
8
9
|
export type { AGUIChatConversationFactoryArgs, AGUIHistoryAdapterFactory, } from "./components/ag-ui-runtime-provider.js";
|
|
9
10
|
export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER, } from "./components/assistant-chat.js";
|
|
10
11
|
export { ComposerAddMenu, type ComposerAddMenuProps } from "./components/composer-add-menu.js";
|
|
@@ -89,6 +90,7 @@ export { registerAttachmentAdapter, useAttachmentAdapterStore } from "./lib/atta
|
|
|
89
90
|
export { registerVoiceAdapter, registerSpeechAdapter, useVoiceAdapterStore, } from "./lib/voice-adapter-registry.js";
|
|
90
91
|
export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
|
|
91
92
|
export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
|
|
93
|
+
export { ConversationMemory, useConversationMemory, type ConversationMemoryUsage, } from "./components/conversation-memory.js";
|
|
92
94
|
export { AuthProvider } from "./lib/auth-provider.js";
|
|
93
95
|
export { createUiId } from "./lib/create-ui-id.js";
|
|
94
96
|
export { useChatRuns, isChatRunActive, removeChatRun, type ChatRun, } from "./lib/chat-runs-store.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
export { AGUIRuntimeProvider, useAGUIAdapter, useIsForegroundChat, } from "./components/ag-ui-runtime-provider.js";
|
|
9
|
+
export { useCurrentThreadId } from "./components/ag-ui-adapter-context.js";
|
|
9
10
|
export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER, } from "./components/assistant-chat.js";
|
|
10
11
|
export { ComposerAddMenu } from "./components/composer-add-menu.js";
|
|
11
12
|
export { ChatBubble } from "./components/chat-bubble.js";
|
|
@@ -97,6 +98,7 @@ export { registerAttachmentAdapter, useAttachmentAdapterStore } from "./lib/atta
|
|
|
97
98
|
export { registerVoiceAdapter, registerSpeechAdapter, useVoiceAdapterStore, } from "./lib/voice-adapter-registry.js";
|
|
98
99
|
export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
|
|
99
100
|
export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
|
|
101
|
+
export { ConversationMemory, useConversationMemory, } from "./components/conversation-memory.js";
|
|
100
102
|
// --- Auth ---
|
|
101
103
|
// `AuthProvider` is the React MSAL bootstrap (uses `@azure/msal-react`).
|
|
102
104
|
// Token store, config, fetch wrapper all live in `@iloveagents/foundry-agent/msal`.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* @see https://www.assistant-ui.com/docs/runtimes/custom/local
|
|
18
18
|
*/
|
|
19
19
|
import { mergeChatState } from "./merge-chat-state.js";
|
|
20
|
-
import { AGUIRunner, clientToolRegistry } from "@iloveagents/foundry-agent";
|
|
20
|
+
import { AGUIRunner, clientToolRegistry, contextUsageStore } from "@iloveagents/foundry-agent";
|
|
21
21
|
import { tokenFetch } from "@iloveagents/foundry-agent/msal";
|
|
22
22
|
import { useAppStore } from "./app-store.js";
|
|
23
23
|
import { useDevStore } from "./dev-store.js";
|
|
@@ -39,7 +39,7 @@ export class AGUIAdapterSDK {
|
|
|
39
39
|
}
|
|
40
40
|
async *run({ messages, abortSignal }) {
|
|
41
41
|
if (useChatRuns.getState().deletedThreads[this.threadId]) {
|
|
42
|
-
throw new Error("This
|
|
42
|
+
throw new Error("This chat was deleted. Start a new chat to continue.");
|
|
43
43
|
}
|
|
44
44
|
const aguiMessages = convertMessagesToAGUI(messages);
|
|
45
45
|
// App-level lifecycle: mark thread active, snapshot context, consume
|
|
@@ -78,7 +78,7 @@ export class AGUIAdapterSDK {
|
|
|
78
78
|
.filter((p) => p.type === "text")
|
|
79
79
|
.map((p) => p.text)
|
|
80
80
|
.join(" ")
|
|
81
|
-
.slice(0, 120) || "
|
|
81
|
+
.slice(0, 120) || "Chat",
|
|
82
82
|
pageLabel: useAppStore.getState().navContext.label ?? undefined,
|
|
83
83
|
contextMeta: { ...useAppStore.getState().navContext.meta },
|
|
84
84
|
stop: () => stopController.abort(),
|
|
@@ -175,6 +175,14 @@ export class AGUIAdapterSDK {
|
|
|
175
175
|
case "heartbeat":
|
|
176
176
|
// Server liveness ping — already recorded via touchSignal above.
|
|
177
177
|
break;
|
|
178
|
+
case "usage":
|
|
179
|
+
contextUsageStore.getState().record(this.threadId, {
|
|
180
|
+
inputTokens: event.inputTokens,
|
|
181
|
+
outputTokens: event.outputTokens ?? null,
|
|
182
|
+
contextUsed: event.contextUsed ?? null,
|
|
183
|
+
contextLimit: event.contextLimit ?? null,
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
178
186
|
case "request-sent":
|
|
179
187
|
if (import.meta.env.DEV) {
|
|
180
188
|
useDevStore.getState().captureRequest(event.input);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep rendering machinery out of the agent state projection.
|
|
3
|
+
*
|
|
4
|
+
* Navigation metadata is written by the UI for the UI: a nav group's `meta`
|
|
5
|
+
* may carry the items a panel renders, with their icon components and click
|
|
6
|
+
* handlers. The projection built from it is `structuredClone`d on every run
|
|
7
|
+
* and sent to the agent, and a component or a function cannot be cloned —
|
|
8
|
+
* the run then fails in the browser before a request is made, with nothing
|
|
9
|
+
* in any server log.
|
|
10
|
+
*
|
|
11
|
+
* The rule: a value that would make the clone throw marks the object holding
|
|
12
|
+
* it as UI, and that object is left out whole — except the projected record
|
|
13
|
+
* itself, a container, which only loses those entries. A half-stripped nav item
|
|
14
|
+
* (a label with its icon and click handler removed) would still tell the agent
|
|
15
|
+
* something the author never meant it to hear. Everything a clone already
|
|
16
|
+
* accepted passes through unchanged, so no projection that works today
|
|
17
|
+
* changes shape.
|
|
18
|
+
*/
|
|
19
|
+
/** Project a record to what may safely be cloned and sent to the agent. */
|
|
20
|
+
export declare function toAgentData(value: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep rendering machinery out of the agent state projection.
|
|
3
|
+
*
|
|
4
|
+
* Navigation metadata is written by the UI for the UI: a nav group's `meta`
|
|
5
|
+
* may carry the items a panel renders, with their icon components and click
|
|
6
|
+
* handlers. The projection built from it is `structuredClone`d on every run
|
|
7
|
+
* and sent to the agent, and a component or a function cannot be cloned —
|
|
8
|
+
* the run then fails in the browser before a request is made, with nothing
|
|
9
|
+
* in any server log.
|
|
10
|
+
*
|
|
11
|
+
* The rule: a value that would make the clone throw marks the object holding
|
|
12
|
+
* it as UI, and that object is left out whole — except the projected record
|
|
13
|
+
* itself, a container, which only loses those entries. A half-stripped nav item
|
|
14
|
+
* (a label with its icon and click handler removed) would still tell the agent
|
|
15
|
+
* something the author never meant it to hear. Everything a clone already
|
|
16
|
+
* accepted passes through unchanged, so no projection that works today
|
|
17
|
+
* changes shape.
|
|
18
|
+
*/
|
|
19
|
+
const OMIT = Symbol("omit");
|
|
20
|
+
/**
|
|
21
|
+
* Built-ins `structuredClone` copies natively (typed arrays via `isView`).
|
|
22
|
+
* Not `Error`: a clone copies its `cause`, which can hold anything.
|
|
23
|
+
*/
|
|
24
|
+
const CLONEABLE_TAGS = new Set([
|
|
25
|
+
"[object Date]",
|
|
26
|
+
"[object RegExp]",
|
|
27
|
+
"[object ArrayBuffer]",
|
|
28
|
+
"[object Blob]",
|
|
29
|
+
"[object File]",
|
|
30
|
+
"[object FileList]",
|
|
31
|
+
"[object ImageData]",
|
|
32
|
+
"[object Boolean]",
|
|
33
|
+
"[object Number]",
|
|
34
|
+
"[object String]",
|
|
35
|
+
"[object BigInt]",
|
|
36
|
+
]);
|
|
37
|
+
function isUiValue(value) {
|
|
38
|
+
if (typeof value === "function" || typeof value === "symbol")
|
|
39
|
+
return true;
|
|
40
|
+
// React elements and component objects (memo / forwardRef / lazy) carry a
|
|
41
|
+
// symbol-valued `$$typeof`. Plain data may use the key too; only the
|
|
42
|
+
// symbol marks React.
|
|
43
|
+
return (typeof value === "object" &&
|
|
44
|
+
value !== null &&
|
|
45
|
+
typeof value.$$typeof === "symbol");
|
|
46
|
+
}
|
|
47
|
+
function project(value, depth, seen) {
|
|
48
|
+
if (isUiValue(value))
|
|
49
|
+
return OMIT;
|
|
50
|
+
if (value === null || typeof value !== "object")
|
|
51
|
+
return value;
|
|
52
|
+
// Cycles only: a depth cap would cut off deep data that clones fine today.
|
|
53
|
+
if (seen.has(value))
|
|
54
|
+
return OMIT;
|
|
55
|
+
seen.add(value);
|
|
56
|
+
try {
|
|
57
|
+
// A clone copies collections by cloning every entry, so an entry holding
|
|
58
|
+
// UI would still make it throw: walk them like any other container.
|
|
59
|
+
if (value instanceof Map) {
|
|
60
|
+
const out = new Map();
|
|
61
|
+
for (const [k, v] of value) {
|
|
62
|
+
const key = project(k, depth + 1, seen);
|
|
63
|
+
const val = project(v, depth + 1, seen);
|
|
64
|
+
if (key !== OMIT && val !== OMIT)
|
|
65
|
+
out.set(key, val);
|
|
66
|
+
}
|
|
67
|
+
return value.size > 0 && out.size === 0 ? OMIT : out;
|
|
68
|
+
}
|
|
69
|
+
if (value instanceof Set) {
|
|
70
|
+
const out = new Set();
|
|
71
|
+
for (const v of value) {
|
|
72
|
+
const val = project(v, depth + 1, seen);
|
|
73
|
+
if (val !== OMIT)
|
|
74
|
+
out.add(val);
|
|
75
|
+
}
|
|
76
|
+
return value.size > 0 && out.size === 0 ? OMIT : out;
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(value) && !isRecord(value)) {
|
|
79
|
+
// Built-ins a clone copies natively keep doing so. Anything else that
|
|
80
|
+
// is not a plain object by tag — a DOM node held by a ref, a WeakMap, a
|
|
81
|
+
// Promise — makes the clone throw. A class instance tags as a plain
|
|
82
|
+
// object, and a clone copies its own fields, so it is walked like one.
|
|
83
|
+
const tag = Object.prototype.toString.call(value);
|
|
84
|
+
if (CLONEABLE_TAGS.has(tag) || ArrayBuffer.isView(value))
|
|
85
|
+
return value;
|
|
86
|
+
if (tag !== "[object Object]")
|
|
87
|
+
return OMIT;
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
const out = value.map((item) => project(item, depth + 1, seen)).filter((v) => v !== OMIT);
|
|
91
|
+
return value.length > 0 && out.length === 0 ? OMIT : out;
|
|
92
|
+
}
|
|
93
|
+
const entries = Object.entries(value);
|
|
94
|
+
// The projected record itself is a container (a page's meta, a group's
|
|
95
|
+
// meta): its UI entries are dropped, the rest kept. Below it, an object
|
|
96
|
+
// holding a component or a handler IS a UI object, and goes whole.
|
|
97
|
+
if (depth > 0 && entries.some(([, v]) => isUiValue(v)))
|
|
98
|
+
return OMIT;
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const [key, v] of entries) {
|
|
101
|
+
const projected = project(v, depth + 1, seen);
|
|
102
|
+
// defineProperty, not assignment: a "__proto__" key from JSON data is
|
|
103
|
+
// an own property, and assigning it would set the prototype instead.
|
|
104
|
+
if (projected !== OMIT) {
|
|
105
|
+
Object.defineProperty(out, key, {
|
|
106
|
+
value: projected,
|
|
107
|
+
enumerable: true,
|
|
108
|
+
writable: true,
|
|
109
|
+
configurable: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
seen.delete(value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isRecord(value) {
|
|
120
|
+
const proto = Object.getPrototypeOf(value);
|
|
121
|
+
return proto === Object.prototype || proto === null;
|
|
122
|
+
}
|
|
123
|
+
/** Project a record to what may safely be cloned and sent to the agent. */
|
|
124
|
+
export function toAgentData(value) {
|
|
125
|
+
const projected = project(value, 0, new WeakSet());
|
|
126
|
+
return projected === OMIT ? {} : projected;
|
|
127
|
+
}
|
package/dist/lib/app-store.js
CHANGED
|
@@ -9,6 +9,7 @@ import { collectFocusContext } from "./focus-context.js";
|
|
|
9
9
|
* - Strict AG-UI state projection sent to the agent on every request
|
|
10
10
|
*/
|
|
11
11
|
import { create } from "zustand";
|
|
12
|
+
import { toAgentData } from "./agent-data.js";
|
|
12
13
|
const MAX_ITEMS = 10;
|
|
13
14
|
const MAX_LABEL_LENGTH = 500;
|
|
14
15
|
const MAX_CONTEXT_VALUE_LENGTH = 2000;
|
|
@@ -207,10 +208,7 @@ export const useAppStore = create((set, get) => ({
|
|
|
207
208
|
? item.label.slice(0, MAX_LABEL_LENGTH) + "..."
|
|
208
209
|
: item.label;
|
|
209
210
|
set({
|
|
210
|
-
contextItems: [
|
|
211
|
-
...contextItems,
|
|
212
|
-
{ ...item, label, id: createUiId(), createdAt: Date.now() },
|
|
213
|
-
],
|
|
211
|
+
contextItems: [...contextItems, { ...item, label, id: createUiId(), createdAt: Date.now() }],
|
|
214
212
|
});
|
|
215
213
|
},
|
|
216
214
|
setContextItem: (key, item) => {
|
|
@@ -277,11 +275,13 @@ export const useAppStore = create((set, get) => ({
|
|
|
277
275
|
navigation: {
|
|
278
276
|
group: s.navContext.group,
|
|
279
277
|
groupDescription: s.navContext.groupDescription,
|
|
280
|
-
|
|
278
|
+
// Nav metadata is written for the UI and may hold what a panel
|
|
279
|
+
// renders — components, handlers. Only data reaches the agent.
|
|
280
|
+
groupMeta: toAgentData(s.navContext.groupMeta),
|
|
281
281
|
page: s.currentPage,
|
|
282
282
|
label: s.navContext.label,
|
|
283
283
|
description: s.navContext.description,
|
|
284
|
-
meta: focusedSurface ? { ...s.navContext.meta, focusedSurface } : s.navContext.meta,
|
|
284
|
+
meta: toAgentData(focusedSurface ? { ...s.navContext.meta, focusedSurface } : s.navContext.meta),
|
|
285
285
|
contextInstructions: focusedSurface
|
|
286
286
|
? [
|
|
287
287
|
s.navContext.contextInstructions,
|
|
@@ -292,7 +292,7 @@ export const useAppStore = create((set, get) => ({
|
|
|
292
292
|
: s.navContext.contextInstructions,
|
|
293
293
|
},
|
|
294
294
|
selectedItemId: s.selectedItemId,
|
|
295
|
-
content: s.content,
|
|
295
|
+
content: toAgentData(s.content),
|
|
296
296
|
highlights: s.highlights,
|
|
297
297
|
contextItems: s.contextItems.map(({ type, label, payload, sourcePage }) => {
|
|
298
298
|
switch (payload.kind) {
|
|
@@ -313,7 +313,7 @@ export const useAppStore = create((set, get) => ({
|
|
|
313
313
|
refId: payload.refId,
|
|
314
314
|
preview: payload.preview?.slice(0, 100),
|
|
315
315
|
target: compactReferenceTarget(payload.target),
|
|
316
|
-
meta: payload.meta,
|
|
316
|
+
meta: payload.meta ? toAgentData(payload.meta) : payload.meta,
|
|
317
317
|
targetPath: typeof payload.target?.path === "string"
|
|
318
318
|
? payload.target.path
|
|
319
319
|
: typeof payload.meta?.targetPath === "string"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
-
import { agentStateStore, citationStore, streamingStatusStore, } from "@iloveagents/foundry-agent";
|
|
2
|
+
import { agentStateStore, citationStore, contextUsageStore, streamingStatusStore, } from "@iloveagents/foundry-agent";
|
|
3
3
|
export const isChatRunActive = (run) => run.status === "running" || run.status === "waiting";
|
|
4
4
|
/** Browser-session activity. Message persistence remains owned by the history adapter. */
|
|
5
5
|
export const useChatRuns = create(() => ({ foregroundId: null, runs: {}, deletedThreads: {} }));
|
|
@@ -48,6 +48,7 @@ export function removeChatRun(threadId) {
|
|
|
48
48
|
return { runs, deletedThreads: { ...state.deletedThreads, [threadId]: true } };
|
|
49
49
|
});
|
|
50
50
|
citationStore.getState().clear(threadId);
|
|
51
|
+
contextUsageStore.getState().forget(threadId);
|
|
51
52
|
signals.delete(threadId);
|
|
52
53
|
agentStates.delete(threadId);
|
|
53
54
|
appProjections.delete(threadId);
|
|
@@ -164,6 +165,7 @@ export function disposeChatRuns() {
|
|
|
164
165
|
agentStates.clear();
|
|
165
166
|
appProjections.clear();
|
|
166
167
|
citationStore.setState({ results: [], byThread: {}, threadId: null });
|
|
168
|
+
contextUsageStore.getState().clear();
|
|
167
169
|
useChatRuns.setState({ foregroundId: null, runs: {}, deletedThreads: {} });
|
|
168
170
|
streamingStatusStore.setState({
|
|
169
171
|
streamingStatus: { status: "idle" },
|
|
@@ -3,7 +3,7 @@ import { TooltipIconButton } from "../index.js";
|
|
|
3
3
|
import { SquarePen, PanelLeft, MessageSquare, Menu, MessagesSquare, LoaderCircle, } from "lucide-react";
|
|
4
4
|
import { RunningChatsMenu } from "./running-chats-menu.js";
|
|
5
5
|
export function WorkbenchChatHeader({ collapsed, running, chatWidth, buttonClass, toggleNavigation, setCollapsed, showHistory, startChat, returnToChat, openHistory, chatOnly, setContentCollapsed, }) {
|
|
6
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-chat-header": true, className: `relative flex shrink-0 items-center ${collapsed ? "justify-center" : "gap-1 px-2"}`, children: [!collapsed && !chatWidth.compact && (_jsx(TooltipIconButton, { tooltip: "Open navigation", className: `${buttonClass} preview-mobile-navigation`, "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { size: 18 }) })), collapsed ? (_jsx(TooltipIconButton, { tooltip: running ? "AI is working — expand chat" : "Expand chat", className: buttonClass, "aria-label": running ? "AI is working — expand chat" : "Expand chat", onClick: () => setCollapsed(false), children: running ? (_jsx(LoaderCircle, { size: 18, className: "animate-spin motion-reduce:animate-none text-primary" })) : (_jsx(MessageSquare, { size: 18 })) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-0 flex-1 px-2 py-2", children: _jsx("div", { className: "text-sm font-semibold", children: showHistory ? "
|
|
6
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-chat-header": true, className: `relative flex shrink-0 items-center ${collapsed ? "justify-center" : "gap-1 px-2"}`, children: [!collapsed && !chatWidth.compact && (_jsx(TooltipIconButton, { tooltip: "Open navigation", className: `${buttonClass} preview-mobile-navigation`, "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { size: 18 }) })), collapsed ? (_jsx(TooltipIconButton, { tooltip: running ? "AI is working — expand chat" : "Expand chat", className: buttonClass, "aria-label": running ? "AI is working — expand chat" : "Expand chat", onClick: () => setCollapsed(false), children: running ? (_jsx(LoaderCircle, { size: 18, className: "animate-spin motion-reduce:animate-none text-primary" })) : (_jsx(MessageSquare, { size: 18 })) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-0 flex-1 px-2 py-2", children: _jsx("div", { className: "text-sm font-semibold", children: showHistory ? "Chats" : "Chat" }) }), _jsx(RunningChatsMenu, { onOpen: returnToChat }), _jsx(TooltipIconButton, { tooltip: "New chat", className: buttonClass, "aria-label": "New chat", onClick: startChat, children: _jsx(SquarePen, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Chat history", className: buttonClass, "aria-label": "Chat history", "aria-expanded": showHistory, onClick: () => (showHistory ? returnToChat() : openHistory()), children: _jsx(MessagesSquare, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Collapse chat", style: chatOnly || chatWidth.compact ? { display: "none" } : undefined, className: buttonClass, "aria-label": "Collapse chat", onClick: () => {
|
|
7
7
|
setContentCollapsed(false);
|
|
8
8
|
setCollapsed(true);
|
|
9
9
|
}, children: _jsx(PanelLeft, { size: 18 }) })] }))] }), collapsed && (_jsxs("div", { "data-chat-rail": true, className: "flex flex-col items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: "Open navigation", className: `${buttonClass} preview-mobile-navigation`, "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { size: 18 }) }), _jsx(TooltipIconButton, { tooltip: "Chat history", className: buttonClass, "aria-label": "Chat history", onClick: () => {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { NavGroup } from "../lib/nav-config.js";
|
|
2
2
|
/** History is a view of existing navigation targets, never a second chat runtime. */
|
|
3
|
-
export declare function ConversationHistory({ groups, workspaceId, onClose, showBack, welcome, compact, onViewAll, entityId, rootContext, }: {
|
|
3
|
+
export declare function ConversationHistory({ groups, workspaceId, onClose, showBack, welcome, compact, home, onViewAll, entityId, rootContext, }: {
|
|
4
4
|
groups: NavGroup[];
|
|
5
5
|
workspaceId: unknown;
|
|
6
6
|
onClose: () => void;
|
|
7
7
|
showBack?: boolean;
|
|
8
8
|
welcome?: boolean;
|
|
9
9
|
compact?: boolean;
|
|
10
|
+
/** The global start page shows the three newest chats below the composer. */
|
|
11
|
+
home?: boolean;
|
|
10
12
|
onViewAll?: () => void;
|
|
11
13
|
/** Current content entity. Compact homes show only an exact match unless this is the workspace root. */
|
|
12
14
|
entityId?: unknown;
|
|
@@ -6,7 +6,7 @@ import { Link } from "react-router";
|
|
|
6
6
|
import { ArrowLeft, MessageSquare, MoreHorizontal, Pin, Loader2, ChevronDown, Check, MessagesSquare, PauseCircle, Copy, } from "lucide-react";
|
|
7
7
|
import { useChatRuns, isChatRunActive } from "../lib/chat-runs-store.js";
|
|
8
8
|
/** History is a view of existing navigation targets, never a second chat runtime. */
|
|
9
|
-
export function ConversationHistory({ groups, workspaceId, onClose, showBack = true, welcome = false, compact = false, onViewAll, entityId, rootContext = false, }) {
|
|
9
|
+
export function ConversationHistory({ groups, workspaceId, onClose, showBack = true, welcome = false, compact = false, home = false, onViewAll, entityId, rootContext = false, }) {
|
|
10
10
|
const [loadingTo, setLoadingTo] = useState(null);
|
|
11
11
|
const [copiedId, setCopiedId] = useState(null);
|
|
12
12
|
const [loadError, setLoadError] = useState(null);
|
|
@@ -18,6 +18,8 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
|
|
|
18
18
|
groups
|
|
19
19
|
.filter((g) => g.meta?.type === "recent-chats" || g.meta?.type === "pinned-chats")
|
|
20
20
|
.flatMap((g) => g.items);
|
|
21
|
+
const savedTargets = new Set(allItems.map((item) => item.to));
|
|
22
|
+
const hasServerTimestamps = allItems.some((item) => typeof item.meta?.updatedAt === "string");
|
|
21
23
|
const runs = useChatRuns((s) => s.runs);
|
|
22
24
|
const runFor = (item) => Object.values(runs).find((run) => (run.conversationUrl ?? `/chat/${encodeURIComponent(run.threadId)}`) === item.to);
|
|
23
25
|
const workspaceItems = all || !workspaceId
|
|
@@ -57,8 +59,26 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
|
|
|
57
59
|
const run = runFor(item);
|
|
58
60
|
return run && isChatRunActive(run) ? 1 : 0;
|
|
59
61
|
};
|
|
62
|
+
if (home) {
|
|
63
|
+
// The server list can lag a just-finished run. Compare its session
|
|
64
|
+
// timestamp with server timestamps so newer chats from other tabs
|
|
65
|
+
// take priority as soon as navigation refreshes.
|
|
66
|
+
const activityAt = (item) => {
|
|
67
|
+
const updatedAt = item.meta?.updatedAt;
|
|
68
|
+
const serverAt = typeof updatedAt === "string" ? Date.parse(updatedAt) || 0 : 0;
|
|
69
|
+
const run = runFor(item);
|
|
70
|
+
const sessionAt = run && (hasServerTimestamps || !savedTargets.has(item.to))
|
|
71
|
+
? (run.finishedAt ?? run.startedAt)
|
|
72
|
+
: 0;
|
|
73
|
+
return Math.max(serverAt, sessionAt);
|
|
74
|
+
};
|
|
75
|
+
return active(b) - active(a) || activityAt(b) - activityAt(a);
|
|
76
|
+
}
|
|
60
77
|
return (active(b) - active(a) || Number(Boolean(b.meta?.pinned)) - Number(Boolean(a.meta?.pinned)));
|
|
61
78
|
});
|
|
79
|
+
const homeRecentItems = home ? items.filter((item) => !item.meta?.pinned).slice(0, 3) : [];
|
|
80
|
+
const homePinnedItems = home ? items.filter((item) => item.meta?.pinned).slice(0, 3) : [];
|
|
81
|
+
const homeHasItems = homeRecentItems.length > 0 || homePinnedItems.length > 0;
|
|
62
82
|
const visibleItems = compact ? items.slice(0, 3) : items;
|
|
63
83
|
const activeItems = visibleItems.filter((item) => {
|
|
64
84
|
const run = runFor(item);
|
|
@@ -66,22 +86,43 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
|
|
|
66
86
|
});
|
|
67
87
|
const idleItems = visibleItems.filter((item) => !activeItems.includes(item));
|
|
68
88
|
const pinnedItems = idleItems.filter((item) => item.meta?.pinned);
|
|
69
|
-
const sections =
|
|
70
|
-
? [
|
|
71
|
-
|
|
72
|
-
{ label: "
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
const sections = home
|
|
90
|
+
? [
|
|
91
|
+
{ label: "Recent", items: homeRecentItems },
|
|
92
|
+
{ label: "Pinned", items: homePinnedItems },
|
|
93
|
+
]
|
|
94
|
+
: compact
|
|
95
|
+
? [{ label: "", items: visibleItems }]
|
|
96
|
+
: [
|
|
97
|
+
{ label: "Running", items: activeItems },
|
|
98
|
+
{ label: "Pinned", items: pinnedItems },
|
|
99
|
+
{
|
|
100
|
+
label: activeItems.length || pinnedItems.length ? "Recent chats" : "",
|
|
101
|
+
items: idleItems.filter((item) => !item.meta?.pinned),
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
return (_jsxs("section", { "data-conversation-history": true, "aria-label": home ? "Home chats" : compact ? "Recent chats" : "Chat history", className: home
|
|
105
|
+
? "w-full"
|
|
106
|
+
: compact
|
|
107
|
+
? "w-full"
|
|
108
|
+
: "mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col px-4 pb-4", children: [welcome && (_jsxs("div", { className: "pb-6 pt-8", children: [_jsx("h1", { className: "text-xl font-semibold", children: "How can I help?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Start something new or continue a recent chat." })] })), showBack && (_jsxs("button", { onClick: onClose, className: "mb-4 flex items-center gap-2 py-2 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { size: 14 }), " Back to chat"] })), (!compact || items.length > 0) && (_jsxs("div", { className: home
|
|
109
|
+
? "mb-2 mt-5 flex items-center justify-between px-1"
|
|
110
|
+
: "mb-2 mt-2 flex items-center justify-between", children: [_jsx("h2", { className: compact ? "text-xs font-medium text-muted-foreground" : "text-base font-semibold", children: home ? "Your chats" : compact ? "Recent chats" : "Chat history" }), home && onViewAll && (_jsx("button", { type: "button", onClick: onViewAll, className: "rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary [@media(pointer:coarse)]:min-h-11", children: "View all" }))] })), Boolean(workspaceId) && !compact && (_jsx("div", { className: "mb-3", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", "aria-label": "History filter", className: "w-full justify-between font-normal shadow-none", children: [_jsx("span", { className: "truncate", children: all ? "All chats" : "Last used in this workspace" }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "w-[var(--radix-dropdown-menu-trigger-width)]", children: [
|
|
80
111
|
{ value: false, label: "Last used in this workspace" },
|
|
81
|
-
{ value: true, label: "All
|
|
82
|
-
].map((option) => (_jsxs(DropdownMenuItem, { role: "menuitemradio", "aria-checked": all === option.value, onSelect: () => setAll(option.value), children: [_jsx("span", { className: "flex-1", children: option.label }), all === option.value && _jsx(Check, { className: "size-4", "aria-hidden": "true" })] }, option.label))) })] }) })), !compact && (_jsx(Input, { "aria-label": "Search chats", placeholder: "Search chats", value: search, onChange: (event) => setSearch(event.target.value), className: "mb-3" })), _jsx("span", { className: "sr-only", role: "status", children: copiedId ? "Chat ID copied" : "" }), loadError && (_jsx("p", { role: "alert", className: "mb-2 text-xs text-destructive", children: loadError })), _jsxs("div", { className:
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
{ value: true, label: "All chats" },
|
|
113
|
+
].map((option) => (_jsxs(DropdownMenuItem, { role: "menuitemradio", "aria-checked": all === option.value, onSelect: () => setAll(option.value), children: [_jsx("span", { className: "flex-1", children: option.label }), all === option.value && _jsx(Check, { className: "size-4", "aria-hidden": "true" })] }, option.label))) })] }) })), !compact && (_jsx(Input, { "aria-label": "Search chats", placeholder: "Search chats", value: search, onChange: (event) => setSearch(event.target.value), className: "mb-3" })), _jsx("span", { className: "sr-only", role: "status", children: copiedId ? "Chat ID copied" : "" }), loadError && (_jsx("p", { role: "alert", className: "mb-2 text-xs text-destructive", children: loadError })), _jsxs("div", { className: home
|
|
114
|
+
? "grid grid-cols-[repeat(auto-fit,minmax(min(100%,18rem),1fr))] gap-4"
|
|
115
|
+
: "min-h-0 flex-1 overflow-y-auto", "aria-busy": Boolean(loadingTo), children: [sections
|
|
116
|
+
.filter((section) => (home ? homeHasItems : section.items.length))
|
|
117
|
+
.map((section) => (_jsxs("section", { "aria-label": section.label || "Recent chats", className: home
|
|
118
|
+
? `min-w-0 rounded-xl border border-border/70 bg-muted/10 p-3 ${section.items.length ? "" : "hidden min-[640px]:block"}`
|
|
119
|
+
: "mb-4 last:mb-0", children: [section.label && (_jsx("h3", { "data-conversation-section-label": true, className: home
|
|
120
|
+
? "px-1 pb-2 text-xs font-medium text-muted-foreground"
|
|
121
|
+
: "px-2 py-2 text-xs font-medium text-muted-foreground", children: section.label })), home && !section.items.length && (_jsx("p", { className: "px-1 py-3 text-xs text-muted-foreground", children: section.label === "Pinned"
|
|
122
|
+
? "Pin chats to keep them close."
|
|
123
|
+
: "Your latest chats will appear here." })), section.items.map((item) => item.disabled ? (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: item.label }, item.to)) : (_jsxs("div", { className: home
|
|
124
|
+
? "group flex items-center rounded-lg transition-colors hover:bg-muted/60"
|
|
125
|
+
: "group flex items-center rounded-lg hover:bg-muted", children: [_jsxs(Link, { to: item.to, onClick: (event) => {
|
|
85
126
|
if (loadingTo) {
|
|
86
127
|
event.preventDefault();
|
|
87
128
|
return;
|
|
@@ -111,7 +152,9 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
|
|
|
111
152
|
}
|
|
112
153
|
setLoadingTo(null);
|
|
113
154
|
onClose();
|
|
114
|
-
}, "data-conversation-row": true, className:
|
|
155
|
+
}, "data-conversation-row": true, className: home
|
|
156
|
+
? "flex min-w-0 flex-1 items-center gap-3 rounded-lg px-2 py-2 text-left text-sm focus-visible:outline-2 focus-visible:outline-primary"
|
|
157
|
+
: "flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 py-1.5 text-sm hover:bg-muted focus-visible:outline-2 focus-visible:outline-primary", children: [loadingTo === item.to ? (_jsx(Loader2, { className: "size-4 shrink-0 animate-spin text-muted-foreground", "aria-label": "Opening chat" })) : runFor(item)?.status === "running" ? (_jsx(Loader2, { className: "size-4 shrink-0 animate-spin text-primary motion-reduce:animate-none", "aria-hidden": true })) : runFor(item)?.status === "waiting" ? (_jsx(PauseCircle, { className: "size-4 shrink-0 text-primary", "aria-hidden": true })) : item.meta?.pinned ? (_jsx(Pin, { className: "size-4 shrink-0 text-muted-foreground", "aria-label": "Pinned chat" })) : (_jsx(MessageSquare, { className: "size-4 shrink-0 text-muted-foreground" })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.label }), " ", runFor(item) && isChatRunActive(runFor(item)) ? (_jsx("span", { className: "block text-xs text-primary", children: runFor(item)?.status === "waiting" ? "Waiting for you" : "Running" })) : ((!compact || rootContext) && (_jsx("span", { className: "block truncate text-xs text-muted-foreground", children: !all &&
|
|
115
158
|
typeof item.meta?.previewEntityName === "string" &&
|
|
116
159
|
item.meta.previewEntityName
|
|
117
160
|
? `Last at ${item.meta.previewEntityName}`
|
|
@@ -138,5 +181,5 @@ export function ConversationHistory({ groups, workspaceId, onClose, showBack = t
|
|
|
138
181
|
.catch(() => setLoadError(`Could not copy. Chat ID: ${decodeURIComponent(id)}`));
|
|
139
182
|
}, children: [_jsx(Copy, { className: "size-4" }), "Copy chat ID"] }), Boolean(item.actions?.length) && _jsx(DropdownMenuSeparator, {}), item.actions?.map((action) => (_jsxs(Fragment, { children: [action.separator && _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => {
|
|
140
183
|
void action.handler();
|
|
141
|
-
}, className: action.destructive ? "text-destructive" : undefined, children: [_jsx(action.icon, { className: "mr-2 size-4" }), action.label] })] }, action.label)))] })] })] }, item.to)))] }, section.label))), items.length === 0 && !compact && (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: search ? "No matching chats" : "No chats yet" }))] }), compact && onViewAll && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onViewAll, className: "mt-1 h-9 w-fit gap-2 px-2 text-sm font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(MessagesSquare, { className: "size-4", "aria-hidden": true }), "
|
|
184
|
+
}, className: action.destructive ? "text-destructive" : undefined, children: [_jsx(action.icon, { className: "mr-2 size-4" }), action.label] })] }, action.label)))] })] })] }, item.to)))] }, section.label))), items.length === 0 && !compact && (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: search ? "No matching chats" : "No chats yet" }))] }), compact && onViewAll && !home && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onViewAll, className: "mt-1 h-9 w-fit gap-2 px-2 text-sm font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(MessagesSquare, { className: "size-4", "aria-hidden": true }), "Chat history"] }))] }));
|
|
142
185
|
}
|
|
@@ -11,7 +11,7 @@ export function RunningChatsMenu({ onOpen } = {}) {
|
|
|
11
11
|
const active = Object.values(runs).filter(isChatRunActive);
|
|
12
12
|
if (!active.length)
|
|
13
13
|
return null;
|
|
14
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": `${active.length} running
|
|
14
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": `${active.length} running chats`, className: "inline-flex items-center gap-1.5 px-2 text-xs text-primary", children: [_jsx(LoaderCircle, { className: "size-3.5 animate-spin motion-reduce:animate-none" }), _jsx("span", { children: active.length })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "max-w-80", children: [_jsx(DropdownMenuLabel, { children: "Running in this browser" }), active.map((run) => (_jsxs(DropdownMenuItem, { onSelect: () => {
|
|
15
15
|
if (!selectRetainedThread(run.threadId)) {
|
|
16
16
|
navigate(run.conversationUrl ?? `/chat/${encodeURIComponent(run.threadId)}`);
|
|
17
17
|
}
|
|
@@ -105,7 +105,7 @@ export function useWorkbenchState(chatRoute) {
|
|
|
105
105
|
const openHistory = () => {
|
|
106
106
|
setHistoryOpen(true);
|
|
107
107
|
};
|
|
108
|
-
const historyShortcut = (_jsxs(Button, { variant: "ghost", size: "sm", onClick: openHistory, className: "h-9 w-fit gap-2 px-2 text-sm font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(MessagesSquare, { className: "size-4", "aria-hidden": true }), "
|
|
108
|
+
const historyShortcut = (_jsxs(Button, { variant: "ghost", size: "sm", onClick: openHistory, className: "h-9 w-fit gap-2 px-2 text-sm font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(MessagesSquare, { className: "size-4", "aria-hidden": true }), "Chat history"] }));
|
|
109
109
|
const toggleNavigation = useSidebarStore((s) => s.toggleMobile);
|
|
110
110
|
useEffect(() => {
|
|
111
111
|
if (panelOpen) {
|
|
@@ -4,7 +4,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
|
|
|
4
4
|
import { ChatHomeStartersSlot } from "../components/chat-slots.js";
|
|
5
5
|
import { Layers } from "lucide-react";
|
|
6
6
|
export function WelcomeIntro({ navGroups, historyShortcut, }) {
|
|
7
|
-
return (_jsxs("div", { className: "mb-4 text-center", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: "What would you like to get done?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Find information, create or edit content, and run actions across your spaces." }), _jsx("div", { className: "mx-auto mt-4 max-w-sm text-left", children: _jsx(ChatHomeStartersSlot, { priority: 10 }) }), _jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [historyShortcut, navGroups.some((group) => group.meta?.type === "spaces-workspaces" && group.createActions?.length) && (_jsx("div", { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "h-9 gap-2 px-
|
|
7
|
+
return (_jsxs("div", { className: "mb-4 text-center", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: "What would you like to get done?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Find information, create or edit content, and run actions across your spaces." }), _jsx("div", { className: "mx-auto mt-4 max-w-sm text-left", children: _jsx(ChatHomeStartersSlot, { priority: 10 }) }), _jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [historyShortcut, navGroups.some((group) => group.meta?.type === "spaces-workspaces" && group.createActions?.length) && (_jsx("div", { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "h-9 gap-2 bg-primary/10 px-3 text-sm font-medium hover:bg-primary/15 [@media(pointer:coarse)]:min-h-11", children: [_jsx(Layers, { className: "size-4", "aria-hidden": "true" }), "Create a space"] }) }), _jsx(DropdownMenuContent, { align: "center", children: navGroups
|
|
8
8
|
.filter((group) => group.meta?.type === "spaces-workspaces")
|
|
9
9
|
.flatMap((group) => group.createActions ?? [])
|
|
10
10
|
.map((action) => (_jsx(DropdownMenuItem, { onSelect: () => action.handler(), children: action.label }, action.id))) })] }) }))] })] }));
|
|
@@ -23,6 +23,7 @@ export function WorkbenchPreview({ page, chatOnly: chatRoute = false, }) {
|
|
|
23
23
|
normalizeWorkbenchPath(rootItem.to) === normalizeWorkbenchPath(currentPage));
|
|
24
24
|
const conversationHome = (_jsx(ConversationHistory, { groups: navGroups, workspaceId: workspaceId, entityId: entityId, rootContext: rootContext, onClose: returnToChat, onViewAll: openHistory, showBack: false, compact: true }));
|
|
25
25
|
const emptyChatFooter = workspaceId ? conversationHome : historyShortcut;
|
|
26
|
+
const homeRecents = !workspaceId ? (_jsx(ConversationHistory, { groups: navGroups, workspaceId: undefined, onClose: returnToChat, onViewAll: openHistory, showBack: false, compact: true, home: true })) : undefined;
|
|
26
27
|
const buttonClass = "inline-flex items-center justify-center rounded-lg p-2 text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary";
|
|
27
28
|
return (_jsxs("div", { className: "workbench-preview flex min-h-0 flex-1 overflow-hidden", "data-workspace-navigation": workspaceGroups ? "true" : undefined, children: [_jsx(WorkbenchStyles, {}), _jsx(WorkbenchNavigation, { groups: staticNavigationGroups, workspace: !!workspaceGroups, onNewConversation: startChat }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", "data-workbench-surface": true, role: "region", "aria-label": "Workspace working area", children: [chatWidth.compact && (_jsxs("div", { className: "flex shrink-0 items-center gap-1 border-b border-border px-2 py-1", role: "group", "aria-label": "Workspace panes", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "preview-mobile-navigation size-11", "aria-label": "Open navigation", onClick: toggleNavigation, children: _jsx(Menu, { className: "size-4" }) }), ["content", "chat"]
|
|
28
29
|
.filter((pane) => !chatOnly || pane === "chat")
|
|
@@ -39,5 +40,5 @@ export function WorkbenchPreview({ page, chatOnly: chatRoute = false, }) {
|
|
|
39
40
|
}, children: [_jsx(WorkbenchChatHeader, { collapsed: collapsed, running: running, chatWidth: chatWidth, buttonClass: buttonClass, toggleNavigation: toggleNavigation, setCollapsed: setCollapsed, showHistory: showHistory, startChat: startChat, returnToChat: returnToChat, openHistory: openHistory, chatOnly: chatOnly, setContentCollapsed: setContentCollapsed }), !collapsed && showHistory && (_jsx(ConversationHistory, { groups: navGroups, workspaceId: workspaceId, onClose: returnToChat, showBack: true })), !chatWidth.compact && !collapsed && !contentCollapsed && (_jsx(ChatResizeHandle, { chatWidth: chatWidth })), _jsx("div", { className: "min-h-0 flex-1 flex-col", style: {
|
|
40
41
|
display: collapsed || showHistory ? "none" : "flex",
|
|
41
42
|
paddingBottom: spaciousChat && emptyChat && !historyOpen ? "clamp(32px, 10vh, 96px)" : undefined,
|
|
42
|
-
}, children: _jsx(ChatContent, { centerComposer: spaciousChat && emptyChat && !historyOpen, composerIntro: spaciousChat && emptyChat ? (_jsx(WelcomeIntro, { navGroups: navGroups, historyShortcut: emptyChatFooter })) : undefined, compactComposer: !spaciousChat, starterSuggestions: [], emptyState: _jsx(ChatEmptyState, { pageLabel: pageLabel, showHomeStarters: !spaciousChat, children: emptyChatFooter }) }) })] }), _jsx(WorkbenchContentPane, { contentHidden: contentHidden, panelOpen: panelOpen, chatWidth: chatWidth, contentCollapsed: contentCollapsed, collapsed: collapsed, buttonClass: buttonClass, setCollapsed: setCollapsed, setContentCollapsed: setContentCollapsed, closePanel: closePanel, page: page }), contentCollapsed && !chatOnly && !chatWidth.compact && (_jsx("aside", { "aria-label": "Collapsed content", className: "flex w-11 shrink-0 flex-col items-center", style: { width: 44 }, children: _jsx("div", { className: "flex h-full w-full flex-col items-center", "data-chat-rail": true, children: _jsx("div", { className: "flex items-center justify-center", style: { height: 60 }, children: _jsx(TooltipIconButton, { tooltip: "Expand content", className: buttonClass, "aria-label": "Expand content", onClick: () => setContentCollapsed(false), children: _jsx(FileText, { size: 18 }) }) }) }) }))] })] })] }));
|
|
43
|
+
}, children: _jsx(ChatContent, { centerComposer: spaciousChat && emptyChat && !historyOpen, composerIntro: spaciousChat && emptyChat ? (_jsx(WelcomeIntro, { navGroups: navGroups, historyShortcut: workspaceId ? emptyChatFooter : null })) : undefined, composerFooter: spaciousChat && emptyChat && !historyOpen && homeRecents ? (_jsx("div", { className: "w-full", children: homeRecents })) : undefined, compactComposer: !spaciousChat, starterSuggestions: [], emptyState: _jsx(ChatEmptyState, { pageLabel: pageLabel, showHomeStarters: !spaciousChat, children: emptyChatFooter }) }) })] }), _jsx(WorkbenchContentPane, { contentHidden: contentHidden, panelOpen: panelOpen, chatWidth: chatWidth, contentCollapsed: contentCollapsed, collapsed: collapsed, buttonClass: buttonClass, setCollapsed: setCollapsed, setContentCollapsed: setContentCollapsed, closePanel: closePanel, page: page }), contentCollapsed && !chatOnly && !chatWidth.compact && (_jsx("aside", { "aria-label": "Collapsed content", className: "flex w-11 shrink-0 flex-col items-center", style: { width: 44 }, children: _jsx("div", { className: "flex h-full w-full flex-col items-center", "data-chat-rail": true, children: _jsx("div", { className: "flex items-center justify-center", style: { height: 60 }, children: _jsx(TooltipIconButton, { tooltip: "Expand content", className: buttonClass, "aria-label": "Expand content", onClick: () => setContentCollapsed(false), children: _jsx(FileText, { size: 18 }) }) }) }) }))] })] })] }));
|
|
43
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
|
|
6
6
|
"keywords": [
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"recharts": "^3.10.1",
|
|
81
81
|
"remark-gfm": "^4.0.0",
|
|
82
82
|
"tailwind-merge": "^3.5.0",
|
|
83
|
-
"@iloveagents/foundry-agent": "^0.
|
|
84
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
83
|
+
"@iloveagents/foundry-agent": "^0.33.0",
|
|
84
|
+
"@iloveagents/foundry-web-primitives": "^0.33.0"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@ag-ui/client": "^0.0.52",
|