@marimo-team/islands 0.23.14-dev2 → 0.23.14-dev21
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/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
- package/dist/{chat-ui-CsPewo4h.js → chat-ui-CO8WuXGb.js} +3243 -3039
- package/dist/{code-visibility-CRYdBxcA.js → code-visibility-CPWEmIAX.js} +1115 -917
- package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-qpmKyAPn.js} +2 -2
- package/dist/{html-to-image-DXwLcQ6l.js → html-to-image-DLSOS-bE.js} +2351 -2282
- package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
- package/dist/main.js +1139 -1071
- package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
- package/dist/{process-output-C6_e1pT_.js → process-output-B9ysqk1y.js} +1 -1
- package/dist/{reveal-component-CbqUMhp8.js → reveal-component-BJwt0B2_.js} +16 -16
- package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
- package/package.json +2 -2
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -2
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/readonly-python-code.tsx +16 -2
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/output/MarimoTracebackOutput.tsx +10 -1
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +6 -2
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/slide-cell-view.tsx +7 -6
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +94 -0
- package/src/core/codemirror/completion/completer.ts +12 -1
- package/src/core/codemirror/completion/signature-hint.ts +68 -0
- package/src/core/codemirror/language/languages/python.ts +10 -4
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +9 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/wasm/store.ts +1 -1
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- /package/dist/{files → export_demos}/wasm-intro.py +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { type Atom, atom } from "jotai";
|
|
4
|
+
import { kernelStateAtom } from "../kernel/state";
|
|
5
|
+
import { islandsInitializedAtom } from "../islands/state";
|
|
6
|
+
import { isIslands } from "../islands/utils";
|
|
7
|
+
import { connectionAtom } from "../network/connection";
|
|
8
|
+
import { isStaticNotebook } from "../static/static-state";
|
|
9
|
+
import { wasmInitStateAtom } from "../wasm/state";
|
|
10
|
+
import { isWasm } from "../wasm/utils";
|
|
11
|
+
import { WebSocketState } from "../websocket/types";
|
|
12
|
+
|
|
13
|
+
export type RuntimeKind = "wasm" | "remote" | "static" | "islands";
|
|
14
|
+
|
|
15
|
+
export type AdapterState =
|
|
16
|
+
| { kind: "connecting"; progress?: { label: string; percent?: number } }
|
|
17
|
+
| { kind: "ready" }
|
|
18
|
+
| {
|
|
19
|
+
kind: "failed";
|
|
20
|
+
error: { message: string; errorKind: "init" | "runtime" };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface RuntimeCapabilities {
|
|
24
|
+
canHealthCheck: boolean;
|
|
25
|
+
canShutdown: boolean;
|
|
26
|
+
canRestart: boolean;
|
|
27
|
+
supportsLsp: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Runtimes with no server-side kernel to manage (wasm, static, islands). */
|
|
31
|
+
const NO_CAPABILITIES: RuntimeCapabilities = {
|
|
32
|
+
canHealthCheck: false,
|
|
33
|
+
canShutdown: false,
|
|
34
|
+
canRestart: false,
|
|
35
|
+
supportsLsp: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One adapter per runtime so consumers don't branch on `isWasm()` /
|
|
40
|
+
* `isStaticNotebook()` / `isIslands()` at the call site.
|
|
41
|
+
*/
|
|
42
|
+
export interface RuntimeAdapter {
|
|
43
|
+
readonly kind: RuntimeKind;
|
|
44
|
+
/** Short label suitable for status pills, e.g. "Kernel", "Pyodide". */
|
|
45
|
+
readonly label: string;
|
|
46
|
+
readonly capabilities: RuntimeCapabilities;
|
|
47
|
+
state: Atom<AdapterState>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- WASM ------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
const wasmStateAtom = atom<AdapterState>((get) => {
|
|
53
|
+
const s = get(wasmInitStateAtom);
|
|
54
|
+
switch (s.kind) {
|
|
55
|
+
case "loading":
|
|
56
|
+
return { kind: "connecting", progress: { label: s.message } };
|
|
57
|
+
case "ready":
|
|
58
|
+
return { kind: "ready" };
|
|
59
|
+
case "error":
|
|
60
|
+
return {
|
|
61
|
+
kind: "failed",
|
|
62
|
+
error: { message: s.message, errorKind: "init" },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const wasmAdapter: RuntimeAdapter = {
|
|
68
|
+
kind: "wasm",
|
|
69
|
+
label: "Pyodide",
|
|
70
|
+
capabilities: NO_CAPABILITIES,
|
|
71
|
+
state: wasmStateAtom,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// --- Remote ----------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const remoteStateAtom = atom<AdapterState>((get) => {
|
|
77
|
+
const conn = get(connectionAtom);
|
|
78
|
+
switch (conn.state) {
|
|
79
|
+
case WebSocketState.OPEN:
|
|
80
|
+
return { kind: "ready" };
|
|
81
|
+
case WebSocketState.CONNECTING:
|
|
82
|
+
return { kind: "connecting", progress: { label: "Connecting…" } };
|
|
83
|
+
case WebSocketState.NOT_STARTED:
|
|
84
|
+
return { kind: "connecting", progress: { label: "Not connected" } };
|
|
85
|
+
case WebSocketState.CLOSING:
|
|
86
|
+
case WebSocketState.CLOSED:
|
|
87
|
+
return {
|
|
88
|
+
kind: "failed",
|
|
89
|
+
error: {
|
|
90
|
+
message:
|
|
91
|
+
conn.state === WebSocketState.CLOSED
|
|
92
|
+
? conn.reason
|
|
93
|
+
: "Disconnecting",
|
|
94
|
+
errorKind: "runtime",
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const remoteAdapter: RuntimeAdapter = {
|
|
101
|
+
kind: "remote",
|
|
102
|
+
label: "Kernel",
|
|
103
|
+
capabilities: {
|
|
104
|
+
canHealthCheck: true,
|
|
105
|
+
canShutdown: true,
|
|
106
|
+
canRestart: true,
|
|
107
|
+
supportsLsp: true,
|
|
108
|
+
},
|
|
109
|
+
state: remoteStateAtom,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// --- Static ----------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
const staticStateAtom = atom<AdapterState>({ kind: "ready" });
|
|
115
|
+
|
|
116
|
+
export const staticAdapter: RuntimeAdapter = {
|
|
117
|
+
kind: "static",
|
|
118
|
+
label: "Static",
|
|
119
|
+
capabilities: NO_CAPABILITIES,
|
|
120
|
+
state: staticStateAtom,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// --- Islands ---------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
const islandsStateAtom = atom<AdapterState>((get) => {
|
|
126
|
+
const status = get(islandsInitializedAtom);
|
|
127
|
+
if (status === true) {
|
|
128
|
+
return { kind: "ready" };
|
|
129
|
+
}
|
|
130
|
+
if (typeof status === "string") {
|
|
131
|
+
return {
|
|
132
|
+
kind: "failed",
|
|
133
|
+
error: { message: status, errorKind: "init" },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
kind: "connecting",
|
|
138
|
+
progress: { label: "Initializing islands…" },
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export const islandsAdapter: RuntimeAdapter = {
|
|
143
|
+
kind: "islands",
|
|
144
|
+
label: "Islands",
|
|
145
|
+
capabilities: NO_CAPABILITIES,
|
|
146
|
+
state: islandsStateAtom,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function selectAdapter(): RuntimeAdapter {
|
|
150
|
+
if (isStaticNotebook()) {
|
|
151
|
+
return staticAdapter;
|
|
152
|
+
}
|
|
153
|
+
if (isIslands()) {
|
|
154
|
+
return islandsAdapter;
|
|
155
|
+
}
|
|
156
|
+
if (isWasm()) {
|
|
157
|
+
return wasmAdapter;
|
|
158
|
+
}
|
|
159
|
+
return remoteAdapter;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Picked once at module load; the choice doesn't change after page load. */
|
|
163
|
+
export const runtimeAdapterAtom = atom<RuntimeAdapter>(selectAdapter());
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Remote runtimes require both the WS to be open *and* the kernel to have
|
|
167
|
+
* ack'd instantiation. WASM/islands have no separate kernel handshake.
|
|
168
|
+
* Static notebooks never run cells.
|
|
169
|
+
*/
|
|
170
|
+
export const canRunCellsAtom = atom<boolean>((get) => {
|
|
171
|
+
const adapter = get(runtimeAdapterAtom);
|
|
172
|
+
if (get(adapter.state).kind !== "ready") {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (adapter.kind === "static") {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (adapter.kind === "remote") {
|
|
179
|
+
return get(kernelStateAtom).isInstantiated;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
@@ -5,54 +5,41 @@ import type React from "react";
|
|
|
5
5
|
import type { PropsWithChildren } from "react";
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
7
7
|
import { LargeSpinner } from "@/components/icons/large-spinner";
|
|
8
|
+
import { ProgressiveBoundary } from "@/components/lifecycle/ProgressiveBoundary";
|
|
8
9
|
import { toast } from "@/components/ui/use-toast";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { store } from "@/core/state/jotai";
|
|
10
|
+
import { canPaintAtom } from "@/core/lifecycle/render-policy";
|
|
11
|
+
import { wasmInitStateAtom } from "@/core/wasm/state";
|
|
12
12
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
13
13
|
import { prettyError } from "@/utils/errors";
|
|
14
14
|
import { Logger } from "@/utils/Logger";
|
|
15
|
-
import { hasQueryParam } from "@/utils/urls";
|
|
16
|
-
import { KnownQueryParams } from "../constants";
|
|
17
|
-
import { type AppMode, getInitialAppMode } from "../mode";
|
|
18
15
|
import { PyodideBridge } from "./bridge";
|
|
19
|
-
import { hasAnyOutputAtom, wasmInitializationAtom } from "./state";
|
|
20
16
|
import { isWasm } from "./utils";
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
|
-
*
|
|
19
|
+
* Paints the snapshot (or hydrated cells) immediately and lets Pyodide
|
|
20
|
+
* finish downloading in the background. UI elements aren't interactive
|
|
21
|
+
* until Pyodide is ready.
|
|
24
22
|
*/
|
|
25
23
|
export const PyodideLoader: React.FC<PropsWithChildren> = ({ children }) => {
|
|
26
24
|
if (!isWasm()) {
|
|
27
25
|
return children;
|
|
28
26
|
}
|
|
29
|
-
|
|
30
27
|
return <PyodideLoaderInner>{children}</PyodideLoaderInner>;
|
|
31
28
|
};
|
|
32
29
|
|
|
33
30
|
const PyodideLoaderInner: React.FC<PropsWithChildren> = ({ children }) => {
|
|
34
|
-
// Don't block render on Pyodide: a hydrated snapshot can paint immediately
|
|
35
|
-
// while Pyodide downloads in the background.
|
|
36
31
|
const { error } = useAsyncData(async () => {
|
|
37
32
|
await PyodideBridge.INSTANCE.initialized.promise;
|
|
38
33
|
return true;
|
|
39
34
|
}, []);
|
|
40
35
|
|
|
41
|
-
const
|
|
42
|
-
const hasOutput = useAtomValue(hasAnyOutputAtom);
|
|
43
|
-
const nothingToShow = shouldShowSpinner({
|
|
44
|
-
hasCells,
|
|
45
|
-
hasOutput,
|
|
46
|
-
mode: getInitialAppMode(),
|
|
47
|
-
codeHidden: isCodeHidden(),
|
|
48
|
-
});
|
|
36
|
+
const canPaint = useAtomValue(canPaintAtom);
|
|
49
37
|
|
|
38
|
+
// If the snapshot is already on screen, toast instead of throwing so we
|
|
39
|
+
// don't tear it down. With nothing painted, throw to surface the error UI.
|
|
50
40
|
const didToastErrorRef = useRef(false);
|
|
51
41
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
// snapshot stays readable. The ref ensures we only toast once even if
|
|
54
|
-
// nothingToShow toggles later.
|
|
55
|
-
if (error && !nothingToShow && !didToastErrorRef.current) {
|
|
42
|
+
if (error && canPaint && !didToastErrorRef.current) {
|
|
56
43
|
didToastErrorRef.current = true;
|
|
57
44
|
Logger.error("Pyodide failed to initialize", error);
|
|
58
45
|
toast({
|
|
@@ -61,50 +48,21 @@ const PyodideLoaderInner: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
61
48
|
variant: "danger",
|
|
62
49
|
});
|
|
63
50
|
}
|
|
64
|
-
}, [error,
|
|
51
|
+
}, [error, canPaint]);
|
|
65
52
|
|
|
66
|
-
if (error &&
|
|
53
|
+
if (error && !canPaint) {
|
|
67
54
|
throw error;
|
|
68
55
|
}
|
|
69
56
|
|
|
70
|
-
if (nothingToShow) {
|
|
71
|
-
return <WasmSpinner />;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return children;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
function isCodeHidden() {
|
|
78
|
-
// Code is hidden if ANY are true:
|
|
79
|
-
// - the query param is set to false
|
|
80
|
-
// - the view.showAppCode is false
|
|
81
57
|
return (
|
|
82
|
-
|
|
83
|
-
|
|
58
|
+
<ProgressiveBoundary requires={canPaintAtom} fallback={<WasmSpinner />}>
|
|
59
|
+
{children}
|
|
60
|
+
</ProgressiveBoundary>
|
|
84
61
|
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Pure predicate: should the WASM loader render a spinner instead of its
|
|
89
|
-
* children? We block render only when nothing user-visible would appear:
|
|
90
|
-
* - no cells have been hydrated (Pyodide hasn't parsed the notebook), or
|
|
91
|
-
* - we are in headless run mode (code hidden) with no outputs to display.
|
|
92
|
-
*/
|
|
93
|
-
export function shouldShowSpinner(input: {
|
|
94
|
-
hasCells: boolean;
|
|
95
|
-
hasOutput: boolean;
|
|
96
|
-
mode: AppMode;
|
|
97
|
-
codeHidden: boolean;
|
|
98
|
-
}): boolean {
|
|
99
|
-
const { hasCells, hasOutput, mode, codeHidden } = input;
|
|
100
|
-
if (!hasCells) {
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
return !hasOutput && mode === "read" && codeHidden;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export const WasmSpinner: React.FC<PropsWithChildren> = ({ children }) => {
|
|
107
|
-
const wasmInitialization = useAtomValue(wasmInitializationAtom);
|
|
62
|
+
};
|
|
108
63
|
|
|
109
|
-
|
|
64
|
+
const WasmSpinner: React.FC = () => {
|
|
65
|
+
const state = useAtomValue(wasmInitStateAtom);
|
|
66
|
+
const title = state.kind === "loading" ? state.message : "Loading…";
|
|
67
|
+
return <LargeSpinner title={title} />;
|
|
110
68
|
};
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type { IConnectionTransport } from "../websocket/transports/transport";
|
|
|
38
38
|
import { PyodideRouter } from "./router";
|
|
39
39
|
import { getWorkerRPC } from "./rpc";
|
|
40
40
|
import { createShareableLink } from "./share";
|
|
41
|
-
import {
|
|
41
|
+
import { wasmInitStateAtom } from "./state";
|
|
42
42
|
import { fallbackFileStore, notebookFileStore } from "./store";
|
|
43
43
|
import { isWasm } from "./utils";
|
|
44
44
|
import type { SaveWorkerSchema } from "./worker/save-worker";
|
|
@@ -120,11 +120,16 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
120
120
|
// By initializing after, we get hits on cached network requests
|
|
121
121
|
this.saveRpc = this.getSaveWorker();
|
|
122
122
|
this.setInterruptBuffer();
|
|
123
|
-
store.set(
|
|
123
|
+
store.set(wasmInitStateAtom, { kind: "ready" });
|
|
124
124
|
this.initialized.resolve();
|
|
125
125
|
});
|
|
126
126
|
this.rpc.addMessageListener("initializingMessage", ({ message }) => {
|
|
127
|
-
|
|
127
|
+
// Only bump the progress label while still loading — if we've already
|
|
128
|
+
// reached "ready" or "error", a late message shouldn't roll us back.
|
|
129
|
+
const current = store.get(wasmInitStateAtom);
|
|
130
|
+
if (current.kind === "loading") {
|
|
131
|
+
store.set(wasmInitStateAtom, { kind: "loading", message });
|
|
132
|
+
}
|
|
128
133
|
});
|
|
129
134
|
this.rpc.addMessageListener("initializedError", ({ error }) => {
|
|
130
135
|
// If already initialized, surface as a toast and leave the deferred /
|
|
@@ -138,7 +143,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
138
143
|
});
|
|
139
144
|
return;
|
|
140
145
|
}
|
|
141
|
-
store.set(
|
|
146
|
+
store.set(wasmInitStateAtom, { kind: "error", message: error });
|
|
142
147
|
this.initialized.reject(new Error(error));
|
|
143
148
|
});
|
|
144
149
|
this.rpc.addMessageListener("kernelMessage", ({ message }) => {
|
package/src/core/wasm/state.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { atom } from "jotai";
|
|
3
|
-
import { notebookAtom } from "../cells/cells";
|
|
4
|
-
import { isOutputEmpty } from "../cells/outputs";
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
/** Tagged so the progress label and error message can't drift apart. */
|
|
5
|
+
export type WasmInitState =
|
|
6
|
+
| { kind: "loading"; message: string }
|
|
7
|
+
| { kind: "ready" }
|
|
8
|
+
| { kind: "error"; message: string };
|
|
7
9
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export const hasAnyOutputAtom = atom<boolean>((get) => {
|
|
12
|
-
const notebook = get(notebookAtom);
|
|
13
|
-
const runtimeStates = Object.values(notebook.cellRuntime);
|
|
14
|
-
// First check if there is any output
|
|
15
|
-
const hasOutput = runtimeStates.some(
|
|
16
|
-
(runtime) => !isOutputEmpty(runtime.output),
|
|
17
|
-
);
|
|
18
|
-
if (hasOutput) {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
// If there is no output, check if they have all run to completion
|
|
22
|
-
// in case this notebook doesn't contain outputs
|
|
23
|
-
return runtimeStates.every((runtime) => runtime.status === "idle");
|
|
10
|
+
export const wasmInitStateAtom = atom<WasmInitState>({
|
|
11
|
+
kind: "loading",
|
|
12
|
+
message: "Initializing...",
|
|
24
13
|
});
|
package/src/core/wasm/store.ts
CHANGED
|
@@ -62,7 +62,7 @@ const remoteDefaultFileStore: FileStore = {
|
|
|
62
62
|
if (window.location.hostname !== "marimo.app") {
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
-
const url = new URL("
|
|
65
|
+
const url = new URL("export_demos/wasm-intro.py", document.baseURI);
|
|
66
66
|
return fetch(url.toString())
|
|
67
67
|
.then((res) => (res.ok ? res.text() : null))
|
|
68
68
|
.catch(() => null);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { useDelayElapsed } from "../useDelayElapsed";
|
|
6
|
+
|
|
7
|
+
describe("useDelayElapsed", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("resolves true immediately when delay <= 0", () => {
|
|
17
|
+
const { result } = renderHook(() => useDelayElapsed(0));
|
|
18
|
+
expect(result.current).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("stays false until the delay elapses, then flips true", () => {
|
|
22
|
+
const { result } = renderHook(() => useDelayElapsed(2000));
|
|
23
|
+
expect(result.current).toBe(false);
|
|
24
|
+
|
|
25
|
+
act(() => vi.advanceTimersByTime(1999));
|
|
26
|
+
expect(result.current).toBe(false);
|
|
27
|
+
|
|
28
|
+
act(() => vi.advanceTimersByTime(1));
|
|
29
|
+
expect(result.current).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("restarts the timer when the delay changes", () => {
|
|
33
|
+
const { result, rerender } = renderHook(
|
|
34
|
+
({ delay }) => useDelayElapsed(delay),
|
|
35
|
+
{ initialProps: { delay: 1000 } },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
act(() => vi.advanceTimersByTime(1000));
|
|
39
|
+
expect(result.current).toBe(true);
|
|
40
|
+
|
|
41
|
+
rerender({ delay: 500 });
|
|
42
|
+
expect(result.current).toBe(false);
|
|
43
|
+
|
|
44
|
+
act(() => vi.advanceTimersByTime(500));
|
|
45
|
+
expect(result.current).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("clears the timer on unmount", () => {
|
|
49
|
+
const { unmount } = renderHook(() => useDelayElapsed(1000));
|
|
50
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
51
|
+
unmount();
|
|
52
|
+
// The pending timeout must be cleared, not left dangling.
|
|
53
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns `false` until `delayMs` has elapsed since mount (or since `delayMs`
|
|
7
|
+
* last changed), then `true`. Resolves to `true` immediately when
|
|
8
|
+
* `delayMs <= 0`. Useful for suppressing spinner/fallback flashes.
|
|
9
|
+
*/
|
|
10
|
+
export function useDelayElapsed(delayMs: number): boolean {
|
|
11
|
+
const [elapsed, setElapsed] = useState(delayMs <= 0);
|
|
12
|
+
|
|
13
|
+
// Layout effect (not a plain effect) so that when `delayMs` flips from `<= 0`
|
|
14
|
+
// to `> 0` — e.g. a gate re-closing — we reset `elapsed` to `false` before
|
|
15
|
+
// the browser paints, rather than briefly flashing the fallback for a frame.
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
if (delayMs <= 0) {
|
|
18
|
+
setElapsed(true);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setElapsed(false);
|
|
22
|
+
const timeout = setTimeout(() => setElapsed(true), delayMs);
|
|
23
|
+
return () => clearTimeout(timeout);
|
|
24
|
+
}, [delayMs]);
|
|
25
|
+
|
|
26
|
+
return elapsed;
|
|
27
|
+
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { shouldShowSpinner } from "../PyodideLoader";
|
|
5
|
-
|
|
6
|
-
describe("shouldShowSpinner", () => {
|
|
7
|
-
it("shows the spinner when there are no cells yet (Pyodide hasn't parsed)", () => {
|
|
8
|
-
expect(
|
|
9
|
-
shouldShowSpinner({
|
|
10
|
-
hasCells: false,
|
|
11
|
-
hasOutput: false,
|
|
12
|
-
mode: "read",
|
|
13
|
-
codeHidden: false,
|
|
14
|
-
}),
|
|
15
|
-
).toBe(true);
|
|
16
|
-
|
|
17
|
-
expect(
|
|
18
|
-
shouldShowSpinner({
|
|
19
|
-
hasCells: false,
|
|
20
|
-
hasOutput: true,
|
|
21
|
-
mode: "edit",
|
|
22
|
-
codeHidden: false,
|
|
23
|
-
}),
|
|
24
|
-
).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("renders children once cells exist with code visible", () => {
|
|
28
|
-
// run mode, code visible, no outputs yet — the user can read the code
|
|
29
|
-
expect(
|
|
30
|
-
shouldShowSpinner({
|
|
31
|
-
hasCells: true,
|
|
32
|
-
hasOutput: false,
|
|
33
|
-
mode: "read",
|
|
34
|
-
codeHidden: false,
|
|
35
|
-
}),
|
|
36
|
-
).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("renders children once cells exist with cached outputs (snapshot case)", () => {
|
|
40
|
-
expect(
|
|
41
|
-
shouldShowSpinner({
|
|
42
|
-
hasCells: true,
|
|
43
|
-
hasOutput: true,
|
|
44
|
-
mode: "read",
|
|
45
|
-
codeHidden: true,
|
|
46
|
-
}),
|
|
47
|
-
).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("keeps the spinner up in headless run mode with no outputs", () => {
|
|
51
|
-
// read mode + code hidden + no outputs = nothing visible to render
|
|
52
|
-
expect(
|
|
53
|
-
shouldShowSpinner({
|
|
54
|
-
hasCells: true,
|
|
55
|
-
hasOutput: false,
|
|
56
|
-
mode: "read",
|
|
57
|
-
codeHidden: true,
|
|
58
|
-
}),
|
|
59
|
-
).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("never blocks edit mode once cells exist", () => {
|
|
63
|
-
expect(
|
|
64
|
-
shouldShowSpinner({
|
|
65
|
-
hasCells: true,
|
|
66
|
-
hasOutput: false,
|
|
67
|
-
mode: "edit",
|
|
68
|
-
codeHidden: true,
|
|
69
|
-
}),
|
|
70
|
-
).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { createRef } from "react";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { cellId } from "@/__tests__/branded";
|
|
6
|
-
import type { NotebookState } from "@/core/cells/cells";
|
|
7
|
-
import { initialNotebookState, notebookAtom } from "@/core/cells/cells";
|
|
8
|
-
import { createCell, createCellRuntimeState } from "@/core/cells/types";
|
|
9
|
-
import type { OutputMessage } from "@/core/kernel/messages";
|
|
10
|
-
import { store } from "@/core/state/jotai";
|
|
11
|
-
import { CollapsibleTree, MultiColumn } from "@/utils/id-tree";
|
|
12
|
-
import { hasAnyOutputAtom } from "../state";
|
|
13
|
-
|
|
14
|
-
describe("hasAnyOutputAtom", () => {
|
|
15
|
-
const createNotebookState = (
|
|
16
|
-
outputs: (OutputMessage | null)[],
|
|
17
|
-
): NotebookState => ({
|
|
18
|
-
...initialNotebookState(),
|
|
19
|
-
cellIds: new MultiColumn([
|
|
20
|
-
CollapsibleTree.from(outputs.map((_, i) => cellId(`${i}`))),
|
|
21
|
-
]),
|
|
22
|
-
cellData: Object.fromEntries(
|
|
23
|
-
outputs.map((_, i) => [
|
|
24
|
-
cellId(`${i}`),
|
|
25
|
-
createCell({ id: cellId(`${i}`) }),
|
|
26
|
-
]),
|
|
27
|
-
),
|
|
28
|
-
cellRuntime: Object.fromEntries(
|
|
29
|
-
outputs.map((output, i) => [
|
|
30
|
-
`${i}`,
|
|
31
|
-
createCellRuntimeState({
|
|
32
|
-
output,
|
|
33
|
-
status: "queued",
|
|
34
|
-
outline: { items: [] },
|
|
35
|
-
}),
|
|
36
|
-
]),
|
|
37
|
-
),
|
|
38
|
-
cellHandles: Object.fromEntries(
|
|
39
|
-
outputs.map((_, i) => [cellId(`${i}`), createRef()]),
|
|
40
|
-
),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("should return false when there are no outputs", () => {
|
|
44
|
-
store.set(notebookAtom, createNotebookState([null, null]));
|
|
45
|
-
expect(store.get(hasAnyOutputAtom)).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should return false when all outputs are empty", () => {
|
|
49
|
-
store.set(
|
|
50
|
-
notebookAtom,
|
|
51
|
-
createNotebookState([
|
|
52
|
-
{ channel: "output", mimetype: "text/plain", data: "", timestamp: 0 },
|
|
53
|
-
{ channel: "output", mimetype: "text/plain", data: "", timestamp: 0 },
|
|
54
|
-
]),
|
|
55
|
-
);
|
|
56
|
-
expect(store.get(hasAnyOutputAtom)).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("should return true when there is at least one non-empty output", () => {
|
|
60
|
-
store.set(
|
|
61
|
-
notebookAtom,
|
|
62
|
-
createNotebookState([
|
|
63
|
-
{ channel: "output", mimetype: "text/plain", data: "", timestamp: 0 },
|
|
64
|
-
{
|
|
65
|
-
channel: "output",
|
|
66
|
-
mimetype: "text/plain",
|
|
67
|
-
data: "hello",
|
|
68
|
-
timestamp: 0,
|
|
69
|
-
},
|
|
70
|
-
]),
|
|
71
|
-
);
|
|
72
|
-
expect(store.get(hasAnyOutputAtom)).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("should handle various output types", () => {
|
|
76
|
-
store.set(
|
|
77
|
-
notebookAtom,
|
|
78
|
-
createNotebookState([
|
|
79
|
-
{
|
|
80
|
-
channel: "output",
|
|
81
|
-
mimetype: "application/json",
|
|
82
|
-
data: { foo: "bar" },
|
|
83
|
-
timestamp: 0,
|
|
84
|
-
},
|
|
85
|
-
{ channel: "output", mimetype: "text/plain", data: "", timestamp: 0 },
|
|
86
|
-
]),
|
|
87
|
-
);
|
|
88
|
-
expect(store.get(hasAnyOutputAtom)).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("should return true when all outputs are idle", () => {
|
|
92
|
-
const notebookState = createNotebookState([null, null]);
|
|
93
|
-
const cellId0 = cellId("0");
|
|
94
|
-
const cellId1 = cellId("1");
|
|
95
|
-
// Some idle cell, so returns false
|
|
96
|
-
store.set(notebookAtom, {
|
|
97
|
-
...notebookState,
|
|
98
|
-
cellRuntime: {
|
|
99
|
-
...notebookState.cellRuntime,
|
|
100
|
-
[cellId0]: {
|
|
101
|
-
...notebookState.cellRuntime[cellId0],
|
|
102
|
-
status: "idle",
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
expect(store.get(hasAnyOutputAtom)).toBe(false);
|
|
107
|
-
|
|
108
|
-
// All cells are idle, so returns true
|
|
109
|
-
store.set(notebookAtom, {
|
|
110
|
-
...notebookState,
|
|
111
|
-
cellRuntime: {
|
|
112
|
-
[cellId0]: {
|
|
113
|
-
...notebookState.cellRuntime[cellId0],
|
|
114
|
-
status: "idle",
|
|
115
|
-
},
|
|
116
|
-
[cellId1]: {
|
|
117
|
-
...notebookState.cellRuntime[cellId1],
|
|
118
|
-
status: "idle",
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
expect(store.get(hasAnyOutputAtom)).toBe(true);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
File without changes
|