@marimo-team/islands 0.24.3-dev81 → 0.24.3-dev83
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/{ErrorBoundary-CCi-VkF7.js → ErrorBoundary-Uf_LvjvY.js} +1 -1
- package/dist/{cells-BIfOnGBK.js → cells-DYIkSxs6.js} +4 -0
- package/dist/{chat-ui-LWkjhObe.js → chat-ui-BXj3uYjy.js} +4 -4
- package/dist/{code-visibility-Bsfqbe2z.js → code-visibility-DxgQWu1j.js} +2 -2
- package/dist/{common-BnKC1tWB.js → common-ByAtncLr.js} +3 -3
- package/dist/{data-grid-overlay-editor-CJnlWykO.js → data-grid-overlay-editor-DL_blFyL.js} +1 -1
- package/dist/{error-entries-CR0oClSL.js → error-entries-C0NsUqXl.js} +1 -1
- package/dist/{feedback-button-MWISfIIf.js → feedback-button-AZcBqfoV.js} +4 -4
- package/dist/{glide-data-editor-dcmP8mKS.js → glide-data-editor-BJdZncJk.js} +3 -3
- package/dist/{globals-fCMBgbAD.js → globals-yWarpyN4.js} +1 -1
- package/dist/{html-to-image-_i03yR4q.js → html-to-image-CeYYvQNb.js} +1 -1
- package/dist/main.js +12 -11
- package/dist/{number-overlay-editor-DhNE6pBe.js → number-overlay-editor-BatBVYxr.js} +1 -1
- package/dist/{process-output-D3zQyTAS.js → process-output-BCTjlyuH.js} +2 -2
- package/dist/{reveal-component-Cdcgngkc.js → reveal-component-D02jcpiJ.js} +3 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +186 -0
- package/src/components/editor/alerts/__tests__/startup-progress.test.tsx +185 -0
- package/src/components/editor/alerts/connection-notice.tsx +100 -54
- package/src/components/editor/alerts/startup-output.tsx +173 -0
- package/src/components/editor/alerts/startup-progress.tsx +204 -0
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +53 -4
- package/src/components/editor/chrome/panels/__tests__/sandbox-details.test.tsx +358 -0
- package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +5 -2
- package/src/components/editor/chrome/panels/packages-panel.tsx +32 -21
- package/src/components/editor/chrome/panels/sandbox-details-state.ts +38 -0
- package/src/components/editor/chrome/panels/sandbox-panel.tsx +188 -131
- package/src/components/editor/chrome/panels/sandbox-toggle.tsx +52 -0
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +7 -0
- package/src/components/editor/package-alert.tsx +112 -66
- package/src/core/alerts/__tests__/environment.test.ts +183 -0
- package/src/core/alerts/__tests__/state.test.tsx +240 -0
- package/src/core/alerts/environment.ts +57 -0
- package/src/core/alerts/state.ts +155 -99
- package/src/core/islands/bootstrap.ts +2 -1
- package/src/core/kernel/messages.ts +2 -2
- package/src/core/network/connection-notice.ts +45 -12
- package/src/core/network/connection.ts +19 -0
- package/src/core/network/useConnectionNotice.ts +53 -0
- package/src/core/packages/__tests__/useSandboxController.test.tsx +86 -5
- package/src/core/packages/sandbox-state.ts +56 -4
- package/src/core/packages/useSandboxController.ts +27 -7
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +199 -8
- package/src/core/websocket/types.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +31 -19
- package/src/stories/sandbox.stories.tsx +230 -50
|
@@ -2,40 +2,69 @@
|
|
|
2
2
|
import { atom } from "jotai";
|
|
3
3
|
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
4
4
|
import { sandboxAtom, sandboxSyncAtom } from "@/core/packages/sandbox-state";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type ConnectionPhase,
|
|
7
|
+
WebSocketClosedReason,
|
|
8
|
+
WebSocketState,
|
|
9
|
+
} from "@/core/websocket/types";
|
|
6
10
|
import { connectionAtom } from "./connection";
|
|
7
11
|
|
|
8
|
-
export
|
|
12
|
+
export interface ConnectionNotice {
|
|
13
|
+
kind: "startup" | "sync" | "connection";
|
|
14
|
+
phase?: ConnectionPhase;
|
|
15
|
+
sandbox: boolean;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
pending: boolean;
|
|
19
|
+
error: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const connectionNoticeAtom = atom<ConnectionNotice | null>((get) => {
|
|
9
23
|
const connection = get(connectionAtom);
|
|
10
24
|
const backend = get(sandboxAtom)?.backend;
|
|
11
25
|
const sandbox = Boolean(backend);
|
|
12
26
|
const sync = get(sandboxSyncAtom);
|
|
27
|
+
const syncing = sync.kind === "running";
|
|
28
|
+
const restartRequired = sync.kind === "restart-required";
|
|
29
|
+
const syncError =
|
|
30
|
+
sync.kind === "failed"
|
|
31
|
+
? sync.error
|
|
32
|
+
: sync.kind === "cancelled"
|
|
33
|
+
? "Sandbox sync was interrupted."
|
|
34
|
+
: restartRequired
|
|
35
|
+
? sync.reason
|
|
36
|
+
: null;
|
|
13
37
|
if (
|
|
14
38
|
sandbox &&
|
|
15
|
-
(
|
|
39
|
+
(syncing || syncError !== null) &&
|
|
16
40
|
connection.state === WebSocketState.OPEN
|
|
17
41
|
) {
|
|
18
42
|
return {
|
|
43
|
+
kind: "sync",
|
|
19
44
|
sandbox,
|
|
20
|
-
title:
|
|
21
|
-
|
|
45
|
+
title: syncing
|
|
46
|
+
? "Syncing sandbox…"
|
|
47
|
+
: restartRequired
|
|
48
|
+
? "Sandbox restart required"
|
|
49
|
+
: "Sandbox sync failed",
|
|
50
|
+
description: syncing
|
|
22
51
|
? "Applying the saved manifest to this notebook’s environment."
|
|
23
|
-
:
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
: restartRequired
|
|
53
|
+
? "Restart the kernel to use the updated environment."
|
|
54
|
+
: "Review the error details, then try again.",
|
|
55
|
+
pending: syncing,
|
|
56
|
+
error: syncError,
|
|
26
57
|
};
|
|
27
58
|
}
|
|
28
59
|
if (connection.state === WebSocketState.CONNECTING) {
|
|
29
60
|
const messages = {
|
|
30
61
|
"preparing-environment": {
|
|
31
|
-
title:
|
|
32
|
-
? `Preparing ${backend} sandbox…`
|
|
33
|
-
: "Preparing environment…",
|
|
62
|
+
title: "Preparing environment…",
|
|
34
63
|
description:
|
|
35
64
|
"Getting this notebook’s Python environment ready. The first start can take a few minutes.",
|
|
36
65
|
},
|
|
37
66
|
"starting-kernel": {
|
|
38
|
-
title: "Starting
|
|
67
|
+
title: "Starting kernel…",
|
|
39
68
|
description:
|
|
40
69
|
"The environment is prepared. Waiting for the Python kernel to connect to this notebook.",
|
|
41
70
|
},
|
|
@@ -46,6 +75,8 @@ export const connectionNoticeAtom = atom((get) => {
|
|
|
46
75
|
},
|
|
47
76
|
};
|
|
48
77
|
return {
|
|
78
|
+
kind: connection.phase === "reconnecting" ? "connection" : "startup",
|
|
79
|
+
phase: connection.phase,
|
|
49
80
|
sandbox,
|
|
50
81
|
...(connection.phase
|
|
51
82
|
? messages[connection.phase]
|
|
@@ -63,6 +94,8 @@ export const connectionNoticeAtom = atom((get) => {
|
|
|
63
94
|
const environmentFailed =
|
|
64
95
|
startupFailed && connection.phase === "preparing-environment";
|
|
65
96
|
return {
|
|
97
|
+
kind: startupFailed ? "startup" : "connection",
|
|
98
|
+
phase: connection.phase,
|
|
66
99
|
sandbox,
|
|
67
100
|
title: environmentFailed
|
|
68
101
|
? "Sandbox setup failed"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { atom } from "jotai";
|
|
3
|
+
import { atomWithReducer } from "jotai/utils";
|
|
4
|
+
import type { NotificationMessageData } from "../kernel/messages";
|
|
3
5
|
import { isNotebookPage } from "../mode";
|
|
4
6
|
import { waitFor } from "../state/jotai";
|
|
5
7
|
import { type ConnectionStatus, WebSocketState } from "../websocket/types";
|
|
@@ -12,6 +14,23 @@ export const connectionAtom = atom<ConnectionStatus>({
|
|
|
12
14
|
state: WebSocketState.NOT_STARTED,
|
|
13
15
|
});
|
|
14
16
|
|
|
17
|
+
type StartupProgress = NotificationMessageData<"startup-progress">;
|
|
18
|
+
|
|
19
|
+
export const startupProgressAtom = atomWithReducer<
|
|
20
|
+
Pick<StartupProgress, "phase" | "logs"> | null,
|
|
21
|
+
StartupProgress | null
|
|
22
|
+
>(null, (previous, update: StartupProgress | null) =>
|
|
23
|
+
update === null
|
|
24
|
+
? null
|
|
25
|
+
: {
|
|
26
|
+
phase: update.phase,
|
|
27
|
+
logs:
|
|
28
|
+
update.log_mode === "append" && previous?.phase === update.phase
|
|
29
|
+
? previous.logs + update.logs
|
|
30
|
+
: update.logs,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
15
34
|
export function waitForConnectionOpen() {
|
|
16
35
|
return waitFor(connectionAtom, (value) => {
|
|
17
36
|
return value.state === WebSocketState.OPEN;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { useAtomValue } from "jotai";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useDelayElapsed } from "@/hooks/useDelayElapsed";
|
|
5
|
+
import { isConnectedAtom } from "./connection";
|
|
6
|
+
import {
|
|
7
|
+
type ConnectionNotice,
|
|
8
|
+
connectionNoticeAtom,
|
|
9
|
+
} from "./connection-notice";
|
|
10
|
+
|
|
11
|
+
export type DisplayConnectionNotice = ConnectionNotice & { ready: boolean };
|
|
12
|
+
|
|
13
|
+
/** Briefly acknowledge completion, but only for setup the user actually saw. */
|
|
14
|
+
export function useConnectionNotice(
|
|
15
|
+
delayMs = 500,
|
|
16
|
+
): DisplayConnectionNotice | null {
|
|
17
|
+
const notice = useAtomValue(connectionNoticeAtom);
|
|
18
|
+
const connected = useAtomValue(isConnectedAtom);
|
|
19
|
+
const elapsed = useDelayElapsed(notice?.pending ? delayMs : 0);
|
|
20
|
+
const [previous, setPrevious] = useState<ConnectionNotice | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (notice || !connected) {
|
|
24
|
+
setPrevious(elapsed ? notice : null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const timeout = setTimeout(() => setPrevious(null), 1500);
|
|
28
|
+
return () => clearTimeout(timeout);
|
|
29
|
+
}, [notice, connected, elapsed]);
|
|
30
|
+
|
|
31
|
+
if (notice) {
|
|
32
|
+
return elapsed ? { ...notice, ready: false } : null;
|
|
33
|
+
}
|
|
34
|
+
if (
|
|
35
|
+
connected &&
|
|
36
|
+
previous?.pending &&
|
|
37
|
+
previous.sandbox &&
|
|
38
|
+
previous.kind !== "connection"
|
|
39
|
+
) {
|
|
40
|
+
return {
|
|
41
|
+
...previous,
|
|
42
|
+
pending: false,
|
|
43
|
+
ready: true,
|
|
44
|
+
title:
|
|
45
|
+
previous.kind === "sync"
|
|
46
|
+
? "Environment synced"
|
|
47
|
+
: "Your notebook is ready",
|
|
48
|
+
description: "The notebook is ready to run.",
|
|
49
|
+
error: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -3,6 +3,11 @@ import { act, renderHook, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { Provider } from "jotai";
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { MockRequestClient } from "@/__mocks__/requests";
|
|
6
|
+
import {
|
|
7
|
+
emptyEnvironmentState,
|
|
8
|
+
type EnvironmentOperation,
|
|
9
|
+
} from "@/core/alerts/environment";
|
|
10
|
+
import { alertAtom } from "@/core/alerts/state";
|
|
6
11
|
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
7
12
|
import { connectionAtom } from "@/core/network/connection";
|
|
8
13
|
import { requestClientAtom } from "@/core/network/requests";
|
|
@@ -25,6 +30,8 @@ import { useSandboxController } from "../useSandboxController";
|
|
|
25
30
|
vi.mock("../toast-components", () => ({
|
|
26
31
|
showPackageRestartToast: vi.fn(),
|
|
27
32
|
showSandboxSyncToast: vi.fn(),
|
|
33
|
+
RESTART_REQUIRED_DESCRIPTION:
|
|
34
|
+
"Restart the kernel to use the updated environment.",
|
|
28
35
|
}));
|
|
29
36
|
|
|
30
37
|
const manifest = 'dependencies = ["numpy"]';
|
|
@@ -35,8 +42,15 @@ const sandbox: SandboxResponse = {
|
|
|
35
42
|
};
|
|
36
43
|
|
|
37
44
|
beforeEach(() => {
|
|
45
|
+
store.set(alertAtom, (value) => ({
|
|
46
|
+
...value,
|
|
47
|
+
environments: {
|
|
48
|
+
kernel: emptyEnvironmentState(),
|
|
49
|
+
server: emptyEnvironmentState(),
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
38
52
|
store.set(sandboxAtom, null);
|
|
39
|
-
store.set(sandboxSyncAtom, {
|
|
53
|
+
store.set(sandboxSyncAtom, { kind: "succeeded" });
|
|
40
54
|
store.set(sandboxActionsAtom, null);
|
|
41
55
|
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
42
56
|
store.set(kernelStartupErrorAtom, null);
|
|
@@ -65,6 +79,73 @@ function setup() {
|
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
describe("useSandboxController", () => {
|
|
82
|
+
it("preserves a restart-required response instead of reporting a failed sync", async () => {
|
|
83
|
+
const { client, result } = setup();
|
|
84
|
+
vi.mocked(client.syncSandbox).mockResolvedValue({
|
|
85
|
+
success: false,
|
|
86
|
+
restartRequired: true,
|
|
87
|
+
error: "Python version changed",
|
|
88
|
+
});
|
|
89
|
+
await act(async () => {
|
|
90
|
+
expect(await store.get(sandboxActionsAtom)?.sync()).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
expect(store.get(sandboxSyncAtom)).toEqual({
|
|
93
|
+
kind: "restart-required",
|
|
94
|
+
reason: "Python version changed",
|
|
95
|
+
});
|
|
96
|
+
expect(result.current.pending).toBe(false);
|
|
97
|
+
expect(result.current.diagnostic).toBe("Python version changed");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("restores sync progress and lets a retry replace the restored failure", async () => {
|
|
101
|
+
const operation: EnvironmentOperation = {
|
|
102
|
+
operation_id: "sync",
|
|
103
|
+
action: "sync",
|
|
104
|
+
source: "kernel",
|
|
105
|
+
status: { kind: "running" },
|
|
106
|
+
packages: {},
|
|
107
|
+
logs: {},
|
|
108
|
+
};
|
|
109
|
+
const restore = (status: EnvironmentOperation["status"]) =>
|
|
110
|
+
store.set(alertAtom, (value) => ({
|
|
111
|
+
...value,
|
|
112
|
+
environments: {
|
|
113
|
+
...value.environments,
|
|
114
|
+
kernel: {
|
|
115
|
+
restart_required: false,
|
|
116
|
+
operations: [{ ...operation, status }],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
}));
|
|
120
|
+
restore({ kind: "running" });
|
|
121
|
+
const { client, result } = setup();
|
|
122
|
+
const actions = store.get(sandboxActionsAtom)!;
|
|
123
|
+
expect(result.current.pending).toBe(true);
|
|
124
|
+
await act(async () => {
|
|
125
|
+
expect(await actions.sync()).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
expect(client.syncSandbox).not.toHaveBeenCalled();
|
|
128
|
+
act(() => restore({ kind: "failed", error: "Dependency conflict" }));
|
|
129
|
+
expect(result.current.diagnostic).toBe("Dependency conflict");
|
|
130
|
+
|
|
131
|
+
const { promise, resolve } = Promise.withResolvers<SyncSandboxResponse>();
|
|
132
|
+
vi.mocked(client.syncSandbox).mockReturnValue(promise);
|
|
133
|
+
let pending: Promise<boolean>;
|
|
134
|
+
act(() => {
|
|
135
|
+
pending = actions.sync();
|
|
136
|
+
});
|
|
137
|
+
expect(store.get(sandboxSyncAtom)).toEqual({ kind: "running" });
|
|
138
|
+
act(() => restore({ kind: "running" }));
|
|
139
|
+
await act(async () => {
|
|
140
|
+
resolve({ success: true, reconnect: false });
|
|
141
|
+
expect(await pending).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
// The response can arrive before the final websocket notification.
|
|
144
|
+
expect(store.get(sandboxSyncAtom)).toEqual({ kind: "running" });
|
|
145
|
+
act(() => restore({ kind: "succeeded" }));
|
|
146
|
+
expect(store.get(sandboxSyncAtom)).toEqual({ kind: "succeeded" });
|
|
147
|
+
});
|
|
148
|
+
|
|
68
149
|
it("keeps a conflicting draft across closing and only discards it on explicit reload", async () => {
|
|
69
150
|
const { result, client } = setup();
|
|
70
151
|
vi.mocked(client.updateManifest).mockRejectedValue(
|
|
@@ -116,7 +197,7 @@ describe("useSandboxController", () => {
|
|
|
116
197
|
act(() => {
|
|
117
198
|
pending = actions.sync();
|
|
118
199
|
});
|
|
119
|
-
expect(store.get(sandboxSyncAtom)).toEqual({
|
|
200
|
+
expect(store.get(sandboxSyncAtom)).toEqual({ kind: "running" });
|
|
120
201
|
await act(async () => {
|
|
121
202
|
expect(await actions.sync()).toBe(false);
|
|
122
203
|
});
|
|
@@ -125,7 +206,7 @@ describe("useSandboxController", () => {
|
|
|
125
206
|
resolve({ success: true, reconnect: false });
|
|
126
207
|
expect(await pending).toBe(true);
|
|
127
208
|
});
|
|
128
|
-
expect(store.get(sandboxSyncAtom)).toEqual({
|
|
209
|
+
expect(store.get(sandboxSyncAtom)).toEqual({ kind: "succeeded" });
|
|
129
210
|
expect(store.get(packageDataVersionAtom)).toBe(1);
|
|
130
211
|
expect(reconnect).not.toHaveBeenCalled();
|
|
131
212
|
});
|
|
@@ -138,13 +219,13 @@ describe("useSandboxController", () => {
|
|
|
138
219
|
expect(await actions.sync()).toBe(false);
|
|
139
220
|
});
|
|
140
221
|
expect(store.get(sandboxSyncAtom)).toEqual({
|
|
141
|
-
|
|
222
|
+
kind: "failed",
|
|
142
223
|
error: "offline",
|
|
143
224
|
});
|
|
144
225
|
await act(async () => {
|
|
145
226
|
expect(await actions.sync()).toBe(true);
|
|
146
227
|
});
|
|
147
|
-
expect(store.get(sandboxSyncAtom)).toEqual({
|
|
228
|
+
expect(store.get(sandboxSyncAtom)).toEqual({ kind: "succeeded" });
|
|
148
229
|
});
|
|
149
230
|
|
|
150
231
|
it("ignores stale metadata after a filename change and unregisters actions on unmount", async () => {
|
|
@@ -1,12 +1,64 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { atom } from "jotai";
|
|
3
|
+
import type { EnvironmentOperation } from "@/core/alerts/environment";
|
|
4
|
+
import { alertAtom } from "@/core/alerts/state";
|
|
3
5
|
import type { SandboxResponse } from "@/core/network/types";
|
|
4
6
|
|
|
5
7
|
export const sandboxAtom = atom<SandboxResponse | null>(null);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
export const preparationAtom = atom((get) =>
|
|
10
|
+
get(alertAtom).environments.kernel.operations.findLast(
|
|
11
|
+
(operation) => operation.action === "prepare",
|
|
12
|
+
),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
type SandboxSyncState = EnvironmentOperation["status"] | { kind: "idle" };
|
|
16
|
+
|
|
17
|
+
export const sandboxSyncOperationAtom = atom((get) => {
|
|
18
|
+
const operations = get(alertAtom).environments.kernel.operations;
|
|
19
|
+
return (
|
|
20
|
+
operations.findLast(
|
|
21
|
+
(operation) =>
|
|
22
|
+
operation.action === "sync" && operation.status.kind === "running",
|
|
23
|
+
) ?? operations.findLast((operation) => operation.action === "sync")
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const sandboxSyncRequestAtom = atom<{
|
|
28
|
+
state: SandboxSyncState;
|
|
29
|
+
operation: EnvironmentOperation | undefined;
|
|
30
|
+
} | null>(null);
|
|
31
|
+
|
|
32
|
+
export const sandboxSyncAtom = atom(
|
|
33
|
+
(get): SandboxSyncState => {
|
|
34
|
+
const operation = get(sandboxSyncOperationAtom);
|
|
35
|
+
const request = get(sandboxSyncRequestAtom);
|
|
36
|
+
// Local request status lasts until newer progress arrives from the server.
|
|
37
|
+
const state: SandboxSyncState =
|
|
38
|
+
request &&
|
|
39
|
+
request.operation === operation &&
|
|
40
|
+
operation?.status.kind !== "running"
|
|
41
|
+
? request.state
|
|
42
|
+
: (operation?.status ?? { kind: "idle" });
|
|
43
|
+
// Later package operations can replace the sync result, but only a new
|
|
44
|
+
// environment snapshot can clear an outstanding restart.
|
|
45
|
+
if (
|
|
46
|
+
get(alertAtom).environments.kernel.restart_required &&
|
|
47
|
+
(state.kind === "idle" || state.kind === "succeeded")
|
|
48
|
+
) {
|
|
49
|
+
return {
|
|
50
|
+
kind: "restart-required",
|
|
51
|
+
reason:
|
|
52
|
+
"Dependency changes are saved; restart the kernel to apply them.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return state;
|
|
56
|
+
},
|
|
57
|
+
(get, set, state: SandboxSyncState) => {
|
|
58
|
+
set(sandboxSyncRequestAtom, {
|
|
59
|
+
state,
|
|
60
|
+
operation: get(sandboxSyncOperationAtom),
|
|
61
|
+
});
|
|
10
62
|
},
|
|
11
63
|
);
|
|
12
64
|
export const sandboxActionsAtom = atom<{
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
sandboxSyncAtom,
|
|
18
18
|
} from "./sandbox-state";
|
|
19
19
|
import {
|
|
20
|
+
RESTART_REQUIRED_DESCRIPTION,
|
|
20
21
|
showPackageRestartToast,
|
|
21
22
|
showSandboxSyncToast,
|
|
22
23
|
} from "./toast-components";
|
|
@@ -63,15 +64,24 @@ export function useSandboxController(onReconnect: () => Promise<void>) {
|
|
|
63
64
|
}, [requests, filename, connection.state, setSandbox]);
|
|
64
65
|
|
|
65
66
|
const sync = useEvent(async () => {
|
|
66
|
-
if (
|
|
67
|
+
if (
|
|
68
|
+
inFlight.current ||
|
|
69
|
+
operation.kind === "running" ||
|
|
70
|
+
connection.state === WebSocketState.CONNECTING
|
|
71
|
+
) {
|
|
67
72
|
return false;
|
|
68
73
|
}
|
|
69
74
|
inFlight.current = true;
|
|
70
|
-
setOperation(
|
|
75
|
+
setOperation({ kind: "running" });
|
|
71
76
|
try {
|
|
72
77
|
const result = await requests.syncSandbox({ fileKey: filename });
|
|
73
78
|
if (result.restartRequired) {
|
|
79
|
+
setOperation({
|
|
80
|
+
kind: "restart-required",
|
|
81
|
+
reason: result.error ?? RESTART_REQUIRED_DESCRIPTION,
|
|
82
|
+
});
|
|
74
83
|
showPackageRestartToast();
|
|
84
|
+
return false;
|
|
75
85
|
}
|
|
76
86
|
if (!result.success) {
|
|
77
87
|
throw new Error(result.error ?? "Sandbox sync failed.");
|
|
@@ -85,6 +95,7 @@ export function useSandboxController(onReconnect: () => Promise<void>) {
|
|
|
85
95
|
value.state === WebSocketState.CLOSED,
|
|
86
96
|
);
|
|
87
97
|
if (status.state === WebSocketState.CLOSED) {
|
|
98
|
+
setOperation({ kind: "idle" });
|
|
88
99
|
return false;
|
|
89
100
|
}
|
|
90
101
|
}
|
|
@@ -92,13 +103,13 @@ export function useSandboxController(onReconnect: () => Promise<void>) {
|
|
|
92
103
|
invalidatePackageData();
|
|
93
104
|
}
|
|
94
105
|
showSandboxSyncToast();
|
|
106
|
+
setOperation({ kind: "succeeded" });
|
|
95
107
|
return true;
|
|
96
108
|
} catch (cause) {
|
|
97
|
-
setOperation(
|
|
109
|
+
setOperation({ kind: "failed", error: prettyError(cause) });
|
|
98
110
|
return false;
|
|
99
111
|
} finally {
|
|
100
112
|
inFlight.current = false;
|
|
101
|
-
setOperation((value) => ({ ...value, pending: false }));
|
|
102
113
|
}
|
|
103
114
|
});
|
|
104
115
|
|
|
@@ -135,7 +146,7 @@ export function useSandboxController(onReconnect: () => Promise<void>) {
|
|
|
135
146
|
}, [sync, editManifest, setActions]);
|
|
136
147
|
|
|
137
148
|
const saveAndSync = async () => {
|
|
138
|
-
if (!document || loading || saving || operation.
|
|
149
|
+
if (!document || loading || saving || operation.kind === "running") {
|
|
139
150
|
return;
|
|
140
151
|
}
|
|
141
152
|
setSaving(true);
|
|
@@ -165,9 +176,18 @@ export function useSandboxController(onReconnect: () => Promise<void>) {
|
|
|
165
176
|
const pending =
|
|
166
177
|
saving ||
|
|
167
178
|
loading ||
|
|
168
|
-
operation.
|
|
179
|
+
operation.kind === "running" ||
|
|
169
180
|
connection.state === WebSocketState.CONNECTING;
|
|
170
|
-
const diagnostic =
|
|
181
|
+
const diagnostic =
|
|
182
|
+
error ??
|
|
183
|
+
(operation.kind === "failed"
|
|
184
|
+
? operation.error
|
|
185
|
+
: operation.kind === "restart-required"
|
|
186
|
+
? operation.reason
|
|
187
|
+
: operation.kind === "cancelled"
|
|
188
|
+
? "Sandbox sync was interrupted."
|
|
189
|
+
: null) ??
|
|
190
|
+
startupError;
|
|
171
191
|
return {
|
|
172
192
|
filename,
|
|
173
193
|
open,
|