@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.
- package/README.md +29 -297
- package/dist-embed/datalayer-app-main.js +40 -36
- package/lib/apps/embed/element.js +20 -5
- package/lib/apps/embed/visitorApp.d.ts +23 -45
- package/lib/apps/embed/visitorApp.js +65 -13
- package/lib/components/teams/A2ATeamGraph.d.ts +58 -152
- package/lib/components/teams/A2ATeamGraph.js +169 -410
- package/lib/components/teams/SceneTranscript.d.ts +5 -1
- package/lib/components/teams/SceneTranscript.js +2 -2
- package/lib/components/teams/SceneView.d.ts +17 -5
- package/lib/components/teams/SceneView.js +7 -3
- package/lib/components/teams/a2aTeamFlow.d.ts +16 -7
- package/lib/components/teams/a2aTeamFlow.js +17 -14
- package/lib/components/teams/backdrops/catalogue.d.ts +35 -0
- package/lib/components/teams/backdrops/catalogue.js +34 -0
- package/lib/components/teams/backdrops/edges.d.ts +20 -0
- package/lib/components/teams/backdrops/edges.js +29 -0
- package/lib/components/teams/backdrops/faces.d.ts +24 -0
- package/lib/components/teams/backdrops/faces.js +39 -0
- package/lib/components/teams/backdrops/halo.d.ts +14 -0
- package/lib/components/teams/backdrops/halo.js +5 -0
- package/lib/components/teams/backdrops/hero.d.ts +14 -0
- package/lib/components/teams/backdrops/hero.js +178 -0
- package/lib/components/teams/backdrops/index.d.ts +22 -0
- package/lib/components/teams/backdrops/index.js +38 -0
- package/lib/components/teams/backdrops/nodes.d.ts +123 -0
- package/lib/components/teams/backdrops/nodes.js +265 -0
- package/lib/components/teams/backdrops/plain.d.ts +14 -0
- package/lib/components/teams/backdrops/plain.js +116 -0
- package/lib/components/teams/backdrops/studio.d.ts +17 -0
- package/lib/components/teams/backdrops/studio.js +122 -0
- package/lib/components/teams/backdrops/theme.d.ts +14 -0
- package/lib/components/teams/backdrops/theme.js +19 -0
- package/lib/components/teams/backdrops/types.d.ts +214 -0
- package/lib/components/teams/backdrops/types.js +5 -0
- package/lib/components/teams/index.d.ts +2 -0
- package/lib/components/teams/index.js +2 -0
- package/lib/components/teams/sceneTranscriptRules.d.ts +32 -1
- package/lib/components/teams/sceneTranscriptRules.js +104 -2
- package/lib/components/teams/transcript/RecordingLabel.d.ts +17 -0
- package/lib/components/teams/transcript/RecordingLabel.js +7 -0
- package/lib/components/teams/transcript/SequenceDiagram.d.ts +39 -0
- package/lib/components/teams/transcript/SequenceDiagram.js +190 -0
- package/lib/components/teams/transcript/TranscriptDetail.d.ts +27 -0
- package/lib/components/teams/transcript/TranscriptDetail.js +48 -0
- package/lib/components/teams/transcript/TranscriptTable.d.ts +23 -0
- package/lib/components/teams/transcript/TranscriptTable.js +112 -0
- package/lib/components/teams/transcript/TranscriptViews.d.ts +66 -0
- package/lib/components/teams/transcript/TranscriptViews.js +135 -0
- package/lib/components/teams/transcript/TranscriptViewsTypes.d.ts +13 -0
- package/lib/components/teams/transcript/TranscriptViewsTypes.js +5 -0
- package/lib/components/teams/transcript/index.d.ts +23 -0
- package/lib/components/teams/transcript/index.js +27 -0
- package/lib/components/teams/transcript/recordingPlayback.d.ts +52 -0
- package/lib/components/teams/transcript/recordingPlayback.js +146 -0
- package/lib/components/teams/transcript/selection.d.ts +59 -0
- package/lib/components/teams/transcript/selection.js +79 -0
- package/lib/components/teams/transcript/sequenceDiagramRules.d.ts +35 -0
- package/lib/components/teams/transcript/sequenceDiagramRules.js +158 -0
- package/lib/components/teams/transcript/transcriptDetailRules.d.ts +80 -0
- package/lib/components/teams/transcript/transcriptDetailRules.js +208 -0
- package/lib/components/teams/transcript/transcriptExport.d.ts +31 -0
- package/lib/components/teams/transcript/transcriptExport.js +82 -0
- package/lib/components/teams/transcript/transcriptRows.d.ts +76 -0
- package/lib/components/teams/transcript/transcriptRows.js +187 -0
- package/lib/components/teams/transcript/useSceneRecording.d.ts +45 -0
- package/lib/components/teams/transcript/useSceneRecording.js +96 -0
- package/lib/examples/ScenesExample.js +16 -3
- package/lib/portable/hosts/agentClient.d.ts +8 -0
- package/lib/portable/hosts/agentClient.js +2 -0
- package/lib/portable/hosts/approvalRenderers.d.ts +10 -2
- package/lib/portable/hosts/approvalRenderers.js +81 -2
- package/lib/portable/hosts/contracts.d.ts +1 -0
- package/lib/portable/hosts/contracts.js +2 -0
- package/lib/portable/hosts/contracts.json +56 -2
- package/lib/specs/actions.js +2 -0
- package/lib/specs/apps.d.ts +1 -0
- package/lib/specs/apps.js +219 -0
- package/lib/specs/sceneRecordings.d.ts +14 -0
- package/lib/specs/sceneRecordings.js +12 -0
- package/lib/specs/sceneSchema.js +11 -0
- package/lib/specs/scenes.d.ts +1 -0
- package/lib/specs/scenes.js +159 -0
- package/lib/types/scenes.d.ts +54 -0
- package/package.json +2 -1
- package/scripts/codegen/generate_mcp_servers.py +5 -16
- package/scripts/codegen/generate_scenes.py +80 -0
|
@@ -1,65 +1,29 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
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 {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
*
|
|
340
|
-
* the
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
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
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
/**
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
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(
|
|
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
|
|
484
|
-
const memberHeight = size
|
|
485
|
-
const connectionSize = Math.round(size
|
|
486
|
-
// Its mark, its label and how it is reached
|
|
487
|
-
//
|
|
488
|
-
const connectionHeight = connectionSize
|
|
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:
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
]), [
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
}
|