@marimo-team/islands 0.23.15-dev32 → 0.23.15-dev4

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 (84) hide show
  1. package/dist/{ConnectedDataExplorerComponent-7p7rt9iw.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-DE6tzZf-.js} +1 -1
  3. package/dist/{ImageComparisonComponent-1i5IBhon.js → ImageComparisonComponent-CHrI72em.js} +129 -173
  4. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  5. package/dist/assets/worker-DAWRHcPq.js +73 -0
  6. package/dist/{chat-ui-C0wkSwxB.js → chat-ui-DinOvbWu.js} +3111 -3111
  7. package/dist/{code-visibility-CEHWVGz1.js → code-visibility-Dk8cVOny.js} +864 -946
  8. package/dist/{debounce-BOD3DbfP.js → constants-T20xxyNf.js} +24 -1
  9. package/dist/{formats-BX3uDQbB.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-CjTu7ukN.js} +3 -3
  11. package/dist/{html-to-image-D8Q5SLSJ.js → html-to-image-_wGfk8V-.js} +2241 -2277
  12. package/dist/{input-BGPrFH3g.js → input-DtsN7xm-.js} +2 -2
  13. package/dist/main.js +1201 -1297
  14. package/dist/{mermaid-UdmxG2PZ.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{process-output-C_A12w9J.js → process-output-Mh4UrjwM.js} +1 -1
  16. package/dist/{reveal-component-kLDhMoa2.js → reveal-component-B5DXLyO7.js} +605 -618
  17. package/dist/{spec-DSs9v0xx.js → spec-Cz-Bj1JI.js} +1 -1
  18. package/dist/style.css +1 -1
  19. package/dist/{toDate-CKpRx4TS.js → toDate-CWNNlFEX.js} +4 -3
  20. package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-KfHB8wQR.js} +1 -1
  21. package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  22. package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DegSo0lV.js} +1 -1
  23. package/dist/{useTheme-Df_vGflw.js → useTheme-6eZ3GOTS.js} +27 -62
  24. package/dist/{vega-component-DNHEV0u0.js → vega-component-DzyyM9fc.js} +6 -6
  25. package/package.json +1 -1
  26. package/src/components/chat/__tests__/chat-utils.test.ts +1 -244
  27. package/src/components/chat/chat-panel.tsx +67 -196
  28. package/src/components/chat/chat-utils.ts +2 -111
  29. package/src/components/editor/SortableCell.tsx +2 -6
  30. package/src/components/editor/actions/pair-with-agent-modal.tsx +64 -20
  31. package/src/components/editor/ai/add-cell-with-ai.tsx +5 -14
  32. package/src/components/editor/ai/ai-completion-editor.tsx +1 -4
  33. package/src/components/editor/cell/cell-context-menu.tsx +0 -7
  34. package/src/components/editor/chrome/panels/packages-panel.tsx +1 -11
  35. package/src/components/editor/columns/cell-column.tsx +4 -34
  36. package/src/components/editor/columns/sortable-column.tsx +4 -24
  37. package/src/components/editor/navigation/__tests__/navigation.test.ts +0 -33
  38. package/src/components/editor/navigation/navigation.ts +1 -8
  39. package/src/components/editor/notebook-cell.tsx +106 -203
  40. package/src/components/editor/renderers/cell-array.tsx +13 -26
  41. package/src/components/editor/renderers/cells-renderer.tsx +2 -8
  42. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -29
  43. package/src/components/editor/renderers/slides-layout/types.ts +0 -4
  44. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
  45. package/src/components/slides/reveal-component.tsx +4 -37
  46. package/src/components/slides/slide-form.tsx +0 -72
  47. package/src/core/cells/__tests__/utils.test.ts +0 -59
  48. package/src/core/cells/utils.ts +0 -51
  49. package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +3 -97
  50. package/src/core/codemirror/completion/signature-hint.ts +11 -78
  51. package/src/core/constants.ts +0 -6
  52. package/src/core/islands/__tests__/bridge.test.ts +46 -341
  53. package/src/core/islands/__tests__/parse.test.ts +0 -105
  54. package/src/core/islands/bootstrap.ts +3 -8
  55. package/src/core/islands/bridge.ts +23 -116
  56. package/src/core/islands/components/web-components.tsx +15 -76
  57. package/src/core/islands/constants.ts +0 -1
  58. package/src/core/islands/main.ts +3 -35
  59. package/src/core/islands/parse.ts +23 -70
  60. package/src/core/islands/worker/worker.tsx +57 -145
  61. package/src/core/wasm/worker/bootstrap.ts +20 -113
  62. package/src/css/app/Cell.css +0 -10
  63. package/src/hooks/useHotkey.ts +4 -29
  64. package/src/plugins/impl/anywidget/__tests__/host.test.ts +9 -6
  65. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +61 -22
  66. package/src/plugins/impl/anywidget/model-proxy.ts +13 -0
  67. package/src/plugins/impl/anywidget/widget-binding.ts +34 -4
  68. package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +2 -53
  69. package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +3 -71
  70. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +0 -1
  71. package/src/theme/useTheme.ts +1 -16
  72. package/dist/assets/__vite-browser-external-CjNAy01L.js +0 -1
  73. package/dist/assets/worker-B3XPCb6y.js +0 -98
  74. package/src/components/chat/__tests__/message-queue.test.tsx +0 -121
  75. package/src/components/editor/__tests__/output-persistence.test.tsx +0 -241
  76. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +0 -153
  77. package/src/components/editor/actions/pair-with-agent-commands.ts +0 -109
  78. package/src/components/editor/columns/__tests__/cell-column.test.tsx +0 -105
  79. package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +0 -66
  80. package/src/core/islands/components/__tests__/web-components.test.tsx +0 -214
  81. package/src/core/islands/worker/__tests__/controller.test.ts +0 -173
  82. package/src/hooks/__tests__/useHotkey.test.tsx +0 -88
  83. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +0 -71
  84. 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({ embeds, materialize, payloads });
