@datalayer/agent-runtimes 1.3.83 → 1.3.85

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 (168) hide show
  1. package/lib/chat/ChatFloating.d.ts +5 -4
  2. package/lib/chat/ChatFloating.js +8 -6
  3. package/lib/chat/ChatLanguage.d.ts +17 -0
  4. package/lib/chat/ChatLanguage.js +29 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +4 -4
  6. package/lib/chat/assistant/AssistantStage.js +4 -2
  7. package/lib/chat/assistant/BalloonParts.d.ts +2 -1
  8. package/lib/chat/assistant/BalloonParts.js +12 -5
  9. package/lib/chat/assistant/BalloonVisual.js +3 -1
  10. package/lib/chat/assistant/ConversationBalloon.d.ts +1 -1
  11. package/lib/chat/assistant/ConversationBalloon.js +3 -2
  12. package/lib/chat/assistant/SpeechBalloon.d.ts +1 -1
  13. package/lib/chat/assistant/SpeechBalloon.js +10 -7
  14. package/lib/chat/assistant/toolLine.d.ts +8 -4
  15. package/lib/chat/assistant/toolLine.js +13 -9
  16. package/lib/chat/base/ChatBase.js +30 -11
  17. package/lib/chat/base/loopElement.d.ts +53 -0
  18. package/lib/chat/base/loopElement.js +78 -0
  19. package/lib/chat/base/loopWindow.d.ts +31 -0
  20. package/lib/chat/base/loopWindow.js +74 -0
  21. package/lib/chat/display/EmptyState.js +3 -1
  22. package/lib/chat/index.d.ts +2 -0
  23. package/lib/chat/index.js +3 -0
  24. package/lib/chat/messages/ChatMessages.js +3 -1
  25. package/lib/chat/messages/TurnFooter.js +3 -1
  26. package/lib/chat/prompt/InputPrompt.js +3 -1
  27. package/lib/chat/prompt/InputPromptLexical.js +3 -1
  28. package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
  29. package/lib/chat/tools/ToolApprovalBanner.js +5 -3
  30. package/lib/chat/tools/ToolApprovalDialog.js +5 -3
  31. package/lib/chat/tools/ToolCallDisplay.js +29 -25
  32. package/lib/chat/words.d.ts +102 -0
  33. package/lib/chat/words.js +460 -0
  34. package/lib/components/a2ui/custom/index.d.ts +39 -0
  35. package/lib/components/a2ui/custom/index.js +95 -0
  36. package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
  37. package/lib/components/a2ui/custom/sandbox.js +188 -0
  38. package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
  39. package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
  40. package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
  41. package/lib/components/a2ui/datalayer/Form.js +9 -2
  42. package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
  43. package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
  44. package/lib/components/a2ui/datalayer/index.d.ts +1 -1
  45. package/lib/components/a2ui/datalayer/index.js +1 -1
  46. package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
  47. package/lib/components/a2ui/datalayer/schema.js +12 -1
  48. package/lib/components/a2ui/index.d.ts +1 -0
  49. package/lib/components/a2ui/index.js +1 -0
  50. package/lib/components/a2ui/visibleWhen.d.ts +12 -1
  51. package/lib/components/a2ui/visibleWhen.js +24 -3
  52. package/lib/components/inspector/AgentInspector.d.ts +25 -44
  53. package/lib/components/inspector/AgentInspector.js +27 -215
  54. package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
  55. package/lib/components/inspector/AgentInspectorDialog.js +2 -2
  56. package/lib/components/inspector/a2aSpans.d.ts +51 -0
  57. package/lib/components/inspector/a2aSpans.js +373 -0
  58. package/lib/components/inspector/agentSpans.d.ts +120 -0
  59. package/lib/components/inspector/agentSpans.js +253 -0
  60. package/lib/components/inspector/chatSpans.d.ts +36 -0
  61. package/lib/components/inspector/chatSpans.js +50 -0
  62. package/lib/components/inspector/classify.d.ts +11 -8
  63. package/lib/components/inspector/classify.js +23 -24
  64. package/lib/components/inspector/index.d.ts +7 -7
  65. package/lib/components/inspector/index.js +7 -7
  66. package/lib/components/teams/A2ATeamGraph.d.ts +4 -4
  67. package/lib/components/teams/SceneTranscript.d.ts +29 -0
  68. package/lib/components/teams/SceneTranscript.js +54 -0
  69. package/lib/components/teams/SceneView.d.ts +41 -0
  70. package/lib/components/teams/SceneView.js +28 -0
  71. package/lib/components/teams/index.d.ts +3 -0
  72. package/lib/components/teams/index.js +3 -0
  73. package/lib/components/teams/sceneTranscript.d.ts +74 -0
  74. package/lib/components/teams/sceneTranscript.js +294 -0
  75. package/lib/components/teams/teamConnections.d.ts +2 -2
  76. package/lib/components/teams/teamConnections.js +3 -3
  77. package/lib/components/teams/useA2ATeam.d.ts +4 -4
  78. package/lib/components/teams/useA2ATeam.js +50 -46
  79. package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
  80. package/lib/examples/AgentA2ATeamExample.js +70 -60
  81. package/lib/examples/AssistantExample.js +2 -2
  82. package/lib/loop/apps/AppComposer.d.ts +40 -4
  83. package/lib/loop/apps/AppComposer.js +194 -19
  84. package/lib/loop/apps/AppRenderer.d.ts +21 -3
  85. package/lib/loop/apps/AppRenderer.js +88 -48
  86. package/lib/loop/apps/appspec.d.ts +13 -1
  87. package/lib/loop/apps/appspec.js +305 -12
  88. package/lib/loop/apps/checks.d.ts +14 -0
  89. package/lib/loop/apps/checks.js +276 -5
  90. package/lib/loop/apps/composer.d.ts +21 -10
  91. package/lib/loop/apps/composer.js +41 -8
  92. package/lib/loop/apps/customComponents.d.ts +41 -0
  93. package/lib/loop/apps/customComponents.js +260 -0
  94. package/lib/loop/apps/hostTools.d.ts +8 -1
  95. package/lib/loop/apps/hostTools.js +9 -0
  96. package/lib/loop/apps/index.d.ts +1 -0
  97. package/lib/loop/apps/index.js +1 -0
  98. package/lib/loop/apps/language.d.ts +68 -0
  99. package/lib/loop/apps/language.js +275 -0
  100. package/lib/loop/apps/settingsInputs.d.ts +29 -0
  101. package/lib/loop/apps/settingsInputs.js +147 -0
  102. package/lib/loop/apps/uploads.d.ts +43 -0
  103. package/lib/loop/apps/uploads.js +66 -0
  104. package/lib/loop/core/a2uiComponents.d.ts +10 -0
  105. package/lib/loop/core/a2uiComponents.js +15 -0
  106. package/lib/loop/core/index.d.ts +15 -1
  107. package/lib/loop/core/index.js +3 -0
  108. package/lib/loop/embed/AppEmbed.d.ts +25 -3
  109. package/lib/loop/embed/AppEmbed.js +106 -18
  110. package/lib/loop/embed/element.d.ts +5 -0
  111. package/lib/loop/embed/element.js +84 -5
  112. package/lib/loop/embed/embedConfig.d.ts +8 -0
  113. package/lib/loop/embed/embedConfig.js +18 -0
  114. package/lib/loop/embed/embedSession.d.ts +14 -0
  115. package/lib/loop/embed/embedSession.js +34 -0
  116. package/lib/loop/embed/hostBridge.d.ts +50 -3
  117. package/lib/loop/embed/hostBridge.js +22 -2
  118. package/lib/loop/index.d.ts +4 -3
  119. package/lib/loop/index.js +4 -3
  120. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
  121. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
  122. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
  123. package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
  124. package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
  125. package/lib/loop/plugins/app-components/index.d.ts +26 -0
  126. package/lib/loop/plugins/app-components/index.js +56 -0
  127. package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
  128. package/lib/loop/plugins/app-elements/AppElements.js +79 -0
  129. package/lib/loop/plugins/app-elements/index.d.ts +20 -0
  130. package/lib/loop/plugins/app-elements/index.js +43 -0
  131. package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
  132. package/lib/loop/plugins/app-page/AppPage.js +106 -8
  133. package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
  134. package/lib/loop/plugins/app-page/appPageModel.js +122 -4
  135. package/lib/loop/plugins/app-page/index.d.ts +1 -0
  136. package/lib/loop/plugins/app-page/index.js +1 -0
  137. package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
  138. package/lib/loop/plugins/app-page/pageRun.js +111 -0
  139. package/lib/loop/plugins/assistant/index.js +8 -6
  140. package/lib/loop/plugins/chat/ChatView.js +16 -6
  141. package/lib/loop/plugins/chat/index.d.ts +1 -1
  142. package/lib/loop/plugins/chat/index.js +2 -1
  143. package/lib/models/PageTag.d.ts +1 -1
  144. package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
  145. package/lib/protocols/BrowserAgentAdapter.js +13 -10
  146. package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
  147. package/lib/runtimes/browser/protocol.d.ts +1 -1
  148. package/lib/specs/actions.js +78 -1
  149. package/lib/specs/apps.d.ts +5 -0
  150. package/lib/specs/apps.js +1518 -216
  151. package/lib/specs/appspecSchema.js +630 -3
  152. package/lib/specs/teams/teams.d.ts +3 -0
  153. package/lib/specs/teams/teams.js +249 -0
  154. package/lib/specs/uiPlugins.js +6 -1
  155. package/lib/types/agentspecs.d.ts +200 -1
  156. package/lib/types/chat.d.ts +6 -5
  157. package/lib/types/teams.d.ts +13 -2
  158. package/package.json +2 -2
  159. package/scripts/codegen/generate_apps.py +20 -2
  160. package/scripts/codegen/generate_teams.py +15 -2
  161. package/lib/components/inspector/JsonTree.d.ts +0 -24
  162. package/lib/components/inspector/JsonTree.js +0 -89
  163. package/lib/components/inspector/a2aInspect.d.ts +0 -56
  164. package/lib/components/inspector/a2aInspect.js +0 -478
  165. package/lib/components/inspector/agentInspector.d.ts +0 -179
  166. package/lib/components/inspector/agentInspector.js +0 -267
  167. package/lib/components/inspector/chatInspect.d.ts +0 -38
  168. package/lib/components/inspector/chatInspect.js +0 -65
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * `app-components` — the components an application's developer wrote (LOOP
7
+ * P-17), registered as that application's A2UI contributions: each a block
8
+ * of its Canvas's palette (`loop.canvas.block`, as a UI plugin's components
9
+ * are) and a renderer of the catalog its page, its elements and its answers
10
+ * are drawn with (`loop.a2ui.component`). One plugin per application, in its
11
+ * workspace's preset only: the catalog grows for it, it does not open.
12
+ *
13
+ * Its components are reviewed before they get here — `checkAppspec` refuses
14
+ * an invalid declaration — and drawn in a sandboxed frame of no origin
15
+ * (`components/a2ui/custom`).
16
+ *
17
+ * @module loop/plugins/app-components
18
+ */
19
+ import { contribution, definePlugin, } from '@datalayer/reactor';
20
+ import { customImplementation } from '../../../components/a2ui/custom';
21
+ import { customComponentEntry } from '../../apps/customComponents';
22
+ import { LoopA2uiComponent } from '../../core/a2uiComponents';
23
+ import { LoopCanvasBlock } from '../../core/canvasBlocks';
24
+ /** The name of the plugin that contributes an application's own components. */
25
+ export const appComponentsPluginName = (appId) => `@datalayer/loop-plugin-app-components-${appId}`;
26
+ /** Whether an application's developer wrote components of its own. */
27
+ export const hasCustomComponents = (app) => (app.interface.customComponents ?? []).length > 0;
28
+ /**
29
+ * The plugin contributing the components an application's developer wrote:
30
+ * a block and a renderer for each. `fetcher` is how their modules are
31
+ * fetched; the page's `fetch` unless given (tests).
32
+ */
33
+ export function defineAppComponentsPlugin(app, fetcher) {
34
+ const own = app.interface.customComponents ?? [];
35
+ if (own.length === 0) {
36
+ throw new Error(`“${app.name}” has no component of its own to contribute.`);
37
+ }
38
+ return definePlugin({
39
+ name: appComponentsPluginName(app.id),
40
+ displayName: `${app.name}'s components`,
41
+ description: `The components ${app.name}'s developer wrote: ${own.map(component => component.name).join(', ')}.`,
42
+ octicon: 'apps',
43
+ contributes: own.flatMap(component => [
44
+ contribution(LoopCanvasBlock, {
45
+ id: component.name,
46
+ uiPlugin: `app:${app.id}`,
47
+ component: customComponentEntry(component, app.version),
48
+ }, { id: component.name }),
49
+ contribution(LoopA2uiComponent, {
50
+ id: component.name,
51
+ app: app.id,
52
+ implementation: customImplementation(component, app.version, fetcher),
53
+ }, { id: component.name }),
54
+ ]),
55
+ });
56
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The elements an application's code opened beside the conversation (LOOP
3
+ * P-18): a side panel on the trailing edge, and a page of its own over the
4
+ * workspace — each drawn from the same A2UI surface an answer is drawn from
5
+ * (`InlineSurface`), its buttons sent to the application's actions as an
6
+ * answer's are (`answerAction`), and closed with its own button.
7
+ *
8
+ * Drawn from the workspace's root slot, positioned against the workspace:
9
+ * nothing is drawn, and nothing is laid out, while nothing is open.
10
+ *
11
+ * @module loop/plugins/app-elements/AppElements
12
+ */
13
+ import type { JSX } from 'react';
14
+ import { type LoopElement } from '../../../chat/base/loopElement';
15
+ import type { LoopWorkspaceContext } from '../../core';
16
+ /** How wide the side panel is, at most. */
17
+ export declare const APP_PANEL_WIDTH = 380;
18
+ /** What is open now: the panels in the order they opened, and the last page. */
19
+ export declare function elementsByPlace(open: readonly LoopElement[]): {
20
+ panels: LoopElement[];
21
+ page: LoopElement | null;
22
+ };
23
+ /** Every element open in a side panel or on a page, drawn where it belongs. */
24
+ export declare function AppElements({ workspace, }: {
25
+ workspace?: LoopWorkspaceContext;
26
+ }): JSX.Element | null;
27
+ export default AppElements;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useSyncExternalStore } from 'react';
3
+ import { IconButton, Text } from '@primer/react';
4
+ import { XIcon } from '@primer/octicons-react';
5
+ import { Box } from '@datalayer/primer-addons';
6
+ import { closeLoopElement, onLoopElements, openLoopElements, } from '../../../chat/base/loopElement';
7
+ import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
8
+ import { answerAction, readA2uiToolResult } from '../a2ui-surface/toolResult';
9
+ import { useOwnCatalog } from '../a2ui-surface/ownComponents';
10
+ /** How wide the side panel is, at most. */
11
+ export const APP_PANEL_WIDTH = 380;
12
+ /** What is open now: the panels in the order they opened, and the last page. */
13
+ export function elementsByPlace(open) {
14
+ const pages = open.filter(element => element.where === 'page');
15
+ return {
16
+ panels: open.filter(element => element.where === 'panel'),
17
+ page: pages.length > 0 ? pages[pages.length - 1] : null,
18
+ };
19
+ }
20
+ function ElementBody({ element, send, }) {
21
+ const parsed = useMemo(() => readA2uiToolResult(element.shows, SURFACE_CATALOG_ID), [element.shows]);
22
+ // With the components its developer wrote (P-17).
23
+ const catalog = useOwnCatalog();
24
+ const onAction = (action) => {
25
+ // A button of what its code shows: its action goes to the application,
26
+ // as a button of an answer does (LOOP P-04).
27
+ const pressed = answerAction(action);
28
+ if (pressed)
29
+ send?.(pressed.message, pressed.forwardedProps);
30
+ };
31
+ if (!parsed?.messages || parsed.messages.length === 0) {
32
+ return _jsx(Text, { sx: { color: 'fg.muted' }, children: "Nothing to show." });
33
+ }
34
+ return (_jsx(InlineSurface
35
+ // Drawn again whole when the code changes it.
36
+ , { messages: parsed.messages, onAction: onAction, catalog: catalog }, JSON.stringify(element.shows)));
37
+ }
38
+ function ElementHeader({ element }) {
39
+ return (_jsxs(Box, { sx: {
40
+ display: 'flex',
41
+ alignItems: 'center',
42
+ justifyContent: 'space-between',
43
+ gap: 2,
44
+ mb: 2,
45
+ }, children: [_jsx(Text, { as: "h2", sx: { fontSize: 2, fontWeight: 'bold', m: 0 }, children: element.title }), _jsx(IconButton, { icon: XIcon, variant: "invisible", size: "small", "aria-label": `Close ${element.title}`, onClick: () => closeLoopElement(element.id) })] }));
46
+ }
47
+ /** Every element open in a side panel or on a page, drawn where it belongs. */
48
+ export function AppElements({ workspace, }) {
49
+ const open = useSyncExternalStore(onLoopElements, openLoopElements, openLoopElements);
50
+ const { panels, page } = elementsByPlace(open);
51
+ const send = workspace?.viewControls.send;
52
+ if (panels.length === 0 && page === null)
53
+ return null;
54
+ return (_jsxs(_Fragment, { children: [panels.length > 0 ? (_jsx(Box, { as: "aside", "aria-label": "Side panel", sx: {
55
+ position: 'absolute',
56
+ top: 0,
57
+ right: 0,
58
+ bottom: 0,
59
+ width: `min(${APP_PANEL_WIDTH}px, 100%)`,
60
+ zIndex: 1,
61
+ overflowY: 'auto',
62
+ borderLeft: '1px solid',
63
+ borderColor: 'border.default',
64
+ bg: 'canvas.subtle',
65
+ boxShadow: 'shadow.medium',
66
+ p: 3,
67
+ display: 'flex',
68
+ flexDirection: 'column',
69
+ gap: 3,
70
+ }, children: panels.map(element => (_jsxs(Box, { as: "section", "aria-label": element.title, children: [_jsx(ElementHeader, { element: element }), _jsx(ElementBody, { element: element, send: send })] }, element.id))) })) : null, page !== null ? (_jsxs(Box, { role: "dialog", "aria-label": page.title, sx: {
71
+ position: 'absolute',
72
+ inset: 0,
73
+ zIndex: 2,
74
+ overflowY: 'auto',
75
+ bg: 'canvas.default',
76
+ p: [3, 4],
77
+ }, children: [_jsx(ElementHeader, { element: page }), _jsx(ElementBody, { element: page, send: send })] })) : null] }));
78
+ }
79
+ export default AppElements;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `app-elements` — what an application's code opens beside the
3
+ * conversation (LOOP P-18): `session.show(..., where="panel")` a side panel,
4
+ * `where="page"` a page of its own over the workspace, each closed by the
5
+ * code (`session.close`) or by the person. Inline, an element is a message,
6
+ * which the chat draws (P-04); this plugin draws the other two.
7
+ *
8
+ * The chat keeps what is open (`chat/base/loopElement`), from the session
9
+ * API's `loop.element` events; this plugin draws it from the workspace's
10
+ * root slot, positioned against the workspace, so that nothing is laid out
11
+ * while nothing is open.
12
+ *
13
+ * @module loop/plugins/app-elements
14
+ */
15
+ import { type ReactorPlugin } from '@datalayer/reactor';
16
+ export declare const APP_ELEMENTS_PLUGIN_NAME = "@datalayer/loop-plugin-app-elements";
17
+ /** The plugin that draws an application's side panel and pages. */
18
+ export declare const AppElementsPlugin: ReactorPlugin<Record<string, never>, unknown, unknown>;
19
+ export { AppElements, APP_PANEL_WIDTH, elementsByPlace } from './AppElements';
20
+ export default AppElementsPlugin;
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * `app-elements` — what an application's code opens beside the
7
+ * conversation (LOOP P-18): `session.show(..., where="panel")` a side panel,
8
+ * `where="page"` a page of its own over the workspace, each closed by the
9
+ * code (`session.close`) or by the person. Inline, an element is a message,
10
+ * which the chat draws (P-04); this plugin draws the other two.
11
+ *
12
+ * The chat keeps what is open (`chat/base/loopElement`), from the session
13
+ * API's `loop.element` events; this plugin draws it from the workspace's
14
+ * root slot, positioned against the workspace, so that nothing is laid out
15
+ * while nothing is open.
16
+ *
17
+ * @module loop/plugins/app-elements
18
+ */
19
+ import { definePlugin } from '@datalayer/reactor';
20
+ import { clearLoopElements } from '../../../chat/base/loopElement';
21
+ import { LoopSlots } from '../../core';
22
+ import { AppElements } from './AppElements';
23
+ export const APP_ELEMENTS_PLUGIN_NAME = '@datalayer/loop-plugin-app-elements';
24
+ /** The plugin that draws an application's side panel and pages. */
25
+ export const AppElementsPlugin = definePlugin({
26
+ name: APP_ELEMENTS_PLUGIN_NAME,
27
+ displayName: 'Side panel and pages',
28
+ description: 'What an application opens beside the conversation: a side panel, or a page of its own.',
29
+ octicon: 'sidebar-expand',
30
+ build: () => ({
31
+ components: [
32
+ {
33
+ id: 'app-elements',
34
+ slot: LoopSlots.root,
35
+ Component: AppElements,
36
+ },
37
+ ],
38
+ }),
39
+ // A workspace that goes takes what it had open with it.
40
+ register: () => clearLoopElements,
41
+ });
42
+ export { AppElements, APP_PANEL_WIDTH, elementsByPlace } from './AppElements';
43
+ export default AppElementsPlugin;
@@ -9,12 +9,21 @@
9
9
  * palette (`loop.canvas.block`, R-01b) and nothing else, so that what the
