@datalayer/agent-runtimes 1.3.35 → 1.3.37

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 (48) hide show
  1. package/lib/chat/ChatFloating.d.ts +8 -2
  2. package/lib/chat/ChatFloating.js +2 -2
  3. package/lib/chat/base/ChatBase.js +10 -6
  4. package/lib/chat/display/EmptyState.js +8 -1
  5. package/lib/chat/presence/Presence.d.ts +15 -1
  6. package/lib/chat/presence/Presence.js +0 -13
  7. package/lib/components/a2ui/datalayer/Chart.d.ts +132 -0
  8. package/lib/components/a2ui/datalayer/Chart.js +173 -0
  9. package/lib/components/a2ui/datalayer/Chat.d.ts +21 -0
  10. package/lib/components/a2ui/datalayer/Chat.js +116 -0
  11. package/lib/components/a2ui/datalayer/Evidence.d.ts +22 -0
  12. package/lib/components/a2ui/datalayer/Evidence.js +73 -0
  13. package/lib/components/a2ui/datalayer/FileUpload.d.ts +22 -0
  14. package/lib/components/a2ui/datalayer/FileUpload.js +107 -0
  15. package/lib/components/a2ui/datalayer/Form.d.ts +12 -0
  16. package/lib/components/a2ui/datalayer/Form.js +46 -0
  17. package/lib/components/a2ui/datalayer/Table.d.ts +20 -0
  18. package/lib/components/a2ui/datalayer/Table.js +103 -0
  19. package/lib/components/a2ui/datalayer/drawn.d.ts +10 -0
  20. package/lib/components/a2ui/datalayer/drawn.js +20 -0
  21. package/lib/components/a2ui/datalayer/implementation.d.ts +17 -0
  22. package/lib/components/a2ui/datalayer/implementation.js +10 -0
  23. package/lib/components/a2ui/datalayer/index.d.ts +23 -0
  24. package/lib/components/a2ui/datalayer/index.js +44 -0
  25. package/lib/components/a2ui/datalayer/parts.d.ts +32 -0
  26. package/lib/components/a2ui/datalayer/parts.js +44 -0
  27. package/lib/components/a2ui/datalayer/schema.d.ts +34 -0
  28. package/lib/components/a2ui/datalayer/schema.js +108 -0
  29. package/lib/components/a2ui/index.d.ts +1 -0
  30. package/lib/components/a2ui/index.js +1 -0
  31. package/lib/components/a2ui/visibleWhen.d.ts +4 -2
  32. package/lib/components/a2ui/visibleWhen.js +6 -3
  33. package/lib/loop/apps/AppRenderer.d.ts +8 -0
  34. package/lib/loop/apps/AppRenderer.js +21 -1
  35. package/lib/loop/embed/AppEmbed.d.ts +16 -1
  36. package/lib/loop/embed/AppEmbed.js +38 -9
  37. package/lib/loop/embed/LoopEmbed.js +2 -0
  38. package/lib/loop/embed/element.js +10 -1
  39. package/lib/loop/embed/embedTheme.d.ts +12 -0
  40. package/lib/loop/embed/embedTheme.js +15 -0
  41. package/lib/loop/plugins/assistant-characters/index.d.ts +10 -1
  42. package/lib/loop/plugins/assistant-characters/index.js +16 -1
  43. package/lib/loop/plugins/chat/ChatView.js +21 -9
  44. package/lib/loop/plugins/chat/index.d.ts +6 -0
  45. package/lib/loop/presets.d.ts +3 -0
  46. package/lib/loop/presets.js +2 -1
  47. package/lib/types/chat.d.ts +11 -0
  48. package/package.json +2 -1
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The A2UI schema of a component of Datalayer's own, read from the catalog
7
+ * (LOOP C-13, C-18): each property its JSON Schema declares, typed as it
8
+ * says; each thing it shows or sends a binding into the data model — a path
9
+ * the application publishes, or a value given in place; its `action`, what
10
+ * it dispatches when a person acts on it, written as A2UI writes a Button's.
11
+ *
12
+ * A2UI's renderer reads a component's schema to tell its properties apart —
13
+ * a binding it resolves and writes back through, an action it dispatches, a
14
+ * plain value it hands over — so the schema is built with the zod A2UI reads
15
+ * (v3), from the catalog, and cannot drift from what the Canvas, the YAML
16
+ * and Python check a component against.
17
+ *
18
+ * @module components/a2ui/datalayer/schema
19
+ */
20
+ import { z } from 'zod/v3';
21
+ import { ActionSchema, DataBindingSchema } from '@a2ui/web_core/v0_9';
22
+ import { COMPONENT_CATALOGUE } from '../../../specs/uiPlugins';
23
+ /** The components of Datalayer's own the catalog has, by id. */
24
+ export const OWN_COMPONENT_IDS = Object.values(COMPONENT_CATALOGUE)
25
+ .filter(component => !component.standard)
26
+ .map(component => component.id);
27
+ /** A component of Datalayer's own, from the catalog; it throws for any other. */
28
+ export function ownComponent(id) {
29
+ const component = COMPONENT_CATALOGUE[id];
30
+ if (!component) {
31
+ throw new Error(`The catalog has no component ${id}.`);
32
+ }
33
+ if (component.standard || !component.properties || !component.bindings) {
34
+ throw new Error(`${id} is A2UI's own: its schema is the basic catalog's, not Datalayer's.`);
35
+ }
36
+ return component;
37
+ }
38
+ /**
39
+ * What a component shows or sends: a list, a record, words, a number, true
40
+ * or false given in place, or a path into the data model. A2UI resolves it
41
+ * — and gives the component a setter for it when it is a path.
42
+ */
43
+ const BindingSchema = z
44
+ .union([
45
+ z.array(z.any()),
46
+ z.record(z.string(), z.unknown()),
47
+ z.string(),
48
+ z.number(),
49
+ z.boolean(),
50
+ DataBindingSchema,
51
+ ])
52
+ .describe('REF:#/$defs/DynamicValue|Datalayer: what the component shows or sends — a path the application publishes or takes, or a value given in place.');
53
+ /** A property as its JSON Schema types it. */
54
+ function propertySchema(name, property) {
55
+ if (property.enum) {
56
+ return z.enum(property.enum);
57
+ }
58
+ switch (property.type) {
59
+ case 'string':
60
+ return z.string();
61
+ case 'integer':
62
+ case 'number':
63
+ return z.number();
64
+ case 'boolean':
65
+ return z.boolean();
66
+ case 'array':
67
+ return z.array(z.any());
68
+ case 'object':
69
+ return z.record(z.string(), z.unknown());
70
+ default:
71
+ throw new Error(`${name} is typed ${String(property.type)}, which a component's property cannot be.`);
72
+ }
73
+ }
74
+ /** The names a component shows and sends through, each once. */
75
+ export function bindingNames(component) {
76
+ const { shows, sends } = component.bindings;
77
+ return [...new Set([...shows, ...sends])];
78
+ }
79
+ /**
80
+ * The A2UI schema of a component of Datalayer's own: its properties as its
81
+ * JSON Schema has them, its bindings, its `action`, and the `weight` every
82
+ * basic component takes inside a Row or a Column. Strict, as the basic
83
+ * catalog's are: a key it does not declare refuses the tree.
84
+ */
85
+ export function ownComponentSchema(id) {
86
+ const component = ownComponent(id);
87
+ const json = component.properties;
88
+ const required = new Set(json.required ?? []);
89
+ const shape = {};
90
+ for (const [name, property] of Object.entries(json.properties ?? {})) {
91
+ const schema = propertySchema(name, property);
92
+ shape[name] = required.has(name) ? schema : schema.optional();
93
+ }
94
+ for (const name of bindingNames(component)) {
95
+ if (name in shape) {
96
+ throw new Error(`${id} both declares ${name} as a property and binds through it.`);
97
+ }
98
+ shape[name] = BindingSchema.optional();
99
+ }
100
+ shape.action = ActionSchema
101
+ .describe('REF:#/$defs/Action|What it dispatches when a person acts on it.')
102
+ .optional();
103
+ shape.weight = z
104
+ .number()
105
+ .describe('The relative weight of this component within a Row or Column.')
106
+ .optional();
107
+ return z.object(shape).strict();
108
+ }
@@ -3,3 +3,4 @@ export * from './A2uiSurfaceComposed';
3
3
  export * from './styles';
