@datalayer/agent-runtimes 0.0.2 → 0.0.4

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.
Files changed (34) hide show
  1. package/lib/components/chat/components/AgentDetails.js +65 -2
  2. package/lib/components/chat/components/ChatFloating.d.ts +13 -3
  3. package/lib/components/chat/components/ChatFloating.js +20 -9
  4. package/lib/components/chat/components/base/ChatBase.d.ts +25 -0
  5. package/lib/components/chat/components/base/ChatBase.js +208 -18
  6. package/lib/components/chat/components/index.d.ts +1 -1
  7. package/lib/components/chat/components/parts/TextPart.js +5 -1
  8. package/lib/components/chat/index.d.ts +1 -1
  9. package/lib/components/chat/protocols/A2AAdapter.d.ts +2 -0
  10. package/lib/components/chat/protocols/A2AAdapter.js +9 -0
  11. package/lib/components/chat/protocols/ACPAdapter.d.ts +2 -0
  12. package/lib/components/chat/protocols/ACPAdapter.js +13 -0
  13. package/lib/components/chat/protocols/AGUIAdapter.d.ts +2 -0
  14. package/lib/components/chat/protocols/AGUIAdapter.js +5 -0
  15. package/lib/components/chat/protocols/BaseProtocolAdapter.d.ts +2 -0
  16. package/lib/components/chat/protocols/VercelAIAdapter.d.ts +6 -0
  17. package/lib/components/chat/protocols/VercelAIAdapter.js +10 -0
  18. package/lib/components/chat/types/protocol.d.ts +4 -0
  19. package/lib/examples/AgentRuntimeCustomExample.d.ts +2 -1
  20. package/lib/examples/AgentRuntimeCustomExample.js +120 -5
  21. package/lib/examples/AgentRuntimeLexicalExample.js +1 -1
  22. package/lib/examples/components/AgentConfiguration.d.ts +22 -0
  23. package/lib/examples/components/AgentConfiguration.js +39 -3
  24. package/lib/examples/components/index.d.ts +1 -1
  25. package/lib/hooks/useNotebookAIAgent.d.ts +2 -2
  26. package/lib/hooks/useNotebookAIAgent.js +26 -9
  27. package/lib/index.d.ts +1 -0
  28. package/lib/index.js +1 -0
  29. package/lib/state/substates/AIAgentState.d.ts +80 -10
  30. package/lib/state/substates/AIAgentState.js +89 -23
  31. package/lib/stubs/keytar.d.ts +30 -0
  32. package/lib/stubs/keytar.js +28 -0
  33. package/package.json +9 -7
  34. package/style/base.css +1 -43
@@ -5,9 +5,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * AgentDetails component - Shows detailed information about the agent
6
6
  * including name, protocol, URL, message count, and context details.
7
7
  */
8
- import { ArrowLeftIcon, GlobeIcon, CommentDiscussionIcon, DatabaseIcon, FileIcon, ToolsIcon, ClockIcon, } from '@primer/octicons-react';
9
- import { Box, Button, Heading, IconButton, Text, Label, ProgressBar, } from '@primer/react';
8
+ import { ArrowLeftIcon, GlobeIcon, CommentDiscussionIcon, DatabaseIcon, FileIcon, ToolsIcon, ClockIcon, CheckCircleIcon, XCircleIcon, } from '@primer/octicons-react';
9
+ import { Box, Button, Heading, IconButton, Text, Label, ProgressBar, Spinner, } from '@primer/react';
10
10
  import { AiAgentIcon } from '@datalayer/icons-react';
11
+ import { useQuery } from '@tanstack/react-query';
11
12
  // Mock context data for display
12
13
  const MOCK_CONTEXT_DATA = {
13
14
  name: 'Context',
@@ -51,6 +52,15 @@ const MOCK_CONTEXT_DATA = {
51
52
  },
52
53
  ],
53
54
  };
55
+ function getLocalApiBase() {
56
+ if (typeof window === 'undefined') {
57
+ return '';
58
+ }
59
+ const host = window.location.hostname;
60
+ return host === 'localhost' || host === '127.0.0.1'
61
+ ? 'http://127.0.0.1:8765'
62
+ : '';
63
+ }
54
64
  /**
55
65
  * Format token count for display
56
66
  */
