@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.
- package/lib/chat/ChatFloating.d.ts +5 -4
- package/lib/chat/ChatFloating.js +8 -6
- package/lib/chat/ChatLanguage.d.ts +17 -0
- package/lib/chat/ChatLanguage.js +29 -0
- package/lib/chat/assistant/AssistantStage.d.ts +4 -4
- package/lib/chat/assistant/AssistantStage.js +4 -2
- package/lib/chat/assistant/BalloonParts.d.ts +2 -1
- package/lib/chat/assistant/BalloonParts.js +12 -5
- package/lib/chat/assistant/BalloonVisual.js +3 -1
- package/lib/chat/assistant/ConversationBalloon.d.ts +1 -1
- package/lib/chat/assistant/ConversationBalloon.js +3 -2
- package/lib/chat/assistant/SpeechBalloon.d.ts +1 -1
- package/lib/chat/assistant/SpeechBalloon.js +10 -7
- package/lib/chat/assistant/toolLine.d.ts +8 -4
- package/lib/chat/assistant/toolLine.js +13 -9
- package/lib/chat/base/ChatBase.js +14 -11
- package/lib/chat/display/EmptyState.js +3 -1
- package/lib/chat/index.d.ts +2 -0
- package/lib/chat/index.js +3 -0
- package/lib/chat/messages/ChatMessages.js +3 -1
- package/lib/chat/messages/TurnFooter.js +3 -1
- package/lib/chat/prompt/InputPrompt.js +3 -1
- package/lib/chat/prompt/InputPromptLexical.js +3 -1
- package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
- package/lib/chat/tools/ToolApprovalBanner.js +5 -3
- package/lib/chat/tools/ToolApprovalDialog.js +5 -3
- package/lib/chat/tools/ToolCallDisplay.js +29 -25
- package/lib/chat/words.d.ts +102 -0
- package/lib/chat/words.js +460 -0
- package/lib/components/a2ui/custom/index.d.ts +39 -0
- package/lib/components/a2ui/custom/index.js +95 -0
- package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
- package/lib/components/a2ui/custom/sandbox.js +188 -0
- package/lib/components/a2ui/datalayer/index.d.ts +1 -1
- package/lib/components/a2ui/datalayer/index.js +1 -1
- package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
- package/lib/components/a2ui/datalayer/schema.js +12 -1
- package/lib/components/a2ui/index.d.ts +1 -0
- package/lib/components/a2ui/index.js +1 -0
- package/lib/components/a2ui/visibleWhen.d.ts +12 -1
- package/lib/components/a2ui/visibleWhen.js +24 -3
- package/lib/components/inspector/AgentInspector.d.ts +25 -44
- package/lib/components/inspector/AgentInspector.js +27 -215
- package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
- package/lib/components/inspector/AgentInspectorDialog.js +2 -2
- package/lib/components/inspector/a2aSpans.d.ts +51 -0
- package/lib/components/inspector/a2aSpans.js +373 -0
- package/lib/components/inspector/agentSpans.d.ts +120 -0
- package/lib/components/inspector/agentSpans.js +253 -0
- package/lib/components/inspector/chatSpans.d.ts +36 -0
- package/lib/components/inspector/chatSpans.js +50 -0
- package/lib/components/inspector/classify.d.ts +11 -8
- package/lib/components/inspector/classify.js +23 -24
- package/lib/components/inspector/index.d.ts +7 -7
- package/lib/components/inspector/index.js +7 -7
- package/lib/components/teams/A2ATeamGraph.d.ts +117 -22
- package/lib/components/teams/A2ATeamGraph.js +217 -97
- package/lib/components/teams/SceneTranscript.d.ts +29 -0
- package/lib/components/teams/SceneTranscript.js +54 -0
- package/lib/components/teams/SceneView.d.ts +41 -0
- package/lib/components/teams/SceneView.js +28 -0
- package/lib/components/teams/index.d.ts +4 -0
- package/lib/components/teams/index.js +4 -0
- package/lib/components/teams/sceneStage.d.ts +120 -0
- package/lib/components/teams/sceneStage.js +170 -0
- package/lib/components/teams/sceneTranscript.d.ts +74 -0
- package/lib/components/teams/sceneTranscript.js +294 -0
- package/lib/components/teams/teamConnections.d.ts +2 -2
- package/lib/components/teams/teamConnections.js +3 -3
- package/lib/components/teams/useA2ATeam.d.ts +94 -22
- package/lib/components/teams/useA2ATeam.js +356 -140
- package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
- package/lib/examples/AgentA2ATeamExample.js +70 -60
- package/lib/examples/AssistantExample.js +2 -2
- package/lib/examples/ScenesExample.d.ts +26 -0
- package/lib/examples/ScenesExample.js +241 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/exampleGroups.js +3 -0
- package/lib/examples/index.d.ts +1 -0
- package/lib/examples/index.js +1 -0
- package/lib/examples/main.js +4 -0
- package/lib/loop/apps/AppComposer.d.ts +2 -1
- package/lib/loop/apps/AppComposer.js +6 -3
- package/lib/loop/apps/AppRenderer.d.ts +2 -1
- package/lib/loop/apps/AppRenderer.js +34 -25
- package/lib/loop/apps/appspec.d.ts +11 -1
- package/lib/loop/apps/appspec.js +115 -1
- package/lib/loop/apps/checks.d.ts +8 -0
- package/lib/loop/apps/checks.js +106 -4
- package/lib/loop/apps/customComponents.d.ts +41 -0
- package/lib/loop/apps/customComponents.js +260 -0
- package/lib/loop/apps/hostTools.d.ts +8 -1
- package/lib/loop/apps/hostTools.js +9 -0
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/a2uiComponents.d.ts +10 -0
- package/lib/loop/core/a2uiComponents.js +15 -0
- package/lib/loop/core/index.d.ts +1 -0
- package/lib/loop/core/index.js +3 -0
- package/lib/loop/embed/AppEmbed.d.ts +11 -3
- package/lib/loop/embed/AppEmbed.js +42 -16
- package/lib/loop/embed/element.d.ts +5 -0
- package/lib/loop/embed/element.js +61 -5
- package/lib/loop/embed/embedConfig.d.ts +8 -0
- package/lib/loop/embed/embedConfig.js +18 -0
- package/lib/loop/embed/hostBridge.d.ts +28 -2
- package/lib/loop/embed/hostBridge.js +22 -2
- package/lib/loop/index.d.ts +4 -3
- package/lib/loop/index.js +4 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
- package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
- package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
- package/lib/loop/plugins/app-components/index.d.ts +26 -0
- package/lib/loop/plugins/app-components/index.js +56 -0
- package/lib/loop/plugins/app-elements/AppElements.js +4 -1
- package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
- package/lib/loop/plugins/app-page/AppPage.js +106 -8
- package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
- package/lib/loop/plugins/app-page/appPageModel.js +120 -4
- package/lib/loop/plugins/app-page/index.d.ts +1 -0
- package/lib/loop/plugins/app-page/index.js +1 -0
- package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
- package/lib/loop/plugins/app-page/pageRun.js +111 -0
- package/lib/loop/plugins/assistant/index.js +8 -6
- package/lib/loop/plugins/chat/ChatView.js +6 -4
- package/lib/loop/plugins/chat/index.d.ts +1 -1
- package/lib/loop/plugins/chat/index.js +2 -1
- package/lib/models/PageTag.d.ts +1 -1
- package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
- package/lib/specs/actions.js +78 -1
- package/lib/specs/apps.d.ts +5 -0
- package/lib/specs/apps.js +1384 -231
- package/lib/specs/appspecSchema.js +191 -1
- package/lib/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/mcpServers.js +8 -2
- package/lib/specs/scenes.d.ts +21 -0
- package/lib/specs/scenes.js +1191 -0
- package/lib/specs/teams/teams.d.ts +3 -0
- package/lib/specs/teams/teams.js +249 -0
- package/lib/types/agentspecs.d.ts +70 -0
- package/lib/types/chat.d.ts +6 -5
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/scenes.d.ts +207 -0
- package/lib/types/scenes.js +5 -0
- package/lib/types/teams.d.ts +13 -2
- package/package.json +4 -4
- package/scripts/codegen/generate_agents.py +1 -0
- package/scripts/codegen/generate_apps.py +4 -2
- package/scripts/codegen/generate_scenes.py +301 -0
- package/scripts/codegen/generate_teams.py +15 -2
- package/lib/components/inspector/JsonTree.d.ts +0 -24
- package/lib/components/inspector/JsonTree.js +0 -89
- package/lib/components/inspector/a2aInspect.d.ts +0 -56
- package/lib/components/inspector/a2aInspect.js +0 -478
- package/lib/components/inspector/agentInspector.d.ts +0 -179
- package/lib/components/inspector/agentInspector.js +0 -267
- package/lib/components/inspector/chatInspect.d.ts +0 -38
- 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) {
|
package/lib/loop/apps/index.d.ts
CHANGED
package/lib/loop/apps/index.js
CHANGED
|
@@ -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');
|
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -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
|
*
|
package/lib/loop/core/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|