@datalayer/agent-runtimes 1.3.10 → 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 (107) hide show
  1. package/README.md +1 -2
  2. package/lib/AgentNode.js +3 -3
  3. package/lib/api/runtimes/environments.d.ts +22 -1
  4. package/lib/api/runtimes/environments.js +31 -0
  5. package/lib/api/runtimes/healthz.js +4 -2
  6. package/lib/api/spacer/healthz.js +4 -2
  7. package/lib/chat/ChatFloating.d.ts +8 -1
  8. package/lib/chat/ChatFloating.js +5 -2
  9. package/lib/chat/base/ChatBase.js +39 -25
  10. package/lib/chat/base/modelChoice.d.ts +62 -0
  11. package/lib/chat/base/modelChoice.js +56 -0
  12. package/lib/chat/display/ButtonGlow.d.ts +9 -0
  13. package/lib/chat/display/ButtonGlow.js +60 -0
  14. package/lib/chat/display/EmptyState.js +4 -1
  15. package/lib/chat/header/ChatHeaderBase.js +4 -56
  16. package/lib/chat/header/ChatViewModeToggle.d.ts +29 -0
  17. package/lib/chat/header/ChatViewModeToggle.js +55 -0
  18. package/lib/chat/index.d.ts +1 -0
  19. package/lib/chat/index.js +1 -0
  20. package/lib/chat/messages/ChatMessageList.js +13 -5
  21. package/lib/chat/messages/FormSubmissionMessage.d.ts +17 -0
  22. package/lib/chat/messages/FormSubmissionMessage.js +49 -0
  23. package/lib/chat/messages/formSubmission.d.ts +41 -0
  24. package/lib/chat/messages/formSubmission.js +90 -0
  25. package/lib/chat/prompt/menus/ModelSelector.js +11 -4
  26. package/lib/chat/styles/streamdownStyles.js +40 -0
  27. package/lib/client/mixins/RuntimesMixin.js +1 -1
  28. package/lib/client/mixins/SpacerMixin.js +3 -3
  29. package/lib/components/a2ui/A2uiMarkdownProvider.d.ts +11 -0
  30. package/lib/components/a2ui/A2uiMarkdownProvider.js +13 -0
  31. package/lib/components/a2ui/index.d.ts +1 -0
  32. package/lib/components/a2ui/index.js +1 -0
  33. package/lib/components/code-sandboxes/CodeSandboxLauncher.d.ts +8 -0
  34. package/lib/components/code-sandboxes/CodeSandboxLauncher.js +54 -5
  35. package/lib/components/code-sandboxes/codeSandboxEnvironments.d.ts +12 -1
  36. package/lib/components/code-sandboxes/codeSandboxEnvironments.js +38 -1
  37. package/lib/components/icons/ArtifactIcon.js +2 -1
  38. package/lib/examples/A2UiAgentExample.js +36 -223
  39. package/lib/examples/A2UiJupyterOutputExample.js +11 -1
  40. package/lib/examples/A2UiViewerExample.d.ts +9 -2
  41. package/lib/examples/A2UiViewerExample.js +33 -3
  42. package/lib/examples/DecksAgent.d.ts +1 -1
  43. package/lib/examples/DecksAgent.js +1 -1
  44. package/lib/examples/main.js +37 -35
  45. package/lib/examples/utils/a2uiMarkdownProvider.d.ts +1 -7
  46. package/lib/examples/utils/a2uiMarkdownProvider.js +6 -8
  47. package/lib/loop/core/index.d.ts +44 -4
  48. package/lib/loop/core/index.js +29 -2
  49. package/lib/loop/embed/LoopEmbed.js +7 -0
  50. package/lib/loop/index.d.ts +1 -1
  51. package/lib/loop/index.js +4 -1
  52. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +22 -0
  53. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +64 -0
  54. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +38 -0
  55. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +64 -0
  56. package/lib/loop/plugins/a2ui-surface/index.d.ts +34 -0
  57. package/lib/loop/plugins/a2ui-surface/index.js +91 -0
  58. package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +52 -0
  59. package/lib/loop/plugins/a2ui-surface/toolResult.js +106 -0
  60. package/lib/loop/plugins/chat/ChatView.js +95 -15
  61. package/lib/loop/plugins/chat/index.d.ts +14 -0
  62. package/lib/loop/plugins/chat/index.js +1 -0
  63. package/lib/loop/plugins/document/index.js +4 -0
  64. package/lib/loop/plugins/notebook/index.js +4 -0
  65. package/lib/loop/plugins/page-layout/index.d.ts +36 -0
  66. package/lib/loop/plugins/page-layout/index.js +87 -7
  67. package/lib/loop/plugins/shell/useEditorPreviews.js +7 -0
  68. package/lib/loop/presets.d.ts +19 -0
  69. package/lib/loop/presets.js +9 -1
  70. package/lib/models/Deck.d.ts +14 -0
  71. package/lib/models/Deck.js +5 -0
  72. package/lib/models/Environment.d.ts +117 -0
  73. package/lib/models/ItemType.d.ts +1 -1
  74. package/lib/models/Runtime.d.ts +6 -0
  75. package/lib/models/SpaceItem.d.ts +2 -1
  76. package/lib/models/environments.generated.d.ts +35 -1
  77. package/lib/models/environments.generated.js +1 -1
  78. package/lib/models/index.d.ts +1 -0
  79. package/lib/models/index.js +1 -0
  80. package/lib/protocols/BrowserAgentAdapter.d.ts +9 -1
  81. package/lib/protocols/BrowserAgentAdapter.js +11 -4
  82. package/lib/runtimes/actions.d.ts +5 -0
  83. package/lib/runtimes/actions.js +19 -0
  84. package/lib/specs/agents/agents.d.ts +3 -1
  85. package/lib/specs/agents/agents.js +426 -130
  86. package/lib/specs/frontendTools.d.ts +1 -0
  87. package/lib/specs/frontendTools.js +31 -0
  88. package/lib/specs/mcpServers.js +13 -13
  89. package/lib/specs/models.js +7 -7
  90. package/lib/specs/outputs.js +9 -0
  91. package/lib/specs/skills.js +7 -7
  92. package/lib/specs/teams/teams.js +1 -1
  93. package/lib/tools/adapters/copilotkit/CopilotKitToolAdapter.js +1 -1
  94. package/lib/types/outputs.d.ts +2 -0
  95. package/package.json +10 -4
  96. package/patches/@primer+react+37.31.0.patch +390 -0
  97. package/patches/eslint-plugin-react+7.37.5.patch +65 -0
  98. package/scripts/apply-patches.sh +15 -1
  99. package/scripts/codegen/compose.py +4 -4
  100. package/scripts/codegen/generate_agents.py +1 -2
  101. package/scripts/codegen/generate_frontend_tools.py +1 -1
  102. package/scripts/codegen/generate_mcp_servers.py +2 -2
  103. package/scripts/codegen/generate_models.py +33 -8
  104. package/scripts/codegen/generate_outputs.py +2 -0
  105. package/scripts/codegen/generate_skills.py +2 -2
  106. package/scripts/codegen/__pycache__/compose.cpython-313.pyc +0 -0
  107. package/scripts/codegen/__pycache__/versioning.cpython-313.pyc +0 -0