@@ -85,6 +95,19 @@ function getCategoryIcon(name) {
85
95
  */
86
96
  export function AgentDetails({ name = 'AI Agent', protocol, url, messageCount, agentId, onBack, }) {
87
97
  const contextUsagePercent = (MOCK_CONTEXT_DATA.usedTokens / MOCK_CONTEXT_DATA.totalTokens) * 100;
98
+ // Fetch MCP toolsets status
99
+ const { data: mcpStatus, isLoading: mcpLoading } = useQuery({
100
+ queryKey: ['mcp-toolsets-status'],
101
+ queryFn: async () => {
102
+ const apiBase = getLocalApiBase();
103
+ const response = await fetch(`${apiBase}/api/v1/configure/mcp-toolsets-status`);
104
+ if (!response.ok) {
105
+ throw new Error('Failed to fetch MCP status');
106
+ }
107
+ return response.json();
108
+ },
109
+ refetchInterval: 5000, // Refresh every 5 seconds
110
+ });
88
111
  return (_jsxs(Box, { sx: {
89
112
  display: 'flex',
90
113
  flexDirection: 'column',
@@ -145,6 +168,46 @@ export function AgentDetails({ name = 'AI Agent', protocol, url, messageCount, a
145
168
  fontWeight: 'semibold',
146
169
  mb: 2,
147
170
  color: 'fg.muted',
171
+ }, children: "MCP Toolsets" }), _jsx(Box, { sx: {
172
+ p: 3,
173
+ bg: 'canvas.subtle',
174
+ borderRadius: 2,
175
+ border: '1px solid',
176
+ borderColor: 'border.default',
177
+ }, children: mcpLoading ? (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Loading MCP status..." })] })) : mcpStatus ? (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: _jsxs(Text, { sx: { fontSize: 1 }, children: [_jsx(Text, { as: "span", sx: { fontWeight: 'semibold' }, children: mcpStatus.ready_count }), ' ', "ready,", ' ', _jsx(Text, { as: "span", sx: { fontWeight: 'semibold' }, children: mcpStatus.failed_count }), ' ', "failed"] }) }), mcpStatus.ready_servers.length > 0 && (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsx(Text, { sx: {
178
+ fontSize: 0,
179
+ fontWeight: 'semibold',
180
+ color: 'fg.muted',
181
+ }, children: "Ready:" }), mcpStatus.ready_servers.map(server => (_jsxs(Box, { sx: {
182
+ display: 'flex',
183
+ alignItems: 'center',
184
+ gap: 2,
185
+ pl: 2,
186
+ }, children: [_jsx(CheckCircleIcon, { size: 16, fill: "success.fg" }), _jsx(Text, { sx: { fontSize: 1 }, children: server })] }, server)))] })), Object.keys(mcpStatus.failed_servers).length > 0 && (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsx(Text, { sx: {
187
+ fontSize: 0,
188
+ fontWeight: 'semibold',
189
+ color: 'fg.muted',
190
+ }, children: "Failed:" }), Object.entries(mcpStatus.failed_servers).map(([server, error]) => (_jsxs(Box, { sx: {
191
+ display: 'flex',
192
+ flexDirection: 'column',
193
+ gap: 1,
194
+ pl: 2,
195
+ }, children: [_jsxs(Box, { sx: {
196
+ display: 'flex',
197
+ alignItems: 'center',
198
+ gap: 2,
199
+ }, children: [_jsx(XCircleIcon, { size: 16, fill: "danger.fg" }), _jsx(Text, { sx: { fontSize: 1 }, children: server })] }), _jsx(Text, { sx: {
200
+ fontSize: 0,
201
+ color: 'danger.fg',
202
+ fontFamily: 'mono',
203
+ pl: 4,
204
+ whiteSpace: 'pre-wrap',
205
+ wordBreak: 'break-word',
206
+ }, children: error.split('\n')[0] })] }, server)))] }))] })) : (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Failed to load MCP status" })) })] }), _jsxs(Box, { children: [_jsx(Heading, { as: "h4", sx: {
207
+ fontSize: 1,
208
+ fontWeight: 'semibold',
209
+ mb: 2,
210
+ color: 'fg.muted',
148
211
  }, children: "Context Usage" }), _jsxs(Box, { sx: {
149
212
  p: 3,
150
213
  bg: 'canvas.subtle',
@@ -12,9 +12,9 @@
12
12
  * @module components/chat/components/ChatFloating
13
13
  */
14
14
  import React from 'react';
15
- import { type ChatBaseProps, type RenderToolResult, type ToolCallRenderContext, type ToolCallStatus, type ProtocolConfig, type RespondCallback, type Suggestion } from './base/ChatBase';
15
+ import { type ChatBaseProps, type RenderToolResult, type ToolCallRenderContext, type ToolCallStatus, type ProtocolConfig, type RespondCallback, type Suggestion, type RemoteConfig, type ModelConfig, type BuiltinTool, type MCPServerConfig, type MCPServerTool } from './base/ChatBase';
16
16
  import type { PoweredByTagProps } from './elements/PoweredByTag';
17
- export type { ToolCallStatus, ToolCallRenderContext, RenderToolResult, RespondCallback, Suggestion, };
17
+ export type { ToolCallStatus, ToolCallRenderContext, RenderToolResult, RespondCallback, Suggestion, RemoteConfig, ModelConfig, BuiltinTool, MCPServerConfig, MCPServerTool, };
18
18
  /**
19
19
  * ChatFloating props
20
20
  */
@@ -135,6 +135,16 @@ export interface ChatFloatingProps {
135
135
  * @default false
136
136
  */
137
137
  showPanelBackdrop?: boolean;
138
+ /**
139
+ * Show model selector in footer.
140
+ * @default false
141
+ */
142
+ showModelSelector?: boolean;
143
+ /**
144
+ * Show tools menu in footer.
145
+ * @default false
146
+ */
147
+ showToolsMenu?: boolean;
138
148
  /** Additional ChatBase props */
139
149
  panelProps?: Partial<ChatBaseProps>;
140
150
  }
@@ -142,5 +152,5 @@ export interface ChatFloatingProps {
142
152
  * ChatFloating component
143
153
  * A floating chat window built on ChatBase
144
154
  */
145
- export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, offset, animationDuration, renderToolResult, tools: _tools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, showPanelBackdrop, panelProps, }: ChatFloatingProps): import("react/jsx-runtime").JSX.Element;
155
+ export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, offset, animationDuration, renderToolResult, tools: _tools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, showPanelBackdrop, showModelSelector, showToolsMenu, panelProps, }: ChatFloatingProps): import("react/jsx-runtime").JSX.Element;
146
156
  export default ChatFloating;
