@datalayer/agent-runtimes 1.3.74 → 1.3.78

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.
Files changed (143) hide show
  1. package/lib/agents/AgentDetails.d.ts +16 -2
  2. package/lib/agents/AgentDetails.js +381 -272
  3. package/lib/chat/ChatFloating.d.ts +27 -1
  4. package/lib/chat/ChatFloating.js +97 -13
  5. package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
  6. package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
  7. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  8. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  9. package/lib/chat/assistant/AssistantStage.d.ts +144 -4
  10. package/lib/chat/assistant/AssistantStage.js +280 -10
  11. package/lib/chat/assistant/BalloonParts.d.ts +33 -5
  12. package/lib/chat/assistant/BalloonParts.js +66 -44
  13. package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
  14. package/lib/chat/assistant/BalloonVisual.js +4 -3
  15. package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
  16. package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
  17. package/lib/chat/assistant/SpeechBalloon.d.ts +54 -6
  18. package/lib/chat/assistant/SpeechBalloon.js +73 -22
  19. package/lib/chat/assistant/assistantDetails.d.ts +84 -0
  20. package/lib/chat/assistant/assistantDetails.js +140 -0
  21. package/lib/chat/assistant/formats/files.d.ts +23 -0
  22. package/lib/chat/assistant/formats/files.js +48 -0
  23. package/lib/chat/assistant/formats/index.d.ts +2 -0
  24. package/lib/chat/assistant/formats/index.js +1 -0
  25. package/lib/chat/assistant/state.d.ts +7 -14
  26. package/lib/chat/assistant/state.js +21 -20
  27. package/lib/chat/assistant/toolLine.d.ts +16 -0
  28. package/lib/chat/assistant/toolLine.js +46 -0
  29. package/lib/chat/base/ChatBase.js +71 -1
  30. package/lib/chat/base/loopMessage.d.ts +37 -0
  31. package/lib/chat/base/loopMessage.js +37 -0
  32. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  33. package/lib/chat/indicators/TypingDots.js +21 -0
  34. package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
  35. package/lib/chat/messages/ChatMarkdown.js +18 -0
  36. package/lib/chat/messages/ChatMessageList.d.ts +8 -1
  37. package/lib/chat/messages/ChatMessageList.js +25 -33
  38. package/lib/chat/styles/streamdownStyles.js +3 -1
  39. package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
  40. package/lib/chat/tools/ToolCallDisplay.js +8 -7
  41. package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
  42. package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
  43. package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
  44. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  45. package/lib/components/inspector/AgentInspector.js +220 -0
  46. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  47. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  48. package/lib/components/inspector/JsonTree.d.ts +24 -0
  49. package/lib/components/inspector/JsonTree.js +89 -0
  50. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  51. package/lib/components/inspector/a2aInspect.js +478 -0
  52. package/lib/components/inspector/agentInspector.d.ts +179 -0
  53. package/lib/components/inspector/agentInspector.js +267 -0
  54. package/lib/components/inspector/chatInspect.d.ts +38 -0
  55. package/lib/components/inspector/chatInspect.js +65 -0
  56. package/lib/components/inspector/classify.d.ts +19 -0
  57. package/lib/components/inspector/classify.js +42 -0
  58. package/lib/components/inspector/index.d.ts +14 -0
  59. package/lib/components/inspector/index.js +18 -0
  60. package/lib/components/teams/A2ATeamGraph.d.ts +69 -1
  61. package/lib/components/teams/A2ATeamGraph.js +277 -37
  62. package/lib/components/teams/NotebookPreviewView.js +9 -1
  63. package/lib/components/teams/TeamNotebook.d.ts +3 -1
  64. package/lib/components/teams/TeamNotebook.js +3 -3
  65. package/lib/components/teams/TeamNotebookView.js +15 -0
  66. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  67. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  68. package/lib/components/teams/teamConnections.d.ts +9 -1
  69. package/lib/components/teams/teamConnections.js +20 -0
  70. package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
  71. package/lib/components/teams/teamNotebookKernel.js +17 -0
  72. package/lib/components/teams/useA2ATeam.d.ts +20 -3
  73. package/lib/components/teams/useA2ATeam.js +108 -23
  74. package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
  75. package/lib/examples/AgentA2ATeamExample.js +67 -13
  76. package/lib/examples/AssistantExample.js +65 -26
  77. package/lib/examples/AssistantGalleryExample.js +2 -1
  78. package/lib/examples/exampleGroups.d.ts +1 -1
  79. package/lib/examples/exampleGroups.js +1 -1
  80. package/lib/examples/utils/assistantGallery.d.ts +7 -6
  81. package/lib/examples/utils/assistantGallery.js +23 -8
  82. package/lib/jupyter/exec/Python.d.ts +0 -1
  83. package/lib/jupyter/exec/Python.js +0 -32
  84. package/lib/jupyter/exec/Snippets.d.ts +4 -12
  85. package/lib/jupyter/exec/Snippets.js +0 -8
  86. package/lib/loop/apps/AppFeedback.js +3 -1
  87. package/lib/loop/apps/AppKept.d.ts +22 -0
  88. package/lib/loop/apps/AppKept.js +36 -0
  89. package/lib/loop/apps/AppRenderer.d.ts +11 -1
  90. package/lib/loop/apps/AppRenderer.js +22 -4
  91. package/lib/loop/apps/checks.d.ts +7 -0
  92. package/lib/loop/apps/checks.js +39 -0
  93. package/lib/loop/apps/index.d.ts +3 -0
  94. package/lib/loop/apps/index.js +3 -0
  95. package/lib/loop/apps/kept.d.ts +30 -0
  96. package/lib/loop/apps/kept.js +52 -0
  97. package/lib/loop/apps/records.d.ts +12 -4
  98. package/lib/loop/apps/records.js +13 -5
  99. package/lib/loop/apps/saved.d.ts +30 -0
  100. package/lib/loop/apps/saved.js +40 -0
  101. package/lib/loop/core/index.d.ts +17 -0
  102. package/lib/loop/core/index.js +1 -0
  103. package/lib/loop/embed/AppEmbed.d.ts +8 -1
  104. package/lib/loop/embed/AppEmbed.js +4 -4
  105. package/lib/loop/embed/LoopEmbed.d.ts +6 -1
  106. package/lib/loop/embed/LoopEmbed.js +4 -2
  107. package/lib/loop/embed/element.js +1 -1
  108. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
  109. package/lib/loop/plugins/agents/browserService.d.ts +7 -1
  110. package/lib/loop/plugins/agents/browserService.js +4 -0
  111. package/lib/loop/plugins/agents/plugin.d.ts +16 -0
  112. package/lib/loop/plugins/agents/switchable.d.ts +3 -0
  113. package/lib/loop/plugins/agents/switchable.js +1 -0
  114. package/lib/loop/plugins/app-activity/index.js +3 -0
  115. package/lib/loop/plugins/app-computer/index.js +3 -0
  116. package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
  117. package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
  118. package/lib/loop/plugins/app-rules/index.js +3 -0
  119. package/lib/loop/plugins/assistant/index.d.ts +7 -2
  120. package/lib/loop/plugins/assistant/index.js +58 -4
  121. package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
  122. package/lib/loop/plugins/assistant-characters/index.js +17 -0
  123. package/lib/loop/plugins/chat/ChatView.js +4 -1
  124. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
  125. package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
  126. package/lib/loop/presets.d.ts +4 -0
  127. package/lib/loop/presets.js +3 -1
  128. package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
  129. package/lib/loop/shell/LoopWorkspace.js +5 -4
  130. package/lib/loop/shell/SidebarRail.d.ts +53 -0
  131. package/lib/loop/shell/SidebarRail.js +74 -0
  132. package/lib/loop/shell/index.d.ts +2 -0
  133. package/lib/loop/shell/index.js +2 -0
  134. package/lib/loop/shell/paneMotion.d.ts +21 -0
  135. package/lib/loop/shell/paneMotion.js +37 -0
  136. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  137. package/lib/specs/apps.js +8 -5
  138. package/lib/specs/uiPlugins.js +529 -0
  139. package/lib/types/agentspecs.d.ts +5 -3
  140. package/lib/types/chat.d.ts +12 -0
  141. package/package.json +2 -2
  142. package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
  143. package/scripts/codegen/generate_ui_plugins.py +112 -2
