@marimo-team/frontend 0.25.3-dev4 → 0.25.3-dev42

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 (286) hide show
  1. package/dist/assets/CellStatus-BpOBz-8m.js +1 -0
  2. package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-D9UK8o9A.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-Gl5nCA8C.js → ErrorBoundary-DmXl4Nre.js} +2 -2
  4. package/dist/assets/JsonOutput-BJraU9kp.js +11 -0
  5. package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-BPYE76jR.js} +2 -2
  6. package/dist/assets/MarimoErrorOutput-DVjK8RgY.js +10 -0
  7. package/dist/assets/{RenderHTML-BbOqmHy3.js → RenderHTML-BFaztTR0.js} +1 -1
  8. package/dist/assets/{RunButton-CK3LhnP5.js → RunButton-B44xE_d5.js} +1 -1
  9. package/dist/assets/{add-cell-with-ai-8UBC0xwR.js → add-cell-with-ai-CkjslziO.js} +13 -13
  10. package/dist/assets/{add-connection-dialog-DXQnd6eB.js → add-connection-dialog-CWLLVKju.js} +2 -2
  11. package/dist/assets/{agent-panel-DdkVL-Kx.js → agent-panel-DYdQ_2pp.js} +5 -5
  12. package/dist/assets/ai-model-dropdown-F3mm17Wa.js +13 -0
  13. package/dist/assets/{alert-dialog-BaPGzeOK.js → alert-dialog-C-sWmKva.js} +1 -1
  14. package/dist/assets/any-language-editor-DUMe1UCk.js +1 -0
  15. package/dist/assets/{app-config-button-BKEG4x9u.js → app-config-button-D0uDs5nC.js} +1 -1
  16. package/dist/assets/arrays-0R2reF2y.js +1 -0
  17. package/dist/assets/{blob-D5nfNHOo.js → blob-BSfsdCNF.js} +1 -1
  18. package/dist/assets/{button-COI-NGrx.js → button-D8gVKwe-.js} +1 -1
  19. package/dist/assets/{cache-panel-DTY1dqvi.js → cache-panel-Byolv2ht.js} +1 -1
  20. package/dist/assets/{capabilities-BJvf-MD4.js → capabilities-Pq1YNPlK.js} +1 -1
  21. package/dist/assets/{cell-editor-32lVhib7.js → cell-editor-DzvvHs9g.js} +9 -9
  22. package/dist/assets/{cell-link-LSth_8hT.js → cell-link-DFPHORAO.js} +1 -1
  23. package/dist/assets/{cells-BRkCaSsh.js → cells-BdgzY0of.js} +31 -31
  24. package/dist/assets/{chat-display-Dnagy4Nv.js → chat-display-BrcwdhKJ.js} +1 -1
  25. package/dist/assets/{chat-panel-DN9vsbgS.js → chat-panel-C__3iSyI.js} +2 -2
  26. package/dist/assets/{chat-ui-Df8Vcj6n.js → chat-ui-NlBK3kK6.js} +1 -1
  27. package/dist/assets/{circle-DXf-VWZG.js → circle-B6D_6D8w.js} +1 -1
  28. package/dist/assets/code-visibility-Cs-Vv3jV.js +1 -0
  29. package/dist/assets/coerce-DWjFeXOY.js +1 -0
  30. package/dist/assets/{column-preview-CHK_DNrs.js → column-preview-ivm1b-GY.js} +1 -1
  31. package/dist/assets/command-palette-TgwswwvD.js +1 -0
  32. package/dist/assets/{common-Cu6CDbuw.js → common-BcEY1yDS.js} +1 -1
  33. package/dist/assets/{components-CP_UoFtT.js → components-DrU8dOzy.js} +1 -1
  34. package/dist/assets/{components-CXfO-F47.js → components-lDm0qi2t.js} +1 -1
  35. package/dist/assets/{components-CGw16R_N.js → components-lkCfd2vy.js} +1 -1
  36. package/dist/assets/config-BwHT0yGp.js +104 -0
  37. package/dist/assets/config-DGX7JYor.js +1 -0
  38. package/dist/assets/config-DXjnAwtL.js +1 -0
  39. package/dist/assets/{copy-D8O_CYCG.js → copy-KlhazlhB.js} +1 -1
  40. package/dist/assets/{copy-icon-BIiRXh_3.js → copy-icon-8-a5vovc.js} +1 -1
  41. package/dist/assets/{createReducer-DgRODTW6.js → createReducer-HKXUbqyz.js} +1 -1
  42. package/dist/assets/{datasource-Cq6x_5ML.js → datasource-dKk3JHSQ.js} +1 -1
  43. package/dist/assets/{dates-CfmdTL6B.js → dates-CQMwFko2.js} +1 -1
  44. package/dist/assets/{dependency-graph-panel--Kgtktcw.js → dependency-graph-panel-BaisS9Zt.js} +1 -1
  45. package/dist/assets/{dist-iaThVDkA.js → dist-jlhMTKo_.js} +1 -1
  46. package/dist/assets/{documentation-panel-BHmbeznw.js → documentation-panel-CpSUoM8r.js} +1 -1
  47. package/dist/assets/{download-DCJXy66H.js → download-RV4i1fWI.js} +1 -1
  48. package/dist/assets/{dropdown-menu-C1rnl-0o.js → dropdown-menu-DS6umAnH.js} +1 -1
  49. package/dist/assets/edit-page-C8RUVAaO.js +9 -0
  50. package/dist/assets/{error-banner-Cd69vvyX.js → error-banner-B5kzVf_0.js} +1 -1
  51. package/dist/assets/{error-entries-Dk3VZ-OK.js → error-entries-CJS0gOId.js} +1 -1
  52. package/dist/assets/{error-panel-BPRYmC5J.js → error-panel-BxCKpZtN.js} +1 -1
  53. package/dist/assets/{errors-DCrsa1nH.js → errors-CNsgAw6m.js} +1 -1
  54. package/dist/assets/{es-Cqst59WX.js → es-N9SHpmG7.js} +1 -1
  55. package/dist/assets/feedback-button-BLvwvndE.js +1 -0
  56. package/dist/assets/{feedback-button-Bik2-e33.js → feedback-button-DYjae3_b.js} +2 -2
  57. package/dist/assets/{file-explorer-panel-BmekFu7T.js → file-explorer-panel-BmjAJusg.js} +1 -1
  58. package/dist/assets/{file-name-input-Cdc-TVqO.js → file-name-input-BpfSzvEB.js} +1 -1
  59. package/dist/assets/{floating-outline-XD9FAV-f.js → floating-outline-CrWdiNQ2.js} +1 -1
  60. package/dist/assets/{formats-yZ06h1Pl.js → formats-zylfwejq.js} +1 -1
  61. package/dist/assets/{formatting-BqrP-n49.js → formatting-uom1es4b.js} +1 -1
  62. package/dist/assets/{fullscreen-B9tHcfpV.js → fullscreen-BWTs8pYZ.js} +1 -1
  63. package/dist/assets/{gallery-page-Dm6mGwBi.js → gallery-page-W_b-XWq6.js} +1 -1
  64. package/dist/assets/{ganttDiagram-EL5Y4UJY-2n7Ig5Jm.js → ganttDiagram-EL5Y4UJY-CYqkCfFG.js} +1 -1
  65. package/dist/assets/{glide-data-editor-J9CAb70d.js → glide-data-editor-C_bIL30J.js} +2 -2
  66. package/dist/assets/{glide-utils-CIrAxdjv.js → glide-utils-HFqhvWHI.js} +2 -2
  67. package/dist/assets/{home-page-s3LUizSn.js → home-page-D1teQDdF.js} +3 -3
  68. package/dist/assets/hooks-CZF85I2f.js +1 -0
  69. package/dist/assets/{html-to-image-DK9Zl1lU.js → html-to-image-D_0rd3_B.js} +1 -1
  70. package/dist/assets/index-4Nwpioic.js +30 -0
  71. package/dist/assets/index-ChfDZJzP.css +2 -0
  72. package/dist/assets/{input-BoBxR50a.js → input-ByHt6aoV.js} +1 -1
  73. package/dist/assets/{kiosk-mode-BZECfg9k.js → kiosk-mode-BGmMSc7A.js} +1 -1
  74. package/dist/assets/{layout-B_GMOySr.js → layout-ByZveRPN.js} +5 -5
  75. package/dist/assets/linear-Bma8J3iM.js +1 -0
  76. package/dist/assets/links-CXcpNIM0.js +1 -0
  77. package/dist/assets/{logs-panel-ABM16htT.js → logs-panel-DVSgfMgv.js} +1 -1
  78. package/dist/assets/{markdown-renderer-BG3sp2rs.js → markdown-renderer-DX2O5YiE.js} +1 -1
  79. package/dist/assets/{menu-items-BrwnTgyK.js → menu-items-BgUyNUpu.js} +1 -1
  80. package/dist/assets/{mermaid-a3xYymTy.js → mermaid-B6mqMm5_.js} +2 -2
  81. package/dist/assets/{name-cell-input-BgEBmqyL.js → name-cell-input-DL1i8Mj0.js} +1 -1
  82. package/dist/assets/numbers-CsZVw3Dc.js +1 -0
  83. package/dist/assets/{objects-snlMn4yk.js → objects-rLPlQ3X_.js} +1 -1
  84. package/dist/assets/{once-Haz_uxEL.js → once-6KNTRW0u.js} +1 -1
  85. package/dist/assets/{outline-panel-DCXQ3_F6.js → outline-panel-DPKPT2eg.js} +1 -1
  86. package/dist/assets/{packages-panel-BMGOmZkR.js → packages-panel-CZZwD9YV.js} +1 -1
  87. package/dist/assets/{panel-accordion-state-C1sdPp0m.js → panel-accordion-state-C7vvsYz1.js} +1 -1
  88. package/dist/assets/panels-BT2oosq8.js +4 -0
  89. package/dist/assets/precisionRound-BxlUs8G4.js +1 -0
  90. package/dist/assets/process-output-BKhybJBb.js +1 -0
  91. package/dist/assets/{quadrantDiagram-AXDQQJYC-yBhCDI7E.js → quadrantDiagram-AXDQQJYC-D9KH5Zge.js} +1 -1
  92. package/dist/assets/react-vega-BTNeboR2.js +189 -0
  93. package/dist/assets/{readonly-code-display-Do50PHI8.js → readonly-code-display-9QrSm_7D.js} +1 -1
  94. package/dist/assets/{readonly-python-code-DCSMIsTX.js → readonly-python-code-BLotL3XP.js} +1 -1
  95. package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-H60uiHBp.js} +1 -1
  96. package/dist/assets/{reveal-component-CLE6vB1Z.js → reveal-component-2QaMbLNx.js} +3 -3
  97. package/dist/assets/{run-page-Depd6lEE.js → run-page-DyZ8JFKU.js} +1 -1
  98. package/dist/assets/{runs-BLp5jezv.js → runs-6lJxKV1w.js} +1 -1
  99. package/dist/assets/{runtime-details-state-CSPobEdR.js → runtime-details-state-B25uM-Ps.js} +1 -1
  100. package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
  101. package/dist/assets/{scratchpad-panel-lc6jmpjv.js → scratchpad-panel-Dt83MaaA.js} +1 -1
  102. package/dist/assets/{scroll-to-bottom-button-1FyowpWV.js → scroll-to-bottom-button-CIkON2WJ.js} +1 -1
  103. package/dist/assets/{secrets-panel-Bctbtkrp.js → secrets-panel-CmLZsNfp.js} +1 -1
  104. package/dist/assets/{select-DTBn5eTb.js → select-3CVGlYrC.js} +1 -1
  105. package/dist/assets/{session-DqoWmb7X.js → session-B8Y-GJnS.js} +1 -1
  106. package/dist/assets/session-panel-xVWVwa1U.js +1 -0
  107. package/dist/assets/{snippets-panel-Bh1oTw4m.js → snippets-panel-IxIxfxNc.js} +1 -1
  108. package/dist/assets/{spec-BfSBcO5B.js → spec-uCpVvaAR.js} +1 -1
  109. package/dist/assets/state-BHuYDaxs.js +3 -0
  110. package/dist/assets/{state-uwRvJLb8.js → state-BY4_P6ko.js} +1 -1
  111. package/dist/assets/{state-BAJYRhmO.js → state-C0irhlOZ.js} +1 -1
  112. package/dist/assets/{state-XsPO8hNi.js → state-CVgqwwCU.js} +1 -1
  113. package/dist/assets/{state-DxQnC_1I.js → state-Ck7FMD5A.js} +1 -1
  114. package/dist/assets/{state-C3oDoyFe.js → state-DaWgt-rn.js} +1 -1
  115. package/dist/assets/{strings-B2vv-ENv.js → strings-C25fhMR_.js} +1 -1
  116. package/dist/assets/{swiper-component-a07domrZ.js → swiper-component-CRGRy5_O.js} +1 -1
  117. package/dist/assets/{switch-CTaiAjrz.js → switch-1h011wlH.js} +1 -1
  118. package/dist/assets/{tabs-BuQlZFbt.js → tabs-CY8E4r9U.js} +1 -1
  119. package/dist/assets/{terminal-Ck_vK-34.js → terminal-CjvCrRrA.js} +3 -3
  120. package/dist/assets/{time-MgToa8VM.js → time-DswSXBDk.js} +1 -1
  121. package/dist/assets/{toast-BGzLGnvw.js → toast-Gzxi-le_.js} +1 -1
  122. package/dist/assets/tooltip-Brya4Ghn.js +61 -0
  123. package/dist/assets/{tooltip-C9TuY18o.js → tooltip-CkGm8X7m.js} +1 -1
  124. package/dist/assets/tracing-V5zejPsw.js +1 -0
  125. package/dist/assets/{tracing-panel-PeRXHD8_.js → tracing-panel-DLS3iwK6.js} +2 -2
  126. package/dist/assets/{tree-actions-GM5E_bUs.js → tree-actions-Be_mzC1C.js} +1 -1
  127. package/dist/assets/{use-restore-focus-BxEGwIbk.js → use-restore-focus-DdS7Tl0F.js} +1 -1
  128. package/dist/assets/{useAddCell-GR7eEQrT.js → useAddCell-DK79W1vA.js} +1 -1
  129. package/dist/assets/useAsyncData-DhhdOGGd.js +1 -0
  130. package/dist/assets/{useBoolean-BDD6_xQQ.js → useBoolean-j0qHaJxu.js} +1 -1
  131. package/dist/assets/{useCellActionButton-B-9eG5z_.js → useCellActionButton-Cw9Ob12j.js} +1 -1
  132. package/dist/assets/{useDeleteCell-2_bMClHd.js → useDeleteCell-BNqX6PPu.js} +1 -1
  133. package/dist/assets/{useDependencyPanelTab-k3C3M18S.js → useDependencyPanelTab-GnTnqKJo.js} +1 -1
  134. package/dist/assets/{useEventListener-D9cftcHj.js → useEventListener-CjDeLqFC.js} +1 -1
  135. package/dist/assets/{useIframeCapabilities-CwTkczVN.js → useIframeCapabilities-Czr9e4uG.js} +1 -1
  136. package/dist/assets/{useInstallPackage-iX1eufdl.js → useInstallPackage-CVItDn1e.js} +1 -1
  137. package/dist/assets/{useInterval--x3s1VpI.js → useInterval-DJdQmXnq.js} +1 -1
  138. package/dist/assets/useNotebookActions-_qJ1Sjtj.js +1 -0
  139. package/dist/assets/{useRunCells-B2vUhREg.js → useRunCells-DP09DHT6.js} +1 -1
  140. package/dist/assets/{useSplitCell-DEb03htp.js → useSplitCell-IfumZ4sm.js} +1 -1
  141. package/dist/assets/{useTheme-COhSHkCj.js → useTheme-DLtPc-39.js} +1 -1
  142. package/dist/assets/{utils-CaQYvazl.js → utils-DfElfBpz.js} +1 -1
  143. package/dist/assets/vega-component-CMn031HF.js +1 -0
  144. package/dist/assets/{vega-embed-container-DJPNkHPb.js → vega-embed-container-DtArSJz1.js} +1 -1
  145. package/dist/assets/vega-loader.browser-Bu25TKax.js +6 -0
  146. package/dist/assets/{write-secret-modal-DzKAInL2.js → write-secret-modal-usRSi1bb.js} +1 -1
  147. package/dist/assets/{xychartDiagram-S5SC5T6Z-KbGzG6-u.js → xychartDiagram-S5SC5T6Z-CeRnBS5e.js} +1 -1
  148. package/dist/index.html +76 -77
  149. package/package.json +4 -4
  150. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  151. package/src/__tests__/host-imports.test.ts +1 -0
  152. package/src/__tests__/mount.test.ts +28 -8
  153. package/src/components/app-config/ai-config.tsx +5 -1
  154. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  155. package/src/components/data-table/__tests__/export-actions.test.tsx +329 -6
  156. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  157. package/src/components/data-table/columns.tsx +2 -1
  158. package/src/components/data-table/date-popover.tsx +6 -4
  159. package/src/components/data-table/export-actions.tsx +192 -116
  160. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  161. package/src/components/data-table/schemas.ts +14 -36
  162. package/src/components/editor/Output.tsx +9 -2
  163. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  164. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  165. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  166. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  167. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  168. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  169. package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
  170. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +366 -0
  171. package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
  172. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
  173. package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
  174. package/src/components/editor/connections/components.tsx +1 -1
  175. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  176. package/src/components/editor/controls/command-palette.tsx +22 -10
  177. package/src/components/editor/dynamic-favicon.tsx +21 -40
  178. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +537 -0
  179. package/src/components/editor/errors/send-error-report-button.tsx +69 -0
  180. package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
  181. package/src/components/editor/output/MarimoTracebackOutput.tsx +36 -21
  182. package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
  183. package/src/components/editor/output/ansi-reduce.ts +6 -0
  184. package/src/components/pages/home-page.tsx +4 -4
  185. package/src/components/tracing/tracing.tsx +2 -2
  186. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  187. package/src/core/attachments/state.ts +18 -0
  188. package/src/core/attachments/toast.ts +15 -0
  189. package/src/core/cells/__tests__/cells.test.ts +52 -0
  190. package/src/core/cells/__tests__/error-report.test.ts +139 -0
  191. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  192. package/src/core/cells/__tests__/session.test.ts +50 -1
  193. package/src/core/cells/cells.ts +2 -1
  194. package/src/core/cells/error-report.ts +63 -0
  195. package/src/core/cells/pending-delete-service.ts +2 -6
  196. package/src/core/cells/session.ts +28 -1
  197. package/src/core/config/pair.ts +5 -3
  198. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  199. package/src/core/datasets/data-source-discovery.ts +4 -6
  200. package/src/core/edit-app.tsx +2 -2
  201. package/src/core/host/AGENTS.md +11 -2
  202. package/src/core/host/__tests__/attachments.test.ts +56 -0
  203. package/src/core/host/__tests__/document.test.ts +126 -0
  204. package/src/core/host/__tests__/events.test.ts +49 -0
  205. package/src/core/host/__tests__/fixtures.ts +41 -0
  206. package/src/core/host/__tests__/notebook-client.test.ts +433 -0
  207. package/src/core/host/connection.ts +81 -0
  208. package/src/core/host/events.ts +77 -0
  209. package/src/core/host/notebook-client.ts +323 -0
  210. package/src/core/host/resources/attachments.ts +19 -0
  211. package/src/core/host/resources/document.ts +101 -0
  212. package/src/core/hotkeys/actions.ts +2 -4
  213. package/src/core/network/connection.ts +4 -2
  214. package/src/core/run-app.tsx +2 -2
  215. package/src/core/runtime/__tests__/runtime.test.ts +55 -11
  216. package/src/core/runtime/runtime.ts +18 -5
  217. package/src/core/runtime/types.ts +2 -0
  218. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  219. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  220. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +170 -0
  221. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +181 -65
  222. package/src/core/websocket/connection-state.ts +70 -0
  223. package/src/core/websocket/state.ts +7 -0
  224. package/src/core/websocket/transports/__tests__/sse.test.ts +1 -4
  225. package/src/core/websocket/transports/create.ts +31 -0
  226. package/src/core/websocket/transports/transport.ts +12 -23
  227. package/src/core/websocket/types.ts +9 -39
  228. package/src/core/websocket/useNotebookClient.ts +88 -0
  229. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +59 -225
  230. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  231. package/src/hooks/useAsyncData.ts +3 -3
  232. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  233. package/src/hooks/useElapsedTime.ts +3 -3
  234. package/src/mount.tsx +7 -3
  235. package/src/plugins/core/registerReactComponent.tsx +1 -1
  236. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  237. package/src/plugins/impl/FormPlugin.tsx +1 -1
  238. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
  239. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  240. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  241. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  242. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  243. package/src/utils/__tests__/arrays.test.ts +35 -0
  244. package/src/utils/__tests__/numbers.test.ts +48 -1
  245. package/src/utils/__tests__/objects.test.ts +21 -0
  246. package/src/utils/arrays.ts +10 -0
  247. package/src/utils/numbers.ts +28 -17
  248. package/src/utils/objects.ts +3 -7
  249. package/src/utils/once.ts +19 -19
  250. package/dist/assets/CellStatus-BP2mavAq.js +0 -1
  251. package/dist/assets/JsonOutput-DxHKOpPG.js +0 -11
  252. package/dist/assets/MarimoErrorOutput-D60XkCny.js +0 -6
  253. package/dist/assets/ai-model-dropdown-B0z6-zhp.js +0 -5
  254. package/dist/assets/any-language-editor-BLCAD3yx.js +0 -1
  255. package/dist/assets/api-BeC_VSGv.js +0 -1
  256. package/dist/assets/arrays-BqUx5USx.js +0 -1
  257. package/dist/assets/code-visibility-B-_GvwR5.js +0 -1
  258. package/dist/assets/coerce-CpE6kUdX.js +0 -1
  259. package/dist/assets/command-palette-Cf7WX-nr.js +0 -1
  260. package/dist/assets/config-BOKzA3tF.js +0 -1
  261. package/dist/assets/config-DX8Y2WbJ.js +0 -1
  262. package/dist/assets/config-VPSq73n8.js +0 -1
  263. package/dist/assets/edit-page-BT74CF4G.js +0 -9
  264. package/dist/assets/feedback-button-B2gKC5cn.js +0 -1
  265. package/dist/assets/hooks-RnjuFmEx.js +0 -1
  266. package/dist/assets/index-Bcdzex-3.css +0 -2
  267. package/dist/assets/index-D-kY3FU1.js +0 -29
  268. package/dist/assets/linear-0eij_aAG.js +0 -1
  269. package/dist/assets/links-DkkL9DVa.js +0 -1
  270. package/dist/assets/numbers-Mk3-CcoU.js +0 -1
  271. package/dist/assets/pair-CaQyK6En.js +0 -1
  272. package/dist/assets/panels-Dn59o8ay.js +0 -4
  273. package/dist/assets/precisionRound-Bq_nmf5u.js +0 -1
  274. package/dist/assets/process-output-D0KGq04R.js +0 -1
  275. package/dist/assets/react-vega-C3rYGtP6.js +0 -189
  276. package/dist/assets/session-panel-BxS5hiG2.js +0 -1
  277. package/dist/assets/state-_Qai_QyX.js +0 -3
  278. package/dist/assets/tooltip-mT3CGest.js +0 -61
  279. package/dist/assets/tracing-Di6pUmX8.js +0 -1
  280. package/dist/assets/useAsyncData-C7aJJz0Y.js +0 -1
  281. package/dist/assets/useNotebookActions-DIc4xTzY.js +0 -1
  282. package/dist/assets/vega-component-CnadTgoE.js +0 -1
  283. package/dist/assets/vega-loader.browser-CLRkNgm7.js +0 -6
  284. package/src/core/host/__tests__/protocol.test.ts +0 -214
  285. package/src/core/host/protocol.ts +0 -1943
  286. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -22,6 +22,7 @@ import {
22
22
  import { Kbd } from "@/components/ui/kbd";
23
23
  import { Tooltip } from "@/components/ui/tooltip";
24
24
  import { getCellEditorView } from "@/core/cells/cells";
25
+ import { buildErrorReport } from "@/core/cells/error-report";
25
26
  import type { CellId } from "@/core/cells/ids";
26
27
  import { SCRATCH_CELL_ID } from "@/core/cells/ids";
27
28
  import { toggleBreakpoint } from "@/core/codemirror/cells/debugger-state";
@@ -34,6 +35,7 @@ import { isWasm } from "@/core/wasm/utils";
34
35
  import { renderHTML } from "@/plugins/core/RenderHTML";
35
36
  import { sanitizeHtml } from "@/plugins/core/sanitize-html";
36
37
  import { copyToClipboard } from "@/utils/copy";
38
+ import { parseHtmlContent } from "@/utils/dom";
37
39
  import {
38
40
  containsMangledLocal,
39
41
  splitMangledLocals,
@@ -46,33 +48,45 @@ import {
46
48
  import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant";
47
49
  import { AIFixButton, buildFixInChatPrompt } from "../errors/auto-fix";
48
50
  import { MangledSegments } from "../errors/mangled-local-chip";
51
+ import { SendErrorReportButton } from "../errors/send-error-report-button";
49
52
  import { CellLinkTraceback } from "../links/cell-link";
50
53
  import type { OnRefactorWithAI } from "../Output";
51
54
 
52
55
  interface Props {
53
56
  cellId: CellId | undefined;
54
57
  traceback: string;
58
+ tracebackFormat?: "html" | "text";
59
+ code?: string;
55
60
  onRefactorWithAI?: OnRefactorWithAI;
56
61
  }
57
62
 
58
- /**
59
- * List of errors due to violations of Marimo semantics.
60
- */
63
+ /** Render a traceback and actions for its captured execution. */
61
64
  export const MarimoTracebackOutput = ({
62
65
  onRefactorWithAI,
63
66
  traceback,
67
+ tracebackFormat = "html",
64
68
  cellId,
69
+ code,
65
70
  }: Props): JSX.Element => {
66
- const htmlTraceback = renderHTML({
67
- html: traceback,
68
- additionalReplacements: [
69
- replaceTracebackFilenames,
70
- replaceTracebackPrefix,
71
- replaceMangledLocal,
72
- ],
73
- });
71
+ const htmlTraceback =
72
+ tracebackFormat === "text" ? (
73
+ <pre>{traceback}</pre>
74
+ ) : (
75
+ renderHTML({
76
+ html: traceback,
77
+ additionalReplacements: [
78
+ replaceTracebackFilenames,
79
+ replaceTracebackPrefix,
80
+ replaceMangledLocal,
81
+ ],
82
+ })
83
+ );
74
84
 
75
- const lastTracebackLine = lastLine(traceback);
85
+ const textTraceback =
86
+ tracebackFormat === "html"
87
+ ? parseHtmlContent(sanitizeHtml(traceback))
88
+ : traceback;
89
+ const lastTracebackLine = lastLine(textTraceback);
76
90
  const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
77
91
  const openAiAssistant = useOpenAiAssistant();
78
92
 
@@ -131,6 +145,13 @@ export const MarimoTracebackOutput = ({
131
145
  </div>
132
146
  )}
133
147
  <div className="flex gap-2">
148
+ {cellId && lastTracebackLine && (
149
+ <SendErrorReportButton
150
+ getContent={() =>
151
+ buildErrorReport(cellId, { code, traceback, tracebackFormat })
152
+ }
153
+ />
154
+ )}
134
155
  {showAIFix && (
135
156
  <AIFixButton
136
157
  tooltip="Fix with AI"
@@ -186,11 +207,7 @@ export const MarimoTracebackOutput = ({
186
207
  </DropdownMenuItem>
187
208
  <DropdownMenuItem
188
209
  onClick={() => {
189
- // Strip HTML from the traceback (sanitize first to prevent XSS)
190
- const div = document.createElement("div");
191
- div.innerHTML = sanitizeHtml(traceback);
192
- const textContent = div.textContent || "";
193
- copyToClipboard(textContent);
210
+ copyToClipboard(textTraceback);
194
211
  }}
195
212
  >
196
213
  <CopyIcon className="h-4 w-4 mr-2" />
@@ -205,9 +222,7 @@ export const MarimoTracebackOutput = ({
205
222
  };
206
223
 
207
224
  function lastLine(text: string): string {
208
- const el = document.createElement("div");
209
- el.innerHTML = sanitizeHtml(text);
210
- const lines = el.textContent?.split("\n").filter(Boolean);
225
+ const lines = text.split("\n").filter(Boolean);
211
226
  return lines?.at(-1) || "";
212
227
  }
213
228
 
@@ -233,7 +248,7 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
233
248
  <Tooltip content={tooltipContent}>
234
249
  <button
235
250
  type="button"
236
- className="ml-1 p-1 rounded-sm hover:bg-muted transition-all inline"
251
+ className="ml-1 p-1 rounded-sm hover:bg-muted transition-colors inline"
237
252
  >
238
253
  <BugPlayIcon
239
254
  onClick={() => {
@@ -7,6 +7,24 @@ import {
7
7
  TerminalBuffer,
8
8
  } from "../ansi-reduce";
9
9
 
10
+ test("preserves source evidence through stateful output and serialization", () => {
11
+ const output = StatefulOutputMessage.create({
12
+ channel: "stderr",
13
+ mimetype: "application/vnd.marimo+traceback",
14
+ timestamp: 1,
15
+ data: "<pre>ValueError:",
16
+ code: "raise ValueError('failed code')",
17
+ }).appendData(" failed code</pre>");
18
+ expect(output.code).toBe("raise ValueError('failed code')");
19
+ expect(output.toJSON()).toEqual({
20
+ channel: "stderr",
21
+ mimetype: "application/vnd.marimo+traceback",
22
+ timestamp: 1,
23
+ data: "<pre>ValueError: failed code</pre>",
24
+ code: "raise ValueError('failed code')",
25
+ });
26
+ });
27
+
10
28
  describe("TerminalBuffer", () => {
11
29
  test("writeChar writes single character", () => {
12
30
  const buffer = new TerminalBuffer();
@@ -368,6 +368,7 @@ export class StatefulOutputMessage implements OutputMessage {
368
368
  public readonly mimetype: OutputMessage["mimetype"];
369
369
  public readonly channel: OutputMessage["channel"];
370
370
  public readonly timestamp: OutputMessage["timestamp"];
371
+ public readonly code: OutputMessage["code"];
371
372
  private ansiReducer = new AnsiReducer();
372
373
  private _data: string;
373
374
 
@@ -383,6 +384,7 @@ export class StatefulOutputMessage implements OutputMessage {
383
384
  message.channel,
384
385
  message.timestamp,
385
386
  ansiReducer,
387
+ message.code,
386
388
  );
387
389
  }
388
390
 
@@ -391,10 +393,12 @@ export class StatefulOutputMessage implements OutputMessage {
391
393
  channel: OutputMessage["channel"],
392
394
  timestamp: OutputMessage["timestamp"],
393
395
  ansiReducer: AnsiReducer,
396
+ code: OutputMessage["code"],
394
397
  ) {
395
398
  this.mimetype = mimetype;
396
399
  this.channel = channel;
397
400
  this.timestamp = timestamp;
401
+ this.code = code;
398
402
  this.ansiReducer = ansiReducer;
399
403
  this._data = this.ansiReducer.render();
400
404
  }
@@ -406,6 +410,7 @@ export class StatefulOutputMessage implements OutputMessage {
406
410
  this.channel,
407
411
  this.timestamp,
408
412
  this.ansiReducer,
413
+ this.code,
409
414
  );
410
415
  }
411
416
 
@@ -415,6 +420,7 @@ export class StatefulOutputMessage implements OutputMessage {
415
420
  channel: this.channel,
416
421
  timestamp: this.timestamp,
417
422
  data: this.data,
423
+ ...(this.code === undefined ? {} : { code: this.code }),
418
424
  };
419
425
  }
420
426
  }
@@ -514,7 +514,7 @@ const MarimoFileComponent = ({ file }: { file: MarimoFile }) => {
514
514
 
515
515
  return (
516
516
  <a
517
- className="py-1.5 px-4 hover:bg-(--blue-2) hover:text-primary transition-all duration-300 cursor-pointer group relative flex gap-4 items-center"
517
+ className="py-1.5 px-4 hover:bg-(--blue-2) hover:text-primary transition-colors duration-300 cursor-pointer group relative flex gap-4 items-center"
518
518
  key={file.path}
519
519
  href={href.toString()}
520
520
  target={tabTarget(file.initializationId || file.path)}
@@ -542,7 +542,7 @@ const MarimoFileComponent = ({ file }: { file: MarimoFile }) => {
542
542
  </div>
543
543
  <ExternalLinkIcon
544
544
  size={20}
545
- className="group-hover:opacity-100 opacity-0 transition-all duration-300 text-primary"
545
+ className="group-hover:opacity-100 opacity-0 transition-opacity duration-300 text-primary"
546
546
  />
547
547
  </div>
548
548
  {!!file.lastModified && (
@@ -617,14 +617,14 @@ const CreateNewNotebook: React.FC = () => {
617
617
  <a
618
618
  className="relative rounded-lg p-6 group
619
619
  text-primary hover:bg-(--blue-2) shadow-md-solid shadow-accent border bg-(--blue-1)
620
- transition-all duration-300 cursor-pointer
620
+ transition-colors duration-300 cursor-pointer
621
621
  "
622
622
  href={url}
623
623
  target="_blank"
624
624
  rel="noreferrer"
625
625
  >
626
626
  <h2 className="text-lg font-semibold">Create a new notebook</h2>
627
- <div className="group-hover:opacity-100 opacity-0 absolute right-5 top-0 bottom-0 rounded-lg flex items-center justify-center transition-all duration-300">
627
+ <div className="group-hover:opacity-100 opacity-0 absolute right-5 top-0 bottom-0 rounded-lg flex items-center justify-center transition-opacity duration-300">
628
628
  <ExternalLinkIcon size={24} />
629
629
  </div>
630
630
  </a>
@@ -145,11 +145,11 @@ const TraceBlock: React.FC<{
145
145
  isMostRecentRun: boolean;
146
146
  chartPosition: ChartPosition;
147
147
  theme: ResolvedTheme;
148
- }> = ({ run, isMostRecentRun, chartPosition, isExpanded, theme }) => {
148
+ }> = ({ run, isMostRecentRun, chartPosition, isExpanded: expanded, theme }) => {
149
149
  const setExpandedRuns = useSetAtom(expandedRunsAtom);
150
150
  // We prefer the user's last click, but if they haven't clicked on this run,
151
151
  // we expand the most recent run by default, otherwise we collapse it.
152
- isExpanded = isExpanded ?? isMostRecentRun;
152
+ const isExpanded = expanded ?? isMostRecentRun;
153
153
 
154
154
  const onToggleExpanded = () => {
155
155
  setExpandedRuns((prev) => {
@@ -0,0 +1,58 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { act, render, renderHook } from "@testing-library/react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { Toaster } from "@/components/ui/toaster";
7
+ import { clearToasts, toast, useToast } from "@/components/ui/use-toast";
8
+ import { agentToast } from "../toast";
9
+
10
+ describe("agent notifications", () => {
11
+ beforeEach(() => {
12
+ vi.useFakeTimers();
13
+ clearToasts();
14
+ });
15
+
16
+ afterEach(() => {
17
+ act(() => {
18
+ clearToasts();
19
+ vi.runOnlyPendingTimers();
20
+ });
21
+ vi.useRealTimers();
22
+ });
23
+
24
+ it.each([
25
+ { variant: undefined, duration: 3000 },
26
+ { variant: "danger" as const, duration: 5000 },
27
+ ])("expires while the window is blurred: %j", ({ variant, duration }) => {
28
+ const { result } = renderHook(useToast);
29
+ render(<Toaster />);
30
+ act(() => agentToast({ title: "Agent notice", variant }));
31
+ act(() => window.dispatchEvent(new Event("blur")));
32
+ act(() => vi.advanceTimersByTime(duration - 1));
33
+ expect(result.current.toasts[0].open).toBe(true);
34
+ act(() => vi.advanceTimersByTime(1));
35
+ expect(result.current.toasts[0].open).toBe(false);
36
+ });
37
+
38
+ it("preserves pause behavior for ordinary notifications", () => {
39
+ const { result } = renderHook(useToast);
40
+ render(<Toaster />);
41
+ act(() => toast({ title: "Ordinary notice", duration: 3000 }));
42
+ act(() => window.dispatchEvent(new Event("blur")));
43
+ act(() => vi.advanceTimersByTime(5000));
44
+ expect(result.current.toasts[0].open).toBe(true);
45
+ });
46
+
47
+ it("does not let an older timer dismiss a newer notification", () => {
48
+ const { result } = renderHook(useToast);
49
+ act(() => agentToast({ title: "First" }));
50
+ act(() => vi.advanceTimersByTime(1000));
51
+ act(() => agentToast({ title: "Second" }));
52
+ act(() => vi.advanceTimersByTime(2000));
53
+ expect(result.current.toasts[0].title).toBe("Second");
54
+ expect(result.current.toasts[0].open).toBe(true);
55
+ act(() => vi.advanceTimersByTime(1000));
56
+ expect(result.current.toasts[0].open).toBe(false);
57
+ });
58
+ });
@@ -0,0 +1,18 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom } from "jotai";
4
+ import type { Attachment } from "@marimo-team/client/protocol";
5
+ import { notebookClientAtom } from "@/core/websocket/state";
6
+
7
+ export type { Attachment } from "@marimo-team/client/protocol";
8
+
9
+ export const agentAttachmentsAtom = atom((get) => {
10
+ const notebook = get(notebookClientAtom);
11
+ return (notebook ? get(notebook.attachmentsAtom) : []).filter(
12
+ (attachment) => attachment.kind === "agent",
13
+ );
14
+ });
15
+
16
+ export function attachmentLabel(attachment: Attachment): string {
17
+ return attachment.name || "Agent";
18
+ }
@@ -0,0 +1,15 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { toast } from "@/components/ui/use-toast";
4
+
5
+ export function agentToast(
6
+ options: Pick<
7
+ Parameters<typeof toast>[0],
8
+ "title" | "variant" | "description"
9
+ >,
10
+ ) {
11
+ const duration = options.variant === "danger" ? 5000 : 3000;
12
+ const { dismiss } = toast({ ...options, duration });
13
+ // Agent notices must also expire while the user focuses the agent terminal.
14
+ window.setTimeout(dismiss, duration);
15
+ }
@@ -789,6 +789,58 @@ describe("cell reducer", () => {
789
789
  `);
790
790
  });
791
791
 
792
+ it("keeps error evidence after an edit and a new queued run", () => {
793
+ const code = "raise ValueError('old failure')";
794
+ const output: OutputMessage = {
795
+ channel: "marimo-error",
796
+ mimetype: "application/vnd.marimo+error",
797
+ data: [
798
+ {
799
+ type: "exception",
800
+ msg: "old failure",
801
+ exception_type: "ValueError",
802
+ raising_cell: null,
803
+ traceback: "<pre>ValueError: old failure</pre>",
804
+ },
805
+ ],
806
+ code,
807
+ timestamp: 1,
808
+ };
809
+ actions.handleCellMessage({
810
+ cell_id: firstCellId,
811
+ output,
812
+ console: {
813
+ channel: "stderr",
814
+ mimetype: "application/vnd.marimo+traceback",
815
+ data: "<pre>ValueError: old failure</pre>",
816
+ code,
817
+ timestamp: 1,
818
+ },
819
+ status: "idle",
820
+ stale_inputs: null,
821
+ timestamp: 1,
822
+ });
823
+ actions.updateCellCode({
824
+ cellId: firstCellId,
825
+ code: "print('new code')",
826
+ formattingChange: false,
827
+ });
828
+ actions.prepareForRun({ cellId: firstCellId });
829
+ actions.handleCellMessage({
830
+ cell_id: firstCellId,
831
+ output: null,
832
+ console: null,
833
+ status: "queued",
834
+ stale_inputs: null,
835
+ timestamp: 2,
836
+ });
837
+ const runtime = state.cellRuntime[firstCellId];
838
+ expect(state.cellData[firstCellId].lastCodeRun).toBe("print('new code')");
839
+ expect(runtime.output).toEqual(output);
840
+ expect(runtime.consoleOutputs[0].code).toBe(code);
841
+ expect(runtime.consoleOutputs[0].data).toContain("old failure");
842
+ });
843
+
792
844
  it("can run cell and receive cell messages", () => {
793
845
  // HAPPY PATH
794
846
  /////////////////
@@ -0,0 +1,139 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { describe, expect, it } from "vitest";
5
+ import { cellId } from "@/__tests__/branded";
6
+ import type { MarimoError } from "@/core/kernel/messages";
7
+ import { buildErrorReport, ERROR_MESSAGE_INSTRUCTIONS } from "../error-report";
8
+
9
+ const CELL_ID = cellId("failed-cell");
10
+
11
+ describe("buildErrorReport", () => {
12
+ it("preserves plain traceback text without treating angle brackets as HTML", () => {
13
+ const traceback =
14
+ ' File "<cell>", line 1\nValueError: expected <value> & got <other>';
15
+ expect(
16
+ buildErrorReport(CELL_ID, { traceback, tracebackFormat: "text" }).output,
17
+ ).toEqual({
18
+ type: "error",
19
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
20
+ message: "ValueError: expected <value> & got <other>",
21
+ traceback,
22
+ });
23
+ });
24
+ it("owns the instruction to repair the live notebook", () => {
25
+ const report = buildErrorReport(CELL_ID, {
26
+ errors: [{ type: "syntax", msg: "invalid code" }],
27
+ });
28
+ expect(report.output.instructions).toEqual("Fix this notebook error.");
29
+ });
30
+
31
+ it("keeps repair intent visible before oversized evidence", () => {
32
+ const report = buildErrorReport(CELL_ID, {
33
+ code: "# long code\n".repeat(10_000),
34
+ errors: [
35
+ {
36
+ type: "exception",
37
+ exception_type: "ValueError",
38
+ msg: "long error ".repeat(10_000),
39
+ raising_cell: null,
40
+ traceback: `<pre>Traceback\n${"long frame\n".repeat(10_000)}</pre>`,
41
+ },
42
+ ],
43
+ });
44
+ const serialized = JSON.stringify(report);
45
+
46
+ expect(serialized.length).toBeGreaterThan(8192);
47
+ expect(serialized.slice(0, 256)).toContain(
48
+ '"instructions":"Fix this notebook error."',
49
+ );
50
+ });
51
+
52
+ it("retains every message and complete traceback without display limits", () => {
53
+ const traceback = `Traceback\n${"long frame\n".repeat(10_000)}ValueError: bad <value>`;
54
+ const errors: MarimoError[] = [
55
+ {
56
+ type: "exception",
57
+ msg: "bad <value>",
58
+ exception_type: "ValueError",
59
+ raising_cell: null,
60
+ traceback: `<pre>${traceback.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</pre>`,
61
+ },
62
+ { type: "syntax", msg: "another error" },
63
+ ];
64
+ expect(buildErrorReport(CELL_ID, { code: "failed()", errors })).toEqual({
65
+ type: "cell-output",
66
+ cell_id: CELL_ID,
67
+ code: "failed()",
68
+ output: {
69
+ type: "error",
70
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
71
+ message: "bad <value>\nanother error",
72
+ traceback,
73
+ },
74
+ });
75
+ });
76
+
77
+ it("leaves source unknown for a restored legacy exception", () => {
78
+ expect(
79
+ buildErrorReport(CELL_ID, {
80
+ errors: [
81
+ {
82
+ type: "exception",
83
+ exception_type: "exception",
84
+ msg: "legacy error",
85
+ raising_cell: null,
86
+ traceback: null,
87
+ },
88
+ ],
89
+ }),
90
+ ).toEqual({
91
+ type: "cell-output",
92
+ cell_id: CELL_ID,
93
+ output: {
94
+ type: "error",
95
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
96
+ message: "legacy error",
97
+ },
98
+ });
99
+ });
100
+
101
+ it("preserves empty captured code", () => {
102
+ expect(
103
+ buildErrorReport(CELL_ID, {
104
+ code: "",
105
+ errors: [{ type: "syntax", msg: "invalid code" }],
106
+ }),
107
+ ).toEqual({
108
+ type: "cell-output",
109
+ cell_id: CELL_ID,
110
+ code: "",
111
+ output: {
112
+ type: "error",
113
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
114
+ message: "invalid code",
115
+ },
116
+ });
117
+ });
118
+
119
+ it("keeps separate structured tracebacks in order", () => {
120
+ const errors: MarimoError[] = ["first", "second"].map((message) => ({
121
+ type: "exception",
122
+ exception_type: "ValueError",
123
+ raising_cell: null,
124
+ msg: message,
125
+ traceback: `<pre>Traceback\nValueError: ${message}</pre>`,
126
+ }));
127
+ expect(buildErrorReport(CELL_ID, { errors })).toEqual({
128
+ type: "cell-output",
129
+ cell_id: CELL_ID,
130
+ output: {
131
+ type: "error",
132
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
133
+ message: "first\nsecond",
134
+ traceback:
135
+ "Traceback\nValueError: first\n\nTraceback\nValueError: second",
136
+ },
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,113 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { createStore } from "jotai";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { MockNotebook } from "@/__mocks__/notebook";
6
+ import { cellId } from "@/__tests__/branded";
7
+ import {
8
+ createNotebookActions,
9
+ notebookAtom,
10
+ notebookOutline,
11
+ notebookReducer,
12
+ } from "../cells";
13
+ import { exportedForTesting } from "../document-changes";
14
+ import type { Outline } from "../outline";
15
+
16
+ const first = cellId("first");
17
+ const second = cellId("second");
18
+ const firstOutline: Outline = {
19
+ items: [{ name: "First", level: 1, by: { id: "first" } }],
20
+ };
21
+ const secondOutline: Outline = {
22
+ items: [{ name: "Second", level: 2, by: { id: "second" } }],
23
+ };
24
+
25
+ function setup() {
26
+ const store = createStore();
27
+ store.set(
28
+ notebookAtom,
29
+ MockNotebook.notebookState({
30
+ cellData: { [first]: { code: "x = 1" }, [second]: {} },
31
+ cellRuntime: {
32
+ [first]: { outline: firstOutline },
33
+ [second]: { outline: secondOutline },
34
+ },
35
+ }),
36
+ );
37
+ const actions = createNotebookActions((action) =>
38
+ store.set(notebookAtom, (state) => notebookReducer(state, action)),
39
+ );
40
+ return { store, actions };
41
+ }
42
+
43
+ afterEach(() => {
44
+ exportedForTesting.cancelPendingChanges();
45
+ });
46
+
47
+ describe("notebookOutline", () => {
48
+ it("does not notify outline subscribers when cell code changes", () => {
49
+ const { store, actions } = setup();
50
+ const initial = store.get(notebookOutline);
51
+ const listener = vi.fn();
52
+ const unsubscribe = store.sub(notebookOutline, listener);
53
+
54
+ actions.updateCellCode({
55
+ cellId: first,
56
+ code: "x = 2",
57
+ formattingChange: false,
58
+ });
59
+
60
+ expect(store.get(notebookOutline)).toBe(initial);
61
+ expect(listener).not.toHaveBeenCalled();
62
+ unsubscribe();
63
+ });
64
+
65
+ it("updates headings when runtime outlines change or are removed", () => {
66
+ const { store } = setup();
67
+ const updated: Outline = {
68
+ items: [{ name: "Updated", level: 1, by: { id: "updated" } }],
69
+ };
70
+
71
+ for (const outline of [updated, null]) {
72
+ store.set(notebookAtom, (state) => ({
73
+ ...state,
74
+ cellRuntime: {
75
+ ...state.cellRuntime,
76
+ [first]: { ...state.cellRuntime[first], outline },
77
+ },
78
+ }));
79
+ expect(store.get(notebookOutline)).toEqual({
80
+ items: [...(outline?.items ?? []), ...secondOutline.items],
81
+ });
82
+ }
83
+ });
84
+
85
+ it("keeps headings in cell order after moving and deleting cells", () => {
86
+ const { store, actions } = setup();
87
+ actions.moveCell({ cellId: second, before: true });
88
+ expect(store.get(notebookOutline)).toEqual({
89
+ items: [...secondOutline.items, ...firstOutline.items],
90
+ });
91
+
92
+ actions.deleteCell({ cellId: second });
93
+ expect(store.get(notebookOutline)).toEqual(firstOutline);
94
+ });
95
+
96
+ it("notifies subscribers when output is replaced with the same headings", () => {
97
+ const { store } = setup();
98
+ const listener = vi.fn();
99
+ const unsubscribe = store.sub(notebookOutline, listener);
100
+
101
+ store.set(notebookAtom, (state) => ({
102
+ ...state,
103
+ cellRuntime: {
104
+ ...state.cellRuntime,
105
+ [first]: { ...state.cellRuntime[first], outline: { ...firstOutline } },
106
+ },
107
+ }));
108
+
109
+ // Consumers must reconnect to replacement heading elements in the DOM.
110
+ expect(listener).toHaveBeenCalledOnce();
111
+ unsubscribe();
112
+ });
113
+ });