@marimo-team/islands 0.23.9-dev4 → 0.23.9-dev6

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.
@@ -17,15 +17,17 @@ import { Button } from "@/components/ui/button";
17
17
  import { Kbd } from "@/components/ui/kbd";
18
18
  import { ExternalLink } from "@/components/ui/links";
19
19
  import type { CellId } from "@/core/cells/ids";
20
+ import { renderHTML } from "@/plugins/core/RenderHTML";
21
+ import { splitMangledLocals } from "@/utils/local-variables";
20
22
  import type { MarimoError } from "../../../core/kernel/messages";
21
23
  import { cn } from "../../../utils/cn";
22
24
  import { Alert, AlertTitle } from "../../ui/alert";
23
25
  import { openPackageManager } from "../chrome/panels/packages-utils";
24
26
  import { useChromeActions } from "../chrome/state";
25
27
  import { AutoFixButton } from "../errors/auto-fix";
28
+ import { MangledSegments } from "../errors/mangled-local-chip";
26
29
  import { CellLinkError } from "../links/cell-link";
27
30
  import { processTextForUrls } from "./console/text-rendering";
28
- import { renderHTML } from "@/plugins/core/RenderHTML";
29
31
 
30
32
  const Tip = (props: {
31
33
  title?: string;
@@ -454,10 +456,13 @@ export const MarimoErrorOutput = ({
454
456
  error.exception_type === "NameError" &&
455
457
  error.msg.startsWith("name '_")
456
458
  ) {
459
+ const segments = splitMangledLocals(error.msg);
457
460
  return (
458
461
  <li className="my-2" key={`exception-${idx}`}>
459
462
  <div>
460
- <p className="text-muted-foreground">{error.msg}</p>
463
+ <p className="text-muted-foreground">
464
+ <MangledSegments segments={segments} />
465
+ </p>
461
466
  <p className="text-muted-foreground mt-2">
462
467
  Variables prefixed with an underscore are local to a cell{" "}
463
468
  (
@@ -36,6 +36,10 @@ import { isWasm } from "@/core/wasm/utils";
36
36
  import { renderHTML } from "@/plugins/core/RenderHTML";
37
37
  import { sanitizeHtml } from "@/plugins/core/sanitize-html";
38
38
  import { copyToClipboard } from "@/utils/copy";
39
+ import {
40
+ containsMangledLocal,
41
+ splitMangledLocals,
42
+ } from "@/utils/local-variables";
39
43
  import {
40
44
  elementContainsMarimoCellFile,
41
45
  extractAllTracebackInfo,
@@ -43,6 +47,7 @@ import {
43
47
  } from "@/utils/traceback";
44
48
  import { cn } from "../../../utils/cn";
45
49
  import { AIFixButton } from "../errors/auto-fix";
50
+ import { MangledSegments } from "../errors/mangled-local-chip";
46
51
  import { CellLinkTraceback } from "../links/cell-link";
47
52
  import type { OnRefactorWithAI } from "../Output";
48
53
 
@@ -64,7 +69,11 @@ export const MarimoTracebackOutput = ({
64
69
  }: Props): JSX.Element => {
65
70
  const htmlTraceback = renderHTML({
66
71
  html: traceback,
67
- additionalReplacements: [replaceTracebackFilenames, replaceTracebackPrefix],
72
+ additionalReplacements: [
73
+ replaceTracebackFilenames,
74
+ replaceTracebackPrefix,
75
+ replaceMangledLocal,
76
+ ],
68
77
  });
69
78
  const [expanded, setExpanded] = useState(true);
70
79
 
@@ -94,6 +103,9 @@ export const MarimoTracebackOutput = ({
94
103
  };
95
104
 
96
105
  const [error, errorMessage] = lastTracebackLine.split(":", 2);
106
+ const errorMessageSegments = errorMessage
107
+ ? splitMangledLocals(errorMessage)
108
+ : [];
97
109
 
98
110
  return (
99
111
  <div className="flex flex-col gap-2 min-w-full w-fit">
@@ -111,7 +123,7 @@ export const MarimoTracebackOutput = ({
111
123
  />
112
124
  <div className="text-sm inline font-mono">
113
125
  <span className="text-destructive">{error || "Error"}:</span>{" "}
114
- {errorMessage}
126
+ <MangledSegments segments={errorMessageSegments} />
115
127
  </div>
116
128
  </div>
117
129
  <AccordionContent className="text-muted-foreground px-4 pt-2 text-xs overflow-auto">
@@ -249,6 +261,23 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
249
261
  }
250
262
  };
251
263
 
264
+ /**
265
+ * Replace any cell-local mangled name (`_cell_<id>_<name>`) inside a text
266
+ * node with a {@link MangledLocalChip}. The mangled name appears in both
267
+ * the final `NameError:` line and inside compiled-cell source lines because
268
+ * the compiler rewrites underscore-prefixed references at AST-visit time.
269
+ */
270
+ export const replaceMangledLocal = (domNode: DOMNode) => {
271
+ if (!(domNode instanceof Text) || !domNode.nodeValue) {
272
+ return;
273
+ }
274
+ if (!containsMangledLocal(domNode.nodeValue)) {
275
+ return;
276
+ }
277
+ const segments = splitMangledLocals(domNode.nodeValue);
278
+ return <MangledSegments segments={segments} />;
279
+ };
280
+
252
281
  export const replaceTracebackPrefix = (domNode: DOMNode) => {
253
282
  if (
254
283
  domNode instanceof Text &&
@@ -190,6 +190,7 @@ interface Data<T> {
190
190
  fieldTypes?: FieldTypesWithExternalType | null;
191
191
  freezeColumnsLeft?: string[];
192
192
  freezeColumnsRight?: string[];
193
+ hiddenColumns?: string[];
193
194
  textJustifyColumns?: Record<string, "left" | "center" | "right">;
194
195
  wrappedColumns?: string[];
195
196
  headerTooltip?: Record<string, string>;
@@ -265,6 +266,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
265
266
  rowHeaders: columnToFieldTypesSchema,
266
267
  freezeColumnsLeft: z.array(z.string()).optional(),
267
268
  freezeColumnsRight: z.array(z.string()).optional(),
269
+ hiddenColumns: z.array(z.string()).optional(),
268
270
  textJustifyColumns: z
269
271
  .record(z.string(), z.enum(["left", "center", "right"]))
270
272
  .optional(),
@@ -814,6 +816,7 @@ const DataTableComponent = ({
814
816
  reloading,
815
817
  freezeColumnsLeft,
816
818
  freezeColumnsRight,
819
+ hiddenColumns,
817
820
  textJustifyColumns,
818
821
  wrappedColumns,
819
822
  headerTooltip,
@@ -1090,6 +1093,7 @@ const DataTableComponent = ({
1090
1093
  onRowSelectionChange={handleRowSelectionChange}
1091
1094
  freezeColumnsLeft={freezeColumnsLeft}
1092
1095
  freezeColumnsRight={freezeColumnsRight}
1096
+ hiddenColumns={hiddenColumns}
1093
1097
  onCellSelectionChange={handleCellSelectionChange}
1094
1098
  getRowIds={get_row_ids}
1095
1099
  toggleDisplayHeader={toggleDisplayHeader}
@@ -0,0 +1,132 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ containsMangledLocal,
5
+ splitMangledLocals,
6
+ unmangleLocal,
7
+ } from "../local-variables";
8
+
9
+ describe("unmangleLocal", () => {
10
+ it("extracts the cell id and original name", () => {
11
+ expect(unmangleLocal("_cell_Hbol_a")).toEqual({
12
+ cellId: "Hbol",
13
+ name: "_a",
14
+ });
15
+ });
16
+
17
+ it("handles names with multiple underscore segments", () => {
18
+ expect(unmangleLocal("_cell_Hbol_a_b")).toEqual({
19
+ cellId: "Hbol",
20
+ name: "_a_b",
21
+ });
22
+ });
23
+
24
+ it("returns null for non-mangled strings", () => {
25
+ expect(unmangleLocal("just_a_variable")).toBeNull();
26
+ expect(unmangleLocal("_private")).toBeNull();
27
+ });
28
+
29
+ it("handles the single-underscore local", () => {
30
+ // `_` is a valid local name (variables.py:62-63), mangling to
31
+ // `_cell_<id>_` with no trailing suffix.
32
+ expect(unmangleLocal("_cell_Hbol_")).toEqual({
33
+ cellId: "Hbol",
34
+ name: "_",
35
+ });
36
+ });
37
+
38
+ it("handles UUID-style cell ids", () => {
39
+ // External / VSCode notebooks use `external_prefix()` which is a
40
+ // `uuid4()` (hyphenated).
41
+ expect(
42
+ unmangleLocal("_cell_c9bf9e57-1685-4c89-bafb-ff5af830be8a_a"),
43
+ ).toEqual({
44
+ cellId: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
45
+ name: "_a",
46
+ });
47
+ });
48
+
49
+ it("does not match marimo cell file paths", () => {
50
+ // The compiled cell file is `__marimo__cell_<id>_.py` (two leading
51
+ // underscores, trailing `_` with no name); it must not be confused with a
52
+ // mangled local.
53
+ expect(unmangleLocal("__marimo__cell_Hbol_.py")).toBeNull();
54
+ expect(unmangleLocal("/tmp/marimo_42/__marimo__cell_Hbol_.py")).toBeNull();
55
+ });
56
+ });
57
+
58
+ describe("splitMangledLocals", () => {
59
+ it("returns a single text segment when nothing matches", () => {
60
+ expect(splitMangledLocals("plain text")).toEqual(["plain text"]);
61
+ });
62
+
63
+ it("splits a NameError message", () => {
64
+ expect(splitMangledLocals("name '_cell_Hbol_a' is not defined")).toEqual([
65
+ "name '",
66
+ { cellId: "Hbol", name: "_a" },
67
+ "' is not defined",
68
+ ]);
69
+ });
70
+
71
+ it("handles multiple mangled names in one string", () => {
72
+ expect(splitMangledLocals("_cell_AAAA_x and _cell_BBBB_y")).toEqual([
73
+ { cellId: "AAAA", name: "_x" },
74
+ " and ",
75
+ { cellId: "BBBB", name: "_y" },
76
+ ]);
77
+ });
78
+
79
+ it("leaves the cell file path alone", () => {
80
+ const path = "/tmp/marimo_42/__marimo__cell_Hbol_.py";
81
+ expect(splitMangledLocals(path)).toEqual([path]);
82
+ });
83
+
84
+ it("ignores `_cell_...` substrings preceded by `_`", () => {
85
+ // Mirrors `(?<!_)` in `variables.py`: a leading `_` (e.g. inside
86
+ // `__marimo__cell_<id>_<...>`) means this is not a mangle the compiler
87
+ // produced, so we must not demangle it.
88
+ const text = "see __marimo__cell_Hbol_a for details";
89
+ expect(splitMangledLocals(text)).toEqual([text]);
90
+ });
91
+
92
+ it("splits a UUID-style cell id", () => {
93
+ expect(
94
+ splitMangledLocals(
95
+ "name '_cell_c9bf9e57-1685-4c89-bafb-ff5af830be8a_a' is not defined",
96
+ ),
97
+ ).toEqual([
98
+ "name '",
99
+ {
100
+ cellId: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
101
+ name: "_a",
102
+ },
103
+ "' is not defined",
104
+ ]);
105
+ });
106
+
107
+ it("splits the single-underscore local", () => {
108
+ expect(splitMangledLocals("name '_cell_Hbol_' is not defined")).toEqual([
109
+ "name '",
110
+ { cellId: "Hbol", name: "_" },
111
+ "' is not defined",
112
+ ]);
113
+ });
114
+ });
115
+
116
+ describe("containsMangledLocal", () => {
117
+ it("detects mangled names", () => {
118
+ expect(containsMangledLocal("name '_cell_Hbol_a' is not defined")).toBe(
119
+ true,
120
+ );
121
+ });
122
+
123
+ it("ignores the cell file path", () => {
124
+ expect(containsMangledLocal("/tmp/marimo_42/__marimo__cell_Hbol_.py")).toBe(
125
+ false,
126
+ );
127
+ });
128
+
129
+ it("ignores `_cell_...` substrings preceded by `_`", () => {
130
+ expect(containsMangledLocal("see __marimo__cell_Hbol_a")).toBe(false);
131
+ });
132
+ });
@@ -0,0 +1,67 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { CellId } from "@/core/cells/ids";
3
+
4
+ /**
5
+ * The marimo compiler rewrites underscore-prefixed references inside a cell
6
+ * (which Python would treat as module-private) into `_cell_<cell_id>_<name>`
7
+ * so each cell gets its own private namespace. When such a reference fails at
8
+ * runtime (`NameError: name '_cell_Hbol_a' is not defined`), the mangled name
9
+ * leaks into the error UI. These helpers undo that mangling for display.
10
+ *
11
+ * Mirrors `marimo/_ast/variables.py`.
12
+ */
13
+
14
+ // Matches `_cell_<cell_id><name>` for normal ids and UUIDs. The `[\w-]`
15
+ // id class admits hyphens; the `_\w*` name group admits the bare `_`
16
+ // local; the `(?<!_)` lookbehind skips `__marimo__cell_...` paths.
17
+ // Mirrors `_MANGLED_LOCAL_IN_TEXT_RE` in `variables.py`.
18
+ const MANGLED_LOCAL_BODY = String.raw`_cell_([^\W_][\w-]*?)(_\w*)`;
19
+ const MANGLED_LOCAL_PATTERN = String.raw`(?<!_)${MANGLED_LOCAL_BODY}`;
20
+ // Strict (whole-string) form for `unmangleLocal`; the leading `^` makes the
21
+ // lookbehind trivially satisfied, so use the bare body.
22
+ const ANCHORED_RE = new RegExp(`^${MANGLED_LOCAL_BODY}$`);
23
+ const UNANCHORED_RE = new RegExp(MANGLED_LOCAL_PATTERN);
24
+ const GLOBAL_RE = new RegExp(MANGLED_LOCAL_PATTERN, "g");
25
+
26
+ export interface UnmangledLocal {
27
+ cellId: CellId;
28
+ /** The original underscore-prefixed name, e.g. "_a". */
29
+ name: string;
30
+ }
31
+
32
+ export function unmangleLocal(mangled: string): UnmangledLocal | null {
33
+ const match = ANCHORED_RE.exec(mangled);
34
+ if (!match) {
35
+ return null;
36
+ }
37
+ return { cellId: match[1] as CellId, name: match[2] };
38
+ }
39
+
40
+ export type MangledSegment = string | UnmangledLocal;
41
+
42
+ /**
43
+ * Split a plain text string into alternating literal text and unmangled-local
44
+ * segments, so callers can render mixed React content.
45
+ */
46
+ export function splitMangledLocals(text: string): MangledSegment[] {
47
+ const segments: MangledSegment[] = [];
48
+ GLOBAL_RE.lastIndex = 0;
49
+ let lastIndex = 0;
50
+ let match: RegExpExecArray | null = GLOBAL_RE.exec(text);
51
+ while (match !== null) {
52
+ if (match.index > lastIndex) {
53
+ segments.push(text.slice(lastIndex, match.index));
54
+ }
55
+ segments.push({ cellId: match[1] as CellId, name: match[2] });
56
+ lastIndex = match.index + match[0].length;
57
+ match = GLOBAL_RE.exec(text);
58
+ }
59
+ if (lastIndex < text.length) {
60
+ segments.push(text.slice(lastIndex));
61
+ }
62
+ return segments;
63
+ }
64
+
65
+ export function containsMangledLocal(text: string): boolean {
66
+ return UNANCHORED_RE.test(text);
67
+ }