@@ -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
  *
@@ -824,14 +855,23 @@ export type ChatLayoutContribution = {
824
855
  /**
825
856
  * How this layout wants the composer built.
826
857
  *
827
- * `floating-top` is a floating card anchored to the top edge rather than
858
+ * `floating-top` and `floating-bottom` are a floating card anchored to
859
+ * that edge of the layout rather than
828
860
  * the bottom — a command line over the document, sized by the card.
829
861
  * `docked-top` is that same command line standing in the flow instead: the
830
862
  * layout gives it a mount point and the composer fills it, so its width is
831
863
  * the layout's to decide and the page below starts under it rather than
832
864
  * behind it.
833
865
  */
834
- prompt?: 'docked' | 'floating' | 'floating-top' | 'docked-top';
866
+ prompt?: 'docked' | 'floating' | 'floating-top' | 'floating-bottom' | 'docked-top';
867
+ /**
868
+ * The same, live, for a layout whose stance changes while it is on
869
+ * screen — the page layout moves the composer between a floating card
870
+ * and its side panel as the person picks a display mode. Read over
871
+ * `prompt` by the chat view whenever it is given; `prompt` is then only
872
+ * the stance the layout starts in.
873
+ */
874
+ promptStance?: ReadonlySignal<'docked' | 'floating' | 'floating-top' | 'floating-bottom' | 'docked-top'>;
835
875
  };
836
876
  export declare const LoopChatLayout: import("@datalayer/reactor").ContributionPoint<ChatLayoutContribution>;
