@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
@@ -1,17 +1,116 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { ActionList, ActionMenu } from '@primer/react';
3
- import { contribution, definePlugin, signal } from '@datalayer/reactor';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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
+ * An application's commands and modes in the composer (LOOP P-19).
8
+ *
9
+ * - Each of its **commands** is a workspace command (`LoopCommand`), listed
10
+ * in the composer's `/` menu (`composer`): picking it writes `/<name> `,
11
+ * and sending runs it — its prompt goes to the agent, the words typed
12
+ * after it in place of `{input}`.
13
+ * - Its **modes** are switches beside the composer (`LoopSlots.promptAction`),
14
+ * one menu each; the option picked goes with every run as
15
+ * `forwardedProps.loop.modes` (`LoopRunProps`), and the runtime tells the
16
+ * agent its instructions, on the model it names, for that run.
17
+ * - What a person may **send** without being asked (LOOP P-21,
18
+ * `interface.uploads`): a paper clip beside the composer takes images,
19
+ * files and recordings of the kinds and sizes the Appspec says — one it
20
+ * does not take is refused in a sentence and never read — and the files
21
+ * held go with the next message as `forwardedProps.loop.files`, then are
22
+ * let go. The runtime refuses again what was sent all the same.
23
+ * - Its **profiles** (LOOP P-20, `interface.profiles`): a picker beside the
24
+ * composer, on the first unless another is picked. The profile picked goes
25
+ * with every run as `forwardedProps.loop.profile`, and its starters (else
26
+ * the application's) are what the empty chat offers, by category
27
+ * (`LoopChatExtras.openers`). Once the first message is sent, the
28
+ * conversation keeps its profile: the picker says so and picks no other.
29
+ *
30
+ * Given the application as the person reads it (`translatedAppspec`, LOOP
31
+ * P-26), its words are theirs; the composer's own, `language`'s.
32
+ *
33
+ * @module loop/apps/AppComposer
34
+ */
35
+ import { useRef } from 'react';
36
+ import { ActionList, ActionMenu, IconButton, Text, Token } from '@primer/react';
37
+ import { PaperclipIcon } from '@primer/octicons-react';
38
+ import { computed, contribution, definePlugin, signal, } from '@datalayer/reactor';
4
39
  import { useSignalValue } from '@datalayer/reactor/react';
5
- import { LoopCommand, LoopRunProps, LoopSlots } from '../core';
6
- import { commandPrompt, modeChoice, modeDefaults } from './composer';
40
+ import { readFile, } from '../../components/a2ui/datalayer/FileUpload';
41
+ import { LoopChatExtras, LoopCommand, LoopRunProps, LoopSlots, } from '../core';
42
+ import { commandPrompt, modeChoice, modeDefaults, profileChoice, startersFor, } from './composer';
43
+ import { DEFAULT_LANGUAGE } from './appspec';
44
+ import { interfaceWords } from './language';
45
+ import { uploadsAccept, uploadsInWords, uploadsRefusal } from './uploads';
46
+ /**
47
+ * Files chosen in the composer, held for the next message: the refusal the
48
+ * runtime would say for what the application does not take, else the files
49
+ * read. Nothing is read when one is refused.
50
+ */
51
+ export async function holdUploads(app, held, chosen, read = readFile) {
52
+ const refused = uploadsRefusal(app, [
53
+ ...held,
54
+ ...chosen.map(file => ({
55
+ name: file.name,
56
+ type: file.type,
57
+ size: file.size,
58
+ })),
59
+ ]);
60
+ if (refused) {
61
+ return { files: [...held], refused };
62
+ }
63
+ return {
64
+ files: [...held, ...(await Promise.all(chosen.map(read)))],
65
+ refused: null,
66
+ };
67
+ }
7
68
  /** The plugin's name, before the application's id. */
8
69
  export const APP_COMPOSER_PLUGIN_NAME = '@datalayer/loop-plugin-app-composer';
70
+ /** Starters as the empty chat offers them: each under its category (P-20). */
71
+ export function startersAsOpeners(starters) {
72
+ return starters.map(starter => ({
73
+ text: starter.label,
74
+ message: starter.message,
75
+ ...(starter.category ? { group: starter.category } : {}),
76
+ }));
77
+ }
78
+ /**
79
+ * Whether an application needs its composer plugin: commands, modes,
80
+ * uploads or profiles to offer there.
81
+ */
82
+ export const needsAppComposer = (app) => (app.interface.commands ?? []).length > 0 ||
83
+ (app.interface.modes ?? []).length > 0 ||
84
+ (app.interface.profiles ?? []).length > 0 ||
85
+ Boolean(app.interface.uploads);
9
86
  /**
10
87
  * The plugin that puts an application's commands in the composer's `/` menu
11
- * and its modes beside the composer. Nothing for an application with
12
- * neither: its plugin contributes nothing.
88
+ * and its modes, its profiles and its paper clip beside the composer.
89
+ * Nothing for an application with none: its plugin contributes nothing.
13
90
  */
14
91
  export function defineAppComposerPlugin(app) {
92
+ const words = interfaceWords(app.interface.language ?? DEFAULT_LANGUAGE);
93
+ const profiles = app.interface.profiles ?? [];
94
+ /** The profile picked, and whether the conversation has started with it. */
95
+ const profile = signal(profileChoice(app)?.id);
96
+ const started = signal(false);
97
+ const openers = computed(() => ({
98
+ openers: startersAsOpeners(startersFor(app, profile.value)),
99
+ }));
100
+ function ProfilePicker() {
101
+ const now = profileChoice(app, useSignalValue(profile));
102
+ const kept = useSignalValue(started);
103
+ if (!now) {
104
+ return null;
105
+ }
106
+ const said = `${words.profile}: ${now.label}`;
107
+ if (kept) {
108
+ return (_jsx(Text, { title: `${said} (${words.profileKept})`, sx: { fontSize: 0, color: 'fg.muted', px: 2 }, children: said }));
109
+ }
110
+ return (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { size: "small", variant: "invisible", "aria-label": said, children: said }), _jsx(ActionMenu.Overlay, { width: "medium", children: _jsx(ActionList, { selectionVariant: "single", children: _jsxs(ActionList.Group, { children: [_jsx(ActionList.GroupHeading, { children: words.profile }), profiles.map(item => (_jsxs(ActionList.Item, { selected: item.id === now.id, onSelect: () => {
111
+ profile.value = profileChoice(app, item.id)?.id;
112
+ }, children: [item.label, item.description ? (_jsx(ActionList.Description, { variant: "block", children: item.description })) : null] }, item.id)))] }) }) })] }));
113
+ }
15
114
  const modes = app.interface.modes ?? [];
