@datalayer/agent-runtimes 1.3.11 → 1.3.13

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 (62) hide show
  1. package/README.md +1 -2
  2. package/lib/AgentNode.js +3 -3
  3. package/lib/api/runtimes/healthz.js +4 -2
  4. package/lib/api/spacer/healthz.js +4 -2
  5. package/lib/chat/base/ChatBase.js +39 -25
  6. package/lib/chat/base/modelChoice.d.ts +62 -0
  7. package/lib/chat/base/modelChoice.js +56 -0
  8. package/lib/chat/messages/ChatMessageList.js +13 -5
  9. package/lib/chat/messages/FormSubmissionMessage.d.ts +17 -0
  10. package/lib/chat/messages/FormSubmissionMessage.js +49 -0
  11. package/lib/chat/messages/formSubmission.d.ts +41 -0
  12. package/lib/chat/messages/formSubmission.js +90 -0
  13. package/lib/chat/prompt/menus/ModelSelector.js +11 -4
  14. package/lib/chat/styles/streamdownStyles.js +40 -0
  15. package/lib/client/mixins/RuntimesMixin.js +1 -1
  16. package/lib/client/mixins/SpacerMixin.js +3 -3
  17. package/lib/components/a2ui/A2uiMarkdownProvider.d.ts +11 -0
  18. package/lib/components/a2ui/A2uiMarkdownProvider.js +13 -0
  19. package/lib/components/a2ui/index.d.ts +1 -0
  20. package/lib/components/a2ui/index.js +1 -0
  21. package/lib/examples/A2UiAgentExample.js +36 -223
  22. package/lib/examples/A2UiJupyterOutputExample.js +11 -1
  23. package/lib/examples/A2UiViewerExample.d.ts +9 -2
  24. package/lib/examples/A2UiViewerExample.js +33 -3
  25. package/lib/examples/main.js +37 -35
  26. package/lib/examples/utils/a2uiMarkdownProvider.d.ts +1 -7
  27. package/lib/examples/utils/a2uiMarkdownProvider.js +6 -8
  28. package/lib/loop/core/index.d.ts +33 -2
  29. package/lib/loop/core/index.js +29 -2
  30. package/lib/loop/embed/LoopEmbed.js +1 -0
  31. package/lib/loop/index.d.ts +1 -1
  32. package/lib/loop/index.js +4 -1
  33. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +22 -0
  34. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +64 -0
  35. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +38 -0
  36. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +64 -0
  37. package/lib/loop/plugins/a2ui-surface/index.d.ts +34 -0
  38. package/lib/loop/plugins/a2ui-surface/index.js +91 -0
  39. package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +52 -0
  40. package/lib/loop/plugins/a2ui-surface/toolResult.js +106 -0
  41. package/lib/loop/plugins/chat/ChatView.js +81 -10
  42. package/lib/loop/plugins/chat/index.d.ts +14 -0
  43. package/lib/loop/plugins/chat/index.js +1 -0
  44. package/lib/loop/plugins/document/index.js +4 -0
  45. package/lib/loop/plugins/notebook/index.js +4 -0
  46. package/lib/loop/plugins/shell/useEditorPreviews.js +7 -0
  47. package/lib/loop/presets.d.ts +5 -0
  48. package/lib/loop/presets.js +7 -1
  49. package/lib/models/environments.generated.d.ts +7 -1
  50. package/lib/models/environments.generated.js +1 -1
  51. package/lib/specs/agents/agents.d.ts +1 -0
  52. package/lib/specs/agents/agents.js +112 -0
  53. package/lib/specs/models.js +3 -3
  54. package/lib/tools/adapters/copilotkit/CopilotKitToolAdapter.js +1 -1
  55. package/package.json +10 -4
  56. package/patches/@primer+react+37.31.0.patch +390 -0
  57. package/patches/eslint-plugin-react+7.37.5.patch +65 -0
  58. package/scripts/apply-patches.sh +15 -1
  59. package/scripts/codegen/compose.py +4 -4
  60. package/scripts/codegen/generate_agents.py +1 -2
  61. package/scripts/codegen/generate_frontend_tools.py +1 -1
  62. package/scripts/codegen/generate_models.py +33 -8
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ /**
3
+ * Provides the A2UI v0.9 markdown renderer to every `A2uiSurface` beneath
4
+ * it. Without it, `Text` components fall back to the raw markdown string
5
+ * (`## Title` instead of a rendered heading).
6
+ *
7
+ * In the library rather than the examples: the loop's own surface plugin
8
+ * draws surfaces, and needs the same context the examples had.
9
+ */
10
+ export declare const A2uiMarkdownProvider: React.FC<React.PropsWithChildren>;
11
+ export default A2uiMarkdownProvider;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MarkdownContext } from '@a2ui/react/v0_9';
3
+ import { renderMarkdown } from '@a2ui/markdown-it';
4
+ /**
5
+ * Provides the A2UI v0.9 markdown renderer to every `A2uiSurface` beneath
6
+ * it. Without it, `Text` components fall back to the raw markdown string
7
+ * (`## Title` instead of a rendered heading).
8
+ *
9
+ * In the library rather than the examples: the loop's own surface plugin
10
+ * draws surfaces, and needs the same context the examples had.
11
+ */
12
+ export const A2uiMarkdownProvider = ({ children, }) => (_jsx(MarkdownContext.Provider, { value: renderMarkdown, children: children }));
13
+ export default A2uiMarkdownProvider;
@@ -1,2 +1,3 @@
1
+ export * from './A2uiMarkdownProvider';
1
2
  export * from './A2uiSurfaceComposed';
