@datalayer/agent-runtimes 1.5.88 → 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.
@@ -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;
@@ -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;
@@ -19,6 +19,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
19
19
  * We subscribe to the chat store, detect the output type of the latest
20
20
  * assistant message, auto-switch the sidebar tab, and render the payload
21
21
  * inline in the sidebar.
22
+ *
23
+ * A spreadsheet (a ```csv block) is also drawn in the chat message itself, by
24
+ * the `AgentOutputsSheetsPlugin` renderer the chat reads from its
25
+ * `ChatCodeBlock` point; the message's grid and the Sheet tab's are one
26
+ * workbook. The Cells agent tools (`AgentOutputsSheetToolsPlugin`, the
27
+ * `cells` bundle the spec's `frontend_tools` names) act on the last one, so
28
+ * "Chart the spreadsheet" inserts a chart that appears on both grids.
22
29
  */
23
30
  /// <reference types="vite/client" />
24
31
  import { useEffect, useState, useRef, useMemo } from 'react';
@@ -26,15 +33,16 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
26
33
  import { Text, Button, Heading, Label } from '@primer/react';
27
34
  import { TableIcon, FileIcon, CodeIcon, GraphIcon, DownloadIcon, } from '@primer/octicons-react';
28
35
  import { Box } from '@datalayer/primer-addons';
36
+ import { CellsProvider, Spreadsheet } from '@datalayer/cells';
29
37
  import { AuthRequiredView } from './components/index.js';
30
38
  import { useSimpleAuthStore } from '@datalayer/core/lib/views/otel/index.js';
31
39
  import { ThemedProvider } from './utils/themedProvider.js';
32
40
  import { uniqueAgentId } from './utils/agentId.js';
33
41
  import { LoopEmbed } from '../apps/index.js';
34
- import { AgentOutputsPlugin } from '../apps/plugins/agent-outputs/index.js';
42
+ import { downloadSheet, parseSheetBlock, useFitHeight, useSheetModel, } from '../apps/plugins/agent-outputs/sheets.js';
43
+ import { AGENT_OUTPUTS_LOOP_PLUGINS } from './utils/agentOutputsPlugins.js';
35
44
  import { useChatStore } from '../stores/chatStore.js';
36
45
  import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl.js';
37
- const LOOP_PLUGINS_AGENTOUT = [AgentOutputsPlugin];
38
46
  const queryClient = new QueryClient();
39
47
  // ─── Constants ─────────────────────────────────────────────────────────────
40
48
  const AGENT_NAME = 'outputs-example-agent';
@@ -57,8 +65,6 @@ const expandCompactMarkdownTable = (text) => {
57
65
  return text.replace(/\|\|/g, '|\n|');
58
66
  };
59
67
  const EXT_LIKE_INFOS = new Set([
60
- 'csv',
61
- 'tsv',
62
68
  'txt',
63
69
  'md',
64
70
  'markdown',
@@ -119,6 +125,20 @@ const detectOutput = (m) => {
119
125
  }
120
126
  // File: info string is a known extension OR first line declares a filename.
121
127
  const filenameMatch = firstLine.match(/^#\s*filename:\s*([\w.-]+)/i);
128
+ // Spreadsheet: a CSV or TSV, by its fence (`csv`, `tsv`, `spreadsheet`,
129
+ // `cells`) or a `# filename:` ending in .csv or .tsv — read as the chat's
130
+ // renderer reads it, so both grids show the same workbook.
131
+ const sheet = parseSheetBlock(info, rawBody);
132
+ if (sheet) {
133
+ return {
134
+ tab: 'sheet',
135
+ payload: sheet.source,
136
+ filename: sheet.name,
137
+ extension: sheet.extension,
138
+ sheet,
139
+ messageId: m.id,
140
+ };
141
+ }
122
142
  if (filenameMatch || EXT_LIKE_INFOS.has(info)) {
123
143
  const filename = filenameMatch?.[1] ?? (info ? `output.${info}` : 'output.txt');
124
144
  return {
@@ -257,6 +277,24 @@ const ChartView = ({ source }) => {
257
277
  }
258
278
  return (_jsx(ReactECharts, { option: option, style: { height: 280, width: '100%' } }));
259
279
  };
280
+ // ─── Spreadsheet renderer (Datalayer Cells) ───────────────────────────────
281
+ /**
282
+ * A CSV or TSV the agent wrote, in a Datalayer Cells grid: the workbook the
283
+ * chat message shows too (read once, on this thread), editable here, and
284
+ * where the agent's chart goes.
285
+ */
286
+ const SheetView = ({ block }) => {
287
+ const { model, error } = useSheetModel(block);
288
+ // As tall as its data and charts (up to 640 px), so a chart the agent adds shows.
289
+ const height = useFitHeight(model, 200, 640);
290
+ if (error) {
291
+ return (_jsxs(Text, { sx: { color: 'danger.fg', fontSize: 0 }, children: ["Could not read the spreadsheet: ", error] }));
292
+ }
293
+ if (!model) {
294
+ return _jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: "Reading\u2026" });
295
+ }
296
+ return (_jsx(CellsProvider, { model: model, children: _jsx(Box, { height: height, border: "1px solid", borderColor: "border.muted", children: _jsx(Spreadsheet, { model: model, "aria-label": block.name }) }) }));
297
+ };
260
298
  // ─── Inner component (rendered after auth) ─────────────────────────────────
