@marimo-team/islands 0.25.3-dev196 → 0.25.3-dev198

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 (135) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BRuwLOIx.js → ConnectedDataExplorerComponent-BsWiIPFk.js} +4 -4
  2. package/dist/{ErrorBoundary-C3Xv3yzV.js → ErrorBoundary-D1Z3KAf_.js} +2 -2
  3. package/dist/{abnfDiagram-VCTEODGH-LlJTOOU_.js → abnfDiagram-VCTEODGH-I2x4Yu25.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-9PQtGLLZ.js → architectureDiagram-5GKGNRK7-1bSQMKzK.js} +1 -1
  5. package/dist/{arrays-DA6KkLfq.js → arrays-DoQ-AILx.js} +7 -21
  6. package/dist/{capabilities-CvWU-ey_.js → capabilities-C2QUNRXi.js} +1 -1
  7. package/dist/{chat-ui-Bl2euMBD.js → chat-ui-BAyHM_GD.js} +524 -524
  8. package/dist/{chunk-IMKFNOWR-DcXv-BD5.js → chunk-IMKFNOWR-fsCGAJ9G.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-VwnELN23.js → chunk-SHT3W25Y-D2STW4hj.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-DgQpI5Zi.js → chunk-SVP7TREG-CiY7cQG-.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-CTg6VTJT.js → chunk-TICWLB2K-C_t5RBp9.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-UzmcFrnW.js → classDiagram-ZZMXUADV-B2JvZAP4.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-DhjiI6IQ.js → classDiagram-v2-VYDZK3BY-L8VsZG5A.js} +2 -2
  14. package/dist/{code-visibility-BCcqmP58.js → code-visibility-CInEQd1e.js} +19 -19
  15. package/dist/{common-DrNB2bY2.js → common-W5HfVJPN.js} +4362 -4362
  16. package/dist/{cynefinDiagram-5FMLGOSQ-O4ShIbJX.js → cynefinDiagram-5FMLGOSQ-DdllC264.js} +1 -1
  17. package/dist/{dagre-GXQ25YYZ-dtHhhcPu.js → dagre-GXQ25YYZ-BaWHw4_f.js} +1 -1
  18. package/dist/{data-grid-overlay-editor-TGXV_Hf4.js → data-grid-overlay-editor-BQCaV0fI.js} +1 -1
  19. package/dist/{diagram-S7CK7UJ4-ppPYVZ6W.js → diagram-S7CK7UJ4-pFt-tUDe.js} +1 -1
  20. package/dist/{diagram-UQ7AKVKN-CMZDf--k.js → diagram-UQ7AKVKN-DqGJiBp4.js} +1 -1
  21. package/dist/{diagram-VX7I27RA-CFvJdx-C.js → diagram-VX7I27RA-Dr-yzMjf.js} +1 -1
  22. package/dist/{diagram-Z3DM3KII-Dt6_uFbv.js → diagram-Z3DM3KII-8xzJzv-w.js} +1 -1
  23. package/dist/{dropdown-menu-CeaOsjWZ.js → dropdown-menu-NlnsmNA6.js} +2 -2
  24. package/dist/{ebnfDiagram-PWID7BFC-DpRFrKK-.js → ebnfDiagram-PWID7BFC-Br04Wy-E.js} +2 -2
  25. package/dist/{erDiagram-RLTQ6QDP-CqAU4b0u.js → erDiagram-RLTQ6QDP-D5-XVgvv.js} +1 -1
  26. package/dist/{error-entries-CYGwr5-O.js → error-entries-BacCWMWj.js} +4678 -5028
  27. package/dist/{feedback-button-PTiPn2GP.js → feedback-button-DLXUcQ7l.js} +113 -113
  28. package/dist/{flowDiagram-HODETNUW-i8-Gw8HO.js → flowDiagram-HODETNUW-CVl2kREF.js} +2 -2
  29. package/dist/{formats-BtoHHy42.js → formats-BCYthwyc.js} +1 -1
  30. package/dist/{glide-data-editor-BSyexjkB.js → glide-data-editor-D5YaHPKx.js} +5 -5
  31. package/dist/{globals-B7gkJAxm.js → globals-DjX1uxzy.js} +2 -2
  32. package/dist/{html-to-image-Cy5UDpQJ.js → html-to-image-DAmgSo5P.js} +624 -573
  33. package/dist/{infoDiagram-27XIBGKW-DpfOKdLL.js → infoDiagram-27XIBGKW-BJ4vNcZE.js} +1 -1
  34. package/dist/{ishikawaDiagram-5VMMS53U-Dg7mrMG7.js → ishikawaDiagram-5VMMS53U-B_Jy1Dsm.js} +1 -1
  35. package/dist/{kanban-definition-UXKFOSKX-QB6e3Jrj.js → kanban-definition-UXKFOSKX-C6MkS9vw.js} +1 -1
  36. package/dist/main.js +1117 -1117
  37. package/dist/{mermaid-CeWG4Egr.js → mermaid-Bh918qrq.js} +34 -34
  38. package/dist/{mindmap-definition-YA3MSWOX-BTz3uTGe.js → mindmap-definition-YA3MSWOX-BP3ZTuir.js} +1 -1
  39. package/dist/{number-overlay-editor-Bb91yQ46.js → number-overlay-editor-3C_K6w_0.js} +1 -1
  40. package/dist/{once-DWGO2mQ2.js → once-NnpX_HnX.js} +1 -1
  41. package/dist/{pegDiagram-XKGWAZYB-3ZIEa3mB.js → pegDiagram-XKGWAZYB-nNGOQtnt.js} +2 -2
  42. package/dist/{pieDiagram-E7YTZNPT-BJAlk046.js → pieDiagram-E7YTZNPT-CpRz5BZz.js} +1 -1
  43. package/dist/{process-output-q9MTAIMX.js → process-output-DweUmAmV.js} +3 -3
  44. package/dist/{railroadDiagram-O6MQD6OU-Dn3bsSRa.js → railroadDiagram-O6MQD6OU-BqOEN4I0.js} +2 -2
  45. package/dist/{requirementDiagram-BXWQKSXE-wcOmQQCL.js → requirementDiagram-BXWQKSXE-CEfg6vu9.js} +1 -1
  46. package/dist/{reveal-component-vTdjiyDv.js → reveal-component-JjJRTT9d.js} +17 -17
  47. package/dist/{sequenceDiagram-WJ2MYXX4-DSh_qxoF.js → sequenceDiagram-WJ2MYXX4-IOmpQcOi.js} +1 -1
  48. package/dist/{spec-C7Fos-If.js → spec-Dkp64vVQ.js} +1 -1
  49. package/dist/{stateDiagram-D77RDMKH-DH_H8NMH.js → stateDiagram-D77RDMKH-tgU-EY9_.js} +2 -2
  50. package/dist/{stateDiagram-v2-MP3YSRHH-D_JM7MEK.js → stateDiagram-v2-MP3YSRHH-BofhG6q1.js} +2 -2
  51. package/dist/{swimlanes-42K2YHIH-CfoTJO-9.js → swimlanes-42K2YHIH-DW4vWDBd.js} +1 -1
  52. package/dist/{swimlanesDiagram-VR7AAH4N-Z_QhvK0G.js → swimlanesDiagram-VR7AAH4N-BJcrqUJr.js} +2 -2
  53. package/dist/{swiper-component-D2geL1Ku.js → swiper-component-BVfnhbnt.js} +1 -1
  54. package/dist/{timeline-definition-24CTP7MA-BFtdScaL.js → timeline-definition-24CTP7MA-D_QU3NGs.js} +1 -1
  55. package/dist/{toDate-CQAFSwos.js → toDate-BNDEXbfV.js} +30 -30
  56. package/dist/{useAsyncData-BeyZyjso.js → useAsyncData-ASDni9vH.js} +1 -1
  57. package/dist/{useDeepCompareMemoize-DOQgae0W.js → useDeepCompareMemoize-B1Ogyf53.js} +1 -1
  58. package/dist/{useIframeCapabilities-DNvaj9wM.js → useIframeCapabilities-6a_yDNXp.js} +1 -1
  59. package/dist/{useLifecycle-n2lnCFS8.js → useLifecycle-Bfpa2m7k.js} +1 -1
  60. package/dist/{useTheme-CY777LXN.js → useTheme-5TwyOh3B.js} +66 -66
  61. package/dist/{vega-component-0nQzA-I8.js → vega-component-BHS9UOtC.js} +19 -19
  62. package/dist/{vennDiagram-4TSXK5OY-BXY8IGCA.js → vennDiagram-4TSXK5OY-YFYsNIAh.js} +1 -1
  63. package/dist/{wardleyDiagram-VM6X3IG4-D0tFe6eC.js → wardleyDiagram-VM6X3IG4-i_n8mAWC.js} +1 -1
  64. package/dist/{xychartDiagram-S5SC5T6Z-Dx8wFM-y.js → xychartDiagram-S5SC5T6Z-BYGN95wW.js} +1 -1
  65. package/package.json +1 -1
  66. package/src/__mocks__/document.tsx +13 -4
  67. package/src/__mocks__/notebook.ts +1 -2
  68. package/src/components/datasources/datasources.tsx +1 -1
  69. package/src/components/dependency-graph/custom-node.tsx +1 -1
  70. package/src/components/dependency-graph/minimap-content.tsx +12 -14
  71. package/src/components/editor/actions/useSetCodeVisibility.ts +1 -1
  72. package/src/components/editor/actions/useSetOutputsExpanded.ts +1 -1
  73. package/src/components/editor/alerts/stdin-blocking-alert.tsx +1 -1
  74. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  75. package/src/components/editor/cell/useSplitCell.tsx +8 -5
  76. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +1 -1
  77. package/src/components/editor/chrome/panels/session-panel.tsx +1 -1
  78. package/src/components/editor/chrome/panels/variable-panel.tsx +1 -5
  79. package/src/components/editor/chrome/wrapper/minimap-state.ts +1 -1
  80. package/src/components/editor/columns/__tests__/cell-column.test.tsx +1 -1
  81. package/src/components/editor/columns/cell-column.tsx +1 -1
  82. package/src/components/editor/columns/sortable-column.tsx +1 -1
  83. package/src/components/editor/links/__tests__/cell-link.test.tsx +1 -1
  84. package/src/components/editor/links/cell-link-list.tsx +1 -1
  85. package/src/components/editor/links/cell-link.tsx +1 -1
  86. package/src/components/editor/navigation/__tests__/navigation.test.ts +20 -6
  87. package/src/components/editor/navigation/__tests__/selection.test.ts +1 -2
  88. package/src/components/editor/navigation/clipboard.ts +5 -3
  89. package/src/components/editor/renderers/cell-array.tsx +7 -12
  90. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +1 -1
  91. package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +4 -4
  92. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +1 -1
  93. package/src/components/editor/stdin-blocking-alert.tsx +1 -1
  94. package/src/components/slides/__tests__/minimap-actions.test.tsx +13 -4
  95. package/src/components/slides/__tests__/minimap.test.ts +38 -19
  96. package/src/components/slides/minimap.tsx +11 -10
  97. package/src/components/sort/SortableCellsProvider.tsx +2 -21
  98. package/src/components/tracing/tracing.tsx +1 -1
  99. package/src/core/ai/__tests__/staged-cells.test.ts +1 -1
  100. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +8 -12
  101. package/src/core/ai/context/providers/cell-output.ts +2 -2
  102. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +34 -66
  103. package/src/core/ai/tools/edit-notebook-tool.ts +12 -10
  104. package/src/core/cells/__tests__/actions.test.ts +64 -25
  105. package/src/core/cells/__tests__/arrange.test.ts +156 -4
  106. package/src/core/cells/__tests__/cells.test.ts +12 -290
  107. package/src/core/cells/__tests__/document.test.ts +2 -46
  108. package/src/core/cells/__tests__/harness.ts +28 -4
  109. package/src/core/cells/__tests__/layout.test.ts +42 -54
  110. package/src/core/cells/__tests__/session.test.ts +0 -2
  111. package/src/core/cells/__tests__/utils.test.ts +9 -107
  112. package/src/core/cells/actions.ts +112 -26
  113. package/src/core/cells/add-missing-import.ts +1 -1
  114. package/src/core/cells/arrange.ts +206 -13
  115. package/src/core/cells/cells.ts +28 -256
  116. package/src/core/cells/document.ts +18 -122
  117. package/src/core/cells/layout.ts +79 -10
  118. package/src/core/cells/pending-delete-service.ts +1 -1
  119. package/src/core/cells/utils.ts +4 -4
  120. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +6 -7
  121. package/src/core/codemirror/copilot/getCodes.ts +2 -2
  122. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +1 -2
  123. package/src/core/codemirror/lsp/utils.ts +1 -1
  124. package/src/core/documentation/doc-lookup.ts +1 -1
  125. package/src/core/edit-app.tsx +2 -5
  126. package/src/core/errors/error-entries.ts +1 -1
  127. package/src/core/islands/__tests__/bootstrap.test.ts +2 -6
  128. package/src/core/islands/components/web-components.tsx +1 -1
  129. package/src/core/notebook/document/__tests__/codemirror.test.ts +8 -0
  130. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +1 -1
  131. package/src/stories/cell.stories.tsx +1 -2
  132. package/src/stories/layout/vertical/one-column.stories.tsx +1 -2
  133. package/src/stories/runtime-story.tsx +2 -3
  134. package/src/utils/__tests__/id-tree.test.ts +0 -617
  135. package/src/utils/id-tree.tsx +0 -609