2
3
  export * from './styles';
@@ -2,5 +2,6 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ export * from './A2uiMarkdownProvider';
5
6
  export * from './A2uiSurfaceComposed';
6
7
  export * from './styles';
@@ -25,17 +25,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
25
25
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
26
26
  import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
27
27
  import { Button, Spinner, Text } from '@primer/react';
28
- import { basicCatalog } from '@a2ui/react/v0_9';
29
28
  import { A2UI_RENDER_SCOPE_SX, A2uiSurfaceComposed } from '../components/a2ui';
30
29
  import { ThemedProvider } from './utils/themedProvider';
31
30
  import { A2uiMarkdownProvider } from './utils/a2uiMarkdownProvider';
32
31
  import { useA2uiProcessor } from './utils/a2ui';
32
+ import { createA2uiSurfacePlugin, validateA2uiSubmission, } from '../loop/plugins/a2ui-surface';
33
33
  import { useExampleAgentRuntime } from './hooks/useExampleAgentRuntime';
34
34
  import { uniqueAgentId } from './utils/agentId';
35
- import { useSpecRenderToolResult, specRendererClassName, } from './hooks/useSpecRenderToolResult';
36
35
  import { LoopEmbed } from '../loop';
37
36
  import { AgentA2uiPlugin } from '../loop/plugins/agent-a2ui';
38
- import { createChatExtrasPlugin } from '../loop/plugins/chat-extras';
39
37
  setupPrimerPortals();
40
38
  const AGENT_NAME = 'a2ui-agent';
41
39
  const AGENTSPEC_ID = 'example-a2ui-agent';
@@ -57,156 +55,6 @@ const SUGGESTIONS = [
57
55
  message: 'Make a product configurator for a laptop with CPU, RAM and add-ons.',
58
56
  },
59
57
  ];
