@datalayer/agent-runtimes 1.5.90 → 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 (74) hide show
  1. package/README.md +29 -297
  2. package/dist-embed/datalayer-app-main.js +2 -2
  3. package/lib/components/teams/A2ATeamGraph.d.ts +40 -155
  4. package/lib/components/teams/A2ATeamGraph.js +122 -400
  5. package/lib/components/teams/SceneTranscript.d.ts +5 -1
  6. package/lib/components/teams/SceneTranscript.js +2 -2
  7. package/lib/components/teams/SceneView.d.ts +17 -5
  8. package/lib/components/teams/SceneView.js +7 -3
  9. package/lib/components/teams/backdrops/catalogue.d.ts +35 -0
  10. package/lib/components/teams/backdrops/catalogue.js +34 -0
  11. package/lib/components/teams/backdrops/edges.d.ts +20 -0
  12. package/lib/components/teams/backdrops/edges.js +29 -0
  13. package/lib/components/teams/backdrops/faces.d.ts +24 -0
  14. package/lib/components/teams/backdrops/faces.js +39 -0
  15. package/lib/components/teams/backdrops/halo.d.ts +14 -0
  16. package/lib/components/teams/backdrops/halo.js +5 -0
  17. package/lib/components/teams/backdrops/hero.d.ts +14 -0
  18. package/lib/components/teams/backdrops/hero.js +178 -0
  19. package/lib/components/teams/backdrops/index.d.ts +22 -0
  20. package/lib/components/teams/backdrops/index.js +38 -0
  21. package/lib/components/teams/backdrops/nodes.d.ts +123 -0
  22. package/lib/components/teams/backdrops/nodes.js +265 -0
  23. package/lib/components/teams/backdrops/plain.d.ts +14 -0
  24. package/lib/components/teams/backdrops/plain.js +116 -0
  25. package/lib/components/teams/backdrops/studio.d.ts +17 -0
  26. package/lib/components/teams/backdrops/studio.js +122 -0
  27. package/lib/components/teams/backdrops/theme.d.ts +14 -0
  28. package/lib/components/teams/backdrops/theme.js +19 -0
  29. package/lib/components/teams/backdrops/types.d.ts +214 -0
  30. package/lib/components/teams/backdrops/types.js +5 -0
  31. package/lib/components/teams/index.d.ts +2 -0
  32. package/lib/components/teams/index.js +2 -0
  33. package/lib/components/teams/sceneTranscriptRules.d.ts +32 -1
  34. package/lib/components/teams/sceneTranscriptRules.js +104 -2
  35. package/lib/components/teams/transcript/RecordingLabel.d.ts +17 -0
  36. package/lib/components/teams/transcript/RecordingLabel.js +7 -0
  37. package/lib/components/teams/transcript/SequenceDiagram.d.ts +39 -0
  38. package/lib/components/teams/transcript/SequenceDiagram.js +190 -0
  39. package/lib/components/teams/transcript/TranscriptDetail.d.ts +27 -0
  40. package/lib/components/teams/transcript/TranscriptDetail.js +48 -0
  41. package/lib/components/teams/transcript/TranscriptTable.d.ts +23 -0
  42. package/lib/components/teams/transcript/TranscriptTable.js +112 -0
  43. package/lib/components/teams/transcript/TranscriptViews.d.ts +66 -0
  44. package/lib/components/teams/transcript/TranscriptViews.js +135 -0
  45. package/lib/components/teams/transcript/TranscriptViewsTypes.d.ts +13 -0
  46. package/lib/components/teams/transcript/TranscriptViewsTypes.js +5 -0
  47. package/lib/components/teams/transcript/index.d.ts +23 -0
  48. package/lib/components/teams/transcript/index.js +27 -0
  49. package/lib/components/teams/transcript/recordingPlayback.d.ts +52 -0
  50. package/lib/components/teams/transcript/recordingPlayback.js +146 -0
  51. package/lib/components/teams/transcript/selection.d.ts +59 -0
  52. package/lib/components/teams/transcript/selection.js +79 -0
  53. package/lib/components/teams/transcript/sequenceDiagramRules.d.ts +35 -0
  54. package/lib/components/teams/transcript/sequenceDiagramRules.js +158 -0
  55. package/lib/components/teams/transcript/transcriptDetailRules.d.ts +80 -0
  56. package/lib/components/teams/transcript/transcriptDetailRules.js +208 -0
  57. package/lib/components/teams/transcript/transcriptExport.d.ts +31 -0
  58. package/lib/components/teams/transcript/transcriptExport.js +82 -0
  59. package/lib/components/teams/transcript/transcriptRows.d.ts +76 -0
  60. package/lib/components/teams/transcript/transcriptRows.js +187 -0
  61. package/lib/components/teams/transcript/useSceneRecording.d.ts +45 -0
  62. package/lib/components/teams/transcript/useSceneRecording.js +96 -0
  63. package/lib/examples/ScenesExample.js +16 -3
  64. package/lib/specs/actions.js +2 -0
  65. package/lib/specs/apps.d.ts +1 -0
  66. package/lib/specs/apps.js +219 -0
  67. package/lib/specs/sceneRecordings.d.ts +14 -0
  68. package/lib/specs/sceneRecordings.js +12 -0
  69. package/lib/specs/sceneSchema.js +11 -0
  70. package/lib/specs/scenes.d.ts +1 -0
  71. package/lib/specs/scenes.js +159 -0
  72. package/lib/types/scenes.d.ts +54 -0
  73. package/package.json +2 -1
  74. package/scripts/codegen/generate_scenes.py +80 -0
