@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.
Files changed (41) hide show
  1. package/dist/{ConnectedDataExplorerComponent-7p7rt9iw.js → ConnectedDataExplorerComponent-Cmq_9LVS.js} +4 -4
  2. package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-CnMJ8jR9.js} +1 -1
  3. package/dist/{chat-ui-C0z13gJB.js → chat-ui-Dy0arY8E.js} +6 -6
  4. package/dist/{code-visibility-CoTgpfnd.js → code-visibility-B5xe1Xp2.js} +9 -9
  5. package/dist/{formats-CitsMtUm.js → formats-CdvkxTS6.js} +1 -1
  6. package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-D5w23DvC.js} +3 -3
  7. package/dist/{html-to-image-CEo5pRYw.js → html-to-image-f-kPEnn0.js} +5 -5
  8. package/dist/{input-BGPrFH3g.js → input-CF5Sgjba.js} +1 -1
  9. package/dist/main.js +19 -18
  10. package/dist/{mermaid-UdmxG2PZ.js → mermaid-BaZJ2mds.js} +2 -2
  11. package/dist/{process-output-BOvvilRG.js → process-output-O9FeFAVM.js} +1 -1
  12. package/dist/{reveal-component-Bvigsc8L.js → reveal-component-BkZp_qsV.js} +5 -5
  13. package/dist/{spec-DSs9v0xx.js → spec-DxIove3q.js} +1 -1
  14. package/dist/style.css +1 -1
  15. package/dist/{toDate-BRJgtOGm.js → toDate-CEOdz9nC.js} +1 -1
  16. package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-Cs0mpJTy.js} +1 -1
  17. package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-DoOZicGb.js} +1 -1
  18. package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DR_F1d7F.js} +1 -1
  19. package/dist/{useTheme-Df_vGflw.js → useTheme-BA-8MM7N.js} +1 -0
  20. package/dist/{vega-component-BAvmvTjO.js → vega-component-BN0fz-QA.js} +5 -5
  21. package/package.json +1 -1
  22. package/src/__mocks__/requests.ts +3 -0
  23. package/src/components/editor/actions/useNotebookActions.tsx +8 -0
  24. package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +221 -0
  25. package/src/components/editor/chrome/components/feedback-button.tsx +308 -77
  26. package/src/core/ai/context/providers/error.ts +6 -158
  27. package/src/core/constants.ts +2 -0
  28. package/src/core/diagnostics/__tests__/issue-details.test.ts +173 -0
  29. package/src/core/diagnostics/issue-details.ts +139 -0
  30. package/src/core/errors/__tests__/error-entries.test.ts +25 -0
  31. package/src/core/errors/error-entries.ts +176 -0
  32. package/src/core/islands/bridge.ts +1 -0
  33. package/src/core/network/__tests__/requests-lazy.test.ts +18 -0
  34. package/src/core/network/__tests__/requests-network.test.ts +7 -0
  35. package/src/core/network/requests-lazy.ts +13 -1
  36. package/src/core/network/requests-network.ts +3 -0
  37. package/src/core/network/requests-static.ts +1 -0
  38. package/src/core/network/requests-toasting.tsx +1 -0
  39. package/src/core/network/types.ts +3 -0
  40. package/src/core/wasm/bridge.ts +8 -0
  41. 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("&lt;script&gt;.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>;
@@ -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>;