@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
@@ -0,0 +1,260 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Components a developer writes (LOOP P-17): an A2UI component of one
7
+ * application only, declared in its Appspec (`interface.custom_components`)
8
+ * with its name, its properties as a JSON Schema, what it shows and sends,
9
+ * and the address of the built ES module that draws it.
10
+ *
11
+ * Reviewed like any component of the catalog — what agentspecs refuses is
12
+ * refused here in the same sentences — and listed beside the catalog's for
13
+ * that application alone (`appComponents`): the catalog grows for it, it
14
+ * does not open. How it is drawn, in a sandboxed frame of no origin, is
15
+ * `components/a2ui/custom`.
16
+ *
17
+ * @module loop/apps/customComponents
18
+ */
19
+ import { getComponent } from '../../specs/uiPlugins';
20
+ /** Its name: a word starting with a capital letter, as the catalog's are named. */
21
+ export const CUSTOM_COMPONENT_NAME = /^[A-Z][A-Za-z0-9]{0,63}$/;
22
+ /** What one of its properties may be typed: what the renderer tells apart. */
23
+ export const CUSTOM_PROPERTY_TYPES = [
24
+ 'string',
25
+ 'integer',
26
+ 'number',
27
+ 'boolean',
28
+ 'array',
29
+ 'object',
30
+ ];
31
+ /** What every component of a surface already has, and a custom one cannot declare. */
32
+ export const CUSTOM_RESERVED_NAMES = [
33
+ 'id',
34
+ 'component',
35
+ 'action',
36
+ 'weight',
37
+ 'visible_when',
38
+ 'children',
39
+ 'child',
40
+ ];
41
+ /**
42
+ * Where its module is loaded from: an address served over HTTPS, or this
43
+ * machine while it is written. A file of the application's folder waits for
44
+ * its packaging (P-29).
45
+ */
46
+ export const CUSTOM_SOURCE = /^(?:https:\/\/[A-Za-z0-9.-]+(?::\d+)?|http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?)\/\S*$/;
47
+ /** A Subresource Integrity hash: the module as it was reviewed. */
48
+ export const INTEGRITY = /^sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2}$/;
49
+ const PROPERTY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
50
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
51
+ const isBound = (value) => isRecord(value) && 'path' in value;
52
+ const quoted = (name) => `'${name}'`;
53
+ /** Why a value is not one a property takes, or null. */
54
+ function valueRefused(name, field, value) {
55
+ if (Array.isArray(field.enum)) {
56
+ return field.enum.includes(value)
57
+ ? null
58
+ : `${quoted(name)} is one of ${field.enum.map(each => quoted(String(each))).join(', ')}`;
59
+ }
60
+ const kind = String(field.type);
61
+ const takes = {
62
+ string: v => typeof v === 'string',
63
+ integer: v => typeof v === 'number' && Number.isInteger(v),
64
+ number: v => typeof v === 'number',
65
+ boolean: v => typeof v === 'boolean',
66
+ array: v => Array.isArray(v),
67
+ object: isRecord,
68
+ };
69
+ if (typeof value === 'boolean' && kind !== 'boolean') {
70
+ return `${quoted(name)} is ${kind}, not true or false`;
71
+ }
72
+ return takes[kind]?.(value) ? null : `${quoted(name)} is ${kind}`;
73
+ }
74
+ /** Its properties, as its schema declares them. */
75
+ const fieldsOf = (component) => (isRecord(component.props.properties)
76
+ ? component.props.properties
77
+ : {});
78
+ /**
79
+ * What its schema refuses of the properties given, in the words agentspecs
80
+ * says them: one it does not have, a value of another type, one it needs.
81
+ * `bound` are given elsewhere — bound to what the application publishes.
82
+ */
83
+ export function customPropsRefused(component, props, bound = []) {
84
+ const fields = fieldsOf(component);
85
+ const problems = Object.keys(props)
86
+ .filter(name => !(name in fields))
87
+ .map(name => `it has no property ${quoted(name)}`);
88
+ for (const [name, value] of Object.entries(props)) {
89
+ if (name in fields && !isBound(value)) {
90
+ const refused = valueRefused(name, fields[name], value);
91
+ if (refused) {
92
+ problems.push(refused);
93
+ }
94
+ }
95
+ }
96
+ const required = Array.isArray(component.props.required)
97
+ ? component.props.required
98
+ : [];
99
+ for (const name of required) {
100
+ if (!(name in props) && !bound.includes(name)) {
101
+ problems.push(`it needs ${quoted(name)}`);
102
+ }
103
+ }
104
+ return problems;
105
+ }
106
+ /** What stops one component its developer wrote from being reviewed, in sentences. */
107
+ export function customComponentProblems(component) {
108
+ const said = `The component ${component.name || '(unnamed)'}`;
109
+ const problems = [];
110
+ if (!CUSTOM_COMPONENT_NAME.test(component.name)) {
111
+ problems.push(`Cannot use “${component.name}” as a component's name: a word starting with a capital letter, as Gauge.`);
112
+ }
113
+ else if (getComponent(component.name)) {
114
+ problems.push(`${said} is a component of the catalog: name yours otherwise.`);
115
+ }
116
+ if (!component.description.trim()) {
117
+ problems.push(`${said} says what it is for, under \`description\`.`);
118
+ }
119
+ if (!/^[a-z][a-z0-9+.-]*:/.test(component.source)) {
120
+ problems.push(`${said}'s module “${component.source}” is a file of the application's folder: it is drawn once the application is packaged with it (LOOP P-29); give the address of a built ES module.`);
121
+ }
122
+ else if (!CUSTOM_SOURCE.test(component.source)) {
123
+ problems.push(`${said}'s module “${component.source}” is loaded over \`https://\`, or from \`http://localhost\` while it is written.`);
124
+ }
125
+ if (component.integrity && !INTEGRITY.test(component.integrity)) {
126
+ problems.push(`${said}'s integrity “${component.integrity}” is no Subresource Integrity hash: \`sha384-\` and the hash in base64.`);
127
+ }
128
+ if (component.height < 40 || component.height > 2000) {
129
+ problems.push(`${said} is between 40 and 2000 pixels high.`);
130
+ }
131
+ const schema = component.props;
132
+ if (schema.type !== 'object' || !isRecord(schema.properties)) {
133
+ problems.push(`${said}'s props are the JSON Schema of an object, with its \`properties\`.`);
134
+ return problems;
135
+ }
136
+ const fields = fieldsOf(component);
137
+ for (const [name, field] of Object.entries(fields)) {
138
+ if (!PROPERTY_NAME.test(name) ||
139
+ CUSTOM_RESERVED_NAMES.includes(name)) {
140
+ problems.push(`${said} cannot have a property named ${quoted(name)}.`);
141
+ continue;
142
+ }
143
+ if (!isRecord(field)) {
144
+ problems.push(`${said}'s property ${quoted(name)} is not a schema.`);
145
+ continue;
146
+ }
147
+ if ('enum' in field) {
148
+ const values = field.enum;
149
+ if (!Array.isArray(values) ||
150
+ values.length === 0 ||
151
+ !values.every(value => typeof value === 'string')) {
152
+ problems.push(`${said}'s property ${quoted(name)} is an \`enum\` of words, one at least.`);
153
+ }
154
+ }
155
+ else if (!CUSTOM_PROPERTY_TYPES.includes(field.type)) {
156
+ problems.push(`${said}'s property ${quoted(name)} is typed ${quoted(String(field.type))}: one of ${CUSTOM_PROPERTY_TYPES.join(', ')}, or an \`enum\`.`);
157
+ continue;
158
+ }
159
+ if ('default' in field) {
160
+ const refused = valueRefused(name, field, field.default);
161
+ if (refused) {
162
+ problems.push(`${said}'s default for ${refused}.`);
163
+ }
164
+ }
165
+ }
166
+ const required = Array.isArray(schema.required)
167
+ ? schema.required
168
+ : [];
169
+ const missing = required.filter(name => !(name in fields));
170
+ if (missing.length > 0) {
171
+ problems.push(`${said} requires ${missing.map(quoted).join(', ')}, which its props do not have.`);
172
+ }
173
+ for (const name of [...component.shows, ...component.sends]) {
174
+ if (!PROPERTY_NAME.test(name) ||
175
+ CUSTOM_RESERVED_NAMES.includes(name)) {
176
+ problems.push(`${said} cannot bind through ${quoted(name)}.`);
177
+ }
178
+ else if (name in fields) {
179
+ problems.push(`${said} both has the property ${quoted(name)} and binds through it.`);
180
+ }
181
+ }
182
+ for (const [bindings, what] of [
183
+ [component.shows, 'shows'],
184
+ [component.sends, 'sends'],
185
+ ]) {
186
+ if (new Set(bindings).size !== bindings.length) {
187
+ problems.push(`${said} ${what} the same binding twice.`);
188
+ }
189
+ }
190
+ if (component.example) {
191
+ const bindings = new Set([...component.shows, ...component.sends]);
192
+ const given = Object.fromEntries(Object.entries(component.example).filter(([key]) => !bindings.has(key)));
193
+ const refused = customPropsRefused(component, given);
194
+ if (refused.length > 0) {
195
+ problems.push(`${said}'s example is refused: ${refused.join('; ')}.`);
196
+ }
197
+ }
198
+ return problems;
199
+ }
200
+ /** What stops the components an application's developer wrote, in sentences. */
201
+ export function customComponentsProblems(app) {
202
+ const own = app.interface.customComponents ?? [];
203
+ const problems = own.flatMap(customComponentProblems);
204
+ const names = own.map(component => component.name);
205
+ if (new Set(names).size !== names.length) {
206
+ problems.push('Two of its own components have the same name.');
207
+ }
208
+ return problems;
209
+ }
210
+ /** A component its developer wrote, by name, or undefined. */
211
+ export function customComponentOf(app, name) {
212
+ return (app.interface.customComponents ?? []).find(component => component.name === name);
213
+ }
214
+ /**
215
+ * It as the catalog lists a component (C-13): what the palette, the
216
+ * components page and the checks read — under the application's version, its
217
+ * category `custom`.
218
+ */
219
+ export function customComponentEntry(component, version) {
220
+ return {
221
+ id: component.name,
222
+ name: component.name,
223
+ description: component.description,
224
+ category: 'custom',
225
+ emoji: '🧩',
226
+ version,
227
+ standard: false,
228
+ properties: component.props,
229
+ bindings: { shows: [...component.shows], sends: [...component.sends] },
230
+ events: component.sends.length > 0 ? ['send'] : [],
231
+ ...(component.example ? { example: component.example } : {}),
232
+ };
233
+ }
234
+ /**
235
+ * The components an application may place: the catalog's own, given, and
236
+ * those its developer wrote, for it alone. A name of its own that the
237
+ * catalog has is not listed twice: the checks refuse it.
238
+ */
239
+ export function appComponents(app, catalog) {
240
+ const names = new Set(catalog.map(component => component.id));
241
+ return [
242
+ ...catalog,
243
+ ...(app.interface.customComponents ?? [])
244
+ .filter(component => !names.has(component.name))
245
+ .map(component => customComponentEntry(component, app.version)),
246
+ ];
247
+ }
248
+ /** What its schema refuses of a node of a surface drawn by it, in sentences; empty when nothing. */
249
+ export function customNodeRefused(component, node) {
250
+ const skip = new Set([
251
+ ...CUSTOM_RESERVED_NAMES,
252
+ ...component.shows,
253
+ ...component.sends,
254
+ ]);
255
+ const given = Object.fromEntries(Object.entries(node).filter(([key]) => !skip.has(key)));
256
+ const bound = Object.entries(given)
257
+ .filter(([, value]) => isBound(value))
258
+ .map(([key]) => key);
259
+ return customPropsRefused(component, given, bound);
260
+ }
@@ -6,12 +6,19 @@
6
6
  *