@@ -1,68 +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
8
  import { callFlow, toolWords, } from './a2aTeamFlow.js';
12
- import { NotebookPreview } from './NotebookPreview.js';
13
- import { notebookBalloonVisual } from './TeamNotebook.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
- /**
30
- * Between a member and its connections, to its right: room for the MCP
31
- * edge's words, as the A2A edge has room for its own.
32
- */
33
- const CONNECTION_GAP = 140;
34
- const CONNECTIONS_APART = 16;
35
- /** Where a member's connections start: right of it. */
36
- const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
37
- /** Below the character's middle, so the A2A edge leaving it passes above. */
38
- /** What a member's connections take beside it. */
39
- const CONNECTIONS_BESIDE = CONNECTION_GAP + CONNECTION_WIDTH;
40
- /** Where the edge leaves and reaches a member: the middle of its character. */
41
- const HANDLE = (size) => ({
42
- opacity: 0,
43
- pointerEvents: 'none',
44
- top: size / 2,
45
- width: 1,
46
- height: 1,
47
- minWidth: 0,
48
- minHeight: 0,
49
- border: 0,
50
- });
51
- /**
52
- * The handles an edge leaves and reaches a member by, named: the middle of
53
- * its character on each side, chosen by where the other member stands.
54
- */
55
- export const MEMBER_HANDLES = {
56
- inLeft: 'in-left',
57
- outRight: 'out-right',
58
- inTop: 'in-top',
59
- outBottom: 'out-bottom',
60
- inRight: 'in-right',
61
- outLeft: 'out-left',
62
- inBottom: 'in-bottom',
63
- outTop: 'out-top',
64
- connections: 'connections',
65
- };
66
27
  /**
67
28
  * The handles for an edge from one place to another: rightward across,
68
29
  * leftward across, or down or up when the other member is mostly below or
@@ -92,243 +53,12 @@ export function handlesBetween(from, to) {
92
53
  targetHandle: MEMBER_HANDLES.inBottom,
93
54
  };
94
55
  }
95
- /**
96
- * The members as they are now, by id. Not in the nodes' data: a node whose
97
- * object changes is taken by React Flow for a new one, measured again, and
98
- * the edge is dropped until it is — the line vanished as the characters
99
- * moved. The nodes stay the same objects; what they show is read from here.
100
- */
101
- const Members = createContext({});
102
- /** The connections being called now, by node id: read by their nodes, as the members are. */
103
- const Busy = createContext({});
104
- const AT_PLACE = { dx: 0, dy: 0 };
105
- /** An offset kept inside its bounds. */
106
- export function clampOffset(offset, bounds) {
107
- return {
108
- dx: Math.min(bounds.maxDx, Math.max(bounds.minDx, offset.dx)),
109
- dy: Math.min(bounds.maxDy, Math.max(bounds.minDy, offset.dy)),
110
- };
111
- }
112
56
  /**
113
57
  * Where the members were moved, by page and member: kept for the session,
114
58
  * in memory, so a graph drawn again on the page keeps them where they were.
115
59
  */