@@ -43,7 +43,7 @@ function useIsMobile(breakpoint = 640) {
43
43
  * ChatFloating component
44
44
  * A floating chat window built on ChatBase
45
45
  */
46
- export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, poweredByProps, clickOutsideToClose = true, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor = '#7c3aed', offset = 20, animationDuration = 200, renderToolResult, tools: _tools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', showPanelBackdrop = false, panelProps, }) {
46
+ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, poweredByProps, clickOutsideToClose = true, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor = '#7c3aed', offset = 20, animationDuration = 200, renderToolResult, tools: _tools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', showPanelBackdrop = false, showModelSelector = false, showToolsMenu = false, panelProps, }) {
47
47
  // Store-based state
48
48
  const storeIsOpen = useChatOpen();
49
49
  const storeMessages = useChatMessages();
@@ -63,13 +63,24 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
63
63
  const [focusTrigger, setFocusTrigger] = useState(0);
64
64
  // Build protocol config from endpoint if not provided directly
65
65
  // Memoize to avoid creating new object on every render (which would trigger useEffect re-runs)
66
- const protocol = useMemo(() => protocolProp ||
67
- (endpoint
68
- ? {
69
- type: 'ag-ui',
70
- endpoint,
71
- }
72
- : undefined), [protocolProp, endpoint]);
66
+ const protocol = useMemo(() => {
67
+ if (protocolProp)
68
+ return protocolProp;
69
+ if (!endpoint)
70
+ return undefined;
71
+ // Extract base URL from endpoint (e.g., http://localhost:8765/api/v1/ag-ui/agent/ -> http://localhost:8765)
72
+ const baseUrl = endpoint.match(/^(https?:\/\/[^/]+)/)?.[1] || '';
73
+ return {
74
+ type: 'ag-ui',
75
+ endpoint,
76
+ // Enable config query for model/tools selector when showModelSelector or showToolsMenu is true
77
+ enableConfigQuery: showModelSelector || showToolsMenu,
78
+ // Config endpoint is at /api/v1/configure (global, not per-agent)
79
+ configEndpoint: showModelSelector || showToolsMenu
80
+ ? `${baseUrl}/api/v1/configure`
81
+ : undefined,
82
+ };
83
+ }, [protocolProp, endpoint, showModelSelector, showToolsMenu]);
73
84
  // Clear messages when endpoint/protocol changes (e.g., switching examples)
74
85
  useEffect(() => {
75
86
  clearStoreMessages();
@@ -376,6 +387,6 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
376
387
  ...poweredByProps,
377
388
  }, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
378
389
  showAvatars: true,
379
- }, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: _tools, ...panelProps, children: children }) })] }));
390
+ }, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: _tools, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, ...panelProps, children: children }) })] }));
380
391
  }
381
392
  export default ChatFloating;
@@ -124,6 +124,7 @@ export interface ModelConfig {
124
124
  id: string;
125
125
  name: string;
126
126
  builtinTools?: string[];
127
+ isAvailable?: boolean;
127
128
  }
128
129
  /**
129
130
  * Builtin tool configuration
@@ -132,12 +133,36 @@ export interface BuiltinTool {
132
133
  name: string;
133
134
  id: string;
134
135
  }
136
+ /**
137
+ * MCP Server Tool configuration
138
+ */
139
+ export interface MCPServerTool {
140
+ name: string;
141
+ description: string;
142
+ enabled: boolean;
143
+ inputSchema?: Record<string, unknown>;
144
+ }
145
+ /**
146
+ * MCP Server configuration from backend
147
+ */
148
+ export interface MCPServerConfig {
149
+ id: string;
150
+ name: string;
151
+ url?: string;
152
+ enabled: boolean;
153
+ tools: MCPServerTool[];
154
+ command?: string;
155
+ args?: string[];
156
+ isAvailable?: boolean;
157
+ transport?: string;
158
+ }
135
159
  /**
136
160
  * Remote configuration from server
137
161
  */
138
162
  export interface RemoteConfig {
139
163
  models: ModelConfig[];
140
164
  builtinTools: BuiltinTool[];
165
+ mcpServers?: MCPServerConfig[];
141
166
  }
142
167
  /**
143
168
  * Protocol configuration for ChatBase
@@ -17,11 +17,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
17
17
  */
18
18
  import { useContext } from 'react';
19
19
  import { useCallback, useEffect, useRef, useState, } from 'react';
20
- import { Heading, Text, Spinner, IconButton, Textarea, Button, ActionMenu, ActionList, LabelGroup, Label, } from '@primer/react';
20
+ import { Heading, Text, Spinner, IconButton, Textarea, Button, ActionMenu, ActionList, LabelGroup, Label, ToggleSwitch, } from '@primer/react';
21
21
  import { Box } from '@datalayer/primer-addons';
22
22
  import { AlertIcon, PlusIcon, TrashIcon, GearIcon, PersonIcon, PaperAirplaneIcon, SquareCircleIcon, ToolsIcon, AiModelIcon, } from '@primer/octicons-react';
23
23
  import { AiAgentIcon } from '@datalayer/icons-react';
24
- import { useQuery, QueryClientContext } from '@tanstack/react-query';
24
+ import { useQuery, QueryClient, QueryClientProvider, QueryClientContext, } from '@tanstack/react-query';
25
25
  import { Streamdown } from 'streamdown';
