@datalayer/agent-runtimes 1.0.18 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/chat/Chat.js CHANGED
@@ -136,7 +136,7 @@ function getProtocolType(protocol) {
136
136
  * />
137
137
  * ```
138
138
  */
139
- export function Chat({ protocol: transport, extensions: _extensions, baseUrl = 'http://localhost:8765', wsUrl, agentId, authToken: authTokenProp, placeholder = 'Type your message...', title, subtitle, brandIcon, 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, overlay, launching = false, launchingMessage, 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, enableEphemeralNotebook, initialEphemeralNotebookOpen, onEphemeralNotebookOpenChange, enableEphemeralDocument, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, onToolCallStart, onToolCallComplete, renderToolResult, hideMessagesAfterToolUI = false, contextSnapshot, mcpStatusData, codemodeStatusData, sandboxStatusData, kernel, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, disableInternalJupyterTheme = false, }) {
139
+ export function Chat({ protocol: transport, extensions: _extensions, baseUrl = 'http://localhost:8765', endpoint: endpointProp, configEndpoint: configEndpointProp, enableConfigQuery: enableConfigQueryProp, wsUrl, agentId, authToken: authTokenProp, placeholder = 'Type your message...', title, subtitle, brandIcon, 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, overlay, launching = false, launchingMessage, 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, enableEphemeralNotebook, initialEphemeralNotebookOpen, onEphemeralNotebookOpenChange, enableEphemeralDocument, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride, onToolCallStart, onToolCallComplete, renderToolResult, hideMessagesAfterToolUI = false, contextSnapshot, mcpStatusData, codemodeStatusData, sandboxStatusData, kernel, kernelEnvironmentName, kernelCpu, kernelMemory, kernelGpu, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, disableInternalJupyterTheme = false, }) {
140
140
  const resolvedAgentId = typeof agentId === 'string' && agentId.trim().length > 0
141
141
  ? agentId.trim()
142
142
  : 'default';
@@ -220,6 +220,10 @@ export function Chat({ protocol: transport, extensions: _extensions, baseUrl = '
220
220
  break;
221
221
  }
222
222
  }
223
+ // Explicit endpoint override (e.g. Agent Node tunnel proxy).
224
+ if (endpointProp) {
225
+ endpoint = endpointProp;
226
+ }
223
227
  return {
224
228
  config: {
225
229
  type: getProtocolType(transport),
@@ -228,12 +232,14 @@ export function Chat({ protocol: transport, extensions: _extensions, baseUrl = '
228
232
  authToken,
229
233
  options,
230
234
  // Enable config query for all protocols to fetch models and tools
231
- enableConfigQuery: true,
235
+ enableConfigQuery: enableConfigQueryProp ?? true,
232
236
  // For Jupyter-based transports, use Jupyter requestAPI (configEndpoint undefined)
233
237
  // For FastAPI-based transports, use direct fetch to the configure endpoint
234
- configEndpoint: transport === 'vercel-ai-jupyter'
235
- ? undefined // Use Jupyter requestAPI
236
- : `${baseUrl}/api/v1/configure`,
238
+ configEndpoint: configEndpointProp !== undefined
239
+ ? configEndpointProp
240
+ : transport === 'vercel-ai-jupyter'
241
+ ? undefined // Use Jupyter requestAPI
242
+ : `${baseUrl}/api/v1/configure`,
237
243
  },
238
244
  error: null,
239
245
  };
@@ -245,7 +251,16 @@ export function Chat({ protocol: transport, extensions: _extensions, baseUrl = '
245
251
  error: err instanceof Error ? err.message : 'Failed to configure',
246
252
  };
247
253
  }
248
- }, [transport, baseUrl, wsUrl, resolvedAgentId, authTokenProp]);
254
+ }, [
255
+ transport,
256
+ baseUrl,
257
+ wsUrl,
258
+ resolvedAgentId,
259
+ authTokenProp,
260
+ endpointProp,
261
+ configEndpointProp,
262
+ enableConfigQueryProp,
263
+ ]);
249
264
  const protocolConfig = protocolConfigResult.config;
250
265
  // Set initialized once protocol config is built
251
266
  useEffect(() => {
@@ -349,6 +364,6 @@ export function Chat({ protocol: transport, extensions: _extensions, baseUrl = '
349
364
  ? 'attention.fg'
350
365
  : 'danger.fg',
351
366
  flex: 1,
352
- }, children: errorBanner.message })] })), _jsx(ChatBase, { title: title, subtitle: subtitle, brandIcon: brandIcon, showHeader: showHeader, protocol: protocolConfig, placeholder: placeholder, description: description, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, autoFocus: autoFocus, runtimeId: runtimeId, historyEndpoint: historyEndpoint, pendingPrompt: pendingPrompt, showInformation: showInformation, onInformationClick: handleInformationClick, headerContent: headerContent, headerActions: headerActions, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, showInput: showInput, disableInputPrompt: disableInputPrompt, overlay: overlay, launching: launching, launchingMessage: launchingMessage, autoConnect: autoConnect, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, codemodeEnabled: codemodeEnabled, onToggleCodemode: onToggleCodemode, initialModel: initialModel, availableModels: availableModels, mcpServers: mcpServers, initialSkills: initialSkills, connectedIdentities: identitiesForChat, onNewChat: handleNewChat, onMessagesChange: handleMessagesChange, headerButtons: headerButtons, avatarConfig: avatarConfig, backgroundColor: "canvas.default", focusTrigger: focusTrigger, chatViewMode: chatViewMode, onChatViewModeChange: onChatViewModeChange, frontendTools: frontendTools, enableEphemeralNotebook: enableEphemeralNotebook, initialEphemeralNotebookOpen: initialEphemeralNotebookOpen, onEphemeralNotebookOpenChange: onEphemeralNotebookOpenChange, enableEphemeralDocument: enableEphemeralDocument, initialEphemeralSurfaceMode: initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange: onEphemeralSurfaceModeChange, collapsed: collapsed, onExpandFromCollapsed: onExpandFromCollapsed, ephemeralNotebookToolbar: ephemeralNotebookToolbar, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, renderToolResult: renderToolResult, hideMessagesAfterToolUI: hideMessagesAfterToolUI, contextSnapshot: contextSnapshot, mcpStatusData: mcpStatusData, sandboxStatusData: sandboxStatusData, kernel: kernel, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, disableInternalJupyterTheme: disableInternalJupyterTheme })] })] }) }) }));
367
+ }, children: errorBanner.message })] })), _jsx(ChatBase, { title: title, subtitle: subtitle, brandIcon: brandIcon, showHeader: showHeader, protocol: protocolConfig, placeholder: placeholder, description: description, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, autoFocus: autoFocus, runtimeId: runtimeId, historyEndpoint: historyEndpoint, pendingPrompt: pendingPrompt, showInformation: showInformation, onInformationClick: handleInformationClick, headerContent: headerContent, headerActions: headerActions, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, showInput: showInput, disableInputPrompt: disableInputPrompt, overlay: overlay, launching: launching, launchingMessage: launchingMessage, autoConnect: autoConnect, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, codemodeEnabled: codemodeEnabled, onToggleCodemode: onToggleCodemode, initialModel: initialModel, availableModels: availableModels, mcpServers: mcpServers, initialSkills: initialSkills, connectedIdentities: identitiesForChat, onNewChat: handleNewChat, onMessagesChange: handleMessagesChange, headerButtons: headerButtons, avatarConfig: avatarConfig, backgroundColor: "canvas.default", focusTrigger: focusTrigger, chatViewMode: chatViewMode, onChatViewModeChange: onChatViewModeChange, frontendTools: frontendTools, enableEphemeralNotebook: enableEphemeralNotebook, initialEphemeralNotebookOpen: initialEphemeralNotebookOpen, onEphemeralNotebookOpenChange: onEphemeralNotebookOpenChange, enableEphemeralDocument: enableEphemeralDocument, initialEphemeralSurfaceMode: initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange: onEphemeralSurfaceModeChange, collapsed: collapsed, onExpandFromCollapsed: onExpandFromCollapsed, ephemeralNotebookToolbar: ephemeralNotebookToolbar, ephemeralNotebookCollaborationProvider: ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId: ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration: ephemeralDocumentCollaboration, ephemeralRuntimeOverride: ephemeralRuntimeOverride, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, renderToolResult: renderToolResult, hideMessagesAfterToolUI: hideMessagesAfterToolUI, contextSnapshot: contextSnapshot, mcpStatusData: mcpStatusData, sandboxStatusData: sandboxStatusData, kernel: kernel, kernelEnvironmentName: kernelEnvironmentName, kernelCpu: kernelCpu, kernelMemory: kernelMemory, kernelGpu: kernelGpu, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, disableInternalJupyterTheme: disableInternalJupyterTheme })] })] }) }) }));
353
368
  }
354
369
  export default Chat;
@@ -17,11 +17,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
17
17
  */
18
18
  import { useContext } from 'react';
19
19
  import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
20
- import { Text, Spinner, IconButton } from '@primer/react';
20
+ import { Box, Text, Spinner, IconButton } from '@primer/react';
21
21
  import { SkeletonText } from '@primer/react/experimental';
22
22
  import { SidebarExpandIcon } from '@primer/octicons-react';
23
23
  import { notebookStore, JupyterReactTheme } from '@datalayer/jupyter-react';
24
- import { Box, setupPrimerPortals, useThemeStore, getColorPalette, getThemeConfig, useSystemColorMode, } from '@datalayer/primer-addons';
24
+ import { setupPrimerPortals, useThemeStore, getColorPalette, getThemeConfig, useSystemColorMode, } from '@datalayer/primer-addons';
25
25
  import { AlertIcon, PersonIcon } from '@primer/octicons-react';
26
26
  import { AiAgentIcon } from '@datalayer/icons-react';
27
27
  import { QueryClientProvider, QueryClientContext } from '@tanstack/react-query';
@@ -472,7 +472,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
472
472
  // Mode selection
473
473
  useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, enableStreaming = false,
474
474
  // Extended props
475
- brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, footerContent, showInformation = false, onInformationClick, 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: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar,
475
+ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, footerContent, showInformation = false, onInformationClick, 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: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
476
476
  // Tool invocation hooks
477
477
  onToolCallStart, onToolCallComplete,
478
478
  // Identity/Authorization props
@@ -510,9 +510,15 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
510
510
  // agent id. This keeps the persisted notebook model addressable by the same
511
511
  // key when navigating away from and back to the same runtime page.
512
512
  const notebookScopeId = runtimeId || protocolConfig?.agentId || activeAgentId;
513
- const ephemeralNotebookId = notebookScopeId
514
- ? `ephemeral-notebook-${notebookScopeId}`
515
- : generatedNotebookIdRef.current;
513
+ // When an explicit collaboration document id is supplied (e.g. an Agent Node
514
+ // sharing a room with the SaaS UI), it becomes the notebook id directly so
515
+ // BOTH peers join the same collaborative room AND the agent's notebook tools
516
+ // (scoped by this same id) drive the shared notebook. Otherwise fall back to
517
+ // the derived, per-runtime in-memory id.
518
+ const ephemeralNotebookId = ephemeralNotebookCollaborationDocumentId ||
519
+ (notebookScopeId
520
+ ? `ephemeral-notebook-${notebookScopeId}`
521
+ : generatedNotebookIdRef.current);
516
522
  const persistedEphemeralNbformat = useAgentRuntimeStore(s => s.getEphemeralNotebookModel(ephemeralNotebookId));
517
523
  const setEphemeralNotebookModel = useAgentRuntimeStore(s => s.setEphemeralNotebookModel);
518
524
  const handleEphemeralNotebookChange = useCallback((model) => {
@@ -520,10 +526,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
520
526
  }, [ephemeralNotebookId, setEphemeralNotebookModel]);
521
527
  // ── Ephemeral document (in-memory Lexical) ──────────────────────────────
522
528
  // Scoped to the same stable runtime identity as the notebook so the persisted
523
- // document survives navigation away from and back to the runtime page.
524
- const ephemeralDocumentId = notebookScopeId
525
- ? `ephemeral-document-${notebookScopeId}`
526
- : `ephemeral-document-${generatedNotebookIdRef.current}`;
529
+ // document survives navigation away from and back to the runtime page. When a
530
+ // collaboration room is supplied it becomes the document id directly so BOTH
531
+ // peers join the same Loro room AND the agent's lexical tools (scoped by this
532
+ // same id) drive the shared document.
533
+ const ephemeralDocumentId = ephemeralDocumentCollaboration?.roomId ||
534
+ (notebookScopeId
535
+ ? `ephemeral-document-${notebookScopeId}`
536
+ : `ephemeral-document-${generatedNotebookIdRef.current}`);
527
537
  const persistedEphemeralDocument = useAgentRuntimeStore(s => s.getEphemeralDocumentModel(ephemeralDocumentId));
528
538
  const setEphemeralDocumentModel = useAgentRuntimeStore(s => s.setEphemeralDocumentModel);
529
539
  const handleEphemeralDocumentChange = useCallback((model) => {
@@ -576,6 +586,18 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
576
586
  unsubscribe();
577
587
  };
578
588
  }, [notebookVisible, ephemeralNotebookId]);
589
+ // Track the ephemeral document's live kernel connection the same way, so the
590
+ // header's kernel indicator reflects the real connected kernel while the
591
+ // document surface is active (instead of the "disconnected" placeholder).
592
+ const [documentKernel, setDocumentKernel] = useState(null);
593
+ const handleDocumentKernelChange = useCallback((next) => {
594
+ setDocumentKernel(prev => (prev?.id === next?.id ? prev : next));
595
+ }, []);
596
+ useEffect(() => {
597
+ if (!documentVisible) {
598
+ setDocumentKernel(null);
599
+ }
600
+ }, [documentVisible]);
579
601
  // When a companion surface is shown, the chat can be docked as a sidebar
580
602
  // (default) or floated over it, driven by the header view-mode toggle.
581
603
  const surfaceChatFloating = surfaceVisible &&
@@ -2827,7 +2849,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2827
2849
  // no ephemeral notebook is shown) or INSIDE the chat body column (when the
2828
2850
  // notebook is visible) so the header always follows the chat body across all
2829
2851
  // view modes (docked sidebar, floating popup, floating-small).
2830
- const chatHeaderElement = showHeader ? (_jsx(ChatBaseHeader, { title: title, subtitle: subtitle, brandIcon: brandIcon, headerContent: headerContent, headerActions: headerActions, showInformation: showInformation, onInformationClick: onInformationClick, padding: padding, kernelIndicatorState: kernelIndicatorState, runtimeStatus: sandboxStatusData ?? sandboxStatusQuery.data, kernel: notebookVisible ? (notebookKernel ?? kernel) : kernel, kernelEnvironmentName: kernelEnvironmentName, kernelCpu: kernelCpu, kernelMemory: kernelMemory, kernelGpu: kernelGpu, headerButtons: headerButtons, messageCount: messages.length, onNewChat: handleNewChat, onClear: handleClear, chatViewMode: chatViewMode, onChatViewModeChange: onChatViewModeChange, showEphemeralSurfaceControl: enableEphemeralNotebook || enableEphemeralDocument, enableEphemeralNotebookOption: enableEphemeralNotebook, enableEphemeralDocumentOption: enableEphemeralDocument, ephemeralSurfaceMode: ephemeralSurfaceMode, onEphemeralSurfaceModeChange: handleEphemeralSurfaceModeChange })) : null;
2852
+ const chatHeaderElement = showHeader ? (_jsx(ChatBaseHeader, { title: title, subtitle: subtitle, brandIcon: brandIcon, headerContent: headerContent, headerActions: headerActions, showInformation: showInformation, onInformationClick: onInformationClick, padding: padding, kernelIndicatorState: kernelIndicatorState, runtimeStatus: sandboxStatusData ?? sandboxStatusQuery.data, kernel: notebookVisible
2853
+ ? (notebookKernel ?? kernel)
2854
+ : documentVisible
2855
+ ? (documentKernel ?? kernel)
2856
+ : kernel, kernelEnvironmentName: kernelEnvironmentName, kernelCpu: kernelCpu, kernelMemory: kernelMemory, kernelGpu: kernelGpu, headerButtons: headerButtons, messageCount: messages.length, onNewChat: handleNewChat, onClear: handleClear, chatViewMode: chatViewMode, onChatViewModeChange: onChatViewModeChange, showEphemeralSurfaceControl: enableEphemeralNotebook || enableEphemeralDocument, enableEphemeralNotebookOption: enableEphemeralNotebook, enableEphemeralDocumentOption: enableEphemeralDocument, ephemeralSurfaceMode: ephemeralSurfaceMode, onEphemeralSurfaceModeChange: handleEphemeralSurfaceModeChange })) : null;
2831
2857
  // ========================================================================
2832
2858
  // Render
2833
2859
  // ========================================================================
@@ -2874,7 +2900,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2874
2900
  borderRight: '1px solid',
2875
2901
  borderColor: 'border.default',
2876
2902
  }),
2877
- }, children: launching || overlay ? (_jsx(CompanionSurfaceSkeleton, { mode: notebookVisible ? 'notebook' : 'document' })) : notebookVisible ? (_jsx(EphemeralNotebook, { notebookId: ephemeralNotebookId, runtimePodName: runtimeId || activeAgentId, nbformat: persistedEphemeralNbformat ?? undefined, onNbformatChange: handleEphemeralNotebookChange, toolbarComponent: ephemeralNotebookToolbar })) : (_jsx(React.Suspense, { fallback: null, children: _jsx(EphemeralDocument, { documentId: ephemeralDocumentId, runtimePodName: runtimeId || activeAgentId, content: persistedEphemeralDocument ?? undefined, onContentChange: handleEphemeralDocumentChange, onToolsReady: handleDocumentToolsReady }) })) }), !surfaceCollapsed && (_jsxs(Box, { sx: surfaceChatFloating
2903
+ }, children: launching || overlay ? (_jsx(CompanionSurfaceSkeleton, { mode: notebookVisible ? 'notebook' : 'document' })) : notebookVisible ? (_jsx(EphemeralNotebook, { notebookId: ephemeralNotebookId, runtimePodName: runtimeId || activeAgentId, runtimeOverride: ephemeralRuntimeOverride, nbformat: persistedEphemeralNbformat ?? undefined, onNbformatChange: handleEphemeralNotebookChange, toolbarComponent: ephemeralNotebookToolbar, collaborationProvider: ephemeralNotebookCollaborationProvider })) : (_jsx(React.Suspense, { fallback: null, children: _jsx(EphemeralDocument, { documentId: ephemeralDocumentId, runtimePodName: runtimeId || activeAgentId, runtimeOverride: ephemeralRuntimeOverride, content: persistedEphemeralDocument ?? undefined, onContentChange: handleEphemeralDocumentChange, onToolsReady: handleDocumentToolsReady, onKernelChange: handleDocumentKernelChange, collaboration: ephemeralDocumentCollaboration }) })) }), !surfaceCollapsed && (_jsxs(Box, { sx: surfaceChatFloating
2878
2904
  ? {
2879
2905
  position: 'absolute',
2880
2906
  right: 16,
@@ -1,6 +1,35 @@
1
+ import type { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel';
1
2
  import type { FrontendToolDefinition } from '../../types/tools';
3
+ import type { EphemeralRuntimeOverride } from '../notebook/EphemeralNotebook';
2
4
  import '@datalayer/jupyter-react/lib/css/PrismCss';
3
5
  import '@datalayer/jupyter-lexical/style/index.css';
6
+ /**
7
+ * Real-time collaboration configuration for the ephemeral document.
8
+ *
9
+ * When supplied, the Lexical editor state is backed by a shared Loro CRDT
10
+ * document synchronised over a WebSocket (the spacer lexical endpoint) instead
11
+ * of the in-memory / persisted local state. Every peer that joins the same
12
+ * `roomId` edits the same document — this is how an Agent Node and the SaaS UI
13
+ * share a live document. The caller owns the room/identity lifecycle.
14
+ */
15
+ export interface EphemeralDocumentCollaboration {
16
+ /**
17
+ * WebSocket URL of the Loro collaboration server, e.g.
18
+ * `wss://…/api/spacer/v1/lexical/ws`.
19
+ */
20
+ websocketUrl: string;
21
+ /** Shared collaboration room / document id joined by all peers. */
22
+ roomId: string;
23
+ /** Optional identity used for collaborative presence (cursors / awareness). */
24
+ identity?: {
25
+ userId?: string;
26
+ handle?: string;
27
+ displayName?: string;
28
+ initials?: string;
29
+ avatarUrl?: string;
30
+ color?: string;
31
+ };
32
+ }
4
33
  export interface EphemeralDocumentProps {
5
34
  /**
6
35
  * Document identifier. Must match the id passed to `useLexicalTools` so the
@@ -9,15 +38,30 @@ export interface EphemeralDocumentProps {
9
38
  documentId: string;
10
39
  /** Preferred runtime pod name to bind the document kernel to. */
11
40
  runtimePodName?: string;
41
+ /** Optional explicit runtime endpoint (agent-node tunnel proxy path). */
42
+ runtimeOverride?: EphemeralRuntimeOverride;
12
43
  /** Optional persisted Lexical editor state (JSON string) to restore. */
13
44
  content?: string;
14
45
  /** Callback fired when the document's editor state changes. */
15
46
  onContentChange?: (content: string) => void;
16
47
  /** Reports the lexical frontend tools so the parent can pass them to the agent. */
17
48
  onToolsReady?: (tools: FrontendToolDefinition[]) => void;
49
+ /**
50
+ * Reports the document's live sandbox kernel connection upward so the chat
51
+ * header can render the same rich kernel indicator (kernel id, client id,
52
+ * status) as the notebook surface. Emits `null` while the kernel is absent.
53
+ */
54
+ onKernelChange?: (kernel: IKernelConnection | null) => void;
55
+ /**
56
+ * Optional real-time collaboration configuration. When supplied the document
57
+ * joins a shared Loro room over WebSocket so its state transits over RTC
58
+ * (e.g. between an Agent Node and the SaaS UI) rather than living purely in
59
+ * local memory.
60
+ */
61
+ collaboration?: EphemeralDocumentCollaboration;
18
62
  }