116
60
  const MOVED = new Map();
117
61
  const movedKey = (id) => `${typeof location === 'undefined' ? '' : location.pathname}#${id}`;
118
- /** How far a press moves before it is a drag, not a click, in pixels. */
119
- const DRAG_THRESHOLD = 4;
120
- /** How far an arrow key moves a member; with Shift, more. */
121
- const NUDGE = 10;
122
- const NUDGE_FAR = 40;
123
- /** The members' moves, read by their nodes. */
124
- const Moving = createContext({
125
- zoom: 1,
126
- offsetOf: () => AT_PLACE,
127
- move: () => undefined,
128
- reset: () => undefined,
129
- });
130
- /** A connection's node id: beside its member. */
131
- const connectionNodeId = (member, connection) => `${member}/${connection}`;
132
- const MemberNode = memo(function MemberNode({ data, }) {
133
- const { id, side, size } = data;
134
- const member = useContext(Members)[id];
135
- const moving = useContext(Moving);
136
- const offset = moving.offsetOf(id);
137
- const stageRef = useRef(null);
138
- // Its balloon's display, as its menu changes it.
139
- const [display, setDisplay] = useState();
140
- // Its balloon as a click on it left it: shown, hidden, or as the team
141
- // says (`auto`: what it is doing, and on hover).
142
- const [shown, setShown] = useState('auto');
143
- // What it says or does now: something new reopens a hidden balloon.
144
- const persona = member?.persona;
145
- const news = [
146
- persona?.saying,
147
- persona?.tool ? toolLineText(persona.tool) : '',
148
- persona?.notebook?.name,
149
- ].join('\u0000');
150
- const seen = useRef(news);
151
- useEffect(() => {
152
- if (news !== seen.current) {
153
- seen.current = news;
154
- setShown(current => (current === 'hidden' ? 'auto' : current));
155
- }
156
- }, [news]);
157
- if (!member || !persona) {
158
- return null;
159
- }
160
- const shownDisplay = display ?? member.balloonDisplay ?? 'current';
161
- // History: what it said and did, listed.
162
- const listed = shownDisplay === 'history' && member.history?.length
163
- ? { history: member.history }
164
- : {};
165
- // A notebook it was given: read-only in its balloon, and its large visual
166
- // the one that runs, editable — under the graph, or in a dialog.
167
- const notebookTitle = member.notebookTitle ?? (persona.notebook?.name || 'The notebook');
168
- const given = persona.notebook
169
- ? {
170
- 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 })),
171
- visual: notebookBalloonVisual(persona.notebook, notebookTitle),
172
- }
173
- : {};
174
- const balloonNow = persona.tool
175
- ? {
176
- text: toolLineText(persona.tool),
177
- tool: persona.tool,
178
- busy: persona.state !== 'idle',
179
- ...given,
180
- ...listed,
181
- }
182
- : persona.saying
183
- ? {
184
- text: persona.saying,
185
- ...(persona.full ? { fullText: persona.full } : {}),
186
- more: persona.saying.endsWith('…'),
187
- speaking: persona.state === 'speaking',
188
- busy: persona.state === 'thinking' ||
189
- persona.state === 'working' ||
190
- persona.state === 'speaking',
191
- ...given,
192
- ...listed,
193
- }
194
- : 'history' in listed
195
- ? { text: '', ...listed }
196
- : undefined;
197
- return (_jsxs(Box
198
- // React Flow lets the pointer through a node that is neither dragged
199
- // nor selected (`pointer-events: none` on its wrapper): the character,
200
- // its balloon and its menu take it back, and are left to themselves —
201
- // no drag, no pan, no wheel.
202
- , {
203
- // React Flow lets the pointer through a node that is neither dragged
204
- // nor selected (`pointer-events: none` on its wrapper): the character,
205
- // its balloon and its menu take it back, and are left to themselves —
206
- // no drag, no pan, no wheel.
207
- 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
208
- ? `${Math.round(offset.dx)},${Math.round(offset.dy)}`
209
- : 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,
210
- // In the node, not over the page; the balloon opens toward the
211
- // middle of the graph: rightward from the left member.
212
- place: side === 'left'
213
- ? { position: 'absolute', left: 'auto' }
214
- : { position: 'absolute', right: 'auto' }, stageRef: stageRef,
215
- // Dragged within the graph's box: a press that moves more than
216
- // a few pixels is a drag; one that does not is a click, and the
217
- // stage ignores the click that ends a drag.
218
- onDragStart: event => {
219
- if (event.button !== 0) {
220
- return;
221
- }
222
- const start = { x: event.clientX, y: event.clientY };
223
- const from = offset;
224
- let dragging = false;
225
- const onMove = (next) => {
226
- const dx = next.clientX - start.x;
227
- const dy = next.clientY - start.y;
228
- if (!dragging && Math.hypot(dx, dy) <= DRAG_THRESHOLD) {
229
- return;
230
- }
231
- dragging = true;
232
- moving.move(id, {
233
- dx: from.dx + dx / moving.zoom,
234
- dy: from.dy + dy / moving.zoom,
235
- });
236
- };
237
- const onUp = () => {
238
- window.removeEventListener('pointermove', onMove);
239
- window.removeEventListener('pointerup', onUp);
240
- window.removeEventListener('pointercancel', onUp);
241
- };
242
- window.addEventListener('pointermove', onMove);
243
- window.addEventListener('pointerup', onUp);
244
- window.addEventListener('pointercancel', onUp);
245
- }, onCharacterKeyDown: event => {
246
- const step = event.shiftKey ? NUDGE_FAR : NUDGE;
247
- const by = {
248
- ArrowLeft: { dx: -step, dy: 0 },
249
- ArrowRight: { dx: step, dy: 0 },
250
- ArrowUp: { dx: 0, dy: -step },
251
- ArrowDown: { dx: 0, dy: step },
252
- };
253
- const nudge = by[event.key];
254
- if (nudge) {
255
- event.preventDefault();
256
- moving.move(id, {
257
- dx: offset.dx + nudge.dx,
258
- dy: offset.dy + nudge.dy,
259
- });
260
- }
261
- }, onResetPosition: offset.dx || offset.dy ? () => moving.reset(id) : undefined, open: false, onToggle: member.onToggle ?? (() => undefined),
262
- // A click shows and hides its balloon; the menu's *Ask Sales*
263
- // still takes the reader to the composer.
264
- onCharacterClick: () => setShown(current => current === 'shown' ||
265
- (current === 'auto' &&
266
- (persona.insist || display === 'history'))
267
- ? 'hidden'
268
- : 'shown'), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
269
- setDisplay(next);
270
- member.onBalloonDisplayChange?.(next);
271
- }, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: shown === 'hidden'
272
- ? undefined
273
- : (balloonNow ??
274
- (shown === 'shown'
275
- ? {
276
- text: `${member.name}, ${member.where ?? ''}`.replace(/, $/, ''),
277
- }
278
- : undefined)), expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
279
- // History, chosen in its menu: shown until Current is.
280
- insist: shown === 'shown' ||
281
- (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,
282
- // A member of the graph: what is clicked around it is the graph.
283
- stayPut: true,
284
- // Named as the member, not its character: `Sales (in your
285
- // browser)`; the character describes it.
286
- 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] }));
287
- });
288
- /** A member's accessible name: its name, and where it runs. */
289
- export function memberLabel(member) {
290
- return member.where ? `${member.name} (${member.where})` : member.name;
291
- }
292
- /** A connection's accessible name: `Odoo, Accounting's MCP connection`. */
293
- export function connectionLabel(connection, member) {
294
- return member
295
- ? `${connection.label}, ${member}\u2019s MCP connection`
296
- : `${connection.label}, an MCP connection`;
297
- }
298
- /** A connection of a member's: the server's mark, what it reaches, and how. */
299
- const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
300
- const { member, connection: connectionId, size } = data;
301
- const owner = useContext(Members)[member];
302
- const connection = owner?.connections?.find(known => known.id === connectionId);
303
- const tool = useContext(Busy)[id];
304
- // Its details, shown and hidden by a click or Enter on its mark.
305
- const [told, setTold] = useState(false);
306
- const detailsId = useId();
307
- if (!connection) {
308
- return null;
309
- }
310
- 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: (CONNECTION_WIDTH - size) / 2 }, 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) => {
311
- if (event.key === 'Escape' && told) {
312
- event.stopPropagation();
313
- setTold(false);
314
- }
315
- }, 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: {
316
- outline: '2px solid',
317
- outlineColor: 'var(--focus-outlineColor, var(--fgColor-accent))',
318
- outlineOffset: 2,
319
- }, 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) })] }));
320
- });
321
- /** What a connection's details say: the server, who reaches it, and the call running now. */
322
- export function connectionDetails(connection, member, calling) {
323
- const tools = connection.tools.length;
324
- return [
325
- `${connection.name}, an MCP server${member ? ` ${member} reaches` : ''}.`,
326
- tools ? `${tools} tool${tools === 1 ? '' : 's'}.` : '',
327
- calling ? `Now: ${calling}.` : '',
328
- ]
329
- .filter(Boolean)
330
- .join(' ');
331
- }
332
62
  /** The words on the edge for a flow. */