10
10
  * Canvas can place and what the page can draw are one list: a page that
11
11
  * uses a block no enabled plugin contributes says so, in place of the page.
12
+ * The components its developer wrote (LOOP P-17) are among them, contributed
13
+ * by its own plugin (`app-components`) with their renderers.
14
+ *
15
+ * A widget's page written in its code (LOOP P-05) runs in a session of its
16
+ * own, beside the conversation: as an input changes, the page sends them all
17
+ * to the session's `page` action and shows what its code answered at
18
+ * `/outputs/<name>`, in place — no message is sent (`./pageRun`).
12
19
  *
13
20
  * @module loop/plugins/app-page/AppPage
14
21
  */
15
22
  import type { JSX } from 'react';
16
23
  import type { AppSpec } from '../../../types/agentspecs';
17
24
  import { type LoopWorkspaceContext } from '../../core';
25
+ /** How long the page waits after an input changed before it runs (P-05). */
26
+ export declare const PAGE_RUN_DELAY_MS = 300;
18
27
  /** The components a page's messages place, each once. */
19
28
  export declare function componentsOnPage(messages: readonly unknown[]): string[];
20
29
  /**
@@ -1,12 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
3
4
  import { Box } from '@datalayer/primer-addons';
4
5
  import { signal } from '@datalayer/reactor';
5
6
  import { useContributions, useSignalValue } from '@datalayer/reactor/react';
6
7
  import { catalogOfBlocks } from '../../../components/a2ui';
7
8
  import { LoopCanvasBlock, LoopChatTurn, } from '../../core';
8
9
  import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
9
- import { appPageAction, appPageData, appPageMessages } from './appPageModel';
10
+ import { useOwnComponents } from '../a2ui-surface/ownComponents';
11
+ import { appPageAction, appPageData, appPageMessages, pageInputsOf, pageOf, } from './appPageModel';
12
+ import { openPageSession, outputsData, runPageIn, } from './pageRun';
13
+ /** How long the page waits after an input changed before it runs (P-05). */
14
+ export const PAGE_RUN_DELAY_MS = 300;
10
15
  /** No chat in the workspace: the page reads a turn that never starts. */
