@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
@@ -62,6 +62,8 @@ import { useAgentInspectorSink } from '../../components/inspector/agentInspector
62
62
  import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
63
63
  import { loopMessageChange, speakerOf, withLoopMessage } from './loopMessage';
64
64
  import { loopStepOf, withLoopStep } from './loopStep';
65
+ import { applyLoopElement, loopElementChange } from './loopElement';
66
+ import { loopWindowMessage, tellLoopWindow } from './loopWindow';
65
67
  // Tracks pending prompts already auto-sent for a given conversation scope.
66
68
  // This prevents layout-driven unmount/remount cycles from re-sending prompts.
67
69
  const sentPendingPromptKeys = new Set();
@@ -2724,6 +2726,20 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2724
2726
  setDisplayItems(prev => withLoopStep(prev, step));
2725
2727
  break;
2726
2728
  }
2729
+ // What its code tells the page it sits in, outside the
2730
+ // conversation (LOOP P-25): handed to whoever listens.
2731
+ const windowMessage = loopWindowMessage(event.activity);
2732
+ if (windowMessage) {
2733
+ tellLoopWindow(windowMessage.data);
2734
+ break;
2735
+ }
2736
+ // An element its code opened in a side panel or on a page of its
2737
+ // own, changed or closed (LOOP P-18): kept for whoever draws them.
2738
+ const element = loopElementChange(event.activity);
2739
+ if (element) {
2740
+ applyLoopElement(element);
2741
+ break;
2742
+ }
2727
2743
  // An application's code changed a message it sent: its author
2728
2744
  // said, or the message taken away (LOOP P-15).
