@marimo-team/islands 0.25.3-dev20 → 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 (688) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-BsWiIPFk.js} +22 -20
  2. package/dist/{ErrorBoundary-DAPIRYqd.js → ErrorBoundary-UWARECs-.js} +19 -19
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-I2x4Yu25.js} +2 -2
  4. package/dist/any-language-editor-BVFHDDQ3.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.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-DaHynk8g.js → chat-ui-DXDoSuS4.js} +370 -369
  12. package/dist/{check-EDwSwP89.js → check-C9Mp9eCK.js} +1 -1
  13. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-fsCGAJ9G.js} +1 -1
  14. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-D2STW4hj.js} +1 -1
  15. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-CiY7cQG-.js} +1 -1
  16. package/dist/{chunk-TICWLB2K-Cu_XMjDM.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-BXXV13Hq.js → classDiagram-ZZMXUADV-B2JvZAP4.js} +2 -2
  19. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-L8VsZG5A.js} +2 -2
  20. package/dist/code-visibility-33fxfX1p.js +134 -0
  21. package/dist/{common-DsUPhkD6.js → common-D09oJJ8Z.js} +6040 -5865
  22. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-De584pZ4.js} +3 -3
  23. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-DdllC264.js} +1 -1
  24. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-BaWHw4_f.js} +1 -1
  25. package/dist/{data-grid-overlay-editor-D7UuVsgB.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-BsSJJldB.js → diagram-S7CK7UJ4-pFt-tUDe.js} +1 -1
  28. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DqGJiBp4.js} +1 -1
  29. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-Dr-yzMjf.js} +1 -1
  30. package/dist/{diagram-Z3DM3KII-BD8mXjOj.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-BIN1sweE.js → ebnfDiagram-PWID7BFC-Br04Wy-E.js} +2 -2
  59. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-D5-XVgvv.js} +1 -1
  60. package/dist/{error-banner-D-WCIiMt.js → error-banner-DxJ3s8YO.js} +86 -86
  61. package/dist/{error-entries-Bv6bZPF3.js → error-entries-C2Ux0Fjt.js} +8404 -9095
  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-3EfLrlFb.js → flowDiagram-HODETNUW-CVl2kREF.js} +2 -2
  66. package/dist/{formats-27CW_UP4.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-KvS90MGh.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-yruoELTW.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-Bf8QMGi4.js → html-to-image-CkH7vdBZ.js} +477 -380
  73. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-BJ4vNcZE.js} +1 -1
  74. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-B_Jy1Dsm.js} +1 -1
  75. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.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 -7709
  80. package/dist/{mermaid-CHo49Yv-.js → mermaid-Bh918qrq.js} +52 -52
  81. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  82. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-BP3ZTuir.js} +1 -1
  83. package/dist/{number-overlay-editor-R2i0FdxV.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-BqvywNJA.js → pegDiagram-XKGWAZYB-nNGOQtnt.js} +2 -2
  87. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-CpRz5BZz.js} +1 -1
  88. package/dist/precisionRound-DhAzeRcm.js +49 -0
  89. package/dist/{process-output-BZg56CCb.js → process-output-OujAtFW4.js} +5 -5
  90. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  91. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.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-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CEfg6vu9.js} +1 -1
  94. package/dist/{reveal-component-C42-YBfQ.js → reveal-component-CbEK0sNX.js} +733 -710
  95. package/dist/{select-BButFi-d.js → select-2KOiReNY.js} +536 -457
  96. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-IOmpQcOi.js} +1 -1
  97. package/dist/{spec-BwarBpUJ.js → spec-Dkp64vVQ.js} +41 -11
  98. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-tgU-EY9_.js} +2 -2
  99. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-BofhG6q1.js} +2 -2
  100. package/dist/style.css +1 -1
  101. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-DW4vWDBd.js} +1 -1
  102. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.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-D3c46WM5.js → timeline-definition-24CTP7MA-D_QU3NGs.js} +1 -1
  106. package/dist/{toDate-UtiwmnH2.js → toDate-BNDEXbfV.js} +44 -43
  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-BeF9dfyO.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-BSZD8JcZ.js → vennDiagram-4TSXK5OY-YFYsNIAh.js} +1 -1
  120. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-i_n8mAWC.js} +1 -1
  121. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.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__/main.test.tsx +24 -1
  128. package/src/__tests__/module-entry.test.ts +44 -0
  129. package/src/__tests__/mount.test.ts +1 -21
  130. package/src/__tests__/{host-imports.test.ts → replica-imports.test.ts} +12 -11
  131. package/src/components/ai/__tests__/ai-model-dropdown.test.tsx +63 -0
  132. package/src/components/ai/ai-model-dropdown.tsx +11 -6
  133. package/src/components/app-config/ai-config.tsx +7 -2
  134. package/src/components/app-config/app-config-form.tsx +5 -9
  135. package/src/components/app-config/user-config-form.tsx +35 -0
  136. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  137. package/src/components/chat/__tests__/notebook-access-selector.test.tsx +64 -0
  138. package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
  139. package/src/components/chat/acp/__tests__/state.test.ts +2 -2
  140. package/src/components/chat/acp/blocks.tsx +3 -2
  141. package/src/components/chat/acp/session-tabs.tsx +8 -3
  142. package/src/components/chat/chat-components.tsx +2 -0
  143. package/src/components/chat/chat-panel.tsx +20 -101
  144. package/src/components/chat/chat-utils.ts +4 -8
  145. package/src/components/chat/notebook-access-selector.stories.tsx +22 -0
  146. package/src/components/chat/notebook-access-selector.tsx +100 -0
  147. package/src/components/data-table/TableBottomBar.tsx +4 -1
  148. package/src/components/data-table/TableTopBar.tsx +2 -0
  149. package/src/components/data-table/__tests__/chart-spec-model.test.ts +195 -195
  150. package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
  151. package/src/components/data-table/__tests__/columns.test.tsx +105 -0
  152. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  153. package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
  154. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  155. package/src/components/data-table/__tests__/export-actions.test.tsx +105 -0
  156. package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
  157. package/src/components/data-table/__tests__/pagination.test.tsx +28 -1
  158. package/src/components/data-table/__tests__/search-highlight.test.tsx +109 -0
  159. package/src/components/data-table/__tests__/url-detector.test.tsx +74 -0
  160. package/src/components/data-table/cell-selection/feature.ts +2 -2
  161. package/src/components/data-table/cell-selection/types.ts +2 -2
  162. package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
  163. package/src/components/data-table/charts/__tests__/spec-snapshot.test.ts +20 -20
  164. package/src/components/data-table/charts/__tests__/spec.test.ts +78 -52
  165. package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
  166. package/src/components/data-table/charts/chart-spec/spec.ts +68 -43
  167. package/src/components/data-table/charts/charts.tsx +40 -30
  168. package/src/components/data-table/charts/storage.ts +16 -5
  169. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
  170. package/src/components/data-table/column-summary/chart-spec-model.tsx +23 -16
  171. package/src/components/data-table/column-summary/column-summary.tsx +2 -4
  172. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  173. package/src/components/data-table/columns.tsx +45 -46
  174. package/src/components/data-table/context-menu.tsx +36 -20
  175. package/src/components/data-table/data-table.tsx +17 -1
  176. package/src/components/data-table/date-popover.tsx +6 -4
  177. package/src/components/data-table/export-actions.tsx +76 -48
  178. package/src/components/data-table/pagination.tsx +44 -33
  179. package/src/components/data-table/range-focus/atoms.ts +4 -6
  180. package/src/components/data-table/range-focus/use-cell-range-selection.ts +65 -38
  181. package/src/components/data-table/range-focus/use-scroll-into-view.ts +21 -2
  182. package/src/components/data-table/renderers.tsx +16 -7
  183. package/src/components/data-table/row-viewer-panel/__tests__/row-viewer.test.tsx +24 -1
  184. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +4 -4
  185. package/src/components/data-table/search-highlight.tsx +79 -0
  186. package/src/components/data-table/types.ts +1 -0
  187. package/src/components/data-table/url-detector.tsx +49 -9
  188. package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
  189. package/src/components/datasources/__tests__/column-preview.test.tsx +96 -5
  190. package/src/components/datasources/__tests__/datasources-interactions.test.tsx +181 -0
  191. package/src/components/datasources/__tests__/table-list.test.tsx +118 -0
  192. package/src/components/datasources/__tests__/utils.test.ts +57 -6
  193. package/src/components/datasources/column-preview.tsx +17 -12
  194. package/src/components/datasources/datasources.tsx +56 -43
  195. package/src/components/datasources/utils.ts +32 -23
  196. package/src/components/debugger/debugger-code.tsx +5 -0
  197. package/src/components/dependency-graph/custom-node.tsx +1 -1
  198. package/src/components/dependency-graph/dependency-graph-tree.tsx +8 -8
  199. package/src/components/dependency-graph/elements.ts +24 -21
  200. package/src/components/dependency-graph/minimap-content.tsx +13 -19
  201. package/src/components/dependency-graph/panels.tsx +4 -2
  202. package/src/components/dependency-graph/utils/__tests__/layout.test.ts +18 -11
  203. package/src/components/editor/SortableCell.tsx +1 -3
  204. package/src/components/editor/__tests__/Output.test.tsx +50 -32
  205. package/src/{core/attachments/__tests__/toast.test.tsx → components/editor/__tests__/agent-toast.test.tsx} +1 -1
  206. package/src/components/editor/__tests__/data-attributes.test.tsx +28 -14
  207. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  208. package/src/components/editor/__tests__/output-persistence.test.tsx +6 -2
  209. package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
  210. package/src/components/editor/actions/__tests__/name-cell-input.test.tsx +69 -0
  211. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +34 -8
  212. package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +2 -6
  213. package/src/components/editor/actions/name-cell-input.tsx +4 -3
  214. package/src/components/editor/actions/pair-with-agent-commands.ts +2 -2
  215. package/src/components/editor/actions/pair-with-agent-modal.tsx +19 -16
  216. package/src/components/editor/actions/useCellActionButton.tsx +26 -31
  217. package/src/components/editor/actions/useConfigActions.tsx +3 -4
  218. package/src/components/editor/actions/useNotebookActions.tsx +19 -23
  219. package/src/components/editor/actions/useRestartKernel.tsx +1 -1
  220. package/src/components/editor/actions/useSetCodeVisibility.ts +11 -12
  221. package/src/components/editor/actions/useSetOutputsExpanded.ts +14 -19
  222. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  223. package/src/components/editor/ai/ai-completion-editor.tsx +1 -0
  224. package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +1 -1
  225. package/src/components/editor/alerts/connection-notice.tsx +1 -1
  226. package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
  227. package/src/components/editor/alerts/stdin-blocking-alert.tsx +1 -1
  228. package/src/components/editor/app-container.tsx +1 -1
  229. package/src/components/editor/boundary/__tests__/ErrorBoundary.test.tsx +1 -1
  230. package/src/components/editor/cell/CreateCellButton.tsx +15 -21
  231. package/src/components/editor/cell/DeleteButton.tsx +3 -2
  232. package/src/components/editor/cell/RunButton.tsx +2 -2
  233. package/src/components/editor/cell/StagedAICell.tsx +10 -2
  234. package/src/components/editor/cell/StopButton.tsx +2 -2
  235. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +3 -1
  236. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  237. package/src/components/editor/cell/cell-actions.tsx +12 -1
  238. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  239. package/src/components/editor/cell/code/cell-editor.tsx +36 -43
  240. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  241. package/src/components/editor/cell/collapse.tsx +11 -4
  242. package/src/components/editor/cell/useAddCell.ts +4 -8
  243. package/src/components/editor/cell/useDeleteCell.tsx +10 -36
  244. package/src/components/editor/cell/useRunCells.ts +2 -5
  245. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  246. package/src/components/editor/cell/useSplitCell.tsx +10 -6
  247. package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +1 -1
  248. package/src/components/editor/chrome/components/feedback-button.tsx +2 -8
  249. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +31 -2
  250. package/src/components/editor/chrome/panels/__tests__/runtime-details.test.tsx +1 -1
  251. package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +1 -1
  252. package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
  253. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +1 -1
  254. package/src/components/editor/chrome/panels/logs-panel.tsx +2 -1
  255. package/src/components/editor/chrome/panels/outline/__tests__/outline-keyboard.test.tsx +84 -0
  256. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +39 -14
  257. package/src/components/editor/chrome/panels/outline/useActiveOutline.tsx +3 -2
  258. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  259. package/src/components/editor/chrome/panels/secrets-panel.tsx +1 -0
  260. package/src/components/editor/chrome/panels/session-panel.tsx +1 -1
  261. package/src/components/editor/chrome/panels/snippets-panel.tsx +10 -12
  262. package/src/components/editor/chrome/panels/variable-panel.tsx +1 -5
  263. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +9 -18
  264. package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
  265. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +61 -2
  266. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +4 -4
  267. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
  268. package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
  269. package/src/components/editor/chrome/wrapper/footer-items/lsp-status.tsx +1 -1
  270. package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +1 -1
  271. package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +1 -7
  272. package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
  273. package/src/components/editor/chrome/wrapper/minimap-state.ts +1 -1
  274. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +12 -2
  275. package/src/components/editor/chrome/wrapper/storage.ts +1 -1
  276. package/src/components/editor/columns/__tests__/cell-column.test.tsx +1 -1
  277. package/src/components/editor/columns/cell-column.tsx +1 -1
  278. package/src/components/editor/columns/sortable-column.tsx +2 -2
  279. package/src/components/editor/connections/components.tsx +5 -10
  280. package/src/components/editor/controls/Controls.tsx +4 -4
  281. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  282. package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
  283. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +10 -4
  284. package/src/components/editor/controls/command-palette.tsx +22 -10
  285. package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
  286. package/src/components/editor/documentation.css +34 -11
  287. package/src/components/editor/dynamic-favicon.tsx +21 -40
  288. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +17 -29
  289. package/src/components/editor/errors/auto-fix.tsx +4 -8
  290. package/src/components/editor/errors/send-error-report-button.tsx +3 -6
  291. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  292. package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
  293. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  294. package/src/components/editor/file-tree/file-explorer.tsx +48 -15
  295. package/src/components/editor/file-tree/file-name-input.tsx +1 -0
  296. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  297. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  298. package/src/components/editor/header/__tests__/status.test.tsx +5 -3
  299. package/src/components/editor/header/app-header.tsx +1 -1
  300. package/src/components/editor/header/status.tsx +40 -20
  301. package/src/components/editor/links/__tests__/cell-link.test.tsx +148 -0
  302. package/src/components/editor/links/cell-link-list.tsx +1 -1
  303. package/src/components/editor/links/cell-link.tsx +17 -7
  304. package/src/components/editor/navigation/__tests__/clipboard.test.ts +60 -38
  305. package/src/components/editor/navigation/__tests__/navigation.test.ts +108 -85
  306. package/src/components/editor/navigation/__tests__/selection.test.ts +43 -21
  307. package/src/components/editor/navigation/clipboard.ts +17 -14
  308. package/src/components/editor/navigation/focus-utils.ts +23 -4
  309. package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +17 -33
  310. package/src/components/editor/navigation/navigation.ts +71 -70
  311. package/src/components/editor/navigation/selection.ts +30 -29
  312. package/src/components/editor/notebook-banner.tsx +1 -0
  313. package/src/components/editor/notebook-cell.tsx +29 -31
  314. package/src/components/editor/output/MarimoErrorOutput.tsx +2 -1
  315. package/src/components/editor/output/MarimoTracebackOutput.tsx +19 -17
  316. package/src/components/editor/output/Outputs.css +8 -0
  317. package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
  318. package/src/components/editor/output/ansi-reduce.ts +33 -20
  319. package/src/components/editor/output/console/ConsoleOutput.tsx +19 -25
  320. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +125 -69
  321. package/src/components/editor/package-alert.tsx +14 -7
  322. package/src/components/editor/renderers/cell-array.tsx +22 -33
  323. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +12 -6
  324. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  325. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +1 -1
  326. package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +4 -4
  327. package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
  328. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +1 -1
  329. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  330. package/src/components/editor/stdin-blocking-alert.tsx +1 -1
  331. package/src/components/home/components.tsx +17 -31
  332. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  333. package/src/components/icons/large-spinner.tsx +3 -14
  334. package/src/components/icons/spinner.tsx +1 -1
  335. package/src/components/markdown/markdown-renderer.tsx +4 -8
  336. package/src/components/mcp/mcp-status-indicator.tsx +1 -1
  337. package/src/components/pages/__tests__/home-page.test.tsx +116 -0
  338. package/src/components/pages/edit-page.tsx +19 -17
  339. package/src/components/pages/home-page.tsx +164 -91
  340. package/src/components/scratchpad/scratchpad.test.tsx +97 -0
  341. package/src/components/scratchpad/scratchpad.tsx +20 -11
  342. package/src/components/slides/__tests__/minimap-actions.test.tsx +22 -29
  343. package/src/components/slides/__tests__/minimap.test.ts +38 -19
  344. package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +37 -0
  345. package/src/components/slides/minimap.tsx +17 -12
  346. package/src/components/slides/reveal-component.tsx +58 -26
  347. package/src/components/slides/reveal-slides.css +5 -4
  348. package/src/components/slides/slide-cell-view.tsx +4 -5
  349. package/src/components/slides/slide-form.tsx +1 -0
  350. package/src/components/slides/slide-scroll-hint.tsx +3 -1
  351. package/src/components/sort/SortableCellsProvider.tsx +8 -25
  352. package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
  353. package/src/components/tracing/tracing.tsx +46 -42
  354. package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
  355. package/src/components/ui/__tests__/range-slider.test.tsx +165 -0
  356. package/src/components/ui/alert-dialog.tsx +1 -1
  357. package/src/components/ui/draggable-popover.tsx +24 -2
  358. package/src/components/ui/range-slider.tsx +22 -9
  359. package/src/core/MarimoApp.tsx +1 -1
  360. package/src/core/ai/__tests__/staged-cells.test.ts +27 -26
  361. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +8 -12
  362. package/src/core/ai/context/providers/cell-output.ts +2 -2
  363. package/src/core/ai/context/providers/datasource.ts +6 -2
  364. package/src/core/ai/staged-cells.ts +15 -18
  365. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +40 -88
  366. package/src/core/ai/tools/__tests__/registry.test.ts +17 -0
  367. package/src/core/ai/tools/base.ts +18 -15
  368. package/src/core/ai/tools/edit-notebook-tool.ts +86 -80
  369. package/src/core/ai/tools/registry.ts +13 -10
  370. package/src/core/ai/tools/sample-tool.ts +6 -7
  371. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -37
  372. package/src/core/cells/__tests__/actions.test.ts +624 -35
  373. package/src/core/cells/__tests__/add-missing-import.test.ts +7 -8
  374. package/src/core/cells/__tests__/arrange.test.ts +432 -0
  375. package/src/core/cells/__tests__/cell.test.ts +39 -0
  376. package/src/core/cells/__tests__/cells.test.ts +218 -2491
  377. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
  378. package/src/core/cells/__tests__/document.test.ts +341 -0
  379. package/src/core/cells/__tests__/editor.test.ts +57 -0
  380. package/src/core/cells/__tests__/flush-document.test.ts +40 -0
  381. package/src/core/cells/__tests__/harness.ts +106 -0
  382. package/src/core/cells/__tests__/layout.test.ts +177 -0
  383. package/src/core/cells/__tests__/notebook-outline.test.ts +120 -0
  384. package/src/core/cells/__tests__/outputs.test.tsx +71 -53
  385. package/src/core/cells/__tests__/session.test.ts +445 -211
  386. package/src/core/cells/__tests__/utils.test.ts +9 -108
  387. package/src/core/cells/__tests__/with-document-save.test.ts +67 -0
  388. package/src/core/cells/actions.ts +636 -6
  389. package/src/core/cells/add-missing-import.ts +47 -43
  390. package/src/core/cells/arrange.ts +529 -0
  391. package/src/core/cells/cell.ts +14 -3
  392. package/src/core/cells/cells.ts +120 -1337
  393. package/src/core/cells/columns.ts +38 -0
  394. package/src/core/cells/document.ts +289 -0
  395. package/src/core/cells/edit.ts +30 -0
  396. package/src/core/cells/editor.ts +82 -0
  397. package/src/core/cells/layout.ts +221 -0
  398. package/src/core/cells/outputs.ts +12 -28
  399. package/src/core/cells/pending-delete-service.ts +3 -7
  400. package/src/core/cells/session.ts +76 -53
  401. package/src/core/cells/types.ts +22 -6
  402. package/src/core/cells/utils.ts +10 -8
  403. package/src/core/codemirror/__tests__/setup.test.ts +29 -29
  404. package/src/core/codemirror/ai/__tests__/resources.test.ts +117 -0
  405. package/src/core/codemirror/ai/resources.ts +1 -1
  406. package/src/core/codemirror/cells/__tests__/extensions.test.ts +42 -1
  407. package/src/core/codemirror/cells/extensions.ts +27 -9
  408. package/src/core/codemirror/cells/state.ts +1 -1
  409. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +6 -7
  410. package/src/core/codemirror/copilot/getCodes.ts +2 -2
  411. package/src/core/codemirror/format.ts +6 -5
  412. package/src/core/codemirror/keymaps/vim.ts +4 -8
  413. package/src/core/codemirror/language/__tests__/sql.test.ts +22 -20
  414. package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
  415. package/src/core/codemirror/language/cell-code.ts +114 -0
  416. package/src/core/codemirror/language/extension.ts +52 -35
  417. package/src/core/codemirror/language/languages/markdown.ts +8 -6
  418. package/src/core/codemirror/language/languages/python.ts +16 -10
  419. package/src/core/codemirror/language/languages/sql/sql.ts +16 -24
  420. package/src/core/codemirror/language/panel/panel.tsx +1 -0
  421. package/src/core/codemirror/language/types.ts +7 -7
  422. package/src/core/codemirror/language/utils.ts +9 -39
  423. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +1 -2
  424. package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
  425. package/src/core/codemirror/lsp/utils.ts +1 -1
  426. package/src/core/codemirror/readonly/__tests__/extension.test.ts +1 -1
  427. package/src/core/codemirror/readonly/extension.ts +2 -2
  428. package/src/core/codemirror/rtc/extension.ts +24 -13
  429. package/src/core/codemirror/rtc/loro/awareness.ts +34 -19
  430. package/src/core/codemirror/rtc/loro/sync.ts +1 -0
  431. package/src/core/codemirror/utils.ts +1 -0
  432. package/src/core/config/__tests__/app-config.test.ts +64 -0
  433. package/src/core/config/__tests__/config-schema.test.ts +13 -6
  434. package/src/core/config/config-schema.ts +13 -3
  435. package/src/core/config/config.ts +26 -12
  436. package/src/core/datasets/__tests__/data-source-discovery.test.ts +5 -5
  437. package/src/core/datasets/data-source-discovery.ts +4 -6
  438. package/src/core/documentation/doc-lookup.ts +1 -1
  439. package/src/core/edit-app.tsx +21 -28
  440. package/src/core/errors/error-entries.ts +4 -1
  441. package/src/core/export/hooks.ts +1 -1
  442. package/src/core/export/layout.ts +2 -2
  443. package/src/core/hotkeys/actions.ts +2 -4
  444. package/src/core/islands/__tests__/bootstrap.test.ts +69 -43
  445. package/src/core/islands/__tests__/bridge.test.ts +72 -25
  446. package/src/core/islands/bootstrap.ts +46 -54
  447. package/src/core/islands/bridge.ts +74 -23
  448. package/src/core/islands/components/__tests__/IslandControls.test.tsx +7 -5
  449. package/src/core/islands/components/web-components.tsx +1 -1
  450. package/src/core/islands/worker/__tests__/controller.test.ts +62 -4
  451. package/src/core/islands/worker/controller.ts +4 -10
  452. package/src/core/islands/worker/worker.tsx +33 -33
  453. package/src/core/kernel/__tests__/RuntimeState.test.ts +1 -1
  454. package/src/core/kernel/__tests__/handlers.test.ts +7 -153
  455. package/src/core/kernel/handlers.ts +19 -72
  456. package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
  457. package/src/core/meta/code-visibility.ts +18 -9
  458. package/src/core/network/__tests__/connection.test.ts +1 -1
  459. package/src/core/network/__tests__/requests-lazy.test.ts +1 -3
  460. package/src/core/network/connection-notice.ts +1 -1
  461. package/src/core/network/connection.ts +1 -1
  462. package/src/core/network/requests-lazy.ts +1 -4
  463. package/src/core/network/requests-network.ts +0 -25
  464. package/src/core/network/requests-static.ts +0 -3
  465. package/src/core/network/requests-toasting.tsx +0 -3
  466. package/src/core/network/types.ts +0 -12
  467. package/src/core/network/useConnectionNotice.ts +8 -2
  468. package/src/core/notebook/__tests__/operations.test.ts +51 -0
  469. package/src/core/notebook/attachments.ts +38 -0
  470. package/src/core/notebook/document/__tests__/codemirror.test.ts +586 -0
  471. package/src/core/notebook/document/__tests__/draft.test.ts +363 -0
  472. package/src/core/notebook/document/__tests__/fixtures.ts +92 -0
  473. package/src/core/notebook/document/__tests__/hold.test.ts +88 -0
  474. package/src/core/notebook/document/__tests__/hooks.test.tsx +391 -0
  475. package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +61 -0
  476. package/src/core/notebook/document/__tests__/model.test.ts +247 -0
  477. package/src/core/notebook/document/__tests__/protocol-fixtures.ts +38 -0
  478. package/src/core/notebook/document/__tests__/recovery-scenarios.test.ts +230 -0
  479. package/src/core/notebook/document/__tests__/scenario-fixture.ts +43 -0
  480. package/src/core/notebook/document/__tests__/scenarios.test.ts +205 -0
  481. package/src/core/notebook/document/__tests__/state.test.ts +459 -0
  482. package/src/core/notebook/document/__tests__/submission.test.ts +203 -0
  483. package/src/core/notebook/document/__tests__/test-host.test.ts +92 -0
  484. package/src/core/notebook/document/__tests__/test-host.ts +247 -0
  485. package/src/core/notebook/document/__tests__/test-notebook.ts +149 -0
  486. package/src/core/notebook/document/__tests__/user.ts +61 -0
  487. package/src/core/notebook/document/codemirror.ts +216 -0
  488. package/src/core/notebook/document/draft.ts +502 -0
  489. package/src/core/notebook/document/hooks.tsx +118 -0
  490. package/src/core/notebook/document/index.ts +24 -0
  491. package/src/core/notebook/document/model.ts +105 -0
  492. package/src/core/notebook/document/select.ts +41 -0
  493. package/src/core/notebook/document/state.ts +378 -0
  494. package/src/core/notebook/document/sync.ts +418 -0
  495. package/src/core/notebook/index.ts +19 -0
  496. package/src/core/notebook/operations.ts +30 -0
  497. package/src/core/notebook/replica/AGENTS.md +11 -0
  498. package/src/core/notebook/replica/__tests__/attachments.test.ts +56 -0
  499. package/src/core/{host → notebook/replica}/__tests__/document.test.ts +18 -16
  500. package/src/core/notebook/replica/__tests__/fixtures.ts +79 -0
  501. package/src/core/notebook/replica/__tests__/replica.test.ts +550 -0
  502. package/src/core/notebook/replica/backend.ts +19 -0
  503. package/src/core/notebook/replica/connection.ts +42 -0
  504. package/src/core/notebook/replica/index.ts +13 -0
  505. package/src/core/notebook/replica/replica.ts +286 -0
  506. package/src/core/notebook/replica/resources/attachments.ts +22 -0
  507. package/src/core/{host → notebook/replica}/resources/document.ts +4 -4
  508. package/src/core/packages/__tests__/useSandboxController.test.tsx +1 -1
  509. package/src/core/packages/useSandboxController.ts +1 -1
  510. package/src/core/run-app.tsx +48 -51
  511. package/src/core/runtime/__tests__/adapter.test.ts +1 -1
  512. package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
  513. package/src/core/runtime/__tests__/runtime.test.ts +32 -58
  514. package/src/core/runtime/adapter.ts +1 -1
  515. package/src/core/runtime/config.ts +2 -2
  516. package/src/core/runtime/runtime.ts +38 -42
  517. package/src/core/runtime/types.ts +0 -2
  518. package/src/core/saving/__tests__/state.test.ts +111 -0
  519. package/src/core/saving/__tests__/useAutoSave.test.tsx +67 -0
  520. package/src/core/saving/filename.ts +1 -1
  521. package/src/core/saving/save-component.tsx +28 -36
  522. package/src/core/saving/state.ts +38 -39
  523. package/src/core/saving/useAutoSave.ts +17 -53
  524. package/src/core/session/__tests__/backend.test.ts +236 -0
  525. package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +184 -0
  526. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +663 -0
  527. package/src/core/session/active-notebook.ts +22 -0
  528. package/src/core/session/backend.ts +123 -0
  529. package/src/core/session/embedded.ts +7 -0
  530. package/src/core/{websocket → session}/types.ts +2 -2
  531. package/src/core/session/useNotebookReplica.ts +74 -0
  532. package/src/core/{websocket → session}/useNotebookSession.tsx +77 -72
  533. package/src/core/state/__tests__/jotai.test.tsx +108 -0
  534. package/src/core/state/jotai.ts +1 -1
  535. package/src/core/static/__tests__/files.test.ts +38 -10
  536. package/src/core/static/files.ts +2 -0
  537. package/src/core/{websocket → transports}/__tests__/connection-state.test.ts +1 -13
  538. package/src/core/transports/__tests__/server.test.ts +408 -0
  539. package/src/core/transports/__tests__/static.test.ts +306 -0
  540. package/src/core/transports/__tests__/wasm.test.ts +166 -0
  541. package/src/core/{websocket → transports}/connection-state.ts +9 -16
  542. package/src/core/transports/server.ts +152 -0
  543. package/src/core/transports/static.ts +166 -0
  544. package/src/core/transports/wasm.ts +97 -0
  545. package/src/core/wasm/__tests__/bridge.test.ts +214 -50
  546. package/src/core/wasm/__tests__/worker.test.ts +187 -0
  547. package/src/core/wasm/bridge.ts +75 -69
  548. package/src/core/wasm/rpc.ts +15 -3
  549. package/src/core/wasm/store.ts +3 -3
  550. package/src/core/wasm/worker/bootstrap.ts +55 -51
  551. package/src/core/wasm/worker/getMarimoWheel.ts +1 -1
  552. package/src/core/wasm/worker/save-worker.ts +9 -10
  553. package/src/core/wasm/worker/types.ts +29 -18
  554. package/src/core/wasm/worker/worker.ts +88 -44
  555. package/src/css/globals.css +66 -22
  556. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  557. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  558. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  559. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  560. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  561. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  562. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  563. package/src/hooks/useAsyncData.ts +3 -3
  564. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  565. package/src/hooks/useDebounce.ts +6 -10
  566. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  567. package/src/hooks/useDelayElapsed.ts +14 -10
  568. package/src/hooks/useElapsedTime.ts +3 -3
  569. package/src/hooks/useEventListener.ts +1 -2
  570. package/src/hooks/useInputHistory.ts +4 -7
  571. package/src/hooks/useInternalStateWithSync.ts +6 -6
  572. package/src/hooks/useResizeObserver.ts +4 -7
  573. package/src/hooks/useScript.ts +67 -70
  574. package/src/hooks/useStateForKey.ts +93 -0
  575. package/src/mount.tsx +20 -15
  576. package/src/plugins/__tests__/plugin-object-schemas.test.ts +104 -0
  577. package/src/plugins/core/registerReactComponent.tsx +1 -1
  578. package/src/plugins/impl/CodeEditorPlugin.tsx +1 -1
  579. package/src/plugins/impl/DataEditorPlugin.tsx +1 -1
  580. package/src/plugins/impl/DataTablePlugin.tsx +69 -37
  581. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  582. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  583. package/src/plugins/impl/FormPlugin.tsx +1 -1
  584. package/src/plugins/impl/RangeSliderPlugin.tsx +16 -2
  585. package/src/plugins/impl/RefreshPlugin.tsx +1 -0
  586. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  587. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +387 -1
  588. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  589. package/src/plugins/impl/__tests__/RangeSliderPlugin.test.tsx +109 -0
  590. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  591. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +153 -0
  592. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  593. package/src/plugins/impl/anywidget/model.ts +3 -1
  594. package/src/plugins/impl/anywidget/registry.ts +18 -14
  595. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  596. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  597. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  598. package/src/plugins/impl/chat/chat-ui.tsx +14 -11
  599. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  600. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  601. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  602. package/src/plugins/impl/data-explorer/ConnectedDataExplorerComponent.tsx +1 -1
  603. package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
  604. package/src/plugins/impl/data-explorer/state/reducer.ts +1 -1
  605. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -2
  606. package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
  607. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +66 -66
  608. package/src/plugins/impl/data-frames/panel.tsx +43 -16
  609. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  610. package/src/plugins/impl/panel/PanelPlugin.tsx +16 -17
  611. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  612. package/src/plugins/impl/panel/utils.ts +17 -0
  613. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +2 -5
  614. package/src/plugins/impl/plotly/selection.ts +25 -19
  615. package/src/plugins/impl/vega/VegaPlugin.tsx +2 -3
  616. package/src/plugins/impl/vega/make-selectable.ts +4 -7
  617. package/src/plugins/layout/RoutesPlugin.tsx +1 -1
  618. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  619. package/src/stories/button.stories.tsx +1 -0
  620. package/src/stories/cell.stories.tsx +2 -5
  621. package/src/stories/data-table.stories.tsx +31 -0
  622. package/src/stories/editor.stories.tsx +24 -17
  623. package/src/stories/form-wrapper.stories.tsx +2 -2
  624. package/src/stories/layout/vertical/one-column.stories.tsx +2 -5
  625. package/src/stories/runtime-story.tsx +23 -17
  626. package/src/stories/select.stories.tsx +2 -2
  627. package/src/stories/switchable-multi-select.stories.tsx +1 -1
  628. package/src/utils/__tests__/arrays.test.ts +35 -0
  629. package/src/utils/__tests__/batch-requests.test.ts +3 -3
  630. package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +29 -0
  631. package/src/utils/__tests__/numbers.test.ts +105 -1
  632. package/src/utils/__tests__/objects.test.ts +21 -0
  633. package/src/utils/arrays.ts +11 -2
  634. package/src/utils/blob.ts +1 -1
  635. package/src/utils/createReducer.ts +53 -26
  636. package/src/utils/maps.ts +0 -3
  637. package/src/utils/numbers.ts +79 -18
  638. package/src/utils/objects.ts +4 -9
  639. package/src/utils/once.ts +19 -19
  640. package/src/utils/tracer.ts +9 -5
  641. package/src/utils/zod-utils.ts +1 -3
  642. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  643. package/dist/assets/__vite-browser-external-BXYkiFsk.js +0 -1
  644. package/dist/assets/worker-HPY9fbhz.js +0 -114
  645. package/dist/code-visibility-DkCqn-PO.js +0 -138
  646. package/dist/feedback-button-wnAMXOGA.js +0 -379
  647. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  648. package/dist/precisionRound-DMIVQrt2.js +0 -97
  649. package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
  650. package/src/components/editor/navigation/state.ts +0 -65
  651. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +0 -656
  652. package/src/core/ai/tools/run-cells-tool.ts +0 -303
  653. package/src/core/attachments/state.ts +0 -21
  654. package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
  655. package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
  656. package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
  657. package/src/core/cells/document-changes.ts +0 -944
  658. package/src/core/host/AGENTS.md +0 -11
  659. package/src/core/host/__tests__/events.test.ts +0 -46
  660. package/src/core/host/__tests__/fixtures.ts +0 -39
  661. package/src/core/host/__tests__/notebook-client.test.ts +0 -303
  662. package/src/core/host/__tests__/protocol.test.ts +0 -214
  663. package/src/core/host/__tests__/stream.test.ts +0 -211
  664. package/src/core/host/connection.ts +0 -81
  665. package/src/core/host/events.ts +0 -60
  666. package/src/core/host/notebook-client.ts +0 -272
  667. package/src/core/host/protocol.ts +0 -1943
  668. package/src/core/host/resources/resource.ts +0 -59
  669. package/src/core/host/stream.ts +0 -213
  670. package/src/core/state/__tests__/jotai.test.ts +0 -25
  671. package/src/core/wasm/worker/message-buffer.ts +0 -33
  672. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  673. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +0 -140
  674. package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +0 -883
  675. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  676. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -335
  677. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  678. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  679. package/src/core/websocket/transports/basic.ts +0 -102
  680. package/src/core/websocket/transports/create.ts +0 -31
  681. package/src/core/websocket/transports/sse.ts +0 -308
  682. package/src/core/websocket/transports/transport.ts +0 -50
  683. package/src/core/websocket/transports/ws.ts +0 -98
  684. package/src/core/websocket/useNotebookClient.ts +0 -80
  685. package/src/utils/__tests__/id-tree.test.ts +0 -2145
  686. package/src/utils/id-tree.tsx +0 -1134
  687. /package/src/{core/attachments/toast.ts → components/editor/agent-toast.ts} +0 -0
  688. /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,655 +121,93 @@ describe("cell reducer", () => {
136
121
  }
137
122
  }
