Repository: marimo-team/marimo
Pull request #4384: improve: group error types in error output
Author: login: akshayka
Created: 2025-04-03T23:10:05Z | Merged: 2025-04-04T03:17:25Z

This PR improves the presentation of errors in the cell output area.

* Errors are grouped by error type, with all multiple definition errors
  appearing in a group, all cycle errors appearing in another group, and
  so on.
* Tips are shown once per error group, instead of once per error.
* More informative tip titles.
* Style improvements to make the error output more legible.

In a follow up, I will improve add docs pages with more context for multiple definition errors and import star errors, and will link to them from Tips.

Comparison images below.

**With changes.**

<details>
<summary>Single multiple definition error</summary>
<img src="https://github.com/user-attachments/assets/623357e8-f242-426c-83cd-3a4d7a5ee72a"></img>
</details>

<details>
<summary>Many multiple definition errors</summary>
<img src="https://github.com/user-attachments/assets/63bb5efc-8e8b-48f0-8505-0a9bb46a6183"></img>
</details>

<details>
<summary>Multiple types of errors</summary>
<img src="https://github.com/user-attachments/assets/406f7c16-6443-490a-99cf-9564537b5e4e"></img>
</details>

**Before changes.**

<details>
<summary>Single multiple definition error</summary>
<img src="https://github.com/user-attachments/assets/8debf560-f47b-45f2-9f9f-d6ed5cf9343c"></img>
</details>

<details>
<summary>Many multiple definition errors</summary>
<img src="https://github.com/user-attachments/assets/b9c31a5e-c5e8-415e-b4b2-c025beba5665"></img>
</details>

<details>
<summary>Multiple types of errors</summary>
<img src="https://github.com/user-attachments/assets/fd6dc901-0f49-47b7-aec8-63a36dcaa714"></img>
</details>



Diff:
diff --git a/frontend/src/components/editor/errors/auto-fix.tsx b/frontend/src/components/editor/errors/auto-fix.tsx
index df3336ffdef..1b4f5ea675c 100644
--- a/frontend/src/components/editor/errors/auto-fix.tsx
+++ b/frontend/src/components/editor/errors/auto-fix.tsx
@@ -6,12 +6,12 @@ import type { CellId } from "@/core/cells/ids";
 import { getAutoFixes } from "@/core/errors/errors";
 import type { MarimoError } from "@/core/kernel/messages";
 import { store } from "@/core/state/jotai";
