@datalayer/agent-runtimes 1.3.29 → 1.3.32
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/AgentNode.js +14 -33
- package/lib/chat/ChatFloating.js +40 -8
- package/lib/chat/assistant/AssistantStage.d.ts +26 -7
- package/lib/chat/assistant/AssistantStage.js +115 -8
- package/lib/chat/assistant/SpeechBalloon.js +7 -3
- package/lib/chat/assistant/characters.d.ts +32 -1
- package/lib/chat/assistant/characters.js +180 -15
- package/lib/chat/assistant/state.d.ts +28 -0
- package/lib/chat/assistant/state.js +31 -0
- package/lib/chat/base/ChatBase.js +74 -50
- package/lib/chat/base/modelChoice.d.ts +40 -1
- package/lib/chat/base/modelChoice.js +42 -15
- package/lib/chat/prompt/InputPrompt.d.ts +7 -1
- package/lib/chat/prompt/InputPrompt.js +2 -2
- package/lib/chat/prompt/InputPromptBase.js +1 -1
- package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
- package/lib/chat/prompt/menus/ModelSelector.js +17 -5
- package/lib/chat/useViewportDrag.d.ts +9 -1
- package/lib/chat/useViewportDrag.js +4 -4
- package/lib/components/a2ui/index.d.ts +2 -0
- package/lib/components/a2ui/index.js +2 -0
- package/lib/components/a2ui/visibility.d.ts +15 -0
- package/lib/components/a2ui/visibility.js +24 -0
- package/lib/components/a2ui/visibleWhen.d.ts +17 -0
- package/lib/components/a2ui/visibleWhen.js +54 -0
- package/lib/examples/ChatAssistantExample.d.ts +4 -0
- package/lib/examples/ChatAssistantExample.js +26 -5
- package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
- package/lib/examples/utils/owlCharacterPlugin.js +44 -0
- package/lib/loop/apps/AppFeedback.d.ts +27 -0
- package/lib/loop/apps/AppFeedback.js +78 -0
- package/lib/loop/apps/AppRenderer.d.ts +33 -1
- package/lib/loop/apps/AppRenderer.js +111 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/appspec.js +6 -3
- package/lib/loop/apps/checks.d.ts +10 -2
- package/lib/loop/apps/checks.js +21 -10
- package/lib/loop/apps/feedback.d.ts +51 -0
- package/lib/loop/apps/feedback.js +61 -0
- package/lib/loop/apps/index.d.ts +3 -0
- package/lib/loop/apps/index.js +3 -0
- package/lib/loop/apps/rules.d.ts +14 -0
- package/lib/loop/apps/rules.js +24 -0
- package/lib/loop/core/canvasBlocks.d.ts +16 -0
- package/lib/loop/core/canvasBlocks.js +13 -0
- package/lib/loop/core/index.d.ts +15 -0
- package/lib/loop/core/index.js +3 -0
- package/lib/loop/embed/AppEmbed.d.ts +42 -0
- package/lib/loop/embed/AppEmbed.js +205 -0
- package/lib/loop/embed/datalayer-app.d.ts +1 -0
- package/lib/loop/embed/datalayer-app.js +16 -0
- package/lib/loop/embed/element.d.ts +57 -0
- package/lib/loop/embed/element.js +405 -0
- package/lib/loop/embed/embedConfig.d.ts +120 -0
- package/lib/loop/embed/embedConfig.js +148 -0
- package/lib/loop/embed/embedTheme.d.ts +24 -0
- package/lib/loop/embed/embedTheme.js +83 -0
- package/lib/loop/index.d.ts +6 -2
- package/lib/loop/index.js +8 -2
- package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +23 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +36 -9
- package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -1
- package/lib/loop/plugins/agents/switchable.d.ts +7 -0
- package/lib/loop/plugins/agents/switchable.js +91 -88
- package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
- package/lib/loop/plugins/app-page/AppPage.js +53 -0
- package/lib/loop/plugins/app-page/appPageModel.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPageModel.js +443 -0
- package/lib/loop/plugins/app-page/index.d.ts +8 -0
- package/lib/loop/plugins/app-page/index.js +34 -0
- package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
- package/lib/loop/plugins/canvas-blocks/index.js +118 -0
- package/lib/loop/plugins/chat/ChatView.js +52 -26
- package/lib/loop/plugins/chat/turnState.d.ts +5 -2
- package/lib/loop/plugins/chat/turnState.js +2 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
- package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
- package/lib/loop/plugins/page-layout/index.d.ts +13 -0
- package/lib/loop/plugins/page-layout/index.js +10 -1
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +9 -0
- package/lib/specs/agents/agents.js +20 -4
- package/lib/specs/apps.d.ts +10 -1
- package/lib/specs/apps.js +1779 -154
- package/lib/specs/appspecSchema.js +14 -8
- package/lib/specs/models.d.ts +1 -1
- package/lib/specs/models.js +8 -8
- package/lib/specs/tools.d.ts +1 -0
- package/lib/specs/tools.js +19 -0
- package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
- package/lib/stories/loop/LoopReference.stories.js +31 -0
- package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
- package/lib/stories/loop/ReferenceScreens.js +256 -0
- package/lib/stories/loop/pictures-main.d.ts +1 -0
- package/lib/stories/loop/pictures-main.js +64 -0
- package/lib/types/agentspecs.d.ts +17 -1
- package/lib/types/chat.d.ts +5 -0
- package/lib/types/config.d.ts +20 -0
- package/package.json +10 -6
- package/scripts/check-cloudflare-models.py +2 -2
- package/scripts/codegen/generate_agents.py +50 -2
- package/scripts/codegen/generate_apps.py +38 -7
- package/scripts/codegen/generate_models.py +4 -4
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ThemeStyles } from '@datalayer/primer-addons';
|
|
2
|
+
import type { AppAccent } from '../../types/agentspecs';
|
|
3
|
+
/**
|
|
4
|
+
* The `loop` theme as the embed wears it: the application's accent over it
|
|
5
|
+
* in both modes (T-05), and the host's face when it named one.
|
|
6
|
+
*/
|
|
7
|
+
export declare function embedThemeStyles({ accent, font, }: {
|
|
8
|
+
accent: AppAccent;
|
|
9
|
+
font?: string;
|
|
10
|
+
}): ThemeStyles;
|
|
11
|
+
/**
|
|
12
|
+
* The element's own stylesheet, written into its shadow root.
|
|
13
|
+
*
|
|
14
|
+
* `all: initial` on the host: nothing the host page sets — a font, a colour,
|
|
15
|
+
* a line height, a text alignment — is inherited by the application. The
|
|
16
|
+
* theme's custom properties are set again inside, on the provider's element,
|
|
17
|
+
* so a host's variable of the same name never reaches what the application
|
|
18
|
+
* draws. A floating mode takes no room in the page: what it draws is fixed to
|
|
19
|
+
* the viewport.
|
|
20
|
+
*
|
|
21
|
+
* With the theme's own stylesheet, scoped to the provider's element in this
|
|
22
|
+
* root.
|
|
23
|
+
*/
|
|
24
|
+
export declare function embedShadowCss(themeCss?: string): string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { loopAccentStyles, loopFontFamily, loopThemeStyles, scopeThemeCss, THEME_SCOPE_ATTRIBUTE, } from '@datalayer/primer-addons';
|
|
6
|
+
/** One mode's properties, with the theme's face replaced by the host's. */
|
|
7
|
+
function withFace(styles, font) {
|
|
8
|
+
if (!font) {
|
|
9
|
+
return styles;
|
|
10
|
+
}
|
|
11
|
+
return Object.fromEntries(Object.entries(styles).map(([name, value]) => [
|
|
12
|
+
name,
|
|
13
|
+
typeof value === 'string'
|
|
14
|
+
? value.split(loopFontFamily).join(font)
|
|
15
|
+
: value,
|
|
16
|
+
]));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The `loop` theme as the embed wears it: the application's accent over it
|
|
20
|
+
* in both modes (T-05), and the host's face when it named one.
|
|
21
|
+
*/
|
|
22
|
+
export function embedThemeStyles({ accent, font = '', }) {
|
|
23
|
+
const mode = (which) => withFace({
|
|
24
|
+
...loopThemeStyles[which],
|
|
25
|
+
...loopAccentStyles(accent, which),
|
|
26
|
+
}, font);
|
|
27
|
+
return {
|
|
28
|
+
...loopThemeStyles,
|
|
29
|
+
light: mode('light'),
|
|
30
|
+
dark: mode('dark'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The element's own stylesheet, written into its shadow root.
|
|
35
|
+
*
|
|
36
|
+
* `all: initial` on the host: nothing the host page sets — a font, a colour,
|
|
37
|
+
* a line height, a text alignment — is inherited by the application. The
|
|
38
|
+
* theme's custom properties are set again inside, on the provider's element,
|
|
39
|
+
* so a host's variable of the same name never reaches what the application
|
|
40
|
+
* draws. A floating mode takes no room in the page: what it draws is fixed to
|
|
41
|
+
* the viewport.
|
|
42
|
+
*
|
|
43
|
+
* With the theme's own stylesheet, scoped to the provider's element in this
|
|
44
|
+
* root.
|
|
45
|
+
*/
|
|
46
|
+
export function embedShadowCss(themeCss) {
|
|
47
|
+
const own = `
|
|
48
|
+
:host {
|
|
49
|
+
all: initial;
|
|
50
|
+
display: block;
|
|
51
|
+
position: relative;
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
:host([hidden]) {
|
|
55
|
+
display: none;
|
|
56
|
+
}
|
|
57
|
+
:host(:not([data-embed-mode="inline"])) {
|
|
58
|
+
width: 0;
|
|
59
|
+
height: 0;
|
|
60
|
+
}
|
|
61
|
+
.datalayer-app-root,
|
|
62
|
+
.datalayer-app-portal {
|
|
63
|
+
font-family: var(--fontStack-sansSerif, system-ui, sans-serif);
|
|
64
|
+
}
|
|
65
|
+
.datalayer-app-said {
|
|
66
|
+
font: 14px/1.5 system-ui, sans-serif;
|
|
67
|
+
padding: 12px 16px;
|
|
68
|
+
border: 1px solid #d0d7de;
|
|
69
|
+
border-radius: 12px;
|
|
70
|
+
color: #1f2328;
|
|
71
|
+
background: #ffffff;
|
|
72
|
+
}
|
|
73
|
+
iframe.datalayer-app-frame {
|
|
74
|
+
display: block;
|
|
75
|
+
width: 100%;
|
|
76
|
+
border: 0;
|
|
77
|
+
min-height: 240px;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
return themeCss
|
|
81
|
+
? `${own}\n${scopeThemeCss(themeCss, [`[${THEME_SCOPE_ATTRIBUTE}]`])}`
|
|
82
|
+
: own;
|
|
83
|
+
}
|
package/lib/loop/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { ChatHeaderPlugin, CHAT_HEADER_PLUGIN_NAME, } from './plugins/chat-heade
|
|
|
16
16
|
export { InputPromptPlugin, INPUT_PROMPT_PLUGIN_NAME, type InputPromptPluginConfig, } from './plugins/input-prompt';
|
|
17
17
|
export { createChatExtrasPlugin, CHAT_EXTRAS_PLUGIN_NAME, type ChatExtrasHandle, } from './plugins/chat-extras';
|
|
18
18
|
export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, LoopOpeningNotebook, LoopPromptPanel, type ChatLayoutContribution, type ChatLayoutParts, type ChatTurnContribution, type ChatTurnSnapshot, type ChatTurnStatus, type LoopChatExtrasValue, type OpeningDocumentContribution, type OpeningNotebookContribution, type PromptPanelContribution, } from './core';
|
|
19
|
-
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, setPageLayoutChatMode, type LoopPageLayoutConfig, } from './plugins/page-layout';
|
|
19
|
+
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, setPageLayoutChatMode, SplitLayout, type LoopPageLayoutConfig, } from './plugins/page-layout';
|
|
20
20
|
export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
|
|
21
21
|
export type { TeamMember, TeamSelection } from './plugins/agents/team';
|
|
22
22
|
export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
|
|
@@ -50,6 +50,7 @@ export { SANDBOX_CAPACITIES, SandboxCapacityPlugins, } from './plugins/agent-cod
|
|
|
50
50
|
export { AgentA2uiPlugin, AGENT_A2UI_PLUGIN_NAME } from './plugins/agent-a2ui';
|
|
51
51
|
export { AgentA2uiJupyterOutputPlugin, AGENT_A2UI_JUPYTER_OUTPUT_PLUGIN_NAME, } from './plugins/agent-a2ui-jupyter-output';
|
|
52
52
|
export { defineA2uiScenePlugin, type A2uiSceneOptions, } from './plugins/a2ui-scene';
|
|
53
|
+
export { APP_KIND_PATHS, APP_PAGE_PLUGIN_NAME, APP_PAGE_SURFACE, AppPage, appKindPaths, appPageAction, appPageData, defineAppPagePlugin, hasAppPage, surfaceUnshown, type AppKindPaths, type AppPageAction, type AppPageKind, type AppPagePath, } from './plugins/app-page';
|
|
53
54
|
export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
|
|
54
55
|
export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
|
|
55
56
|
export { AGENTS_PLUGIN_NAME, AgentsPlugin, SandboxSelector, browserSource, createBrowserSandboxService, createServerSandboxService, createSwitchableSandboxService, suppliedSource, useOptionalSandboxService, useSandboxService, type AgentsConfig, type AgentsOutput, type SandboxExecution, type SandboxService, type SandboxTarget, type ServiceManagerSource, type SwitchableSandboxService, } from './plugins/agents';
|
|
@@ -63,7 +64,10 @@ export { WindowFrame, WindowFramePlugin, WINDOW_ACTIONS_SLOT, WINDOW_CONTROLS_SL
|
|
|
63
64
|
export { LoopCommandsPlugin, COMMANDS_PLUGIN_NAME } from './plugins/commands';
|
|
64
65
|
export { PromptPlugin, PROMPT_PLUGIN_NAME } from './plugins/prompt';
|
|
65
66
|
export { LoopEmbed, type LoopEmbedProps } from './embed/LoopEmbed';
|
|
67
|
+
export { AppEmbed, type AppEmbedProps } from './embed/AppEmbed';
|
|
68
|
+
export { defineDatalayerAppElement, type DatalayerAppElementOptions, } from './embed/element';
|
|
69
|
+
export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, type EmbedLook, type EmbedSnippetOptions, } from './embed/embedConfig';
|
|
66
70
|
export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
|
|
67
71
|
export { loopPlugins, type LoopPresetOptions } from './presets';
|
|
68
72
|
export { DocumentExtension, NotebookExtension } from './extensions';
|
|
69
|
-
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, type AppCheck, defineAppPlugin, type AppRendererProps, type AppInstance, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, } from './apps';
|
|
73
|
+
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, type AppCheck, type CheckContext, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, } from './apps';
|
package/lib/loop/index.js
CHANGED
|
@@ -29,7 +29,7 @@ export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, Loop
|
|
|
29
29
|
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen,
|
|
30
30
|
// How a host moves the composer: `sidebar` stands it in the conversation
|
|
31
31
|
// panel, under what was said, and opens the panel to show it.
|
|
32
|
-
setPageLayoutChatMode, } from './plugins/page-layout';
|
|
32
|
+
setPageLayoutChatMode, SplitLayout, } from './plugins/page-layout';
|
|
33
33
|
export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
|
|
34
34
|
export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
|
|
35
35
|
export { NotebookViewPlugin, NOTEBOOK_VIEW_PLUGIN_NAME, } from './plugins/notebook-view';
|
|
@@ -63,6 +63,7 @@ export { SANDBOX_CAPACITIES, SandboxCapacityPlugins, } from './plugins/agent-cod
|
|
|
63
63
|
export { AgentA2uiPlugin, AGENT_A2UI_PLUGIN_NAME } from './plugins/agent-a2ui';
|
|
64
64
|
export { AgentA2uiJupyterOutputPlugin, AGENT_A2UI_JUPYTER_OUTPUT_PLUGIN_NAME, } from './plugins/agent-a2ui-jupyter-output';
|
|
65
65
|
export { defineA2uiScenePlugin, } from './plugins/a2ui-scene';
|
|
66
|
+
export { APP_KIND_PATHS, APP_PAGE_PLUGIN_NAME, APP_PAGE_SURFACE, AppPage, appKindPaths, appPageAction, appPageData, defineAppPagePlugin, hasAppPage, surfaceUnshown, } from './plugins/app-page';
|
|
66
67
|
export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
|
|
67
68
|
export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
|
|
68
69
|
export { AGENTS_PLUGIN_NAME, AgentsPlugin, SandboxSelector, browserSource, createBrowserSandboxService, createServerSandboxService, createSwitchableSandboxService, suppliedSource, useOptionalSandboxService, useSandboxService, } from './plugins/agents';
|
|
@@ -81,6 +82,11 @@ export { LoopCommandsPlugin, COMMANDS_PLUGIN_NAME } from './plugins/commands';
|
|
|
81
82
|
export { PromptPlugin, PROMPT_PLUGIN_NAME } from './plugins/prompt';
|
|
82
83
|
// Loop, ready to drop into somebody else's page.
|
|
83
84
|
export { LoopEmbed } from './embed/LoopEmbed';
|
|
85
|
+
// An application in another product's page: the React component, the
|
|
86
|
+
// `<datalayer-app>` element, and the snippet a host pastes (LOOP D-07 to D-09).
|
|
87
|
+
export { AppEmbed } from './embed/AppEmbed';
|
|
88
|
+
export { defineDatalayerAppElement, } from './embed/element';
|
|
89
|
+
export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, } from './embed/embedConfig';
|
|
84
90
|
export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
|
|
85
91
|
// The set a workspace normally has, so a host starts from a working Loop
|
|
86
92
|
// rather than assembling one — and never from the example.
|
|
@@ -90,4 +96,4 @@ export { loopPlugins } from './presets';
|
|
|
90
96
|
export { DocumentExtension, NotebookExtension } from './extensions';
|
|
91
97
|
// Applications: what one does when its agent calls a tool — the rules a
|
|
92
98
|
// person wrote, decided on what the tool does.
|
|
93
|
-
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, defineAppPlugin, readAppspecYaml, writeAppspecYaml, } from './apps';
|
|
99
|
+
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, defineAppPlugin, appLayoutOptions, readAppspecYaml, writeAppspecYaml, } from './apps';
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text } from '@primer/react';
|
|
4
|
-
import { basicCatalog, } from '@a2ui/react/v0_9';
|
|
5
4
|
import { MessageProcessor, } from '@a2ui/web_core/v0_9';
|
|
6
5
|
import { useSignalValue } from '@datalayer/reactor/react';
|
|
7
|
-
import { A2uiSurfaceComposed } from '../../../components/a2ui';
|
|
6
|
+
import { A2uiSurfaceComposed, datalayerCatalog, } from '../../../components/a2ui';
|
|
8
7
|
import { useSandboxService } from '../agents';
|
|
9
8
|
/**
|
|
10
9
|
* A2UI's basic catalog draws from inherited `--a2ui-*` custom properties, so a
|
|
@@ -27,7 +26,7 @@ export default function A2uiView({ workspace }) {
|
|
|
27
26
|
// rather than whatever the sandbox happened to execute last.
|
|
28
27
|
const sourceRef = useRef('');
|
|
29
28
|
const renderRef = useRef(null);
|
|
30
|
-
const processor = useMemo(() => new MessageProcessor([
|
|
29
|
+
const processor = useMemo(() => new MessageProcessor([datalayerCatalog], action => {
|
|
31
30
|
// The round-trip: what the reader did goes back to the code that drew
|
|
32
31
|
// the surface, and the surface it returns replaces this one. Without
|
|
33
32
|
// this a surface is a screenshot with buttons on it.
|
|
@@ -8,15 +8,35 @@
|
|
|
8
8
|
* follows, and what keeps a generated form from being the one light card in
|
|
9
9
|
* a dark workspace.
|
|
10
10
|
*
|
|
11
|
+
* The same renderer draws an application's page (`loop/plugins/app-page`):
|
|
12
|
+
* there the surface is fed live `data` by path as the conversation moves, and
|
|
13
|
+
* an action is handed the surface it came from, so that what a block wrote
|
|
14
|
+
* into its data model can be read when a button is pressed. It draws from
|
|
15
|
+
* Datalayer's catalog: the basic one, any block shown only while its
|
|
16
|
+
* `visible_when` holds.
|
|
17
|
+
*
|
|
11
18
|
* @module loop/plugins/a2ui-surface/InlineSurface
|
|
12
19
|
*/
|
|
13
20
|
import type { JSX } from 'react';
|
|
14
|
-
import
|
|
21
|
+
import type { ReactComponentImplementation } from '@a2ui/react/v0_9';
|
|
22
|
+
import { type A2uiClientAction, type A2uiMessage, type SurfaceModel } from '@a2ui/web_core/v0_9';
|
|
23
|
+
/** A surface as the renderer holds it: its data model is `dataModel`. */
|
|
24
|
+
export type InlineSurfaceModel = SurfaceModel<ReactComponentImplementation>;
|
|
25
|
+
type Surface = InlineSurfaceModel;
|
|
15
26
|
/** The catalogue a surface is rewritten to before it is drawn. */
|
|
16
27
|
export declare const SURFACE_CATALOG_ID: string;
|
|
17
|
-
|
|
28
|
+
/** Values to publish into a surface, by path. */
|
|
29
|
+
export type InlineSurfaceData = Record<string, unknown>;
|
|
30
|
+
export declare function InlineSurface({ messages, onAction, validationError, data, }: {
|
|
18
31
|
messages: A2uiMessage[];
|
|
19
|
-
|
|
32
|
+
/** Told of an action, with the surface it came from. */
|
|
33
|
+
onAction: (action: A2uiClientAction, surface?: Surface) => void;
|
|
20
34
|
validationError?: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Values published into every surface's data model, by path, after the
|
|
37
|
+
* messages are processed and whenever one changes. Only a path whose value
|
|
38
|
+
* changed is written: what a person typed elsewhere in the model stays.
|
|
39
|
+
*/
|
|
40
|
+
data?: InlineSurfaceData;
|
|
21
41
|
}): JSX.Element | null;
|
|
22
42
|
export default InlineSurface;
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
|
-
import { basicCatalog, } from '@a2ui/react/v0_9';
|
|
5
4
|
import { MessageProcessor, } from '@a2ui/web_core/v0_9';
|
|
6
|
-
import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, } from '../../../components/a2ui';
|
|
5
|
+
import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, datalayerCatalog, } from '../../../components/a2ui';
|
|
7
6
|
/** The catalogue a surface is rewritten to before it is drawn. */
|
|
8
|
-
export const SURFACE_CATALOG_ID =
|
|
7
|
+
export const SURFACE_CATALOG_ID = datalayerCatalog.id;
|
|
9
8
|
const INHERIT_THEME = {
|
|
10
9
|
['--a2ui-color-surface']: 'var(--bgColor-muted)',
|
|
11
10
|
['--a2ui-color-on-surface']: 'var(--fgColor-default)',
|
|
12
11
|
['--a2ui-color-primary']: 'var(--fgColor-accent)',
|
|
13
12
|
['--a2ui-color-outline']: 'var(--borderColor-default)',
|
|
14
13
|
};
|
|
15
|
-
export function InlineSurface({ messages, onAction, validationError, }) {
|
|
14
|
+
export function InlineSurface({ messages, onAction, validationError, data, }) {
|
|
16
15
|
// Reached through a ref: the processor is built once, and the handler it
|
|
17
16
|
// was built with must not go stale when the host's does not.
|
|
18
17
|
const onActionRef = useRef(onAction);
|
|
19
18
|
onActionRef.current = onAction;
|
|
20
|
-
const processor = useMemo(() => new MessageProcessor([
|
|
19
|
+
const processor = useMemo(() => new MessageProcessor([datalayerCatalog], action => onActionRef.current(action, processorRef.current?.model.getSurface(action.surfaceId))), []);
|
|
20
|
+
const processorRef = useRef(undefined);
|
|
21
|
+
processorRef.current = processor;
|
|
21
22
|
const [surfaces, setSurfaces] = useState([]);
|
|
22
23
|
useEffect(() => {
|
|
23
24
|
const created = processor.onSurfaceCreated(surface => setSurfaces(previous => [...previous, surface]));
|
|
@@ -28,15 +29,41 @@ export function InlineSurface({ messages, onAction, validationError, }) {
|
|
|
28
29
|
};
|
|
29
30
|
}, [processor]);
|
|
30
31
|
// Once: the messages are one tool result, processed when it arrives.
|
|
32
|
+
// A tree the catalog refuses is said, where it would have been drawn,
|
|
33
|
+
// rather than thrown out of an effect and taking the page down with it.
|
|
31
34
|
const processedRef = useRef(false);
|
|
35
|
+
const [processError, setProcessError] = useState(null);
|
|
32
36
|
useEffect(() => {
|
|
33
37
|
if (processedRef.current) {
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
40
|
processedRef.current = true;
|
|
37
|
-
|
|
41
|
+
try {
|
|
42
|
+
processor.processMessages(messages);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
setProcessError(`This surface could not be drawn: ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
}
|
|
38
47
|
}, [messages, processor]);
|
|
39
|
-
|
|
48
|
+
// After the messages: the surfaces exist by then, created synchronously.
|
|
49
|
+
const publishedRef = useRef({});
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!data) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const published = publishedRef.current;
|
|
55
|
+
for (const [path, value] of Object.entries(data)) {
|
|
56
|
+
if (Object.is(published[path], value)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
published[path] = value;
|
|
60
|
+
for (const surface of processor.model.surfacesMap.values()) {
|
|
61
|
+
surface.dataModel.set(path, value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [data, processor]);
|
|
65
|
+
const notice = validationError || processError;
|
|
66
|
+
if (surfaces.length === 0 && !notice) {
|
|
40
67
|
return null;
|
|
41
68
|
}
|
|
42
69
|
return (_jsx(A2uiMarkdownProvider, { children: _jsxs(Box, { style: INHERIT_THEME, sx: {
|
|
@@ -44,7 +71,7 @@ export function InlineSurface({ messages, onAction, validationError, }) {
|
|
|
44
71
|
display: 'flex',
|
|
45
72
|
flexDirection: 'column',
|
|
46
73
|
gap: 2,
|
|
47
|
-
}, children: [
|
|
74
|
+
}, children: [notice ? (_jsx(Box, { role: "alert", sx: {
|
|
48
75
|
px: 3,
|
|
49
76
|
py: 2,
|
|
50
77
|
borderRadius: 2,
|
|
@@ -53,7 +80,7 @@ export function InlineSurface({ messages, onAction, validationError, }) {
|
|
|
53
80
|
borderColor: 'danger.muted',
|
|
54
81
|
color: 'danger.fg',
|
|
55
82
|
fontSize: 1,
|
|
56
|
-
}, children:
|
|
83
|
+
}, children: notice })) : null, surfaces.map(surface => (_jsx(Box, { sx: {
|
|
57
84
|
border: '1px solid',
|
|
58
85
|
borderColor: 'border.default',
|
|
59
86
|
borderRadius: 2,
|
|
@@ -34,6 +34,10 @@ export type AgentCapacityOptions = {
|
|
|
34
34
|
createPayload?: Record<string, unknown>;
|
|
35
35
|
/** The openers the empty chat offers for this capacity. */
|
|
36
36
|
suggestions?: ChatSuggestionItem[];
|
|
37
|
+
/** What the agent is told on top of its spec's prompt when the page turns it. */
|
|
38
|
+
instructions?: string;
|
|
39
|
+
/** The model it runs on in the page, in place of its spec's. */
|
|
40
|
+
model?: string;
|
|
37
41
|
};
|
|
38
42
|
export declare function defineAgentCapacityPlugin(options: AgentCapacityOptions): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
39
43
|
export default defineAgentCapacityPlugin;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { contribution, definePlugin, } from '@datalayer/reactor';
|
|
24
24
|
import { LoopAgentBlueprint, LoopChatSuggestion, } from '../../core';
|
|
25
25
|
export function defineAgentCapacityPlugin(options) {
|
|
26
|
-
const { key, displayName, description, specId, octicon, emoji, codemode, createPayload, suggestions, } = options;
|
|
26
|
+
const { key, displayName, description, specId, octicon, emoji, codemode, createPayload, suggestions, instructions, model, } = options;
|
|
27
27
|
return definePlugin({
|
|
28
28
|
name: `@datalayer/loop-plugin-agent-${key}`,
|
|
29
29
|
displayName,
|
|
@@ -40,6 +40,8 @@ export function defineAgentCapacityPlugin(options) {
|
|
|
40
40
|
enable_codemode: codemode ?? false,
|
|
41
41
|
...createPayload,
|
|
42
42
|
},
|
|
43
|
+
...(instructions ? { instructions } : {}),
|
|
44
|
+
...(model ? { model } : {}),
|
|
43
45
|
}, { id: key }),
|
|
44
46
|
...(suggestions && suggestions.length > 0
|
|
45
47
|
? [
|
|
@@ -105,3 +105,10 @@ export type SwitchableConfig = {
|
|
|
105
105
|
kernelSource?: ServiceManagerSource;
|
|
106
106
|
};
|
|
107
107
|
export declare function createSwitchableSandboxService({ serverUrl, initialTarget, kernelSource, localAgent, blueprint, }: SwitchableConfig): SwitchableSandboxService;
|
|
108
|
+
/**
|
|
109
|
+
* Create the agent whose per-agent Jupyter sandbox backs Local, on the
|
|
110
|
+
* agent-runtimes server at `serverUrl`, unless it is there already — then
|
|
111
|
+
* only its sandbox is asked for. Exported for a host that addresses the
|
|
112
|
+
* agent without a workspace: the embed's floating modes (`AppEmbed`).
|
|
113
|
+
*/
|
|
114
|
+
export declare function ensureServerAgent(serverUrl: string, agentId: string | undefined, createPayload: Record<string, unknown>): Promise<void>;
|
|
@@ -197,94 +197,9 @@ export function createSwitchableSandboxService({ serverUrl, initialTarget = 'loc
|
|
|
197
197
|
(await detail(restart)));
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
* The payload is passed rather than read from the closure: the caller has
|
|
204
|
-
* already established that it exists, and TypeScript cannot carry that
|
|
205
|
-
* narrowing across a function boundary.
|
|
206
|
-
*/
|
|
207
|
-
async function ensureLocalAgent(createPayload) {
|
|
208
|
-
const id = agentId?.trim();
|
|
209
|
-
if (!id) {
|
|
210
|
-
throw new Error('Local needs an agent id before its runtime can start.');
|
|
211
|
-
}
|
|
212
|
-
let existing;
|
|
213
|
-
try {
|
|
214
|
-
existing = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}`);
|
|
215
|
-
}
|
|
216
|
-
catch (reason) {
|
|
217
|
-
throw new Error(`Local needs an agent-runtimes server at ${serverUrl}, and nothing answered there. ` +
|
|
218
|
-
`Start one and try again. (${reason instanceof Error ? reason.message : String(reason)})`, { cause: reason });
|
|
219
|
-
}
|
|
220
|
-
if (existing.ok) {
|
|
221
|
-
// The agent is there, which does not mean its sandbox is. Reconfiguring
|
|
222
|
-
// or restarting the manager stops the per-agent sandboxes, because they
|
|
223
|
-
// were built under the previous settings — so coming back to Local after
|
|
224
|
-
// a spell on Jupyter finds the agent alive and nothing behind it. Asking
|
|
225
|
-
// for the sandbox is idempotent, so it costs a round trip and nothing
|
|
226
|
-
// else when one is already running.
|
|
227
|
-
await ensureLocalSandbox(id);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (existing.status !== 404) {
|
|
231
|
-
throw new Error(`The server could not inspect local agent ${id} (${existing.status} ${existing.statusText}). ` +
|
|
232
|
-
(await detail(existing)));
|
|
233
|
-
}
|
|
234
|
-
const created = await fetch(`${serverUrl}/api/v1/agents`, {
|
|
235
|
-
method: 'POST',
|
|
236
|
-
headers: { 'content-type': 'application/json' },
|
|
237
|
-
body: JSON.stringify({
|
|
238
|
-
// Defaults first, so a blueprint that names its own transport or
|
|
239
|
-
// sandbox variant — the sandbox capacity plugins do — wins over
|
|
240
|
-
// them; the name last, because Local's agent id is not negotiable.
|
|
241
|
-
transport: 'ag-ui',
|
|
242
|
-
sandbox_variant: 'jupyter-server',
|
|
243
|
-
...createPayload,
|
|
244
|
-
name: id,
|
|
245
|
-
}),
|
|
246
|
-
});
|
|
247
|
-
if (!created.ok && created.status !== 409) {
|
|
248
|
-
throw new Error(`The server could not launch local agent ${id} (${created.status} ${created.statusText}). ` +
|
|
249
|
-
(await detail(created)));
|
|
250
|
-
}
|
|
251
|
-
// A 409 means somebody else created it between the two calls above, and
|
|
252
|
-
// that agent is as unlikely to have a sandbox as one found by the GET.
|
|
253
|
-
if (created.status === 409) {
|
|
254
|
-
await ensureLocalSandbox(id);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Make sure the agent has somewhere to run.
|
|
259
|
-
*
|
|
260
|
-
* Creating an agent starts its sandbox, so this is only ever needed for one
|
|
261
|
-
* that already existed. Selecting a target is an operation, not a
|
|
262
|
-
* preference: when the promise resolves the sandbox is meant to be up, and
|
|
263
|
-
* without this the kernel indicator sits empty and the notebook binds to
|
|
264
|
-
* nothing.
|
|
265
|
-
*/
|
|
266
|
-
async function ensureLocalSandbox(id) {
|
|
267
|
-
const response = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}/sandbox/ensure`, { method: 'POST' });
|
|
268
|
-
if (!response.ok) {
|
|
269
|
-
throw new Error(`The server could not start a sandbox for local agent ${id} ` +
|
|
270
|
-
`(${response.status} ${response.statusText}). ` +
|
|
271
|
-
(await detail(response)));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* What the server said went wrong, when it said anything.
|
|
276
|
-
*
|
|
277
|
-
* FastAPI puts the reason in `detail`, and a status code on its own sends a
|
|
278
|
-
* reader to the network tab for something the response already contained.
|
|
279
|
-
*/
|
|
280
|
-
async function detail(response) {
|
|
281
|
-
try {
|
|
282
|
-
const body = (await response.json());
|
|
283
|
-
return typeof body.detail === 'string' ? body.detail : '';
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
return '';
|
|
287
|
-
}
|
|
200
|
+
/** The agent backing Local, on this workspace's server. */
|
|
201
|
+
function ensureLocalAgent(createPayload) {
|
|
202
|
+
return ensureServerAgent(serverUrl, agentId, createPayload);
|
|
288
203
|
}
|
|
289
204
|
function connectActive() {
|
|
290
205
|
disconnect?.();
|
|
@@ -403,3 +318,91 @@ export function createSwitchableSandboxService({ serverUrl, initialTarget = 'loc
|
|
|
403
318
|
},
|
|
404
319
|
};
|
|
405
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Create the agent whose per-agent Jupyter sandbox backs Local, on the
|
|
323
|
+
* agent-runtimes server at `serverUrl`, unless it is there already — then
|
|
324
|
+
* only its sandbox is asked for. Exported for a host that addresses the
|
|
325
|
+
* agent without a workspace: the embed's floating modes (`AppEmbed`).
|
|
326
|
+
*/
|
|
327
|
+
export async function ensureServerAgent(serverUrl, agentId, createPayload) {
|
|
328
|
+
const id = agentId?.trim();
|
|
329
|
+
if (!id) {
|
|
330
|
+
throw new Error('Local needs an agent id before its runtime can start.');
|
|
331
|
+
}
|
|
332
|
+
let existing;
|
|
333
|
+
try {
|
|
334
|
+
existing = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}`);
|
|
335
|
+
}
|
|
336
|
+
catch (reason) {
|
|
337
|
+
throw new Error(`Local needs an agent-runtimes server at ${serverUrl}, and nothing answered there. ` +
|
|
338
|
+
`Start one and try again. (${reason instanceof Error ? reason.message : String(reason)})`, { cause: reason });
|
|
339
|
+
}
|
|
340
|
+
if (existing.ok) {
|
|
341
|
+
// The agent is there, which does not mean its sandbox is. Reconfiguring
|
|
342
|
+
// or restarting the manager stops the per-agent sandboxes, because they
|
|
343
|
+
// were built under the previous settings — so coming back to Local after
|
|
344
|
+
// a spell on Jupyter finds the agent alive and nothing behind it. Asking
|
|
345
|
+
// for the sandbox is idempotent, so it costs a round trip and nothing
|
|
346
|
+
// else when one is already running.
|
|
347
|
+
await ensureServerSandbox(serverUrl, id);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (existing.status !== 404) {
|
|
351
|
+
throw new Error(`The server could not inspect local agent ${id} (${existing.status} ${existing.statusText}). ` +
|
|
352
|
+
(await detail(existing)));
|
|
353
|
+
}
|
|
354
|
+
const created = await fetch(`${serverUrl}/api/v1/agents`, {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
headers: { 'content-type': 'application/json' },
|
|
357
|
+
body: JSON.stringify({
|
|
358
|
+
// Defaults first, so a blueprint that names its own transport or
|
|
359
|
+
// sandbox variant — the sandbox capacity plugins do — wins over
|
|
360
|
+
// them; the name last, because Local's agent id is not negotiable.
|
|
361
|
+
transport: 'ag-ui',
|
|
362
|
+
sandbox_variant: 'jupyter-server',
|
|
363
|
+
...createPayload,
|
|
364
|
+
name: id,
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
if (!created.ok && created.status !== 409) {
|
|
368
|
+
throw new Error(`The server could not launch local agent ${id} (${created.status} ${created.statusText}). ` +
|
|
369
|
+
(await detail(created)));
|
|
370
|
+
}
|
|
371
|
+
// A 409 means somebody else created it between the two calls above, and
|
|
372
|
+
// that agent is as unlikely to have a sandbox as one found by the GET.
|
|
373
|
+
if (created.status === 409) {
|
|
374
|
+
await ensureServerSandbox(serverUrl, id);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Make sure the agent has somewhere to run.
|
|
379
|
+
*
|
|
380
|
+
* Creating an agent starts its sandbox, so this is only ever needed for one
|
|
381
|
+
* that already existed. Selecting a target is an operation, not a
|
|
382
|
+
* preference: when the promise resolves the sandbox is meant to be up, and
|
|
383
|
+
* without this the kernel indicator sits empty and the notebook binds to
|
|
384
|
+
* nothing.
|
|
385
|
+
*/
|
|
386
|
+
async function ensureServerSandbox(serverUrl, id) {
|
|
387
|
+
const response = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}/sandbox/ensure`, { method: 'POST' });
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
throw new Error(`The server could not start a sandbox for local agent ${id} ` +
|
|
390
|
+
`(${response.status} ${response.statusText}). ` +
|
|
391
|
+
(await detail(response)));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* What the server said went wrong, when it said anything.
|
|
396
|
+
*
|
|
397
|
+
* FastAPI puts the reason in `detail`, and a status code on its own sends a
|
|
398
|
+
* reader to the network tab for something the response already contained.
|
|
399
|
+
*/
|
|
400
|
+
async function detail(response) {
|
|
401
|
+
try {
|
|
402
|
+
const body = (await response.json());
|
|
403
|
+
return typeof body.detail === 'string' ? body.detail : '';
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return '';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An application's page, drawn beside its conversation: its A2UI surface on
|
|
3
|
+
* the workspace's own renderer (`InlineSurface`), fed by the chat's current
|
|
4
|
+
* turn, its buttons answered through the chat's controls.
|
|
5
|
+
*
|
|
6
|
+
* @module loop/plugins/app-page/AppPage
|
|
7
|
+
*/
|
|
8
|
+
import type { JSX } from 'react';
|
|
9
|
+
import type { AppSpec } from '../../../types/agentspecs';
|
|
10
|
+
import { type LoopWorkspaceContext } from '../../core';
|
|
11
|
+
export type AppPageProps = {
|
|
12
|
+
app: AppSpec;
|
|
13
|
+
workspace: LoopWorkspaceContext;
|
|
14
|
+
};
|
|
15
|
+
export declare function AppPage({ app, workspace }: AppPageProps): JSX.Element;
|
|
16
|
+
export default AppPage;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
import { signal } from '@datalayer/reactor';
|
|
5
|
+
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
6
|
+
import { LoopChatTurn, } from '../../core';
|
|
7
|
+
import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
|
|
8
|
+
import { appPageAction, appPageData, appPageMessages } from './appPageModel';
|
|
9
|
+
/** No chat in the workspace: the page reads a turn that never starts. */
|
|
10
|
+
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
11
|
+
export function AppPage({ app, workspace }) {
|
|
12
|
+
const entries = useContributions(LoopChatTurn);
|
|
13
|
+
const turn = useSignalValue(entries[0]?.value.turn ?? NO_TURN);
|
|
14
|
+
const messages = useMemo(() => appPageMessages(app, SURFACE_CATALOG_ID), [app]);
|
|
15
|
+
const data = useMemo(() => appPageData(app, turn), [app, turn]);
|
|
16
|
+
const [refusal, setRefusal] = useState(null);
|
|
17
|
+
// The workspace changes as the chat reports itself; the handler reads the latest.
|
|
18
|
+
const workspaceRef = useRef(workspace);
|
|
19
|
+
workspaceRef.current = workspace;
|
|
20
|
+
// The inputs in words as last sent: a chat's message carries them when they change.
|
|
21
|
+
const lastInputs = useRef('');
|
|
22
|
+
const onAction = useCallback((action, surface) => {
|
|
23
|
+
const outcome = appPageAction(app, { name: action.name, context: action.context ?? {} }, path => surface?.dataModel.get(path), lastInputs.current);
|
|
24
|
+
const controls = workspaceRef.current.viewControls;
|
|
25
|
+
if ('refused' in outcome) {
|
|
26
|
+
setRefusal(outcome.refused);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setRefusal(null);
|
|
30
|
+
if ('stop' in outcome) {
|
|
31
|
+
controls.stop?.();
|
|
32
|
+
}
|
|
33
|
+
else if ('newChat' in outcome) {
|
|
34
|
+
controls.newChat?.();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// The chat's own send, as a surface in the transcript submits; the
|
|
38
|
+
// prompt channel when the chat has not reported itself yet.
|
|
39
|
+
if (controls.send) {
|
|
40
|
+
controls.send(outcome.send);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
workspaceRef.current.prompts.submit(outcome.send);
|
|
44
|
+
}
|
|
45
|
+
lastInputs.current = outcome.inputs;
|
|
46
|
+
for (const path of outcome.clear) {
|
|
47
|
+
surface?.dataModel.set(path, '');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [app]);
|
|
51
|
+
return (_jsx(Box, { "data-testid": "app-page", sx: { height: '100%', minHeight: 0, overflow: 'auto', p: 3 }, children: _jsx(InlineSurface, { messages: messages, data: data, onAction: onAction, validationError: refusal }) }));
|
|
52
|
+
}
|
|
53
|
+
export default AppPage;
|