@datalayer/agent-runtimes 1.5.88 → 1.5.90

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 (32) hide show
  1. package/dist-embed/datalayer-app-main.js +57 -40
  2. package/lib/apps/embed/element.js +20 -5
  3. package/lib/apps/embed/visitorApp.d.ts +23 -45
  4. package/lib/apps/embed/visitorApp.js +65 -13
  5. package/lib/apps/plugins/agent-outputs/sheets.d.ts +76 -0
  6. package/lib/apps/plugins/agent-outputs/sheets.js +230 -0
  7. package/lib/chat/messages/ChatMarkdown.d.ts +3 -1
  8. package/lib/chat/messages/ChatMarkdown.js +4 -1
  9. package/lib/chat/messages/codeBlocks.d.ts +50 -0
  10. package/lib/chat/messages/codeBlocks.js +81 -0
  11. package/lib/components/teams/A2ATeamGraph.d.ts +28 -7
  12. package/lib/components/teams/A2ATeamGraph.js +64 -27
  13. package/lib/components/teams/a2aTeamFlow.d.ts +16 -7
  14. package/lib/components/teams/a2aTeamFlow.js +17 -14
  15. package/lib/examples/AgentOutputsExample.d.ts +7 -0
  16. package/lib/examples/AgentOutputsExample.js +47 -7
  17. package/lib/examples/utils/agentOutputsPlugins.d.ts +2 -0
  18. package/lib/examples/utils/agentOutputsPlugins.js +27 -0
  19. package/lib/portable/hosts/agentClient.d.ts +8 -0
  20. package/lib/portable/hosts/agentClient.js +2 -0
  21. package/lib/portable/hosts/approvalRenderers.d.ts +10 -2
  22. package/lib/portable/hosts/approvalRenderers.js +81 -2
  23. package/lib/portable/hosts/contracts.d.ts +1 -0
  24. package/lib/portable/hosts/contracts.js +2 -0
  25. package/lib/portable/hosts/contracts.json +56 -2
  26. package/lib/specs/agents/agents.js +30 -8
  27. package/lib/specs/frontendTools.d.ts +1 -0
  28. package/lib/specs/frontendTools.js +18 -0
  29. package/lib/specs/mcpServers.js +20 -50
  30. package/lib/types/mcp.d.ts +4 -2
  31. package/package.json +2 -1
  32. package/scripts/codegen/generate_mcp_servers.py +35 -20
@@ -72,7 +72,7 @@ import { AppEmbed, embedAssistantCharacter } from './AppEmbed.js';
72
72
  import { WINDOW_NOT_YET } from './embedSession.js';
73
73
  import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EMBED_TOKEN_NAMES_NO_DEPLOYMENT, EmbedAttributeError, adoptPropertiesSetEarly, embedLookOf, embedTokenDeployment, inlineHeightOf, languageOf, resumeOf, runnable, } from './embedConfig.js';
74
74
  import { embedShadowCss } from './embedTheme.js';
75
- import { DEFAULT_API, readVisitorApp } from './visitorApp.js';
75
+ import { DEFAULT_API, DEFAULT_INFERENCE, readVisitorApp } from './visitorApp.js';
76
76
  export { runnable };
77
77
  /** What the framed run page and the element say to each other. */
