@datalayer/agent-runtimes 1.5.89 → 1.5.91

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 (87) hide show
  1. package/README.md +29 -297
  2. package/dist-embed/datalayer-app-main.js +40 -36
  3. package/lib/apps/embed/element.js +20 -5
  4. package/lib/apps/embed/visitorApp.d.ts +23 -45
  5. package/lib/apps/embed/visitorApp.js +65 -13
  6. package/lib/components/teams/A2ATeamGraph.d.ts +58 -152
  7. package/lib/components/teams/A2ATeamGraph.js +169 -410
  8. package/lib/components/teams/SceneTranscript.d.ts +5 -1
  9. package/lib/components/teams/SceneTranscript.js +2 -2
  10. package/lib/components/teams/SceneView.d.ts +17 -5
  11. package/lib/components/teams/SceneView.js +7 -3
  12. package/lib/components/teams/a2aTeamFlow.d.ts +16 -7
  13. package/lib/components/teams/a2aTeamFlow.js +17 -14
  14. package/lib/components/teams/backdrops/catalogue.d.ts +35 -0
  15. package/lib/components/teams/backdrops/catalogue.js +34 -0
  16. package/lib/components/teams/backdrops/edges.d.ts +20 -0
  17. package/lib/components/teams/backdrops/edges.js +29 -0
  18. package/lib/components/teams/backdrops/faces.d.ts +24 -0
  19. package/lib/components/teams/backdrops/faces.js +39 -0
  20. package/lib/components/teams/backdrops/halo.d.ts +14 -0
  21. package/lib/components/teams/backdrops/halo.js +5 -0
  22. package/lib/components/teams/backdrops/hero.d.ts +14 -0
  23. package/lib/components/teams/backdrops/hero.js +178 -0
  24. package/lib/components/teams/backdrops/index.d.ts +22 -0
  25. package/lib/components/teams/backdrops/index.js +38 -0
  26. package/lib/components/teams/backdrops/nodes.d.ts +123 -0
  27. package/lib/components/teams/backdrops/nodes.js +265 -0
  28. package/lib/components/teams/backdrops/plain.d.ts +14 -0
  29. package/lib/components/teams/backdrops/plain.js +116 -0
  30. package/lib/components/teams/backdrops/studio.d.ts +17 -0
  31. package/lib/components/teams/backdrops/studio.js +122 -0
  32. package/lib/components/teams/backdrops/theme.d.ts +14 -0
  33. package/lib/components/teams/backdrops/theme.js +19 -0
  34. package/lib/components/teams/backdrops/types.d.ts +214 -0
  35. package/lib/components/teams/backdrops/types.js +5 -0
  36. package/lib/components/teams/index.d.ts +2 -0
  37. package/lib/components/teams/index.js +2 -0
  38. package/lib/components/teams/sceneTranscriptRules.d.ts +32 -1
  39. package/lib/components/teams/sceneTranscriptRules.js +104 -2
  40. package/lib/components/teams/transcript/RecordingLabel.d.ts +17 -0
  41. package/lib/components/teams/transcript/RecordingLabel.js +7 -0
  42. package/lib/components/teams/transcript/SequenceDiagram.d.ts +39 -0
  43. package/lib/components/teams/transcript/SequenceDiagram.js +190 -0
  44. package/lib/components/teams/transcript/TranscriptDetail.d.ts +27 -0
  45. package/lib/components/teams/transcript/TranscriptDetail.js +48 -0
  46. package/lib/components/teams/transcript/TranscriptTable.d.ts +23 -0
  47. package/lib/components/teams/transcript/TranscriptTable.js +112 -0
  48. package/lib/components/teams/transcript/TranscriptViews.d.ts +66 -0
  49. package/lib/components/teams/transcript/TranscriptViews.js +135 -0
  50. package/lib/components/teams/transcript/TranscriptViewsTypes.d.ts +13 -0
  51. package/lib/components/teams/transcript/TranscriptViewsTypes.js +5 -0
  52. package/lib/components/teams/transcript/index.d.ts +23 -0
  53. package/lib/components/teams/transcript/index.js +27 -0
  54. package/lib/components/teams/transcript/recordingPlayback.d.ts +52 -0
  55. package/lib/components/teams/transcript/recordingPlayback.js +146 -0
  56. package/lib/components/teams/transcript/selection.d.ts +59 -0
  57. package/lib/components/teams/transcript/selection.js +79 -0
  58. package/lib/components/teams/transcript/sequenceDiagramRules.d.ts +35 -0
  59. package/lib/components/teams/transcript/sequenceDiagramRules.js +158 -0
  60. package/lib/components/teams/transcript/transcriptDetailRules.d.ts +80 -0
  61. package/lib/components/teams/transcript/transcriptDetailRules.js +208 -0
  62. package/lib/components/teams/transcript/transcriptExport.d.ts +31 -0
  63. package/lib/components/teams/transcript/transcriptExport.js +82 -0
  64. package/lib/components/teams/transcript/transcriptRows.d.ts +76 -0
  65. package/lib/components/teams/transcript/transcriptRows.js +187 -0
  66. package/lib/components/teams/transcript/useSceneRecording.d.ts +45 -0
  67. package/lib/components/teams/transcript/useSceneRecording.js +96 -0
  68. package/lib/examples/ScenesExample.js +16 -3
  69. package/lib/portable/hosts/agentClient.d.ts +8 -0
  70. package/lib/portable/hosts/agentClient.js +2 -0
  71. package/lib/portable/hosts/approvalRenderers.d.ts +10 -2
  72. package/lib/portable/hosts/approvalRenderers.js +81 -2
  73. package/lib/portable/hosts/contracts.d.ts +1 -0
  74. package/lib/portable/hosts/contracts.js +2 -0
  75. package/lib/portable/hosts/contracts.json +56 -2
  76. package/lib/specs/actions.js +2 -0
  77. package/lib/specs/apps.d.ts +1 -0
  78. package/lib/specs/apps.js +219 -0
  79. package/lib/specs/sceneRecordings.d.ts +14 -0
  80. package/lib/specs/sceneRecordings.js +12 -0
  81. package/lib/specs/sceneSchema.js +11 -0
  82. package/lib/specs/scenes.d.ts +1 -0
  83. package/lib/specs/scenes.js +159 -0
  84. package/lib/types/scenes.d.ts +54 -0
  85. package/package.json +2 -1
  86. package/scripts/codegen/generate_mcp_servers.py +5 -16
  87. package/scripts/codegen/generate_scenes.py +80 -0