11
16
  const NO_TURN = signal({ id: 0, status: 'idle' });
12
17
  const NO_CONVERSATION = signal([]);
@@ -42,13 +47,15 @@ export function AppPage({ app, workspace }) {
42
47
  const blocks = useContributions(LoopCanvasBlock);
43
48
  const contributed = [...new Set(blocks.map(entry => entry.value.id))].sort();
44
49
  const drawnWith = contributed.join(',');
50
+ // The renderers of the components its developer wrote (P-17).
51
+ const own = useOwnComponents();
45
52
  const drawing = useMemo(() => {
46
53
  const missing = blocksMissing(componentsOnPage(messages), contributed);
47
54
  if (missing) {
48
55
  return { problem: missing };
49
56
  }
50
57
  try {
51
- return { catalog: catalogOfBlocks(contributed) };
58
+ return { catalog: catalogOfBlocks(contributed, own) };
52
59
  }
53
60
  catch (error) {
54
61
  return {
@@ -56,9 +63,97 @@ export function AppPage({ app, workspace }) {
56
63
  };
57
64
  }
58
65
  // eslint-disable-next-line react-hooks/exhaustive-deps
59
- }, [messages, drawnWith]);
60
- const data = useMemo(() => appPageData(app, turn, conversation), [app, turn, conversation]);
66
+ }, [messages, drawnWith, own]);
61
67
  const [refusal, setRefusal] = useState(null);