78
78
  export const EMBED_MESSAGES = {
@@ -383,7 +383,11 @@ export function defineDatalayerAppElement(options = {}) {
383
383
  // A public application, as a visitor without an account (D-07):
384
384
  // an example kept warm or an address open to visitors, run on the
385
385
  // visitors' runtime; a decision framed; a refusal said.
386
- const visitor = await readVisitorApp({ api: this.agents(), app });
386
+ const visitor = await readVisitorApp({
387
+ api: this.agents(),
388
+ inference: this.inference(),
389
+ app,
390
+ });
387
391
  if (visitor.kind === 'decision') {
388
392
  return { kind: 'frame', src: frameSrcOf(this.origin(), visitor.app) };
389
393
  }
@@ -410,15 +414,26 @@ export function defineDatalayerAppElement(options = {}) {
410
414
  }
411
415
  /**
412
416
  * ai-agents, where a visitor's address is read and beside which the
413
- * visitors' runtime and ai-inference answer: the `api` attribute, else
414
- * the platform's, else r1's. Not Spacer's address (`api()`), which lives
415
- * on another plane.
417
+ * visitors' runtime answers: the `api` attribute, else the platform's,
418
+ * else the AI plane's (`DEFAULT_API`). Not Spacer's address (`api()`),
419
+ * which lives on another plane.
416
420
  */
417
421
  agents() {
418
422
  return (this.getAttribute('api') ||
419
423
  coreStore.getState().configuration?.aiAgentsUrl ||
420
424
  DEFAULT_API).replace(/\/+$/, '');
421
425
  }
426
+ /**
427
+ * ai-inference, which mints a visitor's token: the `api` attribute, else
428
+ * the platform's, else the AI plane's (`DEFAULT_INFERENCE`) — never the
429
+ * control plane, where no ai-inference answers (28 October 2026: a bare
430
+ * `<datalayer-app app="…">` asked prod1 and got "Failed to fetch").
431
+ */
432
+ inference() {
433
+ return (this.getAttribute('api') ||
434
+ coreStore.getState().configuration?.aiInferenceUrl ||
435
+ DEFAULT_INFERENCE).replace(/\/+$/, '');
436
+ }
422
437
  /** Spacer, where an application is read with its embed token: the `api` attribute, else the platform's, else prod1's. */
423
438
  api() {
424
439
  return (this.getAttribute('api') ||
@@ -1,39 +1,3 @@
1
- /**
2
- * A public application in the element, for a visitor without an account
3
- * (D-07, R-30).
4
- *
5
- * `<datalayer-app app="…">` with no token names a public application. The
6
- * element reads what it is without anybody's credentials, as the landing's
7
- * hosted page does for whoever is signed out:
8
- *
9
- * - **an example Datalayer keeps warm for visitors** (`web-research`,
10
- * `customer-interview`): its Appspec from the catalogue this bundle
11
- * carries; the visitors' runtime answers its id;
12
- * - **an application at its address** (`demo-accounting`): ai-agents'
13
- * `GET /apps/deployments/at/<slug>` without a token says what its page is
14
- * drawn from — the Appspec of the version it runs — and whether somebody
15
- * signed out may talk to it (`session.opens`); the visitors' runtime
16
- * answers `at:<slug>`, making the address's agent when first talked to —
17
- * or, for a version with code of its own (`runs_code`), the runtime its
18
- * deployment is kept on (`kept.url`), which runs that code;
19
- * - **a decision**, named by its id (`01M…`): framed from Datalayer's run
20
- * page, as before.
21
- *
22
- * The conversation then runs as the landing's signed-out one does: the chat
23
- * mints the visitor's token itself from ai-inference (`useVisitorToken`,
24
- * `POST /anonymous/token {app}`), minted again before it runs out, and talks
25
- * to the visitors' runtime with it — in every mode, inline or floating.
26
- *
27
- * What a visitor may not have is said in one sentence, before anything is
28
- * drawn (fail fast): a private address (ai-agents answers *No application is
29
- * at this address*), one that acts through its owner's connections or
30
- * documents (ai-agents' own reason), an example the visitors' runtime does
31
- * not keep. Each says the embed token is what an embed of it takes.
32
- *
33
- * Pure but for the one request, given or `fetch`.
34
- *
35
- * @module apps/embed/visitorApp
36
- */
37
1
  import type { AppSpec } from '../../types/agentspecs';
38
2
  import type { DatalayerVisitors } from '../apps/visitorToken';
39
3
  /** Where the visitors' runtime is published, on ai-agents' host (its chart). */
@@ -45,12 +9,21 @@ export declare const AT_PREFIX = "at:";
45
9
  /**
46
10
  * Datalayer's services for a visitor, where the element names none (`api`)
47
11
  * and the platform's configuration says none: ai-agents, where an address is
48
- * read, and beside it the visitors' runtime and ai-inference, which mints the
49
- * visitor's token. Not Spacer's address, which reads an application with its
50
- * embed token: the two live on different planes (ai-agents on r1, Spacer on
51
- * prod1), found 15 October 2026 on datalayer.ai's embed.
12
+ * read, and beside it the visitors' runtime — the AI plane, as core's plane
13
+ * table says it, never a literal of this bundle's own. Not Spacer's address,
14
+ * which reads an application with its embed token: the two live on different
15
+ * planes (ai-agents on r1, Spacer on prod1), found 15 October 2026 on
16
+ * datalayer.ai's embed; a bundle whose literal still said prod1 failed to
17
+ * mint a visitor's token there on 28 October 2026 ("Failed to fetch"),
18
+ * ai-inference being on r1 with the agents that call it.
19
+ */
20
+ export declare const DEFAULT_API: string;
21
+ /**
22
+ * ai-inference, which mints a visitor's token, where the element names none
23
+ * (`api`) and the platform's configuration says none: the AI plane, as core's
24
+ * plane table says it.
52
25
  */
53
- export declare const DEFAULT_API = "https://r1.datalayer.run";
26
+ export declare const DEFAULT_INFERENCE: string;
54
27
  /** An application's id on Datalayer: a ULID. */
55
28
  export declare const isAppUid: (value: string) => boolean;
56
29
  /** What `app` with no token resolves to. */
@@ -74,9 +47,10 @@ export type VisitorApp =
74
47
  /**
75
48
  * Where a visitor's conversation with `app` runs: the visitors' runtime beside
76
49
  * ai-agents — or `runtime`, the one a deployment with code of its own is
77
- * kept on (R-30) — its token from ai-inference.
50
+ * kept on (R-30) — its token from ai-inference, at `inference` when it is
51
+ * named, else beside ai-agents.
78
52
  */
79
- export declare function visitorsAt(api: string, app: string, runtime?: string): DatalayerVisitors;
53
+ export declare function visitorsAt(api: string, app: string, runtime?: string, inference?: string): DatalayerVisitors;
80
54
  /** What an embed without a token is told it takes. */
81
55
  export declare const TOKEN_HINT = "Embedded for a visitor it refuses, it takes its embed token (the \"token\" attribute).";
82
56
  /** The sentence for an example the visitors' runtime does not keep. */
@@ -87,13 +61,17 @@ export declare const refusedSentence: (reason: string) => string;
87
61
  * Read what `app` is for a visitor without an account: a decision to frame,
88
62
  * an example or an address to run on the visitors' runtime, or a sentence.
89
63
  *
90
- * @param api - Datalayer's services (ai-agents, ai-inference): the `api` attribute.
64
+ * @param api - Datalayer's services (ai-agents, and ai-inference unless
65
+ * `inference` names it): the `api` attribute; the AI plane unless given.
66
+ * @param inference - ai-inference, which mints the visitor's token: `api`
67
+ * when that is given, else the AI plane.
91
68
  * @param app - The `app` attribute: a decision's id, an example's id or an address.
92
69
  * @param fetcher - What asks ai-agents; `fetch` unless given.
93
70
  * @param catalogue - The examples; the bundle's catalogue unless given.
94
71
  */
95
- export declare function readVisitorApp({ api, app, fetcher, catalogue, }: {
72
+ export declare function readVisitorApp({ api, inference, app, fetcher, catalogue, }: {
96
73
  api?: string;
74
+ inference?: string;
97
75
  app: string;
98
76
  fetcher?: typeof fetch;
99
77
  catalogue?: (ref: string) => AppSpec | undefined;
@@ -2,6 +2,43 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ /**
6
+ * A public application in the element, for a visitor without an account
7
+ * (D-07, R-30).
8
+ *
9
+ * `<datalayer-app app="…">` with no token names a public application. The
10
+ * element reads what it is without anybody's credentials, as the landing's
11
+ * hosted page does for whoever is signed out:
12
+ *
13
+ * - **an example Datalayer keeps warm for visitors** (`web-research`,
14
+ * `customer-interview`): its Appspec from the catalogue this bundle
15
+ * carries; the visitors' runtime answers its id;
16
+ * - **an application at its address** (`demo-accounting`): ai-agents'
17
+ * `GET /apps/deployments/at/<slug>` without a token says what its page is
18
+ * drawn from — the Appspec of the version it runs — and whether somebody
19
+ * signed out may talk to it (`session.opens`); the visitors' runtime
20
+ * answers `at:<slug>`, making the address's agent when first talked to —
21
+ * or, for a version with code of its own (`runs_code`), the runtime its
22
+ * deployment is kept on (`kept.url`), which runs that code;
23
+ * - **a decision**, named by its id (`01M…`): framed from Datalayer's run
24
+ * page, as before.
25
+ *
26
+ * The conversation then runs as the landing's signed-out one does: the chat
27
+ * mints the visitor's token itself from ai-inference (`useVisitorToken`,
28
+ * `POST /anonymous/token {app}`), minted again before it runs out, and talks
29
+ * to the visitors' runtime with it — in every mode, inline or floating.
30
+ *
31
+ * What a visitor may not have is said in one sentence, before anything is
32
+ * drawn (fail fast): a private address (ai-agents answers *No application is
33
+ * at this address*), one that acts through its owner's connections or
34
+ * documents (ai-agents' own reason), an example the visitors' runtime does
35
+ * not keep. Each says the embed token is what an embed of it takes.
36
+ *
37
+ * Pure but for the one request, given or `fetch`.
38
+ *
39
+ * @module apps/embed/visitorApp
40
+ */
41
+ import { DEFAULT_PLANE_URLS } from '@datalayer/core/lib/config/planes.js';
5
42
  import { getApp } from '../../specs/apps.js';
6
43
  import { parseAppspec } from '../apps/appspec.js';
7
44
  import { runnable } from './embedConfig.js';
@@ -17,12 +54,21 @@ export const AT_PREFIX = 'at:';
17
54
  /**
18
55
  * Datalayer's services for a visitor, where the element names none (`api`)
19
56
  * and the platform's configuration says none: ai-agents, where an address is
20
- * read, and beside it the visitors' runtime and ai-inference, which mints the
21
- * visitor's token. Not Spacer's address, which reads an application with its
22
- * embed token: the two live on different planes (ai-agents on r1, Spacer on
23
- * prod1), found 15 October 2026 on datalayer.ai's embed.
57
+ * read, and beside it the visitors' runtime — the AI plane, as core's plane
58
+ * table says it, never a literal of this bundle's own. Not Spacer's address,
59
+ * which reads an application with its embed token: the two live on different
60
+ * planes (ai-agents on r1, Spacer on prod1), found 15 October 2026 on
61
+ * datalayer.ai's embed; a bundle whose literal still said prod1 failed to
62
+ * mint a visitor's token there on 28 October 2026 ("Failed to fetch"),
63
+ * ai-inference being on r1 with the agents that call it.
64
+ */
65
+ export const DEFAULT_API = DEFAULT_PLANE_URLS.aiAgentsUrl;
66
+ /**
67
+ * ai-inference, which mints a visitor's token, where the element names none
68
+ * (`api`) and the platform's configuration says none: the AI plane, as core's
69
+ * plane table says it.
24
70
  */
25
- export const DEFAULT_API = 'https://r1.datalayer.run';
71
+ export const DEFAULT_INFERENCE = DEFAULT_PLANE_URLS.aiInferenceUrl;
26
72
  /** An application's id on Datalayer: a ULID. */
27
73
  export const isAppUid = (value) => /^[0-9A-HJKMNP-TV-Z]{26}$/.test(value);
28
74
  /** An address, as ai-agents spells one. */
@@ -30,14 +76,15 @@ const SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/;
30
76
  /**
31
77
  * Where a visitor's conversation with `app` runs: the visitors' runtime beside
32
78
  * ai-agents — or `runtime`, the one a deployment with code of its own is
33
- * kept on (R-30) — its token from ai-inference.
79
+ * kept on (R-30) — its token from ai-inference, at `inference` when it is
80
+ * named, else beside ai-agents.
34
81
  */
35
- export function visitorsAt(api, app, runtime) {
82
+ export function visitorsAt(api, app, runtime, inference) {
36
83
  const base = api.replace(/\/+$/, '');
37
84
  return {
38
85
  url: runtime ? runtime.replace(/\/+$/, '') : `${base}${VISITORS_PATH}`,
39
86
  app,
40
- inferenceUrl: base,
87
+ inferenceUrl: (inference || base).replace(/\/+$/, ''),
41
88
  };
42
89
  }
43
90
  /** What an embed without a token is told it takes. */
@@ -50,12 +97,17 @@ export const refusedSentence = (reason) => `datalayer-app: ${reason.trim().repla
50
97
  * Read what `app` is for a visitor without an account: a decision to frame,
51
98
  * an example or an address to run on the visitors' runtime, or a sentence.
52
99
  *
53
- * @param api - Datalayer's services (ai-agents, ai-inference): the `api` attribute.
100
+ * @param api - Datalayer's services (ai-agents, and ai-inference unless
101
+ * `inference` names it): the `api` attribute; the AI plane unless given.
102
+ * @param inference - ai-inference, which mints the visitor's token: `api`
103
+ * when that is given, else the AI plane.
54
104
  * @param app - The `app` attribute: a decision's id, an example's id or an address.
55
105
  * @param fetcher - What asks ai-agents; `fetch` unless given.
56
106
  * @param catalogue - The examples; the bundle's catalogue unless given.
57
107
  */
58
- export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch, catalogue = getApp, }) {
108
+ export async function readVisitorApp({ api, inference, app, fetcher = fetch, catalogue = getApp, }) {
109
+ const agents = api || DEFAULT_API;
110
+ const mints = inference || api || DEFAULT_INFERENCE;
59
111
  const named = app.trim();
60
112
  if (isAppUid(named)) {
61
113
  return { kind: 'decision', app: named };
@@ -71,7 +123,7 @@ export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch,
71
123
  return {
72
124
  kind: 'visitors',
73
125
  app: runnable({ app: example, problems: [] }),
74
- visitors: visitorsAt(api, example.id),
126
+ visitors: visitorsAt(agents, example.id, undefined, mints),
75
127
  };
76
128
  }
77
129
  if (!SLUG.test(named)) {
@@ -82,7 +134,7 @@ export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch,
82
134
  }
83
135
  let response;
84
136
  try {
85
- response = await fetcher(`${api.replace(/\/+$/, '')}/api/ai-agents/v1/apps/deployments/at/${encodeURIComponent(named)}`);
137
+ response = await fetcher(`${agents.replace(/\/+$/, '')}/api/ai-agents/v1/apps/deployments/at/${encodeURIComponent(named)}`);
86
138
  }
87
139
  catch (error) {
88
140
  return {
@@ -117,7 +169,7 @@ export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch,
117
169
  return {
118
170
  kind: 'visitors',
119
171
  app: runnable({ app: { ...parsed.app, name }, problems: parsed.problems }),
120
- visitors: visitorsAt(api, `${AT_PREFIX}${named}`, runtime.url),
172
+ visitors: visitorsAt(agents, `${AT_PREFIX}${named}`, runtime.url, mints),
121
173
  };
122
174
  }
123
175
  /**
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The outputs agent's spreadsheets, drawn with Datalayer Cells — in the
3
+ * chat message itself, and wherever else the page shows them.
4
+ *
5
+ * A ```csv block in an answer (its first line `# filename: <name>.csv`) is
6
+ * a workbook: one model per block, read once and kept here, so the grid in
7
+ * the message and the one in a side panel are the same workbook. The message
8
+ * draws it read-only (a mirror that follows it), 240 px high, with a download
9
+ * button. {@link AgentOutputsSheetsPlugin} contributes that renderer to the
10
+ * chat's `ChatCodeBlock` point; {@link AgentOutputsSheetToolsPlugin} hands
11
+ * the Cells plugin's agent tools (the `cells` bundle) the last spreadsheet
12
+ * answered, so an agent asked to chart it inserts the chart into that
13
+ * workbook — and it appears on both grids.
14
+ *
15
+ * Kept out of the plugin's index, which the apps barrel re-exports: only a
16
+ * page that wants spreadsheets loads Cells.
17
+ *
18
+ * @module apps/plugins/agent-outputs/sheets
19
+ */
20
+ import { type JSX } from 'react';
21
+ import { type CellsModel } from '@datalayer/cells';
22
+ import { type ChatCodeBlockProps } from '../../../chat/messages/codeBlocks';
23
+ /** The fence languages a spreadsheet is written in. */
24
+ export declare const SHEET_LANGUAGES: string[];
25
+ /** A spreadsheet block: its file name, extension, and the rows' text. */
26
+ export type SheetBlock = {
27
+ name: string;
28
+ extension: 'csv' | 'tsv';
29
+ /** The CSV or TSV itself, without the `# filename:` line. */
30
+ source: string;
31
+ };
32
+ /**
33
+ * The spreadsheet a fenced block holds, by its language or by a
34
+ * `# filename:` first line ending in .csv or .tsv; `null` for another block.
35
+ */
36
+ export declare function parseSheetBlock(language: string, code: string): SheetBlock | null;
37
+ /**
38
+ * The workbook of a spreadsheet block, read once: every grid showing the
39
+ * same block shows the same model. The block met last for the first time is
40
+ * the latest, which is the one the agent tools act on.
41
+ */
42
+ export declare function sheetModel(block: SheetBlock): Promise<CellsModel>;
43
+ /** The workbook of the last spreadsheet answered, once read. */
44
+ export declare function latestSheetModel(): CellsModel | undefined;
45
+ /** Forget every workbook (tests). */
46
+ export declare function resetSheets(): void;
47
+ /** A block's workbook, or the sentence why it could not be read. */
48
+ export declare function useSheetModel(block: SheetBlock | null): {
49
+ model: CellsModel | null;
50
+ error: string;
51
+ };
52
+ /**
53
+ * A read-only copy of a workbook that follows it: the message's grid can
54
+ * neither be typed into nor lose the agent's chart.
55
+ */
56
+ export declare function useReadOnlyMirror(model: CellsModel | null): CellsModel | null;
57
+ /**
58
+ * The height that shows a workbook's active sheet whole — its data and its
59
+ * charts — between `min` and `max`, followed as the workbook changes: a chart
60
+ * the agent inserts below the data comes into view instead of sitting under
61
+ * a frame of fixed height (28 October 2026).
62
+ */
63
+ export declare function useFitHeight(model: CellsModel | null, min?: number, max?: number): number;
64
+ /** Save a block as its file. */
65
+ export declare function downloadSheet(block: SheetBlock): void;
66
+ /** A spreadsheet in a chat message: its grid, read-only, and a download button. */
67
+ export declare function SheetCodeBlock({ language, code, }: ChatCodeBlockProps): JSX.Element;
68
+ export declare const AGENT_OUTPUTS_SHEETS_PLUGIN_NAME = "@datalayer/loop-plugin-agent-outputs-sheets";
69
+ /** A ```csv (or ```tsv) block of an answer, drawn as a Cells grid in the message. */
70
+ export declare const AgentOutputsSheetsPlugin: import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, unknown>;
71
+ /**
72
+ * The Cells agent tools (`cells_read_range`, `cells_insert_chart`, …) on the
73
+ * last spreadsheet answered. Its bundle id is `cells`, the frontend toolset
74
+ * the `example-output` spec names.
75
+ */
76
+ export declare const AgentOutputsSheetToolsPlugin: import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, unknown>;
@@ -0,0 +1,230 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * The outputs agent's spreadsheets, drawn with Datalayer Cells — in the
8
+ * chat message itself, and wherever else the page shows them.
9
+ *
10
+ * A ```csv block in an answer (its first line `# filename: <name>.csv`) is
11
+ * a workbook: one model per block, read once and kept here, so the grid in
12
+ * the message and the one in a side panel are the same workbook. The message
13
+ * draws it read-only (a mirror that follows it), 240 px high, with a download
14
+ * button. {@link AgentOutputsSheetsPlugin} contributes that renderer to the
15
+ * chat's `ChatCodeBlock` point; {@link AgentOutputsSheetToolsPlugin} hands
16
+ * the Cells plugin's agent tools (the `cells` bundle) the last spreadsheet
17
+ * answered, so an agent asked to chart it inserts the chart into that
18
+ * workbook — and it appears on both grids.
19
+ *
20
+ * Kept out of the plugin's index, which the apps barrel re-exports: only a
21
+ * page that wants spreadsheets loads Cells.
22
+ *
23
+ * @module apps/plugins/agent-outputs/sheets
24
+ */
25
+ import { useEffect, useState } from 'react';
26
+ import { Button, Text } from '@primer/react';
27
+ import { DownloadIcon } from '@primer/octicons-react';
28
+ import { Box } from '@datalayer/primer-addons';
29
+ import { contribution, definePlugin } from '@datalayer/reactor';
30
+ import { CellsProvider, createCellsModel, Spreadsheet, } from '@datalayer/cells';
31
+ import { CellsChartsPlugin } from '@datalayer/cells/charts';
32
+ import { chartRect } from '@datalayer/cells/core';
33
+ import { importCsv, usedZone } from '@datalayer/cells/csv';
34
+ import { cellsAgentToolsPlugin } from '@datalayer/cells/plugin';
35
+ import { ChatCodeBlock, } from '../../../chat/messages/codeBlocks.js';
36
+ /** The fence languages a spreadsheet is written in. */
37
+ export const SHEET_LANGUAGES = ['csv', 'tsv', 'spreadsheet', 'cells'];
38
+ const FILENAME = /^#\s*filename:\s*([\w.-]+)\s*(?:\n|$)/i;
39
+ /**
40
+ * The spreadsheet a fenced block holds, by its language or by a
41
+ * `# filename:` first line ending in .csv or .tsv; `null` for another block.
42
+ */
43
+ export function parseSheetBlock(language, code) {
44
+ const text = code.replace(/^\s*\n/, '');
45
+ const named = FILENAME.exec(text)?.[1];
46
+ const byName = /\.(csv|tsv)$/i.exec(named ?? '')?.[1]?.toLowerCase();
47
+ if (!SHEET_LANGUAGES.includes(language) && byName === undefined) {
48
+ return null;
49
+ }
50
+ const extension = byName === 'tsv' || (byName === undefined && language === 'tsv')
51
+ ? 'tsv'
52
+ : 'csv';
53
+ const source = (named ? text.replace(FILENAME, '') : text).replace(/\s+$/, '');
54
+ if (source === '') {
55
+ return null;
56
+ }
57
+ return { name: named ?? `output.${extension}`, extension, source };
58
+ }
59
+ const MAX_SHEETS = 40;
60
+ const sheets = new Map();
61
+ const ready = new Map();
62
+ let latestKey;
63
+ const keyOf = (block) => `${block.name}\n${block.source}`;
64
+ /**
65
+ * The workbook of a spreadsheet block, read once: every grid showing the
66
+ * same block shows the same model. The block met last for the first time is
67
+ * the latest, which is the one the agent tools act on.
68
+ */
69
+ export function sheetModel(block) {
70
+ const key = keyOf(block);
71
+ const known = sheets.get(key);
72
+ if (known) {
73
+ return known;
74
+ }
75
+ latestKey = key;
76
+ const model = createCellsModel({ plugins: [CellsChartsPlugin] });
77
+ const read = importCsv(model, block.source, { worker: false }).then(() => {
78
+ ready.set(key, model);
79
+ return model;
80
+ });
81
+ sheets.set(key, read);
82
+ read.catch(() => sheets.delete(key));
83
+ // Streaming makes a block per chunk: the oldest go, never the latest.
84
+ for (const old of sheets.keys()) {
85
+ if (sheets.size <= MAX_SHEETS) {
86
+ break;
87
+ }
88
+ if (old !== latestKey) {
89
+ sheets.delete(old);
90
+ ready.delete(old);
91
+ }
92
+ }
93
+ return read;
94
+ }
95
+ /** The workbook of the last spreadsheet answered, once read. */
96
+ export function latestSheetModel() {
97
+ return latestKey === undefined ? undefined : ready.get(latestKey);
98
+ }
99
+ /** Forget every workbook (tests). */
100
+ export function resetSheets() {
101
+ sheets.clear();
102
+ ready.clear();
103
+ latestKey = undefined;
104
+ }
105
+ /** A block's workbook, or the sentence why it could not be read. */
106
+ export function useSheetModel(block) {
107
+ const [state, setState] = useState({ model: null, error: '' });
108
+ const key = block ? keyOf(block) : '';
109
+ useEffect(() => {
110
+ if (!block) {
111
+ return;
112
+ }
113
+ let cancelled = false;
114
+ sheetModel(block)
115
+ .then(model => !cancelled && setState({ model, error: '' }))
116
+ .catch((e) => !cancelled &&
117
+ setState({
118
+ model: null,
119
+ error: e instanceof Error ? e.message : String(e),
120
+ }));
121
+ return () => {
122
+ cancelled = true;
123
+ };
124
+ // The block is its key.
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, [key]);
127
+ return state;
128
+ }
129
+ /**
130
+ * A read-only copy of a workbook that follows it: the message's grid can
131
+ * neither be typed into nor lose the agent's chart.
132
+ */
133
+ export function useReadOnlyMirror(model) {
134
+ const [mirror, setMirror] = useState(null);
135
+ useEffect(() => {
136
+ if (!model) {
137
+ setMirror(null);
138
+ return;
139
+ }
140
+ const copy = createCellsModel({
141
+ readOnly: true,
142
+ plugins: [CellsChartsPlugin],
143
+ });
144
+ copy.replaceData(model.toJSON(), 'remote');
145
+ setMirror(copy);
146
+ return model.subscribe(() => copy.replaceData(model.toJSON(), 'remote'));
147
+ }, [model]);
148
+ return mirror;
149
+ }
150
+ /** The column header's height and a scrollbar's, in px, around the content. */
151
+ const GRID_CHROME = 24 + 16;
152
+ /**
153
+ * The height that shows a workbook's active sheet whole — its data and its
154
+ * charts — between `min` and `max`, followed as the workbook changes: a chart
155
+ * the agent inserts below the data comes into view instead of sitting under
156
+ * a frame of fixed height (28 October 2026).
157
+ */
158
+ export function useFitHeight(model, min = 160, max = 560) {
159
+ const [height, setHeight] = useState(min);
160
+ useEffect(() => {
161
+ if (!model) {
162
+ return;
163
+ }
164
+ const measure = () => {
165
+ const sheet = model.sheet(model.selection.sheet);
166
+ let bottom = sheet.rows.offsetOf(usedZone(model, sheet.id).bottom + 1);
167
+ for (const spec of sheet.charts.values()) {
168
+ const rect = chartRect(sheet, spec);
169
+ bottom = Math.max(bottom, rect.y + rect.height);
170
+ }
171
+ setHeight(Math.min(max, Math.max(min, Math.ceil(bottom) + GRID_CHROME)));
172
+ };
173
+ measure();
174
+ return model.subscribe(measure);
175
+ }, [model, min, max]);
176
+ return height;
177
+ }
178
+ /** Save a block as its file. */
179
+ export function downloadSheet(block) {
180
+ const type = block.extension === 'tsv' ? 'text/tab-separated-values' : 'text/csv';
181
+ const url = URL.createObjectURL(new Blob([block.source + '\n'], { type: `${type};charset=utf-8` }));
182
+ const a = document.createElement('a');
183
+ a.href = url;
184
+ a.download = block.name;
185
+ document.body.appendChild(a);
186
+ a.click();
187
+ document.body.removeChild(a);
188
+ URL.revokeObjectURL(url);
189
+ }
190
+ /** A spreadsheet in a chat message: its grid, read-only, and a download button. */
191
+ export function SheetCodeBlock({ language, code, }) {
192
+ const block = parseSheetBlock(language, code);
193
+ const { model, error } = useSheetModel(block);
194
+ const mirror = useReadOnlyMirror(model);
195
+ const height = useFitHeight(mirror);
196
+ if (!block) {
197
+ return _jsx(_Fragment, {});
198
+ }
199
+ return (
200
+ // The whole measure of the message (the chat gives it to what declares
201
+ // `width: 100%`), and wide enough for a few columns even in a bubble that
202
+ // hugs its text: a grid has no width of its own, and showed one column.
203
+ _jsxs(Box, { "data-sheet-block": block.name, my: 2, width: "100%", minWidth: "min(560px, 90vw)", children: [error ? (_jsxs(Text, { sx: { color: 'danger.fg', fontSize: 0 }, children: ["Could not read the spreadsheet: ", error] })) : mirror ? (_jsx(CellsProvider, { model: mirror, children: _jsx(Box, { height: height, border: "1px solid", borderColor: "border.muted", borderRadius: 2, overflow: "hidden", children: _jsx(Spreadsheet, { model: mirror, "aria-label": block.name }) }) })) : (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: "Reading\u2026" })), _jsx(Box, { mt: 1, children: _jsxs(Button, { size: "small", variant: "invisible", leadingVisual: DownloadIcon, onClick: () => downloadSheet(block), children: ["Download ", block.name] }) })] }));
204
+ }
205
+ export const AGENT_OUTPUTS_SHEETS_PLUGIN_NAME = '@datalayer/loop-plugin-agent-outputs-sheets';
206
+ /** A ```csv (or ```tsv) block of an answer, drawn as a Cells grid in the message. */
207
+ export const AgentOutputsSheetsPlugin = definePlugin({
208
+ name: AGENT_OUTPUTS_SHEETS_PLUGIN_NAME,
209
+ displayName: 'Agent Outputs: spreadsheets',
210
+ description: 'A CSV the agent writes, drawn as a Datalayer Cells grid in the chat.',
211
+ octicon: 'table',
212
+ emoji: '🧮',
213
+ contributes: [
214
+ contribution(ChatCodeBlock, {
215
+ id: 'cells-sheet',
216
+ languages: SHEET_LANGUAGES,
217
+ match: block => parseSheetBlock(block.language, block.code) !== null,
218
+ Component: SheetCodeBlock,
219
+ }, { id: 'cells-sheet' }),
220
+ ],
221
+ });
222
+ /**
223
+ * The Cells agent tools (`cells_read_range`, `cells_insert_chart`, …) on the
224
+ * last spreadsheet answered. Its bundle id is `cells`, the frontend toolset
225
+ * the `example-output` spec names.
226
+ */
227
+ export const AgentOutputsSheetToolsPlugin = cellsAgentToolsPlugin({
228
+ model: latestSheetModel,
229
+ name: '@datalayer/loop-plugin-agent-outputs-sheet-tools',
230
+ });
@@ -2,7 +2,9 @@
2
2
  * An agent's words, as markdown: what the chat's messages draw them with
3
3
  * (`ChatMessageList`), and what the floating assistant's balloon draws its
4
4
  * current line with — one renderer, so a code block, a table or a list
5
- * looks the same wherever the words are read.
5
+ * looks the same wherever the words are read. A fenced block whose language
6
+ * a plugin contributed a renderer for (`ChatCodeBlock`, a ```csv block drawn
7
+ * as a spreadsheet) is drawn by that renderer, in the message.
6
8
  *
7
9
  * @module chat/messages/ChatMarkdown
8
10
  */
@@ -4,6 +4,9 @@ import { Streamdown } from 'streamdown';
4
4
  import { REHYPE_PLUGINS_WITHOUT_HTML } from './markdownWithoutHtml.js';
5
5
  import { streamdownMarkdownStyles } from '../styles/streamdownStyles.js';
6
6
  import { normalizeAssistantMarkdown } from './assistantMarkdown.js';
7
+ import { ChatCodePre } from './codeBlocks.js';
8
+ /** The markdown elements the chat draws itself: a `pre` that hands its block to a contributed renderer. */
9
+ const COMPONENTS = { pre: ChatCodePre };
7
10
  export function ChatMarkdown({ text, density = 'comfortable', }) {
8
11
  return (_jsx(Box, { "data-chat-markdown": "", sx: {
9
12
  ...streamdownMarkdownStyles,
@@ -14,6 +17,6 @@ export function ChatMarkdown({ text, density = 'comfortable', }) {
14
17
  '& p:last-child': { marginBottom: 0 },
15
18
  }
16
19
  : {}),
17
- }, children: _jsx(Streamdown, { rehypePlugins: REHYPE_PLUGINS_WITHOUT_HTML, children: normalizeAssistantMarkdown(text) }) }));
20
+ }, children: _jsx(Streamdown, { rehypePlugins: REHYPE_PLUGINS_WITHOUT_HTML, components: COMPONENTS, children: normalizeAssistantMarkdown(text) }) }));
18
21
  }
19
22
  export default ChatMarkdown;