@marimo-team/islands 0.23.16-dev6 → 0.23.16-dev62

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 (198) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
  2. package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
  3. package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
  4. package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
  5. package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
  6. package/dist/{chat-ui-BdSDiVjk.js → chat-ui-DsZJj75A.js} +3117 -3120
  7. package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
  8. package/dist/{code-visibility-CfBftiVM.js → common-Bpnmu1Yz.js} +1085 -1053
  9. package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
  10. package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
  11. package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
  12. package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
  13. package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
  14. package/dist/{formats-BFhkZJwd.js → formats-Dwq1THhr.js} +3 -3
  15. package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
  16. package/dist/{html-to-image-DDENewl2.js → html-to-image-CZ1kLKkq.js} +7350 -6356
  17. package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
  18. package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
  19. package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
  20. package/dist/main.js +259 -194
  21. package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
  22. package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
  23. package/dist/{process-output-2QPcgBQg.js → process-output-bVfbcx5_.js} +32 -28
  24. package/dist/{reveal-component-BclLkYnV.js → reveal-component-JIt3oSeU.js} +642 -634
  25. package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
  26. package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
  27. package/dist/style.css +1 -1
  28. package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
  29. package/dist/toDate-CqhBWjvO.js +713 -0
  30. package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
  31. package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
  32. package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
  33. package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
  34. package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
  35. package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
  36. package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
  37. package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
  38. package/dist/{vega-component-CrFoZT1S.js → vega-component-C7nO5zcf.js} +10 -10
  39. package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
  40. package/package.json +3 -4
  41. package/src/__mocks__/requests.ts +56 -4
  42. package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
  43. package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
  44. package/src/components/data-table/__tests__/columns.test.tsx +9 -1
  45. package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
  46. package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
  47. package/src/components/data-table/column-summary/utils.ts +26 -7
  48. package/src/components/data-table/columns.tsx +11 -1
  49. package/src/components/data-table/types.ts +1 -1
  50. package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
  51. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
  52. package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
  53. package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
  54. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  55. package/src/components/editor/actions/pdf-export.ts +11 -14
  56. package/src/components/editor/actions/useNotebookActions.tsx +28 -19
  57. package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
  58. package/src/components/editor/cell/code/cell-editor.tsx +13 -7
  59. package/src/components/editor/chrome/__tests__/state.test.ts +7 -1
  60. package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
  61. package/src/components/editor/chrome/panels/__tests__/panel-accordion-state.test.ts +42 -0
  62. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +6 -17
  63. package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
  64. package/src/components/editor/chrome/panels/panel-accordion-state.ts +51 -0
  65. package/src/components/editor/chrome/panels/session-panel.tsx +5 -16
  66. package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
  67. package/src/components/editor/chrome/state.ts +10 -0
  68. package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
  69. package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
  70. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
  71. package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
  72. package/src/components/editor/connections/components.tsx +6 -2
  73. package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +27 -5
  74. package/src/components/editor/connections/database/__tests__/as-code.test.ts +33 -0
  75. package/src/components/editor/connections/database/as-code.ts +37 -13
  76. package/src/components/editor/connections/database/schemas.ts +6 -5
  77. package/src/components/editor/connections/form-renderers.tsx +99 -1
  78. package/src/components/editor/connections/json-credentials.ts +57 -0
  79. package/src/components/editor/connections/paths.ts +21 -0
  80. package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
  81. package/src/components/editor/connections/secret-combobox.tsx +41 -20
  82. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
  83. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
  84. package/src/components/editor/connections/storage/as-code.ts +79 -36
  85. package/src/components/editor/connections/storage/schemas.ts +85 -44
  86. package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
  87. package/src/components/editor/file-tree/file-viewer.tsx +11 -7
  88. package/src/components/editor/file-tree/renderers.tsx +21 -0
  89. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -2
  90. package/src/components/editor/viewer-banner.tsx +6 -1
  91. package/src/components/forms/form.tsx +59 -24
  92. package/src/components/forms/options.ts +2 -1
  93. package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
  94. package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
  95. package/src/components/slides/reveal-component.tsx +22 -17
  96. package/src/components/slides/slide-cell-view.tsx +1 -1
  97. package/src/components/slides/slides.css +4 -2
  98. package/src/components/static-html/share-modal.tsx +1 -1
  99. package/src/components/ui/input.tsx +1 -1
  100. package/src/core/ai/context/providers/file.ts +8 -29
  101. package/src/core/cells/__tests__/cells.test.ts +12 -2
  102. package/src/core/cells/__tests__/logs.test.ts +5 -5
  103. package/src/core/cells/cells.ts +17 -4
  104. package/src/core/cells/document-changes.ts +1 -0
  105. package/src/core/cells/logs.ts +14 -4
  106. package/src/core/codemirror/cm.ts +3 -0
  107. package/src/core/codemirror/code-lens/__tests__/actions.test.ts +68 -0
  108. package/src/core/codemirror/code-lens/__tests__/analyzer.test.ts +224 -0
  109. package/src/core/codemirror/code-lens/__tests__/entities.test.ts +175 -0
  110. package/src/core/codemirror/code-lens/__tests__/extension.test.ts +325 -0
  111. package/src/core/codemirror/code-lens/__tests__/perf.test.ts +121 -0
  112. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +93 -0
  113. package/src/core/codemirror/code-lens/actions.ts +33 -0
  114. package/src/core/codemirror/code-lens/analyzer.ts +284 -0
  115. package/src/core/codemirror/code-lens/entities.ts +123 -0
  116. package/src/core/codemirror/code-lens/extension.ts +298 -0
  117. package/src/core/codemirror/code-lens/icons.ts +36 -0
  118. package/src/core/codemirror/code-lens/popover.tsx +240 -0
  119. package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
  120. package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
  121. package/src/core/codemirror/copilot/language-server.ts +182 -70
  122. package/src/core/codemirror/copilot/transport.ts +15 -207
  123. package/src/core/codemirror/copilot/types.ts +9 -2
  124. package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
  125. package/src/core/codemirror/find-replace/navigate.ts +2 -0
  126. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
  127. package/src/core/codemirror/go-to-definition/commands.ts +93 -90
  128. package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
  129. package/src/core/codemirror/keymaps/keymaps.ts +12 -0
  130. package/src/core/codemirror/language/__tests__/sql.test.ts +627 -61
  131. package/src/core/codemirror/language/languages/python.ts +23 -11
  132. package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
  133. package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
  134. package/src/core/codemirror/language/languages/sql/sql.ts +251 -16
  135. package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
  136. package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
  137. package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
  138. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
  139. package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
  140. package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
  141. package/src/core/codemirror/lsp/log-messages.ts +37 -0
  142. package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
  143. package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
  144. package/src/core/codemirror/lsp/transports.ts +8 -1
  145. package/src/core/codemirror/lsp/types.ts +68 -8
  146. package/src/core/codemirror/lsp/utils.ts +14 -0
  147. package/src/core/codemirror/python-node-names.ts +90 -0
  148. package/src/core/codemirror/reactive-references/analyzer.ts +52 -57
  149. package/src/core/codemirror/utils.ts +28 -0
  150. package/src/core/config/feature-flag.tsx +3 -1
  151. package/src/core/datasets/data-source-discovery.ts +5 -0
  152. package/src/core/datasets/request-registry.ts +11 -0
  153. package/src/core/edit-app.tsx +1 -2
  154. package/src/core/export/__tests__/hooks.test.ts +2 -4
  155. package/src/core/export/hooks.ts +4 -3
  156. package/src/core/hotkeys/hotkeys.ts +0 -2
  157. package/src/core/islands/__tests__/bridge.test.ts +107 -0
  158. package/src/core/islands/bootstrap.ts +1 -0
  159. package/src/core/islands/bridge.ts +18 -0
  160. package/src/core/kernel/messages.ts +2 -0
  161. package/src/core/lsp/__tests__/transport.test.ts +509 -337
  162. package/src/core/lsp/transport.ts +304 -132
  163. package/src/core/mode.ts +10 -0
  164. package/src/core/network/__tests__/api.test.ts +42 -0
  165. package/src/core/network/__tests__/connection.test.ts +45 -0
  166. package/src/core/network/__tests__/requests-lazy.test.ts +47 -1
  167. package/src/core/network/__tests__/requests-network.test.ts +48 -0
  168. package/src/core/network/api.ts +27 -0
  169. package/src/core/network/connection.ts +15 -0
  170. package/src/core/network/requests-lazy.ts +3 -0
  171. package/src/core/network/requests-network.ts +31 -8
  172. package/src/core/network/requests-static.ts +3 -0
  173. package/src/core/network/requests-toasting.tsx +3 -0
  174. package/src/core/network/types.ts +27 -6
  175. package/src/core/rtc/state.ts +9 -0
  176. package/src/core/static/download-html.ts +5 -14
  177. package/src/core/wasm/__tests__/bridge.test.ts +99 -5
  178. package/src/core/wasm/bridge.ts +42 -9
  179. package/src/core/wasm/worker/types.ts +10 -3
  180. package/src/core/wasm/worker/worker.ts +7 -8
  181. package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
  182. package/src/css/app/codemirror.css +16 -2
  183. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
  184. package/src/hooks/useDataSourceDiscovery.ts +18 -0
  185. package/src/plugins/impl/DataTablePlugin.tsx +4 -1
  186. package/src/plugins/impl/FormPlugin.tsx +1 -1
  187. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +158 -0
  188. package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
  189. package/src/plugins/impl/anywidget/__tests__/model.test.ts +55 -0
  190. package/src/plugins/impl/anywidget/model.ts +20 -5
  191. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +33 -21
  192. package/src/plugins/impl/data-frames/forms/renderers.tsx +11 -8
  193. package/src/stories/log-viewer.stories.tsx +12 -6
  194. package/src/utils/__tests__/blob.test.ts +27 -1
  195. package/src/utils/__tests__/download.test.tsx +44 -4
  196. package/src/utils/blob.ts +24 -1
  197. package/src/utils/download.ts +14 -15
  198. package/dist/toDate-B3IRfHhw.js +0 -692