837
877
  /**
@@ -71,13 +71,27 @@ export const IDLE_SANDBOX_TARGET_SIGNAL = {
71
71
  export function loopSurfaceId(sessionId) {
72
72
  return `loop-${sessionId || 'default'}`;
73
73
  }
74
- /** Create a prompt channel. The shell owns one per workspace. */
74
+ /**
75
+ * Create a prompt channel. The shell owns one per workspace.
76
+ *
77
+ * The shell's composer is on screen before the view that answers it: the
78
+ * editor and its sandbox are megabytes and take a moment, and the prompt is
79
+ * ready to type into immediately. A message sent in that moment used to be
80
+ * dropped — it appeared in the conversation and was never answered, and the
81
+ * only way to find out was to send it again — so an unheard message is held
82
+ * and handed to the first view that subscribes, in the order it was sent.
83
+ */
75
84
  export function createPromptChannel() {
76
85
  const listeners = new Set();
86
+ /** Said before anything was listening, waiting for the first view. */
87
+ const waiting = [];
77
88
  return {
78
89
  submit(message) {
79
90
  if (listeners.size === 0) {
80
- return false;
91
+ waiting.push(message);
92
+ // Heard, as far as the sender is concerned: it is going to be
93
+ // answered, so the composer clears and no failure is reported.
94
+ return true;
81
95
  }
82
96
  for (const listener of [...listeners]) {
83
97
  listener(message);
@@ -86,6 +100,19 @@ export function createPromptChannel() {
86
100
  },
87
101
  subscribe(listener) {
88
102
  listeners.add(listener);
103
+ if (waiting.length > 0) {
104
+ const held = waiting.splice(0, waiting.length);
105
+ /*
106
+ * Next tick, not now: `subscribe` is called from an effect as the
107
+ * view mounts, and a listener that started answering inside it would
108
+ * be setting state on a component in the middle of mounting.
109
+ */
110
+ queueMicrotask(() => {
111
+ for (const message of held) {
112
+ listener(message);
113
+ }
114
+ });
115
+ }
89
116
  return () => {
90
117
  listeners.delete(listener);
91
118
  };
@@ -66,6 +66,7 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
66
66
  preset.defaultEditor,
67
67
  preset.showViewSelector,
68
68
  preset.hideChatHeader,
69
+ preset.chatHeaderButtons,
69
70
  preset.promptPlacement,
70
71
  preset.autoFocusPrompt,
71
72
  preset.fullScreenTopOffset,
@@ -77,6 +78,12 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
77
78
  preset.pageLayoutTurnPanel,
78
79
  preset.pageLayoutTurnPanelFooter,
79
80
  preset.pageLayoutPrompt,
81
+ preset.pageLayoutPromptAnchor,
82
+ // By its fields: a host that writes the size inline hands over a new
83
+ // object every render, and the reactor must not be rebuilt for that.
84
+ preset.pageLayoutSize?.format,
85
+ preset.pageLayoutSize?.width,
86
+ preset.pageLayoutSize?.height,
80
87
  preset.localAgentSpec,
81
88
  preset.floatingPrompt,
82
89
  preset.editorSelector,
@@ -16,7 +16,7 @@ export { ChatHeaderPlugin, CHAT_HEADER_PLUGIN_NAME, } from './plugins/chat-heade
16
16
  export { InputPromptPlugin, INPUT_PROMPT_PLUGIN_NAME, type InputPromptPluginConfig, } from './plugins/input-prompt';
17
17
  export { createChatExtrasPlugin, CHAT_EXTRAS_PLUGIN_NAME, type ChatExtrasHandle, } from './plugins/chat-extras';
18
18
  export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, LoopOpeningNotebook, LoopPromptPanel, type ChatLayoutContribution, type ChatLayoutParts, type ChatTurnContribution, type ChatTurnSnapshot, type ChatTurnStatus, type LoopChatExtrasValue, type OpeningDocumentContribution, type OpeningNotebookContribution, type PromptPanelContribution, } from './core';
19
- export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, type LoopPageLayoutConfig, } from './plugins/page-layout';
19
+ export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, setPageLayoutChatMode, type LoopPageLayoutConfig, } from './plugins/page-layout';
20
20
  export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
21
21
  export type { TeamMember, TeamSelection } from './plugins/agents/team';
22
22
  export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
package/lib/loop/index.js CHANGED
@@ -26,7 +26,10 @@ export { createChatExtrasPlugin, CHAT_EXTRAS_PLUGIN_NAME, } from './plugins/chat
26
26
  // read back: the chat extras, the opening notebook, and who the team is
27
27
  // talking to. A page composing panels around a `LoopEmbed` needs all three.
28
28
  export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, LoopOpeningNotebook, LoopPromptPanel, } from './core';
29
- export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, } from './plugins/page-layout';
29
+ export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen,
30
+ // How a host moves the composer: `sidebar` stands it in the conversation
31
+ // panel, under what was said, and opens the panel to show it.
32
+ setPageLayoutChatMode, } from './plugins/page-layout';
30
33
  export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
