@marimo-team/islands 0.19.8-dev3 → 0.19.8-dev31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/{Combination-Bg-xN8JV.js → Combination-BTMrlhzT.js} +11 -10
  2. package/dist/{ConnectedDataExplorerComponent-DewsKLl2.js → ConnectedDataExplorerComponent-BAeQ8DWw.js} +11 -11
  3. package/dist/{ImageComparisonComponent-Bijp8beW.js → ImageComparisonComponent-DkEXPki_.js} +2 -2
  4. package/dist/{any-language-editor-DZc6NCTp.js → any-language-editor-D0UQItkS.js} +6 -6
  5. package/dist/{architectureDiagram-VXUJARFQ--NkyBn9Y.js → architectureDiagram-VXUJARFQ-DPPYVq8H.js} +4 -4
  6. package/dist/assets/__vite-browser-external-6-UwTyQC.js +1 -0
  7. package/dist/assets/{worker-SqntmiwV.js → worker-D3e5wDxM.js} +4 -4
  8. package/dist/{blockDiagram-VD42YOAC-DEZZaTW0.js → blockDiagram-VD42YOAC-BA5N05Y9.js} +4 -4
  9. package/dist/{button-BWvsJ2Wr.js → button-Cy0ElmIm.js} +2 -2
  10. package/dist/{c4Diagram-YG6GDRKO-Bj7hwWCO.js → c4Diagram-YG6GDRKO-DJLzuGJJ.js} +3 -3
  11. package/dist/{channel-B_QrFrGg.js → channel-Dob5kWXR.js} +1 -1
  12. package/dist/{check-CM_kewwn.js → check-DkNR52Mm.js} +1 -1
  13. package/dist/{chunk-5FQGJX7Z-D5VFKHmt.js → chunk-5FQGJX7Z-BEb20Lzt.js} +3 -3
  14. package/dist/{chunk-ABZYJK2D-SZPYmRzN.js → chunk-ABZYJK2D-BXTC53mt.js} +1 -1
  15. package/dist/{chunk-ATLVNIR6-BI_WwH1o.js → chunk-ATLVNIR6-BJDjUR_c.js} +1 -1
  16. package/dist/{chunk-B4BG7PRW-BlI9Gm1l.js → chunk-B4BG7PRW-DzmUUpfH.js} +4 -4
  17. package/dist/{chunk-DI55MBZ5-BXxemMn5.js → chunk-DI55MBZ5-gTd3J8Tu.js} +4 -4
  18. package/dist/{chunk-EXTU4WIE-CzWtDV99.js → chunk-EXTU4WIE-DyoOs5QX.js} +1 -1
  19. package/dist/{chunk-JA3XYJ7Z-DQ-2ARfa.js → chunk-JA3XYJ7Z-BGnAIbOP.js} +2 -2
  20. package/dist/{chunk-JZLCHNYA-CVfjf2vv.js → chunk-JZLCHNYA-CIRgweVQ.js} +4 -4
  21. package/dist/{chunk-N4CR4FBY-BCZvQ7Jq.js → chunk-N4CR4FBY-DKSvXAIS.js} +5 -5
  22. package/dist/{chunk-QN33PNHL-DY_2Q2zl.js → chunk-QN33PNHL-B6zC8BTi.js} +1 -1
  23. package/dist/{chunk-QXUST7PY-BMCjAVR_.js → chunk-QXUST7PY-C7750n_u.js} +5 -5
  24. package/dist/{chunk-S3R3BYOJ-Ddu0H4Qa.js → chunk-S3R3BYOJ-CBkH6JZZ.js} +1 -1
  25. package/dist/{chunk-TZMSLE5B-C2wVlbMl.js → chunk-TZMSLE5B-DObGL7xi.js} +1 -1
  26. package/dist/{classDiagram-2ON5EDUG-D-g7zbyO.js → classDiagram-2ON5EDUG-B9pkKjjc.js} +9 -9
  27. package/dist/{classDiagram-v2-WZHVMYZB-C7v5zNRD.js → classDiagram-v2-WZHVMYZB-CRhhA0tV.js} +9 -9
  28. package/dist/{click-outside-container-BCN5BtVO.js → click-outside-container-DNfggvIW.js} +1 -1
  29. package/dist/{code-block-37QAKDTI-eUgXqGNG.js → code-block-37QAKDTI-u5kgjqmr.js} +2 -2
  30. package/dist/{compiler-runtime-DHFVbq0b.js → compiler-runtime-B_OLMU9S.js} +1 -1
  31. package/dist/{copy-B59Bw3-w.js → copy-DRaXIb_a.js} +3 -3
  32. package/dist/{dagre-6UL2VRFP-DKIPL74O.js → dagre-6UL2VRFP-C2C2XxsB.js} +6 -6
  33. package/dist/{data-grid-overlay-editor-COyFwFmE.js → data-grid-overlay-editor-BXqtz1ia.js} +4 -4
  34. package/dist/{diagram-PSM6KHXK-CVTrAZaP.js → diagram-PSM6KHXK-DHBY-94p.js} +5 -5
  35. package/dist/{diagram-QEK2KX5R-BqHBzu3x.js → diagram-QEK2KX5R-CgMshOwn.js} +3 -3
  36. package/dist/{diagram-S2PKOQOG-CJD6owcg.js → diagram-S2PKOQOG-F1KPva3Y.js} +3 -3
  37. package/dist/{dist-Co5PD8Fb.js → dist-BBYTEAvO.js} +1 -1
  38. package/dist/{erDiagram-Q2GNP2WA-CqOceSf9.js → erDiagram-Q2GNP2WA-18gGng8V.js} +9 -9
  39. package/dist/{error-banner-C7KLpECd.js → error-banner-D2zjeN_a.js} +5 -5
  40. package/dist/{esm-D4WO8J3G.js → esm-CgRNPmz8.js} +6 -6
  41. package/dist/{flowDiagram-NV44I4VS-K7-DUifo.js → flowDiagram-NV44I4VS-iHFiHYe0.js} +9 -9
  42. package/dist/{ganttDiagram-JELNMOA3-BwUFY9Nu.js → ganttDiagram-JELNMOA3-D7GixxiF.js} +2 -2
  43. package/dist/{gitGraphDiagram-NY62KEGX-CjGRtLb1.js → gitGraphDiagram-NY62KEGX-CJFHytRK.js} +2 -2
  44. package/dist/{glide-data-editor-C3T7HsLi.js → glide-data-editor-BYwb17Bf.js} +13 -13
  45. package/dist/{infoDiagram-WHAUD3N6-DNhmDn-6.js → infoDiagram-WHAUD3N6-B5Lkh3A9.js} +2 -2
  46. package/dist/{journeyDiagram-XKPGCS4Q-BOdK47P8.js → journeyDiagram-XKPGCS4Q-CV_9R9iP.js} +2 -2
  47. package/dist/{kanban-definition-3W4ZIXB7-A0JC9d0g.js → kanban-definition-3W4ZIXB7-Dp21D5Ym.js} +6 -6
  48. package/dist/{katex-DJyOeQ91.js → katex-CX2BKujk.js} +1 -1
  49. package/dist/{katex-Dm9nZf6A.js → katex-Db0k5oV_.js} +1 -1
  50. package/dist/{label-C4PtQcza.js → label-CxU5JNBW.js} +6 -6
  51. package/dist/main.js +282 -193
  52. package/dist/mermaid-4DMBBIKO-BhDCqnO1.js +6 -0
  53. package/dist/{mermaid-Bqp2Xw99.js → mermaid-B__BZSXU.js} +39 -39
  54. package/dist/{mhchem-BqdXeZVX.js → mhchem-w1tkUnWr.js} +1 -1
  55. package/dist/{mindmap-definition-VGOIOE7T-CS6nKN_L.js → mindmap-definition-VGOIOE7T-B_5mfdYp.js} +8 -8
  56. package/dist/{number-overlay-editor-Bz_bDJQb.js → number-overlay-editor-D-4WQAGX.js} +2 -2
  57. package/dist/{pieDiagram-ADFJNKIX-DSa60Grk.js → pieDiagram-ADFJNKIX-B-DGEopK.js} +3 -3
  58. package/dist/{quadrantDiagram-AYHSOK5B-CFnMbP2J.js → quadrantDiagram-AYHSOK5B-M_yRSIZn.js} +1 -1
  59. package/dist/{react-DdA8EBol.js → react-Bs6Z0kvn.js} +1 -1
  60. package/dist/{react-dom-DJW8xUDg.js → react-dom-CqtLRVZP.js} +2 -2
  61. package/dist/{react-plotly-jVjTu07w.js → react-plotly-BuRa9xtI.js} +1 -1
  62. package/dist/{react-vega-DgHpnZ04.js → react-vega-3WcLHYC7.js} +2 -2
  63. package/dist/{react-vega-CjiPWyw0.js → react-vega-DLFvGrpJ.js} +1 -1
  64. package/dist/{requirementDiagram-UZGBJVZJ-ytLQrFTk.js → requirementDiagram-UZGBJVZJ-9Wt82hOZ.js} +8 -8
  65. package/dist/{sankeyDiagram-TZEHDZUN-KQqXDoky.js → sankeyDiagram-TZEHDZUN-x_aTXZeN.js} +1 -1
  66. package/dist/{sequenceDiagram-WL72ISMW-ByLI04T5.js → sequenceDiagram-WL72ISMW-CXXmJqiQ.js} +3 -3
  67. package/dist/{slides-component-BVjvNo92.js → slides-component-Dp-y50K9.js} +4 -4
  68. package/dist/{spec-Dmb1KfK3.js → spec-HoYHAQo2.js} +6 -6
  69. package/dist/{stateDiagram-FKZM4ZOC-Dfz8vBbP.js → stateDiagram-FKZM4ZOC-CiSKS_Mx.js} +9 -9
  70. package/dist/{stateDiagram-v2-4FDKWEC3-DRYoLdT5.js → stateDiagram-v2-4FDKWEC3-A43Itnjp.js} +9 -9
  71. package/dist/style.css +1 -1
  72. package/dist/{timeline-definition-IT6M3QCI-CO48XU1B.js → timeline-definition-IT6M3QCI-DR26eWb4.js} +1 -1
  73. package/dist/{types-CzEZ3EWT.js → types-Bb-6p8hv.js} +8 -8
  74. package/dist/{useAsyncData-BjNwqCfS.js → useAsyncData-Dyq3DyOF.js} +3 -3
  75. package/dist/{useDeepCompareMemoize-CfoxVor3.js → useDeepCompareMemoize-CMGprt3H.js} +5 -5
  76. package/dist/{useIframeCapabilities-BBO_R0ww.js → useIframeCapabilities-DurI5SJh.js} +2 -2
  77. package/dist/{useTheme-BYG2SH8J.js → useTheme-SlKl8MlS.js} +5 -6
  78. package/dist/{vega-component-rDX7xwxH.js → vega-component-DU3aSp4m.js} +10 -10
  79. package/dist/{xychartDiagram-PRI3JC2R-CUIfjNVD.js → xychartDiagram-PRI3JC2R-BcVxCRox.js} +4 -4
  80. package/dist/{zod-DITCj31F.js → zod-bjADtMKr.js} +3 -3
  81. package/package.json +18 -18
  82. package/src/components/app-config/ai-config.tsx +11 -2
  83. package/src/components/app-config/user-config-form.tsx +0 -54
  84. package/src/components/chat/acp/__tests__/state.test.ts +69 -0
  85. package/src/components/chat/acp/state.ts +6 -6
  86. package/src/components/chat/chat-panel.tsx +47 -30
  87. package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
  88. package/src/components/editor/actions/useCellActionButton.tsx +14 -1
  89. package/src/components/editor/cell/CreateCellButton.tsx +2 -1
  90. package/src/components/editor/cell/code/cell-editor.tsx +12 -0
  91. package/src/components/editor/renderers/cell-array.tsx +2 -1
  92. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -0
  93. package/src/components/pages/gallery-page.tsx +37 -6
  94. package/src/core/MarimoApp.tsx +12 -8
  95. package/src/core/ai/context/providers/file.ts +1 -1
  96. package/src/core/cells/__tests__/cells.test.ts +120 -0
  97. package/src/core/cells/cells.ts +14 -0
  98. package/src/core/codemirror/language/languages/markdown.ts +7 -0
  99. package/src/core/config/feature-flag.tsx +0 -4
  100. package/src/core/islands/__tests__/bridge.test.ts +241 -0
  101. package/src/core/islands/bridge.ts +22 -6
  102. package/src/core/run-app.tsx +11 -4
  103. package/src/core/static/__tests__/files.test.ts +195 -1
  104. package/src/core/static/files.ts +39 -9
  105. package/src/plugins/core/registerReactComponent.tsx +9 -1
  106. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
  107. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +7 -1
  108. package/src/utils/__tests__/blob.test.ts +3 -3
  109. package/src/utils/__tests__/mime-types.test.ts +8 -10
  110. package/src/utils/__tests__/url-parser.test.ts +22 -0
  111. package/src/utils/blob.ts +14 -27
  112. package/src/utils/mime-types.ts +5 -5
  113. package/src/utils/url-parser.ts +1 -1
  114. package/dist/assets/__vite-browser-external-DRa9CT_O.js +0 -1
  115. package/dist/mermaid-4DMBBIKO-o3xNphpD.js +0 -6
