@datalayer/agent-runtimes 1.3.96 → 1.3.98

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.
@@ -71,6 +71,7 @@ import { keepsFeedback } from './feedback';
71
71
  import { keptBeforeFirstMessage } from './kept';
72
72
  import { ChatLanguage } from '../../chat/ChatLanguage';
73
73
  import { defineAppRuntimePlugin } from './AppRuntimePlugins';
74
+ import { hasFolderModules } from './customComponents';
74
75
  import { appPluginPair } from './pluginPair';
75
76
  /** The id of an agent or a Cog, without its version. */
76
77
  export const agentIdOf = (app) => {
@@ -232,8 +233,10 @@ export function appPreset(app, options = {}) {
232
233
  return {
233
234
  plugins: [
234
235
  defineAppPlugin(app, shown),
235
- // What its runtime holds, told to the page (F-15).
236
- defineAppRuntimePlugin(app.id),
236
+ // What its runtime holds, told to the page (F-15), and its packaged
237
+ // page side read from it when its components are files of its folder
238
+ // (P-29).
239
+ defineAppRuntimePlugin(app.id, { pageSide: hasFolderModules(app) }),
237
240
  ...(withPage ? [defineAppPagePlugin(shown), ...blocks] : []),
238
241
  // The components its developer wrote (LOOP P-17): blocks of its page
239
242
  // and renderers of its page, its elements and its answers — its own.
@@ -1,8 +1,12 @@
1
1
  import { type ReactorPlugin } from '@datalayer/reactor';
2
2
  export declare const APP_RUNTIME_PLUGIN_NAME = "@datalayer/loop-plugin-app-runtime";
3
3
  /** Follows the application's runtime, and tells the page what it holds. */
4
- export declare function AppRuntimePlugins({ appId }: {
4
+ export declare function AppRuntimePlugins({ appId, pageSide, }: {
5
5
  appId: string;
6
+ /** Its page side is packaged, to be read from its runtime (P-29). */
7
+ pageSide?: boolean;
6
8
  }): null;
7
9
  /** The plugin that follows an application's runtime for its page. */
8
- export declare function defineAppRuntimePlugin(appId: string): ReactorPlugin<Record<string, never>, unknown, unknown>;
10
+ export declare function defineAppRuntimePlugin(appId: string, { pageSide }?: {
11
+ pageSide?: boolean;
12
+ }): ReactorPlugin<Record<string, never>, unknown, unknown>;
@@ -23,13 +23,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
23
23
  * its own runtime, and the pair is whole: nothing stands down, nothing is
24
24
  * blank.
25
25
  *
26
+ * An application whose components are files of its folder (LOOP P-29) has
27
+ * its page side in its package, installed beside the server that runs it:
28
+ * once on a runtime, the page reads it there with Reactor's
29
+ * `bootstrapExtensions` (`<base>/plugins/frontend-extensions`) and installs
30
+ * it into the running platform — the extension `loop-app-<id>`, the
31
+ * application's, its page-side plugin saying where the folder's files are
32
+ * (`LoopAppFiles`). A runtime with no package of it answers nothing, and its
33
+ * components say so.
34
+ *
26
35
  * A plugin of its own, with no backend requirement, so that it keeps
27
36
  * listening while the application's plugin is stood down. It renders nothing.
28
37
  *
29
38
  * @module apps/apps/AppRuntimePlugins
30
39
  */
31
40
  import { useCallback, useEffect } from 'react';
32
- import { definePlugin } from '@datalayer/reactor';
41
+ import { bootstrapExtensions, definePlugin, } from '@datalayer/reactor';
33
42
  import { registerReactor, useBackendPluginStream, useReactorPlatform, useSignalValue, } from '@datalayer/reactor/react';
34
43
  import { IDLE_SANDBOX_SNAPSHOT_SIGNAL, IDLE_SANDBOX_TARGET_SIGNAL, LoopSlots, } from '../core';
35
44
  import { useOptionalSandboxService } from '../plugins/agents/useSandboxService';
@@ -38,7 +47,7 @@ export const APP_RUNTIME_PLUGIN_NAME = '@datalayer/loop-plugin-app-runtime';
38
47
  /** A host's answer to "is this runtime plugin running?", from a list. */
39
48
  const runningOf = (names) => (name) => names.includes(name);
40
49
  /** Follows the application's runtime, and tells the page what it holds. */
41
- export function AppRuntimePlugins({ appId }) {
50
+ export function AppRuntimePlugins({ appId, pageSide = false, }) {
42
51
  const reactor = useReactorPlatform();
43
52
  const service = useOptionalSandboxService();
44
53
  const target = useSignalValue(service?.target ?? IDLE_SANDBOX_TARGET_SIGNAL);
@@ -64,11 +73,29 @@ export function AppRuntimePlugins({ appId }) {
64
73
  useBackendPluginStream(runtime ? appPluginsBase(runtime, appId) : undefined, {
65
74
  onState,
66
75
  });
76
+ // Its packaged page side, installed from its runtime (P-29): the
77
+ // application's extension only, whatever else the server holds.
78
+ useEffect(() => {
79
+ if (!runtime || !pageSide) {
80
+ return;
81
+ }
82
+ let current = true;
83
+ void bootstrapExtensions(appPluginsBase(runtime, appId)).then(found => {
84
+ for (const extension of found.filter(item => item.name === name)) {
85
+ if (current) {
86
+ void reactor.install(extension);
87
+ }
88
+ }
89
+ });
90
+ return () => {
91
+ current = false;
92
+ };
93
+ }, [reactor, runtime, appId, name, pageSide]);
67
94
  return null;
68
95
  }
69
96
  /** The plugin that follows an application's runtime for its page. */
70
- export function defineAppRuntimePlugin(appId) {
71
- const Follow = () => _jsx(AppRuntimePlugins, { appId: appId });
97
+ export function defineAppRuntimePlugin(appId, { pageSide = false } = {}) {
98
+ const Follow = () => _jsx(AppRuntimePlugins, { appId: appId, pageSide: pageSide });
72
99
  return definePlugin({
73
100
  name: APP_RUNTIME_PLUGIN_NAME,
74
101
  displayName: 'Its runtime',
@@ -7,10 +7,23 @@ export declare const CUSTOM_PROPERTY_TYPES: readonly ["string", "integer", "numb
7
7
  export declare const CUSTOM_RESERVED_NAMES: readonly ["id", "component", "action", "weight", "visible_when", "children", "child"];
8
8
  /**
9
9
  * Where its module is loaded from: an address served over HTTPS, or this
10
- * machine while it is written. A file of the application's folder waits for
11
- * its packaging (P-29).
10
+ * machine while it is written.
12
11
  */
13
12
  export declare const CUSTOM_SOURCE: RegExp;
13
+ /**
14
+ * Or a file of the application's folder, by its path in it (LOOP P-29): a
15
+ * `.js` or `.mjs` file inside the folder — no absolute path, no `..`.
16
+ * Packaged with the application (`loop apps package`), it is served by the
17
+ * server its package is installed beside (`LoopAppFiles`).
18
+ */
19
+ export declare const CUSTOM_FILE: RegExp;
20
+ /** Whether a component's module is a file of its application's folder. */
21
+ export declare const isFolderSource: (source: string) => boolean;
22
+ /**
23
+ * The address a file of the folder is fetched from, under the base its
24
+ * package's page side said (`LoopAppFiles`).
25
+ */
26
+ export declare const folderSourceUrl: (source: string, base: string) => string;
14
27
  /** A Subresource Integrity hash: the module as it was reviewed. */
15
28
  export declare const INTEGRITY: RegExp;
16
29
  /**
@@ -21,6 +34,11 @@ export declare const INTEGRITY: RegExp;
21
34
  export declare function customPropsRefused(component: AppCustomComponentSpec, props: Record<string, unknown>, bound?: readonly string[]): string[];
22
35
  /** What stops one component its developer wrote from being reviewed, in sentences. */
23
36
  export declare function customComponentProblems(component: AppCustomComponentSpec): string[];
37
+ /**
38
+ * Whether an application's page side is packaged (LOOP P-29): a component
39
+ * its developer wrote is a file of its folder.
40
+ */
41
+ export declare const hasFolderModules: (app: Pick<AppSpec, "interface">) => boolean;
24
42
  /** What stops the components an application's developer wrote, in sentences. */
25
43
  export declare function customComponentsProblems(app: AppSpec): string[];
26
44
  /** A component its developer wrote, by name, or undefined. */
@@ -40,10 +40,24 @@ export const CUSTOM_RESERVED_NAMES = [
40
40
  ];
41
41
  /**
42
42
  * Where its module is loaded from: an address served over HTTPS, or this
43
- * machine while it is written. A file of the application's folder waits for
44
- * its packaging (P-29).
43
+ * machine while it is written.
45
44
  */
46
45
  export const CUSTOM_SOURCE = /^(?:https:\/\/[A-Za-z0-9.-]+(?::\d+)?|http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?)\/\S*$/;
46
+ /**
47
+ * Or a file of the application's folder, by its path in it (LOOP P-29): a
48
+ * `.js` or `.mjs` file inside the folder — no absolute path, no `..`.
49
+ * Packaged with the application (`loop apps package`), it is served by the
50
+ * server its package is installed beside (`LoopAppFiles`).
51
+ */
52
+ export const CUSTOM_FILE = /^(?:\.\/)?(?:[A-Za-z0-9_][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_][A-Za-z0-9_.-]*\.m?js$/;
53
+ /** Whether a component's module is a file of its application's folder. */
54
+ export const isFolderSource = (source) => !/^[a-z][a-z0-9+.-]*:/.test(source) && CUSTOM_FILE.test(source);
55
+ /**
56
+ * The address a file of the folder is fetched from, under the base its
57
+ * package's page side said (`LoopAppFiles`).
58
+ */
59
+ export const folderSourceUrl = (source, base) => new URL(source.replace(/^\.\//, ''), base.endsWith('/') ? base : `${base}/`)
60
+ .href;
47
61
  /** A Subresource Integrity hash: the module as it was reviewed. */
48
62
  export const INTEGRITY = /^sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2}$/;
49
63
  const PROPERTY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -117,7 +131,9 @@ export function customComponentProblems(component) {
117
131
  problems.push(`${said} says what it is for, under \`description\`.`);
118
132
  }
119
133
  if (!/^[a-z][a-z0-9+.-]*:/.test(component.source)) {
120
- problems.push(`${said}'s module “${component.source}” is a file of the application's folder: it is drawn once the application is packaged with it (LOOP P-29); give the address of a built ES module.`);
134
+ if (!CUSTOM_FILE.test(component.source)) {
135
+ problems.push(`${said}'s module “${component.source}” is a file of the application's folder, named by its path in it: a \`.js\` or \`.mjs\` file inside the folder (\`gauge.js\`, \`components/gauge.js\`).`);
136
+ }
121
137
  }
122
138
  else if (!CUSTOM_SOURCE.test(component.source)) {
123
139
  problems.push(`${said}'s module “${component.source}” is loaded over \`https://\`, or from \`http://localhost\` while it is written.`);
@@ -197,6 +213,11 @@ export function customComponentProblems(component) {
197
213
  }
198
214
  return problems;
199
215
  }
216
+ /**
217
+ * Whether an application's page side is packaged (LOOP P-29): a component
218
+ * its developer wrote is a file of its folder.
219
+ */
220
+ export const hasFolderModules = (app) => (app.interface.customComponents ?? []).some(component => isFolderSource(component.source));
200
221
  /** What stops the components an application's developer wrote, in sentences. */
201
222
  export function customComponentsProblems(app) {
202
223
  const own = app.interface.customComponents ?? [];
@@ -0,0 +1,7 @@
1
+ export type AppFilesContribution = {
2
+ /** The application whose folder it is, by id. */
3
+ app: string;
4
+ /** The absolute address its folder's files are served under, ending in `/`. */
5
+ base: string;
6
+ };
7
+ export declare const LoopAppFiles: import("@datalayer/reactor").ContributionPoint<AppFilesContribution>;
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Where the files of an application's folder are served (LOOP P-29), as a
7
+ * contribution point, re-exported by `apps/core`.
8
+ *
9
+ * A component its developer wrote may be a file of its folder (`source:
10
+ * gauge.js`). `loop apps package` puts the folder's files in the
11
+ * application's wheel, beside a generated page-side plugin
12
+ * (`loop-app-<id>/page`, `agent_runtimes/loop/apps/packaging.py`) whose one
13
+ * contribution is to this point: the address its own directory is served at
14
+ * by the server the wheel is installed beside. The page installs that plugin
15
+ * through Reactor's `bootstrapExtensions` (`AppRuntimePlugins`), and a file
16
+ * of the folder is fetched from there and drawn in its sandboxed frame.
17
+ *
18
+ * The plugin is a plain object of no import, so the point is named by its
19
+ * id: `loop.app.files` is what the generated module says too.
20
+ *
21
+ * @module apps/core/appFiles
22
+ */
23
+ import { defineContributionPoint } from '@datalayer/reactor';
24
+ export const LoopAppFiles = defineContributionPoint('loop.app.files');
@@ -600,6 +600,7 @@ export declare const LoopAssistantMenu: import("@datalayer/reactor").Contributio
600
600
  export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
601
601
  export { LoopCanvasPart, type CanvasPartContribution, type CanvasPartKind, } from './canvasParts';
602
602
  export { LoopA2uiComponent, type A2uiComponentContribution, } from './a2uiComponents';
603
+ export { LoopAppFiles, type AppFilesContribution } from './appFiles';
603
604
  /**
604
605
  * The per-example chat extras a host feeds the loop's conversation live.
605
606
  *
@@ -178,6 +178,9 @@ export { LoopCanvasPart, } from './canvasParts';
178
178
  // A component an application adds to the catalog it is drawn with (LOOP
179
179
  // P-17): the components its developer wrote, for its workspace alone.
180
180
  export { LoopA2uiComponent, } from './a2uiComponents';
181
+ // Where the files of an application's folder are served (LOOP P-29): said
182
+ // by the page side its package carries.
183
+ export { LoopAppFiles } from './appFiles';
181
184
  export const LoopChatExtras = defineContributionPoint('loop.chat.extras');
182
185
  export const LoopOpeningNotebook = defineContributionPoint('loop.notebook.opening');
183
186
  export const LoopOpeningDocument = defineContributionPoint('loop.document.opening');
@@ -359,7 +359,11 @@ export function EmbedThemed({ accent, colorMode, font, variant, children, style,
359
359
  _jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: getThemeConfig(variant).primerTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
360
360
  }
361
361
  export function AppEmbed({ app, mode, accent, colorMode, font, language, serverUrl, instance, embedToken, visitors, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, resume = true, }) {
362
- const bridge = useHostBridge(app, host);
362
+ const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
363
+ // Inline, the page is bridged here; in a bubble, a panel or the assistant,
364
+ // `AppFloating` bridges it itself. Bridged twice, every window message of a
365
+ // turn reached the page twice (seen 2026-10-10 with P-27's copilot).
366
+ const bridge = useHostBridge(app, shownAs === 'inline' ? host : undefined);
363
367
  // Where a visitor without an account runs it (D-07): one object, so that
364
368
  // the renderer never reads a change.
365
369
  const renderer = useMemo(() => (visitors ? { datalayerVisitors: visitors } : undefined), [visitors]);
@@ -377,7 +381,6 @@ export function AppEmbed({ app, mode, accent, colorMode, font, language, serverU
377
381
  const variant = app.interface.theme?.variant ?? 'loop';
378
382
  const asked = colorMode ?? app.interface.theme?.mode ?? 'auto';
379
383
  const resolvedMode = asked === 'auto' ? system : asked;
380
- const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
381
384
  const view = floatingViewOf(shownAs);
382
385
  const worn = accent ?? app.interface.accent;
383
386
  const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font, variant }), [worn, font, variant]);
@@ -76,4 +76,4 @@ export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSn
76
76
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
77
77
  export { loopPlugins, type LoopPresetOptions } from './presets';
78
78
  export { DocumentExtension, NotebookExtension } from './extensions';
79
- export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, type AppCheck, type CheckContext, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, pageOutputShows, } from './apps';
79
+ export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, type AppCheck, type CheckContext, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, folderSourceUrl, hasFolderModules, isFolderSource, pageOutputShows, } from './apps';
package/lib/apps/index.js CHANGED
@@ -103,4 +103,4 @@ export { loopPlugins } from './presets';
103
103
  export { DocumentExtension, NotebookExtension } from './extensions';
104
104
  // Applications: what one does when its agent calls a tool — the rules a
105
105
  // person wrote, decided on what the tool does.
106
- export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, defineAppPlugin, appLayoutOptions, readAppspecYaml, writeAppspecYaml, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, pageOutputShows, } from './apps';
106
+ export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, defineAppPlugin, appLayoutOptions, readAppspecYaml, writeAppspecYaml, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, folderSourceUrl, hasFolderModules, isFolderSource, pageOutputShows, } from './apps';
@@ -62,7 +62,7 @@ export function SandboxSelector(_props) {
62
62
  // Held here: drawn, said why on hover, not taken. `aria-disabled`
63
63
  // rather than `disabled`: a disabled button takes no pointer, and
64
64
  // its reason would never be read.
65
- const held = heldTargetReason(entry);
65
+ const held = heldTargetReason(entry, service?.serverUrl ?? '');
66
66
  return (_jsx(SegmentedControl.Button, { selected: position === index, "aria-disabled": held ? true : undefined, "data-sandbox-target": entry, "data-sandbox-target-held": held ? '' : undefined, sx: {
67
67
  listStyle: 'none',
68
68
  ...(held
@@ -15,7 +15,7 @@ import { createTeamSelection } from './team';
15
15
  import { createSwitchableSandboxService, heldTargetReason, TARGET_SPECS, targetHasAgent, } from './switchable';
16
16
  export const AGENTS_PLUGIN_NAME = '@datalayer/loop-plugin-agents';
17
17
  /** The target asked for, or the page itself when this page holds it. */
18
- const offeredTarget = (target) => heldTargetReason(target) ? 'browser' : target;
18
+ const offeredTarget = (target, serverUrl) => (heldTargetReason(target, serverUrl) ? 'browser' : target);
19
19
  export const AgentsPlugin = definePlugin({
20
20
  name: AGENTS_PLUGIN_NAME,
21
21
  displayName: 'Agents',
@@ -51,7 +51,7 @@ export const AgentsPlugin = definePlugin({
51
51
  // A host that names no target gets the service's own default, Local —
52
52
  // read here, so that it is held on the web like any other.
53
53
  initialTarget: offeredTarget((showVariants || config.targetFixed ? config.target : 'browser') ??
54
- 'local'),
54
+ 'local', config.serverUrl),
55
55
  kernelSource: config.kernelSource,
56
56
  localAgent: config.localAgent,
57
57
  // The capacity plugins' say, read at switch time — lazily, because a
@@ -66,7 +66,7 @@ export declare const SANDBOX_TARGETS: readonly SandboxTarget[];
66
66
  * JupyterLab it is offered as before — the same line the code-sandbox picker
67
67
  * draws for a Jupyter Server's kernels (`codeSandboxProviders`).
68
68
  */
69
- export declare function heldTargetReason(target: SandboxTarget): string | undefined;
69
+ export declare function heldTargetReason(target: SandboxTarget, serverUrl: string): string | undefined;
70
70
  export declare const TARGET_SPECS: Record<SandboxTarget, SandboxTargetSpec>;
71
71
  /** Whether an agent runs on this target — the question the chat asks. */
72
72
  export declare function targetHasAgent(target: SandboxTarget): boolean;
@@ -52,8 +52,13 @@ export const SANDBOX_TARGETS = [
52
52
  * JupyterLab it is offered as before — the same line the code-sandbox picker
53
53
  * draws for a Jupyter Server's kernels (`codeSandboxProviders`).
54
54
  */
55
- export function heldTargetReason(target) {
56
- if (target === 'local' && !insideJupyterLab()) {
55
+ export function heldTargetReason(target, serverUrl) {
56
+ // A host that names its own agent-runtimes server — `app.mount`, `loop apps
57
+ // run --web` (LOOP P-08, P-25) — is not asking for the reader's machine:
58
+ // Local is that server, and it is offered. Held, every message of a page
59
+ // served by its own server went to the visitors' runtime instead
60
+ // (seen 2026-10-10 on 1.3.96).
61
+ if (target === 'local' && !serverUrl && !insideJupyterLab()) {
57
62
  return 'Not offered on the web for now: a local agent and the Jupyter server it starts run beside JupyterLab, not in a page.';
58
63
  }
59
64
  return undefined;
@@ -317,7 +322,7 @@ export function createSwitchableSandboxService({ serverUrl, initialTarget = 'loc
317
322
  return;
318
323
  }
319
324
  // Held here: refused whoever asks, not only by the switch's button.
320
- const held = heldTargetReason(next);
325
+ const held = heldTargetReason(next, serverUrl);
321
326
  if (held) {
322
327
  throw new Error(held);
323
328
  }
@@ -49,7 +49,7 @@ export function defineAppComponentsPlugin(app, fetcher) {
49
49
  contribution(LoopA2uiComponent, {
50
50
  id: component.name,
51
51
  app: app.id,
52
- implementation: customImplementation(component, app.version, fetcher),
52
+ implementation: customImplementation(component, app.version, fetcher, app.id),
53
53
  }, { id: component.name }),
54
54
  ]),
55
55
  });
@@ -8,7 +8,8 @@
8
8
  * started_at, ended_at}` — which the AG-UI adapter hands on as an
9
9
  * `activity`. A step is drawn as the chat draws a tool call: one row,
10
10
  * collapsed, its input and output inside, running until it ends; a nested
11
- * step says the step it is in.
11
+ * step says the step it is in. A step that streams its output (LOOP P-31)
12
+ * is said again as it writes, its output so far, and drawn in place.
12
13
  */
13
14
  import type { DisplayItem, ToolCallMessage } from '../../types/chat';
14
15
  /** The `CUSTOM` event's name. */
@@ -43,9 +43,8 @@ export function stepItem(step, inside) {
43
43
  kind: step.kind,
44
44
  ...(step.input !== null ? { input: step.input } : {}),
45
45
  },
46
- ...(step.ended && !step.error && step.output !== null
47
- ? { result: step.output }
48
- : {}),
46
+ // Running, what it wrote so far — a step that streams (LOOP P-31).
47
+ ...(!step.error && step.output !== null ? { result: step.output } : {}),
49
48
  status: !step.ended ? 'executing' : step.error ? 'error' : 'complete',
50
49
  ...(step.error ? { error: step.error } : {}),
51
50
  summary: inside ? `${kind}, inside ${inside}` : kind,
@@ -208,7 +208,8 @@ export function ToolCallDisplay({ toolCallId, toolName, args, result, status, er
208
208
  wordBreak: 'break-word',
209
209
  }, children: Object.keys(args).length > 0
210
210
  ? JSON.stringify(args, null, 2)
211
- : `(${chatText.noParameters})` }) })] }), status === 'complete' && result !== undefined && (_jsxs(Box, { children: [_jsx(Text, { sx: {
211
+ : `(${chatText.noParameters})` }) })] }), (status === 'complete' || status === 'executing') &&
212
+ result !== undefined && (_jsxs(Box, { children: [_jsx(Text, { sx: {
212
213
  display: 'block',
213
214
  fontSize: 0,
214
215
  fontWeight: 'semibold',
@@ -21,19 +21,31 @@ type CustomProps = Record<string, unknown> & {
21
21
  };
22
22
  /** What the frame is given: its declared properties and what it shows, nothing else. */
23
23
  export declare function givenToFrame(component: AppCustomComponentSpec, props: Record<string, unknown>): Record<string, unknown>;
24
+ /**
25
+ * Where the files of an application's folder are served, as its package's
26
+ * page side said (`LoopAppFiles`, LOOP P-29); undefined until it says it —
27
+ * while its runtime starts, or for ever on a server it is not installed
28
+ * beside.
29
+ */
30
+ export declare function useAppFilesBase(appId: string): string | undefined;
24
31
  /** A component its developer wrote, in its frame. */
25
- export declare function CustomComponentView({ component, props, fetcher, }: {
32
+ export declare function CustomComponentView({ component, props, fetcher, appId, }: {
26
33
  component: AppCustomComponentSpec;
27
34
  props: CustomProps;
28
35
  /** How its module is fetched; the page's `fetch` unless given (tests). */
29
36
  fetcher?: typeof fetch;
37
+ /**
38
+ * Its application's id: where a module that is a file of its folder is
39
+ * served is said for that application (LOOP P-29).
40
+ */
41
+ appId?: string;
30
42
  }): JSX.Element;
31
43
  /**
32
44
  * A component its developer wrote as A2UI's renderer registers one: its
33
45
  * schema from its declaration, its view by A2UI's own factory, drawn in its
34
46
  * frame.
35
47
  */
36
- export declare function customImplementation(component: AppCustomComponentSpec, version?: string, fetcher?: typeof fetch): ReactComponentImplementation;
48
+ export declare function customImplementation(component: AppCustomComponentSpec, version?: string, fetcher?: typeof fetch, appId?: string): ReactComponentImplementation;
37
49
  /** The renderers of the components an application's developer wrote, for its page alone. */
38
- export declare function customImplementations(app: Pick<AppSpec, 'interface' | 'version'>, fetcher?: typeof fetch): ReactComponentImplementation[];
50
+ export declare function customImplementations(app: Pick<AppSpec, 'id' | 'interface' | 'version'>, fetcher?: typeof fetch): ReactComponentImplementation[];
39
51
  export { CUSTOM_FRAME_SANDBOX, customFrameDocument, customFramePolicy, customModule, fromFrame, integrityHolds, } from './sandbox';
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
3
+ import { useOptionalReactorPlatform } from '@datalayer/reactor/react';
3
4
  import { createComponentImplementation, } from '@a2ui/react/v0_9';
4
- import { customComponentEntry } from '../../../apps/apps/customComponents';
5
+ import { customComponentEntry, folderSourceUrl, isFolderSource, } from '../../../apps/apps/customComponents';
6
+ import { LoopAppFiles } from '../../../apps/core/appFiles';
5
7
  import { componentSchemaOf } from '../datalayer/schema';
6
8
  import { BlockFrame, Problem } from '../datalayer/parts';
7
9
  import { CUSTOM_FRAME_SANDBOX, FRAME_TAG, customFrameDocument, customModule, frameNonce, fromFrame, } from './sandbox';
@@ -13,11 +15,37 @@ export function givenToFrame(component, props) {
13
15
  }
14
16
  /** The setter A2UI gives a binding: `setValue` for `value`. */
15
17
  const setterOf = (name) => `set${name.charAt(0).toUpperCase()}${name.slice(1)}`;
18
+ /**
19
+ * Where the files of an application's folder are served, as its package's
20
+ * page side said (`LoopAppFiles`, LOOP P-29); undefined until it says it —
21
+ * while its runtime starts, or for ever on a server it is not installed
22
+ * beside.
23
+ */
24
+ export function useAppFilesBase(appId) {
25
+ const reactor = useOptionalReactorPlatform();
26
+ const subscribe = useCallback((listener) => reactor ? reactor.subscribe(listener) : () => undefined, [reactor]);
27
+ const revision = useSyncExternalStore(subscribe, () => reactor?.getRevision() ?? 0);
28
+ return useMemo(() => reactor
29
+ ?.getContributions(LoopAppFiles)
30
+ .find(entry => entry.value.app === appId)?.value.base,
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ [reactor, revision, appId]);
33
+ }
16
34
  /** A component its developer wrote, in its frame. */
17
- export function CustomComponentView({ component, props, fetcher, }) {
35
+ export function CustomComponentView({ component, props, fetcher, appId = '', }) {
18
36
  const frame = useRef(null);
19
37
  const [problem, setProblem] = useState(null);
20
38
  const [ready, setReady] = useState(false);
39
+ const [frameReady, setFrameReady] = useState(false);
40
+ // Its module's address: its own, or a file of its folder under the base
41
+ // its package's page side said — none until said.
42
+ const folder = isFolderSource(component.source);
43
+ const base = useAppFilesBase(appId);
44
+ const source = folder
45
+ ? base
46
+ ? folderSourceUrl(component.source, base)
47
+ : undefined
48
+ : component.source;
21
49
  const srcDoc = useMemo(() => customFrameDocument(frameNonce(), component.name), [component.name]);
22
50
  // What it is given, told again only when a value of it changes.
23
51
  const givenKey = JSON.stringify(givenToFrame(component, props));
@@ -32,17 +60,7 @@ export function CustomComponentView({ component, props, fetcher, }) {
32
60
  return;
33
61
  }
34
62
  if (said.kind === 'ready') {
35
- customModule(component.source, component.integrity, fetcher)
36
- .then(code => {
37
- tell({
38
- tag: FRAME_TAG,
39
- kind: 'props',
40
- props: givenToFrame(component, propsRef.current),
41
- });
42
- tell({ tag: FRAME_TAG, kind: 'module', code });
43
- setReady(true);
44
- })
45
- .catch((error) => setProblem(error instanceof Error ? error.message : String(error)));
63
+ setFrameReady(true);
46
64
  }
47
65
  else if (said.kind === 'failed') {
48
66
  setProblem(`It could not be drawn: ${said.message}.`);
@@ -62,13 +80,41 @@ export function CustomComponentView({ component, props, fetcher, }) {
62
80
  };
63
81
  window.addEventListener('message', listen);
64
82
  return () => window.removeEventListener('message', listen);
65
- }, [component, fetcher]);
83
+ }, [component]);
84
+ // Its module, handed over once the frame is ready and its address known.
85
+ useEffect(() => {
86
+ if (!frameReady || !source) {
87
+ return;
88
+ }
89
+ let current = true;
90
+ customModule(source, component.integrity, fetcher)
91
+ .then(code => {
92
+ if (!current) {
93
+ return;
94
+ }
95
+ tell({
96
+ tag: FRAME_TAG,
97
+ kind: 'props',
98
+ props: givenToFrame(component, propsRef.current),
99
+ });
100
+ tell({ tag: FRAME_TAG, kind: 'module', code });
101
+ setReady(true);
102
+ })
103
+ .catch((error) => {
104
+ if (current) {
105
+ setProblem(error instanceof Error ? error.message : String(error));
106
+ }
107
+ });
108
+ return () => {
109
+ current = false;
110
+ };
111
+ }, [frameReady, source, component, fetcher]);
66
112
  useEffect(() => {
67
113
  if (ready) {
68
114
  tell({ tag: FRAME_TAG, kind: 'props', props: given });
69
115
  }
70
116
  }, [ready, given]);
71
- return (_jsxs(BlockFrame, { label: component.description || component.name, weight: props.weight, testId: `custom-${component.name}`, children: [problem ? _jsx(Problem, { children: problem }) : null, _jsx("iframe", { ref: frame, title: component.description || component.name, sandbox: CUSTOM_FRAME_SANDBOX, srcDoc: srcDoc, referrerPolicy: "no-referrer", style: {
117
+ return (_jsxs(BlockFrame, { label: component.description || component.name, weight: props.weight, testId: `custom-${component.name}`, children: [problem ? _jsx(Problem, { children: problem }) : null, !problem && folder && !source ? (_jsx(Problem, { children: `Its module “${component.source}” is a file of its application's folder: it is drawn once the server running the application serves its package (loop apps package, installed beside that server).` })) : null, _jsx("iframe", { ref: frame, title: component.description || component.name, sandbox: CUSTOM_FRAME_SANDBOX, srcDoc: srcDoc, referrerPolicy: "no-referrer", style: {
72
118
  width: '100%',
73
119
  height: `${component.height}px`,
74
120
  border: 0,
@@ -80,14 +126,14 @@ export function CustomComponentView({ component, props, fetcher, }) {
80
126
  * schema from its declaration, its view by A2UI's own factory, drawn in its
81
127
  * frame.
82
128
  */
83
- export function customImplementation(component, version = '0.0.0', fetcher) {
129
+ export function customImplementation(component, version = '0.0.0', fetcher, appId = '') {
84
130
  const entry = customComponentEntry(component, version);
85
131
  const api = { name: component.name, schema: componentSchemaOf(entry) };
86
- const Render = ({ props }) => (_jsx(CustomComponentView, { component: component, props: props, fetcher: fetcher }));
132
+ const Render = ({ props }) => (_jsx(CustomComponentView, { component: component, props: props, fetcher: fetcher, appId: appId }));
87
133
  return createComponentImplementation(api, Render);
88
134
  }
89
135
  /** The renderers of the components an application's developer wrote, for its page alone. */
90
136
  export function customImplementations(app, fetcher) {
91
- return (app.interface.customComponents ?? []).map(component => customImplementation(component, app.version, fetcher));
137
+ return (app.interface.customComponents ?? []).map(component => customImplementation(component, app.version, fetcher, app.id));
92
138
  }
93
139
  export { CUSTOM_FRAME_SANDBOX, customFrameDocument, customFramePolicy, customModule, fromFrame, integrityHolds, } from './sandbox';
@@ -20,6 +20,7 @@ export declare const EXAMPLE_AI_EXPLAINS_NOTEBOOK_OUTPUT_AGENTSPEC_0_0_1: Agents
20
20
  export declare const EXAMPLE_AI_WRITES_PANDAS_CODE_AGENTSPEC_0_0_1: Agentspec;
21
21
  export declare const EXAMPLE_ANALYZE_EXCEL_SPREADSHEET_AGENTSPEC_0_0_1: Agentspec;
22
22
  export declare const EXAMPLE_BACKEND_TOOL_RENDERING_AGENTSPEC_0_0_1: Agentspec;
23
+ export declare const EXAMPLE_BLANK_AGENTSPEC_0_0_1: Agentspec;
23
24
  export declare const EXAMPLE_BUILD_NOTEBOOK_WITH_ONE_PROMPT_AGENTSPEC_0_0_1: Agentspec;
24
25
  export declare const EXAMPLE_CHECKPOINTS_AGENTSPEC_0_0_1: Agentspec;
25
26
  export declare const EXAMPLE_CLOUDFLARE_AGENTSPEC_0_0_1: Agentspec;