@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61
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-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
- package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3630 -1801
- package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -0
- 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/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- 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/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- 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/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- 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/notebook-cell.tsx +7 -0
- package/src/components/editor/output/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- 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/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/components/tracing/tracing.tsx +2 -1
- 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__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/config/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- package/src/core/islands/bridge.ts +1 -0
- 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/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/mount.tsx +3 -3
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- 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/src/plugins/impl/anywidget/schemas.ts +0 -32
package/src/core/mime.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type { OutputMessage } from "./kernel/messages";
|
|
3
3
|
|
|
4
|
+
export function isMarimoErrorsMime(
|
|
5
|
+
mime: OutputMessage["mimetype"] | undefined,
|
|
6
|
+
) {
|
|
7
|
+
return mime === "application/vnd.marimo+error";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isTracebackMime(mime: OutputMessage["mimetype"] | undefined) {
|
|
11
|
+
return mime === "application/vnd.marimo+traceback";
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
export function isErrorMime(mime: OutputMessage["mimetype"] | undefined) {
|
|
5
|
-
return (
|
|
6
|
-
mime === "application/vnd.marimo+error" ||
|
|
7
|
-
mime === "application/vnd.marimo+traceback"
|
|
8
|
-
);
|
|
15
|
+
return isMarimoErrorsMime(mime) || isTracebackMime(mime);
|
|
9
16
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { HTTPError } from "@/utils/errors";
|
|
4
5
|
import { API } from "../api";
|
|
5
6
|
|
|
6
7
|
// Mock fetch globally
|
|
@@ -77,4 +78,20 @@ describe("API", () => {
|
|
|
77
78
|
expect.objectContaining({ method: "POST" }),
|
|
78
79
|
);
|
|
79
80
|
});
|
|
81
|
+
|
|
82
|
+
it("openapi-client error rejects with HTTPError carrying the status", async () => {
|
|
83
|
+
const body = { detail: "This connection is read-only for this action." };
|
|
84
|
+
await expect(
|
|
85
|
+
API.handleResponseReturnNull({
|
|
86
|
+
error: body,
|
|
87
|
+
response: { status: 403, statusText: "Forbidden" } as Response,
|
|
88
|
+
}),
|
|
89
|
+
).rejects.toMatchObject({ status: 403, cause: body });
|
|
90
|
+
await expect(
|
|
91
|
+
API.handleResponseReturnNull({
|
|
92
|
+
error: body,
|
|
93
|
+
response: { status: 403, statusText: "Forbidden" } as Response,
|
|
94
|
+
}),
|
|
95
|
+
).rejects.toBeInstanceOf(HTTPError);
|
|
96
|
+
});
|
|
80
97
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { HTTPError } from "@/utils/errors";
|
|
5
|
+
|
|
6
|
+
const toastMock = vi.fn();
|
|
7
|
+
vi.mock("@/components/ui/use-toast", () => ({
|
|
8
|
+
toast: (...args: unknown[]) => toastMock(...args),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { createErrorToastingRequests } from "../requests-toasting";
|
|
12
|
+
import type { EditRequests, RunRequests } from "../types";
|
|
13
|
+
|
|
14
|
+
function requestsThatReject(error: unknown): EditRequests & RunRequests {
|
|
15
|
+
return createErrorToastingRequests({
|
|
16
|
+
sendRun: vi.fn().mockRejectedValue(error),
|
|
17
|
+
} as unknown as EditRequests & RunRequests);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("createErrorToastingRequests", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
toastMock.mockClear();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("suppresses the error toast for a capability 403", async () => {
|
|
26
|
+
const requests = requestsThatReject(
|
|
27
|
+
new HTTPError(403, "Forbidden", {
|
|
28
|
+
detail: "This connection is read-only for this action.",
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
await expect(requests.sendRun({} as never)).rejects.toBeInstanceOf(
|
|
32
|
+
HTTPError,
|
|
33
|
+
);
|
|
34
|
+
expect(toastMock).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("shows a danger toast for other request errors", async () => {
|
|
38
|
+
const requests = requestsThatReject(new HTTPError(500, "Server Error"));
|
|
39
|
+
await expect(requests.sendRun({} as never)).rejects.toBeInstanceOf(
|
|
40
|
+
HTTPError,
|
|
41
|
+
);
|
|
42
|
+
expect(toastMock).toHaveBeenCalledWith(
|
|
43
|
+
expect.objectContaining({ variant: "danger" }),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/core/network/api.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { createMarimoClient } from "@marimo-team/marimo-api";
|
|
4
|
+
import { HTTPError } from "../../utils/errors";
|
|
4
5
|
import { Logger } from "../../utils/Logger";
|
|
5
6
|
import { Strings } from "../../utils/strings";
|
|
6
7
|
import { getRuntimeManager } from "../runtime/config";
|
|
@@ -50,7 +51,7 @@ export const API = {
|
|
|
50
51
|
const errorBody = isJson
|
|
51
52
|
? await response.json()
|
|
52
53
|
: await response.text();
|
|
53
|
-
throw new
|
|
54
|
+
throw new HTTPError(response.status, response.statusText, errorBody);
|
|
54
55
|
}
|
|
55
56
|
if (isJson) {
|
|
56
57
|
return response.json() as RESP;
|
|
@@ -83,7 +84,7 @@ export const API = {
|
|
|
83
84
|
})
|
|
84
85
|
.then((response) => {
|
|
85
86
|
if (!response.ok) {
|
|
86
|
-
throw new
|
|
87
|
+
throw new HTTPError(response.status, response.statusText);
|
|
87
88
|
}
|
|
88
89
|
if (
|
|
89
90
|
response.headers.get("Content-Type")?.startsWith("application/json")
|
|
@@ -108,8 +109,13 @@ export const API = {
|
|
|
108
109
|
response: Response;
|
|
109
110
|
}): Promise<T> => {
|
|
110
111
|
if (response.error) {
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
return Promise.reject(
|
|
113
|
+
new HTTPError(
|
|
114
|
+
response.response.status,
|
|
115
|
+
response.response.statusText,
|
|
116
|
+
response.error,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
113
119
|
}
|
|
114
120
|
return Promise.resolve(response.data as T);
|
|
115
121
|
},
|
|
@@ -118,8 +124,13 @@ export const API = {
|
|
|
118
124
|
response: Response;
|
|
119
125
|
}): Promise<null> => {
|
|
120
126
|
if (response.error) {
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
return Promise.reject(
|
|
128
|
+
new HTTPError(
|
|
129
|
+
response.response.status,
|
|
130
|
+
response.response.statusText,
|
|
131
|
+
response.error,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
123
134
|
}
|
|
124
135
|
return Promise.resolve(null);
|
|
125
136
|
},
|
|
@@ -108,6 +108,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
108
108
|
sendStdin: "waitForConnectionOpen",
|
|
109
109
|
sendInterrupt: "waitForConnectionOpen",
|
|
110
110
|
sendPdb: "waitForConnectionOpen",
|
|
111
|
+
sendSetBreakpoints: "waitForConnectionOpen",
|
|
111
112
|
sendInstallMissingPackages: "waitForConnectionOpen",
|
|
112
113
|
readSnippets: "waitForConnectionOpen",
|
|
113
114
|
previewDatasetColumn: "waitForConnectionOpen",
|
|
@@ -293,6 +293,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
293
293
|
})
|
|
294
294
|
.then(handleResponseReturnNull);
|
|
295
295
|
},
|
|
296
|
+
sendSetBreakpoints: (request) => {
|
|
297
|
+
return getClient()
|
|
298
|
+
.POST("/api/kernel/pdb/breakpoints", {
|
|
299
|
+
body: request,
|
|
300
|
+
params: getParams(),
|
|
301
|
+
})
|
|
302
|
+
.then(handleResponseReturnNull);
|
|
303
|
+
},
|
|
296
304
|
sendListFiles: async (request) => {
|
|
297
305
|
await waitForConnectionOpen();
|
|
298
306
|
return getClient()
|
|
@@ -68,6 +68,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
68
68
|
sendListFiles: throwNotInEditMode,
|
|
69
69
|
sendSearchFiles: throwNotInEditMode,
|
|
70
70
|
sendPdb: throwNotInEditMode,
|
|
71
|
+
sendSetBreakpoints: throwNotInEditMode,
|
|
71
72
|
sendCreateFileOrFolder: throwNotInEditMode,
|
|
72
73
|
sendDeleteFileOrFolder: throwNotInEditMode,
|
|
73
74
|
sendCopyFileOrFolder: throwNotInEditMode,
|
|
@@ -5,7 +5,7 @@ import { useAtomValue } from "jotai";
|
|
|
5
5
|
import { Spinner } from "@/components/icons/spinner";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { toast } from "@/components/ui/use-toast";
|
|
8
|
-
import { NoKernelConnectedError, prettyError } from "@/utils/errors";
|
|
8
|
+
import { HTTPError, NoKernelConnectedError, prettyError } from "@/utils/errors";
|
|
9
9
|
import { Logger } from "@/utils/Logger";
|
|
10
10
|
import { useConnectToRuntime } from "../runtime/config";
|
|
11
11
|
import { store } from "../state/jotai";
|
|
@@ -49,6 +49,7 @@ export function createErrorToastingRequests(
|
|
|
49
49
|
sendListFiles: "Failed to list files",
|
|
50
50
|
sendSearchFiles: "Failed to search files",
|
|
51
51
|
sendPdb: "Failed to start debug session",
|
|
52
|
+
sendSetBreakpoints: "", // No toast
|
|
52
53
|
sendCreateFileOrFolder: "Failed to create file or folder",
|
|
53
54
|
sendDeleteFileOrFolder: "Failed to delete file or folder",
|
|
54
55
|
sendCopyFileOrFolder: "Failed to duplicate file or folder",
|
|
@@ -109,6 +110,14 @@ export function createErrorToastingRequests(
|
|
|
109
110
|
throw error;
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
// A capability 403: the connection is read-only for this action. The
|
|
114
|
+
// live capability notification already moved the UI to viewer state,
|
|
115
|
+
// so don't alarm the user with an error toast.
|
|
116
|
+
if (error instanceof HTTPError && error.status === 403) {
|
|
117
|
+
Logger.warn(`Request refused, connection is read-only: ${key}`);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
const title = MESSAGES[keyString];
|
|
113
122
|
const message = prettyError(error);
|
|
114
123
|
if (title) {
|
|
@@ -66,6 +66,7 @@ export type ListDataSourceConnectionRequest =
|
|
|
66
66
|
schemas["ListDataSourceConnectionRequest"];
|
|
67
67
|
export type ValidateSQLRequest = schemas["ValidateSQLRequest"];
|
|
68
68
|
export type DebugCellRequest = schemas["DebugCellRequest"];
|
|
69
|
+
export type SetBreakpointsRequest = schemas["SetBreakpointsRequest"];
|
|
69
70
|
export type ReadCodeResponse = schemas["ReadCodeResponse"];
|
|
70
71
|
export type RecentFilesResponse = schemas["RecentFilesResponse"];
|
|
71
72
|
export type RenameNotebookRequest = schemas["RenameNotebookRequest"];
|
|
@@ -172,6 +173,7 @@ export interface EditRequests {
|
|
|
172
173
|
getUsageStats: () => Promise<UsageResponse>;
|
|
173
174
|
// Debugger
|
|
174
175
|
sendPdb: (request: DebugCellRequest) => Promise<null>;
|
|
176
|
+
sendSetBreakpoints: (request: SetBreakpointsRequest) => Promise<null>;
|
|
175
177
|
// File explorer requests
|
|
176
178
|
sendListFiles: (request: FileListRequest) => Promise<FileListResponse>;
|
|
177
179
|
sendSearchFiles: (request: FileSearchRequest) => Promise<FileSearchResponse>;
|
package/src/core/run-app.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtomValue } from "jotai";
|
|
3
|
+
import { atom, useAtomValue } from "jotai";
|
|
4
4
|
import { ArrowLeftIcon } from "lucide-react";
|
|
5
5
|
import { useEffect } from "react";
|
|
6
6
|
import { AppContainer } from "@/components/editor/app-container";
|
|
7
7
|
import { AppHeader } from "@/components/editor/header/app-header";
|
|
8
|
+
import { ProgressiveBoundary } from "@/components/lifecycle/ProgressiveBoundary";
|
|
8
9
|
import { Spinner } from "@/components/icons/spinner";
|
|
9
10
|
import { buttonVariants } from "@/components/ui/button";
|
|
10
|
-
import { DelayMount } from "@/components/utils/delay-mount";
|
|
11
11
|
import { cn } from "@/utils/cn";
|
|
12
12
|
import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
|
|
13
13
|
import {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import type { AppConfig } from "./config/config-schema";
|
|
19
19
|
import { RuntimeState } from "./kernel/RuntimeState";
|
|
20
20
|
import { getSessionId } from "./kernel/session";
|
|
21
|
+
import { connectionAtom } from "./network/connection";
|
|
21
22
|
import { useRequestClient } from "./network/requests";
|
|
22
23
|
import { isAppConnecting } from "./websocket/connection-utils";
|
|
23
24
|
import { useMarimoKernelConnection } from "./websocket/useMarimoKernelConnection";
|
|
@@ -26,6 +27,16 @@ interface AppProps {
|
|
|
26
27
|
appConfig: AppConfig;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Paint the (possibly empty) app once we have cells, or as soon as we are no
|
|
32
|
+
* longer actively connecting. Without the connection check, a connected or
|
|
33
|
+
* disconnected notebook with no cells would sit on a misleading "Connecting…"
|
|
34
|
+
* spinner instead of rendering the empty app.
|
|
35
|
+
*/
|
|
36
|
+
const canPaintRunAppAtom = atom(
|
|
37
|
+
(get) => get(hasCellsAtom) || !isAppConnecting(get(connectionAtom).state),
|
|
38
|
+
);
|
|
39
|
+
|
|
29
40
|
export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
|
|
30
41
|
const { setCells } = useCellActions();
|
|
31
42
|
const { sendComponentValues } = useRequestClient();
|
|
@@ -45,26 +56,6 @@ export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
|
|
|
45
56
|
});
|
|
46
57
|
|
|
47
58
|
const isRunning = useAtomValue(notebookIsRunningAtom);
|
|
48
|
-
const isConnecting = isAppConnecting(connection.state);
|
|
49
|
-
// Skip the "Connecting..." gate when we already have cells to show — from
|
|
50
|
-
// an embedded snapshot or a prior connection.
|
|
51
|
-
const hasExistingCells = useAtomValue(hasCellsAtom);
|
|
52
|
-
|
|
53
|
-
const renderCells = () => {
|
|
54
|
-
// If we are connecting for more than 2 seconds, show a spinner
|
|
55
|
-
if (isConnecting && !hasExistingCells) {
|
|
56
|
-
return (
|
|
57
|
-
<DelayMount milliseconds={2000} fallback={null}>
|
|
58
|
-
<Spinner className="mx-auto" />
|
|
59
|
-
<p className="text-center text-sm text-muted-foreground mt-2">
|
|
60
|
-
Connecting...
|
|
61
|
-
</p>
|
|
62
|
-
</DelayMount>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return <CellsRenderer appConfig={appConfig} mode="read" />;
|
|
67
|
-
};
|
|
68
59
|
|
|
69
60
|
const galleryHref = (() => {
|
|
70
61
|
if (typeof window === "undefined") {
|
|
@@ -103,7 +94,20 @@ export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
|
|
|
103
94
|
</div>
|
|
104
95
|
)}
|
|
105
96
|
</AppHeader>
|
|
106
|
-
|
|
97
|
+
<ProgressiveBoundary
|
|
98
|
+
requires={canPaintRunAppAtom}
|
|
99
|
+
delay={2000}
|
|
100
|
+
fallback={
|
|
101
|
+
<>
|
|
102
|
+
<Spinner className="mx-auto" />
|
|
103
|
+
<p className="text-center text-sm text-muted-foreground mt-2">
|
|
104
|
+
Connecting...
|
|
105
|
+
</p>
|
|
106
|
+
</>
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
<CellsRenderer appConfig={appConfig} mode="read" />
|
|
110
|
+
</ProgressiveBoundary>
|
|
107
111
|
</AppContainer>
|
|
108
112
|
);
|
|
109
113
|
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { createStore } from "jotai";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { islandsInitializedAtom } from "../../islands/state";
|
|
6
|
+
import { kernelStateAtom } from "../../kernel/state";
|
|
7
|
+
import { connectionAtom } from "../../network/connection";
|
|
8
|
+
import { wasmInitStateAtom } from "../../wasm/state";
|
|
9
|
+
import { WebSocketState } from "../../websocket/types";
|
|
10
|
+
import {
|
|
11
|
+
canRunCellsAtom,
|
|
12
|
+
islandsAdapter,
|
|
13
|
+
remoteAdapter,
|
|
14
|
+
runtimeAdapterAtom,
|
|
15
|
+
staticAdapter,
|
|
16
|
+
wasmAdapter,
|
|
17
|
+
} from "../adapter";
|
|
18
|
+
|
|
19
|
+
describe("wasmAdapter", () => {
|
|
20
|
+
it("reports connecting with the progress label while loading", () => {
|
|
21
|
+
const store = createStore();
|
|
22
|
+
store.set(wasmInitStateAtom, {
|
|
23
|
+
kind: "loading",
|
|
24
|
+
message: "Loading Pyodide…",
|
|
25
|
+
});
|
|
26
|
+
expect(store.get(wasmAdapter.state)).toEqual({
|
|
27
|
+
kind: "connecting",
|
|
28
|
+
progress: { label: "Loading Pyodide…" },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("reports ready when initialized", () => {
|
|
33
|
+
const store = createStore();
|
|
34
|
+
store.set(wasmInitStateAtom, { kind: "ready" });
|
|
35
|
+
expect(store.get(wasmAdapter.state)).toEqual({ kind: "ready" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("exposes the real error message on failure", () => {
|
|
39
|
+
const store = createStore();
|
|
40
|
+
store.set(wasmInitStateAtom, {
|
|
41
|
+
kind: "error",
|
|
42
|
+
message: "ImportError: numpy",
|
|
43
|
+
});
|
|
44
|
+
expect(store.get(wasmAdapter.state)).toEqual({
|
|
45
|
+
kind: "failed",
|
|
46
|
+
error: { message: "ImportError: numpy", errorKind: "init" },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("remoteAdapter", () => {
|
|
52
|
+
it.each([
|
|
53
|
+
[{ state: WebSocketState.OPEN }, { kind: "ready" }],
|
|
54
|
+
[
|
|
55
|
+
{ state: WebSocketState.CONNECTING },
|
|
56
|
+
{ kind: "connecting", progress: { label: "Connecting…" } },
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
{ state: WebSocketState.NOT_STARTED },
|
|
60
|
+
{ kind: "connecting", progress: { label: "Not connected" } },
|
|
61
|
+
],
|
|
62
|
+
] as const)("maps %j to %j", (conn, expected) => {
|
|
63
|
+
const store = createStore();
|
|
64
|
+
store.set(connectionAtom, conn as never);
|
|
65
|
+
expect(store.get(remoteAdapter.state)).toEqual(expected);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("surfaces the close reason when closed", () => {
|
|
69
|
+
const store = createStore();
|
|
70
|
+
store.set(connectionAtom, {
|
|
71
|
+
state: WebSocketState.CLOSED,
|
|
72
|
+
code: "KERNEL_DISCONNECTED",
|
|
73
|
+
reason: "Kernel went away",
|
|
74
|
+
});
|
|
75
|
+
expect(store.get(remoteAdapter.state)).toEqual({
|
|
76
|
+
kind: "failed",
|
|
77
|
+
error: { message: "Kernel went away", errorKind: "runtime" },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("staticAdapter", () => {
|
|
83
|
+
it("is always ready", () => {
|
|
84
|
+
const store = createStore();
|
|
85
|
+
expect(store.get(staticAdapter.state)).toEqual({ kind: "ready" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("advertises no capabilities", () => {
|
|
89
|
+
expect(staticAdapter.capabilities).toEqual({
|
|
90
|
+
canHealthCheck: false,
|
|
91
|
+
canShutdown: false,
|
|
92
|
+
canRestart: false,
|
|
93
|
+
supportsLsp: false,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("islandsAdapter", () => {
|
|
99
|
+
it("is connecting before islands report initialization", () => {
|
|
100
|
+
const store = createStore();
|
|
101
|
+
store.set(islandsInitializedAtom, false);
|
|
102
|
+
expect(store.get(islandsAdapter.state)).toEqual({
|
|
103
|
+
kind: "connecting",
|
|
104
|
+
progress: { label: "Initializing islands…" },
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("is ready once islands initialize", () => {
|
|
109
|
+
const store = createStore();
|
|
110
|
+
store.set(islandsInitializedAtom, true);
|
|
111
|
+
expect(store.get(islandsAdapter.state)).toEqual({ kind: "ready" });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("surfaces the error string when islands fail", () => {
|
|
115
|
+
const store = createStore();
|
|
116
|
+
store.set(islandsInitializedAtom, "wheel download failed");
|
|
117
|
+
expect(store.get(islandsAdapter.state)).toEqual({
|
|
118
|
+
kind: "failed",
|
|
119
|
+
error: { message: "wheel download failed", errorKind: "init" },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("canRunCellsAtom", () => {
|
|
125
|
+
it("is false on a remote adapter until kernel acks", () => {
|
|
126
|
+
const store = createStore();
|
|
127
|
+
// Default adapter in test env is remote.
|
|
128
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
129
|
+
expect(store.get(canRunCellsAtom)).toBe(false);
|
|
130
|
+
|
|
131
|
+
store.set(kernelStateAtom, { isInstantiated: true, error: null });
|
|
132
|
+
expect(store.get(canRunCellsAtom)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("is true on WASM as soon as the adapter is ready (no separate kernel ack)", () => {
|
|
136
|
+
const store = createStore();
|
|
137
|
+
store.set(runtimeAdapterAtom, wasmAdapter);
|
|
138
|
+
store.set(wasmInitStateAtom, { kind: "loading", message: "…" });
|
|
139
|
+
expect(store.get(canRunCellsAtom)).toBe(false);
|
|
140
|
+
|
|
141
|
+
store.set(wasmInitStateAtom, { kind: "ready" });
|
|
142
|
+
expect(store.get(canRunCellsAtom)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("is always false on a static notebook", () => {
|
|
146
|
+
const store = createStore();
|
|
147
|
+
store.set(runtimeAdapterAtom, staticAdapter);
|
|
148
|
+
expect(store.get(canRunCellsAtom)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("is true on islands once initialized", () => {
|
|
152
|
+
const store = createStore();
|
|
153
|
+
store.set(runtimeAdapterAtom, islandsAdapter);
|
|
154
|
+
store.set(islandsInitializedAtom, false);
|
|
155
|
+
expect(store.get(canRunCellsAtom)).toBe(false);
|
|
156
|
+
|
|
157
|
+
store.set(islandsInitializedAtom, true);
|
|
158
|
+
expect(store.get(canRunCellsAtom)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -154,6 +154,51 @@ describe("RuntimeManager", () => {
|
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
describe("getSseURL", () => {
|
|
158
|
+
it("should return an http(s) URL with the session ID", () => {
|
|
159
|
+
const runtime = new RuntimeManager(mockConfig);
|
|
160
|
+
const url = runtime.getSseURL("1234" as SessionId);
|
|
161
|
+
|
|
162
|
+
expect(url.protocol).toBe("https:");
|
|
163
|
+
expect(url.pathname).toBe("/sse");
|
|
164
|
+
expect(url.searchParams.get("session_id")).toBe("1234");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should never put the auth token in the URL, even cross-origin", () => {
|
|
168
|
+
// SSE requests can send headers, so auth travels in the
|
|
169
|
+
// Authorization header; a token in the URL would leak into
|
|
170
|
+
// proxy and server logs.
|
|
171
|
+
const runtime = new RuntimeManager(
|
|
172
|
+
{
|
|
173
|
+
url: "https://sandbox.example.com",
|
|
174
|
+
lazy: true,
|
|
175
|
+
authToken: "my-secret-token",
|
|
176
|
+
},
|
|
177
|
+
true,
|
|
178
|
+
);
|
|
179
|
+
const url = runtime.getSseURL("s_123" as SessionId);
|
|
180
|
+
|
|
181
|
+
expect(url.searchParams.get("access_token")).toBeNull();
|
|
182
|
+
expect(url.toString()).not.toContain("my-secret-token");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should strip an access_token forwarded from the page URL", () => {
|
|
186
|
+
// The page URL can briefly carry access_token before auth cleanup
|
|
187
|
+
// removes it; it must not be forwarded to /sse.
|
|
188
|
+
const original = window.location.href;
|
|
189
|
+
window.history.pushState({}, "", "/?access_token=page-token&foo=bar");
|
|
190
|
+
try {
|
|
191
|
+
const runtime = new RuntimeManager(mockConfig);
|
|
192
|
+
const url = runtime.getSseURL("s_123" as SessionId);
|
|
193
|
+
|
|
194
|
+
expect(url.searchParams.get("access_token")).toBeNull();
|
|
195
|
+
expect(url.searchParams.get("foo")).toBe("bar");
|
|
196
|
+
} finally {
|
|
197
|
+
window.history.pushState({}, "", original);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
157
202
|
describe("getWsSyncURL", () => {
|
|
158
203
|
it("should return WebSocket Sync URL", () => {
|
|
159
204
|
const runtime = new RuntimeManager(mockConfig);
|