@@ -1,65 +1,29 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, memo, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
3
- import { BaseEdge, EdgeLabelRenderer, Handle, Position, ReactFlow, } from '@xyflow/react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { ReactFlow } from '@xyflow/react';
4
4
  import '@xyflow/react/dist/base.css';
5
- import { Button, Label, Text } from '@primer/react';
6
5
  import { Box } from '@datalayer/primer-addons';
7
- import { AssistantStage, } from '../../chat/assistant/AssistantStage.js';
8
6
  import { useSignalValue } from '@datalayer/reactor/react';
9
7
  import { teamNotebookKernel } from './teamNotebookKernel.js';
10
- import { SpecMark } from '../../chat/marks/SpecMark.js';
11
- import { toolWords, } from './a2aTeamFlow.js';
12
- import { NotebookPreview } from './NotebookPreview.js';
13
- import { notebookBalloonVisual } from './TeamNotebook.js';
8
+ import { callFlow, toolWords, } from './a2aTeamFlow.js';
14
9
  import { useBalloonRoom } from './useBalloonRoom.js';
15
- import { toolLineText } from '../../chat/assistant/toolLine.js';
16
- /** The graph's own coordinates: the members, and room above them for their balloons. */
17
- const NODE_WIDTH = 200;
18
- const BALLOON_ROOM = 144;
10
+ import { sceneBackdrop, sceneStageSx } from './backdrops/index.js';
11
+ import { AT_PLACE, Busy, MEMBER_HANDLES, Members, Moving, clampOffset, connectionNodeId, } from './backdrops/nodes.js';
12
+ import { STUDIO_LAYOUT } from './backdrops/studio.js';
13
+ export { MEMBER_HANDLES, clampOffset, connectionDetails, connectionLabel, memberLabel, } from './backdrops/nodes.js';
14
+ export { arrowPath } from './backdrops/edges.js';
15
+ export { CallEdge, LinkEdge, MemberNode, ConnectionNode, } from './backdrops/studio.js';
16
+ /** Whether a selection is on the edge between two ends: by its id, or by its ends either way. */
17
+ export function edgeSelected(selected, from, to) {
18
+ return Boolean(selected &&
19
+ (selected.id === `${from}->${to}` ||
20
+ (selected.from === from && selected.to === to) ||
21
+ (selected.from === to && selected.to === from)));
22
+ }
23
+ /** The studio's width at full scale: the entry, a peer, and the room between them. */
24
+ const WIDTH = STUDIO_LAYOUT.nodeWidth * 2 + STUDIO_LAYOUT.gap;
19
25
  /** The room a balloon that holds a notebook needs above its member. */
20
26
  const NOTEBOOK_BALLOON_ROOM = 480;
21
- /** How tall the notebook in a balloon grows before it scrolls. */
22
- const NOTEBOOK_PREVIEW_HEIGHT = 180;
23
- const GAP = 220;
24
- const WIDTH = NODE_WIDTH * 2 + GAP;
25
- /** Between two rows of members. */
26
- const ROW_GAP = 48;
27
- /** A connection's node: half a member's. */
28
- const CONNECTION_WIDTH = NODE_WIDTH / 2;
29
- /** Between a member and its connections, to its right. */
30
- const CONNECTION_GAP = 44;
31
- const CONNECTIONS_APART = 16;
32
- /** Where a member's connections start: right of it. */
33
- const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
34
- /** Below the character's middle, so the A2A edge leaving it passes above. */
35
- /** What a member's connections take beside it. */
36
- const CONNECTIONS_BESIDE = CONNECTION_GAP + CONNECTION_WIDTH;
37
- /** Where the edge leaves and reaches a member: the middle of its character. */
38
- const HANDLE = (size) => ({
39
- opacity: 0,
40
- pointerEvents: 'none',
41
- top: size / 2,
42
- width: 1,
43
- height: 1,
44
- minWidth: 0,
45
- minHeight: 0,
46
- border: 0,
47
- });
48
- /**
49
- * The handles an edge leaves and reaches a member by, named: the middle of
50
- * its character on each side, chosen by where the other member stands.
51
- */
52
- export const MEMBER_HANDLES = {
53
- inLeft: 'in-left',
54
- outRight: 'out-right',
55
- inTop: 'in-top',
56
- outBottom: 'out-bottom',
57
- inRight: 'in-right',
58
- outLeft: 'out-left',
59
- inBottom: 'in-bottom',
60
- outTop: 'out-top',
61
- connections: 'connections',
62
- };
63
27
  /**
64
28
  * The handles for an edge from one place to another: rightward across,
65
29
  * leftward across, or down or up when the other member is mostly below or
@@ -89,243 +53,12 @@ export function handlesBetween(from, to) {
89
53
  targetHandle: MEMBER_HANDLES.inBottom,
90
54
  };
91
55
  }
92
- /**
93
- * The members as they are now, by id. Not in the nodes' data: a node whose
94
- * object changes is taken by React Flow for a new one, measured again, and
95
- * the edge is dropped until it is — the line vanished as the characters
96
- * moved. The nodes stay the same objects; what they show is read from here.
97
- */
98
- const Members = createContext({});
99
- /** The connections being called now, by node id: read by their nodes, as the members are. */
100
- const Busy = createContext({});
101
- const AT_PLACE = { dx: 0, dy: 0 };
102
- /** An offset kept inside its bounds. */
103
- export function clampOffset(offset, bounds) {
104
- return {
105
- dx: Math.min(bounds.maxDx, Math.max(bounds.minDx, offset.dx)),
106
- dy: Math.min(bounds.maxDy, Math.max(bounds.minDy, offset.dy)),
107
- };
108
- }
109
56
  /**
110
57
  * Where the members were moved, by page and member: kept for the session,
111
58
  * in memory, so a graph drawn again on the page keeps them where they were.
112
59
  */
