@datalayer/agent-runtimes 0.0.2 → 0.0.3

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.
@@ -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
@@ -221,9 +221,14 @@ export class ACPAdapter extends BaseProtocolAdapter {
221
221
  .filter(c => c.type === 'text')
222
222
  .map(c => c.text || '')
223
223
  .join('');
224
+ if (_options?.model) {
225
+ console.log('[ACPAdapter] Sending with model:', _options.model);
226
+ }
224
227
  await this.sendRequest(AGENT_METHODS.session_prompt, {
225
228
  sessionId: this.session.sessionId,
226
229
  content: [{ type: 'text', text: content }],
230
+ // Include model for per-request model override
231
+ ...(_options?.model && { metadata: { model: _options.model } }),
227
232
  });
228
233
  }
229
234
  catch (error) {
@@ -450,6 +455,14 @@ export class ACPAdapter extends BaseProtocolAdapter {
450
455
  timestamp: new Date(),
451
456
  });
452
457
  }
458
+ // Handle error events - emit as error, not as message
459
+ if (params.error) {
460
+ this.emit({
461
+ type: 'error',
462
+ error: new Error(params.error),
463
+ timestamp: new Date(),
464
+ });
465
+ }
453
466
  }
454
467
  /**
455
468
  * Handle permission request from agent
@@ -52,6 +52,8 @@ export declare class AGUIAdapter extends BaseProtocolAdapter {
52
52
  metadata?: Record<string, unknown>;
53
53
  /** Full conversation history to send with the message */
54
54
  messages?: ChatMessage[];
55
+ /** Model to use for this request (overrides agent default) */
56
+ model?: string;
55
57
  }): Promise<void>;
56
58
  /**
57
59
  * Send tool result back through AG-UI and continue the conversation
@@ -135,7 +135,12 @@ export class AGUIAdapter extends BaseProtocolAdapter {
135
135
  tools: options?.tools || [],
136
136
  context: [],
137
137
  forwardedProps: null,
138
+ // Include model for per-request model override
139
+ ...(options?.model && { model: options.model }),
138
140
  };
141
+ if (options?.model) {
142
+ console.log('[AGUIAdapter] Sending with model:', options.model);
143
+ }
139
144
  try {
140
145
  const response = await fetch(this.aguiConfig.baseUrl, {
141
146
  method: 'POST',
@@ -35,6 +35,8 @@ export declare abstract class BaseProtocolAdapter implements ProtocolAdapter {
35
35
  metadata?: Record<string, unknown>;
36
36
  /** Full conversation history to send with the message */
37
37
  messages?: ChatMessage[];
38
+ /** Model to use for this request (overrides agent default) */
39
+ model?: string;
38
40
  }): Promise<void>;
39
41
  /**
40
42
  * Send tool execution result back
@@ -57,6 +57,10 @@ export declare class VercelAIAdapter extends BaseProtocolAdapter {
57
57
  tools?: ToolDefinition[];
58
58
  threadId?: string;
59
59
  metadata?: Record<string, unknown>;
60
+ /** Model to use for this request (overrides agent default) */
61
+ model?: string;
62
+ /** Full conversation history to send with the message */
63
+ messages?: ChatMessage[];
60
64
  }): Promise<void>;
61
65
  /**
62
66
  * Parse SSE stream from Vercel AI
@@ -105,7 +105,12 @@ export class VercelAIAdapter extends BaseProtocolAdapter {
105
105
  trigger: 'submit-message',
106
106
  // Optional fields based on Pydantic AI's Vercel adapter
107
107
  ...(options?.tools && { tools: options.tools }),
108
+ // Model override for per-request model selection
109
+ ...(options?.model && { model: options.model }),
108
110
  };
111
+ if (options?.model) {
112
+ console.log('[VercelAIAdapter] Sending with model:', options.model);
113
+ }
109
114
  // Merge custom headers with defaults
110
115
  const headers = {
111
116
  'Content-Type': 'application/json',
@@ -115,6 +115,8 @@ export interface ProtocolAdapter {
115
115
  metadata?: Record<string, unknown>;
116
116
  /** Full conversation history to send with the message */