-import { LightbulbIcon } from "lucide-react";
+import { WrenchIcon } from "lucide-react";
 
 export const AutoFixButton = ({
   errors,
   cellId,
-}: { errors: MarimoError[]; cellId: CellId }) => {
+}: { errors: MarimoError[]; cellId: CellId; className?: string }) => {
   const { createNewCell } = useCellActions();
   const autoFixes = errors.flatMap((error) => getAutoFixes(error));
 
@@ -27,7 +27,8 @@ export const AutoFixButton = ({
     <Tooltip content={firstFix.description} align="start">
       <Button
         size="xs"
-        variant="secondary"
+        variant="outline"
+        className="my-2 font-normal"
         onClick={() => {
           const editorView =
             store.get(notebookAtom).cellHandles[cellId].current?.editorView;
@@ -47,7 +48,7 @@ export const AutoFixButton = ({
           editorView?.focus();
         }}
       >
-        <LightbulbIcon className="h-3 w-3 mr-2" />
+        <WrenchIcon className="h-3 w-3 mr-2" />
         {firstFix.title}
       </Button>
     </Tooltip>
diff --git a/frontend/src/components/editor/output/MarimoErrorOutput.tsx b/frontend/src/components/editor/output/MarimoErrorOutput.tsx
index 69ebcf6e9b7..23debebf63b 100644
--- a/frontend/src/components/editor/output/MarimoErrorOutput.tsx
+++ b/frontend/src/components/editor/output/MarimoErrorOutput.tsx
@@ -1,7 +1,6 @@
 /* Copyright 2024 Marimo. All rights reserved. */
 
 import { cn } from "../../../utils/cn";
-import { logNever } from "../../../utils/assertNever";
 import type { MarimoError } from "../../../core/kernel/messages";
 import { Alert } from "../../ui/alert";
 import { AlertTitle } from "../../ui/alert";
@@ -18,22 +17,6 @@ import type { CellId } from "@/core/cells/ids";
 import { AutoFixButton } from "../errors/auto-fix";
 
 const Tip = (props: {
-  className?: string;
-  children: React.ReactNode;
-}): JSX.Element => {
-  return (
-    <Accordion type="single" collapsible={true} className={props.className}>
-      <AccordionItem value="item-1" className="text-muted-foreground">
-        <AccordionTrigger className="pt-4 pb-2">Tip</AccordionTrigger>
-        <AccordionContent className="mr-24 text-[0.84375rem]">
-          {props.children}
-        </AccordionContent>
-      </AccordionItem>
-    </Accordion>
-  );
-};
-
-const Explainer = (props: {
   title?: string;
   className?: string;
   children: React.ReactNode;
@@ -41,8 +24,8 @@ const Explainer = (props: {
   return (
     <Accordion type="single" collapsible={true} className={props.className}>
       <AccordionItem value="item-1" className="text-muted-foreground">
-        <AccordionTrigger className="pt-4 pb-2">
-          {props.title ?? "Learn more"}
+        <AccordionTrigger className="pt-2 pb-2 font-normal">
+          {props.title ?? "Tip"}
         </AccordionTrigger>
         <AccordionContent className="mr-24 text-[0.84375rem]">
           {props.children}
@@ -68,193 +51,368 @@ export const MarimoErrorOutput = ({
 }: Props): JSX.Element => {
   let titleContents = "This cell wasn't run because it has errors";
   let alertVariant: "destructive" | "default" = "destructive";
-  let textColor = "text-error";
+  let titleColor = "text-error";
   const liStyle = "my-0.5 ml-8 text-muted-foreground/40";
-  const msgs = errors.map((error, idx) => {
-    switch (error.type) {
-      case "syntax":
-      case "unknown":
-        return <p key={idx}>{error.msg}</p>;
 
-      case "cycle":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">
-              {"This cell is in a cycle:"}
-            </p>
-            <ul className="list-disc">
-              {error.edges_with_vars.map((edge) => (
-                <li className={liStyle} key={`${edge[0]}-${edge[1]}`}>
+  // Check for certain error types to adjust title and appearance
+  if (errors.some((e) => e.type === "interruption")) {
+    titleContents = "Interrupted";
+  } else if (errors.some((e) => e.type === "internal")) {
+    titleContents = "An internal error occurred";
+  } else if (errors.some((e) => e.type === "ancestor-prevented")) {
+    titleContents = "Ancestor prevented from running";
+    alertVariant = "default";
+    titleColor = "text-muted-foreground";
+    titleColor = "text-secondary-foreground";
+  } else if (errors.some((e) => e.type === "ancestor-stopped")) {
+    titleContents = "Ancestor stopped";
+    alertVariant = "default";
+    titleColor = "text-secondary-foreground";
+  } else {
+    // Check for exception type
+    const exceptionError = errors.find((e) => e.type === "exception");
+    if (exceptionError && "exception_type" in exceptionError) {
+      titleContents = exceptionError.exception_type;
+    }
+  }
+
+  // Group errors by type
+  const cycleErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "cycle" }> => e.type === "cycle",
+  );
+  const multipleDefsErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "multiple-defs" }> =>
+      e.type === "multiple-defs",
+  );
+  const importStarErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "import-star" }> =>
+      e.type === "import-star",
+  );
+  const deleteNonlocalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "delete-nonlocal" }> =>
+      e.type === "delete-nonlocal",
+  );
+  const interruptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "interruption" }> =>
+      e.type === "interruption",
+  );
+  const exceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "exception" }> =>
+      e.type === "exception",
+  );
+  const strictExceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "strict-exception" }> =>
+      e.type === "strict-exception",
+  );
+  const internalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "internal" }> =>
+      e.type === "internal",
+  );
+  const ancestorPreventedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-prevented" }> =>
+      e.type === "ancestor-prevented",
+  );
+  const ancestorStoppedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-stopped" }> =>
+      e.type === "ancestor-stopped",
+  );
+  const syntaxErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "syntax" }> => e.type === "syntax",
+  );
+  const unknownErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "unknown" }> => e.type === "unknown",
+  );
+
+  const renderMessages = () => {
+    const messages: JSX.Element[] = [];
+
+    if (syntaxErrors.length > 0 || unknownErrors.length > 0) {
+      messages.push(
+        <li key="syntax-unknown">
+          {syntaxErrors.map((error, idx) => (
+            <p key={`syntax-${idx}`}>{error.msg}</p>
+          ))}
+          {unknownErrors.map((error, idx) => (
+            <p key={`unknown-${idx}`}>{error.msg}</p>
+          ))}
+          {cellId && (
+            <AutoFixButton
+              errors={[...syntaxErrors, ...unknownErrors]}
+              cellId={cellId}
+            />
+          )}
+        </li>,
+      );
+    }
+
+    if (cycleErrors.length > 0) {
+      messages.push(
+        <li key="cycle">
+          <p className="text-muted-foreground font-medium">
+            This cell is in a cycle.
+          </p>
+          <ul className="list-disc">
+            {cycleErrors.flatMap((error, errorIdx) =>
+              error.edges_with_vars.map((edge, edgeIdx) => (
+                <li className={liStyle} key={`cycle-${errorIdx}-${edgeIdx}`}>
                   <CellLinkError cellId={edge[0] as CellId} />
                   <span className="text-muted-foreground">
                     {" -> "}
-                    {edge[1].length === 1 ? edge[1] : edge[1].join(", ")}
+                    {edge[1].length === 1 ? edge[1][0] : edge[1].join(", ")}
                     {" -> "}
                   </span>
                   <CellLinkError cellId={edge[2] as CellId} />
                 </li>
-              ))}
-            </ul>
-            <Tip>Merge these cells into a single cell.</Tip>
-          </Fragment>
-        );
+              )),
+            )}
+          </ul>
+          {cellId && <AutoFixButton errors={cycleErrors} cellId={cellId} />}
+          <Tip
+            title="What are cycles and how do I resolve them?"
+            className="mb-2"
+          >
+            <p className="pb-2">
+              An example of a cycle is if one cell declares a variable 'a' and
+              reads 'b', and another cell declares 'b' and and reads 'a'. Cycles
+              like this make it impossible for marimo to know how to run your
+              cells, and generally suggest that your code has a bug.
+            </p>
 
-      case "multiple-defs":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">{`The variable '${error.name}' was defined by another cell:`}</p>
-            <ul className="list-disc">
-              {error.cells.map((cid) => (
-                <li className={liStyle} key={cid}>
-                  <CellLinkError cellId={cid as CellId} />
-                </li>
-              ))}
-            </ul>
-            <Tip>
-              <p className="pb-2">
-                marimo requires that each variable is defined in just one cell.
-                This constraint enables reactive and reproducible execution,
-                arbitrary cell reordering, seamless UI elements, execution as a
-                script, and more.
-              </p>
+            <p className="py-2">
+              Try merging these cells into a single cell to eliminate the cycle.
+            </p>
+          </Tip>
+        </li>,
+      );
+    }
 
