@iloveagents/foundry-web-ui 0.13.0 → 0.14.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/assistant-chat.d.ts +8 -3
- package/dist/components/assistant-chat.js +3 -9
- package/dist/components/chat-bubble.js +5 -8
- package/dist/components/chat-header.js +8 -1
- package/dist/components/chat-message-parts.d.ts +17 -0
- package/dist/components/chat-message-parts.js +43 -0
- package/dist/components/reasoning-effort-picker.js +1 -1
- package/dist/components/tool-run-group.d.ts +23 -0
- package/dist/components/tool-run-group.js +61 -0
- package/dist/lib/use-page-tools.d.ts +21 -2
- package/dist/lib/use-page-tools.js +0 -0
- package/package.json +3 -3
|
@@ -13,9 +13,14 @@ export interface ChatContentProps {
|
|
|
13
13
|
*/
|
|
14
14
|
starterSuggestions?: string[];
|
|
15
15
|
/**
|
|
16
|
-
* Show the page pin button in the composer.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Show the page pin button in the composer.
|
|
17
|
+
*
|
|
18
|
+
* Defaults to TRUE. It defaulted to false, and nothing in the shell ever
|
|
19
|
+
* passed it, so the affordance shipped switched off everywhere: on a
|
|
20
|
+
* document or a view there was no way to put the thing you were looking
|
|
21
|
+
* at into the conversation. Pinning the current page is the core gesture
|
|
22
|
+
* of a page-aware chat, not an opt-in extra — an app that genuinely wants
|
|
23
|
+
* a bare composer opts OUT.
|
|
19
24
|
*/
|
|
20
25
|
showPinButton?: boolean;
|
|
21
26
|
}
|
|
@@ -11,10 +11,8 @@ import { ContextPins } from "./context-bar.js";
|
|
|
11
11
|
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
12
12
|
import { ChatComposerBannerSlot } from "./chat-slots.js";
|
|
13
13
|
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
14
|
-
import { MarkdownText } from "./markdown-text.js";
|
|
15
|
-
import { ReasoningMessagePartComponent } from "./reasoning-part.js";
|
|
16
14
|
import { ReasoningEffortPicker } from "./reasoning-effort-picker.js";
|
|
17
|
-
import {
|
|
15
|
+
import { ChatMessageParts } from "./chat-message-parts.js";
|
|
18
16
|
import { LoadingIndicator, LoadingBar } from "./loading-indicator.js";
|
|
19
17
|
import { MessageTimestamp } from "./message-timestamp.js";
|
|
20
18
|
import { MessageError } from "./message-error.js";
|
|
@@ -24,11 +22,7 @@ const UserMessage = () => {
|
|
|
24
22
|
return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(MessagePrimitive.Attachments, { components: userAttachmentComponents }), _jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: cn("max-w-[80%]", "bg-muted rounded-2xl", "px-4 py-3", "text-foreground"), children: _jsx(MessagePrimitive.Content, {}) }), _jsxs("div", { className: "flex h-8 items-center justify-end gap-2 pr-2", children: [_jsx(MessageTimestamp, {}), _jsx(BranchPicker, {}), _jsx("div", { className: "flex h-8 w-10 items-center justify-end", children: _jsx(UserActionBar, {}) })] })] }) }));
|
|
25
23
|
};
|
|
26
24
|
const UserActionBar = () => (_jsx(ActionBarPrimitive.Root, { hideWhenRunning: true, className: "flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(ActionBarPrimitive.Edit, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Edit", size: "icon", children: _jsx(PencilIcon, { className: "size-4" }) }) }) }));
|
|
27
|
-
const AssistantMessage = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsx("div", { className: "text-foreground leading-relaxed flex flex-col gap-4", children: _jsx(
|
|
28
|
-
Text: MarkdownText,
|
|
29
|
-
Reasoning: ReasoningMessagePartComponent,
|
|
30
|
-
tools: { Fallback: ToolFallback },
|
|
31
|
-
} }) }), _jsx(MessageError, {}), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { className: "flex h-8 w-[4.25rem] items-center", children: _jsx(AssistantActionBar, {}) }), _jsx(BranchPicker, {}), _jsx(MessageTimestamp, {})] })] }) }));
|
|
25
|
+
const AssistantMessage = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsx("div", { className: "text-foreground leading-relaxed flex flex-col gap-4", children: _jsx(ChatMessageParts, {}) }), _jsx(MessageError, {}), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { className: "flex h-8 w-[4.25rem] items-center", children: _jsx(AssistantActionBar, {}) }), _jsx(BranchPicker, {}), _jsx(MessageTimestamp, {})] })] }) }));
|
|
32
26
|
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" }) }) })] }));
|
|
33
27
|
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" }) })] })] }) }) }) }));
|
|
34
28
|
const ComposerActionButton = () => {
|
|
@@ -113,7 +107,7 @@ const DropZone = ({ children }) => {
|
|
|
113
107
|
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
114
108
|
* Renders inside a ThreadPrimitive.Root context.
|
|
115
109
|
*/
|
|
116
|
-
export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton =
|
|
110
|
+
export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = true, } = {}) {
|
|
117
111
|
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
118
112
|
const collapseChat = useChatBubbleStore((s) => s.collapse);
|
|
119
113
|
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
@@ -9,8 +9,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
11
|
import { ChatComposerBannerSlot, ChatLauncherBadgeSlot } from "./chat-slots.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { ChatMessageParts } from "./chat-message-parts.js";
|
|
13
|
+
import { ReasoningEffortPicker } from "./reasoning-effort-picker.js";
|
|
14
14
|
import { LoadingIndicator } from "./loading-indicator.js";
|
|
15
15
|
import { MessageTimestamp } from "./message-timestamp.js";
|
|
16
16
|
import { MessageError } from "./message-error.js";
|
|
@@ -20,10 +20,7 @@ import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
|
20
20
|
import { RAIL_WIDTH, useSidebarStore } from "../lib/sidebar-store.js";
|
|
21
21
|
const CHAT_BUBBLE_INSET = 8;
|
|
22
22
|
function BubbleAssistantMessage() {
|
|
23
|
-
return (_jsxs(MessagePrimitive.Root, { className: "w-full mb-2 group", children: [_jsx("div", { className: "text-foreground text-sm leading-relaxed flex flex-col gap-2", children: _jsx(MessagePrimitive.
|
|
24
|
-
Text: MarkdownText,
|
|
25
|
-
Reasoning: ReasoningMessagePartComponent,
|
|
26
|
-
} }) }), _jsx(MessageError, { compact: true }), _jsxs("div", { className: "flex h-7 items-center gap-2 mt-1", children: [_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 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", className: "size-7", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-3" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-3 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Retry", size: "icon", className: "size-7", children: _jsx(RefreshCw, { className: "size-3" }) }) })] }), _jsx(MessageTimestamp, {})] })] }));
|
|
23
|
+
return (_jsxs(MessagePrimitive.Root, { className: "w-full mb-2 group", children: [_jsx("div", { className: "text-foreground text-sm leading-relaxed flex flex-col gap-2", children: _jsx(ChatMessageParts, {}) }), _jsx(MessageError, { compact: true }), _jsxs("div", { className: "flex h-7 items-center gap-2 mt-1", children: [_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 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", className: "size-7", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-3" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-3 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Retry", size: "icon", className: "size-7", children: _jsx(RefreshCw, { className: "size-3" }) }) })] }), _jsx(MessageTimestamp, {})] })] }));
|
|
27
24
|
}
|
|
28
25
|
function BubbleUserMessage() {
|
|
29
26
|
const messageId = useAuiState((s) => s.message.id);
|
|
@@ -99,10 +96,10 @@ export function ChatBubble() {
|
|
|
99
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: {
|
|
100
97
|
width: isSidebarOpen ? sidebarWidth : RAIL_WIDTH,
|
|
101
98
|
height: 64,
|
|
102
|
-
} })), !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, 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
|
|
99
|
+
} })), !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
|
|
103
100
|
? { left: dragPos.x, top: dragPos.y }
|
|
104
101
|
: { 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: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto", 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", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
105
102
|
UserMessage: BubbleUserMessage,
|
|
106
103
|
AssistantMessage: BubbleAssistantMessage,
|
|
107
|
-
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: _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("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(ComposerSubmitBridge, {}), _jsx(ComposerContextBadges, {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center -space-x-1 shrink-0", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Attach file", size: "icon", className: "size-7 shrink-0", children: _jsx(Paperclip, { className: "size-3.5" }) }) }), _jsx(ContextPins, { compact: true })] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: "Message...", 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: 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" }) }) }) }), _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" }) }) }) })] })] }) })] })] })] }))] }));
|
|
104
|
+
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: _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("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(ComposerSubmitBridge, {}), _jsx(ComposerContextBadges, {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center -space-x-1 shrink-0", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Attach file", size: "icon", className: "size-7 shrink-0", children: _jsx(Paperclip, { className: "size-3.5" }) }) }), _jsx(ContextPins, { compact: true, showPinButton: true })] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: "Message...", 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(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" }) }) }) }), _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" }) }) }) })] })] }) })] })] })] }))] }));
|
|
108
105
|
}
|
|
@@ -8,8 +8,15 @@ import { ThemeToggle } from "./theme-toggle.js";
|
|
|
8
8
|
import { UserMenu } from "./user-menu.js";
|
|
9
9
|
import { useSidebarStore } from "../lib/sidebar-store.js";
|
|
10
10
|
import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
11
|
+
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
11
12
|
export const ChatHeader = () => {
|
|
12
|
-
const
|
|
13
|
+
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
14
|
+
// In expanded mode the default "navigate to /" would land on the chat
|
|
15
|
+
// route, and the shell collapses expanded chat there — so New Thread
|
|
16
|
+
// silently threw the user out of the mode they were working in AND off
|
|
17
|
+
// the page they were reading. Expanded starts the thread in place, the
|
|
18
|
+
// same contract the bubble already uses.
|
|
19
|
+
const handleNewConversation = useNewConversation(isExpanded ? { navigateToChat: false, preserveNavigation: true } : undefined);
|
|
13
20
|
const toggleMobile = useSidebarStore((s) => s.toggleMobile);
|
|
14
21
|
const isSidebarOpen = useSidebarStore((s) => s.isOpen);
|
|
15
22
|
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(ThemeToggle, {}), _jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-primary" }) }), _jsx(UserMenu, {})] })] }) }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of an assistant message — ONE renderer, used by every chat
|
|
3
|
+
* surface.
|
|
4
|
+
*
|
|
5
|
+
* The bubble and the expanded chat used to configure `MessagePrimitive.Parts`
|
|
6
|
+
* separately, and had drifted: the expanded chat passed a `tools` fallback and
|
|
7
|
+
* rendered full tool cards, the bubble passed none and so rendered assistant-ui's
|
|
8
|
+
* default. Same conversation, two different transcripts depending on which
|
|
9
|
+
* window you happened to have open. Composing this component instead of
|
|
10
|
+
* hand-rolling a parts config is what keeps them honest — there is now only
|
|
11
|
+
* one place to drift from.
|
|
12
|
+
*
|
|
13
|
+
* Adjacent tool calls fold into one {@link ToolRunGroup} — a single summary row
|
|
14
|
+
* over the cards, open by default; see that component for why collapsing is the
|
|
15
|
+
* deliberate act rather than the default.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ChatMessageParts(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MessagePrimitive, groupPartByType } from "@assistant-ui/react";
|
|
3
|
+
import { MarkdownText } from "./markdown-text.js";
|
|
4
|
+
import { ReasoningMessagePartComponent } from "./reasoning-part.js";
|
|
5
|
+
import { ToolFallback } from "./tool-fallback.js";
|
|
6
|
+
import { ToolRunGroup } from "./tool-run-group.js";
|
|
7
|
+
/**
|
|
8
|
+
* Coalesce adjacent tool calls into one `group-tool` node. Everything else
|
|
9
|
+
* stays ungrouped and renders as its own leaf.
|
|
10
|
+
*/
|
|
11
|
+
const groupBy = groupPartByType({ "tool-call": ["group-tool"] });
|
|
12
|
+
/**
|
|
13
|
+
* The parts of an assistant message — ONE renderer, used by every chat
|
|
14
|
+
* surface.
|
|
15
|
+
*
|
|
16
|
+
* The bubble and the expanded chat used to configure `MessagePrimitive.Parts`
|
|
17
|
+
* separately, and had drifted: the expanded chat passed a `tools` fallback and
|
|
18
|
+
* rendered full tool cards, the bubble passed none and so rendered assistant-ui's
|
|
19
|
+
* default. Same conversation, two different transcripts depending on which
|
|
20
|
+
* window you happened to have open. Composing this component instead of
|
|
21
|
+
* hand-rolling a parts config is what keeps them honest — there is now only
|
|
22
|
+
* one place to drift from.
|
|
23
|
+
*
|
|
24
|
+
* Adjacent tool calls fold into one {@link ToolRunGroup} — a single summary row
|
|
25
|
+
* over the cards, open by default; see that component for why collapsing is the
|
|
26
|
+
* deliberate act rather than the default.
|
|
27
|
+
*/
|
|
28
|
+
export function ChatMessageParts() {
|
|
29
|
+
return (_jsx(MessagePrimitive.GroupedParts, { groupBy: groupBy, indicator: "never", children: ({ part, children }) => {
|
|
30
|
+
switch (part.type) {
|
|
31
|
+
case "group-tool":
|
|
32
|
+
return (_jsx(ToolRunGroup, { indices: part.indices, isRunning: part.status?.type === "running", children: children }));
|
|
33
|
+
case "text":
|
|
34
|
+
return _jsx(MarkdownText, {});
|
|
35
|
+
case "reasoning":
|
|
36
|
+
return _jsx(ReasoningMessagePartComponent, { text: part.text });
|
|
37
|
+
case "tool-call":
|
|
38
|
+
return part.toolUI ?? _jsx(ToolFallback, { ...part });
|
|
39
|
+
default:
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
} }));
|
|
43
|
+
}
|
|
@@ -46,5 +46,5 @@ export function ReasoningEffortPicker({ className }) {
|
|
|
46
46
|
const triggerLabel = effort === "default" && resolved
|
|
47
47
|
? `${REASONING_EFFORT_LABELS.default} · ${REASONING_EFFORT_LABELS[resolved]}`
|
|
48
48
|
: REASONING_EFFORT_LABELS[effort];
|
|
49
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("flex items-center gap-1 rounded-md px-2 py-1", "text-xs text-muted-foreground hover:text-foreground hover:bg-muted", "transition-colors", className), "aria-label": `Intelligence: ${triggerLabel}`, children: [_jsx("span", { children: triggerLabel }), _jsx(ChevronDown, { className: "size-3.5" })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [_jsx(DropdownMenuLabel, { className: "text-xs font-normal text-muted-foreground", children: "Intelligence" }), OPTIONS.map((opt) => (_jsxs(DropdownMenuItem, { onSelect: () => setEffort(opt.value), className: "flex items-start gap-2", children: [_jsx(Check, { className: cn("size-4 mt-0.5 shrink-0", effort === opt.value ? "opacity-100" : "opacity-0") }), _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm", children: REASONING_EFFORT_LABELS[opt.value] }), _jsx("span", { className: "text-xs text-muted-foreground", children: opt.value === "default" ? autoHint(resolved, scope) : opt.hint })] })] }, opt.value)))] })] }));
|
|
49
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-reasoning-effort-trigger": true, className: cn("flex items-center gap-1 rounded-md px-2 py-1", "text-xs text-muted-foreground hover:text-foreground hover:bg-muted", "transition-colors", className), "aria-label": `Intelligence: ${triggerLabel}`, children: [_jsx("span", { className: "whitespace-nowrap", children: triggerLabel }), _jsx(ChevronDown, { className: "size-3.5 shrink-0" })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [_jsx(DropdownMenuLabel, { className: "text-xs font-normal text-muted-foreground", children: "Intelligence" }), OPTIONS.map((opt) => (_jsxs(DropdownMenuItem, { onSelect: () => setEffort(opt.value), className: "flex items-start gap-2", children: [_jsx(Check, { className: cn("size-4 mt-0.5 shrink-0", effort === opt.value ? "opacity-100" : "opacity-0") }), _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm", children: REASONING_EFFORT_LABELS[opt.value] }), _jsx("span", { className: "text-xs text-muted-foreground", children: opt.value === "default" ? autoHint(resolved, scope) : opt.hint })] })] }, opt.value)))] })] }));
|
|
50
50
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* One row standing in for a run of adjacent tool calls, with the cards under
|
|
4
|
+
* it behind a disclosure.
|
|
5
|
+
*
|
|
6
|
+
* A turn that calls eight tools used to render eight bordered cards with no
|
|
7
|
+
* way to fold them away. The run is now one summary line — "8 tools · done" —
|
|
8
|
+
* over a group the reader can collapse when the machinery is in the way.
|
|
9
|
+
*
|
|
10
|
+
* OPEN by default: the detail is what a developer watching an agent work
|
|
11
|
+
* actually wants, and a transcript that hides every step by default reads as
|
|
12
|
+
* though nothing happened. Collapsing is the deliberate act, not expanding.
|
|
13
|
+
*
|
|
14
|
+
* RED only when the whole run failed. One failure among eight successful
|
|
15
|
+
* calls is not a red turn — the agent recovered, and painting the summary red
|
|
16
|
+
* would report a broken run where there was a working one. A partial failure
|
|
17
|
+
* still says so, in the count, without the alarm.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ToolRunGroup({ indices, isRunning, children, }: {
|
|
20
|
+
indices: readonly number[];
|
|
21
|
+
isRunning: boolean;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useAuiState } from "@assistant-ui/react";
|
|
4
|
+
import { ChevronRight, Loader2, CheckCircle2, XCircle, AlertCircle, Wrench } from "lucide-react";
|
|
5
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
7
|
+
import { _detectErrorResult } from "./tool-fallback.js";
|
|
8
|
+
/**
|
|
9
|
+
* One row standing in for a run of adjacent tool calls, with the cards under
|
|
10
|
+
* it behind a disclosure.
|
|
11
|
+
*
|
|
12
|
+
* A turn that calls eight tools used to render eight bordered cards with no
|
|
13
|
+
* way to fold them away. The run is now one summary line — "8 tools · done" —
|
|
14
|
+
* over a group the reader can collapse when the machinery is in the way.
|
|
15
|
+
*
|
|
16
|
+
* OPEN by default: the detail is what a developer watching an agent work
|
|
17
|
+
* actually wants, and a transcript that hides every step by default reads as
|
|
18
|
+
* though nothing happened. Collapsing is the deliberate act, not expanding.
|
|
19
|
+
*
|
|
20
|
+
* RED only when the whole run failed. One failure among eight successful
|
|
21
|
+
* calls is not a red turn — the agent recovered, and painting the summary red
|
|
22
|
+
* would report a broken run where there was a working one. A partial failure
|
|
23
|
+
* still says so, in the count, without the alarm.
|
|
24
|
+
*/
|
|
25
|
+
export function ToolRunGroup({ indices, isRunning, children, }) {
|
|
26
|
+
const { total, failed } = useToolRunOutcome(indices);
|
|
27
|
+
// Every call in the run failed — the only state that earns the alarm colour.
|
|
28
|
+
const allFailed = total > 0 && failed === total;
|
|
29
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
30
|
+
const label = isRunning
|
|
31
|
+
? `Using ${total === 1 ? "1 tool" : `${total} tools`}…`
|
|
32
|
+
: failed > 0
|
|
33
|
+
? `${total === 1 ? "1 tool" : `${total} tools`} · ${failed} failed`
|
|
34
|
+
: `${total === 1 ? "1 tool" : `${total} tools`} · done`;
|
|
35
|
+
return (_jsxs(Collapsible, { open: isOpen, onOpenChange: setIsOpen, className: "w-full", "data-tool-run-group": true, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-tool-run-trigger": true, className: cn("flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-xs", "text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", allFailed && "text-destructive hover:text-destructive"), children: [_jsx(ChevronRight, { className: cn("size-3.5 shrink-0 transition-transform", isOpen && "rotate-90") }), isRunning ? (_jsx(Loader2, { className: "size-3.5 shrink-0 animate-spin text-primary" })) : allFailed ? (_jsx(XCircle, { className: "size-3.5 shrink-0 text-destructive" })) : failed > 0 ? (
|
|
36
|
+
// Partial: the run produced a result, some of it the hard way.
|
|
37
|
+
_jsx(AlertCircle, { className: "size-3.5 shrink-0 text-muted-foreground" })) : (_jsx(CheckCircle2, { className: "size-3.5 shrink-0 text-primary" })), _jsx(Wrench, { className: "size-3 shrink-0 opacity-60" }), _jsx("span", { className: "truncate", children: label })] }) }), _jsx(CollapsibleContent, { children: _jsx("div", { className: "mt-1.5 flex flex-col gap-2", children: children }) })] }));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* How many calls this run made, and how many of them failed.
|
|
41
|
+
*
|
|
42
|
+
* The group node the primitive hands us carries only the LAST part's status,
|
|
43
|
+
* which is the wrong signal here: a run whose final call succeeded after an
|
|
44
|
+
* earlier one failed would summarise as green. So read the parts themselves
|
|
45
|
+
* and count. A failed tool call shows up two ways — a transport-level
|
|
46
|
+
* `incomplete` status, or a `complete` round-trip whose payload describes an
|
|
47
|
+
* error (a Python `@tool` raising, an MCP `isError`) — and both count.
|
|
48
|
+
*/
|
|
49
|
+
function useToolRunOutcome(indices) {
|
|
50
|
+
const parts = useAuiState((s) => s.message.parts);
|
|
51
|
+
let failed = 0;
|
|
52
|
+
for (const index of indices) {
|
|
53
|
+
const part = parts?.[index];
|
|
54
|
+
if (!part)
|
|
55
|
+
continue;
|
|
56
|
+
const status = part.status;
|
|
57
|
+
if (status?.type === "incomplete" || _detectErrorResult(part.result))
|
|
58
|
+
failed++;
|
|
59
|
+
}
|
|
60
|
+
return { total: indices.length, failed };
|
|
61
|
+
}
|
|
@@ -2,8 +2,27 @@
|
|
|
2
2
|
* usePageTools — hook for pages to register client tools with the agent.
|
|
3
3
|
*
|
|
4
4
|
* Call this in any page component to declare tools available on that page.
|
|
5
|
-
* Tools are registered on mount and removed on unmount, so the agent
|
|
6
|
-
*
|
|
5
|
+
* Tools are registered on mount and removed on unmount, so the agent only
|
|
6
|
+
* sees tools relevant to the current page.
|
|
7
|
+
*
|
|
8
|
+
* **The set may change while the page is mounted.** A page's capabilities are
|
|
9
|
+
* state, not a mount-time constant: a document open read-only cannot accept a
|
|
10
|
+
* live edit, a form with no dirty fields cannot submit. Registering once meant
|
|
11
|
+
* the catalogue advertised tools the surface could not perform, and the model —
|
|
12
|
+
* correctly trusting the catalogue — called them and dead-ended on an error.
|
|
13
|
+
* Offer a tool only while it works, and the agent picks the route that does:
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* usePageTools([
|
|
17
|
+
* fileInfoTool,
|
|
18
|
+
* ...(liveEditAvailable ? [replaceSelectionTool] : []),
|
|
19
|
+
* ]);
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Re-registration keys on the schema the model sees (names, descriptions,
|
|
23
|
+
* parameters) — a fresh array literal with identical schemas does not thrash
|
|
24
|
+
* the store. `execute` is always dispatched to the CURRENT render's closure, so
|
|
25
|
+
* tools read live state directly instead of through refs.
|
|
7
26
|
*
|
|
8
27
|
* @example
|
|
9
28
|
* ```tsx
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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": [
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"tailwind-merge": "^3.5.0",
|
|
71
71
|
"react-markdown": "^10.0.0",
|
|
72
72
|
"remark-gfm": "^4.0.0",
|
|
73
|
-
"@iloveagents/foundry-agent": "^0.
|
|
74
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
73
|
+
"@iloveagents/foundry-agent": "^0.14.0",
|
|
74
|
+
"@iloveagents/foundry-web-primitives": "^0.14.0"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@ag-ui/client": "^0.0.52",
|