@marimo-team/frontend 0.24.3-dev2 → 0.24.3-dev4

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 (94) hide show
  1. package/dist/assets/{CellStatus-LoTwk7fR.js → CellStatus-0QNZS4By.js} +1 -1
  2. package/dist/assets/{JsonOutput-CzIGPEZQ.js → JsonOutput-D2ZWLZs4.js} +7 -7
  3. package/dist/assets/{MarimoErrorOutput-DqpoYOvw.js → MarimoErrorOutput-BlmIJ9WY.js} +2 -2
  4. package/dist/assets/{RenderHTML-DwhMAd9l.js → RenderHTML-DnPB4qpJ.js} +1 -1
  5. package/dist/assets/{RunButton-BlpbweyH.js → RunButton-C7TxE07w.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-R-3DS79K.js → add-cell-with-ai-OQ4sSXg4.js} +12 -12
  7. package/dist/assets/{add-connection-dialog-BGfpw5lC.js → add-connection-dialog-MTOC2ycO.js} +73 -73
  8. package/dist/assets/{agent-panel-Cgt_K0tD.js → agent-panel-BdEDIHdc.js} +4 -4
  9. package/dist/assets/ai-model-dropdown-CmJEEhSC.js +3 -0
  10. package/dist/assets/app-config-button-W8Pytgb5.js +1 -0
  11. package/dist/assets/{blob-B2d_dHwI.js → blob-BrVH7IJp.js} +1 -1
  12. package/dist/assets/{cell-editor-Dw09J9yD.js → cell-editor-C7IiingQ.js} +8 -8
  13. package/dist/assets/{cell-link-6burT2XF.js → cell-link-CbHXmrt8.js} +1 -1
  14. package/dist/assets/{cells-Cp-G34Rz.js → cells-BBhW3FXj.js} +1 -1
  15. package/dist/assets/{chat-display-Bu405iyc.js → chat-display-CIpYPLlF.js} +1 -1
  16. package/dist/assets/{chat-panel-BlUwNY5j.js → chat-panel-CK3X_KPy.js} +2 -2
  17. package/dist/assets/{chat-ui-DGiVDzNh.js → chat-ui-BJwtx1SR.js} +4 -4
  18. package/dist/assets/{column-preview-riSXbYe_.js → column-preview-Coa5PyHk.js} +1 -1
  19. package/dist/assets/{command-palette-CCVzJcyf.js → command-palette-_xpwMniA.js} +1 -1
  20. package/dist/assets/{common-CHR7w7aQ.js → common-KqtqJsml.js} +1 -1
  21. package/dist/assets/{components-C7mWrmXr.js → components-Cpv2z-yV.js} +1 -1
  22. package/dist/assets/{datasource-DrODSIs0.js → datasource-B3qF2oaW.js} +2 -2
  23. package/dist/assets/{dependency-graph-panel-AY5BEVH2.js → dependency-graph-panel-Av1CMZ61.js} +3 -3
  24. package/dist/assets/{documentation-panel-CaJF7e-j.js → documentation-panel-BEz3Hjfu.js} +1 -1
  25. package/dist/assets/{download-DMxOzI7b.js → download-BgO9EN-W.js} +1 -1
  26. package/dist/assets/edit-page-DslcxB67.js +9 -0
  27. package/dist/assets/error-entries-BFzSRy80.js +5 -0
  28. package/dist/assets/{error-panel-Bhlvt6_H.js → error-panel-BtLTJDUq.js} +1 -1
  29. package/dist/assets/{file-explorer-panel-s0ohSG_L.js → file-explorer-panel-DEyChgqS.js} +1 -1
  30. package/dist/assets/{file-icons-DdGYuADJ.js → file-icons-BhotQqrU.js} +1 -1
  31. package/dist/assets/{file-name-input-K4MKESI_.js → file-name-input-CH-hNP6F.js} +1 -1
  32. package/dist/assets/{floating-outline-DaUHnl_e.js → floating-outline-Cmk3XDJi.js} +1 -1
  33. package/dist/assets/{home-page-BLd6rnbk.js → home-page-CUBuu5Go.js} +1 -1
  34. package/dist/assets/index-C5h1aWhw.js +29 -0
  35. package/dist/assets/{layout-DWnrxIo2.js → layout-CsLuqwD3.js} +5 -5
  36. package/dist/assets/{logs-panel-BGS2v3ba.js → logs-panel-BV4VZ_uH.js} +1 -1
  37. package/dist/assets/{markdown-renderer-BZKlbm2Q.js → markdown-renderer-751VNlrQ.js} +1 -1
  38. package/dist/assets/{name-cell-input-BMCdK-el.js → name-cell-input-DexJ7ZIH.js} +1 -1
  39. package/dist/assets/{outline-panel-Dr5u73V6.js → outline-panel-QU3nj1VW.js} +1 -1
  40. package/dist/assets/packages-panel-D7oDuI6v.js +1 -0
  41. package/dist/assets/panel-accordion-state-Dl0VcB79.js +1 -0
  42. package/dist/assets/panels-nM4QKfEI.js +4 -0
  43. package/dist/assets/process-output-CPUOlFhp.js +1 -0
  44. package/dist/assets/readonly-code-display-DP7DxRA1.js +1 -0
  45. package/dist/assets/{readonly-python-code-DpIyjHqY.js → readonly-python-code-CAYyabaV.js} +1 -1
  46. package/dist/assets/{reveal-component-nZIAapoD.js → reveal-component-D9DygPds.js} +2 -2
  47. package/dist/assets/rotate-cw-CESctpLh.js +1 -0
  48. package/dist/assets/{run-page-9pl8qhQR.js → run-page-CzO-uMUn.js} +1 -1
  49. package/dist/assets/{scratchpad-panel-BleGt2KM.js → scratchpad-panel-7YBL6E95.js} +1 -1
  50. package/dist/assets/session-panel-hnFaUP0Y.js +1 -0
  51. package/dist/assets/share-BZW3tzEX.js +1 -0
  52. package/dist/assets/{skeleton-BKeh8hf3.js → skeleton-CM7HxSva.js} +1 -1
  53. package/dist/assets/{snippets-panel-D5yy7kj0.js → snippets-panel-AdjuR23O.js} +1 -1
  54. package/dist/assets/state-DOrY1Jyf.js +1 -0
  55. package/dist/assets/state-DQHafeTV.js +3 -0
  56. package/dist/assets/{tracing-DNk3gmWv.js → tracing-CUlvXr_H.js} +1 -1
  57. package/dist/assets/{tracing-panel-Yqazr6w_.js → tracing-panel-CjyayE-l.js} +2 -2
  58. package/dist/assets/{tree-actions-CG5g7aUQ.js → tree-actions-mfuAcK1x.js} +1 -1
  59. package/dist/assets/{useAddCell-DdB_uBif.js → useAddCell-BR8kYnb6.js} +1 -1
  60. package/dist/assets/{useCellActionButton-B31neI3m.js → useCellActionButton-BhREXvvn.js} +1 -1
  61. package/dist/assets/{useDeleteCell-Bz49_cFj.js → useDeleteCell-DwaN6KAw.js} +1 -1
  62. package/dist/assets/useDependencyPanelTab-I-7B78cW.js +1 -0
  63. package/dist/assets/useInstallPackage-IUt497GC.js +1 -0
  64. package/dist/assets/useNotebookActions-D5_YLR42.js +10 -0
  65. package/dist/assets/useRunCells-BMQuitYY.js +1 -0
  66. package/dist/assets/{useSplitCell-Dy2_DmJR.js → useSplitCell-B7AvHoya.js} +1 -1
  67. package/dist/index.html +22 -22
  68. package/package.json +1 -1
  69. package/src/__mocks__/requests.ts +7 -2
  70. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +143 -0
  71. package/src/components/editor/chrome/panels/packages-panel.tsx +145 -51
  72. package/src/components/editor/package-alert.tsx +25 -1
  73. package/src/core/packages/__tests__/useInstallPackage.test.tsx +47 -1
  74. package/src/core/packages/toast-components.tsx +26 -0
  75. package/src/core/packages/useInstallPackage.ts +7 -2
  76. package/src/core/wasm/bridge.ts +1 -0
  77. package/dist/assets/ai-model-dropdown-DCmiuk7x.js +0 -3
  78. package/dist/assets/app-config-button-DUJC6b6x.js +0 -1
  79. package/dist/assets/edit-page-xlMlcLnI.js +0 -9
  80. package/dist/assets/error-entries-Cv2m1yhc.js +0 -5
  81. package/dist/assets/index-4n22QjCD.js +0 -29
  82. package/dist/assets/packages-panel-BdpRg9tX.js +0 -1
  83. package/dist/assets/panel-accordion-state-DjUTN7i5.js +0 -1
  84. package/dist/assets/panels-CWn0Yh1D.js +0 -4
  85. package/dist/assets/process-output-Ck7-P_xp.js +0 -1
  86. package/dist/assets/readonly-code-display--S2daDa6.js +0 -1
  87. package/dist/assets/session-panel-DRGPlL5J.js +0 -1
  88. package/dist/assets/share-CPtBiNgr.js +0 -1
  89. package/dist/assets/state-B6AklCzD.js +0 -1
  90. package/dist/assets/state-Di9TbuV9.js +0 -3
  91. package/dist/assets/useDependencyPanelTab-C_ps0r--.js +0 -1
  92. package/dist/assets/useInstallPackage-DsD08SLK.js +0 -1
  93. package/dist/assets/useNotebookActions-CQtbk9CB.js +0 -10
  94. package/dist/assets/useRunCells-CX7oW5zy.js +0 -1
