@marimo-team/islands 0.25.3-dev2 → 0.25.3-dev200

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 (690) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNSxdKyJ.js → ConnectedDataExplorerComponent-BsWiIPFk.js} +22 -20
  2. package/dist/{ErrorBoundary-DWEpirf3.js → ErrorBoundary-UWARECs-.js} +19 -19
  3. package/dist/{abnfDiagram-VCTEODGH-DQDjNXt3.js → abnfDiagram-VCTEODGH-I2x4Yu25.js} +2 -2
  4. package/dist/any-language-editor-BVFHDDQ3.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-Nmls7Wxn.js → architectureDiagram-5GKGNRK7-1bSQMKzK.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-DoQ-AILx.js} +15 -27
  7. package/dist/assets/__vite-browser-external-BCJsx8QS.js +1 -0
  8. package/dist/assets/worker-DodjWT_Z.js +113 -0
  9. package/dist/{button-CvjkwdSk.js → button-BQRdqQi1.js} +199 -192
  10. package/dist/{capabilities-CQ5IjhzL.js → capabilities-C2QUNRXi.js} +2 -2
  11. package/dist/{chat-ui-BDpymeoe.js → chat-ui-DXDoSuS4.js} +667 -667
  12. package/dist/{check-EDwSwP89.js → check-C9Mp9eCK.js} +1 -1
  13. package/dist/{chunk-IMKFNOWR-CoIFEo-1.js → chunk-IMKFNOWR-fsCGAJ9G.js} +1 -1
  14. package/dist/{chunk-SHT3W25Y-lHiGx3ZC.js → chunk-SHT3W25Y-D2STW4hj.js} +1 -1
  15. package/dist/{chunk-SVP7TREG-BvsHIrY_.js → chunk-SVP7TREG-CiY7cQG-.js} +1 -1
  16. package/dist/{chunk-TICWLB2K-BC8YIm_n.js → chunk-TICWLB2K-C_t5RBp9.js} +1 -1
  17. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  18. package/dist/{classDiagram-ZZMXUADV-Bt98lkkv.js → classDiagram-ZZMXUADV-B2JvZAP4.js} +2 -2
  19. package/dist/{classDiagram-v2-VYDZK3BY-D9bfbcC3.js → classDiagram-v2-VYDZK3BY-L8VsZG5A.js} +2 -2
  20. package/dist/{code-visibility-CLNfgCjd.js → code-visibility-33fxfX1p.js} +45 -49
  21. package/dist/{common-fyD_6mT2.js → common-D09oJJ8Z.js} +7354 -6884
  22. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-De584pZ4.js} +3 -3
  23. package/dist/{cynefinDiagram-5FMLGOSQ-CgWra7wt.js → cynefinDiagram-5FMLGOSQ-DdllC264.js} +1 -1
  24. package/dist/{dagre-GXQ25YYZ-CFZ8-35c.js → dagre-GXQ25YYZ-BaWHw4_f.js} +1 -1
  25. package/dist/{data-grid-overlay-editor-BmqASKep.js → data-grid-overlay-editor-lGsInXfR.js} +5 -5
  26. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  27. package/dist/{diagram-S7CK7UJ4-lCNQrW7d.js → diagram-S7CK7UJ4-pFt-tUDe.js} +1 -1
  28. package/dist/{diagram-UQ7AKVKN-CQPQ6pP_.js → diagram-UQ7AKVKN-DqGJiBp4.js} +1 -1
  29. package/dist/{diagram-VX7I27RA-Cf3b0llz.js → diagram-VX7I27RA-Dr-yzMjf.js} +1 -1
  30. package/dist/{diagram-Z3DM3KII-Bt4QLrpn.js → diagram-Z3DM3KII-8xzJzv-w.js} +1 -1
  31. package/dist/{dialog-CEBFZNyc.js → dialog-DVRfQuhD.js} +25 -25
  32. package/dist/{dist-F8Ep36rk.js → dist-B1M0bGO3.js} +1 -1
  33. package/dist/{dist-D6d86eNJ.js → dist-BA13RJj5.js} +1 -1
  34. package/dist/{dist-Bvs24R_E.js → dist-BJtmSgcE.js} +48 -48
  35. package/dist/{dist-CN_BeG2I.js → dist-Ba2VIn1K.js} +53 -53
  36. package/dist/{dist-PXYI_yuP.js → dist-Bj4bmwUm.js} +12 -12
  37. package/dist/{dist-DoE4XJBC.js → dist-BmS-ZGjl.js} +8626 -8626
  38. package/dist/{dist-BorMD-uX.js → dist-Bzogh7CW.js} +87 -87
  39. package/dist/{dist-CN8zI-XC.js → dist-CThPVKaP.js} +1385 -1382
  40. package/dist/{dist-DiDibOYr.js → dist-CTi5FTiV.js} +87 -87
  41. package/dist/{dist-diIx5qJW.js → dist-Ck_C32ye.js} +2 -2
  42. package/dist/{dist-BLfKmR-l.js → dist-D1fnFp1J.js} +1 -1
  43. package/dist/{dist-OR1OnN-y.js → dist-D3x9__Fu.js} +46 -46
  44. package/dist/{dist-BMQf5zIS.js → dist-D7mrh5sO.js} +36 -36
  45. package/dist/{dist-CSJ0MDRw.js → dist-D9Q3xjiP.js} +1 -1
  46. package/dist/{dist-BuaRHxOc.js → dist-DP60Ubh8.js} +2 -2
  47. package/dist/{dist-CSbh1PNP.js → dist-DTv9KkpW.js} +12 -12
  48. package/dist/{dist-D-wwqlzJ.js → dist-DVVwOfrD.js} +109 -109
  49. package/dist/{dist-C8fAKSH1.js → dist-DYXM73-k.js} +36 -36
  50. package/dist/{dist-DBtSl3sf.js → dist-DjeybPOb.js} +1 -1
  51. package/dist/{dist-CrSP_H1M.js → dist-DtGn9N4h.js} +197 -197
  52. package/dist/{dist-CLdpGQVr.js → dist-KN80qWCP.js} +2 -2
  53. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  54. package/dist/{dist-Cg7gzw-a.js → dist-O0qFpEJh.js} +56 -56
  55. package/dist/{dist-CkTTIdBe.js → dist-OUle2RT7.js} +207 -207
  56. package/dist/{dist-BK4Oj99o.js → dist-T5rh6JvL.js} +1 -1
  57. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-NlnsmNA6.js} +1481 -1433
  58. package/dist/{ebnfDiagram-PWID7BFC-D3NAyPFY.js → ebnfDiagram-PWID7BFC-Br04Wy-E.js} +2 -2
  59. package/dist/{erDiagram-RLTQ6QDP-Tlu09_Nm.js → erDiagram-RLTQ6QDP-D5-XVgvv.js} +1 -1
  60. package/dist/{error-banner-Da600cwz.js → error-banner-DxJ3s8YO.js} +89 -84
  61. package/dist/{cells-C8iCYvLU.js → error-entries-C2Ux0Fjt.js} +7970 -8605
  62. package/dist/{esm-D2gTsXEV.js → esm-Bbw_Njjx.js} +575 -575
  63. package/dist/{esm-AMFssoNp.js → esm-mDQNEFe_.js} +354 -354
  64. package/dist/feedback-button-B0CMhVv7.js +374 -0
  65. package/dist/{flowDiagram-HODETNUW-CAYbpJmV.js → flowDiagram-HODETNUW-CVl2kREF.js} +2 -2
  66. package/dist/{formats-BGh2thRc.js → formats-BCYthwyc.js} +3 -3
  67. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  68. package/dist/{glide-data-editor-BkjP14aq.js → glide-data-editor-CL535A5q.js} +370 -368
  69. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-D98laidb.js} +16 -16
  70. package/dist/{globals-BN2O0F8N.js → globals-Bsr6T2yD.js} +3 -3
  71. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  72. package/dist/{html-to-image-CZY1pIZH.js → html-to-image-CkH7vdBZ.js} +478 -381
  73. package/dist/{infoDiagram-27XIBGKW-XrDxhr0x.js → infoDiagram-27XIBGKW-BJ4vNcZE.js} +1 -1
  74. package/dist/{ishikawaDiagram-5VMMS53U-CD1muzov.js → ishikawaDiagram-5VMMS53U-B_Jy1Dsm.js} +1 -1
  75. package/dist/{kanban-definition-UXKFOSKX-Dg9W142_.js → kanban-definition-UXKFOSKX-C6MkS9vw.js} +1 -1
  76. package/dist/{label-BDKF3Afc.js → label-Qvw1E97W.js} +3 -3
  77. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  78. package/dist/{loader-BQOfjTIJ.js → loader-DOVB0Syl.js} +12 -12
  79. package/dist/main.js +9706 -7708
  80. package/dist/{mermaid-CiAXjLpA.js → mermaid-Bh918qrq.js} +52 -52
  81. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  82. package/dist/{mindmap-definition-YA3MSWOX-DgCidgYJ.js → mindmap-definition-YA3MSWOX-BP3ZTuir.js} +1 -1
  83. package/dist/{number-overlay-editor-BEBWJnbL.js → number-overlay-editor-CzbkG4E2.js} +3 -3
  84. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  85. package/dist/{once-BtE-wKRL.js → once-NnpX_HnX.js} +1 -1
  86. package/dist/{pegDiagram-XKGWAZYB-coOYYKB6.js → pegDiagram-XKGWAZYB-nNGOQtnt.js} +2 -2
  87. package/dist/{pieDiagram-E7YTZNPT-BnXTc3bj.js → pieDiagram-E7YTZNPT-CpRz5BZz.js} +1 -1
  88. package/dist/precisionRound-DhAzeRcm.js +49 -0
  89. package/dist/{process-output-e4jj9Ijt.js → process-output-OujAtFW4.js} +4 -4
  90. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  91. package/dist/{railroadDiagram-O6MQD6OU-B28-u83n.js → railroadDiagram-O6MQD6OU-BqOEN4I0.js} +2 -2
  92. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  93. package/dist/{requirementDiagram-BXWQKSXE-CdYCTMu0.js → requirementDiagram-BXWQKSXE-CEfg6vu9.js} +1 -1
  94. package/dist/{reveal-component-DK69bOki.js → reveal-component-CbEK0sNX.js} +733 -710
  95. package/dist/{select-BButFi-d.js → select-2KOiReNY.js} +536 -457
  96. package/dist/{sequenceDiagram-WJ2MYXX4-CM3aRAe_.js → sequenceDiagram-WJ2MYXX4-IOmpQcOi.js} +1 -1
  97. package/dist/{spec-BwarBpUJ.js → spec-Dkp64vVQ.js} +41 -11
  98. package/dist/{stateDiagram-D77RDMKH-DOZUMVj8.js → stateDiagram-D77RDMKH-tgU-EY9_.js} +2 -2
  99. package/dist/{stateDiagram-v2-MP3YSRHH-CISYxma6.js → stateDiagram-v2-MP3YSRHH-BofhG6q1.js} +2 -2
  100. package/dist/style.css +1 -1
  101. package/dist/{swimlanes-42K2YHIH-BkSppxp8.js → swimlanes-42K2YHIH-DW4vWDBd.js} +1 -1
  102. package/dist/{swimlanesDiagram-VR7AAH4N-c8G5pGxC.js → swimlanesDiagram-VR7AAH4N-BJcrqUJr.js} +2 -2
  103. package/dist/{swiper-component-DKntyn8p.js → swiper-component-BVfnhbnt.js} +2 -2
  104. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  105. package/dist/{timeline-definition-24CTP7MA-CaMQayqK.js → timeline-definition-24CTP7MA-D_QU3NGs.js} +1 -1
  106. package/dist/{toDate-BNJf01u1.js → toDate-BNDEXbfV.js} +77 -74
  107. package/dist/{toast-B9UMEi8u.js → toast-B5t9o6s_.js} +219 -201
  108. package/dist/{tooltip-BO1REbqs.js → tooltip-s4zZxwKe.js} +325 -301
  109. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-BgDTd2tc.js} +1482 -1433
  110. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ASDni9vH.js} +15 -14
  111. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-D3Tkfzej.js} +2 -2
  112. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-B1Ogyf53.js} +1 -1
  113. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-6a_yDNXp.js} +1 -1
  114. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-Bfpa2m7k.js} +28 -25
  115. package/dist/{useTheme-CRnjkku0.js → useTheme-5TwyOh3B.js} +522 -314
  116. package/dist/{vega-component-DG29TAB7.js → vega-component-BHS9UOtC.js} +158 -158
  117. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  118. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  119. package/dist/{vennDiagram-4TSXK5OY-CF8pfaK3.js → vennDiagram-4TSXK5OY-YFYsNIAh.js} +1 -1
  120. package/dist/{wardleyDiagram-VM6X3IG4-dGPu39d-.js → wardleyDiagram-VM6X3IG4-i_n8mAWC.js} +1 -1
  121. package/dist/{xychartDiagram-S5SC5T6Z-CP6-9q56.js → xychartDiagram-S5SC5T6Z-BYGN95wW.js} +2 -2
  122. package/package.json +16 -16
  123. package/src/__mocks__/document.tsx +63 -0
  124. package/src/__mocks__/notebook.ts +3 -4
  125. package/src/__mocks__/partial.ts +26 -0
  126. package/src/__mocks__/requests.ts +0 -3
  127. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  128. package/src/__tests__/main.test.tsx +24 -1
  129. package/src/__tests__/module-entry.test.ts +44 -0
  130. package/src/__tests__/mount.test.ts +9 -9
  131. package/src/__tests__/{host-imports.test.ts → replica-imports.test.ts} +12 -11
  132. package/src/components/ai/__tests__/ai-model-dropdown.test.tsx +63 -0
  133. package/src/components/ai/ai-model-dropdown.tsx +11 -6
  134. package/src/components/app-config/ai-config.tsx +7 -2
  135. package/src/components/app-config/app-config-form.tsx +5 -9
  136. package/src/components/app-config/user-config-form.tsx +35 -0
  137. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  138. package/src/components/chat/__tests__/notebook-access-selector.test.tsx +64 -0
  139. package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
  140. package/src/components/chat/acp/__tests__/state.test.ts +2 -2
  141. package/src/components/chat/acp/blocks.tsx +3 -2
  142. package/src/components/chat/acp/session-tabs.tsx +8 -3
  143. package/src/components/chat/chat-components.tsx +2 -0
  144. package/src/components/chat/chat-panel.tsx +20 -101
  145. package/src/components/chat/chat-utils.ts +4 -8
  146. package/src/components/chat/notebook-access-selector.stories.tsx +22 -0
  147. package/src/components/chat/notebook-access-selector.tsx +100 -0
  148. package/src/components/data-table/TableBottomBar.tsx +4 -1
  149. package/src/components/data-table/TableTopBar.tsx +2 -0
  150. package/src/components/data-table/__tests__/chart-spec-model.test.ts +195 -195
  151. package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
  152. package/src/components/data-table/__tests__/columns.test.tsx +105 -0
  153. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  154. package/src/components/data-table/__tests__/context-menu.test.ts +176 -0
  155. package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
  156. package/src/components/data-table/__tests__/date-filter-inputs.test.tsx +24 -1
  157. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  158. package/src/components/data-table/__tests__/export-actions.test.tsx +434 -6
  159. package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
  160. package/src/components/data-table/__tests__/pagination.test.tsx +28 -1
  161. package/src/components/data-table/__tests__/search-highlight.test.tsx +109 -0
  162. package/src/components/data-table/__tests__/url-detector.test.tsx +74 -0
  163. package/src/components/data-table/cell-selection/feature.ts +2 -2
  164. package/src/components/data-table/cell-selection/types.ts +2 -2
  165. package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
  166. package/src/components/data-table/charts/__tests__/spec-snapshot.test.ts +20 -20
  167. package/src/components/data-table/charts/__tests__/spec.test.ts +78 -52
  168. package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
  169. package/src/components/data-table/charts/chart-spec/spec.ts +68 -43
  170. package/src/components/data-table/charts/charts.tsx +40 -30
  171. package/src/components/data-table/charts/storage.ts +16 -5
  172. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
  173. package/src/components/data-table/column-summary/chart-spec-model.tsx +23 -16
  174. package/src/components/data-table/column-summary/column-summary.tsx +2 -4
  175. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  176. package/src/components/data-table/columns.tsx +45 -46
  177. package/src/components/data-table/context-menu.tsx +161 -46
  178. package/src/components/data-table/data-table.tsx +17 -1
  179. package/src/components/data-table/date-filter-inputs.tsx +15 -42
  180. package/src/components/data-table/date-popover.tsx +6 -4
  181. package/src/components/data-table/export-actions.tsx +267 -163
  182. package/src/components/data-table/pagination.tsx +44 -33
  183. package/src/components/data-table/range-focus/atoms.ts +4 -6
  184. package/src/components/data-table/range-focus/use-cell-range-selection.ts +65 -38
  185. package/src/components/data-table/range-focus/use-scroll-into-view.ts +21 -2
  186. package/src/components/data-table/renderers.tsx +16 -7
  187. package/src/components/data-table/row-viewer-panel/__tests__/row-viewer.test.tsx +24 -1
  188. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +4 -4
  189. package/src/components/data-table/schemas.ts +14 -36
  190. package/src/components/data-table/search-highlight.tsx +79 -0
  191. package/src/components/data-table/types.ts +1 -0
  192. package/src/components/data-table/url-detector.tsx +49 -9
  193. package/src/components/data-table/utils.ts +18 -0
  194. package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
  195. package/src/components/datasources/__tests__/column-preview.test.tsx +96 -5
  196. package/src/components/datasources/__tests__/datasources-interactions.test.tsx +181 -0
  197. package/src/components/datasources/__tests__/table-list.test.tsx +118 -0
  198. package/src/components/datasources/__tests__/utils.test.ts +57 -6
  199. package/src/components/datasources/column-preview.tsx +17 -12
  200. package/src/components/datasources/datasources.tsx +56 -43
  201. package/src/components/datasources/utils.ts +32 -23
  202. package/src/components/debugger/debugger-code.tsx +5 -0
  203. package/src/components/dependency-graph/custom-node.tsx +1 -1
  204. package/src/components/dependency-graph/dependency-graph-tree.tsx +8 -8
  205. package/src/components/dependency-graph/elements.ts +24 -21
  206. package/src/components/dependency-graph/minimap-content.tsx +13 -19
  207. package/src/components/dependency-graph/panels.tsx +4 -2
  208. package/src/components/dependency-graph/utils/__tests__/layout.test.ts +18 -11
  209. package/src/components/editor/Output.tsx +9 -2
  210. package/src/components/editor/SortableCell.tsx +1 -3
  211. package/src/components/editor/__tests__/Output.test.tsx +50 -32
  212. package/src/components/editor/__tests__/agent-toast.test.tsx +58 -0
  213. package/src/components/editor/__tests__/data-attributes.test.tsx +28 -14
  214. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  215. package/src/components/editor/__tests__/output-persistence.test.tsx +6 -2
  216. package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
  217. package/src/components/editor/actions/__tests__/name-cell-input.test.tsx +69 -0
  218. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  219. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +218 -40
  220. package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +2 -6
  221. package/src/components/editor/actions/name-cell-input.tsx +4 -3
  222. package/src/components/editor/actions/pair-with-agent-commands.ts +44 -53
  223. package/src/components/editor/actions/pair-with-agent-modal.tsx +101 -38
  224. package/src/components/editor/actions/useCellActionButton.tsx +26 -31
  225. package/src/components/editor/actions/useConfigActions.tsx +3 -4
  226. package/src/components/editor/actions/useNotebookActions.tsx +19 -23
  227. package/src/components/editor/actions/useRestartKernel.tsx +1 -1
  228. package/src/components/editor/actions/useSetCodeVisibility.ts +11 -12
  229. package/src/components/editor/actions/useSetOutputsExpanded.ts +14 -19
  230. package/src/components/editor/agent-toast.ts +15 -0
  231. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  232. package/src/components/editor/ai/ai-completion-editor.tsx +1 -0
  233. package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +1 -1
  234. package/src/components/editor/alerts/connection-notice.tsx +1 -1
  235. package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
  236. package/src/components/editor/alerts/stdin-blocking-alert.tsx +1 -1
  237. package/src/components/editor/app-container.tsx +1 -1
  238. package/src/components/editor/boundary/__tests__/ErrorBoundary.test.tsx +1 -1
  239. package/src/components/editor/cell/CreateCellButton.tsx +15 -21
  240. package/src/components/editor/cell/DeleteButton.tsx +3 -2
  241. package/src/components/editor/cell/RunButton.tsx +2 -2
  242. package/src/components/editor/cell/StagedAICell.tsx +10 -2
  243. package/src/components/editor/cell/StopButton.tsx +2 -2
  244. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +3 -1
  245. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  246. package/src/components/editor/cell/cell-actions.tsx +12 -1
  247. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  248. package/src/components/editor/cell/code/cell-editor.tsx +36 -43
  249. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  250. package/src/components/editor/cell/collapse.tsx +11 -4
  251. package/src/components/editor/cell/useAddCell.ts +4 -8
  252. package/src/components/editor/cell/useDeleteCell.tsx +10 -36
  253. package/src/components/editor/cell/useRunCells.ts +2 -5
  254. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  255. package/src/components/editor/cell/useSplitCell.tsx +10 -6
  256. package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +1 -1
  257. package/src/components/editor/chrome/components/feedback-button.tsx +2 -8
  258. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +31 -2
  259. package/src/components/editor/chrome/panels/__tests__/runtime-details.test.tsx +1 -1
  260. package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +1 -1
  261. package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
  262. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +1 -1
  263. package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
  264. package/src/components/editor/chrome/panels/logs-panel.tsx +2 -1
  265. package/src/components/editor/chrome/panels/outline/__tests__/outline-keyboard.test.tsx +84 -0
  266. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +39 -14
  267. package/src/components/editor/chrome/panels/outline/useActiveOutline.tsx +3 -2
  268. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  269. package/src/components/editor/chrome/panels/secrets-panel.tsx +1 -0
  270. package/src/components/editor/chrome/panels/session-panel.tsx +1 -1
  271. package/src/components/editor/chrome/panels/snippets-panel.tsx +10 -12
  272. package/src/components/editor/chrome/panels/variable-panel.tsx +1 -5
  273. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +363 -0
  274. package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
  275. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +61 -2
  276. package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
  277. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
  278. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
  279. package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
  280. package/src/components/editor/chrome/wrapper/footer-items/lsp-status.tsx +1 -1
  281. package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +1 -1
  282. package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +1 -7
  283. package/src/components/editor/chrome/wrapper/footer.tsx +4 -0
  284. package/src/components/editor/chrome/wrapper/minimap-state.ts +1 -1
  285. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +12 -2
  286. package/src/components/editor/chrome/wrapper/storage.ts +1 -1
  287. package/src/components/editor/columns/__tests__/cell-column.test.tsx +1 -1
  288. package/src/components/editor/columns/cell-column.tsx +1 -1
  289. package/src/components/editor/columns/sortable-column.tsx +2 -2
  290. package/src/components/editor/connections/components.tsx +5 -10
  291. package/src/components/editor/controls/Controls.tsx +4 -4
  292. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  293. package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
  294. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +10 -4
  295. package/src/components/editor/controls/command-palette.tsx +22 -10
  296. package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
  297. package/src/components/editor/documentation.css +34 -11
  298. package/src/components/editor/dynamic-favicon.tsx +21 -40
  299. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +536 -0
  300. package/src/components/editor/errors/auto-fix.tsx +4 -8
  301. package/src/components/editor/errors/send-error-report-button.tsx +66 -0
  302. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  303. package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
  304. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  305. package/src/components/editor/file-tree/file-explorer.tsx +48 -15
  306. package/src/components/editor/file-tree/file-name-input.tsx +1 -0
  307. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  308. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  309. package/src/components/editor/header/__tests__/status.test.tsx +5 -3
  310. package/src/components/editor/header/app-header.tsx +1 -1
  311. package/src/components/editor/header/status.tsx +40 -20
  312. package/src/components/editor/links/__tests__/cell-link.test.tsx +148 -0
  313. package/src/components/editor/links/cell-link-list.tsx +1 -1
  314. package/src/components/editor/links/cell-link.tsx +17 -7
  315. package/src/components/editor/navigation/__tests__/clipboard.test.ts +60 -38
  316. package/src/components/editor/navigation/__tests__/navigation.test.ts +108 -85
  317. package/src/components/editor/navigation/__tests__/selection.test.ts +43 -21
  318. package/src/components/editor/navigation/clipboard.ts +17 -14
  319. package/src/components/editor/navigation/focus-utils.ts +23 -4
  320. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +17 -33
  321. package/src/components/editor/navigation/navigation.ts +71 -70
  322. package/src/components/editor/navigation/selection.ts +30 -29
  323. package/src/components/editor/notebook-banner.tsx +1 -0
  324. package/src/components/editor/notebook-cell.tsx +29 -31
  325. package/src/components/editor/output/MarimoErrorOutput.tsx +23 -6
  326. package/src/components/editor/output/MarimoTracebackOutput.tsx +54 -37
  327. package/src/components/editor/output/Outputs.css +8 -0
  328. package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
  329. package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
  330. package/src/components/editor/output/ansi-reduce.ts +36 -17
  331. package/src/components/editor/output/console/ConsoleOutput.tsx +19 -25
  332. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +125 -69
  333. package/src/components/editor/package-alert.tsx +14 -7
  334. package/src/components/editor/renderers/cell-array.tsx +22 -33
  335. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +12 -6
  336. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  337. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +1 -1
  338. package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +4 -4
  339. package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
  340. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +1 -1
  341. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  342. package/src/components/editor/stdin-blocking-alert.tsx +1 -1
  343. package/src/components/home/components.tsx +17 -31
  344. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  345. package/src/components/icons/large-spinner.tsx +3 -14
  346. package/src/components/icons/spinner.tsx +1 -1
  347. package/src/components/markdown/markdown-renderer.tsx +4 -8
  348. package/src/components/mcp/mcp-status-indicator.tsx +1 -1
  349. package/src/components/pages/__tests__/home-page.test.tsx +116 -0
  350. package/src/components/pages/edit-page.tsx +19 -17
  351. package/src/components/pages/home-page.tsx +164 -91
  352. package/src/components/scratchpad/scratchpad.test.tsx +97 -0
  353. package/src/components/scratchpad/scratchpad.tsx +20 -11
  354. package/src/components/slides/__tests__/minimap-actions.test.tsx +22 -29
  355. package/src/components/slides/__tests__/minimap.test.ts +38 -19
  356. package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +37 -0
  357. package/src/components/slides/minimap.tsx +17 -12
  358. package/src/components/slides/reveal-component.tsx +58 -26
  359. package/src/components/slides/reveal-slides.css +5 -4
  360. package/src/components/slides/slide-cell-view.tsx +4 -5
  361. package/src/components/slides/slide-form.tsx +1 -0
  362. package/src/components/slides/slide-scroll-hint.tsx +3 -1
  363. package/src/components/sort/SortableCellsProvider.tsx +8 -25
  364. package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
  365. package/src/components/tracing/tracing.tsx +46 -42
  366. package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
  367. package/src/components/ui/__tests__/range-slider.test.tsx +165 -0
  368. package/src/components/ui/alert-dialog.tsx +1 -1
  369. package/src/components/ui/draggable-popover.tsx +24 -2
  370. package/src/components/ui/range-slider.tsx +22 -9
  371. package/src/core/MarimoApp.tsx +1 -1
  372. package/src/core/ai/__tests__/staged-cells.test.ts +27 -26
  373. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +8 -12
  374. package/src/core/ai/context/providers/cell-output.ts +2 -2
  375. package/src/core/ai/context/providers/datasource.ts +6 -2
  376. package/src/core/ai/staged-cells.ts +15 -18
  377. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +40 -88
  378. package/src/core/ai/tools/__tests__/registry.test.ts +17 -0
  379. package/src/core/ai/tools/base.ts +18 -15
  380. package/src/core/ai/tools/edit-notebook-tool.ts +86 -80
  381. package/src/core/ai/tools/registry.ts +13 -10
  382. package/src/core/ai/tools/sample-tool.ts +6 -7
  383. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -37
  384. package/src/core/cells/__tests__/actions.test.ts +624 -35
  385. package/src/core/cells/__tests__/add-missing-import.test.ts +7 -8
  386. package/src/core/cells/__tests__/arrange.test.ts +432 -0
  387. package/src/core/cells/__tests__/cell.test.ts +39 -0
  388. package/src/core/cells/__tests__/cells.test.ts +256 -2477
  389. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
  390. package/src/core/cells/__tests__/document.test.ts +341 -0
  391. package/src/core/cells/__tests__/editor.test.ts +57 -0
  392. package/src/core/cells/__tests__/error-report.test.ts +139 -0
  393. package/src/core/cells/__tests__/flush-document.test.ts +40 -0
  394. package/src/core/cells/__tests__/harness.ts +106 -0
  395. package/src/core/cells/__tests__/layout.test.ts +177 -0
  396. package/src/core/cells/__tests__/notebook-outline.test.ts +120 -0
  397. package/src/core/cells/__tests__/outputs.test.tsx +71 -53
  398. package/src/core/cells/__tests__/session.test.ts +493 -210
  399. package/src/core/cells/__tests__/utils.test.ts +9 -108
  400. package/src/core/cells/__tests__/with-document-save.test.ts +67 -0
  401. package/src/core/cells/actions.ts +636 -6
  402. package/src/core/cells/add-missing-import.ts +47 -43
  403. package/src/core/cells/arrange.ts +529 -0
  404. package/src/core/cells/cell.ts +14 -3
  405. package/src/core/cells/cells.ts +120 -1337
  406. package/src/core/cells/columns.ts +38 -0
  407. package/src/core/cells/document.ts +289 -0
  408. package/src/core/cells/edit.ts +30 -0
  409. package/src/core/cells/editor.ts +82 -0
  410. package/src/core/cells/error-report.ts +63 -0
  411. package/src/core/cells/layout.ts +221 -0
  412. package/src/core/cells/outputs.ts +12 -28
  413. package/src/core/cells/pending-delete-service.ts +3 -7
  414. package/src/core/cells/session.ts +104 -54
  415. package/src/core/cells/types.ts +22 -6
  416. package/src/core/cells/utils.ts +10 -8
  417. package/src/core/codemirror/__tests__/setup.test.ts +29 -29
  418. package/src/core/codemirror/ai/__tests__/resources.test.ts +117 -0
  419. package/src/core/codemirror/ai/resources.ts +1 -1
  420. package/src/core/codemirror/cells/__tests__/extensions.test.ts +42 -1
  421. package/src/core/codemirror/cells/extensions.ts +27 -9
  422. package/src/core/codemirror/cells/state.ts +1 -1
  423. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +6 -7
  424. package/src/core/codemirror/copilot/getCodes.ts +2 -2
  425. package/src/core/codemirror/format.ts +6 -5
  426. package/src/core/codemirror/keymaps/vim.ts +4 -8
  427. package/src/core/codemirror/language/__tests__/sql.test.ts +22 -20
  428. package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
  429. package/src/core/codemirror/language/cell-code.ts +114 -0
  430. package/src/core/codemirror/language/extension.ts +52 -35
  431. package/src/core/codemirror/language/languages/markdown.ts +8 -6
  432. package/src/core/codemirror/language/languages/python.ts +16 -10
  433. package/src/core/codemirror/language/languages/sql/sql.ts +16 -24
  434. package/src/core/codemirror/language/panel/panel.tsx +1 -0
  435. package/src/core/codemirror/language/types.ts +7 -7
  436. package/src/core/codemirror/language/utils.ts +9 -39
  437. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +1 -2
  438. package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
  439. package/src/core/codemirror/lsp/utils.ts +1 -1
  440. package/src/core/codemirror/readonly/__tests__/extension.test.ts +1 -1
  441. package/src/core/codemirror/readonly/extension.ts +2 -2
  442. package/src/core/codemirror/rtc/extension.ts +24 -13
  443. package/src/core/codemirror/rtc/loro/awareness.ts +34 -19
  444. package/src/core/codemirror/rtc/loro/sync.ts +1 -0
  445. package/src/core/codemirror/utils.ts +1 -0
  446. package/src/core/config/__tests__/app-config.test.ts +64 -0
  447. package/src/core/config/__tests__/config-schema.test.ts +13 -6
  448. package/src/core/config/config-schema.ts +13 -3
  449. package/src/core/config/config.ts +26 -12
  450. package/src/core/config/pair.ts +5 -3
  451. package/src/core/datasets/__tests__/data-source-discovery.test.ts +5 -5
  452. package/src/core/datasets/data-source-discovery.ts +4 -6
  453. package/src/core/documentation/doc-lookup.ts +1 -1
  454. package/src/core/edit-app.tsx +21 -28
  455. package/src/core/errors/error-entries.ts +4 -1
  456. package/src/core/export/hooks.ts +1 -1
  457. package/src/core/export/layout.ts +2 -2
  458. package/src/core/hotkeys/actions.ts +2 -4
  459. package/src/core/islands/__tests__/bootstrap.test.ts +69 -17
  460. package/src/core/islands/__tests__/bridge.test.ts +72 -25
  461. package/src/core/islands/bootstrap.ts +46 -53
  462. package/src/core/islands/bridge.ts +74 -23
  463. package/src/core/islands/components/__tests__/IslandControls.test.tsx +7 -5
  464. package/src/core/islands/components/web-components.tsx +1 -1
  465. package/src/core/islands/worker/__tests__/controller.test.ts +62 -4
  466. package/src/core/islands/worker/controller.ts +4 -10
  467. package/src/core/islands/worker/worker.tsx +33 -33
  468. package/src/core/kernel/__tests__/RuntimeState.test.ts +1 -1
  469. package/src/core/kernel/__tests__/handlers.test.ts +7 -153
  470. package/src/core/kernel/handlers.ts +19 -72
  471. package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
  472. package/src/core/meta/code-visibility.ts +18 -9
  473. package/src/core/network/__tests__/connection.test.ts +1 -1
  474. package/src/core/network/__tests__/requests-lazy.test.ts +1 -3
  475. package/src/core/network/connection-notice.ts +1 -1
  476. package/src/core/network/connection.ts +5 -3
  477. package/src/core/network/requests-lazy.ts +1 -4
  478. package/src/core/network/requests-network.ts +0 -25
  479. package/src/core/network/requests-static.ts +0 -3
  480. package/src/core/network/requests-toasting.tsx +0 -3
  481. package/src/core/network/types.ts +0 -12
  482. package/src/core/network/useConnectionNotice.ts +8 -2
  483. package/src/core/notebook/__tests__/operations.test.ts +51 -0
  484. package/src/core/notebook/attachments.ts +38 -0
  485. package/src/core/notebook/document/__tests__/codemirror.test.ts +586 -0
  486. package/src/core/notebook/document/__tests__/draft.test.ts +363 -0
  487. package/src/core/notebook/document/__tests__/fixtures.ts +92 -0
  488. package/src/core/notebook/document/__tests__/hold.test.ts +88 -0
  489. package/src/core/notebook/document/__tests__/hooks.test.tsx +391 -0
  490. package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +61 -0
  491. package/src/core/notebook/document/__tests__/model.test.ts +247 -0
  492. package/src/core/notebook/document/__tests__/protocol-fixtures.ts +38 -0
  493. package/src/core/notebook/document/__tests__/recovery-scenarios.test.ts +230 -0
  494. package/src/core/notebook/document/__tests__/scenario-fixture.ts +43 -0
  495. package/src/core/notebook/document/__tests__/scenarios.test.ts +205 -0
  496. package/src/core/notebook/document/__tests__/state.test.ts +459 -0
  497. package/src/core/notebook/document/__tests__/submission.test.ts +203 -0
  498. package/src/core/notebook/document/__tests__/test-host.test.ts +92 -0
  499. package/src/core/notebook/document/__tests__/test-host.ts +247 -0
  500. package/src/core/notebook/document/__tests__/test-notebook.ts +149 -0
  501. package/src/core/notebook/document/__tests__/user.ts +61 -0
  502. package/src/core/notebook/document/codemirror.ts +216 -0
  503. package/src/core/notebook/document/draft.ts +502 -0
  504. package/src/core/notebook/document/hooks.tsx +118 -0
  505. package/src/core/notebook/document/index.ts +24 -0
  506. package/src/core/notebook/document/model.ts +105 -0
  507. package/src/core/notebook/document/select.ts +41 -0
  508. package/src/core/notebook/document/state.ts +378 -0
  509. package/src/core/notebook/document/sync.ts +418 -0
  510. package/src/core/notebook/index.ts +19 -0
  511. package/src/core/notebook/operations.ts +30 -0
  512. package/src/core/notebook/replica/AGENTS.md +11 -0
  513. package/src/core/notebook/replica/__tests__/attachments.test.ts +56 -0
  514. package/src/core/notebook/replica/__tests__/document.test.ts +126 -0
  515. package/src/core/notebook/replica/__tests__/fixtures.ts +79 -0
  516. package/src/core/notebook/replica/__tests__/replica.test.ts +550 -0
  517. package/src/core/notebook/replica/backend.ts +19 -0
  518. package/src/core/{websocket/types.ts → notebook/replica/connection.ts} +10 -15
  519. package/src/core/notebook/replica/index.ts +13 -0
  520. package/src/core/notebook/replica/replica.ts +286 -0
  521. package/src/core/notebook/replica/resources/attachments.ts +22 -0
  522. package/src/core/notebook/replica/resources/document.ts +101 -0
  523. package/src/core/packages/__tests__/useSandboxController.test.tsx +1 -1
  524. package/src/core/packages/useSandboxController.ts +1 -1
  525. package/src/core/run-app.tsx +48 -51
  526. package/src/core/runtime/__tests__/adapter.test.ts +1 -1
  527. package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
  528. package/src/core/runtime/__tests__/runtime.test.ts +33 -15
  529. package/src/core/runtime/adapter.ts +1 -1
  530. package/src/core/runtime/config.ts +2 -2
  531. package/src/core/runtime/runtime.ts +39 -30
  532. package/src/core/saving/__tests__/state.test.ts +111 -0
  533. package/src/core/saving/__tests__/useAutoSave.test.tsx +67 -0
  534. package/src/core/saving/filename.ts +1 -1
  535. package/src/core/saving/save-component.tsx +28 -36
  536. package/src/core/saving/state.ts +38 -39
  537. package/src/core/saving/useAutoSave.ts +17 -53
  538. package/src/core/session/__tests__/backend.test.ts +236 -0
  539. package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +184 -0
  540. package/src/core/{websocket/__tests__/useMarimoKernelConnection.hook.test.tsx → session/__tests__/useNotebookSession.hook.test.tsx} +252 -185
  541. package/src/core/session/active-notebook.ts +22 -0
  542. package/src/core/session/backend.ts +123 -0
  543. package/src/core/session/embedded.ts +7 -0
  544. package/src/core/session/types.ts +17 -0
  545. package/src/core/session/useNotebookReplica.ts +74 -0
  546. package/src/core/{websocket/useMarimoKernelConnection.tsx → session/useNotebookSession.tsx} +120 -273
  547. package/src/core/state/__tests__/jotai.test.tsx +108 -0
  548. package/src/core/state/jotai.ts +1 -1
  549. package/src/core/static/__tests__/files.test.ts +38 -10
  550. package/src/core/static/files.ts +2 -0
  551. package/src/core/{websocket/__tests__/useMarimoKernelConnection.test.ts → transports/__tests__/connection-state.test.ts} +16 -30
  552. package/src/core/transports/__tests__/server.test.ts +408 -0
  553. package/src/core/transports/__tests__/static.test.ts +306 -0
  554. package/src/core/transports/__tests__/wasm.test.ts +166 -0
  555. package/src/core/transports/connection-state.ts +63 -0
  556. package/src/core/transports/server.ts +152 -0
  557. package/src/core/transports/static.ts +166 -0
  558. package/src/core/transports/wasm.ts +97 -0
  559. package/src/core/wasm/__tests__/bridge.test.ts +214 -50
  560. package/src/core/wasm/__tests__/worker.test.ts +187 -0
  561. package/src/core/wasm/bridge.ts +75 -69
  562. package/src/core/wasm/rpc.ts +15 -3
  563. package/src/core/wasm/store.ts +3 -3
  564. package/src/core/wasm/worker/bootstrap.ts +55 -51
  565. package/src/core/wasm/worker/getMarimoWheel.ts +1 -1
  566. package/src/core/wasm/worker/save-worker.ts +9 -10
  567. package/src/core/wasm/worker/types.ts +29 -18
  568. package/src/core/wasm/worker/worker.ts +88 -44
  569. package/src/css/globals.css +66 -22
  570. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  571. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  572. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  573. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  574. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  575. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  576. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  577. package/src/hooks/useAsyncData.ts +3 -3
  578. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  579. package/src/hooks/useDebounce.ts +6 -10
  580. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  581. package/src/hooks/useDelayElapsed.ts +14 -10
  582. package/src/hooks/useElapsedTime.ts +3 -3
  583. package/src/hooks/useEventListener.ts +1 -2
  584. package/src/hooks/useInputHistory.ts +4 -7
  585. package/src/hooks/useInternalStateWithSync.ts +6 -6
  586. package/src/hooks/useResizeObserver.ts +4 -7
  587. package/src/hooks/useScript.ts +67 -70
  588. package/src/hooks/useStateForKey.ts +93 -0
  589. package/src/mount.tsx +23 -14
  590. package/src/plugins/__tests__/plugin-object-schemas.test.ts +104 -0
  591. package/src/plugins/core/registerReactComponent.tsx +1 -1
  592. package/src/plugins/impl/CodeEditorPlugin.tsx +1 -1
  593. package/src/plugins/impl/DataEditorPlugin.tsx +1 -1
  594. package/src/plugins/impl/DataTablePlugin.tsx +69 -37
  595. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  596. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  597. package/src/plugins/impl/FormPlugin.tsx +1 -1
  598. package/src/plugins/impl/RangeSliderPlugin.tsx +16 -2
  599. package/src/plugins/impl/RefreshPlugin.tsx +1 -0
  600. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  601. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +471 -1
  602. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  603. package/src/plugins/impl/__tests__/RangeSliderPlugin.test.tsx +109 -0
  604. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  605. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +153 -0
  606. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  607. package/src/plugins/impl/anywidget/model.ts +3 -1
  608. package/src/plugins/impl/anywidget/registry.ts +18 -14
  609. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  610. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  611. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  612. package/src/plugins/impl/chat/chat-ui.tsx +14 -11
  613. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  614. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  615. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  616. package/src/plugins/impl/data-explorer/ConnectedDataExplorerComponent.tsx +1 -1
  617. package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
  618. package/src/plugins/impl/data-explorer/state/reducer.ts +1 -1
  619. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -2
  620. package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
  621. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +66 -66
  622. package/src/plugins/impl/data-frames/panel.tsx +43 -16
  623. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  624. package/src/plugins/impl/panel/PanelPlugin.tsx +16 -17
  625. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  626. package/src/plugins/impl/panel/utils.ts +17 -0
  627. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +2 -5
  628. package/src/plugins/impl/plotly/selection.ts +25 -19
  629. package/src/plugins/impl/vega/VegaPlugin.tsx +2 -3
  630. package/src/plugins/impl/vega/make-selectable.ts +4 -7
  631. package/src/plugins/layout/RoutesPlugin.tsx +1 -1
  632. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  633. package/src/stories/button.stories.tsx +1 -0
  634. package/src/stories/cell.stories.tsx +2 -5
  635. package/src/stories/data-table.stories.tsx +31 -0
  636. package/src/stories/editor.stories.tsx +24 -17
  637. package/src/stories/form-wrapper.stories.tsx +2 -2
  638. package/src/stories/layout/vertical/one-column.stories.tsx +2 -5
  639. package/src/stories/runtime-story.tsx +23 -17
  640. package/src/stories/select.stories.tsx +2 -2
  641. package/src/stories/switchable-multi-select.stories.tsx +1 -1
  642. package/src/utils/__tests__/arrays.test.ts +35 -0
  643. package/src/utils/__tests__/batch-requests.test.ts +3 -3
  644. package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +29 -0
  645. package/src/utils/__tests__/dates.test.ts +116 -0
  646. package/src/utils/__tests__/numbers.test.ts +105 -1
  647. package/src/utils/__tests__/objects.test.ts +21 -0
  648. package/src/utils/arrays.ts +11 -2
  649. package/src/utils/blob.ts +1 -1
  650. package/src/utils/createReducer.ts +53 -26
  651. package/src/utils/dates.ts +94 -0
  652. package/src/utils/maps.ts +0 -3
  653. package/src/utils/numbers.ts +79 -18
  654. package/src/utils/objects.ts +4 -9
  655. package/src/utils/once.ts +19 -19
  656. package/src/utils/tracer.ts +9 -5
  657. package/src/utils/zod-utils.ts +1 -3
  658. package/dist/any-language-editor-B9REGlD9.js +0 -76
  659. package/dist/assets/__vite-browser-external-BXYkiFsk.js +0 -1
  660. package/dist/assets/worker-HPY9fbhz.js +0 -114
  661. package/dist/error-entries-CzoYkIQZ.js +0 -56
  662. package/dist/feedback-button-fND8YT59.js +0 -380
  663. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  664. package/dist/precisionRound-DMIVQrt2.js +0 -97
  665. package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
  666. package/src/components/editor/navigation/state.ts +0 -65
  667. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +0 -656
  668. package/src/core/ai/tools/run-cells-tool.ts +0 -303
  669. package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
  670. package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
  671. package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
  672. package/src/core/cells/document-changes.ts +0 -944
  673. package/src/core/host/AGENTS.md +0 -6
  674. package/src/core/host/__tests__/protocol.test.ts +0 -214
  675. package/src/core/host/protocol.ts +0 -1943
  676. package/src/core/state/__tests__/jotai.test.ts +0 -25
  677. package/src/core/wasm/worker/message-buffer.ts +0 -33
  678. package/src/core/websocket/__tests__/useWebSocket.test.ts +0 -49
  679. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  680. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -338
  681. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  682. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  683. package/src/core/websocket/transports/basic.ts +0 -102
  684. package/src/core/websocket/transports/sse.ts +0 -308
  685. package/src/core/websocket/transports/transport.ts +0 -61
  686. package/src/core/websocket/transports/ws.ts +0 -98
  687. package/src/core/websocket/useWebSocket.tsx +0 -88
  688. package/src/utils/__tests__/id-tree.test.ts +0 -2145
  689. package/src/utils/id-tree.tsx +0 -1134
  690. /package/src/core/{websocket → session}/connection-utils.ts +0 -0
