@marimo-team/islands 0.25.3-dev191 → 0.25.3-dev193

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 (154) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BBbOElk3.js → ConnectedDataExplorerComponent-BRuwLOIx.js} +9 -9
  2. package/dist/{ErrorBoundary--X0z83Yx.js → ErrorBoundary-BVu_xT-p.js} +2 -2
  3. package/dist/{abnfDiagram-VCTEODGH-DhMT6Uhd.js → abnfDiagram-VCTEODGH-LlJTOOU_.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-BVbEiJLq.js → architectureDiagram-5GKGNRK7-9PQtGLLZ.js} +1 -1
  5. package/dist/{arrays-R6JsImiT.js → arrays-DA6KkLfq.js} +6 -12
  6. package/dist/assets/__vite-browser-external-BCJsx8QS.js +1 -0
  7. package/dist/assets/{worker-C6e9ERyd.js → worker-DodjWT_Z.js} +2 -2
  8. package/dist/{capabilities-CI4xUPsp.js → capabilities-CvWU-ey_.js} +1 -1
  9. package/dist/{chat-ui-BguxTQEL.js → chat-ui-Bl2euMBD.js} +93 -93
  10. package/dist/{chunk-IMKFNOWR-By5hSbJJ.js → chunk-IMKFNOWR-DcXv-BD5.js} +1 -1
  11. package/dist/{chunk-SHT3W25Y-DFrEYG54.js → chunk-SHT3W25Y-VwnELN23.js} +1 -1
  12. package/dist/{chunk-SVP7TREG-DFXGc9ew.js → chunk-SVP7TREG-DgQpI5Zi.js} +1 -1
  13. package/dist/{chunk-TICWLB2K-DZdNIpoZ.js → chunk-TICWLB2K-CTg6VTJT.js} +1 -1
  14. package/dist/{classDiagram-ZZMXUADV-Cgj_eEQA.js → classDiagram-ZZMXUADV-UzmcFrnW.js} +2 -2
  15. package/dist/{classDiagram-v2-VYDZK3BY-DLAUrSqj.js → classDiagram-v2-VYDZK3BY-DhjiI6IQ.js} +2 -2
  16. package/dist/{code-visibility-uDyLo9_i.js → code-visibility-BTVimCUw.js} +38 -42
  17. package/dist/{common-DWKY45Md.js → common-fTgq7gO-.js} +849 -849
  18. package/dist/{cynefinDiagram-5FMLGOSQ-Cikf9AKa.js → cynefinDiagram-5FMLGOSQ-O4ShIbJX.js} +1 -1
  19. package/dist/{dagre-GXQ25YYZ-C5KEi5_l.js → dagre-GXQ25YYZ-dtHhhcPu.js} +1 -1
  20. package/dist/{data-grid-overlay-editor-CRnYWD1L.js → data-grid-overlay-editor-DAZZkLWg.js} +1 -1
  21. package/dist/{diagram-S7CK7UJ4-BaoBTQjR.js → diagram-S7CK7UJ4-ppPYVZ6W.js} +1 -1
  22. package/dist/{diagram-UQ7AKVKN-DDufBsUA.js → diagram-UQ7AKVKN-CMZDf--k.js} +1 -1
  23. package/dist/{diagram-VX7I27RA-CY6RALJ8.js → diagram-VX7I27RA-CFvJdx-C.js} +1 -1
  24. package/dist/{diagram-Z3DM3KII-Bc-LHsD6.js → diagram-Z3DM3KII-Dt6_uFbv.js} +1 -1
  25. package/dist/{dropdown-menu-drDjxJyV.js → dropdown-menu-CeaOsjWZ.js} +2 -2
  26. package/dist/{ebnfDiagram-PWID7BFC-Cz0J-tcC.js → ebnfDiagram-PWID7BFC-DpRFrKK-.js} +2 -2
  27. package/dist/{erDiagram-RLTQ6QDP-DTXTDgFu.js → erDiagram-RLTQ6QDP-CqAU4b0u.js} +1 -1
  28. package/dist/{error-entries-CZ1NQ6Kv.js → error-entries-CYGwr5-O.js} +5596 -6012
  29. package/dist/feedback-button-DzFFn-n4.js +374 -0
  30. package/dist/{flowDiagram-HODETNUW-RbwWf2zc.js → flowDiagram-HODETNUW-i8-Gw8HO.js} +2 -2
  31. package/dist/{formats-DbXnijtH.js → formats-BtoHHy42.js} +1 -1
  32. package/dist/{glide-data-editor-CvRiA49N.js → glide-data-editor-DFsqVbWR.js} +5 -5
  33. package/dist/{globals-BR4CmGOS.js → globals-Dr0MRANu.js} +2 -2
  34. package/dist/{html-to-image-D_4_hQ8U.js → html-to-image-Cy5UDpQJ.js} +56 -56
  35. package/dist/{infoDiagram-27XIBGKW-DlmzZjmI.js → infoDiagram-27XIBGKW-DpfOKdLL.js} +1 -1
  36. package/dist/{ishikawaDiagram-5VMMS53U-EN3TYP_q.js → ishikawaDiagram-5VMMS53U-Dg7mrMG7.js} +1 -1
  37. package/dist/{kanban-definition-UXKFOSKX-CK_-Ru7Q.js → kanban-definition-UXKFOSKX-QB6e3Jrj.js} +1 -1
  38. package/dist/main.js +5875 -5876
  39. package/dist/{mermaid-DU9ZRmkG.js → mermaid-CeWG4Egr.js} +48 -48
  40. package/dist/{mindmap-definition-YA3MSWOX-C-hqgUvF.js → mindmap-definition-YA3MSWOX-BTz3uTGe.js} +1 -1
  41. package/dist/{number-overlay-editor-DiYGbxNa.js → number-overlay-editor-DupOSYHx.js} +1 -1
  42. package/dist/{once-eG8Vmvjd.js → once-DWGO2mQ2.js} +1 -1
  43. package/dist/{pegDiagram-XKGWAZYB-BiMejYPB.js → pegDiagram-XKGWAZYB-3ZIEa3mB.js} +2 -2
  44. package/dist/{pieDiagram-E7YTZNPT-B1zCZ1eq.js → pieDiagram-E7YTZNPT-BJAlk046.js} +1 -1
  45. package/dist/{process-output-DxSCHBWp.js → process-output-q9MTAIMX.js} +2 -2
  46. package/dist/{railroadDiagram-O6MQD6OU-BLcfaASl.js → railroadDiagram-O6MQD6OU-Dn3bsSRa.js} +2 -2
  47. package/dist/{requirementDiagram-BXWQKSXE-DtBOf52-.js → requirementDiagram-BXWQKSXE-wcOmQQCL.js} +1 -1
  48. package/dist/{reveal-component-D3DsXPIm.js → reveal-component-Bg8rP_Uo.js} +17 -17
  49. package/dist/{sequenceDiagram-WJ2MYXX4-DIwF19_n.js → sequenceDiagram-WJ2MYXX4-DSh_qxoF.js} +1 -1
  50. package/dist/{spec-DZDnnlgT.js → spec-C7Fos-If.js} +1 -1
  51. package/dist/{stateDiagram-D77RDMKH-sWk1AdYL.js → stateDiagram-D77RDMKH-DH_H8NMH.js} +2 -2
  52. package/dist/{stateDiagram-v2-MP3YSRHH-CiNo-OI7.js → stateDiagram-v2-MP3YSRHH-D_JM7MEK.js} +2 -2
  53. package/dist/style.css +1 -1
  54. package/dist/{swimlanes-42K2YHIH-Bj3hiOY5.js → swimlanes-42K2YHIH-CfoTJO-9.js} +1 -1
  55. package/dist/{swimlanesDiagram-VR7AAH4N-BzEo2r_N.js → swimlanesDiagram-VR7AAH4N-Z_QhvK0G.js} +2 -2
  56. package/dist/{swiper-component-BkCm-kLg.js → swiper-component-D2geL1Ku.js} +1 -1
  57. package/dist/{timeline-definition-24CTP7MA-CrPQk42H.js → timeline-definition-24CTP7MA-BFtdScaL.js} +1 -1
  58. package/dist/{toDate-Cu2pIZcC.js → toDate-CQAFSwos.js} +24 -24
  59. package/dist/{useAsyncData-DEhLdRIu.js → useAsyncData-BeyZyjso.js} +1 -1
  60. package/dist/{useDeepCompareMemoize-RjZGVhBV.js → useDeepCompareMemoize-DOQgae0W.js} +1 -1
  61. package/dist/{useIframeCapabilities-CSdWn6NP.js → useIframeCapabilities-DNvaj9wM.js} +1 -1
  62. package/dist/{useLifecycle-DtcB2s1z.js → useLifecycle-n2lnCFS8.js} +1 -1
  63. package/dist/{useTheme-CoEtr4mK.js → useTheme-CY777LXN.js} +303 -99
  64. package/dist/{vega-component-BhjTrP9-.js → vega-component-0nQzA-I8.js} +6 -6
  65. package/dist/{vennDiagram-4TSXK5OY-BaYMoIpj.js → vennDiagram-4TSXK5OY-BXY8IGCA.js} +1 -1
  66. package/dist/{wardleyDiagram-VM6X3IG4-DjHeuj8p.js → wardleyDiagram-VM6X3IG4-D0tFe6eC.js} +1 -1
  67. package/dist/{xychartDiagram-S5SC5T6Z-Bc_hcuXS.js → xychartDiagram-S5SC5T6Z-Dx8wFM-y.js} +1 -1
  68. package/package.json +1 -1
  69. package/src/__mocks__/notebook.ts +0 -1
  70. package/src/__mocks__/partial.ts +26 -0
  71. package/src/__tests__/main.test.tsx +24 -1
  72. package/src/components/app-config/app-config-form.tsx +5 -9
  73. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -1
  74. package/src/components/editor/__tests__/output-persistence.test.tsx +0 -1
  75. package/src/components/editor/actions/useCellActionButton.tsx +6 -5
  76. package/src/components/editor/actions/useConfigActions.tsx +3 -4
  77. package/src/components/editor/actions/useNotebookActions.tsx +5 -4
  78. package/src/components/editor/cell/code/cell-editor.tsx +13 -6
  79. package/src/components/editor/cell/collapse.tsx +6 -1
  80. package/src/components/editor/chrome/components/feedback-button.tsx +1 -7
  81. package/src/components/editor/controls/Controls.tsx +1 -1
  82. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +8 -6
  83. package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -15
  84. package/src/components/editor/navigation/__tests__/selection.test.ts +44 -21
  85. package/src/components/editor/navigation/focus-utils.ts +19 -0
  86. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +6 -13
  87. package/src/components/editor/navigation/navigation.ts +52 -43
  88. package/src/components/editor/navigation/selection.ts +30 -29
  89. package/src/components/editor/notebook-cell.tsx +10 -18
  90. package/src/components/editor/renderers/cell-array.tsx +6 -5
  91. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  92. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  93. package/src/components/pages/edit-page.tsx +0 -13
  94. package/src/components/scratchpad/scratchpad.tsx +0 -1
  95. package/src/components/slides/reveal-component.tsx +1 -1
  96. package/src/components/slides/slide-cell-view.tsx +0 -1
  97. package/src/components/sort/SortableCellsProvider.tsx +6 -5
  98. package/src/core/MarimoApp.tsx +1 -1
  99. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -37
  100. package/src/core/cells/__tests__/actions.test.ts +190 -80
  101. package/src/core/cells/__tests__/arrange.test.ts +263 -0
  102. package/src/core/cells/__tests__/cells.test.ts +11 -1101
  103. package/src/core/cells/__tests__/document.test.ts +3 -32
  104. package/src/core/cells/__tests__/editor.test.ts +57 -0
  105. package/src/core/cells/__tests__/harness.ts +82 -0
  106. package/src/core/cells/__tests__/layout.test.ts +189 -0
  107. package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
  108. package/src/core/cells/__tests__/session.test.ts +329 -174
  109. package/src/core/cells/__tests__/utils.test.ts +0 -1
  110. package/src/core/cells/actions.ts +247 -82
  111. package/src/core/cells/arrange.ts +341 -0
  112. package/src/core/cells/cells.ts +27 -516
  113. package/src/core/cells/document.ts +6 -5
  114. package/src/core/cells/edit.ts +30 -0
  115. package/src/core/cells/editor.ts +82 -0
  116. package/src/core/cells/layout.ts +152 -0
  117. package/src/core/cells/session.ts +75 -52
  118. package/src/core/cells/types.ts +0 -5
  119. package/src/core/cells/utils.ts +6 -4
  120. package/src/core/config/__tests__/app-config.test.ts +64 -0
  121. package/src/core/config/__tests__/config-schema.test.ts +1 -1
  122. package/src/core/config/config-schema.ts +6 -2
  123. package/src/core/config/config.ts +26 -12
  124. package/src/core/islands/bootstrap.ts +0 -1
  125. package/src/core/kernel/handlers.ts +1 -13
  126. package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
  127. package/src/core/meta/code-visibility.ts +18 -9
  128. package/src/core/notebook/__tests__/operations.test.ts +51 -0
  129. package/src/core/notebook/document/__tests__/codemirror.test.ts +15 -0
  130. package/src/core/notebook/index.ts +2 -1
  131. package/src/core/notebook/operations.ts +30 -0
  132. package/src/core/saving/__tests__/state.test.ts +111 -0
  133. package/src/core/saving/save-component.tsx +19 -33
  134. package/src/core/saving/state.ts +38 -39
  135. package/src/core/saving/useAutoSave.ts +8 -35
  136. package/src/core/session/__tests__/backend.test.ts +1 -0
  137. package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +1 -0
  138. package/src/core/session/active-notebook.ts +11 -0
  139. package/src/core/session/backend.ts +17 -19
  140. package/src/core/session/embedded.ts +7 -0
  141. package/src/core/session/useNotebookReplica.ts +3 -1
  142. package/src/core/session/useNotebookSession.tsx +7 -8
  143. package/src/core/transports/__tests__/static.test.ts +139 -8
  144. package/src/core/transports/static.ts +98 -16
  145. package/src/core/wasm/__tests__/bridge.test.ts +1 -0
  146. package/src/mount.tsx +19 -10
  147. package/src/stories/cell.stories.tsx +0 -2
  148. package/src/stories/layout/vertical/one-column.stories.tsx +0 -2
  149. package/src/utils/__tests__/id-tree.test.ts +20 -1548
  150. package/src/utils/id-tree.tsx +12 -537
  151. package/dist/assets/__vite-browser-external-C8gQho07.js +0 -1
  152. package/dist/feedback-button-CDf1yRm_.js +0 -379
  153. package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
  154. package/src/components/editor/navigation/state.ts +0 -65