@@ -749,14 +749,8 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
749
749
  class="flex flex-col gap-1"
750
750
  >
751
751
  <div
752
- class="flex flex-row items-start space-x-2"
752
+ class="flex flex-row gap-2 items-center"
753
753
  >
754
- <label
755
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
756
- for="_r_1u_-form-item"
757
- >
758
- Drop N/A
759
- </label>
760
754
  <button
761
755
  aria-checked="false"
762
756
  aria-describedby="_r_1u_-form-item-description"
@@ -769,6 +763,16 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
769
763
  type="button"
770
764
  value="on"
771
765
  />
766
+ <div
767
+ class="flex flex-col gap-1"
768
+ >
769
+ <label
770
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
771
+ for="_r_1u_-form-item"
772
+ >
773
+ Drop N/A
774
+ </label>
775
+ </div>
772
776
  </div>
773
777
  </div>
774
778
  </div>
@@ -1252,14 +1256,8 @@ exports[`renderZodSchema > should render a form sample_rows 1`] = `
1252
1256
  class="flex flex-col gap-1"
1253
1257
  >
1254
1258
  <div
1255
- class="flex flex-row items-start space-x-2"
1259
+ class="flex flex-row gap-2 items-center"
1256
1260
  >
1257
- <label
1258
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1259
- for="_r_2n_-form-item"
1260
- >
1261
- Sample with replacement
1262
- </label>
1263
1261
  <button
1264
1262
  aria-checked="false"
1265
1263
  aria-describedby="_r_2n_-form-item-description"
@@ -1272,6 +1270,16 @@ exports[`renderZodSchema > should render a form sample_rows 1`] = `
1272
1270
  type="button"
