@datalayer/agent-runtimes 1.3.83 → 1.3.84

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 (49) hide show
  1. package/lib/chat/base/ChatBase.js +16 -0
  2. package/lib/chat/base/loopElement.d.ts +53 -0
  3. package/lib/chat/base/loopElement.js +78 -0
  4. package/lib/chat/base/loopWindow.d.ts +31 -0
  5. package/lib/chat/base/loopWindow.js +74 -0
  6. package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
  7. package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
  8. package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
  9. package/lib/components/a2ui/datalayer/Form.js +9 -2
  10. package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
  11. package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
  12. package/lib/loop/apps/AppComposer.d.ts +39 -4
  13. package/lib/loop/apps/AppComposer.js +191 -19
  14. package/lib/loop/apps/AppRenderer.d.ts +20 -3
  15. package/lib/loop/apps/AppRenderer.js +58 -27
  16. package/lib/loop/apps/appspec.d.ts +2 -0
  17. package/lib/loop/apps/appspec.js +190 -11
  18. package/lib/loop/apps/checks.d.ts +6 -0
  19. package/lib/loop/apps/checks.js +170 -1
  20. package/lib/loop/apps/composer.d.ts +21 -10
  21. package/lib/loop/apps/composer.js +41 -8
  22. package/lib/loop/apps/language.d.ts +68 -0
  23. package/lib/loop/apps/language.js +275 -0
  24. package/lib/loop/apps/settingsInputs.d.ts +29 -0
  25. package/lib/loop/apps/settingsInputs.js +147 -0
  26. package/lib/loop/apps/uploads.d.ts +43 -0
  27. package/lib/loop/apps/uploads.js +66 -0
  28. package/lib/loop/core/index.d.ts +14 -1
  29. package/lib/loop/embed/AppEmbed.d.ts +14 -0
  30. package/lib/loop/embed/AppEmbed.js +64 -2
  31. package/lib/loop/embed/element.js +23 -0
  32. package/lib/loop/embed/embedSession.d.ts +14 -0
  33. package/lib/loop/embed/embedSession.js +34 -0
  34. package/lib/loop/embed/hostBridge.d.ts +22 -1
  35. package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
  36. package/lib/loop/plugins/app-elements/AppElements.js +76 -0
  37. package/lib/loop/plugins/app-elements/index.d.ts +20 -0
  38. package/lib/loop/plugins/app-elements/index.js +43 -0
  39. package/lib/loop/plugins/app-page/appPageModel.js +2 -0
  40. package/lib/loop/plugins/chat/ChatView.js +10 -2
  41. package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
  42. package/lib/protocols/BrowserAgentAdapter.js +13 -10
  43. package/lib/runtimes/browser/protocol.d.ts +1 -1
  44. package/lib/specs/apps.js +149 -0
  45. package/lib/specs/appspecSchema.js +439 -2
  46. package/lib/specs/uiPlugins.js +6 -1
  47. package/lib/types/agentspecs.d.ts +130 -1
  48. package/package.json +1 -1
  49. package/scripts/codegen/generate_apps.py +16 -0
