@datalayer/agent-runtimes 1.3.75 → 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 (110) 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 +12 -2
  4. package/lib/chat/ChatFloating.js +48 -11
  5. package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
  6. package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
  7. package/lib/chat/assistant/AssistantStage.d.ts +51 -5
  8. package/lib/chat/assistant/AssistantStage.js +60 -10
  9. package/lib/chat/assistant/BalloonParts.d.ts +18 -4
  10. package/lib/chat/assistant/BalloonParts.js +46 -41
  11. package/lib/chat/assistant/BalloonVisual.js +2 -1
  12. package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
  13. package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
  14. package/lib/chat/assistant/SpeechBalloon.d.ts +8 -6
  15. package/lib/chat/assistant/SpeechBalloon.js +29 -30
  16. package/lib/chat/assistant/assistantDetails.d.ts +84 -0
  17. package/lib/chat/assistant/assistantDetails.js +140 -0
  18. package/lib/chat/assistant/formats/files.d.ts +23 -0
  19. package/lib/chat/assistant/formats/files.js +48 -0
  20. package/lib/chat/assistant/formats/index.d.ts +2 -0
  21. package/lib/chat/assistant/formats/index.js +1 -0
  22. package/lib/chat/assistant/state.d.ts +7 -14
  23. package/lib/chat/assistant/state.js +21 -20
  24. package/lib/chat/assistant/toolLine.d.ts +16 -0
  25. package/lib/chat/assistant/toolLine.js +46 -0
  26. package/lib/chat/base/ChatBase.js +20 -0
  27. package/lib/chat/base/loopMessage.d.ts +37 -0
  28. package/lib/chat/base/loopMessage.js +37 -0
  29. package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
  30. package/lib/chat/messages/ChatMarkdown.js +18 -0
  31. package/lib/chat/messages/ChatMessageList.d.ts +8 -1
  32. package/lib/chat/messages/ChatMessageList.js +24 -18
  33. package/lib/chat/styles/streamdownStyles.js +3 -1
  34. package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
  35. package/lib/chat/tools/ToolCallDisplay.js +8 -7
  36. package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
  37. package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
  38. package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
  39. package/lib/components/teams/A2ATeamGraph.d.ts +30 -3
  40. package/lib/components/teams/A2ATeamGraph.js +264 -41
  41. package/lib/components/teams/TeamNotebookView.js +15 -0
  42. package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
  43. package/lib/components/teams/teamNotebookKernel.js +17 -0
  44. package/lib/components/teams/useA2ATeam.d.ts +10 -6
  45. package/lib/components/teams/useA2ATeam.js +29 -8
  46. package/lib/examples/AgentA2ATeamExample.js +6 -0
  47. package/lib/examples/AssistantExample.js +3 -24
  48. package/lib/examples/AssistantGalleryExample.js +2 -1
  49. package/lib/examples/exampleGroups.d.ts +1 -1
  50. package/lib/examples/exampleGroups.js +1 -1
  51. package/lib/examples/utils/assistantGallery.d.ts +7 -6
  52. package/lib/examples/utils/assistantGallery.js +23 -8
  53. package/lib/jupyter/exec/Python.d.ts +0 -1
  54. package/lib/jupyter/exec/Python.js +0 -32
  55. package/lib/jupyter/exec/Snippets.d.ts +4 -12
  56. package/lib/jupyter/exec/Snippets.js +0 -8
  57. package/lib/loop/apps/AppFeedback.js +3 -1
  58. package/lib/loop/apps/AppKept.d.ts +22 -0
  59. package/lib/loop/apps/AppKept.js +36 -0
  60. package/lib/loop/apps/AppRenderer.d.ts +11 -1
  61. package/lib/loop/apps/AppRenderer.js +21 -3
  62. package/lib/loop/apps/checks.d.ts +7 -0
  63. package/lib/loop/apps/checks.js +39 -0
  64. package/lib/loop/apps/index.d.ts +3 -0
  65. package/lib/loop/apps/index.js +3 -0
  66. package/lib/loop/apps/kept.d.ts +30 -0
  67. package/lib/loop/apps/kept.js +52 -0
  68. package/lib/loop/apps/records.d.ts +12 -4
  69. package/lib/loop/apps/records.js +13 -5
  70. package/lib/loop/apps/saved.d.ts +30 -0
  71. package/lib/loop/apps/saved.js +40 -0
  72. package/lib/loop/embed/AppEmbed.d.ts +8 -1
  73. package/lib/loop/embed/AppEmbed.js +4 -4
  74. package/lib/loop/embed/LoopEmbed.d.ts +6 -1
  75. package/lib/loop/embed/LoopEmbed.js +4 -2
  76. package/lib/loop/embed/element.js +1 -1
  77. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
  78. package/lib/loop/plugins/agents/browserService.d.ts +7 -1
  79. package/lib/loop/plugins/agents/browserService.js +4 -0
  80. package/lib/loop/plugins/agents/plugin.d.ts +16 -0
  81. package/lib/loop/plugins/agents/switchable.d.ts +3 -0
  82. package/lib/loop/plugins/agents/switchable.js +1 -0
  83. package/lib/loop/plugins/app-activity/index.js +3 -0
  84. package/lib/loop/plugins/app-computer/index.js +3 -0
  85. package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
  86. package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
  87. package/lib/loop/plugins/app-rules/index.js +3 -0
  88. package/lib/loop/plugins/assistant/index.d.ts +7 -2
  89. package/lib/loop/plugins/assistant/index.js +53 -4
  90. package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
  91. package/lib/loop/plugins/assistant-characters/index.js +17 -0
  92. package/lib/loop/plugins/chat/ChatView.js +4 -1
  93. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
  94. package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
  95. package/lib/loop/presets.d.ts +4 -0
  96. package/lib/loop/presets.js +3 -1
  97. package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
  98. package/lib/loop/shell/LoopWorkspace.js +5 -4
  99. package/lib/loop/shell/SidebarRail.d.ts +53 -0
  100. package/lib/loop/shell/SidebarRail.js +74 -0
  101. package/lib/loop/shell/index.d.ts +2 -0
  102. package/lib/loop/shell/index.js +2 -0
  103. package/lib/loop/shell/paneMotion.d.ts +21 -0
  104. package/lib/loop/shell/paneMotion.js +37 -0
  105. package/lib/specs/apps.js +8 -5
  106. package/lib/specs/uiPlugins.js +529 -0
  107. package/lib/types/agentspecs.d.ts +5 -3
  108. package/lib/types/chat.d.ts +5 -0
  109. package/package.json +2 -2
  110. package/scripts/codegen/generate_ui_plugins.py +112 -2