@@ -22,9 +22,13 @@ import { Tooltip } from "@/components/ui/tooltip";
22
22
  import { toast } from "@/components/ui/use-toast";
23
23
  import { useResolvedMarimoConfig } from "@/core/config/config";
24
24
  import { useRequestClient } from "@/core/network/requests";
25
- import type { DependencyTreeNode } from "@/core/network/types";
25
+ import type {
26
+ DependencyTreeNode,
27
+ DependencyTreeResponse,
28
+ } from "@/core/network/types";
26
29
  import { stripPackageManagerPrefix } from "@/core/packages/package-input-utils";
27
30
  import {
31
+ showPackageRestartToast,
28
32
  showRemovePackageToast,
29
33
  showUpgradePackageToast,
30
34
  } from "@/core/packages/toast-components";
@@ -39,6 +43,7 @@ import { PanelEmptyState } from "./empty-state";
39
43
  import { PACKAGES_INPUT_ID, packagesToInstallAtom } from "./packages-utils";
40
44
 
41
45
  type ViewMode = "tree" | "list";
46
+ type PackageInstallationContext = DependencyTreeResponse["context"];
42
47
 
43
48
  const PackageActionButton: React.FC<{
44
49
  onClick: () => void;
@@ -77,12 +82,22 @@ const PackagesPanel: React.FC = () => {
77
82
  refetch,
78
83
  isPending,
79
84
  } = useAsyncData(async () => {
80
- const [listPackagesResponse, dependencyTreeResponse] = await Promise.all([
81
- getPackageList(),
82
- getDependencyTree(),
83
- ]);
85
+ // A sandbox's list and tree both inspect the same environment. Wait for
86
+ // the context before issuing the list request so sandboxes do that work
87
+ // only once; non-sandbox managers still need both views.
88
+ const dependencyTreeResponse = await getDependencyTree();
89
+ if (dependencyTreeResponse.context.kind === "sandbox") {
90
+ return {
91
+ list: [],
92
+ context: dependencyTreeResponse.context,
93
+ tree: dependencyTreeResponse.tree,
94
+ };
95
+ }
96
+
97
+ const listPackagesResponse = await getPackageList();
84
98
  return {
85
99
  list: listPackagesResponse.packages,
100
+ context: dependencyTreeResponse.context,
86
101
  tree: dependencyTreeResponse.tree,
87
102
  };
88
103
  }, [packageManager]);
@@ -97,48 +112,66 @@ const PackagesPanel: React.FC = () => {
97
112
  }
98
113
 
99
114
  const isTreeSupported = dependencies.tree != null;
100
- const viewMode = resolveViewMode(userViewMode, isTreeSupported);
101
115
  const name = dependencies.tree?.name;
102
116
  const version = dependencies?.tree?.version;
103
- const isSandbox = name === "<root>"; // name is the project name otherwise
117
+ const sandboxBackend =
118
+ dependencies.context.kind === "sandbox"
119
+ ? dependencies.context.backend
120
+ : null;
121
+ const isSandbox = sandboxBackend !== null;
122
+ const viewMode = isSandbox
123
+ ? "tree"
124
+ : resolveViewMode(userViewMode, isTreeSupported);
125
+ const scopeLabel = sandboxBackend
126
+ ? `${sandboxBackend} sandbox`
127
+ : name && name !== "<root>"
128
+ ? "project"
129
+ : "environment";
130
+ const scopeTitle = sandboxBackend
131
+ ? `Dependencies are managed by the ${sandboxBackend} sandbox selected when marimo started.`
132
+ : scopeLabel;
104
133
 
105
134
  return (
106
135
  <div className="flex-1 flex flex-col overflow-hidden">
107
- <InstallPackageForm packageManager={packageManager} onSuccess={refetch} />
108
- {isTreeSupported && (
136
+ <InstallPackageForm context={dependencies.context} onSuccess={refetch} />
137
+ {(isTreeSupported || isSandbox) && (
109
138
  <div className="flex items-center justify-between px-2 py-1 border-b">
110
- <div className="flex gap-1">
111
- <button
112
- type="button"
113
- className={cn(
114
- "px-2 py-1 text-xs rounded",
115
- viewMode === "list"
116
- ? "bg-accent text-accent-foreground"
117
- : "text-muted-foreground hover:text-foreground",
118
- )}
119
- onClick={() => setUserViewMode("list")}
120
- >
121
- List
122
- </button>
123
- <button
124
- type="button"
125
- className={cn(
126
- "px-2 py-1 text-xs rounded",
127
- viewMode === "tree"
128
- ? "bg-accent text-accent-foreground"
129
- : "text-muted-foreground hover:text-foreground",
130
- )}
131
- onClick={() => setUserViewMode("tree")}
132
- >
133
- Tree
134
- </button>
135
- </div>
139
+ {isTreeSupported && !isSandbox ? (
140
+ <div className="flex gap-1">
141
+ <button
142
+ type="button"
143
+ className={cn(
144
+ "px-2 py-1 text-xs rounded",
145
+ viewMode === "list"
146
+ ? "bg-accent text-accent-foreground"
147
+ : "text-muted-foreground hover:text-foreground",
148
+ )}
149
+ onClick={() => setUserViewMode("list")}
150
+ >
151
+ List
152
+ </button>
153
+ <button
154
+ type="button"
155
+ className={cn(
156
+ "px-2 py-1 text-xs rounded",
157
+ viewMode === "tree"
158
+ ? "bg-accent text-accent-foreground"
159
+ : "text-muted-foreground hover:text-foreground",
160
+ )}
161
+ onClick={() => setUserViewMode("tree")}
162
+ >
163
+ Tree
164
+ </button>
165
+ </div>
166
+ ) : (
167
+ <div />
168
+ )}
136
169
  <div className="flex items-center gap-2">
137
170
  <div
138
171
  className="items-center border px-2 py-0.5 text-xs transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground rounded-sm text-ellipsis block overflow-hidden max-w-fit font-medium"
139
- title={isSandbox ? "sandbox" : "project"}
172
+ title={scopeTitle}
140
173
  >
141
- {isSandbox ? "sandbox" : "project"}
174
+ {scopeLabel}
142
175
  </div>
143
176
  {name && !isSandbox && (
144
177
  <span className="text-xs text-muted-foreground">
@@ -155,6 +188,7 @@ const PackagesPanel: React.FC = () => {
155
188
  <DependencyTree
156
189
  tree={dependencies.tree}
157
190
  error={error}
191
+ sandboxBackend={sandboxBackend}
158
192
  onSuccess={refetch}
159
193
  />
160
194
  )}
@@ -165,11 +199,13 @@ const PackagesPanel: React.FC = () => {
165
199
  export default PackagesPanel;
166
200
 
167
201
  const InstallPackageForm: React.FC<{
168
- packageManager: string;
202
+ context: PackageInstallationContext;
169
203
  onSuccess: () => void;
170
- }> = ({ onSuccess, packageManager }) => {
204
+ }> = ({ onSuccess, context }) => {
171
205
  const [input, setInput] = React.useState("");
172
206
  const { handleClick: openSettings } = useOpenSettingsToTab();
207
+ const isSandbox = context.kind === "sandbox";
208
+ const packageManager = isSandbox ? context.backend : context.name;
173
209
 
174
210
  // Get the packages to install from the atom
175
211
  const packagesToInstall = useAtomValue(packagesToInstallAtom);
@@ -201,7 +237,11 @@ const InstallPackageForm: React.FC<{
201
237
  return (
202
238
  <div className="flex items-center w-full border-b">
203
239
  <SearchInput
204
- placeholder={`Install packages with ${packageManager}...`}
240
+ placeholder={
241
+ isSandbox
242
+ ? `Add packages to ${packageManager} sandbox...`
243
+ : `Install packages with ${packageManager}...`
244
+ }
205
245
  id={PACKAGES_INPUT_ID}
206
246
  icon={
207
247
  loading ? (
@@ -209,12 +249,23 @@ const InstallPackageForm: React.FC<{
209
249
  size="small"
210
250
  className="mr-2 h-4 w-4 shrink-0 opacity-50"
211
251
  />
252
+ ) : isSandbox ? (
253
+ <BoxIcon
254
+ aria-hidden="true"
255
+ className="mr-2 h-4 w-4 shrink-0 opacity-50"
256
+ />
212
257
  ) : (
213
- <Tooltip content="Change package manager">
214
- <BoxIcon
258
+ <Tooltip
259
+ content={`Change package manager (currently ${packageManager})`}
260
+ >
261
+ <button
262
+ type="button"
263
+ aria-label="Change package manager"
215
264
  onClick={() => openSettings("packageManagementAndData")}
216
- className="mr-2 h-4 w-4 shrink-0 opacity-50 hover:opacity-80 cursor-pointer"
217
- />
265
+ className="pointer-events-auto mr-2 rounded-sm opacity-50 hover:opacity-80 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
266
+ >
267
+ <BoxIcon aria-hidden="true" className="h-4 w-4 shrink-0" />
268
+ </button>
218
269
  </Tooltip>
219
270
  )
220
271
  }
@@ -234,9 +285,21 @@ const InstallPackageForm: React.FC<{
234
285
  align="start"
235
286
  content={
236
287
  <div className="text-sm flex flex-col w-full max-w-[360px]">
237
- Packages are installed using the package manager specified in your
238
- user configuration. Depending on your package manager, you can
239
- install packages with various formats:
288
+ {isSandbox ? (
289
+ <span>
290
+ Packages are recorded in this notebook&apos;s inline metadata
291
+ and synchronized with its {packageManager} sandbox. To switch
292
+ backends, restart marimo with a different --sandbox option.
293
+ </span>
294
+ ) : (
295
+ <span>
296
+ Packages are installed using {packageManager}, selected in your
297
+ user configuration.
298
+ </span>
299
+ )}
300
+ <span className="mt-2">
301
+ You can install packages with various formats:
302
+ </span>
240
303
  <div className="flex flex-col gap-2 mt-2">
241
304
  <div>
242
305
  <span className="font-bold tracking-wide">Package name:</span> A
@@ -379,7 +442,9 @@ const UpgradeButton: React.FC<{
379
442
  upgrade: true,
380
443
  group,
381
444
  });
382
- if (response.success) {
445
+ if (response.restartRequired) {
446
+ showPackageRestartToast();
447
+ } else if (response.success) {
383
448
  onSuccess();
384
449
  showUpgradePackageToast(packageName);
385
450
  } else {
@@ -413,7 +478,9 @@ const RemoveButton: React.FC<{
413
478
  package: packageName,
414
479
  group,
415
480
  });
416
- if (response.success) {
481
+ if (response.restartRequired) {
482
+ showPackageRestartToast();
483
+ } else if (response.success) {
417
484
  onSuccess();
418
485
  showRemovePackageToast(packageName);
419
486
  } else {
@@ -434,8 +501,9 @@ const RemoveButton: React.FC<{
434
501
  const DependencyTree: React.FC<{
435
502
  tree: DependencyTreeNode | null;
436
503
  error?: Error | null;
504
+ sandboxBackend: "uv" | "pixi" | null;
437
505
  onSuccess: () => void;
438
- }> = ({ tree, error, onSuccess }) => {
506
+ }> = ({ tree, error, sandboxBackend, onSuccess }) => {
439
507
  const [expandedNodes, setExpandedNodes] = React.useState<Set<string>>(
440
508
  new Set(),
441
509
  );
@@ -450,10 +518,25 @@ const DependencyTree: React.FC<{
450
518
  }
451
519
 
452
520
  if (!tree) {
453
- return <Spinner size="medium" centered={true} />;
521
+ return (
522
+ <PanelEmptyState
523
+ title="Dependency tree unavailable"
524
+ description="The sandbox did not return a dependency tree."
525
+ icon={<BoxIcon />}
526
+ />
527
+ );
454
528
  }
455
529
 
456
530
  if (tree.dependencies.length === 0) {
531
+ if (sandboxBackend === "pixi") {
532
+ return (
533
+ <PanelEmptyState
534
+ title="No PyPI dependencies"
535
+ description="Conda dependencies are not shown in this panel."
536
+ icon={<BoxIcon />}
537
+ />
538
+ );
539
+ }
457
540
  return (
458
541
  <PanelEmptyState
459
542
  title="No dependencies"
@@ -575,6 +658,17 @@ const DependencyTreeNode: React.FC<{
575
658
  {/* Tags */}
576
659
  <div className="flex items-center gap-1 ml-2">
577
660
  {node.tags.map((tag, index) => {
661
+ if (tag.kind === "dedupe") {
662
+ return (
663
+ <div
664
+ key={index}
665
+ className="items-center border px-2 py-0.5 text-xs transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 text-muted-foreground rounded-sm text-ellipsis block overflow-hidden max-w-fit font-medium"
666
+ title="Package tree already displayed"
667
+ >
668
+ already in tree
669
+ </div>
670
+ );
671
+ }
578
672
  if (tag.kind === "cycle") {
579
673
  return (
580
674
  <div
@@ -10,6 +10,7 @@ import {
10
10
  PackageCheckIcon,
11
11
  PackageXIcon,
12
12
  PlusIcon,
13
+ RotateCwIcon,
13
14
  XIcon,
14
15
  } from "lucide-react";
15
16
  import type React from "react";
@@ -32,6 +33,7 @@ import {
32
33
  import { useResolvedMarimoConfig } from "@/core/config/config";
33
34
  import type { PackageInstallationStatus } from "@/core/kernel/messages";
34
35
  import { useRequestClient } from "@/core/network/requests";
36
+ import { RESTART_REQUIRED_DESCRIPTION } from "@/core/packages/toast-components";
35
37
  import { isWasm } from "@/core/wasm/utils";
36
38
  import { usePackageMetadata } from "@/hooks/usePackageMetadata";
37
39
  import { Banner } from "@/plugins/impl/common/error-banner";
@@ -55,6 +57,12 @@ import {
55
57
  import { ExternalLink } from "../ui/links";
56
58
  import { NativeSelect } from "../ui/native-select";
57
59
  import { Tooltip } from "../ui/tooltip";
60
+ import { useRestartKernel } from "./actions/useRestartKernel";
61
+
62
+ const RestartKernelButton = () => {
63
+ const restartKernel = useRestartKernel();
64
+ return <Button onClick={restartKernel}>Restart Kernel</Button>;
65
+ };
58
66
 
59
67
  function parsePackageSpecifier(spec: string): {
60
68
  name: string;
@@ -251,6 +259,10 @@ export const PackageAlert: React.FC = () => {
251
259
  )}
252
260
  >
253
261
  <p>{description}</p>
262
+ {status !== "installing" &&
263
+ Object.values(packageAlert.packages).includes(
264
+ "restart-required",
265
+ ) && <RestartKernelButton />}
254
266
  <ul className="list-disc ml-2 mt-1">
255
267
  {Object.entries(packageAlert.packages).map(([pkg, st], index) => (
256
268
  <li
@@ -290,7 +302,9 @@ function getInstallationStatusElements(packages: PackageInstallationStatus) {
290
302
  ? "installing"
291
303
  : statuses.has("failed")
292
304
  ? "failed"
293
- : "installed";
305
+ : statuses.has("restart-required")
306
+ ? "restart-required"
307
+ : "installed";
294
308
 
295
309
  if (status === "installing") {
296
310
  return {
@@ -300,6 +314,14 @@ function getInstallationStatusElements(packages: PackageInstallationStatus) {
300
314
  description: "Installing packages:",
301
315
  };
302
316
  }
317
+ if (status === "restart-required") {
318
+ return {
319
+ status,
320
+ title: "Changes saved — restart required",
321
+ titleIcon: <RotateCwIcon className="w-5 h-5 inline-block mr-2" />,
322
+ description: RESTART_REQUIRED_DESCRIPTION,
323
+ };
324
+ }
303
325
  if (status === "installed") {
304
326
  return {
305
327
  status: "installed",
@@ -330,6 +352,8 @@ const ProgressIcon = ({
330
352
  return <CheckIcon size="1rem" />;
331
353
  case "failed":
332
354
  return <XIcon size="1rem" />;
355
+ case "restart-required":
356
+ return <RotateCwIcon size="1rem" />;
333
357
  default:
334
358
  logNever(status);
335
359
  return null;
@@ -1,10 +1,22 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { act, renderHook } from "@testing-library/react";
2
+ import {
3
+ act,
4
+ fireEvent,
5
+ render,
6
+ renderHook,
7
+ screen,
8
+ } from "@testing-library/react";
3
9
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
10
  import { useInstallPackages } from "../useInstallPackage";
11
+ import { Toast, ToastProvider, ToastViewport } from "@/components/ui/toast";
5
12
 
6
13
  const toast = vi.fn();
7
14
  const addPackage = vi.fn();
15
+ const restartKernel = vi.fn();
16
+
17
+ vi.mock("@/components/editor/actions/useRestartKernel", () => ({
18
+ useRestartKernel: () => restartKernel,
19
+ }));
8
20
 
9
21
  vi.mock("@/components/ui/use-toast", () => ({
10
22
  toast: (...args: unknown[]) => toast(...args),
@@ -20,6 +32,7 @@ describe("useInstallPackages", () => {
20
32
  beforeEach(() => {
21
33
  toast.mockClear();
22
34
  addPackage.mockClear();
35
+ restartKernel.mockClear();
23
36
  });
24
37
 
25
38
  it("batches all packages into a single install call", async () => {
@@ -109,6 +122,39 @@ describe("useInstallPackages", () => {
109
122
  );
110
123
  });
111
124
 
125
+ it("reports saved changes requiring restart without claiming installation", async () => {
126
+ addPackage.mockResolvedValue({
127
+ success: false,
128
+ error: null,
129
+ restartRequired: true,
130
+ });
131
+ const { result } = renderHook(() => useInstallPackages());
132
+ await act(async () => {
133
+ await result.current.handleInstallPackages(["boltons", "six"]);
134
+ });
135
+ expect(toast).toHaveBeenCalledTimes(1);
136
+ expect(toast).toHaveBeenCalledWith(
137
+ expect.objectContaining({
138
+ title: "Changes saved — restart required",
139
+ description: expect.stringContaining(
140
+ "Restarting clears in-memory variables",
141
+ ),
142
+ duration: Infinity,
143
+ action: expect.anything(),
144
+ }),
145
+ );
146
+ expect(toast.mock.calls[0][0].variant).not.toBe("danger");
147
+ render(
148
+ <ToastProvider>
149
+ <Toast>{toast.mock.calls[0][0].action}</Toast>
150
+ <ToastViewport />
151
+ </ToastProvider>,
152
+ );
153
+ expect(restartKernel).not.toHaveBeenCalled();
154
+ fireEvent.click(screen.getByRole("button", { name: "Restart Kernel" }));
155
+ expect(restartKernel).toHaveBeenCalledTimes(1);
156
+ });
157
+
112
158
  it("calls onSuccess after a successful install", async () => {
113
159
  addPackage.mockResolvedValue({ success: true, error: null });
114
160
  const onSuccess = vi.fn();
@@ -1,8 +1,34 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { useRestartKernel } from "@/components/editor/actions/useRestartKernel";
3
4
  import { Kbd } from "@/components/ui/kbd";
5
+ import { ToastAction } from "@/components/ui/toast";
4
6
  import { toast } from "@/components/ui/use-toast";
5
7
 
8
+ export const RESTART_REQUIRED_DESCRIPTION =
9
+ "Your dependency changes are saved. Restart the kernel to use the updated environment. Restarting clears in-memory variables.";
10
+
11
+ const RestartKernelAction = () => {
12
+ const restartKernel = useRestartKernel();
13
+ return (
14
+ <ToastAction
15
+ altText="Restart the kernel to use the updated dependencies"
16
+ onClick={restartKernel}
17
+ >
18
+ Restart Kernel
19
+ </ToastAction>
20
+ );
21
+ };
22
+
23
+ export const showPackageRestartToast = () => {
24
+ toast({
25
+ title: "Changes saved — restart required",
26
+ description: RESTART_REQUIRED_DESCRIPTION,
27
+ duration: Infinity,
28
+ action: <RestartKernelAction />,
29
+ });
30
+ };
31
+
6
32
  export const showAddPackageToast = (
7
33
  packageName: string | string[],
8
34
  error?: string | null,
@@ -3,7 +3,10 @@
3
3
  import { useState } from "react";
4
4
  import { Logger } from "@/utils/Logger";
5
5
  import { useRequestClient } from "../network/requests";
6
- import { showAddPackageToast } from "./toast-components";
6
+ import {
7
+ showAddPackageToast,
8
+ showPackageRestartToast,
9
+ } from "./toast-components";
7
10
 
8
11
  export function useInstallPackages(): {
9
12
  handleInstallPackages: (
@@ -30,7 +33,9 @@ export function useInstallPackages(): {
30
33
  // response only carries an aggregate success/error. Report a single
31
34
  // toast covering all requested packages rather than implying a
32
35
  // per-package outcome we don't actually have.
33
- if (response.success) {
36
+ if (response.restartRequired) {
37
+ showPackageRestartToast();
38
+ } else if (response.success) {
34
39
  showAddPackageToast(packages);
35
40
  } else {
36
41
  showAddPackageToast(packages, response.error);
@@ -649,6 +649,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
649
649
  tags: [],
650
650
  version: null,
651
651
  },
652
+ context: { kind: "package-manager", name: "micropip" },
652
653
  };
653
654
  };
654
655