@marimo-team/islands 0.25.3-dev14 → 0.25.3-dev140

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 (453) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-BAiSVaHj.js} +17 -15
  2. package/dist/{ErrorBoundary-S_edj6VQ.js → ErrorBoundary-CTJ1U1fx.js} +16 -16
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-BBHN92-F.js} +2 -2
  4. package/dist/any-language-editor-3E24SnCV.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-C0Rd64qh.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/assets/__vite-browser-external-BgQNn4dS.js +1 -0
  8. package/dist/assets/worker-FKe3RHT4.js +113 -0
  9. package/dist/{button-CvjkwdSk.js → button-BQRdqQi1.js} +199 -192
  10. package/dist/{capabilities-CQ5IjhzL.js → capabilities-CI4xUPsp.js} +2 -2
  11. package/dist/{chat-ui-DaHynk8g.js → chat-ui-CvB8Map3.js} +1142 -1141
  12. package/dist/{check-EDwSwP89.js → check-SpOMoCLP.js} +1 -1
  13. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-hJXey5p2.js} +1 -1
  14. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-JMzTmmVW.js} +1 -1
  15. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BIyoHTXi.js} +1 -1
  16. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-CCzH2Uwa.js} +1 -1
  17. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  18. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-DB1kOPS6.js} +2 -2
  19. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CPaFL0OB.js} +2 -2
  20. package/dist/{code-visibility-BBdICLTP.js → code-visibility-C1jZfwPW.js} +6 -6
  21. package/dist/{common-7r9HNiTn.js → common-CNv_7gvI.js} +6766 -6590
  22. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-C1trdvVt.js} +3 -3
  23. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-CF93uliH.js} +1 -1
  24. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-BBB-_p9D.js} +1 -1
  25. package/dist/{data-grid-overlay-editor-Bz1FTAfW.js → data-grid-overlay-editor-t0t17DPI.js} +5 -5
  26. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  27. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BynCnbyt.js} +1 -1
  28. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-svOxikMa.js} +1 -1
  29. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-BVQSnPx0.js} +1 -1
  30. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-nR1J0BH5.js} +1 -1
  31. package/dist/{dialog-CEBFZNyc.js → dialog-DgeqyrEZ.js} +3 -3
  32. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  33. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-GWcGCcxE.js} +1277 -1229
  34. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-C9tq9-Qo.js} +2 -2
  35. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-BiaeB7Fi.js} +1 -1
  36. package/dist/{error-banner-D-WCIiMt.js → error-banner-D40pmfQQ.js} +83 -83
  37. package/dist/{error-entries-Bv6bZPF3.js → error-entries-CmFDQ9CA.js} +1018 -977
  38. package/dist/{feedback-button-BAheGhdl.js → feedback-button-JzaiyZZm.js} +23 -23
  39. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-f-sLm87k.js} +2 -2
  40. package/dist/{formats-27CW_UP4.js → formats-CCxaLPNs.js} +3 -3
  41. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  42. package/dist/{glide-data-editor-CAvrTnyB.js → glide-data-editor-B3eq-1SC.js} +370 -368
  43. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Dz_F4pAY.js} +16 -16
  44. package/dist/{globals-C03jB5fY.js → globals-BW8TOLYQ.js} +3 -3
  45. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  46. package/dist/{html-to-image-Bf8QMGi4.js → html-to-image-BrC8OJcj.js} +632 -595
  47. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-BHrWywBe.js} +1 -1
  48. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-Da4s0Reg.js} +1 -1
  49. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-DDf1GVLj.js} +1 -1
  50. package/dist/{label-BDKF3Afc.js → label-D9_-zSg-.js} +3 -3
  51. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  52. package/dist/{loader-BQOfjTIJ.js → loader-DOVB0Syl.js} +12 -12
  53. package/dist/main.js +7551 -7231
  54. package/dist/{mermaid-CHo49Yv-.js → mermaid-ClwTt7OD.js} +38 -38
  55. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  56. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-D60oMZtT.js} +1 -1
  57. package/dist/{number-overlay-editor-DRF7iVLO.js → number-overlay-editor-DoUnDbPE.js} +3 -3
  58. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  59. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  60. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-B9wFpMga.js} +2 -2
  61. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-BZwvMuBT.js} +1 -1
  62. package/dist/precisionRound-DhAzeRcm.js +49 -0
  63. package/dist/{process-output-BZg56CCb.js → process-output-Bw-vzyoy.js} +5 -5
  64. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  65. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-DqhZGsN2.js} +2 -2
  66. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  67. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CL5dYARV.js} +1 -1
  68. package/dist/{reveal-component-QSvE6jwa.js → reveal-component-ClNn1CDl.js} +14 -14
  69. package/dist/{select-BButFi-d.js → select-CkF_GBxO.js} +537 -458
  70. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-BSUCRd2x.js} +1 -1
  71. package/dist/{spec-BwarBpUJ.js → spec-DiSh_qJC.js} +41 -11
  72. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-B-1Hvyb0.js} +2 -2
  73. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH--HtiR4yB.js} +2 -2
  74. package/dist/style.css +1 -1
  75. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-Bdfhrd-2.js} +1 -1
  76. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BJIfxSz0.js} +2 -2
  77. package/dist/{swiper-component-DKntyn8p.js → swiper-component-BkCm-kLg.js} +2 -2
  78. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  79. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-Cs4EfFD3.js} +1 -1
  80. package/dist/{toDate-UtiwmnH2.js → toDate-DGvRHwTk.js} +19 -18
  81. package/dist/{toast-B9UMEi8u.js → toast-Btbve3Lm.js} +231 -213
  82. package/dist/{tooltip-BO1REbqs.js → tooltip-CJ4Rqasp.js} +327 -303
  83. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-CmGwRNId.js} +1482 -1433
  84. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-BmVgnMP5.js} +15 -14
  85. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-eCL_SdeG.js} +2 -2
  86. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Ch_AeGu6.js} +1 -1
  87. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-CSdWn6NP.js} +1 -1
  88. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-B5BX921H.js} +3 -3
  89. package/dist/{useTheme-CRnjkku0.js → useTheme-Bq9pi_vz.js} +6 -6
  90. package/dist/{vega-component-BeF9dfyO.js → vega-component-BgyZ-COt.js} +147 -147
  91. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  92. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  93. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-DRu0vRjO.js} +1 -1
  94. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-DzbDxwDY.js} +1 -1
  95. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CLWj6ZFQ.js} +2 -2
  96. package/package.json +14 -14
  97. package/src/__tests__/host-imports.test.ts +1 -0
  98. package/src/__tests__/module-entry.test.ts +32 -0
  99. package/src/__tests__/mount.test.ts +0 -20
  100. package/src/components/ai/__tests__/ai-model-dropdown.test.tsx +63 -0
  101. package/src/components/ai/ai-model-dropdown.tsx +11 -6
  102. package/src/components/app-config/ai-config.tsx +7 -2
  103. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  104. package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
  105. package/src/components/chat/acp/__tests__/state.test.ts +2 -2
  106. package/src/components/chat/acp/blocks.tsx +3 -2
  107. package/src/components/chat/acp/session-tabs.tsx +8 -3
  108. package/src/components/chat/chat-components.tsx +2 -0
  109. package/src/components/chat/chat-panel.tsx +9 -8
  110. package/src/components/chat/chat-utils.ts +4 -8
  111. package/src/components/data-table/TableTopBar.tsx +2 -0
  112. package/src/components/data-table/__tests__/chart-spec-model.test.ts +195 -195
  113. package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
  114. package/src/components/data-table/__tests__/columns.test.tsx +105 -0
  115. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  116. package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
  117. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  118. package/src/components/data-table/__tests__/export-actions.test.tsx +105 -0
  119. package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
  120. package/src/components/data-table/__tests__/search-highlight.test.tsx +109 -0
  121. package/src/components/data-table/__tests__/url-detector.test.tsx +74 -0
  122. package/src/components/data-table/cell-selection/feature.ts +2 -2
  123. package/src/components/data-table/cell-selection/types.ts +2 -2
  124. package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
  125. package/src/components/data-table/charts/__tests__/spec-snapshot.test.ts +20 -20
  126. package/src/components/data-table/charts/__tests__/spec.test.ts +78 -52
  127. package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
  128. package/src/components/data-table/charts/chart-spec/spec.ts +68 -43
  129. package/src/components/data-table/charts/charts.tsx +40 -30
  130. package/src/components/data-table/charts/storage.ts +16 -5
  131. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
  132. package/src/components/data-table/column-summary/chart-spec-model.tsx +23 -16
  133. package/src/components/data-table/column-summary/column-summary.tsx +2 -4
  134. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  135. package/src/components/data-table/columns.tsx +45 -46
  136. package/src/components/data-table/context-menu.tsx +36 -20
  137. package/src/components/data-table/data-table.tsx +13 -1
  138. package/src/components/data-table/date-popover.tsx +6 -4
  139. package/src/components/data-table/export-actions.tsx +76 -48
  140. package/src/components/data-table/range-focus/atoms.ts +4 -6
  141. package/src/components/data-table/range-focus/use-cell-range-selection.ts +65 -38
  142. package/src/components/data-table/range-focus/use-scroll-into-view.ts +21 -2
  143. package/src/components/data-table/renderers.tsx +16 -7
  144. package/src/components/data-table/row-viewer-panel/__tests__/row-viewer.test.tsx +24 -1
  145. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +4 -4
  146. package/src/components/data-table/search-highlight.tsx +79 -0
  147. package/src/components/data-table/types.ts +1 -0
  148. package/src/components/data-table/url-detector.tsx +49 -9
  149. package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
  150. package/src/components/datasources/__tests__/column-preview.test.tsx +98 -5
  151. package/src/components/datasources/__tests__/datasources-interactions.test.tsx +181 -0
  152. package/src/components/datasources/__tests__/table-list.test.tsx +118 -0
  153. package/src/components/datasources/__tests__/utils.test.ts +57 -6
  154. package/src/components/datasources/column-preview.tsx +13 -4
  155. package/src/components/datasources/datasources.tsx +51 -39
  156. package/src/components/datasources/utils.ts +32 -23
  157. package/src/components/debugger/debugger-code.tsx +5 -0
  158. package/src/components/dependency-graph/dependency-graph-tree.tsx +8 -8
  159. package/src/components/dependency-graph/elements.ts +24 -21
  160. package/src/components/dependency-graph/panels.tsx +2 -1
  161. package/src/components/dependency-graph/utils/__tests__/layout.test.ts +18 -11
  162. package/src/components/editor/SortableCell.tsx +1 -3
  163. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  164. package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
  165. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
  166. package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
  167. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  168. package/src/components/editor/ai/ai-completion-editor.tsx +1 -0
  169. package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
  170. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  171. package/src/components/editor/cell/DeleteButton.tsx +1 -0
  172. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  173. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  174. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  175. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  176. package/src/components/editor/cell/collapse.tsx +5 -3
  177. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  178. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
  179. package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
  180. package/src/components/editor/chrome/panels/outline/__tests__/outline-keyboard.test.tsx +84 -0
  181. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +39 -14
  182. package/src/components/editor/chrome/panels/outline/useActiveOutline.tsx +3 -2
  183. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  184. package/src/components/editor/chrome/panels/secrets-panel.tsx +1 -0
  185. package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -1
  186. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +12 -18
  187. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +58 -1
  188. package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +1 -7
  189. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +12 -2
  190. package/src/components/editor/chrome/wrapper/storage.ts +1 -1
  191. package/src/components/editor/connections/components.tsx +1 -1
  192. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  193. package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
  194. package/src/components/editor/controls/command-palette.tsx +22 -10
  195. package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
  196. package/src/components/editor/dynamic-favicon.tsx +21 -40
  197. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
  198. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  199. package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
  200. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  201. package/src/components/editor/file-tree/file-explorer.tsx +45 -8
  202. package/src/components/editor/file-tree/file-name-input.tsx +1 -0
  203. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  204. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  205. package/src/components/editor/header/status.tsx +28 -5
  206. package/src/components/editor/links/__tests__/cell-link.test.tsx +146 -0
  207. package/src/components/editor/links/cell-link.tsx +14 -5
  208. package/src/components/editor/navigation/focus-utils.ts +4 -4
  209. package/src/components/editor/notebook-banner.tsx +1 -0
  210. package/src/components/editor/notebook-cell.tsx +11 -2
  211. package/src/components/editor/output/MarimoTracebackOutput.tsx +19 -17
  212. package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
  213. package/src/components/editor/output/ansi-reduce.ts +33 -20
  214. package/src/components/editor/output/console/ConsoleOutput.tsx +19 -25
  215. package/src/components/editor/package-alert.tsx +14 -7
  216. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +12 -6
  217. package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
  218. package/src/components/home/components.tsx +17 -31
  219. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  220. package/src/components/icons/large-spinner.tsx +3 -14
  221. package/src/components/icons/spinner.tsx +1 -1
  222. package/src/components/mcp/mcp-status-indicator.tsx +1 -1
  223. package/src/components/pages/__tests__/home-page.test.tsx +116 -0
  224. package/src/components/pages/home-page.tsx +164 -91
  225. package/src/components/scratchpad/scratchpad.test.tsx +95 -0
  226. package/src/components/scratchpad/scratchpad.tsx +16 -3
  227. package/src/components/slides/reveal-component.tsx +4 -2
  228. package/src/components/slides/slide-cell-view.tsx +1 -1
  229. package/src/components/slides/slide-form.tsx +1 -0
  230. package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
  231. package/src/components/tracing/tracing.tsx +45 -41
  232. package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
  233. package/src/components/ui/__tests__/range-slider.test.tsx +165 -0
  234. package/src/components/ui/alert-dialog.tsx +1 -1
  235. package/src/components/ui/draggable-popover.tsx +24 -2
  236. package/src/components/ui/range-slider.tsx +22 -9
  237. package/src/core/ai/context/providers/datasource.ts +6 -2
  238. package/src/core/ai/tools/base.ts +16 -7
  239. package/src/core/ai/tools/edit-notebook-tool.ts +47 -45
  240. package/src/core/ai/tools/registry.ts +13 -8
  241. package/src/core/ai/tools/sample-tool.ts +6 -7
  242. package/src/core/attachments/state.ts +5 -8
  243. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -0
  244. package/src/core/cells/__tests__/cell.test.ts +39 -0
  245. package/src/core/cells/__tests__/cells.test.ts +3 -1
  246. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
  247. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  248. package/src/core/cells/__tests__/session.test.ts +117 -36
  249. package/src/core/cells/cell.ts +14 -3
  250. package/src/core/cells/cells.ts +3 -1
  251. package/src/core/cells/pending-delete-service.ts +2 -6
  252. package/src/core/cells/session.ts +1 -1
  253. package/src/core/cells/types.ts +10 -1
  254. package/src/core/codemirror/__tests__/setup.test.ts +29 -29
  255. package/src/core/codemirror/cells/__tests__/extensions.test.ts +15 -0
  256. package/src/core/codemirror/cells/extensions.ts +7 -3
  257. package/src/core/codemirror/keymaps/vim.ts +2 -6
  258. package/src/core/codemirror/language/__tests__/sql.test.ts +22 -20
  259. package/src/core/codemirror/language/extension.ts +8 -8
  260. package/src/core/codemirror/language/languages/markdown.ts +8 -6
  261. package/src/core/codemirror/language/languages/python.ts +16 -10
  262. package/src/core/codemirror/language/languages/sql/sql.ts +9 -7
  263. package/src/core/codemirror/language/panel/panel.tsx +1 -0
  264. package/src/core/codemirror/language/types.ts +7 -7
  265. package/src/core/codemirror/rtc/extension.ts +24 -12
  266. package/src/core/codemirror/rtc/loro/awareness.ts +34 -19
  267. package/src/core/codemirror/rtc/loro/sync.ts +1 -0
  268. package/src/core/codemirror/utils.ts +1 -0
  269. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  270. package/src/core/datasets/data-source-discovery.ts +4 -6
  271. package/src/core/document/__tests__/codemirror.test.ts +485 -0
  272. package/src/core/document/__tests__/draft.test.ts +322 -0
  273. package/src/core/document/__tests__/fixtures.ts +92 -0
  274. package/src/core/document/__tests__/hooks.test.tsx +386 -0
  275. package/src/core/document/__tests__/lifecycle-scenarios.test.ts +61 -0
  276. package/src/core/document/__tests__/model.test.ts +191 -0
  277. package/src/core/document/__tests__/protocol-fixtures.ts +38 -0
  278. package/src/core/document/__tests__/recovery-scenarios.test.ts +230 -0
  279. package/src/core/document/__tests__/scenario-fixture.ts +43 -0
  280. package/src/core/document/__tests__/scenarios.test.ts +205 -0
  281. package/src/core/document/__tests__/state.test.ts +438 -0
  282. package/src/core/document/__tests__/submission.test.ts +203 -0
  283. package/src/core/document/__tests__/test-host.test.ts +92 -0
  284. package/src/core/document/__tests__/test-host.ts +247 -0
  285. package/src/core/document/__tests__/test-notebook.ts +149 -0
  286. package/src/core/document/__tests__/user.ts +61 -0
  287. package/src/core/document/codemirror.ts +311 -0
  288. package/src/core/document/draft.ts +472 -0
  289. package/src/core/document/hooks.tsx +118 -0
  290. package/src/core/document/index.ts +15 -0
  291. package/src/core/document/model.ts +55 -0
  292. package/src/core/document/select.ts +41 -0
  293. package/src/core/document/state.ts +374 -0
  294. package/src/core/document/sync.ts +369 -0
  295. package/src/core/host/AGENTS.md +8 -2
  296. package/src/core/host/__tests__/attachments.test.ts +56 -0
  297. package/src/core/host/__tests__/document.test.ts +126 -0
  298. package/src/core/host/__tests__/fixtures.ts +71 -0
  299. package/src/core/host/__tests__/replica.test.ts +550 -0
  300. package/src/core/host/__tests__/static.test.ts +169 -0
  301. package/src/core/host/__tests__/wasm.test.ts +163 -0
  302. package/src/core/host/backend.ts +19 -0
  303. package/src/core/host/connection.ts +1 -40
  304. package/src/core/host/index.ts +14 -0
  305. package/src/core/host/replica.ts +286 -0
  306. package/src/core/host/resources/attachments.ts +22 -0
  307. package/src/core/host/resources/document.ts +101 -0
  308. package/src/core/host/static.ts +72 -0
  309. package/src/core/host/wasm.ts +107 -0
  310. package/src/core/hotkeys/actions.ts +2 -4
  311. package/src/core/islands/__tests__/bootstrap.test.ts +16 -42
  312. package/src/core/islands/__tests__/bridge.test.ts +31 -26
  313. package/src/core/islands/bootstrap.ts +22 -52
  314. package/src/core/islands/bridge.ts +31 -20
  315. package/src/core/islands/components/__tests__/IslandControls.test.tsx +7 -5
  316. package/src/core/islands/worker/__tests__/controller.test.ts +62 -4
  317. package/src/core/islands/worker/controller.ts +4 -10
  318. package/src/core/islands/worker/worker.tsx +33 -33
  319. package/src/core/kernel/__tests__/RuntimeState.test.ts +1 -1
  320. package/src/core/network/connection.ts +4 -2
  321. package/src/core/network/useConnectionNotice.ts +8 -2
  322. package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
  323. package/src/core/runtime/__tests__/runtime.test.ts +32 -58
  324. package/src/core/runtime/runtime.ts +38 -42
  325. package/src/core/runtime/types.ts +0 -2
  326. package/src/core/state/__tests__/jotai.test.tsx +108 -0
  327. package/src/core/state/jotai.ts +1 -1
  328. package/src/core/static/__tests__/files.test.ts +38 -10
  329. package/src/core/static/files.ts +2 -0
  330. package/src/core/wasm/__tests__/bridge.test.ts +42 -1
  331. package/src/core/wasm/__tests__/worker.test.ts +182 -0
  332. package/src/core/wasm/bridge.ts +40 -21
  333. package/src/core/wasm/rpc.ts +0 -1
  334. package/src/core/wasm/store.ts +3 -3
  335. package/src/core/wasm/worker/bootstrap.ts +55 -51
  336. package/src/core/wasm/worker/getMarimoWheel.ts +1 -1
  337. package/src/core/wasm/worker/types.ts +23 -14
  338. package/src/core/wasm/worker/worker.ts +77 -36
  339. package/src/core/websocket/__tests__/backend.test.ts +228 -0
  340. package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
  341. package/src/core/websocket/__tests__/useNotebookReplica.hook.test.tsx +141 -0
  342. package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +154 -428
  343. package/src/core/websocket/backend.ts +130 -0
  344. package/src/core/websocket/connection-state.ts +1 -11
  345. package/src/core/websocket/state.ts +7 -0
  346. package/src/core/websocket/transports/__tests__/notebook.test.ts +405 -0
  347. package/src/core/websocket/transports/notebook.ts +159 -0
  348. package/src/core/websocket/types.ts +2 -2
  349. package/src/core/websocket/useNotebookReplica.ts +69 -0
  350. package/src/core/websocket/useNotebookSession.tsx +18 -25
  351. package/src/css/globals.css +5 -0
  352. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  353. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  354. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  355. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  356. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  357. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  358. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  359. package/src/hooks/useAsyncData.ts +3 -3
  360. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  361. package/src/hooks/useDebounce.ts +6 -10
  362. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  363. package/src/hooks/useDelayElapsed.ts +14 -10
  364. package/src/hooks/useElapsedTime.ts +3 -3
  365. package/src/hooks/useEventListener.ts +1 -2
  366. package/src/hooks/useInputHistory.ts +4 -7
  367. package/src/hooks/useInternalStateWithSync.ts +6 -6
  368. package/src/hooks/useResizeObserver.ts +4 -7
  369. package/src/hooks/useScript.ts +67 -70
  370. package/src/hooks/useStateForKey.ts +93 -0
  371. package/src/mount.tsx +0 -4
  372. package/src/plugins/__tests__/plugin-object-schemas.test.ts +104 -0
  373. package/src/plugins/core/registerReactComponent.tsx +1 -1
  374. package/src/plugins/impl/CodeEditorPlugin.tsx +1 -1
  375. package/src/plugins/impl/DataEditorPlugin.tsx +1 -1
  376. package/src/plugins/impl/DataTablePlugin.tsx +29 -33
  377. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  378. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  379. package/src/plugins/impl/FormPlugin.tsx +1 -1
  380. package/src/plugins/impl/RangeSliderPlugin.tsx +16 -2
  381. package/src/plugins/impl/RefreshPlugin.tsx +1 -0
  382. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  383. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +117 -1
  384. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  385. package/src/plugins/impl/__tests__/RangeSliderPlugin.test.tsx +109 -0
  386. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  387. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  388. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  389. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  390. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  391. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  392. package/src/plugins/impl/chat/chat-ui.tsx +14 -11
  393. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  394. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  395. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  396. package/src/plugins/impl/data-explorer/ConnectedDataExplorerComponent.tsx +1 -1
  397. package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
  398. package/src/plugins/impl/data-explorer/state/reducer.ts +1 -1
  399. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -2
  400. package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
  401. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +66 -66
  402. package/src/plugins/impl/data-frames/panel.tsx +43 -16
  403. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  404. package/src/plugins/impl/panel/PanelPlugin.tsx +16 -17
  405. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  406. package/src/plugins/impl/panel/utils.ts +17 -0
  407. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +2 -5
  408. package/src/plugins/impl/plotly/selection.ts +25 -19
  409. package/src/plugins/impl/vega/VegaPlugin.tsx +2 -3
  410. package/src/plugins/impl/vega/make-selectable.ts +4 -7
  411. package/src/plugins/layout/RoutesPlugin.tsx +1 -1
  412. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  413. package/src/stories/button.stories.tsx +1 -0
  414. package/src/stories/editor.stories.tsx +24 -17
  415. package/src/stories/form-wrapper.stories.tsx +2 -2
  416. package/src/stories/select.stories.tsx +2 -2
  417. package/src/stories/switchable-multi-select.stories.tsx +1 -1
  418. package/src/utils/__tests__/arrays.test.ts +35 -0
  419. package/src/utils/__tests__/batch-requests.test.ts +3 -3
  420. package/src/utils/__tests__/numbers.test.ts +105 -1
  421. package/src/utils/__tests__/objects.test.ts +21 -0
  422. package/src/utils/arrays.ts +10 -0
  423. package/src/utils/blob.ts +1 -1
  424. package/src/utils/numbers.ts +79 -18
  425. package/src/utils/objects.ts +3 -7
  426. package/src/utils/once.ts +19 -19
  427. package/src/utils/tracer.ts +9 -5
  428. package/src/utils/zod-utils.ts +1 -3
  429. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  430. package/dist/assets/__vite-browser-external-BXYkiFsk.js +0 -1
  431. package/dist/assets/worker-HPY9fbhz.js +0 -114
  432. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  433. package/dist/precisionRound-DMIVQrt2.js +0 -97
  434. package/src/core/host/__tests__/events.test.ts +0 -46
  435. package/src/core/host/__tests__/notebook-client.test.ts +0 -191
  436. package/src/core/host/__tests__/protocol.test.ts +0 -214
  437. package/src/core/host/events.ts +0 -60
  438. package/src/core/host/notebook-client.ts +0 -226
  439. package/src/core/host/protocol.ts +0 -1943
  440. package/src/core/state/__tests__/jotai.test.ts +0 -25
  441. package/src/core/wasm/worker/message-buffer.ts +0 -33
  442. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  443. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +0 -140
  444. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  445. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -335
  446. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  447. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  448. package/src/core/websocket/transports/basic.ts +0 -102
  449. package/src/core/websocket/transports/create.ts +0 -31
  450. package/src/core/websocket/transports/sse.ts +0 -308
  451. package/src/core/websocket/transports/transport.ts +0 -50
  452. package/src/core/websocket/transports/ws.ts +0 -98
  453. package/src/core/websocket/useNotebookClient.ts +0 -80
