@marimo-team/frontend 0.23.1-dev8 → 0.23.1

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 (69) hide show
  1. package/dist/assets/{JsonOutput-BY31ccA7.js → JsonOutput-CavtrueA.js} +1 -1
  2. package/dist/assets/{MarimoErrorOutput--Yd2Aw0J.js → MarimoErrorOutput-Bmp8DLLo.js} +1 -1
  3. package/dist/assets/RenderHTML-CM3WMmA8.js +1 -0
  4. package/dist/assets/{add-connection-dialog-CjvNOKgb.js → add-connection-dialog-BGZvJkor.js} +1 -1
  5. package/dist/assets/{agent-panel-C24uwabG.js → agent-panel-BvL9Lu9c.js} +1 -1
  6. package/dist/assets/{cell-editor-zW0u82sK.js → cell-editor-B40o_zx_.js} +1 -1
  7. package/dist/assets/{chat-display-DsHMZa9F.js → chat-display-M_nvYuHH.js} +1 -1
  8. package/dist/assets/{chat-panel-o9D3upnX.js → chat-panel-BMOW93uQ.js} +1 -1
  9. package/dist/assets/{chat-ui-BYS03y86.js → chat-ui-DyeimpVh.js} +1 -1
  10. package/dist/assets/{column-preview-Dwv5a_zE.js → column-preview-AfcgbFG_.js} +1 -1
  11. package/dist/assets/{command-palette-BYbKGSF3.js → command-palette-BgvdyU3B.js} +1 -1
  12. package/dist/assets/{documentation-panel-CA2pWMgB.js → documentation-panel-DUPcsi8P.js} +1 -1
  13. package/dist/assets/{edit-page-CMUN3ESy.js → edit-page-DD4uEDmX.js} +4 -4
  14. package/dist/assets/{error-panel-CbqfK1HJ.js → error-panel-DQOeSv5-.js} +1 -1
  15. package/dist/assets/{file-explorer-panel-CbS8z-JR.js → file-explorer-panel-B67zjs2X.js} +1 -1
  16. package/dist/assets/{form-DLyXacSF.js → form-BJ6VFU8l.js} +1 -1
  17. package/dist/assets/{hooks-kZJc1iBf.js → hooks-DvwShzDb.js} +1 -1
  18. package/dist/assets/index-y6osgSWB.js +42 -0
  19. package/dist/assets/{layout-tmN-U1zs.js → layout-erv8pLIP.js} +1 -1
  20. package/dist/assets/{panels-CLfdzLPR.js → panels-1u-RE72f.js} +1 -1
  21. package/dist/assets/{run-page-DPuH6QY4.js → run-page-DfWH_1mz.js} +1 -1
  22. package/dist/assets/{scratchpad-panel-BsMm0GQP.js → scratchpad-panel-CnaiXtoJ.js} +1 -1
  23. package/dist/assets/{session-panel-CTDzGShO.js → session-panel-C68GBFwH.js} +1 -1
  24. package/dist/assets/{snippets-panel-CWof0wHk.js → snippets-panel-BmIdR0lc.js} +1 -1
  25. package/dist/assets/state-D1n-olwf.js +3 -0
  26. package/dist/assets/{useNotebookActions-DHBEqrc_.js → useNotebookActions-Ch1o32Jw.js} +1 -1
  27. package/dist/index.html +7 -7
  28. package/package.json +4 -4
  29. package/src/core/islands/__tests__/bridge.test.ts +2 -12
  30. package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
  31. package/src/core/islands/__tests__/parse.test.ts +466 -24
  32. package/src/core/islands/__tests__/test-utils.tsx +263 -0
  33. package/src/core/islands/bootstrap.ts +265 -0
  34. package/src/core/islands/bridge.ts +154 -75
  35. package/src/core/islands/components/IslandControls.tsx +103 -0
  36. package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
  37. package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
  38. package/src/core/islands/components/output-wrapper.tsx +76 -93
  39. package/src/core/islands/components/useIslandControls.ts +60 -0
  40. package/src/core/islands/components/web-components.tsx +168 -40
  41. package/src/core/islands/constants.ts +28 -0
  42. package/src/core/islands/main.ts +7 -205
  43. package/src/core/islands/parse.ts +73 -26
  44. package/src/core/islands/worker-factory.ts +86 -0
  45. package/src/plugins/core/RenderHTML.tsx +9 -0
  46. package/src/plugins/core/__test__/RenderHTML.test.ts +27 -0
  47. package/src/plugins/core/__test__/trusted-url.test.ts +48 -0
  48. package/src/plugins/core/registerReactComponent.tsx +11 -8
  49. package/src/plugins/core/trusted-url.ts +20 -0
  50. package/src/plugins/impl/ButtonPlugin.tsx +4 -6
  51. package/src/plugins/impl/CodeEditorPlugin.tsx +15 -18
  52. package/src/plugins/impl/DataEditorPlugin.tsx +8 -14
  53. package/src/plugins/impl/DataTablePlugin.tsx +8 -9
  54. package/src/plugins/impl/FileUploadPlugin.tsx +39 -43
  55. package/src/plugins/impl/FormPlugin.tsx +2 -6
  56. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +27 -1
  57. package/src/plugins/impl/anywidget/widget-binding.ts +13 -0
  58. package/src/plugins/impl/chat/ChatPlugin.tsx +17 -20
  59. package/src/plugins/impl/data-explorer/DataExplorerPlugin.tsx +5 -8
  60. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +38 -14
  61. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +21 -0
  62. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +119 -0
  63. package/src/plugins/impl/panel/PanelPlugin.tsx +31 -10
  64. package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +60 -0
  65. package/src/plugins/impl/vega/VegaPlugin.tsx +5 -8
  66. package/src/plugins/layout/NavigationMenuPlugin.tsx +2 -6
  67. package/dist/assets/RenderHTML-CbuarQqA.js +0 -1
  68. package/dist/assets/index-bjxpaV0V.js +0 -42
  69. package/dist/assets/state-BvnlMKdT.js +0 -3