261
299
  const AgentOutputsInner = () => {
262
300
  // The Loop creates the agent from the plugin's blueprint when it connects;
@@ -312,10 +350,11 @@ const AgentOutputsInner = () => {
312
350
  document.body.removeChild(a);
313
351
  URL.revokeObjectURL(url);
314
352
  };
315
- return (_jsxs(Box, { height: "100%", display: "flex", flexDirection: "column", children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, px: 3, py: 2, borderBottom: "1px solid", borderColor: "border.default", flexShrink: 0, children: [_jsx(TableIcon, { size: 16 }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, flex: 1 }, children: "Agent Outputs \u2014 Local Runtime" })] }), _jsxs(Box, { flex: 1, minHeight: 0, display: "flex", children: [_jsx(Box, { flex: 1, minWidth: 0, children: _jsx(LoopEmbed, { serverUrl: agentBaseUrl, target: "local", showAgentVariants: true, agentId: agentName, editors: false, showHeader: true, plugins: LOOP_PLUGINS_AGENTOUT }) }), _jsxs(Box, { width: 420, borderLeft: "1px solid", borderColor: "border.default", display: "flex", flexDirection: "column", overflow: "hidden", children: [_jsx(Box, { display: "flex", borderBottom: "1px solid", borderColor: "border.default", flexShrink: 0, children: [
353
+ return (_jsxs(Box, { height: "100%", display: "flex", flexDirection: "column", children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, px: 3, py: 2, borderBottom: "1px solid", borderColor: "border.default", flexShrink: 0, children: [_jsx(TableIcon, { size: 16 }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, flex: 1 }, children: "Agent Outputs \u2014 Local Runtime" })] }), _jsxs(Box, { flex: 1, minHeight: 0, display: "flex", children: [_jsx(Box, { flex: 1, minWidth: 0, children: _jsx(LoopEmbed, { serverUrl: agentBaseUrl, target: "local", showAgentVariants: true, agentId: agentName, editors: false, showHeader: true, plugins: AGENT_OUTPUTS_LOOP_PLUGINS }) }), _jsxs(Box, { width: 420, borderLeft: "1px solid", borderColor: "border.default", display: "flex", flexDirection: "column", overflow: "hidden", children: [_jsx(Box, { display: "flex", borderBottom: "1px solid", borderColor: "border.default", flexShrink: 0, children: [
316
354
  { key: 'table', icon: TableIcon, label: 'Table' },
317
355
  { key: 'json', icon: CodeIcon, label: 'JSON' },
318
356
  { key: 'chart', icon: GraphIcon, label: 'Chart' },
357
+ { key: 'sheet', icon: TableIcon, label: 'Sheet' },
319
358
  { key: 'files', icon: FileIcon, label: 'Files' },
320
359
  ].map(t => {
321
360
  const n = countByTab(t.key);
@@ -328,14 +367,15 @@ const AgentOutputsInner = () => {
328
367
  borderColor: activeTab === t.key ? 'accent.fg' : 'transparent',
329
368
  fontWeight: activeTab === t.key ? 'bold' : 'normal',
330
369
  }, children: [t.label, n > 0 ? ` (${n})` : ''] }, t.key));
331
- }) }), _jsxs(Box, { p: 3, flex: 1, overflow: "auto", children: [_jsxs(Heading, { as: "h4", sx: { fontSize: 1, mb: 2 }, children: [activeTab.charAt(0).toUpperCase() + activeTab.slice(1), " outputs"] }), filtered.length === 0 ? (_jsxs(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: ["No ", activeTab, " outputs yet. Use one of the openers in the chat to produce one \u2014 the side panel will switch to the matching tab automatically."] })) : (filtered.map((d, idx) => (_jsxs(Box, { p: 2, mb: 2, border: "1px solid", borderColor: "border.default", borderRadius: 2, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, gap: 2, children: [_jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold' }, children: d.filename ?? `${d.tab} output` }), _jsx(Label, { size: "small", variant: "secondary", children: d.tab })] }), d.tab === 'table' && _jsx(MarkdownTable, { source: d.payload }), d.tab === 'json' && (_jsx(Box, { as: "pre", bg: "canvas.default", p: 2, borderRadius: 2, fontFamily: "mono", fontSize: 0, maxHeight: 320, overflow: "auto", whiteSpace: "pre-wrap", m: 0, children: (() => {
370
+ }) }), _jsxs(Box, { p: 3, flex: 1, overflow: "auto", children: [_jsxs(Heading, { as: "h4", sx: { fontSize: 1, mb: 2 }, children: [activeTab.charAt(0).toUpperCase() + activeTab.slice(1), " outputs"] }), filtered.length === 0 ? (_jsxs(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: ["No ", activeTab, " outputs yet. Use one of the openers in the chat to produce one \u2014 the side panel will switch to the matching tab automatically.", activeTab === 'sheet' &&
371
+ ' For a spreadsheet, ask for the data as CSV, for example: "Give me twelve months of sales by region as a CSV".'] })) : (filtered.map((d, idx) => (_jsxs(Box, { p: 2, mb: 2, border: "1px solid", borderColor: "border.default", borderRadius: 2, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, gap: 2, children: [_jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold' }, children: d.filename ?? `${d.tab} output` }), _jsx(Label, { size: "small", variant: "secondary", children: d.tab })] }), d.tab === 'table' && _jsx(MarkdownTable, { source: d.payload }), d.tab === 'json' && (_jsx(Box, { as: "pre", bg: "canvas.default", p: 2, borderRadius: 2, fontFamily: "mono", fontSize: 0, maxHeight: 320, overflow: "auto", whiteSpace: "pre-wrap", m: 0, children: (() => {
332
372
  try {
333
373
  return JSON.stringify(JSON.parse(d.payload), null, 2);
334
374
  }
335
375
  catch {
336
376
  return d.payload;
337
377
  }
338
- })() })), d.tab === 'chart' && _jsx(ChartView, { source: d.payload }), d.tab === 'files' && (_jsxs(_Fragment, { children: [_jsx(Box, { as: "pre", bg: "canvas.default", p: 2, borderRadius: 2, fontFamily: "mono", fontSize: 0, maxHeight: 200, overflow: "auto", whiteSpace: "pre-wrap", m: 0, mb: 2, children: d.payload }), _jsxs(Button, { size: "small", leadingVisual: DownloadIcon, onClick: () => download(d), children: ["Download ", d.filename ?? 'file'] })] }))] }, d.messageId + ':' + idx))))] })] })] })] }));
378
+ })() })), d.tab === 'chart' && _jsx(ChartView, { source: d.payload }), d.tab === 'sheet' && d.sheet && (_jsxs(_Fragment, { children: [_jsx(SheetView, { block: d.sheet }), _jsx(Box, { mt: 2, children: _jsxs(Button, { size: "small", leadingVisual: DownloadIcon, onClick: () => downloadSheet(d.sheet), children: ["Download ", d.filename ?? 'spreadsheet'] }) })] })), d.tab === 'files' && (_jsxs(_Fragment, { children: [_jsx(Box, { as: "pre", bg: "canvas.default", p: 2, borderRadius: 2, fontFamily: "mono", fontSize: 0, maxHeight: 200, overflow: "auto", whiteSpace: "pre-wrap", m: 0, mb: 2, children: d.payload }), _jsxs(Button, { size: "small", leadingVisual: DownloadIcon, onClick: () => download(d), children: ["Download ", d.filename ?? 'file'] })] }))] }, d.messageId + ':' + idx))))] })] })] })] }));
339
379
  };
