@datalayer/agent-runtimes 1.3.74 → 1.3.78

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 (143) hide show
  1. package/lib/agents/AgentDetails.d.ts +16 -2
  2. package/lib/agents/AgentDetails.js +381 -272
  3. package/lib/chat/ChatFloating.d.ts +27 -1
  4. package/lib/chat/ChatFloating.js +97 -13
  5. package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
  6. package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
  7. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  8. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  9. package/lib/chat/assistant/AssistantStage.d.ts +144 -4
  10. package/lib/chat/assistant/AssistantStage.js +280 -10
  11. package/lib/chat/assistant/BalloonParts.d.ts +33 -5
  12. package/lib/chat/assistant/BalloonParts.js +66 -44
  13. package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
  14. package/lib/chat/assistant/BalloonVisual.js +4 -3
  15. package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
  16. package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
  17. package/lib/chat/assistant/SpeechBalloon.d.ts +54 -6
  18. package/lib/chat/assistant/SpeechBalloon.js +73 -22
  19. package/lib/chat/assistant/assistantDetails.d.ts +84 -0
  20. package/lib/chat/assistant/assistantDetails.js +140 -0
  21. package/lib/chat/assistant/formats/files.d.ts +23 -0
  22. package/lib/chat/assistant/formats/files.js +48 -0
  23. package/lib/chat/assistant/formats/index.d.ts +2 -0
  24. package/lib/chat/assistant/formats/index.js +1 -0
  25. package/lib/chat/assistant/state.d.ts +7 -14
  26. package/lib/chat/assistant/state.js +21 -20
  27. package/lib/chat/assistant/toolLine.d.ts +16 -0
  28. package/lib/chat/assistant/toolLine.js +46 -0
  29. package/lib/chat/base/ChatBase.js +71 -1
  30. package/lib/chat/base/loopMessage.d.ts +37 -0
  31. package/lib/chat/base/loopMessage.js +37 -0
  32. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  33. package/lib/chat/indicators/TypingDots.js +21 -0
  34. package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
  35. package/lib/chat/messages/ChatMarkdown.js +18 -0
  36. package/lib/chat/messages/ChatMessageList.d.ts +8 -1
  37. package/lib/chat/messages/ChatMessageList.js +25 -33
  38. package/lib/chat/styles/streamdownStyles.js +3 -1
  39. package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
  40. package/lib/chat/tools/ToolCallDisplay.js +8 -7
  41. package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
  42. package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
  43. package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
  44. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  45. package/lib/components/inspector/AgentInspector.js +220 -0
  46. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  47. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  48. package/lib/components/inspector/JsonTree.d.ts +24 -0
  49. package/lib/components/inspector/JsonTree.js +89 -0
  50. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  51. package/lib/components/inspector/a2aInspect.js +478 -0
  52. package/lib/components/inspector/agentInspector.d.ts +179 -0
  53. package/lib/components/inspector/agentInspector.js +267 -0
  54. package/lib/components/inspector/chatInspect.d.ts +38 -0
  55. package/lib/components/inspector/chatInspect.js +65 -0
  56. package/lib/components/inspector/classify.d.ts +19 -0
  57. package/lib/components/inspector/classify.js +42 -0
  58. package/lib/components/inspector/index.d.ts +14 -0
  59. package/lib/components/inspector/index.js +18 -0
  60. package/lib/components/teams/A2ATeamGraph.d.ts +69 -1
  61. package/lib/components/teams/A2ATeamGraph.js +277 -37
  62. package/lib/components/teams/NotebookPreviewView.js +9 -1
  63. package/lib/components/teams/TeamNotebook.d.ts +3 -1
  64. package/lib/components/teams/TeamNotebook.js +3 -3
  65. package/lib/components/teams/TeamNotebookView.js +15 -0
  66. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  67. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  68. package/lib/components/teams/teamConnections.d.ts +9 -1
  69. package/lib/components/teams/teamConnections.js +20 -0
  70. package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
  71. package/lib/components/teams/teamNotebookKernel.js +17 -0
  72. package/lib/components/teams/useA2ATeam.d.ts +20 -3
  73. package/lib/components/teams/useA2ATeam.js +108 -23
  74. package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
  75. package/lib/examples/AgentA2ATeamExample.js +67 -13
  76. package/lib/examples/AssistantExample.js +65 -26
  77. package/lib/examples/AssistantGalleryExample.js +2 -1
  78. package/lib/examples/exampleGroups.d.ts +1 -1
  79. package/lib/examples/exampleGroups.js +1 -1
  80. package/lib/examples/utils/assistantGallery.d.ts +7 -6
  81. package/lib/examples/utils/assistantGallery.js +23 -8
  82. package/lib/jupyter/exec/Python.d.ts +0 -1
  83. package/lib/jupyter/exec/Python.js +0 -32
  84. package/lib/jupyter/exec/Snippets.d.ts +4 -12
  85. package/lib/jupyter/exec/Snippets.js +0 -8
  86. package/lib/loop/apps/AppFeedback.js +3 -1
  87. package/lib/loop/apps/AppKept.d.ts +22 -0
  88. package/lib/loop/apps/AppKept.js +36 -0
  89. package/lib/loop/apps/AppRenderer.d.ts +11 -1
  90. package/lib/loop/apps/AppRenderer.js +22 -4
  91. package/lib/loop/apps/checks.d.ts +7 -0
  92. package/lib/loop/apps/checks.js +39 -0
  93. package/lib/loop/apps/index.d.ts +3 -0
  94. package/lib/loop/apps/index.js +3 -0
  95. package/lib/loop/apps/kept.d.ts +30 -0
  96. package/lib/loop/apps/kept.js +52 -0
  97. package/lib/loop/apps/records.d.ts +12 -4
  98. package/lib/loop/apps/records.js +13 -5
  99. package/lib/loop/apps/saved.d.ts +30 -0
  100. package/lib/loop/apps/saved.js +40 -0
  101. package/lib/loop/core/index.d.ts +17 -0
  102. package/lib/loop/core/index.js +1 -0
  103. package/lib/loop/embed/AppEmbed.d.ts +8 -1
  104. package/lib/loop/embed/AppEmbed.js +4 -4
  105. package/lib/loop/embed/LoopEmbed.d.ts +6 -1
  106. package/lib/loop/embed/LoopEmbed.js +4 -2
  107. package/lib/loop/embed/element.js +1 -1
  108. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
  109. package/lib/loop/plugins/agents/browserService.d.ts +7 -1
  110. package/lib/loop/plugins/agents/browserService.js +4 -0
  111. package/lib/loop/plugins/agents/plugin.d.ts +16 -0
  112. package/lib/loop/plugins/agents/switchable.d.ts +3 -0
  113. package/lib/loop/plugins/agents/switchable.js +1 -0
  114. package/lib/loop/plugins/app-activity/index.js +3 -0
  115. package/lib/loop/plugins/app-computer/index.js +3 -0
  116. package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
  117. package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
  118. package/lib/loop/plugins/app-rules/index.js +3 -0
  119. package/lib/loop/plugins/assistant/index.d.ts +7 -2
  120. package/lib/loop/plugins/assistant/index.js +58 -4
  121. package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
  122. package/lib/loop/plugins/assistant-characters/index.js +17 -0
  123. package/lib/loop/plugins/chat/ChatView.js +4 -1
  124. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
  125. package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
  126. package/lib/loop/presets.d.ts +4 -0
  127. package/lib/loop/presets.js +3 -1
  128. package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
  129. package/lib/loop/shell/LoopWorkspace.js +5 -4
  130. package/lib/loop/shell/SidebarRail.d.ts +53 -0
  131. package/lib/loop/shell/SidebarRail.js +74 -0
  132. package/lib/loop/shell/index.d.ts +2 -0
  133. package/lib/loop/shell/index.js +2 -0
  134. package/lib/loop/shell/paneMotion.d.ts +21 -0
  135. package/lib/loop/shell/paneMotion.js +37 -0
  136. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  137. package/lib/specs/apps.js +8 -5
  138. package/lib/specs/uiPlugins.js +529 -0
  139. package/lib/types/agentspecs.d.ts +5 -3
  140. package/lib/types/chat.d.ts +12 -0
  141. package/package.json +2 -2
  142. package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
  143. package/scripts/codegen/generate_ui_plugins.py +112 -2
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, memo, useContext, useEffect, useMemo, useRef, useState, } from 'react';
2
+ import { createContext, memo, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
3
3
  import { BaseEdge, EdgeLabelRenderer, Handle, Position, ReactFlow, } from '@xyflow/react';
4
4
  import '@xyflow/react/dist/base.css';
5
5
  import { Button, Label, Text } from '@primer/react';
6
6
  import { Box } from '@datalayer/primer-addons';
7
- import { AssistantStage } from '../../chat/assistant/AssistantStage';
7
+ import { AssistantStage, } from '../../chat/assistant/AssistantStage';
8
+ import { useSignalValue } from '@datalayer/reactor/react';
9
+ import { teamNotebookKernel } from './teamNotebookKernel';
8
10
  import { SpecMark } from '../../chat/marks/SpecMark';
9
11
  import { toolWords, } from './a2aTeamFlow';
10
12
  import { NotebookPreview } from './NotebookPreview';
@@ -14,7 +16,7 @@ import { toolLineText } from '../../chat/assistant/toolLine';
14
16
  const NODE_WIDTH = 200;
15
17
  const BALLOON_ROOM = 144;
16
18
  /** The room a balloon that holds a notebook needs above its member. */
17
- const NOTEBOOK_BALLOON_ROOM = 340;
19
+ const NOTEBOOK_BALLOON_ROOM = 480;
18
20
  /** How tall the notebook in a balloon grows before it scrolls. */
19
21
  const NOTEBOOK_PREVIEW_HEIGHT = 180;
20
22
  const GAP = 220;
@@ -44,16 +46,67 @@ const HANDLE = (size) => ({
44
46
  const Members = createContext({});
45
47
  /** The connections being called now, by node id: read by their nodes, as the members are. */
46
48
  const Busy = createContext({});
49
+ const AT_PLACE = { dx: 0, dy: 0 };
50
+ /** An offset kept inside its bounds. */
51
+ export function clampOffset(offset, bounds) {
52
+ return {
53
+ dx: Math.min(bounds.maxDx, Math.max(bounds.minDx, offset.dx)),
54
+ dy: Math.min(bounds.maxDy, Math.max(bounds.minDy, offset.dy)),
55
+ };
56
+ }
57
+ /**
58
+ * Where the members were moved, by page and member: kept for the session,
59
+ * in memory, so a graph drawn again on the page keeps them where they were.
60
+ */
61
+ const MOVED = new Map();
62
+ const movedKey = (id) => `${typeof location === 'undefined' ? '' : location.pathname}#${id}`;
63
+ /** How far a press moves before it is a drag, not a click, in pixels. */
64
+ const DRAG_THRESHOLD = 4;
65
+ /** How far an arrow key moves a member; with Shift, more. */
66
+ const NUDGE = 10;
67
+ const NUDGE_FAR = 40;
68
+ /** The members' moves, read by their nodes. */
69
+ const Moving = createContext({
70
+ zoom: 1,
71
+ offsetOf: () => AT_PLACE,
72
+ move: () => undefined,
73
+ reset: () => undefined,
74
+ });
47
75
  /** A connection's node id: under its member. */
48
76
  const connectionNodeId = (member, connection) => `${member}/${connection}`;
49
77
  const MemberNode = memo(function MemberNode({ data, }) {
50
78
  const { id, side, size } = data;
51
79
  const member = useContext(Members)[id];
80
+ const moving = useContext(Moving);
81
+ const offset = moving.offsetOf(id);
52
82
  const stageRef = useRef(null);
53
- if (!member) {
83
+ // Its balloon's display, as its menu changes it.
84
+ const [display, setDisplay] = useState();
85
+ // Its balloon as a click on it left it: shown, hidden, or as the team
86
+ // says (`auto`: what it is doing, and on hover).
87
+ const [shown, setShown] = useState('auto');
88
+ // What it says or does now: something new reopens a hidden balloon.
89
+ const persona = member?.persona;
90
+ const news = [
91
+ persona?.saying,
92
+ persona?.tool ? toolLineText(persona.tool) : '',
93
+ persona?.notebook?.name,
94
+ ].join('\u0000');
95
+ const seen = useRef(news);
96
+ useEffect(() => {
97
+ if (news !== seen.current) {
98
+ seen.current = news;
99
+ setShown(current => (current === 'hidden' ? 'auto' : current));
100
+ }
101
+ }, [news]);
102
+ if (!member || !persona) {
54
103
  return null;
55
104
  }
56
- const { persona } = member;
105
+ const shownDisplay = display ?? member.balloonDisplay ?? 'current';
106
+ // History: what it said and did, listed.
107
+ const listed = shownDisplay === 'history' && member.history?.length
108
+ ? { history: member.history }
109
+ : {};
57
110
  // A notebook it was given: read-only in its balloon, and its large visual
58
111
  // the one that runs, editable — under the graph, or in a dialog.
59
112
  const notebookTitle = member.notebookTitle ?? (persona.notebook?.name || 'The notebook');
@@ -63,6 +116,29 @@ const MemberNode = memo(function MemberNode({ data, }) {
63
116
  visual: notebookBalloonVisual(persona.notebook, notebookTitle),
64
117
  }
65
118
  : {};
119
+ const balloonNow = persona.tool
120
+ ? {
121
+ text: toolLineText(persona.tool),
122
+ tool: persona.tool,
123
+ busy: persona.state !== 'idle',
124
+ ...given,
125
+ ...listed,
126
+ }
127
+ : persona.saying
128
+ ? {
129
+ text: persona.saying,
130
+ ...(persona.full ? { fullText: persona.full } : {}),
131
+ more: persona.saying.endsWith('…'),
132
+ speaking: persona.state === 'speaking',
133
+ busy: persona.state === 'thinking' ||
134
+ persona.state === 'working' ||
135
+ persona.state === 'speaking',
136
+ ...given,
137
+ ...listed,
138
+ }
139
+ : 'history' in listed
140
+ ? { text: '', ...listed }
141
+ : undefined;
66
142
  return (_jsxs(Box
67
143
  // React Flow lets the pointer through a node that is neither dragged
68
144
  // nor selected (`pointer-events: none` on its wrapper): the character,
@@ -81,37 +157,106 @@ const MemberNode = memo(function MemberNode({ data, }) {
81
157
  gap: 1,
82
158
  cursor: 'default',
83
159
  pointerEvents: 'all',
84
- }, "data-team-member": member.id, "data-member-state": persona.state, children: [_jsx(Handle, { type: "target", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Box, { sx: { width: size, height: size, position: 'relative' }, children: persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: size / 2 - 16, left: -24 }, onClick: () => member.onAway?.(false), children: ["Call ", member.name, " back"] })) : (_jsx(AssistantStage, { character: member.character, state: persona.state, size: size,
160
+ }, "data-team-member": member.id, "data-member-balloon": shown, "data-member-moved": offset.dx || offset.dy
161
+ ? `${Math.round(offset.dx)},${Math.round(offset.dy)}`
162
+ : undefined, "data-member-state": persona.state, children: [_jsx(Handle, { type: "target", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Box, { sx: { width: size, height: size, position: 'relative' }, children: persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: size / 2 - 16, left: -24 }, onClick: () => member.onAway?.(false), children: ["Call ", member.name, " back"] })) : (_jsx(AssistantStage, { character: member.character, state: persona.state, size: size,
85
163
  // In the node, not over the page; the balloon opens toward the
86
164
  // middle of the graph: rightward from the left member.
87
165
  place: side === 'left'
88
166
  ? { position: 'absolute', left: 'auto' }
89
- : { position: 'absolute', right: 'auto' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: member.onToggle ?? (() => undefined), balloonDisplay: member.balloonDisplay ?? 'current', balloon: persona.tool
90
- ? {
91
- text: toolLineText(persona.tool),
92
- tool: persona.tool,
93
- busy: persona.state !== 'idle',
94
- ...given,
167
+ : { position: 'absolute', right: 'auto' }, stageRef: stageRef,
168
+ // Dragged within the graph's box: a press that moves more than
169
+ // a few pixels is a drag; one that does not is a click, and the
170
+ // stage ignores the click that ends a drag.
171
+ onDragStart: event => {
172
+ if (event.button !== 0) {
173
+ return;
95
174
  }
96
- : persona.saying
97
- ? {
98
- text: persona.saying,
99
- more: persona.saying.endsWith('…'),
100
- speaking: persona.state === 'speaking',
101
- busy: persona.state === 'thinking' ||
102
- persona.state === 'working' ||
103
- persona.state === 'speaking',
104
- ...given,
175
+ const start = { x: event.clientX, y: event.clientY };
176
+ const from = offset;
177
+ let dragging = false;
178
+ const onMove = (next) => {
179
+ const dx = next.clientX - start.x;
180
+ const dy = next.clientY - start.y;
181
+ if (!dragging && Math.hypot(dx, dy) <= DRAG_THRESHOLD) {
182
+ return;
105
183
  }
106
- : undefined, expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget, insist: persona.insist, onDismiss: away => member.onAway?.(away !== 'none'),
184
+ dragging = true;
185
+ moving.move(id, {
186
+ dx: from.dx + dx / moving.zoom,
187
+ dy: from.dy + dy / moving.zoom,
188
+ });
189
+ };
190
+ const onUp = () => {
191
+ window.removeEventListener('pointermove', onMove);
192
+ window.removeEventListener('pointerup', onUp);
193
+ window.removeEventListener('pointercancel', onUp);
194
+ };
195
+ window.addEventListener('pointermove', onMove);
196
+ window.addEventListener('pointerup', onUp);
197
+ window.addEventListener('pointercancel', onUp);
198
+ }, onCharacterKeyDown: event => {
199
+ const step = event.shiftKey ? NUDGE_FAR : NUDGE;
200
+ const by = {
201
+ ArrowLeft: { dx: -step, dy: 0 },
202
+ ArrowRight: { dx: step, dy: 0 },
203
+ ArrowUp: { dx: 0, dy: -step },
204
+ ArrowDown: { dx: 0, dy: step },
205
+ };
206
+ const nudge = by[event.key];
207
+ if (nudge) {
208
+ event.preventDefault();
209
+ moving.move(id, {
210
+ dx: offset.dx + nudge.dx,
211
+ dy: offset.dy + nudge.dy,
212
+ });
213
+ }
214
+ }, onResetPosition: offset.dx || offset.dy ? () => moving.reset(id) : undefined, open: false, onToggle: member.onToggle ?? (() => undefined),
215
+ // A click shows and hides its balloon; the menu's *Ask Sales*
216
+ // still takes the reader to the composer.
217
+ onCharacterClick: () => setShown(current => current === 'shown' ||
218
+ (current === 'auto' &&
219
+ (persona.insist || display === 'history'))
220
+ ? 'hidden'
221
+ : 'shown'), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
222
+ setDisplay(next);
223
+ member.onBalloonDisplayChange?.(next);
224
+ }, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: shown === 'hidden'
225
+ ? undefined
226
+ : (balloonNow ??
227
+ (shown === 'shown'
228
+ ? {
229
+ text: `${member.name}, ${member.where ?? ''}`.replace(/, $/, ''),
230
+ }
231
+ : undefined)), expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
232
+ // History, chosen in its menu: shown until Current is.
233
+ insist: shown === 'shown' ||
234
+ (shown === 'auto' && (persona.insist || display === 'history')), onDismiss: away => member.onAway?.(away !== 'none'), suggestions: member.suggestions, onSuggestion: member.onSuggestion, inspector: member.inspector, inspectAgent: member.name, onStop: member.onStop, about: member.about, sandbox: member.sandbox, contextMenu: member.contextMenu,
107
235
  // A member of the graph: what is clicked around it is the graph.
108
- stayPut: true })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: "connections", type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: 'auto', bottom: 0 }, isConnectable: false })) : null] }));
236
+ stayPut: true,
237
+ // Named as the member, not its character: `Sales (in your
238
+ // browser)`; the character describes it.
239
+ agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: "connections", type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: 'auto', bottom: 0 }, isConnectable: false })) : null] }));
109
240
  });
