@marimo-team/frontend 0.23.17-dev5 → 0.23.17-dev50

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 (326) hide show
  1. package/dist/assets/{CellStatus-DIW20_Z8.js → CellStatus-XlxL4ctu.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-C4zEgCZh.js → ConnectedDataExplorerComponent-BF5CJvB2.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-DzTD8LVq.js → ErrorBoundary-BfMsWVQU.js} +1 -1
  4. package/dist/assets/ImageComparisonComponent-CukPDP1p.js +1 -0
  5. package/dist/assets/{ImperativeModal-fY-ZfT0b.js → ImperativeModal-Ci0icDPt.js} +1 -1
  6. package/dist/assets/{JsonOutput-Bk2exFh0.js → JsonOutput-W3f6H6VQ.js} +13 -13
  7. package/dist/assets/{LazyAnyLanguageCodeMirror-A-Ant1Ad.js → LazyAnyLanguageCodeMirror-Dcvy_p3-.js} +2 -2
  8. package/dist/assets/{MarimoErrorOutput-L_o2Knzo.js → MarimoErrorOutput-COZo8cfk.js} +5 -5
  9. package/dist/assets/{RSPContexts-WiCgGm__.js → RSPContexts-IrDQoFki.js} +1 -1
  10. package/dist/assets/{RenderHTML-BTDLQMKg.js → RenderHTML-CQC8I348.js} +1 -1
  11. package/dist/assets/{RunButton-DiHX9YjB.js → RunButton-DRxV8u_T.js} +1 -1
  12. package/dist/assets/add-cell-with-ai-CuLILhtV.js +107 -0
  13. package/dist/assets/{add-connection-dialog-BZaJAQCM.js → add-connection-dialog-Dld_PDdZ.js} +31 -31
  14. package/dist/assets/{agent-panel-CRxiqCgP.js → agent-panel-3Ta5EPgw.js} +6 -6
  15. package/dist/assets/ai-model-dropdown-CIqhiHxf.js +5 -0
  16. package/dist/assets/{alert-dialog-Bb8Lz8ko.js → alert-dialog-DMh8_LWc.js} +1 -1
  17. package/dist/assets/{any-language-editor-BIeaxtdD.js → any-language-editor-DUQN91an.js} +1 -1
  18. package/dist/assets/{app-config-button-BS4LSo1l.js → app-config-button-EyzXs1Nt.js} +1 -1
  19. package/dist/assets/{blob-B9eg-RfN.js → blob-BzgGkt3B.js} +1 -1
  20. package/dist/assets/{button-DDGiD1NW.js → button-BB1AV4Rb.js} +1 -1
  21. package/dist/assets/{cache-panel-B2B1ePao.js → cache-panel-hpL5o4Y7.js} +1 -1
  22. package/dist/assets/{capabilities-DoEg0XwL.js → capabilities-LI6_PoMn.js} +1 -1
  23. package/dist/assets/{cell-editor-OC7h_nZ3.js → cell-editor-C5qD14SG.js} +11 -11
  24. package/dist/assets/{cell-link-D0aELsL_.js → cell-link-uPmFMRTh.js} +1 -1
  25. package/dist/assets/cells-BE1kpmPF.css +2 -0
  26. package/dist/assets/{cells-DLYs3NiJ.js → cells-DxL32Atg.js} +60 -60
  27. package/dist/assets/chat-display-CBpsXLHT.js +1 -0
  28. package/dist/assets/chat-panel-Dzq-QTrj.js +4 -0
  29. package/dist/assets/chat-ui-BcI4PAQ1.js +4 -0
  30. package/dist/assets/{column-preview-CnqeJb-x.js → column-preview-Dxxs9Brz.js} +1 -1
  31. package/dist/assets/{command-Dqxie4gq.js → command-CPFHYoYk.js} +1 -1
  32. package/dist/assets/command-palette-B1EO8oVR.js +1 -0
  33. package/dist/assets/{common-D_beLbpm.js → common-aHaDUD0-.js} +1 -1
  34. package/dist/assets/components-Bm1d4vm9.js +1 -0
  35. package/dist/assets/{components-CxEGXaTB.js → components-RV25kGIW.js} +1 -1
  36. package/dist/assets/config-BVfo0E1-.js +1 -0
  37. package/dist/assets/config-Bm5B9wGP.js +1 -0
  38. package/dist/assets/{copy-Bxb1_Gtw.js → copy-B-LLR1Yy.js} +1 -1
  39. package/dist/assets/{copy-icon-neDQUo4Q.js → copy-icon-DWG8Y-xW.js} +1 -1
  40. package/dist/assets/{createReducer-DGx5Af-C.js → createReducer-DNehfY0t.js} +1 -1
  41. package/dist/assets/{datasource-Bfg8PKXp.js → datasource-D0eBPOXe.js} +2 -2
  42. package/dist/assets/{dates-C2LWzsCZ.js → dates-BzOh4kS_.js} +1 -1
  43. package/dist/assets/defaultLocale-t77eoiwF.js +1 -0
  44. package/dist/assets/{dependency-graph-panel-CB5ie0zu.js → dependency-graph-panel-BKWUzXHX.js} +2 -2
  45. package/dist/assets/{diagram-PSM6KHXK-DIXxW5au.js → diagram-PSM6KHXK-CrTUa1Xy.js} +1 -1
  46. package/dist/assets/{dialog-BCVepWLJ.js → dialog-88bLE398.js} +1 -1
  47. package/dist/assets/{dist-CPjTeT8p.js → dist-6qzebh8f.js} +1 -1
  48. package/dist/assets/dist-BLqRx0W-.js +1 -0
  49. package/dist/assets/{dist-BP4hpPAE.js → dist-BVr4HQPR.js} +1 -1
  50. package/dist/assets/{dist-DnjsPBRJ.js → dist-BWXIz8tq.js} +1 -1
  51. package/dist/assets/{dist-Cros4IAo.js → dist-BWbS__vR.js} +1 -1
  52. package/dist/assets/dist-BZehnCk2.js +1 -0
  53. package/dist/assets/dist-BaRgF7DU.js +1 -0
  54. package/dist/assets/{dist-DYEuVkEX.js → dist-BblMEWTQ.js} +1 -1
  55. package/dist/assets/dist-BlMeFaGB.js +1 -0
  56. package/dist/assets/{dist-CrKffqgx.js → dist-BmYa2llD.js} +1 -1
  57. package/dist/assets/{dist-9o0Xjd5v.js → dist-Btp8vRFr.js} +1 -1
  58. package/dist/assets/{dist-C-3FVUQA.js → dist-BvMWjDxm.js} +1 -1
  59. package/dist/assets/{dist-DeJBvhAc.js → dist-ByfZ_2N7.js} +1 -1
  60. package/dist/assets/dist-C-g2AKOo.js +1 -0
  61. package/dist/assets/{dist-DQl27SFq.js → dist-C52BqSeO.js} +1 -1
  62. package/dist/assets/{dist-CiVShZKM.js → dist-C7rDfTPJ.js} +1 -1
  63. package/dist/assets/{dist-BKgHjzML.js → dist-C830fIIj.js} +1 -1
  64. package/dist/assets/{dist-fLBYOt9R.js → dist-CHN_sQZw.js} +1 -1
  65. package/dist/assets/dist-CLFhXj8x.js +1 -0
  66. package/dist/assets/{dist-D47O3FBX.js → dist-CLQWOmD7.js} +1 -1
  67. package/dist/assets/dist-CO4dulTL.js +1 -0
  68. package/dist/assets/dist-CPw3iq_l.js +1 -0
  69. package/dist/assets/dist-CQtN-vVI.js +1 -0
  70. package/dist/assets/dist-CZE9FE_5.js +1 -0
  71. package/dist/assets/dist-CexuM4NA.js +1 -0
  72. package/dist/assets/{dist-DJuRuTXr.js → dist-CfiLpZZj.js} +1 -1
  73. package/dist/assets/dist-CjSt6gRp.js +1 -0
  74. package/dist/assets/dist-Cr4q2U0d.js +1 -0
  75. package/dist/assets/{dist-C6_DArtj.js → dist-CxaoIFrU.js} +1 -1
  76. package/dist/assets/{dist-aRc_dRed.js → dist-D5nYeU7Q.js} +1 -1
  77. package/dist/assets/{dist-BJZAIIze.js → dist-DX04AOlt.js} +1 -1
  78. package/dist/assets/dist-DkZos4_s.js +1 -0
  79. package/dist/assets/{dist-DI1-Z60I.js → dist-DmCNk9gq.js} +1 -1
  80. package/dist/assets/{dist-kLs0VbW_.js → dist-DmjT6iaH.js} +1 -1
  81. package/dist/assets/{dist-CvWC93qL.js → dist-DrY8fZeD.js} +1 -1
  82. package/dist/assets/{dist-l4RTzHX-.js → dist-Drexw1Ir.js} +1 -1
  83. package/dist/assets/{dist-DrBqBDHF.js → dist-DtNSqHzW.js} +1 -1
  84. package/dist/assets/dist-DxQA7Dn1.js +1 -0
  85. package/dist/assets/{dist-BLN8PNJV.js → dist-Dza6FJtV.js} +1 -1
  86. package/dist/assets/{dist-8SH6SWEG.js → dist-HrXjBuXP.js} +1 -1
  87. package/dist/assets/dist-J00F7mRY.js +1 -0
  88. package/dist/assets/{dist-DcwTQG1A.js → dist-Jbd9zUbK.js} +1 -1
  89. package/dist/assets/{dist-CZvbHnyT.js → dist-bvfx5OQE.js} +1 -1
  90. package/dist/assets/{dist-12S8I4CH.js → dist-gUHullA_.js} +11 -11
  91. package/dist/assets/dist-nYKCNtEX.js +1 -0
  92. package/dist/assets/dist-oK8dffBy.js +1 -0
  93. package/dist/assets/{documentation-panel-Dii7ouzj.js → documentation-panel-DotG4o5W.js} +1 -1
  94. package/dist/assets/{download-DenADH0w.js → download-Tklnz6pn.js} +3 -3
  95. package/dist/assets/{dropdown-menu-dHmZR8bI.js → dropdown-menu-BGBlPKNz.js} +1 -1
  96. package/dist/assets/{edit-page-B0EhuiM-.js → edit-page-HdLqqfXC.js} +6 -6
  97. package/dist/assets/{error-banner-BSxEOIn2.js → error-banner-BkIr-0dH.js} +1 -1
  98. package/dist/assets/{error-entries-4XYBryls.js → error-entries-BvUR_22-.js} +3 -3
  99. package/dist/assets/{error-panel-xYZ4lX0A.js → error-panel-v0YdlA0h.js} +1 -1
  100. package/dist/assets/{errors-PgHhDa-M.js → errors-DCPV979Q.js} +1 -1
  101. package/dist/assets/{es-gYgfF8y5.js → es-Dl2xAwTv.js} +1 -1
  102. package/dist/assets/{esm-B30N1GS0.js → esm-Bmzgrl_5.js} +1 -1
  103. package/dist/assets/{esm-BDGraAqp.js → esm-Nb9m4lqg.js} +1 -1
  104. package/dist/assets/{field-BsxD35VE.js → field-BFgvUZyh.js} +1 -1
  105. package/dist/assets/file-explorer-panel-DSZY0Q7h.js +37 -0
  106. package/dist/assets/{file-icons-mgXTzM1c.js → file-icons-NkObNkr_.js} +1 -1
  107. package/dist/assets/{file-name-input-Dq7Z_QmU.js → file-name-input-BQJ9U9lU.js} +1 -1
  108. package/dist/assets/{floating-outline-ypOHt2wD.js → floating-outline-BHmregl1.js} +1 -1
  109. package/dist/assets/{focus-0L1nX7M9.js → focus-BBiJct2G.js} +1 -1
  110. package/dist/assets/{form-oz4Z280Z.js → form-BGoLLcgk.js} +1 -1
  111. package/dist/assets/{form-BsW7zr4t.js → form-BIUQB1NI.js} +1 -1
  112. package/dist/assets/{formats-DzmLpc9A.js → formats-DNYtnQk7.js} +1 -1
  113. package/dist/assets/{formatting-COz4XEaB.js → formatting-qNOrQYRN.js} +1 -1
  114. package/dist/assets/{fullscreen-CTDqNQr6.js → fullscreen-B51geQ_6.js} +1 -1
  115. package/dist/assets/{gallery-page-DbuxknAw.js → gallery-page-DBHHOraF.js} +1 -1
  116. package/dist/assets/{ganttDiagram-JELNMOA3-DaS60lKW.js → ganttDiagram-JELNMOA3-C9KBLmAA.js} +1 -1
  117. package/dist/assets/{glide-data-editor-CejKKgnN.js → glide-data-editor-CHN4-xUu.js} +1 -1
  118. package/dist/assets/{home-page-BOP1dDcM.js → home-page-DLeE6pH_.js} +2 -2
  119. package/dist/assets/{html-to-image-D_C-hjtC.js → html-to-image-ChNWEUlR.js} +1 -1
  120. package/dist/assets/index-BxVymcMs.js +38 -0
  121. package/dist/assets/index-WoaGtlKd.css +2 -0
  122. package/dist/assets/{input-JxFwu-3G.js → input-_AaMXWgD.js} +1 -1
  123. package/dist/assets/{kiosk-mode-BvKMuigM.js → kiosk-mode-RTwj6XH4.js} +1 -1
  124. package/dist/assets/{label-CqjrpwRC.js → label-DUD3SZnk.js} +1 -1
  125. package/dist/assets/{layout-yfmf2ZBx.js → layout-QkEddeXT.js} +4 -4
  126. package/dist/assets/{linear-BPv4krGt.js → linear-B1ROUUtm.js} +1 -1
  127. package/dist/assets/{links-ClNQSIal.js → links-DBc28bsl.js} +1 -1
  128. package/dist/assets/{logs-panel-OMd1Z9CL.js → logs-panel-Cdcjc_fq.js} +1 -1
  129. package/dist/assets/markdown-export-CNu5ZTPR.js +1 -0
  130. package/dist/assets/{markdown-renderer-BfLNJGNj.js → markdown-renderer-DpskAUxO.js} +1 -1
  131. package/dist/assets/{menu-items-CCi6aOPm.js → menu-items-Db-bgz92.js} +1 -1
  132. package/dist/assets/{mermaid-B8gNHyGy.js → mermaid-OU2hLna0.js} +3 -3
  133. package/dist/assets/{name-cell-input-COtjRghz.js → name-cell-input-DoxL2EXI.js} +1 -1
  134. package/dist/assets/{numbers-rmawRqQP.js → numbers-D9g2ArIv.js} +1 -1
  135. package/dist/assets/{outline-panel-CIGf4SLb.js → outline-panel-BxjI-YSj.js} +1 -1
  136. package/dist/assets/{packages-panel-DLuMuNuG.js → packages-panel-CiKYP9Ij.js} +1 -1
  137. package/dist/assets/{pair-with-agent-modal-C5OhJ8rX.js → pair-with-agent-modal-1b5Eh8Xo.js} +1 -1
  138. package/dist/assets/{panel-accordion-state-D1UP17Lk.js → panel-accordion-state-BkWYwckU.js} +1 -1
  139. package/dist/assets/{panels-B0w23XJS.js → panels-Swk0l08v.js} +2 -2
  140. package/dist/assets/{popover-Bcz1Zikb.js → popover-DHbixh3D.js} +1 -1
  141. package/dist/assets/{precisionRound-i2ImZxgc.js → precisionRound-DORAUfVY.js} +1 -1
  142. package/dist/assets/process-output-DJonUYp2.js +1 -0
  143. package/dist/assets/{quadrantDiagram-AYHSOK5B-DJuGB95-.js → quadrantDiagram-AYHSOK5B-Duf8sGaa.js} +1 -1
  144. package/dist/assets/{radio-group-BGXYy3u5.js → radio-group-ByLzIKjm.js} +1 -1
  145. package/dist/assets/react-vega-J-J2akIt.js +191 -0
  146. package/dist/assets/{readonly-code-display-DdABj8tH.js → readonly-code-display-B_ZHScA7.js} +1 -1
  147. package/dist/assets/{readonly-python-code-DUjqXwhi.js → readonly-python-code-C4GeGU7O.js} +1 -1
  148. package/dist/assets/{renderShortcut-Bl8_FVn8.js → renderShortcut-DnqsU4zj.js} +1 -1
  149. package/dist/assets/{reveal-component-BjgfnO81.js → reveal-component-De_cjq3p.js} +2 -2
  150. package/dist/assets/{run-page-DDty5GvQ.js → run-page-DmXT_-MQ.js} +1 -1
  151. package/dist/assets/{runs-s9t2f_Tg.js → runs-C6avq6Gi.js} +1 -1
  152. package/dist/assets/scratchpad-panel-BtGHIRdJ.js +1 -0
  153. package/dist/assets/{scroll-to-bottom-button-C8yfT4wq.js → scroll-to-bottom-button-CRMPUjYZ.js} +1 -1
  154. package/dist/assets/{secrets-panel-Cz17_MLf.js → secrets-panel-DmAwH7vI.js} +1 -1
  155. package/dist/assets/{select-BP75_5jJ.js → select-Clmc8Hw2.js} +1 -1
  156. package/dist/assets/{session-BmukRjSi.js → session-BtBr2zr3.js} +1 -1
  157. package/dist/assets/{session-panel-aMoQPTCB.js → session-panel-kBLydN0h.js} +1 -1
  158. package/dist/assets/{share-852X_cqy.js → share-DSOv_SvY.js} +1 -1
  159. package/dist/assets/{skeleton-CPuDx7yn.js → skeleton-CJymG2v3.js} +1 -1
  160. package/dist/assets/{snippets-panel-DBDXr-XP.js → snippets-panel-l_HnIWUj.js} +1 -1
  161. package/dist/assets/{spec-C7opZZRp.js → spec-CdDzA5ef.js} +1 -1
  162. package/dist/assets/{state-DCtLlFJq.js → state-B4lC29et.js} +1 -1
  163. package/dist/assets/{state-C4kiwedg.js → state-BkoVfBqt.js} +1 -1
  164. package/dist/assets/{state-hlEqpZfH.js → state-D9xpDCxa.js} +2 -2
  165. package/dist/assets/state-DDimGh2o.js +1 -0
  166. package/dist/assets/{strings--BknHX9C.js → strings-DMKwTex-.js} +1 -1
  167. package/dist/assets/{swiper-component-DXW8snlt.js → swiper-component-pSAP3cRf.js} +1 -1
  168. package/dist/assets/switch-8FxRs-OU.js +1 -0
  169. package/dist/assets/{terminal-CaHTVNrz.js → terminal-DPQJ6hNg.js} +1 -1
  170. package/dist/assets/{textarea-DtCcemo9.js → textarea-BzWvGIrJ.js} +1 -1
  171. package/dist/assets/{time-p47i6wvp.js → time-BO-naW1O.js} +1 -1
  172. package/dist/assets/tooltip-BMi4WFDl.js +61 -0
  173. package/dist/assets/{tooltip-4bZlU7jl.js → tooltip-BcgbFR7q.js} +1 -1
  174. package/dist/assets/{tracing-CStM7RGI.js → tracing-DCuWLKnk.js} +1 -1
  175. package/dist/assets/{tracing-panel-BTLzu8L-.js → tracing-panel-dVtThkzL.js} +2 -2
  176. package/dist/assets/{tree-actions-BwwVyp3l.js → tree-actions-o2iyMmKN.js} +1 -1
  177. package/dist/assets/{useBoolean-DIi7jQLa.js → useBoolean-sASJEpGA.js} +1 -1
  178. package/dist/assets/{useCellActionButton-uxVyv3JK.js → useCellActionButton-n2-2xb-5.js} +1 -1
  179. package/dist/assets/{useDeleteCell-Cy23OP4e.js → useDeleteCell-DH0weyin.js} +1 -1
  180. package/dist/assets/{useDependencyPanelTab-DNfjp_-X.js → useDependencyPanelTab-DQZmbkd-.js} +1 -1
  181. package/dist/assets/{useEventListener-C0195JsF.js → useEventListener-B_wSLgFR.js} +1 -1
  182. package/dist/assets/{useHotkey-DLZvrRZT.js → useHotkey-DJyYmk5P.js} +1 -1
  183. package/dist/assets/{useIframeCapabilities-j6fR_dBA.js → useIframeCapabilities-D_6fB_8O.js} +1 -1
  184. package/dist/assets/{useInstallPackage-WiYBwhUO.js → useInstallPackage-DyNrAhrx.js} +1 -1
  185. package/dist/assets/{useInterval-CnQfyMa9.js → useInterval-CjGsg4ie.js} +1 -1
  186. package/dist/assets/useNotebookActions-BUQta3nX.js +12 -0
  187. package/dist/assets/useRunCells-8xYRh7wH.js +1 -0
  188. package/dist/assets/{useSplitCell-BK5mnkkL.js → useSplitCell-BvgvIZaz.js} +1 -1
  189. package/dist/assets/{useTheme-Bxv9B31g.js → useTheme-BR31Gjbc.js} +1 -1
  190. package/dist/assets/utils-C5VWChv9.js +1 -0
  191. package/dist/assets/{vega-component-BGiCExka.js → vega-component-DG9G1qk2.js} +1 -1
  192. package/dist/assets/{vega-embed-container-DxSqUOCK.js → vega-embed-container-BJRQVehC.js} +1 -1
  193. package/dist/assets/vega-loader.browser-CAfmqKEc.js +6 -0
  194. package/dist/assets/{write-secret-modal-Dtm66yVf.js → write-secret-modal-DwCUuNRy.js} +1 -1
  195. package/dist/assets/{xychartDiagram-PRI3JC2R-B8A0hi-Q.js → xychartDiagram-PRI3JC2R-ClPpSFDI.js} +1 -1
  196. package/dist/assets/{zod-H_cgTO0M.js → zod-CIOjj3iA.js} +5 -5
  197. package/dist/index.html +87 -87
  198. package/package.json +7 -7
  199. package/src/__mocks__/requests.ts +5 -0
  200. package/src/components/ai/ai-model-dropdown.tsx +2 -2
  201. package/src/components/app-config/user-config-form.tsx +26 -0
  202. package/src/components/chat/__tests__/capabilities-popover.test.tsx +38 -0
  203. package/src/components/chat/__tests__/chat-abort.test.ts +51 -0
  204. package/src/components/chat/__tests__/use-chat-controller-id.test.tsx +39 -0
  205. package/src/components/chat/capabilities-popover.tsx +68 -0
  206. package/src/components/chat/chat-abort.ts +44 -0
  207. package/src/components/chat/chat-display.tsx +17 -0
  208. package/src/components/chat/chat-panel.tsx +107 -50
  209. package/src/components/chat/chat-utils.ts +1 -1
  210. package/src/components/chat/use-chat-controller-id.ts +24 -0
  211. package/src/components/data-table/charts/__tests__/schemas.test.ts +35 -0
  212. package/src/components/data-table/charts/schemas.ts +5 -2
  213. package/src/components/databases/icons/huggingface.svg +8 -0
  214. package/src/components/datasets/__tests__/icons.test.ts +21 -0
  215. package/src/components/datasets/icons.tsx +10 -0
  216. package/src/components/datasources/__tests__/components.test.tsx +29 -0
  217. package/src/components/datasources/components.tsx +8 -3
  218. package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +35 -0
  219. package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +61 -0
  220. package/src/components/editor/actions/export-dialog/export-command.ts +4 -9
  221. package/src/components/editor/actions/export-dialog/export-dialog.tsx +27 -23
  222. package/src/components/editor/actions/export-dialog/format-notice.tsx +28 -0
  223. package/src/components/editor/actions/export-dialog/state.ts +23 -5
  224. package/src/components/editor/actions/export-dialog/use-export-dialog.ts +7 -15
  225. package/src/components/editor/actions/useConfigActions.tsx +24 -0
  226. package/src/components/editor/ai/add-cell-with-ai.tsx +2 -2
  227. package/src/components/editor/ai/ai-completion-editor.tsx +2 -2
  228. package/src/components/editor/chrome/panels/outline/__tests__/useActiveOutline.test.ts +26 -0
  229. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +2 -2
  230. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +114 -2
  231. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +14 -0
  232. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +20 -0
  233. package/src/components/editor/connections/storage/add-storage-form.tsx +10 -0
  234. package/src/components/editor/connections/storage/as-code.ts +19 -1
  235. package/src/components/editor/connections/storage/schemas.ts +19 -0
  236. package/src/components/storage/__tests__/storage-snippets.test.ts +88 -0
  237. package/src/components/storage/components.tsx +2 -0
  238. package/src/components/storage/storage-snippets.ts +58 -0
  239. package/src/core/__tests__/mode.test.ts +85 -0
  240. package/src/core/ai/__tests__/config.test.tsx +96 -0
  241. package/src/core/ai/config.ts +16 -22
  242. package/src/core/ai/constants.ts +7 -0
  243. package/src/core/ai/staged-cells.ts +3 -0
  244. package/src/core/ai/state.ts +10 -0
  245. package/src/core/codemirror/__tests__/setup.test.ts +1 -1
  246. package/src/core/codemirror/cm.ts +2 -2
  247. package/src/core/codemirror/code-lens/__tests__/extension.test.ts +4 -5
  248. package/src/core/codemirror/code-lens/extension.ts +3 -3
  249. package/src/core/codemirror/find-replace/search-highlight.ts +8 -3
  250. package/src/core/codemirror/language/languages/sql/__tests__/renderers.test.tsx +44 -0
  251. package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
  252. package/src/core/codemirror/theme/__tests__/light.test.ts +59 -123
  253. package/src/core/codemirror/theme/dark.ts +6 -5
  254. package/src/core/codemirror/theme/light.ts +6 -4
  255. package/src/core/config/__tests__/config-schema.test.ts +2 -0
  256. package/src/core/config/config-schema.ts +1 -0
  257. package/src/core/config/feature-flag.tsx +0 -2
  258. package/src/core/dom/outline.ts +25 -2
  259. package/src/core/hotkeys/hotkeys.ts +2 -0
  260. package/src/core/mode.ts +11 -9
  261. package/src/core/network/__tests__/api.test.ts +132 -17
  262. package/src/core/network/__tests__/export-filename.test.ts +43 -0
  263. package/src/core/network/__tests__/requests-lazy.test.ts +1 -0
  264. package/src/core/network/api.ts +47 -12
  265. package/src/core/network/export-filename.ts +37 -0
  266. package/src/core/network/markdown-export.ts +19 -0
  267. package/src/core/network/requests-network.ts +29 -5
  268. package/src/core/network/types.ts +4 -0
  269. package/src/core/storage/types.ts +1 -0
  270. package/src/css/app/codemirror.css +11 -7
  271. package/src/css/globals.css +8 -0
  272. package/src/plugins/impl/DataEditorPlugin.tsx +2 -9
  273. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +18 -0
  274. package/src/plugins/impl/chat/chat-ui.tsx +51 -3
  275. package/src/plugins/impl/data-frames/__tests__/schema.test.ts +30 -0
  276. package/src/plugins/impl/data-frames/schema.ts +4 -1
  277. package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +20 -1
  278. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +58 -0
  279. package/src/stories/theme.stories.tsx +71 -1
  280. package/dist/assets/ImageComparisonComponent-COvcNAUu.js +0 -1
  281. package/dist/assets/add-cell-with-ai-BU5EDuHt.js +0 -107
  282. package/dist/assets/ai-model-dropdown-CLCa-gsg.js +0 -5
  283. package/dist/assets/cells-BVzo2MHf.css +0 -2
  284. package/dist/assets/chat-display-7CzBSZRm.js +0 -1
  285. package/dist/assets/chat-panel-Dd5WiR1a.js +0 -4
  286. package/dist/assets/chat-ui-CYqqqoSC.js +0 -4
  287. package/dist/assets/command-palette-BBlSC8AZ.js +0 -1
  288. package/dist/assets/components-K_Mi0vdC.js +0 -1
  289. package/dist/assets/config-BJVx3eBW.js +0 -1
  290. package/dist/assets/config-DAL-jiRM.js +0 -1
  291. package/dist/assets/defaultLocale-DOVIEEPN.js +0 -1
  292. package/dist/assets/dist-B99-E4qp.js +0 -1
  293. package/dist/assets/dist-Bfm9U3vp.js +0 -1
  294. package/dist/assets/dist-BiwuSVkx.js +0 -1
  295. package/dist/assets/dist-BrZjJXDA.js +0 -1
  296. package/dist/assets/dist-CaZf6AoY.js +0 -1
  297. package/dist/assets/dist-Cgoy5550.js +0 -1
  298. package/dist/assets/dist-Csf5i4KY.js +0 -1
  299. package/dist/assets/dist-D6Iobl3P.js +0 -1
  300. package/dist/assets/dist-D6WgndTj.js +0 -1
  301. package/dist/assets/dist-DcXy-1eJ.js +0 -1
  302. package/dist/assets/dist-DsfPGbgk.js +0 -1
  303. package/dist/assets/dist-DtbtRAqz.js +0 -1
  304. package/dist/assets/dist-I3qeQWuP.js +0 -1
  305. package/dist/assets/dist-aeKlrIMv.js +0 -1
  306. package/dist/assets/dist-gDi2I7IJ.js +0 -1
  307. package/dist/assets/dist-miDa4-cz.js +0 -1
  308. package/dist/assets/dist-nDEBxpL6.js +0 -1
  309. package/dist/assets/dist-oUL4XFdB.js +0 -1
  310. package/dist/assets/file-explorer-panel-4h-BKKaM.js +0 -26
  311. package/dist/assets/house-DS0r7dNZ.js +0 -1
  312. package/dist/assets/index-BXlfeAku.js +0 -38
  313. package/dist/assets/index-DYycoCjG.css +0 -2
  314. package/dist/assets/process-output-Cm_vuKBw.js +0 -1
  315. package/dist/assets/react-vega-BLc5RhC7.js +0 -188
  316. package/dist/assets/scratchpad-panel-DJkO5OrI.js +0 -1
  317. package/dist/assets/state-Brswj8bD.js +0 -1
  318. package/dist/assets/switch-CONP-vq2.js +0 -1
  319. package/dist/assets/tooltip-B7U4xkIN.js +0 -61
  320. package/dist/assets/useNotebookActions-BHaEvPEM.js +0 -12
  321. package/dist/assets/useRunCells-kmbsc2R5.js +0 -1
  322. package/dist/assets/utils-BWBBpCkT.js +0 -1
  323. package/dist/assets/vega-loader.browser-Dun7Qe4B.js +0 -6
  324. /package/dist/assets/{bundle.esm-D0TWQ6iT.js → bundle.esm-CvMga-X6.js} +0 -0
  325. /package/dist/assets/{icons-CIPE8AIY.js → icons-NFYKylj6.js} +0 -0
  326. /package/dist/assets/{useDeepCompareMemoize-CSMoa-ga.js → useDeepCompareMemoize-BLRnan2Y.js} +0 -0
