@datalayer/agent-runtimes 1.3.86 → 1.3.88

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 (66) hide show
  1. package/lib/components/a2ui/datalayer/Download.d.ts +21 -0
  2. package/lib/components/a2ui/datalayer/Download.js +66 -0
  3. package/lib/components/a2ui/datalayer/drawn.d.ts +1 -1
  4. package/lib/components/a2ui/datalayer/drawn.js +1 -0
  5. package/lib/components/a2ui/datalayer/index.d.ts +2 -1
  6. package/lib/components/a2ui/datalayer/index.js +3 -0
  7. package/lib/components/inspector/a2aSpans.d.ts +9 -0
  8. package/lib/components/inspector/a2aSpans.js +11 -1
  9. package/lib/components/teams/A2ATeamGraph.d.ts +2 -2
  10. package/lib/components/teams/A2ATeamGraph.js +39 -22
  11. package/lib/components/teams/AnswerSurfaces.d.ts +56 -0
  12. package/lib/components/teams/AnswerSurfaces.js +27 -0
  13. package/lib/components/teams/AnswerSurfacesView.d.ts +14 -0
  14. package/lib/components/teams/AnswerSurfacesView.js +30 -0
  15. package/lib/components/teams/index.d.ts +1 -0
  16. package/lib/components/teams/index.js +1 -0
  17. package/lib/components/teams/useA2ATeam.d.ts +56 -2
  18. package/lib/components/teams/useA2ATeam.js +122 -3
  19. package/lib/hooks/useAgentRuntimes.d.ts +13 -0
  20. package/lib/hooks/useAgentRuntimes.js +11 -1
  21. package/lib/loop/apps/AppFeedback.js +11 -2
  22. package/lib/loop/apps/AppRenderer.d.ts +13 -1
  23. package/lib/loop/apps/AppRenderer.js +34 -1
  24. package/lib/loop/apps/AppRuntimePlugins.d.ts +8 -0
  25. package/lib/loop/apps/AppRuntimePlugins.js +87 -0
  26. package/lib/loop/apps/feedback.d.ts +7 -0
  27. package/lib/loop/apps/feedback.js +3 -0
  28. package/lib/loop/apps/pluginPair.d.ts +60 -0
  29. package/lib/loop/apps/pluginPair.js +44 -0
  30. package/lib/loop/apps/threads.d.ts +35 -0
  31. package/lib/loop/apps/threads.js +79 -0
  32. package/lib/loop/embed/LoopEmbed.d.ts +4 -3
  33. package/lib/loop/embed/hostBridge.d.ts +27 -0
  34. package/lib/loop/embed/hostBridge.js +47 -0
  35. package/lib/loop/index.d.ts +1 -1
  36. package/lib/loop/index.js +1 -1
  37. package/lib/loop/plugins/agent-capacity/index.d.ts +7 -0
  38. package/lib/loop/plugins/agent-capacity/index.js +3 -2
  39. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +11 -0
  40. package/lib/loop/plugins/agents/appLaunch.d.ts +15 -0
  41. package/lib/loop/plugins/agents/appLaunch.js +28 -0
  42. package/lib/loop/plugins/chat/ChatView.js +177 -109
  43. package/lib/loop/shell/LoopWorkspace.d.ts +3 -3
  44. package/lib/loop/shell/LoopWorkspace.js +1 -1
  45. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +10 -2
  46. package/lib/runtimes/actions.js +23 -0
  47. package/lib/runtimes/apis.d.ts +18 -0
  48. package/lib/runtimes/browser/a2aPeer.d.ts +19 -2
  49. package/lib/runtimes/browser/a2aPeer.js +13 -7
  50. package/lib/specs/apps.js +102 -30
  51. package/lib/specs/envvars.d.ts +2 -0
  52. package/lib/specs/envvars.js +22 -0
  53. package/lib/specs/outputs.d.ts +1 -0
  54. package/lib/specs/outputs.js +12 -0
  55. package/lib/specs/sceneSchema.d.ts +8 -0
  56. package/lib/specs/sceneSchema.js +863 -0
  57. package/lib/specs/scenes.js +238 -121
  58. package/lib/specs/teams/teams.js +23 -7
  59. package/lib/specs/uiPlugins.js +55 -0
  60. package/lib/stores/agentRuntimeStore.js +4 -0
  61. package/lib/types/chat.d.ts +6 -0
  62. package/lib/types/scenes.d.ts +7 -1
  63. package/package.json +1 -1
  64. package/scripts/codegen/generate_envvars.py +1 -1
  65. package/scripts/codegen/generate_mcp_servers.py +11 -7
  66. package/scripts/codegen/generate_scenes.py +43 -1