4
4
  export * from './visibleWhen';
5
5
  export * from './visibility';
6
+ export * from './datalayer';
@@ -7,3 +7,4 @@ export * from './A2uiSurfaceComposed';
7
7
  export * from './styles';
8
8
  export * from './visibleWhen';
9
9
  export * from './visibility';
10
+ export * from './datalayer';
@@ -11,7 +11,9 @@ export declare function nodeIsShown(props: Record<string, unknown>): boolean;
11
11
  /** A component, its schema declaring `visible_when`, drawn behind the guard. */
12
12
  export declare function withVisibleWhen(component: ReactComponentImplementation): ReactComponentImplementation;
13
13
  /**
14
- * The catalog Datalayer's renderer draws from: A2UI's basic catalog, under
15
- * its own id — the payloads name it — with `visible_when` on every component.
14
+ * The catalog Datalayer's renderer draws from: A2UI's basic catalog and
15
+ * Datalayer's own components (Table, Chart, File upload, Chat, Evidence,
16
+ * Form — `./datalayer`), under the basic catalog's id — the payloads name
17
+ * it — with `visible_when` on every component.
16
18
  */
17
19
  export declare const datalayerCatalog: Catalog<ReactComponentImplementation, import("@a2ui/web_core/v0_9").FunctionImplementation>;
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { basicCatalog, useSignalValue, } from '@a2ui/react/v0_9';
3
3
  import { Catalog, DynamicBooleanSchema, ResolvedBinding, isWritable, } from '@a2ui/web_core/v0_9';
