@marimo-team/islands 0.25.3-dev27 → 0.25.3-dev270

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 (847) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DfG3PRaJ.js → ConnectedDataExplorerComponent-eLHPh6uu.js} +120 -116
  2. package/dist/{ErrorBoundary-DKAio_E4.js → ErrorBoundary-BDom11jj.js} +66 -71
  3. package/dist/_baseSet-BfI5FjBM.js +328 -0
  4. package/dist/{abnfDiagram-VCTEODGH-B3w-XWX8.js → abnfDiagram-VCTEODGH-hqd0IBtX.js} +2 -2
  5. package/dist/any-language-editor-DnG26QXh.js +75 -0
  6. package/dist/{architectureDiagram-5GKGNRK7-C18U9A6k.js → architectureDiagram-5GKGNRK7-BzLAFcc0.js} +1 -1
  7. package/dist/{arrays-CN1djC4k.js → arrays-DoQ-AILx.js} +15 -27
  8. package/dist/assertNever-DIU3JJbt.js +507 -0
  9. package/dist/assets/__vite-browser-external-BKBpimif.js +1 -0
  10. package/dist/assets/worker-CgoMtD-_.js +113 -0
  11. package/dist/{blockDiagram-I7D4REHJ-Cg25UrxT.js → blockDiagram-I7D4REHJ-Dkhe6_PP.js} +1 -1
  12. package/dist/{button-CvjkwdSk.js → button-0-RqY1DW.js} +229 -218
  13. package/dist/{capabilities-CQ5IjhzL.js → capabilities-B6yy_CBN.js} +2 -2
  14. package/dist/{chat-ui-DDJuqS87.js → chat-ui-BWIkve2R.js} +2071 -1780
  15. package/dist/{check-BMKBBrdc.js → check-gUSZbRsl.js} +1 -1
  16. package/dist/{chunk-IMKFNOWR-CrS5bzqW.js → chunk-IMKFNOWR-DOeVvuyE.js} +1 -1
  17. package/dist/{chunk-SHT3W25Y-StpvPvUG.js → chunk-SHT3W25Y-DZoFI8wR.js} +1 -1
  18. package/dist/{chunk-SVP7TREG-CBXjbYZC.js → chunk-SVP7TREG-CWu86PSW.js} +1 -1
  19. package/dist/{chunk-TICWLB2K-Dp6bIb-q.js → chunk-TICWLB2K-BrM0GoP3.js} +1 -1
  20. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  21. package/dist/{classDiagram-ZZMXUADV-BFYuxV5g.js → classDiagram-ZZMXUADV-DYi4V2bB.js} +2 -2
  22. package/dist/{classDiagram-v2-VYDZK3BY-D_tjXVMl.js → classDiagram-v2-VYDZK3BY-tN7FIk-m.js} +2 -2
  23. package/dist/{code-visibility-CFvLbCZJ.js → code-visibility-BWNUti7D.js} +43 -50
  24. package/dist/{common-1HUDUr7W.js → common-bFQY94fl.js} +10277 -9719
  25. package/dist/config-BsPruWMR.js +636 -0
  26. package/dist/{copy-icon-BgUO8jsi.js → copy-icon-C2I8sw4h.js} +3 -3
  27. package/dist/createReducer-DdtT_7ji.js +67 -0
  28. package/dist/{cynefinDiagram-5FMLGOSQ-DSgYrHB2.js → cynefinDiagram-5FMLGOSQ-D5nCnDc-.js} +1 -1
  29. package/dist/{dagre-GXQ25YYZ-GYlruCBf.js → dagre-GXQ25YYZ-BHkd7f_i.js} +3 -3
  30. package/dist/{dagre-BGSfXNEb.js → dagre-mqiMAsFY.js} +531 -680
  31. package/dist/{data-grid-overlay-editor-ZxoN8gVB.js → data-grid-overlay-editor-CVQ68_by.js} +5 -5
  32. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  33. package/dist/{debounce-C3jyATpc.js → debounce-DrhhziJv.js} +2 -2
  34. package/dist/{diagram-S7CK7UJ4-DRu5LX5_.js → diagram-S7CK7UJ4-D1ALppT2.js} +1 -1
  35. package/dist/{diagram-UQ7AKVKN-DD7uiejP.js → diagram-UQ7AKVKN-DLv0b83L.js} +1 -1
  36. package/dist/{diagram-VX7I27RA-ByH9KAJJ.js → diagram-VX7I27RA-7XJ7J6qg.js} +1 -1
  37. package/dist/{diagram-Z3DM3KII-BsQHPRke.js → diagram-Z3DM3KII-7M7zbHp_.js} +1 -1
  38. package/dist/{dialog-Cc479ZEH.js → dialog-DzCj1qQv.js} +26 -26
  39. package/dist/{dist-Bvs24R_E.js → dist-11T5wq3_.js} +31 -30
  40. package/dist/{dist-F8Ep36rk.js → dist-17ugwGZc.js} +59 -58
  41. package/dist/{dist-BMQf5zIS.js → dist-7cRjf--y.js} +123 -122
  42. package/dist/{dist-diIx5qJW.js → dist-B8pkfkzc.js} +31 -30
  43. package/dist/{dist-CN_BeG2I.js → dist-B9UoM_wq.js} +58 -57
  44. package/dist/{dist-CLdpGQVr.js → dist-BGuMTs1i.js} +118 -117
  45. package/dist/{dist-BK4Oj99o.js → dist-BHW11fni.js} +18 -17
  46. package/dist/dist-BLY4m1Wi.js +850 -0
  47. package/dist/{dist-PXYI_yuP.js → dist-BNlcVoxs.js} +24 -23
  48. package/dist/{dist-D6d86eNJ.js → dist-BQYe5Bju.js} +47 -46
  49. package/dist/{dist-CkTTIdBe.js → dist-BX1mn8na.js} +337 -337
  50. package/dist/{dist-DiDibOYr.js → dist-Bh0PTKUW.js} +151 -150
  51. package/dist/{dist-CSJ0MDRw.js → dist-CKaXK92p.js} +56 -55
  52. package/dist/{dist-C8fAKSH1.js → dist-CLhwfhpJ.js} +53 -52
  53. package/dist/{dist-DoE4XJBC.js → dist-CYeqK4Xw.js} +9801 -10649
  54. package/dist/{dist-BLfKmR-l.js → dist-Cxlp3WH6.js} +16 -15
  55. package/dist/{dist-D-wwqlzJ.js → dist-DSJPGj42.js} +158 -157
  56. package/dist/{dist-BorMD-uX.js → dist-DU9ICsWZ.js} +75 -74
  57. package/dist/{dist-DBtSl3sf.js → dist-DWv0ALBi.js} +47 -46
  58. package/dist/{dist-OR1OnN-y.js → dist-DiBpWEsB.js} +31 -30
  59. package/dist/{dist-CrSP_H1M.js → dist-DiLFTlnl.js} +242 -242
  60. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  61. package/dist/{dist-CN8zI-XC.js → dist-X9CsotJl.js} +239 -239
  62. package/dist/{dist-CSbh1PNP.js → dist-cVDV5IuI.js} +23 -22
  63. package/dist/{dist-BuaRHxOc.js → dist-fRfaOoq8.js} +44 -43
  64. package/dist/{dist-Cg7gzw-a.js → dist-gTTI-9Px.js} +78 -77
  65. package/dist/dropdown-menu-CdXE15an.js +6089 -0
  66. package/dist/{ebnfDiagram-PWID7BFC-CQRK5_eT.js → ebnfDiagram-PWID7BFC-DHH1qlmV.js} +2 -2
  67. package/dist/{erDiagram-RLTQ6QDP-BKtWrDVh.js → erDiagram-RLTQ6QDP-fI_0dGef.js} +1 -1
  68. package/dist/{error-banner-CFPHFw4h.js → error-banner-DPc9eIw5.js} +87 -86
  69. package/dist/error-entries-CxN-Nq3V.js +2573 -0
  70. package/dist/{esm-AMFssoNp.js → esm-BDqqT86j.js} +329 -329
  71. package/dist/{esm-D2gTsXEV.js → esm-BgKjMvWw.js} +530 -529
  72. package/dist/feedback-button-D9dvgMiT.js +374 -0
  73. package/dist/{flowDiagram-HODETNUW-1IN-ih2E.js → flowDiagram-HODETNUW-Brg9MA6i.js} +2 -2
  74. package/dist/{formats-Buu00Z2q.js → formats-DbSvAPY-.js} +6 -6
  75. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  76. package/dist/{glide-data-editor-BBgCAPbk.js → glide-data-editor-DRBPJNtK.js} +6676 -2890
  77. package/dist/{glide-utils-DM9zBO4W.js → glide-utils-BSw3SSHF.js} +84 -45
  78. package/dist/{globals-DhH_o2tF.js → globals-CDnhWJOj.js} +3 -3
  79. package/dist/{graphlib-BmY_CCV9.js → graphlib-nlwT3Wvy.js} +177 -220
  80. package/dist/hasIn-C20JsQ2m.js +28 -0
  81. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  82. package/dist/{error-entries-Cz3yNs7w.js → html-to-image-Bc2AvXHG.js} +26695 -26850
  83. package/dist/{infoDiagram-27XIBGKW-CkB-zr4p.js → infoDiagram-27XIBGKW-E9e403YT.js} +1 -1
  84. package/dist/isArrayLikeObject-B-HXCsex.js +626 -0
  85. package/dist/isEmpty-CdX7MTE1.js +14 -0
  86. package/dist/{ishikawaDiagram-5VMMS53U-wlyLjBmM.js → ishikawaDiagram-5VMMS53U-C-RlYDRo.js} +1 -1
  87. package/dist/{kanban-definition-UXKFOSKX-C2usY0i-.js → kanban-definition-UXKFOSKX-CLFl_uqK.js} +1 -1
  88. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  89. package/dist/{loader-BQOfjTIJ.js → loader-BVEtQ4F3.js} +12 -12
  90. package/dist/main.js +10420 -8311
  91. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  92. package/dist/{mermaid-BA-Z6wGL.js → mermaid-rd09e1JL.js} +54 -54
  93. package/dist/{mindmap-definition-YA3MSWOX-nKOWS8RR.js → mindmap-definition-YA3MSWOX-BQ8TWtyB.js} +1 -1
  94. package/dist/{now-jM8-M7IR.js → now-BKZCLE5P.js} +12 -13
  95. package/dist/{number-overlay-editor-CvLhxHLb.js → number-overlay-editor-CkcPsvT7.js} +3 -3
  96. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  97. package/dist/{once-BtE-wKRL.js → once-NnpX_HnX.js} +1 -1
  98. package/dist/{pegDiagram-XKGWAZYB-Dac9EIKD.js → pegDiagram-XKGWAZYB-CN8UHFPi.js} +2 -2
  99. package/dist/pencil-DcT8AE0h.js +10 -0
  100. package/dist/pick-tCszMUbm.js +35 -0
  101. package/dist/{pieDiagram-E7YTZNPT-B7S-DsW_.js → pieDiagram-E7YTZNPT-DGuJ0EBd.js} +1 -1
  102. package/dist/precisionRound-DhAzeRcm.js +49 -0
  103. package/dist/{process-output-B-KBO95p.js → process-output-qecD7Dk8.js} +4 -4
  104. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  105. package/dist/{railroadDiagram-O6MQD6OU-Cfa5Q-hr.js → railroadDiagram-O6MQD6OU-BPDA8wy8.js} +2 -2
  106. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  107. package/dist/{requirementDiagram-BXWQKSXE-d8zXCJIk.js → requirementDiagram-BXWQKSXE-ChzLGbGW.js} +1 -1
  108. package/dist/{reveal-component-C9BavSsS.js → reveal-component-DMHnoLjA.js} +1131 -1108
  109. package/dist/schemas-CdscWNy_.js +4747 -0
  110. package/dist/select-OxgozOC8.js +1200 -0
  111. package/dist/{sequenceDiagram-WJ2MYXX4-b6pei3lJ.js → sequenceDiagram-WJ2MYXX4-DFX-bE6O.js} +1 -1
  112. package/dist/{spec-BJzgmvVa.js → spec-oJn0RhpQ.js} +41 -11
  113. package/dist/{stateDiagram-D77RDMKH-Be-xSpaZ.js → stateDiagram-D77RDMKH-BR7cI3wK.js} +4 -4
  114. package/dist/{stateDiagram-v2-MP3YSRHH-5Y-FxesJ.js → stateDiagram-v2-MP3YSRHH-oGqB5UDI.js} +2 -2
  115. package/dist/static-state-CuV6Pc3a.js +72 -0
  116. package/dist/style.css +1 -1
  117. package/dist/{swimlanes-42K2YHIH-nRRl4NU0.js → swimlanes-42K2YHIH-DwSZl1yC.js} +1 -1
  118. package/dist/{swimlanesDiagram-VR7AAH4N-BS79510V.js → swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js} +2 -2
  119. package/dist/{swiper-component-DKntyn8p.js → swiper-component-Dvj-goaD.js} +2 -2
  120. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  121. package/dist/{timeline-definition-24CTP7MA-Cfzf8WPf.js → timeline-definition-24CTP7MA-DMmP-G2O.js} +1 -1
  122. package/dist/toast-DbBHmxmB.js +491 -0
  123. package/dist/{tooltip-CtBZ5q8p.js → tooltip-BfcDQd2j.js} +329 -305
  124. package/dist/use-restore-focus-CmflVLWJ.js +1672 -0
  125. package/dist/{useAsyncData-BD534XmW.js → useAsyncData-BYIAoMqn.js} +15 -14
  126. package/dist/{useDateFormatter-DkajGwwd.js → useDateFormatter-ButWkdcK.js} +2 -2
  127. package/dist/{useDeepCompareMemoize-C72_RNsu.js → useDeepCompareMemoize-Bz0c5GN6.js} +1 -1
  128. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BIolGsF1.js} +1 -1
  129. package/dist/useLifecycle-JjxCC7Vu.js +112 -0
  130. package/dist/useTheme-DPLobSFs.js +339 -0
  131. package/dist/utils-EBklsC5R.js +813 -0
  132. package/dist/{vega-component-nB1kV6sb.js → vega-component-rxIxAAh9.js} +171 -170
  133. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  134. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  135. package/dist/{vennDiagram-4TSXK5OY-CeY6Z1xZ.js → vennDiagram-4TSXK5OY-Db8GtH1G.js} +1 -1
  136. package/dist/{wardleyDiagram-VM6X3IG4-Cxs9NtVi.js → wardleyDiagram-VM6X3IG4-DmIiC3FU.js} +1 -1
  137. package/dist/{xychartDiagram-S5SC5T6Z-DNWbvPK7.js → xychartDiagram-S5SC5T6Z-DS9OPrNq.js} +2 -2
  138. package/package.json +17 -20
  139. package/src/__mocks__/document.tsx +63 -0
  140. package/src/__mocks__/executions.ts +195 -0
  141. package/src/__mocks__/notebook.ts +3 -5
  142. package/src/__mocks__/partial.ts +26 -0
  143. package/src/__mocks__/requests.ts +1 -4
  144. package/src/__tests__/CellStatus.test.tsx +19 -20
  145. package/src/__tests__/fixtures/pair-config.ts +1 -1
  146. package/src/__tests__/main.test.tsx +24 -1
  147. package/src/__tests__/module-entry.test.ts +44 -0
  148. package/src/__tests__/mount.test.ts +1 -21
  149. package/src/__tests__/{host-imports.test.ts → replica-imports.test.ts} +12 -11
  150. package/src/components/ai/__tests__/ai-model-dropdown.test.tsx +63 -0
  151. package/src/components/ai/ai-model-dropdown.tsx +11 -6
  152. package/src/components/app-config/__tests__/state.test.tsx +50 -0
  153. package/src/components/app-config/ai-config.tsx +156 -150
  154. package/src/components/app-config/app-config-button.tsx +6 -5
  155. package/src/components/app-config/app-config-form.tsx +5 -9
  156. package/src/components/app-config/common.tsx +7 -9
  157. package/src/components/app-config/data-form.tsx +112 -113
  158. package/src/components/app-config/mcp-config.tsx +30 -53
  159. package/src/components/app-config/user-config-form.css +91 -0
  160. package/src/components/app-config/user-config-form.tsx +266 -349
  161. package/src/components/chat/__tests__/capabilities-popover.test.tsx +0 -1
  162. package/src/components/chat/__tests__/chat-display.test.tsx +196 -0
  163. package/src/components/chat/__tests__/chat-markdown.test.tsx +43 -0
  164. package/src/components/chat/__tests__/chat-utils.test.ts +93 -1
  165. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  166. package/src/components/chat/__tests__/notebook-access-selector.test.tsx +64 -0
  167. package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
  168. package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
  169. package/src/components/chat/__tests__/user-message.test.tsx +27 -0
  170. package/src/components/chat/acp/__tests__/agent-panel-initialization.test.tsx +1 -1
  171. package/src/components/chat/acp/__tests__/agent-panel.test.tsx +22 -17
  172. package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
  173. package/src/components/chat/acp/__tests__/state.test.ts +2 -2
  174. package/src/components/chat/acp/agent-panel.tsx +6 -3
  175. package/src/components/chat/acp/blocks.tsx +13 -12
  176. package/src/components/chat/acp/common.tsx +4 -5
  177. package/src/components/chat/acp/model-selector.tsx +1 -1
  178. package/src/components/chat/acp/session-tabs.tsx +8 -3
  179. package/src/components/chat/agent-activity.tsx +132 -0
  180. package/src/components/chat/capabilities-popover.tsx +2 -11
  181. package/src/components/chat/chat-components.tsx +9 -4
  182. package/src/components/chat/chat-display.tsx +131 -57
  183. package/src/components/chat/chat-history-popover.tsx +6 -1
  184. package/src/components/chat/chat-markdown.tsx +15 -0
  185. package/src/components/chat/chat-panel.tsx +216 -294
  186. package/src/components/chat/chat-typography.css +108 -0
  187. package/src/components/chat/chat-utils.ts +24 -35
  188. package/src/components/chat/notebook-access-selector.stories.tsx +22 -0
  189. package/src/components/chat/notebook-access-selector.tsx +100 -0
  190. package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
  191. package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
  192. package/src/components/chat/tool-call/shared.ts +6 -0
  193. package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
  194. package/src/components/chat/tool-call/tool-labels.ts +172 -0
  195. package/src/components/chat/tool-call/tool-result.tsx +1 -1
  196. package/src/components/chat/tool-call/tool-step.tsx +145 -0
  197. package/src/components/chat/turn-timing.ts +73 -0
  198. package/src/components/chat/use-assistant-turn.ts +65 -0
  199. package/src/components/chat/use-chat-generation.ts +20 -0
  200. package/src/components/chat/user-message.tsx +145 -0
  201. package/src/components/data-table/TableBottomBar.tsx +4 -1
  202. package/src/components/data-table/TableTopBar.tsx +2 -0
  203. package/src/components/data-table/__tests__/chart-spec-model.test.ts +195 -195
  204. package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
  205. package/src/components/data-table/__tests__/columns.test.tsx +105 -0
  206. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  207. package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
  208. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  209. package/src/components/data-table/__tests__/export-actions.test.tsx +105 -0
  210. package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
  211. package/src/components/data-table/__tests__/pagination.test.tsx +28 -1
  212. package/src/components/data-table/__tests__/search-highlight.test.tsx +109 -0
  213. package/src/components/data-table/__tests__/url-detector.test.tsx +74 -0
  214. package/src/components/data-table/cell-selection/feature.ts +2 -2
  215. package/src/components/data-table/cell-selection/types.ts +2 -2
  216. package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
  217. package/src/components/data-table/charts/__tests__/spec-snapshot.test.ts +20 -20
  218. package/src/components/data-table/charts/__tests__/spec.test.ts +78 -52
  219. package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
  220. package/src/components/data-table/charts/chart-spec/spec.ts +68 -43
  221. package/src/components/data-table/charts/charts.tsx +40 -30
  222. package/src/components/data-table/charts/storage.ts +16 -5
  223. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
  224. package/src/components/data-table/column-formatting/types.ts +1 -2
  225. package/src/components/data-table/column-summary/chart-spec-model.tsx +23 -16
  226. package/src/components/data-table/column-summary/column-summary.tsx +2 -4
  227. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  228. package/src/components/data-table/columns.tsx +45 -46
  229. package/src/components/data-table/context-menu.tsx +36 -20
  230. package/src/components/data-table/data-table.tsx +17 -1
  231. package/src/components/data-table/date-popover.tsx +6 -4
  232. package/src/components/data-table/export-actions.tsx +76 -48
  233. package/src/components/data-table/pagination.tsx +44 -33
  234. package/src/components/data-table/range-focus/atoms.ts +4 -6
  235. package/src/components/data-table/range-focus/use-cell-range-selection.ts +65 -38
  236. package/src/components/data-table/range-focus/use-scroll-into-view.ts +21 -2
  237. package/src/components/data-table/renderers.tsx +16 -7
  238. package/src/components/data-table/row-viewer-panel/__tests__/row-viewer.test.tsx +24 -1
  239. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +4 -4
  240. package/src/components/data-table/search-highlight.tsx +79 -0
  241. package/src/components/data-table/types.ts +1 -0
  242. package/src/components/data-table/url-detector.tsx +49 -9
  243. package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
  244. package/src/components/datasources/__tests__/column-preview.test.tsx +96 -5
  245. package/src/components/datasources/__tests__/datasources-interactions.test.tsx +181 -0
  246. package/src/components/datasources/__tests__/table-list.test.tsx +118 -0
  247. package/src/components/datasources/__tests__/utils.test.ts +57 -6
  248. package/src/components/datasources/column-preview.tsx +17 -12
  249. package/src/components/datasources/datasources.tsx +56 -43
  250. package/src/components/datasources/utils.ts +32 -23
  251. package/src/components/debugger/debugger-code.tsx +5 -0
  252. package/src/components/dependency-graph/custom-node.tsx +1 -1
  253. package/src/components/dependency-graph/dependency-graph-tree.tsx +8 -8
  254. package/src/components/dependency-graph/elements.ts +24 -21
  255. package/src/components/dependency-graph/minimap-content.tsx +15 -20
  256. package/src/components/dependency-graph/panels.tsx +4 -2
  257. package/src/components/dependency-graph/utils/__tests__/layout.test.ts +18 -11
  258. package/src/components/editor/SortableCell.tsx +1 -3
  259. package/src/components/editor/__tests__/Output.test.tsx +50 -32
  260. package/src/{core/attachments/__tests__/toast.test.tsx → components/editor/__tests__/agent-toast.test.tsx} +1 -1
  261. package/src/components/editor/__tests__/data-attributes.test.tsx +28 -21
  262. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  263. package/src/components/editor/__tests__/output-persistence.test.tsx +8 -4
  264. package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
  265. package/src/components/editor/actions/__tests__/name-cell-input.test.tsx +69 -0
  266. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +21 -40
  267. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +41 -14
  268. package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +6 -24
  269. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +131 -0
  270. package/src/components/editor/actions/name-cell-input.tsx +4 -3
  271. package/src/components/editor/actions/pair-with-agent-commands.ts +10 -7
  272. package/src/components/editor/actions/pair-with-agent-modal.tsx +29 -30
  273. package/src/components/editor/actions/pair-with-agent.tsx +16 -39
  274. package/src/components/editor/actions/useCellActionButton.tsx +47 -35
  275. package/src/components/editor/actions/useConfigActions.tsx +3 -4
  276. package/src/components/editor/actions/useNotebookActions.tsx +25 -29
  277. package/src/components/editor/actions/useRestartKernel.tsx +1 -1
  278. package/src/components/editor/actions/useSetCodeVisibility.ts +11 -12
  279. package/src/components/editor/actions/useSetOutputsExpanded.ts +14 -19
  280. package/src/components/editor/ai/ai-completion-editor.tsx +2 -1
  281. package/src/components/editor/ai/completion-handlers.tsx +0 -37
  282. package/src/components/editor/ai/prompt-input.tsx +189 -0
  283. package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +1 -1
  284. package/src/components/editor/alerts/connection-notice.tsx +1 -1
  285. package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
  286. package/src/components/editor/alerts/stdin-blocking-alert.tsx +4 -2
  287. package/src/components/editor/app-container.tsx +1 -1
  288. package/src/components/editor/boundary/__tests__/ErrorBoundary.test.tsx +1 -1
  289. package/src/components/editor/cell/CellStatus.tsx +23 -19
  290. package/src/components/editor/cell/CreateCellButton.tsx +15 -21
  291. package/src/components/editor/cell/DeleteButton.tsx +3 -2
  292. package/src/components/editor/cell/RunButton.tsx +18 -8
  293. package/src/components/editor/cell/StagedAICell.tsx +52 -137
  294. package/src/components/editor/cell/StopButton.tsx +2 -2
  295. package/src/components/editor/cell/__tests__/useRunCells.test.ts +45 -0
  296. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  297. package/src/components/editor/cell/cell-actions.tsx +12 -1
  298. package/src/components/editor/cell/cell-context-menu.tsx +4 -1
  299. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  300. package/src/components/editor/cell/code/cell-editor.tsx +65 -169
  301. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  302. package/src/components/editor/cell/collapse.tsx +15 -4
  303. package/src/components/editor/cell/useAddCell.ts +4 -8
  304. package/src/components/editor/cell/useDeleteCell.tsx +10 -36
  305. package/src/components/editor/cell/useRunCells.ts +20 -8
  306. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  307. package/src/components/editor/cell/useSplitCell.tsx +10 -6
  308. package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +1 -1
  309. package/src/components/editor/chrome/components/feedback-button.tsx +2 -8
  310. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +31 -2
  311. package/src/components/editor/chrome/panels/__tests__/runtime-details.test.tsx +1 -1
  312. package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +1 -1
  313. package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
  314. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +1 -1
  315. package/src/components/editor/chrome/panels/logs-panel.tsx +2 -1
  316. package/src/components/editor/chrome/panels/outline/__tests__/outline-keyboard.test.tsx +84 -0
  317. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +39 -14
  318. package/src/components/editor/chrome/panels/outline/useActiveOutline.tsx +3 -2
  319. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  320. package/src/components/editor/chrome/panels/panel-header.tsx +13 -0
  321. package/src/components/editor/chrome/panels/secrets-panel.tsx +1 -0
  322. package/src/components/editor/chrome/panels/session-panel.tsx +1 -1
  323. package/src/components/editor/chrome/panels/snippets-panel.tsx +10 -12
  324. package/src/components/editor/chrome/panels/variable-panel.tsx +1 -5
  325. package/src/components/editor/chrome/types.ts +1 -1
  326. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +7 -9
  327. package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
  328. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +265 -37
  329. package/src/components/editor/chrome/wrapper/__tests__/useAskAiAboutCell.test.tsx +58 -0
  330. package/src/components/editor/chrome/wrapper/app-chrome.tsx +91 -74
  331. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +4 -4
  332. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
  333. package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
  334. package/src/components/editor/chrome/wrapper/footer-items/lsp-status.tsx +1 -1
  335. package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +1 -1
  336. package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +1 -7
  337. package/src/components/editor/chrome/wrapper/footer.tsx +2 -2
  338. package/src/components/editor/chrome/wrapper/minimap-state.ts +1 -1
  339. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +143 -72
  340. package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
  341. package/src/components/editor/chrome/wrapper/sidebar.tsx +8 -4
  342. package/src/components/editor/chrome/wrapper/storage.ts +1 -1
  343. package/src/components/editor/chrome/wrapper/useAskAiAboutCell.ts +36 -0
  344. package/src/components/editor/columns/__tests__/cell-column.test.tsx +1 -1
  345. package/src/components/editor/columns/cell-column.tsx +1 -1
  346. package/src/components/editor/columns/sortable-column.tsx +2 -2
  347. package/src/components/editor/connections/components.tsx +5 -10
  348. package/src/components/editor/controls/Controls.tsx +4 -4
  349. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  350. package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
  351. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +10 -4
  352. package/src/components/editor/controls/command-palette.tsx +22 -10
  353. package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
  354. package/src/components/editor/documentation.css +34 -11
  355. package/src/components/editor/dynamic-favicon.tsx +21 -40
  356. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +25 -20
  357. package/src/components/editor/errors/auto-fix.tsx +4 -8
  358. package/src/components/editor/errors/send-error-report-button.tsx +3 -6
  359. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  360. package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
  361. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  362. package/src/components/editor/file-tree/file-explorer.tsx +48 -15
  363. package/src/components/editor/file-tree/file-name-input.tsx +1 -0
  364. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  365. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  366. package/src/components/editor/header/__tests__/filename-form.test.tsx +1 -4
  367. package/src/components/editor/header/__tests__/status.test.tsx +5 -3
  368. package/src/components/editor/header/app-header.tsx +1 -1
  369. package/src/components/editor/header/filename-form.tsx +1 -1
  370. package/src/components/editor/header/status.tsx +44 -22
  371. package/src/components/editor/links/__tests__/cell-link.test.tsx +148 -0
  372. package/src/components/editor/links/cell-link-list.tsx +1 -1
  373. package/src/components/editor/links/cell-link.tsx +21 -11
  374. package/src/components/editor/navigation/__tests__/clipboard.test.ts +60 -38
  375. package/src/components/editor/navigation/__tests__/navigation.test.ts +108 -85
  376. package/src/components/editor/navigation/__tests__/selection.test.ts +43 -21
  377. package/src/components/editor/navigation/clipboard.ts +17 -14
  378. package/src/components/editor/navigation/focus-utils.ts +23 -4
  379. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +17 -33
  380. package/src/components/editor/navigation/navigation.ts +74 -72
  381. package/src/components/editor/navigation/selection.ts +30 -29
  382. package/src/components/editor/notebook-banner.tsx +1 -0
  383. package/src/components/editor/notebook-cell.tsx +114 -126
  384. package/src/components/editor/output/MarimoErrorOutput.tsx +2 -1
  385. package/src/components/editor/output/MarimoTracebackOutput.tsx +19 -17
  386. package/src/components/editor/output/Outputs.css +8 -0
  387. package/src/components/editor/output/VideoOutput.tsx +6 -0
  388. package/src/components/editor/output/__tests__/VideoOutput.test.tsx +131 -0
  389. package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
  390. package/src/components/editor/output/ansi-reduce.ts +33 -20
  391. package/src/components/editor/output/console/ConsoleOutput.tsx +19 -25
  392. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +125 -69
  393. package/src/components/editor/package-alert.tsx +14 -7
  394. package/src/components/editor/renderers/__tests__/add-cell-buttons.test.tsx +18 -0
  395. package/src/components/editor/renderers/cell-array.tsx +32 -100
  396. package/src/components/editor/renderers/grid-layout/__tests__/grid-layout.test.tsx +75 -0
  397. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +34 -22
  398. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -2
  399. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +1 -1
  400. package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +4 -4
  401. package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
  402. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +1 -1
  403. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +13 -34
  404. package/src/components/editor/stdin-blocking-alert.tsx +4 -2
  405. package/src/components/home/components.tsx +17 -31
  406. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  407. package/src/components/icons/large-spinner.tsx +3 -14
  408. package/src/components/icons/spinner.tsx +1 -1
  409. package/src/components/markdown/markdown-renderer.tsx +39 -28
  410. package/src/components/mcp/mcp-status-indicator.tsx +18 -14
  411. package/src/components/pages/__tests__/home-page.test.tsx +116 -0
  412. package/src/components/pages/edit-page.tsx +19 -17
  413. package/src/components/pages/home-page.tsx +164 -91
  414. package/src/components/scratchpad/scratchpad.test.tsx +89 -0
  415. package/src/components/scratchpad/scratchpad.tsx +47 -33
  416. package/src/components/scratchpad/state.ts +7 -0
  417. package/src/components/slides/__tests__/minimap-actions.test.tsx +22 -29
  418. package/src/components/slides/__tests__/minimap.test.ts +38 -19
  419. package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +37 -0
  420. package/src/components/slides/minimap.tsx +21 -14
  421. package/src/components/slides/reveal-component.tsx +72 -36
  422. package/src/components/slides/reveal-slides.css +5 -4
  423. package/src/components/slides/slide-cell-view.tsx +32 -29
  424. package/src/components/slides/slide-form.tsx +1 -0
  425. package/src/components/slides/slide-scroll-hint.tsx +3 -1
  426. package/src/components/slides/slide.tsx +3 -4
  427. package/src/components/sort/SortableCellsProvider.tsx +8 -25
  428. package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
  429. package/src/components/tracing/tracing.tsx +46 -42
  430. package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
  431. package/src/components/ui/__tests__/fullscreen.test.tsx +44 -2
  432. package/src/components/ui/__tests__/range-slider.test.tsx +165 -0
  433. package/src/components/ui/alert-dialog.tsx +1 -1
  434. package/src/components/ui/draggable-popover.tsx +24 -2
  435. package/src/components/ui/fullscreen.tsx +25 -1
  436. package/src/components/ui/range-slider.tsx +22 -9
  437. package/src/components/ui/toast.tsx +1 -1
  438. package/src/core/MarimoApp.tsx +3 -1
  439. package/src/core/ai/__tests__/staged-cells.test.ts +89 -608
  440. package/src/core/ai/context/context.ts +2 -0
  441. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +8 -12
  442. package/src/core/ai/context/providers/__tests__/cell.test.ts +49 -0
  443. package/src/core/ai/context/providers/cell-output.ts +2 -2
  444. package/src/core/ai/context/providers/cell.ts +54 -0
  445. package/src/core/ai/context/providers/datasource.ts +6 -2
  446. package/src/core/ai/staged-cells.ts +56 -323
  447. package/src/core/ai/state.ts +3 -0
  448. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +40 -88
  449. package/src/core/ai/tools/__tests__/registry.test.ts +17 -0
  450. package/src/core/ai/tools/base.ts +18 -15
  451. package/src/core/ai/tools/edit-notebook-tool.ts +86 -80
  452. package/src/core/ai/tools/registry.ts +13 -10
  453. package/src/core/ai/tools/sample-tool.ts +6 -7
  454. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -426
  455. package/src/core/cells/__tests__/actions.test.ts +624 -35
  456. package/src/core/cells/__tests__/add-missing-import.test.ts +7 -8
  457. package/src/core/cells/__tests__/arrange.test.ts +432 -0
  458. package/src/core/cells/__tests__/cell.test.ts +85 -151
  459. package/src/core/cells/__tests__/cells.test.ts +269 -2667
  460. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
  461. package/src/core/cells/__tests__/document.test.ts +164 -0
  462. package/src/core/cells/__tests__/editor.test.ts +57 -0
  463. package/src/core/cells/__tests__/flush-document.test.ts +40 -0
  464. package/src/core/cells/__tests__/harness.ts +106 -0
  465. package/src/core/cells/__tests__/layout.test.ts +177 -0
  466. package/src/core/cells/__tests__/notebook-outline.test.ts +122 -0
  467. package/src/core/cells/__tests__/outputs.test.tsx +71 -53
  468. package/src/core/cells/__tests__/pending-delete-service.test.tsx +11 -20
  469. package/src/core/cells/__tests__/session.test.ts +445 -211
  470. package/src/core/cells/__tests__/utils.test.ts +76 -347
  471. package/src/core/cells/actions.ts +678 -6
  472. package/src/core/cells/add-missing-import.ts +47 -43
  473. package/src/core/cells/arrange.ts +529 -0
  474. package/src/core/cells/cell.ts +45 -136
  475. package/src/core/cells/cells.ts +133 -1385
  476. package/src/core/cells/columns.ts +38 -0
  477. package/src/core/cells/document.ts +178 -0
  478. package/src/core/cells/edit.ts +30 -0
  479. package/src/core/cells/editor.ts +82 -0
  480. package/src/core/cells/focus.ts +27 -6
  481. package/src/core/cells/layout.ts +221 -0
  482. package/src/core/cells/outputs.ts +12 -28
  483. package/src/core/cells/pending-delete-service.ts +8 -14
  484. package/src/core/cells/scrollCellIntoView.ts +6 -0
  485. package/src/core/cells/session.ts +76 -53
  486. package/src/core/cells/types.ts +34 -41
  487. package/src/core/cells/utils.ts +46 -75
  488. package/src/core/codemirror/__tests__/format.test.ts +0 -34
  489. package/src/core/codemirror/__tests__/setup.test.ts +61 -29
  490. package/src/core/codemirror/ai/__tests__/resources.test.ts +117 -0
  491. package/src/core/codemirror/ai/resources.ts +1 -1
  492. package/src/core/codemirror/cells/__tests__/extensions.test.ts +2 -6
  493. package/src/core/codemirror/cells/extensions.ts +30 -28
  494. package/src/core/codemirror/cells/state.ts +2 -6
  495. package/src/core/codemirror/cm.ts +5 -1
  496. package/src/core/codemirror/config/types.ts +1 -1
  497. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +6 -7
  498. package/src/core/codemirror/copilot/getCodes.ts +2 -2
  499. package/src/core/codemirror/format.ts +6 -23
  500. package/src/core/codemirror/keymaps/vim.ts +4 -8
  501. package/src/core/codemirror/language/__tests__/sql.test.ts +22 -20
  502. package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
  503. package/src/core/codemirror/language/cell-code.ts +114 -0
  504. package/src/core/codemirror/language/extension.ts +52 -35
  505. package/src/core/codemirror/language/languages/markdown.ts +8 -6
  506. package/src/core/codemirror/language/languages/python.ts +19 -13
  507. package/src/core/codemirror/language/languages/sql/sql.ts +16 -24
  508. package/src/core/codemirror/language/panel/panel.tsx +1 -0
  509. package/src/core/codemirror/language/types.ts +7 -7
  510. package/src/core/codemirror/language/utils.ts +9 -39
  511. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +1 -2
  512. package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
  513. package/src/core/codemirror/lsp/utils.ts +1 -1
  514. package/src/core/codemirror/misc/__tests__/paste.test.ts +3 -1
  515. package/src/core/codemirror/misc/paste.ts +1 -1
  516. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +75 -0
  517. package/src/core/codemirror/placeholder/extensions.ts +80 -19
  518. package/src/core/codemirror/readonly/__tests__/extension.test.ts +1 -1
  519. package/src/core/codemirror/readonly/extension.ts +2 -2
  520. package/src/core/codemirror/utils.ts +1 -0
  521. package/src/core/config/__tests__/app-config.test.ts +64 -0
  522. package/src/core/config/__tests__/config-schema.test.ts +56 -6
  523. package/src/core/config/config-schema.ts +17 -4
  524. package/src/core/config/config.ts +65 -20
  525. package/src/core/config/feature-flag.tsx +0 -2
  526. package/src/core/datasets/__tests__/data-source-discovery.test.ts +5 -5
  527. package/src/core/datasets/data-source-discovery.ts +4 -6
  528. package/src/core/documentation/doc-lookup.ts +1 -1
  529. package/src/core/edit-app.tsx +22 -28
  530. package/src/core/errors/error-entries.ts +4 -1
  531. package/src/core/executions/__tests__/atoms.test.ts +99 -0
  532. package/src/core/executions/__tests__/select.test.ts +268 -0
  533. package/src/core/executions/atoms.ts +206 -0
  534. package/src/core/executions/hooks.ts +31 -0
  535. package/src/core/executions/index.ts +31 -0
  536. package/src/core/executions/select.ts +175 -0
  537. package/src/core/export/__tests__/hooks.test.ts +0 -5
  538. package/src/core/export/hooks.ts +1 -1
  539. package/src/core/export/layout.ts +2 -2
  540. package/src/core/hotkeys/actions.ts +2 -4
  541. package/src/core/hotkeys/hotkeys.ts +2 -1
  542. package/src/core/islands/__tests__/bootstrap.test.ts +69 -17
  543. package/src/core/islands/__tests__/bridge.test.ts +85 -27
  544. package/src/core/islands/bootstrap.ts +50 -54
  545. package/src/core/islands/bridge.ts +84 -25
  546. package/src/core/islands/components/__tests__/IslandControls.test.tsx +7 -5
  547. package/src/core/islands/components/output-wrapper.tsx +5 -5
  548. package/src/core/islands/components/web-components.tsx +1 -1
  549. package/src/core/islands/worker/__tests__/controller.test.ts +62 -4
  550. package/src/core/islands/worker/controller.ts +4 -10
  551. package/src/core/islands/worker/worker.tsx +33 -33
  552. package/src/core/kernel/__tests__/RuntimeState.test.ts +1 -1
  553. package/src/core/kernel/__tests__/handlers.test.ts +7 -153
  554. package/src/core/kernel/handlers.ts +17 -79
  555. package/src/core/kernel/state.ts +0 -15
  556. package/src/core/lifecycle/render-policy.ts +12 -26
  557. package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
  558. package/src/core/meta/code-visibility.ts +18 -9
  559. package/src/core/network/__tests__/connection.test.ts +1 -1
  560. package/src/core/network/__tests__/requests-lazy.test.ts +6 -8
  561. package/src/core/network/connection-notice.ts +1 -1
  562. package/src/core/network/connection.ts +1 -1
  563. package/src/core/network/requests-lazy.ts +5 -8
  564. package/src/core/network/requests-network.ts +5 -31
  565. package/src/core/network/requests-static.ts +1 -4
  566. package/src/core/network/requests-toasting.tsx +1 -4
  567. package/src/core/network/types.ts +6 -14
  568. package/src/core/network/useConnectionNotice.ts +8 -2
  569. package/src/core/notebook/__tests__/operations.test.ts +51 -0
  570. package/src/core/notebook/attachments.ts +38 -0
  571. package/src/core/notebook/document/__tests__/codemirror.test.ts +759 -0
  572. package/src/core/notebook/document/__tests__/draft.test.ts +363 -0
  573. package/src/core/notebook/document/__tests__/fixtures.ts +92 -0
  574. package/src/core/notebook/document/__tests__/hooks.test.tsx +391 -0
  575. package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +61 -0
  576. package/src/core/notebook/document/__tests__/model.test.ts +247 -0
  577. package/src/core/notebook/document/__tests__/protocol-fixtures.ts +38 -0
  578. package/src/core/notebook/document/__tests__/recovery-scenarios.test.ts +230 -0
  579. package/src/core/notebook/document/__tests__/scenario-fixture.ts +43 -0
  580. package/src/core/notebook/document/__tests__/scenarios.test.ts +205 -0
  581. package/src/core/notebook/document/__tests__/state.test.ts +459 -0
  582. package/src/core/notebook/document/__tests__/submission.test.ts +203 -0
  583. package/src/core/notebook/document/__tests__/test-host.test.ts +92 -0
  584. package/src/core/notebook/document/__tests__/test-host.ts +247 -0
  585. package/src/core/notebook/document/__tests__/test-notebook.ts +149 -0
  586. package/src/core/notebook/document/__tests__/user.ts +61 -0
  587. package/src/core/notebook/document/codemirror.ts +216 -0
  588. package/src/core/notebook/document/draft.ts +502 -0
  589. package/src/core/notebook/document/hooks.tsx +118 -0
  590. package/src/core/notebook/document/index.ts +24 -0
  591. package/src/core/notebook/document/model.ts +102 -0
  592. package/src/core/notebook/document/select.ts +41 -0
  593. package/src/core/notebook/document/state.ts +378 -0
  594. package/src/core/notebook/document/sync.ts +369 -0
  595. package/src/core/notebook/index.ts +19 -0
  596. package/src/core/notebook/operations.ts +30 -0
  597. package/src/core/notebook/replica/AGENTS.md +11 -0
  598. package/src/core/{host → notebook/replica}/__tests__/attachments.test.ts +9 -10
  599. package/src/core/{host → notebook/replica}/__tests__/document.test.ts +18 -16
  600. package/src/core/notebook/replica/__tests__/executions.test.ts +200 -0
  601. package/src/core/notebook/replica/__tests__/fixtures.ts +79 -0
  602. package/src/core/notebook/replica/__tests__/replica.test.ts +559 -0
  603. package/src/core/notebook/replica/__tests__/results.test.ts +193 -0
  604. package/src/core/notebook/replica/__tests__/runtime.test.ts +78 -0
  605. package/src/core/notebook/replica/backend.ts +19 -0
  606. package/src/core/notebook/replica/connection.ts +42 -0
  607. package/src/core/notebook/replica/index.ts +20 -0
  608. package/src/core/notebook/replica/replica.ts +336 -0
  609. package/src/core/{host → notebook/replica}/resources/attachments.ts +6 -3
  610. package/src/core/{host → notebook/replica}/resources/document.ts +3 -3
  611. package/src/core/notebook/replica/resources/executions.ts +157 -0
  612. package/src/core/notebook/replica/resources/results.ts +48 -0
  613. package/src/core/notebook/replica/resources/runtime.ts +14 -0
  614. package/src/core/packages/__tests__/useSandboxController.test.tsx +1 -1
  615. package/src/core/packages/useSandboxController.ts +1 -1
  616. package/src/core/run-app.tsx +49 -51
  617. package/src/core/runtime/__tests__/adapter.test.ts +4 -4
  618. package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
  619. package/src/core/runtime/__tests__/runtime.test.ts +32 -74
  620. package/src/core/runtime/adapter.ts +5 -5
  621. package/src/core/runtime/config.ts +2 -2
  622. package/src/core/runtime/runtime.ts +33 -46
  623. package/src/core/runtime/types.ts +0 -2
  624. package/src/core/saving/__tests__/document.test.tsx +123 -0
  625. package/src/core/saving/__tests__/state.test.ts +111 -0
  626. package/src/core/saving/__tests__/useAutoSave.test.tsx +67 -0
  627. package/src/core/saving/document.ts +33 -0
  628. package/src/core/saving/filename.ts +1 -1
  629. package/src/core/saving/save-component.tsx +39 -60
  630. package/src/core/saving/state.ts +38 -39
  631. package/src/core/saving/useAutoSave.ts +17 -53
  632. package/src/core/session/__tests__/backend.test.ts +236 -0
  633. package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +186 -0
  634. package/src/core/{websocket → session}/__tests__/useNotebookSession.hook.test.tsx +247 -262
  635. package/src/core/session/active-notebook.ts +28 -0
  636. package/src/core/session/backend.ts +123 -0
  637. package/src/core/session/embedded.ts +7 -0
  638. package/src/core/{websocket → session}/types.ts +2 -2
  639. package/src/core/session/useNotebookReplica.ts +74 -0
  640. package/src/core/{websocket → session}/useNotebookSession.tsx +85 -67
  641. package/src/core/state/__tests__/jotai.test.tsx +108 -0
  642. package/src/core/state/jotai.ts +1 -1
  643. package/src/core/static/__tests__/files.test.ts +38 -10
  644. package/src/core/static/files.ts +2 -0
  645. package/src/core/{websocket → transports}/__tests__/connection-state.test.ts +1 -13
  646. package/src/core/transports/__tests__/server.test.ts +408 -0
  647. package/src/core/transports/__tests__/static.test.ts +306 -0
  648. package/src/core/transports/__tests__/wasm.test.ts +166 -0
  649. package/src/core/{websocket → transports}/connection-state.ts +9 -16
  650. package/src/core/transports/server.ts +152 -0
  651. package/src/core/transports/static.ts +166 -0
  652. package/src/core/transports/wasm.ts +97 -0
  653. package/src/core/vscode/__tests__/vscode-bindings.test.ts +125 -0
  654. package/src/core/vscode/vscode-bindings.ts +14 -2
  655. package/src/core/wasm/__tests__/bridge.test.ts +210 -64
  656. package/src/core/wasm/__tests__/worker.test.ts +188 -0
  657. package/src/core/wasm/bridge.ts +120 -73
  658. package/src/core/wasm/rpc.ts +15 -3
  659. package/src/core/wasm/store.ts +3 -3
  660. package/src/core/wasm/worker/bootstrap.ts +55 -51
  661. package/src/core/wasm/worker/getMarimoWheel.ts +1 -1
  662. package/src/core/wasm/worker/save-worker.ts +9 -10
  663. package/src/core/wasm/worker/types.ts +30 -18
  664. package/src/core/wasm/worker/worker.ts +118 -72
  665. package/src/css/app/Cell.css +7 -36
  666. package/src/css/app/codemirror.css +1 -1
  667. package/src/css/common.css +1 -1
  668. package/src/css/globals.css +66 -22
  669. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  670. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  671. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  672. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  673. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  674. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  675. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  676. package/src/hooks/useAsyncData.ts +3 -3
  677. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  678. package/src/hooks/useDebounce.ts +6 -10
  679. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  680. package/src/hooks/useDelayElapsed.ts +14 -10
  681. package/src/hooks/useElapsedTime.ts +3 -3
  682. package/src/hooks/useEventListener.ts +1 -2
  683. package/src/hooks/useInputHistory.ts +4 -7
  684. package/src/hooks/useInternalStateWithSync.ts +6 -6
  685. package/src/hooks/useResizeObserver.ts +4 -7
  686. package/src/hooks/useScript.ts +67 -70
  687. package/src/hooks/useStateForKey.ts +93 -0
  688. package/src/mount.tsx +20 -15
  689. package/src/plugins/__tests__/plugin-object-schemas.test.ts +104 -0
  690. package/src/plugins/core/RenderHTML.tsx +3 -5
  691. package/src/plugins/core/__test__/RenderHTML-copy.test.tsx +60 -0
  692. package/src/plugins/core/registerReactComponent.tsx +5 -14
  693. package/src/plugins/impl/CodeEditorPlugin.tsx +1 -1
  694. package/src/plugins/impl/DataEditorPlugin.tsx +34 -9
  695. package/src/plugins/impl/DataTablePlugin.tsx +69 -37
  696. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  697. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  698. package/src/plugins/impl/FormPlugin.tsx +1 -1
  699. package/src/plugins/impl/RangeSliderPlugin.tsx +16 -2
  700. package/src/plugins/impl/RefreshPlugin.tsx +1 -0
  701. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  702. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +93 -4
  703. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +387 -1
  704. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  705. package/src/plugins/impl/__tests__/RangeSliderPlugin.test.tsx +109 -0
  706. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  707. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +153 -0
  708. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  709. package/src/plugins/impl/anywidget/model.ts +3 -1
  710. package/src/plugins/impl/anywidget/registry.ts +18 -14
  711. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  712. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  713. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  714. package/src/plugins/impl/chat/chat-ui.tsx +17 -15
  715. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  716. package/src/plugins/impl/common/error-banner.tsx +5 -3
  717. package/src/plugins/impl/data-editor/__tests__/column-types.test.ts +37 -0
  718. package/src/plugins/impl/data-editor/__tests__/editor-state.test.ts +52 -1
  719. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +113 -1
  720. package/src/plugins/impl/data-editor/__tests__/glide-utils.test.ts +116 -0
  721. package/src/plugins/impl/data-editor/column-types.ts +52 -0
  722. package/src/plugins/impl/data-editor/editor-state.ts +18 -2
  723. package/src/plugins/impl/data-editor/glide-data-editor.tsx +73 -12
  724. package/src/plugins/impl/data-editor/glide-utils.ts +33 -10
  725. package/src/plugins/impl/data-editor/types.ts +7 -0
  726. package/src/plugins/impl/data-explorer/ConnectedDataExplorerComponent.tsx +1 -1
  727. package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
  728. package/src/plugins/impl/data-explorer/state/reducer.ts +1 -1
  729. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -2
  730. package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
  731. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +66 -66
  732. package/src/plugins/impl/data-frames/panel.tsx +44 -16
  733. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  734. package/src/plugins/impl/panel/PanelPlugin.tsx +16 -17
  735. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  736. package/src/plugins/impl/panel/utils.ts +17 -0
  737. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +2 -5
  738. package/src/plugins/impl/plotly/selection.ts +25 -19
  739. package/src/plugins/impl/vega/VegaPlugin.tsx +2 -3
  740. package/src/plugins/impl/vega/make-selectable.ts +4 -7
  741. package/src/plugins/layout/DownloadPlugin.tsx +7 -11
  742. package/src/plugins/layout/RoutesPlugin.tsx +1 -1
  743. package/src/plugins/layout/__test__/DownloadPlugin.test.tsx +205 -0
  744. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  745. package/src/stories/button.stories.tsx +1 -0
  746. package/src/stories/cell-status.stories.tsx +17 -10
  747. package/src/stories/cell.stories.tsx +2 -36
  748. package/src/stories/data-table.stories.tsx +31 -0
  749. package/src/stories/editor.stories.tsx +24 -17
  750. package/src/stories/form-wrapper.stories.tsx +2 -2
  751. package/src/stories/layout/vertical/one-column.stories.tsx +28 -32
  752. package/src/stories/runtime-story.tsx +23 -17
  753. package/src/stories/select.stories.tsx +2 -2
  754. package/src/stories/switchable-multi-select.stories.tsx +1 -1
  755. package/src/utils/__tests__/arrays.test.ts +35 -0
  756. package/src/utils/__tests__/batch-requests.test.ts +3 -3
  757. package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +127 -1
  758. package/src/utils/__tests__/download.test.tsx +183 -23
  759. package/src/utils/__tests__/numbers.test.ts +105 -1
  760. package/src/utils/__tests__/objects.test.ts +21 -0
  761. package/src/utils/arrays.ts +11 -2
  762. package/src/utils/blob.ts +1 -1
  763. package/src/utils/createReducer.ts +115 -79
  764. package/src/utils/download.ts +67 -6
  765. package/src/utils/maps.ts +0 -3
  766. package/src/utils/numbers.ts +79 -18
  767. package/src/utils/objects.ts +4 -9
  768. package/src/utils/time.ts +10 -0
  769. package/src/utils/tracer.ts +9 -5
  770. package/src/utils/zod-utils.ts +1 -3
  771. package/dist/_baseMerge-BiGRwuqv.js +0 -165
  772. package/dist/any-language-editor-D4q-IFcM.js +0 -76
  773. package/dist/assets/__vite-browser-external-BXYkiFsk.js +0 -1
  774. package/dist/assets/worker-HPY9fbhz.js +0 -114
  775. package/dist/dropdown-menu-DHP9P4VE.js +0 -4678
  776. package/dist/feedback-button-B2JRVQRd.js +0 -379
  777. package/dist/hasIn-o2Us0uVx.js +0 -132
  778. package/dist/html-to-image-DWTzN7q3.js +0 -3251
  779. package/dist/isArrayLikeObject-BZl-15ir.js +0 -450
  780. package/dist/isEmpty-BSZ1LaUo.js +0 -35
  781. package/dist/isSymbol-kQcfYbNX.js +0 -8
  782. package/dist/label-DoysmLMW.js +0 -281
  783. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  784. package/dist/pick-CJeoxYOS.js +0 -51
  785. package/dist/precisionRound-DMIVQrt2.js +0 -97
  786. package/dist/select-DemoKT4G.js +0 -1344
  787. package/dist/toDate-CJWU9dyl.js +0 -1185
  788. package/dist/toast-PkdZraM8.js +0 -1830
  789. package/dist/use-restore-focus-CKjfFjLM.js +0 -6369
  790. package/dist/useLifecycle-BGWzTwHK.js +0 -169
  791. package/dist/useTheme-BhQvec43.js +0 -904
  792. package/src/components/chat/reasoning-accordion.tsx +0 -61
  793. package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
  794. package/src/components/chat/tool-call/tool-history-row.tsx +0 -174
  795. package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +0 -106
  796. package/src/components/editor/ai/add-cell-with-ai.tsx +0 -506
  797. package/src/components/editor/ai/staged-cell-submission.ts +0 -46
  798. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +0 -64
  799. package/src/components/editor/cell/code/__tests__/rtc-editor-config.test.ts +0 -50
  800. package/src/components/editor/chrome/wrapper/footer-items/rtc-status.tsx +0 -65
  801. package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
  802. package/src/components/editor/navigation/state.ts +0 -65
  803. package/src/components/icons/loading-ellipsis.tsx +0 -46
  804. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +0 -656
  805. package/src/core/ai/tools/run-cells-tool.ts +0 -303
  806. package/src/core/attachments/state.ts +0 -18
  807. package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
  808. package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
  809. package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
  810. package/src/core/cells/document-changes.ts +0 -944
  811. package/src/core/codemirror/rtc/extension.ts +0 -453
  812. package/src/core/codemirror/rtc/loro/__tests__/sync.test.ts +0 -52
  813. package/src/core/codemirror/rtc/loro/awareness.ts +0 -531
  814. package/src/core/codemirror/rtc/loro/colors.ts +0 -38
  815. package/src/core/codemirror/rtc/loro/sync.ts +0 -168
  816. package/src/core/host/AGENTS.md +0 -11
  817. package/src/core/host/__tests__/events.test.ts +0 -46
  818. package/src/core/host/__tests__/fixtures.ts +0 -39
  819. package/src/core/host/__tests__/notebook-client.test.ts +0 -433
  820. package/src/core/host/__tests__/protocol.test.ts +0 -214
  821. package/src/core/host/__tests__/stream.test.ts +0 -209
  822. package/src/core/host/connection.ts +0 -81
  823. package/src/core/host/events.ts +0 -61
  824. package/src/core/host/notebook-client.ts +0 -290
  825. package/src/core/host/protocol.ts +0 -1945
  826. package/src/core/host/resources/resource.ts +0 -47
  827. package/src/core/host/stream.ts +0 -211
  828. package/src/core/rtc/state.ts +0 -34
  829. package/src/core/state/__tests__/jotai.test.ts +0 -25
  830. package/src/core/wasm/worker/message-buffer.ts +0 -33
  831. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  832. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +0 -170
  833. package/src/core/websocket/state.ts +0 -7
  834. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  835. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -335
  836. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  837. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  838. package/src/core/websocket/transports/basic.ts +0 -102
  839. package/src/core/websocket/transports/create.ts +0 -31
  840. package/src/core/websocket/transports/sse.ts +0 -308
  841. package/src/core/websocket/transports/transport.ts +0 -50
  842. package/src/core/websocket/transports/ws.ts +0 -98
  843. package/src/core/websocket/useNotebookClient.ts +0 -88
  844. package/src/utils/__tests__/id-tree.test.ts +0 -2145
  845. package/src/utils/id-tree.tsx +0 -1134
  846. /package/src/{core/attachments/toast.ts → components/editor/agent-toast.ts} +0 -0
  847. /package/src/core/{websocket → session}/connection-utils.ts +0 -0