@@ -4,10 +4,11 @@ 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,
@@ -17,16 +18,12 @@ import {
17
18
  import { MockNotebook } from "@/__mocks__/notebook";
18
19
  import { cellId } from "@/__tests__/branded";
19
20
  import type { CellHandle } from "@/components/editor/notebook-cell";
20
- import { CellId, SCRATCH_CELL_ID, SETUP_CELL_ID } from "@/core/cells/ids";
21
+ import { CellId, SCRATCH_CELL_ID } from "@/core/cells/ids";
21
22
  import { foldAllBulk, unfoldAllBulk } from "@/core/codemirror/editing/commands";
22
23
  import { adaptiveLanguageConfiguration } from "@/core/codemirror/language/extension";
23
24
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
24
25
  import type { OutputMessage } from "@/core/kernel/messages";
25
- import {
26
- type CollapsibleTree,
27
- MultiColumn,
28
- type CellColumnId,
29
- } from "@/utils/id-tree";
26
+ import type { CellConfig } from "@/core/network/types";
30
27
  import type { Seconds } from "@/utils/time";
31
28
  import {
32
29
  exportedForTesting,
@@ -34,13 +31,12 @@ import {
34
31
  type NotebookState,
35
32
  notebookAtom,
36
33
  } from "../cells";
37
- import { exportedForTesting as documentTransactionTestExports } from "../document-changes";
38
34
  import {
39
- focusAndScrollCellIntoView,
40
- scrollToBottom,
41
- scrollToTop,
42
- } from "../scrollCellIntoView";
43
- import type { CellData } from "../types";
35
+ type CellData,
36
+ createCell,
37
+ createCellConfig,
38
+ createCellRuntimeState,
39
+ } from "../types";
44
40
 
45
41
  vi.mock("@/core/codemirror/editing/commands", () => ({
46
42
  foldAllBulk: vi.fn(),
@@ -60,9 +56,6 @@ vi.mock("../scrollCellIntoView", async (importOriginal) => {
60
56
  };
61
57
  });
62
58
 
63
- const FIRST_COLUMN = 0;
64
- const SECOND_COLUMN = 1;
65
-
66
59
  const { initialNotebookState, reducer, createActions } = exportedForTesting;
67
60
 
68
61
  function formatCells(notebook: NotebookState) {
@@ -78,14 +71,7 @@ function formatCells(notebook: NotebookState) {
78
71
  const cells = cellsId.map((cellId) => cellData[cellId]);
79
72
  return cells.map((cell) => printCell(cell)).join("\n\n");
80
73
  };
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));
74
+ return wrap(printCells(cellIds));
89
75
  }
90
76
 
91
77
  function createEditor(content: string) {
@@ -121,8 +107,7 @@ describe("cell reducer", () => {
121
107
  let cells: ReturnType<typeof flattenTopLevelNotebookCells>;
122
108
  let firstCellId: CellId;
123
109
 
124
- const actions = createActions((action) => {
125
- state = reducer(state, action);
110
+ const mountEditors = () => {
126
111
  for (const [cellIdString, handle] of Object.entries(state.cellHandles)) {
127
112
  // @ts-expect-error - Typescript Object.entries doesn't know that keys are CellId
128
113
  const cid: CellId = cellIdString;
@@ -136,7 +121,62 @@ describe("cell reducer", () => {
136
121
  }
137
122
  }
138
123
  cells = flattenTopLevelNotebookCells(state);
139
- });
124
+ };
125
+ const actions = createActions((action) => {
126
+ state = reducer(state, action);
127
+ mountEditors();
128
+ });
129
+
130
+ /** Put a cell into the notebook the way the document would. */
131
+ function seedCell({
132
+ cellId,
133
+ before,
134
+ code = "",
135
+ name,
136
+ config,
137
+ newCellId,
138
+ hideCode,
139
+ autoFocus = true,
140
+ }: {
141
+ cellId: CellId | "__end__";
142
+ before: boolean;
143
+ code?: string;
144
+ name?: string;
145
+ config?: CellConfig;
146
+ newCellId?: CellId;
147
+ hideCode?: boolean;
148
+ autoFocus?: boolean;
149
+ }): CellId {
150
+ const cellIndex =
151
+ cellId === "__end__"
152
+ ? state.cellIds.length
153
+ : state.cellIds.indexOf(cellId);
154
+ const id = newCellId ?? CellId.create();
155
+ const cellIds = [...state.cellIds];
156
+ cellIds.splice(before ? cellIndex : cellIndex + 1, 0, id);
157
+ state = {
158
+ ...state,
159
+ cellIds,
160
+ cellData: {
161
+ ...state.cellData,
162
+ [id]: createCell({
163
+ id,
164
+ code,
165
+ name,
166
+ config: createCellConfig({
167
+ ...config,
168
+ ...(hideCode != null && { hide_code: hideCode }),
169
+ }),
170
+ edited: Boolean(code),
171
+ }),
172
+ },
173
+ cellRuntime: { ...state.cellRuntime, [id]: createCellRuntimeState() },
174
+ cellHandles: { ...state.cellHandles, [id]: createRef() },
175
+ scrollKey: autoFocus ? id : null,
176
+ };
177
+ mountEditors();
178
+ return id;
179
+ }
140
180
 
141
181
  let i = 0;
142
182
  const originalCreate = CellId.create.bind(CellId);
@@ -151,644 +191,79 @@ describe("cell reducer", () => {
151
191
  i = 0;
152
192
 
153
193
  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();
194
+ seedCell({ cellId: "__end__", before: false });
195
+ firstCellId = state.cellIds[0];
161
196
  });
162
197
 
163
198
  afterAll(() => {
164
199
  CellId.create = originalCreate;
165
200
  });
166
201
 
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
- it("can update a cell", () => {
343
- actions.updateCellCode({
344
- cellId: firstCellId,
345
- code: "import numpy as np",
346
- formattingChange: false,
347
- });
348
- expect(formatCells(state)).toMatchInlineSnapshot(`
349
- "
350
- [0] 'import numpy as np'
351
- "
352
- `);
353
- });
354
-
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();
202
+ it("can update a cell", () => {
203
+ actions.updateCellCode({
204
+ cellId: firstCellId,
205
+ code: "import numpy as np",
206
+ formattingChange: false,
207
+ });
781
208
  expect(formatCells(state)).toMatchInlineSnapshot(`
782
209
  "
783
- [0] ''
784
-
785
- [1] ''
786
-
787
- [3] ''
210
+ [0] 'import numpy as np'
788
211
  "
789
212
  `);
790
213
  });
791
214
 
215
+ it("keeps error evidence after an edit and a new queued run", () => {
216
+ const code = "raise ValueError('old failure')";
217
+ const output: OutputMessage = {
218
+ channel: "marimo-error",
219
+ mimetype: "application/vnd.marimo+error",
220
+ data: [
221
+ {
222
+ type: "exception",
223
+ msg: "old failure",
224
+ exception_type: "ValueError",
225
+ raising_cell: null,
226
+ traceback: "<pre>ValueError: old failure</pre>",
227
+ },
228
+ ],
229
+ code,
230
+ timestamp: 1,
231
+ };
232
+ actions.handleCellMessage({
233
+ cell_id: firstCellId,
234
+ output,
235
+ console: {
236
+ channel: "stderr",
237
+ mimetype: "application/vnd.marimo+traceback",
238
+ data: "<pre>ValueError: old failure</pre>",
239
+ code,
240
+ timestamp: 1,
241
+ },
242
+ status: "idle",
243
+ stale_inputs: null,
244
+ timestamp: 1,
245
+ });
246
+ actions.updateCellCode({
247
+ cellId: firstCellId,
248
+ code: "print('new code')",
249
+ formattingChange: false,
250
+ });
251
+ actions.prepareForRun({ cellId: firstCellId });
252
+ actions.handleCellMessage({
253
+ cell_id: firstCellId,
254
+ output: null,
255
+ console: null,
256
+ status: "queued",
257
+ stale_inputs: null,
258
+ timestamp: 2,
259
+ });
260
+ const runtime = state.cellRuntime[firstCellId];
261
+ expect(state.cellData[firstCellId].lastCodeRun).toBe("print('new code')");
262
+ expect(runtime.output).toEqual(output);
263
+ expect(runtime.consoleOutputs[0].code).toBe(code);
264
+ expect(runtime.consoleOutputs[0].data).toContain("old failure");
265
+ });
266
+
792
267
  it("can run cell and receive cell messages", () => {
793
268
  // HAPPY PATH
794
269
  /////////////////
@@ -1120,7 +595,7 @@ describe("cell reducer", () => {
1120
595
  formattingChange: false,
1121
596
  });
1122
597
  // Add cell
1123
- actions.createNewCell({
598
+ seedCell({
1124
599
  cellId: firstCellId,
1125
600
  before: false,
1126
601
  });
@@ -1225,33 +700,6 @@ describe("cell reducer", () => {
1225
700
  expect(cell.edited).toBe(false);
1226
701
  });
1227
702
 
1228
- it("can update a cells config", () => {
1229
- const firstCellId = cells[0].id;
1230
- let cell = cells[0];
1231
- // Starts empty
1232
- expect(cell.config).toEqual({
1233
- disabled: false,
1234
- hide_code: false,
1235
- column: null,
1236
- expand_output: false,
1237
- });
1238
-
1239
- actions.updateCellConfig({
1240
- cellId: firstCellId,
1241
- config: { disabled: true },
1242
- });
1243
- cell = cells[0];
1244
- expect(cell.config.disabled).toBe(true);
1245
-
1246
- // Revert
1247
- actions.updateCellConfig({
1248
- cellId: firstCellId,
1249
- config: { disabled: false },
1250
- });
1251
- cell = cells[0];
1252
- expect(cell.config.disabled).toBe(false);
1253
- });
1254
-
1255
703
  it("can run a stopped cell", () => {
1256
704
  // Update code of first cell
1257
705
  actions.updateCellCode({
@@ -1527,7 +975,9 @@ describe("cell reducer", () => {
1527
975
  response: secret,
1528
976
  });
1529
977
 
1530
- expect(cells[0].consoleOutputs).toEqual([{ ...prompt, response: "" }]);
978
+ expect(cells[0].consoleOutputs).toEqual([
979
+ { ...prompt, id: 1, response: "" },
980
+ ]);
1531
981
  expect(JSON.stringify(cells[0].consoleOutputs)).not.toContain(secret);
1532
982
  });
1533
983
 
@@ -1636,619 +1086,49 @@ describe("cell reducer", () => {
1636
1086
  });
1637
1087
  cell = cells[0];
1638
1088
  expect(cell.status).toBe("running");
1639
- expect(cell.consoleOutputs).toMatchObject([]);
1640
- expect(cell).toMatchSnapshot("console cleared while running");
1641
-
1642
- // Add console
1643
- actions.handleCellMessage({
1644
- cell_id: firstCellId,
1645
- output: undefined,
1646
- console: [STDOUT],
1647
- status: "idle",
1648
- stale_inputs: null,
1649
- timestamp: new Date(22).getTime() as Seconds,
1650
- });
1651
- cell = cells[0];
1652
- expect(cell.consoleOutputs).toMatchObject([STDOUT]);
1653
- expect(cell.status).toBe("idle");
1654
- expect(cell).toMatchSnapshot("console received while idle");
1655
- });
1656
-
1657
- it("can send a cell to the top", () => {
1658
- actions.createNewCell({ cellId: firstCellId, before: false });
1659
- actions.createNewCell({ cellId: cellId("1"), before: false });
1660
- actions.sendToTop({ cellId: cellId("2") });
1661
- expect(formatCells(state)).toMatchInlineSnapshot(`
1662
- "
1663
- [2] ''
1664
-
1665
- [0] ''
1666
-
1667
- [1] ''
1668
- "
1669
- `);
1670
- });
1671
-
1672
- it("can send a cell to the bottom", () => {
1673
- actions.createNewCell({ cellId: firstCellId, before: false });
1674
- actions.createNewCell({ cellId: cellId("1"), before: false });
1675
- actions.sendToBottom({ cellId: firstCellId });
1676
- expect(formatCells(state)).toMatchInlineSnapshot(`
1677
- "
1678
- [1] ''
1679
-
1680
- [2] ''
1681
-
1682
- [0] ''
1683
- "
1684
- `);
1685
- });
1686
-
1687
- it("can focus cells", () => {
1688
- actions.createNewCell({ cellId: firstCellId, before: false });
1689
- actions.createNewCell({ cellId: cellId("1"), before: false });
1690
-
1691
- actions.focusCell({ cellId: cellId("1"), where: "before" });
1692
- expect(focusAndScrollCellIntoView).toHaveBeenCalledWith(
1693
- expect.objectContaining({
1694
- cellId: cellId("0"),
1695
- }),
1696
- );
1697
-
1698
- actions.focusTopCell();
1699
- expect(scrollToTop).toHaveBeenCalled();
1700
-
1701
- actions.focusBottomCell();
1702
- expect(scrollToBottom).toHaveBeenCalled();
1703
- });
1704
-
1705
- it("can update cell name", () => {
1706
- actions.updateCellName({ cellId: firstCellId, name: "Test Cell" });
1707
- expect(state.cellData[firstCellId].name).toBe("Test Cell");
1708
- });
1709
-
1710
- it("can set cell IDs and codes", () => {
1711
- const newIds = [cellId("3"), cellId("4"), cellId("5")];
1712
- const newCodes = ["code1", "code2", "code3"];
1713
-
1714
- actions.setCellIds({ cellIds: newIds });
1715
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual(newIds);
1716
-
1717
- // When codeIsStale is false, lastCodeRun should match code
1718
- actions.setCellCodes({
1719
- codes: newCodes,
1720
- ids: newIds,
1721
- codeIsStale: false,
1722
- });
1723
- newIds.forEach((id, index) => {
1724
- expect(state.cellData[id].code).toBe(newCodes[index]);
1725
- expect(state.cellData[id].lastCodeRun).toBe(newCodes[index]);
1726
- expect(state.cellData[id].edited).toBe(false);
1727
- });
1728
-
1729
- // When codeIsStale is true, lastCodeRun should not change
1730
- const staleCodes = ["stale1", "stale2", "stale3"];
1731
- actions.setCellCodes({
1732
- codes: staleCodes,
1733
- ids: newIds,
1734
- codeIsStale: true,
1735
- });
1736
- newIds.forEach((id, index) => {
1737
- expect(state.cellData[id].code).toBe(staleCodes[index]);
1738
- expect(state.cellData[id].lastCodeRun).toBe(newCodes[index]);
1739
- expect(state.cellData[id].edited).toBe(true);
1740
- });
1741
- });
1742
-
1743
- it("can can add a new cell with/without stale code", () => {
1744
- actions.setCellCodes({
1745
- codes: ["new code"],
1746
- ids: [cellId("2")],
1747
- codeIsStale: false,
1748
- });
1749
-
1750
- expect(state.cellData[cellId("2")].code).toBe("new code");
1751
- expect(state.cellData[cellId("2")].edited).toBe(false);
1752
- expect(state.cellData[cellId("2")].lastCodeRun).toBe("new code");
1753
-
1754
- actions.setCellCodes({
1755
- codes: ["new code 2"],
1756
- ids: [cellId("9")],
1757
- codeIsStale: true,
1758
- });
1759
-
1760
- expect(state.cellData[cellId("9")].code).toBe("new code 2");
1761
- expect(state.cellData[cellId("9")].edited).toBe(true);
1762
- expect(state.cellData[cellId("9")].lastCodeRun).toBe(null);
1763
- });
1764
-
1765
- it("can partial update cell codes", () => {
1766
- actions.createNewCell({ cellId: firstCellId, before: false });
1767
- actions.createNewCell({ cellId: cellId("1"), before: false });
1768
-
1769
- expect(state.cellIds.inOrderIds).toEqual([
1770
- cellId("0"),
1771
- cellId("1"),
1772
- cellId("2"),
1773
- ]);
1774
- expect(state.cellData[cellId("0")].code).toBe("");
1775
- expect(state.cellData[cellId("1")].code).toBe("");
1776
- expect(state.cellData[cellId("2")].code).toBe("");
1777
-
1778
- // Update cell 1
1779
- actions.setCellCodes({
1780
- codes: ["new code 2"],
1781
- ids: [cellId("1")],
1782
- codeIsStale: false,
1783
- });
1784
-
1785
- expect(state.cellIds.inOrderIds).toEqual([
1786
- cellId("0"),
1787
- cellId("1"),
1788
- cellId("2"),
1789
- ]);
1790
- expect(state.cellData[cellId("0")].code).toBe("");
1791
- expect(state.cellData[cellId("1")].code).toBe("new code 2");
1792
- expect(state.cellData[cellId("1")].edited).toBe(false);
1793
- expect(state.cellData[cellId("2")].code).toBe("");
1794
- });
1795
-
1796
- it("can set cell codes with new cell ids, while preserving the old cell data", () => {
1797
- actions.setCellCodes({
1798
- codes: ["code1", "code2", "code3"],
1799
- ids: [cellId("3"), cellId("4"), cellId("5")],
1800
- codeIsStale: false,
1801
- });
1802
- expect(state.cellData[cellId("3")].code).toBe("code1");
1803
- expect(state.cellData[cellId("4")].code).toBe("code2");
1804
- expect(state.cellData[cellId("5")].code).toBe("code3");
1805
-
1806
- // Update with some new cell ids and some old cell ids
1807
- actions.setCellIds({
1808
- cellIds: [cellId("1"), cellId("2"), cellId("3"), cellId("4")],
1809
- });
1810
- actions.setCellCodes({
1811
- codes: ["new1", "new2", "code1", "code2"],
1812
- ids: [cellId("1"), cellId("2"), cellId("3"), cellId("4")],
1813
- codeIsStale: false,
1814
- });
1815
- expect(state.cellData[cellId("1")].code).toBe("new1");
1816
- expect(state.cellData[cellId("2")].code).toBe("new2");
1817
- expect(state.cellData[cellId("3")].code).toBe("code1");
1818
- expect(state.cellData[cellId("4")].code).toBe("code2");
1819
- expect(state.cellIds.inOrderIds).toEqual([
1820
- cellId("1"),
1821
- cellId("2"),
1822
- cellId("3"),
1823
- cellId("4"),
1824
- ]);
1825
- // Cell 5 data is preserved (possibly used for tracing), but it's not in the cellIds
1826
- expect(state.cellData[cellId("5")]).not.toBeUndefined();
1827
- });
1828
-
1829
- it("can set cell codes with names and configs", () => {
1830
- const newIds = [cellId("3"), cellId("4")];
1831
- actions.setCellIds({ cellIds: newIds });
1832
- actions.setCellCodes({
1833
- codes: ["code1", "code2"],
1834
- ids: newIds,
1835
- codeIsStale: false,
1836
- names: ["setup_cell", "analysis"],
1837
- configs: [
1838
- { hide_code: true, disabled: false, column: null },
1839
- { hide_code: false, disabled: true, column: null },
1840
- ],
1841
- });
1842
-
1843
- expect(state.cellData[cellId("3")].name).toBe("setup_cell");
1844
- expect(state.cellData[cellId("3")].config.hide_code).toBe(true);
1845
- expect(state.cellData[cellId("3")].config.disabled).toBe(false);
1846
-
1847
- expect(state.cellData[cellId("4")].name).toBe("analysis");
1848
- expect(state.cellData[cellId("4")].config.hide_code).toBe(false);
1849
- expect(state.cellData[cellId("4")].config.disabled).toBe(true);
1850
- });
1851
-
1852
- it("can set cell codes without names/configs (backward compat)", () => {
1853
- const newIds = [cellId("3")];
1854
- actions.setCellIds({ cellIds: newIds });
1855
- actions.setCellCodes({
1856
- codes: ["code1"],
1857
- ids: newIds,
1858
- codeIsStale: false,
1859
- });
1860
-
1861
- // Should use defaults when names/configs not provided
1862
- expect(state.cellData[cellId("3")].code).toBe("code1");
1863
- expect(state.cellData[cellId("3")].config.hide_code).toBe(false);
1864
- expect(state.cellData[cellId("3")].config.disabled).toBe(false);
1865
- });
1866
-
1867
- it("can update names and configs on existing cells via setCellCodes", () => {
1868
- // Set initial state
1869
- actions.setCellCodes({
1870
- codes: ["x = 1"],
1871
- ids: [firstCellId],
1872
- codeIsStale: false,
1873
- names: ["old_name"],
1874
- configs: [{ hide_code: false, disabled: false, column: null }],
1875
- });
1876
- expect(state.cellData[firstCellId].name).toBe("old_name");
1877
- expect(state.cellData[firstCellId].config.hide_code).toBe(false);
1878
-
1879
- // Update with new name and config (same code)
1880
- actions.setCellCodes({
1881
- codes: ["x = 1"],
1882
- ids: [firstCellId],
1883
- codeIsStale: true,
1884
- names: ["new_name"],
1885
- configs: [{ hide_code: true, disabled: false, column: null }],
1886
- });
1887
- expect(state.cellData[firstCellId].name).toBe("new_name");
1888
- expect(state.cellData[firstCellId].config.hide_code).toBe(true);
1889
- });
1890
-
1891
- it("can fold and unfold all cells", () => {
1892
- actions.foldAll();
1893
- expect(foldAllBulk).toHaveBeenCalled();
1894
-
1895
- actions.unfoldAll();
1896
- expect(unfoldAllBulk).toHaveBeenCalled();
1897
- });
1898
-
1899
- it("can clear logs", () => {
1900
- state.cellLogs = [
1901
- {
1902
- level: "stderr",
1903
- message: "log1",
1904
- timestamp: 0,
1905
- source: { type: "cell", cellId: firstCellId },
1906
- },
1907
- {
1908
- level: "stderr",
1909
- message: "log1",
1910
- timestamp: 0,
1911
- source: { type: "cell", cellId: firstCellId },
1912
- },
1913
- ];
1914
- actions.clearLogs();
1915
- expect(state.cellLogs).toEqual([]);
1916
- });
1917
-
1918
- it("can collapse and expand cells", () => {
1919
- actions.createNewCell({ cellId: firstCellId, before: false });
1920
- actions.createNewCell({
1921
- cellId: cellId("1"),
1922
- before: false,
1923
- code: "# Header",
1924
- });
1925
- actions.createNewCell({
1926
- cellId: cellId("2"),
1927
- before: false,
1928
- code: "## Subheader",
1929
- });
1930
-
1931
- const id = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
1932
- state.cellRuntime[id] = {
1933
- ...state.cellRuntime[id],
1934
- outline: {
1935
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
1936
- },
1937
- };
1938
-
1939
- actions.collapseCell({ cellId: id });
1940
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(id)).toBe(true);
1941
-
1942
- actions.expandCell({ cellId: id });
1943
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(id)).toBe(false);
1944
- });
1945
-
1946
- it("can collapse and expand all cells in multiple columns", () => {
1947
- actions.createNewCell({ cellId: firstCellId, before: false });
1948
- actions.createNewCell({
1949
- cellId: cellId("1"),
1950
- before: false,
1951
- code: "# First Column Header",
1952
- });
1953
- actions.createNewCell({
1954
- cellId: cellId("2"),
1955
- before: false,
1956
- code: "## First Column Subheader",
1957
- });
1958
-
1959
- actions.addColumnBreakpoint({ cellId: cellId("2") });
1960
-
1961
- actions.createNewCell({
1962
- cellId: cellId("3"),
1963
- before: false,
1964
- code: "# Second Column Header",
1965
- });
1966
- actions.createNewCell({
1967
- cellId: cellId("4"),
1968
- before: false,
1969
- code: "## Second Column Subheader",
1970
- });
1971
-
1972
- const firstColumnHeaderId = state.cellIds
1973
- .atOrThrow(FIRST_COLUMN)
1974
- .atOrThrow(1);
1975
- state.cellRuntime[firstColumnHeaderId] = {
1976
- ...state.cellRuntime[firstColumnHeaderId],
1977
- outline: {
1978
- items: [
1979
- { name: "First Column Header", level: 1, by: { id: "header" } },
1980
- ],
1981
- },
1982
- };
1983
-
1984
- const secondColumnHeaderId = state.cellIds
1985
- .atOrThrow(SECOND_COLUMN)
1986
- .atOrThrow(0);
1987
- state.cellRuntime[secondColumnHeaderId] = {
1988
- ...state.cellRuntime[secondColumnHeaderId],
1989
- outline: {
1990
- items: [
1991
- { name: "Second Column Header", level: 1, by: { id: "header" } },
1992
- ],
1993
- },
1994
- };
1995
-
1996
- actions.collapseAllCells();
1997
- expect(
1998
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstColumnHeaderId),
1999
- ).toBe(true);
2000
- expect(
2001
- state.cellIds.atOrThrow(SECOND_COLUMN).isCollapsed(secondColumnHeaderId),
2002
- ).toBe(true);
2003
-
2004
- actions.expandAllCells();
2005
- expect(
2006
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstColumnHeaderId),
2007
- ).toBe(false);
2008
- expect(
2009
- state.cellIds.atOrThrow(SECOND_COLUMN).isCollapsed(secondColumnHeaderId),
2010
- ).toBe(false);
2011
- });
2012
-
2013
- it("can collapse and expand nested cells in one call", () => {
2014
- actions.createNewCell({ cellId: firstCellId, before: false });
2015
- actions.createNewCell({
2016
- cellId: cellId("1"),
2017
- before: false,
2018
- code: "# Header",
2019
- });
2020
- actions.createNewCell({
2021
- cellId: cellId("2"),
2022
- before: false,
2023
- code: "## Subheader",
2024
- });
2025
- actions.createNewCell({
2026
- cellId: cellId("3"),
2027
- before: false,
2028
- code: "### Subsubheader",
2029
- });
2030
-
2031
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2032
- state.cellRuntime[headerId] = {
2033
- ...state.cellRuntime[headerId],
2034
- outline: {
2035
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2036
- },
2037
- };
2038
-
2039
- const subheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2040
- state.cellRuntime[subheaderId] = {
2041
- ...state.cellRuntime[subheaderId],
2042
- outline: {
2043
- items: [{ name: "Subheader", level: 2, by: { id: "subheader" } }],
2044
- },
2045
- };
2046
-
2047
- // Check if both the parent and child are collapsed
2048
- actions.collapseAllCells();
2049
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2050
- true,
2051
- );
2052
- actions.expandCell({ cellId: headerId });
2053
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2054
- true,
2055
- );
2056
- actions.collapseCell({ cellId: headerId });
2057
-
2058
- // Check if both the parent and child are expanded
2059
- actions.expandAllCells();
2060
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2061
- false,
2062
- );
2063
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2064
- false,
2065
- );
2066
- });
2067
-
2068
- it("can collapse all cells when nested heading ranges end at the same cell", () => {
2069
- actions.createNewCell({ cellId: firstCellId, before: false });
2070
- actions.createNewCell({
2071
- cellId: cellId("1"),
2072
- before: false,
2073
- code: "# Header",
2074
- });
2075
- actions.createNewCell({
2076
- cellId: cellId("2"),
2077
- before: false,
2078
- code: "## Subheader",
2079
- });
2080
- actions.createNewCell({
2081
- cellId: cellId("3"),
2082
- before: false,
2083
- code: "### Subsubheader",
2084
- });
2085
-
2086
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2087
- state.cellRuntime[headerId] = {
2088
- ...state.cellRuntime[headerId],
2089
- outline: {
2090
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2091
- },
2092
- };
2093
-
2094
- const subheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2095
- state.cellRuntime[subheaderId] = {
2096
- ...state.cellRuntime[subheaderId],
2097
- outline: {
2098
- items: [{ name: "Subheader", level: 2, by: { id: "subheader" } }],
2099
- },
2100
- };
2101
-
2102
- const subsubheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2103
- state.cellRuntime[subsubheaderId] = {
2104
- ...state.cellRuntime[subsubheaderId],
2105
- outline: {
2106
- items: [{ name: "Subsubheader", level: 3, by: { id: "subsubheader" } }],
2107
- },
2108
- };
2109
-
2110
- // The header, subheader and subsubheader ranges all end at the last
2111
- // cell. Collapsing all used to throw "Node ... not found in tree" and
2112
- // leave the whole notebook unchanged.
2113
- actions.collapseAllCells();
2114
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2115
- true,
2116
- );
2117
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2118
- firstCellId,
2119
- headerId,
2120
- ]);
2121
-
2122
- // Each level is nested under its parent, collapsed
2123
- actions.expandCell({ cellId: headerId });
2124
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2125
- true,
2126
- );
2127
- actions.expandCell({ cellId: subheaderId });
2128
- expect(
2129
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsubheaderId),
2130
- ).toBe(true);
2131
- });
2132
-
2133
- it("can collapse all cells when a deep chain is followed by a later section", () => {
2134
- actions.createNewCell({ cellId: firstCellId, before: false });
2135
- actions.createNewCell({
2136
- cellId: cellId("1"),
2137
- before: false,
2138
- code: "# Header",
2139
- });
2140
- actions.createNewCell({
2141
- cellId: cellId("2"),
2142
- before: false,
2143
- code: "## Section A",
2144
- });
2145
- actions.createNewCell({
2146
- cellId: cellId("3"),
2147
- before: false,
2148
- code: "### Sub of A",
2149
- });
2150
- actions.createNewCell({
2151
- cellId: cellId("4"),
2152
- before: false,
2153
- code: "## Section B",
2154
- });
2155
-
2156
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2157
- state.cellRuntime[headerId] = {
2158
- ...state.cellRuntime[headerId],
2159
- outline: {
2160
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2161
- },
2162
- };
2163
-
2164
- const sectionAId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2165
- state.cellRuntime[sectionAId] = {
2166
- ...state.cellRuntime[sectionAId],
2167
- outline: {
2168
- items: [{ name: "Section A", level: 2, by: { id: "section-a" } }],
2169
- },
2170
- };
2171
-
2172
- const subsectionId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2173
- state.cellRuntime[subsectionId] = {
2174
- ...state.cellRuntime[subsectionId],
2175
- outline: {
2176
- items: [{ name: "Sub of A", level: 3, by: { id: "sub-of-a" } }],
2177
- },
2178
- };
2179
-
2180
- const sectionBId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(4);
2181
- state.cellRuntime[sectionBId] = {
2182
- ...state.cellRuntime[sectionBId],
2183
- outline: {
2184
- items: [{ name: "Section B", level: 2, by: { id: "section-b" } }],
2185
- },
2186
- };
2187
-
2188
- actions.collapseAllCells();
2189
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2190
- true,
2191
- );
2192
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2193
- firstCellId,
2194
- headerId,
2195
- ]);
1089
+ expect(cell.consoleOutputs).toMatchObject([]);
1090
+ expect(cell).toMatchSnapshot("console cleared while running");
2196
1091
 
