@datalayer/agent-runtimes 1.3.73 → 1.3.75

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 (72) hide show
  1. package/lib/chat/ChatFloating.d.ts +25 -2
  2. package/lib/chat/ChatFloating.js +80 -12
  3. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  4. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +123 -5
  6. package/lib/chat/assistant/AssistantStage.js +245 -7
  7. package/lib/chat/assistant/BalloonParts.d.ts +15 -1
  8. package/lib/chat/assistant/BalloonParts.js +26 -9
  9. package/lib/chat/assistant/BalloonVisual.d.ts +65 -0
  10. package/lib/chat/assistant/BalloonVisual.js +58 -0
  11. package/lib/chat/assistant/SpeechBalloon.d.ts +68 -2
  12. package/lib/chat/assistant/SpeechBalloon.js +113 -7
  13. package/lib/chat/assistant/state.d.ts +19 -0
  14. package/lib/chat/assistant/state.js +44 -17
  15. package/lib/chat/base/ChatBase.js +51 -1
  16. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  17. package/lib/chat/indicators/TypingDots.js +21 -0
  18. package/lib/chat/messages/ChatMessageList.js +1 -15
  19. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  20. package/lib/components/inspector/AgentInspector.js +220 -0
  21. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  22. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  23. package/lib/components/inspector/JsonTree.d.ts +24 -0
  24. package/lib/components/inspector/JsonTree.js +89 -0
  25. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  26. package/lib/components/inspector/a2aInspect.js +478 -0
  27. package/lib/components/inspector/agentInspector.d.ts +179 -0
  28. package/lib/components/inspector/agentInspector.js +267 -0
  29. package/lib/components/inspector/chatInspect.d.ts +38 -0
  30. package/lib/components/inspector/chatInspect.js +65 -0
  31. package/lib/components/inspector/classify.d.ts +19 -0
  32. package/lib/components/inspector/classify.js +42 -0
  33. package/lib/components/inspector/index.d.ts +14 -0
  34. package/lib/components/inspector/index.js +18 -0
  35. package/lib/components/teams/A2ATeamGraph.d.ts +54 -3
  36. package/lib/components/teams/A2ATeamGraph.js +37 -11
  37. package/lib/components/teams/NotebookPreview.d.ts +11 -2
  38. package/lib/components/teams/NotebookPreview.js +6 -3
  39. package/lib/components/teams/NotebookPreviewView.js +9 -1
  40. package/lib/components/teams/TeamNotebook.d.ts +10 -1
  41. package/lib/components/teams/TeamNotebook.js +15 -3
  42. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  43. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  44. package/lib/components/teams/teamConnections.d.ts +9 -1
  45. package/lib/components/teams/teamConnections.js +20 -0
  46. package/lib/components/teams/useA2ATeam.d.ts +17 -4
  47. package/lib/components/teams/useA2ATeam.js +87 -23
  48. package/lib/examples/AgentA2ATeamExample.d.ts +16 -2
  49. package/lib/examples/AgentA2ATeamExample.js +65 -13
  50. package/lib/examples/{ChatAssistantExample.d.ts → AssistantExample.d.ts} +2 -2
  51. package/lib/examples/{ChatAssistantExample.js → AssistantExample.js} +69 -7
  52. package/lib/examples/{ChatAssistantGalleryExample.d.ts → AssistantGalleryExample.d.ts} +2 -2
  53. package/lib/examples/{ChatAssistantGalleryExample.js → AssistantGalleryExample.js} +45 -13
  54. package/lib/examples/example-selector.js +4 -2
  55. package/lib/examples/exampleGroups.d.ts +9 -0
  56. package/lib/examples/exampleGroups.js +56 -0
  57. package/lib/examples/index.d.ts +1 -1
  58. package/lib/examples/index.js +1 -1
  59. package/lib/examples/main.js +3 -43
  60. package/lib/examples/utils/AssistantGalleryGrid.d.ts +6 -1
  61. package/lib/examples/utils/AssistantGalleryGrid.js +16 -3
  62. package/lib/examples/utils/assistantGallery.d.ts +51 -2
  63. package/lib/examples/utils/assistantGallery.js +32 -7
  64. package/lib/loop/apps/AppRenderer.js +1 -1
  65. package/lib/loop/core/index.d.ts +17 -0
  66. package/lib/loop/core/index.js +1 -0
  67. package/lib/loop/plugins/assistant/index.d.ts +1 -1
  68. package/lib/loop/plugins/assistant/index.js +8 -3
  69. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  70. package/lib/types/chat.d.ts +7 -0
  71. package/package.json +1 -1
  72. package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
