@marimo-team/islands 0.25.3-dev308 → 0.25.3-dev310

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 (106) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DJnSxHJB.js → ConnectedDataExplorerComponent-DieU6L4x.js} +1 -1
  2. package/dist/{ErrorBoundary-flfhLFje.js → ErrorBoundary-DjoeT4kv.js} +1 -1
  3. package/dist/{abnfDiagram-VCTEODGH-CR0U5hup.js → abnfDiagram-VCTEODGH-LOVrjbdp.js} +2 -2
  4. package/dist/{any-language-editor-C5dsTpeB.js → any-language-editor-DMC2JE9_.js} +1 -1
  5. package/dist/{architectureDiagram-5GKGNRK7-Bc_fbI1Q.js → architectureDiagram-5GKGNRK7-RZsm312h.js} +1 -1
  6. package/dist/assets/__vite-browser-external-BSp9tJ_k.js +1 -0
  7. package/dist/assets/worker-BdfPA0g5.js +171 -0
  8. package/dist/{chat-ui-C4iLRzY0.js → chat-ui-DkuxGEDp.js} +418 -418
  9. package/dist/{chunk-IMKFNOWR-y9z5oeAY.js → chunk-IMKFNOWR-B6agbLjo.js} +1 -1
  10. package/dist/{chunk-SHT3W25Y-Tu-V-e_v.js → chunk-SHT3W25Y-CfULafYG.js} +1 -1
  11. package/dist/{chunk-SVP7TREG-CJFxs5R_.js → chunk-SVP7TREG-DEOpPMJp.js} +1 -1
  12. package/dist/{chunk-TICWLB2K-zkTP7ya7.js → chunk-TICWLB2K-DaH-gkX5.js} +1 -1
  13. package/dist/{classDiagram-ZZMXUADV-DxJAZI0N.js → classDiagram-ZZMXUADV-C0CyiWQ7.js} +2 -2
  14. package/dist/{classDiagram-v2-VYDZK3BY-TmHo2BpS.js → classDiagram-v2-VYDZK3BY-xfIc9e2Y.js} +2 -2
  15. package/dist/{code-visibility-BDs_r7jH.js → code-visibility-CduMVVFS.js} +32 -32
  16. package/dist/{common-CMTsL35V.js → common-CMcEcucH.js} +919 -919
  17. package/dist/{config-BG2qKzDg.js → config-BVYA1P0C.js} +103 -76
  18. package/dist/{cynefinDiagram-5FMLGOSQ-Eb40hmhD.js → cynefinDiagram-5FMLGOSQ-DwGYhKu0.js} +1 -1
  19. package/dist/{dagre-GXQ25YYZ-_paaVwit.js → dagre-GXQ25YYZ-DfMaWJOV.js} +1 -1
  20. package/dist/{data-grid-overlay-editor-es6V6qf-.js → data-grid-overlay-editor-BYrIVut3.js} +1 -1
  21. package/dist/{diagram-S7CK7UJ4-BtKJm1_4.js → diagram-S7CK7UJ4-Bw_L87GZ.js} +1 -1
  22. package/dist/{diagram-UQ7AKVKN-DQbxhEMd.js → diagram-UQ7AKVKN-nsrs3Fbz.js} +1 -1
  23. package/dist/{diagram-VX7I27RA-DITDIfGC.js → diagram-VX7I27RA-7ST9lEqP.js} +1 -1
  24. package/dist/{diagram-Z3DM3KII-Ct9_zo6F.js → diagram-Z3DM3KII-BnGte-HX.js} +1 -1
  25. package/dist/{ebnfDiagram-PWID7BFC-6cOntwfv.js → ebnfDiagram-PWID7BFC-zaKyJ1YL.js} +2 -2
  26. package/dist/{erDiagram-RLTQ6QDP-CJiOER41.js → erDiagram-RLTQ6QDP-Cx7g95FZ.js} +1 -1
  27. package/dist/{error-banner-DGOrmBk5.js → error-banner-DkQNU4bn.js} +128 -128
  28. package/dist/{error-entries-BWmleiL0.js → error-entries-istW7Xbs.js} +589 -544
  29. package/dist/{feedback-button-D4eVjeNs.js → feedback-button-Zvm26aOQ.js} +107 -106
  30. package/dist/{flowDiagram-HODETNUW-1lPBEUuW.js → flowDiagram-HODETNUW-C7nIqaHP.js} +2 -2
  31. package/dist/{formats-qhBmYb0l.js → formats-CMa7L3E8.js} +1 -1
  32. package/dist/{glide-data-editor-ahfaPdFW.js → glide-data-editor-BE-GgVp4.js} +3 -3
  33. package/dist/{globals-m8oswz--.js → globals-Dbru-WLP.js} +1 -1
  34. package/dist/{html-to-image-B8hE6HIH.js → html-to-image-pPbR2cDx.js} +4512 -4464
  35. package/dist/{infoDiagram-27XIBGKW-C6K4tri5.js → infoDiagram-27XIBGKW-BTM_NXsn.js} +1 -1
  36. package/dist/{ishikawaDiagram-5VMMS53U-DwcwgRNH.js → ishikawaDiagram-5VMMS53U-K0FZrlvv.js} +1 -1
  37. package/dist/{kanban-definition-UXKFOSKX-E489OMdc.js → kanban-definition-UXKFOSKX-ftznOyrz.js} +1 -1
  38. package/dist/main.js +6595 -6186
  39. package/dist/{mermaid-DMPMkVqY.js → mermaid-CNgH43ED.js} +33 -33
  40. package/dist/{mindmap-definition-YA3MSWOX-DWHjasOp.js → mindmap-definition-YA3MSWOX-CmzbxKfM.js} +1 -1
  41. package/dist/{number-overlay-editor-C6klKzEr.js → number-overlay-editor-qvlPZ-l-.js} +1 -1
  42. package/dist/{pegDiagram-XKGWAZYB-COjcPOZT.js → pegDiagram-XKGWAZYB-DfTAWfGE.js} +2 -2
  43. package/dist/{pieDiagram-E7YTZNPT-BHAVZ38h.js → pieDiagram-E7YTZNPT-_kGQstZj.js} +1 -1
  44. package/dist/{process-output-DZDL_j1a.js → process-output-BySO1nIs.js} +3 -3
  45. package/dist/{railroadDiagram-O6MQD6OU-94ThFyKX.js → railroadDiagram-O6MQD6OU-D41WmIff.js} +2 -2
  46. package/dist/{requirementDiagram-BXWQKSXE-CpF-Fxbb.js → requirementDiagram-BXWQKSXE-Bh53ZI3I.js} +1 -1
  47. package/dist/{reveal-component-DwnxPyVz.js → reveal-component-DQaegdcW.js} +4 -4
  48. package/dist/{sequenceDiagram-WJ2MYXX4-CvANK2x7.js → sequenceDiagram-WJ2MYXX4-DBX27dNm.js} +1 -1
  49. package/dist/{stateDiagram-D77RDMKH-WHuJ4mJL.js → stateDiagram-D77RDMKH-CSYz2IZi.js} +2 -2
  50. package/dist/{stateDiagram-v2-MP3YSRHH-DbO7lwYq.js → stateDiagram-v2-MP3YSRHH-DVLvtOqn.js} +2 -2
  51. package/dist/{swimlanes-42K2YHIH-CS2s29Vo.js → swimlanes-42K2YHIH-D59N0ktA.js} +1 -1
  52. package/dist/{swimlanesDiagram-VR7AAH4N-CXaA1vQg.js → swimlanesDiagram-VR7AAH4N-CRgpmVSW.js} +2 -2
  53. package/dist/{timeline-definition-24CTP7MA-B-SQMg5h.js → timeline-definition-24CTP7MA-C_xIIZDM.js} +1 -1
  54. package/dist/toDate-5qPtZ6HL.js +94 -0
  55. package/dist/{vega-component-DwqGfN3_.js → vega-component-Bly2lDhz.js} +10 -10
  56. package/dist/{vennDiagram-4TSXK5OY-etEFgvaG.js → vennDiagram-4TSXK5OY-DlOf0Mjs.js} +1 -1
  57. package/dist/{wardleyDiagram-VM6X3IG4-BEQN7g62.js → wardleyDiagram-VM6X3IG4-zD_2guVe.js} +1 -1
  58. package/dist/{xychartDiagram-S5SC5T6Z-D8-e5Z0R.js → xychartDiagram-S5SC5T6Z-CA0Dy1ZM.js} +1 -1
  59. package/package.json +1 -1
  60. package/src/__mocks__/executions.ts +35 -13
  61. package/src/__mocks__/requests.ts +0 -1
  62. package/src/components/editor/cell/__tests__/useRunCells.test.tsx +86 -101
  63. package/src/components/editor/cell/useRunCells.ts +38 -65
  64. package/src/components/editor/notebook-cell.tsx +32 -20
  65. package/src/components/editor/output/ansi-reduce.ts +7 -0
  66. package/src/core/ai/__tests__/staged-cells.test.ts +1 -1
  67. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +2 -0
  68. package/src/core/cells/__tests__/cells.test.ts +0 -14
  69. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +15 -0
  70. package/src/core/cells/__tests__/utils.test.ts +1 -1
  71. package/src/core/cells/add-missing-import.ts +2 -5
  72. package/src/core/cells/cell.ts +1 -0
  73. package/src/core/cells/cells.ts +1 -2
  74. package/src/core/executions/__tests__/atoms.test.ts +227 -43
  75. package/src/core/executions/__tests__/hooks.test.tsx +1 -2
  76. package/src/core/executions/__tests__/input.test.ts +86 -0
  77. package/src/core/executions/__tests__/run.test.ts +390 -0
  78. package/src/core/executions/__tests__/select.test.ts +105 -30
  79. package/src/core/executions/atoms.ts +186 -71
  80. package/src/core/executions/index.ts +7 -6
  81. package/src/core/executions/input.ts +55 -0
  82. package/src/core/executions/run.ts +120 -0
  83. package/src/core/executions/select.ts +59 -24
  84. package/src/core/islands/__tests__/bridge.test.ts +35 -54
  85. package/src/core/islands/bootstrap.ts +5 -1
  86. package/src/core/islands/bridge.ts +16 -28
  87. package/src/core/islands/components/IslandControls.tsx +15 -7
  88. package/src/core/islands/components/__tests__/IslandControls.test.tsx +36 -53
  89. package/src/core/islands/worker/worker.tsx +45 -19
  90. package/src/core/network/__tests__/requests-lazy.test.ts +14 -24
  91. package/src/core/network/__tests__/requests-toasting.test.tsx +7 -7
  92. package/src/core/network/requests-lazy.ts +2 -3
  93. package/src/core/network/requests-network.ts +0 -9
  94. package/src/core/network/requests-static.ts +0 -1
  95. package/src/core/network/requests-toasting.tsx +0 -1
  96. package/src/core/network/types.ts +0 -2
  97. package/src/core/notebook/replica/__tests__/fixtures.ts +17 -1
  98. package/src/core/notebook/replica/backend.ts +3 -1
  99. package/src/core/runtime/__tests__/runtime.test.ts +36 -0
  100. package/src/core/runtime/config.ts +36 -1
  101. package/src/core/runtime/runtime.ts +21 -1
  102. package/src/core/transports/static.ts +4 -3
  103. package/src/core/wasm/bridge.ts +0 -17
  104. package/dist/assets/__vite-browser-external-DzTYIHH_.js +0 -1
  105. package/dist/assets/worker-BH08I2Il.js +0 -171
  106. package/dist/static-state-Dtocd9UO.js +0 -68