333
63
  export function flowWords(flow, entry, peer) {
334
64
  return flow === 'asking'
@@ -337,70 +67,17 @@ export function flowWords(flow, entry, peer) {
337
67
  ? `${peer} answers ${entry}`
338
68
  : '';
339
69
  }
340
- const ARROW = 9;
341
70
  /**
342
- * An arrowhead at the end a message goes to — the target's when `forward`,
343
- * 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.
344
74
  */
345
- export function arrowPath(source, target, forward) {
346
- const tip = forward ? target : source;
347
- const from = forward ? source : target;
348
- const length = Math.hypot(tip.x - from.x, tip.y - from.y) || 1;
349
- // Along the edge, toward the tip; and across it.
350
- const ax = (tip.x - from.x) / length;
351
- const ay = (tip.y - from.y) / length;
352
- const bx = tip.x - ax * ARROW;
353
- const by = tip.y - ay * ARROW;
354
- const px = -ay * (ARROW / 1.6);
355
- const py = ax * (ARROW / 1.6);
356
- return `M ${tip.x},${tip.y} L ${bx + px},${by + py} L ${bx - px},${by - py} Z`;
357
- }
358
- const LinkEdge = memo(function LinkEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
359
- const flow = data?.flow ?? 'still';
360
- const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
361
- // The arrow at the end the message goes to, along the edge.
362
- const towardPeer = flow === 'asking';
363
- const arrow = arrowPath({ x: sourceX, y: sourceY }, { x: targetX, y: targetY }, towardPeer);
364
- return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
365
- stroke: data?.connected
366
- ? 'var(--borderColor-accent-emphasis)'
367
- : 'var(--borderColor-muted)',
368
- strokeWidth: 2,
369
- strokeDasharray: data?.connected ? undefined : '6 6',
370
- opacity: flow === 'still' ? 0.6 : 0.25,
371
- } }), 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 }) }) })] }));
372
- });
373
- /**
374
- * A member's link to a connection, drawn as the A2A link is: still, flowing
375
- * toward the connection while a call runs, and back with its result.
376
- */
377
- export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
378
- const flow = data?.flow ?? 'still';
379
- const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
380
- // The arrow at the end the call or its result goes to, along the edge.
381
- const arrow = arrowPath({ x: sourceX, y: sourceY }, { x: targetX, y: targetY }, flow === 'asking');
382
- return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
383
- stroke: 'var(--borderColor-accent-emphasis)',
384
- strokeWidth: 2,
385
- opacity: flow === 'still' ? 0.6 : 0.25,
386
- } }), flow !== 'still' && (_jsxs("g", { "data-mcp-edge-call": data?.calling, "data-mcp-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", whiteSpace: "nowrap", "data-mcp-edge-label": "", children: _jsx(Label, { variant: "accent", children: data?.label }) }) })] }));
387
- });
388
- const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
389
- const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
390
- /**
391
- * Where a member's connections sit: right of it, the first one's mark level
392
- * with the character's middle (a mark half the character's size, so a
393
- * quarter down), the next ones under it.
394
- */
395
- function connectionsY(memberY, size, count, connectionHeight) {
396
- const top = memberY + size / 4;
397
- return Array.from({ length: count }, (_, at) => top + at * (connectionHeight + CONNECTIONS_APART));
75
+ function connectionsY(memberY, top, count, connectionHeight, apart) {
76
+ return Array.from({ length: count }, (_, at) => memberY + top + at * (connectionHeight + apart));
398
77
  }
399
78
  /** How far down a member's connections reach, from its top; none, 0. */
400
- function connectionsReach(size, count, connectionHeight) {
401
- return count
402
- ? size / 4 + count * connectionHeight + (count - 1) * CONNECTIONS_APART
403
- : 0;
79
+ function connectionsReach(top, count, connectionHeight, apart) {
80
+ return count ? top + count * connectionHeight + (count - 1) * apart : 0;
404
81
  }
405
82
  /** What a link to a connection says at rest, as `A2A` says the members' link. */
406
83
  export const MCP_LABEL = 'MCP';
@@ -471,8 +148,58 @@ export function flowOfAll(flows) {
471
148
  : 'still';
472
149
  }
473
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
+ }
474
190
  /** Draw a team over A2A, each link moving with what travels on it. */
475
- 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;
476
203
  const peers = useMemo(() => [...(peer ? [peer] : []), ...more], [peer, more]);
477
204
  const members = useMemo(() => [entry, ...peers], [entry, peers]);
478
205
  const memberOf = (id) => members.find(member => member.id === id);
@@ -502,16 +229,17 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
502
229
  const placing = placeMembers(entry.id, peerIds, positions);
503
230
  const hasConnections = members.some(member => member.connections?.length);
504
231
  // The last column's connections sit beside it, inside the graph.
505
- const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH +
232
+ const drawn = Math.max(NODE_WIDTH * 2 + GAP, placing.columns * NODE_WIDTH +
506
233
  (placing.columns - 1) * GAP +
507
234
  (hasConnections ? CONNECTIONS_BESIDE : 0));
508
235
  const viewport = useMemo(() => teamViewport(width || drawn, drawn), [width, drawn]);
509
- // The members' height: the character, its name and where it runs.
510
- const memberHeight = size + 56;
511
- const connectionSize = Math.round(size / 2);
512
- // Its mark, its label and how it is reached (about 45px under the mark),
513
- // and room under it before the graph's edge.
514
- 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);
515
243
  // The connections' ids, member by member: the nodes change only when they do.
