@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
@@ -42,6 +42,7 @@ vi.mock("@/core/runtime/config", async () => {
42
42
 
43
43
  import { MockNotebook } from "@/__mocks__/notebook";
44
44
  import { cellId } from "@/__tests__/branded";
45
+ import { alertAtom, getPackageAlert } from "@/core/alerts/state";
45
46
  import { notebookAtom } from "@/core/cells/cells";
46
47
  import { AppConfigSchema } from "@/core/config/config-schema";
47
48
  import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
@@ -49,7 +50,7 @@ import { kernelStartupErrorAtom } from "@/core/errors/state";
49
50
  import type { NotificationPayload } from "@/core/kernel/messages";
50
51
  import { useRuntimeManager } from "@/core/runtime/config";
51
52
  import { initialRunCompletedAtom } from "../../kernel/state";
52
- import { connectionAtom } from "../../network/connection";
53
+ import { connectionAtom, startupProgressAtom } from "../../network/connection";
53
54
  import type { SessionId } from "../../kernel/session";
54
55
  import { WebSocketClosedReason, WebSocketState } from "../types";
55
56
  import type { IConnectionTransport } from "../transports/transport";
@@ -306,7 +307,12 @@ describe("connection notice", () => {
306
307
 
307
308
  it("keeps elapsed time across startup phases and leaves reconnection to the footer once cells are available", () => {
308
309
  const { store, options, send } = renderNotice();
309
- send({ op: "startup-progress", phase: "preparing-environment" });
310
+ send({
311
+ op: "startup-progress",
312
+ phase: "preparing-environment",
313
+ logs: "",
314
+ log_mode: "replace",
315
+ });
310
316
  expect(
311
317
  screen.queryByRole("region", { name: "Notebook startup" }),
312
318
  ).not.toBeInTheDocument();
@@ -324,8 +330,13 @@ describe("connection notice", () => {
324
330
  }),
325
331
  ),
326
332
  );
327
- send({ op: "startup-progress", phase: "starting-kernel" });
328
- expect(screen.getByRole("status")).toHaveTextContent("Starting notebook");
333
+ send({
334
+ op: "startup-progress",
335
+ phase: "starting-kernel",
336
+ logs: "",
337
+ log_mode: "replace",
338
+ });
339
+ expect(screen.getByRole("status")).toHaveTextContent("Starting kernel");
329
340
  expect(screen.getByText("Elapsed 35s")).toBeInTheDocument();
330
341
  send({ op: "reconnected" });
331
342
  expect(
@@ -342,11 +353,71 @@ describe("connection notice", () => {
342
353
  ).not.toBeInTheDocument();
343
354
  });
344
355
 
356
+ it("restores startup output before appending live chunks and resets it for a new attempt", () => {
357
+ const { store, send } = renderNotice();
358
+ const snapshot = {
359
+ op: "startup-progress",
360
+ phase: "starting-kernel",
361
+ logs: "Downloading runtime\n",
362
+ log_mode: "replace",
363
+ } as const;
364
+ send(snapshot);
365
+ send(snapshot);
366
+ const connection = store.get(connectionAtom);
367
+ send({ ...snapshot, logs: "Loading kernel\n", log_mode: "append" });
368
+ expect(store.get(startupProgressAtom)).toEqual({
369
+ phase: "starting-kernel",
370
+ logs: "Downloading runtime\nLoading kernel\n",
371
+ });
372
+ expect(store.get(connectionAtom)).toBe(connection);
373
+ expect(getPackageAlert(store.get(alertAtom))).toBeNull();
374
+
375
+ send({ ...snapshot, phase: "preparing-environment", logs: "" });
376
+ expect(store.get(startupProgressAtom)).toEqual({
377
+ phase: "preparing-environment",
378
+ logs: "",
379
+ });
380
+ });
381
+
382
+ it("restores completed startup output without putting a connected kernel back into startup", async () => {
383
+ const { store, options, send } = renderNotice();
384
+ send({ op: "reconnected" });
385
+ send({
386
+ op: "startup-progress",
387
+ phase: "starting-kernel",
388
+ logs: "Kernel startup output\n",
389
+ log_mode: "replace",
390
+ });
391
+ expect(store.get(connectionAtom)).toEqual({ state: WebSocketState.OPEN });
392
+ expect(store.get(startupProgressAtom)).toEqual({
393
+ phase: "starting-kernel",
394
+ logs: "Kernel startup output\n",
395
+ });
396
+ act(() => vi.advanceTimersByTime(500));
397
+ expect(
398
+ screen.queryByRole("region", { name: "Notebook startup" }),
399
+ ).not.toBeInTheDocument();
400
+
401
+ // A new transport starts from the next session's authoritative snapshot.
402
+ await act(async () => options.onOpen(new Event("open")));
403
+ expect(store.get(startupProgressAtom)).toBeNull();
404
+ });
405
+
345
406
  it("does not flash a notice when startup finishes within the delay", () => {
346
407
  const { send } = renderNotice();
347
- send({ op: "startup-progress", phase: "preparing-environment" });
408
+ send({
409
+ op: "startup-progress",
410
+ phase: "preparing-environment",
411
+ logs: "",
412
+ log_mode: "replace",
413
+ });
348
414
  act(() => vi.advanceTimersByTime(200));
349
- send({ op: "startup-progress", phase: "starting-kernel" });
415
+ send({
416
+ op: "startup-progress",
417
+ phase: "starting-kernel",
418
+ logs: "",
419
+ log_mode: "replace",
420
+ });
350
421
  act(() => vi.advanceTimersByTime(200));
351
422
  expect(
352
423
  screen.queryByRole("region", { name: "Notebook startup" }),
@@ -365,7 +436,7 @@ describe("connection notice", () => {
365
436
  "keeps a %s failure available until the user retries",
366
437
  async (phase, title) => {
367
438
  const { store, transport, options, send } = renderNotice();
368
- send({ op: "startup-progress", phase });
439
+ send({ op: "startup-progress", phase, logs: "", log_mode: "replace" });
369
440
  const error = "A full diagnostic\nwith <stderr> details";
370
441
  send({ op: "kernel-startup-error", error });
371
442
  transport.readyState = WebSocket.CLOSED;
@@ -389,7 +460,12 @@ describe("connection notice", () => {
389
460
  expect(
390
461
  screen.queryByRole("button", { name: "Try again" }),
391
462
  ).not.toBeInTheDocument();
392
- send({ op: "startup-progress", phase: "preparing-environment" });
463
+ send({
464
+ op: "startup-progress",
465
+ phase: "preparing-environment",
466
+ logs: "",
467
+ log_mode: "replace",
468
+ });
393
469
  act(() => vi.advanceTimersByTime(500));
394
470
  expect(screen.getByRole("status")).toHaveTextContent(
395
471
  "Preparing environment",
@@ -398,3 +474,118 @@ describe("connection notice", () => {
398
474
  },
399
475
  );
400
476
  });
477
+
478
+ it("replaces environment state from snapshots, then continues live progress", () => {
479
+ const store = createStore();
480
+ vi.mocked(useConnectionTransport).mockClear();
481
+ vi.mocked(useConnectionTransport).mockReturnValue(
482
+ makeTransport(WebSocket.OPEN),
483
+ );
484
+ vi.mocked(useRuntimeManager).mockReturnValue(
485
+ makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
486
+ );
487
+ renderConnectionHook(store);
488
+ const options = vi.mocked(useConnectionTransport).mock.calls.at(-1)?.[0];
489
+ function receive(data: NotificationPayload["data"]) {
490
+ act(() =>
491
+ options?.onMessage(
492
+ new MessageEvent("message", {
493
+ data: JSON.stringify({ op: data.op, data }),
494
+ }),
495
+ ),
496
+ );
497
+ }
498
+ receive({
499
+ op: "environment-operation",
500
+ source: "kernel",
501
+ operation_id: "old",
502
+ action: "install",
503
+ status: { kind: "running" },
504
+ packages: { pandas: "running" },
505
+ logs: { pandas: "stale" },
506
+ log_mode: "append",
507
+ });
508
+ const operation = {
509
+ operation_id: "new",
510
+ action: "install",
511
+ source: "kernel",
512
+ status: { kind: "running" },
513
+ packages: { numpy: "running" },
514
+ logs: { numpy: "Downloading\n" },
515
+ } as const;
516
+ const serverOperation = {
517
+ ...operation,
518
+ operation_id: "server",
519
+ action: "install",
520
+ source: "server",
521
+ status: { kind: "succeeded" },
522
+ packages: { numpy: "succeeded" },
523
+ logs: { numpy: "Server logs\n" },
524
+ } as const;
525
+ receive({
526
+ op: "environment-state",
527
+ source: "server",
528
+ state: {
529
+ restart_required: false,
530
+ operations: [serverOperation],
531
+ },
532
+ });
533
+ const snapshot = {
534
+ op: "environment-state",
535
+ source: "kernel",
536
+ state: { restart_required: false, operations: [operation] },
537
+ } as const;
538
+ // Receiving a snapshot twice must not duplicate logs.
539
+ receive({
540
+ ...snapshot,
541
+ state: { ...snapshot.state, operations: [operation] },
542
+ });
543
+ receive({
544
+ ...snapshot,
545
+ state: { ...snapshot.state, operations: [operation] },
546
+ });
547
+ receive({
548
+ op: "environment-operation",
549
+ operation_id: "new",
550
+ action: "install",
551
+ source: "kernel",
552
+ status: { kind: "failed", error: "Network unavailable" },
553
+ packages: { numpy: "running" },
554
+ logs: { numpy: "Failed\n" },
555
+ log_mode: "append",
556
+ });
557
+ expect(getPackageAlert(store.get(alertAtom))).toEqual({
558
+ ...operation,
559
+ id: "new",
560
+ kind: "environment",
561
+ restartRequired: false,
562
+ status: { kind: "failed", error: "Network unavailable" },
563
+ logs: { numpy: "Downloading\nFailed\n" },
564
+ });
565
+ receive({
566
+ op: "environment-state",
567
+ source: "kernel",
568
+ state: {
569
+ restart_required: false,
570
+ operations: [],
571
+ },
572
+ });
573
+ // Clearing the active environment must not promote an old server success.
574
+ expect(getPackageAlert(store.get(alertAtom))).toBeNull();
575
+ expect(store.get(alertAtom).environments.server.operations).toEqual([
576
+ serverOperation,
577
+ ]);
578
+ receive({
579
+ op: "environment-state",
580
+ source: "server",
581
+ state: {
582
+ restart_required: false,
583
+ operations: [],
584
+ },
585
+ });
586
+ expect(getPackageAlert(store.get(alertAtom))).toBeNull();
587
+ expect(store.get(alertAtom).environments).toEqual({
588
+ kernel: { restart_required: false, operations: [] },
589
+ server: { restart_required: false, operations: [] },
590
+ });
591
+ });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { NotificationMessageData } from "../kernel/messages";
4
4
 
5
- type ConnectionPhase =
5
+ export type ConnectionPhase =
6
6
  | NotificationMessageData<"startup-progress">["phase"]
7
7
  | "reconnecting";
8
8
 
@@ -64,7 +64,7 @@ import type { SessionId } from "../kernel/session";
64
64
  import { initialRunCompletedAtom, kernelStateAtom } from "../kernel/state";
65
65
  import { type LayoutState, useLayoutActions } from "../layout/state";
66
66
  import { kioskModeAtom } from "../mode";
67
- import { connectionAtom } from "../network/connection";
67
+ import { connectionAtom, startupProgressAtom } from "../network/connection";
68
68
  import type { RequestId } from "../network/DeferredRequestRegistry";
69
69
  import { useRuntimeManager } from "../runtime/config";
70
70
  import { SECRETS_REGISTRY } from "../secrets/request-registry";
@@ -222,8 +222,14 @@ export function useMarimoKernelConnection(opts: {
222
222
  useDataSourceActions();
223
223
  const { setLayoutData } = useLayoutActions();
224
224
  const [connection, setConnection] = useAtom(connectionAtom);
225
+ const updateStartupProgress = useSetAtom(startupProgressAtom);
225
226
  const { addBanner } = useBannersActions();
226
- const { addPackageAlert, addStartupLog } = useAlertActions();
227
+ const {
228
+ addMissingPackageAlert,
229
+ updateEnvironment,
230
+ setEnvironment,
231
+ addStartupLog,
232
+ } = useAlertActions();
227
233
  const setKioskMode = useSetAtom(kioskModeAtom);
228
234
  const setCapabilities = useSetAtom(capabilitiesAtom);
229
235
  const runtimeManager = useRuntimeManager();
@@ -241,12 +247,18 @@ export function useMarimoKernelConnection(opts: {
241
247
  case "reload":
242
248
  reloadSafe();
243
249
  return;
244
- case "startup-progress":
245
- setConnection({
246
- state: WebSocketState.CONNECTING,
247
- phase: msg.data.phase,
248
- });
250
+ case "startup-progress": {
251
+ const { phase } = msg.data;
252
+ updateStartupProgress(msg.data);
253
+ setConnection((previous) =>
254
+ previous.state === WebSocketState.OPEN ||
255
+ (previous.state === WebSocketState.CONNECTING &&
256
+ previous.phase === phase)
257
+ ? previous
258
+ : { state: WebSocketState.CONNECTING, phase },
259
+ );
249
260
  return;
261
+ }
250
262
  case "kernel-ready": {
251
263
  setKernelStartupError(null);
252
264
  setConnection({ state: WebSocketState.OPEN });
@@ -366,26 +378,25 @@ export function useMarimoKernelConnection(opts: {
366
378
  addBanner(msg.data);
367
379
  return;
368
380
  case "missing-package-alert":
369
- addPackageAlert({
381
+ addMissingPackageAlert({
370
382
  ...msg.data,
371
383
  kind: "missing",
372
384
  });
373
385
  return;
374
- case "installing-package-alert":
386
+ case "environment-operation":
375
387
  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
- )
388
+ msg.data.source === "kernel" &&
389
+ msg.data.status.kind !== "running"
382
390
  ) {
383
391
  invalidatePackageData();
384
392
  }
385
- addPackageAlert({
386
- ...msg.data,
387
- kind: "installing",
388
- });
393
+ updateEnvironment(msg.data);
394
+ return;
395
+ case "environment-state":
396
+ setEnvironment(msg.data);
397
+ if (msg.data.source === "kernel") {
398
+ invalidatePackageData();
399
+ }
389
400
  return;
390
401
  case "startup-logs":
391
402
  addStartupLog({
@@ -538,6 +549,7 @@ export function useMarimoKernelConnection(opts: {
538
549
  * The transport is open; kernel-ready establishes session readiness.
539
550
  */
540
551
  onOpen: async () => {
552
+ updateStartupProgress(null);
541
553
  // If we are open, we can reset our reconnecting flag.
542
554
  shouldTryReconnecting.current = true;
543
555
  },