@@ -0,0 +1,84 @@
1
+ /**
2
+ * What the assistant's menu knows of its agent and of its code sandbox, for
3
+ * *Agent Details…* and *Code Sandbox Details…*.
4
+ *
5
+ * Light on purpose: types and pure helpers only. The dialogs that draw them
6
+ * — `AgentDetails`, jupyter-react's `KernelVariables` — are loaded when one
7
+ * is opened, never with the character.
8
+ *
9
+ * @module chat/assistant/assistantDetails
10
+ */
11
+ import type { Kernel } from '@jupyterlab/services';
12
+ import type { KernelVariablesExecution } from '@datalayer/jupyter-react';
13
+ /** Where a sandbox runs. */
14
+ export type AssistantSandboxKind =
15
+ /** Pyodide, in this page. */
16
+ 'browser'
17
+ /** A Jupyter server on this machine, the agent's own. */
18
+ | 'local'
19
+ /** A Jupyter server elsewhere. */
20
+ | 'jupyter'
21
+ /** A Datalayer cloud runtime. */
22
+ | 'cloud'
23
+ /** In the agent runtime's process, or at a provider (`eval`, `e2b`, …). */
24
+ | 'runtime';
25
+ /** A code sandbox, as much of it as its host knows. */
26
+ export type AssistantSandbox = {
27
+ kind: AssistantSandboxKind;
28
+ /** `running`, `starting`, `idle`, `stopped`, `error`. */
29
+ status?: string;
30
+ /** The sandbox manager's variant: `jupyter-server`, `eval`, `datalayer`, a provider. */
31
+ variant?: string;
32
+ /** The Jupyter server, or the base the sandbox is reached at. Shown without its tokens. */
33
+ url?: string;
34
+ /** The token the Jupyter server wants: used to connect, never shown. */
35
+ token?: string;
36
+ /** The kernel the sandbox runs, on that server. */
37
+ kernelId?: string;
38
+ /** The agent whose sandbox it is (a local server's per-agent sandbox). */
39
+ agentId?: string;
40
+ /** The environment it runs in. */
41
+ environment?: string;
42
+ /** A cloud runtime's identity: its uid, never its pod's name. */
43
+ runtimeUid?: string;
44
+ /** When a cloud runtime ends; `null` when it is not metered. */
45
+ expiresAt?: string | number | null;
46
+ /** What a cloud runtime burns, in credits per hour. */
47
+ burningRate?: number;
48
+ startedAt?: string | number;
49
+ lastActivity?: string | number;
50
+ /** The kernel, when the page talks to it: listed live, widgets drawn. */
51
+ connection?: Kernel.IKernelConnection | null;
52
+ /**
53
+ * The agent-runtimes server whose `/api/v1/sandbox/execute` runs code in
54
+ * the agent's sandbox, when the page has no kernel connection to it.
55
+ */
56
+ serverUrl?: string;
57
+ };
58
+ /** Whether a sandbox is up: one with no status is taken to be. */
59
+ export declare function sandboxIsLive(sandbox: AssistantSandbox): boolean;
60
+ /** What the dialog says of a sandbox that is not running yet. */
61
+ export declare const NO_SANDBOX_YET = "No sandbox is running yet: run a cell or ask the agent to execute code.";
62
+ /** What each kind is called. */
63
+ export declare const SANDBOX_KIND_LABELS: Record<AssistantSandboxKind, string>;
64
+ /** Whether a URL is on this machine. */
65
+ export declare function isLocalUrl(url: string | undefined): boolean;
66
+ /** A URL fit to show: no credentials, no token in its query. */
67
+ export declare function displayUrl(url: string | undefined): string | undefined;
68
+ /**
69
+ * A sandbox from the status its server reports — the Loop's sandbox
70
+ * service, or the agent's codemode status — and what the host adds.
71
+ */
72
+ export declare function assistantSandboxOf(status: Record<string, unknown> | null | undefined, options?: {
73
+ agentId?: string;
74
+ serverUrl?: string;
75
+ state?: string;
76
+ }): AssistantSandbox | undefined;
77
+ /**
78
+ * Run code in an agent's sandbox through its agent-runtimes server
79
+ * (`POST /api/v1/sandbox/execute`), whatever the variant: what a page with
80
+ * no kernel connection to the sandbox lists its variables with.
81
+ */
82
+ export declare function sandboxExecuteOverHttp(serverUrl: string, agentId?: string): (code: string) => Promise<KernelVariablesExecution>;
83
+ /** How long until a moment, for a reader: `1 h 20 min`, `never`. */
84
+ export declare function timeLeft(expiresAt: string | number | null | undefined, now?: number): string | undefined;
@@ -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
+ }
@@ -60,6 +60,7 @@ const EphemeralDocument = React.lazy(() => import('../document/EphemeralDocument
60
60
  import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHooks';
61
61
  import { useAgentInspectorSink } from '../../components/inspector/agentInspector';
62
62
  import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
63
+ import { loopMessageChange, speakerOf, withLoopMessage } from './loopMessage';
63
64
  // Tracks pending prompts already auto-sent for a given conversation scope.
64
65
  // This prevents layout-driven unmount/remount cycles from re-sending prompts.
65
66
  const sentPendingPromptKeys = new Set();
@@ -2682,6 +2683,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2682
2683
  }
2683
2684
  }