516
244
  const placed = members.map(member => ({
517
245
  member: member.id,
@@ -523,13 +251,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
523
251
  const measured = useBalloonRoom(box, {
524
252
  anchor: '[data-team-member]',
525
253
  zoom: viewport.zoom,
526
- floor: balloonRoom,
254
+ floor: roomFloor,
527
255
  });
528
256
  const room = members.some(member => member.persona.notebook)
529
257
  ? Math.max(NOTEBOOK_BALLOON_ROOM, measured)
530
258
  : measured;
531
259
  // Beside a member, its connections may reach below it: the row takes them.
532
- 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)));
533
261
  const rowHeight = Math.max(memberHeight, reach);
534
262
  const total = room + placing.rows * rowHeight + (placing.rows - 1) * ROW_GAP;
535
263
  const height = Math.ceil(total * viewport.zoom);
@@ -546,7 +274,7 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
546
274
  const layout = layoutOf(id);
547
275
  const count = placed.find(({ member }) => member === id)?.connections.length ?? 0;
548
276
  const beside = count ? CONNECTIONS_BESIDE : 0;
549
- const tall = Math.max(memberHeight, connectionsReach(size, count, connectionHeight));
277
+ const tall = Math.max(memberHeight, connectionsReach(connectionsTop, count, connectionHeight, CONNECTIONS_APART));
550
278
  return {
551
279
  minDx: -layout.x,
552
280
  maxDx: drawn - NODE_WIDTH - beside - layout.x,
@@ -604,7 +332,7 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
604
332
  },