113
60
  const MOVED = new Map();
114
61
  const movedKey = (id) => `${typeof location === 'undefined' ? '' : location.pathname}#${id}`;
115
- /** How far a press moves before it is a drag, not a click, in pixels. */
116
- const DRAG_THRESHOLD = 4;
117
- /** How far an arrow key moves a member; with Shift, more. */
118
- const NUDGE = 10;
119
- const NUDGE_FAR = 40;
120
- /** The members' moves, read by their nodes. */
121
- const Moving = createContext({
122
- zoom: 1,
123
- offsetOf: () => AT_PLACE,
124
- move: () => undefined,
125
- reset: () => undefined,
126
- });
127
- /** A connection's node id: beside its member. */
128
- const connectionNodeId = (member, connection) => `${member}/${connection}`;
129
- const MemberNode = memo(function MemberNode({ data, }) {
130
- const { id, side, size } = data;
131
- const member = useContext(Members)[id];
132
- const moving = useContext(Moving);
133
- const offset = moving.offsetOf(id);
134
- const stageRef = useRef(null);
135
- // Its balloon's display, as its menu changes it.
136
- const [display, setDisplay] = useState();
137
- // Its balloon as a click on it left it: shown, hidden, or as the team
138
- // says (`auto`: what it is doing, and on hover).
139
- const [shown, setShown] = useState('auto');
140
- // What it says or does now: something new reopens a hidden balloon.
141
- const persona = member?.persona;
142
- const news = [
143
- persona?.saying,
144
- persona?.tool ? toolLineText(persona.tool) : '',
145
- persona?.notebook?.name,
146
- ].join('\u0000');
147
- const seen = useRef(news);
148
- useEffect(() => {
149
- if (news !== seen.current) {
150
- seen.current = news;
151
- setShown(current => (current === 'hidden' ? 'auto' : current));
152
- }
153
- }, [news]);
154
- if (!member || !persona) {
155
- return null;
156
- }
157
- const shownDisplay = display ?? member.balloonDisplay ?? 'current';
158
- // History: what it said and did, listed.
159
- const listed = shownDisplay === 'history' && member.history?.length
160
- ? { history: member.history }
161
- : {};
162
- // A notebook it was given: read-only in its balloon, and its large visual
163
- // the one that runs, editable — under the graph, or in a dialog.
164
- const notebookTitle = member.notebookTitle ?? (persona.notebook?.name || 'The notebook');
165
- const given = persona.notebook
166
- ? {
167
- attachment: (_jsx(NotebookPreview, { notebook: persona.notebook.data, title: notebookTitle, maxHeight: NOTEBOOK_PREVIEW_HEIGHT, onOpen: member.onNotebookOpen, openLabel: member.expandTarget ? 'Open it below to run it' : undefined })),
168
- visual: notebookBalloonVisual(persona.notebook, notebookTitle),
169
- }
170
- : {};
171
- const balloonNow = persona.tool
172
- ? {
173
- text: toolLineText(persona.tool),
174
- tool: persona.tool,
175
- busy: persona.state !== 'idle',
176
- ...given,
177
- ...listed,
178
- }
179
- : persona.saying
180
- ? {
181
- text: persona.saying,
182
- ...(persona.full ? { fullText: persona.full } : {}),
183
- more: persona.saying.endsWith('…'),
184
- speaking: persona.state === 'speaking',
185
- busy: persona.state === 'thinking' ||
186
- persona.state === 'working' ||
187
- persona.state === 'speaking',
188
- ...given,
189
- ...listed,
190
- }
191
- : 'history' in listed
192
- ? { text: '', ...listed }
193
- : undefined;
194
- return (_jsxs(Box
195
- // React Flow lets the pointer through a node that is neither dragged
196
- // nor selected (`pointer-events: none` on its wrapper): the character,
197
- // its balloon and its menu take it back, and are left to themselves —
198
- // no drag, no pan, no wheel.
199
- , {
200
- // React Flow lets the pointer through a node that is neither dragged
201
- // nor selected (`pointer-events: none` on its wrapper): the character,
202
- // its balloon and its menu take it back, and are left to themselves —
203
- // no drag, no pan, no wheel.
204
- className: "nodrag nopan nowheel", width: NODE_WIDTH, display: "flex", flexDirection: "column", alignItems: "center", gap: 1, cursor: "default", pointerEvents: "all", "data-team-member": member.id, "data-member-balloon": shown, "data-member-moved": offset.dx || offset.dy
205
- ? `${Math.round(offset.dx)},${Math.round(offset.dy)}`
206
- : undefined, "data-member-state": persona.state, children: [_jsx(Handle, { id: MEMBER_HANDLES.inLeft, type: "target", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inRight, type: "target", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inTop, type: "target", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inBottom, type: "target", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Box, { 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,
207
- // In the node, not over the page; the balloon opens toward the
208
- // middle of the graph: rightward from the left member.
209
- place: side === 'left'
210
- ? { position: 'absolute', left: 'auto' }
211
- : { position: 'absolute', right: 'auto' }, stageRef: stageRef,
212
- // Dragged within the graph's box: a press that moves more than
213
- // a few pixels is a drag; one that does not is a click, and the
214
- // stage ignores the click that ends a drag.
215
- onDragStart: event => {
216
- if (event.button !== 0) {
217
- return;
218
- }
219
- const start = { x: event.clientX, y: event.clientY };
220
- const from = offset;
221
- let dragging = false;
222
- const onMove = (next) => {
223
- const dx = next.clientX - start.x;
224
- const dy = next.clientY - start.y;
225
- if (!dragging && Math.hypot(dx, dy) <= DRAG_THRESHOLD) {
226
- return;
227
- }
228
- dragging = true;
229
- moving.move(id, {
230
- dx: from.dx + dx / moving.zoom,
231
- dy: from.dy + dy / moving.zoom,
232
- });
233
- };
234
- const onUp = () => {
235
- window.removeEventListener('pointermove', onMove);
236
- window.removeEventListener('pointerup', onUp);
237
- window.removeEventListener('pointercancel', onUp);
238
- };
239
- window.addEventListener('pointermove', onMove);
240
- window.addEventListener('pointerup', onUp);
241
- window.addEventListener('pointercancel', onUp);
242
- }, onCharacterKeyDown: event => {
243
- const step = event.shiftKey ? NUDGE_FAR : NUDGE;
244
- const by = {
245
- ArrowLeft: { dx: -step, dy: 0 },
246
- ArrowRight: { dx: step, dy: 0 },
247
- ArrowUp: { dx: 0, dy: -step },
248
- ArrowDown: { dx: 0, dy: step },
249
- };
250
- const nudge = by[event.key];
251
- if (nudge) {
252
- event.preventDefault();
253
- moving.move(id, {
254
- dx: offset.dx + nudge.dx,
255
- dy: offset.dy + nudge.dy,
256
- });
257
- }
258
- }, onResetPosition: offset.dx || offset.dy ? () => moving.reset(id) : undefined, open: false, onToggle: member.onToggle ?? (() => undefined),
259
- // A click shows and hides its balloon; the menu's *Ask Sales*
260
- // still takes the reader to the composer.
261
- onCharacterClick: () => setShown(current => current === 'shown' ||
262
- (current === 'auto' &&
263
- (persona.insist || display === 'history'))
264
- ? 'hidden'
265
- : 'shown'), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
266
- setDisplay(next);
267
- member.onBalloonDisplayChange?.(next);
268
- }, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: shown === 'hidden'
269
- ? undefined
270
- : (balloonNow ??
271
- (shown === 'shown'
272
- ? {
273
- text: `${member.name}, ${member.where ?? ''}`.replace(/, $/, ''),
274
- }
275
- : undefined)), expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
276
- // History, chosen in its menu: shown until Current is.
277
- insist: shown === 'shown' ||
278
- (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,
279
- // A member of the graph: what is clicked around it is the graph.
280
- stayPut: true,
281
- // Named as the member, not its character: `Sales (in your
282
- // browser)`; the character describes it.
283
- 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, { id: MEMBER_HANDLES.outRight, type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outLeft, type: "source", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outBottom, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outTop, type: "source", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: MEMBER_HANDLES.connections, type: "source", position: Position.Right, style: { ...HANDLE(size), left: 'auto', right: 0 }, isConnectable: false })) : null] }));
284
- });
285
- /** A member's accessible name: its name, and where it runs. */
286
- export function memberLabel(member) {
287
- return member.where ? `${member.name} (${member.where})` : member.name;
288
- }
289
- /** A connection's accessible name: `Odoo, Accounting's MCP connection`. */
290
- export function connectionLabel(connection, member) {
291
- return member
292
- ? `${connection.label}, ${member}\u2019s MCP connection`
293
- : `${connection.label}, an MCP connection`;
294
- }
295
- /** A connection of a member's: the server's mark, what it reaches, and how. */
296
- const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
297
- const { member, connection: connectionId, size } = data;
298
- const owner = useContext(Members)[member];
299
- const connection = owner?.connections?.find(known => known.id === connectionId);
300
- const tool = useContext(Busy)[id];
301
- // Its details, shown and hidden by a click or Enter on its mark.
302
- const [told, setTold] = useState(false);
303
- const detailsId = useId();
304
- if (!connection) {
305
- return null;
306
- }
307
- return (_jsxs(Box, { className: "nodrag nopan nowheel", width: CONNECTION_WIDTH, display: "flex", flexDirection: "column", alignItems: "center", gap: "2px", cursor: "default", pointerEvents: "all", title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Left, style: { ...HANDLE(size), left: 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) => {
308
- if (event.key === 'Escape' && told) {
309
- event.stopPropagation();
310
- setTold(false);
311
- }
312
- }, width: size, height: size, p: 0, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: 2, border: "1px solid", borderColor: tool ? 'accent.emphasis' : 'border.default', bg: "canvas.default", cursor: "pointer", focusVisible: {
313
- outline: '2px solid',
314
- outlineColor: 'var(--focus-outlineColor, var(--fgColor-accent))',
315
- outlineOffset: 2,
316
- }, 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": "", fontSize: "10px", color: "fg.muted", textAlign: "center", maxWidth: CONNECTION_WIDTH + 40, children: connectionDetails(connection, owner?.name, tool) })] }));
317
- });
318
- /** What a connection's details say: the server, who reaches it, and the call running now. */
319
- export function connectionDetails(connection, member, calling) {
320
- const tools = connection.tools.length;
321
- return [
322
- `${connection.name}, an MCP server${member ? ` ${member} reaches` : ''}.`,
323
- tools ? `${tools} tool${tools === 1 ? '' : 's'}.` : '',
324
- calling ? `Now: ${calling}.` : '',
325
- ]
326
- .filter(Boolean)
327
- .join(' ');
328
- }
329
62
  /** The words on the edge for a flow. */
330
63
  export function flowWords(flow, entry, peer) {
331
64
  return flow === 'asking'
@@ -334,72 +67,42 @@ export function flowWords(flow, entry, peer) {
334
67
  ? `${peer} answers ${entry}`
335
68
  : '';
336
69
  }
337
- const ARROW = 9;
338
70
  /**
339
- * An arrowhead at the end a message goes to — the target's when `forward`,
340
- * the source's otherwise — pointing along the edge.
71
+ * Where a member's connections sit: right of it, the first one's top where
72
+ * the backdrop puts it (level with the character's middle on `studio`),
73
+ * the next ones under it.
341
74
  */
342
- export function arrowPath(source, target, forward) {
343
- const tip = forward ? target : source;
344
- const from = forward ? source : target;
345
- const length = Math.hypot(tip.x - from.x, tip.y - from.y) || 1;
346
- // Along the edge, toward the tip; and across it.
347
- const ax = (tip.x - from.x) / length;
348
- const ay = (tip.y - from.y) / length;
349
- const bx = tip.x - ax * ARROW;
350
- const by = tip.y - ay * ARROW;
351
- const px = -ay * (ARROW / 1.6);
352
- const py = ax * (ARROW / 1.6);
353
- return `M ${tip.x},${tip.y} L ${bx + px},${by + py} L ${bx - px},${by - py} Z`;
75
+ function connectionsY(memberY, top, count, connectionHeight, apart) {
76
+ return Array.from({ length: count }, (_, at) => memberY + top + at * (connectionHeight + apart));
354
77
  }
355
- const LinkEdge = memo(function LinkEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
356
- const flow = data?.flow ?? 'still';
357
- const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
358
- // The arrow at the end the message goes to, along the edge.
359
- const towardPeer = flow === 'asking';
360
- const arrow = arrowPath({ x: sourceX, y: sourceY }, { x: targetX, y: targetY }, towardPeer);
361
- return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
362
- stroke: data?.connected
363
- ? 'var(--borderColor-accent-emphasis)'
364
- : 'var(--borderColor-muted)',
365
- strokeWidth: 2,
366
- strokeDasharray: data?.connected ? undefined : '6 6',
367
- opacity: flow === 'still' ? 0.6 : 0.25,
368
- } }), flow !== 'still' && (_jsxs("g", { "data-a2a-edge-flow": flow, children: [_jsx("path", { d: path, fill: "none", className: `a2a-team-flow a2a-team-flow-${flow}`, stroke: "var(--fgColor-accent)", strokeWidth: 3, strokeLinecap: "round", strokeDasharray: "10 14" }), _jsx("path", { d: arrow, fill: "var(--fgColor-accent)" })] })), _jsx(EdgeLabelRenderer, { children: _jsx(Box, { className: "nodrag nopan", position: "absolute", transform: `translate(-50%, 12px) translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`, pointerEvents: "none", textAlign: "center", children: _jsx(Label, { variant: data?.connected ? 'accent' : 'secondary', children: data?.label }) }) })] }));
369
- });
370
- /** A member's link to a connection: still, or flowing toward it while a call runs. */
371
- export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
372
- const calling = Boolean(data?.calling);
373
- const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
374
- const arrow = `M ${targetX},${targetY} L ${targetX - ARROW / 1.6},${targetY - ARROW} L ${targetX + ARROW / 1.6},${targetY - ARROW} Z`;
375
- return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
376
- stroke: 'var(--borderColor-default)',
377
- strokeWidth: 2,
378
- opacity: calling ? 0.3 : 0.8,
379
- } }), calling && (_jsxs("g", { "data-mcp-edge-call": data?.calling, children: [_jsx("path", { d: path, fill: "none", className: "a2a-team-flow a2a-team-call", stroke: "var(--fgColor-accent)", strokeWidth: 3, strokeLinecap: "round", strokeDasharray: "6 8" }), _jsx("path", { d: arrow, fill: "var(--fgColor-accent)" })] })), calling && (_jsx(EdgeLabelRenderer, { children: _jsx(Box, { className: "nodrag nopan", position: "absolute",
380
- // Beside the edge, toward the middle of the graph.
381
- transform: `translate(calc(-100% - 10px), -50%) translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`, pointerEvents: "none", whiteSpace: "nowrap", children: _jsx(Label, { variant: "accent", children: data?.calling }) }) }))] }));
382
- });
383
- const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
384
- const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
78
+ /** How far down a member's connections reach, from its top; none, 0. */
79
+ function connectionsReach(top, count, connectionHeight, apart) {
80
+ return count ? top + count * connectionHeight + (count - 1) * apart : 0;
81
+ }
82
+ /** What a link to a connection says at rest, as `A2A` says the members' link. */
83
+ export const MCP_LABEL = 'MCP';
385
84
  /**
386
- * Where a member's connections sit: right of it, the first one's mark level
387
- * with the character's middle (a mark half the character's size, so a
388
- * quarter down), the next ones under it.
85
+ * What a link to a connection says while it carries a call, short enough
86
+ * for the room beside the member: the tool going out, `MCP · list invoices`,
87
+ * then `MCP · Odoo answers` as its result comes back.
389
88
  */