4
4
  import { VISIBLE_WHEN, isShown } from './visibility';
5
+ import { OWN_COMPONENTS } from './datalayer';
5
6
  /**
6
7
  * Whether a node's resolved properties let it be drawn: no condition, or one
7
8
  * that holds. A2UI resolves a dynamic property to a binding whether the block
@@ -48,7 +49,9 @@ export function withVisibleWhen(component) {
48
49
  };
49
50
  }
50
51
  /**
51
- * The catalog Datalayer's renderer draws from: A2UI's basic catalog, under
52
- * its own id — the payloads name it — with `visible_when` on every component.
52
+ * The catalog Datalayer's renderer draws from: A2UI's basic catalog and
53
+ * Datalayer's own components (Table, Chart, File upload, Chat, Evidence,
54
+ * Form — `./datalayer`), under the basic catalog's id — the payloads name
55
+ * it — with `visible_when` on every component.
53
56
  */
54
- export const datalayerCatalog = new Catalog(basicCatalog.id, basicCatalog.protocolVersion, [...basicCatalog.components.values()].map(withVisibleWhen), [...basicCatalog.functions.values()], basicCatalog.themeSchema, basicCatalog.instructions);
57
+ export const datalayerCatalog = new Catalog(basicCatalog.id, basicCatalog.protocolVersion, [...basicCatalog.components.values(), ...OWN_COMPONENTS].map(withVisibleWhen), [...basicCatalog.functions.values()], basicCatalog.themeSchema, basicCatalog.instructions);
@@ -1,5 +1,6 @@
1
1
  import type { ReactorPlugin } from '@datalayer/reactor';
2
2
  import type { AppSpec } from '../../types/agentspecs';
3
+ import type { ThemeOverrides } from '../../types/chat';
3
4
  import { type LoopEmbedProps } from '../embed/LoopEmbed';
4
5
  import type { LoopPresetOptions } from '../presets';
5
6
  import type { PresenceState } from '../../chat/presence/presenceStatus';
@@ -79,5 +80,12 @@ export declare function appDatalayerCreatePayload(app: AppSpec, instance?: AppIn
79
80
  /** No host plugins: one array, so that it never reads as a change. */
80
81
  /** The agentspec a runtime is allocated with before an application's agent is created on it. */
81
82
  export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
83
+ /**
84
+ * The application's accent over the theme its conversation wears, in both
85
+ * modes (LOOP T-05): its bubbles and its one button in its own colour, and
86
+ * not in the theme's default — the chat sets its theme again inside it, so
87
+ * what the page around it set does not reach in.
88
+ */
89
+ export declare function appThemeOverrides(app: AppSpec): ThemeOverrides | undefined;
82
90
  export declare function AppRenderer({ app, plugins, instance, onPresence, frame, ...embed }: AppRendererProps): React.JSX.Element;
83
91
  export default AppRenderer;
@@ -34,6 +34,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
34
34
  * @module loop/apps/AppRenderer
35
35
  */
36
36
  import { useMemo } from 'react';
37
+ import { loopAccentStyles } from '@datalayer/primer-addons';
37
38
  import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
38
39
  import { APP_PAGE_SURFACE, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
39
40
  import { LoopEmbed } from '../embed/LoopEmbed';
@@ -158,6 +159,21 @@ export function appDatalayerCreatePayload(app, instance) {
158
159
  /** The agentspec a runtime is allocated with before an application's agent is created on it. */
159
160
  export const DATALAYER_BOOTSTRAP_AGENTSPEC = 'example-simple';
160
161
  const NO_PLUGINS = [];
162
+ /**
163
+ * The application's accent over the theme its conversation wears, in both
164
+ * modes (LOOP T-05): its bubbles and its one button in its own colour, and
165
+ * not in the theme's default — the chat sets its theme again inside it, so
166
+ * what the page around it set does not reach in.
167
+ */
168
+ export function appThemeOverrides(app) {
169
+ const accent = app.interface?.accent;
170
+ return accent
171
+ ? {
172
+ light: loopAccentStyles(accent, 'light'),
173
+ dark: loopAccentStyles(accent, 'dark'),
174
+ }
175
+ : undefined;
176
+ }
161
177
  export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, frame = false, ...embed }) {
162
178
  /*
163
179
  * The application plugin, made once per application. `LoopEmbed` rebuilds
@@ -198,6 +214,10 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, f
198
214
  const datalayerCreatePayload = useMemo(() => (app.agent ? appDatalayerCreatePayload(app, instance) : undefined),
199
215
  // eslint-disable-next-line react-hooks/exhaustive-deps
200
216
  [source, instance?.appUid, instance?.deploymentUid, instance?.version]);
217
+ const accent = app.interface?.accent;
218
+ const themeOverrides = useMemo(() => appThemeOverrides(app),
219
+ // eslint-disable-next-line react-hooks/exhaustive-deps
220
+ [accent]);
201
221
  if (!appPlugin) {
202
222
  return (_jsxs("div", { role: "status", style: { padding: 16 }, children: ["\u201C", app.name, "\u201D is run by a team, which the workspace does not run as an application yet."] }));
203
223
  }
@@ -217,7 +237,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, f
217
237
  targetFixed: true,
218
238
  // Applications first (LOOP T-12): an application's conversation wears
219
239
  // the `loop` theme; a host may say otherwise.
220
- themeVariant: "loop",
240
+ themeVariant: "loop", themeOverrides: themeOverrides,
221
241
  // Its own face, name and welcome in the chat (T-08); no counters: a
222
242
  // person using an application is not asking about tokens.
223
243
  presence: {
@@ -1,6 +1,8 @@
1
1
  import type { CSSProperties, JSX, ReactNode } from 'react';
2
+ import type { PluginRef } from '@datalayer/reactor';
2
3
  import type { AppAccent, AppEmbedMode, AppSpec } from '../../types/agentspecs';
3
4
  import { type AppInstance } from '../apps/AppRenderer';
5
+ import { type AssistantCharacterChosen } from '../plugins/assistant-characters';
4
6
  import { type EmbedColorMode } from './embedConfig';
5
7
  export type AppEmbedProps = {
6
8
  /** The application. */
@@ -29,7 +31,20 @@ export type AppEmbedProps = {
29
31
  * shadow root's styles stop at its edge. A React host leaves it off.
30
32
  */
31
33
  ownPortal?: boolean;
34
+ /**
35
+ * The host's plugins, for the assistant's character: what they contribute
36
+ * to `loop.assistant.character` is there beside Datalayer's four. The
37
+ * element passes none.
38
+ */
39
+ plugins?: PluginRef[];
32
40
  };
41
+ /**
42
+ * The character the assistant mode draws (T-24, D-07): the application's own
43
+ * (`interface.assistant`), else the paper clip, from what Datalayer's
44
+ * characters and the host's plugins contribute — as a page decides it, with
45
+ * no person's choice, since a visitor to another product has none here.
46
+ */
47
+ export declare function embedAssistantCharacter(app: AppSpec, plugins?: PluginRef[]): AssistantCharacterChosen;
33
48
  /** The `loop` theme, with the application's (or the host's) accent and face. */
34
49
  export declare function EmbedThemed({ accent, colorMode, font, children, style, }: {
35
50
  accent: AppAccent;
@@ -38,5 +53,5 @@ export declare function EmbedThemed({ accent, colorMode, font, children, style,
38
53
  children: ReactNode;
39
54
  style?: CSSProperties;
40
55
  }): JSX.Element;
41
- export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, height, ownPortal, }: AppEmbedProps): JSX.Element;
56
+ export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, height, ownPortal, plugins, }: AppEmbedProps): JSX.Element;
42
57
  export default AppEmbed;
