@marimo-team/frontend 0.23.16-dev4 → 0.23.16-dev41
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-BalYv0pK.js → CellStatus-C6Cj3oJc.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
- package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
- package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
- package/dist/assets/{JsonOutput-CkCD-FgM.js → JsonOutput-CyzvjIjx.js} +12 -12
- package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-CIuzXFFo.js} +4 -4
- package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
- package/dist/assets/RenderHTML-Cd-PP5ZR.js +1 -0
- package/dist/assets/{RunButton-BKRupqr7.js → RunButton-CnYWedW4.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-ByOBQT99.js} +8 -8
- package/dist/assets/add-connection-dialog-C97ylgk0.js +170 -0
- package/dist/assets/{agent-panel-CHB6t7Zy.js → agent-panel-Dxo9WUKV.js} +5 -5
- package/dist/assets/ai-model-dropdown-Bvqy98cI.js +5 -0
- package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
- package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
- package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-v1lG63C2.js} +1 -1
- package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
- package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
- package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
- package/dist/assets/{cell-editor-B5JVq7_q.js → cell-editor-BKrHbyX_.js} +16 -16
- package/dist/assets/cell-link-w5IbCQV4.js +1 -0
- package/dist/assets/cells-DzyDERZy.js +235 -0
- package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-Ccgk4vbE.js} +1 -1
- package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel--53TIc3t.js} +4 -4
- package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-B5rBxgsw.js} +4 -4
- package/dist/assets/{column-preview-Dne1dIt0.js → column-preview-Cb8R9kfl.js} +1 -1
- package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
- package/dist/assets/{command-palette-iOCqUIs1.js → command-palette-B1S9PjZi.js} +1 -1
- package/dist/assets/{common-CD6Mj8n6.js → common-CJ3baa6D.js} +1 -1
- package/dist/assets/components-DpUIWGaj.js +1 -0
- package/dist/assets/config-B-Q7J_6d.js +1 -0
- package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
- package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
- package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
- package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
- package/dist/assets/{datasource-ySAiC9gr.js → datasource-D_VcQMEn.js} +2 -2
- package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-DY5lvPDx.js} +3 -3
- package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
- package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
- package/dist/assets/dist-CiVShZKM.js +2 -0
- package/dist/assets/{documentation-panel-e9Fi9R7e.js → documentation-panel-CGhwxsXK.js} +1 -1
- package/dist/assets/download-DpS57jyt.js +9 -0
- package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
- package/dist/assets/{edit-page-7X3HYey7.js → edit-page-DA765yQV.js} +6 -6
- package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
- package/dist/assets/error-entries-CTYMwfsh.js +8 -0
- package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-DGAkG5kW.js} +1 -1
- package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
- package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
- package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
- package/dist/assets/{file-explorer-panel-Ntq1-VbF.js → file-explorer-panel-BKUo-MDh.js} +3 -3
- package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-DfWENcb8.js} +1 -1
- package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CNVZtm5K.js} +1 -1
- package/dist/assets/floating-outline-LitP0wCD.js +1 -0
- package/dist/assets/focus-CNKbm-Xz.js +1 -0
- package/dist/assets/{form-B6BkSCJb.js → form-nHzLTU1m.js} +2 -2
- package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
- package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
- package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
- package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
- package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
- package/dist/assets/home-page-DM9Z_IJ7.js +4 -0
- package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-JaxFFZcr.js} +2 -2
- package/dist/assets/index-C7_REKh1.js +38 -0
- package/dist/assets/index-D6Dp_PFj.css +2 -0
- package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
- package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
- package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
- package/dist/assets/{layout-CUlZijwG.js → layout-B29hqiiK.js} +5 -5
- package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
- package/dist/assets/logs-panel-CQRPYS8j.js +1 -0
- package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-sEayY9-m.js} +2 -2
- package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
- package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
- package/dist/assets/{name-cell-input-C6cukQ9k.js → name-cell-input-B-aj1KH3.js} +1 -1
- package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
- package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
- package/dist/assets/{outline-panel-BPlg9_CE.js → outline-panel-BOryk60D.js} +1 -1
- package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-DoYDLty3.js} +1 -1
- package/dist/assets/pair-with-agent-modal-BCRSACei.js +4 -0
- package/dist/assets/panels-Bb4fIHgH.js +4 -0
- package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
- package/dist/assets/process-output-d-LEMtGI.js +1 -0
- package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-DfioQthW.js} +1 -1
- package/dist/assets/{readonly-code-display-D63oRtBP.js → readonly-code-display-C9fu8tvK.js} +1 -1
- package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-DHxOnQGj.js} +1 -1
- package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
- package/dist/assets/{reveal-component-BLMTmd8T.js → reveal-component-5jf0W625.js} +60 -60
- package/dist/assets/{run-page-B6pgDYEJ.js → run-page-BMKx9ld_.js} +1 -1
- package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
- package/dist/assets/scratchpad-panel-C8z8vzBX.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
- package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
- package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
- package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
- package/dist/assets/{session-panel-Y0Oon0sD.js → session-panel-BVVk-02h.js} +1 -1
- package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
- package/dist/assets/{skeleton-D3QPxqWC.js → skeleton-Bubr9pIj.js} +1 -1
- package/dist/assets/slides-DkEgzTJB.css +1 -0
- package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-U2-iok-c.js} +1 -1
- package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
- package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
- package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
- package/dist/assets/state-DtziTd1T.js +3 -0
- package/dist/assets/{state-DBzEQtWz.js → state-DxQhUsG9.js} +1 -1
- package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
- package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-g8EXE911.js} +1 -1
- package/dist/assets/switch-OVajJBPR.js +1 -0
- package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
- package/dist/assets/{textarea-CpFfEUet.js → textarea-Bmzmbbpo.js} +1 -1
- package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
- package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
- package/dist/assets/{tracing-CKn3gWHw.js → tracing-B5XFvMmV.js} +1 -1
- package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-DnR2bYDe.js} +2 -2
- package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-Dx9CDaxV.js} +1 -1
- package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
- package/dist/assets/useCellActionButton-DypR3yDt.js +1 -0
- package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-vMXAr3rY.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-CwdwKf7A.js} +1 -1
- package/dist/assets/useEventListener-C0195JsF.js +1 -0
- package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
- package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
- package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
- package/dist/assets/useNotebookActions-jWIcmpv0.js +12 -0
- package/dist/assets/{useRunCells-CSSF_mJ0.js → useRunCells-C2dvU2jA.js} +1 -1
- package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-C1tubdNi.js} +1 -1
- package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
- package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
- package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
- package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
- package/dist/index.html +68 -68
- package/package.json +3 -4
- package/src/__mocks__/requests.ts +45 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +0 -4
- package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
- package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
- package/src/components/editor/actions/pdf-export.ts +1 -4
- package/src/components/editor/actions/useNotebookActions.tsx +7 -13
- package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
- package/src/components/editor/cell/code/cell-editor.tsx +13 -7
- package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
- package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +25 -19
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/core/cells/__tests__/cells.test.ts +12 -2
- package/src/core/cells/__tests__/logs.test.ts +5 -5
- package/src/core/cells/cells.ts +17 -4
- package/src/core/cells/document-changes.ts +1 -0
- package/src/core/cells/logs.ts +14 -4
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
- package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
- package/src/core/codemirror/copilot/language-server.ts +182 -70
- package/src/core/codemirror/copilot/transport.ts +15 -207
- package/src/core/codemirror/copilot/types.ts +9 -2
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +3 -0
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/python.ts +23 -11
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
- package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
- package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
- package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
- package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
- package/src/core/codemirror/lsp/log-messages.ts +37 -0
- package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
- package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
- package/src/core/codemirror/lsp/transports.ts +8 -1
- package/src/core/codemirror/lsp/types.ts +68 -8
- package/src/core/codemirror/lsp/utils.ts +14 -0
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +16 -0
- package/src/core/lsp/__tests__/transport.test.ts +509 -337
- package/src/core/lsp/transport.ts +304 -132
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
- package/src/core/network/__tests__/requests-network.test.ts +21 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +14 -8
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +22 -6
- package/src/core/rtc/state.ts +9 -0
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/bridge.ts +4 -2
- package/src/core/wasm/worker/types.ts +5 -2
- package/src/css/app/codemirror.css +16 -2
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/stories/log-viewer.stories.tsx +12 -6
- package/src/utils/__tests__/download.test.tsx +24 -1
- package/src/utils/download.ts +11 -6
- package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
- package/dist/assets/add-connection-dialog-B-jCRNTN.js +0 -170
- package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
- package/dist/assets/cell-link-DqBkyAFe.js +0 -1
- package/dist/assets/cells-Cf7bHHq3.js +0 -229
- package/dist/assets/components-BH6gDVv6.js +0 -1
- package/dist/assets/config-BbOTTCYY.js +0 -1
- package/dist/assets/dist-BCMBu0rO.js +0 -2
- package/dist/assets/download-BomYt3nl.js +0 -9
- package/dist/assets/error-entries-Birw-8VX.js +0 -8
- package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
- package/dist/assets/focus-DiEyfiAG.js +0 -1
- package/dist/assets/home-page-DJ4jJsZj.js +0 -4
- package/dist/assets/index-A71LAt6h.css +0 -2
- package/dist/assets/index-CMZzy-dF.js +0 -38
- package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
- package/dist/assets/logs-panel-iFV4ZJ9b.js +0 -1
- package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
- package/dist/assets/pair-with-agent-modal-CKOYwOYw.js +0 -4
- package/dist/assets/panels-Dj45fW-v.js +0 -4
- package/dist/assets/process-output-BEhIGfkO.js +0 -1
- package/dist/assets/scratchpad-panel-B0mEqBNk.js +0 -1
- package/dist/assets/slides-MCbMDtlY.css +0 -1
- package/dist/assets/state-BZQc7xxL.js +0 -3
- package/dist/assets/switch-CvweaCXq.js +0 -1
- package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
- package/dist/assets/useEventListener-B_w3A283.js +0 -1
- package/dist/assets/useNotebookActions-DTXAoukG.js +0 -12
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, render, screen } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
shouldShowScrollHint,
|
|
7
|
+
SlideScrollContainer,
|
|
8
|
+
} from "../slide-scroll-hint";
|
|
9
|
+
|
|
10
|
+
describe("shouldShowScrollHint", () => {
|
|
11
|
+
it("shows when content overflows and scroll is at the top", () => {
|
|
12
|
+
expect(
|
|
13
|
+
shouldShowScrollHint({
|
|
14
|
+
scrollHeight: 800,
|
|
15
|
+
clientHeight: 400,
|
|
16
|
+
scrollTop: 0,
|
|
17
|
+
}),
|
|
18
|
+
).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("hides when content fits", () => {
|
|
22
|
+
expect(
|
|
23
|
+
shouldShowScrollHint({
|
|
24
|
+
scrollHeight: 300,
|
|
25
|
+
clientHeight: 400,
|
|
26
|
+
scrollTop: 0,
|
|
27
|
+
}),
|
|
28
|
+
).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("hides after the user scrolls down", () => {
|
|
32
|
+
expect(
|
|
33
|
+
shouldShowScrollHint({
|
|
34
|
+
scrollHeight: 800,
|
|
35
|
+
clientHeight: 400,
|
|
36
|
+
scrollTop: 40,
|
|
37
|
+
}),
|
|
38
|
+
).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("ignores sub-pixel overflow", () => {
|
|
42
|
+
expect(
|
|
43
|
+
shouldShowScrollHint({
|
|
44
|
+
scrollHeight: 401,
|
|
45
|
+
clientHeight: 400,
|
|
46
|
+
scrollTop: 0,
|
|
47
|
+
}),
|
|
48
|
+
).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("still shows within the top threshold", () => {
|
|
52
|
+
expect(
|
|
53
|
+
shouldShowScrollHint({
|
|
54
|
+
scrollHeight: 800,
|
|
55
|
+
clientHeight: 400,
|
|
56
|
+
scrollTop: 8,
|
|
57
|
+
}),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("SlideScrollContainer", () => {
|
|
63
|
+
const resizeObservers: Array<{
|
|
64
|
+
callback: ResizeObserverCallback;
|
|
65
|
+
observe: ReturnType<typeof vi.fn>;
|
|
66
|
+
disconnect: ReturnType<typeof vi.fn>;
|
|
67
|
+
}> = [];
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
resizeObservers.length = 0;
|
|
71
|
+
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
|
72
|
+
cb(0);
|
|
73
|
+
return 0;
|
|
74
|
+
});
|
|
75
|
+
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
|
76
|
+
vi.stubGlobal(
|
|
77
|
+
"ResizeObserver",
|
|
78
|
+
class MockResizeObserver {
|
|
79
|
+
callback: ResizeObserverCallback;
|
|
80
|
+
observe = vi.fn();
|
|
81
|
+
unobserve = vi.fn();
|
|
82
|
+
disconnect = vi.fn();
|
|
83
|
+
|
|
84
|
+
constructor(callback: ResizeObserverCallback) {
|
|
85
|
+
this.callback = callback;
|
|
86
|
+
resizeObservers.push(this);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
vi.stubGlobal(
|
|
91
|
+
"MutationObserver",
|
|
92
|
+
class MockMutationObserver {
|
|
93
|
+
observe = vi.fn();
|
|
94
|
+
disconnect = vi.fn();
|
|
95
|
+
takeRecords = vi.fn(() => []);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
vi.unstubAllGlobals();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function stubOverflow(
|
|
105
|
+
el: HTMLElement,
|
|
106
|
+
metrics: {
|
|
107
|
+
scrollHeight: number;
|
|
108
|
+
clientHeight: number;
|
|
109
|
+
scrollTop?: number;
|
|
110
|
+
},
|
|
111
|
+
) {
|
|
112
|
+
Object.defineProperty(el, "scrollHeight", {
|
|
113
|
+
configurable: true,
|
|
114
|
+
get: () => metrics.scrollHeight,
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(el, "clientHeight", {
|
|
117
|
+
configurable: true,
|
|
118
|
+
get: () => metrics.clientHeight,
|
|
119
|
+
});
|
|
120
|
+
Object.defineProperty(el, "scrollTop", {
|
|
121
|
+
configurable: true,
|
|
122
|
+
get: () => metrics.scrollTop ?? 0,
|
|
123
|
+
set: (value: number) => {
|
|
124
|
+
metrics.scrollTop = value;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it("renders a scroll hint when content overflows", () => {
|
|
130
|
+
render(
|
|
131
|
+
<SlideScrollContainer>
|
|
132
|
+
<div>tall content</div>
|
|
133
|
+
</SlideScrollContainer>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const container = screen.getByTestId("slide-scroll-container");
|
|
137
|
+
stubOverflow(container, { scrollHeight: 900, clientHeight: 400 });
|
|
138
|
+
|
|
139
|
+
act(() => {
|
|
140
|
+
for (const observer of resizeObservers) {
|
|
141
|
+
observer.callback([], observer as unknown as ResizeObserver);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(screen.getByTestId("slide-scroll-hint")).toBeTruthy();
|
|
146
|
+
expect(screen.getByText("Scroll for more")).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("hides the hint after scrolling", () => {
|
|
150
|
+
const metrics = {
|
|
151
|
+
scrollHeight: 900,
|
|
152
|
+
clientHeight: 400,
|
|
153
|
+
scrollTop: 0,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
render(
|
|
157
|
+
<SlideScrollContainer>
|
|
158
|
+
<div>tall content</div>
|
|
159
|
+
</SlideScrollContainer>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const container = screen.getByTestId("slide-scroll-container");
|
|
163
|
+
stubOverflow(container, metrics);
|
|
164
|
+
|
|
165
|
+
act(() => {
|
|
166
|
+
for (const observer of resizeObservers) {
|
|
167
|
+
observer.callback([], observer as unknown as ResizeObserver);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
expect(screen.getByTestId("slide-scroll-hint")).toBeTruthy();
|
|
171
|
+
|
|
172
|
+
act(() => {
|
|
173
|
+
metrics.scrollTop = 50;
|
|
174
|
+
container.dispatchEvent(new Event("scroll"));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(screen.queryByTestId("slide-scroll-hint")).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("does not show a hint when content fits", () => {
|
|
181
|
+
render(
|
|
182
|
+
<SlideScrollContainer>
|
|
183
|
+
<div>short content</div>
|
|
184
|
+
</SlideScrollContainer>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const container = screen.getByTestId("slide-scroll-container");
|
|
188
|
+
stubOverflow(container, { scrollHeight: 200, clientHeight: 400 });
|
|
189
|
+
|
|
190
|
+
act(() => {
|
|
191
|
+
for (const observer of resizeObservers) {
|
|
192
|
+
observer.callback([], observer as unknown as ResizeObserver);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(screen.queryByTestId("slide-scroll-hint")).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -13,7 +13,9 @@ import useEvent from "react-use-event-hook";
|
|
|
13
13
|
import { CodeIcon, ExpandIcon, EyeOffIcon } from "lucide-react";
|
|
14
14
|
import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
|
|
15
15
|
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
|
16
|
+
import { cellDomProps } from "@/components/editor/common";
|
|
16
17
|
import { Slide as CellOutputSlide } from "@/components/slides/slide";
|
|
18
|
+
import { SlideScrollContainer } from "@/components/slides/slide-scroll-hint";
|
|
17
19
|
import { Button } from "@/components/ui/button";
|
|
18
20
|
import { useFullScreenElement } from "@/components/ui/fullscreen";
|
|
19
21
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
@@ -303,6 +305,25 @@ function resolveSlideContentStyle(
|
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Renders a cell's output wrapped in a `data-cell-id`/`data-cell-name` div
|
|
310
|
+
* (see `cellDomProps`), so it can be targeted by custom CSS in the slides
|
|
311
|
+
* view just like it can in the vertical layout. The wrapper is required
|
|
312
|
+
* because `CellOutputSlide` sets its own `id` from `CellOutputId` (a
|
|
313
|
+
* different scheme than `HTMLCellId`) and its inner `OutputArea` uses
|
|
314
|
+
* `display: contents`, which can't be targeted for background/border styling.
|
|
315
|
+
*/
|
|
316
|
+
export const CellOutputBlock = ({ cell }: { cell: RuntimeCell }) => (
|
|
317
|
+
<div className="flex flex-col" {...cellDomProps(cell.id, cell.name)}>
|
|
318
|
+
<CellOutputSlide
|
|
319
|
+
cellId={cell.id}
|
|
320
|
+
status={cell.status}
|
|
321
|
+
output={cell.output}
|
|
322
|
+
stale={outputIsStale(cell, false)}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
|
|
306
327
|
const SubslideView = ({
|
|
307
328
|
subslide,
|
|
308
329
|
resolveShowCode,
|
|
@@ -327,7 +348,7 @@ const SubslideView = ({
|
|
|
327
348
|
|
|
328
349
|
return (
|
|
329
350
|
<Slide>
|
|
330
|
-
<
|
|
351
|
+
<SlideScrollContainer>
|
|
331
352
|
<div
|
|
332
353
|
className={
|
|
333
354
|
anyCodeShown
|
|
@@ -339,15 +360,7 @@ const SubslideView = ({
|
|
|
339
360
|
{subslide.blocks.map((block, i) => {
|
|
340
361
|
const rendered = block.cells.map((cell) => {
|
|
341
362
|
if (!resolveShowCode(cell.id)) {
|
|
342
|
-
return
|
|
343
|
-
<CellOutputSlide
|
|
344
|
-
key={cell.id}
|
|
345
|
-
cellId={cell.id}
|
|
346
|
-
status={cell.status}
|
|
347
|
-
output={cell.output}
|
|
348
|
-
stale={outputIsStale(cell, false)}
|
|
349
|
-
/>
|
|
350
|
-
);
|
|
363
|
+
return <CellOutputBlock key={cell.id} cell={cell} />;
|
|
351
364
|
}
|
|
352
365
|
return isEditable ? (
|
|
353
366
|
<SlideCellView key={cell.id} cell={cell} />
|
|
@@ -367,7 +380,7 @@ const SubslideView = ({
|
|
|
367
380
|
return <ReactFragment key={i}>{rendered}</ReactFragment>;
|
|
368
381
|
})}
|
|
369
382
|
</div>
|
|
370
|
-
</
|
|
383
|
+
</SlideScrollContainer>
|
|
371
384
|
{/* Outside any `.fragment`: shown only before any fragment is revealed. */}
|
|
372
385
|
{slideLevel && <NotesAside text={slideLevel} />}
|
|
373
386
|
</Slide>
|
|
@@ -405,14 +418,7 @@ const ParkedPreviewContent = ({
|
|
|
405
418
|
<SlideCellReadOnlyView cell={cell} />
|
|
406
419
|
);
|
|
407
420
|
}
|
|
408
|
-
return
|
|
409
|
-
<CellOutputSlide
|
|
410
|
-
cellId={cell.id}
|
|
411
|
-
status={cell.status}
|
|
412
|
-
output={cell.output}
|
|
413
|
-
stale={outputIsStale(cell, false)}
|
|
414
|
-
/>
|
|
415
|
-
);
|
|
421
|
+
return <CellOutputBlock cell={cell} />;
|
|
416
422
|
};
|
|
417
423
|
|
|
418
424
|
// There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
|
|
@@ -202,7 +202,7 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
|
|
|
202
202
|
);
|
|
203
203
|
|
|
204
204
|
const editor = (
|
|
205
|
-
<div className="marimo-cell">
|
|
205
|
+
<div className="marimo-cell" {...cellDomProps(cell.id, cell.name)}>
|
|
206
206
|
<ReadonlyCode
|
|
207
207
|
code={display.code}
|
|
208
208
|
language={display.language}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
type ReactNode,
|
|
6
|
+
type RefObject,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cn } from "@/utils/cn";
|
|
12
|
+
|
|
13
|
+
/** Pixels of scroll before the hint is considered dismissed. */
|
|
14
|
+
const TOP_THRESHOLD_PX = 8;
|
|
15
|
+
/** Ignore sub-pixel overflow from rounding / borders. */
|
|
16
|
+
const OVERFLOW_TOLERANCE_PX = 1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether a scroll container should show a "more content below" affordance.
|
|
20
|
+
* Visible only when content overflows and the viewport is still near the top.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldShowScrollHint(options: {
|
|
23
|
+
scrollHeight: number;
|
|
24
|
+
clientHeight: number;
|
|
25
|
+
scrollTop: number;
|
|
26
|
+
}): boolean {
|
|
27
|
+
const { scrollHeight, clientHeight, scrollTop } = options;
|
|
28
|
+
const overflowing = scrollHeight > clientHeight + OVERFLOW_TOLERANCE_PX;
|
|
29
|
+
const atTop = scrollTop <= TOP_THRESHOLD_PX;
|
|
30
|
+
return overflowing && atTop;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Measure only up to the first unrevealed fragment so
|
|
35
|
+
* the hint reflects currently revealed content, not future fragment steps.
|
|
36
|
+
*/
|
|
37
|
+
function measureRevealedHeight(el: HTMLElement): number {
|
|
38
|
+
const firstUnrevealed = el.querySelector<HTMLElement>(
|
|
39
|
+
".fragment:not(.visible)",
|
|
40
|
+
);
|
|
41
|
+
if (!firstUnrevealed) {
|
|
42
|
+
return el.scrollHeight;
|
|
43
|
+
}
|
|
44
|
+
const fragmentTop = firstUnrevealed.getBoundingClientRect().top;
|
|
45
|
+
return fragmentTop - el.getBoundingClientRect().top + el.scrollTop;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tracks whether a scroll container is overflowing at the top, so we can
|
|
50
|
+
* surface a scroll affordance.
|
|
51
|
+
*/
|
|
52
|
+
function useScrollHint(
|
|
53
|
+
scrollRef: RefObject<HTMLElement | null>,
|
|
54
|
+
contentRef: RefObject<HTMLElement | null>,
|
|
55
|
+
): boolean {
|
|
56
|
+
const [showHint, setShowHint] = useState(false);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const el = scrollRef.current;
|
|
60
|
+
const content = contentRef.current;
|
|
61
|
+
if (!el || !content) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let frame = 0;
|
|
66
|
+
const update = () => {
|
|
67
|
+
cancelAnimationFrame(frame);
|
|
68
|
+
frame = requestAnimationFrame(() => {
|
|
69
|
+
setShowHint(
|
|
70
|
+
shouldShowScrollHint({
|
|
71
|
+
scrollHeight: measureRevealedHeight(el),
|
|
72
|
+
clientHeight: el.clientHeight,
|
|
73
|
+
scrollTop: el.scrollTop,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
update();
|
|
80
|
+
|
|
81
|
+
const resizeObserver = new ResizeObserver(update);
|
|
82
|
+
resizeObserver.observe(el);
|
|
83
|
+
resizeObserver.observe(content);
|
|
84
|
+
|
|
85
|
+
// Fragments reveal via class changes (opacity/visibility, no layout shift),
|
|
86
|
+
// which the ResizeObserver can't see, so re-check on class mutations. Only
|
|
87
|
+
// attach on slides that have fragments to avoid reacting to unrelated class
|
|
88
|
+
// churn (e.g. animating widgets) on the common non-fragment slide.
|
|
89
|
+
const fragmentObserver = content.querySelector(".fragment")
|
|
90
|
+
? new MutationObserver(update)
|
|
91
|
+
: null;
|
|
92
|
+
fragmentObserver?.observe(content, {
|
|
93
|
+
subtree: true,
|
|
94
|
+
attributes: true,
|
|
95
|
+
attributeFilter: ["class"],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
cancelAnimationFrame(frame);
|
|
102
|
+
resizeObserver.disconnect();
|
|
103
|
+
fragmentObserver?.disconnect();
|
|
104
|
+
el.removeEventListener("scroll", update);
|
|
105
|
+
};
|
|
106
|
+
}, [scrollRef, contentRef]);
|
|
107
|
+
|
|
108
|
+
return showHint;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ScrollHintOverlay = ({ className }: { className?: string }) => {
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
className={cn(
|
|
115
|
+
"pointer-events-none absolute inset-x-0 bottom-0 z-10 flex flex-col items-center",
|
|
116
|
+
"animate-in fade-in-0 duration-200",
|
|
117
|
+
className,
|
|
118
|
+
)}
|
|
119
|
+
data-testid="slide-scroll-hint"
|
|
120
|
+
aria-hidden={true}
|
|
121
|
+
>
|
|
122
|
+
<div className="h-16 w-full bg-linear-to-t from-background via-background/80 to-transparent" />
|
|
123
|
+
<div className="absolute bottom-3 flex items-center gap-1 rounded-full border border-border/60 bg-background/90 px-2.5 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur-sm">
|
|
124
|
+
<ChevronDownIcon className="h-3.5 w-3.5" />
|
|
125
|
+
<span>Scroll for more</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Full-height scroll container for a slide. Shows a dismissible "Scroll for
|
|
133
|
+
* more" cue when content overflows the frame (easy to miss in fullscreen when
|
|
134
|
+
* OS overlay scrollbars are hidden).
|
|
135
|
+
*/
|
|
136
|
+
export const SlideScrollContainer = ({
|
|
137
|
+
children,
|
|
138
|
+
className,
|
|
139
|
+
}: {
|
|
140
|
+
children: ReactNode;
|
|
141
|
+
className?: string;
|
|
142
|
+
}) => {
|
|
143
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
144
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
145
|
+
const showHint = useScrollHint(scrollRef, contentRef);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className={cn("relative h-full w-full", className)}>
|
|
149
|
+
<div
|
|
150
|
+
ref={scrollRef}
|
|
151
|
+
className="h-full w-full overflow-auto"
|
|
152
|
+
data-testid="slide-scroll-container"
|
|
153
|
+
>
|
|
154
|
+
<div ref={contentRef} className="flex w-full min-h-full">
|
|
155
|
+
{children}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
{showHint && <ScrollHintOverlay />}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
width: unset;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/* If the first output is the only child, make it flex 1
|
|
19
|
-
|
|
18
|
+
/* If the first output is the only child, make it flex 1. Cells are wrapped
|
|
19
|
+
in a `data-cell-id`/`data-cell-name` div (see `cellDomProps`), so `.output`
|
|
20
|
+
is a grandchild rather than a direct child here. */
|
|
21
|
+
> *:only-child:has(> .output:only-child, > * > .output:only-child) {
|
|
20
22
|
flex: 1;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -40,7 +40,7 @@ export const ShareStaticNotebookModal: React.FC<{
|
|
|
40
40
|
e.preventDefault();
|
|
41
41
|
|
|
42
42
|
onClose();
|
|
43
|
-
const html = await exportAsHTML({
|
|
43
|
+
const { contents: html } = await exportAsHTML({
|
|
44
44
|
download: false,
|
|
45
45
|
includeCode: true,
|
|
46
46
|
files: VirtualFileTracker.INSTANCE.filenames(),
|
|
@@ -195,7 +195,7 @@ export const OnBlurredInput = React.forwardRef<
|
|
|
195
195
|
onChange={(event) => setInternalValue(event.target.value)}
|
|
196
196
|
onBlur={() => setValue(internalValue || "")}
|
|
197
197
|
onKeyDown={(event) => {
|
|
198
|
-
if (event.key !== "Enter") {
|
|
198
|
+
if (event.key !== "Enter" || event.shiftKey) {
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
setValue(internalValue || "");
|
|
@@ -1870,8 +1870,18 @@ describe("cell reducer", () => {
|
|
|
1870
1870
|
|
|
1871
1871
|
it("can clear logs", () => {
|
|
1872
1872
|
state.cellLogs = [
|
|
1873
|
-
{
|
|
1874
|
-
|
|
1873
|
+
{
|
|
1874
|
+
level: "stderr",
|
|
1875
|
+
message: "log1",
|
|
1876
|
+
timestamp: 0,
|
|
1877
|
+
source: { type: "cell", cellId: firstCellId },
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
level: "stderr",
|
|
1881
|
+
message: "log1",
|
|
1882
|
+
timestamp: 0,
|
|
1883
|
+
source: { type: "cell", cellId: firstCellId },
|
|
1884
|
+
},
|
|
1875
1885
|
];
|
|
1876
1886
|
actions.clearLogs();
|
|
1877
1887
|
expect(state.cellLogs).toEqual([]);
|
|
@@ -46,7 +46,7 @@ describe("getCellLogsForMessage", () => {
|
|
|
46
46
|
timestamp: 1_234_567_890,
|
|
47
47
|
level: "stdout",
|
|
48
48
|
message: "Hello, World!",
|
|
49
|
-
cellId: "cell-1",
|
|
49
|
+
source: { type: "cell", cellId: "cell-1" },
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -74,7 +74,7 @@ describe("getCellLogsForMessage", () => {
|
|
|
74
74
|
timestamp: 1_234_567_890,
|
|
75
75
|
level: "stderr",
|
|
76
76
|
message: "Error occurred",
|
|
77
|
-
cellId: "cell-2",
|
|
77
|
+
source: { type: "cell", cellId: "cell-2" },
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -102,7 +102,7 @@ describe("getCellLogsForMessage", () => {
|
|
|
102
102
|
timestamp: 1_234_567_890,
|
|
103
103
|
level: "stdout",
|
|
104
104
|
message: "Error: Something went wrong",
|
|
105
|
-
cellId: "cell-3",
|
|
105
|
+
source: { type: "cell", cellId: "cell-3" },
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
|
|
@@ -130,7 +130,7 @@ describe("getCellLogsForMessage", () => {
|
|
|
130
130
|
timestamp: 1_234_567_890,
|
|
131
131
|
level: "stderr",
|
|
132
132
|
message: "Critical Error: System failure",
|
|
133
|
-
cellId: "cell-4",
|
|
133
|
+
source: { type: "cell", cellId: "cell-4" },
|
|
134
134
|
});
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -157,7 +157,7 @@ describe("getCellLogsForMessage", () => {
|
|
|
157
157
|
expect(logs[0].level).toBe("stderr"); // marimo-error should be treated as stderr
|
|
158
158
|
expect(logs[0].message).toContain("Traceback (most recent call last):");
|
|
159
159
|
expect(logs[0].message).toContain('File "test.py", line 1');
|
|
160
|
-
expect(logs[0].
|
|
160
|
+
expect(logs[0].source).toEqual({ type: "cell", cellId: "cell-5" });
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
test("handles multiple console outputs with different MIME types", () => {
|
package/src/core/cells/cells.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { findCollapseRange, mergeOutlines } from "../dom/outline";
|
|
|
28
28
|
import type { CellMessage } from "../kernel/messages";
|
|
29
29
|
import { isErrorMime } from "../mime";
|
|
30
30
|
import type { CellConfig } from "../network/types";
|
|
31
|
-
import { isRtcEnabled } from "../rtc/state";
|
|
31
|
+
import { canUseRtc, isRtcEnabled } from "../rtc/state";
|
|
32
32
|
import { createDeepEqualAtom, store } from "../state/jotai";
|
|
33
33
|
import { isWasm } from "../wasm/utils";
|
|
34
34
|
import { prepareCellForExecution, transitionCell } from "./cell";
|
|
@@ -927,6 +927,12 @@ const {
|
|
|
927
927
|
cellLogs: [...nextState.cellLogs, ...getCellLogsForMessage(message)],
|
|
928
928
|
};
|
|
929
929
|
},
|
|
930
|
+
addLogs: (state, action: { logs: CellLog[] }) => {
|
|
931
|
+
return {
|
|
932
|
+
...state,
|
|
933
|
+
cellLogs: [...state.cellLogs, ...action.logs],
|
|
934
|
+
};
|
|
935
|
+
},
|
|
930
936
|
setCellIds: (state, action: { cellIds: CellId[] }) => {
|
|
931
937
|
const isTheSame = isEqual(state.cellIds.inOrderIds, action.cellIds);
|
|
932
938
|
if (isTheSame) {
|
|
@@ -1037,9 +1043,9 @@ const {
|
|
|
1037
1043
|
};
|
|
1038
1044
|
}
|
|
1039
1045
|
|
|
1040
|
-
// Update codemirror if mounted
|
|
1041
|
-
//
|
|
1042
|
-
if (!isRtcEnabled()) {
|
|
1046
|
+
// Update codemirror if mounted. RTC-backed cells sync via Loro; others
|
|
1047
|
+
// (e.g. scratchpad) still need an explicit editor update.
|
|
1048
|
+
if (!isRtcEnabled() || !canUseRtc(cellId)) {
|
|
1043
1049
|
const cellHandle = nextState.cellHandles[cellId].current;
|
|
1044
1050
|
if (cellHandle?.editorViewOrNull) {
|
|
1045
1051
|
updateEditorCodeFromPython(cellHandle.editorViewOrNull, code);
|
|
@@ -1698,6 +1704,13 @@ export {
|
|
|
1698
1704
|
notebookAtom,
|
|
1699
1705
|
};
|
|
1700
1706
|
|
|
1707
|
+
/** Actions for dispatching outside of React. */
|
|
1708
|
+
const imperativeActions = createActions((action) => {
|
|
1709
|
+
store.set(notebookAtom, (state) => reducer(state, action));
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
export const addLogs = imperativeActions.addLogs;
|
|
1713
|
+
|
|
1701
1714
|
/// ATOMS
|
|
1702
1715
|
|
|
1703
1716
|
export const cellIdsAtom = atom((get) => get(notebookAtom).cellIds);
|
package/src/core/cells/logs.ts
CHANGED
|
@@ -13,11 +13,21 @@ import { tracebackModalAtom } from "../errors/traceback-atom";
|
|
|
13
13
|
import React from "react";
|
|
14
14
|
import { ToastAction } from "@/components/ui/toast";
|
|
15
15
|
|
|
16
|
+
/** Logs come from a cell, or from a language server, which has no cell. */
|
|
17
|
+
export type LogSource =
|
|
18
|
+
| { type: "cell"; cellId: CellId }
|
|
19
|
+
| { type: "lsp"; name: string };
|
|
20
|
+
|
|
16
21
|
export interface CellLog {
|
|
22
|
+
/** Unix timestamp, in seconds. */
|
|
17
23
|
timestamp: number;
|
|
18
24
|
level: "stdout" | "stderr";
|
|
19
25
|
message: string;
|
|
20
|
-
|
|
26
|
+
source: LogSource;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function logSourceLabel(source: LogSource): string {
|
|
30
|
+
return source.type === "cell" ? source.cellId : `lsp:${source.name}`;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
let didAlreadyToastError = false;
|
|
@@ -48,7 +58,7 @@ export function getCellLogsForMessage(cell: CellMessage): CellLog[] {
|
|
|
48
58
|
timestamp: output.timestamp || Date.now(),
|
|
49
59
|
level: isError ? "stderr" : "stdout",
|
|
50
60
|
message: message,
|
|
51
|
-
cellId: cell.cell_id,
|
|
61
|
+
source: { type: "cell", cellId: cell.cell_id },
|
|
52
62
|
});
|
|
53
63
|
break;
|
|
54
64
|
}
|
|
@@ -71,7 +81,7 @@ export function getCellLogsForMessage(cell: CellMessage): CellLog[] {
|
|
|
71
81
|
cell.output.data.forEach((error) => {
|
|
72
82
|
CellLogLogger.log({
|
|
73
83
|
level: "stderr",
|
|
74
|
-
cellId: cell.cell_id,
|
|
84
|
+
source: { type: "cell", cellId: cell.cell_id },
|
|
75
85
|
timestamp: cell.timestamp ?? 0,
|
|
76
86
|
message: JSON.stringify(error),
|
|
77
87
|
});
|
|
@@ -150,7 +160,7 @@ const CellLogLogger = {
|
|
|
150
160
|
`%c[${status}]`,
|
|
151
161
|
`color:${color}; padding:2px 0; border-radius:2px; font-weight:bold`,
|
|
152
162
|
`[${formatLogTimestamp(payload.timestamp)}]`,
|
|
153
|
-
`(${payload.
|
|
163
|
+
`(${logSourceLabel(payload.source)}) ${payload.message}`,
|
|
154
164
|
);
|
|
155
165
|
},
|
|
156
166
|
};
|