@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.
- package/dist/{ConnectedDataExplorerComponent-DJnSxHJB.js → ConnectedDataExplorerComponent-DieU6L4x.js} +1 -1
- package/dist/{ErrorBoundary-flfhLFje.js → ErrorBoundary-DjoeT4kv.js} +1 -1
- package/dist/{abnfDiagram-VCTEODGH-CR0U5hup.js → abnfDiagram-VCTEODGH-LOVrjbdp.js} +2 -2
- package/dist/{any-language-editor-C5dsTpeB.js → any-language-editor-DMC2JE9_.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-Bc_fbI1Q.js → architectureDiagram-5GKGNRK7-RZsm312h.js} +1 -1
- package/dist/assets/__vite-browser-external-BSp9tJ_k.js +1 -0
- package/dist/assets/worker-BdfPA0g5.js +171 -0
- package/dist/{chat-ui-C4iLRzY0.js → chat-ui-DkuxGEDp.js} +418 -418
- package/dist/{chunk-IMKFNOWR-y9z5oeAY.js → chunk-IMKFNOWR-B6agbLjo.js} +1 -1
- package/dist/{chunk-SHT3W25Y-Tu-V-e_v.js → chunk-SHT3W25Y-CfULafYG.js} +1 -1
- package/dist/{chunk-SVP7TREG-CJFxs5R_.js → chunk-SVP7TREG-DEOpPMJp.js} +1 -1
- package/dist/{chunk-TICWLB2K-zkTP7ya7.js → chunk-TICWLB2K-DaH-gkX5.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-DxJAZI0N.js → classDiagram-ZZMXUADV-C0CyiWQ7.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-TmHo2BpS.js → classDiagram-v2-VYDZK3BY-xfIc9e2Y.js} +2 -2
- package/dist/{code-visibility-BDs_r7jH.js → code-visibility-CduMVVFS.js} +32 -32
- package/dist/{common-CMTsL35V.js → common-CMcEcucH.js} +919 -919
- package/dist/{config-BG2qKzDg.js → config-BVYA1P0C.js} +103 -76
- package/dist/{cynefinDiagram-5FMLGOSQ-Eb40hmhD.js → cynefinDiagram-5FMLGOSQ-DwGYhKu0.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-_paaVwit.js → dagre-GXQ25YYZ-DfMaWJOV.js} +1 -1
- package/dist/{data-grid-overlay-editor-es6V6qf-.js → data-grid-overlay-editor-BYrIVut3.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BtKJm1_4.js → diagram-S7CK7UJ4-Bw_L87GZ.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DQbxhEMd.js → diagram-UQ7AKVKN-nsrs3Fbz.js} +1 -1
- package/dist/{diagram-VX7I27RA-DITDIfGC.js → diagram-VX7I27RA-7ST9lEqP.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Ct9_zo6F.js → diagram-Z3DM3KII-BnGte-HX.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-6cOntwfv.js → ebnfDiagram-PWID7BFC-zaKyJ1YL.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-CJiOER41.js → erDiagram-RLTQ6QDP-Cx7g95FZ.js} +1 -1
- package/dist/{error-banner-DGOrmBk5.js → error-banner-DkQNU4bn.js} +128 -128
- package/dist/{error-entries-BWmleiL0.js → error-entries-istW7Xbs.js} +589 -544
- package/dist/{feedback-button-D4eVjeNs.js → feedback-button-Zvm26aOQ.js} +107 -106
- package/dist/{flowDiagram-HODETNUW-1lPBEUuW.js → flowDiagram-HODETNUW-C7nIqaHP.js} +2 -2
- package/dist/{formats-qhBmYb0l.js → formats-CMa7L3E8.js} +1 -1
- package/dist/{glide-data-editor-ahfaPdFW.js → glide-data-editor-BE-GgVp4.js} +3 -3
- package/dist/{globals-m8oswz--.js → globals-Dbru-WLP.js} +1 -1
- package/dist/{html-to-image-B8hE6HIH.js → html-to-image-pPbR2cDx.js} +4512 -4464
- package/dist/{infoDiagram-27XIBGKW-C6K4tri5.js → infoDiagram-27XIBGKW-BTM_NXsn.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DwcwgRNH.js → ishikawaDiagram-5VMMS53U-K0FZrlvv.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-E489OMdc.js → kanban-definition-UXKFOSKX-ftznOyrz.js} +1 -1
- package/dist/main.js +6595 -6186
- package/dist/{mermaid-DMPMkVqY.js → mermaid-CNgH43ED.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-DWHjasOp.js → mindmap-definition-YA3MSWOX-CmzbxKfM.js} +1 -1
- package/dist/{number-overlay-editor-C6klKzEr.js → number-overlay-editor-qvlPZ-l-.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-COjcPOZT.js → pegDiagram-XKGWAZYB-DfTAWfGE.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BHAVZ38h.js → pieDiagram-E7YTZNPT-_kGQstZj.js} +1 -1
- package/dist/{process-output-DZDL_j1a.js → process-output-BySO1nIs.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-94ThFyKX.js → railroadDiagram-O6MQD6OU-D41WmIff.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CpF-Fxbb.js → requirementDiagram-BXWQKSXE-Bh53ZI3I.js} +1 -1
- package/dist/{reveal-component-DwnxPyVz.js → reveal-component-DQaegdcW.js} +4 -4
- package/dist/{sequenceDiagram-WJ2MYXX4-CvANK2x7.js → sequenceDiagram-WJ2MYXX4-DBX27dNm.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-WHuJ4mJL.js → stateDiagram-D77RDMKH-CSYz2IZi.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-DbO7lwYq.js → stateDiagram-v2-MP3YSRHH-DVLvtOqn.js} +2 -2
- package/dist/{swimlanes-42K2YHIH-CS2s29Vo.js → swimlanes-42K2YHIH-D59N0ktA.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-CXaA1vQg.js → swimlanesDiagram-VR7AAH4N-CRgpmVSW.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-B-SQMg5h.js → timeline-definition-24CTP7MA-C_xIIZDM.js} +1 -1
- package/dist/toDate-5qPtZ6HL.js +94 -0
- package/dist/{vega-component-DwqGfN3_.js → vega-component-Bly2lDhz.js} +10 -10
- package/dist/{vennDiagram-4TSXK5OY-etEFgvaG.js → vennDiagram-4TSXK5OY-DlOf0Mjs.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-BEQN7g62.js → wardleyDiagram-VM6X3IG4-zD_2guVe.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-D8-e5Z0R.js → xychartDiagram-S5SC5T6Z-CA0Dy1ZM.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/executions.ts +35 -13
- package/src/__mocks__/requests.ts +0 -1
- package/src/components/editor/cell/__tests__/useRunCells.test.tsx +86 -101
- package/src/components/editor/cell/useRunCells.ts +38 -65
- package/src/components/editor/notebook-cell.tsx +32 -20
- package/src/components/editor/output/ansi-reduce.ts +7 -0
- package/src/core/ai/__tests__/staged-cells.test.ts +1 -1
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +2 -0
- package/src/core/cells/__tests__/cells.test.ts +0 -14
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +15 -0
- package/src/core/cells/__tests__/utils.test.ts +1 -1
- package/src/core/cells/add-missing-import.ts +2 -5
- package/src/core/cells/cell.ts +1 -0
- package/src/core/cells/cells.ts +1 -2
- package/src/core/executions/__tests__/atoms.test.ts +227 -43
- package/src/core/executions/__tests__/hooks.test.tsx +1 -2
- package/src/core/executions/__tests__/input.test.ts +86 -0
- package/src/core/executions/__tests__/run.test.ts +390 -0
- package/src/core/executions/__tests__/select.test.ts +105 -30
- package/src/core/executions/atoms.ts +186 -71
- package/src/core/executions/index.ts +7 -6
- package/src/core/executions/input.ts +55 -0
- package/src/core/executions/run.ts +120 -0
- package/src/core/executions/select.ts +59 -24
- package/src/core/islands/__tests__/bridge.test.ts +35 -54
- package/src/core/islands/bootstrap.ts +5 -1
- package/src/core/islands/bridge.ts +16 -28
- package/src/core/islands/components/IslandControls.tsx +15 -7
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +36 -53
- package/src/core/islands/worker/worker.tsx +45 -19
- package/src/core/network/__tests__/requests-lazy.test.ts +14 -24
- package/src/core/network/__tests__/requests-toasting.test.tsx +7 -7
- package/src/core/network/requests-lazy.ts +2 -3
- package/src/core/network/requests-network.ts +0 -9
- package/src/core/network/requests-static.ts +0 -1
- package/src/core/network/requests-toasting.tsx +0 -1
- package/src/core/network/types.ts +0 -2
- package/src/core/notebook/replica/__tests__/fixtures.ts +17 -1
- package/src/core/notebook/replica/backend.ts +3 -1
- package/src/core/runtime/__tests__/runtime.test.ts +36 -0
- package/src/core/runtime/config.ts +36 -1
- package/src/core/runtime/runtime.ts +21 -1
- package/src/core/transports/static.ts +4 -3
- package/src/core/wasm/bridge.ts +0 -17
- package/dist/assets/__vite-browser-external-DzTYIHH_.js +0 -1
- package/dist/assets/worker-BH08I2Il.js +0 -171
- 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("
|
|
752
|
+
describe("notebookBackend.notebook", () => {
|
|
758
753
|
beforeEach(initializeSingleApp);
|
|
759
754
|
|
|
760
|
-
it("
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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.
|
|
770
|
+
const result = await bridge.notebookBackend
|
|
771
|
+
.notebook("n1")
|
|
772
|
+
.run({ cells: [{ id: "c1" }], idempotencyKey: "key-1" });
|
|
798
773
|
|
|
799
|
-
|
|
800
|
-
expect(
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
78
|
-
cellIds: [cellId],
|
|
79
|
-
codes: [codeCallback()],
|
|
80
|
-
});
|
|
86
|
+
store.set(model.editAtom, (tx) => tx.setCode(cellId, code));
|
|
81
87
|
} catch (error) {
|
|
82
|
-
Logger.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 {
|
|
5
|
-
import {
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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("
|
|
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(
|
|
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(
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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 = {
|
|
212
|
-
await lazyRequests.
|
|
210
|
+
const args = { code: "code" };
|
|
211
|
+
await lazyRequests.sendRunScratchpad(args);
|
|
213
212
|
|
|
214
|
-
expect(mockDelegate.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
33
|
-
|
|
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(
|
|
41
|
-
|
|
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
|
);
|