@@ -0,0 +1,66 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The largest file a person may send an application, in megabytes. */
6
+ export const MAX_UPLOAD_MB = 25;
7
+ /** What a kind takes when it does not say, in megabytes. */
8
+ export const DEFAULT_UPLOAD_MB = 10;
9
+ /** How many files go with one message when it does not say. */
10
+ export const DEFAULT_MAX_FILES = 5;
11
+ /** A media type, a family of them, or an extension, lowercase. */
12
+ export const UPLOAD_KIND = /^(?:[a-z0-9][a-z0-9.+-]*\/(?:\*|[a-z0-9][a-z0-9.+-]*)|\.[a-z0-9][a-z0-9_+-]*)$/;
13
+ /**
14
+ * Whether a kind of file takes a file: an extension (`.csv`) by its name, a
15
+ * media type (`application/pdf`) or a family (`image/*`) by its type.
16
+ */
17
+ export function kindTakes(kind, file) {
18
+ const wanted = kind.trim().toLowerCase();
19
+ if (wanted.startsWith('.')) {
20
+ return file.name.toLowerCase().endsWith(wanted);
21
+ }
22
+ const type = (file.type || '').split(';', 1)[0].trim().toLowerCase();
23
+ if (wanted.endsWith('/*')) {
24
+ return type.startsWith(wanted.slice(0, -1));
25
+ }
26
+ return type === wanted;
27
+ }
28
+ /** Whether any of `kinds` takes a file; any file when there are none. */
29
+ export const acceptsFile = (kinds, file) => !kinds?.length || kinds.some(kind => kindTakes(kind, file));
30
+ const megabytes = (size) => `${(size / (1024 * 1024)).toFixed(1)} MB`;
31
+ /** The `accept` of a file input for what the application takes. */
32
+ export const uploadsAccept = (uploads) => uploads.kinds.map(kind => kind.type).join(',');
33
+ /**
34
+ * Why files sent without being asked are refused, in a sentence — the
35
+ * runtime's — or `null` when the application takes every one of them.
36
+ */
37
+ export function uploadsRefusal(app, files) {
38
+ if (files.length === 0) {
39
+ return null;
40
+ }
41
+ const uploads = app.interface.uploads;
42
+ if (!uploads) {
43
+ return `${app.name} takes no file sent with a message: its Appspec names none it takes (interface.uploads).`;
44
+ }
45
+ if (files.length > uploads.maxFiles) {
46
+ return `${files.length} files were sent at once: ${app.name} takes at most ${uploads.maxFiles}.`;
47
+ }
48
+ for (const file of files) {
49
+ const kind = uploads.kinds.find(candidate => kindTakes(candidate.type, file));
50
+ if (!kind) {
51
+ const taken = uploads.kinds.map(candidate => candidate.type).join(', ');
52
+ return `${file.name} is not a kind of file ${app.name} takes: it takes ${taken}.`;
53
+ }
54
+ if (file.size > kind.maxMb * 1024 * 1024) {
55
+ return `${file.name} is ${megabytes(file.size)}: ${app.name} takes ${kind.type} of at most ${kind.maxMb} MB.`;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /** What the composer says it takes, in a sentence. */
61
+ export function uploadsInWords(uploads) {
62
+ const kinds = uploads.kinds
63
+ .map(kind => `${kind.type} up to ${kind.maxMb} MB`)
64
+ .join(', ');
65
+ return `Takes ${kinds}; ${uploads.maxFiles} at most at once.`;
66
+ }
@@ -519,7 +519,7 @@ export type AgentBlueprintContribution = {
519
519
  * P-19) tell a run turned in the page, and the model it runs on — as the
520
520
  * runtime's session API applies them. Unsaid for an agent with no modes.
521
521
  */
522
- modeEffect?: (chosen: Record<string, string>) => AppModeEffect;
522
+ modeEffect?: (chosen: Record<string, string>, profile?: string) => AppModeEffect;
523
523
  };
524
524
  export declare const LoopAgentBlueprint: import("@datalayer/reactor").ContributionPoint<AgentBlueprintContribution>;
525
525
  /** One conversation opener a plugin offers on the empty chat. */
@@ -529,6 +529,12 @@ export type ChatSuggestionItem = {
529
529
  /** What is submitted; the chip's words when absent. */
530
530
  message?: string;
531
531
  emoji?: string;
532
+ /**
533
+ * The heading it is offered under: openers of one group together, under
534
+ * its name, those without one first — an application's starter's category
535
+ * (LOOP P-20).
536
+ */
537
+ group?: string;
532
538
  };
533
539
  /**
534
540
  * Conversation openers, contributed ahead of the spec's own.
@@ -649,6 +655,13 @@ export type LoopChatExtrasValue = {
649
655
  * offering a first question to somebody who has already asked one.
650
656
  */
651
657
  hideOpeners?: boolean;
658
+ /**
659
+ * Openers in place of the contributed ones, live: what an application
660
+ * offers with the profile picked in its composer (LOOP P-20) — the empty
661
+ * chat offers these, and changes them when the profile changes, without
662
+ * rebuilding the reactor. `hiddenOpeners` still applies.
663
+ */
664
+ openers?: ChatSuggestionItem[];
652
665
  /**
653
666
  * Told what the agent has used, each time the chat learns it.
654
667
  *
@@ -91,6 +91,20 @@ export declare function useEmbedSession({ app, serverUrl, embedToken, resume, }:
91
91
  embedToken?: string;
92
92
  resume?: boolean;
93
93
  }): EmbedSession;
94
+ /**
95
+ * Window messages from the page (LOOP P-25): on the host's own server, the
96
+ * page is handed how to post one to the application's session
97
+ * (`host.onWindowPort`) — the session the chat goes on with, its uid known
98
+ * from its first message — and what the code answers is raised on the page
99
+ * as `window-message` events. The session returned is the one to draw: its
100
+ * thread told when it starts.
101
+ */
102
+ export declare function useWindowPort({ serverUrl, embedToken, session, host, }: {
103
+ serverUrl?: string;
104
+ embedToken?: string;
105
+ session: EmbedSession;
106
+ host?: AppEmbedHost;
107
+ }): EmbedSession;
94
108
  /**
95
109
  * The page and the application talking (LOOP D-10): the tools of the host
96
110
  * the Appspec names, given to its agent through a chat-extras plugin made
@@ -52,7 +52,8 @@ import { floatingViewOf } from './embedConfig';
52
52
  import { createChatExtrasPlugin } from '../plugins/chat-extras';
53
53
  import { hostFrontendTools } from './hostBridge';
54
54
  import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
55
- import { embedSessionKey, reattachSession, sessionGoneSentence, sessionKeeper, visitOfToken, } from './embedSession';
55
+ import { WINDOW_NOT_YET, embedSessionKey, postWindowMessage, reattachSession, sessionGoneSentence, sessionKeeper, visitOfToken, } from './embedSession';
56
+ import { onLoopWindowMessage } from '../../chat/base/loopWindow';
56
57
  /**
57
58
  * The visit's session, kept and picked up again (LOOP D-13): on the host's
58
59
  * server with an embed token that names its visit, the session the chat
@@ -120,6 +121,55 @@ export function useEmbedSession({ app, serverUrl, embedToken, resume = true, })
120
121
  ...(found.said ? { said: found.said } : {}),
121
122
  };
122
123
  }
124
+ /**
125
+ * Window messages from the page (LOOP P-25): on the host's own server, the
126
+ * page is handed how to post one to the application's session
127
+ * (`host.onWindowPort`) — the session the chat goes on with, its uid known
128
+ * from its first message — and what the code answers is raised on the page
129
+ * as `window-message` events. The session returned is the one to draw: its
130
+ * thread told when it starts.
131
+ */
132
+ export function useWindowPort({ serverUrl, embedToken, session, host, }) {
133
+ const hostRef = useRef(host);
134
+ hostRef.current = host;
135
+ const given = session.thread;
136
+ const uid = useRef(given?.messages.length ? given.id : undefined);
137
+ // A thread of its own where the embed keeps none, so that its uid is known.
138
+ const own = useMemo(() => ({ id: generateMessageId(), messages: [] }), []);
139
+ const base = given ?? (serverUrl && session.ready ? own : undefined);
140
+ const thread = useMemo(() => base && {
141
+ ...base,
142
+ onStarted: (id) => {
143
+ uid.current = id;
144
+ base.onStarted?.(id);
145
+ },
146
+ }, [base]);
147
+ useEffect(() => {
148
+ const port = host?.onWindowPort;
149
+ if (!serverUrl || !port) {
150
+ return undefined;
151
+ }
152
+ port(async (data) => {
153
+ if (!uid.current) {
154
+ throw new Error(WINDOW_NOT_YET);
155
+ }
156
+ const said = await postWindowMessage({
157
+ serverUrl,
158
+ uid: uid.current,
159
+ ...(embedToken ? { token: embedToken } : {}),
160
+ data,
161
+ });
162
+ for (const answer of said) {
163
+ hostRef.current?.onEvent?.({
164
+ type: 'window-message',
165
+ detail: { data: answer },
166
+ });
167
+ }
168
+ });
169
+ return () => port(null);
170
+ }, [serverUrl, embedToken, host]);
171
+ return thread ? { ...session, thread } : session;
172
+ }
123
173
  /** What the embed says once of its session, above the conversation. */
124
174
  function SessionSaid({ said }) {
125
175
  return said ? (_jsx(Text, { as: "p", role: "status", sx: { px: 3, py: 2, m: 0, fontSize: 1 }, children: said })) : null;
@@ -146,6 +196,11 @@ export function useHostBridge(app, host) {
146
196
  // eslint-disable-next-line react-hooks/exhaustive-deps
147
197
  }, [handle, key]);
148
198
  const plugins = useMemo(() => (bridge ? [handle.plugin] : NO_HOST_PLUGINS), [bridge, handle]);
199
+ // What its code tells the page during a turn (LOOP P-25), raised on it.
200
+ useEffect(() => onLoopWindowMessage(data => hostRef.current?.onEvent?.({
201
+ type: 'window-message',
202
+ detail: { data },
203
+ })), []);
149
204
  const said = useRef({ answering: false });
150
205
  const before = useRef('idle');
151
206
  const told = useRef('');
@@ -278,7 +333,14 @@ export function EmbedThemed({ accent, colorMode, font, variant, children, style,
278
333
  }
279
334
  export function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, resume = true, }) {
280
335
  const bridge = useHostBridge(app, host);
281
- const session = useEmbedSession({ app, serverUrl, embedToken, resume });
336
+ const kept = useEmbedSession({ app, serverUrl, embedToken, resume });
337
+ // Window messages from the page, on the host's own server (P-25).
338
+ const session = useWindowPort({
339
+ serverUrl,
340
+ embedToken,
341
+ session: kept,
342
+ host,
343
+ });
282
344
  const system = useSystemMode();
283
345
  // The theme the application names (T-30), `loop` when none; its mode
284
346
  // the host's, else the application's, else the visitor's system's.
@@ -38,6 +38,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
38
38
  * when it called a function of the page, beside `decision` and
39
39
  * `token-expired`.
40
40
  *
41
+ * Window messages (LOOP P-25), on the host's own server: the element raises
42
+ * `window-message` (`detail.data`) when the application's code tells the page
43
+ * something outside the conversation (`session.send_window_message`), and
44
+ * `element.postWindowMessage(data)` hands the page's to its `@app.window`.
45
+ *
41
46
  * On the host's server (`server`) with the visit's embed token, the
42
47
  * session the application opens is kept in the page's `localStorage`, under
43
48
  * the application and the visit its token names, and picked up again after
@@ -54,6 +59,7 @@ import { coreStore } from '@datalayer/core/lib/state/substates/CoreState';
54
59
  import { parseAppspec } from '../apps/appspec';
55
60
  import { readAppspecYaml } from '../apps/yaml';
56
61
  import { AppEmbed, embedAssistantCharacter } from './AppEmbed';
62
+ import { WINDOW_NOT_YET } from './embedSession';
57
63
  import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, embedLookOf, inlineHeightOf, resumeOf, } from './embedConfig';
58
64
  import { embedShadowCss } from './embedTheme';
59
65
  /** What the framed run page and the element say to each other. */
@@ -193,10 +199,15 @@ export function defineDatalayerAppElement(options = {}) {
193
199
  passed = {};
194
200
  /** One object, its functions replaced in place: the host below holds it. */
195
201
  offered = {};
202
+ /** How a window message reaches the application's session, once it can (P-25). */
203
+ windowPort = null;
196
204
  /** What the page gives the application, read at each call (D-10). */
197
205
  host = {
198
206
  context: () => this.passed,
199
207
  functions: this.offered,
208
+ onWindowPort: post => {
209
+ this.windowPort = post;
210
+ },
200
211
  onEvent: event => this.dispatchEvent(new CustomEvent(event.type, {
201
212
  detail: event.detail,
202
213
  bubbles: true,
@@ -210,6 +221,18 @@ export function defineDatalayerAppElement(options = {}) {
210
221
  set context(value) {
211
222
  this.passed = { ...(value ?? {}) };
212
223
  }
224
+ /**
225
+ * Post a window message to the application (LOOP P-25): its code's
226
+ * `@app.window` reads it, and what it answers is raised as
227
+ * `window-message` events. Refused, in a sentence, before its
228
+ * conversation has started or off the host's own server.
229
+ */
230
+ postWindowMessage(data) {
231
+ const post = this.windowPort;
232
+ return post
233
+ ? post(data)
234
+ : Promise.reject(new Error(`datalayer-app: ${WINDOW_NOT_YET}`));
235
+ }
213
236
  /** The page's functions the application may call, by name (D-10). */
214
237
  get functions() {
215
238
  return this.offered;
@@ -75,5 +75,19 @@ export declare function reattachSession({ serverUrl, uid, token, fetcher, }: {
75
75
  token: string;
76
76
  fetcher?: typeof fetch;
77
77
  }): Promise<Reattached>;
78
+ /** Said when a window message is posted before the conversation has started. */
79
+ export declare const WINDOW_NOT_YET = "The application takes a window message once its conversation has started, on its own server (the \"server\" attribute).";
80
+ /**
81
+ * Post a window message to a session (LOOP P-25): its code's `@app.window`
82
+ * runs on it. Returns what the code told the page back; refused — the
83
+ * runtime's sentence — when the session or its code takes none.
84
+ */
85
+ export declare function postWindowMessage({ serverUrl, uid, token, data, fetcher, }: {
86
+ serverUrl: string;
87
+ uid: string;
88
+ token?: string;
89
+ data: unknown;
90
+ fetcher?: typeof fetch;
91
+ }): Promise<unknown[]>;
78
92
  /** What the page says, once, when the conversation it kept is gone. */
79
93
  export declare const sessionGoneSentence: (name: string) => string;
@@ -2,6 +2,7 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ import { windowTurnOf } from '../../chat/base/loopWindow';
5
6
  /** Where a visit's session is kept in the host's storage: `<prefix>:<app>:<visit>`. */
6
7
  export const EMBED_SESSION_KEY_PREFIX = 'datalayer-app:session';
7
8
  /**
@@ -132,5 +133,38 @@ export async function reattachSession({ serverUrl, uid, token, fetcher = fetch,
132
133
  return { kind: 'gone', uid };
133
134
  }
134
135
  }
136
+ /** Said when a window message is posted before the conversation has started. */
137
+ export const WINDOW_NOT_YET = 'The application takes a window message once its conversation has started, on its own server (the "server" attribute).';
138
+ /**
139
+ * Post a window message to a session (LOOP P-25): its code's `@app.window`
140
+ * runs on it. Returns what the code told the page back; refused — the
141
+ * runtime's sentence — when the session or its code takes none.
142
+ */
143
+ export async function postWindowMessage({ serverUrl, uid, token, data, fetcher = fetch, }) {
144
+ const response = await fetcher(`${serverUrl.replace(/\/+$/, '')}/api/v1/apps/sessions/${encodeURIComponent(uid)}/window`, {
145
+ method: 'POST',
146
+ headers: {
147
+ 'content-type': 'application/json',
148
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
149
+ },
150
+ body: JSON.stringify({ data }),
151
+ });
152
+ if (!response.ok) {
153
+ let detail = '';
154
+ try {
155
+ const body = (await response.json());
156
+ detail = typeof body.detail === 'string' ? body.detail : '';
157
+ }
158
+ catch {
159
+ // No sentence: the status says it.
160
+ }
161
+ throw new Error(detail || `The window message was refused (${response.status}).`);
162
+ }
163
+ const turn = windowTurnOf(await response.text());
164
+ if (turn.error) {
165
+ throw new Error(turn.error);
166
+ }
167
+ return turn.said;
168
+ }
135
169
  /** What the page says, once, when the conversation it kept is gone. */
136
170
  export const sessionGoneSentence = (name) => `Your earlier conversation with ${name} has ended, so this is a new one.`;
@@ -6,9 +6,16 @@
6
6
  * its Appspec names (`deployment.embedded.host.context`).
7
7
  *
8
8
  * Out: what the application does, as events — `message` when it has said
9
- * something, `action` when it called a function of the page — beside the
9
+ * something, `action` when it called a function of the page,
10
+ * `window-message` when its code told the page something outside the
11
+ * conversation (LOOP P-25, `session.send_window_message`) — beside the
10
12
  * `decision` and `token-expired` the element raises already.
11
13
  *
14
+ * Window messages, in (P-25): the page posts one with
15
+ * `element.postWindowMessage(data)`; its code's `@app.window` reads it, on
16
+ * the host's own server (`server`), and what it answers comes back as
17
+ * `window-message` events.
18
+ *
12
19
  * Offered: the functions of the page its Appspec names
13
20
  * (`deployment.embedded.host.functions`), each called by its agent as the
14
21
  * tool `host_<name>`. Every one of these tools is decided in the page by
@@ -42,7 +49,15 @@ export type HostEvent = {
42
49
  result?: unknown;
43
50
  error?: string;
44
51
  };
52
+ } | {
53
+ type: 'window-message';
54
+ /** What its code told the page (LOOP P-25): anything JSON writes. */
55
+ detail: {
56
+ data: unknown;
57
+ };
45
58
  };
59
+ /** Posts a window message to the application's session (LOOP P-25). */
60
+ export type WindowPost = (data: unknown) => Promise<void>;
46
61
  /** A function of the page, as the page gives it. */
47
62
  export type HostFunction = (args: Record<string, unknown>) => unknown | Promise<unknown>;
48
63
  /** What the page gives an embedded application (LOOP D-10). */
@@ -58,6 +73,12 @@ export type AppEmbedHost = {
58
73
  * may be made; the browser's `confirm` when the page gives none.
59
74
  */
60
75
  ask?: (question: HostQuestion) => boolean | Promise<boolean>;
76
+ /**
77
+ * Handed how a window message is posted to the application's session once
78
+ * the embed can (LOOP P-25) — on the host's own server, its conversation
79
+ * started — and `null` when it no longer can.
80
+ */
81
+ onWindowPort?: (post: WindowPost | null) => void;
61
82
  };
62
83
  /** What the person is asked before a call of the page is made. */
63
84
  export type HostQuestion = {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The elements an application's code opened beside the conversation (LOOP
3
+ * P-18): a side panel on the trailing edge, and a page of its own over the
4
+ * workspace — each drawn from the same A2UI surface an answer is drawn from
5
+ * (`InlineSurface`), its buttons sent to the application's actions as an
6
+ * answer's are (`answerAction`), and closed with its own button.
7
+ *
8
+ * Drawn from the workspace's root slot, positioned against the workspace:
9
+ * nothing is drawn, and nothing is laid out, while nothing is open.
10
+ *
11
+ * @module loop/plugins/app-elements/AppElements
12
+ */
13
+ import type { JSX } from 'react';
14
+ import { type LoopElement } from '../../../chat/base/loopElement';
15
+ import type { LoopWorkspaceContext } from '../../core';
16
+ /** How wide the side panel is, at most. */
17
+ export declare const APP_PANEL_WIDTH = 380;
18
+ /** What is open now: the panels in the order they opened, and the last page. */
19
+ export declare function elementsByPlace(open: readonly LoopElement[]): {
20
+ panels: LoopElement[];
21
+ page: LoopElement | null;
22
+ };
23
+ /** Every element open in a side panel or on a page, drawn where it belongs. */
24
+ export declare function AppElements({ workspace, }: {
25
+ workspace?: LoopWorkspaceContext;
26
+ }): JSX.Element | null;
27
+ export default AppElements;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useSyncExternalStore } from 'react';
3
+ import { IconButton, Text } from '@primer/react';
4
+ import { XIcon } from '@primer/octicons-react';
5
+ import { Box } from '@datalayer/primer-addons';
6
+ import { closeLoopElement, onLoopElements, openLoopElements, } from '../../../chat/base/loopElement';
7
+ import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
8
+ import { answerAction, readA2uiToolResult } from '../a2ui-surface/toolResult';
9
+ /** How wide the side panel is, at most. */
10
+ export const APP_PANEL_WIDTH = 380;
11
+ /** What is open now: the panels in the order they opened, and the last page. */
12
+ export function elementsByPlace(open) {
13
+ const pages = open.filter(element => element.where === 'page');
14
+ return {
15
+ panels: open.filter(element => element.where === 'panel'),
16
+ page: pages.length > 0 ? pages[pages.length - 1] : null,
17
+ };
18
+ }
19
+ function ElementBody({ element, send, }) {
20
+ const parsed = useMemo(() => readA2uiToolResult(element.shows, SURFACE_CATALOG_ID), [element.shows]);
21
+ const onAction = (action) => {
22
+ // A button of what its code shows: its action goes to the application,
23
+ // as a button of an answer does (LOOP P-04).
24
+ const pressed = answerAction(action);
25
+ if (pressed)
26
+ send?.(pressed.message, pressed.forwardedProps);
27
+ };
28
+ if (!parsed?.messages || parsed.messages.length === 0) {
29
+ return _jsx(Text, { sx: { color: 'fg.muted' }, children: "Nothing to show." });
30
+ }
31
+ return (_jsx(InlineSurface
32
+ // Drawn again whole when the code changes it.
33
+ , { messages: parsed.messages, onAction: onAction }, JSON.stringify(element.shows)));
34
+ }
35
+ function ElementHeader({ element }) {
36
+ return (_jsxs(Box, { sx: {
37
+ display: 'flex',
38
+ alignItems: 'center',
39
+ justifyContent: 'space-between',
40
+ gap: 2,
41
+ mb: 2,
42
+ }, children: [_jsx(Text, { as: "h2", sx: { fontSize: 2, fontWeight: 'bold', m: 0 }, children: element.title }), _jsx(IconButton, { icon: XIcon, variant: "invisible", size: "small", "aria-label": `Close ${element.title}`, onClick: () => closeLoopElement(element.id) })] }));
43
+ }
44
+ /** Every element open in a side panel or on a page, drawn where it belongs. */
45
+ export function AppElements({ workspace, }) {
46
+ const open = useSyncExternalStore(onLoopElements, openLoopElements, openLoopElements);
47
+ const { panels, page } = elementsByPlace(open);
48
+ const send = workspace?.viewControls.send;
49
+ if (panels.length === 0 && page === null)
50
+ return null;
51
+ return (_jsxs(_Fragment, { children: [panels.length > 0 ? (_jsx(Box, { as: "aside", "aria-label": "Side panel", sx: {
52
+ position: 'absolute',
53
+ top: 0,
54
+ right: 0,
55
+ bottom: 0,
56
+ width: `min(${APP_PANEL_WIDTH}px, 100%)`,
57
+ zIndex: 1,
58
+ overflowY: 'auto',
59
+ borderLeft: '1px solid',
60
+ borderColor: 'border.default',
61
+ bg: 'canvas.subtle',
62
+ boxShadow: 'shadow.medium',
63
+ p: 3,
64
+ display: 'flex',
65
+ flexDirection: 'column',
66
+ gap: 3,
67
+ }, children: panels.map(element => (_jsxs(Box, { as: "section", "aria-label": element.title, children: [_jsx(ElementHeader, { element: element }), _jsx(ElementBody, { element: element, send: send })] }, element.id))) })) : null, page !== null ? (_jsxs(Box, { role: "dialog", "aria-label": page.title, sx: {
68
+ position: 'absolute',
69
+ inset: 0,
70
+ zIndex: 2,
71
+ overflowY: 'auto',
72
+ bg: 'canvas.default',
73
+ p: [3, 4],
74
+ }, children: [_jsx(ElementHeader, { element: page }), _jsx(ElementBody, { element: page, send: send })] })) : null] }));
75
+ }
76
+ export default AppElements;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `app-elements` — what an application's code opens beside the
3
+ * conversation (LOOP P-18): `session.show(..., where="panel")` a side panel,
4
+ * `where="page"` a page of its own over the workspace, each closed by the
5
+ * code (`session.close`) or by the person. Inline, an element is a message,
6
+ * which the chat draws (P-04); this plugin draws the other two.
7
+ *
8
+ * The chat keeps what is open (`chat/base/loopElement`), from the session
9
+ * API's `loop.element` events; this plugin draws it from the workspace's
10
+ * root slot, positioned against the workspace, so that nothing is laid out
11
+ * while nothing is open.
12
+ *
13
+ * @module loop/plugins/app-elements
14
+ */
15
+ import { type ReactorPlugin } from '@datalayer/reactor';
16
+ export declare const APP_ELEMENTS_PLUGIN_NAME = "@datalayer/loop-plugin-app-elements";
17
+ /** The plugin that draws an application's side panel and pages. */
18
+ export declare const AppElementsPlugin: ReactorPlugin<Record<string, never>, unknown, unknown>;
19
+ export { AppElements, APP_PANEL_WIDTH, elementsByPlace } from './AppElements';
20
+ export default AppElementsPlugin;
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * `app-elements` — what an application's code opens beside the
7
+ * conversation (LOOP P-18): `session.show(..., where="panel")` a side panel,
8
+ * `where="page"` a page of its own over the workspace, each closed by the
9
+ * code (`session.close`) or by the person. Inline, an element is a message,
10
+ * which the chat draws (P-04); this plugin draws the other two.
11
+ *
12
+ * The chat keeps what is open (`chat/base/loopElement`), from the session
13
+ * API's `loop.element` events; this plugin draws it from the workspace's
14
+ * root slot, positioned against the workspace, so that nothing is laid out
15
+ * while nothing is open.
16
+ *
17
+ * @module loop/plugins/app-elements
18
+ */
19
+ import { definePlugin } from '@datalayer/reactor';
20
+ import { clearLoopElements } from '../../../chat/base/loopElement';
21
+ import { LoopSlots } from '../../core';
22
+ import { AppElements } from './AppElements';
23
+ export const APP_ELEMENTS_PLUGIN_NAME = '@datalayer/loop-plugin-app-elements';
24
+ /** The plugin that draws an application's side panel and pages. */
25
+ export const AppElementsPlugin = definePlugin({
26
+ name: APP_ELEMENTS_PLUGIN_NAME,
27
+ displayName: 'Side panel and pages',
28
+ description: 'What an application opens beside the conversation: a side panel, or a page of its own.',
29
+ octicon: 'sidebar-expand',
30
+ build: () => ({
31
+ components: [
32
+ {
33
+ id: 'app-elements',
34
+ slot: LoopSlots.root,
35
+ Component: AppElements,
36
+ },
37
+ ],
38
+ }),
39
+ // A workspace that goes takes what it had open with it.
40
+ register: () => clearLoopElements,
41
+ });
42
+ export { AppElements, APP_PANEL_WIDTH, elementsByPlace } from './AppElements';
43
+ export default AppElementsPlugin;
@@ -492,6 +492,8 @@ function settingsForm(app) {
492
492
  id: 'settings',
493
493
  component: 'Form',
494
494
  schema: app.interface.settings,
495
+ // Drawn with the inputs its uiSchema names (P-20).
496
+ ...(app.interface.settingsUi ? { ui: app.interface.settingsUi } : {}),
495
497
  values: { path: '/inputs' },
496
498
  },
497
499
  ]
@@ -372,11 +372,12 @@ export default function ChatView({ workspace }) {
372
372
  * itself (`modeEffect`, `BrowserAgentAdapter`).
373
373
  */
374
374
  const sendNow = useCallback((message, given) => {
375
- const loop = given?.loop;
375
+ const forwardedProps = runForwardedProps(runPropsRef.current.map(entry => entry.value), given);
376
+ // The page's files, or those attached in the composer (LOOP P-21).
377
+ const loop = forwardedProps?.loop;
376
378
  if (inPageRef.current && loop?.files && loop.files.length > 0) {
377
379
  return 'A file is given to an application running on a runtime: run it on Datalayer or on your machine to give it one.';
378
380
  }
379
- const forwardedProps = runForwardedProps(runPropsRef.current.map(entry => entry.value), given);
380
381
  // A new turn: whatever the panel showed is gone, this message is it.
381
382
  turnFeedRef.current?.begin(message, controlsRef.current?.thread());
382
383
  const send = controlsRef.current?.send;
@@ -752,6 +753,7 @@ export default function ChatView({ workspace }) {
752
753
  const suggestionEntries = useContributions(LoopChatSuggestion);
753
754
  const hiddenOpeners = chatExtras.hiddenOpeners;
754
755
  const hideOpeners = chatExtras.hideOpeners;
756
+ const liveOpeners = chatExtras.openers;
755
757
  const suggestions = useMemo(() => {
756
758
  if (hideOpeners) {
757
759
  return [];
@@ -760,6 +762,11 @@ export default function ChatView({ workspace }) {
760
762
  // the two-group merge below uses to tell openers apart.
761
763
  const hidden = new Set(hiddenOpeners ?? []);
762
764
  const offered = (items) => hidden.size === 0 ? items : items.filter(item => !hidden.has(item.text));
765
+ // What a plugin offers live (an application's profile's starters,
766
+ // LOOP P-20) wins over what was contributed when the reactor was built.
767
+ if (liveOpeners) {
768
+ return offered(liveOpeners);
769
+ }
763
770
  const contributed = suggestionEntries.flatMap(entry => entry.value.suggestions);
764
771
  if (contributed.length > 0) {
765
772
  return offered(contributed);
@@ -788,6 +795,7 @@ export default function ChatView({ workspace }) {
788
795
  agentId,
789
796
  hiddenOpeners,
790
797
  hideOpeners,
798
+ liveOpeners,
791
799
  ]);
792
800
  /*
793
801
  * The same openers in the shape the chat's empty state asks for.
@@ -70,7 +70,7 @@ export interface BrowserAgentAdapterConfig extends ProtocolAdapterConfig, Fronte
70
70
  * rules. Unsaid for an agent with no modes, which refuses a run sent in
71
71
  * some.
72
72
  */
73
- modeEffect?: (chosen: Record<string, string>) => AppModeEffect;
73
+ modeEffect?: (chosen: Record<string, string>, profile?: string) => AppModeEffect;
74
74
  }
75
75
  /**
76
76
  * An agent loop running in the page, speaking the protocol adapter interface.
@@ -120,12 +120,14 @@ export declare class BrowserAgentAdapter extends BaseProtocolAdapter {
120
120
  */
121
121
  private resolveModel;
122
122
  /**
123
- * What the modes a run is sent in tell it, and the model it runs on (LOOP
124
- * P-19) — the runtime's session API applies the same, for that run only.
123
+ * What the modes a run is sent in, and the profile its conversation is
124
+ * with, tell it, and the model it runs on (LOOP P-19, P-20) — the
125
+ * runtime's session API applies the same, for that run only.
125
126
  *
126
- * Refused, in a sentence, rather than run without them: modes sent to an
127
- * agent that has none, modes that are not the application's, and a mode
128
- * naming a model for an agent whose model is the host's own.
127
+ * Refused, in a sentence, rather than run without them: modes or a profile
128
+ * sent to an agent that has none, modes or a profile that are not the
129
+ * application's, and a model chosen for an agent whose model is the host's
130
+ * own.
129
131
  */
130
132
  private runModes;
131
133
  /**