@marimo-team/islands 0.24.3-dev82 → 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.
Files changed (47) hide show
  1. package/dist/{ErrorBoundary-CegtSjfd.js → ErrorBoundary-Uf_LvjvY.js} +1 -1
  2. package/dist/{cells-BIfOnGBK.js → cells-DYIkSxs6.js} +4 -0
  3. package/dist/{chat-ui-LWkjhObe.js → chat-ui-BXj3uYjy.js} +4 -4
  4. package/dist/{code-visibility-Blq7oGrC.js → code-visibility-DxgQWu1j.js} +2 -2
  5. package/dist/{common-BQZDWy8O.js → common-ByAtncLr.js} +3 -3
  6. package/dist/{data-grid-overlay-editor-DsU0Zpgt.js → data-grid-overlay-editor-DL_blFyL.js} +1 -1
  7. package/dist/{error-entries-CR0oClSL.js → error-entries-C0NsUqXl.js} +1 -1
  8. package/dist/{feedback-button-pGaDcCJS.js → feedback-button-AZcBqfoV.js} +4 -4
  9. package/dist/{glide-data-editor-DtJQCRsf.js → glide-data-editor-BJdZncJk.js} +3 -3
  10. package/dist/{globals-DBAC4Ihd.js → globals-yWarpyN4.js} +1 -1
  11. package/dist/{html-to-image-_i03yR4q.js → html-to-image-CeYYvQNb.js} +1 -1
  12. package/dist/main.js +12 -11
  13. package/dist/{number-overlay-editor-9M7FnzxL.js → number-overlay-editor-BatBVYxr.js} +1 -1
  14. package/dist/{process-output-D3zQyTAS.js → process-output-BCTjlyuH.js} +2 -2
  15. package/dist/{reveal-component-ovaRyk0R.js → reveal-component-D02jcpiJ.js} +3 -3
  16. package/dist/style.css +1 -1
  17. package/package.json +1 -1
  18. package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +186 -0
  19. package/src/components/editor/alerts/__tests__/startup-progress.test.tsx +185 -0
  20. package/src/components/editor/alerts/connection-notice.tsx +100 -54
  21. package/src/components/editor/alerts/startup-output.tsx +173 -0
  22. package/src/components/editor/alerts/startup-progress.tsx +204 -0
  23. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +53 -4
  24. package/src/components/editor/chrome/panels/__tests__/sandbox-details.test.tsx +358 -0
  25. package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +5 -2
  26. package/src/components/editor/chrome/panels/packages-panel.tsx +32 -21
  27. package/src/components/editor/chrome/panels/sandbox-details-state.ts +38 -0
  28. package/src/components/editor/chrome/panels/sandbox-panel.tsx +188 -131
  29. package/src/components/editor/chrome/panels/sandbox-toggle.tsx +52 -0
  30. package/src/components/editor/chrome/wrapper/app-chrome.tsx +7 -0
  31. package/src/components/editor/package-alert.tsx +112 -66
  32. package/src/core/alerts/__tests__/environment.test.ts +183 -0
  33. package/src/core/alerts/__tests__/state.test.tsx +240 -0
  34. package/src/core/alerts/environment.ts +57 -0
  35. package/src/core/alerts/state.ts +155 -99
  36. package/src/core/islands/bootstrap.ts +2 -1
  37. package/src/core/kernel/messages.ts +2 -2
  38. package/src/core/network/connection-notice.ts +45 -12
  39. package/src/core/network/connection.ts +19 -0
  40. package/src/core/network/useConnectionNotice.ts +53 -0
  41. package/src/core/packages/__tests__/useSandboxController.test.tsx +86 -5
  42. package/src/core/packages/sandbox-state.ts +56 -4
  43. package/src/core/packages/useSandboxController.ts +27 -7
  44. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +199 -8
  45. package/src/core/websocket/types.ts +1 -1
  46. package/src/core/websocket/useMarimoKernelConnection.tsx +31 -19
  47. package/src/stories/sandbox.stories.tsx +230 -50
@@ -14,7 +14,7 @@ import {
14
14
  XIcon,
15
15
  } from "lucide-react";
16
16
  import type React from "react";
17
- import { useState } from "react";
17
+ import { useEffect, useState } from "react";
18
18
  import { useForm } from "react-hook-form";
19
19
  import type { z } from "zod";