@@ -0,0 +1,140 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The statuses of a sandbox that has something to read. */
6
+ const LIVE = new Set(['running', 'idle', 'busy', 'starting']);
7
+ /** Whether a sandbox is up: one with no status is taken to be. */
8
+ export function sandboxIsLive(sandbox) {
9
+ return sandbox.status === undefined || LIVE.has(sandbox.status);
10
+ }
11
+ /** What the dialog says of a sandbox that is not running yet. */
12
+ export const NO_SANDBOX_YET = 'No sandbox is running yet: run a cell or ask the agent to execute code.';
13
+ /** What each kind is called. */
14
+ export const SANDBOX_KIND_LABELS = {
15
+ browser: 'Python in your browser (Pyodide)',
16
+ local: 'Local Jupyter server',
17
+ jupyter: 'Jupyter server',
18
+ cloud: 'Datalayer cloud runtime',
19
+ runtime: 'In the agent runtime',
20
+ };
21
+ /** Whether a URL is on this machine. */
22
+ export function isLocalUrl(url) {
23
+ if (!url) {
24
+ return false;
25
+ }
26
+ try {
27
+ const host = new URL(url).hostname;
28
+ return (host === 'localhost' ||
29
+ host === '127.0.0.1' ||
30
+ host === '::1' ||
31
+ host === '[::1]' ||
32
+ host.endsWith('.localhost'));
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /** Query parameters that carry a secret. */
39
+ const SECRET_PARAMS = /^(token|access_token|api_key|apikey|key|auth|password|secret|signature|sig|jwt)$/i;
40
+ /** A URL fit to show: no credentials, no token in its query. */
41
+ export function displayUrl(url) {
42
+ if (!url) {
43
+ return undefined;
44
+ }
45
+ try {
46
+ const parsed = new URL(url);
47
+ parsed.username = '';
48
+ parsed.password = '';
49
+ for (const name of [...parsed.searchParams.keys()]) {
50
+ if (SECRET_PARAMS.test(name)) {
51
+ parsed.searchParams.delete(name);
52
+ }
53
+ }
54
+ return parsed.toString();
55
+ }
56
+ catch {
57
+ // Not a URL: shown up to its query, which is where a token would be.
58
+ return url.split('?')[0];
59
+ }
60
+ }
61
+ const text = (value) => typeof value === 'string' && value ? value : undefined;
62
+ /**
63
+ * A sandbox from the status its server reports — the Loop's sandbox
64
+ * service, or the agent's codemode status — and what the host adds.
65
+ */
66
+ export function assistantSandboxOf(status, options = {}) {
67
+ if (!status) {
68
+ return undefined;
69
+ }
70
+ const variant = text(status.variant);
71
+ const url = text(status.jupyter_url) ?? text(status.agent_base_url);
72
+ const running = status.sandbox_running === true || status.jupyter_connected === true;
73
+ if (!variant && !running) {
74
+ return undefined;
75
+ }
76
+ const kind = variant === 'datalayer'
77
+ ? 'cloud'
78
+ : variant === 'jupyter-server' || variant === 'jupyter'
79
+ ? isLocalUrl(text(status.jupyter_url))
80
+ ? 'local'
81
+ : 'jupyter'
82
+ : 'runtime';
83
+ return {
84
+ kind,
85
+ status: options.state ?? (running ? 'running' : 'stopped'),
86
+ variant,
87
+ url,
88
+ token: text(status.jupyter_token),
89
+ kernelId: text(status.kernel_id),
90
+ agentId: options.agentId,
91
+ environment: text(status.environment_name),
92
+ serverUrl: options.serverUrl,
93
+ };
94
+ }
95
+ /**
96
+ * Run code in an agent's sandbox through its agent-runtimes server
97
+ * (`POST /api/v1/sandbox/execute`), whatever the variant: what a page with
98
+ * no kernel connection to the sandbox lists its variables with.
99
+ */
100
+ export function sandboxExecuteOverHttp(serverUrl, agentId) {
101
+ const base = serverUrl.replace(/\/+$/, '').replace(/\/api\/v1$/, '');
102
+ return async (code) => {
103
+ const response = await fetch(`${base}/api/v1/sandbox/execute`, {
104
+ method: 'POST',
105
+ headers: { 'content-type': 'application/json' },
106
+ body: JSON.stringify({ code, agent_id: agentId }),
107
+ });
108
+ if (!response.ok) {
109
+ return {
110
+ stdout: '',
111
+ error: `The sandbox did not run the code (HTTP ${response.status}).`,
112
+ };
113
+ }
114
+ const payload = (await response.json());
115
+ return { stdout: payload.stdout ?? '', error: payload.error ?? undefined };
116
+ };
117
+ }
118
+ /** How long until a moment, for a reader: `1 h 20 min`, `never`. */
119
+ export function timeLeft(expiresAt, now = Date.now()) {
120
+ if (expiresAt === null) {
121
+ return 'never';
122
+ }
123
+ if (expiresAt === undefined || expiresAt === '') {
124
+ return undefined;
125
+ }
126
+ const at = typeof expiresAt === 'number'
127
+ ? expiresAt < 1e12
128
+ ? expiresAt * 1000
129
+ : expiresAt
130
+ : Date.parse(expiresAt);
131
+ if (!Number.isFinite(at)) {
132
+ return undefined;
133
+ }
134
+ const minutes = Math.round((at - now) / 60000);
135
+ if (minutes <= 0) {
136
+ return 'ended';
137
+ }
138
+ const hours = Math.floor(minutes / 60);
139
+ return hours ? `${hours} h ${minutes % 60} min` : `${minutes} min`;
140
+ }
@@ -0,0 +1,23 @@
1
+ import { type AssistantCharacterData } from './types';
2
+ /** What a file picker for a character accepts. */
3
+ export declare const CHARACTER_FILES_ACCEPT = ".acs,.js,.png,.gif,.webp,.jpg,.jpeg";
4
+ /** The sentence for a pick that is not a character. */
5
+ export declare const CHARACTER_FILES_EXPECTED = "Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).";
6
+ /** A picked file, as much of `File` as the reader needs. */
7
+ export type PickedFile = Blob & {
8
+ name: string;
9
+ };
10
+ /** Which of the picked files make the character, or why none do. */
11
+ export declare function characterFilesOf(files: ReadonlyArray<PickedFile>): {
12
+ kind: 'acs';
13
+ acs: PickedFile;
14
+ } | {
15
+ kind: 'clippy';
16
+ agentJs: PickedFile;
17
+ map: PickedFile;
18
+ sounds?: PickedFile;
19
+ } | {
20
+ problem: string;
21
+ };
22
+ /** Read the picked files as a character; refuses in a sentence. */
23
+ export declare function readCharacterFiles(files: ReadonlyArray<PickedFile>): Promise<AssistantCharacterData>;
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The files a person picked, read as one character (LOOP T-26): one
7
+ * Microsoft Agent `.acs`, or a clippy.js character's `agent.js` and map
8
+ * image (and its sounds file, if there is one). Read in the page; nothing is
9
+ * sent anywhere.
10
+ *
11
+ * @module chat/assistant/formats/files
12
+ */
13
+ import { readAcsCharacter } from './acs';
14
+ import { readClippyCharacter } from './clippy';
15
+ import { AssistantCharacterFormatError, } from './types';
16
+ /** What a file picker for a character accepts. */
17
+ export const CHARACTER_FILES_ACCEPT = '.acs,.js,.png,.gif,.webp,.jpg,.jpeg';
18
+ /** The sentence for a pick that is not a character. */
19
+ export const CHARACTER_FILES_EXPECTED = 'Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).';
20
+ /** Which of the picked files make the character, or why none do. */
21
+ export function characterFilesOf(files) {
22
+ const acs = files.find(file => file.name.toLowerCase().endsWith('.acs'));
23
+ if (acs) {
24
+ return { kind: 'acs', acs };
25
+ }
26
+ const agentJs = files.find(file => file.name.toLowerCase() === 'agent.js');
27
+ const map = files.find(file => /\.(png|gif|webp|jpe?g)$/i.test(file.name));
28
+ const sounds = files.find(file => /^sounds-.*\.js$/i.test(file.name));
29
+ if (!agentJs || !map) {
30
+ return { problem: CHARACTER_FILES_EXPECTED };
31
+ }
32
+ return { kind: 'clippy', agentJs, map, ...(sounds ? { sounds } : {}) };
33
+ }
34
+ /** Read the picked files as a character; refuses in a sentence. */
35
+ export async function readCharacterFiles(files) {
36
+ const picked = characterFilesOf(files);
37
+ if ('problem' in picked) {
38
+ throw new AssistantCharacterFormatError(picked.problem);
39
+ }
40
+ if (picked.kind === 'acs') {
41
+ return readAcsCharacter(await picked.acs.arrayBuffer());
42
+ }
43
+ return readClippyCharacter({
44
+ agentJs: await picked.agentJs.text(),
45
+ mapPng: picked.map,
46
+ soundsJs: picked.sounds ? await picked.sounds.text() : undefined,
47
+ });
48
+ }
@@ -11,3 +11,5 @@ export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcs
11
11
  export type { AcsFile, AcsAnimation, AcsFrame, AcsImage } from './acs';
12
12
  export { decompressAgentData } from './agentCompression';
13
13
  export { stateAnimations } from './stateAnimations';
14
+ export { CHARACTER_FILES_ACCEPT, CHARACTER_FILES_EXPECTED, characterFilesOf, readCharacterFiles, } from './files';
15
+ export type { PickedFile } from './files';
@@ -13,3 +13,4 @@ export { readClippyCharacter, readClippyAgent, readClippySounds, } from './clipp
13
13
  export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, } from './acs';
14
14
  export { decompressAgentData } from './agentCompression';
15
15
  export { stateAnimations } from './stateAnimations';
16
+ export { CHARACTER_FILES_ACCEPT, CHARACTER_FILES_EXPECTED, characterFilesOf, readCharacterFiles, } from './files';
@@ -76,6 +76,11 @@ export interface AssistantSaying {
76
76
  text: string;
77
77
  /** Whether there was more than the balloon holds. */
78
78
  more: boolean;
79
+ /**
80
+ * Its words as written, markdown and all: what the `current` balloon
81
+ * draws, through the chat's markdown.
82
+ */
83
+ markdown: string;
79
84
  }
80
85
  /** How much of a message is kept as the agent's saying before it is cut. */
81
86
  export declare const SAYING_LIMIT = 220;
@@ -84,20 +89,8 @@ export declare const SAYING_LIMIT = 220;
84
89
  * code blocks left out, links and Markdown read as plain words.
85
90
  */
86
91
  export declare function plainWordsOf(content: unknown): string;
87
- /** A message of the conversation, as the `history` balloon lists it. */
88
- export interface BalloonHistoryMessage {
89
- id: string;
90
- /** Who said it: the person, or the agent. */
91
- role: 'user' | 'assistant';
92
- /** Its words, plain. */
93
- text: string;
94
- }
95
- /**
96
- * The conversation as the `history` balloon lists it (T-23): the person's
97
- * and the agent's messages with words, in order — not the tool calls, not
98
- * an empty message — the same messages its header counts.
99
- */
100
- export declare function balloonHistoryOf(items: readonly unknown[]): BalloonHistoryMessage[];
92
+ /** A message's text parts as written, markdown and all. */
93
+ export declare function markdownOf(content: unknown): string;
101
94
  /**
102
95
  * The newest thing the agent said, as the Office Assistant said it: in the
103
96
  * balloon, while the conversation is closed (T-23). Only its own messages
@@ -75,25 +75,19 @@ export function plainWordsOf(content) {
75
75
  .replace(/\s+/g, ' ')
76
76
  .trim();
77
77
  }
78
- /**
79
- * The conversation as the `history` balloon lists it (T-23): the person's
80
- * and the agent's messages with words, in order — not the tool calls, not
81
- * an empty message — the same messages its header counts.
82
- */
83
- export function balloonHistoryOf(items) {
84
- const history = [];
85
- items.forEach((entry, index) => {
86
- const item = entry;
87
- if ((item.role !== 'user' && item.role !== 'assistant') ||
88
- typeof item.toolName === 'string') {
89
- return;
90
- }
91
- const text = plainWordsOf(item.content);
92
- if (text) {
93
- history.push({ id: String(item.id ?? index), role: item.role, text });
94
- }
95
- });
96
- return history;
78
+ /** A message's text parts as written, markdown and all. */
79
+ export function markdownOf(content) {
80
+ return typeof content === 'string'
81
+ ? content
82
+ : Array.isArray(content)
83
+ ? content
84
+ .map(part => part &&
85
+ typeof part === 'object' &&
86
+ part.type === 'text'
87
+ ? String(part.text ?? '')
88
+ : '')
89
+ .join('\n\n')
90
+ : '';
97
91
  }
98
92
  /**
99
93
  * The newest thing the agent said, as the Office Assistant said it: in the
@@ -111,8 +105,14 @@ export function latestSaying(items) {
111
105
  if (!plain) {
112
106
  continue;
113
107
  }
108
+ const markdown = markdownOf(item.content);
114
109
  if (plain.length <= SAYING_LIMIT) {
115
- return { id: String(item.id ?? index), text: plain, more: false };
110
+ return {
111
+ id: String(item.id ?? index),
112
+ text: plain,
113
+ more: false,
114
+ markdown,
115
+ };
116
116
  }
117
117
  const cut = plain.slice(0, SAYING_LIMIT);
118
118
  const atWord = cut.slice(0, Math.max(cut.lastIndexOf(' '), SAYING_LIMIT * 0.6));
@@ -120,6 +120,7 @@ export function latestSaying(items) {
120
120
  id: String(item.id ?? index),
121
121
  text: `${atWord.trimEnd()}…`,
122
122
  more: true,
123
+ markdown,
123
124
  };
124
125
  }
125
126
  return undefined;
@@ -1,3 +1,4 @@
1
+ import type { DisplayItem, ToolCallMessage } from '../../types/chat';
1
2
  /**
2
3
  * How the balloon shows the conversation: `history`, every message,
3
4
  * scrolled, the composer last; `current`, only what it says or does now, in
@@ -67,3 +68,18 @@ export declare function toolAnnouncement(line: BalloonToolLine | undefined, prev
67
68
  * header counts them: the person's and the agent's, not the tool calls.
68
69
  */
69
70
  export declare function conversationCount(items: readonly unknown[]): number;
71
+ /**
72
+ * A tool line as the chat holds it: a tool call (`ToolCallMessage`), drawn
73
+ * by the chat's own tool card in the balloon, its runtime's own words
74
+ * (`Asking Accounting…`) as its summary; or, when it names no tool — what
75
+ * the agent is doing, in its own words only — the agent's message saying
76
+ * so.
77
+ */
78
+ export declare function displayItemOfLine(line: BalloonToolLine): DisplayItem;
79
+ /** A tool line as the chat's own tool call (`ToolCallMessage`). */
80
+ export declare function toolCallOfLine(line: BalloonToolLine): ToolCallMessage;
81
+ /**
82
+ * The conversation with the tool being called, when the conversation does
83
+ * not hold that call already: what the history balloon lists.
84
+ */
85
+ export declare function withToolCall(items: readonly DisplayItem[], line: BalloonToolLine | undefined): readonly DisplayItem[];
@@ -116,3 +116,49 @@ export function conversationCount(items) {
116
116
  typeof message.toolName !== 'string');
117
117
  }).length;