68
+ // A widget's page written in its code (P-05): its outputs, as last answered.
69
+ const page = pageOf(app);
70
+ const [outputs, setOutputs] = useState({});
71
+ const data = useMemo(() => ({
72
+ ...appPageData(app, turn, conversation),
73
+ ...outputsData(outputs),
74
+ }), [app, turn, conversation, outputs]);
75
+ const token = useIAMStore(state => state.token) ?? '';
76
+ const pageContext = useRef({ serverUrl: '', agentId: '' });
77
+ pageContext.current = {
78
+ serverUrl: workspace.sandbox?.agentBaseUrl || workspace.serverUrl || '',
79
+ agentId: workspace.agentId || app.id,
80
+ token: token || undefined,
81
+ };
82
+ const pageSession = useRef(null);
83
+ const pageRunning = useRef(false);
84
+ const pageNext = useRef(null);
85
+ const pageSent = useRef('');
86
+ const alive = useRef(true);
87
+ // The latest inputs win: one run at a time, the last ones written next.
88
+ const runPage = useCallback((inputs) => {
89
+ pageNext.current = inputs;
90
+ if (pageRunning.current) {
91
+ return;
92
+ }
93
+ pageRunning.current = true;
94
+ void (async () => {
95
+ while (pageNext.current && alive.current) {
96
+ const next = pageNext.current;
97
+ pageNext.current = null;
98
+ const context = pageContext.current;
99
+ try {
100
+ pageSession.current ??= openPageSession(context);
101
+ const turn = await runPageIn(context, await pageSession.current, next);
102
+ if (!alive.current)
103
+ break;
104
+ if ('refused' in turn) {
105
+ setRefusal(turn.refused);
106
+ }
107
+ else {
108
+ setRefusal(null);
109
+ setOutputs(turn.outputs);
110
+ }
111
+ }
112
+ catch (error) {
113
+ // No session: the next change opens one again.
114
+ pageSession.current = null;
115
+ if (alive.current) {
116
+ setRefusal(error instanceof Error ? error.message : String(error));
117
+ }
118
+ }
119
+ }
120
+ pageRunning.current = false;
121
+ })();
122
+ }, []);
123
+ // A live page runs as its inputs change, from the start (their defaults).
124
+ const unwatch = useRef(null);
125
+ useEffect(() => {
126
+ alive.current = true;
127
+ return () => {
128
+ alive.current = false;
129
+ unwatch.current?.();
130
+ };
131
+ }, []);
132
+ const onSurface = useCallback((surface) => {
133
+ if (!page?.live) {
134
+ return;
135
+ }
136
+ unwatch.current?.();
137
+ let timer;
138
+ const changed = (value) => {
139
+ const inputs = pageInputsOf(app, value);
140
+ const said = JSON.stringify(inputs);
141
+ if (said === pageSent.current) {
142
+ return;
143
+ }
144
+ clearTimeout(timer);
145
+ timer = setTimeout(() => {
146
+ pageSent.current = said;
147
+ runPage(inputs);
148
+ }, PAGE_RUN_DELAY_MS);
149
+ };
150
+ const watching = surface.dataModel.subscribe('/inputs', changed);
151
+ changed(watching.value);
152
+ unwatch.current = () => {
153
+ clearTimeout(timer);
154
+ watching.unsubscribe();
155
+ };
156
+ }, [app, page?.live, runPage]);
62
157
  // The workspace changes as the chat reports itself; the handler reads the latest.