-              <p className="py-2">
-                Try merging this cell with the above cells or wrapping it in a
-                function. Alternatively, rename '{error.name}' to '_{error.name}
-                ' to make the variable private to this cell.
-              </p>
-            </Tip>
-          </Fragment>
-        );
+    if (multipleDefsErrors.length > 0) {
+      messages.push(
+        <li key="multiple-defs">
+          <p className="text-muted-foreground font-medium">
+            This cell redefines variables from other cells.
+          </p>
+          {multipleDefsErrors.map((error, idx) => (
+            <Fragment key={`multiple-defs-${idx}`}>
+              <p className="text-muted-foreground mt-2">{`'${error.name}' was also defined by:`}</p>
+              <ul className="list-disc">
+                {error.cells.map((cid, cidIdx) => (
+                  <li className={liStyle} key={`cell-${cidIdx}`}>
+                    <CellLinkError cellId={cid as CellId} />
+                  </li>
+                ))}
+              </ul>
+            </Fragment>
+          ))}
+          {cellId && (
+            <AutoFixButton errors={multipleDefsErrors} cellId={cellId} />
+          )}
+          <Tip title="Why can't I redefine variables?">
+            <p className="pb-2">
+              marimo requires that each variable is defined in just one cell.
+              This constraint enables reactive and reproducible execution,
+              arbitrary cell reordering, seamless UI elements, execution as a
+              script, and more.
+            </p>
 
-      case "import-star":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">{error.msg}</p>
+            <p className="py-2">
+              Try merging this cell with the mentioned cells or wrapping it in a
+              function. Alternatively, rename variables to make them private to
+              this cell by prefixing them with an underscore.
+            </p>
+          </Tip>
+        </li>,
+      );
+    }
 
-            <Explainer>
-              <p className="pb-2">
-                Star imports are incompatible with marimo's git-friendly file
-                format and reproducible reactive execution.
-              </p>
+    if (importStarErrors.length > 0) {
+      messages.push(
+        <li key="import-star">
+          {importStarErrors.map((error, idx) => (
+            <p key={`import-star-${idx}`} className="text-muted-foreground">
+              {error.msg}
+            </p>
+          ))}
+          {cellId && (
+            <AutoFixButton errors={importStarErrors} cellId={cellId} />
+          )}
+          <Tip title="Why can't I use `import *`?">
+            <p className="pb-2">
+              Star imports are incompatible with marimo's git-friendly file
+              format and reproducible reactive execution.
+            </p>
 
-              <p className="py-2">
-                marimo's Python file format stores code in functions, so
-                notebooks can be imported as regular Python modules without
-                executing all their code. But Python disallows `import *`
-                everywhere except at the top-level of a module.
-              </p>
+            <p className="py-2">
+              marimo's Python file format stores code in functions, so notebooks
+              can be imported as regular Python modules without executing all
+              their code. But Python disallows `import *` everywhere except at
+              the top-level of a module.
+            </p>
 
-              <p className="py-2">
-                Star imports would also silently add names to globals, which
-                would be incompatible with reactive execution.
-              </p>
-            </Explainer>
-          </Fragment>
-        );
+            <p className="py-2">
+              Star imports would also silently add names to globals, which would
+              be incompatible with reactive execution.
+            </p>
+          </Tip>
+        </li>,
+      );
+    }
 
-      case "delete-nonlocal":
-        return (
-          <Fragment key={idx}>
-            <div>
+    if (deleteNonlocalErrors.length > 0) {
+      messages.push(
+        <li key="delete-nonlocal">
+          {deleteNonlocalErrors.map((error, idx) => (
+            <div key={`delete-nonlocal-${idx}`}>
               {`The variable '${error.name}' can't be deleted because it was defined by another cell (`}
               <CellLinkError cellId={error.cells[0] as CellId} />
               {")"}
             </div>
-            <Tip>
-              Try refactoring so that you can delete '{error.name}' in the cell
-              that creates it.
-            </Tip>
-          </Fragment>
-        );
+          ))}
+          {cellId && (
+            <AutoFixButton errors={deleteNonlocalErrors} cellId={cellId} />
+          )}
+          <Tip title="Why can't I delete other cells' variables?">
+            marimo determines how to run your notebook based on variables
+            definitions and references only. When a cell deletes a variable it
+            didn't define, marimo cannot determine an unambiguous execution
+            order. Try refactoring so that you can delete variables in the cells
+            that create them.
+          </Tip>
+        </li>,
+      );
+    }
 
-      case "interruption":
-        titleContents = "Interrupted";
-        return (
-          <p key={idx}>{"This cell was interrupted and needs to be re-run."}</p>
-        );
+    if (interruptionErrors.length > 0) {
+      messages.push(
+        <li key="interruption">
+          {interruptionErrors.map((_, idx) => (
+            <p key={`interruption-${idx}`}>
+              {"This cell was interrupted and needs to be re-run."}
+            </p>
+          ))}
+          {cellId && (
+            <AutoFixButton errors={interruptionErrors} cellId={cellId} />
+          )}
+        </li>,
+      );
+    }
 