7
7
  * @module loop/apps/hostTools
8
8
  */
9
- import type { AppHostBridgeSpec } from '../../types/agentspecs';
9
+ import type { AppHostBridgeSpec, AppHostUser } from '../../types/agentspecs';
10
10
  /** The tool its agent reads what the page passes it with. */
11
11
  export declare const HOST_CONTEXT_TOOL = "host_context";
12
12
  /** The tool its agent calls a function of the page with. */
13
13
  export declare const hostTool: (name: string) => string;
14
14
  /** How a value or a function of the host is named. */
15
15
  export declare const HOST_NAME: RegExp;
16
+ /** What the page's word on its visitor may be worth (LOOP D-21). */
17
+ export declare const HOST_USERS: readonly AppHostUser[];
18
+ /**
19
+ * Whether a session is opened only for a user the host's server signed
20
+ * (LOOP D-21): `deployment.embedded.host.user: signed`.
21
+ */
22
+ export declare function signedUser(bridge: AppHostBridgeSpec | undefined): boolean;
16
23
  /** The tools its agent is given for the host. */
17
24
  export declare function hostToolsOf(bridge: AppHostBridgeSpec | undefined): string[];
@@ -8,6 +8,15 @@ export const HOST_CONTEXT_TOOL = 'host_context';
8
8
  export const hostTool = (name) => `host_${name}`;