118
118
  }
119
+ /**
120
+ * A tool line as the chat holds it: a tool call (`ToolCallMessage`), drawn
121
+ * by the chat's own tool card in the balloon, its runtime's own words
122
+ * (`Asking Accounting…`) as its summary; or, when it names no tool — what
123
+ * the agent is doing, in its own words only — the agent's message saying
124
+ * so.
125
+ */
126
+ export function displayItemOfLine(line) {
127
+ if (!line.tool && !line.name) {
128
+ return {
129
+ id: `balloon-activity:${line.id}`,
130
+ role: 'assistant',
131
+ content: line.words ?? '',
132
+ createdAt: new Date(0),
133
+ };
134
+ }
135
+ return toolCallOfLine(line);
136
+ }
137
+ /** A tool line as the chat's own tool call (`ToolCallMessage`). */
138
+ export function toolCallOfLine(line) {
139
+ return {
140
+ id: `balloon-tool:${line.id}`,
141
+ type: 'tool-call',
142
+ toolCallId: line.id,
143
+ toolName: line.tool || line.name,
144
+ args: {},
145
+ status: line.phase === 'failed'
146
+ ? 'error'
147
+ : line.phase === 'done'
148
+ ? 'complete'
149
+ : 'executing',
150
+ ...(line.words ? { summary: line.words } : {}),
151
+ };
152
+ }
153
+ /**
154
+ * The conversation with the tool being called, when the conversation does
155
+ * not hold that call already: what the history balloon lists.
156
+ */
157
+ export function withToolCall(items, line) {
158
+ if (!line ||
159
+ items.some(item => item.toolCallId === line.id ||
160
+ item.id === line.id)) {
161
+ return items;
162
+ }
163
+ return [...items, displayItemOfLine(line)];
164
+ }
@@ -58,6 +58,9 @@ const EphemeralDocument = React.lazy(() => import('../document/EphemeralDocument
58
58
  default: m.EphemeralDocument,
59
59
  })));
