@marimo-team/islands 0.20.5-dev5 → 0.20.5-dev52

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 (169) hide show
  1. package/dist/{Combination-Du-o_hC9.js → Combination-Dk6JxauT.js} +1 -1
  2. package/dist/{ConnectedDataExplorerComponent-DUS-zJoR.js → ConnectedDataExplorerComponent-B07FkeWC.js} +10 -10
  3. package/dist/{any-language-editor-BL9o7y0_.js → any-language-editor-BIj11a2e.js} +19 -19
  4. package/dist/{architectureDiagram-VXUJARFQ-DrJeyFHq.js → architectureDiagram-VXUJARFQ-IZt4NuSd.js} +5 -5
  5. package/dist/{blockDiagram-VD42YOAC-BJrP6qKc.js → blockDiagram-VD42YOAC-mhFHC3Ty.js} +5 -5
  6. package/dist/{button-KYalaJYu.js → button-DQpBib29.js} +24 -11
  7. package/dist/{c4Diagram-YG6GDRKO-Bo4gytQ5.js → c4Diagram-YG6GDRKO-BzStmvfT.js} +4 -4
  8. package/dist/{channel-IWLGkaBE.js → channel-CUFaIkTh.js} +1 -1
  9. package/dist/{check-C50jsehH.js → check-DpqPQmzz.js} +1 -1
  10. package/dist/{chunk-ABZYJK2D-CRwanrkd.js → chunk-ABZYJK2D-7QYXAAhe.js} +1 -1
  11. package/dist/{chunk-ATLVNIR6-CMMCMvOK.js → chunk-ATLVNIR6-pmHPAPSd.js} +1 -1
  12. package/dist/{chunk-B4BG7PRW-BNsHrGHG.js → chunk-B4BG7PRW-C9mfKT9i.js} +4 -4
  13. package/dist/{chunk-DI55MBZ5-DQeYbfMV.js → chunk-DI55MBZ5-IKrK49rX.js} +4 -4
  14. package/dist/{chunk-EXTU4WIE-CV_DQeaX.js → chunk-EXTU4WIE-BRFl4iNd.js} +1 -1
  15. package/dist/{chunk-JA3XYJ7Z-Cmt--e0q.js → chunk-JA3XYJ7Z-C9q_MXZQ.js} +2 -2
  16. package/dist/{chunk-JZLCHNYA-CkyMJnI9.js → chunk-JZLCHNYA-DVjoFib5.js} +4 -4
  17. package/dist/{chunk-N4CR4FBY-BJfHtJbD.js → chunk-N4CR4FBY-BYr5N5mX.js} +5 -5
  18. package/dist/{chunk-QN33PNHL-WOLIPUAJ.js → chunk-QN33PNHL-CXfJywHv.js} +1 -1
  19. package/dist/{chunk-QXUST7PY-DYuD50pU.js → chunk-QXUST7PY-YO0PM8b3.js} +5 -5
  20. package/dist/{chunk-S3R3BYOJ-CsnX6RKs.js → chunk-S3R3BYOJ-DgI4FlvW.js} +1 -1
  21. package/dist/{chunk-TZMSLE5B-B3eYTGCw.js → chunk-TZMSLE5B-DSfBOnzx.js} +1 -1
  22. package/dist/{classDiagram-2ON5EDUG-C7C-oefv.js → classDiagram-2ON5EDUG-CvpnTWzz.js} +10 -10
  23. package/dist/{classDiagram-v2-WZHVMYZB-UTw37Gg8.js → classDiagram-v2-WZHVMYZB-DEQrBHLI.js} +10 -10
  24. package/dist/{copy-oc-FcZzt.js → copy-BkBF0Xgk.js} +2 -2
  25. package/dist/{dagre-6UL2VRFP-BgsUhJrV.js → dagre-6UL2VRFP-DC-emrm5.js} +7 -7
  26. package/dist/{diagram-PSM6KHXK-BIUUOfKo.js → diagram-PSM6KHXK-BAgNlpL8.js} +6 -6
  27. package/dist/{diagram-QEK2KX5R-BFjolZQv.js → diagram-QEK2KX5R-BM7QE5WA.js} +4 -4
  28. package/dist/{diagram-S2PKOQOG-4jfkWoZw.js → diagram-S2PKOQOG-qs4mB1gW.js} +4 -4
  29. package/dist/dist-B4MxkKHf.js +8 -0
  30. package/dist/{dist-De9X_Des.js → dist-B9EjSb9T.js} +1 -1
  31. package/dist/{dist-IW_ARJ3S.js → dist-BFxYppVR.js} +4 -4
  32. package/dist/{dist-D7ZGWV_9.js → dist-BGZ7TWS9.js} +3 -3
  33. package/dist/{dist-CwtEWuFb.js → dist-BSfYc7vq.js} +2 -2
  34. package/dist/{dist-DMS81OrU.js → dist-BUrWeMEP.js} +1 -1
  35. package/dist/dist-BYghZv6b.js +5 -0
  36. package/dist/dist-Be-uQhz5.js +6 -0
  37. package/dist/{dist-Ch_JuCvc.js → dist-BpMlUdNO.js} +3 -3
  38. package/dist/{dist-C6z8U-ms.js → dist-Bq5eYK43.js} +2 -2
  39. package/dist/{dist-BFL9TlzD.js → dist-Bq9zYwJs.js} +5 -5
  40. package/dist/{dist-7ZF--V_D.js → dist-C4K7pumm.js} +2 -2
  41. package/dist/{dist-Qjf6pcqK.js → dist-CAKwXCWI.js} +2 -2
  42. package/dist/dist-CB_xf0ju.js +5 -0
  43. package/dist/{dist-BwQHkjA9.js → dist-CDHl2i1x.js} +4 -4
  44. package/dist/dist-CK0qFAbF.js +8 -0
  45. package/dist/{dist-C4XMUaob.js → dist-CPlGUbk-.js} +2 -2
  46. package/dist/{dist-BT6_J2eq.js → dist-CSEWGuDq.js} +7 -2
  47. package/dist/dist-CYEk-qrr.js +8 -0
  48. package/dist/{dist-CYo3w-nC.js → dist-Cl5iM8xL.js} +3 -3
  49. package/dist/dist-CmKoWpMk.js +5 -0
  50. package/dist/{dist-I8MQW60_.js → dist-CseYuPtL.js} +2 -2
  51. package/dist/dist-D1nf4IQl.js +5 -0
  52. package/dist/{dist-CsqiXw7J.js → dist-D4gcY469.js} +2 -2
  53. package/dist/{dist-DUxS2paD.js → dist-D5NMgbbv.js} +2 -2
  54. package/dist/{dist-UYm1IE5s.js → dist-DERtJN02.js} +2 -2
  55. package/dist/{dist-CFToYDWO.js → dist-DEj2X26M.js} +2 -2
  56. package/dist/{dist-BuapEdlD.js → dist-DOoqn-VL.js} +70 -67
  57. package/dist/{dist-BLThQiU4.js → dist-DUretbKK.js} +2 -2
  58. package/dist/{dist-DEFZ7dnD.js → dist-D_-CGmlh.js} +2 -2
  59. package/dist/dist-Df3AcKpt.js +6 -0
  60. package/dist/dist-DgaFHt_I.js +5 -0
  61. package/dist/dist-Dk10C3ui.js +5 -0
  62. package/dist/{dist-D0f6Yrrb.js → dist-DodLQWPg.js} +1 -1
  63. package/dist/dist-DtyPVMHR.js +5 -0
  64. package/dist/{dist-Cb3cLT39.js → dist-HoZO6brh.js} +2 -2
  65. package/dist/{dist-Cqpjy6bK.js → dist-RNGn_-uD.js} +1 -1
  66. package/dist/{dist-BBcqvpvP.js → dist-Ux6dL_VB.js} +1 -1
  67. package/dist/{dist-B8Y11RWn.js → dist-WIWVvdBh.js} +2 -2
  68. package/dist/{dist-CB6qhQ8K.js → dist-gc9KgJuA.js} +1 -1
  69. package/dist/{dist-ovDpXuSB.js → dist-i-ud9aCA.js} +1 -1
  70. package/dist/dist-ko7WnHAO.js +5 -0
  71. package/dist/{dist-BTQbjEKU.js → dist-lNe4i1Nm.js} +1 -1
  72. package/dist/dist-of7gLRFK.js +8 -0
  73. package/dist/{erDiagram-Q2GNP2WA-Cq5Bz5lG.js → erDiagram-Q2GNP2WA-Dh5nhgY3.js} +10 -10
  74. package/dist/{error-banner-D0tXnwl4.js → error-banner-BctofTCP.js} +2 -2
  75. package/dist/{esm-BxMbHo0y.js → esm-BBkPJL8N.js} +29 -27
  76. package/dist/{flowDiagram-NV44I4VS-6WPJVFl7.js → flowDiagram-NV44I4VS-ChR1Vbmj.js} +10 -10
  77. package/dist/{ganttDiagram-JELNMOA3-AfDhh9CI.js → ganttDiagram-JELNMOA3-sK0z-5KM.js} +3 -3
  78. package/dist/{gitGraphDiagram-V2S2FVAM-BRSwuj0Q.js → gitGraphDiagram-V2S2FVAM-9S1VqQrL.js} +3 -3
  79. package/dist/{glide-data-editor-ByPNTNVG.js → glide-data-editor-DI5VFwRB.js} +63 -63
  80. package/dist/{infoDiagram-HS3SLOUP-Cmxo6jKx.js → infoDiagram-HS3SLOUP-C5A8b-2O.js} +3 -3
  81. package/dist/{journeyDiagram-XKPGCS4Q-CKYr8cSR.js → journeyDiagram-XKPGCS4Q-D5BIjS4N.js} +3 -3
  82. package/dist/{kanban-definition-3W4ZIXB7-DVvAZzQD.js → kanban-definition-3W4ZIXB7-C1vZZabj.js} +7 -7
  83. package/dist/{label-CV0KYhtH.js → label-Cx28eo0O.js} +5 -5
  84. package/dist/{loader-eJCvvApN.js → loader-C62dRCuy.js} +1 -1
  85. package/dist/main.js +1542 -1072
  86. package/dist/{mermaid-COOB_abB.js → mermaid-BgeZPIms.js} +41 -41
  87. package/dist/{mindmap-definition-VGOIOE7T-1ExmnvYy.js → mindmap-definition-VGOIOE7T-Cn9_H_5f.js} +9 -9
  88. package/dist/{pieDiagram-ADFJNKIX-CJlIsdsU.js → pieDiagram-ADFJNKIX-iA0mvRW9.js} +4 -4
  89. package/dist/{purify.es-CyOIw8ru.js → purify.es-DGenX2XH.js} +67 -67
  90. package/dist/{quadrantDiagram-AYHSOK5B-BU78RiaH.js → quadrantDiagram-AYHSOK5B-CAcVWXc-.js} +2 -2
  91. package/dist/{requirementDiagram-UZGBJVZJ-DACHtrFr.js → requirementDiagram-UZGBJVZJ-1HxQ6I5Z.js} +9 -9
  92. package/dist/{sankeyDiagram-TZEHDZUN-Bzg7_UWs.js → sankeyDiagram-TZEHDZUN-BVJnR4_b.js} +2 -2
  93. package/dist/{sequenceDiagram-WL72ISMW-agybEe9J.js → sequenceDiagram-WL72ISMW-ByirOtHb.js} +4 -4
  94. package/dist/{slides-component-B0yK5GXP.js → slides-component-DwvL_HJi.js} +2 -2
  95. package/dist/{spec-Dq_reDGM.js → spec-B8V2Bcbi.js} +4 -4
  96. package/dist/{stateDiagram-FKZM4ZOC-DehQAt8g.js → stateDiagram-FKZM4ZOC-DrYNXdQr.js} +10 -10
  97. package/dist/{stateDiagram-v2-4FDKWEC3-8VzeREl9.js → stateDiagram-v2-4FDKWEC3-C9CFKCSr.js} +10 -10
  98. package/dist/style.css +1 -1
  99. package/dist/{timeline-definition-IT6M3QCI-CdCfdaCF.js → timeline-definition-IT6M3QCI-D8B3p7ID.js} +2 -2
  100. package/dist/{tooltip-CL8m4f9y.js → tooltip-SPkubVH3.js} +3 -3
  101. package/dist/{types-BwnzGcE4.js → types-DqrGPzsT.js} +517 -406
  102. package/dist/{useAsyncData-B4hMFGnF.js → useAsyncData-Ioeh75f8.js} +1 -1
  103. package/dist/{useDeepCompareMemoize-DuPhOXzr.js → useDeepCompareMemoize-DtbTAJq3.js} +4 -4
  104. package/dist/{useIframeCapabilities-CAt6D2EI.js → useIframeCapabilities-DFGZKWkO.js} +1 -1
  105. package/dist/{useTheme-BNYQnvu-.js → useTheme-OvBNH9t3.js} +2 -2
  106. package/dist/{vega-component-DouPy8AI.js → vega-component-B_4Lp3hK.js} +8 -8
  107. package/dist/{xychartDiagram-PRI3JC2R-rEm_SIsC.js → xychartDiagram-PRI3JC2R-KuxgQuK9.js} +5 -5
  108. package/package.json +9 -9
  109. package/src/__mocks__/requests.ts +1 -0
  110. package/src/components/app-config/ai-config.tsx +10 -0
  111. package/src/components/datasources/components.tsx +3 -6
  112. package/src/components/datasources/datasources.tsx +8 -21
  113. package/src/components/editor/actions/types.ts +6 -1
  114. package/src/components/editor/actions/useNotebookActions.tsx +50 -13
  115. package/src/components/editor/chrome/types.ts +17 -0
  116. package/src/components/editor/controls/command-palette.tsx +7 -0
  117. package/src/components/editor/controls/keyboard-shortcuts.tsx +3 -1
  118. package/src/components/editor/file-tree/file-explorer.tsx +48 -62
  119. package/src/components/editor/file-tree/file-icons.tsx +132 -0
  120. package/src/components/editor/file-tree/file-viewer.tsx +1 -1
  121. package/src/components/editor/file-tree/tree-actions.tsx +107 -0
  122. package/src/components/editor/file-tree/types.ts +2 -96
  123. package/src/components/editor/header/filename-input.tsx +4 -1
  124. package/src/components/icons/marimo-icons.tsx +2 -2
  125. package/src/components/pages/home-page.tsx +5 -5
  126. package/src/components/storage/__tests__/storage-snippets.test.ts +253 -0
  127. package/src/components/storage/components.tsx +0 -38
  128. package/src/components/storage/storage-file-viewer.tsx +1 -1
  129. package/src/components/storage/storage-inspector.tsx +65 -50
  130. package/src/components/storage/storage-snippets.ts +67 -0
  131. package/src/components/ui/command.tsx +2 -0
  132. package/src/components/ui/links.tsx +1 -0
  133. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +206 -0
  134. package/src/core/ai/tools/run-cells-tool.ts +75 -40
  135. package/src/core/hotkeys/__tests__/hotkeys.test.ts +64 -1
  136. package/src/core/hotkeys/hotkeys.ts +29 -3
  137. package/src/core/islands/bridge.ts +1 -0
  138. package/src/core/network/__tests__/requests-network.test.ts +17 -0
  139. package/src/core/network/requests-lazy.ts +1 -0
  140. package/src/core/network/requests-network.ts +9 -0
  141. package/src/core/network/requests-static.ts +1 -0
  142. package/src/core/network/requests-toasting.tsx +1 -0
  143. package/src/core/network/types.ts +1 -0
  144. package/src/core/storage/__tests__/state.test.ts +1 -0
  145. package/src/core/wasm/bridge.ts +1 -0
  146. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -4
  147. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +309 -0
  148. package/src/plugins/impl/mpl-interactive/__tests__/mpl-websocket-shim.test.ts +110 -0
  149. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +57 -0
  150. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +8 -2
  151. package/src/plugins/plugins.ts +2 -0
  152. package/src/utils/__tests__/filenames.test.ts +7 -0
  153. package/src/utils/__tests__/smartMatch.test.ts +61 -0
  154. package/src/utils/filenames.ts +3 -0
  155. package/src/utils/smartMatch.ts +62 -0
  156. package/dist/dist-BAeGo2rp.js +0 -5
  157. package/dist/dist-BqwCMSEa.js +0 -5
  158. package/dist/dist-Bum8FwTO.js +0 -6
  159. package/dist/dist-C0YiOwt_.js +0 -5
  160. package/dist/dist-C2uPv4iU.js +0 -5
  161. package/dist/dist-C5hOLsJN.js +0 -8
  162. package/dist/dist-C9NIAKMs.js +0 -8
  163. package/dist/dist-CCrzTtvk.js +0 -5
  164. package/dist/dist-CFS9i1rS.js +0 -8
  165. package/dist/dist-CyHZuhPH.js +0 -5
  166. package/dist/dist-CzcjWdIk.js +0 -6
  167. package/dist/dist-DaYyUSNC.js +0 -5
  168. package/dist/dist-DpDcJYNh.js +0 -8
  169. package/dist/dist-U_BfxcPn.js +0 -5
