@marimo-team/islands 0.24.3-dev36 → 0.24.3-dev39

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.
Files changed (45) hide show
  1. package/dist/{common-CWnP5Rod.js → common-eNTtBSo2.js} +2 -2
  2. package/dist/main.js +6 -2
  3. package/dist/{reveal-component-SdzpGkJs.js → reveal-component-CNqmC7b6.js} +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/__mocks__/requests.ts +7 -0
  7. package/src/components/editor/alerts/connecting-alert.tsx +1 -66
  8. package/src/components/editor/alerts/connection-notice.tsx +140 -0
  9. package/src/components/editor/app-container.tsx +9 -3
  10. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +114 -7
  11. package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +165 -0
  12. package/src/components/editor/chrome/panels/packages-panel.tsx +93 -166
  13. package/src/components/editor/chrome/panels/sandbox-controller.tsx +106 -0
  14. package/src/components/editor/chrome/panels/sandbox-panel.tsx +173 -0
  15. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +3 -1
  16. package/src/components/editor/header/__tests__/status.test.tsx +8 -20
  17. package/src/components/editor/header/status.tsx +5 -3
  18. package/src/components/editor/renderers/cell-array.tsx +1 -5
  19. package/src/core/MarimoApp.tsx +0 -2
  20. package/src/core/edit-app.tsx +8 -5
  21. package/src/core/errors/state.ts +1 -2
  22. package/src/core/islands/bootstrap.ts +1 -0
  23. package/src/core/islands/bridge.ts +3 -0
  24. package/src/core/network/connection-notice.ts +84 -0
  25. package/src/core/network/requests-lazy.ts +3 -0
  26. package/src/core/network/requests-network.ts +12 -0
  27. package/src/core/network/requests-static.ts +3 -0
  28. package/src/core/network/requests-toasting.tsx +3 -0
  29. package/src/core/network/resolve.ts +2 -1
  30. package/src/core/network/types.ts +7 -0
  31. package/src/core/packages/__tests__/usePackageAction.test.tsx +129 -0
  32. package/src/core/packages/__tests__/useSandboxController.test.tsx +165 -0
  33. package/src/core/packages/package-data.ts +35 -0
  34. package/src/core/packages/sandbox-state.ts +15 -0
  35. package/src/core/packages/toast-components.tsx +11 -0
  36. package/src/core/packages/useInstallPackage.ts +1 -1
  37. package/src/core/packages/usePackageAction.ts +51 -0
  38. package/src/core/packages/usePackageDependencies.ts +22 -0
  39. package/src/core/packages/useSandboxController.ts +188 -0
  40. package/src/core/run-app.tsx +2 -0
  41. package/src/core/wasm/bridge.ts +11 -0
  42. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +212 -2
  43. package/src/core/websocket/types.ts +11 -1
  44. package/src/core/websocket/useMarimoKernelConnection.tsx +43 -8
  45. package/src/stories/sandbox.stories.tsx +308 -0
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { invalidatePackageData } from "@/core/packages/package-data";
3
4
  import { useAtom, useAtomValue, useSetAtom } from "jotai";
4
5
  import { useRef } from "react";
5
6
  import { useErrorBoundary } from "react-error-boundary";
