@datalayer/agent-runtimes 1.3.71 → 1.3.73

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 (44) hide show
  1. package/lib/chat/ChatFloating.d.ts +16 -1
  2. package/lib/chat/ChatFloating.js +62 -17
  3. package/lib/chat/assistant/AssistantStage.d.ts +17 -2
  4. package/lib/chat/assistant/AssistantStage.js +3 -3
  5. package/lib/chat/assistant/BalloonParts.d.ts +55 -0
  6. package/lib/chat/assistant/BalloonParts.js +140 -0
  7. package/lib/chat/assistant/ConversationBalloon.d.ts +5 -0
  8. package/lib/chat/assistant/ConversationBalloon.js +5 -0
  9. package/lib/chat/assistant/SpeechBalloon.d.ts +23 -2
  10. package/lib/chat/assistant/SpeechBalloon.js +22 -8
  11. package/lib/chat/assistant/toolLine.d.ts +69 -0
  12. package/lib/chat/assistant/toolLine.js +118 -0
  13. package/lib/components/teams/A2ATeamGraph.d.ts +12 -0
  14. package/lib/components/teams/A2ATeamGraph.js +35 -8
  15. package/lib/components/teams/NotebookPreview.d.ts +39 -0
  16. package/lib/components/teams/NotebookPreview.js +68 -0
  17. package/lib/components/teams/NotebookPreviewView.d.ts +21 -0
  18. package/lib/components/teams/NotebookPreviewView.js +42 -0
  19. package/lib/components/teams/TeamNotebook.js +16 -1
  20. package/lib/components/teams/a2aTeamFlow.d.ts +5 -0
  21. package/lib/components/teams/a2aTeamFlow.js +11 -0
  22. package/lib/components/teams/index.d.ts +1 -0
  23. package/lib/components/teams/index.js +1 -0
  24. package/lib/components/teams/useA2ATeam.d.ts +8 -0
  25. package/lib/components/teams/useA2ATeam.js +38 -3
  26. package/lib/examples/AgentA2ATeamExample.d.ts +8 -4
  27. package/lib/examples/AgentA2ATeamExample.js +2 -2
  28. package/lib/examples/ChatAssistantExample.js +4 -2
  29. package/lib/examples/ChatAssistantGalleryExample.d.ts +5 -1
  30. package/lib/examples/ChatAssistantGalleryExample.js +40 -10
  31. package/lib/examples/utils/AssistantGalleryGrid.d.ts +14 -11
  32. package/lib/examples/utils/AssistantGalleryGrid.js +42 -1
  33. package/lib/examples/utils/assistantGallery.d.ts +49 -1
  34. package/lib/examples/utils/assistantGallery.js +52 -1
  35. package/lib/loop/apps/appspec.js +6 -0
  36. package/lib/loop/apps/checks.js +2 -0
  37. package/lib/loop/embed/AppEmbed.js +3 -1
  38. package/lib/loop/plugins/assistant/index.d.ts +14 -1
  39. package/lib/loop/plugins/assistant/index.js +39 -13
  40. package/lib/specs/appspecSchema.js +18 -0
  41. package/lib/stories/loop/ReferenceScreens.d.ts +1 -1
  42. package/lib/stories/loop/ReferenceScreens.js +20 -3
  43. package/lib/types/agentspecs.d.ts +8 -0
  44. package/package.json +1 -1
@@ -28,7 +28,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
28
28
  import { stepCountIs, ToolLoopAgent } from 'ai';
29
29
  import { createBrowserModel, } from '../../runtimes/browser/model';
30
30
  import { a2aPeerTool, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
31
- import { callsAfter, flowAfter, pruneCalls, } from './a2aTeamFlow';
31
+ import { callsAfter, flowAfter, toolOwnName, pruneCalls, } from './a2aTeamFlow';
32
+ import { toolLineOfStep, } from '../../chat/assistant/toolLine';
32
33
  const NO_CONNECTIONS = [];
33
34
  export const AT_REST = {
34
35
  state: 'idle',
@@ -116,6 +117,7 @@ export function useA2ATeam(options) {
116
117
  state: 'greeting',
117
118
  saying: 'On it. Let me read the books.',
118
119
  insist: true,
120
+ tool: undefined,
119
121
  }));
120
122
  }
121
123
  else if (event.phase === 'working') {
@@ -126,7 +128,12 @@ export function useA2ATeam(options) {
126
128
  ...prev,
127
129
  state: 'working',
128
130
  saying: event.note ? balloonLine(event.note) : prev.saying,
129
- insist: Boolean(event.note) || prev.insist,
131
+ insist: Boolean(event.note) || Boolean(event.tool) || prev.insist,
132
+ // The tool it calls, in plain words, from the step it told; a
133
+ // step without a tool (its own words) puts the tool line away.
134
+ tool: event.tool
135
+ ? toolLineOfStep(event.tool, toolOwnName(event.tool.name, peerConnections))
136
+ : undefined,
130
137
  }));
131
138
  }
132
139
  else if (event.phase === 'answered') {
@@ -145,7 +152,12 @@ export function useA2ATeam(options) {
145
152
  state: 'speaking',
146
153
  saying: balloonLine(event.answer),
147
154
  insist: true,
155
+ tool: undefined,
148
156
  }));
157
+ // The entry was given a notebook: in its balloon, read-only.
158
+ if (given) {
159
+ setEntryPersona(prev => ({ ...prev, notebook: given }));
160
+ }
149
161
  }