@@ -13,7 +13,9 @@ describe("RuntimeManager.getWsURL", () => {
13
13
  });
14
14
  const sessionId = "1234" as SessionId;
15
15
  const url = runtime.getWsURL(sessionId);
16
- expect(url.toString()).toBe("wss://marimo.app/ws?session_id=1234");
16
+ expect(url.toString()).toBe(
17
+ "wss://marimo.app/ws?query_params=&session_id=1234",
18
+ );
17
19
  expect(url.searchParams.get(KnownQueryParams.sessionId)).toBe(sessionId);
18
20
  });
19
21
 
@@ -24,7 +26,9 @@ describe("RuntimeManager.getWsURL", () => {
24
26
  });
25
27
  const sessionId = "1234" as SessionId;
26
28
  const url = runtime.getWsURL(sessionId);
27
- expect(url.toString()).toBe("ws://marimo.app/ws?session_id=1234");
29
+ expect(url.toString()).toBe(
30
+ "ws://marimo.app/ws?query_params=&session_id=1234",
31
+ );
28
32
  expect(url.searchParams.get(KnownQueryParams.sessionId)).toBe(sessionId);
29
33
  });
30
34
 
@@ -35,7 +39,9 @@ describe("RuntimeManager.getWsURL", () => {
35
39
  });
