@marimo-team/islands 0.24.3-dev82 → 0.24.3-dev87
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-CegtSjfd.js → ErrorBoundary-Bc52FkKN.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-Blq7oGrC.js → code-visibility-CI3ySb6F.js} +2 -2
- package/dist/{common-BQZDWy8O.js → common-BOJoCp6P.js} +3 -3
- package/dist/{data-grid-overlay-editor-DsU0Zpgt.js → data-grid-overlay-editor-BsT4_auq.js} +1 -1
- package/dist/{error-entries-CR0oClSL.js → error-entries-C0NsUqXl.js} +1 -1
- package/dist/{feedback-button-pGaDcCJS.js → feedback-button-B7KS_3RP.js} +4 -4
- package/dist/{glide-data-editor-DtJQCRsf.js → glide-data-editor-DtckYC_c.js} +3 -3
- package/dist/{globals-DBAC4Ihd.js → globals-CruHzAUQ.js} +1 -1
- package/dist/{html-to-image-_i03yR4q.js → html-to-image-CeYYvQNb.js} +1 -1
- package/dist/main.js +25 -14
- package/dist/{number-overlay-editor-9M7FnzxL.js → number-overlay-editor-CV1RN5Cg.js} +1 -1
- package/dist/{process-output-D3zQyTAS.js → process-output-BCTjlyuH.js} +2 -2
- package/dist/{reveal-component-ovaRyk0R.js → reveal-component-C47L1uNq.js} +3 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/__tests__/fixtures/pair-preview.ts +1 -1
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +6 -6
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +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/plugins/impl/FileBrowserPlugin.tsx +10 -4
- package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +50 -1
- package/src/stories/sandbox.stories.tsx +230 -50
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
3
|
+
import { createStore, Provider } from "jotai";
|
|
4
|
+
import { expect, it, vi } from "vitest";
|
|
5
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
6
|
+
import { ModalProvider } from "@/components/modal/ImperativeModal";
|
|
7
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
|
+
import { alertAtom } from "@/core/alerts/state";
|
|
9
|
+
import type { EnvironmentOperation } from "@/core/alerts/environment";
|
|
10
|
+
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
11
|
+
import { connectionAtom, startupProgressAtom } from "@/core/network/connection";
|
|
12
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
13
|
+
import { sandboxAtom } from "@/core/packages/sandbox-state";
|
|
14
|
+
import { WebSocketClosedReason, WebSocketState } from "@/core/websocket/types";
|
|
15
|
+
import PackagesPanel from "../packages-panel";
|
|
16
|
+
import { PanelSectionProvider } from "../panel-context";
|
|
17
|
+
import { SandboxToggle } from "../sandbox-toggle";
|
|
18
|
+
|
|
19
|
+
vi.mock("@/utils/reload-safe", () => ({ reloadSafe: vi.fn() }));
|
|
20
|
+
|
|
21
|
+
const preparation: EnvironmentOperation = {
|
|
22
|
+
operation_id: "prepare-1",
|
|
23
|
+
action: "prepare",
|
|
24
|
+
source: "kernel",
|
|
25
|
+
status: { kind: "succeeded" },
|
|
26
|
+
packages: {},
|
|
27
|
+
logs: {
|
|
28
|
+
environment:
|
|
29
|
+
"Using Python\nResolved dependencies\nDownloaded numpy\nInstalled numpy\n",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function mount({
|
|
34
|
+
preparing = false,
|
|
35
|
+
backend = "uv",
|
|
36
|
+
}: { preparing?: boolean; backend?: "uv" | "pixi" | null } = {}) {
|
|
37
|
+
const store = createStore();
|
|
38
|
+
store.set(sandboxAtom, {
|
|
39
|
+
backend,
|
|
40
|
+
manifest: "dependencies = []",
|
|
41
|
+
filename: "notebook.py",
|
|
42
|
+
});
|
|
43
|
+
store.set(
|
|
44
|
+
connectionAtom,
|
|
45
|
+
preparing
|
|
46
|
+
? { state: WebSocketState.CONNECTING, phase: "preparing-environment" }
|
|
47
|
+
: { state: WebSocketState.OPEN },
|
|
48
|
+
);
|
|
49
|
+
store.set(alertAtom, (state) => ({
|
|
50
|
+
...state,
|
|
51
|
+
environments: {
|
|
52
|
+
...state.environments,
|
|
53
|
+
kernel: {
|
|
54
|
+
restart_required: false,
|
|
55
|
+
operations: backend ? [preparation] : [],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
if (backend) {
|
|
60
|
+
store.set(startupProgressAtom, {
|
|
61
|
+
phase: preparing ? "preparing-environment" : "starting-kernel",
|
|
62
|
+
logs: preparing
|
|
63
|
+
? ""
|
|
64
|
+
: "Loading runtime\nLaunching kernel\nKernel connected\n",
|
|
65
|
+
log_mode: "replace",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const client = MockRequestClient.create({
|
|
69
|
+
sendRestart: vi.fn(async () => null),
|
|
70
|
+
getDependencyTree: vi.fn(async () => ({
|
|
71
|
+
context: backend
|
|
72
|
+
? { kind: "sandbox" as const, backend }
|
|
73
|
+
: { kind: "package-manager" as const, name: "pip" },
|
|
74
|
+
tree: {
|
|
75
|
+
name: "<root>",
|
|
76
|
+
version: null,
|
|
77
|
+
tags: [],
|
|
78
|
+
dependencies: [
|
|
79
|
+
{ name: "numpy", version: "2.0.0", tags: [], dependencies: [] },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
})),
|
|
83
|
+
getPackageList: vi.fn(async () => ({
|
|
84
|
+
packages: [{ name: "numpy", version: "2.0.0" }],
|
|
85
|
+
})),
|
|
86
|
+
});
|
|
87
|
+
store.set(requestClientAtom, client);
|
|
88
|
+
const { unmount } = render(
|
|
89
|
+
<Provider store={store}>
|
|
90
|
+
<TooltipProvider>
|
|
91
|
+
<ModalProvider>
|
|
92
|
+
<SandboxToggle section="sidebar" />
|
|
93
|
+
<PanelSectionProvider value="sidebar">
|
|
94
|
+
<PackagesPanel />
|
|
95
|
+
</PanelSectionProvider>
|
|
96
|
+
</ModalProvider>
|
|
97
|
+
</TooltipProvider>
|
|
98
|
+
</Provider>,
|
|
99
|
+
);
|
|
100
|
+
return { store, client, unmount };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
it("opens restored preparation and kernel logs without reopening completed startup on refresh", async () => {
|
|
104
|
+
mount();
|
|
105
|
+
await screen.findByText("numpy");
|
|
106
|
+
const toggle = screen.getByRole("button", { name: "uv sandbox" });
|
|
107
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
108
|
+
expect(
|
|
109
|
+
screen.queryByRole("region", { name: "Sandbox details" }),
|
|
110
|
+
).not.toBeInTheDocument();
|
|
111
|
+
fireEvent.click(toggle);
|
|
112
|
+
const details = within(
|
|
113
|
+
screen.getByRole("region", { name: "Sandbox details" }),
|
|
114
|
+
);
|
|
115
|
+
expect(details.getByText("Environment prepared")).toBeVisible();
|
|
116
|
+
expect(details.getByText("Kernel started")).toBeVisible();
|
|
117
|
+
fireEvent.click(
|
|
118
|
+
details.getByRole("button", {
|
|
119
|
+
name: "Expand environment preparation output",
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
fireEvent.click(
|
|
123
|
+
details.getByRole("button", { name: "Expand kernel startup output" }),
|
|
124
|
+
);
|
|
125
|
+
expect(
|
|
126
|
+
details.getByLabelText("environment preparation output").textContent,
|
|
127
|
+
).toBe(preparation.logs.environment);
|
|
128
|
+
expect(details.getByLabelText("kernel startup output")).toHaveTextContent(
|
|
129
|
+
"Loading runtime",
|
|
130
|
+
);
|
|
131
|
+
fireEvent.click(toggle);
|
|
132
|
+
fireEvent.click(toggle);
|
|
133
|
+
expect(
|
|
134
|
+
details.getByLabelText("environment preparation output"),
|
|
135
|
+
).toBeVisible();
|
|
136
|
+
expect(details.getByLabelText("kernel startup output")).toBeVisible();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("restores a restart-required sync with the kernel restart action", async () => {
|
|
140
|
+
const { store, client } = mount();
|
|
141
|
+
act(() =>
|
|
142
|
+
store.set(alertAtom, (state) => ({
|
|
143
|
+
...state,
|
|
144
|
+
environments: {
|
|
145
|
+
...state.environments,
|
|
146
|
+
kernel: {
|
|
147
|
+
restart_required: true,
|
|
148
|
+
operations: [
|
|
149
|
+
preparation,
|
|
150
|
+
{
|
|
151
|
+
operation_id: "sync-python",
|
|
152
|
+
action: "sync",
|
|
153
|
+
source: "kernel",
|
|
154
|
+
status: {
|
|
155
|
+
kind: "restart-required",
|
|
156
|
+
reason: "Python version changed",
|
|
157
|
+
},
|
|
158
|
+
packages: {},
|
|
159
|
+
logs: { environment: "Resolved Python 3.14\n" },
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
})),
|
|
165
|
+
);
|
|
166
|
+
await screen.findByText("numpy");
|
|
167
|
+
expect(screen.getByText("Sandbox restart required")).toBeVisible();
|
|
168
|
+
expect(
|
|
169
|
+
screen.queryByRole("button", { name: "Retry sync" }),
|
|
170
|
+
).not.toBeInTheDocument();
|
|
171
|
+
expect(screen.getByLabelText("Error details")).toHaveTextContent(
|
|
172
|
+
"Python version changed",
|
|
173
|
+
);
|
|
174
|
+
// A later package operation can displace the sync, but not its restart.
|
|
175
|
+
act(() =>
|
|
176
|
+
store.set(alertAtom, (state) => ({
|
|
177
|
+
...state,
|
|
178
|
+
environments: {
|
|
179
|
+
...state.environments,
|
|
180
|
+
kernel: {
|
|
181
|
+
restart_required: true,
|
|
182
|
+
operations: [
|
|
183
|
+
preparation,
|
|
184
|
+
{
|
|
185
|
+
operation_id: "install-1",
|
|
186
|
+
action: "install",
|
|
187
|
+
source: "kernel",
|
|
188
|
+
status: { kind: "succeeded" },
|
|
189
|
+
packages: { numpy: "succeeded" },
|
|
190
|
+
logs: {},
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
})),
|
|
196
|
+
);
|
|
197
|
+
expect(screen.getByText("Sandbox restart required")).toBeVisible();
|
|
198
|
+
fireEvent.click(screen.getByRole("button", { name: "Restart Kernel" }));
|
|
199
|
+
expect(screen.getByRole("alertdialog")).toHaveTextContent("Restart Kernel");
|
|
200
|
+
expect(client.sendRestart).not.toHaveBeenCalled();
|
|
201
|
+
fireEvent.click(screen.getByRole("button", { name: "Confirm Restart" }));
|
|
202
|
+
expect(client.sendRestart).toHaveBeenCalledTimes(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("keeps sync output open through completion and restores it quietly after refresh", () => {
|
|
206
|
+
const { store, unmount } = mount();
|
|
207
|
+
const restore = (status: EnvironmentOperation["status"], logs: string) =>
|
|
208
|
+
store.set(alertAtom, (state) => ({
|
|
209
|
+
...state,
|
|
210
|
+
environments: {
|
|
211
|
+
...state.environments,
|
|
212
|
+
kernel: {
|
|
213
|
+
restart_required: false,
|
|
214
|
+
operations: [
|
|
215
|
+
preparation,
|
|
216
|
+
{
|
|
217
|
+
operation_id: "sync-1",
|
|
218
|
+
action: "sync",
|
|
219
|
+
source: "kernel",
|
|
220
|
+
status,
|
|
221
|
+
packages: {},
|
|
222
|
+
logs: { environment: logs },
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
}));
|
|
228
|
+
act(() => restore({ kind: "running" }, "Resolving dependencies\n"));
|
|
229
|
+
const preview = screen.getByRole("button", {
|
|
230
|
+
name: "Expand sandbox sync output",
|
|
231
|
+
});
|
|
232
|
+
fireEvent.pointerDown(preview);
|
|
233
|
+
fireEvent.click(preview);
|
|
234
|
+
const output = screen.getByLabelText("sandbox sync output");
|
|
235
|
+
act(() =>
|
|
236
|
+
restore({ kind: "running" }, "Resolving dependencies\nDownloading numpy\n"),
|
|
237
|
+
);
|
|
238
|
+
expect(output).toHaveTextContent("Downloading numpy");
|
|
239
|
+
act(() =>
|
|
240
|
+
restore(
|
|
241
|
+
{ kind: "succeeded" },
|
|
242
|
+
"Resolving dependencies\nDownloading numpy\nDone\n",
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
expect(output).toBeVisible();
|
|
246
|
+
expect(output).toHaveTextContent("Done");
|
|
247
|
+
expect(screen.getByText("Environment prepared")).toBeVisible();
|
|
248
|
+
expect(screen.getByText("Kernel started")).toBeVisible();
|
|
249
|
+
|
|
250
|
+
const snapshot = store.get(alertAtom);
|
|
251
|
+
unmount();
|
|
252
|
+
const refreshed = mount();
|
|
253
|
+
act(() => refreshed.store.set(alertAtom, snapshot));
|
|
254
|
+
const toggle = screen.getByRole("button", { name: "uv sandbox" });
|
|
255
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
256
|
+
fireEvent.click(toggle);
|
|
257
|
+
fireEvent.click(
|
|
258
|
+
screen.getByRole("button", { name: "Expand sandbox sync output" }),
|
|
259
|
+
);
|
|
260
|
+
expect(screen.getByLabelText("sandbox sync output").textContent).toBe(
|
|
261
|
+
"Resolving dependencies\nDownloading numpy\nDone\n",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it.each([false, true])(
|
|
266
|
+
"retains the progression after completion with inspecting=%s",
|
|
267
|
+
async (inspecting) => {
|
|
268
|
+
const { store, client } = mount({ preparing: true });
|
|
269
|
+
const toggle = screen.getByRole("button", { name: "uv sandbox" });
|
|
270
|
+
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
271
|
+
expect(client.getDependencyTree).not.toHaveBeenCalled();
|
|
272
|
+
if (inspecting) {
|
|
273
|
+
const preview = screen.getByRole("button", {
|
|
274
|
+
name: "Expand environment preparation output",
|
|
275
|
+
});
|
|
276
|
+
fireEvent.pointerDown(preview);
|
|
277
|
+
fireEvent.click(preview);
|
|
278
|
+
}
|
|
279
|
+
act(() => {
|
|
280
|
+
store.set(startupProgressAtom, {
|
|
281
|
+
phase: "starting-kernel",
|
|
282
|
+
logs: "Launching kernel\n",
|
|
283
|
+
log_mode: "replace",
|
|
284
|
+
});
|
|
285
|
+
store.set(connectionAtom, {
|
|
286
|
+
state: WebSocketState.CONNECTING,
|
|
287
|
+
phase: "starting-kernel",
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
act(() => store.set(connectionAtom, { state: WebSocketState.OPEN }));
|
|
291
|
+
await screen.findByText("numpy");
|
|
292
|
+
expect(toggle).toHaveAttribute("aria-expanded", String(inspecting));
|
|
293
|
+
if (!inspecting) {
|
|
294
|
+
fireEvent.click(toggle);
|
|
295
|
+
}
|
|
296
|
+
expect(screen.getByText("Environment prepared")).toBeVisible();
|
|
297
|
+
expect(screen.getByText("Kernel started")).toBeVisible();
|
|
298
|
+
expect(
|
|
299
|
+
screen.getByRole("button", {
|
|
300
|
+
name: `${inspecting ? "Collapse" : "Expand"} environment preparation output`,
|
|
301
|
+
}),
|
|
302
|
+
).toBeVisible();
|
|
303
|
+
expect(
|
|
304
|
+
screen.getByRole("button", { name: "Expand kernel startup output" }),
|
|
305
|
+
).toHaveTextContent("Launching kernel");
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
it("opens a startup error even after collapsing preparation and keeps both step outputs available", () => {
|
|
310
|
+
const { store } = mount({ preparing: true, backend: "pixi" });
|
|
311
|
+
const toggle = screen.getByRole("button", { name: "pixi sandbox" });
|
|
312
|
+
fireEvent.click(toggle);
|
|
313
|
+
act(() => {
|
|
314
|
+
store.set(startupProgressAtom, {
|
|
315
|
+
phase: "starting-kernel",
|
|
316
|
+
logs: "Loading runtime\nImport failed\n",
|
|
317
|
+
log_mode: "replace",
|
|
318
|
+
});
|
|
319
|
+
store.set(kernelStartupErrorAtom, "Could not import the runtime");
|
|
320
|
+
store.set(connectionAtom, {
|
|
321
|
+
state: WebSocketState.CLOSED,
|
|
322
|
+
phase: "starting-kernel",
|
|
323
|
+
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
324
|
+
reason: "Kernel startup failed",
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
328
|
+
expect(within(toggle).getByRole("status")).toHaveAccessibleName(
|
|
329
|
+
"Kernel failed to start",
|
|
330
|
+
);
|
|
331
|
+
expect(screen.getByLabelText("Error details")).toHaveTextContent(
|
|
332
|
+
"Could not import the runtime",
|
|
333
|
+
);
|
|
334
|
+
expect(screen.getByText("Environment prepared")).toBeVisible();
|
|
335
|
+
expect(
|
|
336
|
+
screen.getByRole("button", {
|
|
337
|
+
name: "Expand environment preparation output",
|
|
338
|
+
}),
|
|
339
|
+
).toHaveTextContent("Installed numpy");
|
|
340
|
+
expect(
|
|
341
|
+
screen.getByRole("button", { name: "Expand kernel startup output" }),
|
|
342
|
+
).toHaveTextContent("Import failed");
|
|
343
|
+
fireEvent.click(toggle);
|
|
344
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
345
|
+
fireEvent.click(toggle);
|
|
346
|
+
expect(screen.getByLabelText("Error details")).toBeVisible();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("does not add a toggle or startup details to an existing environment", async () => {
|
|
350
|
+
mount({ backend: null });
|
|
351
|
+
await screen.findByText("numpy");
|
|
352
|
+
expect(
|
|
353
|
+
screen.queryByRole("button", { name: /sandbox/ }),
|
|
354
|
+
).not.toBeInTheDocument();
|
|
355
|
+
expect(
|
|
356
|
+
screen.queryByRole("region", { name: "Sandbox details" }),
|
|
357
|
+
).not.toBeInTheDocument();
|
|
358
|
+
});
|
|
@@ -27,6 +27,7 @@ import { store } from "@/core/state/jotai";
|
|
|
27
27
|
import { WebSocketClosedReason, WebSocketState } from "@/core/websocket/types";
|
|
28
28
|
import { chromeAtom } from "../../state";
|
|
29
29
|
import PackagesPanel from "../packages-panel";
|
|
30
|
+
import { PanelSectionProvider } from "../panel-context";
|
|
30
31
|
import { SandboxController } from "../sandbox-controller";
|
|
31
32
|
|
|
32
33
|
vi.mock("@/plugins/impl/code/LazyAnyLanguageCodeMirror", () => ({
|
|
@@ -58,7 +59,7 @@ const failed = {
|
|
|
58
59
|
} as const;
|
|
59
60
|
beforeEach(() => {
|
|
60
61
|
store.set(sandboxAtom, { backend: "uv", manifest, filename: "notebook.py" });
|
|
61
|
-
store.set(sandboxSyncAtom, {
|
|
62
|
+
store.set(sandboxSyncAtom, { kind: "succeeded" });
|
|
62
63
|
store.set(sandboxActionsAtom, null);
|
|
63
64
|
store.set(connectionAtom, failed);
|
|
64
65
|
store.set(kernelStartupErrorAtom, "resolver output\n no solution");
|
|
@@ -83,7 +84,9 @@ function mount(
|
|
|
83
84
|
appConfig={AppConfigSchema.parse({})}
|
|
84
85
|
onRetry={reconnect}
|
|
85
86
|
/>
|
|
86
|
-
<
|
|
87
|
+
<PanelSectionProvider value="sidebar">
|
|
88
|
+
<PackagesPanel />
|
|
89
|
+
</PanelSectionProvider>
|
|
87
90
|
</TooltipProvider>
|
|
88
91
|
</Provider>,
|
|
89
92
|
);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
HelpCircleIcon,
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
import React from "react";
|
|
10
|
-
import {
|
|
10
|
+
import { useConnectionNotice } from "@/core/network/useConnectionNotice";
|
|
11
11
|
import { useOpenSettingsToTab } from "@/components/app-config/state";
|
|
12
12
|
import { Spinner } from "@/components/icons/spinner";
|
|
13
13
|
import { SearchInput } from "@/components/ui/input";
|
|
@@ -36,7 +36,7 @@ import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
|
36
36
|
import { cn } from "@/utils/cn";
|
|
37
37
|
import { copyToClipboard } from "@/utils/copy";
|
|
38
38
|
import { Events } from "@/utils/events";
|
|
39
|
-
import {
|
|
39
|
+
import { SandboxDetails } from "./sandbox-panel";
|
|
40
40
|
import { PanelEmptyState } from "./empty-state";
|
|
41
41
|
import { PACKAGES_INPUT_ID, packagesToInstallAtom } from "./packages-utils";
|
|
42
42
|
|
|
@@ -71,17 +71,12 @@ const PackageActionButton: React.FC<{
|
|
|
71
71
|
const PackagesPanel: React.FC = () => {
|
|
72
72
|
const sandbox = useAtomValue(sandboxAtom);
|
|
73
73
|
const connected = useAtomValue(isConnectedAtom);
|
|
74
|
-
const notice =
|
|
75
|
-
const operation = useAtomValue(sandboxSyncAtom);
|
|
74
|
+
const notice = useConnectionNotice(0);
|
|
76
75
|
if (sandbox?.backend) {
|
|
77
76
|
return (
|
|
78
77
|
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
) : (
|
|
82
|
-
<PackageContents />
|
|
83
|
-
)}
|
|
84
|
-
<SandboxFooter />
|
|
78
|
+
<SandboxDetails />
|
|
79
|
+
{connected && <PackageContents />}
|
|
85
80
|
</div>
|
|
86
81
|
);
|
|
87
82
|
}
|
|
@@ -104,13 +99,18 @@ const PackagesPanel: React.FC = () => {
|
|
|
104
99
|
};
|
|
105
100
|
|
|
106
101
|
const PackageContents: React.FC = () => {
|
|
107
|
-
const
|
|
102
|
+
const syncing = useAtomValue(sandboxSyncAtom).kind === "running";
|
|
108
103
|
const [userViewMode, setUserViewMode] = React.useState<ViewMode | null>(null);
|
|
109
104
|
const { data: dependencies, error, isPending } = usePackageDependencies();
|
|
110
105
|
|
|
111
106
|
// Only show on the first load
|
|
112
107
|
if (isPending) {
|
|
113
|
-
return
|
|
108
|
+
return (
|
|
109
|
+
<output className="flex flex-1 items-center justify-center gap-2 text-xs text-muted-foreground">
|
|
110
|
+
<Spinner className="size-4" />
|
|
111
|
+
Loading installed packages…
|
|
112
|
+
</output>
|
|
113
|
+
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
if (error) {
|
|
@@ -179,15 +179,26 @@ const PackageContents: React.FC = () => {
|
|
|
179
179
|
</div>
|
|
180
180
|
</div>
|
|
181
181
|
)}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
<fieldset
|
|
183
|
+
disabled={syncing}
|
|
184
|
+
className="flex flex-col flex-1 min-h-0"
|
|
185
|
+
aria-busy={syncing}
|
|
186
|
+
>
|
|
187
|
+
{syncing && (
|
|
188
|
+
<p className="px-3 py-2 text-xs text-muted-foreground">
|
|
189
|
+
Showing packages from the last sync.
|
|
190
|
+
</p>
|
|
191
|
+
)}
|
|
192
|
+
{viewMode === "list" ? (
|
|
193
|
+
<PackagesList packages={dependencies.list} />
|
|
194
|
+
) : (
|
|
195
|
+
<DependencyTree
|
|
196
|
+
tree={dependencies.tree}
|
|
197
|
+
error={error}
|
|
198
|
+
sandboxBackend={sandboxBackend}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
</fieldset>
|
|
191
202
|
</div>
|
|
192
203
|
);
|
|
193
204
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { atom } from "jotai";
|
|
3
|
+
import { connectionNoticeAtom } from "@/core/network/connection-notice";
|
|
4
|
+
import { preparationAtom } from "@/core/packages/sandbox-state";
|
|
5
|
+
|
|
6
|
+
const preferenceAtom = atom<{
|
|
7
|
+
preparationId: string | undefined;
|
|
8
|
+
error: string | null;
|
|
9
|
+
expanded: boolean;
|
|
10
|
+
} | null>(null);
|
|
11
|
+
|
|
12
|
+
const disclosureContextAtom = atom((get) => {
|
|
13
|
+
const notice = get(connectionNoticeAtom);
|
|
14
|
+
return {
|
|
15
|
+
preparationId: get(preparationAtom)?.operation_id,
|
|
16
|
+
error: notice && !notice.pending ? (notice.error ?? notice.title) : null,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const sandboxDetailsExpandedAtom = atom(
|
|
21
|
+
(get) => {
|
|
22
|
+
const context = get(disclosureContextAtom);
|
|
23
|
+
const preference = get(preferenceAtom);
|
|
24
|
+
if (
|
|
25
|
+
preference &&
|
|
26
|
+
preference.preparationId === context.preparationId &&
|
|
27
|
+
preference.error === context.error
|
|
28
|
+
) {
|
|
29
|
+
return preference.expanded;
|
|
30
|
+
}
|
|
31
|
+
// New work and failures open automatically. Completed startup stays quiet
|
|
32
|
+
// unless the user chose to inspect it.
|
|
33
|
+
return get(connectionNoticeAtom) !== null;
|
|
34
|
+
},
|
|
35
|
+
(get, set, expanded: boolean) => {
|
|
36
|
+
set(preferenceAtom, { ...get(disclosureContextAtom), expanded });
|
|
37
|
+
},
|
|
38
|
+
);
|