138
123
  cells = flattenTopLevelNotebookCells(state);
139
- });
140
-
141
- let i = 0;
142
- const originalCreate = CellId.create.bind(CellId);
143
-
144
- beforeAll(() => {
145
- CellId.create = () => {
146
- return cellId(`${i++}`);
147
- };
148
- });
149
-
150
- beforeEach(() => {
151
- i = 0;
152
-
153
- 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();
161
- });
162
-
163
- afterAll(() => {
164
- CellId.create = originalCreate;
165
- });
166
-
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
- `);
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
+ }
759
180
 
760
- actions.deleteCell({ cellId: cellId("2") });
761
- expect(formatCells(state)).toMatchInlineSnapshot(`
762
- "
763
- [0] ''
181
+ let i = 0;
182
+ const originalCreate = CellId.create.bind(CellId);
764
183
 
765
- [1] ''
766
- "
767
- `);
184
+ beforeAll(() => {
185
+ CellId.create = () => {
186
+ return cellId(`${i++}`);
187
+ };
188
+ });
768
189
 
769
- actions.undoDeleteCell();
770
- expect(formatCells(state)).toMatchInlineSnapshot(`
771
- "
772
- [3] ''
190
+ beforeEach(() => {
191
+ i = 0;
773
192
 
774
- [0] ''
193
+ state = initialNotebookState();
194
+ seedCell({ cellId: "__end__", before: false });
195
+ firstCellId = state.cellIds[0];
196
+ });
775
197
 