@@ -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 type { NotificationPayload } from "../kernel/messages";
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(request);
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(request);
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(request);
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(request);
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
- private async putControlRequest(operation: object) {
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,
@@ -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={"sm:pt-8"}>
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="inline-flex items-center"
87
+ className={cn(
88
+ buttonVariants({ variant: "text", size: "sm" }),
89
+ "gap-2 px-0 text-muted-foreground hover:text-foreground",
90
+ )}
85
91
  >
86
- <img src="logo.png" alt="marimo logo" className="h-6 w-auto" />
92
+ <ArrowLeftIcon className="size-4" aria-hidden={true} />
93
+ <span>Back</span>
87
94
  </a>
88
95
  </div>
89
96
  )}
@@ -6,7 +6,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
6
6
  import { createLoader } from "@/plugins/impl/vega/vega-loader";
7
7
  import { Functions } from "@/utils/functions";
8
8
  import type { DataURLString } from "@/utils/json/base64";
9
- import { patchFetch, patchVegaLoader } from "../files";
9
+ import { patchFetch, patchVegaLoader, resolveVirtualFileURL } from "../files";
10
10
 
11
11
  // Start a tiny server to serve virtual files
12
12
  const server = http.createServer((request, response) => {
@@ -350,6 +350,181 @@ describe("patchVegaLoader - loader.load", () => {
350
350
  });
351
351
  });