241
+ /** A member's accessible name: its name, and where it runs. */
242
+ export function memberLabel(member) {
243
+ return member.where ? `${member.name} (${member.where})` : member.name;
244
+ }
245
+ /** A connection's accessible name: `Odoo, Accounting's MCP connection`. */
246
+ export function connectionLabel(connection, member) {
247
+ return member
248
+ ? `${connection.label}, ${member}\u2019s MCP connection`
249
+ : `${connection.label}, an MCP connection`;
250
+ }
110
251
  /** A connection of a member's: the server's mark, what it reaches, and how. */
111
252
  const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
112
253
  const { member, connection: connectionId, size } = data;
113
- const connection = useContext(Members)[member]?.connections?.find(known => known.id === connectionId);
254
+ const owner = useContext(Members)[member];
255
+ const connection = owner?.connections?.find(known => known.id === connectionId);
114
256
  const tool = useContext(Busy)[id];
257
+ // Its details, shown and hidden by a click or Enter on its mark.
258
+ const [told, setTold] = useState(false);
259
+ const detailsId = useId();
115
260
  if (!connection) {
116
261
  return null;
117
262
  }
@@ -123,9 +268,15 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
123
268
  gap: '2px',
124
269
  cursor: 'default',
125
270
  pointerEvents: 'all',
