@marimo-team/islands 0.23.15-dev33 → 0.23.15-dev5
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-7p7rt9iw.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-DE6tzZf-.js} +1 -1
- package/dist/{ImageComparisonComponent-1i5IBhon.js → ImageComparisonComponent-CHrI72em.js} +129 -173
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/worker-DAWRHcPq.js +73 -0
- package/dist/{chat-ui-C0wkSwxB.js → chat-ui-DinOvbWu.js} +3111 -3111
- package/dist/{code-visibility-BI94mK4u.js → code-visibility-DBuB5MVY.js} +864 -946
- package/dist/{debounce-BOD3DbfP.js → constants-T20xxyNf.js} +24 -1
- package/dist/{formats-BX3uDQbB.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-CjTu7ukN.js} +3 -3
- package/dist/{html-to-image-D8Q5SLSJ.js → html-to-image-_wGfk8V-.js} +2241 -2277
- package/dist/{input-BGPrFH3g.js → input-DtsN7xm-.js} +2 -2
- package/dist/main.js +1201 -1297
- package/dist/{mermaid-UdmxG2PZ.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{process-output-C_A12w9J.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-ELidGmPF.js → reveal-component-DfOZrkd6.js} +605 -618
- package/dist/{spec-DSs9v0xx.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CKpRx4TS.js → toDate-CWNNlFEX.js} +4 -3
- package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-Df_vGflw.js → useTheme-6eZ3GOTS.js} +27 -62
- package/dist/{vega-component-DNHEV0u0.js → vega-component-DzyyM9fc.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +1 -244
- package/src/components/chat/chat-panel.tsx +67 -196
- package/src/components/chat/chat-utils.ts +2 -111
- package/src/components/editor/SortableCell.tsx +2 -6
- package/src/components/editor/actions/pair-with-agent-modal.tsx +64 -20
- package/src/components/editor/ai/add-cell-with-ai.tsx +5 -14
- package/src/components/editor/ai/ai-completion-editor.tsx +1 -4
- package/src/components/editor/cell/cell-context-menu.tsx +0 -7
- package/src/components/editor/chrome/panels/packages-panel.tsx +1 -11
- package/src/components/editor/columns/cell-column.tsx +4 -34
- package/src/components/editor/columns/sortable-column.tsx +4 -24
- package/src/components/editor/navigation/__tests__/navigation.test.ts +0 -33
- package/src/components/editor/navigation/navigation.ts +1 -8
- package/src/components/editor/notebook-cell.tsx +106 -203
- package/src/components/editor/renderers/cell-array.tsx +13 -26
- package/src/components/editor/renderers/cells-renderer.tsx +2 -8
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -29
- package/src/components/editor/renderers/slides-layout/types.ts +0 -4
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +4 -37
- package/src/components/slides/slide-form.tsx +0 -72
- package/src/core/cells/__tests__/utils.test.ts +0 -59
- package/src/core/cells/utils.ts +0 -51
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +3 -97
- package/src/core/codemirror/completion/signature-hint.ts +11 -78
- package/src/core/constants.ts +0 -6
- package/src/core/islands/__tests__/bridge.test.ts +46 -341
- package/src/core/islands/__tests__/parse.test.ts +0 -105
- package/src/core/islands/bootstrap.ts +3 -8
- package/src/core/islands/bridge.ts +23 -116
- package/src/core/islands/components/web-components.tsx +15 -76
- package/src/core/islands/constants.ts +0 -1
- package/src/core/islands/main.ts +3 -69
- package/src/core/islands/parse.ts +23 -70
- package/src/core/islands/worker/worker.tsx +57 -145
- package/src/core/wasm/worker/bootstrap.ts +20 -113
- package/src/css/app/Cell.css +0 -10
- package/src/hooks/useHotkey.ts +4 -29
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +9 -6
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +61 -22
- package/src/plugins/impl/anywidget/model-proxy.ts +13 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +34 -4
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +2 -53
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +3 -71
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +0 -1
- package/src/theme/useTheme.ts +1 -16
- package/dist/assets/__vite-browser-external-CjNAy01L.js +0 -1
- package/dist/assets/worker-B3XPCb6y.js +0 -98
- package/src/components/chat/__tests__/message-queue.test.tsx +0 -121
- package/src/components/editor/__tests__/output-persistence.test.tsx +0 -241
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +0 -153
- package/src/components/editor/actions/pair-with-agent-commands.ts +0 -109
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +0 -105
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +0 -66
- package/src/core/islands/__tests__/main.test.ts +0 -176
- package/src/core/islands/components/__tests__/web-components.test.tsx +0 -214
- package/src/core/islands/worker/__tests__/controller.test.ts +0 -173
- package/src/hooks/__tests__/useHotkey.test.tsx +0 -88
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +0 -71
- package/src/theme/__tests__/useTheme.test.ts +0 -68
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ISLAND_DATA_ATTRIBUTES,
|
|
5
|
-
ISLAND_SOURCE_CHANGED_EVENT,
|
|
6
5
|
ISLAND_TAG_NAMES,
|
|
7
6
|
ISLANDS_JSON_SCRIPT_TYPE,
|
|
8
7
|
} from "@/core/islands/constants";
|
|
@@ -75,15 +74,12 @@ interface MarimoIslandPayloadCell {
|
|
|
75
74
|
displayOutput: boolean;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
const retainedPayloadCellIds = new WeakMap<HTMLElement, string>();
|
|
79
|
-
|
|
80
77
|
/**
|
|
81
78
|
* Parses marimo island apps from the DOM
|
|
82
79
|
* @param root - Root element to search within (defaults to document)
|
|
83
80
|
*/
|
|
84
81
|
export function parseMarimoIslandApps(
|
|
85
82
|
root: Document | Element = document,
|
|
86
|
-
options: { materialize?: boolean } = {},
|
|
87
83
|
): MarimoIslandApp[] {
|
|
88
84
|
const embeds = [
|
|
89
85
|
...root.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND),
|
|
@@ -94,12 +90,11 @@ export function parseMarimoIslandApps(
|
|
|
94
90
|
return [];
|
|
95
91
|
}
|
|
96
92
|
|
|
97
|
-
const materialize = options.materialize ?? true;
|
|
98
93
|
if (payloads.length > 0) {
|
|
99
|
-
return parsePayloadBackedApps(
|
|
94
|
+
return parsePayloadBackedApps(embeds, payloads);
|
|
100
95
|
}
|
|
101
96
|
|
|
102
|
-
return parseIslandElementsIntoApps(embeds
|
|
97
|
+
return parseIslandElementsIntoApps(embeds);
|
|
103
98
|
}
|
|
104
99
|
|
|
105
100
|
/**
|
|
@@ -108,7 +103,6 @@ export function parseMarimoIslandApps(
|
|
|
108
103
|
*/
|
|
109
104
|
export function parseIslandElementsIntoApps(
|
|
110
105
|
embeds: HTMLElement[],
|
|
111
|
-
materialize = true,
|
|
112
106
|
): MarimoIslandApp[] {
|
|
113
107
|
const apps = new Map<string, MarimoIslandApp>();
|
|
114
108
|
|
|
@@ -146,24 +140,16 @@ export function parseIslandElementsIntoApps(
|
|
|
146
140
|
});
|
|
147
141
|
|
|
148
142
|
// Add data-cell-idx attribute to the island element
|
|
149
|
-
|
|
150
|
-
embed.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
|
|
151
|
-
embed.dispatchEvent(new Event(ISLAND_SOURCE_CHANGED_EVENT));
|
|
152
|
-
}
|
|
143
|
+
embed.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
|
|
153
144
|
}
|
|
154
145
|
|
|
155
146
|
return [...apps.values()];
|
|
156
147
|
}
|
|
157
148
|
|
|
158
|
-
function parsePayloadBackedApps(
|
|
159
|
-
embeds,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}: {
|
|
163
|
-
embeds: HTMLElement[];
|
|
164
|
-
materialize: boolean;
|
|
165
|
-
payloads: MarimoIslandPayload[];
|
|
166
|
-
}): MarimoIslandApp[] {
|
|
149
|
+
function parsePayloadBackedApps(
|
|
150
|
+
embeds: HTMLElement[],
|
|
151
|
+
payloads: MarimoIslandPayload[],
|
|
152
|
+
): MarimoIslandApp[] {
|
|
167
153
|
const apps = new Map<string, MarimoIslandApp>();
|
|
168
154
|
const matchedPayloadCells = new Map<MarimoIslandPayloadCell, HTMLElement>();
|
|
169
155
|
const consumedEmbeds = new Set<HTMLElement>();
|
|
@@ -183,9 +169,7 @@ function parsePayloadBackedApps({
|
|
|
183
169
|
}
|
|
184
170
|
consumedEmbeds.add(embed);
|
|
185
171
|
matchedPayloadCells.set(cell, embed);
|
|
186
|
-
|
|
187
|
-
materializeIslandPayload(embed, cell);
|
|
188
|
-
}
|
|
172
|
+
materializeIslandPayload(embed, cell);
|
|
189
173
|
hasMatchedIsland = true;
|
|
190
174
|
}
|
|
191
175
|
// Only payloads matched to island anchors can start runtime apps.
|
|
@@ -231,7 +215,7 @@ function parsePayloadBackedApps({
|
|
|
231
215
|
appCell.disabled = true;
|
|
232
216
|
}
|
|
233
217
|
app.cells.push(appCell);
|
|
234
|
-
if (
|
|
218
|
+
if (cell.reactive) {
|
|
235
219
|
embed?.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
|
|
236
220
|
}
|
|
237
221
|
}
|
|
@@ -239,15 +223,12 @@ function parsePayloadBackedApps({
|
|
|
239
223
|
|
|
240
224
|
// A supported payload is the runtime source for its app. Extra same-app DOM
|
|
241
225
|
// islands are disconnected from runtime binding.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
embed.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "false");
|
|
249
|
-
consumedEmbeds.add(embed);
|
|
250
|
-
}
|
|
226
|
+
for (const embed of embeds) {
|
|
227
|
+
const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
|
|
228
|
+
if (appId && payloadAppIds.has(appId) && !consumedEmbeds.has(embed)) {
|
|
229
|
+
embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
|
|
230
|
+
embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
|
|
231
|
+
embed.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "false");
|
|
251
232
|
}
|
|
252
233
|
}
|
|
253
234
|
|
|
@@ -256,16 +237,7 @@ function parsePayloadBackedApps({
|
|
|
256
237
|
return !appId || !payloadAppIds.has(appId);
|
|
257
238
|
});
|
|
258
239
|
|
|
259
|
-
|
|
260
|
-
for (const embed of consumedEmbeds) {
|
|
261
|
-
embed.dispatchEvent(new Event(ISLAND_SOURCE_CHANGED_EVENT));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return [
|
|
266
|
-
...apps.values(),
|
|
267
|
-
...parseIslandElementsIntoApps(domOnlyEmbeds, materialize),
|
|
268
|
-
];
|
|
240
|
+
return [...apps.values(), ...parseIslandElementsIntoApps(domOnlyEmbeds)];
|
|
269
241
|
}
|
|
270
242
|
|
|
271
243
|
function findMatchingIsland({
|
|
@@ -285,8 +257,7 @@ function findMatchingIsland({
|
|
|
285
257
|
}
|
|
286
258
|
return (
|
|
287
259
|
embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID) === appId &&
|
|
288
|
-
|
|
289
|
-
retainedPayloadCellIds.get(embed)) === cell.cellId
|
|
260
|
+
embed.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID) === cell.cellId
|
|
290
261
|
);
|
|
291
262
|
});
|
|
292
263
|
}
|
|
@@ -295,7 +266,6 @@ function materializeIslandPayload(
|
|
|
295
266
|
embed: HTMLElement,
|
|
296
267
|
cell: MarimoIslandPayloadCell,
|
|
297
268
|
): void {
|
|
298
|
-
retainedPayloadCellIds.set(embed, cell.cellId);
|
|
299
269
|
embed.setAttribute(
|
|
300
270
|
ISLAND_DATA_ATTRIBUTES.REACTIVE,
|
|
301
271
|
JSON.stringify(cell.reactive),
|
|
@@ -334,22 +304,6 @@ function ensureIslandChild(embed: HTMLElement, tagName: string): HTMLElement {
|
|
|
334
304
|
* @param embed - The island HTML element
|
|
335
305
|
* @returns Cell data or null if invalid
|
|
336
306
|
*/
|
|
337
|
-
const retainedIslandSources = new WeakMap<
|
|
338
|
-
HTMLElement,
|
|
339
|
-
{ output: string; code: string }
|
|
340
|
-
>();
|
|
341
|
-
|
|
342
|
-
export function retainIslandSource(
|
|
343
|
-
embed: HTMLElement,
|
|
344
|
-
source: { output: string; code: string },
|
|
345
|
-
): void {
|
|
346
|
-
if (source.code) {
|
|
347
|
-
retainedIslandSources.set(embed, source);
|
|
348
|
-
} else {
|
|
349
|
-
retainedIslandSources.delete(embed);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
307
|
export function parseIslandElement(
|
|
354
308
|
embed: HTMLElement,
|
|
355
309
|
): { output: string; code: string } | null {
|
|
@@ -358,15 +312,14 @@ export function parseIslandElement(
|
|
|
358
312
|
);
|
|
359
313
|
const code = extractIslandCodeFromEmbed(embed);
|
|
360
314
|
|
|
361
|
-
if (cellOutput
|
|
362
|
-
return
|
|
363
|
-
output: cellOutput.innerHTML,
|
|
364
|
-
code: code,
|
|
365
|
-
};
|
|
315
|
+
if (!cellOutput || !code) {
|
|
316
|
+
return null;
|
|
366
317
|
}
|
|
367
318
|
|
|
368
|
-
|
|
369
|
-
|
|
319
|
+
return {
|
|
320
|
+
output: cellOutput.innerHTML,
|
|
321
|
+
code: code,
|
|
322
|
+
};
|
|
370
323
|
}
|
|
371
324
|
|
|
372
325
|
export function createMarimoFile(app: {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { PyodideInterface } from "pyodide";
|
|
4
|
-
import type { PyProxy } from "pyodide/ffi";
|
|
5
4
|
import {
|
|
6
5
|
createRPC,
|
|
7
6
|
createRPCRequestHandler,
|
|
@@ -17,6 +16,7 @@ import { MessageBuffer } from "@/core/wasm/worker/message-buffer";
|
|
|
17
16
|
import type { RawBridge, SerializedBridge } from "@/core/wasm/worker/types";
|
|
18
17
|
import type { JsonString } from "@/utils/json/base64";
|
|
19
18
|
import { Logger } from "@/utils/Logger";
|
|
19
|
+
import { Deferred } from "../../../utils/Deferred";
|
|
20
20
|
import { prettyError } from "../../../utils/errors";
|
|
21
21
|
import { invariant } from "../../../utils/invariant";
|
|
22
22
|
import { ReadonlyWasmController } from "./controller";
|
|
@@ -50,128 +50,59 @@ const messageBuffer = new MessageBuffer(
|
|
|
50
50
|
rpc.send.kernelMessage({ message });
|
|
51
51
|
},
|
|
52
52
|
);
|
|
53
|
-
|
|
54
|
-
code: string;
|
|
55
|
-
appId: string;
|
|
56
|
-
sessionGeneration: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let activeSession:
|
|
60
|
-
| (Omit<SessionRequest, "code"> & { bridge: SerializedBridge })
|
|
61
|
-
| undefined;
|
|
62
|
-
let sessionQueue = Promise.resolve();
|
|
63
|
-
|
|
64
|
-
async function startSession(
|
|
65
|
-
opts: SessionRequest,
|
|
66
|
-
replace: boolean,
|
|
67
|
-
): Promise<void> {
|
|
68
|
-
await pyodideReadyPromise;
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
invariant(self.controller, "Controller not loaded");
|
|
72
|
-
if (replace) {
|
|
73
|
-
await stopActiveSession();
|
|
74
|
-
}
|
|
75
|
-
const notebook = await self.controller.mountFilesystem({
|
|
76
|
-
code: opts.code,
|
|
77
|
-
filename: `app-${opts.appId}.py`,
|
|
78
|
-
});
|
|
79
|
-
const nextBridge = await self.controller.startSession({
|
|
80
|
-
...notebook,
|
|
81
|
-
onMessage: messageBuffer.push,
|
|
82
|
-
});
|
|
83
|
-
if (activeSession) {
|
|
84
|
-
(nextBridge as unknown as PyProxy).destroy();
|
|
85
|
-
} else {
|
|
86
|
-
activeSession = {
|
|
87
|
-
appId: opts.appId,
|
|
88
|
-
bridge: nextBridge,
|
|
89
|
-
sessionGeneration: opts.sessionGeneration,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
rpc.send.initialized({});
|
|
93
|
-
} catch (error) {
|
|
94
|
-
rpc.send.initializedError({
|
|
95
|
-
error: prettyError(error),
|
|
96
|
-
});
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function stopActiveSession(): Promise<void> {
|
|
102
|
-
const session = activeSession;
|
|
103
|
-
await self.controller.stopSession();
|
|
104
|
-
activeSession = undefined;
|
|
105
|
-
(session?.bridge as unknown as PyProxy | undefined)?.destroy();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function enqueueSession<T>(operation: () => Promise<T>): Promise<T> {
|
|
109
|
-
const result = sessionQueue.then(operation);
|
|
110
|
-
sessionQueue = result.then(
|
|
111
|
-
() => undefined,
|
|
112
|
-
() => undefined,
|
|
113
|
-
);
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
53
|
+
const bridgeReady = new Deferred<SerializedBridge>();
|
|
116
54
|
|
|
117
55
|
// Handle RPC requests
|
|
118
56
|
const requestHandler = createRPCRequestHandler({
|
|
119
57
|
/**
|
|
120
58
|
* Start the session
|
|
121
59
|
*/
|
|
122
|
-
startSession: async (opts:
|
|
123
|
-
await
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
replaceSession: async (opts: SessionRequest) => {
|
|
127
|
-
await enqueueSession(() => startSession(opts, true));
|
|
128
|
-
},
|
|
60
|
+
startSession: async (opts: { code: string; appId: string }) => {
|
|
61
|
+
await pyodideReadyPromise; // Make sure loading is done
|
|
129
62
|
|
|
130
|
-
|
|
131
|
-
await enqueueSession(async () => {
|
|
132
|
-
if (
|
|
133
|
-
activeSession?.appId !== opts.appId ||
|
|
134
|
-
activeSession?.sessionGeneration !== opts.sessionGeneration
|
|
135
|
-
) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
63
|
+
try {
|
|
138
64
|
invariant(self.controller, "Controller not loaded");
|
|
139
|
-
await
|
|
140
|
-
|
|
65
|
+
const notebook = await self.controller.mountFilesystem({
|
|
66
|
+
code: opts.code,
|
|
67
|
+
filename: `app-${opts.appId}.py`,
|
|
68
|
+
});
|
|
69
|
+
const bridge = await self.controller.startSession({
|
|
70
|
+
...notebook,
|
|
71
|
+
onMessage: messageBuffer.push,
|
|
72
|
+
});
|
|
73
|
+
bridgeReady.resolve(bridge);
|
|
74
|
+
rpc.send.initialized({});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
rpc.send.initializedError({
|
|
77
|
+
error: prettyError(error),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
141
81
|
},
|
|
142
82
|
|
|
143
83
|
/**
|
|
144
84
|
* Load packages
|
|
145
85
|
*/
|
|
146
|
-
loadPackages: async (
|
|
147
|
-
appId: string;
|
|
148
|
-
code: string;
|
|
149
|
-
sessionGeneration: number;
|
|
150
|
-
}) => {
|
|
86
|
+
loadPackages: async (code: string) => {
|
|
151
87
|
await pyodideReadyPromise; // Make sure loading is done
|
|
152
|
-
await enqueueSession(async () => {
|
|
153
|
-
requireActiveBridge(opts);
|
|
154
|
-
|
|
155
|
-
let { code } = opts;
|
|
156
88
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
89
|
+
if (shouldLoadDuckDBPackages(code)) {
|
|
90
|
+
// Add pandas and duckdb to the code for mo.sql and for remote duckdb sources
|
|
91
|
+
code = `import pandas\n${code}`;
|
|
92
|
+
code = `import duckdb\n${code}`;
|
|
93
|
+
code = `import sqlglot\n${code}`;
|
|
94
|
+
|
|
95
|
+
// Polars + SQL requires pyarrow, and installing
|
|
96
|
+
// after notebook load does not work. As a heuristic,
|
|
97
|
+
// if it appears that the notebook uses polars, add pyarrow.
|
|
98
|
+
if (code.includes("polars")) {
|
|
99
|
+
code = `import pyarrow\n${code}`;
|
|
169
100
|
}
|
|
101
|
+
}
|
|
170
102
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
});
|
|
103
|
+
await self.pyodide.loadPackagesFromImports(code, {
|
|
104
|
+
messageCallback: Logger.log,
|
|
105
|
+
errorCallback: Logger.error,
|
|
175
106
|
});
|
|
176
107
|
},
|
|
177
108
|
|
|
@@ -179,56 +110,37 @@ const requestHandler = createRPCRequestHandler({
|
|
|
179
110
|
* Call a function on the bridge
|
|
180
111
|
*/
|
|
181
112
|
bridge: async (opts: {
|
|
182
|
-
appId: string;
|
|
183
113
|
functionName: keyof RawBridge;
|
|
184
114
|
payload: {} | undefined | null;
|
|
185
|
-
sessionGeneration: number;
|
|
186
115
|
}) => {
|
|
187
116
|
await pyodideReadyPromise; // Make sure loading is done
|
|
188
117
|
|
|
189
118
|
const { functionName, payload } = opts;
|
|
190
119
|
|
|
191
120
|
// Perform the function call to the Python bridge
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return typeof response === "string" ? JSON.parse(response) : response;
|
|
213
|
-
});
|
|
121
|
+
const bridge = await bridgeReady.promise;
|
|
122
|
+
|
|
123
|
+
// Serialize the payload
|
|
124
|
+
const payloadString =
|
|
125
|
+
payload == null
|
|
126
|
+
? null
|
|
127
|
+
: typeof payload === "string"
|
|
128
|
+
? payload
|
|
129
|
+
: JSON.stringify(payload);
|
|
130
|
+
|
|
131
|
+
// Make the request
|
|
132
|
+
const response =
|
|
133
|
+
payloadString == null
|
|
134
|
+
? // @ts-expect-error ehh TypeScript
|
|
135
|
+
await bridge[functionName]()
|
|
136
|
+
: // @ts-expect-error ehh TypeScript
|
|
137
|
+
await bridge[functionName](payloadString);
|
|
138
|
+
|
|
139
|
+
// Post the response back to the main thread
|
|
140
|
+
return typeof response === "string" ? JSON.parse(response) : response;
|
|
214
141
|
},
|
|
215
142
|
});
|
|
216
143
|
|
|
217
|
-
function requireActiveBridge(opts: {
|
|
218
|
-
appId: string;
|
|
219
|
-
sessionGeneration: number;
|
|
220
|
-
}): SerializedBridge {
|
|
221
|
-
const session = activeSession;
|
|
222
|
-
if (
|
|
223
|
-
!session ||
|
|
224
|
-
opts.appId !== session.appId ||
|
|
225
|
-
opts.sessionGeneration !== session.sessionGeneration
|
|
226
|
-
) {
|
|
227
|
-
throw new Error("The marimo app session is no longer active.");
|
|
228
|
-
}
|
|
229
|
-
return session.bridge;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
144
|
// create the iframe's schema
|
|
233
145
|
export type WorkerSchema = RPCSchema<
|
|
234
146
|
{
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { loadPyodide, type PyodideInterface } from "pyodide";
|
|
3
|
-
import type { PyCallable, PyProxy } from "pyodide/ffi";
|
|
4
3
|
import type { UserConfig } from "@/core/config/config-schema";
|
|
5
4
|
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
6
5
|
import type { JsonString } from "@/utils/json/base64";
|
|
@@ -13,12 +12,6 @@ import type { SerializedBridge, WasmController } from "./types";
|
|
|
13
12
|
import { shouldLoadDuckDBPackages } from "../utils";
|
|
14
13
|
|
|
15
14
|
const MAKE_SNAPSHOT = false;
|
|
16
|
-
type SessionResources = [
|
|
17
|
-
bridge: PyProxy,
|
|
18
|
-
init: PyCallable,
|
|
19
|
-
packages: PyProxy,
|
|
20
|
-
stop: PyCallable,
|
|
21
|
-
];
|
|
22
15
|
|
|
23
16
|
// This class initializes the wasm environment
|
|
24
17
|
// We would like this initialization to be parallelizable
|
|
@@ -32,9 +25,6 @@ type SessionResources = [
|
|
|
32
25
|
|
|
33
26
|
export class DefaultWasmController implements WasmController {
|
|
34
27
|
protected pyodide: PyodideInterface | null = null;
|
|
35
|
-
private packageLoadQueue = Promise.resolve();
|
|
36
|
-
private sessionGeneration = 0;
|
|
37
|
-
private activeSessionStops = new Set<PyCallable>();
|
|
38
28
|
|
|
39
29
|
get requirePyodide() {
|
|
40
30
|
invariant(this.pyodide, "Pyodide not loaded");
|
|
@@ -121,7 +111,6 @@ export class DefaultWasmController implements WasmController {
|
|
|
121
111
|
onMessage: (message: JsonString<NotificationPayload>) => void;
|
|
122
112
|
userConfig: UserConfig;
|
|
123
113
|
}): Promise<SerializedBridge> {
|
|
124
|
-
const sessionGeneration = this.sessionGeneration;
|
|
125
114
|
const { code, filename, onMessage, queryParameters, userConfig } = opts;
|
|
126
115
|
// We pass down a messenger object to the code
|
|
127
116
|
// This is used to have synchronous communication between the JS and Python code
|
|
@@ -137,129 +126,47 @@ export class DefaultWasmController implements WasmController {
|
|
|
137
126
|
|
|
138
127
|
const span = t.startSpan("startSession.runPython");
|
|
139
128
|
const nbFilename = filename || WasmFileSystem.NOTEBOOK_FILENAME;
|
|
140
|
-
const
|
|
129
|
+
const [bridge, init, packages] = this.requirePyodide.runPython(
|
|
141
130
|
`
|
|
142
131
|
print("[py] Starting marimo...")
|
|
143
132
|
import asyncio
|
|
144
|
-
import gc
|
|
145
133
|
import js
|
|
146
134
|
from marimo._pyodide.bootstrap import create_session, instantiate
|
|
147
135
|
|
|
148
136
|
assert js.messenger, "messenger is not defined"
|
|
149
137
|
assert js.query_params, "query_params is not defined"
|
|
150
138
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
session_task = None
|
|
139
|
+
session, bridge = create_session(
|
|
140
|
+
filename="${nbFilename}",
|
|
141
|
+
query_params=js.query_params.to_py(),
|
|
142
|
+
message_callback=js.messenger.callback,
|
|
143
|
+
user_config=js.user_config.to_py(),
|
|
144
|
+
)
|
|
159
145
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
session_task = asyncio.create_task(session.start())
|
|
146
|
+
def init(auto_instantiate=True):
|
|
147
|
+
instantiate(session, auto_instantiate)
|
|
148
|
+
asyncio.create_task(session.start())
|
|
164
149
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
kernel_task = getattr(session, "kernel_task", None)
|
|
169
|
-
if kernel_task is None:
|
|
170
|
-
if task is not None:
|
|
171
|
-
task.cancel()
|
|
172
|
-
else:
|
|
173
|
-
kernel_task.stop()
|
|
174
|
-
if task is not None:
|
|
175
|
-
await asyncio.gather(task, return_exceptions=True)
|
|
176
|
-
if bridge is not None:
|
|
177
|
-
bridge.session = None
|
|
178
|
-
task = None
|
|
179
|
-
kernel_task = None
|
|
180
|
-
session_task = None
|
|
181
|
-
session = None
|
|
182
|
-
bridge = None
|
|
183
|
-
gc.collect()
|
|
150
|
+
# Find the packages to install
|
|
151
|
+
with open("${nbFilename}", "r") as f:
|
|
152
|
+
packages = session.find_packages(f.read())
|
|
184
153
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return bridge, init, packages, stop
|
|
189
|
-
|
|
190
|
-
create_session_resources()`,
|
|
191
|
-
) as PyProxy;
|
|
154
|
+
bridge, init, packages`,
|
|
155
|
+
);
|
|
192
156
|
span.end();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let packagesProxy!: PyProxy;
|
|
196
|
-
let stopSession!: PyCallable;
|
|
197
|
-
try {
|
|
198
|
-
[bridgeProxy, initSession, packagesProxy, stopSession] =
|
|
199
|
-
sessionResources as unknown as SessionResources;
|
|
200
|
-
} finally {
|
|
201
|
-
sessionResources.destroy();
|
|
202
|
-
}
|
|
203
|
-
let foundPackages: Set<string>;
|
|
204
|
-
try {
|
|
205
|
-
foundPackages = new Set<string>(packagesProxy.toJs());
|
|
206
|
-
} catch (error) {
|
|
207
|
-
bridgeProxy.destroy();
|
|
208
|
-
initSession.destroy();
|
|
209
|
-
stopSession.destroy();
|
|
210
|
-
throw error;
|
|
211
|
-
} finally {
|
|
212
|
-
packagesProxy.destroy();
|
|
213
|
-
}
|
|
214
|
-
this.activeSessionStops.add(stopSession);
|
|
157
|
+
|
|
158
|
+
const foundPackages = new Set<string>(packages.toJs());
|
|
215
159
|
|
|
216
160
|
// Fire and forget:
|
|
217
161
|
// Load notebook dependencies and instantiate the session
|
|
218
162
|
// We don't want to wait for this to finish,
|
|
219
163
|
// so we can show the initial code immediately giving
|
|
220
164
|
// a sense of responsiveness.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
return this.loadNotebookDeps(code, foundPackages);
|
|
165
|
+
void this.loadNotebookDeps(code, foundPackages).then(() => {
|
|
166
|
+
return init(userConfig.runtime.auto_instantiate);
|
|
226
167
|
});
|
|
227
|
-
this.packageLoadQueue = dependenciesReady.catch(() => undefined);
|
|
228
|
-
void dependenciesReady
|
|
229
|
-
.then(() => {
|
|
230
|
-
if (sessionGeneration !== this.sessionGeneration) {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
return initSession(userConfig.runtime.auto_instantiate);
|
|
234
|
-
})
|
|
235
|
-
.catch((error: unknown) => {
|
|
236
|
-
Logger.error("Failed to load notebook dependencies", error);
|
|
237
|
-
})
|
|
238
|
-
.finally(() => initSession.destroy());
|
|
239
|
-
|
|
240
|
-
return bridgeProxy as unknown as SerializedBridge;
|
|
241
|
-
}
|
|
242
168
|
|
|
243
|
-
|
|
244
|
-
this.sessionGeneration += 1;
|
|
245
|
-
const stops = [...this.activeSessionStops];
|
|
246
|
-
let hasFailure = false;
|
|
247
|
-
let firstFailure: unknown;
|
|
248
|
-
for (const stop of stops) {
|
|
249
|
-
try {
|
|
250
|
-
await stop();
|
|
251
|
-
this.activeSessionStops.delete(stop);
|
|
252
|
-
stop.destroy();
|
|
253
|
-
} catch (error) {
|
|
254
|
-
if (!hasFailure) {
|
|
255
|
-
hasFailure = true;
|
|
256
|
-
firstFailure = error;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (hasFailure) {
|
|
261
|
-
throw firstFailure;
|
|
262
|
-
}
|
|
169
|
+
return bridge;
|
|
263
170
|
}
|
|
264
171
|
|
|
265
172
|
private async loadNotebookDeps(code: string, foundPackages: Set<string>) {
|
package/src/css/app/Cell.css
CHANGED
|
@@ -255,12 +255,6 @@
|
|
|
255
255
|
border: none;
|
|
256
256
|
box-shadow: none;
|
|
257
257
|
|
|
258
|
-
/* No dividers between sections. Tailwind v4's divide-y is structural
|
|
259
|
-
* (`> :not(:last-child)`), so hidden-but-mounted children (e.g. the tray
|
|
260
|
-
* while presenting) still count as siblings and would otherwise draw a
|
|
261
|
-
* stray rule under the output. */
|
|
262
|
-
@apply divide-y-0;
|
|
263
|
-
|
|
264
258
|
.output-area {
|
|
265
259
|
/* flow-root interferes with margin collapsing, but appears to be unneeded
|
|
266
260
|
* when cell outlines are hidden; clear:both is sufficient.
|
|
@@ -449,10 +443,6 @@
|
|
|
449
443
|
/* Set a fixed gutter width to ensure the hover area is always wide enough */
|
|
450
444
|
/* And make the gutter bigger when the window is super wide */
|
|
451
445
|
--gutter-width: 100px;
|
|
452
|
-
|
|
453
|
-
/* Vertical gap between notebook cells (gap-5). Overridable via custom css;
|
|
454
|
-
* zeroed while presenting, when published cells flow together. */
|
|
455
|
-
--notebook-cell-gap: 1.25rem;
|
|
456
446
|
}
|
|
457
447
|
|
|
458
448
|
/* ------------------------ CodeMirror Editor ----------------------------- */
|