@datalayer/agent-runtimes 1.3.25 → 1.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/chat/messages/ChatMessages.d.ts +4 -4
- package/lib/chat/messages/ChatMessages.js +17 -17
- package/lib/config/AgentConfiguration.js +1 -1
- package/lib/examples/example-selector.js +1 -1
- package/lib/loop/apps/AppRenderer.d.ts +8 -1
- package/lib/loop/apps/AppRenderer.js +10 -2
- package/lib/loop/apps/checks.d.ts +6 -1
- package/lib/loop/apps/checks.js +17 -0
- package/lib/loop/embed/LoopEmbed.js +14 -1
- package/lib/loop/plugins/chat/ChatView.js +40 -6
- package/lib/loop/plugins/chat/Presence.d.ts +9 -0
- package/lib/loop/plugins/chat/Presence.js +57 -0
- package/lib/loop/plugins/chat/index.d.ts +27 -0
- package/lib/loop/plugins/chat/presenceStatus.d.ts +26 -0
- package/lib/loop/plugins/chat/presenceStatus.js +28 -0
- package/lib/loop/presets.d.ts +5 -1
- package/lib/loop/presets.js +4 -2
- package/lib/protocols/A2AAdapter.d.ts +1 -1
- package/lib/specs/uiPlugins.d.ts +6 -1
- package/lib/specs/uiPlugins.js +471 -0
- package/lib/stores/chatStore.d.ts +11 -11
- package/lib/stores/chatStore.js +34 -34
- package/lib/stores/index.d.ts +1 -1
- package/lib/stores/index.js +1 -1
- package/lib/types/agentspecs.d.ts +33 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.js +1 -1
- package/lib/types/{extensions.d.ts → uiPlugins.d.ts} +49 -49
- package/lib/types/{extensions.js → uiPlugins.js} +8 -8
- package/lib/{extensions/A2UIExtension.d.ts → ui-plugins/A2UIPlugin.d.ts} +7 -7
- package/lib/{extensions/A2UIExtension.js → ui-plugins/A2UIPlugin.js} +4 -4
- package/lib/{extensions/MCPUIExtension.d.ts → ui-plugins/MCPUIPlugin.d.ts} +9 -9
- package/lib/{extensions/MCPUIExtension.js → ui-plugins/MCPUIPlugin.js} +6 -6
- package/lib/ui-plugins/UIPluginRegistry.d.ts +65 -0
- package/lib/{extensions/ExtensionRegistry.js → ui-plugins/UIPluginRegistry.js} +32 -32
- package/lib/ui-plugins/index.d.ts +9 -0
- package/lib/ui-plugins/index.js +12 -0
- package/package.json +1 -1
- package/scripts/codegen/generate_agents.py +6 -6
- package/scripts/codegen/generate_ui_plugins.py +64 -4
- package/lib/extensions/ExtensionRegistry.d.ts +0 -65
- package/lib/extensions/index.d.ts +0 -9
- package/lib/extensions/index.js +0 -12
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import type { ChatMessage } from '../../types/messages';
|
|
9
|
-
import type {
|
|
9
|
+
import type { UIPluginRegistry } from '../../ui-plugins/UIPluginRegistry';
|
|
10
10
|
/**
|
|
11
11
|
* ChatMessages props
|
|
12
12
|
*/
|
|
@@ -26,11 +26,11 @@ export interface ChatMessagesProps {
|
|
|
26
26
|
autoScroll?: boolean;
|
|
27
27
|
/** Custom class name */
|
|
28
28
|
className?: string;
|
|
29
|
-
/** Optional
|
|
30
|
-
|
|
29
|
+
/** Optional plugin registry (defaults to store value) */
|
|
30
|
+
uiPluginRegistry?: UIPluginRegistry;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Chat Messages component
|
|
34
34
|
*/
|
|
35
|
-
export declare function ChatMessages({ messageRenderer, activityRenderer, showTimestamps, showAvatars, autoScroll, className,
|
|
35
|
+
export declare function ChatMessages({ messageRenderer, activityRenderer, showTimestamps, showAvatars, autoScroll, className, uiPluginRegistry: uiPluginRegistryProp, }: ChatMessagesProps): React.JSX.Element;
|
|
36
36
|
export default ChatMessages;
|
|
@@ -14,14 +14,14 @@ import { Text, RelativeTime } from '@primer/react';
|
|
|
14
14
|
import { Box, useThemeStore, getColorPalette } from '@datalayer/primer-addons';
|
|
15
15
|
import { PersonIcon, ToolsIcon } from '@primer/octicons-react';
|
|
16
16
|
import { AiAgentIcon } from '@datalayer/icons-react';
|
|
17
|
-
import { useChatMessages,
|
|
17
|
+
import { useChatMessages, useChatUIPluginRegistry, } from '../../stores/chatStore';
|
|
18
18
|
/**
|
|
19
19
|
* Chat Messages component
|
|
20
20
|
*/
|
|
21
|
-
export function ChatMessages({ messageRenderer, activityRenderer, showTimestamps = true, showAvatars = true, autoScroll = true, className,
|
|
21
|
+
export function ChatMessages({ messageRenderer, activityRenderer, showTimestamps = true, showAvatars = true, autoScroll = true, className, uiPluginRegistry: uiPluginRegistryProp, }) {
|
|
22
22
|
const messages = useChatMessages();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const storeUIPluginRegistry = useChatUIPluginRegistry();
|
|
24
|
+
const uiPluginRegistry = uiPluginRegistryProp ?? storeUIPluginRegistry;
|
|
25
25
|
const containerRef = useRef(null);
|
|
26
26
|
const lastMessageRef = useRef(null);
|
|
27
27
|
// Resolve a light color from the current user theme palette so the
|
|
@@ -40,12 +40,12 @@ export function ChatMessages({ messageRenderer, activityRenderer, showTimestamps
|
|
|
40
40
|
if (messageRenderer) {
|
|
41
41
|
return messageRenderer(message);
|
|
42
42
|
}
|
|
43
|
-
// Check
|
|
44
|
-
if (
|
|
45
|
-
const messageRenderers =
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
43
|
+
// Check plugin registry for custom renderer
|
|
44
|
+
if (uiPluginRegistry) {
|
|
45
|
+
const messageRenderers = uiPluginRegistry.getMessageRenderers();
|
|
46
|
+
const pluginRenderer = messageRenderers.find(r => r.canRender(message));
|
|
47
|
+
if (pluginRenderer) {
|
|
48
|
+
return pluginRenderer.render({
|
|
49
49
|
message,
|
|
50
50
|
isStreaming: false,
|
|
51
51
|
});
|
|
@@ -86,7 +86,7 @@ export function ChatMessages({ messageRenderer, activityRenderer, showTimestamps
|
|
|
86
86
|
? 'You'
|
|
87
87
|
: message.role === 'assistant'
|
|
88
88
|
? 'Assistant'
|
|
89
|
-
: 'System' }), showTimestamps && message.createdAt && (_jsx(RelativeTime, { datetime: message.createdAt.toISOString(), sx: { fontSize: 0, color: 'fg.muted' } }))] }), _jsx(Box, { sx: { fontSize: 1, lineHeight: 1.5 }, children: renderMessageContent(message, activityRenderer,
|
|
89
|
+
: 'System' }), showTimestamps && message.createdAt && (_jsx(RelativeTime, { datetime: message.createdAt.toISOString(), sx: { fontSize: 0, color: 'fg.muted' } }))] }), _jsx(Box, { sx: { fontSize: 1, lineHeight: 1.5 }, children: renderMessageContent(message, activityRenderer, uiPluginRegistry ?? undefined) })] })] }, message.id));
|
|
90
90
|
};
|
|
91
91
|
if (messages.length === 0) {
|
|
92
92
|
return (_jsxs(Box, { className: className, sx: {
|
|
@@ -107,7 +107,7 @@ export function ChatMessages({ messageRenderer, activityRenderer, showTimestamps
|
|
|
107
107
|
/**
|
|
108
108
|
* Render message content based on type
|
|
109
109
|
*/
|
|
110
|
-
function renderMessageContent(message, activityRenderer,
|
|
110
|
+
function renderMessageContent(message, activityRenderer, uiPluginRegistry) {
|
|
111
111
|
const { content } = message;
|
|
112
112
|
// Simple string content
|
|
113
113
|
if (typeof content === 'string') {
|
|
@@ -115,14 +115,14 @@ function renderMessageContent(message, activityRenderer, extensionRegistry) {
|
|
|
115
115
|
}
|
|
116
116
|
// Array of content parts
|
|
117
117
|
if (Array.isArray(content)) {
|
|
118
|
-
return (_jsx(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: content.map((part, index) => renderContentPart(part, index, activityRenderer,
|
|
118
|
+
return (_jsx(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: content.map((part, index) => renderContentPart(part, index, activityRenderer, uiPluginRegistry)) }));
|
|
119
119
|
}
|
|
120
120
|
return null;
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
123
|
* Render individual content part
|
|
124
124
|
*/
|
|
125
|
-
function renderContentPart(part, index, activityRenderer,
|
|
125
|
+
function renderContentPart(part, index, activityRenderer, uiPluginRegistry) {
|
|
126
126
|
switch (part.type) {
|
|
127
127
|
case 'text':
|
|
128
128
|
return (_jsx(Text, { sx: { whiteSpace: 'pre-wrap' }, children: part.text }, index));
|
|
@@ -168,9 +168,9 @@ function renderContentPart(part, index, activityRenderer, extensionRegistry) {
|
|
|
168
168
|
data: part.data,
|
|
169
169
|
}) }, index));
|
|
170
170
|
}
|
|
171
|
-
// Check
|
|
172
|
-
if (
|
|
173
|
-
const renderer =
|
|
171
|
+
// Check plugin registry
|
|
172
|
+
if (uiPluginRegistry) {
|
|
173
|
+
const renderer = uiPluginRegistry.getActivityRenderer(part.activityType || 'unknown');
|
|
174
174
|
if (renderer) {
|
|
175
175
|
return (_jsx(Box, { children: renderer.render({
|
|
176
176
|
activityType: part.activityType || 'unknown',
|
|
@@ -75,7 +75,7 @@ export const EXAMPLE_ENTRIES = [
|
|
|
75
75
|
makeEntry('A2UiRestaurantExample', () => import('./A2UiRestaurantExample'), 'A2UI restaurant flow example.'),
|
|
76
76
|
makeEntry('A2UiViewerExample', () => import('./A2UiViewerExample'), 'A2UI viewer integration example.'),
|
|
77
77
|
makeEntry('A2UiJupyterOutputExample', () => import('./A2UiJupyterOutputExample'), 'One Jupyter execution shown twice: raw kernel outputs, and the A2UI surface the server converter makes of them.'),
|
|
78
|
-
makeEntry('A2UiAgentExample', () => import('./A2UiAgentExample'), 'A2UI Agent with built-in chat component and Python A2UI
|
|
78
|
+
makeEntry('A2UiAgentExample', () => import('./A2UiAgentExample'), 'A2UI Agent with built-in chat component and Python A2UI UI plugin surface.'),
|
|
79
79
|
makeEntry('AgUiAgenticExample', () => import('./AgUiAgenticExample'), 'AG-UI agentic workflow example.'),
|
|
80
80
|
makeEntry('AgUiBackendToolRenderingExample', () => import('./AgUiBackendToolRenderingExample'), 'AG-UI backend tool rendering example.'),
|
|
81
81
|
makeEntry('AgUiHaikuGenUiExample', () => import('./AgUiHaikuGenUiExample'), 'AG-UI generative UI Haiku example.'),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactorPlugin } from '@datalayer/reactor';
|
|
2
2
|
import type { AppSpec } from '../../types/agentspecs';
|
|
3
3
|
import { type LoopEmbedProps } from '../embed/LoopEmbed';
|
|
4
|
+
import type { PresenceState } from '../plugins/chat/presenceStatus';
|
|
4
5
|
/** The id of an agent or a Cog, without its version. */
|
|
5
6
|
export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string;
|
|
6
7
|
/**
|
|
@@ -30,6 +31,12 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
|
|
|
30
31
|
app: AppSpec;
|
|
31
32
|
/** What its record is kept under, on Datalayer: the application, its deployment. */
|
|
32
33
|
instance?: AppInstance;
|
|
34
|
+
/**
|
|
35
|
+
* Told what the application is doing — idle, thinking, working, waiting for
|
|
36
|
+
* you — for a host that draws its face in a frame of its own (T-08). A
|
|
37
|
+
* stable function, such as a state setter.
|
|
38
|
+
*/
|
|
39
|
+
onPresence?: (state: PresenceState) => void;
|
|
33
40
|
};
|
|
34
41
|
/**
|
|
35
42
|
* An application in a LOOP workspace: its agent, its starters, its layout.
|
|
@@ -40,5 +47,5 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
|
|
|
40
47
|
/** No host plugins: one array, so that it never reads as a change. */
|
|
41
48
|
/** The agentspec a runtime is allocated with before an application's agent is created on it. */
|
|
42
49
|
export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
|
|
43
|
-
export declare function AppRenderer({ app, plugins, instance, ...embed }: AppRendererProps): React.JSX.Element;
|
|
50
|
+
export declare function AppRenderer({ app, plugins, instance, onPresence, ...embed }: AppRendererProps): React.JSX.Element;
|
|
44
51
|
export default AppRenderer;
|
|
@@ -76,7 +76,7 @@ export function defineAppPlugin(app) {
|
|
|
76
76
|
/** The agentspec a runtime is allocated with before an application's agent is created on it. */
|
|
77
77
|
export const DATALAYER_BOOTSTRAP_AGENTSPEC = 'example-simple';
|
|
78
78
|
const NO_PLUGINS = [];
|
|
79
|
-
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, ...embed }) {
|
|
79
|
+
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, ...embed }) {
|
|
80
80
|
const chatOnly = app.interface.layout === 'chat';
|
|
81
81
|
/*
|
|
82
82
|
* The application plugin, made once per application. `LoopEmbed` rebuilds
|
|
@@ -135,6 +135,14 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, ...embed }) {
|
|
|
135
135
|
targetFixed: true,
|
|
136
136
|
// Applications first (LOOP T-12): an application's conversation wears
|
|
137
137
|
// the `loop` theme; a host may say otherwise.
|
|
138
|
-
themeVariant: "loop",
|
|
138
|
+
themeVariant: "loop",
|
|
139
|
+
// Its own face, name and welcome in the chat (T-08); no counters: a
|
|
140
|
+
// person using an application is not asking about tokens.
|
|
141
|
+
presence: {
|
|
142
|
+
name: app.name,
|
|
143
|
+
face: app.emoji,
|
|
144
|
+
welcome: app.interface.welcome || app.description,
|
|
145
|
+
onPresence,
|
|
146
|
+
}, showTokenUsage: false, datalayerCreatePayload: datalayerCreatePayload, ...embed, plugins: allPlugins }));
|
|
139
147
|
}
|
|
140
148
|
export default AppRenderer;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppSpec } from '../../types/agentspecs';
|
|
1
|
+
import type { AppSpec, ComponentSpec } from '../../types/agentspecs';
|
|
2
2
|
export declare const NOT_READY = "Not ready";
|
|
3
3
|
export declare const NEEDS_ATTENTION = "Needs attention";
|
|
4
4
|
export declare const PASSES = "Passes the instant checks";
|
|
@@ -9,6 +9,11 @@ export interface AppCheck {
|
|
|
9
9
|
attention: string[];
|
|
10
10
|
setup: string[];
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* The component a layout names (LOOP C-13), from the catalogs of the enabled
|
|
14
|
+
* UI plugins, by the name a surface gives it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const componentNamed: (name: string) => ComponentSpec | undefined;
|
|
12
17
|
/** The Guards the runtime executes (agent-runtimes `loop/apps/guards.py`). */
|
|
13
18
|
export declare const EXECUTED_GUARDS: readonly string[];
|
|
14
19
|
/** The instant checks of an application as an editor holds it. */
|
package/lib/loop/apps/checks.js
CHANGED
|
@@ -29,6 +29,7 @@ import { getCog } from '../../specs/cogs';
|
|
|
29
29
|
import { getFrame } from '../../specs/frames';
|
|
30
30
|
import { getGate } from '../../specs/gates';
|
|
31
31
|
import { GUARD_CATALOGUE } from '../../specs/guards';
|
|
32
|
+
import { getComponent } from '../../specs/uiPlugins';
|
|
32
33
|
import { MCP_SERVER_LIBRARY } from '../../specs/mcpServers';
|
|
33
34
|
import { getMemory } from '../../specs/memory';
|
|
34
35
|
import { getModel } from '../../specs/models';
|
|
@@ -47,6 +48,11 @@ const idOf = (ref) => {
|
|
|
47
48
|
const at = ref.lastIndexOf(':');
|
|
48
49
|
return at > 0 && ref.slice(at + 1).includes('.') ? ref.slice(0, at) : ref;
|
|
49
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* The component a layout names (LOOP C-13), from the catalogs of the enabled
|
|
53
|
+
* UI plugins, by the name a surface gives it.
|
|
54
|
+
*/
|
|
55
|
+
export const componentNamed = (name) => getComponent(name);
|
|
50
56
|
const own = (record, key) => Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
|
|
51
57
|
const ACTIONS = {
|
|
52
58
|
write: 'create or change things',
|
|
@@ -175,6 +181,17 @@ function referenceProblems(app) {
|
|
|
175
181
|
if (app.model && !getModel(app.model)) {
|
|
176
182
|
missing('model', app.model);
|
|
177
183
|
}
|
|
184
|
+
// The components it may use, and those its surface uses, are the catalog's (C-13).
|
|
185
|
+
for (const name of app.interface.components ?? []) {
|
|
186
|
+
if (!componentNamed(name)) {
|
|
187
|
+
problems.push(`There is no component named “${name}” in the catalog.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const node of app.interface.surface?.components ?? []) {
|
|
191
|
+
if (!componentNamed(String(node.component))) {
|
|
192
|
+
problems.push(`The surface's “${String(node.id)}” is a “${String(node.component)}”, which the catalog does not have.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
178
195
|
const judge = app.decision?.judgmentModel;
|
|
179
196
|
if (judge) {
|
|
180
197
|
const model = getModel(judge);
|
|
@@ -66,6 +66,10 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
|
|
|
66
66
|
preset.datalayerAgentSpecId,
|
|
67
67
|
preset.targetFixed,
|
|
68
68
|
preset.themeVariant,
|
|
69
|
+
preset.presence?.name,
|
|
70
|
+
preset.presence?.face,
|
|
71
|
+
preset.presence?.welcome,
|
|
72
|
+
preset.showTokenUsage,
|
|
69
73
|
// By its content: an application's spec written inline is a new object each render.
|
|
70
74
|
JSON.stringify(preset.datalayerCreatePayload ?? null),
|
|
71
75
|
preset.defaultEditor,
|
|
@@ -126,6 +130,15 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
|
|
|
126
130
|
*/
|
|
127
131
|
useReactor(reactor);
|
|
128
132
|
const shell = (_jsx(LoopWorkspace, { serverUrl: preset.serverUrl ?? '', agentId: agentId, reactor: reactor, manageReactor: false, showViewSelector: preset.showViewSelector, showHeader: showHeader, chatHeaderActions: chatHeaderActions, headerActions: headerActions, headerContainer: headerContainer }));
|
|
129
|
-
return (_jsx(QueryClientProvider, { client: internalQueryClient, children: frameTitle !== undefined ? (_jsx(WindowFrame, { title: frameTitle, height: frameHeight, children: shell })) : (
|
|
133
|
+
return (_jsx(QueryClientProvider, { client: internalQueryClient, children: frameTitle !== undefined ? (_jsx(WindowFrame, { title: frameTitle, height: frameHeight, children: shell })) : (
|
|
134
|
+
// Filling its host, whatever the host is: in a flex row it used to
|
|
135
|
+
// shrink to the chat's own width and leave the rest of the frame bare.
|
|
136
|
+
_jsx(Box, { sx: {
|
|
137
|
+
height: '100%',
|
|
138
|
+
minHeight: 0,
|
|
139
|
+
width: '100%',
|
|
140
|
+
flex: '1 1 auto',
|
|
141
|
+
minWidth: 0,
|
|
142
|
+
}, children: shell })) }));
|
|
130
143
|
}
|
|
131
144
|
export default LoopEmbed;
|
|
@@ -43,6 +43,8 @@ import { turnWritersOf } from './turnState';
|
|
|
43
43
|
import { isInactiveSurfaceContribution, orderToolContributions, toolsForChatView, } from './chatViewTools';
|
|
44
44
|
import { useAgentCommandTools } from '../../../tools/adapters/commands';
|
|
45
45
|
import { NONE_EDITOR, seedEditorChoice, setEditorOptions, } from '../shell/editorChoice';
|
|
46
|
+
import { PresenceFace, PresenceLine } from './Presence';
|
|
47
|
+
import { presenceState } from './presenceStatus';
|
|
46
48
|
/** No editor: the conversation on its own. */
|
|
47
49
|
const NO_SURFACE = '';
|
|
48
50
|
/**
|
|
@@ -186,6 +188,16 @@ export default function ChatView({ workspace }) {
|
|
|
186
188
|
*/
|
|
187
189
|
const [busy, setBusy] = useState(false);
|
|
188
190
|
const [sendReady, setSendReady] = useState(false);
|
|
191
|
+
const [presenceTool, setPresenceTool] = useState({
|
|
192
|
+
open: false,
|
|
193
|
+
pendingApproval: false,
|
|
194
|
+
});
|
|
195
|
+
const presenceNow = presenceState(busy, presenceTool);
|
|
196
|
+
const onPresence = config?.presence?.onPresence;
|
|
197
|
+
// A host drawing the face in a frame of its own is told as it changes.
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
onPresence?.(presenceNow);
|
|
200
|
+
}, [onPresence, presenceNow]);
|
|
189
201
|
const handleSendReady = useCallback((controls) => {
|
|
190
202
|
controlsRef.current = controls;
|
|
191
203
|
setSendReady(!!controls);
|
|
@@ -199,6 +211,22 @@ export default function ChatView({ workspace }) {
|
|
|
199
211
|
* Cleared when nothing is running.
|
|
200
212
|
*/
|
|
201
213
|
const handleDisplayItemsChange = useCallback((items) => {
|
|
214
|
+
// The newest tool call, for an application's line of status (T-08).
|
|
215
|
+
let newest;
|
|
216
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
217
|
+
const item = items[index];
|
|
218
|
+
if (typeof item.toolName === 'string' && item.toolCallId) {
|
|
219
|
+
newest = item;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const open = !!newest && newest.status !== 'complete' && newest.status !== 'error';
|
|
224
|
+
const pendingApproval = open &&
|
|
225
|
+
newest?.result
|
|
226
|
+
?.pending_approval === true;
|
|
227
|
+
setPresenceTool(previous => previous.open === open && previous.pendingApproval === pendingApproval
|
|
228
|
+
? previous
|
|
229
|
+
: { open, pendingApproval });
|
|
202
230
|
const feed = turnFeedRef.current;
|
|
203
231
|
if (!feed) {
|
|
204
232
|
return;
|
|
@@ -722,6 +750,7 @@ export default function ChatView({ workspace }) {
|
|
|
722
750
|
})), [suggestions]);
|
|
723
751
|
/* The icon its spec asked for, at the size the empty state draws. */
|
|
724
752
|
const BrandIcon = agentIcon(spec?.icon);
|
|
753
|
+
const presence = config?.presence;
|
|
725
754
|
/* And the team's, for the level above it. */
|
|
726
755
|
const TeamIcon = agentIcon(team?.team.icon);
|
|
727
756
|
/*
|
|
@@ -1290,7 +1319,7 @@ export default function ChatView({ workspace }) {
|
|
|
1290
1319
|
onSelectAgent: id => team?.select(id),
|
|
1291
1320
|
// On unless the host said otherwise: a public page turns the counters
|
|
1292
1321
|
// off, because they answer questions a visitor is not asking.
|
|
1293
|
-
showTokenUsage: chatExtras.showTokenUsage ?? true,
|
|
1322
|
+
showTokenUsage: chatExtras.showTokenUsage ?? config?.showTokenUsage ?? true,
|
|
1294
1323
|
showContextRing: true,
|
|
1295
1324
|
agentUsage: contextUsage ?? undefined,
|
|
1296
1325
|
showModelSelector: true,
|
|
@@ -1423,7 +1452,7 @@ export default function ChatView({ workspace }) {
|
|
|
1423
1452
|
nothing about this one. The empty state is the first thing a
|
|
1424
1453
|
person sees and the only place the agent introduces itself.
|
|
1425
1454
|
*/
|
|
1426
|
-
title: member?.name ?? spec?.name ?? agentId, description: spec?.description,
|
|
1455
|
+
title: presence?.name ?? member?.name ?? spec?.name ?? agentId, description: presence?.welcome ?? spec?.description,
|
|
1427
1456
|
/*
|
|
1428
1457
|
Two sizes, because it is drawn in two places.
|
|
1429
1458
|
|
|
@@ -1435,13 +1464,18 @@ export default function ChatView({ workspace }) {
|
|
|
1435
1464
|
// Big enough to read as the agent's mark rather than as
|
|
1436
1465
|
// punctuation before its name, and still short enough not to
|
|
1437
1466
|
// set the header's height.
|
|
1438
|
-
brandIcon: _jsx(
|
|
1439
|
-
|
|
1467
|
+
brandIcon: presence?.face ? (_jsx(PresenceFace, { face: presence.face, size: 20, state: presenceNow })) : (_jsx(BrandIcon, { size: 20 })),
|
|
1468
|
+
// An application says what it is doing, in plain words, beside
|
|
1469
|
+
// its name (T-08); an agent's chat keeps its header as it is.
|
|
1470
|
+
headerContent: presence ? _jsx(PresenceLine, { state: presenceNow }) : undefined, emptyState: {
|
|
1471
|
+
// An application's own face, large: the thing a person's eye
|
|
1472
|
+
// lands on first (LOOP T-08).
|
|
1473
|
+
icon: presence?.face ? (_jsx("span", { "aria-hidden": true, style: { fontSize: 48, lineHeight: 1 }, children: presence.face })) : (_jsx(BrandIcon, { size: 48 })),
|
|
1440
1474
|
// `ChatEmptyState` reads its heading from here and nowhere
|
|
1441
1475
|
// else — the `title` above reaches the header only — so
|
|
1442
1476
|
// without this the agent introduced itself as "Start a
|
|
1443
1477
|
// conversation".
|
|
1444
|
-
title: member?.name ?? spec?.name ?? agentId,
|
|
1478
|
+
title: presence?.name ?? member?.name ?? spec?.name ?? agentId,
|
|
1445
1479
|
/*
|
|
1446
1480
|
Two levels when there is a team: the team first — its name,
|
|
1447
1481
|
what it is for, what it can be asked — and under it the
|
|
@@ -1600,7 +1634,7 @@ export default function ChatView({ workspace }) {
|
|
|
1600
1634
|
// A host example's own tool-result renderer, when it feeds one
|
|
1601
1635
|
// through the extras channel — the A2UI examples draw their
|
|
1602
1636
|
// surface here. Wins over the notebook surfaces in ChatBase.
|
|
1603
|
-
renderToolResult: chatExtras.renderToolResult, showTurnFooter: chatExtras.showTokenUsage ?? true, onContextSnapshot: handleContextSnapshot, onLoadingChange: handleLoadingChange, onItemsChange: handleMessagesChange, onDisplayItemsChange: handleDisplayItemsChange, enableStreaming: true }) }), besideChat && !promptHidden ? prompt : null] }) }));
|
|
1637
|
+
renderToolResult: chatExtras.renderToolResult, showTurnFooter: chatExtras.showTokenUsage ?? config?.showTokenUsage ?? true, onContextSnapshot: handleContextSnapshot, onLoadingChange: handleLoadingChange, onItemsChange: handleMessagesChange, onDisplayItemsChange: handleDisplayItemsChange, enableStreaming: true }) }), besideChat && !promptHidden ? prompt : null] }) }));
|
|
1604
1638
|
// Off when the host asked for the menu alone: the composer's suggestions
|
|
1605
1639
|
// control carries the same openers (`suggestions` above).
|
|
1606
1640
|
const chipsEl = config?.suggestionLabels !== false && chatSuggestions.length > 0 ? (_jsx(Box, { "aria-label": "Suggested prompts", sx: { display: 'flex', flexWrap: 'wrap', gap: 2, px: 2, pb: '6px' }, children: chatSuggestions.map(item => (_jsx(Box, { as: "button", type: "button",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PresenceState } from './presenceStatus';
|
|
2
|
+
export declare function PresenceFace({ face, size, state, }: {
|
|
3
|
+
face: string;
|
|
4
|
+
size: number;
|
|
5
|
+
state: PresenceState;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function PresenceLine({ state }: {
|
|
8
|
+
state: PresenceState;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* An application's presence in its chat (LOOP T-08): its face, with a quiet
|
|
8
|
+
* motion around it while it thinks or works, and one line saying what it is
|
|
9
|
+
* doing. The face is never redrawn; the motion is a soft ring in the accent,
|
|
10
|
+
* still under `prefers-reduced-motion`, and the line changes at the theme's
|
|
11
|
+
* status pace (T-10).
|
|
12
|
+
*
|
|
13
|
+
* @module loop/plugins/chat/Presence
|
|
14
|
+
*/
|
|
15
|
+
import { Box, Text } from '@primer/react';
|
|
16
|
+
import { PRESENCE_LINES } from './presenceStatus';
|
|
17
|
+
const ACCENT = 'var(--loop-accent, var(--fgColor-accent))';
|
|
18
|
+
export function PresenceFace({ face, size, state, }) {
|
|
19
|
+
const active = state === 'thinking' || state === 'working';
|
|
20
|
+
return (_jsxs(Box, { as: "span", "aria-hidden": true, "data-presence": state, sx: {
|
|
21
|
+
position: 'relative',
|
|
22
|
+
display: 'inline-flex',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
width: size + 8,
|
|
26
|
+
height: size + 8,
|
|
27
|
+
flexShrink: 0,
|
|
28
|
+
fontSize: size,
|
|
29
|
+
lineHeight: 1,
|
|
30
|
+
}, children: [_jsx(Box, { as: "span", sx: {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
inset: 0,
|
|
33
|
+
borderRadius: '50%',
|
|
34
|
+
boxShadow: `0 0 0 2px ${ACCENT}`,
|
|
35
|
+
opacity: active ? 0.9 : state === 'waiting' ? 0.9 : 0,
|
|
36
|
+
transition: 'opacity var(--theme-motion-status, 0ms) var(--theme-motion-easing, ease)',
|
|
37
|
+
animation: active
|
|
38
|
+
? 'loopPresenceBreath 1.8s ease-in-out infinite'
|
|
39
|
+
: 'none',
|
|
40
|
+
'@keyframes loopPresenceBreath': {
|
|
41
|
+
'0%, 100%': { transform: 'scale(0.92)', opacity: 0.35 },
|
|
42
|
+
'50%': { transform: 'scale(1.06)', opacity: 0.9 },
|
|
43
|
+
},
|
|
44
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
45
|
+
animation: 'none',
|
|
46
|
+
},
|
|
47
|
+
} }), face] }));
|
|
48
|
+
}
|
|
49
|
+
export function PresenceLine({ state }) {
|
|
50
|
+
return (_jsx(Text, { role: "status", "aria-live": "polite", "data-presence-line": state, sx: {
|
|
51
|
+
fontSize: 0,
|
|
52
|
+
color: state === 'waiting' ? 'fg.default' : 'fg.muted',
|
|
53
|
+
fontWeight: state === 'waiting' ? 'semibold' : 'normal',
|
|
54
|
+
whiteSpace: 'nowrap',
|
|
55
|
+
flexShrink: 0,
|
|
56
|
+
}, children: PRESENCE_LINES[state] }));
|
|
57
|
+
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import type { PresenceState } from './presenceStatus';
|
|
1
2
|
/** What a host may set on the chat. */
|
|
3
|
+
/** Who is answering, as the person meets them. */
|
|
4
|
+
export interface ChatPresence {
|
|
5
|
+
name: string;
|
|
6
|
+
/** An emoji: its face where only an icon goes. */
|
|
7
|
+
face?: string;
|
|
8
|
+
/** What it says before anyone writes. */
|
|
9
|
+
welcome?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Told what it is doing as it changes — idle, thinking, working, waiting
|
|
12
|
+
* for you — for a host that draws the face itself, in a frame of its own.
|
|
13
|
+
* Read once, when the workspace is built: pass a stable function, such as
|
|
14
|
+
* a state setter.
|
|
15
|
+
*/
|
|
16
|
+
onPresence?: (state: PresenceState) => void;
|
|
17
|
+
}
|
|
2
18
|
export type ChatPluginConfig = {
|
|
3
19
|
/**
|
|
4
20
|
* The theme the conversation wears, by name: the person's unless said. An
|
|
@@ -6,6 +22,17 @@ export type ChatPluginConfig = {
|
|
|
6
22
|
* the theme's bubbles, pills and colours whatever the page around it wears.
|
|
7
23
|
*/
|
|
8
24
|
themeVariant?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Who is answering, as the person meets them (LOOP T-08): a name, a face
|
|
27
|
+
* and a welcome, in place of the agent's id and a generic icon. An
|
|
28
|
+
* application sets its own.
|
|
29
|
+
*/
|
|
30
|
+
presence?: ChatPresence;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the counters — tokens, context — are shown under the prompt.
|
|
33
|
+
* On unless said: a person using an application is not asking.
|
|
34
|
+
*/
|
|
35
|
+
showTokenUsage?: boolean;
|
|
9
36
|
/**
|
|
10
37
|
* What the empty prompt says.
|
|
11
38
|
*
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an application is doing, in plain words (LOOP T-08): the one line of
|
|
3
|
+
* status beside its face.
|
|
4
|
+
*
|
|
5
|
+
* @module loop/plugins/chat/presenceStatus
|
|
6
|
+
*/
|
|
7
|
+
/** The states a person is told about. Paused waits for a pause to exist. */
|
|
8
|
+
export type PresenceState = 'idle' | 'thinking' | 'working' | 'waiting';
|
|
9
|
+
/** The line each state is said by. */
|
|
10
|
+
export declare const PRESENCE_LINES: Record<PresenceState, string>;
|
|
11
|
+
/** The newest tool call of the conversation, as far as presence reads it. */
|
|
12
|
+
export interface PresenceTool {
|
|
13
|
+
/** It has not returned. */
|
|
14
|
+
open: boolean;
|
|
15
|
+
/** It waits on a person's approval. */
|
|
16
|
+
pendingApproval: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The state, from whether a turn is running and the newest tool call.
|
|
20
|
+
*
|
|
21
|
+
* Waiting for an approval is said whether or not the turn is still
|
|
22
|
+
* streaming — the stream may end while the approval is asked. A tool that
|
|
23
|
+
* has not returned counts as work only during a turn: a stopped turn can
|
|
24
|
+
* leave one open, and the application is not working then.
|
|
25
|
+
*/
|
|
26
|
+
export declare function presenceState(busy: boolean, tool?: PresenceTool): PresenceState;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The line each state is said by. */
|
|
6
|
+
export const PRESENCE_LINES = {
|
|
7
|
+
idle: 'Ready',
|
|
8
|
+
thinking: 'Thinking…',
|
|
9
|
+
working: 'Working…',
|
|
10
|
+
waiting: 'Waiting for you',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The state, from whether a turn is running and the newest tool call.
|
|
14
|
+
*
|
|
15
|
+
* Waiting for an approval is said whether or not the turn is still
|
|
16
|
+
* streaming — the stream may end while the approval is asked. A tool that
|
|
17
|
+
* has not returned counts as work only during a turn: a stopped turn can
|
|
18
|
+
* leave one open, and the application is not working then.
|
|
19
|
+
*/
|
|
20
|
+
export function presenceState(busy, tool) {
|
|
21
|
+
if (tool?.open && tool.pendingApproval) {
|
|
22
|
+
return 'waiting';
|
|
23
|
+
}
|
|
24
|
+
if (!busy) {
|
|
25
|
+
return 'idle';
|
|
26
|
+
}
|
|
27
|
+
return tool?.open ? 'working' : 'thinking';
|
|
28
|
+
}
|
package/lib/loop/presets.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { type PluginRef } from '@datalayer/reactor';
|
|
23
23
|
import { type AgentsConfig, type SandboxTarget } from './plugins/agents';
|
|
24
|
-
import { type ChatPluginConfig } from './plugins/chat';
|
|
24
|
+
import { type ChatPluginConfig, type ChatPresence } from './plugins/chat';
|
|
25
25
|
import { type PageSize } from './plugins/page-layout';
|
|
26
26
|
export type LoopPresetOptions = {
|
|
27
27
|
/** Where the agent runtimes service is. */
|
|
@@ -128,6 +128,10 @@ export type LoopPresetOptions = {
|
|
|
128
128
|
targetFixed?: boolean;
|
|
129
129
|
/** The theme the conversation wears, by name; the person's unless said. */
|
|
130
130
|
themeVariant?: string;
|
|
131
|
+
/** Who is answering, as the person meets them (LOOP T-08). */
|
|
132
|
+
presence?: ChatPresence;
|
|
133
|
+
/** Whether the counters are shown under the prompt; on unless said. */
|
|
134
|
+
showTokenUsage?: boolean;
|
|
131
135
|
/**
|
|
132
136
|
* The agentspec the local agent is created from, by id.
|
|
133
137
|
*
|
package/lib/loop/presets.js
CHANGED
|
@@ -31,7 +31,7 @@ import { ShellPlugin } from './plugins/shell';
|
|
|
31
31
|
import { PromptPlugin } from './plugins/prompt';
|
|
32
32
|
import { AgentspecsPlugin } from './plugins/agentspecs';
|
|
33
33
|
import { AgentsPlugin, } from './plugins/agents';
|
|
34
|
-
import { ChatPlugin } from './plugins/chat';
|
|
34
|
+
import { ChatPlugin, } from './plugins/chat';
|
|
35
35
|
import { ChatHeaderPlugin } from './plugins/chat-header';
|
|
36
36
|
import { A2uiSurfacePlugin } from './plugins/a2ui-surface';
|
|
37
37
|
import { ChatViewPlugin } from './plugins/chat-view';
|
|
@@ -52,7 +52,7 @@ import { LoopPageLayoutPlugin } from './plugins/page-layout';
|
|
|
52
52
|
* be installed or signed into, and the switches a demonstration wants are off.
|
|
53
53
|
*/
|
|
54
54
|
export function loopPlugins(options = {}) {
|
|
55
|
-
const { serverUrl, target = 'browser', defaultEditor = 'notebook', showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, targetFixed, themeVariant, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
|
|
55
|
+
const { serverUrl, target = 'browser', defaultEditor = 'notebook', showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, targetFixed, themeVariant, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
|
|
56
56
|
return [
|
|
57
57
|
// The chat owns the editor beside it, so which one opens is its
|
|
58
58
|
// configuration rather than the workspace's.
|
|
@@ -69,6 +69,8 @@ export function loopPlugins(options = {}) {
|
|
|
69
69
|
suggestionLabels,
|
|
70
70
|
fullScreenTopOffset,
|
|
71
71
|
themeVariant,
|
|
72
|
+
presence,
|
|
73
|
+
showTokenUsage,
|
|
72
74
|
}),
|
|
73
75
|
// The composer and the title bar are plugins of their own: the chat
|
|
74
76
|
// assembles their props, these render them. In the preset by default —
|
|
@@ -8,7 +8,7 @@ import { BaseProtocolAdapter } from './BaseProtocolAdapter';
|
|
|
8
8
|
export interface A2AAdapterConfig extends ProtocolAdapterConfig {
|
|
9
9
|
/** Agent URL for .well-known/agent-card.json discovery */
|
|
10
10
|
agentUrl?: string;
|
|
11
|
-
/** Enable A2UI
|
|
11
|
+
/** Enable the A2UI UI plugin */
|
|
12
12
|
enableA2UI?: boolean;
|
|
13
13
|
}
|
|
14
14
|
/**
|
package/lib/specs/uiPlugins.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This file is AUTO-GENERATED from YAML specifications.
|
|
7
7
|
* DO NOT EDIT MANUALLY - run 'make specs' to regenerate.
|
|
8
8
|
*/
|
|
9
|
-
import type { UIPluginSpec } from '../types/agentspecs';
|
|
9
|
+
import type { ComponentSpec, UIPluginSpec } from '../types/agentspecs';
|
|
10
10
|
export declare const A2UI_UI_PLUGIN_0_0_1: UIPluginSpec;
|
|
11
11
|
export declare const MCP_APPS_UI_PLUGIN_0_0_1: UIPluginSpec;
|
|
12
12
|
export declare const MCP_UI_UI_PLUGIN_0_0_1: UIPluginSpec;
|
|
@@ -14,3 +14,8 @@ export declare const UI_PLUGIN_CATALOGUE: Record<string, UIPluginSpec>;
|
|
|
14
14
|
/** The plugin an agent spec's `uiPlugin` names, or undefined. */
|
|
15
15
|
export declare function getUIPlugin(pluginId: string): UIPluginSpec | undefined;
|
|
16
16
|
export declare function listUIPlugins(): UIPluginSpec[];
|
|
17
|
+
/** The visual components the enabled UI plugins render, by the name a surface gives them (LOOP C-13). */
|
|
18
|
+
export declare const COMPONENT_CATALOGUE: Record<string, ComponentSpec>;
|
|
19
|
+
/** The component a layout names, or undefined. */
|
|
20
|
+
export declare function getComponent(name: string): ComponentSpec | undefined;
|
|
21
|
+
export declare function listComponents(): ComponentSpec[];
|