94
+ return parsePayloadBackedApps(embeds, payloads);
100
95
  }
101
96
 
102
- return parseIslandElementsIntoApps(embeds, materialize);
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
- if (materialize) {
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
- materialize,
161
- payloads,
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
- if (materialize) {
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 (materialize && cell.reactive) {
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
- if (materialize) {
243
- for (const embed of embeds) {
244
- const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
245
- if (appId && payloadAppIds.has(appId) && !consumedEmbeds.has(embed)) {
246
- embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
247
- embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
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
- if (materialize) {
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
- (embed.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID) ??
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 && code) {
362
- return {
363
- output: cellOutput.innerHTML,
364
- code: code,
365
- };
315
+ if (!cellOutput || !code) {
316
+ return null;
366
317
  }
367
318
 
368
- const retained = retainedIslandSources.get(embed);
369
- return retained?.code ? retained : null;
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
- interface SessionRequest {
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: SessionRequest) => {
123
- await enqueueSession(() => startSession(opts, false));
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
- stopSession: async (opts: { appId: string; sessionGeneration: number }) => {
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 stopActiveSession();
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 (opts: {
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
- if (shouldLoadDuckDBPackages(code)) {
158
- // Add pandas and duckdb to the code for mo.sql and for remote duckdb sources
159
- code = `import pandas\n${code}`;
160
- code = `import duckdb\n${code}`;
161
- code = `import sqlglot\n${code}`;
162
-
163
- // Polars + SQL requires pyarrow, and installing
164
- // after notebook load does not work. As a heuristic,
165
- // if it appears that the notebook uses polars, add pyarrow.
166
- if (code.includes("polars")) {
167
- code = `import pyarrow\n${code}`;
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
- await self.pyodide.loadPackagesFromImports(code, {
172
- messageCallback: Logger.log,
173
- errorCallback: Logger.error,
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
- return enqueueSession(async () => {
193
- const bridge = requireActiveBridge(opts);
194
-
195
- // Serialize the payload
196
- const payloadString =
197
- payload == null
198
- ? null
199
- : typeof payload === "string"
200
- ? payload
201
- : JSON.stringify(payload);
202
-
203
- // Make the request
204
- const response =
205
- payloadString == null
206
- ? // @ts-expect-error ehh TypeScript
207
- await bridge[functionName]()
208
- : // @ts-expect-error ehh TypeScript
209
- await bridge[functionName](payloadString);
210
-
211
- // Post the response back to the main thread
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 sessionResources = this.requirePyodide.runPython(
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
- def create_session_resources():
152
- session, bridge = create_session(
153
- filename="${nbFilename}",
154
- query_params=js.query_params.to_py(),
155
- message_callback=js.messenger.callback,
156
- user_config=js.user_config.to_py(),
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
- def init(auto_instantiate=True):
161
- nonlocal session_task
162
- instantiate(session, auto_instantiate)
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
- async def stop():
166
- nonlocal bridge, session, session_task
167
- task = session_task
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
- with open("${nbFilename}", "r") as f:
186
- packages = session.find_packages(f.read())
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
- let bridgeProxy!: PyProxy;
194
- let initSession!: PyCallable;
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
- const dependenciesReady = this.packageLoadQueue.then(() => {
222
- if (sessionGeneration !== this.sessionGeneration) {
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
- async stopSession(): Promise<void> {
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>) {
@@ -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 ----------------------------- */