150
162
  else {
151
163
  setExchange(prev => [
@@ -157,6 +169,7 @@ export function useA2ATeam(options) {
157
169
  state: 'idle',
158
170
  saying: balloonLine(event.error),
159
171
  insist: true,
172
+ tool: undefined,
160
173
  }));
161
174
  }
162
175
  }, [entry.name, peerApp.id, peerApp.name, peerConnections]);
@@ -210,6 +223,14 @@ export function useA2ATeam(options) {
210
223
  state: 'waiting',
211
224
  saying: balloonLine(`${peerApp.name}, could you: ${request}`),
212
225
  insist: true,
226
+ // Its one tool, in its own words: it asks its peer.
227
+ tool: {
228
+ id: part.toolCallId,
229
+ tool: askTool,
230
+ name: askTool,
231
+ phase: 'running',
232
+ words: `Asking ${peerApp.name}…`,
233
+ },
213
234
  }));
214
235
  }
215
236
  else if (part.type === 'tool-result' && part.toolName === askTool) {
@@ -217,6 +238,7 @@ export function useA2ATeam(options) {
217
238
  ...prev,
218
239
  state: 'thinking',
219
240
  saying: 'Thanks!',
241
+ tool: undefined,
220
242
  }));
221
243
  // Accounting has handed its answer over: back at rest, its last
222
244
  // words still in its balloon.
@@ -229,6 +251,16 @@ export function useA2ATeam(options) {
229
251
  state: 'speaking',
230
252
  saying: balloonLine(said),
231
253
  insist: true,
254
+ tool: undefined,
255
+ }));
256
+ }
257
+ else if (part.type === 'tool-error' && part.toolName === askTool) {
258
+ setEntryPersona(prev => ({
259
+ ...prev,
260
+ state: 'thinking',
261
+ tool: prev.tool
262
+ ? { ...prev.tool, phase: 'failed', words: undefined }
263
+ : undefined,
232
264
  }));
233
265
  }
234
266
  else if (part.type === 'error') {
@@ -257,6 +289,7 @@ export function useA2ATeam(options) {
257
289
  state: 'idle',
258
290
  saying: balloonLine(`Something went wrong: ${message}`),
259
291
  insist: true,
292
+ tool: undefined,
260
293
  }));
261
294
  setFlow('still');
262
295
  callsNow.current = [];
@@ -266,7 +299,9 @@ export function useA2ATeam(options) {
266
299
  abort.current = null;
267
300
  setBusy(false);
268
301
  // However the turn ended, neither member is still at work.
269
- setPeerPersona(prev => prev.state === 'idle' ? prev : { ...prev, state: 'idle' });
302
+ setPeerPersona(prev => prev.state === 'idle' && !prev.tool
303
+ ? prev
304
+ : { ...prev, state: 'idle', tool: undefined });
270
305
  }
271
306
  }, [agent, busy, askTool, peerApp.name]);
272
307
  const stop = useCallback(() => abort.current?.abort(), []);
@@ -14,14 +14,18 @@
14
14
  * — the edge flows back — each in its own state and balloon. Under
15
15
  * Accounting hangs Odoo, its connection (the odoo-accounting MCP server, from
16
16
  * its Appspec): the edge to it flows while Accounting calls one of its tools,
17
- * the tool's name on it. Then the report appears. The page's own state is `useA2ATeam`'s, which the landing's home
18
- * page runs too.
17
+ * the tool's name on it. Each balloon shows only what its member says or
18
+ * does now (`current`): "Asking Accounting…" for Sales' one tool, "Using
19
+ * list_invoices…" while Accounting calls Odoo. Then the report appears. The
20
+ * page's own state is `useA2ATeam`'s, which the landing's home page runs too.
19
21
  *
20
22
  * Sales accepts a Jupyter notebook from Accounting besides its words
21
23
  * (`acceptedOutputModes`, `NOTEBOOK_AND_WORDS`). When Accounting gives one,
22
- * it opens under the conversation in place of the report, on the browser
24
+ * Sales' balloon shows it read-only (`NotebookPreview`: no kernel), and it
25
+ * opens right under the team, in place of the report, on the browser
23
26
  * sandbox — a Pyodide kernel in this page — to run, change and download
24
- * (`TeamNotebook`, loaded only then).
27
+ * (`TeamNotebook`, loaded only then); a click on the one in the balloon
28
+ * comes to it.
25
29
  *
26
30
  * Where Accounting is: `VITE_A2A_ACCOUNTING_URL`, else the examples' local
27
31
  * server (`…:8765/api/v1/a2a/agents/accounting`), which answers this machine
