@datalayer/agent-runtimes 1.3.90 → 1.3.91

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.
@@ -20,6 +20,13 @@ export type ChatWords = {
20
20
  demoKeyExpired: string;
21
21
  /** The person's key has run out. */
22
22
  keyExpired: string;
23
+ /**
24
+ * No runtime is assigned to the conversation on Datalayer (the pool had
25
+ * none, or its launch was refused), with why: nothing is sent.
26
+ */
27
+ noRuntime: (reason: string) => string;
28
+ /** Its runtime is still being assigned: nothing is sent until it is. */
29
+ runtimeStarting: string;
23
30
  /** The empty chat's title. */
24
31
  startConversation: string;
25
32
  /** The empty chat's line when it has no starters. */
package/lib/chat/words.js CHANGED
@@ -30,6 +30,8 @@ export const CHAT_WORDS = {
30
30
  suggestedPrompts: 'Suggested prompts',
31
31
  demoKeyExpired: 'This demo runs on a shared key, and its time is up. Sign in to keep going.',
32
32
  keyExpired: 'Your key has expired. Sign in to keep going.',
33
+ noRuntime: reason => `No runtime is assigned to this conversation, so nothing can be sent. ${reason}`,
34
+ runtimeStarting: 'Its runtime is being assigned: you can send once it is.',
33
35
  startConversation: 'Start a conversation',
34
36
  sendToBegin: 'Send a message to begin chatting',
35
37
  copyTurn: 'Copy this turn',
@@ -99,6 +101,8 @@ export const CHAT_WORDS = {
99
101
  suggestedPrompts: 'Suggestions de questions',
100
102
  demoKeyExpired: 'Cette démo utilise une clé partagée, et son temps est écoulé. Connectez-vous pour continuer.',
101
103
  keyExpired: 'Votre clé a expiré. Connectez-vous pour continuer.',
104
+ noRuntime: reason => `Aucun runtime n’est attribué à cette conversation : rien ne peut être envoyé. ${reason}`,
105
+ runtimeStarting: 'Son runtime est en cours d’attribution : vous pourrez envoyer une fois qu’il le sera.',
102
106
  startConversation: 'Commencer une conversation',
103
107
  sendToBegin: 'Envoyez un message pour commencer',
104
108
  copyTurn: 'Copier cet échange',
@@ -170,6 +174,8 @@ export const CHAT_WORDS = {
170
174
  suggestedPrompts: 'Preguntas sugeridas',
171
175
  demoKeyExpired: 'Esta demo usa una clave compartida y su tiempo se ha agotado. Inicia sesión para continuar.',
172
176
  keyExpired: 'Tu clave ha caducado. Inicia sesión para continuar.',
177
+ noRuntime: reason => `No hay ningún runtime asignado a esta conversación, así que no se puede enviar nada. ${reason}`,
178
+ runtimeStarting: 'Se le está asignando un runtime: podrás enviar cuando lo tenga.',
173
179
  startConversation: 'Empieza una conversación',
174
180
  sendToBegin: 'Envía un mensaje para empezar',
175
181
  copyTurn: 'Copiar este turno',
@@ -241,6 +247,8 @@ export const CHAT_WORDS = {
241
247
  suggestedPrompts: 'Vorgeschlagene Fragen',
242
248
  demoKeyExpired: 'Diese Demo läuft mit einem geteilten Schlüssel, und ihre Zeit ist abgelaufen. Melde dich an, um weiterzumachen.',
243
249
  keyExpired: 'Dein Schlüssel ist abgelaufen. Melde dich an, um weiterzumachen.',
250
+ noRuntime: reason => `Diesem Gespräch ist keine Runtime zugewiesen, daher kann nichts gesendet werden. ${reason}`,
251
+ runtimeStarting: 'Eine Runtime wird zugewiesen: Du kannst senden, sobald sie da ist.',
244
252
  startConversation: 'Ein Gespräch beginnen',
245
253
  sendToBegin: 'Sende eine Nachricht, um zu beginnen',
246
254
  copyTurn: 'Diesen Austausch kopieren',
@@ -312,6 +320,8 @@ export const CHAT_WORDS = {
312
320
  suggestedPrompts: 'Domande suggerite',
313
321
  demoKeyExpired: 'Questa demo usa una chiave condivisa e il suo tempo è scaduto. Accedi per continuare.',
314
322
  keyExpired: 'La tua chiave è scaduta. Accedi per continuare.',
323
+ noRuntime: reason => `Nessun runtime è assegnato a questa conversazione, quindi non si può inviare nulla. ${reason}`,
324
+ runtimeStarting: 'Gli si sta assegnando un runtime: potrai inviare appena sarà pronto.',
315
325
  startConversation: 'Inizia una conversazione',
316
326
  sendToBegin: 'Invia un messaggio per iniziare',
317
327
  copyTurn: 'Copia questo scambio',
@@ -383,6 +393,8 @@ export const CHAT_WORDS = {
383
393
  suggestedPrompts: 'Perguntas sugeridas',
384
394
  demoKeyExpired: 'Esta demonstração usa uma chave compartilhada e o tempo dela acabou. Entre para continuar.',
385
395
  keyExpired: 'Sua chave expirou. Entre para continuar.',
396
+ noRuntime: reason => `Nenhum runtime está atribuído a esta conversa, então nada pode ser enviado. ${reason}`,
397
+ runtimeStarting: 'Um runtime está sendo atribuído: você poderá enviar assim que estiver.',
386
398
  startConversation: 'Comece uma conversa',
387
399
  sendToBegin: 'Envie uma mensagem para começar',
388
400
  copyTurn: 'Copiar esta troca',
@@ -36,21 +36,32 @@ export const A2UI_RENDER_SCOPE_SX = {
36
36
  '& .a2ui-caption': {
37
37
  color: 'var(--a2ui-text-caption-color, var(--a2ui-color-on-surface, #656d76))',
38
38
  },
39
+ /*
40
+ * A button is the theme's filled button (STUDIO P-04, T-30): its fill and
41
+ * its label are Primer's primary-button properties, which every theme and
42
+ * an application's accent set together (`themeAccentVars`), so the label
43
+ * is always the accent's own text on it. The label is a Text inside the
44
+ * button, which the `.a2ui-text` rule above paints in the surface's ink —
45
+ * a dark label on Spatial's dark accent — so it inherits the button's.
46
+ */
39
47
  '& .a2ui-button': {
40
48
  appearance: 'none',
41
49
  cursor: 'pointer',
42
- border: '1px solid var(--a2ui-color-primary, #1f883d)',
43
- background: 'var(--a2ui-color-primary, #1f883d)',
44
- color: 'var(--a2ui-color-on-primary, #ffffff)',
50
+ border: '1px solid var(--button-primary-bgColor-rest)',
51
+ background: 'var(--button-primary-bgColor-rest)',
52
+ color: 'var(--button-primary-fgColor-rest)',
45
53
  borderRadius: 'var(--a2ui-button-border-radius, 6px)',
46
54
  padding: 'var(--a2ui-button-padding, 5px 16px)',
47
55
  fontWeight: 'var(--a2ui-button-font-weight, 500)',
48
56
  boxShadow: 'var(--a2ui-button-box-shadow, none)',
49
57
  transition: 'background-color 0.15s ease, border-color 0.15s ease',
50
58
  },
59
+ '& .a2ui-button .a2ui-text': {
60
+ color: 'inherit',
61
+ },
51
62
  '& .a2ui-button:hover': {
52
- background: 'var(--a2ui-color-primary-hover, #1a7f37)',
53
- borderColor: 'var(--a2ui-color-primary-hover, #1a7f37)',
63
+ background: 'var(--button-primary-bgColor-hover)',
64
+ borderColor: 'var(--button-primary-bgColor-hover)',
54
65
  },
55
66
  '& .a2ui-button:disabled': {
56
67
  opacity: 0.6,
@@ -5,7 +5,7 @@ import { ThumbsdownIcon, ThumbsupIcon } from '@primer/octicons-react';
5
5
  import { contribution, definePlugin, signal } from '@datalayer/reactor';
6
6
  import { useContributions, useSignalValue } from '@datalayer/reactor/react';
7
7
  import { useIAMStore } from '../../state';
8
- import { LoopChatTurn, LoopPromptPanel, } from '../core';
8
+ import { LoopChatTurn, LoopPromptPanel, agentServerOf, } from '../core';
9
9
  import { answerIdOf, FEEDBACK_WORDS, sendFeedback } from './feedback';
10
10
  export const APP_FEEDBACK_PLUGIN_NAME = '@datalayer/loop-plugin-app-feedback';
11
11
  /* A signal to read when no chat contributed a turn: the hook needs one. */
@@ -28,11 +28,20 @@ export function AppFeedback({ app, workspace, }) {
28
28
  const thread = turn.thread;
29
29
  const choice = liked?.turn === turn.id ? liked.liked : null;
30
30
  const done = said?.turn === turn.id ? said : null;
31
- const agentBaseUrl = workspace.sandbox.agentBaseUrl || workspace.serverUrl || '';
31
+ // The runtime that answered: none, nothing is sent (no stand-in server).
32
+ const agentBaseUrl = agentServerOf(workspace.sandbox, workspace.serverUrl);
32
33
  const send = async () => {
33
34
  if (choice === null) {
34
35
  return;
35
36
  }
37
+ if (agentBaseUrl === undefined) {
38
+ setSaid({
39
+ turn: turn.id,
40
+ state: 'refused',
41
+ says: FEEDBACK_WORDS.noRuntime,
42
+ });
43
+ return;
44
+ }
36
45
  setSaid({ turn: turn.id, state: 'sending', says: '' });
37
46
  try {
38
47
  await sendFeedback({
@@ -64,7 +73,7 @@ export function AppFeedback({ app, workspace, }) {
64
73
  flexDirection: 'column',
65
74
  gap: 4,
66
75
  padding: '4px 8px',
67
- }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.ask }), _jsx(IconButton, { size: "small", variant: choice === true ? 'default' : 'invisible', icon: ThumbsupIcon, "aria-label": FEEDBACK_WORDS.liked, "aria-pressed": choice === true, onClick: () => setLiked({ turn: turn.id, liked: true }) }), _jsx(IconButton, { size: "small", variant: choice === false ? 'default' : 'invisible', icon: ThumbsdownIcon, "aria-label": FEEDBACK_WORDS.disliked, "aria-pressed": choice === false, onClick: () => setLiked({ turn: turn.id, liked: false }) })] }), choice !== null ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": FEEDBACK_WORDS.comment, placeholder: FEEDBACK_WORDS.comment, value: comment, rows: 2, resize: "vertical", block: true, onChange: event => setComment(event.target.value) }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Button, { size: "small", variant: "default", disabled: done?.state === 'sending', onClick: () => void send(), children: FEEDBACK_WORDS.send }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.where(app.name) })] })] })) : null, done?.state === 'refused' ? (
76
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.ask }), _jsx(IconButton, { size: "small", variant: choice === true ? 'default' : 'invisible', icon: ThumbsupIcon, "aria-label": FEEDBACK_WORDS.liked, "aria-pressed": choice === true, unsafeDisableTooltip: true, onClick: () => setLiked({ turn: turn.id, liked: true }) }), _jsx(IconButton, { size: "small", variant: choice === false ? 'default' : 'invisible', icon: ThumbsdownIcon, "aria-label": FEEDBACK_WORDS.disliked, "aria-pressed": choice === false, unsafeDisableTooltip: true, onClick: () => setLiked({ turn: turn.id, liked: false }) })] }), choice !== null ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": FEEDBACK_WORDS.comment, placeholder: FEEDBACK_WORDS.comment, value: comment, rows: 2, resize: "vertical", block: true, onChange: event => setComment(event.target.value) }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Button, { size: "small", variant: "default", disabled: done?.state === 'sending', onClick: () => void send(), children: FEEDBACK_WORDS.send }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.where(app.name) })] })] })) : null, done?.state === 'refused' ? (
68
77
  // Said in the ink, as an alert: not a verdict, so not its colour.
69
78
  _jsx(Text, { role: "alert", sx: { fontSize: 0, color: 'fg.default' }, children: done.says })) : null] }));