63
158
  const workspaceRef = useRef(workspace);
64
159
  workspaceRef.current = workspace;
@@ -72,7 +167,10 @@ export function AppPage({ app, workspace }) {
72
167
  return;
73
168
  }
74
169
  setRefusal(null);
75
- if ('stop' in outcome) {
170
+ if ('runPage' in outcome) {
171
+ runPage(outcome.runPage);
172
+ }
173
+ else if ('stop' in outcome) {
76
174
  controls.stop?.();
77
175
  }
78
176
  else if ('newChat' in outcome) {
@@ -102,9 +200,9 @@ export function AppPage({ app, workspace }) {
102
200
  surface?.dataModel.set(path, '');
103
201
  }
104
202
  }
105
- }, [app]);
203
+ }, [app, runPage]);
106
204
  return (_jsx(Box, { "data-testid": "app-page", sx: { height: '100%', minHeight: 0, overflow: 'auto', p: 3 }, children: 'problem' in drawing ? (_jsx(Box, { role: "status", sx: { color: 'fg.muted', fontSize: 1 }, children: drawing.problem })) : (_jsx(InlineSurface
107
205
  // A new list of blocks is a new surface, drawn with it.
108
- , { messages: messages, data: data, onAction: onAction, validationError: refusal, catalog: drawing.catalog }, drawnWith)) }));
206
+ , { messages: messages, data: data, onAction: onAction, validationError: refusal, catalog: drawing.catalog, ...(page ? { onSurface } : {}) }, drawnWith)) }));
109
207
  }