20
20
  import {
@@ -25,19 +25,20 @@ import {
25
25
  FormMessage,
26
26
  } from "@/components/ui/form";
27
27
  import {
28
- isInstallingPackageAlert,
28
+ type EnvironmentOperationAlert,
29
+ isEnvironmentOperationAlert,
29
30
  isMissingPackageAlert,
30
31
  useAlertActions,
31
32
  useAlerts,
32
33
  } from "@/core/alerts/state";
33
34
  import { useResolvedMarimoConfig } from "@/core/config/config";
34
- import type { PackageInstallationStatus } from "@/core/kernel/messages";
35
+ import type { PackageOperationStatus } from "@/core/kernel/messages";
35
36
  import { useRequestClient } from "@/core/network/requests";
36
37
  import { RESTART_REQUIRED_DESCRIPTION } from "@/core/packages/toast-components";
37
38
  import { isWasm } from "@/core/wasm/utils";
38
39
  import { usePackageMetadata } from "@/hooks/usePackageMetadata";
39
40
  import { Banner } from "@/plugins/impl/common/error-banner";
40
- import { logNever } from "@/utils/assertNever";
41
+ import { assertNever, logNever } from "@/utils/assertNever";
41
42
  import { cn } from "@/utils/cn";
42
43
  import { Logger } from "@/utils/Logger";
43
44
  import {
@@ -100,7 +101,7 @@ const SourceBadge: React.FC<{ source?: "kernel" | "server" }> = ({
100
101
  };
101
102
 
102
103
  export const PackageAlert: React.FC = () => {
103
- const { packageAlert, packageLogs } = useAlerts();
104
+ const { packageAlert } = useAlerts();
104
105
  const { clearPackageAlert } = useAlertActions();
105
106
  const [userConfig] = useResolvedMarimoConfig();
106
107
  const [desiredPackageVersions, setDesiredPackageVersions] = useState<
@@ -110,6 +111,23 @@ export const PackageAlert: React.FC = () => {
110
111
  Record<string, string[]>
111
112
  >({});
112
113
 
114
+ const completedAlertId =
115
+ packageAlert?.kind === "environment" &&
116
+ packageAlert.status.kind === "succeeded" &&
117
+ !packageAlert.restartRequired
118
+ ? packageAlert.id
119
+ : null;
120
+ useEffect(() => {
121
+ if (completedAlertId === null) {
122
+ return;
123
+ }
124
+ const timeout = setTimeout(
125
+ () => clearPackageAlert(completedAlertId),
126
+ 10_000,
127
+ );
128
+ return () => clearTimeout(timeout);
129
+ }, [completedAlertId, clearPackageAlert]);
130
+
113
131
  if (packageAlert === null) {
114
132
  return null;
115
133
  }
@@ -224,12 +242,9 @@ export const PackageAlert: React.FC = () => {
224
242
  );
225
243
  }
226
244
 
227
- if (isInstallingPackageAlert(packageAlert)) {
245
+ if (isEnvironmentOperationAlert(packageAlert)) {
228
246
  const { status, title, titleIcon, description } =
229
- getInstallationStatusElements(packageAlert.packages);
230
- if (status === "installed") {
231
- setTimeout(() => clearPackageAlert(packageAlert.id), 10_000);
232
- }
247
+ getOperationStatusElements(packageAlert);
233
248
 
234
249
  return (
235
250
  <div className="flex flex-col gap-4 mb-5 fixed top-5 left-12 min-w-[400px] z-200 opacity-95 max-w-[600px] pointer-events-none">
@@ -255,23 +270,27 @@ export const PackageAlert: React.FC = () => {
255
270
  <div
256
271
  className={cn(
257
272
  "flex flex-col gap-4 justify-between items-start text-muted-foreground text-base",
258
- status === "installed" && "text-accent-foreground",
273
+ status === "succeeded" && "text-accent-foreground",
259
274
  )}
260
275
  >
261
276
  <p>{description}</p>
262
- {status !== "installing" &&
263
- Object.values(packageAlert.packages).includes(
264
- "restart-required",
265
- ) && <RestartKernelButton />}
277
+ {status !== "running" && packageAlert.restartRequired && (
278
+ <>
279
+ {status !== "restart-required" && (
280
+ <p>{RESTART_REQUIRED_DESCRIPTION}</p>
281
+ )}
282
+ <RestartKernelButton />
283
+ </>
284
+ )}
266
285
  <ul className="list-disc ml-2 mt-1">
267
286
  {Object.entries(packageAlert.packages).map(([pkg, st], index) => (
268
287
  <li
269
288
  className={cn(
270
289
  "flex items-center gap-1 font-mono text-sm",
271
- st === "installing" && "font-semibold",
290
+ st === "running" && "font-semibold",
272
291
  st === "failed" && "text-destructive",
273
- st === "installed" && "text-accent-foreground",
274
- st === "installed" &&
292
+ st === "succeeded" && "text-accent-foreground",
293
+ st === "succeeded" &&
275
294
  status === "failed" &&
276
295
  "text-muted-foreground",
277
296
  )}
@@ -282,8 +301,11 @@ export const PackageAlert: React.FC = () => {
282
301
  </li>
283
302
  ))}
284
303
  </ul>
285
- {Object.keys(packageLogs).length > 0 && (
286
- <StreamingLogsViewer packageLogs={packageLogs} />
304
+ {Object.keys(packageAlert.logs).length > 0 && (
305
+ <StreamingLogsViewer
306
+ key={packageAlert.id}
307
+ packageLogs={packageAlert.logs}
308
+ />
287
309
  )}
288
310
  </div>
289
311
  </Banner>
@@ -295,60 +317,84 @@ export const PackageAlert: React.FC = () => {
295
317
  return null;
296
318
  };
297
319
 
298
- function getInstallationStatusElements(packages: PackageInstallationStatus) {
299
- const statuses = new Set(Object.values(packages));
300
- const status =
301
- statuses.has("queued") || statuses.has("installing")
302
- ? "installing"
303
- : statuses.has("failed")
304
- ? "failed"
305
- : statuses.has("restart-required")
306
- ? "restart-required"
307
- : "installed";
308
-
309
- if (status === "installing") {
310
- return {
311
- status: "installing",
312
- title: "Installing packages",
313
- titleIcon: <DownloadCloudIcon className="w-5 h-5 inline-block mr-2" />,
314
- description: "Installing packages:",
315
- };
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
- }
325
- if (status === "installed") {
326
- return {
327
- status: "installed",
328
- title: "All packages installed!",
329
- titleIcon: <PackageCheckIcon className="w-5 h-5 inline-block mr-2" />,
330
- description: "Installed packages:",
331
- };
320
+ function getOperationStatusElements({
321
+ action,
322
+ status: outcome,
323
+ restartRequired,
324
+ }: EnvironmentOperationAlert) {
325
+ const status: EnvironmentOperationAlert["status"] =
326
+ outcome.kind === "succeeded" && restartRequired
327
+ ? { kind: "restart-required", reason: RESTART_REQUIRED_DESCRIPTION }
328
+ : outcome;
329
+ const titles = {
330
+ install: {
331
+ running: "Installing packages",
332
+ succeeded: "All packages installed!",
333
+ failed: "Some packages failed to install",
334
+ cancelled: "Package installation cancelled",
335
+ },
336
+ remove: {
337
+ running: "Removing packages",
338
+ succeeded: "Packages removed",
339
+ failed: "Some packages failed to remove",
340
+ cancelled: "Package removal cancelled",
341
+ },
342
+ }[action];
343
+ switch (status.kind) {
344
+ case "running":
345
+ return {
346
+ status: "running",
347
+ title: titles.running,
348
+ titleIcon: <DownloadCloudIcon className="w-5 h-5 inline-block mr-2" />,
349
+ description:
350
+ action === "install" ? "Installing packages:" : "Removing packages:",
351
+ };
352
+ case "restart-required":
353
+ return {
354
+ status: "restart-required",
355
+ title: "Changes saved — restart required",
356
+ titleIcon: <RotateCwIcon className="w-5 h-5 inline-block mr-2" />,
357
+ description: status.reason,
358
+ };
359
+ case "succeeded":
360
+ return {
361
+ status: "succeeded",
362
+ title: titles.succeeded,
363
+ titleIcon: <PackageCheckIcon className="w-5 h-5 inline-block mr-2" />,
364
+ description:
365
+ action === "install" ? "Installed packages:" : "Removed packages:",
366
+ };
367
+ case "failed":
368
+ return {
369
+ status: "failed",
370
+ title: titles.failed,
371
+ titleIcon: <PackageXIcon className="w-5 h-5 inline-block mr-2" />,
372
+ description: status.error,
373
+ };
374
+ case "cancelled":
375
+ return {
376
+ status: "cancelled",
377
+ title: titles.cancelled,
378
+ titleIcon: <XIcon className="w-5 h-5 inline-block mr-2" />,
379
+ description:
380
+ "The operation was interrupted. Some packages may have changed.",
381
+ };
382
+ default:
383
+ return assertNever(status);
332
384
  }
333
- return {
334
- status: "failed",
335
- title: "Some packages failed to install",
336
- titleIcon: <PackageXIcon className="w-5 h-5 inline-block mr-2" />,
337
- description: "See error logs.",
338
- };
339
385
  }
340
386
 
341
387
  const ProgressIcon = ({
342
388
  status,
343
389
  }: {
344
- status: PackageInstallationStatus[string];
390
+ status: PackageOperationStatus[string];
345
391
  }) => {
346
392
  switch (status) {
347
393
  case "queued":
348
394
  return <BoxIcon size="1rem" />;
349
- case "installing":
395
+ case "running":
350
396
  return <DownloadCloudIcon size="1rem" />;
351
- case "installed":
397
+ case "succeeded":
352
398
  return <CheckIcon size="1rem" />;
353
399
  case "failed":
354
400
  return <XIcon size="1rem" />;
@@ -672,7 +718,7 @@ const StreamingLogsViewer: React.FC<StreamingLogsViewerProps> = ({
672
718
  }
673
719
 
674
720
  return (
675
- <div className="mt-4 border-t border-border pt-4 w-full">
721
+ <div className="mt-4 border-t border-border pt-4 w-full contain-inline-size">
676
722
  <button
677
723
  onClick={() => setIsExpanded(!isExpanded)}
678
724
  className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
@@ -683,7 +729,7 @@ const StreamingLogsViewer: React.FC<StreamingLogsViewerProps> = ({
683
729
  ) : (
684
730
  <ChevronRightIcon className="w-4 h-4" />
685
731
  )}
686
- Installation logs
732
+ Logs
687
733
  </button>
688
734
 
689
735
  {isExpanded && (
@@ -694,7 +740,7 @@ const StreamingLogsViewer: React.FC<StreamingLogsViewerProps> = ({
694
740
  {packageName}
695
741
  </h4>
696
742
  <div className="border border-border rounded w-full">
697
- <pre className="p-3 text-xs font-mono bg-background max-h-64 overflow-y-auto text-muted-foreground whitespace-pre-wrap scrollbar-thin">
743
+ <pre className="p-3 text-xs font-mono bg-background max-h-64 overflow-auto text-muted-foreground whitespace-pre scrollbar-thin">
698
744
  {logs || "No logs available"}
699
745
  </pre>
700
746
  </div>
@@ -0,0 +1,183 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import type { NotificationMessageData } from "@/core/kernel/messages";
5
+ import { emptyEnvironmentState, reduceEnvironmentState } from "../environment";
6
+
7
+ function progress(
8
+ operation_id: string,
9
+ update: Partial<NotificationMessageData<"environment-operation">> = {},
10
+ ): NotificationMessageData<"environment-operation"> {
11
+ return {
12
+ operation_id,
13
+ action: "install",
14
+ source: "kernel",
15
+ status: { kind: "running" },
16
+ packages: { numpy: "running" },
17
+ logs: { numpy: "Downloading\n" },
18
+ log_mode: "append",
19
+ ...update,
20
+ };
21
+ }
22
+
23
+ describe("environment progress", () => {
24
+ it("continues snapshot logs without mutating or duplicating them", () => {
25
+ const snapshot = reduceEnvironmentState(
26
+ emptyEnvironmentState(),
27
+ progress("one"),
28
+ );
29
+ const result = reduceEnvironmentState(
30
+ snapshot,
31
+ progress("one", {
32
+ status: { kind: "succeeded" },
33
+ packages: { numpy: "succeeded" },
34
+ logs: { numpy: "Installed\n" },
35
+ log_mode: "append",
36
+ }),
37
+ );
38
+ expect(result).toEqual({
39
+ restart_required: false,
40
+ operations: [
41
+ {
42
+ operation_id: "one",
43
+ action: "install",
44
+ source: "kernel",
45
+ status: { kind: "succeeded" },
46
+ packages: { numpy: "succeeded" },
47
+ logs: { numpy: "Downloading\nInstalled\n" },
48
+ },
49
+ ],
50
+ });
51
+ expect(snapshot.operations[0].logs).toEqual({ numpy: "Downloading\n" });
52
+ });
53
+
54
+ it("isolates overlapping operations and starts retries with fresh logs", () => {
55
+ let state = reduceEnvironmentState(
56
+ emptyEnvironmentState(),
57
+ progress("one"),
58
+ );
59
+ state = reduceEnvironmentState(
60
+ state,
61
+ progress("two", { logs: { numpy: "Other\n" } }),
62
+ );
63
+ state = reduceEnvironmentState(
64
+ state,
65
+ progress("one", {
66
+ status: { kind: "failed", error: "Network unavailable" },
67
+ logs: {},
68
+ }),
69
+ );
70
+ expect(state.operations.map((op) => [op.operation_id, op.logs])).toEqual([
71
+ ["one", { numpy: "Downloading\n" }],
72
+ ["two", { numpy: "Other\n" }],
73
+ ]);
74
+ state = reduceEnvironmentState(state, progress("retry", { logs: {} }));
75
+ expect(state.operations.map((op) => [op.operation_id, op.logs])).toEqual([
76
+ ["two", { numpy: "Other\n" }],
77
+ ["retry", {}],
78
+ ]);
79
+ });
80
+
81
+ it("replaces named logs independently of the package map", () => {
82
+ const state = reduceEnvironmentState(
83
+ emptyEnvironmentState(),
84
+ progress("one", {
85
+ packages: { numpy: "succeeded", pandas: "running" },
86
+ logs: { numpy: "Old\n", pandas: "Old\n" },
87
+ }),
88
+ );
89
+ const result = reduceEnvironmentState(
90
+ state,
91
+ progress("one", {
92
+ log_mode: "replace",
93
+ logs: { numpy: "", pandas: "Ignored\n" },
94
+ }),
95
+ );
96
+ expect(result.operations[0].logs).toEqual({
97
+ numpy: "",
98
+ pandas: "Ignored\n",
99
+ });
100
+ });
101
+
102
+ it("keeps a restart requirement after a later operation succeeds", () => {
103
+ let state = reduceEnvironmentState(
104
+ emptyEnvironmentState(),
105
+ progress("one", {
106
+ status: { kind: "restart-required", reason: "Python changed" },
107
+ }),
108
+ );
109
+ state = reduceEnvironmentState(
110
+ state,
111
+ progress("two", {
112
+ status: { kind: "succeeded" },
113
+ packages: { numpy: "succeeded" },
114
+ logs: {},
115
+ }),
116
+ );
117
+ expect(state).toEqual({
118
+ restart_required: true,
119
+ operations: [
120
+ {
121
+ operation_id: "two",
122
+ action: "install",
123
+ source: "kernel",
124
+ status: { kind: "succeeded" },
125
+ packages: { numpy: "succeeded" },
126
+ logs: {},
127
+ },
128
+ ],
129
+ });
130
+ });
131
+ });
132
+
133
+ it("retains preparation through later package changes and replaces it on a new attempt", () => {
134
+ const preparation = progress("prepare", {
135
+ action: "prepare",
136
+ status: { kind: "succeeded" },
137
+ packages: {},
138
+ logs: { environment: "Prepared environment\n" },
139
+ });
140
+ let state = reduceEnvironmentState(emptyEnvironmentState(), preparation);
141
+ const retained = state.operations[0];
142
+ for (const action of ["install", "sync", "remove"] as const) {
143
+ state = reduceEnvironmentState(state, progress(action, { action }));
144
+ state = reduceEnvironmentState(
145
+ state,
146
+ progress(action, {
147
+ action,
148
+ status: { kind: "succeeded" },
149
+ packages: { numpy: "succeeded" },
150
+ logs: {},
151
+ }),
152
+ );
153
+ expect(state.operations).toEqual([
154
+ retained,
155
+ {
156
+ operation_id: action,
157
+ action,
158
+ source: "kernel",
159
+ status: { kind: "succeeded" },
160
+ packages: { numpy: "succeeded" },
161
+ logs: { numpy: "Downloading\n" },
162
+ },
163
+ ]);
164
+ }
165
+ state = reduceEnvironmentState(
166
+ state,
167
+ progress("prepare-again", {
168
+ action: "prepare",
169
+ packages: {},
170
+ logs: {},
171
+ }),
172
+ );
173
+ expect(state.operations).toEqual([
174
+ {
175
+ operation_id: "prepare-again",
176
+ action: "prepare",
177
+ source: "kernel",
178
+ status: { kind: "running" },
179
+ packages: {},
180
+ logs: {},
181
+ },
182
+ ]);
183
+ });