@@ -17,7 +17,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
17
  * - **panel** — the conversation at the right edge of the page, at full
18
18
  * height;
19
19
  * - **assistant** — the application's character on the page, speaking in a
20
- * balloon (§6.9), the one its Appspec names (`interface.assistant`).
20
+ * balloon (§6.9): the one its Appspec names (`interface.assistant`), else
21
+ * the paper clip, looked up in what is contributed — Datalayer's four and
22
+ * the host's own plugins (T-24); an id nothing contributes is said, never
23
+ * replaced.
21
24
  *
22
25
  * The three floating modes are `ChatFloating`'s, given the application's
23
26
  * name, welcome, starters and accent, and its agent's endpoint — created
@@ -25,7 +28,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
28
  * rules there as it does inline.
26
29
  *
27
30
  * In the `loop` theme with the application's accent (T-12, T-05), which the
28
- * host may override with the face and the mode (D-11).
31
+ * host may override with the face and the mode (D-11) — around the
32
+ * conversation and inside it, where the chat sets its theme again.
29
33
  *
30
34
  * @module loop/embed/AppEmbed
31
35
  */
@@ -38,8 +42,20 @@ import { useAgentRuntimes } from '../../hooks/useAgentRuntimes';
38
42
  import { AppRenderer, DATALAYER_BOOTSTRAP_AGENTSPEC, appDatalayerCreatePayload, defineAppPlugin, } from '../apps/AppRenderer';
39
43
  import { LoopAgentBlueprint } from '../core';
40
44
  import { ensureServerAgent } from '../plugins/agents/switchable';
45
+ import { AssistantCharactersPlugin, assistantCharacterFor, assistantCharactersFrom, } from '../plugins/assistant-characters';
41
46
  import { floatingViewOf } from './embedConfig';
42
- import { embedThemeStyles } from './embedTheme';
47
+ import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
48
+ /** No host plugins: one array, so that it never reads as a change. */
49
+ const NO_PLUGINS = [];
50
+ /**
51
+ * The character the assistant mode draws (T-24, D-07): the application's own
52
+ * (`interface.assistant`), else the paper clip, from what Datalayer's
53
+ * characters and the host's plugins contribute — as a page decides it, with
54
+ * no person's choice, since a visitor to another product has none here.
55
+ */
56
+ export function embedAssistantCharacter(app, plugins = NO_PLUGINS) {
57
+ return assistantCharacterFor(assistantCharactersFrom([AssistantCharactersPlugin, ...plugins]), { app: app.interface.assistant });
58
+ }
43
59
  /** What the application's agent is created with on an agent-runtimes server. */
