@datalayer/agent-runtimes 1.3.39 → 1.3.42

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.
@@ -22,10 +22,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
22
  * the host's own plugins (T-24); an id nothing contributes is said, never
23
23
  * replaced.
24
24
  *
25
- * The three floating modes are `ChatFloating`'s, given the application's
26
- * name, welcome, starters and accent, and its agent's endpoint — created
27
- * with the application's spec in its payload, so the runtime enforces its
28
- * rules there as it does inline.
25
+ * The three floating modes are `ChatFloating`'s chrome around the same
26
+ * `AppRenderer` (R-01): the application's preset per kind and its layout in
27
+ * the window, its agent created with its spec in its payload and spoken to
28
+ * at its session endpoint (R-04), so the runtime enforces its rules there as
29
+ * it does inline; the chrome told by the workspace what it is doing and
30
+ * what it last said, for the balloon and the blink.
29
31
  *
30
32
  * In the `loop` theme with the application's accent (T-12, T-05), which the
31
33
  * host may override with the face and the mode (D-11) — around the
@@ -34,14 +36,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
34
36
  * @module loop/embed/AppEmbed
35
37
  */
36
38
  import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
37
- import { registerPortalRoot } from '@primer/react';
38
- import { DatalayerThemeProvider, loopTheme } from '@datalayer/primer-addons';
39
+ import { Text, registerPortalRoot } from '@primer/react';
39
40
  import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
41
+ import { FluentEmoji } from '@datalayer/core/lib/components/emoji';
42
+ import { DatalayerThemeProvider, loopTheme } from '@datalayer/primer-addons';
40
43
  import { ChatFloating } from '../../chat/ChatFloating';
41
- import { useAgentRuntimes } from '../../hooks/useAgentRuntimes';
42
- import { AppRenderer, DATALAYER_BOOTSTRAP_AGENTSPEC, appDatalayerCreatePayload, defineAppPlugin, } from '../apps/AppRenderer';
43
- import { LoopAgentBlueprint } from '../core';
44
- import { ensureServerAgent } from '../plugins/agents/switchable';
44
+ import { AppRenderer } from '../apps/AppRenderer';
45
45
  import { AssistantCharactersPlugin, assistantCharacterFor, assistantCharactersFrom, } from '../plugins/assistant-characters';
46
46
  import { floatingViewOf } from './embedConfig';
47
47
  import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
@@ -56,116 +56,43 @@ const NO_PLUGINS = [];
56
56
  export function embedAssistantCharacter(app, plugins = NO_PLUGINS) {
57
57
  return assistantCharacterFor(assistantCharactersFrom([AssistantCharactersPlugin, ...plugins]), { app: app.interface.assistant });
58
58
  }
