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

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 (134) hide show
  1. package/dist/{ErrorBoundary-wT3f3iJ1.js → ErrorBoundary-DNvcAIsP.js} +1 -1
  2. package/dist/{any-language-editor-B92WMzoC.js → any-language-editor-BVFHDDQ3.js} +2 -2
  3. package/dist/{chat-ui-CuORr6fL.js → chat-ui-X-cWrL9u.js} +1222 -1222
  4. package/dist/{code-visibility-ZReaeMaF.js → code-visibility-CH1lmM1h.js} +2 -2
  5. package/dist/{common-DCevHg1m.js → common-DIrS1UR9.js} +1074 -1078
  6. package/dist/{data-grid-overlay-editor-CTC-bJus.js → data-grid-overlay-editor-DFLZvFK0.js} +1 -1
  7. package/dist/{dist-F8Ep36rk.js → dist-B1M0bGO3.js} +1 -1
  8. package/dist/{dist-D6d86eNJ.js → dist-BA13RJj5.js} +1 -1
  9. package/dist/{dist-Bvs24R_E.js → dist-BJtmSgcE.js} +48 -48
  10. package/dist/{dist-CN_BeG2I.js → dist-Ba2VIn1K.js} +53 -53
  11. package/dist/{dist-PXYI_yuP.js → dist-Bj4bmwUm.js} +12 -12
  12. package/dist/{dist-DoE4XJBC.js → dist-BmS-ZGjl.js} +8626 -8626
  13. package/dist/{dist-BorMD-uX.js → dist-Bzogh7CW.js} +87 -87
  14. package/dist/{dist-CN8zI-XC.js → dist-CThPVKaP.js} +1385 -1382
  15. package/dist/{dist-DiDibOYr.js → dist-CTi5FTiV.js} +87 -87
  16. package/dist/{dist-diIx5qJW.js → dist-Ck_C32ye.js} +2 -2
  17. package/dist/{dist-BLfKmR-l.js → dist-D1fnFp1J.js} +1 -1
  18. package/dist/{dist-OR1OnN-y.js → dist-D3x9__Fu.js} +46 -46
  19. package/dist/{dist-BMQf5zIS.js → dist-D7mrh5sO.js} +36 -36
  20. package/dist/{dist-CSJ0MDRw.js → dist-D9Q3xjiP.js} +1 -1
  21. package/dist/{dist-BuaRHxOc.js → dist-DP60Ubh8.js} +2 -2
  22. package/dist/{dist-CSbh1PNP.js → dist-DTv9KkpW.js} +12 -12
  23. package/dist/{dist-D-wwqlzJ.js → dist-DVVwOfrD.js} +109 -109
  24. package/dist/{dist-C8fAKSH1.js → dist-DYXM73-k.js} +36 -36
  25. package/dist/{dist-DBtSl3sf.js → dist-DjeybPOb.js} +1 -1
  26. package/dist/{dist-CrSP_H1M.js → dist-DtGn9N4h.js} +197 -197
  27. package/dist/{dist-CLdpGQVr.js → dist-KN80qWCP.js} +2 -2
  28. package/dist/{dist-Cg7gzw-a.js → dist-O0qFpEJh.js} +56 -56
  29. package/dist/{dist-CkTTIdBe.js → dist-OUle2RT7.js} +207 -207
  30. package/dist/{dist-BK4Oj99o.js → dist-T5rh6JvL.js} +1 -1
  31. package/dist/{error-entries-CmWMEsY_.js → error-entries-mpoD5t79.js} +4939 -4983
  32. package/dist/{esm-D2gTsXEV.js → esm-Bbw_Njjx.js} +575 -575
  33. package/dist/{esm-AMFssoNp.js → esm-mDQNEFe_.js} +354 -354
  34. package/dist/{feedback-button-DwXkmewo.js → feedback-button-DL0DJ-LB.js} +21 -21
  35. package/dist/{glide-data-editor-BlmcP-lw.js → glide-data-editor-n4wnx19S.js} +3 -3
  36. package/dist/{globals-BdRitZ4p.js → globals-NCsYyjpB.js} +1 -1
  37. package/dist/{html-to-image-bpiQh7ND.js → html-to-image-BoZthB4P.js} +454 -445
  38. package/dist/main.js +3587 -3587
  39. package/dist/{number-overlay-editor-DQG-hFHT.js → number-overlay-editor-rlvo4mo3.js} +1 -1
  40. package/dist/{process-output-BzloOI7F.js → process-output-DLS4Mqg_.js} +2 -2
  41. package/dist/{reveal-component-D3r3vSTK.js → reveal-component-BJfmRUzb.js} +3 -3
  42. package/package.json +1 -1
  43. package/src/__mocks__/document.tsx +54 -0
  44. package/src/__mocks__/notebook.ts +2 -1
  45. package/src/__mocks__/requests.ts +0 -2
  46. package/src/components/chat/chat-panel.tsx +3 -3
  47. package/src/components/datasources/__tests__/column-preview.test.tsx +6 -8
  48. package/src/components/datasources/column-preview.tsx +4 -8
  49. package/src/components/datasources/datasources.tsx +4 -3
  50. package/src/components/dependency-graph/minimap-content.tsx +2 -6
  51. package/src/components/dependency-graph/panels.tsx +2 -1
  52. package/src/components/editor/__tests__/Output.test.tsx +50 -32
  53. package/src/components/editor/__tests__/data-attributes.test.tsx +22 -11
  54. package/src/components/editor/__tests__/output-persistence.test.tsx +6 -1
  55. package/src/components/editor/actions/__tests__/name-cell-input.test.tsx +69 -0
  56. package/src/components/editor/actions/name-cell-input.tsx +4 -3
  57. package/src/components/editor/actions/useCellActionButton.tsx +20 -26
  58. package/src/components/editor/actions/useNotebookActions.tsx +13 -18
  59. package/src/components/editor/actions/useSetCodeVisibility.ts +10 -11
  60. package/src/components/editor/actions/useSetOutputsExpanded.ts +13 -18
  61. package/src/components/editor/cell/CreateCellButton.tsx +5 -9
  62. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +3 -1
  63. package/src/components/editor/cell/code/cell-editor.tsx +13 -20
  64. package/src/components/editor/cell/useAddCell.ts +4 -8
  65. package/src/components/editor/cell/useDeleteCell.tsx +10 -36
  66. package/src/components/editor/cell/useRunCells.ts +2 -5
  67. package/src/components/editor/cell/useSplitCell.tsx +2 -1
  68. package/src/components/editor/chrome/panels/logs-panel.tsx +2 -1
  69. package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -11
  70. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +3 -1
  71. package/src/components/editor/columns/sortable-column.tsx +1 -1
  72. package/src/components/editor/connections/components.tsx +4 -9
  73. package/src/components/editor/controls/Controls.tsx +1 -1
  74. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +9 -3
  75. package/src/components/editor/documentation.css +34 -11
  76. package/src/components/editor/errors/auto-fix.tsx +4 -8
  77. package/src/components/editor/file-tree/file-explorer.tsx +3 -7
  78. package/src/components/editor/links/__tests__/cell-link.test.tsx +3 -1
  79. package/src/components/editor/links/cell-link.tsx +2 -1
  80. package/src/components/editor/navigation/__tests__/clipboard.test.ts +60 -38
  81. package/src/components/editor/navigation/__tests__/navigation.test.ts +73 -64
  82. package/src/components/editor/navigation/clipboard.ts +12 -11
  83. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +11 -20
  84. package/src/components/editor/navigation/navigation.ts +20 -28
  85. package/src/components/editor/notebook-cell.tsx +7 -10
  86. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +125 -69
  87. package/src/components/editor/renderers/cell-array.tsx +10 -17
  88. package/src/components/markdown/markdown-renderer.tsx +4 -8
  89. package/src/components/pages/edit-page.tsx +32 -17
  90. package/src/components/scratchpad/scratchpad.test.tsx +3 -1
  91. package/src/components/scratchpad/scratchpad.tsx +4 -7
  92. package/src/components/slides/__tests__/minimap-actions.test.tsx +9 -25
  93. package/src/components/slides/minimap.tsx +7 -3
  94. package/src/components/slides/slide-cell-view.tsx +3 -3
  95. package/src/components/sort/SortableCellsProvider.tsx +2 -1
  96. package/src/core/ai/__tests__/staged-cells.test.ts +18 -20
  97. package/src/core/ai/staged-cells.ts +8 -16
  98. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +2 -2
  99. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +2 -2
  100. package/src/core/ai/tools/base.ts +2 -2
  101. package/src/core/ai/tools/edit-notebook-tool.ts +13 -21
  102. package/src/core/cells/__tests__/actions.test.ts +474 -34
  103. package/src/core/cells/__tests__/add-missing-import.test.ts +7 -8
  104. package/src/core/cells/__tests__/cells.test.ts +171 -813
  105. package/src/core/cells/__tests__/document.test.ts +5 -35
  106. package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
  107. package/src/core/cells/__tests__/outputs.test.tsx +71 -53
  108. package/src/core/cells/actions.ts +391 -3
  109. package/src/core/cells/add-missing-import.ts +46 -42
  110. package/src/core/cells/cells.ts +38 -413
  111. package/src/core/cells/document.ts +7 -4
  112. package/src/core/cells/outputs.ts +12 -28
  113. package/src/core/codemirror/cells/extensions.ts +6 -6
  114. package/src/core/codemirror/cells/state.ts +1 -1
  115. package/src/core/codemirror/keymaps/vim.ts +2 -2
  116. package/src/core/edit-app.tsx +19 -27
  117. package/src/core/islands/bridge.ts +0 -2
  118. package/src/core/kernel/handlers.ts +3 -3
  119. package/src/core/network/__tests__/requests-lazy.test.ts +1 -3
  120. package/src/core/network/requests-lazy.ts +0 -2
  121. package/src/core/network/requests-network.ts +0 -16
  122. package/src/core/network/requests-static.ts +0 -2
  123. package/src/core/network/requests-toasting.tsx +0 -2
  124. package/src/core/network/types.ts +0 -7
  125. package/src/core/notebook/document/__tests__/codemirror.test.ts +5 -0
  126. package/src/core/notebook/document/__tests__/draft.test.ts +41 -0
  127. package/src/core/notebook/document/__tests__/state.test.ts +21 -0
  128. package/src/core/notebook/document/draft.ts +35 -5
  129. package/src/core/notebook/document/index.ts +1 -0
  130. package/src/core/notebook/document/state.ts +6 -2
  131. package/src/core/session/useNotebookSession.tsx +6 -2
  132. package/src/core/wasm/bridge.ts +0 -16
  133. package/src/css/globals.css +56 -22
  134. package/src/stories/runtime-story.tsx +22 -15