605
333
  // A member's connections move with it.
606
334
  ...connections.map((connection, at) => {
607
- const ys = connectionsY(y, size, connections.length, connectionHeight);
335
+ const ys = connectionsY(y, connectionsTop, connections.length, connectionHeight, CONNECTIONS_APART);
608
336
  return {
609
337
  id: connectionNodeId(id, connection),
610
338
  type: 'connection',
@@ -620,7 +348,18 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
620
348
  data: { member: id, connection, size: connectionSize },
621
349
  };
622
350
  }),
623
- ]), [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
+ ]);
624
363
  // The notebook open under the graph runs on a Pyodide kernel in the page:
625
364
  // the entry's sandbox, while it is open.
626
365
  const notebookKernel = useSignalValue(teamNotebookKernel);
@@ -683,10 +422,11 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
683
422
  label: wordsOf(link)
684
423
  ? `${labelOf(link.to)} · ${wordsOf(link)}`
685
424
  : labelOf(link.to),
425
+ selected: edgeSelected(selected, link.from, link.to),
686
426
  ...handlesBetween(layoutOf(link.from), layoutOf(link.to)),
687
427
  })));
688
428
  const edges = useMemo(() => [
689
- ...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, }) => ({
690
430
  id: `${from}->${to}`,
691
431
  source: from,
692
432
  sourceHandle,
@@ -695,7 +435,12 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
695
435
  type: 'a2a',
696
436
  selectable: false,
697
437
  focusable: false,
698
- data: { flow: linkFlow, connected: reached, label: said },
438
+ data: {
439
+ flow: linkFlow,
440
+ connected: reached,
441
+ label: said,
442
+ selected: chosen,
443
+ },
699
444
  })),
