@datalayer/agent-runtimes 1.3.27 → 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/loop/apps/AppRenderer.d.ts +8 -1
- package/lib/loop/apps/AppRenderer.js +2 -1
- package/lib/loop/plugins/chat/ChatView.js +32 -1
- 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 +8 -0
- package/lib/loop/plugins/chat/presenceStatus.d.ts +26 -0
- package/lib/loop/plugins/chat/presenceStatus.js +28 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -142,6 +142,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, ...embed }) {
|
|
|
142
142
|
name: app.name,
|
|
143
143
|
face: app.emoji,
|
|
144
144
|
welcome: app.interface.welcome || app.description,
|
|
145
|
+
onPresence,
|
|
145
146
|
}, showTokenUsage: false, datalayerCreatePayload: datalayerCreatePayload, ...embed, plugins: allPlugins }));
|
|
146
147
|
}
|
|
147
148
|
export default AppRenderer;
|
|
@@ -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;
|
|
@@ -1436,7 +1464,10 @@ export default function ChatView({ workspace }) {
|
|
|
1436
1464
|
// Big enough to read as the agent's mark rather than as
|
|
1437
1465
|
// punctuation before its name, and still short enough not to
|
|
1438
1466
|
// set the header's height.
|
|
1439
|
-
brandIcon: presence?.face ? (_jsx(
|
|
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: {
|
|
1440
1471
|
// An application's own face, large: the thing a person's eye
|
|
1441
1472
|
// lands on first (LOOP T-08).
|
|
1442
1473
|
icon: presence?.face ? (_jsx("span", { "aria-hidden": true, style: { fontSize: 48, lineHeight: 1 }, children: presence.face })) : (_jsx(BrandIcon, { size: 48 })),
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import type { PresenceState } from './presenceStatus';
|
|
1
2
|
/** What a host may set on the chat. */
|
|
2
3
|
/** Who is answering, as the person meets them. */
|
|
3
4
|
export interface ChatPresence {
|
|
@@ -6,6 +7,13 @@ export interface ChatPresence {
|
|
|
6
7
|
face?: string;
|
|
7
8
|
/** What it says before anyone writes. */
|
|
8
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;
|
|
9
17
|
}
|
|
10
18
|
export type ChatPluginConfig = {
|
|
11
19
|
/**
|
|
@@ -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
|
+
}
|