776
- [1] ''
777
- "
778
- `);
198
+ afterAll(() => {
199
+ CellId.create = originalCreate;
200
+ });
779
201
 
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
  });
@@ -1172,7 +595,7 @@ describe("cell reducer", () => {
1172
595
  formattingChange: false,
1173
596
  });
1174
597
  // Add cell
1175
- actions.createNewCell({
598
+ seedCell({
1176
599
  cellId: firstCellId,
1177
600
  before: false,
1178
601
  });
@@ -1277,33 +700,6 @@ describe("cell reducer", () => {
1277
700
  expect(cell.edited).toBe(false);
1278
701
  });
1279
702
 
1280
- it("can update a cells config", () => {
1281
- const firstCellId = cells[0].id;
1282
- let cell = cells[0];
1283
- // Starts empty
1284
- expect(cell.config).toEqual({
1285
- disabled: false,
1286
- hide_code: false,
1287
- column: null,
1288
- expand_output: false,
1289
- });
1290
-
1291
- actions.updateCellConfig({
1292
- cellId: firstCellId,
1293
- config: { disabled: true },
1294
- });
1295
- cell = cells[0];
1296
- expect(cell.config.disabled).toBe(true);
1297
-
1298
- // Revert
1299
- actions.updateCellConfig({
1300
- cellId: firstCellId,
1301
- config: { disabled: false },
1302
- });
1303
- cell = cells[0];
1304
- expect(cell.config.disabled).toBe(false);
1305
- });
1306
-
1307
703
  it("can run a stopped cell", () => {
1308
704
  // Update code of first cell
1309
705
  actions.updateCellCode({
@@ -1579,7 +975,9 @@ describe("cell reducer", () => {
1579
975
  response: secret,
1580
976
  });
1581
977
 
1582
- expect(cells[0].consoleOutputs).toEqual([{ ...prompt, response: "" }]);
978
+ expect(cells[0].consoleOutputs).toEqual([
979
+ { ...prompt, id: 1, response: "" },
980
+ ]);
1583
981
  expect(JSON.stringify(cells[0].consoleOutputs)).not.toContain(secret);
1584
982
  });
1585
983
 
@@ -1688,619 +1086,49 @@ describe("cell reducer", () => {
1688
1086
  });
1689
1087
  cell = cells[0];
1690
1088
  expect(cell.status).toBe("running");
1691
- expect(cell.consoleOutputs).toMatchObject([]);
1692
- expect(cell).toMatchSnapshot("console cleared while running");
1693
-
1694
- // Add console
1695
- actions.handleCellMessage({
1696
- cell_id: firstCellId,
1697
- output: undefined,
1698
- console: [STDOUT],
1699
- status: "idle",
1700
- stale_inputs: null,
1701
- timestamp: new Date(22).getTime() as Seconds,
1702
- });
1703
- cell = cells[0];
1704
- expect(cell.consoleOutputs).toMatchObject([STDOUT]);
1705
- expect(cell.status).toBe("idle");
1706
- expect(cell).toMatchSnapshot("console received while idle");
1707
- });
1708
-
1709
- it("can send a cell to the top", () => {
1710
- actions.createNewCell({ cellId: firstCellId, before: false });
1711
- actions.createNewCell({ cellId: cellId("1"), before: false });
1712
- actions.sendToTop({ cellId: cellId("2") });
1713
- expect(formatCells(state)).toMatchInlineSnapshot(`
1714
- "
1715
- [2] ''
1716
-
1717
- [0] ''
1718
-
1719
- [1] ''
1720
- "
1721
- `);
1722
- });
1723
-
1724
- it("can send a cell to the bottom", () => {
1725
- actions.createNewCell({ cellId: firstCellId, before: false });
1726
- actions.createNewCell({ cellId: cellId("1"), before: false });
1727
- actions.sendToBottom({ cellId: firstCellId });
1728
- expect(formatCells(state)).toMatchInlineSnapshot(`
1729
- "
1730
- [1] ''
1731
-
1732
- [2] ''
1733
-
1734
- [0] ''
1735
- "
1736
- `);
1737
- });
1738
-
1739
- it("can focus cells", () => {
1740
- actions.createNewCell({ cellId: firstCellId, before: false });
1741
- actions.createNewCell({ cellId: cellId("1"), before: false });
1742
-
1743
- actions.focusCell({ cellId: cellId("1"), where: "before" });
1744
- expect(focusAndScrollCellIntoView).toHaveBeenCalledWith(
1745
- expect.objectContaining({
1746
- cellId: cellId("0"),
1747
- }),
1748
- );
1749
-
1750
- actions.focusTopCell();
1751
- expect(scrollToTop).toHaveBeenCalled();
1752
-
1753
- actions.focusBottomCell();
1754
- expect(scrollToBottom).toHaveBeenCalled();
1755
- });
1756
-
1757
- it("can update cell name", () => {
1758
- actions.updateCellName({ cellId: firstCellId, name: "Test Cell" });
1759
- expect(state.cellData[firstCellId].name).toBe("Test Cell");
1760
- });
1761
-
1762
- it("can set cell IDs and codes", () => {
1763
- const newIds = [cellId("3"), cellId("4"), cellId("5")];
1764
- const newCodes = ["code1", "code2", "code3"];
1765
-
1766
- actions.setCellIds({ cellIds: newIds });
1767
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual(newIds);
1768
-
1769
- // When codeIsStale is false, lastCodeRun should match code
1770
- actions.setCellCodes({
1771
- codes: newCodes,
1772
- ids: newIds,
1773
- codeIsStale: false,
1774
- });
1775
- newIds.forEach((id, index) => {
1776
- expect(state.cellData[id].code).toBe(newCodes[index]);
1777
- expect(state.cellData[id].lastCodeRun).toBe(newCodes[index]);
1778
- expect(state.cellData[id].edited).toBe(false);
1779
- });
1780
-
1781
- // When codeIsStale is true, lastCodeRun should not change
1782
- const staleCodes = ["stale1", "stale2", "stale3"];
1783
- actions.setCellCodes({
1784
- codes: staleCodes,
1785
- ids: newIds,
1786
- codeIsStale: true,
1787
- });
1788
- newIds.forEach((id, index) => {
1789
- expect(state.cellData[id].code).toBe(staleCodes[index]);
1790
- expect(state.cellData[id].lastCodeRun).toBe(newCodes[index]);
1791
- expect(state.cellData[id].edited).toBe(true);
1792
- });
1793
- });
1794
-
1795
- it("can can add a new cell with/without stale code", () => {
1796
- actions.setCellCodes({
1797
- codes: ["new code"],
1798
- ids: [cellId("2")],
1799
- codeIsStale: false,
1800
- });
1801
-
1802
- expect(state.cellData[cellId("2")].code).toBe("new code");
1803
- expect(state.cellData[cellId("2")].edited).toBe(false);
1804
- expect(state.cellData[cellId("2")].lastCodeRun).toBe("new code");
1805
-
1806
- actions.setCellCodes({
1807
- codes: ["new code 2"],
1808
- ids: [cellId("9")],
1809
- codeIsStale: true,
1810
- });
1811
-
1812
- expect(state.cellData[cellId("9")].code).toBe("new code 2");
1813
- expect(state.cellData[cellId("9")].edited).toBe(true);
1814
- expect(state.cellData[cellId("9")].lastCodeRun).toBe(null);
1815
- });
1816
-
1817
- it("can partial update cell codes", () => {
1818
- actions.createNewCell({ cellId: firstCellId, before: false });
1819
- actions.createNewCell({ cellId: cellId("1"), before: false });
1820
-
1821
- expect(state.cellIds.inOrderIds).toEqual([
1822
- cellId("0"),
1823
- cellId("1"),
1824
- cellId("2"),
1825
- ]);
1826
- expect(state.cellData[cellId("0")].code).toBe("");
1827
- expect(state.cellData[cellId("1")].code).toBe("");
1828
- expect(state.cellData[cellId("2")].code).toBe("");
1829
-
1830
- // Update cell 1
1831
- actions.setCellCodes({
1832
- codes: ["new code 2"],
1833
- ids: [cellId("1")],
1834
- codeIsStale: false,
1835
- });
1836
-
1837
- expect(state.cellIds.inOrderIds).toEqual([
1838
- cellId("0"),
1839
- cellId("1"),
1840
- cellId("2"),
1841
- ]);
1842
- expect(state.cellData[cellId("0")].code).toBe("");
1843
- expect(state.cellData[cellId("1")].code).toBe("new code 2");
1844
- expect(state.cellData[cellId("1")].edited).toBe(false);
1845
- expect(state.cellData[cellId("2")].code).toBe("");
1846
- });
1847
-
1848
- it("can set cell codes with new cell ids, while preserving the old cell data", () => {
1849
- actions.setCellCodes({
1850
- codes: ["code1", "code2", "code3"],
1851
- ids: [cellId("3"), cellId("4"), cellId("5")],
1852
- codeIsStale: false,
1853
- });
1854
- expect(state.cellData[cellId("3")].code).toBe("code1");
1855
- expect(state.cellData[cellId("4")].code).toBe("code2");
1856
- expect(state.cellData[cellId("5")].code).toBe("code3");
1857
-
1858
- // Update with some new cell ids and some old cell ids
1859
- actions.setCellIds({
1860
- cellIds: [cellId("1"), cellId("2"), cellId("3"), cellId("4")],
1861
- });
1862
- actions.setCellCodes({
1863
- codes: ["new1", "new2", "code1", "code2"],
1864
- ids: [cellId("1"), cellId("2"), cellId("3"), cellId("4")],
1865
- codeIsStale: false,
1866
- });
1867
- expect(state.cellData[cellId("1")].code).toBe("new1");
1868
- expect(state.cellData[cellId("2")].code).toBe("new2");
1869
- expect(state.cellData[cellId("3")].code).toBe("code1");
1870
- expect(state.cellData[cellId("4")].code).toBe("code2");
1871
- expect(state.cellIds.inOrderIds).toEqual([
1872
- cellId("1"),
1873
- cellId("2"),
1874
- cellId("3"),
1875
- cellId("4"),
1876
- ]);
1877
- // Cell 5 data is preserved (possibly used for tracing), but it's not in the cellIds
1878
- expect(state.cellData[cellId("5")]).not.toBeUndefined();
1879
- });
1880
-
1881
- it("can set cell codes with names and configs", () => {
1882
- const newIds = [cellId("3"), cellId("4")];
1883
- actions.setCellIds({ cellIds: newIds });
1884
- actions.setCellCodes({
1885
- codes: ["code1", "code2"],
1886
- ids: newIds,
1887
- codeIsStale: false,
1888
- names: ["setup_cell", "analysis"],
1889
- configs: [
1890
- { hide_code: true, disabled: false, column: null },
1891
- { hide_code: false, disabled: true, column: null },
1892
- ],
1893
- });
1894
-
1895
- expect(state.cellData[cellId("3")].name).toBe("setup_cell");
1896
- expect(state.cellData[cellId("3")].config.hide_code).toBe(true);
1897
- expect(state.cellData[cellId("3")].config.disabled).toBe(false);
1898
-
1899
- expect(state.cellData[cellId("4")].name).toBe("analysis");
1900
- expect(state.cellData[cellId("4")].config.hide_code).toBe(false);
1901
- expect(state.cellData[cellId("4")].config.disabled).toBe(true);
1902
- });
1903
-
1904
- it("can set cell codes without names/configs (backward compat)", () => {
1905
- const newIds = [cellId("3")];
1906
- actions.setCellIds({ cellIds: newIds });
1907
- actions.setCellCodes({
1908
- codes: ["code1"],
1909
- ids: newIds,
1910
- codeIsStale: false,
1911
- });
1912
-
1913
- // Should use defaults when names/configs not provided
1914
- expect(state.cellData[cellId("3")].code).toBe("code1");
1915
- expect(state.cellData[cellId("3")].config.hide_code).toBe(false);
1916
- expect(state.cellData[cellId("3")].config.disabled).toBe(false);
1917
- });
1918
-
1919
- it("can update names and configs on existing cells via setCellCodes", () => {
1920
- // Set initial state
1921
- actions.setCellCodes({
1922
- codes: ["x = 1"],
1923
- ids: [firstCellId],
1924
- codeIsStale: false,
1925
- names: ["old_name"],
1926
- configs: [{ hide_code: false, disabled: false, column: null }],
1927
- });
1928
- expect(state.cellData[firstCellId].name).toBe("old_name");
1929
- expect(state.cellData[firstCellId].config.hide_code).toBe(false);
1930
-
1931
- // Update with new name and config (same code)
1932
- actions.setCellCodes({
1933
- codes: ["x = 1"],
1934
- ids: [firstCellId],
1935
- codeIsStale: true,
1936
- names: ["new_name"],
1937
- configs: [{ hide_code: true, disabled: false, column: null }],
1938
- });
1939
- expect(state.cellData[firstCellId].name).toBe("new_name");
1940
- expect(state.cellData[firstCellId].config.hide_code).toBe(true);
1941
- });
1942
-
1943
- it("can fold and unfold all cells", () => {
1944
- actions.foldAll();
1945
- expect(foldAllBulk).toHaveBeenCalled();
1946
-
1947
- actions.unfoldAll();
1948
- expect(unfoldAllBulk).toHaveBeenCalled();
1949
- });
1950
-
1951
- it("can clear logs", () => {
1952
- state.cellLogs = [
1953
- {
1954
- level: "stderr",
1955
- message: "log1",
1956
- timestamp: 0,
1957
- source: { type: "cell", cellId: firstCellId },
1958
- },
1959
- {
1960
- level: "stderr",
1961
- message: "log1",
1962
- timestamp: 0,
1963
- source: { type: "cell", cellId: firstCellId },
1964
- },
1965
- ];
1966
- actions.clearLogs();
1967
- expect(state.cellLogs).toEqual([]);
1968
- });
1969
-
1970
- it("can collapse and expand cells", () => {
1971
- actions.createNewCell({ cellId: firstCellId, before: false });
1972
- actions.createNewCell({
1973
- cellId: cellId("1"),
1974
- before: false,
1975
- code: "# Header",
1976
- });
1977
- actions.createNewCell({
1978
- cellId: cellId("2"),
1979
- before: false,
1980
- code: "## Subheader",
1981
- });
1982
-
1983
- const id = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
1984
- state.cellRuntime[id] = {
1985
- ...state.cellRuntime[id],
1986
- outline: {
1987
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
1988
- },
1989
- };
1990
-
1991
- actions.collapseCell({ cellId: id });
1992
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(id)).toBe(true);
1993
-
1994
- actions.expandCell({ cellId: id });
1995
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(id)).toBe(false);
1996
- });
1997
-
1998
- it("can collapse and expand all cells in multiple columns", () => {
1999
- actions.createNewCell({ cellId: firstCellId, before: false });
2000
- actions.createNewCell({
2001
- cellId: cellId("1"),
2002
- before: false,
2003
- code: "# First Column Header",
2004
- });
2005
- actions.createNewCell({
2006
- cellId: cellId("2"),
2007
- before: false,
2008
- code: "## First Column Subheader",
2009
- });
2010
-
2011
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2012
-
2013
- actions.createNewCell({
2014
- cellId: cellId("3"),
2015
- before: false,
2016
- code: "# Second Column Header",
2017
- });
2018
- actions.createNewCell({
2019
- cellId: cellId("4"),
2020
- before: false,
2021
- code: "## Second Column Subheader",
2022
- });
2023
-
2024
- const firstColumnHeaderId = state.cellIds
2025
- .atOrThrow(FIRST_COLUMN)
2026
- .atOrThrow(1);
2027
- state.cellRuntime[firstColumnHeaderId] = {
2028
- ...state.cellRuntime[firstColumnHeaderId],
2029
- outline: {
2030
- items: [
2031
- { name: "First Column Header", level: 1, by: { id: "header" } },
2032
- ],
2033
- },
2034
- };
2035
-
2036
- const secondColumnHeaderId = state.cellIds
2037
- .atOrThrow(SECOND_COLUMN)
2038
- .atOrThrow(0);
2039
- state.cellRuntime[secondColumnHeaderId] = {
2040
- ...state.cellRuntime[secondColumnHeaderId],
2041
- outline: {
2042
- items: [
2043
- { name: "Second Column Header", level: 1, by: { id: "header" } },
2044
- ],
2045
- },
2046
- };
2047
-
2048
- actions.collapseAllCells();
2049
- expect(
2050
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstColumnHeaderId),
2051
- ).toBe(true);
2052
- expect(
2053
- state.cellIds.atOrThrow(SECOND_COLUMN).isCollapsed(secondColumnHeaderId),
2054
- ).toBe(true);
2055
-
2056
- actions.expandAllCells();
2057
- expect(
2058
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstColumnHeaderId),
2059
- ).toBe(false);
2060
- expect(
2061
- state.cellIds.atOrThrow(SECOND_COLUMN).isCollapsed(secondColumnHeaderId),
2062
- ).toBe(false);
2063
- });
2064
-
2065
- it("can collapse and expand nested cells in one call", () => {
2066
- actions.createNewCell({ cellId: firstCellId, before: false });
2067
- actions.createNewCell({
2068
- cellId: cellId("1"),
2069
- before: false,
2070
- code: "# Header",
2071
- });
2072
- actions.createNewCell({
2073
- cellId: cellId("2"),
2074
- before: false,
2075
- code: "## Subheader",
2076
- });
2077
- actions.createNewCell({
2078
- cellId: cellId("3"),
2079
- before: false,
2080
- code: "### Subsubheader",
2081
- });
2082
-
2083
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2084
- state.cellRuntime[headerId] = {
2085
- ...state.cellRuntime[headerId],
2086
- outline: {
2087
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2088
- },
2089
- };
2090
-
2091
- const subheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2092
- state.cellRuntime[subheaderId] = {
2093
- ...state.cellRuntime[subheaderId],
2094
- outline: {
2095
- items: [{ name: "Subheader", level: 2, by: { id: "subheader" } }],
2096
- },
2097
- };
2098
-
2099
- // Check if both the parent and child are collapsed
2100
- actions.collapseAllCells();
2101
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2102
- true,
2103
- );
2104
- actions.expandCell({ cellId: headerId });
2105
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2106
- true,
2107
- );
2108
- actions.collapseCell({ cellId: headerId });
2109
-
2110
- // Check if both the parent and child are expanded
2111
- actions.expandAllCells();
2112
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2113
- false,
2114
- );
2115
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2116
- false,
2117
- );
2118
- });
2119
-
2120
- it("can collapse all cells when nested heading ranges end at the same cell", () => {
2121
- actions.createNewCell({ cellId: firstCellId, before: false });
2122
- actions.createNewCell({
2123
- cellId: cellId("1"),
2124
- before: false,
2125
- code: "# Header",
2126
- });
2127
- actions.createNewCell({
2128
- cellId: cellId("2"),
2129
- before: false,
2130
- code: "## Subheader",
2131
- });
2132
- actions.createNewCell({
2133
- cellId: cellId("3"),
2134
- before: false,
2135
- code: "### Subsubheader",
2136
- });
2137
-
2138
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2139
- state.cellRuntime[headerId] = {
2140
- ...state.cellRuntime[headerId],
2141
- outline: {
2142
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2143
- },
2144
- };
2145
-
2146
- const subheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2147
- state.cellRuntime[subheaderId] = {
2148
- ...state.cellRuntime[subheaderId],
2149
- outline: {
2150
- items: [{ name: "Subheader", level: 2, by: { id: "subheader" } }],
2151
- },
2152
- };
2153
-
2154
- const subsubheaderId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2155
- state.cellRuntime[subsubheaderId] = {
2156
- ...state.cellRuntime[subsubheaderId],
2157
- outline: {
2158
- items: [{ name: "Subsubheader", level: 3, by: { id: "subsubheader" } }],
2159
- },
2160
- };
2161
-
2162
- // The header, subheader and subsubheader ranges all end at the last
2163
- // cell. Collapsing all used to throw "Node ... not found in tree" and
2164
- // leave the whole notebook unchanged.
2165
- actions.collapseAllCells();
2166
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2167
- true,
2168
- );
2169
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2170
- firstCellId,
2171
- headerId,
2172
- ]);
2173
-
2174
- // Each level is nested under its parent, collapsed
2175
- actions.expandCell({ cellId: headerId });
2176
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subheaderId)).toBe(
2177
- true,
2178
- );
2179
- actions.expandCell({ cellId: subheaderId });
2180
- expect(
2181
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsubheaderId),
2182
- ).toBe(true);
2183
- });
2184
-
2185
- it("can collapse all cells when a deep chain is followed by a later section", () => {
2186
- actions.createNewCell({ cellId: firstCellId, before: false });
2187
- actions.createNewCell({
2188
- cellId: cellId("1"),
2189
- before: false,
2190
- code: "# Header",
2191
- });
2192
- actions.createNewCell({
2193
- cellId: cellId("2"),
2194
- before: false,
2195
- code: "## Section A",
2196
- });
2197
- actions.createNewCell({
2198
- cellId: cellId("3"),
2199
- before: false,
2200
- code: "### Sub of A",
2201
- });
2202
- actions.createNewCell({
2203
- cellId: cellId("4"),
2204
- before: false,
2205
- code: "## Section B",
2206
- });
2207
-
2208
- const headerId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2209
- state.cellRuntime[headerId] = {
2210
- ...state.cellRuntime[headerId],
2211
- outline: {
2212
- items: [{ name: "Header", level: 1, by: { id: "header" } }],
2213
- },
2214
- };
2215
-
2216
- const sectionAId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2);
2217
- state.cellRuntime[sectionAId] = {
2218
- ...state.cellRuntime[sectionAId],
2219
- outline: {
2220
- items: [{ name: "Section A", level: 2, by: { id: "section-a" } }],
2221
- },
2222
- };
2223
-
2224
- const subsectionId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(3);
2225
- state.cellRuntime[subsectionId] = {
2226
- ...state.cellRuntime[subsectionId],
2227
- outline: {
2228
- items: [{ name: "Sub of A", level: 3, by: { id: "sub-of-a" } }],
2229
- },
2230
- };
2231
-
2232
- const sectionBId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(4);
2233
- state.cellRuntime[sectionBId] = {
2234
- ...state.cellRuntime[sectionBId],
2235
- outline: {
2236
- items: [{ name: "Section B", level: 2, by: { id: "section-b" } }],
2237
- },
2238
- };
2239
-
2240
- actions.collapseAllCells();
2241
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(headerId)).toBe(
2242
- true,
2243
- );
2244
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).topLevelIds).toEqual([
2245
- firstCellId,
2246
- headerId,
2247
- ]);
1089
+ expect(cell.consoleOutputs).toMatchObject([]);
1090
+ expect(cell).toMatchSnapshot("console cleared while running");
2248
1091
 
2249
- // Both sections sit under the header, collapsed, with the subsection
2250
- // nested under section A
2251
- actions.expandCell({ cellId: headerId });
2252
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionAId)).toBe(
2253
- true,
2254
- );
2255
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(sectionBId)).toBe(
2256
- true,
2257
- );
2258
- actions.expandCell({ cellId: sectionAId });
2259
- expect(
2260
- state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(subsectionId),
2261
- ).toBe(true);
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");
2262
1105
  });
2263
1106
 
2264
- it("can show hidden cells", () => {
2265
- actions.createNewCell({ cellId: firstCellId, before: false });
2266
- actions.createNewCell({ cellId: cellId("1"), before: false });
2267
- actions.collapseCell({ cellId: firstCellId });
1107
+ it("can fold and unfold all cells", () => {
1108
+ actions.foldAll();
1109
+ expect(foldAllBulk).toHaveBeenCalled();
2268
1110
 
2269
- actions.showCellIfHidden({ cellId: cellId("1") });
2270
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).isCollapsed(firstCellId)).toBe(
2271
- false,
2272
- );
1111
+ actions.unfoldAll();
1112
+ expect(unfoldAllBulk).toHaveBeenCalled();
2273
1113
  });
2274
1114
 
2275
- it("can split and undo split cells", () => {
2276
- actions.createNewCell({
2277
- cellId: firstCellId,
2278
- before: false,
2279
- code: "line1\nline2",
2280
- });
2281
- const nextCellId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
2282
-
2283
- const originalCellCount = state.cellIds.atOrThrow(FIRST_COLUMN).length;
2284
- // Move cursor to the second line
2285
- const editor = state.cellHandles[nextCellId].current?.editorView;
2286
- if (!editor) {
2287
- throw new Error("Editor not found");
2288
- }
2289
- editor.dispatch({ selection: { anchor: 5, head: 5 } });
2290
- actions.splitCell({ cellId: nextCellId });
2291
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).length).toBe(
2292
- originalCellCount + 1,
2293
- );
2294
- expect(state.cellData[nextCellId].code).toBe("line1");
2295
- expect(
2296
- state.cellData[state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(2)].code,
2297
- ).toBe("line2");
2298
-
2299
- actions.undoSplitCell({ cellId: nextCellId, snapshot: "line1\nline2" });
2300
- expect(state.cellIds.atOrThrow(FIRST_COLUMN).length).toBe(
2301
- originalCellCount,
2302
- );
2303
- expect(state.cellData[nextCellId].code).toBe("line1\nline2");
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([]);
2304
1132
  });
2305
1133
 
2306
1134
  it("can handle multiple console outputs", () => {
@@ -2339,407 +1167,6 @@ describe("cell reducer", () => {
2339
1167
  `);