70
79
  }
@@ -32,13 +32,14 @@ export declare const answerIdOf: (answers: number) => string | undefined;
32
32
  /** What the feedback strip says, in a person's words. */
33
33
  export declare const FEEDBACK_WORDS: {
34
34
  readonly ask: "Was this answer useful?";
35
- readonly liked: "Useful";
36
- readonly disliked: "Not useful";
35
+ readonly liked: "This answer was useful";
36
+ readonly disliked: "This answer was not useful";
37
37
  readonly comment: "Anything to add? (optional)";
38
38
  readonly send: "Send";
39
39
  readonly kept: (app: string) => string;
40
40
  readonly where: (app: string) => string;
41
41
  readonly tooLong: "A comment is at most 2000 characters.";
42
+ readonly noRuntime: "No runtime is assigned to this conversation, so nothing can be sent.";
42
43
  readonly noSession: "This conversation has no name the application knows yet: send a message first.";
43
44
  };
44
45
  /** Whether an application keeps feedback: its record names it. */
@@ -9,13 +9,15 @@ export const answerIdOf = (answers) => answers > 0 ? `answer-${answers}` : undef
9
9
  /** What the feedback strip says, in a person's words. */
10
10
  export const FEEDBACK_WORDS = {
11
11
  ask: 'Was this answer useful?',
12
- liked: 'Useful',
13
- disliked: 'Not useful',
12
+ // The thumbs' names (P-24): what pressing one says of the answer.
13
+ liked: 'This answer was useful',
14
+ disliked: 'This answer was not useful',
14
15
  comment: 'Anything to add? (optional)',
15
16
  send: 'Send',
16
17
  kept: (app) => `Thank you. Kept in ${app}’s record.`,
17
18
  where: (app) => `What you say is kept with this conversation in ${app}’s record, for its builder to read.`,
18
19
  tooLong: `A comment is at most ${FEEDBACK_COMMENT_LIMIT} characters.`,
20
+ noRuntime: 'No runtime is assigned to this conversation, so nothing can be sent.',
19
21
  noSession: 'This conversation has no name the application knows yet: send a message first.',
20
22
  };