26
26
  import { PoweredByTag } from '../elements/PoweredByTag';
27
27
  import { requestAPI } from '../../handler';
@@ -29,6 +29,51 @@ import { useChatStore } from '../../store/chatStore';
29
29
  import { generateMessageId, createUserMessage, createAssistantMessage, } from '../../types/message';
30
30
  import { AGUIAdapter, A2AAdapter, VercelAIAdapter, ACPAdapter, } from '../../protocols';
31
31
  import { ToolCallDisplay } from '../display/ToolCallDisplay';
32
+ // Singleton QueryClient for ChatBase instances without external QueryClientProvider
33
+ const internalQueryClient = new QueryClient({
34
+ defaultOptions: {
35
+ queries: {
36
+ staleTime: 5 * 60 * 1000, // 5 minutes
37
+ refetchOnWindowFocus: false,
38
+ },
39
+ },
40
+ });
41
+ // Primer's default portal root ID
42
+ const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__';
43
+ /**
44
+ * Hook to ensure Primer's default portal root has a high z-index.
45
+ * This ensures dropdown menus appear above floating chat panels.
46
+ */
47
+ function useHighZIndexPortal() {
48
+ useEffect(() => {
49
+ // Set up a MutationObserver to watch for the portal root being added
50
+ const setPortalZIndex = () => {
51
+ const portalRoot = document.getElementById(PRIMER_PORTAL_ROOT_ID);
52
+ if (portalRoot) {
53
+ portalRoot.style.zIndex = '9999';
54
+ return true;
55
+ }
56
+ return false;
57
+ };
58
+ // Try immediately
59
+ if (setPortalZIndex()) {
60
+ return;
61
+ }
62
+ // If not found yet, observe for it
63
+ const observer = new MutationObserver(() => {
64
+ if (setPortalZIndex()) {
65
+ observer.disconnect();
66
+ }
67
+ });
68
+ observer.observe(document.body, {
69
+ childList: true,
70
+ subtree: true,
71
+ });
72
+ return () => {
73
+ observer.disconnect();
74
+ };
75
+ }, []);
76
+ }
32
77
  /**
33
78
  * Check if an item is a tool call message
34
79
  */
