@marimo-team/frontend 0.24.3-dev1 → 0.24.3-dev10
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-DXkwkuL9.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DW6cJ-Lr.js → ConnectedDataExplorerComponent-DiXd-oqU.js} +1 -1
- package/dist/assets/JsonOutput-izmGhjov.js +50 -0
- package/dist/assets/LazyAnyLanguageCodeMirror-C_cwGmDg.js +2 -0
- package/dist/assets/{MarimoErrorOutput-DqpoYOvw.js → MarimoErrorOutput-CdmP3ZHL.js} +2 -2
- package/dist/assets/{RenderHTML-DwhMAd9l.js → RenderHTML-DlYXvh8D.js} +1 -1
- package/dist/assets/{RunButton-BlpbweyH.js → RunButton-Bieadi9w.js} +1 -1
- package/dist/assets/add-cell-with-ai-ZpV9L4qe.js +76 -0
- package/dist/assets/{add-connection-dialog-BGfpw5lC.js → add-connection-dialog-a8mkIeXa.js} +73 -73
- package/dist/assets/agent-panel-rVtDSi07.js +286 -0
- package/dist/assets/ai-model-dropdown-C8IztRe2.js +3 -0
- package/dist/assets/{any-language-editor-DflLjPSQ.js → any-language-editor-pGZDaSqz.js} +1 -1
- package/dist/assets/app-config-button-DLr-6jwI.js +1 -0
- package/dist/assets/{blob-B2d_dHwI.js → blob-B2Qs47Zi.js} +1 -1
- package/dist/assets/cell-editor-3VpJI5EQ.js +21 -0
- package/dist/assets/cell-link-DEXHmwy1.js +1 -0
- package/dist/assets/{cells-Cp-G34Rz.js → cells--Hh9voS5.js} +25 -25
- package/dist/assets/chat-display-DtQ0tOGK.js +1 -0
- package/dist/assets/{chat-panel-BlUwNY5j.js → chat-panel-DLgUUTdo.js} +2 -2
- package/dist/assets/{chat-ui-DGiVDzNh.js → chat-ui-DIXDoCsM.js} +4 -4
- package/dist/assets/{column-preview-riSXbYe_.js → column-preview-H8rmRiii.js} +1 -1
- package/dist/assets/{command-palette-CCVzJcyf.js → command-palette-Pmg3QxZ0.js} +1 -1
- package/dist/assets/{common-CHR7w7aQ.js → common-CrhvbKbI.js} +1 -1
- package/dist/assets/components-DWwMSfV5.js +1 -0
- package/dist/assets/components-rCVlCccf.js +1 -0
- package/dist/assets/{config-BlNM0caL.js → config-BuGkUPaK.js} +1 -1
- package/dist/assets/{copy-icon-z8fuTE3w.js → copy-icon-BUOcnUgJ.js} +1 -1
- package/dist/assets/{datasource-DrODSIs0.js → datasource-DgkQfSw3.js} +2 -2
- package/dist/assets/{dependency-graph-panel-AY5BEVH2.js → dependency-graph-panel-D1BZdQJH.js} +3 -3
- package/dist/assets/dist-0lSCnrrf.js +1 -0
- package/dist/assets/dist-CLXTUtr2.js +1 -0
- package/dist/assets/{documentation-panel-CaJF7e-j.js → documentation-panel-DzNGB1Zx.js} +1 -1
- package/dist/assets/{download-DMxOzI7b.js → download-DtQweFN1.js} +1 -1
- package/dist/assets/dropdown-menu-DnvwbPho.js +1 -0
- package/dist/assets/edit-page-fb5fV9UA.js +9 -0
- package/dist/assets/{error-entries-Cv2m1yhc.js → error-entries-CMsalXvZ.js} +1 -1
- package/dist/assets/{error-panel-Bhlvt6_H.js → error-panel-D6kqFA9f.js} +1 -1
- package/dist/assets/file-explorer-panel-D3APtOiy.js +1 -0
- package/dist/assets/file-icons-wArFRwmS.js +1 -0
- package/dist/assets/{file-name-input-K4MKESI_.js → file-name-input-CdDGwb85.js} +3 -3
- package/dist/assets/{floating-outline-DaUHnl_e.js → floating-outline-D2WpILR-.js} +1 -1
- package/dist/assets/gallery-page-B39w2KuX.js +1 -0
- package/dist/assets/{glide-data-editor-sICUMEhk.js → glide-data-editor-BufxOYjB.js} +1 -1
- package/dist/assets/home-page-BXgJYrZ5.js +4 -0
- package/dist/assets/index-B4Fq20-p.js +29 -0
- package/dist/assets/{kiosk-mode-mxgb5lWf.js → kiosk-mode-m6yKu6eh.js} +1 -1
- package/dist/assets/{layout-DWnrxIo2.js → layout-VgriO3Ll.js} +5 -5
- package/dist/assets/logs-panel-DsbX3K3h.js +1 -0
- package/dist/assets/{markdown-renderer-BZKlbm2Q.js → markdown-renderer-DD7ui8RW.js} +1 -1
- package/dist/assets/{name-cell-input-BMCdK-el.js → name-cell-input-EjA-eGvA.js} +1 -1
- package/dist/assets/{outline-panel-Dr5u73V6.js → outline-panel-DeDFCw9s.js} +1 -1
- package/dist/assets/packages-panel-B__MaP9u.js +1 -0
- package/dist/assets/{pair-with-agent-modal-CbUFKJHP.js → pair-with-agent-modal-DYvcjx3q.js} +1 -1
- package/dist/assets/panel-accordion-state-CwLKISCL.js +1 -0
- package/dist/assets/panels-c3iYYVuV.js +4 -0
- package/dist/assets/{pathUtils-Cryrlpma.js → pathUtils-D7lk2rYQ.js} +1 -1
- package/dist/assets/paths-ClYq6Exe.js +1 -0
- package/dist/assets/process-output-DQoLxPra.js +1 -0
- package/dist/assets/readonly-code-display-DuFy8860.js +1 -0
- package/dist/assets/{readonly-python-code-DpIyjHqY.js → readonly-python-code-yR65TOiM.js} +1 -1
- package/dist/assets/{renderShortcut-X-CJUSM8.js → renderShortcut-BE8QI9Yi.js} +1 -1
- package/dist/assets/{reveal-component-nZIAapoD.js → reveal-component-CgskYNVn.js} +2 -2
- package/dist/assets/rotate-cw-CESctpLh.js +1 -0
- package/dist/assets/run-page-C_mciAdT.js +1 -0
- package/dist/assets/{scratchpad-panel-BleGt2KM.js → scratchpad-panel-Bj4yxnuS.js} +1 -1
- package/dist/assets/{secrets-panel-D3BpDIgg.js → secrets-panel-Cf-U5wY0.js} +1 -1
- package/dist/assets/select-C1IYgK48.js +1 -0
- package/dist/assets/session-panel-NQyDQouJ.js +1 -0
- package/dist/assets/share-BZW3tzEX.js +1 -0
- package/dist/assets/{skeleton-BKeh8hf3.js → skeleton-CUFHwpIk.js} +1 -1
- package/dist/assets/{snippets-panel-D5yy7kj0.js → snippets-panel-Bx2wFck8.js} +1 -1
- package/dist/assets/{state-B6AklCzD.js → state-CdjswcRf.js} +1 -1
- package/dist/assets/state-CiPrV156.js +3 -0
- package/dist/assets/switch-DhPDzMo3.js +1 -0
- package/dist/assets/{terminal-CQAFGBfN.js → terminal-DH54UV0y.js} +2 -2
- package/dist/assets/tooltip-BvU0ko_d.js +1 -0
- package/dist/assets/tracing-Qjfb3Qm1.js +1 -0
- package/dist/assets/tracing-panel-Cj_-LD2X.js +2 -0
- package/dist/assets/{tree-actions-CG5g7aUQ.js → tree-actions-CmY9PFuu.js} +1 -1
- package/dist/assets/{useAddCell-DdB_uBif.js → useAddCell-Bvb-C_H5.js} +1 -1
- package/dist/assets/{useBoolean-BXC4CItZ.js → useBoolean-zcg-oxnj.js} +1 -1
- package/dist/assets/useCellActionButton-Df4qSnWR.js +1 -0
- package/dist/assets/{useDeleteCell-Bz49_cFj.js → useDeleteCell-eNMTvg8D.js} +1 -1
- package/dist/assets/useDependencyPanelTab-BusYWoB8.js +1 -0
- package/dist/assets/useInstallPackage-ClaR_nYb.js +1 -0
- package/dist/assets/useNotebookActions-bWJW4dS_.js +10 -0
- package/dist/assets/{useRunCells-CX7oW5zy.js → useRunCells-D8B7UNsg.js} +1 -1
- package/dist/assets/{useSplitCell-Dy2_DmJR.js → useSplitCell-Cy7fCcQW.js} +1 -1
- package/dist/assets/{vega-component-C7TCzyaU.js → vega-component-Z8mt9AUF.js} +1 -1
- package/dist/assets/{write-secret-modal-D6zoZgLY.js → write-secret-modal-CSWlDX_Z.js} +1 -1
- package/dist/index.html +36 -35
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +7 -2
- package/src/components/data-table/column-visibility-dropdown.tsx +9 -3
- package/src/components/datasources/components.tsx +4 -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/components/home/components.tsx +17 -0
- package/src/components/pages/gallery-page.tsx +2 -4
- package/src/components/pages/home-page.tsx +2 -4
- package/src/components/terminal/terminal.tsx +4 -4
- package/src/core/islands/__tests__/parse.test.ts +140 -7
- package/src/core/islands/parse.ts +20 -0
- 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/runtime/__tests__/runtime.test.ts +11 -0
- package/src/core/runtime/runtime.ts +7 -2
- package/src/core/wasm/bridge.ts +1 -0
- package/dist/assets/JsonOutput-CzIGPEZQ.js +0 -50
- package/dist/assets/LazyAnyLanguageCodeMirror-LboTfNDv.js +0 -2
- package/dist/assets/add-cell-with-ai-R-3DS79K.js +0 -76
- package/dist/assets/agent-panel-Cgt_K0tD.js +0 -286
- package/dist/assets/ai-model-dropdown-DCmiuk7x.js +0 -3
- package/dist/assets/app-config-button-DUJC6b6x.js +0 -1
- package/dist/assets/cell-editor-Dw09J9yD.js +0 -21
- package/dist/assets/cell-link-6burT2XF.js +0 -1
- package/dist/assets/chat-display-Bu405iyc.js +0 -1
- package/dist/assets/components-C7mWrmXr.js +0 -1
- package/dist/assets/dist-DzCQtGkt.js +0 -1
- package/dist/assets/dropdown-menu-C40jM9o4.js +0 -1
- package/dist/assets/edit-page-xlMlcLnI.js +0 -9
- package/dist/assets/file-explorer-panel-s0ohSG_L.js +0 -1
- package/dist/assets/file-icons-DdGYuADJ.js +0 -1
- package/dist/assets/gallery-page-C3pBpHlY.js +0 -1
- package/dist/assets/home-page-BLd6rnbk.js +0 -4
- package/dist/assets/index-4n22QjCD.js +0 -29
- package/dist/assets/logs-panel-BGS2v3ba.js +0 -1
- 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/paths-zcL04qUW.js +0 -1
- package/dist/assets/process-output-Ck7-P_xp.js +0 -1
- package/dist/assets/readonly-code-display--S2daDa6.js +0 -1
- package/dist/assets/run-page-9pl8qhQR.js +0 -1
- package/dist/assets/select-nLt2y0Sw.js +0 -1
- package/dist/assets/session-panel-DRGPlL5J.js +0 -1
- package/dist/assets/share-CPtBiNgr.js +0 -1
- package/dist/assets/state-Di9TbuV9.js +0 -3
- package/dist/assets/switch-DjXr9Y9D.js +0 -1
- package/dist/assets/tooltip-BUtPzKB4.js +0 -1
- package/dist/assets/tracing-DNk3gmWv.js +0 -1
- package/dist/assets/tracing-panel-Yqazr6w_.js +0 -2
- package/dist/assets/useCellActionButton-B31neI3m.js +0 -1
- 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
|
@@ -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;
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { Constants } from "@/core/constants";
|
|
32
32
|
import { useRequestClient } from "@/core/network/requests";
|
|
33
33
|
import type { TutorialId } from "@/core/network/types";
|
|
34
|
+
import { Banner } from "@/plugins/impl/common/error-banner";
|
|
34
35
|
import { openNotebook } from "@/utils/links";
|
|
35
36
|
import { Objects } from "@/utils/objects";
|
|
36
37
|
import { MarimoPlusIcon } from "../icons/marimo-icons";
|
|
@@ -186,6 +187,22 @@ export const ResourceLinks: React.FC = () => {
|
|
|
186
187
|
);
|
|
187
188
|
};
|
|
188
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Shown when the workspace scan stopped early, so the file list the user is
|
|
192
|
+
* looking at is missing files and folders.
|
|
193
|
+
*/
|
|
194
|
+
export const TruncatedWorkspaceBanner: React.FC<{
|
|
195
|
+
fileCount: number | undefined;
|
|
196
|
+
}> = ({ fileCount }) => {
|
|
197
|
+
return (
|
|
198
|
+
<Banner kind="warn" className="rounded p-4">
|
|
199
|
+
Showing {fileCount} files. Some files and folders were skipped because
|
|
200
|
+
your workspace is large or deeply nested. Start marimo from a subfolder to
|
|
201
|
+
see them.
|
|
202
|
+
</Banner>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
189
206
|
export const Header: React.FC<{
|
|
190
207
|
Icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
|
191
208
|
control?: React.ReactNode;
|
|
@@ -4,6 +4,7 @@ import { SearchIcon } from "lucide-react";
|
|
|
4
4
|
import type React from "react";
|
|
5
5
|
import { Suspense, useMemo, useState } from "react";
|
|
6
6
|
import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
|
|
7
|
+
import { TruncatedWorkspaceBanner } from "@/components/home/components";
|
|
7
8
|
import { Spinner } from "@/components/icons/spinner";
|
|
8
9
|
import { Card, CardContent } from "@/components/ui/card";
|
|
9
10
|
import { Input } from "@/components/ui/input";
|
|
@@ -119,10 +120,7 @@ const GalleryPage: React.FC = () => {
|
|
|
119
120
|
<ErrorBoundary>
|
|
120
121
|
<div className="flex flex-col gap-2">
|
|
121
122
|
{workspace.hasMore && (
|
|
122
|
-
<
|
|
123
|
-
Showing first {workspace.fileCount} files. Your workspace has
|
|
124
|
-
more files.
|
|
125
|
-
</Banner>
|
|
123
|
+
<TruncatedWorkspaceBanner fileCount={workspace.fileCount} />
|
|
126
124
|
)}
|
|
127
125
|
{formattedFiles.length > SEARCH_THRESHOLD && (
|
|
128
126
|
<Input
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
Header,
|
|
68
68
|
OpenTutorialDropDown,
|
|
69
69
|
ResourceLinks,
|
|
70
|
+
TruncatedWorkspaceBanner,
|
|
70
71
|
} from "../home/components";
|
|
71
72
|
import {
|
|
72
73
|
expandedFoldersAtom,
|
|
@@ -196,10 +197,7 @@ const WorkspaceNotebooks: React.FC<{ onRefreshRecents: () => void }> = ({
|
|
|
196
197
|
<WorkspaceContext value={workspaceContextValue}>
|
|
197
198
|
<div className="flex flex-col gap-2">
|
|
198
199
|
{workspace.hasMore && (
|
|
199
|
-
<
|
|
200
|
-
Showing first {workspace.fileCount} files. Your workspace has more
|
|
201
|
-
files.
|
|
202
|
-
</Banner>
|
|
200
|
+
<TruncatedWorkspaceBanner fileCount={workspace.fileCount} />
|
|
203
201
|
)}
|
|
204
202
|
<Header
|
|
205
203
|
Icon={BookTextIcon}
|
|
@@ -254,7 +254,9 @@ const TerminalComponent: React.FC<TerminalComponentProps> = ({
|
|
|
254
254
|
try {
|
|
255
255
|
await waitForConnectionOpen();
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
// Size the PTY before the shell draws its first prompt.
|
|
258
|
+
fitAddon.fit();
|
|
259
|
+
const socket = new WebSocket(runtimeManager.getTerminalWsURL(terminal));
|
|
258
260
|
const attachAddon = new AttachAddon(socket);
|
|
259
261
|
terminal.loadAddon(attachAddon);
|
|
260
262
|
wsRef.current = socket;
|
|
@@ -270,9 +272,7 @@ const TerminalComponent: React.FC<TerminalComponentProps> = ({
|
|
|
270
272
|
|
|
271
273
|
const handleOpen = () => {
|
|
272
274
|
updateReadyState();
|
|
273
|
-
//
|
|
274
|
-
// before the WS was OPEN, dropping the resize message and leaving
|
|
275
|
-
// the PTY at its default 0x0 winsize.
|
|
275
|
+
// The container may have resized while the connection was opening.
|
|
276
276
|
fitAddon.fit();
|
|
277
277
|
if (terminal.cols > 0 && terminal.rows > 0) {
|
|
278
278
|
socket.send(
|
|
@@ -19,6 +19,13 @@ import {
|
|
|
19
19
|
} from "../parse";
|
|
20
20
|
import { createMockIslandElement, createMockIslands } from "./test-utils.tsx";
|
|
21
21
|
|
|
22
|
+
function islandSnapshotPath(filename: string): string {
|
|
23
|
+
return new URL(
|
|
24
|
+
`../../../../../tests/_islands/snapshots/${filename}`,
|
|
25
|
+
import.meta.url,
|
|
26
|
+
).pathname.replace(/^\/@fs/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
function createPayloadCell(
|
|
23
30
|
overrides: Partial<{
|
|
24
31
|
cellId: string;
|
|
@@ -47,6 +54,7 @@ function appendPayload(
|
|
|
47
54
|
payload: {
|
|
48
55
|
schemaVersion: number;
|
|
49
56
|
appId: string;
|
|
57
|
+
dependencies?: unknown;
|
|
50
58
|
cells: ReturnType<typeof createPayloadCell>[];
|
|
51
59
|
},
|
|
52
60
|
) {
|
|
@@ -58,6 +66,33 @@ function appendPayload(
|
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
describe("createMarimoFile", () => {
|
|
69
|
+
it("should serialize dependencies as top-level script metadata", () => {
|
|
70
|
+
const file = createMarimoFile({
|
|
71
|
+
dependencies: ['cowsay==6.1; sys_platform == "emscripten"'],
|
|
72
|
+
cells: [{ code: "import cowsay" }],
|
|
73
|
+
});
|
|
74
|
+
expect(file).toBe(
|
|
75
|
+
[
|
|
76
|
+
"# /// script",
|
|
77
|
+
'# dependencies = ["cowsay==6.1; sys_platform == \\"emscripten\\""]',
|
|
78
|
+
"# ///",
|
|
79
|
+
"import marimo",
|
|
80
|
+
"app = marimo.App()",
|
|
81
|
+
"@app.cell",
|
|
82
|
+
"def __():",
|
|
83
|
+
" import cowsay",
|
|
84
|
+
" return",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should omit metadata for empty dependencies", () => {
|
|
90
|
+
const cells = [{ code: "import cowsay" }];
|
|
91
|
+
expect(createMarimoFile({ cells, dependencies: [] })).toBe(
|
|
92
|
+
createMarimoFile({ cells }),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
61
96
|
it("should return a string", () => {
|
|
62
97
|
const app = {
|
|
63
98
|
cells: [
|
|
@@ -736,13 +771,7 @@ describe("parseMarimoIslandApps", () => {
|
|
|
736
771
|
});
|
|
737
772
|
|
|
738
773
|
it("should parse Python-generated island payload snapshots", () => {
|
|
739
|
-
const html = readFileSync(
|
|
740
|
-
new URL(
|
|
741
|
-
"../../../../../tests/_islands/snapshots/html-payload.txt",
|
|
742
|
-
import.meta.url,
|
|
743
|
-
).pathname.replace(/^\/@fs/, ""),
|
|
744
|
-
"utf8",
|
|
745
|
-
);
|
|
774
|
+
const html = readFileSync(islandSnapshotPath("html-payload.txt"), "utf8");
|
|
746
775
|
container.innerHTML = html;
|
|
747
776
|
|
|
748
777
|
const result = parseMarimoIslandApps(container);
|
|
@@ -775,6 +804,108 @@ describe("parseMarimoIslandApps", () => {
|
|
|
775
804
|
expect(islands[2].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("1");
|
|
776
805
|
});
|
|
777
806
|
|
|
807
|
+
it("should restore metadata from a Python-generated dependency payload", () => {
|
|
808
|
+
container.innerHTML = readFileSync(
|
|
809
|
+
islandSnapshotPath("body-dependencies.txt"),
|
|
810
|
+
"utf8",
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
const probed = parseMarimoIslandApps(container, { materialize: false });
|
|
814
|
+
const apps = parseMarimoIslandApps(container);
|
|
815
|
+
|
|
816
|
+
expect(probed).toEqual(apps);
|
|
817
|
+
expect(apps).toEqual([
|
|
818
|
+
{
|
|
819
|
+
id: "main",
|
|
820
|
+
payloadBacked: true,
|
|
821
|
+
dependencies: [
|
|
822
|
+
'cowsay==6.1; sys_platform == "emscripten"',
|
|
823
|
+
"rich[jupyter]>=13",
|
|
824
|
+
"desktop; sys_platform != 'emscripten'",
|
|
825
|
+
"older-python; python_version < '3.13'",
|
|
826
|
+
"newer-python; python_version >= '3.13'",
|
|
827
|
+
],
|
|
828
|
+
cells: [{ cellId: "Hbol", code: "import cowsay", output: "", idx: 0 }],
|
|
829
|
+
},
|
|
830
|
+
]);
|
|
831
|
+
// The Python Pyodide test reads this file with the real metadata parser.
|
|
832
|
+
expect(createMarimoFile(apps[0])).toBe(
|
|
833
|
+
readFileSync(islandSnapshotPath("notebook-dependencies.txt"), "utf8"),
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("should carry dependencies from payloads into each generated notebook", () => {
|
|
838
|
+
for (const [appId, dependencies] of [
|
|
839
|
+
["app1", ["cowsay==6.1"]],
|
|
840
|
+
["app2", ["rich[jupyter]>=13"]],
|
|
841
|
+
] as const) {
|
|
842
|
+
container.appendChild(
|
|
843
|
+
createMockIslandElement({ appId, cellId: "cell-1" }),
|
|
844
|
+
);
|
|
845
|
+
appendPayload(container, {
|
|
846
|
+
schemaVersion: 1,
|
|
847
|
+
appId,
|
|
848
|
+
dependencies,
|
|
849
|
+
cells: [createPayloadCell()],
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const apps = parseMarimoIslandApps(container);
|
|
854
|
+
expect(apps.map((app) => [app.id, app.dependencies])).toEqual([
|
|
855
|
+
["app1", ["cowsay==6.1"]],
|
|
856
|
+
["app2", ["rich[jupyter]>=13"]],
|
|
857
|
+
]);
|
|
858
|
+
expect(createMarimoFile(apps[0])).toContain(
|
|
859
|
+
'# /// script\n# dependencies = ["cowsay==6.1"]\n# ///\n',
|
|
860
|
+
);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it.each([null, "cowsay", [123], ["cowsay", null]])(
|
|
864
|
+
"should ignore payloads with malformed dependencies: %j",
|
|
865
|
+
(dependencies) => {
|
|
866
|
+
const island = createMockIslandElement({
|
|
867
|
+
appId: "app1",
|
|
868
|
+
cellId: "cell-1",
|
|
869
|
+
code: "import cowsay",
|
|
870
|
+
innerHTML: "<div>dom</div>",
|
|
871
|
+
});
|
|
872
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
873
|
+
container.appendChild(island);
|
|
874
|
+
appendPayload(container, {
|
|
875
|
+
schemaVersion: 1,
|
|
876
|
+
appId: "app1",
|
|
877
|
+
dependencies,
|
|
878
|
+
cells: [createPayloadCell()],
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
expect(parseMarimoIslandApps(container)).toEqual([
|
|
882
|
+
{
|
|
883
|
+
id: "app1",
|
|
884
|
+
cells: [{ code: "import cowsay", output: "<div>dom</div>", idx: 0 }],
|
|
885
|
+
},
|
|
886
|
+
]);
|
|
887
|
+
},
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
it("should merge dependencies from payloads belonging to the same app", () => {
|
|
891
|
+
for (const [cellId, dependencies] of [
|
|
892
|
+
["cell-1", ["cowsay"]],
|
|
893
|
+
["cell-2", ["cowsay", "rich"]],
|
|
894
|
+
] as const) {
|
|
895
|
+
container.appendChild(createMockIslandElement({ appId: "app1", cellId }));
|
|
896
|
+
appendPayload(container, {
|
|
897
|
+
schemaVersion: 1,
|
|
898
|
+
appId: "app1",
|
|
899
|
+
dependencies,
|
|
900
|
+
cells: [createPayloadCell({ cellId })],
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
expect(
|
|
905
|
+
parseMarimoIslandApps(container).map((app) => app.dependencies),
|
|
906
|
+
).toEqual([["cowsay", "rich"]]);
|
|
907
|
+
});
|
|
908
|
+
|
|
778
909
|
it("should fall back to DOM islands for unsupported payload versions", () => {
|
|
779
910
|
const island = createMockIslandElement({
|
|
780
911
|
appId: "app1",
|
|
@@ -990,6 +1121,7 @@ describe("parseMarimoIslandApps", () => {
|
|
|
990
1121
|
outputHtml: "<div>payload only</div>",
|
|
991
1122
|
}),
|
|
992
1123
|
],
|
|
1124
|
+
dependencies: ["cowsay"],
|
|
993
1125
|
});
|
|
994
1126
|
|
|
995
1127
|
const result = parseMarimoIslandApps(container);
|
|
@@ -1076,6 +1208,7 @@ describe("parseMarimoIslandApps", () => {
|
|
|
1076
1208
|
reactive: false,
|
|
1077
1209
|
}),
|
|
1078
1210
|
],
|
|
1211
|
+
dependencies: ["cowsay"],
|
|
1079
1212
|
});
|
|
1080
1213
|
|
|
1081
1214
|
const result = parseMarimoIslandApps(container);
|