@marimo-team/islands 0.20.5-dev18 → 0.20.5-dev20

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/main.js CHANGED
@@ -70404,7 +70404,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
70404
70404
  return Logger.warn("Failed to get version from mount config"), null;
70405
70405
  }
70406
70406
  }
70407
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.20.5-dev18"), showCodeInRunModeAtom = atom(true);
70407
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.20.5-dev20"), showCodeInRunModeAtom = atom(true);
70408
70408
  atom(null);
70409
70409
  var import_compiler_runtime$88 = require_compiler_runtime();
70410
70410
  function useKeydownOnElement(e, r) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.20.5-dev18",
3
+ "version": "0.20.5-dev20",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -81,9 +81,9 @@ export const MarimoIcon = ({
81
81
  width={width ?? size}
82
82
  height={height ?? size}
83
83
  viewBox={viewBox}
84
+ aria-hidden={true}
84
85
  {...props}
85
86
  >
86
- <title>marimo icon</title>
87
87
  <MarimoCirclePaths
88
88
  fill={fill}
89
89
  stroke={stroke ?? "currentColor"}
@@ -119,9 +119,9 @@ const MarimoMultiIcon = ({
119
119
  width={width ?? size}
120
120
  height={height ?? size}
121
121
  viewBox={viewBox}
122
+ aria-hidden={true}
122
123
  {...props}
123
124
  >
124
- <title>marimo multi icon</title>
125
125
  <defs>
126
126
  <mask id={maskId}>
127
127
  <rect width="100%" height="100%" fill="white" />
@@ -411,6 +411,85 @@ describe("RunStaleCellsTool", () => {
411
411
  });
412
412
  });
413
413
 
414
+ describe("output truncation", () => {
415
+ it("should summarize text/html output instead of dumping raw content", async () => {
416
+ const notebook = MockNotebook.notebookState({
417
+ cellData: {
418
+ [cellId1]: { code: "fig.show()", edited: true },
419
+ },
420
+ });
421
+ store.set(notebookAtom, notebook);
422
+
423
+ vi.mocked(runCells).mockImplementation(async () => {
424
+ const updatedNotebook = store.get(notebookAtom);
425
+ updatedNotebook.cellRuntime[cellId1] = {
426
+ ...updatedNotebook.cellRuntime[cellId1],
427
+ status: "idle",
428
+ };
429
+ store.set(notebookAtom, updatedNotebook);
430
+ });
431
+
432
+ const largeHtml = `<div>${"x".repeat(2_000_000)}</div>`;
433
+ vi.mocked(getCellContextData).mockReturnValue({
434
+ cellOutput: {
435
+ outputType: "text",
436
+ processedContent: null,
437
+ imageUrl: null,
438
+ output: { mimetype: "text/html", data: largeHtml },
439
+ },
440
+ consoleOutputs: null,
441
+ cellName: "cell1",
442
+ } as never);
443
+
444
+ const result = await tool.handler({}, toolContext as never);
445
+
446
+ expect(result.status).toBe("success");
447
+ const output = result.cellsToOutput?.[cellId1]?.cellOutput ?? "";
448
+ expect(output).toContain("HTML Output:");
449
+ expect(output).toContain("text/html");
450
+ expect(output.length).toBeLessThan(200);
451
+ expect(output).not.toContain(largeHtml);
452
+ });
453
+
454
+ it("should truncate large text output to MAX_TEXT_OUTPUT_CHARS", async () => {
455
+ const notebook = MockNotebook.notebookState({
456
+ cellData: {
457
+ [cellId1]: { code: "print(big_string)", edited: true },
458
+ },
459
+ });
460
+ store.set(notebookAtom, notebook);
461
+
462
+ vi.mocked(runCells).mockImplementation(async () => {
463
+ const updatedNotebook = store.get(notebookAtom);
464
+ updatedNotebook.cellRuntime[cellId1] = {
465
+ ...updatedNotebook.cellRuntime[cellId1],
466
+ status: "idle",
467
+ };
468
+ store.set(notebookAtom, updatedNotebook);
469
+ });
470
+
471
+ const largeText = "a".repeat(10_000);
472
+ vi.mocked(getCellContextData).mockReturnValue({
473
+ cellOutput: {
474
+ outputType: "text",
475
+ processedContent: largeText,
476
+ imageUrl: null,
477
+ output: { mimetype: "text/plain", data: largeText },
478
+ },
479
+ consoleOutputs: null,
480
+ cellName: "cell1",
481
+ } as never);
482
+
483
+ const result = await tool.handler({}, toolContext as never);
484
+
485
+ const output = result.cellsToOutput?.[cellId1]?.cellOutput ?? "";
486
+ expect(output).toContain("[TRUNCATED:");
487
+ expect(output).toContain("Full output visible in the notebook UI.");
488
+ // Output should be capped (2000 chars content + "Output:\n" prefix + truncation message)
489
+ expect(output.length).toBeLessThan(2200);
490
+ });
491
+ });
492
+
414
493
  describe("cell execution completion", () => {
415
494
  it("should complete immediately if cells are already idle", async () => {
416
495
  const notebook = MockNotebook.notebookState({
@@ -24,6 +24,11 @@ import type { CopilotMode } from "./registry";
24
24
  const POST_EXECUTION_DELAY = 200;
25
25
  const WAIT_FOR_CELLS_TIMEOUT = 30_000;
26
26
 
27
+ // Output size limits to prevent exceeding LLM token limits.
28
+ const MAX_TEXT_OUTPUT_CHARS = 2000;
29
+ const MAX_ERROR_OUTPUT_CHARS = 3000;
30
+ const MAX_TOOL_OUTPUT_CHARS = 40_000;
31
+
27
32
  interface CellOutput {
28
33
  consoleOutput?: string;
29
34
  cellOutput?: string;
@@ -118,42 +123,60 @@ export class RunStaleCellsTool
118
123
  const cellsToOutput = new Map<CellId, CellOutput | null>();
119
124
  let resultMessage = "";
120
125
  let outputHasErrors = false;
126
+ let totalOutputChars = 0;
121
127
 
122
128
  for (const cellId of staleCells) {
123
129
  const cellContextData = getCellContextData(cellId, updatedNotebook, {
124
130
  includeConsoleOutput: true,
125
131
  });
126
132
 
127
- let cellOutputString: string | undefined;
128
- let consoleOutputString: string | undefined;
129
-
130
133
  const cellOutput = cellContextData.cellOutput;
131
134
  const consoleOutputs = cellContextData.consoleOutputs;
132
135
  const hasConsoleOutput = consoleOutputs && consoleOutputs.length > 0;
133
136
 
137
+ // Track errors regardless of budget
138
+ if (cellOutput && this.outputHasErrors(cellOutput)) {
139
+ outputHasErrors = true;
140
+ }
141
+ if (
142
+ hasConsoleOutput &&
143
+ consoleOutputs.some((output) => this.outputHasErrors(output))
144
+ ) {
145
+ outputHasErrors = true;
146
+ }
147
+
134
148
  if (!cellOutput && !hasConsoleOutput) {
135
- // Set null to show no output
136
149
  cellsToOutput.set(cellId, null);
137
150
  continue;
138
151
  }
139
152
 
153
+ // If total budget exceeded, summarize remaining cells
154
+ if (totalOutputChars >= MAX_TOOL_OUTPUT_CHARS) {
155
+ cellsToOutput.set(cellId, {
156
+ cellOutput: "Cell executed (output omitted due to context limits).",
157
+ });
158
+ continue;
159
+ }
160
+
161
+ let cellOutputString: string | undefined;
162
+ let consoleOutputString: string | undefined;
163
+
140
164
  if (cellOutput) {
141
165
  cellOutputString = this.formatOutputString(cellOutput);
142
- if (this.outputHasErrors(cellOutput)) {
143
- outputHasErrors = true;
144
- }
166
+ totalOutputChars += cellOutputString.length;
145
167
  }
146
168
 
147
169
  if (hasConsoleOutput) {
148
170
  consoleOutputString = consoleOutputs
149
171
  .map((output) => this.formatOutputString(output))
150
172
  .join("\n");
173
+ consoleOutputString = this.truncateString(
174
+ consoleOutputString,
175
+ MAX_TEXT_OUTPUT_CHARS,
176
+ );
177
+ totalOutputChars += consoleOutputString.length;
151
178
  resultMessage +=
152
179
  "Console output represents the stdout or stderr of the cell (eg. print statements).";
153
-
154
- if (consoleOutputs.some((output) => this.outputHasErrors(output))) {
155
- outputHasErrors = true;
156
- }
157
180
  }
158
181
 
159
182
  cellsToOutput.set(cellId, {
@@ -199,13 +222,29 @@ export class RunStaleCellsTool
199
222
  let outputString = "";
200
223
  const { outputType, processedContent, imageUrl, output } = cellOutput;
201
224
  if (outputType === "text") {
202
- outputString += "Output:\n";
203
- if (processedContent) {
204
- outputString += processedContent;
205
- } else if (typeof output.data === "string") {
206
- outputString += output.data;
225
+ if (output.mimetype === "text/html") {
226
+ // text/html (e.g. plotly figures, rich dataframes) can be millions of
227
+ // chars and is not interpretable by LLMs — summarize instead
228
+ const dataLength =
229
+ typeof output.data === "string"
230
+ ? output.data.length
231
+ : JSON.stringify(output.data).length;
232
+ outputString += `HTML Output: text/html content (${dataLength.toLocaleString()} chars). Full output visible in notebook UI.`;
207
233
  } else {
208
- outputString += JSON.stringify(output.data);
234
+ const isError = this.outputHasErrors(cellOutput);
235
+ const maxChars = isError
236
+ ? MAX_ERROR_OUTPUT_CHARS
237
+ : MAX_TEXT_OUTPUT_CHARS;
238
+ outputString += "Output:\n";
239
+ let content: string;
240
+ if (processedContent) {
241
+ content = processedContent;
242
+ } else if (typeof output.data === "string") {
243
+ content = output.data;
244
+ } else {
245
+ content = JSON.stringify(output.data);
246
+ }
247
+ outputString += this.truncateString(content, maxChars);
209
248
  }
210
249
  } else if (outputType === "media") {
211
250
  outputString += `Media Output: Contains ${output.mimetype} content`;
@@ -216,6 +255,13 @@ export class RunStaleCellsTool
216
255
  return outputString;
217
256
  }
218
257
 
258
+ private truncateString(str: string, maxLength: number): string {
259
+ if (str.length <= maxLength) {
260
+ return str;
261
+ }
262
+ return `${str.slice(0, maxLength)}\n\n[TRUNCATED: ${str.length.toLocaleString()} → ${maxLength.toLocaleString()} chars. Full output visible in the notebook UI.]`;
263
+ }
264
+
219
265
  /**
220
266
  * Wait for cells to finish executing (status becomes "idle")
221
267
  * Returns true if all cells finished executing, false if the timeout was reached