2197
- // Both sections sit under the header, collapsed, with the subsection
2198
- // nested under section A
2199
- actions.expandCell({ cellId: headerId });
2200
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionAId)).toBe(
2201
- true,
2202
- );
2203
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionBId)).toBe(
2204
- true,
2205
- );
2206
- actions.expandCell({ cellId: sectionAId });
2207
- expect(
2208
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsectionId),
2209
- ).toBe(true);
1092
+ // Add console
1093
+ actions.handleCellMessage({
1094
+ cell_id: firstCellId,
1095
+ output: undefined,
1096
+ console: [STDOUT],
1097
+ status: "idle",
1098
+ stale_inputs: null,
1099
+ timestamp: new Date(22).getTime() as Seconds,
1100
+ });
1101
+ cell = cells[0];
1102
+ expect(cell.consoleOutputs).toMatchObject([STDOUT]);
1103
+ expect(cell.status).toBe("idle");
1104
+ expect(cell).toMatchSnapshot("console received while idle");
2210
1105
  });
2211
1106
 
2212
- it("can show hidden cells", () => {
2213
- actions.createNewCell({ cellId: firstCellId, before: false });
2214
- actions.createNewCell({ cellId: cellId("1"), before: false });
2215
- actions.collapseCell({ cellId: firstCellId });
1107
+ it("can fold and unfold all cells", () => {
1108
+ actions.foldAll();
1109
+ expect(foldAllBulk).toHaveBeenCalled();
2216
1110
 
2217
- actions.showCellIfHidden({ cellId: cellId("1") });
2218
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstCellId)).toBe(
2219
- false,
2220
- );
1111
+ actions.unfoldAll();
1112
+ expect(unfoldAllBulk).toHaveBeenCalled();
2221
1113
  });