1273
1271
  value="on"
1274
1272
  />
1273
+ <div
1274
+ class="flex flex-col gap-1"
1275
+ >
1276
+ <label
1277
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1278
+ for="_r_2n_-form-item"
1279
+ >
1280
+ Sample with replacement
1281
+ </label>
1282
+ </div>
1275
1283
  </div>
1276
1284
  </div>
1277
1285
  </div>
@@ -1468,14 +1476,8 @@ exports[`renderZodSchema > should render a form sort_column 1`] = `
1468
1476
  class="flex flex-col gap-1"
1469
1477
  >
1470
1478
  <div
1471
- class="flex flex-row items-start space-x-2"
1479
+ class="flex flex-row gap-2 items-center"
1472
1480
  >
1473
- <label
1474
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1475
- for="_r_1f_-form-item"
1476
- >
1477
- Ascending
1478
- </label>
1479
1481
  <button
1480
1482
  aria-checked="false"
1481
1483
  aria-describedby="_r_1f_-form-item-description"
@@ -1488,6 +1490,16 @@ exports[`renderZodSchema > should render a form sort_column 1`] = `
1488
1490
  type="button"
1489
1491
  value="on"
1490
1492
  />
1493
+ <div
1494
+ class="flex flex-col gap-1"
1495
+ >
1496
+ <label
1497
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1498
+ for="_r_1f_-form-item"
1499
+ >
1500
+ Ascending
1501
+ </label>
1502
+ </div>
1491
1503
  </div>