@@ -112,7 +112,7 @@ function AgentA2ATeam() {
112
112
  persona: team.peerPersona,
113
113
  onAway: team.setPeerAway,
114
114
  connections: ACCOUNTING_CONNECTIONS,
115
- }, flow: team.flow, calls: team.calls, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _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] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !team.ready || team.busy, onClick: () => send(starter.message), children: starter.label }, starter.label))) }), _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 => {
115
+ }, flow: team.flow, calls: team.calls, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), team.notebook && (_jsx(Box, { sx: { mb: 3 }, "data-team-notebook-placement": "under-graph", children: _jsx(TeamNotebook, { notebook: team.notebook, title: `${ACCOUNTING.name}\u2019s notebook` }) })), _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] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !team.ready || team.busy, onClick: () => send(starter.message), children: starter.label }, starter.label))) }), _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 => {
116
116
  if (event.key === 'Enter' && !event.shiftKey) {
117
117
  event.preventDefault();
118
118
  send(draft);
@@ -128,7 +128,7 @@ function AgentA2ATeam() {
128
128
  borderColor: 'border.default',
129
129
  borderRadius: 2,
130
130
  minHeight: 120,
131
- }, "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." })) })] }))] })] }), team.notebook && (_jsx(Box, { sx: { mt: 3 }, children: _jsx(TeamNotebook, { notebook: team.notebook, title: `${ACCOUNTING.name}\u2019s notebook` }) }))] }));
131
+ }, "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." })) })] }))] })] })] }));
132
132
  }
133
133
  export function AgentA2ATeamExample() {
134
134
  return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
@@ -73,6 +73,8 @@ const ChatAssistantExample = () => {
73
73
  ? assistantCharacterNamed(reactor, character)
74
74
  : character;
75
75
  const [loadError, setLoadError] = useState();
76
+ // How the balloon shows the conversation (T-23): history, or current.
77
+ const [balloon, setBalloon] = useState('history');
76
78
  // Pixel, the test sprite, read once through the clippy.js reader.
77
79
  const [pixel, setPixel] = useState();
78
80
  useEffect(() => {
@@ -135,7 +137,7 @@ const ChatAssistantExample = () => {
135
137
  }, "aria-pressed": option.id === character, "data-assistant-character": option.id, children: option.name }, option.id))), pixel && (_jsxs(Button, { variant: character === pixel ? 'primary' : 'default', onClick: () => {
136
138
  setCharacter(pixel);
137
139
  setLoadError(undefined);
138
- }, "aria-pressed": character === pixel, "data-assistant-character": "sprite", children: [pixel.name, " (sprite)"] }))] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The clippy.js characters" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["Fetched when picked from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr and read as data by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CLIPPY_JS_CHARACTERS.map(name => {
140
+ }, "aria-pressed": character === pixel, "data-assistant-character": "sprite", children: [pixel.name, " (sprite)"] }))] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The balloon" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: [_jsx("strong", { children: "History" }), ": open, the whole conversation, counted in its header, the composer last. ", _jsx("strong", { children: "Current" }), ": only what it says or does now \u2014 the answer as it is written, or the tool it calls (\u201CUsing current_time\u2026\u201D) \u2014 in one compact balloon. Either way, a tool call is said in the balloon."] }), _jsx(Box, { sx: { display: 'flex', gap: 2 }, children: ['history', 'current'].map(option => (_jsx(Button, { variant: option === balloon ? 'primary' : 'default', "aria-pressed": option === balloon, "data-assistant-balloon-choice": option, onClick: () => setBalloon(option), children: option === 'history' ? 'History' : 'Current' }, option))) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The clippy.js characters" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["Fetched when picked from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr and read as data by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CLIPPY_JS_CHARACTERS.map(name => {
139
141
  const picked = clippyJs[name] !== undefined && character === clippyJs[name];
140
142
  return (_jsx(Button, { variant: picked ? 'primary' : 'default', onClick: () => void pickClippyJs(name), disabled: fetching !== undefined, "aria-pressed": picked, "data-assistant-character": `clippy-js-${name.toLowerCase()}`, children: fetching === name ? `${name}…` : name }, name));
141
143
  }) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
@@ -152,7 +154,7 @@ const ChatAssistantExample = () => {
152
154
  }
153
155
  } }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
154
156
  character !== pixel &&
155
- !isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
157
+ !isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
156
158
  // Ask a decision beside the composer: Jev, through this server.
157
159
  decisions: { serverUrl: SERVER } }, typeof character === 'string' ? character : character.sprite)] }) }));
158
160
  };
@@ -4,7 +4,11 @@
4
4
  * contributes to `loop.assistant.character`, a test sprite read through
5
5
  * the clippy.js reader, and, when asked, the characters clippy.js publishes,
6
6
  * fetched from jsDelivr — in each state it acts, stepped aside, with its
