@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.
- package/lib/chat/base/ChatBase.js +16 -0
- package/lib/chat/base/loopElement.d.ts +53 -0
- package/lib/chat/base/loopElement.js +78 -0
- package/lib/chat/base/loopWindow.d.ts +31 -0
- package/lib/chat/base/loopWindow.js +74 -0
- package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
- package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
- package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
- package/lib/components/a2ui/datalayer/Form.js +26 -2
- package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
- package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
- package/lib/loop/apps/AppComposer.d.ts +39 -4
- package/lib/loop/apps/AppComposer.js +191 -19
- package/lib/loop/apps/AppRenderer.d.ts +20 -3
- package/lib/loop/apps/AppRenderer.js +58 -27
- package/lib/loop/apps/appspec.d.ts +2 -0
- package/lib/loop/apps/appspec.js +196 -39
- package/lib/loop/apps/checks.d.ts +6 -0
- package/lib/loop/apps/checks.js +187 -8
- package/lib/loop/apps/composer.d.ts +21 -10
- package/lib/loop/apps/composer.js +41 -8
- package/lib/loop/apps/language.d.ts +68 -0
- package/lib/loop/apps/language.js +275 -0
- package/lib/loop/apps/settingsInputs.d.ts +29 -0
- package/lib/loop/apps/settingsInputs.js +147 -0
- package/lib/loop/apps/uploads.d.ts +43 -0
- package/lib/loop/apps/uploads.js +66 -0
- package/lib/loop/core/index.d.ts +14 -1
- package/lib/loop/embed/AppEmbed.d.ts +14 -0
- package/lib/loop/embed/AppEmbed.js +64 -2
- package/lib/loop/embed/element.js +23 -0
- package/lib/loop/embed/embedSession.d.ts +14 -0
- package/lib/loop/embed/embedSession.js +34 -0
- package/lib/loop/embed/hostBridge.d.ts +22 -1
- package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
- package/lib/loop/plugins/app-elements/AppElements.js +76 -0
- package/lib/loop/plugins/app-elements/index.d.ts +20 -0
- package/lib/loop/plugins/app-elements/index.js +43 -0
- package/lib/loop/plugins/app-page/appPageModel.d.ts +14 -5
- package/lib/loop/plugins/app-page/appPageModel.js +82 -88
- package/lib/loop/plugins/chat/ChatView.js +10 -2
- package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
- package/lib/protocols/BrowserAgentAdapter.js +13 -10
- package/lib/runtimes/browser/protocol.d.ts +1 -1
- package/lib/specs/agents/agents.js +6 -3
- package/lib/specs/apps.js +337 -176
- package/lib/specs/appspecSchema.js +455 -88
- package/lib/specs/cogs.js +3 -2
- package/lib/specs/uiPlugins.js +6 -1
- package/lib/types/agentspecs.d.ts +167 -11
- package/package.json +1 -1
- package/scripts/codegen/generate_apps.py +16 -1
|
@@ -1,17 +1,116 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
|
12
|
-
*
|
|
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
|
|
34
|
-
description: `The commands and
|
|
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-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
111
|
-
//
|
|
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);
|
|
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(
|
|
195
|
-
// Its commands in the composer's `/` menu
|
|
196
|
-
// composer (LOOP P-19)
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
[
|
|
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,
|
|
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:
|
|
407
|
+
name: shown.name,
|
|
377
408
|
face: app.emoji,
|
|
378
|
-
welcome:
|
|
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[];
|