@datalayer/agent-runtimes 1.3.84 → 1.3.86

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 (162) 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 +14 -11
  17. package/lib/chat/display/EmptyState.js +3 -1
  18. package/lib/chat/index.d.ts +2 -0
  19. package/lib/chat/index.js +3 -0
  20. package/lib/chat/messages/ChatMessages.js +3 -1
  21. package/lib/chat/messages/TurnFooter.js +3 -1
  22. package/lib/chat/prompt/InputPrompt.js +3 -1
  23. package/lib/chat/prompt/InputPromptLexical.js +3 -1
  24. package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
  25. package/lib/chat/tools/ToolApprovalBanner.js +5 -3
  26. package/lib/chat/tools/ToolApprovalDialog.js +5 -3
  27. package/lib/chat/tools/ToolCallDisplay.js +29 -25
  28. package/lib/chat/words.d.ts +102 -0
  29. package/lib/chat/words.js +460 -0
  30. package/lib/components/a2ui/custom/index.d.ts +39 -0
  31. package/lib/components/a2ui/custom/index.js +95 -0
  32. package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
  33. package/lib/components/a2ui/custom/sandbox.js +188 -0
  34. package/lib/components/a2ui/datalayer/index.d.ts +1 -1
  35. package/lib/components/a2ui/datalayer/index.js +1 -1
  36. package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
  37. package/lib/components/a2ui/datalayer/schema.js +12 -1
  38. package/lib/components/a2ui/index.d.ts +1 -0
  39. package/lib/components/a2ui/index.js +1 -0
  40. package/lib/components/a2ui/visibleWhen.d.ts +12 -1
  41. package/lib/components/a2ui/visibleWhen.js +24 -3
  42. package/lib/components/inspector/AgentInspector.d.ts +25 -44
  43. package/lib/components/inspector/AgentInspector.js +27 -215
  44. package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
  45. package/lib/components/inspector/AgentInspectorDialog.js +2 -2
  46. package/lib/components/inspector/a2aSpans.d.ts +51 -0
  47. package/lib/components/inspector/a2aSpans.js +373 -0
  48. package/lib/components/inspector/agentSpans.d.ts +120 -0
  49. package/lib/components/inspector/agentSpans.js +253 -0
  50. package/lib/components/inspector/chatSpans.d.ts +36 -0
  51. package/lib/components/inspector/chatSpans.js +50 -0
  52. package/lib/components/inspector/classify.d.ts +11 -8
  53. package/lib/components/inspector/classify.js +23 -24
  54. package/lib/components/inspector/index.d.ts +7 -7
  55. package/lib/components/inspector/index.js +7 -7
  56. package/lib/components/teams/A2ATeamGraph.d.ts +117 -22
  57. package/lib/components/teams/A2ATeamGraph.js +217 -97
  58. package/lib/components/teams/SceneTranscript.d.ts +29 -0
  59. package/lib/components/teams/SceneTranscript.js +54 -0
  60. package/lib/components/teams/SceneView.d.ts +41 -0
  61. package/lib/components/teams/SceneView.js +28 -0
  62. package/lib/components/teams/index.d.ts +4 -0
  63. package/lib/components/teams/index.js +4 -0
  64. package/lib/components/teams/sceneStage.d.ts +120 -0
  65. package/lib/components/teams/sceneStage.js +170 -0
  66. package/lib/components/teams/sceneTranscript.d.ts +74 -0
  67. package/lib/components/teams/sceneTranscript.js +294 -0
  68. package/lib/components/teams/teamConnections.d.ts +2 -2
  69. package/lib/components/teams/teamConnections.js +3 -3
  70. package/lib/components/teams/useA2ATeam.d.ts +94 -22
  71. package/lib/components/teams/useA2ATeam.js +356 -140
  72. package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
  73. package/lib/examples/AgentA2ATeamExample.js +70 -60
  74. package/lib/examples/AssistantExample.js +2 -2
  75. package/lib/examples/ScenesExample.d.ts +26 -0
  76. package/lib/examples/ScenesExample.js +241 -0
  77. package/lib/examples/example-selector.js +1 -0
  78. package/lib/examples/exampleGroups.js +3 -0
  79. package/lib/examples/index.d.ts +1 -0
  80. package/lib/examples/index.js +1 -0
  81. package/lib/examples/main.js +4 -0
  82. package/lib/loop/apps/AppComposer.d.ts +2 -1
  83. package/lib/loop/apps/AppComposer.js +6 -3
  84. package/lib/loop/apps/AppRenderer.d.ts +2 -1
  85. package/lib/loop/apps/AppRenderer.js +34 -25
  86. package/lib/loop/apps/appspec.d.ts +11 -1
  87. package/lib/loop/apps/appspec.js +115 -1
  88. package/lib/loop/apps/checks.d.ts +8 -0
  89. package/lib/loop/apps/checks.js +106 -4
  90. package/lib/loop/apps/customComponents.d.ts +41 -0
  91. package/lib/loop/apps/customComponents.js +260 -0
  92. package/lib/loop/apps/hostTools.d.ts +8 -1
  93. package/lib/loop/apps/hostTools.js +9 -0
  94. package/lib/loop/apps/index.d.ts +1 -0
  95. package/lib/loop/apps/index.js +1 -0
  96. package/lib/loop/core/a2uiComponents.d.ts +10 -0
  97. package/lib/loop/core/a2uiComponents.js +15 -0
  98. package/lib/loop/core/index.d.ts +1 -0
  99. package/lib/loop/core/index.js +3 -0
  100. package/lib/loop/embed/AppEmbed.d.ts +11 -3
  101. package/lib/loop/embed/AppEmbed.js +42 -16
  102. package/lib/loop/embed/element.d.ts +5 -0
  103. package/lib/loop/embed/element.js +61 -5
  104. package/lib/loop/embed/embedConfig.d.ts +8 -0
  105. package/lib/loop/embed/embedConfig.js +18 -0
  106. package/lib/loop/embed/hostBridge.d.ts +28 -2
  107. package/lib/loop/embed/hostBridge.js +22 -2
  108. package/lib/loop/index.d.ts +4 -3
  109. package/lib/loop/index.js +4 -3
  110. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
  111. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
  112. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
  113. package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
  114. package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
  115. package/lib/loop/plugins/app-components/index.d.ts +26 -0
  116. package/lib/loop/plugins/app-components/index.js +56 -0
  117. package/lib/loop/plugins/app-elements/AppElements.js +4 -1
  118. package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
  119. package/lib/loop/plugins/app-page/AppPage.js +106 -8
  120. package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
  121. package/lib/loop/plugins/app-page/appPageModel.js +120 -4
  122. package/lib/loop/plugins/app-page/index.d.ts +1 -0
  123. package/lib/loop/plugins/app-page/index.js +1 -0
  124. package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
  125. package/lib/loop/plugins/app-page/pageRun.js +111 -0
  126. package/lib/loop/plugins/assistant/index.js +8 -6
  127. package/lib/loop/plugins/chat/ChatView.js +6 -4
  128. package/lib/loop/plugins/chat/index.d.ts +1 -1
  129. package/lib/loop/plugins/chat/index.js +2 -1
  130. package/lib/models/PageTag.d.ts +1 -1
  131. package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
  132. package/lib/specs/actions.js +78 -1
  133. package/lib/specs/apps.d.ts +5 -0
  134. package/lib/specs/apps.js +1384 -231
  135. package/lib/specs/appspecSchema.js +191 -1
  136. package/lib/specs/index.d.ts +1 -0
  137. package/lib/specs/index.js +1 -0
  138. package/lib/specs/mcpServers.js +8 -2
  139. package/lib/specs/scenes.d.ts +21 -0
  140. package/lib/specs/scenes.js +1191 -0
  141. package/lib/specs/teams/teams.d.ts +3 -0
  142. package/lib/specs/teams/teams.js +249 -0
  143. package/lib/types/agentspecs.d.ts +70 -0
  144. package/lib/types/chat.d.ts +6 -5
  145. package/lib/types/index.d.ts +1 -0
  146. package/lib/types/index.js +1 -0
  147. package/lib/types/scenes.d.ts +207 -0
  148. package/lib/types/scenes.js +5 -0
  149. package/lib/types/teams.d.ts +13 -2
  150. package/package.json +4 -4
  151. package/scripts/codegen/generate_agents.py +1 -0
  152. package/scripts/codegen/generate_apps.py +4 -2
  153. package/scripts/codegen/generate_scenes.py +301 -0
  154. package/scripts/codegen/generate_teams.py +15 -2
  155. package/lib/components/inspector/JsonTree.d.ts +0 -24
  156. package/lib/components/inspector/JsonTree.js +0 -89
  157. package/lib/components/inspector/a2aInspect.d.ts +0 -56
  158. package/lib/components/inspector/a2aInspect.js +0 -478
  159. package/lib/components/inspector/agentInspector.d.ts +0 -179
  160. package/lib/components/inspector/agentInspector.js +0 -267
  161. package/lib/components/inspector/chatInspect.d.ts +0 -38
  162. package/lib/components/inspector/chatInspect.js +0 -65
