@marimo-team/islands 0.25.3-dev170 → 0.25.3-dev173

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 (86) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BbSXV-Xa.js → ConnectedDataExplorerComponent-Le_OBfmt.js} +9 -9
  2. package/dist/{ErrorBoundary-NE86qEWZ.js → ErrorBoundary-wT3f3iJ1.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D7n07nXf.js → abnfDiagram-VCTEODGH-Tf1ghyQQ.js} +2 -2
  4. package/dist/{any-language-editor-3E24SnCV.js → any-language-editor-B92WMzoC.js} +2 -2
  5. package/dist/{architectureDiagram-5GKGNRK7-D6CjRib5.js → architectureDiagram-5GKGNRK7-xlnbTG0T.js} +1 -1
  6. package/dist/{chat-ui-CC7GekpG.js → chat-ui-CuORr6fL.js} +70 -70
  7. package/dist/{check-SpOMoCLP.js → check-C9Mp9eCK.js} +1 -1
  8. package/dist/{chunk-IMKFNOWR-6dtPz7uS.js → chunk-IMKFNOWR-Cmd4a5jy.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-BwnFNKDA.js → chunk-SHT3W25Y-mSNp-WpO.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-D2eV4vBJ.js → chunk-SVP7TREG-CWC49D8X.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-BDs4-_-r.js → chunk-TICWLB2K-De9FINtG.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-DCJnf_8v.js → classDiagram-ZZMXUADV-DweTkYdp.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-BOmSzPBD.js → classDiagram-v2-VYDZK3BY-A6MR2TiE.js} +2 -2
  14. package/dist/{code-visibility-Bzvc6ajl.js → code-visibility-ZReaeMaF.js} +5 -5
  15. package/dist/{common-Ciw4zkh2.js → common-DCevHg1m.js} +231 -231
  16. package/dist/{copy-icon-C1trdvVt.js → copy-icon-De584pZ4.js} +2 -2
  17. package/dist/{cynefinDiagram-5FMLGOSQ-Bln_Soky.js → cynefinDiagram-5FMLGOSQ-BSvo5le-.js} +1 -1
  18. package/dist/{dagre-GXQ25YYZ-D-nf7WzW.js → dagre-GXQ25YYZ-tXRQslO6.js} +1 -1
  19. package/dist/{data-grid-overlay-editor-AQREmxI7.js → data-grid-overlay-editor-CTC-bJus.js} +1 -1
  20. package/dist/{diagram-S7CK7UJ4-PsgSMkY2.js → diagram-S7CK7UJ4-r3t5VtwI.js} +1 -1
  21. package/dist/{diagram-UQ7AKVKN-DfwfUuSw.js → diagram-UQ7AKVKN-qgbC8YnT.js} +1 -1
  22. package/dist/{diagram-VX7I27RA-C9_e-oMc.js → diagram-VX7I27RA-CiHxEwtp.js} +1 -1
  23. package/dist/{diagram-Z3DM3KII-CWLa5tuY.js → diagram-Z3DM3KII-D95u15Ev.js} +1 -1
  24. package/dist/{dialog-DgeqyrEZ.js → dialog-DVRfQuhD.js} +24 -24
  25. package/dist/{dropdown-menu-B6ftyzOg.js → dropdown-menu-DCMbbQ35.js} +691 -691
  26. package/dist/{ebnfDiagram-PWID7BFC-B1QRM58v.js → ebnfDiagram-PWID7BFC-P7e4bryK.js} +2 -2
  27. package/dist/{erDiagram-RLTQ6QDP-DJK_FqvW.js → erDiagram-RLTQ6QDP-CUHZcmRj.js} +1 -1
  28. package/dist/{error-banner-D40pmfQQ.js → error-banner-DxJ3s8YO.js} +57 -57
  29. package/dist/{error-entries-DgK3wUJR.js → error-entries-CmWMEsY_.js} +984 -961
  30. package/dist/{feedback-button-8a6SETmT.js → feedback-button-DwXkmewo.js} +29 -29
  31. package/dist/{flowDiagram-HODETNUW-Uxc25XSJ.js → flowDiagram-HODETNUW-BGZ95AzI.js} +2 -2
  32. package/dist/{formats-DQCyPd5Z.js → formats-DI7HEVv7.js} +1 -1
  33. package/dist/{glide-data-editor-B70oke_v.js → glide-data-editor-BlmcP-lw.js} +10 -10
  34. package/dist/{glide-utils-Dz_F4pAY.js → glide-utils-D98laidb.js} +2 -2
  35. package/dist/{globals-Cxd5hNlI.js → globals-BdRitZ4p.js} +3 -3
  36. package/dist/{html-to-image-D4yG5Ru0.js → html-to-image-bpiQh7ND.js} +55 -55
  37. package/dist/{infoDiagram-27XIBGKW-DmjxqwL0.js → infoDiagram-27XIBGKW-BF0YX70D.js} +1 -1
  38. package/dist/{ishikawaDiagram-5VMMS53U-NgCLHqQi.js → ishikawaDiagram-5VMMS53U-_itsAAn-.js} +1 -1
  39. package/dist/{kanban-definition-UXKFOSKX-CZaG-iam.js → kanban-definition-UXKFOSKX-VNW2Beqh.js} +1 -1
  40. package/dist/{label-D9_-zSg-.js → label-Qvw1E97W.js} +1 -1
  41. package/dist/main.js +8159 -6508
  42. package/dist/{mermaid-CSvw7c71.js → mermaid-CZ6FrClS.js} +35 -35
  43. package/dist/{mindmap-definition-YA3MSWOX-DJYQBDH_.js → mindmap-definition-YA3MSWOX-DR4ripvd.js} +1 -1
  44. package/dist/{number-overlay-editor-ChK-SKUk.js → number-overlay-editor-DQG-hFHT.js} +1 -1
  45. package/dist/{pegDiagram-XKGWAZYB-CHKO_dAe.js → pegDiagram-XKGWAZYB-CFr3XlGl.js} +2 -2
  46. package/dist/{pieDiagram-E7YTZNPT-DVevbMAo.js → pieDiagram-E7YTZNPT-j91XYSLY.js} +1 -1
  47. package/dist/{process-output-DxvBAeL3.js → process-output-BzloOI7F.js} +4 -4
  48. package/dist/{railroadDiagram-O6MQD6OU-Detj1kcB.js → railroadDiagram-O6MQD6OU-Cd47ZV9E.js} +2 -2
  49. package/dist/{requirementDiagram-BXWQKSXE-BLFD0T_Z.js → requirementDiagram-BXWQKSXE-eJH6gLtX.js} +1 -1
  50. package/dist/{reveal-component-CXMXeDZD.js → reveal-component-D3r3vSTK.js} +9 -9
  51. package/dist/{select-CkF_GBxO.js → select-2KOiReNY.js} +324 -324
  52. package/dist/{sequenceDiagram-WJ2MYXX4-BZBgatQV.js → sequenceDiagram-WJ2MYXX4-DdfFEIrm.js} +1 -1
  53. package/dist/{spec-Mnkn3XEr.js → spec-h69nC3SI.js} +4 -4
  54. package/dist/{stateDiagram-D77RDMKH-4BZW9-6_.js → stateDiagram-D77RDMKH-DkjDvip8.js} +2 -2
  55. package/dist/{stateDiagram-v2-MP3YSRHH-C4sKC8Iq.js → stateDiagram-v2-MP3YSRHH-DBHACnOo.js} +2 -2
  56. package/dist/{swimlanes-42K2YHIH-SL5yeGe2.js → swimlanes-42K2YHIH-ogImlOmp.js} +1 -1
  57. package/dist/{swimlanesDiagram-VR7AAH4N-DqPmAYUv.js → swimlanesDiagram-VR7AAH4N-D6x-zsci.js} +2 -2
  58. package/dist/{timeline-definition-24CTP7MA-BfXUMWkq.js → timeline-definition-24CTP7MA-DQpLtsHD.js} +1 -1
  59. package/dist/{toDate-TMubKIVL.js → toDate-CDzoofJo.js} +2 -2
  60. package/dist/{toast-Btbve3Lm.js → toast-B5t9o6s_.js} +47 -47
  61. package/dist/{tooltip-CJ4Rqasp.js → tooltip-s4zZxwKe.js} +67 -67
  62. package/dist/{use-restore-focus-CmGwRNId.js → use-restore-focus-BgDTd2tc.js} +1 -1
  63. package/dist/{useAsyncData-DjMGFzFy.js → useAsyncData-Bn8SxUqM.js} +1 -1
  64. package/dist/{useDateFormatter-eCL_SdeG.js → useDateFormatter-D3Tkfzej.js} +2 -2
  65. package/dist/{useDeepCompareMemoize-BQR5TJot.js → useDeepCompareMemoize-CC2tnPCk.js} +1 -1
  66. package/dist/{useLifecycle-C_GvKWbQ.js → useLifecycle-BJlxHqWa.js} +2 -2
  67. package/dist/{useTheme-Bns2gJZ6.js → useTheme-DMfQYmlG.js} +85 -85
  68. package/dist/{vega-component-BsVptuhz.js → vega-component-CovWHi60.js} +7 -7
  69. package/dist/{vennDiagram-4TSXK5OY-CH65znGa.js → vennDiagram-4TSXK5OY-BD-SL_vq.js} +1 -1
  70. package/dist/{wardleyDiagram-VM6X3IG4-z9pWKb2_.js → wardleyDiagram-VM6X3IG4-BDgJV4ah.js} +1 -1
  71. package/dist/{xychartDiagram-S5SC5T6Z-MxyaWjjq.js → xychartDiagram-S5SC5T6Z-1hIHN6Rl.js} +1 -1
  72. package/package.json +1 -1
  73. package/src/core/cells/__tests__/cells.test.ts +80 -48
  74. package/src/core/cells/__tests__/document.test.ts +37 -3
  75. package/src/core/cells/cells.ts +38 -34
  76. package/src/core/cells/document.ts +31 -2
  77. package/src/core/edit-app.tsx +7 -4
  78. package/src/core/islands/__tests__/bootstrap.test.ts +56 -0
  79. package/src/core/islands/__tests__/bridge.test.ts +42 -0
  80. package/src/core/islands/bootstrap.ts +24 -1
  81. package/src/core/islands/bridge.ts +46 -3
  82. package/src/core/kernel/__tests__/handlers.test.ts +7 -153
  83. package/src/core/kernel/handlers.ts +18 -59
  84. package/src/core/run-app.tsx +1 -7
  85. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +0 -6
  86. package/src/core/session/useNotebookSession.tsx +24 -35