36
40
  const sessionId = "1234" as SessionId;
37
41
  const url = runtime.getWsURL(sessionId);
38
- expect(url.toString()).toBe("ws://marimo.app/nested/ws?session_id=1234");
42
+ expect(url.toString()).toBe(
43
+ "ws://marimo.app/nested/ws?query_params=&session_id=1234",
44
+ );
39
45
  expect(url.searchParams.get(KnownQueryParams.sessionId)).toBe(sessionId);
40
46
  });
41
47
 
@@ -48,7 +54,7 @@ describe("RuntimeManager.getWsURL", () => {
48
54
  const sessionId = "1234" as SessionId;
49
55
  const url = runtime.getWsURL(sessionId);
50
56
  expect(url.toString()).toBe(
51
- "ws://marimo.app/nested/ws?foo=bar&file=test.py&session_id=1234",
57
+ "ws://marimo.app/nested/ws?foo=bar&file=test.py&query_params=foo%3Dbar%26file%3Dtest.py&session_id=1234",
52
58
  );
53
59
  expect(url.searchParams.get(KnownQueryParams.sessionId)).toBe(sessionId);
54
60
  });
@@ -62,11 +68,75 @@ describe("RuntimeManager.getWsURL", () => {
62
68
  const sessionId = "1234" as SessionId;
63
69
  const url = runtime.getWsURL(sessionId);
64
70
  expect(url.toString()).toBe(
65
- "ws://marimo.app/ws?file=test.py&search=test&other=last&session_id=1234",
71
+ "ws://marimo.app/ws?file=test.py&query_params=file%3Dtest.py%26search%3Dtest%26other%3Dlast&session_id=1234",
66
72
  );
67
73
  expect(url.searchParams.get("file")).toBe("test.py");
68
- expect(url.searchParams.get("search")).toBe("test");
69
- expect(url.searchParams.get("other")).toBe("last");
74
+ expect(url.searchParams.get("query_params")).toBe(
75
+ "file=test.py&search=test&other=last",
76
+ );
70
77
  expect(url.searchParams.get(KnownQueryParams.sessionId)).toBe(sessionId);
71
78
  });
