@marimo-team/islands 0.23.15-dev49 → 0.23.15-dev50
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-Cmq_9LVS.js} +4 -4
- package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-CnMJ8jR9.js} +1 -1
- package/dist/{chat-ui-C0z13gJB.js → chat-ui-Dy0arY8E.js} +6 -6
- package/dist/{code-visibility-CoTgpfnd.js → code-visibility-B5xe1Xp2.js} +9 -9
- package/dist/{formats-CitsMtUm.js → formats-CdvkxTS6.js} +1 -1
- package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-D5w23DvC.js} +3 -3
- package/dist/{html-to-image-CEo5pRYw.js → html-to-image-f-kPEnn0.js} +5 -5
- package/dist/{input-BGPrFH3g.js → input-CF5Sgjba.js} +1 -1
- package/dist/main.js +19 -18
- package/dist/{mermaid-UdmxG2PZ.js → mermaid-BaZJ2mds.js} +2 -2
- package/dist/{process-output-BOvvilRG.js → process-output-O9FeFAVM.js} +1 -1
- package/dist/{reveal-component-Bvigsc8L.js → reveal-component-BkZp_qsV.js} +5 -5
- package/dist/{spec-DSs9v0xx.js → spec-DxIove3q.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-BRJgtOGm.js → toDate-CEOdz9nC.js} +1 -1
- package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-Cs0mpJTy.js} +1 -1
- package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-DoOZicGb.js} +1 -1
- package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DR_F1d7F.js} +1 -1
- package/dist/{useTheme-Df_vGflw.js → useTheme-BA-8MM7N.js} +1 -0
- package/dist/{vega-component-BAvmvTjO.js → vega-component-BN0fz-QA.js} +5 -5
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +3 -0
- package/src/components/editor/actions/useNotebookActions.tsx +8 -0
- package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +221 -0
- package/src/components/editor/chrome/components/feedback-button.tsx +308 -77
- package/src/core/ai/context/providers/error.ts +6 -158
- package/src/core/constants.ts +2 -0
- package/src/core/diagnostics/__tests__/issue-details.test.ts +173 -0
- package/src/core/diagnostics/issue-details.ts +139 -0
- package/src/core/errors/__tests__/error-entries.test.ts +25 -0
- package/src/core/errors/error-entries.ts +176 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +18 -0
- package/src/core/network/__tests__/requests-network.test.ts +7 -0
- package/src/core/network/requests-lazy.ts +13 -1
- package/src/core/network/requests-network.ts +3 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +3 -0
- package/src/core/wasm/bridge.ts +8 -0
- package/src/core/wasm/worker/types.ts +2 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { cellId } from "@/__tests__/branded";
|
|
5
|
+
import type { CellErrorEntry } from "@/core/errors/error-entries";
|
|
6
|
+
import type { EnvironmentInfo } from "@/core/network/types";
|
|
7
|
+
import {
|
|
8
|
+
buildBugReportUrl,
|
|
9
|
+
buildIssueDetails,
|
|
10
|
+
createPartialEnvironment,
|
|
11
|
+
enrichEnvironment,
|
|
12
|
+
markdownCodeFence,
|
|
13
|
+
MAX_PREFILL_URL_LENGTH,
|
|
14
|
+
} from "../issue-details";
|
|
15
|
+
|
|
16
|
+
const environment: EnvironmentInfo = {
|
|
17
|
+
marimo: "1.2.3",
|
|
18
|
+
editable: false,
|
|
19
|
+
location: "~/.venv/site-packages/marimo",
|
|
20
|
+
OS: "Darwin",
|
|
21
|
+
"OS Version": "25.0",
|
|
22
|
+
Processor: "arm",
|
|
23
|
+
"Python Version": "3.12.9",
|
|
24
|
+
Locale: "en_US",
|
|
25
|
+
Binaries: { Browser: "chrome 140", Node: "v22", uv: "0.11" },
|
|
26
|
+
Dependencies: { click: "8.4.2" },
|
|
27
|
+
"Optional Dependencies": { pandas: "3.0.0" },
|
|
28
|
+
"Experimental Flags": {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("enrichEnvironment", () => {
|
|
32
|
+
it("replaces server Chrome detection with the active browser", () => {
|
|
33
|
+
const result = enrichEnvironment(environment, "Firefox/140");
|
|
34
|
+
expect(result.Binaries.Browser).toBe("Firefox/140");
|
|
35
|
+
expect(result.Binaries.Node).toBe("v22");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("does not mutate the input", () => {
|
|
39
|
+
enrichEnvironment(environment, "Firefox/140");
|
|
40
|
+
expect(environment.Binaries.Browser).toBe("chrome 140");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("createPartialEnvironment", () => {
|
|
45
|
+
it("records the collection error and keeps the active browser", () => {
|
|
46
|
+
const partial = createPartialEnvironment(
|
|
47
|
+
"1.2.3",
|
|
48
|
+
"Firefox/140",
|
|
49
|
+
"en_US",
|
|
50
|
+
"Server environment information unavailable",
|
|
51
|
+
);
|
|
52
|
+
expect(partial.marimo).toBe("1.2.3");
|
|
53
|
+
expect(partial.Binaries?.Browser).toBe("Firefox/140");
|
|
54
|
+
expect(partial.Locale).toBe("en_US");
|
|
55
|
+
expect(partial["Environment Collection Error"]).toBe(
|
|
56
|
+
"Server environment information unavailable",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("omits empty fields rather than filling placeholders", () => {
|
|
61
|
+
const partial = createPartialEnvironment(
|
|
62
|
+
"1.2.3",
|
|
63
|
+
"Firefox/140",
|
|
64
|
+
"",
|
|
65
|
+
"boom",
|
|
66
|
+
);
|
|
67
|
+
expect(partial.Locale).toBeUndefined();
|
|
68
|
+
expect(partial.location).toBeUndefined();
|
|
69
|
+
expect(partial.OS).toBeUndefined();
|
|
70
|
+
expect(partial.Dependencies).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("markdownCodeFence", () => {
|
|
75
|
+
it("uses a fence longer than any backtick run in the content", () => {
|
|
76
|
+
const block = markdownCodeFence("python", 'text = "```"');
|
|
77
|
+
expect(block.startsWith("````python\n")).toBe(true);
|
|
78
|
+
expect(block.endsWith("\n````")).toBe(true);
|
|
79
|
+
expect(block).toContain('text = "```"');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses a minimum fence of three backticks", () => {
|
|
83
|
+
const block = markdownCodeFence("json", "{}");
|
|
84
|
+
expect(block.startsWith("```json\n")).toBe(true);
|
|
85
|
+
expect(block.endsWith("\n```")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("buildIssueDetails", () => {
|
|
90
|
+
it("includes the environment and omits notebook source unless provided", () => {
|
|
91
|
+
const markdown = buildIssueDetails({
|
|
92
|
+
environment,
|
|
93
|
+
errors: [],
|
|
94
|
+
notebook: undefined,
|
|
95
|
+
});
|
|
96
|
+
expect(markdown).toContain("<summary>Environment</summary>");
|
|
97
|
+
expect(markdown).toContain('"marimo": "1.2.3"');
|
|
98
|
+
expect(markdown).not.toContain("Notebook source");
|
|
99
|
+
expect(markdown).not.toContain("Current errors");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("includes current errors as plain text without notebook source", () => {
|
|
103
|
+
const errors: CellErrorEntry[] = [
|
|
104
|
+
{
|
|
105
|
+
cellId: cellId("cell-1"),
|
|
106
|
+
cellName: "Cell 1",
|
|
107
|
+
cellCode: "password = 'private'",
|
|
108
|
+
errorData: [],
|
|
109
|
+
tracebackHtml:
|
|
110
|
+
'<span class="gr">ValueError</span>: <span class="n">bad value</span>',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const markdown = buildIssueDetails({ environment, errors });
|
|
114
|
+
expect(markdown).toContain("<summary>Current errors</summary>");
|
|
115
|
+
expect(markdown).toContain("ValueError: bad value");
|
|
116
|
+
expect(markdown).not.toContain("password");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("includes notebook source under its basename when provided", () => {
|
|
120
|
+
const markdown = buildIssueDetails({
|
|
121
|
+
environment,
|
|
122
|
+
errors: [],
|
|
123
|
+
notebook: {
|
|
124
|
+
filename: "/project/example.py",
|
|
125
|
+
contents: "x = 1",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
expect(markdown).toContain(
|
|
129
|
+
"<summary>Notebook source: example.py</summary>",
|
|
130
|
+
);
|
|
131
|
+
expect(markdown).toContain("x = 1");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("escapes the summary label as text", () => {
|
|
135
|
+
const markdown = buildIssueDetails({
|
|
136
|
+
environment,
|
|
137
|
+
errors: [],
|
|
138
|
+
notebook: {
|
|
139
|
+
filename: "/project/<script>.py",
|
|
140
|
+
contents: "x = 1",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
expect(markdown).toContain("<script>.py");
|
|
144
|
+
expect(markdown).not.toContain("<script>.py");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("buildBugReportUrl", () => {
|
|
149
|
+
const baseUrl =
|
|
150
|
+
"https://github.com/marimo-team/marimo/issues/new?template=bug_report.yaml";
|
|
151
|
+
|
|
152
|
+
it("prefills the env field with the encoded issue details", () => {
|
|
153
|
+
const url = buildBugReportUrl(baseUrl, "hello world");
|
|
154
|
+
expect(url).toBe(`${baseUrl}&env=hello%20world`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("appends env with a query separator when the base URL has none", () => {
|
|
158
|
+
const url = buildBugReportUrl("https://example.com/new", "x");
|
|
159
|
+
expect(url).toBe("https://example.com/new?env=x");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("encodes markdown so it survives as a single query param", () => {
|
|
163
|
+
const details = buildIssueDetails({ environment, errors: [] });
|
|
164
|
+
const url = buildBugReportUrl(baseUrl, details);
|
|
165
|
+
expect(url).toContain("&env=");
|
|
166
|
+
expect(new URL(url).searchParams.get("env")).toBe(details);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("falls back to the plain base URL when the prefill exceeds the cap", () => {
|
|
170
|
+
const huge = "x".repeat(MAX_PREFILL_URL_LENGTH);
|
|
171
|
+
expect(buildBugReportUrl(baseUrl, huge)).toBe(baseUrl);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type CellErrorEntry,
|
|
5
|
+
formatCellError,
|
|
6
|
+
} from "@/core/errors/error-entries";
|
|
7
|
+
import type { EnvironmentInfo } from "@/core/network/types";
|
|
8
|
+
import { Paths } from "@/utils/paths";
|
|
9
|
+
import { Strings } from "@/utils/strings";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Environment information augmented with a client-side collection error, used
|
|
13
|
+
* when the server environment request fails and only partial data is available.
|
|
14
|
+
* Fields are optional because a partial environment only carries what the
|
|
15
|
+
* client could determine without the server.
|
|
16
|
+
*/
|
|
17
|
+
export type EnvironmentDiagnostics = Partial<EnvironmentInfo> & {
|
|
18
|
+
"Environment Collection Error"?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface NotebookSource {
|
|
22
|
+
filename: string;
|
|
23
|
+
contents: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface IssueDetailsInput {
|
|
27
|
+
environment: EnvironmentDiagnostics;
|
|
28
|
+
errors: CellErrorEntry[];
|
|
29
|
+
notebook?: NotebookSource;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Replace the server-detected browser with the active browser's user agent.
|
|
34
|
+
*
|
|
35
|
+
* The server cannot know which browser is driving the UI, so diagnostics
|
|
36
|
+
* generated from the modal reflect the live `navigator.userAgent` instead.
|
|
37
|
+
*/
|
|
38
|
+
export function enrichEnvironment(
|
|
39
|
+
environment: EnvironmentInfo,
|
|
40
|
+
userAgent: string,
|
|
41
|
+
): EnvironmentInfo {
|
|
42
|
+
return {
|
|
43
|
+
...environment,
|
|
44
|
+
Binaries: {
|
|
45
|
+
...environment.Binaries,
|
|
46
|
+
Browser: userAgent,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createPartialEnvironment(
|
|
52
|
+
marimoVersion: string,
|
|
53
|
+
userAgent: string,
|
|
54
|
+
locale: string,
|
|
55
|
+
message: string,
|
|
56
|
+
): EnvironmentDiagnostics {
|
|
57
|
+
return {
|
|
58
|
+
marimo: marimoVersion,
|
|
59
|
+
Locale: locale || undefined,
|
|
60
|
+
Binaries: { Browser: userAgent },
|
|
61
|
+
"Environment Collection Error": message,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Wrap `contents` in a Markdown code fence long enough to survive backtick runs
|
|
67
|
+
* inside it, so pasted diagnostics render as a single block on GitHub.
|
|
68
|
+
*/
|
|
69
|
+
export function markdownCodeFence(language: string, contents: string): string {
|
|
70
|
+
const longest = Math.max(
|
|
71
|
+
2,
|
|
72
|
+
...Array.from(contents.matchAll(/`+/g), (match) => match[0].length),
|
|
73
|
+
);
|
|
74
|
+
const fence = "`".repeat(longest + 1);
|
|
75
|
+
return `${fence}${language}\n${contents}\n${fence}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* GitHub returns HTTP 414 for issue-form URLs beyond roughly 8 KB, so prefill is
|
|
80
|
+
* skipped once the encoded body would push the URL past this conservative cap.
|
|
81
|
+
*/
|
|
82
|
+
export const MAX_PREFILL_URL_LENGTH = 6000;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build a bug-report URL with `issueDetails` prefilled into the form's `env`
|
|
86
|
+
* field. Falls back to `baseUrl` unchanged when the prefilled URL would exceed
|
|
87
|
+
* `MAX_PREFILL_URL_LENGTH`.
|
|
88
|
+
*/
|
|
89
|
+
export function buildBugReportUrl(
|
|
90
|
+
baseUrl: string,
|
|
91
|
+
issueDetails: string,
|
|
92
|
+
): string {
|
|
93
|
+
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
94
|
+
const prefilled = `${baseUrl}${separator}env=${encodeURIComponent(issueDetails)}`;
|
|
95
|
+
return prefilled.length > MAX_PREFILL_URL_LENGTH ? baseUrl : prefilled;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function detailsSection(summary: string, body: string): string {
|
|
99
|
+
return [
|
|
100
|
+
"<details>",
|
|
101
|
+
`<summary>${Strings.htmlEscape(summary) ?? ""}</summary>`,
|
|
102
|
+
"",
|
|
103
|
+
body,
|
|
104
|
+
"",
|
|
105
|
+
"</details>",
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildIssueDetails(input: IssueDetailsInput): string {
|
|
110
|
+
const sections = [
|
|
111
|
+
detailsSection(
|
|
112
|
+
"Environment",
|
|
113
|
+
markdownCodeFence("json", JSON.stringify(input.environment, null, 2)),
|
|
114
|
+
),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (input.errors.length > 0) {
|
|
118
|
+
sections.push(
|
|
119
|
+
detailsSection(
|
|
120
|
+
"Current errors",
|
|
121
|
+
markdownCodeFence(
|
|
122
|
+
"text",
|
|
123
|
+
input.errors.map(formatCellError).join("\n\n---\n\n"),
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (input.notebook) {
|
|
130
|
+
sections.push(
|
|
131
|
+
detailsSection(
|
|
132
|
+
`Notebook source: ${Paths.basename(input.notebook.filename)}`,
|
|
133
|
+
markdownCodeFence("python", input.notebook.contents),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return sections.join("\n\n");
|
|
139
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { cellId } from "@/__tests__/branded";
|
|
5
|
+
import { type CellErrorEntry, formatCellError } from "../error-entries";
|
|
6
|
+
|
|
7
|
+
describe("formatCellError", () => {
|
|
8
|
+
it("formats current tracebacks without notebook source", () => {
|
|
9
|
+
const entry: CellErrorEntry = {
|
|
10
|
+
cellId: cellId("cell-1"),
|
|
11
|
+
cellName: "Cell 1",
|
|
12
|
+
cellCode: "password = 'private'",
|
|
13
|
+
errorData: [],
|
|
14
|
+
tracebackHtml:
|
|
15
|
+
'<span class="gr">ValueError</span>: <span class="n">bad value</span>',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const text = formatCellError(entry);
|
|
19
|
+
|
|
20
|
+
expect(text).toContain("Cell 1");
|
|
21
|
+
expect(text).toContain("ValueError: bad value");
|
|
22
|
+
expect(text).not.toContain("password");
|
|
23
|
+
expect(text).not.toContain("<span");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
4
|
+
import type { CellId } from "@/core/cells/ids";
|
|
5
|
+
import { displayCellName } from "@/core/cells/names";
|
|
6
|
+
import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
|
|
7
|
+
import { isErrorMime, isMarimoErrorsMime, isTracebackMime } from "@/core/mime";
|
|
8
|
+
import type { JotaiStore } from "@/core/state/jotai";
|
|
9
|
+
import { logNever } from "@/utils/assertNever";
|
|
10
|
+
import { parseHtmlContent } from "@/utils/dom";
|
|
11
|
+
|
|
12
|
+
export interface CellErrorEntry {
|
|
13
|
+
cellId: CellId;
|
|
14
|
+
cellName: string;
|
|
15
|
+
cellCode: string;
|
|
16
|
+
errorData: MarimoError[];
|
|
17
|
+
tracebackHtml?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseCellErrorOutput(
|
|
21
|
+
output: OutputMessage,
|
|
22
|
+
): Pick<CellErrorEntry, "errorData" | "tracebackHtml"> | null {
|
|
23
|
+
if (isMarimoErrorsMime(output.mimetype)) {
|
|
24
|
+
if (!Array.isArray(output.data)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const errorData = output.data.filter(
|
|
28
|
+
(error) => !error.type.includes("ancestor"),
|
|
29
|
+
);
|
|
30
|
+
if (errorData.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { errorData };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isTracebackMime(output.mimetype)) {
|
|
37
|
+
if (typeof output.data !== "string" || output.data.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return { errorData: [], tracebackHtml: output.data };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function errorDataHasTraceback(errorData: MarimoError[]): boolean {
|
|
47
|
+
return errorData.some((error) => "traceback" in error && error.traceback);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getTracebackFromConsole(
|
|
51
|
+
consoleOutputs: OutputMessage[] | undefined,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
const tracebackOutput = consoleOutputs?.find((output) =>
|
|
54
|
+
isTracebackMime(output.mimetype),
|
|
55
|
+
);
|
|
56
|
+
if (
|
|
57
|
+
!tracebackOutput ||
|
|
58
|
+
typeof tracebackOutput.data !== "string" ||
|
|
59
|
+
tracebackOutput.data.length === 0
|
|
60
|
+
) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return tracebackOutput.data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveTracebackHtml(
|
|
67
|
+
parsed: Pick<CellErrorEntry, "errorData" | "tracebackHtml">,
|
|
68
|
+
consoleOutputs: OutputMessage[] | undefined,
|
|
69
|
+
): string | undefined {
|
|
70
|
+
if (parsed.tracebackHtml) {
|
|
71
|
+
return parsed.tracebackHtml;
|
|
72
|
+
}
|
|
73
|
+
if (errorDataHasTraceback(parsed.errorData)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return getTracebackFromConsole(consoleOutputs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getCellErrorEntries(store: JotaiStore): CellErrorEntry[] {
|
|
80
|
+
const { cellIds, cellRuntime, cellData } = store.get(notebookAtom);
|
|
81
|
+
const entries: CellErrorEntry[] = [];
|
|
82
|
+
|
|
83
|
+
for (const [cellIndex, cellId] of cellIds.inOrderIds.entries()) {
|
|
84
|
+
const cell = cellRuntime[cellId];
|
|
85
|
+
const output = cell.output;
|
|
86
|
+
if (!output || !isErrorMime(output.mimetype)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parsed = parseCellErrorOutput(output);
|
|
91
|
+
if (!parsed) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cellName = displayCellName(cellData[cellId].name, cellIndex);
|
|
96
|
+
// Prefer the code from the last execution: runtime errors correspond to
|
|
97
|
+
// the previous run, while `code` may already contain unsaved edits.
|
|
98
|
+
const cellCode = cellData[cellId].lastCodeRun ?? cellData[cellId].code;
|
|
99
|
+
|
|
100
|
+
entries.push({
|
|
101
|
+
cellId,
|
|
102
|
+
cellName,
|
|
103
|
+
cellCode,
|
|
104
|
+
errorData: parsed.errorData,
|
|
105
|
+
tracebackHtml: resolveTracebackHtml(parsed, cell.consoleOutputs),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return entries;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function describeError(error: MarimoError): string {
|
|
113
|
+
if (error.type === "setup-refs") {
|
|
114
|
+
return "The setup cell cannot have references";
|
|
115
|
+
}
|
|
116
|
+
if (error.type === "cycle") {
|
|
117
|
+
return "This cell is in a cycle";
|
|
118
|
+
}
|
|
119
|
+
if (error.type === "multiple-defs") {
|
|
120
|
+
return `The variable '${error.name}' was defined by another cell`;
|
|
121
|
+
}
|
|
122
|
+
if (error.type === "import-star") {
|
|
123
|
+
return error.msg;
|
|
124
|
+
}
|
|
125
|
+
if (error.type === "ancestor-stopped") {
|
|
126
|
+
return error.msg;
|
|
127
|
+
}
|
|
128
|
+
if (error.type === "ancestor-prevented") {
|
|
129
|
+
return error.msg;
|
|
130
|
+
}
|
|
131
|
+
if (error.type === "exception") {
|
|
132
|
+
return error.msg;
|
|
133
|
+
}
|
|
134
|
+
if (error.type === "strict-exception") {
|
|
135
|
+
return error.msg;
|
|
136
|
+
}
|
|
137
|
+
if (error.type === "interruption") {
|
|
138
|
+
return "This cell was interrupted and needs to be re-run";
|
|
139
|
+
}
|
|
140
|
+
if (error.type === "syntax") {
|
|
141
|
+
return error.msg;
|
|
142
|
+
}
|
|
143
|
+
if (error.type === "unknown") {
|
|
144
|
+
return error.msg;
|
|
145
|
+
}
|
|
146
|
+
if (error.type === "sql-error") {
|
|
147
|
+
return error.msg;
|
|
148
|
+
}
|
|
149
|
+
if (error.type === "internal") {
|
|
150
|
+
return error.msg || "An internal error occurred";
|
|
151
|
+
}
|
|
152
|
+
logNever(error);
|
|
153
|
+
return "Unknown error";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatSingleError(error: MarimoError): string {
|
|
157
|
+
let text = describeError(error);
|
|
158
|
+
if ("traceback" in error && error.traceback) {
|
|
159
|
+
text += `\n\nTraceback:\n${parseHtmlContent(error.traceback)}`;
|
|
160
|
+
}
|
|
161
|
+
return text;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatCellError(entry: CellErrorEntry): string {
|
|
165
|
+
const parts = [entry.cellName];
|
|
166
|
+
|
|
167
|
+
if (entry.tracebackHtml) {
|
|
168
|
+
parts.push(parseHtmlContent(entry.tracebackHtml));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (entry.errorData.length > 0) {
|
|
172
|
+
parts.push(entry.errorData.map(formatSingleError).join("\n\n"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return parts.join("\n\n");
|
|
176
|
+
}
|
|
@@ -295,6 +295,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
295
295
|
// ============================================================================
|
|
296
296
|
|
|
297
297
|
getUsageStats = throwNotImplemented;
|
|
298
|
+
getEnvironmentInfo = throwNotImplemented;
|
|
298
299
|
sendRename = throwNotImplemented;
|
|
299
300
|
sendSave = throwNotImplemented;
|
|
300
301
|
sendCopy = throwNotImplemented;
|
|
@@ -54,6 +54,24 @@ describe("createLazyRequests", () => {
|
|
|
54
54
|
} as unknown as EditRequests & RunRequests;
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it("passes server-only requests through without starting a kernel", async () => {
|
|
58
|
+
const environment = { marimo: "1.2.3" };
|
|
59
|
+
mockDelegate.getEnvironmentInfo = vi
|
|
60
|
+
.fn()
|
|
61
|
+
.mockResolvedValue(environment) as EditRequests["getEnvironmentInfo"];
|
|
62
|
+
|
|
63
|
+
const lazyRequests = createLazyRequests(
|
|
64
|
+
mockDelegate,
|
|
65
|
+
mockGetRuntimeManager,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await expect(lazyRequests.getEnvironmentInfo()).resolves.toEqual(
|
|
69
|
+
environment,
|
|
70
|
+
);
|
|
71
|
+
expect(mockInit).not.toHaveBeenCalled();
|
|
72
|
+
expect(mockDelegate.getEnvironmentInfo).toHaveBeenCalledOnce();
|
|
73
|
+
});
|
|
74
|
+
|
|
57
75
|
it("should call init once before first request", async () => {
|
|
58
76
|
const lazyRequests = createLazyRequests(
|
|
59
77
|
mockDelegate,
|
|
@@ -114,6 +114,13 @@ describe("createNetworkRequests", () => {
|
|
|
114
114
|
);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("getEnvironmentInfo should GET /api/environment", async () => {
|
|
118
|
+
const requests = createNetworkRequests();
|
|
119
|
+
await requests.getEnvironmentInfo();
|
|
120
|
+
|
|
121
|
+
expect(mockClient.GET).toHaveBeenCalledWith("/api/environment");
|
|
122
|
+
});
|
|
123
|
+
|
|
117
124
|
it("exportAsIPYNB should call the new endpoint as text", async () => {
|
|
118
125
|
const requests = createNetworkRequests();
|
|
119
126
|
await requests.exportAsIPYNB({
|
|
@@ -29,12 +29,17 @@ type AllRequests = EditRequests & RunRequests;
|
|
|
29
29
|
// - waitForConnectionOpen: Waits for an existing connection but won't start one.
|
|
30
30
|
// Use for operations that depend on a running kernel but shouldn't be the
|
|
31
31
|
// trigger to start it (e.g., saving, interrupting).
|
|
32
|
+
//
|
|
33
|
+
// - serverOnly: Calls the HTTP delegate directly without touching the kernel.
|
|
34
|
+
// Use for requests served by the marimo server itself, which resolve without
|
|
35
|
+
// a session (e.g., fetching environment diagnostics).
|
|
32
36
|
|
|
33
37
|
type Action =
|
|
34
38
|
| "throwError"
|
|
35
39
|
| "dropRequest"
|
|
36
40
|
| "startConnection"
|
|
37
|
-
| "waitForConnectionOpen"
|
|
41
|
+
| "waitForConnectionOpen"
|
|
42
|
+
| "serverOnly";
|
|
38
43
|
|
|
39
44
|
const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
40
45
|
// These will start a connection if not already connected and then wait until the connection is open
|
|
@@ -96,6 +101,9 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
96
101
|
sendFileDetails: "throwError",
|
|
97
102
|
openFile: "throwError",
|
|
98
103
|
|
|
104
|
+
// Served by the marimo server without a kernel session
|
|
105
|
+
getEnvironmentInfo: "serverOnly",
|
|
106
|
+
|
|
99
107
|
// Home operations throw errors
|
|
100
108
|
getRecentFiles: "startConnection",
|
|
101
109
|
getWorkspaceFiles: "startConnection",
|
|
@@ -155,6 +163,10 @@ export function createLazyRequests(
|
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
switch (action) {
|
|
166
|
+
case "serverOnly":
|
|
167
|
+
// Served by the marimo server itself; no kernel required
|
|
168
|
+
return request(...args);
|
|
169
|
+
|
|
158
170
|
case "dropRequest":
|
|
159
171
|
Logger.debug(
|
|
160
172
|
`Dropping request: ${key}, since not connected to a kernel.`,
|
|
@@ -285,6 +285,9 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
285
285
|
await waitForConnectionOpen();
|
|
286
286
|
return getClient().GET("/api/usage").then(handleResponse);
|
|
287
287
|
},
|
|
288
|
+
getEnvironmentInfo: () => {
|
|
289
|
+
return getClient().GET("/api/environment").then(handleResponse);
|
|
290
|
+
},
|
|
288
291
|
sendPdb: (request) => {
|
|
289
292
|
return getClient()
|
|
290
293
|
.POST("/api/kernel/pdb/pm", {
|
|
@@ -65,6 +65,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
65
65
|
validateSQL: throwNotInEditMode,
|
|
66
66
|
openFile: throwNotInEditMode,
|
|
67
67
|
getUsageStats: throwNotInEditMode,
|
|
68
|
+
getEnvironmentInfo: throwNotInEditMode,
|
|
68
69
|
sendListFiles: throwNotInEditMode,
|
|
69
70
|
sendSearchFiles: throwNotInEditMode,
|
|
70
71
|
sendPdb: throwNotInEditMode,
|
|
@@ -46,6 +46,7 @@ export function createErrorToastingRequests(
|
|
|
46
46
|
validateSQL: "Failed to validate SQL",
|
|
47
47
|
openFile: "Failed to open file",
|
|
48
48
|
getUsageStats: "", // No toast
|
|
49
|
+
getEnvironmentInfo: "", // No toast
|
|
49
50
|
sendListFiles: "Failed to list files",
|
|
50
51
|
sendSearchFiles: "Failed to search files",
|
|
51
52
|
sendPdb: "Failed to start debug session",
|
|
@@ -107,6 +107,8 @@ export type UpdateUIElementValuesRequest =
|
|
|
107
107
|
schemas["UpdateUIElementValuesRequest"];
|
|
108
108
|
export type UsageResponse =
|
|
109
109
|
paths["/api/usage"]["get"]["responses"]["200"]["content"]["application/json"];
|
|
110
|
+
export type EnvironmentInfo =
|
|
111
|
+
paths["/api/environment"]["get"]["responses"]["200"]["content"]["application/json"];
|
|
110
112
|
export type WorkspaceFilesRequest = schemas["WorkspaceFilesRequest"];
|
|
111
113
|
export type WorkspaceFilesResponse = schemas["WorkspaceFilesResponse"];
|
|
112
114
|
export type RunningNotebooksResponse = schemas["RunningNotebooksResponse"];
|
|
@@ -171,6 +173,7 @@ export interface EditRequests {
|
|
|
171
173
|
validateSQL: (request: ValidateSQLRequest) => Promise<null>;
|
|
172
174
|
openFile: (request: { path: string; lineNumber?: number }) => Promise<null>;
|
|
173
175
|
getUsageStats: () => Promise<UsageResponse>;
|
|
176
|
+
getEnvironmentInfo: () => Promise<EnvironmentInfo>;
|
|
174
177
|
// Debugger
|
|
175
178
|
sendPdb: (request: DebugCellRequest) => Promise<null>;
|
|
176
179
|
sendSetBreakpoints: (request: SetBreakpointsRequest) => Promise<null>;
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { getInitialAppMode } from "../mode";
|
|
|
16
16
|
import { API } from "../network/api";
|
|
17
17
|
import type {
|
|
18
18
|
EditRequests,
|
|
19
|
+
EnvironmentInfo,
|
|
19
20
|
ExportAsHTMLRequest,
|
|
20
21
|
ExportAsMarkdownRequest,
|
|
21
22
|
FileCopyResponse,
|
|
@@ -629,6 +630,13 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
629
630
|
};
|
|
630
631
|
|
|
631
632
|
getUsageStats = throwNotImplemented;
|
|
633
|
+
getEnvironmentInfo: EditRequests["getEnvironmentInfo"] = async () => {
|
|
634
|
+
const response = await this.rpc.proxy.request.bridge({
|
|
635
|
+
functionName: "get_environment_info",
|
|
636
|
+
payload: undefined,
|
|
637
|
+
});
|
|
638
|
+
return response as EnvironmentInfo;
|
|
639
|
+
};
|
|
632
640
|
openTutorial = throwNotImplemented;
|
|
633
641
|
getRecentFiles = throwNotImplemented;
|
|
634
642
|
getWorkspaceFiles = throwNotImplemented;
|
|
@@ -9,6 +9,7 @@ import type { JsonString } from "@/utils/json/base64";
|
|
|
9
9
|
import type {
|
|
10
10
|
CodeCompletionRequest,
|
|
11
11
|
CopyNotebookRequest,
|
|
12
|
+
EnvironmentInfo,
|
|
12
13
|
ExportAsHTMLRequest,
|
|
13
14
|
ExportAsMarkdownRequest,
|
|
14
15
|
FileCopyRequest,
|
|
@@ -73,6 +74,7 @@ export interface RawBridge {
|
|
|
73
74
|
code_complete(request: CodeCompletionRequest): Promise<string>;
|
|
74
75
|
read_code(): Promise<{ contents: string }>;
|
|
75
76
|
read_snippets(): Promise<Snippets>;
|
|
77
|
+
get_environment_info(): Promise<EnvironmentInfo>;
|
|
76
78
|
format(request: FormatCellsRequest): Promise<FormatResponse>;
|
|
77
79
|
save(request: SaveNotebookRequest): Promise<string>;
|
|
78
80
|
copy(request: CopyNotebookRequest): Promise<string>;
|