110
208
  export default AppPage;
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * @module loop/plugins/app-page/appPageModel
25
25
  */
26
- import type { AppFormField, AppFormSchema, AppSpec } from '../../../types/agentspecs';
26
+ import type { AppFormField, AppFormSchema, AppPageSpec, AppSpec } from '../../../types/agentspecs';
27
27
  import type { ChatTurnSnapshot, ConversationEntry } from '../../core';
28
28
  /** The id of the editor surface an application's page is drawn in. */
29
29
  export declare const APP_PAGE_SURFACE = "app-page";
@@ -92,6 +92,12 @@ export declare function settingDefaults(settings: AppFormSchema | undefined): Re
92
92
  * shown and written. Empty lists for a decision, or a kind not known.
93
93
  */
94
94
  export declare function appKindPaths(app: Pick<AppSpec, 'kind' | 'interface'>): AppKindPaths;
95
+ /** The page its code writes (P-05): a widget's, or none. */
96
+ export declare const pageOf: (app: Pick<AppSpec, "kind" | "interface">) => AppPageSpec | undefined;
97
+ /** The path an output of its page is shown at (P-05). */
98
+ export declare const outputPath: (name: string) => string;
99
+ /** The action that runs a widget's page written in its code (P-05). */
100
+ export declare const PAGE_ACTION = "page";
95
101
  /**
96
102
  * Whether the workspace draws the application's page: a chat, a widget or a
97
103
  * worker whose layout has one (`page`, `split`). A `chat` layout is the
@@ -149,6 +155,12 @@ export declare function givenFiles(files: unknown): {
149
155
  * them against the settings' form.
150
156
  */