126
- }, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Top, style: { ...HANDLE(0), top: 0 }, isConnectable: false }), _jsx(Box, { className: tool ? 'a2a-team-connection-busy' : undefined, sx: {
271
+ }, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Top, style: { ...HANDLE(0), top: 0 }, isConnectable: false }), _jsx(Box, { as: "button", type: "button", className: tool ? 'a2a-team-connection-busy' : undefined, "aria-label": connectionLabel(connection, owner?.name), "aria-expanded": told, "aria-controls": detailsId, "data-connection-figure": "", onClick: () => setTold(shown => !shown), onKeyDown: (event) => {
272
+ if (event.key === 'Escape' && told) {
273
+ event.stopPropagation();
274
+ setTold(false);
275
+ }
276
+ }, sx: {
127
277
  width: size,
128
278
  height: size,
279
+ p: 0,
129
280
  display: 'flex',
130
281
  alignItems: 'center',
131
282
  justifyContent: 'center',
@@ -133,8 +284,30 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
133
284
  border: '1px solid',
134
285
  borderColor: tool ? 'accent.emphasis' : 'border.default',
135
286
  bg: 'canvas.default',
136
- }, children: _jsx(SpecMark, { icon: connection.icon, emoji: connection.emoji, size: Math.round(size * 0.66) }) }), _jsx(Text, { sx: { fontWeight: 600, fontSize: 1, textAlign: 'center' }, children: connection.label }), connection.via && (_jsx(Text, { sx: { fontSize: '10px', color: 'fg.muted', textAlign: 'center' }, "data-connection-via": "", children: connection.via }))] }));
287
+ cursor: 'pointer',
288
+ '&:focus-visible': {
289
+ outline: '2px solid',
290
+ outlineColor: 'var(--focus-outlineColor, var(--fgColor-accent))',
291
+ outlineOffset: 2,
292
+ },
293
+ }, children: _jsx(SpecMark, { icon: connection.icon, emoji: connection.emoji, size: Math.round(size * 0.66) }) }), _jsx(Text, { sx: { fontWeight: 600, fontSize: 1, textAlign: 'center' }, children: connection.label }), connection.via && (_jsx(Text, { sx: { fontSize: '10px', color: 'fg.muted', textAlign: 'center' }, "data-connection-via": "", children: connection.via })), _jsx(Box, { id: detailsId, hidden: !told, "data-connection-details": "", sx: {
294
+ fontSize: '10px',
295
+ color: 'fg.muted',
296
+ textAlign: 'center',
297
+ maxWidth: CONNECTION_WIDTH + 40,
298
+ }, children: connectionDetails(connection, owner?.name, tool) })] }));
137
299
  });