44
60
  function serverCreatePayload(app) {
45
61
  const blueprint = (defineAppPlugin(app).contributes ?? []).find((item) => item.point === LoopAgentBlueprint);
@@ -132,14 +148,14 @@ function Face({ emoji }) {
132
148
  return (_jsx("span", { "aria-hidden": true, style: { fontSize: 18, lineHeight: 1 }, children: emoji }));
133
149
  }
134
150
  /** The bubble, the panel and the assistant: `ChatFloating` in that mode. */
135
- function FloatingChat({ app, view, colorMode, agent, }) {
151
+ function FloatingChat({ app, view, colorMode, themeOverrides, character, agent, }) {
136
152
  const welcome = app.interface.welcome || app.description;
137
153
  return (_jsx(ChatFloating
138
154
  // A new agent is a new conversation.
139
- , { defaultViewMode: view, assistantCharacter: app.interface.assistant ?? 'paperclip', protocol: agent.protocol, authToken: agent.protocol?.authToken, title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, suggestions: app.interface.starters.map(starter => ({
155
+ , { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), protocol: agent.protocol, authToken: agent.protocol?.authToken, title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, suggestions: app.interface.starters.map(starter => ({
140
156
  title: starter.label,
141
157
  message: starter.message,
142
- })), themeVariant: "loop", colorMode: colorMode, showTokenUsage: false, showSettingsButton: false, showPoweredBy: false, useStore: false, launching: !agent.protocol && !agent.error, launchingMessage: agent.waiting, disabled: Boolean(agent.error), disableReason: agent.error }, agent.protocol?.endpoint ?? 'waiting'));
158
+ })), themeVariant: "loop", themeOverrides: themeOverrides, colorMode: colorMode, showTokenUsage: false, showSettingsButton: false, showPoweredBy: false, useStore: false, launching: !agent.protocol && !agent.error, launchingMessage: agent.waiting, disabled: Boolean(agent.error), disableReason: agent.error }, agent.protocol?.endpoint ?? 'waiting'));
143
159
  }
144
160
  /** Floating, its agent on the host's agent-runtimes server. */
145
161
  function ServerFloating({ serverUrl, ...props }) {
@@ -192,14 +208,27 @@ export function EmbedThemed({ accent, colorMode, font, children, style, }) {
192
208
  */
193
209
  _jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: loopTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
194
210
  }
195
- export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUrl, instance, height = 640, ownPortal = false, }) {
211
+ export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUrl, instance, height = 640, ownPortal = false, plugins = NO_PLUGINS, }) {
196
212
  const system = useSystemMode();
197
213
  const resolvedMode = colorMode === 'auto' ? system : colorMode;
198
214
  const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
199
215
  const view = floatingViewOf(shownAs);
200
- return (_jsxs(EmbedThemed, { accent: accent ?? app.interface.accent, colorMode: resolvedMode, font: font, style: view
216
+ const worn = accent ?? app.interface.accent;
217
+ const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font }), [worn, font]);
218
+ const named = app.interface.assistant;
219
+ const assistant = useMemo(() => view === 'assistant' ? embedAssistantCharacter(app, plugins) : undefined,
220
+ // eslint-disable-next-line react-hooks/exhaustive-deps
221
+ [view, named, plugins]);
222
+ if (assistant && 'problem' in assistant) {
223
+ return (_jsx("p", { role: "status", className: "datalayer-app-said", children: assistant.problem }));
224
+ }
225
+ const character = assistant?.character;
226
+ return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, style: view
201
227
  ? // Floating: nothing in the page's flow, nothing painted behind.
202
228
  { backgroundColor: 'transparent' }
203
- : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view && serverUrl ? (_jsx(ServerFloating, { app: app, view: view, serverUrl: serverUrl, colorMode: resolvedMode })) : view ? (_jsx(DatalayerFloating, { app: app, view: view, instance: instance, colorMode: resolvedMode })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance }))] }));
229
+ : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view && serverUrl ? (_jsx(ServerFloating, { app: app, view: view, serverUrl: serverUrl, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : view ? (_jsx(DatalayerFloating, { app: app, view: view, instance: instance, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance,
230
+ // The host's accent and face over the application's own, inside
231
+ // its conversation too.
232
+ themeOverrides: themeOverrides }))] }));
204
233
  }
205
234
  export default AppEmbed;
@@ -66,6 +66,8 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
66
66
  preset.datalayerAgentSpecId,
67
67
  preset.targetFixed,
68
68
  preset.themeVariant,
69
+ // By its content, as the payload below.
70
+ JSON.stringify(preset.themeOverrides ?? null),
69
71
  preset.presence?.name,
70
72
  preset.presence?.face,