390
- function connectionsY(memberY, size, count, connectionHeight) {
391
- const top = memberY + size / 4;
392
- return Array.from({ length: count }, (_, at) => top + at * (connectionHeight + CONNECTIONS_APART));
393
- }
394
- /** How far down a member's connections reach, from its top; none, 0. */
395
- function connectionsReach(size, count, connectionHeight) {
396
- return count
397
- ? size / 4 + count * connectionHeight + (count - 1) * CONNECTIONS_APART
398
- : 0;
89
+ export function callEdgeWords(connection, tool, flow) {
90
+ return flow === 'asking'
91
+ ? `${MCP_LABEL} · ${toolWords(tool, connection)}`
92
+ : flow === 'answering'
93
+ ? `${MCP_LABEL} · ${connection.label} answers`
94
+ : MCP_LABEL;
399
95
  }
400
- /** What a member's call says, on its edge and to a screen reader. */
401
- export function callWords(member, connection, tool) {
402
- return `${member} calls ${connection.label} · ${toolWords(tool, connection)}`;
96
+ /**
97
+ * What a member's call says, on its edge and to a screen reader: the call
98
+ * going out, `Accounting calls Odoo · list invoices`, then its result
99
+ * coming back, `Odoo answers Accounting · list invoices`.
100
+ */
101
+ export function callWords(member, connection, tool, flow = 'asking') {
102
+ const words = toolWords(tool, connection);
103
+ return flow === 'answering'
104
+ ? `${connection.label} answers ${member} · ${words}`
105
+ : `${member} calls ${connection.label} · ${words}`;
403
106
  }
404
107
  /** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
405
108
  export function teamViewport(width, drawn = WIDTH) {
@@ -445,8 +148,58 @@ export function flowOfAll(flows) {
445
148
  : 'still';
446
149
  }
447
150
  const NO_PEERS = [];
151
+ /**
152
+ * The rules on the graph's box: what every backdrop needs — React Flow
153
+ * overflowing so a balloon shows whole, the cursors, a busy connection's
154
+ * pulse, and no movement under reduced motion — then the backdrop's own
155
+ * and its variables, light and dark (`sceneStageSx`).
156
+ */
157
+ function stageRules(theme, clickable) {
158
+ return {
159
+ // An edge is clicked for its latest exchange: it takes the pointer.
160
+ ...(clickable
161
+ ? { '& .react-flow__edge': { cursor: 'pointer', pointerEvents: 'all' } }
162
+ : {}),
163
+ '@keyframes a2aTeamPulse': {
164
+ '0%, 100%': { boxShadow: '0 0 0 0 transparent' },
165
+ '50%': {
166
+ boxShadow: '0 0 0 4px var(--bgColor-accent-muted)',
167
+ },
168
+ },
169
+ '& .a2a-team-connection-busy': {
170
+ animation: 'a2aTeamPulse 1s ease-in-out infinite',
171
+ },
172
+ // The arrow and the words say which way; the movement is extra.
173
+ '@media (prefers-reduced-motion: reduce)': {
174
+ '& .a2a-team-flow, & .a2a-team-connection-busy': {
175
+ animation: 'none',
176
+ },
177
+ },
178
+ '& .react-flow, & .react-flow__renderer, & .react-flow__viewport': {
179
+ overflow: 'visible',
180
+ },
181
+ '& .react-flow__node': { cursor: 'default' },
182
+ // A member is moved by dragging its figure.
183
+ '& [data-team-member] [data-assistant-figure]': { cursor: 'grab' },
184
+ '& [data-team-member] [data-assistant-figure]:active': {
185
+ cursor: 'grabbing',
186
+ },
187
+ ...sceneStageSx(theme),
188
+ };
189
+ }
448
190
  /** Draw a team over A2A, each link moving with what travels on it. */
449
- export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows, connected, label = 'A2A', labels, links, positions, size = 96, calls = [], balloonRoom = BALLOON_ROOM, }) {
191
+ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows, connected, label = 'A2A', labels, links, positions, size = 96, calls = [], balloonRoom, backdrop, selected, onSelectEdge, }) {
192
+ // How the stage is drawn: its nodes and edges, its colours, its layout.
193
+ const look = sceneBackdrop(backdrop);
194
+ const { layout: sized, theme } = look;
195
+ const NODE_WIDTH = sized.nodeWidth;
196
+ const GAP = sized.gap;
197
+ const ROW_GAP = sized.rowGap;
198
+ const CONNECTION_WIDTH = sized.connectionWidth;
199
+ const CONNECTIONS_APART = sized.connectionsApart;
200
+ const CONNECTION_X = NODE_WIDTH + sized.connectionGap;
201
+ const CONNECTIONS_BESIDE = sized.connectionGap + CONNECTION_WIDTH;
202
+ const roomFloor = balloonRoom ?? sized.balloonRoom;
450
203
  const peers = useMemo(() => [...(peer ? [peer] : []), ...more], [peer, more]);
451
204
  const members = useMemo(() => [entry, ...peers], [entry, peers]);
452
205
  const memberOf = (id) => members.find(member => member.id === id);
@@ -476,16 +229,17 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
476
229
  const placing = placeMembers(entry.id, peerIds, positions);
477
230
  const hasConnections = members.some(member => member.connections?.length);
478
231
  // The last column's connections sit beside it, inside the graph.
479
- const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH +
232
+ const drawn = Math.max(NODE_WIDTH * 2 + GAP, placing.columns * NODE_WIDTH +
480
233
  (placing.columns - 1) * GAP +
481
234
  (hasConnections ? CONNECTIONS_BESIDE : 0));
482
235
  const viewport = useMemo(() => teamViewport(width || drawn, drawn), [width, drawn]);
483
- // The members' height: the character, its name and where it runs.
484
- const memberHeight = size + 56;
485
- const connectionSize = Math.round(size / 2);
486
- // Its mark, its label and how it is reached (about 45px under the mark),
487
- // and room under it before the graph's edge.
488
- const connectionHeight = connectionSize + 58;
236
+ // The members' height: the figure, its name and where it runs.
237
+ const memberHeight = sized.memberHeight(size);
238
+ const connectionSize = Math.round(size * sized.connectionScale);
239
+ // Its mark, its label and how it is reached, and room under it before
240
+ // the graph's edge.
241
+ const connectionHeight = sized.connectionHeight(connectionSize);
242
+ const connectionsTop = sized.connectionsTop(size);
489
243
  // The connections' ids, member by member: the nodes change only when they do.
490
244
  const placed = members.map(member => ({
491
245
  member: member.id,
@@ -497,13 +251,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
497
251
  const measured = useBalloonRoom(box, {
498
252
  anchor: '[data-team-member]',
499
253
  zoom: viewport.zoom,
500
- floor: balloonRoom,
254
+ floor: roomFloor,
501
255
  });
502
256
  const room = members.some(member => member.persona.notebook)
503
257
  ? Math.max(NOTEBOOK_BALLOON_ROOM, measured)
504
258
  : measured;
505
259
  // Beside a member, its connections may reach below it: the row takes them.
506
- const reach = Math.max(0, ...placed.map(({ connections }) => connectionsReach(size, connections.length, connectionHeight)));
260
+ const reach = Math.max(0, ...placed.map(({ connections }) => connectionsReach(connectionsTop, connections.length, connectionHeight, CONNECTIONS_APART)));
507
261
  const rowHeight = Math.max(memberHeight, reach);
508
262
  const total = room + placing.rows * rowHeight + (placing.rows - 1) * ROW_GAP;
509
263
  const height = Math.ceil(total * viewport.zoom);
@@ -520,7 +274,7 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
520
274
  const layout = layoutOf(id);
521
275
  const count = placed.find(({ member }) => member === id)?.connections.length ?? 0;
522
276
  const beside = count ? CONNECTIONS_BESIDE : 0;
523
- const tall = Math.max(memberHeight, connectionsReach(size, count, connectionHeight));
277
+ const tall = Math.max(memberHeight, connectionsReach(connectionsTop, count, connectionHeight, CONNECTIONS_APART));
524
278
  return {
525
279
  minDx: -layout.x,
526
280
  maxDx: drawn - NODE_WIDTH - beside - layout.x,
@@ -578,7 +332,7 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
578
332
  },
579
333
  // A member's connections move with it.
580
334
  ...connections.map((connection, at) => {
581
- const ys = connectionsY(y, size, connections.length, connectionHeight);
335
+ const ys = connectionsY(y, connectionsTop, connections.length, connectionHeight, CONNECTIONS_APART);
582
336
  return {
583
337
  id: connectionNodeId(id, connection),
584
338
  type: 'connection',
@@ -594,7 +348,18 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
594
348
  data: { member: id, connection, size: connectionSize },
595
349
  };
596
350
  }),
597
- ]), [layoutKey, size, memberHeight, connectionSize, connectionHeight]);
351
+ ]), [
352
+ layoutKey,
353
+ size,
354
+ memberHeight,
355
+ connectionSize,
356
+ connectionHeight,
357
+ connectionsTop,
358
+ CONNECTIONS_APART,
359
+ CONNECTION_X,
360
+ CONNECTION_WIDTH,
361
+ NODE_WIDTH,
362
+ ]);
598
363
  // The notebook open under the graph runs on a Pyodide kernel in the page:
