@marimo-team/frontend 0.24.1-dev2 → 0.24.1-dev20

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 (104) hide show
  1. package/dist/assets/{CellStatus-DqJypoFY.js → CellStatus-B_dBNTJO.js} +1 -1
  2. package/dist/assets/{JsonOutput-DnrBQBRT.js → JsonOutput-C5g_bqiZ.js} +1 -1
  3. package/dist/assets/{MarimoErrorOutput-BVdFtnJz.js → MarimoErrorOutput-Cb0evuEp.js} +1 -1
  4. package/dist/assets/{RenderHTML-X9kskQZ0.js → RenderHTML-BoN8Qu7U.js} +1 -1
  5. package/dist/assets/{RunButton-D83R2wvd.js → RunButton-C3V3v58F.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-HD16K3PB.js → add-cell-with-ai-C5JH9LS2.js} +1 -1
  7. package/dist/assets/{add-connection-dialog-V86wrXca.js → add-connection-dialog-DHyonb9T.js} +1 -1
  8. package/dist/assets/{agent-panel-XdLSrKjl.js → agent-panel-D7DLUBam.js} +1 -1
  9. package/dist/assets/{ai-model-dropdown-BJp70TST.js → ai-model-dropdown-DWDm50eM.js} +4 -4
  10. package/dist/assets/app-config-button-DhZke6rt.js +1 -0
  11. package/dist/assets/{blob-BKW9TqBu.js → blob-XD7AvCQ1.js} +1 -1
  12. package/dist/assets/{cell-editor-DQSrfeL3.js → cell-editor-DZg8uKWr.js} +14 -14
  13. package/dist/assets/{cell-link-D26WjdN1.js → cell-link-WDIIwzUn.js} +1 -1
  14. package/dist/assets/{cells-DTFqEQb0.js → cells-BvgwUKB9.js} +1 -1
  15. package/dist/assets/{chat-display-CclXFQQP.js → chat-display-CR9X_gwB.js} +1 -1
  16. package/dist/assets/{chat-panel-ft0VIKS8.js → chat-panel-DnGBFlLk.js} +1 -1
  17. package/dist/assets/{chat-ui-DUYenr6D.js → chat-ui-BVFULpKr.js} +1 -1
  18. package/dist/assets/{column-preview-BLap3eAk.js → column-preview-DMqGohE5.js} +1 -1
  19. package/dist/assets/{command-palette-C7nz0zHt.js → command-palette-fK5U3VCd.js} +1 -1
  20. package/dist/assets/{common-DKQZgzC9.js → common-Dx_4ekWj.js} +1 -1
  21. package/dist/assets/{components-Bf_hykBb.js → components-DVs9Yu7X.js} +1 -1
  22. package/dist/assets/config-DFK5eCC3.js +1 -0
  23. package/dist/assets/{datasource-DZgUyqAK.js → datasource-Le540Ng6.js} +1 -1
  24. package/dist/assets/{dependency-graph-panel-CH6kIoat.js → dependency-graph-panel-BsdBM_AS.js} +1 -1
  25. package/dist/assets/{documentation-panel-BQYA8gCW.js → documentation-panel-RJNMjUK6.js} +1 -1
  26. package/dist/assets/{download-_oAlkvj6.js → download-B8NwKEtr.js} +1 -1
  27. package/dist/assets/{edit-page-DnXTBsfC.js → edit-page-DUnHDaYA.js} +6 -6
  28. package/dist/assets/{error-entries-uxjSsTal.js → error-entries-yYB8PZoo.js} +1 -1
  29. package/dist/assets/{error-panel-Bq0TKquh.js → error-panel-Dr928gBs.js} +1 -1
  30. package/dist/assets/{file-explorer-panel-uhg7VNMd.js → file-explorer-panel-Brf39GIG.js} +1 -1
  31. package/dist/assets/{file-icons-V3puz20c.js → file-icons-D39zbjsS.js} +1 -1
  32. package/dist/assets/{file-name-input-DWf6YZwM.js → file-name-input-BLH5fTEA.js} +1 -1
  33. package/dist/assets/{floating-outline-Vqr5D17R.js → floating-outline-UPrDtFZF.js} +1 -1
  34. package/dist/assets/{focus-BTlyH5K_.js → focus-BArgHYf8.js} +1 -1
  35. package/dist/assets/{form-CRSnWxLR.js → form-BN1AKD58.js} +1 -1
  36. package/dist/assets/{form-Bb1JVuzT.js → form-BvzcNsxB.js} +1 -1
  37. package/dist/assets/{home-page-RduyhdYP.js → home-page-Dwl_Carc.js} +1 -1
  38. package/dist/assets/{html-to-image-BsXiWo0v.js → html-to-image-DKEUbBwc.js} +1 -1
  39. package/dist/assets/index-Dqr3-dR1.js +38 -0
  40. package/dist/assets/{index-WoaGtlKd.css → index-n_AVX7aW.css} +1 -1
  41. package/dist/assets/{kiosk-mode-BH15-Gn-.js → kiosk-mode-B8UhlOLw.js} +1 -1
  42. package/dist/assets/{layout-1X5SpE18.js → layout-YbL5eg7A.js} +3 -3
  43. package/dist/assets/{logs-panel-CVxksxou.js → logs-panel-DItocggZ.js} +1 -1
  44. package/dist/assets/{markdown-renderer-BjuEVUOa.js → markdown-renderer-D7RLn6TA.js} +1 -1
  45. package/dist/assets/{name-cell-input-CtYGGTR5.js → name-cell-input-jausH-CL.js} +1 -1
  46. package/dist/assets/{outline-panel-BhxoKAbS.js → outline-panel-C_pASL6b.js} +1 -1
  47. package/dist/assets/{packages-panel-BWP3YWNW.js → packages-panel-ItQn6qH4.js} +1 -1
  48. package/dist/assets/{pair-with-agent-modal-BCMZbqSR.js → pair-with-agent-modal-DiDLAjxV.js} +1 -1
  49. package/dist/assets/{panel-accordion-state-0CKLFeAf.js → panel-accordion-state-DaMmC7UV.js} +1 -1
  50. package/dist/assets/{panels-D40pa9F1.js → panels-Bhzy-m2X.js} +1 -1
  51. package/dist/assets/{process-output-WaMx3G7b.js → process-output-ClE9lj2U.js} +1 -1
  52. package/dist/assets/{radio-group-5KDs5aO1.js → radio-group-CSwRGU0A.js} +1 -1
  53. package/dist/assets/{readonly-code-display-DnHcjfFl.js → readonly-code-display-kRUXu5aJ.js} +1 -1
  54. package/dist/assets/{readonly-python-code-BV8otm7e.js → readonly-python-code-BCOkadQf.js} +1 -1
  55. package/dist/assets/{reveal-component-yVci-fc9.js → reveal-component-CnIgubBp.js} +1 -1
  56. package/dist/assets/{run-page-aQycuCjn.js → run-page-Du8ylzny.js} +1 -1
  57. package/dist/assets/{scratchpad-panel-lsqN24Au.js → scratchpad-panel-BEn7TrAb.js} +1 -1
  58. package/dist/assets/{session-panel-C4JUkPt7.js → session-panel-CidCOM4_.js} +1 -1
  59. package/dist/assets/{skeleton-DOZFvsSE.js → skeleton-CL2xKYhl.js} +1 -1
  60. package/dist/assets/{snippets-panel-QNY_Dl2J.js → snippets-panel-V4x4wLMQ.js} +1 -1
  61. package/dist/assets/state-C7arWhwL.js +3 -0
  62. package/dist/assets/{state-ByxLiTvP.js → state-m2zUPztG.js} +1 -1
  63. package/dist/assets/{switch-CDT2KH2g.js → switch-ulnbZFaI.js} +1 -1
  64. package/dist/assets/{terminal-UyHT9ie2.js → terminal-Bo6pnmSU.js} +1 -1
  65. package/dist/assets/{tracing-BiBPyXVg.js → tracing-Cmwuiwj9.js} +1 -1
  66. package/dist/assets/{tracing-panel-D8LtcU_g.js → tracing-panel-CCXeYfOu.js} +2 -2
  67. package/dist/assets/{tree-actions-DUAdNn1i.js → tree-actions-QMLf8Qhz.js} +1 -1
  68. package/dist/assets/{useBoolean-CgN_4RAl.js → useBoolean-C_UV6tQ7.js} +1 -1
  69. package/dist/assets/{useCellActionButton-Bp2eEIzO.js → useCellActionButton-D8qyXNKv.js} +1 -1
  70. package/dist/assets/{useDeleteCell-DO-9USfB.js → useDeleteCell-Ctnxl8AT.js} +1 -1
  71. package/dist/assets/{useDependencyPanelTab-Bi8pUBnn.js → useDependencyPanelTab-CYSEZi6j.js} +1 -1
  72. package/dist/assets/useNotebookActions-CblNR75b.js +12 -0
  73. package/dist/assets/{useRunCells-BpZ_0Xuz.js → useRunCells-DjQN3XYP.js} +1 -1
  74. package/dist/assets/{useSplitCell-C2fjcfID.js → useSplitCell-BG8Zd4lw.js} +1 -1
  75. package/dist/assets/{vega-component-3G_aA1Wa.js → vega-component-1VoQOWvU.js} +1 -1
  76. package/dist/index.html +27 -27
  77. package/package.json +1 -1
  78. package/src/__mocks__/requests.ts +4 -0
  79. package/src/components/app-config/optional-features.test.ts +78 -0
  80. package/src/components/app-config/optional-features.tsx +33 -6
  81. package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +206 -34
  82. package/src/components/editor/actions/export-dialog/export-dialog.tsx +9 -3
  83. package/src/components/editor/actions/export-dialog/format-notice.tsx +69 -24
  84. package/src/components/editor/actions/export-dialog/use-export-dialog.ts +27 -2
  85. package/src/components/ui/toaster.tsx +14 -1
  86. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +13 -0
  87. package/src/core/codemirror/code-lens/popover.tsx +11 -5
  88. package/src/core/islands/bridge.ts +1 -0
  89. package/src/core/mode.ts +6 -1
  90. package/src/core/network/__tests__/requests-lazy.test.ts +40 -0
  91. package/src/core/network/__tests__/requests-network.test.ts +13 -0
  92. package/src/core/network/requests-lazy.ts +11 -4
  93. package/src/core/network/requests-network.ts +8 -0
  94. package/src/core/network/requests-static.ts +1 -0
  95. package/src/core/network/requests-toasting.tsx +1 -0
  96. package/src/core/network/types.ts +5 -0
  97. package/src/core/wasm/bridge.ts +1 -0
  98. package/src/plugins/impl/MatrixPlugin.tsx +256 -35
  99. package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +540 -3
  100. package/dist/assets/app-config-button-C2zJXSrK.js +0 -1
  101. package/dist/assets/config-EoJoeZuK.js +0 -1
  102. package/dist/assets/index-C5WESBbZ.js +0 -38
  103. package/dist/assets/state-Ci7n55A9.js +0 -3
  104. package/dist/assets/useNotebookActions-Bm9VzJ_M.js +0 -12