2222
1114
 
2223
- it("can split and undo split cells", () => {
2224
- actions.createNewCell({
2225
- cellId: firstCellId,
2226
- before: false,
2227
- code: "line1\nline2",
2228
- });
2229
- const nextCellId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2230
-
2231
- const originalCellCount = state.cellIds.atOrThrow(FIRST_COLUMN).length;
2232
- // Move cursor to the second line
2233
- const editor = state.cellHandles[nextCellId].current?.editorView;
2234
- if (!editor) {
2235
- throw new Error("Editor not found");
2236
- }
2237
- editor.dispatch({ selection: { anchor: 5, head: 5 } });
2238
- actions.splitCell({ cellId: nextCellId });
2239
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).length).toBe(
2240
- originalCellCount + 1,
2241
- );
2242
- expect(state.cellData[nextCellId].code).toBe("line1");
2243
- expect(
2244
- state.cellData[state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2)].code,
2245
- ).toBe("line2");
2246
-
2247
- actions.undoSplitCell({ cellId: nextCellId, snapshot: "line1\nline2" });
2248
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).length).toBe(
2249
- originalCellCount,
2250
- );
2251
- expect(state.cellData[nextCellId].code).toBe("line1\nline2");
1115
+ it("can clear logs", () => {
1116
+ state.cellLogs = [
1117
+ {
1118
+ level: "stderr",
1119
+ message: "log1",
1120
+ timestamp: 0,
1121
+ source: { type: "cell", cellId: firstCellId },
1122
+ },
1123
+ {
1124
+ level: "stderr",
1125
+ message: "log1",
1126
+ timestamp: 0,
1127
+ source: { type: "cell", cellId: firstCellId },
1128
+ },
1129
+ ];
1130
+ actions.clearLogs();
1131
+ expect(state.cellLogs).toEqual([]);
2252
1132
  });