@@ -0,0 +1,21 @@
1
+ import { type OwnCommon } from './implementation';
2
+ export type DownloadProps = OwnCommon & {
3
+ name?: string;
4
+ url?: string;
5
+ media_type?: string;
6
+ size?: number;
7
+ description?: string;
8
+ };
9
+ /** How large a file is, in words. */
10
+ export declare function sizeInWords(size: number | undefined): string;
11
+ /** The file a block offers, or why it cannot offer it. */
12
+ export declare function downloadOf(props: Pick<DownloadProps, 'name' | 'url'>): {
13
+ name: string;
14
+ url: string;
15
+ } | {
16
+ problem: string;
17
+ };
18
+ export declare function DownloadView({ props }: {
19
+ props: DownloadProps;
20
+ }): import("react").JSX.Element;
21
+ export declare const Download: import("@a2ui/react/v0_9").ReactComponentImplementation;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * A file to download (STUDIO P-04): what an application gives to save — a
8
+ * report, a sheet, an export — by its name and its link, an http(s) link or
9
+ * the file itself as a `data:` URL. Its kind and size are said beside its
10
+ * name, its description under it. Pressing it saves the file under its name
11
+ * and dispatches the block's action (its `download` event), when it has one.
12
+ * A link of any other scheme is not followed: the block says so.
13
+ *
14
+ * @module components/a2ui/datalayer/Download
15
+ */
16
+ import { useMemo } from 'react';
17
+ import { Link, Text } from '@primer/react';
18
+ import { DownloadIcon } from '@primer/octicons-react';
19
+ import { Box } from '@datalayer/primer-addons';
20
+ import { BlockFrame, Problem } from './parts';
21
+ import { ownImplementation } from './implementation';
22
+ /** The schemes a file is fetched by: the web's, or the file itself. */
23
+ const SCHEMES = /^(https?:\/\/|data:)/i;
24
+ /** How large a file is, in words. */
25
+ export function sizeInWords(size) {
26
+ if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) {
27
+ return '';
28
+ }
29
+ if (size < 1024) {
30
+ return size === 1 ? '1 byte' : `${size} bytes`;
31
+ }
32
+ const units = ['KB', 'MB', 'GB'];
33
+ let value = size / 1024;
34
+ let unit = 0;
35
+ while (value >= 1024 && unit < units.length - 1) {
36
+ value /= 1024;
37
+ unit += 1;
38
+ }
39
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
40
+ }
41
+ /** The file a block offers, or why it cannot offer it. */
42
+ export function downloadOf(props) {
43
+ const name = typeof props.name === 'string' ? props.name.trim() : '';
44
+ const url = typeof props.url === 'string' ? props.url.trim() : '';
45
+ if (!name) {
46
+ return { problem: 'This file has no name.' };
47
+ }
48
+ if (!url) {
49
+ return { problem: `${name} has no link.` };
50
+ }
51
+ if (!SCHEMES.test(url)) {
52
+ return {
53
+ problem: `${name} is not offered: its link is neither an http(s) link nor a data: URL.`,
54
+ };
55
+ }
56
+ return { name, url };
57
+ }
58
+ export function DownloadView({ props }) {
59
+ const { action } = props;
60
+ const offered = useMemo(() => downloadOf({ name: props.name, url: props.url }), [props.name, props.url]);
61
+ const said = [props.media_type, sizeInWords(props.size)]
62
+ .filter(Boolean)
63
+ .join(' · ');
64
+ return (_jsx(BlockFrame, { label: "File to download", weight: props.weight, testId: "a2ui-download", children: 'problem' in offered ? (_jsx(Problem, { children: offered.problem })) : (_jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'flex-start' }, children: [_jsx(Box, { sx: { color: 'fg.muted', pt: '2px' }, children: _jsx(DownloadIcon, { size: 16, "aria-hidden": "true" }) }), _jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', minWidth: 0 }, children: [_jsxs(Box, { children: [_jsx(Link, { href: offered.url, download: offered.name, target: "_blank", rel: "noreferrer noopener", onClick: () => action?.(), sx: { fontWeight: 'semibold', wordBreak: 'break-word' }, children: offered.name }), said ? (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 1, ml: 2 }, children: said })) : null] }), props.description ? (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: props.description })) : null] })] })) }));
65
+ }
66
+ export const Download = ownImplementation('Download', DownloadView);
@@ -6,5 +6,5 @@
6
6
  *
7
7
  * @module components/a2ui/datalayer/drawn
8
8
  */
9
- export declare const DRAWN_OWN_COMPONENTS: readonly ["Table", "Chart", "FileUpload", "Chat", "Evidence", "Form"];
9
+ export declare const DRAWN_OWN_COMPONENTS: readonly ["Table", "Chart", "FileUpload", "Chat", "Evidence", "Form", "Download"];
10
10
  export type DrawnOwnComponent = (typeof DRAWN_OWN_COMPONENTS)[number];
@@ -17,4 +17,5 @@ export const DRAWN_OWN_COMPONENTS = [
17
17
  'Chat',
18
18
  'Evidence',
19
19
  'Form',
20
+ 'Download',
20
21
  ];
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Datalayer's own components, drawn (LOOP C-18): a renderer for each the
3
3
  * catalog has beside A2UI's basic ones — Table, Chart, File upload, Chat,
4
- * Evidence and Form — made as A2UI makes its own, so that the decision
4
+ * Evidence, Form and File to download — made as A2UI makes its own, so that the decision
5
5
  * surface, an application's page, the Surface view and the components page
6
6
  * draw them as they draw a Button.
7
7
  *
@@ -19,5 +19,6 @@ export { DRAWN_OWN_COMPONENTS, type DrawnOwnComponent } from './drawn';
19
19
  export { chartOption, chartPoints, chartSeries } from './Chart';
20
20
  export { chatItems } from './Chat';
21
21
  export { evidenceSources } from './Evidence';
22
+ export { downloadOf, sizeInWords } from './Download';
22
23
  export { refusal as fileRefusal, type GivenFile } from './FileUpload';
23
24
  export { tableRows } from './Table';
@@ -10,6 +10,7 @@ import { FileUpload } from './FileUpload';
10
10
  import { Chat } from './Chat';
11
11
  import { Evidence } from './Evidence';
12
12
  import { Form } from './Form';
13
+ import { Download } from './Download';
13
14
  const RENDERERS = {
14
15
  Table,
15
16
  Chart,
@@ -17,6 +18,7 @@ const RENDERERS = {
17
18
  Chat,
18
19
  Evidence,
19
20
  Form,
21
+ Download,
20
22
  };
21
23
  /**
22
24
  * A renderer for every component of Datalayer's own the catalog has, in the
@@ -40,5 +42,6 @@ export { DRAWN_OWN_COMPONENTS } from './drawn';
40
42
  export { chartOption, chartPoints, chartSeries } from './Chart';
41
43
  export { chatItems } from './Chat';
42
44
  export { evidenceSources } from './Evidence';
45
+ export { downloadOf, sizeInWords } from './Download';
43
46
  export { refusal as fileRefusal } from './FileUpload';
44
47
  export { tableRows } from './Table';
@@ -41,9 +41,18 @@ export type A2ASpansOptions = {
41
41
  * Without it, a tool the peer's runtime runs.
42
42
  */
43
43
  classifyTool?: (name: string, args?: Record<string, unknown>) => ToolClass;
44
+ /**
45
+ * Who presses the buttons of what the peer shows: a request carrying a
46
+ * pressed button's action (`loop.action` in its message's metadata) is
47
+ * theirs, not the asker's — the person (`You`) on a page that draws the
48
+ * surfaces (STUDIO H-02). Unsaid, every request is the asker's.
49
+ */
50
+ pressedBy?: string;
44
51
  };
45
52
  /** The text of an A2A 1.0 message's or artifact's parts, as JSON carries them. */
46
53
  export declare function partsText(parts: unknown): string;
54
+ /** Whether a message carries a pressed button's action: `metadata.loop.action`. */
55
+ export declare function pressedAction(message: unknown): boolean;
47
56
  /**
48
57
  * A fetch that records the A2A traffic it carries as spans into
49
58
  * `options.tracer`, and hands the SDK what the network gave.
@@ -53,6 +53,14 @@ function stepOf(message) {
53
53
  }
54
54
  return undefined;
55
55
  }
56
+ /** Whether a message carries a pressed button's action: `metadata.loop.action`. */
57
+ export function pressedAction(message) {
58
+ if (!isObject(message) || !isObject(message.metadata)) {
59
+ return false;
60
+ }
61
+ const loop = message.metadata.loop;
62
+ return isObject(loop) && isObject(loop.action);
63
+ }
56
64
  const FAILED_STATES = /FAILED|REJECTED|CANCELED/;
57
65
  /** Records what came back for one request onto its span. */
58
66
  class ResponseRecorder {
@@ -288,7 +296,9 @@ export function traceA2AFetch(base, options) {
288
296
  const params = rpc && isObject(rpc.params) ? rpc.params : undefined;
289
297
  const message = params && isObject(params.message) ? params.message : undefined;
290
298
  const span = tracer.startSpan(rpc ? `a2a ${method}` : `${httpMethod} ${path}`, {
291
- serviceName: asker,
299
+ serviceName: options.pressedBy && pressedAction(message)
300
+ ? options.pressedBy
301
+ : asker,
292
302
  kind: 'CLIENT',
293
303
  attributes: {
294
304
  'peer.service': peer,
@@ -20,7 +20,7 @@
20
20
  * values the rows, top to bottom.
21
21
  *
22
22
  * A member's connections — the MCP servers it reaches, Odoo for Accounting —
23
- * hang under it, each a node half a member's size with the server's mark
23
+ * stand to its right, each a node half a member's size with the server's mark
24
24
  * (`SpecMark`), linked to it by an edge of its own. That edge is still until
25
25
  * the member calls one of the connection's tools: then it flows toward the
26
26
  * connection for as long as the call runs ({@link A2ATeamCall}), with the
@@ -59,7 +59,7 @@ export type A2ATeamGraphMember = {
59
59
  onToggle?: () => void;
60
60
  /** It was sent away, or called back. */
61
61
  onAway?: (away: boolean) => void;
62
- /** The MCP servers it reaches, drawn under it (`teamConnectionsOf`). */
62
+ /** The MCP servers it reaches, drawn to its right (`teamConnectionsOf`). */
63
63
  connections?: A2ATeamConnection[];
64
64
  /**
65
65
  * How its balloon shows what it says (LOOP T-23): only what it says or
@@ -25,9 +25,15 @@ const WIDTH = NODE_WIDTH * 2 + GAP;
25
25
  const ROW_GAP = 48;
26
26
  /** A connection's node: half a member's. */
27
27
  const CONNECTION_WIDTH = NODE_WIDTH / 2;
28
- /** Between a member and its connections. */
28
+ /** Between a member and its connections, to its right. */
29
29
  const CONNECTION_GAP = 44;
30
30
  const CONNECTIONS_APART = 16;
31
+ /** Where a member's connections start: right of it. */
32
+ const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
33
+ /** Below the character's middle, so the A2A edge leaving it passes above. */
34
+ const CONNECTION_DROP = 12;
35
+ /** What a member's connections take beside it. */
36
+ const CONNECTIONS_BESIDE = CONNECTION_GAP + CONNECTION_WIDTH;
31
37
  /** Where the edge leaves and reaches a member: the middle of its character. */
32
38
  const HANDLE = (size) => ({
33
39
  opacity: 0,
@@ -118,7 +124,7 @@ const Moving = createContext({
118
124
  move: () => undefined,
119
125
  reset: () => undefined,
120
126
  });
121
- /** A connection's node id: under its member. */
127
+ /** A connection's node id: beside its member. */
122
128
  const connectionNodeId = (member, connection) => `${member}/${connection}`;
123
129
  const MemberNode = memo(function MemberNode({ data, }) {
124
130
  const { id, side, size } = data;
@@ -282,7 +288,7 @@ const MemberNode = memo(function MemberNode({ data, }) {
282
288
  stayPut: true,
283
289
  // Named as the member, not its character: `Sales (in your
284
290
  // browser)`; the character describes it.
285
- 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.Bottom, style: { ...HANDLE(size), top: 'auto', bottom: 0 }, isConnectable: false })) : null] }));
291
+ 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] }));
286
292
  });
287
293
  /** A member's accessible name: its name, and where it runs. */
288
294
  export function memberLabel(member) {
@@ -314,7 +320,7 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
314
320
  gap: '2px',
315
321
  cursor: 'default',
316
322
  pointerEvents: 'all',
317
- }, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Top, style: { ...HANDLE(0), top: 0 }, isConnectable: false }), _jsx(Box, { as: "button", type: "button", className: tool ? 'a2a-team-connection-busy' : undefined, "aria-label": connectionLabel(connection, owner?.name), "aria-expanded": told, "aria-controls": detailsId, "data-connection-figure": "", onClick: () => setTold(shown => !shown), onKeyDown: (event) => {
323
+ }, 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) => {
318
324
  if (event.key === 'Escape' && told) {
319
325
  event.stopPropagation();
320
326
  setTold(false);
@@ -419,11 +425,19 @@ export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX,
419
425
  });
420
426
  const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
421
427
  const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
422
- /** Where a member's connections sit: under it, side by side, centred. */
423
- function connectionsX(memberX, count) {
424
- const row = count * CONNECTION_WIDTH + (count - 1) * CONNECTIONS_APART;
425
- const left = memberX + (NODE_WIDTH - row) / 2;
426
- return Array.from({ length: count }, (_, at) => left + at * (CONNECTION_WIDTH + CONNECTIONS_APART));
428
+ /** Where a member's connections sit: right of it, one under another from its character's middle. */
429
+ function connectionsY(memberY, size, count, connectionHeight) {
430
+ const top = memberY + size / 2 + CONNECTION_DROP;
431
+ return Array.from({ length: count }, (_, at) => top + at * (connectionHeight + CONNECTIONS_APART));
432
+ }
433
+ /** How far down a member's connections reach, from its top; none, 0. */
434
+ function connectionsReach(size, count, connectionHeight) {
435
+ return count
436
+ ? size / 2 +
437
+ CONNECTION_DROP +
438
+ count * connectionHeight +
439
+ (count - 1) * CONNECTIONS_APART
440
+ : 0;
427
441
  }
428
442
  /** What a member's call says, on its edge and to a screen reader. */
429
443
  export function callWords(member, connection, tool) {
@@ -502,7 +516,11 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
502
516
  // Where the members stand, as columns and rows, and the width that takes.
503
517
  const peerIds = peers.map(one => one.id);
504
518
  const placing = placeMembers(entry.id, peerIds, positions);
505
- const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH + (placing.columns - 1) * GAP);
519
+ const hasConnections = members.some(member => member.connections?.length);
520
+ // The last column's connections sit beside it, inside the graph.
521
+ const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH +
522
+ (placing.columns - 1) * GAP +
523
+ (hasConnections ? CONNECTIONS_BESIDE : 0));
506
524
  const viewport = useMemo(() => teamViewport(width || drawn, drawn), [width, drawn]);
507
525
  // The members' height: the character, its name and where it runs.
508
526
  const memberHeight = size + 56;
@@ -516,14 +534,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
516
534
  connections: (member.connections ?? []).map(connection => connection.id),
517
535
  }));