@@ -11,7 +11,6 @@ import {
11
11
  createCellRuntimeState,
12
12
  } from "@/core/cells/types";
13
13
  import type { MarimoError } from "@/core/kernel/messages";
14
- import { MultiColumn } from "@/utils/id-tree";
15
14
  import { Objects } from "@/utils/objects";
16
15
 
17
16
  export const MockNotebook = {
@@ -46,7 +45,7 @@ export const MockNotebook = {
46
45
  serializedEditorState: null,
47
46
  ...data,
48
47
  })),
49
- cellIds: MultiColumn.from([Object.keys(cellData) as CellId[]]),
48
+ cellIds: Object.keys(cellData) as CellId[],
50
49
  cellRuntime: Objects.mapValues(cellData, (_data, cid) =>
51
50
  createCellRuntimeState({ ...cellRuntime[cid] }),
52
51
  ),
@@ -134,7 +134,7 @@ const sortedTablesAtom = atom((get) => {
134
134
  return 0;
135
135
  }
136
136
 
137
- const index = cellIds.inOrderIds.indexOf(variable.declaredBy[0]);
137
+ const index = cellIds.indexOf(variable.declaredBy[0]);
138
138
  if (index === -1) {
139
139
  return 0;
140
140
  }
@@ -30,7 +30,7 @@ const EQUALITY_CHECK = (
30
30
  export const CustomNode = memo((props: CustomNodeProps) => {
31
31
  const { data, selected } = props; // must match the equality check
32
32
  const cell = useAtomValue(data.atom);
33
- const cellIndex = useCellIds().inOrderIds.indexOf(cell.id);
33
+ const cellIndex = useCellIds().indexOf(cell.id);
34
34
  const handleColor = selected ? "var(--gray-9)" : "var(--gray-3)";
35
35
  const edgeMarkers = use(EdgeMarkerContext);
36
36
 
@@ -7,10 +7,11 @@ import {
7
7
  cellGraphsAtom,
8
8
  isVariableAffectedBySelectedCell,
9
9
  } from "@/components/editor/chrome/wrapper/minimap-state";
10
- import { useCellData, useCellIds, useCellRuntime } from "@/core/cells/cells";
10
+ import { useCellData, useCellRuntime } from "@/core/cells/cells";
11
11
  import { useCellActions } from "@/core/cells/actions";
12
12
  import { cellFocusAtom, useCellFocusActions } from "@/core/cells/focus";
13
13
  import type { CellId } from "@/core/cells/ids";
14
+ import { useLayout } from "@/core/cells/layout";
14
15
  import { useVariables } from "@/core/variables/state";
15
16
  import { cn } from "@/utils/cn";
16
17
  import { extractCellPreview } from "./utils/cell-preview";
@@ -380,28 +381,25 @@ function isNonReferenceableCell(graph: CellGraph): boolean {
380
381
  * Shows a scrollable list of cells with dependency visualization.
381
382
  */
382
383
  export const MinimapContent: React.FC = () => {
383
- const cellIds = useCellIds();
384
+ const layout = useLayout();
385
+ const rows = layout.columns.flatMap((column, index) =>
386
+ column.ids.map((cellId, position) => ({
387
+ cellId,
388
+ startsColumn: index > 0 && position === 0,
389
+ })),
390
+ );
384
391
  const cellPositions: Record<CellId, number> = Object.fromEntries(
385
- cellIds.inOrderIds.map((id, idx) => [id, idx]),
392
+ rows.map(({ cellId }, idx) => [cellId, idx]),
386
393
  );
387
- const columnBoundaries: number[] = [];
388
- let cellCount = 0;
389
- for (const [idx, column] of cellIds.getColumns().entries()) {
390
- if (idx > 0) {
391
- columnBoundaries.push(cellCount);
392
- }
393
- cellCount += column.inOrderIds.length;
394
- }
395
394
 
396
395
  return (
397
396
  <div className="overflow-y-auto overflow-x-hidden flex-1 scrollbar-none h-full max-w-80">
398
397
  <div className="py-3 pl-3 pr-4 relative min-h-full">
399
- {cellIds.inOrderIds.map((cellId, idx) => {
400
- const isColumnBoundary = columnBoundaries.includes(idx);
398
+ {rows.map(({ cellId, startsColumn }, idx) => {
401
399
  return (
402
400
  <React.Fragment key={cellId}>
403
401
  {/* Subtle visual divider between nodes */}
404
- {isColumnBoundary && (
402
+ {startsColumn && (
405
403
  <div
406
404
  className="absolute left-5 w-[36px] h-px bg-(--gray-4) pointer-events-none"
407
405
  aria-hidden="true"
@@ -29,7 +29,7 @@ export const useSetCodeVisibility = () => {
29
29
 
30
30
  const newConfigs: Record<CellId, Partial<CellConfig>> = {};
31
31
 
32
- for (const cellId of notebook.cellIds.inOrderIds) {
32
+ for (const cellId of notebook.cellIds) {
33
33
  const cell = notebook.cellData[cellId];
34
34
  if (cell === undefined || cell.config.hide_code === hidden) {
35
35
  continue;
@@ -19,7 +19,7 @@ export function outputExpansionConfigs(
19
19
  expanded: boolean,
20
20
  ): Record<CellId, Partial<CellConfig>> {
21
21
  const configs: Record<CellId, Partial<CellConfig>> = {};
22
- for (const cellId of notebook.cellIds.inOrderIds) {
22
+ for (const cellId of notebook.cellIds) {
23
23
  const cell = notebook.cellData[cellId];
24
24
  if (
25
25
  cell === undefined ||
@@ -12,7 +12,7 @@ const hasBlockingStdinAtom = atom((get) => {
12
12
  const notebook = get(notebookAtom);
13
13
 
14
14
  // Check each cell in order
15
- for (const cellId of notebook.cellIds.inOrderIds) {
15
+ for (const cellId of notebook.cellIds) {
16
16
  // Check if the cell is idle and has unresolved stdin
17
17
  const runtime = notebook.cellRuntime[cellId];
18
18
  if (runtime.status === "idle") {
@@ -236,7 +236,7 @@ const CellEditorInternal = ({
236
236
  addOrAppendSetupCell: (code) => {
237
237
  const notebook = getNotebook();
238
238
  // No setup cell yet: create one with this code at the top.
239
- if (!notebook.cellIds.setupCellExists()) {
239
+ if (!notebook.cellIds.includes(SETUP_CELL_ID)) {
240
240
  cellActions.addSetupCell({ code });
241
241
  return;
242
242
  }
@@ -11,16 +11,18 @@ import { getEditorCodeAsPython } from "@/core/codemirror/language/utils";
11
11
  export function useSplitCellCallback() {
12
12
  const { splitCell, undoSplitCell } = useCellActions();
13
13
 
14
- return useEvent((opts: { cellId: CellId }) => {
14
+ return useEvent((opts: { cellId: CellId }): CellId | null => {
15
15
  // Save snapshot of code for undo
16
16
  const cellEditorView = getCellEditorView(opts.cellId);
17
17
  if (!cellEditorView) {
18
- return;
18
+ return null;
19
19
  }
20
20
  const code = getEditorCodeAsPython(cellEditorView);
21
21
 
22
- // Optimistic update
23
- splitCell(opts);
22
+ const newCellId = splitCell(opts);
23
+ if (newCellId === null) {
24
+ return null;
25
+ }
24
26
 
25
27
  const { dismiss } = toast({
26
28
  title: "Cell split",
@@ -30,7 +32,7 @@ export function useSplitCellCallback() {
30
32
  size="sm"
31
33
  variant="outline"
32
34
  onClick={() => {
33
- undoSplitCell({ ...opts, snapshot: code });
35
+ undoSplitCell({ ...opts, newCellId, snapshot: code });
34
36
  dismiss();
35
37
  }}
36
38
  >
@@ -38,5 +40,6 @@ export function useSplitCellCallback() {
38
40
  </UndoButton>
39
41
  ),
40
42
  });
43
+ return newCellId;
41
44
  });
42
45
  }
@@ -57,7 +57,7 @@ const DependencyGraphPanel: React.FC = () => {
57
57
  <DependencyGraph
58
58
  cellAtoms={cells}
59
59
  variables={variables}
60
- cellIds={cellIds.inOrderIds}
60
+ cellIds={cellIds}
61
61
  />
62
62
  )}
63
63
  </div>
@@ -88,7 +88,7 @@ const SessionPanel: React.FC = () => {
88
88
  No variables defined
89
89
  </div>
90
90
  ) : (
91
- <VariableTable cellIds={cellIds.inOrderIds} variables={variables} />
91
+ <VariableTable cellIds={cellIds} variables={variables} />
92
92
  )}
93
93
  </PanelAccordionContent>
94
94
  </PanelAccordionItem>
@@ -22,11 +22,7 @@ const VariablePanel: React.FC = () => {
22
22
  }
23
23
 
24
24
  return (
25
- <VariableTable
26
- className="flex-1"
27
- cellIds={cellIds.inOrderIds}
28
- variables={variables}
29
- />
25
+ <VariableTable className="flex-1" cellIds={cellIds} variables={variables} />
30
26
  );
31
27
  };
32
28
 
@@ -104,7 +104,7 @@ export function buildCellGraph(
104
104
  export const cellGraphsAtom = atom((get) => {
105
105
  const cellIds = get(cellIdsAtom);
106
106
  const variables = get(variablesAtom);
107
- return buildCellGraph(cellIds.inOrderIds, variables);
107
+ return buildCellGraph(cellIds, variables);
108
108
  });
109
109
 
110
110
  /**
@@ -3,7 +3,7 @@ import { render } from "@testing-library/react";
3
3
  import { createStore, Provider } from "jotai";
4
4
  import { describe, expect, it } from "vitest";
5
5
  import { TooltipProvider } from "@/components/ui/tooltip";
6
- import type { CellColumnId } from "@/utils/id-tree";
6
+ import type { CellColumnId } from "@/core/cells/layout";
7
7
  import { Column } from "../cell-column";
8
8
 
9
9
  const columnId = "col-1" as CellColumnId;
@@ -4,7 +4,7 @@ import React, { memo, useRef } from "react";
4
4
  import type { AppConfig } from "@/core/config/config-schema";
5
5
  import { useResizeHandle } from "@/hooks/useResizeHandle";
6
6
  import { cn } from "@/utils/cn";
7
- import type { CellColumnId } from "@/utils/id-tree";
7
+ import type { CellColumnId } from "@/core/cells/layout";
8
8
  import { SortableColumn } from "./sortable-column";
9
9
  import { storageFn } from "./storage";
10
10
 
@@ -21,7 +21,7 @@ import {
21
21
  import { Tooltip } from "@/components/ui/tooltip";
22
22
  import { useCellActions } from "@/core/cells/actions";
23
23
  import { cn } from "@/utils/cn";
24
- import type { CellColumnId } from "@/utils/id-tree";
24
+ import type { CellColumnId } from "@/core/cells/layout";
25
25
  import { mergeRefs } from "../../../utils/mergeRefs";
26
26
 
27
27
  interface Props extends React.HTMLAttributes<HTMLDivElement> {
@@ -15,7 +15,7 @@ vi.mock("@/core/cells/actions", () => ({
15
15
  useCellActions: () => ({ showCellIfHidden }),
16
16
  }));
17
17
  vi.mock("@/core/cells/cells", () => ({
18
- useCellIds: () => ({ inOrderIds: [targetId] }),
18
+ useCellIds: () => [targetId],
19
19
  useCellNames: () => ({ [targetId]: "Target" }),
20
20
  }));
21
21
  vi.mock("@/core/saving/filename", () => ({ useFilename: () => "notebook.py" }));
@@ -25,7 +25,7 @@ export const CellLinkList: React.FC<Props> = ({
25
25
  }) => {
26
26
  const cellIndex = useCellIds();
27
27
  const sortedCellIds = cellIds.toSorted((a, b) => {
28
- return cellIndex.inOrderIds.indexOf(a) - cellIndex.inOrderIds.indexOf(b);
28
+ return cellIndex.indexOf(a) - cellIndex.indexOf(b);
29
29
  });
30
30
 
31
31
  if (cellIds.length === 0) {
@@ -27,7 +27,7 @@ export const CellLink = (props: Props): JSX.Element => {
27
27
  const { className, cellId, variant, onClick, formatCellName, skipScroll } =
28
28
  props;
29
29
  const cellName = useCellNames()[cellId] ?? "";
30
- const cellIndex = useCellIds().inOrderIds.indexOf(cellId);
30
+ const cellIndex = useCellIds().indexOf(cellId);
31
31
  const { showCellIfHidden } = useCellActions();
32
32
  const formatName = formatCellName ?? ((name: string) => name);
33
33
 
@@ -125,7 +125,6 @@ afterAll(() => {
125
125
  });
126
126
 
127
127
  import { defaultUserConfig } from "@/core/config/config-schema";
128
- import { MultiColumn } from "@/utils/id-tree";
129
128
  import { focusCell, focusCellEditor } from "../focus-utils";
130
129
  import {
131
130
  type CellSelectionState,
@@ -1427,12 +1426,27 @@ describe("useCellNavigationProps", () => {
1427
1426
  },
1428
1427
  },
1429
1428
  });
1430
- // Override cellIds to be multi-column
1431
- notebookState.cellIds = MultiColumn.from([
1432
- [cellId1, cellId3],
1433
- [cellId2, cellId4, cellId5],
1434
- ]);
1435
1429
  store.set(notebookAtom, notebookState);
1430
+ // The layout reads columns from the document's cells.
1431
+ stopModel();
1432
+ const inColumn = (id: CellId, column: number) =>
1433
+ cell(id, "", { config: { ...CONFIG, column } });
1434
+ ({ model, stop: stopModel } = mockDocumentModel(
1435
+ store,
1436
+ withCells(
1437
+ documentOf(),
1438
+ {
1439
+ c1: null,
1440
+ c2: null,
1441
+ [cellId1]: inColumn(cellId1, 0),
1442
+ [cellId3]: inColumn(cellId3, 0),
1443
+ [cellId2]: inColumn(cellId2, 1),
1444
+ [cellId4]: inColumn(cellId4, 1),
1445
+ [cellId5]: inColumn(cellId5, 1),
1446
+ },
1447
+ [cellId1, cellId3, cellId2, cellId4, cellId5],
1448
+ ),
1449
+ ));
1436
1450
 
1437
1451
  // Mock DOM elements and their positions
1438
1452
  const mockGetElementById = vi.spyOn(document, "getElementById");
@@ -3,8 +3,7 @@
3
3
  import { beforeEach, describe, expect, it } from "vitest";
4
4
  import { cellId } from "@/__tests__/branded";
5
5
  import type { CellId } from "@/core/cells/ids";
6
- import type { Layout } from "@/core/cells/layout";
7
- import type { CellColumnId } from "@/utils/id-tree";
6
+ import type { CellColumnId, Layout } from "@/core/cells/layout";
8
7
  import type { CellSelectionState } from "../selection";
9
8
  import { exportedForTesting } from "../selection";
10
9
 
@@ -6,6 +6,8 @@ import { toast } from "@/components/ui/use-toast";
6
6
  import { getNotebook } from "@/core/cells/cells";
7
7
  import { useCellActions } from "@/core/cells/actions";
8
8
  import type { CellId } from "@/core/cells/ids";
9
+ import { columnOf, layoutAtom } from "@/core/cells/layout";
10
+ import { store } from "@/core/state/jotai";
9
11
  import {
10
12
  usePendingCutActions,
11
13
  usePendingCutState,
@@ -140,10 +142,10 @@ export function useCellClipboard() {
140
142
  // Check if we have pending cut cells (internal move)
141
143
  if (pendingCutState.cellIds.size > 0) {
142
144
  const pendingCellIds = [...pendingCutState.cellIds];
143
- const notebook = getNotebook();
145
+ const layout = store.get(layoutAtom);
144
146
  const previousPlacements = pendingCellIds.map((id) => {
145
- const column = notebook.cellIds.findWithId(id);
146
- return { columnId: column.id, index: column.indexOfOrThrow(id) };
147
+ const column = columnOf(layout, id);
148
+ return { columnId: column.id, index: column.ids.indexOf(id) };
147
149
  });
148
150
 
149
151
  actions.moveCellsRelativeTo({
@@ -29,14 +29,9 @@ import { canInteractWithAppAtom } from "@/core/network/connection";
29
29
  import { useBoolean } from "@/hooks/useBoolean";
30
30
  import { cn } from "@/utils/cn";
31
31
  import { Functions } from "@/utils/functions";
32
- import { findColumn, useLayout } from "@/core/cells/layout";
33
- import type { CellColumnId } from "@/utils/id-tree";
32
+ import { type CellColumnId, findColumn, useLayout } from "@/core/cells/layout";
34
33
  import { invariant } from "@/utils/invariant";
35
- import {
36
- columnIdsAtom,
37
- useCellIds,
38
- useScrollKey,
39
- } from "../../../core/cells/cells";
34
+ import { useCellIds, useScrollKey } from "../../../core/cells/cells";
40
35
  import { useCellActions } from "../../../core/cells/actions";
41
36
  import { formatAll } from "../../../core/codemirror/format";
42
37
  import type { AppConfig, UserConfig } from "../../../core/config/config-schema";
@@ -61,7 +56,7 @@ interface CellArrayProps {
61
56
  }
62
57
 
63
58
  export const CellArray: React.FC<CellArrayProps> = (props) => {
64
- const columnIds = useAtomValue(columnIdsAtom);
59
+ const columnIds = useLayout().columns.map((column) => column.id);
65
60
 
66
61
  // Setup context for sorting
67
62
  return (
@@ -116,7 +111,7 @@ const CellArrayInternal: React.FC<CellArrayProps> = ({
116
111
 
117
112
  const cellIds = useCellIds();
118
113
  const scrollKey = useScrollKey();
119
- const columnIds = cellIds.getColumnIds();
114
+ const columnIds = useLayout().columns.map((column) => column.id);
120
115
 
121
116
  // Scroll to a cell targeted by a previous action
122
117
  const scrollToTarget = actions.scrollToTarget;
@@ -140,7 +135,7 @@ const CellArrayInternal: React.FC<CellArrayProps> = ({
140
135
  <StdinBlockingAlert />
141
136
  <NotebookBanner width={appConfig.width} />
142
137
  {/* Only show if not cells, otherwise running a single cell will start the connection */}
143
- {cellIds.idLength === 0 && <NotStartedConnectionAlert />}
138
+ {cellIds.length === 0 && <NotStartedConnectionAlert />}
144
139
  <div
145
140
  className={cn(
146
141
  appConfig.width === "columns" &&
@@ -192,8 +187,8 @@ const CellColumn: React.FC<{
192
187
  const column = findColumn(useLayout(), columnId);
193
188
  invariant(column, `Expected column for: ${columnId}`);
194
189
 
195
- const hasOnlyOneCell = cellIds.hasOnlyOneId();
196
- const hasSetupCell = cellIds.inOrderIds.includes(SETUP_CELL_ID);
190
+ const hasOnlyOneCell = cellIds.length === 1;
191
+ const hasSetupCell = cellIds.includes(SETUP_CELL_ID);
197
192
  const isPresenting = mode === "present";
198
193
 
199
194
  return (
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import React, { useMemo, useState } from "react";
3
3
  import { useAtomValue } from "jotai";
4
- import { numColumnsAtom } from "@/core/cells/cells";
4
+ import { numColumnsAtom } from "@/core/cells/layout";
5
5
  import type { CellId } from "@/core/cells/ids";
6
6
  import { kioskModeAtom } from "@/core/mode";
7
7
  import type { ICellRendererProps } from "../types";
@@ -25,7 +25,7 @@ describe("useFocusFirstEditor", () => {
25
25
  const getNotebookSpy = vi
26
26
  .spyOn(cellsModule, "getNotebook")
27
27
  .mockReturnValue({
28
- cellIds: { iterateTopLevelIds: ["cell-1", "cell-2"] },
28
+ cellIds: ["cell-1", "cell-2"],
29
29
  cellData: {
30
30
  "cell-1": { config: { hide_code: false } },
31
31
  "cell-2": { config: { hide_code: false } },
@@ -52,7 +52,7 @@ describe("useFocusFirstEditor", () => {
52
52
  const getNotebookSpy = vi
53
53
  .spyOn(cellsModule, "getNotebook")
54
54
  .mockReturnValue({
55
- cellIds: { iterateTopLevelIds: ["cell-1", "cell-2"] },
55
+ cellIds: ["cell-1", "cell-2"],
56
56
  cellData: {
57
57
  "cell-1": { config: { hide_code: true } }, // hidden
58
58
  "cell-2": { config: { hide_code: false } }, // visible
@@ -94,7 +94,7 @@ describe("useFocusFirstEditor", () => {
94
94
  // Mock getNotebook
95
95
  const mockEditor = { focus: vi.fn() };
96
96
  vi.spyOn(cellsModule, "getNotebook").mockReturnValue({
97
- cellIds: { iterateTopLevelIds: [] },
97
+ cellIds: [],
98
98
  cellData: {},
99
99
  cellHandles: {
100
100
  "cell-123": { current: { editorView: mockEditor } },
@@ -129,7 +129,7 @@ describe("useFocusFirstEditor", () => {
129
129
 
130
130
  const mockEditor = { focus: vi.fn() };
131
131
  vi.spyOn(cellsModule, "getNotebook").mockReturnValue({
132
- cellIds: { iterateTopLevelIds: ["cell-1"] },
132
+ cellIds: ["cell-1"],
133
133
  cellData: {
134
134
  "cell-1": { config: { hide_code: false } },
135
135
  },
@@ -52,7 +52,7 @@ function focusFirstEditor() {
52
52
  const { cellIds, cellData, cellHandles } = getNotebook();
53
53
 
54
54
  // Focus on the first cell if it's been mounted and is not hidden
55
- for (const cellId of cellIds.iterateTopLevelIds) {
55
+ for (const cellId of cellIds) {
56
56
  const handle = cellHandles[cellId];
57
57
  const hidden = cellData[cellId]?.config.hide_code;
58
58
  if (!hidden && handle?.current?.editorView) {
@@ -13,7 +13,7 @@ const hasBlockingStdinAtom = atom((get) => {
13
13
  const notebook = get(notebookAtom);
14
14
 
15
15
  // Check each cell in order
16
- for (const cellId of notebook.cellIds.inOrderIds) {
16
+ for (const cellId of notebook.cellIds) {
17
17
  // Check if the cell is idle and has unresolved stdin
18
18
  const runtime = notebook.cellRuntime[cellId];
19
19
  if (runtime.status === "idle") {
@@ -6,7 +6,7 @@ import { cellId } from "@/__tests__/branded";
6
6
  import { TooltipProvider } from "@/components/ui/tooltip";
7
7
  import type { CellId } from "@/core/cells/ids";
8
8
  import type { CellData, CellRuntimeState } from "@/core/cells/types";
9
- import { MultiColumn } from "@/utils/id-tree";
9
+ import type { CellColumnId } from "@/core/cells/layout";
10
10
  import { SlidesMinimap } from "../minimap";
11
11
 
12
12
  const A = cellId("a");
@@ -22,11 +22,20 @@ const { createCell, deleteCell, moveCellToIndex } = vi.hoisted(() => ({
22
22
  vi.mock("@/core/cells/actions", () => ({
23
23
  useCellActions: () => ({ moveCellToIndex, createCell }),
24
24
  }));
25
- vi.mock("@/core/cells/cells", async (importOriginal) => {
26
- const actual = await importOriginal<typeof import("@/core/cells/cells")>();
25
+ vi.mock("@/core/cells/layout", async (importOriginal) => {
26
+ const actual = await importOriginal<typeof import("@/core/cells/layout")>();
27
27
  return {
28
28
  ...actual,
29
- useCellIds: () => MultiColumn.from([[A, B]]),
29
+ useLayout: () => ({
30
+ columns: [
31
+ {
32
+ id: "column-0" as CellColumnId,
33
+ ids: [A, B],
34
+ visible: [A, B],
35
+ folds: new Map(),
36
+ },
37
+ ],
38
+ }),
30
39
  };
31
40
  });
32
41
 
@@ -4,7 +4,7 @@ import { renderHook, act } from "@testing-library/react";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import type { CellId } from "@/core/cells/ids";
6
6
  import type { DragMoveEvent } from "@dnd-kit/core";
7
- import { MultiColumn } from "@/utils/id-tree";
7
+ import type { CellColumnId, Layout } from "@/core/cells/layout";
8
8
  import { exportedForTesting } from "../minimap";
9
9
 
10
10
  const { useVisibleCellIds, projectDropTarget, resolveDropTarget } =
@@ -318,15 +318,23 @@ describe("resolveDropTarget", () => {
318
318
  const C = "c" as CellId;
319
319
  const D = "d" as CellId;
320
320
 
321
- function makeSingleColumnIds(...ids: CellId[]) {
322
- return MultiColumn.from([ids]);
321
+ function makeLayout(...columns: CellId[][]): Layout {
322
+ return {
323
+ columns: columns.map((ids, index) => ({
324
+ id: `column-${index}` as CellColumnId,
325
+ ids,
326
+ visible: ids,
327
+ folds: new Map(),
328
+ })),
329
+ };
323
330
  }
331
+ const makeSingleColumnIds = (...ids: CellId[]) => makeLayout(ids);
324
332
 
325
333
  it("returns null when activeId matches target overId", () => {
326
- const cellIds = makeSingleColumnIds(A, B);
334
+ const layout = makeSingleColumnIds(A, B);
327
335
  expect(
328
336
  resolveDropTarget({
329
- cellIds,
337
+ layout,
330
338
  activeId: A,
331
339
  target: { overId: A, position: "after" },
332
340
  }),
@@ -334,68 +342,79 @@ describe("resolveDropTarget", () => {
334
342
  });
335
343
 
336
344
  it("returns null for multi-column layouts", () => {
337
- const cellIds = MultiColumn.from([[A, B], [C]]);
345
+ const layout = makeLayout([A, B], [C]);
338
346
  expect(
339
347
  resolveDropTarget({
340
- cellIds,
348
+ layout,
341
349
  activeId: A,
342
350
  target: { overId: B, position: "after" },
343
351
  }),
344
352
  ).toBeNull();
345
353
  });
346
354
 
355
+ it("returns null when the over cell has left the layout", () => {
356
+ const layout = makeSingleColumnIds(A, B);
357
+ expect(
358
+ resolveDropTarget({
359
+ layout,
360
+ activeId: A,
361
+ target: { overId: C, position: "after" },
362
+ }),
363
+ ).toBeNull();
364
+ });
365
+
347
366
  it('resolves "before" to the over index', () => {
348
- const cellIds = makeSingleColumnIds(A, B, C);
367
+ const layout = makeSingleColumnIds(A, B, C);
349
368
  const result = resolveDropTarget({
350
- cellIds,
369
+ layout,
351
370
  activeId: A,
352
371
  target: { overId: C, position: "before" },
353
372
  });
354
373
  expect(result).toEqual({
355
374
  cellId: A,
356
- columnId: cellIds.atOrThrow(0).id,
375
+ columnId: layout.columns[0].id,
357
376
  index: 2,
358
377
  });
359
378
  });
360
379
 
361
380
  it('resolves "after" to one past the over index', () => {
362
- const cellIds = makeSingleColumnIds(A, B, C);
381
+ const layout = makeSingleColumnIds(A, B, C);
363
382
  const result = resolveDropTarget({
364
- cellIds,
383
+ layout,
365
384
  activeId: C,
366
385
  target: { overId: A, position: "after" },
367
386
  });
368
387
  expect(result).toEqual({
369
388
  cellId: C,
370
- columnId: cellIds.atOrThrow(0).id,
389
+ columnId: layout.columns[0].id,
371
390
  index: 1,
372
391
  });
373
392
  });
374
393
 
375
394
  it('resolves "before" on first item to index 0', () => {
376
- const cellIds = makeSingleColumnIds(A, B, C);
395
+ const layout = makeSingleColumnIds(A, B, C);
377
396
  const result = resolveDropTarget({
378
- cellIds,
397
+ layout,
379
398
  activeId: C,
380
399
  target: { overId: A, position: "before" },
381
400
  });
382
401
  expect(result).toEqual({
383
402
  cellId: C,
384
- columnId: cellIds.atOrThrow(0).id,
403
+ columnId: layout.columns[0].id,
385
404
  index: 0,
386
405
  });
387
406
  });
388
407
 
389
408
  it('resolves "after" on last item to one past the end', () => {
390
- const cellIds = makeSingleColumnIds(A, B, C, D);
409
+ const layout = makeSingleColumnIds(A, B, C, D);
391
410
  const result = resolveDropTarget({
392
- cellIds,
411
+ layout,
393
412
  activeId: A,
394
413
  target: { overId: D, position: "after" },
395
414
  });
396
415
  expect(result).toEqual({
397
416
  cellId: A,
398
- columnId: cellIds.atOrThrow(0).id,
417
+ columnId: layout.columns[0].id,
399
418
  index: 4,
400
419
  });
401
420
  });