@marimo-team/frontend 0.25.3-dev4 → 0.25.3-dev40
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.
- package/dist/assets/CellStatus-BGIWF94J.js +1 -0
- package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-D9UK8o9A.js} +1 -1
- package/dist/assets/{ErrorBoundary-Gl5nCA8C.js → ErrorBoundary-hliG40Pk.js} +2 -2
- package/dist/assets/JsonOutput-9tmC-p6h.js +11 -0
- package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-BPYE76jR.js} +2 -2
- package/dist/assets/MarimoErrorOutput-BaHdbtck.js +10 -0
- package/dist/assets/{RenderHTML-BbOqmHy3.js → RenderHTML-B1FzB5NY.js} +1 -1
- package/dist/assets/{RunButton-CK3LhnP5.js → RunButton-DpGokdF1.js} +1 -1
- package/dist/assets/{add-cell-with-ai-8UBC0xwR.js → add-cell-with-ai-BYWG0iKO.js} +13 -13
- package/dist/assets/{add-connection-dialog-DXQnd6eB.js → add-connection-dialog-BWbUVPJk.js} +2 -2
- package/dist/assets/{agent-panel-DdkVL-Kx.js → agent-panel-DKNiQrAl.js} +5 -5
- package/dist/assets/ai-model-dropdown-CIg9eu47.js +13 -0
- package/dist/assets/{alert-dialog-BaPGzeOK.js → alert-dialog-C-sWmKva.js} +1 -1
- package/dist/assets/any-language-editor-DUMe1UCk.js +1 -0
- package/dist/assets/{app-config-button-BKEG4x9u.js → app-config-button-BvkL2eSY.js} +1 -1
- package/dist/assets/arrays-0R2reF2y.js +1 -0
- package/dist/assets/{blob-D5nfNHOo.js → blob-BQfLiwKr.js} +1 -1
- package/dist/assets/{button-COI-NGrx.js → button-D8gVKwe-.js} +1 -1
- package/dist/assets/{cache-panel-DTY1dqvi.js → cache-panel-Byolv2ht.js} +1 -1
- package/dist/assets/{capabilities-BJvf-MD4.js → capabilities-Pq1YNPlK.js} +1 -1
- package/dist/assets/{cell-editor-32lVhib7.js → cell-editor-BKKUv4d5.js} +9 -9
- package/dist/assets/{cell-link-LSth_8hT.js → cell-link-B_QkRkm-.js} +1 -1
- package/dist/assets/{cells-BRkCaSsh.js → cells-CdH98VSn.js} +31 -31
- package/dist/assets/{chat-display-Dnagy4Nv.js → chat-display-CNbRSebo.js} +1 -1
- package/dist/assets/{chat-panel-DN9vsbgS.js → chat-panel-HbEKDf0c.js} +2 -2
- package/dist/assets/{chat-ui-Df8Vcj6n.js → chat-ui-BwMev8Bf.js} +1 -1
- package/dist/assets/{circle-DXf-VWZG.js → circle-B6D_6D8w.js} +1 -1
- package/dist/assets/code-visibility-sQ37CBNo.js +1 -0
- package/dist/assets/coerce-DWjFeXOY.js +1 -0
- package/dist/assets/{column-preview-CHK_DNrs.js → column-preview-CsEBzIUE.js} +1 -1
- package/dist/assets/command-palette-BzZ86pUz.js +1 -0
- package/dist/assets/{common-Cu6CDbuw.js → common-S2v8dsEy.js} +1 -1
- package/dist/assets/{components-CGw16R_N.js → components-D-TnFWjo.js} +1 -1
- package/dist/assets/{components-CP_UoFtT.js → components-DrU8dOzy.js} +1 -1
- package/dist/assets/{components-CXfO-F47.js → components-lDm0qi2t.js} +1 -1
- package/dist/assets/config-BoBwH7CU.js +1 -0
- package/dist/assets/config-CYsbZpwa.js +104 -0
- package/dist/assets/config-DGX7JYor.js +1 -0
- package/dist/assets/{copy-D8O_CYCG.js → copy-KlhazlhB.js} +1 -1
- package/dist/assets/{copy-icon-BIiRXh_3.js → copy-icon-8-a5vovc.js} +1 -1
- package/dist/assets/{createReducer-DgRODTW6.js → createReducer-HKXUbqyz.js} +1 -1
- package/dist/assets/{datasource-Cq6x_5ML.js → datasource-BA7YF4eo.js} +1 -1
- package/dist/assets/{dates-CfmdTL6B.js → dates-CQMwFko2.js} +1 -1
- package/dist/assets/{dependency-graph-panel--Kgtktcw.js → dependency-graph-panel-DsjI61Qa.js} +1 -1
- package/dist/assets/{dist-iaThVDkA.js → dist-jlhMTKo_.js} +1 -1
- package/dist/assets/{documentation-panel-BHmbeznw.js → documentation-panel-hk3IdeXy.js} +1 -1
- package/dist/assets/{download-DCJXy66H.js → download-CCZkq0y4.js} +1 -1
- package/dist/assets/{dropdown-menu-C1rnl-0o.js → dropdown-menu-DS6umAnH.js} +1 -1
- package/dist/assets/edit-page-BgS6tjth.js +9 -0
- package/dist/assets/{error-banner-Cd69vvyX.js → error-banner-B5kzVf_0.js} +1 -1
- package/dist/assets/{error-entries-Dk3VZ-OK.js → error-entries-BVXrGOTZ.js} +1 -1
- package/dist/assets/{error-panel-BPRYmC5J.js → error-panel-DYethUUR.js} +1 -1
- package/dist/assets/{errors-DCrsa1nH.js → errors-CNsgAw6m.js} +1 -1
- package/dist/assets/{es-Cqst59WX.js → es-N9SHpmG7.js} +1 -1
- package/dist/assets/feedback-button-DDzObtF2.js +1 -0
- package/dist/assets/{feedback-button-Bik2-e33.js → feedback-button-DaNc3uJa.js} +2 -2
- package/dist/assets/{file-explorer-panel-BmekFu7T.js → file-explorer-panel-CqnzSSYc.js} +1 -1
- package/dist/assets/{file-name-input-Cdc-TVqO.js → file-name-input-D4AvwZV9.js} +1 -1
- package/dist/assets/{floating-outline-XD9FAV-f.js → floating-outline-CTy-Iakk.js} +1 -1
- package/dist/assets/{formats-yZ06h1Pl.js → formats-zylfwejq.js} +1 -1
- package/dist/assets/{formatting-BqrP-n49.js → formatting-uom1es4b.js} +1 -1
- package/dist/assets/{fullscreen-B9tHcfpV.js → fullscreen-BWTs8pYZ.js} +1 -1
- package/dist/assets/{gallery-page-Dm6mGwBi.js → gallery-page-ByjlvM6k.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-2n7Ig5Jm.js → ganttDiagram-EL5Y4UJY-CYqkCfFG.js} +1 -1
- package/dist/assets/{glide-data-editor-J9CAb70d.js → glide-data-editor-CABBj2b0.js} +2 -2
- package/dist/assets/{glide-utils-CIrAxdjv.js → glide-utils-HFqhvWHI.js} +2 -2
- package/dist/assets/{home-page-s3LUizSn.js → home-page-DejRSjeJ.js} +3 -3
- package/dist/assets/hooks-BJgCcVXZ.js +1 -0
- package/dist/assets/{html-to-image-DK9Zl1lU.js → html-to-image-D_0rd3_B.js} +1 -1
- package/dist/assets/index-ChfDZJzP.css +2 -0
- package/dist/assets/index-PDJQd7X6.js +30 -0
- package/dist/assets/{input-BoBxR50a.js → input-ByHt6aoV.js} +1 -1
- package/dist/assets/{kiosk-mode-BZECfg9k.js → kiosk-mode-BGmMSc7A.js} +1 -1
- package/dist/assets/{layout-B_GMOySr.js → layout-Ct0rjb-5.js} +5 -5
- package/dist/assets/linear-Bma8J3iM.js +1 -0
- package/dist/assets/links-CXcpNIM0.js +1 -0
- package/dist/assets/{logs-panel-ABM16htT.js → logs-panel-TyeqEvOY.js} +1 -1
- package/dist/assets/{markdown-renderer-BG3sp2rs.js → markdown-renderer-BFyD6Jhs.js} +1 -1
- package/dist/assets/{menu-items-BrwnTgyK.js → menu-items-BgUyNUpu.js} +1 -1
- package/dist/assets/{mermaid-a3xYymTy.js → mermaid-B6mqMm5_.js} +2 -2
- package/dist/assets/{name-cell-input-BgEBmqyL.js → name-cell-input-D-0VF3qd.js} +1 -1
- package/dist/assets/numbers-CsZVw3Dc.js +1 -0
- package/dist/assets/{objects-snlMn4yk.js → objects-rLPlQ3X_.js} +1 -1
- package/dist/assets/{once-Haz_uxEL.js → once-6KNTRW0u.js} +1 -1
- package/dist/assets/{outline-panel-DCXQ3_F6.js → outline-panel-DMTjk2WC.js} +1 -1
- package/dist/assets/{packages-panel-BMGOmZkR.js → packages-panel-DrT69k9L.js} +1 -1
- package/dist/assets/{panel-accordion-state-C1sdPp0m.js → panel-accordion-state-Ban6j---.js} +1 -1
- package/dist/assets/panels-BLYQRkEc.js +4 -0
- package/dist/assets/precisionRound-BxlUs8G4.js +1 -0
- package/dist/assets/process-output-DzpQYlpY.js +1 -0
- package/dist/assets/{quadrantDiagram-AXDQQJYC-yBhCDI7E.js → quadrantDiagram-AXDQQJYC-D9KH5Zge.js} +1 -1
- package/dist/assets/react-vega-BTNeboR2.js +189 -0
- package/dist/assets/{readonly-code-display-Do50PHI8.js → readonly-code-display-Cyn3k5TX.js} +1 -1
- package/dist/assets/{readonly-python-code-DCSMIsTX.js → readonly-python-code-DrXDDAnA.js} +1 -1
- package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-H60uiHBp.js} +1 -1
- package/dist/assets/{reveal-component-CLE6vB1Z.js → reveal-component-_lKImPWP.js} +3 -3
- package/dist/assets/{run-page-Depd6lEE.js → run-page-7UNg--Rj.js} +1 -1
- package/dist/assets/{runs-BLp5jezv.js → runs-6lJxKV1w.js} +1 -1
- package/dist/assets/{runtime-details-state-CSPobEdR.js → runtime-details-state-CKOKhMhv.js} +1 -1
- package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
- package/dist/assets/{scratchpad-panel-lc6jmpjv.js → scratchpad-panel-C7h4MgqN.js} +1 -1
- package/dist/assets/{scroll-to-bottom-button-1FyowpWV.js → scroll-to-bottom-button-CIkON2WJ.js} +1 -1
- package/dist/assets/{secrets-panel-Bctbtkrp.js → secrets-panel-CmLZsNfp.js} +1 -1
- package/dist/assets/{select-DTBn5eTb.js → select-3CVGlYrC.js} +1 -1
- package/dist/assets/{session-DqoWmb7X.js → session-B8Y-GJnS.js} +1 -1
- package/dist/assets/session-panel-CLiWXc5y.js +1 -0
- package/dist/assets/{snippets-panel-Bh1oTw4m.js → snippets-panel-Wrqaorxa.js} +1 -1
- package/dist/assets/{spec-BfSBcO5B.js → spec-uCpVvaAR.js} +1 -1
- package/dist/assets/{state-uwRvJLb8.js → state-BY4_P6ko.js} +1 -1
- package/dist/assets/{state-BAJYRhmO.js → state-C0irhlOZ.js} +1 -1
- package/dist/assets/{state-DxQnC_1I.js → state-Ck7FMD5A.js} +1 -1
- package/dist/assets/{state-C3oDoyFe.js → state-DaWgt-rn.js} +1 -1
- package/dist/assets/state-TVREh4u4.js +3 -0
- package/dist/assets/{state-XsPO8hNi.js → state-_uXVmwuN.js} +1 -1
- package/dist/assets/{strings-B2vv-ENv.js → strings-C25fhMR_.js} +1 -1
- package/dist/assets/{swiper-component-a07domrZ.js → swiper-component-CRGRy5_O.js} +1 -1
- package/dist/assets/{switch-CTaiAjrz.js → switch-1h011wlH.js} +1 -1
- package/dist/assets/{tabs-BuQlZFbt.js → tabs-CY8E4r9U.js} +1 -1
- package/dist/assets/{terminal-Ck_vK-34.js → terminal-Dp5gyJ7l.js} +3 -3
- package/dist/assets/{time-MgToa8VM.js → time-DswSXBDk.js} +1 -1
- package/dist/assets/{toast-BGzLGnvw.js → toast-Gzxi-le_.js} +1 -1
- package/dist/assets/tooltip-Brya4Ghn.js +61 -0
- package/dist/assets/{tooltip-C9TuY18o.js → tooltip-CkGm8X7m.js} +1 -1
- package/dist/assets/tracing-fUNLyKuj.js +1 -0
- package/dist/assets/{tracing-panel-PeRXHD8_.js → tracing-panel-Cx4OqOoP.js} +2 -2
- package/dist/assets/{tree-actions-GM5E_bUs.js → tree-actions-B1vsdenY.js} +1 -1
- package/dist/assets/{use-restore-focus-BxEGwIbk.js → use-restore-focus-DdS7Tl0F.js} +1 -1
- package/dist/assets/{useAddCell-GR7eEQrT.js → useAddCell-DZ-50qEG.js} +1 -1
- package/dist/assets/useAsyncData-DhhdOGGd.js +1 -0
- package/dist/assets/{useBoolean-BDD6_xQQ.js → useBoolean-5g-bBaYF.js} +1 -1
- package/dist/assets/{useCellActionButton-B-9eG5z_.js → useCellActionButton-DINnRstv.js} +1 -1
- package/dist/assets/{useDeleteCell-2_bMClHd.js → useDeleteCell-CREJXVNp.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-k3C3M18S.js → useDependencyPanelTab-Cw5VT16Q.js} +1 -1
- package/dist/assets/{useEventListener-D9cftcHj.js → useEventListener-CjDeLqFC.js} +1 -1
- package/dist/assets/{useIframeCapabilities-CwTkczVN.js → useIframeCapabilities-Czr9e4uG.js} +1 -1
- package/dist/assets/{useInstallPackage-iX1eufdl.js → useInstallPackage-Cw0dVELh.js} +1 -1
- package/dist/assets/{useInterval--x3s1VpI.js → useInterval-DJdQmXnq.js} +1 -1
- package/dist/assets/useNotebookActions-B4av4mLc.js +1 -0
- package/dist/assets/{useRunCells-B2vUhREg.js → useRunCells-BHBMk9zW.js} +1 -1
- package/dist/assets/{useSplitCell-DEb03htp.js → useSplitCell-DAgwZV0q.js} +1 -1
- package/dist/assets/{useTheme-COhSHkCj.js → useTheme-DLtPc-39.js} +1 -1
- package/dist/assets/{utils-CaQYvazl.js → utils-DfElfBpz.js} +1 -1
- package/dist/assets/vega-component-pQE2mKc1.js +1 -0
- package/dist/assets/{vega-embed-container-DJPNkHPb.js → vega-embed-container-DtArSJz1.js} +1 -1
- package/dist/assets/vega-loader.browser-Bu25TKax.js +6 -0
- package/dist/assets/{write-secret-modal-DzKAInL2.js → write-secret-modal-usRSi1bb.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-KbGzG6-u.js → xychartDiagram-S5SC5T6Z-CeRnBS5e.js} +1 -1
- package/dist/index.html +76 -77
- package/package.json +4 -2
- package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
- package/src/__tests__/mount.test.ts +28 -8
- package/src/components/app-config/ai-config.tsx +5 -1
- package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
- package/src/components/data-table/__tests__/export-actions.test.tsx +329 -6
- package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
- package/src/components/data-table/columns.tsx +2 -1
- package/src/components/data-table/date-popover.tsx +6 -4
- package/src/components/data-table/export-actions.tsx +192 -116
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
- package/src/components/data-table/schemas.ts +14 -36
- package/src/components/editor/Output.tsx +9 -2
- package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
- package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
- package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
- package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
- package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +366 -0
- package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
- package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/components/editor/connections/components.tsx +1 -1
- package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
- package/src/components/editor/controls/command-palette.tsx +22 -10
- package/src/components/editor/dynamic-favicon.tsx +21 -40
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +537 -0
- package/src/components/editor/errors/send-error-report-button.tsx +69 -0
- package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
- package/src/components/editor/output/MarimoTracebackOutput.tsx +36 -21
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
- package/src/components/editor/output/ansi-reduce.ts +6 -0
- package/src/components/pages/home-page.tsx +4 -4
- package/src/components/tracing/tracing.tsx +2 -2
- package/src/core/attachments/__tests__/toast.test.tsx +58 -0
- package/src/core/attachments/state.ts +18 -0
- package/src/core/attachments/toast.ts +15 -0
- package/src/core/cells/__tests__/cells.test.ts +52 -0
- package/src/core/cells/__tests__/error-report.test.ts +139 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
- package/src/core/cells/__tests__/session.test.ts +50 -1
- package/src/core/cells/cells.ts +2 -1
- package/src/core/cells/error-report.ts +63 -0
- package/src/core/cells/pending-delete-service.ts +2 -6
- package/src/core/cells/session.ts +28 -1
- package/src/core/config/pair.ts +5 -3
- package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
- package/src/core/datasets/data-source-discovery.ts +4 -6
- package/src/core/edit-app.tsx +2 -2
- package/src/core/host/AGENTS.md +5 -0
- package/src/core/host/__tests__/attachments.test.ts +57 -0
- package/src/core/host/__tests__/document.test.ts +124 -0
- package/src/core/host/__tests__/events.test.ts +46 -0
- package/src/core/host/__tests__/fixtures.ts +39 -0
- package/src/core/host/__tests__/notebook-client.test.ts +433 -0
- package/src/core/host/__tests__/protocol.test.ts +15 -0
- package/src/core/host/__tests__/stream.test.ts +209 -0
- package/src/core/host/connection.ts +81 -0
- package/src/core/host/events.ts +61 -0
- package/src/core/host/notebook-client.ts +290 -0
- package/src/core/host/protocol.ts +11 -6
- package/src/core/host/resources/attachments.ts +19 -0
- package/src/core/host/resources/document.ts +101 -0
- package/src/core/host/resources/resource.ts +47 -0
- package/src/core/host/stream.ts +211 -0
- package/src/core/hotkeys/actions.ts +2 -4
- package/src/core/network/connection.ts +4 -2
- package/src/core/run-app.tsx +2 -2
- package/src/core/runtime/__tests__/runtime.test.ts +55 -11
- package/src/core/runtime/runtime.ts +18 -5
- package/src/core/runtime/types.ts +2 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
- package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +170 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +181 -65
- package/src/core/websocket/connection-state.ts +70 -0
- package/src/core/websocket/state.ts +7 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +1 -4
- package/src/core/websocket/transports/create.ts +31 -0
- package/src/core/websocket/transports/transport.ts +12 -23
- package/src/core/websocket/types.ts +9 -39
- package/src/core/websocket/useNotebookClient.ts +88 -0
- package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +59 -225
- package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
- package/src/hooks/useAsyncData.ts +3 -3
- package/src/hooks/useDataSourceDiscovery.ts +6 -4
- package/src/hooks/useElapsedTime.ts +3 -3
- package/src/mount.tsx +7 -3
- package/src/plugins/core/registerReactComponent.tsx +1 -1
- package/src/plugins/impl/DropdownPlugin.tsx +2 -6
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
- package/src/plugins/impl/code/any-language-editor.tsx +2 -2
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
- package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
- package/src/plugins/impl/vega/make-selectable.ts +3 -6
- package/src/utils/__tests__/arrays.test.ts +35 -0
- package/src/utils/__tests__/numbers.test.ts +48 -1
- package/src/utils/__tests__/objects.test.ts +21 -0
- package/src/utils/arrays.ts +10 -0
- package/src/utils/numbers.ts +28 -17
- package/src/utils/objects.ts +3 -7
- package/src/utils/once.ts +19 -19
- package/dist/assets/CellStatus-BP2mavAq.js +0 -1
- package/dist/assets/JsonOutput-DxHKOpPG.js +0 -11
- package/dist/assets/MarimoErrorOutput-D60XkCny.js +0 -6
- package/dist/assets/ai-model-dropdown-B0z6-zhp.js +0 -5
- package/dist/assets/any-language-editor-BLCAD3yx.js +0 -1
- package/dist/assets/api-BeC_VSGv.js +0 -1
- package/dist/assets/arrays-BqUx5USx.js +0 -1
- package/dist/assets/code-visibility-B-_GvwR5.js +0 -1
- package/dist/assets/coerce-CpE6kUdX.js +0 -1
- package/dist/assets/command-palette-Cf7WX-nr.js +0 -1
- package/dist/assets/config-BOKzA3tF.js +0 -1
- package/dist/assets/config-DX8Y2WbJ.js +0 -1
- package/dist/assets/config-VPSq73n8.js +0 -1
- package/dist/assets/edit-page-BT74CF4G.js +0 -9
- package/dist/assets/feedback-button-B2gKC5cn.js +0 -1
- package/dist/assets/hooks-RnjuFmEx.js +0 -1
- package/dist/assets/index-Bcdzex-3.css +0 -2
- package/dist/assets/index-D-kY3FU1.js +0 -29
- package/dist/assets/linear-0eij_aAG.js +0 -1
- package/dist/assets/links-DkkL9DVa.js +0 -1
- package/dist/assets/numbers-Mk3-CcoU.js +0 -1
- package/dist/assets/pair-CaQyK6En.js +0 -1
- package/dist/assets/panels-Dn59o8ay.js +0 -4
- package/dist/assets/precisionRound-Bq_nmf5u.js +0 -1
- package/dist/assets/process-output-D0KGq04R.js +0 -1
- package/dist/assets/react-vega-C3rYGtP6.js +0 -189
- package/dist/assets/session-panel-BxS5hiG2.js +0 -1
- package/dist/assets/state-_Qai_QyX.js +0 -3
- package/dist/assets/tooltip-mT3CGest.js +0 -61
- package/dist/assets/tracing-Di6pUmX8.js +0 -1
- package/dist/assets/useAsyncData-C7aJJz0Y.js +0 -1
- package/dist/assets/useNotebookActions-DIc4xTzY.js +0 -1
- package/dist/assets/vega-component-CnadTgoE.js +0 -1
- package/dist/assets/vega-loader.browser-CLRkNgm7.js +0 -6
- package/src/core/websocket/useWebSocket.tsx +0 -88
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { NotebookEvent } from "./events";
|
|
4
|
+
import type * as Protocol from "./protocol";
|
|
5
|
+
import type {
|
|
6
|
+
HandlerContext,
|
|
7
|
+
Resource,
|
|
8
|
+
ResourceEvent,
|
|
9
|
+
} from "./resources/resource";
|
|
10
|
+
|
|
11
|
+
/** A resource for each entry of a state map under the same name. */
|
|
12
|
+
type Resources<States> = {
|
|
13
|
+
readonly [Name in keyof States]: Resource<States[Name]>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The state of one notebook stream.
|
|
18
|
+
*
|
|
19
|
+
* Holds the phase, the notebook identity, the resume cursor, and the state
|
|
20
|
+
* of each resource.
|
|
21
|
+
*/
|
|
22
|
+
interface StreamState<States> {
|
|
23
|
+
readonly phase: HandlerContext["phase"];
|
|
24
|
+
/**
|
|
25
|
+
* The notebook the stream describes.
|
|
26
|
+
*
|
|
27
|
+
* Retained across `reset`.
|
|
28
|
+
*/
|
|
29
|
+
readonly notebookId: Protocol.NotebookIdOutput | null;
|
|
30
|
+
/**
|
|
31
|
+
* The id of the last live event.
|
|
32
|
+
*
|
|
33
|
+
* Null before `ready` and after `reset`.
|
|
34
|
+
*/
|
|
35
|
+
readonly cursor: string | null;
|
|
36
|
+
/** The state of each resource by name. */
|
|
37
|
+
readonly resources: States;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A reducer over the events of one notebook stream. */
|
|
41
|
+
interface Stream<States> {
|
|
42
|
+
readonly initial: StreamState<States>;
|
|
43
|
+
/**
|
|
44
|
+
* Apply one event.
|
|
45
|
+
*
|
|
46
|
+
* Throws on a live event without a cursor or on an object from another
|
|
47
|
+
* notebook.
|
|
48
|
+
*/
|
|
49
|
+
reduce(state: StreamState<States>, event: NotebookEvent): StreamState<States>;
|
|
50
|
+
/**
|
|
51
|
+
* The state to hold while reconnecting.
|
|
52
|
+
*
|
|
53
|
+
* A live stream keeps its state and resumes from `cursor`. An incomplete
|
|
54
|
+
* snapshot starts over.
|
|
55
|
+
*/
|
|
56
|
+
resume(state: StreamState<States>): StreamState<States>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Events that may arrive without a cursor after `ready`. */
|
|
60
|
+
const UNCURSORED = new Set<NotebookEvent["event"]>([
|
|
61
|
+
"reset",
|
|
62
|
+
"marimo",
|
|
63
|
+
"unknown",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The notebook the event's object belongs to.
|
|
68
|
+
*
|
|
69
|
+
* Undefined for objects that do not name one.
|
|
70
|
+
*/
|
|
71
|
+
function notebookIdOf(
|
|
72
|
+
event: ResourceEvent,
|
|
73
|
+
): Protocol.NotebookIdOutput | undefined {
|
|
74
|
+
switch (event.event) {
|
|
75
|
+
case "document":
|
|
76
|
+
return event.data.document.notebook_id;
|
|
77
|
+
case "cell":
|
|
78
|
+
return event.data.cell.notebook_id;
|
|
79
|
+
case "analysis":
|
|
80
|
+
return event.data.analysis.notebook_id;
|
|
81
|
+
case "runtime":
|
|
82
|
+
return event.data.runtime.notebook_id;
|
|
83
|
+
case "attachment":
|
|
84
|
+
return event.data.attachment.notebook_id;
|
|
85
|
+
case "run":
|
|
86
|
+
return event.data.run.notebook_id;
|
|
87
|
+
default:
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Compose resources into one stream. */
|
|
93
|
+
export function createStream<States extends object>(
|
|
94
|
+
resources: Resources<States>,
|
|
95
|
+
): Stream<States> {
|
|
96
|
+
const names = Object.keys(resources) as (keyof States & string)[];
|
|
97
|
+
const initialResources = Object.fromEntries(
|
|
98
|
+
names.map((name) => [name, resources[name].initial]),
|
|
99
|
+
) as States;
|
|
100
|
+
const initial: StreamState<States> = {
|
|
101
|
+
phase: "snapshot",
|
|
102
|
+
notebookId: null,
|
|
103
|
+
cursor: null,
|
|
104
|
+
resources: initialResources,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function identify(
|
|
108
|
+
state: StreamState<States>,
|
|
109
|
+
notebookId: Protocol.NotebookIdOutput,
|
|
110
|
+
): StreamState<States> {
|
|
111
|
+
if (state.notebookId === notebookId) {
|
|
112
|
+
return state;
|
|
113
|
+
}
|
|
114
|
+
if (state.notebookId !== null) {
|
|
115
|
+
throw new Error("Notebook stream changed identity");
|
|
116
|
+
}
|
|
117
|
+
return { ...state, notebookId };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clear(state: StreamState<States>): StreamState<States> {
|
|
121
|
+
return state.resources === initialResources
|
|
122
|
+
? state
|
|
123
|
+
: { ...state, resources: initialResources };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fanOut(
|
|
127
|
+
state: StreamState<States>,
|
|
128
|
+
event: ResourceEvent,
|
|
129
|
+
): StreamState<States> {
|
|
130
|
+
const context: HandlerContext = { phase: state.phase };
|
|
131
|
+
let next = state.resources;
|
|
132
|
+
for (const name of names) {
|
|
133
|
+
type State = States[typeof name];
|
|
134
|
+
// Handlers are keyed by the event name they accept.
|
|
135
|
+
const handler = resources[name].on[event.event] as
|
|
136
|
+
| ((
|
|
137
|
+
state: State,
|
|
138
|
+
event: Protocol.EventOutput,
|
|
139
|
+
context: HandlerContext,
|
|
140
|
+
) => State)
|
|
141
|
+
| undefined;
|
|
142
|
+
if (handler === undefined) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const before = next[name];
|
|
146
|
+
const after = handler(before, event, context);
|
|
147
|
+
if (after !== before) {
|
|
148
|
+
next = { ...next, [name]: after };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return next === state.resources ? state : { ...state, resources: next };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Record the position of a live event. */
|
|
155
|
+
function advance(
|
|
156
|
+
state: StreamState<States>,
|
|
157
|
+
id: string | undefined,
|
|
158
|
+
): StreamState<States> {
|
|
159
|
+
return state.phase === "live" && id !== undefined && id !== state.cursor
|
|
160
|
+
? { ...state, cursor: id }
|
|
161
|
+
: state;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
initial,
|
|
166
|
+
reduce(state, event) {
|
|
167
|
+
if (
|
|
168
|
+
state.phase === "live" &&
|
|
169
|
+
event.id === undefined &&
|
|
170
|
+
!UNCURSORED.has(event.event)
|
|
171
|
+
) {
|
|
172
|
+
throw new Error("Live notebook event is missing its cursor");
|
|
173
|
+
}
|
|
174
|
+
switch (event.event) {
|
|
175
|
+
case "marimo":
|
|
176
|
+
case "unknown":
|
|
177
|
+
return advance(state, event.id);
|
|
178
|
+
case "reset":
|
|
179
|
+
return { ...initial, notebookId: state.notebookId };
|
|
180
|
+
case "ready":
|
|
181
|
+
// Handlers see the phase that ended, so a snapshot's ready differs
|
|
182
|
+
// from one that ends a replay.
|
|
183
|
+
return { ...fanOut(state, event), phase: "live", cursor: event.id };
|
|
184
|
+
case "notebook":
|
|
185
|
+
return advance(identify(state, event.data.notebook.id), event.id);
|
|
186
|
+
case "notebook.removed":
|
|
187
|
+
return advance(
|
|
188
|
+
state.notebookId !== null && event.data.id !== state.notebookId
|
|
189
|
+
? state
|
|
190
|
+
: clear(state),
|
|
191
|
+
event.id,
|
|
192
|
+
);
|
|
193
|
+
default: {
|
|
194
|
+
const notebookId = notebookIdOf(event);
|
|
195
|
+
return advance(
|
|
196
|
+
fanOut(
|
|
197
|
+
notebookId === undefined ? state : identify(state, notebookId),
|
|
198
|
+
event,
|
|
199
|
+
),
|
|
200
|
+
event.id,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
resume(state) {
|
|
206
|
+
return state.phase === "snapshot"
|
|
207
|
+
? { ...initial, notebookId: state.notebookId }
|
|
208
|
+
: state;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { atom, useAtomValue, useSetAtom } from "jotai";
|
|
3
3
|
import useEvent from "react-use-event-hook";
|
|
4
4
|
import type { HotkeyAction } from "@/core/hotkeys/hotkeys";
|
|
5
|
+
import { Objects } from "@/utils/objects";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Map of registered keyboard shortcuts and their callbacks.
|
|
@@ -29,10 +30,7 @@ export function useSetRegisteredAction() {
|
|
|
29
30
|
},
|
|
30
31
|
),
|
|
31
32
|
unregisterAction: useEvent((shortcut: HotkeyAction) => {
|
|
32
|
-
set((actions) =>
|
|
33
|
-
const { [shortcut]: unused, ...rest } = actions;
|
|
34
|
-
return rest;
|
|
35
|
-
});
|
|
33
|
+
set((actions) => Objects.omit(actions, [shortcut]));
|
|
36
34
|
}),
|
|
37
35
|
};
|
|
38
36
|
}
|
|
@@ -7,8 +7,10 @@ import { waitFor } from "../state/jotai";
|
|
|
7
7
|
import { type ConnectionStatus, ConnectionState } from "../websocket/types";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Shared application state, not the actual event-stream status.
|
|
11
|
+
*
|
|
12
|
+
* TODO(MO-8051) Expose read-only connection state and derive UI availability
|
|
13
|
+
* separately.
|
|
12
14
|
*/
|
|
13
15
|
export const connectionAtom = atom<ConnectionStatus>({
|
|
14
16
|
state: ConnectionState.NOT_STARTED,
|
package/src/core/run-app.tsx
CHANGED
|
@@ -22,7 +22,7 @@ import { getSessionId } from "./kernel/session";
|
|
|
22
22
|
import { connectionAtom } from "./network/connection";
|
|
23
23
|
import { useRequestClient } from "./network/requests";
|
|
24
24
|
import { isAppConnecting } from "./websocket/connection-utils";
|
|
25
|
-
import {
|
|
25
|
+
import { useNotebookSession } from "./websocket/useNotebookSession";
|
|
26
26
|
|
|
27
27
|
interface AppProps {
|
|
28
28
|
appConfig: AppConfig;
|
|
@@ -51,7 +51,7 @@ export const RunApp: React.FC<AppProps> = ({ appConfig, hideHeader }) => {
|
|
|
51
51
|
};
|
|
52
52
|
}, []);
|
|
53
53
|
|
|
54
|
-
const { connection, reconnect } =
|
|
54
|
+
const { connection, reconnect } = useNotebookSession({
|
|
55
55
|
autoInstantiate: true,
|
|
56
56
|
setCells: setCells,
|
|
57
57
|
sessionId: getSessionId(),
|
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { Mocks } from "@/__mocks__/common";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
generateSessionId,
|
|
7
|
+
isSessionId,
|
|
8
|
+
type SessionId,
|
|
9
|
+
} from "@/core/kernel/session";
|
|
6
10
|
import { Logger } from "@/utils/Logger";
|
|
7
11
|
import { RuntimeManager } from "../runtime";
|
|
8
12
|
import type { RuntimeConfig } from "../types";
|
|
9
13
|
|
|
10
14
|
// Mock the session module
|
|
11
|
-
vi.mock("@/core/kernel/session", () => ({
|
|
12
|
-
|
|
15
|
+
vi.mock("@/core/kernel/session", async (importOriginal) => ({
|
|
16
|
+
...(await importOriginal<typeof import("@/core/kernel/session")>()),
|
|
17
|
+
getSessionId: () => "test-session-id",
|
|
13
18
|
}));
|
|
14
19
|
|
|
15
20
|
vi.mock("@/utils/Logger", () => ({ Logger: Mocks.quietLogger() }));
|
|
@@ -51,6 +56,38 @@ describe("RuntimeManager", () => {
|
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
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: "notebook,document,cell,attachment,marimo",
|
|
82
|
+
session_id: sessionId,
|
|
83
|
+
access_token: "test-token",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
} finally {
|
|
87
|
+
window.history.replaceState({}, "", original);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
54
91
|
it("should return WebSocket URL with session ID for https", () => {
|
|
55
92
|
const runtime = new RuntimeManager(mockConfig);
|
|
56
93
|
const sessionId = "1234" as SessionId;
|
|
@@ -59,6 +96,7 @@ describe("RuntimeManager", () => {
|
|
|
59
96
|
expect(url.protocol).toBe("wss:");
|
|
60
97
|
expect(url.hostname).toBe("example.com");
|
|
61
98
|
expect(url.pathname).toBe("/ws");
|
|
99
|
+
expect(url.searchParams.has("types")).toBe(false);
|
|
62
100
|
expect(url.searchParams.get("session_id")).toBe(sessionId);
|
|
63
101
|
});
|
|
64
102
|
|
|
@@ -219,14 +257,20 @@ describe("RuntimeManager", () => {
|
|
|
219
257
|
});
|
|
220
258
|
|
|
221
259
|
describe("getSseURL", () => {
|
|
222
|
-
it("
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
);
|
|
230
274
|
|
|
231
275
|
it("should never put the auth token in the URL, even cross-origin", () => {
|
|
232
276
|
// SSE requests can send headers, so auth travels in the
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
import { Deferred } from "@/utils/Deferred";
|
|
4
4
|
import { Logger } from "@/utils/Logger";
|
|
5
5
|
import { KnownQueryParams } from "../constants";
|
|
6
|
+
import { NOTEBOOK_EVENT_TYPES } from "../host/events";
|
|
6
7
|
import { isIslands } from "../islands/utils";
|
|
7
8
|
import { getSessionId, type SessionId } from "../kernel/session";
|
|
8
9
|
import { isStaticNotebook } from "../static/static-state";
|
|
9
10
|
import { isWasm } from "../wasm/utils";
|
|
10
11
|
import type { RuntimeConfig } from "./types";
|
|
11
12
|
|
|
13
|
+
const knownQueryParams = new Set(Object.values(KnownQueryParams));
|
|
14
|
+
|
|
12
15
|
export class RuntimeManager {
|
|
13
16
|
private initialHealthyCheck = new Deferred<void>();
|
|
14
17
|
private config: RuntimeConfig;
|
|
@@ -71,10 +74,7 @@ export class RuntimeManager {
|
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
for (const [key, value] of currentParams.entries()) {
|
|
74
|
-
if (
|
|
75
|
-
restrictToKnownQueryParams &&
|
|
76
|
-
!Object.values(KnownQueryParams).includes(key)
|
|
77
|
-
) {
|
|
77
|
+
if (restrictToKnownQueryParams && !knownQueryParams.has(key)) {
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
baseUrl.searchParams.set(key, value);
|
|
@@ -154,7 +154,18 @@ export class RuntimeManager {
|
|
|
154
154
|
* The WebSocket URL of the runtime.
|
|
155
155
|
*/
|
|
156
156
|
public getWsURL(sessionId: SessionId): URL {
|
|
157
|
-
|
|
157
|
+
const searchParams = this.getSessionSearchParams(sessionId);
|
|
158
|
+
// TODO(MO-8039): Rediscover hosts and resolve notebooks on reconnect.
|
|
159
|
+
const notebookId = this.config.notebookId;
|
|
160
|
+
const path = notebookId
|
|
161
|
+
? `/api/marimo/v1/notebooks/${encodeURIComponent(notebookId)}/events/ws`
|
|
162
|
+
: "/ws";
|
|
163
|
+
const url = this.formatWsURL(path, searchParams);
|
|
164
|
+
if (notebookId) {
|
|
165
|
+
// TODO(MO-8034): Separate host options from notebook query params.
|
|
166
|
+
url.searchParams.set("types", NOTEBOOK_EVENT_TYPES.join(","));
|
|
167
|
+
}
|
|
168
|
+
return url;
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
/**
|
|
@@ -164,6 +175,8 @@ export class RuntimeManager {
|
|
|
164
175
|
* (see `headers()`) instead of the URL.
|
|
165
176
|
*/
|
|
166
177
|
public getSseURL(sessionId: SessionId): URL {
|
|
178
|
+
// TODO: Migrate editor SSE to the host event stream. Until then, host
|
|
179
|
+
// state such as attachments is unavailable on this legacy connection.
|
|
167
180
|
const url = this.formatHttpURL({
|
|
168
181
|
path: "/sse",
|
|
169
182
|
searchParams: this.getSessionSearchParams(sessionId),
|
package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts}
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { Logger } from "@/utils/Logger";
|
|
5
5
|
import { WebSocketClosedReason, ConnectionState } from "../types";
|
|
6
|
-
import { classifyCloseEvent } from "../
|
|
6
|
+
import { classifyCloseEvent } from "../connection-state";
|
|
7
7
|
|
|
8
8
|
function classify(reason: string | undefined) {
|
|
9
9
|
return classifyCloseEvent({ reason });
|
|
@@ -13,8 +13,10 @@ describe("classifyCloseEvent", () => {
|
|
|
13
13
|
describe("transient closes (default branch)", () => {
|
|
14
14
|
it("retries on empty/undefined reason", () => {
|
|
15
15
|
const decision = classify(undefined);
|
|
16
|
-
expect(decision
|
|
17
|
-
|
|
16
|
+
expect(decision).toEqual({
|
|
17
|
+
kind: "retry",
|
|
18
|
+
status: { state: ConnectionState.CONNECTING },
|
|
19
|
+
});
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
it("treats unknown reason strings as transient and logs a warning", () => {
|
|
@@ -40,29 +42,25 @@ describe("classifyCloseEvent", () => {
|
|
|
40
42
|
// terminal or a rejected client would reconnect forever.
|
|
41
43
|
"MARIMO_UNAUTHORIZED",
|
|
42
44
|
"MARIMO_KIOSK_NOT_ALLOWED",
|
|
43
|
-
])("%s → terminal with KERNEL_DISCONNECTED
|
|
45
|
+
])("%s → terminal with KERNEL_DISCONNECTED", (reason) => {
|
|
44
46
|
const decision = classify(reason);
|
|
45
|
-
expect(decision.kind).toBe("terminal");
|
|
46
|
-
expect(decision.status).toMatchObject({
|
|
47
|
-
state: ConnectionState.CLOSED,
|
|
48
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
49
|
-
});
|
|
50
47
|
expect(decision).toMatchObject({
|
|
51
48
|
kind: "terminal",
|
|
52
|
-
|
|
49
|
+
status: {
|
|
50
|
+
state: ConnectionState.CLOSED,
|
|
51
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
52
|
+
},
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it("MARIMO_KERNEL_STARTUP_ERROR → terminal
|
|
56
|
+
it("MARIMO_KERNEL_STARTUP_ERROR → terminal", () => {
|
|
57
57
|
const decision = classify("MARIMO_KERNEL_STARTUP_ERROR");
|
|
58
|
-
expect(decision.kind).toBe("terminal");
|
|
59
|
-
expect(decision.status).toMatchObject({
|
|
60
|
-
state: ConnectionState.CLOSED,
|
|
61
|
-
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
62
|
-
});
|
|
63
58
|
expect(decision).toMatchObject({
|
|
64
59
|
kind: "terminal",
|
|
65
|
-
|
|
60
|
+
status: {
|
|
61
|
+
state: ConnectionState.CLOSED,
|
|
62
|
+
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
63
|
+
},
|
|
66
64
|
});
|
|
67
65
|
});
|
|
68
66
|
});
|
package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts}
RENAMED
|
@@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
4
4
|
import { BasicTransport } from "../transports/basic";
|
|
5
5
|
import { SseTransport } from "../transports/sse";
|
|
6
6
|
import { WsTransport } from "../transports/ws";
|
|
7
|
-
import { createConnectionTransport } from "../
|
|
7
|
+
import { createConnectionTransport } from "../transports/create";
|
|
8
8
|
|
|
9
9
|
vi.mock("../../wasm/utils", () => ({
|
|
10
10
|
isWasm: () => false,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import { act, renderHook } from "@testing-library/react";
|
|
5
|
+
import { createStore, Provider } from "jotai";
|
|
6
|
+
import type { PropsWithChildren } from "react";
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { ConnectionState } from "../types";
|
|
9
|
+
import { createConnectionTransport } from "../transports/create";
|
|
10
|
+
import type { IConnectionTransport } from "../transports/transport";
|
|
11
|
+
import { useNotebookClient } from "../useNotebookClient";
|
|
12
|
+
import { fixtureMessages } from "../../host/__tests__/fixtures";
|
|
13
|
+
import { agentAttachmentsAtom } from "../../attachments/state";
|
|
14
|
+
import { notebookClientAtom } from "../state";
|
|
15
|
+
|
|
16
|
+
vi.mock("../transports/create", () => ({ createConnectionTransport: vi.fn() }));
|
|
17
|
+
|
|
18
|
+
function makeTransport() {
|
|
19
|
+
const events = new EventTarget();
|
|
20
|
+
const transport: IConnectionTransport = {
|
|
21
|
+
readyState: WebSocket.CLOSED,
|
|
22
|
+
reconnect: vi.fn(() => {
|
|
23
|
+
transport.readyState = WebSocket.OPEN;
|
|
24
|
+
events.dispatchEvent(new Event("open"));
|
|
25
|
+
}),
|
|
26
|
+
close: vi.fn(() => {
|
|
27
|
+
transport.readyState = WebSocket.CLOSED;
|
|
28
|
+
events.dispatchEvent(new CloseEvent("close"));
|
|
29
|
+
}),
|
|
30
|
+
send: vi.fn(),
|
|
31
|
+
addEventListener: events.addEventListener.bind(events),
|
|
32
|
+
removeEventListener: events.removeEventListener.bind(events),
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
transport,
|
|
36
|
+
receive: (message: unknown) =>
|
|
37
|
+
events.dispatchEvent(
|
|
38
|
+
new MessageEvent("message", { data: JSON.stringify(message) }),
|
|
39
|
+
),
|
|
40
|
+
notify: () =>
|
|
41
|
+
events.dispatchEvent(
|
|
42
|
+
new MessageEvent("message", {
|
|
43
|
+
data: JSON.stringify({
|
|
44
|
+
event: "marimo",
|
|
45
|
+
data: { marimo: { op: "reconnected" } },
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setup(strict = false) {
|
|
53
|
+
const store = createStore();
|
|
54
|
+
const options = {
|
|
55
|
+
sessionId: "first",
|
|
56
|
+
static: false,
|
|
57
|
+
transportType: "websocket" as const,
|
|
58
|
+
messageFormat: "host" as const,
|
|
59
|
+
url: () => "ws://localhost/ws",
|
|
60
|
+
headers: () => ({}),
|
|
61
|
+
waitUntilReady: async () => undefined,
|
|
62
|
+
checkHealth: async () => true,
|
|
63
|
+
onNotification: vi.fn(),
|
|
64
|
+
onMessageError: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
const wrapper = ({ children }: PropsWithChildren) => (
|
|
67
|
+
<Provider store={store}>{children}</Provider>
|
|
68
|
+
);
|
|
69
|
+
const hook = renderHook((props) => useNotebookClient(props), {
|
|
70
|
+
initialProps: options,
|
|
71
|
+
reactStrictMode: strict,
|
|
72
|
+
wrapper,
|
|
73
|
+
});
|
|
74
|
+
return { ...hook, options, store };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.mocked(createConnectionTransport).mockReset();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("useNotebookClient", () => {
|
|
82
|
+
it("exposes host presence to the UI without waiting for kernel-ready", async () => {
|
|
83
|
+
const transport = makeTransport();
|
|
84
|
+
vi.mocked(createConnectionTransport).mockReturnValue(transport.transport);
|
|
85
|
+
const { result, store, unmount } = setup();
|
|
86
|
+
await act(async () => Promise.resolve());
|
|
87
|
+
expect(store.get(notebookClientAtom)).toBe(result.current);
|
|
88
|
+
act(() =>
|
|
89
|
+
fixtureMessages("notebook-events.sse").forEach(transport.receive),
|
|
90
|
+
);
|
|
91
|
+
expect(store.get(agentAttachmentsAtom)).toEqual([
|
|
92
|
+
{
|
|
93
|
+
id: "a2",
|
|
94
|
+
notebook_id: "n1",
|
|
95
|
+
kind: "agent",
|
|
96
|
+
name: "Notebook CLI",
|
|
97
|
+
since: "2026-10-01T09:20:03Z",
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
unmount();
|
|
101
|
+
expect(store.get(notebookClientAtom)).toBeNull();
|
|
102
|
+
expect(store.get(agentAttachmentsAtom)).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses current callbacks and URL providers without reconnecting on a rerender", async () => {
|
|
106
|
+
const first = makeTransport();
|
|
107
|
+
vi.mocked(createConnectionTransport).mockReturnValue(first.transport);
|
|
108
|
+
const { rerender, options, unmount } = setup();
|
|
109
|
+
await act(async () => Promise.resolve());
|
|
110
|
+
const onNotification = vi.fn();
|
|
111
|
+
rerender({ ...options, onNotification, url: () => "ws://redirected/ws" });
|
|
112
|
+
act(() => first.notify());
|
|
113
|
+
expect(options.onNotification).not.toHaveBeenCalled();
|
|
114
|
+
expect(onNotification).toHaveBeenCalledExactlyOnceWith({
|
|
115
|
+
data: { op: "reconnected" },
|
|
116
|
+
});
|
|
117
|
+
expect(createConnectionTransport).toHaveBeenCalledOnce();
|
|
118
|
+
expect(vi.mocked(createConnectionTransport).mock.calls[0][0].url()).toBe(
|
|
119
|
+
"ws://redirected/ws",
|
|
120
|
+
);
|
|
121
|
+
unmount();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("replaces the transport when session identity changes and ignores old messages", async () => {
|
|
125
|
+
const first = makeTransport();
|
|
126
|
+
const second = makeTransport();
|
|
127
|
+
vi.mocked(createConnectionTransport)
|
|
128
|
+
.mockReturnValueOnce(first.transport)
|
|
129
|
+
.mockReturnValueOnce(second.transport);
|
|
130
|
+
const { rerender, options, unmount } = setup();
|
|
131
|
+
await act(async () => Promise.resolve());
|
|
132
|
+
rerender({ ...options, sessionId: "second" });
|
|
133
|
+
await act(async () => Promise.resolve());
|
|
134
|
+
expect(first.transport.close).toHaveBeenCalledOnce();
|
|
135
|
+
act(() => {
|
|
136
|
+
first.notify();
|
|
137
|
+
second.notify();
|
|
138
|
+
});
|
|
139
|
+
expect(options.onNotification).toHaveBeenCalledOnce();
|
|
140
|
+
unmount();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("cleans up every transport under Strict Mode and only connects the active lifetime", async () => {
|
|
144
|
+
const transports: ReturnType<typeof makeTransport>[] = [];
|
|
145
|
+
vi.mocked(createConnectionTransport).mockImplementation(() => {
|
|
146
|
+
const next = makeTransport();
|
|
147
|
+
transports.push(next);
|
|
148
|
+
return next.transport;
|
|
149
|
+
});
|
|
150
|
+
const { result, store, options, unmount } = setup(true);
|
|
151
|
+
await act(async () => Promise.resolve());
|
|
152
|
+
expect(transports).toHaveLength(2);
|
|
153
|
+
expect(transports[0].transport.reconnect).not.toHaveBeenCalled();
|
|
154
|
+
expect(transports[0].transport.close).toHaveBeenCalledOnce();
|
|
155
|
+
expect(transports[1].transport.reconnect).toHaveBeenCalledOnce();
|
|
156
|
+
expect(store.get(result.current.connectionAtom)).toEqual({
|
|
157
|
+
state: ConnectionState.OPEN,
|
|
158
|
+
});
|
|
159
|
+
act(() => {
|
|
160
|
+
for (const transport of transports) {
|
|
161
|
+
transport.notify();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
expect(options.onNotification).toHaveBeenCalledOnce();
|
|
165
|
+
unmount();
|
|
166
|
+
expect(transports[1].transport.close).toHaveBeenCalledOnce();
|
|
167
|
+
await result.current.reconnect();
|
|
168
|
+
expect(transports[1].transport.reconnect).toHaveBeenCalledOnce();
|
|
169
|
+
});
|
|
170
|
+
});
|