518
536
  const placedKey = JSON.stringify(placed);
519
- const hasConnections = placed.some(({ connections }) => connections.length);
520
537
  // A notebook in a balloon: more room above the members.
521
538
  const room = members.some(member => member.persona.notebook)
522
539
  ? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
523
540
  : balloonRoom;
524
- // Under a member: its connections, when any member has some.
525
- const below = hasConnections ? CONNECTION_GAP + connectionHeight : 0;
526
- const rowHeight = memberHeight + below;
541
+ // Beside a member, its connections may reach below it: the row takes them.
542
+ const reach = Math.max(0, ...placed.map(({ connections }) => connectionsReach(size, connections.length, connectionHeight)));
543
+ const rowHeight = Math.max(memberHeight, reach);
527
544
  const total = room + placing.rows * rowHeight + (placing.rows - 1) * ROW_GAP;
528
545
  const height = Math.ceil(total * viewport.zoom);
529
546
  // Where each member is laid out, before it is moved.
@@ -537,14 +554,14 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
537
554
  // Where the members were moved, kept inside the graph's box.
538
555
  const boundsOf = (id) => {
539
556
  const layout = layoutOf(id);
540
- const under = placed.find(({ member }) => member === id)?.connections.length
541
- ? CONNECTION_GAP + connectionHeight
542
- : 0;
557
+ const count = placed.find(({ member }) => member === id)?.connections.length ?? 0;
558
+ const beside = count ? CONNECTIONS_BESIDE : 0;
559
+ const tall = Math.max(memberHeight, connectionsReach(size, count, connectionHeight));
543
560
  return {
544
561
  minDx: -layout.x,
545
- maxDx: drawn - NODE_WIDTH - layout.x,
562
+ maxDx: drawn - NODE_WIDTH - beside - layout.x,
546
563
  minDy: -layout.y,
547
- maxDy: total - layout.y - memberHeight - under,
564
+ maxDy: total - layout.y - tall,
548
565
  };
549
566
  };