9
9
  /** How a value or a function of the host is named. */
10
10
  export const HOST_NAME = /^[a-z][a-z0-9_]{0,62}$/;
11
+ /** What the page's word on its visitor may be worth (LOOP D-21). */
12
+ export const HOST_USERS = ['claimed', 'signed'];
13
+ /**
14
+ * Whether a session is opened only for a user the host's server signed
15
+ * (LOOP D-21): `deployment.embedded.host.user: signed`.
16
+ */
17
+ export function signedUser(bridge) {
18
+ return bridge?.user === 'signed';
19
+ }
11
20
  /** The tools its agent is given for the host. */
12
21
  export function hostToolsOf(bridge) {
13
22
  if (!bridge) {
@@ -18,3 +18,4 @@ export * from './records';
18
18
  export * from './rules';
19
19
  export * from './saved';
20
20
  export * from './yaml';
21
+ export * from './customComponents';
@@ -22,3 +22,4 @@ export * from './records';
22
22
  export * from './rules';
23
23
  export * from './saved';
24
24
  export * from './yaml';
25
+ export * from './customComponents';
@@ -0,0 +1,10 @@
1
+ import type { ReactComponentImplementation } from '@a2ui/react/v0_9';
2
+ export type A2uiComponentContribution = {
3
+ /** The name a surface gives it (e.g. 'Gauge'). */
4
+ id: string;
5
+ /** The application whose developer wrote it, by id. */
6
+ app: string;
7
+ /** Its renderer, as A2UI's catalog registers one. */
8
+ implementation: ReactComponentImplementation;
9
+ };
10
+ export declare const LoopA2uiComponent: import("@datalayer/reactor").ContributionPoint<A2uiComponentContribution>;
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A component an application adds to the A2UI catalog it is drawn with (LOOP
7
+ * P-17), as a contribution point, re-exported by `loop/core`: the components
8
+ * its developer wrote, contributed by the application's own plugin, so that
9
+ * its page, the elements its code shows and its answers draw them — in that
10
+ * application's workspace, and in no other.
11
+ *
12
+ * @module loop/core/a2uiComponents
13
+ */
14
+ import { defineContributionPoint } from '@datalayer/reactor';
15
+ export const LoopA2uiComponent = defineContributionPoint('loop.a2ui.component');
@@ -579,6 +579,7 @@ export type AssistantMenuContribution = {
579
579
  };
580
580
  export declare const LoopAssistantMenu: import("@datalayer/reactor").ContributionPoint<AssistantMenuContribution>;
581
581
  export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
582
+ export { LoopA2uiComponent, type A2uiComponentContribution, } from './a2uiComponents';
582
583
  /**
583
584
  * The per-example chat extras a host feeds the loop's conversation live.
584
585
  *
@@ -146,6 +146,9 @@ export const LoopAssistantMenu = defineContributionPoint('loop.assistant.menu');
146
146
  // A block the Canvas may place (LOOP C-12), in a module of its own so that
147
147
  // the palette is read without the rest of the contracts.
148
148
  export { LoopCanvasBlock } from './canvasBlocks';
149
+ // A component an application adds to the catalog it is drawn with (LOOP
150
+ // P-17): the components its developer wrote, for its workspace alone.
151
+ export { LoopA2uiComponent, } from './a2uiComponents';
149
152
  export const LoopChatExtras = defineContributionPoint('loop.chat.extras');
150
153
  export const LoopOpeningNotebook = defineContributionPoint('loop.notebook.opening');
151
154
  export const LoopOpeningDocument = defineContributionPoint('loop.document.opening');
@@ -1,5 +1,5 @@
1
1
  import type { CSSProperties, JSX, ReactNode } from 'react';
2
- import type { PluginRef } from '@datalayer/reactor';
2
+ import { type PluginRef } from '@datalayer/reactor';
3
3
  import type { AppAccent, AppEmbedMode, AppSpec, AppThemeVariant } from '../../types/agentspecs';
4
4
  import type { ResumedThread, ThemeOverrides } from '../../types/chat';
5
5
  import type { AssistantCharacter } from '../../chat/assistant/characters';
@@ -24,6 +24,12 @@ export type AppEmbedProps = {
24
24
  colorMode?: EmbedColorMode;
25
25
  /** The host's face, a CSS font family; the theme's by default. */
26
26
  font?: string;
27
+ /**
28
+ * The language the visitor reads it in, as BCP 47 tags it (`fr`, `pt-BR`):
29
+ * its translation into it and the chat's own words (LOOP P-26). The
30
+ * visitor's browser's by default.
31
+ */
32
+ language?: string;
27
33
  /**
28
34
  * An agent-runtimes server the application's agent runs on. Without one,
29
35
  * it runs on a Datalayer runtime, launched with the visitor's Datalayer
@@ -150,6 +156,8 @@ export type AppFloatingProps = {
150
156
  host?: AppEmbedHost;
151
157
  /** The visit's session, picked up again after a reload (D-13). */
152
158
  session?: EmbedSession;
159
+ /** The language the visitor reads it in (P-26); the browser's by default. */
160
+ language?: string;
153
161
  };
154
162
  /**
155
163
  * The bubble, the panel and the assistant: `ChatFloating`'s chrome in that
@@ -160,7 +168,7 @@ export type AppFloatingProps = {
160
168
  * The workspace tells the chrome what the application is doing and what it
161
169
  * last said (`onPresence`, `onSaying`).
162
170
  */
163
- export declare function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, }: AppFloatingProps): JSX.Element;
171
+ export declare function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, language, }: AppFloatingProps): JSX.Element;
164
172
  /** The application's theme (`loop` unless it names one), with its (or the host's) accent and face. */