16
115
  /** The option of each mode the person is in. */
17
116
  const chosen = signal(modeDefaults(app));
@@ -28,13 +127,48 @@ export function defineAppComposerPlugin(app) {
28
127
  }, children: [option.label, option.description ? (_jsx(ActionList.Description, { variant: "block", children: option.description })) : null] }, option.id)))] }) }) })] }, mode.id));
29
128
  }) }));
30
129
  }
130
+ const uploads = app.interface.uploads;
131
+ /** The files held for the next message, and the last refusal (P-21). */
132
+ const held = signal({ files: [], refused: null });
133
+ function Attach() {
134
+ const now = useSignalValue(held);
135
+ const input = useRef(null);
136
+ if (!uploads) {
137
+ return null;
138
+ }
139
+ return (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: PaperclipIcon, size: "small", variant: "invisible", "aria-label": `${words.attach}. ${uploadsInWords(uploads)}`, onClick: () => input.current?.click() }), _jsx("input", { ref: input, type: "file", hidden: true, multiple: uploads.maxFiles > 1, accept: uploadsAccept(uploads), "aria-label": words.attach, onChange: event => {
140
+ const chosen = Array.from(event.target.files ?? []);
141
+ event.target.value = '';
142
+ void holdUploads(app, held.peek().files, chosen).then(next => {
143
+ held.value = next;
144
+ });
145
+ } }), now.files.map(file => (_jsx(Token, { text: file.name, size: "small", onRemove: () => {
146
+ held.value = {
147
+ files: held.peek().files.filter(kept => kept !== file),
148
+ refused: null,
149
+ };
150
+ } }, file.name))), now.refused ? (_jsx(Text, { role: "alert", sx: { fontSize: 0, color: 'danger.fg' }, children: now.refused })) : null] }));
151
+ }
31
152
  return definePlugin({
32
153
  name: `${APP_COMPOSER_PLUGIN_NAME}-${app.id}`,
33
- displayName: `${app.name}: commands and modes`,
34
- description: `The commands and modes ${app.name} offers in its composer.`,
154
+ displayName: `${app.name}: commands, modes, profiles and uploads`,
155
+ description: `The commands, modes, profiles and files ${app.name} takes in its composer.`,
35
156
  octicon: 'command-palette',
36
157
  emoji: '\u{2318}',
37
158
  contributes: [
159
+ ...(profiles.length > 0
160
+ ? [
161
+ contribution(LoopRunProps, {
162
+ id: 'app-profile',
163
+ // Sent with every run; from the first, the conversation keeps it.
164
+ props: () => {
165
+ started.value = true;
166
+ return { loop: { profile: profile.peek() } };
167
+ },
168
+ }, { id: 'app-profile' }),
169
+ contribution(LoopChatExtras, { id: 'app-profile-starters', extras: openers }, { id: 'app-profile-starters' }),
170
+ ]
171
+ : []),
38
172
  ...(app.interface.commands ?? []).map(command => contribution(LoopCommand, {
39
173
  name: command.name,
40
174
  description: command.description,
@@ -50,18 +184,56 @@ export function defineAppComposerPlugin(app) {
50
184
  }, { id: 'app-modes' }),
51
185
  ]
52
186
  : []),
53
- ],
54
- build: () => ({
55
- components: modes.length > 0
187
+ ...(uploads
56
188
  ? [
57
- {
58
- id: 'app-modes',
59
- slot: LoopSlots.promptAction,
60
- order: 5,
61
- Component: ModeSwitches,
62
- },
189
+ contribution(LoopRunProps, {
190
+ id: 'app-uploads',
191
+ // The files held go with this message, once, and are let go.
192
+ props: () => {
193
+ const files = held.peek().files;
194
+ if (files.length === 0) {
195
+ return {};
196
+ }
197
+ held.value = { files: [], refused: null };
198
+ return { loop: { files } };
199
+ },
200
+ }, { id: 'app-uploads' }),
63
201
  ]
64
- : [],
202
+ : []),
203
+ ],
204
+ build: () => ({
205
+ components: [
206
+ ...(profiles.length > 0
207
+ ? [
208
+ {
209
+ id: 'app-profile',
210
+ slot: LoopSlots.promptAction,
211
+ order: 3,
212
+ Component: ProfilePicker,
213
+ },
214
+ ]
215
+ : []),
216
+ ...(modes.length > 0
217
+ ? [
218
+ {
219
+ id: 'app-modes',
220
+ slot: LoopSlots.promptAction,
221
+ order: 5,
222
+ Component: ModeSwitches,
223
+ },
224
+ ]
225
+ : []),
226
+ ...(uploads
227
+ ? [
228
+ {
229
+ id: 'app-uploads',
230
+ slot: LoopSlots.promptAction,
231
+ order: 4,
232
+ Component: Attach,
233
+ },
234
+ ]
235
+ : []),
236
+ ],
65
237
  }),
66
238
  });
67
239
  }
