@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.
- package/dist/{ConnectedDataExplorerComponent-BBbOElk3.js → ConnectedDataExplorerComponent-BRuwLOIx.js} +9 -9
- package/dist/{ErrorBoundary--X0z83Yx.js → ErrorBoundary-BVu_xT-p.js} +2 -2
- package/dist/{abnfDiagram-VCTEODGH-DhMT6Uhd.js → abnfDiagram-VCTEODGH-LlJTOOU_.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-BVbEiJLq.js → architectureDiagram-5GKGNRK7-9PQtGLLZ.js} +1 -1
- package/dist/{arrays-R6JsImiT.js → arrays-DA6KkLfq.js} +6 -12
- package/dist/assets/__vite-browser-external-BCJsx8QS.js +1 -0
- package/dist/assets/{worker-C6e9ERyd.js → worker-DodjWT_Z.js} +2 -2
- package/dist/{capabilities-CI4xUPsp.js → capabilities-CvWU-ey_.js} +1 -1
- package/dist/{chat-ui-BguxTQEL.js → chat-ui-Bl2euMBD.js} +93 -93
- package/dist/{chunk-IMKFNOWR-By5hSbJJ.js → chunk-IMKFNOWR-DcXv-BD5.js} +1 -1
- package/dist/{chunk-SHT3W25Y-DFrEYG54.js → chunk-SHT3W25Y-VwnELN23.js} +1 -1
- package/dist/{chunk-SVP7TREG-DFXGc9ew.js → chunk-SVP7TREG-DgQpI5Zi.js} +1 -1
- package/dist/{chunk-TICWLB2K-DZdNIpoZ.js → chunk-TICWLB2K-CTg6VTJT.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Cgj_eEQA.js → classDiagram-ZZMXUADV-UzmcFrnW.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DLAUrSqj.js → classDiagram-v2-VYDZK3BY-DhjiI6IQ.js} +2 -2
- package/dist/{code-visibility-uDyLo9_i.js → code-visibility-BTVimCUw.js} +38 -42
- package/dist/{common-DWKY45Md.js → common-fTgq7gO-.js} +849 -849
- package/dist/{cynefinDiagram-5FMLGOSQ-Cikf9AKa.js → cynefinDiagram-5FMLGOSQ-O4ShIbJX.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-C5KEi5_l.js → dagre-GXQ25YYZ-dtHhhcPu.js} +1 -1
- package/dist/{data-grid-overlay-editor-CRnYWD1L.js → data-grid-overlay-editor-DAZZkLWg.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BaoBTQjR.js → diagram-S7CK7UJ4-ppPYVZ6W.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DDufBsUA.js → diagram-UQ7AKVKN-CMZDf--k.js} +1 -1
- package/dist/{diagram-VX7I27RA-CY6RALJ8.js → diagram-VX7I27RA-CFvJdx-C.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Bc-LHsD6.js → diagram-Z3DM3KII-Dt6_uFbv.js} +1 -1
- package/dist/{dropdown-menu-drDjxJyV.js → dropdown-menu-CeaOsjWZ.js} +2 -2
- package/dist/{ebnfDiagram-PWID7BFC-Cz0J-tcC.js → ebnfDiagram-PWID7BFC-DpRFrKK-.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-DTXTDgFu.js → erDiagram-RLTQ6QDP-CqAU4b0u.js} +1 -1
- package/dist/{error-entries-CZ1NQ6Kv.js → error-entries-CYGwr5-O.js} +5596 -6012
- package/dist/feedback-button-DzFFn-n4.js +374 -0
- package/dist/{flowDiagram-HODETNUW-RbwWf2zc.js → flowDiagram-HODETNUW-i8-Gw8HO.js} +2 -2
- package/dist/{formats-DbXnijtH.js → formats-BtoHHy42.js} +1 -1
- package/dist/{glide-data-editor-CvRiA49N.js → glide-data-editor-DFsqVbWR.js} +5 -5
- package/dist/{globals-BR4CmGOS.js → globals-Dr0MRANu.js} +2 -2
- package/dist/{html-to-image-D_4_hQ8U.js → html-to-image-Cy5UDpQJ.js} +56 -56
- package/dist/{infoDiagram-27XIBGKW-DlmzZjmI.js → infoDiagram-27XIBGKW-DpfOKdLL.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-EN3TYP_q.js → ishikawaDiagram-5VMMS53U-Dg7mrMG7.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-CK_-Ru7Q.js → kanban-definition-UXKFOSKX-QB6e3Jrj.js} +1 -1
- package/dist/main.js +5875 -5876
- package/dist/{mermaid-DU9ZRmkG.js → mermaid-CeWG4Egr.js} +48 -48
- package/dist/{mindmap-definition-YA3MSWOX-C-hqgUvF.js → mindmap-definition-YA3MSWOX-BTz3uTGe.js} +1 -1
- package/dist/{number-overlay-editor-DiYGbxNa.js → number-overlay-editor-DupOSYHx.js} +1 -1
- package/dist/{once-eG8Vmvjd.js → once-DWGO2mQ2.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-BiMejYPB.js → pegDiagram-XKGWAZYB-3ZIEa3mB.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-B1zCZ1eq.js → pieDiagram-E7YTZNPT-BJAlk046.js} +1 -1
- package/dist/{process-output-DxSCHBWp.js → process-output-q9MTAIMX.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-BLcfaASl.js → railroadDiagram-O6MQD6OU-Dn3bsSRa.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-DtBOf52-.js → requirementDiagram-BXWQKSXE-wcOmQQCL.js} +1 -1
- package/dist/{reveal-component-D3DsXPIm.js → reveal-component-Bg8rP_Uo.js} +17 -17
- package/dist/{sequenceDiagram-WJ2MYXX4-DIwF19_n.js → sequenceDiagram-WJ2MYXX4-DSh_qxoF.js} +1 -1
- package/dist/{spec-DZDnnlgT.js → spec-C7Fos-If.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-sWk1AdYL.js → stateDiagram-D77RDMKH-DH_H8NMH.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CiNo-OI7.js → stateDiagram-v2-MP3YSRHH-D_JM7MEK.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Bj3hiOY5.js → swimlanes-42K2YHIH-CfoTJO-9.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BzEo2r_N.js → swimlanesDiagram-VR7AAH4N-Z_QhvK0G.js} +2 -2
- package/dist/{swiper-component-BkCm-kLg.js → swiper-component-D2geL1Ku.js} +1 -1
- package/dist/{timeline-definition-24CTP7MA-CrPQk42H.js → timeline-definition-24CTP7MA-BFtdScaL.js} +1 -1
- package/dist/{toDate-Cu2pIZcC.js → toDate-CQAFSwos.js} +24 -24
- package/dist/{useAsyncData-DEhLdRIu.js → useAsyncData-BeyZyjso.js} +1 -1
- package/dist/{useDeepCompareMemoize-RjZGVhBV.js → useDeepCompareMemoize-DOQgae0W.js} +1 -1
- package/dist/{useIframeCapabilities-CSdWn6NP.js → useIframeCapabilities-DNvaj9wM.js} +1 -1
- package/dist/{useLifecycle-DtcB2s1z.js → useLifecycle-n2lnCFS8.js} +1 -1
- package/dist/{useTheme-CoEtr4mK.js → useTheme-CY777LXN.js} +303 -99
- package/dist/{vega-component-BhjTrP9-.js → vega-component-0nQzA-I8.js} +6 -6
- package/dist/{vennDiagram-4TSXK5OY-BaYMoIpj.js → vennDiagram-4TSXK5OY-BXY8IGCA.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-DjHeuj8p.js → wardleyDiagram-VM6X3IG4-D0tFe6eC.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-Bc_hcuXS.js → xychartDiagram-S5SC5T6Z-Dx8wFM-y.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/notebook.ts +0 -1
- package/src/__mocks__/partial.ts +26 -0
- package/src/__tests__/main.test.tsx +24 -1
- package/src/components/app-config/app-config-form.tsx +5 -9
- package/src/components/editor/__tests__/data-attributes.test.tsx +0 -1
- package/src/components/editor/__tests__/output-persistence.test.tsx +0 -1
- package/src/components/editor/actions/useCellActionButton.tsx +6 -5
- package/src/components/editor/actions/useConfigActions.tsx +3 -4
- package/src/components/editor/actions/useNotebookActions.tsx +5 -4
- package/src/components/editor/cell/code/cell-editor.tsx +13 -6
- package/src/components/editor/cell/collapse.tsx +6 -1
- package/src/components/editor/chrome/components/feedback-button.tsx +1 -7
- package/src/components/editor/controls/Controls.tsx +1 -1
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +8 -6
- package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -15
- package/src/components/editor/navigation/__tests__/selection.test.ts +44 -21
- package/src/components/editor/navigation/focus-utils.ts +19 -0
- package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +6 -13
- package/src/components/editor/navigation/navigation.ts +52 -43
- package/src/components/editor/navigation/selection.ts +30 -29
- package/src/components/editor/notebook-cell.tsx +10 -18
- package/src/components/editor/renderers/cell-array.tsx +6 -5
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/pages/edit-page.tsx +0 -13
- package/src/components/scratchpad/scratchpad.tsx +0 -1
- package/src/components/slides/reveal-component.tsx +1 -1
- package/src/components/slides/slide-cell-view.tsx +0 -1
- package/src/components/sort/SortableCellsProvider.tsx +6 -5
- package/src/core/MarimoApp.tsx +1 -1
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -37
- package/src/core/cells/__tests__/actions.test.ts +190 -80
- package/src/core/cells/__tests__/arrange.test.ts +263 -0
- package/src/core/cells/__tests__/cells.test.ts +11 -1101
- package/src/core/cells/__tests__/document.test.ts +3 -32
- package/src/core/cells/__tests__/editor.test.ts +57 -0
- package/src/core/cells/__tests__/harness.ts +82 -0
- package/src/core/cells/__tests__/layout.test.ts +189 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
- package/src/core/cells/__tests__/session.test.ts +329 -174
- package/src/core/cells/__tests__/utils.test.ts +0 -1
- package/src/core/cells/actions.ts +247 -82
- package/src/core/cells/arrange.ts +341 -0
- package/src/core/cells/cells.ts +27 -516
- package/src/core/cells/document.ts +6 -5
- package/src/core/cells/edit.ts +30 -0
- package/src/core/cells/editor.ts +82 -0
- package/src/core/cells/layout.ts +152 -0
- package/src/core/cells/session.ts +75 -52
- package/src/core/cells/types.ts +0 -5
- package/src/core/cells/utils.ts +6 -4
- package/src/core/config/__tests__/app-config.test.ts +64 -0
- package/src/core/config/__tests__/config-schema.test.ts +1 -1
- package/src/core/config/config-schema.ts +6 -2
- package/src/core/config/config.ts +26 -12
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/kernel/handlers.ts +1 -13
- package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
- package/src/core/meta/code-visibility.ts +18 -9
- package/src/core/notebook/__tests__/operations.test.ts +51 -0
- package/src/core/notebook/document/__tests__/codemirror.test.ts +15 -0
- package/src/core/notebook/index.ts +2 -1
- package/src/core/notebook/operations.ts +30 -0
- package/src/core/saving/__tests__/state.test.ts +111 -0
- package/src/core/saving/save-component.tsx +19 -33
- package/src/core/saving/state.ts +38 -39
- package/src/core/saving/useAutoSave.ts +8 -35
- package/src/core/session/__tests__/backend.test.ts +1 -0
- package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +1 -0
- package/src/core/session/active-notebook.ts +11 -0
- package/src/core/session/backend.ts +17 -19
- package/src/core/session/embedded.ts +7 -0
- package/src/core/session/useNotebookReplica.ts +3 -1
- package/src/core/session/useNotebookSession.tsx +7 -8
- package/src/core/transports/__tests__/static.test.ts +139 -8
- package/src/core/transports/static.ts +98 -16
- package/src/core/wasm/__tests__/bridge.test.ts +1 -0
- package/src/mount.tsx +19 -10
- package/src/stories/cell.stories.tsx +0 -2
- package/src/stories/layout/vertical/one-column.stories.tsx +0 -2
- package/src/utils/__tests__/id-tree.test.ts +20 -1548
- package/src/utils/id-tree.tsx +12 -537
- package/dist/assets/__vite-browser-external-C8gQho07.js +0 -1
- package/dist/feedback-button-CDf1yRm_.js +0 -379
- package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
- 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 {
|
|
10
|
+
import type { StaticNotebookSnapshot } from "../transports/static";
|
|
9
11
|
import { CellId, SETUP_CELL_ID } from "./ids";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
173
|
-
code:
|
|
174
|
-
|
|
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:
|
|
179
|
-
disabled:
|
|
180
|
-
hide_code:
|
|
181
|
-
expand_output:
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
}
|
package/src/core/cells/types.ts
CHANGED
|
@@ -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 {
|
package/src/core/cells/utils.ts
CHANGED
|
@@ -79,11 +79,13 @@ export function getUndoLabel(state: NotebookState): string {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
82
|
+
* Summarize the run state of the given descendants, whether any is stale,
|
|
83
|
+
* errored, or running.
|
|
83
84
|
*/
|
|
84
|
-
export function getDescendantsStatus(
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
});
|
|
@@ -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("
|
|
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: "
|
|
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
|
-
*
|
|
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
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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,
|
|
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
|