59
- /** What the application's agent is created with on an agent-runtimes server. */
60
- function serverCreatePayload(app) {
61
- const blueprint = (defineAppPlugin(app).contributes ?? []).find((item) => item.point === LoopAgentBlueprint);
62
- return blueprint?.value.createPayload ?? { name: app.id };
63
- }
64
- /**
65
- * The application's agent on the host's agent-runtimes server: created
66
- * there with the application's spec unless it is there already, as the
67
- * workspace's Local target does.
68
- */
69
- function useServerAgent(app, serverUrl) {
70
- const [state, setState] = useState({
71
- waiting: 'Starting…',
72
- });
73
- const source = JSON.stringify(app);
74
- useEffect(() => {
75
- let cancelled = false;
76
- const base = serverUrl.replace(/\/+$/, '');
77
- setState({ waiting: 'Starting…' });
78
- ensureServerAgent(base, app.id, serverCreatePayload(app)).then(() => {
79
- if (!cancelled) {
80
- setState({
81
- protocol: {
82
- type: 'ag-ui',
83
- // Its session API: each thread a session (LOOP R-04).
84
- endpoint: `${base}/api/v1/apps/agents/${encodeURIComponent(app.id)}/ag-ui/`,
85
- agentId: app.id,
86
- },
87
- });
88
- }
89
- }, (error) => {
90
- if (!cancelled) {
91
- setState({
92
- error: error instanceof Error ? error.message : String(error),
93
- });
94
- }
95
- });
96
- return () => {
97
- cancelled = true;
98
- };
99
- // eslint-disable-next-line react-hooks/exhaustive-deps
100
- }, [serverUrl, source]);
101
- return state;
59
+ /** What a floating application says to a visitor Datalayer does not know. */
60
+ export const signedOutSentence = (app) => `${app.name} runs on Datalayer, which needs you signed in. An application embedded for visitors who are not needs its embed token (LOOP D-14), or an agent-runtimes server of the host's own (the "server" attribute).`;
61
+ /** The application's face, as the floating window's button and header draw it. */
62
+ function Face({ emoji }) {
63
+ // In Fluent Emoji, at a line's size (T-19, T-20).
64
+ return _jsx(FluentEmoji, { emoji: emoji, size: 20, label: "" });
102
65
  }
103
66
  /**
104
- * The application's agent on a Datalayer runtime: allocated with a plain
105
- * agentspec, the agent created there with the application's spec — the
106
- * runtime `AppRenderer` launches inline (R-03, R-05).
67
+ * The bubble, the panel and the assistant: `ChatFloating`'s chrome in that
68
+ * mode — the button, its blink and its balloon, the panel at the edge, the
69
+ * character dragged about and sent away — holding the application as
70
+ * `AppRenderer` draws it inline (R-01): its kind's preset, its layout, its
71
+ * agent created with its spec and spoken to at its session endpoint (R-04).
72
+ * The workspace tells the chrome what the application is doing and what it
73
+ * last said (`onPresence`, `onSaying`).
107
74
  */
108
- function useDatalayerAgent(app, instance) {
109
- const token = useIAMStore(state => state.token);
110
- const source = JSON.stringify(app);
111
- const agentConfig = useMemo(() => ({
112
- name: app.id,
113
- protocol: 'ag-ui',
114
- agentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
115
- createPayload: appDatalayerCreatePayload(app, instance),
116
- }),
117
- // eslint-disable-next-line react-hooks/exhaustive-deps
118
- [source, instance?.appUid, instance?.deploymentUid, instance?.version]);
119
- const { runtime, error } = useAgentRuntimes({
120
- agentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
121
- agentConfig,
122
- variant: 'cloud-pydanticai',
123
- // Only for a visitor Datalayer knows: a runtime is somebody's to pay for.
124
- autoStart: Boolean(token),
125
- autoCreateAgent: Boolean(token),
126
- });
127
- if (!token) {
128
- return {
129
- error: `${app.name} runs on Datalayer, which needs you signed in. An application embedded for visitors who are not needs its embed token (LOOP D-14), or an agent-runtimes server of the host's own (the "server" attribute).`,
130
- };
131
- }
132
- if (error) {
133
- return { error };
134
- }
135
- if (!runtime?.agentBaseUrl) {
136
- return { waiting: 'Starting…' };
137
- }
138
- return {
139
- protocol: {
140
- type: 'ag-ui',
141
- // Its session API: each thread a session (LOOP R-04).
142
- endpoint: `${runtime.agentBaseUrl}/api/v1/apps/agents/${encodeURIComponent(app.id)}/ag-ui/`,
143
- agentId: app.id,
144
- authToken: token,
145
- },
146
- };
147
- }
148
- /** The application's face, as the floating chat's button and header draw it. */
149
- function Face({ emoji }) {
150
- return (_jsx("span", { "aria-hidden": true, style: { fontSize: 18, lineHeight: 1 }, children: emoji }));
151
- }
152
- /** The bubble, the panel and the assistant: `ChatFloating` in that mode. */
153
- function FloatingChat({ app, view, colorMode, themeOverrides, character, agent, }) {
75
+ function FloatingApp({ app, view, colorMode, themeOverrides, character, serverUrl, instance, }) {
76
+ const [presence, setPresence] = useState('idle');
77
+ const [said, setSaid] = useState({ answering: false });
154
78
  const welcome = app.interface.welcome || app.description;
155
- return (_jsx(ChatFloating
156
- // A new agent is a new conversation.
157
- , { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), protocol: agent.protocol, authToken: agent.protocol?.authToken, title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, suggestions: app.interface.starters.map(starter => ({
158
- title: starter.label,
159
- message: starter.message,
160
- })), themeVariant: "loop", themeOverrides: themeOverrides, colorMode: colorMode, showTokenUsage: false, showSettingsButton: false, showPoweredBy: false, useStore: false, launching: !agent.protocol && !agent.error, launchingMessage: agent.waiting, disabled: Boolean(agent.error), disableReason: agent.error }, agent.protocol?.endpoint ?? 'waiting'));
161
- }
162
- /** Floating, its agent on the host's agent-runtimes server. */
163
- function ServerFloating({ serverUrl, ...props }) {
164
- return (_jsx(FloatingChat, { ...props, agent: useServerAgent(props.app, serverUrl) }));
165
- }
166
- /** Floating, its agent on a Datalayer runtime. */
167
- function DatalayerFloating({ instance, ...props }) {
168
- return (_jsx(FloatingChat, { ...props, agent: useDatalayerAgent(props.app, instance) }));
79
+ // A runtime is somebody's to pay for: on Datalayer, only for a visitor
80
+ // Datalayer knows — said, and nothing launched, for one it does not.
81
+ const token = useIAMStore(state => state.token);
82
+ const signedOut = !serverUrl && !token;
83
+ return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
84
+ body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance,
85
+ // The host's accent and face over the application's own, inside
86
+ // its conversation too, in the embed's mode.
87
+ themeOverrides: themeOverrides, colorMode: colorMode,
88
+ // The window draws its face, its name and close.
89
+ hideChatHeader: true,
90
+ // Mounted closed: the caret goes in when the window opens.
91
+ autoFocusPrompt: false, onPresence: setPresence, onSaying: setSaid })),
92
+ presence,
93
+ saying: said.saying,
94
+ answering: said.answering,
95
+ } }));
169
96
  }