@@ -411,6 +411,212 @@ 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
+ it("should omit output for cells that exceed total output budget", async () => {
493
+ const cellIds = Array.from(
494
+ { length: 25 },
495
+ (_, i) => `budget-cell-${i}` as CellId,
496
+ );
497
+ const cellData: Record<string, { code: string; edited: boolean }> = {};
498
+ for (const id of cellIds) {
499
+ cellData[id] = { code: "x = 1", edited: true };
500
+ }
501
+
502
+ const notebook = MockNotebook.notebookState({ cellData });
503
+ store.set(notebookAtom, notebook);
504
+
505
+ vi.mocked(runCells).mockImplementation(async () => {
506
+ const updatedNotebook = store.get(notebookAtom);
507
+ for (const id of cellIds) {
508
+ updatedNotebook.cellRuntime[id] = {
509
+ ...updatedNotebook.cellRuntime[id],
510
+ status: "idle",
511
+ };
512
+ }
513
+ store.set(notebookAtom, updatedNotebook);
514
+ });
515
+
516
+ // Each cell produces ~2008 chars of formatted output ("Output:\n" + 2000 chars).
517
+ // After 20 cells the running total exceeds MAX_TOOL_OUTPUT_CHARS (40,000).
518
+ const content = "a".repeat(2000);
519
+ vi.mocked(getCellContextData).mockReturnValue({
520
+ cellOutput: {
521
+ outputType: "text",
522
+ processedContent: content,
523
+ imageUrl: null,
524
+ output: { mimetype: "text/plain", data: content },
525
+ },
526
+ consoleOutputs: null,
527
+ cellName: "cell",
528
+ } as never);
529
+
530
+ const result = await tool.handler({}, toolContext as never);
531
+
532
+ expect(result.cellsToOutput?.[cellIds[0]]?.cellOutput).toContain(
533
+ "Output:",
534
+ );
535
+ expect(result.cellsToOutput?.[cellIds[24]]?.cellOutput).toBe(
536
+ "Cell executed (output omitted due to context limits).",
537
+ );
538
+ });
539
+
540
+ it("should use higher truncation limit for error outputs", async () => {
541
+ const notebook = MockNotebook.notebookState({
542
+ cellData: {
543
+ [cellId1]: { code: "raise Exception()", edited: true },
544
+ },
545
+ });
546
+ store.set(notebookAtom, notebook);
547
+
548
+ vi.mocked(runCells).mockImplementation(async () => {
549
+ const updatedNotebook = store.get(notebookAtom);
550
+ updatedNotebook.cellRuntime[cellId1] = {
551
+ ...updatedNotebook.cellRuntime[cellId1],
552
+ status: "idle",
553
+ };
554
+ store.set(notebookAtom, updatedNotebook);
555
+ });
556
+
557
+ // 2500 chars sits between MAX_TEXT_OUTPUT_CHARS (2000) and MAX_ERROR_OUTPUT_CHARS (3000)
558
+ const errorContent = "E".repeat(2500);
559
+ vi.mocked(getCellContextData).mockReturnValue({
560
+ cellOutput: {
561
+ outputType: "text",
562
+ processedContent: errorContent,
563
+ imageUrl: null,
564
+ output: {
565
+ mimetype: "application/vnd.marimo+error",
566
+ data: errorContent,
567
+ },
568
+ },
569
+ consoleOutputs: null,
570
+ cellName: "cell1",
571
+ } as never);
572
+
573
+ const result = await tool.handler({}, toolContext as never);
574
+
575
+ const output = result.cellsToOutput?.[cellId1]?.cellOutput ?? "";
576
+ expect(output).not.toContain("[TRUNCATED:");
577
+ expect(output).toContain(errorContent);
578
+ });
579
+
580
+ it("should truncate large console output", async () => {
581
+ const notebook = MockNotebook.notebookState({
582
+ cellData: {
583
+ [cellId1]: { code: 'print("x" * 10000)', edited: true },
584
+ },
585
+ });
586
+ store.set(notebookAtom, notebook);
587
+
588
+ vi.mocked(runCells).mockImplementation(async () => {
589
+ const updatedNotebook = store.get(notebookAtom);
590
+ updatedNotebook.cellRuntime[cellId1] = {
591
+ ...updatedNotebook.cellRuntime[cellId1],
592
+ status: "idle",
593
+ };
594
+ store.set(notebookAtom, updatedNotebook);
595
+ });
596
+
597
+ const largeConsoleText = "x".repeat(10_000);
598
+ vi.mocked(getCellContextData).mockReturnValue({
599
+ cellOutput: null,
600
+ consoleOutputs: [
601
+ {
602
+ outputType: "text",
603
+ processedContent: largeConsoleText,
604
+ imageUrl: null,
605
+ output: { mimetype: "text/plain", data: largeConsoleText },
606
+ },
607
+ ],
608
+ cellName: "cell1",
609
+ } as never);
610
+
611
+ const result = await tool.handler({}, toolContext as never);
612
+
613
+ const consoleOutput =
614
+ result.cellsToOutput?.[cellId1]?.consoleOutput ?? "";
615
+ expect(consoleOutput).toContain("[TRUNCATED:");
616
+ expect(consoleOutput.length).toBeLessThan(2200);
617
+ });
618
+ });
619
+
414
620
  describe("cell execution completion", () => {
415
621
  it("should complete immediately if cells are already idle", async () => {
416
622
  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;
@@ -92,7 +97,7 @@ export class RunStaleCellsTool
92
97
 
93
98
  await runCells({
94
99
  cellIds: staleCells,
95
- sendRun: sendRun,
100
+ sendRun,
96
101
  prepareForRun,
97
102
  notebook,
98
103
  });
@@ -116,44 +121,59 @@ export class RunStaleCellsTool
116
121
  const updatedNotebook = store.get(notebookAtom);
117
122
 
118
123
  const cellsToOutput = new Map<CellId, CellOutput | null>();
119
- let resultMessage = "";
120
124
  let outputHasErrors = false;
125
+ let hasAnyConsoleOutput = 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 (
139
+ (cellOutput && this.outputHasErrors(cellOutput)) ||
140
+ (hasConsoleOutput &&
141
+ consoleOutputs.some((output) => this.outputHasErrors(output)))
142
+ ) {
143
+ outputHasErrors = true;
144
+ }
145
+
134
146
  if (!cellOutput && !hasConsoleOutput) {
135
- // Set null to show no output
136
147
  cellsToOutput.set(cellId, null);
137
148
  continue;
138
149
  }
139
150
 
151
+ // If total budget exceeded, summarize remaining cells
152
+ if (totalOutputChars >= MAX_TOOL_OUTPUT_CHARS) {
153
+ cellsToOutput.set(cellId, {
154
+ cellOutput: "Cell executed (output omitted due to context limits).",
155
+ });
156
+ continue;
157
+ }
158
+
159
+ let cellOutputString: string | undefined;
160
+ let consoleOutputString: string | undefined;
161
+
140
162
  if (cellOutput) {
141
163
  cellOutputString = this.formatOutputString(cellOutput);
142
- if (this.outputHasErrors(cellOutput)) {
143
- outputHasErrors = true;
144
- }
164
+ totalOutputChars += cellOutputString.length;
145
165
  }
146
166
 
147
167
  if (hasConsoleOutput) {
168
+ hasAnyConsoleOutput = true;
148
169
  consoleOutputString = consoleOutputs
149
170
  .map((output) => this.formatOutputString(output))
150
171
  .join("\n");
151
- resultMessage +=
152
- "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
- }
172
+ consoleOutputString = this.truncateString(
173
+ consoleOutputString,
174
+ MAX_TEXT_OUTPUT_CHARS,
175
+ );
176
+ totalOutputChars += consoleOutputString.length;
157
177
  }
158
178
 
159
179
  cellsToOutput.set(cellId, {
@@ -179,43 +199,58 @@ export class RunStaleCellsTool
179
199
  return {
180
200
  status: "success",
181
201
  cellsToOutput: Object.fromEntries(cellsToOutput),
182
- message: resultMessage === "" ? undefined : resultMessage,
202
+ message: hasAnyConsoleOutput
203
+ ? "Console output represents the stdout or stderr of the cell (eg. print statements)."
204
+ : undefined,
183
205
  next_steps: nextSteps,
184
206
  };
185
207
  };
186
208
 
187
209
  private outputHasErrors(cellOutput: BaseOutput): boolean {
188
- const { output } = cellOutput;
189
- if (
190
- output.mimetype === "application/vnd.marimo+error" ||
191
- output.mimetype === "application/vnd.marimo+traceback"
192
- ) {
193
- return true;
194
- }
195
- return false;
210
+ return (
211
+ cellOutput.output.mimetype === "application/vnd.marimo+error" ||
212
+ cellOutput.output.mimetype === "application/vnd.marimo+traceback"
213
+ );
196
214
  }
197
215
 
198
216
  private formatOutputString(cellOutput: BaseOutput): string {
199
- let outputString = "";
200
217
  const { outputType, processedContent, imageUrl, output } = cellOutput;
201
- if (outputType === "text") {
202
- outputString += "Output:\n";
203
- if (processedContent) {
204
- outputString += processedContent;
205
- } else if (typeof output.data === "string") {
206
- outputString += output.data;
207
- } else {
208
- outputString += JSON.stringify(output.data);
209
- }
210
- } else if (outputType === "media") {
211
- outputString += `Media Output: Contains ${output.mimetype} content`;
212
- if (imageUrl) {
213
- outputString += `\nImage URL: ${imageUrl}`;
214
- }
218
+
219
+ if (outputType === "media") {
220
+ const base = `Media Output: Contains ${output.mimetype} content`;
221
+ return imageUrl ? `${base}\nImage URL: ${imageUrl}` : base;
215
222
  }
216
- return outputString;
223
+
224
+ if (output.mimetype === "text/html") {
225
+ // text/html (e.g. plotly figures, rich dataframes) can be millions of
226
+ // chars and is not interpretable by LLMs — summarize instead
227
+ const dataLength =
228
+ typeof output.data === "string"
229
+ ? output.data.length
230
+ : JSON.stringify(output.data).length;
231
+ return `HTML Output: text/html content (${dataLength.toLocaleString()} chars). Full output visible in notebook UI.`;
232
+ }
233
+
234
+ const maxChars = this.outputHasErrors(cellOutput)
235
+ ? MAX_ERROR_OUTPUT_CHARS
236
+ : MAX_TEXT_OUTPUT_CHARS;
237
+
238
+ let content = processedContent;
239
+ if (!content) {
240
+ content =
241
+ typeof output.data === "string"
242
+ ? output.data
243
+ : JSON.stringify(output.data);
244
+ }
245
+ return `Output:\n${this.truncateString(content, maxChars)}`;
217
246
  }
218
247
 
248
+ private truncateString(str: string, maxLength: number): string {
249
+ if (str.length <= maxLength) {
250
+ return str;
251
+ }
252
+ return `${str.slice(0, maxLength)}\n\n[TRUNCATED: ${str.length.toLocaleString()} → ${maxLength.toLocaleString()} chars. Full output visible in the notebook UI.]`;
253
+ }
219
254
  /**
220
255
  * Wait for cells to finish executing (status becomes "idle")
221
256
  * Returns true if all cells finished executing, false if the timeout was reached
@@ -1,6 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
- import { type Hotkey, type HotkeyAction, HotkeyProvider } from "../hotkeys";
3
+ import {
4
+ type Hotkey,
5
+ type HotkeyAction,
6
+ HotkeyProvider,
7
+ normalizeKeyString,
8
+ OverridingHotkeyProvider,
9
+ } from "../hotkeys";
4
10
 
5
11
  /**
6
12
  * Just a helper.
@@ -72,3 +78,60 @@ describe("HotkeyProvider platform separation", () => {
72
78
  expect(linux.getHotkey("cell.format").key).toBe("Ctrl-Shift-L");
73
79
  });
74
80
  });
81
+
82
+ describe("normalizeKeyString", () => {
83
+ it("should capitalize multi-character base key names", () => {
84
+ expect(normalizeKeyString("Shift-enter")).toBe("Shift-Enter");
85
+ expect(normalizeKeyString("Cmd-enter")).toBe("Cmd-Enter");
86
+ expect(normalizeKeyString("Ctrl-backspace")).toBe("Ctrl-Backspace");
87
+ expect(normalizeKeyString("Alt-tab")).toBe("Alt-Tab");
88
+ expect(normalizeKeyString("Cmd-Shift-arrowUp")).toBe("Cmd-Shift-ArrowUp");
89
+ });
90
+
91
+ it("should leave already-correct key names unchanged", () => {
92
+ expect(normalizeKeyString("Shift-Enter")).toBe("Shift-Enter");
93
+ expect(normalizeKeyString("Cmd-Enter")).toBe("Cmd-Enter");
94
+ expect(normalizeKeyString("Mod-Shift-Enter")).toBe("Mod-Shift-Enter");
95
+ });
96
+
97
+ it("should leave single-character keys unchanged", () => {
98
+ expect(normalizeKeyString("Cmd-a")).toBe("Cmd-a");
99
+ expect(normalizeKeyString("Ctrl-Shift-z")).toBe("Ctrl-Shift-z");
100
+ expect(normalizeKeyString("a")).toBe("a");
101
+ });
102
+
103
+ it("should handle keys without modifiers", () => {
104
+ expect(normalizeKeyString("enter")).toBe("Enter");
105
+ expect(normalizeKeyString("Escape")).toBe("Escape");
106
+ expect(normalizeKeyString("F12")).toBe("F12");
107
+ });
108
+ });
109
+
110
+ describe("OverridingHotkeyProvider", () => {
111
+ it("should normalize lowercase key overrides", () => {
112
+ const provider = new OverridingHotkeyProvider(
113
+ {
114
+ "cell.run": "Shift-enter",
115
+ "cell.runAndNewBelow": "Cmd-enter",
116
+ },
117
+ { platform: "mac" },
118
+ );
119
+
120
+ expect(provider.getHotkey("cell.run").key).toBe("Shift-Enter");
121
+ expect(provider.getHotkey("cell.runAndNewBelow").key).toBe("Cmd-Enter");
122
+ });
123
+
124
+ it("should return defaults when no override is set", () => {
125
+ const provider = new OverridingHotkeyProvider({}, { platform: "mac" });
126
+ expect(provider.getHotkey("cell.run").key).toBe("Cmd-Enter");
127
+ expect(provider.getHotkey("cell.runAndNewBelow").key).toBe("Shift-Enter");
128
+ });
129
+
130
+ it("should pass through correctly-cased overrides unchanged", () => {
131
+ const provider = new OverridingHotkeyProvider(
132
+ { "cell.run": "Shift-Enter" },
133
+ { platform: "mac" },
134
+ );
135
+ expect(provider.getHotkey("cell.run").key).toBe("Shift-Enter");
136
+ });
137
+ });
@@ -27,11 +27,13 @@ export interface Hotkey {
27
27
  * @default true
28
28
  */
29
29
  editable?: boolean;
30
+ additionalKeywords?: string[];
30
31
  }
31
32
 
32
33
  interface ResolvedHotkey {
33
34
  name: string;
34
35
  key: string;
36
+ additionalKeywords?: string[];
35
37
  }
36
38
 
37
39
  type ModKey = "Cmd" | "Ctrl";
@@ -110,6 +112,7 @@ const DEFAULT_HOT_KEY = {
110
112
  name: "Run",
111
113
  group: "Running Cells",
112
114
  key: "Mod-Enter",
115
+ additionalKeywords: ["execute", "submit"],
113
116
  },
114
117
  "cell.runAndNewBelow": {
115
118
  name: "Run and new below",
@@ -132,6 +135,7 @@ const DEFAULT_HOT_KEY = {
132
135
  name: "Format cell",
133
136
  group: "Editing",
134
137
  key: "Mod-b",
138
+ additionalKeywords: ["lint"],
135
139
  },
136
140
  "cell.viewAsMarkdown": {
137
141
  name: "View as Markdown",
@@ -201,6 +205,7 @@ const DEFAULT_HOT_KEY = {
201
205
  name: "Delete cell",
202
206
  group: "Editing",
203
207
  key: "Shift-Backspace",
208
+ additionalKeywords: ["remove"],
204
209
  },
205
210
  "cell.hideCode": {
206
211
  name: "Hide cell code",
@@ -320,6 +325,7 @@ const DEFAULT_HOT_KEY = {
320
325
  name: "Save file",
321
326
  group: "Other",
322
327
  key: "Mod-s",
328
+ additionalKeywords: ["write", "persist"],
323
329
  },
324
330
  "global.commandPalette": {
325
331
  name: "Show command palette",
@@ -485,23 +491,26 @@ export class HotkeyProvider implements IHotkeyProvider {
485
491
  }
486
492
 
487
493
  getHotkey(action: HotkeyAction): ResolvedHotkey {
488
- const { name, key } = this.hotkeys[action];
494
+ const { name, key, additionalKeywords } = this.hotkeys[action];
489
495
  if (typeof key === "string") {
490
496
  return {
491
497
  name,
492
498
  key: key.replace("Mod", this.mod),
499
+ additionalKeywords,
493
500
  };
494
501
  }
495
502
  if (key === NOT_SET) {
496
503
  return {
497
504
  name,
498
505
  key: "",
506
+ additionalKeywords,
499
507
  };
500
508
  }
501
509
  const platformKey = key[this.platform] || key.main;
502
510
  return {
503
511
  name,
504
512
  key: platformKey.replace("Mod", this.mod),
513
+ additionalKeywords,
505
514
  };
506
515
  }
507
516
 
@@ -535,10 +544,27 @@ export class OverridingHotkeyProvider extends HotkeyProvider {
535
544
 
536
545
  override getHotkey(action: HotkeyAction): ResolvedHotkey {
537
546
  const base = super.getHotkey(action);
538
- const key = this.overrides[action] || base.key;
547
+ const override = this.overrides[action];
539
548
  return {
540
549
  name: base.name,
541
- key,
550
+ key: override ? normalizeKeyString(override) : base.key,
551
+ additionalKeywords: base.additionalKeywords,
542
552
  };
543
553
  }
544
554
  }
555
+
556
+ const MODIFIER_RE = /^(cmd|ctrl|alt|shift|meta|mod)$/i;
557
+
558
+ /**
559
+ * Capitalize multi-character base key names so they match the
560
+ * casing that KeyboardEvent.key (and therefore CodeMirror) uses.
561
+ * e.g. "Shift-enter" → "Shift-Enter", "Cmd-backspace" → "Cmd-Backspace"
562
+ */
563
+ export function normalizeKeyString(key: string): string {
564
+ const parts = key.split("-");
565
+ const last = parts[parts.length - 1];
566
+ if (last.length > 1 && !MODIFIER_RE.test(last)) {
567
+ parts[parts.length - 1] = last.charAt(0).toUpperCase() + last.slice(1);
568
+ }
569
+ return parts.join("-");
570
+ }
@@ -181,6 +181,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
181
181
  sendFileDetails = throwNotImplemented;
182
182
  openTutorial = throwNotImplemented;
183
183
  exportAsHTML = throwNotImplemented;
184
+ exportAsIPYNB = throwNotImplemented;
184
185
  exportAsMarkdown = throwNotImplemented;
185
186
  exportAsPDF = throwNotImplemented;
186
187
  autoExportAsHTML = throwNotImplemented;
@@ -113,5 +113,22 @@ describe("createNetworkRequests", () => {
113
113
  }),
114
114
  );
115
115
  });
116
+
117
+ it("exportAsIPYNB should call the new endpoint as text", async () => {
118
+ const requests = createNetworkRequests();
119
+ await requests.exportAsIPYNB({
120
+ download: false,
121
+ } as any);
122
+
123
+ expect(mockClient.POST).toHaveBeenCalledWith(
124
+ "/api/export/ipynb",
125
+ expect.objectContaining({
126
+ body: expect.objectContaining({
127
+ download: false,
128
+ }),
129
+ parseAs: "text",
130
+ }),
131
+ );
132
+ });
116
133
  });
117
134
  });
@@ -49,6 +49,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
49
49
 
50
50
  // Export operations start a connection
51
51
  exportAsHTML: "startConnection",
52
+ exportAsIPYNB: "startConnection",
52
53
  exportAsMarkdown: "startConnection",
53
54
  exportAsPDF: "startConnection",
54
55
  readCode: "startConnection",
@@ -381,6 +381,15 @@ export function createNetworkRequests(): EditRequests & RunRequests {
381
381
  })