@@ -120,8 +165,29 @@ export function ChatBase({ title, showHeader = false, showLoadingIndicator = tru
120
165
  useStore: useStoreMode = true, protocol, onSendMessage, enableStreaming = false,
121
166
  // Extended props
122
167
  brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, footerContent, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools, }) {
168
+ // Check if QueryClientProvider is already available
169
+ const existingQueryClient = useContext(QueryClientContext);
170
+ // If no QueryClient is available, wrap with our internal provider
171
+ if (!existingQueryClient) {
172
+ return (_jsx(QueryClientProvider, { client: internalQueryClient, children: _jsx(ChatBaseInner, { title: title, showHeader: showHeader, showLoadingIndicator: showLoadingIndicator, showErrors: showErrors, showInput: showInput, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, className: className, loadingState: loadingState, headerActions: headerActions, useStore: useStoreMode, protocol: protocol, onSendMessage: onSendMessage, enableStreaming: enableStreaming, brandIcon: brandIcon, avatarConfig: avatarConfig, headerButtons: headerButtons, showPoweredBy: showPoweredBy, poweredByProps: poweredByProps, emptyState: emptyState, renderToolResult: renderToolResult, footerContent: footerContent, headerContent: headerContent, children: children, borderRadius: borderRadius, backgroundColor: backgroundColor, border: border, boxShadow: boxShadow, compact: compact, placeholder: placeholder, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, onClear: onClear, onMessagesChange: onMessagesChange, autoFocus: autoFocus, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, focusTrigger: focusTrigger, frontendTools: frontendTools }) }));
173
+ }
174
+ // QueryClient already available, render inner component directly
175
+ return (_jsx(ChatBaseInner, { title: title, showHeader: showHeader, showLoadingIndicator: showLoadingIndicator, showErrors: showErrors, showInput: showInput, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, className: className, loadingState: loadingState, headerActions: headerActions, useStore: useStoreMode, protocol: protocol, onSendMessage: onSendMessage, enableStreaming: enableStreaming, brandIcon: brandIcon, avatarConfig: avatarConfig, headerButtons: headerButtons, showPoweredBy: showPoweredBy, poweredByProps: poweredByProps, emptyState: emptyState, renderToolResult: renderToolResult, footerContent: footerContent, headerContent: headerContent, children: children, borderRadius: borderRadius, backgroundColor: backgroundColor, border: border, boxShadow: boxShadow, compact: compact, placeholder: placeholder, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, onClear: onClear, onMessagesChange: onMessagesChange, autoFocus: autoFocus, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, focusTrigger: focusTrigger, frontendTools: frontendTools }));
176
+ }
177
+ /**
178
+ * Inner ChatBase component - contains all the actual logic
179
+ */
180
+ function ChatBaseInner({ title, showHeader = false, showLoadingIndicator = true, showErrors = true, showInput = true, showModelSelector = false, showToolsMenu = false, className, loadingState, headerActions,
181
+ // Mode selection
182
+ useStore: useStoreMode = true, protocol, onSendMessage, enableStreaming = false,
183
+ // Extended props
184
+ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, footerContent, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools, }) {
185
+ // Ensure Primer's default portal has high z-index for ActionMenu overlays
186
+ useHighZIndexPortal();
123
187
  // Store (optional for message persistence)
124
188
  const clearStoreMessages = useChatStore(state => state.clearMessages);
189
+ // Check if protocol is A2A (doesn't support per-request model override)
190
+ const isA2AProtocol = protocol?.type === 'a2a';
125
191
  // Component state
126
192
  const [displayItems, setDisplayItems] = useState([]);
127
193
  const [isLoading, setIsLoading] = useState(false);
@@ -130,10 +196,12 @@ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, e
130
196
  const [input, setInput] = useState('');
131
197
  // Model and tools state
132
198
  const [selectedModel, setSelectedModel] = useState('');
133
- // Note: enabledTools is used for backend-defined tools from config query
199
+ // enabledTools tracks which MCP server tools are enabled
200
+ // Format: Map<serverId, Set<toolName>>
201
+ const [enabledMcpTools, setEnabledMcpTools] = useState(new Map());
202
+ // Note: legacy _enabledTools for backend-defined tools from config query
134
203
  // Frontend tools are passed via frontendTools prop
135
- const [enabledTools, setEnabledTools] = useState([]);
136
- void enabledTools; // Suppress unused warning - may be used in future
204
+ const [_enabledTools, setEnabledTools] = useState([]);
137
205
  // Config query (for protocols that support it)
138
206
  // Safely handles missing QueryClientProvider
139
207
  const configQuery = useConfigQuery(Boolean(protocol?.enableConfigQuery), protocol?.configEndpoint, protocol?.authToken);
@@ -201,14 +269,63 @@ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, e
201
269
  // Initialize model and tools when config is available
202
270
  useEffect(() => {
203
271
  if (configQuery.data && !selectedModel) {
204
- const firstModel = configQuery.data.models[0];
272
+ // Select first available model, or fallback to first model if none available
273
+ const firstAvailableModel = configQuery.data.models.find(m => m.isAvailable !== false);
274
+ const firstModel = firstAvailableModel || configQuery.data.models[0];
205
275
  if (firstModel) {
206
276
  setSelectedModel(firstModel.id);
207
277
  const allToolIds = configQuery.data.builtinTools?.map(tool => tool.id) || [];
208
278
  setEnabledTools(allToolIds);
209
279
  }
280
+ // Initialize MCP server tools - all enabled by default
281
+ if (configQuery.data.mcpServers) {
282
+ const newEnabledMcpTools = new Map();
283
+ for (const server of configQuery.data.mcpServers) {
284
+ if (server.isAvailable && server.enabled) {
285
+ const enabledToolNames = new Set(server.tools.filter(t => t.enabled).map(t => t.name));
286
+ newEnabledMcpTools.set(server.id, enabledToolNames);
287
+ }
288
+ }
289
+ setEnabledMcpTools(newEnabledMcpTools);
290
+ }
210
291
  }
211
292
  }, [configQuery.data, selectedModel]);
293
+ // Helper to toggle MCP tool enabled state
294
+ const toggleMcpTool = useCallback((serverId, toolName) => {
295
+ setEnabledMcpTools(prev => {
296
+ const newMap = new Map(prev);
297
+ const serverTools = new Set(prev.get(serverId) || []);
298
+ if (serverTools.has(toolName)) {
299
+ serverTools.delete(toolName);
300
+ }
301
+ else {
302
+ serverTools.add(toolName);
303
+ }
304
+ newMap.set(serverId, serverTools);
305
+ return newMap;
306
+ });
307
+ }, []);
308
+ // Helper to toggle all tools for a MCP server
309
+ const toggleAllMcpServerTools = useCallback((serverId, allToolNames, enable) => {
310
+ setEnabledMcpTools(prev => {
311
+ const newMap = new Map(prev);
312
+ if (enable) {
313
+ newMap.set(serverId, new Set(allToolNames));
314
+ }
315
+ else {
316
+ newMap.set(serverId, new Set());
317
+ }
318
+ return newMap;
319
+ });
320
+ }, []);
321
+ // Get all enabled MCP tool names (for sending with requests)
322
+ const getEnabledMcpToolNames = useCallback(() => {
323
+ const toolNames = [];
324
+ enabledMcpTools.forEach(tools => {
325
+ tools.forEach(toolName => toolNames.push(toolName));
326
+ });
327
+ return toolNames;
328
+ }, [enabledMcpTools]);
212
329
  // Load messages from store on mount when useStoreMode is enabled
213
330
  useEffect(() => {
214
331
  if (useStoreMode) {
@@ -499,6 +616,8 @@ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, e
499
616
  unsubscribeRef.current?.();
500
617
  adapterRef.current?.disconnect();
501
618
  };
619
+ // Note: frontendTools is accessed via ref-like closure, not as reactive dependency
620
+ // eslint-disable-next-line react-hooks/exhaustive-deps
502
621
  }, [protocol, renderToolResult, onStateUpdate, useStoreMode]);
503
622
  // Auto-scroll to bottom
504
623
  useEffect(() => {
@@ -605,14 +724,15 @@ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, e
605
724
  description: tool.description,
606
725
  parameters: tool.parameters || { type: 'object', properties: {} },
607
726
  }));