700
445
  ...JSON.parse(placedKey).flatMap(({ member, connections }) => connections.map(connection => {
701
446
  const node = connectionNodeId(member, connection);
@@ -713,48 +458,25 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
713
458
  flow: carried?.flow ?? 'still',
714
459
  calling,
715
460
  label: carried?.label ?? MCP_LABEL,
461
+ selected: edgeSelected(selected, member, node),
716
462
  },
717
463
  };
718
464
  })),
719
- ], [linkKey, placedKey, busy]);
465
+ ], [linkKey, placedKey, busy, selected]);
720
466
  const heard = [...words, ...busy.said].filter(Boolean).join('. ');
721
467
  const flowNow = flowOfAll(peers.map(one => flowOf(one.id)));
722
- 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: {
723
- '@keyframes a2aTeamFlow': {
724
- from: { strokeDashoffset: 24 },
725
- to: { strokeDashoffset: 0 },
726
- },
727
- '& .a2a-team-flow-asking': {
728
- animation: 'a2aTeamFlow 0.6s linear infinite',
729
- },
730
- '& .a2a-team-flow-answering': {
731
- animation: 'a2aTeamFlow 0.6s linear infinite reverse',
732
- },
733
- '@keyframes a2aTeamPulse': {
734
- '0%, 100%': { boxShadow: '0 0 0 0 transparent' },
735
- '50%': {
736
- boxShadow: '0 0 0 4px var(--bgColor-accent-muted)',
737
- },
738
- },
739
- '& .a2a-team-connection-busy': {
740
- animation: 'a2aTeamPulse 1s ease-in-out infinite',
741
- },
742
- // The arrow and the words say which way; the movement is extra.
743
- '@media (prefers-reduced-motion: reduce)': {
744
- '& .a2a-team-flow, & .a2a-team-connection-busy': {
745
- animation: 'none',
746
- },
747
- },
748
- '& .react-flow, & .react-flow__renderer, & .react-flow__viewport': {
749
- overflow: 'visible',
750
- },
751
- '& .react-flow__node': { cursor: 'default' },
752
- // A member is moved by dragging its character.
753
- '& [data-team-member] [data-assistant-figure]': { cursor: 'grab' },
754
- '& [data-team-member] [data-assistant-figure]:active': {
755
- cursor: 'grabbing',
756
- },
757
- }, 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' },
758
480
  // The library is MIT: its credit is not required on the page.
759
481
  proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${peers.map(one => one.name).join(', ')}, over A2A` }) }) }) })] }));
760
482
  }