382
382
  .then(handleResponse);
383
383
  },
384
+ exportAsIPYNB: async (request) => {
385
+ return getClient()
386
+ .POST("/api/export/ipynb", {
387
+ body: request,
388
+ parseAs: "text",
389
+ params: getParams(),
390
+ })
391
+ .then(handleResponse);
392
+ },
384
393
  exportAsPDF: async (request) => {
385
394
  return getClient()
386
395
  .POST("/api/export/pdf", {
@@ -75,6 +75,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
75
75
  getRunningNotebooks: throwNotInEditMode,
76
76
  shutdownSession: throwNotInEditMode,
77
77
  exportAsHTML: throwNotInEditMode,
78
+ exportAsIPYNB: throwNotInEditMode,
78
79
  exportAsMarkdown: throwNotInEditMode,
79
80
  exportAsPDF: throwNotInEditMode,
80
81
  autoExportAsHTML: throwNotInEditMode,
@@ -60,6 +60,7 @@ export function createErrorToastingRequests(
60
60
  getRunningNotebooks: "Failed to get running notebooks",
61
61
  shutdownSession: "Failed to shutdown session",
62
62
  exportAsHTML: "Failed to export HTML",
63
+ exportAsIPYNB: "Failed to export ipynb",
63
64
  exportAsMarkdown: "Failed to export Markdown",
64
65
  exportAsPDF: "Failed to export PDF",
65
66
  autoExportAsHTML: "", // No toast
@@ -180,6 +180,7 @@ export interface EditRequests {
180
180
  ) => Promise<RunningNotebooksResponse>;
181
181
  // Export requests
182
182
  exportAsHTML: (request: ExportAsHTMLRequest) => Promise<string>;
183
+ exportAsIPYNB: (request: ExportAsIPYNBRequest) => Promise<string>;
183
184
  exportAsMarkdown: (request: ExportAsMarkdownRequest) => Promise<string>;
184
185
  exportAsPDF: (request: ExportAsPDFRequest) => Promise<Blob>;
185
186
  autoExportAsHTML: (request: ExportAsHTMLRequest) => Promise<null>;
@@ -10,6 +10,7 @@ function makeNamespace(
10
10
  overrides: Partial<StorageNamespace> & { name: string },
11
11
  ): StorageNamespace {
12
12
  return {
13
+ backendType: overrides.backendType ?? "obstore",
13
14
  displayName: overrides.displayName ?? overrides.name,
14
15
  name: overrides.name,
15
16
  protocol: overrides.protocol ?? "s3",
@@ -587,6 +587,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
587
587
  getWorkspaceFiles = throwNotImplemented;
588
588
  getRunningNotebooks = throwNotImplemented;
589
589
  shutdownSession = throwNotImplemented;
590
+ exportAsIPYNB = throwNotImplemented;
590
591
  exportAsPDF = throwNotImplemented;
591
592
  autoExportAsHTML = throwNotImplemented;
592
593
  autoExportAsMarkdown = throwNotImplemented;
@@ -4,10 +4,10 @@ import { CornerLeftUp } from "lucide-react";
4
4
  import { type JSX, useEffect, useState } from "react";
5
5
  import { z } from "zod";
6
6
  import {
7
- FILE_TYPE_ICONS,
8
- type FileType,
9
- guessFileType,
10
- } from "@/components/editor/file-tree/types";
7
+ FILE_ICON as FILE_TYPE_ICONS,
8
+ type FileIconType as FileType,
9
+ guessFileIconType as guessFileType,
10
+ } from "@/components/editor/file-tree/file-icons";
11
11
  import { Spinner } from "@/components/icons/spinner";
12
12
  import { Button } from "@/components/ui/button";
13
13
  import { Checkbox } from "@/components/ui/checkbox";