165
173
  export declare function EmbedThemed({ accent, colorMode, font, variant, children, style, }: {
166
174
  accent: AppAccent;
@@ -170,5 +178,5 @@ export declare function EmbedThemed({ accent, colorMode, font, variant, children
170
178
  children: ReactNode;
171
179
  style?: CSSProperties;
172
180
  }): JSX.Element;
173
- export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height, ownPortal, plugins, host, resume, }: AppEmbedProps): JSX.Element;
181
+ export declare function AppEmbed({ app, mode, accent, colorMode, font, language, serverUrl, instance, embedToken, height, ownPortal, plugins, host, resume, }: AppEmbedProps): JSX.Element;
174
182
  export default AppEmbed;
@@ -40,6 +40,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
40
40
  * @module loop/embed/AppEmbed
41
41
  */
42
42
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
43
+ import { contribution, definePlugin } from '@datalayer/reactor';
43
44
  import { Text, registerPortalRoot } from '@primer/react';
44
45
  import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
45
46
  import { FluentEmoji } from '@datalayer/core/lib/components/emoji';
@@ -50,10 +51,12 @@ import { AppRenderer, } from '../apps/AppRenderer';
50
51
  import { AssistantCharactersPlugin, assistantCharacterFor, assistantCharactersFrom, } from '../plugins/assistant-characters';