@@ -140,11 +140,16 @@ export const searchHighlighter = ViewPlugin.fromClass(
140
140
  );
141
141
 
142
142
  export const highlightTheme = EditorView.baseTheme({
143
- "&light .cm-searchMatch": { backgroundColor: "#99ff7780" },
144
- "&dark .cm-searchMatch": { backgroundColor: "#22bb0070" },
143
+ "&light .cm-searchMatch": {
144
+ backgroundColor: "var(--cm-search-match-background-light)",
145
+ },
146
+ "&dark .cm-searchMatch": {
147
+ backgroundColor: "var(--cm-search-match-background-dark)",
148
+ },
145
149
 
146
150
  "&light .cm-searchMatch-selected": { backgroundColor: "transparent" },
147
151
  "&dark .cm-searchMatch.cm-searchMatch-selected": {
148
- backgroundColor: "#6199ff88 !important",
152
+ backgroundColor:
153
+ "var(--cm-search-match-selected-background-dark) !important",
149
154
  },
150
155
  });
@@ -0,0 +1,44 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import type {
6
+ DataTable,
7
+ DataTableColumn,
8
+ DataType,
9
+ } from "@/core/kernel/messages";
10
+ import { renderColumnInfo, renderTableInfo } from "../renderers";
11
+
12
+ const geometryColumn = {
13
+ name: "geom",
14
+ type: "geometry" as DataType,
15
+ external_type: "geometry",
16
+ sample_values: [],
17
+ } as DataTableColumn;
18
+
19
+ describe("SQL renderers", () => {
20
+ it("renders table information with an unrecognized column type", () => {
21
+ const table: DataTable = {
22
+ name: "shapes",
23
+ type: "table",
24
+ source: "local",
25
+ source_type: "local",
26
+ num_rows: null,
27
+ num_columns: 1,
28
+ variable_name: null,
29
+ columns: [geometryColumn],
30
+ primary_keys: [],
31
+ indexes: [],
32
+ };
33
+
34
+ const result = render(renderTableInfo(table));
35
+
36
+ expect(result.container.querySelector("svg")).not.toBeNull();
37
+ });
38
+
39
+ it("renders column information with an unrecognized column type", () => {
40
+ const result = render(renderColumnInfo(geometryColumn));
41
+
42
+ expect(result.container.querySelector("svg")).not.toBeNull();
43
+ });
44
+ });
@@ -13,7 +13,7 @@ import {
13
13
  TableIcon,
14
14
  ViewIcon,
15
15
  } from "@/components/databases/namespace-icons";
