@marimo-team/islands 0.19.8-dev5 → 0.19.8-dev50

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 (147) hide show
  1. package/dist/{Combination-Bg-xN8JV.js → Combination-BTMrlhzT.js} +11 -10
  2. package/dist/{ConnectedDataExplorerComponent-DewsKLl2.js → ConnectedDataExplorerComponent-BAeQ8DWw.js} +11 -11
  3. package/dist/{ImageComparisonComponent-Bijp8beW.js → ImageComparisonComponent-DkEXPki_.js} +2 -2
  4. package/dist/{any-language-editor-DZc6NCTp.js → any-language-editor-D0UQItkS.js} +6 -6
  5. package/dist/{architectureDiagram-VXUJARFQ--NkyBn9Y.js → architectureDiagram-VXUJARFQ-DPPYVq8H.js} +4 -4
  6. package/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
  7. package/dist/assets/worker-DUYMdbtA.js +73 -0
  8. package/dist/{blockDiagram-VD42YOAC-DEZZaTW0.js → blockDiagram-VD42YOAC-BA5N05Y9.js} +4 -4
  9. package/dist/{button-BWvsJ2Wr.js → button-Cy0ElmIm.js} +2 -2
  10. package/dist/{c4Diagram-YG6GDRKO-Bj7hwWCO.js → c4Diagram-YG6GDRKO-DJLzuGJJ.js} +3 -3
  11. package/dist/{channel-B_QrFrGg.js → channel-Dob5kWXR.js} +1 -1
  12. package/dist/{check-CM_kewwn.js → check-DkNR52Mm.js} +1 -1
  13. package/dist/{chunk-5FQGJX7Z-D5VFKHmt.js → chunk-5FQGJX7Z-BEb20Lzt.js} +3 -3
  14. package/dist/{chunk-ABZYJK2D-SZPYmRzN.js → chunk-ABZYJK2D-BXTC53mt.js} +1 -1
  15. package/dist/{chunk-ATLVNIR6-BI_WwH1o.js → chunk-ATLVNIR6-BJDjUR_c.js} +1 -1
  16. package/dist/{chunk-B4BG7PRW-BlI9Gm1l.js → chunk-B4BG7PRW-DzmUUpfH.js} +4 -4
  17. package/dist/{chunk-DI55MBZ5-BXxemMn5.js → chunk-DI55MBZ5-gTd3J8Tu.js} +4 -4
  18. package/dist/{chunk-EXTU4WIE-CzWtDV99.js → chunk-EXTU4WIE-DyoOs5QX.js} +1 -1
  19. package/dist/{chunk-JA3XYJ7Z-DQ-2ARfa.js → chunk-JA3XYJ7Z-BGnAIbOP.js} +2 -2
  20. package/dist/{chunk-JZLCHNYA-CVfjf2vv.js → chunk-JZLCHNYA-CIRgweVQ.js} +4 -4
  21. package/dist/{chunk-N4CR4FBY-BCZvQ7Jq.js → chunk-N4CR4FBY-DKSvXAIS.js} +5 -5
  22. package/dist/{chunk-QN33PNHL-DY_2Q2zl.js → chunk-QN33PNHL-B6zC8BTi.js} +1 -1
  23. package/dist/{chunk-QXUST7PY-BMCjAVR_.js → chunk-QXUST7PY-C7750n_u.js} +5 -5
  24. package/dist/{chunk-S3R3BYOJ-Ddu0H4Qa.js → chunk-S3R3BYOJ-CBkH6JZZ.js} +1 -1
  25. package/dist/{chunk-TZMSLE5B-C2wVlbMl.js → chunk-TZMSLE5B-DObGL7xi.js} +1 -1
  26. package/dist/{classDiagram-2ON5EDUG-D-g7zbyO.js → classDiagram-2ON5EDUG-B9pkKjjc.js} +9 -9
  27. package/dist/{classDiagram-v2-WZHVMYZB-C7v5zNRD.js → classDiagram-v2-WZHVMYZB-CRhhA0tV.js} +9 -9
  28. package/dist/{click-outside-container-BCN5BtVO.js → click-outside-container-DNfggvIW.js} +1 -1
  29. package/dist/{code-block-37QAKDTI-eUgXqGNG.js → code-block-37QAKDTI-u5kgjqmr.js} +2 -2
  30. package/dist/{compiler-runtime-DHFVbq0b.js → compiler-runtime-B_OLMU9S.js} +1 -1
  31. package/dist/{copy-B59Bw3-w.js → copy-DRaXIb_a.js} +3 -3
  32. package/dist/{dagre-6UL2VRFP-DKIPL74O.js → dagre-6UL2VRFP-C2C2XxsB.js} +6 -6
  33. package/dist/{data-grid-overlay-editor-COyFwFmE.js → data-grid-overlay-editor-BXqtz1ia.js} +4 -4
  34. package/dist/{diagram-PSM6KHXK-CVTrAZaP.js → diagram-PSM6KHXK-DHBY-94p.js} +5 -5
  35. package/dist/{diagram-QEK2KX5R-BqHBzu3x.js → diagram-QEK2KX5R-CgMshOwn.js} +3 -3
  36. package/dist/{diagram-S2PKOQOG-CJD6owcg.js → diagram-S2PKOQOG-F1KPva3Y.js} +3 -3
  37. package/dist/{dist-Co5PD8Fb.js → dist-BBYTEAvO.js} +1 -1
  38. package/dist/{erDiagram-Q2GNP2WA-CqOceSf9.js → erDiagram-Q2GNP2WA-18gGng8V.js} +9 -9
  39. package/dist/{error-banner-C7KLpECd.js → error-banner-D2zjeN_a.js} +5 -5
  40. package/dist/{esm-D4WO8J3G.js → esm-CgRNPmz8.js} +6 -6
  41. package/dist/{flowDiagram-NV44I4VS-K7-DUifo.js → flowDiagram-NV44I4VS-iHFiHYe0.js} +9 -9
  42. package/dist/{ganttDiagram-JELNMOA3-BwUFY9Nu.js → ganttDiagram-JELNMOA3-D7GixxiF.js} +2 -2
  43. package/dist/{gitGraphDiagram-NY62KEGX-CjGRtLb1.js → gitGraphDiagram-NY62KEGX-CJFHytRK.js} +2 -2
  44. package/dist/{glide-data-editor-C3T7HsLi.js → glide-data-editor-BYwb17Bf.js} +13 -13
  45. package/dist/{infoDiagram-WHAUD3N6-DNhmDn-6.js → infoDiagram-WHAUD3N6-B5Lkh3A9.js} +2 -2
  46. package/dist/{journeyDiagram-XKPGCS4Q-BOdK47P8.js → journeyDiagram-XKPGCS4Q-CV_9R9iP.js} +2 -2
  47. package/dist/{kanban-definition-3W4ZIXB7-A0JC9d0g.js → kanban-definition-3W4ZIXB7-Dp21D5Ym.js} +6 -6
  48. package/dist/{katex-DJyOeQ91.js → katex-CX2BKujk.js} +1 -1
  49. package/dist/{katex-Dm9nZf6A.js → katex-Db0k5oV_.js} +1 -1
  50. package/dist/{label-C4PtQcza.js → label-CxU5JNBW.js} +6 -6
  51. package/dist/main.js +2000 -1887
  52. package/dist/mermaid-4DMBBIKO-BhDCqnO1.js +6 -0
  53. package/dist/{mermaid-Bqp2Xw99.js → mermaid-B__BZSXU.js} +39 -39
  54. package/dist/{mhchem-BqdXeZVX.js → mhchem-w1tkUnWr.js} +1 -1
  55. package/dist/{mindmap-definition-VGOIOE7T-CS6nKN_L.js → mindmap-definition-VGOIOE7T-B_5mfdYp.js} +8 -8
  56. package/dist/{number-overlay-editor-Bz_bDJQb.js → number-overlay-editor-D-4WQAGX.js} +2 -2
  57. package/dist/{pieDiagram-ADFJNKIX-DSa60Grk.js → pieDiagram-ADFJNKIX-B-DGEopK.js} +3 -3
  58. package/dist/{quadrantDiagram-AYHSOK5B-CFnMbP2J.js → quadrantDiagram-AYHSOK5B-M_yRSIZn.js} +1 -1
  59. package/dist/{react-DdA8EBol.js → react-Bs6Z0kvn.js} +1 -1
  60. package/dist/{react-dom-DJW8xUDg.js → react-dom-CqtLRVZP.js} +2 -2
  61. package/dist/{react-plotly-jVjTu07w.js → react-plotly-BuRa9xtI.js} +1 -1
  62. package/dist/{react-vega-DgHpnZ04.js → react-vega-3WcLHYC7.js} +2 -2
  63. package/dist/{react-vega-CjiPWyw0.js → react-vega-DLFvGrpJ.js} +1 -1
  64. package/dist/{requirementDiagram-UZGBJVZJ-ytLQrFTk.js → requirementDiagram-UZGBJVZJ-9Wt82hOZ.js} +8 -8
  65. package/dist/{sankeyDiagram-TZEHDZUN-KQqXDoky.js → sankeyDiagram-TZEHDZUN-x_aTXZeN.js} +1 -1
  66. package/dist/{sequenceDiagram-WL72ISMW-ByLI04T5.js → sequenceDiagram-WL72ISMW-CXXmJqiQ.js} +3 -3
  67. package/dist/{slides-component-BVjvNo92.js → slides-component-Dp-y50K9.js} +4 -4
  68. package/dist/{spec-Dmb1KfK3.js → spec-HoYHAQo2.js} +6 -6
  69. package/dist/{stateDiagram-FKZM4ZOC-Dfz8vBbP.js → stateDiagram-FKZM4ZOC-CiSKS_Mx.js} +9 -9
  70. package/dist/{stateDiagram-v2-4FDKWEC3-DRYoLdT5.js → stateDiagram-v2-4FDKWEC3-A43Itnjp.js} +9 -9
  71. package/dist/style.css +1 -1
  72. package/dist/{timeline-definition-IT6M3QCI-CO48XU1B.js → timeline-definition-IT6M3QCI-DR26eWb4.js} +1 -1
  73. package/dist/{types-CzEZ3EWT.js → types-Bb-6p8hv.js} +8 -8
  74. package/dist/{useAsyncData-BjNwqCfS.js → useAsyncData-Dyq3DyOF.js} +3 -3
  75. package/dist/{useDeepCompareMemoize-CfoxVor3.js → useDeepCompareMemoize-BhZZsis0.js} +12 -8
  76. package/dist/{useIframeCapabilities-BBO_R0ww.js → useIframeCapabilities-DurI5SJh.js} +2 -2
  77. package/dist/{useTheme-BYG2SH8J.js → useTheme-SlKl8MlS.js} +5 -6
  78. package/dist/{vega-component-rDX7xwxH.js → vega-component-DCxUyPnb.js} +10 -10
  79. package/dist/{xychartDiagram-PRI3JC2R-CUIfjNVD.js → xychartDiagram-PRI3JC2R-BcVxCRox.js} +4 -4
  80. package/dist/{zod-DITCj31F.js → zod-bjADtMKr.js} +3 -3
  81. package/package.json +18 -18
  82. package/src/components/app-config/ai-config.tsx +11 -2
  83. package/src/components/app-config/optional-features.tsx +1 -1
  84. package/src/components/app-config/user-config-form.tsx +0 -54
  85. package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
  86. package/src/components/chat/acp/__tests__/state.test.ts +69 -0
  87. package/src/components/chat/acp/agent-panel.tsx +26 -77
  88. package/src/components/chat/acp/state.ts +6 -6
  89. package/src/components/chat/chat-components.tsx +114 -1
  90. package/src/components/chat/chat-panel.tsx +79 -134
  91. package/src/components/chat/chat-utils.ts +42 -0
  92. package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
  93. package/src/components/editor/actions/useCellActionButton.tsx +14 -1
  94. package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
  95. package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
  96. package/src/components/editor/cell/CreateCellButton.tsx +2 -1
  97. package/src/components/editor/cell/code/cell-editor.tsx +12 -0
  98. package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
  99. package/src/components/editor/database/__tests__/__snapshots__/as-code.test.ts.snap +15 -0
  100. package/src/components/editor/database/__tests__/as-code.test.ts +8 -0
  101. package/src/components/editor/database/as-code.ts +3 -0
  102. package/src/components/editor/database/schemas.ts +9 -0
  103. package/src/components/editor/renderers/cell-array.tsx +2 -1
  104. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -0
  105. package/src/components/pages/gallery-page.tsx +37 -6
  106. package/src/core/MarimoApp.tsx +12 -8
  107. package/src/core/ai/context/providers/file.ts +1 -1
  108. package/src/core/cells/__tests__/cells.test.ts +120 -0
  109. package/src/core/cells/__tests__/session.test.ts +37 -1
  110. package/src/core/cells/cells.ts +14 -0
  111. package/src/core/cells/session.ts +20 -8
  112. package/src/core/codemirror/language/languages/markdown.ts +7 -0
  113. package/src/core/config/feature-flag.tsx +0 -4
  114. package/src/core/dom/uiregistry.ts +4 -1
  115. package/src/core/islands/__tests__/bridge.test.ts +7 -2
  116. package/src/core/islands/bridge.ts +1 -1
  117. package/src/core/islands/main.ts +7 -0
  118. package/src/core/network/types.ts +2 -2
  119. package/src/core/run-app.tsx +11 -4
  120. package/src/core/static/__tests__/files.test.ts +195 -1
  121. package/src/core/static/files.ts +39 -9
  122. package/src/core/wasm/bridge.ts +1 -1
  123. package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
  124. package/src/plugins/core/registerReactComponent.tsx +9 -1
  125. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
  126. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +93 -168
  127. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
  128. package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
  129. package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
  130. package/src/plugins/impl/anywidget/model.ts +348 -223
  131. package/src/plugins/impl/anywidget/schemas.ts +32 -0
  132. package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
  133. package/src/plugins/impl/anywidget/types.ts +27 -0
  134. package/src/plugins/impl/chat/chat-ui.tsx +22 -20
  135. package/src/utils/Deferred.ts +21 -0
  136. package/src/utils/__tests__/blob.test.ts +3 -3
  137. package/src/utils/__tests__/id-tree.test.ts +22 -7
  138. package/src/utils/__tests__/mime-types.test.ts +8 -10
  139. package/src/utils/__tests__/url-parser.test.ts +22 -0
  140. package/src/utils/blob.ts +14 -27
  141. package/src/utils/id-tree.tsx +11 -19
  142. package/src/utils/json/base64.ts +38 -8
  143. package/src/utils/mime-types.ts +5 -5
  144. package/src/utils/url-parser.ts +1 -1
  145. package/dist/assets/__vite-browser-external-DRa9CT_O.js +0 -1
  146. package/dist/assets/worker-SqntmiwV.js +0 -73
  147. package/dist/mermaid-4DMBBIKO-o3xNphpD.js +0 -6