117
117
  messages?: ChatMessage[];
118
+ /** Model to use for this request (overrides agent default) */
119
+ model?: string;
118
120
  }): Promise<void>;
119
121
  /**
120
122
  * Send tool execution result back
@@ -161,6 +163,8 @@ export declare namespace AGUI {
161
163
  content: string;
162
164
  }>;
163
165
  forwardedProps: Record<string, unknown> | null;
166
+ /** Optional model override for per-request model selection */
167
+ model?: string;
164
168
  }
165
169
  interface Event {
166
170
  type: string;
@@ -239,7 +239,7 @@ function LexicalWithChat({ content, serviceManager, }) {
239
239
  color: 'danger.fg',
240
240
  borderRadius: 2,
241
241
  maxWidth: 300,
242
- }, children: [_jsx("strong", { children: "Error:" }), " ", error] })), isReady && (_jsx(ChatFloating, { endpoint: AG_UI_ENDPOINT, title: "Lexical AI Agent Runtime", description: "Hi! I can help you edit documents. Try: 'Insert a heading', 'Add a code block', or 'Create a list'", defaultOpen: true, defaultViewMode: "panel", position: "bottom-right", brandColor: "#7c3aed", tools: tools, useStore: false, suggestions: [
242
+ }, children: [_jsx("strong", { children: "Error:" }), " ", error] })), isReady && (_jsx(ChatFloating, { endpoint: AG_UI_ENDPOINT, title: "Lexical AI Agent Runtime", description: "Hi! I can help you edit documents. Try: 'Insert a heading', 'Add a code block', or 'Create a list'", defaultOpen: true, defaultViewMode: "panel", position: "bottom-right", brandColor: "#7c3aed", tools: tools, useStore: false, showModelSelector: true, showToolsMenu: true, suggestions: [
243
243
  {
244
244
  title: 'Insert heading',
245
245
  message: 'Insert a heading that says "Welcome"',
@@ -1,6 +1,28 @@
1
1
  import React from 'react';
2
2
  import type { Agent } from '../stores/examplesStore';
3
3
  import type { Transport, Extension } from '../../components/chat';
4
+ /**
5
+ * MCP Server Tool type
6
+ */
7
+ export interface MCPServerTool {
8
+ name: string;
9
+ description?: string;
10
+ enabled: boolean;
11
+ }
12
+ /**
13
+ * MCP Server configuration from backend
14
+ */
15
+ export interface MCPServerConfig {
16
+ id: string;
17
+ name: string;
18
+ url?: string;
19
+ enabled: boolean;
20
+ tools: MCPServerTool[];
21
+ command?: string;
22
+ args?: string[];
23
+ isAvailable?: boolean;
24
+ transport?: string;
25
+ }
4
26
  type AgentLibrary = 'pydantic-ai' | 'langchain' | 'jupyter-ai';
5
27
  export type { AgentLibrary };
6
28
  export type { Transport };
@@ -1,5 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Text, TextInput, Button, FormControl, Select, CheckboxGroup, Checkbox, Spinner, Flash, } from '@primer/react';
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ import React from 'react';
7
+ import { Text, TextInput, Button, FormControl, Select, Checkbox, Spinner, Flash, Label, ActionList, } from '@primer/react';
8
+ import { CheckIcon, XIcon, ToolsIcon } from '@primer/octicons-react';
9
+ import { useQuery } from '@tanstack/react-query';
3
10
  import { Box } from '@datalayer/primer-addons';
4
11
  const AGENT_LIBRARIES = [
5
12
  {
@@ -65,6 +72,21 @@ const EXTENSIONS = [
65
72
  * Form for configuring agent connection settings.
66
73
  */
67
74
  export const AgentConfiguration = ({ agentLibrary, transport, extensions, wsUrl, baseUrl, agentName, agents, selectedAgentId, isCreatingAgent = false, createError = null, onAgentLibraryChange, onTransportChange, onExtensionsChange, onWsUrlChange, onBaseUrlChange, onAgentNameChange, onAgentSelect, onConnect, }) => {
75
+ // Fetch MCP servers configuration from the backend
76
+ const configQuery = useQuery({
77
+ queryKey: ['agent-config', baseUrl],
78
+ queryFn: async () => {
79
+ const response = await fetch(`${baseUrl}/api/v1/configure`);
80
+ if (!response.ok) {
81
+ throw new Error('Failed to fetch configuration');
82
+ }
83
+ return response.json();
84
+ },
85
+ enabled: !!baseUrl,
86
+ staleTime: 1000 * 60 * 5, // 5 minutes
87
+ retry: 1,
88
+ });
89
+ const mcpServers = configQuery.data?.mcpServers || [];
68
90
  // Determine which extensions are enabled based on transport
69
91
  const isExtensionEnabled = (ext) => {
70
92
  if (selectedAgentId !== 'new-agent')
@@ -95,7 +117,7 @@ export const AgentConfiguration = ({ agentLibrary, transport, extensions, wsUrl,
95
117
  fontWeight: 'bold',
96
118
  display: 'block',
97
119
  marginBottom: 3,
98
- }, children: "Connection Settings" }), _jsxs(FormControl, { sx: { marginBottom: 3 }, children: [_jsx(FormControl.Label, { children: "Available Agents" }), _jsxs(Select, { value: selectedAgentId, onChange: e => onAgentSelect(e.target.value), sx: { width: '100%' }, children: [_jsx(Select.Option, { value: "new-agent", children: "+ New Agent..." }), agents.map(agent => (_jsxs(Select.Option, { value: agent.id, children: [agent.status === 'running' && '● ', agent.name] }, agent.id)))] }), _jsx(FormControl.Caption, { children: selectedAgentId === 'new-agent'
120
+ }, children: "Create a new Agent" }), _jsxs(FormControl, { sx: { marginBottom: 3 }, children: [_jsx(FormControl.Label, { children: "Available Agents" }), _jsxs(Select, { value: selectedAgentId, onChange: e => onAgentSelect(e.target.value), sx: { width: '100%' }, children: [_jsx(Select.Option, { value: "new-agent", children: "+ New Agent..." }), agents.map(agent => (_jsxs(Select.Option, { value: agent.id, children: [agent.status === 'running' && '● ', agent.name] }, agent.id)))] }), _jsx(FormControl.Caption, { children: selectedAgentId === 'new-agent'
99
121
  ? 'Configure a new custom agent'
100
122
  : 'Selected agent - form fields below are disabled' })] }), _jsxs(Box, { sx: { display: 'flex', gap: 3, marginBottom: 3 }, children: [_jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "Agent Library" }), _jsx(Select, { value: agentLibrary, onChange: e => onAgentLibraryChange(e.target.value), disabled: selectedAgentId !== 'new-agent', sx: { width: '100%' }, children: AGENT_LIBRARIES.map(lib => (_jsxs(Select.Option, { value: lib.value, disabled: lib.disabled, children: [lib.label, lib.disabled && ' (Coming Soon)'] }, lib.value))) })] }), _jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "Transport" }), _jsx(Select, { value: transport, onChange: e => onTransportChange(e.target.value), disabled: selectedAgentId !== 'new-agent', sx: { width: '100%' }, children: TRANSPORTS.map(t => (_jsx(Select.Option, { value: t.value, children: t.label }, t.value))) })] }), _jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "Extensions" }), _jsx(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: EXTENSIONS.map(ext => (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Checkbox, { value: ext.value, checked: extensions.includes(ext.value), disabled: !isExtensionEnabled(ext.value), onChange: e => handleExtensionChange(ext.value, e.target.checked) }), _jsx(Text, { children: ext.label })] }, ext.value))) })] })] }), _jsxs(FormControl, { sx: { marginBottom: 3 }, children: [_jsx(FormControl.Label, { children: transport === 'acp' ? 'WebSocket URL' : 'Base URL' }), _jsx(TextInput, { value: transport === 'acp' ? wsUrl : baseUrl, onChange: e => transport === 'acp'
101
123
  ? onWsUrlChange(e.target.value)
@@ -103,7 +125,21 @@ export const AgentConfiguration = ({ agentLibrary, transport, extensions, wsUrl,
103
125
  ? 'ws://localhost:8000/api/v1/acp/ws'
104
126
  : 'http://localhost:8000', sx: { width: '100%' } }), _jsx(FormControl.Caption, { children: transport === 'acp'
105
127
  ? 'The WebSocket endpoint of your agent-runtimes server'
106
- : 'The base URL of your agent-runtimes server' })] }), _jsxs(FormControl, { sx: { marginBottom: 3 }, children: [_jsx(FormControl.Label, { children: "Agent Name" }), _jsx(TextInput, { value: agentName, onChange: e => onAgentNameChange(e.target.value), disabled: selectedAgentId !== 'new-agent', placeholder: "demo-agent", sx: { width: '100%' } }), _jsx(FormControl.Caption, { children: "The name of the agent to connect to" })] }), _jsxs(CheckboxGroup, { sx: { marginBottom: 3 }, disabled: true, children: [_jsx(CheckboxGroup.Label, { children: "MCP Servers (Coming Soon)" }), _jsx(CheckboxGroup.Caption, { children: "Select MCP servers to connect to" }), _jsxs(FormControl, { disabled: true, children: [_jsx(Checkbox, { value: "github", defaultChecked: true, disabled: true }), _jsx(FormControl.Label, { children: "GitHub" })] }), _jsxs(FormControl, { disabled: true, children: [_jsx(Checkbox, { value: "anaconda", disabled: true }), _jsx(FormControl.Label, { children: "Anaconda" })] }), _jsxs(FormControl, { disabled: true, children: [_jsx(Checkbox, { value: "tavily", disabled: true }), _jsx(FormControl.Label, { children: "Tavily" })] })] }), createError && (_jsx(Flash, { variant: "danger", sx: { marginBottom: 3 }, children: createError })), _jsx(Button, { variant: "primary", onClick: onConnect, disabled: isCreatingAgent ||
128
+ : 'The base URL of your agent-runtimes server' })] }), _jsxs(FormControl, { sx: { marginBottom: 3 }, children: [_jsx(FormControl.Label, { children: "Agent Name" }), _jsx(TextInput, { value: agentName, onChange: e => onAgentNameChange(e.target.value), disabled: selectedAgentId !== 'new-agent', placeholder: "demo-agent", sx: { width: '100%' } }), _jsx(FormControl.Caption, { children: "The name of the agent to connect to" })] }), _jsxs(Box, { sx: {
129
+ marginBottom: 3,
130
+ padding: 3,
131
+ border: '1px solid',
132
+ borderColor: 'border.default',
133
+ borderRadius: 2,
134
+ backgroundColor: 'canvas.default',
135
+ }, children: [_jsxs(Box, { sx: {
136
+ display: 'flex',
137
+ alignItems: 'center',
138
+ gap: 2,
139
+ marginBottom: 2,
140
+ }, children: [_jsx(ToolsIcon, { size: 16 }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold' }, children: "MCP Servers" }), configQuery.isLoading && _jsx(Spinner, { size: "small" })] }), configQuery.isError && (_jsx(Flash, { variant: "warning", sx: { marginBottom: 2 }, children: _jsx(Text, { sx: { fontSize: 0 }, children: "Unable to fetch MCP servers. Check that the server is running." }) })), mcpServers.length === 0 &&
141
+ !configQuery.isLoading &&
142
+ !configQuery.isError && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "No MCP servers configured." })), mcpServers.length > 0 && (_jsx(ActionList, { children: mcpServers.map((server, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(ActionList.Divider, {}), _jsxs(ActionList.Item, { disabled: true, children: [_jsx(ActionList.LeadingVisual, { children: server.isAvailable ? (_jsx(CheckIcon, { size: 16 })) : (_jsx(XIcon, { size: 16 })) }), _jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: server.name }), _jsx(Label, { variant: server.isAvailable ? 'success' : 'secondary', size: "small", children: server.isAvailable ? 'Available' : 'Not Available' })] }), server.tools.length > 0 && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Tools: ", server.tools.map(t => t.name).join(', ')] }))] })] })] }, server.id))) }))] }), createError && (_jsx(Flash, { variant: "danger", sx: { marginBottom: 3 }, children: createError })), _jsx(Button, { variant: "primary", onClick: onConnect, disabled: isCreatingAgent ||
107
143
  !agentName ||
108
144
  (transport === 'acp' ? !wsUrl : !baseUrl), sx: { width: '100%' }, children: isCreatingAgent ? (_jsxs(Box, { sx: {
109
145
  display: 'flex',
@@ -8,4 +8,4 @@ export { TimeTravel } from './TimeTravel';
8
8
  export { LexicalEditor } from './LexicalEditor';
9
9
  export { Rating } from './Rating';
10
10
  export { AgentConfiguration, AGENT_LIBRARIES, TRANSPORTS, EXTENSIONS, } from './AgentConfiguration';
11
- export type { AgentLibrary, Transport, Extension } from './AgentConfiguration';
11
+ export type { AgentLibrary, Transport, Extension, MCPServerConfig, MCPServerTool, } from './AgentConfiguration';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Stub for keytar module in browser environments.
3
+ * keytar is a native Node.js module for system keychain access
4
+ * and cannot run in the browser.
5
+ */
6
+ export declare const getPassword: (_service: string, _account: string) => Promise<string | null>;
7
+ export declare const setPassword: (_service: string, _account: string, _password: string) => Promise<void>;
8
+ export declare const deletePassword: (_service: string, _account: string) => Promise<boolean>;
9
+ export declare const findPassword: (_service: string) => Promise<string | null>;
10
+ export declare const findCredentials: (_service: string) => Promise<Array<{
11
+ account: string;
12
+ password: string;
13
+ }>>;
14
+ export declare const getPasswordSync: (_service: string, _account: string) => string | null;
15
+ export declare const setPasswordSync: (_service: string, _account: string, _password: string) => void;
16
+ export declare const deletePasswordSync: (_service: string, _account: string) => boolean;
17
+ declare const _default: {
18
+ getPassword: (_service: string, _account: string) => Promise<string | null>;
19
+ setPassword: (_service: string, _account: string, _password: string) => Promise<void>;
20
+ deletePassword: (_service: string, _account: string) => Promise<boolean>;
21
+ findPassword: (_service: string) => Promise<string | null>;
22
+ findCredentials: (_service: string) => Promise<Array<{
23
+ account: string;
24
+ password: string;
25
+ }>>;
26
+ getPasswordSync: (_service: string, _account: string) => string | null;
27
+ setPasswordSync: (_service: string, _account: string, _password: string) => void;
28
+ deletePasswordSync: (_service: string, _account: string) => boolean;
29
+ };
30
+ export default _default;
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Stub for keytar module in browser environments.
7
+ * keytar is a native Node.js module for system keychain access
8
+ * and cannot run in the browser.
9
+ */
10
+ export const getPassword = async (_service, _account) => null;
11
+ export const setPassword = async (_service, _account, _password) => { };
12
+ export const deletePassword = async (_service, _account) => false;
13
+ export const findPassword = async (_service) => null;
14
+ export const findCredentials = async (_service) => [];
15
+ // Sync versions (if any code tries to use them)
16
+ export const getPasswordSync = (_service, _account) => null;
17
+ export const setPasswordSync = (_service, _account, _password) => { };
18
+ export const deletePasswordSync = (_service, _account) => false;
19
+ export default {
20
+ getPassword,
21
+ setPassword,
22
+ deletePassword,
23
+ findPassword,
24
+ findCredentials,
25
+ getPasswordSync,
26
+ setPasswordSync,
27
+ deletePasswordSync,
28
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -65,7 +65,7 @@
65
65
  "clean:dist": "rimraf dist",
66
66
  "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
67
67
  "create:patches": "bash scripts/create-patches.sh",
68
- "examples": "run-p jupyter:start server:start examples:vite",
68
+ "examples": "run-p server:start examples:vite",
69
69
  "examples:fresh": "npm run clean:cache && npm run examples",
70
70
  "examples:nextjs": "npm run dev --workspace=nextjs-notebook-example",
71
71
  "examples:vite": "VITE_APP_TARGET=examples VITE_DATALAYER_RUN_URL=http://localhost:8888 vite",
@@ -118,9 +118,9 @@
118
118
  "@ag-ui/encoder": "^0.0.42",
119
119
  "@ag-ui/proto": "^0.0.42",
120
120
  "@agentclientprotocol/sdk": "^0.8.0",
121
- "@ai-sdk/react": "^3.0.5",
121
+ "@ai-sdk/react": "3.0.0-beta.172",
122
122
  "@anthropic-ai/sdk": "^0.52.0",
123
- "@datalayer/core": "^0.0.20",
123
+ "@datalayer/core": "^0.0.24",
124
124
  "@datalayer/icons-react": "^1.0.6",
125
125
  "@datalayer/jupyter-lexical": "^1.0.6",
126
126
  "@datalayer/jupyter-react": "^2.0.0",
@@ -281,6 +281,7 @@
281
281
  "vitest": "^3.2.4"
282
282
  },
283
283
  "resolutions": {
284
+ "@ai-sdk/react": "3.0.0-beta.172",
284
285
  "@microsoft/fast-colors": "5.3.1",
285
286
  "@microsoft/fast-components": "2.30.6",
286
287
  "@microsoft/fast-element": "1.13.0",
@@ -297,6 +298,7 @@
297
298
  "typescript": "^5.8.3"
298
299
  },
299
300
  "overrides": {
301
+ "@ai-sdk/react": "3.0.0-beta.172",
300
302
  "@microsoft/fast-colors": "5.3.1",
301
303
  "@microsoft/fast-components": "2.30.6",
302
304
  "@microsoft/fast-element": "1.13.0",
package/style/base.css CHANGED
@@ -2,6 +2,7 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+
5
6
  /*
6
7
  ---break---
7
8
  */
@@ -64,49 +65,6 @@ body {
64
65
  /* --jp-ui-font-size1: 12px; */
65
66
  }
66
67
 
67
- .Primer_Brand__Hero-module__Hero___EM3jf {
68
- padding-top: 88px;
69
- }
70
-
71
- #dla-hero .Primer_Brand__Heading-module__Heading--1___Ufc7G {
72
- font-size: 62px;
73
- }
74
-
75
- #dla-hero .Primer_Brand__Grid-module__Grid___q48mT {
76
- grid-column-gap: 12px !important;
77
- }
78
-
79
- #dla-hero .Primer_Brand__Text-module__Text___pecHN {
80
- max-width: 1000px;
81
- padding-right: 15px;
82
- }
83
-
84
- #dla-hero .Primer_Brand__Text-module__Text___pecHN {
85
- line-height: 16px;
86
- }
87
-
88
- /* TODO Check the specificity of this rule */
89
- .jp-ThemedContainer a {
90
- color: var(--jp-brand-color1, #1976d2) !important;
91
- text-decoration: none !important
92
- }
93
-
94
- .jp-Shout-button {
95
- height: 30px;
96
- width: 150px;
97
- margin: 8px;
98
- padding-top: 9px;
99
- text-align: center;
100
- vertical-align: middle;
101
- border-radius: 2px;
102
- background-color: #2296f3;
103
- color: #212121;
104
- }
105
-
106
- .jp-Shout-summary {
107
- margin: 4px;
108
- }
109
-
110
68
  /*
111
69
  ---break---
112
70
  */