2684
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
+ }
2685
2705
  case 'done':
2686
2706
  /*
2687
2707
  * What the turn cost, when nobody else is counting.
@@ -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,16 @@
1
+ /**
2
+ * An agent's words, as markdown: what the chat's messages draw them with
3
+ * (`ChatMessageList`), and what the floating assistant's balloon draws its
4
+ * current line with — one renderer, so a code block, a table or a list
5
+ * looks the same wherever the words are read.
6
+ *
7
+ * @module chat/messages/ChatMarkdown
8
+ */
9
+ import type { JSX } from 'react';
10
+ /** How dense the chat's components draw: the chat's own, or the balloon's. */
11
+ export type ChatDensity = 'comfortable' | 'compact';
12
+ export declare function ChatMarkdown({ text, density, }: {
13
+ text: string;
14
+ density?: ChatDensity;
15
+ }): JSX.Element;
16
+ export default ChatMarkdown;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from '@datalayer/primer-addons';
3
+ import { Streamdown } from 'streamdown';
4
+ import { streamdownMarkdownStyles } from '../styles/streamdownStyles';
5
+ import { normalizeAssistantMarkdown } from './assistantMarkdown';
6
+ export function ChatMarkdown({ text, density = 'comfortable', }) {
7
+ return (_jsx(Box, { "data-chat-markdown": "", sx: {
8
+ ...streamdownMarkdownStyles,
9
+ ...(density === 'compact'
10
+ ? {
11
+ lineHeight: 1.4,
12
+ '& p': { marginTop: 0, marginBottom: '0.4em' },
13
+ '& p:last-child': { marginBottom: 0 },
14
+ }
15
+ : {}),
16
+ }, children: _jsx(Streamdown, { children: normalizeAssistantMarkdown(text) }) }));
17
+ }
18
+ export default ChatMarkdown;
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode, type RefObject } from 'react';
2
+ import { type ChatDensity } from './ChatMarkdown';
2
3
  import { type MarkedMcpServer } from '../marks';
