@marimo-team/frontend 0.25.3-dev5 → 0.25.3-dev55

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 (343) hide show
  1. package/dist/assets/CellStatus-cD3S0wEA.js +1 -0
  2. package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-D9UK8o9A.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-B4n9EnHB.js → ErrorBoundary-DJhsFCsX.js} +2 -2
  4. package/dist/assets/JsonOutput-Pm6OCP3J.js +11 -0
  5. package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-BPYE76jR.js} +2 -2
  6. package/dist/assets/MarimoErrorOutput-DlEjLCZ-.js +10 -0
  7. package/dist/assets/{RenderHTML-9bMgbYmo.js → RenderHTML-CfQ6i3QV.js} +1 -1
  8. package/dist/assets/RunButton-DvuvkgrG.js +1 -0
  9. package/dist/assets/{add-cell-with-ai-C_SruRWS.js → add-cell-with-ai-BhmuohAY.js} +7 -7
  10. package/dist/assets/{add-connection-dialog-CoE_PhuD.js → add-connection-dialog-D7EYSraq.js} +2 -2
  11. package/dist/assets/{agent-panel-0b3k4QzJ.js → agent-panel-GFhlWVub.js} +3 -3
  12. package/dist/assets/ai-model-dropdown-YT1HiXiK.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-iXdCWvfC.js → app-config-button-C_whRVO8.js} +1 -1
  16. package/dist/assets/arrays-0R2reF2y.js +1 -0
  17. package/dist/assets/{blob-iOBwGaP9.js → blob-e6VlOmQP.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-CR-tZNnc.js → cell-editor-3cVIcyHK.js} +6 -6
  22. package/dist/assets/cell-link-DFqfW-Ym.js +1 -0
  23. package/dist/assets/{cells-rA_jsZCj.js → cells-T9_Y4Jlv.js} +56 -56
  24. package/dist/assets/{chat-display-luF7psRA.js → chat-display-BtaARI-N.js} +1 -1
  25. package/dist/assets/chat-panel-eT2lfQrB.js +4 -0
  26. package/dist/assets/chat-ui-nNFa2LdU.js +4 -0
  27. package/dist/assets/{circle-DXf-VWZG.js → circle-B6D_6D8w.js} +1 -1
  28. package/dist/assets/code-visibility-CV3S4xuf.js +1 -0
  29. package/dist/assets/coerce-DWjFeXOY.js +1 -0
  30. package/dist/assets/{column-preview-uosAAL4X.js → column-preview-C6uXRJry.js} +1 -1
  31. package/dist/assets/command-palette-OZPTmtR-.js +1 -0
  32. package/dist/assets/{common-lisZ0l6a.js → common-DeeLgjWQ.js} +1 -1
  33. package/dist/assets/{components-CP_UoFtT.js → components-DrU8dOzy.js} +1 -1
  34. package/dist/assets/{components-S7ZwOrvO.js → components-DyAF3_Nf.js} +1 -1
  35. package/dist/assets/{components-CXfO-F47.js → components-lDm0qi2t.js} +1 -1
  36. package/dist/assets/config-CjQ4WJnJ.js +1 -0
  37. package/dist/assets/config-DGX7JYor.js +1 -0
  38. package/dist/assets/config-DzdxZb17.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-V0-52XOh.js → datasource-gqFgG9Qd.js} +1 -1
  43. package/dist/assets/{dates-CfmdTL6B.js → dates-CQMwFko2.js} +1 -1
  44. package/dist/assets/{dependency-graph-panel-B0uP-_id.js → dependency-graph-panel-BFFxLgR6.js} +1 -1
  45. package/dist/assets/{dist-iaThVDkA.js → dist-jlhMTKo_.js} +1 -1
  46. package/dist/assets/{documentation-panel-DVl_ebWy.js → documentation-panel-BBpGbT-f.js} +1 -1
  47. package/dist/assets/{download-Bx55LMki.js → download-C8Gw-D0Y.js} +1 -1
  48. package/dist/assets/{dropdown-menu-C1rnl-0o.js → dropdown-menu-DS6umAnH.js} +1 -1
  49. package/dist/assets/{edit-page-CGyjT-CQ.js → edit-page-B0lN_Rib.js} +6 -6
  50. package/dist/assets/{error-banner-Cd69vvyX.js → error-banner-B5kzVf_0.js} +1 -1
  51. package/dist/assets/{error-entries-DOWpdGvI.js → error-entries-uyqOG5tg.js} +1 -1
  52. package/dist/assets/{error-panel-C-uxLOHF.js → error-panel-Cli5zPWi.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-B99ThNJW.js → feedback-button-CvrwBGr7.js} +2 -2
  56. package/dist/assets/feedback-button-DknbRCN_.js +1 -0
  57. package/dist/assets/file-explorer-panel-BSU5ultY.js +1 -0
  58. package/dist/assets/{file-name-input-ChnBsDE_.js → file-name-input-CDrRlIiX.js} +1 -1
  59. package/dist/assets/{floating-outline-MYqQW4Vp.js → floating-outline-GQD9km0W.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-DSL0J4md.js → gallery-page-CxmV1JLF.js} +1 -1
  64. package/dist/assets/{ganttDiagram-EL5Y4UJY-2n7Ig5Jm.js → ganttDiagram-EL5Y4UJY-CYqkCfFG.js} +1 -1
  65. package/dist/assets/{glide-data-editor-B1MWlKeM.js → glide-data-editor-B1kpoetH.js} +2 -2
  66. package/dist/assets/{glide-utils-CIrAxdjv.js → glide-utils-HFqhvWHI.js} +2 -2
  67. package/dist/assets/{home-page-B1M3X9Yf.js → home-page-KnJyha6O.js} +3 -3
  68. package/dist/assets/{hooks-Bj0C0zrZ.js → hooks-w6zKs3CJ.js} +1 -1
  69. package/dist/assets/{html-to-image-DK9Zl1lU.js → html-to-image-D_0rd3_B.js} +1 -1
  70. package/dist/assets/index-Bu8_lPMx.js +30 -0
  71. package/dist/assets/index-evFWHKmM.css +2 -0
  72. package/dist/assets/{input-BoBxR50a.js → input-BZdRe7pb.js} +1 -1
  73. package/dist/assets/{kiosk-mode-BZECfg9k.js → kiosk-mode-BGmMSc7A.js} +1 -1
  74. package/dist/assets/layout-sYdHI-lW.js +9 -0
  75. package/dist/assets/linear-Bma8J3iM.js +1 -0
  76. package/dist/assets/links-CXcpNIM0.js +1 -0
  77. package/dist/assets/{logs-panel-y0XnfulJ.js → logs-panel-CdUc5vtA.js} +1 -1
  78. package/dist/assets/{markdown-renderer-Q5AhbRU3.js → markdown-renderer-pKc_bFac.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-BNOnF6-0.js → name-cell-input-csH3Cy54.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-Be7stWwv.js → outline-panel-DhsHYTuo.js} +1 -1
  86. package/dist/assets/packages-panel-lkNX6Oov.js +1 -0
  87. package/dist/assets/{panel-accordion-state-GeMEdCPw.js → panel-accordion-state-Bco6nsUI.js} +1 -1
  88. package/dist/assets/panels-B2UqDbHy.js +107 -0
  89. package/dist/assets/precisionRound-BxlUs8G4.js +1 -0
  90. package/dist/assets/process-output-B2-bT4cq.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-YULOXf4h.js → readonly-code-display-Dsp6vWPh.js} +1 -1
  94. package/dist/assets/{readonly-python-code-CadgXgRu.js → readonly-python-code-B81AjzXM.js} +1 -1
  95. package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-H60uiHBp.js} +1 -1
  96. package/dist/assets/{reveal-component-B9FLjTg_.js → reveal-component-CrQRQhW4.js} +37 -37
  97. package/dist/assets/run-page-BGzpa3D9.js +1 -0
  98. package/dist/assets/{runs-BLp5jezv.js → runs-6lJxKV1w.js} +1 -1
  99. package/dist/assets/{runtime-details-state-DuChzsA6.js → runtime-details-state-CznIe40L.js} +1 -1
  100. package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
  101. package/dist/assets/{scratchpad-panel-DtB0A8xj.js → scratchpad-panel-ClLxdqdK.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-Dh3v7XPE.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-B6tA28BD.js → session-panel-C7-wmZhc.js} +1 -1
  107. package/dist/assets/{snippets-panel-CQ1dn3lA.js → snippets-panel-BARpOPyS.js} +1 -1
  108. package/dist/assets/{spec-BfSBcO5B.js → spec-uCpVvaAR.js} +1 -1
  109. package/dist/assets/{state-BUsmcyaT.js → state-BRYi026f.js} +3 -3
  110. package/dist/assets/{state-uwRvJLb8.js → state-BY4_P6ko.js} +1 -1
  111. package/dist/assets/{state-Bfh_2SLl.js → state-Bye6rCo4.js} +1 -1
  112. package/dist/assets/{state-BAJYRhmO.js → state-C0irhlOZ.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-IoWE8PbW.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-Bhcn_y1N.js} +1 -1
  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-BW2ZVnXs.js +1 -0
  125. package/dist/assets/{tracing-panel-dgnr2fIN.js → tracing-panel-UKdh_YXC.js} +2 -2
  126. package/dist/assets/{tree-actions-DAd80MbP.js → tree-actions-Cb-nUK1F.js} +1 -1
  127. package/dist/assets/{use-restore-focus-BxEGwIbk.js → use-restore-focus-DdS7Tl0F.js} +1 -1
  128. package/dist/assets/{useAddCell-DGstJK0S.js → useAddCell-Bielffe0.js} +1 -1
  129. package/dist/assets/useAsyncData-DhhdOGGd.js +1 -0
  130. package/dist/assets/useBoolean-CMgj9hnq.js +1 -0
  131. package/dist/assets/{useCellActionButton-DY9RlJTb.js → useCellActionButton-Cu34Zs6x.js} +1 -1
  132. package/dist/assets/useDebounce-k9QTpQ9-.js +1 -0
  133. package/dist/assets/useDelayElapsed-BlsDT4TV.js +1 -0
  134. package/dist/assets/{useDeleteCell-DFBGlSQk.js → useDeleteCell-BV4VemCv.js} +1 -1
  135. package/dist/assets/{useDependencyPanelTab-DydxsE5d.js → useDependencyPanelTab-BEGqRiWi.js} +1 -1
  136. package/dist/assets/{useEventListener-D9cftcHj.js → useEventListener-CjDeLqFC.js} +1 -1
  137. package/dist/assets/{useIframeCapabilities-CwTkczVN.js → useIframeCapabilities-Czr9e4uG.js} +1 -1
  138. package/dist/assets/{useInstallPackage-iX1eufdl.js → useInstallPackage-C7neMPwh.js} +1 -1
  139. package/dist/assets/{useInterval--x3s1VpI.js → useInterval-DJdQmXnq.js} +1 -1
  140. package/dist/assets/{useNotebookActions-CupIPSlg.js → useNotebookActions-OLol05t9.js} +1 -1
  141. package/dist/assets/{useRunCells-BFjFX3hz.js → useRunCells-BFeZMi7f.js} +1 -1
  142. package/dist/assets/{useSplitCell-Tqv5pPtL.js → useSplitCell-DllkNFke.js} +1 -1
  143. package/dist/assets/useStateForKey-CGMJQT8x.js +1 -0
  144. package/dist/assets/{useTheme-COhSHkCj.js → useTheme-DLtPc-39.js} +1 -1
  145. package/dist/assets/{utils-CaQYvazl.js → utils-DfElfBpz.js} +1 -1
  146. package/dist/assets/vega-component-D7JFthDD.js +1 -0
  147. package/dist/assets/{vega-embed-container-DJPNkHPb.js → vega-embed-container-DtArSJz1.js} +1 -1
  148. package/dist/assets/vega-loader.browser-Bu25TKax.js +6 -0
  149. package/dist/assets/{write-secret-modal-DzKAInL2.js → write-secret-modal-7DzehGyr.js} +1 -1
  150. package/dist/assets/{xychartDiagram-S5SC5T6Z-KbGzG6-u.js → xychartDiagram-S5SC5T6Z-CeRnBS5e.js} +1 -1
  151. package/dist/index.html +76 -74
  152. package/package.json +4 -4
  153. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  154. package/src/__tests__/host-imports.test.ts +1 -0
  155. package/src/__tests__/mount.test.ts +8 -8
  156. package/src/components/app-config/ai-config.tsx +6 -2
  157. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  158. package/src/components/chat/chat-panel.tsx +3 -7
  159. package/src/components/chat/chat-utils.ts +4 -8
  160. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  161. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  162. package/src/components/data-table/__tests__/export-actions.test.tsx +434 -6
  163. package/src/components/data-table/charts/charts.tsx +14 -19
  164. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  165. package/src/components/data-table/columns.tsx +2 -1
  166. package/src/components/data-table/context-menu.tsx +36 -20
  167. package/src/components/data-table/date-popover.tsx +6 -4
  168. package/src/components/data-table/export-actions.tsx +267 -163
  169. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  170. package/src/components/data-table/schemas.ts +14 -36
  171. package/src/components/editor/Output.tsx +5 -1
  172. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  173. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  174. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +210 -34
  175. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  176. package/src/components/editor/actions/pair-with-agent-modal.tsx +100 -37
  177. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  178. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  179. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  180. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  181. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  182. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  183. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  184. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
  185. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  186. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +277 -78
  187. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +196 -41
  188. package/src/components/editor/connections/components.tsx +1 -1
  189. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  190. package/src/components/editor/controls/command-palette.tsx +22 -10
  191. package/src/components/editor/dynamic-favicon.tsx +21 -40
  192. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +139 -36
  193. package/src/components/editor/errors/send-error-report-button.tsx +10 -7
  194. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  195. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  196. package/src/components/editor/file-tree/file-explorer.tsx +10 -5
  197. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  198. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  199. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -22
  200. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  201. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  202. package/src/components/icons/large-spinner.tsx +3 -14
  203. package/src/components/pages/home-page.tsx +4 -4
  204. package/src/components/slides/slide-cell-view.tsx +1 -1
  205. package/src/components/tracing/tracing.tsx +2 -2
  206. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  207. package/src/core/attachments/state.ts +5 -8
  208. package/src/core/attachments/toast.ts +15 -0
  209. package/src/core/cells/__tests__/error-report.test.ts +53 -3
  210. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  211. package/src/core/cells/__tests__/session.test.ts +18 -3
  212. package/src/core/cells/cells.ts +2 -1
  213. package/src/core/cells/error-report.ts +14 -6
  214. package/src/core/cells/pending-delete-service.ts +2 -6
  215. package/src/core/cells/session.ts +19 -1
  216. package/src/core/config/pair.ts +5 -3
  217. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  218. package/src/core/datasets/data-source-discovery.ts +4 -6
  219. package/src/core/edit-app.tsx +2 -2
  220. package/src/core/host/AGENTS.md +11 -2
  221. package/src/core/host/__tests__/attachments.test.ts +56 -0
  222. package/src/core/host/__tests__/document.test.ts +126 -0
  223. package/src/core/host/__tests__/events.test.ts +49 -0
  224. package/src/core/host/__tests__/fixtures.ts +41 -0
  225. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  226. package/src/core/host/connection.ts +81 -0
  227. package/src/core/host/events.ts +68 -0
  228. package/src/core/host/notebook-client.ts +316 -0
  229. package/src/core/host/resources/attachments.ts +19 -0
  230. package/src/core/host/resources/document.ts +101 -0
  231. package/src/core/hotkeys/actions.ts +2 -4
  232. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  233. package/src/core/islands/bootstrap.ts +0 -1
  234. package/src/core/network/connection.ts +4 -2
  235. package/src/core/network/useConnectionNotice.ts +8 -2
  236. package/src/core/run-app.tsx +2 -2
  237. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  238. package/src/core/runtime/runtime.ts +5 -5
  239. package/src/core/static/files.ts +2 -0
  240. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  241. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  242. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  243. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  244. package/src/core/websocket/connection-state.ts +70 -0
  245. package/src/core/websocket/state.ts +7 -0
  246. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  247. package/src/core/websocket/transports/create.ts +31 -0
  248. package/src/core/websocket/transports/sse.ts +14 -2
  249. package/src/core/websocket/transports/transport.ts +12 -23
  250. package/src/core/websocket/types.ts +9 -39
  251. package/src/core/websocket/useNotebookClient.ts +78 -0
  252. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  253. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  254. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  255. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  256. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  257. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  258. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  259. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  260. package/src/hooks/useAsyncData.ts +3 -3
  261. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  262. package/src/hooks/useDebounce.ts +5 -8
  263. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  264. package/src/hooks/useDelayElapsed.ts +14 -10
  265. package/src/hooks/useElapsedTime.ts +3 -3
  266. package/src/hooks/useInputHistory.ts +4 -7
  267. package/src/hooks/useInternalStateWithSync.ts +6 -6
  268. package/src/hooks/useResizeObserver.ts +4 -7
  269. package/src/hooks/useScript.ts +67 -70
  270. package/src/hooks/useStateForKey.ts +93 -0
  271. package/src/mount.tsx +3 -3
  272. package/src/plugins/core/registerReactComponent.tsx +1 -1
  273. package/src/plugins/impl/DataTablePlugin.tsx +14 -15
  274. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  275. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  276. package/src/plugins/impl/FormPlugin.tsx +1 -1
  277. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  278. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +201 -1
  279. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  280. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  281. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  282. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  283. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  284. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  285. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  286. package/src/plugins/impl/chat/chat-ui.tsx +10 -9
  287. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  288. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  289. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  290. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  291. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  292. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  293. package/src/plugins/impl/panel/utils.ts +17 -0
  294. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  295. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  296. package/src/stories/editor.stories.tsx +24 -17
  297. package/src/utils/__tests__/arrays.test.ts +35 -0
  298. package/src/utils/__tests__/numbers.test.ts +48 -1
  299. package/src/utils/__tests__/objects.test.ts +21 -0
  300. package/src/utils/arrays.ts +10 -0
  301. package/src/utils/numbers.ts +28 -17
  302. package/src/utils/objects.ts +3 -7
  303. package/src/utils/once.ts +19 -19
  304. package/dist/assets/CellStatus-nZpuDkm7.js +0 -1
  305. package/dist/assets/JsonOutput-d-k8v_17.js +0 -11
  306. package/dist/assets/MarimoErrorOutput-CLYUxiKW.js +0 -10
  307. package/dist/assets/RunButton-CFQUNN5c.js +0 -1
  308. package/dist/assets/ai-model-dropdown-C6GHHEdR.js +0 -5
  309. package/dist/assets/any-language-editor-BLCAD3yx.js +0 -1
  310. package/dist/assets/arrays-BqUx5USx.js +0 -1
  311. package/dist/assets/cell-link-1CynTVGQ.js +0 -1
  312. package/dist/assets/chat-panel-DDDTnLvB.js +0 -4
  313. package/dist/assets/chat-ui-nV_m4Btr.js +0 -4
  314. package/dist/assets/code-visibility-B-_GvwR5.js +0 -1
  315. package/dist/assets/coerce-CpE6kUdX.js +0 -1
  316. package/dist/assets/command-palette-BZ13RKDJ.js +0 -1
  317. package/dist/assets/config-DX8Y2WbJ.js +0 -1
  318. package/dist/assets/config-FzDtjF1-.js +0 -1
  319. package/dist/assets/config-VPSq73n8.js +0 -1
  320. package/dist/assets/feedback-button-DlqZkJwz.js +0 -1
  321. package/dist/assets/file-explorer-panel-D3CphidS.js +0 -1
  322. package/dist/assets/index-CXIE-QEG.css +0 -2
  323. package/dist/assets/index-DnIgeCyQ.js +0 -29
  324. package/dist/assets/layout-DBuMeWh0.js +0 -9
  325. package/dist/assets/linear-0eij_aAG.js +0 -1
  326. package/dist/assets/links-DkkL9DVa.js +0 -1
  327. package/dist/assets/numbers-Mk3-CcoU.js +0 -1
  328. package/dist/assets/packages-panel-9Qs1npCZ.js +0 -1
  329. package/dist/assets/panels-B9x935By.js +0 -4
  330. package/dist/assets/precisionRound-Bq_nmf5u.js +0 -1
  331. package/dist/assets/process-output-DNFKMODr.js +0 -1
  332. package/dist/assets/react-vega-C3rYGtP6.js +0 -189
  333. package/dist/assets/run-page-CjdGaic3.js +0 -1
  334. package/dist/assets/tooltip-mT3CGest.js +0 -61
  335. package/dist/assets/tracing-BARlJ_Dk.js +0 -1
  336. package/dist/assets/useAsyncData-C7aJJz0Y.js +0 -1
  337. package/dist/assets/useBoolean-BDD6_xQQ.js +0 -1
  338. package/dist/assets/useDebounce-B_ccBZTp.js +0 -1
  339. package/dist/assets/vega-component-CnadTgoE.js +0 -1
  340. package/dist/assets/vega-loader.browser-CLRkNgm7.js +0 -6
  341. package/src/core/host/__tests__/protocol.test.ts +0 -214
  342. package/src/core/host/protocol.ts +0 -1943
  343. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -35,6 +35,7 @@ import { isWasm } from "@/core/wasm/utils";
