@marimo-team/islands 0.25.3-dev312 → 0.25.3-dev317

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 (53) hide show
  1. package/dist/{ErrorBoundary-CCrWQ8Zi.js → ErrorBoundary-BzPZHD9j.js} +1 -1
  2. package/dist/{chat-ui-DkuxGEDp.js → chat-ui-DASKYW38.js} +13 -13
  3. package/dist/{code-visibility-DLBADpvq.js → code-visibility-nvHsJTCm.js} +31 -32
  4. package/dist/{common-Cc-bvilP.js → common-DmnownXk.js} +5074 -5074
  5. package/dist/{config-BVYA1P0C.js → config-CL7mwGLX.js} +87 -129
  6. package/dist/{data-grid-overlay-editor-CAEih52I.js → data-grid-overlay-editor-eXoZLGys.js} +1 -1
  7. package/dist/{error-entries-istW7Xbs.js → error-entries-kb4H6PAT.js} +533 -508
  8. package/dist/feedback-button-DDdckIvG.js +370 -0
  9. package/dist/{formats-CMa7L3E8.js → formats-qhBmYb0l.js} +1 -1
  10. package/dist/{glide-data-editor-hsf5AOwE.js → glide-data-editor-keVyefZP.js} +3 -3
  11. package/dist/{globals-Blgj9EWO.js → globals-UY7m7ERN.js} +1 -1
  12. package/dist/{html-to-image-pPbR2cDx.js → html-to-image-DR8VPB7E.js} +4018 -4021
  13. package/dist/main.js +606 -607
  14. package/dist/{number-overlay-editor-BgNsDtQP.js → number-overlay-editor-Dryq4VDA.js} +1 -1
  15. package/dist/{process-output-BySO1nIs.js → process-output-BDMtqL6K.js} +2 -2
  16. package/dist/{reveal-component-MUD9gGln.js → reveal-component-BIBrITpY.js} +4 -4
  17. package/dist/static-state-Dtocd9UO.js +68 -0
  18. package/dist/{vega-component-Bly2lDhz.js → vega-component-CeN8zClP.js} +9 -9
  19. package/package.json +1 -1
  20. package/src/__tests__/mount.test.ts +17 -58
  21. package/src/components/editor/actions/useRestartKernel.tsx +0 -5
  22. package/src/components/editor/cell/__tests__/useRunCells.test.tsx +0 -3
  23. package/src/components/editor/cell/useRunCells.ts +2 -2
  24. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +2 -16
  25. package/src/components/editor/renderers/cell-array.tsx +0 -3
  26. package/src/core/edit-app.tsx +2 -7
  27. package/src/core/executions/__tests__/run.test.ts +0 -3
  28. package/src/core/executions/index.ts +1 -1
  29. package/src/core/executions/run.ts +1 -14
  30. package/src/core/islands/bootstrap.ts +0 -1
  31. package/src/core/network/__tests__/requests-network.test.ts +0 -16
  32. package/src/core/network/connection.ts +0 -5
  33. package/src/core/network/requests-toasting.tsx +1 -49
  34. package/src/core/network/resolve.ts +1 -5
  35. package/src/core/notebook/replica/__tests__/replica.test.ts +3 -3
  36. package/src/core/notebook/replica/connection.ts +1 -5
  37. package/src/core/runtime/__tests__/createWsUrl.test.ts +4 -16
  38. package/src/core/runtime/__tests__/runtime.test.ts +58 -168
  39. package/src/core/runtime/adapter.ts +1 -8
  40. package/src/core/runtime/config.ts +4 -59
  41. package/src/core/runtime/runtime.ts +1 -31
  42. package/src/core/runtime/types.ts +0 -5
  43. package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +5 -5
  44. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +125 -29
  45. package/src/core/session/connection-utils.ts +2 -14
  46. package/src/core/session/useNotebookSession.tsx +19 -12
  47. package/src/mount.tsx +3 -9
  48. package/src/utils/errors.ts +0 -7
  49. package/dist/feedback-button-BJvIOw-c.js +0 -371
  50. package/dist/toDate-5qPtZ6HL.js +0 -94
  51. package/src/components/editor/alerts/connecting-alert.tsx +0 -27
  52. package/src/core/network/__tests__/requests-lazy.test.ts +0 -322
  53. package/src/core/network/requests-lazy.ts +0 -222
