@marimo-team/frontend 0.25.3-dev2 → 0.25.3-dev21

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 (183) hide show
  1. package/dist/assets/{CellStatus-BP2mavAq.js → CellStatus-O0rU3_5j.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-TprnxZeJ.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-Gl5nCA8C.js → ErrorBoundary-BkSCfv3B.js} +2 -2
  4. package/dist/assets/JsonOutput-Dq6MGIMV.js +11 -0
  5. package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-B4fCknq1.js} +2 -2
  6. package/dist/assets/MarimoErrorOutput-BLtCVffa.js +10 -0
  7. package/dist/assets/{RenderHTML-BbOqmHy3.js → RenderHTML-PtQ99V0D.js} +1 -1
  8. package/dist/assets/{RunButton-CK3LhnP5.js → RunButton-B8KRdPZ-.js} +1 -1
  9. package/dist/assets/{add-cell-with-ai-8UBC0xwR.js → add-cell-with-ai-BOBZjUxI.js} +13 -13
  10. package/dist/assets/{add-connection-dialog-DXQnd6eB.js → add-connection-dialog-DP62zXw2.js} +2 -2
  11. package/dist/assets/{agent-panel-D7kMZLG-.js → agent-panel-BFul7UiF.js} +5 -5
  12. package/dist/assets/ai-model-dropdown-sTKkdrbX.js +13 -0
  13. package/dist/assets/{any-language-editor-BLCAD3yx.js → any-language-editor-BdTWdMCI.js} +1 -1
  14. package/dist/assets/{app-config-button-BKEG4x9u.js → app-config-button-Bp44UZdD.js} +1 -1
  15. package/dist/assets/{blob-D5nfNHOo.js → blob-ykpU69Lk.js} +1 -1
  16. package/dist/assets/{cell-editor-ADVfycpN.js → cell-editor-B7YlotyH.js} +9 -9
  17. package/dist/assets/{cell-link-LSth_8hT.js → cell-link-CmO9D2a2.js} +1 -1
  18. package/dist/assets/{cells-BRkCaSsh.js → cells-BMs-CYDt.js} +31 -31
  19. package/dist/assets/{chat-display-Dnagy4Nv.js → chat-display-Dw8HQAAY.js} +1 -1
  20. package/dist/assets/{chat-panel-DQ6HkWEF.js → chat-panel-DPDZrfoJ.js} +2 -2
  21. package/dist/assets/{chat-ui-Df8Vcj6n.js → chat-ui-BxnFw9Za.js} +1 -1
  22. package/dist/assets/code-visibility-CGVmpSdE.js +1 -0
  23. package/dist/assets/coerce-DWjFeXOY.js +1 -0
  24. package/dist/assets/{column-preview-Cm2D6vyX.js → column-preview-BQ4k86Lv.js} +1 -1
  25. package/dist/assets/{command-palette-DTSQ7gOC.js → command-palette-BfxYnBUD.js} +1 -1
  26. package/dist/assets/{common-Cu6CDbuw.js → common-CzWTWhcY.js} +1 -1
  27. package/dist/assets/{components-CGw16R_N.js → components-C-p2nJ78.js} +1 -1
  28. package/dist/assets/{components-CP_UoFtT.js → components-CskjK6xZ.js} +1 -1
  29. package/dist/assets/{components-CXfO-F47.js → components-YBmtV0p6.js} +1 -1
  30. package/dist/assets/config-BtRLgrlt.js +104 -0
  31. package/dist/assets/{config-VPSq73n8.js → config-CA1V-wjh.js} +1 -1
  32. package/dist/assets/config-CMUeR8Wr.js +1 -0
  33. package/dist/assets/{datasource-Cq6x_5ML.js → datasource-CEDUN47W.js} +1 -1
  34. package/dist/assets/{dates-BbFQ2oE4.js → dates-CfmdTL6B.js} +1 -1
  35. package/dist/assets/{dependency-graph-panel--Kgtktcw.js → dependency-graph-panel-KWuUrABU.js} +1 -1
  36. package/dist/assets/{documentation-panel-BHmbeznw.js → documentation-panel-T1wYs9YS.js} +1 -1
  37. package/dist/assets/{download-DCJXy66H.js → download-Bt4rM2gS.js} +1 -1
  38. package/dist/assets/edit-page-DIxL37jf.js +9 -0
  39. package/dist/assets/{error-banner-Cd69vvyX.js → error-banner-COZ7SfYV.js} +1 -1
  40. package/dist/assets/{error-entries-Dk3VZ-OK.js → error-entries-CQTemuon.js} +1 -1
  41. package/dist/assets/{error-panel-BPRYmC5J.js → error-panel-BmlN965r.js} +1 -1
  42. package/dist/assets/{errors-DCrsa1nH.js → errors-CNsgAw6m.js} +1 -1
  43. package/dist/assets/{feedback-button-Bik2-e33.js → feedback-button-CO89l9HJ.js} +2 -2
  44. package/dist/assets/feedback-button-QWfd9e1X.js +1 -0
  45. package/dist/assets/{file-explorer-panel-BEEK5lcl.js → file-explorer-panel-Bs25Xwng.js} +1 -1
  46. package/dist/assets/{file-name-input-Cdc-TVqO.js → file-name-input-DgR3ynbz.js} +1 -1
  47. package/dist/assets/{floating-outline-XD9FAV-f.js → floating-outline-kOJ_XOxZ.js} +1 -1
  48. package/dist/assets/{gallery-page-Dm6mGwBi.js → gallery-page--AiS7R2S.js} +1 -1
  49. package/dist/assets/{glide-data-editor-J9CAb70d.js → glide-data-editor-FopeFe04.js} +1 -1
  50. package/dist/assets/{home-page-DMHnsRxm.js → home-page-DEUEDKZQ.js} +1 -1
  51. package/dist/assets/hooks-CsCHI0xl.js +1 -0
  52. package/dist/assets/index-6b4mf8Ai.css +2 -0
  53. package/dist/assets/index-BXheV3uz.js +30 -0
  54. package/dist/assets/{layout-DmcNEu3P.js → layout-CcQIAduN.js} +5 -5
  55. package/dist/assets/{logs-panel-ABM16htT.js → logs-panel-Cj-mRfbP.js} +1 -1
  56. package/dist/assets/{markdown-renderer-BG3sp2rs.js → markdown-renderer-Dg2Tj8xg.js} +1 -1
  57. package/dist/assets/{mermaid-a3xYymTy.js → mermaid-C4EiVqcd.js} +1 -1
  58. package/dist/assets/{name-cell-input-BgEBmqyL.js → name-cell-input-DhWcLBL9.js} +1 -1
  59. package/dist/assets/{outline-panel-DCXQ3_F6.js → outline-panel-DCCN7CCz.js} +1 -1
  60. package/dist/assets/{packages-panel-BMGOmZkR.js → packages-panel-cI3VRnI0.js} +1 -1
  61. package/dist/assets/{panel-accordion-state-C1sdPp0m.js → panel-accordion-state-CrUAB8rV.js} +1 -1
  62. package/dist/assets/panels-DeK2CjCU.js +4 -0
  63. package/dist/assets/{process-output-D0KGq04R.js → process-output-C0AVlTcL.js} +1 -1
  64. package/dist/assets/{readonly-code-display-Do50PHI8.js → readonly-code-display-CEGKPKlf.js} +1 -1
  65. package/dist/assets/{readonly-python-code-DCSMIsTX.js → readonly-python-code-kOLuuvuL.js} +1 -1
  66. package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-C1JwYcqW.js} +1 -1
  67. package/dist/assets/{reveal-component-ltaepAqw.js → reveal-component-m_Q_Dhsa.js} +3 -3
  68. package/dist/assets/{run-page-DF3c5cDk.js → run-page-ZPnT0tT8.js} +1 -1
  69. package/dist/assets/{runtime-details-state-CSPobEdR.js → runtime-details-state-DRpKx1d7.js} +1 -1
  70. package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
  71. package/dist/assets/{scratchpad-panel-Dwd6W5PE.js → scratchpad-panel-4r5I7_4T.js} +1 -1
  72. package/dist/assets/{secrets-panel-Bctbtkrp.js → secrets-panel-D7KN4tVs.js} +1 -1
  73. package/dist/assets/session-panel-B-fUHivk.js +1 -0
  74. package/dist/assets/{snippets-panel-Bh1oTw4m.js → snippets-panel-QqliOy2l.js} +1 -1
  75. package/dist/assets/state-BAKG3nB6.js +3 -0
  76. package/dist/assets/{state-XsPO8hNi.js → state-BiuybHTd.js} +1 -1
  77. package/dist/assets/{terminal-Ck_vK-34.js → terminal-CP30at9D.js} +3 -3
  78. package/dist/assets/{tracing-Di6pUmX8.js → tracing-Cg6nJ9oa.js} +1 -1
  79. package/dist/assets/{tracing-panel-PeRXHD8_.js → tracing-panel-_OkwL3ST.js} +2 -2
  80. package/dist/assets/{tree-actions-GM5E_bUs.js → tree-actions-D5U7Yym8.js} +1 -1
  81. package/dist/assets/{useAddCell-GR7eEQrT.js → useAddCell-C3jdhSc9.js} +1 -1
  82. package/dist/assets/{useBoolean-BDD6_xQQ.js → useBoolean-BHCcNH_F.js} +1 -1
  83. package/dist/assets/{useCellActionButton-B-9eG5z_.js → useCellActionButton-Db-2kvJ0.js} +1 -1
  84. package/dist/assets/{useDeleteCell-2_bMClHd.js → useDeleteCell-D0dwhgwH.js} +1 -1
  85. package/dist/assets/{useDependencyPanelTab-k3C3M18S.js → useDependencyPanelTab-BjQVPzm0.js} +1 -1
  86. package/dist/assets/{useInstallPackage-iX1eufdl.js → useInstallPackage-B-krIeGp.js} +1 -1
  87. package/dist/assets/useNotebookActions-CY_gTNzg.js +1 -0
  88. package/dist/assets/{useRunCells-B2vUhREg.js → useRunCells-Dyf_vzJl.js} +1 -1
  89. package/dist/assets/{useSplitCell-DEb03htp.js → useSplitCell-DA8ujlOv.js} +1 -1
  90. package/dist/assets/{useTheme-COhSHkCj.js → useTheme-n8Dj1zH7.js} +1 -1
  91. package/dist/assets/{vega-component-CnadTgoE.js → vega-component-CaZvZfgt.js} +1 -1
  92. package/dist/index.html +40 -41
  93. package/package.json +1 -1
  94. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  95. package/src/__tests__/mount.test.ts +28 -8
  96. package/src/components/data-table/__tests__/context-menu.test.ts +176 -0
  97. package/src/components/data-table/__tests__/date-filter-inputs.test.tsx +24 -1
  98. package/src/components/data-table/__tests__/export-actions.test.tsx +329 -6
  99. package/src/components/data-table/context-menu.tsx +125 -26
  100. package/src/components/data-table/date-filter-inputs.tsx +15 -42
  101. package/src/components/data-table/export-actions.tsx +192 -116
  102. package/src/components/data-table/schemas.ts +14 -36
  103. package/src/components/data-table/utils.ts +18 -0
  104. package/src/components/editor/Output.tsx +9 -2
  105. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  106. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  107. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  108. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  109. package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
  110. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +366 -0
  111. package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
  112. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
  113. package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
  114. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +537 -0
  115. package/src/components/editor/errors/send-error-report-button.tsx +69 -0
  116. package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
  117. package/src/components/editor/output/MarimoTracebackOutput.tsx +35 -20
  118. package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
  119. package/src/components/editor/output/ansi-reduce.ts +6 -0
  120. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  121. package/src/core/attachments/state.ts +18 -0
  122. package/src/core/attachments/toast.ts +15 -0
  123. package/src/core/cells/__tests__/cells.test.ts +52 -0
  124. package/src/core/cells/__tests__/error-report.test.ts +139 -0
  125. package/src/core/cells/__tests__/session.test.ts +50 -1
  126. package/src/core/cells/error-report.ts +63 -0
  127. package/src/core/cells/session.ts +28 -1
  128. package/src/core/config/pair.ts +5 -3
  129. package/src/core/edit-app.tsx +2 -2
  130. package/src/core/host/AGENTS.md +5 -0
  131. package/src/core/host/__tests__/attachments.test.ts +57 -0
  132. package/src/core/host/__tests__/document.test.ts +124 -0
  133. package/src/core/host/__tests__/events.test.ts +46 -0
  134. package/src/core/host/__tests__/fixtures.ts +39 -0
  135. package/src/core/host/__tests__/notebook-client.test.ts +433 -0
  136. package/src/core/host/__tests__/stream.test.ts +211 -0
  137. package/src/core/host/connection.ts +81 -0
  138. package/src/core/host/events.ts +61 -0
  139. package/src/core/host/notebook-client.ts +290 -0
  140. package/src/core/host/resources/attachments.ts +19 -0
  141. package/src/core/host/resources/document.ts +101 -0
  142. package/src/core/host/resources/resource.ts +59 -0
  143. package/src/core/host/stream.ts +213 -0
  144. package/src/core/network/connection.ts +4 -2
  145. package/src/core/run-app.tsx +2 -2
  146. package/src/core/runtime/__tests__/runtime.test.ts +55 -11
  147. package/src/core/runtime/runtime.ts +15 -1
  148. package/src/core/runtime/types.ts +2 -0
  149. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  150. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  151. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +170 -0
  152. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +181 -65
  153. package/src/core/websocket/connection-state.ts +70 -0
  154. package/src/core/websocket/state.ts +7 -0
  155. package/src/core/websocket/transports/__tests__/sse.test.ts +1 -4
  156. package/src/core/websocket/transports/create.ts +31 -0
  157. package/src/core/websocket/transports/transport.ts +12 -23
  158. package/src/core/websocket/types.ts +9 -39
  159. package/src/core/websocket/useNotebookClient.ts +88 -0
  160. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +59 -225
  161. package/src/mount.tsx +7 -3
  162. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
  163. package/src/utils/__tests__/dates.test.ts +116 -0
  164. package/src/utils/dates.ts +94 -0
  165. package/dist/assets/JsonOutput-D6HIIsCb.js +0 -11
  166. package/dist/assets/MarimoErrorOutput-D60XkCny.js +0 -6
  167. package/dist/assets/ai-model-dropdown-B0z6-zhp.js +0 -5
  168. package/dist/assets/api-BeC_VSGv.js +0 -1
  169. package/dist/assets/code-visibility-B-_GvwR5.js +0 -1
  170. package/dist/assets/coerce-CpE6kUdX.js +0 -1
  171. package/dist/assets/config-BOKzA3tF.js +0 -1
  172. package/dist/assets/config-DX8Y2WbJ.js +0 -1
  173. package/dist/assets/edit-page-ChcSgpke.js +0 -9
  174. package/dist/assets/feedback-button-B2gKC5cn.js +0 -1
  175. package/dist/assets/hooks-93IwHd2u.js +0 -1
  176. package/dist/assets/index-Bcdzex-3.css +0 -2
  177. package/dist/assets/index-s-M1OXjs.js +0 -29
  178. package/dist/assets/pair-CaQyK6En.js +0 -1
  179. package/dist/assets/panels-CxVG8-v2.js +0 -4
  180. package/dist/assets/session-panel-VdDhEBa-.js +0 -1
  181. package/dist/assets/state-_Qai_QyX.js +0 -3
  182. package/dist/assets/useNotebookActions-NHN6A-cN.js +0 -1
  183. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -14,6 +14,7 @@ import {
14
14
  TableIcon,
15
15
  } from "lucide-react";