2729
2745
  const change = loopMessageChange(event.activity);
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Where an element of an application's code shows (LOOP P-18): in a side
3
+ * panel beside the conversation, or on a page of its own over it. Inline,
4
+ * an element is a message, and the chat draws it as one (P-04).
5
+ *
6
+ * The session API says it with a `CUSTOM` event named `loop.element` —
7
+ * `{id, where, title, shows}`, `shows` the A2UI surface an answer's
8
+ * components are drawn from, or `{id, closed: true}` — which the AG-UI
9
+ * adapter hands on as an `activity`. The chat does not draw it: it keeps
10
+ * what is open here, and whoever draws panels and pages (the `app-elements`
11
+ * plugin) listens. One store per page, as there is one chat per
12
+ * application on it.
13
+ */
14
+ /** The `CUSTOM` event's name. */
15
+ export declare const LOOP_ELEMENT = "loop.element";
16
+ /** Where an element opens, besides inline. */
17
+ export type LoopElementWhere = 'panel' | 'page';
18
+ /** An element open in a side panel or on a page. */
19
+ export type LoopElement = {
20
+ id: string;
21
+ where: LoopElementWhere;
22
+ title: string;
23
+ /** The surface it draws: what `render_a2ui_surface` returns. */
24
+ shows: unknown;
25
+ };
26
+ /** What an event says: an element opened or changed, or one closed. */
27
+ export type LoopElementChange = LoopElement | {
28
+ id: string;
29
+ closed: true;
30
+ };
31
+ /** The change an activity says, or `null` when it says none. */
32
+ export declare function loopElementChange(activity: {
33
+ type: string;
34
+ data: unknown;
35
+ } | undefined): LoopElementChange | null;
36
+ /**
37
+ * The elements open with a change applied: one opened is added last, one
38
+ * changed is replaced where it is, one closed is taken away.
39
+ */
40
+ export declare function withLoopElement(open: readonly LoopElement[], change: LoopElementChange): LoopElement[];
41
+ /** The elements open now, in the order they opened. */
42
+ export declare const openLoopElements: () => LoopElement[];
43
+ /** Apply a change, and tell whoever draws them. */
44
+ export declare function applyLoopElement(change: LoopElementChange): void;
45
+ /**
46
+ * Close an element on this screen: the person closed it. The code is not
47
+ * told; it may show it again.
48
+ */
49
+ export declare const closeLoopElement: (id: string) => void;
50
+ /** Close every element: a new conversation starts with none. */
51
+ export declare function clearLoopElements(): void;
52
+ /** Listen for what is open. Returns the unsubscribe. */
53
+ export declare function onLoopElements(listener: (open: LoopElement[]) => void): () => void;
@@ -0,0 +1,78 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Where an element of an application's code shows (LOOP P-18): in a side
7
+ * panel beside the conversation, or on a page of its own over it. Inline,
8
+ * an element is a message, and the chat draws it as one (P-04).
9
+ *
10
+ * The session API says it with a `CUSTOM` event named `loop.element` —
11
+ * `{id, where, title, shows}`, `shows` the A2UI surface an answer's
12
+ * components are drawn from, or `{id, closed: true}` — which the AG-UI
13
+ * adapter hands on as an `activity`. The chat does not draw it: it keeps
14
+ * what is open here, and whoever draws panels and pages (the `app-elements`
15
+ * plugin) listens. One store per page, as there is one chat per
16
+ * application on it.
17
+ */
18
+ /** The `CUSTOM` event's name. */
19
+ export const LOOP_ELEMENT = 'loop.element';
20
+ /** The change an activity says, or `null` when it says none. */
21
+ export function loopElementChange(activity) {
22
+ if (!activity || activity.type !== LOOP_ELEMENT)
23
+ return null;
24
+ const data = activity.data;
25
+ if (!data || typeof data.id !== 'string' || !data.id)
26
+ return null;
27
+ if (data.closed === true)
28
+ return { id: data.id, closed: true };
29
+ if (data.where !== 'panel' && data.where !== 'page')
30
+ return null;
31
+ return {
32
+ id: data.id,
33
+ where: data.where,
34
+ title: typeof data.title === 'string' ? data.title : '',
35
+ shows: data.shows ?? null,
36
+ };
37
+ }
38
+ /**
39
+ * The elements open with a change applied: one opened is added last, one
40
+ * changed is replaced where it is, one closed is taken away.
41
+ */
42
+ export function withLoopElement(open, change) {
43
+ if ('closed' in change) {
44
+ return open.filter(element => element.id !== change.id);
45
+ }
46
+ const at = open.findIndex(element => element.id === change.id);
47
+ if (at < 0)
48
+ return [...open, change];
49
+ return open.map((element, index) => (index === at ? change : element));
50
+ }
51
+ let opened = [];
52
+ const listeners = new Set();
53
+ /** The elements open now, in the order they opened. */
54
+ export const openLoopElements = () => opened;
55
+ /** Apply a change, and tell whoever draws them. */
56
+ export function applyLoopElement(change) {
57
+ opened = withLoopElement(opened, change);
58
+ for (const listener of [...listeners])
59
+ listener(opened);
60
+ }
61
+ /**
62
+ * Close an element on this screen: the person closed it. The code is not
63
+ * told; it may show it again.
64
+ */
65
+ export const closeLoopElement = (id) => applyLoopElement({ id, closed: true });
66
+ /** Close every element: a new conversation starts with none. */
67
+ export function clearLoopElements() {
68
+ opened = [];
69
+ for (const listener of [...listeners])
70
+ listener(opened);
71
+ }
72
+ /** Listen for what is open. Returns the unsubscribe. */
73
+ export function onLoopElements(listener) {
74
+ listeners.add(listener);
75
+ return () => {
76
+ listeners.delete(listener);
77
+ };
78
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Window messages (LOOP P-25): what an application's code tells the page it
3
+ * sits in, outside the conversation — ``session.send_window_message(data)``.
4
+ *
5
+ * The session API says it with a `CUSTOM` event named `loop.window` —
6
+ * `{data}` — which the AG-UI adapter hands on as an `activity`. The chat
7
+ * does not draw it: it hands it to whoever listens here — the embed's host
8
+ * bridge, which raises it on the element as its `window-message` event.
9
+ */
10
+ /** The `CUSTOM` event's name. */
11
+ export declare const LOOP_WINDOW = "loop.window";
12
+ /** What an activity says to the page, or `null` when it is not a window message. */
13
+ export declare function loopWindowMessage(activity: {
14
+ type: string;
15
+ data: unknown;
16
+ } | undefined): {
17
+ data: unknown;
18
+ } | null;
19
+ /** Hand a window message to whoever listens. */
20
+ export declare function tellLoopWindow(data: unknown): void;
21
+ /** Listen for window messages. Returns the unsubscribe. */
22
+ export declare function onLoopWindowMessage(listener: (data: unknown) => void): () => void;
23
+ /**
24
+ * What a turn's stream says to the page — the `loop.window` events of the
25
+ * turn a window message posted by the page ran — and the sentence its
26
+ * `RUN_ERROR` said, if it failed.
27
+ */
28
+ export declare function windowTurnOf(stream: string): {
29
+ said: unknown[];
30
+ error: string | null;
31
+ };
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Window messages (LOOP P-25): what an application's code tells the page it
7
+ * sits in, outside the conversation — ``session.send_window_message(data)``.
8
+ *
9
+ * The session API says it with a `CUSTOM` event named `loop.window` —
10
+ * `{data}` — which the AG-UI adapter hands on as an `activity`. The chat
11
+ * does not draw it: it hands it to whoever listens here — the embed's host
12
+ * bridge, which raises it on the element as its `window-message` event.
13
+ */
14
+ /** The `CUSTOM` event's name. */
15
+ export const LOOP_WINDOW = 'loop.window';
16
+ /** What an activity says to the page, or `null` when it is not a window message. */
17
+ export function loopWindowMessage(activity) {
18
+ if (!activity || activity.type !== LOOP_WINDOW)
19
+ return null;
20
+ const said = activity.data;
21
+ if (!said || typeof said !== 'object' || !('data' in said))
22
+ return null;
23
+ return { data: said.data };
24
+ }
25
+ const listeners = new Set();
26
+ /** Hand a window message to whoever listens. */
27
+ export function tellLoopWindow(data) {
28
+ for (const listener of [...listeners])
29
+ listener(data);
30
+ }
31
+ /** Listen for window messages. Returns the unsubscribe. */
32
+ export function onLoopWindowMessage(listener) {
33
+ listeners.add(listener);
34
+ return () => {
35
+ listeners.delete(listener);
36
+ };
37
+ }
38
+ /**
39
+ * What a turn's stream says to the page — the `loop.window` events of the
40
+ * turn a window message posted by the page ran — and the sentence its
41
+ * `RUN_ERROR` said, if it failed.
42
+ */
43
+ export function windowTurnOf(stream) {
44
+ const said = [];
45
+ let error = null;
46
+ for (const block of stream.split('\n\n')) {
47
+ const data = block
48
+ .split('\n')
49
+ .filter(line => line.startsWith('data:'))
50
+ .map(line => line.slice('data:'.length).trim())
51
+ .join('');
52
+ if (!data)
53
+ continue;
54
+ let event;
55
+ try {
56
+ event = JSON.parse(data);
57
+ }
58
+ catch {
59
+ continue;
60
+ }
61
+ if (event.type === 'RUN_ERROR') {
62
+ error = String(event.message ?? 'The turn failed.');
63
+ }
64
+ else if (event.type === 'CUSTOM') {
65
+ const message = loopWindowMessage({
66
+ type: String(event.name),
67
+ data: event.value,
68
+ });
69
+ if (message)
70
+ said.push(message.data);
71
+ }
72
+ }
73
+ return { said, error };
74
+ }
@@ -14,8 +14,16 @@ export type GivenFile = {
14
14
  size: number;
15
15
  data_url: string;
16
16
  };
17
- /** Why a file is refused, or `null` when it is taken. */
18
- export declare function refusal(file: Pick<File, 'name' | 'size'>, accept: string[] | undefined, maxMb: number): string | null;
17
+ /**
18
+ * Why a file is refused, or `null` when it is taken: its kinds are
19
+ * extensions (`.csv`), media types (`application/pdf`) or families
20
+ * (`image/*`), as the Appspec's uploads say them (LOOP P-21).
21
+ */
22
+ export declare function refusal(file: Pick<File, 'name' | 'size'> & {
23
+ type?: string;
24
+ }, accept: string[] | undefined, maxMb: number): string | null;
25
+ /** A file as the application receives it: its content as a data URL. */
26
+ export declare const readFile: (file: File) => Promise<GivenFile>;
19
27
  export declare function FileUploadView({ props }: {
20
28
  props: FileUploadProps;
21
29
  }): import("react").JSX.Element;
@@ -21,22 +21,25 @@ import { UploadIcon } from '@primer/octicons-react';
21
21
  import { Box } from '@datalayer/primer-addons';
22
22
  import { BlockFrame, CARD_RADIUS, Problem, Quiet, isRecord } from './parts';
23
23
  import { ownImplementation } from './implementation';
24
+ import { acceptsFile } from '../../../loop/apps/uploads';
24
25
  /** The default the catalog gives `max_mb`. */
25
26
  const MAX_MB = 25;
26
- /** Why a file is refused, or `null` when it is taken. */
27
+ /**
28
+ * Why a file is refused, or `null` when it is taken: its kinds are
29
+ * extensions (`.csv`), media types (`application/pdf`) or families
30
+ * (`image/*`), as the Appspec's uploads say them (LOOP P-21).
31
+ */
27
32
  export function refusal(file, accept, maxMb) {
28
- if (accept?.length) {
29
- const lower = file.name.toLowerCase();
30
- if (!accept.some(extension => lower.endsWith(extension.toLowerCase()))) {
31
- return `${file.name} is not one it takes (${accept.join(', ')}).`;
32
- }
33
+ if (!acceptsFile(accept, { name: file.name, type: file.type ?? '' })) {
34
+ return `${file.name} is not one it takes (${(accept ?? []).join(', ')}).`;
33
35
  }
34
36
  if (file.size > maxMb * 1024 * 1024) {
35
37
  return `${file.name} is larger than ${maxMb} MB.`;
36
38
  }
37
39
  return null;
38
40
  }
39
- const readFile = (file) => new Promise((resolve, reject) => {
41
+ /** A file as the application receives it: its content as a data URL. */
42
+ export const readFile = (file) => new Promise((resolve, reject) => {
40
43
  const reader = new FileReader();
41
44
  reader.onload = () => resolve({
42
45
  name: file.name,
@@ -2,6 +2,8 @@ import { type OwnCommon } from './implementation';
2
2
  export type FormProps = OwnCommon & {
3
3
  title?: string;
4
4
  schema: Record<string, unknown>;
5
+ /** How its fields are drawn: a uiSchema by field name (P-20). */
6
+ ui?: Record<string, unknown>;
5
7
  submit_label?: string;
6
8
  values?: unknown;
7
9
  setValues: (values: Record<string, unknown>) => void;
@@ -12,6 +12,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
12
12
  * its action is dispatched, so the action's context reads them. A form the
13
13
  * schema refuses says why beside each field and sends nothing.
14
14
  *
15
+ * A form without an action is a set of settings (C-16) — an application's
16
+ * `interface.settings` drawn beside its conversation: no button, its values
17
+ * written where `values` points as they are filled, for the next run to take;
18
+ * the runtime checks them against the same schema.
19
+ *
20
+ * Its `ui` says how its fields are drawn (LOOP P-20): a uiSchema by field
21
+ * name, `ui:widget` one of primer-rjsf's widgets or of `switch` and `tags`
22
+ * (`formWidgets`) — the nine inputs a setting is drawn with.
23
+ *
15
24
  * @module components/a2ui/datalayer/Form
16
25
  */
17
26
  import { useEffect, useMemo, useState } from 'react';
@@ -19,6 +28,7 @@ import { Form as SchemaForm } from '@datalayer/primer-rjsf';
19
28
  import validator from '@rjsf/validator-ajv8';
20
29
  import { BlockFrame, Problem, isRecord } from './parts';
21
30
  import { ownImplementation } from './implementation';
31
+ import { FORM_WIDGETS } from './formWidgets';
22
32
  /** The default the catalog gives `submit_label`. */
23
33
  const SUBMIT_LABEL = 'Send';
24
34
  export function FormView({ props }) {
@@ -34,11 +44,25 @@ export function FormView({ props }) {
34
44
  useEffect(() => {
35
45
  setLocal(JSON.parse(shownKey));
36
46
  }, [shownKey]);
37
- const uiSchema = useMemo(() => ({ 'ui:submitButtonOptions': { submitText: submitLabel } }), [submitLabel]);
47
+ // Without an action, nothing is sent: no button, the values written as filled.
48
+ const live = action === undefined;
49
+ const drawn = JSON.stringify(props.ui ?? {});
50
+ const uiSchema = useMemo(() => ({
51
+ ...JSON.parse(drawn),
52
+ 'ui:submitButtonOptions': live
53
+ ? { norender: true }
54
+ : { submitText: submitLabel },
55
+ }), [drawn, live, submitLabel]);
38
56
  const problem = schema.type !== undefined && schema.type !== 'object'
39
57
  ? 'Its fields are not an object schema: a form asks for several named values.'
40
58
  : null;
41
- return (_jsx(BlockFrame, { title: title, label: "Form", weight: props.weight, testId: "a2ui-form", children: problem ? (_jsx(Problem, { children: problem })) : (_jsx(SchemaForm, { schema: schema, uiSchema: uiSchema, formData: values, validator: validator, liveValidate: touched, onBlur: () => setTouched(true), onError: () => setTouched(true), showErrorList: false, onChange: event => setLocal((event.formData ?? {})), onSubmit: event => {
59
+ return (_jsx(BlockFrame, { title: title, label: "Form", weight: props.weight, testId: "a2ui-form", children: problem ? (_jsx(Problem, { children: problem })) : (_jsx(SchemaForm, { schema: schema, uiSchema: uiSchema, widgets: FORM_WIDGETS, formData: values, validator: validator, liveValidate: touched, onBlur: () => setTouched(true), onError: () => setTouched(true), showErrorList: false, onChange: event => {
60
+ const filled = (event.formData ?? {});
61
+ setLocal(filled);
62
+ if (live) {
63
+ setValues(filled);
64
+ }
65
+ }, onSubmit: event => {
42
66
  setValues((event.formData ?? {}));
43
67
  action?.();
44
68
  } })) }));
@@ -0,0 +1,19 @@
1
+ /** What rjsf gives a widget, of what these two read. */
2
+ export type WidgetProps = {
3
+ id: string;
4
+ value: unknown;
5
+ label: string;
6
+ disabled?: boolean;
7
+ readonly?: boolean;
8
+ placeholder?: string;
9
+ onChange: (value: unknown) => void;
10
+ };
11
+ /** A `boolean` as a toggle switch. */
12
+ export declare function SwitchWidget({ id, value, label, disabled, readonly, onChange, }: WidgetProps): import("react").JSX.Element;
13
+ /** An `array` of `string` items as tokens: typed, then Enter or a comma. */
14
+ export declare function TagsWidget({ id, value, label, disabled, readonly, placeholder, onChange, }: WidgetProps): import("react").JSX.Element;
15
+ /** What a form's uiSchema may name besides primer-rjsf's own widgets. */
16
+ export declare const FORM_WIDGETS: {
17
+ switch: typeof SwitchWidget;
18
+ tags: typeof TagsWidget;
19
+ };
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * The two inputs of the nine (LOOP P-20) that `@datalayer/primer-rjsf` does
8
+ * not draw itself, as rjsf widgets a form's uiSchema names (`ui:widget`):
9
+ *
10
+ * - `switch` — a `boolean` as Primer's toggle switch;
11
+ * - `tags` — an `array` of `string` items as words in a field, each a token:
12
+ * Enter or a comma adds what is typed, a token's cross takes it away.
13
+ *
14
+ * The seven others are primer-rjsf's: Select (an `enum`), Slider (`range`),
15
+ * TextInput (a `string`, or `textarea`), Checkbox (a `boolean`), DatePicker
16
+ * (`format: date`), MultiSelect (an `array` of `enum` items, or
17
+ * `checkboxes`) and RadioGroup (`radio`).
18
+ *
19
+ * @module components/a2ui/datalayer/formWidgets
20
+ */
21
+ import { useState } from 'react';
22
+ import { Box, FormControl, Text, TextInputWithTokens, ToggleSwitch, } from '@primer/react';
23
+ /** A `boolean` as a toggle switch. */
24
+ export function SwitchWidget({ id, value, label, disabled, readonly, onChange, }) {
25
+ return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsx(Text, { as: "span", id: `${id}-label`, sx: { fontWeight: 'semibold' }, children: label }), _jsx(ToggleSwitch, { size: "small", "aria-labelledby": `${id}-label`, checked: Boolean(value), disabled: disabled || readonly, onClick: () => onChange(!value) })] }));
26
+ }
27
+ /** An `array` of `string` items as tokens: typed, then Enter or a comma. */
28
+ export function TagsWidget({ id, value, label, disabled, readonly, placeholder, onChange, }) {
29
+ const tags = Array.isArray(value) ? value.map(String) : [];
30
+ const [typed, setTyped] = useState('');
31
+ const add = () => {
32
+ const word = typed.trim();
33
+ if (word && !tags.includes(word)) {
34
+ onChange([...tags, word]);
35
+ }
36
+ setTyped('');
37
+ };
38
+ return (_jsxs(FormControl, { id: id, disabled: disabled || readonly, children: [_jsx(FormControl.Label, { children: label }), _jsx(TextInputWithTokens, { block: true, placeholder: placeholder, tokens: tags.map((text, index) => ({ id: index, text })), value: typed, onChange: event => setTyped(event.target.value.replace(',', '')), onKeyDown: event => {
39
+ if (event.key === 'Enter' || event.key === ',') {
40
+ event.preventDefault();
41
+ add();
42
+ }
43
+ }, onBlur: add, onTokenRemove: index => {
44
+ onChange(tags.filter((_, at) => at !== index));
45
+ } })] }));
46
+ }
47
+ /** What a form's uiSchema may name besides primer-rjsf's own widgets. */
48
+ export const FORM_WIDGETS = {
49
+ switch: SwitchWidget,
50
+ tags: TagsWidget,
51
+ };
@@ -9,17 +9,52 @@
9
9
  * one menu each; the option picked goes with every run as
10
10
  * `forwardedProps.loop.modes` (`LoopRunProps`), and the runtime tells the
11
11
  * agent its instructions, on the model it names, for that run.
12
+ * - What a person may **send** without being asked (LOOP P-21,
13
+ * `interface.uploads`): a paper clip beside the composer takes images,
14
+ * files and recordings of the kinds and sizes the Appspec says — one it
15
+ * does not take is refused in a sentence and never read — and the files
16
+ * held go with the next message as `forwardedProps.loop.files`, then are
17
+ * let go. The runtime refuses again what was sent all the same.
18
+ * - Its **profiles** (LOOP P-20, `interface.profiles`): a picker beside the
19
+ * composer, on the first unless another is picked. The profile picked goes
20
+ * with every run as `forwardedProps.loop.profile`, and its starters (else
21
+ * the application's) are what the empty chat offers, by category
22
+ * (`LoopChatExtras.openers`). Once the first message is sent, the
23
+ * conversation keeps its profile: the picker says so and picks no other.
24
+ *
25
+ * Given the application as the person reads it (`translatedAppspec`, LOOP
26
+ * P-26), its words are theirs; the composer's own, `language`'s.
12
27
  *
13
28
  * @module loop/apps/AppComposer
14
29
  */
15
- import type { JSX } from 'react';
16
- import type { AppSpec } from '../../types/agentspecs';
30
+ import { type JSX } from 'react';
31
+ import type { AppSpec, AppStarterSpec } from '../../types/agentspecs';
32
+ import { type GivenFile } from '../../components/a2ui/datalayer/FileUpload';
33
+ import { type ChatSuggestionItem } from '../core';
34
+ /** What the composer holds to send: the files, and why the last were refused. */
35
+ export type HeldUploads = {
36
+ files: GivenFile[];
37
+ refused: string | null;
38
+ };
39
+ /**
40
+ * Files chosen in the composer, held for the next message: the refusal the
41
+ * runtime would say for what the application does not take, else the files
42
+ * read. Nothing is read when one is refused.
43
+ */
44
+ export declare function holdUploads(app: Pick<AppSpec, 'name' | 'interface'>, held: readonly GivenFile[], chosen: readonly File[], read?: (file: File) => Promise<GivenFile>): Promise<HeldUploads>;
17
45
  /** The plugin's name, before the application's id. */
18
46
  export declare const APP_COMPOSER_PLUGIN_NAME = "@datalayer/loop-plugin-app-composer";
47
+ /** Starters as the empty chat offers them: each under its category (P-20). */
48
+ export declare function startersAsOpeners(starters: readonly AppStarterSpec[]): ChatSuggestionItem[];
49
+ /**
50
+ * Whether an application needs its composer plugin: commands, modes,
51
+ * uploads or profiles to offer there.
52
+ */
53
+ export declare const needsAppComposer: (app: AppSpec) => boolean;
19
54
  /**
20
55
  * The plugin that puts an application's commands in the composer's `/` menu
21
- * and its modes beside the composer. Nothing for an application with
22
- * neither: its plugin contributes nothing.
56
+ * and its modes, its profiles and its paper clip beside the composer.
57
+ * Nothing for an application with none: its plugin contributes nothing.
23
58
  */
24
59
  export declare function defineAppComposerPlugin(app: AppSpec): import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, {
25
60
  components: {