@@ -11,73 +11,172 @@ import { UI_ELEMENT_REGISTRY } from "@/core/dom/uiregistry";
11
11
  import { LocaleProvider } from "@/core/i18n/locale-provider";
12
12
  import { renderHTML } from "@/plugins/core/RenderHTML";
13
13
  import { invariant } from "@/utils/invariant";
14
- import type { CellId } from "../../cells/ids";
14
+ import type { CellId, UIElementId } from "../../cells/ids";
15
15
  import { store } from "../../state/jotai";
16
+ import {
17
+ ISLAND_CSS_CLASSES,
18
+ ISLAND_DATA_ATTRIBUTES,
19
+ ISLAND_TAG_NAMES,
20
+ } from "../constants";
16
21
  import { extractIslandCodeFromEmbed } from "../parse";
17
22
  import { MarimoOutputWrapper } from "./output-wrapper";
18
23
 
19
24
  /**
20
- * A custom element that renders the output of a marimo cell
25
+ * Configuration for rendering a marimo island
26
+ */
27
+ export interface IslandRenderConfig {
28
+ html: string;
29
+ codeCallback: () => string;
30
+ editor: JSX.Element | null;
31
+ cellId: CellId | undefined;
32
+ }
33
+
34
+ /**
35
+ * A custom element that renders the output of a marimo cell.
36
+ *
37
+ * This web component wraps marimo cell outputs and provides interactive
38
+ * functionality like re-running cells and copying code.
21
39
  */