60
60
  import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHooks';
61
+ import { useAgentInspectorSink } from '../../components/inspector/agentInspector';
62
+ import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
63
+ import { loopMessageChange, speakerOf, withLoopMessage } from './loopMessage';
61
64
  // Tracks pending prompts already auto-sent for a given conversation scope.
62
65
  // This prevents layout-driven unmount/remount cycles from re-sending prompts.
63
66
  const sentPendingPromptKeys = new Set();
@@ -513,7 +516,7 @@ useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady
513
516
  // Extended props
514
517
  brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
515
518
  // Tool invocation hooks
516
- onToolCallStart, onToolCallComplete,
519
+ onToolCallStart, onToolCallComplete, inspector: inspectorGiven,
517
520
  // Identity/Authorization props
518
521
  onAuthorizationRequired: _onAuthorizationRequired, connectedIdentities,
519
522
  // Conversation persistence
@@ -1589,6 +1592,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1589
1592
  onToolCallStartRef.current = onToolCallStart;
1590
1593
  const onToolCallCompleteRef = useRef(onToolCallComplete);
1591
1594
  onToolCallCompleteRef.current = onToolCallComplete;
1595
+ // The Agent Inspector: the turns and tool calls, recorded as they happen.
1596
+ const inspectorFromContext = useAgentInspectorSink();
1597
+ const inspectorSink = inspectorGiven ?? inspectorFromContext;
1598
+ const inspectorActorRef = useRef('Agent');
1599
+ inspectorActorRef.current =
1600
+ (typeof title === 'string' && title) || activeAgentId || 'Agent';
1601
+ const recorderRef = useRef(null);
1602
+ if (inspectorSink && recorderRef.current?.sink !== inspectorSink) {
1603
+ recorderRef.current = {
1604
+ sink: inspectorSink,
1605
+ recorder: chatInspectorRecorder(inspectorSink, () => inspectorActorRef.current, {
1606
+ frontendTools: () => (frontendToolsRef.current ?? []).map(tool => tool.name),
1607
+ }),
1608
+ };
1609
+ }
1610
+ else if (!inspectorSink) {
1611
+ recorderRef.current = null;
1612
+ }
1613
+ const inspectAnswerRef = useRef('');
1592
1614
  const handleRespondRef = useRef(null);
