@datalayer/agent-runtimes 1.3.97 → 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';
@@ -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
  });
@@ -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';
@@ -269,7 +269,7 @@ export const APPSPEC_SCHEMA = {
269
269
  type: 'array',
270
270
  },
271
271
  source: {
272
- description: "The address of its module, a built ES module whose default export draws it (`export default function (root, {props, send})`): over `https://`, or `http://localhost` while it is written. A module of the application's folder waits for its packaging (LOOP P-29)",
272
+ description: "Its module, a built ES module whose default export draws it (`export default function (root, {props, send})`): an address over `https://`, or `http://localhost` while it is written; or a file of the application's folder, by its path in it (`gauge.js`), packaged with the application and served by the server its package is installed beside (LOOP P-29)",
273
273
  title: 'Source',
274
274
  type: 'string',
275
275
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.97",
3
+ "version": "1.3.98",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",