@@ -9,7 +9,7 @@ import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
9
9
  import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-BwiyaNpg.js";
10
10
  import "./chunk-PWAF6VOD-CQw5qATZ.js";
11
11
  import { t as b } from "./chunk-GMAD6QVW-BhaJx0X3.js";
12
- import { p as x } from "./mermaid-CSvw7c71.js";
12
+ import { p as x } from "./mermaid-CZ6FrClS.js";
13
13
  //#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
14
14
  function S() {
15
15
  var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev170",
3
+ "version": "0.25.3-dev173",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -7,6 +7,7 @@ import { createStore } from "jotai";
7
7
  import {
8
8
  afterAll,
9
9
  beforeAll,
10
+ afterEach,
10
11
  beforeEach,
11
12
  describe,
12
13
  expect,
@@ -3591,31 +3592,9 @@ describe("createTracebackInfoAtom", () => {
3591
3592
  });
3592
3593
  });
3593
3594
 
3594
- describe("setCells snapshot preservation", () => {
3595
+ describe("kernel-ready", () => {
3595
3596
  const CELL_A = cellId("A");
3596
3597
  const CELL_B = cellId("B");
3597
- const newCells: CellData[] = [
3598
- {
3599
- id: CELL_A,
3600
- name: "a",
3601
- code: "1",
3602
- edited: false,
3603
- lastCodeRun: null,
3604
- lastExecutionTime: null,
3605
- config: { hide_code: false, disabled: false, column: null },
3606
- serializedEditorState: null,
3607
- },
3608
- {
3609
- id: CELL_B,
3610
- name: "b",
3611
- code: "2",
3612
- edited: false,
3613
- lastCodeRun: null,
3614
- lastExecutionTime: null,
3615
- config: { hide_code: false, disabled: false, column: null },
3616
- serializedEditorState: null,
3617
- },
3618
- ];
3619
3598
 
3620
3599
  const hydratedState = () =>
3621
3600
  MockNotebook.notebookState({
@@ -3645,44 +3624,97 @@ describe("setCells snapshot preservation", () => {
3645
3624
  },
3646
3625
  });
3647
3626
 
3648
- beforeEach(async () => {
3649
- const { isWasm } = await import("@/core/wasm/utils");
3650
- vi.mocked(isWasm).mockReturnValue(true);
3627
+ it("setLastRun records what the kernel last ran", () => {
3628
+ const state = hydratedState();
3629
+ const next = exportedForTesting.reducer(state, {
3630
+ type: "setLastRun",
3631
+ payload: {
3632
+ lastCodeRun: { [CELL_A]: "0" },
3633
+ lastExecutionTime: { [CELL_A]: 5 },
3634
+ },
3635
+ });
3636
+
3637
+ expect(next.cellData[CELL_A]).toMatchObject({
3638
+ lastCodeRun: "0",
3639
+ lastExecutionTime: 5,
3640
+ edited: true,
3641
+ });
3642
+ expect(next.cellData[CELL_B]).toMatchObject({
3643
+ lastCodeRun: null,
3644
+ lastExecutionTime: null,
3645
+ edited: false,
3646
+ });
3647
+ expect(next.cellRuntime).toBe(state.cellRuntime);
3651
3648
  });
3652
3649
 
3653
- it("preserves hydrated output in WASM", () => {
3650
+ it("resetCellRuntimes starts every cell's runtime over", () => {
3654
3651
  const next = exportedForTesting.reducer(hydratedState(), {
3655
- type: "setCells",
3656
- payload: newCells,
3652
+ type: "resetCellRuntimes",
3653
+ payload: undefined,
3657
3654
  });
3658
3655
 
3659
- expect(next.cellRuntime[CELL_A].output).toMatchObject({
3660
- data: "hydrated-A",
3656
+ expect(next.cellRuntime[CELL_A].output).toBeNull();
3657
+ expect(next.cellRuntime[CELL_B].consoleOutputs).toEqual([]);
3658
+ });
3659
+
3660
+ it("resetCellRuntimes starts the scratch cell over too", () => {
3661
+ const state = MockNotebook.notebookState({
3662
+ cellData: { [CELL_A]: { id: CELL_A, code: "1" } },
3663
+ cellRuntime: {
3664
+ [SCRATCH_CELL_ID]: {
3665
+ output: {
3666
+ channel: "output",
3667
+ mimetype: "text/plain",
3668
+ data: "scratch",
3669
+ timestamp: 0,
3670
+ },
3671
+ },
3672
+ },
3673
+ });
3674
+ const next = exportedForTesting.reducer(state, {
3675
+ type: "resetCellRuntimes",
3676
+ payload: undefined,
3661
3677
  });
3678
+
3679
+ expect(next.cellRuntime[SCRATCH_CELL_ID].output).toBeNull();
3662
3680
  });
3663
3681
 
3664
- it("preserves console-only hydration in WASM", () => {
3665
- const next = exportedForTesting.reducer(hydratedState(), {
3666
- type: "setCells",
3667
- payload: newCells,
3682
+ describe("resetCellRuntimes in WASM", () => {
3683
+ beforeEach(async () => {
3684
+ const { isWasm } = await import("@/core/wasm/utils");
3685
+ vi.mocked(isWasm).mockReturnValue(true);
3668
3686
  });
3669
3687
 
3670
- expect(next.cellRuntime[CELL_B].consoleOutputs).toHaveLength(1);
3671
- expect(next.cellRuntime[CELL_B].consoleOutputs[0]).toMatchObject({
3672
- data: "hydrated-B-stdout",
3688
+ afterEach(async () => {
3689
+ const { isWasm } = await import("@/core/wasm/utils");
3690
+ vi.mocked(isWasm).mockReturnValue(false);
3673
3691
  });
3674
- });
3675
3692
 
3676
- it("resets cells with no prior runtime even in WASM", () => {
3677
- const empty = MockNotebook.notebookState({ cellData: {} });
3678
- const next = exportedForTesting.reducer(empty, {
3679
- type: "setCells",
3680
- payload: newCells,
3693
+ it("keeps hydrated outputs and console", () => {
3694
+ const next = exportedForTesting.reducer(hydratedState(), {
3695
+ type: "resetCellRuntimes",
3696
+ payload: undefined,
3697
+ });
3698
+
3699
+ expect(next.cellRuntime[CELL_A].output).toMatchObject({
3700
+ data: "hydrated-A",
3701
+ });
3702
+ expect(next.cellRuntime[CELL_B].consoleOutputs[0]).toMatchObject({
3703
+ data: "hydrated-B-stdout",
3704
+ });
3681
3705
  });
3682
3706
 
3683
- expect(next.cellRuntime[CELL_A].output).toBeNull();
3684
- expect(next.cellRuntime[CELL_A].consoleOutputs).toEqual([]);
3685
- expect(next.cellRuntime[CELL_B].output).toBeNull();
3686
- expect(next.cellRuntime[CELL_B].consoleOutputs).toEqual([]);
3707
+ it("starts cells with no prior runtime over", () => {
3708
+ const state = MockNotebook.notebookState({
3709
+ cellData: { [CELL_A]: { id: CELL_A, code: "1" } },
3710
+ });
3711
+ const next = exportedForTesting.reducer(state, {
3712
+ type: "resetCellRuntimes",
3713
+ payload: undefined,
3714
+ });
3715
+
3716
+ expect(next.cellRuntime[CELL_A].output).toBeNull();
3717
+ expect(next.cellRuntime[CELL_A].consoleOutputs).toEqual([]);
3718
+ });
3687
3719
  });
3688
3720
  });
@@ -29,7 +29,12 @@ import {
29
29
  type NotebookState,
30
30
  notebookReducer,
31
31
  } from "../cells";
32
- import { applyDocument, flushDocument, withDocumentSave } from "../document";
32
+ import {
33
+ applyDocument,
34
+ flushDocument,
35
+ followDocument,
36
+ withDocumentSave,
37
+ } from "../document";
33
38
  import { type CellId, SCRATCH_CELL_ID } from "../ids";
34
39
 
35
40
  vi.mock("../../rtc/state", async (importOriginal) => ({
@@ -349,9 +354,14 @@ describe("documentEditMiddleware", () => {
349
354
  expect(calls).toEqual([["updateConfig", c, { column: null }]]);
350
355
  });
351
356
 
352
- it("ignores the kernel's cells and runtime state", () => {
357
+ it("ignores the document and the kernel's run state", () => {
353
358
  actions.prepareForRun({ cellId: a });
354
- actions.setCells([state.cellData[a]]);
359
+ actions.setLastRun({
360
+ lastCodeRun: { [a]: "a = 0" },
361
+ lastExecutionTime: {},
362
+ });
363
+ actions.resetCellRuntimes();
364
+ actions.reconcileDocument(documentOf([cell(a)]));
355
365
 
356
366
  expect(calls).toEqual([]);
357
367
  });
@@ -367,6 +377,30 @@ describe("documentEditMiddleware", () => {
367
377
  });
368
378
  });
369
379
 
380
+ describe("followDocument", () => {
381
+ it("reconciles the document now and on each change, until aborted", () => {
382
+ const documentAtom = atom<EditableDocument | null>(null);
383
+ const reconcile = vi.fn();
384
+ const controller = new AbortController();
385
+
386
+ followDocument({
387
+ store,
388
+ model: { documentAtom },
389
+ reconcile,
390
+ signal: controller.signal,
391
+ });
392
+ expect(reconcile).not.toHaveBeenCalled();
393
+
394
+ const first = documentOf([cell(a)]);
395
+ store.set(documentAtom, first);
396
+ expect(reconcile).toHaveBeenCalledWith(first);
397
+
398
+ controller.abort();
399
+ store.set(documentAtom, documentOf([cell(b)]));
400
+ expect(reconcile).toHaveBeenCalledOnce();
401
+ });
402
+ });
403
+
370
404
  describe("flushing before host commands", () => {
371
405
  const model = {
372
406
  syncStatusAtom: atom<DocumentSyncStatus>({ status: "idle" }),
@@ -44,6 +44,7 @@ import {
44
44
  createCell,
45
45
  createCellConfig,
46
46
  createCellRuntimeState,
47
+ isEdited,
47
48
  } from "./types";
48
49
  import {
49
50
  canUndoDeletes,
@@ -936,12 +937,7 @@ const {
936
937
  cellLogs: [...state.cellLogs, ...action.logs],
937
938
  };
938
939
  },
939
- /**
940
- * Follow the host document's cells and order.
941
- *
942
- * Dispatched without middleware, since the document already has these
943
- * changes.
944
- */
940
+ /** Follow the host document's cells and order. */
945
941
  reconcileDocument: (state, document: EditableDocument) => {
946
942
  const nextState = applyDocument(state, document);
947
943
  for (const cellId of state.cellIds.inOrderIds) {
@@ -985,36 +981,44 @@ const {
985
981
  },
986
982
  });
987
983
  },
988
- setCells: (state, cells: CellData[]) => {
989
- const cellData = Object.fromEntries(cells.map((cell) => [cell.id, cell]));
990
-
991
- // WASM has no server-side SessionView to replay outputs, so the
992
- // snapshot hydrated by notebookStateFromSession is the only source.
984
+ /** Record what the kernel last ran for each cell. */
985
+ setLastRun: (
986
+ state,
987
+ action: {
988
+ lastCodeRun: Record<CellId, string>;
989
+ lastExecutionTime: Record<CellId, number>;
990
+ },
991
+ ) => {
992
+ const cellData = { ...state.cellData };
993
+ for (const cellId of state.cellIds.inOrderIds) {
994
+ const cell = state.cellData[cellId];
995
+ const lastCodeRun = action.lastCodeRun[cellId] ?? null;
996
+ cellData[cellId] = {
997
+ ...cell,
998
+ lastCodeRun,
999
+ lastExecutionTime: action.lastExecutionTime[cellId] ?? null,
1000
+ // A cell the kernel never ran is not stale on load.
1001
+ edited: lastCodeRun !== null && isEdited(lastCodeRun, cell.code),
1002
+ };
1003
+ }
1004
+ return { ...state, cellData };
1005
+ },
1006
+ /**
1007
+ * Start each cell's runtime over for a new kernel. WASM keeps the outputs
1008
+ * its mount-time snapshot gave it.
1009
+ */
1010
+ resetCellRuntimes: (state) => {
993
1011
  const preserveSnapshot = isWasm();
994
- const runtimeFor = (cellId: CellId): CellRuntimeState => {
995
- if (!preserveSnapshot) {
996
- return createCellRuntimeState();
997
- }
998
- const prev = state.cellRuntime[cellId];
1012
+ const cellRuntime = { ...state.cellRuntime };
1013
+ for (const cellId of [...state.cellIds.inOrderIds, SCRATCH_CELL_ID]) {
1014
+ const previous = state.cellRuntime[cellId];
999
1015
  const hasSnapshot =
1000
- prev && (prev.output != null || prev.consoleOutputs.length > 0);
1001
- return hasSnapshot ? prev : createCellRuntimeState();
1002
- };
1003
- const cellRuntime = Object.fromEntries(
1004
- cells.map((cell) => [cell.id, runtimeFor(cell.id)]),
1005
- );
1006
-
1007
- return withScratchCell({
1008
- ...state,
1009
- cellIds: MultiColumn.fromIdsAndColumns(
1010
- cells.map((cell) => [cell.id, cell.config.column]),
1011
- ),
1012
- cellData: cellData,
1013
- cellRuntime: cellRuntime,
1014
- cellHandles: Object.fromEntries(
1015
- cells.map((cell) => [cell.id, createRef()]),
1016
- ),
1017
- });
1016
+ preserveSnapshot &&
1017
+ previous !== undefined &&
1018
+ (previous.output != null || previous.consoleOutputs.length > 0);
1019
+ cellRuntime[cellId] = hasSnapshot ? previous : createCellRuntimeState();
1020
+ }
1021
+ return { ...state, cellRuntime };
1018
1022
  },
1019
1023
  /**
1020
1024
  * Move focus to next cell
@@ -10,11 +10,13 @@
10
10
 
11
11
  import type * as Protocol from "@marimo-team/client/protocol";
12
12
  import { dequal as isEqual } from "dequal";
13
+ import type { createStore } from "jotai";
13
14
  import { createRef, type RefObject } from "react";
14
15
  import type { CellHandle } from "@/components/editor/notebook-cell";
15
16
  import type {
16
17
  DocumentEdit,
17
18
  DocumentEditor,
19
+ DocumentModel,
18
20
  EditableCell,
19
21
  EditableDocument,
20
22
  } from "@/core/notebook/document";
@@ -165,6 +167,33 @@ export function applyDocument(
165
167
  return { ...state, cellIds, cellData, cellRuntime, cellHandles };
166
168
  }
167
169
 
170
+ /**
171
+ * Reconcile the model's document now and on each change, until the signal aborts.
172
+ *
173
+ * TODO(MO-8028): Delete with `applyDocument`.
174
+ */
175
+ export function followDocument({
176
+ store,
177
+ model,
178
+ reconcile,
179
+ signal,
180
+ }: {
181
+ store: ReturnType<typeof createStore>;
182
+ model: Pick<DocumentModel, "documentAtom">;
183
+ reconcile: (document: EditableDocument) => void;
184
+ signal: AbortSignal;
185
+ }): void {
186
+ const follow = () => {
187
+ const document = store.get(model.documentAtom);
188
+ if (document !== null) {
189
+ reconcile(document);
190
+ }
191
+ };
192
+ const unsubscribe = store.sub(model.documentAtom, follow);
193
+ signal.addEventListener("abort", unsubscribe, { once: true });
194
+ follow();
195
+ }
196
+
168
197
  // Editing the document ------------------------------------------------------
169
198
 
170
199
  const CONFIG_FIELDS = ["disabled", "hide_code", "expand_output"] as const;
@@ -291,8 +320,8 @@ export function documentEditMiddleware(
291
320
  next: NotebookState,
292
321
  action: CellAction,
293
322
  ): void {
294
- // The kernel's cells replace the notebook rather than edit it.
295
- if (action.type === "setCells") {
323
+ // The document replaces the notebook's cells, so following it is no edit.
324
+ if (action.type === "reconcileDocument") {
296
325
  return;
297
326
  }
298
327
  if (prev.cellIds === next.cellIds && prev.cellData === next.cellData) {
@@ -5,7 +5,7 @@ import { Tooltip } from "radix-ui";
5
5
 
6
6
  const TooltipProvider = Tooltip.Provider;
7
7
 
8
- import { useAtomValue, useSetAtom } from "jotai";
8
+ import { useAtomValue, useSetAtom, useStore } from "jotai";
9
9
  import { useEffect } from "react";
10
10
  import { NotStartedConnectionAlert } from "@/components/editor/alerts/connecting-alert";
11
11
  import { Controls } from "@/components/editor/controls/Controls";
@@ -29,10 +29,12 @@ import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
29
29
  import { useHotkey } from "../hooks/useHotkey";
30
30
  import {
31
31
  hasCellsAtom,
32
+ notebookAtom,
32
33
  notebookIsRunningAtom,
33
34
  numColumnsAtom,
34
35
  useCellActions,
35
36
  } from "./cells/cells";
37
+ import { notebookCells } from "./cells/utils";
36
38
  import type { AppConfig, UserConfig } from "./config/config-schema";
37
39
  import { DocumentProvider } from "./notebook/document";
38
40
  import { RuntimeState } from "./kernel/RuntimeState";
@@ -64,8 +66,9 @@ export const EditApp: React.FC<AppProps> = ({
64
66
  appConfig,
65
67
  hideControls = false,
66
68
  }) => {
67
- const { setCells, mergeAllColumns, collapseAllCells, expandAllCells } =
69
+ const { mergeAllColumns, collapseAllCells, expandAllCells } =
68
70
  useCellActions();
71
+ const store = useStore();
69
72
  const viewState = useAtomValue(viewStateAtom);
70
73
  const numColumns = useAtomValue(numColumnsAtom);
71
74
  const filename = useFilename();
@@ -86,8 +89,8 @@ export const EditApp: React.FC<AppProps> = ({
86
89
 
87
90
  const { connection, reconnect, documentModel } = useNotebookSession({
88
91
  autoInstantiate: userConfig.runtime.auto_instantiate,
89
- setCells: (cells, layout) => {
90
- setCells(cells);
92
+ onKernelReady: (layout) => {
93
+ const cells = notebookCells(store.get(notebookAtom));
91
94
  const names = cells.map((cell) => cell.name);
92
95
  const codes = cells.map((cell) => cell.code);
93
96
  const configs = cells.map((cell) => cell.config);
@@ -1,7 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { NotebookTransport } from "@marimo-team/client";
3
4
  import { createStore } from "jotai";
4
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { cellId } from "@/__tests__/branded";
7
+ import { notebookAtom } from "@/core/cells/cells";
8
+ import {
9
+ fakeBackend,
10
+ fixtureMessages,
11
+ } from "@/core/notebook/replica/__tests__/fixtures";
5
12
  import { RuntimeState } from "@/core/kernel/RuntimeState";
6
13
  import { initializeIslands } from "../bootstrap";
7
14
  import type { IslandsPyodideBridge } from "../bridge";
@@ -32,6 +39,7 @@ describe("initializeIslands", () => {
32
39
  consumeMessage = consumer;
33
40
  },
34
41
  sendComponentValues: vi.fn(),
42
+ notebookBackend: fakeBackend(),
35
43
  } as unknown as IslandsPyodideBridge;
36
44
  const store = createStore();
37
45
  vi.spyOn(RuntimeState.INSTANCE, "start").mockImplementation(
@@ -59,3 +67,51 @@ describe("initializeIslands", () => {
59
67
  expect(store.get(islandsHydratedAtom)).toBe(true);
60
68
  });
61
69
  });
70
+
71
+ describe("initializeIslands document", () => {
72
+ afterEach(vi.restoreAllMocks);
73
+
74
+ it("fills the notebook's cells from the managed session's host document", async () => {
75
+ const root = document.createElement("div");
76
+ root.innerHTML =
77
+ '<marimo-island data-app-id="app-1" data-reactive="true"></marimo-island>';
78
+ let handlers: Parameters<NotebookTransport>[1] | undefined;
79
+ const transport: NotebookTransport = (_attempt, opened) =>
80
+ new Promise(() => {
81
+ handlers = opened;
82
+ opened.open();
83
+ });
84
+ const bridge = {
85
+ consumeNotifications: vi.fn(),
86
+ sendComponentValues: vi.fn(),
87
+ notebookBackend: fakeBackend({ transport }),
88
+ } as unknown as IslandsPyodideBridge;
89
+ const store = createStore();
90
+ vi.spyOn(RuntimeState.INSTANCE, "start").mockImplementation(
91
+ () => undefined,
92
+ );
93
+
94
+ await initializeIslands({
95
+ bridge,
96
+ store,
97
+ root,
98
+ autoInitializePlugins: false,
99
+ });
100
+ await vi.waitFor(() => expect(handlers).toBeDefined());
101
+ for (const message of fixtureMessages("notebook-events.sse")) {
102
+ handlers?.message(message);
103
+ }
104
+
105
+ const notebook = store.get(notebookAtom);
106
+ expect(notebook.cellIds.inOrderIds).toEqual(["c1", "c3", "k7f2"]);
107
+ expect(notebook.cellData[cellId("c3")].code).toBe("x = 21");
108
+ expect(notebook.cellRuntime[cellId("c3")]).toBeDefined();
109
+
110
+ handlers?.message({ event: "reset", data: {} });
111
+ expect(store.get(notebookAtom).cellIds.inOrderIds).toEqual([
112
+ "c1",
113
+ "c3",
114
+ "k7f2",
115
+ ]);
116
+ });
117
+ });
@@ -396,6 +396,48 @@ describe("IslandsPyodideBridge", () => {
396
396
  expect(consumeMessage.mock.calls).toEqual([notification]);
397
397
  });
398
398
 
399
+ it("forwards the managed session's host events to the notebook stream", async () => {
400
+ await initializeSingleApp();
401
+ const handlers = { open: vi.fn(), message: vi.fn() };
402
+ void bridge.notebookBackend.transport(
403
+ { signal: new AbortController().signal, cursor: null },
404
+ handlers,
405
+ );
406
+ expect(handlers.open).toHaveBeenCalledOnce();
407
+
408
+ const cell = { event: "cell", id: "1", data: { cell: { id: "c1" } } };
409
+ sendNotebookEvent(1, cell);
410
+ sendNotebookEvent(2, { event: "ready", id: "2", data: {} });
411
+ sendKernelMessage(1);
412
+
413
+ expect(handlers.message.mock.calls).toEqual([[cell]]);
414
+ expect(() => bridge.notebookBackend.notebook("n1")).toThrow(
415
+ "Islands do not edit",
416
+ );
417
+ });
418
+
419
+ it("resets the notebook stream when the managed session is replaced or stopped", async () => {
420
+ await initializeSingleApp();
421
+ const handlers = { open: vi.fn(), message: vi.fn() };
422
+ void bridge.notebookBackend.transport(
423
+ { signal: new AbortController().signal, cursor: null },
424
+ handlers,
425
+ );
426
+
427
+ mockSingleApp("generated app 2");
428
+ await bridge.initializeApps();
429
+ const ready = { event: "ready", id: "2", data: {} };
430
+ sendNotebookEvent(1, { event: "ready", id: "1", data: {} });
431
+ sendNotebookEvent(2, ready);
432
+ await bridge.stopSession("app-1");
433
+
434
+ expect(handlers.message.mock.calls).toEqual([
435
+ [{ event: "reset", data: {} }],
436
+ [ready],
437
+ [{ event: "reset", data: {} }],
438
+ ]);
439
+ });
440
+
399
441
  it("holds controls until the first app session is ready", async () => {
400
442
  setSingleApp();
401
443
 
@@ -17,10 +17,14 @@ import {
17
17
  notebookAtom,
18
18
  notebookReducer,
19
19
  } from "../cells/cells";
20
+ import { followDocument } from "../cells/document";
21
+ import { notebookCells } from "../cells/utils";
22
+ import { createDocumentModel } from "../notebook/document";
20
23
  import { defineCustomElement } from "../dom/defineCustomElement";
21
24
  import { MarimoValueInputEvent } from "../dom/events";
22
25
  import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
23
26
  import { FUNCTIONS_REGISTRY } from "../functions/FunctionRegistry";
27
+ import { createNotebookReplica } from "../notebook/replica";
24
28
  import {
25
29
  handleCellNotificationeration,
26
30
  handleKernelReady,
@@ -124,6 +128,23 @@ export async function initializeIslands(
124
128
  handleNotification(notification, actions, store, sessionGeneration);
125
129
  });
126
130
 
131
+ // The notebook's cells follow the managed session's host document.
132
+ const replica = createNotebookReplica(store);
133
+ replica.connect(bridge.notebookBackend, { onNotification: Functions.NOOP });
134
+ const documentModel = createDocumentModel({
135
+ store,
136
+ documentAtom: replica.documentAtom,
137
+ backendAtom: replica.backendAtom,
138
+ });
139
+ const lifetime = new AbortController();
140
+ documentModel.start({ signal: lifetime.signal });
141
+ followDocument({
142
+ store,
143
+ model: documentModel,
144
+ reconcile: actions.reconcileDocument,
145
+ signal: lifetime.signal,
146
+ });
147
+
127
148
  // Track first user interaction before initialization completes
128
149
  document.addEventListener(
129
150
  MarimoValueInputEvent.TYPE,
@@ -196,7 +217,9 @@ function handleNotification(
196
217
  case "kernel-ready":
197
218
  handleKernelReady(msg, {
198
219
  autoInstantiate: true,
199
- setCells: actions.setCells,
220
+ cells: notebookCells(store.get(notebookAtom)),
221
+ setLastRun: actions.setLastRun,
222
+ resetCellRuntimes: actions.resetCellRuntimes,
200
223
  setLayoutData: Functions.NOOP,
201
224
  setAppConfig: Functions.NOOP,
202
225
  setCapabilities: Functions.NOOP,