@marimo-team/islands 0.25.3-dev170 → 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 (196) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BbSXV-Xa.js → ConnectedDataExplorerComponent-Le_OBfmt.js} +9 -9
  2. package/dist/{ErrorBoundary-NE86qEWZ.js → ErrorBoundary-DNvcAIsP.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D7n07nXf.js → abnfDiagram-VCTEODGH-Tf1ghyQQ.js} +2 -2
  4. package/dist/{any-language-editor-3E24SnCV.js → any-language-editor-BVFHDDQ3.js} +4 -4
  5. package/dist/{architectureDiagram-5GKGNRK7-D6CjRib5.js → architectureDiagram-5GKGNRK7-xlnbTG0T.js} +1 -1
  6. package/dist/{chat-ui-CC7GekpG.js → chat-ui-X-cWrL9u.js} +1218 -1218
  7. package/dist/{check-SpOMoCLP.js → check-C9Mp9eCK.js} +1 -1
  8. package/dist/{chunk-IMKFNOWR-6dtPz7uS.js → chunk-IMKFNOWR-Cmd4a5jy.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-BwnFNKDA.js → chunk-SHT3W25Y-mSNp-WpO.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-D2eV4vBJ.js → chunk-SVP7TREG-CWC49D8X.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-BDs4-_-r.js → chunk-TICWLB2K-De9FINtG.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-DCJnf_8v.js → classDiagram-ZZMXUADV-DweTkYdp.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-BOmSzPBD.js → classDiagram-v2-VYDZK3BY-A6MR2TiE.js} +2 -2
  14. package/dist/{code-visibility-Bzvc6ajl.js → code-visibility-CH1lmM1h.js} +5 -5
  15. package/dist/{common-Ciw4zkh2.js → common-DIrS1UR9.js} +1081 -1085
  16. package/dist/{copy-icon-C1trdvVt.js → copy-icon-De584pZ4.js} +2 -2
  17. package/dist/{cynefinDiagram-5FMLGOSQ-Bln_Soky.js → cynefinDiagram-5FMLGOSQ-BSvo5le-.js} +1 -1
  18. package/dist/{dagre-GXQ25YYZ-D-nf7WzW.js → dagre-GXQ25YYZ-tXRQslO6.js} +1 -1
  19. package/dist/{data-grid-overlay-editor-AQREmxI7.js → data-grid-overlay-editor-DFLZvFK0.js} +1 -1
  20. package/dist/{diagram-S7CK7UJ4-PsgSMkY2.js → diagram-S7CK7UJ4-r3t5VtwI.js} +1 -1
  21. package/dist/{diagram-UQ7AKVKN-DfwfUuSw.js → diagram-UQ7AKVKN-qgbC8YnT.js} +1 -1
  22. package/dist/{diagram-VX7I27RA-C9_e-oMc.js → diagram-VX7I27RA-CiHxEwtp.js} +1 -1
  23. package/dist/{diagram-Z3DM3KII-CWLa5tuY.js → diagram-Z3DM3KII-D95u15Ev.js} +1 -1
  24. package/dist/{dialog-DgeqyrEZ.js → dialog-DVRfQuhD.js} +24 -24
  25. package/dist/{dist-F8Ep36rk.js → dist-B1M0bGO3.js} +1 -1
  26. package/dist/{dist-D6d86eNJ.js → dist-BA13RJj5.js} +1 -1
  27. package/dist/{dist-Bvs24R_E.js → dist-BJtmSgcE.js} +48 -48
  28. package/dist/{dist-CN_BeG2I.js → dist-Ba2VIn1K.js} +53 -53
  29. package/dist/{dist-PXYI_yuP.js → dist-Bj4bmwUm.js} +12 -12
  30. package/dist/{dist-DoE4XJBC.js → dist-BmS-ZGjl.js} +8626 -8626
  31. package/dist/{dist-BorMD-uX.js → dist-Bzogh7CW.js} +87 -87
  32. package/dist/{dist-CN8zI-XC.js → dist-CThPVKaP.js} +1385 -1382
  33. package/dist/{dist-DiDibOYr.js → dist-CTi5FTiV.js} +87 -87
  34. package/dist/{dist-diIx5qJW.js → dist-Ck_C32ye.js} +2 -2
  35. package/dist/{dist-BLfKmR-l.js → dist-D1fnFp1J.js} +1 -1
  36. package/dist/{dist-OR1OnN-y.js → dist-D3x9__Fu.js} +46 -46
  37. package/dist/{dist-BMQf5zIS.js → dist-D7mrh5sO.js} +36 -36
  38. package/dist/{dist-CSJ0MDRw.js → dist-D9Q3xjiP.js} +1 -1
  39. package/dist/{dist-BuaRHxOc.js → dist-DP60Ubh8.js} +2 -2
  40. package/dist/{dist-CSbh1PNP.js → dist-DTv9KkpW.js} +12 -12
  41. package/dist/{dist-D-wwqlzJ.js → dist-DVVwOfrD.js} +109 -109
  42. package/dist/{dist-C8fAKSH1.js → dist-DYXM73-k.js} +36 -36
  43. package/dist/{dist-DBtSl3sf.js → dist-DjeybPOb.js} +1 -1
  44. package/dist/{dist-CrSP_H1M.js → dist-DtGn9N4h.js} +197 -197
  45. package/dist/{dist-CLdpGQVr.js → dist-KN80qWCP.js} +2 -2
  46. package/dist/{dist-Cg7gzw-a.js → dist-O0qFpEJh.js} +56 -56
  47. package/dist/{dist-CkTTIdBe.js → dist-OUle2RT7.js} +207 -207
  48. package/dist/{dist-BK4Oj99o.js → dist-T5rh6JvL.js} +1 -1
  49. package/dist/{dropdown-menu-B6ftyzOg.js → dropdown-menu-DCMbbQ35.js} +691 -691
  50. package/dist/{ebnfDiagram-PWID7BFC-B1QRM58v.js → ebnfDiagram-PWID7BFC-P7e4bryK.js} +2 -2
  51. package/dist/{erDiagram-RLTQ6QDP-DJK_FqvW.js → erDiagram-RLTQ6QDP-CUHZcmRj.js} +1 -1
  52. package/dist/{error-banner-D40pmfQQ.js → error-banner-DxJ3s8YO.js} +57 -57
  53. package/dist/{error-entries-DgK3wUJR.js → error-entries-mpoD5t79.js} +4985 -5006
  54. package/dist/{esm-D2gTsXEV.js → esm-Bbw_Njjx.js} +575 -575
  55. package/dist/{esm-AMFssoNp.js → esm-mDQNEFe_.js} +354 -354
  56. package/dist/{feedback-button-8a6SETmT.js → feedback-button-DL0DJ-LB.js} +30 -30
  57. package/dist/{flowDiagram-HODETNUW-Uxc25XSJ.js → flowDiagram-HODETNUW-BGZ95AzI.js} +2 -2
  58. package/dist/{formats-DQCyPd5Z.js → formats-DI7HEVv7.js} +1 -1
  59. package/dist/{glide-data-editor-B70oke_v.js → glide-data-editor-n4wnx19S.js} +10 -10
  60. package/dist/{glide-utils-Dz_F4pAY.js → glide-utils-D98laidb.js} +2 -2
  61. package/dist/{globals-Cxd5hNlI.js → globals-NCsYyjpB.js} +3 -3
  62. package/dist/{html-to-image-D4yG5Ru0.js → html-to-image-BoZthB4P.js} +467 -458
  63. package/dist/{infoDiagram-27XIBGKW-DmjxqwL0.js → infoDiagram-27XIBGKW-BF0YX70D.js} +1 -1
  64. package/dist/{ishikawaDiagram-5VMMS53U-NgCLHqQi.js → ishikawaDiagram-5VMMS53U-_itsAAn-.js} +1 -1
  65. package/dist/{kanban-definition-UXKFOSKX-CZaG-iam.js → kanban-definition-UXKFOSKX-VNW2Beqh.js} +1 -1
  66. package/dist/{label-D9_-zSg-.js → label-Qvw1E97W.js} +1 -1
  67. package/dist/main.js +8257 -6606
  68. package/dist/{mermaid-CSvw7c71.js → mermaid-CZ6FrClS.js} +35 -35
  69. package/dist/{mindmap-definition-YA3MSWOX-DJYQBDH_.js → mindmap-definition-YA3MSWOX-DR4ripvd.js} +1 -1
  70. package/dist/{number-overlay-editor-ChK-SKUk.js → number-overlay-editor-rlvo4mo3.js} +1 -1
  71. package/dist/{pegDiagram-XKGWAZYB-CHKO_dAe.js → pegDiagram-XKGWAZYB-CFr3XlGl.js} +2 -2
  72. package/dist/{pieDiagram-E7YTZNPT-DVevbMAo.js → pieDiagram-E7YTZNPT-j91XYSLY.js} +1 -1
  73. package/dist/{process-output-DxvBAeL3.js → process-output-DLS4Mqg_.js} +4 -4
  74. package/dist/{railroadDiagram-O6MQD6OU-Detj1kcB.js → railroadDiagram-O6MQD6OU-Cd47ZV9E.js} +2 -2
  75. package/dist/{requirementDiagram-BXWQKSXE-BLFD0T_Z.js → requirementDiagram-BXWQKSXE-eJH6gLtX.js} +1 -1
  76. package/dist/{reveal-component-CXMXeDZD.js → reveal-component-BJfmRUzb.js} +9 -9
  77. package/dist/{select-CkF_GBxO.js → select-2KOiReNY.js} +324 -324
  78. package/dist/{sequenceDiagram-WJ2MYXX4-BZBgatQV.js → sequenceDiagram-WJ2MYXX4-DdfFEIrm.js} +1 -1
  79. package/dist/{spec-Mnkn3XEr.js → spec-h69nC3SI.js} +4 -4
  80. package/dist/{stateDiagram-D77RDMKH-4BZW9-6_.js → stateDiagram-D77RDMKH-DkjDvip8.js} +2 -2
  81. package/dist/{stateDiagram-v2-MP3YSRHH-C4sKC8Iq.js → stateDiagram-v2-MP3YSRHH-DBHACnOo.js} +2 -2
  82. package/dist/{swimlanes-42K2YHIH-SL5yeGe2.js → swimlanes-42K2YHIH-ogImlOmp.js} +1 -1
  83. package/dist/{swimlanesDiagram-VR7AAH4N-DqPmAYUv.js → swimlanesDiagram-VR7AAH4N-D6x-zsci.js} +2 -2
  84. package/dist/{timeline-definition-24CTP7MA-BfXUMWkq.js → timeline-definition-24CTP7MA-DQpLtsHD.js} +1 -1
  85. package/dist/{toDate-TMubKIVL.js → toDate-CDzoofJo.js} +2 -2
  86. package/dist/{toast-Btbve3Lm.js → toast-B5t9o6s_.js} +47 -47
  87. package/dist/{tooltip-CJ4Rqasp.js → tooltip-s4zZxwKe.js} +67 -67
  88. package/dist/{use-restore-focus-CmGwRNId.js → use-restore-focus-BgDTd2tc.js} +1 -1
  89. package/dist/{useAsyncData-DjMGFzFy.js → useAsyncData-Bn8SxUqM.js} +1 -1
  90. package/dist/{useDateFormatter-eCL_SdeG.js → useDateFormatter-D3Tkfzej.js} +2 -2
  91. package/dist/{useDeepCompareMemoize-BQR5TJot.js → useDeepCompareMemoize-CC2tnPCk.js} +1 -1
  92. package/dist/{useLifecycle-C_GvKWbQ.js → useLifecycle-BJlxHqWa.js} +2 -2
  93. package/dist/{useTheme-Bns2gJZ6.js → useTheme-DMfQYmlG.js} +85 -85
  94. package/dist/{vega-component-BsVptuhz.js → vega-component-CovWHi60.js} +7 -7
  95. package/dist/{vennDiagram-4TSXK5OY-CH65znGa.js → vennDiagram-4TSXK5OY-BD-SL_vq.js} +1 -1
  96. package/dist/{wardleyDiagram-VM6X3IG4-z9pWKb2_.js → wardleyDiagram-VM6X3IG4-BDgJV4ah.js} +1 -1
  97. package/dist/{xychartDiagram-S5SC5T6Z-MxyaWjjq.js → xychartDiagram-S5SC5T6Z-1hIHN6Rl.js} +1 -1
  98. package/package.json +1 -1
  99. package/src/__mocks__/document.tsx +54 -0
  100. package/src/__mocks__/notebook.ts +2 -1
  101. package/src/__mocks__/requests.ts +0 -2
  102. package/src/components/chat/chat-panel.tsx +3 -3
  103. package/src/components/datasources/__tests__/column-preview.test.tsx +6 -8
  104. package/src/components/datasources/column-preview.tsx +4 -8
  105. package/src/components/datasources/datasources.tsx +4 -3
  106. package/src/components/dependency-graph/minimap-content.tsx +2 -6
  107. package/src/components/dependency-graph/panels.tsx +2 -1
  108. package/src/components/editor/__tests__/Output.test.tsx +50 -32
  109. package/src/components/editor/__tests__/data-attributes.test.tsx +22 -11
  110. package/src/components/editor/__tests__/output-persistence.test.tsx +6 -1
  111. package/src/components/editor/actions/__tests__/name-cell-input.test.tsx +69 -0
  112. package/src/components/editor/actions/name-cell-input.tsx +4 -3
  113. package/src/components/editor/actions/useCellActionButton.tsx +20 -26
  114. package/src/components/editor/actions/useNotebookActions.tsx +13 -18
  115. package/src/components/editor/actions/useSetCodeVisibility.ts +10 -11
  116. package/src/components/editor/actions/useSetOutputsExpanded.ts +13 -18
  117. package/src/components/editor/cell/CreateCellButton.tsx +5 -9
  118. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +3 -1
  119. package/src/components/editor/cell/code/cell-editor.tsx +13 -20
  120. package/src/components/editor/cell/useAddCell.ts +4 -8
  121. package/src/components/editor/cell/useDeleteCell.tsx +10 -36
  122. package/src/components/editor/cell/useRunCells.ts +2 -5
  123. package/src/components/editor/cell/useSplitCell.tsx +2 -1
  124. package/src/components/editor/chrome/panels/logs-panel.tsx +2 -1
  125. package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -11
  126. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +3 -1
  127. package/src/components/editor/columns/sortable-column.tsx +1 -1
  128. package/src/components/editor/connections/components.tsx +4 -9
  129. package/src/components/editor/controls/Controls.tsx +1 -1
  130. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +9 -3
  131. package/src/components/editor/documentation.css +34 -11
  132. package/src/components/editor/errors/auto-fix.tsx +4 -8
  133. package/src/components/editor/file-tree/file-explorer.tsx +3 -7
  134. package/src/components/editor/links/__tests__/cell-link.test.tsx +3 -1
  135. package/src/components/editor/links/cell-link.tsx +2 -1
  136. package/src/components/editor/navigation/__tests__/clipboard.test.ts +60 -38
  137. package/src/components/editor/navigation/__tests__/navigation.test.ts +73 -64
  138. package/src/components/editor/navigation/clipboard.ts +12 -11
  139. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +11 -20
  140. package/src/components/editor/navigation/navigation.ts +20 -28
  141. package/src/components/editor/notebook-cell.tsx +7 -10
  142. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +125 -69
  143. package/src/components/editor/renderers/cell-array.tsx +10 -17
  144. package/src/components/markdown/markdown-renderer.tsx +4 -8
  145. package/src/components/pages/edit-page.tsx +32 -17
  146. package/src/components/scratchpad/scratchpad.test.tsx +3 -1
  147. package/src/components/scratchpad/scratchpad.tsx +4 -7
  148. package/src/components/slides/__tests__/minimap-actions.test.tsx +9 -25
  149. package/src/components/slides/minimap.tsx +7 -3
  150. package/src/components/slides/slide-cell-view.tsx +3 -3
  151. package/src/components/sort/SortableCellsProvider.tsx +2 -1
  152. package/src/core/ai/__tests__/staged-cells.test.ts +18 -20
  153. package/src/core/ai/staged-cells.ts +8 -16
  154. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +2 -2
  155. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +2 -2
  156. package/src/core/ai/tools/base.ts +2 -2
  157. package/src/core/ai/tools/edit-notebook-tool.ts +13 -21
  158. package/src/core/cells/__tests__/actions.test.ts +474 -34
  159. package/src/core/cells/__tests__/add-missing-import.test.ts +7 -8
  160. package/src/core/cells/__tests__/cells.test.ts +251 -861
  161. package/src/core/cells/__tests__/document.test.ts +42 -38
  162. package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
  163. package/src/core/cells/__tests__/outputs.test.tsx +71 -53
  164. package/src/core/cells/actions.ts +391 -3
  165. package/src/core/cells/add-missing-import.ts +46 -42
  166. package/src/core/cells/cells.ts +73 -444
  167. package/src/core/cells/document.ts +38 -6
  168. package/src/core/cells/outputs.ts +12 -28
  169. package/src/core/codemirror/cells/extensions.ts +6 -6
  170. package/src/core/codemirror/cells/state.ts +1 -1
  171. package/src/core/codemirror/keymaps/vim.ts +2 -2
  172. package/src/core/edit-app.tsx +20 -25
  173. package/src/core/islands/__tests__/bootstrap.test.ts +56 -0
  174. package/src/core/islands/__tests__/bridge.test.ts +42 -0
  175. package/src/core/islands/bootstrap.ts +24 -1
  176. package/src/core/islands/bridge.ts +46 -5
  177. package/src/core/kernel/__tests__/handlers.test.ts +7 -153
  178. package/src/core/kernel/handlers.ts +18 -59
  179. package/src/core/network/__tests__/requests-lazy.test.ts +1 -3
  180. package/src/core/network/requests-lazy.ts +0 -2
  181. package/src/core/network/requests-network.ts +0 -16
  182. package/src/core/network/requests-static.ts +0 -2
  183. package/src/core/network/requests-toasting.tsx +0 -2
  184. package/src/core/network/types.ts +0 -7
  185. package/src/core/notebook/document/__tests__/codemirror.test.ts +5 -0
  186. package/src/core/notebook/document/__tests__/draft.test.ts +41 -0
  187. package/src/core/notebook/document/__tests__/state.test.ts +21 -0
  188. package/src/core/notebook/document/draft.ts +35 -5
  189. package/src/core/notebook/document/index.ts +1 -0
  190. package/src/core/notebook/document/state.ts +6 -2
  191. package/src/core/run-app.tsx +1 -7
  192. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +0 -6
  193. package/src/core/session/useNotebookSession.tsx +28 -35
  194. package/src/core/wasm/bridge.ts +0 -16
  195. package/src/css/globals.css +56 -22
  196. package/src/stories/runtime-story.tsx +22 -15