3
4
  import type { DisplayItem, AvatarConfig, RenderToolResult } from '../../types/chat';
4
5
  import type { ChatMessage } from '../../types/messages';
@@ -67,6 +68,12 @@ export interface ChatMessageListProps {
67
68
  * and runtime tools are found in the catalogues.
68
69
  */
69
70
  mcpServers?: readonly MarkedMcpServer[];
71
+ /**
72
+ * How dense it draws: the chat's own (`comfortable`), or the floating
73
+ * assistant's balloon (`compact`: no avatars, smaller bubbles and tool
74
+ * rows, no turn footers). The same components either way.
75
+ */
76
+ density?: ChatDensity;
70
77
  }
71
78
  type SubagentAvatarConfig = Required<Pick<AvatarConfig, 'userAvatar' | 'assistantAvatar' | 'showAvatars' | 'avatarSize' | 'userAvatarBg' | 'assistantAvatarBg'>>;
72
79
  export interface SubagentChatPanelProps {
@@ -86,5 +93,5 @@ export interface SubagentChatPanelProps {
86
93
  * approval decisions are sent over the shared monitoring socket.
87
94
  */
88
95
  export declare function SubagentChatPanel({ toolCallId, subagentName: subagentNameHint, height, avatarConfig, }: SubagentChatPanelProps): React.ReactElement | null;
89
- export declare function ChatMessageList({ displayItems, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, renderToolResult, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, }: ChatMessageListProps): import("react").JSX.Element;
96
+ export declare function ChatMessageList({ displayItems, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, renderToolResult, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, density, }: ChatMessageListProps): import("react").JSX.Element;
90
97
  export {};