72
79
  });
80
+
81
+ describe.each(["getWsURL", "getSseURL"] as const)(
82
+ "RuntimeManager.%s",
83
+ (method) => {
84
+ it("preserves runtime parameters and repeated page values", () => {
85
+ window.history.pushState({}, "", "/?tag=page-one&tag=page-two");
86
+ const runtime = new RuntimeManager(
87
+ {
88
+ url: `${window.location.origin}/nested?proxy_token=secret&tag=base&base=one&base=two`,
89
+ lazy: true,
90
+ },
91
+ true,
92
+ );
93
+ const url = runtime[method]("session" as SessionId);
94
+ expect([...url.searchParams]).toEqual([
95
+ ["proxy_token", "secret"],
96
+ ["tag", "base"],
97
+ ["base", "one"],
98
+ ["base", "two"],
99
+ [
100
+ "query_params",
101
+ "proxy_token=secret&base=one&base=two&tag=page-one&tag=page-two",
102
+ ],
103
+ ["session_id", "session"],
104
+ ]);
105
+ });
106
+
107
+ it("keeps notebook parameters out of the stream options", () => {
108
+ const notebookParams = new URLSearchParams([
109
+ ["types", "scatter"],
110
+ ["cursor", "page-2"],
111
+ ["run_id", "experiment"],
112
+ ["query_params", "nested=value&other=1"],
113
+ ["tag", "one"],
114
+ ["tag", "two"],
115
+ ["empty", ""],
116
+ ["text", "a+b & café"],
117
+ ["file", "test.py"],
118
+ ["kiosk", "true"],
119
+ ]);
120
+ window.history.pushState(
121
+ {},
122
+ "",
123
+ `/?${notebookParams}&session_id=page-session`,
124
+ );
125
+ const runtime = new RuntimeManager(
126
+ { url: window.location.origin, lazy: true },
127
+ true,
128
+ );
129
+ const url = runtime[method]("actual-session" as SessionId);
130
+
131
+ expect(Object.fromEntries(url.searchParams)).toEqual({
132
+ file: "test.py",
133
+ kiosk: "true",
134
+ session_id: "actual-session",
135
+ query_params: notebookParams.toString(),
136
+ });
137
+ expect([
138
+ ...new URLSearchParams(url.searchParams.get("query_params") ?? ""),
139
+ ]).toEqual([...notebookParams]);
140
+ });
141
+ },
142
+ );
@@ -2,11 +2,7 @@
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { Mocks } from "@/__mocks__/common";
5
- import {
6
- generateSessionId,
7
- isSessionId,
8
- type SessionId,
9
- } from "@/core/kernel/session";
5
+ import type { SessionId } from "@/core/kernel/session";
10
6
  import { Logger } from "@/utils/Logger";