31
34
  export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
32
35
  export { NotebookViewPlugin, NOTEBOOK_VIEW_PLUGIN_NAME, } from './plugins/notebook-view';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A generated A2UI surface, drawn inline where the agent's answer lands.
3
+ *
4
+ * Its own processor, so the surface can be filled in and submitted here
5
+ * whatever else is showing it. The basic catalogue draws from inherited
6
+ * `--a2ui-*` custom properties, so the surface follows the workspace's theme
7
+ * by inheriting rather than by being told — the same rule the Surface view
8
+ * follows, and what keeps a generated form from being the one light card in
9
+ * a dark workspace.
10
+ *
11
+ * @module loop/plugins/a2ui-surface/InlineSurface
12
+ */
13
+ import type { JSX } from 'react';
14
+ import { type A2uiClientAction, type A2uiMessage } from '@a2ui/web_core/v0_9';
15
+ /** The catalogue a surface is rewritten to before it is drawn. */
16
+ export declare const SURFACE_CATALOG_ID: string;
17
+ export declare function InlineSurface({ messages, onAction, validationError, }: {
18
+ messages: A2uiMessage[];
19
+ onAction: (action: A2uiClientAction) => void;
20
+ validationError?: string | null;
21
+ }): JSX.Element | null;
22
+ export default InlineSurface;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Box } from '@datalayer/primer-addons';
4
+ import { basicCatalog, } from '@a2ui/react/v0_9';
5
+ import { MessageProcessor, } from '@a2ui/web_core/v0_9';
6
+ import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, } from '../../../components/a2ui';
7
+ /** The catalogue a surface is rewritten to before it is drawn. */
8
+ export const SURFACE_CATALOG_ID = basicCatalog.id;
9
+ const INHERIT_THEME = {
10
+ ['--a2ui-color-surface']: 'var(--bgColor-muted)',
11
+ ['--a2ui-color-on-surface']: 'var(--fgColor-default)',
12
+ ['--a2ui-color-primary']: 'var(--fgColor-accent)',
13
+ ['--a2ui-color-outline']: 'var(--borderColor-default)',
14
+ };
15
+ export function InlineSurface({ messages, onAction, validationError, }) {
16
+ // Reached through a ref: the processor is built once, and the handler it
17
+ // was built with must not go stale when the host's does not.
18
+ const onActionRef = useRef(onAction);
19
+ onActionRef.current = onAction;
20
+ const processor = useMemo(() => new MessageProcessor([basicCatalog], action => onActionRef.current(action)), []);
21
+ const [surfaces, setSurfaces] = useState([]);
22
+ useEffect(() => {
23
+ const created = processor.onSurfaceCreated(surface => setSurfaces(previous => [...previous, surface]));
24
+ const deleted = processor.onSurfaceDeleted(id => setSurfaces(previous => previous.filter(surface => surface.id !== id)));
25
+ return () => {
26
+ created.unsubscribe();
27
+ deleted.unsubscribe();
28
+ };
29
+ }, [processor]);
30
+ // Once: the messages are one tool result, processed when it arrives.
31
+ const processedRef = useRef(false);
32
+ useEffect(() => {
33
+ if (processedRef.current) {
34
+ return;
35
+ }
36
+ processedRef.current = true;
37
+ processor.processMessages(messages);
38
+ }, [messages, processor]);
39
+ if (surfaces.length === 0) {
40
+ return null;
41
+ }
42
+ return (_jsx(A2uiMarkdownProvider, { children: _jsxs(Box, { style: INHERIT_THEME, sx: {
43
+ ...A2UI_RENDER_SCOPE_SX,
44
+ display: 'flex',
45
+ flexDirection: 'column',
46
+ gap: 2,
47
+ }, children: [validationError ? (_jsx(Box, { role: "alert", sx: {
48
+ px: 3,
49
+ py: 2,
50
+ borderRadius: 2,
51
+ bg: 'danger.subtle',
52
+ border: '1px solid',
53
+ borderColor: 'danger.muted',
54
+ color: 'danger.fg',
55
+ fontSize: 1,
56
+ }, children: validationError })) : null, surfaces.map(surface => (_jsx(Box, { sx: {
57
+ border: '1px solid',
58
+ borderColor: 'border.default',
59
+ borderRadius: 2,
60
+ p: 3,
61
+ bg: 'canvas.default',
62
+ }, children: _jsx(A2uiSurfaceComposed, { surface: surface }) }, surface.id)))] }) }));
63
+ }
64
+ export default InlineSurface;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * One `render_a2ui_surface` result in the transcript: the surface, and what
3
+ * pressing its Submit does.
4
+ *
5
+ * The values are checked against the rules the tool sent with the surface —
6
+ * a missing required field is said under the form — and, when they pass,
7
+ * handed back to the agent as the reader's next turn, so the confirmation
8
+ * arrives in the conversation where the form was asked for. A host that
9
+ * wants to know is told too (`onRendered`, `onSubmitted`): the A2UI Agent
10
+ * example mirrors the surface on a canvas beside the chat.
11
+ *
12
+ * @module loop/plugins/a2ui-surface/SurfaceToolResult
13
+ */
14
+ import type { JSX } from 'react';
15
+ import type { A2uiMessage } from '@a2ui/web_core/v0_9';
16
+ import type { ToolCallRenderContext } from '../../../types/chat';
17
+ import { type A2uiToolResult } from './toolResult';
18
+ export type SurfaceRendered = {
19
+ toolCallId: string;
20
+ surfaceId?: string;
21
+ title: string;
22
+ messages: A2uiMessage[];
23
+ result: A2uiToolResult;
24
+ };
25
+ export type SurfaceSubmitted = {
26
+ surfaceId: string;
27
+ title: string;
28
+ values: Record<string, unknown>;
29
+ };
30
+ export type SurfaceToolResultProps = {
31
+ context: ToolCallRenderContext;
32
+ /** Say something as the reader; `false` when the chat cannot take it. */
33
+ send: (message: string) => boolean;
34
+ onRendered?: (rendered: SurfaceRendered) => void;
35
+ onSubmitted?: (submitted: SurfaceSubmitted) => void;
36
+ };
37
+ export declare function SurfaceToolResult({ context, send, onRendered, onSubmitted, }: SurfaceToolResultProps): JSX.Element;
38
+ export default SurfaceToolResult;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { Spinner, Text } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { encodeFormSubmission } from '../../../chat/messages/formSubmission';
6
+ import { InlineSurface, SURFACE_CATALOG_ID } from './InlineSurface';
7
+ import { readA2uiToolResult, validateA2uiSubmission, } from './toolResult';
8
+ export function SurfaceToolResult({ context, send, onRendered, onSubmitted, }) {
9
+ const parsed = useMemo(() => context.status === 'complete'
10
+ ? readA2uiToolResult(context.result, SURFACE_CATALOG_ID)
11
+ : null, [context.status, context.result]);
12
+ const title = parsed?.title ?? 'the form';
13
+ const [validationError, setValidationError] = useState(null);
14
+ // Told once per result, when it is there to be told about.
15
+ useEffect(() => {
16
+ if (parsed?.messages && parsed.messages.length > 0) {
17
+ onRendered?.({
18
+ toolCallId: context.toolCallId,
19
+ surfaceId: parsed.surfaceId,
20
+ title,
21
+ messages: parsed.messages,
22
+ result: parsed,
23
+ });
24
+ }
25
+ // The result is what changes; the callback's identity is the host's.
26
+ // eslint-disable-next-line react-hooks/exhaustive-deps
27
+ }, [parsed, context.toolCallId]);
28
+ const handleAction = useCallback((action) => {
29
+ if (action.name !== 'submit_a2ui_form') {
30
+ return;
31
+ }
32
+ const values = (action.context ?? {});
33
+ const errors = validateA2uiSubmission(parsed?.fieldRules ?? [], values);
34
+ if (errors.length > 0) {
35
+ setValidationError(errors.join(' '));
36
+ return;
37
+ }
38
+ setValidationError(null);
39
+ onSubmitted?.({ surfaceId: action.surfaceId, title, values });
40
+ const sent = send(encodeFormSubmission({
41
+ title,
42
+ surfaceId: action.surfaceId,
43
+ values,
44
+ }));
45
+ if (!sent) {
46
+ setValidationError('The chat is not ready to take the submission yet.');
47
+ }
48
+ }, [parsed, title, send, onSubmitted]);
49
+ if (!parsed?.messages || parsed.messages.length === 0) {
50
+ return (_jsxs(Box, { sx: {
51
+ border: '1px solid',
52
+ borderColor: 'border.default',
53
+ borderRadius: 2,
54
+ px: 3,
55
+ py: 2,
56
+ bg: 'canvas.default',
57
+ display: 'flex',
58
+ alignItems: 'center',
59
+ gap: 2,
60
+ }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Rendering A2UI surface..." })] }));
61
+ }
62
+ return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(Text, { sx: { fontWeight: 'bold' }, children: title }), _jsx(InlineSurface, { messages: parsed.messages, onAction: handleAction, validationError: validationError })] }));
63
+ }
64
+ export default SurfaceToolResult;
@@ -0,0 +1,34 @@
1
+ import type { ReactorPlugin } from '@datalayer/reactor';
2
+ import { type SurfaceRendered, type SurfaceSubmitted } from './SurfaceToolResult';
3
+ export declare const A2UI_SURFACE_PLUGIN_NAME = "@datalayer/loop-plugin-a2ui-surface";
4
+ /** The renderer name a spec binds a tool to. */
5
+ export declare const A2UI_SURFACE_RENDERER = "a2ui-surface";
6
+ /**
7
+ * Every tool some spec binds to this renderer.
8
+ *
9
+ * Read from the specs rather than from the workspace's current agent: a
10
+ * tool result arrives named by its tool, and a name bound in any spec is a
11
+ * surface wherever it turns up.
12
+ */
13
+ export declare function surfaceToolNames(): Set<string>;
14
+ export type A2uiSurfacePluginOptions = {
15
+ /** Told when a surface has been drawn from a tool result. */
16
+ onRendered?: (rendered: SurfaceRendered) => void;
17
+ /** Told when a submission passed its checks and went to the agent. */
18
+ onSubmitted?: (submitted: SurfaceSubmitted) => void;
19
+ };
20
+ /**
21
+ * An instance of the plugin, with a host's callbacks.
22
+ *
23
+ * The chat reaches a workspace's `send` only through slot props, so the
24
+ * plugin keeps a silent component in the chat's title-bar slot — it draws
25
+ * nothing and holds the `send` of the workspace it is in. The slot is asked
26
+ * whether anyone contributed before it is drawn, so this costs the header
27
+ * no space.
28
+ */
29
+ export declare function createA2uiSurfacePlugin(options?: A2uiSurfacePluginOptions): ReactorPlugin<Record<string, never>, unknown, unknown>;
30
+ /** The instance the preset mounts: no host callbacks. */
31
+ export declare const A2uiSurfacePlugin: ReactorPlugin<Record<string, never>, unknown, unknown>;
32
+ export type { SurfaceRendered, SurfaceSubmitted } from './SurfaceToolResult';
33
+ export { readA2uiToolResult, validateA2uiField, validateA2uiSubmission, type A2uiFieldRule, type A2uiToolResult, } from './toolResult';
34
+ export default A2uiSurfacePlugin;
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
3
+ import { contribution, definePlugin, signal } from '@datalayer/reactor';
4
+ import { AGENTSPECS } from '../../../specs/agents';
5
+ import { LoopChatExtras, LoopSlots, } from '../../core';
6
+ import { SurfaceToolResult, } from './SurfaceToolResult';
7
+ export const A2UI_SURFACE_PLUGIN_NAME = '@datalayer/loop-plugin-a2ui-surface';
8
+ /** The renderer name a spec binds a tool to. */
9
+ export const A2UI_SURFACE_RENDERER = 'a2ui-surface';
10
+ /**
11
+ * Every tool some spec binds to this renderer.
12
+ *
13
+ * Read from the specs rather than from the workspace's current agent: a
14
+ * tool result arrives named by its tool, and a name bound in any spec is a
15
+ * surface wherever it turns up.
16
+ */
17
+ export function surfaceToolNames() {
18
+ const names = new Set();
19
+ for (const spec of Object.values(AGENTSPECS)) {
20
+ for (const binding of spec.frontendRenderTools ?? []) {
21
+ if (binding.renderer === A2UI_SURFACE_RENDERER) {
22
+ names.add(binding.tool);
23
+ }
24
+ }
25
+ }
26
+ return names;
27
+ }
28
+ let counter = 0;
29
+ /**
30
+ * An instance of the plugin, with a host's callbacks.
31
+ *
32
+ * The chat reaches a workspace's `send` only through slot props, so the
33
+ * plugin keeps a silent component in the chat's title-bar slot — it draws
34
+ * nothing and holds the `send` of the workspace it is in. The slot is asked
35
+ * whether anyone contributed before it is drawn, so this costs the header
36
+ * no space.
37
+ */
38
+ export function createA2uiSurfacePlugin(options = {}) {
39
+ const id = `a2ui-surface-${(counter += 1)}`;
40
+ let currentSend;
41
+ const send = (message) => {
42
+ if (!currentSend) {
43
+ return false;
44
+ }
45
+ currentSend(message);
46
+ return true;
47
+ };
48
+ function Capture({ workspace, }) {
49
+ const workspaceSend = workspace?.viewControls.send;
50
+ useEffect(() => {
51
+ currentSend = workspaceSend;
52
+ return () => {
53
+ if (currentSend === workspaceSend) {
54
+ currentSend = undefined;
55
+ }
56
+ };
57
+ }, [workspaceSend]);
58
+ return null;
59
+ }
60
+ const tools = surfaceToolNames();
61
+ // A failed call is the chat's to draw: its default card says what went
62
+ // wrong, and a spinner that never ends would say nothing at all. So is
63
+ // every tool that is not a surface: this renderer is the chat's only one,
64
+ // and returning nothing for a tool it does not own hid every other tool
65
+ // row — the notebook reads, the cell runs — from the transcript.
66
+ const renderToolResult = (context) => tools.has(context.toolName) && context.status !== 'error' ? (_jsx(SurfaceToolResult, { context: context, send: send, onRendered: options.onRendered, onSubmitted: options.onSubmitted }, context.toolCallId)) : (context.defaultUI);
67
+ const plugin = definePlugin({
68
+ name: counter === 1
69
+ ? A2UI_SURFACE_PLUGIN_NAME
70
+ : `${A2UI_SURFACE_PLUGIN_NAME}-${id}`,
71
+ displayName: 'A2UI surfaces',
72
+ description: 'Generated surfaces drawn in the transcript, and submitted back to the agent.',
73
+ octicon: 'browser',
74
+ emoji: '\u{1FA9F}',
75
+ contributes: [
76
+ contribution(LoopChatExtras, { id, extras: signal({ renderToolResult }) }, { id }),
77
+ ],
78
+ build() {
79
+ return {
80
+ components: [
81
+ { slot: LoopSlots.chatHeader, id, Component: Capture },
82
+ ],
83
+ };
84
+ },
85
+ });
86
+ return plugin;
87
+ }
88
+ /** The instance the preset mounts: no host callbacks. */
89
+ export const A2uiSurfacePlugin = createA2uiSurfacePlugin();
90
+ export { readA2uiToolResult, validateA2uiField, validateA2uiSubmission, } from './toolResult';
91
+ export default A2uiSurfacePlugin;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What the `render_a2ui_surface` tool answers, read and checked.
3
+ *
4
+ * Pure: no React, no catalogue instance. What is here is what a test wants
5
+ * to reach without a chat around it — the shape of a result, the rules a
6
+ * surface ships for its own fields, and the verdict on a submission.
7
+ *
8
+ * @module loop/plugins/a2ui-surface/toolResult
9
+ */
10
+ import type { A2uiMessage } from '@a2ui/web_core/v0_9';
11
+ export type A2uiRequiredField = {
12
+ id: string;
13
+ label: string;
14
+ };
15
+ /**
16
+ * Per-field validation descriptor emitted by the backend tool. The rules
17
+ * gate a submission: required, email format, pattern, length, slider range.
18
+ */
19
+ export type A2uiFieldRule = {
20
+ id: string;
21
+ label: string;
22
+ type?: string;
23
+ required?: boolean;
24
+ format?: 'email';
25
+ pattern?: string;
26
+ minLength?: number;
27
+ min?: number;
28
+ max?: number;
29
+ };
30
+ export type A2uiToolResult = {
31
+ surfaceId?: string;
32
+ title?: string;
33
+ messages?: A2uiMessage[];
34
+ requiredFields?: A2uiRequiredField[];
35
+ fieldRules?: A2uiFieldRule[];
36
+ };
37
+ /**
38
+ * Read a tool result — an object, or the JSON of one — as A2UI messages,
39
+ * with the catalogue id rewritten to `catalogId` so the frontend catalogue
40
+ * accepts what a backend named by its own id.
41
+ */
42
+ export declare function readA2uiToolResult(result: unknown, catalogId: string): A2uiToolResult | null;
43
+ /**
44
+ * Whether a submitted value counts as "not provided". Handles the shapes the
45
+ * basic catalogue emits: strings (text/email), arrays (choice), booleans
46
+ * (checkbox).
47
+ */
48
+ export declare function isEmptyA2uiValue(value: unknown): boolean;
49
+ /** One rule's complaint about one value, or `null` when it is acceptable. */
50
+ export declare function validateA2uiField(rule: A2uiFieldRule, value: unknown): string | null;
51
+ /** Every rule's complaint about a submission; empty when it passes. */
52
+ export declare function validateA2uiSubmission(rules: A2uiFieldRule[], values: Record<string, unknown>): string[];
@@ -0,0 +1,106 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Read a tool result — an object, or the JSON of one — as A2UI messages,
7
+ * with the catalogue id rewritten to `catalogId` so the frontend catalogue
8
+ * accepts what a backend named by its own id.
9
+ */
10
+ export function readA2uiToolResult(result, catalogId) {
11
+ if (!result) {
12
+ return null;
13
+ }
14
+ let obj = result;
15
+ if (typeof result === 'string') {
16
+ try {
17
+ obj = JSON.parse(result);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ if (typeof obj !== 'object' ||
24
+ obj === null ||
25
+ !Array.isArray(obj.messages)) {
26
+ return null;
27
+ }
28
+ const parsed = obj;
29
+ const messages = (parsed.messages ?? []).map(message => {
30
+ const payload = message;
31
+ if (payload.createSurface &&
32
+ payload.createSurface.catalogId !== catalogId) {
33
+ return {
34
+ ...payload,
35
+ createSurface: { ...payload.createSurface, catalogId },
36
+ };
37
+ }
38
+ return message;
39
+ });
40
+ return { ...parsed, messages };
41
+ }
42
+ /**
43
+ * Whether a submitted value counts as "not provided". Handles the shapes the
44
+ * basic catalogue emits: strings (text/email), arrays (choice), booleans
45
+ * (checkbox).
46
+ */
47
+ export function isEmptyA2uiValue(value) {
48
+ if (value === null || value === undefined) {
49
+ return true;
50
+ }
51
+ if (typeof value === 'string') {
52
+ return value.trim() === '';
53
+ }
54
+ if (Array.isArray(value)) {
55
+ return value.length === 0;
56
+ }
57
+ if (typeof value === 'boolean') {
58
+ return value === false;
59
+ }
60
+ return false;
61
+ }
62
+ /** Permissive on purpose: `local@domain.tld` with no spaces. */
63
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
64
+ /** One rule's complaint about one value, or `null` when it is acceptable. */
65
+ export function validateA2uiField(rule, value) {
66
+ if (isEmptyA2uiValue(value)) {
67
+ return rule.required ? `${rule.label} is required.` : null;
68
+ }
69
+ if (rule.format === 'email' &&
70
+ typeof value === 'string' &&
71
+ !EMAIL_PATTERN.test(value.trim())) {
72
+ return `${rule.label} must be a valid email address.`;
73
+ }
74
+ if (rule.pattern && typeof value === 'string') {
75
+ let matches;
76
+ try {
77
+ matches = new RegExp(rule.pattern).test(value.trim());
78
+ }
79
+ catch {
80
+ matches = true; // A malformed pattern blocks nothing.
81
+ }
82
+ if (!matches) {
83
+ return `${rule.label} is not in the expected format.`;
84
+ }
85
+ }
86
+ if (rule.minLength !== undefined &&
87
+ typeof value === 'string' &&
88
+ value.trim().length < rule.minLength) {
89
+ return `${rule.label} must be at least ${rule.minLength} characters.`;
90
+ }
91
+ if (rule.type === 'slider' && typeof value === 'number') {
92
+ if (rule.min !== undefined && value < rule.min) {
93
+ return `${rule.label} must be at least ${rule.min}.`;
94
+ }
95
+ if (rule.max !== undefined && value > rule.max) {
96
+ return `${rule.label} must be at most ${rule.max}.`;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ /** Every rule's complaint about a submission; empty when it passes. */
102
+ export function validateA2uiSubmission(rules, values) {
103
+ return rules
104
+ .map(rule => validateA2uiField(rule, values[rule.id]))
105
+ .filter((message) => message !== null);
106
+ }