@datalayer/agent-runtimes 1.3.75 → 1.3.79

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 (110) 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 +12 -2
  4. package/lib/chat/ChatFloating.js +48 -11
  5. package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
  6. package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
  7. package/lib/chat/assistant/AssistantStage.d.ts +51 -5
  8. package/lib/chat/assistant/AssistantStage.js +60 -10
  9. package/lib/chat/assistant/BalloonParts.d.ts +18 -4
  10. package/lib/chat/assistant/BalloonParts.js +46 -41
  11. package/lib/chat/assistant/BalloonVisual.js +2 -1
  12. package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
  13. package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
  14. package/lib/chat/assistant/SpeechBalloon.d.ts +8 -6
  15. package/lib/chat/assistant/SpeechBalloon.js +29 -30
  16. package/lib/chat/assistant/assistantDetails.d.ts +84 -0
  17. package/lib/chat/assistant/assistantDetails.js +140 -0
  18. package/lib/chat/assistant/formats/files.d.ts +23 -0
  19. package/lib/chat/assistant/formats/files.js +48 -0
  20. package/lib/chat/assistant/formats/index.d.ts +2 -0
  21. package/lib/chat/assistant/formats/index.js +1 -0
  22. package/lib/chat/assistant/state.d.ts +7 -14
  23. package/lib/chat/assistant/state.js +21 -20
  24. package/lib/chat/assistant/toolLine.d.ts +16 -0
  25. package/lib/chat/assistant/toolLine.js +46 -0
  26. package/lib/chat/base/ChatBase.js +20 -0
  27. package/lib/chat/base/loopMessage.d.ts +37 -0
  28. package/lib/chat/base/loopMessage.js +37 -0
  29. package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
  30. package/lib/chat/messages/ChatMarkdown.js +18 -0
  31. package/lib/chat/messages/ChatMessageList.d.ts +8 -1
  32. package/lib/chat/messages/ChatMessageList.js +24 -18
  33. package/lib/chat/styles/streamdownStyles.js +3 -1
  34. package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
  35. package/lib/chat/tools/ToolCallDisplay.js +8 -7
  36. package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
  37. package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
  38. package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
  39. package/lib/components/teams/A2ATeamGraph.d.ts +30 -3
  40. package/lib/components/teams/A2ATeamGraph.js +264 -41
  41. package/lib/components/teams/TeamNotebookView.js +15 -0
  42. package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
  43. package/lib/components/teams/teamNotebookKernel.js +17 -0
  44. package/lib/components/teams/useA2ATeam.d.ts +10 -6
  45. package/lib/components/teams/useA2ATeam.js +29 -8
  46. package/lib/examples/AgentA2ATeamExample.js +6 -0
  47. package/lib/examples/AssistantExample.js +3 -24
  48. package/lib/examples/AssistantGalleryExample.js +2 -1
  49. package/lib/examples/exampleGroups.d.ts +1 -1
  50. package/lib/examples/exampleGroups.js +1 -1
  51. package/lib/examples/utils/assistantGallery.d.ts +7 -6
  52. package/lib/examples/utils/assistantGallery.js +23 -8
  53. package/lib/jupyter/exec/Python.d.ts +0 -1
  54. package/lib/jupyter/exec/Python.js +0 -32
  55. package/lib/jupyter/exec/Snippets.d.ts +4 -12
  56. package/lib/jupyter/exec/Snippets.js +0 -8
  57. package/lib/loop/apps/AppFeedback.js +3 -1
  58. package/lib/loop/apps/AppKept.d.ts +22 -0
  59. package/lib/loop/apps/AppKept.js +36 -0
  60. package/lib/loop/apps/AppRenderer.d.ts +11 -1
  61. package/lib/loop/apps/AppRenderer.js +21 -3
  62. package/lib/loop/apps/checks.d.ts +7 -0
  63. package/lib/loop/apps/checks.js +39 -0
  64. package/lib/loop/apps/index.d.ts +3 -0
  65. package/lib/loop/apps/index.js +3 -0
  66. package/lib/loop/apps/kept.d.ts +30 -0
  67. package/lib/loop/apps/kept.js +52 -0
  68. package/lib/loop/apps/records.d.ts +12 -4
  69. package/lib/loop/apps/records.js +13 -5
  70. package/lib/loop/apps/saved.d.ts +30 -0
  71. package/lib/loop/apps/saved.js +40 -0
  72. package/lib/loop/embed/AppEmbed.d.ts +8 -1
  73. package/lib/loop/embed/AppEmbed.js +4 -4
  74. package/lib/loop/embed/LoopEmbed.d.ts +6 -1
  75. package/lib/loop/embed/LoopEmbed.js +4 -2
  76. package/lib/loop/embed/element.js +1 -1
  77. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
  78. package/lib/loop/plugins/agents/browserService.d.ts +7 -1
  79. package/lib/loop/plugins/agents/browserService.js +4 -0
  80. package/lib/loop/plugins/agents/plugin.d.ts +16 -0
  81. package/lib/loop/plugins/agents/switchable.d.ts +3 -0
  82. package/lib/loop/plugins/agents/switchable.js +1 -0
  83. package/lib/loop/plugins/app-activity/index.js +3 -0
  84. package/lib/loop/plugins/app-computer/index.js +3 -0
  85. package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
  86. package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
  87. package/lib/loop/plugins/app-rules/index.js +3 -0
  88. package/lib/loop/plugins/assistant/index.d.ts +7 -2
  89. package/lib/loop/plugins/assistant/index.js +53 -4
  90. package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
  91. package/lib/loop/plugins/assistant-characters/index.js +17 -0
  92. package/lib/loop/plugins/chat/ChatView.js +4 -1
  93. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
  94. package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
  95. package/lib/loop/presets.d.ts +4 -0
  96. package/lib/loop/presets.js +3 -1
  97. package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
  98. package/lib/loop/shell/LoopWorkspace.js +5 -4
  99. package/lib/loop/shell/SidebarRail.d.ts +53 -0
  100. package/lib/loop/shell/SidebarRail.js +74 -0
  101. package/lib/loop/shell/index.d.ts +2 -0
  102. package/lib/loop/shell/index.js +2 -0
  103. package/lib/loop/shell/paneMotion.d.ts +21 -0
  104. package/lib/loop/shell/paneMotion.js +37 -0
  105. package/lib/specs/apps.js +8 -5
  106. package/lib/specs/uiPlugins.js +529 -0
  107. package/lib/types/agentspecs.d.ts +5 -3
  108. package/lib/types/chat.d.ts +5 -0
  109. package/package.json +2 -2
  110. 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
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';
@@ -44,18 +46,62 @@ 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
83
  // Its balloon's display, as its menu changes it.