-      case "exception":
-        titleContents = error.exception_type;
-        return error.raising_cell == null ? (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">{error.msg}</p>
-            <div className="text-muted-foreground mt-2">
-              See the console area for a traceback.
-            </div>
-          </Fragment>
-        ) : (
-          <div key={idx}>
-            {error.msg}
-            <CellLinkError cellId={error.raising_cell as CellId} />
-            <Tip>
-              Fix the error in{" "}
-              <CellLinkError cellId={error.raising_cell as CellId} />, or handle
-              the exception in with a try/except block.
-            </Tip>
-          </div>
-        );
-      case "strict-exception":
-        return error.blamed_cell == null ? (
-          <Fragment key={idx}>
-            <p>{error.msg}</p>
-            <Tip>
-              Something is wrong with your declaration of `{error.ref}`. Fix any
-              discrepancies, or turn off strict execution.
-            </Tip>
-          </Fragment>
-        ) : (
-          <div key={idx}>
-            {error.msg}
-            <CellLinkError cellId={error.blamed_cell as CellId} />
+    if (exceptionErrors.length > 0) {
+      messages.push(
+        <li key="exception">
+          {exceptionErrors.map((error, idx) => (
+            <li className="my-2" key={`exception-${idx}`}>
+              {error.raising_cell == null ? (
+                <li>
+                  <p className="text-muted-foreground">{error.msg}</p>
+                  <div className="text-muted-foreground mt-2">
+                    See the console area for a traceback.
+                  </div>
+                </li>
+              ) : (
+                <div>
+                  {error.msg}
+                  <CellLinkError cellId={error.raising_cell as CellId} />
+                </div>
+              )}
+            </li>
+          ))}
+          {exceptionErrors.some((e) => e.raising_cell != null) && (
             <Tip>
-              Ensure that&nbsp;
-              <CellLinkError cellId={error.blamed_cell as CellId} />
-              &nbsp;defines the variable `{error.ref}`, or turn off strict
-              execution.
+              Fix the error in the mentioned cells, or handle the exceptions
+              with try/except blocks.
             </Tip>
-          </div>
-        );
-      case "internal":
-        titleContents = "An internal error occurred";
-        return <p key={idx}>{error.msg}</p>;
+          )}
+          {cellId && <AutoFixButton errors={exceptionErrors} cellId={cellId} />}
+        </li>,
+      );
+    }
 
-      case "ancestor-prevented":
-        titleContents = "Ancestor prevented from running";
-        alertVariant = "default";
-        textColor = "text-secondary-foreground";
-        return error.blamed_cell == null ? (
-          <div key={idx}>
-            {error.msg}
-            (<CellLinkError cellId={error.raising_cell as CellId} />)
-          </div>
-        ) : (
-          <div key={idx}>
-            {error.msg}
-            (<CellLinkError cellId={error.raising_cell as CellId} />
-            &nbsp;blames&nbsp;
-            <CellLinkError cellId={error.blamed_cell as CellId} />)
-          </div>
-        );
-      case "ancestor-stopped":
-        titleContents = "Ancestor stopped";
-        alertVariant = "default";
-        textColor = "text-secondary-foreground";
-        return (
-          <div key={idx}>
-            {error.msg}
-            <CellLinkError cellId={error.raising_cell as CellId} />
-          </div>
-        );
+    if (strictExceptionErrors.length > 0) {
+      messages.push(
+        <li key="strict-exception">
+          {strictExceptionErrors.map((error, idx) => (
+            <li className="my-2" key={`strict-exception-${idx}`}>
+              {error.blamed_cell == null ? (
+                <p>{error.msg}</p>
+              ) : (
+                <div>
+                  {error.msg}
+                  <CellLinkError cellId={error.blamed_cell as CellId} />
+                </div>
+              )}
+            </li>
+          ))}
+          {cellId && (
+            <AutoFixButton errors={strictExceptionErrors} cellId={cellId} />
+          )}
+          <Tip>
+            {strictExceptionErrors.some((e) => e.blamed_cell != null)
+              ? "Ensure that the referenced cells define the required variables, or turn off strict execution."
+              : "Something is wrong with your declarations. Fix any discrepancies, or turn off strict execution."}
+          </Tip>
+        </li>,
+      );
+    }
+
+    if (internalErrors.length > 0) {
+      messages.push(
+        <li key="internal">
+          {internalErrors.map((error, idx) => (
+            <p key={`internal-${idx}`}>{error.msg}</p>
+          ))}
+          {cellId && <AutoFixButton errors={internalErrors} cellId={cellId} />}
+        </li>,
+      );
+    }
+
+    if (ancestorPreventedErrors.length > 0) {
+      messages.push(
+        <li key="ancestor-prevented">
+          {ancestorPreventedErrors.map((error, idx) => (
+            <div key={`ancestor-prevented-${idx}`}>
+              {error.msg}
+              {error.blamed_cell == null ? (
+                <span>
+                  (<CellLinkError cellId={error.raising_cell as CellId} />)
+                </span>
+              ) : (
+                <span>
+                  (<CellLinkError cellId={error.raising_cell as CellId} />
+                  &nbsp;blames&nbsp;
+                  <CellLinkError cellId={error.blamed_cell as CellId} />)
+                </span>
+              )}
+            </div>
+          ))}
+          {cellId && (
+            <AutoFixButton errors={ancestorPreventedErrors} cellId={cellId} />
+          )}
+        </li>,
+      );
+    }
 
-      default:
-        logNever(error);
-        return null;
+    if (ancestorStoppedErrors.length > 0) {
+      messages.push(
+        <li key="ancestor-stopped">
+          {ancestorStoppedErrors.map((error, idx) => (
+            <div key={`ancestor-stopped-${idx}`}>
+              {error.msg}
+              <CellLinkError cellId={error.raising_cell as CellId} />
+            </div>
+          ))}
+          {cellId && (
+            <AutoFixButton errors={ancestorStoppedErrors} cellId={cellId} />
+          )}
+        </li>,
+      );
     }
-  });
+
+    return messages;
+  };
 
   const title = (
-    <AlertTitle className="font-code font-bold tracking-wide">
+    <AlertTitle className={`font-code font-medium tracking-wide ${titleColor}`}>
       {titleContents}
     </AlertTitle>
   );