@@ -1,13 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { atom, useAtom, useAtomValue } from "jotai";
3
- import useEvent from "react-use-event-hook";
4
- import { prettyError } from "@/utils/errors";
5
- import { Logger } from "@/utils/Logger";
6
- import { connectionAtom } from "../network/connection";
7
- import { ConnectionClosedReason } from "../notebook/replica";
2
+ import { atom, useAtomValue } from "jotai";
8
3
  import { store } from "../state/jotai";
9
- import { isAppNotStarted } from "../session/connection-utils";
10
- import { ConnectionState } from "../session/types";
11
4
  import { RuntimeManager } from "./runtime";
12
5
  import type { RuntimeConfig } from "./types";
13
6
 
@@ -19,33 +12,18 @@ function getBaseURI(): string {
19
12
  }
20
13
 
21
14
  export const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
22
- lazy: true,
23
15
  url: getBaseURI(),
24
16
  };
25
17
 
26
18
  export const runtimeConfigAtom = atom<RuntimeConfig>(DEFAULT_RUNTIME_CONFIG);
27
- const runtimeManagerAtom = atom<RuntimeManager>((get) => {
28
- const config = get(runtimeConfigAtom);
29
- return new RuntimeManager(config, config.lazy);
30
- });
19
+ const runtimeManagerAtom = atom<RuntimeManager>(
20
+ (get) => new RuntimeManager(get(runtimeConfigAtom)),
21
+ );
31
22
 
32
23
  export function useRuntimeManager(): RuntimeManager {
33
24
  return useAtomValue(runtimeManagerAtom);
34
25
  }
35
26
 
36
- export function useConnectToRuntime(): () => Promise<void> {
37
- const runtimeManager = useRuntimeManager();
38
- const [connection, setConnection] = useAtom(connectionAtom);
39
- return useEvent(async () => {
40
- if (isAppNotStarted(connection.state)) {
41
- setConnection({ state: ConnectionState.CONNECTING });
42
- await runtimeManager.start();
43
- } else {
44
- Logger.log("Runtime already started or starting...");
45
- }
46
- });
47
- }
48
-
49
27
  /**
50
28
  * Prefer to use useRuntimeManager instead of this function.
51
29
  */
@@ -53,39 +31,6 @@ export function getRuntimeManager(): RuntimeManager {
53
31
  return store.get(runtimeManagerAtom);
54
32
  }
55
33
 