11
7
  import { RuntimeManager } from "../runtime";
12
8
  import type { RuntimeConfig } from "../types";
@@ -56,38 +52,6 @@ describe("RuntimeManager", () => {
56
52
  });
57
53
 
58
54
  describe("getWsURL", () => {
59
- it("uses host notebook events with the existing session and auth parameters", () => {
60
- const original = window.location.href;
61
- window.history.replaceState({}, "", "/?types=run");
62
- try {
63
- const runtime = new RuntimeManager({
64
- ...mockConfig,
65
- url: "https://example.com/nested?file=notebook.py",
66
- notebookId: "nb_123",
67
- });
68
- const sessionId = "s_123456";
69
- if (!isSessionId(sessionId)) {
70
- throw new Error("Invalid test session ID");
71
- }
72
- const url = runtime.getWsURL(sessionId);
73
- expect({
74
- endpoint: `${url.origin}${url.pathname}`,
75
- params: Object.fromEntries(url.searchParams),
76
- }).toEqual({
77
- endpoint:
78
- "wss://example.com/nested/api/marimo/v1/notebooks/nb_123/events/ws",
79
- params: {
80
- file: "notebook.py",
81
- types: "marimo",
82
- session_id: sessionId,
83
- access_token: "test-token",
84
- },
85
- });
86
- } finally {
87
- window.history.replaceState({}, "", original);
88
- }
89
- });
90
-
91
55
  it("should return WebSocket URL with session ID for https", () => {
92
56
  const runtime = new RuntimeManager(mockConfig);
93
57
  const sessionId = "1234" as SessionId;
@@ -256,22 +220,24 @@ describe("RuntimeManager", () => {
256
220
  });
257
221
  });
258
222
 
259
- describe("getSseURL", () => {
260
- it.each([undefined, "nb_123"])(
261
- "uses the legacy session stream when notebookId is %s",
262
- (notebookId) => {
263
- const runtime = new RuntimeManager({
264
- ...mockConfig,
265
- url: "https://example.com/nested?file=notebook.py",
266
- notebookId,
267
- });
268
- const sessionId = generateSessionId();
269
- expect(runtime.getSseURL(sessionId).toString()).toBe(
270
- `https://example.com/nested/sse?file=notebook.py&session_id=${sessionId}`,
271
- );
272
- },
273
- );
223
+ describe("getNotebookURL", () => {
224
+ it("addresses the notebook below the host API without the auth token", () => {
225
+ const runtime = new RuntimeManager(
226
+ {
227
+ url: "https://sandbox.example.com/base",
228
+ lazy: true,
229
+ authToken: "my-secret-token",
230
+ },
231
+ true,
232
+ );
233
+ const url = runtime.getNotebookURL("n/1");
274
234
 
235
+ expect(url.pathname).toBe("/base/api/marimo/v1/notebooks/n%2F1");
236
+ expect(url.toString()).not.toContain("my-secret-token");
237
+ });
238
+ });
239
+
240
+ describe("getSseURL", () => {
275
241
  it("should never put the auth token in the URL, even cross-origin", () => {
276
242
  // SSE requests can send headers, so auth travels in the
277
243
  // Authorization header; a token in the URL would leak into
@@ -300,7 +266,8 @@ describe("RuntimeManager", () => {
300
266
  const url = runtime.getSseURL("s_123" as SessionId);
301
267
 
302
268
  expect(url.searchParams.get("access_token")).toBeNull();
303
- expect(url.searchParams.get("foo")).toBe("bar");
269
+ expect(url.searchParams.get("query_params")).toBe("foo=bar");
270
+ expect(url.toString()).not.toContain("page-token");
304
271
  } finally {
305
272
  window.history.pushState({}, "", original);
306
273
  }
@@ -720,9 +687,16 @@ describe("RuntimeManager", () => {
720
687
  expect(httpUrl.searchParams.get("base_param")).toBe("existing");
721
688
 
722
689
  // Should preserve all window-level query params (including custom ones)
723
- expect(wsUrl.searchParams.get("custom_param")).toBe("value123");
724
- expect(wsUrl.searchParams.get("user_token")).toBe("abc");
725
- expect(wsUrl.searchParams.get("theme")).toBe("dark");
690
+ expect(
691
+ Object.fromEntries(
692
+ new URLSearchParams(wsUrl.searchParams.get("query_params") ?? ""),
693
+ ),
694
+ ).toEqual({
695
+ base_param: "existing",
696
+ custom_param: "value123",
697
+ user_token: "abc",
698
+ theme: "dark",
699
+ });
726
700
  expect(httpUrl.searchParams.get("custom_param")).toBe("value123");
727
701
  expect(httpUrl.searchParams.get("user_token")).toBe("abc");
728
702
  expect(httpUrl.searchParams.get("theme")).toBe("dark");
@@ -781,7 +755,7 @@ describe("RuntimeManager", () => {
781
755
  );
782
756
  });
783
757
 
784
- it("should throw when creating WebSocket URLs from blob URLs", () => {
758
+ it("warns when creating WebSocket URLs from blob URLs", () => {
785
759
  const runtime = new RuntimeManager({
786
760
  url: "blob:https://example.com/12345678-1234-1234-1234-123456789abc",
787
761
  lazy: true,
@@ -789,7 +763,7 @@ describe("RuntimeManager", () => {
789
763
  const sessionId = "test" as SessionId;
790
764
 
791
765
  expect(runtime.getWsURL(sessionId).toString()).toMatchInlineSnapshot(
792
- `"blob:https://example.com/12345678-1234-1234-1234-123456789abc?session_id=test"`,
766
+ `"blob:https://example.com/12345678-1234-1234-1234-123456789abc?query_params=&session_id=test"`,
793
767
  );
794
768
  expect(Logger.warn).toHaveBeenCalledOnce();
795
769
  });
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { NotebookId } from "@marimo-team/client/protocol";
3
4
  import { Deferred } from "@/utils/Deferred";
4
5
  import { Logger } from "@/utils/Logger";
5
6
  import { KnownQueryParams } from "../constants";
@@ -9,7 +10,7 @@ import { isStaticNotebook } from "../static/static-state";
9
10
  import { isWasm } from "../wasm/utils";
10
11
  import type { RuntimeConfig } from "./types";
11
12
 
12
- const HOST_EVENT_TYPES = ["marimo"];
13
+ const knownQueryParams = new Set(Object.values(KnownQueryParams));
13
14
 
14
15
  export class RuntimeManager {
15
16
  private initialHealthyCheck = new Deferred<void>();
@@ -73,10 +74,7 @@ export class RuntimeManager {
73
74
  }
74
75
 
75
76
  for (const [key, value] of currentParams.entries()) {
76
- if (
77
- restrictToKnownQueryParams &&
78
- !Object.values(KnownQueryParams).includes(key)
79
- ) {
77
+ if (restrictToKnownQueryParams && !knownQueryParams.has(key)) {
80
78
  continue;
81
79
  }
82
80
  baseUrl.searchParams.set(key, value);
@@ -131,43 +129,32 @@ export class RuntimeManager {
131
129
  return asWsUrl(this.appendCrossOriginAuth(url).toString());
132
130
  }
133
131
 
134
- /**
135
- * Search params for a session connection: the base URL's params, merged
136
- * with the current page's, plus the session id.
137
- */
138
- private getSessionSearchParams(sessionId: SessionId): URLSearchParams {
139
- const baseUrl = new URL(this.config.url);
140
- const searchParams = new URLSearchParams(baseUrl.search);
141
-
142
- // Merge in current page's query parameters
143
- const currentParams = new URLSearchParams(window.location.search);
144
- currentParams.forEach((value, key) => {
145
- // Don't override base URL params
146
- if (!searchParams.has(key)) {
147
- searchParams.set(key, value);
148
- }
149
- });
150
-
151
- searchParams.set(KnownQueryParams.sessionId, sessionId);
152
- return searchParams;
132
+ /** Keep notebook page parameters separate from connection options. */
133
+ private getSessionURL(path: string, sessionId: SessionId): URL {
134
+ const url = this.formatHttpURL({ path });
135
+ const notebookParams = new URLSearchParams(this.httpURL.search);
136
+ const pageParams = new URLSearchParams(window.location.search);
137
+ for (const key of pageParams.keys()) {
138
+ notebookParams.delete(key);
139
+ }
140
+ for (const [key, value] of pageParams) {
141
+ notebookParams.append(key, value);
142
+ }
143
+ // These parameters are internal and are not exposed by mo.query_params().
144
+ notebookParams.delete(KnownQueryParams.accessToken);
145
+ notebookParams.delete("refresh_token");
146
+ notebookParams.delete(KnownQueryParams.sessionId);
147
+ url.searchParams.set("query_params", notebookParams.toString());
148
+ url.searchParams.set(KnownQueryParams.sessionId, sessionId);
149
+ return url;
153
150
  }
154
151
 
155
152
  /**
156
153
  * The WebSocket URL of the runtime.
157
154
  */
158
155
  public getWsURL(sessionId: SessionId): URL {
159
- const searchParams = this.getSessionSearchParams(sessionId);
160
- // TODO(MO-8039): Rediscover hosts and resolve notebooks on reconnect.
161
- const notebookId = this.config.notebookId;
162
- const path = notebookId
163
- ? `/api/marimo/v1/notebooks/${encodeURIComponent(notebookId)}/events/ws`
164
- : "/ws";
165
- const url = this.formatWsURL(path, searchParams);
166
- if (notebookId) {
167
- // TODO(MO-8034): Separate host options from notebook query params.
168
- url.searchParams.set("types", HOST_EVENT_TYPES.join(","));
169
- }
170
- return url;
156
+ const url = this.getSessionURL("/ws", sessionId);
157
+ return asWsUrl(this.appendCrossOriginAuth(url).toString());
171
158
  }
172
159
 
173
160
  /**
@@ -177,11 +164,7 @@ export class RuntimeManager {
177
164
  * (see `headers()`) instead of the URL.
178
165
  */
179
166
  public getSseURL(sessionId: SessionId): URL {
180
- const url = this.formatHttpURL({
181
- path: "/sse",
182
- searchParams: this.getSessionSearchParams(sessionId),
183
- restrictToKnownQueryParams: false,
184
- });
167
+ const url = this.getSessionURL("/sse", sessionId);
185
168
  // The page URL can still carry access_token (merged in from the
186
169
  // current page's params, before auth cleanup strips it); never
187
170
  // forward it — a token in the URL leaks into proxy/server logs.
@@ -189,11 +172,24 @@ export class RuntimeManager {
189
172
  return url;
190
173
  }
191
174
 
175
+ /**
176
+ * The host API URL of a notebook, below which its operations are served.
177
+ * Requests send auth in headers (see `headers()`), never in the URL.
178
+ */
179
+ public getNotebookURL(notebookId: NotebookId): URL {
180
+ const url = this.formatHttpURL({
181
+ path: `/api/marimo/v1/notebooks/${encodeURIComponent(notebookId)}`,
182
+ });
183
+ url.searchParams.delete(KnownQueryParams.accessToken);
184
+ return url;
185
+ }
186
+
192
187
  /**
193
188
  * The WebSocket Sync URL of the runtime, for real-time updates.
194
189
  */
195
190
  public getWsSyncURL(sessionId: SessionId): URL {
196
- return this.formatWsURL("/ws_sync", this.getSessionSearchParams(sessionId));
191
+ const url = this.getSessionURL("/ws_sync", sessionId);
192
+ return asWsUrl(this.appendCrossOriginAuth(url).toString());
197
193
  }
198
194
 
199
195
  /**
@@ -1,8 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  export interface RuntimeConfig {
4
- /** The host notebook for WebSocket events. SSE uses the legacy session stream. */
5
- readonly notebookId?: string;
6
4
  /**
7
5
  * The URL of the runtime server.
8
6
  */
@@ -0,0 +1,108 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { atom, Provider } from "jotai";
5
+ import type React from "react";
6
+ import { describe, expect, it, vi } from "vitest";
7
+ import { store, useJotaiEffect, waitFor } from "../jotai";
8
+
9
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
10
+ <Provider store={store}>{children}</Provider>
11
+ );
12
+
13
+ describe("waitFor function", () => {
14
+ it("should resolve when the atom satisfies the predicate", async () => {
15
+ const testAtom = atom(0);
16
+ store.set(testAtom, 10);
17
+
18
+ const result = await waitFor(testAtom, (value) => value === 10);
19
+ expect(result).toBe(10);
20
+ });
21
+
22
+ it("should resolve when the atom changes to satisfy the predicate", async () => {
23
+ const testAtom = atom(0);
24
+
25
+ setTimeout(() => store.set(testAtom, 15), 5);
26
+ setTimeout(() => store.set(testAtom, 20), 10);
27
+
28
+ const result = await waitFor(testAtom, (value) => value === 20);
29
+ expect(result).toBe(20);
30
+ });
31
+ });
32
+
33
+ describe("useJotaiEffect", () => {
34
+ it("calls the effect with the new and previous values", () => {
35
+ const testAtom = atom(0);
36
+ const effect = vi.fn();
37
+ renderHook(() => useJotaiEffect(testAtom, effect), { wrapper });
38
+
39
+ act(() => store.set(testAtom, 1));
40
+ act(() => store.set(testAtom, 2));
41
+
42
+ expect(effect.mock.calls).toEqual([
43
+ [1, 0],
44
+ [2, 1],
45
+ ]);
46
+ });
47
+
48
+ it("calls only the latest callback after a rerender", () => {
49
+ const testAtom = atom(0);
50
+ const first = vi.fn();
51
+ const second = vi.fn();
52
+ const { rerender } = renderHook(
53
+ ({ effect }) => useJotaiEffect(testAtom, effect),
54
+ { initialProps: { effect: first }, wrapper },
55
+ );
56
+
57
+ rerender({ effect: second });
58
+ act(() => store.set(testAtom, 1));
59
+
60
+ expect(first).not.toHaveBeenCalled();
61
+ expect(second).toHaveBeenCalledTimes(1);
62
+ });
63
+
64
+ it("subscribes once per rerender with an inline callback", () => {
65
+ const testAtom = atom(0);
66
+ const calls: number[] = [];
67
+ const { rerender } = renderHook(
68
+ () => useJotaiEffect(testAtom, (value) => calls.push(value)),
69
+ { wrapper },
70
+ );
71
+
72
+ rerender();
73
+ rerender();
74
+ act(() => store.set(testAtom, 1));
75
+
76
+ expect(calls).toEqual([1]);
77
+ });
78
+
79
+ it("stops listening to the previous atom when the atom changes", () => {
80
+ const firstAtom = atom(0);
81
+ const secondAtom = atom(0);
82
+ const effect = vi.fn();
83
+ const { rerender } = renderHook(
84
+ ({ target }) => useJotaiEffect(target, effect),
85
+ { initialProps: { target: firstAtom }, wrapper },
86
+ );
87
+
88
+ rerender({ target: secondAtom });
89
+ act(() => store.set(firstAtom, 1));
90
+ expect(effect).not.toHaveBeenCalled();
91
+
92
+ act(() => store.set(secondAtom, 1));
93
+ expect(effect).toHaveBeenCalledTimes(1);
94
+ });
95
+
96
+ it("unsubscribes on unmount", () => {
97
+ const testAtom = atom(0);
98
+ const effect = vi.fn();
99
+ const { unmount } = renderHook(() => useJotaiEffect(testAtom, effect), {
100
+ wrapper,
101
+ });
102
+
103
+ unmount();
104
+ act(() => store.set(testAtom, 1));
105
+
106
+ expect(effect).not.toHaveBeenCalled();
107
+ });
108
+ });
@@ -42,7 +42,7 @@ export function useJotaiEffect<T>(
42
42
  const store = useStore();
43
43
  useEffect(() => {
44
44
  let prevValue = store.get(atom);
45
- store.sub(atom, () => {
45
+ return store.sub(atom, () => {
46
46
  const value = store.get(atom);
47
47
  effect(value, prevValue);
48
48
  prevValue = value;
@@ -654,19 +654,47 @@ describe("error handling and edge cases", () => {
654
654
  unpatch();
655
655
  });
656
656
 
657
- it("should handle malformed URLs gracefully", async () => {
658
- const virtualFiles = {};
659
- const unpatch = patchFetch(virtualFiles);
657
+ it("forwards relative URLs and options to the original fetch", async () => {
658
+ const response = new Response("relative content");
659
+ const originalFetch = vi.spyOn(window, "fetch").mockResolvedValue(response);
660
+ const unpatch = patchFetch({});
661
+ const options = { headers: { Accept: "text/plain" } };
662
+
663
+ try {
664
+ expect(await window.fetch("relative-file.txt", options)).toBe(response);
665
+ expect(originalFetch).toHaveBeenCalledExactlyOnceWith(
666
+ "relative-file.txt",
667
+ options,
668
+ );
669
+ } finally {
670
+ unpatch();
671
+ originalFetch.mockRestore();
672
+ }
673
+ });
660
674
 
661
- // Mock Logger.error to avoid test output
675
+ it("preserves the original fetch rejection for malformed URLs", async () => {
676
+ const error = new TypeError("Invalid URL");
677
+ const originalFetch = vi.spyOn(window, "fetch").mockRejectedValue(error);
662
678
  const loggerSpy = vi
663
679
  .spyOn(console, "error")
664
680
  .mockImplementation(Functions.NOOP);
665
-
666
- // This should catch the error and fallback to original fetch
667
- await window.fetch("not-a-valid-url").catch(() => undefined);
668
-
669
- unpatch();
670
- loggerSpy.mockRestore();
681
+ const unpatch = patchFetch({});
682
+ const options = { headers: { Accept: "text/plain" } };
683
+
684
+ try {
685
+ await expect(window.fetch("http://[", options)).rejects.toBe(error);
686
+ expect(originalFetch).toHaveBeenCalledExactlyOnceWith(
687
+ "http://[",
688
+ options,
689
+ );
690
+ expect(loggerSpy).toHaveBeenCalledWith(
691
+ "Error parsing URL",
692
+ expect.any(TypeError),
693
+ );
694
+ } finally {
695
+ unpatch();
696
+ originalFetch.mockRestore();
697
+ loggerSpy.mockRestore();
698
+ }
671
699
  });
672
700
  });
@@ -125,6 +125,8 @@ function withoutLeadingDot(path: string): string {
125
125
  /**
126
126
  * Resolve a URL to a blob URL if it's a virtual file, for use with dynamic import().
127
127
  * Unlike fetch, import() can't be patched, so we need to convert data URLs to blob URLs.
128
+ * The caller owns a returned URL that differs from the input and must revoke it
129
+ * once no module consumers or their cleanup callbacks need it.
128
130
  *
129
131
  * @returns The original URL if not a virtual file, or a blob URL if it is
130
132
  */
@@ -11,6 +11,9 @@ const {
11
11
  mockSaveNotebook,
12
12
  mockBootstrap,
13
13
  rpcListeners,
14
+ mockNotebookInfo,
15
+ mockNotebookSubscribe,
16
+ mockNotebookUnsubscribe,
14
17
  } = vi.hoisted(() => ({
15
18
  mockBridge: vi.fn(),
16
19
  mockNotebookReadFile: vi.fn(),
@@ -18,6 +21,9 @@ const {
18
21
  mockSaveNotebook: vi.fn(),
19
22
  mockBootstrap: vi.fn(),
20
23
  rpcListeners: {} as Record<string, () => void>,
24
+ mockNotebookInfo: vi.fn(),
25
+ mockNotebookSubscribe: vi.fn(),
26
+ mockNotebookUnsubscribe: vi.fn(),
21
27
  }));
22
28
 
23
29
  // Mock browser globals before any imports
@@ -50,8 +56,10 @@ vi.mock("@/core/wasm/rpc", () => ({
50
56
  readFile: vi.fn(),
51
57
  readNotebook: mockReadNotebook,
52
58
  saveNotebook: mockSaveNotebook,
59
+ notebookInfo: mockNotebookInfo,
60
+ notebookSubscribe: mockNotebookSubscribe,
61
+ notebookUnsubscribe: mockNotebookUnsubscribe,
53
62
  },
54
- send: { consumerReady: vi.fn() },
55
63
  },
56
64
  addMessageListener: (event: string, cb: () => void) => {
57
65
  rpcListeners[event] = cb;
@@ -218,6 +226,39 @@ describe("PyodideBridge.sendSave", () => {
218
226
  });
219
227
  });
220
228
 
229
+ it("cancels a pending notebook subscription before its RPC response", async () => {
230
+ vi.clearAllMocks();
231
+ store.set(initialModeAtom, "read");
232
+ rpcListeners.initialized();
233
+ const pending = new Deferred<void>();
234
+ mockNotebookInfo.mockResolvedValue({ id: "n1" });
235
+ mockNotebookSubscribe.mockReturnValue(pending.promise);
236
+ mockNotebookUnsubscribe.mockResolvedValue(undefined);
237
+ const controller = new AbortController();
238
+ try {
239
+ const backend = await PyodideBridge.INSTANCE.createNotebookBackend();
240
+ const following = backend.transport(
241
+ { cursor: null, signal: controller.signal },
242
+ { open: vi.fn(), message: vi.fn() },
243
+ );
244
+ await vi.waitFor(() =>
245
+ expect(mockNotebookSubscribe).toHaveBeenCalledOnce(),
246
+ );
247
+ const [{ id }] = mockNotebookSubscribe.mock.calls[0];
248
+ controller.abort();
249
+ await following;
250
+ expect(mockNotebookUnsubscribe.mock.calls).toEqual([[id]]);
251
+
252
+ pending.resolve();
253
+ await new Promise((resolve) => setTimeout(resolve, 0));
254
+ expect(mockNotebookUnsubscribe.mock.calls).toEqual([[id]]);
255
+ } finally {
256
+ controller.abort();
257
+ pending.resolve();
258
+ store.set(initialModeAtom, undefined);
259
+ }
260
+ });
261
+
221
262
  describe("getWasmWorkerName", () => {
222
263
  afterEach(() => {
223
264
  delete (window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: boolean })