599
364
  // the entry's sandbox, while it is open.
600
365
  const notebookKernel = useSignalValue(teamNotebookKernel);
@@ -621,21 +386,34 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
621
386
  ? flowWords(flowOf(link.to), entry.name, memberOf(link.to)?.name ?? link.to)
622
387
  : '';
623
388
  const words = edgesWanted.map(wordsOf).filter(Boolean);
624
- // The tool each connection is answering now, in words, by its node's id.
389
+ // The tool each connection is answering now, in words, by its node's id,
390
+ // and which way its edge carries it: a call still running before one
391
+ // whose result is coming back.
625
392
  const busy = useMemo(() => {
626
393
  const byNode = {};
394
+ const edgeByNode = {};
627
395
  const said = [];
628
- for (const call of calls) {
396
+ const running = [
397
+ ...calls.filter(call => callFlow(call) === 'asking'),
398
+ ...calls.filter(call => callFlow(call) === 'answering'),
399
+ ];
400
+ for (const call of running) {
629
401
  const member = members.find(one => one.id === call.member);
630
402
  const connection = member?.connections?.find(known => known.id === call.connection);
631
403
  if (!member || !connection) {
632
404
  continue;
633
405
  }
634
406
  const node = connectionNodeId(member.id, connection.id);
407
+ const flow = callFlow(call);
408
+ const words = callWords(member.name, connection, call.tool, flow);
635
409
  byNode[node] ??= toolWords(call.tool, connection);
636
- said.push(callWords(member.name, connection, call.tool));
410
+ edgeByNode[node] ??= {
411
+ flow,
412
+ label: callEdgeWords(connection, call.tool, flow),
413
+ };
414
+ said.push(words);
637
415
  }
638
- return { byNode, said: [...new Set(said)] };
416
+ return { byNode, edgeByNode, said: [...new Set(said)] };
639
417
  }, [calls, members]);
640
418
  const linkKey = JSON.stringify(edgesWanted.map(link => ({
641
419
  ...link,
@@ -644,10 +422,11 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
644
422
  label: wordsOf(link)
645
423
  ? `${labelOf(link.to)} · ${wordsOf(link)}`
646
424
  : labelOf(link.to),
425
+ selected: edgeSelected(selected, link.from, link.to),
647
426
  ...handlesBetween(layoutOf(link.from), layoutOf(link.to)),
648
427
  })));
649
428
  const edges = useMemo(() => [
650
- ...JSON.parse(linkKey).map(({ from, to, flow: linkFlow, connected: reached, label: said, sourceHandle, targetHandle, }) => ({
429
+ ...JSON.parse(linkKey).map(({ from, to, flow: linkFlow, connected: reached, label: said, selected: chosen, sourceHandle, targetHandle, }) => ({
651
430
  id: `${from}->${to}`,
652
431
  source: from,
653
432
  sourceHandle,
@@ -656,11 +435,17 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
656
435
  type: 'a2a',
657
436
  selectable: false,
658
437
  focusable: false,
659
- data: { flow: linkFlow, connected: reached, label: said },
438
+ data: {
439
+ flow: linkFlow,
440
+ connected: reached,
441
+ label: said,
442
+ selected: chosen,
443
+ },
660
444
  })),
661
445
  ...JSON.parse(placedKey).flatMap(({ member, connections }) => connections.map(connection => {
662
446
  const node = connectionNodeId(member, connection);
663
447
  const calling = busy.byNode[node] ?? '';
448
+ const carried = busy.edgeByNode[node];
664
449
  return {
665
450
  id: `${member}->${node}`,
666
451
  source: member,
@@ -669,55 +454,29 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
669
454
  type: 'mcp',
670
455
  selectable: false,
671
456
  focusable: false,
672
- data: { calling },
457
+ data: {
458
+ flow: carried?.flow ?? 'still',
459
+ calling,
460
+ label: carried?.label ?? MCP_LABEL,
461
+ selected: edgeSelected(selected, member, node),
462
+ },
673
463
  };
674
464
  })),
675
- ], [linkKey, placedKey, busy]);
465
+ ], [linkKey, placedKey, busy, selected]);
676
466
  const heard = [...words, ...busy.said].filter(Boolean).join('. ');
677
467
  const flowNow = flowOfAll(peers.map(one => flowOf(one.id)));
678
- return (_jsxs(Box, { ref: box, "data-a2a-team-graph": "", "data-a2a-flow": flowNow, "data-a2a-calls": calls.length, "data-a2a-members": members.length, width: "100%", height: height, position: "relative", sx: {
679
- '@keyframes a2aTeamFlow': {
680
- from: { strokeDashoffset: 24 },
681
- to: { strokeDashoffset: 0 },
682
- },
683
- '& .a2a-team-flow-asking': {
684
- animation: 'a2aTeamFlow 0.6s linear infinite',
685
- },
686
- '& .a2a-team-flow-answering': {
687
- animation: 'a2aTeamFlow 0.6s linear infinite reverse',
688
- },
689
- '@keyframes a2aTeamCall': {
690
- from: { strokeDashoffset: 14 },
691
- to: { strokeDashoffset: 0 },
692
- },
693
- '@keyframes a2aTeamPulse': {
694
- '0%, 100%': { boxShadow: '0 0 0 0 transparent' },
695
- '50%': {
696
- boxShadow: '0 0 0 4px var(--bgColor-accent-muted)',
697
- },
698
- },
699
- '& .a2a-team-call': {
700
- animation: 'a2aTeamCall 0.5s linear infinite',
701
- },
702
- '& .a2a-team-connection-busy': {
703
- animation: 'a2aTeamPulse 1s ease-in-out infinite',
704
- },
705
- // The arrow and the words say which way; the movement is extra.
706
- '@media (prefers-reduced-motion: reduce)': {
707
- '& .a2a-team-flow, & .a2a-team-connection-busy': {
708
- animation: 'none',
709
- },
710
- },
711
- '& .react-flow, & .react-flow__renderer, & .react-flow__viewport': {
712
- overflow: 'visible',
713
- },
714
- '& .react-flow__node': { cursor: 'default' },
715
- // A member is moved by dragging its character.
716
- '& [data-team-member] [data-assistant-figure]': { cursor: 'grab' },
717
- '& [data-team-member] [data-assistant-figure]:active': {
718
- cursor: 'grabbing',
719
- },
720
- }, children: [_jsx(Box, { as: "span", "aria-live": "polite", position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", children: heard }), _jsx(Moving.Provider, { value: movingValue, children: _jsx(Members.Provider, { value: membersById, 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' },
468
+ const clickable = Boolean(onSelectEdge);
469
+ const stageSx = useMemo(() => stageRules(theme, clickable), [theme, clickable]);
470
+ return (_jsxs(Box, { ref: box, "data-a2a-team-graph": "", "data-scene-backdrop": look.id, "data-a2a-flow": flowNow, "data-a2a-calls": calls.length, "data-a2a-members": members.length, "data-a2a-selected": selected ? selected.id : undefined, width: "100%", height: height, position: "relative", sx: stageSx, children: [_jsx(Box, { as: "span", "aria-live": "polite", position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", children: heard }), _jsx(Moving.Provider, { value: movingValue, children: _jsx(Members.Provider, { value: membersById, children: _jsx(Busy.Provider, { value: busy.byNode, children: _jsx(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: look.nodeTypes, edgeTypes: look.edgeTypes,
471
+ // A click on an edge: its latest exchange, for the page (S3-02).
472
+ onEdgeClick: onSelectEdge
473
+ ? (_event, edge) => onSelectEdge({
474
+ id: edge.id,
475
+ from: edge.source,
476
+ to: edge.target,
477
+ over: edge.type === 'mcp' ? 'mcp' : 'a2a',
478
+ })
479
+ : undefined, 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' },
721
480
  // The library is MIT: its credit is not required on the page.
722
481
  proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${peers.map(one => one.name).join(', ')}, over A2A` }) }) }) })] }));
723
482
  }