@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.
Files changed (85) 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-BI94mK4u.js → code-visibility-DBuB5MVY.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-ELidGmPF.js → reveal-component-DfOZrkd6.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 -69
  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/__tests__/main.test.ts +0 -176
  81. package/src/core/islands/components/__tests__/web-components.test.tsx +0 -214
  82. package/src/core/islands/worker/__tests__/controller.test.ts +0 -173
  83. package/src/hooks/__tests__/useHotkey.test.tsx +0 -88
  84. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +0 -71
  85. package/src/theme/__tests__/useTheme.test.ts +0 -68
@@ -15,7 +15,6 @@ import {
15
15
  parseIslandElement,
16
16
  parseIslandElementsIntoApps,
17
17
  parseMarimoIslandApps,
18
- retainIslandSource,
19
18
  } from "../parse";
20
19
  import { createMockIslandElement, createMockIslands } from "./test-utils.tsx";
21
20
 
@@ -54,7 +53,6 @@ function appendPayload(
54
53
  script.type = ISLANDS_JSON_SCRIPT_TYPE;
55
54
  script.textContent = JSON.stringify(payload);
56
55
  root.appendChild(script);
57
- return script;
58
56
  }
59
57
 
60
58
  describe("createMarimoFile", () => {
@@ -319,39 +317,6 @@ describe("parseIslandElement", () => {
319
317
 
320
318
  expect(result).toBeNull();
321
319
  });
322
-
323
- it("uses retained source after the custom element renders", () => {
324
- const element = document.createElement(ISLAND_TAG_NAMES.ISLAND);
325
- retainIslandSource(element, {
326
- code: 'print("retained")',
327
- output: "<div>retained output</div>",
328
- });
329
-
330
- expect(parseIslandElement(element)).toEqual({
331
- code: 'print("retained")',
332
- output: "<div>retained output</div>",
333
- });
334
-
335
- retainIslandSource(element, { code: "", output: "<div>static</div>" });
336
- expect(parseIslandElement(element)).toBeNull();
337
- });
338
-
339
- it("prefers new live source when an island element is reused", () => {
340
- const element = createMockIslandElement({
341
- code: 'print("new")',
342
- innerHTML: "<div>new output</div>",
343
- });
344
- element.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
345
- retainIslandSource(element, {
346
- code: 'print("retained")',
347
- output: "<div>retained output</div>",
348
- });
349
-
350
- expect(parseIslandElement(element)).toEqual({
351
- code: 'print("new")',
352
- output: "<div>new output</div>",
353
- });
354
- });
355
320
  });
356
321
 