@@ -27,7 +27,7 @@ import type { CellConfig } from "../network/types";
27
27
  import { canUseRtc, isRtcEnabled } from "../rtc/state";
28
28
  import { store } from "../state/jotai";
29
29
  import { activeDocumentModelAtom } from "../session/active-notebook";
30
- import type { CellActions, NotebookState } from "./cells";
30
+ import type { NotebookState, ReducerCellActions } from "./cells";
31
31
  import { groupByColumn } from "./columns";
32
32
  import type { CellId } from "./ids";
33
33
  import {
@@ -38,7 +38,7 @@ import {
38
38
  isEdited,
39
39
  } from "./types";
40
40
 
41
- type CellAction = DispatchedActionOf<CellActions>;
41
+ type CellAction = DispatchedActionOf<ReducerCellActions>;
42
42
 
43
43
  // Following the document ----------------------------------------------------
44
44
 
@@ -126,14 +126,17 @@ export function applyDocument(
126
126
  continue;
127
127
  }
128
128
  if (current === undefined) {
129
- // A new cell has no run to be edited from.
129
+ // A new cell with code is stale until it runs. The kernel's last run
130
+ // settles it on kernel-ready.
131
+ const code = cell.code ?? "";
130
132
  cellData = {
131
133
  ...cellData,
132
134
  [id]: createCell({
133
135
  id,
134
- code: cell.code ?? "",
136
+ code,
135
137
  name: cell.name,
136
138
  config: createCellConfig(cell.config),
139
+ edited: code.trim().length > 0,
137
140
  }),
138
141
  };
139
142
  cellRuntime = { ...cellRuntime, [id]: createCellRuntimeState() };
@@ -1,15 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { useAtomValue } from "jotai";
4
- import { selectAtom } from "jotai/utils";
5
- import { useEffect, useMemo, useState } from "react";
4
+ import { useEffect, useState } from "react";
6
5
  import { useEvent } from "@/hooks/useEvent";
7
- import { Logger } from "@/utils/Logger";
6
+ import {
7
+ useNotebookCellConfig,
8
+ useNotebookDocumentEdit,
9
+ } from "../notebook/document";
8
10
  import type { OutputMessage } from "../kernel/messages";
9
11
  import { viewStateAtom } from "../mode";
10
- import { getRequestClient } from "../network/requests";
11
- import { cellDataAtom, useCellActions } from "./cells";
12
- import type { CellId } from "./ids";
12
+ import { type CellId, SCRATCH_CELL_ID } from "./ids";
13
13
 
14
14
  // This does not need to be overcomplicated. We can just store the expanded
15
15
  // state in a global map instead of Jotai since state is not shared between cells.
@@ -27,20 +27,9 @@ const expandedConsoleOutputs: Record<CellId, boolean> = {};
27
27
  */
28
28
  export function useExpandedOutput(cellId: CellId) {
29
29
  const { mode } = useAtomValue(viewStateAtom);
30
- const { updateCellConfig } = useCellActions();
31
- // Select just the flag: every output would otherwise re-render on each
32
- // keystroke in its cell.
33
- const isConfigExpanded = useAtomValue(
34
- useMemo(
35
- () =>
36
- selectAtom(
37
- cellDataAtom(cellId),
38
- (data) => data?.config.expand_output ?? false,
39
- Object.is,
40
- ),
41
- [cellId],
42
- ),
43
- );
30
+ const edit = useNotebookDocumentEdit();
31
+ const isConfigExpanded =
32
+ useNotebookCellConfig(cellId)?.expand_output ?? false;
44
33
  const [isSessionExpanded, setIsSessionExpanded] = useState(
45
34
  () => expandedOutputs[cellId] ?? isConfigExpanded,
46
35
  );
@@ -55,16 +44,11 @@ export function useExpandedOutput(cellId: CellId) {
55
44
 
56
45
  const setIsExpanded = useEvent((expanded: boolean) => {
57
46
  setIsSessionExpanded(expanded);
58
- if (!isEditable) {
47
+ // The scratchpad is not in the document.
48
+ if (!isEditable || cellId === SCRATCH_CELL_ID) {
59
49
  return;
60
50
  }
61
- const config = { expand_output: expanded };
62
- updateCellConfig({ cellId, config });
63
- getRequestClient()
64
- .saveCellConfig({ configs: { [cellId]: config } })
65
- .catch((error) => {
66
- Logger.error("Failed to save expand_output config", error);
67
- });
51
+ edit((tx) => tx.updateConfig(cellId, { expand_output: expanded }));
68
52
  });
69
53
 
70
54
  return [isExpanded, setIsExpanded] as const;
@@ -190,7 +190,7 @@ function cellKeymaps({
190
190
 
191
191
  if (isAtStartOfEditor(ev)) {
192
192
  const actions = ev.state.facet(cellActionsState);
193
- actions.moveToNextCell({ cellId, before: true, noCreate: true });
193
+ actions.moveToNextCell({ cellId, before: true, create: false });
194
194
  return true;
195
195
  }
196
196
  return false;
@@ -207,7 +207,7 @@ function cellKeymaps({
207
207
 
208
208
  if (isAtEndOfEditor(ev)) {
209
209
  const actions = ev.state.facet(cellActionsState);
210
- actions.moveToNextCell({ cellId, before: false, noCreate: true });
210
+ actions.moveToNextCell({ cellId, before: false, create: false });
211
211
  return true;
212
212
  }
213
213
  return false;
@@ -219,7 +219,7 @@ function cellKeymaps({
219
219
  stopPropagation: true,
220
220
  run: (ev) => {
221
221
  const actions = ev.state.facet(cellActionsState);
222
- actions.moveToNextCell({ cellId, before: false, noCreate: true });
222
+ actions.moveToNextCell({ cellId, before: false, create: false });
223
223
  return true;
224
224
  },
225
225
  },
@@ -229,7 +229,7 @@ function cellKeymaps({
229
229
  stopPropagation: true,
230
230
  run: (ev) => {
231
231
  const actions = ev.state.facet(cellActionsState);
232
- actions.moveToNextCell({ cellId, before: true, noCreate: true });
232
+ actions.moveToNextCell({ cellId, before: true, create: false });
233
233
  return true;
234
234
  },
235
235
  },
@@ -260,7 +260,7 @@ function cellKeymaps({
260
260
  run: (ev) => {
261
261
  ev.contentDOM.blur();
262
262
  const actions = ev.state.facet(cellActionsState);
263
- actions.createNewCell({ cellId, before: true });
263
+ actions.createCell({ before: cellId });
264
264
  return true;
265
265
  },
266
266
  },
@@ -271,7 +271,7 @@ function cellKeymaps({
271
271
  run: (ev) => {
272
272
  ev.contentDOM.blur();
273
273
  const actions = ev.state.facet(cellActionsState);
274
- actions.createNewCell({ cellId, before: false });
274
+ actions.createCell({ after: cellId });
275
275
  return true;
276
276
  },
277
277
  },
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { Facet } from "@codemirror/state";
4
- import type { CellActions } from "@/core/cells/cells";
4
+ import type { CellActions } from "@/core/cells/actions";
5
5
  import type { CellId } from "@/core/cells/ids";
6
6
 
7
7
  export interface CodemirrorCellActions extends CellActions {
@@ -37,7 +37,7 @@ export function vimKeymapExtension(): Extension[] {
37
37
  if (isAtEndOfEditor(ev, true) && isInVimNormalMode(ev)) {
38
38
  const actions = ev.state.facet(cellActionsState);
39
39
  const cellId = ev.state.facet(cellIdState);
40
- actions.moveToNextCell({ cellId, before: false, noCreate: true });
40
+ actions.moveToNextCell({ cellId, before: false, create: false });
41
41
  return true;
42
42
  }
43
43
  return false;
@@ -51,7 +51,7 @@ export function vimKeymapExtension(): Extension[] {
51
51
  if (isAtStartOfEditor(ev) && isInVimNormalMode(ev)) {
52
52
  const actions = ev.state.facet(cellActionsState);
53
53
  const cellId = ev.state.facet(cellIdState);
54
- actions.moveToNextCell({ cellId, before: true, noCreate: true });
54
+ actions.moveToNextCell({ cellId, before: true, create: false });
55
55
  return true;
56
56
  }
57
57
  return false;
@@ -5,8 +5,8 @@ import { Tooltip } from "radix-ui";
5
5
 
6
6
  const TooltipProvider = Tooltip.Provider;
7
7
 
8
- import { useAtomValue, useSetAtom, useStore } from "jotai";
9
- import { useEffect } from "react";
8
+ import { useAtomValue } from "jotai";
9
+ import { lazy, useEffect } from "react";
10
10
  import { NotStartedConnectionAlert } from "@/components/editor/alerts/connecting-alert";
11
11
  import { Controls } from "@/components/editor/controls/Controls";
12
12
  import { SandboxController } from "@/components/editor/chrome/panels/sandbox-controller";
@@ -29,24 +29,23 @@ import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
29
29
  import { useHotkey } from "../hooks/useHotkey";
30
30
  import {
31
31
  hasCellsAtom,
32
- notebookAtom,
33
32
  notebookIsRunningAtom,
34
33
  numColumnsAtom,
35
- useCellActions,
36
34
  } from "./cells/cells";
37
- import { notebookCells } from "./cells/utils";
35
+ import { useCellActions } from "./cells/actions";
38
36
  import type { AppConfig, UserConfig } from "./config/config-schema";
39
- import { DocumentProvider } from "./notebook/document";
40
37
  import { RuntimeState } from "./kernel/RuntimeState";
41
- import { getSessionId } from "./kernel/session";
42
38
  import { useTogglePresenting } from "./layout/useTogglePresenting";
43
39
  import { viewStateAtom } from "./mode";
44
40
  import { useRequestClient } from "./network/requests";
45
41
  import { useFilename } from "./saving/filename";
46
- import { lastSavedNotebookAtom } from "./saving/state";
47
- import { useNotebookSession } from "./session/useNotebookSession";
42
+ import type { ConnectionStatus } from "./session/types";
48
43
 
49
- interface AppProps {
44
+ const LazyCommandPalette = lazy(
45
+ () => import("../components/editor/controls/command-palette"),
46
+ );
47
+
48
+ export interface EditAppProps {
50
49
  /**
51
50
  * The user config.
52
51
  */
@@ -59,20 +58,24 @@ interface AppProps {
59
58
  * If true, the floating controls will be hidden.
60
59
  */
61
60
  hideControls?: boolean;
61
+ /** The session's connection, which the page owns with the document model. */
62
+ connection: ConnectionStatus;
63
+ reconnect: () => Promise<void>;
62
64
  }
63
65
 
64
- export const EditApp: React.FC<AppProps> = ({
66
+ /** The notebook editor. Rendered inside the page's `DocumentProvider`. */
67
+ export const EditApp: React.FC<EditAppProps> = ({
65
68
  userConfig,
66
69
  appConfig,
67
70
  hideControls = false,
71
+ connection,
72
+ reconnect,
68
73
  }) => {
69
74
  const { mergeAllColumns, collapseAllCells, expandAllCells } =
70
75
  useCellActions();
71
- const store = useStore();
72
76
  const viewState = useAtomValue(viewStateAtom);
73
77
  const numColumns = useAtomValue(numColumnsAtom);
74
78
  const filename = useFilename();
75
- const setLastSavedNotebook = useSetAtom(lastSavedNotebookAtom);
76
79
  const { sendComponentValues, sendInterrupt } = useRequestClient();
77
80
 
78
81
  const isEditing = viewState.mode === "edit";
@@ -87,18 +90,6 @@ export const EditApp: React.FC<AppProps> = ({
87
90
  };
88
91
  }, [sendComponentValues]);
89
92
 
90
- const { connection, reconnect, documentModel } = useNotebookSession({
91
- autoInstantiate: userConfig.runtime.auto_instantiate,
92
- onKernelReady: (layout) => {
93
- const cells = notebookCells(store.get(notebookAtom));
94
- const names = cells.map((cell) => cell.name);
95
- const codes = cells.map((cell) => cell.code);
96
- const configs = cells.map((cell) => cell.config);
97
- setLastSavedNotebook({ names, codes, configs, layout });
98
- },
99
- sessionId: getSessionId(),
100
- });
101
-
102
93
  // Update document title whenever filename or app_title changes
103
94
  useEffect(() => {
104
95
  // Set document title: app_title takes precedence, then filename, then default
@@ -178,7 +169,7 @@ export const EditApp: React.FC<AppProps> = ({
178
169
  );
179
170
 
180
171
  return (
181
- <DocumentProvider model={documentModel}>
172
+ <>
182
173
  <SandboxController onReconnect={reconnect} />
183
174
  <AppContainer
184
175
  connection={connection}
@@ -232,6 +223,7 @@ export const EditApp: React.FC<AppProps> = ({
232
223
  />
233
224
  </TooltipProvider>
234
225
  )}
235
- </DocumentProvider>
226
+ <LazyCommandPalette />
227
+ </>
236
228
  );
237
229
  };
@@ -383,12 +383,10 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
383
383
  public sendInterrupt = throwNotImplemented;
384
384
  public sendShutdown = throwNotImplemented;
385
385
  public sendFormat = throwNotImplemented;
386
- public sendDeleteCell = throwNotImplemented;
387
386
  public sendInstallMissingPackages = throwNotImplemented;
388
387
  public sendCodeCompletionRequest = throwNotImplemented;
389
388
  public saveUserConfig = throwNotImplemented;
390
389
  public saveAppConfig = throwNotImplemented;
391
- public saveCellConfig = throwNotImplemented;
392
390
  public sendRestart = throwNotImplemented;
393
391
  public readCode = throwNotImplemented;
394
392
  public readSnippets = throwNotImplemented;
@@ -3,7 +3,7 @@ import type { LayoutType } from "@/components/editor/renderers/types";
3
3
  import { Logger } from "@/utils/Logger";
4
4
  import { Objects } from "@/utils/objects";
5
5
  import type { UIElementId } from "../cells/ids";
6
- import type { CellActions } from "../cells/cells";
6
+ import type { ReducerCellActions } from "../cells/cells";
7
7
  import type { CellData } from "../cells/types";
8
8
  import { type AppConfig, AppConfigSchema } from "../config/config-schema";
9
9
  import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
@@ -63,8 +63,8 @@ export function handleKernelReady(
63
63
  autoInstantiate: boolean;
64
64
  /** The notebook's cells, as the host document provided them. */
65
65
  cells: CellData[];
66
- setLastRun: CellActions["setLastRun"];
67
- resetCellRuntimes: CellActions["resetCellRuntimes"];
66
+ setLastRun: ReducerCellActions["setLastRun"];
67
+ resetCellRuntimes: ReducerCellActions["resetCellRuntimes"];
68
68
  setLayoutData: (payload: {
69
69
  layoutView: LayoutType;
70
70
  data: LayoutData;
@@ -53,7 +53,6 @@ describe("createLazyRequests", () => {
53
53
  sendInstantiate: vi.fn().mockResolvedValue({ instantiated: true }),
54
54
  sendFunctionRequest: vi.fn().mockResolvedValue({ result: "data" }),
55
55
  sendRestart: vi.fn().mockResolvedValue({ restarted: true }),
56
- sendDeleteCell: vi.fn().mockResolvedValue({ deleted: true }),
57
56
  sendInterrupt: vi.fn().mockResolvedValue({ interrupted: true }),
58
57
  sendPdb: vi.fn().mockResolvedValue({ pdb: true }),
59
58
  sendRunScratchpad: vi.fn().mockResolvedValue({ run: true }),
@@ -320,7 +319,6 @@ describe("createLazyRequests", () => {
320
319
  cellIds: [cellId("cell1")],
321
320
  codes: ["code"],
322
321
  });
323
- await lazyRequests.sendDeleteCell({ cellId: cellId("cell2") });
324
322
  await lazyRequests.sendInstantiate({
325
323
  objectIds: [uiElementId("obj1")],
326
324
  values: [],
@@ -328,7 +326,7 @@ describe("createLazyRequests", () => {
328
326
 
329
327
  // Init should only be called once
330
328
  expect(mockInit).toHaveBeenCalledTimes(1);
331
- expect(mockGetRuntimeManager).toHaveBeenCalledTimes(3);
329
+ expect(mockGetRuntimeManager).toHaveBeenCalledTimes(2);
332
330
  });
333
331
  });
334
332
  });
@@ -52,10 +52,8 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
52
52
  sendModelValue: "startConnection",
53
53
  sendInstantiate: "startConnectionWithoutKernel",
54
54
  sendRun: "startConnection",
55
- sendDeleteCell: "startConnection",
56
55
  sendRunScratchpad: "startConnection",
57
56
  saveAppConfig: "startConnection",
58
- saveCellConfig: "startConnection",
59
57
  discoverDataSources: "startConnection",
60
58
 
61
59
  // Export operations start a connection
@@ -142,14 +142,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
142
142
  })
143
143
  .then(handleResponseReturnNull);
144
144
  },
145
- sendDeleteCell: (request) => {
146
- return getClient()
147
- .POST("/api/kernel/delete", {
148
- body: request,
149
- params: getParams(),
150
- })
151
- .then(handleResponseReturnNull);
152
- },
153
145
  sendCodeCompletionRequest: async (request) => {
154
146
  await waitForConnectionOpen();
155
147
  return getClient()
@@ -175,14 +167,6 @@ export function createNetworkRequests(): EditRequests & RunRequests {
175
167
  })
176
168
  .then(handleResponseReturnNull);
177
169
  },
178
- saveCellConfig: (request) => {
179
- return getClient()
180
- .POST("/api/kernel/set_cell_config", {
181
- body: request,
182
- params: getParams(),
183
- })
184
- .then(handleResponseReturnNull);
185
- },
186
170
  sendFunctionRequest: (request) => {
187
171
  return getClient()
188
172
  .POST("/api/kernel/function_call", {
@@ -48,11 +48,9 @@ export function createStaticRequests(): EditRequests & RunRequests {
48
48
  sendInterrupt: throwNotInEditMode,
49
49
  sendShutdown: throwNotInEditMode,
50
50
  sendFormat: throwNotInEditMode,
51
- sendDeleteCell: throwNotInEditMode,
52
51
  sendCodeCompletionRequest: throwNotInEditMode,
53
52
  saveUserConfig: throwNotInEditMode,
54
53
  saveAppConfig: throwNotInEditMode,
55
- saveCellConfig: throwNotInEditMode,
56
54
  sendStdin: throwNotInEditMode,
57
55
  readCode: throwNotInEditMode,
58
56
  readSnippets: throwNotInEditMode,
@@ -29,11 +29,9 @@ export function createErrorToastingRequests(
29
29
  sendInterrupt: "Failed to interrupt",
30
30
  sendShutdown: "Failed to shutdown",
31
31
  sendFormat: "Failed to format",
32
- sendDeleteCell: "Failed to delete cell",
33
32
  sendCodeCompletionRequest: "Failed to complete code",
34
33
  saveUserConfig: "Failed to save user config",
35
34
  saveAppConfig: "Failed to save app config",
36
- saveCellConfig: "Failed to save cell config",
37
35
  sendStdin: "Failed to send stdin",
38
36
  readCode: "Failed to read code",
39
37
  readSnippets: "Failed to fetch snippets",
@@ -1,6 +1,5 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { components, paths } from "@marimo-team/marimo-api";
3
- import type { CellId } from "../cells/ids";
4
3
 
5
4
  export type schemas = components["schemas"];
6
5
  export type AiCompletionRequest = schemas["AiCompletionRequest"];
@@ -16,7 +15,6 @@ export type CellConfig = schemas["CellConfig"];
16
15
  */
17
16
  export type RuntimeState = schemas["CellNotification"]["status"];
18
17
  export type CodeCompletionRequest = schemas["CodeCompletionRequest"];
19
- export type DeleteCellRequest = schemas["DeleteCellRequest"];
20
18
  export type AutoExportAsIPYNBRequest = schemas["AutoExportAsIPYNBRequest"];
21
19
  export type AutoExportAsMarkdownRequest =
22
20
  schemas["AutoExportAsMarkdownRequest"];
@@ -101,9 +99,6 @@ export type SaveNotebookRequest = schemas["SaveNotebookRequest"];
101
99
  export type CopyNotebookRequest = schemas["CopyNotebookRequest"];
102
100
  export type SaveUserConfigurationRequest =
103
101
  schemas["SaveUserConfigurationRequest"];
104
- export interface SetCellConfigRequest {
105
- configs: Record<CellId, Partial<CellConfig>>;
106
- }
107
102
  /**
108
103
  * Client-side shape for creating a file/directory/notebook. The HTTP
109
104
  * transport sends this as multipart/form-data; the WASM bridge base64-encodes
@@ -171,11 +166,9 @@ export interface EditRequests {
171
166
  sendInterrupt: () => Promise<null>;
172
167
  sendShutdown: () => Promise<null>;
173
168
  sendFormat: (request: FormatCellsRequest) => Promise<FormatResponse>;
174
- sendDeleteCell: (request: DeleteCellRequest) => Promise<null>;
175
169
  sendCodeCompletionRequest: (request: CodeCompletionRequest) => Promise<null>;
176
170
  saveUserConfig: (request: SaveUserConfigurationRequest) => Promise<null>;
177
171
  saveAppConfig: (request: SaveAppConfigurationRequest) => Promise<null>;
178
- saveCellConfig: (request: SetCellConfigRequest) => Promise<null>;
179
172
  sendRestart: () => Promise<null>;
180
173
  sendInstallMissingPackages: (
181
174
  request: InstallPackagesRequest,
@@ -60,6 +60,11 @@ function languages(id: CellId) {
60
60
  aiCellCompletion: () => false,
61
61
  createManyBelow: vi.fn(),
62
62
  addOrAppendSetupCell: vi.fn(),
63
+ createCell: vi.fn(),
64
+ moveToNextCell: vi.fn(),
65
+ addSetupCell: vi.fn(),
66
+ deleteCells: vi.fn(),
67
+ undoDeleteCell: vi.fn(),
63
68
  onRun: vi.fn(),
64
69
  deleteCell: vi.fn(),
65
70
  afterToggleMarkdown: vi.fn(),
@@ -9,6 +9,7 @@ import {
9
9
  type EditableCell,
10
10
  editCell,
11
11
  EMPTY_DRAFT,
12
+ restoreCell,
12
13
  setOrder,
13
14
  toChanges,
14
15
  viewDocument,
@@ -99,6 +100,46 @@ describe("createCell and deleteCell", () => {
99
100
  expect(toChanges(confirmed, draft)).toEqual([]);
100
101
  expect(viewDocument(confirmed, draft).order).toEqual(["c1", "c2"]);
101
102
  });
103
+
104
+ it("restores a deleted cell as an edit at the requested place", () => {
105
+ const deleted = deleteCell(EMPTY_DRAFT, "c1");
106
+ const back = { id: "c1", code: "back" };
107
+ const put = {
108
+ kind: "put_cell",
109
+ base_etag: "c1-etag",
110
+ cell: { ...back, name: "_", config: CONFIG },
111
+ };
112
+ const inPlace = restoreCell(confirmed, deleted, back, { before: "c2" });
113
+ const moved = restoreCell(confirmed, deleted, back, { after: "c2" });
114
+ expect(viewDocument(confirmed, inPlace).cells.get("c1")?.code).toBe("back");
115
+ expect(viewDocument(confirmed, inPlace).order).toEqual(["c1", "c2"]);
116
+ expect(toChanges(confirmed, inPlace)).toEqual([put]);
117
+ expect(viewDocument(confirmed, moved).order).toEqual(["c2", "c1"]);
118
+ expect(toChanges(confirmed, moved)).toEqual([
119
+ put,
120
+ { kind: "set_order", base_etag: "order-1", order: ["c2", "c1"] },
121
+ ]);
122
+ });
123
+
124
+ it("recreates a cell whose deletion is in flight once the host removes it", () => {
125
+ const sent = deleteCell(EMPTY_DRAFT, "c1");
126
+ const back = { id: "c1", code: "back" };
127
+ const draft = restoreCell(confirmed, sent, back, { before: "c2" }, sent);
128
+ expect(viewDocument(confirmed, draft).cells.get("c1")?.code).toBe("back");
129
+ expect(viewDocument(confirmed, draft).order).toEqual(["c1", "c2"]);
130
+
131
+ const removed = withCells(confirmed, { c1: null });
132
+ const settled = acknowledge(draft, sent);
133
+ expect(viewDocument(removed, settled).order).toEqual(["c1", "c2"]);
134
+ expect(toChanges(removed, settled)).toEqual([
135
+ {
136
+ kind: "put_cell",
137
+ base_etag: null,
138
+ cell: { ...back, name: "_", config: CONFIG },
139
+ before: "c2",
140
+ },
141
+ ]);
142
+ });
102
143
  });
103
144
 
104
145
  describe("viewDocument", () => {
@@ -124,6 +124,27 @@ describe("document model", () => {
124
124
  },
125
125
  );
126
126
 
127
+ it("brings back a cell deleted earlier in the draft with new content", () => {
128
+ const { store, cellAtom, orderAtom, changesAtom, edit } = setup();
129
+ edit((tx) => tx.deleteCell("c1"));
130
+ edit((tx) => tx.createCell({ id: "c1", code: "back" }, { after: "c2" }));
131
+ expect(store.get(cellAtom("c1"))?.code).toBe("back");
132
+ expect(store.get(orderAtom)).toEqual(["c2", "c1"]);
133
+ expect(store.get(changesAtom)).toEqual([
134
+ {
135
+ kind: "put_cell",
136
+ base_etag: "c1-etag",
137
+ cell: {
138
+ id: "c1",
139
+ code: "back",
140
+ name: "_",
141
+ config: cell("c1", "").config,
142
+ },
143
+ },
144
+ { kind: "set_order", base_etag: "order-1", order: ["c2", "c1"] },
145
+ ]);
146
+ });
147
+
127
148
  it("publishes the visible order when an anchor is deleted later in the draft", () => {
128
149
  const { store, orderAtom, changesAtom, edit } = setup();
129
150
  edit((tx) => {
@@ -253,17 +253,47 @@ export function createCell(
253
253
  input: Protocol.CellInput,
254
254
  placement: Placement = {},
255
255
  ): Draft {
256
+ const cell = parseCell(input);
257
+ const next = withCell(draft, cell.id, { kind: "create", cell, placement });
258
+ return draft.order === null
259
+ ? next
260
+ : { ...next, order: insertAt(draft.order, cell.id, placement) };
261
+ }
262
+
263
+ /**
264
+ * Bring back a cell this draft deletes, with the given content.
265
+ *
266
+ * A deletion not yet sent is withdrawn and the cell edited in place. One in
267
+ * flight is superseded by a creation, so the cell returns once the host has
268
+ * removed it. Either way the cell takes the requested placement.
269
+ */
270
+ export function restoreCell(
271
+ confirmed: Document,
272
+ draft: Draft,
273
+ input: Protocol.CellInput,
274
+ placement: Placement = {},
275
+ sent: Draft = EMPTY_DRAFT,
276
+ ): Draft {
277
+ const { id, code, name, config } = parseCell(input);
278
+ const withdrawn = withCell(draft, id, undefined);
279
+ const next =
280
+ sent.cells.get(id)?.kind === "delete"
281
+ ? createCell(withdrawn, input, placement)
282
+ : editCell(confirmed, withdrawn, id, { code, name, config });
283
+ const { order } = viewDocument(confirmed, next);
284
+ const placed = insertAt(order, id, placement);
285
+ return sameOrder(order, placed) ? next : setOrder(next, placed);
286
+ }
287
+
288
+ /** A cell input with the protocol's defaults filled in. */
289
+ function parseCell(input: Protocol.CellInput): CellWithSource {
256
290
  const parsed = Protocol.CellInput.parse(input);
257
- const cell: CellWithSource = {
291
+ return {
258
292
  id: parsed.id,
259
293
  code: parsed.code,
260
294
  name: parsed.name,
261
295
  config: parsed.config ?? Protocol.CellConfigInput.parse({}),
262
296
  };
263
- const next = withCell(draft, cell.id, { kind: "create", cell, placement });
264
- return draft.order === null
265
- ? next
266
- : { ...next, order: insertAt(draft.order, cell.id, placement) };
267
297
  }
268
298
 
269
299
  /** Forget an unsent creation; otherwise retain the deletion until confirmed. */
@@ -21,3 +21,4 @@ export {
21
21
  type EditableCell,
22
22
  type EditableDocument,
23
23
  } from "./model";
24
+ export type { Placement } from "./draft";
@@ -13,6 +13,7 @@ import {
13
13
  type CellEdit,
14
14
  createCell,
15
15
  deleteCell,
16
+ restoreCell,
16
17
  type Draft,
17
18
  type EditableDocument,
18
19
  editCell,
@@ -30,6 +31,7 @@ export interface DocumentEditor {
30
31
  setCode(id: Protocol.CellId, code: string): void;
31
32
  setName(id: Protocol.CellId, name: string): void;
32
33
  updateConfig(id: Protocol.CellId, patch: Partial<Protocol.CellConfig>): void;
34
+ /** Create a cell, or bring back one this draft deletes with this content. */
33
35
  createCell(cell: Protocol.CellInput, placement?: Placement): void;
34
36
  deleteCell(id: Protocol.CellId): void;
35
37
  /** A permutation of the cells present at this point in the transaction. */
@@ -330,7 +332,7 @@ function editDocument(
330
332
  updateCell(id, { config: Protocol.CellConfig.partial().parse(patch) }),
331
333
  createCell: (cell, placement = {}) => {
332
334
  assertActive();
333
- if (confirmed.cells.has(cell.id) || hasCell(cell.id)) {
335
+ if (hasCell(cell.id)) {
334
336
  throw new Error(`Cell ${cell.id} already exists`);
335
337
  }
336
338
  if (placement.after !== undefined && placement.before !== undefined) {
@@ -340,7 +342,9 @@ function editDocument(
340
342
  if (anchor !== undefined) {
341
343
  requireCell(anchor);
342
344
  }
343
- next = createCell(next, cell, { ...placement });
345
+ next = confirmed.cells.has(cell.id)
346
+ ? restoreCell(confirmed, next, cell, { ...placement }, sent)
347
+ : createCell(next, cell, { ...placement });
344
348
  },
345
349
  deleteCell: (id) => {
346
350
  requireCell(id);