71
73
  preset.presence?.welcome,
@@ -37,7 +37,7 @@ import { loopThemeStyles } from '@datalayer/primer-addons';
37
37
  import { coreStore } from '@datalayer/core/lib/state/substates/CoreState';
38
38
  import { parseAppspec } from '../apps/appspec';
39
39
  import { readAppspecYaml } from '../apps/yaml';
40
- import { AppEmbed } from './AppEmbed';
40
+ import { AppEmbed, embedAssistantCharacter } from './AppEmbed';
41
41
  import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, embedLookOf, inlineHeightOf, } from './embedConfig';
42
42
  import { embedShadowCss } from './embedTheme';
43
43
  /** What the framed run page and the element say to each other. */
@@ -362,6 +362,15 @@ export function defineDatalayerAppElement(options = {}) {
362
362
  this.say(error.message);
363
363
  return;
364
364
  }
365
+ // A character nothing contributes is said in place, as an attribute
366
+ // that is not one is (D-07, T-24).
367
+ if (look.mode === 'assistant') {
368
+ const chosen = embedAssistantCharacter(source.app);
369
+ if ('problem' in chosen) {
370
+ this.say(chosen.problem);
371
+ return;
372
+ }
373
+ }
365
374
  this.setAttribute('data-embed-mode', look.mode);
366
375
  const shadow = this.skeleton(loopThemeStyles.css);
367
376
  const mount = document.createElement('div');
@@ -1,5 +1,6 @@
1
1
  import { type ThemeStyles } from '@datalayer/primer-addons';
2
2
  import type { AppAccent } from '../../types/agentspecs';
3
+ import type { ThemeOverrides } from '../../types/chat';
3
4
  /**
4
5
  * The `loop` theme as the embed wears it: the application's accent over it
5
6
  * in both modes (T-05), and the host's face when it named one.
@@ -8,6 +9,17 @@ export declare function embedThemeStyles({ accent, font, }: {
8
9
  accent: AppAccent;
9
10
  font?: string;
10
11
  }): ThemeStyles;
12
+ /**
13
+ * The same accent and face laid over the conversation's own theme (T-05,
14
+ * D-11): the chat sets the `loop` theme again inside it, which would put the
15
+ * theme's mint and face back over what the embed set around it. The
16
+ * accent's properties in each mode, and, when the host named a face, every
17
+ * property of the theme that names its own.
18
+ */
19
+ export declare function embedThemeOverrides({ accent, font, }: {
20
+ accent: AppAccent;
21
+ font?: string;
22
+ }): ThemeOverrides;
11
23
  /**
12
24
  * The element's own stylesheet, written into its shadow root.
13
25
  *
@@ -30,6 +30,21 @@ export function embedThemeStyles({ accent, font = '', }) {
30
30
  dark: mode('dark'),
31
31
  };
32
32
  }
33
+ /**
34
+ * The same accent and face laid over the conversation's own theme (T-05,
35
+ * D-11): the chat sets the `loop` theme again inside it, which would put the
36
+ * theme's mint and face back over what the embed set around it. The
37
+ * accent's properties in each mode, and, when the host named a face, every
38
+ * property of the theme that names its own.
39
+ */
40
+ export function embedThemeOverrides({ accent, font = '', }) {
41
+ const mode = (which) => {
42
+ const theme = loopThemeStyles[which];
43
+ const faced = Object.fromEntries(Object.entries(withFace(theme, font)).filter(([name, value]) => typeof value === 'string' && value !== theme[name]));
44
+ return { ...faced, ...loopAccentStyles(accent, which) };
45
+ };
46
+ return { light: mode('light'), dark: mode('dark') };
47
+ }
33
48
  /**
34
49
  * The element's own stylesheet, written into its shadow root.
35
50
  *
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @module loop/plugins/assistant-characters
7
7
  */
8
- import { type ReactorPlugin } from '@datalayer/reactor';
8
+ import { type PluginRef, type ReactorPlugin } from '@datalayer/reactor';
9
9
  import { type AssistantCharacter } from '../../../chat/assistant/characters';
10
10
  import type { AssistantCharacterData } from '../../../chat/assistant/formats/types';
11
11
  import { LoopAssistantCharacter, type AssistantCharacterContribution } from '../../core';
@@ -22,6 +22,15 @@ export declare function assistantCharactersOf(reactor: ContributionsReader): {
22
22
  id: string;
23
23
  character: AssistantCharacter | AssistantCharacterData;
24
24
  }[];
25
+ /**
26
+ * What these plugins contribute, enabled on their own: for a host with no
27
+ * workspace to read them from — the embed's floating assistant (D-07) —
28
+ * which enables Datalayer's characters and whatever plugins it is given.
29
+ */
30
+ export declare function assistantCharactersFrom(plugins: PluginRef[]): {
31
+ id: string;
32
+ character: AssistantCharacter | AssistantCharacterData;
33
+ }[];
25
34
  /**
26
35
  * The character an application names, from what is contributed; an id no
27
36
  * enabled plugin contributes is an error that says which ids there are.
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module loop/plugins/assistant-characters
11
11
  */