2253
1133
 
2254
1134
  it("can handle multiple console outputs", () => {
@@ -2287,407 +1167,6 @@ describe("cell reducer", () => {
2287
1167
  `);
2288
1168
  });
2289
1169
 
2290
- it("can add a column breakpoint", () => {
2291
- actions.createNewCell({ cellId: firstCellId, before: false });
2292
- actions.createNewCell({ cellId: cellId("1"), before: false });
2293
- actions.createNewCell({ cellId: cellId("2"), before: false });
2294
-
2295
- expect(state.cellIds.getColumns().length).toBe(1);
2296
- expect(formatCells(state)).toMatchInlineSnapshot(`
2297
- "
2298
- [0] ''
2299
-
2300
- [1] ''
2301
-
2302
- [2] ''
2303
-
2304
- [3] ''
2305
- "
2306
- `);
2307
-
2308
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2309
-
2310
- expect(state.cellIds.getColumns().length).toBe(2);
2311
- expect(formatCells(state)).toMatchInlineSnapshot(`
2312
- "
2313
- > col 0
2314
- [0] ''
2315
-
2316
- [1] ''
2317
-
2318
- > col 1
2319
- [2] ''
2320
-
2321
- [3] ''
2322
- "
2323
- `);
2324
-
2325
- // Check that the cells are in the correct columns
2326
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2327
- cellId("0"),
2328
- cellId("1"),
2329
- ]);
2330
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2331
- cellId("2"),
2332
- cellId("3"),
2333
- ]);
2334
- });
2335
-
2336
- it("cannot add a column breakpoint before the first cell", () => {
2337
- expect(state.cellIds.getColumns().length).toBe(1);
2338
- actions.createNewCell({ cellId: firstCellId, before: false });
2339
- actions.createNewCell({ cellId: cellId("1"), before: false });
2340
- actions.addColumnBreakpoint({ cellId: firstCellId });
2341
- expect(state.cellIds.getColumns().length).toBe(1);
2342
- });
2343
-
2344
- it("can delete a column", () => {
2345
- actions.createNewCell({ cellId: firstCellId, before: false });
2346
- actions.createNewCell({ cellId: cellId("1"), before: false });
2347
- actions.createNewCell({ cellId: cellId("2"), before: false });
2348
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2349
-
2350
- expect(state.cellIds.getColumns().length).toBe(2);
2351
- expect(formatCells(state)).toMatchInlineSnapshot(`
2352
- "
2353
- > col 0
2354
- [0] ''
2355
-
2356
- [1] ''
2357
-
2358
- > col 1
2359
- [2] ''
2360
-
2361
- [3] ''
2362
- "
2363
- `);
2364
-
2365
- const columnId = state.cellIds.atOrThrow(0).id;
2366
- actions.deleteColumn({ columnId: columnId });
2367
-
2368
- expect(state.cellIds.getColumns().length).toBe(1);
2369
- expect(formatCells(state)).toMatchInlineSnapshot(`
2370
- "
2371
- [2] ''
2372
-
2373
- [3] ''
2374
-
2375
- [0] ''
2376
-
2377
- [1] ''
2378
- "
2379
- `);
2380
-
2381
- // Check that all cells are now in the remaining column
2382
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2383
- "2",
2384
- "3",
2385
- "0",
2386
- "1",
2387
- ]);
2388
- });
2389
-
2390
- it("deleting the last column does nothing", () => {
2391
- actions.createNewCell({ cellId: firstCellId, before: false });
2392
- actions.createNewCell({ cellId: cellId("1"), before: false });
2393
-
2394
- const initialState = { ...state };
2395
-
2396
- actions.deleteColumn({ columnId: initialState.cellIds.atOrThrow(0).id });
2397
-
2398
- // State should not change
2399
- expect(state).toEqual(initialState);
2400
- });
2401
-
2402
- it("can drop a cell over another cell", () => {
2403
- actions.createNewCell({ cellId: firstCellId, before: false });
2404
- actions.createNewCell({ cellId: cellId("1"), before: false });
2405
- actions.createNewCell({ cellId: cellId("2"), before: false });
2406
-
2407
- expect(formatCells(state)).toMatchInlineSnapshot(`
2408
- "
2409
- [0] ''
2410
-
2411
- [1] ''
2412
-
2413
- [2] ''
2414
-
2415
- [3] ''
2416
- "
2417
- `);
2418
-
2419
- actions.dropCellOverCell({
2420
- cellId: cellId("0"),
2421
- overCellId: cellId("3"),
2422
- });
2423
-
2424
- expect(formatCells(state)).toMatchInlineSnapshot(`
2425
- "
2426
- [1] ''
2427
-
2428
- [2] ''
2429
-
2430
- [3] ''
2431
-
2432
- [0] ''
2433
- "
2434
- `);
2435
- });
2436
-
2437
- it("can drop a cell over a new column", () => {
2438
- actions.createNewCell({ cellId: firstCellId, before: false });
2439
- actions.createNewCell({ cellId: cellId("1"), before: false });
2440
-
2441
- expect(state.cellIds.getColumns().length).toBe(1);
2442
- expect(formatCells(state)).toMatchInlineSnapshot(`
2443
- "
2444
- [0] ''
2445
-
2446
- [1] ''
2447
-
2448
- [2] ''
2449
- "
2450
- `);
2451
-
2452
- actions.dropOverNewColumn({ cellId: cellId("1") });
2453
-
2454
- expect(state.cellIds.getColumns().length).toBe(2);
2455
- expect(formatCells(state)).toMatchInlineSnapshot(`
2456
- "
2457
- > col 0
2458
- [0] ''
2459
-
2460
- [2] ''
2461
-
2462
- > col 1
2463
- [1] ''
2464
- "
2465
- `);
2466
-
2467
- // Check that the cells are in the correct columns
2468
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2469
- cellId("0"),
2470
- cellId("2"),
2471
- ]);
2472
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2473
- });
2474
-
2475
- it("can drop a column over another column", () => {
2476
- actions.createNewCell({ cellId: firstCellId, before: false });
2477
- actions.createNewCell({ cellId: cellId("1"), before: false });
2478
- actions.createNewCell({ cellId: cellId("2"), before: false });
2479
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2480
-
2481
- expect(state.cellIds.getColumns().length).toBe(2);
2482
- expect(formatCells(state)).toMatchInlineSnapshot(`
2483
- "
2484
- > col 0
2485
- [0] ''
2486
-
2487
- [1] ''
2488
-
2489
- > col 1
2490
- [2] ''
2491
-
2492
- [3] ''
2493
- "
2494
- `);
2495
-
2496
- const columnId0 = state.cellIds.atOrThrow(0).id;
2497
- const columnId1 = state.cellIds.atOrThrow(1).id;
2498
- expect(columnId0).not.toBe(columnId1);
2499
- actions.moveColumn({ column: columnId1, overColumn: columnId0 });
2500
-
2501
- expect(state.cellIds.getColumns().length).toBe(2);
2502
- expect(formatCells(state)).toMatchInlineSnapshot(`
2503
- "
2504
- > col 0
2505
- [2] ''
2506
-
2507
- [3] ''
2508
-
2509
- > col 1
2510
- [0] ''
2511
-
2512
- [1] ''
2513
- "
2514
- `);
2515
-
2516
- // Check that the columns have swapped positions
2517
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2518
- cellId("2"),
2519
- cellId("3"),
2520
- ]);
2521
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2522
- cellId("0"),
2523
- cellId("1"),
2524
- ]);
2525
- });
2526
-
2527
- it("can compact columns", () => {
2528
- // Create initial state with 3 columns, including an empty one
2529
- actions.createNewCell({ cellId: firstCellId, before: false });
2530
- actions.createNewCell({ cellId: cellId("1"), before: false });
2531
- actions.addColumnBreakpoint({ cellId: cellId("1") });
2532
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2533
- actions.dropOverNewColumn({ cellId: cellId("2") });
2534
-
2535
- expect(state.cellIds.getColumns().length).toBe(4);
2536
- expect(formatCells(state)).toMatchInlineSnapshot(`
2537
- "
2538
- > col 0
2539
- [0] ''
2540
-
2541
- > col 1
2542
- [1] ''
2543
-
2544
- > col 2
2545
-
2546
-
2547
- > col 3
2548
- [2] ''
2549
- "
2550
- `);
2551
-
2552
- // Check initial column structure
2553
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2554
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2555
- expect(state.cellIds.getColumns()[2].topLevelIds).toEqual([]);
2556
- expect(state.cellIds.getColumns()[3].topLevelIds).toEqual([cellId("2")]);
2557
-
2558
- // Compact columns
2559
- actions.compactColumns();
2560
-
2561
- expect(state.cellIds.getColumns().length).toBe(3);
2562
- expect(formatCells(state)).toMatchInlineSnapshot(`
2563
- "
2564
- > col 0
2565
- [0] ''
2566
-
2567
- > col 1
2568
- [1] ''
2569
-
2570
- > col 2
2571
- [2] ''
2572
- "
2573
- `);
2574
-
2575
- // Check compacted column structure
2576
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2577
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2578
- expect(state.cellIds.getColumns()[2].topLevelIds).toEqual([cellId("2")]);
2579
- });
2580
-
2581
- it("rebuildCellColumns regroups cells by config.column", () => {
2582
- // Create four cells in a single column.
2583
- actions.createNewCell({ cellId: firstCellId, before: false });
2584
- actions.createNewCell({ cellId: cellId("1"), before: false });
2585
- actions.createNewCell({ cellId: cellId("2"), before: false });
2586
- expect(state.cellIds.getColumns().length).toBe(1);
2587
-
2588
- // Explicitly set config.column on each cell. This ONLY updates metadata —
2589
- // updateCellConfig does not touch the MultiColumn tree.
2590
- actions.updateCellConfig({
2591
- cellId: cellId("0"),
2592
- config: { column: 0 },
2593
- });
2594
- actions.updateCellConfig({
2595
- cellId: cellId("2"),
2596
- config: { column: 1 },
2597
- });
2598
-
2599
- // Tree is still a single column at this point.
2600
- expect(state.cellIds.getColumns().length).toBe(1);
2601
-
2602
- // Now rebuild the column tree from metadata.
2603
- actions.rebuildCellColumns({
2604
- cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
2605
- });
2606
-
2607
- expect(state.cellIds.getColumns().length).toBe(2);
2608
- // Cell 1 inherits from 0 (col 0), cell 3 inherits from 2 (col 1).
2609
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2610
- cellId("0"),
2611
- cellId("1"),
2612
- ]);
2613
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2614
- cellId("2"),
2615
- cellId("3"),
2616
- ]);
2617
- });
2618
-
2619
- it("rebuildCellColumns with explicit column on every cell", () => {
2620
- actions.createNewCell({ cellId: firstCellId, before: false });
2621
- actions.createNewCell({ cellId: cellId("1"), before: false });
2622
- actions.createNewCell({ cellId: cellId("2"), before: false });
2623
-
2624
- for (const [id, col] of [
2625
- ["0", 1],
2626
- ["1", 0],
2627
- ["2", 1],
2628
- ["3", 0],
2629
- ] as const) {
2630
- actions.updateCellConfig({ cellId: cellId(id), config: { column: col } });
2631
- }
2632
-
2633
- actions.rebuildCellColumns({
2634
- cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
2635
- });
2636
-
2637
- expect(state.cellIds.getColumns().length).toBe(2);
2638
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2639
- cellId("1"),
2640
- cellId("3"),
2641
- ]);
2642
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2643
- cellId("0"),
2644
- cellId("2"),
2645
- ]);
2646
- });
2647
-
2648
- it("rebuildCellColumns collapses to one column when all cells are col 0", () => {
2649
- actions.createNewCell({ cellId: firstCellId, before: false });
2650
- actions.addColumnBreakpoint({ cellId: cellId("1") });
2651
- expect(state.cellIds.getColumns().length).toBe(2);
2652
-
2653
- // Wipe column metadata back to 0 for both cells.
2654
- actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
2655
- actions.updateCellConfig({ cellId: cellId("1"), config: { column: 0 } });
2656
-
2657
- actions.rebuildCellColumns({
2658
- cellIds: [cellId("0"), cellId("1")],
2659
- });
2660
-
2661
- expect(state.cellIds.getColumns().length).toBe(1);
2662
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2663
- cellId("0"),
2664
- cellId("1"),
2665
- ]);
2666
- });
2667
-
2668
- it("rebuildCellColumns follows the provided order, not current tree order", () => {
2669
- // Start with a single column.
2670
- actions.createNewCell({ cellId: firstCellId, before: false });
2671
- actions.createNewCell({ cellId: cellId("1"), before: false });
2672
- actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
2673
- actions.updateCellConfig({ cellId: cellId("2"), config: { column: 1 } });
2674
-
2675
- // Pass the reversed order. The rebuild should honor it.
2676
- actions.rebuildCellColumns({
2677
- cellIds: [cellId("2"), cellId("1"), cellId("0")],
2678
- });
2679
-
2680
- // cellIds iteration:
2681
- // 2 → col=1, pushed to col1, prev=1
2682
- // 1 → col=null, pushed to col[prev=1] = col1
2683
- // 0 → col=0, pushed to col0
2684
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2685
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2686
- cellId("2"),
2687
- cellId("1"),
2688
- ]);
2689
- });
2690
-
2691
1170
  it("can clear output of a single cell", () => {
2692
1171
  // Set up initial state with output
2693
1172
  actions.handleCellMessage({
@@ -2776,8 +1255,8 @@ describe("cell reducer", () => {
2776
1255
 
2777
1256
  it("can clear output of all cells", () => {
2778
1257
  // Create multiple cells with output
2779
- actions.createNewCell({ cellId: firstCellId, before: false });
2780
- const secondCellId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
1258
+ seedCell({ cellId: firstCellId, before: false });
1259
+ const secondCellId = state.cellIds[1];
2781
1260
 
2782
1261
  // Add output to both cells
2783
1262
  const outputMessage = {
@@ -2818,126 +1297,14 @@ describe("cell reducer", () => {
2818
1297
 
2819
1298
  // Verify all outputs are cleared
2820
1299
  expect(state.cellRuntime[firstCellId].output).toBeNull();
2821
- expect(state.cellRuntime[firstCellId].consoleOutputs).toEqual([]);
2822
- expect(state.cellRuntime[secondCellId].output).toBeNull();
2823
- expect(state.cellRuntime[secondCellId].consoleOutputs).toEqual([]);
2824
- });
2825
-
2826
- it("skips creating new cell if code exists and skipIfCodeExists is true", () => {
2827
- // Add initial cell with code
2828
- actions.updateCellCode({
2829
- cellId: firstCellId,
2830
- code: "import numpy as np",
2831
- formattingChange: false,
2832
- });
2833
-
2834
- // Try to create new cell with same code and skipIfCodeExists
2835
- actions.createNewCell({
2836
- cellId: "__end__",
2837
- code: "import numpy as np",
2838
- before: false,
2839
- skipIfCodeExists: true,
2840
- });
2841
-
2842
- // Should still only have one cell
2843
- expect(state.cellIds.inOrderIds.length).toBe(1);
2844
- expect(formatCells(state)).toMatchInlineSnapshot(`
2845
- "
2846
- [0] 'import numpy as np'
2847
- "
2848
- `);
2849
-
2850
- // Verify we can still add cell with different code
2851
- actions.createNewCell({
2852
- cellId: "__end__",
2853
- code: "import pandas as pd",
2854
- before: false,
2855
- skipIfCodeExists: true,
2856
- });
2857
-
2858
- expect(state.cellIds.inOrderIds.length).toBe(2);
2859
- expect(formatCells(state)).toMatchInlineSnapshot(`
2860
- "
2861
- [0] 'import numpy as np'
2862
-
2863
- [1] 'import pandas as pd'
2864
- "
2865
- `);
2866
- });
2867
-
2868
- it("can create and noop-update a setup cell", () => {
2869
- // Create the setup cell
2870
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2871
-
2872
- // Check that setup cell was created
2873
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2874
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2875
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2876
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2877
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2878
-
2879
- // Update the setup cell
2880
- actions.addSetupCellIfDoesntExist({ code: "# Updated setup code" });
2881
-
2882
- // Check that the setup cell did not change, since it already exists
2883
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2884
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2885
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2886
- });
2887
-
2888
- it("can delete and undelete the setup cell", () => {
2889
- // Create the setup cell
2890
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2891
-
2892
- // Check that setup cell was created
2893
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2894
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2895
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2896
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2897
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2898
-
2899
- // Delete the setup cell
2900
- actions.deleteCell({ cellId: SETUP_CELL_ID });
2901
-
2902
- // Check that setup cell was deleted
2903
- expect(state.cellData[SETUP_CELL_ID]).toBeDefined(); // we keep old state
2904
- expect(state.cellIds.inOrderIds).not.toContain(SETUP_CELL_ID);
2905
-
2906
- // Undo delete the setup cell
2907
- actions.undoDeleteCell();
2908
-
2909
- // Check that setup cell was restored
2910
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2911
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2912
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2913
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2914
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2915
-
2916
- // Verify scrollKey is set to the restored setup cell
2917
- expect(state.scrollKey).toBe(SETUP_CELL_ID);
2918
- });
2919
-
2920
- it("can delete and then create a new setup cell", () => {
2921
- // Create the setup cell
2922
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2923
-
2924
- // Delete the setup cell
2925
- actions.deleteCell({ cellId: SETUP_CELL_ID });
2926
-
2927
- // Create a new setup cell
2928
- actions.addSetupCellIfDoesntExist({ code: "# New code" });
2929
-
2930
- // Check that the new setup cell was created
2931
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2932
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2933
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# New code");
2934
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2935
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
1300
+ expect(state.cellRuntime[firstCellId].consoleOutputs).toEqual([]);
1301
+ expect(state.cellRuntime[secondCellId].output).toBeNull();
1302
+ expect(state.cellRuntime[secondCellId].consoleOutputs).toEqual([]);
2936
1303
  });
2937
1304
 
2938
1305
  it("can clear all outputs", () => {
2939
1306
  // Add a cell and give it output
2940
- actions.createNewCell({
1307
+ seedCell({
2941
1308
  cellId: firstCellId,
2942
1309
  before: false,
2943
1310
  });
@@ -2965,522 +1332,13 @@ describe("cell reducer", () => {
2965
1332
  expect(state.cellRuntime[cell2Id].output).toBeNull();
2966
1333
  });
2967
1334
 
2968
- describe("moveToNextCell", () => {
2969
- let cell1Id: CellId;
2970
- let cell2Id: CellId;
2971
- let cell3Id: CellId;
2972
-
2973
- beforeEach(() => {
2974
- // Create a few cells to work with
2975
- actions.createNewCell({ cellId: "__end__", before: false });
2976
- actions.createNewCell({ cellId: "__end__", before: false });
2977
-
2978
- cell1Id = state.cellIds.inOrderIds[0];
2979
- cell2Id = state.cellIds.inOrderIds[1];
2980
- cell3Id = state.cellIds.inOrderIds[2];
2981
- });
2982
-
2983
- it("creates new cell when moving after last cell with noCreate=false", () => {
2984
- const initialCellCount = state.cellIds.inOrderIds.length;
2985
-
2986
- actions.moveToNextCell({
2987
- cellId: cell3Id,
2988
- before: false,
2989
- noCreate: false,
2990
- });
2991
-
2992
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
2993
- });
2994
-
2995
- it("creates new cell when moving before first cell with noCreate=false", () => {
2996
- const initialCellCount = state.cellIds.inOrderIds.length;
2997
-
2998
- actions.moveToNextCell({
2999
- cellId: cell1Id,
3000
- before: true,
3001
- noCreate: false,
3002
- });
3003
-
3004
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3005
- });
3006
-
3007
- it("does not create new cell when moving after last cell with noCreate=true", () => {
3008
- const initialCellCount = state.cellIds.inOrderIds.length;
3009
- const initialState = { ...state };
3010
-
3011
- actions.moveToNextCell({
3012
- cellId: cell3Id,
3013
- before: false,
3014
- noCreate: true,
3015
- });
3016
-
3017
- // Should not create a new cell
3018
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount);
3019
- // Should not crash or throw an error
3020
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3021
- });
3022
-
3023
- it("does not create new cell when moving before first cell with noCreate=true", () => {
3024
- const initialCellCount = state.cellIds.inOrderIds.length;
3025
- const initialState = { ...state };
3026
-
3027
- actions.moveToNextCell({ cellId: cell1Id, before: true, noCreate: true });
3028
-
3029
- // Should not create a new cell
3030
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount);
3031
- // Should not crash or throw an error
3032
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3033
- });
3034
-
3035
- it("focuses next cell when moving within bounds", () => {
3036
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3037
- focusSpy.mockClear();
3038
-
3039
- actions.moveToNextCell({
3040
- cellId: cell1Id,
3041
- before: false,
3042
- noCreate: true,
3043
- });
3044
-
3045
- expect(focusSpy).toHaveBeenCalledWith({
3046
- cellId: cell2Id,
3047
- cell: state.cellHandles[cell2Id],
3048
- isCodeHidden: false,
3049
- codeFocus: "top",
3050
- variableName: undefined,
3051
- });
3052
- });
3053
-
3054
- it("focuses previous cell when moving backward within bounds", () => {
3055
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3056
- focusSpy.mockClear();
3057
-
3058
- actions.moveToNextCell({ cellId: cell2Id, before: true, noCreate: true });
3059
-
3060
- expect(focusSpy).toHaveBeenCalledWith({
3061
- cellId: cell1Id,
3062
- cell: state.cellHandles[cell1Id],
3063
- isCodeHidden: false,
3064
- codeFocus: "bottom",
3065
- variableName: undefined,
3066
- });
3067
- });
3068
-
3069
- it("does not move focus from scratch cell", () => {
3070
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3071
- focusSpy.mockClear();
3072
- const initialState = { ...state };
3073
-
3074
- actions.moveToNextCell({
3075
- cellId: SCRATCH_CELL_ID,
3076
- before: false,
3077
- noCreate: false,
3078
- });
3079
-
3080
- expect(focusSpy).not.toHaveBeenCalled();
3081
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3082
- });
3083
- });
3084
-
3085
- describe("untouched cells functionality", () => {
3086
- it("starts with empty untouchedNewCells set", () => {
3087
- expect(state.untouchedNewCells).toEqual(new Set());
3088
- });
3089
-
3090
- it("can create a new cell with hideCode option", () => {
3091
- const initialCellCount = state.cellIds.inOrderIds.length;
3092
-
3093
- actions.createNewCell({
3094
- cellId: "__end__",
3095
- before: false,
3096
- hideCode: true,
3097
- });
3098
-
3099
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3100
- const newCellId =
3101
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3102
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3103
- });
3104
-
3105
- it("adds interactively-created hidden cell with boilerplate code to untouchedNewCells", () => {
3106
- // Markdown cells are created with hideCode and non-empty default code
3107
- // (e.g. `mo.md(r"""\n""")`). They are user-initiated, so their editor
3108
- // should be shown until first blur.
3109
- actions.createNewCell({
3110
- cellId: "__end__",
3111
- before: false,
3112
- code: 'mo.md(r"""\n""")',
3113
- hideCode: true,
3114
- autoFocus: true,
3115
- });
3116
-
3117
- const newCellId =
3118
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3119
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3120
- });
3121
-
3122
- it("does not add programmatically-created hidden cell with code to untouchedNewCells", () => {
3123
- // Cells created by the kernel (e.g. via code_mode) carry code and
3124
- // autoFocus=false; their hide_code must take effect immediately.
3125
- actions.createNewCell({
3126
- cellId: "__end__",
3127
- before: false,
3128
- code: "x = 1",
3129
- hideCode: true,
3130
- autoFocus: false,
3131
- });
3132
-
3133
- const newCellId =
3134
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3135
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3136
- });
3137
-
3138
- it("does not add cell to untouchedNewCells when hideCode is false", () => {
3139
- actions.createNewCell({
3140
- cellId: "__end__",
3141
- before: false,
3142
- hideCode: false,
3143
- });
3144
-
3145
- const newCellId =
3146
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3147
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3148
- });
3149
-
3150
- it("does not add cell to untouchedNewCells when hideCode is undefined", () => {
3151
- actions.createNewCell({
3152
- cellId: "__end__",
3153
- before: false,
3154
- });
3155
-
3156
- const newCellId =
3157
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3158
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3159
- });
3160
-
3161
- it("can mark a cell as touched", () => {
3162
- // Create a cell with hideCode
3163
- actions.createNewCell({
3164
- cellId: "__end__",
3165
- before: false,
3166
- hideCode: 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
- // Mark it as touched
3174
- actions.markTouched({ cellId: newCellId });
3175
-
3176
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3177
- });
3178
-
3179
- it("markTouched is idempotent", () => {
3180
- // Create a cell with hideCode
3181
- actions.createNewCell({
3182
- cellId: "__end__",
3183
- before: false,
3184
- hideCode: true,
3185
- });
3186
-
3187
- const newCellId =
3188
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3189
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3190
-
3191
- // Mark it as touched multiple times
3192
- actions.markTouched({ cellId: newCellId });
3193
- actions.markTouched({ cellId: newCellId });
3194
- actions.markTouched({ cellId: newCellId });
3195
-
3196
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3197
- });
3198
-
3199
- it("can mark a non-existent cell as touched without error", () => {
3200
- const nonExistentCellId = cellId("non-existent");
3201
-
3202
- expect(() => {
3203
- actions.markTouched({ cellId: nonExistentCellId });
3204
- }).not.toThrow();
3205
-
3206
- expect(state.untouchedNewCells.has(nonExistentCellId)).toBe(false);
3207
- });
3208
-
3209
- it("can handle multiple untouched cells", () => {
3210
- // Create multiple cells with hideCode
3211
- actions.createNewCell({
3212
- cellId: "__end__",
3213
- before: false,
3214
- hideCode: true,
3215
- });
3216
- actions.createNewCell({
3217
- cellId: "__end__",
3218
- before: false,
3219
- hideCode: true,
3220
- });
3221
- actions.createNewCell({
3222
- cellId: "__end__",
3223
- before: false,
3224
- hideCode: false,
3225
- });
3226
-
3227
- const cellIds = state.cellIds.inOrderIds;
3228
- const cell1Id = cellIds[cellIds.length - 3];
3229
- const cell2Id = cellIds[cellIds.length - 2];
3230
- const cell3Id = cellIds[cellIds.length - 1];
3231
-
3232
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3233
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3234
- expect(state.untouchedNewCells.has(cell3Id)).toBe(false);
3235
-
3236
- // Mark one as touched
3237
- actions.markTouched({ cellId: cell1Id });
3238
-
3239
- expect(state.untouchedNewCells.has(cell1Id)).toBe(false);
3240
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3241
- expect(state.untouchedNewCells.has(cell3Id)).toBe(false);
3242
- });
3243
-
3244
- it("preserves untouched state when cells are moved", () => {
3245
- // Create cells with hideCode
3246
- actions.createNewCell({
3247
- cellId: "__end__",
3248
- before: false,
3249
- hideCode: true,
3250
- });
3251
- actions.createNewCell({
3252
- cellId: "__end__",
3253
- before: false,
3254
- hideCode: true,
3255
- });
3256
-
3257
- const cellIds = state.cellIds.inOrderIds;
3258
- const cell1Id = cellIds[cellIds.length - 2];
3259
- const cell2Id = cellIds[cellIds.length - 1];
3260
-
3261
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3262
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3263
-
3264
- // Move cell1 to the end
3265
- actions.moveCell({
3266
- cellId: cell1Id,
3267
- before: false,
3268
- });
3269
-
3270
- // Both cells should still be untouched
3271
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3272
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3273
- });
3274
-
3275
- it("does not remove untouched state when cell is deleted", () => {
3276
- // We could implement this, but it doesn't actually affect downstream behavior.
3277
- // It is easier to leave it, so `undo` works as expected.
3278
-
3279
- // Create a cell with hideCode
3280
- actions.createNewCell({
3281
- cellId: "__end__",
3282
- before: false,
3283
- hideCode: true,
3284
- });
3285
-
3286
- const newCellId =
3287
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3288
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3289
-
3290
- // Delete the cell
3291
- actions.deleteCell({ cellId: newCellId });
3292
-
3293
- // Does not actually remove untouched state
3294
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3295
- });
3296
-
3297
- it("restores untouched state when cell deletion is undone", () => {
3298
- // Create a cell with hideCode
3299
- actions.createNewCell({
3300
- cellId: "__end__",
3301
- before: false,
3302
- hideCode: true,
3303
- });
3304
-
3305
- const newCellId =
3306
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3307
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3308
-
3309
- // Delete the cell
3310
- actions.deleteCell({ cellId: newCellId });
3311
- // Still exists in untouchedNewCells
3312
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3313
-
3314
- // Undo the deletion
3315
- actions.undoDeleteCell();
3316
-
3317
- // The cell should be restored but no longer untouched
3318
- // (this is expected behavior - undoing doesn't restore untouched state)
3319
- const restoredCellId =
3320
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3321
- expect(state.untouchedNewCells.has(restoredCellId)).toBe(false);
3322
- });
3323
-
3324
- it("untouched cell behavior with hide_code config", () => {
3325
- // Create a cell with hideCode and hide_code config
3326
- actions.createNewCell({
3327
- cellId: "__end__",
3328
- before: false,
3329
- hideCode: true,
3330
- });
3331
-
3332
- const newCellId =
3333
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3334
-
3335
- // Update the cell config to hide code
3336
- actions.updateCellConfig({
3337
- cellId: newCellId,
3338
- config: { hide_code: true },
3339
- });
3340
-
3341
- // Cell should be untouched and have hide_code config
3342
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3343
- expect(state.cellData[newCellId].config.hide_code).toBe(true);
3344
-
3345
- // Code should not be hidden because cell is untouched
3346
- expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(false);
3347
-
3348
- // Mark as touched
3349
- actions.markTouched({ cellId: newCellId });
3350
-
3351
- // Now code should be hidden
3352
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3353
- expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(true);
3354
- });
3355
-
3356
- it("can mark an existing cell as untouched", () => {
3357
- // Create a cell without hideCode (not in untouchedNewCells)
3358
- actions.createNewCell({
3359
- cellId: "__end__",
3360
- before: false,
3361
- hideCode: false,
3362
- });
3363
-
3364
- const newCellId =
3365
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3366
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3367
-
3368
- // Mark it as untouched
3369
- actions.markUntouched({ cellId: newCellId });
3370
-
3371
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3372
- });
3373
-
3374
- it("markUntouched is idempotent", () => {
3375
- // Create a cell without hideCode
3376
- actions.createNewCell({
3377
- cellId: "__end__",
3378
- before: false,
3379
- hideCode: false,
3380
- });
3381
-
3382
- const newCellId =
3383
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3384
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3385
-
3386
- // Mark as untouched multiple times
3387
- actions.markUntouched({ cellId: newCellId });
3388
- actions.markUntouched({ cellId: newCellId });
3389
- actions.markUntouched({ cellId: newCellId });
3390
-
3391
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3392
- });
3393
-
3394
- it("markUntouched does not affect already untouched cells", () => {
3395
- // Create a cell with hideCode (already in untouchedNewCells)
3396
- actions.createNewCell({
3397
- cellId: "__end__",
3398
- before: false,
3399
- hideCode: true,
3400
- });
3401
-
3402
- const newCellId =
3403
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3404
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3405
-
3406
- // Calling markUntouched should not change anything
3407
- actions.markUntouched({ cellId: newCellId });
3408
-
3409
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3410
- });
3411
-
3412
- it("markTouched and markUntouched can toggle cell state", () => {
3413
- // Create a cell without hideCode
3414
- actions.createNewCell({
3415
- cellId: "__end__",
3416
- before: false,
3417
- hideCode: false,
3418
- });
3419
-
3420
- const newCellId =
3421
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3422
-
3423
- // Initially not untouched
3424
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3425
-
3426
- // Mark as untouched
3427
- actions.markUntouched({ cellId: newCellId });
3428
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3429
-
3430
- // Mark as touched
3431
- actions.markTouched({ cellId: newCellId });
3432
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3433
-
3434
- // Mark as untouched again
3435
- actions.markUntouched({ cellId: newCellId });
3436
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3437
- });
3438
-
3439
- it("markUntouched works for markdown cell conversion scenario", () => {
3440
- // Simulates converting a Python cell to Markdown
3441
- // 1. Create a regular cell (no hideCode)
3442
- actions.createNewCell({
3443
- cellId: "__end__",
3444
- before: false,
3445
- hideCode: false,
3446
- });
3447
-
3448
- const cellId =
3449
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3450
-
3451
- // Cell starts without hide_code and not in untouchedNewCells
3452
- expect(state.cellData[cellId].config.hide_code).toBe(false);
3453
- expect(state.untouchedNewCells.has(cellId)).toBe(false);
3454
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
3455
-
3456
- // 2. Convert to markdown: set hide_code and mark as untouched
3457
- actions.updateCellConfig({
3458
- cellId,
3459
- config: { hide_code: true },
3460
- });
3461
- actions.markUntouched({ cellId });
3462
-
3463
- // Code should NOT be hidden because cell is untouched (user can edit)
3464
- expect(state.cellData[cellId].config.hide_code).toBe(true);
3465
- expect(state.untouchedNewCells.has(cellId)).toBe(true);
3466
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
3467
-
3468
- // 3. User blurs the cell (markTouched)
3469
- actions.markTouched({ cellId });
3470
-
3471
- // Now code should be hidden
3472
- expect(state.untouchedNewCells.has(cellId)).toBe(false);
3473
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(true);
3474
- });
3475
- });
3476
-
3477
1335
  describe("releaseCellAtoms", () => {
3478
1336
  it("atom families cache atoms until explicitly removed", () => {
3479
1337
  const { cellDataAtom, cellRuntimeAtom, cellHandleAtom } =
3480
1338
  exportedForTesting;
3481
1339
 
3482
- actions.createNewCell({ cellId: firstCellId, before: false });
3483
- const newCellId = state.cellIds.inOrderIds[1];
1340
+ seedCell({ cellId: firstCellId, before: false });
1341
+ const newCellId = state.cellIds[1];
3484
1342
 
3485
1343
  const dataAtom1 = cellDataAtom(newCellId);
3486
1344
  const runtimeAtom1 = cellRuntimeAtom(newCellId);
@@ -3495,116 +1353,6 @@ describe("cell reducer", () => {
3495
1353
  expect(runtimeAtom2).toBe(runtimeAtom1);
3496
1354
  expect(handleAtom2).toBe(handleAtom1);
3497
1355
  });
3498
-
3499
- it("cleans up atom family cache when cells are deleted", () => {
3500
- const { cellDataAtom, cellRuntimeAtom, cellHandleAtom } =
3501
- exportedForTesting;
3502
-
3503
- actions.createNewCell({ cellId: firstCellId, before: false });
3504
- const newCellId = state.cellIds.inOrderIds[1];
3505
-
3506
- // Access to create cache entries
3507
- const dataAtom1 = cellDataAtom(newCellId);
3508
- const runtimeAtom1 = cellRuntimeAtom(newCellId);
3509
- const handleAtom1 = cellHandleAtom(newCellId);
3510
-
3511
- // Triggers purge
3512
- actions.deleteCell({ cellId: newCellId });
3513
-
3514
- // Access again (should be new instances after cleanup)
3515
- const dataAtom2 = cellDataAtom(newCellId);
3516
- const runtimeAtom2 = cellRuntimeAtom(newCellId);
3517
- const handleAtom2 = cellHandleAtom(newCellId);
3518
-
3519
- expect(dataAtom2).not.toBe(dataAtom1);
3520
- expect(runtimeAtom2).not.toBe(runtimeAtom1);
3521
- expect(handleAtom2).not.toBe(handleAtom1);
3522
- });
3523
- });
3524
- });
3525
-
3526
- describe("isCellCodeHidden", () => {
3527
- const state = initialNotebookState();
3528
- const firstCellId = state.cellIds.inOrderIds[0];
3529
-
3530
- it("returns false when hide_code is false and cell is not untouched", () => {
3531
- const testCellId = cellId("test-cell");
3532
- const testState: NotebookState = {
3533
- ...state,
3534
- cellData: {
3535
- ...state.cellData,
3536
- [testCellId]: {
3537
- ...state.cellData[firstCellId],
3538
- id: testCellId,
3539
- config: { hide_code: false, disabled: false, column: null },
3540
- },
3541
- },
3542
- untouchedNewCells: new Set(),
3543
- };
3544
-
3545
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3546
- false,
3547
- );
3548
- });
3549
-
3550
- it("returns true when hide_code is true and cell is not untouched", () => {
3551
- const testCellId = cellId("test-cell");
3552
- const testState: NotebookState = {
3553
- ...state,
3554
- cellData: {
3555
- ...state.cellData,
3556
- [testCellId]: {
3557
- ...state.cellData[firstCellId],
3558
- id: testCellId,
3559
- config: { hide_code: true, disabled: false, column: null },
3560
- },
3561
- },
3562
- untouchedNewCells: new Set(),
3563
- };
3564
-
3565
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3566
- true,
3567
- );
3568
- });
3569
-
3570
- it("returns false when hide_code is true but cell is untouched", () => {
3571
- const testCellId = cellId("test-cell");
3572
- const testState: NotebookState = {
3573
- ...state,
3574
- cellData: {
3575
- ...state.cellData,
3576
- [testCellId]: {
3577
- ...state.cellData[firstCellId],
3578
- id: testCellId,
3579
- config: { hide_code: true, disabled: false, column: null },
3580
- },
3581
- },
3582
- untouchedNewCells: new Set([testCellId]),
3583
- };
3584
-
3585
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3586
- false,
3587
- );
3588
- });
3589
-
3590
- it("returns false when hide_code is false and cell is untouched", () => {
3591
- const testCellId = cellId("test-cell");
3592
- const testState: NotebookState = {
3593
- ...state,
3594
- cellData: {
3595
- ...state.cellData,
3596
- [testCellId]: {
3597
- ...state.cellData[firstCellId],
3598
- id: testCellId,
3599
- config: { hide_code: false, disabled: false, column: null },
3600
- },
3601
- },
3602
- untouchedNewCells: new Set([testCellId]),
3603
- };
3604
-
3605
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3606
- false,
3607
- );
3608
1356
  });
3609
1357
  });
3610
1358
 
@@ -3834,31 +1582,9 @@ describe("createTracebackInfoAtom", () => {
3834
1582
  });
3835
1583
  });
3836
1584
 
3837
- describe("setCells snapshot preservation", () => {
1585
+ describe("kernel-ready", () => {
3838
1586
  const CELL_A = cellId("A");
3839
1587
  const CELL_B = cellId("B");
3840
- const newCells: CellData[] = [
3841
- {
3842
- id: CELL_A,
3843
- name: "a",
3844
- code: "1",
3845
- edited: false,
3846
- lastCodeRun: null,
3847
- lastExecutionTime: null,
3848
- config: { hide_code: false, disabled: false, column: null },
3849
- serializedEditorState: null,
3850
- },
3851
- {
3852
- id: CELL_B,
3853
- name: "b",
3854
- code: "2",
3855
- edited: false,
3856
- lastCodeRun: null,
3857
- lastExecutionTime: null,
3858
- config: { hide_code: false, disabled: false, column: null },
3859
- serializedEditorState: null,
3860
- },
3861
- ];
3862
1588
 
3863
1589
  const hydratedState = () =>
3864
1590
  MockNotebook.notebookState({
@@ -3888,44 +1614,97 @@ describe("setCells snapshot preservation", () => {
3888
1614
  },
3889
1615
  });
3890
1616
 
3891
- beforeEach(async () => {
3892
- const { isWasm } = await import("@/core/wasm/utils");
3893
- vi.mocked(isWasm).mockReturnValue(true);
1617
+ it("setLastRun records what the kernel last ran", () => {
1618
+ const state = hydratedState();
1619
+ const next = exportedForTesting.reducer(state, {
1620
+ type: "setLastRun",
1621
+ payload: {
1622
+ lastCodeRun: { [CELL_A]: "0" },
1623
+ lastExecutionTime: { [CELL_A]: 5 },
1624
+ },
1625
+ });
1626
+
1627
+ expect(next.cellData[CELL_A]).toMatchObject({
1628
+ lastCodeRun: "0",
1629
+ lastExecutionTime: 5,
1630
+ edited: true,
1631
+ });
1632
+ expect(next.cellData[CELL_B]).toMatchObject({
1633
+ lastCodeRun: null,
1634
+ lastExecutionTime: null,
1635
+ edited: false,
1636
+ });
1637
+ expect(next.cellRuntime).toBe(state.cellRuntime);
3894
1638
  });
3895
1639
 
3896
- it("preserves hydrated output in WASM", () => {
1640
+ it("resetCellRuntimes starts every cell's runtime over", () => {
3897
1641
  const next = exportedForTesting.reducer(hydratedState(), {
3898
- type: "setCells",
3899
- payload: newCells,
1642
+ type: "resetCellRuntimes",
1643
+ payload: undefined,
3900
1644
  });
3901
1645
 
3902
- expect(next.cellRuntime[CELL_A].output).toMatchObject({
3903
- data: "hydrated-A",
1646
+ expect(next.cellRuntime[CELL_A].output).toBeNull();
1647
+ expect(next.cellRuntime[CELL_B].consoleOutputs).toEqual([]);
1648
+ });
1649
+
1650
+ it("resetCellRuntimes starts the scratch cell over too", () => {
1651
+ const state = MockNotebook.notebookState({
1652
+ cellData: { [CELL_A]: { id: CELL_A, code: "1" } },
1653
+ cellRuntime: {
1654
+ [SCRATCH_CELL_ID]: {
1655
+ output: {
1656
+ channel: "output",
1657
+ mimetype: "text/plain",
1658
+ data: "scratch",
1659
+ timestamp: 0,
1660
+ },
1661
+ },
1662
+ },
3904
1663
  });
1664
+ const next = exportedForTesting.reducer(state, {
1665
+ type: "resetCellRuntimes",
1666
+ payload: undefined,
1667
+ });
1668
+
1669
+ expect(next.cellRuntime[SCRATCH_CELL_ID].output).toBeNull();
3905
1670
  });
3906
1671
 
3907
- it("preserves console-only hydration in WASM", () => {
3908
- const next = exportedForTesting.reducer(hydratedState(), {
3909
- type: "setCells",
3910
- payload: newCells,
1672
+ describe("resetCellRuntimes in WASM", () => {
1673
+ beforeEach(async () => {
1674
+ const { isWasm } = await import("@/core/wasm/utils");
1675
+ vi.mocked(isWasm).mockReturnValue(true);
3911
1676
  });
3912
1677
 
3913
- expect(next.cellRuntime[CELL_B].consoleOutputs).toHaveLength(1);
3914
- expect(next.cellRuntime[CELL_B].consoleOutputs[0]).toMatchObject({
3915
- data: "hydrated-B-stdout",
1678
+ afterEach(async () => {
1679
+ const { isWasm } = await import("@/core/wasm/utils");
1680
+ vi.mocked(isWasm).mockReturnValue(false);
3916
1681
  });
3917
- });
3918
1682
 
3919
- it("resets cells with no prior runtime even in WASM", () => {
3920
- const empty = MockNotebook.notebookState({ cellData: {} });
3921
- const next = exportedForTesting.reducer(empty, {
3922
- type: "setCells",
3923
- payload: newCells,
1683
+ it("keeps hydrated outputs and console", () => {
1684
+ const next = exportedForTesting.reducer(hydratedState(), {
1685
+ type: "resetCellRuntimes",
1686
+ payload: undefined,
1687
+ });
1688
+
1689
+ expect(next.cellRuntime[CELL_A].output).toMatchObject({
1690
+ data: "hydrated-A",
1691
+ });
1692
+ expect(next.cellRuntime[CELL_B].consoleOutputs[0]).toMatchObject({
1693
+ data: "hydrated-B-stdout",
1694
+ });
3924
1695
  });
3925
1696
 
3926
- expect(next.cellRuntime[CELL_A].output).toBeNull();
3927
- expect(next.cellRuntime[CELL_A].consoleOutputs).toEqual([]);
3928
- expect(next.cellRuntime[CELL_B].output).toBeNull();
3929
- expect(next.cellRuntime[CELL_B].consoleOutputs).toEqual([]);
1697
+ it("starts cells with no prior runtime over", () => {
1698
+ const state = MockNotebook.notebookState({
1699
+ cellData: { [CELL_A]: { id: CELL_A, code: "1" } },
1700
+ });
1701
+ const next = exportedForTesting.reducer(state, {
1702
+ type: "resetCellRuntimes",
1703
+ payload: undefined,
1704
+ });
1705
+
1706
+ expect(next.cellRuntime[CELL_A].output).toBeNull();
1707
+ expect(next.cellRuntime[CELL_A].consoleOutputs).toEqual([]);
1708
+ });
3930
1709
  });
3931
1710
  });