@datalayer/agent-runtimes 1.0.5 → 1.0.7

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 (111) hide show
  1. package/README.md +157 -10
  2. package/lib/AgentNode.d.ts +3 -0
  3. package/lib/AgentNode.js +676 -0
  4. package/lib/agent-node/themeStore.d.ts +3 -0
  5. package/lib/agent-node/themeStore.js +156 -0
  6. package/lib/agent-node-main.d.ts +1 -0
  7. package/lib/agent-node-main.js +14 -0
  8. package/lib/chat/Chat.js +48 -27
  9. package/lib/chat/ChatFloating.js +1 -1
  10. package/lib/chat/ChatSidebar.js +81 -49
  11. package/lib/chat/base/ChatBase.js +388 -74
  12. package/lib/chat/display/FloatingBrandButton.js +8 -1
  13. package/lib/chat/header/ChatHeader.d.ts +3 -1
  14. package/lib/chat/header/ChatHeader.js +15 -12
  15. package/lib/chat/header/ChatHeaderBase.d.ts +29 -9
  16. package/lib/chat/header/ChatHeaderBase.js +26 -3
  17. package/lib/chat/indicators/SandboxStatusIndicator.js +82 -47
  18. package/lib/chat/messages/ChatMessageList.js +46 -1
  19. package/lib/chat/messages/ChatMessages.js +6 -2
  20. package/lib/chat/prompt/InputFooter.d.ts +3 -1
  21. package/lib/chat/prompt/InputFooter.js +8 -5
  22. package/lib/chat/prompt/InputPrompt.d.ts +3 -1
  23. package/lib/chat/prompt/InputPrompt.js +2 -2
  24. package/lib/chat/prompt/InputPromptFooter.d.ts +3 -1
  25. package/lib/chat/prompt/InputPromptFooter.js +3 -3
  26. package/lib/client/AgentsMixin.js +14 -0
  27. package/lib/config/AgentConfiguration.d.ts +22 -0
  28. package/lib/config/AgentConfiguration.js +319 -64
  29. package/lib/examples/AgUiSharedStateExample.js +2 -1
  30. package/lib/examples/AgentCheckpointsExample.js +3 -3
  31. package/lib/examples/AgentCodemodeExample.d.ts +3 -3
  32. package/lib/examples/AgentCodemodeExample.js +24 -12
  33. package/lib/examples/AgentEvalsExample.js +330 -40
  34. package/lib/examples/AgentGuardrailsExample.js +16 -5
  35. package/lib/examples/AgentHooksExample.js +27 -9
  36. package/lib/examples/AgentInferenceProviderExample.d.ts +3 -0
  37. package/lib/examples/AgentInferenceProviderExample.js +329 -0
  38. package/lib/examples/AgentMCPExample.js +6 -5
  39. package/lib/examples/AgentMemoryExample.d.ts +1 -2
  40. package/lib/examples/AgentMemoryExample.js +71 -22
  41. package/lib/examples/AgentMonitoringExample.js +5 -5
  42. package/lib/examples/AgentNotificationsExample.d.ts +1 -2
  43. package/lib/examples/AgentNotificationsExample.js +71 -22
  44. package/lib/examples/AgentOtelExample.js +31 -40
  45. package/lib/examples/AgentOutputsExample.d.ts +1 -1
  46. package/lib/examples/AgentOutputsExample.js +67 -16
  47. package/lib/examples/AgentParametersExample.js +10 -8
  48. package/lib/examples/AgentSandboxExample.d.ts +1 -1
  49. package/lib/examples/AgentSandboxExample.js +7 -6
  50. package/lib/examples/AgentSkillsExample.js +6 -6
  51. package/lib/examples/AgentSubagentsExample.d.ts +1 -1
  52. package/lib/examples/AgentSubagentsExample.js +6 -6
  53. package/lib/examples/AgentToolApprovalsExample.js +27 -11
  54. package/lib/examples/AgentTriggersExample.js +5 -5
  55. package/lib/examples/{AgentSpecsExample.d.ts → AgentspecsExample.d.ts} +2 -2
  56. package/lib/examples/AgentspecsExample.js +1096 -0
  57. package/lib/examples/ChatCustomExample.js +6 -5
  58. package/lib/examples/ChatExample.js +6 -5
  59. package/lib/examples/Lexical2Example.js +1 -1
  60. package/lib/examples/LexicalAgentExample.js +1 -1
  61. package/lib/examples/NotebookAgentExample.js +3 -3
  62. package/lib/examples/components/ExampleWrapper.d.ts +6 -7
  63. package/lib/examples/components/ExampleWrapper.js +27 -10
  64. package/lib/examples/example-selector.js +2 -1
  65. package/lib/examples/index.d.ts +2 -1
  66. package/lib/examples/index.js +2 -1
  67. package/lib/examples/lexical/initial-content.json +6 -6
  68. package/lib/examples/main.js +56 -16
  69. package/lib/examples/utils/agentId.d.ts +1 -1
  70. package/lib/examples/utils/agentId.js +1 -1
  71. package/lib/examples/utils/useExampleAgentRuntimesUrl.d.ts +5 -0
  72. package/lib/examples/utils/useExampleAgentRuntimesUrl.js +19 -0
  73. package/lib/hooks/useAIAgentsWebSocket.js +35 -0
  74. package/lib/hooks/useAgentRuntimes.d.ts +32 -3
  75. package/lib/hooks/useAgentRuntimes.js +114 -19
  76. package/lib/index.d.ts +1 -1
  77. package/lib/specs/agents/agents.d.ts +20 -13
  78. package/lib/specs/agents/agents.js +1267 -581
  79. package/lib/specs/benchmarks.d.ts +20 -0
  80. package/lib/specs/benchmarks.js +205 -0
  81. package/lib/specs/envvars.d.ts +0 -1
  82. package/lib/specs/envvars.js +0 -11
  83. package/lib/specs/evals.d.ts +10 -9
  84. package/lib/specs/evals.js +128 -88
  85. package/lib/specs/index.d.ts +0 -1
  86. package/lib/specs/index.js +0 -1
  87. package/lib/specs/models.d.ts +0 -2
  88. package/lib/specs/models.js +0 -15
  89. package/lib/specs/skills.d.ts +0 -1
  90. package/lib/specs/skills.js +0 -18
  91. package/lib/stores/agentRuntimeStore.d.ts +5 -1
  92. package/lib/stores/agentRuntimeStore.js +22 -8
  93. package/lib/stores/conversationStore.js +2 -2
  94. package/lib/types/agents-lifecycle.d.ts +18 -0
  95. package/lib/types/agents.d.ts +6 -0
  96. package/lib/types/agentspecs.d.ts +4 -0
  97. package/lib/types/benchmarks.d.ts +43 -0
  98. package/lib/types/benchmarks.js +5 -0
  99. package/lib/types/chat.d.ts +16 -0
  100. package/lib/types/evals.d.ts +26 -17
  101. package/lib/types/index.d.ts +1 -0
  102. package/lib/types/index.js +1 -0
  103. package/package.json +9 -5
  104. package/scripts/codegen/__pycache__/generate_agents.cpython-313.pyc +0 -0
  105. package/scripts/codegen/__pycache__/generate_benchmarks.cpython-313.pyc +0 -0
  106. package/scripts/codegen/__pycache__/generate_evals.cpython-313.pyc +0 -0
  107. package/scripts/codegen/generate_agents.py +89 -43
  108. package/scripts/codegen/generate_benchmarks.py +441 -0
  109. package/scripts/codegen/generate_evals.py +94 -16
  110. package/scripts/codegen/generate_events.py +0 -1
  111. package/lib/examples/AgentSpecsExample.js +0 -694