12
- import { contribution, definePlugin, } from '@datalayer/reactor';
12
+ import { buildReactorFromPlugins, contribution, definePlugin, } from '@datalayer/reactor';
13
13
  import { ASSISTANT_CHARACTERS, DEFAULT_ASSISTANT_CHARACTER, } from '../../../chat/assistant/characters';
14
14
  import { LoopAssistantCharacter, } from '../../core';
15
15
  export const ASSISTANT_CHARACTERS_PLUGIN_NAME = '@datalayer/loop-plugin-assistant-characters';
@@ -26,6 +26,21 @@ export function assistantCharactersOf(reactor) {
26
26
  .getContributions(LoopAssistantCharacter)
27
27
  .map(entry => entry.value);
28
28
  }
29
+ /**
30
+ * What these plugins contribute, enabled on their own: for a host with no
31
+ * workspace to read them from — the embed's floating assistant (D-07) —
32
+ * which enables Datalayer's characters and whatever plugins it is given.
33
+ */
34
+ export function assistantCharactersFrom(plugins) {
35
+ const reactor = buildReactorFromPlugins(plugins);
36
+ reactor.start();
37
+ try {
38
+ return assistantCharactersOf(reactor);
39
+ }
40
+ finally {
41
+ reactor.stop();
42
+ }
43
+ }
29
44
  /**
30
45
  * The character an application names, from what is contributed; an id no
31
46
  * enabled plugin contributes is an error that says which ids there are.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Box, IconButton, SegmentedControl, Text, Truncate, } from '@primer/react';
4
- import { useColorPalette } from '@datalayer/primer-addons';
4
+ import { loopShapeVars, useColorPalette } from '@datalayer/primer-addons';
5
5
  import { ScreenFullIcon, ScreenNormalIcon } from '@primer/octicons-react';
6
6
  import { computed, signal } from '@datalayer/reactor';
7
7
  /* A signal to read when the layout publishes no live stance: the hook needs one. */
@@ -54,6 +54,11 @@ const NO_SURFACE = '';
54
54
  * small control reads as an error state, and this one is an invitation.
55
55
  */
56
56
  const FULLSCREEN_HINT_PERIOD_MS = 1400;
