@marimo-team/islands 0.23.15-dev32 → 0.23.15-dev4
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-7p7rt9iw.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-DE6tzZf-.js} +1 -1
- package/dist/{ImageComparisonComponent-1i5IBhon.js → ImageComparisonComponent-CHrI72em.js} +129 -173
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/worker-DAWRHcPq.js +73 -0
- package/dist/{chat-ui-C0wkSwxB.js → chat-ui-DinOvbWu.js} +3111 -3111
- package/dist/{code-visibility-CEHWVGz1.js → code-visibility-Dk8cVOny.js} +864 -946
- package/dist/{debounce-BOD3DbfP.js → constants-T20xxyNf.js} +24 -1
- package/dist/{formats-BX3uDQbB.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-CjTu7ukN.js} +3 -3
- package/dist/{html-to-image-D8Q5SLSJ.js → html-to-image-_wGfk8V-.js} +2241 -2277
- package/dist/{input-BGPrFH3g.js → input-DtsN7xm-.js} +2 -2
- package/dist/main.js +1201 -1297
- package/dist/{mermaid-UdmxG2PZ.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{process-output-C_A12w9J.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-kLDhMoa2.js → reveal-component-B5DXLyO7.js} +605 -618
- package/dist/{spec-DSs9v0xx.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CKpRx4TS.js → toDate-CWNNlFEX.js} +4 -3
- package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-Df_vGflw.js → useTheme-6eZ3GOTS.js} +27 -62
- package/dist/{vega-component-DNHEV0u0.js → vega-component-DzyyM9fc.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +1 -244
- package/src/components/chat/chat-panel.tsx +67 -196
- package/src/components/chat/chat-utils.ts +2 -111
- package/src/components/editor/SortableCell.tsx +2 -6
- package/src/components/editor/actions/pair-with-agent-modal.tsx +64 -20
- package/src/components/editor/ai/add-cell-with-ai.tsx +5 -14
- package/src/components/editor/ai/ai-completion-editor.tsx +1 -4
- package/src/components/editor/cell/cell-context-menu.tsx +0 -7
- package/src/components/editor/chrome/panels/packages-panel.tsx +1 -11
- package/src/components/editor/columns/cell-column.tsx +4 -34
- package/src/components/editor/columns/sortable-column.tsx +4 -24
- package/src/components/editor/navigation/__tests__/navigation.test.ts +0 -33
- package/src/components/editor/navigation/navigation.ts +1 -8
- package/src/components/editor/notebook-cell.tsx +106 -203
- package/src/components/editor/renderers/cell-array.tsx +13 -26
- package/src/components/editor/renderers/cells-renderer.tsx +2 -8
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -29
- package/src/components/editor/renderers/slides-layout/types.ts +0 -4
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +4 -37
- package/src/components/slides/slide-form.tsx +0 -72
- package/src/core/cells/__tests__/utils.test.ts +0 -59
- package/src/core/cells/utils.ts +0 -51
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +3 -97
- package/src/core/codemirror/completion/signature-hint.ts +11 -78
- package/src/core/constants.ts +0 -6
- package/src/core/islands/__tests__/bridge.test.ts +46 -341
- package/src/core/islands/__tests__/parse.test.ts +0 -105
- package/src/core/islands/bootstrap.ts +3 -8
- package/src/core/islands/bridge.ts +23 -116
- package/src/core/islands/components/web-components.tsx +15 -76
- package/src/core/islands/constants.ts +0 -1
- package/src/core/islands/main.ts +3 -35
- package/src/core/islands/parse.ts +23 -70
- package/src/core/islands/worker/worker.tsx +57 -145
- package/src/core/wasm/worker/bootstrap.ts +20 -113
- package/src/css/app/Cell.css +0 -10
- package/src/hooks/useHotkey.ts +4 -29
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +9 -6
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +61 -22
- package/src/plugins/impl/anywidget/model-proxy.ts +13 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +34 -4
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +2 -53
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +3 -71
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +0 -1
- package/src/theme/useTheme.ts +1 -16
- package/dist/assets/__vite-browser-external-CjNAy01L.js +0 -1
- package/dist/assets/worker-B3XPCb6y.js +0 -98
- package/src/components/chat/__tests__/message-queue.test.tsx +0 -121
- package/src/components/editor/__tests__/output-persistence.test.tsx +0 -241
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +0 -153
- package/src/components/editor/actions/pair-with-agent-commands.ts +0 -109
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +0 -105
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +0 -66
- package/src/core/islands/components/__tests__/web-components.test.tsx +0 -214
- package/src/core/islands/worker/__tests__/controller.test.ts +0 -173
- package/src/hooks/__tests__/useHotkey.test.tsx +0 -88
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +0 -71
- package/src/theme/__tests__/useTheme.test.ts +0 -68
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
parseIslandElement,
|
|
16
16
|
parseIslandElementsIntoApps,
|
|
17
17
|
parseMarimoIslandApps,
|
|
18
|
-
retainIslandSource,
|
|
19
18
|
} from "../parse";
|
|
20
19
|
import { createMockIslandElement, createMockIslands } from "./test-utils.tsx";
|
|
21
20
|
|
|
@@ -54,7 +53,6 @@ function appendPayload(
|
|
|
54
53
|
script.type = ISLANDS_JSON_SCRIPT_TYPE;
|
|
55
54
|
script.textContent = JSON.stringify(payload);
|
|
56
55
|
root.appendChild(script);
|
|
57
|
-
return script;
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
describe("createMarimoFile", () => {
|
|
@@ -319,39 +317,6 @@ describe("parseIslandElement", () => {
|
|
|
319
317
|
|
|
320
318
|
expect(result).toBeNull();
|
|
321
319
|
});
|
|
322
|
-
|
|
323
|
-
it("uses retained source after the custom element renders", () => {
|
|
324
|
-
const element = document.createElement(ISLAND_TAG_NAMES.ISLAND);
|
|
325
|
-
retainIslandSource(element, {
|
|
326
|
-
code: 'print("retained")',
|
|
327
|
-
output: "<div>retained output</div>",
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
expect(parseIslandElement(element)).toEqual({
|
|
331
|
-
code: 'print("retained")',
|
|
332
|
-
output: "<div>retained output</div>",
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
retainIslandSource(element, { code: "", output: "<div>static</div>" });
|
|
336
|
-
expect(parseIslandElement(element)).toBeNull();
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("prefers new live source when an island element is reused", () => {
|
|
340
|
-
const element = createMockIslandElement({
|
|
341
|
-
code: 'print("new")',
|
|
342
|
-
innerHTML: "<div>new output</div>",
|
|
343
|
-
});
|
|
344
|
-
element.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
345
|
-
retainIslandSource(element, {
|
|
346
|
-
code: 'print("retained")',
|
|
347
|
-
output: "<div>retained output</div>",
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
expect(parseIslandElement(element)).toEqual({
|
|
351
|
-
code: 'print("new")',
|
|
352
|
-
output: "<div>new output</div>",
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
320
|
});
|
|
356
321
|
|
|
357
322
|
describe("parseIslandElementsIntoApps", () => {
|
|
@@ -665,76 +630,6 @@ describe("parseMarimoIslandApps", () => {
|
|
|
665
630
|
);
|
|
666
631
|
});
|
|
667
632
|
|
|
668
|
-
it("preserves payload-backed cells after a non-materializing capability probe", () => {
|
|
669
|
-
const island = createMockIslandElement({
|
|
670
|
-
appId: "app1",
|
|
671
|
-
cellId: "cell-2",
|
|
672
|
-
code: "dom_code = True",
|
|
673
|
-
innerHTML: "<div>dom output</div>",
|
|
674
|
-
});
|
|
675
|
-
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
676
|
-
container.appendChild(island);
|
|
677
|
-
appendPayload(container, {
|
|
678
|
-
schemaVersion: 1,
|
|
679
|
-
appId: "app1",
|
|
680
|
-
cells: [createPayloadCell({ cellId: "cell-2" })],
|
|
681
|
-
});
|
|
682
|
-
const originalIsland = island.outerHTML;
|
|
683
|
-
|
|
684
|
-
const probed = parseMarimoIslandApps(container, { materialize: false });
|
|
685
|
-
|
|
686
|
-
expect(probed).toHaveLength(1);
|
|
687
|
-
expect(island.outerHTML).toBe(originalIsland);
|
|
688
|
-
|
|
689
|
-
expect(parseMarimoIslandApps(container)).toEqual(probed);
|
|
690
|
-
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID)).toBeNull();
|
|
691
|
-
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
it("refreshes a reused payload anchor when its content changes", () => {
|
|
695
|
-
const island = createMockIslandElement({
|
|
696
|
-
appId: "app1",
|
|
697
|
-
cellId: "cell-1",
|
|
698
|
-
code: 'print("dom")',
|
|
699
|
-
innerHTML: "<div>dom output</div>",
|
|
700
|
-
});
|
|
701
|
-
island.insertAdjacentHTML(
|
|
702
|
-
"beforeend",
|
|
703
|
-
'<div data-marimo-element><marimo-code-editor data-initial-value="old"></marimo-code-editor></div>',
|
|
704
|
-
);
|
|
705
|
-
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
706
|
-
container.appendChild(island);
|
|
707
|
-
const script = appendPayload(container, {
|
|
708
|
-
schemaVersion: 1,
|
|
709
|
-
appId: "app1",
|
|
710
|
-
cells: [createPayloadCell()],
|
|
711
|
-
});
|
|
712
|
-
parseMarimoIslandApps(container);
|
|
713
|
-
|
|
714
|
-
script.textContent = JSON.stringify({
|
|
715
|
-
schemaVersion: 1,
|
|
716
|
-
appId: "app1",
|
|
717
|
-
cells: [
|
|
718
|
-
createPayloadCell({
|
|
719
|
-
code: 'print("updated")',
|
|
720
|
-
outputHtml: "<div>updated output</div>",
|
|
721
|
-
}),
|
|
722
|
-
],
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
parseMarimoIslandApps(container);
|
|
726
|
-
|
|
727
|
-
expect(extractIslandCodeFromEmbed(island)).toBe('print("updated")');
|
|
728
|
-
expect(island.querySelector(ISLAND_TAG_NAMES.CELL_OUTPUT)?.innerHTML).toBe(
|
|
729
|
-
"<div>updated output</div>",
|
|
730
|
-
);
|
|
731
|
-
expect(
|
|
732
|
-
island
|
|
733
|
-
.querySelector(ISLAND_TAG_NAMES.CODE_EDITOR)
|
|
734
|
-
?.getAttribute("data-initial-value"),
|
|
735
|
-
).toBe(JSON.stringify('print("updated")'));
|
|
736
|
-
});
|
|
737
|
-
|
|
738
633
|
it("should parse Python-generated island payload snapshots", () => {
|
|
739
634
|
const html = readFileSync(
|
|
740
635
|
new URL(
|
|
@@ -108,17 +108,14 @@ export async function initializeIslands(
|
|
|
108
108
|
// Loading indicator: dim islands while Pyodide initializes
|
|
109
109
|
store.sub(shouldShowIslandsWarningIndicatorAtom, () => {
|
|
110
110
|
const showing = store.get(shouldShowIslandsWarningIndicatorAtom);
|
|
111
|
-
const currentIslands = root.querySelectorAll<HTMLElement>(
|
|
112
|
-
ISLAND_TAG_NAMES.ISLAND,
|
|
113
|
-
);
|
|
114
111
|
if (showing) {
|
|
115
112
|
toastIslandsLoading();
|
|
116
|
-
for (const island of
|
|
113
|
+
for (const island of islands) {
|
|
117
114
|
island.style.setProperty("opacity", "0.5");
|
|
118
115
|
}
|
|
119
116
|
} else {
|
|
120
117
|
dismissIslandsLoadingToast();
|
|
121
|
-
for (const island of
|
|
118
|
+
for (const island of islands) {
|
|
122
119
|
island.style.removeProperty("opacity");
|
|
123
120
|
}
|
|
124
121
|
}
|
|
@@ -211,9 +208,7 @@ function handleMessage(
|
|
|
211
208
|
setKernelState: Functions.NOOP,
|
|
212
209
|
onError: Logger.error,
|
|
213
210
|
});
|
|
214
|
-
|
|
215
|
-
defineCustomElement(ISLAND_TAG_NAMES.ISLAND, MarimoIslandElement);
|
|
216
|
-
}
|
|
211
|
+
defineCustomElement(ISLAND_TAG_NAMES.ISLAND, MarimoIslandElement);
|
|
217
212
|
return;
|
|
218
213
|
|
|
219
214
|
case "send-ui-element-message":
|
|
@@ -7,7 +7,6 @@ import { throwNotImplemented } from "@/utils/functions";
|
|
|
7
7
|
import type { JsonString } from "@/utils/json/base64";
|
|
8
8
|
import { Logger } from "@/utils/Logger";
|
|
9
9
|
import { generateUUID } from "@/utils/uuid";
|
|
10
|
-
import { initialNotebookState, notebookAtom } from "../cells/cells";
|
|
11
10
|
import type { CommandMessage, NotificationPayload } from "../kernel/messages";
|
|
12
11
|
import type { EditRequests, RunRequests } from "../network/types";
|
|
13
12
|
import { store as defaultStore } from "../state/jotai";
|
|
@@ -36,12 +35,11 @@ export interface IslandsBridgeConfig {
|
|
|
36
35
|
* Optional root element for parsing islands (for testing)
|
|
37
36
|
*/
|
|
38
37
|
root?: Document | Element;
|
|
39
|
-
}
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Whether to auto-start sessions on worker ready (default: true)
|
|
41
|
+
*/
|
|
42
|
+
autoStartSessions?: boolean;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
/**
|
|
@@ -53,8 +51,8 @@ interface AppSession {
|
|
|
53
51
|
* @example
|
|
54
52
|
* ```ts
|
|
55
53
|
* const bridge = new IslandsPyodideBridge();
|
|
54
|
+
* await bridge.initialized;
|
|
56
55
|
* bridge.consumeMessages(message => console.log(message));
|
|
57
|
-
* await bridge.initializeApps();
|
|
58
56
|
* ```
|
|
59
57
|
*/
|
|
60
58
|
export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
@@ -64,17 +62,14 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
64
62
|
| undefined;
|
|
65
63
|
private readonly store: typeof defaultStore;
|
|
66
64
|
private readonly root: Document | Element;
|
|
67
|
-
private
|
|
68
|
-
private sessionReady = new Deferred<AppSession>();
|
|
69
|
-
private nextSessionGeneration = 0;
|
|
70
|
-
private appTransition = Promise.resolve();
|
|
71
|
-
private workerReady = new Deferred<void>();
|
|
65
|
+
private readonly autoStartSessions: boolean;
|
|
72
66
|
|
|
73
67
|
public initialized = new Deferred<void>();
|
|
74
68
|
|
|
75
69
|
constructor(config: IslandsBridgeConfig = {}) {
|
|
76
70
|
this.store = config.store || defaultStore;
|
|
77
71
|
this.root = config.root || document;
|
|
72
|
+
this.autoStartSessions = config.autoStartSessions ?? true;
|
|
78
73
|
|
|
79
74
|
try {
|
|
80
75
|
const factory = config.workerFactory || new DefaultWorkerFactory();
|
|
@@ -93,7 +88,9 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
93
88
|
*/
|
|
94
89
|
private setupMessageListeners(): void {
|
|
95
90
|
this.rpc.addMessageListener("ready", () => {
|
|
96
|
-
this.
|
|
91
|
+
if (this.autoStartSessions) {
|
|
92
|
+
this.startSessionsForAllApps();
|
|
93
|
+
}
|
|
97
94
|
});
|
|
98
95
|
|
|
99
96
|
this.rpc.addMessageListener("initialized", () => {
|
|
@@ -118,16 +115,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
118
115
|
);
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async startApps(): Promise<void> {
|
|
118
|
+
/**
|
|
119
|
+
* Starts sessions for all apps found in the DOM
|
|
120
|
+
*/
|
|
121
|
+
private startSessionsForAllApps(): void {
|
|
129
122
|
const apps = parseMarimoIslandApps(this.root);
|
|
130
|
-
const managesSingleApp = apps.length === 1;
|
|
131
123
|
Logger.debug(
|
|
132
124
|
`Starting sessions for ${apps.length} app(s):`,
|
|
133
125
|
apps.map((a) => `${a.id} (${a.cells.length} cells)`),
|
|
@@ -142,83 +134,20 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
142
134
|
const notebookCode = exportContext?.notebookCode;
|
|
143
135
|
for (const app of apps) {
|
|
144
136
|
const file = notebookCode || createMarimoFile(app);
|
|
145
|
-
if (
|
|
146
|
-
managesSingleApp &&
|
|
147
|
-
this.session?.appId === app.id &&
|
|
148
|
-
this.session.code === file
|
|
149
|
-
) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
137
|
Logger.debug(`App ${app.id} marimo file:\n`, file);
|
|
153
|
-
|
|
138
|
+
this.startSession({
|
|
154
139
|
code: file,
|
|
155
140
|
appId: app.id,
|
|
156
|
-
|
|
157
|
-
};
|
|
158
|
-
const previousSession = this.session;
|
|
159
|
-
const replacesSession = managesSingleApp && previousSession !== undefined;
|
|
160
|
-
if (replacesSession) {
|
|
161
|
-
this.store.set(notebookAtom, initialNotebookState());
|
|
162
|
-
}
|
|
163
|
-
if (managesSingleApp || !previousSession) {
|
|
164
|
-
if (managesSingleApp && this.sessionReady.status !== "pending") {
|
|
165
|
-
this.sessionReady = new Deferred<AppSession>();
|
|
166
|
-
}
|
|
167
|
-
this.session = {
|
|
168
|
-
...request,
|
|
169
|
-
code: managesSingleApp ? file : undefined,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
const operation = replacesSession
|
|
173
|
-
? this.rpc.proxy.request.replaceSession(request)
|
|
174
|
-
: this.startSession(request);
|
|
175
|
-
try {
|
|
176
|
-
await operation;
|
|
177
|
-
if (this.sessionReady.status === "pending" && this.session) {
|
|
178
|
-
this.sessionReady.resolve(this.session);
|
|
179
|
-
}
|
|
180
|
-
} catch (error) {
|
|
181
|
-
if (this.session?.sessionGeneration === request.sessionGeneration) {
|
|
182
|
-
this.session = previousSession;
|
|
183
|
-
}
|
|
141
|
+
}).catch((error) => {
|
|
184
142
|
Logger.error(`Failed to start session for app ${app.id}:`, error);
|
|
185
|
-
if (managesSingleApp) {
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async stopSession(appId?: string): Promise<void> {
|
|
193
|
-
await this.enqueueAppTransition(async () => {
|
|
194
|
-
const session = this.session;
|
|
195
|
-
if (session?.code === undefined || (appId && session.appId !== appId)) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
await this.rpc.proxy.request.stopSession({
|
|
199
|
-
appId: session.appId,
|
|
200
|
-
sessionGeneration: session.sessionGeneration,
|
|
201
143
|
});
|
|
202
|
-
|
|
203
|
-
this.sessionReady = new Deferred<AppSession>();
|
|
204
|
-
this.store.set(notebookAtom, initialNotebookState());
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private enqueueAppTransition(operation: () => Promise<void>): Promise<void> {
|
|
209
|
-
const result = this.appTransition.then(operation);
|
|
210
|
-
this.appTransition = result.catch(() => undefined);
|
|
211
|
-
return result;
|
|
144
|
+
}
|
|
212
145
|
}
|
|
213
146
|
|
|
214
147
|
/**
|
|
215
148
|
* Starts a new Python session for an app
|
|
216
149
|
*/
|
|
217
|
-
async startSession(opts: {
|
|
218
|
-
code: string;
|
|
219
|
-
appId: string;
|
|
220
|
-
sessionGeneration: number;
|
|
221
|
-
}): Promise<void> {
|
|
150
|
+
async startSession(opts: { code: string; appId: string }): Promise<void> {
|
|
222
151
|
await this.rpc.proxy.request.startSession(opts);
|
|
223
152
|
}
|
|
224
153
|
|
|
@@ -274,19 +203,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
274
203
|
// ============================================================================
|
|
275
204
|
|
|
276
205
|
sendRun: EditRequests["sendRun"] = async (request): Promise<null> => {
|
|
277
|
-
|
|
278
|
-
await this.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
sessionGeneration: session.sessionGeneration,
|
|
206
|
+
await this.rpc.proxy.request.loadPackages(request.codes.join("\n"));
|
|
207
|
+
await this.putControlRequest({
|
|
208
|
+
type: "execute-cells",
|
|
209
|
+
...request,
|
|
282
210
|
});
|
|
283
|
-
await this.putControlRequest(
|
|
284
|
-
{
|
|
285
|
-
type: "execute-cells",
|
|
286
|
-
...request,
|
|
287
|
-
},
|
|
288
|
-
session,
|
|
289
|
-
);
|
|
290
211
|
return null;
|
|
291
212
|
};
|
|
292
213
|
|
|
@@ -357,27 +278,13 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
357
278
|
|
|
358
279
|
// The kernel uses msgspec to parse control requests, which requires a 'type'
|
|
359
280
|
// field for discriminated union deserialization.
|
|
360
|
-
private async putControlRequest(
|
|
361
|
-
operation: CommandMessage,
|
|
362
|
-
session?: AppSession,
|
|
363
|
-
): Promise<void> {
|
|
364
|
-
session ??= await this.getActiveSession();
|
|
281
|
+
private async putControlRequest(operation: CommandMessage): Promise<void> {
|
|
365
282
|
await this.rpc.proxy.request.bridge({
|
|
366
|
-
appId: session.appId,
|
|
367
283
|
functionName: "put_control_request",
|
|
368
284
|
payload: operation,
|
|
369
|
-
sessionGeneration: session.sessionGeneration,
|
|
370
285
|
});
|
|
371
286
|
}
|
|
372
287
|
|
|
373
|
-
private async getActiveSession(): Promise<AppSession> {
|
|
374
|
-
const transition = this.appTransition;
|
|
375
|
-
const session = this.session;
|
|
376
|
-
const ready = this.sessionReady;
|
|
377
|
-
await transition;
|
|
378
|
-
return session ?? ready.promise;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
288
|
/**
|
|
382
289
|
* Cleans up resources (for testing)
|
|
383
290
|
*/
|
|
@@ -5,7 +5,7 @@ import { isValidElement, type JSX } from "react";
|
|
|
5
5
|
import ReactDOM, { type Root } from "react-dom/client";
|
|
6
6
|
import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
|
|
7
7
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
|
-
import {
|
|
8
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
9
9
|
import { OBJECT_ID_ATTR } from "@/core/dom/ui-element-constants";
|
|
10
10
|
import { UI_ELEMENT_REGISTRY } from "@/core/dom/uiregistry";
|
|
11
11
|
import { LocaleProvider } from "@/core/i18n/locale-provider";
|
|
@@ -16,10 +16,9 @@ import { store } from "../../state/jotai";
|
|
|
16
16
|
import {
|
|
17
17
|
ISLAND_CSS_CLASSES,
|
|
18
18
|
ISLAND_DATA_ATTRIBUTES,
|
|
19
|
-
ISLAND_SOURCE_CHANGED_EVENT,
|
|
20
19
|
ISLAND_TAG_NAMES,
|
|
21
20
|
} from "../constants";
|
|
22
|
-
import { extractIslandCodeFromEmbed
|
|
21
|
+
import { extractIslandCodeFromEmbed } from "../parse";
|
|
23
22
|
import { MarimoOutputWrapper } from "./output-wrapper";
|
|
24
23
|
|
|
25
24
|
/**
|
|
@@ -29,6 +28,7 @@ export interface IslandRenderConfig {
|
|
|
29
28
|
html: string;
|
|
30
29
|
codeCallback: () => string;
|
|
31
30
|
editor: JSX.Element | null;
|
|
31
|
+
cellId: CellId | undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -38,12 +38,7 @@ export interface IslandRenderConfig {
|
|
|
38
38
|
* functionality like re-running cells and copying code.
|
|
39
39
|
*/
|
|
40
40
|
export class MarimoIslandElement extends HTMLElement {
|
|
41
|
-
private connectionGeneration = 0;
|
|
42
41
|
private root?: Root;
|
|
43
|
-
private renderConfig?: IslandRenderConfig;
|
|
44
|
-
private runtimeAppId?: string;
|
|
45
|
-
private runtimeCellId?: CellId;
|
|
46
|
-
private unsubscribeCellIds?: () => void;
|
|
47
42
|
|
|
48
43
|
public static readonly tagName = ISLAND_TAG_NAMES.ISLAND;
|
|
49
44
|
public static readonly outputTagName = ISLAND_TAG_NAMES.CELL_OUTPUT;
|
|
@@ -77,9 +72,6 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
77
72
|
* Returns undefined for non-reactive islands (they have no corresponding cell).
|
|
78
73
|
*/
|
|
79
74
|
get cellId(): CellId | undefined {
|
|
80
|
-
if (!this.isReactive) {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
75
|
const cellId = this.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
|
|
84
76
|
if (cellId) {
|
|
85
77
|
return cellId as CellId;
|
|
@@ -102,9 +94,11 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
102
94
|
/**
|
|
103
95
|
* Looks up a cell ID from the notebook state by index
|
|
104
96
|
*/
|
|
105
|
-
private getCellIdFromIndex(idx: number): CellId
|
|
97
|
+
private getCellIdFromIndex(idx: number): CellId {
|
|
106
98
|
const { cellIds } = store.get(notebookAtom);
|
|
107
|
-
|
|
99
|
+
const cellId = cellIds.inOrderIds.at(idx);
|
|
100
|
+
invariant(cellId, `Missing cell ID at index ${idx}`);
|
|
101
|
+
return cellId;
|
|
108
102
|
}
|
|
109
103
|
|
|
110
104
|
/**
|
|
@@ -116,62 +110,18 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
116
110
|
* synchronously from there causes "unmount during render" warnings.
|
|
117
111
|
*/
|
|
118
112
|
connectedCallback(): void {
|
|
119
|
-
|
|
120
|
-
this.
|
|
121
|
-
ISLAND_SOURCE_CHANGED_EVENT,
|
|
122
|
-
this.handleSourceChanged,
|
|
123
|
-
);
|
|
124
|
-
// Capture config synchronously (before children get cleared by createRoot).
|
|
125
|
-
// A reconnected element reuses the source captured on its first mount.
|
|
126
|
-
if (this.querySelector(MarimoIslandElement.outputTagName)) {
|
|
127
|
-
this.renderConfig = this.extractRenderConfig();
|
|
128
|
-
}
|
|
129
|
-
invariant(this.renderConfig, "Missing island render source");
|
|
130
|
-
this.runtimeAppId = this.appId;
|
|
131
|
-
this.runtimeCellId = this.hasAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)
|
|
132
|
-
? this.cellId
|
|
133
|
-
: undefined;
|
|
134
|
-
this.syncCellIdsSubscription();
|
|
113
|
+
// Capture config synchronously (before children get cleared by createRoot)
|
|
114
|
+
const config = this.extractRenderConfig();
|
|
135
115
|
queueMicrotask(() => {
|
|
136
116
|
// Guard against disconnect between connectedCallback and microtask
|
|
137
|
-
if (
|
|
138
|
-
!this.isConnected ||
|
|
139
|
-
connectionGeneration !== this.connectionGeneration
|
|
140
|
-
) {
|
|
117
|
+
if (!this.isConnected) {
|
|
141
118
|
return;
|
|
142
119
|
}
|
|
143
120
|
this.root = ReactDOM.createRoot(this);
|
|
144
|
-
this.renderIsland();
|
|
121
|
+
this.renderIsland(config);
|
|
145
122
|
});
|
|
146
123
|
}
|
|
147
124
|
|
|
148
|
-
private handleSourceChanged = (): void => {
|
|
149
|
-
if (this.querySelector(MarimoIslandElement.outputTagName)) {
|
|
150
|
-
this.renderConfig = this.extractRenderConfig();
|
|
151
|
-
this.querySelector(MarimoIslandElement.outputTagName)?.remove();
|
|
152
|
-
this.querySelector(MarimoIslandElement.codeTagName)?.remove();
|
|
153
|
-
}
|
|
154
|
-
this.runtimeCellId =
|
|
155
|
-
this.isReactive && this.runtimeAppId === this.appId
|
|
156
|
-
? this.cellId
|
|
157
|
-
: undefined;
|
|
158
|
-
this.runtimeAppId = this.appId;
|
|
159
|
-
this.syncCellIdsSubscription();
|
|
160
|
-
this.renderIsland();
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
private syncCellIdsSubscription(): void {
|
|
164
|
-
if (this.isReactive) {
|
|
165
|
-
this.unsubscribeCellIds ??= store.sub(cellIdsAtom, () => {
|
|
166
|
-
this.runtimeCellId = this.cellId;
|
|
167
|
-
this.renderIsland();
|
|
168
|
-
});
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
this.unsubscribeCellIds?.();
|
|
172
|
-
this.unsubscribeCellIds = undefined;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
125
|
/**
|
|
176
126
|
* Extracts configuration needed for rendering
|
|
177
127
|
*/
|
|
@@ -180,7 +130,7 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
180
130
|
const initialOutput = output.innerHTML;
|
|
181
131
|
const optionalEditor = this.getOptionalEditor();
|
|
182
132
|
const code = this.code;
|
|
183
|
-
|
|
133
|
+
const cellId = this.cellId;
|
|
184
134
|
|
|
185
135
|
// Read objectId directly from the DOM before createRoot clears children.
|
|
186
136
|
// optionalEditor is a <RenderHTML> wrapper, so its .props don't carry the
|
|
@@ -202,19 +152,15 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
202
152
|
html: initialOutput,
|
|
203
153
|
codeCallback,
|
|
204
154
|
editor: optionalEditor,
|
|
155
|
+
cellId,
|
|
205
156
|
};
|
|
206
157
|
}
|
|
207
158
|
|
|
208
159
|
/**
|
|
209
160
|
* Renders the island with React
|
|
210
161
|
*/
|
|
211
|
-
private renderIsland(): void {
|
|
212
|
-
const
|
|
213
|
-
if (!config) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const { html, codeCallback, editor } = config;
|
|
217
|
-
const cellId = this.runtimeCellId;
|
|
162
|
+
private renderIsland(config: IslandRenderConfig): void {
|
|
163
|
+
const { html, codeCallback, editor, cellId } = config;
|
|
218
164
|
const alwaysShowRun = !!editor;
|
|
219
165
|
const trimmedHtml = html.trim();
|
|
220
166
|
const isEmpty = trimmedHtml === "<span></span>" || trimmedHtml === "";
|
|
@@ -222,7 +168,6 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
222
168
|
|
|
223
169
|
// Non-reactive islands have no cell in the kernel — just render static HTML
|
|
224
170
|
if (!cellId) {
|
|
225
|
-
this.removeAttribute("data-status");
|
|
226
171
|
this.root?.render(
|
|
227
172
|
<ErrorBoundary>
|
|
228
173
|
<Provider store={store}>
|
|
@@ -297,12 +242,6 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
297
242
|
* Cleanup when element is removed from DOM
|
|
298
243
|
*/
|
|
299
244
|
disconnectedCallback(): void {
|
|
300
|
-
this.removeEventListener(
|
|
301
|
-
ISLAND_SOURCE_CHANGED_EVENT,
|
|
302
|
-
this.handleSourceChanged,
|
|
303
|
-
);
|
|
304
|
-
this.unsubscribeCellIds?.();
|
|
305
|
-
this.unsubscribeCellIds = undefined;
|
|
306
245
|
const root = this.root;
|
|
307
246
|
this.root = undefined;
|
|
308
247
|
// Defer unmount to avoid "unmount during render" race
|
package/src/core/islands/main.ts
CHANGED
|
@@ -14,44 +14,12 @@ import "iconify-icon";
|
|
|
14
14
|
import { Logger } from "@/utils/Logger";
|
|
15
15
|
import { initializeIslands } from "./bootstrap";
|
|
16
16
|
import { getGlobalBridge } from "./bridge";
|
|
17
|
-
import { ISLAND_CSS_CLASSES, ISLAND_TAG_NAMES } from "./constants";
|
|
18
|
-
import { parseMarimoIslandApps } from "./parse";
|
|
19
|
-
|
|
20
|
-
const bridge = getGlobalBridge();
|
|
21
|
-
let bootstrapPromise: Promise<void> | undefined;
|
|
22
|
-
|
|
23
|
-
/** Returns whether the current document can replace its app in this worker. */
|
|
24
|
-
export function canReplaceApp(): boolean {
|
|
25
|
-
if (!document.querySelector(ISLAND_TAG_NAMES.ISLAND)) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
return parseMarimoIslandApps(document, { materialize: false }).length === 1;
|
|
29
|
-
}
|
|
30
17
|
|
|
31
18
|
/**
|
|
32
|
-
*
|
|
19
|
+
* Main entry point for the js bundle for embedded marimo apps.
|
|
33
20
|
*/
|
|
34
|
-
export async function initialize()
|
|
35
|
-
|
|
36
|
-
ISLAND_TAG_NAMES.ISLAND,
|
|
37
|
-
);
|
|
38
|
-
if (islands.length === 0) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
for (const island of islands) {
|
|
42
|
-
island.classList.add(ISLAND_CSS_CLASSES.NAMESPACE);
|
|
43
|
-
}
|
|
44
|
-
bootstrapPromise ??= initializeIslands({ bridge }).catch((error: unknown) => {
|
|
45
|
-
bootstrapPromise = undefined;
|
|
46
|
-
throw error;
|
|
47
|
-
});
|
|
48
|
-
await bootstrapPromise;
|
|
49
|
-
await bridge.initializeApps();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Stops the matching active app session. */
|
|
53
|
-
export async function stopApp(appId?: string): Promise<void> {
|
|
54
|
-
await bridge.stopSession(appId);
|
|
21
|
+
export async function initialize() {
|
|
22
|
+
await initializeIslands({ bridge: getGlobalBridge() });
|
|
55
23
|
}
|
|
56
24
|
|
|
57
25
|
// Auto-initialize on module load
|