56
- /**
57
- * Start a lazy runtime the first time something needs it.
58
- *
59
- * The connection can already read as connecting before anyone has asked
60
- * for the runtime, because the notebook stream waits for the runtime's
61
- * health and the editor mirrors that wait. So whether the runtime has
62
- * been asked to start is the runtime's own business, and this only keeps
63
- * the connection state honest around the attempt. A start that gives up
64
- * closes the connection with the reason, which leaves the user a way to
65
- * reconnect instead of a connection that looks like it is still coming up.
66
- */
67
- export async function startRuntimeIfLazy(): Promise<void> {
68
- const runtimeManager = getRuntimeManager();
69
- if (runtimeManager.isServerless || !runtimeManager.isLazy) {
70
- return;
71
- }
72
- if (isAppNotStarted(store.get(connectionAtom).state)) {
73
- store.set(connectionAtom, { state: ConnectionState.CONNECTING });
74
- }
75
- try {
76
- await runtimeManager.start();
77
- } catch (error) {
78
- if (store.get(connectionAtom).state === ConnectionState.CONNECTING) {
79
- store.set(connectionAtom, {
80
- state: ConnectionState.CLOSED,
81
- code: ConnectionClosedReason.KERNEL_STARTUP_ERROR,
82
- reason: prettyError(error),
83
- });
84
- }
85
- throw error;
86
- }
87
- }
88
-
89
34
  export function asRemoteURL(path: string): URL {
90
35
  if (path.startsWith("http")) {
91
36
  return new URL(path);
@@ -14,13 +14,10 @@ const knownQueryParams = new Set(Object.values(KnownQueryParams));
14
14
 
15
15
  export class RuntimeManager {
16
16
  private initialHealthyCheck = new Deferred<void>();
17
- private starting: Promise<void> | undefined;
18
17
  private config: RuntimeConfig;
19
- private lazy: boolean;
20
18
 
21
- public constructor(config: RuntimeConfig, lazy = false) {
19
+ public constructor(config: RuntimeConfig) {
22
20
  this.config = config;
23
- this.lazy = lazy;
24
21
  // Validate the URL on construction
25
22
  try {
26
23
  new URL(this.config.url);
@@ -30,14 +27,6 @@ export class RuntimeManager {
30
27
  { cause: error },
31
28
  );
32
29
  }
33
-
34
- if (!this.lazy) {
35
- this.init();
36
- }
37
- }
38
-
39
- public get isLazy(): boolean {
40
- return this.lazy;
41
30
  }
42
31
 
43
32
  public get httpURL(): URL {
@@ -346,25 +335,6 @@ export class RuntimeManager {
346
335
  return this.initialHealthyCheck.promise;
347
336
  }
348
337
 
349
- /**
350
- * Start the runtime once and wait until it is healthy.
351
- *
352
- * A run, the connect button, and the notebook stream can each ask for
353
- * the runtime, so the first call starts it and later calls join that
354
- * attempt. An attempt that gave up is forgotten, so the next call tries
355
- * again instead of failing on the old result.
356
- */
357
- public start(): Promise<void> {
358
- this.starting ??= this.init()
359
- .then(() => this.waitForHealthy())
360
- .catch((error: unknown) => {
361
- this.starting = undefined;
362
- this.initialHealthyCheck = new Deferred<void>();
363
- throw error;
364
- });
365
- return this.starting;
366
- }
367
-
368
338
  public headers(): KnownHeaders {
369
339
  const headers: KnownHeaders = {
370
340
  "Marimo-Session-Id": getSessionId(),
@@ -5,11 +5,6 @@ export interface RuntimeConfig {
5
5
  * The URL of the runtime server.
6
6
  */
7
7
  url: string;
8
- /**
9
- * If true, the runtime will not be loaded until the user interacts with the notebook.
10
- * If false, the runtime will be loaded immediately.
11
- */
12
- readonly lazy: boolean;
13
8
  /**
14
9
  * The server token for the runtime (Skew protection token)
15
10
  */
@@ -20,9 +20,9 @@ import { activeNotebookReplicaAtom } from "../active-notebook";
20
20
  vi.mock("../backend", () => ({ createEditorBackend: vi.fn() }));
21
21
 
22
22
  /** A host-framed native notification. */
23
- const reconnected = {
23
+ const notification = {
24
24
  event: "marimo",
25
- data: { marimo: { op: "reconnected" } },
25
+ data: { marimo: { op: "interrupted" } },
26
26
  };
27
27
 
28
28
  /** A transport whose connections the test drives. */
@@ -41,7 +41,7 @@ function makeTransport() {
41
41
  transport,
42
42
  opened,
43
43
  /** Open the latest connection and deliver a host-framed notification. */
44
- notify: () => opened.at(-1)?.receive(reconnected),
44
+ notify: () => opened.at(-1)?.receive(notification),
45
45
  };
46
46
  }
47
47
 
@@ -153,7 +153,7 @@ describe("useNotebookReplica", () => {
153
153
  act(() => fake.notify());
154
154
  expect(options.onNotification).not.toHaveBeenCalled();
155
155
  expect(onNotification).toHaveBeenCalledExactlyOnceWith({
156
- data: { op: "reconnected" },
156
+ data: { op: "interrupted" },
157
157
  });
158
158
  const [{ url }] = vi.mocked(createEditorBackend).mock.calls[0];
159
159
  expect(url()).toBe("ws://redirected/ws");
@@ -175,7 +175,7 @@ describe("useNotebookReplica", () => {
175
175
  expect(store.get(result.current.connectionAtom)).toEqual({
176
176
  state: ConnectionState.OPEN,
177
177
  });
178
- act(() => fake.opened.forEach((attempt) => attempt.receive(reconnected)));
178
+ act(() => fake.opened.forEach((attempt) => attempt.receive(notification)));
179
179
  expect(options.onNotification).toHaveBeenCalledOnce();
180
180
  unmount();
181
181
  expect(active.signal.aborted).toBe(true);
@@ -55,6 +55,7 @@ import { classifyCloseEvent } from "../../transports/connection-state";
55
55
  import { createEditorBackend } from "../backend";
56
56
  import {
57
57
  fakeBackend,
58
+ fixtureEvents,
58
59
  fixtureMessages,
59
60
  } from "../../notebook/replica/__tests__/fixtures";
60
61
  import { useNotebookSession } from "../useNotebookSession";
@@ -114,6 +115,15 @@ function connection(fake: FakeTransport) {
114
115
  /** Deliver a notification as the session routes frame it. */
115
116
  send: (notification: unknown) =>
116
117
  current().receive({ event: "marimo", data: { marimo: notification } }),
118
+ /** Deliver a snapshot of the notebook, ending in the stream's `ready`. */
119
+ ready: () => {
120
+ for (const message of fixtureMessages("notebook-events.sse")) {
121
+ current().receive(message);
122
+ if (message.event === "ready") {
123
+ break;
124
+ }
125
+ }
126
+ },
117
127
  /** End the connection so the stream reconnects. */
118
128
  drop: () => current().drop(),
119
129
  /** Close the connection with a reason the editor treats as final. */
@@ -129,10 +139,7 @@ function connection(fake: FakeTransport) {
129
139
  function makeRuntimeManager(
130
140
  reconcileFromHealth = vi.fn<() => Promise<boolean>>().mockResolvedValue(true),
131
141
  ) {
132
- const runtime = new RuntimeManager(
133
- { url: "http://localhost", lazy: true },
134
- true,
135
- );
142
+ const runtime = new RuntimeManager({ url: "http://localhost" });
136
143
  vi.spyOn(runtime, "reconcileFromHealth").mockImplementation(
137
144
  reconcileFromHealth,
138
145
  );
@@ -350,11 +357,67 @@ describe("useNotebookSession messages", () => {
350
357
  });
351
358
  });
352
359
 
353
- it.each(["kernel-ready", "reconnected"])(
354
- "waits for %s before marking the session connected",
355
- async (op) => {
360
+ it("waits for the stream's snapshot before marking the session connected", async () => {
361
+ const store = createStore();
362
+ store.set(connectionAtom, { state: ConnectionState.CONNECTING });
363
+ const fake = withTransport();
364
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
365
+ renderConnectionHook(store);
366
+ await started();
367
+ const conn = connection(fake);
368
+
369
+ await act(async () => conn.open());
370
+ expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
371
+
372
+ // A host that is not marimo never sends kernel-ready.
373
+ for (const message of fixtureMessages("notebook-events.sse")) {
374
+ act(() => conn.receive(message));
375
+ if (message.event === "ready") {
376
+ break;
377
+ }
378
+ expect(store.get(connectionAtom)).toEqual({
379
+ state: ConnectionState.CONNECTING,
380
+ });
381
+ }
382
+ expect(store.get(connectionAtom)).toEqual({ state: ConnectionState.OPEN });
383
+ });
384
+
385
+ it("waits for another ready snapshot after the stream resets", async () => {
386
+ const store = createStore();
387
+ const fake = withTransport();
388
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
389
+ renderConnectionHook(store);
390
+ await started();
391
+ const conn = connection(fake);
392
+ act(() => {
393
+ conn.open();
394
+ conn.ready();
395
+ });
396
+ expect(store.get(connectionAtom)).toEqual({ state: ConnectionState.OPEN });
397
+
398
+ act(() => conn.receive({ event: "reset", data: {} }));
399
+ expect(store.get(connectionAtom)).toEqual({
400
+ state: ConnectionState.CONNECTING,
401
+ phase: "reconnecting",
402
+ });
403
+
404
+ for (const message of fixtureMessages("notebook-events.sse")) {
405
+ act(() => conn.receive(message));
406
+ if (message.event === "ready") {
407
+ break;
408
+ }
409
+ expect(store.get(connectionAtom)).toEqual({
410
+ state: ConnectionState.CONNECTING,
411
+ phase: "reconnecting",
412
+ });
413
+ }
414
+ expect(store.get(connectionAtom)).toEqual({ state: ConnectionState.OPEN });
415
+ });
416
+
417
+ it("does not mark a static page connected", async () => {
418
+ window.__MARIMO_STATIC__ = { files: {} };
419
+ try {
356
420
  const store = createStore();
357
- store.set(connectionAtom, { state: ConnectionState.CONNECTING });
358
421
  const fake = withTransport();
359
422
  vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
360
423
  renderConnectionHook(store);
@@ -362,24 +425,38 @@ it.each(["kernel-ready", "reconnected"])(
362
425
  const conn = connection(fake);
363
426
 
364
427
  await act(async () => conn.open());
365
- expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
428
+ act(() => conn.ready());
429
+ expect(store.get(connectionAtom).state).not.toBe(ConnectionState.OPEN);
430
+ } finally {
431
+ delete window.__MARIMO_STATIC__;
432
+ }
433
+ });
366
434
 
367
- act(() =>
368
- conn.send({
369
- op,
370
- layout: null,
371
- resumed: true,
372
- ui_values: {},
373
- app_config: { width: "normal" },
374
- kiosk: false,
375
- capabilities: { terminal: false },
376
- auto_instantiated: false,
377
- consumer_capabilities: { edit: true, interact: true },
378
- }),
379
- );
380
- expect(store.get(connectionAtom).state).toBe(ConnectionState.OPEN);
381
- },
382
- );
435
+ it("does not mark the session connected on kernel-ready alone", async () => {
436
+ const store = createStore();
437
+ store.set(connectionAtom, { state: ConnectionState.CONNECTING });
438
+ const fake = withTransport();
439
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
440
+ renderConnectionHook(store);
441
+ await started();
442
+ const conn = connection(fake);
443
+ await act(async () => conn.open());
444
+
445
+ act(() =>
446
+ conn.send({
447
+ op: "kernel-ready",
448
+ layout: null,
449
+ resumed: true,
450
+ ui_values: {},
451
+ app_config: { width: "normal" },
452
+ kiosk: false,
453
+ capabilities: { terminal: false },
454
+ auto_instantiated: false,
455
+ consumer_capabilities: { edit: true, interact: true },
456
+ }),
457
+ );
458
+ expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
459
+ });
383
460
 
384
461
  describe("connection notice", () => {
385
462
  beforeEach(() => vi.useFakeTimers());
@@ -455,7 +532,7 @@ describe("connection notice", () => {
455
532
  screen.getByRole("button", { name: "Starting kernel… Open Packages" }),
456
533
  ).toBeVisible();
457
534
  expect(screen.queryByText(/Elapsed/)).not.toBeInTheDocument();
458
- send({ op: "reconnected" });
535
+ act(() => conn.ready());
459
536
  expect(
460
537
  screen.queryByRole("region", { name: "Notebook startup" }),
461
538
  ).not.toBeInTheDocument();
@@ -498,7 +575,7 @@ describe("connection notice", () => {
498
575
 
499
576
  it("restores completed startup output without putting a connected kernel back into startup", async () => {
500
577
  const { store, conn, send } = await renderNotice();
501
- send({ op: "reconnected" });
578
+ act(() => conn.ready());
502
579
  send({
503
580
  op: "startup-progress",
504
581
  phase: "starting-kernel",
@@ -515,6 +592,25 @@ describe("connection notice", () => {
515
592
  screen.queryByRole("region", { name: "Notebook startup" }),
516
593
  ).not.toBeInTheDocument();
517
594
 
595
+ const notebook = fixtureEvents("notebook-events.sse").find(
596
+ (event) => event.event === "notebook",
597
+ );
598
+ if (!notebook) {
599
+ throw new Error("Missing notebook fixture");
600
+ }
601
+ act(() =>
602
+ conn.receive({
603
+ ...notebook,
604
+ data: {
605
+ notebook: { ...notebook.data.notebook, name: "Renamed notebook" },
606
+ },
607
+ }),
608
+ );
609
+ expect(store.get(startupProgressAtom)).toEqual({
610
+ phase: "starting-kernel",
611
+ logs: "Kernel startup output\n",
612
+ });
613
+
518
614
  // A new connection starts from the next session's authoritative snapshot.
519
615
  await act(async () => conn.drop());
520
616
  await act(async () => vi.advanceTimersByTime(2000));
@@ -523,7 +619,7 @@ describe("connection notice", () => {
523
619
  });
524
620
 
525
621
  it("does not flash a notice when startup finishes within the delay", async () => {
526
- const { send } = await renderNotice();
622
+ const { conn, send } = await renderNotice();
527
623
  send({
528
624
  op: "startup-progress",
529
625
  phase: "preparing-environment",
@@ -541,7 +637,7 @@ describe("connection notice", () => {
541
637
  expect(
542
638
  screen.queryByRole("region", { name: "Notebook startup" }),
543
639
  ).not.toBeInTheDocument();
544
- send({ op: "reconnected" });
640
+ act(() => conn.ready());
545
641
  act(() => vi.advanceTimersByTime(500));
546
642
  expect(
547
643
  screen.queryByRole("region", { name: "Notebook startup" }),
@@ -22,13 +22,6 @@ export function isAppConnected(state: ConnectionState): boolean {
22
22
  return state === ConnectionState.OPEN;
23
23
  }
24
24
 
25
- /**
26
- * Check if the app is in a closing state
27
- */
28
- export function isAppClosing(state: ConnectionState): boolean {
29
- return state === ConnectionState.CLOSING;
30
- }
31
-
32
25
  /**
33
26
  * Check if the app is in a not started state
34
27
  */
@@ -41,9 +34,7 @@ export function isAppNotStarted(state: ConnectionState): boolean {
41
34
  */
42
35
  export function isAppInteractionDisabled(state: ConnectionState): boolean {
43
36
  return (
44
- state === ConnectionState.CLOSED ||
45
- state === ConnectionState.CLOSING ||
46
- state === ConnectionState.CONNECTING
37
+ state === ConnectionState.CLOSED || state === ConnectionState.CONNECTING
47
38
  );
48
39
  }
49
40
 
@@ -56,12 +47,9 @@ export function getConnectionTooltip(state: ConnectionState): string {
56
47
  return "App disconnected";
57
48
  case ConnectionState.CONNECTING:
58
49
  return "Connecting to a runtime ...";
59
- case ConnectionState.CLOSING:
60
- return "App disconnecting...";
61
50
  case ConnectionState.OPEN:
62
- return "";
63
51
  case ConnectionState.NOT_STARTED:
64
- return "Click to connect to a runtime";
52
+ return "";
65
53
  default:
66
54
  return "";
67
55
  }
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { invalidatePackageData } from "@/core/packages/package-data";
4
- import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
4
+ import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
5
5
  import { useEffect, useLayoutEffect, useMemo } from "react";
6
6
  import { useErrorBoundary } from "react-error-boundary";
7
7
  import { toast } from "@/components/ui/use-toast";
@@ -142,8 +142,6 @@ export function useNotebookSession(opts: {
142
142
  return;
143
143
  }
144
144
  case "kernel-ready": {
145
- setKernelStartupError(null);
146
- setConnection({ state: ConnectionState.OPEN });
147
145
  setInitialRunCompleted(
148
146
  Boolean(msg.data.resumed || msg.data.auto_instantiated),
149
147
  );
@@ -359,11 +357,6 @@ export function useNotebookSession(opts: {
359
357
  DownloadStorage.resolve(msg.data.request_id as RequestId, msg.data);
360
358
  return;
361
359
 
362
- case "reconnected":
363
- setKernelStartupError(null);
364
- setConnection({ state: ConnectionState.OPEN });
365
- return;
366
-
367
360
  case "focus-cell":
368
361
  focusAndScrollCellOutputIntoView(msg.data.cell_id);
369
362
  return;
@@ -454,14 +447,25 @@ export function useNotebookSession(opts: {
454
447
 
455
448
  // Subscribe before passive connection setup: static/WASM transports can emit synchronously.
456
449
  useLayoutEffect(() => {
450
+ const readyAtom = atom((get) => get(replica.notebookAtom) !== null);
457
451
  const updateConnection = () => {
458
- const status = store.get(replica.connectionAtom);
452
+ let status = store.get(replica.connectionAtom);
459
453
  if (status.state === ConnectionState.NOT_STARTED) {
460
454
  return;
461
455
  }
462
456
  if (status.state === ConnectionState.OPEN) {
463
457
  updateStartupProgress(null);
464
- return; // kernel-ready, not the socket, establishes session readiness.
458
+ // The stream's snapshot, not the socket, establishes readiness. A
459
+ // static page has a snapshot but no host to be connected to.
460
+ if (isStaticNotebook()) {
461
+ return;
462
+ }
463
+ if (store.get(readyAtom)) {
464
+ setKernelStartupError(null);
465
+ setConnection({ state: ConnectionState.OPEN });
466
+ return;
467
+ }
468
+ status = { state: ConnectionState.CONNECTING };
465
469
  }
466
470
  if (status.state === ConnectionState.CONNECTING) {
467
471
  setKernelStartupError(null);
@@ -486,9 +490,12 @@ export function useNotebookSession(opts: {
486
490
  return status;
487
491
  });
488
492
  };
489
- const unsubscribe = store.sub(replica.connectionAtom, updateConnection);
493
+ const unsubscribes = [
494
+ store.sub(replica.connectionAtom, updateConnection),
495
+ store.sub(readyAtom, updateConnection),
496
+ ];
490
497
  updateConnection();
491
- return unsubscribe;
498
+ return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
492
499
  }, [
493
500
  replica,
494
501
  store,
package/src/mount.tsx CHANGED
@@ -40,11 +40,11 @@ import {
40
40
  viewStateAtom,
41
41
  } from "./core/mode";
42
42
  import { cleanupAuthQueryParams } from "./core/network/auth";
43
- import { connectionAtom } from "./core/network/connection";
44
43
  import { requestClientAtom } from "./core/network/requests";
45
44
  import { resolveRequestClient } from "./core/network/resolve";
46
45
  import {
47
46
  DEFAULT_RUNTIME_CONFIG,
47
+ getRuntimeManager,
48
48
  runtimeConfigAtom,
49
49
  } from "./core/runtime/config";
50
50
  import {
@@ -63,7 +63,6 @@ import { maybeRegisterVSCodeBindings } from "./core/vscode/vscode-bindings";
63
63
  import type { FileStore } from "./core/wasm/store";
64
64
  import { notebookFileStore } from "./core/wasm/store";
65
65
  import { embeddedSnapshotAtom } from "./core/session/embedded";
66
- import { ConnectionState } from "./core/session/types";
67
66
  import {
68
67
  handleWidgetMessage,
69
68
  WIDGET_REGISTRY,
@@ -282,8 +281,6 @@ const mountOptionsSchema = z.object({
282
281
  .array(
283
282
  z.looseObject({
284
283
  url: z.string(),
285
- // Lazy by default, but can be overridden by the runtime config
286
- lazy: z.boolean().default(true),
287
284
  authToken: z.string().nullish(),
288
285
  }),
289
286
  )
@@ -359,17 +356,14 @@ function initStore(options: unknown) {
359
356
  ...firstRuntimeConfig,
360
357
  serverToken: parsedOptions.data.serverToken,
361
358
  });
362
- // If the remote runtime is not lazy, start it in CONNECTING
363
- if (!firstRuntimeConfig.lazy && !isStaticNotebook()) {
364
- store.set(connectionAtom, { state: ConnectionState.CONNECTING });
365
- }
366
359
  } else {
367
360
  store.set(runtimeConfigAtom, {
368
361
  ...DEFAULT_RUNTIME_CONFIG,
369
- lazy: false,
370
362
  serverToken: parsedOptions.data.serverToken,
371
363
  });
372
364
  }
365
+ // The page's runtime is known from here on, so look for it now.
366
+ void getRuntimeManager().init();
373
367
 
374
368
  // The notebook the page was served with shows until the host answers.
375
369
  const { snapshot, cellRuntime } = readExport({
@@ -94,10 +94,3 @@ export class FunctionNotFoundError extends Error {
94
94
  this.name = "FunctionNotFoundError";
95
95
  }
96
96
  }
97
-
98
- export class NoKernelConnectedError extends Error {
99
- public constructor(message = "Not yet connected to a kernel.") {
100
- super(message);
101
- this.name = "NoKernelConnectedError";
102
- }
103
- }