608
- if (toolsForRequest.length > 0) {
609
- console.log('[ChatBase] Sending tools to AG-UI:', toolsForRequest.map(t => t.name));
610
- }
727
+ // Get enabled MCP tool names
728
+ const enabledMcpToolNames = getEnabledMcpToolNames();
611
729
  await adapterRef.current.sendMessage(userMessage, {
612
730
  threadId: threadIdRef.current,
613
731
  messages: allMessages,
614
732
  ...(selectedModel && { model: selectedModel }),
615
733
  tools: toolsForRequest,
734
+ // Include enabled MCP tools as builtin_tools for backend
735
+ builtinTools: enabledMcpToolNames,
616
736
  });
617
737
  }
618
738
  }
@@ -639,6 +759,7 @@ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, e
639
759
  useStoreMode,
640
760
  onSendMessage,
641
761
  enableStreaming,
762
+ getEnabledMcpToolNames,
642
763
  ]);
643
764
  // Handle stop
644
765
  const handleStop = useCallback(() => {
@@ -1049,8 +1170,20 @@ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, e
1049
1170
  fontSize: 1,
1050
1171
  whiteSpace: 'pre-wrap',
1051
1172
  wordBreak: 'break-word',
1052
- }, children: getMessageText(message) })) : (_jsx(Box, { sx: { fontSize: 1, lineHeight: 1.5 }, children: _jsx(Streamdown, { children: getMessageText(message) || (isStreaming ? '...' : '') }) })) })] }) }, message.id));
1053
- }), (isLoading || isStreaming) && (_jsx(Box, { sx: {
1173
+ }, children: getMessageText(message) })) : (_jsx(Box, { sx: {
1174
+ fontSize: 1,
1175
+ lineHeight: 1.5,
1176
+ '& ul, & ol': {
1177
+ marginTop: '0.5em',
1178
+ marginBottom: '0.5em',
1179
+ paddingInlineStart: '1.25em',
1180
+ listStylePosition: 'inside',
1181
+ },
1182
+ '& li': {
1183
+ paddingInlineStart: '0.25em',
1184
+ },
1185
+ }, children: _jsx(Streamdown, { children: getMessageText(message) || (isStreaming ? '...' : '') }) })) })] }) }, message.id));
1186
+ }), showLoadingIndicator && (isLoading || isStreaming) && (_jsx(Box, { sx: {
1054
1187
  display: 'flex',
1055
1188
  alignItems: 'flex-start',
1056
1189
  px: padding,
@@ -1157,13 +1290,70 @@ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, e
1157
1290
  borderColor: 'border.default',
1158
1291
  alignItems: 'center',
1159
1292
  bg: 'canvas.subtle',
1160
- }, children: [showToolsMenu && (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { icon: ToolsIcon, "aria-label": "Tools", variant: "invisible", size: "small" }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "start", children: _jsx(ActionList, { children: _jsx(ActionList.Group, { title: "Available Tools", children: availableTools.length > 0 ? (availableTools.map(tool => (_jsxs(ActionList.Item, { disabled: true, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(Box, { sx: {
1161
- width: 8,
1162
- height: 8,
1163
- borderRadius: '50%',
1164
- backgroundColor: 'success.emphasis',
1165
- } }) }), tool.name] }, tool.id)))) : (_jsx(ActionList.Item, { disabled: true, children: _jsx(Text, { sx: { color: 'fg.muted', fontStyle: 'italic' }, children: "No tools available" }) })) }) }) })] })), showModelSelector && models.length > 0 && selectedModel && (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Button, { type: "button", variant: "invisible", size: "small", leadingVisual: AiModelIcon, children: _jsx(Text, { sx: { fontSize: 0 }, children: models.find(m => m.id === selectedModel)?.name ||
1166
- 'Select Model' }) }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "end", children: _jsx(ActionList, { selectionVariant: "single", children: models.map(modelItem => (_jsx(ActionList.Item, { selected: selectedModel === modelItem.id, onSelect: () => setSelectedModel(modelItem.id), children: modelItem.name }, modelItem.id))) }) })] }))] }))] }));
1293
+ }, children: [showToolsMenu && (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { icon: ToolsIcon, "aria-label": "Tools", variant: "invisible", size: "small" }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "start", width: "large", children: _jsx(Box, { sx: {
1294
+ maxHeight: '60vh',
1295
+ overflowY: 'auto',
1296
+ }, children: _jsx(ActionList, { children: configQuery.data?.mcpServers &&
1297
+ configQuery.data.mcpServers.length > 0 ? (configQuery.data.mcpServers.map(server => {
1298
+ const serverTools = enabledMcpTools.get(server.id);
1299
+ const allToolNames = server.tools.map(t => t.name);
1300
+ const enabledCount = serverTools?.size ?? 0;
1301
+ const allEnabled = enabledCount === allToolNames.length &&
1302
+ allToolNames.length > 0;
1303
+ return (_jsxs(ActionList.Group, { title: `${server.name}${server.isAvailable ? '' : ' (unavailable)'}`, children: [server.isAvailable &&
1304
+ server.tools.length > 0 && (_jsxs(Box, { sx: {
1305
+ display: 'flex',
1306
+ alignItems: 'center',
1307
+ justifyContent: 'space-between',
1308
+ px: 3,
1309
+ py: 2,
1310
+ borderBottom: '1px solid',
1311
+ borderColor: 'border.muted',
1312
+ }, children: [_jsxs(Text, { id: `toggle-all-${server.id}`, sx: {
1313
+ fontSize: 0,
1314
+ fontWeight: 'semibold',
1315
+ color: 'fg.muted',
1316
+ }, children: ["Enable all (", enabledCount, "/", allToolNames.length, ")"] }), _jsx(ToggleSwitch, { size: "small", checked: allEnabled, onClick: () => toggleAllMcpServerTools(server.id, allToolNames, !allEnabled), "aria-labelledby": `toggle-all-${server.id}` })] })), server.isAvailable && server.tools.length > 0 ? (server.tools.map(tool => {
1317
+ const isEnabled = serverTools?.has(tool.name) ?? false;
1318
+ return (_jsxs(Box, { sx: {
1319
+ display: 'flex',
1320
+ alignItems: 'center',
1321
+ justifyContent: 'space-between',
1322
+ px: 3,
1323
+ py: 2,
1324
+ '&:hover': {
1325
+ backgroundColor: 'canvas.subtle',
1326
+ },
1327
+ }, children: [_jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { id: `toggle-tool-${server.id}-${tool.name}`, sx: { fontWeight: 'semibold' }, children: tool.name }), tool.description && (_jsx(Text, { sx: {
1328
+ display: 'block',
1329
+ fontSize: 0,
1330
+ color: 'fg.muted',
1331
+ overflow: 'hidden',
1332
+ textOverflow: 'ellipsis',
1333
+ whiteSpace: 'nowrap',
1334
+ }, children: tool.description }))] }), _jsx(ToggleSwitch, { size: "small", checked: isEnabled, onClick: () => toggleMcpTool(server.id, tool.name), "aria-labelledby": `toggle-tool-${server.id}-${tool.name}` })] }, `${server.id}-${tool.name}`));
1335
+ })) : server.isAvailable ? (_jsx(ActionList.Item, { disabled: true, children: _jsx(Text, { sx: {
1336
+ color: 'fg.muted',
1337
+ fontStyle: 'italic',
1338
+ }, children: "No tools discovered" }) })) : (_jsx(ActionList.Item, { disabled: true, children: _jsx(Text, { sx: {
1339
+ color: 'fg.muted',
1340
+ fontStyle: 'italic',
1341
+ }, children: "Server unavailable" }) }))] }, server.id));
1342
+ })) : (_jsx(ActionList.Group, { title: "Available Tools", children: availableTools.length > 0 ? (availableTools.map(tool => (_jsxs(ActionList.Item, { disabled: true, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(Box, { sx: {
1343
+ width: 8,
1344
+ height: 8,
1345
+ borderRadius: '50%',
1346
+ backgroundColor: 'success.emphasis',
1347
+ } }) }), tool.name] }, tool.id)))) : (_jsx(ActionList.Item, { disabled: true, children: _jsx(Text, { sx: { color: 'fg.muted', fontStyle: 'italic' }, children: "No tools available" }) })) })) }) }) })] })), showModelSelector && models.length > 0 && selectedModel && (_jsxs(Box, { sx: {
1348
+ display: 'flex',
1349
+ flexDirection: 'column',
1350
+ alignItems: 'flex-end',
1351
+ }, children: [_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Button, { type: "button", variant: "invisible", size: "small", leadingVisual: AiModelIcon, disabled: isA2AProtocol, sx: isA2AProtocol
1352
+ ? { opacity: 0.5, cursor: 'not-allowed' }
1353
+ : undefined, children: _jsx(Text, { sx: { fontSize: 0 }, children: models.find(m => m.id === selectedModel)?.name ||
1354
+ 'Select Model' }) }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "end", children: _jsx(ActionList, { selectionVariant: "single", children: models.map(modelItem => (_jsxs(ActionList.Item, { selected: selectedModel === modelItem.id, onSelect: () => setSelectedModel(modelItem.id), disabled: modelItem.isAvailable === false || isA2AProtocol, sx: modelItem.isAvailable === false
1355
+ ? { color: 'fg.muted' }
1356
+ : undefined, children: [modelItem.name, modelItem.isAvailable === false && (_jsx(ActionList.Description, { variant: "block", children: "Missing API key" }))] }, modelItem.id))) }) })] }), isA2AProtocol && (_jsx(Text, { sx: { fontSize: 0, color: 'attention.fg', mt: 1 }, children: "A2A: Model set by agent config" }))] }))] }))] }));
1167
1357
  };