@@ -2561,6 +2561,126 @@ describe("cell reducer", () => {
2561
2561
  expect(state.untouchedNewCells.has(newCellId)).toBe(false);
2562
2562
  expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(true);
2563
2563
  });
2564
+
2565
+ it("can mark an existing cell as untouched", () => {
2566
+ // Create a cell without hideCode (not in untouchedNewCells)
2567
+ actions.createNewCell({
2568
+ cellId: "__end__",
2569
+ before: false,
2570
+ hideCode: false,
2571
+ });
2572
+
2573
+ const newCellId =
2574
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2575
+ expect(state.untouchedNewCells.has(newCellId)).toBe(false);
2576
+
2577
+ // Mark it as untouched
2578
+ actions.markUntouched({ cellId: newCellId });
2579
+
2580
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2581
+ });
2582
+
2583
+ it("markUntouched is idempotent", () => {
2584
+ // Create a cell without hideCode
2585
+ actions.createNewCell({
2586
+ cellId: "__end__",
2587
+ before: false,
2588
+ hideCode: false,
2589
+ });
2590
+
2591
+ const newCellId =
2592
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2593
+ expect(state.untouchedNewCells.has(newCellId)).toBe(false);
2594
+
2595
+ // Mark as untouched multiple times
2596
+ actions.markUntouched({ cellId: newCellId });
2597
+ actions.markUntouched({ cellId: newCellId });
2598
+ actions.markUntouched({ cellId: newCellId });
2599
+
2600
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2601
+ });
2602
+
2603
+ it("markUntouched does not affect already untouched cells", () => {
2604
+ // Create a cell with hideCode (already in untouchedNewCells)
2605
+ actions.createNewCell({
2606
+ cellId: "__end__",
2607
+ before: false,
2608
+ hideCode: true,
2609
+ });
2610
+
2611
+ const newCellId =
2612
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2613
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2614
+
2615
+ // Calling markUntouched should not change anything
2616
+ actions.markUntouched({ cellId: newCellId });
2617
+
2618
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2619
+ });
2620
+
2621
+ it("markTouched and markUntouched can toggle cell state", () => {
2622
+ // Create a cell without hideCode
2623
+ actions.createNewCell({
2624
+ cellId: "__end__",
2625
+ before: false,
2626
+ hideCode: false,
2627
+ });
2628
+
2629
+ const newCellId =
2630
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2631
+
2632
+ // Initially not untouched
2633
+ expect(state.untouchedNewCells.has(newCellId)).toBe(false);
2634
+
2635
+ // Mark as untouched
2636
+ actions.markUntouched({ cellId: newCellId });
2637
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2638
+
2639
+ // Mark as touched
2640
+ actions.markTouched({ cellId: newCellId });
2641
+ expect(state.untouchedNewCells.has(newCellId)).toBe(false);
2642
+
2643
+ // Mark as untouched again
2644
+ actions.markUntouched({ cellId: newCellId });
2645
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2646
+ });
2647
+
2648
+ it("markUntouched works for markdown cell conversion scenario", () => {
2649
+ // Simulates converting a Python cell to Markdown
2650
+ // 1. Create a regular cell (no hideCode)
2651
+ actions.createNewCell({
2652
+ cellId: "__end__",
2653
+ before: false,
2654
+ hideCode: false,
2655
+ });
2656
+
2657
+ const cellId =
2658
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2659
+
2660
+ // Cell starts without hide_code and not in untouchedNewCells
2661
+ expect(state.cellData[cellId].config.hide_code).toBe(false);
2662
+ expect(state.untouchedNewCells.has(cellId)).toBe(false);
2663
+ expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
2664
+
2665
+ // 2. Convert to markdown: set hide_code and mark as untouched
2666
+ actions.updateCellConfig({
2667
+ cellId,
2668
+ config: { hide_code: true },
2669
+ });
2670
+ actions.markUntouched({ cellId });
2671
+
2672
+ // Code should NOT be hidden because cell is untouched (user can edit)
2673
+ expect(state.cellData[cellId].config.hide_code).toBe(true);
2674
+ expect(state.untouchedNewCells.has(cellId)).toBe(true);
2675
+ expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
2676
+
2677
+ // 3. User blurs the cell (markTouched)
2678
+ actions.markTouched({ cellId });
2679
+
2680
+ // Now code should be hidden
2681
+ expect(state.untouchedNewCells.has(cellId)).toBe(false);
2682
+ expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(true);
2683
+ });
2564
2684
  });
