@marimo-team/islands 0.24.3-dev0 → 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/{ConnectedDataExplorerComponent-qIKbwYin.js → ConnectedDataExplorerComponent-C9OF_75w.js} +1 -1
- package/dist/{any-language-editor-CpTnK43-.js → any-language-editor-DmQ7dyCx.js} +1 -1
- package/dist/{chat-ui-DttQ-5Ut.js → chat-ui-BSwr409J.js} +1300 -1300
- package/dist/{common-BcmCsjI0.js → common-SEaoDOh7.js} +6484 -6276
- package/dist/{error-banner-WDhUzJyg.js → error-banner-CsXbEEjo.js} +160 -160
- package/dist/{formats-CJMVUfb3.js → formats-CslNOKy0.js} +1 -1
- package/dist/{html-to-image-BTYnN4ev.js → html-to-image-BtFMe_Nq.js} +3 -3
- package/dist/main.js +234 -229
- package/dist/{mermaid-BsHdQeQf.js → mermaid-Bi23Ibnw.js} +1 -1
- package/dist/{process-output-CsALk6tp.js → process-output-CPy0YKUe.js} +2 -2
- package/dist/{reveal-component-CMGZCw99.js → reveal-component-Cun9aLyZ.js} +2 -2
- package/dist/{toDate-D-Rx3ux5.js → toDate-CpXBeDq0.js} +3 -2
- package/dist/{vega-component-D-IXRnlx.js → vega-component-ovBXgp91.js} +3 -3
- 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/__tests__/mode.test.ts +8 -1
- 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
|
@@ -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(
|
|
@@ -49,7 +49,14 @@ describe("runDuringPresentMode", () => {
|
|
|
49
49
|
store.set(viewStateAtom, state);
|
|
50
50
|
|
|
51
51
|
const result = runDuringPresentMode(() => Promise.reject(error));
|
|
52
|
-
const rejection =
|
|
52
|
+
const rejection = result.then(
|
|
53
|
+
() => {
|
|
54
|
+
throw new Error("Expected runDuringPresentMode to reject");
|
|
55
|
+
},
|
|
56
|
+
(caughtError: unknown) => {
|
|
57
|
+
expect(caughtError).toBe(error);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
53
60
|
await vi.runAllTimersAsync();
|
|
54
61
|
await rejection;
|
|
55
62
|
|
|
@@ -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);
|
|
@@ -30,6 +30,8 @@ export interface MarimoIslandApp {
|
|
|
30
30
|
* Whether cells came from a supported JSON payload instead of DOM parsing.
|
|
31
31
|
*/
|
|
32
32
|
payloadBacked?: boolean;
|
|
33
|
+
/** Explicit notebook dependencies to install before starting the app. */
|
|
34
|
+
dependencies?: string[];
|
|
33
35
|
/**
|
|
34
36
|
* Cells in the app.
|
|
35
37
|
*/
|
|
@@ -62,6 +64,7 @@ interface MarimoIslandCell {
|
|
|
62
64
|
interface MarimoIslandPayload {
|
|
63
65
|
schemaVersion: 1;
|
|
64
66
|
appId: string;
|
|
67
|
+
dependencies?: string[];
|
|
65
68
|
cells: MarimoIslandPayloadCell[];
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -235,6 +238,12 @@ function parsePayloadBackedApps({
|
|
|
235
238
|
embed?.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
|
|
236
239
|
}
|
|
237
240
|
}
|
|
241
|
+
const app = apps.get(payload.appId);
|
|
242
|
+
if (app && payload.dependencies?.length) {
|
|
243
|
+
app.dependencies = [
|
|
244
|
+
...new Set([...(app.dependencies ?? []), ...payload.dependencies]),
|
|
245
|
+
];
|
|
246
|
+
}
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
// A supported payload is the runtime source for its app. Extra same-app DOM
|
|
@@ -371,6 +380,7 @@ export function parseIslandElement(
|
|
|
371
380
|
|
|
372
381
|
export function createMarimoFile(app: {
|
|
373
382
|
cells: { code: string; disabled?: boolean }[];
|
|
383
|
+
dependencies?: string[];
|
|
374
384
|
}): string {
|
|
375
385
|
const lines = [
|
|
376
386
|
"import marimo",
|
|
@@ -402,6 +412,13 @@ export function createMarimoFile(app: {
|
|
|
402
412
|
.join("\n"),
|
|
403
413
|
];
|
|
404
414
|
|
|
415
|
+
if (app.dependencies?.length) {
|
|
416
|
+
lines.unshift(
|
|
417
|
+
"# /// script",
|
|
418
|
+
`# dependencies = ${JSON.stringify(app.dependencies)}`,
|
|
419
|
+
"# ///",
|
|
420
|
+
);
|
|
421
|
+
}
|
|
405
422
|
return lines.join("\n");
|
|
406
423
|
}
|
|
407
424
|
|
|
@@ -507,6 +524,9 @@ function isMarimoIslandPayload(
|
|
|
507
524
|
return (
|
|
508
525
|
payload.schemaVersion === 1 &&
|
|
509
526
|
typeof payload.appId === "string" &&
|
|
527
|
+
(payload.dependencies === undefined ||
|
|
528
|
+
(Array.isArray(payload.dependencies) &&
|
|
529
|
+
payload.dependencies.every((dep) => typeof dep === "string"))) &&
|
|
510
530
|
Array.isArray(payload.cells) &&
|
|
511
531
|
payload.cells.every(isMarimoIslandPayloadCell)
|
|
512
532
|
);
|
|
@@ -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);
|
|
@@ -276,6 +276,17 @@ describe("RuntimeManager", () => {
|
|
|
276
276
|
});
|
|
277
277
|
|
|
278
278
|
describe("getTerminalWsURL", () => {
|
|
279
|
+
it("should include dimensions in the terminal handshake", () => {
|
|
280
|
+
const runtime = new RuntimeManager(mockConfig);
|
|
281
|
+
const url = runtime.getTerminalWsURL({ rows: 30, cols: 140 });
|
|
282
|
+
|
|
283
|
+
expect(Object.fromEntries(url.searchParams)).toEqual({
|
|
284
|
+
access_token: "test-token",
|
|
285
|
+
rows: "30",
|
|
286
|
+
cols: "140",
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
279
290
|
it("should return terminal WebSocket URL", () => {
|
|
280
291
|
const runtime = new RuntimeManager(mockConfig);
|
|
281
292
|
const url = runtime.getTerminalWsURL();
|
|
@@ -183,8 +183,13 @@ export class RuntimeManager {
|
|
|
183
183
|
/**
|
|
184
184
|
* The WebSocket URL of the terminal.
|
|
185
185
|
*/
|
|
186
|
-
getTerminalWsURL(): URL {
|
|
187
|
-
|
|
186
|
+
getTerminalWsURL(size?: { rows: number; cols: number }): URL {
|
|
187
|
+
const url = this.formatWsURL("/terminal/ws");
|
|
188
|
+
if (size) {
|
|
189
|
+
url.searchParams.set("rows", String(size.rows));
|
|
190
|
+
url.searchParams.set("cols", String(size.cols));
|
|
191
|
+
}
|
|
192
|
+
return url;
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
/**
|