@@ -4,29 +4,32 @@ import { python } from "@codemirror/lang-python";
4
4
  import { EditorState } from "@codemirror/state";
5
5
  import { EditorView } from "@codemirror/view";
6
6
  import { createStore } from "jotai";
7
+ import { createRef } from "react";
7
8
  import {
8
9
  afterAll,
9
- afterEach,
10
10
  beforeAll,
11
+ afterEach,
11
12
  beforeEach,
12
13
  describe,
13
14
  expect,
14
15
  it,
15
16
  vi,
16
17
  } from "vitest";
18
+ import { MockExecutions } from "@/__mocks__/executions";
19
+ import {
20
+ notebookQueuedOrRunningCountAtom,
21
+ submitCells,
22
+ } from "@/core/executions";
23
+ import { store } from "@/core/state/jotai";
17
24
  import { MockNotebook } from "@/__mocks__/notebook";
18
25
  import { cellId } from "@/__tests__/branded";
19
26
  import type { CellHandle } from "@/components/editor/notebook-cell";
20
- import { CellId, SCRATCH_CELL_ID, SETUP_CELL_ID } from "@/core/cells/ids";
27
+ import { CellId, SCRATCH_CELL_ID } from "@/core/cells/ids";
21
28
  import { foldAllBulk, unfoldAllBulk } from "@/core/codemirror/editing/commands";