2340
1168
  });
2341
1169
 
2342
- it("can add a column breakpoint", () => {
2343
- actions.createNewCell({ cellId: firstCellId, before: false });
2344
- actions.createNewCell({ cellId: cellId("1"), before: false });
2345
- actions.createNewCell({ cellId: cellId("2"), before: false });
2346
-
2347
- expect(state.cellIds.getColumns().length).toBe(1);
2348
- expect(formatCells(state)).toMatchInlineSnapshot(`
2349
- "
2350
- [0] ''
2351
-
2352
- [1] ''
2353
-
2354
- [2] ''
2355
-
2356
- [3] ''
2357
- "
2358
- `);
2359
-
2360
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2361
-
2362
- expect(state.cellIds.getColumns().length).toBe(2);
2363
- expect(formatCells(state)).toMatchInlineSnapshot(`
2364
- "
2365
- > col 0
2366
- [0] ''
2367
-
2368
- [1] ''
2369
-
2370
- > col 1
2371
- [2] ''
2372
-
2373
- [3] ''
2374
- "
2375
- `);
2376
-
2377
- // Check that the cells are in the correct columns
2378
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2379
- cellId("0"),
2380
- cellId("1"),
2381
- ]);
2382
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2383
- cellId("2"),
2384
- cellId("3"),
2385
- ]);
2386
- });
2387
-
2388
- it("cannot add a column breakpoint before the first cell", () => {
2389
- expect(state.cellIds.getColumns().length).toBe(1);
2390
- actions.createNewCell({ cellId: firstCellId, before: false });
2391
- actions.createNewCell({ cellId: cellId("1"), before: false });
2392
- actions.addColumnBreakpoint({ cellId: firstCellId });
2393
- expect(state.cellIds.getColumns().length).toBe(1);
2394
- });
2395
-
2396
- it("can delete a column", () => {
2397
- actions.createNewCell({ cellId: firstCellId, before: false });
2398
- actions.createNewCell({ cellId: cellId("1"), before: false });
2399
- actions.createNewCell({ cellId: cellId("2"), before: false });
2400
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2401
-
2402
- expect(state.cellIds.getColumns().length).toBe(2);
2403
- expect(formatCells(state)).toMatchInlineSnapshot(`
2404
- "
2405
- > col 0
2406
- [0] ''
2407
-
2408
- [1] ''
2409
-
2410
- > col 1
2411
- [2] ''
2412
-
2413
- [3] ''
2414
- "
2415
- `);
2416
-
2417
- const columnId = state.cellIds.atOrThrow(0).id;
2418
- actions.deleteColumn({ columnId: columnId });
2419
-
2420
- expect(state.cellIds.getColumns().length).toBe(1);
2421
- expect(formatCells(state)).toMatchInlineSnapshot(`
2422
- "
2423
- [2] ''
2424
-
2425
- [3] ''
2426
-
2427
- [0] ''
2428
-
2429
- [1] ''
2430
- "
2431
- `);
2432
-
2433
- // Check that all cells are now in the remaining column
2434
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2435
- "2",
2436
- "3",
2437
- "0",
2438
- "1",
2439
- ]);
2440
- });
2441
-
2442
- it("deleting the last column does nothing", () => {
2443
- actions.createNewCell({ cellId: firstCellId, before: false });
2444
- actions.createNewCell({ cellId: cellId("1"), before: false });
2445
-
2446
- const initialState = { ...state };
2447
-
2448
- actions.deleteColumn({ columnId: initialState.cellIds.atOrThrow(0).id });
2449
-
2450
- // State should not change
2451
- expect(state).toEqual(initialState);
2452
- });
2453
-
2454
- it("can drop a cell over another cell", () => {
2455
- actions.createNewCell({ cellId: firstCellId, before: false });
2456
- actions.createNewCell({ cellId: cellId("1"), before: false });
2457
- actions.createNewCell({ cellId: cellId("2"), before: false });
2458
-
2459
- expect(formatCells(state)).toMatchInlineSnapshot(`
2460
- "
2461
- [0] ''
2462
-
2463
- [1] ''
2464
-
2465
- [2] ''
2466
-
2467
- [3] ''
2468
- "
2469
- `);
2470
-
2471
- actions.dropCellOverCell({
2472
- cellId: cellId("0"),
2473
- overCellId: cellId("3"),
2474
- });
2475
-
2476
- expect(formatCells(state)).toMatchInlineSnapshot(`
2477
- "
2478
- [1] ''
2479
-
2480
- [2] ''
2481
-
2482
- [3] ''
2483
-
2484
- [0] ''
2485
- "
2486
- `);
2487
- });
2488
-
2489
- it("can drop a cell over a new column", () => {
2490
- actions.createNewCell({ cellId: firstCellId, before: false });
2491
- actions.createNewCell({ cellId: cellId("1"), before: false });
2492
-
2493
- expect(state.cellIds.getColumns().length).toBe(1);
2494
- expect(formatCells(state)).toMatchInlineSnapshot(`
2495
- "
2496
- [0] ''
2497
-
2498
- [1] ''
2499
-
2500
- [2] ''
2501
- "
2502
- `);
2503
-
2504
- actions.dropOverNewColumn({ cellId: cellId("1") });
2505
-
2506
- expect(state.cellIds.getColumns().length).toBe(2);
2507
- expect(formatCells(state)).toMatchInlineSnapshot(`
2508
- "
2509
- > col 0
2510
- [0] ''
2511
-
2512
- [2] ''
2513
-
2514
- > col 1
2515
- [1] ''
2516
- "
2517
- `);
2518
-
2519
- // Check that the cells are in the correct columns
2520
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2521
- cellId("0"),
2522
- cellId("2"),
2523
- ]);
2524
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2525
- });
2526
-
2527
- it("can drop a column over another column", () => {
2528
- actions.createNewCell({ cellId: firstCellId, before: false });
2529
- actions.createNewCell({ cellId: cellId("1"), before: false });
2530
- actions.createNewCell({ cellId: cellId("2"), before: false });
2531
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2532
-
2533
- expect(state.cellIds.getColumns().length).toBe(2);
2534
- expect(formatCells(state)).toMatchInlineSnapshot(`
2535
- "
2536
- > col 0
2537
- [0] ''
2538
-
2539
- [1] ''
2540
-
2541
- > col 1
2542
- [2] ''
2543
-
2544
- [3] ''
2545
- "
2546
- `);
2547
-
2548
- const columnId0 = state.cellIds.atOrThrow(0).id;
2549
- const columnId1 = state.cellIds.atOrThrow(1).id;
2550
- expect(columnId0).not.toBe(columnId1);
2551
- actions.moveColumn({ column: columnId1, overColumn: columnId0 });
2552
-
2553
- expect(state.cellIds.getColumns().length).toBe(2);
2554
- expect(formatCells(state)).toMatchInlineSnapshot(`
2555
- "
2556
- > col 0
2557
- [2] ''
2558
-
2559
- [3] ''
2560
-
2561
- > col 1
2562
- [0] ''
2563
-
2564
- [1] ''
2565
- "
2566
- `);
2567
-
2568
- // Check that the columns have swapped positions
2569
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2570
- cellId("2"),
2571
- cellId("3"),
2572
- ]);
2573
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2574
- cellId("0"),
2575
- cellId("1"),
2576
- ]);
2577
- });
2578
-
2579
- it("can compact columns", () => {
2580
- // Create initial state with 3 columns, including an empty one
2581
- actions.createNewCell({ cellId: firstCellId, before: false });
2582
- actions.createNewCell({ cellId: cellId("1"), before: false });
2583
- actions.addColumnBreakpoint({ cellId: cellId("1") });
2584
- actions.addColumnBreakpoint({ cellId: cellId("2") });
2585
- actions.dropOverNewColumn({ cellId: cellId("2") });
2586
-
2587
- expect(state.cellIds.getColumns().length).toBe(4);
2588
- expect(formatCells(state)).toMatchInlineSnapshot(`
2589
- "
2590
- > col 0
2591
- [0] ''
2592
-
2593
- > col 1
2594
- [1] ''
2595
-
2596
- > col 2
2597
-
2598
-
2599
- > col 3
2600
- [2] ''
2601
- "
2602
- `);
2603
-
2604
- // Check initial column structure
2605
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2606
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2607
- expect(state.cellIds.getColumns()[2].topLevelIds).toEqual([]);
2608
- expect(state.cellIds.getColumns()[3].topLevelIds).toEqual([cellId("2")]);
2609
-
2610
- // Compact columns
2611
- actions.compactColumns();
2612
-
2613
- expect(state.cellIds.getColumns().length).toBe(3);
2614
- expect(formatCells(state)).toMatchInlineSnapshot(`
2615
- "
2616
- > col 0
2617
- [0] ''
2618
-
2619
- > col 1
2620
- [1] ''
2621
-
2622
- > col 2
2623
- [2] ''
2624
- "
2625
- `);
2626
-
2627
- // Check compacted column structure
2628
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2629
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([cellId("1")]);
2630
- expect(state.cellIds.getColumns()[2].topLevelIds).toEqual([cellId("2")]);
2631
- });
2632
-
2633
- it("rebuildCellColumns regroups cells by config.column", () => {
2634
- // Create four cells in a single column.
2635
- actions.createNewCell({ cellId: firstCellId, before: false });
2636
- actions.createNewCell({ cellId: cellId("1"), before: false });
2637
- actions.createNewCell({ cellId: cellId("2"), before: false });
2638
- expect(state.cellIds.getColumns().length).toBe(1);
2639
-
2640
- // Explicitly set config.column on each cell. This ONLY updates metadata —
2641
- // updateCellConfig does not touch the MultiColumn tree.
2642
- actions.updateCellConfig({
2643
- cellId: cellId("0"),
2644
- config: { column: 0 },
2645
- });
2646
- actions.updateCellConfig({
2647
- cellId: cellId("2"),
2648
- config: { column: 1 },
2649
- });
2650
-
2651
- // Tree is still a single column at this point.
2652
- expect(state.cellIds.getColumns().length).toBe(1);
2653
-
2654
- // Now rebuild the column tree from metadata.
2655
- actions.rebuildCellColumns({
2656
- cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
2657
- });
2658
-
2659
- expect(state.cellIds.getColumns().length).toBe(2);
2660
- // Cell 1 inherits from 0 (col 0), cell 3 inherits from 2 (col 1).
2661
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2662
- cellId("0"),
2663
- cellId("1"),
2664
- ]);
2665
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2666
- cellId("2"),
2667
- cellId("3"),
2668
- ]);
2669
- });
2670
-
2671
- it("rebuildCellColumns with explicit column on every cell", () => {
2672
- actions.createNewCell({ cellId: firstCellId, before: false });
2673
- actions.createNewCell({ cellId: cellId("1"), before: false });
2674
- actions.createNewCell({ cellId: cellId("2"), before: false });
2675
-
2676
- for (const [id, col] of [
2677
- ["0", 1],
2678
- ["1", 0],
2679
- ["2", 1],
2680
- ["3", 0],
2681
- ] as const) {
2682
- actions.updateCellConfig({ cellId: cellId(id), config: { column: col } });
2683
- }
2684
-
2685
- actions.rebuildCellColumns({
2686
- cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
2687
- });
2688
-
2689
- expect(state.cellIds.getColumns().length).toBe(2);
2690
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2691
- cellId("1"),
2692
- cellId("3"),
2693
- ]);
2694
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2695
- cellId("0"),
2696
- cellId("2"),
2697
- ]);
2698
- });
2699
-
2700
- it("rebuildCellColumns collapses to one column when all cells are col 0", () => {
2701
- actions.createNewCell({ cellId: firstCellId, before: false });
2702
- actions.addColumnBreakpoint({ cellId: cellId("1") });
2703
- expect(state.cellIds.getColumns().length).toBe(2);
2704
-
2705
- // Wipe column metadata back to 0 for both cells.
2706
- actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
2707
- actions.updateCellConfig({ cellId: cellId("1"), config: { column: 0 } });
2708
-
2709
- actions.rebuildCellColumns({
2710
- cellIds: [cellId("0"), cellId("1")],
2711
- });
2712
-
2713
- expect(state.cellIds.getColumns().length).toBe(1);
2714
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
2715
- cellId("0"),
2716
- cellId("1"),
2717
- ]);
2718
- });
2719
-
2720
- it("rebuildCellColumns follows the provided order, not current tree order", () => {
2721
- // Start with a single column.
2722
- actions.createNewCell({ cellId: firstCellId, before: false });
2723
- actions.createNewCell({ cellId: cellId("1"), before: false });
2724
- actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
2725
- actions.updateCellConfig({ cellId: cellId("2"), config: { column: 1 } });
2726
-
2727
- // Pass the reversed order. The rebuild should honor it.
2728
- actions.rebuildCellColumns({
2729
- cellIds: [cellId("2"), cellId("1"), cellId("0")],
2730
- });
2731
-
2732
- // cellIds iteration:
2733
- // 2 → col=1, pushed to col1, prev=1
2734
- // 1 → col=null, pushed to col[prev=1] = col1
2735
- // 0 → col=0, pushed to col0
2736
- expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
2737
- expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
2738
- cellId("2"),
2739
- cellId("1"),
2740
- ]);
2741
- });
2742
-
2743
1170
  it("can clear output of a single cell", () => {
2744
1171
  // Set up initial state with output
2745
1172
  actions.handleCellMessage({
@@ -2828,8 +1255,8 @@ describe("cell reducer", () => {
2828
1255
 
2829
1256
  it("can clear output of all cells", () => {
2830
1257
  // Create multiple cells with output
2831
- actions.createNewCell({ cellId: firstCellId, before: false });
2832
- const secondCellId = state.cellIds.atOrThrow(FIRST_COLUMN).atOrThrow(1);
1258
+ seedCell({ cellId: firstCellId, before: false });
1259
+ const secondCellId = state.cellIds[1];
2833
1260
 
2834
1261
  // Add output to both cells
2835
1262
  const outputMessage = {
@@ -2870,126 +1297,14 @@ describe("cell reducer", () => {
2870
1297
 
2871
1298
  // Verify all outputs are cleared
2872
1299
  expect(state.cellRuntime[firstCellId].output).toBeNull();
2873
- expect(state.cellRuntime[firstCellId].consoleOutputs).toEqual([]);
2874
- expect(state.cellRuntime[secondCellId].output).toBeNull();
2875
- expect(state.cellRuntime[secondCellId].consoleOutputs).toEqual([]);
2876
- });
2877
-
2878
- it("skips creating new cell if code exists and skipIfCodeExists is true", () => {
2879
- // Add initial cell with code
2880
- actions.updateCellCode({
2881
- cellId: firstCellId,
2882
- code: "import numpy as np",
2883
- formattingChange: false,
2884
- });
2885
-
2886
- // Try to create new cell with same code and skipIfCodeExists
2887
- actions.createNewCell({
2888
- cellId: "__end__",
2889
- code: "import numpy as np",
2890
- before: false,
2891
- skipIfCodeExists: true,
2892
- });
2893
-
2894
- // Should still only have one cell
2895
- expect(state.cellIds.inOrderIds.length).toBe(1);
2896
- expect(formatCells(state)).toMatchInlineSnapshot(`
2897
- "
2898
- [0] 'import numpy as np'
2899
- "
2900
- `);
2901
-
2902
- // Verify we can still add cell with different code
2903
- actions.createNewCell({
2904
- cellId: "__end__",
2905
- code: "import pandas as pd",
2906
- before: false,
2907
- skipIfCodeExists: true,
2908
- });
2909
-
2910
- expect(state.cellIds.inOrderIds.length).toBe(2);
2911
- expect(formatCells(state)).toMatchInlineSnapshot(`
2912
- "
2913
- [0] 'import numpy as np'
2914
-
2915
- [1] 'import pandas as pd'
2916
- "
2917
- `);
2918
- });
2919
-
2920
- it("can create and noop-update a setup cell", () => {
2921
- // Create the setup cell
2922
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2923
-
2924
- // Check that setup cell was created
2925
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2926
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2927
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2928
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2929
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2930
-
2931
- // Update the setup cell
2932
- actions.addSetupCellIfDoesntExist({ code: "# Updated setup code" });
2933
-
2934
- // Check that the setup cell did not change, since it already exists
2935
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2936
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2937
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2938
- });
2939
-
2940
- it("can delete and undelete the setup cell", () => {
2941
- // Create the setup cell
2942
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2943
-
2944
- // Check that setup cell was created
2945
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2946
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2947
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2948
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2949
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2950
-
2951
- // Delete the setup cell
2952
- actions.deleteCell({ cellId: SETUP_CELL_ID });
2953
-
2954
- // Check that setup cell was deleted
2955
- expect(state.cellData[SETUP_CELL_ID]).toBeDefined(); // we keep old state
2956
- expect(state.cellIds.inOrderIds).not.toContain(SETUP_CELL_ID);
2957
-
2958
- // Undo delete the setup cell
2959
- actions.undoDeleteCell();
2960
-
2961
- // Check that setup cell was restored
2962
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2963
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2964
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# Setup code");
2965
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2966
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
2967
-
2968
- // Verify scrollKey is set to the restored setup cell
2969
- expect(state.scrollKey).toBe(SETUP_CELL_ID);
2970
- });
2971
-
2972
- it("can delete and then create a new setup cell", () => {
2973
- // Create the setup cell
2974
- actions.addSetupCellIfDoesntExist({ code: "# Setup code" });
2975
-
2976
- // Delete the setup cell
2977
- actions.deleteCell({ cellId: SETUP_CELL_ID });
2978
-
2979
- // Create a new setup cell
2980
- actions.addSetupCellIfDoesntExist({ code: "# New code" });
2981
-
2982
- // Check that the new setup cell was created
2983
- expect(state.cellData[SETUP_CELL_ID].id).toBe(SETUP_CELL_ID);
2984
- expect(state.cellData[SETUP_CELL_ID].name).toBe("setup");
2985
- expect(state.cellData[SETUP_CELL_ID].code).toBe("# New code");
2986
- expect(state.cellData[SETUP_CELL_ID].edited).toBe(true);
2987
- expect(state.cellIds.inOrderIds).toContain(SETUP_CELL_ID);
1300
+ expect(state.cellRuntime[firstCellId].consoleOutputs).toEqual([]);
1301
+ expect(state.cellRuntime[secondCellId].output).toBeNull();
1302
+ expect(state.cellRuntime[secondCellId].consoleOutputs).toEqual([]);
2988
1303
  });
2989
1304
 
2990
1305
  it("can clear all outputs", () => {
2991
1306
  // Add a cell and give it output
2992
- actions.createNewCell({
1307
+ seedCell({
2993
1308
  cellId: firstCellId,
2994
1309
  before: false,
2995
1310
  });
@@ -3017,522 +1332,13 @@ describe("cell reducer", () => {
3017
1332
  expect(state.cellRuntime[cell2Id].output).toBeNull();
3018
1333
  });
3019
1334
 
3020
- describe("moveToNextCell", () => {
3021
- let cell1Id: CellId;
3022
- let cell2Id: CellId;
3023
- let cell3Id: CellId;
3024
-
3025
- beforeEach(() => {
3026
- // Create a few cells to work with
3027
- actions.createNewCell({ cellId: "__end__", before: false });
3028
- actions.createNewCell({ cellId: "__end__", before: false });
3029
-
3030
- cell1Id = state.cellIds.inOrderIds[0];
3031
- cell2Id = state.cellIds.inOrderIds[1];
3032
- cell3Id = state.cellIds.inOrderIds[2];
3033
- });
3034
-
3035
- it("creates new cell when moving after last cell with noCreate=false", () => {
3036
- const initialCellCount = state.cellIds.inOrderIds.length;
3037
-
3038
- actions.moveToNextCell({
3039
- cellId: cell3Id,
3040
- before: false,
3041
- noCreate: false,
3042
- });
3043
-
3044
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3045
- });
3046
-
3047
- it("creates new cell when moving before first cell with noCreate=false", () => {
3048
- const initialCellCount = state.cellIds.inOrderIds.length;
3049
-
3050
- actions.moveToNextCell({
3051
- cellId: cell1Id,
3052
- before: true,
3053
- noCreate: false,
3054
- });
3055
-
3056
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3057
- });
3058
-
3059
- it("does not create new cell when moving after last cell with noCreate=true", () => {
3060
- const initialCellCount = state.cellIds.inOrderIds.length;
3061
- const initialState = { ...state };
3062
-
3063
- actions.moveToNextCell({
3064
- cellId: cell3Id,
3065
- before: false,
3066
- noCreate: true,
3067
- });
3068
-
3069
- // Should not create a new cell
3070
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount);
3071
- // Should not crash or throw an error
3072
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3073
- });
3074
-
3075
- it("does not create new cell when moving before first cell with noCreate=true", () => {
3076
- const initialCellCount = state.cellIds.inOrderIds.length;
3077
- const initialState = { ...state };
3078
-
3079
- actions.moveToNextCell({ cellId: cell1Id, before: true, noCreate: true });
3080
-
3081
- // Should not create a new cell
3082
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount);
3083
- // Should not crash or throw an error
3084
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3085
- });
3086
-
3087
- it("focuses next cell when moving within bounds", () => {
3088
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3089
- focusSpy.mockClear();
3090
-
3091
- actions.moveToNextCell({
3092
- cellId: cell1Id,
3093
- before: false,
3094
- noCreate: true,
3095
- });
3096
-
3097
- expect(focusSpy).toHaveBeenCalledWith({
3098
- cellId: cell2Id,
3099
- cell: state.cellHandles[cell2Id],
3100
- isCodeHidden: false,
3101
- codeFocus: "top",
3102
- variableName: undefined,
3103
- });
3104
- });
3105
-
3106
- it("focuses previous cell when moving backward within bounds", () => {
3107
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3108
- focusSpy.mockClear();
3109
-
3110
- actions.moveToNextCell({ cellId: cell2Id, before: true, noCreate: true });
3111
-
3112
- expect(focusSpy).toHaveBeenCalledWith({
3113
- cellId: cell1Id,
3114
- cell: state.cellHandles[cell1Id],
3115
- isCodeHidden: false,
3116
- codeFocus: "bottom",
3117
- variableName: undefined,
3118
- });
3119
- });
3120
-
3121
- it("does not move focus from scratch cell", () => {
3122
- const focusSpy = vi.mocked(focusAndScrollCellIntoView);
3123
- focusSpy.mockClear();
3124
- const initialState = { ...state };
3125
-
3126
- actions.moveToNextCell({
3127
- cellId: SCRATCH_CELL_ID,
3128
- before: false,
3129
- noCreate: false,
3130
- });
3131
-
3132
- expect(focusSpy).not.toHaveBeenCalled();
3133
- expect(state.cellIds.inOrderIds).toEqual(initialState.cellIds.inOrderIds);
3134
- });
3135
- });
3136
-
3137
- describe("untouched cells functionality", () => {
3138
- it("starts with empty untouchedNewCells set", () => {
3139
- expect(state.untouchedNewCells).toEqual(new Set());
3140
- });
3141
-
3142
- it("can create a new cell with hideCode option", () => {
3143
- const initialCellCount = state.cellIds.inOrderIds.length;
3144
-
3145
- actions.createNewCell({
3146
- cellId: "__end__",
3147
- before: false,
3148
- hideCode: true,
3149
- });
3150
-
3151
- expect(state.cellIds.inOrderIds.length).toBe(initialCellCount + 1);
3152
- const newCellId =
3153
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3154
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3155
- });
3156
-
3157
- it("adds interactively-created hidden cell with boilerplate code to untouchedNewCells", () => {
3158
- // Markdown cells are created with hideCode and non-empty default code
3159
- // (e.g. `mo.md(r"""\n""")`). They are user-initiated, so their editor
3160
- // should be shown until first blur.
3161
- actions.createNewCell({
3162
- cellId: "__end__",
3163
- before: false,
3164
- code: 'mo.md(r"""\n""")',
3165
- hideCode: true,
3166
- autoFocus: true,
3167
- });
3168
-
3169
- const newCellId =
3170
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3171
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3172
- });
3173
-
3174
- it("does not add programmatically-created hidden cell with code to untouchedNewCells", () => {
3175
- // Cells created by the kernel (e.g. via code_mode) carry code and
3176
- // autoFocus=false; their hide_code must take effect immediately.
3177
- actions.createNewCell({
3178
- cellId: "__end__",
3179
- before: false,
3180
- code: "x = 1",
3181
- hideCode: true,
3182
- autoFocus: false,
3183
- });
3184
-
3185
- const newCellId =
3186
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3187
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3188
- });
3189
-
3190
- it("does not add cell to untouchedNewCells when hideCode is false", () => {
3191
- actions.createNewCell({
3192
- cellId: "__end__",
3193
- before: false,
3194
- hideCode: false,
3195
- });
3196
-
3197
- const newCellId =
3198
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3199
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3200
- });
3201
-
3202
- it("does not add cell to untouchedNewCells when hideCode is undefined", () => {
3203
- actions.createNewCell({
3204
- cellId: "__end__",
3205
- before: false,
3206
- });
3207
-
3208
- const newCellId =
3209
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3210
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3211
- });
3212
-
3213
- it("can mark a cell as touched", () => {
3214
- // Create a cell with hideCode
3215
- actions.createNewCell({
3216
- cellId: "__end__",
3217
- before: false,
3218
- hideCode: true,
3219
- });
3220
-
3221
- const newCellId =
3222
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3223
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3224
-
3225
- // Mark it as touched
3226
- actions.markTouched({ cellId: newCellId });
3227
-
3228
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3229
- });
3230
-
3231
- it("markTouched is idempotent", () => {
3232
- // Create a cell with hideCode
3233
- actions.createNewCell({
3234
- cellId: "__end__",
3235
- before: false,
3236
- hideCode: true,
3237
- });
3238
-
3239
- const newCellId =
3240
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3241
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3242
-
3243
- // Mark it as touched multiple times
3244
- actions.markTouched({ cellId: newCellId });
3245
- actions.markTouched({ cellId: newCellId });
3246
- actions.markTouched({ cellId: newCellId });
3247
-
3248
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3249
- });
3250
-
3251
- it("can mark a non-existent cell as touched without error", () => {
3252
- const nonExistentCellId = cellId("non-existent");
3253
-
3254
- expect(() => {
3255
- actions.markTouched({ cellId: nonExistentCellId });
3256
- }).not.toThrow();
3257
-
3258
- expect(state.untouchedNewCells.has(nonExistentCellId)).toBe(false);
3259
- });
3260
-
3261
- it("can handle multiple untouched cells", () => {
3262
- // Create multiple cells with hideCode
3263
- actions.createNewCell({
3264
- cellId: "__end__",
3265
- before: false,
3266
- hideCode: true,
3267
- });
3268
- actions.createNewCell({
3269
- cellId: "__end__",
3270
- before: false,
3271
- hideCode: true,
3272
- });
3273
- actions.createNewCell({
3274
- cellId: "__end__",
3275
- before: false,
3276
- hideCode: false,
3277
- });
3278
-
3279
- const cellIds = state.cellIds.inOrderIds;
3280
- const cell1Id = cellIds[cellIds.length - 3];
3281
- const cell2Id = cellIds[cellIds.length - 2];
3282
- const cell3Id = cellIds[cellIds.length - 1];
3283
-
3284
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3285
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3286
- expect(state.untouchedNewCells.has(cell3Id)).toBe(false);
3287
-
3288
- // Mark one as touched
3289
- actions.markTouched({ cellId: cell1Id });
3290
-
3291
- expect(state.untouchedNewCells.has(cell1Id)).toBe(false);
3292
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3293
- expect(state.untouchedNewCells.has(cell3Id)).toBe(false);
3294
- });
3295
-
3296
- it("preserves untouched state when cells are moved", () => {
3297
- // Create cells with hideCode
3298
- actions.createNewCell({
3299
- cellId: "__end__",
3300
- before: false,
3301
- hideCode: true,
3302
- });
3303
- actions.createNewCell({
3304
- cellId: "__end__",
3305
- before: false,
3306
- hideCode: true,
3307
- });
3308
-
3309
- const cellIds = state.cellIds.inOrderIds;
3310
- const cell1Id = cellIds[cellIds.length - 2];
3311
- const cell2Id = cellIds[cellIds.length - 1];
3312
-
3313
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3314
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3315
-
3316
- // Move cell1 to the end
3317
- actions.moveCell({
3318
- cellId: cell1Id,
3319
- before: false,
3320
- });
3321
-
3322
- // Both cells should still be untouched
3323
- expect(state.untouchedNewCells.has(cell1Id)).toBe(true);
3324
- expect(state.untouchedNewCells.has(cell2Id)).toBe(true);
3325
- });
3326
-
3327
- it("does not remove untouched state when cell is deleted", () => {
3328
- // We could implement this, but it doesn't actually affect downstream behavior.
3329
- // It is easier to leave it, so `undo` works as expected.
3330
-
3331
- // Create a cell with hideCode
3332
- actions.createNewCell({
3333
- cellId: "__end__",
3334
- before: false,
3335
- hideCode: true,
3336
- });
3337
-
3338
- const newCellId =
3339
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3340
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3341
-
3342
- // Delete the cell
3343
- actions.deleteCell({ cellId: newCellId });
3344
-
3345
- // Does not actually remove untouched state
3346
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3347
- });
3348
-
3349
- it("restores untouched state when cell deletion is undone", () => {
3350
- // Create a cell with hideCode
3351
- actions.createNewCell({
3352
- cellId: "__end__",
3353
- before: false,
3354
- hideCode: true,
3355
- });
3356
-
3357
- const newCellId =
3358
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3359
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3360
-
3361
- // Delete the cell
3362
- actions.deleteCell({ cellId: newCellId });
3363
- // Still exists in untouchedNewCells
3364
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3365
-
3366
- // Undo the deletion
3367
- actions.undoDeleteCell();
3368
-
3369
- // The cell should be restored but no longer untouched
3370
- // (this is expected behavior - undoing doesn't restore untouched state)
3371
- const restoredCellId =
3372
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3373
- expect(state.untouchedNewCells.has(restoredCellId)).toBe(false);
3374
- });
3375
-
3376
- it("untouched cell behavior with hide_code config", () => {
3377
- // Create a cell with hideCode and hide_code config
3378
- actions.createNewCell({
3379
- cellId: "__end__",
3380
- before: false,
3381
- hideCode: true,
3382
- });
3383
-
3384
- const newCellId =
3385
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3386
-
3387
- // Update the cell config to hide code
3388
- actions.updateCellConfig({
3389
- cellId: newCellId,
3390
- config: { hide_code: true },
3391
- });
3392
-
3393
- // Cell should be untouched and have hide_code config
3394
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3395
- expect(state.cellData[newCellId].config.hide_code).toBe(true);
3396
-
3397
- // Code should not be hidden because cell is untouched
3398
- expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(false);
3399
-
3400
- // Mark as touched
3401
- actions.markTouched({ cellId: newCellId });
3402
-
3403
- // Now code should be hidden
3404
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3405
- expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(true);
3406
- });
3407
-
3408
- it("can mark an existing cell as untouched", () => {
3409
- // Create a cell without hideCode (not in untouchedNewCells)
3410
- actions.createNewCell({
3411
- cellId: "__end__",
3412
- before: false,
3413
- hideCode: false,
3414
- });
3415
-
3416
- const newCellId =
3417
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3418
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3419
-
3420
- // Mark it as untouched
3421
- actions.markUntouched({ cellId: newCellId });
3422
-
3423
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3424
- });
3425
-
3426
- it("markUntouched is idempotent", () => {
3427
- // Create a cell without hideCode
3428
- actions.createNewCell({
3429
- cellId: "__end__",
3430
- before: false,
3431
- hideCode: false,
3432
- });
3433
-
3434
- const newCellId =
3435
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3436
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3437
-
3438
- // Mark as untouched multiple times
3439
- actions.markUntouched({ cellId: newCellId });
3440
- actions.markUntouched({ cellId: newCellId });
3441
- actions.markUntouched({ cellId: newCellId });
3442
-
3443
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3444
- });
3445
-
3446
- it("markUntouched does not affect already untouched cells", () => {
3447
- // Create a cell with hideCode (already in untouchedNewCells)
3448
- actions.createNewCell({
3449
- cellId: "__end__",
3450
- before: false,
3451
- hideCode: true,
3452
- });
3453
-
3454
- const newCellId =
3455
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3456
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3457
-
3458
- // Calling markUntouched should not change anything
3459
- actions.markUntouched({ cellId: newCellId });
3460
-
3461
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3462
- });
3463
-
3464
- it("markTouched and markUntouched can toggle cell state", () => {
3465
- // Create a cell without hideCode
3466
- actions.createNewCell({
3467
- cellId: "__end__",
3468
- before: false,
3469
- hideCode: false,
3470
- });
3471
-
3472
- const newCellId =
3473
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3474
-
3475
- // Initially not untouched
3476
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3477
-
3478
- // Mark as untouched
3479
- actions.markUntouched({ cellId: newCellId });
3480
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3481
-
3482
- // Mark as touched
3483
- actions.markTouched({ cellId: newCellId });
3484
- expect(state.untouchedNewCells.has(newCellId)).toBe(false);
3485
-
3486
- // Mark as untouched again
3487
- actions.markUntouched({ cellId: newCellId });
3488
- expect(state.untouchedNewCells.has(newCellId)).toBe(true);
3489
- });
3490
-
3491
- it("markUntouched works for markdown cell conversion scenario", () => {
3492
- // Simulates converting a Python cell to Markdown
3493
- // 1. Create a regular cell (no hideCode)
3494
- actions.createNewCell({
3495
- cellId: "__end__",
3496
- before: false,
3497
- hideCode: false,
3498
- });
3499
-
3500
- const cellId =
3501
- state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
3502
-
3503
- // Cell starts without hide_code and not in untouchedNewCells
3504
- expect(state.cellData[cellId].config.hide_code).toBe(false);
3505
- expect(state.untouchedNewCells.has(cellId)).toBe(false);
3506
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
3507
-
3508
- // 2. Convert to markdown: set hide_code and mark as untouched
3509
- actions.updateCellConfig({
3510
- cellId,
3511
- config: { hide_code: true },
3512
- });
3513
- actions.markUntouched({ cellId });
3514
-
3515
- // Code should NOT be hidden because cell is untouched (user can edit)
3516
- expect(state.cellData[cellId].config.hide_code).toBe(true);
3517
- expect(state.untouchedNewCells.has(cellId)).toBe(true);
3518
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
3519
-
3520
- // 3. User blurs the cell (markTouched)
3521
- actions.markTouched({ cellId });
3522
-
3523
- // Now code should be hidden
3524
- expect(state.untouchedNewCells.has(cellId)).toBe(false);
3525
- expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(true);
3526
- });
3527
- });
3528
-
3529
1335
  describe("releaseCellAtoms", () => {
3530
1336
  it("atom families cache atoms until explicitly removed", () => {
3531
1337
  const { cellDataAtom, cellRuntimeAtom, cellHandleAtom } =
3532
1338
  exportedForTesting;
3533
1339
 
3534
- actions.createNewCell({ cellId: firstCellId, before: false });
3535
- const newCellId = state.cellIds.inOrderIds[1];
1340
+ seedCell({ cellId: firstCellId, before: false });
1341
+ const newCellId = state.cellIds[1];
3536
1342
 
3537
1343
  const dataAtom1 = cellDataAtom(newCellId);
3538
1344
  const runtimeAtom1 = cellRuntimeAtom(newCellId);
@@ -3547,116 +1353,6 @@ describe("cell reducer", () => {
3547
1353
  expect(runtimeAtom2).toBe(runtimeAtom1);
3548
1354
  expect(handleAtom2).toBe(handleAtom1);
3549
1355
  });
3550
-
3551
- it("cleans up atom family cache when cells are deleted", () => {
3552
- const { cellDataAtom, cellRuntimeAtom, cellHandleAtom } =
3553
- exportedForTesting;
3554
-
3555
- actions.createNewCell({ cellId: firstCellId, before: false });
3556
- const newCellId = state.cellIds.inOrderIds[1];
3557
-
3558
- // Access to create cache entries
3559
- const dataAtom1 = cellDataAtom(newCellId);
3560
- const runtimeAtom1 = cellRuntimeAtom(newCellId);
3561
- const handleAtom1 = cellHandleAtom(newCellId);
3562
-
3563
- // Triggers purge
3564
- actions.deleteCell({ cellId: newCellId });
3565
-
3566
- // Access again (should be new instances after cleanup)
3567
- const dataAtom2 = cellDataAtom(newCellId);
3568
- const runtimeAtom2 = cellRuntimeAtom(newCellId);
3569
- const handleAtom2 = cellHandleAtom(newCellId);
3570
-
3571
- expect(dataAtom2).not.toBe(dataAtom1);
3572
- expect(runtimeAtom2).not.toBe(runtimeAtom1);
3573
- expect(handleAtom2).not.toBe(handleAtom1);
3574
- });
3575
- });
3576
- });
3577
-
3578
- describe("isCellCodeHidden", () => {
3579
- const state = initialNotebookState();
3580
- const firstCellId = state.cellIds.inOrderIds[0];
3581
-
3582
- it("returns false when hide_code is false and cell is not untouched", () => {
3583
- const testCellId = cellId("test-cell");
3584
- const testState: NotebookState = {
3585
- ...state,
3586
- cellData: {
3587
- ...state.cellData,
3588
- [testCellId]: {
3589
- ...state.cellData[firstCellId],
3590
- id: testCellId,
3591
- config: { hide_code: false, disabled: false, column: null },
3592
- },
3593
- },
3594
- untouchedNewCells: new Set(),
3595
- };
3596
-
3597
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3598
- false,
3599
- );
3600
- });
3601
-
3602
- it("returns true when hide_code is true and cell is not untouched", () => {
3603
- const testCellId = cellId("test-cell");
3604
- const testState: NotebookState = {
3605
- ...state,
3606
- cellData: {
3607
- ...state.cellData,
3608
- [testCellId]: {
3609
- ...state.cellData[firstCellId],
3610
- id: testCellId,
3611
- config: { hide_code: true, disabled: false, column: null },
3612
- },
3613
- },
3614
- untouchedNewCells: new Set(),
3615
- };
3616
-
3617
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3618
- true,
3619
- );
3620
- });
3621
-
3622
- it("returns false when hide_code is true but cell is untouched", () => {
3623
- const testCellId = cellId("test-cell");
3624
- const testState: NotebookState = {
3625
- ...state,
3626
- cellData: {
3627
- ...state.cellData,
3628
- [testCellId]: {
3629
- ...state.cellData[firstCellId],
3630
- id: testCellId,
3631
- config: { hide_code: true, disabled: false, column: null },
3632
- },
3633
- },
3634
- untouchedNewCells: new Set([testCellId]),
3635
- };
3636
-
3637
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3638
- false,
3639
- );
3640
- });
3641
-
3642
- it("returns false when hide_code is false and cell is untouched", () => {
3643
- const testCellId = cellId("test-cell");
3644
- const testState: NotebookState = {
3645
- ...state,
3646
- cellData: {
3647
- ...state.cellData,
3648
- [testCellId]: {
3649
- ...state.cellData[firstCellId],
3650
- id: testCellId,
3651
- config: { hide_code: false, disabled: false, column: null },
3652
- },
3653
- },
3654
- untouchedNewCells: new Set([testCellId]),
3655
- };
3656
-
3657
- expect(exportedForTesting.isCellCodeHidden(testState, testCellId)).toBe(
3658
- false,
3659
- );
3660
1356
  });
3661
1357
  });
3662
1358
 
@@ -3886,31 +1582,9 @@ describe("createTracebackInfoAtom", () => {
3886
1582
  });
3887
1583
  });
3888
1584
 
3889
- describe("setCells snapshot preservation", () => {
1585
+ describe("kernel-ready", () => {
3890
1586
  const CELL_A = cellId("A");
3891
1587
  const CELL_B = cellId("B");
3892
- const newCells: CellData[] = [
3893
- {
3894
- id: CELL_A,
3895
- name: "a",
3896
- code: "1",
3897
- edited: false,
3898
- lastCodeRun: null,
3899
- lastExecutionTime: null,
3900
- config: { hide_code: false, disabled: false, column: null },
3901
- serializedEditorState: null,
3902
- },
3903
- {
3904
- id: CELL_B,
3905
- name: "b",
3906
- code: "2",
3907
- edited: false,
3908
- lastCodeRun: null,
3909
- lastExecutionTime: null,
3910
- config: { hide_code: false, disabled: false, column: null },
3911
- serializedEditorState: null,
3912
- },
3913
- ];
3914
1588
 
3915
1589
  const hydratedState = () =>
3916
1590
  MockNotebook.notebookState({
@@ -3940,44 +1614,97 @@ describe("setCells snapshot preservation", () => {
3940
1614
  },
3941
1615
  });
3942
1616
 
3943
- beforeEach(async () => {
3944
- const { isWasm } = await import("@/core/wasm/utils");
3945
- 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);
3946
1638
  });
3947
1639
 
3948
- it("preserves hydrated output in WASM", () => {
1640
+ it("resetCellRuntimes starts every cell's runtime over", () => {
3949
1641
  const next = exportedForTesting.reducer(hydratedState(), {
3950
- type: "setCells",
3951
- payload: newCells,
1642
+ type: "resetCellRuntimes",
1643
+ payload: undefined,
3952
1644
  });
3953
1645
 
3954
- expect(next.cellRuntime[CELL_A].output).toMatchObject({
3955
- 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
+ },
3956
1663
  });
1664
+ const next = exportedForTesting.reducer(state, {
1665
+ type: "resetCellRuntimes",
1666
+ payload: undefined,
1667
+ });
1668
+
1669
+ expect(next.cellRuntime[SCRATCH_CELL_ID].output).toBeNull();
3957
1670
  });
3958
1671
 
3959
- it("preserves console-only hydration in WASM", () => {
3960
- const next = exportedForTesting.reducer(hydratedState(), {
3961
- type: "setCells",
3962
- payload: newCells,
1672
+ describe("resetCellRuntimes in WASM", () => {
1673
+ beforeEach(async () => {
1674
+ const { isWasm } = await import("@/core/wasm/utils");
1675
+ vi.mocked(isWasm).mockReturnValue(true);
3963
1676
  });
3964
1677
 
3965
- expect(next.cellRuntime[CELL_B].consoleOutputs).toHaveLength(1);
3966
- expect(next.cellRuntime[CELL_B].consoleOutputs[0]).toMatchObject({
3967
- data: "hydrated-B-stdout",
1678
+ afterEach(async () => {
1679
+ const { isWasm } = await import("@/core/wasm/utils");
1680
+ vi.mocked(isWasm).mockReturnValue(false);
3968
1681
  });
3969
- });
3970
1682
 
3971
- it("resets cells with no prior runtime even in WASM", () => {
3972
- const empty = MockNotebook.notebookState({ cellData: {} });
3973
- const next = exportedForTesting.reducer(empty, {
3974
- type: "setCells",
3975
- payload: newCells,
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
+ });
3976
1695
  });
3977
1696
 
3978
- expect(next.cellRuntime[CELL_A].output).toBeNull();
3979
- expect(next.cellRuntime[CELL_A].consoleOutputs).toEqual([]);
3980
- expect(next.cellRuntime[CELL_B].output).toBeNull();
3981
- expect(next.cellRuntime[CELL_B].consoleOutputs).toEqual([]);
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
+ });
3982
1709
  });
3983
1710
  });