@datalayer/agent-runtimes 1.3.81 → 1.3.83

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 (72) hide show
  1. package/lib/chat/assistant/SpeechBalloon.js +16 -1
  2. package/lib/chat/base/ChatBase.js +40 -4
  3. package/lib/chat/indicators/McpStatusIndicator.d.ts +1 -1
  4. package/lib/chat/indicators/McpStatusIndicator.js +13 -6
  5. package/lib/chat/indicators/SkillsStatusIndicator.d.ts +1 -1
  6. package/lib/chat/indicators/SkillsStatusIndicator.js +7 -3
  7. package/lib/chat/prompt/InputPrompt.d.ts +4 -1
  8. package/lib/chat/prompt/InputPrompt.js +2 -2
  9. package/lib/chat/prompt/InputPromptBase.d.ts +4 -1
  10. package/lib/chat/prompt/InputPromptBase.js +2 -2
  11. package/lib/chat/prompt/InputPromptLexical.d.ts +7 -1
  12. package/lib/chat/prompt/InputPromptLexical.js +5 -3
  13. package/lib/components/a2ui/datalayer/Form.js +19 -2
  14. package/lib/components/teams/index.d.ts +1 -0
  15. package/lib/components/teams/index.js +1 -0
  16. package/lib/components/teams/usePeerSandbox.d.ts +13 -0
  17. package/lib/components/teams/usePeerSandbox.js +59 -0
  18. package/lib/examples/AgentA2ATeamExample.js +7 -2
  19. package/lib/loop/apps/AppComposer.d.ts +31 -0
  20. package/lib/loop/apps/AppComposer.js +67 -0
  21. package/lib/loop/apps/AppRenderer.d.ts +16 -5
  22. package/lib/loop/apps/AppRenderer.js +49 -16
  23. package/lib/loop/apps/appspec.d.ts +8 -1
  24. package/lib/loop/apps/appspec.js +88 -23
  25. package/lib/loop/apps/checks.js +125 -8
  26. package/lib/loop/apps/composer.d.ts +37 -0
  27. package/lib/loop/apps/composer.js +61 -0
  28. package/lib/loop/apps/index.d.ts +2 -0
  29. package/lib/loop/apps/index.js +2 -0
  30. package/lib/loop/core/index.d.ts +31 -0
  31. package/lib/loop/core/index.js +22 -0
  32. package/lib/loop/embed/AppEmbed.d.ts +46 -7
  33. package/lib/loop/embed/AppEmbed.js +108 -26
  34. package/lib/loop/embed/LoopEmbed.js +2 -0
  35. package/lib/loop/embed/element.js +19 -5
  36. package/lib/loop/embed/embedConfig.d.ts +10 -1
  37. package/lib/loop/embed/embedConfig.js +12 -1
  38. package/lib/loop/embed/embedSession.d.ts +79 -0
  39. package/lib/loop/embed/embedSession.js +136 -0
  40. package/lib/loop/embed/embedTheme.d.ts +12 -9
  41. package/lib/loop/embed/embedTheme.js +32 -22
  42. package/lib/loop/index.d.ts +4 -3
  43. package/lib/loop/index.js +4 -3
  44. package/lib/loop/plugins/agent-capacity/index.d.ts +3 -1
  45. package/lib/loop/plugins/agent-capacity/index.js +2 -1
  46. package/lib/loop/plugins/app-page/appPageModel.d.ts +14 -5
  47. package/lib/loop/plugins/app-page/appPageModel.js +80 -88
  48. package/lib/loop/plugins/chat/ChatView.js +31 -4
  49. package/lib/loop/plugins/chat/index.d.ts +6 -1
  50. package/lib/loop/presets.d.ts +3 -1
  51. package/lib/loop/presets.js +2 -1
  52. package/lib/loop/shell/LoopWorkspace.js +3 -0
  53. package/lib/protocols/BrowserAgentAdapter.d.ts +18 -0
  54. package/lib/protocols/BrowserAgentAdapter.js +35 -2
  55. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +8 -0
  56. package/lib/runtimes/browser/a2aPeer.d.ts +11 -0
  57. package/lib/runtimes/browser/a2aPeer.js +15 -1
  58. package/lib/runtimes/browser/protocol.d.ts +3 -0
  59. package/lib/runtimes/browser/protocol.js +2 -1
  60. package/lib/specs/agents/agents.js +6 -3
  61. package/lib/specs/apps.js +216 -176
  62. package/lib/specs/appspecSchema.js +205 -84
  63. package/lib/specs/cogs.js +3 -2
  64. package/lib/types/agentspecs.d.ts +92 -7
  65. package/lib/types/chat.d.ts +29 -1
  66. package/package.json +1 -1
  67. package/scripts/codegen/generate_agents.py +4 -2
  68. package/scripts/codegen/generate_apps.py +0 -1
  69. package/scripts/codegen/generate_backend_tools.py +4 -12
  70. package/scripts/codegen/generate_cogs.py +3 -1
  71. package/scripts/codegen/generate_ui_plugins.py +22 -9
  72. package/scripts/voice/transcribe.mjs +29 -21