21
23
  /** Whether an application keeps feedback: its record names it. */
@@ -8,6 +8,7 @@ import type { ContextSnapshotData } from '../../types/context';
8
8
  import type { AssistantCharacter } from '../../chat/assistant/characters';
9
9
  import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
10
10
  import type { PresenceState } from '../../chat/presence/presenceStatus';
11
+ import type { ChatWords } from '../../chat/words';
11
12
  /** Lifecycle of the sandbox a workspace is attached to. */
12
13
  export type SandboxState = 'idle' | 'starting' | 'running' | 'stopping' | 'error';
13
14
  /** What the workspace knows about the sandbox behind it. */
@@ -53,6 +54,24 @@ export type SandboxSnapshot = {
53
54
  */
54
55
  agentBaseUrl?: string;
55
56
  };
57
+ /**
58
+ * The agent-runtimes server a workspace's agent is on, or nothing while
59
+ * there is none.
60
+ *
61
+ * On Datalayer the agent is on its runtime and nowhere else: until one is
62
+ * assigned there is nothing to address, and the host's own server is not a
63
+ * stand-in — on a hosted page it is the page's own origin, which answered a
64
+ * question with a 404 while the pool had no runtime (STUDIO P-24, round 9).
65
+ * Every other target's agent is on the host's server, or on the one its
66
+ * sandbox names.
67
+ */
68
+ export declare function agentServerOf(sandbox: Pick<SandboxSnapshot, 'target' | 'agentBaseUrl'>, serverUrl: string): string | undefined;
69
+ /**
70
+ * What is said while {@link agentServerOf} names no server: the runtime is
71
+ * being assigned, or — its launch refused, the pool empty — none is, and
72
+ * why. One sentence for the chat, the page and the computer alike.
73
+ */
74
+ export declare function noRuntimeSaid(sandbox: Pick<SandboxSnapshot, 'state' | 'errorReason'>, words: Pick<ChatWords, 'noRuntime' | 'runtimeStarting'>): string;
56
75
  /**
57
76
  * What a view reads while the sandbox plugin is absent.
58
77
  *
@@ -14,6 +14,33 @@
14
14
  * @module loop/core
15
15
  */