16
16
  import React from "react";
17
+ import useEvent from "react-use-event-hook";
17
18
  import { downloadSizeLimitAtom } from "./download-policy/atoms";
18
19
  import type {
19
20
  DownloadAsArgs,
@@ -22,6 +23,7 @@ import type {
22
23
  ExportMetadata,
23
24
  GetExportMetadata,
24
25
  } from "./schemas";
26
+ import { useAsyncData } from "@/hooks/useAsyncData";
25
27
  import { logNever } from "@/utils/assertNever";
26
28
  import { cn } from "@/utils/cn";
27
29
  import { copyToClipboard } from "@/utils/copy";
@@ -274,19 +276,130 @@ const failureTitleForAction = (action: ExportAction): string =>
274
276
  const failureDescription = (error: unknown): string =>
275
277
  typeof error === "string" ? error : prettyError(error);
276
278
 
279
+ const OBSOLETE_GEOMETRY_MESSAGE =
280
+ "The selected geometry is no longer available. Choose another geometry to export.";
281
+
282
+ type GeographicBlocker =
283
+ | "format-unavailable"
284
+ | "geometry-required"
285
+ | "choose-geometry"
286
+ | "obsolete-geometry"
287
+ | "missing-crs";
288
+
289
+ /**
290
+ * Find the first reason that a geographic export cannot start with the
291
+ * selected geometry. The dialog uses it to disable a format, and the retry
292
+ * after a package installation uses it to decide whether to export.
293
+ */
294
+ function geographicBlocker(
295
+ metadata: ExportMetadata,
296
+ format: GeographicFormat,
297
+ geometryColumn: string | null,
298
+ ): GeographicBlocker | null {
299
+ if (metadata.formats[format]?.available === false) {
300
+ return "format-unavailable";
301
+ }
302
+ const selected = metadata.geometry_columns.find(
303
+ (column) => column.name === geometryColumn,
304
+ );
305
+ if (geometryColumn !== null && !selected) {
306
+ return "obsolete-geometry";
307
+ }
308
+ if (metadata.geometry_columns.length === 0) {
309
+ return format === "geojson" ? "geometry-required" : null;
310
+ }
311
+ if (!selected) {
312
+ return "choose-geometry";
313
+ }
314
+ if (format === "geojson" && !selected.crs) {
315
+ return "missing-crs";
316
+ }
317
+ return null;
318
+ }
319
+
320
+ const geometryDisplayName = (name: string | null): string | null =>
321
+ name === "" ? "(unnamed geometry)" : name;
322
+
323
+ function geographicBlockerReason(
324
+ blocker: GeographicBlocker | null,
325
+ formatLabel: string,
326
+ geometryColumn: string | null,
327
+ ): string | null {
328
+ switch (blocker) {
329
+ case "choose-geometry":
330
+ return `Choose a primary geometry column to export ${formatLabel}.`;
331
+ case "obsolete-geometry":
332
+ return OBSOLETE_GEOMETRY_MESSAGE;
333
+ case "missing-crs":
334
+ return `Geometry column ${geometryDisplayName(geometryColumn)} needs a CRS for GeoJSON export. Declare its source CRS in Python before exporting.`;
335
+ case "format-unavailable":
336
+ case "geometry-required":
337
+ case null:
338
+ return null;
339
+ default:
340
+ logNever(blocker);
341
+ return null;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * One open dialog for one table source. Closing the dialog or replacing the
347
+ * source creates a new context, and work started under an older context must
348
+ * not change the dialog or start an export.
349
+ */
350
+ interface ExportContext {
351
+ source: unknown;
352
+ }
353
+
277
354
  export const ExportActions: React.FC<ExportActionProps> = (props) => {
278
355
  const { getExportMetadata, metadataSource } = props;
279
356
  const [exportDialogOpen, setExportDialogOpen] = React.useState(false);
280
357
  const [failure, setFailure] = React.useState<ExportFailure | null>(null);
281
358
  const [settings, setSettings] =
282
359
  React.useState<ExportSettings>(DEFAULT_SETTINGS);
283
- const [metadata, setMetadata] = React.useState<ExportMetadata | null>(null);
284
- const [metadataLoading, setMetadataLoading] = React.useState(false);
285
- const [metadataError, setMetadataError] = React.useState<string | null>(null);
286
- const [metadataRefresh, setMetadataRefresh] = React.useState(0);
360
+ const [exportContext, setExportContext] = React.useState<ExportContext>(
361
+ () => ({ source: metadataSource }),
362
+ );
363
+ if (exportContext.source !== metadataSource) {
364
+ setExportContext({ source: metadataSource });
365
+ }
366
+ const mounted = React.useRef(false);
367
+ const isCurrentContext = useEvent(
368
+ (context: ExportContext) => mounted.current && context === exportContext,
369
+ );
370
+ const metadataRequest = useAsyncData(async () => {
371
+ if (!exportDialogOpen || !getExportMetadata) {
372
+ return null;
373
+ }
374
+ return { context: exportContext, metadata: await getExportMetadata({}) };
375
+ }, [exportDialogOpen, getExportMetadata, exportContext]);
376
+ // The hook keeps earlier data while it reloads and after a failure. Only a
377
+ // completed response for the current context can enable an export.
378
+ const metadata =
379
+ metadataRequest.status === "success" &&
380
+ metadataRequest.data?.context === exportContext
381
+ ? metadataRequest.data.metadata
382
+ : null;
383
+ const metadataError =
384
+ metadataRequest.status === "error"
385
+ ? failureDescription(metadataRequest.error)
386
+ : null;
287
387
  const [geometryColumn, setGeometryColumn] = React.useState<string | null>(
288
388
  null,
289
389
  );
390
+ const [reconciledMetadata, setReconciledMetadata] =
391
+ React.useState<ExportMetadata | null>(null);
392
+ if (metadata !== reconciledMetadata) {
393
+ setReconciledMetadata(metadata);
394
+ if (metadata) {
395
+ setGeometryColumn((current) =>
396
+ current !== null &&
397
+ metadata.geometry_columns.some((column) => column.name === current)
398
+ ? current
399
+ : metadata.default_geometry_column,
400
+ );
401
+ }
402
+ }
290
403
  const hasGeometry = !!metadata?.geometry_columns.length;
291
404
  const [expandedFormat, setExpandedFormat] = React.useState<
292
405
  ConfigurableFormat | GeographicFormat | null
@@ -294,43 +407,17 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
294
407
  const triggerRef = React.useRef<HTMLButtonElement>(null);
295
408
  const dialogRef = React.useRef<HTMLDivElement>(null);
296
409
  const latestActionId = React.useRef(0);
410
+ React.useLayoutEffect(() => {
411
+ mounted.current = true;
412
+ return () => {
413
+ mounted.current = false;
414
+ latestActionId.current += 1;
415
+ };
416
+ }, []);
297
417
  React.useEffect(() => {
298
418
  latestActionId.current += 1;
419
+ setFailure(null);
299
420
  }, [metadataSource]);
300
- React.useEffect(() => {
301
- if (!exportDialogOpen || !getExportMetadata) {
302
- return;
303
- }
304
- let active = true;
305
- setMetadata(null);
306
- setMetadataLoading(true);
307
- setMetadataError(null);
308
- void getExportMetadata({}).then(
309
- (result) => {
310
- if (!active) {
311
- return;
312
- }
313
- setMetadata(result);
314
- setGeometryColumn((current) =>
315
- current !== null &&
316
- result.geometry_columns.some((column) => column.name === current)
317
- ? current
318
- : result.default_geometry_column,
319
- );
320
- setMetadataLoading(false);
321
- },
322
- (error: unknown) => {
323
- if (!active) {
324
- return;
325
- }
326
- setMetadataError(failureDescription(error));
327
- setMetadataLoading(false);
328
- },
329
- );
330
- return () => {
331
- active = false;
332
- };
333
- }, [exportDialogOpen, getExportMetadata, metadataSource, metadataRefresh]);
334
421
  const policy = useAtomValue(downloadSizeLimitAtom);
335
422
  const overLimit = !!(
336
423
  policy &&
@@ -368,9 +455,8 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
368
455
  setFailure(null);
369
456
  setExpandedFormat(null);
370
457
  setSettings(DEFAULT_SETTINGS);
371
- setMetadata(null);
372
- setMetadataError(null);
373
458
  setGeometryColumn(null);
459
+ setExportContext({ source: metadataSource });
374
460
  }
375
461
  };
376
462
 
@@ -553,57 +639,67 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
553
639
  }
554
640
  };
555
641
 
556
- const retryAfterInstall = async (action: ExportAction) => {
557
- if (isGeographic(action.format) && getExportMetadata) {
642
+ // Package installation can finish after the dialog closes or the source
643
+ // changes, so the retry carries the context in which installation began.
644
+ const retryAfterInstall = useEvent(
645
+ async (action: ExportAction, context: ExportContext) => {
646
+ if (!isCurrentContext(context)) {
647
+ return;
648
+ }
649
+ if (!isGeographic(action.format) || !getExportMetadata) {
650
+ retryAction(action);
651
+ return;
652
+ }
558
653
  const actionId = latestActionId.current;
654
+ const isCurrent = () =>
655
+ isCurrentContext(context) && actionId === latestActionId.current;
559
656
  let result: ExportMetadata;
560
657
  try {
561
658
  result = await getExportMetadata({});
562
- } catch (error) {
563
- if (actionId === latestActionId.current) {
564
- setMetadataError(failureDescription(error));
659
+ } catch {
660
+ if (isCurrent()) {
661
+ metadataRequest.refetch();
565
662
  }
566
663
  return;
567
664
  }
568
- if (actionId !== latestActionId.current) {
665
+ if (!isCurrent()) {
569
666
  return;
570
667
  }
571
- setMetadata(result);
572
- setMetadataError(null);
573
- if (result.formats[action.format]?.available === false) {
574
- setFailure(null);
575
- return;
576
- }
577
- if (
578
- geometryColumn !== null &&
579
- !result.geometry_columns.some(
580
- (column) => column.name === geometryColumn,
581
- )
582
- ) {
583
- setGeometryColumn(result.default_geometry_column);
584
- setActionFailure(actionId, {
585
- kind: "error",
586
- title: failureTitleForAction(action),
587
- description:
588
- "The selected geometry is no longer available. Choose another geometry to export.",
589
- action,
590
- });
591
- return;
592
- }
593
- if (result.geometry_columns.length > 0 && geometryColumn === null) {
594
- return;
595
- }
596
- if (
597
- action.format === "geojson" &&
598
- !result.geometry_columns.find(
599
- (column) => column.name === geometryColumn,
600
- )?.crs
601
- ) {
602
- setFailure(null);
603
- return;
668
+ // Validation reads its own response, so a display refresh cannot
669
+ // start an export that this check rejected.
670
+ metadataRequest.refetch();
671
+ const blocker = geographicBlocker(result, action.format, geometryColumn);
672
+ switch (blocker) {
673
+ case null:
674
+ retryAction(action);
675
+ return;
676
+ case "obsolete-geometry":
677
+ setGeometryColumn(result.default_geometry_column);
678
+ setActionFailure(actionId, {
679
+ kind: "error",
680
+ title: failureTitleForAction(action),
681
+ description: OBSOLETE_GEOMETRY_MESSAGE,
682
+ action,
683
+ });
684
+ return;
685
+ case "choose-geometry":
686
+ return;
687
+ case "format-unavailable":
688
+ case "geometry-required":
689
+ case "missing-crs":
690
+ setFailure(null);
691
+ return;
692
+ default:
693
+ logNever(blocker);
604
694
  }
605
- }
606
- retryAction(action);
695
+ },
696
+ );
697
+
698
+ const createInstallRetry = (action: ExportAction) => {
699
+ const context = exportContext;
700
+ return () => {
701
+ void retryAfterInstall(action, context);
702
+ };
607
703
  };
608
704
 
609
705
  const toggleExpanded = (format: ExportFormat) => {
@@ -670,9 +766,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
670
766
  failure?.action.format === option.format ? failure : null;
671
767
  const summary =
672
768
  isGeographic(option.format) && hasGeometry
673
- ? geometryColumn === ""
674
- ? "(unnamed geometry)"
675
- : (geometryColumn ?? "Choose geometry")
769
+ ? (geometryDisplayName(geometryColumn) ?? "Choose geometry")
676
770
  : settingsSummary(settings, option.format);
677
771
  const selectedGeometryIndex =
678
772
  metadata?.geometry_columns.findIndex(
@@ -681,20 +775,14 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
681
775
  const eligibility = isGeographic(option.format)
682
776
  ? metadata?.formats[option.format]
683
777
  : undefined;
684
- const selectedGeometry =
685
- metadata?.geometry_columns[selectedGeometryIndex];
686
- const missingCrs =
687
- option.format === "geojson" &&
688
- selectedGeometry !== undefined &&
689
- !selectedGeometry.crs;
778
+ const blocker =
779
+ isGeographic(option.format) && metadata
780
+ ? geographicBlocker(metadata, option.format, geometryColumn)
781
+ : null;
690
782
  const geometryReason = metadataError
691
783
  ? `Could not load geometry options: ${metadataError}`
692
784
  : (eligibility?.reason ??
693
- (hasGeometry && geometryColumn === null
694
- ? `Choose a primary geometry column to export ${optionLabel}.`
695
- : missingCrs
696
- ? `Geometry column ${geometryColumn === "" ? "(unnamed geometry)" : geometryColumn} needs a CRS for GeoJSON export. Declare its source CRS in Python before exporting.`
697
- : null));
785
+ geographicBlockerReason(blocker, optionLabel, geometryColumn));
698
786
  const missingPackageFailure =
699
787
  rowFailure?.kind === "missing-packages" ? rowFailure : null;
700
788
  const missingPackages =
@@ -704,13 +792,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
704
792
  : []);
705
793
  const geographicUnavailable =
706
794
  isGeographic(option.format) &&
707
- ((!!getExportMetadata && !metadata) ||
708
- metadataLoading ||
709
- !!metadataError ||
710
- eligibility?.available === false ||
711
- missingCrs ||
712
- (!!metadata?.geometry_columns.length &&
713
- geometryColumn === null));
795
+ ((!!getExportMetadata && !metadata) || blocker !== null);
714
796
  const copyUnavailable =
715
797
  isGeographic(option.format) &&
716
798
  (geographicUnavailable || missingPackages.length > 0);
@@ -888,9 +970,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
888
970
  type="button"
889
971
  size="xs"
890
972
  variant="outline"
891
- onClick={() =>
892
- setMetadataRefresh((current) => current + 1)
893
- }
973
+ onClick={metadataRequest.refetch}
894
974
  >
895
975
  Retry
896
976
  </Button>
@@ -906,17 +986,13 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
906
986
  missingPackageFailure?.description ??
907
987
  eligibility?.reason
908
988
  }
909
- onInstall={() => {
910
- void retryAfterInstall(
911
- missingPackageFailure?.action ?? {
912
- destination: "download",
913
- format:
914
- option.format === "geojson"
915
- ? "geojson"
916
- : "parquet",
917
- },
918
- );
919
- }}
989
+ onInstall={createInstallRetry(
990
+ missingPackageFailure?.action ?? {
991
+ destination: "download",
992
+ format:
993
+ option.format === "geojson" ? "geojson" : "parquet",
994
+ },
995
+ )}
920
996
  className="items-start"
921
997
  />
922
998
  </div>
@@ -3,42 +3,6 @@
3
3
  import z from "zod";
4
4
  import { rpc } from "@/plugins/core/rpc";
5
5
 
6
- export type DownloadFormat = "csv" | "json" | "parquet" | "tsv" | "geojson";
7
-
8
- /**
9
- * Per-request export options. Each field is optional. A missing field keeps
10
- * the widget's default for that setting.
11
- */
12
- export interface DownloadAsOptions {
13
- separator?: string;
14
- encoding?: string;
15
- ensure_ascii?: boolean;
16
- }
17
-
18
- export interface DownloadAsRequest {
19
- format: DownloadFormat;
20
- options?: DownloadAsOptions;
21
- geometry_column?: string | null;
22
- }
23
-
24
- export type DownloadAsArgs = (req: DownloadAsRequest) => Promise<{
25
- url: string;
26
- filename: string;
27
- error?: string | null;
28
- missing_packages?: string[] | null;
29
- code?:
30
- | "geometry_required"
31
- | "invalid_geometry"
32
- | "invalid_metadata"
33
- | "unsupported_representation"
34
- | "unsupported_version"
35
- | "missing_packages"
36
- | "missing_crs"
37
- | "conversion_failed"
38
- | null;
39
- column?: string | null;
40
- }>;
41
-
42
6
  const DownloadRequestSchema = z.object({
43
7
  geometry_column: z.string().nullish(),
44
8
  options: z
@@ -88,6 +52,20 @@ export const DownloadGeoJSONSchema = rpc
88
52
  }),
89
53
  );
90
54
 
55
+ export type DownloadAsRequest =
56
+ | z.infer<typeof DownloadAsSchema.input>
57
+ | z.infer<typeof DownloadGeoJSONSchema.input>;
58
+
59
+ type DownloadResponse =
60
+ | z.infer<typeof DownloadAsSchema.output>
61
+ | z.infer<typeof DownloadGeoJSONSchema.output>;
62
+
63
+ export type DownloadFormat = DownloadAsRequest["format"];
64
+ export type DownloadAsOptions = NonNullable<DownloadAsRequest["options"]>;
65
+ export type DownloadAsArgs = (
66
+ req: DownloadAsRequest,
67
+ ) => Promise<DownloadResponse>;
68
+
91
69
  export type DownloadAsFunction = (
92
70
  req: z.infer<typeof DownloadAsSchema.input>,
93
71
  ) => Promise<z.infer<typeof DownloadAsSchema.output>>;
@@ -228,6 +228,24 @@ export function detectSentinel(
228
228
  return null;
229
229
  }
230
230
 
231
+ type NullishSentinelType = Extract<CellValueSentinel["type"], "null" | "nat">;
232
+
233
+ const NULLISH_SENTINEL_TYPES: ReadonlySet<CellValueSentinel["type"]> =
234
+ new Set<NullishSentinelType>(["null", "nat"]);
235
+
236
+ /**
237
+ * True when the cell value represents a missing value (null, NaT)
238
+ * rather than a real value that happens to look special.
239
+ */
240
+ export function isNullishSentinel(
241
+ value: unknown,
242
+ dataType: DataType | undefined,
243
+ dtype?: string,
244
+ ): boolean {
245
+ const sentinel = detectSentinel(value, dataType, dtype);
246
+ return sentinel !== null && NULLISH_SENTINEL_TYPES.has(sentinel.type);
247
+ }
248
+
231
249
  /**
232
250
  * Stringify an unknown value. Converts objects to JSON strings.
233
251
  * @param opts.value - The value to stringify.
@@ -66,7 +66,10 @@ export type OnRefactorWithAI = (opts: {
66
66
  * Renders an output based on an OutputMessage.
67
67
  */
68
68
  export const OutputRenderer: React.FC<{
69
- message: Pick<OutputMessage, "channel" | "data" | "mimetype">;
69
+ message: Pick<
70
+ OutputMessage,
71
+ "channel" | "data" | "mimetype" | "code" | "traceback_format"
72
+ >;
70
73
  cellId?: CellId;
71
74
  onRefactorWithAI?: OnRefactorWithAI;
72
75
  wrapText?: boolean;
@@ -172,7 +175,9 @@ export const OutputRenderer: React.FC<{
172
175
 
173
176
  case "application/vnd.marimo+error":
174
177
  invariant(Array.isArray(data), "Expected array data");
175
- return <MarimoErrorOutput cellId={cellId} errors={data} />;
178
+ return (
179
+ <MarimoErrorOutput cellId={cellId} errors={data} code={message.code} />
180
+ );
176
181
 
177
182
  case "application/vnd.marimo+traceback":
178
183
  invariant(
@@ -183,6 +188,8 @@ export const OutputRenderer: React.FC<{
183
188
  <MarimoTracebackOutput
184
189
  onRefactorWithAI={onRefactorWithAI}
185
190
  traceback={data}
191
+ tracebackFormat={message.traceback_format}
192
+ code={message.code}
186
193
  cellId={cellId}
187
194
  />
188
195
  );