357
322
  describe("parseIslandElementsIntoApps", () => {
@@ -665,76 +630,6 @@ describe("parseMarimoIslandApps", () => {
665
630
  );
666
631
  });
667
632
 
668
- it("preserves payload-backed cells after a non-materializing capability probe", () => {
669
- const island = createMockIslandElement({
670
- appId: "app1",
671
- cellId: "cell-2",
672
- code: "dom_code = True",
673
- innerHTML: "<div>dom output</div>",
674
- });
675
- island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
676
- container.appendChild(island);
677
- appendPayload(container, {
678
- schemaVersion: 1,
679
- appId: "app1",
680
- cells: [createPayloadCell({ cellId: "cell-2" })],
681
- });
682
- const originalIsland = island.outerHTML;
683
-
684
- const probed = parseMarimoIslandApps(container, { materialize: false });
685
-
686
- expect(probed).toHaveLength(1);
687
- expect(island.outerHTML).toBe(originalIsland);
688
-
689
- expect(parseMarimoIslandApps(container)).toEqual(probed);
690
- expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID)).toBeNull();
691
- expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
692
- });
693
-
694
- it("refreshes a reused payload anchor when its content changes", () => {
695
- const island = createMockIslandElement({
696
- appId: "app1",
697
- cellId: "cell-1",
698
- code: 'print("dom")',
699
- innerHTML: "<div>dom output</div>",
700
- });
701
- island.insertAdjacentHTML(
702
- "beforeend",
703
- '<div data-marimo-element><marimo-code-editor data-initial-value="old"></marimo-code-editor></div>',
704
- );
705
- island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
706
- container.appendChild(island);
707
- const script = appendPayload(container, {
708
- schemaVersion: 1,
709
- appId: "app1",
710
- cells: [createPayloadCell()],
711
- });
712
- parseMarimoIslandApps(container);
713
-
714
- script.textContent = JSON.stringify({
715
- schemaVersion: 1,
716
- appId: "app1",
717
- cells: [
718
- createPayloadCell({
719
- code: 'print("updated")',
720
- outputHtml: "<div>updated output</div>",
721
- }),
722
- ],
723
- });
724
-
725
- parseMarimoIslandApps(container);
726
-
727
- expect(extractIslandCodeFromEmbed(island)).toBe('print("updated")');
728
- expect(island.querySelector(ISLAND_TAG_NAMES.CELL_OUTPUT)?.innerHTML).toBe(
729
- "<div>updated output</div>",
730
- );
731
- expect(
732
- island
733
- .querySelector(ISLAND_TAG_NAMES.CODE_EDITOR)
734
- ?.getAttribute("data-initial-value"),
735
- ).toBe(JSON.stringify('print("updated")'));
736
- });
737
-
738
633
  it("should parse Python-generated island payload snapshots", () => {
739
634
  const html = readFileSync(
740
635
  new URL(
@@ -108,17 +108,14 @@ export async function initializeIslands(
108
108
  // Loading indicator: dim islands while Pyodide initializes
109
109
  store.sub(shouldShowIslandsWarningIndicatorAtom, () => {
110
110
  const showing = store.get(shouldShowIslandsWarningIndicatorAtom);
111
- const currentIslands = root.querySelectorAll<HTMLElement>(
112
- ISLAND_TAG_NAMES.ISLAND,
113
- );
114
111
  if (showing) {
115
112
  toastIslandsLoading();
116
- for (const island of currentIslands) {
113
+ for (const island of islands) {
117
114
  island.style.setProperty("opacity", "0.5");
118
115
  }
119
116
  } else {
120
117
  dismissIslandsLoadingToast();
121
- for (const island of currentIslands) {
118
+ for (const island of islands) {
122
119
  island.style.removeProperty("opacity");
123
120
  }
124
121
  }
@@ -211,9 +208,7 @@ function handleMessage(
211
208
  setKernelState: Functions.NOOP,
212
209
  onError: Logger.error,
213
210
  });
214
- if (!window.customElements?.get(ISLAND_TAG_NAMES.ISLAND)) {
215
- defineCustomElement(ISLAND_TAG_NAMES.ISLAND, MarimoIslandElement);
216
- }
211
+ defineCustomElement(ISLAND_TAG_NAMES.ISLAND, MarimoIslandElement);
217
212
  return;
218
213
 
219
214
  case "send-ui-element-message":
@@ -7,7 +7,6 @@ import { throwNotImplemented } from "@/utils/functions";
7
7
  import type { JsonString } from "@/utils/json/base64";
8
8
  import { Logger } from "@/utils/Logger";
9
9
  import { generateUUID } from "@/utils/uuid";
10
- import { initialNotebookState, notebookAtom } from "../cells/cells";
11
10
  import type { CommandMessage, NotificationPayload } from "../kernel/messages";
12
11
  import type { EditRequests, RunRequests } from "../network/types";
13
12
  import { store as defaultStore } from "../state/jotai";
@@ -36,12 +35,11 @@ export interface IslandsBridgeConfig {
36
35
  * Optional root element for parsing islands (for testing)
37
36
  */
38
37
  root?: Document | Element;
39
- }
40
38
 
41
- interface AppSession {
42
- appId: string;
43
- code?: string;
44
- sessionGeneration: number;
39
+ /**
40
+ * Whether to auto-start sessions on worker ready (default: true)
41
+ */
42
+ autoStartSessions?: boolean;
45
43
  }
46
44
 
47
45
  /**
@@ -53,8 +51,8 @@ interface AppSession {
53
51
  * @example
54
52
  * ```ts
55
53
  * const bridge = new IslandsPyodideBridge();
54
+ * await bridge.initialized;
56
55
  * bridge.consumeMessages(message => console.log(message));
57
- * await bridge.initializeApps();
58
56
  * ```
59
57
  */
60
58
  export class IslandsPyodideBridge implements RunRequests, EditRequests {
@@ -64,17 +62,14 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
64
62
  | undefined;
65
63
  private readonly store: typeof defaultStore;
66
64
  private readonly root: Document | Element;
67
- private session: AppSession | undefined;
68
- private sessionReady = new Deferred<AppSession>();
69
- private nextSessionGeneration = 0;
70
- private appTransition = Promise.resolve();
71
- private workerReady = new Deferred<void>();
65
+ private readonly autoStartSessions: boolean;
72
66
 
73
67
  public initialized = new Deferred<void>();
74
68
 
75
69
  constructor(config: IslandsBridgeConfig = {}) {
76
70
  this.store = config.store || defaultStore;
77
71
  this.root = config.root || document;
72
+ this.autoStartSessions = config.autoStartSessions ?? true;
78
73
 
79
74
  try {
80
75
  const factory = config.workerFactory || new DefaultWorkerFactory();
@@ -93,7 +88,9 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
93
88
  */
94
89
  private setupMessageListeners(): void {
95
90
  this.rpc.addMessageListener("ready", () => {
96
- this.workerReady.resolve();
91
+ if (this.autoStartSessions) {
92
+ this.startSessionsForAllApps();
93
+ }
97
94
  });
98
95
 
99
96
  this.rpc.addMessageListener("initialized", () => {
@@ -118,16 +115,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
118
115
  );
119
116
  }
120
117
 
121
- async initializeApps(): Promise<void> {
122
- await this.enqueueAppTransition(async () => {
123
- await this.workerReady.promise;
124
- await this.startApps();
125
- });
126
- }
127
-
128
- private async startApps(): Promise<void> {
118
+ /**
119
+ * Starts sessions for all apps found in the DOM
120
+ */
121
+ private startSessionsForAllApps(): void {
129
122
  const apps = parseMarimoIslandApps(this.root);
130
- const managesSingleApp = apps.length === 1;
131
123
  Logger.debug(
132
124
  `Starting sessions for ${apps.length} app(s):`,
133
125
  apps.map((a) => `${a.id} (${a.cells.length} cells)`),
@@ -142,83 +134,20 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
142
134
  const notebookCode = exportContext?.notebookCode;
143
135
  for (const app of apps) {
144
136
  const file = notebookCode || createMarimoFile(app);
145
- if (
146
- managesSingleApp &&
147
- this.session?.appId === app.id &&
148
- this.session.code === file
149
- ) {
150
- return;
151
- }
152
137
  Logger.debug(`App ${app.id} marimo file:\n`, file);
153
- const request = {
138
+ this.startSession({
154
139
  code: file,
155
140
  appId: app.id,
156
- sessionGeneration: ++this.nextSessionGeneration,
157
- };
158
- const previousSession = this.session;
159
- const replacesSession = managesSingleApp && previousSession !== undefined;
160
- if (replacesSession) {
161
- this.store.set(notebookAtom, initialNotebookState());
162
- }
163
- if (managesSingleApp || !previousSession) {
164
- if (managesSingleApp && this.sessionReady.status !== "pending") {
165
- this.sessionReady = new Deferred<AppSession>();
166
- }
167
- this.session = {
168
- ...request,
169
- code: managesSingleApp ? file : undefined,
170
- };
171
- }
172
- const operation = replacesSession
173
- ? this.rpc.proxy.request.replaceSession(request)
174
- : this.startSession(request);
175
- try {
176
- await operation;
177
- if (this.sessionReady.status === "pending" && this.session) {
178
- this.sessionReady.resolve(this.session);
179
- }
180
- } catch (error) {
181
- if (this.session?.sessionGeneration === request.sessionGeneration) {
182
- this.session = previousSession;
183
- }
141
+ }).catch((error) => {
184
142
  Logger.error(`Failed to start session for app ${app.id}:`, error);
185
- if (managesSingleApp) {
186
- throw error;
187
- }
188
- }
189
- }
190
- }
191
-
192
- async stopSession(appId?: string): Promise<void> {
193
- await this.enqueueAppTransition(async () => {
194
- const session = this.session;
195
- if (session?.code === undefined || (appId && session.appId !== appId)) {
196
- return;
197
- }
198
- await this.rpc.proxy.request.stopSession({
199
- appId: session.appId,
200
- sessionGeneration: session.sessionGeneration,
201
143
  });
202
- this.session = undefined;
203
- this.sessionReady = new Deferred<AppSession>();
204
- this.store.set(notebookAtom, initialNotebookState());
205
- });
206
- }
207
-
208
- private enqueueAppTransition(operation: () => Promise<void>): Promise<void> {
209
- const result = this.appTransition.then(operation);
210
- this.appTransition = result.catch(() => undefined);
211
- return result;
144
+ }
212
145
  }
213
146
 
214
147
  /**
215
148
  * Starts a new Python session for an app
216
149
  */
217
- async startSession(opts: {
218
- code: string;
219
- appId: string;
220
- sessionGeneration: number;
221
- }): Promise<void> {
150
+ async startSession(opts: { code: string; appId: string }): Promise<void> {
222
151
  await this.rpc.proxy.request.startSession(opts);
223
152
  }
224
153
 
@@ -274,19 +203,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
274
203
  // ============================================================================
275
204
 
276
205
  sendRun: EditRequests["sendRun"] = async (request): Promise<null> => {
277
- const session = await this.getActiveSession();
278
- await this.rpc.proxy.request.loadPackages({
279
- appId: session.appId,
280
- code: request.codes.join("\n"),
281
- sessionGeneration: session.sessionGeneration,
206
+ await this.rpc.proxy.request.loadPackages(request.codes.join("\n"));
207
+ await this.putControlRequest({
208
+ type: "execute-cells",
209
+ ...request,
282
210
  });
283
- await this.putControlRequest(
284
- {
285
- type: "execute-cells",
286
- ...request,
287
- },
288
- session,
289
- );
290
211
  return null;
291
212
  };
292
213
 
@@ -357,27 +278,13 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
357
278
 
358
279
  // The kernel uses msgspec to parse control requests, which requires a 'type'
359
280
  // field for discriminated union deserialization.
360
- private async putControlRequest(
361
- operation: CommandMessage,
362
- session?: AppSession,
363
- ): Promise<void> {
364
- session ??= await this.getActiveSession();
281
+ private async putControlRequest(operation: CommandMessage): Promise<void> {
365
282
  await this.rpc.proxy.request.bridge({
366
- appId: session.appId,
367
283
  functionName: "put_control_request",
368
284
  payload: operation,
369
- sessionGeneration: session.sessionGeneration,
370
285
  });
371
286
  }
372
287
 
373
- private async getActiveSession(): Promise<AppSession> {
374
- const transition = this.appTransition;
375
- const session = this.session;
376
- const ready = this.sessionReady;
377
- await transition;
378
- return session ?? ready.promise;
379
- }
380
-
381
288
  /**
382
289
  * Cleans up resources (for testing)
383
290
  */
@@ -5,7 +5,7 @@ import { isValidElement, type JSX } from "react";
5
5
  import ReactDOM, { type Root } from "react-dom/client";
6
6
  import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
7
7
  import { TooltipProvider } from "@/components/ui/tooltip";
8
- import { cellIdsAtom, notebookAtom } from "@/core/cells/cells";
8
+ import { notebookAtom } from "@/core/cells/cells";
9
9
  import { OBJECT_ID_ATTR } from "@/core/dom/ui-element-constants";
10
10
  import { UI_ELEMENT_REGISTRY } from "@/core/dom/uiregistry";
11
11
  import { LocaleProvider } from "@/core/i18n/locale-provider";
@@ -16,10 +16,9 @@ import { store } from "../../state/jotai";
16
16
  import {
17
17
  ISLAND_CSS_CLASSES,
18
18
  ISLAND_DATA_ATTRIBUTES,
19
- ISLAND_SOURCE_CHANGED_EVENT,
20
19
  ISLAND_TAG_NAMES,
21
20
  } from "../constants";
22
- import { extractIslandCodeFromEmbed, retainIslandSource } from "../parse";
21
+ import { extractIslandCodeFromEmbed } from "../parse";
23
22
  import { MarimoOutputWrapper } from "./output-wrapper";
24
23
 
25
24
  /**
@@ -29,6 +28,7 @@ export interface IslandRenderConfig {
29
28
  html: string;
30
29
  codeCallback: () => string;
31
30
  editor: JSX.Element | null;
31
+ cellId: CellId | undefined;
32
32
  }
33
33
 
34
34
  /**
@@ -38,12 +38,7 @@ export interface IslandRenderConfig {
38
38
  * functionality like re-running cells and copying code.
39
39
  */
40
40
  export class MarimoIslandElement extends HTMLElement {
41
- private connectionGeneration = 0;
42
41
  private root?: Root;
43
- private renderConfig?: IslandRenderConfig;
44
- private runtimeAppId?: string;
45
- private runtimeCellId?: CellId;
46
- private unsubscribeCellIds?: () => void;
47
42
 
48
43
  public static readonly tagName = ISLAND_TAG_NAMES.ISLAND;
49
44
  public static readonly outputTagName = ISLAND_TAG_NAMES.CELL_OUTPUT;
@@ -77,9 +72,6 @@ export class MarimoIslandElement extends HTMLElement {
77
72
  * Returns undefined for non-reactive islands (they have no corresponding cell).
78
73
  */
79
74
  get cellId(): CellId | undefined {
80
- if (!this.isReactive) {
81
- return undefined;
82
- }
83
75
  const cellId = this.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
84
76
  if (cellId) {
85
77
  return cellId as CellId;
@@ -102,9 +94,11 @@ export class MarimoIslandElement extends HTMLElement {
102
94
  /**
103
95
  * Looks up a cell ID from the notebook state by index
104
96
  */
105
- private getCellIdFromIndex(idx: number): CellId | undefined {
97
+ private getCellIdFromIndex(idx: number): CellId {
106
98
  const { cellIds } = store.get(notebookAtom);
107
- return cellIds.inOrderIds.at(idx);
99
+ const cellId = cellIds.inOrderIds.at(idx);
100
+ invariant(cellId, `Missing cell ID at index ${idx}`);
101
+ return cellId;
108
102
  }
109
103
 
110
104
  /**
@@ -116,62 +110,18 @@ export class MarimoIslandElement extends HTMLElement {
116
110
  * synchronously from there causes "unmount during render" warnings.
117
111
  */
118
112
  connectedCallback(): void {
119
- const connectionGeneration = ++this.connectionGeneration;
120
- this.addEventListener(
121
- ISLAND_SOURCE_CHANGED_EVENT,
122
- this.handleSourceChanged,
123
- );
124
- // Capture config synchronously (before children get cleared by createRoot).
125
- // A reconnected element reuses the source captured on its first mount.
126
- if (this.querySelector(MarimoIslandElement.outputTagName)) {
127
- this.renderConfig = this.extractRenderConfig();
128
- }
129
- invariant(this.renderConfig, "Missing island render source");
130
- this.runtimeAppId = this.appId;
131
- this.runtimeCellId = this.hasAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)
132
- ? this.cellId
133
- : undefined;
134
- this.syncCellIdsSubscription();
113
+ // Capture config synchronously (before children get cleared by createRoot)
114
+ const config = this.extractRenderConfig();
135
115
  queueMicrotask(() => {
136
116
  // Guard against disconnect between connectedCallback and microtask
137
- if (
138
- !this.isConnected ||
139
- connectionGeneration !== this.connectionGeneration
140
- ) {
117
+ if (!this.isConnected) {
141
118
  return;
142
119
  }
143
120
  this.root = ReactDOM.createRoot(this);
144
- this.renderIsland();
121
+ this.renderIsland(config);
145
122
  });
146
123
  }
147
124
 
148
- private handleSourceChanged = (): void => {
149
- if (this.querySelector(MarimoIslandElement.outputTagName)) {
150
- this.renderConfig = this.extractRenderConfig();
151
- this.querySelector(MarimoIslandElement.outputTagName)?.remove();
152
- this.querySelector(MarimoIslandElement.codeTagName)?.remove();
153
- }
154
- this.runtimeCellId =
155
- this.isReactive && this.runtimeAppId === this.appId
156
- ? this.cellId
157
- : undefined;
158
- this.runtimeAppId = this.appId;
159
- this.syncCellIdsSubscription();
160
- this.renderIsland();
161
- };
162
-
163
- private syncCellIdsSubscription(): void {
164
- if (this.isReactive) {
165
- this.unsubscribeCellIds ??= store.sub(cellIdsAtom, () => {
166
- this.runtimeCellId = this.cellId;
167
- this.renderIsland();
168
- });
169
- return;
170
- }
171
- this.unsubscribeCellIds?.();
172
- this.unsubscribeCellIds = undefined;
173
- }
174
-
175
125
  /**
176
126
  * Extracts configuration needed for rendering
177
127
  */
@@ -180,7 +130,7 @@ export class MarimoIslandElement extends HTMLElement {
180
130
  const initialOutput = output.innerHTML;
181
131
  const optionalEditor = this.getOptionalEditor();
182
132
  const code = this.code;
183
- retainIslandSource(this, { code, output: initialOutput });
133
+ const cellId = this.cellId;
184
134
 
185
135
  // Read objectId directly from the DOM before createRoot clears children.
186
136
  // optionalEditor is a <RenderHTML> wrapper, so its .props don't carry the
@@ -202,19 +152,15 @@ export class MarimoIslandElement extends HTMLElement {
202
152
  html: initialOutput,
203
153
  codeCallback,
204
154
  editor: optionalEditor,
155
+ cellId,
205
156
  };
206
157
  }
207
158
 
208
159
  /**
209
160
  * Renders the island with React
210
161
  */
211
- private renderIsland(): void {
212
- const config = this.renderConfig;
213
- if (!config) {
214
- return;
215
- }
216
- const { html, codeCallback, editor } = config;
217
- const cellId = this.runtimeCellId;
162
+ private renderIsland(config: IslandRenderConfig): void {
163
+ const { html, codeCallback, editor, cellId } = config;
218
164
  const alwaysShowRun = !!editor;
219
165
  const trimmedHtml = html.trim();
220
166
  const isEmpty = trimmedHtml === "<span></span>" || trimmedHtml === "";
@@ -222,7 +168,6 @@ export class MarimoIslandElement extends HTMLElement {
222
168
 
223
169
  // Non-reactive islands have no cell in the kernel — just render static HTML
224
170
  if (!cellId) {
225
- this.removeAttribute("data-status");
226
171
  this.root?.render(
227
172
  <ErrorBoundary>
228
173
  <Provider store={store}>
@@ -297,12 +242,6 @@ export class MarimoIslandElement extends HTMLElement {
297
242
  * Cleanup when element is removed from DOM
298
243
  */
299
244
  disconnectedCallback(): void {
300
- this.removeEventListener(
301
- ISLAND_SOURCE_CHANGED_EVENT,
302
- this.handleSourceChanged,
303
- );
304
- this.unsubscribeCellIds?.();
305
- this.unsubscribeCellIds = undefined;
306
245
  const root = this.root;
307
246
  this.root = undefined;
308
247
  // Defer unmount to avoid "unmount during render" race
@@ -11,7 +11,6 @@ export const ISLAND_TAG_NAMES = {
11
11
  } as const;
12
12
 
13
13
  export const ISLANDS_JSON_SCRIPT_TYPE = "application/vnd.marimo.islands+json";
14
- export const ISLAND_SOURCE_CHANGED_EVENT = "marimo-island-source-changed";
15
14
 
16
15
  /**
17
16
  * Data attributes for islands
@@ -14,78 +14,12 @@ import "iconify-icon";
14
14
  import { Logger } from "@/utils/Logger";
15
15
  import { initializeIslands } from "./bootstrap";
16
16
  import { getGlobalBridge } from "./bridge";
17
- import { ISLAND_CSS_CLASSES, ISLAND_TAG_NAMES } from "./constants";
18
- import { parseMarimoIslandApps } from "./parse";
19
-
20
- const bridge = getGlobalBridge();
21
- let bootstrapPromise: Promise<void> | undefined;
22
-
23
- /**
24
- * Public entry point for the islands bundle.
25
- *
26
- * Islands auto-initialize on load. Hosts that keep the browser realm alive
27
- * across client-side navigation (retaining this worker and its Pyodide
28
- * environment) can drive page transitions with:
29
- *
30
- * ```ts
31
- * if (canReplaceApp()) {
32
- * await stopApp();
33
- * updatePage();
34
- * await initialize();
35
- * }
36
- * ```
37
- *
38
- * The retained lifecycle (`canReplaceApp`/`stopApp`) only applies to pages
39
- * whose islands resolve to a single app; multi-app pages are not replaceable.
40
- */
41
-
42
- /**
43
- * Returns whether the current document can replace its app in this worker.
44
- *
45
- * True only when the document contains exactly one replaceable app. This is a
46
- * read-only probe: it does not materialize island payloads or mutate the DOM.
47
- */
48
- export function canReplaceApp(): boolean {
49
- if (!document.querySelector(ISLAND_TAG_NAMES.ISLAND)) {
50
- return false;
51
- }
52
- return parseMarimoIslandApps(document, { materialize: false }).length === 1;
53
- }
54
-
55
- /**
56
- * Mounts marimo custom elements and starts the apps in the current document.
57
- *
58
- * Safe to call repeatedly, including after client-side navigation: the worker
59
- * bootstrap is memoized (and cleared if it fails, so a later call retries),
60
- * while app discovery re-runs on every call to pick up the current DOM.
61
- */
62
- export async function initialize(): Promise<void> {
63
- const islands = document.querySelectorAll<HTMLElement>(
64
- ISLAND_TAG_NAMES.ISLAND,
65
- );
66
- if (islands.length === 0) {
67
- return;
68
- }
69
- for (const island of islands) {
70
- island.classList.add(ISLAND_CSS_CLASSES.NAMESPACE);
71
- }
72
- bootstrapPromise ??= initializeIslands({ bridge }).catch((error: unknown) => {
73
- bootstrapPromise = undefined;
74
- throw error;
75
- });
76
- await bootstrapPromise;
77
- await bridge.initializeApps();
78
- }
79
17
 
80
18
  /**
81
- * Stops the matching active app session.
82
- *
83
- * With no `appId`, stops the current retained single-app session; with an
84
- * `appId`, stops it only if it matches the active app. No-op when there is no
85
- * retained session to stop.
19
+ * Main entry point for the js bundle for embedded marimo apps.
86
20
  */
87
- export async function stopApp(appId?: string): Promise<void> {
88
- await bridge.stopSession(appId);
21
+ export async function initialize() {
22
+ await initializeIslands({ bridge: getGlobalBridge() });
89
23
  }
90
24
 
91
25
  // Auto-initialize on module load