@@ -0,0 +1,3 @@
1
+ import { type StoreApi, type UseBoundStore } from 'zustand';
2
+ import { type ThemeState } from '@datalayer/primer-addons';
3
+ export declare const useAgentNodeThemeStore: UseBoundStore<StoreApi<ThemeState>>;
@@ -0,0 +1,156 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { create } from 'zustand';
6
+ import { createJSONStorage, persist } from 'zustand/middleware';
7
+ import { themeConfigs, } from '@datalayer/primer-addons';
8
+ const STORAGE_KEY = 'agent-runtimes-agent-node-theme';
9
+ const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
10
+ const colorModeCycle = {
11
+ light: 'dark',
12
+ dark: 'auto',
13
+ auto: 'light',
14
+ };
15
+ const normalizeThemeVariant = (value) => {
16
+ // Backward compatibility: "ocean" maps to the Earth theme variant.
17
+ if (value === 'ocean') {
18
+ return 'earth';
19
+ }
20
+ if (value === 'datalayer' ||
21
+ value === 'spatial' ||
22
+ value === 'lovely' ||
23
+ value === 'matrix' ||
24
+ value === 'earth') {
25
+ return value;
26
+ }
27
+ return 'earth';
28
+ };
29
+ const cookieStorage = {
30
+ getItem: (name) => {
31
+ if (typeof document === 'undefined') {
32
+ return null;
33
+ }
34
+ const encodedName = `${encodeURIComponent(name)}=`;
35
+ const parts = document.cookie.split('; ');
36
+ for (const part of parts) {
37
+ if (part.startsWith(encodedName)) {
38
+ return decodeURIComponent(part.slice(encodedName.length));
39
+ }
40
+ }
41
+ return null;
42
+ },
43
+ setItem: (name, value) => {
44
+ if (typeof document === 'undefined') {
45
+ return;
46
+ }
47
+ document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE_SECONDS}; samesite=lax`;
48
+ },
49
+ removeItem: (name) => {
50
+ if (typeof document === 'undefined') {
51
+ return;
52
+ }
53
+ document.cookie = `${encodeURIComponent(name)}=; path=/; max-age=0; samesite=lax`;
54
+ },
55
+ };
56
+ export const useAgentNodeThemeStore = create()(persist(set => ({
57
+ // "ocean dark" default: Earth theme (ocean palette) + dark mode.
58
+ colorMode: 'dark',
59
+ theme: 'earth',
60
+ activeVariant: null,
61
+ variants: {},
62
+ toggleColorMode: () => set(state => {
63
+ const next = colorModeCycle[state.colorMode];
64
+ if (state.activeVariant && state.variants[state.activeVariant]) {
65
+ return {
66
+ colorMode: next,
67
+ variants: {
68
+ ...state.variants,
69
+ [state.activeVariant]: {
70
+ ...state.variants[state.activeVariant],
71
+ colorMode: next,
72
+ },
73
+ },
74
+ };
75
+ }
76
+ return { colorMode: next };
77
+ }),
78
+ setColorMode: (mode) => set(state => {
79
+ if (state.activeVariant && state.variants[state.activeVariant]) {
80
+ return {
81
+ colorMode: mode,
82
+ variants: {
83
+ ...state.variants,
84
+ [state.activeVariant]: {
85
+ ...state.variants[state.activeVariant],
86
+ colorMode: mode,
87
+ },
88
+ },
89
+ };
90
+ }
91
+ return { colorMode: mode };
92
+ }),
93
+ setTheme: (theme, applyDefaultColorMode = true) => set(state => {
94
+ const normalizedTheme = normalizeThemeVariant(theme);
95
+ const nextColorMode = applyDefaultColorMode
96
+ ? themeConfigs[normalizedTheme].defaultColorMode
97
+ : state.colorMode;
98
+ if (state.activeVariant && state.variants[state.activeVariant]) {
99
+ return {
100
+ theme: normalizedTheme,
101
+ colorMode: nextColorMode,
102
+ variants: {
103
+ ...state.variants,
104
+ [state.activeVariant]: {
105
+ theme: normalizedTheme,
106
+ colorMode: nextColorMode,
107
+ },
108
+ },
109
+ };
110
+ }
111
+ return { theme: normalizedTheme, colorMode: nextColorMode };
112
+ }),
113
+ registerVariants: defs => set(state => {
114
+ const merged = { ...state.variants };
115
+ for (const [key, val] of Object.entries(defs)) {
116
+ if (!merged[key]) {
117
+ merged[key] = val;
118
+ }
119
+ }
120
+ return { variants: merged };
121
+ }),
122
+ setVariant: variant => set(state => {
123
+ const variantState = state.variants[variant];
124
+ if (!variantState) {
125
+ return { activeVariant: variant };
126
+ }
127
+ return {
128
+ activeVariant: variant,
129
+ theme: normalizeThemeVariant(variantState.theme),
130
+ colorMode: variantState.colorMode,
131
+ };
132
+ }),
133
+ }), {
134
+ name: STORAGE_KEY,
135
+ version: 1,
136
+ storage: createJSONStorage(() => cookieStorage),
137
+ partialize: state => ({
138
+ colorMode: state.colorMode,
139
+ theme: state.theme,
140
+ activeVariant: state.activeVariant,
141
+ variants: state.variants,
142
+ }),
143
+ merge: (persisted, current) => {
144
+ const persistedState = (persisted ?? {});
145
+ const persistedTheme = normalizeThemeVariant(persistedState.theme);
146
+ return {
147
+ ...current,
148
+ ...persistedState,
149
+ theme: persistedTheme,
150
+ variants: {
151
+ ...current.variants,
152
+ ...(persistedState.variants ?? {}),
153
+ },
154
+ };
155
+ },
156
+ }));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ import { createRoot } from 'react-dom/client';
7
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
8
+ import { MemoryRouter } from 'react-router-dom';
9
+ import { AgentNode } from './AgentNode';
10
+ const queryClient = new QueryClient();
11
+ const rootElement = document.getElementById('root');
12
+ if (rootElement) {
13
+ createRoot(rootElement).render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(QueryClientProvider, { client: queryClient, children: _jsx(AgentNode, {}) }) }));
14
+ }
package/lib/chat/Chat.js CHANGED
@@ -18,7 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
18
  *
19
19
  * @module chat/Chat
20
20
  */
21
- import { useEffect, useMemo, useState } from 'react';
21
+ import { useEffect, useMemo, useState, useRef } from 'react';
22
22
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
23
23
  import { Text, Button, Spinner } from '@primer/react';
24
24
  import { AlertIcon, SyncIcon } from '@primer/octicons-react';
@@ -63,22 +63,25 @@ const queryClient = new QueryClient();
63
63
  * Get transport endpoint path
64
64
  */
65
65
  function getEndpointPath(protocol, agentId) {
66
+ const resolvedAgentId = typeof agentId === 'string' && agentId.trim().length > 0
67
+ ? agentId.trim()
68
+ : 'default';
66
69
  switch (protocol) {
67
70
  case 'vercel-ai':
68
- return `/api/v1/vercel-ai/${agentId}`;
71
+ return `/api/v1/vercel-ai/${resolvedAgentId}`;
69
72
  case 'vercel-ai-jupyter':
70
73
  // Jupyter server endpoint - same protocol as vercel-ai
71
74
  // Note: no leading slash - will be joined with baseUrl that may have trailing slash
72
75
  return 'agent_runtimes/chat';
73
76
  case 'ag-ui':
74
- return `/api/v1/ag-ui/${agentId}/`;
77
+ return `/api/v1/ag-ui/${resolvedAgentId}/`;
75
78
  case 'a2a':
76
79
  // A2A requires trailing slash for FastA2A compatibility
77
- return `/api/v1/a2a/agents/${agentId}/`;
80
+ return `/api/v1/a2a/agents/${resolvedAgentId}/`;
78
81
  case 'acp':
79
- return `/api/v1/acp/ws/${agentId}`;
82
+ return `/api/v1/acp/ws/${resolvedAgentId}`;
80
83
  default:
81
- return `/api/v1/agents/${agentId}/chat`;
84
+ return `/api/v1/agents/${resolvedAgentId}/chat`;
82
85
  }
83
86
  }
84
87
  /**
@@ -134,11 +137,17 @@ function getProtocolType(protocol) {
134
137
  * ```
135
138
  */
136
139
  export function Chat({ protocol: transport, extensions: _extensions, baseUrl = 'http://localhost:8765', wsUrl, agentId, authToken: authTokenProp, placeholder = 'Type your message...', title, subtitle, brandIcon, autoConnect: _autoConnect = true, streaming: _streaming = true, onMessageSent: _onMessageSent, onMessageReceived: _onMessageReceived, onDisconnect, onLogout: _onLogout, onCollapsePanel: _onCollapsePanel, className, height = '600px', showHeader = true, showNewChatButton = true, showClearButton = true, showModelSelector = true, showToolsMenu = true, showInput = true, disableInputPrompt = false, showSkillsMenu = true, codemodeEnabled = false, onToggleCodemode, showTokenUsage = true, initialModel, availableModels, mcpServers, initialSkills, clearOnMount: _clearOnMount = true, suggestions, submitOnSuggestionClick = true, description, headerContent, headerActions, autoFocus = false, identityProviders, onIdentityConnect, onIdentityDisconnect, runtimeId, historyEndpoint, pendingPrompt, errorBanner, showInformation = true, chatViewMode, onChatViewModeChange, frontendTools, onToolCallStart, onToolCallComplete, renderToolResult, hideMessagesAfterToolUI = false, contextSnapshot, mcpStatusData, codemodeStatusData, sandboxStatusData, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, }) {
140
+ const resolvedAgentId = typeof agentId === 'string' && agentId.trim().length > 0
141
+ ? agentId.trim()
142
+ : 'default';
137
143
  const [error, setError] = useState(null);
138
144
  const [isInitializing, setIsInitializing] = useState(true);
139
145
  const [showDetails, setShowDetails] = useState(false);
140
146
  const [messageCount, setMessageCount] = useState(0);
141
147
  const [focusTrigger, setFocusTrigger] = useState(0);
148
+ const renderCountRef = useRef(0);
149
+ renderCountRef.current += 1;
150
+ console.log(`[AgentRuntimes][Chat] render count: ${renderCountRef.current}`);
142
151
  // Get connected identities to pass to backend for skill execution
143
152
  const connectedIdentities = useConnectedIdentities();
144
153
  // Map identities to the format expected by ChatBase
@@ -161,8 +170,9 @@ export function Chat({ protocol: transport, extensions: _extensions, baseUrl = '
161
170
  return () => clearTimeout(timer);
162
171
  }
163
172
  }, [showDetails]);
164
- // Build protocol config based on transport
165
- const protocolConfig = useMemo(() => {
173
+ // Build protocol config based on transport.
174
+ // Keep this pure: never call React state setters inside this memo.
175
+ const protocolConfigResult = useMemo(() => {
166
176
  try {
167
177
  let endpoint;
168
178
  let authToken = authTokenProp;
@@ -201,45 +211,56 @@ export function Chat({ protocol: transport, extensions: _extensions, baseUrl = '
201
211
  break;
202
212
  }
203
213
  case 'acp': {
204
- endpoint = `${baseUrl}${getEndpointPath(transport, agentId)}`;
214
+ endpoint = `${baseUrl}${getEndpointPath(transport, resolvedAgentId)}`;
205
215
  const acpWsUrl = wsUrl ||
206
- `${baseUrl.replace('http', 'ws')}/api/v1/acp/ws/${agentId}`;
216
+ `${baseUrl.replace('http', 'ws')}/api/v1/acp/ws/${resolvedAgentId}`;
207
217
  options = { wsUrl: acpWsUrl };
208
218
  break;
209
219
  }
210
220
  default: {
211
- endpoint = `${baseUrl}${getEndpointPath(transport, agentId)}`;
221
+ endpoint = `${baseUrl}${getEndpointPath(transport, resolvedAgentId)}`;
212
222
  break;
213
223
  }
214
224
  }
215
225
  return {
216
- type: getProtocolType(transport),
217
- endpoint,
218
- agentId,
219
- authToken,
220
- options,
221
- // Enable config query for all protocols to fetch models and tools
222
- enableConfigQuery: true,
223
- // For Jupyter-based transports, use Jupyter requestAPI (configEndpoint undefined)
224
- // For FastAPI-based transports, use direct fetch to the configure endpoint
225
- configEndpoint: transport === 'vercel-ai-jupyter'
226
- ? undefined // Use Jupyter requestAPI
227
- : `${baseUrl}/api/v1/configure`,
226
+ config: {
227
+ type: getProtocolType(transport),
228
+ endpoint,
229
+ agentId: resolvedAgentId,
230
+ authToken,
231
+ options,
232
+ // Enable config query for all protocols to fetch models and tools
233
+ enableConfigQuery: true,
234
+ // For Jupyter-based transports, use Jupyter requestAPI (configEndpoint undefined)
235
+ // For FastAPI-based transports, use direct fetch to the configure endpoint
236
+ configEndpoint: transport === 'vercel-ai-jupyter'
237
+ ? undefined // Use Jupyter requestAPI
238
+ : `${baseUrl}/api/v1/configure`,
239
+ },
240
+ error: null,
228
241
  };
229
242
  }
230
243
  catch (err) {
231
244
  console.error('[Chat] Error building protocol config:', err);
232
- setError(err instanceof Error ? err.message : 'Failed to configure');
233
- return undefined;
245
+ return {
246
+ config: undefined,
247
+ error: err instanceof Error ? err.message : 'Failed to configure',
248
+ };
234
249
  }
235
- }, [transport, baseUrl, wsUrl, agentId, authTokenProp]);
250
+ }, [transport, baseUrl, wsUrl, resolvedAgentId, authTokenProp]);
251
+ const protocolConfig = protocolConfigResult.config;
236
252
  // Set initialized once protocol config is built
237
253
  useEffect(() => {
254
+ if (protocolConfigResult.error) {
255
+ setError(protocolConfigResult.error);
256
+ setIsInitializing(false);
257
+ return;
258
+ }
238
259
  if (protocolConfig) {
239
260
  setIsInitializing(false);
240
261
  setError(null);
241
262
  }
242
- }, [protocolConfig]);
263
+ }, [protocolConfig, protocolConfigResult.error]);
243
264
  // Handle reconnect