@@ -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,6 +544,10 @@ 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
  {
@@ -499,6 +561,55 @@ function settingsForm(app) {
499
561
  ]
500
562
  : [];
501
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
+ }
502
613
  /**
503
614
  * The page drawn when the application has a page layout and no surface
504
615
  * composed: what its kind publishes, its settings as inputs, and the
@@ -512,6 +623,11 @@ export function defaultAppSurface(app) {
512
623
  body.push(text('question', '/question', 'h3'), text('answer', '/answer'), text('status', '/status', 'caption'), ...inputs);
513
624
  break;
514
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
+ }
515
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'));
516
632
  break;
517
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;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A widget's page written in its code, run on its inputs (LOOP P-05).
3
+ *
4
+ * The page opens a session of the application's agent on its runtime
5
+ * (`POST /api/v1/apps/sessions`), then, each time an input changes, sends
6
+ * them all as the session's `page` action (`POST …/sessions/{uid}/actions`,
7
+ * `{name: "page", payload: {inputs}}`). The runtime checks them against the
8
+ * page's form — refused with 422 and a sentence — runs its code's
9
+ * `@app.page` on them, and answers its outputs as a `CUSTOM` event named
10
+ * `loop.page`, `{inputs, outputs}`, which the page shows in place. Nothing is
11
+ * said in the conversation.
12
+ *
13
+ * @module loop/plugins/app-page/pageRun
14
+ */
15
+ /** The `CUSTOM` event that says what a page shows. */
16
+ export declare const LOOP_PAGE = "loop.page";
17
+ /** Where the page reaches the application's session API, and as whom. */
18
+ export type PageRunContext = {
19
+ /** The agent-runtimes server that runs the application. */
20
+ serverUrl: string;
21
+ /** The application's agent there. */
22
+ agentId: string;
23
+ /** The person's token; none on the machine itself. */
24
+ token?: string;
25
+ fetcher?: typeof fetch;
26
+ };
27
+ /** What a turn of the session API said: the events of its stream, parsed. */
28
+ export declare function streamEvents(stream: string): Array<Record<string, unknown>>;
29
+ /** What a run of the page answered: its outputs, by name, or why there are none. */
30
+ export type PageTurn = {
31
+ outputs: Record<string, unknown>;
32
+ inputs: Record<string, unknown>;
33
+ } | {
34
+ refused: string;
35
+ };
36
+ /** The page's turn, read from the stream the runtime answered. */
37
+ export declare function pageTurnOf(stream: string): PageTurn;
38
+ /** Open the session its page runs in; its uid, as the runtime says it. */
39
+ export declare function openPageSession(context: PageRunContext): Promise<string>;
40
+ /**
41
+ * Run the page on its inputs in a session: its outputs, or the runtime's
42
+ * sentence — its form refusing an input, its code failing.
43
+ */
44
+ export declare function runPageIn(context: PageRunContext, uid: string, inputs: Record<string, unknown>): Promise<PageTurn>;
45
+ /** Each output as the page's data holds it, by path. */
46
+ export declare function outputsData(outputs: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,111 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A widget's page written in its code, run on its inputs (LOOP P-05).
7
+ *
8
+ * The page opens a session of the application's agent on its runtime
9
+ * (`POST /api/v1/apps/sessions`), then, each time an input changes, sends
10
+ * them all as the session's `page` action (`POST …/sessions/{uid}/actions`,
11
+ * `{name: "page", payload: {inputs}}`). The runtime checks them against the
12
+ * page's form — refused with 422 and a sentence — runs its code's
13
+ * `@app.page` on them, and answers its outputs as a `CUSTOM` event named
14
+ * `loop.page`, `{inputs, outputs}`, which the page shows in place. Nothing is
15
+ * said in the conversation.
16
+ *
17
+ * @module loop/plugins/app-page/pageRun
18
+ */
19
+ /** The `CUSTOM` event that says what a page shows. */
20
+ export const LOOP_PAGE = 'loop.page';
21
+ /** What a turn of the session API said: the events of its stream, parsed. */
22
+ export function streamEvents(stream) {
23
+ const events = [];
24
+ for (const block of stream.split('\n\n')) {
25
+ const data = block
26
+ .split('\n')
27
+ .filter(line => line.startsWith('data:'))
28
+ .map(line => line.slice('data:'.length).trim())
29
+ .join('');
30
+ if (!data)
31
+ continue;
32
+ try {
33
+ events.push(JSON.parse(data));
34
+ }
35
+ catch {
36
+ // Not an event: skipped, as a reader of the stream skips it.
37
+ }
38
+ }
39
+ return events;
40
+ }
41
+ /** The page's turn, read from the stream the runtime answered. */
42
+ export function pageTurnOf(stream) {
43
+ let shown = null;
44
+ for (const event of streamEvents(stream)) {
45
+ if (event.type === 'RUN_ERROR') {
46
+ return { refused: String(event.message ?? 'Its page could not run.') };
47
+ }
48
+ if (event.type === 'CUSTOM' && event.name === LOOP_PAGE) {
49
+ const value = (event.value ?? {});
50
+ shown = { outputs: value.outputs ?? {}, inputs: value.inputs ?? {} };
51
+ }
52
+ }
53
+ return (shown ?? {
54
+ refused: 'Its page showed nothing: its code answered no outputs.',
55
+ });
56
+ }
57
+ const base = (context) => `${context.serverUrl.replace(/\/+$/, '')}/api/v1/apps/sessions`;
58
+ async function posted(context, url, body) {
59
+ const response = await (context.fetcher ?? fetch)(url, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'content-type': 'application/json',
63
+ ...(context.token ? { Authorization: `Bearer ${context.token}` } : {}),
64
+ },
65
+ body: JSON.stringify(body),
66
+ });
67
+ if (!response.ok) {
68
+ let detail = '';
69
+ try {
70
+ const said = (await response.json());
71
+ detail = typeof said.detail === 'string' ? said.detail : '';
72
+ }
73
+ catch {
74
+ // No sentence: the status says it.
75
+ }
76
+ throw new Error(detail || `Its page was refused (${response.status}).`);
77
+ }
78
+ return response.text();
79
+ }
80
+ /** Open the session its page runs in; its uid, as the runtime says it. */
81
+ export async function openPageSession(context) {
82
+ const stream = await posted(context, base(context), {
83
+ agent: context.agentId,
84
+ });
85
+ for (const event of streamEvents(stream)) {
86
+ if (event.type === 'CUSTOM' && event.name === 'loop.session') {
87
+ const uid = event.value?.uid;
88
+ if (typeof uid === 'string' && uid) {
89
+ return uid;
90
+ }
91
+ }
92
+ }
93
+ throw new Error('The runtime opened no session for its page.');
94
+ }
95
+ /**
96
+ * Run the page on its inputs in a session: its outputs, or the runtime's
97
+ * sentence — its form refusing an input, its code failing.
98
+ */
99
+ export async function runPageIn(context, uid, inputs) {
100
+ try {
101
+ const stream = await posted(context, `${base(context)}/${encodeURIComponent(uid)}/actions`, { name: 'page', payload: { inputs } });
102
+ return pageTurnOf(stream);
103
+ }
104
+ catch (error) {
105
+ return { refused: error instanceof Error ? error.message : String(error) };
106
+ }
107
+ }
108
+ /** Each output as the page's data holds it, by path. */
109
+ export function outputsData(outputs) {
110
+ return Object.fromEntries(Object.entries(outputs).map(([name, value]) => [`/outputs/${name}`, value]));
111
+ }
@@ -9,7 +9,7 @@ import { pageLayoutPanelOpen, togglePagePanel, } from '@datalayer/primer-addons/
9
9
  import { AssistantStage } from '../../../chat/assistant/AssistantStage';