2565
2685
 
2566
2686
  describe("releaseCellAtoms", () => {
@@ -7,7 +7,7 @@ import { parseOutline } from "@/core/dom/outline";
7
7
  import { MultiColumn, visibleForTesting } from "@/utils/id-tree";
8
8
  import { invariant } from "@/utils/invariant";
9
9
  import { Logger } from "@/utils/Logger";
10
- import type { CellId } from "../ids";
10
+ import { type CellId, SETUP_CELL_ID } from "../ids";
11
11
  import { notebookStateFromSession } from "../session";
12
12
 
13
13
  // Mock dependencies
@@ -358,6 +358,42 @@ describe("notebookStateFromSession", () => {
358
358
  serializedEditorState: null,
359
359
  });
360
360
  });
361
+
362
+ it("uses SETUP_CELL_ID for setup cell with null id", () => {
363
+ const notebookCell = {
364
+ id: null,
365
+ code: "import marimo as mo",
366
+ name: "setup",
367
+ code_hash: null,
368
+ config: {
369
+ hide_code: null,
370
+ disabled: null,
371
+ column: null,
372
+ },
373
+ };
374
+ const notebook = createNotebook([notebookCell as any]);
375
+ const result = notebookStateFromSession(null, notebook);
376
+
377
+ expect(result).not.toBeNull();
378
+ invariant(result, "result is null");
379
+ expect(result.cellIds.inOrderIds).toEqual(
380
+ MultiColumn.from([[SETUP_CELL_ID]]).inOrderIds,
381
+ );
382
+ expect(result.cellData[SETUP_CELL_ID]).toEqual({
383
+ id: SETUP_CELL_ID,
384
+ name: "setup",
385
+ code: "import marimo as mo",
386
+ edited: false,
387
+ lastCodeRun: null,
388
+ lastExecutionTime: null,
389
+ config: {
390
+ hide_code: false,
391
+ disabled: false,
392
+ column: null,
393
+ },
394
+ serializedEditorState: null,
395
+ });
396
+ });
361
397
  });
