@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
@@ -4,16 +4,75 @@ import { Slot as SlotPrimitive } from "radix-ui";
4
4
 
5
5
  const Slot = SlotPrimitive.Slot;
6
6
 
7
- import React, { type PropsWithChildren } from "react";
7
+ import { useAtomValue } from "jotai";
8
+ import { CheckIcon, ExternalLinkIcon, TriangleAlertIcon } from "lucide-react";
9
+ import React, { type PropsWithChildren, useMemo, useState } from "react";
10
+ import { CopyClipboardIcon } from "@/components/icons/copy-icon";
8
11
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Checkbox } from "@/components/ui/checkbox";
9
14
  import {
10
15
  DialogContent,
11
16
  DialogDescription,
12
17
  DialogHeader,
13
18
  DialogTitle,
14
19
  } from "@/components/ui/dialog";
20
+ import { Skeleton } from "@/components/ui/skeleton";
15
21
  import { toast } from "@/components/ui/use-toast";
22
+ import { notebookAtom } from "@/core/cells/cells";
16
23
  import { Constants } from "@/core/constants";
24
+ import {
25
+ buildBugReportUrl,
26
+ buildIssueDetails,
27
+ createPartialEnvironment,
28
+ type EnvironmentDiagnostics,
29
+ enrichEnvironment,
30
+ type NotebookSource,
31
+ } from "@/core/diagnostics/issue-details";
32
+ import {
33
+ formatCellError,
34
+ getCellErrorEntries,
35
+ } from "@/core/errors/error-entries";
36
+ import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
37
+ import { getMarimoVersion } from "@/core/meta/globals";
38
+ import { connectionAtom } from "@/core/network/connection";
39
+ import { useRequestClient } from "@/core/network/requests";
40
+ import { filenameAtom } from "@/core/saving/file-state";
41
+ import { store } from "@/core/state/jotai";
42
+ import { WebSocketState } from "@/core/websocket/types";
43
+ import { useAsyncData } from "@/hooks/useAsyncData";
44
+ import { cn } from "@/utils/cn";
45
+ import { copyToClipboard } from "@/utils/copy";
46
+
47
+ const CollapsiblePreview: React.FC<{ content: string }> = ({ content }) => {
48
+ const [expanded, setExpanded] = useState(false);
49
+ return (
50
+ <div className="flex flex-col gap-1">
51
+ <div className="relative">
52
+ <pre
53
+ className={cn(
54
+ "text-xs bg-muted rounded p-2 overflow-x-auto whitespace-pre-wrap",
55
+ !expanded && "max-h-24 overflow-hidden",
56
+ )}
57
+ >
58
+ {content}
59
+ </pre>
60
+ {!expanded && (
61
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 h-10 rounded-b bg-gradient-to-b from-transparent to-muted" />
62
+ )}
63
+ </div>
64
+ <Button
65
+ type="button"
66
+ variant="link"
67
+ size="xs"
68
+ className="self-start"
69
+ onClick={() => setExpanded((value) => !value)}
70
+ >
71
+ {expanded ? "Show less" : "Show more"}
72
+ </Button>
73
+ </div>
74
+ );
75
+ };
17
76
 
18
77
  export const FeedbackButton: React.FC<PropsWithChildren> = ({ children }) => {
19
78
  const { openModal, closeModal } = useImperativeModal();
@@ -25,84 +84,256 @@ export const FeedbackButton: React.FC<PropsWithChildren> = ({ children }) => {
25
84
  );
26
85
  };
27
86
 