1593
1615
  // A decision made outside the chat — a host page's own approval banner —
1594
1616
  // is answered exactly as the tool card's buttons answer it: through the
@@ -2415,6 +2437,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2415
2437
  const sanitizedContent = typeof rawContent === 'string'
2416
2438
  ? sanitizeAssistantContent(rawContent)
2417
2439
  : (rawContent ?? '');
2440
+ if (typeof sanitizedContent === 'string' && sanitizedContent) {
2441
+ inspectAnswerRef.current = sanitizedContent;
2442
+ }
2418
2443
  setDisplayItems(prev => {
2419
2444
  const newItems = [...prev];
2420
2445
  const idx = newItems.findIndex(item => !isToolCallMessage(item) && item.id === currentId);
@@ -2520,6 +2545,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2520
2545
  toolCallId,
2521
2546
  args,
2522
2547
  });
2548
+ recorderRef.current?.recorder.toolStarted({
2549
+ id: toolCallId,
2550
+ name: toolName,
2551
+ args,
2552
+ });
2523
2553
  const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
2524
2554
  const toolHandler = frontendTool?.handler;
2525
2555
  // Execute when the args are complete. The protocol says so
@@ -2604,6 +2634,13 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2604
2634
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2605
2635
  ? updatedToolCall
2606
2636
  : item));