170
97
  /**
171
98
  * Overlays — menus, dialogs — drawn inside the embed's own root, under the
@@ -228,9 +155,10 @@ export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUr
228
155
  return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, style: view
229
156
  ? // Floating: nothing in the page's flow, nothing painted behind.
230
157
  { backgroundColor: 'transparent' }
231
- : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view && serverUrl ? (_jsx(ServerFloating, { app: app, view: view, serverUrl: serverUrl, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : view ? (_jsx(DatalayerFloating, { app: app, view: view, instance: instance, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance,
158
+ : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(FloatingApp, { app: app, view: view, serverUrl: serverUrl, instance: instance, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance,
232
159
  // The host's accent and face over the application's own, inside
233
- // its conversation too.
234
- themeOverrides: themeOverrides }))] }));
160
+ // its conversation too, in the embed's mode: the host's or its
161
+ // visitor's system's, not a Datalayer setting.
162
+ themeOverrides: themeOverrides, colorMode: resolvedMode }))] }));
235
163
  }
236
164
  export default AppEmbed;
@@ -68,6 +68,7 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
68
68
  preset.themeVariant,
69
69
  // By its content, as the payload below.
70
70
  JSON.stringify(preset.themeOverrides ?? null),
71
+ preset.colorMode,
71
72
  preset.presence?.name,
72
73
  preset.presence?.face,
73
74
  preset.presence?.welcome,
@@ -51,6 +51,8 @@ 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
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';
54
+ export { APP_RULES_PLUGIN_NAME, APP_RULES_WORDS, AppRulesCard, defineAppRulesPlugin, } from './plugins/app-rules';
55
+ export { APP_ACTIVITY_PLUGIN_NAME, APP_ACTIVITY_WORDS, AppActivity, defineAppActivityPlugin, } from './plugins/app-activity';
54
56
  export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
55
57
  export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
56
58
  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';
package/lib/loop/index.js CHANGED
@@ -64,6 +64,8 @@ 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
66
  export { APP_KIND_PATHS, APP_PAGE_PLUGIN_NAME, APP_PAGE_SURFACE, AppPage, appKindPaths, appPageAction, appPageData, defineAppPagePlugin, hasAppPage, surfaceUnshown, } from './plugins/app-page';
67
+ export { APP_RULES_PLUGIN_NAME, APP_RULES_WORDS, AppRulesCard, defineAppRulesPlugin, } from './plugins/app-rules';
68
+ export { APP_ACTIVITY_PLUGIN_NAME, APP_ACTIVITY_WORDS, AppActivity, defineAppActivityPlugin, } from './plugins/app-activity';
67
69
  export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
68
70
  export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
69
71
  export { AGENTS_PLUGIN_NAME, AgentsPlugin, SandboxSelector, browserSource, createBrowserSandboxService, createServerSandboxService, createSwitchableSandboxService, suppliedSource, useOptionalSandboxService, useSandboxService, } from './plugins/agents';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * What an application did, beside its page (LOOP R-01b, R-15, U-20): its
3
+ * sessions, newest first, each in a sentence, opened on what it did step by
4
+ * step — tool calls, the rules' decisions, the checks that stopped a step,
5
+ * the approvals, the answer. Read from the record ai-agents keeps (R-07),
6
+ * again each time a turn of the conversation ends.
7
+ *
8
+ * @module loop/plugins/app-activity/AppActivity
9
+ */
10
+ import type { JSX } from 'react';
11
+ /** What the feed says. */
12
+ export declare const APP_ACTIVITY_WORDS: {
13
+ readonly title: "Activity";
14
+ readonly unsaved: "Recorded once it is saved on Datalayer: each session there — its address, or its Preview on Datalayer — is listed here.";
15
+ readonly signedOut: "Its activity is its owner’s: sign in to Datalayer to read it.";
16
+ readonly none: "Nothing yet: a session on Datalayer — its address, or the Preview on Datalayer — is recorded here.";
17
+ };
18
+ export declare function AppActivity({ appUid }: {
19
+ appUid?: string;
20
+ }): JSX.Element;
21
+ export default AppActivity;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Heading, Label, RelativeTime, Spinner, Text } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { signal } from '@datalayer/reactor';
6
+ import { useContributions, useSignalValue } from '@datalayer/reactor/react';
7
+ import { useCoreStore } from '@datalayer/core/lib/state';
8
+ import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
9
+ import { LoopChatTurn } from '../../core';
10
+ import { RECORD_KIND_WORDS, listSessions, readSession, sessionSentence, } from '../../apps/records';
11
+ /** What the feed says. */
12
+ export const APP_ACTIVITY_WORDS = {
13
+ title: 'Activity',
14
+ unsaved: 'Recorded once it is saved on Datalayer: each session there — its address, or its Preview on Datalayer — is listed here.',
15
+ signedOut: 'Its activity is its owner’s: sign in to Datalayer to read it.',
16
+ none: 'Nothing yet: a session on Datalayer — its address, or the Preview on Datalayer — is recorded here.',
17
+ };
18
+ const NO_TURN = signal({ id: 0, status: 'idle' });
19
+ /** Read `load` while `when`, again whenever `key` changes. */
20
+ function useRead(load, key, when = true) {
21
+ const [state, setState] = useState({ loading: when });
22
+ useEffect(() => {
23
+ if (!when) {
24
+ return undefined;
25
+ }
26
+ let cancelled = false;
27
+ setState(previous => ({ ...previous, loading: true }));
28
+ load().then(value => !cancelled && setState({ value, loading: false }), (error) => !cancelled &&
29
+ setState({
30
+ error: error instanceof Error ? error.message : String(error),
31
+ loading: false,
32
+ }));
33
+ return () => {
34
+ cancelled = true;
35
+ };
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, [key, when]);
38
+ return state;
39
+ }
40
+ function Session({ context, session, }) {
41
+ const [open, setOpen] = useState(false);
42
+ const entries = useRead(() => readSession(context, session.sessionUid), `${session.sessionUid}:${context.token}`, open);
43
+ return (_jsxs(Box, { as: "details", onToggle: (event) => setOpen(event.currentTarget.open), sx: { borderTop: '1px solid', borderColor: 'border.muted', py: 2 }, children: [_jsxs(Box, { as: "summary", sx: { cursor: 'pointer', fontSize: 1 }, children: [session.createdAt ? (_jsx(RelativeTime, { datetime: session.createdAt })) : ('A session'), session.version ? ` · version ${session.version}` : '', entries.value ? ` · ${sessionSentence(entries.value)}` : ''] }), !open ? null : entries.loading ? (_jsx(Spinner, { size: "small" })) : entries.error ? (_jsx(Text, { role: "alert", sx: { fontSize: 1 }, children: entries.error })) : (_jsx(Box, { as: "ol", sx: { pl: 3, mt: 2, mb: 0, fontSize: 1 }, children: (entries.value ?? []).map(entry => (_jsxs(Box, { as: "li", sx: { mb: 1 }, children: [_jsx(Label, { sx: { mr: 2 }, children: RECORD_KIND_WORDS[entry.kind] ?? entry.kind }), entry.summary] }, entry.uid))) }))] }));
44
+ }
45
+ export function AppActivity({ appUid }) {
46
+ const token = useIAMStore(state => state.token) ?? '';
47
+ const aiAgentsUrl = useCoreStore((state) => state.configuration)?.aiAgentsUrl ?? '';
48
+ // Read again as each turn ends: what it just did is in the record then.
49
+ const turnEntries = useContributions(LoopChatTurn);
50
+ const turn = useSignalValue(turnEntries[0]?.value.turn ?? NO_TURN);
51
+ const settled = turn.status !== 'thinking' && turn.status !== 'streaming';
52
+ const [ended, setEnded] = useState(0);
53
+ useEffect(() => {
54
+ if (settled) {
55
+ setEnded(turn.id);
56
+ }
57
+ }, [settled, turn.id]);
58
+ const context = { aiAgentsUrl, token };
59
+ const readable = Boolean(appUid && token);
60
+ const sessions = useRead(() => listSessions(context, appUid ?? ''), `${appUid}:${token}:${aiAgentsUrl}:${ended}`, readable);
61
+ return (_jsxs(Box, { "data-testid": "app-activity", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: APP_ACTIVITY_WORDS.title }), !appUid ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: APP_ACTIVITY_WORDS.unsaved })) : !token ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: APP_ACTIVITY_WORDS.signedOut })) : sessions.loading && !sessions.value ? (_jsx(Spinner, { size: "small" })) : sessions.error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: sessions.error })) : (sessions.value ?? []).length === 0 ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: APP_ACTIVITY_WORDS.none })) : ((sessions.value ?? []).map(session => (_jsx(Session, { context: context, session: session }, session.uid))))] }));
62
+ }
63
+ export default AppActivity;
@@ -0,0 +1,11 @@
1
+ import { type ReactorPlugin } from '@datalayer/reactor';
2
+ import type { AppSpec } from '../../../types/agentspecs';
3
+ export declare const APP_ACTIVITY_PLUGIN_NAME = "@datalayer/loop-plugin-app-activity";
4
+ /**
5
+ * The plugin that lists what an application did: its record is kept under
6
+ * its `app` item (`appUid`); without one — an application not saved on
7
+ * Datalayer — the feed says it is recorded once it is.
8
+ */
9
+ export declare function defineAppActivityPlugin(app: AppSpec, appUid?: string): ReactorPlugin<Record<string, never>, unknown, unknown>;
10
+ export { AppActivity, APP_ACTIVITY_WORDS } from './AppActivity';
11
+ export default defineAppActivityPlugin;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { definePlugin } from '@datalayer/reactor';
3
+ import { LoopSlots } from '../../core';
4
+ import { AppActivity } from './AppActivity';
5
+ export const APP_ACTIVITY_PLUGIN_NAME = '@datalayer/loop-plugin-app-activity';
6
+ /**
7
+ * The plugin that lists what an application did: its record is kept under
8
+ * its `app` item (`appUid`); without one — an application not saved on
9
+ * Datalayer — the feed says it is recorded once it is.
10
+ */
11
+ export function defineAppActivityPlugin(app, appUid) {
12
+ // One component per plugin: the slot keeps it, and the application with it.
13
+ function Feed() {
14
+ return _jsx(AppActivity, { appUid: appUid });
15
+ }
16
+ const plugin = definePlugin({
17
+ name: `${APP_ACTIVITY_PLUGIN_NAME}-${app.id}`,
18
+ displayName: `${app.name}: activity`,
19
+ description: `What ${app.name} did, session by session, from its record.`,
20
+ octicon: 'pulse',
21
+ build: () => ({
22
+ components: [
23
+ {
24
+ id: 'app-activity',
25
+ slot: LoopSlots.sidebar,
26
+ order: 20,
27
+ Component: Feed,
28
+ },
29
+ ],
30
+ }),
31
+ });
32
+ return plugin;
33
+ }
34
+ export { AppActivity, APP_ACTIVITY_WORDS } from './AppActivity';
35
+ export default defineAppActivityPlugin;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * An application's rules, and the approvals it waits on, beside its page
3
+ * (LOOP R-01b, U-19).
4
+ *
5
+ * The rules in words — each action, what it covers, what it does about it —
6
+ * and, under them, what is waiting for the person's yes: the approvals its
7
+ * agent asked for, as the existing tool-approvals path carries them (the
8
+ * ai-agents `/ws` stream, the Tool Approvals page's), answered here with
9
+ * *Approve* or *Reject* over the same socket.
10
+ *
11
+ * @module loop/plugins/app-rules/AppRulesCard
12
+ */
13
+ import type { JSX } from 'react';
14
+ import type { AppRuleSpec, AppSpec } from '../../../types/agentspecs';
15
+ import { type ApprovalRecord } from '../../../hooks/useToolApprovals';
16
+ /** What the card says. */
17
+ export declare const APP_RULES_WORDS: {
18
+ readonly title: "Rules and approvals";
19
+ readonly byDefault: "With no rule of its own: reading is done, anything else waits for you.";
20
+ readonly none: "It has no rule of its own.";
21
+ readonly waiting: "Waiting for you";
22
+ readonly nothingWaiting: "Nothing is waiting for your yes.";
23
+ readonly signedOut: "Its approvals reach you here once you are signed in to Datalayer.";
24
+ readonly offline: "Approvals are not connected yet: what you approve or reject is not sent until they are.";
25
+ readonly approve: "Approve";
26
+ readonly reject: "Reject";
27
+ };
28
+ /** One rule, in words: its action, what it covers, and what it does about it. */
29
+ export declare function ruleInWords(rule: AppRuleSpec): {
30
+ action: string;
31
+ covers: string;
32
+ says: string;
33
+ means: string;
34
+ };
35
+ /** The rule an approval was asked under, as the runtime wrote it in its arguments. */
36
+ export declare const ruleOfApproval: (approval: ApprovalRecord) => string;
37
+ export declare function AppRulesCard({ app }: {
38
+ app: AppSpec;
39
+ }): JSX.Element;
40
+ export default AppRulesCard;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Button, Heading, Label, Text } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
6
+ import { BEHAVIOUR_WORDS, coverOf } from '../../apps/rules';
7
+ import { useApproveToolRequest, useRejectToolRequest, useToolApprovalsQuery, } from '../../../hooks/useToolApprovals';
8
+ /** What the card says. */
9
+ export const APP_RULES_WORDS = {
10
+ title: 'Rules and approvals',
11
+ byDefault: 'With no rule of its own: reading is done, anything else waits for you.',
12
+ none: 'It has no rule of its own.',
13
+ waiting: 'Waiting for you',
14
+ nothingWaiting: 'Nothing is waiting for your yes.',
15
+ signedOut: 'Its approvals reach you here once you are signed in to Datalayer.',
16
+ offline: 'Approvals are not connected yet: what you approve or reject is not sent until they are.',
17
+ approve: 'Approve',
18
+ reject: 'Reject',
19
+ };
20
+ /** One rule, in words: its action, what it covers, and what it does about it. */
21
+ export function ruleInWords(rule) {
22
+ const words = BEHAVIOUR_WORDS[rule.behaviour];
23
+ return {
24
+ action: rule.action,
25
+ covers: rule.appliesTo.map(coverOf).join(', '),
26
+ says: words.says,
27
+ means: words.means,
28
+ };
29
+ }
30
+ /** The rule an approval was asked under, as the runtime wrote it in its arguments. */
31
+ export const ruleOfApproval = (approval) => typeof approval.tool_args?._rule === 'string' ? approval.tool_args._rule : '';
32
+ function Approvals({ app }) {
33
+ // Its agent is created under the application's id: what it asks is under it.
34
+ const filters = useMemo(() => ({ agentId: app.id, status: 'pending' }), [app.id]);
35
+ const query = useToolApprovalsQuery(filters);
36
+ const approve = useApproveToolRequest();
37
+ const reject = useRejectToolRequest();
38
+ const waiting = query.data?.approvals ?? [];
39
+ return (_jsxs(Box, { "data-testid": "app-approvals", children: [_jsx(Heading, { as: "h4", sx: { fontSize: 1, mt: 3, mb: 1 }, children: APP_RULES_WORDS.waiting }), approve.connectionState !== 'connected' ? (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mb: 1 }, children: APP_RULES_WORDS.offline })) : null, waiting.length === 0 ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: APP_RULES_WORDS.nothingWaiting })) : (_jsx(Box, { as: "ul", sx: { listStyle: 'none', p: 0, m: 0 }, children: waiting.map(approval => (_jsxs(Box, { as: "li", sx: {
40
+ py: 2,
41
+ borderTop: '1px solid',
42
+ borderColor: 'border.muted',
43
+ fontSize: 1,
44
+ }, children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: approval.tool_name }), ruleOfApproval(approval) ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: ruleOfApproval(approval) })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 1 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approve.isPending, onClick: () => approve.mutate({ id: approval.id }), children: APP_RULES_WORDS.approve }), _jsx(Button, { size: "small", disabled: reject.isPending, onClick: () => reject.mutate({ id: approval.id }), children: APP_RULES_WORDS.reject })] })] }, approval.id))) }))] }));
45
+ }
46
+ export function AppRulesCard({ app }) {
47
+ const signedIn = Boolean(useIAMStore(state => state.token));
48
+ return (_jsxs(Box, { "data-testid": "app-rules", sx: { mb: 4 }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: APP_RULES_WORDS.title }), app.rules.length === 0 ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: APP_RULES_WORDS.none })) : (_jsx(Box, { as: "ul", sx: { listStyle: 'none', p: 0, m: 0 }, children: app.rules.map(ruleInWords).map((rule, index) => (_jsxs(Box, { as: "li", sx: { py: 1, fontSize: 1 }, children: [_jsx(Text, { children: rule.action }), ' ', _jsx(Label, { title: rule.means, children: rule.says }), _jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: rule.covers })] }, index))) })), _jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1, mb: 0 }, children: APP_RULES_WORDS.byDefault }), signedIn ? (_jsx(Approvals, { app: app })) : (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', mt: 3, mb: 0 }, children: APP_RULES_WORDS.signedOut }))] }));
49
+ }
50
+ export default AppRulesCard;
@@ -0,0 +1,7 @@
1
+ import { type ReactorPlugin } from '@datalayer/reactor';
2
+ import type { AppSpec } from '../../../types/agentspecs';
3
+ export declare const APP_RULES_PLUGIN_NAME = "@datalayer/loop-plugin-app-rules";
4
+ /** The plugin that lists an application's rules and the approvals waiting for the person. */
5
+ export declare function defineAppRulesPlugin(app: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
6
+ export { AppRulesCard, APP_RULES_WORDS, ruleInWords, ruleOfApproval, } from './AppRulesCard';
7
+ export default defineAppRulesPlugin;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { definePlugin } from '@datalayer/reactor';
3
+ import { LoopSlots } from '../../core';
4
+ import { AppRulesCard } from './AppRulesCard';
5
+ export const APP_RULES_PLUGIN_NAME = '@datalayer/loop-plugin-app-rules';
6
+ /** The plugin that lists an application's rules and the approvals waiting for the person. */
7
+ export function defineAppRulesPlugin(app) {
8
+ // One component per plugin: the slot keeps it, and the application with it.
9
+ function Card() {
10
+ return _jsx(AppRulesCard, { app: app });
11
+ }
12
+ const plugin = definePlugin({
13
+ name: `${APP_RULES_PLUGIN_NAME}-${app.id}`,
14
+ displayName: `${app.name}: rules and approvals`,
15
+ description: `When ${app.name} acts alone, when it asks, and what waits for your yes.`,
16
+ octicon: 'shield-check',
17
+ build: () => ({
18
+ components: [
19
+ {
20
+ id: 'app-rules',
21
+ slot: LoopSlots.sidebar,
22
+ order: 10,
23
+ Component: Card,
24
+ },
25
+ ],
26
+ }),
27
+ });
28
+ return plugin;
29
+ }
30
+ export { AppRulesCard, APP_RULES_WORDS, ruleInWords, ruleOfApproval, } from './AppRulesCard';
31
+ export default defineAppRulesPlugin;
@@ -43,8 +43,9 @@ import { messageText, 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 '../../../chat/presence/Presence';
46
+ import { FaceDrawing, PresenceFace, PresenceLine, } from '../../../chat/presence/Presence';
47
47
  import { presenceState, presenceToolOf, } from '../../../chat/presence/presenceStatus';
48
+ import { latestSaying, newestIsAnswer, } from '../../../chat/assistant/state';
48
49
  /** No editor: the conversation on its own. */
49
50
  const NO_SURFACE = '';
50
51
  /**
@@ -203,6 +204,14 @@ export default function ChatView({ workspace }) {
203
204
  useEffect(() => {
204
205
  onPresence?.(presenceNow);
205
206
  }, [onPresence, presenceNow]);
207
+ // What it last said, for a host that says it outside the conversation —
208
+ // the embed's floating chrome (LOOP R-01): its balloon and its blink.
209
+ // Through a ref: the handler below is made once.
210
+ const onSayingRef = useRef(config?.presence?.onSaying);
211
+ onSayingRef.current = config?.presence?.onSaying;
212
+ const saidRef = useRef({
213
+ answering: false,
214
+ });
206
215
  const handleSendReady = useCallback((controls) => {
207
216
  controlsRef.current = controls;
208
217
  setSendReady(!!controls);
@@ -221,6 +230,17 @@ export default function ChatView({ workspace }) {
221
230
  setPresenceTool(previous => previous.open === open && previous.pendingApproval === pendingApproval
222
231
  ? previous
223
232
  : { open, pendingApproval });
233
+ const said = {
234
+ saying: latestSaying(items),
235
+ answering: newestIsAnswer(items),
236
+ };
237
+ const before = saidRef.current;
238
+ if (before.saying?.id !== said.saying?.id ||
239
+ before.saying?.text !== said.saying?.text ||
240
+ before.answering !== said.answering) {
241
+ saidRef.current = said;
242
+ onSayingRef.current?.(said);
243
+ }
224
244
  const feed = turnFeedRef.current;
225
245
  if (!feed) {
226
246
  return;
@@ -1100,6 +1120,12 @@ export default function ChatView({ workspace }) {
1100
1120
  // a new identity would resend the last suggestion.
1101
1121
  // eslint-disable-next-line react-hooks/exhaustive-deps
1102
1122
  }, [suggestion]);
1123
+ /*
1124
+ * An application's session checks who is calling (LOOP R-32, R-04): its
1125
+ * chat says so with the person's token. The embed hands its own.
1126
+ */
1127
+ const runsApp = Boolean(blueprintTurn?.createPayload?.app_spec);
1128
+ const iamToken = useIAMStore(state => state.token);
1103
1129
  const protocol = useMemo(() => inPage
1104
1130
  ? browserProtocolConfig({
1105
1131
  agentId: member?.specId ?? agentId,
@@ -1132,9 +1158,10 @@ export default function ChatView({ workspace }) {
1132
1158
  // (LOOP R-04): each thread a session, recorded under its uid,
1133
1159
  // run in the name the application runs in, and taking what the
1134
1160
  // page did besides the message — the same AG-UI events back.
1135
- endpoint: blueprintTurn?.createPayload?.app_spec
1161
+ endpoint: runsApp
1136
1162
  ? `${agentServerUrl}/api/v1/apps/agents/${encodeURIComponent(agentId)}/ag-ui/`
1137
1163
  : `${agentServerUrl}/api/v1/ag-ui/${agentId}/`,
1164
+ ...(runsApp && iamToken ? { authToken: iamToken } : {}),
1138
1165
  agentId,
1139
1166
  // `/api/v1/configure`, not `/api/v1/configure/config`: the hooks
1140
1167
  // strip one trailing `config`/`configure` segment to find the API
@@ -1152,6 +1179,8 @@ export default function ChatView({ workspace }) {
1152
1179
  blueprintTurn,
1153
1180
  activeModel,
1154
1181
  agentServerUrl,
1182
+ runsApp,
1183
+ iamToken,
1155
1184
  ]);
1156
1185
  /*
1157
1186
  * The model catalogue, and the builtin tools and skills the agent reports.
@@ -1475,7 +1504,7 @@ export default function ChatView({ workspace }) {
1475
1504
  // The theme it wears, when the host names one (an application's).
1476
1505
  , {
1477
1506
  // The theme it wears, when the host names one (an application's).
1478
- themeVariant: config?.themeVariant, themeOverrides: config?.themeOverrides,
1507
+ themeVariant: config?.themeVariant, themeOverrides: config?.themeOverrides, colorMode: config?.colorMode,
1479
1508
  // The header says why, beside the title, for the same reason the
1480
1509
  // placeholder does: a dead control with no explanation is worse
1481
1510
  // than an absent one.
@@ -1516,7 +1545,7 @@ export default function ChatView({ workspace }) {
1516
1545
  headerContent: presence ? _jsx(PresenceLine, { state: presenceNow }) : undefined, emptyState: {
1517
1546
  // An application's own face, at the page's size of the three
1518
1547
  // (LOOP T-08, T-19): the thing a person's eye lands on first.
1519
- icon: presence?.face ? (_jsx("span", { "aria-hidden": true, style: { fontSize: FACE_LARGE, lineHeight: 1 }, children: presence.face })) : (_jsx(BrandIcon, { size: 48 })),
1548
+ icon: presence?.face ? (_jsx("span", { "aria-hidden": true, style: { fontSize: FACE_LARGE, lineHeight: 1 }, children: _jsx(FaceDrawing, { face: presence.face, size: FACE_LARGE }) })) : (_jsx(BrandIcon, { size: 48 })),
1520
1549
  // `ChatEmptyState` reads its heading from here and nowhere
1521
1550
  // else — the `title` above reaches the header only — so
1522
1551
  // without this the agent introduced itself as "Start a