@@ -0,0 +1,267 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Agent Inspector's record: what an agent does, as it does it, one entry
7
+ * a thing — an A2A request or event, an MCP tool call, a skill, a frontend or
8
+ * backend tool, a model turn, an approval — each with its time, the agent
9
+ * that did it (`actor`), where it came from (`source`), its kind, its name,
10
+ * its status and, once it ends, its duration, a line that says it, and its
11
+ * payload whole.
12
+ *
13
+ * A call's start and its end are one entry: {@link AgentInspectorSink.start}
14
+ * opens it under a key (a tool call's id), {@link AgentInspectorSink.end}
15
+ * closes it with its result. Anything can push into a sink: the A2A capture
16
+ * (`inspectA2AFetch`), a team (`useA2ATeam`'s `inspector`), a chat
17
+ * (`ChatBase`, under {@link AgentInspectorProvider}). `AgentInspector` draws
18
+ * it.
19
+ *
20
+ * @module components/inspector/agentInspector
21
+ */
22
+ import { createContext, createElement, useContext, useState, useSyncExternalStore, } from 'react';
23
+ /** Every source, in the order the filters list them. */
24
+ export const AGENT_INSPECTOR_SOURCES = [
25
+ 'a2a',
26
+ 'mcp',
27
+ 'skill',
28
+ 'frontend-tool',
29
+ 'backend-tool',
30
+ 'model',
31
+ 'approval',
32
+ ];
33
+ /** A source, in words. */
34
+ export const AGENT_INSPECTOR_SOURCE_LABELS = {
35
+ a2a: 'A2A',
36
+ mcp: 'MCP',
37
+ skill: 'Skill',
38
+ 'frontend-tool': 'Frontend tool',
39
+ 'backend-tool': 'Backend tool',
40
+ model: 'Model',
41
+ approval: 'Approval',
42
+ };
43
+ /** How large a value is, as JSON, in bytes. */
44
+ export function jsonBytes(value) {
45
+ if (value === undefined) {
46
+ return 0;
47
+ }
48
+ try {
49
+ const text = JSON.stringify(value);
50
+ return text === undefined ? 0 : new TextEncoder().encode(text).length;
51
+ }
52
+ catch {
53
+ return 0;
54
+ }
55
+ }
56
+ /** A size, in words: `812 B`, `12.3 kB`, `1.2 MB`. */
57
+ export function formatBytes(bytes) {
58
+ if (bytes < 1000) {
59
+ return `${bytes} B`;
60
+ }
61
+ if (bytes < 1_000_000) {
62
+ return `${(bytes / 1000).toFixed(1)} kB`;
63
+ }
64
+ return `${(bytes / 1_000_000).toFixed(1)} MB`;
65
+ }
66
+ /** A line short enough for a row. */
67
+ export function shortLine(text, length = 160) {
68
+ const flat = text.replace(/\s+/g, ' ').trim();
69
+ return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
70
+ }
71
+ /** How many semantic events wait for their entry. */
72
+ const PENDING_LIMIT = 32;
73
+ /** Make a sink: the record, and who listens to it. */
74
+ export function createAgentInspector(options = {}) {
75
+ const now = options.now ?? Date.now;
76
+ const limit = options.limit ?? 2000;
77
+ let list = [];
78
+ let counter = 0;
79
+ const open = new Map();
80
+ const listeners = new Set();
81
+ let pending = [];
82
+ let scheduled = false;
83
+ const linkers = new Set();
84
+ const notify = () => {
85
+ if (scheduled) {
86
+ return;
87
+ }
88
+ scheduled = true;
89
+ queueMicrotask(() => {
90
+ scheduled = false;
91
+ listeners.forEach(listener => listener());
92
+ });
93
+ };
94
+ const replace = (id, next) => {
95
+ list = list.map(entry => (entry.id === id ? next : entry));
96
+ };
97
+ const settlePending = (entry) => {
98
+ if (!pending.length) {
99
+ return entry;
100
+ }
101
+ const taken = pending.filter(waiting => waiting.match(entry));
102
+ if (!taken.length) {
103
+ return entry;
104
+ }
105
+ pending = pending.filter(waiting => !taken.includes(waiting));
106
+ return {
107
+ ...entry,
108
+ events: [...entry.events, ...taken.map(waiting => waiting.event)],
109
+ };
110
+ };
111
+ const push = (input) => {
112
+ counter += 1;
113
+ const id = `e${counter}`;
114
+ const { links, at, ...rest } = input;
115
+ const entry = settlePending({
116
+ ...rest,
117
+ id,
118
+ at: at ?? now(),
119
+ bytes: jsonBytes(input.payload) + jsonBytes(input.result),
120
+ links: links ?? [],
121
+ events: [],
122
+ });
123
+ list = [...list, entry];
124
+ if (list.length > limit) {
125
+ list = list.slice(list.length - limit);
126
+ }
127
+ notify();
128
+ return id;
129
+ };
130
+ const find = (id) => list.find(entry => entry.id === id);
131
+ const sink = {
132
+ push,
133
+ start(input) {
134
+ const known = open.get(input.key);
135
+ const entry = known ? find(known) : undefined;
136
+ if (entry) {
137
+ const payload = input.payload ?? entry.payload;
138
+ replace(entry.id, {
139
+ ...entry,
140
+ payload,
141
+ summary: input.summary || entry.summary,
142
+ links: [...entry.links, ...(input.links ?? [])],
143
+ bytes: jsonBytes(payload) + jsonBytes(entry.result),
144
+ });
145
+ notify();
146
+ return entry.id;
147
+ }
148
+ const id = push({ ...input, status: 'running' });
149
+ open.set(input.key, id);
150
+ return id;
151
+ },
152
+ end(key, outcome = {}) {
153
+ const id = open.get(key);
154
+ const entry = id ? find(id) : undefined;
155
+ if (!id || !entry) {
156
+ return undefined;
157
+ }
158
+ open.delete(key);
159
+ const result = outcome.result ?? entry.result;
160
+ replace(id, {
161
+ ...entry,
162
+ status: outcome.status ?? (outcome.error ? 'failed' : 'done'),
163
+ endedAt: outcome.at ?? now(),
164
+ result,
165
+ error: outcome.error ?? entry.error,
166
+ summary: outcome.summary ?? entry.summary,
167
+ links: [...entry.links, ...(outcome.links ?? [])],
168
+ bytes: jsonBytes(entry.payload) + jsonBytes(result),
169
+ });
170
+ notify();
171
+ return id;
172
+ },
173
+ update(id, patch) {
174
+ const entry = find(id);
175
+ if (!entry) {
176
+ return;
177
+ }
178
+ const next = { ...entry, ...patch(entry) };
179
+ replace(id, {
180
+ ...next,
181
+ bytes: jsonBytes(next.payload) + jsonBytes(next.result),
182
+ });
183
+ notify();
184
+ },
185
+ attach(match, event) {
186
+ for (let index = list.length - 1; index >= 0; index -= 1) {
187
+ const entry = list[index];
188
+ if (match(entry) && !entry.events.includes(event)) {
189
+ replace(entry.id, { ...entry, events: [...entry.events, event] });
190
+ notify();
191
+ return;
192
+ }
193
+ }
194
+ pending = [...pending, { match, event }].slice(-PENDING_LIMIT);
195
+ },
196
+ running(key) {
197
+ return open.has(key);
198
+ },
199
+ note(event) {
200
+ for (const linker of linkers) {
201
+ const match = linker(event);
202
+ if (match) {
203
+ sink.attach(match, event);
204
+ return;
205
+ }
206
+ }
207
+ },
208
+ addLinker(linker) {
209
+ linkers.add(linker);
210
+ return () => {
211
+ linkers.delete(linker);
212
+ };
213
+ },
214
+ clear() {
215
+ list = [];
216
+ open.clear();
217
+ pending = [];
218
+ notify();
219
+ },
220
+ entries() {
221
+ return list;
222
+ },
223
+ subscribe(listener) {
224
+ listeners.add(listener);
225
+ return () => {
226
+ listeners.delete(listener);
227
+ };
228
+ },
229
+ exportSession() {
230
+ return { exportedAt: new Date(now()).toISOString(), entries: [...list] };
231
+ },
232
+ };
233
+ return sink;
234
+ }
235
+ const NO_ENTRIES = [];
236
+ const NO_SUBSCRIPTION = () => () => undefined;
237
+ /** A sink's entries, kept current; none without a sink. */
238
+ export function useAgentInspectorEntries(sink) {
239
+ return useSyncExternalStore(sink ? sink.subscribe : NO_SUBSCRIPTION, () => (sink ? sink.entries() : NO_ENTRIES), () => (sink ? sink.entries() : NO_ENTRIES));
240
+ }
241
+ /**
242
+ * A sink for a page, made once (or the one given), and its entries: what a
243
+ * page passes to its team, its chat and its `AgentInspector`.
244
+ */
245
+ export function useAgentInspector(given) {
246
+ const [own] = useState(() => given ?? createAgentInspector());
247
+ const sink = given ?? own;
248
+ return { sink, entries: useAgentInspectorEntries(sink) };
249
+ }
250
+ /** The sink the chats under it push into. */
251
+ export const AgentInspectorContext = createContext(null);
252
+ /** The sink of the nearest {@link AgentInspectorProvider}, if any. */
253
+ export function useAgentInspectorSink() {
254
+ return useContext(AgentInspectorContext);
255
+ }
256
+ /** Every chat under it records what its agent does into `sink`. */
257
+ export function AgentInspectorProvider({ sink, children, }) {
258
+ return createElement(AgentInspectorContext.Provider, { value: sink }, children);
259
+ }
260
+ /** A call's arguments, in a few words: `kind=customer, limit=100`. */
261
+ export function argumentsLine(args, length = 100) {
262
+ if (!args || typeof args !== 'object') {
263
+ return args === undefined ? '' : shortLine(String(args), length);
264
+ }
265
+ const parts = Object.entries(args).map(([key, value]) => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
266
+ return shortLine(parts.join(', '), length);
267
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A chat's agent, as the Agent Inspector records it: each turn of its model
3
+ * (the prompt, the answer, the tokens), and each tool call from its start to
4
+ * its end — an MCP server's, a skill, a frontend tool the page runs, a tool
5
+ * the runtime runs — whatever transport the chat speaks (AG-UI, Vercel AI,
6
+ * A2A, the in-browser harness): `ChatBase` tells it what its adapter told.
7
+ *
8
+ * @module components/inspector/chatInspect
9
+ */
10
+ import type { MarkedMcpServer } from '../../chat/marks/toolMarks';
11
+ import { type AgentInspectorSink } from './agentInspector';
12
+ export type ChatInspectorRecorder = {
13
+ turnStarted: (prompt: string, model?: string) => void;
14
+ turnEnded: (outcome: {
15
+ text?: string;
16
+ usage?: {
17
+ promptTokens?: number;
18
+ completionTokens?: number;
19
+ totalTokens?: number;
20
+ };
21
+ error?: string;
22
+ }) => void;
23
+ toolStarted: (call: {
24
+ id: string;
25
+ name: string;
26
+ args: Record<string, unknown>;
27
+ }) => void;
28
+ toolEnded: (id: string, outcome: {
29
+ result?: unknown;
30
+ error?: string;
31
+ }) => void;
32
+ };
33
+ /** A recorder for one chat's agent (`actor`) into `sink`. */
34
+ export declare function chatInspectorRecorder(sink: AgentInspectorSink, actor: () => string, options?: {
35
+ /** The page's own tools, by name: frontend tools. */
36
+ frontendTools?: () => readonly string[];
37
+ mcpServers?: () => readonly MarkedMcpServer[];
38
+ }): ChatInspectorRecorder;
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { argumentsLine, shortLine, } from './agentInspector';
6
+ import { classifyToolCall } from './classify';
7
+ /** A recorder for one chat's agent (`actor`) into `sink`. */
8
+ export function chatInspectorRecorder(sink, actor, options = {}) {
9
+ let turns = 0;
10
+ let turnKey;
11
+ return {
12
+ turnStarted(prompt, model) {
13
+ turns += 1;
14
+ turnKey = `chat-turn:${turns}`;
15
+ sink.start({
16
+ key: turnKey,
17
+ actor: actor(),
18
+ source: 'model',
19
+ kind: 'turn',
20
+ ...(model ? { name: model } : {}),
21
+ summary: shortLine(prompt),
22
+ payload: { prompt, ...(model ? { model } : {}) },
23
+ });
24
+ },
25
+ turnEnded({ text, usage, error }) {
26
+ if (!turnKey) {
27
+ return;
28
+ }
29
+ const total = usage?.totalTokens ??
30
+ (usage?.promptTokens ?? 0) + (usage?.completionTokens ?? 0);
31
+ sink.end(turnKey, {
32
+ ...(error ? { status: 'failed', error } : {}),
33
+ result: {
34
+ ...(text ? { text } : {}),
35
+ ...(usage ? { usage } : {}),
36
+ },
37
+ ...(total ? { summary: `${total} tokens` } : {}),
38
+ });
39
+ turnKey = undefined;
40
+ },
41
+ toolStarted({ id, name, args }) {
42
+ const kind = classifyToolCall(name, args, {
43
+ frontendTools: options.frontendTools?.(),
44
+ mcpServers: options.mcpServers?.(),
45
+ });
46
+ sink.start({
47
+ key: `chat-tool:${id}`,
48
+ actor: actor(),
49
+ source: kind.source,
50
+ ...(kind.icon ? { icon: kind.icon } : {}),
51
+ ...(kind.emoji ? { emoji: kind.emoji } : {}),
52
+ kind: 'tool-call',
53
+ name,
54
+ summary: argumentsLine(args),
55
+ payload: args,
56
+ });
57
+ },
58
+ toolEnded(id, { result, error }) {
59
+ sink.end(`chat-tool:${id}`, {
60
+ ...(error ? { status: 'failed', error } : {}),
61
+ result,
62
+ });
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Where a tool call comes from, for the Agent Inspector, by what the
3
+ * catalogue says of its name. Apart from the record itself, which a page can
4
+ * keep without loading the catalogues.
5
+ *
6
+ * @module components/inspector/classify
7
+ */
8
+ import { type MarkedMcpServer } from '../../chat/marks/toolMarks';
9
+ import type { AgentInspectorSource, ToolClass } from './agentInspector';
10
+ /**
11
+ * Where a tool call comes from, by who claims its name: a skill, one of
12
+ * `mcpServers`, a frontend tool (the catalogue's, or one of `frontendTools`),
13
+ * a runtime tool; otherwise `fallback` — a tool the runtime runs.
14
+ */
15
+ export declare function classifyToolCall(name: string, args?: Record<string, unknown>, options?: {
16
+ mcpServers?: readonly MarkedMcpServer[];
17
+ frontendTools?: readonly string[];
18
+ fallback?: AgentInspectorSource;
19
+ }): ToolClass;
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Where a tool call comes from, for the Agent Inspector, by what the
7
+ * catalogue says of its name. Apart from the record itself, which a page can
8
+ * keep without loading the catalogues.
9
+ *
10
+ * @module components/inspector/classify
11
+ */
12
+ import { marksOfToolCall, } from '../../chat/marks/toolMarks';
13
+ /**
14
+ * Where a tool call comes from, by who claims its name: a skill, one of
15
+ * `mcpServers`, a frontend tool (the catalogue's, or one of `frontendTools`),
16
+ * a runtime tool; otherwise `fallback` — a tool the runtime runs.
17
+ */
18
+ export function classifyToolCall(name, args, options = {}) {
19
+ if (options.frontendTools?.includes(name)) {
20
+ const marks = marksOfToolCall(name, args, options.mcpServers);
21
+ return {
22
+ source: 'frontend-tool',
23
+ ...(marks?.icon ? { icon: marks.icon } : {}),
24
+ ...(marks?.emoji ? { emoji: marks.emoji } : {}),
25
+ };
26
+ }
27
+ const marks = marksOfToolCall(name, args, options.mcpServers);
28
+ const source = marks?.kind === 'skill'
29
+ ? 'skill'
30
+ : marks?.kind === 'mcp'
31
+ ? 'mcp'
32
+ : marks?.kind === 'frontend'
33
+ ? 'frontend-tool'
34
+ : marks?.kind === 'runtime'
35
+ ? 'backend-tool'
36
+ : (options.fallback ?? 'backend-tool');
37
+ return {
38
+ source,
39
+ ...(marks?.icon ? { icon: marks.icon } : {}),
40
+ ...(marks?.emoji ? { emoji: marks.emoji } : {}),
41
+ };
42
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The Agent Inspector: what agents do — A2A traffic, MCP calls, skills,
3
+ * frontend and backend tools, model turns, approvals — recorded into a sink
4
+ * and drawn as rows that unfold to their payloads.
5
+ *
6
+ * @module components/inspector
7
+ */
8
+ export * from './agentInspector';
9
+ export * from './classify';
10
+ export * from './chatInspect';
11
+ export * from './a2aInspect';
12
+ export * from './JsonTree';
13
+ export * from './AgentInspector';
14
+ export * from './AgentInspectorDialog';
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Agent Inspector: what agents do — A2A traffic, MCP calls, skills,
7
+ * frontend and backend tools, model turns, approvals — recorded into a sink
8
+ * and drawn as rows that unfold to their payloads.
9
+ *
10
+ * @module components/inspector
11
+ */
12
+ export * from './agentInspector';
13
+ export * from './classify';
14
+ export * from './chatInspect';
15
+ export * from './a2aInspect';
16
+ export * from './JsonTree';
17
+ export * from './AgentInspector';
18
+ export * from './AgentInspectorDialog';
@@ -25,8 +25,14 @@
25
25
  import type { JSX } from 'react';
26
26
  import { type Edge, type EdgeProps, type Viewport } from '@xyflow/react';
27
27
  import '@xyflow/react/dist/base.css';
28
+ import { type AssistantAbout } from '../../chat/assistant/AssistantStage';
29
+ import type { BalloonSuggestion } from '../../chat/assistant/SpeechBalloon';
30
+ import type { BalloonHistoryMessage } from '../../chat/assistant/state';
31
+ import type { AssistantMenuItem } from '../../chat/assistant/AssistantContextMenu';
32
+ import type { AgentInspectorSink } from '../inspector/agentInspector';
28
33
  import { type A2ATeamCall, type A2ATeamConnection, type A2ATeamFlow } from './a2aTeamFlow';
29
34
  import type { A2ATeamPersona } from './useA2ATeam';
35
+ import type { BalloonExpandTarget } from '../../chat/assistant/BalloonVisual';
30
36
  import type { BalloonDisplay } from '../../chat/assistant/toolLine';
31
37
  /** One member of the team, as the graph draws it. */
32
38
  export type A2ATeamGraphMember = {
@@ -51,10 +57,49 @@ export type A2ATeamGraphMember = {
51
57
  */
52
58
  balloonDisplay?: BalloonDisplay;
53
59
  /**
54
- * The notebook in its balloon was clicked: by default, the page's
55
- * `TeamNotebook` is scrolled to and focused.
60
+ * The notebook in its balloon was clicked: by default, it is expanded —
61
+ * into `expandTarget`, scrolled to and focused, or in a dialog.
56
62
  */
57
63
  onNotebookOpen?: () => void;
64
+ /**
65
+ * Where the notebook it was given runs (`TeamNotebook`, editable, on the
66
+ * browser sandbox): an element of the page — the area under the graph —
67
+ * where it is drawn as it arrives and when it is expanded; without one,
68
+ * *Expand* opens it in a dialog over the page.
69
+ */
70
+ expandTarget?: BalloonExpandTarget;
71
+ /** What its notebook is called: `Accounting's notebook`. */
72
+ notebookTitle?: string;
73
+ /**
74
+ * What the person may ask it: chips in its balloon while it waits for a
75
+ * question, and a group of its menu; one chosen is sent (`onSuggestion`).
76
+ */
77
+ suggestions?: readonly BalloonSuggestion[];
78
+ onSuggestion?: (suggestion: BalloonSuggestion) => void;
79
+ /**
80
+ * The team's Agent Inspector: its menu offers *Inspect the agent…*, its
81
+ * own record (what it did, and the A2A messages it sent or received).
82
+ */
83
+ inspector?: AgentInspectorSink | null;
84
+ /** Stops its turn, from its menu, while it works. */
85
+ onStop?: () => void;
86
+ /** What its menu's *About* says. */
87
+ about?: AssistantAbout;
88
+ /** The host's own entries of its menu. */
89
+ contextMenu?: readonly AssistantMenuItem[];
90
+ /**
91
+ * What its menu calls a click on it (`onToggle`): `Ask Sales` where it
92
+ * takes the person to the composer. Without `onToggle`, its menu has no
93
+ * such entry.
94
+ */
95
+ conversationLabel?: string;
96
+ /**
97
+ * What it said and did, as its `history` balloon lists it (`useA2ATeam`'s
98
+ * `entryHistory`, `peerHistory`): shown when its menu chooses *History*.
99
+ */
100
+ history?: readonly BalloonHistoryMessage[];
101
+ /** Its balloon's display was chosen in its menu: the page may keep it. */
102
+ onBalloonDisplayChange?: (display: BalloonDisplay) => void;
58
103
  };
59
104
  export type A2ATeamGraphProps = {
60
105
  /** The member that asks: drawn on the left. */
@@ -71,6 +116,12 @@ export type A2ATeamGraphProps = {
71
116
  size?: number;
72
117
  /** The tool calls running now, to the members' connections (`useA2ATeam`). */
73
118
  calls?: A2ATeamCall[];
119
+ /**
120
+ * The room above the members for their balloons, in pixels at full scale:
121
+ * a page that shows suggestions, *more* and a notebook in them gives more.
122
+ * A notebook in a balloon gets at least its own.
123
+ */
124
+ balloonRoom?: number;
74
125
  };
75
126
  /** The words on the edge for a flow. */
76
127
  export declare function flowWords(flow: A2ATeamFlow, entry: string, peer: string): string;
@@ -85,5 +136,5 @@ export declare function callWords(member: string, connection: A2ATeamConnection,
85
136
  /** The viewport that shows the graph's width whole in a box `width` wide, never larger than drawn. */
86
137
  export declare function teamViewport(width: number): Viewport;
87
138
  /** Draw a team of two over A2A, the link moving with what travels on it. */
88
- export declare function A2ATeamGraph({ entry, peer, flow, connected, label, size, calls, }: A2ATeamGraphProps): JSX.Element;
139
+ export declare function A2ATeamGraph({ entry, peer, flow, connected, label, size, calls, balloonRoom, }: A2ATeamGraphProps): JSX.Element;
89
140
  export default A2ATeamGraph;
@@ -4,16 +4,17 @@ import { BaseEdge, EdgeLabelRenderer, Handle, Position, ReactFlow, } from '@xyfl
4
4
  import '@xyflow/react/dist/base.css';
5
5
  import { Button, Label, Text } from '@primer/react';
6
6
  import { Box } from '@datalayer/primer-addons';
7
- import { AssistantStage } from '../../chat/assistant/AssistantStage';
7
+ import { AssistantStage, } from '../../chat/assistant/AssistantStage';
8
8
  import { SpecMark } from '../../chat/marks/SpecMark';
9
9
  import { toolWords, } from './a2aTeamFlow';
10
- import { NotebookPreview, focusTeamNotebook } from './NotebookPreview';
10
+ import { NotebookPreview } from './NotebookPreview';
11
+ import { notebookBalloonVisual } from './TeamNotebook';
11
12
  import { toolLineText } from '../../chat/assistant/toolLine';
12
13
  /** The graph's own coordinates: the members, and room above them for their balloons. */
13
14
  const NODE_WIDTH = 200;
14
15
  const BALLOON_ROOM = 144;
15
16
  /** The room a balloon that holds a notebook needs above its member. */
16
- const NOTEBOOK_BALLOON_ROOM = 340;
17
+ const NOTEBOOK_BALLOON_ROOM = 480;
17
18
  /** How tall the notebook in a balloon grows before it scrolls. */
18
19
  const NOTEBOOK_PREVIEW_HEIGHT = 180;
19
20
  const GAP = 220;
@@ -49,10 +50,26 @@ const MemberNode = memo(function MemberNode({ data, }) {
49
50
  const { id, side, size } = data;
50
51
  const member = useContext(Members)[id];
51
52
  const stageRef = useRef(null);
53
+ // Its balloon's display, as its menu changes it.
54
+ const [display, setDisplay] = useState();
52
55
  if (!member) {
53
56
  return null;
54
57
  }
55
58
  const { persona } = member;
59
+ const shownDisplay = display ?? member.balloonDisplay ?? 'current';
60
+ // History: what it said and did, listed.
61
+ const listed = shownDisplay === 'history' && member.history?.length
62
+ ? { history: member.history }
63
+ : {};
64
+ // A notebook it was given: read-only in its balloon, and its large visual
65
+ // the one that runs, editable — under the graph, or in a dialog.
66
+ const notebookTitle = member.notebookTitle ?? (persona.notebook?.name || 'The notebook');
67
+ const given = persona.notebook
68
+ ? {
69
+ 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 })),
70
+ visual: notebookBalloonVisual(persona.notebook, notebookTitle),
71
+ }
72
+ : {};
56
73
  return (_jsxs(Box
57
74
  // React Flow lets the pointer through a node that is neither dragged
58
75
  // nor selected (`pointer-events: none` on its wrapper): the character,
@@ -76,25 +93,34 @@ const MemberNode = memo(function MemberNode({ data, }) {
76
93
  // middle of the graph: rightward from the left member.
77
94
  place: side === 'left'
78
95
  ? { position: 'absolute', left: 'auto' }
79
- : { position: 'absolute', right: 'auto' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: member.onToggle ?? (() => undefined), balloonDisplay: member.balloonDisplay ?? 'current', balloon: persona.tool
96
+ : { position: 'absolute', right: 'auto' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: member.onToggle ?? (() => undefined), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
97
+ setDisplay(next);
98
+ member.onBalloonDisplayChange?.(next);
99
+ }, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: persona.tool
80
100
  ? {
81
101
  text: toolLineText(persona.tool),
82
102
  tool: persona.tool,
83
103
  busy: persona.state !== 'idle',
104
+ ...given,
105
+ ...listed,
84
106
  }
85
107
  : persona.saying
86
108
  ? {
87
109
  text: persona.saying,
110
+ ...(persona.full ? { fullText: persona.full } : {}),
88
111
  more: persona.saying.endsWith('…'),
89
112
  speaking: persona.state === 'speaking',
90
113
  busy: persona.state === 'thinking' ||
91
114
  persona.state === 'working' ||
92
115
  persona.state === 'speaking',
93
- // A notebook it was given, read-only: the one that
94
- // runs is under the graph.
95
- attachment: persona.notebook ? (_jsx(NotebookPreview, { notebook: persona.notebook.data, title: persona.notebook.name || 'The notebook', maxHeight: NOTEBOOK_PREVIEW_HEIGHT, onOpen: member.onNotebookOpen ?? focusTeamNotebook })) : undefined,
116
+ ...given,
117
+ ...listed,
96
118
  }
97
- : undefined, insist: persona.insist, onDismiss: away => member.onAway?.(away !== 'none'),
119
+ : 'history' in listed
120
+ ? { text: '', ...listed }
121
+ : undefined, expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
122
+ // History, chosen in its menu: shown until Current is.
123
+ insist: 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, contextMenu: member.contextMenu,
98
124
  // A member of the graph: what is clicked around it is the graph.
99
125
  stayPut: true })) }), _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, { type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: "connections", type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: 'auto', bottom: 0 }, isConnectable: false })) : null] }));
100
126
  });
@@ -192,7 +218,7 @@ export function teamViewport(width) {
192
218
  return { zoom, x: (width - WIDTH * zoom) / 2, y: 0 };
193
219
  }
194
220
  /** Draw a team of two over A2A, the link moving with what travels on it. */
195
- export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size = 96, calls = [], }) {
221
+ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size = 96, calls = [], balloonRoom = BALLOON_ROOM, }) {
196
222
  const box = useRef(null);
197
223
  const [width, setWidth] = useState(0);
198
224
  useEffect(() => {
@@ -225,8 +251,8 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
225
251
  const hasConnections = placed.some(({ connections }) => connections.length);
226
252
  // A notebook in a balloon: more room above the members.
227
253
  const room = [entry, peer].some(member => member.persona.notebook)
228
- ? NOTEBOOK_BALLOON_ROOM
229
- : BALLOON_ROOM;
254
+ ? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
255
+ : balloonRoom;
230
256
  const height = Math.ceil((room +
231
257
  memberHeight +
232
258
  (hasConnections ? CONNECTION_GAP + connectionHeight : 0)) *