2637
+ recorderRef.current?.recorder.toolEnded(toolCallId, {
2638
+ result: event.toolResult.result,
2639
+ error: event.toolResult.error ??
2640
+ (updatedToolCall.status === 'error'
2641
+ ? (executionError ?? 'failed')
2642
+ : undefined),
2643
+ });
2607
2644
  // Fire post-hook for tool results
2608
2645
  onToolCallCompleteRef.current?.({
2609
2646
  toolName: existingToolCall.toolName,
@@ -2646,6 +2683,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2646
2683
  }
2647
2684
  }
2648
2685
  break;
2686
+ case 'activity': {
2687
+ // An application's code changed a message it sent: its author
2688
+ // said, or the message taken away (LOOP P-15).
2689
+ const change = loopMessageChange(event.activity);
2690
+ if (!change)
2691
+ break;
2692
+ setDisplayItems(prev => withLoopMessage(prev, change));
2693
+ if (useStoreMode) {
2694
+ if ('removed' in change) {
2695
+ useChatStore.getState().deleteMessage(change.id);
2696
+ }
2697
+ else {
2698
+ useChatStore.getState().updateMessage(change.id, {
2699
+ speaker: speakerOf(change.author),
2700
+ });
2701
+ }
2702
+ }
2703
+ break;
2704
+ }
2649
2705
  case 'done':