151
157
  export declare function settingsOf(app: Pick<AppSpec, 'interface'>, inputs: unknown): Record<string, unknown>;
158
+ /**
159
+ * The inputs of a widget's page written in its code (P-05), as the page
160
+ * holds them, typed as its form says, for the session's `page` action: the
161
+ * runtime checks them against the same form.
162
+ */
163
+ export declare function pageInputsOf(app: Pick<AppSpec, 'kind' | 'interface'>, inputs: unknown): Record<string, unknown>;
152
164
  /**
153
165
  * What the page did besides its message, for the session API: the action of
154
166
  * the block pressed, the files given with it, the settings it holds. Sent
@@ -182,6 +194,10 @@ export type AppPageOutcome = {
182
194
  stop: true;
183
195
  } | {
184
196
  newChat: true;
197
+ }
198
+ /** A widget's page, to run on these inputs (P-05): no message is sent. */
199
+ | {
200
+ runPage: Record<string, unknown>;
185
201
  } | {
186
202
  refused: string;
187
203
  };
@@ -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 { pageOutputShows } from '../../apps/appspec';
5
6
  /** The id of the editor surface an application's page is drawn in. */
6
7
  export const APP_PAGE_SURFACE = 'app-page';
7
8
  /** The id of the A2UI surface itself, inside it. */
@@ -178,12 +179,49 @@ export function appKindPaths(app) {
178
179
  meaning: `The setting “${settingTitle(id, field)}”.`,
179
180
  words: settingTitle(id, field),
180
181
  }));
182
+ // A widget's page written in its code (P-05): its inputs taken, its
183
+ // outputs published, and the action that runs it.
184
+ const page = pageOf(app);
185
+ if (!page) {
186
+ return {
187
+ ...kind,
188
+ publishes: [...kind.publishes, ...inputs],
189
+ accepts: [...kind.accepts, ...inputs],
190
+ };
191
+ }
192
+ const pageInputs = settingFields(page.inputs).map(([id, field]) => ({
193
+ path: inputPath(id),
194
+ meaning: `The input “${settingTitle(id, field)}” of its page.`,
195
+ words: settingTitle(id, field),
196
+ }));
197
+ const outputs = page.outputs.map(output => ({
198
+ path: outputPath(output.name),
199
+ meaning: `What its page shows as “${output.title || output.name}”, drawn with a ${output.component}, for its inputs.`,
200
+ words: output.title || output.name,
201
+ ...(output.component === 'Table' || output.component === 'Chart'
202
+ ? { list: true }
203
+ : {}),
204
+ }));
181
205
  return {
182
206
  ...kind,
183
- publishes: [...kind.publishes, ...inputs],
184
- accepts: [...kind.accepts, ...inputs],
207
+ publishes: [...kind.publishes, ...inputs, ...pageInputs, ...outputs],
208
+ accepts: [...kind.accepts, ...inputs, ...pageInputs],
209
+ actions: [...kind.actions, PAGE],
185
210
  };
186
211
  }
212
+ /** The page its code writes (P-05): a widget's, or none. */
213
+ export const pageOf = (app) => app.kind === 'widget' ? app.interface.page : undefined;
214
+ /** The path an output of its page is shown at (P-05). */
215
+ export const outputPath = (name) => `/outputs/${name}`;
216
+ /** The action that runs a widget's page written in its code (P-05). */
217
+ export const PAGE_ACTION = 'page';
218
+ const PAGE = {
219
+ name: PAGE_ACTION,
220
+ meaning: 'Run its page on its inputs: every value written under /inputs, its outputs shown in place at /outputs.',
221
+ words: 'Run its page',
222
+ context: [],
223
+ asks: {},
224
+ };
187
225
  /** The components its surface has, or none. */
188
226
  const surfaceComponents = (app) => app.interface.surface?.components ?? [];
189
227
  /**
@@ -257,7 +295,10 @@ export function appPageInitialData(app) {
257
295
  // Each setting at its default, as the settings' form holds it; a
258
296
  // ChoicePicker a builder placed at a setting holds a list of the values
259
297
  // chosen.
260
- const inputs = settingDefaults(app.interface.settings);
298
+ const inputs = {
299
+ ...settingDefaults(app.interface.settings),
300
+ ...settingDefaults(pageOf(app)?.inputs),
301
+ };
261
302
  for (const node of app.interface.surface?.components ?? []) {
262
303
  const bound = node.value?.path;
263
304
  const id = typeof bound === 'string' && bound.startsWith('/inputs/')
@@ -269,6 +310,9 @@ export function appPageInitialData(app) {
269
310
  }
270
311
  tree.inputs = inputs;
271
312
  }
313
+ if (pageOf(app)) {
314
+ tree.outputs = {};
315
+ }
272
316
  if (paths.accepts.some(entry => entry.path === DRAFT.path)) {
273
317
  tree.draft = '';
274
318
  }
@@ -362,11 +406,23 @@ export function givenFiles(files) {
362
406
  * them against the settings' form.
363
407
  */