22
29
  import { adaptiveLanguageConfiguration } from "@/core/codemirror/language/extension";
23
30
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
24
31
  import type { OutputMessage } from "@/core/kernel/messages";
25
- import {
26
- type CollapsibleTree,
27
- MultiColumn,
28
- type CellColumnId,
29
- } from "@/utils/id-tree";
32
+ import type { CellConfig } from "@/core/network/types";
30
33
  import type { Seconds } from "@/utils/time";
31
34
  import {
32
35
  exportedForTesting,
@@ -34,13 +37,12 @@ import {
34
37
  type NotebookState,
35
38
  notebookAtom,
36
39
  } from "../cells";
37
- import { exportedForTesting as documentTransactionTestExports } from "../document-changes";
38
40
  import {
39
- focusAndScrollCellIntoView,
40
- scrollToBottom,
41
- scrollToTop,
42
- } from "../scrollCellIntoView";
43
- import type { CellData } from "../types";
41
+ type CellData,
42
+ createCell,
43
+ createCellConfig,
44
+ createCellRuntimeState,
45
+ } from "../types";
44
46
 
45
47
  vi.mock("@/core/codemirror/editing/commands", () => ({
46
48
  foldAllBulk: vi.fn(),
@@ -60,9 +62,6 @@ vi.mock("../scrollCellIntoView", async (importOriginal) => {
60
62
  };
61
63
  });
62
64
 
63
- const FIRST_COLUMN = 0;
64
- const SECOND_COLUMN = 1;
65
-
66
65
  const { initialNotebookState, reducer, createActions } = exportedForTesting;
67
66
 
68
67
  function formatCells(notebook: NotebookState) {
@@ -78,14 +77,7 @@ function formatCells(notebook: NotebookState) {
78
77
  const cells = cellsId.map((cellId) => cellData[cellId]);
79
78
  return cells.map((cell) => printCell(cell)).join("\n\n");
80
79
  };
81
- const printColumn = (column: CollapsibleTree<CellId>, columnIdx: number) => {
82
- return `> col ${columnIdx}\n${printCells(column.inOrderIds)}`;
83
- };
84
- const columns = cellIds.getColumns();
85
- if (columns.length > 1) {
86
- return wrap(columns.map(printColumn).join("\n\n"));
87
- }
88
- return wrap(printCells(cellIds.inOrderIds));
80
+ return wrap(printCells(cellIds));
89
81
  }
90
82
 
91
83
  function createEditor(content: string) {
@@ -121,8 +113,7 @@ describe("cell reducer", () => {
121
113
  let cells: ReturnType<typeof flattenTopLevelNotebookCells>;
122
114
  let firstCellId: CellId;
123
115
 
124
- const actions = createActions((action) => {
125
- state = reducer(state, action);
116
+ const mountEditors = () => {
126
117
  for (const [cellIdString, handle] of Object.entries(state.cellHandles)) {
127
118
  // @ts-expect-error - Typescript Object.entries doesn't know that keys are CellId
128
119
  const cid: CellId = cellIdString;
@@ -136,7 +127,94 @@ describe("cell reducer", () => {
136
127
  }
137
128
  }
138
129
  cells = flattenTopLevelNotebookCells(state);
139
- });
130
+ };
131
+ const actions = createActions((action) => {
132
+ state = reducer(state, action);
133
+ mountEditors();
134
+ });
135
+
136
+ /** Put a cell into the notebook the way the document would. */
137
+ function seedCell({
138
+ cellId,
139
+ before,
140
+ code = "",
141
+ name,
142
+ config,
143
+ newCellId,
144
+ hideCode,
145
+ autoFocus = true,
146
+ }: {
147
+ cellId: CellId | "__end__";
148
+ before: boolean;
149
+ code?: string;
150
+ name?: string;
151
+ config?: CellConfig;
152
+ newCellId?: CellId;
153
+ hideCode?: boolean;
154
+ autoFocus?: boolean;
155
+ }): CellId {
156
+ const cellIndex =
157
+ cellId === "__end__"
158
+ ? state.cellIds.length
159
+ : state.cellIds.indexOf(cellId);
160
+ const id = newCellId ?? CellId.create();
161
+ const cellIds = [...state.cellIds];
162
+ cellIds.splice(before ? cellIndex : cellIndex + 1, 0, id);
163
+ state = {
164
+ ...state,
165
+ cellIds,
166
+ cellData: {
167
+ ...state.cellData,
168
+ [id]: createCell({
169
+ id,
170
+ code,
171
+ name,
172
+ config: createCellConfig({
173
+ ...config,
174
+ ...(hideCode != null && { hide_code: hideCode }),
175
+ }),
176
+ edited: Boolean(code),
177
+ }),
178
+ },
179
+ cellRuntime: { ...state.cellRuntime, [id]: createCellRuntimeState() },
180
+ cellHandles: { ...state.cellHandles, [id]: createRef() },
181
+ scrollKey: autoFocus ? id : null,
182
+ };
183
+ mountEditors();
184
+ return id;
185
+ }
186
+
187
+ /** Edit a cell's code as the document delivers it. */
188
+ function editCode(cellId: CellId, code: string) {
189
+ const cells = state.cellIds.map((id) => {
190
+ const cell = state.cellData[id];
191
+ return {
192
+ id,
193
+ code: id === cellId ? code : cell.code,
194
+ name: cell.name,
195
+ config: {
196
+ column: cell.config.column ?? null,
197
+ disabled: cell.config.disabled ?? false,
198
+ hide_code: cell.config.hide_code ?? false,
199
+ expand_output: cell.config.expand_output ?? false,
200
+ },
201
+ };
202
+ });
203
+ actions.reconcileDocument({
204
+ order: state.cellIds,
205
+ cells: new Map(cells.map((cell) => [cell.id, cell])),
206
+ });
207
+ }
208
+
209
+ /** Format a cell's code, as the editor reports it. */
210
+ function formatCode(cellId: CellId, code: string) {
211
+ actions.codeFormatted({
212
+ cellId,
213
+ before: state.cellData[cellId].code,
214
+ after: code,
215
+ });
216
+ editCode(cellId, code);
217
+ }
140
218
 
141
219
  let i = 0;
142
220
  const originalCreate = CellId.create.bind(CellId);
@@ -151,200 +229,16 @@ describe("cell reducer", () => {
151
229
  i = 0;
152
230
 
153
231
  state = initialNotebookState();
154
- state.cellIds = MultiColumn.from([]);
155
- actions.createNewCell({ cellId: "__end__", before: false });
156
- firstCellId = state.cellIds.inOrderIds[0];
157
- });
158
-
159
- afterEach(() => {
160
- documentTransactionTestExports.cancelPendingChanges();
232
+ seedCell({ cellId: "__end__", before: false });
233
+ firstCellId = state.cellIds[0];
161
234
  });
162
235
 
163
236
  afterAll(() => {
164
237
  CellId.create = originalCreate;
165
238
  });
166
239
 
167
- it("can add a cell after another cell", () => {
168
- actions.createNewCell({
169
- cellId: firstCellId,
170
- before: false,
171
- });
172
- expect(formatCells(state)).toMatchInlineSnapshot(`
173
- "
174
- [0] ''
175
-
176
- [1] ''
177
- "
178
- `);
179
- });
180
-
181
- it("can add a cell to the end with code", () => {
182
- actions.createNewCell({
183
- cellId: "__end__",
184
- code: "import numpy as np",
185
- before: false,
186
- });
187
- expect(formatCells(state)).toMatchInlineSnapshot(`
188
- "
189
- [0] ''
190
-
191
- [1] 'import numpy as np'
192
- "
193
- `);
194
-
195
- // Cell should be added to the end and edited
196
- expect(cells[1].code).toBe("import numpy as np");
197
- expect(cells[1].edited).toBe(true);
198
- expect(cells[1].lastCodeRun).toBe(null);
199
- });
200
-
201
- it("can add a cell before another cell", () => {
202
- actions.createNewCell({
203
- cellId: firstCellId,
204
- before: true,
205
- });
206
- expect(formatCells(state)).toMatchInlineSnapshot(`
207
- "
208
- [1] ''
209
-
210
- [0] ''
211
- "
212
- `);
213
- });
214
-
215
- it("can add a cell with name and config", () => {
216
- actions.createNewCell({
217
- cellId: firstCellId,
218
- before: false,
219
- code: "x = 1",
220
- name: "My Cell",
221
- config: { hide_code: true, disabled: false },
222
- });
223
- const newCellId = state.cellIds.inOrderIds[1];
224
- expect(state.cellData[newCellId].name).toBe("My Cell");
225
- expect(state.cellData[newCellId].config.hide_code).toBe(true);
226
- expect(state.cellData[newCellId].config.disabled).toBe(false);
227
- });
228
-
229
- it("can delete a Python cell and undo delete", () => {
230
- actions.createNewCell({
231
- cellId: firstCellId,
232
- before: false,
233
- });
234
- actions.deleteCell({
235
- cellId: firstCellId,
236
- });
237
- expect(formatCells(state)).toMatchInlineSnapshot(`
238
- "
239
- [1] ''
240
- "
241
- `);
242
-
243
- // undo
244
- actions.undoDeleteCell();
245
- expect(formatCells(state)).toMatchInlineSnapshot(`
246
- "
247
- [2] ''
248
-
249
- [1] ''
250
- "
251
- `);
252
-
253
- // Verify scrollKey is set to the restored cell
254
- expect(state.scrollKey).toBe("2");
255
- });
256
-
257
- it("can delete a SQL cell and undo delete", () => {
258
- actions.createNewCell({
259
- cellId: firstCellId,
260
- before: false,
261
- code: `df = mo.sql("""SELECT * FROM table""")`,
262
- });
263
- const newCellId = state.cellIds.getColumns()[0].atOrThrow(1);
264
- actions.deleteCell({
265
- cellId: newCellId,
266
- });
267
- expect(formatCells(state)).toMatchInlineSnapshot(`
268
- "
269
- [0] ''
270
- "
271
- `);
272
-
273
- // undo
274
- actions.undoDeleteCell();
275
- expect(formatCells(state)).toMatchInlineSnapshot(`
276
- "
277
- [0] ''
278
-
279
- [2] 'df = mo.sql("""SELECT * FROM table""")'
280
- "
281
- `);
282
- });
283
-
284
- it("can delete a Markdown cell and undo delete", () => {
285
- const text = "The quick brown fox jumps over the lazy dog.";
286
- actions.createNewCell({
287
- cellId: firstCellId,
288
- before: false,
289
- code: `mo.md(r"""${text}""")`,
290
- });
291
- const newCellId = state.cellIds.getColumns()[0].atOrThrow(1);
292
- actions.deleteCell({
293
- cellId: newCellId,
294
- });
295
- expect(formatCells(state)).toMatchInlineSnapshot(`
296
- "
297
- [0] ''
298
- "
299
- `);
300
-
301
- // undo
302
- actions.undoDeleteCell();
303
- expect(formatCells(state)).toMatchInlineSnapshot(`
304
- "
305
- [0] ''
306
-
307
- [2] 'mo.md(r"""The quick brown fox jumps over the lazy dog.""")'
308
- "
309
- `);
310
- });
311
-
312
- it("undo delete restores cell config (hide_code, disabled)", () => {
313
- actions.createNewCell({
314
- cellId: firstCellId,
315
- before: false,
316
- code: "x = 1",
317
- });
318
- const newCellId = state.cellIds.getColumns()[0].atOrThrow(1);
319
-
320
- // Set config to hidden and disabled
321
- actions.updateCellConfig({
322
- cellId: newCellId,
323
- config: { hide_code: true, disabled: true },
324
- });
325
- expect(state.cellData[newCellId].config.hide_code).toBe(true);
326
- expect(state.cellData[newCellId].config.disabled).toBe(true);
327
-
328
- // Delete the cell
329
- actions.deleteCell({ cellId: newCellId });
330
- expect(state.cellIds.inOrderIds).not.toContain(newCellId);
331
-
332
- // Undo delete
333
- actions.undoDeleteCell();
334
- const restoredCellId =
335
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
336
-
337
- // Config should be preserved
338
- expect(state.cellData[restoredCellId].config.hide_code).toBe(true);
339
- expect(state.cellData[restoredCellId].config.disabled).toBe(true);
340
- });
341
-
342
240
  it("can update a cell", () => {
343
- actions.updateCellCode({
344
- cellId: firstCellId,
345
- code: "import numpy as np",
346
- formattingChange: false,
347
- });
241
+ editCode(firstCellId, "import numpy as np");
348
242
  expect(formatCells(state)).toMatchInlineSnapshot(`
349
243
  "
350
244
  [0] 'import numpy as np'
@@ -352,443 +246,6 @@ describe("cell reducer", () => {
352
246
  `);
353
247
  });
354
248
 
355
- it("can move a cell", () => {
356
- actions.createNewCell({
357
- cellId: firstCellId,
358
- before: false,
359
- });
360
- expect(formatCells(state)).toMatchInlineSnapshot(`
361
- "
362
- [0] ''
363
-
364
- [1] ''
365
- "
366
- `);
367
-
368
- // move first cell to the end
369
- actions.moveCell({
370
- cellId: firstCellId,
371
- before: false,
372
- });
373
- expect(formatCells(state)).toMatchInlineSnapshot(`
374
- "
375
- [1] ''
376
-
377
- [0] ''
378
- "
379
- `);
380
-
381
- // move it back
382
- actions.moveCell({
383
- cellId: firstCellId,
384
- before: true,
385
- });
386
- expect(formatCells(state)).toMatchInlineSnapshot(`
387
- "
388
- [0] ''
389
-
390
- [1] ''
391
- "
392
- `);
393
-
394
- // Add a column breakpoint to test left/right movement
395
- actions.addColumnBreakpoint({ cellId: cellId("1") });
396
- expect(formatCells(state)).toMatchInlineSnapshot(`
397
- "
398
- > col 0
399
- [0] ''
400
-
401
- > col 1
402
- [1] ''
403
- "
404
- `);
405
-
406
- // Move cell right
407
- actions.moveCell({
408
- cellId: firstCellId,
409
- direction: "right",
410
- });
411
- expect(formatCells(state)).toMatchInlineSnapshot(`
412
- "
413
- > col 0
414
-
415
-
416
- > col 1
417
- [0] ''
418
-
419
- [1] ''
420
- "
421
- `);
422
-
423
- // Move cell left
424
- actions.moveCell({
425
- cellId: firstCellId,
426
- direction: "left",
427
- });
428
- expect(formatCells(state)).toMatchInlineSnapshot(`
429
- "
430
- > col 0
431
- [0] ''
432
-
433
- > col 1
434
- [1] ''
435
- "
436
- `);
437
-
438
- // Try to move cell left when it's already in leftmost column (should noop)
439
- actions.moveCell({
440
- cellId: firstCellId,
441
- direction: "left",
442
- });
443
- expect(formatCells(state)).toMatchInlineSnapshot(`
444
- "
445
- > col 0
446
- [0] ''
447
-
448
- > col 1
449
- [1] ''
450
- "
451
- `);
452
-
453
- // Try to move cell right when it's already in rightmost column (should noop)
454
- actions.moveCell({
455
- cellId: cellId("1"),
456
- direction: "right",
457
- });
458
- expect(formatCells(state)).toMatchInlineSnapshot(`
459
- "
460
- > col 0
461
- [0] ''
462
-
463
- > col 1
464
- [1] ''
465
- "
466
- `);
467
- });
468
-
469
- it("can drag and drop a cell", () => {
470
- actions.createNewCell({
471
- cellId: firstCellId,
472
- before: false,
473
- });
474
- actions.createNewCell({
475
- cellId: cellId("1"),
476
- before: false,
477
- });
478
- expect(formatCells(state)).toMatchInlineSnapshot(`
479
- "
480
- [0] ''
481
-
482
- [1] ''
483
-
484
- [2] ''
485
- "
486
- `);
487
-
488
- // drag first cell to the end
489
- actions.dropCellOverCell({
490
- cellId: firstCellId,
491
- overCellId: cellId("2"),
492
- });
493
- expect(formatCells(state)).toMatchInlineSnapshot(`
494
- "
495
- [1] ''
496
-
497
- [2] ''
498
-
499
- [0] ''
500
- "
501
- `);
502
-
503
- // drag it back to the middle
504
- actions.dropCellOverCell({
505
- cellId: firstCellId,
506
- overCellId: cellId("2"),
507
- });
508
- expect(formatCells(state)).toMatchInlineSnapshot(`
509
- "
510
- [1] ''
511
-
512
- [0] ''
513
-
514
- [2] ''
515
- "
516
- `);
517
- });
518
-
519
- it("can move a cell to an exact index within a column", () => {
520
- actions.createNewCell({
521
- cellId: firstCellId,
522
- before: false,
523
- });
524
- actions.createNewCell({
525
- cellId: cellId("1"),
526
- before: false,
527
- });
528
-
529
- const columnId = state.cellIds.atOrThrow(0).id;
530
- actions.moveCellToIndex({
531
- cellId: firstCellId,
532
- columnId,
533
- index: 3,
534
- });
535
-
536
- expect(formatCells(state)).toMatchInlineSnapshot(`
537
- "
538
- [1] ''
539
-
540
- [2] ''
541
-
542
- [0] ''
543
- "
544
- `);
545
- });
546
-
547
- it("can move a cell to an exact index across columns", () => {
548
- actions.createNewCell({
549
- cellId: firstCellId,
550
- before: false,
551
- });
552
- actions.createNewCell({
553
- cellId: cellId("1"),
554
- before: false,
555
- });
556
- actions.addColumnBreakpoint({ cellId: cellId("1") });
557
-
558
- const secondColumnId = state.cellIds.atOrThrow(1).id;
559
- actions.moveCellToIndex({
560
- cellId: firstCellId,
561
- columnId: secondColumnId,
562
- index: 1,
563
- });
564
-
565
- expect(formatCells(state)).toMatchInlineSnapshot(`
566
- "
567
- > col 0
568
-
569
-
570
- > col 1
571
- [1] ''
572
-
573
- [0] ''
574
-
575
- [2] ''
576
- "
577
- `);
578
- });
579
-
580
- it("moveCellToIndex is a no-op when moving to the same position", () => {
581
- actions.createNewCell({
582
- cellId: firstCellId,
583
- before: false,
584
- });
585
- actions.createNewCell({
586
- cellId: cellId("1"),
587
- before: false,
588
- });
589
-
590
- const columnId = state.cellIds.atOrThrow(0).id;
591
- const before = formatCells(state);
592
-
593
- actions.moveCellToIndex({
594
- cellId: firstCellId,
595
- columnId,
596
- index: 0,
597
- });
598
-
599
- expect(formatCells(state)).toBe(before);
600
- });
601
-
602
- it("moveCellToIndex is a no-op for an invalid columnId", () => {
603
- actions.createNewCell({
604
- cellId: firstCellId,
605
- before: false,
606
- });
607
-
608
- const before = formatCells(state);
609
-
610
- actions.moveCellToIndex({
611
- cellId: firstCellId,
612
- columnId: "nonexistent-column" as CellColumnId,
613
- index: 0,
614
- });
615
-
616
- expect(formatCells(state)).toBe(before);
617
- });
618
-
619
- it("can move multiple cells relative to target", () => {
620
- actions.createNewCell({
621
- cellId: firstCellId,
622
- before: false,
623
- });
624
- actions.createNewCell({
625
- cellId: cellId("1"),
626
- before: false,
627
- });
628
- expect(formatCells(state)).toMatchInlineSnapshot(`
629
- "
630
- [0] ''
631
-
632
- [1] ''
633
-
634
- [2] ''
635
- "
636
- `);
637
-
638
- // Move first two cells after the third
639
- actions.moveCellsRelativeTo({
640
- cellIds: [firstCellId, cellId("1")],
641
- targetCellId: cellId("2"),
642
- position: "after",
643
- });
644
- expect(formatCells(state)).toMatchInlineSnapshot(`
645
- "
646
- [2] ''
647
-
648
- [0] ''
649
-
650
- [1] ''
651
- "
652
- `);
653
- });
654
-
655
- it("can undo cut-paste (move with previousPlacements)", () => {
656
- actions.createNewCell({
657
- cellId: firstCellId,
658
- before: false,
659
- });
660
- actions.createNewCell({
661
- cellId: cellId("1"),
662
- before: false,
663
- });
664
- expect(formatCells(state)).toMatchInlineSnapshot(`
665
- "
666
- [0] ''
667
-
668
- [1] ''
669
-
670
- [2] ''
671
- "
672
- `);
673
-
674
- const col = state.cellIds.findWithId(firstCellId);
675
- const previousPlacements = [
676
- {
677
- columnId: col.id,
678
- index: col.indexOfOrThrow(
679
- firstCellId,
680
- ) as import("@/utils/id-tree").CellIndex,
681
- },
682
- {
683
- columnId: col.id,
684
- index: col.indexOfOrThrow(
685
- cellId("1"),
686
- ) as import("@/utils/id-tree").CellIndex,
687
- },
688
- ];
689
-
690
- actions.moveCellsRelativeTo({
691
- cellIds: [firstCellId, cellId("1")],
692
- targetCellId: cellId("2"),
693
- position: "after",
694
- previousPlacements,
695
- });
696
- expect(formatCells(state)).toMatchInlineSnapshot(`
697
- "
698
- [2] ''
699
-
700
- [0] ''
701
-
702
- [1] ''
703
- "
704
- `);
705
-
706
- actions.undoDeleteCell();
707
- expect(formatCells(state)).toMatchInlineSnapshot(`
708
- "
709
- [0] ''
710
-
711
- [1] ''
712
-
713
- [2] ''
714
- "
715
- `);
716
- });
717
-
718
- it("undo order: cut-paste then delete — first undo restores delete, second undo undoes move", () => {
719
- actions.createNewCell({
720
- cellId: firstCellId,
721
- before: false,
722
- });
723
- actions.createNewCell({
724
- cellId: cellId("1"),
725
- before: false,
726
- });
727
-
728
- const col = state.cellIds.findWithId(firstCellId);
729
- const previousPlacements = [
730
- {
731
- columnId: col.id,
732
- index: col.indexOfOrThrow(
733
- firstCellId,
734
- ) as import("@/utils/id-tree").CellIndex,
735
- },
736
- {
737
- columnId: col.id,
738
- index: col.indexOfOrThrow(
739
- cellId("1"),
740
- ) as import("@/utils/id-tree").CellIndex,
741
- },
742
- ];
743
-
744
- actions.moveCellsRelativeTo({
745
- cellIds: [firstCellId, cellId("1")],
746
- targetCellId: cellId("2"),
747
- position: "after",
748
- previousPlacements,
749
- });
750
- expect(formatCells(state)).toMatchInlineSnapshot(`
751
- "
752
- [2] ''
753
-
754
- [0] ''
755
-
756
- [1] ''
757
- "
758
- `);
759
-
760
- actions.deleteCell({ cellId: cellId("2") });
761
- expect(formatCells(state)).toMatchInlineSnapshot(`
762
- "
763
- [0] ''
764
-
765
- [1] ''
766
- "
767
- `);
768
-
769
- actions.undoDeleteCell();
770
- expect(formatCells(state)).toMatchInlineSnapshot(`
771
- "
772
- [3] ''
773
-
774
- [0] ''
775
-
776
- [1] ''
777
- "
778
- `);
779
-
780
- actions.undoDeleteCell();
781
- expect(formatCells(state)).toMatchInlineSnapshot(`
782
- "
783
- [0] ''
784
-
785
- [1] ''
786
-
787
- [3] ''
788
- "
789
- `);
790
- });
791
-
792
249
  it("keeps error evidence after an edit and a new queued run", () => {
793
250
  const code = "raise ValueError('old failure')";
794
251
  const output: OutputMessage = {
@@ -820,11 +277,7 @@ describe("cell reducer", () => {
820
277
  stale_inputs: null,
821
278
  timestamp: 1,
822
279
  });
823
- actions.updateCellCode({
824
- cellId: firstCellId,
825
- code: "print('new code')",
826
- formattingChange: false,
827
- });
280
+ editCode(firstCellId, "print('new code')");
828
281
  actions.prepareForRun({ cellId: firstCellId });
829
282
  actions.handleCellMessage({
830
283
  cell_id: firstCellId,
@@ -846,19 +299,13 @@ describe("cell reducer", () => {
846
299
  /////////////////
847
300
  // Initial state
848
301
  let cell = cells[0];
849
- expect(cell.status).toBe("idle");
850
302
  expect(cell.lastCodeRun).toBe(null);
851
303
  expect(cell.edited).toBe(false);
852
304
  expect(cell).toMatchSnapshot("initial state");
853
305
 
854
306
  // Update code
855
- actions.updateCellCode({
856
- cellId: firstCellId,
857
- code: "import marimo as mo",
858
- formattingChange: false,
859
- });
307
+ editCode(firstCellId, "import marimo as mo");
860
308
  cell = cells[0];
861
- expect(cell.status).toBe("idle");
862
309
  expect(cell.lastCodeRun).toBe(null);
863
310
  expect(cell.edited).toBe(true);
864
311
  expect(cell).toMatchSnapshot("code edited");
@@ -868,7 +315,6 @@ describe("cell reducer", () => {
868
315
  cellId: firstCellId,
869
316
  });
870
317
  cell = cells[0];
871
- expect(cell.status).toBe("queued");
872
318
  expect(cell.lastCodeRun).toBe("import marimo as mo");
873
319
  expect(cell.edited).toBe(false);
874
320
  expect(cell).toMatchSnapshot("prepared for run");
@@ -883,12 +329,8 @@ describe("cell reducer", () => {
883
329
  timestamp: new Date(10).getTime() as Seconds,
884
330
  });
885
331
  cell = cells[0];
886
- expect(cell.status).toBe("queued");
887
332
  expect(cell.lastCodeRun).toBe("import marimo as mo");
888
333
  expect(cell.edited).toBe(false);
889
- expect(cell.runElapsedTimeMs).toBe(null);
890
- expect(cell.runStartTimestamp).toBe(null);
891
- expect(cell.lastRunStartTimestamp).toBe(null);
892
334
  expect(cell).toMatchSnapshot("queued message received");
893
335
 
894
336
  // Receive running messages
@@ -901,12 +343,8 @@ describe("cell reducer", () => {
901
343
  timestamp: new Date(20).getTime() as Seconds,
902
344
  });
903
345
  cell = cells[0];
904
- expect(cell.status).toBe("running");
905
346
  expect(cell.lastCodeRun).toBe("import marimo as mo");
906
347
  expect(cell.edited).toBe(false);
907
- expect(cell.runElapsedTimeMs).toBe(null);
908
- expect(cell.runStartTimestamp).toBe(20);
909
- expect(cell.lastRunStartTimestamp).toBe(20);
910
348
  expect(cell).toMatchSnapshot("running message received");
911
349
 
912
350
  // Console messages shouldn't transition status
@@ -924,11 +362,7 @@ describe("cell reducer", () => {
924
362
  timestamp: new Date(22).getTime() as Seconds,
925
363
  });
926
364
  cell = cells[0];
927
- expect(cell.status).toBe("running");
928
365
  expect(cell.edited).toBe(false);
929
- expect(cell.runElapsedTimeMs).toBe(null);
930
- expect(cell.runStartTimestamp).toBe(20);
931
- expect(cell.lastRunStartTimestamp).toBe(20);
932
366
  expect(cell).toMatchSnapshot("console message received");
933
367
 
934
368
  // Receive output messages
@@ -951,44 +385,26 @@ describe("cell reducer", () => {
951
385
  timestamp: new Date(33).getTime() as Seconds,
952
386
  });
953
387
  cell = cells[0];
954
- expect(cell.status).toBe("idle");
955
388
  expect(cell.edited).toBe(false);
956
- expect(cell.runElapsedTimeMs).toBe(13_000);
957
- expect(cell.runStartTimestamp).toBe(null);
958
- expect(cell.lastRunStartTimestamp).toBe(20);
959
389
  expect(cell).toMatchSnapshot("output received");
960
390
 
961
391
  // EDITING BACK AND FORTH
962
392
  /////////////////
963
393
  // Update code again
964
- actions.updateCellCode({
965
- cellId: firstCellId,
966
- code: "import marimo as mo\nimport numpy",
967
- formattingChange: false,
968
- });
394
+ editCode(firstCellId, "import marimo as mo\nimport numpy");
969
395
  cell = cells[0];
970
- expect(cell.status).toBe("idle");
971
396
  expect(cell.edited).toBe(true);
972
397
  expect(cell).toMatchSnapshot("code edited again");
973
398
 
974
399
  // Update code should be unedited
975
- actions.updateCellCode({
976
- cellId: firstCellId,
977
- code: "import marimo as mo",
978
- formattingChange: false,
979
- });
400
+ editCode(firstCellId, "import marimo as mo");
980
401
  cell = cells[0];
981
402
  expect(cell.edited).toBe(false);
982
403
  expect(cell).toMatchSnapshot("code reverted");
983
404
 
984
405
  // Update code should be edited again
985
- actions.updateCellCode({
986
- cellId: firstCellId,
987
- code: "import marimo as mo\nimport numpy",
988
- formattingChange: false,
989
- });
406
+ editCode(firstCellId, "import marimo as mo\nimport numpy");
990
407
  cell = cells[0];
991
- expect(cell.status).toBe("idle");
992
408
  expect(cell.lastCodeRun).toBe("import marimo as mo");
993
409
  expect(cell.edited).toBe(true);
994
410
  expect(cell).toMatchSnapshot("code changed after revert");
@@ -1041,11 +457,8 @@ describe("cell reducer", () => {
1041
457
  });
1042
458
  cell = cells[0];
1043
459
  expect(cell.output).not.toBe(null); // keep old output
1044
- expect(cell.status).toBe("idle");
1045
460
  expect(cell.lastCodeRun).toBe("import marimo as mo\nimport numpy");
1046
461
  expect(cell.edited).toBe(false);
1047
- expect(cell.runElapsedTimeMs).toBe(11_000);
1048
- expect(cell.runStartTimestamp).toBe(null);
1049
462
  expect(cell).toMatchSnapshot("error response received");
1050
463
 
1051
464
  // INTERRUPT RESPONSE
@@ -1062,127 +475,53 @@ describe("cell reducer", () => {
1062
475
  output: undefined,
1063
476
  console: null,
1064
477
  status: "queued",
1065
- stale_inputs: null,
1066
- timestamp: new Date(40).getTime() as Seconds,
1067
- });
1068
- cell = cells[0];
1069
- expect(cell.output).not.toBe(null); // keep old output
1070
- // Running
1071
- actions.handleCellMessage({
1072
- cell_id: firstCellId,
1073
- output: undefined,
1074
- console: null,
1075
- status: "running",
1076
- stale_inputs: null,
1077
- timestamp: new Date(50).getTime() as Seconds,
1078
- });
1079
- cell = cells[0];
1080
- expect(cell.output).not.toBe(null); // keep old output
1081
- // Receive error
1082
- actions.handleCellMessage({
1083
- cell_id: firstCellId,
1084
- output: {
1085
- channel: "marimo-error",
1086
- mimetype: "application/vnd.marimo+error",
1087
- data: [{ type: "interruption" }],
1088
- timestamp: 0,
1089
- },
1090
- console: null,
1091
- status: "idle",
1092
- stale_inputs: null,
1093
- timestamp: new Date(61).getTime() as Seconds,
1094
- });
1095
- cell = cells[0];
1096
- expect(cell.status).toBe("idle");
1097
- expect(cell.lastCodeRun).toBe(cell.code);
1098
- expect(cell.edited).toBe(false);
1099
- expect(cell.runElapsedTimeMs).toBe(11_000);
1100
- expect(cell.runStartTimestamp).toBe(null);
1101
- expect(cell).toMatchSnapshot("interruption response received");
1102
- });
1103
-
1104
- it("errors reset status to idle", () => {
1105
- // Initial state
1106
- let cell = cells[0];
1107
- expect(cell.status).toBe("idle");
1108
- expect(cell.lastCodeRun).toBe(null);
1109
- expect(cell.edited).toBe(false);
1110
- expect(cell).toMatchSnapshot("initial state");
1111
-
1112
- // Update code
1113
- actions.updateCellCode({
1114
- cellId: firstCellId,
1115
- code: "import marimo as mo",
1116
- formattingChange: false,
1117
- });
1118
- cell = cells[0];
1119
- expect(cell.status).toBe("idle");
1120
- expect(cell.lastCodeRun).toBe(null);
1121
- expect(cell.edited).toBe(true);
1122
- expect(cell).toMatchSnapshot("code edited");
1123
-
1124
- // Prepare for run
1125
- actions.prepareForRun({
1126
- cellId: firstCellId,
478
+ stale_inputs: null,
479
+ timestamp: new Date(40).getTime() as Seconds,
1127
480
  });
1128
481
  cell = cells[0];
1129
- expect(cell.status).toBe("queued");
1130
- expect(cell.lastCodeRun).toBe("import marimo as mo");
1131
- expect(cell.edited).toBe(false);
1132
- expect(cell).toMatchSnapshot("prepared for run");
1133
-
1134
- // ERROR RESPONSE
1135
- //
1136
- // should reset status to idle
1137
- /////////////////
1138
- // Prepare for run
1139
- actions.prepareForRun({
1140
- cellId: firstCellId,
482
+ expect(cell.output).not.toBe(null); // keep old output
483
+ // Running
484
+ actions.handleCellMessage({
485
+ cell_id: firstCellId,
486
+ output: undefined,
487
+ console: null,
488
+ status: "running",
489
+ stale_inputs: null,
490
+ timestamp: new Date(50).getTime() as Seconds,
1141
491
  });
1142
492
  cell = cells[0];
493
+ expect(cell.output).not.toBe(null); // keep old output
1143
494
  // Receive error
1144
495
  actions.handleCellMessage({
1145
496
  cell_id: firstCellId,
1146
497
  output: {
1147
498
  channel: "marimo-error",
1148
499
  mimetype: "application/vnd.marimo+error",
1149
- data: [
1150
- { type: "exception", exception_type: "SyntaxError", msg: "Oh no!" },
1151
- ],
500
+ data: [{ type: "interruption" }],
1152
501
  timestamp: 0,
1153
502
  },
1154
503
  console: null,
504
+ status: "idle",
1155
505
  stale_inputs: null,
1156
506
  timestamp: new Date(61).getTime() as Seconds,
1157
507
  });
1158
508
  cell = cells[0];
1159
- expect(cell.status).toBe("idle");
1160
- expect(cell.lastCodeRun).toBe("import marimo as mo");
509
+ expect(cell.lastCodeRun).toBe(cell.code);
1161
510
  expect(cell.edited).toBe(false);
1162
- expect(cell.runElapsedTimeMs).toBe(null);
1163
- expect(cell.runStartTimestamp).toBe(null);
1164
- expect(cell).toMatchSnapshot("error response received");
511
+ expect(cell).toMatchSnapshot("interruption response received");
1165
512
  });
1166
513
 
1167
514
  it("can run a stale cell", () => {
1168
515
  // Update code of first cell
1169
- actions.updateCellCode({
1170
- cellId: firstCellId,
1171
- code: "import marimo as mo",
1172
- formattingChange: false,
1173
- });
516
+ editCode(firstCellId, "import marimo as mo");
1174
517
  // Add cell
1175
- actions.createNewCell({
518
+ seedCell({
1176
519
  cellId: firstCellId,
1177
520
  before: false,
1178
521
  });
1179
522
  const secondCell = cells[1].id;
1180
523
  // Update code
1181
- actions.updateCellCode({
1182
- code: "mo.slider()",
1183
- cellId: secondCell,
1184
- formattingChange: false,
1185
- });
524
+ editCode(secondCell, "mo.slider()");
1186
525
 
1187
526
  // Prepare for run
1188
527
  actions.prepareForRun({
@@ -1199,11 +538,8 @@ describe("cell reducer", () => {
1199
538
  timestamp: new Date(10).getTime() as Seconds,
1200
539
  });
1201
540
  let cell = cells[1];
1202
- expect(cell.status).toBe("queued");
1203
541
  expect(cell.lastCodeRun).toBe("mo.slider()");
1204
542
  expect(cell.edited).toBe(false);
1205
- expect(cell.runElapsedTimeMs).toBe(null);
1206
- expect(cell.runStartTimestamp).toBe(null);
1207
543
  expect(cell).toMatchSnapshot("queued message received");
1208
544
 
1209
545
  // Receive stale message
@@ -1216,23 +552,15 @@ describe("cell reducer", () => {
1216
552
  timestamp: new Date(20).getTime() as Seconds,
1217
553
  });
1218
554
  cell = cells[1];
1219
- expect(cell.status).toBe("queued");
1220
555
  expect(cell.lastCodeRun).toBe("mo.slider()");
1221
556
  expect(cell.edited).toBe(false);
1222
- expect(cell.runElapsedTimeMs).toBe(null);
1223
- expect(cell.runStartTimestamp).toBe(null);
1224
557
  expect(cell).toMatchSnapshot("stale inputs received");
1225
558
  });
1226
559
 
1227
560
  it("can format code and update cell", () => {
1228
561
  const firstCellId = cells[0].id;
1229
- actions.updateCellCode({
1230
- cellId: firstCellId,
1231
- code: "import marimo as mo",
1232
- formattingChange: false,
1233
- });
562
+ editCode(firstCellId, "import marimo as mo");
1234
563
  let cell = cells[0];
1235
- expect(cell.status).toBe("idle");
1236
564
  expect(cell.lastCodeRun).toBe(null);
1237
565
  expect(cell.edited).toBe(true);
1238
566
 
@@ -1261,56 +589,19 @@ describe("cell reducer", () => {
1261
589
 
1262
590
  // Check steady state
1263
591
  cell = cells[0];
1264
- expect(cell.status).toBe("idle");
1265
592
  expect(cell.edited).toBe(false);
1266
593
  expect(cell.lastCodeRun).toBe("import marimo as mo");
1267
594
 
1268
595
  // Format code
1269
- actions.updateCellCode({
1270
- cellId: firstCellId,
1271
- code: "import marimo as mo",
1272
- formattingChange: true,
1273
- });
596
+ formatCode(firstCellId, "import marimo as mo");
1274
597
  cell = cells[0];
1275
- expect(cell.status).toBe("idle");
1276
598
  expect(cell.lastCodeRun).toBe("import marimo as mo");
1277
599
  expect(cell.edited).toBe(false);
1278
600
  });
1279
601
 
1280
- it("can update a cells config", () => {
1281
- const firstCellId = cells[0].id;
1282
- let cell = cells[0];
1283
- // Starts empty
1284
- expect(cell.config).toEqual({
1285
- disabled: false,
1286
- hide_code: false,
1287
- column: null,
1288
- expand_output: false,
1289
- });
1290
-
1291
- actions.updateCellConfig({
1292
- cellId: firstCellId,
1293
- config: { disabled: true },
1294
- });
1295
- cell = cells[0];
1296
- expect(cell.config.disabled).toBe(true);
1297
-
1298
- // Revert
1299
- actions.updateCellConfig({
1300
- cellId: firstCellId,
1301
- config: { disabled: false },
1302
- });
1303
- cell = cells[0];
1304
- expect(cell.config.disabled).toBe(false);
1305
- });
1306
-
1307
602
  it("can run a stopped cell", () => {
1308
603
  // Update code of first cell
1309
- actions.updateCellCode({
1310
- cellId: firstCellId,
1311
- code: "mo.md('This has an ancestor that was stopped')",
1312
- formattingChange: false,
1313
- });
604
+ editCode(firstCellId, "mo.md('This has an ancestor that was stopped')");
1314
605
 
1315
606
  // Prepare for run
1316
607
  actions.prepareForRun({
@@ -1327,13 +618,10 @@ describe("cell reducer", () => {
1327
618
  timestamp: new Date(10).getTime() as Seconds,
1328
619
  });
1329
620
  let cell = cells[0];
1330
- expect(cell.status).toBe("queued");
1331
621
  expect(cell.lastCodeRun).toBe(
1332
622
  "mo.md('This has an ancestor that was stopped')",
1333
623
  );
1334
624
  expect(cell.edited).toBe(false);
1335
- expect(cell.runElapsedTimeMs).toBe(null);
1336
- expect(cell.runStartTimestamp).toBe(null);
1337
625
  expect(cell).toMatchSnapshot("queued message received");
1338
626
 
1339
627
  // Receive idle message
@@ -1369,12 +657,10 @@ describe("cell reducer", () => {
1369
657
  timestamp: new Date(20).getTime() as Seconds,
1370
658
  });
1371
659
  cell = cells[0];
1372
- expect(cell.status).toBe("idle");
1373
660
  // oxlint-disable-next-line typescript/no-explicit-any no-unsafe-optional-chaining
1374
661
  expect((cell.output?.data as any)[0].msg).toBe(
1375
662
  "This cell wasn't run because an ancestor was stopped with `mo.stop`: ",
1376
663
  );
1377
- expect(cell.stopped).toBe(true);
1378
664
  expect(cell).toMatchSnapshot("ancestor stopped");
1379
665
 
1380
666
  // Receive queued message
@@ -1387,8 +673,6 @@ describe("cell reducer", () => {
1387
673
  timestamp: new Date(30).getTime() as Seconds,
1388
674
  });
1389
675
  cell = cells[0];
1390
- expect(cell.status).toBe("queued");
1391
- expect(cell.stopped).toBe(true);
1392
676
  expect(cell).toMatchSnapshot("queued after stop");
1393
677
 
1394
678
  // Receive running message
@@ -1401,8 +685,6 @@ describe("cell reducer", () => {
1401
685
  timestamp: new Date(40).getTime() as Seconds,
1402
686
  });
1403
687
  cell = cells[0];
1404
- expect(cell.status).toBe("running");
1405
- expect(cell.stopped).toBe(false);
1406
688
  expect(cell.output).toBe(null);
1407
689
  expect(cell).toMatchSnapshot("running after stop");
1408
690
  });
@@ -1429,7 +711,6 @@ describe("cell reducer", () => {
1429
711
 
1430
712
  // Initial state
1431
713
  let cell = cells[0];
1432
- expect(cell.status).toBe("idle");
1433
714
  expect(cell.consoleOutputs).toEqual([]);
1434
715
  expect(cell).toMatchSnapshot("initial state");
1435
716
 
@@ -1438,7 +719,6 @@ describe("cell reducer", () => {
1438
719
  cellId: firstCellId,
1439
720
  });
1440
721
  cell = cells[0];
1441
- expect(cell.status).toBe("queued");
1442
722
  expect(cell.consoleOutputs).toEqual([]);
1443
723
  expect(cell).toMatchSnapshot("prepared for run");
1444
724
 
@@ -1452,7 +732,6 @@ describe("cell reducer", () => {
1452
732
  timestamp: new Date(10).getTime() as Seconds,
1453
733
  });
1454
734
  cell = cells[0];
1455
- expect(cell.status).toBe("queued");
1456
735
  expect(cell).toMatchSnapshot("queued message received");
1457
736
 
1458
737
  // Receive running messages
@@ -1465,7 +744,6 @@ describe("cell reducer", () => {
1465
744
  timestamp: new Date(20).getTime() as Seconds,
1466
745
  });
1467
746
  cell = cells[0];
1468
- expect(cell.status).toBe("running");
1469
747
  expect(cell).toMatchSnapshot("running message received");
1470
748
 
1471
749
  // Add console
@@ -1478,12 +756,10 @@ describe("cell reducer", () => {
1478
756
  timestamp: new Date(22).getTime() as Seconds,
1479
757
  });
1480
758
  cell = cells[0];
1481
- expect(cell.status).toBe("running");
1482
759
  expect(cell).toMatchSnapshot("stdout received");
1483
760
 
1484
761
  cell = cells[0];
1485
762
  expect(cell.consoleOutputs[0]).toMatchObject(STDOUT);
1486
- expect(cell.status).toBe("running");
1487
763
  expect(cell).toMatchSnapshot("stdout verified");
1488
764
 
1489
765
  // Ask via stdin
@@ -1498,7 +774,6 @@ describe("cell reducer", () => {
1498
774
  cell = cells[0];
1499
775
  expect(cell.consoleOutputs[0]).toMatchObject(STDOUT);
1500
776
  expect(cell.consoleOutputs[1]).toMatchObject(STD_IN_1);
1501
- expect(cell.status).toBe("running");
1502
777
  expect(cell).toMatchSnapshot("stdin requested");
1503
778
 
1504
779
  // Response to stdin
@@ -1512,7 +787,6 @@ describe("cell reducer", () => {
1512
787
  ...STD_IN_1,
1513
788
  response: "Marimo!",
1514
789
  });
1515
- expect(cell.status).toBe("running");
1516
790
  expect(cell).toMatchSnapshot("stdin answered");
1517
791
 
1518
792
  // Ask via stdin, again
@@ -1530,7 +804,6 @@ describe("cell reducer", () => {
1530
804
  { ...STD_IN_1, response: "Marimo!" },
1531
805
  STD_IN_2,
1532
806
  ]);
1533
- expect(cell.status).toBe("running");
1534
807
  expect(cell).toMatchSnapshot("second stdin requested");
1535
808
 
1536
809
  // Interrupt, so we respond with ""
@@ -1548,7 +821,6 @@ describe("cell reducer", () => {
1548
821
  timestamp: new Date(61).getTime() as Seconds,
1549
822
  });
1550
823
  cell = cells[0];
1551
- expect(cell.status).toBe("idle");
1552
824
  expect(cell.consoleOutputs).toMatchObject([
1553
825
  STDOUT,
1554
826
  { ...STD_IN_1, response: "Marimo!" },
@@ -1579,7 +851,9 @@ describe("cell reducer", () => {
1579
851
  response: secret,
1580
852
  });
1581
853
 
1582
- expect(cells[0].consoleOutputs).toEqual([{ ...prompt, response: "" }]);
854
+ expect(cells[0].consoleOutputs).toEqual([
855
+ { ...prompt, id: 1, response: "" },
856
+ ]);
1583
857
  expect(JSON.stringify(cells[0].consoleOutputs)).not.toContain(secret);
1584
858
  });
1585
859
 
@@ -1640,7 +914,6 @@ describe("cell reducer", () => {
1640
914
 
1641
915
  // Initial state
1642
916
  let cell = cells[0];
1643
- expect(cell.status).toBe("idle");
1644
917
  expect(cell.consoleOutputs).toEqual([]);
1645
918
  expect(cell).toMatchSnapshot("initial state");
1646
919
 
@@ -1659,7 +932,6 @@ describe("cell reducer", () => {
1659
932
  cellId: firstCellId,
1660
933
  });
1661
934
  cell = cells[0];
1662
- expect(cell.status).toBe("queued");
1663
935
  expect(cell.consoleOutputs).toMatchObject([OLD_STDOUT]); // Old stays there until it starts running
1664
936
  expect(cell).toMatchSnapshot("prepared for run");
1665
937
 
@@ -1673,7 +945,6 @@ describe("cell reducer", () => {
1673
945
  timestamp: new Date(10).getTime() as Seconds,
1674
946
  });
1675
947
  cell = cells[0];
1676
- expect(cell.status).toBe("queued");
1677
948
  expect(cell.consoleOutputs).toMatchObject([OLD_STDOUT]); // Old stays there until it starts running
1678
949
  expect(cell).toMatchSnapshot("queued message received");
1679
950
 
@@ -1687,620 +958,48 @@ describe("cell reducer", () => {
1687
958
  timestamp: new Date(20).getTime() as Seconds,
1688
959
  });
1689
960
  cell = cells[0];
1690
- expect(cell.status).toBe("running");
1691
961
  expect(cell.consoleOutputs).toMatchObject([]);
1692
962
  expect(cell).toMatchSnapshot("console cleared while running");
1693
963
 
1694
- // Add console
1695
- actions.handleCellMessage({
1696
- cell_id: firstCellId,
1697
- output: undefined,
1698
- console: [STDOUT],
1699
- status: "idle",
1700
- stale_inputs: null,
1701
- timestamp: new Date(22).getTime() as Seconds,
1702
- });
1703
- cell = cells[0];
1704
- expect(cell.consoleOutputs).toMatchObject([STDOUT]);
1705
- expect(cell.status).toBe("idle");
1706
- expect(cell).toMatchSnapshot("console received while idle");
1707
- });
1708
-
1709
- it("can send a cell to the top", () => {
1710
- actions.createNewCell({ cellId: firstCellId, before: false });
1711
- actions.createNewCell({ cellId: cellId("1"), before: false });
1712
- actions.sendToTop({ cellId: cellId("2") });
1713
- expect(formatCells(state)).toMatchInlineSnapshot(`
1714
- "
1715
- [2] ''
1716
-
1717
- [0] ''
1718
-
1719
- [1] ''
1720
- "
1721
- `);
1722
- });
1723
-
1724
- it("can send a cell to the bottom", () => {
1725
- actions.createNewCell({ cellId: firstCellId, before: false });
1726
- actions.createNewCell({ cellId: cellId("1"), before: false });
1727
- actions.sendToBottom({ cellId: firstCellId });
1728
- expect(formatCells(state)).toMatchInlineSnapshot(`
1729
- "
1730
- [1] ''
1731
-
1732
- [2] ''
1733
-
1734
- [0] ''
1735
- "
1736
- `);
1737
- });
1738
-
1739
- it("can focus cells", () => {
1740
- actions.createNewCell({ cellId: firstCellId, before: false });
1741
- actions.createNewCell({ cellId: cellId("1"), before: false });
1742
-
1743
- actions.focusCell({ cellId: cellId("1"), where: "before" });
1744
- expect(focusAndScrollCellIntoView).toHaveBeenCalledWith(
1745
- expect.objectContaining({
1746
- cellId: cellId("0"),
1747
- }),
1748
- );
1749
-
1750
- actions.focusTopCell();
1751
- expect(scrollToTop).toHaveBeenCalled();
1752
-
1753
- actions.focusBottomCell();
1754
- expect(scrollToBottom).toHaveBeenCalled();
1755
- });
1756
-
1757
- it("can update cell name", () => {
1758
- actions.updateCellName({ cellId: firstCellId, name: "Test Cell" });
1759
- expect(state.cellData[firstCellId].name).toBe("Test Cell");
1760
- });
1761
-
1762
- it("can set cell IDs and codes", () => {
1763
- const newIds = [cellId("3"), cellId("4"), cellId("5")];
1764
- const newCodes = ["code1", "code2", "code3"];
1765
-
1766
- actions.setCellIds({ cellIds: newIds });
1767
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual(newIds);
1768
-
1769
- // When codeIsStale is false, lastCodeRun should match code
1770
- actions.setCellCodes({
1771
- codes: newCodes,
1772
- ids: newIds,
1773
- codeIsStale: false,
1774
- });
1775
- newIds.forEach((id, index) => {
1776
- expect(state.cellData[id].code).toBe(newCodes[index]);
1777
- expect(state.cellData[id].lastCodeRun).toBe(newCodes[index]);
1778
- expect(state.cellData[id].edited).toBe(false);
1779
- });
1780
-
1781
- // When codeIsStale is true, lastCodeRun should not change
1782
- const staleCodes = ["stale1", "stale2", "stale3"];
1783
- actions.setCellCodes({
1784
- codes: staleCodes,
1785
- ids: newIds,
1786
- codeIsStale: true,
1787
- });
1788
- newIds.forEach((id, index) => {
1789
- expect(state.cellData[id].code).toBe(staleCodes[index]);
1790
- expect(state.cellData[id].lastCodeRun).toBe(newCodes[index]);
1791
- expect(state.cellData[id].edited).toBe(true);
1792
- });
1793
- });
1794
-
1795
- it("can can add a new cell with/without stale code", () => {
1796
- actions.setCellCodes({
1797
- codes: ["new code"],
1798
- ids: [cellId("2")],
1799
- codeIsStale: false,
1800
- });
1801
-
1802
- expect(state.cellData[cellId("2")].code).toBe("new code");
1803
- expect(state.cellData[cellId("2")].edited).toBe(false);
1804
- expect(state.cellData[cellId("2")].lastCodeRun).toBe("new code");
1805
-
1806
- actions.setCellCodes({
1807
- codes: ["new code 2"],
1808
- ids: [cellId("9")],
1809
- codeIsStale: true,
1810
- });
1811
-
1812
- expect(state.cellData[cellId("9")].code).toBe("new code 2");
1813
- expect(state.cellData[cellId("9")].edited).toBe(true);
1814
- expect(state.cellData[cellId("9")].lastCodeRun).toBe(null);
1815
- });
1816
-
1817
- it("can partial update cell codes", () => {
1818
- actions.createNewCell({ cellId: firstCellId, before: false });
1819
- actions.createNewCell({ cellId: cellId("1"), before: false });
1820
-
1821
- expect(state.cellIds.inOrderIds).toEqual([
1822
- cellId("0"),
1823
- cellId("1"),
1824
- cellId("2"),
1825
- ]);
1826
- expect(state.cellData[cellId("0")].code).toBe("");
1827
- expect(state.cellData[cellId("1")].code).toBe("");
1828
- expect(state.cellData[cellId("2")].code).toBe("");
1829
-
1830
- // Update cell 1
1831
- actions.setCellCodes({
1832
- codes: ["new code 2"],
1833
- ids: [cellId("1")],
1834
- codeIsStale: false,
1835
- });
1836
-
1837
- expect(state.cellIds.inOrderIds).toEqual([
1838
- cellId("0"),
1839
- cellId("1"),
1840
- cellId("2"),
1841
- ]);
1842
- expect(state.cellData[cellId("0")].code).toBe("");
1843
- expect(state.cellData[cellId("1")].code).toBe("new code 2");
1844
- expect(state.cellData[cellId("1")].edited).toBe(false);
1845
- expect(state.cellData[cellId("2")].code).toBe("");
1846
- });
1847
-
1848
- it("can set cell codes with new cell ids, while preserving the old cell data", () => {
1849
- actions.setCellCodes({
1850
- codes: ["code1", "code2", "code3"],
1851
- ids: [cellId("3"), cellId("4"), cellId("5")],
1852
- codeIsStale: false,
1853
- });
1854
- expect(state.cellData[cellId("3")].code).toBe("code1");
1855
- expect(state.cellData[cellId("4")].code).toBe("code2");
1856
- expect(state.cellData[cellId("5")].code).toBe("code3");
1857
-
1858
- // Update with some new cell ids and some old cell ids
1859
- actions.setCellIds({
1860
- cellIds: [cellId("1"), cellId("2"), cellId("3"), cellId("4")],
1861
- });
1862
- actions.setCellCodes({
1863
- codes: ["new1", "new2", "code1", "code2"],
1864
- ids: [cellId("1"), cellId("2"), cellId("3"), cellId("4")],
1865
- codeIsStale: false,
1866
- });
1867
- expect(state.cellData[cellId("1")].code).toBe("new1");
1868
- expect(state.cellData[cellId("2")].code).toBe("new2");
1869
- expect(state.cellData[cellId("3")].code).toBe("code1");
1870
- expect(state.cellData[cellId("4")].code).toBe("code2");
1871
- expect(state.cellIds.inOrderIds).toEqual([
1872
- cellId("1"),
1873
- cellId("2"),
1874
- cellId("3"),
1875
- cellId("4"),
1876
- ]);
1877
- // Cell 5 data is preserved (possibly used for tracing), but it's not in the cellIds
1878
- expect(state.cellData[cellId("5")]).not.toBeUndefined();
1879
- });
1880
-
1881
- it("can set cell codes with names and configs", () => {
1882
- const newIds = [cellId("3"), cellId("4")];
1883
- actions.setCellIds({ cellIds: newIds });
1884
- actions.setCellCodes({
1885
- codes: ["code1", "code2"],
1886
- ids: newIds,
1887
- codeIsStale: false,
1888
- names: ["setup_cell", "analysis"],
1889
- configs: [
1890
- { hide_code: true, disabled: false, column: null },
1891
- { hide_code: false, disabled: true, column: null },
1892
- ],
1893
- });
1894
-
1895
- expect(state.cellData[cellId("3")].name).toBe("setup_cell");
1896
- expect(state.cellData[cellId("3")].config.hide_code).toBe(true);
1897
- expect(state.cellData[cellId("3")].config.disabled).toBe(false);
1898
-
1899
- expect(state.cellData[cellId("4")].name).toBe("analysis");
1900
- expect(state.cellData[cellId("4")].config.hide_code).toBe(false);
1901
- expect(state.cellData[cellId("4")].config.disabled).toBe(true);
1902
- });
1903
-
1904
- it("can set cell codes without names/configs (backward compat)", () => {
1905
- const newIds = [cellId("3")];
1906
- actions.setCellIds({ cellIds: newIds });
1907
- actions.setCellCodes({
1908
- codes: ["code1"],
1909
- ids: newIds,
1910
- codeIsStale: false,
1911
- });
1912
-
1913
- // Should use defaults when names/configs not provided
1914
- expect(state.cellData[cellId("3")].code).toBe("code1");
1915
- expect(state.cellData[cellId("3")].config.hide_code).toBe(false);
1916
- expect(state.cellData[cellId("3")].config.disabled).toBe(false);
1917
- });
1918
-
1919
- it("can update names and configs on existing cells via setCellCodes", () => {
1920
- // Set initial state
1921
- actions.setCellCodes({
1922
- codes: ["x = 1"],
1923
- ids: [firstCellId],
1924
- codeIsStale: false,
1925
- names: ["old_name"],
1926
- configs: [{ hide_code: false, disabled: false, column: null }],
1927
- });
1928
- expect(state.cellData[firstCellId].name).toBe("old_name");
1929
- expect(state.cellData[firstCellId].config.hide_code).toBe(false);
1930
-
1931
- // Update with new name and config (same code)
1932
- actions.setCellCodes({
1933
- codes: ["x = 1"],
1934
- ids: [firstCellId],
1935
- codeIsStale: true,
1936
- names: ["new_name"],
1937
- configs: [{ hide_code: true, disabled: false, column: null }],
1938
- });
1939
- expect(state.cellData[firstCellId].name).toBe("new_name");
1940
- expect(state.cellData[firstCellId].config.hide_code).toBe(true);
1941
- });
1942
-
1943
- it("can fold and unfold all cells", () => {
1944
- actions.foldAll();
1945
- expect(foldAllBulk).toHaveBeenCalled();
1946
-
1947
- actions.unfoldAll();
1948
- expect(unfoldAllBulk).toHaveBeenCalled();
1949
- });
1950
-
1951
- it("can clear logs", () => {
1952
- state.cellLogs = [
1953
- {
1954
- level: "stderr",
1955
- message: "log1",
1956
- timestamp: 0,
1957
- source: { type: "cell", cellId: firstCellId },
1958
- },
1959
- {
1960
- level: "stderr",
1961
- message: "log1",
1962
- timestamp: 0,
1963
- source: { type: "cell", cellId: firstCellId },
1964
- },
1965
- ];
1966
- actions.clearLogs();
1967
- expect(state.cellLogs).toEqual([]);
1968
- });
1969
-
1970
- it("can collapse and expand cells", () => {
1971
- actions.createNewCell({ cellId: firstCellId, before: false });
1972
- actions.createNewCell({
1973
- cellId: cellId("1"),
1974
- before: false,
1975
- code: "# Header",
1976
- });
1977
- actions.createNewCell({
1978
- cellId: cellId("2"),
1979
- before: false,
1980
- code: "## Subheader",
1981
- });
1982
-
1983
- const id = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
1984
- state.cellRuntime[id] = {
1985
- ...state.cellRuntime[id],
1986
- outline: {
1987
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
1988
- },
1989
- };
1990
-
1991
- actions.collapseCell({ cellId: id });
1992
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(id)).toBe(true);
1993
-
1994
- actions.expandCell({ cellId: id });
1995
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(id)).toBe(false);
1996
- });
1997
-
1998
- it("can collapse and expand all cells in multiple columns", () => {
1999
- actions.createNewCell({ cellId: firstCellId, before: false });
2000
- actions.createNewCell({
2001
- cellId: cellId("1"),
2002
- before: false,
2003
- code: "# First Column Header",
2004
- });
2005
- actions.createNewCell({
2006
- cellId: cellId("2"),
2007
- before: false,
2008
- code: "## First Column Subheader",
2009
- });
2010
-
2011
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2012
-
2013
- actions.createNewCell({
2014
- cellId: cellId("3"),
2015
- before: false,
2016
- code: "# Second Column Header",
2017
- });
2018
- actions.createNewCell({
2019
- cellId: cellId("4"),
2020
- before: false,
2021
- code: "## Second Column Subheader",
2022
- });
2023
-
2024
- const firstColumnHeaderId = state.cellIds
2025
- .atOrThrow(FIRST_COLUMN)
2026
- .atOrThrow(1);
2027
- state.cellRuntime[firstColumnHeaderId] = {
2028
- ...state.cellRuntime[firstColumnHeaderId],
2029
- outline: {
2030
- items: [
2031
- { name: "First Column Header", level: 1, by: { id: "header" } },
2032
- ],
2033
- },
2034
- };
2035
-
2036
- const secondColumnHeaderId = state.cellIds
2037
- .atOrThrow(SECOND_COLUMN)
2038
- .atOrThrow(0);
2039
- state.cellRuntime[secondColumnHeaderId] = {
2040
- ...state.cellRuntime[secondColumnHeaderId],
2041
- outline: {
2042
- items: [
2043
- { name: "Second Column Header", level: 1, by: { id: "header" } },
2044
- ],
2045
- },
2046
- };
2047
-
2048
- actions.collapseAllCells();
2049
- expect(
2050
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstColumnHeaderId),
2051
- ).toBe(true);
2052
- expect(
2053
- state.cellIds.atOrThrow(SECOND_COLUMN).isCollapsed(secondColumnHeaderId),
2054
- ).toBe(true);
2055
-
2056
- actions.expandAllCells();
2057
- expect(
2058
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstColumnHeaderId),
2059
- ).toBe(false);
2060
- expect(
2061
- state.cellIds.atOrThrow(SECOND_COLUMN).isCollapsed(secondColumnHeaderId),
2062
- ).toBe(false);
2063
- });
2064
-
2065
- it("can collapse and expand nested cells in one call", () => {
2066
- actions.createNewCell({ cellId: firstCellId, before: false });
2067
- actions.createNewCell({
2068
- cellId: cellId("1"),
2069
- before: false,
2070
- code: "# Header",
2071
- });
2072
- actions.createNewCell({
2073
- cellId: cellId("2"),
2074
- before: false,
2075
- code: "## Subheader",
2076
- });
2077
- actions.createNewCell({
2078
- cellId: cellId("3"),
2079
- before: false,
2080
- code: "### Subsubheader",
2081
- });
2082
-
2083
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2084
- state.cellRuntime[headerId] = {
2085
- ...state.cellRuntime[headerId],
2086
- outline: {
2087
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2088
- },
2089
- };
2090
-
2091
- const subheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2092
- state.cellRuntime[subheaderId] = {
2093
- ...state.cellRuntime[subheaderId],
2094
- outline: {
2095
- items: [{ name: "Subheader", level: 2, by: { id: "subheader" } }],
2096
- },
2097
- };
2098
-
2099
- // Check if both the parent and child are collapsed
2100
- actions.collapseAllCells();
2101
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2102
- true,
2103
- );
2104
- actions.expandCell({ cellId: headerId });
2105
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2106
- true,
2107
- );
2108
- actions.collapseCell({ cellId: headerId });
2109
-
2110
- // Check if both the parent and child are expanded
2111
- actions.expandAllCells();
2112
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2113
- false,
2114
- );
2115
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2116
- false,
2117
- );
2118
- });
2119
-
2120
- it("can collapse all cells when nested heading ranges end at the same cell", () => {
2121
- actions.createNewCell({ cellId: firstCellId, before: false });
2122
- actions.createNewCell({
2123
- cellId: cellId("1"),
2124
- before: false,
2125
- code: "# Header",
2126
- });
2127
- actions.createNewCell({
2128
- cellId: cellId("2"),
2129
- before: false,
2130
- code: "## Subheader",
2131
- });
2132
- actions.createNewCell({
2133
- cellId: cellId("3"),
2134
- before: false,
2135
- code: "### Subsubheader",
2136
- });
2137
-
2138
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2139
- state.cellRuntime[headerId] = {
2140
- ...state.cellRuntime[headerId],
2141
- outline: {
2142
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2143
- },
2144
- };
2145
-
2146
- const subheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2147
- state.cellRuntime[subheaderId] = {
2148
- ...state.cellRuntime[subheaderId],
2149
- outline: {
2150
- items: [{ name: "Subheader", level: 2, by: { id: "subheader" } }],
2151
- },
2152
- };
2153
-
2154
- const subsubheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2155
- state.cellRuntime[subsubheaderId] = {
2156
- ...state.cellRuntime[subsubheaderId],
2157
- outline: {
2158
- items: [{ name: "Subsubheader", level: 3, by: { id: "subsubheader" } }],
2159
- },
2160
- };
2161
-
2162
- // The header, subheader and subsubheader ranges all end at the last
2163
- // cell. Collapsing all used to throw "Node ... not found in tree" and
2164
- // leave the whole notebook unchanged.
2165
- actions.collapseAllCells();
2166
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2167
- true,
2168
- );
2169
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2170
- firstCellId,
2171
- headerId,
2172
- ]);
2173
-
2174
- // Each level is nested under its parent, collapsed
2175
- actions.expandCell({ cellId: headerId });
2176
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2177
- true,
2178
- );
2179
- actions.expandCell({ cellId: subheaderId });
2180
- expect(
2181
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsubheaderId),
2182
- ).toBe(true);
2183
- });
2184
-
2185
- it("can collapse all cells when a deep chain is followed by a later section", () => {
2186
- actions.createNewCell({ cellId: firstCellId, before: false });
2187
- actions.createNewCell({
2188
- cellId: cellId("1"),
2189
- before: false,
2190
- code: "# Header",
2191
- });
2192
- actions.createNewCell({
2193
- cellId: cellId("2"),
2194
- before: false,
2195
- code: "## Section A",
2196
- });
2197
- actions.createNewCell({
2198
- cellId: cellId("3"),
2199
- before: false,
2200
- code: "### Sub of A",
2201
- });
2202
- actions.createNewCell({
2203
- cellId: cellId("4"),
2204
- before: false,
2205
- code: "## Section B",
2206
- });
2207
-
2208
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2209
- state.cellRuntime[headerId] = {
2210
- ...state.cellRuntime[headerId],
2211
- outline: {
2212
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2213
- },
2214
- };
2215
-
2216
- const sectionAId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2217
- state.cellRuntime[sectionAId] = {
2218
- ...state.cellRuntime[sectionAId],
2219
- outline: {
2220
- items: [{ name: "Section A", level: 2, by: { id: "section-a" } }],
2221
- },
2222
- };
2223
-
2224
- const subsectionId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2225
- state.cellRuntime[subsectionId] = {
2226
- ...state.cellRuntime[subsectionId],
2227
- outline: {
2228
- items: [{ name: "Sub of A", level: 3, by: { id: "sub-of-a" } }],
2229
- },
2230
- };
2231
-
2232
- const sectionBId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(4);
2233
- state.cellRuntime[sectionBId] = {
2234
- ...state.cellRuntime[sectionBId],
2235
- outline: {
2236
- items: [{ name: "Section B", level: 2, by: { id: "section-b" } }],
2237
- },
2238
- };
2239
-
2240
- actions.collapseAllCells();
2241
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2242
- true,
2243
- );
2244
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2245
- firstCellId,
2246
- headerId,
2247
- ]);
2248
-
2249
- // Both sections sit under the header, collapsed, with the subsection
2250
- // nested under section A
2251
- actions.expandCell({ cellId: headerId });
2252
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionAId)).toBe(
2253
- true,
2254
- );
2255
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionBId)).toBe(
2256
- true,
2257
- );
2258
- actions.expandCell({ cellId: sectionAId });
2259
- expect(
2260
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsectionId),
2261
- ).toBe(true);
964
+ // Add console
965
+ actions.handleCellMessage({
966
+ cell_id: firstCellId,
967
+ output: undefined,
968
+ console: [STDOUT],
969
+ status: "idle",
970
+ stale_inputs: null,
971
+ timestamp: new Date(22).getTime() as Seconds,
972
+ });
973
+ cell = cells[0];
974
+ expect(cell.consoleOutputs).toMatchObject([STDOUT]);
975
+ expect(cell).toMatchSnapshot("console received while idle");
2262
976
  });
2263
977
 
2264
- it("can show hidden cells", () => {
2265
- actions.createNewCell({ cellId: firstCellId, before: false });
2266
- actions.createNewCell({ cellId: cellId("1"), before: false });
2267
- actions.collapseCell({ cellId: firstCellId });
978
+ it("can fold and unfold all cells", () => {
979
+ actions.foldAll();
980
+ expect(foldAllBulk).toHaveBeenCalled();
2268
981
 
2269
- actions.showCellIfHidden({ cellId: cellId("1") });
2270
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstCellId)).toBe(
2271
- false,
2272
- );
982
+ actions.unfoldAll();
983
+ expect(unfoldAllBulk).toHaveBeenCalled();
2273
984
  });
2274
985
 
2275
- it("can split and undo split cells", () => {
2276
- actions.createNewCell({
2277
- cellId: firstCellId,
2278
- before: false,
2279
- code: "line1\nline2",
2280
- });
2281
- const nextCellId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2282
-
2283
- const originalCellCount = state.cellIds.atOrThrow(FIRST_COLUMN).length;
2284
- // Move cursor to the second line
2285
- const editor = state.cellHandles[nextCellId].current?.editorView;
2286
- if (!editor) {
2287
- throw new Error("Editor not found");
2288
- }
2289
- editor.dispatch({ selection: { anchor: 5, head: 5 } });
2290
- actions.splitCell({ cellId: nextCellId });
2291
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).length).toBe(
2292
- originalCellCount + 1,
2293
- );
2294
- expect(state.cellData[nextCellId].code).toBe("line1");
2295
- expect(
2296
- state.cellData[state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2)].code,
2297
- ).toBe("line2");
2298
-
2299
- actions.undoSplitCell({ cellId: nextCellId, snapshot: "line1\nline2" });
2300
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).length).toBe(
2301
- originalCellCount,
2302
- );
2303
- expect(state.cellData[nextCellId].code).toBe("line1\nline2");
986
+ it("can clear logs", () => {
987
+ state.cellLogs = [
988
+ {
989
+ level: "stderr",
990
+ message: "log1",
991
+ timestamp: 0,
992
+ source: { type: "cell", cellId: firstCellId },
993
+ },
994
+ {
995
+ level: "stderr",
996
+ message: "log1",
997
+ timestamp: 0,
998
+ source: { type: "cell", cellId: firstCellId },
999
+ },
1000
+ ];
1001
+ actions.clearLogs();
1002
+ expect(state.cellLogs).toEqual([]);
2304
1003
  });
2305
1004
 
2306
1005
  it("can handle multiple console outputs", () => {
@@ -2339,407 +1038,6 @@ describe("cell reducer", () => {
2339
1038
  `);
2340
1039
  });
2341
1040
 
2342
- it("can add a column breakpoint", () => {
2343
- actions.createNewCell({ cellId: firstCellId, before: false });
2344
- actions.createNewCell({ cellId: cellId("1"), before: false });
2345
- actions.createNewCell({ cellId: cellId("2"), before: false });
2346
-
2347
- expect(state.cellIds.getColumns().length).toBe(1);
2348
- expect(formatCells(state)).toMatchInlineSnapshot(`
2349
- "
2350
- [0] ''
2351
-
2352
- [1] ''
2353
-
2354
- [2] ''
2355
-
2356
- [3] ''
2357
- "
2358
- `);
2359
-
2360
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2361
-
2362
- expect(state.cellIds.getColumns().length).toBe(2);
2363
- expect(formatCells(state)).toMatchInlineSnapshot(`
2364
- "
2365
- > col 0
2366
- [0] ''
2367
-
2368
- [1] ''
2369
-
2370
- > col 1
2371
- [2] ''
2372
-
2373
- [3] ''
2374
- "
2375
- `);
2376
-
2377
- // Check that the cells are in the correct columns
2378
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2379
- cellId("0"),
2380
- cellId("1"),
2381
- ]);
2382
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2383
- cellId("2"),
2384
- cellId("3"),
2385
- ]);
2386
- });
2387
-
2388
- it("cannot add a column breakpoint before the first cell", () => {
2389
- expect(state.cellIds.getColumns().length).toBe(1);
2390
- actions.createNewCell({ cellId: firstCellId, before: false });
2391
- actions.createNewCell({ cellId: cellId("1"), before: false });
2392
- actions.addColumnBreakpoint({ cellId: firstCellId });
2393
- expect(state.cellIds.getColumns().length).toBe(1);
2394
- });
2395
-
2396
- it("can delete a column", () => {
2397
- actions.createNewCell({ cellId: firstCellId, before: false });
2398
- actions.createNewCell({ cellId: cellId("1"), before: false });
2399
- actions.createNewCell({ cellId: cellId("2"), before: false });
2400
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2401
-
2402
- expect(state.cellIds.getColumns().length).toBe(2);
2403
- expect(formatCells(state)).toMatchInlineSnapshot(`
2404
- "
2405
- > col 0
2406
- [0] ''
2407
-
2408
- [1] ''
2409
-
2410
- > col 1
2411
- [2] ''
2412
-
2413
- [3] ''
2414
- "
2415
- `);
2416
-
2417
- const columnId = state.cellIds.atOrThrow(0).id;
2418
- actions.deleteColumn({ columnId: columnId });
2419
-
2420
- expect(state.cellIds.getColumns().length).toBe(1);
2421
- expect(formatCells(state)).toMatchInlineSnapshot(`
2422
- "
2423
- [2] ''
2424
-
2425
- [3] ''
2426
-
2427
- [0] ''
2428
-
2429
- [1] ''
2430
- "
2431
- `);
2432
-
2433
- // Check that all cells are now in the remaining column
2434
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2435
- "2",
2436
- "3",
2437
- "0",
2438
- "1",
2439
- ]);
2440
- });
2441
-
2442
- it("deleting the last column does nothing", () => {
2443
- actions.createNewCell({ cellId: firstCellId, before: false });
2444
- actions.createNewCell({ cellId: cellId("1"), before: false });
2445
-
2446
- const initialState = { ...state };
2447
-
2448
- actions.deleteColumn({ columnId: initialState.cellIds.atOrThrow(0).id });
2449
-
2450
- // State should not change
2451
- expect(state).toEqual(initialState);
2452
- });
2453
-
2454
- it("can drop a cell over another cell", () => {
2455
- actions.createNewCell({ cellId: firstCellId, before: false });
2456
- actions.createNewCell({ cellId: cellId("1"), before: false });
2457
- actions.createNewCell({ cellId: cellId("2"), before: false });
2458
-
2459
- expect(formatCells(state)).toMatchInlineSnapshot(`
2460
- "
2461
- [0] ''
2462
-
2463
- [1] ''
2464
-
2465
- [2] ''
2466
-
2467
- [3] ''
2468
- "
2469
- `);
2470
-
2471
- actions.dropCellOverCell({
2472
- cellId: cellId("0"),
2473
- overCellId: cellId("3"),
2474
- });
2475
-
2476
- expect(formatCells(state)).toMatchInlineSnapshot(`
2477
- "
2478
- [1] ''
2479
-
2480
- [2] ''
2481
-
2482
- [3] ''
2483
-
2484
- [0] ''
2485
- "
2486
- `);
2487
- });
2488
-
2489
- it("can drop a cell over a new column", () => {
2490
- actions.createNewCell({ cellId: firstCellId, before: false });
2491
- actions.createNewCell({ cellId: cellId("1"), before: false });
2492
-
2493
- expect(state.cellIds.getColumns().length).toBe(1);
2494
- expect(formatCells(state)).toMatchInlineSnapshot(`
2495
- "
2496
- [0] ''
2497
-
2498
- [1] ''
2499
-
2500
- [2] ''
2501
- "
2502
- `);
2503
-
2504
- actions.dropOverNewColumn({ cellId: cellId("1") });
2505
-
2506
- expect(state.cellIds.getColumns().length).toBe(2);
2507
- expect(formatCells(state)).toMatchInlineSnapshot(`
2508
- "
2509
- > col 0
2510
- [0] ''
2511
-
2512
- [2] ''
2513
-
2514
- > col 1
2515
- [1] ''
2516
- "
2517
- `);
2518
-
2519
- // Check that the cells are in the correct columns
2520
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2521
- cellId("0"),
2522
- cellId("2"),
2523
- ]);
2524
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2525
- });
2526
-
2527
- it("can drop a column over another column", () => {
2528
- actions.createNewCell({ cellId: firstCellId, before: false });
2529
- actions.createNewCell({ cellId: cellId("1"), before: false });
2530
- actions.createNewCell({ cellId: cellId("2"), before: false });
2531
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2532
-
2533
- expect(state.cellIds.getColumns().length).toBe(2);
2534
- expect(formatCells(state)).toMatchInlineSnapshot(`
2535
- "
2536
- > col 0
2537
- [0] ''
2538
-
2539
- [1] ''
2540
-
2541
- > col 1
2542
- [2] ''
2543
-
2544
- [3] ''
2545
- "
2546
- `);
2547
-
2548
- const columnId0 = state.cellIds.atOrThrow(0).id;
2549
- const columnId1 = state.cellIds.atOrThrow(1).id;
2550
- expect(columnId0).not.toBe(columnId1);
2551
- actions.moveColumn({ column: columnId1, overColumn: columnId0 });
2552
-
2553
- expect(state.cellIds.getColumns().length).toBe(2);
2554
- expect(formatCells(state)).toMatchInlineSnapshot(`
2555
- "
2556
- > col 0
2557
- [2] ''
2558
-
2559
- [3] ''
2560
-
2561
- > col 1
2562
- [0] ''
2563
-
2564
- [1] ''
2565
- "
2566
- `);
2567
-
2568
- // Check that the columns have swapped positions
2569
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2570
- cellId("2"),
2571
- cellId("3"),
2572
- ]);
2573
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2574
- cellId("0"),
2575
- cellId("1"),
2576
- ]);
2577
- });
2578
-
2579
- it("can compact columns", () => {
2580
- // Create initial state with 3 columns, including an empty one
2581
- actions.createNewCell({ cellId: firstCellId, before: false });
2582
- actions.createNewCell({ cellId: cellId("1"), before: false });
2583
- actions.addColumnBreakpoint({ cellId: cellId("1") });
2584
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2585
- actions.dropOverNewColumn({ cellId: cellId("2") });
2586
-
2587
- expect(state.cellIds.getColumns().length).toBe(4);
2588
- expect(formatCells(state)).toMatchInlineSnapshot(`
2589
- "
2590
- > col 0
2591
- [0] ''
2592
-
2593
- > col 1
2594
- [1] ''
2595
-
2596
- > col 2
2597
-
2598
-
2599
- > col 3
2600
- [2] ''
2601
- "
2602
- `);
2603
-
2604
- // Check initial column structure
2605
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2606
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2607
- expect(state.cellIds.getColumns()[2].topLevelIds).toEqual([]);
2608
- expect(state.cellIds.getColumns()[3].topLevelIds).toEqual([cellId("2")]);
2609
-
2610
- // Compact columns
2611
- actions.compactColumns();
2612
-
2613
- expect(state.cellIds.getColumns().length).toBe(3);
2614
- expect(formatCells(state)).toMatchInlineSnapshot(`
2615
- "
2616
- > col 0
2617
- [0] ''
2618
-
2619
- > col 1
2620
- [1] ''
2621
-
2622
- > col 2
2623
- [2] ''
2624
- "
2625
- `);
2626
-
2627
- // Check compacted column structure
2628
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2629
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2630
- expect(state.cellIds.getColumns()[2].topLevelIds).toEqual([cellId("2")]);
2631
- });
2632
-
2633
- it("rebuildCellColumns regroups cells by config.column", () => {
2634
- // Create four cells in a single column.
2635
- actions.createNewCell({ cellId: firstCellId, before: false });
2636
- actions.createNewCell({ cellId: cellId("1"), before: false });
2637
- actions.createNewCell({ cellId: cellId("2"), before: false });
2638
- expect(state.cellIds.getColumns().length).toBe(1);
2639
-
2640
- // Explicitly set config.column on each cell. This ONLY updates metadata —
2641
- // updateCellConfig does not touch the MultiColumn tree.
2642
- actions.updateCellConfig({
2643
- cellId: cellId("0"),
2644
- config: { column: 0 },
2645
- });
2646
- actions.updateCellConfig({
2647
- cellId: cellId("2"),
2648
- config: { column: 1 },
2649
- });
2650
-
2651
- // Tree is still a single column at this point.
2652
- expect(state.cellIds.getColumns().length).toBe(1);
2653
-
2654
- // Now rebuild the column tree from metadata.
2655
- actions.rebuildCellColumns({
2656
- cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
2657
- });
2658
-
2659
- expect(state.cellIds.getColumns().length).toBe(2);
2660
- // Cell 1 inherits from 0 (col 0), cell 3 inherits from 2 (col 1).
2661
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2662
- cellId("0"),
2663
- cellId("1"),
2664
- ]);
2665
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2666
- cellId("2"),
2667
- cellId("3"),
2668
- ]);
2669
- });
2670
-
2671
- it("rebuildCellColumns with explicit column on every cell", () => {
2672
- actions.createNewCell({ cellId: firstCellId, before: false });
2673
- actions.createNewCell({ cellId: cellId("1"), before: false });
2674
- actions.createNewCell({ cellId: cellId("2"), before: false });
2675
-
2676
- for (const [id, col] of [
2677
- ["0", 1],
2678
- ["1", 0],
2679
- ["2", 1],
2680
- ["3", 0],
2681
- ] as const) {
2682
- actions.updateCellConfig({ cellId: cellId(id), config: { column: col } });
2683
- }
2684
-
2685
- actions.rebuildCellColumns({
2686
- cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
2687
- });
2688
-
2689
- expect(state.cellIds.getColumns().length).toBe(2);
2690
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2691
- cellId("1"),
2692
- cellId("3"),
2693
- ]);
2694
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2695
- cellId("0"),
2696
- cellId("2"),
2697
- ]);
2698
- });
2699
-
2700
- it("rebuildCellColumns collapses to one column when all cells are col 0", () => {
2701
- actions.createNewCell({ cellId: firstCellId, before: false });
2702
- actions.addColumnBreakpoint({ cellId: cellId("1") });
2703
- expect(state.cellIds.getColumns().length).toBe(2);
2704
-
2705
- // Wipe column metadata back to 0 for both cells.
2706
- actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
2707
- actions.updateCellConfig({ cellId: cellId("1"), config: { column: 0 } });
2708
-
2709
- actions.rebuildCellColumns({
2710
- cellIds: [cellId("0"), cellId("1")],
2711
- });
2712
-
2713
- expect(state.cellIds.getColumns().length).toBe(1);
2714
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2715
- cellId("0"),
2716
- cellId("1"),
2717
- ]);
2718
- });
2719
-
2720
- it("rebuildCellColumns follows the provided order, not current tree order", () => {
2721
- // Start with a single column.
2722
- actions.createNewCell({ cellId: firstCellId, before: false });
2723
- actions.createNewCell({ cellId: cellId("1"), before: false });
2724
- actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
2725
- actions.updateCellConfig({ cellId: cellId("2"), config: { column: 1 } });
2726
-
2727
- // Pass the reversed order. The rebuild should honor it.
2728
- actions.rebuildCellColumns({
2729
- cellIds: [cellId("2"), cellId("1"), cellId("0")],
2730
- });
2731
-
2732
- // cellIds iteration:
2733
- // 2 → col=1, pushed to col1, prev=1
2734
- // 1 → col=null, pushed to col[prev=1] = col1
2735
- // 0 → col=0, pushed to col0
2736
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2737
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2738
- cellId("2"),
2739
- cellId("1"),
2740
- ]);
2741
- });
2742
-
2743
1041
  it("can clear output of a single cell", () => {
2744
1042
  // Set up initial state with output
2745
1043
  actions.handleCellMessage({
@@ -2828,8 +1126,8 @@ describe("cell reducer", () => {
2828
1126
 
2829
1127
  it("can clear output of all cells", () => {
2830
1128
  // Create multiple cells with output
2831
- actions.createNewCell({ cellId: firstCellId, before: false });
2832
- const secondCellId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
1129
+ seedCell({ cellId: firstCellId, before: false });
1130
+ const secondCellId = state.cellIds[1];
2833
1131
 
2834
1132
  // Add output to both cells
2835
1133
  const outputMessage = {
@@ -2861,135 +1159,23 @@ describe("cell reducer", () => {
2861
1159
 
2862
1160
  // Verify initial state has output
2863
1161
  expect(state.cellRuntime[firstCellId].output).not.toBeNull();
2864
- expect(state.cellRuntime[firstCellId].consoleOutputs.length).toBe(1);
2865
- expect(state.cellRuntime[secondCellId].output).not.toBeNull();
2866
- expect(state.cellRuntime[secondCellId].consoleOutputs.length).toBe(1);
2867
-
2868
- // Clear all outputs
2869
- actions.clearAllCellOutputs();
2870
-
2871
- // Verify all outputs are cleared
2872
- expect(state.cellRuntime[firstCellId].output).toBeNull();
2873
- expect(state.cellRuntime[firstCellId].consoleOutputs).toEqual([]);
2874
- expect(state.cellRuntime[secondCellId].output).toBeNull();
2875
- expect(state.cellRuntime[secondCellId].consoleOutputs).toEqual([]);
2876
- });
2877
-
2878
- it("skips creating new cell if code exists and skipIfCodeExists is true", () => {
2879
- // Add initial cell with code
2880
- actions.updateCellCode({
2881
- cellId: firstCellId,
2882
- code: "import numpy as np",
2883
- formattingChange: false,
2884
- });
2885
-
2886
- // Try to create new cell with same code and skipIfCodeExists
2887
- actions.createNewCell({
2888
- cellId: "__end__",
2889
- code: "import numpy as np",
2890
- before: false,
2891
- skipIfCodeExists: true,
2892
- });
2893
-
2894
- // Should still only have one cell
2895
- expect(state.cellIds.inOrderIds.length).toBe(1);
2896
- expect(formatCells(state)).toMatchInlineSnapshot(`
2897
- "
2898
- [0] 'import numpy as np'
2899
- "
2900
- `);
2901
-
2902
- // Verify we can still add cell with different code
2903
- actions.createNewCell({
2904
- cellId: "__end__",
2905
- code: "import pandas as pd",
2906
- before: false,
2907
- skipIfCodeExists: true,
2908
- });
2909
-
2910
- expect(state.cellIds.inOrderIds.length).toBe(2);
2911
- expect(formatCells(state)).toMatchInlineSnapshot(`
2912
- "
2913
- [0] 'import numpy as np'
2914
-
2915
- [1] 'import pandas as pd'
2916
- "
2917
- `);
2918
- });
2919
-
2920
- it("can create and noop-update a setup cell", () => {
2921
- // Create the setup cell
2922
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2923
-
2924
- // Check that setup cell was created
2925
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2926
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2927
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2928
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2929
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2930
-
2931
- // Update the setup cell
2932
- actions.addSetupCellIfDoesntExist({ code: "# Updated setup code" });
2933
-
2934
- // Check that the setup cell did not change, since it already exists
2935
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2936
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2937
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2938
- });
2939
-
2940
- it("can delete and undelete the setup cell", () => {
2941
- // Create the setup cell
2942
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2943
-
2944
- // Check that setup cell was created
2945
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2946
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2947
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2948
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2949
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2950
-
2951
- // Delete the setup cell
2952
- actions.deleteCell({ cellId: SETUP_CELL_ID });
2953
-
2954
- // Check that setup cell was deleted
2955
- expect(state.cellData[SETUP_CELL_ID]).toBeDefined(); // we keep old state
2956
- expect(state.cellIds.inOrderIds).not.toContain(SETUP_CELL_ID);
2957
-
2958
- // Undo delete the setup cell
2959
- actions.undoDeleteCell();
2960
-
2961
- // Check that setup cell was restored
2962
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2963
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2964
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2965
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2966
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2967
-
2968
- // Verify scrollKey is set to the restored setup cell
2969
- expect(state.scrollKey).toBe(SETUP_CELL_ID);
2970
- });
2971
-
2972
- it("can delete and then create a new setup cell", () => {
2973
- // Create the setup cell
2974
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2975
-
2976
- // Delete the setup cell
2977
- actions.deleteCell({ cellId: SETUP_CELL_ID });
1162
+ expect(state.cellRuntime[firstCellId].consoleOutputs.length).toBe(1);
1163
+ expect(state.cellRuntime[secondCellId].output).not.toBeNull();
1164
+ expect(state.cellRuntime[secondCellId].consoleOutputs.length).toBe(1);
2978
1165
 
2979
- // Create a new setup cell
2980
- actions.addSetupCellIfDoesntExist({ code: "# New code" });
1166
+ // Clear all outputs
1167
+ actions.clearAllCellOutputs();
2981
1168
 
2982
- // Check that the new setup cell was created
2983
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2984
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2985
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# New code");
2986
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2987
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
1169
+ // Verify all outputs are cleared
1170
+ expect(state.cellRuntime[firstCellId].output).toBeNull();
1171
+ expect(state.cellRuntime[firstCellId].consoleOutputs).toEqual([]);
1172
+ expect(state.cellRuntime[secondCellId].output).toBeNull();
1173
+ expect(state.cellRuntime[secondCellId].consoleOutputs).toEqual([]);
2988
1174
  });
2989
1175
 
2990
1176
  it("can clear all outputs", () => {
2991
1177
  // Add a cell and give it output
2992
- actions.createNewCell({
1178
+ seedCell({
2993
1179
  cellId: firstCellId,
2994
1180
  before: false,
2995
1181
  });
@@ -3017,522 +1203,13 @@ describe("cell reducer", () => {
3017
1203
  expect(state.cellRuntime[cell2Id].output).toBeNull();
3018
1204
  });
3019
1205
 
3020
- describe("moveToNextCell", () => {
3021
- let cell1Id: CellId;
3022
- let cell2Id: CellId;
3023
- let cell3Id: CellId;
3024
-
3025
- beforeEach(() => {
3026
- // Create a few cells to work with
3027
- actions.createNewCell({ cellId: "__end__", before: false });
3028
- actions.createNewCell({ cellId: "__end__", before: false });
3029
-
3030
- cell1Id = state.cellIds.inOrderIds[0];
3031
- cell2Id = state.cellIds.inOrderIds[1];
3032
- cell3Id = state.cellIds.inOrderIds[2];
3033
- });
3034
-
3035
- it("creates new cell when moving after last cell with noCreate=false", () => {
3036
- const initialCellCount = state.cellIds.inOrderIds.length;
3037
-
3038
- actions.moveToNextCell({
3039
- cellId: cell3Id,
3040
- before: false,
3041
- noCreate: false,
3042
- });
3043
-
3044
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3045
- });
3046
-
3047
- it("creates new cell when moving before first cell with noCreate=false", () => {
3048
- const initialCellCount = state.cellIds.inOrderIds.length;
3049
-
3050
- actions.moveToNextCell({
3051
- cellId: cell1Id,
3052
- before: true,
3053
- noCreate: false,
3054
- });
3055
-
3056
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3057
- });
3058
-
3059
- it("does not create new cell when moving after last cell with noCreate=true", () => {
3060
- const initialCellCount = state.cellIds.inOrderIds.length;
3061
- const initialState = { ...state };
3062
-
3063
- actions.moveToNextCell({
3064
- cellId: cell3Id,
3065
- before: false,
3066
- noCreate: true,
3067
- });
3068
-
3069
- // Should not create a new cell
3070
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount);
3071
- // Should not crash or throw an error
3072
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3073
- });
3074
-
3075
- it("does not create new cell when moving before first cell with noCreate=true", () => {
3076
- const initialCellCount = state.cellIds.inOrderIds.length;
3077
- const initialState = { ...state };
3078
-
3079
- actions.moveToNextCell({ cellId: cell1Id, before: true, noCreate: true });
3080
-
3081
- // Should not create a new cell
3082
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount);
3083
- // Should not crash or throw an error
3084
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3085
- });
3086
-
3087
- it("focuses next cell when moving within bounds", () => {
3088
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3089
- focusSpy.mockClear();
3090
-
3091
- actions.moveToNextCell({
3092
- cellId: cell1Id,
3093
- before: false,
3094
- noCreate: true,
3095
- });
3096
-
3097
- expect(focusSpy).toHaveBeenCalledWith({
3098
- cellId: cell2Id,
3099
- cell: state.cellHandles[cell2Id],
3100
- isCodeHidden: false,
3101
- codeFocus: "top",
3102
- variableName: undefined,
3103
- });
3104
- });
3105
-
3106
- it("focuses previous cell when moving backward within bounds", () => {
3107
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3108
- focusSpy.mockClear();
3109
-
3110
- actions.moveToNextCell({ cellId: cell2Id, before: true, noCreate: true });
3111
-
3112
- expect(focusSpy).toHaveBeenCalledWith({
3113
- cellId: cell1Id,
3114
- cell: state.cellHandles[cell1Id],
3115
- isCodeHidden: false,
3116
- codeFocus: "bottom",
3117
- variableName: undefined,
3118
- });
3119
- });
3120
-
3121
- it("does not move focus from scratch cell", () => {
3122
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3123
- focusSpy.mockClear();
3124
- const initialState = { ...state };
3125
-
3126
- actions.moveToNextCell({
3127
- cellId: SCRATCH_CELL_ID,
3128
- before: false,
3129
- noCreate: false,
3130
- });
3131
-
3132
- expect(focusSpy).not.toHaveBeenCalled();
3133
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3134
- });
3135
- });
3136
-
3137
- describe("untouched cells functionality", () => {
3138
- it("starts with empty untouchedNewCells set", () => {
3139
- expect(state.untouchedNewCells).toEqual(new Set());
3140
- });
3141
-
3142
- it("can create a new cell with hideCode option", () => {
3143
- const initialCellCount = state.cellIds.inOrderIds.length;
3144
-
3145
- actions.createNewCell({
3146
- cellId: "__end__",
3147
- before: false,
3148
- hideCode: true,
3149
- });
3150
-
3151
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3152
- const newCellId =
3153
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3154
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3155
- });
3156
-
3157
- it("adds interactively-created hidden cell with boilerplate code to untouchedNewCells", () => {
3158
- // Markdown cells are created with hideCode and non-empty default code
3159
- // (e.g. `mo.md(r"""\n""")`). They are user-initiated, so their editor
3160
- // should be shown until first blur.
3161
- actions.createNewCell({
3162
- cellId: "__end__",
3163
- before: false,
3164
- code: 'mo.md(r"""\n""")',
3165
- hideCode: true,
3166
- autoFocus: true,
3167
- });
3168
-
3169
- const newCellId =
3170
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3171
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3172
- });
3173
-
3174
- it("does not add programmatically-created hidden cell with code to untouchedNewCells", () => {
3175
- // Cells created by the kernel (e.g. via code_mode) carry code and
3176
- // autoFocus=false; their hide_code must take effect immediately.
3177
- actions.createNewCell({
3178
- cellId: "__end__",
3179
- before: false,
3180
- code: "x = 1",
3181
- hideCode: true,
3182
- autoFocus: false,
3183
- });
3184
-
3185
- const newCellId =
3186
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3187
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3188
- });
3189
-
3190
- it("does not add cell to untouchedNewCells when hideCode is false", () => {
3191
- actions.createNewCell({
3192
- cellId: "__end__",
3193
- before: false,
3194
- hideCode: false,
3195
- });
3196
-
3197
- const newCellId =
3198
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3199
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3200
- });
3201
-
3202
- it("does not add cell to untouchedNewCells when hideCode is undefined", () => {
3203
- actions.createNewCell({
3204
- cellId: "__end__",
3205
- before: false,
3206
- });
3207
-
3208
- const newCellId =
3209
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3210
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3211
- });
3212
-
3213
- it("can mark a cell as touched", () => {
3214
- // Create a cell with hideCode
3215
- actions.createNewCell({
3216
- cellId: "__end__",
3217
- before: false,
3218
- hideCode: true,
3219
- });
3220
-
3221
- const newCellId =
3222
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3223
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3224
-
3225
- // Mark it as touched
3226
- actions.markTouched({ cellId: newCellId });
3227
-
3228
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3229
- });
3230
-
3231
- it("markTouched is idempotent", () => {
3232
- // Create a cell with hideCode
3233
- actions.createNewCell({
3234
- cellId: "__end__",
3235
- before: false,
3236
- hideCode: true,
3237
- });
3238
-
3239
- const newCellId =
3240
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3241
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3242
-
3243
- // Mark it as touched multiple times
3244
- actions.markTouched({ cellId: newCellId });
3245
- actions.markTouched({ cellId: newCellId });
3246
- actions.markTouched({ cellId: newCellId });
3247
-
3248
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3249
- });
3250
-
3251
- it("can mark a non-existent cell as touched without error", () => {
3252
- const nonExistentCellId = cellId("non-existent");
3253
-
3254
- expect(() => {
3255
- actions.markTouched({ cellId: nonExistentCellId });
3256
- }).not.toThrow();
3257
-
3258
- expect(state.untouchedNewCells.has(nonExistentCellId)).toBe(false);
3259
- });
3260
-
3261
- it("can handle multiple untouched cells", () => {
3262
- // Create multiple cells with hideCode
3263
- actions.createNewCell({
3264
- cellId: "__end__",
3265
- before: false,
3266
- hideCode: true,
3267
- });
3268
- actions.createNewCell({
3269
- cellId: "__end__",
3270
- before: false,
3271
- hideCode: true,
3272
- });
3273
- actions.createNewCell({
3274
- cellId: "__end__",
3275
- before: false,
3276
- hideCode: false,
3277
- });
3278
-
3279
- const cellIds = state.cellIds.inOrderIds;
3280
- const cell1Id = cellIds[cellIds.length - 3];
3281
- const cell2Id = cellIds[cellIds.length - 2];
3282
- const cell3Id = cellIds[cellIds.length - 1];
3283
-
3284
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3285
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3286
- expect(state.untouchedNewCells.has(cell3Id)).toBe(false);
3287
-
3288
- // Mark one as touched
3289
- actions.markTouched({ cellId: cell1Id });
3290
-
3291
- expect(state.untouchedNewCells.has(cell1Id)).toBe(false);
3292
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3293
- expect(state.untouchedNewCells.has(cell3Id)).toBe(false);
3294
- });
3295
-
3296
- it("preserves untouched state when cells are moved", () => {
3297
- // Create cells with hideCode
3298
- actions.createNewCell({
3299
- cellId: "__end__",
3300
- before: false,
3301
- hideCode: true,
3302
- });
3303
- actions.createNewCell({
3304
- cellId: "__end__",
3305
- before: false,
3306
- hideCode: true,
3307
- });
3308
-
3309
- const cellIds = state.cellIds.inOrderIds;
3310
- const cell1Id = cellIds[cellIds.length - 2];
3311
- const cell2Id = cellIds[cellIds.length - 1];
3312
-
3313
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3314
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3315
-
3316
- // Move cell1 to the end
3317
- actions.moveCell({
3318
- cellId: cell1Id,
3319
- before: false,
3320
- });
3321
-
3322
- // Both cells should still be untouched
3323
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3324
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3325
- });
3326
-
3327
- it("does not remove untouched state when cell is deleted", () => {
3328
- // We could implement this, but it doesn't actually affect downstream behavior.
3329
- // It is easier to leave it, so `undo` works as expected.
3330
-
3331
- // Create a cell with hideCode
3332
- actions.createNewCell({
3333
- cellId: "__end__",
3334
- before: false,
3335
- hideCode: true,
3336
- });
3337
-
3338
- const newCellId =
3339
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3340
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3341
-
3342
- // Delete the cell
3343
- actions.deleteCell({ cellId: newCellId });
3344
-
3345
- // Does not actually remove untouched state
3346
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3347
- });
3348
-
3349
- it("restores untouched state when cell deletion is undone", () => {
3350
- // Create a cell with hideCode
3351
- actions.createNewCell({
3352
- cellId: "__end__",
3353
- before: false,
3354
- hideCode: true,
3355
- });
3356
-
3357
- const newCellId =
3358
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3359
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3360
-
3361
- // Delete the cell
3362
- actions.deleteCell({ cellId: newCellId });
3363
- // Still exists in untouchedNewCells
3364
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3365
-
3366
- // Undo the deletion
3367
- actions.undoDeleteCell();
3368
-
3369
- // The cell should be restored but no longer untouched
3370
- // (this is expected behavior - undoing doesn't restore untouched state)
3371
- const restoredCellId =
3372
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3373
- expect(state.untouchedNewCells.has(restoredCellId)).toBe(false);
3374
- });
3375
-
3376
- it("untouched cell behavior with hide_code config", () => {
3377
- // Create a cell with hideCode and hide_code config
3378
- actions.createNewCell({
3379
- cellId: "__end__",
3380
- before: false,
3381
- hideCode: true,
3382
- });
3383
-
3384
- const newCellId =
3385
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3386
-
3387
- // Update the cell config to hide code
3388
- actions.updateCellConfig({
3389
- cellId: newCellId,
3390
- config: { hide_code: true },
3391
- });
3392
-
3393
- // Cell should be untouched and have hide_code config
3394
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3395
- expect(state.cellData[newCellId].config.hide_code).toBe(true);
3396
-
3397
- // Code should not be hidden because cell is untouched
3398
- expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(false);
3399
-
3400
- // Mark as touched
3401
- actions.markTouched({ cellId: newCellId });
3402
-
3403
- // Now code should be hidden
3404
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3405
- expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(true);
3406
- });
3407
-
3408
- it("can mark an existing cell as untouched", () => {
3409
- // Create a cell without hideCode (not in untouchedNewCells)
3410
- actions.createNewCell({
3411
- cellId: "__end__",
3412
- before: false,
3413
- hideCode: false,
3414
- });
3415
-
3416
- const newCellId =
3417
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3418
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3419
-
3420
- // Mark it as untouched
3421
- actions.markUntouched({ cellId: newCellId });
3422
-
3423
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3424
- });
3425
-
3426
- it("markUntouched is idempotent", () => {
3427
- // Create a cell without hideCode
3428
- actions.createNewCell({
3429
- cellId: "__end__",
3430
- before: false,
3431
- hideCode: false,
3432
- });
3433
-
3434
- const newCellId =
3435
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3436
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3437
-
3438
- // Mark as untouched multiple times
3439
- actions.markUntouched({ cellId: newCellId });
3440
- actions.markUntouched({ cellId: newCellId });
3441
- actions.markUntouched({ cellId: newCellId });
3442
-
3443
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3444
- });
3445
-
3446
- it("markUntouched does not affect already untouched cells", () => {
3447
- // Create a cell with hideCode (already in untouchedNewCells)
3448
- actions.createNewCell({
3449
- cellId: "__end__",
3450
- before: false,
3451
- hideCode: true,
3452
- });
3453
-
3454
- const newCellId =
3455
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3456
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3457
-
3458
- // Calling markUntouched should not change anything
3459
- actions.markUntouched({ cellId: newCellId });
3460
-
3461
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3462
- });
3463
-
3464
- it("markTouched and markUntouched can toggle cell state", () => {
3465
- // Create a cell without hideCode
3466
- actions.createNewCell({
3467
- cellId: "__end__",
3468
- before: false,
3469
- hideCode: false,
3470
- });
3471
-
3472
- const newCellId =
3473
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3474
-
3475
- // Initially not untouched
3476
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3477
-
3478
- // Mark as untouched
3479
- actions.markUntouched({ cellId: newCellId });
3480
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3481
-
3482
- // Mark as touched
3483
- actions.markTouched({ cellId: newCellId });
3484
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3485
-
3486
- // Mark as untouched again
3487
- actions.markUntouched({ cellId: newCellId });
3488
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3489
- });
3490
-
3491
- it("markUntouched works for markdown cell conversion scenario", () => {
3492
- // Simulates converting a Python cell to Markdown
3493
- // 1. Create a regular cell (no hideCode)
3494
- actions.createNewCell({
3495
- cellId: "__end__",
3496
- before: false,
3497
- hideCode: false,
3498
- });
3499
-
3500
- const cellId =
3501
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3502
-
3503
- // Cell starts without hide_code and not in untouchedNewCells
3504
- expect(state.cellData[cellId].config.hide_code).toBe(false);
3505
- expect(state.untouchedNewCells.has(cellId)).toBe(false);
3506
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
3507
-
3508
- // 2. Convert to markdown: set hide_code and mark as untouched
3509
- actions.updateCellConfig({
3510
- cellId,
3511
- config: { hide_code: true },
3512
- });
3513
- actions.markUntouched({ cellId });
3514
-
3515
- // Code should NOT be hidden because cell is untouched (user can edit)
3516
- expect(state.cellData[cellId].config.hide_code).toBe(true);
3517
- expect(state.untouchedNewCells.has(cellId)).toBe(true);
3518
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
3519
-
3520
- // 3. User blurs the cell (markTouched)
3521
- actions.markTouched({ cellId });
3522
-
3523
- // Now code should be hidden
3524
- expect(state.untouchedNewCells.has(cellId)).toBe(false);
3525
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(true);
3526
- });
3527
- });
3528
-
3529
1206
  describe("releaseCellAtoms", () => {
3530
1207
  it("atom families cache atoms until explicitly removed", () => {
3531
1208
  const { cellDataAtom, cellRuntimeAtom, cellHandleAtom } =
3532
1209
  exportedForTesting;
3533
1210
 
3534
- actions.createNewCell({ cellId: firstCellId, before: false });
3535
- const newCellId = state.cellIds.inOrderIds[1];
1211
+ seedCell({ cellId: firstCellId, before: false });
1212
+ const newCellId = state.cellIds[1];
3536
1213
 
3537
1214
  const dataAtom1 = cellDataAtom(newCellId);
3538
1215
  const runtimeAtom1 = cellRuntimeAtom(newCellId);
@@ -3547,116 +1224,15 @@ describe("cell reducer", () => {
3547
1224
  expect(runtimeAtom2).toBe(runtimeAtom1);
3548
1225
  expect(handleAtom2).toBe(handleAtom1);
3549
1226
  });
3550
-
3551
- it("cleans up atom family cache when cells are deleted", () => {
3552
- const { cellDataAtom, cellRuntimeAtom, cellHandleAtom } =
3553
- exportedForTesting;
3554
-
3555
- actions.createNewCell({ cellId: firstCellId, before: false });
3556
- const newCellId = state.cellIds.inOrderIds[1];
3557
-
3558
- // Access to create cache entries
3559
- const dataAtom1 = cellDataAtom(newCellId);
3560
- const runtimeAtom1 = cellRuntimeAtom(newCellId);
3561
- const handleAtom1 = cellHandleAtom(newCellId);
3562
-
3563
- // Triggers purge
3564
- actions.deleteCell({ cellId: newCellId });
3565
-
3566
- // Access again (should be new instances after cleanup)
3567
- const dataAtom2 = cellDataAtom(newCellId);
3568
- const runtimeAtom2 = cellRuntimeAtom(newCellId);
3569
- const handleAtom2 = cellHandleAtom(newCellId);
3570
-
3571
- expect(dataAtom2).not.toBe(dataAtom1);
3572
- expect(runtimeAtom2).not.toBe(runtimeAtom1);
3573
- expect(handleAtom2).not.toBe(handleAtom1);
3574
- });
3575
- });
3576
- });
3577
-
3578
- describe("isCellCodeHidden", () => {
3579
- const state = initialNotebookState();
3580
- const firstCellId = state.cellIds.inOrderIds[0];
3581
-
3582
- it("returns false when hide_code is false and cell is not untouched", () => {
3583
- const testCellId = cellId("test-cell");
3584
- const testState: NotebookState = {
3585
- ...state,
3586
- cellData: {
3587
- ...state.cellData,
3588
- [testCellId]: {
3589
- ...state.cellData[firstCellId],
3590
- id: testCellId,
3591
- config: { hide_code: false, disabled: false, column: null },
3592
- },
3593
- },
3594
- untouchedNewCells: new Set(),
3595
- };
3596
-
3597
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3598
- false,
3599
- );
3600
- });
3601
-
3602
- it("returns true when hide_code is true and cell is not untouched", () => {
3603
- const testCellId = cellId("test-cell");
3604
- const testState: NotebookState = {
3605
- ...state,
3606
- cellData: {
3607
- ...state.cellData,
3608
- [testCellId]: {
3609
- ...state.cellData[firstCellId],
3610
- id: testCellId,
3611
- config: { hide_code: true, disabled: false, column: null },
3612
- },
3613
- },
3614
- untouchedNewCells: new Set(),
3615
- };
3616
-
3617
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3618
- true,
3619
- );
3620
- });
3621
-
3622
- it("returns false when hide_code is true but cell is untouched", () => {
3623
- const testCellId = cellId("test-cell");
3624
- const testState: NotebookState = {
3625
- ...state,
3626
- cellData: {
3627
- ...state.cellData,
3628
- [testCellId]: {
3629
- ...state.cellData[firstCellId],
3630
- id: testCellId,
3631
- config: { hide_code: true, disabled: false, column: null },
3632
- },
3633
- },
3634
- untouchedNewCells: new Set([testCellId]),
3635
- };
3636
-
3637
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3638
- false,
3639
- );
3640
1227
  });
1228
+ it("withdraws the submission of a cell that leaves the document", () => {
1229
+ MockExecutions.seed(store, []);
1230
+ submitCells(store, [firstCellId]);
1231
+ expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(1);
3641
1232
 
3642
- it("returns false when hide_code is false and cell is untouched", () => {
3643
- const testCellId = cellId("test-cell");
3644
- const testState: NotebookState = {
3645
- ...state,
3646
- cellData: {
3647
- ...state.cellData,
3648
- [testCellId]: {
3649
- ...state.cellData[firstCellId],
3650
- id: testCellId,
3651
- config: { hide_code: false, disabled: false, column: null },
3652
- },
3653
- },
3654
- untouchedNewCells: new Set([testCellId]),
3655
- };
1233
+ actions.reconcileDocument({ order: [], cells: new Map() });
3656
1234
 
3657
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3658
- false,
3659
- );
1235
+ expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(0);
3660
1236
  });
3661
1237
  });
3662
1238
 
@@ -3839,11 +1415,11 @@ describe("createTracebackInfoAtom", () => {
3839
1415
  data: [{ type: "syntax", msg: "Syntax error", lineno: 1 }],
3840
1416
  mimetype: "application/vnd.marimo+error",
3841
1417
  },
3842
- status: "queued",
3843
1418
  },
3844
1419
  },
3845
1420
  }),
3846
1421
  );
1422
+ MockExecutions.seed(store, [MockExecutions.queued(cellId("cell1"))]);
3847
1423
 
3848
1424
  const tracebackAtom = exportedForTesting.createTracebackInfoAtom(
3849
1425
  cellId("cell1"),
@@ -3871,11 +1447,11 @@ describe("createTracebackInfoAtom", () => {
3871
1447
  data: [{ type: "syntax", msg: "Syntax error", lineno: 1 }],
3872
1448
  mimetype: "application/vnd.marimo+error",
3873
1449
  },
3874
- status: "running",
3875
1450
  },
3876
1451
  },
3877
1452
  }),
3878
1453
  );
1454
+ MockExecutions.seed(store, [MockExecutions.running(cellId("cell1"))]);
3879
1455
 
3880
1456
  const tracebackAtom = exportedForTesting.createTracebackInfoAtom(
3881
1457
  cellId("cell1"),
@@ -3886,31 +1462,9 @@ describe("createTracebackInfoAtom", () => {
3886
1462
  });
3887
1463
  });
3888
1464
 
3889
- describe("setCells snapshot preservation", () => {
1465
+ describe("kernel-ready", () => {
3890
1466
  const CELL_A = cellId("A");
3891
1467
  const CELL_B = cellId("B");
3892
- const newCells: CellData[] = [
3893
- {
3894
- id: CELL_A,
3895
- name: "a",
3896
- code: "1",
3897
- edited: false,
3898
- lastCodeRun: null,
3899
- lastExecutionTime: null,
3900
- config: { hide_code: false, disabled: false, column: null },
3901
- serializedEditorState: null,
3902
- },
3903
- {
3904
- id: CELL_B,
3905
- name: "b",
3906
- code: "2",
3907
- edited: false,
3908
- lastCodeRun: null,
3909
- lastExecutionTime: null,
3910
- config: { hide_code: false, disabled: false, column: null },
3911
- serializedEditorState: null,
3912
- },
3913
- ];
3914
1468
 
3915
1469
  const hydratedState = () =>
3916
1470
  MockNotebook.notebookState({
@@ -3940,44 +1494,92 @@ describe("setCells snapshot preservation", () => {
3940
1494
  },
3941
1495
  });
3942
1496
 
3943
- beforeEach(async () => {
3944
- const { isWasm } = await import("@/core/wasm/utils");
3945
- vi.mocked(isWasm).mockReturnValue(true);
1497
+ it("setLastRun records what the kernel last ran", () => {
1498
+ const state = hydratedState();
1499
+ const next = exportedForTesting.reducer(state, {
1500
+ type: "setLastRun",
1501
+ payload: { lastCodeRun: { [CELL_A]: "0" } },
1502
+ });
1503
+
1504
+ expect(next.cellData[CELL_A]).toMatchObject({
1505
+ lastCodeRun: "0",
1506
+ edited: true,
1507
+ });
1508
+ expect(next.cellData[CELL_B]).toMatchObject({
1509
+ lastCodeRun: null,
1510
+ edited: false,
1511
+ });
1512
+ expect(next.cellRuntime).toBe(state.cellRuntime);
3946
1513
  });
3947
1514
 
3948
- it("preserves hydrated output in WASM", () => {
1515
+ it("resetCellRuntimes starts every cell's runtime over", () => {
3949
1516
  const next = exportedForTesting.reducer(hydratedState(), {
3950
- type: "setCells",
3951
- payload: newCells,
1517
+ type: "resetCellRuntimes",
1518
+ payload: undefined,
3952
1519
  });
3953
1520
 
3954
- expect(next.cellRuntime[CELL_A].output).toMatchObject({
3955
- data: "hydrated-A",
1521
+ expect(next.cellRuntime[CELL_A].output).toBeNull();
1522
+ expect(next.cellRuntime[CELL_B].consoleOutputs).toEqual([]);
1523
+ });
1524
+
1525
+ it("resetCellRuntimes starts the scratch cell over too", () => {
1526
+ const state = MockNotebook.notebookState({
1527
+ cellData: { [CELL_A]: { id: CELL_A, code: "1" } },
1528
+ cellRuntime: {
1529
+ [SCRATCH_CELL_ID]: {
1530
+ output: {
1531
+ channel: "output",
1532
+ mimetype: "text/plain",
1533
+ data: "scratch",
1534
+ timestamp: 0,
1535
+ },
1536
+ },
1537
+ },
3956
1538
  });
1539
+ const next = exportedForTesting.reducer(state, {
1540
+ type: "resetCellRuntimes",
1541
+ payload: undefined,
1542
+ });
1543
+
1544
+ expect(next.cellRuntime[SCRATCH_CELL_ID].output).toBeNull();
3957
1545
  });
3958
1546
 
3959
- it("preserves console-only hydration in WASM", () => {
3960
- const next = exportedForTesting.reducer(hydratedState(), {
3961
- type: "setCells",
3962
- payload: newCells,
1547
+ describe("resetCellRuntimes in WASM", () => {
1548
+ beforeEach(async () => {
1549
+ const { isWasm } = await import("@/core/wasm/utils");
1550
+ vi.mocked(isWasm).mockReturnValue(true);
3963
1551
  });
3964
1552
 
3965
- expect(next.cellRuntime[CELL_B].consoleOutputs).toHaveLength(1);
3966
- expect(next.cellRuntime[CELL_B].consoleOutputs[0]).toMatchObject({
3967
- data: "hydrated-B-stdout",
1553
+ afterEach(async () => {
1554
+ const { isWasm } = await import("@/core/wasm/utils");
1555
+ vi.mocked(isWasm).mockReturnValue(false);
3968
1556
  });
3969
- });
3970
1557
 
3971
- it("resets cells with no prior runtime even in WASM", () => {
3972
- const empty = MockNotebook.notebookState({ cellData: {} });
3973
- const next = exportedForTesting.reducer(empty, {
3974
- type: "setCells",
3975
- payload: newCells,
1558
+ it("keeps hydrated outputs and console", () => {
1559
+ const next = exportedForTesting.reducer(hydratedState(), {
1560
+ type: "resetCellRuntimes",
1561
+ payload: undefined,
1562
+ });
1563
+
1564
+ expect(next.cellRuntime[CELL_A].output).toMatchObject({
1565
+ data: "hydrated-A",
1566
+ });
1567
+ expect(next.cellRuntime[CELL_B].consoleOutputs[0]).toMatchObject({
1568
+ data: "hydrated-B-stdout",
1569
+ });
3976
1570
  });
3977
1571
 
3978
- expect(next.cellRuntime[CELL_A].output).toBeNull();
3979
- expect(next.cellRuntime[CELL_A].consoleOutputs).toEqual([]);
3980
- expect(next.cellRuntime[CELL_B].output).toBeNull();
3981
- expect(next.cellRuntime[CELL_B].consoleOutputs).toEqual([]);
1572
+ it("starts cells with no prior runtime over", () => {
1573
+ const state = MockNotebook.notebookState({
1574
+ cellData: { [CELL_A]: { id: CELL_A, code: "1" } },
1575
+ });
1576
+ const next = exportedForTesting.reducer(state, {
1577
+ type: "resetCellRuntimes",
1578
+ payload: undefined,
1579
+ });
1580
+
1581
+ expect(next.cellRuntime[CELL_A].output).toBeNull();
1582
+ expect(next.cellRuntime[CELL_A].consoleOutputs).toEqual([]);
1583
+ });
3982
1584
  });
3983
1585
  });