2650
2706
  /*
2651
2707
  * What the turn cost, when nobody else is counting.
@@ -2657,6 +2713,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2657
2713
  * The bar and its ring simply never appeared for a browser agent.
2658
2714
  */
2659
2715
  {
2716
+ recorderRef.current?.recorder.turnEnded({
2717
+ text: inspectAnswerRef.current || undefined,
2718
+ usage: event.usage,
2719
+ });
2720
+ inspectAnswerRef.current = '';
2660
2721
  const turnInput = event.usage?.promptTokens ?? 0;
2661
2722
  const turnOutput = event.usage?.completionTokens ?? 0;
2662
2723
  // The parts sum to the total when the harness did not send one:
@@ -2710,6 +2771,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2710
2771
  break;
2711
2772
  case 'error':
2712
2773
  console.error('[ChatBase] Protocol error:', event.error);
2774
+ recorderRef.current?.recorder.turnEnded({
2775
+ error: event.error?.message ?? 'error',
2776
+ });
2713
2777
  if (event.error?.message &&
2714
2778
  /exceeded maximum retries/i.test(event.error.message) &&
2715
2779
  hideMessagesAfterToolUIRef.current) {
@@ -2777,6 +2841,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2777
2841
  result,
2778
2842
  status: 'complete',
2779
2843
  };
2844
+ recorderRef.current?.recorder.toolEnded(toolCallId, { result });
2780
2845
  toolCallsRef.current.set(toolCallId, completedToolCall);
2781
2846
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2782
2847
  ? completedToolCall
@@ -2789,6 +2854,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2789
2854
  status: 'error',
2790
2855
  error: err.message,
2791
2856
  };
2857
+ recorderRef.current?.recorder.toolEnded(toolCallId, {
2858
+ error: err.message,
2859
+ });
2792
2860
  toolCallsRef.current.set(toolCallId, errorToolCall);
2793
2861
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2794
2862
  ? errorToolCall
@@ -2838,6 +2906,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2838
2906
  if (!adapterRef.current && !onSendMessage)
2839
2907
  return;
