@datalayer/agent-runtimes 1.5.88 → 1.5.90
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/dist-embed/datalayer-app-main.js +57 -40
- 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/apps/plugins/agent-outputs/sheets.d.ts +76 -0
- package/lib/apps/plugins/agent-outputs/sheets.js +230 -0
- package/lib/chat/messages/ChatMarkdown.d.ts +3 -1
- package/lib/chat/messages/ChatMarkdown.js +4 -1
- package/lib/chat/messages/codeBlocks.d.ts +50 -0
- package/lib/chat/messages/codeBlocks.js +81 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +28 -7
- package/lib/components/teams/A2ATeamGraph.js +64 -27
- package/lib/components/teams/a2aTeamFlow.d.ts +16 -7
- package/lib/components/teams/a2aTeamFlow.js +17 -14
- package/lib/examples/AgentOutputsExample.d.ts +7 -0
- package/lib/examples/AgentOutputsExample.js +47 -7
- package/lib/examples/utils/agentOutputsPlugins.d.ts +2 -0
- package/lib/examples/utils/agentOutputsPlugins.js +27 -0
- 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/agents/agents.js +30 -8
- package/lib/specs/frontendTools.d.ts +1 -0
- package/lib/specs/frontendTools.js +18 -0
- package/lib/specs/mcpServers.js +20 -50
- package/lib/types/mcp.d.ts +4 -2
- package/package.json +2 -1
- package/scripts/codegen/generate_mcp_servers.py +35 -20
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fenced code blocks drawn by a plugin, in the agent's words.
|
|
3
|
+
*
|
|
4
|
+
* The chat draws an agent's answer as markdown (`ChatMarkdown`), and a
|
|
5
|
+
* fenced block as code. A plugin that knows better what a block of some
|
|
6
|
+
* language is — a ```csv block is a spreadsheet — contributes a renderer to
|
|
7
|
+
* {@link ChatCodeBlock}, and the block is drawn by it, in the message itself.
|
|
8
|
+
* Read from the Reactor platform the chat is mounted in, like the renderers
|
|
9
|
+
* of a tool call's display: a chat with no platform, or no renderer for the
|
|
10
|
+
* language, draws the block as code, as before.
|
|
11
|
+
*
|
|
12
|
+
* @module chat/messages/codeBlocks
|
|
13
|
+
*/
|
|
14
|
+
import { type ComponentType, type JSX, type ReactNode } from 'react';
|
|
15
|
+
import { type Contribution } from '@datalayer/reactor';
|
|
16
|
+
/** One fenced block of an answer: its info string's language, and its text. */
|
|
17
|
+
export type ChatCodeBlockProps = {
|
|
18
|
+
/** The fence's language, lower-cased: `csv` for ```csv. Empty for a bare fence. */
|
|
19
|
+
language: string;
|
|
20
|
+
/** The block's text, as written between the fences. */
|
|
21
|
+
code: string;
|
|
22
|
+
};
|
|
23
|
+
/** A renderer of fenced blocks, contributed to {@link ChatCodeBlock}. */
|
|
24
|
+
export type ChatCodeBlockContribution = {
|
|
25
|
+
/** Stable id, for the registry and the graph. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** The fence languages it draws, lower-case: `['csv', 'tsv']`. */
|
|
28
|
+
languages: string[];
|
|
29
|
+
/** A finer test, on the block itself; the block is drawn as code when it says no. */
|
|
30
|
+
match?: (block: ChatCodeBlockProps) => boolean;
|
|
31
|
+
/** What draws the block, in place of the code. */
|
|
32
|
+
Component: ComponentType<ChatCodeBlockProps>;
|
|
33
|
+
};
|
|
34
|
+
/** Where plugins put their fenced-block renderers. The first that takes a block draws it. */
|
|
35
|
+
export declare const ChatCodeBlock: import("@datalayer/reactor").ContributionPoint<ChatCodeBlockContribution>;
|
|
36
|
+
/**
|
|
37
|
+
* The fenced block a `<pre>` holds, as markdown renders one: a `<code>`
|
|
38
|
+
* element whose class names the language. `null` for anything else.
|
|
39
|
+
*/
|
|
40
|
+
export declare function codeBlockOf(children: ReactNode): ChatCodeBlockProps | null;
|
|
41
|
+
/** The renderer that takes a block, among the contributions, in their order. */
|
|
42
|
+
export declare function codeBlockRenderer(contributions: readonly Contribution<ChatCodeBlockContribution>[], block: ChatCodeBlockProps): ChatCodeBlockContribution | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The markdown `pre` the chat hands its renderer: a block a contribution
|
|
45
|
+
* takes is drawn by it, every other one exactly as the default `pre` draws
|
|
46
|
+
* it (its code element, unwrapped).
|
|
47
|
+
*/
|
|
48
|
+
export declare function ChatCodePre({ children, }: {
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } 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
|
+
* Fenced code blocks drawn by a plugin, in the agent's words.
|
|
8
|
+
*
|
|
9
|
+
* The chat draws an agent's answer as markdown (`ChatMarkdown`), and a
|
|
10
|
+
* fenced block as code. A plugin that knows better what a block of some
|
|
11
|
+
* language is — a ```csv block is a spreadsheet — contributes a renderer to
|
|
12
|
+
* {@link ChatCodeBlock}, and the block is drawn by it, in the message itself.
|
|
13
|
+
* Read from the Reactor platform the chat is mounted in, like the renderers
|
|
14
|
+
* of a tool call's display: a chat with no platform, or no renderer for the
|
|
15
|
+
* language, draws the block as code, as before.
|
|
16
|
+
*
|
|
17
|
+
* @module chat/messages/codeBlocks
|
|
18
|
+
*/
|
|
19
|
+
import { isValidElement, useSyncExternalStore, } from 'react';
|
|
20
|
+
import { defineContributionPoint } from '@datalayer/reactor';
|
|
21
|
+
import { useOptionalReactorPlatform } from '@datalayer/reactor/react';
|
|
22
|
+
/** Where plugins put their fenced-block renderers. The first that takes a block draws it. */
|
|
23
|
+
export const ChatCodeBlock = defineContributionPoint('chat.codeBlock');
|
|
24
|
+
const LANGUAGE = /(?:^|\s)language-([^\s]+)/;
|
|
25
|
+
/** The text of a React node: strings and numbers, joined, through elements' children. */
|
|
26
|
+
function textOf(node) {
|
|
27
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
28
|
+
return String(node);
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(node)) {
|
|
31
|
+
return node.map(textOf).join('');
|
|
32
|
+
}
|
|
33
|
+
if (isValidElement(node)) {
|
|
34
|
+
return textOf(node.props.children);
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The fenced block a `<pre>` holds, as markdown renders one: a `<code>`
|
|
40
|
+
* element whose class names the language. `null` for anything else.
|
|
41
|
+
*/
|
|
42
|
+
export function codeBlockOf(children) {
|
|
43
|
+
const only = Array.isArray(children)
|
|
44
|
+
? children.filter(child => child !== null && child !== '\n')
|
|
45
|
+
: [children];
|
|
46
|
+
if (only.length !== 1 || !isValidElement(only[0])) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const props = only[0].props;
|
|
50
|
+
const language = LANGUAGE.exec(props.className ?? '')?.[1]?.toLowerCase();
|
|
51
|
+
if (language === undefined) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return { language, code: textOf(props.children) };
|
|
55
|
+
}
|
|
56
|
+
/** The renderer that takes a block, among the contributions, in their order. */
|
|
57
|
+
export function codeBlockRenderer(contributions, block) {
|
|
58
|
+
return contributions
|
|
59
|
+
.map(entry => entry.value)
|
|
60
|
+
.find(value => value.languages.includes(block.language) &&
|
|
61
|
+
(value.match ? value.match(block) : true));
|
|
62
|
+
}
|
|
63
|
+
const NO_SUBSCRIPTION = () => () => { };
|
|
64
|
+
/**
|
|
65
|
+
* The markdown `pre` the chat hands its renderer: a block a contribution
|
|
66
|
+
* takes is drawn by it, every other one exactly as the default `pre` draws
|
|
67
|
+
* it (its code element, unwrapped).
|
|
68
|
+
*/
|
|
69
|
+
export function ChatCodePre({ children, }) {
|
|
70
|
+
const platform = useOptionalReactorPlatform();
|
|
71
|
+
useSyncExternalStore(platform?.subscribe ?? NO_SUBSCRIPTION, () => platform?.getRevision() ?? 0);
|
|
72
|
+
const block = platform ? codeBlockOf(children) : null;
|
|
73
|
+
const renderer = block
|
|
74
|
+
? codeBlockRenderer(platform.getContributions(ChatCodeBlock), block)
|
|
75
|
+
: undefined;
|
|
76
|
+
if (block && renderer) {
|
|
77
|
+
const { Component } = renderer;
|
|
78
|
+
return (_jsx("div", { "data-chat-code-block": renderer.id, children: _jsx(Component, { language: block.language, code: block.code }) }));
|
|
79
|
+
}
|
|
80
|
+
return _jsx(_Fragment, { children: children });
|
|
81
|
+
}
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
*
|
|
22
22
|
* A member's connections — the MCP servers it reaches, Odoo for Accounting —
|
|
23
23
|
* stand to its right, each a node half a member's size with the server's mark
|
|
24
|
-
* (`SpecMark`), linked to it by an edge of its own
|
|
25
|
-
* the member calls one of the connection's tools
|
|
26
|
-
* connection for as long as the call runs
|
|
27
|
-
*
|
|
24
|
+
* (`SpecMark`), linked to it by an edge of its own, `MCP`, drawn as the A2A
|
|
25
|
+
* link is: still until the member calls one of the connection's tools, then
|
|
26
|
+
* flowing toward the connection for as long as the call runs
|
|
27
|
+
* ({@link A2ATeamCall}), `MCP · list invoices`, and back with its result for
|
|
28
|
+
* a moment, `MCP · Odoo answers` (a screen reader hears who calls whom);
|
|
29
|
+
* under reduced motion, the arrow and the words alone.
|
|
28
30
|
*
|
|
29
31
|
* The graph is a picture, not an editor: nothing is dragged, panned or
|
|
30
32
|
* zoomed, and the wheel scrolls the page. Its viewport is worked out from its
|
|
@@ -232,13 +234,32 @@ export declare function arrowPath(source: {
|
|
|
232
234
|
y: number;
|
|
233
235
|
}, forward: boolean): string;
|
|
234
236
|
type CallData = {
|
|
237
|
+
/** Which way it carries a call: out with the call, back with its result. */
|
|
238
|
+
flow: A2ATeamFlow;
|
|
235
239
|
/** The tool being called, in words (`list invoices`); empty while none is. */
|
|
236
240
|
calling: string;
|
|
241
|
+
/** What it says: `MCP`, and what it carries while it carries something. */
|
|
242
|
+
label: string;
|
|
237
243
|
};
|
|
238
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* A member's link to a connection, drawn as the A2A link is: still, flowing
|
|
246
|
+
* toward the connection while a call runs, and back with its result.
|
|
247
|
+
*/
|
|
239
248
|
export declare const CallEdge: import("react").NamedExoticComponent<EdgeProps<Edge<CallData>>>;
|
|
240
|
-
/** What a
|
|
241
|
-
export declare
|
|
249
|
+
/** What a link to a connection says at rest, as `A2A` says the members' link. */
|
|
250
|
+
export declare const MCP_LABEL = "MCP";
|
|
251
|
+
/**
|
|
252
|
+
* What a link to a connection says while it carries a call, short enough
|
|
253
|
+
* for the room beside the member: the tool going out, `MCP · list invoices`,
|
|
254
|
+
* then `MCP · Odoo answers` as its result comes back.
|
|
255
|
+
*/
|
|
256
|
+
export declare function callEdgeWords(connection: A2ATeamConnection, tool: string, flow: A2ATeamFlow): string;
|
|
257
|
+
/**
|
|
258
|
+
* What a member's call says, on its edge and to a screen reader: the call
|
|
259
|
+
* going out, `Accounting calls Odoo · list invoices`, then its result
|
|
260
|
+
* coming back, `Odoo answers Accounting · list invoices`.
|
|
261
|
+
*/
|
|
262
|
+
export declare function callWords(member: string, connection: A2ATeamConnection, tool: string, flow?: A2ATeamFlow): string;
|
|
242
263
|
/** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
|
|
243
264
|
export declare function teamViewport(width: number, drawn?: number): Viewport;
|
|
244
265
|
/** Where a member is laid out: its column and row, from the positions or the default. */
|
|
@@ -8,7 +8,7 @@ import { AssistantStage, } from '../../chat/assistant/AssistantStage.js';
|
|
|
8
8
|
import { useSignalValue } from '@datalayer/reactor/react';
|
|
9
9
|
import { teamNotebookKernel } from './teamNotebookKernel.js';
|
|
10
10
|
import { SpecMark } from '../../chat/marks/SpecMark.js';
|
|
11
|
-
import { toolWords, } from './a2aTeamFlow.js';
|
|
11
|
+
import { callFlow, toolWords, } from './a2aTeamFlow.js';
|
|
12
12
|
import { NotebookPreview } from './NotebookPreview.js';
|
|
13
13
|
import { notebookBalloonVisual } from './TeamNotebook.js';
|
|
14
14
|
import { useBalloonRoom } from './useBalloonRoom.js';
|
|
@@ -26,8 +26,11 @@ const WIDTH = NODE_WIDTH * 2 + GAP;
|
|
|
26
26
|
const ROW_GAP = 48;
|
|
27
27
|
/** A connection's node: half a member's. */
|
|
28
28
|
const CONNECTION_WIDTH = NODE_WIDTH / 2;
|
|
29
|
-
/**
|
|
30
|
-
|
|
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;
|
|
31
34
|
const CONNECTIONS_APART = 16;
|
|
32
35
|
/** Where a member's connections start: right of it. */
|
|
33
36
|
const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
|
|
@@ -304,7 +307,7 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
|
|
|
304
307
|
if (!connection) {
|
|
305
308
|
return null;
|
|
306
309
|
}
|
|
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:
|
|
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) => {
|
|
308
311
|
if (event.key === 'Escape' && told) {
|
|
309
312
|
event.stopPropagation();
|
|
310
313
|
setTold(false);
|
|
@@ -367,18 +370,20 @@ const LinkEdge = memo(function LinkEdge({ id, sourceX, sourceY, targetX, targetY
|
|
|
367
370
|
opacity: flow === 'still' ? 0.6 : 0.25,
|
|
368
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 }) }) })] }));
|
|
369
372
|
});
|
|
370
|
-
/**
|
|
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
|
+
*/
|
|
371
377
|
export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
|
|
372
|
-
const
|
|
378
|
+
const flow = data?.flow ?? 'still';
|
|
373
379
|
const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
|
|
374
|
-
|
|
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');
|
|
375
382
|
return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
|
|
376
|
-
stroke: 'var(--borderColor-
|
|
383
|
+
stroke: 'var(--borderColor-accent-emphasis)',
|
|
377
384
|
strokeWidth: 2,
|
|
378
|
-
opacity:
|
|
379
|
-
} }),
|
|
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 }) }) }))] }));
|
|
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 }) }) })] }));
|
|
382
387
|
});
|
|
383
388
|
const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
|
|
384
389
|
const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
|
|
@@ -397,9 +402,30 @@ function connectionsReach(size, count, connectionHeight) {
|
|
|
397
402
|
? size / 4 + count * connectionHeight + (count - 1) * CONNECTIONS_APART
|
|
398
403
|
: 0;
|
|
399
404
|
}
|
|
400
|
-
/** What a
|
|
401
|
-
export
|
|
402
|
-
|
|
405
|
+
/** What a link to a connection says at rest, as `A2A` says the members' link. */
|
|
406
|
+
export const MCP_LABEL = 'MCP';
|
|
407
|
+
/**
|
|
408
|
+
* What a link to a connection says while it carries a call, short enough
|
|
409
|
+
* for the room beside the member: the tool going out, `MCP · list invoices`,
|
|
410
|
+
* then `MCP · Odoo answers` as its result comes back.
|
|
411
|
+
*/
|
|
412
|
+
export function callEdgeWords(connection, tool, flow) {
|
|
413
|
+
return flow === 'asking'
|
|
414
|
+
? `${MCP_LABEL} · ${toolWords(tool, connection)}`
|
|
415
|
+
: flow === 'answering'
|
|
416
|
+
? `${MCP_LABEL} · ${connection.label} answers`
|
|
417
|
+
: MCP_LABEL;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* What a member's call says, on its edge and to a screen reader: the call
|
|
421
|
+
* going out, `Accounting calls Odoo · list invoices`, then its result
|
|
422
|
+
* coming back, `Odoo answers Accounting · list invoices`.
|
|
423
|
+
*/
|
|
424
|
+
export function callWords(member, connection, tool, flow = 'asking') {
|
|
425
|
+
const words = toolWords(tool, connection);
|
|
426
|
+
return flow === 'answering'
|
|
427
|
+
? `${connection.label} answers ${member} · ${words}`
|
|
428
|
+
: `${member} calls ${connection.label} · ${words}`;
|
|
403
429
|
}
|
|
404
430
|
/** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
|
|
405
431
|
export function teamViewport(width, drawn = WIDTH) {
|
|
@@ -621,21 +647,34 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
621
647
|
? flowWords(flowOf(link.to), entry.name, memberOf(link.to)?.name ?? link.to)
|
|
622
648
|
: '';
|
|
623
649
|
const words = edgesWanted.map(wordsOf).filter(Boolean);
|
|
624
|
-
// The tool each connection is answering now, in words, by its node's id
|
|
650
|
+
// The tool each connection is answering now, in words, by its node's id,
|
|
651
|
+
// and which way its edge carries it: a call still running before one
|
|
652
|
+
// whose result is coming back.
|
|
625
653
|
const busy = useMemo(() => {
|
|
626
654
|
const byNode = {};
|
|
655
|
+
const edgeByNode = {};
|
|
627
656
|
const said = [];
|
|
628
|
-
|
|
657
|
+
const running = [
|
|
658
|
+
...calls.filter(call => callFlow(call) === 'asking'),
|
|
659
|
+
...calls.filter(call => callFlow(call) === 'answering'),
|
|
660
|
+
];
|
|
661
|
+
for (const call of running) {
|
|
629
662
|
const member = members.find(one => one.id === call.member);
|
|
630
663
|
const connection = member?.connections?.find(known => known.id === call.connection);
|
|
631
664
|
if (!member || !connection) {
|
|
632
665
|
continue;
|
|
633
666
|
}
|
|
634
667
|
const node = connectionNodeId(member.id, connection.id);
|
|
668
|
+
const flow = callFlow(call);
|
|
669
|
+
const words = callWords(member.name, connection, call.tool, flow);
|
|
635
670
|
byNode[node] ??= toolWords(call.tool, connection);
|
|
636
|
-
|
|
671
|
+
edgeByNode[node] ??= {
|
|
672
|
+
flow,
|
|
673
|
+
label: callEdgeWords(connection, call.tool, flow),
|
|
674
|
+
};
|
|
675
|
+
said.push(words);
|
|
637
676
|
}
|
|
638
|
-
return { byNode, said: [...new Set(said)] };
|
|
677
|
+
return { byNode, edgeByNode, said: [...new Set(said)] };
|
|
639
678
|
}, [calls, members]);
|
|
640
679
|
const linkKey = JSON.stringify(edgesWanted.map(link => ({
|
|
641
680
|
...link,
|
|
@@ -661,6 +700,7 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
661
700
|
...JSON.parse(placedKey).flatMap(({ member, connections }) => connections.map(connection => {
|
|
662
701
|
const node = connectionNodeId(member, connection);
|
|
663
702
|
const calling = busy.byNode[node] ?? '';
|
|
703
|
+
const carried = busy.edgeByNode[node];
|
|
664
704
|
return {
|
|
665
705
|
id: `${member}->${node}`,
|
|
666
706
|
source: member,
|
|
@@ -669,7 +709,11 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
669
709
|
type: 'mcp',
|
|
670
710
|
selectable: false,
|
|
671
711
|
focusable: false,
|
|
672
|
-
data: {
|
|
712
|
+
data: {
|
|
713
|
+
flow: carried?.flow ?? 'still',
|
|
714
|
+
calling,
|
|
715
|
+
label: carried?.label ?? MCP_LABEL,
|
|
716
|
+
},
|
|
673
717
|
};
|
|
674
718
|
})),
|
|
675
719
|
], [linkKey, placedKey, busy]);
|
|
@@ -686,19 +730,12 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
686
730
|
'& .a2a-team-flow-answering': {
|
|
687
731
|
animation: 'a2aTeamFlow 0.6s linear infinite reverse',
|
|
688
732
|
},
|
|
689
|
-
'@keyframes a2aTeamCall': {
|
|
690
|
-
from: { strokeDashoffset: 14 },
|
|
691
|
-
to: { strokeDashoffset: 0 },
|
|
692
|
-
},
|
|
693
733
|
'@keyframes a2aTeamPulse': {
|
|
694
734
|
'0%, 100%': { boxShadow: '0 0 0 0 transparent' },
|
|
695
735
|
'50%': {
|
|
696
736
|
boxShadow: '0 0 0 4px var(--bgColor-accent-muted)',
|
|
697
737
|
},
|
|
698
738
|
},
|
|
699
|
-
'& .a2a-team-call': {
|
|
700
|
-
animation: 'a2aTeamCall 0.5s linear infinite',
|
|
701
|
-
},
|
|
702
739
|
'& .a2a-team-connection-busy': {
|
|
703
740
|
animation: 'a2aTeamPulse 1s ease-in-out infinite',
|
|
704
741
|
},
|
|
@@ -65,11 +65,20 @@ export type A2ATeamCall = {
|
|
|
65
65
|
id?: string;
|
|
66
66
|
/** When it started, in milliseconds. */
|
|
67
67
|
started: number;
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* When it ended: its result is shown coming back from the connection
|
|
70
|
+
* until {@link RESULT_SHOWN_MS} after.
|
|
71
|
+
*/
|
|
72
|
+
ended?: number;
|
|
70
73
|
};
|
|
71
|
-
/**
|
|
72
|
-
|
|
74
|
+
/**
|
|
75
|
+
* How long a call's result is shown coming back to the member, in
|
|
76
|
+
* milliseconds: the link to a connection flows like the A2A link, out with
|
|
77
|
+
* the call, back with its result.
|
|
78
|
+
*/
|
|
79
|
+
export declare const RESULT_SHOWN_MS = 1200;
|
|
80
|
+
/** Which way a member's link to a connection carries a call now. */
|
|
81
|
+
export declare function callFlow(call: A2ATeamCall): A2ATeamFlow;
|
|
73
82
|
/**
|
|
74
83
|
* The connection a tool is of: one whose tools name it (a toolset's prefix
|
|
75
84
|
* before the name is allowed, as a host may add one), else one whose
|
|
@@ -85,8 +94,8 @@ export declare function toolWords(tool: string, connection?: A2ATeamConnection):
|
|
|
85
94
|
export declare function toolOwnName(tool: string, connections: A2ATeamConnection[]): string;
|
|
86
95
|
/**
|
|
87
96
|
* The calls running after an event of the peer's (`member`): a call it
|
|
88
|
-
* starts to one of its connections is added; its end marks it ended,
|
|
89
|
-
*
|
|
97
|
+
* starts to one of its connections is added; its end marks it ended, its
|
|
98
|
+
* result coming back, for {@link RESULT_SHOWN_MS} (`holdMs` says when to
|
|
90
99
|
* look again, with {@link pruneCalls}). A new request, an answer or a
|
|
91
100
|
* failure ends them all.
|
|
92
101
|
*/
|
|
@@ -94,5 +103,5 @@ export declare function callsAfter(calls: A2ATeamCall[], event: A2APeerEvent, me
|
|
|
94
103
|
calls: A2ATeamCall[];
|
|
95
104
|
holdMs?: number;
|
|
96
105
|
};
|
|
97
|
-
/** The calls without those
|
|
106
|
+
/** The calls without those whose result has been shown long enough. */
|
|
98
107
|
export declare function pruneCalls(calls: A2ATeamCall[], now: number): A2ATeamCall[];
|
|
@@ -30,8 +30,16 @@ export function flowEnds(flow, entry, peer) {
|
|
|
30
30
|
}
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* How long a call's result is shown coming back to the member, in
|
|
35
|
+
* milliseconds: the link to a connection flows like the A2A link, out with
|
|
36
|
+
* the call, back with its result.
|
|
37
|
+
*/
|
|
38
|
+
export const RESULT_SHOWN_MS = 1200;
|
|
39
|
+
/** Which way a member's link to a connection carries a call now. */
|
|
40
|
+
export function callFlow(call) {
|
|
41
|
+
return call.ended === undefined ? 'asking' : 'answering';
|
|
42
|
+
}
|
|
35
43
|
/**
|
|
36
44
|
* The connection a tool is of: one whose tools name it (a toolset's prefix
|
|
37
45
|
* before the name is allowed, as a host may add one), else one whose
|
|
@@ -65,8 +73,8 @@ export function toolOwnName(tool, connections) {
|
|
|
65
73
|
}
|
|
66
74
|
/**
|
|
67
75
|
* The calls running after an event of the peer's (`member`): a call it
|
|
68
|
-
* starts to one of its connections is added; its end marks it ended,
|
|
69
|
-
*
|
|
76
|
+
* starts to one of its connections is added; its end marks it ended, its
|
|
77
|
+
* result coming back, for {@link RESULT_SHOWN_MS} (`holdMs` says when to
|
|
70
78
|
* look again, with {@link pruneCalls}). A new request, an answer or a
|
|
71
79
|
* failure ends them all.
|
|
72
80
|
*/
|
|
@@ -96,24 +104,19 @@ export function callsAfter(calls, event, member, connections, now) {
|
|
|
96
104
|
],
|
|
97
105
|
};
|
|
98
106
|
}
|
|
99
|
-
const index = calls.findIndex(call =>
|
|
107
|
+
const index = calls.findIndex(call => call.ended === undefined &&
|
|
100
108
|
call.connection === connection.id &&
|
|
101
109
|
(step.id && call.id ? call.id === step.id : call.tool === step.name));
|
|
102
110
|
if (index < 0) {
|
|
103
111
|
return { calls };
|
|
104
112
|
}
|
|
105
|
-
const call = calls[index];
|
|
106
|
-
const left = call.started + CALL_SHOWN_MS - now;
|
|
107
|
-
if (left <= 0) {
|
|
108
|
-
return { calls: calls.filter((_, at) => at !== index) };
|
|
109
|
-
}
|
|
110
113
|
return {
|
|
111
|
-
calls: calls.map((c, at) => (at === index ? { ...c, ended:
|
|
112
|
-
holdMs:
|
|
114
|
+
calls: calls.map((c, at) => (at === index ? { ...c, ended: now } : c)),
|
|
115
|
+
holdMs: RESULT_SHOWN_MS,
|
|
113
116
|
};
|
|
114
117
|
}
|
|
115
|
-
/** The calls without those
|
|
118
|
+
/** The calls without those whose result has been shown long enough. */
|
|
116
119
|
export function pruneCalls(calls, now) {
|
|
117
|
-
const kept = calls.filter(call =>
|
|
120
|
+
const kept = calls.filter(call => call.ended === undefined || call.ended + RESULT_SHOWN_MS > now);
|
|
118
121
|
return kept.length === calls.length ? calls : kept;
|
|
119
122
|
}
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* We subscribe to the chat store, detect the output type of the latest
|
|
15
15
|
* assistant message, auto-switch the sidebar tab, and render the payload
|
|
16
16
|
* inline in the sidebar.
|
|
17
|
+
*
|
|
18
|
+
* A spreadsheet (a ```csv block) is also drawn in the chat message itself, by
|
|
19
|
+
* the `AgentOutputsSheetsPlugin` renderer the chat reads from its
|
|
20
|
+
* `ChatCodeBlock` point; the message's grid and the Sheet tab's are one
|
|
21
|
+
* workbook. The Cells agent tools (`AgentOutputsSheetToolsPlugin`, the
|
|
22
|
+
* `cells` bundle the spec's `frontend_tools` names) act on the last one, so
|
|
23
|
+
* "Chart the spreadsheet" inserts a chart that appears on both grids.
|
|
17
24
|
*/
|
|
18
25
|
import React from 'react';
|
|
19
26
|
declare const AgentOutputsExample: React.FC;
|
|
@@ -19,6 +19,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
19
19
|
* We subscribe to the chat store, detect the output type of the latest
|
|
20
20
|
* assistant message, auto-switch the sidebar tab, and render the payload
|
|
21
21
|
* inline in the sidebar.
|
|
22
|
+
*
|
|
23
|
+
* A spreadsheet (a ```csv block) is also drawn in the chat message itself, by
|
|
24
|
+
* the `AgentOutputsSheetsPlugin` renderer the chat reads from its
|
|
25
|
+
* `ChatCodeBlock` point; the message's grid and the Sheet tab's are one
|
|
26
|
+
* workbook. The Cells agent tools (`AgentOutputsSheetToolsPlugin`, the
|
|
27
|
+
* `cells` bundle the spec's `frontend_tools` names) act on the last one, so
|
|
28
|
+
* "Chart the spreadsheet" inserts a chart that appears on both grids.
|
|
22
29
|
*/
|
|
23
30
|
/// <reference types="vite/client" />
|
|
24
31
|
import { useEffect, useState, useRef, useMemo } from 'react';
|
|
@@ -26,15 +33,16 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
26
33
|
import { Text, Button, Heading, Label } from '@primer/react';
|
|
27
34
|
import { TableIcon, FileIcon, CodeIcon, GraphIcon, DownloadIcon, } from '@primer/octicons-react';
|
|
28
35
|
import { Box } from '@datalayer/primer-addons';
|
|
36
|
+
import { CellsProvider, Spreadsheet } from '@datalayer/cells';
|
|
29
37
|
import { AuthRequiredView } from './components/index.js';
|
|
30
38
|
import { useSimpleAuthStore } from '@datalayer/core/lib/views/otel/index.js';
|
|
31
39
|
import { ThemedProvider } from './utils/themedProvider.js';
|
|
32
40
|
import { uniqueAgentId } from './utils/agentId.js';
|
|
33
41
|
import { LoopEmbed } from '../apps/index.js';
|
|
34
|
-
import {
|
|
42
|
+
import { downloadSheet, parseSheetBlock, useFitHeight, useSheetModel, } from '../apps/plugins/agent-outputs/sheets.js';
|
|
43
|
+
import { AGENT_OUTPUTS_LOOP_PLUGINS } from './utils/agentOutputsPlugins.js';
|
|
35
44
|
import { useChatStore } from '../stores/chatStore.js';
|
|
36
45
|
import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl.js';
|
|
37
|
-
const LOOP_PLUGINS_AGENTOUT = [AgentOutputsPlugin];
|
|
38
46
|
const queryClient = new QueryClient();
|
|
39
47
|
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
40
48
|
const AGENT_NAME = 'outputs-example-agent';
|
|
@@ -57,8 +65,6 @@ const expandCompactMarkdownTable = (text) => {
|
|
|
57
65
|
return text.replace(/\|\|/g, '|\n|');
|
|
58
66
|
};
|
|
59
67
|
const EXT_LIKE_INFOS = new Set([
|
|
60
|
-
'csv',
|
|
61
|
-
'tsv',
|
|
62
68
|
'txt',
|
|
63
69
|
'md',
|
|
64
70
|
'markdown',
|
|
@@ -119,6 +125,20 @@ const detectOutput = (m) => {
|
|
|
119
125
|
}
|
|
120
126
|
// File: info string is a known extension OR first line declares a filename.
|
|
121
127
|
const filenameMatch = firstLine.match(/^#\s*filename:\s*([\w.-]+)/i);
|
|
128
|
+
// Spreadsheet: a CSV or TSV, by its fence (`csv`, `tsv`, `spreadsheet`,
|
|
129
|
+
// `cells`) or a `# filename:` ending in .csv or .tsv — read as the chat's
|
|
130
|
+
// renderer reads it, so both grids show the same workbook.
|
|
131
|
+
const sheet = parseSheetBlock(info, rawBody);
|
|
132
|
+
if (sheet) {
|
|
133
|
+
return {
|
|
134
|
+
tab: 'sheet',
|
|
135
|
+
payload: sheet.source,
|
|
136
|
+
filename: sheet.name,
|
|
137
|
+
extension: sheet.extension,
|
|
138
|
+
sheet,
|
|
139
|
+
messageId: m.id,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
122
142
|
if (filenameMatch || EXT_LIKE_INFOS.has(info)) {
|
|
123
143
|
const filename = filenameMatch?.[1] ?? (info ? `output.${info}` : 'output.txt');
|
|
124
144
|
return {
|
|
@@ -257,6 +277,24 @@ const ChartView = ({ source }) => {
|
|
|
257
277
|
}
|
|
258
278
|
return (_jsx(ReactECharts, { option: option, style: { height: 280, width: '100%' } }));
|
|
259
279
|
};
|
|
280
|
+
// ─── Spreadsheet renderer (Datalayer Cells) ───────────────────────────────
|
|
281
|
+
/**
|
|
282
|
+
* A CSV or TSV the agent wrote, in a Datalayer Cells grid: the workbook the
|
|
283
|
+
* chat message shows too (read once, on this thread), editable here, and
|
|
284
|
+
* where the agent's chart goes.
|
|
285
|
+
*/
|
|
286
|
+
const SheetView = ({ block }) => {
|
|
287
|
+
const { model, error } = useSheetModel(block);
|
|
288
|
+
// As tall as its data and charts (up to 640 px), so a chart the agent adds shows.
|
|
289
|
+
const height = useFitHeight(model, 200, 640);
|
|
290
|
+
if (error) {
|
|
291
|
+
return (_jsxs(Text, { sx: { color: 'danger.fg', fontSize: 0 }, children: ["Could not read the spreadsheet: ", error] }));
|
|
292
|
+
}
|
|
293
|
+
if (!model) {
|
|
294
|
+
return _jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: "Reading\u2026" });
|
|
295
|
+
}
|
|
296
|
+
return (_jsx(CellsProvider, { model: model, children: _jsx(Box, { height: height, border: "1px solid", borderColor: "border.muted", children: _jsx(Spreadsheet, { model: model, "aria-label": block.name }) }) }));
|
|
297
|
+
};
|
|
260
298
|
// ─── Inner component (rendered after auth) ─────────────────────────────────
|
|
261
299
|
const AgentOutputsInner = () => {
|
|
262
300
|
// The Loop creates the agent from the plugin's blueprint when it connects;
|
|
@@ -312,10 +350,11 @@ const AgentOutputsInner = () => {
|
|
|
312
350
|
document.body.removeChild(a);
|
|
313
351
|
URL.revokeObjectURL(url);
|
|
314
352
|
};
|
|
315
|
-
return (_jsxs(Box, { height: "100%", display: "flex", flexDirection: "column", children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, px: 3, py: 2, borderBottom: "1px solid", borderColor: "border.default", flexShrink: 0, children: [_jsx(TableIcon, { size: 16 }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, flex: 1 }, children: "Agent Outputs \u2014 Local Runtime" })] }), _jsxs(Box, { flex: 1, minHeight: 0, display: "flex", children: [_jsx(Box, { flex: 1, minWidth: 0, children: _jsx(LoopEmbed, { serverUrl: agentBaseUrl, target: "local", showAgentVariants: true, agentId: agentName, editors: false, showHeader: true, plugins:
|
|
353
|
+
return (_jsxs(Box, { height: "100%", display: "flex", flexDirection: "column", children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, px: 3, py: 2, borderBottom: "1px solid", borderColor: "border.default", flexShrink: 0, children: [_jsx(TableIcon, { size: 16 }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, flex: 1 }, children: "Agent Outputs \u2014 Local Runtime" })] }), _jsxs(Box, { flex: 1, minHeight: 0, display: "flex", children: [_jsx(Box, { flex: 1, minWidth: 0, children: _jsx(LoopEmbed, { serverUrl: agentBaseUrl, target: "local", showAgentVariants: true, agentId: agentName, editors: false, showHeader: true, plugins: AGENT_OUTPUTS_LOOP_PLUGINS }) }), _jsxs(Box, { width: 420, borderLeft: "1px solid", borderColor: "border.default", display: "flex", flexDirection: "column", overflow: "hidden", children: [_jsx(Box, { display: "flex", borderBottom: "1px solid", borderColor: "border.default", flexShrink: 0, children: [
|
|
316
354
|
{ key: 'table', icon: TableIcon, label: 'Table' },
|
|
317
355
|
{ key: 'json', icon: CodeIcon, label: 'JSON' },
|
|
318
356
|
{ key: 'chart', icon: GraphIcon, label: 'Chart' },
|
|
357
|
+
{ key: 'sheet', icon: TableIcon, label: 'Sheet' },
|
|
319
358
|
{ key: 'files', icon: FileIcon, label: 'Files' },
|
|
320
359
|
].map(t => {
|
|
321
360
|
const n = countByTab(t.key);
|
|
@@ -328,14 +367,15 @@ const AgentOutputsInner = () => {
|
|
|
328
367
|
borderColor: activeTab === t.key ? 'accent.fg' : 'transparent',
|
|
329
368
|
fontWeight: activeTab === t.key ? 'bold' : 'normal',
|
|
330
369
|
}, children: [t.label, n > 0 ? ` (${n})` : ''] }, t.key));
|
|
331
|
-
}) }), _jsxs(Box, { p: 3, flex: 1, overflow: "auto", children: [_jsxs(Heading, { as: "h4", sx: { fontSize: 1, mb: 2 }, children: [activeTab.charAt(0).toUpperCase() + activeTab.slice(1), " outputs"] }), filtered.length === 0 ? (_jsxs(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: ["No ", activeTab, " outputs yet. Use one of the openers in the chat to produce one \u2014 the side panel will switch to the matching tab automatically."
|
|
370
|
+
}) }), _jsxs(Box, { p: 3, flex: 1, overflow: "auto", children: [_jsxs(Heading, { as: "h4", sx: { fontSize: 1, mb: 2 }, children: [activeTab.charAt(0).toUpperCase() + activeTab.slice(1), " outputs"] }), filtered.length === 0 ? (_jsxs(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: ["No ", activeTab, " outputs yet. Use one of the openers in the chat to produce one \u2014 the side panel will switch to the matching tab automatically.", activeTab === 'sheet' &&
|
|
371
|
+
' For a spreadsheet, ask for the data as CSV, for example: "Give me twelve months of sales by region as a CSV".'] })) : (filtered.map((d, idx) => (_jsxs(Box, { p: 2, mb: 2, border: "1px solid", borderColor: "border.default", borderRadius: 2, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, gap: 2, children: [_jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold' }, children: d.filename ?? `${d.tab} output` }), _jsx(Label, { size: "small", variant: "secondary", children: d.tab })] }), d.tab === 'table' && _jsx(MarkdownTable, { source: d.payload }), d.tab === 'json' && (_jsx(Box, { as: "pre", bg: "canvas.default", p: 2, borderRadius: 2, fontFamily: "mono", fontSize: 0, maxHeight: 320, overflow: "auto", whiteSpace: "pre-wrap", m: 0, children: (() => {
|
|
332
372
|
try {
|
|
333
373
|
return JSON.stringify(JSON.parse(d.payload), null, 2);
|
|
334
374
|
}
|
|
335
375
|
catch {
|
|
336
376
|
return d.payload;
|
|
337
377
|
}
|
|
338
|
-
})() })), d.tab === 'chart' && _jsx(ChartView, { source: d.payload }), d.tab === 'files' && (_jsxs(_Fragment, { children: [_jsx(Box, { as: "pre", bg: "canvas.default", p: 2, borderRadius: 2, fontFamily: "mono", fontSize: 0, maxHeight: 200, overflow: "auto", whiteSpace: "pre-wrap", m: 0, mb: 2, children: d.payload }), _jsxs(Button, { size: "small", leadingVisual: DownloadIcon, onClick: () => download(d), children: ["Download ", d.filename ?? 'file'] })] }))] }, d.messageId + ':' + idx))))] })] })] })] }));
|
|
378
|
+
})() })), d.tab === 'chart' && _jsx(ChartView, { source: d.payload }), d.tab === 'sheet' && d.sheet && (_jsxs(_Fragment, { children: [_jsx(SheetView, { block: d.sheet }), _jsx(Box, { mt: 2, children: _jsxs(Button, { size: "small", leadingVisual: DownloadIcon, onClick: () => downloadSheet(d.sheet), children: ["Download ", d.filename ?? 'spreadsheet'] }) })] })), d.tab === 'files' && (_jsxs(_Fragment, { children: [_jsx(Box, { as: "pre", bg: "canvas.default", p: 2, borderRadius: 2, fontFamily: "mono", fontSize: 0, maxHeight: 200, overflow: "auto", whiteSpace: "pre-wrap", m: 0, mb: 2, children: d.payload }), _jsxs(Button, { size: "small", leadingVisual: DownloadIcon, onClick: () => download(d), children: ["Download ", d.filename ?? 'file'] })] }))] }, d.messageId + ':' + idx))))] })] })] })] }));
|
|
339
379
|
};
|
|
340
380
|
// ─── Sync token to core IAM store ──────────────────────────────────────────
|
|
341
381
|
const syncTokenToIamStore = (token) => {
|