244
265
  const handleReconnect = () => {
245
266
  setError(null);
@@ -431,7 +431,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
431
431
  }, children: _jsx(ChatBase, { title: title, showHeader: showHeader, useStore: useStoreMode, protocol: protocol, autoFocus: isOpen, focusTrigger: focusTrigger, brandIcon: brandIcon || _jsx(AiAgentIcon, { colored: true, size: 20 }), headerButtons: {
432
432
  showNewChat: showNewChatButton,
433
433
  showClear: showClearButton && messages.length > 0,
434
- showSettings: showSettingsButton,
434
+ showSettings: showSettingsButton && !!onSettingsClick,
435
435
  onNewChat: handleNewChat,
436
436
  onClear: handleClear,
437
437
  onSettings: onSettingsClick,
@@ -12,10 +12,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
12
12
  * @module chat/ChatSidebar
13
13
  */
14
14
  import { useCallback, useEffect, useRef, useState } from 'react';
15
- import { IconButton, Text } from '@primer/react';
15
+ import { createPortal } from 'react-dom';
16
+ import { IconButton } from '@primer/react';
16
17
  import { Box } from '@datalayer/primer-addons';
17
18
  import { SidebarCollapseIcon, SidebarExpandIcon, XIcon, } from '@primer/octicons-react';
18
- import { AiAgentIcon } from '@datalayer/icons-react';
19
19
  import { useChatKeyboardShortcuts, getShortcutDisplay, } from '@datalayer/core/lib/hooks';
20
20
  import { ChatBase } from './base/ChatBase';
21
21
  import { useChatStore, useChatOpen, useChatMessages, } from '../stores/chatStore';
@@ -45,6 +45,37 @@ export function ChatSidebar({ title = 'Chat', protocol: protocolProp, defaultOpe
45
45
  const sidebarRef = useRef(null);
46
46
  const inputRef = useRef(null);
47
47
  const isMobile = useIsMobile();
48
+ const [desktopViewportHeight, setDesktopViewportHeight] = useState(null);
49
+ // Compute available desktop height from the element's top position so the
50
+ // sidebar always fits within the visible viewport.
51
+ useEffect(() => {
52
+ if (isMobile) {
53
+ setDesktopViewportHeight(null);
54
+ return;
55
+ }
56
+ const updateHeight = () => {
57
+ const el = sidebarRef.current;
58
+ if (!el)
59
+ return;
60
+ const rect = el.getBoundingClientRect();
61
+ const viewportHeight = window.visualViewport?.height || window.innerHeight || 0;
62
+ const bottomPadding = 4; // keep a small breathing space from viewport edge
63
+ const available = Math.max(220, Math.floor(viewportHeight - rect.top - bottomPadding));
64
+ setDesktopViewportHeight(prev => prev !== null && Math.abs(prev - available) < 2 ? prev : available);
65
+ };
66
+ updateHeight();
67
+ window.addEventListener('resize', updateHeight);
68
+ window.addEventListener('scroll', updateHeight, true);
69
+ const observer = new ResizeObserver(() => updateHeight());
70
+ if (sidebarRef.current) {
71
+ observer.observe(sidebarRef.current);
72
+ }
73
+ return () => {
74
+ window.removeEventListener('resize', updateHeight);
75
+ window.removeEventListener('scroll', updateHeight, true);
76
+ observer.disconnect();
77
+ };
78
+ }, [isMobile]);
48
79
  // Initialize open state from defaultOpen
49
80
  useEffect(() => {
50
81
  setOpen(defaultOpen);
@@ -137,58 +168,45 @@ export function ChatSidebar({ title = 'Chat', protocol: protocolProp, defaultOpe
137
168
  : SidebarExpandIcon, "aria-label": `Close sidebar${shortcutHint ? ` (${shortcutHint})` : ''}`, onClick: handleToggle, variant: "invisible", size: "small" }));
138
169
  // Collapsed state
139
170
  if (!isOpen) {
140
- return (_jsxs(Box, { ref: sidebarRef, className: className, sx: {
171
+ const collapsedLauncher = (_jsx(Box, { ref: sidebarRef, className: className, sx: {
141
172
  position: 'fixed',
142
173
  top: 12,
143
- ...(position === 'right' ? { right: 12 } : { left: 12 }),
174
+ ...(position === 'right'
175
+ ? { right: 'env(safe-area-inset-right)' }
176
+ : { left: 'env(safe-area-inset-left)' }),
144
177
  zIndex: 1001,
145
178
  display: 'flex',
146
179
  flexDirection: 'column',
147
180
  alignItems: 'center',
148
181
  gap: 2,
149
- }, children: [_jsxs(Box, { sx: { position: 'relative' }, children: [_jsx(IconButton, { icon: position === 'right' ? SidebarExpandIcon : SidebarCollapseIcon, "aria-label": `Open chat${shortcutHint ? ` (${shortcutHint})` : ''}`, onClick: handleToggle, variant: "default", size: "small", sx: {
150
- bg: 'canvas.default',
151
- border: '1px solid',
152
- borderColor: 'border.default',
153
- boxShadow: 'shadow.small',
154
- } }), messages.length > 0 && (_jsx(Box, { sx: {
155
- position: 'absolute',
156
- top: -6,
157
- right: -6,
158
- minWidth: 16,
159
- height: 16,
160
- px: 1,
161
- display: 'flex',
162
- alignItems: 'center',
163
- justifyContent: 'center',
164
- bg: 'accent.emphasis',
165
- color: 'fg.onEmphasis',
166
- borderRadius: '50%',
167
- fontSize: '10px',
168
- fontWeight: 'bold',
169
- boxShadow: 'shadow.small',
170
- }, children: messages.length > 99 ? '99+' : messages.length }))] }), _jsx(Box, { sx: {
171
- width: 32,
172
- height: 32,
173
- display: 'flex',
174
- alignItems: 'center',
175
- justifyContent: 'center',
176
- bg: 'canvas.default',
177
- border: '1px solid',
178
- borderColor: 'border.default',
179
- borderRadius: 2,
180
- boxShadow: 'shadow.small',
181
- }, children: brandIcon || _jsx(AiAgentIcon, { colored: true, size: 20 }) }), shortcutHint && (_jsx(Box, { sx: {
182
- px: 1,
183
- py: '2px',
184
- bg: 'canvas.default',
185
- border: '1px solid',
186
- borderColor: 'border.default',
187
- color: 'fg.muted',
188
- borderRadius: 1,
189
- fontSize: 0,
190
- boxShadow: 'shadow.small',
191
- }, children: _jsx(Text, { sx: { fontSize: '10px', fontFamily: 'mono' }, children: shortcutHint }) }))] }));
182
+ }, children: _jsxs(Box, { sx: { position: 'relative' }, children: [_jsx(IconButton, { icon: position === 'right' ? SidebarExpandIcon : SidebarCollapseIcon, "aria-label": "Open chat", description: shortcutHint ? `Open chat (${shortcutHint})` : 'Open chat', onClick: handleToggle, variant: "default", size: "small", sx: {
183
+ bg: 'canvas.default',
184
+ border: '1px solid',
185
+ borderColor: 'border.default',
186
+ boxShadow: 'shadow.small',
187
+ } }), messages.length > 0 && (_jsx(Box, { sx: {
188
+ position: 'absolute',
189
+ top: -6,
190
+ right: -6,
191
+ minWidth: 16,
192
+ height: 16,
193
+ px: 1,
194
+ display: 'flex',
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ bg: 'accent.emphasis',
198
+ color: 'fg.onEmphasis',
199
+ borderRadius: '50%',
200
+ fontSize: '10px',
201
+ fontWeight: 'bold',
202
+ boxShadow: 'shadow.small',
203
+ }, children: messages.length > 99 ? '99+' : messages.length }))] }) }));
204
+ // Render the collapsed launcher in a body portal so it stays pinned to the
205
+ // viewport edge even when ancestors use transforms/positioning contexts.
206
+ if (typeof document !== 'undefined' && document.body) {
207
+ return createPortal(collapsedLauncher, document.body);
208
+ }
209
+ return collapsedLauncher;
192
210
  }
193
211
  // Mobile full-screen overlay
194
212
  const mobileStyles = isMobile
@@ -217,21 +235,35 @@ export function ChatSidebar({ title = 'Chat', protocol: protocolProp, defaultOpe
217
235
  position: 'relative',
218
236
  display: 'flex',
219
237
  flexDirection: 'column',
238
+ alignSelf: 'stretch',
220
239
  width: isMobile
221
240
  ? '100%'
222
241
  : typeof width === 'number'
223
242
  ? `${width}px`
224
243
  : width,
225
- height: '100%',
244
+ height: isMobile
245
+ ? '100%'
246
+ : desktopViewportHeight
247
+ ? `${desktopViewportHeight}px`
248
+ : 'calc(100dvh - 8px)',
249
+ minHeight: 0,
250
+ maxHeight: isMobile
251
+ ? '100%'
252
+ : desktopViewportHeight
253
+ ? `${desktopViewportHeight}px`
254
+ : 'calc(100dvh - 8px)',
255
+ marginBlock: isMobile ? 0 : '4px',
256
+ flex: isMobile ? '1 1 auto' : '0 0 auto',
226
257
  bg: 'canvas.default',
227
258
  borderLeft: !isMobile && position === 'right' ? '1px solid' : 'none',
228
259
  borderRight: !isMobile && position === 'left' ? '1px solid' : 'none',
229
260
  borderColor: 'border.default',
261
+ overflow: 'hidden',
230
262
  ...mobileStyles,
231
263
  }, children: _jsx(ChatBase, { title: title, showHeader: showHeader, brandIcon: brandIcon, protocol: protocolProp, headerButtons: {
232
264
  showNewChat: showNewChatButton,
233
265
  showClear: showClearButton && messages.length > 0,
234
- showSettings: showSettingsButton,
266
+ showSettings: showSettingsButton && !!onSettingsClick,
235
267
  onNewChat: handleNewChat,
236
268
  onClear: handleClear,
237
269
  onSettings: onSettingsClick,