60
- /**
61
- * Whether a submitted form value should count as "not provided" for the
62
- * purpose of required-field validation. Handles the value shapes the A2UI
63
- * basic catalog emits: strings (text/email), arrays (choice), booleans
64
- * (checkbox).
65
- */
66
- function isEmptyValue(value) {
67
- if (value === null || value === undefined) {
68
- return true;
69
- }
70
- if (typeof value === 'string') {
71
- return value.trim() === '';
72
- }
73
- if (Array.isArray(value)) {
74
- return value.length === 0;
75
- }
76
- if (typeof value === 'boolean') {
77
- return value === false;
78
- }
79
- return false;
80
- }
81
- /**
82
- * Basic email format check (matches the intent of the backend `email` field
83
- * type). Intentionally permissive: requires `local@domain.tld` with no spaces.
84
- */
85
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
86
- /**
87
- * Validate a single submitted value against its backend-provided rule. Returns
88
- * a human-readable error message, or `null` when the value is acceptable.
89
- */
90
- function validateFieldValue(rule, value) {
91
- const empty = isEmptyValue(value);
92
- if (empty) {
93
- // Optional fields left blank are fine; only required ones fail here.
94
- return rule.required ? `${rule.label} is required.` : null;
95
- }
96
- if (rule.format === 'email' &&
97
- typeof value === 'string' &&
98
- !EMAIL_PATTERN.test(value.trim())) {
99
- return `${rule.label} must be a valid email address.`;
100
- }
101
- if (rule.pattern && typeof value === 'string') {
102
- let matches;
103
- try {
104
- matches = new RegExp(rule.pattern).test(value.trim());
105
- }
106
- catch {
107
- matches = true; // Ignore malformed patterns rather than blocking.
108
- }
109
- if (!matches) {
110
- return `${rule.label} is not in the expected format.`;
111
- }
112
- }
113
- if (rule.minLength !== undefined &&
114
- typeof value === 'string' &&
115
- value.trim().length < rule.minLength) {
116
- return `${rule.label} must be at least ${rule.minLength} characters.`;
117
- }
118
- if (rule.type === 'slider' && typeof value === 'number') {
119
- if (rule.min !== undefined && value < rule.min) {
120
- return `${rule.label} must be at least ${rule.min}.`;
121
- }
122
- if (rule.max !== undefined && value > rule.max) {
123
- return `${rule.label} must be at most ${rule.max}.`;
124
- }
125
- }
126
- return null;
127
- }
128
- /**
129
- * Normalize a backend tool result (object or JSON string) into A2UI messages,
130
- * rewriting the catalog id to the frontend catalog so the renderer accepts it.
131
- */
132
- function extractToolResult(result) {
133
- if (!result) {
134
- return null;
135
- }
136
- let obj = result;
137
- if (typeof result === 'string') {
138
- try {
139
- obj = JSON.parse(result);
140
- }
141
- catch {
142
- return null;
143
- }
144
- }
145
- if (typeof obj !== 'object' ||
146
- obj === null ||
147
- !Array.isArray(obj.messages)) {
148
- return null;
149
- }
150
- const parsed = obj;
151
- const messages = (parsed.messages ?? []).map(message => {
152
- const payload = message;
153
- if (payload.createSurface &&
154
- payload.createSurface.catalogId !== basicCatalog.id) {
155
- return {
156
- ...payload,
157
- createSurface: {
158
- ...payload.createSurface,
159
- catalogId: basicCatalog.id,
160
- },
161
- };
162
- }
163
- return message;
164
- });
165
- return { ...parsed, messages };
166
- }
167
- /**
168
- * Renders a fully interactive A2UI surface *inline* in the chat transcript.
169
- *
170
- * Uses its own `useA2uiProcessor` instance (independent from the canvas
171
- * processor) so the same generated surface can be filled in and submitted from
172
- * both places. Messages are processed once on mount.
173
- */
174
- const InlineA2uiSurface = ({ messages, onAction, validationError }) => {
175
- const { surfaces, processMessages, resetSurfaces, themeStyle } = useA2uiProcessor(onAction);
176
- const processedRef = useRef(false);
177
- useEffect(() => {
178
- if (processedRef.current) {
179
- return;
180
- }
181
- processedRef.current = true;
182
- resetSurfaces();
183
- processMessages(messages);
184
- }, [messages, processMessages, resetSurfaces]);
185
- if (surfaces.length === 0) {
186
- return null;
187
- }
188
- return (_jsxs(Box, { style: themeStyle, sx: {
189
- ...A2UI_RENDER_SCOPE_SX,
190
- display: 'flex',
191
- flexDirection: 'column',
192
- gap: 2,
193
- }, children: [validationError && (_jsx(Box, { role: "alert", sx: {
194
- px: 3,
195
- py: 2,
196
- borderRadius: 2,
197
- bg: 'danger.subtle',
198
- border: '1px solid',
199
- borderColor: 'danger.muted',
200
- color: 'danger.fg',
201
- fontSize: 1,
202
- }, children: validationError })), surfaces.map(surface => (_jsx(Box, { sx: {
203
- border: '1px solid',
204
- borderColor: 'border.default',
205
- borderRadius: 2,
206
- p: 3,
207
- bg: 'canvas.default',
208
- }, children: _jsx(A2uiSurfaceComposed, { surface: surface }) }, surface.id)))] }));
209
- };
210
58
  const A2UiAgentExample = () => {
211
59
  const agentName = useMemo(() => uniqueAgentId(AGENT_NAME), []);
212
60
  const { agentId, error: chatError, status, isReady, baseUrl, } = useExampleAgentRuntime({
@@ -221,8 +69,6 @@ const A2UiAgentExample = () => {
221
69
  const isCreatingChat = !isReady && status !== 'error';
222
70
  const [surfaceTitle, setSurfaceTitle] = useState(null);
223
71
  const [submissions, setSubmissions] = useState([]);
224
- const [pendingPrompt, setPendingPrompt] = useState(undefined);
225
- void pendingPrompt;
226
72
  const [validationError, setValidationError] = useState(null);
227
73
  const submissionSeq = useRef(0);
228
74
  // Validation rules per surface id, populated from each tool result so the
@@ -241,9 +87,7 @@ const A2UiAgentExample = () => {
241
87
  // Block submission when any field fails validation (required, email
242
88
  // format, pattern, length, slider range).
243
89
  const rules = fieldRulesBySurfaceRef.current[action.surfaceId] ?? [];
244
- const errors = rules
245
- .map(rule => validateFieldValue(rule, values[rule.id]))
246
- .filter((message) => message !== null);
90
+ const errors = validateA2uiSubmission(rules, values);
247
91
  if (errors.length > 0) {
248
92
  setValidationError({
249
93
  surfaceId: action.surfaceId,
@@ -252,23 +96,19 @@ const A2UiAgentExample = () => {
252
96
  return;
253
97
  }
254
98
  setValidationError(null);
99
+ // The canvas copy: recorded here, since the transcript's own copy is
100
+ // the one that goes back to the agent.
255
101
  submissionSeq.current += 1;
256
- const seq = submissionSeq.current;
257
- setSubmissions(prev => [
258
- {
259
- id: seq,
260
- values,
261
- },
262
- ...prev,
263
- ]);
264
- // Feed the submission back to the agent so it replies with a confirmation.
265
- const title = surfaceTitleRef.current ?? 'the form';
266
- const json = JSON.stringify(values, null, 2);
267
- setPendingPrompt(`I just submitted "${title}" (submission #${seq}). Here are the values:\n\n\`\`\`json\n${json}\n\`\`\`\n\nPlease confirm you received them and briefly summarize what happens next.`);
102
+ setSubmissions(prev => [{ id: submissionSeq.current, values }, ...prev]);
268
103
  }
269
104
  }, []);
270
105
  const { surfaces, processMessages, resetSurfaces, themeStyle } = useA2uiProcessor(handleAction);
271
- // Stable ref so the memoized renderer can push surfaces without re-creating.
106
+ /*
107
+ * The transcript's surfaces are the loop's own — the preset's surface
108
+ * plugin draws, checks and submits them. This example asks for an instance
109
+ * of its own so the canvas beside the chat can mirror what was drawn, and
110
+ * the "Submitted values" panel can record what went back to the agent.
111
+ */
272
112
  const showSurfaceRef = useRef(() => { });
273
113
  useEffect(() => {
274
114
  showSurfaceRef.current = (messages) => {
@@ -276,59 +116,27 @@ const A2UiAgentExample = () => {
276
116
  processMessages(messages);
277
117
  };
278
118
  }, [resetSurfaces, processMessages]);
279
- const processedToolCallIds = useRef(new Set());
280
- const renderers = useMemo(() => ({
281
- 'a2ui-surface': (context, binding) => {
282
- const parsed = context.status === 'complete'
283
- ? extractToolResult(context.result)
284
- : null;
285
- if (context.status === 'complete' &&
286
- parsed?.messages &&
287
- parsed.messages.length > 0 &&
288
- context.toolCallId &&
289
- !processedToolCallIds.current.has(context.toolCallId)) {
290
- processedToolCallIds.current.add(context.toolCallId);
291
- showSurfaceRef.current(parsed.messages);
292
- setSurfaceTitle(parsed.title ?? 'A2UI surface');
293
- if (parsed.surfaceId) {
294
- fieldRulesBySurfaceRef.current[parsed.surfaceId] =
295
- parsed.fieldRules ?? [];
119
+ const chatPlugins = useMemo(() => [
120
+ AgentA2uiPlugin,
121
+ createA2uiSurfacePlugin({
122
+ onRendered: rendered => {
123
+ showSurfaceRef.current(rendered.messages);
124
+ setSurfaceTitle(rendered.title);
125
+ if (rendered.surfaceId) {
126
+ fieldRulesBySurfaceRef.current[rendered.surfaceId] =
127
+ rendered.result.fieldRules ?? [];
296
128
  }
297
129
  setValidationError(null);
298
- }
299
- return (_jsx("div", { className: specRendererClassName(binding), children: context.status === 'complete' &&
300
- parsed?.messages &&
301
- parsed.messages.length > 0 ? (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsxs(Box, { sx: {
302
- display: 'flex',
303
- alignItems: 'center',
304
- gap: 2,
305
- color: 'fg.muted',
306
- }, children: [_jsx(Text, { sx: { fontSize: 2 }, children: "\uD83C\uDF9B\uFE0F" }), _jsx(Text, { sx: { fontWeight: 'bold', color: 'fg.default' }, children: parsed.title ?? 'A2UI surface' }), _jsx(Text, { sx: { fontSize: 0 }, children: "\u00B7 also in the canvas \u2192 fill it in and submit" })] }), _jsx(InlineA2uiSurface, { messages: parsed.messages, onAction: handleAction, validationError: validationError &&
307
- parsed.surfaceId &&
308
- validationError.surfaceId === parsed.surfaceId
309
- ? validationError.message
310
- : null })] })) : (_jsxs(Box, { sx: {
311
- border: '1px solid',
312
- borderColor: 'border.default',
313
- borderRadius: 2,
314
- px: 3,
315
- py: 2,
316
- bg: 'canvas.default',
317
- display: 'flex',
318
- alignItems: 'center',
319
- gap: 2,
320
- }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Rendering A2UI surface..." })] })) }));
321
- },
322
- }), [handleAction, validationError]);
323
- const renderToolResult = useSpecRenderToolResult(AGENTSPEC_ID, renderers);
324
- // The chat column is the shared loop on the A2UI capacity plugin; the
325
- // example's own surface renderer reaches the transcript through the
326
- // chat-extras channel, so tool results still draw as A2UI surfaces.
327
- const { plugin: extrasPlugin, setExtras } = useMemo(() => createChatExtrasPlugin(), []);
328
- const chatPlugins = useMemo(() => [AgentA2uiPlugin, extrasPlugin], [extrasPlugin]);
329
- useEffect(() => {
330
- setExtras({ renderToolResult });
331
- }, [renderToolResult, setExtras]);
130
+ },
131
+ onSubmitted: submitted => {
132
+ submissionSeq.current += 1;
133
+ setSubmissions(prev => [
134
+ { id: submissionSeq.current, values: submitted.values },
135
+ ...prev,
136
+ ]);
137
+ },
138
+ }),
139
+ ], []);
332
140
  const clearCanvas = useCallback(() => {
333
141
  resetSurfaces();
334
142
  setSurfaceTitle(null);
@@ -446,6 +254,11 @@ const A2UiAgentExample = () => {
446
254
  justifyContent: 'center',
447
255
  height: '100%',
448
256
  gap: 3,
449
- }, children: [_jsx(Spinner, { size: "large" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Creating A2UI agent..." })] })) : chatError || !agentId ? (_jsx(Box, { sx: { p: 3 }, children: _jsxs(Text, { sx: { color: 'danger.fg' }, children: ["Failed to initialize chat:", ' ', chatError || 'No agent id returned'] }) })) : (_jsx(LoopEmbed, { serverUrl: baseUrl, target: "local", agentId: agentId, defaultEditor: "none", showHeader: true, plugins: chatPlugins })) })] }) }) }));
257
+ }, children: [_jsx(Spinner, { size: "large" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Creating A2UI agent..." })] })) : chatError || !agentId ? (_jsx(Box, { sx: { p: 3 }, children: _jsxs(Text, { sx: { color: 'danger.fg' }, children: ["Failed to initialize chat:", ' ', chatError || 'No agent id returned'] }) })) : (_jsx(LoopEmbed, { serverUrl: baseUrl, target: "local",
258
+ // What lets the target take: without the choice the agents
259
+ // plugin pins the sandbox to the page, whatever `target` says,
260
+ // and the chat then turns in-page, where the server's tools
261
+ // do not exist.
262
+ showAgentVariants: true, agentId: agentId, defaultEditor: "none", showHeader: true, plugins: chatPlugins })) })] }) }) }));
450
263
  };
451
264
  export default A2UiAgentExample;
@@ -765,7 +765,17 @@ const A2UiJupyterOutputExample = () => {
765
765
  borderRadius: 2,
766
766
  overflow: 'hidden',
767
767
  bg: 'canvas.default',
768
- }, children: agentReady && agentId ? (_jsx(LoopEmbed, { serverUrl: serverUrl, target: "local", agentId: agentId, defaultEditor: "none", showHeader: true, plugins: chatPlugins })) : (_jsxs(Box, { sx: {
768
+ }, children: agentReady && agentId ? (_jsx(LoopEmbed, { serverUrl: serverUrl, target: "local",
769
+ // No `showAgentVariants` on purpose: without the choice the
770
+ // agents plugin pins the sandbox to the page, and this chat is
771
+ // meant to turn in the page. The server agent above speaks
772
+ // vercel-ai and only runs the code; it has no AG-UI route, so a
773
+ // chat addressing it on the server got "404 Not Found". The
774
+ // demo tool below is a frontend tool, and the in-page loop
775
+ // calls it.
776
+ agentId: agentId, defaultEditor: "none", showHeader: true,
777
+ // A reader asks for one demonstration after another: `+` starts over.
778
+ chatHeaderButtons: true, plugins: chatPlugins })) : (_jsxs(Box, { sx: {
769
779
  height: '100%',
770
780
  display: 'flex',
771
781
  alignItems: 'center',
@@ -3,8 +3,15 @@
3
3
  *
4
4
  * The scene lives in `./scenes/A2UiViewerScene` and arrives as an A2UI scene
5
5
  * plugin — one workspace view, ordered ahead of the chat. The rest is the
6
- * standard chat plugins; there is no agent behind this scene, so the
7
- * workspace runs on the browser target and costs no server.
6
+ * standard chat plugins.
7
+ *
8
+ * The workspace runs on whatever the header's target says, like the examples
9
+ * with an agent of their own. It used to be pinned to the browser — there is
10
+ * no agent behind the scene, so the page was the cheapest place — which left
11
+ * the header's *Local* selected and ignored: the sandbox stayed in the page.
12
+ * On Local the loop asks the local agent-runtimes server for its shell agent
13
+ * and the Jupyter server that agent starts beside itself, which is what
14
+ * choosing Local means everywhere else here.
8
15
  *
9
16
  * @module examples/A2UiViewerExample
10
17
  */
@@ -8,8 +8,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
8
8
  *
9
9
  * The scene lives in `./scenes/A2UiViewerScene` and arrives as an A2UI scene
10
10
  * plugin — one workspace view, ordered ahead of the chat. The rest is the
11
- * standard chat plugins; there is no agent behind this scene, so the
12
- * workspace runs on the browser target and costs no server.
11
+ * standard chat plugins.
12
+ *
13
+ * The workspace runs on whatever the header's target says, like the examples
14
+ * with an agent of their own. It used to be pinned to the browser — there is
15
+ * no agent behind the scene, so the page was the cheapest place — which left
16
+ * the header's *Local* selected and ignored: the sandbox stayed in the page.
17
+ * On Local the loop asks the local agent-runtimes server for its shell agent
18
+ * and the Jupyter server that agent starts beside itself, which is what
19
+ * choosing Local means everywhere else here.
13
20
  *
14
21
  * @module examples/A2UiViewerExample
15
22
  */
@@ -17,6 +24,8 @@ import { useMemo } from 'react';
17
24
  import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
18
25
  import { ThemedProvider } from './utils/themedProvider';
19
26
  import { LoopEmbed } from '../loop';
27
+ import { useRuntimeTargetStore } from './utils/runtimeTargetStore';
28
+ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
20
29
  import { defineA2uiScenePlugin } from '../loop/plugins/a2ui-scene';
21
30
  setupPrimerPortals();
22
31
  const ScenePlugin = defineA2uiScenePlugin({
@@ -25,8 +34,29 @@ const ScenePlugin = defineA2uiScenePlugin({
25
34
  description: 'Paste A2UI protocol messages and watch the surface they describe.',
26
35
  load: () => import('./scenes/A2UiViewerScene'),
27
36
  });
37
+ const AGENTSPEC_ID = 'example-a2ui-viewer';
28
38
  const A2UiViewerExample = () => {
39
+ /*
40
+ * The agent's surfaces, and what pressing their Submit does, are the
41
+ * loop's own: its spec binds `render_a2ui_surface` to the `a2ui-surface`
42
+ * renderer, and the preset's surface plugin draws, checks and submits.
43
+ * Nothing here but the scene.
44
+ */
29
45
  const plugins = useMemo(() => [ScenePlugin], []);
30
- return (_jsx(ThemedProvider, { children: _jsx(Box, { sx: { height: '100vh', minHeight: 0 }, children: _jsx(LoopEmbed, { target: "browser", agentId: "loop-shell", defaultEditor: "none", showHeader: true, plugins: plugins }) }) }));
46
+ // The header's choice; the shell remounts this example when it changes.
47
+ const target = useRuntimeTargetStore(state => state.target);
48
+ const serverUrl = useExampleAgentRuntimesUrl();
49
+ return (_jsx(ThemedProvider, { children: _jsx(Box, { sx: { height: '100vh', minHeight: 0 }, children: _jsx(LoopEmbed, { target: target,
50
+ // An in-page agent has no server to ask; the others need to know
51
+ // where theirs is.
52
+ serverUrl: target === 'browser' ? undefined : serverUrl, agentId: AGENTSPEC_ID,
53
+ // What a server target creates that agent from. Without it the
54
+ // chat on Local posted to an agent nobody had created — a 404.
55
+ localAgentSpec: AGENTSPEC_ID,
56
+ // What lets the header's target take: without the choice the
57
+ // sandbox plugin pins itself to the page, whatever `target` says.
58
+ showAgentVariants: true, defaultEditor: "none", showHeader: true,
59
+ // A reader asks for one surface after another: `+` starts over.
60
+ chatHeaderButtons: true, plugins: plugins }) }) }));
31
61
  };
32
62
  export default A2UiViewerExample;
@@ -259,6 +259,23 @@ const ensureLocalJupyterToken = () => {
259
259
  setJupyterServerToken(DEFAULT_LOCAL_JUPYTER_SERVER_TOKEN);
260
260
  }
261
261
  };
262
+ /**
263
+ * Whether somebody signed in to this browser and is still signed in.
264
+ *
265
+ * The shell's own persisted sign-in, or the token core stored at sign-in —
266
+ * either one is a person, and a configured key must not stand in for them.
267
+ */
268
+ const hasPersistedSignIn = () => {
269
+ try {
270
+ if (useSimpleAuthStore.getState().token) {
271
+ return true;
272
+ }
273
+ return Boolean(window.localStorage.getItem(DATALAYER_IAM_TOKEN_KEY));
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ };
262
279
  // Load configurations from DOM
263
280
  const loadConfigurations = () => {
264
281
  // Load Datalayer configuration
@@ -277,36 +294,22 @@ const loadConfigurations = () => {
277
294
  if (datalayerConfig.iamUrl) {
278
295
  datalayerConfig.runtimesUrl = resolveRuntimesUrl(datalayerConfig.runtimesUrl);
279
296
  coreStore.getState().setConfiguration(datalayerConfig);
280
- // Also set the token in the IAM store for API authentication
281
- if (datalayerConfig.token) {
282
- // Use the setLogin method to set the token in IAM store
283
- // For now, we'll just set a minimal user object since we don't have full user data
284
- iamStore.getState().setLogin({
285
- id: 'example-id',
286
- handle: 'example-user',
287
- email: 'example@datalayer.com',
288
- firstName: 'Example',
289
- lastName: 'User',
290
- initials: 'EU',
291
- displayName: 'Example User',
292
- avatarUrl: '',
293
- roles: [],
294
- setRoles: () => { },
295
- iamProviders: [],
296
- settings: {},
297
- unsubscribedFromOutbounds: false,
298
- onboarding: {
299
- clients: {
300
- Platform: 0,
301
- JupyterLab: 0,
302
- CLI: 0,
303
- VSCode: 0,
304
- },
305
- position: 'top',
306
- tours: {},
307
- },
308
- events: [],
309
- }, datalayerConfig.token);
297
+ /*
298
+ A configured token — `VITE_DATALAYER_API_KEY` — is for a browser
299
+ nobody has signed in to. Somebody's own sign-in wins over it.
300
+
301
+ This used to sign the configured token in as a made-up user,
302
+ `example-id`, on every load. Core claims the browser's session for
303
+ whoever signs in and forgets it when a *different* person appears —
304
+ and a made-up user is a different person from whoever really signed
305
+ in, so every refresh wiped the real session: token, user and the
306
+ persisted sign-in of the shell. Now the token is only set, and core
307
+ asks IAM who it belongs to, which claims the session under the real
308
+ identity or logs the token out if IAM refuses it.
309
+ */
310
+ if (datalayerConfig.token && !hasPersistedSignIn()) {
311
+ iamStore.setState({ token: datalayerConfig.token });
312
+ void iamStore.getState().refreshUserByToken(datalayerConfig.token);
310
313
  }
311
314
  }
312
315
  }
@@ -476,12 +479,11 @@ const AgentRuntimesIAMCallback = () => {
476
479
  const providerAccessToken = provider
477
480
  ? params.get(`${provider}_access_token`)
478
481
  : null;
482
+ // Only the providers whose token the browser is handed. A LinkedIn or
483
+ // Bluesky token never leaves IAM (IBrowserTokenProviderName), so there is
484
+ // nothing of theirs to keep here.
479
485
  if (providerAccessToken &&
480
- (provider === 'github' ||
481
- provider === 'google' ||
482
- provider === 'linkedin' ||
483
- provider === 'okta' ||
484
- provider === 'bluesky')) {
486
+ (provider === 'github' || provider === 'google' || provider === 'okta')) {
485
487
  iamStore
486
488
  .getState()
487
489
  .setIAMProviderAccessToken(provider, providerAccessToken);
@@ -1,7 +1 @@
1
- import React from 'react';
2
- /**
3
- * Provides the A2UI v0.9 markdown renderer to all child `A2uiSurface`
4
- * instances. Without it, `Text` components fall back to the raw markdown
5
- * string (e.g. `## Title` instead of a rendered `<h2>`).
6
- */
7
- export declare const A2uiMarkdownProvider: React.FC<React.PropsWithChildren>;
1
+ export { A2uiMarkdownProvider } from '../../components/a2ui';
@@ -1,9 +1,7 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { MarkdownContext } from '@a2ui/react/v0_9';
3
- import { renderMarkdown } from '@a2ui/markdown-it';
4
- /**
5
- * Provides the A2UI v0.9 markdown renderer to all child `A2uiSurface`
6
- * instances. Without it, `Text` components fall back to the raw markdown
7
- * string (e.g. `## Title` instead of a rendered `<h2>`).
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
8
4
  */
9
- export const A2uiMarkdownProvider = ({ children, }) => (_jsx(MarkdownContext.Provider, { value: renderMarkdown, children: children }));
5
+ // Moved into the library, beside the surface components: the loop's own
6
+ // surface plugin needs the same context the examples had.
7
+ export { A2uiMarkdownProvider } from '../../components/a2ui';
@@ -183,12 +183,25 @@ export type LoopWorkspaceContext = {
183
183
  * publishes, a mounted view subscribes, and neither imports the other.
184
184
  */
185
185
  export type PromptChannel = {
186
- /** Publish a prompt. Returns whether anything was listening. */
186
+ /**
187
+ * Publish a prompt. Returns whether it will be answered: `false` only when
188
+ * nothing is listening and nothing is coming — a view that has not mounted
189
+ * yet holds the message instead (see {@link createPromptChannel}).
190
+ */
187
191
  submit: (message: string) => boolean;
188
192
  /** Listen for prompts. Returns an unsubscribe. */
189
193
  subscribe: (listener: (message: string) => void) => () => void;
190
194
  };
191
- /** Create a prompt channel. The shell owns one per workspace. */
195
+ /**
196
+ * Create a prompt channel. The shell owns one per workspace.
197
+ *
198
+ * The shell's composer is on screen before the view that answers it: the
199
+ * editor and its sandbox are megabytes and take a moment, and the prompt is
200
+ * ready to type into immediately. A message sent in that moment used to be
201
+ * dropped — it appeared in the conversation and was never answered, and the
202
+ * only way to find out was to send it again — so an unheard message is held
203
+ * and handed to the first view that subscribes, in the order it was sent.
204
+ */
192
205
  export declare function createPromptChannel(): PromptChannel;
193
206
  /**
194
207
  * What the active view tells the shell about itself.
@@ -205,6 +218,15 @@ export type ViewControls = {
205
218
  stop?: () => void;
206
219
  /** Start the view's work over — the chat resets its conversation. */
207
220
  newChat?: () => void;
221
+ /**
222
+ * Say something as the reader: the chat sends it as the next turn.
223
+ *
224
+ * For whatever speaks on the reader's behalf — a form pressed on a
225
+ * rendered surface, whose values the agent should answer. The chat had
226
+ * this control from the start and kept it to itself; a host with a
227
+ * message had no way in.
228
+ */
229
+ send?: (message: string) => void;
208
230
  };
209
231
  /** Props every view receives. */
210
232
  export type LoopViewProps = {
@@ -548,6 +570,15 @@ export type LoopChatExtrasValue = {
548
570
  * on the chip's text, which is how the chat tells openers apart already.
549
571
  */
550
572
  hiddenOpeners?: string[];
573
+ /**
574
+ * Every opener off, however many there are.
575
+ *
576
+ * `hiddenOpeners` takes a page's exceptions by name; this is the page that
577
+ * wants none of them — a landing page whose composer has moved under a
578
+ * conversation that has started, where a row of openers over it would be
579
+ * offering a first question to somebody who has already asked one.
580
+ */
581
+ hideOpeners?: boolean;
551
582
  /**
552
583
  * Told what the agent has used, each time the chat learns it.
553
584
  *