@marimo-team/frontend 0.25.3-dev4 → 0.25.3-dev42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/CellStatus-BpOBz-8m.js +1 -0
- package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-D9UK8o9A.js} +1 -1
- package/dist/assets/{ErrorBoundary-Gl5nCA8C.js → ErrorBoundary-DmXl4Nre.js} +2 -2
- package/dist/assets/JsonOutput-BJraU9kp.js +11 -0
- package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-BPYE76jR.js} +2 -2
- package/dist/assets/MarimoErrorOutput-DVjK8RgY.js +10 -0
- package/dist/assets/{RenderHTML-BbOqmHy3.js → RenderHTML-BFaztTR0.js} +1 -1
- package/dist/assets/{RunButton-CK3LhnP5.js → RunButton-B44xE_d5.js} +1 -1
- package/dist/assets/{add-cell-with-ai-8UBC0xwR.js → add-cell-with-ai-CkjslziO.js} +13 -13
- package/dist/assets/{add-connection-dialog-DXQnd6eB.js → add-connection-dialog-CWLLVKju.js} +2 -2
- package/dist/assets/{agent-panel-DdkVL-Kx.js → agent-panel-DYdQ_2pp.js} +5 -5
- package/dist/assets/ai-model-dropdown-F3mm17Wa.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-D0uDs5nC.js} +1 -1
- package/dist/assets/arrays-0R2reF2y.js +1 -0
- package/dist/assets/{blob-D5nfNHOo.js → blob-BSfsdCNF.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-DzvvHs9g.js} +9 -9
- package/dist/assets/{cell-link-LSth_8hT.js → cell-link-DFPHORAO.js} +1 -1
- package/dist/assets/{cells-BRkCaSsh.js → cells-BdgzY0of.js} +31 -31
- package/dist/assets/{chat-display-Dnagy4Nv.js → chat-display-BrcwdhKJ.js} +1 -1
- package/dist/assets/{chat-panel-DN9vsbgS.js → chat-panel-C__3iSyI.js} +2 -2
- package/dist/assets/{chat-ui-Df8Vcj6n.js → chat-ui-NlBK3kK6.js} +1 -1
- package/dist/assets/{circle-DXf-VWZG.js → circle-B6D_6D8w.js} +1 -1
- package/dist/assets/code-visibility-Cs-Vv3jV.js +1 -0
- package/dist/assets/coerce-DWjFeXOY.js +1 -0
- package/dist/assets/{column-preview-CHK_DNrs.js → column-preview-ivm1b-GY.js} +1 -1
- package/dist/assets/command-palette-TgwswwvD.js +1 -0
- package/dist/assets/{common-Cu6CDbuw.js → common-BcEY1yDS.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/{components-CGw16R_N.js → components-lkCfd2vy.js} +1 -1
- package/dist/assets/config-BwHT0yGp.js +104 -0
- package/dist/assets/config-DGX7JYor.js +1 -0
- package/dist/assets/config-DXjnAwtL.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-dKk3JHSQ.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-BaisS9Zt.js} +1 -1
- package/dist/assets/{dist-iaThVDkA.js → dist-jlhMTKo_.js} +1 -1
- package/dist/assets/{documentation-panel-BHmbeznw.js → documentation-panel-CpSUoM8r.js} +1 -1
- package/dist/assets/{download-DCJXy66H.js → download-RV4i1fWI.js} +1 -1
- package/dist/assets/{dropdown-menu-C1rnl-0o.js → dropdown-menu-DS6umAnH.js} +1 -1
- package/dist/assets/edit-page-C8RUVAaO.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-CJS0gOId.js} +1 -1
- package/dist/assets/{error-panel-BPRYmC5J.js → error-panel-BxCKpZtN.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-BLvwvndE.js +1 -0
- package/dist/assets/{feedback-button-Bik2-e33.js → feedback-button-DYjae3_b.js} +2 -2
- package/dist/assets/{file-explorer-panel-BmekFu7T.js → file-explorer-panel-BmjAJusg.js} +1 -1
- package/dist/assets/{file-name-input-Cdc-TVqO.js → file-name-input-BpfSzvEB.js} +1 -1
- package/dist/assets/{floating-outline-XD9FAV-f.js → floating-outline-CrWdiNQ2.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-W_b-XWq6.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-C_bIL30J.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-D1teQDdF.js} +3 -3
- package/dist/assets/hooks-CZF85I2f.js +1 -0
- package/dist/assets/{html-to-image-DK9Zl1lU.js → html-to-image-D_0rd3_B.js} +1 -1
- package/dist/assets/index-4Nwpioic.js +30 -0
- package/dist/assets/index-ChfDZJzP.css +2 -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-ByZveRPN.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-DVSgfMgv.js} +1 -1
- package/dist/assets/{markdown-renderer-BG3sp2rs.js → markdown-renderer-DX2O5YiE.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-DL1i8Mj0.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-DPKPT2eg.js} +1 -1
- package/dist/assets/{packages-panel-BMGOmZkR.js → packages-panel-CZZwD9YV.js} +1 -1
- package/dist/assets/{panel-accordion-state-C1sdPp0m.js → panel-accordion-state-C7vvsYz1.js} +1 -1
- package/dist/assets/panels-BT2oosq8.js +4 -0
- package/dist/assets/precisionRound-BxlUs8G4.js +1 -0
- package/dist/assets/process-output-BKhybJBb.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-9QrSm_7D.js} +1 -1
- package/dist/assets/{readonly-python-code-DCSMIsTX.js → readonly-python-code-BLotL3XP.js} +1 -1
- package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-H60uiHBp.js} +1 -1
- package/dist/assets/{reveal-component-CLE6vB1Z.js → reveal-component-2QaMbLNx.js} +3 -3
- package/dist/assets/{run-page-Depd6lEE.js → run-page-DyZ8JFKU.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-B25uM-Ps.js} +1 -1
- package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
- package/dist/assets/{scratchpad-panel-lc6jmpjv.js → scratchpad-panel-Dt83MaaA.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-xVWVwa1U.js +1 -0
- package/dist/assets/{snippets-panel-Bh1oTw4m.js → snippets-panel-IxIxfxNc.js} +1 -1
- package/dist/assets/{spec-BfSBcO5B.js → spec-uCpVvaAR.js} +1 -1
- package/dist/assets/state-BHuYDaxs.js +3 -0
- 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-XsPO8hNi.js → state-CVgqwwCU.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/{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-CjvCrRrA.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-V5zejPsw.js +1 -0
- package/dist/assets/{tracing-panel-PeRXHD8_.js → tracing-panel-DLS3iwK6.js} +2 -2
- package/dist/assets/{tree-actions-GM5E_bUs.js → tree-actions-Be_mzC1C.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-DK79W1vA.js} +1 -1
- package/dist/assets/useAsyncData-DhhdOGGd.js +1 -0
- package/dist/assets/{useBoolean-BDD6_xQQ.js → useBoolean-j0qHaJxu.js} +1 -1
- package/dist/assets/{useCellActionButton-B-9eG5z_.js → useCellActionButton-Cw9Ob12j.js} +1 -1
- package/dist/assets/{useDeleteCell-2_bMClHd.js → useDeleteCell-BNqX6PPu.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-k3C3M18S.js → useDependencyPanelTab-GnTnqKJo.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-CVItDn1e.js} +1 -1
- package/dist/assets/{useInterval--x3s1VpI.js → useInterval-DJdQmXnq.js} +1 -1
- package/dist/assets/useNotebookActions-_qJ1Sjtj.js +1 -0
- package/dist/assets/{useRunCells-B2vUhREg.js → useRunCells-DP09DHT6.js} +1 -1
- package/dist/assets/{useSplitCell-DEb03htp.js → useSplitCell-IfumZ4sm.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-CMn031HF.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 -4
- package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
- package/src/__tests__/host-imports.test.ts +1 -0
- 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 +11 -2
- package/src/core/host/__tests__/attachments.test.ts +56 -0
- package/src/core/host/__tests__/document.test.ts +126 -0
- package/src/core/host/__tests__/events.test.ts +49 -0
- package/src/core/host/__tests__/fixtures.ts +41 -0
- package/src/core/host/__tests__/notebook-client.test.ts +433 -0
- package/src/core/host/connection.ts +81 -0
- package/src/core/host/events.ts +77 -0
- package/src/core/host/notebook-client.ts +323 -0
- package/src/core/host/resources/attachments.ts +19 -0
- package/src/core/host/resources/document.ts +101 -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/host/__tests__/protocol.test.ts +0 -214
- package/src/core/host/protocol.ts +0 -1943
- package/src/core/websocket/useWebSocket.tsx +0 -88
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { Logger } from "@/utils/Logger";
|
|
4
|
+
import type { Attachment } from "@marimo-team/client/protocol";
|
|
5
|
+
import { createReducer } from "@marimo-team/client/reducer";
|
|
6
|
+
import { type Atom, atom, type createStore } from "jotai/vanilla";
|
|
7
|
+
import {
|
|
8
|
+
type ConnectionStatus,
|
|
9
|
+
ConnectionState,
|
|
10
|
+
ConnectionClosedReason,
|
|
11
|
+
type CloseDecision,
|
|
12
|
+
type NotebookTransport,
|
|
13
|
+
TransportReadyState,
|
|
14
|
+
} from "./connection";
|
|
15
|
+
import { type NotebookEvent, parseNotebookEvent } from "./events";
|
|
16
|
+
import { attachments } from "./resources/attachments";
|
|
17
|
+
import { type Document, document } from "./resources/document";
|
|
18
|
+
|
|
19
|
+
/** Events that may arrive without a cursor after `ready`. */
|
|
20
|
+
const UNCURSORED = new Set<NotebookEvent["event"]>([
|
|
21
|
+
"reset",
|
|
22
|
+
"marimo",
|
|
23
|
+
"unknown",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** A connection to a notebook on a marimo host. */
|
|
27
|
+
export interface NotebookClient {
|
|
28
|
+
/** Client connection status, independent of application session readiness. */
|
|
29
|
+
readonly connectionAtom: Atom<ConnectionStatus>;
|
|
30
|
+
/**
|
|
31
|
+
* The most recently confirmed document.
|
|
32
|
+
*
|
|
33
|
+
* Null until a snapshot is complete, after reset or removal, and on
|
|
34
|
+
* connections without host document events. A temporary disconnection
|
|
35
|
+
* retains the last document. Partial updates are not exposed.
|
|
36
|
+
*/
|
|
37
|
+
readonly documentAtom: Atom<Document | null>;
|
|
38
|
+
/** Live attachments, empty until the stream is ready or while disconnected. */
|
|
39
|
+
readonly attachmentsAtom: Atom<readonly Attachment[]>;
|
|
40
|
+
/** Attempt to restore a closed connection after checking host health. */
|
|
41
|
+
reconnect(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The transport and application callbacks needed to connect a notebook. */
|
|
45
|
+
export interface NotebookConnection {
|
|
46
|
+
/** Create a transport that reads the URL again on each connection attempt. */
|
|
47
|
+
createTransport: (url: () => string) => NotebookTransport;
|
|
48
|
+
/** The current stream URL, including authentication and subscription options. */
|
|
49
|
+
url: () => string;
|
|
50
|
+
/** Host events require an intact stream; legacy notifications are independent. */
|
|
51
|
+
messageFormat: "host" | "session";
|
|
52
|
+
/** Abort to close the transport and clear the client's state. */
|
|
53
|
+
signal: AbortSignal;
|
|
54
|
+
/** Wait until the host can accept the initial connection. */
|
|
55
|
+
waitUntilReady: () => Promise<void>;
|
|
56
|
+
/** Check whether the host is reachable before an explicit reconnect. */
|
|
57
|
+
checkHealth: () => Promise<boolean>;
|
|
58
|
+
/** Decode wire text without interpreting its event type. */
|
|
59
|
+
decodeJson: (message: string) => unknown;
|
|
60
|
+
/** Handle a native marimo notification. Its payload has not been validated. */
|
|
61
|
+
onNotification: (notification: unknown) => void;
|
|
62
|
+
/** Report a malformed message without coupling the client to editor UI. */
|
|
63
|
+
onMessageError: (error: unknown) => void;
|
|
64
|
+
/** Choose the connection status and retry policy after the transport closes. */
|
|
65
|
+
classifyClose: (event: CloseEvent) => CloseDecision;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Create a client without opening a connection. */
|
|
69
|
+
export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
70
|
+
const connectionAtom = atom<ConnectionStatus>({
|
|
71
|
+
state: ConnectionState.NOT_STARTED,
|
|
72
|
+
});
|
|
73
|
+
// One atom holds the whole stream, which resource atoms derive from.
|
|
74
|
+
const reducer = createReducer({ document, attachments });
|
|
75
|
+
const streamAtom = atom(reducer.initial);
|
|
76
|
+
// A resumed stream is live, but presence waits for this connection's ready.
|
|
77
|
+
const readyAtom = atom(false);
|
|
78
|
+
const documentAtom = atom(
|
|
79
|
+
(get) => get(streamAtom).resources.document.current,
|
|
80
|
+
);
|
|
81
|
+
const attachmentsAtom = atom((get) =>
|
|
82
|
+
get(readyAtom) && get(connectionAtom).state === ConnectionState.OPEN
|
|
83
|
+
? get(streamAtom).resources.attachments
|
|
84
|
+
: attachments.initial,
|
|
85
|
+
);
|
|
86
|
+
let reconnect: (() => Promise<void>) | undefined;
|
|
87
|
+
const client: NotebookClient = {
|
|
88
|
+
connectionAtom,
|
|
89
|
+
documentAtom,
|
|
90
|
+
attachmentsAtom,
|
|
91
|
+
reconnect: async () => reconnect?.(),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function connect({
|
|
95
|
+
createTransport,
|
|
96
|
+
url,
|
|
97
|
+
messageFormat,
|
|
98
|
+
signal,
|
|
99
|
+
waitUntilReady,
|
|
100
|
+
checkHealth,
|
|
101
|
+
onNotification,
|
|
102
|
+
onMessageError,
|
|
103
|
+
decodeJson,
|
|
104
|
+
classifyClose,
|
|
105
|
+
}: NotebookConnection) {
|
|
106
|
+
if (signal.aborted) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let shouldRetry = true;
|
|
110
|
+
let terminal = false;
|
|
111
|
+
let checkingHealth = false;
|
|
112
|
+
let streamKey: string | undefined;
|
|
113
|
+
// The id of the last live event. Null before `ready` and after `reset`.
|
|
114
|
+
let cursor: string | null = null;
|
|
115
|
+
const transport = createTransport(() => {
|
|
116
|
+
if (messageFormat === "session") {
|
|
117
|
+
return url();
|
|
118
|
+
}
|
|
119
|
+
const next = new URL(url());
|
|
120
|
+
next.searchParams.delete("cursor");
|
|
121
|
+
if (signal.aborted) {
|
|
122
|
+
return next.toString();
|
|
123
|
+
}
|
|
124
|
+
// A cursor is useful with the state that produced it, which a change
|
|
125
|
+
// of session or notebook invalidates. Authentication may change freely.
|
|
126
|
+
const identity = new URL(next);
|
|
127
|
+
identity.searchParams.delete("access_token");
|
|
128
|
+
identity.searchParams.sort();
|
|
129
|
+
const key = identity.toString();
|
|
130
|
+
const current = store.get(streamAtom);
|
|
131
|
+
// A live stream keeps its state and resumes from the cursor. An
|
|
132
|
+
// incomplete snapshot starts over.
|
|
133
|
+
const state =
|
|
134
|
+
key !== streamKey
|
|
135
|
+
? reducer.initial
|
|
136
|
+
: current.phase === "snapshot"
|
|
137
|
+
? { ...reducer.initial, notebookId: current.notebookId }
|
|
138
|
+
: current;
|
|
139
|
+
if (state.phase === "snapshot") {
|
|
140
|
+
cursor = null;
|
|
141
|
+
}
|
|
142
|
+
streamKey = key;
|
|
143
|
+
store.set(readyAtom, false);
|
|
144
|
+
store.set(streamAtom, state);
|
|
145
|
+
if (cursor !== null) {
|
|
146
|
+
next.searchParams.set("cursor", cursor);
|
|
147
|
+
}
|
|
148
|
+
return next.toString();
|
|
149
|
+
});
|
|
150
|
+
const setStatus = (status: ConnectionStatus) => {
|
|
151
|
+
store.set(readyAtom, false);
|
|
152
|
+
store.set(connectionAtom, status);
|
|
153
|
+
};
|
|
154
|
+
const disconnected = {
|
|
155
|
+
state: ConnectionState.CLOSED,
|
|
156
|
+
code: ConnectionClosedReason.KERNEL_DISCONNECTED,
|
|
157
|
+
reason: "kernel not found",
|
|
158
|
+
} satisfies ConnectionStatus;
|
|
159
|
+
|
|
160
|
+
function retry(code?: number, reason?: string) {
|
|
161
|
+
if (!signal.aborted && !terminal && shouldRetry) {
|
|
162
|
+
shouldRetry = false;
|
|
163
|
+
transport.reconnect(code, reason);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function onOpen() {
|
|
168
|
+
if (signal.aborted || terminal) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
shouldRetry = true;
|
|
172
|
+
setStatus({ state: ConnectionState.OPEN });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onClose(event: CloseEvent) {
|
|
176
|
+
if (signal.aborted || terminal) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
Logger.warn("Notebook connection closed", event.code, event.reason);
|
|
180
|
+
const decision = classifyClose(event);
|
|
181
|
+
setStatus(decision.status);
|
|
182
|
+
if (decision.kind === "terminal") {
|
|
183
|
+
terminal = true;
|
|
184
|
+
transport.close();
|
|
185
|
+
} else if (decision.kind === "retry") {
|
|
186
|
+
retry(event.code, event.reason);
|
|
187
|
+
} else {
|
|
188
|
+
terminal = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function onError(event: Event) {
|
|
193
|
+
if (signal.aborted || terminal) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
Logger.warn("Notebook connection error", event);
|
|
197
|
+
setStatus(disconnected);
|
|
198
|
+
retry();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function onMessage(message: MessageEvent<string>) {
|
|
202
|
+
if (signal.aborted || terminal) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
let event;
|
|
206
|
+
try {
|
|
207
|
+
event = parseNotebookEvent(decodeJson(message.data));
|
|
208
|
+
const state = store.get(streamAtom);
|
|
209
|
+
if (
|
|
210
|
+
state.phase === "live" &&
|
|
211
|
+
event.id === undefined &&
|
|
212
|
+
!UNCURSORED.has(event.event)
|
|
213
|
+
) {
|
|
214
|
+
throw new Error("Live notebook event is missing its cursor");
|
|
215
|
+
}
|
|
216
|
+
if (event.event !== "marimo" && event.event !== "unknown") {
|
|
217
|
+
store.set(streamAtom, reducer.reduce(state, event));
|
|
218
|
+
}
|
|
219
|
+
if (event.event === "reset") {
|
|
220
|
+
cursor = null;
|
|
221
|
+
} else if (event.event === "ready" || state.phase === "live") {
|
|
222
|
+
cursor = event.id ?? cursor;
|
|
223
|
+
}
|
|
224
|
+
if (event.event === "ready" || event.event === "reset") {
|
|
225
|
+
store.set(readyAtom, event.event === "ready");
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
Logger.error("Failed to decode notebook message", error);
|
|
229
|
+
if (messageFormat === "session") {
|
|
230
|
+
onMessageError(error);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
store.set(streamAtom, reducer.initial);
|
|
234
|
+
cursor = null;
|
|
235
|
+
terminal = true;
|
|
236
|
+
setStatus({
|
|
237
|
+
...disconnected,
|
|
238
|
+
reason: "Failed to decode notebook message",
|
|
239
|
+
});
|
|
240
|
+
transport.close();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (event.event === "marimo") {
|
|
244
|
+
onNotification(event.data.marimo);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
reconnect = async () => {
|
|
249
|
+
if (
|
|
250
|
+
signal.aborted ||
|
|
251
|
+
checkingHealth ||
|
|
252
|
+
transport.readyState === TransportReadyState.OPEN ||
|
|
253
|
+
transport.readyState === TransportReadyState.CONNECTING
|
|
254
|
+
) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
checkingHealth = true;
|
|
258
|
+
setStatus({ state: ConnectionState.CONNECTING });
|
|
259
|
+
try {
|
|
260
|
+
const healthy = await checkHealth();
|
|
261
|
+
if (signal.aborted) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!healthy) {
|
|
265
|
+
shouldRetry = false;
|
|
266
|
+
setStatus(disconnected);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
terminal = false;
|
|
270
|
+
shouldRetry = true;
|
|
271
|
+
transport.reconnect();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!signal.aborted) {
|
|
274
|
+
Logger.error("Notebook health check failed", error);
|
|
275
|
+
shouldRetry = false;
|
|
276
|
+
setStatus(disconnected);
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
checkingHealth = false;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
signal.addEventListener(
|
|
284
|
+
"abort",
|
|
285
|
+
() => {
|
|
286
|
+
transport.removeEventListener("open", onOpen);
|
|
287
|
+
transport.removeEventListener("close", onClose);
|
|
288
|
+
transport.removeEventListener("error", onError);
|
|
289
|
+
transport.removeEventListener("message", onMessage);
|
|
290
|
+
transport.close();
|
|
291
|
+
reconnect = undefined;
|
|
292
|
+
store.set(streamAtom, reducer.initial);
|
|
293
|
+
setStatus({ state: ConnectionState.NOT_STARTED });
|
|
294
|
+
},
|
|
295
|
+
{ once: true },
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
transport.addEventListener("open", onOpen);
|
|
299
|
+
transport.addEventListener("close", onClose);
|
|
300
|
+
transport.addEventListener("error", onError);
|
|
301
|
+
transport.addEventListener("message", onMessage);
|
|
302
|
+
|
|
303
|
+
if (transport.readyState === TransportReadyState.CLOSED) {
|
|
304
|
+
setStatus({ state: ConnectionState.CONNECTING });
|
|
305
|
+
void waitUntilReady()
|
|
306
|
+
.then(() => {
|
|
307
|
+
if (!signal.aborted) {
|
|
308
|
+
transport.reconnect();
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
.catch((error) => {
|
|
312
|
+
if (!signal.aborted) {
|
|
313
|
+
Logger.error("Healthy connection never made", error);
|
|
314
|
+
terminal = true;
|
|
315
|
+
setStatus(disconnected);
|
|
316
|
+
transport.close();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { client, connect };
|
|
323
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { Attachment } from "@marimo-team/client/protocol";
|
|
4
|
+
import type { Resource } from "@marimo-team/client/reducer";
|
|
5
|
+
|
|
6
|
+
/** Attachment records retained with the stream's resume cursor. */
|
|
7
|
+
export const attachments: Resource<readonly Attachment[], "attachment"> = {
|
|
8
|
+
initial: [],
|
|
9
|
+
on: {
|
|
10
|
+
attachment: (state, { data: { attachment } }) =>
|
|
11
|
+
state.some((item) => item.id === attachment.id)
|
|
12
|
+
? state.map((item) => (item.id === attachment.id ? attachment : item))
|
|
13
|
+
: [...state, attachment],
|
|
14
|
+
"attachment.removed": (state, { data }) =>
|
|
15
|
+
state.some((item) => item.id === data.id)
|
|
16
|
+
? state.filter((item) => item.id !== data.id)
|
|
17
|
+
: state,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
4
|
+
import type { Resource } from "@marimo-team/client/reducer";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The notebook's content at one revision.
|
|
8
|
+
*
|
|
9
|
+
* The revision record carries the cell order, app config, etags, and
|
|
10
|
+
* version. Each cell in its order has a record in `cells`.
|
|
11
|
+
*/
|
|
12
|
+
export interface Document {
|
|
13
|
+
readonly revision: Protocol.Document;
|
|
14
|
+
readonly cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The records of a revision still being received. */
|
|
18
|
+
interface PendingDocument {
|
|
19
|
+
/** Null until the `document` event that closes the group arrives. */
|
|
20
|
+
readonly revision: Protocol.Document | null;
|
|
21
|
+
readonly cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The confirmed document and the revision being assembled after it.
|
|
26
|
+
*
|
|
27
|
+
* A snapshot's records are pending until `ready`. A live revision is pending
|
|
28
|
+
* from its first cell until its `document` event.
|
|
29
|
+
*/
|
|
30
|
+
interface DocumentState {
|
|
31
|
+
readonly current: Document | null;
|
|
32
|
+
readonly pending: PendingDocument | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pendingOf(state: DocumentState): PendingDocument {
|
|
36
|
+
return (
|
|
37
|
+
state.pending ?? {
|
|
38
|
+
revision: state.current?.revision ?? null,
|
|
39
|
+
cells: state.current?.cells ?? new Map(),
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Confirm a revision whose records are complete. */
|
|
45
|
+
function publish(
|
|
46
|
+
revision: Protocol.Document,
|
|
47
|
+
cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>,
|
|
48
|
+
): DocumentState {
|
|
49
|
+
const order = new Set(revision.order);
|
|
50
|
+
if (
|
|
51
|
+
order.size !== revision.order.length ||
|
|
52
|
+
order.size !== cells.size ||
|
|
53
|
+
[...order].some((id) => !cells.has(id)) ||
|
|
54
|
+
[...cells.values()].some((cell) => cell.version > revision.version)
|
|
55
|
+
) {
|
|
56
|
+
throw new Error("Document records do not match its revision and order");
|
|
57
|
+
}
|
|
58
|
+
return { current: { revision, cells }, pending: null };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const document: Resource<DocumentState, "cell" | "document"> = {
|
|
62
|
+
initial: { current: null, pending: null },
|
|
63
|
+
on: {
|
|
64
|
+
cell: (state, { data }) => {
|
|
65
|
+
const pending = pendingOf(state);
|
|
66
|
+
return {
|
|
67
|
+
...state,
|
|
68
|
+
pending: {
|
|
69
|
+
revision: pending.revision,
|
|
70
|
+
cells: new Map(pending.cells).set(data.cell.id, data.cell),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
"cell.removed": (state, { data }) => {
|
|
75
|
+
const pending = pendingOf(state);
|
|
76
|
+
if (!pending.cells.has(data.id)) {
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
const cells = new Map(pending.cells);
|
|
80
|
+
cells.delete(data.id);
|
|
81
|
+
return { ...state, pending: { ...pending, cells } };
|
|
82
|
+
},
|
|
83
|
+
document: (state, { data }, { phase }) => {
|
|
84
|
+
const { cells } = pendingOf(state);
|
|
85
|
+
return phase === "snapshot"
|
|
86
|
+
? { ...state, pending: { revision: data.document, cells } }
|
|
87
|
+
: publish(data.document, cells);
|
|
88
|
+
},
|
|
89
|
+
ready: (state, _event, { phase }) => {
|
|
90
|
+
if (state.pending === null) {
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
if (phase === "live") {
|
|
94
|
+
throw new Error("Stream ended with an incomplete document update");
|
|
95
|
+
}
|
|
96
|
+
return state.pending.revision === null
|
|
97
|
+
? { current: null, pending: null }
|
|
98
|
+
: publish(state.pending.revision, state.pending.cells);
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -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),
|