22
40
  export class MarimoIslandElement extends HTMLElement {
23
41
  private root?: Root;
24
42
 
25
- public static readonly tagName = "marimo-island";
26
- public static readonly outputTagName = "marimo-cell-output";
27
- public static readonly codeTagName = "marimo-cell-code";
28
- public static readonly editorTagName = "marimo-code-editor";
29
- public static readonly styleNamespace = "marimo";
43
+ public static readonly tagName = ISLAND_TAG_NAMES.ISLAND;
44
+ public static readonly outputTagName = ISLAND_TAG_NAMES.CELL_OUTPUT;
45
+ public static readonly codeTagName = ISLAND_TAG_NAMES.CELL_CODE;
46
+ public static readonly editorTagName = ISLAND_TAG_NAMES.CODE_EDITOR;
47
+ public static readonly styleNamespace = ISLAND_CSS_CLASSES.NAMESPACE;
30
48
 
31
49
  constructor() {
32
50
  super();
33
51
  this.classList.add(MarimoIslandElement.styleNamespace);
34
52
  }
35
53
 
54
+ /**
55
+ * Gets the app ID from the element's data attribute
56
+ */
36
57
  get appId(): string {
37
- invariant(this.dataset.appId, "Missing data-app-id attribute");
38
- return this.dataset.appId;
58
+ const appId = this.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
59
+ invariant(appId, "Missing data-app-id attribute");
60
+ return appId;
61
+ }
62
+
63
+ /**
64
+ * Whether this island is reactive (has code sent to Python for execution)
65
+ */
66
+ get isReactive(): boolean {
67
+ return this.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true";
68
+ }
69
+
70
+ /**
71
+ * Gets the cell ID by looking up the cell index in the notebook state.
72
+ * Returns undefined for non-reactive islands (they have no corresponding cell).
73
+ */
74
+ get cellId(): CellId | undefined {
75
+ const cellId = this.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
76
+ if (cellId) {
77
+ return cellId as CellId;
78
+ }
79
+
80
+ const cellIdx = this.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
81
+ if (!cellIdx) {
82
+ return undefined;
83
+ }
84
+ return this.getCellIdFromIndex(Number.parseInt(cellIdx, 10));
39
85
  }
40
86
 
41
- get cellId(): CellId {
42
- // Get the cell ID from the code
43
- invariant(this.dataset.cellIdx, "Missing data-cell-idx attribute");
87
+ /**
88
+ * Gets the code for this island cell
89
+ */
90
+ get code(): string {
91
+ return extractIslandCodeFromEmbed(this);
92
+ }
93
+
94
+ /**
95
+ * Looks up a cell ID from the notebook state by index
96
+ */
97
+ private getCellIdFromIndex(idx: number): CellId {
44
98
  const { cellIds } = store.get(notebookAtom);
45
- const idx = Number.parseInt(this.dataset.cellIdx, 10);
46
99
  const cellId = cellIds.inOrderIds.at(idx);
47
- invariant(cellId, "Missing cell ID");
100
+ invariant(cellId, `Missing cell ID at index ${idx}`);
48
101
  return cellId;
49
102
  }
50
103
 
51
- get code(): string {
52
- return extractIslandCodeFromEmbed(this);
104
+ /**
105
+ * Called when the element is added to the DOM.
106
+ *
107
+ * Deferred to a microtask because `defineCustomElement` during
108
+ * `kernel-ready` upgrades all existing elements synchronously,
109
+ * which can happen inside a React render cycle. Rendering
110
+ * synchronously from there causes "unmount during render" warnings.
111
+ */
112
+ connectedCallback(): void {
113
+ // Capture config synchronously (before children get cleared by createRoot)
114
+ const config = this.extractRenderConfig();
115
+ queueMicrotask(() => {
116
+ // Guard against disconnect between connectedCallback and microtask
117
+ if (!this.isConnected) {
118
+ return;
119
+ }
120
+ this.root = ReactDOM.createRoot(this);
121
+ this.renderIsland(config);
122
+ });
53
123
  }
54
124
 
55
- connectedCallback() {
125
+ /**
126
+ * Extracts configuration needed for rendering
127
+ */
128
+ private extractRenderConfig(): IslandRenderConfig {
56
129
  const output = this.querySelectorOrThrow(MarimoIslandElement.outputTagName);
57
130
  const initialOutput = output.innerHTML;
58
-
59
131
  const optionalEditor = this.getOptionalEditor();
60
132
  const code = this.code;
61
- const codeCallback: () => string = optionalEditor
62
- ? () =>
63
- `${UI_ELEMENT_REGISTRY.lookupValue(
64
- optionalEditor.props[OBJECT_ID_ATTR],
65
- )}`
66
- : () => code;
67
-
68
- this.root = ReactDOM.createRoot(this);
69
- this.render(initialOutput, codeCallback, optionalEditor);
133
+ const cellId = this.cellId;
134
+
135
+ // Read objectId directly from the DOM before createRoot clears children.
136
+ // optionalEditor is a <RenderHTML> wrapper, so its .props don't carry the
137
+ // underlying element's attributes — we must grab objectId here instead.
138
+ const editorElement = this.querySelector(MarimoIslandElement.editorTagName);
139
+ const editorObjectId = (
140
+ editorElement?.parentElement as Element | null
141
+ )?.getAttribute(OBJECT_ID_ATTR) as UIElementId | null;
142
+
143
+ const codeCallback: () => string =
144
+ optionalEditor && editorObjectId
145
+ ? () => {
146
+ const val = UI_ELEMENT_REGISTRY.lookupValue(editorObjectId);
147
+ return val !== undefined ? String(val) : code;
148
+ }
149
+ : () => code;
150
+
151
+ return {
152
+ html: initialOutput,
153
+ codeCallback,
154
+ editor: optionalEditor,
155
+ cellId,
156
+ };
70
157
  }
71
158
 
72
- private render(
73
- html: string,
74
- codeCallback: () => string,
75
- editor: JSX.Element | null,
76
- ) {
159
+ /**
160
+ * Renders the island with React
161
+ */
162
+ private renderIsland(config: IslandRenderConfig): void {
163
+ const { html, codeCallback, editor, cellId } = config;
77
164
  const alwaysShowRun = !!editor;
78
- html = html.trim();
79
- const isEmpty = html === "<span></span>" || html === "";
80
- const initialHtml = isEmpty ? null : renderHTML({ html });
165
+ const trimmedHtml = html.trim();
166
+ const isEmpty = trimmedHtml === "<span></span>" || trimmedHtml === "";
167
+ const initialHtml = isEmpty ? null : renderHTML({ html: trimmedHtml });
168
+
169
+ // Non-reactive islands have no cell in the kernel — just render static HTML
170
+ if (!cellId) {
171
+ this.root?.render(
172
+ <ErrorBoundary>
173
+ <Provider store={store}>
174
+ <LocaleProvider>{initialHtml}</LocaleProvider>
175
+ </Provider>
176
+ </ErrorBoundary>,
177
+ );
178
+ return;
179
+ }
81
180
 
82
181
  this.root?.render(
83
182
  <ErrorBoundary>
@@ -85,7 +184,7 @@ export class MarimoIslandElement extends HTMLElement {
85
184
  <LocaleProvider>
86
185
  <TooltipProvider>
87
186
  <MarimoOutputWrapper
88
- cellId={this.cellId}
187
+ cellId={cellId}
89
188
  codeCallback={codeCallback}
90
189
  alwaysShowRun={alwaysShowRun}
91
190
  >
@@ -99,16 +198,30 @@ export class MarimoIslandElement extends HTMLElement {
99
198
  );
100
199
  }
101
200
 
201
+ /**
202
+ * Attempts to find and render an optional code editor.
203
+ *
204
+ * The DOM structure is:
205
+ * <marimo-cell-output>
206
+ * <div data-marimo-element> ← parent wrapper from Python render
207
+ * <marimo-code-editor .../>
208
+ * </div>
209
+ * </marimo-cell-output>
210
+ *
211
+ * We take the parent's outerHTML (the wrapper div) so that the rendered
212
+ * React element includes the UI element registration attributes.
213
+ *
214
+ * @returns A React element for the editor, or null if not found
215
+ */
102
216
  private getOptionalEditor(): JSX.Element | null {
103
- // TODO: Maybe add specificity with a [editor=island] selector or something.
104
217
  const optionalElement = this.querySelector(
105
218
  MarimoIslandElement.editorTagName,
106
219
  );
107
220
  const html = (optionalElement?.parentNode as Element)?.outerHTML;
108
221
  if (html) {
109
- // Push back to virtual dom.
222
+ // Convert HTML to virtual DOM
110
223
  const virtualDom = renderHTML({ html });
111
- // and prove that it's an element.
224
+ // Verify it's a valid React element
112
225
  if (isValidElement(virtualDom)) {
113
226
  return virtualDom;
114
227
  }
@@ -116,9 +229,24 @@ export class MarimoIslandElement extends HTMLElement {
116
229
  return null;
117
230
  }
118
231
 
119
- private querySelectorOrThrow(selector: string) {
232
+ /**
233
+ * Queries for an element and throws if not found
234
+ */
235
+ private querySelectorOrThrow(selector: string): Element {
120
236
  const element = this.querySelector(selector);
121
237
  invariant(element, `Missing ${selector} element`);
122
238
  return element;
123
239
  }
240
+
241
+ /**
242
+ * Cleanup when element is removed from DOM
243
+ */
244
+ disconnectedCallback(): void {
245
+ const root = this.root;
246
+ this.root = undefined;
247
+ // Defer unmount to avoid "unmount during render" race
248
+ if (root) {
249
+ queueMicrotask(() => root.unmount());
250
+ }
251
+ }
124
252
  }
@@ -0,0 +1,28 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Custom element tag names for islands
5
+ */
6
+ export const ISLAND_TAG_NAMES = {
7
+ ISLAND: "marimo-island",
8
+ CELL_OUTPUT: "marimo-cell-output",
9
+ CELL_CODE: "marimo-cell-code",
10
+ CODE_EDITOR: "marimo-code-editor",
11
+ } as const;
12
+
13
+ /**
14
+ * Data attributes for islands
15
+ */
16
+ export const ISLAND_DATA_ATTRIBUTES = {
17
+ APP_ID: "data-app-id",
18
+ CELL_IDX: "data-cell-idx",
19
+ CELL_ID: "data-cell-id",
20
+ REACTIVE: "data-reactive",
21
+ } as const;
22
+
23
+ /**
24
+ * CSS classes for islands
25
+ */
26
+ export const ISLAND_CSS_CLASSES = {
27
+ NAMESPACE: "marimo",
28
+ } as const;
@@ -11,216 +11,18 @@ import "../../css/table.css";
11
11
 
12
12
  import "iconify-icon";
13
13
 
14
- import { toast } from "@/components/ui/use-toast";
15
- import { renderHTML } from "@/plugins/core/RenderHTML";
16
- import {
17
- handleWidgetMessage,
18
- MODEL_MANAGER,
19
- } from "@/plugins/impl/anywidget/model";
20
- import { initializePlugins } from "@/plugins/plugins";
21
- import { logNever } from "@/utils/assertNever";
22
- import { Functions } from "@/utils/functions";
23
- import { safeExtractSetUIElementMessageBuffers } from "@/utils/json/base64";
24
- import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
25
14
  import { Logger } from "@/utils/Logger";
26
- import {
27
- createNotebookActions,
28
- notebookAtom,
29
- notebookReducer,
30
- } from "../cells/cells";
31
- import { defineCustomElement } from "../dom/defineCustomElement";
32
- import { MarimoValueInputEvent } from "../dom/events";
33
- import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
34
- import { FUNCTIONS_REGISTRY } from "../functions/FunctionRegistry";
35
- import {
36
- handleCellNotificationeration,
37
- handleKernelReady,
38
- handleRemoveUIElements,
39
- } from "../kernel/handlers";
40
- import { queryParamHandlers } from "../kernel/queryParamHandlers";
41
- import { RuntimeState } from "../kernel/RuntimeState";
42
- import { initialModeAtom } from "../mode";
43
- import { requestClientAtom } from "../network/requests";
44
- import { store } from "../state/jotai";
45
- import { IslandsPyodideBridge } from "./bridge";
46
- import { MarimoIslandElement } from "./components/web-components";
47
- import {
48
- shouldShowIslandsWarningIndicatorAtom,
49
- userTriedToInteractWithIslandsAtom,
50
- } from "./state";
51
- import { dismissIslandsLoadingToast, toastIslandsLoading } from "./toast";
15
+ import { initializeIslands } from "./bootstrap";
16
+ import { getGlobalBridge } from "./bridge";
52
17
 
53
18
  /**
54
19
  * Main entry point for the js bundle for embedded marimo apps.
55
20
  */
56
-
57
- /**
58
- * Initialize the Marimo app.
59
- */
60
21
  export async function initialize() {
61
- // Setup networking
62
- store.set(requestClientAtom, IslandsPyodideBridge.INSTANCE);
63
- store.set(initialModeAtom, "read");
64
-
65
- // This will display all the static HTML content.
66
- initializePlugins();
67
-
68
- // Find all `marimo-island` elements.
69
- const islands = document.querySelectorAll<HTMLElement>(
70
- MarimoIslandElement.tagName,
71
- );
72
-
73
- // If no islands are found, we can skip the rest of the initialization.
74
- if (islands.length === 0) {
75
- return;
76
- }
77
-
78
- // Add 'marimo' class name to all `marimo-island` elements.
79
- // This makes our styles apply to the islands.
80
- for (const island of islands) {
81
- island.classList.add(MarimoIslandElement.styleNamespace);
82
- }
83
-
84
- const actions = createNotebookActions((action) => {
85
- store.set(notebookAtom, (state) => notebookReducer(state, action));
86
- });
87
-
88
- // If the user has interacted with the islands before they are initialized,
89
- // we show the loading toast.
90
- store.sub(shouldShowIslandsWarningIndicatorAtom, () => {
91
- const showing = store.get(shouldShowIslandsWarningIndicatorAtom);
92
- if (showing) {
93
- toastIslandsLoading();
94
- // For each island, set the opacity to 0.5
95
- for (const island of islands) {
96
- island.style.setProperty("opacity", "0.5");
97
- }
98
- } else {
99
- dismissIslandsLoadingToast();
100
- // For each island, remove the opacity
101
- for (const island of islands) {
102
- island.style.removeProperty("opacity");
103
- }
104
- }
105
- });
106
-
107
- // Consume messages from the kernel
108
- IslandsPyodideBridge.INSTANCE.consumeMessages((message) => {
109
- const msg = jsonParseWithSpecialChar(message);
110
- switch (msg.data.op) {
111
- case "banner":
112
- case "missing-package-alert":
113
- case "installing-package-alert":
114
- case "completion-result":
115
- case "reload":
116
- case "focus-cell":
117
- case "variables":
118
- case "variable-values":
119
- case "data-column-preview":
120
- case "sql-table-preview":
121
- case "sql-table-list-preview":
122
- case "sql-schema-list-preview":
123
- case "datasets":
124
- case "data-source-connections":
125
- case "validate-sql-result":
126
- case "storage-namespaces":
127
- case "storage-entries":
128
- case "storage-download-ready":
129
- case "secret-keys-result":
130
- case "startup-logs":
131
- // Unsupported
132
- return;
133
- case "kernel-ready":
134
- handleKernelReady(msg.data, {
135
- autoInstantiate: true,
136
- setCells: actions.setCells,
137
- setLayoutData: Functions.NOOP,
138
- setAppConfig: Functions.NOOP,
139
- setCapabilities: Functions.NOOP,
140
- setKernelState: Functions.NOOP,
141
- onError: Logger.error,
142
- });
143
- // Define the custom element for the marimo-island tag.
144
- // This comes after initializing since this reads from the store.
145
- defineCustomElement(MarimoIslandElement.tagName, MarimoIslandElement);
146
- return;
147
- case "completed-run":
148
- return;
149
- case "interrupted":
150
- return;
151
- case "send-ui-element-message":
152
- UI_ELEMENT_REGISTRY.broadcastMessage(
153
- msg.data.ui_element,
154
- msg.data.message,
155
- safeExtractSetUIElementMessageBuffers(msg.data),
156
- );
157
- return;
158
-
159
- case "remove-ui-elements":
160
- handleRemoveUIElements(msg.data);
161
- return;
162
- case "function-call-result":
163
- FUNCTIONS_REGISTRY.resolve(msg.data.function_call_id, msg.data);
164
- return;
165
- case "cell-op":
166
- handleCellNotificationeration(msg.data, actions.handleCellMessage);
167
- return;
168
- case "alert":
169
- // TODO: support toast with islands
170
- toast({
171
- title: msg.data.title,
172
- description: renderHTML({
173
- html: msg.data.description,
174
- }),
175
- variant: msg.data.variant,
176
- });
177
- return;
178
- case "query-params-append":
179
- queryParamHandlers.append(msg.data);
180
- return;
181
- case "query-params-set":
182
- queryParamHandlers.set(msg.data);
183
- return;
184
- case "query-params-delete":
185
- queryParamHandlers.delete(msg.data);
186
- return;
187
- case "query-params-clear":
188
- queryParamHandlers.clear();
189
- return;
190
- case "reconnected":
191
- return;
192
- case "cache-cleared":
193
- return;
194
- case "cache-info":
195
- return;
196
- case "kernel-startup-error":
197
- return;
198
- case "notebook-document-transaction":
199
- return;
200
- case "model-lifecycle":
201
- handleWidgetMessage(MODEL_MANAGER, msg.data);
202
- return;
203
- default:
204
- logNever(msg.data);
205
- }
206
- });
207
-
208
- // Set the user tried to interact with islands
209
- // before they are initialized.
210
- document.addEventListener(
211
- MarimoValueInputEvent.TYPE,
212
- () => {
213
- store.set(userTriedToInteractWithIslandsAtom, true);
214
- },
215
- {
216
- once: true,
217
- },
218
- );
219
-
220
- // Start the runtime
221
- RuntimeState.INSTANCE.start(
222
- IslandsPyodideBridge.INSTANCE.sendComponentValues,
223
- );
22
+ await initializeIslands({ bridge: getGlobalBridge() });
224
23
  }
225
24
 
226
- initialize();
25
+ // Auto-initialize on module load
26
+ void initialize().catch((error) => {
27
+ Logger.error("Failed to initialize islands:", error);
28
+ });
@@ -1,6 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { MarimoIslandElement } from "@/core/islands/components/web-components";
3
+ import {
4
+ ISLAND_DATA_ATTRIBUTES,
5
+ ISLAND_TAG_NAMES,
6
+ } from "@/core/islands/constants";
4
7
  import { Logger } from "@/utils/Logger";
5
8
 
6
9
  /**
@@ -41,53 +44,94 @@ interface MarimoIslandCell {
41
44
  idx: number;
42
45
  }
43
46
 
44
- export function parseMarimoIslandApps(): MarimoIslandApp[] {
45
- const apps = new Map<string, MarimoIslandApp>();
46
-
47
- const embeds = document.querySelectorAll<HTMLElement>(
48
- MarimoIslandElement.tagName,
49
- );
47
+ /**
48
+ * Parses marimo island apps from the DOM
49
+ * @param root - Root element to search within (defaults to document)
50
+ */
51
+ export function parseMarimoIslandApps(
52
+ root: Document | Element = document,
53
+ ): MarimoIslandApp[] {
54
+ const embeds = root.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND);
50
55
  if (embeds.length === 0) {
51
56
  Logger.warn("No embedded marimo apps found.");
52
57
  return [];
53
58
  }
54
59
 
60
+ // eslint-disable-next-line prefer-spread
61
+ return parseIslandElementsIntoApps(Array.from(embeds));
62
+ }
63
+
64
+ /**
65
+ * Pure function to parse island elements into app structures
66
+ * @param embeds - Array of island HTML elements
67
+ */
68
+ export function parseIslandElementsIntoApps(
69
+ embeds: HTMLElement[],
70
+ ): MarimoIslandApp[] {
71
+ const apps = new Map<string, MarimoIslandApp>();
72
+
55
73
  for (const embed of embeds) {
56
- const id = embed.dataset.appId;
57
- if (!id) {
74
+ const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
75
+ if (!appId) {
58
76
  Logger.warn("Embedded marimo cell missing data-app-id attribute.");
59
77
  continue;
60
78
  }
61
79
 
62
- const cellOutput = embed.querySelector<HTMLElement>(
63
- MarimoIslandElement.outputTagName,
64
- );
65
- const code = extractIslandCodeFromEmbed(embed);
80
+ // Non-reactive islands are static — they don't participate in the kernel
81
+ const reactive =
82
+ embed.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true";
83
+ if (!reactive) {
84
+ continue;
85
+ }
66
86
 
67
- if (!cellOutput || !code) {
68
- Logger.warn(`Embedded marimo app ${id} missing cell output or code.`);
87
+ const cellData = parseIslandElement(embed);
88
+ if (!cellData) {
89
+ Logger.warn(`Embedded marimo app ${appId} missing cell output or code.`);
69
90
  continue;
70
91
  }
71
92
 
72
- if (!apps.has(id)) {
73
- apps.set(id, { id, cells: [] });
93
+ if (!apps.has(appId)) {
94
+ apps.set(appId, { id: appId, cells: [] });
74
95
  }
75
- // oxlint-disable-next-line typescript/no-non-null-assertion
76
- const app = apps.get(id)!;
96
+
97
+ const app = apps.get(appId)!;
77
98
  const idx = app.cells.length;
78
99
  app.cells.push({
79
- output: cellOutput.innerHTML,
80
- code: code,
100
+ output: cellData.output,
101
+ code: cellData.code,
81
102
  idx: idx,
82
103
  });
83
104
 
84
105
  // Add data-cell-idx attribute to the island element
85
- embed.dataset.cellIdx = idx.toString();
106
+ embed.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
86
107
  }
87
108
 
88
109
  return [...apps.values()];
89
110
  }
90
111
 
112
+ /**
113
+ * Parses a single island element into cell data
114
+ * @param embed - The island HTML element
115
+ * @returns Cell data or null if invalid
116
+ */
117
+ export function parseIslandElement(
118
+ embed: HTMLElement,
119
+ ): { output: string; code: string } | null {
120
+ const cellOutput = embed.querySelector<HTMLElement>(
121
+ ISLAND_TAG_NAMES.CELL_OUTPUT,
122
+ );
123
+ const code = extractIslandCodeFromEmbed(embed);
124
+
125
+ if (!cellOutput || !code) {
126
+ return null;
127
+ }
128
+
129
+ return {
130
+ output: cellOutput.innerHTML,
131
+ code: code,
132
+ };
133
+ }
134
+
91
135
  export function createMarimoFile(app: { cells: { code: string }[] }): string {
92
136
  const lines = [
93
137
  "import marimo",
@@ -134,7 +178,8 @@ export function parseIslandCode(code: string | undefined | null): string {
134
178
  }
135
179
 
136
180
  export function extractIslandCodeFromEmbed(embed: HTMLElement): string {
137
- const reactive = embed.dataset.reactive === "true";
181
+ const reactive =
182
+ embed.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true";
138
183
  // Non-reactive cells are not guaranteed to have code, and should be treated as
139
184
  // such.
140
185
  if (!reactive) {
@@ -142,17 +187,19 @@ export function extractIslandCodeFromEmbed(embed: HTMLElement): string {
142
187
  }
143
188
 
144
189
  const cellCodeElement = embed.querySelector<HTMLElement>(
145
- MarimoIslandElement.codeTagName,
190
+ ISLAND_TAG_NAMES.CELL_CODE,
146
191
  );
147
192
  if (cellCodeElement) {
148
193
  return parseIslandCode(cellCodeElement.textContent);
149
194
  }
150
195
 
151
196
  const editorCodeElement = embed.querySelector<HTMLElement>(
152
- MarimoIslandElement.editorTagName,
197
+ ISLAND_TAG_NAMES.CODE_EDITOR,
153
198
  );
154
199
  if (editorCodeElement) {
155
- return parseIslandEditor(editorCodeElement.dataset.initialValue);
200
+ return parseIslandEditor(
201
+ editorCodeElement.getAttribute("data-initial-value"),
202
+ );
156
203
  }
157
204
 
158
205
  return "";