@@ -28,13 +28,17 @@ export const ExportDialog: React.FC<{
28
28
  const {
29
29
  dialogRef,
30
30
  isExporting,
31
+ isInstalling,
32
+ canInstall,
31
33
  formats,
32
34
  options,
33
35
  selected,
34
36
  selectFormat,
35
37
  updateOptions,
38
+ installRequirements,
36
39
  submit,
37
40
  } = useExportDialog({ initialFormat, onClose });
41
+ const isBusy = isExporting || isInstalling;
38
42
  const desktopLayout = useMediaQuery(DESKTOP_LAYOUT_QUERY);
39
43
  const {
40
44
  format,
@@ -86,7 +90,7 @@ export const ExportDialog: React.FC<{
86
90
  <TabsTrigger
87
91
  key={candidate}
88
92
  value={candidate}
89
- disabled={isExporting}
93
+ disabled={isBusy}
90
94
  className="min-w-0 justify-start gap-2 px-2.5 py-2 text-xs data-[state=active]:shadow-xs sm:w-full sm:text-sm"
91
95
  data-testid={`export-format-${candidate}`}
92
96
  >
@@ -119,13 +123,15 @@ export const ExportDialog: React.FC<{
119
123
  format={format}
120
124
  formatLabel={definition.label}
121
125
  status={status}
126
+ onInstall={canInstall ? installRequirements : undefined}
127
+ isInstalling={isInstalling}
122
128
  />
123
129
 
124
130
  {usesBrowserPrint ? null : (
125
131
  <FormatOptions
126
132
  options={options}
127
133
  updateOptions={updateOptions}
128
- disabled={isExporting}
134
+ disabled={isBusy}
129
135
  />
130
136
  )}
131
137
  </TabsContent>
@@ -173,7 +179,7 @@ export const ExportDialog: React.FC<{
173
179
  ) : null}
174
180
  <Button
175
181
  type="button"
176
- disabled={!status.available || isExporting}
182
+ disabled={!status.available || isBusy}
177
183
  aria-busy={isExporting}
178
184
  onClick={submit}
179
185
  className={cn(
@@ -1,9 +1,10 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { AlertCircleIcon } from "lucide-react";
3
+ import { AlertCircleIcon, DownloadCloudIcon } from "lucide-react";
4
4
  import type { PropsWithChildren } from "react";
5
5
  import { Spinner } from "@/components/icons/spinner";
6
6
  import { Alert, AlertDescription } from "@/components/ui/alert";
7
+ import { Button } from "@/components/ui/button";
7
8
  import { assertNever } from "@/utils/assertNever";
8
9
  import { cn } from "@/utils/cn";
9
10
  import type {
@@ -56,10 +57,14 @@ export function FormatNotice({
56
57
  format,
57
58
  formatLabel,
58
59
  status,
60
+ onInstall,
61
+ isInstalling = false,
59
62
  }: {
60
63
  format: ExportFormat;
61
64
  formatLabel: string;
62
65
  status: ExportFormatStatus;
66
+ onInstall?: () => void;
67
+ isInstalling?: boolean;
63
68
  }) {
64
69
  if (status.availabilityCheckFailed) {
65
70
  return (
@@ -80,20 +85,47 @@ export function FormatNotice({
80
85
  format={format}
81
86
  formatLabel={formatLabel}
82
87
  reason={status.reason}
88
+ onInstall={onInstall}
89
+ isInstalling={isInstalling}
83
90
  />
84
91
  </div>
85
92
  );
86
93
  }
87
94
 
88
- function Notice({ children }: PropsWithChildren) {
95
+ function Notice({
96
+ children,
97
+ onInstall,
98
+ isInstalling = false,
99
+ }: PropsWithChildren<{
100
+ onInstall?: () => void;
101
+ isInstalling?: boolean;
102
+ }>) {
89
103
  return (
90
104
  <Alert
91
105
  variant="warning"
92
- className="flex items-start gap-2.5 p-3 text-(--yellow-12) has-[svg]:pl-3 sm:items-center [&>svg]:static [&>svg+div]:translate-y-0"
106
+ className="flex items-center gap-2.5 px-3 py-2 text-(--yellow-12) has-[svg]:pl-3 [&>svg]:static [&>svg+div]:translate-y-0"
93
107
  >
94
- <AlertCircleIcon className="mt-0.5 size-4 shrink-0 sm:mt-0" />
95
- <AlertDescription className="min-w-0 flex-1 text-sm leading-5">
96
- {children}
108
+ <AlertCircleIcon className="size-4 shrink-0" />
109
+ <AlertDescription className="flex min-w-0 flex-1 items-center gap-3 text-sm leading-5">
110
+ <span className="min-w-0 flex-1">{children}</span>
111
+ {onInstall ? (
112
+ <Button
113
+ type="button"
114
+ variant="outline"
115
+ size="xs"
116
+ className="shrink-0 border-input bg-background text-foreground hover:border-input hover:bg-muted hover:text-foreground"
117
+ disabled={isInstalling}
118
+ aria-busy={isInstalling}
119
+ onClick={onInstall}
120
+ >
121
+ {isInstalling ? (
122
+ <Spinner size="small" className="mr-1.5 size-3.5" />
123
+ ) : (
124
+ <DownloadCloudIcon className="mr-1.5 size-3.5" />
125
+ )}
126
+ Install
127
+ </Button>
128
+ ) : null}
97
129
  </AlertDescription>
98
130
  </Alert>
99
131
  );
@@ -103,10 +135,14 @@ function ReasonNotice({
103
135
  format,
104
136
  formatLabel,
105
137
  reason,
138
+ onInstall,
139
+ isInstalling,
106
140
  }: {
107
141
  format: ExportFormat;
108
142
  formatLabel: string;
109
143
  reason: ExportBlockReason;
144
+ onInstall?: () => void;
145
+ isInstalling: boolean;
110
146
  }) {
111
147
  switch (reason.type) {
112
148
  case "checking-requirements":
@@ -126,30 +162,39 @@ function ReasonNotice({
126
162
  return <Notice>Name and save this notebook before exporting.</Notice>;
127
163
  case "missing-packages":
128
164
  return (
129
- <Notice>
130
- Install{" "}
165
+ <Notice onInstall={onInstall} isInstalling={isInstalling}>
166
+ {formatLabel} export requires{" "}
131
167
  <code className="break-words font-mono">
132
168
  {reason.packages.join(", ")}
133
- </code>{" "}
134
- where marimo is running to use this export.
169
+ </code>
170
+ .
135
171
  </Notice>
136
172
  );
137
173
  case "missing-setup":
138
174
  return (
139
- <Notice>
140
- <span className="block min-w-0">
141
- {reason.requirements.map((requirement) => {
142
- const details = SETUP_REQUIREMENTS[requirement.name];
143
- return (
144
- <span className="block" key={requirement.name}>
145
- {details.description}
146
- <code className="mt-1 block break-all font-mono">
147
- {requirement.command}
148
- </code>
149
- </span>
150
- );
151
- })}
152
- </span>
175
+ <Notice onInstall={onInstall} isInstalling={isInstalling}>
176
+ {onInstall ? (
177
+ reason.requirements
178
+ .map(
179
+ (requirement) =>
180
+ SETUP_REQUIREMENTS[requirement.name].description,
181
+ )
182
+ .join(" ")
183
+ ) : (
184
+ <span className="block min-w-0">
185
+ {reason.requirements.map((requirement) => {
186
+ const details = SETUP_REQUIREMENTS[requirement.name];
187
+ return (
188
+ <span className="block" key={requirement.name}>
189
+ {details.description}
190
+ <code className="mt-1 block break-all font-mono">
191
+ {requirement.command}
192
+ </code>
193
+ </span>
194
+ );
195
+ })}
196
+ </span>
197
+ )}
153
198
  </Notice>
154
199
  );
155
200
  case "wasm-runtime":
@@ -7,7 +7,7 @@ import {
7
7
  updateCellOutputsWithScreenshots,
8
8
  useEnrichCellOutputs,
9
9
  } from "@/core/export/hooks";
10
- import { runDuringPresentMode } from "@/core/mode";
10
+ import { runDuringPresentMode, useInstallAllowed } from "@/core/mode";
11
11
  import { useRequestClient } from "@/core/network/requests";
12
12
  import type { ExportAvailabilityResponse } from "@/core/network/types";
13
13
  import { useFilename } from "@/core/saving/filename";
@@ -116,6 +116,8 @@ function useExportDialogState(initialFormat?: ExportFormat) {
116
116
  );
117
117
  const runtime = isWasm() ? "wasm" : "server";
118
118
  const requests = useRequestClient();
119
+ const installAllowed = useInstallAllowed();
120
+ const [isInstalling, setIsInstalling] = useState(false);
119
121
 
120
122
  const availabilityRequest =
121
123
  useAsyncData<ExportAvailabilityResponse | null>(async () => {
@@ -135,6 +137,10 @@ function useExportDialogState(initialFormat?: ExportFormat) {
135
137
  availability,
136
138
  });
137
139
  const status = statusFor(format);
140
+ const canInstall =
141
+ installAllowed &&
142
+ (status.reason?.type === "missing-packages" ||
143
+ status.reason?.type === "missing-setup");
138
144
  const formats = EXPORT_FORMATS.map((candidate) => ({
139
145
  format: candidate,
140
146
  status: statusFor(candidate),
@@ -184,6 +190,22 @@ function useExportDialogState(initialFormat?: ExportFormat) {
184
190
  });
185
191
  };
186
192
 
193
+ const installRequirements = async () => {
194
+ if (!canInstall || isInstalling || format === "png") {
195
+ return;
196
+ }
197
+
198
+ setIsInstalling(true);
199
+ try {
200
+ const availability = await requests.installExportRequirements({ format });
201
+ availabilityRequest.setData(availability);
202
+ } catch {
203
+ // Request failures are surfaced by the shared toast layer.
204
+ } finally {
205
+ setIsInstalling(false);
206
+ }
207
+ };
208
+
187
209
  return {
188
210
  formats,
189
211
  options,
@@ -205,6 +227,9 @@ function useExportDialogState(initialFormat?: ExportFormat) {
205
227
  },
206
228
  selectFormat,
207
229
  updateOptions,
230
+ canInstall,
231
+ isInstalling,
232
+ installRequirements,
208
233
  };
209
234
  }
210
235
 
@@ -364,7 +389,7 @@ export function useExportDialog({
364
389
  ...state,
365
390
  ...action,
366
391
  selectFormat: (value: string) => {
367
- if (!action.isExporting) {
392
+ if (!action.isExporting && !state.isInstalling) {
368
393
  selectFormat(value);
369
394
  }
370
395
  },
@@ -1,9 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
3
3
  import { useRef } from "react";
4
+ import { createPortal } from "react-dom";
4
5
  import { CopyClipboardIcon } from "@/components/icons/copy-icon";
5
6
  import { useToast } from "@/components/ui/use-toast";
7
+ import { StyleNamespace } from "@/theme/namespace";
6
8
  import { cn } from "@/utils/cn";
9
+ import { withFullScreenAsRoot } from "./fullscreen";
7
10
  import {
8
11
  Toast,
9
12
  ToastClose,
@@ -28,11 +31,21 @@ export const Toaster = () => {
28
31
  {...props}
29
32
  />
30
33
  ))}
31
- <ToastViewport />
34
+ <ToastViewportPortal />
32
35
  </ToastProvider>
33
36
  );
34
37
  };
35
38
 
39
+ const ToastViewportPortal = withFullScreenAsRoot(
40
+ ({ container }: { container?: Element | DocumentFragment | null }) =>
41
+ createPortal(
42
+ <StyleNamespace>
43
+ <ToastViewport />
44
+ </StyleNamespace>,
45
+ container ?? document.body,
46
+ ),
47
+ );
48
+
36
49
  type ToastItemProps = Omit<
37
50
  ComponentPropsWithoutRef<typeof Toast>,
38
51
  "title" | "children"
@@ -56,6 +56,19 @@ describe("CachePopover getCacheInfo throttling", () => {
56
56
  return () => act(() => dispose());
57
57
  }
58
58
 
59
+ it("renders content synchronously for CodeMirror's initial measurement", async () => {
60
+ const dom = document.createElement("div");
61
+
62
+ // Keep the mount outside `act()`. It would flush an asynchronous render
63
+ // and make this assertion pass without `flushSync`.
64
+ const dispose = mountLensPopover(dom, CACHE_SPEC);
65
+
66
+ expect(dom.textContent).toContain("my_cache");
67
+ await act(async () => {
68
+ dispose();
69
+ });
70
+ });
71
+
59
72
  it("does not refetch on every hover, only after the throttle interval elapses", () => {
60
73
  const disposeFirst = mount();
61
74
  expect(getCacheInfoMock).toHaveBeenCalledTimes(1);
@@ -4,6 +4,7 @@ import { Provider, useAtomValue } from "jotai";
4
4
  import { DatabaseZapIcon } from "lucide-react";
5
5
  import type React from "react";
6
6
  import { useEffect } from "react";
7
+ import { flushSync } from "react-dom";
7
8
  import { createRoot } from "react-dom/client";
8
9
  import { useLocale } from "react-aria";
9
10
  import { ProtocolIcon } from "@/components/storage/components";
@@ -39,11 +40,16 @@ export function mountLensPopover(
39
40
  spec: CodeLensSpec,
40
41
  ): () => void {
41
42
  const root = createRoot(dom);
42
- root.render(
43
- <Provider store={store}>
44
- <LensPopover spec={spec} />
45
- </Provider>,
46
- );
43
+ // CodeMirror measures and positions the tooltip as soon as this function
44
+ // returns. Render now so it measures the popover content, not an empty
45
+ // container that later grows over the hovered icon.
46
+ flushSync(() => {
47
+ root.render(
48
+ <Provider store={store}>
49
+ <LensPopover spec={spec} />
50
+ </Provider>,
51
+ );
52
+ });
47
53
  // Defer so tearing the tooltip down from inside a React event (e.g. the
48
54
  // icon's click handler) doesn't unmount mid-render
49
55
  return () => queueMicrotask(() => root.unmount());
@@ -356,6 +356,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
356
356
  sendFileDetails = throwNotImplemented;
357
357
  openTutorial = throwNotImplemented;
358
358
  getExportAvailability = throwNotImplemented;
359
+ installExportRequirements = throwNotImplemented;
359
360
  exportAsHTML = throwNotImplemented;
360
361
  exportAsIPYNB = throwNotImplemented;
361
362
  exportAsMarkdown = throwNotImplemented;
package/src/core/mode.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { atom, useAtomValue } from "jotai";
4
+ import { KnownQueryParams } from "@/core/constants";
4
5
  import { isIslands } from "@/core/islands/utils";
5
6
  import { assertExists } from "@/utils/assertExists";
6
7
  import { invariant } from "@/utils/invariant";
@@ -98,5 +99,9 @@ export const kioskModeAtom = atom<boolean>(false);
98
99
  */
99
100
  export function useInstallAllowed(): boolean {
100
101
  const { mode } = useAtomValue(viewStateAtom);
101
- return mode !== "read";
102
+ const kioskMode = useAtomValue(kioskModeAtom);
103
+ const kioskRequested =
104
+ new URLSearchParams(window.location.search).get(KnownQueryParams.kiosk) ===
105
+ "true";
106
+ return mode !== "read" && !kioskMode && !kioskRequested;
102
107
  }
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { cellId, requestId, uiElementId } from "@/__tests__/branded";
5
+ import { waitForKernelToBeInstantiated } from "../../kernel/state";
5
6
  import type { RuntimeManager } from "../../runtime/runtime";
7
+ import { waitForConnectionOpen } from "../connection";
6
8
  import { createLazyRequests } from "../requests-lazy";
7
9
  import type {
8
10
  EditRequests,
@@ -55,6 +57,10 @@ describe("createLazyRequests", () => {
55
57
  sendInterrupt: vi.fn().mockResolvedValue({ interrupted: true }),
56
58
  sendPdb: vi.fn().mockResolvedValue({ pdb: true }),
57
59
  sendRunScratchpad: vi.fn().mockResolvedValue({ run: true }),
60
+ installExportRequirements: vi.fn().mockResolvedValue({
61
+ source: "server",
62
+ formats: [],
63
+ }),
58
64
  } as unknown as EditRequests & RunRequests;
59
65
  });
60
66
 
@@ -104,6 +110,40 @@ describe("createLazyRequests", () => {
104
110
  expect(mockDelegate.getExportAvailability).toHaveBeenCalledOnce();
105
111
  });
106
112
 
113
+ it("installs export requirements without waiting for the kernel", async () => {
114
+ const lazyRequests = createLazyRequests(
115
+ mockDelegate,
116
+ mockGetRuntimeManager,
117
+ );
118
+
119
+ await lazyRequests.installExportRequirements({ format: "pdf" });
120
+
121
+ expect(mockInit).toHaveBeenCalledOnce();
122
+ expect(waitForConnectionOpen).toHaveBeenCalledOnce();
123
+ expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
124
+ expect(mockDelegate.installExportRequirements).toHaveBeenCalledWith({
125
+ format: "pdf",
126
+ });
127
+ });
128
+
129
+ it("sends instantiate without waiting for the kernel", async () => {
130
+ const lazyRequests = createLazyRequests(
131
+ mockDelegate,
132
+ mockGetRuntimeManager,
133
+ );
134
+ const request = {
135
+ objectIds: [uiElementId("obj1")],
136
+ values: [],
137
+ };
138
+
139
+ await lazyRequests.sendInstantiate(request);
140
+
141
+ expect(mockInit).toHaveBeenCalledOnce();
142
+ expect(waitForConnectionOpen).toHaveBeenCalledOnce();
143
+ expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
144
+ expect(mockDelegate.sendInstantiate).toHaveBeenCalledWith(request);
145
+ });
146
+
107
147
  it("should call init once before first request", async () => {
108
148
  const lazyRequests = createLazyRequests(
109
149
  mockDelegate,
@@ -143,6 +143,19 @@ describe("createNetworkRequests", () => {
143
143
  expect(mockClient.GET).toHaveBeenCalledWith("/api/export/availability");
144
144
  });
145
145
 
146
+ it("installExportRequirements should POST the export format", async () => {
147
+ const requests = createNetworkRequests();
148
+ await requests.installExportRequirements({ format: "pdf" });
149
+
150
+ expect(mockClient.POST).toHaveBeenCalledWith(
151
+ "/api/export/requirements/install",
152
+ expect.objectContaining({
153
+ body: { format: "pdf" },
154
+ params: expect.anything(),
155
+ }),
156
+ );
157
+ });
158
+
146
159
  it("discoverDataSources should POST to the discovery endpoint", async () => {
147
160
  const requests = createNetworkRequests();
148
161
  const request = { requestId: "discovery-request" } as any;
@@ -26,6 +26,10 @@ type AllRequests = EditRequests & RunRequests;
26
26
  // executing. Use for user-initiated actions that should "just work" and
27
27
  // kick off the kernel if needed (e.g., clicking Run).
28
28
  //
29
+ // - startConnectionWithoutKernel: Initializes the runtime and waits for an open
30
+ // connection. Use for session-scoped server requests that do not need an
31
+ // instantiated kernel.
32
+ //
29
33
  // - waitForConnectionOpen: Waits for an existing connection but won't start one.
30
34
  // Use for operations that depend on a running kernel but shouldn't be the
31
35
  // trigger to start it (e.g., saving, interrupting).
@@ -38,6 +42,7 @@ type Action =
38
42
  | "throwError"
39
43
  | "dropRequest"
40
44
  | "startConnection"
45
+ | "startConnectionWithoutKernel"
41
46
  | "waitForConnectionOpen"
42
47
  | "serverOnly";
43
48
 
@@ -45,7 +50,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
45
50
  // These will start a connection if not already connected and then wait until the connection is open
46
51
  sendComponentValues: "startConnection",
47
52
  sendModelValue: "startConnection",
48
- sendInstantiate: "startConnection",
53
+ sendInstantiate: "startConnectionWithoutKernel",
49
54
  sendRun: "startConnection",
50
55
  sendDeleteCell: "startConnection",
51
56
  sendRunScratchpad: "startConnection",
@@ -107,6 +112,9 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
107
112
  getEnvironmentInfo: "serverOnly",
108
113
  getExportAvailability: "serverOnly",
109
114
 
115
+ // Session-scoped server operations
116
+ installExportRequirements: "startConnectionWithoutKernel",
117
+
110
118
  // Home operations throw errors
111
119
  getRecentFiles: "startConnection",
112
120
  getWorkspaceFiles: "startConnection",
@@ -186,13 +194,12 @@ export function createLazyRequests(
186
194
  await waitForKernelToBeInstantiated();
187
195
  return request(...args);
188
196
 
197
+ case "startConnectionWithoutKernel":
189
198
  case "startConnection":
190
199
  // Start connection and wait for it to be open
191
200
  await initOnce(runtimeManager);
192
201
  await waitForConnectionOpen();
193
- if (key !== "sendInstantiate") {
194
- // We don't need to wait for kernel to be instantiated if we are sending an instantiate request
195
- // otherwise we will wait forever
202
+ if (action === "startConnection") {
196
203
  await waitForKernelToBeInstantiated();
197
204
  }
198
205
  return request(...args);
@@ -417,6 +417,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
417
417
  getExportAvailability: () => {
418
418
  return getClient().GET("/api/export/availability").then(handleResponse);
419
419
  },
420
+ installExportRequirements: (request) => {
421
+ return getClient()
422
+ .POST("/api/export/requirements/install", {
423
+ body: request,
424
+ params: getParams(),
425
+ })
426
+ .then(handleResponse);
427
+ },
420
428
  exportAsHTML: async (request) => {
421
429
  if (
422
430
  process.env.NODE_ENV === "development" ||
@@ -84,6 +84,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
84
84
  getRunningNotebooks: throwNotInEditMode,
85
85
  shutdownSession: throwNotInEditMode,
86
86
  getExportAvailability: throwNotInEditMode,
87
+ installExportRequirements: throwNotInEditMode,
87
88
  exportAsHTML: throwNotInEditMode,
88
89
  exportAsIPYNB: throwNotInEditMode,
89
90
  exportAsMarkdown: throwNotInEditMode,
@@ -65,6 +65,7 @@ export function createErrorToastingRequests(
65
65
  getRunningNotebooks: "Failed to get running notebooks",
66
66
  shutdownSession: "Failed to shutdown session",
67
67
  getExportAvailability: "", // No toast
68
+ installExportRequirements: "Failed to install export requirements",
68
69
  exportAsHTML: "Failed to export HTML",
69
70
  exportAsIPYNB: "Failed to export ipynb",
70
71
  exportAsMarkdown: "Failed to export Markdown",
@@ -29,6 +29,8 @@ export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
29
29
  export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
30
30
  export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
31
31
  export type ExportAvailabilityResponse = schemas["ExportAvailabilityResponse"];
32
+ export type InstallExportRequirementsRequest =
33
+ schemas["InstallExportRequirementsRequest"];
32
34
  export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
33
35
 
34
36
  export interface ExportedFile<T extends BlobPart = BlobPart> {
@@ -223,6 +225,9 @@ export interface EditRequests {
223
225
  ) => Promise<RunningNotebooksResponse>;
224
226
  // Export requests
225
227
  getExportAvailability: () => Promise<ExportAvailabilityResponse>;
228
+ installExportRequirements: (
229
+ request: InstallExportRequirementsRequest,
230
+ ) => Promise<ExportAvailabilityResponse>;
226
231
  exportAsHTML: (request: ExportAsHTMLRequest) => Promise<ExportedFile<string>>;
227
232
  exportAsIPYNB: (
228
233
  request: ExportAsIPYNBRequest,
@@ -675,6 +675,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
675
675
  getRunningNotebooks = throwNotImplemented;
676
676
  shutdownSession = throwNotImplemented;
677
677
  getExportAvailability = throwNotImplemented;
678
+ installExportRequirements = throwNotImplemented;
678
679
  exportAsIPYNB = throwNotImplemented;
679
680
  exportAsPDF = throwNotImplemented;
680
681
  autoExportAsHTML = throwNotImplemented;