16
16
  import { defineContributionPoint, defineGate } from '@datalayer/reactor';
17
+ /**
18
+ * The agent-runtimes server a workspace's agent is on, or nothing while
19
+ * there is none.
20
+ *
21
+ * On Datalayer the agent is on its runtime and nowhere else: until one is
22
+ * assigned there is nothing to address, and the host's own server is not a
23
+ * stand-in — on a hosted page it is the page's own origin, which answered a
24
+ * question with a 404 while the pool had no runtime (STUDIO P-24, round 9).
25
+ * Every other target's agent is on the host's server, or on the one its
26
+ * sandbox names.
27
+ */
28
+ export function agentServerOf(sandbox, serverUrl) {
29
+ if (sandbox.target === 'datalayer') {
30
+ return sandbox.agentBaseUrl || undefined;
31
+ }
32
+ return sandbox.agentBaseUrl || serverUrl;
33
+ }
34
+ /**
35
+ * What is said while {@link agentServerOf} names no server: the runtime is
36
+ * being assigned, or — its launch refused, the pool empty — none is, and
37
+ * why. One sentence for the chat, the page and the computer alike.
38
+ */
39
+ export function noRuntimeSaid(sandbox, words) {
40
+ return sandbox.state === 'error'
41
+ ? words.noRuntime(sandbox.errorReason ?? '')
42
+ : words.runtimeStarting;
43
+ }
17
44
  /**
18
45
  * What a view reads while the sandbox plugin is absent.
19
46
  *
@@ -5,7 +5,8 @@ import { Box } from '@datalayer/primer-addons';
5
5
  import { signal } from '@datalayer/reactor';
6
6
  import { useContributions, useSignalValue } from '@datalayer/reactor/react';
7
7
  import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
8
- import { LoopChatTurn, } from '../../core';
8
+ import { useChatWords } from '../../../chat/ChatLanguage';
9
+ import { LoopChatTurn, agentServerOf, noRuntimeSaid, IDLE_SANDBOX_SNAPSHOT, } from '../../core';
9
10
  import { COMPUTER_PARTS, COMPUTER_WORDS, computerParts, downloadComputerFile, handBackComputer, hasComputer, listComputerFiles, parentOf, partsOff, readComputer, runOnComputer, takeOverComputer, terminalEntries, } from '../../apps/computer';
10
11
  const NO_TURN = signal({ id: 0, status: 'idle' });
11
12
  const NO_CONVERSATION = signal([]);
@@ -109,9 +110,23 @@ export function AppComputer({ app, workspace, }) {
109
110
  setEnded(turn.id);
110
111
  }
111
112
  }, [settled, turn.id]);
112
- const serverUrl = workspace?.sandbox.agentBaseUrl || workspace?.serverUrl || '';
113
+ const chatText = useChatWords();
114
+ /*
115
+ * The runtime its agent is on, or nothing while none is assigned: then the
116
+ * computer is asked nothing and says why, rather than asking the host's
117
+ * server (on a hosted page, the page's own origin). Outside a workspace
118
+ * there is none either.
119
+ */
120
+ const serverUrl = workspace
121
+ ? agentServerOf(workspace.sandbox, workspace.serverUrl)
122
+ : undefined;
123
+ const noRuntime = serverUrl === undefined
124
+ ? noRuntimeSaid(workspace?.sandbox ?? IDLE_SANDBOX_SNAPSHOT, chatText)
125
+ : undefined;
113
126
  const agentId = workspace?.agentId || app.id;