7
- * balloon, in light and dark, moving or still.
7
+ * balloon, in light and dark, moving or still. The balloon shows the
8
+ * conversation either way (T-23): a peek of it (`history`), or only what is
9
+ * said or done now (`current`); a tool call is said in plain words ("Using
10
+ * list_invoices…", "Done: list_invoices", "list_invoices failed"), and a
11
+ * recorded notebook shows read-only in the current balloon.
8
12
  *
9
13
  * Static data, no agent: it opens without a server or an account.
10
14
  */
@@ -9,7 +9,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
9
9
  * contributes to `loop.assistant.character`, a test sprite read through
10
10
  * the clippy.js reader, and, when asked, the characters clippy.js publishes,
11
11
  * fetched from jsDelivr — in each state it acts, stepped aside, with its
12
- * balloon, in light and dark, moving or still.
12
+ * balloon, in light and dark, moving or still. The balloon shows the
13
+ * conversation either way (T-23): a peek of it (`history`), or only what is
14
+ * said or done now (`current`); a tool call is said in plain words ("Using
15
+ * list_invoices…", "Done: list_invoices", "list_invoices failed"), and a
16
+ * recorded notebook shows read-only in the current balloon.
13
17
  *
14
18
  * Static data, no agent: it opens without a server or an account.
15
19
  */
@@ -20,11 +24,11 @@ import { ThemedProvider } from './utils/themedProvider';
20
24
  import { useExampleThemeStore } from './utils/themeStore';
21
25
  import { AssistantStage } from '../chat/assistant/AssistantStage';
22
26
  import { useViewportDrag } from '../chat/useViewportDrag';
23
- import { AssistantGalleryGrid, GalleryObstacle, Still, useGalleryCharacters, } from './utils/AssistantGalleryGrid';
24
- import { GALLERY_POSES, GALLERY_POSE_LABELS, balloonForPose, sampleApproval, sampleSaying, stateOfPose, } from './utils/assistantGallery';
27
+ import { AssistantGalleryGrid, GalleryBalloons, GalleryNotebook, GalleryObstacle, Still, useGalleryCharacters, } from './utils/AssistantGalleryGrid';
28
+ import { GALLERY_POSES, GALLERY_POSE_LABELS, SAMPLE_NOTEBOOK_SAYING, balloonForPose, sampleApproval, sampleSaying, sampleToolLine, stateOfPose, } from './utils/assistantGallery';
25
29
  import { CLIPPY_JS_VERSION, useClippyJsCharacters, } from './utils/clippyJsCharacters';
26
30
  const STAGE_SIZE = 96;
27
- const PANEL_HEIGHT = 420;
31
+ const PANEL_HEIGHT = 520;
28
32
  const LEAVE_MS = 700;
29
33
  const ARRIVE_MS = 1900;
30
34
  const AWAY_WORDS = {
@@ -34,7 +38,7 @@ const AWAY_WORDS = {
34
38
  always: 'for good',
35
39
  };
36
40
  /** One colour mode's panel, with the character on its own stage. */
37
- function ModePanel({ mode, character, pose, presence, balloon, insist, onToggle, onDismiss, onCallBack, away, replay, }) {
41
+ function ModePanel({ mode, character, pose, presence, balloon, insist, onToggle, onDismiss, onCallBack, away, replay, display, }) {
38
42
  const panelRef = useRef(null);
39
43
  const stageRef = useRef(null);
40
44
  const drag = useViewportDrag(stageRef, { whole: true });
@@ -80,7 +84,7 @@ function ModePanel({ mode, character, pose, presence, balloon, insist, onToggle,
80
84
  position: 'absolute',
81
85
  left: spot.left,
82
86
  top: spot.top + STAGE_SIZE / 2 - 16,
83
- }, children: [_jsxs(Button, { size: "small", onClick: onCallBack, "data-gallery-call-back": "", children: ["Call ", character.name, " back"] }), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1 }, children: ["Sent away ", AWAY_WORDS[away], "."] })] })) : (_jsx(AssistantStage, { character: character.character, state: state, size: STAGE_SIZE, place: { position: 'absolute', left: spot.left, top: spot.top }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: false, onToggle: onToggle, balloon: presence === 'here' ? balloon : undefined, insist: insist, onDismiss: onDismiss }, `${character.id}-${replay}`)), pose === 'aside' && presence === 'here' && (_jsx(GalleryObstacle, { sx: {
87
+ }, children: [_jsxs(Button, { size: "small", onClick: onCallBack, "data-gallery-call-back": "", children: ["Call ", character.name, " back"] }), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1 }, children: ["Sent away ", AWAY_WORDS[away], "."] })] })) : (_jsx(AssistantStage, { character: character.character, state: state, size: STAGE_SIZE, place: { position: 'absolute', left: spot.left, top: spot.top }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: false, onToggle: onToggle, balloon: presence === 'here' ? balloon : undefined, balloonDisplay: display, insist: insist, onDismiss: onDismiss }, `${character.id}-${replay}`)), pose === 'aside' && presence === 'here' && (_jsx(GalleryObstacle, { sx: {
84
88
  left: spot.left - 12,
85
89
  top: spot.top - 12,
86
90
  width: STAGE_SIZE + 24,
@@ -115,6 +119,10 @@ const ChatAssistantGalleryExample = () => {
115
119
  const [presence, setPresence] = useState('here');
116
120
  const [away, setAway] = useState('none');
117
121
  const [replay, setReplay] = useState(0);
122
+ // How the balloon shows the conversation, a tool being called, a notebook (T-23).
123
+ const [display, setDisplay] = useState('current');
124
+ const [toolPhase, setToolPhase] = useState();
125
+ const [withNotebook, setWithNotebook] = useState(false);
118
126
  const character = characters.find(entry => entry.id === characterId) ?? characters[0];
119
127
  // Leaving takes the goodbye's length; arriving, the greeting's.
120
128
  useEffect(() => {
@@ -138,10 +146,20 @@ const ChatAssistantGalleryExample = () => {
138
146
  };
139
147
  const approval = sampleApproval(() => setDecision('Approved: send_reply.'), () => setDecision('Denied: send_reply.'));
140
148
  const balloon = balloonForPose(pose, {
141
- saying: saying === undefined ? undefined : sampleSaying(saying),
149
+ saying: saying === undefined
150
+ ? withNotebook
151
+ ? { text: SAMPLE_NOTEBOOK_SAYING, more: false }
152
+ : undefined
153
+ : sampleSaying(saying),
142
154
  approval,
155
+ tool: toolPhase ? sampleToolLine(toolPhase) : undefined,
156
+ attachment: withNotebook && display === 'current' ? (_jsx(GalleryNotebook, { onOpen: () => setDecision('The notebook would open below to run.') })) : undefined,
143
157
  });
144
- const insist = saying !== undefined || pose === 'waiting' || pose === 'paused';
158
+ const insist = saying !== undefined ||
159
+ toolPhase !== undefined ||
160
+ withNotebook ||
161
+ pose === 'waiting' ||
162
+ pose === 'paused';
145
163
  return (_jsx(ThemedProvider, { children: _jsx(Box, { sx: {
146
164
  minHeight: '100vh',
147
165
  bg: 'canvas.default',
@@ -174,13 +192,25 @@ const ChatAssistantGalleryExample = () => {
174
192
  gap: 2,
175
193
  flexWrap: 'wrap',
176
194
  alignItems: 'center',
177
- }, children: [_jsx(Button, { onClick: say, "data-gallery-say": "", children: "Say something" }), saying !== undefined && (_jsx(Button, { variant: "invisible", onClick: () => setSaying(undefined), children: "Hush" })), presence === 'away' ? (_jsx(Button, { onClick: callBack, children: "Call it back" })) : (_jsx(Button, { onClick: () => sendAway('page'), disabled: presence !== 'here', "data-gallery-send-away": "", children: "Send it away" })), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Drag the character to move it; hover it for its own send-away menu." }), decision && (_jsx(Text, { role: "status", sx: { fontSize: 1 }, children: decision }))] })] }))] }), problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["The test sprite could not be read: ", problem] })), clippyJs.problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["A clippy.js character could not be read: ", clippyJs.problem] })), _jsx(Still, { still: still, children: view === 'stage' ? (_jsx(InModes, { modes: modes, children: mode => (_jsx(ModePanel, { mode: mode, character: character, pose: pose, presence: presence, balloon: balloon, insist: insist, onToggle: () => saying === undefined ? say() : setSaying(undefined), onDismiss: sendAway, onCallBack: callBack, away: away, replay: replay })) })) : (_jsx(InModes, { modes: modes, stacked: true, children: mode => (_jsx(Box, { "data-gallery-mode": mode, sx: {
195
+ }, children: [_jsx(SegmentedControl, { "aria-label": "Balloon", children: ['history', 'current'].map(option => (_jsx(SegmentedControl.Button, { selected: display === option, onClick: () => setDisplay(option), "data-gallery-balloon": option, children: option === 'history' ? 'History' : 'Current' }, option))) }), _jsx(Button, { onClick: say, "data-gallery-say": "", children: "Say something" }), _jsx(Button, { "data-gallery-tool": toolPhase ?? 'none', onClick: () => setToolPhase(toolPhase === undefined
196
+ ? 'running'
197
+ : toolPhase === 'running'
198
+ ? 'done'
199
+ : toolPhase === 'done'
200
+ ? 'failed'
201
+ : undefined), children: toolPhase === undefined
202
+ ? 'Use a tool'
203
+ : toolPhase === 'running'
204
+ ? 'Tool done'
205
+ : toolPhase === 'done'
206
+ ? 'Tool failed'
207
+ : 'No tool' }), _jsx(Button, { "data-gallery-notebook": withNotebook ? 'on' : 'off', "aria-pressed": withNotebook, onClick: () => setWithNotebook(!withNotebook), children: withNotebook ? 'Take the notebook' : 'Give a notebook' }), saying !== undefined && (_jsx(Button, { variant: "invisible", onClick: () => setSaying(undefined), children: "Hush" })), presence === 'away' ? (_jsx(Button, { onClick: callBack, children: "Call it back" })) : (_jsx(Button, { onClick: () => sendAway('page'), disabled: presence !== 'here', "data-gallery-send-away": "", children: "Send it away" })), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Drag the character to move it; hover it for its own send-away menu." }), decision && (_jsx(Text, { role: "status", sx: { fontSize: 1 }, children: decision }))] })] }))] }), problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["The test sprite could not be read: ", problem] })), clippyJs.problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["A clippy.js character could not be read: ", clippyJs.problem] })), _jsx(Still, { still: still, children: view === 'stage' ? (_jsx(InModes, { modes: modes, children: mode => (_jsx(ModePanel, { mode: mode, character: character, pose: pose, presence: presence, balloon: balloon, insist: insist, onToggle: () => saying === undefined ? say() : setSaying(undefined), onDismiss: sendAway, onCallBack: callBack, away: away, replay: replay, display: display })) })) : (_jsx(InModes, { modes: modes, stacked: true, children: mode => (_jsxs(Box, { "data-gallery-mode": mode, sx: {
178
208
  bg: 'canvas.default',
179
209
  border: '1px solid',
180
210
  borderColor: 'border.default',
181
211
  borderRadius: 2,
182
212
  p: 2,
183
213
  overflowX: 'auto',
184
- }, children: _jsx(AssistantGalleryGrid, { characters: characters }, replay) })) })) })] }) }) }));
214
+ }, children: [_jsx(AssistantGalleryGrid, { characters: characters }, replay), _jsx(GalleryBalloons, { character: character })] })) })) })] }) }) }));
185
215
  };