@@ -263,15 +421,14 @@ export const MarimoErrorOutput = ({
     <Alert
       variant={alertVariant}
       className={cn(
-        `border-none font-code text-sm text-[0.84375rem] px-0 ${textColor} normal [&:has(svg)]:pl-0 space-y-4`,
+        "border-none font-code text-sm text-[0.84375rem] px-0 text-muted-foreground normal [&:has(svg)]:pl-0 space-y-4",
         className,
       )}
     >
       {title}
       <div>
-        <ul>{msgs}</ul>
+        <ul className="flex flex-col gap-8">{renderMessages()}</ul>
       </div>
-      {cellId && <AutoFixButton errors={errors} cellId={cellId} />}
     </Alert>
   );
 };
diff --git a/frontend/src/core/errors/__tests__/errors.test.ts b/frontend/src/core/errors/__tests__/errors.test.ts
index 32da1c60873..8439c90c7ea 100644
--- a/frontend/src/core/errors/__tests__/errors.test.ts
+++ b/frontend/src/core/errors/__tests__/errors.test.ts
@@ -26,7 +26,7 @@ describe("getAutoFixes", () => {
 
     const fixes = getAutoFixes(error);
     expect(fixes).toHaveLength(1);
-    expect(fixes[0].title).toBe("Wrap in a function");
+    expect(fixes[0].title).toBe("Fix: Wrap in a function");
   });
 
   it("returns import fix for NameError with known import", () => {
@@ -38,7 +38,7 @@ describe("getAutoFixes", () => {
 
     const fixes = getAutoFixes(error);
     expect(fixes).toHaveLength(1);
-    expect(fixes[0].title).toBe("Add 'import numpy as np'");
+    expect(fixes[0].title).toBe("Fix: Add 'import numpy as np'");
   });
 
   it("returns no fixes for NameError with unknown import", () => {
diff --git a/frontend/src/core/errors/errors.ts b/frontend/src/core/errors/errors.ts
index 369266db426..e06ef2413bf 100644
--- a/frontend/src/core/errors/errors.ts
+++ b/frontend/src/core/errors/errors.ts
@@ -19,7 +19,7 @@ export function getAutoFixes(error: MarimoError): AutoFix[] {
   if (error.type === "multiple-defs") {
     return [
       {
-        title: "Wrap in a function",
+        title: "Fix: Wrap in a function",
         description:
           "Make this cell's variables local by wrapping the cell in a function.",
         onFix: async (ctx) => {
@@ -48,7 +48,7 @@ export function getAutoFixes(error: MarimoError): AutoFix[] {
 
     return [
       {
-        title: `Add '${cellCode}'`,
+        title: `Fix: Add '${cellCode}'`,
         description: "Add a new cell for the missing import",
         onFix: async (ctx) => {
           ctx.addCodeBelow(cellCode);


Reviews:
user: login: dmadisetti
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T00:23:11Z
user: login: akshayka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T00:35:39Z
user: login: mscolnick
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T02:28:55Z

Review comments:
diff_hunk: @@ -68,193 +51,356 @@ export const MarimoErrorOutput = ({
 }: Props): JSX.Element => {
   let titleContents = "This cell wasn't run because it has errors";
   let alertVariant: "destructive" | "default" = "destructive";
-  let textColor = "text-error";
+  let titleColor = "text-error";
   const liStyle = "my-0.5 ml-8 text-muted-foreground/40";
-  const msgs = errors.map((error, idx) => {
-    switch (error.type) {
-      case "syntax":
-      case "unknown":
-        return <p key={idx}>{error.msg}</p>;
 
-      case "cycle":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">
-              {"This cell is in a cycle:"}
-            </p>
-            <ul className="list-disc">
-              {error.edges_with_vars.map((edge) => (
-                <li className={liStyle} key={`${edge[0]}-${edge[1]}`}>
+  // Check for certain error types to adjust title and appearance
+  if (errors.some((e) => e.type === "interruption")) {
+    titleContents = "Interrupted";
+  } else if (errors.some((e) => e.type === "internal")) {
+    titleContents = "An internal error occurred";
+  } else if (errors.some((e) => e.type === "ancestor-prevented")) {
+    titleContents = "Ancestor prevented from running";
+    alertVariant = "default";
+    titleColor = "text-muted-foreground";
+    titleColor = "text-secondary-foreground";
+  } else if (errors.some((e) => e.type === "ancestor-stopped")) {
+    titleContents = "Ancestor stopped";
+    alertVariant = "default";
+    titleColor = "text-secondary-foreground";
+  } else {
+    // Check for exception type
+    const exceptionError = errors.find((e) => e.type === "exception");
+    if (exceptionError && "exception_type" in exceptionError) {
+      titleContents = exceptionError.exception_type;
+    }
+  }
+
+  // Group errors by type
+  const cycleErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "cycle" }> => e.type === "cycle",
+  );
+  const multipleDefsErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "multiple-defs" }> =>
+      e.type === "multiple-defs",
+  );
+  const importStarErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "import-star" }> =>
+      e.type === "import-star",
+  );
+  const deleteNonlocalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "delete-nonlocal" }> =>
+      e.type === "delete-nonlocal",
+  );
+  const interruptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "interruption" }> =>
+      e.type === "interruption",
+  );
+  const exceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "exception" }> =>
+      e.type === "exception",
+  );
+  const strictExceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "strict-exception" }> =>
+      e.type === "strict-exception",
+  );
+  const internalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "internal" }> =>
+      e.type === "internal",
+  );
+  const ancestorPreventedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-prevented" }> =>
+      e.type === "ancestor-prevented",
+  );
+  const ancestorStoppedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-stopped" }> =>
+      e.type === "ancestor-stopped",
+  );
+  const syntaxErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "syntax" }> => e.type === "syntax",
+  );
+  const unknownErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "unknown" }> => e.type === "unknown",
+  );
+
+  const renderMessages = () => {
+    const messages: JSX.Element[] = [];
+
+    if (syntaxErrors.length > 0 || unknownErrors.length > 0) {
+      messages.push(
+        <li key="syntax-unknown">
+          {syntaxErrors.map((error, idx) => (
+            <p key={`syntax-${idx}`}>{error.msg}</p>
+          ))}
+          {unknownErrors.map((error, idx) => (
+            <p key={`unknown-${idx}`}>{error.msg}</p>
+          ))}
+          {cellId && (
+            <AutoFixButton
+              errors={[...syntaxErrors, ...unknownErrors]}
+              cellId={cellId}
+            />
+          )}
+        </li>,
+      );
+    }
+
+    if (cycleErrors.length > 0) {
+      messages.push(
+        <li key="cycle">
+          <p className="text-muted-foreground font-medium">
+            This cell is in a cycle.
+          </p>
+          <ul className="list-disc">
+            {cycleErrors.flatMap((error, errorIdx) =>
+              error.edges_with_vars.map((edge, edgeIdx) => (
+                <li className={liStyle} key={`cycle-${errorIdx}-${edgeIdx}`}>
                   <CellLinkError cellId={edge[0] as CellId} />
                   <span className="text-muted-foreground">
                     {" -> "}
-                    {edge[1].length === 1 ? edge[1] : edge[1].join(", ")}
+                    {edge[1].length === 1 ? edge[1][0] : edge[1].join(", ")}
                     {" -> "}
                   </span>
                   <CellLinkError cellId={edge[2] as CellId} />
                 </li>
-              ))}
-            </ul>
-            <Tip>Merge these cells into a single cell.</Tip>
-          </Fragment>
-        );
-
-      case "multiple-defs":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">{`The variable '${error.name}' was defined by another cell:`}</p>
-            <ul className="list-disc">
-              {error.cells.map((cid) => (
-                <li className={liStyle} key={cid}>
-                  <CellLinkError cellId={cid as CellId} />
-                </li>
-              ))}
-            </ul>
-            <Tip>
-              <p className="pb-2">
-                marimo requires that each variable is defined in just one cell.
-                This constraint enables reactive and reproducible execution,
-                arbitrary cell reordering, seamless UI elements, execution as a
-                script, and more.
-              </p>
+              )),
+            )}
+          </ul>
+          {cellId && <AutoFixButton errors={cycleErrors} cellId={cellId} />}
+          <Tip title="Resolving cycles" className="mb-2">
path: frontend/src/components/editor/output/MarimoErrorOutput.tsx
user: login: dmadisetti
body: I think this copy could do with an update too?
Ideas:

 - Why does my code have cycles, and how can I resolve them?
 - How do I remove cycles?
 - Why can't I have cycles?