550
567
  const [moved, setMoved] = useState(() => Object.fromEntries(members.map(member => [
@@ -597,13 +614,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
597
614
  },
598
615
  // A member's connections move with it.
599
616
  ...connections.map((connection, at) => {
600
- const xs = connectionsX(x, connections.length);
617
+ const ys = connectionsY(y, size, connections.length, connectionHeight);
601
618
  return {
602
619
  id: connectionNodeId(id, connection),
603
620
  type: 'connection',
604
621
  position: {
605
- x: xs[at],
606
- y: y + memberHeight + CONNECTION_GAP,
622
+ x: x + CONNECTION_X,
623
+ y: ys[at],
607
624
  },
608
625
  width: CONNECTION_WIDTH,
609
626
  height: connectionHeight,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What the members of a team showed with their answers: components of the
3
+ * catalog, drawn under the conversation (STUDIO H-02).
4
+ *
5
+ * A member that gives `application/json+a2ui` shows an A2UI surface beside
6
+ * its words — the sources as cards that open (`Evidence`), a comparison as a
7
+ * table, a series as a chart, a choice as buttons that answer the
8
+ * application — one per artifact ({@link A2ATeamSurface}). Each is drawn
9
+ * with the A2UI surface plugin's `InlineSurface`, Datalayer's catalog, in
10
+ * the page's theme. A button pressed is the reader's answer to the member
11
+ * that showed it: its words as the next request and its action beside them
12
+ * (`pressedOf`, the `loop.action` the runtime reads), so that an option
13
+ * that does more than read is decided by the member's rules — and refused
14
+ * to a visitor in a sentence. What came back is said under the surface.
15
+ *
16
+ * This module is light: A2UI's renderer is in `AnswerSurfacesView`, loaded
17
+ * only once a surface is drawn.
18
+ *
19
+ * @module components/teams/AnswerSurfaces
20
+ */
21
+ import type { JSX } from 'react';
22
+ import type { A2APeerAction } from '../../runtimes/browser/a2aPeer';
23
+ import type { A2ATeamSurface } from './useA2ATeam';
24
+ /** A button pressed on a surface, as it is sent back: its words, and its action. */
25
+ export type AnswerPressed = {
26
+ /** The reader's turn, in words: the option's label. */
27
+ message: string;
28
+ action: A2APeerAction;
29
+ /** What choosing it does, as the option said: `read`, `send`, `write`… */
30
+ does: string;
31
+ };
32
+ /**
33
+ * What a button pressed on an answer's surface sends back, or `null` for an
34
+ * action that is not one of an answer's buttons (a form's submission).
35
+ */
36
+ export declare function pressedOf(action: {
37
+ name: string;
38
+ surfaceId: string;
39
+ context?: Record<string, unknown>;
40
+ }): AnswerPressed | null;
41
+ export type AnswerSurfacesProps = {
42
+ /** The surfaces shown, in the order they came. */
43
+ surfaces: A2ATeamSurface[];
44
+ /** Each member's name, by its id: who showed a surface. */
45
+ names?: Record<string, string>;
46
+ /**
47
+ * A button pressed on one of them: ask the member that showed it, and
48
+ * answer what it said, in words, to be shown under the surface. A team's
49
+ * `pressAction(surface.giver, pressed)` makes the press a turn of its
50
+ * conversation too. A press that throws is said, and another is taken.
51
+ */
52
+ onPress?: (surface: A2ATeamSurface, pressed: AnswerPressed) => Promise<string | undefined>;
53
+ };
54
+ /** The surfaces the members showed, each under who showed it; nothing when there is none. */
55
+ export declare function AnswerSurfaces(props: AnswerSurfacesProps): JSX.Element | null;
56
+ export default AnswerSurfaces;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense, lazy } from 'react';
3
+ import { Text } from '@primer/react';
4
+ import { answerAction } from '../../loop/plugins/a2ui-surface/toolResult';
5
+ /** The renderer itself: A2UI and the catalog, loaded when a surface is drawn. */
6
+ const AnswerSurfacesView = lazy(() => import('./AnswerSurfacesView'));
7
+ /**
8
+ * What a button pressed on an answer's surface sends back, or `null` for an
9
+ * action that is not one of an answer's buttons (a form's submission).
10
+ */
11
+ export function pressedOf(action) {
12
+ const pressed = answerAction(action);
13
+ if (!pressed) {
14
+ return null;
15
+ }
16
+ const { name, payload } = pressed.forwardedProps.loop.action;
17
+ const does = typeof payload.does === 'string' ? payload.does : 'read';
18
+ return { message: pressed.message, action: { name, payload }, does };
19
+ }
20
+ /** The surfaces the members showed, each under who showed it; nothing when there is none. */
21
+ export function AnswerSurfaces(props) {
22
+ if (!props.surfaces.length) {
23
+ return null;
24
+ }
25
+ return (_jsx(Suspense, { fallback: _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Drawing what it shows\u2026" }), children: _jsx(AnswerSurfacesView, { ...props }) }));
26
+ }
27
+ export default AnswerSurfaces;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The surfaces of `AnswerSurfaces`, drawn: each with `InlineSurface` on
3
+ * Datalayer's catalog, its title and who showed it above it, and under it
4
+ * what was chosen and what the member answered. One choice per surface:
5
+ * once a button is pressed the surface says what was chosen and asks no
6
+ * more — unless the press failed (the member not reached, a turn under
7
+ * way): what failed is said, and another press is taken.
8
+ *
9
+ * @module components/teams/AnswerSurfacesView
10
+ */
11
+ import type { JSX } from 'react';
12
+ import { type AnswerSurfacesProps } from './AnswerSurfaces';
13
+ export declare function AnswerSurfacesView({ surfaces, names, onPress, }: AnswerSurfacesProps): JSX.Element;
14
+ export default AnswerSurfacesView;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useState } from 'react';
3
+ import { Spinner, Text } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { InlineSurface, SURFACE_CATALOG_ID, } from '../../loop/plugins/a2ui-surface/InlineSurface';
6
+ import { readA2uiToolResult } from '../../loop/plugins/a2ui-surface/toolResult';
7
+ import { pressedOf } from './AnswerSurfaces';
8
+ function OneSurface({ surface, name, onPress, }) {
9
+ const parsed = useMemo(() => readA2uiToolResult(surface.artifact.data, SURFACE_CATALOG_ID), [surface.artifact.data]);
10
+ const [answered, setAnswered] = useState(null);
11
+ const onAction = useCallback((action) => {
12
+ const pressed = pressedOf(action);
13
+ if (!pressed || (answered && !answered.failed) || !onPress) {
14
+ return;
15
+ }
16
+ setAnswered({ chose: pressed.message, waiting: true });
17
+ onPress(surface, pressed).then(reply => setAnswered({ chose: pressed.message, reply, waiting: false }), (error) => setAnswered({
18
+ chose: pressed.message,
19
+ reply: error instanceof Error ? error.message : String(error),
20
+ waiting: false,
21
+ failed: true,
22
+ }));
23
+ }, [answered, onPress, surface]);
24
+ const title = parsed?.title || surface.artifact.name;
25
+ return (_jsxs(Box, { as: "section", "aria-label": name ? `${name}: ${title}` : title, "data-answer-surface": surface.id, sx: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: name ? `${name} · ${title}` : title }), parsed?.messages?.length ? (_jsx(InlineSurface, { messages: parsed.messages, onAction: onAction })) : (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "This could not be drawn: it holds no surface." })), answered ? (_jsxs(Box, { role: "status", "data-answer-reply": "", sx: { display: 'flex', flexDirection: 'column', gap: 1, fontSize: 1 }, children: [_jsxs(Text, { sx: { color: 'fg.muted' }, children: ["You chose: ", answered.chose] }), answered.waiting ? (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: name ? `Asking ${name}…` : 'Asking…' })] })) : answered.reply ? (_jsx(Text, { children: name ? `${name}: ${answered.reply}` : answered.reply })) : null] })) : null] }));
26
+ }
27
+ export function AnswerSurfacesView({ surfaces, names = {}, onPress, }) {
28
+ return (_jsx(Box, { "data-answer-surfaces": "", sx: { display: 'flex', flexDirection: 'column', gap: 3, minWidth: 0 }, children: surfaces.map(surface => (_jsx(OneSurface, { surface: surface, name: names[surface.giver], onPress: onPress }, surface.id))) }));
29
+ }
30
+ export default AnswerSurfacesView;
@@ -17,3 +17,4 @@ export * from './sceneTranscript';
17
17
  export * from './SceneTranscript';
