@datalayer/agent-runtimes 1.3.74 → 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.
- package/lib/chat/ChatFloating.d.ts +17 -1
- package/lib/chat/ChatFloating.js +52 -5
- package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
- package/lib/chat/assistant/AssistantContextMenu.js +68 -0
- package/lib/chat/assistant/AssistantStage.d.ts +95 -1
- package/lib/chat/assistant/AssistantStage.js +227 -7
- package/lib/chat/assistant/BalloonParts.d.ts +15 -1
- package/lib/chat/assistant/BalloonParts.js +26 -9
- package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
- package/lib/chat/assistant/BalloonVisual.js +3 -3
- package/lib/chat/assistant/SpeechBalloon.d.ts +47 -1
- package/lib/chat/assistant/SpeechBalloon.js +60 -8
- package/lib/chat/base/ChatBase.js +51 -1
- package/lib/chat/indicators/TypingDots.d.ts +13 -0
- package/lib/chat/indicators/TypingDots.js +21 -0
- package/lib/chat/messages/ChatMessageList.js +1 -15
- package/lib/components/inspector/AgentInspector.d.ts +59 -0
- package/lib/components/inspector/AgentInspector.js +220 -0
- package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
- package/lib/components/inspector/AgentInspectorDialog.js +9 -0
- package/lib/components/inspector/JsonTree.d.ts +24 -0
- package/lib/components/inspector/JsonTree.js +89 -0
- package/lib/components/inspector/a2aInspect.d.ts +56 -0
- package/lib/components/inspector/a2aInspect.js +478 -0
- package/lib/components/inspector/agentInspector.d.ts +179 -0
- package/lib/components/inspector/agentInspector.js +267 -0
- package/lib/components/inspector/chatInspect.d.ts +38 -0
- package/lib/components/inspector/chatInspect.js +65 -0
- package/lib/components/inspector/classify.d.ts +19 -0
- package/lib/components/inspector/classify.js +42 -0
- package/lib/components/inspector/index.d.ts +14 -0
- package/lib/components/inspector/index.js +18 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +42 -1
- package/lib/components/teams/A2ATeamGraph.js +24 -7
- package/lib/components/teams/NotebookPreviewView.js +9 -1
- package/lib/components/teams/TeamNotebook.d.ts +3 -1
- package/lib/components/teams/TeamNotebook.js +3 -3
- package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
- package/lib/components/teams/jupyterThemeVariables.js +441 -0
- package/lib/components/teams/teamConnections.d.ts +9 -1
- package/lib/components/teams/teamConnections.js +20 -0
- package/lib/components/teams/useA2ATeam.d.ts +17 -4
- package/lib/components/teams/useA2ATeam.js +87 -23
- package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
- package/lib/examples/AgentA2ATeamExample.js +61 -13
- package/lib/examples/AssistantExample.js +62 -2
- package/lib/loop/apps/AppRenderer.js +1 -1
- package/lib/loop/core/index.d.ts +17 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/plugins/assistant/index.d.ts +1 -1
- package/lib/loop/plugins/assistant/index.js +8 -3
- package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
- package/lib/types/chat.d.ts +7 -0
- package/package.json +1 -1
- package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* with whatever `inference` holds — a signed-in person's token, or a
|
|
12
12
|
* visitor's trial key. Its one tool asks the peer (Accounting) with
|
|
13
13
|
* {@link a2aPeerTool}. What each of them does is kept as a persona — the
|
|
14
|
-
* state its character acts and what its balloon says —
|
|
15
|
-
*
|
|
14
|
+
* state its character acts and what its balloon says — the peer's last
|
|
15
|
+
* answer as the report, and the way
|
|
16
16
|
* the link carries a message as the flow ({@link flowAfter}). What the peer
|
|
17
17
|
* gives besides words — the formats the page accepts (`accept`), such as a
|
|
18
18
|
* Jupyter notebook — is kept as its artifacts, and its latest notebook kept
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* peer calls on its connections (its MCP servers), told over A2A as it calls
|
|
21
21
|
* them, are kept as the calls running now ({@link callsAfter}).
|
|
22
22
|
*
|
|
23
|
+
* Given an Agent Inspector's sink (`inspector`), the entry's turns (its
|
|
24
|
+
* model, its tokens), its call to the peer, and what the peer's events were
|
|
25
|
+
* made of are recorded there; the A2A traffic itself is recorded by the
|
|
26
|
+
* peer's fetch (`connectA2APeer`'s `inspect`).
|
|
27
|
+
*
|
|
23
28
|
* What `A2ATeamGraph` draws, and what a page's composer sends.
|
|
24
29
|
*
|
|
25
30
|
* @module components/teams/useA2ATeam
|
|
@@ -41,7 +46,7 @@ export const NOTEBOOK_AND_WORDS = [
|
|
|
41
46
|
NOTEBOOK_MEDIA_TYPE,
|
|
42
47
|
'text/markdown',
|
|
43
48
|
];
|
|
44
|
-
/** What
|
|
49
|
+
/** What the peer gave, in words: its words, and each of its artifacts. */
|
|
45
50
|
export function answeredLine(artifacts) {
|
|
46
51
|
const besides = artifacts.map(artifact => artifact.mediaType === NOTEBOOK_MEDIA_TYPE
|
|
47
52
|
? `a notebook, ${artifact.name}`
|
|
@@ -61,7 +66,7 @@ export function balloonLine(text, length = 120) {
|
|
|
61
66
|
}
|
|
62
67
|
/** Run a team of two over A2A in the page. */
|
|
63
68
|
export function useA2ATeam(options) {
|
|
64
|
-
const { entry, peerApp, peer, inference, maxSteps = 6, peerConnections = NO_CONNECTIONS, } = options;
|
|
69
|
+
const { entry, peerApp, peer, inference, maxSteps = 6, peerConnections = NO_CONNECTIONS, inspector = null, } = options;
|
|
65
70
|
// By what it names, so that a page passing a new array of the same media
|
|
66
71
|
// types does not make the agent again.
|
|
67
72
|
const acceptKey = options.accept?.join('\n');
|
|
@@ -71,24 +76,34 @@ export function useA2ATeam(options) {
|
|
|
71
76
|
...AT_REST,
|
|
72
77
|
state: 'greeting',
|
|
73
78
|
saying: balloonLine(entry.interface.welcome ?? ''),
|
|
79
|
+
full: entry.interface.welcome ?? '',
|
|
74
80
|
insist: Boolean(entry.interface.welcome),
|
|
75
81
|
}));
|
|
76
82
|
const [peerPersona, setPeerPersona] = useState(AT_REST);
|
|
77
83
|
const [turns, setTurns] = useState([]);
|
|
84
|
+
const [peerHistory, setPeerHistory] = useState([]);
|
|
85
|
+
const told = useRef(0);
|
|
86
|
+
const tell = useCallback((role, text) => {
|
|
87
|
+
told.current += 1;
|
|
88
|
+
const id = `peer-${told.current}`;
|
|
89
|
+
setPeerHistory(prev => [...prev, { id, role, text }]);
|
|
90
|
+
}, []);
|
|
78
91
|
const [report, setReport] = useState(null);
|
|
79
92
|
const [artifacts, setArtifacts] = useState([]);
|
|
80
93
|
const [notebook, setNotebook] = useState(null);
|
|
81
|
-
const [exchange, setExchange] = useState([]);
|
|
82
94
|
const [flow, setFlow] = useState('still');
|
|
83
95
|
const [calls, setCalls] = useState([]);
|
|
84
96
|
const callsNow = useRef([]);
|
|
85
97
|
const callTimer = useRef(undefined);
|
|
86
98
|
const [busy, setBusy] = useState(false);
|
|
87
99
|
const history = useRef([]);
|
|
100
|
+
const turnCount = useRef(0);
|
|
88
101
|
const abort = useRef(null);
|
|
89
102
|
const flowTimer = useRef(undefined);
|
|
90
103
|
// What the peer does, as it does it: its own state and balloon, and the link.
|
|
91
104
|
const onPeerEvent = useCallback((event) => {
|
|
105
|
+
// Linked to the A2A entry it came from (the capture's linker).
|
|
106
|
+
inspector?.note(event);
|
|
92
107
|
const next = flowAfter(event);
|
|
93
108
|
clearTimeout(flowTimer.current);
|
|
94
109
|
setFlow(next.flow);
|
|
@@ -108,26 +123,27 @@ export function useA2ATeam(options) {
|
|
|
108
123
|
}, after.holdMs);
|
|
109
124
|
}
|
|
110
125
|
if (event.phase === 'asked') {
|
|
111
|
-
|
|
112
|
-
...prev,
|
|
113
|
-
`${entry.name} → ${peerApp.name}: ${event.request}`,
|
|
114
|
-
]);
|
|
126
|
+
tell('user', event.request);
|
|
115
127
|
setPeerPersona(prev => ({
|
|
116
128
|
...prev,
|
|
117
129
|
state: 'greeting',
|
|
118
130
|
saying: 'On it. Let me read the books.',
|
|
131
|
+
full: undefined,
|
|
119
132
|
insist: true,
|
|
120
133
|
tool: undefined,
|
|
121
134
|
}));
|
|
122
135
|
}
|
|
123
136
|
else if (event.phase === 'working') {
|
|
124
|
-
if (event.
|
|
125
|
-
|
|
137
|
+
if (event.tool?.ended) {
|
|
138
|
+
tell('assistant', event.tool.error
|
|
139
|
+
? `${event.tool.name} failed: ${event.tool.error}`
|
|
140
|
+
: `Used ${toolOwnName(event.tool.name, peerConnections)}`);
|
|
126
141
|
}
|
|
127
142
|
setPeerPersona(prev => ({
|
|
128
143
|
...prev,
|
|
129
144
|
state: 'working',
|
|
130
145
|
saying: event.note ? balloonLine(event.note) : prev.saying,
|
|
146
|
+
full: event.note ?? prev.full,
|
|
131
147
|
insist: Boolean(event.note) || Boolean(event.tool) || prev.insist,
|
|
132
148
|
// The tool it calls, in plain words, from the step it told; a
|
|
133
149
|
// step without a tool (its own words) puts the tool line away.
|
|
@@ -137,10 +153,7 @@ export function useA2ATeam(options) {
|
|
|
137
153
|
}));
|
|
138
154
|
}
|
|
139
155
|
else if (event.phase === 'answered') {
|
|
140
|
-
|
|
141
|
-
...prev,
|
|
142
|
-
`${peerApp.name} → ${entry.name}: ${answeredLine(event.artifacts)}`,
|
|
143
|
-
]);
|
|
156
|
+
tell('assistant', event.answer);
|
|
144
157
|
setReport(event.answer);
|
|
145
158
|
setArtifacts(event.artifacts);
|
|
146
159
|
const given = notebookAmong(event.artifacts);
|
|
@@ -151,6 +164,7 @@ export function useA2ATeam(options) {
|
|
|
151
164
|
...prev,
|
|
152
165
|
state: 'speaking',
|
|
153
166
|
saying: balloonLine(event.answer),
|
|
167
|
+
full: event.answer,
|
|
154
168
|
insist: true,
|
|
155
169
|
tool: undefined,
|
|
156
170
|
}));
|
|
@@ -160,19 +174,17 @@ export function useA2ATeam(options) {
|
|
|
160
174
|
}
|
|
161
175
|
}
|
|
162
176
|
else {
|
|
163
|
-
|
|
164
|
-
...prev,
|
|
165
|
-
`${peerApp.name} could not answer: ${event.error}`,
|
|
166
|
-
]);
|
|
177
|
+
tell('assistant', `Could not answer: ${event.error}`);
|
|
167
178
|
setPeerPersona(prev => ({
|
|
168
179
|
...prev,
|
|
169
180
|
state: 'idle',
|
|
170
181
|
saying: balloonLine(event.error),
|
|
182
|
+
full: event.error,
|
|
171
183
|
insist: true,
|
|
172
184
|
tool: undefined,
|
|
173
185
|
}));
|
|
174
186
|
}
|
|
175
|
-
}, [
|
|
187
|
+
}, [peerApp.id, peerConnections, inspector, tell]);
|
|
176
188
|
const agent = useMemo(() => {
|
|
177
189
|
if (!peer) {
|
|
178
190
|
return null;
|
|
@@ -198,7 +210,6 @@ export function useA2ATeam(options) {
|
|
|
198
210
|
setBusy(true);
|
|
199
211
|
setReport(null);
|
|
200
212
|
setArtifacts([]);
|
|
201
|
-
setExchange([]);
|
|
202
213
|
setTurns(prev => [...prev, { role: 'user', text: asked }]);
|
|
203
214
|
setEntryPersona({
|
|
204
215
|
...AT_REST,
|
|
@@ -210,6 +221,19 @@ export function useA2ATeam(options) {
|
|
|
210
221
|
history.current = [...history.current, { role: 'user', content: asked }];
|
|
211
222
|
abort.current = new AbortController();
|
|
212
223
|
let said = '';
|
|
224
|
+
// The turn, in the inspector: the entry's model, from the question to
|
|
225
|
+
// the answer, and its tokens.
|
|
226
|
+
turnCount.current += 1;
|
|
227
|
+
const turnKey = `turn:${entry.id}:${turnCount.current}`;
|
|
228
|
+
inspector?.start({
|
|
229
|
+
key: turnKey,
|
|
230
|
+
actor: entry.name,
|
|
231
|
+
source: 'model',
|
|
232
|
+
kind: 'turn',
|
|
233
|
+
name: entry.model || undefined,
|
|
234
|
+
summary: balloonLine(asked, 160),
|
|
235
|
+
payload: { prompt: asked, model: entry.model || undefined },
|
|
236
|
+
});
|
|
213
237
|
try {
|
|
214
238
|
const result = await agent.stream({
|
|
215
239
|
messages: history.current,
|
|
@@ -218,10 +242,21 @@ export function useA2ATeam(options) {
|
|
|
218
242
|
for await (const part of result.fullStream) {
|
|
219
243
|
if (part.type === 'tool-call' && part.toolName === askTool) {
|
|
220
244
|
const request = String(part.input?.request ?? '');
|
|
245
|
+
// Its tool runs here, in the page, and asks the peer over A2A.
|
|
246
|
+
inspector?.start({
|
|
247
|
+
key: `tool:${part.toolCallId}`,
|
|
248
|
+
actor: entry.name,
|
|
249
|
+
source: 'frontend-tool',
|
|
250
|
+
kind: 'tool-call',
|
|
251
|
+
name: askTool,
|
|
252
|
+
summary: balloonLine(request, 160),
|
|
253
|
+
payload: part.input,
|
|
254
|
+
});
|
|
221
255
|
setEntryPersona(prev => ({
|
|
222
256
|
...prev,
|
|
223
257
|
state: 'waiting',
|
|
224
258
|
saying: balloonLine(`${peerApp.name}, could you: ${request}`),
|
|
259
|
+
full: `${peerApp.name}, could you: ${request}`,
|
|
225
260
|
insist: true,
|
|
226
261
|
// Its one tool, in its own words: it asks its peer.
|
|
227
262
|
tool: {
|
|
@@ -234,10 +269,16 @@ export function useA2ATeam(options) {
|
|
|
234
269
|
}));
|
|
235
270
|
}
|
|
236
271
|
else if (part.type === 'tool-result' && part.toolName === askTool) {
|
|
272
|
+
const output = part.output;
|
|
273
|
+
inspector?.end(`tool:${part.toolCallId}`, {
|
|
274
|
+
result: part.output,
|
|
275
|
+
...(output?.error ? { error: String(output.error) } : {}),
|
|
276
|
+
});
|
|
237
277
|
setEntryPersona(prev => ({
|
|
238
278
|
...prev,
|
|
239
279
|
state: 'thinking',
|
|
240
280
|
saying: 'Thanks!',
|
|
281
|
+
full: undefined,
|
|
241
282
|
tool: undefined,
|
|
242
283
|
}));
|
|
243
284
|
// Accounting has handed its answer over: back at rest, its last
|
|
@@ -250,11 +291,18 @@ export function useA2ATeam(options) {
|
|
|
250
291
|
...prev,
|
|
251
292
|
state: 'speaking',
|
|
252
293
|
saying: balloonLine(said),
|
|
294
|
+
full: said,
|
|
253
295
|
insist: true,
|
|
254
296
|
tool: undefined,
|
|
255
297
|
}));
|
|
256
298
|
}
|
|
257
299
|
else if (part.type === 'tool-error' && part.toolName === askTool) {
|
|
300
|
+
inspector?.end(`tool:${part.toolCallId}`, {
|
|
301
|
+
status: 'failed',
|
|
302
|
+
error: part.error instanceof Error
|
|
303
|
+
? part.error.message
|
|
304
|
+
: String(part.error),
|
|
305
|
+
});
|
|
258
306
|
setEntryPersona(prev => ({
|
|
259
307
|
...prev,
|
|
260
308
|
state: 'thinking',
|
|
@@ -271,15 +319,24 @@ export function useA2ATeam(options) {
|
|
|
271
319
|
...history.current,
|
|
272
320
|
{ role: 'assistant', content: said },
|
|
273
321
|
];
|
|
322
|
+
if (inspector) {
|
|
323
|
+
const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);
|
|
324
|
+
inspector.end(turnKey, {
|
|
325
|
+
result: { text: said, ...(usage ? { usage } : {}) },
|
|
326
|
+
summary: `${balloonLine(asked, 60)} → ${balloonLine(said, 80)}${usage?.totalTokens ? ` · ${usage.totalTokens} tokens` : ''}`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
274
329
|
setTurns(prev => [...prev, { role: 'assistant', text: said }]);
|
|
275
330
|
setEntryPersona(prev => ({
|
|
276
331
|
...prev,
|
|
277
332
|
state: 'idle',
|
|
278
333
|
saying: balloonLine(said),
|
|
334
|
+
full: said,
|
|
279
335
|
}));
|
|
280
336
|
}
|
|
281
337
|
catch (error) {
|
|
282
338
|
const message = error instanceof Error ? error.message : String(error);
|
|
339
|
+
inspector?.end(turnKey, { status: 'failed', error: message });
|
|
283
340
|
setTurns(prev => [
|
|
284
341
|
...prev,
|
|
285
342
|
{ role: 'assistant', text: `Something went wrong: ${message}` },
|
|
@@ -288,6 +345,7 @@ export function useA2ATeam(options) {
|
|
|
288
345
|
...prev,
|
|
289
346
|
state: 'idle',
|
|
290
347
|
saying: balloonLine(`Something went wrong: ${message}`),
|
|
348
|
+
full: `Something went wrong: ${message}`,
|
|
291
349
|
insist: true,
|
|
292
350
|
tool: undefined,
|
|
293
351
|
}));
|
|
@@ -303,7 +361,7 @@ export function useA2ATeam(options) {
|
|
|
303
361
|
? prev
|
|
304
362
|
: { ...prev, state: 'idle', tool: undefined });
|
|
305
363
|
}
|
|
306
|
-
}, [agent, busy, askTool, peerApp.name]);
|
|
364
|
+
}, [agent, busy, askTool, peerApp.name, entry, inspector]);
|
|
307
365
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
308
366
|
useEffect(() => () => {
|
|
309
367
|
abort.current?.abort();
|
|
@@ -312,13 +370,19 @@ export function useA2ATeam(options) {
|
|
|
312
370
|
}, []);
|
|
313
371
|
const setEntryAway = useCallback((away) => setEntryPersona(prev => ({ ...prev, away })), []);
|
|
314
372
|
const setPeerAway = useCallback((away) => setPeerPersona(prev => ({ ...prev, away })), []);
|
|
373
|
+
const entryHistory = useMemo(() => turns.map((turn, index) => ({
|
|
374
|
+
id: `turn-${index}`,
|
|
375
|
+
role: turn.role,
|
|
376
|
+
text: turn.text,
|
|
377
|
+
})), [turns]);
|
|
315
378
|
return {
|
|
379
|
+
entryHistory,
|
|
380
|
+
peerHistory,
|
|
316
381
|
entryPersona,
|
|
317
382
|
peerPersona,
|
|
318
383
|
setEntryAway,
|
|
319
384
|
setPeerAway,
|
|
320
385
|
turns,
|
|
321
|
-
exchange,
|
|
322
386
|
report,
|
|
323
387
|
artifacts,
|
|
324
388
|
notebook,
|
|
@@ -19,6 +19,19 @@
|
|
|
19
19
|
* list_invoices…" while Accounting calls Odoo. Then the report appears. The
|
|
20
20
|
* page's own state is `useA2ATeam`'s, which the landing's home page runs too.
|
|
21
21
|
*
|
|
22
|
+
* Under the graph, each member has its own Agent Inspector: Sales' — its
|
|
23
|
+
* model turns, its `ask_accounting` call, the A2A messages it sends and
|
|
24
|
+
* receives — and Accounting's — what it received over A2A, its Odoo MCP
|
|
25
|
+
* calls, `write_notebook`, its status updates and artifacts, as the A2A
|
|
26
|
+
* stream tells them. Both read one record (`useAgentInspector`), fed by the
|
|
27
|
+
* team (`useA2ATeam`'s `inspector`) and by the fetch the A2A client sends
|
|
28
|
+
* with (`inspectA2AFetch`); an A2A message shows in both, sent in one and
|
|
29
|
+
* received in the other. Right-click a member for its menu: *Inspect the
|
|
30
|
+
* agent…* opens its inspector over the page.
|
|
31
|
+
*
|
|
32
|
+
* Sales' suggestions — its Appspec's starters — are chips in its balloon
|
|
33
|
+
* while it waits for a question: one clicked is asked, as if typed.
|
|
34
|
+
*
|
|
22
35
|
* Sales accepts a Jupyter notebook from Accounting besides its words
|
|
23
36
|
* (`acceptedOutputModes`, `NOTEBOOK_AND_WORDS`). When Accounting gives one,
|
|
24
37
|
* Sales' balloon shows it read-only (`NotebookPreview`: no kernel), and it
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Button, Flash, Heading, Text, Textarea } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -10,8 +10,9 @@ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
|
|
|
10
10
|
import { useBrowserInference } from '../hooks/useBrowserInference';
|
|
11
11
|
import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
|
|
12
12
|
import { connectA2APeer } from '../runtimes/browser/a2aPeer';
|
|
13
|
-
import { A2ATeamGraph, NOTEBOOK_AND_WORDS, teamConnectionsOf, useA2ATeam, } from '../components/teams';
|
|
13
|
+
import { A2ATeamGraph, NOTEBOOK_AND_WORDS, teamConnectionsOf, teamToolClassifier, useA2ATeam, } from '../components/teams';
|
|
14
14
|
import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
|
|
15
|
+
import { AgentInspector, classifyToolCall, inspectA2AFetch, useAgentInspector, } from '../components/inspector';
|
|
15
16
|
import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
|
|
16
17
|
const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
|
|
17
18
|
const SALES = SALES_APP_0_0_1;
|
|
@@ -45,6 +46,8 @@ function AgentA2ATeam() {
|
|
|
45
46
|
}, [accountingKey]);
|
|
46
47
|
const [accountingKeyEnded, setAccountingKeyEnded] = useState(() => accountingKeyClock !== undefined &&
|
|
47
48
|
accountingKeyClock.expiresAt <= Date.now());
|
|
49
|
+
// One record of what both members do, each inspector showing its own.
|
|
50
|
+
const { sink: inspector } = useAgentInspector();
|
|
48
51
|
const [peer, setPeer] = useState(null);
|
|
49
52
|
const [peerError, setPeerError] = useState(null);
|
|
50
53
|
const [draft, setDraft] = useState('');
|
|
@@ -55,14 +58,24 @@ function AgentA2ATeam() {
|
|
|
55
58
|
let current = true;
|
|
56
59
|
setPeer(null);
|
|
57
60
|
setPeerError(null);
|
|
58
|
-
connectA2APeer({
|
|
61
|
+
connectA2APeer({
|
|
62
|
+
url: accountingUrl,
|
|
63
|
+
key: accountingKey,
|
|
64
|
+
// The A2A traffic, recorded: Sales sends, Accounting answers.
|
|
65
|
+
fetch: inspectA2AFetch(globalThis.fetch.bind(globalThis), {
|
|
66
|
+
sink: inspector,
|
|
67
|
+
asker: SALES.name,
|
|
68
|
+
peer: ACCOUNTING.name,
|
|
69
|
+
classifyTool: teamToolClassifier(ACCOUNTING_CONNECTIONS, classifyToolCall),
|
|
70
|
+
}),
|
|
71
|
+
})
|
|
59
72
|
.then(found => current && setPeer(found))
|
|
60
73
|
.catch(error => current &&
|
|
61
74
|
setPeerError(error instanceof Error ? error.message : String(error)));
|
|
62
75
|
return () => {
|
|
63
76
|
current = false;
|
|
64
77
|
};
|
|
65
|
-
}, [accountingUrl, accountingKey]);
|
|
78
|
+
}, [accountingUrl, accountingKey, inspector]);
|
|
66
79
|
// Sales runs here and asks Accounting over A2A: the conversation, the
|
|
67
80
|
// exchange, the report and the way the link carries a message.
|
|
68
81
|
const team = useA2ATeam({
|
|
@@ -74,7 +87,13 @@ function AgentA2ATeam() {
|
|
|
74
87
|
peerConnections: ACCOUNTING_CONNECTIONS,
|
|
75
88
|
// Its words, and a notebook to run here when it gives one.
|
|
76
89
|
accept: NOTEBOOK_AND_WORDS,
|
|
90
|
+
inspector,
|
|
77
91
|
});
|
|
92
|
+
// Sales' starters, in its balloon while it waits for a question.
|
|
93
|
+
const suggestions = useMemo(() => (SALES.interface.starters ?? []).map(starter => ({
|
|
94
|
+
label: starter.label,
|
|
95
|
+
prompt: starter.message,
|
|
96
|
+
})), []);
|
|
78
97
|
const send = (text) => {
|
|
79
98
|
setDraft('');
|
|
80
99
|
void team.send(text);
|
|
@@ -82,7 +101,7 @@ function AgentA2ATeam() {
|
|
|
82
101
|
const accountingWhere = accountingKey
|
|
83
102
|
? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
|
|
84
103
|
: `on a runtime, ${new URL(accountingUrl).host}`;
|
|
85
|
-
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [
|
|
104
|
+
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: "Agents A2A Team" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: [TEAM.emoji, " ", _jsx("strong", { children: TEAM.name }), ": ", TEAM.description] }), ((anonymous.status === 'active' && anonymous.expiresAt) ||
|
|
86
105
|
(accountingKeyClock && !accountingKeyEnded)) && (_jsxs(Box, { sx: { display: 'flex', gap: 4, flexWrap: 'wrap', mb: 3 }, children: [anonymous.status === 'active' && anonymous.expiresAt && (_jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${SALES.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() })), accountingKeyClock && !accountingKeyEnded && (_jsx(AnonymousKeyTimer, { expiresAt: accountingKeyClock.expiresAt, grantedMs: accountingKeyClock.grantedMs, label: `${ACCOUNTING.name}'s temporary key`, onExpire: () => setAccountingKeyEnded(true) }))] })), anonymous.status === 'expired' && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyExpired, { agentName: SALES.name,
|
|
87
106
|
// Sales runs in this page: what ended is its model's key, and
|
|
88
107
|
// signing in gives it the person's own.
|
|
@@ -104,9 +123,21 @@ function AgentA2ATeam() {
|
|
|
104
123
|
: 'on a runtime',
|
|
105
124
|
persona: team.entryPersona,
|
|
106
125
|
onToggle: () => composer.current?.focus(),
|
|
126
|
+
conversationLabel: `Ask ${SALES.name}`,
|
|
107
127
|
onAway: team.setEntryAway,
|
|
128
|
+
history: team.entryHistory,
|
|
108
129
|
expandTarget: notebookArea,
|
|
109
130
|
notebookTitle: `${ACCOUNTING.name}\u2019s notebook`,
|
|
131
|
+
suggestions: team.ready && !team.busy ? suggestions : undefined,
|
|
132
|
+
onSuggestion: suggestion => send(suggestion.prompt),
|
|
133
|
+
inspector,
|
|
134
|
+
onStop: team.busy ? team.stop : undefined,
|
|
135
|
+
about: {
|
|
136
|
+
name: SALES.name,
|
|
137
|
+
spec: `${SALES.id}:${SALES.version}`,
|
|
138
|
+
model: SALES.model || undefined,
|
|
139
|
+
where: 'in your browser',
|
|
140
|
+
},
|
|
110
141
|
}, peer: {
|
|
111
142
|
id: ACCOUNTING.id,
|
|
112
143
|
name: ACCOUNTING.name,
|
|
@@ -115,8 +146,25 @@ function AgentA2ATeam() {
|
|
|
115
146
|
where: accountingWhere,
|
|
116
147
|
persona: team.peerPersona,
|
|
117
148
|
onAway: team.setPeerAway,
|
|
149
|
+
history: team.peerHistory,
|
|
118
150
|
connections: ACCOUNTING_CONNECTIONS,
|
|
119
|
-
|
|
151
|
+
inspector,
|
|
152
|
+
about: {
|
|
153
|
+
name: ACCOUNTING.name,
|
|
154
|
+
spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
|
|
155
|
+
model: ACCOUNTING.model || undefined,
|
|
156
|
+
where: accountingWhere,
|
|
157
|
+
},
|
|
158
|
+
}, flow: team.flow, calls: team.calls,
|
|
159
|
+
// Room for the balloons: suggestions, *more* and a notebook.
|
|
160
|
+
balloonRoom: 320, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsx(Box, { sx: {
|
|
161
|
+
display: 'grid',
|
|
162
|
+
gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'],
|
|
163
|
+
gap: 3,
|
|
164
|
+
mb: 3,
|
|
165
|
+
}, "data-team-inspectors": "", children: [SALES, ACCOUNTING].map(member => (_jsxs(Box, { sx: { minWidth: 0 }, "data-team-inspector": member.id, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: [member.emoji, " ", member.name, "\u2019s Agent Inspector"] }), _jsx(AgentInspector, { sink: inspector, agent: member.name, maxHeight: 360, exportName: `agent-inspector-${member.id}`, emptyText: member === SALES
|
|
166
|
+
? 'Its model turns, its call to Accounting and the A2A messages it sends and receives show here.'
|
|
167
|
+
: 'What it receives over A2A, its Odoo MCP calls and what it sends back show here.' })] }, member.id))) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: ["Or choose one of ", SALES.name, "\u2019s suggestions in its balloon."] }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
|
|
120
168
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
121
169
|
event.preventDefault();
|
|
122
170
|
send(draft);
|
|
@@ -126,13 +174,13 @@ function AgentA2ATeam() {
|
|
|
126
174
|
mb: 2,
|
|
127
175
|
borderRadius: 2,
|
|
128
176
|
bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
|
|
129
|
-
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }),
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsx(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: !team.notebook && (_jsxs(_Fragment, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
|
|
178
|
+
p: 3,
|
|
179
|
+
border: '1px solid',
|
|
180
|
+
borderColor: 'border.default',
|
|
181
|
+
borderRadius: 2,
|
|
182
|
+
minHeight: 120,
|
|
183
|
+
}, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it, or as a notebook to run when it gives one." })) })] })) })] })] }));
|
|
136
184
|
}
|
|
137
185
|
export function AgentA2ATeamExample() {
|
|
138
186
|
return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
|
|
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
* and so are the characters clippy.js publishes, fetched when picked.
|
|
16
16
|
*/
|
|
17
17
|
import { useEffect, useRef, useState } from 'react';
|
|
18
|
+
import { useAgentInspector } from '../components/inspector/agentInspector';
|
|
18
19
|
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
19
20
|
import { Button, Heading, Text, ToggleSwitch } from '@primer/react';
|
|
20
21
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -34,6 +35,35 @@ const SERVER = ((typeof window === 'undefined'
|
|
|
34
35
|
? null
|
|
35
36
|
: new URLSearchParams(window.location.search).get('agentRuntimesUrl')) ||
|
|
36
37
|
'http://127.0.0.1:8765').replace(/\/+$/, '');
|
|
38
|
+
/** The agent the assistant talks to on that server. */
|
|
39
|
+
const AGENT_ID = 'assistant';
|
|
40
|
+
/**
|
|
41
|
+
* Make sure the server has the assistant's agent: `npm run examples` starts
|
|
42
|
+
* the server with none, so the page creates it from the
|
|
43
|
+
* `example-agentic-chat` spec the first time, over AG-UI, the spec's own
|
|
44
|
+
* protocol.
|
|
45
|
+
*/
|
|
46
|
+
async function ensureAssistantAgent() {
|
|
47
|
+
const found = await fetch(`${SERVER}/api/v1/agents/${AGENT_ID}`);
|
|
48
|
+
if (found.ok) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (found.status !== 404) {
|
|
52
|
+
throw new Error(`The server could not say whether the ${AGENT_ID} agent exists (${found.status}).`);
|
|
53
|
+
}
|
|
54
|
+
const created = await fetch(`${SERVER}/api/v1/agents`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'content-type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
name: AGENT_ID,
|
|
59
|
+
agent_spec_id: 'example-agentic-chat',
|
|
60
|
+
transport: 'ag-ui',
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
if (!created.ok && created.status !== 409) {
|
|
64
|
+
throw new Error(`The server could not create the ${AGENT_ID} agent (${created.status}).`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
37
67
|
/** The plugins this page enables: Datalayer's characters and the owl's. */
|
|
38
68
|
const reactor = buildReactorFromPlugins([
|
|
39
69
|
AssistantCharactersPlugin,
|
|
@@ -73,8 +103,21 @@ const AssistantExample = () => {
|
|
|
73
103
|
? assistantCharacterNamed(reactor, character)
|
|
74
104
|
: character;
|
|
75
105
|
const [loadError, setLoadError] = useState();
|
|
106
|
+
// The agent on the server, made if it is not there yet.
|
|
107
|
+
const [agentReady, setAgentReady] = useState(false);
|
|
108
|
+
const [agentError, setAgentError] = useState();
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
let cancelled = false;
|
|
111
|
+
ensureAssistantAgent().then(() => !cancelled && setAgentReady(true), error => !cancelled &&
|
|
112
|
+
setAgentError(`${error instanceof Error ? error.message : String(error)} The assistant talks to an agent-runtimes server at ${SERVER}.`));
|
|
113
|
+
return () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
};
|
|
116
|
+
}, []);
|
|
76
117
|
// How the balloon shows the conversation (T-23): history, or current.
|
|
77
118
|
const [balloon, setBalloon] = useState('history');
|
|
119
|
+
// What the agent does, for the menu's *Inspect the agent…*.
|
|
120
|
+
const { sink: inspector } = useAgentInspector();
|
|
78
121
|
// Pixel, the test sprite, read once through the clippy.js reader.
|
|
79
122
|
const [pixel, setPixel] = useState();
|
|
80
123
|
useEffect(() => {
|
|
@@ -156,8 +199,25 @@ const AssistantExample = () => {
|
|
|
156
199
|
}
|
|
157
200
|
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
|
|
158
201
|
character !== pixel &&
|
|
159
|
-
!isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "
|
|
202
|
+
!isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), agentError && (_jsx(Box, { sx: { maxWidth: 720, mx: 'auto', mt: 3 }, children: _jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg' }, children: agentError }) })), agentReady && (_jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "ag-ui", endpoint: `${SERVER}/api/v1/ag-ui/${AGENT_ID}/`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
|
|
160
203
|
// Ask a decision beside the composer: Jev, through this server.
|
|
161
|
-
decisions: { serverUrl: SERVER }
|
|
204
|
+
decisions: { serverUrl: SERVER },
|
|
205
|
+
// Right-click the character: its menu.
|
|
206
|
+
inspector: inspector, onBalloonDisplayChange: setBalloon, onChangeCharacter: () => document
|
|
207
|
+
.querySelector('[data-assistant-character]')
|
|
208
|
+
?.focus(), about: {
|
|
209
|
+
name: 'Assistant',
|
|
210
|
+
spec: AGENT_ID,
|
|
211
|
+
where: `on a runtime, ${new URL(SERVER).host}`,
|
|
212
|
+
}, suggestions: [
|
|
213
|
+
{
|
|
214
|
+
title: 'What is on this page?',
|
|
215
|
+
message: 'What is on this page?',
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
title: 'Tell me a joke',
|
|
219
|
+
message: 'Tell me a short joke about accountants.',
|
|
220
|
+
},
|
|
221
|
+
] }, typeof character === 'string' ? character : character.sprite))] }) }));
|
|
162
222
|
};
|
|
163
223
|
export default AssistantExample;
|
|
@@ -209,7 +209,7 @@ export function appDatalayerCreatePayload(app, instance) {
|
|
|
209
209
|
transport: 'ag-ui',
|
|
210
210
|
agent_spec_id: agentIdOf(app),
|
|
211
211
|
app_spec: dumpAppspec(app),
|
|
212
|
-
//
|
|
212
|
+
// Codemode calls every tool through `execute_code`, which an
|
|
213
213
|
// application without a shell is refused: its tools are called
|
|
214
214
|
// one by one instead, each decided by its rules.
|
|
215
215
|
enable_codemode: Boolean(app.permissions?.computer?.shell),
|
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -531,6 +531,23 @@ export type AssistantCharacterContribution = {
|
|
|
531
531
|
character: AssistantCharacter | AssistantCharacterData;
|
|
532
532
|
};
|
|
533
533
|
export declare const LoopAssistantCharacter: import("@datalayer/reactor").ContributionPoint<AssistantCharacterContribution>;
|
|
534
|
+
/**
|
|
535
|
+
* An entry of the floating assistant's own menu (right-click, Shift+F10, a
|
|
536
|
+
* long press): a plugin's, beside what the assistant offers itself. Its
|
|
537
|
+
* `group` is the heading it is listed under; `checked` makes it one of a
|
|
538
|
+
* choice.
|
|
539
|
+
*/
|
|
540
|
+
export type AssistantMenuContribution = {
|
|
541
|
+
/** Stable id, for the registry and the graph. */
|
|
542
|
+
id: string;
|
|
543
|
+
label: string;
|
|
544
|
+
description?: string;
|
|
545
|
+
onSelect: () => void;
|
|
546
|
+
checked?: boolean;
|
|
547
|
+
disabled?: boolean;
|
|
548
|
+
group?: string;
|
|
549
|
+
};
|
|
550
|
+
export declare const LoopAssistantMenu: import("@datalayer/reactor").ContributionPoint<AssistantMenuContribution>;
|
|
534
551
|
export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
|
|
535
552
|
/**
|
|
536
553
|
* The per-example chat extras a host feeds the loop's conversation live.
|
package/lib/loop/core/index.js
CHANGED
|
@@ -142,6 +142,7 @@ export const LoopChatHeader = defineContributionPoint('loop.chat.header');
|
|
|
142
142
|
export const LoopAgentBlueprint = defineContributionPoint('loop.agent.blueprint');
|
|
143
143
|
export const LoopChatSuggestion = defineContributionPoint('loop.chat.suggestion');
|
|
144
144
|
export const LoopAssistantCharacter = defineContributionPoint('loop.assistant.character');
|
|
145
|
+
export const LoopAssistantMenu = defineContributionPoint('loop.assistant.menu');
|
|
145
146
|
// A block the Canvas may place (LOOP C-12), in a module of its own so that
|
|
146
147
|
// the palette is read without the rest of the contracts.
|
|
147
148
|
export { LoopCanvasBlock } from './canvasBlocks';
|
|
@@ -68,6 +68,6 @@ export declare function assistantStateOfTurn(turn: ChatTurnSnapshot, presence: P
|
|
|
68
68
|
arriving?: boolean;
|
|
69
69
|
leaving?: boolean;
|
|
70
70
|
}): AssistantState;
|
|
71
|
-
export declare function LoopAssistant({ app, person, appId, decisions, balloon:
|
|
71
|
+
export declare function LoopAssistant({ app, person, appId, decisions, balloon: displayGiven, }: LoopAssistantConfig): JSX.Element | null;
|
|
72
72
|
export declare const LoopAssistantPlugin: import("@datalayer/reactor").ReactorPlugin<LoopAssistantConfig, unknown, unknown>;
|
|
73
73
|
export default LoopAssistantPlugin;
|
|
@@ -13,7 +13,7 @@ import { ASSISTANT_WORDS, assistantStateOf, keepAway, keptAway, latestSaying, }
|
|
|
13
13
|
import { useApproveToolRequest, useRejectToolRequest, useToolApprovalsQuery, } from '../../../hooks/useToolApprovals';
|
|
14
14
|
import { approvalsOfApp, ruleOfApproval } from '../app-rules/AppRulesCard';
|
|
15
15
|
import { useViewportDrag } from '../../../chat/useViewportDrag';
|
|
16
|
-
import { LoopAssistantCharacter, LoopChatLayout, LoopChatTurn, LoopSlots, } from '../../core';
|
|
16
|
+
import { LoopAssistantCharacter, LoopAssistantMenu, LoopChatLayout, LoopChatTurn, LoopSlots, } from '../../core';
|
|
17
17
|
import { assistantCharacterFor } from '../assistant-characters';
|
|
18
18
|
export const LOOP_ASSISTANT_PLUGIN_NAME = '@datalayer/loop-plugin-assistant';
|
|
19
19
|
/** The character's size, in pixels, as the chat's own assistant. */
|
|
@@ -69,9 +69,14 @@ function focusComposer() {
|
|
|
69
69
|
const composer = document.querySelector('[data-chat-composer] textarea, [data-chat-composer] [contenteditable="true"]');
|
|
70
70
|
composer?.focus();
|
|
71
71
|
}
|
|
72
|
-
export function LoopAssistant({ app, person, appId, decisions, balloon:
|
|
72
|
+
export function LoopAssistant({ app, person, appId, decisions, balloon: displayGiven = 'history', }) {
|
|
73
|
+
// The balloon's display, as its menu chose it; the Appspec's until then.
|
|
74
|
+
const [chosenDisplay, setChosenDisplay] = useState();
|
|
75
|
+
const display = chosenDisplay ?? displayGiven;
|
|
73
76
|
const contributed = useContributions(LoopAssistantCharacter).map(entry => entry.value);
|
|
74
77
|
const chosen = assistantCharacterFor(contributed, { app, person });
|
|
78
|
+
// What the enabled plugins add to its menu.
|
|
79
|
+
const menuContributions = useContributions(LoopAssistantMenu).map(entry => entry.value);
|
|
75
80
|
const turnEntries = useContributions(LoopChatTurn);
|
|
76
81
|
const turn = useSignalValue(turnEntries[0]?.value.turn ?? NO_TURN);
|
|
77
82
|
const presence = useSignalValue(turnEntries[0]?.value.presence ?? NO_PRESENCE);
|
|
@@ -199,7 +204,7 @@ export function LoopAssistant({ app, person, appId, decisions, balloon: display
|
|
|
199
204
|
};
|
|
200
205
|
return (_jsxs(_Fragment, { children: [appId && signedIn ? (_jsx(WaitingApprovals, { appId: appId, onWaiting: setApproval })) : null, _jsx(AssistantStage, { character: chosen.character, state: state, size: SIZE, place: drag.position
|
|
201
206
|
? { left: `${drag.position.left}px`, top: `${drag.position.top}px` }
|
|
202
|
-
: { right: '24px', bottom: '24px' }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: open, onToggle: onToggle, balloon: balloon, balloonDisplay: display, insist: insist, onDismiss: onDismiss, decide: decide })] }));
|
|
207
|
+
: { right: '24px', bottom: '24px' }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: open, onToggle: onToggle, balloon: balloon, balloonDisplay: display, insist: insist, onDismiss: onDismiss, decide: decide, onResetPosition: drag.position ? drag.reset : undefined, onBalloonDisplayChange: setChosenDisplay, about: appId ? { name: appId, spec: appId } : undefined, contextMenu: menuContributions })] }));
|
|
203
208
|
}
|
|
204
209
|
export const LoopAssistantPlugin = definePlugin({
|
|
205
210
|
name: LOOP_ASSISTANT_PLUGIN_NAME,
|