57
+ /**
58
+ * An application's face in the empty state: the page's size of the three a
59
+ * face is drawn at (LOOP T-19, `--loop-face-large`, 72px).
60
+ */
61
+ const FACE_LARGE = parseInt(loopShapeVars['--loop-face-large'], 10);
57
62
  export default function ChatView({ workspace }) {
58
63
  /* The active theme's own colours. Read from the store rather than a
59
64
  provider, so a workspace mounted without the addons theme still gets the
@@ -1326,7 +1331,14 @@ export default function ChatView({ workspace }) {
1326
1331
  mentionableAgents: mentionable,
1327
1332
  headerContent: inpromptMenu.length > 0 ? (_jsx(ReactorSlot, { slot: LoopSlots.inpromptMenu, props: { workspace } })) : undefined,
1328
1333
  footerExtras: promptActions.length > 0 ? (_jsx(ReactorSlot, { slot: LoopSlots.promptAction, props: { workspace } })) : undefined,
1329
- showAgentsMenu: true,
1334
+ /*
1335
+ An application's conversation keeps none of the session's controls
1336
+ under its composer — the agents, the model, the tools and their servers,
1337
+ the skills (LOOP T-17, T-18): a person using an application is not
1338
+ choosing them, and its builder chose them in its spec. An agent's chat
1339
+ keeps them.
1340
+ */
1341
+ showAgentsMenu: !presence,
1330
1342
  showInlineAgentsMenu: false,
1331
1343
  agents: footerAgents,
1332
1344
  selectedAgentId: selectedMemberId,
@@ -1336,18 +1348,18 @@ export default function ChatView({ workspace }) {
1336
1348
  showTokenUsage: chatExtras.showTokenUsage ?? config?.showTokenUsage ?? true,
1337
1349
  showContextRing: true,
1338
1350
  agentUsage: contextUsage ?? undefined,
1339
- showModelSelector: true,
1351
+ showModelSelector: !presence,
1340
1352
  models: offeredModels,
1341
1353
  decisions,
1342
1354
  selectedModel: activeModel,
1343
1355
  onModelSelect: model => void selectModel(model),
1344
- showToolsMenu: true,
1356
+ showToolsMenu: !presence,
1345
1357
  availableTools: offeredTools,
1346
1358
  mcpServers: configQuery.data?.mcpServers ?? [],
1347
1359
  // Live from the host example, when one feeds it: the footer's MCP status
1348
1360
  // indicator. (Codemode surfaces through the toggle below, not a blob.)
1349
1361
  mcpStatusData: chatExtras.mcpStatusData ?? undefined,
1350
- showSkillsMenu: true,
1362
+ showSkillsMenu: !presence,
1351
1363
  skills: skillsQuery.data?.skills ?? [],
1352
1364
  skillsLoading: skillsQuery.isLoading,
1353
1365
  enabledSkills,
@@ -1444,7 +1456,7 @@ export default function ChatView({ workspace }) {
1444
1456
  // The theme it wears, when the host names one (an application's).
1445
1457
  , {
1446
1458
  // The theme it wears, when the host names one (an application's).
1447
- themeVariant: config?.themeVariant,
1459
+ themeVariant: config?.themeVariant, themeOverrides: config?.themeOverrides,
1448
1460
  // The header says why, beside the title, for the same reason the
1449
1461
  // placeholder does: a dead control with no explanation is worse
1450
1462
  // than an absent one.
@@ -1483,9 +1495,9 @@ export default function ChatView({ workspace }) {
1483
1495
  // An application says what it is doing, in plain words, beside
1484
1496
  // its name (T-08); an agent's chat keeps its header as it is.
1485
1497
  headerContent: presence ? _jsx(PresenceLine, { state: presenceNow }) : undefined, emptyState: {
1486
- // An application's own face, large: the thing a person's eye
1487
- // lands on first (LOOP T-08).
1488
- icon: presence?.face ? (_jsx("span", { "aria-hidden": true, style: { fontSize: 48, lineHeight: 1 }, children: presence.face })) : (_jsx(BrandIcon, { size: 48 })),
1498
+ // An application's own face, at the page's size of the three
1499
+ // (LOOP T-08, T-19): the thing a person's eye lands on first.
1500
+ icon: presence?.face ? (_jsx("span", { "aria-hidden": true, style: { fontSize: FACE_LARGE, lineHeight: 1 }, children: presence.face })) : (_jsx(BrandIcon, { size: 48 })),
1489
1501
  // `ChatEmptyState` reads its heading from here and nowhere
1490
1502
  // else — the `title` above reaches the header only — so
1491
1503
  // without this the agent introduced itself as "Start a
@@ -1,4 +1,5 @@
1
1
  import type { PresenceState } from '../../../chat/presence/presenceStatus';
2
+ import type { ThemeOverrides } from '../../../types/chat';
2
3
  /** What a host may set on the chat. */
3
4
  /** Who is answering, as the person meets them. */
4
5
  export interface ChatPresence {
@@ -22,6 +23,11 @@ export type ChatPluginConfig = {
22
23
  * the theme's bubbles, pills and colours whatever the page around it wears.
23
24
  */
24
25
  themeVariant?: string;
26
+ /**
27
+ * Laid over that theme, by mode: an application's accent (LOOP T-05), so
28
+ * that its bubbles and its one button wear its colour and not the theme's.
29
+ */
30
+ themeOverrides?: ThemeOverrides;
25
31
  /**
26
32
  * Who is answering, as the person meets them (LOOP T-08): a name, a face
27
33
  * and a welcome, in place of the agent's id and a generic icon. An
@@ -23,6 +23,7 @@ import { type PluginRef } from '@datalayer/reactor';
23
23
  import { type AgentsConfig, type SandboxTarget } from './plugins/agents';
24
24
  import { type ChatPluginConfig, type ChatPresence } from './plugins/chat';
25
25
  import { type PageSize } from './plugins/page-layout';
26
+ import type { ThemeOverrides } from '../types/chat';
26
27
  export type LoopPresetOptions = {
27
28
  /** Where the agent runtimes service is. */
28
29
  serverUrl?: string;
@@ -140,6 +141,8 @@ export type LoopPresetOptions = {
140
141
  targetFixed?: boolean;
141
142
  /** The theme the conversation wears, by name; the person's unless said. */
142
143
  themeVariant?: string;
144
+ /** Laid over that theme, by mode: an application's accent (LOOP T-05). */
145
+ themeOverrides?: ThemeOverrides;
143
146
  /** Who is answering, as the person meets them (LOOP T-08). */
144
147
  presence?: ChatPresence;
145
148
  /** Whether the counters are shown under the prompt; on unless said. */
@@ -52,7 +52,7 @@ import { LoopPageLayoutPlugin } from './plugins/page-layout';
52
52
  * be installed or signed into, and the switches a demonstration wants are off.
53
53
  */
54
54
  export function loopPlugins(options = {}) {
55
- const { serverUrl, target = 'browser', defaultEditor: askedEditor, showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutArrangement = 'page', pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, targetFixed, themeVariant, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
55
+ const { serverUrl, target = 'browser', defaultEditor: askedEditor, showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutArrangement = 'page', pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, targetFixed, themeVariant, themeOverrides, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
56
56
  const defaultEditor = askedEditor ?? (editors ? 'notebook' : 'none');
57
57
  return [
58
58
  // The chat owns the editor beside it, so which one opens is its
@@ -70,6 +70,7 @@ export function loopPlugins(options = {}) {
70
70
  suggestionLabels,
71
71
  fullScreenTopOffset,
72
72
  themeVariant,
73
+ themeOverrides,
73
74
  presence,
74
75
  showTokenUsage,
75
76
  }),