54
84
  const [display, setDisplay] = useState();
55
- if (!member) {
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) {
56
103
  return null;
57
104
  }
58
- const { persona } = member;
59
105
  const shownDisplay = display ?? member.balloonDisplay ?? 'current';
60
106
  // History: what it said and did, listed.
61
107
  const listed = shownDisplay === 'history' && member.history?.length
@@ -70,6 +116,29 @@ const MemberNode = memo(function MemberNode({ data, }) {
70
116
  visual: notebookBalloonVisual(persona.notebook, notebookTitle),
71
117
  }
72
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;
73
142
  return (_jsxs(Box
74
143
  // React Flow lets the pointer through a node that is neither dragged
75
144
  // nor selected (`pointer-events: none` on its wrapper): the character,
@@ -88,47 +157,106 @@ const MemberNode = memo(function MemberNode({ data, }) {
88
157
  gap: 1,
89
158
  cursor: 'default',
90
159
  pointerEvents: 'all',
91
- }, "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,
92
163
  // In the node, not over the page; the balloon opens toward the
93
164
  // middle of the graph: rightward from the left member.
94
165
  place: side === 'left'
95
166
  ? { position: 'absolute', left: 'auto' }
96
- : { position: 'absolute', right: 'auto' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: member.onToggle ?? (() => undefined), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
97
- setDisplay(next);
98
- member.onBalloonDisplayChange?.(next);
99
- }, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: persona.tool
100
- ? {
101
- text: toolLineText(persona.tool),
102
- tool: persona.tool,
103
- busy: persona.state !== 'idle',
104
- ...given,
105
- ...listed,
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;
106
174
  }
107
- : persona.saying
108
- ? {
109
- text: persona.saying,
110
- ...(persona.full ? { fullText: persona.full } : {}),
111
- more: persona.saying.endsWith('…'),
112
- speaking: persona.state === 'speaking',
113
- busy: persona.state === 'thinking' ||
114
- persona.state === 'working' ||
115
- persona.state === 'speaking',
116
- ...given,
117
- ...listed,
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;
118
183
  }
119
- : 'history' in listed
120
- ? { text: '', ...listed }
121
- : undefined, expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
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,
122
232
  // History, chosen in its menu: shown until Current is.
123
- insist: 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, contextMenu: member.contextMenu,
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,
124
235
  // A member of the graph: what is clicked around it is the graph.
125
- 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] }));
126
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
+ }
127
251
  /** A connection of a member's: the server's mark, what it reaches, and how. */