51
52
  import { floatingViewOf } from './embedConfig';
52
53
  import { createChatExtrasPlugin } from '../plugins/chat-extras';
53
- import { hostFrontendTools } from './hostBridge';
54
+ import { hostFrontendTools, hostUserRunProps, } from './hostBridge';
55
+ import { LoopRunProps } from '../core';
54
56
  import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
55
57
  import { WINDOW_NOT_YET, embedSessionKey, postWindowMessage, reattachSession, sessionGoneSentence, sessionKeeper, visitOfToken, } from './embedSession';
56
58
  import { onLoopWindowMessage } from '../../chat/base/loopWindow';
59
+ import { ChatLanguage, useChatWords } from '../../chat/ChatLanguage';
57
60
  /**
58
61
  * The visit's session, kept and picked up again (LOOP D-13): on the host's
59
62
  * server with an embed token that names its visit, the session the chat
@@ -195,7 +198,29 @@ export function useHostBridge(app, host) {
195
198
  // The tools change with what the Appspec names, and the rules that decide them.
196
199
  // eslint-disable-next-line react-hooks/exhaustive-deps
197
200
  }, [handle, key]);
198
- const plugins = useMemo(() => (bridge ? [handle.plugin] : NO_HOST_PLUGINS), [bridge, handle]);
201
+ // Who its user is (D-21): the token the host's server signed goes with
202
+ // every run, for an application that takes only a signed user.
203
+ const signed = bridge?.user === 'signed';
204
+ const userPlugin = useMemo(() => {
205
+ const runProps = hostUserRunProps(app, () => hostRef.current ?? {});
206
+ return runProps
207
+ ? definePlugin({
208
+ name: `loop-host-user-${app.id}`,
209
+ displayName: `${app.name}: who its user is`,
210
+ description: `The token naming its user that the host's server signed, sent with every run of ${app.name}.`,
211
+ octicon: 'person',
212
+ emoji: '\u{1F464}',
213
+ contributes: [
214
+ contribution(LoopRunProps, runProps, { id: 'host-user' }),
215
+ ],
216
+ })
217
+ : undefined;
218
+ // Made again only when whether it is signed changes.
219
+ // eslint-disable-next-line react-hooks/exhaustive-deps
220
+ }, [app.id, signed]);
221
+ const plugins = useMemo(() => bridge
222
+ ? [handle.plugin, ...(userPlugin ? [userPlugin] : [])]
223
+ : NO_HOST_PLUGINS, [bridge, handle, userPlugin]);
199
224
  // What its code tells the page during a turn (LOOP P-25), raised on it.
200
225
  useEffect(() => onLoopWindowMessage(data => hostRef.current?.onEvent?.({
201
226
  type: 'window-message',
@@ -254,7 +279,8 @@ function Face({ emoji }) {
254
279
  * The workspace tells the chrome what the application is doing and what it
255
280
  * last said (`onPresence`, `onSaying`).
256
281
  */