@@ -28,6 +28,11 @@
28
28
  * address and embedded;
29
29
  * - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
30
30
  * window, its title the application's face and name;
31
+ * - its **language** (LOOP P-26): what a person reads of it — its name,
32
+ * welcome, starters, settings, commands, modes and profiles — in the first
33
+ * language they prefer that its Appspec is translated into (`language`,
34
+ * else the browser's), its own words otherwise (`translatedAppspec`);
35
+ * what it does is the same in every language;
31
36
  * - its **sidebar**, when the host asks (`sidebar`, R-01b): its rules and
32
37
  * the approvals waiting for the person (`app-rules`), what it did
33
38
  * (`app-activity`), and its computer, live (`app-computer`, R-23), beside
@@ -62,7 +67,7 @@ export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string
62
67
  * An application run by a team is not supported in the workspace yet: a team
63
68
  * is not an agent spec, and is refused here rather than launched as one.
64
69
  */
65
- export declare function defineAppPlugin(app: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
70
+ export declare function defineAppPlugin(app: AppSpec, shown?: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
66
71
  /** Which instance of an application runs, as the platform knows it (LOOP R-07). */
67
72
  export type AppInstance = {
68
73
  /** Its `app` item. */
@@ -132,6 +137,12 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
132
137
  * Canvas's palette. Compared by content.
133
138
  */
134
139
  pluginsOff?: readonly string[];
140
+ /**
141
+ * The language the person reads it in, as BCP 47 tags it (`fr`, `pt-BR`):
142
+ * the platform's setting when the host knows it. The browser's languages
143
+ * when unsaid (LOOP P-26).
144
+ */
145
+ language?: string;
135
146
  };
136
147
  /** What `interface.layout` sets on the workspace. */
137
148
  export type AppLayoutOptions = Pick<LoopPresetOptions, 'editors' | 'showViewSelector' | 'defaultEditor' | 'pageLayout' | 'pageLayoutArrangement' | 'pageLayoutPrompt' | 'pageLayoutPromptAnchor' | 'pageLayoutTurnPanelFooter'>;
@@ -164,7 +175,8 @@ export type AppPreset = {
164
175
  * but those of the UI plugins its organization turned off (`pluginsOff`),
165
176
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
166
177
  * comment on each answer when its record keeps feedback (V-18); its
167
- * commands in the composer's `/` menu and its modes beside it (P-19); what it
178
+ * commands in the composer's `/` menu and its modes beside it (P-19); the
179
+ * side panel and the pages its code opens (`app-elements`, P-18); what it
168
180
  * keeps and for how long, under its prompt before the first message, when
169
181
  * `kept` says it (R-31); with
170
182
  * `sidebar`, its rules and approvals card, its activity feed and its
@@ -186,6 +198,11 @@ export declare function appPreset(app: AppSpec, options?: {
186
198
  computer?: boolean;
187
199
  /** What its record is kept under: its activity's. */
188
200
  appUid?: string;
201
+ /**
202
+ * The application as the person reads it (P-26): its words in their
203
+ * language. Its own when unsaid.
204
+ */
205
+ shown?: AppSpec;
189
206
  /**
190
207
  * Say what it keeps before the first message (R-31): at its address and
191
208
  * embedded, not in its builder's Preview — to a `visitor` not signed
@@ -221,5 +238,5 @@ export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
221
238
  * colours: over another theme (T-30) nothing is laid, the theme's own rule.
222
239
  */
223
240
  export declare function appThemeOverrides(app: AppSpec, variant: string): ThemeOverrides | undefined;
224
- export declare function AppRenderer({ app, plugins, instance, onPresence, onSaying, paused, sidebar, computer, frame, page, pluginsOff, ...embed }: AppRendererProps): React.JSX.Element;
241
+ export declare function AppRenderer({ app, plugins, instance, onPresence, onSaying, paused, sidebar, computer, frame, page, pluginsOff, language, ...embed }: AppRendererProps): React.JSX.Element;
225
242
  export default AppRenderer;
@@ -33,6 +33,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
33
33
  * address and embedded;
34
34
  * - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
35
35
  * window, its title the application's face and name;
36
+ * - its **language** (LOOP P-26): what a person reads of it — its name,
37
+ * welcome, starters, settings, commands, modes and profiles — in the first
38
+ * language they prefer that its Appspec is translated into (`language`,
39
+ * else the browser's), its own words otherwise (`translatedAppspec`);
40
+ * what it does is the same in every language;
36
41
  * - its **sidebar**, when the host asks (`sidebar`, R-01b): its rules and
37
42
  * the approvals waiting for the person (`app-rules`), what it did
38
43
  * (`app-activity`), and its computer, live (`app-computer`, R-23), beside
@@ -52,10 +57,12 @@ import { CANVAS_BLOCK_PLUGINS, canvasBlocksPluginName, } from '../plugins/canvas
52
57
  import { LoopEmbed } from '../embed/LoopEmbed';
53
58
  import { dumpAppspec } from './appspec';
54
59
  import { defineAppActivityPlugin } from '../plugins/app-activity';
60
+ import { AppElementsPlugin } from '../plugins/app-elements';
55
61
  import { defineAppComputerPlugin } from '../plugins/app-computer';
56
62
  import { defineAppRulesPlugin } from '../plugins/app-rules';
57
- import { defineAppComposerPlugin } from './AppComposer';
63
+ import { defineAppComposerPlugin, needsAppComposer, startersAsOpeners, } from './AppComposer';
58
64
  import { modeEffect } from './composer';
65
+ import { preferredLanguages, translatedAppspec } from './language';
59
66
  import { defineAppFeedbackPlugin } from './AppFeedback';
60
67
  import { defineAppKeptPlugin } from './AppKept';
61
68
  import { keepsFeedback } from './feedback';
@@ -80,14 +87,16 @@ export const agentIdOf = (app) => {
80
87
  * An application run by a team is not supported in the workspace yet: a team
81
88
  * is not an agent spec, and is refused here rather than launched as one.
82
89
  */
83
- export function defineAppPlugin(app) {
90
+ export function defineAppPlugin(app, shown = app) {
84
91
  if (!app.agent) {
85
92
  throw new Error(`“${app.name}” is run by a team, which the workspace does not run as an application yet.`);
86
93
  }
87
94
  return defineAgentCapacityPlugin({
88
95
  key: `app-${app.id}`,
89
- displayName: app.name,
90
- description: app.description || `The ${app.name} application`,
96
+ // What a person reads is in their language (P-26); what it is created
97
+ // with is its own Appspec.
98
+ displayName: shown.name,
99
+ description: shown.description || `The ${shown.name} application`,
91
100
  specId: agentIdOf(app),
92
101
  emoji: app.emoji,
93
102
  createPayload: {
@@ -98,21 +107,21 @@ export function defineAppPlugin(app) {
98
107
  enable_codemode: Boolean(app.permissions?.computer?.shell),
99
108
  ...(app.model ? { model: app.model } : {}),
100
109
  },
101
- suggestions: app.interface.starters.map(starter => ({
102
- text: starter.label,
103
- message: starter.message,
104
- })),
110
+ // Its starters, each under its category (P-20); with profiles, the
111
+ // composer offers the profile's in their place.
112
+ suggestions: startersAsOpeners(shown.interface.starters),
105
113
  // In the page, the agent is told what a runtime tells it: its own
106
114
  // spec's prompt, then the application's instructions; on the
107
115
  // application's model when it names one (`loop/apps/agent`).
108
116
  instructions: app.instructions,
109
117
  model: app.model,
110
- // Its modes (LOOP P-19), as the session API applies them: the
111
- // instructions of the options chosen and the model one names, for the
112
- // run they are sent with.
113
- ...((app.interface.modes ?? []).length > 0
118
+ // Its modes and its profiles (LOOP P-19, P-20), as the session API
119
+ // applies them: the profile's instructions and those of the options
120
+ // chosen, and the model one names, for the run they are sent with.
121
+ ...((app.interface.modes ?? []).length > 0 ||
122
+ (app.interface.profiles ?? []).length > 0
114
123
  ? {
115
- modeEffect: (chosen) => modeEffect(app, chosen),
124
+ modeEffect: (chosen, profile) => modeEffect(app, chosen, profile),
116
125
  }
117
126
  : {}),
118
127
  });
@@ -157,7 +166,8 @@ export function appLayoutOptions(app) {
157
166
  * but those of the UI plugins its organization turned off (`pluginsOff`),
158
167
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
159
168
  * comment on each answer when its record keeps feedback (V-18); its
160
- * commands in the composer's `/` menu and its modes beside it (P-19); what it
169
+ * commands in the composer's `/` menu and its modes beside it (P-19); the
170
+ * side panel and the pages its code opens (`app-elements`, P-18); what it
161
171
  * keeps and for how long, under its prompt before the first message, when
162
172
  * `kept` says it (R-31); with
163
173
  * `sidebar`, its rules and approvals card, its activity feed and its
@@ -185,19 +195,18 @@ export function appPreset(app, options = {}) {
185
195
  };
186
196
  }
187
197
  const withPage = hasAppPage(app);
198
+ const shown = options.shown ?? app;
188
199
  // The blocks its Canvas offers: those of the UI plugins not turned off.
189
200
  const off = new Set((options.pluginsOff ?? []).map(canvasBlocksPluginName));
190
201
  const blocks = CANVAS_BLOCK_PLUGINS.filter(plugin => !off.has(plugin.name));
191
202
  return {
192
203
  plugins: [
193
- defineAppPlugin(app),
194
- ...(withPage ? [defineAppPagePlugin(app), ...blocks] : []),
195
- // Its commands in the composer's `/` menu and its modes beside the
196
- // composer (LOOP P-19): only for an application that has some.
197
- ...((app.interface.commands ?? []).length > 0 ||
198
- (app.interface.modes ?? []).length > 0
199
- ? [defineAppComposerPlugin(app)]
200
- : []),
204
+ defineAppPlugin(app, shown),
205
+ ...(withPage ? [defineAppPagePlugin(shown), ...blocks] : []),
206
+ // Its commands in the composer's `/` menu, its modes and its profiles
207
+ // beside the composer (LOOP P-19, P-20) and what a person may attach
208
+ // there (P-21): only for an application that has some.
209
+ ...(needsAppComposer(app) ? [defineAppComposerPlugin(shown)] : []),
201
210
  // A thumb and a comment on each answer, kept in its record (LOOP
202
211
  // V-18): only for an application whose record keeps feedback.
203
212
  ...(keepsFeedback(app) ? [defineAppFeedbackPlugin(app)] : []),
@@ -217,6 +226,9 @@ export function appPreset(app, options = {}) {
217
226
  : options.computer
218
227
  ? [defineAppComputerPlugin(app)]
219
228
  : []),
229
+ // What its code opens beside the conversation (LOOP P-18): a side
230
+ // panel, a page of its own; nothing drawn while nothing is open.
231
+ AppElementsPlugin,
220
232
  ],
221
233
  workspace: appLayoutOptions(app),
222
234
  };
@@ -281,7 +293,7 @@ export function appThemeOverrides(app, variant) {
281
293
  }
282
294
  : undefined;
283
295
  }
284
- export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, paused = false, sidebar = false, computer = false, frame = false, page, pluginsOff, ...embed }) {
296
+ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, paused = false, sidebar = false, computer = false, frame = false, page, pluginsOff, language, ...embed }) {
285
297
  /*
286
298
  * The kind's plugins, made once per application. `LoopEmbed` rebuilds its
287
299
  * whole reactor when its plugins change, so a new plugin on every render
@@ -290,6 +302,14 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
290
302
  */
291
303
  const source = JSON.stringify(dumpAppspec(app));
292
304
  const off = [...(pluginsOff ?? [])].sort().join(',');
305
+ /*
306
+ * What the person reads, in their language (P-26): the host's, else the
307
+ * browser's. Read once per language: a new one is a new workspace.
308
+ */
309
+ const preferred = (language ? [language] : preferredLanguages()).join(',');
310
+ const shown = useMemo(() => translatedAppspec(app, preferred.split(',').filter(Boolean)),
311
+ // eslint-disable-next-line react-hooks/exhaustive-deps
312
+ [source, preferred]);
293
313
  /*
294
314
  * What it keeps, said before the first message (R-31): run as a
295
315
  * deployment — at its address, embedded — and not in its builder's
@@ -305,6 +325,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
305
325
  sidebar,
306
326
  computer,
307
327
  appUid: instance?.appUid,
328
+ shown,
308
329
  ...(deployed ? { kept: { visitor } } : {}),
309
330
  });
310
331
  }
@@ -315,7 +336,17 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
315
336
  }
316
337
  },
317
338
  // eslint-disable-next-line react-hooks/exhaustive-deps
318
- [source, page, off, sidebar, computer, instance?.appUid, deployed, visitor]);
339
+ [
340
+ source,
341
+ shown,
342
+ page,
343
+ off,
344
+ sidebar,
345
+ computer,
346
+ instance?.appUid,
347
+ deployed,
348
+ visitor,
349
+ ]);
319
350
  const allPlugins = useMemo(() => ('problem' in preset ? plugins : [...preset.plugins, ...plugins]), [preset, plugins]);
320
351
  /*
321
352
  * On Datalayer, the application runs on a runtime: allocated with a plain
@@ -362,7 +393,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
362
393
  // spec is applied to an agent of its own, never to the one it extends.
363
394
  agentId: app.id, ...preset.workspace, ...(frame
364
395
  ? {
365
- frameTitle: [app.emoji, app.name].filter(Boolean).join(' '),
396
+ frameTitle: [app.emoji, shown.name].filter(Boolean).join(' '),
366
397
  }
367
398
  : {}),
368
399
  // Its rules, activity and computer on a rail, one at a time (T-07).
@@ -373,9 +404,9 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
373
404
  // Its own face, name and welcome in the chat (T-08); no counters: a
374
405
  // person using an application is not asking about tokens.
375
406
  presence: {
376
- name: app.name,
407
+ name: shown.name,
377
408
  face: app.emoji,
378
- welcome: app.interface.welcome || app.description,
409
+ welcome: shown.interface.welcome || shown.description,
379
410
  paused,
380
411
  onPresence,
381
412
  onSaying,
@@ -27,6 +27,8 @@
27
27
  import type { AppAccent, AppThemeMode, AppThemeVariant, AppAssistantCharacter, AppEmbedMode, AppVoiceSpec, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
28
28
  /** The version of the spec itself. */
29
29
  export declare const APP_SCHEMA = "loop.app/v1";
30
+ /** The language an application's own words are in, unless it says (LOOP P-26). */
31
+ export declare const DEFAULT_LANGUAGE = "en";
30
32
  /** The face of an application that has not chosen one. */
31
33
  export declare const DEFAULT_EMOJI = "\uD83D\uDC40";
32
34
  export declare const APP_KINDS: AppKind[];