@iloveagents/foundry-web-ui 0.12.1 → 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/sidebar.js +149 -51
- 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
|
}
|
|
@@ -17,28 +17,78 @@ const ACTIVE_NAV_ROW_CLASS = "border border-transparent bg-sidebar-selected shad
|
|
|
17
17
|
const ACTIVE_NAV_TEXT_CLASS = "font-semibold text-sidebar-foreground";
|
|
18
18
|
const ACTIVE_NAV_ICON_CLASS = "text-primary";
|
|
19
19
|
const ACTIVE_NAV_BADGE_CLASS = "border border-sidebar-border bg-background/60 text-sidebar-foreground";
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
20
|
+
// Indent ladder for the nav tree, indexed by depth (0 = top-level item).
|
|
21
|
+
//
|
|
22
|
+
// ONE array of PAIRS rather than two parallel arrays, because the two
|
|
23
|
+
// numbers are not independent: ``body`` is always ``column`` plus the 20px
|
|
24
|
+
// disclosure cluster (16px cell + 4px gap). Split into separate lists they
|
|
25
|
+
// drift apart, and the tree stops looking like a tree.
|
|
26
|
+
//
|
|
27
|
+
// ``column`` is a LEFT OFFSET, not padding, because the twisty is absolutely
|
|
28
|
+
// positioned: it floats over the row body's own left padding instead of
|
|
29
|
+
// occupying a flex cell. That is what keeps the entire row — indent strip
|
|
30
|
+
// and disclosure cell included — a live click target for navigation, while
|
|
31
|
+
// the branch chevron still catches its own clicks on top. Laying the twisty
|
|
32
|
+
// out in flow instead makes every pixel left of the icon dead.
|
|
33
|
+
//
|
|
34
|
+
// Depth 0 sits at 12px so the twisty shares a column with the group-header
|
|
35
|
+
// chevron; every further level adds 8px, which keeps deep trees readable
|
|
36
|
+
// inside a ~260px sidebar.
|
|
37
|
+
const NAV_DEPTH_OFFSETS = [
|
|
38
|
+
{ column: "left-3", body: "pl-8" },
|
|
39
|
+
{ column: "left-5", body: "pl-10" },
|
|
40
|
+
{ column: "left-7", body: "pl-12" },
|
|
41
|
+
{ column: "left-9", body: "pl-14" },
|
|
42
|
+
{ column: "left-11", body: "pl-16" },
|
|
43
|
+
{ column: "left-13", body: "pl-18" },
|
|
44
|
+
];
|
|
45
|
+
// Fixed-width cell every nav row leads with, holding either a chevron
|
|
46
|
+
// (branch) or a dot (leaf). Same width as ``SIDEBAR_ITEM_ICON_CLASS`` so
|
|
47
|
+
// the disclosure column and the group-header chevron share one x.
|
|
48
|
+
const NAV_TWISTY_CLASS = "absolute inset-y-0 flex w-4 items-center justify-center";
|
|
49
|
+
// The chevron is a real control, so it needs a real touch target: it draws
|
|
50
|
+
// at ``size-3.5`` but CLICKS in 24px (WCAG 2.2 target-size minimum). It can
|
|
51
|
+
// simply overflow the 16px cell — the cell is absolutely positioned, so
|
|
52
|
+
// nothing downstream moves. The leaf dot needs no target at all; it is
|
|
53
|
+
// decorative and passes its clicks through to the row.
|
|
54
|
+
const NAV_TWISTY_HIT_CLASS = "size-6 shrink-0";
|
|
55
|
+
// Hover-reveal clearance for the right-hand actions menu. That control is a
|
|
56
|
+
// 28px button inside ``right-1`` + ``px-1`` (4 + 4 + 28 + 4 = 40px), so 44px
|
|
57
|
+
// keeps the label from butting straight into it. Applied ONLY to rows that
|
|
58
|
+
// actually have an actions menu — on a row without one the reserved padding
|
|
59
|
+
// buys nothing and just re-truncates the label on every hover.
|
|
60
|
+
const NAV_ROW_ACTIONS_CLEARANCE = "group-hover:pr-11 group-focus-within:pr-11";
|
|
61
|
+
// Shared icon-column geometry. The group-header chevron and the
|
|
62
|
+
// per-row disclosure cell (``NavTwisty``) are the SAME width, and the
|
|
63
|
+
// header wrapper below pads to the same x as
|
|
64
|
+
// ``NAV_DEPTH_OFFSETS[0].column`` — the depth-0 rung the rows use — so
|
|
65
|
+
// every chevron in the sidebar (section headers and tree rows
|
|
66
|
+
// alike) stacks on one vertical line. That single line is what makes
|
|
67
|
+
// the tree scan as a tree; when the columns drift the sidebar looks
|
|
68
|
+
// ragged (an earlier regression had the header chevron defaulting to
|
|
69
|
+
// ``size-6`` = 24px). Item labels now sit one cell further right than
|
|
70
|
+
// group labels, because item rows carry a twisty AND an icon while
|
|
71
|
+
// group headers carry only the chevron.
|
|
31
72
|
const SIDEBAR_ITEM_ICON_CLASS = "size-4 shrink-0";
|
|
32
73
|
const SIDEBAR_ITEM_GAP_CLASS = "gap-2";
|
|
33
|
-
// Outer padding for the group-header wrapper.
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
74
|
+
// Outer padding for the group-header wrapper. The group header lays its
|
|
75
|
+
// chevron out in normal flow, so it needs the depth-0 column expressed as
|
|
76
|
+
// a PADDING where rows express it as an offset — the same number, two
|
|
77
|
+
// spellings, which is exactly how columns silently desync when the indent
|
|
78
|
+
// ladder is retuned. The group-header alignment test in
|
|
79
|
+
// ``sidebar.test.tsx`` parses both numbers back out and fails if they
|
|
80
|
+
// ever diverge.
|
|
81
|
+
// Vertical is asymmetric: ``pt-4`` above gives breathing room between
|
|
82
|
+
// sections, ``pb-0`` below bonds the header to its first child — the
|
|
83
|
+
// ChatGPT / Linear / Notion "header sits on its children" pattern.
|
|
84
|
+
// Symmetric ``py-2`` made the header float midway between sections
|
|
85
|
+
// instead of belonging to the group below it.
|
|
86
|
+
const SIDEBAR_GROUP_HEADER_PADDING =
|
|
87
|
+
// ``border border-transparent`` for the same reason every item row
|
|
88
|
+
// carries one: the border is part of the box model that decides where
|
|
89
|
+
// the padding box starts. Without it the header chevron lands 1px left
|
|
90
|
+
// of every row's disclosure cell and the column is visibly ragged.
|
|
91
|
+
"border border-transparent pl-3 pr-3 pt-4 pb-0";
|
|
42
92
|
// Typography for the group-header LABEL text (CHATS / WORKSPACES /
|
|
43
93
|
// ADMIN / …). Stays at ``text-xs`` (12px) — same size as before —
|
|
44
94
|
// but bumps weight from ``font-medium`` to ``font-semibold``. The
|
|
@@ -49,8 +99,8 @@ const SIDEBAR_GROUP_HEADER_PADDING = "px-3 pt-4 pb-0";
|
|
|
49
99
|
// Bumping size AND weight together stacked too much visual mass
|
|
50
100
|
// against the already-wide uppercase tracking — felt shouty.
|
|
51
101
|
const SIDEBAR_GROUP_HEADER_LABEL_CLASS = "text-xs font-semibold uppercase tracking-wider";
|
|
52
|
-
function
|
|
53
|
-
return
|
|
102
|
+
function getNavDepthOffsets(depth) {
|
|
103
|
+
return NAV_DEPTH_OFFSETS[depth] ?? NAV_DEPTH_OFFSETS[NAV_DEPTH_OFFSETS.length - 1];
|
|
54
104
|
}
|
|
55
105
|
function scrollNavRowIntoView(nav, row) {
|
|
56
106
|
const navRect = nav.getBoundingClientRect();
|
|
@@ -349,6 +399,40 @@ function ContainerDropZone({ dnd }) {
|
|
|
349
399
|
: "bg-primary/10 border border-dashed border-primary/30")
|
|
350
400
|
: "h-1"), children: isDragOver && (_jsxs(_Fragment, { children: [isFileDragOver && _jsx(Upload, { className: "size-3 text-primary" }), _jsx("span", { className: "text-[10px] text-primary/70", children: getContainerDropZoneLabel(isFileDragOver) })] })) }));
|
|
351
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Leading disclosure cell on every nav row — the Confluence page-tree
|
|
404
|
+
* affordance.
|
|
405
|
+
*
|
|
406
|
+
* A BRANCH (item with children) gets a chevron that is always visible and
|
|
407
|
+
* rotates on expand; a LEAF gets a small dot. Both occupy the identical
|
|
408
|
+
* cell, so the whole tree shares one disclosure column and "does this row
|
|
409
|
+
* go deeper?" is answerable without hovering. That was the flaw in the
|
|
410
|
+
* previous design: the chevron lived in the hover-only right-hand controls,
|
|
411
|
+
* so a sidebar at rest carried no branch/leaf information at all.
|
|
412
|
+
*
|
|
413
|
+
* ABSOLUTELY POSITIONED over the row's own left padding, for two reasons:
|
|
414
|
+
*
|
|
415
|
+
* - The row stays clickable edge to edge. In flow the cell would be a dead
|
|
416
|
+
* strip — a <button> may not nest, so the twisty cannot live inside the
|
|
417
|
+
* navigate button, and everything left of the icon would stop navigating.
|
|
418
|
+
* - The chevron can take a 24px hit area out of a 16px cell without
|
|
419
|
+
* pushing the icon column right.
|
|
420
|
+
*
|
|
421
|
+
* The leaf dot is ``pointer-events-none`` so its clicks fall through to the
|
|
422
|
+
* row underneath; the branch chevron keeps its own, since positioned
|
|
423
|
+
* elements paint above the static button and win the hit test.
|
|
424
|
+
*/
|
|
425
|
+
function NavTwisty({ hasChildren, isExpanded, onToggle, label, isActive, depth, }) {
|
|
426
|
+
const cell = cn(NAV_TWISTY_CLASS, getNavDepthOffsets(depth).column);
|
|
427
|
+
if (!hasChildren) {
|
|
428
|
+
return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: _jsx("span", { className: cn("size-1 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") }) }));
|
|
429
|
+
}
|
|
430
|
+
return (_jsx("span", { "data-nav-twisty-cell": true, className: cell, children: _jsx("button", { type: "button", "data-nav-twisty": true, "aria-expanded": isExpanded, "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`, onClick: (e) => {
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
e.stopPropagation();
|
|
433
|
+
onToggle?.();
|
|
434
|
+
}, className: cn("flex items-center justify-center rounded", NAV_TWISTY_HIT_CLASS, isActive ? "text-primary" : "text-sidebar-foreground/50 hover:text-sidebar-foreground"), children: _jsx(ChevronRight, { className: cn("size-3.5 transition-transform duration-150", isExpanded && "rotate-90") }) }) }));
|
|
435
|
+
}
|
|
352
436
|
function NavItemActions({ actions }) {
|
|
353
437
|
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7 rounded-md text-muted-foreground hover:bg-muted/80 hover:text-foreground", onClick: (e) => {
|
|
354
438
|
e.preventDefault();
|
|
@@ -358,17 +442,23 @@ function NavItemActions({ actions }) {
|
|
|
358
442
|
void action.handler();
|
|
359
443
|
}, className: cn("gap-2", action.destructive && "text-destructive focus:text-destructive"), children: [_jsx(action.icon, { className: cn("size-3.5", action.destructive ? "text-destructive" : "text-muted-foreground") }), _jsx("span", { children: action.label })] })] }, action.label))) })] }));
|
|
360
444
|
}
|
|
361
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Hover-revealed controls pinned to the right edge of a nav row.
|
|
447
|
+
*
|
|
448
|
+
* Expand/collapse deliberately does NOT live here — it's the leading
|
|
449
|
+
* ``NavTwisty`` cell instead (see that component). This slot is for the
|
|
450
|
+
* item's own actions menu only.
|
|
451
|
+
*/
|
|
452
|
+
function NavRowControls({ item }) {
|
|
362
453
|
const isDisabled = item.disabled === true;
|
|
363
454
|
const hasActions = !isDisabled && item.actions && item.actions.length > 0;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
onToggleExpand?.();
|
|
368
|
-
}, className: "flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground", "aria-label": isExpanded ? "Collapse" : "Expand", children: _jsx(ChevronRight, { className: cn("size-3.5 transition-transform", isExpanded && "rotate-90") }) }))] }));
|
|
455
|
+
if (!hasActions)
|
|
456
|
+
return null;
|
|
457
|
+
return (_jsx("div", { className: "absolute inset-y-0 right-1 flex items-center gap-1 px-1 opacity-0 translate-x-1 pointer-events-none transition-all group-hover:opacity-100 group-hover:translate-x-0 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:translate-x-0 group-focus-within:pointer-events-auto", children: _jsx(NavItemActions, { actions: item.actions }) }));
|
|
369
458
|
}
|
|
370
459
|
/** Leaf nav item with optional drag-and-drop support via NavItem.dnd. */
|
|
371
|
-
function NavLeafItem({ item, isActive, onClick,
|
|
460
|
+
function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
461
|
+
const bodyPl = getNavDepthOffsets(depth).body;
|
|
372
462
|
const isDisabled = item.disabled === true;
|
|
373
463
|
const hasActions = Boolean(!isDisabled && item.actions && item.actions.length > 0);
|
|
374
464
|
const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
|
|
@@ -376,7 +466,7 @@ function NavLeafItem({ item, isActive, onClick, depthPl = getNestedNavIndent(0),
|
|
|
376
466
|
const activeButtonRef = useActiveNavRowScroll(isActive && !hasActions);
|
|
377
467
|
// Items with actions use the group layout (button + action controls)
|
|
378
468
|
if (hasActions) {
|
|
379
|
-
return (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
|
|
469
|
+
return (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
|
|
380
470
|
? "opacity-40"
|
|
381
471
|
: isActive
|
|
382
472
|
? ACTIVE_NAV_ROW_CLASS
|
|
@@ -385,20 +475,20 @@ function NavLeafItem({ item, isActive, onClick, depthPl = getNestedNavIndent(0),
|
|
|
385
475
|
!isDisabled &&
|
|
386
476
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
387
477
|
!isDisabled &&
|
|
388
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsxs("button", { type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1.5 text-sm text-left",
|
|
478
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1.5 text-sm text-left", bodyPl, "pr-3 transition-[padding] duration-150", NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
389
479
|
? "text-sidebar-foreground/45 cursor-default"
|
|
390
480
|
: isActive
|
|
391
481
|
? ACTIVE_NAV_TEXT_CLASS
|
|
392
|
-
: "text-sidebar-foreground/80"), children: [_jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item
|
|
482
|
+
: "text-sidebar-foreground/80"), children: [_jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
393
483
|
}
|
|
394
|
-
return (_jsxs("button", { ref: activeButtonRef, "data-active-nav-row": isActive ? "true" : undefined, type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, ...(isDisabled ? {} : draggableProps), ...(isDisabled ? {} : dropTargetProps), className: cn("flex items-center gap-2 w-full", "rounded-lg border border-transparent pr-3 py-1.5 text-sm text-left",
|
|
484
|
+
return (_jsxs("button", { ref: activeButtonRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, ...(isDisabled ? {} : draggableProps), ...(isDisabled ? {} : dropTargetProps), className: cn("relative flex items-center gap-2 w-full", "rounded-lg border border-transparent pr-3 py-1.5 text-sm text-left", bodyPl, isDisabled
|
|
395
485
|
? "opacity-40 cursor-default"
|
|
396
486
|
: "text-sidebar-foreground/80 transition-colors hover:bg-sidebar-accent/70", !isDisabled && isActive && ACTIVE_NAV_ROW_CLASS, isDragOver &&
|
|
397
487
|
!isFileDragOver &&
|
|
398
488
|
!isDisabled &&
|
|
399
489
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
400
490
|
!isDisabled &&
|
|
401
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: cn("truncate", isActive && !isDisabled && ACTIVE_NAV_TEXT_CLASS), children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] }));
|
|
491
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: cn("truncate", isActive && !isDisabled && ACTIVE_NAV_TEXT_CLASS), children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] }));
|
|
402
492
|
}
|
|
403
493
|
function SubNavItems({ items, parentPath, depth = 1, }) {
|
|
404
494
|
const navigate = useNavigate();
|
|
@@ -407,7 +497,6 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
|
|
|
407
497
|
const navContext = useAppStore((s) => s.navContext);
|
|
408
498
|
const isOnParent = currentPath === parentPath;
|
|
409
499
|
const [activeLabel, setActiveLabel] = useState(items[0]?.label ?? "");
|
|
410
|
-
const childDepthPl = getNestedNavIndent(depth);
|
|
411
500
|
// Children with unique paths (e.g., SPACES documents) use URL matching;
|
|
412
501
|
// children sharing the parent path (e.g., Documents sub-filters) use label matching.
|
|
413
502
|
const allShareParent = items.every((c) => c.to === parentPath);
|
|
@@ -423,7 +512,7 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
|
|
|
423
512
|
const isActive = allShareParent
|
|
424
513
|
? isOnParent && activeLabel === child.label
|
|
425
514
|
: child.to !== parentPath && currentPath === child.to;
|
|
426
|
-
return (_jsx(NavLeafItem, { item: child, isActive: isActive,
|
|
515
|
+
return (_jsx(NavLeafItem, { item: child, isActive: isActive, depth: depth, onClick: () => {
|
|
427
516
|
setActiveLabel(child.label);
|
|
428
517
|
setNavContext({
|
|
429
518
|
...navContext,
|
|
@@ -456,10 +545,11 @@ function NestedFolderItem({ item, depth }) {
|
|
|
456
545
|
const [manualOverride, setManualOverride] = useState(null);
|
|
457
546
|
const isExpanded = manualOverride ?? isInsideSubtree;
|
|
458
547
|
const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
|
|
459
|
-
const
|
|
548
|
+
const bodyPl = getNavDepthOffsets(depth).body;
|
|
460
549
|
const isDisabled = item.disabled === true;
|
|
550
|
+
const hasActions = Boolean(!isDisabled && item.actions && item.actions.length > 0);
|
|
461
551
|
const activeRowRef = useActiveNavRowScroll(isActive);
|
|
462
|
-
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), children: [_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full", "rounded-xl border border-transparent transition-colors", isDisabled
|
|
552
|
+
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), children: [_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full", "rounded-xl border border-transparent transition-colors", isDisabled
|
|
463
553
|
? "opacity-40"
|
|
464
554
|
: isActive
|
|
465
555
|
? ACTIVE_NAV_ROW_CLASS
|
|
@@ -468,7 +558,7 @@ function NestedFolderItem({ item, depth }) {
|
|
|
468
558
|
!isDisabled &&
|
|
469
559
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
470
560
|
!isDisabled &&
|
|
471
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsxs("button", { type: "button", disabled: isDisabled, onClick: () => {
|
|
561
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: true, isExpanded: isExpanded, onToggle: () => setManualOverride(!isExpanded), label: item.label, isActive: (isActive || isAncestorActive) && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: () => {
|
|
472
562
|
if (isDisabled)
|
|
473
563
|
return;
|
|
474
564
|
setManualOverride(true);
|
|
@@ -482,7 +572,7 @@ function NestedFolderItem({ item, depth }) {
|
|
|
482
572
|
.join("\n") || null,
|
|
483
573
|
});
|
|
484
574
|
navigate(item.to);
|
|
485
|
-
}, className: cn("flex items-center gap-2 flex-1 min-w-0",
|
|
575
|
+
}, className: cn("flex items-center gap-2 flex-1 min-w-0", bodyPl, "pr-3 py-1.5 text-sm text-left transition-[padding] duration-150", hasActions && NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
486
576
|
? "text-sidebar-foreground/45 cursor-default"
|
|
487
577
|
: isActive
|
|
488
578
|
? ACTIVE_NAV_TEXT_CLASS
|
|
@@ -492,13 +582,13 @@ function NestedFolderItem({ item, depth }) {
|
|
|
492
582
|
? ACTIVE_NAV_ICON_CLASS
|
|
493
583
|
: isAncestorActive
|
|
494
584
|
? "text-sidebar-foreground/80"
|
|
495
|
-
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item
|
|
585
|
+
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }), isExpanded && item.children && (_jsxs("div", { children: [item.children.map((child) => {
|
|
496
586
|
const hasNestedChildren = child.children && child.children.length > 0;
|
|
497
587
|
if (hasNestedChildren) {
|
|
498
588
|
return (_jsx(NestedFolderItem, { item: child, depth: depth + 1 }, child.to + child.label));
|
|
499
589
|
}
|
|
500
590
|
const isChildActive = currentPath === child.to;
|
|
501
|
-
return (_jsx(NavLeafItem, { item: child, isActive: isChildActive,
|
|
591
|
+
return (_jsx(NavLeafItem, { item: child, isActive: isChildActive, depth: depth + 1, onClick: () => {
|
|
502
592
|
setNavContext({
|
|
503
593
|
...navContext,
|
|
504
594
|
label: child.label,
|
|
@@ -530,7 +620,7 @@ function CollapsibleNavItem({ item }) {
|
|
|
530
620
|
const activeRowRef = useActiveNavRowScroll(isActive && rendersRow);
|
|
531
621
|
const activeLinkRef = useActiveNavRowScroll(isActive && !rendersRow);
|
|
532
622
|
if (rendersRow) {
|
|
533
|
-
const inner = (selected, ancestor) => (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": selected ? "true" : undefined, className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
|
|
623
|
+
const inner = (selected, ancestor) => (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": selected ? "true" : undefined, "data-nav-depth": 0, className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
|
|
534
624
|
? "opacity-40"
|
|
535
625
|
: selected
|
|
536
626
|
? ACTIVE_NAV_ROW_CLASS
|
|
@@ -539,7 +629,7 @@ function CollapsibleNavItem({ item }) {
|
|
|
539
629
|
!isDisabled &&
|
|
540
630
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
541
631
|
!isDisabled &&
|
|
542
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsxs("button", { type: "button", onClick: () => {
|
|
632
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: hasChildren, isExpanded: isExpanded, onToggle: hasChildren ? () => setManualOverride(!isExpanded) : undefined, label: item.label, isActive: (selected || ancestor) && !isDisabled, depth: 0 }), _jsxs("button", { type: "button", onClick: () => {
|
|
543
633
|
if (isDisabled)
|
|
544
634
|
return;
|
|
545
635
|
setNavContext({
|
|
@@ -554,7 +644,7 @@ function CollapsibleNavItem({ item }) {
|
|
|
554
644
|
if (hasChildren)
|
|
555
645
|
setManualOverride(true);
|
|
556
646
|
navigate(item.to);
|
|
557
|
-
}, className: cn("flex items-center gap-2 flex-1 min-w-0
|
|
647
|
+
}, className: cn("flex items-center gap-2 flex-1 min-w-0 py-2 text-sm text-left", getNavDepthOffsets(0).body, "pr-3 transition-[padding] duration-150", hasActions && NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
558
648
|
? "text-sidebar-foreground/45 cursor-default"
|
|
559
649
|
: selected
|
|
560
650
|
? ACTIVE_NAV_TEXT_CLASS
|
|
@@ -564,15 +654,23 @@ function CollapsibleNavItem({ item }) {
|
|
|
564
654
|
? ACTIVE_NAV_ICON_CLASS
|
|
565
655
|
: ancestor
|
|
566
656
|
? "text-sidebar-foreground/80"
|
|
567
|
-
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", selected ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item
|
|
657
|
+
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", selected ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
568
658
|
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), children: [inner(isActive, isAncestorActive), hasChildren && isExpanded && (_jsxs(_Fragment, { children: [_jsx(SubNavItems, { items: item.children, parentPath: item.to }), item.dnd?.canDrop() && _jsx(ContainerDropZone, { dnd: item.dnd })] }))] }));
|
|
569
659
|
}
|
|
570
660
|
if (isDisabled) {
|
|
571
|
-
return (_jsxs("div", { className: cn("flex items-center gap-2 w-full opacity-40",
|
|
661
|
+
return (_jsxs("div", { "data-nav-depth": 0, className: cn("relative flex items-center gap-2 w-full opacity-40", getNavDepthOffsets(0).body,
|
|
662
|
+
// Transparent border for box-model parity — see the NavLink variant.
|
|
663
|
+
"rounded-lg border border-transparent pr-3 py-2 text-left text-sm text-sidebar-foreground/45"), children: [_jsx(NavTwisty, { hasChildren: false, label: item.label, depth: 0 }), _jsx(item.icon, { className: "size-4 shrink-0 text-sidebar-foreground/45" }), _jsx("span", { className: "truncate", children: item.label })] }));
|
|
572
664
|
}
|
|
573
|
-
return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": currentPath === item.to ? "true" : undefined, to: item.to, ...draggableProps, ...dropTargetProps, className: ({ isActive }) => cn("flex items-center gap-2 w-full",
|
|
665
|
+
return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": currentPath === item.to ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: ({ isActive }) => cn("relative flex items-center gap-2 w-full", getNavDepthOffsets(0).body,
|
|
666
|
+
// The transparent border is NOT decoration: it is what makes this
|
|
667
|
+
// row's box model match every other variant. An absolutely
|
|
668
|
+
// positioned twisty resolves against the PADDING box, so a row
|
|
669
|
+
// without the border puts its disclosure cell 1px left of its
|
|
670
|
+
// siblings' and the column goes ragged.
|
|
671
|
+
"rounded-xl border border-transparent pr-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70", isActive && cn(ACTIVE_NAV_ROW_CLASS, ACTIVE_NAV_TEXT_CLASS), isDragOver &&
|
|
574
672
|
!isFileDragOver &&
|
|
575
|
-
"bg-sidebar-selected ring-2 ring-inset ring-sidebar-ring/50", isFileDragOver && "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
673
|
+
"bg-sidebar-selected ring-2 ring-inset ring-sidebar-ring/50", isFileDragOver && "outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, label: item.label, isActive: isActive, depth: 0 }), _jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
576
674
|
? ACTIVE_NAV_BADGE_CLASS
|
|
577
675
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] })) }));
|
|
578
676
|
}
|
|
@@ -891,13 +989,13 @@ function MobileDrawer() {
|
|
|
891
989
|
window.addEventListener("keydown", handleEscape);
|
|
892
990
|
return () => window.removeEventListener("keydown", handleEscape);
|
|
893
991
|
}, [isMobileOpen, isToolPanelOpen, closeMobile]);
|
|
894
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn("fixed inset-0 z-50 bg-black/50 transition-opacity duration-200", isMobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"), onClick: closeMobile, "aria-hidden": "true" }), _jsx("aside", { className: cn("fixed top-0 left-0 z-50 h-dvh", "flex flex-col", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground shadow-xl", "transition-transform duration-200 ease-in-out", isMobileOpen ? "translate-x-0" : "-translate-x-full"), style: { width: drawerWidth }, role: "complementary", "aria-label": "Navigation sidebar", children: _jsx(SidebarContent, { collapsed: false }) })] }));
|
|
992
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn("fixed inset-0 z-50 bg-black/50 transition-opacity duration-200", isMobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"), onClick: closeMobile, "aria-hidden": "true" }), _jsx("aside", { "data-sidebar-surface": "mobile", className: cn("fixed top-0 left-0 z-50 h-dvh", "flex flex-col", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground shadow-xl", "transition-transform duration-200 ease-in-out", isMobileOpen ? "translate-x-0" : "-translate-x-full"), style: { width: drawerWidth }, role: "complementary", "aria-label": "Navigation sidebar", children: _jsx(SidebarContent, { collapsed: false }) })] }));
|
|
895
993
|
}
|
|
896
994
|
/** Desktop inline sidebar (expanded or collapsed rail) */
|
|
897
995
|
function DesktopSidebar() {
|
|
898
996
|
const isOpen = useSidebarStore((s) => s.isOpen);
|
|
899
997
|
const width = useSidebarStore((s) => s.width);
|
|
900
|
-
return (_jsxs("aside", { className: cn("group/sidebar relative hidden md:flex flex-col shrink-0", "h-full", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground", "transition-[width] duration-200 ease-in-out", "overflow-hidden"), style: { width: isOpen ? width : RAIL_WIDTH }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: !isOpen }), isOpen && _jsx(SidebarResizeHandle, {})] }));
|
|
998
|
+
return (_jsxs("aside", { "data-sidebar-surface": "desktop", className: cn("group/sidebar relative hidden md:flex flex-col shrink-0", "h-full", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground", "transition-[width] duration-200 ease-in-out", "overflow-hidden"), style: { width: isOpen ? width : RAIL_WIDTH }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: !isOpen }), isOpen && _jsx(SidebarResizeHandle, {})] }));
|
|
901
999
|
}
|
|
902
1000
|
export function Sidebar() {
|
|
903
1001
|
return (_jsxs(_Fragment, { children: [_jsx(MobileDrawer, {}), _jsx(DesktopSidebar, {})] }));
|
|
@@ -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",
|