18
18
  export * from './SceneView';
19
19
  export * from './sceneStage';
20
+ export * from './AnswerSurfaces';
@@ -21,3 +21,4 @@ export * from './sceneTranscript';
21
21
  export * from './SceneTranscript';
22
22
  export * from './SceneView';
23
23
  export * from './sceneStage';
24
+ export * from './AnswerSurfaces';
@@ -1,7 +1,7 @@
1
1
  import type { AssistantState } from '../../chat/assistant/state';
2
2
  import type { DisplayItem } from '../../types/chat';
3
3
  import { type BrowserModelOptions } from '../../runtimes/browser/model';
4
- import { type A2APeer, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
4
+ import { type A2APeer, type A2APeerAction, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
5
5
  import type { AppSpec } from '../../types/agentspecs';
6
6
  import type { OtelLiveTracer } from '@datalayer/core/lib/otel/live';
7
7
  import { type A2ATeamCall, type A2ATeamConnection, type A2ATeamFlow } from './a2aTeamFlow';
@@ -25,13 +25,51 @@ export type A2ATeamPersona = {
25
25
  notebook?: A2APeerArtifact;
26
26
  };
27
27
  export declare const AT_REST: A2ATeamPersona;
28
- /** One turn of the conversation with the entry. */
28
+ /**
29
+ * One turn of the conversation with the entry. A button pressed on what
30
+ * another member showed is a turn of it too: the person's line addressed to
31
+ * that member, and its answer from it (`member`, STUDIO H-02).
32
+ */
29
33
  export type A2ATeamTurn = {
30
34
  role: 'user' | 'assistant';
31
35
  text: string;
36
+ /**
37
+ * The member other than the entry this turn is with: the one whose
38
+ * button the person pressed, and who answered it.
39
+ */
40
+ member?: {
41
+ id: string;
42
+ name: string;
43
+ };
44
+ };
45
+ /**
46
+ * A button pressed on a surface a member showed (`AnswerSurfaces`'
47
+ * `AnswerPressed`): its words, the person's turn, and its action.
48
+ */
49
+ export type A2ATeamPress = {
50
+ message: string;
51
+ action: A2APeerAction;
32
52
  };
33
53
  /** What a page that shows notebooks accepts: a notebook, and words in Markdown. */
34
54
  export declare const NOTEBOOK_AND_WORDS: readonly string[];
55
+ /**
56
+ * What a page that draws an answer's components accepts besides: the
57
+ * catalog's surfaces, a notebook, and words in Markdown (STUDIO H-02).
58
+ */
59
+ export declare const COMPONENTS_NOTEBOOK_AND_WORDS: readonly string[];
60
+ /** A surface of components a member showed with an answer: who gave it, and the surface. */
61
+ export type A2ATeamSurface = {
62
+ /** Its surface's id: `answer-…`. */
63
+ id: string;
64
+ /** The member that showed it, by its id. */
65
+ giver: string;
66
+ artifact: A2APeerArtifact;
67
+ };
68
+ /**
69
+ * The surfaces among what a member gave, each once by its surface's id,
70
+ * after those already kept.
71
+ */
72
+ export declare function surfacesAfter(kept: A2ATeamSurface[], giver: string, artifacts: A2APeerArtifact[]): A2ATeamSurface[];
35
73
  /** What the peer gave, in words: its words, and each of its artifacts. */
36
74
  export declare function answeredLine(artifacts: A2APeerArtifact[]): string;
37
75
  /** The latest notebook among what a peer gave, or none. */
@@ -158,6 +196,11 @@ export type A2ATeam = {
158
196
  artifacts: A2APeerArtifact[];
159
197
  /** The latest notebook a peer gave, kept until another one comes. */
160
198
  notebook: A2APeerArtifact | null;
199
+ /**
200
+ * Every surface of components the members showed in this conversation,
201
+ * in the order they came, with who showed it (STUDIO H-02).
202
+ */
203
+ surfaces: A2ATeamSurface[];
161
204
  /** Which way the first peer's link carries a message now. */
162
205
  flow: A2ATeamFlow;
163
206
  /** The tool calls the peers' connections are answering now. */
@@ -167,6 +210,17 @@ export type A2ATeam = {
167
210
  /** Whether the entry can be asked: every peer is connected, or the entry is. */
168
211
  ready: boolean;
169
212
  send: (text: string) => Promise<void>;
213
+ /**
214
+ * A button pressed on what a member showed (its id: the entry's, or a
215
+ * peer's), as a turn of the conversation (STUDIO H-02): the person's line
216
+ * in the entry's history, the member asked directly over A2A with the
217
+ * button's action beside its words (`askA2APeer({ action })`), and its
218
+ * reply landing as an `ask_<peer>` answer does — in its history, its
219
+ * balloon, its flow, the team's report, surfaces and notebook — and in
220
+ * the entry's history as that member's answer. Answers its words; throws
221
+ * when the member is not reached, a turn is under way, or it failed.
222
+ */
223
+ pressAction: (memberId: string, pressed: A2ATeamPress) => Promise<string>;
170
224
  stop: () => void;
171
225
  };
172
226
  /** Run a team over A2A in the page. */