Phrasing the help as a question works in the latter case
created_at: 2025-04-04T00:23:10Z
author_association: COLLABORATOR
diff_hunk: @@ -68,193 +51,356 @@ export const MarimoErrorOutput = ({
 }: Props): JSX.Element => {
   let titleContents = "This cell wasn't run because it has errors";
   let alertVariant: "destructive" | "default" = "destructive";
-  let textColor = "text-error";
+  let titleColor = "text-error";
   const liStyle = "my-0.5 ml-8 text-muted-foreground/40";
-  const msgs = errors.map((error, idx) => {
-    switch (error.type) {
-      case "syntax":
-      case "unknown":
-        return <p key={idx}>{error.msg}</p>;
 
-      case "cycle":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">
-              {"This cell is in a cycle:"}
-            </p>
-            <ul className="list-disc">
-              {error.edges_with_vars.map((edge) => (
-                <li className={liStyle} key={`${edge[0]}-${edge[1]}`}>
+  // Check for certain error types to adjust title and appearance
+  if (errors.some((e) => e.type === "interruption")) {
+    titleContents = "Interrupted";
+  } else if (errors.some((e) => e.type === "internal")) {
+    titleContents = "An internal error occurred";
+  } else if (errors.some((e) => e.type === "ancestor-prevented")) {
+    titleContents = "Ancestor prevented from running";
+    alertVariant = "default";
+    titleColor = "text-muted-foreground";
+    titleColor = "text-secondary-foreground";
+  } else if (errors.some((e) => e.type === "ancestor-stopped")) {
+    titleContents = "Ancestor stopped";
+    alertVariant = "default";
+    titleColor = "text-secondary-foreground";
+  } else {
+    // Check for exception type
+    const exceptionError = errors.find((e) => e.type === "exception");
+    if (exceptionError && "exception_type" in exceptionError) {
+      titleContents = exceptionError.exception_type;
+    }
+  }
+
+  // Group errors by type
+  const cycleErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "cycle" }> => e.type === "cycle",
+  );
+  const multipleDefsErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "multiple-defs" }> =>
+      e.type === "multiple-defs",
+  );
+  const importStarErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "import-star" }> =>
+      e.type === "import-star",
+  );
+  const deleteNonlocalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "delete-nonlocal" }> =>
+      e.type === "delete-nonlocal",
+  );
+  const interruptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "interruption" }> =>
+      e.type === "interruption",
+  );
+  const exceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "exception" }> =>
+      e.type === "exception",
+  );
+  const strictExceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "strict-exception" }> =>
+      e.type === "strict-exception",
+  );
+  const internalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "internal" }> =>
+      e.type === "internal",
+  );
+  const ancestorPreventedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-prevented" }> =>
+      e.type === "ancestor-prevented",
+  );
+  const ancestorStoppedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-stopped" }> =>
+      e.type === "ancestor-stopped",
+  );
+  const syntaxErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "syntax" }> => e.type === "syntax",
+  );
+  const unknownErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "unknown" }> => e.type === "unknown",
+  );
+
+  const renderMessages = () => {
+    const messages: JSX.Element[] = [];
+
+    if (syntaxErrors.length > 0 || unknownErrors.length > 0) {
+      messages.push(
+        <li key="syntax-unknown">
+          {syntaxErrors.map((error, idx) => (
+            <p key={`syntax-${idx}`}>{error.msg}</p>
+          ))}
+          {unknownErrors.map((error, idx) => (
+            <p key={`unknown-${idx}`}>{error.msg}</p>
+          ))}
+          {cellId && (
+            <AutoFixButton
+              errors={[...syntaxErrors, ...unknownErrors]}
+              cellId={cellId}
+            />
+          )}
+        </li>,
+      );
+    }
+
+    if (cycleErrors.length > 0) {
+      messages.push(
+        <li key="cycle">
+          <p className="text-muted-foreground font-medium">
+            This cell is in a cycle.
+          </p>
+          <ul className="list-disc">
+            {cycleErrors.flatMap((error, errorIdx) =>
+              error.edges_with_vars.map((edge, edgeIdx) => (
+                <li className={liStyle} key={`cycle-${errorIdx}-${edgeIdx}`}>
                   <CellLinkError cellId={edge[0] as CellId} />
                   <span className="text-muted-foreground">
                     {" -> "}
-                    {edge[1].length === 1 ? edge[1] : edge[1].join(", ")}
+                    {edge[1].length === 1 ? edge[1][0] : edge[1].join(", ")}
                     {" -> "}
                   </span>
                   <CellLinkError cellId={edge[2] as CellId} />
                 </li>
-              ))}
-            </ul>
-            <Tip>Merge these cells into a single cell.</Tip>
-          </Fragment>
-        );
-
-      case "multiple-defs":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">{`The variable '${error.name}' was defined by another cell:`}</p>
-            <ul className="list-disc">
-              {error.cells.map((cid) => (
-                <li className={liStyle} key={cid}>
-                  <CellLinkError cellId={cid as CellId} />
-                </li>
-              ))}
-            </ul>
-            <Tip>
-              <p className="pb-2">
-                marimo requires that each variable is defined in just one cell.
-                This constraint enables reactive and reproducible execution,
-                arbitrary cell reordering, seamless UI elements, execution as a
-                script, and more.
-              </p>
+              )),
+            )}
+          </ul>
+          {cellId && <AutoFixButton errors={cycleErrors} cellId={cellId} />}
+          <Tip title="Resolving cycles" className="mb-2">
path: frontend/src/components/editor/output/MarimoErrorOutput.tsx
user: login: dmadisetti
body: I think this copy could do with an update too?
Ideas:

 - Why does my code have cycles, and how can I resolve them?
 - How do I remove cycles?
 - Why can't I have cycles?