1168
1358
  return (_jsxs(Box, { className: className, sx: {
1169
1359
  display: 'flex',
@@ -17,5 +17,5 @@ export { MessagePart, type MessagePartProps } from './elements/MessagePart';
17
17
  export { TextPart, type TextPartProps, ReasoningPart, type ReasoningPartProps, ToolPart, type ToolPartProps, DynamicToolPart, type DynamicToolPartProps, } from './parts';
18
18
  export { ToolCallDisplay, type ToolCallDisplayProps } from './display';
19
19
  export { Chat, type ChatProps, type Transport, type Extension } from './Chat';
20
- export { ChatFloating, type ChatFloatingProps, type ToolCallRenderContext, type ToolCallStatus, type RenderToolResult, type RespondCallback, type Suggestion, } from './ChatFloating';
20
+ export { ChatFloating, type ChatFloatingProps, type ToolCallRenderContext, type ToolCallStatus, type RenderToolResult, type RespondCallback, type Suggestion, type RemoteConfig, type ModelConfig, type BuiltinTool, type MCPServerConfig, type MCPServerTool, } from './ChatFloating';
21
21
  export { ChatInline, type ChatInlineProps, type ChatInlineProtocolConfig, } from './ChatInline';
@@ -47,7 +47,11 @@ export function TextPart({ text, message, isLastPart, onRegenerate, }) {
47
47
  '& ul, & ol': {
48
48
  marginTop: '0.5em',
49
49
  marginBottom: '0.5em',
50
- paddingLeft: '1.5em',
50
+ paddingLeft: '1.2em',
51
+ marginLeft: '0.3em',
52
+ },
53
+ '& li': {
54
+ listStylePosition: 'inside',
51
55
  },
52
56
  '& code': {
53
57
  backgroundColor: 'neutral.muted',
@@ -56,6 +56,6 @@ export { BaseProtocolAdapter, AGUIAdapter, A2AAdapter, ACPAdapter, type AGUIAdap
56
56
  export { ToolExecutor, type ToolExecutionContext } from './tools';
57
57
  export { MiddlewarePipeline, createMiddleware, loggingMiddleware, createHITLMiddleware, type RequestContext, type ResponseContext, } from './middleware';
58
58
  export { ExtensionRegistry, createMessageRenderer, createActivityRenderer, createA2UIRenderer, A2UIExtensionImpl, type A2UIMessage, } from './extensions';
59
- export { ChatMessages, ChatInputPrompt, ChatSidebar, ChatStandalone, ChatBase, ToolApprovalDialog, useToolApprovalDialog, PoweredByTag, FloatingBrandButton, ChatHeader, MessagePart, TextPart, ReasoningPart, ToolPart, DynamicToolPart, ToolCallDisplay, Chat, ChatFloating, type ChatMessagesProps, type ChatInputPromptProps, type ChatSidebarProps, type ChatStandaloneProps, type MessageHandler, type ChatBaseProps, type ProtocolConfig, type ToolApprovalDialogProps, type PoweredByTagProps, type FloatingBrandButtonProps, type ChatFloatingProps, type ToolCallRenderContext, type ToolCallStatus, type RenderToolResult, type RespondCallback, type Suggestion, type ChatHeaderProps, type ConnectionState, type MessagePartProps, type TextPartProps, type ReasoningPartProps, type ToolPartProps, type DynamicToolPartProps, type ToolCallDisplayProps, type ChatProps, type Transport, type Extension, } from './components';
59
+ export { ChatMessages, ChatInputPrompt, ChatSidebar, ChatStandalone, ChatBase, ToolApprovalDialog, useToolApprovalDialog, PoweredByTag, FloatingBrandButton, ChatHeader, MessagePart, TextPart, ReasoningPart, ToolPart, DynamicToolPart, ToolCallDisplay, Chat, ChatFloating, type ChatMessagesProps, type ChatInputPromptProps, type ChatSidebarProps, type ChatStandaloneProps, type MessageHandler, type ChatBaseProps, type ProtocolConfig, type ToolApprovalDialogProps, type PoweredByTagProps, type FloatingBrandButtonProps, type ChatFloatingProps, type ToolCallRenderContext, type ToolCallStatus, type RenderToolResult, type RespondCallback, type Suggestion, type RemoteConfig, type ModelConfig, type BuiltinTool, type MCPServerConfig, type MCPServerTool, type ChatHeaderProps, type ConnectionState, type MessagePartProps, type TextPartProps, type ReasoningPartProps, type ToolPartProps, type DynamicToolPartProps, type ToolCallDisplayProps, type ChatProps, type Transport, type Extension, } from './components';
60
60
  export { requestAPI } from './handler';
61
61
  export { useKeyboardShortcuts, useChatKeyboardShortcuts, getShortcutDisplay, type KeyboardShortcut, type UseKeyboardShortcutsOptions, } from '../../hooks';
@@ -51,6 +51,8 @@ export declare class A2AAdapter extends BaseProtocolAdapter {
51
51
  tools?: ToolDefinition[];
52
52
  threadId?: string;
53
53
  metadata?: Record<string, unknown>;
54
+ /** Model to use for this request (overrides agent default) */
55
+ model?: string;
54
56
  }): Promise<void>;
55
57
  /**
56
58
  * Send tool result back
@@ -137,10 +137,19 @@ export class A2AAdapter extends BaseProtocolAdapter {
137
137
  configuration: {
138
138
  acceptedOutputModes: ['text', 'text/plain'],
139
139
  requestedExtensions: this.a2aConfig.enableA2UI ? ['a2ui'] : [],
140
+ // Model override for per-request model selection
141
+ // Note: fasta2a/pydantic-ai A2A doesn't currently support per-request model override
142
+ // The model is configured at agent creation time
143
+ ...(options?.model && { model: options.model }),
140
144
  },
145
+ // Also send model in metadata for potential future support
146
+ ...(options?.model && { metadata: { model: options.model } }),
141
147
  },
142
148
  id: taskId,
143
149
  };
150
+ if (options?.model) {
151
+ console.log('[A2AAdapter] Sending with model:', options.model, '(Note: A2A uses agent-level model, not per-request)');
152
+ }
144
153
  try {
145
154
  const response = await fetch(this.a2aConfig.baseUrl, {
146
155
  method: 'POST',
@@ -113,6 +113,8 @@ export declare class ACPAdapter extends BaseProtocolAdapter {
113
113
  tools?: ToolDefinition[];
114
114
  threadId?: string;
115
115
  metadata?: Record<string, unknown>;
116
+ /** Model to use for this request (overrides agent default) */
117
+ model?: string;
116
118
  }): Promise<void>;
117
119
  /**
118
120
  * Send tool result back through ACP