340
380
  // ─── Sync token to core IAM store ──────────────────────────────────────────
341
381
  const syncTokenToIamStore = (token) => {
@@ -0,0 +1,2 @@
1
+ /** The Loop's plugins on the outputs page: the agent, the chat's grids, the grid's tools. */
2
+ export declare const AGENT_OUTPUTS_LOOP_PLUGINS: import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, unknown>[];
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Loop plugins of the outputs example (`AgentOutputsExample`): the
7
+ * `example-output` agent, the chat's spreadsheet renderer, and the Cells
8
+ * agent tools on the last spreadsheet answered. The agent runs on a server,
9
+ * so the page names the bundle whose tools it hands it — `cells`, the grid's
10
+ * tools, run on this page's reactor — through the chat extras.
11
+ *
12
+ * Apart from the page, so a test reads the list without mounting the Loop.
13
+ *
14
+ * @module examples/utils/agentOutputsPlugins
15
+ */
16
+ import { AgentOutputsPlugin } from '../../apps/plugins/agent-outputs/index.js';
17
+ import { AgentOutputsSheetsPlugin, AgentOutputsSheetToolsPlugin, } from '../../apps/plugins/agent-outputs/sheets.js';
18
+ import { createChatExtrasPlugin } from '../../apps/plugins/chat-extras/index.js';
19
+ const SHEET_EXTRAS = createChatExtrasPlugin();
20
+ SHEET_EXTRAS.setExtras({ agentToolBundles: ['cells'] });
21
+ /** The Loop's plugins on the outputs page: the agent, the chat's grids, the grid's tools. */
22
+ export const AGENT_OUTPUTS_LOOP_PLUGINS = [
23
+ AgentOutputsPlugin,
24
+ AgentOutputsSheetsPlugin,
25
+ AgentOutputsSheetToolsPlugin,
26
+ SHEET_EXTRAS.plugin,
27
+ ];