35
35
  import { renderHTML } from "@/plugins/core/RenderHTML";
36
36
  import { sanitizeHtml } from "@/plugins/core/sanitize-html";
37
37
  import { copyToClipboard } from "@/utils/copy";
38
+ import { parseHtmlContent } from "@/utils/dom";
38
39
  import {
39
40
  containsMangledLocal,
40
41
  splitMangledLocals,
@@ -54,29 +55,38 @@ import type { OnRefactorWithAI } from "../Output";
54
55
  interface Props {
55
56
  cellId: CellId | undefined;
56
57
  traceback: string;
58
+ tracebackFormat?: "html" | "text";
57
59
  code?: string;
58
60
  onRefactorWithAI?: OnRefactorWithAI;
59
61
  }
60
62
 
61
- /**
62
- * List of errors due to violations of Marimo semantics.
63
- */
63
+ /** Render a traceback and actions for its captured execution. */
64
64
  export const MarimoTracebackOutput = ({
65
65
  onRefactorWithAI,
66
66
  traceback,
67
+ tracebackFormat = "html",
67
68
  cellId,
68
69
  code,
69
70
  }: Props): JSX.Element => {
70
- const htmlTraceback = renderHTML({
71
- html: traceback,
72
- additionalReplacements: [
73
- replaceTracebackFilenames,
74
- replaceTracebackPrefix,
75
- replaceMangledLocal,
76
- ],
77
- });
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
+ );
78
84
 
79
- const lastTracebackLine = lastLine(traceback);
85
+ const textTraceback =
86
+ tracebackFormat === "html"
87
+ ? parseHtmlContent(sanitizeHtml(traceback))
88
+ : traceback;
89
+ const lastTracebackLine = lastLine(textTraceback);
80
90
  const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
81
91
  const openAiAssistant = useOpenAiAssistant();
82
92
 
@@ -137,7 +147,9 @@ export const MarimoTracebackOutput = ({
137
147
  <div className="flex gap-2">
138
148
  {cellId && lastTracebackLine && (
139
149
  <SendErrorReportButton
140
- getContent={() => buildErrorReport(cellId, { code, traceback })}
150
+ getContent={() =>
151
+ buildErrorReport(cellId, { code, traceback, tracebackFormat })
152
+ }
141
153
  />
142
154
  )}
143
155
  {showAIFix && (
@@ -195,11 +207,7 @@ export const MarimoTracebackOutput = ({
195
207
  </DropdownMenuItem>
196
208
  <DropdownMenuItem
197
209
  onClick={() => {
198
- // Strip HTML from the traceback (sanitize first to prevent XSS)
199
- const div = document.createElement("div");
200
- div.innerHTML = sanitizeHtml(traceback);
201
- const textContent = div.textContent || "";
202
- copyToClipboard(textContent);
210
+ copyToClipboard(textTraceback);
203
211
  }}
204
212
  >
205
213
  <CopyIcon className="h-4 w-4 mr-2" />
@@ -214,9 +222,7 @@ export const MarimoTracebackOutput = ({
214
222
  };
215
223
 
216
224
  function lastLine(text: string): string {
217
- const el = document.createElement("div");
218
- el.innerHTML = sanitizeHtml(text);
219
- const lines = el.textContent?.split("\n").filter(Boolean);
225
+ const lines = text.split("\n").filter(Boolean);
220
226
  return lines?.at(-1) || "";
221
227
  }
222
228
 
@@ -242,7 +248,7 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
242
248
  <Tooltip content={tooltipContent}>
243
249
  <button
244
250
  type="button"
245
- 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"
246
252
  >
247
253
  <BugPlayIcon
248
254
  onClick={() => {
@@ -6,7 +6,7 @@ import {
6
6
  ChevronsUpDownIcon,
7
7
  WrapTextIcon,
8
8
  } from "lucide-react";
9
- import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
9
+ import React, { useEffect, useLayoutEffect, useState } from "react";
10
10
  import { ToggleButton } from "react-aria-components";
11
11
  import { DebuggerControls } from "@/components/debugger/debugger-code";
12
12
  import { CopyClipboardIcon } from "@/components/icons/copy-icon";
@@ -49,31 +49,21 @@ export const CONSOLE_CLEAR_DEBOUNCE_MS = 200;
49
49
  */
50
50
  function useDebouncedConsoleOutputs<T>(outputs: T[]): T[] {
51
51
  const [debouncedOutputs, setDebouncedOutputs] = useState(outputs);
52
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
53
52
 
54
- // Non-empty outputs: apply immediately and cancel any pending clear
53
+ // Non-empty outputs: apply immediately. Effect cleanup cancels pending clears.
55
54
  if (outputs.length > 0 && debouncedOutputs !== outputs) {
56
- if (timerRef.current !== null) {
57
- clearTimeout(timerRef.current);
58
- timerRef.current = null;
59
- }
60
55
  setDebouncedOutputs(outputs);
61
56
  }
62
57
 
63
58
  // Empty outputs: delay the clear so new outputs can arrive first
64
59
  useEffect(() => {
65
- if (outputs.length === 0 && timerRef.current === null) {
66
- timerRef.current = setTimeout(() => {
67
- timerRef.current = null;
68
- setDebouncedOutputs([]);
69
- }, CONSOLE_CLEAR_DEBOUNCE_MS);
60
+ if (outputs.length > 0) {
61
+ return;
70
62
  }
71
- return () => {
72
- if (timerRef.current !== null) {
73
- clearTimeout(timerRef.current);
74
- timerRef.current = null;
75
- }
76
- };
63
+ const timer = setTimeout(() => {
64
+ setDebouncedOutputs([]);
65
+ }, CONSOLE_CLEAR_DEBOUNCE_MS);
66
+ return () => clearTimeout(timer);
77
67
  }, [outputs]);
78
68
 
79
69
  return debouncedOutputs;
@@ -0,0 +1,39 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, render, screen } from "@testing-library/react";
4
+ import { Profiler } from "react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { LargeSpinner } from "../large-spinner";
7
+
8
+ describe("LargeSpinner", () => {
9
+ beforeEach(() => vi.useFakeTimers());
10
+ afterEach(() => vi.useRealTimers());
11
+
12
+ it("fades the old title without a second commit before the timer fires", () => {
13
+ const onRender = vi.fn();
14
+ const spinner = (title: string) => (
15
+ <Profiler id="spinner" onRender={onRender}>
16
+ <LargeSpinner title={title} />
17
+ </Profiler>
18
+ );
19
+ const { rerender } = render(spinner("Starting"));
20
+ onRender.mockClear();
21
+
22
+ rerender(spinner("Loading"));
23
+ expect(onRender).toHaveBeenCalledOnce();
24
+ expect(screen.getByText("Starting")).toHaveClass("opacity-0");
25
+
26
+ act(() => vi.advanceTimersByTime(300));
27
+ expect(screen.getByText("Loading")).toHaveClass("opacity-100");
28
+ });
29
+
30
+ it("restores visibility if the title changes back during the fade", () => {
31
+ const { rerender } = render(<LargeSpinner title="Starting" />);
32
+ rerender(<LargeSpinner title="Loading" />);
33
+ rerender(<LargeSpinner title="Starting" />);
34
+
35
+ expect(screen.getByText("Starting")).toHaveClass("opacity-100");
36
+ act(() => vi.advanceTimersByTime(300));
37
+ expect(screen.queryByText("Loading")).not.toBeInTheDocument();
38
+ });
39
+ });
@@ -1,23 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { Loader2Icon } from "lucide-react";
4
- import { useEffect, useState } from "react";
4
+ import { useDebounce } from "@/hooks/useDebounce";
5
5
  import { cn } from "@/utils/cn";
6
6
 
7
7
  export const LargeSpinner = ({ title }: { title?: string }) => {
8
- const [currentTitle, setCurrentTitle] = useState(title);
9
- const [isVisible, setIsVisible] = useState(true);
10
-
11
- useEffect(() => {
12
- if (title !== currentTitle) {
13
- setIsVisible(false);
14
- const timer = setTimeout(() => {
15
- setCurrentTitle(title);
16
- setIsVisible(true);
17
- }, 300); // Wait for fade out animation to complete
18
- return () => clearTimeout(timer);
19
- }
20
- }, [title, currentTitle]);
8
+ const currentTitle = useDebounce(title, 300);
9
+ const isVisible = title === currentTitle;
21
10
 
22
11
  return (
23
12
  <div className="flex flex-col h-full flex-1 items-center justify-center p-4">
@@ -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>
@@ -117,7 +117,7 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
117
117
  />
118
118
  <LanguageToggles
119
119
  code={cell.code}
120
- editorView={editorViewRef.current}
120
+ editorViewRef={editorViewRef}
121
121
  currentLanguageAdapter={languageAdapter}
122
122
  onAfterToggle={afterToggleLanguage}
123
123
  className="flex items-center gap-1"
@@ -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
+ });
@@ -1,17 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { components } from "@marimo-team/marimo-api";
4
3
  import { atom } from "jotai";
5
- import { isConnectedAtom } from "@/core/network/connection";
4
+ import type { Attachment } from "@marimo-team/client/protocol";
5
+ import { notebookClientAtom } from "@/core/websocket/state";
6
6
 
7
- export type Attachment = components["schemas"]["Attachment"];
7
+ export type { Attachment } from "@marimo-team/client/protocol";
8
8
 
9
- export const attachmentsAtom = atom<Attachment[]>([]);
10
9
  export const agentAttachmentsAtom = atom((get) => {
11
- if (!get(isConnectedAtom)) {
12
- return [];
13
- }
14
- return get(attachmentsAtom).filter(
10
+ const notebook = get(notebookClientAtom);
11
+ return (notebook ? get(notebook.attachmentsAtom) : []).filter(
15
12
  (attachment) => attachment.kind === "agent",
16
13
  );
17
14
  });
@@ -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
+ }
@@ -4,11 +4,51 @@
4
4
  import { describe, expect, it } from "vitest";
5
5
  import { cellId } from "@/__tests__/branded";
6
6
  import type { MarimoError } from "@/core/kernel/messages";
7
- import { buildErrorReport } from "../error-report";
7
+ import { buildErrorReport, ERROR_MESSAGE_INSTRUCTIONS } from "../error-report";
8
8
 
9
9
  const CELL_ID = cellId("failed-cell");
10
10
 
11
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
+
12
52
  it("retains every message and complete traceback without display limits", () => {
13
53
  const traceback = `Traceback\n${"long frame\n".repeat(10_000)}ValueError: bad <value>`;
14
54
  const errors: MarimoError[] = [
@@ -27,6 +67,7 @@ describe("buildErrorReport", () => {
27
67
  code: "failed()",
28
68
  output: {
29
69
  type: "error",
70
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
30
71
  message: "bad <value>\nanother error",
31
72
  traceback,
32
73
  },
@@ -49,7 +90,11 @@ describe("buildErrorReport", () => {
49
90
  ).toEqual({
50
91
  type: "cell-output",
51
92
  cell_id: CELL_ID,
52
- output: { type: "error", message: "legacy error" },
93
+ output: {
94
+ type: "error",
95
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
96
+ message: "legacy error",
97
+ },
53
98
  });
54
99
  });
55
100
 
@@ -63,7 +108,11 @@ describe("buildErrorReport", () => {
63
108
  type: "cell-output",
64
109
  cell_id: CELL_ID,
65
110
  code: "",
66
- output: { type: "error", message: "invalid code" },
111
+ output: {
112
+ type: "error",
113
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
114
+ message: "invalid code",
115
+ },
67
116
  });
68
117
  });
69
118
 
@@ -80,6 +129,7 @@ describe("buildErrorReport", () => {
80
129
  cell_id: CELL_ID,
81
130
  output: {
82
131
  type: "error",
132
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
83
133
  message: "first\nsecond",
84
134
  traceback:
85
135
  "Traceback\nValueError: first\n\nTraceback\nValueError: second",
@@ -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
+ });
@@ -174,8 +174,11 @@ describe("notebookStateFromSession", () => {
174
174
  channel: "marimo-error",
175
175
  data: [
176
176
  {
177
- type: "unknown",
177
+ type: "exception",
178
+ exception_type: "ValueError",
178
179
  msg: "Something went wrong",
180
+ raising_cell: null,
181
+ traceback: "Traceback line 1",
179
182
  },
180
183
  ],
181
184
  mimetype: "application/vnd.marimo+error",
@@ -201,8 +204,9 @@ describe("notebookStateFromSession", () => {
201
204
  {
202
205
  type: "stream",
203
206
  name: "stderr",
204
- text: "<pre>ValueError: cached failure</pre>",
207
+ text: "ValueError: cached <failure>",
205
208
  mimetype: "application/vnd.marimo+traceback",
209
+ traceback_format: "text",
206
210
  code,
207
211
  },
208
212
  ],
@@ -212,9 +216,20 @@ describe("notebookStateFromSession", () => {
212
216
  invariant(state, "state is null");
213
217
  expect(state.cellRuntime[CELL_1].output).toMatchObject({
214
218
  code,
215
- data: [{ type: "unknown", msg: "cached failure" }],
219
+ data: [
220
+ {
221
+ type: "exception",
222
+ exception_type: "ValueError",
223
+ msg: "cached failure",
224
+ traceback: "Traceback\nValueError: cached failure",
225
+ },
226
+ ],
216
227
  });
217
228
  expect(state.cellRuntime[CELL_1].consoleOutputs[0].code).toBe(code);
229
+ expect(state.cellRuntime[CELL_1].consoleOutputs[0]).toMatchObject({
230
+ traceback_format: "text",
231
+ data: "ValueError: cached <failure>",
232
+ });
218
233
  });
219
234
 
220
235
  it("handles data output in session cell", () => {
@@ -1761,7 +1761,8 @@ export const cellErrorsAtom = atom((get) => {
1761
1761
  });
1762
1762
 
1763
1763
  export const notebookOutline = atom((get) => {
1764
- const { cellIds, cellRuntime } = get(notebookAtom);
1764
+ const cellIds = get(cellIdsAtom);
1765
+ const cellRuntime = get(cellsRuntimeAtom);
1765
1766
  const outlines = cellIds.inOrderIds.map(
1766
1767
  (cellId) => cellRuntime[cellId].outline,
1767
1768
  );