257
- export function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, }) {
282
+ export function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, language, }) {
283
+ const chatText = useChatWords();
258
284
  const [presence, setPresence] = useState('idle');
259
285
  const [said, setSaid] = useState({ answering: false });
260
286
  const bridge = useHostBridge(app, host);
@@ -274,7 +300,7 @@ export function AppFloating({ app, view, colorMode, themeVariant, themeOverrides
274
300
  const signedOut = !serverUrl && !token && !renderer?.datalayerVisitors;
275
301
  return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), ...(app.interface.balloon
276
302
  ? { balloonDisplay: app.interface.balloon }
277
- : {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
303
+ : {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: chatText.talkTo(app.name), colorMode: colorMode, useStore: false, conversation: {
278
304
  body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : session && !session.ready ? null : (_jsxs(_Fragment, { children: [_jsx(SessionSaid, { said: session?.said }), _jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
279
305
  // The host's accent and face over the application's own, inside
280
306
  // its conversation too, in the embed's mode.
@@ -282,7 +308,7 @@ export function AppFloating({ app, view, colorMode, themeVariant, themeOverrides
282
308
  // The window draws its face, its name and close.
283
309
  hideChatHeader: true,
284
310
  // Mounted closed: the caret goes in when the window opens.
285
- autoFocusPrompt: false, onPresence: onPresence, onSaying: onSaying, plugins: bridge.plugins, ...(session?.thread ? { thread: session.thread } : {}), ...renderer })] })),
311
+ autoFocusPrompt: false, onPresence: onPresence, onSaying: onSaying, plugins: bridge.plugins, ...(session?.thread ? { thread: session.thread } : {}), ...(language ? { language } : {}), ...renderer })] })),
286
312
  presence,