@@ -2,12 +2,7 @@
2
2
 
3
3
  import type { components } from "@marimo-team/marimo-api";
4
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
- import {
6
- cellId,
7
- requestId,
8
- uiElementId,
9
- widgetModelId,
10
- } from "@/__tests__/branded";
5
+ import { requestId, uiElementId, widgetModelId } from "@/__tests__/branded";
11
6
  import { notebookAtom } from "@/core/cells/cells";
12
7
  import { islandsPendingInitialRunsAtom } from "@/core/islands/state";
13
8
 
@@ -50,6 +45,7 @@ vi.stubGlobal("URL", MockURL);
50
45
  const {
51
46
  mockBridge,
52
47
  mockLoadPackages,
48
+ mockNotebookCall,
53
49
  mockReplaceSessionRequest,
54
50
  mockStopSessionRequest,
55
51
  mockStartSessionRequest,
@@ -61,6 +57,7 @@ const {
61
57
  } = vi.hoisted(() => ({
62
58
  mockBridge: vi.fn(),
63
59
  mockLoadPackages: vi.fn(),
60
+ mockNotebookCall: vi.fn(),
64
61
  mockReplaceSessionRequest: vi.fn(),
65
62
  mockStopSessionRequest: vi.fn(),
66
63
  mockStartSessionRequest: vi.fn(),
@@ -84,6 +81,7 @@ vi.mock("@/core/wasm/rpc", () => ({
84
81
  request: {
85
82
  bridge: mockBridge,
86
83
  loadPackages: mockLoadPackages,
84
+ notebookCall: mockNotebookCall,
87
85
  replaceSession: mockReplaceSessionRequest,
88
86
  startSession: mockStartSessionRequest,
89
87
  stopSession: mockStopSessionRequest,
@@ -411,9 +409,6 @@ describe("IslandsPyodideBridge", () => {
411
409
  sendKernelMessage(1);
412
410
 
413
411
  expect(handlers.message.mock.calls).toEqual([[cell]]);
414
- expect(() => bridge.notebookBackend.notebook("n1")).toThrow(
415
- "Islands do not edit",
416
- );
417
412
  });
418
413
 
419
414
  it("resets the notebook stream when the managed session is replaced or stopped", async () => {
@@ -754,60 +749,47 @@ describe("IslandsPyodideBridge", () => {
754
749
  });
755
750
  });
756
751
 
757
- describe("sendRun", () => {
752
+ describe("notebookBackend.notebook", () => {
758
753
  beforeEach(initializeSingleApp);
759
754
 
760
- it("should include type field in control request", async () => {
761
- const request = {
762
- cellIds: [cellId("cell-1"), cellId("cell-2")],
763
- codes: ["print('hello')", "print('world')"],
764
- };
765
-
766
- await bridge.sendRun(request);
767
-
768
- expect(mockBridge).toHaveBeenCalledWith({
769
- appId: "app-1",
770
- functionName: "put_control_request",
771
- payload: {
772
- type: "execute-cells",
773
- request: undefined,
774
- executionRequests: [
775
- {
776
- type: "execute-cell",
777
- cellId: "cell-1",
778
- code: "print('hello')",
779
- },
780
- {
781
- type: "execute-cell",
782
- cellId: "cell-2",
783
- code: "print('world')",
784
- },
785
- ],
755
+ it("sends notebook commands to the managed session's host", async () => {
756
+ mockNotebookCall.mockResolvedValue({
757
+ status: 202,
758
+ body: {
759
+ id: "run-1",
760
+ notebook_id: "n1",
761
+ runtime_id: "r1",
762
+ cells: ["c1"],
763
+ status: "queued",
764
+ outcome: null,
765
+ started_at: null,
766
+ completed_at: null,
786
767
  },
787
- sessionGeneration: 1,
788
768
  });
789
- });
790
-
791
- it("should call loadPackages before putControlRequest", async () => {
792
- const request = {
793
- cellIds: [cellId("cell-1")],
794
- codes: ["import pandas"],
795
- };
796
769
 
797
- await bridge.sendRun(request);
770
+ const result = await bridge.notebookBackend
771
+ .notebook("n1")
772
+ .run({ cells: [{ id: "c1" }], idempotencyKey: "key-1" });
798
773
 
799
- // Verify loadPackages was called with joined codes
800
- expect(mockLoadPackages).toHaveBeenCalledWith({
774
+ expect(result.ok).toBe(true);
775
+ expect(mockNotebookCall).toHaveBeenCalledWith({
801
776
  appId: "app-1",
802
- code: "import pandas",
803
777
  sessionGeneration: 1,
778
+ call: expect.objectContaining({
779
+ operation: "runtime.run",
780
+ params: { notebook_id: "n1" },
781
+ headers: { "idempotency-key": "key-1" },
782
+ body: { cells: [{ id: "c1" }] },
783
+ }),
804
784
  });
785
+ });
786
+
787
+ it("has no commands without an active session", async () => {
788
+ await bridge.stopSession();
805
789
 
806
- // Verify order: loadPackages should be called before bridge
807
- const loadPackagesCallOrder =
808
- mockLoadPackages.mock.invocationCallOrder[0];
809
- const bridgeCallOrder = mockBridge.mock.invocationCallOrder[0];
810
- expect(loadPackagesCallOrder).toBeLessThan(bridgeCallOrder);
790
+ expect(() => bridge.notebookBackend.notebook("n1")).toThrow(
791
+ "No island session is active",
792
+ );
811
793
  });
812
794
  });
813
795
 
@@ -857,7 +839,6 @@ describe("IslandsPyodideBridge", () => {
857
839
  functionName: "",
858
840
  args: {},
859
841
  });
860
- await bridge.sendRun({ cellIds: [], codes: [] });
861
842
  await bridge.sendModelValue({
862
843
  modelId: widgetModelId(""),
863
844
  message: { method: "update", state: {}, bufferPaths: [] },
@@ -36,7 +36,10 @@ import { queryParamHandlers } from "../kernel/queryParamHandlers";
36
36
  import { RuntimeState } from "../kernel/RuntimeState";
37
37
  import { initialModeAtom } from "../mode";
38
38
  import { requestClientAtom } from "../network/requests";
39
- import { activeNotebookReplicaAtom } from "../session/active-notebook";
39
+ import {
40
+ activeDocumentModelAtom,
41
+ activeNotebookReplicaAtom,
42
+ } from "../session/active-notebook";
40
43
  import { store as defaultStore } from "../state/jotai";
41
44
  import type { IslandsPyodideBridge } from "./bridge";
42
45
  import { MarimoIslandElement } from "./components/web-components";
@@ -141,6 +144,7 @@ export async function initializeIslands(
141
144
  });
142
145
  const lifetime = new AbortController();
143
146
  documentModel.start({ signal: lifetime.signal });
147
+ store.set(activeDocumentModelAtom, documentModel);
144
148
  followDocument({
145
149
  store,
146
150
  model: documentModel,
@@ -1,9 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
3
 
4
- import type { NotebookTransport } from "@marimo-team/client";
4
+ import { NotebookClient, type NotebookTransport } from "@marimo-team/client";
5
5
  import { getWorkerRPC } from "@/core/wasm/rpc";
6
- import { Arrays } from "@/utils/arrays";
7
6
  import { Deferred } from "@/utils/Deferred";
8
7
  import { throwNotImplemented } from "@/utils/functions";
9
8
  import { Logger } from "@/utils/Logger";
@@ -99,8 +98,21 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
99
98
  { once: true },
100
99
  );
101
100
  }),
102
- notebook: () => {
103
- throw new Error("Islands do not edit notebooks");
101
+ notebook: (notebookId) => {
102
+ const session = this.session;
103
+ if (session === undefined) {
104
+ throw new Error("No island session is active");
105
+ }
106
+ const { appId, sessionGeneration } = session;
107
+ return NotebookClient.unstable_over(
108
+ (call) =>
109
+ this.rpc.proxy.request.notebookCall({
110
+ appId,
111
+ sessionGeneration,
112
+ call,
113
+ }),
114
+ { notebookId },
115
+ );
104
116
  },
105
117
  };
106
118
 
@@ -353,30 +365,6 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
353
365
  // EditRequests Implementation
354
366
  // ============================================================================
355
367
 
356
- public sendRun: EditRequests["sendRun"] = async (request): Promise<null> => {
357
- const session = await this.getActiveSession();
358
- await this.rpc.proxy.request.loadPackages({
359
- appId: session.appId,
360
- code: request.codes.join("\n"),
361
- sessionGeneration: session.sessionGeneration,
362
- });
363
- await this.putControlRequest(
364
- {
365
- type: "execute-cells",
366
- request: request.request,
367
- executionRequests: Arrays.zip(request.cellIds, request.codes).map(
368
- ([cellId, code]) => ({
369
- type: "execute-cell",
370
- cellId,
371
- code,
372
- }),
373
- ),
374
- },
375
- session,
376
- );
377
- return null;
378
- };
379
-
380
368
  // ============================================================================
381
369
  // Not Implemented (Read-Only Mode)
382
370
  // ============================================================================
@@ -1,11 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { useAtomValue, useStore } from "jotai";
3
4
  import { CopyIcon, PlayIcon } from "lucide-react";
4
5
  import type { JSX } from "react";
5
6
  import { Button } from "@/components/ui/button";
6
7
  import { Tooltip } from "@/components/ui/tooltip";
7
8
  import type { CellId } from "@/core/cells/ids";
8
- import { useRequestClient } from "@/core/network/requests";
9
+ import { runCells } from "@/core/executions";
10
+ import { activeDocumentModelAtom } from "@/core/session/active-notebook";
9
11
  import { copyToClipboard } from "@/utils/copy";
10
12
  import { Logger } from "@/utils/Logger";
11
13
 
@@ -66,21 +68,27 @@ export const IslandControls: React.FC<IslandControlsProps> = ({
66
68
  codeCallback,
67
69
  visible,
68
70
  }) => {
69
- const { sendRun } = useRequestClient();
71
+ const store = useStore();
72
+ const model = useAtomValue(activeDocumentModelAtom);
70
73
 
71
74
  const handleCopy = () => {
72
75
  copyToClipboard(codeCallback());
73
76
  };
74
77
 
78
+ // The island's editor is a UI element, so its code reaches the document
79
+ // here rather than through an editor binding.
75
80
  const handleRun = async () => {
81
+ if (model === null) {
82
+ return;
83
+ }
84
+ const code = codeCallback();
76
85
  try {
77
- await sendRun({
78
- cellIds: [cellId],
79
- codes: [codeCallback()],
80
- });
86
+ store.set(model.editAtom, (tx) => tx.setCode(cellId, code));
81
87
  } catch (error) {
82
- Logger.error("Failed to run cell:", error);
88
+ Logger.error("The island's cell is not in the document", error);
89
+ return;
83
90
  }
91
+ await runCells(store, [cellId]);
84
92
  };
85
93
 
86
94
  return (
@@ -1,17 +1,21 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
3
  import { fireEvent, render, screen } from "@testing-library/react";
4
- import { beforeEach, describe, expect, it, vi } from "vitest";
5
- import { MockRequestClient } from "@/__mocks__/requests";
4
+ import { Provider } from "jotai";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { mockDocumentModel } from "@/__mocks__/document";
6
7
  import { TooltipProvider } from "@/components/ui/tooltip";
7
8
  import type { CellId } from "@/core/cells/ids";
8
- import * as requestsModule from "@/core/network/requests";
9
+ import type { runCells } from "@/core/executions";
10
+ import { cell, document } from "@/core/notebook/document/__tests__/fixtures";
11
+ import { store } from "@/core/state/jotai";
9
12
  import * as copyModule from "@/utils/copy";
10
13
  import { IslandControls } from "../IslandControls";
11
14
 
12
- // Mock the dependencies
13
- vi.mock("@/core/network/requests", () => ({
14
- useRequestClient: vi.fn(),
15
+ const mockRunCells = vi.fn<typeof runCells>();
16
+
17
+ vi.mock("@/core/executions", () => ({
18
+ runCells: (...args: Parameters<typeof runCells>) => mockRunCells(...args),
15
19
  }));
16
20
 
17
21
  vi.mock("@/utils/copy", () => ({
@@ -19,30 +23,43 @@ vi.mock("@/utils/copy", () => ({
19
23
  }));
20
24
 
21
25
  describe("IslandControls", () => {
22
- const mockSendRun =
23
- vi.fn<ReturnType<typeof requestsModule.useRequestClient>["sendRun"]>();
24
26
  const mockCopyToClipboard = vi.fn();
25
27
  const mockCodeCallback = vi.fn(() => "print('test code')");
26
28
  const cellId = "test-cell-id" as CellId;
29
+ let model: ReturnType<typeof mockDocumentModel>;
30
+
31
+ function cellCode() {
32
+ return store.get(model.model.documentAtom)?.cells.get(cellId)?.code;
33
+ }
27
34
 
28
35
  // Helper to render with required providers
29
36
  const renderWithProviders = (component: React.ReactElement) => {
30
- return render(<TooltipProvider>{component}</TooltipProvider>);
37
+ return render(
38
+ <Provider store={store}>
39
+ <TooltipProvider>{component}</TooltipProvider>
40
+ </Provider>,
41
+ );
31
42
  };
32
43
 
33
44
  beforeEach(() => {
34
45
  vi.clearAllMocks();
35
- mockSendRun.mockResolvedValue(null);
36
-
37
- vi.spyOn(requestsModule, "useRequestClient").mockReturnValue(
38
- MockRequestClient.create({ sendRun: mockSendRun }),
39
- );
46
+ mockRunCells.mockResolvedValue(undefined);
47
+ const confirmed = document();
48
+ model = mockDocumentModel(store, {
49
+ ...confirmed,
50
+ cells: new Map([[cellId, cell(cellId, "print('old')")]]),
51
+ revision: { ...confirmed.revision, order: [cellId] },
52
+ });
40
53
 
41
54
  vi.spyOn(copyModule, "copyToClipboard").mockImplementation(
42
55
  mockCopyToClipboard,
43
56
  );
44
57
  });
45
58
 
59
+ afterEach(() => {
60
+ model.stop();
61
+ });
62
+
46
63
  it("should not display when visible is false", () => {
47
64
  const { container } = renderWithProviders(
48
65
  <IslandControls
@@ -102,7 +119,7 @@ describe("IslandControls", () => {
102
119
  expect(mockCopyToClipboard).toHaveBeenCalledWith("print('test code')");
103
120
  });
104
121
 
105
- it("should run cell when run button is clicked", async () => {
122
+ it("writes the editor's code to the cell and runs it through the host", async () => {
106
123
  renderWithProviders(
107
124
  <IslandControls
108
125
  cellId={cellId}
@@ -116,42 +133,10 @@ describe("IslandControls", () => {
116
133
 
117
134
  fireEvent.click(runButton);
118
135
 
119
- // Wait for async operation
120
- await vi.waitFor(() => {
121
- expect(mockCodeCallback).toHaveBeenCalled();
122
- expect(mockSendRun).toHaveBeenCalledWith({
123
- cellIds: [cellId],
124
- codes: ["print('test code')"],
125
- });
126
- });
127
- });
128
-
129
- it("should handle run errors gracefully", async () => {
130
- const consoleErrorSpy = vi
131
- .spyOn(console, "error")
132
- .mockImplementation(() => {});
133
-
134
- mockSendRun.mockRejectedValueOnce(new Error("Run failed"));
135
-
136
- renderWithProviders(
137
- <IslandControls
138
- cellId={cellId}
139
- codeCallback={mockCodeCallback}
140
- visible={true}
141
- />,
142
- );
143
-
144
- const buttons = screen.getAllByRole("button");
145
- const runButton = buttons[1];
146
-
147
- fireEvent.click(runButton);
148
-
149
- // Wait for error to be logged
150
136
  await vi.waitFor(() => {
151
- expect(consoleErrorSpy).toHaveBeenCalled();
137
+ expect(mockRunCells).toHaveBeenCalledWith(store, [cellId]);
152
138
  });
153
-
154
- consoleErrorSpy.mockRestore();
139
+ expect(cellCode()).toBe("print('test code')");
155
140
  });
156
141
 
157
142
  it("should get fresh code on each button click", async () => {
@@ -175,11 +160,9 @@ describe("IslandControls", () => {
175
160
 
176
161
  fireEvent.click(runButton);
177
162
  await vi.waitFor(() => {
178
- expect(mockSendRun).toHaveBeenCalledWith({
179
- cellIds: [cellId],
180
- codes: ["code version 2"],
181
- });
163
+ expect(mockRunCells).toHaveBeenCalledWith(store, [cellId]);
182
164
  });
165
+ expect(cellCode()).toBe("code version 2");
183
166
 
184
167
  fireEvent.click(copyButton);
185
168
  expect(mockCopyToClipboard).toHaveBeenCalledWith("code version 3");
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { unstable_NotebookCall } from "@marimo-team/client";
3
4
  import type { PyodideInterface } from "pyodide";
4
5
  import {
5
6
  createRPC,
@@ -76,6 +77,13 @@ async function startSession(
76
77
  // Follow before the session instantiates. Each subscription begins with
77
78
  // its own kernel-ready and the session's retained notifications.
78
79
  try {
80
+ const host = session.bridge.notebook;
81
+ try {
82
+ // Host runs name cells, so the worker loads their imports itself.
83
+ host.set_package_loader(loadPackagesForCode);
84
+ } finally {
85
+ host.destroy();
86
+ }
79
87
  await follow(session.bridge, opts.sessionGeneration);
80
88
  } catch (error) {
81
89
  await session.stop();
@@ -100,6 +108,27 @@ async function startSession(
100
108
  }
101
109
  }
102
110
 
111
+ async function loadPackagesForCode(code: string): Promise<void> {
112
+ if (shouldLoadDuckDBPackages(code)) {
113
+ // Add pandas and duckdb to the code for mo.sql and for remote duckdb sources
114
+ code = `import pandas\n${code}`;
115
+ code = `import duckdb\n${code}`;
116
+ code = `import sqlglot\n${code}`;
117
+
118
+ // Polars + SQL requires pyarrow, and installing
119
+ // after notebook load does not work. As a heuristic,
120
+ // if it appears that the notebook uses polars, add pyarrow.
121
+ if (code.includes("polars")) {
122
+ code = `import pyarrow\n${code}`;
123
+ }
124
+ }
125
+
126
+ await self.pyodide.loadPackagesFromImports(code, {
127
+ messageCallback: Logger.log,
128
+ errorCallback: Logger.error,
129
+ });
130
+ }
131
+
103
132
  async function stopActiveSession(): Promise<void> {
104
133
  // Stopping closes every session and its notebook subscriptions.
105
134
  await self.controller.stopSession();
@@ -166,27 +195,24 @@ const requestHandler = createRPCRequestHandler({
166
195
  await pyodideReadyPromise; // Make sure loading is done
167
196
  await enqueueSession(async () => {
168
197
  requireActiveBridge(opts);
198
+ await loadPackagesForCode(opts.code);
199
+ });
200
+ },
169
201
 
170
- let { code } = opts;
171
-
172
- if (shouldLoadDuckDBPackages(code)) {
173
- // Add pandas and duckdb to the code for mo.sql and for remote duckdb sources
174
- code = `import pandas\n${code}`;
175
- code = `import duckdb\n${code}`;
176
- code = `import sqlglot\n${code}`;
177
-
178
- // Polars + SQL requires pyarrow, and installing
179
- // after notebook load does not work. As a heuristic,
180
- // if it appears that the notebook uses polars, add pyarrow.
181
- if (code.includes("polars")) {
182
- code = `import pyarrow\n${code}`;
183
- }
202
+ /** Deliver a notebook operation to the active session's host. */
203
+ notebookCall: async (opts: {
204
+ appId: string;
205
+ sessionGeneration: number;
206
+ call: unstable_NotebookCall;
207
+ }): Promise<unknown> => {
208
+ await pyodideReadyPromise;
209
+ return enqueueSession(async () => {
210
+ const { notebook } = requireActiveBridge(opts);
211
+ try {
212
+ return JSON.parse(await notebook.call(JSON.stringify(opts.call)));
213
+ } finally {
214
+ notebook.destroy();
184
215
  }
185
-
186
- await self.pyodide.loadPackagesFromImports(code, {
187
- messageCallback: Logger.log,
188
- errorCallback: Logger.error,
189
- });
190
216
  });
191
217
  },
192
218
 
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
- import { cellId, requestId, uiElementId } from "@/__tests__/branded";
4
+ import { requestId, uiElementId } from "@/__tests__/branded";
5
5
  import { waitForRuntimeRunning } from "../../executions";
6
6
  import type { RuntimeManager } from "../../runtime/runtime";
7
7
  import { waitForConnectionOpen } from "../connection";
@@ -49,7 +49,6 @@ describe("createLazyRequests", () => {
49
49
 
50
50
  // Mock delegate with some sample methods
51
51
  mockDelegate = {
52
- sendRun: vi.fn().mockResolvedValue({ success: true }),
53
52
  sendInstantiate: vi.fn().mockResolvedValue({ instantiated: true }),
54
53
  sendFunctionRequest: vi.fn().mockResolvedValue({ result: "data" }),
55
54
  sendRestart: vi.fn().mockResolvedValue({ restarted: true }),
@@ -149,7 +148,7 @@ describe("createLazyRequests", () => {
149
148
  mockGetRuntimeManager,
150
149
  );
151
150
 
152
- await lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] });
151
+ await lazyRequests.sendRunScratchpad({ code: "code" });
153
152
 
154
153
  expect(mockInit).toHaveBeenCalledTimes(1);
155
154
  });
@@ -175,7 +174,7 @@ describe("createLazyRequests", () => {
175
174
  mockGetRuntimeManager,
176
175
  );
177
176
 
178
- await lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] });
177
+ await lazyRequests.sendRunScratchpad({ code: "code" });
179
178
  await lazyRequests.sendInstantiate({
180
179
  objectIds: [uiElementId("obj1")],
181
180
  values: [],
@@ -197,7 +196,7 @@ describe("createLazyRequests", () => {
197
196
  mockGetRuntimeManager,
198
197
  );
199
198
 
200
- await lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] });
199
+ await lazyRequests.sendRunScratchpad({ code: "code" });
201
200
 
202
201
  expect(waitForConnectionOpen).toHaveBeenCalled();
203
202
  });
@@ -208,10 +207,10 @@ describe("createLazyRequests", () => {
208
207
  mockGetRuntimeManager,
209
208
  );
210
209
 
211
- const args = { cellIds: [cellId("cell1")], codes: ["code"] };
212
- await lazyRequests.sendRun(args);
210
+ const args = { code: "code" };
211
+ await lazyRequests.sendRunScratchpad(args);
213
212
 
214
- expect(mockDelegate.sendRun).toHaveBeenCalledWith(args);
213
+ expect(mockDelegate.sendRunScratchpad).toHaveBeenCalledWith(args);
215
214
  });
216
215
 
217
216
  it("should return the result from delegate method", async () => {
@@ -236,7 +235,7 @@ describe("createLazyRequests", () => {
236
235
  mockGetRuntimeManager,
237
236
  );
238
237
 
239
- expect(lazyRequests.sendRun).toBeDefined();
238
+ expect(lazyRequests.sendRunScratchpad).toBeDefined();
240
239
  expect(lazyRequests.sendInstantiate).toBeDefined();
241
240
  expect(lazyRequests.sendFunctionRequest).toBeDefined();
242
241
  });
@@ -251,13 +250,13 @@ describe("createLazyRequests", () => {
251
250
  );
252
251
 
253
252
  await expect(
254
- lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] }),
253
+ lazyRequests.sendRunScratchpad({ code: "code" }),
255
254
  ).rejects.toThrow("Init failed");
256
255
  });
257
256
 
258
257
  it("should handle errors from delegate method", async () => {
259
258
  const error = new Error("Request failed");
260
- mockDelegate.sendRun = vi.fn().mockRejectedValue(error);
259
+ mockDelegate.sendRunScratchpad = vi.fn().mockRejectedValue(error);
261
260
 
262
261
  const lazyRequests = createLazyRequests(
263
262
  mockDelegate,
@@ -265,7 +264,7 @@ describe("createLazyRequests", () => {
265
264
  );
266
265
 
267
266
  await expect(
268
- lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] }),
267
+ lazyRequests.sendRunScratchpad({ code: "code" }),
269
268
  ).rejects.toThrow("Request failed");
270
269
  });
271
270
 
@@ -277,10 +276,7 @@ describe("createLazyRequests", () => {
277
276
  );
278
277
 
279
278
  // First request with first runtime manager
280
- await lazyRequests.sendRun({
281
- cellIds: [cellId("cell1")],
282
- codes: ["code"],
283
- });
279
+ await lazyRequests.sendRunScratchpad({ code: "code" });
284
280
  expect(mockInit).toHaveBeenCalledTimes(1);
285
281
 
286
282
  // Create a new runtime manager
@@ -298,10 +294,7 @@ describe("createLazyRequests", () => {
298
294
  );
299
295
 
300
296
  // Second request with second runtime manager
301
- await lazyRequests2.sendRun({
302
- cellIds: [cellId("cell2")],
303
- codes: ["code2"],
304
- });
297
+ await lazyRequests2.sendRunScratchpad({ code: "code" });
305
298
 
306
299
  // Both inits should have been called
307
300
  expect(mockInit).toHaveBeenCalledTimes(1);
@@ -315,10 +308,7 @@ describe("createLazyRequests", () => {
315
308
  );
316
309
 
317
310
  // Multiple requests
318
- await lazyRequests.sendRun({
319
- cellIds: [cellId("cell1")],
320
- codes: ["code"],
321
- });
311
+ await lazyRequests.sendRunScratchpad({ code: "code" });
322
312
  await lazyRequests.sendInstantiate({
323
313
  objectIds: [uiElementId("obj1")],
324
314
  values: [],
@@ -14,7 +14,7 @@ import type { EditRequests, RunRequests } from "../types";
14
14
 
15
15
  function requestsThatReject(error: unknown): EditRequests & RunRequests {
16
16
  return createErrorToastingRequests({
17
- sendRun: vi.fn().mockRejectedValue(error),
17
+ sendRunScratchpad: vi.fn().mockRejectedValue(error),
18
18
  } as unknown as EditRequests & RunRequests);
19
19
  }
20
20
 
@@ -29,17 +29,17 @@ describe("createErrorToastingRequests", () => {
29
29
  detail: "This connection is read-only for this action.",
30
30
  }),
31
31
  );
32
- await expect(requests.sendRun({} as never)).rejects.toBeInstanceOf(
33
- HTTPError,
34
- );
32
+ await expect(
33
+ requests.sendRunScratchpad({} as never),
34
+ ).rejects.toBeInstanceOf(HTTPError);
35
35
  expect(toastMock).not.toHaveBeenCalled();
36
36
  });
37
37
 
38
38
  it("shows a danger toast for other request errors", async () => {
39
39
  const requests = requestsThatReject(new HTTPError(500, "Server Error"));
40
- await expect(requests.sendRun({} as never)).rejects.toBeInstanceOf(
41
- HTTPError,
42
- );
40
+ await expect(
41
+ requests.sendRunScratchpad({} as never),
42
+ ).rejects.toBeInstanceOf(HTTPError);
43
43
  expect(toastMock).toHaveBeenCalledWith(
44
44
  expect.objectContaining({ variant: "danger" }),
45
45
  );