19
63
  /**
20
64
  * Renders an in-memory Lexical document backed by a sandbox kernel.
21
65
  */
22
- export declare function EphemeralDocument({ documentId, runtimePodName, content, onContentChange, onToolsReady, }: EphemeralDocumentProps): import("react/jsx-runtime").JSX.Element;
66
+ export declare function EphemeralDocument({ documentId, runtimePodName, runtimeOverride, content, onContentChange, onToolsReady, onKernelChange, collaboration, }: EphemeralDocumentProps): import("react/jsx-runtime").JSX.Element;
23
67
  export default EphemeralDocument;
@@ -37,9 +37,11 @@ import { ListPlugin } from '@lexical/react/LexicalListPlugin';
37
37
  import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
38
38
  import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
39
39
  import { ServerConnection, ServiceManager } from '@jupyterlab/services';
40
- import { Box, DatalayerThemeProvider, getThemeConfig, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
40
+ import { DatalayerThemeProvider, getThemeConfig, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
41
+ import { Box, Spinner, Text } from '@primer/react';
41
42
  import { JupyterReactTheme, Kernel, disposeServiceManager, } from '@datalayer/jupyter-react';
42
43
  import { ComponentPickerMenuPlugin, JupyterCellPlugin, JupyterInputOutputPlugin, DraggableBlockPlugin, ImagesPlugin, ExcalidrawPlugin, TablePlugin, TableCellResizerPlugin, TableActionMenuPlugin, TableHoverActionsV2Plugin, CollapsiblePlugin, HorizontalRulePlugin, EquationsPlugin, YouTubePlugin, AutoLinkPlugin, AutoEmbedPlugin, LexicalConfigProvider, LexicalStatePlugin, FloatingTextFormatToolbarPlugin, CodeActionMenuPlugin, ListMaxIndentLevelPlugin, ToolbarPlugin, ToolbarContext, CommentsProvider, commentTheme, } from '@datalayer/jupyter-lexical';
44
+ import { createWebsocketProvider, LoroCollaborationPlugin, } from '@datalayer/lexical-loro';
43
45
  import { editorConfig } from '../../examples/lexical/editorConfig';
44
46
  import { useLexicalTools } from '../../tools/adapters/agent-runtimes/lexicalHooks';
45
47
  import { useAgentsRuntimes } from '../../hooks/useAgentRuntimes';
@@ -106,7 +108,32 @@ function DocumentToolbar({ setIsLinkEditMode, }) {
106
108
  /**
107
109
  * Renders an in-memory Lexical document backed by a sandbox kernel.
108
110
  */
109
- export function EphemeralDocument({ documentId, runtimePodName, content, onContentChange, onToolsReady, }) {
111
+ export function EphemeralDocument({ documentId, runtimePodName, runtimeOverride, content, onContentChange, onToolsReady, onKernelChange, collaboration, }) {
112
+ // Real-time collaboration is active only when both a WebSocket endpoint and a
113
+ // room id are supplied. In that mode the shared Loro CRDT is the single source
114
+ // of truth: local content-seeding and undo history are disabled so the local
115
+ // editor never diverges from (or fights) the collaborative document.
116
+ const isCollaborative = Boolean(collaboration?.websocketUrl && collaboration?.roomId);
117
+ const collaborationName = collaboration?.identity?.displayName ||
118
+ collaboration?.identity?.handle ||
119
+ collaboration?.identity?.userId;
120
+ const collaborationColor = collaboration?.identity?.color;
121
+ const collaborationAwarenessData = useMemo(() => ({
122
+ user: {
123
+ id: collaboration?.identity?.userId,
124
+ username: collaboration?.identity?.userId ||
125
+ collaboration?.identity?.handle ||
126
+ collaboration?.identity?.displayName,
127
+ name: collaboration?.identity?.handle ||
128
+ collaboration?.identity?.displayName,
129
+ display_name: collaboration?.identity?.displayName ||
130
+ collaboration?.identity?.handle,
131
+ initials: collaboration?.identity?.initials,
132
+ color: collaboration?.identity?.color,
133
+ avatar_url: collaboration?.identity?.avatarUrl,
134
+ handle: collaboration?.identity?.handle,
135
+ },
136
+ }), [collaboration?.identity]);
110
137
  // Capture the restore seed ONCE per documentId so parent re-renders don't
111
138
  // remount the composer (which reads `initialConfig` a single time).
112
139
  const initialContentRef = useRef(content);
@@ -127,14 +154,38 @@ export function EphemeralDocument({ documentId, runtimePodName, content, onConte
127
154
  // Resolve the runtime sandbox strictly by its assigned pod name. There is
128
155
  // deliberately NO local fallback kernel: the ephemeral document executes on
129
156
  // exactly the runtime assigned to this agent, or shows a waiting state.
130
- const { runtimes } = useAgentsRuntimes();
157
+ const { runtimes, refetchRuntimes } = useAgentsRuntimes();
131
158
  const selectedRuntime = useMemo(() => {
159
+ const overrideBaseUrl = String(runtimeOverride?.baseUrl || '').trim();
160
+ if (overrideBaseUrl) {
161
+ return {
162
+ url: overrideBaseUrl,
163
+ wsUrl: String(runtimeOverride?.wsUrl || '').trim() || undefined,
164
+ token: String(runtimeOverride?.token || '').trim(),
165
+ pod_name: String(runtimeOverride?.podName || '').trim() || 'agent-node-proxy',
166
+ };
167
+ }
132
168
  const preferredPod = String(runtimePodName || '').trim();
133
169
  if (!preferredPod) {
134
170
  return undefined;
135
171
  }
136
172
  return runtimes.find(rt => String(rt?.pod_name || '') === preferredPod);
137
- }, [runtimePodName, runtimes]);
173
+ }, [runtimeOverride, runtimePodName, runtimes]);
174
+ const needsRuntimeLookup = Boolean(!runtimeOverride?.baseUrl &&
175
+ String(runtimePodName || '').trim() &&
176
+ !selectedRuntime);
177
+ useEffect(() => {
178
+ if (!needsRuntimeLookup) {
179
+ return;
180
+ }
181
+ void refetchRuntimes();
182
+ const intervalId = window.setInterval(() => {
183
+ void refetchRuntimes();
184
+ }, 1000);
185
+ return () => {
186
+ window.clearInterval(intervalId);
187
+ };
188
+ }, [needsRuntimeLookup, refetchRuntimes]);
138
189
  const [runtimeServiceManager, setRuntimeServiceManager] = useState(null);
139
190
  // Kernel started inside the agent runtime sandbox and threaded into the
140
191
  // Jupyter cell plugins, mirroring how the document editor wires its kernel.
@@ -154,9 +205,11 @@ export function EphemeralDocument({ documentId, runtimePodName, content, onConte
154
205
  }
155
206
  try {
156
207
  const token = String(selectedRuntime?.token || '').trim();
208
+ const wsUrl = String(selectedRuntime?.wsUrl || '').trim() ||
209
+ baseUrl.replace(/^http/, 'ws');
157
210
  const serverSettings = ServerConnection.makeSettings({
158
211
  baseUrl,
159
- wsUrl: baseUrl.replace(/^http/, 'ws'),
212
+ wsUrl,
160
213
  token,
161
214
  appendToken: true,
162
215
  });
@@ -195,8 +248,19 @@ export function EphemeralDocument({ documentId, runtimePodName, content, onConte
195
248
  }
196
249
  };
197
250
  }, [selectedRuntime?.pod_name, selectedRuntime?.url, selectedRuntime?.token]);
251
+ // Surface the document's live kernel connection to the parent so the chat
252
+ // header's kernel indicator reflects the real connected kernel instead of
253
+ // the "disconnected"/"no-kernel" placeholder while the document is active.
254
+ useEffect(() => {
255
+ onKernelChange?.(documentKernel?.connection ?? null);
256
+ return () => {
257
+ onKernelChange?.(null);
258
+ };
259
+ }, [documentKernel, onKernelChange]);
198
260
  const activeServiceManager = runtimeServiceManager;
199
- const isRuntimeStarting = Boolean(String(runtimePodName || '').trim() && !activeServiceManager);
261
+ const isRuntimeStarting = Boolean((String(runtimePodName || '').trim() ||
262
+ String(runtimeOverride?.baseUrl || '').trim()) &&
263
+ !activeServiceManager);
200
264
  useProgressTask(`ephemeral-document-start-${documentId}`, isRuntimeStarting);
201
265
  // Build the initial config once per documentId, without the demo content so
202
266
  // the document starts empty and is either restored or filled by the agent.
@@ -252,19 +316,79 @@ export function EphemeralDocument({ documentId, runtimePodName, content, onConte
252
316
  inset: 0,
253
317
  display: 'flex',
254
318
  flexDirection: 'column',
255
- overflow: 'auto',
319
+ minHeight: 0,
320
+ overflow: 'hidden',
256
321
  overscrollBehaviorY: 'contain',
257
322
  padding: 2,
258
323
  '& .editor-shell': {
259
324
  backgroundColor: themeBackground,
260
325
  borderRadius: 2,
326
+ display: 'flex',
327
+ flexDirection: 'column',
328
+ minHeight: 0,
329
+ flex: 1,
330
+ // The shared lexical stylesheet caps `.editor-shell` at
331
+ // `max-width: 1100px; margin: 20px auto`, which leaves the
332
+ // ephemeral document narrow and centered. Take the full
333
+ // wrapping box width instead (matching the ui LiterateEditor,
334
+ // whose root is `.editor-container` with no such cap).
335
+ maxWidth: 'none',
336
+ width: '100%',
337
+ margin: 0,
338
+ },
339
+ '& .editor-container': {
340
+ backgroundColor: themeBackground,
341
+ display: 'flex',
342
+ flexDirection: 'column',
343
+ minHeight: 0,
344
+ flex: 1,
345
+ width: '100%',
346
+ maxWidth: 'none',
347
+ margin: 0,
348
+ },
349
+ '& .editor-inner': {
350
+ backgroundColor: themeBackground,
351
+ display: 'flex',
352
+ flexDirection: 'column',
353
+ minHeight: 0,
354
+ flex: 1,
355
+ width: '100%',
356
+ maxWidth: 'none',
357
+ },
358
+ '& .editor-scroller': {
359
+ minHeight: 0,
360
+ flex: 1,
361
+ overflow: 'auto',
362
+ width: '100%',
363
+ maxWidth: 'none',
364
+ },
365
+ '& .editor': {
366
+ minHeight: '100%',
367
+ width: '100%',
368
+ maxWidth: 'none',
369
+ margin: 0,
370
+ },
371
+ '& .editor-input': {
372
+ backgroundColor: themeBackground,
373
+ width: '100%',
374
+ maxWidth: 'none',
375
+ margin: 0,
376
+ },
377
+ '& .ContentEditable__root': {
378
+ width: '100%',
379
+ maxWidth: 'none',
380
+ margin: 0,
261
381
  },
262
- '& .editor-container': { backgroundColor: themeBackground },
263
- '& .editor-inner': { backgroundColor: themeBackground },
264
- '& .editor-input': { backgroundColor: themeBackground },
265
382
  '& [role="toolbar"][aria-label="Editor toolbar"]': {
266
383
  backgroundColor: themeBackground,
267
384
  },
268
- }, children: _jsx(LexicalConfigProvider, { lexicalId: documentId, serviceManager: activeServiceManager, children: _jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(CommentsProvider, { children: _jsx(ToolbarContext, { children: _jsxs("div", { className: "editor-shell", children: [_jsx(DocumentToolbar, { setIsLinkEditMode: setIsLinkEditMode }), _jsx("div", { className: "editor-container", children: _jsxs("div", { className: "editor-inner", children: [_jsx(LexicalStatePlugin, {}), _jsx(RichTextPlugin, { contentEditable: _jsx("div", { className: "editor-scroller", children: _jsx("div", { className: "editor", ref: onAnchorRef, children: _jsx(ContentEditable, { className: "editor-input", "aria-label": "Ephemeral document editor" }) }) }), ErrorBoundary: LexicalErrorBoundary }), _jsx(OnChangePlugin, { onChange: handleChange }), _jsx(HistoryPlugin, {}), _jsx(AutoFocusPlugin, {}), _jsx(ListPlugin, {}), _jsx(CheckListPlugin, {}), _jsx(LinkPlugin, {}), _jsx(AutoLinkPlugin, {}), _jsx(ListMaxIndentLevelPlugin, { maxDepth: 7 }), _jsx(MarkdownShortcutPlugin, { transformers: TRANSFORMERS }), _jsx(LoadContentPlugin, { content: initialContent }), _jsx(CodeHighlightPlugin, {}), _jsx(ImagesPlugin, { captionsEnabled: false }), _jsx(ExcalidrawPlugin, {}), _jsx(TablePlugin, {}), _jsx(TableCellResizerPlugin, {}), _jsx(TableActionMenuPlugin, {}), _jsx(TableHoverActionsV2Plugin, {}), _jsx(CollapsiblePlugin, {}), _jsx(HorizontalRulePlugin, {}), _jsx(EquationsPlugin, {}), _jsx(YouTubePlugin, {}), _jsx(AutoEmbedPlugin, {}), _jsx(JupyterCellPlugin, {}), _jsx(ComponentPickerMenuPlugin, { kernel: documentKernel }), _jsx(JupyterInputOutputPlugin, { kernel: documentKernel }), floatingAnchorElem && (_jsxs(_Fragment, { children: [_jsx(DraggableBlockPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem, setIsLinkEditMode: setIsLinkEditMode }), _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem })] }))] }) })] }) }) }) }) }) }) }) })) : null }));
385
+ }, children: _jsx(LexicalConfigProvider, { lexicalId: documentId, serviceManager: activeServiceManager, children: _jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(CommentsProvider, { children: _jsx(ToolbarContext, { children: _jsxs("div", { className: "editor-shell", children: [_jsx(DocumentToolbar, { setIsLinkEditMode: setIsLinkEditMode }), _jsx("div", { className: "editor-container", children: _jsxs("div", { className: "editor-inner", children: [isCollaborative && collaboration ? (_jsx(LoroCollaborationPlugin, { id: collaboration.roomId, providerFactory: createWebsocketProvider, websocketUrl: collaboration.websocketUrl, shouldBootstrap: true, showCollaborators: true, username: collaborationName, cursorColor: collaborationColor, awarenessData: collaborationAwarenessData })) : null, _jsx(LexicalStatePlugin, {}), _jsx(RichTextPlugin, { contentEditable: _jsx("div", { className: "editor-scroller", children: _jsx("div", { className: "editor", ref: onAnchorRef, children: _jsx(ContentEditable, { className: "editor-input", "aria-label": "Ephemeral document editor" }) }) }), ErrorBoundary: LexicalErrorBoundary }), !isCollaborative && (_jsx(OnChangePlugin, { onChange: handleChange })), !isCollaborative && _jsx(HistoryPlugin, {}), _jsx(AutoFocusPlugin, {}), _jsx(ListPlugin, {}), _jsx(CheckListPlugin, {}), _jsx(LinkPlugin, {}), _jsx(AutoLinkPlugin, {}), _jsx(ListMaxIndentLevelPlugin, { maxDepth: 7 }), _jsx(MarkdownShortcutPlugin, { transformers: TRANSFORMERS }), !isCollaborative && (_jsx(LoadContentPlugin, { content: initialContent })), _jsx(CodeHighlightPlugin, {}), _jsx(ImagesPlugin, { captionsEnabled: false }), _jsx(ExcalidrawPlugin, {}), _jsx(TablePlugin, {}), _jsx(TableCellResizerPlugin, {}), _jsx(TableActionMenuPlugin, {}), _jsx(TableHoverActionsV2Plugin, {}), _jsx(CollapsiblePlugin, {}), _jsx(HorizontalRulePlugin, {}), _jsx(EquationsPlugin, {}), _jsx(YouTubePlugin, {}), _jsx(AutoEmbedPlugin, {}), _jsx(JupyterCellPlugin, {}), _jsx(ComponentPickerMenuPlugin, { kernel: documentKernel }), _jsx(JupyterInputOutputPlugin, { kernel: documentKernel }), floatingAnchorElem && (_jsxs(_Fragment, { children: [_jsx(DraggableBlockPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem, setIsLinkEditMode: setIsLinkEditMode }), _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem })] }))] }) })] }) }) }) }) }) }) }) })) : isRuntimeStarting ? (_jsxs(Box, { sx: {
386
+ height: '100%',
387
+ display: 'flex',
388
+ alignItems: 'center',
389
+ justifyContent: 'center',
390
+ gap: 2,
391
+ color: 'fg.muted',
392
+ }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Starting document..." })] })) : null }));
269
393
  }
270
394
  export default EphemeralDocument;
@@ -1,5 +1,24 @@
1
1
  import type { INotebookContent } from '@jupyterlab/nbformat';
2
+ import type { ICollaborationProvider } from '@datalayer/jupyter-react';
2
3
  import type { EphemeralNotebookToolbarComponent } from '../../types/chat';
4
+ /**
5
+ * Explicit runtime endpoint used to bind an ephemeral notebook kernel without
6
+ * resolving a pod from the user's runtimes list. Used to reach an agent node's
7
+ * Jupyter server through the runtimes tunnel HTTP/WebSocket proxy.
8
+ */
9
+ export interface EphemeralRuntimeOverride {
10
+ /** REST base URL, e.g. `https://.../agent-nodes/{nodeId}/jupyter`. */
11
+ baseUrl: string;
12
+ /**
13
+ * WebSocket base URL. Defaults to `baseUrl` with the http(s) scheme swapped
14
+ * for ws(s). For the tunnel proxy this is the `/ws` sibling of `baseUrl`.
15
+ */
16
+ wsUrl?: string;
17
+ /** Bearer/JWT token appended to requests (`appendToken`). */
18
+ token?: string;
19
+ /** Stable identifier used for the sandbox service-manager registry. */
20
+ podName?: string;
21
+ }
3
22
  export interface EphemeralNotebookProps {
4
23
  /**
5
24
  * Notebook identifier. Must match the id passed to `useNotebookTools` so the
@@ -8,6 +27,14 @@ export interface EphemeralNotebookProps {
8
27
  notebookId: string;
9
28
  /** Preferred runtime pod name to bind the notebook kernel to. */
10
29
  runtimePodName?: string;
30
+ /**
31
+ * Explicit runtime endpoint override. When supplied, the notebook binds its
32
+ * kernel to this endpoint directly instead of resolving a pod from the user's
33
+ * runtimes list. This is how a SaaS browser reaches an agent node's Jupyter
34
+ * server through the runtimes tunnel proxy: `baseUrl` points at
35
+ * `.../agent-nodes/{nodeId}/jupyter` and `wsUrl` at its `/ws` sibling.
36
+ */
37
+ runtimeOverride?: EphemeralRuntimeOverride;
11
38
  /** Left margin reserved for the cell sidebar (default 120). */
12
39
  cellSidebarMargin?: number;
13
40
  /** Optional persisted notebook model to restore when mounting. */
@@ -16,9 +43,17 @@ export interface EphemeralNotebookProps {
16
43
  onNbformatChange?: (content: INotebookContent) => void;
17
44
  /** Optional toolbar component override. */
18
45
  toolbarComponent?: EphemeralNotebookToolbarComponent;
46
+ /**
47
+ * Optional real-time collaboration provider. When supplied, the notebook
48
+ * joins a shared collaborative room (the ydoc becomes the source of truth)
49
+ * and the local in-memory persistence poll is disabled. This is how a node's
50
+ * notebook state transits to the SaaS UI (and back) over RTC instead of the
51
+ * tunnel.
52
+ */
53
+ collaborationProvider?: ICollaborationProvider;
19
54
  }
20
55
  /**
21
56
  * Renders an in-memory notebook backed by a sandbox kernel.
22
57
  */
23
- export declare function EphemeralNotebook({ notebookId, runtimePodName, cellSidebarMargin, nbformat, onNbformatChange, toolbarComponent, }: EphemeralNotebookProps): import("react/jsx-runtime").JSX.Element;
58
+ export declare function EphemeralNotebook({ notebookId, runtimePodName, runtimeOverride, cellSidebarMargin, nbformat, onNbformatChange, toolbarComponent, collaborationProvider, }: EphemeralNotebookProps): import("react/jsx-runtime").JSX.Element;
24
59
  export default EphemeralNotebook;