@marimo-team/islands 0.19.8-dev3 → 0.19.8-dev32
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/{Combination-Bg-xN8JV.js → Combination-BTMrlhzT.js} +11 -10
- package/dist/{ConnectedDataExplorerComponent-DewsKLl2.js → ConnectedDataExplorerComponent-BAeQ8DWw.js} +11 -11
- package/dist/{ImageComparisonComponent-Bijp8beW.js → ImageComparisonComponent-DkEXPki_.js} +2 -2
- package/dist/{any-language-editor-DZc6NCTp.js → any-language-editor-D0UQItkS.js} +6 -6
- package/dist/{architectureDiagram-VXUJARFQ--NkyBn9Y.js → architectureDiagram-VXUJARFQ-DPPYVq8H.js} +4 -4
- package/dist/assets/__vite-browser-external-6-UwTyQC.js +1 -0
- package/dist/assets/{worker-SqntmiwV.js → worker-D3e5wDxM.js} +4 -4
- package/dist/{blockDiagram-VD42YOAC-DEZZaTW0.js → blockDiagram-VD42YOAC-BA5N05Y9.js} +4 -4
- package/dist/{button-BWvsJ2Wr.js → button-Cy0ElmIm.js} +2 -2
- package/dist/{c4Diagram-YG6GDRKO-Bj7hwWCO.js → c4Diagram-YG6GDRKO-DJLzuGJJ.js} +3 -3
- package/dist/{channel-B_QrFrGg.js → channel-Dob5kWXR.js} +1 -1
- package/dist/{check-CM_kewwn.js → check-DkNR52Mm.js} +1 -1
- package/dist/{chunk-5FQGJX7Z-D5VFKHmt.js → chunk-5FQGJX7Z-BEb20Lzt.js} +3 -3
- package/dist/{chunk-ABZYJK2D-SZPYmRzN.js → chunk-ABZYJK2D-BXTC53mt.js} +1 -1
- package/dist/{chunk-ATLVNIR6-BI_WwH1o.js → chunk-ATLVNIR6-BJDjUR_c.js} +1 -1
- package/dist/{chunk-B4BG7PRW-BlI9Gm1l.js → chunk-B4BG7PRW-DzmUUpfH.js} +4 -4
- package/dist/{chunk-DI55MBZ5-BXxemMn5.js → chunk-DI55MBZ5-gTd3J8Tu.js} +4 -4
- package/dist/{chunk-EXTU4WIE-CzWtDV99.js → chunk-EXTU4WIE-DyoOs5QX.js} +1 -1
- package/dist/{chunk-JA3XYJ7Z-DQ-2ARfa.js → chunk-JA3XYJ7Z-BGnAIbOP.js} +2 -2
- package/dist/{chunk-JZLCHNYA-CVfjf2vv.js → chunk-JZLCHNYA-CIRgweVQ.js} +4 -4
- package/dist/{chunk-N4CR4FBY-BCZvQ7Jq.js → chunk-N4CR4FBY-DKSvXAIS.js} +5 -5
- package/dist/{chunk-QN33PNHL-DY_2Q2zl.js → chunk-QN33PNHL-B6zC8BTi.js} +1 -1
- package/dist/{chunk-QXUST7PY-BMCjAVR_.js → chunk-QXUST7PY-C7750n_u.js} +5 -5
- package/dist/{chunk-S3R3BYOJ-Ddu0H4Qa.js → chunk-S3R3BYOJ-CBkH6JZZ.js} +1 -1
- package/dist/{chunk-TZMSLE5B-C2wVlbMl.js → chunk-TZMSLE5B-DObGL7xi.js} +1 -1
- package/dist/{classDiagram-2ON5EDUG-D-g7zbyO.js → classDiagram-2ON5EDUG-B9pkKjjc.js} +9 -9
- package/dist/{classDiagram-v2-WZHVMYZB-C7v5zNRD.js → classDiagram-v2-WZHVMYZB-CRhhA0tV.js} +9 -9
- package/dist/{click-outside-container-BCN5BtVO.js → click-outside-container-DNfggvIW.js} +1 -1
- package/dist/{code-block-37QAKDTI-eUgXqGNG.js → code-block-37QAKDTI-u5kgjqmr.js} +2 -2
- package/dist/{compiler-runtime-DHFVbq0b.js → compiler-runtime-B_OLMU9S.js} +1 -1
- package/dist/{copy-B59Bw3-w.js → copy-DRaXIb_a.js} +3 -3
- package/dist/{dagre-6UL2VRFP-DKIPL74O.js → dagre-6UL2VRFP-C2C2XxsB.js} +6 -6
- package/dist/{data-grid-overlay-editor-COyFwFmE.js → data-grid-overlay-editor-BXqtz1ia.js} +4 -4
- package/dist/{diagram-PSM6KHXK-CVTrAZaP.js → diagram-PSM6KHXK-DHBY-94p.js} +5 -5
- package/dist/{diagram-QEK2KX5R-BqHBzu3x.js → diagram-QEK2KX5R-CgMshOwn.js} +3 -3
- package/dist/{diagram-S2PKOQOG-CJD6owcg.js → diagram-S2PKOQOG-F1KPva3Y.js} +3 -3
- package/dist/{dist-Co5PD8Fb.js → dist-BBYTEAvO.js} +1 -1
- package/dist/{erDiagram-Q2GNP2WA-CqOceSf9.js → erDiagram-Q2GNP2WA-18gGng8V.js} +9 -9
- package/dist/{error-banner-C7KLpECd.js → error-banner-D2zjeN_a.js} +5 -5
- package/dist/{esm-D4WO8J3G.js → esm-CgRNPmz8.js} +6 -6
- package/dist/{flowDiagram-NV44I4VS-K7-DUifo.js → flowDiagram-NV44I4VS-iHFiHYe0.js} +9 -9
- package/dist/{ganttDiagram-JELNMOA3-BwUFY9Nu.js → ganttDiagram-JELNMOA3-D7GixxiF.js} +2 -2
- package/dist/{gitGraphDiagram-NY62KEGX-CjGRtLb1.js → gitGraphDiagram-NY62KEGX-CJFHytRK.js} +2 -2
- package/dist/{glide-data-editor-C3T7HsLi.js → glide-data-editor-BYwb17Bf.js} +13 -13
- package/dist/{infoDiagram-WHAUD3N6-DNhmDn-6.js → infoDiagram-WHAUD3N6-B5Lkh3A9.js} +2 -2
- package/dist/{journeyDiagram-XKPGCS4Q-BOdK47P8.js → journeyDiagram-XKPGCS4Q-CV_9R9iP.js} +2 -2
- package/dist/{kanban-definition-3W4ZIXB7-A0JC9d0g.js → kanban-definition-3W4ZIXB7-Dp21D5Ym.js} +6 -6
- package/dist/{katex-DJyOeQ91.js → katex-CX2BKujk.js} +1 -1
- package/dist/{katex-Dm9nZf6A.js → katex-Db0k5oV_.js} +1 -1
- package/dist/{label-C4PtQcza.js → label-CxU5JNBW.js} +6 -6
- package/dist/main.js +282 -193
- package/dist/mermaid-4DMBBIKO-BhDCqnO1.js +6 -0
- package/dist/{mermaid-Bqp2Xw99.js → mermaid-B__BZSXU.js} +39 -39
- package/dist/{mhchem-BqdXeZVX.js → mhchem-w1tkUnWr.js} +1 -1
- package/dist/{mindmap-definition-VGOIOE7T-CS6nKN_L.js → mindmap-definition-VGOIOE7T-B_5mfdYp.js} +8 -8
- package/dist/{number-overlay-editor-Bz_bDJQb.js → number-overlay-editor-D-4WQAGX.js} +2 -2
- package/dist/{pieDiagram-ADFJNKIX-DSa60Grk.js → pieDiagram-ADFJNKIX-B-DGEopK.js} +3 -3
- package/dist/{quadrantDiagram-AYHSOK5B-CFnMbP2J.js → quadrantDiagram-AYHSOK5B-M_yRSIZn.js} +1 -1
- package/dist/{react-DdA8EBol.js → react-Bs6Z0kvn.js} +1 -1
- package/dist/{react-dom-DJW8xUDg.js → react-dom-CqtLRVZP.js} +2 -2
- package/dist/{react-plotly-jVjTu07w.js → react-plotly-BuRa9xtI.js} +1 -1
- package/dist/{react-vega-DgHpnZ04.js → react-vega-3WcLHYC7.js} +2 -2
- package/dist/{react-vega-CjiPWyw0.js → react-vega-DLFvGrpJ.js} +1 -1
- package/dist/{requirementDiagram-UZGBJVZJ-ytLQrFTk.js → requirementDiagram-UZGBJVZJ-9Wt82hOZ.js} +8 -8
- package/dist/{sankeyDiagram-TZEHDZUN-KQqXDoky.js → sankeyDiagram-TZEHDZUN-x_aTXZeN.js} +1 -1
- package/dist/{sequenceDiagram-WL72ISMW-ByLI04T5.js → sequenceDiagram-WL72ISMW-CXXmJqiQ.js} +3 -3
- package/dist/{slides-component-BVjvNo92.js → slides-component-Dp-y50K9.js} +4 -4
- package/dist/{spec-Dmb1KfK3.js → spec-HoYHAQo2.js} +6 -6
- package/dist/{stateDiagram-FKZM4ZOC-Dfz8vBbP.js → stateDiagram-FKZM4ZOC-CiSKS_Mx.js} +9 -9
- package/dist/{stateDiagram-v2-4FDKWEC3-DRYoLdT5.js → stateDiagram-v2-4FDKWEC3-A43Itnjp.js} +9 -9
- package/dist/style.css +1 -1
- package/dist/{timeline-definition-IT6M3QCI-CO48XU1B.js → timeline-definition-IT6M3QCI-DR26eWb4.js} +1 -1
- package/dist/{types-CzEZ3EWT.js → types-Bb-6p8hv.js} +8 -8
- package/dist/{useAsyncData-BjNwqCfS.js → useAsyncData-Dyq3DyOF.js} +3 -3
- package/dist/{useDeepCompareMemoize-CfoxVor3.js → useDeepCompareMemoize-CMGprt3H.js} +5 -5
- package/dist/{useIframeCapabilities-BBO_R0ww.js → useIframeCapabilities-DurI5SJh.js} +2 -2
- package/dist/{useTheme-BYG2SH8J.js → useTheme-SlKl8MlS.js} +5 -6
- package/dist/{vega-component-rDX7xwxH.js → vega-component-DU3aSp4m.js} +10 -10
- package/dist/{xychartDiagram-PRI3JC2R-CUIfjNVD.js → xychartDiagram-PRI3JC2R-BcVxCRox.js} +4 -4
- package/dist/{zod-DITCj31F.js → zod-bjADtMKr.js} +3 -3
- package/package.json +18 -18
- package/src/components/app-config/ai-config.tsx +11 -2
- package/src/components/app-config/user-config-form.tsx +0 -54
- package/src/components/chat/acp/__tests__/state.test.ts +69 -0
- package/src/components/chat/acp/state.ts +6 -6
- package/src/components/chat/chat-panel.tsx +47 -30
- package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
- package/src/components/editor/actions/useCellActionButton.tsx +14 -1
- package/src/components/editor/cell/CreateCellButton.tsx +2 -1
- package/src/components/editor/cell/code/cell-editor.tsx +12 -0
- package/src/components/editor/database/__tests__/__snapshots__/as-code.test.ts.snap +15 -0
- package/src/components/editor/database/__tests__/as-code.test.ts +8 -0
- package/src/components/editor/database/as-code.ts +3 -0
- package/src/components/editor/database/schemas.ts +9 -0
- package/src/components/editor/renderers/cell-array.tsx +2 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -0
- package/src/components/pages/gallery-page.tsx +37 -6
- package/src/core/MarimoApp.tsx +12 -8
- package/src/core/ai/context/providers/file.ts +1 -1
- package/src/core/cells/__tests__/cells.test.ts +120 -0
- package/src/core/cells/cells.ts +14 -0
- package/src/core/codemirror/language/languages/markdown.ts +7 -0
- package/src/core/config/feature-flag.tsx +0 -4
- package/src/core/islands/__tests__/bridge.test.ts +241 -0
- package/src/core/islands/bridge.ts +22 -6
- package/src/core/run-app.tsx +11 -4
- package/src/core/static/__tests__/files.test.ts +195 -1
- package/src/core/static/files.ts +39 -9
- package/src/plugins/core/registerReactComponent.tsx +9 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +7 -1
- package/src/utils/__tests__/blob.test.ts +3 -3
- package/src/utils/__tests__/mime-types.test.ts +8 -10
- package/src/utils/__tests__/url-parser.test.ts +22 -0
- package/src/utils/blob.ts +14 -27
- package/src/utils/mime-types.ts +5 -5
- package/src/utils/url-parser.ts +1 -1
- package/dist/assets/__vite-browser-external-DRa9CT_O.js +0 -1
- package/dist/mermaid-4DMBBIKO-o3xNphpD.js +0 -6
|
@@ -2561,6 +2561,126 @@ describe("cell reducer", () => {
|
|
|
2561
2561
|
expect(state.untouchedNewCells.has(newCellId)).toBe(false);
|
|
2562
2562
|
expect(exportedForTesting.isCellCodeHidden(state, newCellId)).toBe(true);
|
|
2563
2563
|
});
|
|
2564
|
+
|
|
2565
|
+
it("can mark an existing cell as untouched", () => {
|
|
2566
|
+
// Create a cell without hideCode (not in untouchedNewCells)
|
|
2567
|
+
actions.createNewCell({
|
|
2568
|
+
cellId: "__end__",
|
|
2569
|
+
before: false,
|
|
2570
|
+
hideCode: false,
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
const newCellId =
|
|
2574
|
+
state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
|
|
2575
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(false);
|
|
2576
|
+
|
|
2577
|
+
// Mark it as untouched
|
|
2578
|
+
actions.markUntouched({ cellId: newCellId });
|
|
2579
|
+
|
|
2580
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(true);
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
it("markUntouched is idempotent", () => {
|
|
2584
|
+
// Create a cell without hideCode
|
|
2585
|
+
actions.createNewCell({
|
|
2586
|
+
cellId: "__end__",
|
|
2587
|
+
before: false,
|
|
2588
|
+
hideCode: false,
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
const newCellId =
|
|
2592
|
+
state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
|
|
2593
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(false);
|
|
2594
|
+
|
|
2595
|
+
// Mark as untouched multiple times
|
|
2596
|
+
actions.markUntouched({ cellId: newCellId });
|
|
2597
|
+
actions.markUntouched({ cellId: newCellId });
|
|
2598
|
+
actions.markUntouched({ cellId: newCellId });
|
|
2599
|
+
|
|
2600
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(true);
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
it("markUntouched does not affect already untouched cells", () => {
|
|
2604
|
+
// Create a cell with hideCode (already in untouchedNewCells)
|
|
2605
|
+
actions.createNewCell({
|
|
2606
|
+
cellId: "__end__",
|
|
2607
|
+
before: false,
|
|
2608
|
+
hideCode: true,
|
|
2609
|
+
});
|
|
2610
|
+
|
|
2611
|
+
const newCellId =
|
|
2612
|
+
state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
|
|
2613
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(true);
|
|
2614
|
+
|
|
2615
|
+
// Calling markUntouched should not change anything
|
|
2616
|
+
actions.markUntouched({ cellId: newCellId });
|
|
2617
|
+
|
|
2618
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(true);
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
it("markTouched and markUntouched can toggle cell state", () => {
|
|
2622
|
+
// Create a cell without hideCode
|
|
2623
|
+
actions.createNewCell({
|
|
2624
|
+
cellId: "__end__",
|
|
2625
|
+
before: false,
|
|
2626
|
+
hideCode: false,
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
const newCellId =
|
|
2630
|
+
state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
|
|
2631
|
+
|
|
2632
|
+
// Initially not untouched
|
|
2633
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(false);
|
|
2634
|
+
|
|
2635
|
+
// Mark as untouched
|
|
2636
|
+
actions.markUntouched({ cellId: newCellId });
|
|
2637
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(true);
|
|
2638
|
+
|
|
2639
|
+
// Mark as touched
|
|
2640
|
+
actions.markTouched({ cellId: newCellId });
|
|
2641
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(false);
|
|
2642
|
+
|
|
2643
|
+
// Mark as untouched again
|
|
2644
|
+
actions.markUntouched({ cellId: newCellId });
|
|
2645
|
+
expect(state.untouchedNewCells.has(newCellId)).toBe(true);
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
it("markUntouched works for markdown cell conversion scenario", () => {
|
|
2649
|
+
// Simulates converting a Python cell to Markdown
|
|
2650
|
+
// 1. Create a regular cell (no hideCode)
|
|
2651
|
+
actions.createNewCell({
|
|
2652
|
+
cellId: "__end__",
|
|
2653
|
+
before: false,
|
|
2654
|
+
hideCode: false,
|
|
2655
|
+
});
|
|
2656
|
+
|
|
2657
|
+
const cellId =
|
|
2658
|
+
state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
|
|
2659
|
+
|
|
2660
|
+
// Cell starts without hide_code and not in untouchedNewCells
|
|
2661
|
+
expect(state.cellData[cellId].config.hide_code).toBe(false);
|
|
2662
|
+
expect(state.untouchedNewCells.has(cellId)).toBe(false);
|
|
2663
|
+
expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
|
|
2664
|
+
|
|
2665
|
+
// 2. Convert to markdown: set hide_code and mark as untouched
|
|
2666
|
+
actions.updateCellConfig({
|
|
2667
|
+
cellId,
|
|
2668
|
+
config: { hide_code: true },
|
|
2669
|
+
});
|
|
2670
|
+
actions.markUntouched({ cellId });
|
|
2671
|
+
|
|
2672
|
+
// Code should NOT be hidden because cell is untouched (user can edit)
|
|
2673
|
+
expect(state.cellData[cellId].config.hide_code).toBe(true);
|
|
2674
|
+
expect(state.untouchedNewCells.has(cellId)).toBe(true);
|
|
2675
|
+
expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(false);
|
|
2676
|
+
|
|
2677
|
+
// 3. User blurs the cell (markTouched)
|
|
2678
|
+
actions.markTouched({ cellId });
|
|
2679
|
+
|
|
2680
|
+
// Now code should be hidden
|
|
2681
|
+
expect(state.untouchedNewCells.has(cellId)).toBe(false);
|
|
2682
|
+
expect(exportedForTesting.isCellCodeHidden(state, cellId)).toBe(true);
|
|
2683
|
+
});
|
|
2564
2684
|
});
|
|
2565
2685
|
|
|
2566
2686
|
describe("releaseCellAtoms", () => {
|
package/src/core/cells/cells.ts
CHANGED
|
@@ -1033,6 +1033,20 @@ const {
|
|
|
1033
1033
|
|
|
1034
1034
|
return state;
|
|
1035
1035
|
},
|
|
1036
|
+
markUntouched: (state, action: { cellId: CellId }) => {
|
|
1037
|
+
const { cellId } = action;
|
|
1038
|
+
|
|
1039
|
+
if (!state.untouchedNewCells.has(cellId)) {
|
|
1040
|
+
const nextUntouchedNewCells = new Set(state.untouchedNewCells);
|
|
1041
|
+
nextUntouchedNewCells.add(cellId);
|
|
1042
|
+
return {
|
|
1043
|
+
...state,
|
|
1044
|
+
untouchedNewCells: nextUntouchedNewCells,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return state;
|
|
1049
|
+
},
|
|
1036
1050
|
scrollToTarget: (state) => {
|
|
1037
1051
|
// Scroll to the specified cell and clear the scroll key.
|
|
1038
1052
|
const scrollKey = state.scrollKey;
|
|
@@ -28,6 +28,13 @@ import type { LanguageAdapter } from "../types";
|
|
|
28
28
|
|
|
29
29
|
export type MarkdownLanguageAdapterMetadata = MarkdownMetadata;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Default hide_code setting for markdown cells.
|
|
33
|
+
* When true, the markdown code is hidden after the cell is blurred,
|
|
34
|
+
* showing only the rendered output.
|
|
35
|
+
*/
|
|
36
|
+
export const MARKDOWN_INITIAL_HIDE_CODE = true;
|
|
37
|
+
|
|
31
38
|
/**
|
|
32
39
|
* Language adapter for Markdown.
|
|
33
40
|
*/
|
|
@@ -9,8 +9,6 @@ export interface ExperimentalFeatures {
|
|
|
9
9
|
markdown: boolean; // Used in playground (community cloud)
|
|
10
10
|
wasm_layouts: boolean; // Used in playground (community cloud)
|
|
11
11
|
rtc_v2: boolean;
|
|
12
|
-
performant_table_charts: boolean;
|
|
13
|
-
chat_modes: boolean;
|
|
14
12
|
cache_panel: boolean;
|
|
15
13
|
external_agents: boolean;
|
|
16
14
|
server_side_pdf_export: boolean;
|
|
@@ -21,8 +19,6 @@ const defaultValues: ExperimentalFeatures = {
|
|
|
21
19
|
markdown: true,
|
|
22
20
|
wasm_layouts: false,
|
|
23
21
|
rtc_v2: false,
|
|
24
|
-
performant_table_charts: false,
|
|
25
|
-
chat_modes: false,
|
|
26
22
|
cache_panel: false,
|
|
27
23
|
external_agents: import.meta.env.DEV,
|
|
28
24
|
server_side_pdf_export: true,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Mock browser APIs before any imports
|
|
5
|
+
vi.stubGlobal(
|
|
6
|
+
"Worker",
|
|
7
|
+
vi.fn(() => ({
|
|
8
|
+
addEventListener: vi.fn(),
|
|
9
|
+
postMessage: vi.fn(),
|
|
10
|
+
terminate: vi.fn(),
|
|
11
|
+
})),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Create a mock URL class that works as a constructor
|
|
15
|
+
class MockURL {
|
|
16
|
+
href: string;
|
|
17
|
+
constructor(url: string, base?: string | URL) {
|
|
18
|
+
this.href = base ? `${base}/${url}` : url;
|
|
19
|
+
}
|
|
20
|
+
static createObjectURL = vi.fn(() => "blob:mock-url");
|
|
21
|
+
static revokeObjectURL = vi.fn();
|
|
22
|
+
}
|
|
23
|
+
vi.stubGlobal("URL", MockURL);
|
|
24
|
+
|
|
25
|
+
// Mock the worker RPC before importing the bridge
|
|
26
|
+
const mockBridge = vi.fn();
|
|
27
|
+
const mockLoadPackages = vi.fn();
|
|
28
|
+
|
|
29
|
+
vi.mock("@/core/wasm/rpc", () => ({
|
|
30
|
+
getWorkerRPC: () => ({
|
|
31
|
+
proxy: {
|
|
32
|
+
request: {
|
|
33
|
+
bridge: mockBridge,
|
|
34
|
+
loadPackages: mockLoadPackages,
|
|
35
|
+
startSession: vi.fn(),
|
|
36
|
+
},
|
|
37
|
+
send: {
|
|
38
|
+
consumerReady: vi.fn(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
addMessageListener: vi.fn(),
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Mock the parse module to avoid DOM dependencies
|
|
46
|
+
vi.mock("../parse", () => ({
|
|
47
|
+
parseMarimoIslandApps: () => [],
|
|
48
|
+
createMarimoFile: vi.fn(),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Mock uuid to have predictable tokens
|
|
52
|
+
vi.mock("@/utils/uuid", () => ({
|
|
53
|
+
generateUUID: () => "test-uuid-12345",
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Mock getMarimoVersion
|
|
57
|
+
vi.mock("@/core/meta/globals", () => ({
|
|
58
|
+
getMarimoVersion: () => "0.0.0-test",
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Mock the jotai store
|
|
62
|
+
vi.mock("@/core/state/jotai", () => ({
|
|
63
|
+
store: {
|
|
64
|
+
set: vi.fn(),
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Now import the bridge class
|
|
69
|
+
import { IslandsPyodideBridge } from "../bridge";
|
|
70
|
+
|
|
71
|
+
describe("IslandsPyodideBridge", () => {
|
|
72
|
+
let bridge: IslandsPyodideBridge;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
vi.clearAllMocks();
|
|
76
|
+
// Reset the singleton by clearing the window property
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
79
|
+
// Access the singleton - creates a fresh instance
|
|
80
|
+
bridge = IslandsPyodideBridge.INSTANCE;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
// Clean up singleton
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("sendComponentValues", () => {
|
|
90
|
+
it("should include type field and token in control request", async () => {
|
|
91
|
+
const request = {
|
|
92
|
+
objectIds: ["Hbol-0"],
|
|
93
|
+
values: [58],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await bridge.sendComponentValues(request);
|
|
97
|
+
|
|
98
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
99
|
+
functionName: "put_control_request",
|
|
100
|
+
payload: {
|
|
101
|
+
type: "update-ui-element",
|
|
102
|
+
objectIds: ["Hbol-0"],
|
|
103
|
+
values: [58],
|
|
104
|
+
token: "test-uuid-12345",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should preserve all request properties", async () => {
|
|
110
|
+
const request = {
|
|
111
|
+
objectIds: ["slider-1", "slider-2"],
|
|
112
|
+
values: [10, 20],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await bridge.sendComponentValues(request);
|
|
116
|
+
|
|
117
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
118
|
+
functionName: "put_control_request",
|
|
119
|
+
payload: expect.objectContaining({
|
|
120
|
+
type: "update-ui-element",
|
|
121
|
+
objectIds: ["slider-1", "slider-2"],
|
|
122
|
+
values: [10, 20],
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("sendFunctionRequest", () => {
|
|
129
|
+
it("should include type field in control request", async () => {
|
|
130
|
+
const request = {
|
|
131
|
+
functionCallId: "call-123",
|
|
132
|
+
namespace: "test_namespace",
|
|
133
|
+
functionName: "my_function",
|
|
134
|
+
args: { x: 1, y: 2 },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await bridge.sendFunctionRequest(request);
|
|
138
|
+
|
|
139
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
140
|
+
functionName: "put_control_request",
|
|
141
|
+
payload: {
|
|
142
|
+
type: "invoke-function",
|
|
143
|
+
functionCallId: "call-123",
|
|
144
|
+
namespace: "test_namespace",
|
|
145
|
+
functionName: "my_function",
|
|
146
|
+
args: { x: 1, y: 2 },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("sendRun", () => {
|
|
153
|
+
it("should include type field in control request", async () => {
|
|
154
|
+
const request = {
|
|
155
|
+
cellIds: ["cell-1", "cell-2"],
|
|
156
|
+
codes: ["print('hello')", "print('world')"],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await bridge.sendRun(request);
|
|
160
|
+
|
|
161
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
162
|
+
functionName: "put_control_request",
|
|
163
|
+
payload: {
|
|
164
|
+
type: "execute-cells",
|
|
165
|
+
cellIds: ["cell-1", "cell-2"],
|
|
166
|
+
codes: ["print('hello')", "print('world')"],
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should call loadPackages before putControlRequest", async () => {
|
|
172
|
+
const request = {
|
|
173
|
+
cellIds: ["cell-1"],
|
|
174
|
+
codes: ["import pandas"],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
await bridge.sendRun(request);
|
|
178
|
+
|
|
179
|
+
// Verify loadPackages was called with joined codes
|
|
180
|
+
expect(mockLoadPackages).toHaveBeenCalledWith("import pandas");
|
|
181
|
+
|
|
182
|
+
// Verify order: loadPackages should be called before bridge
|
|
183
|
+
const loadPackagesCallOrder =
|
|
184
|
+
mockLoadPackages.mock.invocationCallOrder[0];
|
|
185
|
+
const bridgeCallOrder = mockBridge.mock.invocationCallOrder[0];
|
|
186
|
+
expect(loadPackagesCallOrder).toBeLessThan(bridgeCallOrder);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("sendModelValue", () => {
|
|
191
|
+
it("should include type field in control request", async () => {
|
|
192
|
+
const request = {
|
|
193
|
+
modelId: "widget-1",
|
|
194
|
+
message: {
|
|
195
|
+
state: { value: 42 },
|
|
196
|
+
bufferPaths: [],
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await bridge.sendModelValue(request);
|
|
201
|
+
|
|
202
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
203
|
+
functionName: "put_control_request",
|
|
204
|
+
payload: {
|
|
205
|
+
type: "update-widget-model",
|
|
206
|
+
modelId: "widget-1",
|
|
207
|
+
message: {
|
|
208
|
+
state: { value: 42 },
|
|
209
|
+
bufferPaths: [],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("control request message format", () => {
|
|
217
|
+
it("should always include the type field required by msgspec", async () => {
|
|
218
|
+
// Test all methods to ensure they include the type field
|
|
219
|
+
await bridge.sendComponentValues({ objectIds: [], values: [] });
|
|
220
|
+
await bridge.sendFunctionRequest({
|
|
221
|
+
functionCallId: "",
|
|
222
|
+
namespace: "",
|
|
223
|
+
functionName: "",
|
|
224
|
+
args: {},
|
|
225
|
+
});
|
|
226
|
+
await bridge.sendRun({ cellIds: [], codes: [] });
|
|
227
|
+
await bridge.sendModelValue({
|
|
228
|
+
modelId: "",
|
|
229
|
+
message: { state: {}, bufferPaths: [] },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// All calls should have the type field
|
|
233
|
+
const allCalls = mockBridge.mock.calls;
|
|
234
|
+
for (const call of allCalls) {
|
|
235
|
+
const payload = call[0].payload;
|
|
236
|
+
expect(payload).toHaveProperty("type");
|
|
237
|
+
expect(typeof payload.type).toBe("string");
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -6,7 +6,8 @@ import { Deferred } from "@/utils/Deferred";
|
|
|
6
6
|
import { throwNotImplemented } from "@/utils/functions";
|
|
7
7
|
import type { JsonString } from "@/utils/json/base64";
|
|
8
8
|
import { Logger } from "@/utils/Logger";
|
|
9
|
-
import
|
|
9
|
+
import { generateUUID } from "@/utils/uuid";
|
|
10
|
+
import type { CommandMessage, NotificationPayload } from "../kernel/messages";
|
|
10
11
|
import { getMarimoVersion } from "../meta/globals";
|
|
11
12
|
import type { EditRequests, RunRequests } from "../network/types";
|
|
12
13
|
import { store } from "../state/jotai";
|
|
@@ -104,7 +105,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
104
105
|
sendComponentValues: RunRequests["sendComponentValues"] = async (
|
|
105
106
|
request,
|
|
106
107
|
): Promise<null> => {
|
|
107
|
-
await this.putControlRequest(
|
|
108
|
+
await this.putControlRequest({
|
|
109
|
+
type: "update-ui-element",
|
|
110
|
+
...request,
|
|
111
|
+
token: generateUUID(),
|
|
112
|
+
});
|
|
108
113
|
return null;
|
|
109
114
|
};
|
|
110
115
|
|
|
@@ -117,18 +122,27 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
117
122
|
sendFunctionRequest: RunRequests["sendFunctionRequest"] = async (
|
|
118
123
|
request,
|
|
119
124
|
): Promise<null> => {
|
|
120
|
-
await this.putControlRequest(
|
|
125
|
+
await this.putControlRequest({
|
|
126
|
+
type: "invoke-function",
|
|
127
|
+
...request,
|
|
128
|
+
});
|
|
121
129
|
return null;
|
|
122
130
|
};
|
|
123
131
|
|
|
124
132
|
sendRun: EditRequests["sendRun"] = async (request): Promise<null> => {
|
|
125
133
|
await this.rpc.proxy.request.loadPackages(request.codes.join("\n"));
|
|
126
|
-
await this.putControlRequest(
|
|
134
|
+
await this.putControlRequest({
|
|
135
|
+
type: "execute-cells",
|
|
136
|
+
...request,
|
|
137
|
+
});
|
|
127
138
|
return null;
|
|
128
139
|
};
|
|
129
140
|
|
|
130
141
|
sendModelValue: RunRequests["sendModelValue"] = async (request) => {
|
|
131
|
-
await this.putControlRequest(
|
|
142
|
+
await this.putControlRequest({
|
|
143
|
+
type: "update-widget-model",
|
|
144
|
+
...request,
|
|
145
|
+
});
|
|
132
146
|
return null;
|
|
133
147
|
};
|
|
134
148
|
|
|
@@ -187,7 +201,9 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
187
201
|
clearCache = throwNotImplemented;
|
|
188
202
|
getCacheInfo = throwNotImplemented;
|
|
189
203
|
|
|
190
|
-
|
|
204
|
+
// The kernel uses msgspec to parse control requests, which requires a 'type'
|
|
205
|
+
// field for discriminated union deserialization.
|
|
206
|
+
private async putControlRequest(operation: CommandMessage) {
|
|
191
207
|
await this.rpc.proxy.request.bridge({
|
|
192
208
|
functionName: "put_control_request",
|
|
193
209
|
payload: operation,
|
package/src/core/run-app.tsx
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
|
+
import { ArrowLeftIcon } from "lucide-react";
|
|
4
5
|
import { useEffect } from "react";
|
|
5
6
|
import { AppContainer } from "@/components/editor/app-container";
|
|
6
7
|
import { AppHeader } from "@/components/editor/header/app-header";
|
|
7
8
|
import { Spinner } from "@/components/icons/spinner";
|
|
9
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
8
10
|
import { DelayMount } from "@/components/utils/delay-mount";
|
|
11
|
+
import { cn } from "@/utils/cn";
|
|
9
12
|
import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
|
|
10
13
|
import { notebookIsRunningAtom, useCellActions } from "./cells/cells";
|
|
11
14
|
import type { AppConfig } from "./config/config-schema";
|
|
@@ -75,15 +78,19 @@ export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
|
|
|
75
78
|
isRunning={isRunning}
|
|
76
79
|
width={appConfig.width}
|
|
77
80
|
>
|
|
78
|
-
<AppHeader connection={connection} className=
|
|
81
|
+
<AppHeader connection={connection} className="sm:pt-8">
|
|
79
82
|
{galleryHref && (
|
|
80
|
-
<div className="flex items-center px-6 pt-4">
|
|
83
|
+
<div className="flex items-center px-6 pt-4 sm:-mt-8">
|
|
81
84
|
<a
|
|
82
85
|
href={galleryHref}
|
|
83
86
|
aria-label="Back to gallery"
|
|
84
|
-
className=
|
|
87
|
+
className={cn(
|
|
88
|
+
buttonVariants({ variant: "text", size: "sm" }),
|
|
89
|
+
"gap-2 px-0 text-muted-foreground hover:text-foreground",
|
|
90
|
+
)}
|
|
85
91
|
>
|
|
86
|
-
<
|
|
92
|
+
<ArrowLeftIcon className="size-4" aria-hidden={true} />
|
|
93
|
+
<span>Back</span>
|
|
87
94
|
</a>
|
|
88
95
|
</div>
|
|
89
96
|
)}
|