@@ -0,0 +1,82 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /** What only this browser knows about cells: hidden code showing for now, and editor state to restore. */
4
+
5
+ import { atom, useAtomValue } from "jotai";
6
+ import { useMemo } from "react";
7
+ import type { SerializedEditorState } from "../codemirror/types";
8
+ import type { CellId } from "./ids";
9
+
10
+ /**
11
+ * Hidden cells whose code is showing for now: a cell being edited, one just
12
+ * created, or one just made hidden. Blurring the cell hides it again.
13
+ */
14
+ export const shownHiddenCodeAtom = atom<ReadonlySet<CellId>>(new Set<CellId>());
15
+
16
+ export const useShowsHiddenCode = (cellId: CellId) =>
17
+ useAtomValue(
18
+ useMemo(
19
+ () => atom((get) => get(shownHiddenCodeAtom).has(cellId)),
20
+ [cellId],
21
+ ),
22
+ );
23
+
24
+ /** Whether a cell's code is hidden on the page. */
25
+ export function isCodeHidden(
26
+ hideCode: boolean | undefined,
27
+ shown: ReadonlySet<CellId>,
28
+ cellId: CellId,
29
+ ): boolean {
30
+ return Boolean(hideCode) && !shown.has(cellId);
31
+ }
32
+
33
+ export const showHiddenCodeAtom = atom(
34
+ null,
35
+ (_get, set, { cellId }: { cellId: CellId }): void => {
36
+ set(shownHiddenCodeAtom, (cells) => new Set([...cells, cellId]));
37
+ },
38
+ );
39
+
40
+ /** Hide a cell's code again. Returns whether it was showing. */
41
+ export const hideShownCodeAtom = atom(
42
+ null,
43
+ (get, set, { cellId }: { cellId: CellId }): boolean => {
44
+ if (!get(shownHiddenCodeAtom).has(cellId)) {
45
+ return false;
46
+ }
47
+ set(shownHiddenCodeAtom, (cells) => {
48
+ const next = new Set(cells);
49
+ next.delete(cellId);
50
+ return next;
51
+ });
52
+ return true;
53
+ },
54
+ );
55
+
56
+ /**
57
+ * Editor state for cells about to mount again, such as a cell undo brings
58
+ * back. The editor takes its entry when it mounts, and deleting the cell
59
+ * drops it.
60
+ */
61
+ export const restoredEditorStatesAtom = atom<
62
+ ReadonlyMap<CellId, SerializedEditorState>
63
+ >(new Map<CellId, SerializedEditorState>());
64
+
65
+ export const useRestoredEditorState = (cellId: CellId) =>
66
+ useAtomValue(
67
+ useMemo(
68
+ () => atom((get) => get(restoredEditorStatesAtom).get(cellId) ?? null),
69
+ [cellId],
70
+ ),
71
+ );
72
+
73
+ export const takeRestoredEditorStateAtom = atom(
74
+ null,
75
+ (_get, set, { cellId }: { cellId: CellId }): void => {
76
+ set(restoredEditorStatesAtom, (states) => {
77
+ const next = new Map(states);
78
+ next.delete(cellId);
79
+ return next;
80
+ });
81
+ },
82
+ );
@@ -0,0 +1,152 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /** The cells as they appear on the page: by column, with folded sections hidden. */
4
+
5
+ import { atom, useAtomValue } from "jotai";
6
+ import type { CellColumnId } from "@/utils/id-tree";
7
+ import { findCollapseRange } from "../dom/outline";
8
+ import { cellIdsAtom, cellOutlinesAtom } from "./cells";
9
+ import type { CellId } from "./ids";
10
+ import type { Outline } from "./outline";
11
+
12
+ /** The headers whose sections the user folded. Only this browser knows. */
13
+ export const collapsedAtom = atom<ReadonlySet<CellId>>(new Set<CellId>());
14
+
15
+ export interface LayoutColumn {
16
+ readonly id: CellColumnId;
17
+ /** Every cell of the column, in order. */
18
+ readonly ids: readonly CellId[];
19
+ /** The cells on the page: those no fold hides. */
20
+ readonly visible: readonly CellId[];
21
+ /**
22
+ * Each folded header and the cells it hides, nested sections included. A
23
+ * folded header with nothing under it yet has an empty fold.
24
+ */
25
+ readonly folds: ReadonlyMap<CellId, readonly CellId[]>;
26
+ }
27
+
28
+ export interface Layout {
29
+ readonly columns: readonly LayoutColumn[];
30
+ }
31
+
32
+ /**
33
+ * The cells each folded header hides. A section runs from its header to the
34
+ * cell before the next header of the same or a higher level, so a folded
35
+ * header inside a folded section is hidden along with its own section.
36
+ */
37
+ export function foldSections(
38
+ ids: readonly CellId[],
39
+ outlines: (Outline | null)[],
40
+ collapsed: ReadonlySet<CellId>,
41
+ ): Map<CellId, readonly CellId[]> {
42
+ const folds = new Map<CellId, readonly CellId[]>();
43
+ ids.forEach((id, index) => {
44
+ const outline = outlines[index];
45
+ if (!collapsed.has(id) || !outline || outline.items.length === 0) {
46
+ return;
47
+ }
48
+ const range = findCollapseRange(index, outlines);
49
+ if (range === null) {
50
+ return;
51
+ }
52
+ folds.set(id, ids.slice(index + 1, range[1] + 1));
53
+ });
54
+ return folds;
55
+ }
56
+
57
+ export function deriveLayout(
58
+ columns: ReadonlyArray<{ id: CellColumnId; ids: readonly CellId[] }>,
59
+ outlineOf: (id: CellId) => Outline | null,
60
+ collapsed: ReadonlySet<CellId>,
61
+ ): Layout {
62
+ return {
63
+ columns: columns.map(({ id, ids }) => {
64
+ const folds = foldSections(ids, ids.map(outlineOf), collapsed);
65
+ const hidden = new Set([...folds.values()].flat());
66
+ return { id, ids, visible: ids.filter((c) => !hidden.has(c)), folds };
67
+ }),
68
+ };
69
+ }
70
+
71
+ /**
72
+ * The layout of the notebook's cells.
73
+ *
74
+ * TODO(MO-8028): Order and columns come from the reducer's tree until the
75
+ * arrange family edits the document; then they derive from the document and
76
+ * the column ids this browser keeps. Outlines come from executions later.
77
+ */
78
+ export const layoutAtom = atom((get): Layout => {
79
+ const outlines = get(cellOutlinesAtom);
80
+ return deriveLayout(
81
+ get(cellIdsAtom)
82
+ .getColumns()
83
+ .map((column) => ({ id: column.id, ids: column.inOrderIds })),
84
+ (id) => outlines[id] ?? null,
85
+ get(collapsedAtom),
86
+ );
87
+ });
88
+
89
+ export const useLayout = () => useAtomValue(layoutAtom);
90
+
91
+ export function findColumn(
92
+ layout: Layout,
93
+ columnId: CellColumnId,
94
+ ): LayoutColumn | undefined {
95
+ return layout.columns.find((column) => column.id === columnId);
96
+ }
97
+
98
+ /** The column holding `id`. Throws for a cell the layout does not have. */
99
+ export function columnOf(layout: Layout, id: CellId): LayoutColumn {
100
+ const column = layout.columns.find((column) => column.ids.includes(id));
101
+ if (column === undefined) {
102
+ throw new Error(`Cell ${id} is not in the layout`);
103
+ }
104
+ return column;
105
+ }
106
+
107
+ /** The cells a folded header hides, or undefined when `id` is not a folded header. */
108
+ export function foldOf(
109
+ layout: Layout,
110
+ id: CellId,
111
+ ): readonly CellId[] | undefined {
112
+ for (const column of layout.columns) {
113
+ const fold = column.folds.get(id);
114
+ if (fold !== undefined) {
115
+ return fold;
116
+ }
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ /** The headers whose folds hide `id`. */
122
+ export function foldsHiding(layout: Layout, id: CellId): CellId[] {
123
+ const column = layout.columns.find((column) => column.ids.includes(id));
124
+ if (column === undefined) {
125
+ return [];
126
+ }
127
+ return [...column.folds]
128
+ .filter(([, hidden]) => hidden.includes(id))
129
+ .map(([header]) => header);
130
+ }
131
+
132
+ /**
133
+ * The folded headers whose sections a cell without a heading would fall
134
+ * into, placed right after `id`: those hiding `id`, and `id` itself when it
135
+ * is a folded header. A section runs until the next heading, so a cell
136
+ * placed after the last cell of a fold is still inside it.
137
+ */
138
+ export function foldsAfter(layout: Layout, id: CellId): CellId[] {
139
+ const hiding = foldsHiding(layout, id);
140
+ return foldOf(layout, id) === undefined ? hiding : [...hiding, id];
141
+ }
142
+
143
+ /** The visible cell `offset` places from `id` in its column, if there is one. */
144
+ export function visibleNeighbor(
145
+ layout: Layout,
146
+ id: CellId,
147
+ offset: -1 | 1,
148
+ ): CellId | undefined {
149
+ const { visible } = columnOf(layout, id);
150
+ const index = visible.indexOf(id);
151
+ return index === -1 ? undefined : visible[index + offset];
152
+ }
@@ -1,17 +1,19 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ /** Read the notebook a page was served with: its cells as a host snapshot, and their outputs. */
4
+
5
+ import type * as Protocol from "@marimo-team/client/protocol";
3
6
  import type * as api from "@marimo-team/marimo-api";
4
7
  import { mergeArray } from "@/utils/edit-distance";
5
- import { MultiColumn } from "@/utils/id-tree";
6
8
  import { Logger } from "@/utils/Logger";
7
9
  import { parseOutline } from "../dom/outline";
8
- import type { NotebookState } from "./cells";
10
+ import type { StaticNotebookSnapshot } from "../transports/static";
9
11
  import { CellId, SETUP_CELL_ID } from "./ids";
10
- import {
11
- type CellData,
12
- type CellRuntimeState,
13
- createCellRuntimeState,
14
- } from "./types";
12
+ import { DEFAULT_CELL_NAME } from "./names";
13
+ import { type CellRuntimeState, createCellRuntimeState } from "./types";
14
+
15
+ /** The identity of a snapshot the page carries. A host's own stream replaces it. */
16
+ const EMBEDDED = "embedded";
15
17
 
16
18
  // Constants
17
19
  const DEFAULT_TIMESTAMP = 0;
@@ -143,7 +145,7 @@ function createEmptyNotebookCell(): NotebookCell {
143
145
  return {
144
146
  id: CellId.create(),
145
147
  name: EMPTY_STRING,
146
- code: EMPTY_STRING,
148
+ code: null,
147
149
  code_hash: null,
148
150
  config: {
149
151
  column: null,
@@ -163,24 +165,22 @@ function createEmptySessionCell(): SessionCell {
163
165
  };
164
166
  }
165
167
 
166
- function createCellDataFromNotebook(
167
- cellId: CellId,
168
- notebookCell: NotebookCell,
169
- ): CellData {
168
+ /** The cell as a host would describe it. A snapshot never changes, so its etags only need to be distinct. */
169
+ function cellRecord(cellId: CellId, cell: NotebookCell): Protocol.Cell {
170
170
  return {
171
171
  id: cellId,
172
- name: notebookCell.name ?? EMPTY_STRING,
173
- code: notebookCell.code ?? EMPTY_STRING,
174
- edited: false,
175
- lastCodeRun: null,
176
- lastExecutionTime: null,
172
+ notebook_id: EMBEDDED,
173
+ code: cell.code,
174
+ name: cell.name || DEFAULT_CELL_NAME,
177
175
  config: {
178
- column: notebookCell.config?.column ?? null,
179
- disabled: notebookCell.config?.disabled ?? false,
180
- hide_code: notebookCell.config?.hide_code ?? false,
181
- expand_output: notebookCell.config?.expand_output ?? false,
176
+ column: cell.config?.column ?? null,
177
+ disabled: cell.config?.disabled ?? false,
178
+ hide_code: cell.config?.hide_code ?? false,
179
+ expand_output: cell.config?.expand_output ?? false,
182
180
  },
183
- serializedEditorState: null,
181
+ etag: `${EMBEDDED}:${cellId}`,
182
+ code_hash: cell.code_hash ?? `${EMBEDDED}:${cellId}:code`,
183
+ version: 1,
184
184
  };
185
185
  }
186
186
 
@@ -278,45 +278,68 @@ function createCellRuntimeFromSession(
278
278
  };
279
279
  }
280
280
 
281
- export function notebookStateFromSession(
282
- session: api.Session["NotebookSessionV1"] | null | undefined,
283
- notebook: api.Notebook["NotebookV1"] | null | undefined,
284
- ): NotebookState | null {
285
- // Merge session and notebook cells using edit distance
281
+ /**
282
+ * Read the session and notebook a page embeds.
283
+ *
284
+ * The cells become a host snapshot the replica starts from, and the session's
285
+ * outputs seed each cell's runtime until the kernel speaks. Null when the page
286
+ * carries no cells.
287
+ */
288
+ export function readExport({
289
+ session,
290
+ notebook,
291
+ filename,
292
+ appConfig,
293
+ }: {
294
+ session: api.Session["NotebookSessionV1"] | null | undefined;
295
+ notebook: api.Notebook["NotebookV1"] | null | undefined;
296
+ filename: string | null;
297
+ appConfig: Record<string, unknown>;
298
+ }): {
299
+ snapshot: StaticNotebookSnapshot | null;
300
+ cellRuntime: Record<CellId, CellRuntimeState>;
301
+ } {
286
302
  const { cellIds, sessionCellData, notebookCellData } =
287
303
  mergeSessionAndNotebookCells(session, notebook);
288
-
289
304
  if (cellIds.length === 0) {
290
- return null;
305
+ return { snapshot: null, cellRuntime: {} };
291
306
  }
292
307
 
293
- const cellData: Record<CellId, CellData> = {};
308
+ const cells: Protocol.Cell[] = [];
294
309
  const cellRuntime: Record<CellId, CellRuntimeState> = {};
295
-
296
- // Process each cell
297
310
  for (const cellId of cellIds) {
298
- const sessionCell = sessionCellData.get(cellId);
299
- const notebookCell = notebookCellData.get(cellId);
300
-
301
- // Create cell data from notebook if available
302
- if (notebookCell) {
303
- cellData[cellId] = createCellDataFromNotebook(cellId, notebookCell);
304
- }
305
-
306
- // Create cell runtime
307
- // This needs always be created even if there is no session cell
308
- // in order to display the cell in the correct state
309
- cellRuntime[cellId] = createCellRuntimeFromSession(sessionCell);
311
+ cells.push(
312
+ cellRecord(
313
+ cellId,
314
+ notebookCellData.get(cellId) ?? createEmptyNotebookCell(),
315
+ ),
316
+ );
317
+ cellRuntime[cellId] = createCellRuntimeFromSession(
318
+ sessionCellData.get(cellId),
319
+ );
310
320
  }
311
321
 
312
322
  return {
313
- cellIds: MultiColumn.from([cellIds]),
314
- cellData: cellData,
315
- cellRuntime: cellRuntime,
316
- cellHandles: {},
317
- history: [],
318
- scrollKey: null,
319
- cellLogs: [],
320
- untouchedNewCells: new Set(),
323
+ snapshot: {
324
+ notebook: {
325
+ id: EMBEDDED,
326
+ project_id: EMBEDDED,
327
+ path: filename,
328
+ name: filename ?? EMPTY_STRING,
329
+ updated_at: null,
330
+ },
331
+ document: {
332
+ notebook_id: EMBEDDED,
333
+ order: cellIds,
334
+ app_config: appConfig,
335
+ etag: `${EMBEDDED}:document`,
336
+ saved_etag: `${EMBEDDED}:document`,
337
+ order_etag: `${EMBEDDED}:order`,
338
+ app_config_etag: `${EMBEDDED}:app_config`,
339
+ version: 1,
340
+ },
341
+ cells,
342
+ },
343
+ cellRuntime,
321
344
  };
322
345
  }
@@ -1,7 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { Milliseconds, Seconds } from "@/utils/time";
4
- import type { SerializedEditorState } from "../codemirror/types";
5
4
  import type { OutputMessage } from "../kernel/messages";
6
5
  import type { CellConfig, RuntimeState } from "../network/types";
7
6
  import type { CellId } from "./ids";
@@ -31,7 +30,6 @@ export function createCell({
31
30
  lastExecutionTime = null,
32
31
  edited = false,
33
32
  config,
34
- serializedEditorState = null,
35
33
  }: Partial<CellData> & { id: CellId }): CellData {
36
34
  return {
37
35
  id: id,
@@ -46,7 +44,6 @@ export function createCell({
46
44
  edited: edited,
47
45
  lastCodeRun: lastCodeRun,
48
46
  lastExecutionTime: lastExecutionTime,
49
- serializedEditorState: serializedEditorState,
50
47
  };
51
48
  }
52
49
 
@@ -97,8 +94,6 @@ export interface CellData {
97
94
  lastExecutionTime: number | null;
98
95
  /** cell configuration */
99
96
  config: CellConfig;
100
- /** serialized state of the underlying editor */
101
- serializedEditorState: SerializedEditorState | null;
102
97
  }
103
98
 
104
99
  export interface CellRuntimeState {
@@ -79,11 +79,13 @@ export function getUndoLabel(state: NotebookState): string {
79
79
  }
80
80
 
81
81
  /**
82
- * Get the status of the descendants of the given cell.
82
+ * Summarize the run state of the given descendants, whether any is stale,
83
+ * errored, or running.
83
84
  */
84
- export function getDescendantsStatus(state: NotebookState, cellId: CellId) {
85
- const column = state.cellIds.findWithId(cellId);
86
- const descendants = column.getDescendants(cellId);
85
+ export function getDescendantsStatus(
86
+ state: NotebookState,
87
+ descendants: readonly CellId[],
88
+ ) {
87
89
  const stale = descendants.some(
88
90
  (id) => state.cellRuntime[id]?.staleInputs || state.cellData[id]?.edited,
89
91
  );
@@ -0,0 +1,64 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom, createStore } from "jotai";
4
+ import { describe, expect, it } from "vitest";
5
+ import { partial } from "@/__mocks__/partial";
6
+ import type { Document, NotebookReplica } from "@/core/notebook/replica";
7
+ import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
8
+ import { appConfigAtom, pageAppConfigAtom } from "../config";
9
+
10
+ function document(app_config: Record<string, unknown>, version = 1): Document {
11
+ return {
12
+ revision: {
13
+ notebook_id: "nb",
14
+ order: [],
15
+ app_config,
16
+ etag: `etag-${version}`,
17
+ saved_etag: null,
18
+ order_etag: "order",
19
+ app_config_etag: "config",
20
+ version,
21
+ },
22
+ cells: new Map(),
23
+ };
24
+ }
25
+
26
+ function connect(store: ReturnType<typeof createStore>, current: Document) {
27
+ store.set(
28
+ activeNotebookReplicaAtom,
29
+ partial<NotebookReplica>({ documentAtom: atom(current) }, "replica"),
30
+ );
31
+ }
32
+
33
+ describe("appConfigAtom", () => {
34
+ it("reads the page's config until the host document arrives", () => {
35
+ const store = createStore();
36
+ expect(store.get(appConfigAtom).width).toBe("compact");
37
+ store.set(pageAppConfigAtom, { width: "full" });
38
+ expect(store.get(appConfigAtom).width).toBe("full");
39
+ connect(store, document({ width: "compact" }));
40
+ expect(store.get(appConfigAtom).width).toBe("compact");
41
+ });
42
+
43
+ it("fills what the host document leaves out with Python's defaults", () => {
44
+ const store = createStore();
45
+ store.set(pageAppConfigAtom, { width: "full" });
46
+ // The document carries only the options that differ from the defaults.
47
+ connect(store, document({}));
48
+ expect(store.get(appConfigAtom)).toEqual({
49
+ width: "compact",
50
+ auto_download: [],
51
+ sql_output: "auto",
52
+ });
53
+ });
54
+
55
+ it("keeps the same object while the record is unchanged", () => {
56
+ const store = createStore();
57
+ connect(store, document({ width: "full" }, 1));
58
+ const first = store.get(appConfigAtom);
59
+ connect(store, document({ width: "full" }, 2));
60
+ expect(store.get(appConfigAtom)).toBe(first);
61
+ connect(store, document({ width: "medium" }, 3));
62
+ expect(store.get(appConfigAtom).width).toBe("medium");
63
+ });
64
+ });
@@ -27,7 +27,7 @@ test("default AppConfig", () => {
27
27
  {
28
28
  "auto_download": [],
29
29
  "sql_output": "auto",
30
- "width": "medium",
30
+ "width": "compact",
31
31
  }
32
32
  `);
33
33
  });
@@ -278,11 +278,15 @@ export const SqlOutputSchema = z
278
278
  .enum(VALID_SQL_OUTPUT_FORMATS)
279
279
  .prefault("auto");
280
280
 
281
+ /**
282
+ * The notebook's app config. The defaults are Python's `_AppConfig` defaults,
283
+ * since the host document carries only what differs from them.
284
+ */
281
285
  export const AppConfigSchema = z
282
286
  .object({
283
287
  width: z
284
288
  .enum(VALID_APP_WIDTHS)
285
- .prefault("medium")
289
+ .prefault("compact")
286
290
  .transform((width) => {
287
291
  if (width === "normal") {
288
292
  return "compact";
@@ -296,7 +300,7 @@ export const AppConfigSchema = z
296
300
  sql_output: SqlOutputSchema,
297
301
  })
298
302
  .prefault(() => ({
299
- width: "medium" as const,
303
+ width: "compact" as const,
300
304
  auto_download: [],
301
305
  sql_output: "auto" as const,
302
306
  }));
@@ -1,8 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { dequal } from "dequal";
2
3
  import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
4
+ import { selectAtom } from "jotai/vanilla/utils";
3
5
  import { mergeWith } from "lodash-es";
4
6
  import { OverridingHotkeyProvider } from "../hotkeys/hotkeys";
5
7
  import { type Platform, resolvePlatform } from "../hotkeys/shortcuts";
8
+ import { confirmedDocumentAtom } from "../session/active-notebook";
6
9
  import { store } from "../state/jotai";
7
10
  import {
8
11
  type AppConfig,
@@ -134,19 +137,30 @@ export function isAiFeatureEnabled(config: UserConfig) {
134
137
  }
135
138
 
136
139
  /**
137
- * Atom for storing the app config.
140
+ * The app config the page was served with, as the server wrote it.
141
+ *
142
+ * The host document carries the current one; this holds its place until
143
+ * that document arrives. It goes once every page carries its document.
138
144
  */
139
- export const appConfigAtom = atom<AppConfig>(parseAppConfig({}));
140
-
141
- /**
142
- * Returns the app config.
143
- */
144
- export function useAppConfig() {
145
- return useAtom(appConfigAtom);
146
- }
147
-
148
- export function useSetAppConfig() {
149
- return useSetAtom(appConfigAtom);
145
+ export const pageAppConfigAtom = atom<unknown>({});
146
+
147
+ /** The app config record in force, stable while its contents are. */
148
+ const appConfigRecordAtom = selectAtom(
149
+ atom(
150
+ (get) =>
151
+ get(confirmedDocumentAtom)?.revision.app_config ?? get(pageAppConfigAtom),
152
+ ),
153
+ (record) => record,
154
+ dequal,
155
+ );
156
+
157
+ /** The notebook's app config, as the host document carries it. */
158
+ export const appConfigAtom = atom((get) =>
159
+ parseAppConfig(get(appConfigRecordAtom)),
160
+ );
161
+
162
+ export function useAppConfig(): AppConfig {
163
+ return useAtomValue(appConfigAtom);
150
164
  }
151
165
 
152
166
  export function getAppConfig() {
@@ -221,7 +221,6 @@ function handleNotification(
221
221
  setLastRun: actions.setLastRun,
222
222
  resetCellRuntimes: actions.resetCellRuntimes,
223
223
  setLayoutData: Functions.NOOP,
224
- setAppConfig: Functions.NOOP,
225
224
  setCapabilities: Functions.NOOP,
226
225
  setKernelState: Functions.NOOP,
227
226
  onError: Logger.error,
@@ -1,11 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { LayoutType } from "@/components/editor/renderers/types";
3
- import { Logger } from "@/utils/Logger";
4
3
  import { Objects } from "@/utils/objects";
5
4
  import type { UIElementId } from "../cells/ids";
6
5
  import type { ReducerCellActions } from "../cells/cells";
7
6
  import type { CellData } from "../cells/types";
8
- import { type AppConfig, AppConfigSchema } from "../config/config-schema";
9
7
  import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
10
8
  import { deserializeLayoutState } from "../layout/layout";
11
9
  import type { LayoutData, LayoutState } from "../layout/state";
@@ -71,7 +69,6 @@ export function handleKernelReady(
71
69
  }) => void;
72
70
  setCapabilities: (capabilities: Capabilities) => void;
73
71
  setKernelState: (state: KernelState) => void;
74
- setAppConfig: (config: AppConfig) => void;
75
72
  onError: (error: Error) => void;
76
73
  },
77
74
  ): LayoutState {
@@ -81,13 +78,11 @@ export function handleKernelReady(
81
78
  setLastRun,
82
79
  resetCellRuntimes,
83
80
  setLayoutData,
84
- setAppConfig,
85
81
  setCapabilities,
86
82
  setKernelState,
87
83
  onError,
88
84
  } = opts;
89
- const { resumed, ui_values, app_config, capabilities, auto_instantiated } =
90
- data;
85
+ const { resumed, ui_values, capabilities, auto_instantiated } = data;
91
86
 
92
87
  resetCellRuntimes();
93
88
  setLastRun({
@@ -96,13 +91,6 @@ export function handleKernelReady(
96
91
  });
97
92
  const layoutState = buildLayoutState(data, cells, setLayoutData);
98
93
 
99
- // Set app config and capabilities
100
- const parsedAppConfig = AppConfigSchema.safeParse(app_config);
101
- if (parsedAppConfig.success) {
102
- setAppConfig(parsedAppConfig.data);
103
- } else {
104
- Logger.error("Failed to parse app config", parsedAppConfig.error);
105
- }
106
94
  setCapabilities(capabilities);
107
95
 
108
96
  // If the kernel was already instantiated server-side (e.g. run mode), we're done