300
+ /** What a connection's details say: the server, who reaches it, and the call running now. */
301
+ export function connectionDetails(connection, member, calling) {
302
+ const tools = connection.tools.length;
303
+ return [
304
+ `${connection.name}, an MCP server${member ? ` ${member} reaches` : ''}.`,
305
+ tools ? `${tools} tool${tools === 1 ? '' : 's'}.` : '',
306
+ calling ? `Now: ${calling}.` : '',
307
+ ]
308
+ .filter(Boolean)
309
+ .join(' ');
310
+ }
138
311
  /** The words on the edge for a flow. */
139
312
  export function flowWords(flow, entry, peer) {
140
313
  return flow === 'asking'
@@ -201,7 +374,7 @@ export function teamViewport(width) {
201
374
  return { zoom, x: (width - WIDTH * zoom) / 2, y: 0 };
202
375
  }
203
376
  /** Draw a team of two over A2A, the link moving with what travels on it. */
204
- export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size = 96, calls = [], }) {
377
+ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size = 96, calls = [], balloonRoom = BALLOON_ROOM, }) {
205
378
  const box = useRef(null);
206
379
  const [width, setWidth] = useState(0);
207
380
  useEffect(() => {
@@ -234,17 +407,53 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
234
407
  const hasConnections = placed.some(({ connections }) => connections.length);
235
408
  // A notebook in a balloon: more room above the members.
236
409
  const room = [entry, peer].some(member => member.persona.notebook)
237
- ? NOTEBOOK_BALLOON_ROOM
238
- : BALLOON_ROOM;
410
+ ? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
411
+ : balloonRoom;
239
412
  const height = Math.ceil((room +
240
413
  memberHeight +
241
414
  (hasConnections ? CONNECTION_GAP + connectionHeight : 0)) *
242
415
  viewport.zoom);
416
+ // Where the members were moved, kept inside the graph's box.
417
+ const total = room +
418
+ memberHeight +
419
+ (hasConnections ? CONNECTION_GAP + connectionHeight : 0);
420
+ const boundsOf = (id) => {
421
+ const layoutX = id === entry.id ? 0 : NODE_WIDTH + GAP;
422
+ const below = placed.find(({ member }) => member === id)?.connections.length
423
+ ? CONNECTION_GAP + connectionHeight
424
+ : 0;
425
+ return {
426
+ minDx: -layoutX,
427
+ maxDx: WIDTH - NODE_WIDTH - layoutX,
428
+ minDy: -room,
429
+ maxDy: total - room - memberHeight - below,
430
+ };
431
+ };
432
+ const [moved, setMoved] = useState(() => ({
433
+ [entry.id]: MOVED.get(movedKey(entry.id)) ?? AT_PLACE,
434
+ [peer.id]: MOVED.get(movedKey(peer.id)) ?? AT_PLACE,
435
+ }));
436
+ const offsetOf = (id) => clampOffset(moved[id] ?? AT_PLACE, boundsOf(id));
437
+ const entryOffset = offsetOf(entry.id);
438
+ const peerOffset = offsetOf(peer.id);
439
+ const movingValue = {
440
+ zoom: viewport.zoom,
441
+ offsetOf,
442
+ move: (id, offset) => {
443
+ const next = clampOffset(offset, boundsOf(id));
444
+ MOVED.set(movedKey(id), next);
445
+ setMoved(current => ({ ...current, [id]: next }));
446
+ },
447
+ reset: (id) => {
448
+ MOVED.delete(movedKey(id));
449
+ setMoved(current => ({ ...current, [id]: AT_PLACE }));
450
+ },
451
+ };
243
452
  const nodes = useMemo(() => [
244
453
  {
245
454
  id: entry.id,
246
455
  type: 'member',
247
- position: { x: 0, y: room },
456
+ position: { x: entryOffset.dx, y: room + entryOffset.dy },
248
457
  width: NODE_WIDTH,
249
458
  height: memberHeight,
250
459
  draggable: false,
@@ -256,7 +465,10 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
256
465
  {
257
466
  id: peer.id,
258
467
  type: 'member',
259
- position: { x: NODE_WIDTH + GAP, y: room },
468
+ position: {
469
+ x: NODE_WIDTH + GAP + peerOffset.dx,
470
+ y: room + peerOffset.dy,
471
+ },
260
472
  width: NODE_WIDTH,
261
473
  height: memberHeight,
262
474
  draggable: false,
@@ -265,13 +477,15 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
265
477
  data: { id: peer.id, side: 'right', size },
266
478
  },
267
479
  ...JSON.parse(placedKey).flatMap(({ member, connections }, side) => {
268
- const xs = connectionsX(side === 0 ? 0 : NODE_WIDTH + GAP, connections.length);
480
+ // A member's connections move with it.
481
+ const moveBy = side === 0 ? entryOffset : peerOffset;
482
+ const xs = connectionsX((side === 0 ? 0 : NODE_WIDTH + GAP) + moveBy.dx, connections.length);
269
483
  return connections.map((connection, at) => ({
270
484
  id: connectionNodeId(member, connection),
271
485
  type: 'connection',
272
486
  position: {
273
487
  x: xs[at],
274
- y: room + memberHeight + CONNECTION_GAP,
488
+ y: room + memberHeight + CONNECTION_GAP + moveBy.dy,
275
489
  },
276
490
  width: CONNECTION_WIDTH,
277
491
  height: connectionHeight,
@@ -290,8 +504,29 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
290
504
  placedKey,
291
505
  connectionSize,
292
506
  connectionHeight,
507
+ entryOffset.dx,
508
+ entryOffset.dy,
509
+ peerOffset.dx,
510
+ peerOffset.dy,
293
511
  ]);
294
- const members = useMemo(() => ({ [entry.id]: entry, [peer.id]: peer }), [entry, peer]);
512
+ // The notebook open under the graph runs on a Pyodide kernel in the page:
513
+ // the entry's sandbox, while it is open.
514
+ const notebookKernel = useSignalValue(teamNotebookKernel);
515
+ const entryMember = useMemo(() => entry.sandbox
516
+ ? entry
517
+ : {
518
+ ...entry,
519
+ // The entry runs in the page: its sandbox is the Pyodide kernel
520
+ // of the notebook open under the graph, and none until one is.
521
+ sandbox: notebookKernel
522
+ ? {
523
+ kind: 'browser',
524
+ status: 'running',
525
+ connection: notebookKernel,
526
+ }
527
+ : { kind: 'browser', status: 'not running' },
528
+ }, [entry, notebookKernel]);
529
+ const members = useMemo(() => ({ [entry.id]: entryMember, [peer.id]: peer }), [entry.id, entryMember, peer]);
295
530
  const words = flowWords(flow, entry.name, peer.name);
296
531
  // The tool each connection is answering now, in words, by its node's id.
297
532
  const busy = useMemo(() => {
@@ -379,6 +614,11 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
379
614
  overflow: 'visible',
380
615
  },
381
616
  '& .react-flow__node': { cursor: 'default' },
617
+ // A member is moved by dragging its character.
618
+ '& [data-team-member] [data-assistant-figure]': { cursor: 'grab' },
619
+ '& [data-team-member] [data-assistant-figure]:active': {
620
+ cursor: 'grabbing',
621
+ },
382
622
  }, children: [_jsx(Box, { as: "span", "aria-live": "polite", sx: {
383
623
  position: 'absolute',
384
624
  width: 1,
@@ -386,8 +626,8 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
386
626
  overflow: 'hidden',
387
627
  clip: 'rect(0 0 0 0)',
388
628
  whiteSpace: 'nowrap',
389
- }, children: heard }), _jsx(Members.Provider, { value: members, children: _jsx(Busy.Provider, { value: busy.byNode, children: _jsx(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: NODE_TYPES, edgeTypes: EDGE_TYPES, viewport: viewport, nodesDraggable: false, nodesConnectable: false, nodesFocusable: false, edgesFocusable: false, elementsSelectable: false, deleteKeyCode: null, selectionKeyCode: null, multiSelectionKeyCode: null, panOnDrag: false, panOnScroll: false, zoomOnScroll: false, zoomOnPinch: false, zoomOnDoubleClick: false, preventScrolling: false, style: { overflow: 'visible', background: 'transparent' },
390
- // The library is MIT: its credit is not required on the page.
391
- proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${peer.name}, over A2A` }) }) })] }));
629
+ }, children: heard }), _jsx(Moving.Provider, { value: movingValue, children: _jsx(Members.Provider, { value: members, children: _jsx(Busy.Provider, { value: busy.byNode, children: _jsx(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: NODE_TYPES, edgeTypes: EDGE_TYPES, viewport: viewport, nodesDraggable: false, nodesConnectable: false, nodesFocusable: false, edgesFocusable: false, elementsSelectable: false, deleteKeyCode: null, selectionKeyCode: null, multiSelectionKeyCode: null, panOnDrag: false, panOnScroll: false, zoomOnScroll: false, zoomOnPinch: false, zoomOnDoubleClick: false, preventScrolling: false, style: { overflow: 'visible', background: 'transparent' },
630
+ // The library is MIT: its credit is not required on the page.
631
+ proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${peer.name}, over A2A` }) }) }) })] }));
392
632
  }
393
633
  export default A2ATeamGraph;
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
3
3
  import { useTheme } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
5
  import { JupyterReactTheme, Notebook, ServiceManagerLess, } from '@datalayer/jupyter-react';
6
+ import { JUPYTER_DARK_VARIABLES, JUPYTER_LIGHT_VARIABLES, } from './jupyterThemeVariables';
6
7
  /** Each preview has its own id in jupyter-react's store. */
7
8
  let drawn = 0;
8
9
  export function NotebookPreviewView({ nbformat, height, }) {
@@ -16,7 +17,14 @@ export function NotebookPreviewView({ nbformat, height, }) {
16
17
  }, [nbformat]);
17
18
  const content = useMemo(() => JSON.parse(JSON.stringify(nbformat)), [nbformat]);
18
19
  const { colorScheme } = useTheme();
19
- return (_jsxs(Box, { "data-notebook-preview-theme": "", sx: { height }, children: [_jsx("span", { "data-jupyter-react-theme-root": "notebook-preview", hidden: true }), _jsx(JupyterReactTheme, { colormode: colorScheme?.startsWith('dark') ? 'dark' : 'light', children: _jsx(Box, { "data-notebook-preview-view": id, sx: {
20
+ const mode = colorScheme?.startsWith('dark') ? 'dark' : 'light';
21
+ return (_jsxs(Box, { "data-notebook-preview-theme": mode, sx: { height },
22
+ // JupyterLab writes its theme for the whole page, so two previews in
23
+ // two modes would share the last one written: each carries its own
24
+ // mode's variables on its own subtree.
25
+ style: (mode === 'dark'
26
+ ? JUPYTER_DARK_VARIABLES
27
+ : JUPYTER_LIGHT_VARIABLES), children: [_jsx("span", { "data-jupyter-react-theme-root": "notebook-preview", hidden: true }), _jsx(JupyterReactTheme, { colormode: mode, children: _jsx(Box, { "data-notebook-preview-view": id, sx: {
20
28
  height,
21
29
  // No toolbar: no room kept for one.
22
30
  '& .datalayer-NotebookPanel-header': {
@@ -25,11 +25,13 @@ export type TeamNotebookProps = {
25
25
  title: string;
26
26
  /** How tall the notebook is, in pixels. */
27
27
  height?: number;
28
+ /** Leave the heading out: the dialog it is drawn in shows the title. */
29
+ hideTitle?: boolean;
28
30
  };
29
31
  /** The file a notebook is saved as: the peer's name for it, else its name. */
30
32
  export declare function notebookFileName(notebook: A2APeerArtifact): string;
31
33
  /** A notebook a member of the team gave, under its title, to run in the page. */
32
- export declare function TeamNotebook({ notebook, title, height, }: TeamNotebookProps): JSX.Element;
34
+ export declare function TeamNotebook({ notebook, title, height, hideTitle, }: TeamNotebookProps): JSX.Element;
33
35
  /**
34
36
  * A notebook as a balloon's large visual: read-only in the balloon, and,
35
37
  * expanded, this notebook — editable, run on the browser sandbox, loaded
@@ -16,7 +16,7 @@ export function notebookFileName(notebook) {
16
16
  return `${stem}.ipynb`;
17
17
  }
18
18
  /** A notebook a member of the team gave, under its title, to run in the page. */
19
- export function TeamNotebook({ notebook, title, height = 560, }) {
19
+ export function TeamNotebook({ notebook, title, height = 560, hideTitle = false, }) {
20
20
  return (_jsxs(Box, { as: "section", "aria-label": title, "data-team-notebook": "",
21
21
  // Focused when the notebook in a balloon is clicked (`focusExpanded`).
22
22
  tabIndex: -1, sx: {
@@ -32,7 +32,7 @@ export function TeamNotebook({ notebook, title, height = 560, }) {
32
32
  outlineOffset: 4,
33
33
  borderRadius: 2,
34
34
  },
35
- }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, m: 0 }, children: title }), _jsx(Suspense, { fallback: _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: "Opening the notebook\u2026" }), children: _jsx(TeamNotebookView, { notebook: notebook, fileName: notebookFileName(notebook), height: height }) })] }));
35
+ }, children: [!hideTitle && (_jsx(Heading, { as: "h3", sx: { fontSize: 2, m: 0 }, children: title })), _jsx(Suspense, { fallback: _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: "Opening the notebook\u2026" }), children: _jsx(TeamNotebookView, { notebook: notebook, fileName: notebookFileName(notebook), height: height }) })] }));
36
36
  }
37
37
  /**
38
38
  * A notebook as a balloon's large visual: read-only in the balloon, and,
@@ -43,7 +43,7 @@ export function notebookBalloonVisual(notebook, title, height) {
43
43
  return {
44
44
  id: `notebook:${notebook.filename ?? notebook.name}`,
45
45
  title,
46
- render: () => (_jsx(TeamNotebook, { notebook: notebook, title: title, height: height })),
46
+ render: place => (_jsx(TeamNotebook, { notebook: notebook, title: title, height: height, hideTitle: place === 'overlay' })),
47
47
  };
48
48
  }
49
49
  export default TeamNotebook;
@@ -8,6 +8,7 @@ import { useSignalValue } from '@datalayer/reactor/react';
8
8
  import { notebookStore } from '@datalayer/jupyter-react';
9
9
  import { EphemeralNotebook } from '../../chat/notebook/EphemeralNotebook';
10
10
  import { createBrowserSandboxService } from '../../loop/plugins/agents/browserService';
11
+ import { teamNotebookKernel } from './teamNotebookKernel';
11
12
  /** Each notebook drawn has its own id in jupyter-react's store. */
12
13
  let drawn = 0;
13
14
  /** Save a notebook as an `.ipynb` file, as Jupyter writes one. */
@@ -57,6 +58,20 @@ export function TeamNotebookView({ notebook, fileName, height, }) {
57
58
  const manager = service.getServiceManager() ??
58
59
  undefined;
59
60
  const running = snapshot.state === 'running' && manager !== undefined;
61
+ // Say which kernel the notebook runs on, while it does: the browser
62
+ // agent's *Code Sandbox Details…* lists its variables.
63
+ useEffect(() => {
64
+ const kernel = running ? service.getKernelConnection() : null;
65
+ if (!kernel) {
66
+ return;
67
+ }
68
+ teamNotebookKernel.value = kernel;
69
+ return () => {
70
+ if (teamNotebookKernel.peek() === kernel) {
71
+ teamNotebookKernel.value = null;
72
+ }
73
+ };
74
+ }, [running, service]);
60
75
  return (_jsx(QueryClientProvider, { client: queries, children: _jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsxs(Box, { sx: {
61
76
  display: 'flex',
62
77
  alignItems: 'center',
@@ -0,0 +1,9 @@
1
+ /**
2
+ * JupyterLab's theme variables, light and dark, as its theme extensions state
3
+ * them. Generated by scripts/codegen/generate_jupyter_theme_variables.mjs: do
4
+ * not edit.
5
+ *
6
+ * @module components/teams/jupyterThemeVariables
7
+ */
8
+ export declare const JUPYTER_LIGHT_VARIABLES: Readonly<Record<string, string>>;
9
+ export declare const JUPYTER_DARK_VARIABLES: Readonly<Record<string, string>>;