287
313
  saying: said.saying,
288
314
  answering: said.answering,
@@ -331,7 +357,7 @@ export function EmbedThemed({ accent, colorMode, font, variant, children, style,
331
357
  */
332
358
  _jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: getThemeConfig(variant).primerTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
333
359
  }
334
- export function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, resume = true, }) {
360
+ export function AppEmbed({ app, mode, accent, colorMode, font, language, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, resume = true, }) {
335
361
  const bridge = useHostBridge(app, host);
336
362
  const kept = useEmbedSession({ app, serverUrl, embedToken, resume });
337
363
  // Window messages from the page, on the host's own server (P-25).
@@ -359,15 +385,15 @@ export function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instan
359
385
  return (_jsx("p", { role: "status", className: "datalayer-app-said", children: assistant.problem }));
360
386
  }
361
387
  const character = assistant?.character;
362
- return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, variant: variant, style: view
363
- ? // Floating: nothing in the page's flow, nothing painted behind.
364
- { backgroundColor: 'transparent' }
365
- : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(AppFloating, { app: app, view: view, serverUrl: serverUrl, instance: instance, embedToken: embedToken, colorMode: resolvedMode, themeVariant: variant, themeOverrides: themeOverrides, character: character, host: host, session: session })) : !session.ready ? null : (_jsxs(_Fragment, { children: [_jsx(SessionSaid, { said: session.said }), _jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
366
- // The host's accent and face over the application's own, inside
367
- // its conversation too, in the embed's mode: the host's or its
368
- // visitor's system's, not a Datalayer setting.
369
- themeOverrides: themeOverrides, colorMode: resolvedMode, themeVariant: variant,
370
- // What the page gives it, and what it tells the page (D-10).
371
- plugins: bridge.plugins, onPresence: bridge.onPresence, onSaying: bridge.onSaying, ...(session.thread ? { thread: session.thread } : {}) })] }))] }));
388
+ return (_jsx(ChatLanguage, { language: language, children: _jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, variant: variant, style: view
389
+ ? // Floating: nothing in the page's flow, nothing painted behind.
390
+ { backgroundColor: 'transparent' }
391
+ : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(AppFloating, { app: app, view: view, serverUrl: serverUrl, instance: instance, embedToken: embedToken, colorMode: resolvedMode, themeVariant: variant, themeOverrides: themeOverrides, character: character, host: host, session: session, ...(language ? { language } : {}) })) : !session.ready ? null : (_jsxs(_Fragment, { children: [_jsx(SessionSaid, { said: session.said }), _jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
392
+ // The host's accent and face over the application's own, inside
393
+ // its conversation too, in the embed's mode: the host's or its
394
+ // visitor's system's, not a Datalayer setting.
395
+ themeOverrides: themeOverrides, colorMode: resolvedMode, themeVariant: variant,
396
+ // What the page gives it, and what it tells the page (D-10).
397
+ plugins: bridge.plugins, onPresence: bridge.onPresence, onSaying: bridge.onSaying, ...(session.thread ? { thread: session.thread } : {}), ...(language ? { language } : {}) })] }))] }) }));
372
398
  }
373
399
  export default AppEmbed;
@@ -31,6 +31,11 @@ export declare function appspecOfText(text: string): AppSpec;
31
31
  export declare function runnable({ app, problems }: ParsedAppspec): AppSpec;
32
32
  /** The run page an application on Datalayer is framed from. */
33
33
  export declare function frameSrcOf(origin: string, app: string, token?: string): string;
34
+ /**
35
+ * A framed run page told the visitor's language (P-26): `language=` in its
36
+ * query, before the fragment that holds the token.
37
+ */
38
+ export declare function frameInLanguage(src: string, language?: string): string;
34
39
  /**
35
40
  * An application on Datalayer, read with its embed token: what the space
36
41
  * keeps of it (`model_s`, the stored definition), whose `spec` is the