@iloveagents/foundry-web-ui 0.20.1 → 0.21.1
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.js +11 -1
- package/dist/components/assistant-chat.d.ts +11 -1
- package/dist/components/assistant-chat.js +22 -7
- package/dist/components/chat-bubble.js +20 -15
- package/dist/components/chat-slots.d.ts +35 -0
- package/dist/components/chat-slots.js +31 -0
- package/dist/components/collection-empty-state.js +1 -1
- package/dist/components/composer-add-menu.d.ts +25 -0
- package/dist/components/composer-add-menu.js +16 -0
- package/dist/components/composer-submit-bridge.d.ts +25 -0
- package/dist/components/composer-submit-bridge.js +35 -1
- package/dist/components/context-bar.d.ts +9 -0
- package/dist/components/context-bar.js +32 -19
- package/dist/components/data-table/data-table-selection-bar.d.ts +2 -1
- package/dist/components/data-table/data-table-selection-bar.js +21 -1
- package/dist/components/data-table/data-table-toolbar.js +29 -3
- package/dist/components/global-selection-popover.js +1 -1
- package/dist/components/theme-runtime-provider.js +144 -2
- package/dist/components/tool-call-card.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/lib/auth-provider.js +6 -4
- package/dist/lib/composer-submit-store.d.ts +10 -1
- package/dist/lib/composer-submit-store.js +7 -4
- package/dist/lib/voice-adapter-registry.d.ts +49 -0
- package/dist/lib/voice-adapter-registry.js +51 -0
- package/dist/styles.css +21 -0
- package/package.json +3 -3
|
@@ -5,6 +5,7 @@ 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
|
+
import { useVoiceAdapterStore } from "../lib/voice-adapter-registry.js";
|
|
8
9
|
import { ShowDocumentToolUI } from "./show-document-tool-ui.js";
|
|
9
10
|
import { ClientToolExecutor } from "./client-tool-executor.js";
|
|
10
11
|
const AGUIAdapterContext = createContext(null);
|
|
@@ -27,7 +28,7 @@ export function AGUIRuntimeProvider(props) {
|
|
|
27
28
|
const [resetKey, setResetKey] = useState(0);
|
|
28
29
|
const resetThread = useCallback(() => setResetKey((p) => p + 1), []);
|
|
29
30
|
const innerKey = `${props.threadId ?? "fresh"}:${resetKey}`;
|
|
30
|
-
return
|
|
31
|
+
return _jsx(AGUIRuntimeInner, { ...props, resetThread: resetThread }, innerKey);
|
|
31
32
|
}
|
|
32
33
|
function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapterFactory, resetThread, }) {
|
|
33
34
|
const adapter = useMemo(() => new AGUIAdapterSDK("/api/agent",
|
|
@@ -61,6 +62,13 @@ function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapte
|
|
|
61
62
|
// factory + urlMatch here.
|
|
62
63
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
64
|
}, [historyAdapterFactory, urlMatch]);
|
|
65
|
+
// Voice is opt-in and host-supplied (see ``registerVoiceAdapter``).
|
|
66
|
+
// Both slots stay absent unless a module registered one, so a runtime
|
|
67
|
+
// without voice is byte-for-byte the runtime that existed before:
|
|
68
|
+
// assistant-ui reports the capability as unavailable and every voice
|
|
69
|
+
// affordance hides itself.
|
|
70
|
+
const voiceAdapter = useVoiceAdapterStore((s) => s.voice);
|
|
71
|
+
const speechAdapter = useVoiceAdapterStore((s) => s.speech);
|
|
64
72
|
const runtime = useLocalRuntime(adapter, {
|
|
65
73
|
adapters: {
|
|
66
74
|
attachments: attachmentAdapter,
|
|
@@ -69,6 +77,8 @@ function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapte
|
|
|
69
77
|
// assistant-ui calls .load() on mount to seed past messages and
|
|
70
78
|
// .append() after every turn.
|
|
71
79
|
...(historyAdapter ? { history: historyAdapter } : {}),
|
|
80
|
+
...(voiceAdapter ? { voice: voiceAdapter } : {}),
|
|
81
|
+
...(speechAdapter ? { speech: speechAdapter } : {}),
|
|
72
82
|
},
|
|
73
83
|
});
|
|
74
84
|
return (_jsx(AGUIAdapterContext.Provider, { value: { adapter, resetThread }, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsxs(TooltipProvider, { children: [_jsx(ShowDocumentToolUI, {}), _jsx(ClientToolExecutor, {}), children] }) }) }));
|
|
@@ -23,9 +23,19 @@ export interface ChatContentProps {
|
|
|
23
23
|
* a bare composer opts OUT.
|
|
24
24
|
*/
|
|
25
25
|
showPinButton?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Composer placeholder. Defaults to `DEFAULT_COMPOSER_PLACEHOLDER`.
|
|
28
|
+
*/
|
|
29
|
+
placeholder?: string;
|
|
26
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* "Message Assistant..." named the machine and told the user nothing. The
|
|
33
|
+
* invitation is what belongs here: it says the box takes anything, which is
|
|
34
|
+
* the one thing a first-time user does not know.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEFAULT_COMPOSER_PLACEHOLDER = "Ask anything";
|
|
27
37
|
/**
|
|
28
38
|
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
29
39
|
* Renders inside a ThreadPrimitive.Root context.
|
|
30
40
|
*/
|
|
31
|
-
export declare function ChatContent({ starterSuggestions, showPinButton, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare function ChatContent({ starterSuggestions, showPinButton, placeholder, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
3
3
|
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive, useAui, useAuiState, useThreadViewport, } from "@assistant-ui/react";
|
|
4
|
-
import { ArrowUp, Copy, Check, RefreshCw, PencilIcon, ChevronLeft, ChevronRight, ChevronDown, Square, Bot,
|
|
4
|
+
import { ArrowUp, Copy, Check, RefreshCw, PencilIcon, ChevronLeft, ChevronRight, ChevronDown, Square, Bot, Minimize2, PanelRightClose, PanelRightOpen, } from "lucide-react";
|
|
5
5
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
6
|
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
7
7
|
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
8
8
|
import { userAttachmentComponents, composerAttachmentComponents } from "./chat-attachments.js";
|
|
9
9
|
import { SentContextBadges, ComposerContextBadges } from "./context-badges.js";
|
|
10
|
+
import { ComposerAddMenu } from "./composer-add-menu.js";
|
|
10
11
|
import { ContextPins } from "./context-bar.js";
|
|
11
12
|
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
12
|
-
import { ChatComposerBannerSlot } from "./chat-slots.js";
|
|
13
|
+
import { ChatComposerActionsSlot, ChatComposerBannerSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
|
|
13
14
|
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
14
15
|
import { ReasoningEffortPicker } from "./reasoning-effort-picker.js";
|
|
15
16
|
import { ChatMessageParts } from "./chat-message-parts.js";
|
|
@@ -34,7 +35,10 @@ const AssistantMessage = () => {
|
|
|
34
35
|
const AssistantActionBar = () => (_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 -ml-2 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: [_jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-4" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-4 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Refresh", size: "icon", children: _jsx(RefreshCw, { className: "size-4" }) }) })] }));
|
|
35
36
|
const EditComposer = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2", children: _jsx("div", { className: "flex items-start justify-end", children: _jsx("div", { className: "w-full max-w-[80%]", children: _jsxs(ComposerPrimitive.Root, { className: cn("flex flex-col gap-2", "rounded-2xl border border-input", "bg-background shadow-sm", "px-4 py-3"), children: [_jsx(ComposerPrimitive.Input, { autoFocus: true, className: cn("w-full bg-transparent", "text-base text-foreground", "outline-none resize-none", "placeholder:text-muted-foreground") }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", children: "Cancel" }) }), _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "sm", children: "Update" }) })] })] }) }) }) }));
|
|
36
37
|
const ComposerActionButton = () => {
|
|
37
|
-
|
|
38
|
+
// With nothing typed, a module-supplied control (a voice button) may hold
|
|
39
|
+
// the send corner instead of a Send that has nothing to send.
|
|
40
|
+
const actionsOwnSend = useComposerActionsOwnSend();
|
|
41
|
+
return (_jsxs(_Fragment, { children: [_jsx(ThreadPrimitive.If, { running: false, children: _jsx(ReasoningEffortPicker, {}) }), _jsx(ChatComposerActionsSlot, {}), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Stop generating", children: _jsx(Square, { className: "size-4" }) }) }) }), !actionsOwnSend && (_jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Send message", children: _jsx(ArrowUp, { className: "size-5" }) }) }) }))] }));
|
|
38
42
|
};
|
|
39
43
|
/**
|
|
40
44
|
* Default starter prompts shown on empty state. Override per-app by passing
|
|
@@ -111,22 +115,33 @@ const DropZone = ({ children }) => {
|
|
|
111
115
|
}, [aui, showDropError]);
|
|
112
116
|
return (_jsxs("div", { className: "relative flex flex-col flex-1 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 }))] }));
|
|
113
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* "Message Assistant..." named the machine and told the user nothing. The
|
|
120
|
+
* invitation is what belongs here: it says the box takes anything, which is
|
|
121
|
+
* the one thing a first-time user does not know.
|
|
122
|
+
*/
|
|
123
|
+
export const DEFAULT_COMPOSER_PLACEHOLDER = "Ask anything";
|
|
114
124
|
/**
|
|
115
125
|
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
116
126
|
* Renders inside a ThreadPrimitive.Root context.
|
|
117
127
|
*/
|
|
118
|
-
export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = true, } = {}) {
|
|
128
|
+
export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = true, placeholder = DEFAULT_COMPOSER_PLACEHOLDER, } = {}) {
|
|
129
|
+
const ThreadSurface = useChatThreadSurface();
|
|
119
130
|
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
120
131
|
const collapseChat = useChatBubbleStore((s) => s.collapse);
|
|
121
132
|
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
122
133
|
const togglePagePanel = useChatBubbleStore((s) => s.togglePagePanel);
|
|
123
|
-
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [
|
|
134
|
+
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [ThreadSurface && (
|
|
135
|
+
// `overflow-hidden`, not scroll: a surface owns its own layout,
|
|
136
|
+
// and a full-bleed one (an avatar cropped to the frame) must be
|
|
137
|
+
// able to reach the edges rather than sit in a scroll box.
|
|
138
|
+
_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 bg-background", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
124
139
|
UserMessage,
|
|
125
140
|
EditComposer,
|
|
126
141
|
AssistantMessage,
|
|
127
|
-
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) })] }), _jsxs("div", { className: "shrink-0 bg-background", children: [_jsx(ChatComposerBannerSlot, {}), _jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }), _jsx("div", { className: "mx-auto max-w-3xl px-4 py-4", children: _jsxs(ComposerPrimitive.Root, { className: cn(
|
|
142
|
+
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), !ThreadSurface && (_jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) }))] }), _jsxs("div", { className: "shrink-0 bg-background", children: [_jsx(ChatComposerBannerSlot, {}), _jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }), _jsx("div", { className: "mx-auto max-w-3xl px-4 py-4", children: _jsxs(ComposerPrimitive.Root, { className: cn(
|
|
128
143
|
// Query container for the controls inside, so they size
|
|
129
144
|
// against the composer rather than the viewport. This one
|
|
130
145
|
// is wide, so the effort picker keeps its full label.
|
|
131
|
-
"@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(ComposerContextBadges, {}), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }),
|
|
146
|
+
"@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(ComposerContextBadges, {}), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 @md:flex-nowrap", children: [_jsxs("div", { className: "order-2 flex shrink-0 items-center gap-1 @md:order-1", children: [_jsx(ComposerAddMenu, { showPin: showPinButton }), _jsx(ContextPins, {})] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: placeholder, rows: 1, className: cn("order-1 w-full bg-transparent", "py-2 text-base", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-32", "@md:order-2 @md:w-auto @md:flex-1") }), _jsx("div", { className: "order-3 ml-auto flex shrink-0 items-center gap-2 @md:ml-0", children: _jsx(ComposerActionButton, {}) })] })] }) })] })] })] }));
|
|
132
147
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useState } from "react";
|
|
3
3
|
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, useAuiState, useThreadViewport, } from "@assistant-ui/react";
|
|
4
|
-
import { Sparkles, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check,
|
|
4
|
+
import { Sparkles, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, RefreshCw, } from "lucide-react";
|
|
5
5
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
6
|
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
7
7
|
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
8
8
|
import { ContextPins } from "./context-bar.js";
|
|
9
9
|
import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
|
|
10
10
|
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
11
|
-
import {
|
|
11
|
+
import { ComposerAddMenu } from "./composer-add-menu.js";
|
|
12
|
+
import { DEFAULT_COMPOSER_PLACEHOLDER } from "./assistant-chat.js";
|
|
13
|
+
import { ChatComposerActionsSlot, ChatComposerBannerSlot, ChatLauncherBadgeSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
|
|
12
14
|
import { ChatMessageParts } from "./chat-message-parts.js";
|
|
13
15
|
import { ReasoningEffortPicker } from "./reasoning-effort-picker.js";
|
|
14
16
|
import { LoadingIndicator } from "./loading-indicator.js";
|
|
@@ -34,6 +36,7 @@ function BubbleScrollToBottomButton() {
|
|
|
34
36
|
return (_jsx(Button, { variant: "outline", size: "icon", className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-10 rounded-full shadow-md size-7", onClick: () => scrollToBottom({ behavior: "smooth" }), children: _jsx(ChevronDown, { className: "size-3" }) }));
|
|
35
37
|
}
|
|
36
38
|
export function ChatBubble() {
|
|
39
|
+
const ThreadSurface = useChatThreadSurface();
|
|
37
40
|
const isOpen = useChatBubbleStore((s) => s.isOpen);
|
|
38
41
|
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
39
42
|
const openBubble = useChatBubbleStore((s) => s.open);
|
|
@@ -45,6 +48,8 @@ export function ChatBubble() {
|
|
|
45
48
|
const pageLabel = useAppStore((s) => s.navContext.label);
|
|
46
49
|
const currentPage = useAppStore((s) => s.currentPage);
|
|
47
50
|
const threadActive = useAppStore((s) => s.threadActive);
|
|
51
|
+
// With nothing typed, a module-supplied control may hold the send corner.
|
|
52
|
+
const actionsOwnSend = useComposerActionsOwnSend();
|
|
48
53
|
// Draggable position: null = use default CSS anchored with equal left/bottom inset.
|
|
49
54
|
const [dragPos, setDragPos] = useState(null);
|
|
50
55
|
const dragStart = useRef(null);
|
|
@@ -93,17 +98,17 @@ export function ChatBubble() {
|
|
|
93
98
|
// Hide bubble on chat page (already full-screen) and in expanded mode
|
|
94
99
|
if (currentPage === "/" || isExpanded)
|
|
95
100
|
return null;
|
|
96
|
-
return (_jsxs(_Fragment, { children: [!isOpen && (_jsx("div", { "aria-hidden": "true", className: cn("pointer-events-none fixed bottom-0 left-0 z-40 hidden md:block", "border-r border-sidebar-border", "bg-gradient-to-b from-sidebar/0 via-sidebar/88 to-sidebar/96", "supports-[backdrop-filter]:backdrop-blur"), style: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
return (_jsxs(_Fragment, { children: [_jsx(ComposerSubmitBridge, {}), _jsxs(_Fragment, { children: [!isOpen && (_jsx("div", { "aria-hidden": "true", className: cn("pointer-events-none fixed bottom-0 left-0 z-40 hidden md:block", "border-r border-sidebar-border", "bg-gradient-to-b from-sidebar/0 via-sidebar/88 to-sidebar/96", "supports-[backdrop-filter]:backdrop-blur"), style: {
|
|
102
|
+
width: isSidebarOpen ? sidebarWidth : RAIL_WIDTH,
|
|
103
|
+
height: 64,
|
|
104
|
+
} })), !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
|
|
105
|
+
? { left: dragPos.x, top: dragPos.y }
|
|
106
|
+
: { 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 thread" : "New conversation" })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(TooltipIconButton, { tooltip: "New Thread", 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: _jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Ask the agent about this page" }) }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "px-4 pt-4 pb-3", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
107
|
+
UserMessage: BubbleUserMessage,
|
|
108
|
+
AssistantMessage: BubbleAssistantMessage,
|
|
109
|
+
} }), _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", children: [_jsx(ChatComposerBannerSlot, {}), _jsx("div", { className: "px-3 py-3", children: _jsxs(ComposerPrimitive.Root, { className: cn(
|
|
110
|
+
// Query container for the controls inside: the bubble is
|
|
111
|
+
// a narrow panel in a wide window, so they have to size
|
|
112
|
+
// against THIS box, not the viewport.
|
|
113
|
+
"@container", "flex flex-col gap-1", "rounded-2xl border border-input", "bg-background shadow-sm", "px-3 py-2", "focus-within:border-primary/50", "transition-all duration-200"), children: [_jsx(ComposerContextBadges, {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [_jsx(ComposerAddMenu, { compact: true }), _jsx(ContextPins, { compact: true })] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: DEFAULT_COMPOSER_PLACEHOLDER, rows: 1, className: cn("flex-1 bg-transparent", "py-1 text-sm", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-20") }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ReasoningEffortPicker, {}) }), _jsx(ChatComposerActionsSlot, {}), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Stop", children: _jsx(Square, { className: "size-3" }) }) }) }), !actionsOwnSend && (_jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Send", children: _jsx(ArrowUp, { className: "size-4" }) }) }) }))] })] }) })] })] })] }))] })] }));
|
|
109
114
|
}
|
|
@@ -18,11 +18,35 @@
|
|
|
18
18
|
* wrapper (a positioned ``group`` container around the round button),
|
|
19
19
|
* so a badge can position itself absolutely over the button and use
|
|
20
20
|
* ``group-hover:`` styles for a hover flyout.
|
|
21
|
+
* - ``composerActions`` — in the composer's trailing control row, just
|
|
22
|
+
* before Send, in BOTH the full chat and the floating bubble panel.
|
|
23
|
+
* For controls that are an alternative *way to send* rather than
|
|
24
|
+
* content about the thread — a push-to-talk mic, say. Leading-edge
|
|
25
|
+
* controls (attach, context pins) stay owned by the composer itself.
|
|
26
|
+
*
|
|
27
|
+
* - ``threadSurface`` — rendered *instead of* the message viewport, for a
|
|
28
|
+
* mode that replaces the conversation rather than adding to it. A live
|
|
29
|
+
* voice call is the case it exists for: while you are talking, the
|
|
30
|
+
* transcript scrolling past is not what you are looking at. Unlike the
|
|
31
|
+
* other slots this one is set and cleared at runtime — registering a
|
|
32
|
+
* component IS the takeover, clearing it hands the thread back.
|
|
33
|
+
*
|
|
34
|
+
* A module that registers ``composerActions`` may also claim the Send
|
|
35
|
+
* position while the composer is empty (``composerActionsReplaceSend``).
|
|
36
|
+
* That is the shape a voice control wants: nothing has been typed, so Send
|
|
37
|
+
* has nothing to do, and the corner belongs to the other way of asking.
|
|
38
|
+
* Type one character and Send comes back. Opt-in, because a slot holding
|
|
39
|
+
* something unrelated must not make Send disappear.
|
|
21
40
|
*/
|
|
22
41
|
import type { ComponentType } from "react";
|
|
23
42
|
interface ChatSlotsState {
|
|
24
43
|
composerBanner: ComponentType | null;
|
|
25
44
|
launcherBadge: ComponentType | null;
|
|
45
|
+
composerActions: ComponentType | null;
|
|
46
|
+
/** Let ``composerActions`` stand in for Send while the composer is empty. */
|
|
47
|
+
composerActionsReplaceSend: boolean;
|
|
48
|
+
/** Replaces the message viewport entirely while set. */
|
|
49
|
+
threadSurface: ComponentType | null;
|
|
26
50
|
}
|
|
27
51
|
export declare const useChatSlotsStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ChatSlotsState>>;
|
|
28
52
|
/**
|
|
@@ -34,4 +58,15 @@ export declare const useChatSlotsStore: import("zustand").UseBoundStore<import("
|
|
|
34
58
|
export declare function registerChatSlots(slots: Partial<ChatSlotsState>): void;
|
|
35
59
|
export declare function ChatComposerBannerSlot(): import("react/jsx-runtime").JSX.Element | null;
|
|
36
60
|
export declare function ChatLauncherBadgeSlot(): import("react/jsx-runtime").JSX.Element | null;
|
|
61
|
+
export declare function ChatComposerActionsSlot(): import("react/jsx-runtime").JSX.Element | null;
|
|
62
|
+
/**
|
|
63
|
+
* True when Send should stand aside for the ``composerActions`` slot: a
|
|
64
|
+
* module claimed the position and there is nothing to send.
|
|
65
|
+
*/
|
|
66
|
+
export declare function useComposerActionsOwnSend(): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The module-supplied surface that replaces the message viewport, or null
|
|
69
|
+
* when the thread should render normally.
|
|
70
|
+
*/
|
|
71
|
+
export declare function useChatThreadSurface(): ComponentType | null;
|
|
37
72
|
export {};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { create } from "zustand";
|
|
3
|
+
import { useAuiState } from "@assistant-ui/react";
|
|
3
4
|
export const useChatSlotsStore = create(() => ({
|
|
4
5
|
composerBanner: null,
|
|
5
6
|
launcherBadge: null,
|
|
7
|
+
composerActions: null,
|
|
8
|
+
composerActionsReplaceSend: false,
|
|
9
|
+
threadSurface: null,
|
|
6
10
|
}));
|
|
7
11
|
/**
|
|
8
12
|
* Register chat slot components. Later calls overwrite only the keys
|
|
@@ -16,6 +20,13 @@ export function registerChatSlots(slots) {
|
|
|
16
20
|
next.composerBanner = slots.composerBanner;
|
|
17
21
|
if (slots.launcherBadge !== undefined)
|
|
18
22
|
next.launcherBadge = slots.launcherBadge;
|
|
23
|
+
if (slots.composerActions !== undefined)
|
|
24
|
+
next.composerActions = slots.composerActions;
|
|
25
|
+
if (slots.composerActionsReplaceSend !== undefined) {
|
|
26
|
+
next.composerActionsReplaceSend = slots.composerActionsReplaceSend;
|
|
27
|
+
}
|
|
28
|
+
if (slots.threadSurface !== undefined)
|
|
29
|
+
next.threadSurface = slots.threadSurface;
|
|
19
30
|
useChatSlotsStore.setState(next);
|
|
20
31
|
}
|
|
21
32
|
export function ChatComposerBannerSlot() {
|
|
@@ -26,3 +37,23 @@ export function ChatLauncherBadgeSlot() {
|
|
|
26
37
|
const Badge = useChatSlotsStore((s) => s.launcherBadge);
|
|
27
38
|
return Badge ? _jsx(Badge, {}) : null;
|
|
28
39
|
}
|
|
40
|
+
export function ChatComposerActionsSlot() {
|
|
41
|
+
const Actions = useChatSlotsStore((s) => s.composerActions);
|
|
42
|
+
return Actions ? _jsx(Actions, {}) : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* True when Send should stand aside for the ``composerActions`` slot: a
|
|
46
|
+
* module claimed the position and there is nothing to send.
|
|
47
|
+
*/
|
|
48
|
+
export function useComposerActionsOwnSend() {
|
|
49
|
+
const claimed = useChatSlotsStore((s) => s.composerActionsReplaceSend && !!s.composerActions);
|
|
50
|
+
const isEmpty = useAuiState((s) => s.optional.composer?.isEmpty ?? true);
|
|
51
|
+
return claimed && isEmpty;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The module-supplied surface that replaces the message viewport, or null
|
|
55
|
+
* when the thread should render normally.
|
|
56
|
+
*/
|
|
57
|
+
export function useChatThreadSurface() {
|
|
58
|
+
return useChatSlotsStore((s) => s.threadSurface);
|
|
59
|
+
}
|
|
@@ -2,5 +2,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { SearchX } from "lucide-react";
|
|
3
3
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
4
4
|
export function CollectionEmptyState({ icon: Icon = SearchX, title, description, action, className, }) {
|
|
5
|
-
return (_jsxs("div", { className: cn("flex min-h-[18rem] flex-col items-center justify-center rounded-[1.75rem] bg-muted/18 px-6 py-10 text-center ring-1 ring-border/35", className), children: [Icon ? (_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-background/85 text-primary shadow-[0_8px_24px_-18px_rgb(15_23_42/0.35)]", children: _jsx(Icon, { className: "size-6" }) })) : null, _jsx("div", { className: "text-base font-semibold text-foreground", children: title }), description ? _jsx("p", { className: "mt-2 max-w-xl text-sm leading-6 text-muted-foreground", children: description }) : null, action ? _jsx("div", { className: "mt-5", children: action }) : null] }));
|
|
5
|
+
return (_jsxs("div", { className: cn("flex min-h-[18rem] flex-col items-center justify-center rounded-[1.75rem] bg-muted/18 px-6 py-10 text-center ring-1 ring-border/35", className), children: [Icon ? (_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-background/85 text-primary shadow-[0_8px_24px_-18px_rgb(15_23_42/0.35)]", children: _jsx(Icon, { className: "size-6" }) })) : null, _jsx("div", { className: "text-base font-semibold text-foreground", children: title }), description ? (_jsx("p", { className: "mt-2 max-w-xl text-sm leading-6 text-muted-foreground", children: description })) : null, action ? _jsx("div", { className: "mt-5", children: action }) : null] }));
|
|
6
6
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composer's leading "+" — one affordance for everything you can add to
|
|
3
|
+
* a turn.
|
|
4
|
+
*
|
|
5
|
+
* It replaces a row of single-purpose icons (paperclip, pin, and whatever
|
|
6
|
+
* came next). That row grows every time the product learns a new trick, and
|
|
7
|
+
* each icon costs the user a guess about what it does; a labelled menu costs
|
|
8
|
+
* one click and reads itself. Pinning in particular is a power feature that
|
|
9
|
+
* does not deserve permanent residency next to the text field.
|
|
10
|
+
*
|
|
11
|
+
* Attaching still goes through `ComposerPrimitive.AddAttachment`, so the
|
|
12
|
+
* file picker keeps whatever `accept` filter the registered attachment
|
|
13
|
+
* adapter declares — reimplementing the picker here would drift from it.
|
|
14
|
+
*/
|
|
15
|
+
import type { FC } from "react";
|
|
16
|
+
export interface ComposerAddMenuProps {
|
|
17
|
+
/** Bubble-composer sizing. */
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Offer "Pin this page". A bare composer with no page behind it (or a host
|
|
21
|
+
* that does not use page context) opts out.
|
|
22
|
+
*/
|
|
23
|
+
showPin?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare const ComposerAddMenu: FC<ComposerAddMenuProps>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ComposerPrimitive } from "@assistant-ui/react";
|
|
3
|
+
import { Paperclip, Pin, PinOff, Plus } from "lucide-react";
|
|
4
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
|
|
6
|
+
import { usePagePin } from "./context-bar.js";
|
|
7
|
+
export const ComposerAddMenu = ({ compact = false, showPin = true }) => {
|
|
8
|
+
const { isPinned, toggle } = usePagePin();
|
|
9
|
+
const icon = compact ? "size-3.5" : "size-4";
|
|
10
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": "Add to this message", className: cn("shrink-0 rounded-full", compact ? "size-7" : "size-9"), children: _jsx(Plus, { className: compact ? "size-4" : "size-5" }) }) }), _jsxs(DropdownMenuContent, { align: "start", side: "top", className: "w-52", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsxs(DropdownMenuItem, { children: [_jsx(Paperclip, { className: icon }), "Attach file"] }) }), showPin && (_jsxs(DropdownMenuItem, { onSelect: (event) => {
|
|
11
|
+
// Keep the menu anchored while the pin badge appears, so the
|
|
12
|
+
// user sees the result of what they just clicked.
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
toggle();
|
|
15
|
+
}, children: [isPinned ? _jsx(PinOff, { className: icon }) : _jsx(Pin, { className: icon }), isPinned ? "Unpin this page" : "Pin this page"] }))] })] }));
|
|
16
|
+
};
|
|
@@ -1 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sends text a module submitted on the user's behalf — a spoken turn, most of
|
|
3
|
+
* the time — through the ordinary composer.
|
|
4
|
+
*
|
|
5
|
+
* The composer stays editable while a module is using it, so the user's own
|
|
6
|
+
* draft can be sitting in it when a turn arrives. Sending would take that with
|
|
7
|
+
* it, so the text is saved and put back.
|
|
8
|
+
*
|
|
9
|
+
* **Attachments are NOT preserved**, and that is a decision rather than an
|
|
10
|
+
* oversight. There is no way to send without them — `SendOptions` is
|
|
11
|
+
* `{ startRun, steer }` — so preserving them means lifting them out of the
|
|
12
|
+
* composer, sending, and adding them back. That is asynchronous, through a
|
|
13
|
+
* host-supplied adapter, and it produced four separate defects in short order:
|
|
14
|
+
* a cancellation check that dropped them mid-sequence, two turns interleaving
|
|
15
|
+
* their save/restore, a stalled upload wedging every later turn, and a
|
|
16
|
+
* timed-out operation landing on a later turn instead. Each fix was correct
|
|
17
|
+
* and each one exposed the next.
|
|
18
|
+
*
|
|
19
|
+
* So this stays synchronous, which is what makes it easy to be sure about: no
|
|
20
|
+
* queue, no timeout, no window in which a second turn can interleave. A file
|
|
21
|
+
* attached but not yet sent goes with the spoken turn, exactly as it did
|
|
22
|
+
* before any of this — a real limitation, and a smaller one than the failures
|
|
23
|
+
* that came with trying to remove it. It is worth revisiting if the composer
|
|
24
|
+
* ever grows a way to send without attachments.
|
|
25
|
+
*/
|
|
1
26
|
export declare function ComposerSubmitBridge(): null;
|
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { useAui } from "@assistant-ui/react";
|
|
3
3
|
import { useComposerSubmitStore } from "../lib/composer-submit-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Sends text a module submitted on the user's behalf — a spoken turn, most of
|
|
6
|
+
* the time — through the ordinary composer.
|
|
7
|
+
*
|
|
8
|
+
* The composer stays editable while a module is using it, so the user's own
|
|
9
|
+
* draft can be sitting in it when a turn arrives. Sending would take that with
|
|
10
|
+
* it, so the text is saved and put back.
|
|
11
|
+
*
|
|
12
|
+
* **Attachments are NOT preserved**, and that is a decision rather than an
|
|
13
|
+
* oversight. There is no way to send without them — `SendOptions` is
|
|
14
|
+
* `{ startRun, steer }` — so preserving them means lifting them out of the
|
|
15
|
+
* composer, sending, and adding them back. That is asynchronous, through a
|
|
16
|
+
* host-supplied adapter, and it produced four separate defects in short order:
|
|
17
|
+
* a cancellation check that dropped them mid-sequence, two turns interleaving
|
|
18
|
+
* their save/restore, a stalled upload wedging every later turn, and a
|
|
19
|
+
* timed-out operation landing on a later turn instead. Each fix was correct
|
|
20
|
+
* and each one exposed the next.
|
|
21
|
+
*
|
|
22
|
+
* So this stays synchronous, which is what makes it easy to be sure about: no
|
|
23
|
+
* queue, no timeout, no window in which a second turn can interleave. A file
|
|
24
|
+
* attached but not yet sent goes with the spoken turn, exactly as it did
|
|
25
|
+
* before any of this — a real limitation, and a smaller one than the failures
|
|
26
|
+
* that came with trying to remove it. It is worth revisiting if the composer
|
|
27
|
+
* ever grows a way to send without attachments.
|
|
28
|
+
*/
|
|
4
29
|
export function ComposerSubmitBridge() {
|
|
5
30
|
const aui = useAui();
|
|
6
31
|
const pending = useComposerSubmitStore((s) => s.pending);
|
|
7
32
|
useEffect(() => {
|
|
8
33
|
if (!pending)
|
|
9
34
|
return;
|
|
35
|
+
// Claim first, send second. Several bridges may be mounted at once, and
|
|
36
|
+
// StrictMode runs this effect twice; both would otherwise send the same
|
|
37
|
+
// text before either had cleared it.
|
|
38
|
+
if (!useComposerSubmitStore.getState().claim(pending.id))
|
|
39
|
+
return;
|
|
40
|
+
const draft = aui.composer.getState().text;
|
|
10
41
|
// send() starts the run for the queued text; steering/queueing semantics
|
|
11
42
|
// are handled by the composer client (assistant-ui >= 0.15).
|
|
12
43
|
aui.composer.setText(pending.text);
|
|
13
44
|
aui.composer.send();
|
|
14
|
-
|
|
45
|
+
// send() clears the composer, so this restores the user's own text into an
|
|
46
|
+
// empty box rather than appending it to the turn just sent.
|
|
47
|
+
if (draft)
|
|
48
|
+
aui.composer.setText(draft);
|
|
15
49
|
}, [aui, pending]);
|
|
16
50
|
return null;
|
|
17
51
|
}
|
|
@@ -37,5 +37,14 @@ interface ContextPinsProps {
|
|
|
37
37
|
* @internal Exported for unit tests; not part of the public API.
|
|
38
38
|
*/
|
|
39
39
|
export declare function getNavigablePath(item: ContextItem): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* The "is this page pinned, and how do I flip that" logic, shared by the
|
|
42
|
+
* pin badge here and the composer's add-menu. Two call sites deciding
|
|
43
|
+
* independently what "pinned" means is how they end up disagreeing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function usePagePin(): {
|
|
46
|
+
isPinned: boolean;
|
|
47
|
+
toggle: () => void;
|
|
48
|
+
};
|
|
40
49
|
export declare const ContextPins: FC<ContextPinsProps>;
|
|
41
50
|
export {};
|
|
@@ -57,22 +57,49 @@ export function getNavigablePath(item) {
|
|
|
57
57
|
}
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The "is this page pinned, and how do I flip that" logic, shared by the
|
|
62
|
+
* pin badge here and the composer's add-menu. Two call sites deciding
|
|
63
|
+
* independently what "pinned" means is how they end up disagreeing.
|
|
64
|
+
*/
|
|
65
|
+
export function usePagePin() {
|
|
61
66
|
const items = useAppStore((s) => s.contextItems);
|
|
62
67
|
const removeItem = useAppStore((s) => s.removeContextItem);
|
|
63
68
|
const addContextItem = useAppStore((s) => s.addContextItem);
|
|
64
69
|
const currentPage = useAppStore((s) => s.currentPage);
|
|
65
70
|
const pageLabel = useAppStore((s) => s.navContext.label);
|
|
66
71
|
const navMeta = useAppStore((s) => s.navContext.meta);
|
|
72
|
+
const currentPagePin = items.find((i) => i.persistence === "persistent" &&
|
|
73
|
+
i.type === "page" &&
|
|
74
|
+
i.payload.kind === "page" &&
|
|
75
|
+
i.payload.path === currentPage);
|
|
76
|
+
return {
|
|
77
|
+
isPinned: !!currentPagePin,
|
|
78
|
+
toggle: () => {
|
|
79
|
+
if (currentPagePin) {
|
|
80
|
+
removeItem(currentPagePin.id);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
addContextItem({
|
|
84
|
+
type: "page",
|
|
85
|
+
label: pageLabel,
|
|
86
|
+
payload: { kind: "page", path: currentPage, label: pageLabel, meta: navMeta },
|
|
87
|
+
sourcePage: currentPage,
|
|
88
|
+
persistence: "persistent",
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export const ContextPins = ({ showPinButton = false, compact = false, flyoutDirection = "up", }) => {
|
|
94
|
+
const items = useAppStore((s) => s.contextItems);
|
|
95
|
+
const removeItem = useAppStore((s) => s.removeContextItem);
|
|
96
|
+
const currentPage = useAppStore((s) => s.currentPage);
|
|
97
|
+
const { isPinned: isCurrentPagePinned, toggle: handleTogglePin } = usePagePin();
|
|
67
98
|
const navigate = useNavigate();
|
|
68
99
|
const [flyoutOpen, setFlyoutOpen] = useState(false);
|
|
69
100
|
const containerRef = useRef(null);
|
|
70
101
|
const hoverTimeout = useRef(undefined);
|
|
71
102
|
const pinned = items.filter((i) => i.persistence === "persistent");
|
|
72
|
-
// Check if current page is already pinned
|
|
73
|
-
const isCurrentPagePinned = pinned.some((i) => i.type === "page" && i.payload.kind === "page" && i.payload.path === currentPage);
|
|
74
|
-
// Find the pin item for current page (to unpin)
|
|
75
|
-
const currentPagePin = pinned.find((i) => i.type === "page" && i.payload.kind === "page" && i.payload.path === currentPage);
|
|
76
103
|
// Close flyout on outside click
|
|
77
104
|
useEffect(() => {
|
|
78
105
|
if (!flyoutOpen)
|
|
@@ -85,20 +112,6 @@ export const ContextPins = ({ showPinButton = false, compact = false, flyoutDire
|
|
|
85
112
|
document.addEventListener("mousedown", handleClick);
|
|
86
113
|
return () => document.removeEventListener("mousedown", handleClick);
|
|
87
114
|
}, [flyoutOpen]);
|
|
88
|
-
const handleTogglePin = () => {
|
|
89
|
-
if (isCurrentPagePinned && currentPagePin) {
|
|
90
|
-
removeItem(currentPagePin.id);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
addContextItem({
|
|
94
|
-
type: "page",
|
|
95
|
-
label: pageLabel,
|
|
96
|
-
payload: { kind: "page", path: currentPage, label: pageLabel, meta: navMeta },
|
|
97
|
-
sourcePage: currentPage,
|
|
98
|
-
persistence: "persistent",
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
115
|
const handleMouseEnter = () => {
|
|
103
116
|
clearTimeout(hoverTimeout.current);
|
|
104
117
|
if (pinned.length > 0)
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* habit across the platform (and the same set the chat agent reads).
|
|
6
6
|
*/
|
|
7
7
|
import type { Row, Table } from "@tanstack/react-table";
|
|
8
|
-
import type
|
|
8
|
+
import { type ReactNode } from "react";
|
|
9
|
+
export declare const SelectionBarSingleLine: import("react").Provider<boolean>;
|
|
9
10
|
export interface DataTableSelectionBarProps<T> {
|
|
10
11
|
table: Table<T>;
|
|
11
12
|
/** Bulk actions for the selected rows. */
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
3
4
|
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
/**
|
|
6
|
+
* Whether the bar has a row to itself or shares one.
|
|
7
|
+
*
|
|
8
|
+
* The toolbar's non-stacked layout puts the bar on the SAME line as the
|
|
9
|
+
* search, in a region that scrolls sideways rather than growing — because a
|
|
10
|
+
* toolbar that gains height on the first tick shoves the table down under
|
|
11
|
+
* the cursor mid-click, which is the whole reason the bar moved into the
|
|
12
|
+
* toolbar. A bar that wraps internally defeats that just as thoroughly as
|
|
13
|
+
* one that wraps externally, and its nested actions container is a second
|
|
14
|
+
* place it can happen (review catch).
|
|
15
|
+
*
|
|
16
|
+
* The container knows the constraint and the bar owns the class names, so
|
|
17
|
+
* the policy travels between them instead of the toolbar reaching in with a
|
|
18
|
+
* descendant selector — which would apply to a host's own `selectionBar`
|
|
19
|
+
* node too, and lose to it on specificity besides.
|
|
20
|
+
*/
|
|
21
|
+
const SelectionBarSingleLineContext = createContext(false);
|
|
22
|
+
export const SelectionBarSingleLine = SelectionBarSingleLineContext.Provider;
|
|
4
23
|
export function DataTableSelectionBar({ table, children, label = "row", plural, className, }) {
|
|
5
24
|
// Every selected row, not only the ones the current filter shows: a
|
|
6
25
|
// selection the user cannot see is still a selection, and hiding the bar
|
|
7
26
|
// would take away the only way to clear it.
|
|
8
27
|
const rows = table.getSelectedRowModel().rows;
|
|
28
|
+
const singleLine = useContext(SelectionBarSingleLineContext);
|
|
9
29
|
if (rows.length === 0)
|
|
10
30
|
return null;
|
|
11
|
-
return (_jsxs("div", { "data-selection-bar": "", role: "status", className: cn("flex min-h-8
|
|
31
|
+
return (_jsxs("div", { "data-selection-bar": "", role: "status", className: cn("flex min-h-8 items-center gap-2 rounded-xl bg-primary/5 px-3 text-sm", singleLine ? "flex-nowrap whitespace-nowrap" : "flex-wrap", className), children: [_jsxs("span", { className: "font-medium", children: [rows.length, " ", rows.length === 1 ? label : (plural ?? `${label}s`), " selected"] }), _jsx("div", { "data-selection-actions": "", className: cn("flex items-center gap-2", singleLine ? "flex-nowrap" : "flex-wrap"), children: typeof children === "function" ? children(rows) : children }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm",
|
|
12
32
|
// `true` = the blank state, not the caller's `initialState`, which
|
|
13
33
|
// would make "Clear" resurrect rows.
|
|
14
34
|
onClick: () => table.resetRowSelection(true), className: "ml-auto h-8 rounded-lg px-2.5", children: ["Clear", _jsx(X, { className: "size-4" })] })] }));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Maximize2, Minimize2, Plus, Search, SlidersHorizontal, X } from "lucide-react";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, Input, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
5
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
|
|
6
6
|
import { DataTableFacetedFilter } from "./data-table-faceted-filter.js";
|
|
7
|
+
import { SelectionBarSingleLine } from "./data-table-selection-bar.js";
|
|
7
8
|
import { useDataTableFrame } from "./data-table-frame.js";
|
|
8
9
|
import { DataTableViewOptions } from "./data-table-view-options.js";
|
|
9
10
|
import { columnLabel, normalizeFacetValue } from "./facets.js";
|
|
@@ -75,7 +76,32 @@ export function DataTableToolbar({ table, facets = [], layout = "auto", collapsi
|
|
|
75
76
|
const filtersToggle = canCollapse ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", "data-filters-toggle": "", "aria-expanded": showFilters, onClick: () => setFiltersOpen((current) => !current), className: cn("h-8 rounded-xl border-border/45 bg-background/65 shadow-none hover:bg-muted/26", activeFilters > 0 && "border-primary/30"), children: [_jsx(SlidersHorizontal, { className: "size-4" }), "Filters", activeFilters > 0 ? (_jsxs("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal tabular-nums", children: [_jsx("span", { "aria-hidden": "true", children: activeFilters }), _jsxs("span", { className: "sr-only", children: [activeFilters, " active"] })] })) : null] })) : null;
|
|
76
77
|
const rightGroup = actions || viewOptions || focusButton || filtersToggle ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [filtersToggle, actions, viewOptions ? _jsx(DataTableViewOptions, { table: table }) : null, focusButton] })) : null;
|
|
77
78
|
if (!stacked) {
|
|
78
|
-
return (
|
|
79
|
+
return (
|
|
80
|
+
// A stable hook for tests and hosts, rather than leaving them to
|
|
81
|
+
// match on Tailwind classes — a selector that silently stops
|
|
82
|
+
// matching is how an assertion starts passing for the wrong reason
|
|
83
|
+
// (review catch).
|
|
84
|
+
_jsx("div", { "data-slot": "data-table-toolbar", className: cn("flex flex-col gap-2", className), children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsxs("div", { "data-toolbar-lead": showSelectionBar ? "selection" : "filters", className: cn("flex min-w-0 flex-1 items-center gap-2",
|
|
85
|
+
// Facets genuinely want rows: six of them in a narrow pane
|
|
86
|
+
// should stack rather than scroll off the side. A selection
|
|
87
|
+
// bar is the opposite — one row by definition. Left wrapping,
|
|
88
|
+
// it drops below the search as soon as the search alone is
|
|
89
|
+
// wider than the space, which grows the toolbar again and is
|
|
90
|
+
// the jump this whole change exists to remove (review catch).
|
|
91
|
+
showSelectionBar ? "flex-nowrap" : "flex-wrap"), children: [searchNode, showSelectionBar ? (
|
|
92
|
+
// The bar shares this row with a search that has a width
|
|
93
|
+
// floor and a right group that does not shrink, so in a
|
|
94
|
+
// narrow pane it gets squeezed. It scrolls rather than
|
|
95
|
+
// wrapping inside itself — the row stays exactly one high.
|
|
96
|
+
//
|
|
97
|
+
// And the scrollbar itself stays out of layout. A classic,
|
|
98
|
+
// space-consuming one — Windows and Linux by default, macOS
|
|
99
|
+
// set to "always show" — appears only once the bar overflows,
|
|
100
|
+
// which is only once something is ticked, so its height lands
|
|
101
|
+
// on the toolbar as the same jump by another route (review
|
|
102
|
+
// catch). `[data-selection-row]` in `styles.css` keeps it out
|
|
103
|
+
// of layout; hiding the widget does not hide the scrolling.
|
|
104
|
+
_jsx("div", { "data-selection-row": "", className: "min-w-0 flex-1 overflow-x-auto", children: _jsx(SelectionBarSingleLine, { value: true, children: selectionBar }) })) : (_jsxs(_Fragment, { children: [facetNodes, addFilterNode, children, resetNode] }))] }), rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }) }));
|
|
79
105
|
}
|
|
80
|
-
return (_jsxs("div", { className: cn("flex flex-col gap-2", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [searchNode, rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }), showSelectionBar ? (_jsx("div", { "data-selection-row": "", children: selectionBar })) : showFilters ? (_jsxs("div", { "data-filter-row": "", className: "flex flex-wrap items-center gap-2", children: [facetNodes, addFilterNode, children, resetNode] })) : null] }));
|
|
106
|
+
return (_jsxs("div", { "data-slot": "data-table-toolbar", className: cn("flex flex-col gap-2", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [searchNode, rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }), showSelectionBar ? (_jsx("div", { "data-selection-row": "", children: selectionBar })) : showFilters ? (_jsxs("div", { "data-filter-row": "", className: "flex flex-wrap items-center gap-2", children: [facetNodes, addFilterNode, children, resetNode] })) : null] }));
|
|
81
107
|
}
|
|
@@ -11,7 +11,7 @@ import { cn } from "@iloveagents/foundry-web-primitives";
|
|
|
11
11
|
import { useAppStore } from "../lib/app-store.js";
|
|
12
12
|
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
13
13
|
import { submitComposerText } from "../lib/composer-submit-store.js";
|
|
14
|
-
import { resolveSelectionContext
|
|
14
|
+
import { resolveSelectionContext } from "../lib/selection-context.js";
|
|
15
15
|
const ACTION_POPOVER_HEIGHT = 38;
|
|
16
16
|
const SIMPLE_POPOVER_WIDTH = 126;
|
|
17
17
|
const ACTION_POPOVER_WIDTH = 236;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { resolveThemeRuntime, } from "../lib/theme-runtime.js";
|
|
4
4
|
const ThemeRuntimeContext = createContext(null);
|
|
5
|
+
/** True inside a ThemeScope — the outermost one owns the document. */
|
|
6
|
+
const ThemeScopeNestedContext = createContext(false);
|
|
5
7
|
export function ThemeRuntimeProvider({ layers, mode, children, }) {
|
|
6
8
|
const fallback = useRef(resolveThemeRuntime([], "system"));
|
|
7
9
|
const [systemPrefersDark, setSystemPrefersDark] = useState(() => typeof window !== "undefined"
|
|
@@ -33,9 +35,149 @@ export function ThemeRuntimeProvider({ layers, mode, children, }) {
|
|
|
33
35
|
export function useThemeRuntime() {
|
|
34
36
|
return useContext(ThemeRuntimeContext) ?? resolveThemeRuntime([], "system");
|
|
35
37
|
}
|
|
38
|
+
const rootThemeStack = [];
|
|
39
|
+
const rootThemeBase = new Map();
|
|
40
|
+
// What this module last put on the element, per name. Anything else there
|
|
41
|
+
// now came from outside, and outside wins — see `applyTopRootTheme`. Value
|
|
42
|
+
// AND priority: a host re-declaring the same value as `!important` has said
|
|
43
|
+
// something new about that name, and comparing values alone would read it as
|
|
44
|
+
// our own write and quietly drop the flag (review catch).
|
|
45
|
+
const rootThemeWritten = new Map();
|
|
46
|
+
function currentlyOurs(root, name) {
|
|
47
|
+
const written = rootThemeWritten.get(name);
|
|
48
|
+
if (written === undefined)
|
|
49
|
+
return true;
|
|
50
|
+
return (root.style.getPropertyValue(name) === written.value
|
|
51
|
+
&& root.style.getPropertyPriority(name) === written.priority);
|
|
52
|
+
}
|
|
53
|
+
function rememberWrite(root, name) {
|
|
54
|
+
// Read back rather than storing what we passed: the browser normalises the
|
|
55
|
+
// value, and a comparison against the un-normalised string would read
|
|
56
|
+
// every one of our own writes as somebody else's.
|
|
57
|
+
rootThemeWritten.set(name, {
|
|
58
|
+
value: root.style.getPropertyValue(name),
|
|
59
|
+
priority: root.style.getPropertyPriority(name),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function applyTopRootTheme(root) {
|
|
63
|
+
const top = rootThemeStack[rootThemeStack.length - 1];
|
|
64
|
+
for (const [name, base] of rootThemeBase) {
|
|
65
|
+
if (!currentlyOurs(root, name)) {
|
|
66
|
+
// Someone outside this module wrote this name after we did — a host
|
|
67
|
+
// theme manager, a design-mode preview, a test. Their write is newer
|
|
68
|
+
// than anything we have to say about it, so we neither overwrite it
|
|
69
|
+
// now nor restore over it later: the name is theirs from here
|
|
70
|
+
// (review catch).
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const next = top?.vars[name];
|
|
74
|
+
if (next !== undefined) {
|
|
75
|
+
root.style.setProperty(name, String(next));
|
|
76
|
+
}
|
|
77
|
+
else if (base.value) {
|
|
78
|
+
// Value AND priority: a host token declared `!important` keeps its
|
|
79
|
+
// flag, or anything else can start overriding a theme it owns.
|
|
80
|
+
root.style.setProperty(name, base.value, base.priority);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
root.style.removeProperty(name);
|
|
84
|
+
rootThemeWritten.delete(name);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
rememberWrite(root, name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Publish `vars` on the document for `id`, claiming a place in the stack the
|
|
92
|
+
* first time and KEEPING it afterwards.
|
|
93
|
+
*
|
|
94
|
+
* Keeping it is the point: a scope whose theme merely changes — a rerender
|
|
95
|
+
* that hands back a new `variables` object, even a semantically identical
|
|
96
|
+
* one from an inline `layers` array — must not jump over a scope that
|
|
97
|
+
* mounted after it. Re-registering on every dependency change would make
|
|
98
|
+
* the newest write win instead of the newest owner (review catch).
|
|
99
|
+
*/
|
|
100
|
+
function writeRootTheme(root, id, vars) {
|
|
101
|
+
for (const name of Object.keys(vars)) {
|
|
102
|
+
if (!rootThemeBase.has(name)) {
|
|
103
|
+
rootThemeBase.set(name, {
|
|
104
|
+
value: root.style.getPropertyValue(name),
|
|
105
|
+
priority: root.style.getPropertyPriority(name),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const existing = rootThemeStack.find((write) => write.id === id);
|
|
110
|
+
if (existing)
|
|
111
|
+
existing.vars = vars;
|
|
112
|
+
else
|
|
113
|
+
rootThemeStack.push({ id, vars });
|
|
114
|
+
applyTopRootTheme(root);
|
|
115
|
+
}
|
|
116
|
+
/** Give up `id`'s place, wherever in the stack it sits. */
|
|
117
|
+
function releaseRootTheme(root, id) {
|
|
118
|
+
const at = rootThemeStack.findIndex((write) => write.id === id);
|
|
119
|
+
if (at >= 0)
|
|
120
|
+
rootThemeStack.splice(at, 1);
|
|
121
|
+
applyTopRootTheme(root);
|
|
122
|
+
if (rootThemeStack.length === 0) {
|
|
123
|
+
rootThemeBase.clear();
|
|
124
|
+
rootThemeWritten.clear();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
36
127
|
export function ThemeScope({ children, className, style, }) {
|
|
37
128
|
const runtime = useThemeRuntime();
|
|
38
|
-
|
|
129
|
+
const nested = useContext(ThemeScopeNestedContext);
|
|
130
|
+
// Variables on this div alone reach only what renders INSIDE it. Radix
|
|
131
|
+
// portals mount into document.body, outside it, so every dropdown,
|
|
132
|
+
// popover, select and tooltip fell back to :root — the base preset —
|
|
133
|
+
// while the page around them wore the customer's theme. `dialog` papered
|
|
134
|
+
// over its own case with a nested ThemeScope; that fixes one portal and
|
|
135
|
+
// leaves the next one to rediscover the bug.
|
|
136
|
+
//
|
|
137
|
+
// The outermost scope publishes to the document element instead, so
|
|
138
|
+
// anything portalled anywhere inherits. Nested scopes keep the div only:
|
|
139
|
+
// they exist to theme a subtree differently, and must not overwrite the
|
|
140
|
+
// document with their own values.
|
|
141
|
+
//
|
|
142
|
+
// Residual, stated because it is a real limit and not an oversight: a
|
|
143
|
+
// NESTED scope's own portalled content still resolves the document's
|
|
144
|
+
// (outermost) theme, because the portal escapes the nested div and the
|
|
145
|
+
// nested scope deliberately does not publish. That is strictly better
|
|
146
|
+
// than before — such content used to get the base `:root` preset rather
|
|
147
|
+
// than any theme at all — and the remedy already exists and is used:
|
|
148
|
+
// wrap the portalled content in a `ThemeScope`, as `ui/dialog.tsx` does
|
|
149
|
+
// with `display: contents`. Context reaches through a portal, so the
|
|
150
|
+
// nested scope's variables land on the portalled subtree (review catch).
|
|
151
|
+
// BEFORE paint, not after. A portal that exists on the very first render —
|
|
152
|
+
// a default-open dialog, an overlay restored during hydration — would
|
|
153
|
+
// otherwise paint once against the base `:root` preset and then correct
|
|
154
|
+
// itself: the mismatched-theme flash this exists to remove, in miniature
|
|
155
|
+
// (review catch).
|
|
156
|
+
// The identity of THIS scope, stable for its whole life. Its place in the
|
|
157
|
+
// stack is claimed once and released once; changing its theme in between
|
|
158
|
+
// must not re-order it (review catch).
|
|
159
|
+
const idRef = useRef(undefined);
|
|
160
|
+
idRef.current ?? (idRef.current = Symbol("theme-scope"));
|
|
161
|
+
useLayoutEffect(() => {
|
|
162
|
+
if (nested || typeof document === "undefined")
|
|
163
|
+
return;
|
|
164
|
+
const root = document.documentElement;
|
|
165
|
+
const id = idRef.current;
|
|
166
|
+
return () => releaseRootTheme(root, id);
|
|
167
|
+
}, [nested]);
|
|
168
|
+
useLayoutEffect(() => {
|
|
169
|
+
if (nested || typeof document === "undefined")
|
|
170
|
+
return;
|
|
171
|
+
// `font-family` rides along as one more name: `<html>` needs it set,
|
|
172
|
+
// not just `--font-body` defined, or portalled content keeps the host's
|
|
173
|
+
// font (review catch).
|
|
174
|
+
const vars = {
|
|
175
|
+
...runtime.variables,
|
|
176
|
+
"font-family": "var(--font-body)",
|
|
177
|
+
};
|
|
178
|
+
writeRootTheme(document.documentElement, idRef.current, vars);
|
|
179
|
+
}, [nested, runtime.variables]);
|
|
180
|
+
return (_jsx(ThemeScopeNestedContext.Provider, { value: true, children: _jsx("div", { className: className, style: { ...runtime.variables, fontFamily: "var(--font-body)", ...style }, children: children }) }));
|
|
39
181
|
}
|
|
40
182
|
export function ThemeDocumentMetadata() {
|
|
41
183
|
const runtime = useThemeRuntime();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Loader2, CheckCircle2, XCircle, AlertCircle, ChevronDown } from "lucide-react";
|
|
3
3
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
-
import { Collapsible, CollapsibleContent, CollapsibleTrigger
|
|
4
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
5
5
|
import { useState } from "react";
|
|
6
6
|
export function ToolCallCard({ icon, title, description, status, action, children, className, }) {
|
|
7
7
|
const [isOpen, setIsOpen] = useState(false);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,11 @@ export { MarkdownText, markdownComponents } from "./components/markdown-text.js"
|
|
|
5
5
|
export { ClientToolExecutor } from "./components/client-tool-executor.js";
|
|
6
6
|
export { AGUIRuntimeProvider, useAGUIAdapter } from "./components/ag-ui-runtime-provider.js";
|
|
7
7
|
export type { AGUIChatConversationFactoryArgs, AGUIHistoryAdapterFactory, } from "./components/ag-ui-runtime-provider.js";
|
|
8
|
-
export { ChatContent, DEFAULT_STARTER_SUGGESTIONS } from "./components/assistant-chat.js";
|
|
8
|
+
export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER, } from "./components/assistant-chat.js";
|
|
9
|
+
export { ComposerAddMenu, type ComposerAddMenuProps } from "./components/composer-add-menu.js";
|
|
9
10
|
export type { ChatContentProps } from "./components/assistant-chat.js";
|
|
10
11
|
export { ChatBubble } from "./components/chat-bubble.js";
|
|
11
|
-
export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatLauncherBadgeSlot, } from "./components/chat-slots.js";
|
|
12
|
+
export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatLauncherBadgeSlot, ChatComposerActionsSlot, useComposerActionsOwnSend, useChatThreadSurface, } from "./components/chat-slots.js";
|
|
12
13
|
export { ChatHeader } from "./components/chat-header.js";
|
|
13
14
|
export { ComposerAttachment, UserMessageAttachment, userAttachmentComponents, composerAttachmentComponents, } from "./components/chat-attachments.js";
|
|
14
15
|
export { ShowDocumentToolUI } from "./components/show-document-tool-ui.js";
|
|
@@ -18,7 +19,7 @@ export { ToolFallback } from "./components/tool-fallback.js";
|
|
|
18
19
|
export { ConfirmationCard } from "./components/confirmation-card.js";
|
|
19
20
|
export { TooltipIconButton } from "./components/tooltip-icon-button.js";
|
|
20
21
|
export { ComposerContextBadges, SentContextBadges } from "./components/context-badges.js";
|
|
21
|
-
export { ContextPins } from "./components/context-bar.js";
|
|
22
|
+
export { ContextPins, usePagePin } from "./components/context-bar.js";
|
|
22
23
|
export { SelectionPopover } from "./components/selection-popover.js";
|
|
23
24
|
export { GlobalSelectionPopover } from "./components/global-selection-popover.js";
|
|
24
25
|
export { LoadingIndicator } from "./components/loading-indicator.js";
|
|
@@ -84,6 +85,7 @@ export { useNewConversation } from "./lib/use-new-conversation.js";
|
|
|
84
85
|
export { AGUIAdapterSDK } from "./lib/ag-ui-adapter.js";
|
|
85
86
|
export { FileAttachmentAdapter, resolveMimeType } from "./lib/attachment-adapter.js";
|
|
86
87
|
export { registerAttachmentAdapter, useAttachmentAdapterStore } from "./lib/attachment-registry.js";
|
|
88
|
+
export { registerVoiceAdapter, registerSpeechAdapter, useVoiceAdapterStore, } from "./lib/voice-adapter-registry.js";
|
|
87
89
|
export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
|
|
88
90
|
export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
|
|
89
91
|
export { AuthProvider } from "./lib/auth-provider.js";
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,10 @@ 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 } from "./components/ag-ui-runtime-provider.js";
|
|
8
|
-
export { ChatContent, DEFAULT_STARTER_SUGGESTIONS } from "./components/assistant-chat.js";
|
|
8
|
+
export { ChatContent, DEFAULT_STARTER_SUGGESTIONS, DEFAULT_COMPOSER_PLACEHOLDER, } from "./components/assistant-chat.js";
|
|
9
|
+
export { ComposerAddMenu } from "./components/composer-add-menu.js";
|
|
9
10
|
export { ChatBubble } from "./components/chat-bubble.js";
|
|
10
|
-
export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatLauncherBadgeSlot, } from "./components/chat-slots.js";
|
|
11
|
+
export { registerChatSlots, useChatSlotsStore, ChatComposerBannerSlot, ChatLauncherBadgeSlot, ChatComposerActionsSlot, useComposerActionsOwnSend, useChatThreadSurface, } from "./components/chat-slots.js";
|
|
11
12
|
export { ChatHeader } from "./components/chat-header.js";
|
|
12
13
|
export { ComposerAttachment, UserMessageAttachment, userAttachmentComponents, composerAttachmentComponents, } from "./components/chat-attachments.js";
|
|
13
14
|
export { ShowDocumentToolUI } from "./components/show-document-tool-ui.js";
|
|
@@ -16,7 +17,7 @@ export { ToolFallback } from "./components/tool-fallback.js";
|
|
|
16
17
|
export { ConfirmationCard } from "./components/confirmation-card.js";
|
|
17
18
|
export { TooltipIconButton } from "./components/tooltip-icon-button.js";
|
|
18
19
|
export { ComposerContextBadges, SentContextBadges } from "./components/context-badges.js";
|
|
19
|
-
export { ContextPins } from "./components/context-bar.js";
|
|
20
|
+
export { ContextPins, usePagePin } from "./components/context-bar.js";
|
|
20
21
|
export { SelectionPopover } from "./components/selection-popover.js";
|
|
21
22
|
export { GlobalSelectionPopover } from "./components/global-selection-popover.js";
|
|
22
23
|
export { LoadingIndicator } from "./components/loading-indicator.js";
|
|
@@ -92,6 +93,7 @@ export { useNewConversation } from "./lib/use-new-conversation.js";
|
|
|
92
93
|
export { AGUIAdapterSDK } from "./lib/ag-ui-adapter.js";
|
|
93
94
|
export { FileAttachmentAdapter, resolveMimeType } from "./lib/attachment-adapter.js";
|
|
94
95
|
export { registerAttachmentAdapter, useAttachmentAdapterStore } from "./lib/attachment-registry.js";
|
|
96
|
+
export { registerVoiceAdapter, registerSpeechAdapter, useVoiceAdapterStore, } from "./lib/voice-adapter-registry.js";
|
|
95
97
|
export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
|
|
96
98
|
export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
|
|
97
99
|
// --- Auth ---
|
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
8
8
|
* The provider expects a fully resolved MSAL configuration.
|
|
9
9
|
* Missing configuration is treated as an application bootstrap error.
|
|
10
10
|
*/
|
|
11
|
-
import { useEffect, useState
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
12
12
|
import { authStore, initializeMsal, getMsalInstance, } from "@iloveagents/foundry-agent/msal";
|
|
13
|
-
export const AuthProvider = ({ config, children
|
|
13
|
+
export const AuthProvider = ({ config, children }) => {
|
|
14
14
|
const [ready, setReady] = useState(false);
|
|
15
15
|
const [initError, setInitError] = useState(null);
|
|
16
16
|
const [MsalProvider, setMsalProvider] = useState(null);
|
|
@@ -35,7 +35,9 @@ export const AuthProvider = ({ config, children, }) => {
|
|
|
35
35
|
setReady(true);
|
|
36
36
|
}
|
|
37
37
|
})();
|
|
38
|
-
return () => {
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
};
|
|
39
41
|
}, [config]);
|
|
40
42
|
if (!ready)
|
|
41
43
|
return null;
|
|
@@ -55,7 +57,7 @@ export const AuthProvider = ({ config, children, }) => {
|
|
|
55
57
|
* Uses inProgress === "none" to avoid redirect loops (best practice from
|
|
56
58
|
* Microsoft docs — never call loginRedirect while another interaction is active).
|
|
57
59
|
*/
|
|
58
|
-
const AuthGuard = ({ useMsalHook, children
|
|
60
|
+
const AuthGuard = ({ useMsalHook, children }) => {
|
|
59
61
|
const { instance, accounts, inProgress } = useMsalHook();
|
|
60
62
|
const [authenticated, setAuthenticated] = useState(false);
|
|
61
63
|
useEffect(() => {
|
|
@@ -5,7 +5,16 @@ interface PendingComposerSubmit {
|
|
|
5
5
|
interface ComposerSubmitState {
|
|
6
6
|
pending: PendingComposerSubmit | null;
|
|
7
7
|
submit: (text: string) => void;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Take ownership of a pending submission. True for exactly one caller.
|
|
10
|
+
*
|
|
11
|
+
* More than one bridge can legitimately be mounted — the expanded chat and
|
|
12
|
+
* the floating bubble can both exist, and voice needs a consumer alive even
|
|
13
|
+
* while the bubble is closed. Reading `pending` and clearing it afterwards
|
|
14
|
+
* let every mounted bridge see the same value and send it, so the claim and
|
|
15
|
+
* the clear happen together here instead.
|
|
16
|
+
*/
|
|
17
|
+
claim: (id: number) => boolean;
|
|
9
18
|
}
|
|
10
19
|
export declare const useComposerSubmitStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ComposerSubmitState>>;
|
|
11
20
|
export declare function submitComposerText(text: string): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
2
|
let nextSubmitId = 1;
|
|
3
|
-
export const useComposerSubmitStore = create((set) => ({
|
|
3
|
+
export const useComposerSubmitStore = create((set, get) => ({
|
|
4
4
|
pending: null,
|
|
5
5
|
submit: (text) => {
|
|
6
6
|
const trimmed = text.trim();
|
|
@@ -8,9 +8,12 @@ export const useComposerSubmitStore = create((set) => ({
|
|
|
8
8
|
return;
|
|
9
9
|
set({ pending: { id: nextSubmitId++, text: trimmed } });
|
|
10
10
|
},
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
claim: (id) => {
|
|
12
|
+
if (get().pending?.id !== id)
|
|
13
|
+
return false;
|
|
14
|
+
set({ pending: null });
|
|
15
|
+
return true;
|
|
16
|
+
},
|
|
14
17
|
}));
|
|
15
18
|
export function submitComposerText(text) {
|
|
16
19
|
useComposerSubmitStore.getState().submit(text);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice adapter registry — host-injectable realtime voice + read-aloud.
|
|
3
|
+
*
|
|
4
|
+
* assistant-ui owns realtime voice natively: ``useLocalRuntime`` accepts a
|
|
5
|
+
* ``RealtimeVoiceAdapter``, the thread runtime merges the transcripts it
|
|
6
|
+
* emits into ``thread.messages``, and ``useVoiceControls()`` /
|
|
7
|
+
* ``useVoiceState()`` / ``useVoiceVolume()`` expose the session to any
|
|
8
|
+
* component. What the runtime does NOT provide is a transport — that is
|
|
9
|
+
* the adapter's whole job, and it is deliberately not something this
|
|
10
|
+
* package implements.
|
|
11
|
+
*
|
|
12
|
+
* Threading an adapter through every chat mount would couple the shell to
|
|
13
|
+
* whichever voice backend a deployment happens to use, so this is a tiny
|
|
14
|
+
* registry instead — the exact pattern of :func:`registerAttachmentAdapter`
|
|
15
|
+
* and ``registerChatSlots``: a module calls :func:`registerVoiceAdapter`
|
|
16
|
+
* once (import time or ``useInit``) and the runtime provider picks it up on
|
|
17
|
+
* its next mount.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here knows about any particular voice service.
|
|
20
|
+
* ``@iloveagents/foundry-web-voice`` registers an Azure Voice Live adapter;
|
|
21
|
+
* a different backend is a different registration and no change here.
|
|
22
|
+
*
|
|
23
|
+
* Two independent slots, because they answer different questions:
|
|
24
|
+
* - ``voice`` — a live, two-way spoken session (``RealtimeVoiceAdapter``).
|
|
25
|
+
* - ``speech`` — one-shot "read this message aloud"
|
|
26
|
+
* (``SpeechSynthesisAdapter``, driven by ``thread.speak(messageId)``).
|
|
27
|
+
* A host may register either, both, or neither. Registering neither leaves
|
|
28
|
+
* the runtime exactly as it was before this file existed: assistant-ui
|
|
29
|
+
* reports the capability as unavailable and the UI hides the affordances.
|
|
30
|
+
*/
|
|
31
|
+
import type { RealtimeVoiceAdapter, SpeechSynthesisAdapter } from "@assistant-ui/react";
|
|
32
|
+
interface VoiceAdapterState {
|
|
33
|
+
voice: RealtimeVoiceAdapter | null;
|
|
34
|
+
speech: SpeechSynthesisAdapter | null;
|
|
35
|
+
}
|
|
36
|
+
export declare const useVoiceAdapterStore: import("zustand").UseBoundStore<import("zustand").StoreApi<VoiceAdapterState>>;
|
|
37
|
+
/**
|
|
38
|
+
* Register the realtime voice adapter. ``null`` clears it (the runtime
|
|
39
|
+
* then reports voice as unavailable). Later calls overwrite earlier ones —
|
|
40
|
+
* last registration wins.
|
|
41
|
+
*/
|
|
42
|
+
export declare function registerVoiceAdapter(adapter: RealtimeVoiceAdapter | null): void;
|
|
43
|
+
/**
|
|
44
|
+
* Register the read-aloud adapter behind ``thread.speak(messageId)``.
|
|
45
|
+
* ``null`` clears it. Independent of :func:`registerVoiceAdapter` — a host
|
|
46
|
+
* may want read-aloud without a live voice session, or the reverse.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerSpeechAdapter(adapter: SpeechSynthesisAdapter | null): void;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice adapter registry — host-injectable realtime voice + read-aloud.
|
|
3
|
+
*
|
|
4
|
+
* assistant-ui owns realtime voice natively: ``useLocalRuntime`` accepts a
|
|
5
|
+
* ``RealtimeVoiceAdapter``, the thread runtime merges the transcripts it
|
|
6
|
+
* emits into ``thread.messages``, and ``useVoiceControls()`` /
|
|
7
|
+
* ``useVoiceState()`` / ``useVoiceVolume()`` expose the session to any
|
|
8
|
+
* component. What the runtime does NOT provide is a transport — that is
|
|
9
|
+
* the adapter's whole job, and it is deliberately not something this
|
|
10
|
+
* package implements.
|
|
11
|
+
*
|
|
12
|
+
* Threading an adapter through every chat mount would couple the shell to
|
|
13
|
+
* whichever voice backend a deployment happens to use, so this is a tiny
|
|
14
|
+
* registry instead — the exact pattern of :func:`registerAttachmentAdapter`
|
|
15
|
+
* and ``registerChatSlots``: a module calls :func:`registerVoiceAdapter`
|
|
16
|
+
* once (import time or ``useInit``) and the runtime provider picks it up on
|
|
17
|
+
* its next mount.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here knows about any particular voice service.
|
|
20
|
+
* ``@iloveagents/foundry-web-voice`` registers an Azure Voice Live adapter;
|
|
21
|
+
* a different backend is a different registration and no change here.
|
|
22
|
+
*
|
|
23
|
+
* Two independent slots, because they answer different questions:
|
|
24
|
+
* - ``voice`` — a live, two-way spoken session (``RealtimeVoiceAdapter``).
|
|
25
|
+
* - ``speech`` — one-shot "read this message aloud"
|
|
26
|
+
* (``SpeechSynthesisAdapter``, driven by ``thread.speak(messageId)``).
|
|
27
|
+
* A host may register either, both, or neither. Registering neither leaves
|
|
28
|
+
* the runtime exactly as it was before this file existed: assistant-ui
|
|
29
|
+
* reports the capability as unavailable and the UI hides the affordances.
|
|
30
|
+
*/
|
|
31
|
+
import { create } from "zustand";
|
|
32
|
+
export const useVoiceAdapterStore = create(() => ({
|
|
33
|
+
voice: null,
|
|
34
|
+
speech: null,
|
|
35
|
+
}));
|
|
36
|
+
/**
|
|
37
|
+
* Register the realtime voice adapter. ``null`` clears it (the runtime
|
|
38
|
+
* then reports voice as unavailable). Later calls overwrite earlier ones —
|
|
39
|
+
* last registration wins.
|
|
40
|
+
*/
|
|
41
|
+
export function registerVoiceAdapter(adapter) {
|
|
42
|
+
useVoiceAdapterStore.setState({ voice: adapter });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Register the read-aloud adapter behind ``thread.speak(messageId)``.
|
|
46
|
+
* ``null`` clears it. Independent of :func:`registerVoiceAdapter` — a host
|
|
47
|
+
* may want read-aloud without a live voice session, or the reverse.
|
|
48
|
+
*/
|
|
49
|
+
export function registerSpeechAdapter(adapter) {
|
|
50
|
+
useVoiceAdapterStore.setState({ speech: adapter });
|
|
51
|
+
}
|
package/dist/styles.css
CHANGED
|
@@ -184,6 +184,27 @@
|
|
|
184
184
|
box-shadow: 6px 0 6px -6px color-mix(in srgb, var(--foreground) 12%, transparent);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/* The opposite call from the list scroller below, for the opposite reason.
|
|
188
|
+
* The toolbar's selection row shares a line with the search and the actions,
|
|
189
|
+
* and its entire contract is that ticking a row never changes the toolbar's
|
|
190
|
+
* height. A classic, space-consuming scrollbar — Windows and Linux by
|
|
191
|
+
* default, macOS set to "always show" — would appear exactly when the bar
|
|
192
|
+
* overflows, which is exactly when something is ticked, and its height would
|
|
193
|
+
* land on the toolbar as the same jump by another route (review catch).
|
|
194
|
+
*
|
|
195
|
+
* So the widget is hidden, not the scrolling: wheel, trackpad, and tabbing
|
|
196
|
+
* to a button past the edge all still reach it. Plain CSS rather than
|
|
197
|
+
* utilities, because a class that a consumer's Tailwind never scans fails
|
|
198
|
+
* silently, and this one has no visible symptom until the platform is one we
|
|
199
|
+
* do not develop on. */
|
|
200
|
+
[data-selection-row] {
|
|
201
|
+
scrollbar-width: none;
|
|
202
|
+
}
|
|
203
|
+
[data-selection-row]::-webkit-scrollbar {
|
|
204
|
+
height: 0;
|
|
205
|
+
display: none;
|
|
206
|
+
}
|
|
207
|
+
|
|
187
208
|
/* A list you can scroll should say so: keep its scrollbars visible rather
|
|
188
209
|
* than relying on the overlay ones macOS hides until you already scroll. */
|
|
189
210
|
[data-list-scroller] {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
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": [
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"react-markdown": "^10.0.0",
|
|
72
72
|
"remark-gfm": "^4.0.0",
|
|
73
73
|
"tailwind-merge": "^3.5.0",
|
|
74
|
-
"@iloveagents/foundry-agent": "^0.
|
|
75
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
74
|
+
"@iloveagents/foundry-agent": "^0.21.1",
|
|
75
|
+
"@iloveagents/foundry-web-primitives": "^0.21.1"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@ag-ui/client": "^0.0.52",
|