@@ -0,0 +1,67 @@
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';
4
+ import { useSignalValue } from '@datalayer/reactor/react';
5
+ import { LoopCommand, LoopRunProps, LoopSlots } from '../core';
6
+ import { commandPrompt, modeChoice, modeDefaults } from './composer';
7
+ /** The plugin's name, before the application's id. */
8
+ export const APP_COMPOSER_PLUGIN_NAME = '@datalayer/loop-plugin-app-composer';
9
+ /**
10
+ * 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.
13
+ */
14
+ export function defineAppComposerPlugin(app) {
15
+ const modes = app.interface.modes ?? [];
16
+ /** The option of each mode the person is in. */
17
+ const chosen = signal(modeDefaults(app));
18
+ function ModeSwitches() {
19
+ const now = useSignalValue(chosen);
20
+ return (_jsx(_Fragment, { children: modes.map(mode => {
21
+ const on = mode.options.find(option => option.id === now[mode.id]) ??
22
+ mode.options[0];
23
+ return (_jsxs(ActionMenu, { children: [_jsxs(ActionMenu.Button, { size: "small", variant: "invisible", "aria-label": `${mode.label}: ${on.label}`, children: [mode.label, ": ", on.label] }), _jsx(ActionMenu.Overlay, { width: "medium", children: _jsx(ActionList, { selectionVariant: "single", children: _jsxs(ActionList.Group, { children: [_jsx(ActionList.GroupHeading, { children: mode.label }), mode.options.map(option => (_jsxs(ActionList.Item, { selected: option.id === on.id, onSelect: () => {
24
+ chosen.value = modeChoice(app, {
25
+ ...chosen.peek(),
26
+ [mode.id]: option.id,
27
+ });
28
+ }, children: [option.label, option.description ? (_jsx(ActionList.Description, { variant: "block", children: option.description })) : null] }, option.id)))] }) }) })] }, mode.id));
29
+ }) }));
30
+ }
31
+ return definePlugin({
32
+ 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.`,
35
+ octicon: 'command-palette',
36
+ emoji: '\u{2318}',
37
+ contributes: [
38
+ ...(app.interface.commands ?? []).map(command => contribution(LoopCommand, {
39
+ name: command.name,
40
+ description: command.description,
41
+ group: app.name,
42
+ composer: true,
43
+ run: async ({ argv }) => ({ prompt: commandPrompt(command, argv) }),
44
+ }, { id: `app-command-${command.name}` })),
45
+ ...(modes.length > 0
46
+ ? [
47
+ contribution(LoopRunProps, {
48
+ id: 'app-modes',
49
+ props: () => ({ loop: { modes: { ...chosen.peek() } } }),
50
+ }, { id: 'app-modes' }),
51
+ ]
52
+ : []),
53
+ ],
54
+ build: () => ({
55
+ components: modes.length > 0
56
+ ? [
57
+ {
58
+ id: 'app-modes',
59
+ slot: LoopSlots.promptAction,
60
+ order: 5,
61
+ Component: ModeSwitches,
62
+ },
63
+ ]
64
+ : [],
65
+ }),
66
+ });
67
+ }
@@ -108,6 +108,12 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
108
108
  * `instance.appUid`.
109
109
  */
110
110
  sidebar?: boolean;
111
+ /**
112
+ * Draw its computer alone beside its page (R-23): what a deployment's
113
+ * owner and the editors of its application see of it at its address — its
114
+ * runtime shows it to them only (decided 2026-10-06). Implied by `sidebar`.
115
+ */
116
+ computer?: boolean;
111
117
  /**
112
118
  * Draw the application in a window (the `window-frame` plugin), its title
113
119
  * the application's face and name. A host's own `frameTitle` wins.
@@ -157,11 +163,13 @@ export type AppPreset = {
157
163
  * its page (`app-page`) when it has one, with the Canvas's block plugins
158
164
  * but those of the UI plugins its organization turned off (`pluginsOff`),
159
165
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
160
- * comment on each answer when its record keeps feedback (V-18); what it
166
+ * 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
161
168
  * keeps and for how long, under its prompt before the first message, when
162
169
  * `kept` says it (R-31); with
163
170
  * `sidebar`, its rules and approvals card, its activity feed and its
164
- * computer (R-01b, R-23);
171
+ * computer (R-01b, R-23); with `computer`, its computer alone — at its
172
+ * address, for its owner and the editors of its application;
165
173
  * laid out as `interface.layout` says;
166
174
  * - a **decision**: the page its host draws (`page`), as the one view of a
167
175
  * workspace without a conversation (R-02).
@@ -174,6 +182,8 @@ export declare function appPreset(app: AppSpec, options?: {
174
182
  pluginsOff?: readonly string[];
175
183
  /** Its rules and approvals, its activity and its computer, beside its page. */
176
184
  sidebar?: boolean;
185
+ /** Its computer alone beside its page (R-23): for its owner and editors at its address. */
186
+ computer?: boolean;
177
187
  /** What its record is kept under: its activity's. */
178
188
  appUid?: string;
179
189
  /**
@@ -207,8 +217,9 @@ export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
207
217
  * The application's accent over the theme its conversation wears, in both
208
218
  * modes (LOOP T-05): its bubbles and its one button in its own colour, and
209
219
  * not in the theme's default — the chat sets its theme again inside it, so
210
- * what the page around it set does not reach in.
220
+ * what the page around it set does not reach in. The accents are `loop`'s
221
+ * colours: over another theme (T-30) nothing is laid, the theme's own rule.
211
222
  */
212
- export declare function appThemeOverrides(app: AppSpec): ThemeOverrides | undefined;
213
- export declare function AppRenderer({ app, plugins, instance, onPresence, onSaying, paused, sidebar, frame, page, pluginsOff, ...embed }: AppRendererProps): React.JSX.Element;
223
+ 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;
214
225
  export default AppRenderer;
@@ -45,7 +45,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
45
45
  * @module loop/apps/AppRenderer
46
46
  */
47
47
  import { useMemo } from 'react';
48
- import { loopAccentStyles } from '@datalayer/primer-addons';
48
+ import { loopAccentStyles, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
49
49
  import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
50
50
  import { APP_PAGE_SURFACE, defineAppHostPagePlugin, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
51
51
  import { CANVAS_BLOCK_PLUGINS, canvasBlocksPluginName, } from '../plugins/canvas-blocks';
@@ -54,6 +54,8 @@ import { dumpAppspec } from './appspec';
54
54
  import { defineAppActivityPlugin } from '../plugins/app-activity';
55
55
  import { defineAppComputerPlugin } from '../plugins/app-computer';
56
56
  import { defineAppRulesPlugin } from '../plugins/app-rules';
57
+ import { defineAppComposerPlugin } from './AppComposer';
58
+ import { modeEffect } from './composer';
57
59
  import { defineAppFeedbackPlugin } from './AppFeedback';
58
60
  import { defineAppKeptPlugin } from './AppKept';
59
61
  import { keepsFeedback } from './feedback';
@@ -105,6 +107,14 @@ export function defineAppPlugin(app) {
105
107
  // application's model when it names one (`loop/apps/agent`).
106
108
  instructions: app.instructions,
107
109
  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
114
+ ? {
115
+ modeEffect: (chosen) => modeEffect(app, chosen),
116
+ }
117
+ : {}),
108
118
  });
109
119
  }
110
120
  /**
@@ -146,11 +156,13 @@ export function appLayoutOptions(app) {
146
156
  * its page (`app-page`) when it has one, with the Canvas's block plugins
147
157
  * but those of the UI plugins its organization turned off (`pluginsOff`),
148
158
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
149
- * comment on each answer when its record keeps feedback (V-18); what it
159
+ * 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
150
161
  * keeps and for how long, under its prompt before the first message, when
151
162
  * `kept` says it (R-31); with
152
163
  * `sidebar`, its rules and approvals card, its activity feed and its
153
- * computer (R-01b, R-23);
164
+ * computer (R-01b, R-23); with `computer`, its computer alone — at its
165
+ * address, for its owner and the editors of its application;
154
166
  * laid out as `interface.layout` says;
155
167
  * - a **decision**: the page its host draws (`page`), as the one view of a
156
168
  * workspace without a conversation (R-02).
@@ -180,6 +192,12 @@ export function appPreset(app, options = {}) {
180
192
  plugins: [
181
193
  defineAppPlugin(app),
182
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
+ : []),
183
201
  // A thumb and a comment on each answer, kept in its record (LOOP
184
202
  // V-18): only for an application whose record keeps feedback.
185
203
  ...(keepsFeedback(app) ? [defineAppFeedbackPlugin(app)] : []),
@@ -196,7 +214,9 @@ export function appPreset(app, options = {}) {
196
214
  // Its computer, live (R-23).
197
215
  defineAppComputerPlugin(app),
198
216
  ]
199
- : []),
217
+ : options.computer
218
+ ? [defineAppComputerPlugin(app)]
219
+ : []),
200
220
  ],
201
221
  workspace: appLayoutOptions(app),
202
222
  };
@@ -249,18 +269,19 @@ const NO_PLUGINS = [];
249
269
  * The application's accent over the theme its conversation wears, in both
250
270
  * modes (LOOP T-05): its bubbles and its one button in its own colour, and
251
271
  * not in the theme's default — the chat sets its theme again inside it, so
252
- * what the page around it set does not reach in.
272
+ * what the page around it set does not reach in. The accents are `loop`'s
273
+ * colours: over another theme (T-30) nothing is laid, the theme's own rule.
253
274
  */
254
- export function appThemeOverrides(app) {
275
+ export function appThemeOverrides(app, variant) {
255
276
  const accent = app.interface?.accent;
256
- return accent
277
+ return accent && variant === 'loop'
257
278
  ? {
258
279
  light: loopAccentStyles(accent, 'light'),
259
280
  dark: loopAccentStyles(accent, 'dark'),
260
281
  }
261
282
  : undefined;
262
283
  }
263
- export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, paused = false, sidebar = false, frame = false, page, pluginsOff, ...embed }) {
284
+ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, paused = false, sidebar = false, computer = false, frame = false, page, pluginsOff, ...embed }) {
264
285
  /*
265
286
  * The kind's plugins, made once per application. `LoopEmbed` rebuilds its
266
287
  * whole reactor when its plugins change, so a new plugin on every render
@@ -282,6 +303,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
282
303
  page,
283
304
  pluginsOff,
284
305
  sidebar,
306
+ computer,
285
307
  appUid: instance?.appUid,
286
308
  ...(deployed ? { kept: { visitor } } : {}),
287
309
  });
@@ -293,7 +315,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
293
315
  }
294
316
  },
295
317
  // eslint-disable-next-line react-hooks/exhaustive-deps
296
- [source, page, off, sidebar, instance?.appUid, deployed, visitor]);
318
+ [source, page, off, sidebar, computer, instance?.appUid, deployed, visitor]);
297
319
  const allPlugins = useMemo(() => ('problem' in preset ? plugins : [...preset.plugins, ...plugins]), [preset, plugins]);
298
320
  /*
299
321
  * On Datalayer, the application runs on a runtime: allocated with a plain
@@ -311,10 +333,24 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
311
333
  instance?.deploymentUid,
312
334
  instance?.version,
313
335
  ]);
336
+ /*
337
+ * The theme it runs in (T-30): the one its host says, else the one its
338
+ * Appspec names, else the page's — the person's own. Its mode likewise;
339
+ * `auto` is the device's.
340
+ */
341
+ const pageTheme = useThemeStore(state => state.theme);
342
+ const system = useSystemColorMode();
343
+ const named = app.interface?.theme;
344
+ const worn = embed.themeVariant ?? named?.variant ?? pageTheme;
345
+ const namedMode = named?.mode === 'auto'
346
+ ? system
347
+ : named?.mode === 'light' || named?.mode === 'dark'
348
+ ? named.mode
349
+ : undefined;
314
350
  const accent = app.interface?.accent;
315
- const themeOverrides = useMemo(() => appThemeOverrides(app),
351
+ const themeOverrides = useMemo(() => appThemeOverrides(app, worn),
316
352
  // eslint-disable-next-line react-hooks/exhaustive-deps
317
- [accent]);
353
+ [accent, worn]);
318
354
  if ('problem' in preset) {
319
355
  return (_jsx("div", { role: "status", style: { padding: 16 }, children: preset.problem }));
320
356
  }
@@ -330,13 +366,10 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
330
366
  }
331
367
  : {}),
332
368
  // Its rules, activity and computer on a rail, one at a time (T-07).
333
- sidebarRail: sidebar, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
369
+ sidebarRail: sidebar || computer, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
334
370
  // Where an application runs is decided by its host — the Studio's
335
371
  // Preview, the hosted page — not offered to its user.
336
- targetFixed: true,
337
- // Applications first (LOOP T-12): an application's conversation wears
338
- // the `loop` theme; a host may say otherwise.
339
- themeVariant: "loop", themeOverrides: themeOverrides,
372
+ targetFixed: true, ...(named ? { themeVariant: named.variant } : {}), ...(namedMode ? { colorMode: namedMode } : {}), themeOverrides: themeOverrides,
340
373
  // Its own face, name and welcome in the chat (T-08); no counters: a
341
374
  // person using an application is not asking about tokens.
342
375
  presence: {
@@ -24,13 +24,20 @@
24
24
  *
25
25
  * @module loop/apps/appspec
26
26
  */
27
- import type { AppAccent, AppAssistantCharacter, AppEmbedMode, AppVoiceSpec, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
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
30
  /** The face of an application that has not chosen one. */
31
31
  export declare const DEFAULT_EMOJI = "\uD83D\uDC40";
32
32
  export declare const APP_KINDS: AppKind[];
33
33
  export declare const APP_ACCENTS: AppAccent[];
34
+ /**
35
+ * The themes an application may run in (LOOP T-30): Appearance's, as
36
+ * primer-addons lists them (`themeVariants`), in its order.
37
+ */
38
+ export declare const APP_THEME_VARIANTS: AppThemeVariant[];
39
+ /** The colour modes a theme is worn in. */
40
+ export declare const APP_THEME_MODES: AppThemeMode[];
34
41
  /** The four ways an application sits in another product's page (LOOP D-07). */
35
42
  export declare const APP_EMBED_MODES: AppEmbedMode[];
36
43
  /**
@@ -16,6 +16,23 @@ export const APP_ACCENTS = [
16
16
  'sun',
17
17
  'violet',
18
18
  ];
19
+ /**
20
+ * The themes an application may run in (LOOP T-30): Appearance's, as
21
+ * primer-addons lists them (`themeVariants`), in its order.
22
+ */
23
+ export const APP_THEME_VARIANTS = [
24
+ 'datalayer',
25
+ 'spatial',
26
+ 'lovely',
27
+ 'matrix',
28
+ 'earth',
29
+ 'sand',
30
+ 'ivory',
31
+ 'sun',
32
+ 'loop',
33
+ ];
34
+ /** The colour modes a theme is worn in. */
35
+ export const APP_THEME_MODES = ['light', 'dark', 'auto'];
19
36
  /** The four ways an application sits in another product's page (LOOP D-07). */
20
37
  export const APP_EMBED_MODES = [
21
38
  'inline',
@@ -105,7 +122,8 @@ export function emptyAppspec(kind = 'chat') {
105
122
  accent: 'green',
106
123
  welcome: '',
107
124
  starters: [],
108
- settings: [],
125
+ commands: [],
126
+ modes: [],
109
127
  components: [],
110
128
  outputs: [],
111
129
  },
@@ -129,6 +147,7 @@ export function emptyAppspec(kind = 'chat') {
129
147
  notifications: [],
130
148
  setup: [],
131
149
  enabled: true,
150
+ unavailable_because: '',
132
151
  tags: [],
133
152
  icon: DEFAULT_ICON,
134
153
  emoji: DEFAULT_EMOJI,
@@ -167,6 +186,7 @@ const KNOWN_KEYS = [
167
186
  'notifications',
168
187
  'decision',
169
188
  'enabled',
189
+ 'unavailable_because',
170
190
  'tags',
171
191
  'icon',
172
192
  'emoji',
@@ -190,23 +210,38 @@ function parseRule(data) {
190
210
  behaviour: oneOf(data.behaviour, BEHAVIOURS, 'ask_first'),
191
211
  };
192
212
  }
193
- function parseSetting(data) {
194
- const setting = {
213
+ /** A slash command (LOOP P-19); a wrong one is said by the checks. */
214
+ function parseCommand(data) {
215
+ return {
216
+ name: text(data.name),
217
+ description: text(data.description),
218
+ prompt: text(data.prompt),
219
+ };
220
+ }
221
+ /** One position of a mode switch, what it does said only when said. */
222
+ function parseModeOption(data) {
223
+ const option = {
195
224
  id: text(data.id),
196
- type: oneOf(data.type, ['select', 'text', 'toggle', 'slider', 'number'], 'text'),
197
225
  label: text(data.label),
198
- options: texts(data.options),
199
226
  };
200
- if (['string', 'boolean', 'number'].includes(typeof data.default)) {
201
- setting.default = data.default;
202
- }
203
- if (typeof data.min === 'number') {
204
- setting.min = data.min;
227
+ for (const key of ['description', 'instructions', 'model']) {
228
+ if (typeof data[key] === 'string' && data[key]) {
229
+ option[key] = data[key];
230
+ }
205
231
  }
206
- if (typeof data.max === 'number') {
207
- setting.max = data.max;
232
+ return option;
233
+ }
234
+ /** A mode switch (LOOP P-19). */
235
+ function parseMode(data) {
236
+ const mode = {
237
+ id: text(data.id),
238
+ label: text(data.label),
239
+ options: records(data.options).map(parseModeOption),
240
+ };
241
+ if (typeof data.default === 'string' && data.default) {
242
+ mode.default = data.default;
208
243
  }
209
- return setting;
244
+ return mode;
210
245
  }
211
246
  function parseSurface(data) {
212
247
  return {
@@ -246,11 +281,18 @@ function parseInterface(data, kind) {
246
281
  label: text(starter.label),
247
282
  message: text(starter.message),
248
283
  })),
249
- settings: records(data.settings).map(parseSetting),
284
+ commands: records(data.commands).map(parseCommand),
285
+ modes: records(data.modes).map(parseMode),
250
286
  components: texts(data.components),
251
287
  voice: parseVoice(data.voice),
252
288
  outputs: texts(data.outputs),
253
289
  };
290
+ // Its settings are the JSON Schema of a form (C-16), kept as written: what
291
+ // is not one — the list of settings of earlier versions among it — is not
292
+ // read, and is said by the checks.
293
+ if (isData(data.settings)) {
294
+ parsed.settings = JSON.parse(JSON.stringify(data.settings));
295
+ }
254
296
  if (isData(data.surface)) {
255
297
  parsed.surface = parseSurface(data.surface);
256
298
  }
@@ -260,6 +302,14 @@ function parseInterface(data, kind) {
260
302
  if (data.balloon === 'history' || data.balloon === 'current') {
261
303
  parsed.balloon = data.balloon;
262
304
  }
305
+ // A theme of Appearance's, or none: an unknown one is said by the checks.
306
+ if (isData(data.theme) &&
307
+ APP_THEME_VARIANTS.includes(data.theme.variant)) {
308
+ parsed.theme = { variant: data.theme.variant };
309
+ if (APP_THEME_MODES.includes(data.theme.mode)) {
310
+ parsed.theme.mode = data.theme.mode;
311
+ }
312
+ }
263
313
  return parsed;
264
314
  }
265
315
  function parseCriterion(data) {
@@ -433,6 +483,7 @@ export function parseAppspec(document) {
433
483
  memory: text(data.memory),
434
484
  notifications: texts(data.notifications),
435
485
  enabled: flag(data.enabled, true),
486
+ unavailable_because: text(data.unavailable_because),
436
487
  tags: texts(data.tags),
437
488
  icon: text(data.icon, DEFAULT_ICON),
438
489
  emoji: text(data.emoji, DEFAULT_EMOJI) || DEFAULT_EMOJI,
@@ -516,19 +567,27 @@ function dumpInterface(spec, kind) {
516
567
  label: starter.label,
517
568
  message: starter.message,
518
569
  })))
519
- .list('settings', spec.settings.map(setting => {
570
+ .list('commands', spec.commands.map(command => ({
571
+ name: command.name,
572
+ description: command.description,
573
+ prompt: command.prompt,
574
+ })))
575
+ .list('modes', spec.modes.map(mode => {
520
576
  const written = new Writer()
521
- .text('id', setting.id, '\u0000')
522
- .text('type', setting.type, '\u0000')
523
- .text('label', setting.label, '\u0000')
524
- .list('options', setting.options);
525
- for (const key of ['default', 'min', 'max']) {
526
- if (setting[key] !== undefined) {
527
- written.data[key] = setting[key];
528
- }
577
+ .text('id', mode.id, '\u0000')
578
+ .text('label', mode.label, '\u0000')
579
+ .list('options', mode.options.map(option => new Writer()
580
+ .text('id', option.id, '\u0000')
581
+ .text('label', option.label, '\u0000')
582
+ .text('description', option.description ?? '')
583
+ .text('instructions', option.instructions ?? '')
584
+ .text('model', option.model ?? '').data));
585
+ if (mode.default) {
586
+ written.data.default = mode.default;
529
587
  }
530
588
  return written.data;
531
589
  }))
590
+ .part('settings', spec.settings ? { ...spec.settings } : {})
532
591
  .list('components', spec.components);
533
592
  if (spec.surface) {
534
593
  writer.data.surface = dumpSurface(spec.surface);
@@ -539,6 +598,11 @@ function dumpInterface(spec, kind) {
539
598
  if (spec.balloon) {
540
599
  writer.data.balloon = spec.balloon;
541
600
  }
601
+ if (spec.theme) {
602
+ writer.data.theme = spec.theme.mode
603
+ ? { variant: spec.theme.variant, mode: spec.theme.mode }
604
+ : { variant: spec.theme.variant };
605
+ }
542
606
  // Its voice, when it says one (VO-41): a spec made before voice has none.
543
607
  if (spec.voice) {
544
608
  writer.part('voice', new Writer()
@@ -678,6 +742,7 @@ export function dumpAppspec(app) {
678
742
  }
679
743
  writer
680
744
  .value('enabled', app.enabled, true)
745
+ .text('unavailable_because', app.unavailable_because)
681
746
  .list('tags', app.tags)
682
747
  .text('icon', app.icon ?? DEFAULT_ICON, DEFAULT_ICON)
683
748
  .text('emoji', app.emoji, DEFAULT_EMOJI)