28
- const FeedbackModal: React.FC<{
87
+ export const FeedbackModal: React.FC<{
29
88
  onClose: () => void;
30
- }> = ({ onClose }) => {
89
+ }> = () => {
90
+ const { getEnvironmentInfo, readCode } = useRequestClient();
91
+ const environmentRequest = useAsyncData(
92
+ async () => getEnvironmentInfo(),
93
+ [getEnvironmentInfo],
94
+ );
95
+
96
+ const notebook = useAtomValue(notebookAtom);
97
+ // biome-ignore lint/correctness/useExhaustiveDependencies: recompute when the notebook changes
98
+ const errors = useMemo(() => getCellErrorEntries(store), [notebook]);
99
+
100
+ const cells = notebook.cellIds.inOrderIds.map(
101
+ (cellId) => notebook.cellData[cellId],
102
+ );
103
+ const codeAvailable = useNotebookCodeAvailable(cells);
104
+ const filename = useAtomValue(filenameAtom);
105
+ const connection = useAtomValue(connectionAtom);
106
+ const notebookSourceAvailable =
107
+ filename !== null &&
108
+ codeAvailable &&
109
+ connection.state === WebSocketState.OPEN;
110
+
111
+ const [includeNotebook, setIncludeNotebook] = useState(false);
112
+
113
+ const notebookSourceReason = notebookSourceAvailable
114
+ ? undefined
115
+ : filename === null
116
+ ? "Save the notebook first."
117
+ : !codeAvailable
118
+ ? "Notebook source is hidden in this view."
119
+ : "Connect the notebook to include its source.";
120
+
121
+ const notebookSourceRequest = useAsyncData(async () => {
122
+ if (!includeNotebook || !notebookSourceAvailable || filename === null) {
123
+ return undefined;
124
+ }
125
+ const { contents } = await readCode();
126
+ return { filename, contents } satisfies NotebookSource;
127
+ }, [includeNotebook, notebookSourceAvailable, filename, readCode]);
128
+
129
+ const environment: EnvironmentDiagnostics | undefined =
130
+ environmentRequest.data
131
+ ? enrichEnvironment(environmentRequest.data, navigator.userAgent)
132
+ : environmentRequest.status === "error"
133
+ ? createPartialEnvironment(
134
+ getMarimoVersion(),
135
+ navigator.userAgent,
136
+ navigator.language,
137
+ "Server environment information unavailable",
138
+ )
139
+ : undefined;
140
+
141
+ const [issueDetailsCopied, setIssueDetailsCopied] = useState(false);
142
+
143
+ const githubIssueUrl = useMemo(() => {
144
+ if (!environment) {
145
+ return Constants.bugReportUrl;
146
+ }
147
+ return buildBugReportUrl(
148
+ Constants.bugReportUrl,
149
+ buildIssueDetails({ environment, errors }),
150
+ );
151
+ }, [environment, errors]);
152
+
153
+ const copyIssueDetails = async () => {
154
+ if (!environment) {
155
+ return;
156
+ }
157
+ const notebookForReport = includeNotebook
158
+ ? notebookSourceRequest.data
159
+ : undefined;
160
+ await copyToClipboard(
161
+ buildIssueDetails({ environment, errors, notebook: notebookForReport }),
162
+ );
163
+ setIssueDetailsCopied(true);
164
+ setTimeout(() => setIssueDetailsCopied(false), 2000);
165
+ toast({
166
+ title:
167
+ environmentRequest.status === "error"
168
+ ? "Partial issue details copied"
169
+ : "Issue details copied",
170
+ });
171
+ };
172
+
31
173
  return (
32
- <DialogContent className="w-fit">
33
- <form
34
- onSubmit={async (e) => {
35
- e.preventDefault();
36
-
37
- const formData = new FormData(e.target as HTMLFormElement);
38
- const rating = formData.get("rating");
39
- const message = formData.get("message");
40
-
41
- // Fire-and-forget we don't care about the response
42
- void fetch("https://marimo.io/api/feedback", {
43
- method: "POST",
44
- headers: {
45
- "Content-Type": "application/json",
46
- },
47
- body: JSON.stringify({
48
- rating: rating,
49
- message,
50
- }),
51
- });
52
- onClose();
53
- toast({
54
- title: "Feedback sent!",
55
- description: "Thank you for your feedback!",
56
- });
57
- }}
58
- >
59
- <DialogHeader>
60
- <DialogTitle>Send Feedback</DialogTitle>
61
- <DialogDescription>
62
- <p className="my-2 prose dark:prose-invert">
63
- We want to hear from you — from minor bug reports to wishlist
64
- features and everything in between. Here are some ways you can get
65
- in touch:
66
- </p>
67
- <ul className="list-disc ml-8 my-2 prose dark:prose-invert">
68
- <li className="my-0">
69
- Take our{" "}
70
- <a
71
- href={Constants.feedbackForm}
72
- target="_blank"
73
- className="underline"
74
- >
75
- two-minute survey.
76
- </a>
77
- </li>
78
- <li className="my-0">
79
- File a{" "}
80
- <a
81
- href={Constants.issuesPage}
82
- target="_blank"
83
- className="underline"
84
- >
85
- GitHub issue.
86
- </a>
87
- </li>
88
- <li className="my-0">
89
- Chat with us on{" "}
90
- <a
91
- href={Constants.discordLink}
92
- target="_blank"
93
- className="underline"
94
- >
95
- Discord.
96
- </a>
97
- </li>
98
- </ul>
99
- <p className="my-2 prose dark:prose-invert">
100
- We're excited you're here as we build the future of Python data
101
- tooling. Thanks for being part of our community!
102
- </p>
103
- </DialogDescription>
104
- </DialogHeader>
105
- </form>
174
+ <DialogContent className="w-[540px] max-w-[90vw]">
175
+ <DialogHeader>
176
+ <DialogTitle>Report an issue</DialogTitle>
177
+ <DialogDescription>
178
+ Copy your environment and any current errors to include in a GitHub
179
+ bug report. Nothing is uploaded automatically; review the details
180
+ before posting.
181
+ </DialogDescription>
182
+ </DialogHeader>
183
+
184
+ <div className="flex flex-col gap-4 max-h-[60vh] overflow-y-auto">
185
+ <div className="flex flex-wrap gap-2">
186
+ <Button
187
+ type="button"
188
+ variant="default"
189
+ size="xs"
190
+ disabled={
191
+ !environment ||
192
+ (includeNotebook && notebookSourceRequest.isFetching)
193
+ }
194
+ onClick={copyIssueDetails}
195
+ >
196
+ {issueDetailsCopied && (
197
+ <CheckIcon
198
+ className="w-4 h-4 mr-2 text-(--grass-11)"
199
+ color="white"
200
+ />
201
+ )}
202
+ {issueDetailsCopied ? "Copied!" : "Copy issue details"}
203
+ </Button>
204
+ <Button type="button" variant="outline" size="xs" asChild={true}>
205
+ <a href={githubIssueUrl} target="_blank" rel="noreferrer">
206
+ <ExternalLinkIcon className="w-4 h-4 mr-2" />
207
+ Open GitHub issue
208
+ </a>
209
+ </Button>
210
+ </div>
211
+
212
+ {includeNotebook && (
213
+ <p className="text-xs text-muted-foreground">
214
+ The GitHub link prefills your environment only. Use Copy issue
215
+ details to include the full notebook source.
216
+ </p>
217
+ )}
218
+
219
+ {environmentRequest.status === "pending" && (
220
+ <div className="flex flex-col gap-2">
221
+ <span className="text-sm text-muted-foreground">
222
+ Loading environment details…
223
+ </span>
224
+ <Skeleton className="h-4 w-full" />
225
+ <Skeleton className="h-4 w-3/4" />
226
+ <Skeleton className="h-4 w-1/2" />
227
+ </div>
228
+ )}
229
+
230
+ {environmentRequest.status === "error" && (
231
+ <div className="flex items-center gap-2 text-sm">
232
+ <TriangleAlertIcon className="w-4 h-4 text-(--yellow-11) shrink-0" />
233
+ <span>Server environment information unavailable</span>
234
+ <Button
235
+ type="button"
236
+ variant="link"
237
+ size="xs"
238
+ onClick={() => environmentRequest.refetch()}
239
+ >
240
+ Retry
241
+ </Button>
242
+ </div>
243
+ )}
244
+
245
+ {environment && (
246
+ <div className="flex flex-col gap-1">
247
+ <div className="flex items-center justify-between">
248
+ <span className="text-sm font-medium">Environment details</span>
249
+ <CopyClipboardIcon
250
+ className="w-3.5 h-3.5"
251
+ value={JSON.stringify(environment, null, 2)}
252
+ ariaLabel="Copy environment JSON"
253
+ toastTitle="Environment details copied"
254
+ />
255
+ </div>
256
+ <CollapsiblePreview
257
+ content={JSON.stringify(environment, null, 2)}
258
+ />
259
+ </div>
260
+ )}
261
+
262
+ {environment && errors.length > 0 && (
263
+ <div className="flex flex-col gap-1">
264
+ <span className="text-sm font-medium">Current errors</span>
265
+ <CollapsiblePreview
266
+ content={errors.map(formatCellError).join("\n\n---\n\n")}
267
+ />
268
+ </div>
269
+ )}
270
+
271
+ {environment && errors.length === 0 && (
272
+ <span className="text-sm text-muted-foreground">
273
+ No current errors detected.
274
+ </span>
275
+ )}
276
+
277
+ <div className="flex flex-col gap-1">
278
+ <div className="flex items-start gap-2 text-sm">
279
+ <Checkbox
280
+ id="issue-include-notebook"
281
+ className="mt-0.5"
282
+ checked={includeNotebook}
283
+ disabled={!notebookSourceAvailable}
284
+ onCheckedChange={(checked) =>
285
+ setIncludeNotebook(checked === true)
286
+ }
287
+ aria-label="Include full notebook source"
288
+ />
289
+ <label htmlFor="issue-include-notebook">
290
+ Include full notebook source
291
+ </label>
292
+ </div>
293
+ <p className="text-xs text-muted-foreground ml-6">
294
+ Copies the entire saved Python source, including comments, literal
295
+ data, embedded credentials, and package metadata. Outputs and
296
+ external files are not included. Review it before posting.{" "}
297
+ <span className="font-bold">
298
+ For private notebooks, paste a minimal reproduction instead.
299
+ </span>
300
+ </p>
301
+ {notebookSourceReason && (
302
+ <span className="text-xs text-muted-foreground ml-6">
303
+ {notebookSourceReason}
304
+ </span>
305
+ )}
306
+ {includeNotebook && notebookSourceRequest.status === "error" && (
307
+ <span className="text-xs text-(--red-11) ml-6">
308
+ Notebook source could not be loaded.
309
+ </span>
310
+ )}
311
+ </div>
312
+
313
+ <div className="border-t pt-3">
314
+ <p className="text-sm text-muted-foreground">
315
+ Other feedback? Take our{" "}
316
+ <a
317
+ href={Constants.feedbackForm}
318
+ target="_blank"
319
+ rel="noreferrer"
320
+ className="underline"
321
+ >
322
+ two-minute survey
323
+ </a>{" "}
324
+ or chat with us on{" "}
325
+ <a
326
+ href={Constants.discordLink}
327
+ target="_blank"
328
+ rel="noreferrer"
329
+ className="underline"
330
+ >
331
+ Discord
332
+ </a>
333
+ .
334
+ </p>
335
+ </div>
336
+ </div>
106
337
  </DialogContent>
107
338
  );
108
339
  };
@@ -1,13 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { Completion } from "@codemirror/autocomplete";
4
- import { notebookAtom } from "@/core/cells/cells";
5
- import type { CellId } from "@/core/cells/ids";
6
- import { displayCellName } from "@/core/cells/names";
7
- import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
8
- import { isErrorMime, isMarimoErrorsMime, isTracebackMime } from "@/core/mime";
4
+ import {
5
+ type CellErrorEntry,
6
+ describeError,
7
+ formatSingleError,
8
+ getCellErrorEntries,
9
+ } from "@/core/errors/error-entries";
9
10
  import type { JotaiStore } from "@/core/state/jotai";
10
- import { logNever } from "@/utils/assertNever";
11
11
  import { parseHtmlContent } from "@/utils/dom";
12
12
  import { PluralWord } from "@/utils/pluralize";
13
13
  import { type AIContextItem, AIContextProvider } from "../registry";
@@ -15,14 +15,6 @@ import { contextToXml } from "../utils";
15
15
  import { Sections } from "./common";
16
16
  import { formatDatasourceContextForCell } from "./datasource";
17
17
 
18
- export interface CellErrorEntry {
19
- cellId: CellId;
20
- cellName: string;
21
- cellCode: string;
22
- errorData: MarimoError[];
23
- tracebackHtml?: string;
24
- }
25
-
26
18
  export type ErrorContextItemData =
27
19
  | {
28
20
  type: "all-errors";
@@ -38,150 +30,6 @@ export interface ErrorContextItem extends AIContextItem {
38
30
  data: ErrorContextItemData;
39
31
  }
40
32
 
41
- function parseCellErrorOutput(
42
- output: OutputMessage,
43
- ): Pick<CellErrorEntry, "errorData" | "tracebackHtml"> | null {
44
- if (isMarimoErrorsMime(output.mimetype)) {
45
- if (!Array.isArray(output.data)) {
46
- return null;
47
- }
48
- const errorData = output.data.filter(
49
- (error) => !error.type.includes("ancestor"),
50
- );
51
- if (errorData.length === 0) {
52
- return null;
53
- }
54
- return { errorData };
55
- }
56
-
57
- if (isTracebackMime(output.mimetype)) {
58
- if (typeof output.data !== "string" || output.data.length === 0) {
59
- return null;
60
- }
61
- return { errorData: [], tracebackHtml: output.data };
62
- }
63
-
64
- return null;
65
- }
66
-
67
- function errorDataHasTraceback(errorData: MarimoError[]): boolean {
68
- return errorData.some((error) => "traceback" in error && error.traceback);
69
- }
70
-
71
- function getTracebackFromConsole(
72
- consoleOutputs: OutputMessage[] | undefined,
73
- ): string | undefined {
74
- const tracebackOutput = consoleOutputs?.find((output) =>
75
- isTracebackMime(output.mimetype),
76
- );
77
- if (
78
- !tracebackOutput ||
79
- typeof tracebackOutput.data !== "string" ||
80
- tracebackOutput.data.length === 0
81
- ) {
82
- return undefined;
83
- }
84
- return tracebackOutput.data;
85
- }
86
-
87
- function resolveTracebackHtml(
88
- parsed: Pick<CellErrorEntry, "errorData" | "tracebackHtml">,
89
- consoleOutputs: OutputMessage[] | undefined,
90
- ): string | undefined {
91
- if (parsed.tracebackHtml) {
92
- return parsed.tracebackHtml;
93
- }
94
- if (errorDataHasTraceback(parsed.errorData)) {
95
- return undefined;
96
- }
97
- return getTracebackFromConsole(consoleOutputs);
98
- }
99
-
100
- export function getCellErrorEntries(store: JotaiStore): CellErrorEntry[] {
101
- const { cellIds, cellRuntime, cellData } = store.get(notebookAtom);
102
- const entries: CellErrorEntry[] = [];
103
-
104
- for (const [cellIndex, cellId] of cellIds.inOrderIds.entries()) {
105
- const cell = cellRuntime[cellId];
106
- const output = cell.output;
107
- if (!output || !isErrorMime(output.mimetype)) {
108
- continue;
109
- }
110
-
111
- const parsed = parseCellErrorOutput(output);
112
- if (!parsed) {
113
- continue;
114
- }
115
-
116
- const cellName = displayCellName(cellData[cellId].name, cellIndex);
117
- // Prefer the code from the last execution: runtime errors correspond to
118
- // the previous run, while `code` may already contain unsaved edits.
119
- const cellCode = cellData[cellId].lastCodeRun ?? cellData[cellId].code;
120
-
121
- entries.push({
122
- cellId,
123
- cellName,
124
- cellCode,
125
- errorData: parsed.errorData,
126
- tracebackHtml: resolveTracebackHtml(parsed, cell.consoleOutputs),
127
- });
128
- }
129
-
130
- return entries;
131
- }
132
-
133
- function describeError(error: MarimoError): string {
134
- if (error.type === "setup-refs") {
135
- return "The setup cell cannot have references";
136
- }
137
- if (error.type === "cycle") {
138
- return "This cell is in a cycle";
139
- }
140
- if (error.type === "multiple-defs") {
141
- return `The variable '${error.name}' was defined by another cell`;
142
- }
143
- if (error.type === "import-star") {
144
- return error.msg;
145
- }
146
- if (error.type === "ancestor-stopped") {
147
- return error.msg;
148
- }
149
- if (error.type === "ancestor-prevented") {
150
- return error.msg;
151
- }
152
- if (error.type === "exception") {
153
- return error.msg;
154
- }
155
- if (error.type === "strict-exception") {
156
- return error.msg;
157
- }
158
- if (error.type === "interruption") {
159
- return "This cell was interrupted and needs to be re-run";
160
- }
161
- if (error.type === "syntax") {
162
- return error.msg;
163
- }
164
- if (error.type === "unknown") {
165
- return error.msg;
166
- }
167
- if (error.type === "sql-error") {
168
- return error.msg;
169
- }
170
- if (error.type === "internal") {
171
- return error.msg || "An internal error occurred";
172
- }
173
- logNever(error);
174
- return "Unknown error";
175
- }
176
-
177
- function formatSingleError(error: MarimoError): string {
178
- let text = describeError(error);
179
- if ("traceback" in error && error.traceback) {
180
- text += `\n\nTraceback:\n${parseHtmlContent(error.traceback)}`;
181
- }
182
- return text;
183
- }
184
-
185
33
  function formatCellErrorDetails(
186
34
  entry: CellErrorEntry,
187
35
  store: JotaiStore,
@@ -3,6 +3,8 @@ export const Constants = {
3
3
  githubPage: "https://github.com/marimo-team/marimo",
4
4
  releasesPage: "https://github.com/marimo-team/marimo/releases",
5
5
  issuesPage: "https://github.com/marimo-team/marimo/issues",
6
+ bugReportUrl:
7
+ "https://github.com/marimo-team/marimo/issues/new?template=bug_report.yaml",
6
8
  feedbackForm: "https://marimo.io/feedback",
7
9
  discordLink: "https://marimo.io/discord?ref=notebook",
8
10
  docsPage: "https://docs.marimo.io",