@datalayer/agent-runtimes 1.5.87 → 1.5.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-embed/datalayer-app-main.js +22 -9
- package/lib/apps/apps/checks.d.ts +1 -1
- package/lib/apps/apps/checks.js +1 -1
- package/lib/apps/apps/computer.d.ts +1 -1
- package/lib/apps/apps/computer.js +1 -1
- package/lib/apps/apps/pluginPair.d.ts +1 -1
- package/lib/apps/apps/rules.d.ts +1 -1
- package/lib/apps/apps/rules.js +2 -2
- package/lib/apps/apps/visitorToken.d.ts +1 -1
- package/lib/apps/apps/visitorToken.js +1 -1
- package/lib/apps/core/appFiles.js +1 -1
- package/lib/apps/plugins/a2ui-surface/toolResult.d.ts +1 -1
- package/lib/apps/plugins/a2ui-surface/toolResult.js +1 -1
- package/lib/apps/plugins/agent-outputs/sheets.d.ts +76 -0
- package/lib/apps/plugins/agent-outputs/sheets.js +230 -0
- package/lib/apps/plugins/app-page/appPageModel.d.ts +1 -1
- package/lib/apps/plugins/app-page/appPageModel.js +1 -1
- package/lib/chat/messages/ChatMarkdown.d.ts +3 -1
- package/lib/chat/messages/ChatMarkdown.js +4 -1
- package/lib/chat/messages/codeBlocks.d.ts +50 -0
- package/lib/chat/messages/codeBlocks.js +81 -0
- package/lib/examples/AgentOutputsExample.d.ts +7 -0
- package/lib/examples/AgentOutputsExample.js +47 -7
- package/lib/examples/utils/agentOutputsPlugins.d.ts +2 -0
- package/lib/examples/utils/agentOutputsPlugins.js +27 -0
- package/lib/portable/hosts/client.d.ts +11 -1
- package/lib/portable/hosts/client.js +12 -6
- package/lib/portable/hosts/contracts.json +9 -3
- package/lib/portable/hosts/index.d.ts +1 -1
- package/lib/portable/records.d.ts +1 -1
- package/lib/portable/records.js +1 -1
- package/lib/portable/semantic-events.json +3 -3
- package/lib/portable/semanticOfAgUi.d.ts +1 -1
- package/lib/runtimes/variants.d.ts +1 -1
- package/lib/runtimes/variants.js +1 -1
- package/lib/specs/agents/agents.d.ts +1 -0
- package/lib/specs/agents/agents.js +79 -8
- package/lib/specs/apps.js +32 -32
- package/lib/specs/frontendTools.d.ts +1 -0
- package/lib/specs/frontendTools.js +18 -0
- package/lib/specs/harnesses.d.ts +1 -0
- package/lib/specs/harnesses.js +62 -19
- package/lib/specs/index.d.ts +1 -1
- package/lib/specs/index.js +1 -1
- package/lib/specs/mcpServers.js +20 -50
- package/lib/types/mcp.d.ts +4 -2
- package/package.json +2 -1
- package/scripts/codegen/generate_apps.py +1 -1
- package/scripts/codegen/generate_mcp_servers.py +30 -4
- package/scripts/validate_pap_examples.py +1 -1
|
@@ -49,7 +49,7 @@ export declare const ORGANIZATION_FRAME_PREFIX = "org-";
|
|
|
49
49
|
/** Whether a reference names an organization's own context rather than the catalogue's (U-32). */
|
|
50
50
|
export declare const isOrganizationFrame: (ref: string) => boolean;
|
|
51
51
|
/**
|
|
52
|
-
* The Guards the runtime executes (agent-runtimes `
|
|
52
|
+
* The Guards the runtime executes (agent-runtimes `apps/guards.py`): the
|
|
53
53
|
* three of a session's start, run once before its model is first asked and
|
|
54
54
|
* each written to the record, and the two of every tool call and the answer.
|
|
55
55
|
*/
|
package/lib/apps/apps/checks.js
CHANGED
|
@@ -634,7 +634,7 @@ function attentionNotes(app) {
|
|
|
634
634
|
return notes;
|
|
635
635
|
}
|
|
636
636
|
/**
|
|
637
|
-
* The Guards the runtime executes (agent-runtimes `
|
|
637
|
+
* The Guards the runtime executes (agent-runtimes `apps/guards.py`): the
|
|
638
638
|
* three of a session's start, run once before its model is first asked and
|
|
639
639
|
* each written to the record, and the two of every tool call and the answer.
|
|
640
640
|
*/
|
|
@@ -16,7 +16,7 @@ import type { ConversationEntry } from '../core';
|
|
|
16
16
|
export declare const COMPUTER_PARTS: readonly ["browse", "files", "shell"];
|
|
17
17
|
export type ComputerPart = (typeof COMPUTER_PARTS)[number];
|
|
18
18
|
export type ComputerParts = Record<ComputerPart, boolean>;
|
|
19
|
-
/** The tools of each part, as the runtime gives them (`
|
|
19
|
+
/** The tools of each part, as the runtime gives them (`apps/computer.py`). */
|
|
20
20
|
export declare const COMPUTER_PART_TOOLS: Record<ComputerPart, readonly string[]>;
|
|
21
21
|
/** What the computer view says. */
|
|
22
22
|
export declare const COMPUTER_WORDS: {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/** The parts of a computer, in the order a person reads them. */
|
|
6
6
|
export const COMPUTER_PARTS = ['browse', 'files', 'shell'];
|
|
7
|
-
/** The tools of each part, as the runtime gives them (`
|
|
7
|
+
/** The tools of each part, as the runtime gives them (`apps/computer.py`). */
|
|
8
8
|
export const COMPUTER_PART_TOOLS = {
|
|
9
9
|
browse: [
|
|
10
10
|
'open_page',
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Whichever editor wrote it — a spec, the Canvas, an `app.py` — an
|
|
5
5
|
* application is a pair of plugins of one name, `loop-app-<id>`: the page's
|
|
6
6
|
* (`defineAppPlugin`) and the runtime's (`manifest_of` in
|
|
7
|
-
* `agent_runtimes.
|
|
7
|
+
* `agent_runtimes.apps.plugins`, which says the same name). Each
|
|
8
8
|
* declares the other: the page's `requiredBackendPlugins`, the runtime's
|
|
9
9
|
* `frontend_dependencies`; and both are delivered as one extension of that
|
|
10
10
|
* name, which is how Reactor's manager and graph show them as one
|
package/lib/apps/apps/rules.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare const strictest: (behaviours: AppBehaviour[]) => AppBehaviour;
|
|
|
68
68
|
* A tool of a server the application is not connected to is not given.
|
|
69
69
|
*
|
|
70
70
|
* The runtime gives its agent only these (`AppRulesCapability.prepare_tools`,
|
|
71
|
-
* through `agent_runtimes.
|
|
71
|
+
* through `agent_runtimes.apps.rules.gives`, its Python twin).
|
|
72
72
|
*
|
|
73
73
|
* @throws When `tool` names no server: a catalogue tool is given by the
|
|
74
74
|
* spec's `tools`, not by a connection.
|
package/lib/apps/apps/rules.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* and the most restricted wins.
|
|
27
27
|
*
|
|
28
28
|
* The same decision is written in Python, in `agentspecs.apps.behaviour_for`
|
|
29
|
-
* and in `agent_runtimes.
|
|
29
|
+
* and in `agent_runtimes.apps.rules`. `APP_BEHAVIOURS`, generated from
|
|
30
30
|
* agentspecs, is what all three have to agree on. The server enforces; this
|
|
31
31
|
* one shows the decision before it is made.
|
|
32
32
|
*
|
|
@@ -217,7 +217,7 @@ const reaches = (connection, tool) => connection.only.length === 0 ||
|
|
|
217
217
|
* A tool of a server the application is not connected to is not given.
|
|
218
218
|
*
|
|
219
219
|
* The runtime gives its agent only these (`AppRulesCapability.prepare_tools`,
|
|
220
|
-
* through `agent_runtimes.
|
|
220
|
+
* through `agent_runtimes.apps.rules.gives`, its Python twin).
|
|
221
221
|
*
|
|
222
222
|
* @throws When `tool` names no server: a catalogue tool is given by the
|
|
223
223
|
* spec's `tools`, not by a connection.
|
|
@@ -35,7 +35,7 @@ export declare function useVisitorToken(visitors: DatalayerVisitors | undefined)
|
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
37
|
* The agent of the visitors' runtime a visitor's token reaches, as the
|
|
38
|
-
* runtime names it (`agent_of` in `agent_runtimes.
|
|
38
|
+
* runtime names it (`agent_of` in `agent_runtimes.apps.visitors`): an
|
|
39
39
|
* example's by its id, an application at its address by `at-<slug>`. The
|
|
40
40
|
* chat speaks to that agent and to no other: the token reaches no other.
|
|
41
41
|
*
|
|
@@ -118,7 +118,7 @@ const AT_PREFIX = 'at:';
|
|
|
118
118
|
const SLUG = /^[a-z0-9][a-z0-9-]{0,99}$/;
|
|
119
119
|
/**
|
|
120
120
|
* The agent of the visitors' runtime a visitor's token reaches, as the
|
|
121
|
-
* runtime names it (`agent_of` in `agent_runtimes.
|
|
121
|
+
* runtime names it (`agent_of` in `agent_runtimes.apps.visitors`): an
|
|
122
122
|
* example's by its id, an application at its address by `at-<slug>`. The
|
|
123
123
|
* chat speaks to that agent and to no other: the token reaches no other.
|
|
124
124
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* A component its developer wrote may be a file of its folder (`source:
|
|
10
10
|
* gauge.js`). `loop apps package` puts the folder's files in the
|
|
11
11
|
* application's wheel, beside a generated page-side plugin
|
|
12
|
-
* (`loop-app-<id>/page`, `agent_runtimes/
|
|
12
|
+
* (`loop-app-<id>/page`, `agent_runtimes/apps/packaging.py`) whose one
|
|
13
13
|
* contribution is to this point: the address its own directory is served at
|
|
14
14
|
* by the server the wheel is installed beside. The page installs that plugin
|
|
15
15
|
* through Reactor's `bootstrapExtensions` (`AppRuntimePlugins`), and a file
|
|
@@ -53,7 +53,7 @@ export declare function validateA2uiField(rule: A2uiFieldRule, value: unknown):
|
|
|
53
53
|
export declare function validateA2uiSubmission(rules: A2uiFieldRule[], values: Record<string, unknown>): string[];
|
|
54
54
|
/**
|
|
55
55
|
* The display type of what an application's answer shows (P-04,
|
|
56
|
-
* RU-2): `AnswerShows` in `agent_runtimes.
|
|
56
|
+
* RU-2): `AnswerShows` in `agent_runtimes.apps.components`.
|
|
57
57
|
*/
|
|
58
58
|
export declare const ANSWER_DISPLAY_TYPE = "application/vnd.datalayer.loop.answer+json";
|
|
59
59
|
/** A button pressed on what an answer shows, as the chat sends it on. */
|
|
@@ -106,7 +106,7 @@ export function validateA2uiSubmission(rules, values) {
|
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
108
|
* The display type of what an application's answer shows (P-04,
|
|
109
|
-
* RU-2): `AnswerShows` in `agent_runtimes.
|
|
109
|
+
* RU-2): `AnswerShows` in `agent_runtimes.apps.components`.
|
|
110
110
|
*/
|
|
111
111
|
export const ANSWER_DISPLAY_TYPE = 'application/vnd.datalayer.loop.answer+json';
|
|
112
112
|
/** The A2UI surface an answer's display draws, or `null` for a display that has none. */
|
|
@@ -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
|
+
});
|
|
@@ -124,7 +124,7 @@ export declare function appPageInitialData(app: Pick<AppSpec, 'kind' | 'name' |
|
|
|
124
124
|
export declare function inputsInWords(app: Pick<AppSpec, 'interface'>, inputs: unknown): string;
|
|
125
125
|
/**
|
|
126
126
|
* The largest file a session takes: what the session API refuses above
|
|
127
|
-
* (`agent_runtimes.
|
|
127
|
+
* (`agent_runtimes.apps.sessions.MAX_FILE_BYTES`), said here before
|
|
128
128
|
* the file is sent.
|
|
129
129
|
*/
|
|
130
130
|
export declare const MAX_FILE_BYTES: number;
|
|
@@ -357,7 +357,7 @@ export function inputsInWords(app, inputs) {
|
|
|
357
357
|
}
|
|
358
358
|
/**
|
|
359
359
|
* The largest file a session takes: what the session API refuses above
|
|
360
|
-
* (`agent_runtimes.
|
|
360
|
+
* (`agent_runtimes.apps.sessions.MAX_FILE_BYTES`), said here before
|
|
361
361
|
* the file is sent.
|
|
362
362
|
*/
|
|
363
363
|
export const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -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;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fenced code blocks drawn by a plugin, in the agent's words.
|
|
3
|
+
*
|
|
4
|
+
* The chat draws an agent's answer as markdown (`ChatMarkdown`), and a
|
|
5
|
+
* fenced block as code. A plugin that knows better what a block of some
|
|
6
|
+
* language is — a ```csv block is a spreadsheet — contributes a renderer to
|
|
7
|
+
* {@link ChatCodeBlock}, and the block is drawn by it, in the message itself.
|
|
8
|
+
* Read from the Reactor platform the chat is mounted in, like the renderers
|
|
9
|
+
* of a tool call's display: a chat with no platform, or no renderer for the
|
|
10
|
+
* language, draws the block as code, as before.
|
|
11
|
+
*
|
|
12
|
+
* @module chat/messages/codeBlocks
|
|
13
|
+
*/
|
|
14
|
+
import { type ComponentType, type JSX, type ReactNode } from 'react';
|
|
15
|
+
import { type Contribution } from '@datalayer/reactor';
|
|
16
|
+
/** One fenced block of an answer: its info string's language, and its text. */
|
|
17
|
+
export type ChatCodeBlockProps = {
|
|
18
|
+
/** The fence's language, lower-cased: `csv` for ```csv. Empty for a bare fence. */
|
|
19
|
+
language: string;
|
|
20
|
+
/** The block's text, as written between the fences. */
|
|
21
|
+
code: string;
|
|
22
|
+
};
|
|
23
|
+
/** A renderer of fenced blocks, contributed to {@link ChatCodeBlock}. */
|
|
24
|
+
export type ChatCodeBlockContribution = {
|
|
25
|
+
/** Stable id, for the registry and the graph. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** The fence languages it draws, lower-case: `['csv', 'tsv']`. */
|
|
28
|
+
languages: string[];
|
|
29
|
+
/** A finer test, on the block itself; the block is drawn as code when it says no. */
|
|
30
|
+
match?: (block: ChatCodeBlockProps) => boolean;
|
|
31
|
+
/** What draws the block, in place of the code. */
|
|
32
|
+
Component: ComponentType<ChatCodeBlockProps>;
|
|
33
|
+
};
|
|
34
|
+
/** Where plugins put their fenced-block renderers. The first that takes a block draws it. */
|
|
35
|
+
export declare const ChatCodeBlock: import("@datalayer/reactor").ContributionPoint<ChatCodeBlockContribution>;
|
|
36
|
+
/**
|
|
37
|
+
* The fenced block a `<pre>` holds, as markdown renders one: a `<code>`
|
|
38
|
+
* element whose class names the language. `null` for anything else.
|
|
39
|
+
*/
|
|
40
|
+
export declare function codeBlockOf(children: ReactNode): ChatCodeBlockProps | null;
|
|
41
|
+
/** The renderer that takes a block, among the contributions, in their order. */
|
|
42
|
+
export declare function codeBlockRenderer(contributions: readonly Contribution<ChatCodeBlockContribution>[], block: ChatCodeBlockProps): ChatCodeBlockContribution | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The markdown `pre` the chat hands its renderer: a block a contribution
|
|
45
|
+
* takes is drawn by it, every other one exactly as the default `pre` draws
|
|
46
|
+
* it (its code element, unwrapped).
|
|
47
|
+
*/
|
|
48
|
+
export declare function ChatCodePre({ children, }: {
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } 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
|
+
* Fenced code blocks drawn by a plugin, in the agent's words.
|
|
8
|
+
*
|
|
9
|
+
* The chat draws an agent's answer as markdown (`ChatMarkdown`), and a
|
|
10
|
+
* fenced block as code. A plugin that knows better what a block of some
|
|
11
|
+
* language is — a ```csv block is a spreadsheet — contributes a renderer to
|
|
12
|
+
* {@link ChatCodeBlock}, and the block is drawn by it, in the message itself.
|
|
13
|
+
* Read from the Reactor platform the chat is mounted in, like the renderers
|
|
14
|
+
* of a tool call's display: a chat with no platform, or no renderer for the
|
|
15
|
+
* language, draws the block as code, as before.
|
|
16
|
+
*
|
|
17
|
+
* @module chat/messages/codeBlocks
|
|
18
|
+
*/
|
|
19
|
+
import { isValidElement, useSyncExternalStore, } from 'react';
|
|
20
|
+
import { defineContributionPoint } from '@datalayer/reactor';
|
|
21
|
+
import { useOptionalReactorPlatform } from '@datalayer/reactor/react';
|
|
22
|
+
/** Where plugins put their fenced-block renderers. The first that takes a block draws it. */
|
|
23
|
+
export const ChatCodeBlock = defineContributionPoint('chat.codeBlock');
|
|
24
|
+
const LANGUAGE = /(?:^|\s)language-([^\s]+)/;
|
|
25
|
+
/** The text of a React node: strings and numbers, joined, through elements' children. */
|
|
26
|
+
function textOf(node) {
|
|
27
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
28
|
+
return String(node);
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(node)) {
|
|
31
|
+
return node.map(textOf).join('');
|
|
32
|
+
}
|
|
33
|
+
if (isValidElement(node)) {
|
|
34
|
+
return textOf(node.props.children);
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The fenced block a `<pre>` holds, as markdown renders one: a `<code>`
|
|
40
|
+
* element whose class names the language. `null` for anything else.
|
|
41
|
+
*/
|
|
42
|
+
export function codeBlockOf(children) {
|
|
43
|
+
const only = Array.isArray(children)
|
|
44
|
+
? children.filter(child => child !== null && child !== '\n')
|
|
45
|
+
: [children];
|
|
46
|
+
if (only.length !== 1 || !isValidElement(only[0])) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const props = only[0].props;
|
|
50
|
+
const language = LANGUAGE.exec(props.className ?? '')?.[1]?.toLowerCase();
|
|
51
|
+
if (language === undefined) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return { language, code: textOf(props.children) };
|
|
55
|
+
}
|
|
56
|
+
/** The renderer that takes a block, among the contributions, in their order. */
|
|
57
|
+
export function codeBlockRenderer(contributions, block) {
|
|
58
|
+
return contributions
|
|
59
|
+
.map(entry => entry.value)
|
|
60
|
+
.find(value => value.languages.includes(block.language) &&
|
|
61
|
+
(value.match ? value.match(block) : true));
|
|
62
|
+
}
|
|
63
|
+
const NO_SUBSCRIPTION = () => () => { };
|
|
64
|
+
/**
|
|
65
|
+
* The markdown `pre` the chat hands its renderer: a block a contribution
|
|
66
|
+
* takes is drawn by it, every other one exactly as the default `pre` draws
|
|
67
|
+
* it (its code element, unwrapped).
|
|
68
|
+
*/
|
|
69
|
+
export function ChatCodePre({ children, }) {
|
|
70
|
+
const platform = useOptionalReactorPlatform();
|
|
71
|
+
useSyncExternalStore(platform?.subscribe ?? NO_SUBSCRIPTION, () => platform?.getRevision() ?? 0);
|
|
72
|
+
const block = platform ? codeBlockOf(children) : null;
|
|
73
|
+
const renderer = block
|
|
74
|
+
? codeBlockRenderer(platform.getContributions(ChatCodeBlock), block)
|
|
75
|
+
: undefined;
|
|
76
|
+
if (block && renderer) {
|
|
77
|
+
const { Component } = renderer;
|
|
78
|
+
return (_jsx("div", { "data-chat-code-block": renderer.id, children: _jsx(Component, { language: block.language, code: block.code }) }));
|
|
79
|
+
}
|
|
80
|
+
return _jsx(_Fragment, { children: children });
|
|
81
|
+
}
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* We subscribe to the chat store, detect the output type of the latest
|
|
15
15
|
* assistant message, auto-switch the sidebar tab, and render the payload
|
|
16
16
|
* inline in the sidebar.
|
|
17
|
+
*
|
|
18
|
+
* A spreadsheet (a ```csv block) is also drawn in the chat message itself, by
|
|
19
|
+
* the `AgentOutputsSheetsPlugin` renderer the chat reads from its
|
|
20
|
+
* `ChatCodeBlock` point; the message's grid and the Sheet tab's are one
|
|
21
|
+
* workbook. The Cells agent tools (`AgentOutputsSheetToolsPlugin`, the
|
|
22
|
+
* `cells` bundle the spec's `frontend_tools` names) act on the last one, so
|
|
23
|
+
* "Chart the spreadsheet" inserts a chart that appears on both grids.
|
|
17
24
|
*/
|
|
18
25
|
import React from 'react';
|
|
19
26
|
declare const AgentOutputsExample: React.FC;
|