From 4ff9d9f1b0f2782db178fbfd1eabcf0cca230ace Mon Sep 17 00:00:00 2001 From: Sahil Date: Mon, 2 Feb 2026 20:03:30 +0530 Subject: [PATCH] feat(docs): enhance Copilot SDK documentation and introduce loader variants - Added detailed documentation for new loader variants in the Copilot UI, including examples and descriptions for each variant. - Updated the "Why Copilot SDK" page to reflect changes in feature descriptions, replacing "Tool UI Rendering" with "Generative UI." - Improved the organization of the documentation by adding a new section for avatars, detailing customization options and usage examples. - Updated the playground components to support the new loader variants and avatar configurations. BREAKING: None - changes are backward compatible. --- apps/docs/content/docs/meta.json | 4 +- apps/docs/content/docs/ui.mdx | 92 +++++ apps/docs/content/docs/why-copilot-sdk.mdx | 2 +- examples/playground/app/page.tsx | 6 + .../components/playground/ControlPanel.tsx | 64 +++ .../components/playground/CopilotPanel.tsx | 9 +- .../components/playground/CopilotSidebar.tsx | 4 + .../playground/layouts/DefaultLayout.tsx | 13 +- .../playground/layouts/SaasLayout.tsx | 16 +- .../playground/layouts/SupportLayout.tsx | 16 +- examples/playground/lib/constants.ts | 11 + examples/playground/lib/types.ts | 10 +- examples/playground/package.json | 2 +- packages/copilot-sdk/package.json | 2 +- .../src/ui/components/composed/chat/chat.tsx | 302 ++++++-------- .../composed/chat/default-message.tsx | 376 ++++++++++-------- .../src/ui/components/composed/chat/types.ts | 13 +- .../composed/tools/loop-progress.tsx | 6 +- .../composed/tools/tool-execution-list.tsx | 4 +- .../src/ui/components/ui/avatar.tsx | 2 +- .../src/ui/components/ui/loader.tsx | 219 ++-------- .../src/ui/components/ui/message.tsx | 20 +- .../src/ui/components/ui/scroll-button.tsx | 4 +- packages/copilot-sdk/src/ui/styles/base.css | 171 ++++++-- pnpm-lock.yaml | 99 +---- 25 files changed, 774 insertions(+), 693 deletions(-) diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index c4db2bb..e559015 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -20,6 +20,8 @@ "multimodal", "---AI---", "llm-sdk", - "providers" + "providers", + "---API Reference---", + "api-reference" ] } diff --git a/apps/docs/content/docs/ui.mdx b/apps/docs/content/docs/ui.mdx index 1aa81ab..6d9e314 100644 --- a/apps/docs/content/docs/ui.mdx +++ b/apps/docs/content/docs/ui.mdx @@ -201,6 +201,98 @@ Themes automatically support dark mode when the `dark` class is on an ancestor e --- +## Loader Variants + +Customize the loading indicator shown when AI is generating a response: + +```tsx + +``` + +### Available Variants + +| Variant | Description | +|---------|-------------| +| `typing` | Typing indicator dots (default) | +| `dots` | Bouncing dots | +| `wave` | Wave animation | +| `terminal` | Terminal cursor blink | +| `text-blink` | Blinking "Thinking" text | +| `text-shimmer` | Shimmer effect "Thinking" text | +| `loading-dots` | "Thinking..." with animated dots | + +### Text-based Loaders + +The text-based variants (`text-blink`, `text-shimmer`, `loading-dots`) display "Thinking" text with animations: + +```tsx +// Blinking text + + +// Shimmer effect + + +// "Thinking..." with animated dots + +``` + +--- + +## Avatars + +Customize the avatars shown next to messages: + +### Basic Usage + +```tsx + +``` + +### Custom Avatar Component + +Pass a custom React component for full control: + +```tsx + + ) + }} + userAvatar={{ + component: + }} + showUserAvatar +/> +``` + +### Avatar Props + +| Prop | Type | Description | +|------|------|-------------| +| `src` | `string` | Image URL | +| `fallback` | `string` | Text shown while image loads or if it fails | +| `component` | `ReactNode` | Custom component (overrides src/fallback) | + + +User avatars are hidden by default. Set `showUserAvatar` to display them. + + +--- + ## Pre-built Components The SDK exports these ready-to-use components: diff --git a/apps/docs/content/docs/why-copilot-sdk.mdx b/apps/docs/content/docs/why-copilot-sdk.mdx index 245c43f..1ce2557 100644 --- a/apps/docs/content/docs/why-copilot-sdk.mdx +++ b/apps/docs/content/docs/why-copilot-sdk.mdx @@ -52,7 +52,7 @@ Building AI assistants shouldn't require a team of ML engineers and months of de | **100% Self-Hosted** | | | | | | **Smart Context** | | | | | | **Your Knowledgebase** | | | | | -| **Tool UI Rendering** | | | | | +| **Generative UI** | | | | | | **MCP Support** | Soon | | | | diff --git a/examples/playground/app/page.tsx b/examples/playground/app/page.tsx index 1b1ac16..87b852e 100644 --- a/examples/playground/app/page.tsx +++ b/examples/playground/app/page.tsx @@ -17,6 +17,7 @@ import { ApiKeyModal } from "@/components/modals/ApiKeyModal"; import { WelcomeModal } from "@/components/modals/WelcomeModal"; // Theme CSS imports +import "@yourgpt/copilot-sdk/ui/styles.css"; import "@yourgpt/copilot-sdk/ui/themes/claude.css"; import "@yourgpt/copilot-sdk/ui/themes/linear.css"; import "@yourgpt/copilot-sdk/ui/themes/vercel.css"; @@ -36,6 +37,7 @@ export default function PlaygroundPage() { systemPrompt, generativeUI, toolsEnabled, + sdkConfig, selectedProvider, selectedOpenRouterModel, updateTheme, @@ -43,6 +45,7 @@ export default function PlaygroundPage() { updateSystemPrompt, toggleGenerativeUI, toggleTool, + updateSDKConfig, updateProvider, updateOpenRouterModel, } = usePlaygroundConfig(); @@ -154,6 +157,8 @@ export default function PlaygroundPage() { onReset={actions.reset} selectedPerson={selectedPerson} onSelectPerson={handleSelectPerson} + sdkConfig={sdkConfig} + onUpdateSDKConfig={updateSDKConfig} /> @@ -171,6 +176,7 @@ export default function PlaygroundPage() { selectedProvider={selectedProvider} selectedOpenRouterModel={selectedOpenRouterModel} apiKeys={apiKeys} + loaderVariant={sdkConfig.loaderVariant} /> diff --git a/examples/playground/components/playground/ControlPanel.tsx b/examples/playground/components/playground/ControlPanel.tsx index 4a82271..260fd61 100644 --- a/examples/playground/components/playground/ControlPanel.tsx +++ b/examples/playground/components/playground/ControlPanel.tsx @@ -62,6 +62,8 @@ import type { PersonData, LayoutTemplate, ProviderId, + SDKConfig, + LoaderVariant, } from "@/lib/types"; import { themes, @@ -69,6 +71,7 @@ import { layoutTemplates, providers, OPENROUTER_MODELS, + LOADER_VARIANTS, } from "@/lib/constants"; import { WeatherModule } from "./modules/WeatherModule"; import { StockModule } from "./modules/StockModule"; @@ -273,6 +276,11 @@ interface ControlPanelProps { onReset: () => void; selectedPerson: PersonData; onSelectPerson: (person: PersonData) => void; + sdkConfig: SDKConfig; + onUpdateSDKConfig: ( + key: K, + value: SDKConfig[K], + ) => void; } function ControlPanelComponent({ @@ -298,6 +306,8 @@ function ControlPanelComponent({ onReset, selectedPerson, onSelectPerson, + sdkConfig, + onUpdateSDKConfig, }: ControlPanelProps) { const activeToolCount = Object.values(toolsEnabled).filter(Boolean).length; const selectedTheme = themes.find((t) => t.id === copilotTheme); @@ -522,6 +532,60 @@ function ControlPanelComponent({ placeholder="// Define AI behavior..." /> + + {/* Row 3: More Settings - Bare collapsible */} + + + + + More Settings + + + +
+ {/* Loader Variant */} +
+
+ + + Loader + +
+ +
+
+
+
+
diff --git a/examples/playground/components/playground/CopilotPanel.tsx b/examples/playground/components/playground/CopilotPanel.tsx index debcf78..abc06a2 100644 --- a/examples/playground/components/playground/CopilotPanel.tsx +++ b/examples/playground/components/playground/CopilotPanel.tsx @@ -7,6 +7,7 @@ import type { LayoutTemplate, ToolsEnabledConfig, GenerativeUIConfig, + LoaderVariant, } from "@/lib/types"; import type { DashboardActions } from "@/hooks/useDashboardState"; import { useDashboardContext } from "@/hooks/useDashboardContext"; @@ -23,6 +24,7 @@ interface CopilotPanelProps { currentPerson: PersonData; toolsEnabled: ToolsEnabledConfig; generativeUI: GenerativeUIConfig; + loaderVariant: LoaderVariant; } export function CopilotPanel({ @@ -33,6 +35,7 @@ export function CopilotPanel({ currentPerson, toolsEnabled, generativeUI, + loaderVariant, }: CopilotPanelProps) { // Provide dashboard and user context to the AI useDashboardContext({ dashboardState, currentPerson }); @@ -41,12 +44,12 @@ export function CopilotPanel({ const renderLayout = () => { switch (layoutTemplate) { case "saas": - return ; + return ; case "support": - return ; + return ; case "default": default: - return ; + return ; } }; diff --git a/examples/playground/components/playground/CopilotSidebar.tsx b/examples/playground/components/playground/CopilotSidebar.tsx index ca0e547..52d1151 100644 --- a/examples/playground/components/playground/CopilotSidebar.tsx +++ b/examples/playground/components/playground/CopilotSidebar.tsx @@ -13,6 +13,7 @@ import type { GenerativeUIConfig, ProviderId, ApiKeys, + LoaderVariant, } from "@/lib/types"; import type { DashboardActions } from "@/hooks/useDashboardState"; @@ -28,6 +29,7 @@ interface CopilotSidebarProps { selectedProvider: ProviderId; selectedOpenRouterModel: string; apiKeys: ApiKeys; + loaderVariant: LoaderVariant; } export function CopilotSidebar({ @@ -42,6 +44,7 @@ export function CopilotSidebar({ selectedProvider, selectedOpenRouterModel, apiKeys, + loaderVariant, }: CopilotSidebarProps) { // Build runtime URL with provider and optional API key const runtimeUrl = useMemo(() => { @@ -99,6 +102,7 @@ export function CopilotSidebar({ currentPerson={selectedPerson} toolsEnabled={toolsEnabled} generativeUI={generativeUI} + loaderVariant={loaderVariant} /> diff --git a/examples/playground/components/playground/layouts/DefaultLayout.tsx b/examples/playground/components/playground/layouts/DefaultLayout.tsx index 1af4830..962ea8f 100644 --- a/examples/playground/components/playground/layouts/DefaultLayout.tsx +++ b/examples/playground/components/playground/layouts/DefaultLayout.tsx @@ -1,13 +1,14 @@ "use client"; import { CopilotChat } from "@yourgpt/copilot-sdk/ui"; -import type { CopilotTheme } from "@/lib/types"; +import type { CopilotTheme, LoaderVariant } from "@/lib/types"; export interface LayoutProps { theme: CopilotTheme; + loaderVariant: LoaderVariant; } -export function DefaultLayout({ theme }: LayoutProps) { +export function DefaultLayout({ theme, loaderVariant }: LayoutProps) { return (
); diff --git a/examples/playground/components/playground/layouts/SaasLayout.tsx b/examples/playground/components/playground/layouts/SaasLayout.tsx index 9e8444e..24af0d0 100644 --- a/examples/playground/components/playground/layouts/SaasLayout.tsx +++ b/examples/playground/components/playground/layouts/SaasLayout.tsx @@ -88,13 +88,25 @@ function CustomSuggestions() { ); } -export function SaasLayout({ theme }: LayoutProps) { +export function SaasLayout({ theme, loaderVariant }: LayoutProps) { // Use supabase theme by default for this layout, unless a different theme is selected const effectiveTheme = theme === "default" ? "supabase" : theme; return (
- + {/* Home View - Custom welcome screen */} {/* Logo */} diff --git a/examples/playground/components/playground/layouts/SupportLayout.tsx b/examples/playground/components/playground/layouts/SupportLayout.tsx index e132df6..f00977f 100644 --- a/examples/playground/components/playground/layouts/SupportLayout.tsx +++ b/examples/playground/components/playground/layouts/SupportLayout.tsx @@ -184,13 +184,25 @@ function SupportHome({ input }: { input?: React.ReactNode }) { ); } -export function SupportLayout({ theme }: LayoutProps) { +export function SupportLayout({ theme, loaderVariant }: LayoutProps) { return (
- + {/* Custom Home View */} - {/* Messages */} - - + - {/* Welcome message */} - {messages.length === 0 && ( -
- {welcomeMessage || - "Send a message to start the conversation"} -
- )} - - {/* Messages */} - {messages.map((message, index) => { - const isLastMessage = index === messages.length - 1; - const isEmptyAssistant = - message.role === "assistant" && !message.content?.trim(); - - // Check if message has tool_calls or toolExecutions - const hasToolCalls = - message.tool_calls && message.tool_calls.length > 0; - const hasToolExecutions = - message.toolExecutions && message.toolExecutions.length > 0; - - // Check if this message has pending tool approvals - const hasPendingApprovals = message.toolExecutions?.some( - (exec) => exec.approvalStatus === "required", - ); - - if (isEmptyAssistant) { - if (hasToolCalls || hasToolExecutions) { - // Has tools - continue to render - } else if (isLastMessage && hasPendingApprovals) { - // Has pending approvals - continue to render - } else if (isLastMessage && isLoading && !isProcessing) { - // Show streaming loader - return ( - - - ) : undefined - } - className="bg-background" - /> -
- -
-
- ); - } else { - // Hide empty assistant messages - return null; - } - } - - // Check for saved executions in metadata (historical) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const savedExecutions = (message as any).metadata - ?.toolExecutions as ToolExecutionData[] | undefined; - const messageToolExecutions = - message.toolExecutions || savedExecutions; - - const messageWithExecutions = messageToolExecutions - ? { ...message, toolExecutions: messageToolExecutions } - : message; - - // Handle follow-up click - use onSendMessage if available - const handleFollowUpClick = (question: string) => { - if (onSuggestionClick) { - onSuggestionClick(question); - } else { - onSendMessage?.(question); - } - }; - - return renderMessage ? ( - - {renderMessage(messageWithExecutions, index)} - - ) : ( - - ); - })} - - {/* "Continuing..." loader - shown after tool completion while waiting for server */} - {isProcessing && ( - - - ) : undefined - } - className="bg-background" - /> -
- - - Continuing... - + + {/* Welcome message */} + {messages.length === 0 && ( +
+ {welcomeMessage || + "Send a message to start the conversation"}
- - )} - - {/* Loading indicator for non-streaming - when last message is user and waiting for response */} - {isLoading && - !isProcessing && - (() => { - const lastMessage = messages[messages.length - 1]; - // Show loader if last message is from user (non-streaming doesn't create empty assistant message) - if (lastMessage?.role === "user") { - return ( - - - ) : undefined - } - className="bg-background" - /> -
- -
-
- ); + )} + + {/* Messages */} + {messages.map((message, index) => { + const isLastMessage = index === messages.length - 1; + const isEmptyAssistant = + message.role === "assistant" && !message.content?.trim(); + + // Check if message has tool_calls or toolExecutions + const hasToolCalls = + message.tool_calls && message.tool_calls.length > 0; + const hasToolExecutions = + message.toolExecutions && + message.toolExecutions.length > 0; + + // Check if this message has pending tool approvals + const hasPendingApprovals = message.toolExecutions?.some( + (exec) => exec.approvalStatus === "required", + ); + + // Hide empty assistant messages that aren't loading and have no content to show + if (isEmptyAssistant) { + const shouldShowMessage = + hasToolCalls || + hasToolExecutions || + hasPendingApprovals || + (isLastMessage && (isLoading || isProcessing)); + + if (!shouldShowMessage) { + return null; + } + // Otherwise, continue to render via DefaultMessage } - return null; - })()} - - -
- {/* Scroll to bottom button */} -
- -
- + // Check for saved executions in metadata (historical) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const savedExecutions = (message as any).metadata + ?.toolExecutions as ToolExecutionData[] | undefined; + const messageToolExecutions = + message.toolExecutions || savedExecutions; + + const messageWithExecutions = messageToolExecutions + ? { ...message, toolExecutions: messageToolExecutions } + : message; + + // Handle follow-up click - use onSendMessage if available + const handleFollowUpClick = (question: string) => { + if (onSuggestionClick) { + onSuggestionClick(question); + } else { + onSendMessage?.(question); + } + }; + + return renderMessage ? ( + + {renderMessage(messageWithExecutions, index)} + + ) : ( + + ); + })} + + {/* Loading indicator for non-streaming - when last message is user and no assistant message yet */} + {isLoading && + !isProcessing && + messages.length > 0 && + messages[messages.length - 1]?.role === "user" && ( + + )} + + + + + {/* Scroll to bottom button - inside ChatContainerRoot for context, outside ChatContainerContent so it doesn't scroll */} +
+ +
+ +
{/* Suggestions */} {suggestions.length > 0 && !isLoading && ( diff --git a/packages/copilot-sdk/src/ui/components/composed/chat/default-message.tsx b/packages/copilot-sdk/src/ui/components/composed/chat/default-message.tsx index 85c3143..75025d0 100644 --- a/packages/copilot-sdk/src/ui/components/composed/chat/default-message.tsx +++ b/packages/copilot-sdk/src/ui/components/composed/chat/default-message.tsx @@ -10,14 +10,19 @@ import { type PermissionLevel, } from "../../ui/permission-confirmation"; import { FollowUpQuestions, parseFollowUps } from "../../ui/follow-up"; +import { Loader } from "../../ui/loader"; import type { ChatMessage, MessageAttachment, ToolRenderers } from "./types"; import type { ToolDefinition, ToolRenderProps } from "../../../../core"; import CopilotSDKLogo from "../../icons/copilot-sdk-logo"; type DefaultMessageProps = { message: ChatMessage; - userAvatar: { src?: string; fallback?: string }; - assistantAvatar: { src?: string; fallback?: string }; + userAvatar: { src?: string; fallback?: string; component?: React.ReactNode }; + assistantAvatar: { + src?: string; + fallback?: string; + component?: React.ReactNode; + }; showUserAvatar?: boolean; userMessageClassName?: string; assistantMessageClassName?: string; @@ -27,6 +32,17 @@ type DefaultMessageProps = { isLastMessage?: boolean; /** Whether the chat is currently loading/streaming */ isLoading?: boolean; + /** Whether waiting for server after tool completion */ + isProcessing?: boolean; + /** Loader variant for typing indicator */ + loaderVariant?: + | "dots" + | "typing" + | "wave" + | "terminal" + | "text-blink" + | "text-shimmer" + | "loading-dots"; /** Registered tools (for accessing tool's render function) */ registeredTools?: ToolDefinition[]; /** Custom renderers for tool results (Generative UI) - higher priority than tool.render */ @@ -63,6 +79,8 @@ export function DefaultMessage({ size = "sm", isLastMessage = false, isLoading = false, + isProcessing = false, + loaderVariant = "typing", registeredTools, toolRenderers, onApproveToolExecution, @@ -129,10 +147,12 @@ export function DefaultMessage({
{showUserAvatar && ( + > + {userAvatar.component} + )} ); @@ -176,17 +196,21 @@ export function DefaultMessage({ return ( ) : undefined } - className="bg-background" - /> -
+ className="bg-muted" + > + {assistantAvatar.component} + +
{/* Reasoning/Thinking (collapsible, above content) */} {message.thinking && ( )} - {/* Message Content - show FIRST (AI's words before tool calls) */} - {cleanContent?.trim() && ( - + + Continuing... +
+ ) : /* Show streaming loader when loading with no content and no tools */ + isLastMessage && + isLoading && + !cleanContent?.trim() && + !toolsWithCustomRender?.length && + !toolsWithoutCustomRender?.length && + !pendingApprovalTools?.length ? ( +
+ +
+ ) : ( + <> + {/* Message Content - show FIRST (AI's words before tool calls) */} + {cleanContent?.trim() && ( + + {cleanContent} + )} - markdown - size={size} - > - {cleanContent} - - )} - {/* Custom Tool Renderers - Priority: toolRenderers > tool.render */} - {toolsWithCustomRender && toolsWithCustomRender.length > 0 && ( -
- {toolsWithCustomRender.map((exec) => { - // PRIORITY 1: toolRenderers (app-level override) - const Renderer = toolRenderers?.[exec.name]; - if (Renderer) { - return ( - tool.render */} + {toolsWithCustomRender && toolsWithCustomRender.length > 0 && ( +
+ {toolsWithCustomRender.map((exec) => { + // PRIORITY 1: toolRenderers (app-level override) + const Renderer = toolRenderers?.[exec.name]; + if (Renderer) { + return ( + + ); + } + + // PRIORITY 2: tool's own render function + const toolDef = registeredTools?.find( + (t) => t.name === exec.name, + ); + if (toolDef?.render) { + // Map execution status to ToolRenderProps status + let status: ToolRenderProps["status"] = "pending"; + if (exec.status === "executing") status = "executing"; + else if (exec.status === "completed") status = "completed"; + else if ( + exec.status === "error" || + exec.status === "failed" || + exec.status === "rejected" + ) + status = "error"; + + const renderProps: ToolRenderProps = { + status, args: exec.args, - status: exec.status, result: exec.result, error: exec.error, - approvalStatus: exec.approvalStatus, - }} - /> - ); - } - - // PRIORITY 2: tool's own render function - const toolDef = registeredTools?.find( - (t) => t.name === exec.name, - ); - if (toolDef?.render) { - // Map execution status to ToolRenderProps status - let status: ToolRenderProps["status"] = "pending"; - if (exec.status === "executing") status = "executing"; - else if (exec.status === "completed") status = "completed"; - else if ( - exec.status === "error" || - exec.status === "failed" || - exec.status === "rejected" - ) - status = "error"; - - const renderProps: ToolRenderProps = { - status, - args: exec.args, - result: exec.result, - error: exec.error, - toolCallId: exec.id, - toolName: exec.name, - }; - const output = toolDef.render(renderProps) as React.ReactNode; - return {output}; - } - - // Shouldn't reach here since we filtered, but fallback - return null; - })} -
- )} - - {/* Tool Steps (default display for tools without custom renderers) */} - {toolSteps && toolSteps.length > 0 && ( -
- -
- )} + toolCallId: exec.id, + toolName: exec.name, + }; + const output = toolDef.render( + renderProps, + ) as React.ReactNode; + return ( + {output} + ); + } - {/* Tool Approval Confirmations - Priority: toolRenderers > tool.render > default */} - {pendingApprovalTools && pendingApprovalTools.length > 0 && ( -
- {pendingApprovalTools.map((tool) => { - // Approval callbacks for custom renders - const approvalCallbacks = { - onApprove: (extraData?: Record) => - onApproveToolExecution?.(tool.id, extraData), - onReject: (reason?: string) => - onRejectToolExecution?.(tool.id, reason), - message: tool.approvalMessage, - }; + // Shouldn't reach here since we filtered, but fallback + return null; + })} +
+ )} - // PRIORITY 1: toolRenderers (app-level override) - const CustomRenderer = toolRenderers?.[tool.name]; - if (CustomRenderer) { - return ( - - ); - } + {/* Tool Steps (default display for tools without custom renderers) */} + {toolSteps && toolSteps.length > 0 && ( +
+ +
+ )} - // PRIORITY 2: tool's own render function - const toolDef = registeredTools?.find( - (t) => t.name === tool.name, - ); - if (toolDef?.render) { - const renderProps: ToolRenderProps = { - status: "approval-required", - args: tool.args, - result: tool.result, - error: tool.error, - toolCallId: tool.id, - toolName: tool.name, - approval: approvalCallbacks, - }; - const output = toolDef.render(renderProps) as React.ReactNode; - return {output}; - } + {/* Tool Approval Confirmations - Priority: toolRenderers > tool.render > default */} + {pendingApprovalTools && pendingApprovalTools.length > 0 && ( +
+ {pendingApprovalTools.map((tool) => { + // Approval callbacks for custom renders + const approvalCallbacks = { + onApprove: (extraData?: Record) => + onApproveToolExecution?.(tool.id, extraData), + onReject: (reason?: string) => + onRejectToolExecution?.(tool.id, reason), + message: tool.approvalMessage, + }; - // PRIORITY 3: Default PermissionConfirmation - return ( - + ); } - onApprove={(permissionLevel) => - onApproveToolExecution?.( - tool.id, - undefined, - permissionLevel, - ) - } - onReject={(permissionLevel) => - onRejectToolExecution?.(tool.id, undefined, permissionLevel) + + // PRIORITY 2: tool's own render function + const toolDef = registeredTools?.find( + (t) => t.name === tool.name, + ); + if (toolDef?.render) { + const renderProps: ToolRenderProps = { + status: "approval-required", + args: tool.args, + result: tool.result, + error: tool.error, + toolCallId: tool.id, + toolName: tool.name, + approval: approvalCallbacks, + }; + const output = toolDef.render( + renderProps, + ) as React.ReactNode; + return ( + {output} + ); } - /> - ); - })} -
- )} - {/* Image Attachments */} - {message.attachments && message.attachments.length > 0 && ( -
- {message.attachments.map((attachment, index) => ( - - ))} -
- )} + // PRIORITY 3: Default PermissionConfirmation + return ( + + onApproveToolExecution?.( + tool.id, + undefined, + permissionLevel, + ) + } + onReject={(permissionLevel) => + onRejectToolExecution?.( + tool.id, + undefined, + permissionLevel, + ) + } + /> + ); + })} +
+ )} - {/* Follow-up Questions */} - {shouldShowFollowUps && ( - + {/* Image Attachments */} + {message.attachments && message.attachments.length > 0 && ( +
+ {message.attachments.map((attachment, index) => ( + + ))} +
+ )} + + {/* Follow-up Questions */} + {shouldShowFollowUps && ( + + )} + )}
diff --git a/packages/copilot-sdk/src/ui/components/composed/chat/types.ts b/packages/copilot-sdk/src/ui/components/composed/chat/types.ts index eb52d65..5928c65 100644 --- a/packages/copilot-sdk/src/ui/components/composed/chat/types.ts +++ b/packages/copilot-sdk/src/ui/components/composed/chat/types.ts @@ -267,14 +267,25 @@ export type ChatProps = { userAvatar?: { src?: string; fallback?: string; + /** Custom avatar component - when provided, replaces the default avatar */ + component?: React.ReactNode; }; /** Assistant avatar config */ assistantAvatar?: { src?: string; fallback?: string; + /** Custom avatar component - when provided, replaces the default avatar */ + component?: React.ReactNode; }; /** Loader variant for typing indicator */ - loaderVariant?: "circular" | "classic" | "dots" | "pulse" | "typing"; + loaderVariant?: + | "dots" + | "typing" + | "wave" + | "terminal" + | "text-blink" + | "text-shimmer" + | "loading-dots"; /** Font size for messages: 'sm' (14px), 'base' (16px), 'lg' (18px) */ fontSize?: "sm" | "base" | "lg"; diff --git a/packages/copilot-sdk/src/ui/components/composed/tools/loop-progress.tsx b/packages/copilot-sdk/src/ui/components/composed/tools/loop-progress.tsx index 5106f20..dada3e7 100644 --- a/packages/copilot-sdk/src/ui/components/composed/tools/loop-progress.tsx +++ b/packages/copilot-sdk/src/ui/components/composed/tools/loop-progress.tsx @@ -2,7 +2,7 @@ import React from "react"; import { cn } from "../../../lib/utils"; -import { CircularLoader } from "../../ui/loader"; +import { TypingLoader } from "../../ui/loader"; /** * Props for LoopProgress @@ -95,7 +95,7 @@ export function LoopProgress({ {showLabel && (
{isRunning && ( - + )} - {isRunning && } + {isRunning && } {iteration}/{maxIterations} diff --git a/packages/copilot-sdk/src/ui/components/composed/tools/tool-execution-list.tsx b/packages/copilot-sdk/src/ui/components/composed/tools/tool-execution-list.tsx index fbe03d5..9d67a59 100644 --- a/packages/copilot-sdk/src/ui/components/composed/tools/tool-execution-list.tsx +++ b/packages/copilot-sdk/src/ui/components/composed/tools/tool-execution-list.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { cn } from "../../../lib/utils"; -import { CircularLoader } from "../../ui/loader"; +import { TypingLoader } from "../../ui/loader"; /** * Tool execution status @@ -79,7 +79,7 @@ function StatusIcon({ status }: { status: ToolExecutionStatus }) {
); case "executing": - return ; + return ; case "completed": return (
diff --git a/packages/copilot-sdk/src/ui/components/ui/avatar.tsx b/packages/copilot-sdk/src/ui/components/ui/avatar.tsx index f6fc09d..e7219a5 100644 --- a/packages/copilot-sdk/src/ui/components/ui/avatar.tsx +++ b/packages/copilot-sdk/src/ui/components/ui/avatar.tsx @@ -24,7 +24,7 @@ const AvatarImage = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/packages/copilot-sdk/src/ui/components/ui/loader.tsx b/packages/copilot-sdk/src/ui/components/ui/loader.tsx index 8caf549..73c0eb8 100644 --- a/packages/copilot-sdk/src/ui/components/ui/loader.tsx +++ b/packages/copilot-sdk/src/ui/components/ui/loader.tsx @@ -1,16 +1,10 @@ import { cn } from "../../lib/utils"; -import React from "react"; export interface LoaderProps { variant?: - | "circular" - | "classic" - | "pulse" - | "pulse-dot" | "dots" | "typing" | "wave" - | "bars" | "terminal" | "text-blink" | "text-shimmer" @@ -20,125 +14,6 @@ export interface LoaderProps { className?: string; } -export function CircularLoader({ - className, - size = "md", -}: { - className?: string; - size?: "sm" | "md" | "lg"; -}) { - const sizeClasses = { - sm: "size-4", - md: "size-5", - lg: "size-6", - }; - - return ( -
- Loading -
- ); -} - -export function ClassicLoader({ - className, - size = "md", -}: { - className?: string; - size?: "sm" | "md" | "lg"; -}) { - const sizeClasses = { - sm: "size-4", - md: "size-5", - lg: "size-6", - }; - - const barSizes = { - sm: { height: "6px", width: "1.5px" }, - md: { height: "8px", width: "2px" }, - lg: { height: "10px", width: "2.5px" }, - }; - - return ( -
-
- {[...Array(12)].map((_, i) => ( -
- ))} -
- Loading -
- ); -} - -export function PulseLoader({ - className, - size = "md", -}: { - className?: string; - size?: "sm" | "md" | "lg"; -}) { - const sizeClasses = { - sm: "size-4", - md: "size-5", - lg: "size-6", - }; - - return ( -
-
- Loading -
- ); -} - -export function PulseDotLoader({ - className, - size = "md", -}: { - className?: string; - size?: "sm" | "md" | "lg"; -}) { - const sizeClasses = { - sm: "size-1", - md: "size-2", - lg: "size-3", - }; - - return ( -
- Loading -
- ); -} - export function DotsLoader({ className, size = "md", @@ -147,9 +22,9 @@ export function DotsLoader({ size?: "sm" | "md" | "lg"; }) { const dotSizes = { - sm: "h-1.5 w-1.5", - md: "h-2 w-2", - lg: "h-2.5 w-2.5", + sm: "h-1 w-1", + md: "h-1.5 w-1.5", + lg: "h-2 w-2", }; const containerSizes = { @@ -161,7 +36,7 @@ export function DotsLoader({ return (
- {[...Array(3)].map((_, i) => ( -
- ))} - Loading -
- ); -} - export function TerminalLoader({ className, size = "md", @@ -352,12 +189,7 @@ export function TerminalLoader({ {">"} -
+
Loading
); @@ -381,7 +213,7 @@ export function TextBlinkLoader({ return (
- + . - + . - + . @@ -457,28 +298,18 @@ export function TextDotsLoader({ } function Loader({ - variant = "circular", + variant = "typing", size = "md", text, className, }: LoaderProps) { switch (variant) { - case "circular": - return ; - case "classic": - return ; - case "pulse": - return ; - case "pulse-dot": - return ; case "dots": return ; case "typing": return ; case "wave": return ; - case "bars": - return ; case "terminal": return ; case "text-blink": @@ -490,7 +321,7 @@ function Loader({ case "loading-dots": return ; default: - return ; + return ; } } diff --git a/packages/copilot-sdk/src/ui/components/ui/message.tsx b/packages/copilot-sdk/src/ui/components/ui/message.tsx index c19573d..a3a69ee 100644 --- a/packages/copilot-sdk/src/ui/components/ui/message.tsx +++ b/packages/copilot-sdk/src/ui/components/ui/message.tsx @@ -20,24 +20,38 @@ const Message = ({ children, className, ...props }: MessageProps) => ( ); export type MessageAvatarProps = { - src: string; - alt: string; + /** Image source URL */ + src?: string; + /** Alt text for the image */ + alt?: string; /** Text fallback (e.g. "AI") */ fallback?: string; /** Icon/component fallback (takes precedence over text fallback when src is empty) */ fallbackIcon?: React.ReactNode; + /** Custom avatar component - when provided, replaces the default avatar */ + children?: React.ReactNode; delayMs?: number; className?: string; }; const MessageAvatar = ({ src, - alt, + alt = "Avatar", fallback, fallbackIcon, + children, delayMs, className, }: MessageAvatarProps) => { + // If custom children provided, render them in a wrapper with proper sizing + if (children) { + return ( + + {children} + + ); + } + return ( diff --git a/packages/copilot-sdk/src/ui/components/ui/scroll-button.tsx b/packages/copilot-sdk/src/ui/components/ui/scroll-button.tsx index 4540726..8ae40e1 100644 --- a/packages/copilot-sdk/src/ui/components/ui/scroll-button.tsx +++ b/packages/copilot-sdk/src/ui/components/ui/scroll-button.tsx @@ -28,7 +28,7 @@ export type ScrollButtonProps = { function ScrollButton({ className, - variant = "outline", + variant = "secondary", size = "sm", ...props }: ScrollButtonProps) { @@ -39,7 +39,7 @@ function ScrollButton({ variant={variant} size={size} className={cn( - "h-10 w-10 rounded-full transition-all duration-150 ease-out", + "h-10 w-10 rounded-full transition-all duration-150 ease-out shadow-md bg-background", !isAtBottom ? "translate-y-0 scale-100 opacity-100" : "pointer-events-none translate-y-4 scale-95 opacity-0", diff --git a/packages/copilot-sdk/src/ui/styles/base.css b/packages/copilot-sdk/src/ui/styles/base.css index 82af092..08e12eb 100644 --- a/packages/copilot-sdk/src/ui/styles/base.css +++ b/packages/copilot-sdk/src/ui/styles/base.css @@ -1,64 +1,151 @@ /* YourGPT Copilot SDK - Base Styles */ -/* Default neutral theme */ +/* + * IMPORTANT: These are fallback defaults only. + * If user has their own shadcn/ui CSS variables defined on :root, + * those will be used instead (user styles should be imported AFTER this file + * or defined outside @layer to take precedence). + * + * To ensure your app's variables take precedence: + * 1. Import SDK styles first: import "@yourgpt/copilot-sdk/ui/styles.css" + * 2. Then import your globals.css with your :root variables + */ + +/* Low-priority layer for SDK defaults - user styles will override */ +@layer csdk-base { + :root { + --background: hsl(0 0% 100%); + --foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --primary: hsl(240 5.9% 10%); + --primary-foreground: hsl(0 0% 98%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --ring: hsl(240 5.9% 10%); + --radius: 0.5rem; + } + + .dark { + --background: hsl(240 10% 3.9%); + --foreground: hsl(0 0% 98%); + --card: hsl(240 10% 3.9%); + --card-foreground: hsl(0 0% 98%); + --popover: hsl(240 10% 3.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(0 0% 98%); + --primary-foreground: hsl(240 5.9% 10%); + --secondary: hsl(240 3.7% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(240 3.7% 15.9%); + --muted-foreground: hsl(240 5% 64.9%); + --accent: hsl(240 3.7% 15.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 3.7% 15.9%); + --input: hsl(240 3.7% 15.9%); + --ring: hsl(240 4.9% 83.9%); + } +} -:root { - --background: hsl(0 0% 100%); - --foreground: hsl(240 10% 3.9%); +/* Loader Animations */ +@keyframes csdk-spinner-fade { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.15; } +} - --card: hsl(0 0% 100%); - --card-foreground: hsl(240 10% 3.9%); +@keyframes csdk-thin-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.85); } +} + +@keyframes csdk-pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.75); } +} - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(240 10% 3.9%); +@keyframes csdk-bounce-dots { + 0%, 80%, 100% { transform: translateY(0); } + 40% { transform: translateY(-6px); } +} - --primary: hsl(240 5.9% 10%); - --primary-foreground: hsl(0 0% 98%); +@keyframes csdk-typing { + 0%, 60%, 100% { opacity: 0.3; transform: scale(0.8); } + 30% { opacity: 1; transform: scale(1); } +} - --secondary: hsl(240 4.8% 95.9%); - --secondary-foreground: hsl(240 5.9% 10%); +@keyframes csdk-wave { + 0%, 100% { transform: scaleY(0.5); } + 50% { transform: scaleY(1); } +} - --muted: hsl(240 4.8% 95.9%); - --muted-foreground: hsl(240 3.8% 46.1%); +@keyframes csdk-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} - --accent: hsl(240 4.8% 95.9%); - --accent-foreground: hsl(240 5.9% 10%); +@keyframes csdk-text-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} - --destructive: hsl(0 84.2% 60.2%); - --destructive-foreground: hsl(0 0% 98%); +@keyframes csdk-shimmer { + 0% { background-position: 100% center; } + 100% { background-position: -100% center; } +} - --border: hsl(240 5.9% 90%); - --input: hsl(240 5.9% 90%); - --ring: hsl(240 5.9% 10%); +@keyframes csdk-loading-dots { + 0%, 20% { opacity: 0; } + 40%, 100% { opacity: 1; } +} - --radius: 0.5rem; +/* Loader Animation Classes */ +.csdk-loader-spinner-fade { + animation: csdk-spinner-fade 1.2s linear infinite; } -.dark { - --background: hsl(240 10% 3.9%); - --foreground: hsl(0 0% 98%); +.csdk-loader-thin-pulse { + animation: csdk-thin-pulse 1.5s ease-in-out infinite; +} - --card: hsl(240 10% 3.9%); - --card-foreground: hsl(0 0% 98%); +.csdk-loader-pulse-dot { + animation: csdk-pulse-dot 1.2s ease-in-out infinite; +} - --popover: hsl(240 10% 3.9%); - --popover-foreground: hsl(0 0% 98%); +.csdk-loader-bounce-dots { + animation: csdk-bounce-dots 1.4s ease-in-out infinite; +} - --primary: hsl(0 0% 98%); - --primary-foreground: hsl(240 5.9% 10%); +.csdk-loader-typing { + animation: csdk-typing 1s infinite; +} - --secondary: hsl(240 3.7% 15.9%); - --secondary-foreground: hsl(0 0% 98%); +.csdk-loader-wave { + animation: csdk-wave 1s ease-in-out infinite; +} - --muted: hsl(240 3.7% 15.9%); - --muted-foreground: hsl(240 5% 64.9%); +.csdk-loader-blink { + animation: csdk-blink 1s step-end infinite; +} - --accent: hsl(240 3.7% 15.9%); - --accent-foreground: hsl(0 0% 98%); +.csdk-loader-text-blink { + animation: csdk-text-blink 2s ease-in-out infinite; +} - --destructive: hsl(0 62.8% 30.6%); - --destructive-foreground: hsl(0 0% 98%); +.csdk-loader-shimmer { + animation: csdk-shimmer 4s infinite linear; +} - --border: hsl(240 3.7% 15.9%); - --input: hsl(240 3.7% 15.9%); - --ring: hsl(240 4.9% 83.9%); +.csdk-loader-loading-dots { + animation: csdk-loading-dots 1.4s infinite; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09522e0..ba89a40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -539,8 +539,8 @@ importers: specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.1.18) '@yourgpt/copilot-sdk': - specifier: ^1.4.32 - version: 1.4.32(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: workspace:* + version: link:../../packages/copilot-sdk '@yourgpt/llm-sdk': specifier: ^1.5.42 version: 1.5.42(@anthropic-ai/sdk@0.71.2(zod@3.25.76))(@google/generative-ai@0.24.1)(openai@6.16.0(ws@8.18.0)(zod@3.25.76)) @@ -3696,18 +3696,6 @@ packages: cpu: [x64] os: [win32] - '@yourgpt/copilot-sdk@1.4.32': - resolution: {integrity: sha512-Pi+DAxtPUz651jptQZ7xRWvpLdranm29O7dzKLzrPDmgNFawgsWNJ4GvYzzqm9Mn1RRcx+pP1Uwm6FpsLhv3lA==} - engines: {node: '>=18'} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - '@yourgpt/llm-sdk@1.5.42': resolution: {integrity: sha512-xw+z+YZ9JwWsLrLc3QLFjN5bRiMssGoMhPkzKFCrClFDeqPPaI+N9qJ/VE9Avf+Aaw3zl8H7b76CVcdddnPJYA==} engines: {node: '>=18'} @@ -7711,20 +7699,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 - '@base-ui/react@1.0.0(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@babel/runtime': 7.28.4 - '@base-ui/utils': 0.2.3(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@floating-ui/utils': 0.2.10 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - reselect: 5.1.1 - tabbable: 6.3.0 - use-sync-external-store: 1.6.0(react@19.2.3) - optionalDependencies: - '@types/react': 18.3.27 - '@base-ui/utils@0.2.3(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -7736,17 +7710,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 - '@base-ui/utils@0.2.3(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@babel/runtime': 7.28.4 - '@floating-ui/utils': 0.2.10 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - reselect: 5.1.1 - use-sync-external-store: 1.6.0(react@19.2.3) - optionalDependencies: - '@types/react': 18.3.27 - '@changesets/apply-release-plan@7.0.14': dependencies: '@changesets/config': 3.1.2 @@ -10238,11 +10201,6 @@ snapshots: react: 18.3.1 shiki: 3.20.0 - '@streamdown/code@1.0.1(react@19.2.3)': - dependencies: - react: 19.2.3 - shiki: 3.20.0 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -10626,31 +10584,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@yourgpt/copilot-sdk@1.4.32(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@base-ui/react': 1.0.0(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-avatar': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.4(@types/react@18.3.27)(react@19.2.3) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@streamdown/code': 1.0.1(react@19.2.3) - class-variance-authority: 0.7.1 - clsx: 2.1.1 - html-to-image: 1.11.13 - html2canvas: 1.4.1 - lucide-react: 0.561.0(react@19.2.3) - streamdown: 2.1.0(react@19.2.3) - tailwind-merge: 3.4.0 - use-stick-to-bottom: 1.1.1(react@19.2.3) - zod: 3.25.76 - optionalDependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - - supports-color - '@yourgpt/llm-sdk@1.5.42(@anthropic-ai/sdk@0.71.2(zod@3.25.76))(@google/generative-ai@0.24.1)(openai@6.16.0(ws@8.18.0)(zod@3.25.76))': dependencies: hono: 4.11.0 @@ -12874,10 +12807,6 @@ snapshots: dependencies: react: 18.3.1 - lucide-react@0.561.0(react@19.2.3): - dependencies: - react: 19.2.3 - lucide-react@0.562.0(react@19.2.1): dependencies: react: 19.2.1 @@ -14682,26 +14611,6 @@ snapshots: transitivePeerDependencies: - supports-color - streamdown@2.1.0(react@19.2.3): - dependencies: - clsx: 2.1.1 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - marked: 17.0.1 - react: 19.2.3 - rehype-harden: 1.1.7 - rehype-raw: 7.0.0 - rehype-sanitize: 6.0.0 - remark-gfm: 4.0.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - remend: 1.1.0 - tailwind-merge: 3.4.0 - unified: 11.0.5 - unist-util-visit: 5.0.0 - transitivePeerDependencies: - - supports-color - strict-event-emitter@0.5.1: {} string-argv@0.3.2: {} @@ -15199,10 +15108,6 @@ snapshots: dependencies: react: 18.3.1 - use-stick-to-bottom@1.1.1(react@19.2.3): - dependencies: - react: 19.2.3 - use-sync-external-store@1.6.0(react@18.3.1): dependencies: react: 18.3.1