186
216
  export default ChatAssistantGalleryExample;
@@ -1,14 +1,3 @@
1
- /**
2
- * The floating assistant's characters, every one in every pose, small, for
3
- * a quick visual check (LOOP T-16, T-22, T-24 to T-27): Datalayer's four and
4
- * the owl as the enabled plugins contribute them to
5
- * `loop.assistant.character`, and a test sprite read through the clippy.js
6
- * reader. Each cell is the real `AssistantStage`, set in the grid's flow
7
- * rather than fixed to the window; stepped aside is the real `useKeepClear`
8
- * stepping it aside from a composer laid over the cell.
9
- *
10
- * @module examples/utils/AssistantGalleryGrid
11
- */
12
1
  import type { JSX, ReactNode } from 'react';
13
2
  import type { AssistantCharacter } from '../../chat/assistant/characters';
14
3
  import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
@@ -50,4 +39,18 @@ export declare function AssistantGalleryGrid({ characters, size, }: {
50
39
  characters: readonly GalleryCharacter[];
51
40
  size?: number;
52
41
  }): JSX.Element;
42
+ /** How tall the notebook in a balloon is, in the stage and in the grid. */
43
+ export declare const GALLERY_NOTEBOOK_HEIGHT = 160;
44
+ /** The notebook a balloon holds in the gallery: jupyter-react's, read-only. */
45
+ export declare function GalleryNotebook({ onOpen, }: {
46
+ onOpen?: () => void;
47
+ }): JSX.Element;
48
+ /**
49
+ * The balloon's displays (T-23), one character: a peek of the history, the
50
+ * current balloon with a tool line, and with a notebook given — the same
51
+ * read-only jupyter-react notebook as in the stage, loaded when drawn.
52
+ */
53
+ export declare function GalleryBalloons({ character, }: {
54
+ character: GalleryCharacter;
55
+ }): JSX.Element;
53
56
  export default AssistantGalleryGrid;