@@ -240,7 +241,15 @@ export function useMarimoKernelConnection(opts: {
240
241
  case "reload":
241
242
  reloadSafe();
242
243
  return;
244
+ case "startup-progress":
245
+ setConnection({
246
+ state: WebSocketState.CONNECTING,
247
+ phase: msg.data.phase,
248
+ });
249
+ return;
243
250
  case "kernel-ready": {
251
+ setKernelStartupError(null);
252
+ setConnection({ state: WebSocketState.OPEN });
244
253
  setInitialRunCompleted(
245
254
  Boolean(msg.data.resumed || msg.data.auto_instantiated),
246
255
  );
@@ -363,6 +372,16 @@ export function useMarimoKernelConnection(opts: {
363
372
  });
364
373
  return;
365
374
  case "installing-package-alert":
375
+ if (
376
+ msg.data.source !== "server" &&
377
+ msg.data.log_status !== "append" &&
378
+ msg.data.log_status !== "start" &&
379
+ Object.values(msg.data.packages).some(
380
+ (status) => status === "installed" || status === "restart-required",
381
+ )
382
+ ) {
383
+ invalidatePackageData();
384
+ }
366
385
  addPackageAlert({
367
386
  ...msg.data,
368
387
  kind: "installing",
@@ -439,6 +458,8 @@ export function useMarimoKernelConnection(opts: {
439
458
  return;
440
459
 
441
460
  case "reconnected":
461
+ setKernelStartupError(null);
462
+ setConnection({ state: WebSocketState.OPEN });
442
463
  return;
443
464
 
444
465
  case "focus-cell":
@@ -482,6 +503,7 @@ export function useMarimoKernelConnection(opts: {
482
503
  return;
483
504
  }
484
505
  shouldTryReconnecting.current = true;
506
+ setKernelStartupError(null);
485
507
  setConnection({ state: WebSocketState.CONNECTING });
486
508
  const healthy = await runtimeManager.reconcileFromHealth();
487
509
  if (!healthy) {
@@ -513,17 +535,11 @@ export function useMarimoKernelConnection(opts: {
513
535
  headers: () => runtimeManager.headers(),
514
536
 
515
537
  /**
516
- * Open callback. Set the connection status to open.
538
+ * The transport is open; kernel-ready establishes session readiness.
517
539
  */
518
540
  onOpen: async () => {
519
541
  // If we are open, we can reset our reconnecting flag.
520
542
  shouldTryReconnecting.current = true;
521
-
522
- // DO NOT COMMIT THIS UNCOMMENTED
523
- // Uncomment to emulate a slow connection
524
- // await new Promise((resolve) => setTimeout(resolve, 10_000));
525
-
526
- setConnection({ state: WebSocketState.OPEN });
527
543
  },
528
544
 
529
545
  /**
@@ -565,7 +581,26 @@ export function useMarimoKernelConnection(opts: {
565
581
  onClose: (e) => {
566
582
  Logger.warn("WebSocket closed", e.code, e.reason);
567
583
  const decision = classifyCloseEvent(e);
568
- setConnection(decision.status);
584
+ setConnection((previous) => {
585
+ const status = decision.status;
586
+ if (
587
+ status.state === WebSocketState.CLOSED &&
588
+ status.code === WebSocketClosedReason.KERNEL_STARTUP_ERROR &&
589
+ (previous.state === WebSocketState.CONNECTING ||
590
+ previous.state === WebSocketState.CLOSED)
591
+ ) {
592
+ return { ...status, phase: previous.phase };
593
+ }
594
+ if (
595
+ status.state === WebSocketState.CONNECTING &&
596
+ (previous.state === WebSocketState.OPEN ||
597
+ (previous.state === WebSocketState.CONNECTING &&
598
+ previous.phase === "reconnecting"))
599
+ ) {
600
+ return { ...status, phase: "reconnecting" };
601
+ }
602
+ return status;
603
+ });
569
604
  if (decision.kind === "terminal" && decision.closeTransport) {
570
605
  ws.close(); // close to prevent reconnecting
571
606
  return;
@@ -0,0 +1,308 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { createStore, Provider, useAtomValue } from "jotai";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
6
+ import {
7
+ SandboxFooter,
8
+ SandboxStartupPanel,
9
+ } from "@/components/editor/chrome/panels/sandbox-panel";
10
+ import { SandboxController } from "@/components/editor/chrome/panels/sandbox-controller";
11
+ import { chromeAtom } from "@/components/editor/chrome/state";
12
+ import { Cell } from "@/components/editor/notebook-cell";
13
+ import { VerticalLayoutWrapper } from "@/components/editor/renderers/vertical-layout/vertical-layout-wrapper";
14
+ import {
15
+ createNotebookActions,
16
+ notebookAtom,
17
+ notebookReducer,
18
+ } from "@/core/cells/cells";
19
+ import { userConfigAtom } from "@/core/config/config";
20
+ import {
21
+ AppConfigSchema,
22
+ defaultUserConfig,
23
+ } from "@/core/config/config-schema";
24
+ import { kernelStartupErrorAtom } from "@/core/errors/state";
25
+ import { connectionAtom } from "@/core/network/connection";
26
+ import { requestClientAtom } from "@/core/network/requests";
27
+ import { createStaticRequests } from "@/core/network/requests-static";
28
+ import {
29
+ sandboxActionsAtom,
30
+ sandboxAtom,
31
+ sandboxSyncAtom,
32
+ } from "@/core/packages/sandbox-state";
33
+ import { filenameAtom } from "@/core/saving/file-state";
34
+ import { WebSocketClosedReason, WebSocketState } from "@/core/websocket/types";
35
+ import { HTTPError } from "@/utils/errors";
36
+
37
+ const manifest = `requires-python = ">=3.13"
38
+ dependencies = ["marimo", "polars", "rich==999.0.0"]
39
+
40
+ [tool.uv]
41
+ index-url = "https://pypi.org/simple"
42
+
43
+ [tool.notebook]
44
+ description = "Explore bike trips"
45
+ `;
46
+ const resolutionError = `No solution found when resolving dependencies:
47
+ Because there is no version of rich==999.0.0 and your script depends
48
+ on rich==999.0.0, your script's requirements are unsatisfiable.`;
49
+
50
+ interface Props {
51
+ surface: "notebook" | "packages" | "manifest";
52
+ phase:
53
+ | "preparing"
54
+ | "starting"
55
+ | "failed"
56
+ | "ready"
57
+ | "syncing"
58
+ | "sync-failed";
59
+ existingCells: boolean;
60
+ backend: "uv" | "pixi";
61
+ saveResult: "success" | "failure" | "stale";
62
+ theme: "light" | "dark";
63
+ }
64
+
65
+ function OpenManifest() {
66
+ const actions = useAtomValue(sandboxActionsAtom);
67
+ const opened = useRef(false);
68
+ useEffect(() => {
69
+ if (actions && !opened.current) {
70
+ opened.current = true;
71
+ void actions.editManifest();
72
+ }
73
+ }, [actions]);
74
+ return null;
75
+ }
76
+
77
+ function SandboxStory(props: Props) {
78
+ const [store] = useState(() => {
79
+ const state = createStore();
80
+ let currentManifest =
81
+ props.phase === "failed"
82
+ ? manifest
83
+ : manifest.replace("rich==999.0.0", "rich>=13.0.0");
84
+ const config = defaultUserConfig();
85
+ state.set(userConfigAtom, {
86
+ ...config,
87
+ display: { ...config.display, theme: props.theme },
88
+ });
89
+ state.set(filenameAtom, "bike_trips.py");
90
+ state.set(sandboxAtom, {
91
+ backend: props.backend,
92
+ manifest: currentManifest,
93
+ filename: "bike_trips.py",
94
+ });
95
+ state.set(chromeAtom, {
96
+ selectedPanel: "packages",
97
+ isSidebarOpen: props.surface !== "notebook",
98
+ isDeveloperPanelOpen: false,
99
+ selectedDeveloperPanelTab: "errors",
100
+ });
101
+ state.set(
102
+ connectionAtom,
103
+ props.phase === "preparing" || props.phase === "starting"
104
+ ? {
105
+ state: WebSocketState.CONNECTING,
106
+ phase:
107
+ props.phase === "preparing"
108
+ ? "preparing-environment"
109
+ : "starting-kernel",
110
+ }
111
+ : props.phase === "failed"
112
+ ? {
113
+ state: WebSocketState.CLOSED,
114
+ code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
115
+ reason: "Kernel startup failed",
116
+ phase: "preparing-environment",
117
+ }
118
+ : { state: WebSocketState.OPEN },
119
+ );
120
+ state.set(
121
+ kernelStartupErrorAtom,
122
+ props.phase === "failed" ? resolutionError : null,
123
+ );
124
+ state.set(sandboxSyncAtom, {
125
+ pending: props.phase === "syncing",
126
+ error: props.phase === "sync-failed" ? resolutionError : null,
127
+ });
128
+ if (props.existingCells) {
129
+ const actions = createNotebookActions((action) =>
130
+ state.set(notebookAtom, (value) => notebookReducer(value, action)),
131
+ );
132
+ actions.createNewCell({
133
+ cellId: "__end__",
134
+ before: false,
135
+ code: 'import polars as pl\n\ntrips = pl.read_csv("bike_trips.csv")\ntrips.head()',
136
+ autoFocus: false,
137
+ });
138
+ }
139
+ state.set(requestClientAtom, {
140
+ ...createStaticRequests(),
141
+ getSandbox: async () => ({
142
+ backend: props.backend,
143
+ manifest: currentManifest,
144
+ filename: "bike_trips.py",
145
+ }),
146
+ updateManifest: async ({ contents }) => {
147
+ if (props.saveResult === "stale") {
148
+ throw new HTTPError(
149
+ 409,
150
+ "The manifest changed on disk. Reopen it before saving.",
151
+ );
152
+ }
153
+ currentManifest = contents;
154
+ return {
155
+ backend: props.backend,
156
+ manifest: currentManifest,
157
+ filename: "bike_trips.py",
158
+ };
159
+ },
160
+ syncSandbox: async () => {
161
+ await new Promise((resolve) => setTimeout(resolve, 2000));
162
+ if (props.saveResult === "failure") {
163
+ return {
164
+ success: false,
165
+ error:
166
+ "Failed to download rich from https://pypi.org/simple/rich/\n Request failed after 3 retries: connection timed out",
167
+ restartRequired: false,
168
+ reconnect: false,
169
+ };
170
+ }
171
+ state.set(connectionAtom, { state: WebSocketState.OPEN });
172
+ state.set(kernelStartupErrorAtom, null);
173
+ return {
174
+ success: true,
175
+ error: null,
176
+ restartRequired: false,
177
+ reconnect: false,
178
+ };
179
+ },
180
+ });
181
+ return state;
182
+ });
183
+ const appConfig = AppConfigSchema.parse({ width: "full" });
184
+ const reconnect = async () => {
185
+ store.set(connectionAtom, { state: WebSocketState.OPEN });
186
+ };
187
+ return (
188
+ <Provider store={store}>
189
+ <SandboxController onReconnect={reconnect} />
190
+ {props.surface === "manifest" && <OpenManifest />}
191
+ <div
192
+ data-testid="sandbox-story"
193
+ className="bg-background text-foreground"
194
+ style={{
195
+ width: props.surface === "notebook" ? 820 : 400,
196
+ }}
197
+ >
198
+ {props.surface === "notebook" && (
199
+ <>
200
+ <ConnectionNotice appConfig={appConfig} onRetry={reconnect} />
201
+ {store.get(notebookAtom).cellIds.inOrderIds.map((cellId) => (
202
+ <VerticalLayoutWrapper key={cellId} appConfig={appConfig}>
203
+ <Cell
204
+ cellId={cellId}
205
+ theme={props.theme}
206
+ mode="edit"
207
+ showPlaceholder={false}
208
+ canDelete={true}
209
+ isCollapsed={false}
210
+ collapseCount={0}
211
+ canMoveX={false}
212
+ userConfig={store.get(userConfigAtom)}
213
+ />
214
+ </VerticalLayoutWrapper>
215
+ ))}
216
+ </>
217
+ )}
218
+ {props.surface === "packages" &&
219
+ (props.phase === "failed" ||
220
+ props.phase === "sync-failed" ||
221
+ props.phase === "preparing" ? (
222
+ <SandboxStartupPanel />
223
+ ) : (
224
+ <SandboxFooter />
225
+ ))}
226
+ </div>
227
+ </Provider>
228
+ );
229
+ }
230
+
231
+ const meta = {
232
+ title: "Sandbox/States",
233
+ component: SandboxStory,
234
+ parameters: { layout: "centered" },
235
+ args: {
236
+ surface: "notebook",
237
+ phase: "preparing",
238
+ existingCells: true,
239
+ backend: "uv",
240
+ saveResult: "success",
241
+ theme: "light",
242
+ },
243
+ render: (args, context) => (
244
+ <SandboxStory
245
+ key={JSON.stringify([args, context.globals.theme])}
246
+ {...args}
247
+ theme={context.globals.theme === "dark" ? "dark" : "light"}
248
+ />
249
+ ),
250
+ } satisfies Meta<typeof SandboxStory>;
251
+ export default meta;
252
+ type Story = StoryObj<typeof meta>;
253
+
254
+ export const ExistingNotebookPreparing: Story = {
255
+ name: "Startup notice / preparing",
256
+ };
257
+ export const EmptyNotebookPreparing: Story = {
258
+ name: "Empty notebook / preparing",
259
+ args: { existingCells: false },
260
+ };
261
+ export const StartingKernel: Story = {
262
+ name: "Startup notice / starting kernel",
263
+ args: { phase: "starting" },
264
+ };
265
+ export const ExistingNotebookFailed: Story = {
266
+ name: "Startup notice / failed",
267
+ args: { phase: "failed" },
268
+ };
269
+ export const EmptyNotebookFailed: Story = {
270
+ name: "Empty notebook / failed",
271
+ args: { phase: "failed", existingCells: false },
272
+ };
273
+ export const PackagesPreparing: Story = {
274
+ name: "Setup details / preparing",
275
+ args: { surface: "packages" },
276
+ };
277
+ export const PackagesFailed: Story = {
278
+ name: "Setup details / failed",
279
+ args: { surface: "packages", phase: "failed" },
280
+ };
281
+ export const PackagesReady: Story = {
282
+ name: "Status row / ready",
283
+ args: { surface: "packages", phase: "ready" },
284
+ };
285
+ export const PackagesSyncing: Story = {
286
+ name: "Status row / syncing",
287
+ args: { surface: "packages", phase: "syncing" },
288
+ };
289
+ export const PackagesSyncFailed: Story = {
290
+ name: "Setup details / sync failed",
291
+ args: { surface: "packages", phase: "sync-failed" },
292
+ };
293
+ export const PixiPackagesReady: Story = {
294
+ name: "Status row / pixi",
295
+ args: { surface: "packages", phase: "ready", backend: "pixi" },
296
+ };
297
+ export const ManifestEditor: Story = {
298
+ name: "Manifest / edit",
299
+ args: { surface: "manifest", phase: "failed" },
300
+ };
301
+ export const ManifestSyncFails: Story = {
302
+ name: "Manifest / failed save",
303
+ args: { surface: "manifest", phase: "failed", saveResult: "failure" },
304
+ };
305
+ export const ManifestChangedOnDisk: Story = {
306
+ name: "Manifest / changed on disk",
307
+ args: { surface: "manifest", phase: "failed", saveResult: "stale" },
308
+ };