114
- const context = useMemo(() => ({ serverUrl, agentId, token: token || undefined }), [serverUrl, agentId, token]);
127
+ const context = useMemo(() => serverUrl === undefined
128
+ ? undefined
129
+ : { serverUrl, agentId, token: token || undefined }, [serverUrl, agentId, token]);
115
130
  const parts = computerParts(app);
116
131
  const [state, setState] = useState();
117
132
  const [error, setError] = useState('');
@@ -125,6 +140,9 @@ export function AppComputer({ app, workspace, }) {
125
140
  };
126
141
  }, []);
127
142
  const read = useCallback(() => {
143
+ if (!context) {
144
+ return;
145
+ }
128
146
  readComputer(context).then(found => {
129
147
  if (alive.current) {
130
148
  setState(found);
@@ -135,11 +153,14 @@ export function AppComputer({ app, workspace, }) {
135
153
  }, [context]);
136
154
  const used = hasComputer(parts);
137
155
  useEffect(() => {
138
- if (used && serverUrl) {
156
+ if (used && context) {
139
157
  read();
140
158
  }
141
- }, [used, serverUrl, read, ended]);
159
+ }, [used, context, read, ended]);
142
160
  const act = (step) => {
161
+ if (!context) {
162
+ return;
163
+ }
143
164
  setBusy(true);
144
165
  step(context).then(() => {
145
166
  if (alive.current) {
@@ -154,10 +175,10 @@ export function AppComputer({ app, workspace, }) {
154
175
  });
155
176
  };
156
177
  const me = Boolean(state?.yours);
157
- return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.noBrowser }), error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state ? (_jsxs(Box, { mt: 2, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
178
+ return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.noBrowser }), noRuntime ? (_jsx(Text, { as: "p", role: "status", sx: { fontSize: 1, color: 'fg.default', m: 0, mt: 2 }, children: noRuntime })) : null, error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state && context ? (_jsxs(Box, { mt: 2, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
158
179
  ? COMPUTER_WORDS.agentHas
159
180
  : me
160
181
  ? COMPUTER_WORDS.youHave
161
- : COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, _jsx(Terminal, { conversation: conversation }), state ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
182
+ : COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, _jsx(Terminal, { conversation: conversation }), state && context ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
162
183
  }
163
184
  export default AppComputer;
@@ -5,7 +5,8 @@ import { Box } from '@datalayer/primer-addons';
5
5
  import { signal } from '@datalayer/reactor';
6
6
  import { useContributions, useSignalValue } from '@datalayer/reactor/react';
7
7
  import { catalogOfBlocks } from '../../../components/a2ui';
8
- import { LoopCanvasBlock, LoopChatTurn, } from '../../core';
8
+ import { useChatWords } from '../../../chat/ChatLanguage';
9
+ import { LoopCanvasBlock, LoopChatTurn, agentServerOf, noRuntimeSaid, } from '../../core';
9
10
  import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
10
11
  import { useOwnComponents } from '../a2ui-surface/ownComponents';
11
12
  import { appPageAction, appPageData, appPageMessages, pageInputsOf, pageOf, } from './appPageModel';
@@ -73,12 +74,25 @@ export function AppPage({ app, workspace }) {
73
74
  ...outputsData(outputs),
74
75
  }), [app, turn, conversation, outputs]);
75
76
  const token = useIAMStore(state => state.token) ?? '';
76
- const pageContext = useRef({ serverUrl: '', agentId: '' });
77
- pageContext.current = {
78
- serverUrl: workspace.sandbox?.agentBaseUrl || workspace.serverUrl || '',
79
- agentId: workspace.agentId || app.id,
80
- token: token || undefined,
81
- };
77
+ const chatText = useChatWords();
78
+ /*
79
+ * Where its page runs: the runtime its agent is on, or nothing while none
80
+ * is assigned — then a run is not sent and the page says why, rather than
81
+ * reaching for the host's server (on a hosted page, the page's own origin).
82
+ */
83
+ const pageServer = agentServerOf(workspace.sandbox, workspace.serverUrl);
84
+ const pageContext = useRef(null);
85
+ pageContext.current =
86
+ pageServer === undefined
87
+ ? null
88
+ : {
89
+ serverUrl: pageServer,
90
+ agentId: workspace.agentId || app.id,
91
+ token: token || undefined,
92
+ };
93
+ const noRuntime = useRef('');
94
+ noRuntime.current =
95
+ pageServer === undefined ? noRuntimeSaid(workspace.sandbox, chatText) : '';
82
96
  const pageSession = useRef(null);
83
97
  const pageRunning = useRef(false);
84
98
  const pageNext = useRef(null);
@@ -96,6 +110,10 @@ export function AppPage({ app, workspace }) {
96
110
  const next = pageNext.current;
97
111
  pageNext.current = null;
98
112
  const context = pageContext.current;
113
+ if (!context) {
114
+ setRefusal(noRuntime.current);
115
+ continue;
116
+ }
99
117
  try {
100
118
  pageSession.current ??= openPageSession(context);
101
119
  const turn = await runPageIn(context, await pageSession.current, next);
@@ -41,7 +41,7 @@ import { AGENTS_PLUGIN_NAME } from '../agents/plugin';
41
41
  import { useConfig } from '../../../hooks/useConfig';
42
42
  import { useSkills, useSkillActions } from '../../../hooks/useSkills';
43
43
  import { AI_MODEL_CATALOGUE, isChatModel } from '../../../specs/models';
44
- import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopCommand, LoopFrontendTool, LoopRunProps, LoopSlots, canOpenView, runForwardedProps, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
44
+ import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopCommand, LoopFrontendTool, LoopRunProps, LoopSlots, agentServerOf, noRuntimeSaid, canOpenView, runForwardedProps, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
45
45
  import { messageText, turnWritersOf } from './turnState';
46
46
  import { isInactiveSurfaceContribution, orderToolContributions, toolsForChatView, } from './chatViewTools';
47
47
  import { useAgentCommandTools } from '../../../tools/adapters/commands';
@@ -946,8 +946,12 @@ export default function ChatView({ workspace }) {
946
946
  * opened against. Addressing the latter would be talking to a machine that
947
947
  * has never heard of it. The same value for every other target, where the
948
948
  * two are the same server.
949
+ *
950
+ * Nothing while a Datalayer runtime is not assigned (the pool had none, or
951
+ * it is still being given): the chat is off and says so below, rather than
952
+ * sending to the host's server — on a hosted page, the page's own origin.
949
953
  */
950
- const agentServerUrl = workspace.sandbox.agentBaseUrl || workspace.serverUrl;
954
+ const agentServerUrl = agentServerOf(workspace.sandbox, workspace.serverUrl);
951
955
  /*
952
956
  * Who a single request may be addressed to.
953
957
  *
@@ -1041,12 +1045,25 @@ export default function ChatView({ workspace }) {
1041
1045
  * Studio's Preview and the hosted page, rather than left unanswered.
1042
1046
  */
1043
1047
  const ambient = useChatAvailability();
1044
- const chatDisabled = gateBlocked || keyExpired || Boolean(inPageRefusal) || ambient.disabled;
1048
+ /*
1049
+ * No agent to talk to: a Datalayer runtime not assigned (STUDIO P-24,
1050
+ * round 9). Said in a sentence — why, when its launch was refused — and
1051
+ * nothing is sent; the header does not say *Ready*.
1052
+ */
1053
+ const noRuntime = agentServerUrl === undefined
1054
+ ? noRuntimeSaid(workspace.sandbox, chatText)
1055
+ : undefined;
1056
+ const chatDisabled = gateBlocked ||
1057
+ keyExpired ||
1058
+ Boolean(inPageRefusal) ||
1059
+ Boolean(noRuntime) ||
1060
+ ambient.disabled;
1045
1061
  const disabledReason = keyExpired
1046
1062
  ? expiredKeyIsTemporary
1047
1063
  ? chatText.demoKeyExpired
1048
1064
  : chatText.keyExpired
1049
1065
  : (inPageRefusal ??
1066
+ noRuntime ??
1050
1067
  gateReason ??
1051
1068
  (ambient.disabled ? ambient.disableReason : undefined));
1052
1069
  /*
@@ -1250,6 +1267,10 @@ export default function ChatView({ workspace }) {
1250
1267
  ? openedThread.thread
1251
1268
  : undefined
1252
1269
  : givenThread;
1270
+ /*
1271
+ * None without a server (`noRuntime`): the chat is off and connects to
1272
+ * nothing — no address is made up for it.
1273
+ */
1253
1274
  const protocol = useMemo(() => inPage
1254
1275
  ? browserProtocolConfig({
1255
1276
  agentId: member?.specId ?? agentId,
@@ -1280,23 +1301,25 @@ export default function ChatView({ workspace }) {
1280
1301
  // its session API does on a runtime.
1281
1302
  modeEffect: blueprintTurn?.modeEffect,
1282
1303
  })
1283
- : {
1284
- type: 'ag-ui',
1285
- // An application's agent is spoken to through its session API
1286
- // (LOOP R-04): each thread a session, recorded under its uid,
1287
- // run in the name the application runs in, and taking what the
1288
- // page did besides the message — the same AG-UI events back.
1289
- endpoint: runsApp
1290
- ? `${agentServerUrl}/api/v1/apps/agents/${encodeURIComponent(agentId)}/ag-ui/`
1291
- : `${agentServerUrl}/api/v1/ag-ui/${agentId}/`,
1292
- ...(runsApp && iamToken ? { authToken: iamToken } : {}),
1293
- agentId,
1294
- // `/api/v1/configure`, not `/api/v1/configure/config`: the hooks
1295
- // strip one trailing `config`/`configure` segment to find the API
1296
- // base, so the longer form doubles it.
1297
- configEndpoint: `${agentServerUrl}/api/v1/configure`,
1298
- enableConfigQuery: true,
1299
- }, [
1304
+ : agentServerUrl !== undefined
1305
+ ? {
1306
+ type: 'ag-ui',
1307
+ // An application's agent is spoken to through its session API
1308
+ // (LOOP R-04): each thread a session, recorded under its uid,
1309
+ // run in the name the application runs in, and taking what the
1310
+ // page did besides the message — the same AG-UI events back.
1311
+ endpoint: runsApp
1312
+ ? `${agentServerUrl}/api/v1/apps/agents/${encodeURIComponent(agentId)}/ag-ui/`
1313
+ : `${agentServerUrl}/api/v1/ag-ui/${agentId}/`,
1314
+ ...(runsApp && iamToken ? { authToken: iamToken } : {}),
1315
+ agentId,
1316
+ // `/api/v1/configure`, not `/api/v1/configure/config`: the hooks
1317
+ // strip one trailing `config`/`configure` segment to find the API
1318
+ // base, so the longer form doubles it.
1319
+ configEndpoint: `${agentServerUrl}/api/v1/configure`,
1320
+ enableConfigQuery: true,
1321
+ }
1322
+ : undefined, [
1300
1323
  agentId,
1301
1324
  inPage,
1302
1325
  inference,
@@ -1321,7 +1344,7 @@ export default function ChatView({ workspace }) {
1321
1344
  * need one, and a browser agent has none — hence the menus below being
1322
1345
  * gated on the data rather than switched on unconditionally.
1323
1346
  */
1324
- const configQuery = useConfig(!inPage, protocol.type === 'ag-ui' ? protocol.configEndpoint : undefined, undefined, agentId);
1347
+ const configQuery = useConfig(!inPage, protocol?.type === 'ag-ui' ? protocol.configEndpoint : undefined, undefined, agentId);
1325
1348
  const skillsQuery = useSkills(true);
1326
1349
  const skillActions = useSkillActions(agentId);
1327
1350
  // `undefined` until the runtime answers: its answer is then the list.
@@ -1338,7 +1361,7 @@ export default function ChatView({ workspace }) {
1338
1361
  * catalogue that was never going to arrive. The agentspecs catalogue below
1339
1362
  * is the answer for this case and needs no request at all.
1340
1363
  */
1341
- if (inPage) {
1364
+ if (inPage || agentServerUrl === undefined) {
1342
1365
  return undefined;
1343
1366
  }
1344
1367
  let cancelled = false;
@@ -1424,7 +1447,7 @@ export default function ChatView({ workspace }) {
1424
1447
  */
1425
1448
  const selectModel = useCallback(async (model) => {
1426
1449
  setPickedModel(model);
1427
- if (inPage) {
1450
+ if (inPage || agentServerUrl === undefined) {
1428
1451
  return;
1429
1452
  }
1430
1453
  await fetch(`${agentServerUrl}/api/v1/configure/inference/provider`, {
@@ -1663,7 +1686,9 @@ export default function ChatView({ workspace }) {
1663
1686
  brandIcon: presence?.face ? (_jsx(PresenceFace, { face: presence.face, size: 20, state: presenceNow })) : (_jsx(BrandIcon, { size: 20 })),
1664
1687
  // An application says what it is doing, in plain words, beside
1665
1688
  // its name (T-08); an agent's chat keeps its header as it is.
1666
- headerContent: presence ? _jsx(PresenceLine, { state: presenceNow }) : undefined, emptyState: {
1689
+ headerContent:
1690
+ // Not *Ready* with no runtime: the header says why instead.
1691
+ presence && !noRuntime ? (_jsx(PresenceLine, { state: presenceNow })) : undefined, emptyState: {
1667
1692
  // An application's own face, at the page's size of the three
1668
1693
  // (LOOP T-08, T-19): the thing a person's eye lands on first.
1669
1694
  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 })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.90",
3
+ "version": "1.3.91",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",