@datalayer/agent-runtimes 1.3.27 → 1.3.29
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/ChatFloating.d.ts +12 -2
- package/lib/chat/ChatFloating.js +196 -25
- package/lib/chat/assistant/AssistantStage.d.ts +70 -0
- package/lib/chat/assistant/AssistantStage.js +192 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +26 -0
- package/lib/chat/assistant/SpeechBalloon.js +46 -0
- package/lib/chat/assistant/SpriteCharacter.d.ts +26 -0
- package/lib/chat/assistant/SpriteCharacter.js +73 -0
- package/lib/chat/assistant/characters.d.ts +30 -0
- package/lib/chat/assistant/characters.js +41 -0
- package/lib/chat/assistant/formats/acs.d.ts +88 -0
- package/lib/chat/assistant/formats/acs.js +493 -0
- package/lib/chat/assistant/formats/agentCompression.d.ts +5 -0
- package/lib/chat/assistant/formats/agentCompression.js +92 -0
- package/lib/chat/assistant/formats/blobs.d.ts +6 -0
- package/lib/chat/assistant/formats/blobs.js +52 -0
- package/lib/chat/assistant/formats/clippy.d.ts +21 -0
- package/lib/chat/assistant/formats/clippy.js +167 -0
- package/lib/chat/assistant/formats/index.d.ts +13 -0
- package/lib/chat/assistant/formats/index.js +15 -0
- package/lib/chat/assistant/formats/objectLiteral.d.ts +23 -0
- package/lib/chat/assistant/formats/objectLiteral.js +233 -0
- package/lib/chat/assistant/formats/stateAnimations.d.ts +16 -0
- package/lib/chat/assistant/formats/stateAnimations.js +115 -0
- package/lib/chat/assistant/formats/types.d.ts +56 -0
- package/lib/chat/assistant/formats/types.js +11 -0
- package/lib/chat/assistant/state.d.ts +62 -0
- package/lib/chat/assistant/state.js +116 -0
- package/lib/chat/header/ChatViewModeToggle.js +6 -3
- package/lib/chat/presence/Presence.d.ts +9 -0
- package/lib/chat/presence/Presence.js +57 -0
- package/lib/chat/presence/presenceStatus.d.ts +32 -0
- package/lib/chat/presence/presenceStatus.js +46 -0
- package/lib/chat/viewModes.d.ts +6 -0
- package/lib/chat/viewModes.js +10 -1
- package/lib/examples/ChatAssistantExample.d.ts +8 -0
- package/lib/examples/ChatAssistantExample.js +59 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/main.js +3 -0
- package/lib/loop/apps/AppRenderer.d.ts +8 -1
- package/lib/loop/apps/AppRenderer.js +2 -1
- package/lib/loop/apps/appspec.d.ts +5 -1
- package/lib/loop/apps/appspec.js +21 -1
- package/lib/loop/apps/checks.js +3 -1
- package/lib/loop/core/index.d.ts +15 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/plugins/assistant-characters/index.d.ts +30 -0
- package/lib/loop/plugins/assistant-characters/index.js +41 -0
- package/lib/loop/plugins/chat/ChatView.js +21 -1
- package/lib/loop/plugins/chat/index.d.ts +8 -0
- package/lib/loop/plugins/page-layout/index.js +5 -1
- package/lib/specs/actions.js +6 -0
- package/lib/specs/apps.d.ts +3 -0
- package/lib/specs/apps.js +793 -65
- package/lib/specs/appspecSchema.js +20 -2
- package/lib/types/agentspecs.d.ts +10 -1
- package/lib/types/chat.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the floating assistant acts out (LOOP T-22): the presence of the
|
|
3
|
+
* application (T-08), and three moments of its own — greeting when it
|
|
4
|
+
* arrives, speaking while an answer streams in, goodbye when it is sent away.
|
|
5
|
+
*
|
|
6
|
+
* @module chat/assistant/state
|
|
7
|
+
*/
|
|
8
|
+
import type { PresenceState } from '../presence/presenceStatus';
|
|
9
|
+
/** The states a character has an animation for. */
|
|
10
|
+
export type AssistantState = 'idle' | 'thinking' | 'working' | 'waiting' | 'greeting' | 'speaking' | 'goodbye';
|
|
11
|
+
/** What a character is doing, beyond what the application is doing. */
|
|
12
|
+
export interface AssistantMoment {
|
|
13
|
+
/** It has just appeared, and says hello once. */
|
|
14
|
+
arriving?: boolean;
|
|
15
|
+
/** It is being sent away. */
|
|
16
|
+
leaving?: boolean;
|
|
17
|
+
/** The answer is arriving: words are being written. */
|
|
18
|
+
speaking?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The state to act, from the application's presence and the moment.
|
|
22
|
+
*
|
|
23
|
+
* Leaving and arriving win, being short; then waiting for the person, which
|
|
24
|
+
* must never be missed; then speaking over thinking, since words arriving
|
|
25
|
+
* are what the person watches; then the presence as it is.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assistantStateOf(presence: PresenceState, moment?: AssistantMoment): AssistantState;
|
|
28
|
+
/**
|
|
29
|
+
* Whether words are arriving: the newest item of the conversation is the
|
|
30
|
+
* assistant's own message, not a tool call. Read with `busy`, it is speaking.
|
|
31
|
+
*/
|
|
32
|
+
export declare function newestIsAnswer(items: readonly unknown[]): boolean;
|
|
33
|
+
/** A word from the agent, for the character's balloon. */
|
|
34
|
+
export interface AssistantSaying {
|
|
35
|
+
/** The message it comes from: a new id is a new thing said. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Its text, short enough for a balloon. */
|
|
38
|
+
text: string;
|
|
39
|
+
/** Whether there was more than the balloon holds. */
|
|
40
|
+
more: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** How much of a message the balloon says before *Open the conversation*. */
|
|
43
|
+
export declare const SAYING_LIMIT = 220;
|
|
44
|
+
/**
|
|
45
|
+
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
46
|
+
* balloon, while the conversation is closed (T-23). Only its own messages
|
|
47
|
+
* with text — not a tool call, not the person's words; markdown is read as
|
|
48
|
+
* plain words, and a long message is cut at a word with `more` set.
|
|
49
|
+
*/
|
|
50
|
+
export declare function latestSaying(items: readonly unknown[]): AssistantSaying | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* How long the assistant is away, once sent (T-27): for the page — the
|
|
53
|
+
* popup's button calls it back — for the session, or for good; the header's
|
|
54
|
+
* *Floating assistant* always calls it back.
|
|
55
|
+
*/
|
|
56
|
+
export type AssistantAway = 'none' | 'page' | 'session' | 'always';
|
|
57
|
+
/** Where the choice is kept: the session's storage, or for good. */
|
|
58
|
+
export declare const ASSISTANT_AWAY_KEY = "datalayer-assistant-away";
|
|
59
|
+
/** The away a page starts with, from what was kept. Storage may be refused. */
|
|
60
|
+
export declare function keptAway(): AssistantAway;
|
|
61
|
+
/** Keep the choice where it lasts as long as it says; `none` forgets it. */
|
|
62
|
+
export declare function keepAway(away: AssistantAway): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The state to act, from the application's presence and the moment.
|
|
7
|
+
*
|
|
8
|
+
* Leaving and arriving win, being short; then waiting for the person, which
|
|
9
|
+
* must never be missed; then speaking over thinking, since words arriving
|
|
10
|
+
* are what the person watches; then the presence as it is.
|
|
11
|
+
*/
|
|
12
|
+
export function assistantStateOf(presence, moment = {}) {
|
|
13
|
+
if (moment.leaving) {
|
|
14
|
+
return 'goodbye';
|
|
15
|
+
}
|
|
16
|
+
if (moment.arriving) {
|
|
17
|
+
return 'greeting';
|
|
18
|
+
}
|
|
19
|
+
if (presence === 'waiting') {
|
|
20
|
+
return 'waiting';
|
|
21
|
+
}
|
|
22
|
+
if (moment.speaking && presence !== 'idle') {
|
|
23
|
+
return 'speaking';
|
|
24
|
+
}
|
|
25
|
+
return presence;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether words are arriving: the newest item of the conversation is the
|
|
29
|
+
* assistant's own message, not a tool call. Read with `busy`, it is speaking.
|
|
30
|
+
*/
|
|
31
|
+
export function newestIsAnswer(items) {
|
|
32
|
+
const newest = items[items.length - 1];
|
|
33
|
+
return (!!newest &&
|
|
34
|
+
newest.role === 'assistant' &&
|
|
35
|
+
typeof newest.toolName !== 'string');
|
|
36
|
+
}
|
|
37
|
+
/** How much of a message the balloon says before *Open the conversation*. */
|
|
38
|
+
export const SAYING_LIMIT = 220;
|
|
39
|
+
/**
|
|
40
|
+
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
41
|
+
* balloon, while the conversation is closed (T-23). Only its own messages
|
|
42
|
+
* with text — not a tool call, not the person's words; markdown is read as
|
|
43
|
+
* plain words, and a long message is cut at a word with `more` set.
|
|
44
|
+
*/
|
|
45
|
+
export function latestSaying(items) {
|
|
46
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
47
|
+
const item = items[index];
|
|
48
|
+
if (item.role !== 'assistant' || typeof item.toolName === 'string') {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const raw = typeof item.content === 'string'
|
|
52
|
+
? item.content
|
|
53
|
+
: Array.isArray(item.content)
|
|
54
|
+
? item.content
|
|
55
|
+
.map(part => part &&
|
|
56
|
+
typeof part === 'object' &&
|
|
57
|
+
part.type === 'text'
|
|
58
|
+
? String(part.text ?? '')
|
|
59
|
+
: '')
|
|
60
|
+
.join(' ')
|
|
61
|
+
: '';
|
|
62
|
+
const plain = raw
|
|
63
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
64
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
65
|
+
.replace(/[*_`#>]+/g, '')
|
|
66
|
+
.replace(/\s+/g, ' ')
|
|
67
|
+
.trim();
|
|
68
|
+
if (!plain) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (plain.length <= SAYING_LIMIT) {
|
|
72
|
+
return { id: String(item.id ?? index), text: plain, more: false };
|
|
73
|
+
}
|
|
74
|
+
const cut = plain.slice(0, SAYING_LIMIT);
|
|
75
|
+
const atWord = cut.slice(0, Math.max(cut.lastIndexOf(' '), SAYING_LIMIT * 0.6));
|
|
76
|
+
return {
|
|
77
|
+
id: String(item.id ?? index),
|
|
78
|
+
text: `${atWord.trimEnd()}…`,
|
|
79
|
+
more: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Where the choice is kept: the session's storage, or for good. */
|
|
85
|
+
export const ASSISTANT_AWAY_KEY = 'datalayer-assistant-away';
|
|
86
|
+
/** The away a page starts with, from what was kept. Storage may be refused. */
|
|
87
|
+
export function keptAway() {
|
|
88
|
+
try {
|
|
89
|
+
if (window.localStorage.getItem(ASSISTANT_AWAY_KEY) === 'always') {
|
|
90
|
+
return 'always';
|
|
91
|
+
}
|
|
92
|
+
if (window.sessionStorage.getItem(ASSISTANT_AWAY_KEY) === 'session') {
|
|
93
|
+
return 'session';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A page without storage keeps the assistant for the page only.
|
|
98
|
+
}
|
|
99
|
+
return 'none';
|
|
100
|
+
}
|
|
101
|
+
/** Keep the choice where it lasts as long as it says; `none` forgets it. */
|
|
102
|
+
export function keepAway(away) {
|
|
103
|
+
try {
|
|
104
|
+
window.localStorage.removeItem(ASSISTANT_AWAY_KEY);
|
|
105
|
+
window.sessionStorage.removeItem(ASSISTANT_AWAY_KEY);
|
|
106
|
+
if (away === 'always') {
|
|
107
|
+
window.localStorage.setItem(ASSISTANT_AWAY_KEY, 'always');
|
|
108
|
+
}
|
|
109
|
+
else if (away === 'session') {
|
|
110
|
+
window.sessionStorage.setItem(ASSISTANT_AWAY_KEY, 'session');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Without storage the choice holds for the page.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Tooltip } from '@primer/react';
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
|
-
import { CommentDiscussionIcon, DeviceMobileIcon, GrabberIcon, SidebarExpandIcon, } from '@primer/octicons-react';
|
|
5
|
-
import { CHAT_VIEW_MODES, SIDEBAR_NEEDS_MOUNT_POINT } from '../viewModes';
|
|
4
|
+
import { CommentDiscussionIcon, DeviceMobileIcon, GrabberIcon, PaperclipIcon, SidebarExpandIcon, } from '@primer/octicons-react';
|
|
5
|
+
import { ASSISTANT_NEEDS_A_FLOATING_CHAT, CHAT_VIEW_MODES, SIDEBAR_NEEDS_MOUNT_POINT, } from '../viewModes';
|
|
6
6
|
/** The drawing of each mode. */
|
|
7
7
|
export const VIEW_MODE_ICONS = {
|
|
8
8
|
floating: CommentDiscussionIcon,
|
|
9
9
|
'floating-small': DeviceMobileIcon,
|
|
10
10
|
'floating-draggable': GrabberIcon,
|
|
11
|
+
assistant: PaperclipIcon,
|
|
11
12
|
sidebar: SidebarExpandIcon,
|
|
12
13
|
};
|
|
13
14
|
export function ChatViewModeToggle({ value, onChange, disabledModes = [], tooltipDirection = 'n', }) {
|
|
@@ -26,7 +27,9 @@ export function ChatViewModeToggle({ value, onChange, disabledModes = [], toolti
|
|
|
26
27
|
const disabled = disabledModes.includes(mode);
|
|
27
28
|
return (_jsx(Tooltip, { text: disabled && mode === 'sidebar'
|
|
28
29
|
? `${label} — ${SIDEBAR_NEEDS_MOUNT_POINT}`
|
|
29
|
-
:
|
|
30
|
+
: disabled && mode === 'assistant'
|
|
31
|
+
? `${label} — ${ASSISTANT_NEEDS_A_FLOATING_CHAT}`
|
|
32
|
+
: label, direction: tooltipDirection, children: _jsx(Box, { as: "button", type: "button", "aria-label": label, "aria-pressed": selected, "aria-disabled": disabled || undefined, onClick: () => {
|
|
30
33
|
if (!disabled) {
|
|
31
34
|
onChange(mode);
|
|
32
35
|
}
|
|
@@ -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 chat/presence/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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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 chat/presence/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;
|
|
27
|
+
/**
|
|
28
|
+
* The newest tool call of the conversation: whether it has not returned, and
|
|
29
|
+
* whether it waits on a person's approval. A tool call is an item with a
|
|
30
|
+
* tool name and a call id; it has returned once it is complete or failed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function presenceToolOf(items: readonly unknown[]): PresenceTool;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The newest tool call of the conversation: whether it has not returned, and
|
|
31
|
+
* whether it waits on a person's approval. A tool call is an item with a
|
|
32
|
+
* tool name and a call id; it has returned once it is complete or failed.
|
|
33
|
+
*/
|
|
34
|
+
export function presenceToolOf(items) {
|
|
35
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
const item = items[index];
|
|
37
|
+
if (typeof item.toolName === 'string' && item.toolCallId) {
|
|
38
|
+
const open = item.status !== 'complete' && item.status !== 'error';
|
|
39
|
+
const pendingApproval = open &&
|
|
40
|
+
item.result
|
|
41
|
+
?.pending_approval === true;
|
|
42
|
+
return { open, pendingApproval };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { open: false, pendingApproval: false };
|
|
46
|
+
}
|
package/lib/chat/viewModes.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export type ChatViewModeOption = {
|
|
|
23
23
|
};
|
|
24
24
|
/** The modes, in the order the toggle shows them: floating first, docked last. */
|
|
25
25
|
export declare const CHAT_VIEW_MODES: readonly ChatViewModeOption[];
|
|
26
|
+
/**
|
|
27
|
+
* Why the assistant is greyed out, for its tooltip: it is drawn by the
|
|
28
|
+
* floating chat (`ChatFloating`); a layout that places the chat itself, as
|
|
29
|
+
* the page layout does, has no character to show (LOOP T-21).
|
|
30
|
+
*/
|
|
31
|
+
export declare const ASSISTANT_NEEDS_A_FLOATING_CHAT = "The assistant is drawn by the floating chat, not by this layout";
|
|
26
32
|
/** Why the sidebar option is greyed out, for its tooltip. */
|
|
27
33
|
export declare const SIDEBAR_NEEDS_MOUNT_POINT = "No mount point for a sidebar on this page";
|
|
28
34
|
/** Whether a mode floats over the page rather than docking into it. */
|
package/lib/chat/viewModes.js
CHANGED
|
@@ -7,15 +7,24 @@ export const CHAT_VIEW_MODES = [
|
|
|
7
7
|
{ mode: 'floating', label: 'Full-height popup' },
|
|
8
8
|
{ mode: 'floating-small', label: 'Floating popup' },
|
|
9
9
|
{ mode: 'floating-draggable', label: 'Floating draggable' },
|
|
10
|
+
// A character on the page that speaks in a balloon (LOOP T-21).
|
|
11
|
+
{ mode: 'assistant', label: 'Floating assistant' },
|
|
10
12
|
{ mode: 'sidebar', label: 'Sidebar panel' },
|
|
11
13
|
];
|
|
14
|
+
/**
|
|
15
|
+
* Why the assistant is greyed out, for its tooltip: it is drawn by the
|
|
16
|
+
* floating chat (`ChatFloating`); a layout that places the chat itself, as
|
|
17
|
+
* the page layout does, has no character to show (LOOP T-21).
|
|
18
|
+
*/
|
|
19
|
+
export const ASSISTANT_NEEDS_A_FLOATING_CHAT = 'The assistant is drawn by the floating chat, not by this layout';
|
|
12
20
|
/** Why the sidebar option is greyed out, for its tooltip. */
|
|
13
21
|
export const SIDEBAR_NEEDS_MOUNT_POINT = 'No mount point for a sidebar on this page';
|
|
14
22
|
/** Whether a mode floats over the page rather than docking into it. */
|
|
15
23
|
export function isFloatingChatViewMode(mode) {
|
|
16
24
|
return (mode === 'floating' ||
|
|
17
25
|
mode === 'floating-small' ||
|
|
18
|
-
mode === 'floating-draggable'
|
|
26
|
+
mode === 'floating-draggable' ||
|
|
27
|
+
mode === 'assistant');
|
|
19
28
|
}
|
|
20
29
|
/**
|
|
21
30
|
* Where a host said the sidebar goes.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating assistant (LOOP T-21 to T-27): the chat as a character on the
|
|
3
|
+
* page, after the Office Assistant — Datalayer's own characters, chosen
|
|
4
|
+
* here, acting out what the agent does and speaking in a balloon.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
declare const ChatAssistantExample: React.FC;
|
|
8
|
+
export default ChatAssistantExample;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
* The floating assistant (LOOP T-21 to T-27): the chat as a character on the
|
|
8
|
+
* page, after the Office Assistant — Datalayer's own characters, chosen
|
|
9
|
+
* here, acting out what the agent does and speaking in a balloon.
|
|
10
|
+
*/
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { Button, Heading, Text } from '@primer/react';
|
|
13
|
+
import { Box } from '@datalayer/primer-addons';
|
|
14
|
+
import { ThemedProvider } from './utils/themedProvider';
|
|
15
|
+
import { ChatFloating } from '../chat';
|
|
16
|
+
import { ASSISTANT_CHARACTERS } from '../chat/assistant/characters';
|
|
17
|
+
import { readAcsCharacter, readClippyCharacter, } from '../chat/assistant/formats';
|
|
18
|
+
/**
|
|
19
|
+
* A character file a person picked, read in the page (T-26): one `.acs`, or
|
|
20
|
+
* a clippy.js character's `agent.js` and `map.png` (and its sounds file).
|
|
21
|
+
*/
|
|
22
|
+
async function readPicked(files) {
|
|
23
|
+
const acs = files.find(file => file.name.toLowerCase().endsWith('.acs'));
|
|
24
|
+
if (acs) {
|
|
25
|
+
return readAcsCharacter(await acs.arrayBuffer());
|
|
26
|
+
}
|
|
27
|
+
const agentJs = files.find(file => file.name.toLowerCase() === 'agent.js');
|
|
28
|
+
const mapPng = files.find(file => /\.(png|gif|webp|jpe?g)$/i.test(file.name));
|
|
29
|
+
const soundsJs = files.find(file => /^sounds-.*\.js$/i.test(file.name));
|
|
30
|
+
if (!agentJs || !mapPng) {
|
|
31
|
+
throw new Error('Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).');
|
|
32
|
+
}
|
|
33
|
+
return readClippyCharacter({
|
|
34
|
+
agentJs: await agentJs.text(),
|
|
35
|
+
mapPng,
|
|
36
|
+
soundsJs: soundsJs ? await soundsJs.text() : undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const ChatAssistantExample = () => {
|
|
40
|
+
const [character, setCharacter] = useState(ASSISTANT_CHARACTERS[0].id);
|
|
41
|
+
const [loadError, setLoadError] = useState();
|
|
42
|
+
return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Floating assistant" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: "The chat as a character on the page. It greets you, acts out what the agent is doing, and says the agent\u2019s words in a balloon while the conversation is closed. Click it to talk, drag it to move it, hover it to send it away." }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: ASSISTANT_CHARACTERS.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
|
|
43
|
+
setCharacter(option.id);
|
|
44
|
+
setLoadError(undefined);
|
|
45
|
+
}, "aria-pressed": option.id === character, children: option.name }, option.id))) }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
|
|
46
|
+
const files = Array.from(event.target.files ?? []);
|
|
47
|
+
if (!files.length) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
setCharacter(await readPicked(files));
|
|
52
|
+
setLoadError(undefined);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
56
|
+
}
|
|
57
|
+
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: character, protocol: "vercel-ai", endpoint: "http://127.0.0.1:8765/api/v1/vercel-ai/assistant", title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false }, typeof character === 'string' ? character : character.sprite)] }) }));
|
|
58
|
+
};
|
|
59
|
+
export default ChatAssistantExample;
|
|
@@ -87,6 +87,7 @@ export const EXAMPLE_ENTRIES = [
|
|
|
87
87
|
makeEntry('DocumentPageAgent', () => import('./DocumentPageAgent'), 'A document as a page: floating prompt on top, an agent writing in it.'),
|
|
88
88
|
makeEntry('DecksAgent', () => import('./DecksAgent'), 'A deck beside the chat: the decks plugin in a Loop, and an agent that writes presentations as data.'),
|
|
89
89
|
makeEntry('CellExample', () => import('./CellExample'), 'Simple cell example.'),
|
|
90
|
+
makeEntry('ChatAssistantExample', () => import('./ChatAssistantExample'), 'The floating assistant: the chat as a character on the page.'),
|
|
90
91
|
makeEntry('ChatCustomExample', () => import('./ChatCustomExample'), 'Custom chat experience composition example.'),
|
|
91
92
|
makeEntry('ChatExample', () => import('./ChatExample'), 'Baseline chat integration example.'),
|
|
92
93
|
makeEntry('ChatStandaloneExample', () => import('./ChatStandaloneExample'), 'Standalone chat component usage example.'),
|
package/lib/examples/main.js
CHANGED
|
@@ -77,6 +77,9 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
77
77
|
'DocumentPageAgent',
|
|
78
78
|
// The decks plugin in a Loop; the agent and the decks both live in the page.
|
|
79
79
|
'DecksAgent',
|
|
80
|
+
// The floating assistant: the character, its balloon and its motions need
|
|
81
|
+
// no runtime; a conversation does, and says so when there is none.
|
|
82
|
+
'ChatAssistantExample',
|
|
80
83
|
]);
|
|
81
84
|
const getExampleGroup = (id) => {
|
|
82
85
|
if (id === 'AgentspecsExample' ||
|
|
@@ -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 '../../chat/presence/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;
|
|
@@ -24,13 +24,17 @@
|
|
|
24
24
|
*
|
|
25
25
|
* @module loop/apps/appspec
|
|
26
26
|
*/
|
|
27
|
-
import type { AppAccent, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
|
|
27
|
+
import type { AppAccent, AppAssistantCharacter, AppEmbedMode, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
|
|
28
28
|
/** The version of the spec itself. */
|
|
29
29
|
export declare const APP_SCHEMA = "loop.app/v1";
|
|
30
30
|
/** The face of an application that has not chosen one. */
|
|
31
31
|
export declare const DEFAULT_EMOJI = "\uD83D\uDC40";
|
|
32
32
|
export declare const APP_KINDS: AppKind[];
|
|
33
33
|
export declare const APP_ACCENTS: AppAccent[];
|
|
34
|
+
/** The four ways an application sits in another product's page (LOOP D-07). */
|
|
35
|
+
export declare const APP_EMBED_MODES: AppEmbedMode[];
|
|
36
|
+
/** The characters an Appspec may name for its floating assistant (LOOP T-24). */
|
|
37
|
+
export declare const APP_ASSISTANT_CHARACTERS: AppAssistantCharacter[];
|
|
34
38
|
/** The layout a kind starts with. */
|
|
35
39
|
export declare const DEFAULT_LAYOUTS: Record<AppKind, AppLayout>;
|
|
36
40
|
/** What an application keeps of what it did, when it does not say. */
|
package/lib/loop/apps/appspec.js
CHANGED
|
@@ -16,6 +16,20 @@ export const APP_ACCENTS = [
|
|
|
16
16
|
'sun',
|
|
17
17
|
'violet',
|
|
18
18
|
];
|
|
19
|
+
/** The four ways an application sits in another product's page (LOOP D-07). */
|
|
20
|
+
export const APP_EMBED_MODES = [
|
|
21
|
+
'inline',
|
|
22
|
+
'bubble',
|
|
23
|
+
'panel',
|
|
24
|
+
'assistant',
|
|
25
|
+
];
|
|
26
|
+
/** The characters an Appspec may name for its floating assistant (LOOP T-24). */
|
|
27
|
+
export const APP_ASSISTANT_CHARACTERS = [
|
|
28
|
+
'paperclip',
|
|
29
|
+
'wizard',
|
|
30
|
+
'cat',
|
|
31
|
+
'eyes',
|
|
32
|
+
];
|
|
19
33
|
const ACTION_CLASS_NAMES = [
|
|
20
34
|
'read',
|
|
21
35
|
'write',
|
|
@@ -203,6 +217,9 @@ function parseInterface(data, kind) {
|
|
|
203
217
|
if (isData(data.surface)) {
|
|
204
218
|
parsed.surface = parseSurface(data.surface);
|
|
205
219
|
}
|
|
220
|
+
if (APP_ASSISTANT_CHARACTERS.includes(data.assistant)) {
|
|
221
|
+
parsed.assistant = data.assistant;
|
|
222
|
+
}
|
|
206
223
|
return parsed;
|
|
207
224
|
}
|
|
208
225
|
function parseCriterion(data) {
|
|
@@ -239,7 +256,7 @@ function parseDeployment(data) {
|
|
|
239
256
|
}
|
|
240
257
|
if (isData(data.embedded)) {
|
|
241
258
|
deployment.embedded = {
|
|
242
|
-
mode: oneOf(data.embedded.mode,
|
|
259
|
+
mode: oneOf(data.embedded.mode, APP_EMBED_MODES, 'inline'),
|
|
243
260
|
origins: texts(data.embedded.origins),
|
|
244
261
|
};
|
|
245
262
|
}
|
|
@@ -454,6 +471,9 @@ function dumpInterface(spec, kind) {
|
|
|
454
471
|
if (spec.surface) {
|
|
455
472
|
writer.data.surface = dumpSurface(spec.surface);
|
|
456
473
|
}
|
|
474
|
+
if (spec.assistant) {
|
|
475
|
+
writer.data.assistant = spec.assistant;
|
|
476
|
+
}
|
|
457
477
|
return writer.data;
|
|
458
478
|
}
|
|
459
479
|
function dumpDecision(decision) {
|
package/lib/loop/apps/checks.js
CHANGED
|
@@ -370,7 +370,8 @@ const ENUMS = {
|
|
|
370
370
|
'feedback',
|
|
371
371
|
],
|
|
372
372
|
visibility: ['private', 'invited', 'organization', 'link', 'public'],
|
|
373
|
-
mode: ['inline', 'bubble', 'panel'],
|
|
373
|
+
mode: ['inline', 'bubble', 'panel', 'assistant'],
|
|
374
|
+
character: ['paperclip', 'wizard', 'cat', 'eyes'],
|
|
374
375
|
trigger: ['schedule', 'event', 'once'],
|
|
375
376
|
criterion: ['metric', 'noul', 'choice', 'score'],
|
|
376
377
|
direction: ['higher', 'lower'],
|
|
@@ -498,6 +499,7 @@ export function documentShapeProblems(document) {
|
|
|
498
499
|
mapping(d.interface, 'interface', ui => {
|
|
499
500
|
oneOf(ui.layout, ENUMS.layout, 'interface.layout');
|
|
500
501
|
oneOf(ui.accent, ENUMS.accent, 'interface.accent');
|
|
502
|
+
oneOf(ui.assistant, ENUMS.character, 'interface.assistant');
|
|
501
503
|
text(ui.welcome, 'interface.welcome');
|
|
502
504
|
texts(ui.components, 'interface.components');
|
|
503
505
|
records(ui.starters, 'interface.starters', (item, where) => {
|
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { ExecutionState } from '@datalayer/jupyter-react/kernel-indicator';
|
|
|
4
4
|
import type { ToolbarItem } from '@datalayer/primer-addons';
|
|
5
5
|
import type { FrontendToolDefinition } from '../../types/tools';
|
|
6
6
|
import type { ContextSnapshotData } from '../../types/context';
|
|
7
|
+
import type { AssistantCharacter } from '../../chat/assistant/characters';
|
|
8
|
+
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
7
9
|
/** Lifecycle of the sandbox a workspace is attached to. */
|
|
8
10
|
export type SandboxState = 'idle' | 'starting' | 'running' | 'stopping' | 'error';
|
|
9
11
|
/** What the workspace knows about the sandbox behind it. */
|
|
@@ -503,6 +505,19 @@ export type ChatSuggestionContribution = {
|
|
|
503
505
|
suggestions: ChatSuggestionItem[];
|
|
504
506
|
};
|
|
505
507
|
export declare const LoopChatSuggestion: import("@datalayer/reactor").ContributionPoint<ChatSuggestionContribution>;
|
|
508
|
+
/**
|
|
509
|
+
* A character the floating assistant may show (LOOP T-24): a drawing — one
|
|
510
|
+
* of Datalayer's own (T-25) or a plugin's — or a character read from a file
|
|
511
|
+
* a person holds the rights to (T-26). The catalogue of characters is what
|
|
512
|
+
* the enabled plugins contribute here, and nothing else; an application
|
|
513
|
+
* names one by id (`interface.assistant`).
|
|
514
|
+
*/
|
|
515
|
+
export type AssistantCharacterContribution = {
|
|
516
|
+
/** Stable id, what an application names. */
|
|
517
|
+
id: string;
|
|
518
|
+
character: AssistantCharacter | AssistantCharacterData;
|
|
519
|
+
};
|
|
520
|
+
export declare const LoopAssistantCharacter: import("@datalayer/reactor").ContributionPoint<AssistantCharacterContribution>;
|
|
506
521
|
/**
|
|
507
522
|
* The per-example chat extras a host feeds the loop's conversation live.
|
|
508
523
|
*
|