128
252
  const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
129
253
  const { member, connection: connectionId, size } = data;
130
- 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);
131
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();
132
260
  if (!connection) {
133
261
  return null;
134
262
  }
@@ -140,9 +268,15 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
140
268
  gap: '2px',
141
269
  cursor: 'default',
142
270
  pointerEvents: 'all',
143
- }, 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: {
144
277
  width: size,
145
278
  height: size,
279
+ p: 0,
146
280
  display: 'flex',
147
281
  alignItems: 'center',
148
282
  justifyContent: 'center',
@@ -150,8 +284,30 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
150
284
  border: '1px solid',
151
285
  borderColor: tool ? 'accent.emphasis' : 'border.default',
152
286
  bg: 'canvas.default',
153
- }, 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) })] }));
154
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
+ }
155
311
  /** The words on the edge for a flow. */
156
312
  export function flowWords(flow, entry, peer) {
157
313
  return flow === 'asking'
@@ -257,11 +413,47 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
257
413
  memberHeight +
258
414
  (hasConnections ? CONNECTION_GAP + connectionHeight : 0)) *
259
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
+ };
260
452
  const nodes = useMemo(() => [
261
453
  {
262
454
  id: entry.id,
263
455
  type: 'member',
264
- position: { x: 0, y: room },
456
+ position: { x: entryOffset.dx, y: room + entryOffset.dy },
265
457
  width: NODE_WIDTH,
266
458
  height: memberHeight,
267
459
  draggable: false,
@@ -273,7 +465,10 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
273
465
  {
274
466
  id: peer.id,
275
467
  type: 'member',
276
- position: { x: NODE_WIDTH + GAP, y: room },
468
+ position: {
469
+ x: NODE_WIDTH + GAP + peerOffset.dx,
470
+ y: room + peerOffset.dy,
471
+ },
277
472
  width: NODE_WIDTH,
278
473
  height: memberHeight,
279
474
  draggable: false,
@@ -282,13 +477,15 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
282
477
  data: { id: peer.id, side: 'right', size },
283
478
  },
284
479
  ...JSON.parse(placedKey).flatMap(({ member, connections }, side) => {
285
- 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);
286
483
  return connections.map((connection, at) => ({
287
484
  id: connectionNodeId(member, connection),
288
485
  type: 'connection',
289
486
  position: {
290
487
  x: xs[at],
291
- y: room + memberHeight + CONNECTION_GAP,
488
+ y: room + memberHeight + CONNECTION_GAP + moveBy.dy,
292
489
  },
293
490
  width: CONNECTION_WIDTH,
294
491
  height: connectionHeight,
@@ -307,8 +504,29 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
307
504
  placedKey,
308
505
  connectionSize,
309
506
  connectionHeight,
507
+ entryOffset.dx,
508
+ entryOffset.dy,
509
+ peerOffset.dx,
510
+ peerOffset.dy,
310
511
  ]);
311
- 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]);
312
530
  const words = flowWords(flow, entry.name, peer.name);
313
531
  // The tool each connection is answering now, in words, by its node's id.
314
532
  const busy = useMemo(() => {
@@ -396,6 +614,11 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
396
614
  overflow: 'visible',
397
615
  },
398
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
+ },
399
622
  }, children: [_jsx(Box, { as: "span", "aria-live": "polite", sx: {
400
623
  position: 'absolute',
401
624
  width: 1,
@@ -403,8 +626,8 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
403
626
  overflow: 'hidden',
404
627
  clip: 'rect(0 0 0 0)',
405
628
  whiteSpace: 'nowrap',
406
- }, 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' },
407
- // The library is MIT: its credit is not required on the page.
408
- 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` }) }) }) })] }));
409
632
  }
410
633
  export default A2ATeamGraph;
@@ -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,3 @@
1
+ import type { Kernel } from '@jupyterlab/services';
2
+ /** The open team notebook's kernel, or `null` when none is open. */
3
+ export declare const teamNotebookKernel: import("@datalayer/reactor").Signal<Kernel.IKernelConnection | null>;
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The kernel of the notebook a team member gave, while one is open.
7
+ *
8
+ * `TeamNotebookView` runs the notebook on a Pyodide kernel in the page and
9
+ * says so here; the team's graph reads it for the browser agent's *Code
10
+ * Sandbox Details…*, which lists that kernel's variables. Light: a signal,
11
+ * nothing of Jupyter loaded with it.
12
+ *
13
+ * @module components/teams/teamNotebookKernel
14
+ */
15
+ import { signal } from '@datalayer/reactor';
16
+ /** The open team notebook's kernel, or `null` when none is open. */
17
+ export const teamNotebookKernel = signal(null);
@@ -1,4 +1,5 @@
1
- import type { AssistantState, BalloonHistoryMessage } from '../../chat/assistant/state';
1
+ import type { AssistantState } from '../../chat/assistant/state';
2
+ import type { DisplayItem } from '../../types/chat';
2
3
  import { type BrowserModelOptions } from '../../runtimes/browser/model';
3
4
  import { type A2APeer, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
4
5
  import type { AppSpec } from '../../types/agentspecs';
@@ -69,13 +70,16 @@ export type A2ATeam = {
69
70
  setEntryAway: (away: boolean) => void;
70
71
  setPeerAway: (away: boolean) => void;
71
72
  turns: A2ATeamTurn[];
72
- /** The entry's conversation, as its `history` balloon lists it. */
73
- entryHistory: BalloonHistoryMessage[];
74
73
  /**
75
- * What the peer was asked, what it did (its tool lines) and what it
76
- * answered, as its `history` balloon lists it.
74
+ * The entry's conversation, in the chat's model (its messages): what its
75
+ * `history` balloon draws with the chat's components.
77
76
  */
78
- peerHistory: BalloonHistoryMessage[];
77
+ entryHistory: DisplayItem[];
78
+ /**
79
+ * What the peer was asked, the tools it called and what it answered, in
80
+ * the chat's model (messages and tool calls).
81
+ */
82
+ peerHistory: DisplayItem[];
79
83
  /** The peer's last answer, as it gave it. */
80
84
  report: string | null;
81
85
  /** What the peer gave besides words with its last answer, by media type. */
@@ -85,9 +85,31 @@ export function useA2ATeam(options) {
85
85
  const told = useRef(0);
86
86
  const tell = useCallback((role, text) => {
87
87
  told.current += 1;
88
- const id = `peer-${told.current}`;
89
- setPeerHistory(prev => [...prev, { id, role, text }]);
88
+ const message = {
89
+ id: `peer-${told.current}`,
90
+ role,
91
+ content: text,
92
+ createdAt: new Date(),
93
+ };
94
+ setPeerHistory(prev => [...prev, message]);
90
95
  }, []);
96
+ // A tool the peer calls, as the chat holds one: added when it starts,
97
+ // updated when it ends.
98
+ const toolCalled = useCallback((step) => {
99
+ const id = step.id ?? step.name;
100
+ const call = {
101
+ id: `peer-tool:${id}`,
102
+ type: 'tool-call',
103
+ toolCallId: id,
104
+ toolName: toolOwnName(step.name, peerConnections),
105
+ args: {},
106
+ status: step.error ? 'error' : step.ended ? 'complete' : 'executing',
107
+ ...(step.error ? { error: step.error } : {}),
108
+ };
109
+ setPeerHistory(prev => prev.some(item => item.id === call.id)
110
+ ? prev.map(item => (item.id === call.id ? call : item))
111
+ : [...prev, call]);
112
+ }, [peerConnections]);
91
113
  const [report, setReport] = useState(null);
92
114
  const [artifacts, setArtifacts] = useState([]);
93
115
  const [notebook, setNotebook] = useState(null);
@@ -134,10 +156,8 @@ export function useA2ATeam(options) {
134
156
  }));
135
157
  }
136
158
  else if (event.phase === 'working') {
137
- if (event.tool?.ended) {
138
- tell('assistant', event.tool.error
139
- ? `${event.tool.name} failed: ${event.tool.error}`
140
- : `Used ${toolOwnName(event.tool.name, peerConnections)}`);
159
+ if (event.tool) {
160
+ toolCalled(event.tool);
141
161
  }
142
162
  setPeerPersona(prev => ({
143
163
  ...prev,
@@ -184,7 +204,7 @@ export function useA2ATeam(options) {
184
204
  tool: undefined,
185
205
  }));
186
206
  }
187
- }, [peerApp.id, peerConnections, inspector, tell]);
207
+ }, [peerApp.id, peerConnections, inspector, tell, toolCalled]);
188
208
  const agent = useMemo(() => {
189
209
  if (!peer) {
190
210
  return null;
@@ -373,7 +393,8 @@ export function useA2ATeam(options) {
373
393
  const entryHistory = useMemo(() => turns.map((turn, index) => ({
374
394
  id: `turn-${index}`,
375
395
  role: turn.role,
376
- text: turn.text,
396
+ content: turn.text,
397
+ createdAt: new Date(0),
377
398
  })), [turns]);
378
399
  return {
379
400
  entryHistory,
@@ -137,6 +137,7 @@ function AgentA2ATeam() {
137
137
  spec: `${SALES.id}:${SALES.version}`,
138
138
  model: SALES.model || undefined,
139
139
  where: 'in your browser',
140
+ description: SALES.description || undefined,
140
141
  },
141
142
  }, peer: {
142
143
  id: ACCOUNTING.id,
@@ -154,6 +155,11 @@ function AgentA2ATeam() {
154
155
  spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
155
156
  model: ACCOUNTING.model || undefined,
156
157
  where: accountingWhere,
158
+ // What its A2A card says, once it is reached.
159
+ description: peer?.card.description || ACCOUNTING.description || undefined,
160
+ skills: peer?.card.skills.map(skill => skill.name),
161
+ protocol: peer ? 'a2a' : undefined,
162
+ url: peer ? accountingUrl : undefined,
157
163
  },
158
164
  }, flow: team.flow, calls: team.calls,
159
165
  // Room for the balloons: suggestions, *more* and a notebook.