@@ -6,8 +6,9 @@ import { buildReactorFromPlugins } from '@datalayer/reactor';
6
6
  import { AssistantStage } from '../../chat/assistant/AssistantStage';
7
7
  import { AssistantCharactersPlugin, assistantCharactersOf, } from '../../loop/plugins/assistant-characters';
8
8
  import { OWL_CHARACTER, OwlCharacterPlugin } from './owlCharacterPlugin';
9
+ import { NotebookPreview } from '../../components/teams/NotebookPreview';
9
10
  import { readTestSpriteCharacter } from './testSpriteCharacter';
10
- import { GALLERY_POSES, GALLERY_POSE_LABELS, stateOfPose, } from './assistantGallery';
11
+ import { GALLERY_POSES, GALLERY_POSE_LABELS, SAMPLE_NOTEBOOK, SAMPLE_NOTEBOOK_SAYING, sampleSaying, sampleToolLine, stateOfPose, } from './assistantGallery';
11
12
  /** The plugins the gallery enables: Datalayer's characters and the owl's. */
12
13
  const reactor = buildReactorFromPlugins([
13
14
  AssistantCharactersPlugin,
@@ -120,4 +121,44 @@ export function AssistantGalleryGrid({ characters, size = 48, }) {
120
121
  '& td': { p: 0, textAlign: 'center' },
121
122
  }, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", {}), GALLERY_POSES.map(pose => (_jsx("th", { scope: "col", children: GALLERY_POSE_LABELS[pose] }, pose)))] }) }), _jsx("tbody", { children: characters.map(character => (_jsxs("tr", { children: [_jsxs(Box, { as: "th", scope: "row", sx: { textAlign: 'left !important', pr: '12px !important' }, children: [_jsx(Text, { sx: { display: 'block', color: 'fg.default' }, children: character.name }), _jsx(Text, { sx: { fontWeight: 400 }, children: character.from })] }), GALLERY_POSES.map(pose => (_jsx("td", { children: _jsx(GalleryCell, { character: character, pose: pose, size: size }) }, pose)))] }, character.id))) })] }));
122
123
  }
124
+ /** How tall the notebook in a balloon is, in the stage and in the grid. */
125
+ export const GALLERY_NOTEBOOK_HEIGHT = 160;
126
+ /** The notebook a balloon holds in the gallery: jupyter-react's, read-only. */
127
+ export function GalleryNotebook({ onOpen, }) {
128
+ return (_jsx(NotebookPreview, { notebook: SAMPLE_NOTEBOOK, title: "Accounting\u2019s notebook", maxHeight: GALLERY_NOTEBOOK_HEIGHT, onOpen: onOpen }));
129
+ }
130
+ /** One balloon display, pictured: a character with what its balloon says. */
131
+ function BalloonCell({ character, label, display, balloon, height, }) {
132
+ const stageRef = useRef(null);
133
+ return (_jsxs(Box, { "data-gallery-cell": `balloon-${label.toLowerCase().replace(/\W+/g, '-')}`, sx: {
134
+ position: 'relative',
135
+ width: 330,
136
+ height,
137
+ border: '1px solid',
138
+ borderColor: 'border.muted',
139
+ borderRadius: 2,
140
+ }, children: [_jsx(Text, { sx: {
141
+ position: 'absolute',
142
+ top: 1,
143
+ right: 2,
144
+ fontSize: 0,
145
+ fontWeight: 600,
146
+ color: 'fg.muted',
147
+ }, children: label }), _jsx(AssistantStage, { character: character.character, state: balloon.tool ? 'working' : 'speaking', size: 48, place: { position: 'absolute', left: '12px', bottom: '8px' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: () => undefined, onDismiss: () => undefined, balloon: balloon, balloonDisplay: display, insist: true })] }));
148
+ }
149
+ /**
150
+ * The balloon's displays (T-23), one character: a peek of the history, the
151
+ * current balloon with a tool line, and with a notebook given — the same
152
+ * read-only jupyter-react notebook as in the stage, loaded when drawn.
153
+ */
154
+ export function GalleryBalloons({ character, }) {
155
+ return (_jsxs(Box, { "data-gallery-balloons": "", sx: { display: 'flex', gap: 3, flexWrap: 'wrap', mt: 3 }, children: [_jsx(BalloonCell, { character: character, label: "History", display: "history", height: 180, balloon: { ...sampleSaying(0), onDismiss: () => undefined } }), _jsx(BalloonCell, { character: character, label: "Current \u00B7 tool", display: "current", height: 180, balloon: {
156
+ text: 'Using list_invoices…',
157
+ tool: sampleToolLine('running'),
158
+ busy: true,
159
+ } }), _jsx(BalloonCell, { character: character, label: "Current \u00B7 notebook", display: "current", height: GALLERY_NOTEBOOK_HEIGHT + 210, balloon: {
160
+ text: SAMPLE_NOTEBOOK_SAYING,
161
+ attachment: _jsx(GalleryNotebook, {}),
162
+ } })] }));
163
+ }
123
164
  export default AssistantGalleryGrid;
@@ -7,6 +7,7 @@
7
7
  * @module examples/utils/assistantGallery
8
8
  */
9
9
  import type { AssistantStageProps } from '../../chat/assistant/AssistantStage';
10
+ import { type BalloonToolLine, type ToolLinePhase } from '../../chat/assistant/toolLine';
10
11
  import { type AssistantState, type BalloonApproval } from '../../chat/assistant/state';
11
12
  /** What the gallery poses a character in: a state, or stepped aside. */
12
13
  export type GalleryPose = AssistantState | 'aside';
@@ -27,16 +28,63 @@ export declare function sampleSaying(index: number): {
27
28
  text: string;
28
29
  more: boolean;
29
30
  };
31
+ /** The tool the gallery calls (T-23): Odoo's, as Accounting calls it. */
32
+ export declare const SAMPLE_TOOL = "list_invoices";
33
+ /** The sample tool's line, at a phase: "Using list_invoices…", "Done: …", "… failed". */
34
+ export declare function sampleToolLine(phase: ToolLinePhase): BalloonToolLine;
35
+ /**
36
+ * A small recorded notebook (nbformat 4), as Accounting gives one: a
37
+ * heading, the code that totals the open invoices, and its one output.
38
+ */
39
+ export declare const SAMPLE_NOTEBOOK: {
40
+ nbformat: number;
41
+ nbformat_minor: number;
42
+ metadata: {
43
+ kernelspec: {
44
+ name: string;
45
+ display_name: string;
46
+ language: string;
47
+ };
48
+ language_info: {
49
+ name: string;
50
+ };
51
+ };
52
+ cells: ({
53
+ id: string;
54
+ cell_type: string;
55
+ metadata: {};
56
+ source: string;
57
+ execution_count?: undefined;
58
+ outputs?: undefined;
59
+ } | {
60
+ id: string;
61
+ cell_type: string;
62
+ metadata: {};
63
+ execution_count: number;
64
+ source: string;
65
+ outputs: {
66
+ output_type: string;
67
+ name: string;
68
+ text: string;
69
+ }[];
70
+ })[];
71
+ };
72
+ /** What the gallery says with the notebook it was given. */
73
+ export declare const SAMPLE_NOTEBOOK_SAYING = "Here is the notebook Accounting gave: the open invoices.";
30
74
  /** The approval the gallery waits on (T-23): answered in the balloon. */
31
75
  export declare function sampleApproval(onApprove: () => void, onDeny: () => void): BalloonApproval;
32
76
  /**
33
77
  * The balloon for a pose: waiting holds the approval, paused says so, and
34
78
  * any other pose says the sample's words when asked to (`saying`).
35
79
  */
36
- export declare function balloonForPose(pose: GalleryPose, { saying, approval, }: {
80
+ export declare function balloonForPose(pose: GalleryPose, { saying, approval, tool, attachment, }: {
37
81
  saying?: {
38
82
  text: string;
39
83
  more: boolean;
40
84
  };
41
85
  approval: BalloonApproval;
86
+ /** A tool being called: its line stands in for the words. */
87
+ tool?: BalloonToolLine;
88
+ /** What goes with the words in a `current` balloon: a notebook. */
89
+ attachment?: NonNullable<AssistantStageProps['balloon']>['attachment'];
42
90
  }): AssistantStageProps['balloon'];
@@ -2,6 +2,7 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ import { toolLineText, } from '../../chat/assistant/toolLine';
5
6
  import { ASSISTANT_WORDS, latestSaying, } from '../../chat/assistant/state';
6
7
  /** Every state, in the order an assistant lives them, then stepped aside. */
7
8
  export const GALLERY_POSES = [
@@ -73,6 +74,46 @@ export function sampleSaying(index) {
73
74
  }
74
75
  return { text: saying.text, more: saying.more };
75
76
  }
77
+ /** The tool the gallery calls (T-23): Odoo's, as Accounting calls it. */
78
+ export const SAMPLE_TOOL = 'list_invoices';
79
+ /** The sample tool's line, at a phase: "Using list_invoices…", "Done: …", "… failed". */
80
+ export function sampleToolLine(phase) {
81
+ return { id: 'gallery-tool', tool: SAMPLE_TOOL, name: SAMPLE_TOOL, phase };
82
+ }
83
+ /**
84
+ * A small recorded notebook (nbformat 4), as Accounting gives one: a
85
+ * heading, the code that totals the open invoices, and its one output.
86
+ */
87
+ export const SAMPLE_NOTEBOOK = {
88
+ nbformat: 4,
89
+ nbformat_minor: 5,
90
+ metadata: {
91
+ kernelspec: {
92
+ name: 'python3',
93
+ display_name: 'Python 3',
94
+ language: 'python',
95
+ },
96
+ language_info: { name: 'python' },
97
+ },
98
+ cells: [
99
+ {
100
+ id: 'gallery-markdown',
101
+ cell_type: 'markdown',
102
+ metadata: {},
103
+ source: '## Open invoices\n\nFrom the books, **in EUR**.',
104
+ },
105
+ {
106
+ id: 'gallery-code',
107
+ cell_type: 'code',
108
+ metadata: {},
109
+ execution_count: 1,
110
+ source: 'due = {"Ada": 1200, "Grace": 860}\nprint(sum(due.values()))',
111
+ outputs: [{ output_type: 'stream', name: 'stdout', text: '2060\n' }],
112
+ },
113
+ ],
114
+ };
115
+ /** What the gallery says with the notebook it was given. */
116
+ export const SAMPLE_NOTEBOOK_SAYING = 'Here is the notebook Accounting gave: the open invoices.';
76
117
  /** The approval the gallery waits on (T-23): answered in the balloon. */
77
118
  export function sampleApproval(onApprove, onDeny) {
78
119
  return {
@@ -88,7 +129,7 @@ export function sampleApproval(onApprove, onDeny) {
88
129
  * The balloon for a pose: waiting holds the approval, paused says so, and
89
130
  * any other pose says the sample's words when asked to (`saying`).
90
131
  */
91
- export function balloonForPose(pose, { saying, approval, }) {
132
+ export function balloonForPose(pose, { saying, approval, tool, attachment, }) {
92
133
  if (pose === 'waiting') {
93
134
  return { text: ASSISTANT_WORDS.approval, approval };
94
135
  }
@@ -98,5 +139,15 @@ export function balloonForPose(pose, { saying, approval, }) {
98
139
  if (pose === 'goodbye' || pose === 'aside') {
99
140
  return undefined;
100
141
  }
142
+ if (tool) {
143
+ return {
144
+ text: toolLineText(tool),
145
+ tool,
146
+ busy: tool.phase === 'running',
147
+ };
148
+ }
149
+ if (saying && attachment) {
150
+ return { ...saying, attachment };
151
+ }
101
152
  return saying;
102
153
  }
@@ -255,6 +255,9 @@ function parseInterface(data, kind) {
255
255
  if (isAssistantCharacterId(data.assistant)) {
256
256
  parsed.assistant = data.assistant;
257
257
  }
258
+ if (data.balloon === 'history' || data.balloon === 'current') {
259
+ parsed.balloon = data.balloon;
260
+ }
258
261
  return parsed;
259
262
  }
260
263
  function parseCriterion(data) {
@@ -516,6 +519,9 @@ function dumpInterface(spec, kind) {
516
519
  if (spec.assistant) {
517
520
  writer.data.assistant = spec.assistant;
518
521
  }
522
+ if (spec.balloon) {
523
+ writer.data.balloon = spec.balloon;
524
+ }
519
525
  // Its voice, when it says one (VO-41): a spec made before voice has none.
520
526
  if (spec.voice) {
521
527
  writer.part('voice', new Writer()
@@ -398,6 +398,7 @@ const ENUMS = {
398
398
  ],
399
399
  visibility: ['private', 'invited', 'organization', 'link', 'public'],
400
400
  mode: ['inline', 'bubble', 'panel', 'assistant'],
401
+ balloon: ['history', 'current'],
401
402
  trigger: ['schedule', 'event', 'once'],
402
403
  criterion: ['metric', 'noul', 'choice', 'score'],
403
404
  direction: ['higher', 'lower'],
@@ -525,6 +526,7 @@ export function documentShapeProblems(document) {
525
526
  mapping(d.interface, 'interface', ui => {
526
527
  oneOf(ui.layout, ENUMS.layout, 'interface.layout');
527
528
  oneOf(ui.accent, ENUMS.accent, 'interface.accent');
529
+ oneOf(ui.balloon, ENUMS.balloon, 'interface.balloon');
528
530
  if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
529
531
  at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
530
532
  }
@@ -80,7 +80,9 @@ function FloatingApp({ app, view, colorMode, themeOverrides, character, serverUr
80
80
  // Datalayer knows — said, and nothing launched, for one it does not.
81
81
  const token = useIAMStore(state => state.token);
82
82
  const signedOut = !serverUrl && !token;
83
- return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
83
+ return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), ...(app.interface.balloon
84
+ ? { balloonDisplay: app.interface.balloon }
85
+ : {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
84
86
  body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance,
85
87
  // The host's accent and face over the application's own, inside
86
88
  // its conversation too, in the embed's mode.