Phrasing the help as a question works in the latter case
created_at: 2025-04-04T00:23:10Z
author_association: COLLABORATOR
diff_hunk: @@ -68,193 +51,356 @@ export const MarimoErrorOutput = ({
 }: Props): JSX.Element => {
   let titleContents = "This cell wasn't run because it has errors";
   let alertVariant: "destructive" | "default" = "destructive";
-  let textColor = "text-error";
+  let titleColor = "text-error";
   const liStyle = "my-0.5 ml-8 text-muted-foreground/40";
-  const msgs = errors.map((error, idx) => {
-    switch (error.type) {
-      case "syntax":
-      case "unknown":
-        return <p key={idx}>{error.msg}</p>;
 
-      case "cycle":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">
-              {"This cell is in a cycle:"}
-            </p>
-            <ul className="list-disc">
-              {error.edges_with_vars.map((edge) => (
-                <li className={liStyle} key={`${edge[0]}-${edge[1]}`}>
+  // Check for certain error types to adjust title and appearance
+  if (errors.some((e) => e.type === "interruption")) {
+    titleContents = "Interrupted";
+  } else if (errors.some((e) => e.type === "internal")) {
+    titleContents = "An internal error occurred";
+  } else if (errors.some((e) => e.type === "ancestor-prevented")) {
+    titleContents = "Ancestor prevented from running";
+    alertVariant = "default";
+    titleColor = "text-muted-foreground";
+    titleColor = "text-secondary-foreground";
+  } else if (errors.some((e) => e.type === "ancestor-stopped")) {
+    titleContents = "Ancestor stopped";
+    alertVariant = "default";
+    titleColor = "text-secondary-foreground";
+  } else {
+    // Check for exception type
+    const exceptionError = errors.find((e) => e.type === "exception");
+    if (exceptionError && "exception_type" in exceptionError) {
+      titleContents = exceptionError.exception_type;
+    }
+  }
+
+  // Group errors by type
+  const cycleErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "cycle" }> => e.type === "cycle",
+  );
+  const multipleDefsErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "multiple-defs" }> =>
+      e.type === "multiple-defs",
+  );
+  const importStarErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "import-star" }> =>
+      e.type === "import-star",
+  );
+  const deleteNonlocalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "delete-nonlocal" }> =>
+      e.type === "delete-nonlocal",
+  );
+  const interruptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "interruption" }> =>
+      e.type === "interruption",
+  );
+  const exceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "exception" }> =>
+      e.type === "exception",
+  );
+  const strictExceptionErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "strict-exception" }> =>
+      e.type === "strict-exception",
+  );
+  const internalErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "internal" }> =>
+      e.type === "internal",
+  );
+  const ancestorPreventedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-prevented" }> =>
+      e.type === "ancestor-prevented",
+  );
+  const ancestorStoppedErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "ancestor-stopped" }> =>
+      e.type === "ancestor-stopped",
+  );
+  const syntaxErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "syntax" }> => e.type === "syntax",
+  );
+  const unknownErrors = errors.filter(
+    (e): e is Extract<MarimoError, { type: "unknown" }> => e.type === "unknown",
+  );
+
+  const renderMessages = () => {
+    const messages: JSX.Element[] = [];
+
+    if (syntaxErrors.length > 0 || unknownErrors.length > 0) {
+      messages.push(
+        <li key="syntax-unknown">
+          {syntaxErrors.map((error, idx) => (
+            <p key={`syntax-${idx}`}>{error.msg}</p>
+          ))}
+          {unknownErrors.map((error, idx) => (
+            <p key={`unknown-${idx}`}>{error.msg}</p>
+          ))}
+          {cellId && (
+            <AutoFixButton
+              errors={[...syntaxErrors, ...unknownErrors]}
+              cellId={cellId}
+            />
+          )}
+        </li>,
+      );
+    }
+
+    if (cycleErrors.length > 0) {
+      messages.push(
+        <li key="cycle">
+          <p className="text-muted-foreground font-medium">
+            This cell is in a cycle.
+          </p>
+          <ul className="list-disc">
+            {cycleErrors.flatMap((error, errorIdx) =>
+              error.edges_with_vars.map((edge, edgeIdx) => (
+                <li className={liStyle} key={`cycle-${errorIdx}-${edgeIdx}`}>
                   <CellLinkError cellId={edge[0] as CellId} />
                   <span className="text-muted-foreground">
                     {" -> "}
-                    {edge[1].length === 1 ? edge[1] : edge[1].join(", ")}
+                    {edge[1].length === 1 ? edge[1][0] : edge[1].join(", ")}
                     {" -> "}
                   </span>
                   <CellLinkError cellId={edge[2] as CellId} />
                 </li>
-              ))}
-            </ul>
-            <Tip>Merge these cells into a single cell.</Tip>
-          </Fragment>
-        );
-
-      case "multiple-defs":
-        return (
-          <Fragment key={idx}>
-            <p className="text-muted-foreground">{`The variable '${error.name}' was defined by another cell:`}</p>
-            <ul className="list-disc">
-              {error.cells.map((cid) => (
-                <li className={liStyle} key={cid}>
-                  <CellLinkError cellId={cid as CellId} />
-                </li>
-              ))}
-            </ul>
-            <Tip>
-              <p className="pb-2">
-                marimo requires that each variable is defined in just one cell.
-                This constraint enables reactive and reproducible execution,
-                arbitrary cell reordering, seamless UI elements, execution as a
-                script, and more.
-              </p>
+              )),
+            )}
+          </ul>
+          {cellId && <AutoFixButton errors={cycleErrors} cellId={cellId} />}
+          <Tip title="Resolving cycles" className="mb-2">
path: frontend/src/components/editor/output/MarimoErrorOutput.tsx
user: login: akshayka
body: Thanks, done!
created_at: 2025-04-04T00:35:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-03T23:10:10Z
body: [vc]: #coaRv5hZBW67TO0WmnmgNqrqmaqFEgVVfRQMAemvl1E=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtYXJpbW8tZG9jcyIsInJvb3REaXJlY3RvcnkiOiJkb2NzIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Im1hcmltby1kb2NzLWdpdC1ha2EtaW1wcm92ZS1ncm91cC1lcnJvci10eXBlcy1tYXJpbW8udmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbWFyaW1vL21hcmltby1kb2NzLzJvcmRBZkZhejlkOHVldlFmUE5pcE13eXNic0YiLCJwcmV2aWV3VXJsIjoibWFyaW1vLWRvY3MtZ2l0LWFrYS1pbXByb3ZlLWdyb3VwLWVycm9yLXR5cGVzLW1hcmltby52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn0seyJuYW1lIjoibWFyaW1vLXN0b3J5Ym9vayIsInJvb3REaXJlY3RvcnkiOiJmcm9udGVuZCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJtYXJpbW8tc3Rvcnlib29rLWdpdC1ha2EtaW1wcm92ZS1ncm91cC1lcnJvci10eXBlcy1tYXJpbW8udmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbWFyaW1vL21hcmltby1zdG9yeWJvb2svQmZKaXNGRG9LZzhwcGVyNFpkVlQ1RXhKQ2JtaSIsInByZXZpZXdVcmwiOiJtYXJpbW8tc3Rvcnlib29rLWdpdC1ha2EtaW1wcm92ZS1ncm91cC1lcnJvci10eXBlcy1tYXJpbW8udmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **marimo-docs** | ✅ Ready ([Inspect](https://vercel.com/marimo/marimo-docs/2ordAfFaz9d8uevQfPNipMwysbsF)) | [Visit Preview](https://marimo-docs-git-aka-improve-group-error-types-marimo.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/marimo-docs-git-aka-improve-group-error-types-marimo.vercel.app?via=pr-comment-feedback-link) | Apr 4, 2025 0:37am |
| **marimo-storybook** | ✅ Ready ([Inspect](https://vercel.com/marimo/marimo-storybook/BfJisFDoKg8pper4ZdVT5ExJCbmi)) | [Visit Preview](https://marimo-storybook-git-aka-improve-group-error-types-marimo.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/marimo-storybook-git-aka-improve-group-error-types-marimo.vercel.app?via=pr-comment-feedback-link) | Apr 4, 2025 0:37am |



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-04T03:26:30Z
body: 🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.12.5-dev3
author_association: NONE