@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.
- package/dist/assets/{CellStatus-LoTwk7fR.js → CellStatus-0QNZS4By.js} +1 -1
- package/dist/assets/{JsonOutput-CzIGPEZQ.js → JsonOutput-D2ZWLZs4.js} +7 -7
- package/dist/assets/{MarimoErrorOutput-DqpoYOvw.js → MarimoErrorOutput-BlmIJ9WY.js} +2 -2
- package/dist/assets/{RenderHTML-DwhMAd9l.js → RenderHTML-DnPB4qpJ.js} +1 -1
- package/dist/assets/{RunButton-BlpbweyH.js → RunButton-C7TxE07w.js} +1 -1
- package/dist/assets/{add-cell-with-ai-R-3DS79K.js → add-cell-with-ai-OQ4sSXg4.js} +12 -12
- package/dist/assets/{add-connection-dialog-BGfpw5lC.js → add-connection-dialog-MTOC2ycO.js} +73 -73
- package/dist/assets/{agent-panel-Cgt_K0tD.js → agent-panel-BdEDIHdc.js} +4 -4
- package/dist/assets/ai-model-dropdown-CmJEEhSC.js +3 -0
- package/dist/assets/app-config-button-W8Pytgb5.js +1 -0
- package/dist/assets/{blob-B2d_dHwI.js → blob-BrVH7IJp.js} +1 -1
- package/dist/assets/{cell-editor-Dw09J9yD.js → cell-editor-C7IiingQ.js} +8 -8
- package/dist/assets/{cell-link-6burT2XF.js → cell-link-CbHXmrt8.js} +1 -1
- package/dist/assets/{cells-Cp-G34Rz.js → cells-BBhW3FXj.js} +1 -1
- package/dist/assets/{chat-display-Bu405iyc.js → chat-display-CIpYPLlF.js} +1 -1
- package/dist/assets/{chat-panel-BlUwNY5j.js → chat-panel-CK3X_KPy.js} +2 -2
- package/dist/assets/{chat-ui-DGiVDzNh.js → chat-ui-BJwtx1SR.js} +4 -4
- package/dist/assets/{column-preview-riSXbYe_.js → column-preview-Coa5PyHk.js} +1 -1
- package/dist/assets/{command-palette-CCVzJcyf.js → command-palette-_xpwMniA.js} +1 -1
- package/dist/assets/{common-CHR7w7aQ.js → common-KqtqJsml.js} +1 -1
- package/dist/assets/{components-C7mWrmXr.js → components-Cpv2z-yV.js} +1 -1
- package/dist/assets/{datasource-DrODSIs0.js → datasource-B3qF2oaW.js} +2 -2
- package/dist/assets/{dependency-graph-panel-AY5BEVH2.js → dependency-graph-panel-Av1CMZ61.js} +3 -3
- package/dist/assets/{documentation-panel-CaJF7e-j.js → documentation-panel-BEz3Hjfu.js} +1 -1
- package/dist/assets/{download-DMxOzI7b.js → download-BgO9EN-W.js} +1 -1
- package/dist/assets/edit-page-DslcxB67.js +9 -0
- package/dist/assets/error-entries-BFzSRy80.js +5 -0
- package/dist/assets/{error-panel-Bhlvt6_H.js → error-panel-BtLTJDUq.js} +1 -1
- package/dist/assets/{file-explorer-panel-s0ohSG_L.js → file-explorer-panel-DEyChgqS.js} +1 -1
- package/dist/assets/{file-icons-DdGYuADJ.js → file-icons-BhotQqrU.js} +1 -1
- package/dist/assets/{file-name-input-K4MKESI_.js → file-name-input-CH-hNP6F.js} +1 -1
- package/dist/assets/{floating-outline-DaUHnl_e.js → floating-outline-Cmk3XDJi.js} +1 -1
- package/dist/assets/{home-page-BLd6rnbk.js → home-page-CUBuu5Go.js} +1 -1
- package/dist/assets/index-C5h1aWhw.js +29 -0
- package/dist/assets/{layout-DWnrxIo2.js → layout-CsLuqwD3.js} +5 -5
- package/dist/assets/{logs-panel-BGS2v3ba.js → logs-panel-BV4VZ_uH.js} +1 -1
- package/dist/assets/{markdown-renderer-BZKlbm2Q.js → markdown-renderer-751VNlrQ.js} +1 -1
- package/dist/assets/{name-cell-input-BMCdK-el.js → name-cell-input-DexJ7ZIH.js} +1 -1
- package/dist/assets/{outline-panel-Dr5u73V6.js → outline-panel-QU3nj1VW.js} +1 -1
- package/dist/assets/packages-panel-D7oDuI6v.js +1 -0
- package/dist/assets/panel-accordion-state-Dl0VcB79.js +1 -0
- package/dist/assets/panels-nM4QKfEI.js +4 -0
- package/dist/assets/process-output-CPUOlFhp.js +1 -0
- package/dist/assets/readonly-code-display-DP7DxRA1.js +1 -0
- package/dist/assets/{readonly-python-code-DpIyjHqY.js → readonly-python-code-CAYyabaV.js} +1 -1
- package/dist/assets/{reveal-component-nZIAapoD.js → reveal-component-D9DygPds.js} +2 -2
- package/dist/assets/rotate-cw-CESctpLh.js +1 -0
- package/dist/assets/{run-page-9pl8qhQR.js → run-page-CzO-uMUn.js} +1 -1
- package/dist/assets/{scratchpad-panel-BleGt2KM.js → scratchpad-panel-7YBL6E95.js} +1 -1
- package/dist/assets/session-panel-hnFaUP0Y.js +1 -0
- package/dist/assets/share-BZW3tzEX.js +1 -0
- package/dist/assets/{skeleton-BKeh8hf3.js → skeleton-CM7HxSva.js} +1 -1
- package/dist/assets/{snippets-panel-D5yy7kj0.js → snippets-panel-AdjuR23O.js} +1 -1
- package/dist/assets/state-DOrY1Jyf.js +1 -0
- package/dist/assets/state-DQHafeTV.js +3 -0
- package/dist/assets/{tracing-DNk3gmWv.js → tracing-CUlvXr_H.js} +1 -1
- package/dist/assets/{tracing-panel-Yqazr6w_.js → tracing-panel-CjyayE-l.js} +2 -2
- package/dist/assets/{tree-actions-CG5g7aUQ.js → tree-actions-mfuAcK1x.js} +1 -1
- package/dist/assets/{useAddCell-DdB_uBif.js → useAddCell-BR8kYnb6.js} +1 -1
- package/dist/assets/{useCellActionButton-B31neI3m.js → useCellActionButton-BhREXvvn.js} +1 -1
- package/dist/assets/{useDeleteCell-Bz49_cFj.js → useDeleteCell-DwaN6KAw.js} +1 -1
- package/dist/assets/useDependencyPanelTab-I-7B78cW.js +1 -0
- package/dist/assets/useInstallPackage-IUt497GC.js +1 -0
- package/dist/assets/useNotebookActions-D5_YLR42.js +10 -0
- package/dist/assets/useRunCells-BMQuitYY.js +1 -0
- package/dist/assets/{useSplitCell-Dy2_DmJR.js → useSplitCell-B7AvHoya.js} +1 -1
- package/dist/index.html +22 -22
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +7 -2
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +143 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +145 -51
- package/src/components/editor/package-alert.tsx +25 -1
- package/src/core/packages/__tests__/useInstallPackage.test.tsx +47 -1
- package/src/core/packages/toast-components.tsx +26 -0
- package/src/core/packages/useInstallPackage.ts +7 -2
- package/src/core/wasm/bridge.ts +1 -0
- package/dist/assets/ai-model-dropdown-DCmiuk7x.js +0 -3
- package/dist/assets/app-config-button-DUJC6b6x.js +0 -1
- package/dist/assets/edit-page-xlMlcLnI.js +0 -9
- package/dist/assets/error-entries-Cv2m1yhc.js +0 -5
- package/dist/assets/index-4n22QjCD.js +0 -29
- package/dist/assets/packages-panel-BdpRg9tX.js +0 -1
- package/dist/assets/panel-accordion-state-DjUTN7i5.js +0 -1
- package/dist/assets/panels-CWn0Yh1D.js +0 -4
- package/dist/assets/process-output-Ck7-P_xp.js +0 -1
- package/dist/assets/readonly-code-display--S2daDa6.js +0 -1
- package/dist/assets/session-panel-DRGPlL5J.js +0 -1
- package/dist/assets/share-CPtBiNgr.js +0 -1
- package/dist/assets/state-B6AklCzD.js +0 -1
- package/dist/assets/state-Di9TbuV9.js +0 -3
- package/dist/assets/useDependencyPanelTab-C_ps0r--.js +0 -1
- package/dist/assets/useInstallPackage-DsD08SLK.js +0 -1
- package/dist/assets/useNotebookActions-CQtbk9CB.js +0 -10
- 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 {
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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={
|
|
172
|
+
title={scopeTitle}
|
|
140
173
|
>
|
|
141
|
-
{
|
|
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
|
-
|
|
202
|
+
context: PackageInstallationContext;
|
|
169
203
|
onSuccess: () => void;
|
|
170
|
-
}> = ({ onSuccess,
|
|
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={
|
|
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
|
|
214
|
-
|
|
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="
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
{isSandbox ? (
|
|
289
|
+
<span>
|
|
290
|
+
Packages are recorded in this notebook'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.
|
|
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.
|
|
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
|
|
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
|
-
: "
|
|
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 {
|
|
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 {
|
|
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.
|
|
36
|
+
if (response.restartRequired) {
|
|
37
|
+
showPackageRestartToast();
|
|
38
|
+
} else if (response.success) {
|
|
34
39
|
showAddPackageToast(packages);
|
|
35
40
|
} else {
|
|
36
41
|
showAddPackageToast(packages, response.error);
|