2840
2908
  stoppedRef.current = false;
2909
+ inspectAnswerRef.current = '';
2910
+ recorderRef.current?.recorder.turnStarted(messageContent, selectedModel || undefined);
2841
2911
  suppressAssistantTextForToolOnlyRef.current =
2842
2912
  isToolCallOnlyPrompt(messageContent);
2843
2913
  // Said, then sent: by the microphone at once, or from the composer it
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A message of an application's code that changed after it was sent
3
+ * (LOOP P-15).
4
+ *
5
+ * The session API says a message's author, or that it was taken away, with a
6
+ * `CUSTOM` event named `loop.message` — `{id, author}` or `{id, removed: true}`
7
+ * — which the AG-UI adapter hands on as an `activity`. A message whose text
8
+ * changed needs nothing here: it is written again under its id, and the chat
9
+ * draws it in place.
10
+ */
11
+ import type { MessageSpeaker } from '../../types/messages';
12
+ /** The `CUSTOM` event's name. */
13
+ export declare const LOOP_MESSAGE = "loop.message";
14
+ /** What changed about a message already in the conversation. */
15
+ export type LoopMessageChange = {
16
+ id: string;
17
+ removed: true;
18
+ } | {
19
+ id: string;
20
+ author: string;
21
+ };
22
+ /** The change an activity says, or `null` when it says none. */
23
+ export declare function loopMessageChange(activity: {
24
+ type: string;
25
+ data: unknown;
26
+ } | undefined): LoopMessageChange | null;
27
+ /** Who a message is by, as the chat draws a named voice. */
28
+ export declare function speakerOf(author: string): MessageSpeaker;
29
+ /**
30
+ * The conversation with the change made: the message taken away, or given its
31
+ * author. Items that are not that message — tool calls among them — are left
32
+ * as they are.
33
+ */
34
+ export declare function withLoopMessage<T extends {
35
+ id?: string;
36
+ speaker?: MessageSpeaker;
37
+ }>(items: T[], change: LoopMessageChange): T[];
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The `CUSTOM` event's name. */
6
+ export const LOOP_MESSAGE = 'loop.message';
7
+ /** The change an activity says, or `null` when it says none. */
8
+ export function loopMessageChange(activity) {
9
+ if (!activity || activity.type !== LOOP_MESSAGE)
10
+ return null;
11
+ const data = activity.data;
12
+ if (!data || typeof data.id !== 'string' || !data.id)
13
+ return null;
14
+ if (data.removed === true)
15
+ return { id: data.id, removed: true };
16
+ if (typeof data.author === 'string' && data.author.trim()) {
17
+ return { id: data.id, author: data.author.trim() };
18
+ }
19
+ return null;
20
+ }
21
+ /** Who a message is by, as the chat draws a named voice. */
22
+ export function speakerOf(author) {
23
+ return { id: `author:${author}`, name: author };
24
+ }
25
+ /**
26
+ * The conversation with the change made: the message taken away, or given its
27
+ * author. Items that are not that message — tool calls among them — are left
28
+ * as they are.
29
+ */
30
+ export function withLoopMessage(items, change) {
31
+ if ('removed' in change) {
32
+ return items.filter(item => item.id !== change.id);
33
+ }
34
+ return items.map(item => item.id === change.id
35
+ ? { ...item, speaker: speakerOf(change.author) }
36
+ : item);
37
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Three pulsing dots: a speaker is still at work and has not written yet —
3
+ * the chat's waiting indicator, and the assistant's balloon's. Still, but
4
+ * shown, for a reader who asks for reduced motion. Hidden from a screen
5
+ * reader: the live region beside it says the state.
6
+ *
7
+ * @module chat/indicators/TypingDots
8
+ */
9
+ import type { ReactElement } from 'react';
10
+ export declare function TypingDots({ size }: {
11
+ size?: number;
12
+ }): ReactElement;
13
+ export default TypingDots;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from '@datalayer/primer-addons';
3
+ export function TypingDots({ size = 8 }) {
4
+ return (_jsx(Box, { "aria-hidden": "true", "data-typing-dots": "", sx: { display: 'inline-flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
5
+ width: size,
6
+ height: size,
7
+ borderRadius: '50%',
8
+ bg: 'fg.muted',
9
+ animation: 'typingPulse 1.4s ease-in-out infinite',
10
+ animationDelay: `${delay}s`,
11
+ '@keyframes typingPulse': {
12
+ '0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
13
+ '30%': { transform: 'scale(1)', opacity: 1 },
14
+ },
15
+ '@media (prefers-reduced-motion: reduce)': {
16
+ animation: 'none',
17
+ opacity: 0.7,
18
+ },
19
+ } }, index))) }));
20
+ }
21
+ export default TypingDots;