16
- import { DATA_TYPE_ICON } from "@/components/datasets/icons";
16
+ import { DATA_TYPE_ICON, resolveDataType } from "@/components/datasets/icons";
17
17
  import { Badge } from "@/components/ui/badge";
18
18
  import {
19
19
  type ConnectionName,
@@ -149,7 +149,7 @@ export const renderTableInfo = (table: DataTable): React.ReactNode => {
149
149
  );
150
150
 
151
151
  const columnItems = table.columns.map((column) => {
152
- const TypeIcon = DATA_TYPE_ICON[column.type];
152
+ const TypeIcon = DATA_TYPE_ICON[resolveDataType(column.type)];
153
153
  return (
154
154
  <div
155
155
  key={column.name}
@@ -288,7 +288,7 @@ export const renderTableInfo = (table: DataTable): React.ReactNode => {
288
288
  };
289
289
 
290
290
  export const renderColumnInfo = (column: DataTableColumn): React.ReactNode => {
291
- const TypeIcon = DATA_TYPE_ICON[column.type];
291
+ const TypeIcon = DATA_TYPE_ICON[resolveDataType(column.type)];
292
292
 
293
293
  const typeBadge = (
294
294
  <Badge
@@ -1,136 +1,72 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { type Tag, tags as t } from "@lezer/highlight";
4
- import { describe, expect, it } from "vitest";
3
+ import type { Extension } from "@codemirror/state";
4
+ import { EditorView } from "@codemirror/view";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { darkTheme } from "../dark";
5
7
  import { lightTheme } from "../light";
6
8
 
7
- // Helper function to get theme configuration from the source
8
- const getThemeConfig = () => ({
9
- variant: "light",
10
- settings: {
11
- background: "#ffffff",
12
- foreground: "#000000",
13
- caret: "#000000",
14
- selection: "#d7d4f0",
15
- lineHighlight: "#cceeff44",
16
- gutterBackground: "var(--color-background)",
17
- gutterForeground: "var(--gray-10)",
18
- },
19
- styles: [
20
- { tag: t.comment, color: "#708090" },
21
- { tag: t.variableName, color: "#000000" },
22
- { tag: [t.string, t.special(t.brace)], color: "#a11" },
23
- { tag: t.number, color: "#164" },
24
- { tag: t.bool, color: "#219" },
25
- { tag: t.null, color: "#219" },
26
- { tag: t.keyword, color: "#708", fontWeight: 500 },
27
- { tag: t.className, color: "#00f" },
28
- { tag: t.definition(t.typeName), color: "#00f" },
29
- { tag: t.typeName, color: "#085" },
30
- { tag: t.angleBracket, color: "#000000" },
31
- { tag: t.tagName, color: "#170" },
32
- { tag: t.attributeName, color: "#00c" },
33
- { tag: t.operator, color: "#a2f", fontWeight: 500 },
34
- { tag: [t.function(t.variableName)], color: "#00c" },
35
- { tag: [t.propertyName], color: "#05a" },
36
- ],
37
- });
38
-
39
- describe("lightTheme", () => {
40
- it("should export a theme", () => {
41
- expect(lightTheme).toBeDefined();
42
- expect(typeof lightTheme).toBe("object");
43
- });
44
-
45
- describe("theme configuration", () => {
46
- const config = getThemeConfig();
47
-
48
- it("should have correct basic settings", () => {
49
- expect(config.variant).toBe("light");
50
- expect(config.settings).toEqual({
51
- background: "#ffffff",
52
- foreground: "#000000",
53
- caret: "#000000",
54
- selection: "#d7d4f0",
55
- lineHighlight: "#cceeff44",
56
- gutterBackground: "var(--color-background)",
57
- gutterForeground: "var(--gray-10)",
58
- });
59
- });
9
+ const mounted: Array<{ host: HTMLDivElement; view: EditorView }> = [];
60
10
 
61
- describe("syntax highlighting", () => {
62
- const findStyle = (tag: Tag | readonly Tag[]) =>
63
- config.styles.find((s) =>
64
- Array.isArray(s.tag) ? s.tag.includes(tag as Tag) : s.tag === tag,
65
- );
11
+ function mountTheme(extension: Extension) {
12
+ const host = document.createElement("div");
13
+ document.body.append(host);
14
+ const root = host.attachShadow({ mode: "open" });
15
+ const parent = document.createElement("div");
16
+ root.append(parent);
17
+ const view = new EditorView({ extensions: extension, parent, root });
18
+ mounted.push({ host, view });
66
19
 
67
- it("should style comments", () => {
68
- expect(findStyle(t.comment)).toEqual({
69
- tag: t.comment,
70
- color: "#708090",
71
- });
72
- });
20
+ const style = root.querySelector("style");
21
+ if (!style) {
22
+ throw new Error("CodeMirror did not mount its theme styles");
23
+ }
24
+ return { css: style.textContent ?? "", view };
25
+ }
73
26
 
74
- it("should style strings", () => {
75
- const style = findStyle(t.string);
76
- expect(style).toBeDefined();
77
- expect(style?.color).toBe("#a11");
78
- expect(Array.isArray(style?.tag)).toBe(true);
79
- expect(style?.tag).toContain(t.string);
80
- expect(style?.tag).toContain(t.special(t.brace));
81
- });
82
-
83
- it("should style numbers and boolean values", () => {
84
- expect(findStyle(t.number)).toEqual({
85
- tag: t.number,
86
- color: "#164",
87
- });
88
- expect(findStyle(t.bool)).toEqual({
89
- tag: t.bool,
90
- color: "#219",
91
- });
92
- });
93
-
94
- it("should style keywords with emphasis", () => {
95
- expect(findStyle(t.keyword)).toEqual({
96
- tag: t.keyword,
97
- color: "#708",
98
- fontWeight: 500,
99
- });
100
- });
27
+ afterEach(() => {
28
+ for (const { host, view } of mounted.splice(0)) {
29
+ view.destroy();
30
+ host.remove();
31
+ }
32
+ });
101
33
 
102
- it("should style operators with emphasis", () => {
103
- expect(findStyle(t.operator)).toEqual({
104
- tag: t.operator,
105
- color: "#a2f",
106
- fontWeight: 500,
107
- });
108
- });
34
+ const themeCases = [
35
+ {
36
+ name: "light",
37
+ extension: lightTheme,
38
+ dark: false,
39
+ selectionAlias: "var(--cm-selection-background-light)",
40
+ reactiveColor: "var(--cm-reactive-reference-color-light)",
41
+ },
42
+ {
43
+ name: "dark",
44
+ extension: darkTheme,
45
+ dark: true,
46
+ selectionAlias: "var(--cm-selection-background-dark)",
47
+ reactiveColor: "var(--cm-reactive-reference-color-dark)",
48
+ },
49
+ ] as const;
109
50
 
110
- it("should style class and type names", () => {
111
- expect(findStyle(t.className)).toEqual({
112
- tag: t.className,
113
- color: "#00f",
114
- });
115
- expect(findStyle(t.typeName)).toEqual({
116
- tag: t.typeName,
117
- color: "#085",
118
- });
119
- });
51
+ describe.each(themeCases)("$name theme", (theme) => {
52
+ it("uses the production theme tokens", () => {
53
+ const { css, view } = mountTheme(theme.extension);
120
54
 
121
- it("should style function names", () => {
122
- const style = findStyle(t.function(t.variableName));
123
- expect(style).toBeDefined();
124
- expect(style?.color).toBe("#00c");
125
- expect(Array.isArray(style?.tag)).toBe(true);
126
- });
55
+ expect(view.state.facet(EditorView.darkTheme)).toBe(theme.dark);
56
+ expect(css).toContain(
57
+ `--cm-selection-background: ${theme.selectionAlias};`,
58
+ );
59
+ expect(css).toContain("background-color: var(--cm-selection-background);");
60
+ expect(css).toContain(`color: ${theme.reactiveColor};`);
61
+ expect(css).toContain(
62
+ "border-bottom: 2px solid var(--cm-reactive-reference-border-color);",
63
+ );
127
64
 
128
- it("should style property names", () => {
129
- const style = findStyle(t.propertyName);
130
- expect(style).toBeDefined();
131
- expect(style?.color).toBe("#05a");
132
- expect(Array.isArray(style?.tag)).toBe(true);
133
- });
134
- });
65
+ const hoverRule = css
66
+ .split("\n")
67
+ .find((rule) => rule.includes(".mo-cm-reactive-reference-hover"));
68
+ expect(hoverRule).toBeDefined();
69
+ expect(hoverRule).toContain("cursor: pointer;");
70
+ expect(hoverRule).not.toContain("border-bottom");
135
71
  });
136
72
  });
@@ -11,7 +11,7 @@ export const darkTheme = [
11
11
  background: "var(--cm-background)",
12
12
  foreground: "#abb2bf",
13
13
  caret: "#528bff",
14
- selection: "#3E4451",
14
+ selection: "var(--cm-selection-background)",
15
15
  lineHighlight: "#2c313c",
16
16
  gutterBackground: "var(--color-background)",
17
17
  gutterForeground: "var(--gray-10)",
@@ -36,15 +36,16 @@ export const darkTheme = [
36
36
  ],
37
37
  }),
38
38
  EditorView.theme({
39
+ "&": {
40
+ "--cm-selection-background": "var(--cm-selection-background-dark)",
41
+ },
39
42
  ".mo-cm-reactive-reference": {
40
43
  fontWeight: "400",
41
- color: "#2a7aa5",
42
- borderBottom: "2px solid #bad3de",
44
+ color: "var(--cm-reactive-reference-color-dark)",
45
+ borderBottom: "2px solid var(--cm-reactive-reference-border-color)",
43
46
  },
44
47
  ".mo-cm-reactive-reference-hover": {
45
48
  cursor: "pointer",
46
- borderBottomWidth: "3px",
47
- borderBottomColor: "#4a90a5",
48
49
  },
49
50
  }),
50
51
  ];
@@ -11,7 +11,7 @@ export const lightTheme = [
11
11
  background: "#ffffff",
12
12
  foreground: "#000000",
13
13
  caret: "#000000",
14
- selection: "#d7d4f0",
14
+ selection: "var(--cm-selection-background)",
15
15
  lineHighlight: "#cceeff44",
16
16
  gutterBackground: "var(--color-background)",
17
17
  gutterForeground: "var(--gray-10)",
@@ -45,14 +45,16 @@ export const lightTheme = [
45
45
  ],
46
46
  }),
47
47
  EditorView.theme({
48
+ "&": {
49
+ "--cm-selection-background": "var(--cm-selection-background-light)",
50
+ },
48
51
  ".mo-cm-reactive-reference": {
49
52
  fontWeight: "400",
50
- color: "#005f87",
51
- borderBottom: "2px solid #bad3de",
53
+ color: "var(--cm-reactive-reference-color-light)",
54
+ borderBottom: "2px solid var(--cm-reactive-reference-border-color)",
52
55
  },
53
56
  ".mo-cm-reactive-reference-hover": {
54
57
  cursor: "pointer",
55
- borderBottomWidth: "3px",
56
58
  },
57
59
  }),
58
60
  ];
@@ -66,6 +66,7 @@ test("default UserConfig - empty", () => {
66
66
  "display": {
67
67
  "cell_output": "below",
68
68
  "code_editor_font_size": 14,
69
+ "code_lens": true,
69
70
  "dataframes": "rich",
70
71
  "default_table_max_columns": 50,
71
72
  "default_table_page_size": 10,
@@ -139,6 +140,7 @@ test("default UserConfig - one level", () => {
139
140
  "display": {
140
141
  "cell_output": "below",
141
142
  "code_editor_font_size": 14,
143
+ "code_lens": true,
142
144
  "dataframes": "rich",
143
145
  "default_table_max_columns": 50,
144
146
  "default_table_page_size": 10,
@@ -154,6 +154,7 @@ export const UserConfigSchema = z
154
154
  }),
155
155
  locale: z.string().nullable().optional(),
156
156
  reference_highlighting: z.boolean().prefault(true),
157
+ code_lens: z.boolean().prefault(true),
157
158
  })
158
159
  .prefault({}),
159
160
  package_management: z
@@ -13,7 +13,6 @@ export interface ExperimentalFeatures {
13
13
  external_agents: boolean;
14
14
  debugger: boolean; // Live frame-watching debugger (gutter breakpoints + pdb)
15
15
  line_timing: boolean; // Green active-line highlight + per-line elapsed timer
16
- editor_code_lens: boolean; // Inline icons in cell editors linking datasources/buckets/caches to their panels
17
16
  // Add new feature flags here
18
17
  }
19
18
 
@@ -25,7 +24,6 @@ const defaultValues: ExperimentalFeatures = {
25
24
  external_agents: import.meta.env.DEV,
26
25
  debugger: false,
27
26
  line_timing: false,
28
- editor_code_lens: import.meta.env.DEV,
29
27
  };
30
28
 
31
29
  export function getFeatureFlag<T extends keyof ExperimentalFeatures>(
@@ -63,13 +63,36 @@ function getOutline(html: string): Outline | null {
63
63
  return { items };
64
64
  }
65
65
 
66
+ function toXPathStringLiteral(value: string): string {
67
+ if (!value.includes('"')) {
68
+ return `"${value}"`;
69
+ }
70
+ if (!value.includes("'")) {
71
+ return `'${value}'`;
72
+ }
73
+
74
+ const segments = value.split('"');
75
+ const literals: string[] = [];
76
+ for (const [index, segment] of segments.entries()) {
77
+ if (segment) {
78
+ literals.push(`"${segment}"`);
79
+ }
80
+ if (index < segments.length - 1) {
81
+ literals.push(`'"'`);
82
+ }
83
+ }
84
+ return `concat(${literals.join(", ")})`;
85
+ }
86
+
66
87
  export function headingToIdentifier(heading: Element): OutlineItem["by"] {
67
88
  const id = heading.id;
68
89
  if (id) {
69
90
  return { id };
70
91
  }
71
- const name = heading.textContent;
72
- return { path: `//${heading.tagName}[contains(., "${name}")]` };
92
+ const name = heading.textContent ?? "";
93
+ return {
94
+ path: `//${heading.tagName}[contains(., ${toXPathStringLiteral(name)})]`,
95
+ };
73
96
  }
74
97
 
75
98
  export function mergeOutlines(outlines: (Outline | null)[]): Outline {
@@ -393,11 +393,13 @@ const DEFAULT_HOT_KEY = {
393
393
  name: "Collapse all sections",
394
394
  group: "Editing",
395
395
  key: "Mod-Shift-\\",
396
+ additionalKeywords: ["fold", "headers"],
396
397
  },
397
398
  "global.expandAllSections": {
398
399
  name: "Expand all sections",
399
400
  group: "Editing",
400
401
  key: "Mod-Shift-/",
402
+ additionalKeywords: ["unfold", "headers"],
401
403
  },
402
404
  "global.toggleMinimap": {
403
405
  name: "Toggle Minimap",
package/src/core/mode.ts CHANGED
@@ -55,20 +55,22 @@ export async function runDuringPresentMode(
55
55
  fn: () => void | Promise<void>,
56
56
  ): Promise<void> {
57
57
  const state = store.get(viewStateAtom);
58
- if (state.mode === "present") {
58
+ if (state.mode !== "edit") {
59
59
  await fn();
60
60
  return;
61
61
  }
62
62
 
63
63
  store.set(viewStateAtom, { ...state, mode: "present" });
64
- // Wait 100ms to allow the page to render
65
- await new Promise((resolve) => setTimeout(resolve, 100));
66
- // Wait 2 frames
67
- await new Promise((resolve) => requestAnimationFrame(resolve));
68
- await new Promise((resolve) => requestAnimationFrame(resolve));
69
- await fn();
70
- store.set(viewStateAtom, { ...state, mode: "edit" });
71
- return undefined;
64
+ try {
65
+ // Wait 100ms to allow the page to render
66
+ await new Promise((resolve) => setTimeout(resolve, 100));
67
+ // Wait 2 frames
68
+ await new Promise((resolve) => requestAnimationFrame(resolve));
69
+ await new Promise((resolve) => requestAnimationFrame(resolve));
70
+ await fn();
71
+ } finally {
72
+ store.set(viewStateAtom, state);
73
+ }
72
74
  }
73
75
 
74
76
  export const viewStateAtom = atom<ViewState>({
@@ -105,7 +105,10 @@ describe("API", () => {
105
105
  });
106
106
 
107
107
  await expect(
108
- API.handleExportResponse({ data: "markdown", response }),
108
+ API.handleExportResponse(
109
+ { data: "markdown", response },
110
+ { defaultFilename: "download.md" },
111
+ ),
109
112
  ).resolves.toEqual({
110
113
  contents: "markdown",
111
114
  filename: "résumé.qmd",
@@ -113,27 +116,139 @@ describe("API", () => {
113
116
  });
114
117
  });
115
118
 
116
- it("rejects export responses without a filename", async () => {
119
+ it("parses quoted Content-Disposition filenames", async () => {
120
+ const response = new Response("html", {
121
+ headers: {
122
+ "Content-Disposition": 'inline; filename="notebook.html"',
123
+ "Content-Type": "text/html; charset=utf-8",
124
+ },
125
+ });
126
+
117
127
  await expect(
118
- API.handleExportResponse({
119
- data: "markdown",
120
- response: new Response(null, {
121
- headers: { "Content-Type": "text/plain" },
122
- }),
123
- }),
124
- ).rejects.toThrow("Export response is missing a filename");
128
+ API.handleExportResponse(
129
+ { data: "html", response },
130
+ { defaultFilename: "download.html" },
131
+ ),
132
+ ).resolves.toEqual({
133
+ contents: "html",
134
+ filename: "notebook.html",
135
+ mediaType: "text/html; charset=utf-8",
136
+ });
137
+ });
138
+
139
+ it("parses unquoted Content-Disposition filenames", async () => {
140
+ const response = new Response("pdf", {
141
+ headers: {
142
+ "Content-Disposition": "attachment; filename=notebook.pdf",
143
+ "Content-Type": "application/pdf",
144
+ },
145
+ });
146
+
147
+ await expect(
148
+ API.handleExportResponse(
149
+ { data: "pdf", response },
150
+ { defaultFilename: "download.pdf" },
151
+ ),
152
+ ).resolves.toEqual({
153
+ contents: "pdf",
154
+ filename: "notebook.pdf",
155
+ mediaType: "application/pdf",
156
+ });
157
+ });
158
+
159
+ it("prefers RFC 5987 filenames over quoted fallbacks", async () => {
160
+ const response = new Response("markdown", {
161
+ headers: {
162
+ "Content-Disposition":
163
+ "attachment; filename=\"wrong.md\"; filename*=UTF-8''notebook.md",
164
+ "Content-Type": "text/plain; charset=utf-8",
165
+ },
166
+ });
167
+
168
+ await expect(
169
+ API.handleExportResponse(
170
+ { data: "markdown", response },
171
+ { defaultFilename: "download.md" },
172
+ ),
173
+ ).resolves.toEqual({
174
+ contents: "markdown",
175
+ filename: "notebook.md",
176
+ mediaType: "text/plain; charset=utf-8",
177
+ });
178
+ });
179
+
180
+ it("falls back to a default filename when headers are missing", async () => {
181
+ const response = new Response("markdown", {
182
+ headers: {
183
+ "Content-Type": "text/plain; charset=utf-8",
184
+ },
185
+ });
186
+
187
+ await expect(
188
+ API.handleExportResponse(
189
+ { data: "markdown", response },
190
+ { defaultFilename: "download.md" },
191
+ ),
192
+ ).resolves.toEqual({
193
+ contents: "markdown",
194
+ filename: "download.md",
195
+ mediaType: "text/plain; charset=utf-8",
196
+ });
197
+ });
198
+
199
+ it("falls back to a default filename when Content-Disposition is unparsable", async () => {
200
+ const response = new Response("markdown", {
201
+ headers: {
202
+ "Content-Disposition": "attachment",
203
+ "Content-Type": "text/plain; charset=utf-8",
204
+ },
205
+ });
206
+
207
+ await expect(
208
+ API.handleExportResponse(
209
+ { data: "markdown", response },
210
+ { defaultFilename: "download.md" },
211
+ ),
212
+ ).resolves.toEqual({
213
+ contents: "markdown",
214
+ filename: "download.md",
215
+ mediaType: "text/plain; charset=utf-8",
216
+ });
217
+ });
218
+
219
+ it("falls back to a default filename when RFC 5987 encoding is malformed", async () => {
220
+ const response = new Response("markdown", {
221
+ headers: {
222
+ "Content-Disposition": "attachment; filename*=UTF-8''%E0%A4%A",
223
+ "Content-Type": "text/plain; charset=utf-8",
224
+ },
225
+ });
226
+
227
+ await expect(
228
+ API.handleExportResponse(
229
+ { data: "markdown", response },
230
+ { defaultFilename: "download.md" },
231
+ ),
232
+ ).resolves.toEqual({
233
+ contents: "markdown",
234
+ filename: "download.md",
235
+ mediaType: "text/plain; charset=utf-8",
236
+ });
125
237
  });
126
238
 
127
239
  it("rejects export responses without a media type", async () => {
128
240
  await expect(
129
- API.handleExportResponse({
130
- data: "markdown",
131
- response: new Response(null, {
132
- headers: {
133
- "Content-Disposition": "attachment; filename*=UTF-8''notebook.md",
134
- },
135
- }),
136
- }),
241
+ API.handleExportResponse(
242
+ {
243
+ data: "markdown",
244
+ response: new Response(null, {
245
+ headers: {
246
+ "Content-Disposition": "attachment; filename*=UTF-8''notebook.md",
247
+ },
248
+ }),
249
+ },
250
+ { defaultFilename: "download.md" },
251
+ ),
137
252
  ).rejects.toThrow("Export response is missing a media type");
138
253
  });
139
254
  });