10
10
  import { decisionsAskerAt } from '../../../chat/assistant/decisions';
11
11
  import { peekLine } from '../../../chat/assistant/ConversationBalloon';
12
- import { ASSISTANT_WORDS, assistantStateOf, keepAway, keptAway, latestSaying, } from '../../../chat/assistant/state';
12
+ import { assistantStateOf, keepAway, keptAway, latestSaying, } from '../../../chat/assistant/state';
13
13
  import { useApproveToolRequest, useRejectToolRequest, useToolApprovalsQuery, } from '../../../hooks/useToolApprovals';
14
14
  import { approvalsOfApp, ruleOfApproval } from '../app-rules/AppRulesCard';
15
15
  import { useViewportDrag } from '../../../chat/useViewportDrag';
@@ -17,6 +17,7 @@ import { LoopAssistantCharacter, LoopAssistantMenu, LoopChatLayout, LoopChatTurn
17
17
  import { assistantCharacterFor } from '../assistant-characters';
18
18
  import { useOptionalSandboxService } from '../agents/useSandboxService';
19
19
  import { TARGET_SPECS } from '../agents/switchable';
20
+ import { useChatWords } from '../../../chat/ChatLanguage';
20
21
  export const LOOP_ASSISTANT_PLUGIN_NAME = '@datalayer/loop-plugin-assistant';
21
22
  /** The character's size, in pixels, as the chat's own assistant. */
22
23
  const SIZE = 88;
@@ -72,6 +73,7 @@ function focusComposer() {
72
73
  composer?.focus();
73
74
  }
74
75
  export function LoopAssistant({ app, person, appId, decisions, balloon: displayGiven = 'history', workspace, }) {
76
+ const chatText = useChatWords();
75
77
  // The balloon's display, as its menu chose it; the Appspec's until then.
76
78
  const [chosenDisplay, setChosenDisplay] = useState();
77
79
  const display = chosenDisplay ?? displayGiven;
@@ -203,18 +205,18 @@ export function LoopAssistant({ app, person, appId, decisions, balloon: displayG
203
205
  const atWork = turn.status === 'streaming' || turn.status === 'thinking';
204
206
  const current = display === 'current';
205
207
  const balloon = state === 'paused'
206
- ? { text: ASSISTANT_WORDS.paused }
208
+ ? { text: chatText.paused }
207
209
  : approval
208
- ? { text: ASSISTANT_WORDS.approval, approval }
210
+ ? { text: chatText.waitingForYou, approval }
209
211
  : presence === 'waiting'
210
- ? { text: ASSISTANT_WORDS.waiting }
212
+ ? { text: chatText.waitingOpen }
211
213
  : tool
212
214
  ? { text: turn.activity ?? '', tool, busy: true }
213
215
  : current && (atWork || unheard)
214
216
  ? {
215
217
  text: turn.status === 'thinking'
216
- ? 'Thinking…'
217
- : (saying?.text ?? 'Thinking…'),
218
+ ? chatText.thinking
219
+ : (saying?.text ?? chatText.thinking),
218
220
  more: saying?.more,
219
221
  speaking: turn.status === 'streaming',
220
222
  busy: atWork,
@@ -49,6 +49,7 @@ import { NONE_EDITOR, seedEditorChoice, setEditorOptions, } from '../shell/edito
49
49
  import { FaceDrawing, PresenceFace, PresenceLine, } from '../../../chat/presence/Presence';
50
50
  import { presenceState, presenceToolOf, } from '../../../chat/presence/presenceStatus';
51
51
  import { latestSaying, newestIsAnswer, } from '../../../chat/assistant/state';
52
+ import { useChatWords } from '../../../chat/ChatLanguage';
52
53
  /** No editor: the conversation on its own. */
53
54
  const NO_SURFACE = '';
54
55
  /**
@@ -64,6 +65,7 @@ const FULLSCREEN_HINT_PERIOD_MS = 1400;
64
65
  */
65
66
  const FACE_LARGE = parseInt(loopShapeVars['--loop-face-large'], 10);
66
67
  export default function ChatView({ workspace }) {
68
+ const chatText = useChatWords();
67
69
  /* The active theme's own colours. Read from the store rather than a
68
70
  provider, so a workspace mounted without the addons theme still gets the
69
71
  default palette instead of throwing. */
@@ -76,7 +78,7 @@ export default function ChatView({ workspace }) {
76
78
  // where it sets anything else about this plugin.
77
79
  const reactor = useReactorPlatform();
78
80
  const config = reactor.getConfig(CHAT_PLUGIN_NAME);
79
- const placeholder = config?.placeholder ?? 'Ask anything, type / for commands or @ for mention';
81
+ const placeholder = config?.placeholder || chatText.placeholder;
80
82
  const defaultSurface = config?.defaultSurface ?? 'notebook';
81
83
  /*
82
84
  * The input-prompt plugin's own configuration, read from here rather than
@@ -1040,8 +1042,8 @@ export default function ChatView({ workspace }) {
1040
1042
  const chatDisabled = gateBlocked || keyExpired || Boolean(inPageRefusal) || ambient.disabled;
1041
1043
  const disabledReason = keyExpired
1042
1044
  ? expiredKeyIsTemporary
1043
- ? 'This demo runs on a shared key, and its time is up. Sign in to keep going.'
1044
- : 'Your key has expired. Sign in to keep going.'
1045
+ ? chatText.demoKeyExpired
1046
+ : chatText.keyExpired
1045
1047
  : (inPageRefusal ??
1046
1048
  gateReason ??
1047
1049
  (ambient.disabled ? ambient.disableReason : undefined));
@@ -1773,7 +1775,7 @@ export default function ChatView({ workspace }) {
1773
1775
  renderToolResult: chatExtras.renderToolResult, showTurnFooter: chatExtras.showTokenUsage ?? config?.showTokenUsage ?? true, onContextSnapshot: handleContextSnapshot, onLoadingChange: handleLoadingChange, onItemsChange: handleMessagesChange, onDisplayItemsChange: handleDisplayItemsChange, enableStreaming: true }) }), besideChat && !promptHidden ? prompt : null] }) }));
1774
1776
  // Off when the host asked for the menu alone: the composer's suggestions
1775
1777
  // control carries the same openers (`suggestions` above).
1776
- const chipsEl = config?.suggestionLabels !== false && chatSuggestions.length > 0 ? (_jsx(Box, { "aria-label": "Suggested prompts", sx: { display: 'flex', flexWrap: 'wrap', gap: 2, px: 2, pb: '6px' }, children: chatSuggestions.map(item => (_jsx(Box, { as: "button", type: "button",
1778
+ const chipsEl = config?.suggestionLabels !== false && chatSuggestions.length > 0 ? (_jsx(Box, { "aria-label": chatText.suggestedPrompts, sx: { display: 'flex', flexWrap: 'wrap', gap: 2, px: 2, pb: '6px' }, children: chatSuggestions.map(item => (_jsx(Box, { as: "button", type: "button",
1777
1779
  // The whole request, where the chip shows only its summary.
1778
1780
  title: item.message, disabled: busy || chatDisabled, onClick: () => void handleSend(item.message), sx: {
1779
1781
  appearance: 'none',