352
352
 
353
+ describe("resolveVirtualFileURL", () => {
354
+ // Mock URL.createObjectURL for jsdom environment
355
+ const mockBlobURLs = new Map<string, Blob>();
356
+ let blobCounter = 0;
357
+
358
+ beforeAll(() => {
359
+ URL.createObjectURL = vi.fn((blob: Blob) => {
360
+ const url = `blob:test-${blobCounter++}`;
361
+ mockBlobURLs.set(url, blob);
362
+ return url;
363
+ });
364
+ URL.revokeObjectURL = vi.fn((url: string) => {
365
+ mockBlobURLs.delete(url);
366
+ });
367
+ });
368
+
369
+ afterAll(() => {
370
+ mockBlobURLs.clear();
371
+ });
372
+
373
+ it("should return a blob URL for virtual files", () => {
374
+ const virtualFiles = {
375
+ "/@file/widget.js":
376
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQgeyByZW5kZXI6ICgpID0+IHt9IH0=" as DataURLString,
377
+ };
378
+
379
+ const result = resolveVirtualFileURL("/@file/widget.js", virtualFiles);
380
+
381
+ expect(result).toMatch(/^blob:/);
382
+ });
383
+
384
+ it("should return the original URL for non-virtual files", () => {
385
+ const virtualFiles = {};
386
+
387
+ const result = resolveVirtualFileURL(
388
+ "http://example.com/widget.js",
389
+ virtualFiles,
390
+ );
391
+
392
+ expect(result).toBe("http://example.com/widget.js");
393
+ });
394
+
395
+ it("should handle various URL formats", () => {
396
+ const virtualFiles = {
397
+ "/@file/module.js":
398
+ "data:text/javascript;base64,Y29uc29sZS5sb2coJ3Rlc3QnKQ==" as DataURLString,
399
+ };
400
+
401
+ const testUrls = [
402
+ "/@file/module.js",
403
+ "./@file/module.js",
404
+ "http://example.com/@file/module.js",
405
+ ];
406
+
407
+ for (const url of testUrls) {
408
+ const result = resolveVirtualFileURL(url, virtualFiles);
409
+ expect(result).toMatch(/^blob:/);
410
+ }
411
+ });
412
+
413
+ it("should create blob URL with correct content", async () => {
414
+ const jsCode = "export default { render: () => {} }";
415
+ const base64Code = btoa(jsCode);
416
+ const virtualFiles = {
417
+ "/@file/test-module.js":
418
+ `data:text/javascript;base64,${base64Code}` as DataURLString,
419
+ };
420
+
421
+ const blobUrl = resolveVirtualFileURL(
422
+ "/@file/test-module.js",
423
+ virtualFiles,
424
+ );
425
+
426
+ expect(blobUrl).toMatch(/^blob:/);
427
+ expect(URL.createObjectURL).toHaveBeenCalled();
428
+
429
+ // Verify blob content through the mock
430
+ const blob = mockBlobURLs.get(blobUrl);
431
+ expect(blob).toBeDefined();
432
+ const text = await blob!.text();
433
+ expect(text).toBe(jsCode);
434
+ });
435
+
436
+ it("should handle file:// URLs with @file/ paths", () => {
437
+ const virtualFiles = {
438
+ "/@file/local-module.js":
439
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=" as DataURLString,
440
+ };
441
+
442
+ const result = resolveVirtualFileURL(
443
+ "file:///Users/test/@file/local-module.js",
444
+ virtualFiles,
445
+ );
446
+
447
+ expect(result).toMatch(/^blob:/);
448
+ });
449
+
450
+ it("should handle different MIME types", async () => {
451
+ const virtualFiles = {
452
+ "/@file/script.js":
453
+ "data:application/javascript;base64,Y29uc3QgeCA9IDE=" as DataURLString,
454
+ };
455
+
456
+ const blobUrl = resolveVirtualFileURL("/@file/script.js", virtualFiles);
457
+
458
+ // Should still be a valid blob URL
459
+ expect(blobUrl).toMatch(/^blob:/);
460
+
461
+ // Verify blob content through the mock
462
+ const blob = mockBlobURLs.get(blobUrl);
463
+ expect(blob).toBeDefined();
464
+ const text = await blob!.text();
465
+ expect(text).toBe("const x = 1");
466
+ });
467
+
468
+ it("should handle blob: base URIs correctly", () => {
469
+ // Mock document.baseURI to simulate blob: protocol
470
+ const originalBaseURI = document.baseURI;
471
+ Object.defineProperty(document, "baseURI", {
472
+ value: "blob:https://example.com/uuid",
473
+ configurable: true,
474
+ });
475
+
476
+ const virtualFiles = {
477
+ "/@file/blob-module.js":
478
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=" as DataURLString,
479
+ };
480
+
481
+ const result = resolveVirtualFileURL("/@file/blob-module.js", virtualFiles);
482
+
483
+ expect(result).toMatch(/^blob:/);
484
+
485
+ // Restore original baseURI
486
+ Object.defineProperty(document, "baseURI", {
487
+ value: originalBaseURI,
488
+ configurable: true,
489
+ });
490
+ });
491
+
492
+ it("should handle data URLs with no explicit MIME type", async () => {
493
+ const virtualFiles = {
494
+ "/@file/generic.bin": "data:;base64,SGVsbG8gV29ybGQ=" as DataURLString,
495
+ };
496
+
497
+ const blobUrl = resolveVirtualFileURL("/@file/generic.bin", virtualFiles);
498
+ expect(blobUrl).toMatch(/^blob:/);
499
+
500
+ // Verify blob content through the mock
501
+ const blob = mockBlobURLs.get(blobUrl);
502
+ expect(blob).toBeDefined();
503
+ const text = await blob!.text();
504
+ expect(text).toBe("Hello World");
505
+ });
506
+
507
+ it("should match URLs with prefix paths before /@file/", async () => {
508
+ const virtualFiles = {
509
+ "/@file/4263-66-yUGhgQXp.js":
510
+ "data:application/javascript;base64,ZnVuY3Rpb24gcmVuZGVyKCkge30=" as DataURLString,
511
+ };
512
+
513
+ const blobUrl = resolveVirtualFileURL(
514
+ "https://molab.marimo.app/preview/@file/4263-66-yUGhgQXp.js",
515
+ virtualFiles,
516
+ );
517
+
518
+ expect(blobUrl).toMatch(/^blob:/);
519
+
520
+ // Verify blob content through the mock
521
+ const blob = mockBlobURLs.get(blobUrl);
522
+ expect(blob).toBeDefined();
523
+ const text = await blob!.text();
524
+ expect(text).toBe("function render() {}");
525
+ });
526
+ });
527
+
353
528
  describe("maybeGetVirtualFile utility function", () => {
354
529
  it("should handle URLs without leading dots correctly", async () => {
355
530
  const virtualFiles = {
@@ -370,6 +545,25 @@ describe("maybeGetVirtualFile utility function", () => {
370
545
  expect(text2).toBe("test");
371
546
  });
372
547
 
548
+ it("should match URLs with prefix paths before /@file/", async () => {
549
+ const virtualFiles = {
550
+ "/@file/4263-66-yUGhgQXp.js":
551
+ "data:application/javascript;base64,ZnVuY3Rpb24gcmVuZGVyKCkge30=" as DataURLString,
552
+ };
553
+
554
+ const unpatch = patchFetch(virtualFiles);
555
+
556
+ // Test URL with a prefix path before /@file/
557
+ const response = await window.fetch(
558
+ "https://molab.marimo.app/preview/@file/4263-66-yUGhgQXp.js",
559
+ );
560
+ const text = await response.text();
561
+
562
+ expect(text).toBe("function render() {}");
563
+
564
+ unpatch();
565
+ });
566
+
373
567
  it("should handle complex file:// URLs with nested paths", async () => {
374
568
  const virtualFiles = {
375
569
  "/@file/nested/data.json":
@@ -1,6 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { Loader } from "@/plugins/impl/vega/vega-loader";
4
+ import { deserializeBlob } from "@/utils/blob";
5
+ import type { DataURLString } from "@/utils/json/base64";
4
6
  import { Logger } from "@/utils/Logger";
5
7
  import { getStaticVirtualFiles } from "./static-state";
6
8
  import type { StaticVirtualFiles } from "./types";
@@ -120,6 +122,24 @@ function withoutLeadingDot(path: string): string {
120
122
  return path.startsWith(".") ? path.slice(1) : path;
121
123
  }
122
124
 
125
+ /**
126
+ * Resolve a URL to a blob URL if it's a virtual file, for use with dynamic import().
127
+ * Unlike fetch, import() can't be patched, so we need to convert data URLs to blob URLs.
128
+ *
129
+ * @returns The original URL if not a virtual file, or a blob URL if it is
130
+ */
131
+ export function resolveVirtualFileURL(
132
+ url: string,
133
+ files: StaticVirtualFiles = getStaticVirtualFiles(),
134
+ ): string {
135
+ const vfile = maybeGetVirtualFile(url, files);
136
+ if (!vfile) {
137
+ return url;
138
+ }
139
+ const blob = deserializeBlob(vfile as DataURLString);
140
+ return URL.createObjectURL(blob);
141
+ }
142
+
123
143
  function maybeGetVirtualFile(
124
144
  url: string,
125
145
  files: StaticVirtualFiles,
@@ -130,14 +150,11 @@ function maybeGetVirtualFile(
130
150
  }
131
151
  const pathname = new URL(url, base).pathname;
132
152
 
133
- // If if the URL starts with file://, then using the document.baseURI
134
- // will not work. In this case, should just chop off from /@file/...
135
- if (url.startsWith("file://")) {
136
- const indexOfFile = url.indexOf("/@file/");
137
- if (indexOfFile !== -1) {
138
- url = url.slice(indexOfFile);
139
- }
140
- }
153
+ // Extract the /@file/... suffix from the URL or pathname
154
+ // This handles URLs like https://example.com/prefix/@file/foo.js
155
+ // or file:///path/to/@file/foo.js
156
+ const filePathFromUrl = extractFilePath(url);
157
+ const filePathFromPathname = extractFilePath(pathname);
141
158
 
142
159
  // Few variations to grab the URL.
143
160
  // This can happen if a static file was open at file:// or https://
@@ -145,6 +162,19 @@ function maybeGetVirtualFile(
145
162
  files[url] ||
146
163
  files[withoutLeadingDot(url)] ||
147
164
  files[pathname] ||
148
- files[withoutLeadingDot(pathname)]
165
+ files[withoutLeadingDot(pathname)] ||
166
+ (filePathFromUrl && files[filePathFromUrl]) ||
167
+ (filePathFromPathname && files[filePathFromPathname])
149
168
  );
150
169
  }
170
+
171
+ /**
172
+ * Extract the /@file/... path from a URL string
173
+ */
174
+ function extractFilePath(url: string): string | null {
175
+ const indexOfFile = url.indexOf("/@file/");
176
+ if (indexOfFile !== -1) {
177
+ return url.slice(indexOfFile);
178
+ }
179
+ return null;
180
+ }
@@ -101,10 +101,17 @@ function PluginSlotInternal<T>(
101
101
  return plugin.validator.safeParse(parseDataset(hostElement));
102
102
  });
103
103
 
104
+ // Incremented on each reset to invalidate memoized function references.
105
+ // This ensures that plugin functions (e.g., search) are re-created when
106
+ // the underlying UI element instance changes (new object-id), even if
107
+ // the element's data attributes haven't changed.
108
+ const [resetNonce, setResetNonce] = useState(0);
109
+
104
110
  useImperativeHandle(ref, () => ({
105
111
  reset: () => {
106
112
  setValue(getInitialValue());
107
113
  setParsedResult(plugin.validator.safeParse(parseDataset(hostElement)));
114
+ setResetNonce((n) => n + 1);
108
115
  },
109
116
  setChildren: (children) => {
110
117
  setChildNodes(children);
@@ -224,7 +231,8 @@ function PluginSlotInternal<T>(
224
231
  }
225
232
 
226
233
  return methods;
227
- }, [plugin.functions, hostElement]);
234
+ // eslint-disable-next-line react-hooks/exhaustive-deps
235
+ }, [plugin.functions, hostElement, resetNonce]);
228
236
 
229
237
  // If we failed to parse the initial value, render an error
230
238
  if (!parsedResult.success) {