1492
1504
  </div>
1493
1505
  </div>
@@ -411,9 +411,12 @@ const ColumnFilterForm = <T extends FieldValues>({
411
411
 
412
412
  const columnRenderer = columnIdRenderer<T>();
413
413
  const children = [
414
- renderZodSchema(columnIdSchema, form, `${path}.column_id` as Path<T>, [
415
- columnRenderer,
416
- ]),
414
+ renderZodSchema({
415
+ schema: columnIdSchema,
416
+ form,
417
+ path: `${path}.column_id` as Path<T>,
418
+ renderers: [columnRenderer],
419
+ }),
417
420
  ];
418
421
 
419
422
  // When column ID changes, get the new dtype and reset the operator
@@ -486,12 +489,12 @@ const ColumnFilterForm = <T extends FieldValues>({
486
489
  if (operandSchemas.length === 1) {
487
490
  children.push(
488
491
  <React.Fragment key="value">
489
- {renderZodSchema(
490
- operandSchemas[0],
492
+ {renderZodSchema({
493
+ schema: operandSchemas[0],
491
494
  form,
492
- `${path}.value` as Path<T>,
493
- [],
494
- )}
495
+ path: `${path}.value` as Path<T>,
496
+ renderers: [],
497
+ })}
495
498
  </React.Fragment>,
496
499
  );
497
500
  }
@@ -23,37 +23,43 @@ export const Primary: Story = {
23
23
  {
24
24
  timestamp: Date.now(),
25
25
  level: "stdout",
26
- cellId: cellId("cell1"),
26
+ source: { type: "cell", cellId: cellId("cell1") },
27
27
  message: "Hello world!",
28
28
  },
29
29
  {
30
30
  timestamp: Date.now(),
31
31
  level: "stdout",
32
- cellId: cellId("cell1"),
32
+ source: { type: "cell", cellId: cellId("cell1") },
33
33
  message: "Running cell...",
34
34
  },
35
35
  {
36
36
  timestamp: Date.now(),
37
37
  level: "stdout",
38
- cellId: cellId("cell1"),
38
+ source: { type: "cell", cellId: cellId("cell1") },
39
39
  message: "Done!",
40
40
  },
41
41
  {
42
42
  timestamp: Date.now(),
43
43
  level: "stderr",
44
- cellId: cellId("cell2"),
44
+ source: { type: "cell", cellId: cellId("cell2") },
45
45
  message: "Output is too large!",
46
46
  },
47
47
  {
48
48
  timestamp: Date.now(),
49
49
  level: "stderr",
50
- cellId: cellId("cell2"),
50
+ source: { type: "cell", cellId: cellId("cell2") },
51
51
  message: "String length is too short.".repeat(100),
52
52
  },
53
+ {
54
+ timestamp: Date.now(),
55
+ level: "stderr",
56
+ source: { type: "lsp", name: "pylsp" },
57
+ message: "Failed to load plugin 'ruff': not installed",
58
+ },
53
59
  ...Array.from({ length: 100 }).map(() => ({
54
60
  timestamp: Date.now(),
55
61
  level: "stdout" as const,
56
- cellId: cellId("cell1"),
62
+ source: { type: "cell", cellId: cellId("cell1") } as const,
57
63
  message: "Running cell...",
58
64
  })),
59
65
  ]}
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { beforeEach, describe, expect, test } from "vitest";
3
- import { deserializeBlob, serializeBlob } from "../blob";
3
+ import { deserializeBlob, fileDetailsToBlob, serializeBlob } from "../blob";
4
4
  import { blobToString } from "../fileToBase64";
5
5
 
6
6
  describe("Blob serialization and deserialization", () => {
@@ -52,6 +52,32 @@ describe("Blob serialization and deserialization", () => {
52
52
  });
53
53
  });
54
54
 
55
+ describe("fileDetailsToBlob", () => {
56
+ test("decodes base64 contents into a binary blob", async () => {
57
+ const bytes = new Uint8Array([0, 1, 2, 3, 255]);
58
+ const base64 = btoa(String.fromCharCode(...bytes));
59
+ const blob = fileDetailsToBlob({
60
+ contents: base64,
61
+ mimeType: "image/png",
62
+ isBase64: true,
63
+ });
64
+ expect(blob.type).toBe("image/png");
65
+ const roundTripped = new Uint8Array(await blob.arrayBuffer());
66
+ expect(roundTripped).toEqual(bytes);
67
+ });
68
+
69
+ test("treats non-base64 contents as UTF-8 text", async () => {
70
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg"></svg>';
71
+ const blob = fileDetailsToBlob({
72
+ contents: svg,
73
+ mimeType: "image/svg+xml",
74
+ isBase64: false,
75
+ });
76
+ expect(blob.type).toBe("image/svg+xml");
77
+ expect(await blob.text()).toBe(svg);
78
+ });
79
+ });
80
+
55
81
  describe("blobToString", () => {
56
82
  const testString = "Hello, world!";
57
83
  const mimeType = "text/plain";
@@ -8,6 +8,7 @@ import {
8
8
  downloadAsPDF,
9
9
  downloadByURL,
10
10
  downloadCellOutputAsImage,
11
+ downloadExportedFile,
11
12
  downloadHTMLAsImage,
12
13
  getImageDataUrlForCell,
13
14
  withLoadingToast,
@@ -195,7 +196,6 @@ describe("downloadAsPDF", () => {
195
196
 
196
197
  await expect(
197
198
  downloadAsPDF({
198
- filename: "path/to/notebook.py",
199
199
  webpdf: false,
200
200
  preset: "slides",
201
201
  }),
@@ -205,9 +205,26 @@ describe("downloadAsPDF", () => {
205
205
  webpdf: false,
206
206
  preset: "slides",
207
207
  includeInputs: true,
208
- rasterizeOutputs: true,
209
- rasterScale: 4,
210
- rasterServer: "static",
208
+ includeOutputs: true,
209
+ });
210
+ });
211
+
212
+ it("should send input and output selections", async () => {
213
+ mockExportAsPDF.mockRejectedValue(new Error("network"));
214
+
215
+ await expect(
216
+ downloadAsPDF({
217
+ webpdf: true,
218
+ includeInputs: false,
219
+ includeOutputs: false,
220
+ }),
221
+ ).rejects.toThrow("network");
222
+
223
+ expect(mockExportAsPDF).toHaveBeenCalledWith({
224
+ webpdf: true,
225
+ preset: "document",
226
+ includeInputs: false,
227
+ includeOutputs: false,
211
228
  });
212
229
  });
213
230
  });
@@ -572,6 +589,7 @@ describe("downloadByURL", () => {
572
589
 
573
590
  afterEach(() => {
574
591
  vi.restoreAllMocks();
592
+ vi.unstubAllGlobals();
575
593
  });
576
594
 
577
595
  it("should create anchor, set attributes, click, and remove", () => {
@@ -583,4 +601,26 @@ describe("downloadByURL", () => {
583
601
  expect(mockAnchor.click).toHaveBeenCalled();
584
602
  expect(mockAnchor.remove).toHaveBeenCalled();
585
603
  });
604
+
605
+ it("downloads an exported file with its returned metadata", async () => {
606
+ let downloadedBlob: Blob | undefined;
607
+ vi.stubGlobal("URL", {
608
+ createObjectURL: vi.fn((blob: Blob) => {
609
+ downloadedBlob = blob;
610
+ return "blob:export";
611
+ }),
612
+ revokeObjectURL: vi.fn(),
613
+ });
614
+
615
+ downloadExportedFile({
616
+ contents: "notebook",
617
+ filename: "résumé.qmd",
618
+ mediaType: "text/plain; charset=utf-8",
619
+ });
620
+
621
+ expect(downloadedBlob?.type).toBe("text/plain; charset=utf-8");
622
+ expect(await downloadedBlob?.text()).toBe("notebook");
623
+ expect(mockAnchor.download).toBe("résumé.qmd");
624
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:export");
625
+ });
586
626
  });
package/src/utils/blob.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import type { DataURLString } from "./json/base64";
2
+ import {
3
+ type Base64String,
4
+ base64ToDataURL,
5
+ type DataURLString,
6
+ } from "./json/base64";
3
7
 
4
8
  export function serializeBlob<T>(blob: Blob): Promise<DataURLString> {
5
9
  return new Promise<DataURLString>((resolve, reject) => {
@@ -30,3 +34,22 @@ export function deserializeBlob(serializedBlob: DataURLString): Blob {
30
34
  const blob = new Blob([bytes], { type: mimeType });
31
35
  return blob;
32
36
  }
37
+
38
+ /**
39
+ * Convert file details into a `Blob`, using the authoritative `isBase64` flag
40
+ * to decide between decoding base64 bytes and treating contents as UTF-8 text.
41
+ */
42
+ export function fileDetailsToBlob({
43
+ contents,
44
+ mimeType,
45
+ isBase64,
46
+ }: {
47
+ contents: string;
48
+ mimeType: string;
49
+ isBase64: boolean;
50
+ }): Blob {
51
+ if (isBase64) {
52
+ return deserializeBlob(base64ToDataURL(contents as Base64String, mimeType));
53
+ }
54
+ return new Blob([contents], { type: mimeType });
55
+ }
@@ -4,8 +4,8 @@ import React from "react";
4
4
  import { toast } from "@/components/ui/use-toast";
5
5
  import { type CellId, CellOutputId } from "@/core/cells/ids";
6
6
  import { getRequestClient } from "@/core/network/requests";
7
+ import type { ExportedFile } from "@/core/network/types";
7
8
  import { Filenames } from "@/utils/filenames";
8
- import { Paths } from "@/utils/paths";
9
9
  import { prettyError } from "./errors";
10
10
  import { toPng } from "./html-to-image";
11
11
  import { captureExternalIframes } from "./iframe";
@@ -191,6 +191,14 @@ export function downloadBlob(blob: Blob, filename: string) {
191
191
  URL.revokeObjectURL(url);
192
192
  }
193
193
 
194
+ export function downloadExportedFile({
195
+ contents,
196
+ filename,
197
+ mediaType,
198
+ }: ExportedFile) {
199
+ downloadBlob(new Blob([contents], { type: mediaType }), filename);
200
+ }
201
+
194
202
  export type PDFExportPreset = "document" | "slides";
195
203
 
196
204
  /**
@@ -200,37 +208,28 @@ export type PDFExportPreset = "document" | "slides";
200
208
  * Standard PDF requires Pandoc & TeX (~few GBs) but is of higher quality.
201
209
  */
202
210
  export async function downloadAsPDF(opts: {
203
- filename: string;
204
211
  webpdf: boolean;
205
212
  preset?: PDFExportPreset;
206
213
  includeInputs?: boolean;
207
- rasterizeOutputs?: boolean;
208
- rasterScale?: number;
209
- rasterServer?: "static" | "live";
214
+ includeOutputs?: boolean;
210
215
  }) {
211
216
  const client = getRequestClient();
212
217
  const {
213
- filename,
214
218
  webpdf,
215
219
  preset = "document",
216
220
  includeInputs = true,
217
- rasterizeOutputs = true,
218
- rasterScale = 4,
219
- rasterServer = "static",
221
+ includeOutputs = true,
220
222
  } = opts;
221
223
 
222
224
  try {
223
- const pdfBlob = await client.exportAsPDF({
225
+ const exportedFile = await client.exportAsPDF({
224
226
  webpdf,
225
227
  preset,
226
228
  includeInputs,
227
- rasterizeOutputs,
228
- rasterScale,
229
- rasterServer,
229
+ includeOutputs,
230
230
  });
231
231
 
232
- const filenameWithoutPath = Paths.basename(filename);
233
- downloadBlob(pdfBlob, Filenames.toPDF(filenameWithoutPath));
232
+ downloadExportedFile(exportedFile);
234
233
  } catch (error) {
235
234
  toast({
236
235
  title: "Failed to download",