@datalayer/agent-runtimes 1.3.82 → 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 (52) 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 +26 -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 +196 -39
  18. package/lib/loop/apps/checks.d.ts +6 -0
  19. package/lib/loop/apps/checks.js +187 -8
  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.d.ts +14 -5
  40. package/lib/loop/plugins/app-page/appPageModel.js +82 -88
  41. package/lib/loop/plugins/chat/ChatView.js +10 -2
  42. package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
  43. package/lib/protocols/BrowserAgentAdapter.js +13 -10
  44. package/lib/runtimes/browser/protocol.d.ts +1 -1
  45. package/lib/specs/agents/agents.js +6 -3
  46. package/lib/specs/apps.js +337 -176
  47. package/lib/specs/appspecSchema.js +455 -88
  48. package/lib/specs/cogs.js +3 -2
  49. package/lib/specs/uiPlugins.js +6 -1
  50. package/lib/types/agentspecs.d.ts +167 -11
  51. package/package.json +1 -1
  52. package/scripts/codegen/generate_apps.py +16 -1
@@ -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;
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * @module loop/plugins/app-page/appPageModel
25
25
  */
26
- import type { AppSettingSpec, AppSpec } from '../../../types/agentspecs';
26
+ import type { AppFormField, AppFormSchema, AppSpec } from '../../../types/agentspecs';
27
27
  import type { ChatTurnSnapshot, ConversationEntry } from '../../core';
28
28
  /** The id of the editor surface an application's page is drawn in. */
29
29
  export declare const APP_PAGE_SURFACE = "app-page";
@@ -77,6 +77,15 @@ export declare const APP_KIND_PATHS: Record<AppPageKind, AppKindPaths>;
77
77
  export declare const isAppPageKind: (kind: string) => kind is AppPageKind;
78
78
  /** The path a setting, an input of the page, is kept at. */
79
79
  export declare const inputPath: (id: string) => string;
80
+ /**
81
+ * The fields of an application's settings (C-16), by name, in the order its
82
+ * form says them; none when it has no settings.
83
+ */
84
+ export declare function settingFields(settings: AppFormSchema | undefined): Array<[string, AppFormField]>;
85
+ /** A setting as a person reads it: its field's `title`, else its name. */
86
+ export declare const settingTitle: (id: string, field: AppFormField) => string;
87
+ /** What each setting starts at: its field's `default`, for those that say one. */
88
+ export declare function settingDefaults(settings: AppFormSchema | undefined): Record<string, unknown>;
80
89
  /**
81
90
  * What an application publishes and takes, with its own settings: the kind's
82
91
  * list, and for a kind that has inputs each setting at `/inputs/<id>`, both
@@ -95,8 +104,6 @@ export declare function hasAppPage(app: Pick<AppSpec, 'kind' | 'interface' | 'ag
95
104
  * none): the application's layout is `chat`, the conversation alone.
96
105
  */
97
106
  export declare function surfaceUnshown(app: Pick<AppSpec, 'kind' | 'interface' | 'agent'>): string | null;
98
- /** A setting's first value, as its input holds it. */
99
- export declare function initialInput(setting: AppSettingSpec): unknown;
100
107
  /**
101
108
  * What the conversation publishes to the page, by path, now: from its
102
109
  * current turn, and for a chat its whole conversation.
@@ -136,8 +143,10 @@ export declare function givenFiles(files: unknown): {
136
143
  };
137
144
  /**
138
145
  * The application's settings as the page holds them, as the session API
139
- * takes them: a choice as the option chosen, a number as a number, a toggle
140
- * as on or off — each setting the page has a value for.
146
+ * takes them, each as its field's type says (C-16): a number as a number, a
147
+ * boolean as on or off, a word as the option chosen — a ChoicePicker holds a
148
+ * list of them — each setting the page has a value for. The runtime checks
149
+ * them against the settings' form.
141
150
  */
142
151
  export declare function settingsOf(app: Pick<AppSpec, 'interface'>, inputs: unknown): Record<string, unknown>;
143
152
  /**