@@ -10,11 +10,13 @@
10
10
 
11
11
  import type * as Protocol from "@marimo-team/client/protocol";
12
12
  import { dequal as isEqual } from "dequal";
13
+ import type { createStore } from "jotai";
13
14
  import { createRef, type RefObject } from "react";
14
15
  import type { CellHandle } from "@/components/editor/notebook-cell";
15
16
  import type {
16
17
  DocumentEdit,
17
18
  DocumentEditor,
19
+ DocumentModel,
18
20
  EditableCell,
19
21
  EditableDocument,
20
22
  } from "@/core/notebook/document";
@@ -25,7 +27,7 @@ import type { CellConfig } from "../network/types";
25
27
  import { canUseRtc, isRtcEnabled } from "../rtc/state";
26
28
  import { store } from "../state/jotai";
27
29
  import { activeDocumentModelAtom } from "../session/active-notebook";
28
- import type { CellActions, NotebookState } from "./cells";
30
+ import type { NotebookState, ReducerCellActions } from "./cells";
29
31
  import { groupByColumn } from "./columns";
30
32
  import type { CellId } from "./ids";
31
33
  import {
@@ -36,7 +38,7 @@ import {
36
38
  isEdited,
37
39
  } from "./types";
38
40
 
39
- type CellAction = DispatchedActionOf<CellActions>;
41
+ type CellAction = DispatchedActionOf<ReducerCellActions>;
40
42
 
41
43
  // Following the document ----------------------------------------------------
42
44
 
@@ -124,14 +126,17 @@ export function applyDocument(
124
126
  continue;
125
127
  }
126
128
  if (current === undefined) {
127
- // 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 ?? "";
128
132
  cellData = {
129
133
  ...cellData,
130
134
  [id]: createCell({
131
135
  id,
132
- code: cell.code ?? "",
136
+ code,
133
137
  name: cell.name,
134
138
  config: createCellConfig(cell.config),
139
+ edited: code.trim().length > 0,
135
140
  }),
136
141
  };
137
142
  cellRuntime = { ...cellRuntime, [id]: createCellRuntimeState() };
@@ -165,6 +170,33 @@ export function applyDocument(
165
170
  return { ...state, cellIds, cellData, cellRuntime, cellHandles };
166
171
  }
167
172
 
173
+ /**
174
+ * Reconcile the model's document now and on each change, until the signal aborts.
175
+ *
176
+ * TODO(MO-8028): Delete with `applyDocument`.
177
+ */
178
+ export function followDocument({
179
+ store,
180
+ model,
181
+ reconcile,
182
+ signal,
183
+ }: {
184
+ store: ReturnType<typeof createStore>;
185
+ model: Pick<DocumentModel, "documentAtom">;
186
+ reconcile: (document: EditableDocument) => void;
187
+ signal: AbortSignal;
188
+ }): void {
189
+ const follow = () => {
190
+ const document = store.get(model.documentAtom);
191
+ if (document !== null) {
192
+ reconcile(document);
193
+ }
194
+ };
195
+ const unsubscribe = store.sub(model.documentAtom, follow);
196
+ signal.addEventListener("abort", unsubscribe, { once: true });
197
+ follow();
198
+ }
199
+
168
200
  // Editing the document ------------------------------------------------------
169
201
 
170
202
  const CONFIG_FIELDS = ["disabled", "hide_code", "expand_output"] as const;
@@ -291,8 +323,8 @@ export function documentEditMiddleware(
291
323
  next: NotebookState,
292
324
  action: CellAction,
293
325
  ): void {
294
- // The kernel's cells replace the notebook rather than edit it.
295
- if (action.type === "setCells") {
326
+ // The document replaces the notebook's cells, so following it is no edit.
327
+ if (action.type === "reconcileDocument") {
296
328
  return;
297
329
  }
298
330
  if (prev.cellIds === next.cellIds && prev.cellData === next.cellData) {
@@ -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 } 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";
@@ -31,20 +31,21 @@ import {
31
31
  hasCellsAtom,
32
32
  notebookIsRunningAtom,
33
33
  numColumnsAtom,
34
- useCellActions,
35
34
  } from "./cells/cells";
35
+ import { useCellActions } from "./cells/actions";
36
36
  import type { AppConfig, UserConfig } from "./config/config-schema";
37
- import { DocumentProvider } from "./notebook/document";
38
37
  import { RuntimeState } from "./kernel/RuntimeState";
39
- import { getSessionId } from "./kernel/session";
40
38
  import { useTogglePresenting } from "./layout/useTogglePresenting";
41
39
  import { viewStateAtom } from "./mode";
42
40
  import { useRequestClient } from "./network/requests";
43
41
  import { useFilename } from "./saving/filename";
44
- import { lastSavedNotebookAtom } from "./saving/state";
45
- import { useNotebookSession } from "./session/useNotebookSession";
42
+ import type { ConnectionStatus } from "./session/types";
46
43
 
47
- interface AppProps {
44
+ const LazyCommandPalette = lazy(
45
+ () => import("../components/editor/controls/command-palette"),
46
+ );
47
+
48
+ export interface EditAppProps {
48
49
  /**
49
50
  * The user config.
50
51
  */
@@ -57,19 +58,24 @@ interface AppProps {
57
58
  * If true, the floating controls will be hidden.
58
59
  */
59
60
  hideControls?: boolean;
61
+ /** The session's connection, which the page owns with the document model. */
62
+ connection: ConnectionStatus;
63
+ reconnect: () => Promise<void>;
60
64
  }
61
65
 
62
- export const EditApp: React.FC<AppProps> = ({
66
+ /** The notebook editor. Rendered inside the page's `DocumentProvider`. */
67
+ export const EditApp: React.FC<EditAppProps> = ({
63
68
  userConfig,
64
69
  appConfig,
65
70
  hideControls = false,
71
+ connection,
72
+ reconnect,
66
73
  }) => {
67
- const { setCells, mergeAllColumns, collapseAllCells, expandAllCells } =
74
+ const { mergeAllColumns, collapseAllCells, expandAllCells } =
68
75
  useCellActions();
69
76
  const viewState = useAtomValue(viewStateAtom);
70
77
  const numColumns = useAtomValue(numColumnsAtom);
71
78
  const filename = useFilename();
72
- const setLastSavedNotebook = useSetAtom(lastSavedNotebookAtom);
73
79
  const { sendComponentValues, sendInterrupt } = useRequestClient();
74
80
 
75
81
  const isEditing = viewState.mode === "edit";
@@ -84,18 +90,6 @@ export const EditApp: React.FC<AppProps> = ({
84
90
  };
85
91
  }, [sendComponentValues]);
86
92
 
87
- const { connection, reconnect, documentModel } = useNotebookSession({
88
- autoInstantiate: userConfig.runtime.auto_instantiate,
89
- setCells: (cells, layout) => {
90
- setCells(cells);
91
- const names = cells.map((cell) => cell.name);
92
- const codes = cells.map((cell) => cell.code);
93
- const configs = cells.map((cell) => cell.config);
94
- setLastSavedNotebook({ names, codes, configs, layout });
95
- },
96
- sessionId: getSessionId(),
97
- });
98
-
99
93
  // Update document title whenever filename or app_title changes
100
94
  useEffect(() => {
101
95
  // Set document title: app_title takes precedence, then filename, then default
@@ -175,7 +169,7 @@ export const EditApp: React.FC<AppProps> = ({
175
169
  );
176
170
 
177
171
  return (
178
- <DocumentProvider model={documentModel}>
172
+ <>
179
173
  <SandboxController onReconnect={reconnect} />
180
174
  <AppContainer
181
175
  connection={connection}
@@ -229,6 +223,7 @@ export const EditApp: React.FC<AppProps> = ({
229
223
  />
230
224
  </TooltipProvider>
231
225
  )}
232
- </DocumentProvider>
226
+ <LazyCommandPalette />
227
+ </>
233
228
  );
234
229
  };
@@ -1,7 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { NotebookTransport } from "@marimo-team/client";
3
4
  import { createStore } from "jotai";
4
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { cellId } from "@/__tests__/branded";
7
+ import { notebookAtom } from "@/core/cells/cells";
8
+ import {
9
+ fakeBackend,
10
+ fixtureMessages,
11
+ } from "@/core/notebook/replica/__tests__/fixtures";
5
12
  import { RuntimeState } from "@/core/kernel/RuntimeState";
6
13
  import { initializeIslands } from "../bootstrap";
7
14
  import type { IslandsPyodideBridge } from "../bridge";
@@ -32,6 +39,7 @@ describe("initializeIslands", () => {
32
39
  consumeMessage = consumer;
33
40
  },
34
41
  sendComponentValues: vi.fn(),
42
+ notebookBackend: fakeBackend(),
35
43
  } as unknown as IslandsPyodideBridge;
36
44
  const store = createStore();
37
45
  vi.spyOn(RuntimeState.INSTANCE, "start").mockImplementation(
@@ -59,3 +67,51 @@ describe("initializeIslands", () => {
59
67
  expect(store.get(islandsHydratedAtom)).toBe(true);
60
68
  });
61
69
  });
70
+
71
+ describe("initializeIslands document", () => {
72
+ afterEach(vi.restoreAllMocks);
73
+
74
+ it("fills the notebook's cells from the managed session's host document", async () => {
75
+ const root = document.createElement("div");
76
+ root.innerHTML =
77
+ '<marimo-island data-app-id="app-1" data-reactive="true"></marimo-island>';
78
+ let handlers: Parameters<NotebookTransport>[1] | undefined;
79
+ const transport: NotebookTransport = (_attempt, opened) =>
80
+ new Promise(() => {
81
+ handlers = opened;
82
+ opened.open();
83
+ });
84
+ const bridge = {
85
+ consumeNotifications: vi.fn(),
86
+ sendComponentValues: vi.fn(),
87
+ notebookBackend: fakeBackend({ transport }),
88
+ } as unknown as IslandsPyodideBridge;
89
+ const store = createStore();
90
+ vi.spyOn(RuntimeState.INSTANCE, "start").mockImplementation(
91
+ () => undefined,
92
+ );
93
+
94
+ await initializeIslands({
95
+ bridge,
96
+ store,
97
+ root,
98
+ autoInitializePlugins: false,
99
+ });
100
+ await vi.waitFor(() => expect(handlers).toBeDefined());
101
+ for (const message of fixtureMessages("notebook-events.sse")) {
102
+ handlers?.message(message);
103
+ }
104
+
105
+ const notebook = store.get(notebookAtom);
106
+ expect(notebook.cellIds.inOrderIds).toEqual(["c1", "c3", "k7f2"]);
107
+ expect(notebook.cellData[cellId("c3")].code).toBe("x = 21");
108
+ expect(notebook.cellRuntime[cellId("c3")]).toBeDefined();
109
+
110
+ handlers?.message({ event: "reset", data: {} });
111
+ expect(store.get(notebookAtom).cellIds.inOrderIds).toEqual([
112
+ "c1",
113
+ "c3",
114
+ "k7f2",
115
+ ]);
116
+ });
117
+ });
@@ -396,6 +396,48 @@ describe("IslandsPyodideBridge", () => {
396
396
  expect(consumeMessage.mock.calls).toEqual([notification]);
397
397
  });
398
398
 
399
+ it("forwards the managed session's host events to the notebook stream", async () => {
400
+ await initializeSingleApp();
401
+ const handlers = { open: vi.fn(), message: vi.fn() };
402
+ void bridge.notebookBackend.transport(
403
+ { signal: new AbortController().signal, cursor: null },
404
+ handlers,
405
+ );
406
+ expect(handlers.open).toHaveBeenCalledOnce();
407
+
408
+ const cell = { event: "cell", id: "1", data: { cell: { id: "c1" } } };
409
+ sendNotebookEvent(1, cell);
410
+ sendNotebookEvent(2, { event: "ready", id: "2", data: {} });
411
+ sendKernelMessage(1);
412
+
413
+ expect(handlers.message.mock.calls).toEqual([[cell]]);
414
+ expect(() => bridge.notebookBackend.notebook("n1")).toThrow(
415
+ "Islands do not edit",
416
+ );
417
+ });
418
+
419
+ it("resets the notebook stream when the managed session is replaced or stopped", async () => {
420
+ await initializeSingleApp();
421
+ const handlers = { open: vi.fn(), message: vi.fn() };
422
+ void bridge.notebookBackend.transport(
423
+ { signal: new AbortController().signal, cursor: null },
424
+ handlers,
425
+ );
426
+
427
+ mockSingleApp("generated app 2");
428
+ await bridge.initializeApps();
429
+ const ready = { event: "ready", id: "2", data: {} };
430
+ sendNotebookEvent(1, { event: "ready", id: "1", data: {} });
431
+ sendNotebookEvent(2, ready);
432
+ await bridge.stopSession("app-1");
433
+
434
+ expect(handlers.message.mock.calls).toEqual([
435
+ [{ event: "reset", data: {} }],
436
+ [ready],
437
+ [{ event: "reset", data: {} }],
438
+ ]);
439
+ });
440
+
399
441
  it("holds controls until the first app session is ready", async () => {
400
442
  setSingleApp();
401
443
 
@@ -17,10 +17,14 @@ import {
17
17
  notebookAtom,
18
18
  notebookReducer,
19
19
  } from "../cells/cells";
20
+ import { followDocument } from "../cells/document";
21
+ import { notebookCells } from "../cells/utils";
22
+ import { createDocumentModel } from "../notebook/document";
20
23
  import { defineCustomElement } from "../dom/defineCustomElement";
21
24
  import { MarimoValueInputEvent } from "../dom/events";
22
25
  import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
23
26
  import { FUNCTIONS_REGISTRY } from "../functions/FunctionRegistry";
27
+ import { createNotebookReplica } from "../notebook/replica";
24
28
  import {
25
29
  handleCellNotificationeration,
26
30
  handleKernelReady,
@@ -124,6 +128,23 @@ export async function initializeIslands(
124
128
  handleNotification(notification, actions, store, sessionGeneration);
125
129
  });
126
130
 
131
+ // The notebook's cells follow the managed session's host document.
132
+ const replica = createNotebookReplica(store);
133
+ replica.connect(bridge.notebookBackend, { onNotification: Functions.NOOP });
134
+ const documentModel = createDocumentModel({
135
+ store,
136
+ documentAtom: replica.documentAtom,
137
+ backendAtom: replica.backendAtom,
138
+ });
139
+ const lifetime = new AbortController();
140
+ documentModel.start({ signal: lifetime.signal });
141
+ followDocument({
142
+ store,
143
+ model: documentModel,
144
+ reconcile: actions.reconcileDocument,
145
+ signal: lifetime.signal,
146
+ });
147
+
127
148
  // Track first user interaction before initialization completes
128
149
  document.addEventListener(
129
150
  MarimoValueInputEvent.TYPE,
@@ -196,7 +217,9 @@ function handleNotification(
196
217
  case "kernel-ready":
197
218
  handleKernelReady(msg, {
198
219
  autoInstantiate: true,
199
- setCells: actions.setCells,
220
+ cells: notebookCells(store.get(notebookAtom)),
221
+ setLastRun: actions.setLastRun,
222
+ resetCellRuntimes: actions.resetCellRuntimes,
200
223
  setLayoutData: Functions.NOOP,
201
224
  setAppConfig: Functions.NOOP,
202
225
  setCapabilities: Functions.NOOP,
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
3
 
4
+ import type { NotebookTransport } from "@marimo-team/client";
4
5
  import { getWorkerRPC } from "@/core/wasm/rpc";
5
6
  import { Deferred } from "@/utils/Deferred";
6
7
  import { throwNotImplemented } from "@/utils/functions";
@@ -8,6 +9,7 @@ import { Logger } from "@/utils/Logger";
8
9
  import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
9
10
  import { generateUUID } from "@/utils/uuid";
10
11
  import { initialNotebookState, notebookAtom } from "../cells/cells";
12
+ import type { NotebookBackend } from "../notebook/replica";
11
13
  import type { ClientCommand, NotificationMessage } from "../kernel/messages";
12
14
  import type { EditRequests, RunRequests } from "../network/types";
13
15
  import { store as defaultStore } from "../state/jotai";
@@ -70,9 +72,37 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
70
72
  private readonly activeSessionGenerations = new Set<number>();
71
73
  private appTransition = Promise.resolve();
72
74
  private workerReady = new Deferred<void>();
75
+ /** Where the managed session's host events go while a replica follows them. */
76
+ private stream: Parameters<NotebookTransport>[1] | undefined;
73
77
 
74
78
  public initialized = new Deferred<void>();
75
79
 
80
+ /**
81
+ * The managed session's host stream, for a replica to follow. Replacing
82
+ * or stopping the session resets the stream, and the next session's
83
+ * snapshot follows.
84
+ */
85
+ public readonly notebookBackend: NotebookBackend = {
86
+ transport: ({ signal }, handlers) =>
87
+ new Promise((resolve) => {
88
+ this.stream = handlers;
89
+ handlers.open();
90
+ signal.addEventListener(
91
+ "abort",
92
+ () => {
93
+ if (this.stream === handlers) {
94
+ this.stream = undefined;
95
+ }
96
+ resolve();
97
+ },
98
+ { once: true },
99
+ );
100
+ }),
101
+ notebook: () => {
102
+ throw new Error("Islands do not edit notebooks");
103
+ },
104
+ };
105
+
76
106
  public constructor(config: IslandsBridgeConfig = {}) {
77
107
  this.store = config.store || defaultStore;
78
108
  this.root = config.root || document;
@@ -119,18 +149,29 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
119
149
  if (!this.activeSessionGenerations.has(sessionGeneration)) {
120
150
  return;
121
151
  }
122
- // Islands follow only the notebook's marimo notifications.
123
152
  const frame = jsonParseWithSpecialChar(event) as {
124
153
  event: string;
125
154
  data: { marimo?: NotificationMessage };
126
155
  };
127
- if (frame.event === "marimo" && frame.data.marimo) {
128
- this.notificationConsumer?.(frame.data.marimo, sessionGeneration);
156
+ if (frame.event === "marimo") {
157
+ if (frame.data.marimo) {
158
+ this.notificationConsumer?.(frame.data.marimo, sessionGeneration);
159
+ }
160
+ return;
161
+ }
162
+ // The replica follows the managed session's host document.
163
+ if (sessionGeneration === this.session?.sessionGeneration) {
164
+ this.stream?.message(frame);
129
165
  }
130
166
  },
131
167
  );
132
168
  }
133
169
 
170
+ /** The managed session is gone, so its stream starts over. */
171
+ private resetNotebookStream(): void {
172
+ this.stream?.message({ event: "reset", data: {} });
173
+ }
174
+
134
175
  public async initializeApps(): Promise<void> {
135
176
  await this.enqueueAppTransition(async () => {
136
177
  await this.workerReady.promise;
@@ -185,6 +226,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
185
226
  const replacesSession = managesSingleApp && previousSession !== undefined;
186
227
  this.activeSessionGenerations.add(request.sessionGeneration);
187
228
  if (replacesSession) {
229
+ this.resetNotebookStream();
188
230
  this.store.set(notebookAtom, initialNotebookState());
189
231
  }
190
232
  if (managesSingleApp || !previousSession) {
@@ -236,6 +278,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
236
278
  this.store.set(islandsPendingInitialRunsAtom, null);
237
279
  this.session = undefined;
238
280
  this.sessionReady = new Deferred<AppSession>();
281
+ this.resetNotebookStream();
239
282
  this.store.set(notebookAtom, initialNotebookState());
240
283
  });
241
284
  }
@@ -340,12 +383,10 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
340
383
  public sendInterrupt = throwNotImplemented;
341
384
  public sendShutdown = throwNotImplemented;
342
385
  public sendFormat = throwNotImplemented;
343
- public sendDeleteCell = throwNotImplemented;
344
386
  public sendInstallMissingPackages = throwNotImplemented;
345
387
  public sendCodeCompletionRequest = throwNotImplemented;
346
388
  public saveUserConfig = throwNotImplemented;
347
389
  public saveAppConfig = throwNotImplemented;
348
- public saveCellConfig = throwNotImplemented;
349
390
  public sendRestart = throwNotImplemented;
350
391
  public readCode = throwNotImplemented;
351
392
  public readSnippets = throwNotImplemented;