364
408
  export function settingsOf(app, inputs) {
409
+ return formValuesOf(app.interface.settings, inputs);
410
+ }
411
+ /**
412
+ * The inputs of a widget's page written in its code (P-05), as the page
413
+ * holds them, typed as its form says, for the session's `page` action: the
414
+ * runtime checks them against the same form.
415
+ */
416
+ export function pageInputsOf(app, inputs) {
417
+ return formValuesOf(pageOf(app)?.inputs, inputs);
418
+ }
419
+ /** A form's values as the page holds them, each typed as its field says. */
420
+ function formValuesOf(form, inputs) {
365
421
  const values = inputs && typeof inputs === 'object'
366
422
  ? inputs
367
423
  : {};
368
424
  const settings = {};
369
- for (const [id, field] of settingFields(app.interface.settings)) {
425
+ for (const [id, field] of settingFields(form)) {
370
426
  const held = values[id];
371
427
  if (held === undefined || held === null) {
372
428
  continue;
@@ -425,6 +481,8 @@ export function appPageAction(app, event, read, lastInputs = '') {
425
481
  ...(settings ? { settings } : {}),
426
482
  });
427
483
  switch (event.name) {
484
+ case PAGE_ACTION:
485
+ return { runPage: pageInputsOf(app, read('/inputs')) };
428
486
  case 'stop':
429
487
  return { stop: true };
430
488
  case 'new':
@@ -486,17 +544,72 @@ const button = (id, label, name, primary = false) => [
486
544
  * filled — no action, no button: they go with the next run.
487
545
  */
488
546
  function settingsForm(app) {
547
+ const page = pageOf(app);
548
+ if (page) {
549
+ return pageForm(app, page);
550
+ }
489
551
  return app.interface.settings && settingFields(app.interface.settings).length
490
552
  ? [
491
553
  {
492
554
  id: 'settings',
493
555
  component: 'Form',
494
556
  schema: app.interface.settings,
557
+ // Drawn with the inputs its uiSchema names (P-20).
558
+ ...(app.interface.settingsUi ? { ui: app.interface.settingsUi } : {}),
495
559
  values: { path: '/inputs' },
496
560
  },
497
561
  ]
498
562
  : [];
499
563
  }
564
+ /**
565
+ * A widget's page written in its code (P-05): one Form of its settings and
566
+ * its inputs, written at `/inputs` as they are filled — the page runs its
567
+ * code on them as they change — then each output, under its title, drawn
568
+ * with its component from `/outputs/<name>`; a Run button when it is not
569
+ * live.
570
+ */
571
+ function pageForm(app, page) {
572
+ const settings = app.interface.settings;
573
+ const schema = {
574
+ ...page.inputs,
575
+ type: 'object',
576
+ properties: { ...settings?.properties, ...page.inputs.properties },
577
+ required: [...(settings?.required ?? []), ...(page.inputs.required ?? [])],
578
+ };
579
+ const ui = { ...app.interface.settingsUi, ...page.inputsUi };
580
+ const outputs = page.outputs.flatMap(output => [
581
+ ...(output.title
582
+ ? [
583
+ {
584
+ id: `output-${output.name}-title`,
585
+ component: 'Text',
586
+ text: output.title,
587
+ variant: 'h4',
588
+ },
589
+ ]
590
+ : []),
591
+ {
592
+ ...output.props,
593
+ id: `output-${output.name}`,
594
+ component: output.component,
595
+ [pageOutputShows(output.component, app.interface.customComponents) ??
596
+ 'text']: {
597
+ path: outputPath(output.name),
598
+ },
599
+ },
600
+ ]);
601
+ return [
602
+ {
603
+ id: 'inputs',
604
+ component: 'Form',
605
+ schema,
606
+ ...(Object.keys(ui).length > 0 ? { ui } : {}),
607
+ values: { path: '/inputs' },
608
+ },
609
+ ...(page.live ? [] : button('run-page', 'Run', PAGE_ACTION, true)),
610
+ ...outputs,
611
+ ];
612
+ }
500
613
  /**
501
614
  * The page drawn when the application has a page layout and no surface
502
615
  * composed: what its kind publishes, its settings as inputs, and the
@@ -510,6 +623,11 @@ export function defaultAppSurface(app) {
510
623
  body.push(text('question', '/question', 'h3'), text('answer', '/answer'), text('status', '/status', 'caption'), ...inputs);
511
624
  break;
512
625
  case 'widget':
626
+ if (pageOf(app)) {
627
+ // Its page is its code's (P-05): the inputs, and what it shows for them.
628
+ body.push(...inputs);
629
+ break;
630
+ }
513
631
  body.push(...inputs, { id: 'actions', component: 'Row', children: ['run', 'stop'] }, ...button('run', 'Run', 'run', true), ...button('stop', 'Stop', 'stop'), text('status', '/status', 'caption'), text('output', '/output'));
514
632
  break;
515
633
  case 'worker':
@@ -41,4 +41,5 @@ export type AppHostPageProps = {
41
41
  export declare function defineAppHostPagePlugin(app: AppSpec, Page: ComponentType<AppHostPageProps>): ReactorPlugin<Record<string, never>, unknown, unknown>;
42
42
  export { AppPage, type AppPageProps } from './AppPage';
43
43
  export * from './appPageModel';
44
+ export * from './pageRun';
44
45
  export default defineAppPagePlugin;
@@ -62,4 +62,5 @@ export function defineAppHostPagePlugin(app, Page) {
62
62
  }
63
63
  export { AppPage } from './AppPage';
64
64
  export * from './appPageModel';
65
+ export * from './pageRun';
65
66
  export default defineAppPagePlugin;