362
398
 
363
399
  describe("both session and notebook scenarios", () => {
@@ -1033,6 +1033,20 @@ const {
1033
1033
 
1034
1034
  return state;
1035
1035
  },
1036
+ markUntouched: (state, action: { cellId: CellId }) => {
1037
+ const { cellId } = action;
1038
+
1039
+ if (!state.untouchedNewCells.has(cellId)) {
1040
+ const nextUntouchedNewCells = new Set(state.untouchedNewCells);
1041
+ nextUntouchedNewCells.add(cellId);
1042
+ return {
1043
+ ...state,
1044
+ untouchedNewCells: nextUntouchedNewCells,
1045
+ };
1046
+ }
1047
+
1048
+ return state;
1049
+ },
1036
1050
  scrollToTarget: (state) => {
1037
1051
  // Scroll to the specified cell and clear the scroll key.
1038
1052
  const scrollKey = state.scrollKey;
@@ -6,7 +6,7 @@ import { MultiColumn } from "@/utils/id-tree";
6
6
  import { Logger } from "@/utils/Logger";
7
7
  import { parseOutline } from "../dom/outline";
8
8
  import type { NotebookState } from "./cells";
9
- import { CellId } from "./ids";
9
+ import { CellId, SETUP_CELL_ID } from "./ids";
10
10
  import {
11
11
  type CellData,
12
12
  type CellRuntimeState,
@@ -20,6 +20,22 @@ const EMPTY_STRING = "";
20
20
  type SessionCell = api.Session["NotebookSessionV1"]["cells"][0];
21
21
  type NotebookCell = api.Notebook["NotebookV1"]["cells"][0];
22
22
 
23
+ /**
24
+ * Get the cell ID from a cell object.
25
+ * If the cell has an ID, use it.
26
+ * If the cell has a name of "setup", use the special SETUP_CELL_ID.
27
+ * Otherwise, generate a new random ID.
28
+ */
29
+ function getCellId(cell: { id?: string | null; name?: string | null }): CellId {
30
+ if (cell.id) {
31
+ return cell.id as CellId;
32
+ }
33
+ if (cell.name === SETUP_CELL_ID) {
34
+ return SETUP_CELL_ID;
35
+ }
36
+ return CellId.create();
37
+ }
38
+
23
39
  function mergeSessionAndNotebookCells(
24
40
  session: api.Session["NotebookSessionV1"] | null | undefined,
25
41
  notebook: api.Notebook["NotebookV1"] | null | undefined,
@@ -36,9 +52,7 @@ function mergeSessionAndNotebookCells(
36
52
  }
37
53
 
38
54
  if (!session) {
39
- const cellIds = (notebook?.cells.map(
40
- (cell) => cell.id ?? CellId.create(),
41
- ) || []) as CellId[];
55
+ const cellIds = notebook?.cells.map((cell) => getCellId(cell)) || [];
42
56
  return {
43
57
  cellIds,
44
58
  sessionCellData: new Map(
@@ -57,9 +71,7 @@ function mergeSessionAndNotebookCells(
57
71
  }
58
72
 
59
73
  if (!notebook) {
60
- const cellIds = session.cells.map(
61
- (cell) => cell.id ?? CellId.create(),
62
- ) as CellId[];
74
+ const cellIds = session.cells.map((cell) => getCellId(cell));
63
75
  return {
64
76
  cellIds,
65
77
  sessionCellData: new Map(
@@ -105,7 +117,7 @@ function mergeSessionAndNotebookCells(
105
117
  for (let i = 0; i < notebook.cells.length; i++) {
106
118
  const notebookCell = notebook.cells[i];
107
119
  if (notebookCell) {
108
- const id = (notebookCell.id ?? CellId.create()) as CellId;
120
+ const id = getCellId(notebookCell);
109
121
  mergedCellIdsTyped.push(id);
110
122
 
111
123
  // Should always be set, but good typing fallback too.
@@ -28,6 +28,13 @@ import type { LanguageAdapter } from "../types";
28
28
 
29
29
  export type MarkdownLanguageAdapterMetadata = MarkdownMetadata;
30
30
 
31
+ /**
32
+ * Default hide_code setting for markdown cells.
33
+ * When true, the markdown code is hidden after the cell is blurred,
34
+ * showing only the rendered output.
35
+ */
36
+ export const MARKDOWN_INITIAL_HIDE_CODE = true;
37
+
31
38
  /**
32
39
  * Language adapter for Markdown.
33
40
  */
@@ -9,8 +9,6 @@ export interface ExperimentalFeatures {
9
9
  markdown: boolean; // Used in playground (community cloud)
10
10
  wasm_layouts: boolean; // Used in playground (community cloud)
11
11
  rtc_v2: boolean;
12
- performant_table_charts: boolean;
13
- chat_modes: boolean;
14
12
  cache_panel: boolean;
15
13
  external_agents: boolean;
16
14
  server_side_pdf_export: boolean;
@@ -21,8 +19,6 @@ const defaultValues: ExperimentalFeatures = {
21
19
  markdown: true,
22
20
  wasm_layouts: false,
23
21
  rtc_v2: false,
24
- performant_table_charts: false,
25
- chat_modes: false,
26
22
  cache_panel: false,
27
23
  external_agents: import.meta.env.DEV,
28
24
  server_side_pdf_export: true,
@@ -51,7 +51,10 @@ export class UIElementRegistry {
51
51
 
52
52
  set(objectId: UIElementId, value: ValueType): void {
53
53
  if (this.entries.has(objectId)) {
54
- throw new Error(`UIElement ${objectId} already registered`);
54
+ Logger.debug(
55
+ "UIElementRegistry overwriting entry for objectId.",
56
+ objectId,
57
+ );
55
58
  }
56
59
  this.entries.set(objectId, {
57
60
  objectId: objectId,
@@ -192,9 +192,11 @@ describe("IslandsPyodideBridge", () => {
192
192
  const request = {
193
193
  modelId: "widget-1",
194
194
  message: {
195
+ method: "update" as const,
195
196
  state: { value: 42 },
196
197
  bufferPaths: [],
197
198
  },
199
+ buffers: [],
198
200
  };
199
201
 
200
202
  await bridge.sendModelValue(request);
@@ -202,12 +204,14 @@ describe("IslandsPyodideBridge", () => {
202
204
  expect(mockBridge).toHaveBeenCalledWith({
203
205
  functionName: "put_control_request",
204
206
  payload: {
205
- type: "update-widget-model",
207
+ type: "model",
206
208
  modelId: "widget-1",
207
209
  message: {
210
+ method: "update",
208
211
  state: { value: 42 },
209
212
  bufferPaths: [],
210
213
  },
214
+ buffers: [],
211
215
  },
212
216
  });
213
217
  });
@@ -226,7 +230,8 @@ describe("IslandsPyodideBridge", () => {
226
230
  await bridge.sendRun({ cellIds: [], codes: [] });
227
231
  await bridge.sendModelValue({
228
232
  modelId: "",
229
- message: { state: {}, bufferPaths: [] },
233
+ message: { method: "update", state: {}, bufferPaths: [] },
234
+ buffers: [],
230
235
  });
231
236
 
232
237
  // All calls should have the type field
@@ -140,7 +140,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
140
140
 
141
141
  sendModelValue: RunRequests["sendModelValue"] = async (request) => {
142
142
  await this.putControlRequest({
143
- type: "update-widget-model",
143
+ type: "model",
144
144
  ...request,
145
145
  });
146
146
  return null;
@@ -13,6 +13,10 @@ import "iconify-icon";
13
13
 
14
14
  import { toast } from "@/components/ui/use-toast";
15
15
  import { renderHTML } from "@/plugins/core/RenderHTML";
16
+ import {
17
+ handleWidgetMessage,
18
+ MODEL_MANAGER,
19
+ } from "@/plugins/impl/anywidget/model";
16
20
  import { initializePlugins } from "@/plugins/plugins";
17
21
  import { logNever } from "@/utils/assertNever";
18
22
  import { Functions } from "@/utils/functions";
@@ -194,6 +198,9 @@ export async function initialize() {
194
198
  return;
195
199
  case "kernel-startup-error":
196
200
  return;
201
+ case "model-lifecycle":
202
+ handleWidgetMessage(MODEL_MANAGER, msg.data);
203
+ return;
197
204
  default:
198
205
  logNever(msg.data);
199
206
  }
@@ -78,7 +78,7 @@ export interface SetCellConfigRequest {
78
78
  configs: Record<CellId, Partial<CellConfig>>;
79
79
  }
80
80
  export type UpdateUIElementRequest = schemas["UpdateUIElementRequest"];
81
- export type UpdateWidgetModelRequest = schemas["UpdateWidgetModelRequest"];
81
+ export type ModelRequest = schemas["ModelRequest"];
82
82
  export type UpdateCellIdsRequest = schemas["UpdateCellIdsRequest"];
83
83
  export type UpdateUserConfigRequest = schemas["UpdateUserConfigRequest"];
84
84
  export type ShutdownSessionRequest = schemas["ShutdownSessionRequest"];
@@ -110,7 +110,7 @@ export type LspServerHealth = schemas["LspServerHealth"];
110
110
  */
111
111
  export interface RunRequests {
112
112
  sendComponentValues: (request: UpdateUIElementValuesRequest) => Promise<null>;
113
- sendModelValue: (request: UpdateWidgetModelRequest) => Promise<null>;
113
+ sendModelValue: (request: ModelRequest) => Promise<null>;
114
114
  sendInstantiate: (request: InstantiateNotebookRequest) => Promise<null>;
115
115
  sendFunctionRequest: (request: InvokeFunctionRequest) => Promise<null>;
116
116
  }
@@ -1,11 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { useAtomValue } from "jotai";
4
+ import { ArrowLeftIcon } from "lucide-react";
4
5
  import { useEffect } from "react";
5
6
  import { AppContainer } from "@/components/editor/app-container";
6
7
  import { AppHeader } from "@/components/editor/header/app-header";
7
8
  import { Spinner } from "@/components/icons/spinner";
9
+ import { buttonVariants } from "@/components/ui/button";
8
10
  import { DelayMount } from "@/components/utils/delay-mount";
11
+ import { cn } from "@/utils/cn";
9
12
  import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
10
13
  import { notebookIsRunningAtom, useCellActions } from "./cells/cells";
11
14
  import type { AppConfig } from "./config/config-schema";
@@ -75,15 +78,19 @@ export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
75
78
  isRunning={isRunning}
76
79
  width={appConfig.width}
77
80
  >
78
- <AppHeader connection={connection} className={"sm:pt-8"}>
81
+ <AppHeader connection={connection} className="sm:pt-8">
79
82
  {galleryHref && (
80
- <div className="flex items-center px-6 pt-4">
83
+ <div className="flex items-center px-6 pt-4 sm:-mt-8">
81
84
  <a
82
85
  href={galleryHref}
83
86
  aria-label="Back to gallery"
84
- className="inline-flex items-center"
87
+ className={cn(
88
+ buttonVariants({ variant: "text", size: "sm" }),
89
+ "gap-2 px-0 text-muted-foreground hover:text-foreground",
90
+ )}
85
91
  >
86
- <img src="logo.png" alt="marimo logo" className="h-6 w-auto" />
92
+ <ArrowLeftIcon className="size-4" aria-hidden={true} />
93
+ <span>Back</span>
87
94
  </a>
88
95
  </div>
89
96
  )}
@@ -6,7 +6,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
6
6
  import { createLoader } from "@/plugins/impl/vega/vega-loader";
7
7
  import { Functions } from "@/utils/functions";
8
8
  import type { DataURLString } from "@/utils/json/base64";
9
- import { patchFetch, patchVegaLoader } from "../files";
9
+ import { patchFetch, patchVegaLoader, resolveVirtualFileURL } from "../files";
10
10
 
11
11
  // Start a tiny server to serve virtual files
12
12
  const server = http.createServer((request, response) => {
@@ -350,6 +350,181 @@ describe("patchVegaLoader - loader.load", () => {
350
350
  });
351
351
  });
352
352
 
353
+ describe("resolveVirtualFileURL", () => {
354
+ // Mock URL.createObjectURL for jsdom environment
355
+ const mockBlobURLs = new Map<string, Blob>();
356
+ let blobCounter = 0;
357
+
358
+ beforeAll(() => {
359
+ URL.createObjectURL = vi.fn((blob: Blob) => {
360
+ const url = `blob:test-${blobCounter++}`;
361
+ mockBlobURLs.set(url, blob);
362
+ return url;
363
+ });
364
+ URL.revokeObjectURL = vi.fn((url: string) => {
365
+ mockBlobURLs.delete(url);
366
+ });
367
+ });
368
+
369
+ afterAll(() => {
370
+ mockBlobURLs.clear();
371
+ });
372
+
373
+ it("should return a blob URL for virtual files", () => {
374
+ const virtualFiles = {
375
+ "/@file/widget.js":
376
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQgeyByZW5kZXI6ICgpID0+IHt9IH0=" as DataURLString,
377
+ };
378
+
379
+ const result = resolveVirtualFileURL("/@file/widget.js", virtualFiles);
380
+
381
+ expect(result).toMatch(/^blob:/);
382
+ });
383
+
384
+ it("should return the original URL for non-virtual files", () => {
385
+ const virtualFiles = {};
386
+
387
+ const result = resolveVirtualFileURL(
388
+ "http://example.com/widget.js",
389
+ virtualFiles,
390
+ );
391
+
392
+ expect(result).toBe("http://example.com/widget.js");
393
+ });
394
+
395
+ it("should handle various URL formats", () => {
396
+ const virtualFiles = {
397
+ "/@file/module.js":
398
+ "data:text/javascript;base64,Y29uc29sZS5sb2coJ3Rlc3QnKQ==" as DataURLString,
399
+ };
400
+
401
+ const testUrls = [
402
+ "/@file/module.js",
403
+ "./@file/module.js",
404
+ "http://example.com/@file/module.js",
405
+ ];
406
+
407
+ for (const url of testUrls) {
408
+ const result = resolveVirtualFileURL(url, virtualFiles);
409
+ expect(result).toMatch(/^blob:/);
410
+ }
411
+ });
412
+
413
+ it("should create blob URL with correct content", async () => {
414
+ const jsCode = "export default { render: () => {} }";
415
+ const base64Code = btoa(jsCode);
416
+ const virtualFiles = {
417
+ "/@file/test-module.js":
418
+ `data:text/javascript;base64,${base64Code}` as DataURLString,
419
+ };
420
+
421
+ const blobUrl = resolveVirtualFileURL(
422
+ "/@file/test-module.js",
423
+ virtualFiles,
424
+ );
425
+
426
+ expect(blobUrl).toMatch(/^blob:/);
427
+ expect(URL.createObjectURL).toHaveBeenCalled();
428
+
429
+ // Verify blob content through the mock
430
+ const blob = mockBlobURLs.get(blobUrl);
431
+ expect(blob).toBeDefined();
432
+ const text = await blob!.text();
433
+ expect(text).toBe(jsCode);
434
+ });
435
+
436
+ it("should handle file:// URLs with @file/ paths", () => {
437
+ const virtualFiles = {
438
+ "/@file/local-module.js":
439
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=" as DataURLString,
440
+ };
441
+
442
+ const result = resolveVirtualFileURL(
443
+ "file:///Users/test/@file/local-module.js",
444
+ virtualFiles,
445
+ );
446
+
447
+ expect(result).toMatch(/^blob:/);
448
+ });
449
+
450
+ it("should handle different MIME types", async () => {
451
+ const virtualFiles = {
452
+ "/@file/script.js":
453
+ "data:application/javascript;base64,Y29uc3QgeCA9IDE=" as DataURLString,
454
+ };
455
+
456
+ const blobUrl = resolveVirtualFileURL("/@file/script.js", virtualFiles);
457
+
458
+ // Should still be a valid blob URL
459
+ expect(blobUrl).toMatch(/^blob:/);
460
+
461
+ // Verify blob content through the mock
462
+ const blob = mockBlobURLs.get(blobUrl);
463
+ expect(blob).toBeDefined();
464
+ const text = await blob!.text();
465
+ expect(text).toBe("const x = 1");
466
+ });
467
+
468
+ it("should handle blob: base URIs correctly", () => {
469
+ // Mock document.baseURI to simulate blob: protocol
470
+ const originalBaseURI = document.baseURI;
471
+ Object.defineProperty(document, "baseURI", {
472
+ value: "blob:https://example.com/uuid",
473
+ configurable: true,
474
+ });
475
+
476
+ const virtualFiles = {
477
+ "/@file/blob-module.js":
478
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=" as DataURLString,
479
+ };
480
+
481
+ const result = resolveVirtualFileURL("/@file/blob-module.js", virtualFiles);
482
+
483
+ expect(result).toMatch(/^blob:/);
484
+
485
+ // Restore original baseURI
486
+ Object.defineProperty(document, "baseURI", {
487
+ value: originalBaseURI,
488
+ configurable: true,
489
+ });
490
+ });
491
+
492
+ it("should handle data URLs with no explicit MIME type", async () => {
493
+ const virtualFiles = {
494
+ "/@file/generic.bin": "data:;base64,SGVsbG8gV29ybGQ=" as DataURLString,
495
+ };
496
+
497
+ const blobUrl = resolveVirtualFileURL("/@file/generic.bin", virtualFiles);
498
+ expect(blobUrl).toMatch(/^blob:/);
499
+
500
+ // Verify blob content through the mock
501
+ const blob = mockBlobURLs.get(blobUrl);
502
+ expect(blob).toBeDefined();
503
+ const text = await blob!.text();
504
+ expect(text).toBe("Hello World");
505
+ });
506
+
507
+ it("should match URLs with prefix paths before /@file/", async () => {
508
+ const virtualFiles = {
509
+ "/@file/4263-66-yUGhgQXp.js":
510
+ "data:application/javascript;base64,ZnVuY3Rpb24gcmVuZGVyKCkge30=" as DataURLString,
511
+ };
512
+
513
+ const blobUrl = resolveVirtualFileURL(
514
+ "https://molab.marimo.app/preview/@file/4263-66-yUGhgQXp.js",
515
+ virtualFiles,
516
+ );
517
+
518
+ expect(blobUrl).toMatch(/^blob:/);
519
+
520
+ // Verify blob content through the mock
521
+ const blob = mockBlobURLs.get(blobUrl);
522
+ expect(blob).toBeDefined();
523
+ const text = await blob!.text();
524
+ expect(text).toBe("function render() {}");
525
+ });
526
+ });
527
+
353
528
  describe("maybeGetVirtualFile utility function", () => {
354
529
  it("should handle URLs without leading dots correctly", async () => {
355
530
  const virtualFiles = {
@@ -370,6 +545,25 @@ describe("maybeGetVirtualFile utility function", () => {
370
545
  expect(text2).toBe("test");
371
546
  });
372
547
 
548
+ it("should match URLs with prefix paths before /@file/", async () => {
549
+ const virtualFiles = {
550
+ "/@file/4263-66-yUGhgQXp.js":
551
+ "data:application/javascript;base64,ZnVuY3Rpb24gcmVuZGVyKCkge30=" as DataURLString,
552
+ };
553
+
554
+ const unpatch = patchFetch(virtualFiles);
555
+
556
+ // Test URL with a prefix path before /@file/
557
+ const response = await window.fetch(
558
+ "https://molab.marimo.app/preview/@file/4263-66-yUGhgQXp.js",
559
+ );
560
+ const text = await response.text();
561
+
562
+ expect(text).toBe("function render() {}");
563
+
564
+ unpatch();
565
+ });
566
+
373
567
  it("should handle complex file:// URLs with nested paths", async () => {
374
568
  const virtualFiles = {
375
569
  "/@file/nested/data.json":