@marimo-team/islands 0.24.1-dev87 → 0.24.1-dev90

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.
@@ -1,4 +1,4 @@
1
- import { O as e, nt as t, tt as n } from "./html-to-image-LGo891lC.js";
1
+ import { O as e, nt as t, tt as n } from "./html-to-image-BMTAw0YV.js";
2
2
  import { u as r } from "./dist-wdOJ5T7-.js";
3
3
  import { b as i } from "./select-DQ9jU_B_.js";
4
4
  import { n as a } from "./semaphore-89jsoHtS.js";
@@ -2,9 +2,9 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { c as t, g as n, h as r, m as i, t as a } from "./button-DlqUC8W-.js";
3
3
  import { t as o } from "./react-DMWcHin-.js";
4
4
  import { t as s } from "./compiler-runtime-CAH9RLw2.js";
5
- import { Ot as c, ot as l } from "./html-to-image-LGo891lC.js";
5
+ import { Ot as c, ot as l } from "./html-to-image-BMTAw0YV.js";
6
6
  import { u } from "./dist-wdOJ5T7-.js";
7
- import { at as d, c as f, g as p, h as m, hn as h, l as g, mn as _, n as v, o as y, ot as b, r as x, st as S, t as C, u as w, vn as T } from "./common-Csjs6-Cq.js";
7
+ import { at as d, c as f, g as p, h as m, hn as h, l as g, mn as _, n as v, o as y, ot as b, r as x, st as S, t as C, u as w, vn as T } from "./common-FuL_5-mY.js";
8
8
  import { R as E } from "./select-DQ9jU_B_.js";
9
9
  import { Y as D } from "./dropdown-menu-Baf29jmS.js";
10
10
  import { t as O } from "./jsx-runtime-CpoKNRgu.js";
@@ -672,4 +672,4 @@ function Z(e, t) {
672
672
  return X(t || e, e);
673
673
  }
674
674
  //#endregion
675
- export { l as _, G as a, V as c, M as d, C as f, u as g, d as h, K as i, B as l, v as m, X as n, q as o, S as p, W as r, U as s, Z as t, N as u };
675
+ export { u as _, G as a, V as c, M as d, C as f, d as g, v as h, K as i, B as l, b as m, X as n, q as o, S as p, W as r, U as s, Z as t, N as u, l as v };
@@ -2,7 +2,7 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { g as t, h as n, m as r } from "./button-DlqUC8W-.js";
3
3
  import { t as i } from "./react-DMWcHin-.js";
4
4
  import { t as a } from "./compiler-runtime-CAH9RLw2.js";
5
- import { _ as o, c as s } from "./toDate-BbnMXiIl.js";
5
+ import { c as o, v as s } from "./toDate-DfFzRDkN.js";
6
6
  import { t as c } from "./jsx-runtime-CpoKNRgu.js";
7
7
  import { n as l } from "./error-banner-xVkWHg51.js";
8
8
  import { t as u } from "./tooltip-7p_32zdw.js";
@@ -11,7 +11,7 @@ import { j as f, n as p } from "./useTheme-C2ZreN00.js";
11
11
  import { c as m } from "./arrays-CN1djC4k.js";
12
12
  import { t as ee } from "./objects-CzUgoMIF.js";
13
13
  import { t as h } from "./useAsyncData-Cyh2jLTR.js";
14
- import { a as te, i as ne, n as re, t as g } from "./formats-CMBaNtI6.js";
14
+ import { a as te, i as ne, n as re, t as g } from "./formats-nPPeLOk2.js";
15
15
  import { A as ie, n as ae } from "./vega-loader.browser-DEggZzni.js";
16
16
  import { t as oe } from "./react-vega-DSD5aT7_.js";
17
17
  import { a as se, n as _ } from "./loader-D8jS1unC.js";
@@ -21,7 +21,7 @@ import { t as ue } from "./use-vega-container-remeasure-DUW4tRxq.js";
21
21
  //#region src/plugins/impl/vega/fix-relative-url.ts
22
22
  var v = /* @__PURE__ */ e(i(), 1), de = a();
23
23
  function fe(e) {
24
- return e.data && "url" in e.data && (e.data.url = s(e.data.url).href), e;
24
+ return e.data && "url" in e.data && (e.data.url = o(e.data.url).href), e;
25
25
  }
26
26
  //#endregion
27
27
  //#region src/plugins/impl/vega/types.ts
@@ -486,12 +486,12 @@ async function U(e) {
486
486
  }), !e.data || !("url" in e.data)) return e;
487
487
  let i;
488
488
  try {
489
- i = s(e.data.url);
489
+ i = o(e.data.url);
490
490
  } catch {
491
491
  return e;
492
492
  }
493
- let a = e.data.format, o = await n.run(() => _(i.href, a));
494
- return t[i.pathname] = o, {
493
+ let a = e.data.format, s = await n.run(() => _(i.href, a));
494
+ return t[i.pathname] = s, {
495
495
  ...e,
496
496
  data: { name: i.pathname }
497
497
  };
@@ -524,7 +524,7 @@ var G = (e) => {
524
524
  embedOptions: s
525
525
  }), t[5] = i, t[6] = s, t[7] = a, t[8] = d, t[9] = r, t[10] = n, t[11] = p) : p = t[11], p;
526
526
  }, K = (e) => {
527
- let i = (0, de.c)(50), { value: a, setValue: s, chartSelection: c, fieldSelection: l, spec: m, embedOptions: h } = e, { theme: te } = p(), g = (0, v.useRef)(null), ae = (0, v.useRef)(void 0), [_, le] = (0, v.useState)(), y;
527
+ let i = (0, de.c)(50), { value: a, setValue: o, chartSelection: c, fieldSelection: l, spec: m, embedOptions: h } = e, { theme: te } = p(), g = (0, v.useRef)(null), ae = (0, v.useRef)(void 0), [_, le] = (0, v.useState)(), y;
528
528
  bb0: {
529
529
  if (h && "actions" in h) {
530
530
  y = h.actions;
@@ -544,12 +544,12 @@ var G = (e) => {
544
544
  let w = C, E;
545
545
  i[5] === w ? E = i[6] : (E = T(w), i[5] = w, i[6] = E);
546
546
  let D = E, O;
547
- i[7] !== s || i[8] !== a ? (O = (e) => {
548
- s({
547
+ i[7] !== o || i[8] !== a ? (O = (e) => {
548
+ o({
549
549
  ...a,
550
550
  ...e
551
551
  });
552
- }, i[7] = s, i[8] = a, i[9] = O) : O = i[9];
552
+ }, i[7] = o, i[8] = a, i[9] = O) : O = i[9];
553
553
  let k = f(O), A;
554
554
  i[10] === k ? A = i[11] : (A = d((e, n) => {
555
555
  t.debug("[Vega signal]", e, n);
@@ -582,7 +582,7 @@ var G = (e) => {
582
582
  className: "text-xs flex flex-col",
583
583
  children: e.map(ge)
584
584
  }),
585
- children: /* @__PURE__ */ (0, W.jsx)(o, { className: "absolute bottom-1 right-0 m-2 h-4 w-4 cursor-help text-muted-foreground hover:text-foreground" })
585
+ children: /* @__PURE__ */ (0, W.jsx)(s, { className: "absolute bottom-1 right-0 m-2 h-4 w-4 cursor-help text-muted-foreground hover:text-foreground" })
586
586
  });
587
587
  }, i[20] = D, i[21] = z);
588
588
  let B = z, V = te === "dark" ? "dark" : void 0, H;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.24.1-dev87",
3
+ "version": "0.24.1-dev90",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -101,7 +101,10 @@ describe("TerminalBuffer", () => {
101
101
  buffer.writeChar("b");
102
102
  buffer.handleEscape("\u001B[1A"); // Move up 1
103
103
  buffer.writeChar("X");
104
- expect(buffer.render()).toMatchInlineSnapshot(`"aX"`);
104
+ expect(buffer.render()).toMatchInlineSnapshot(`
105
+ "aX
106
+ b"
107
+ `);
105
108
  });
106
109
 
107
110
  test("handleEscape cursor down", () => {
@@ -328,6 +331,24 @@ describe("AnsiReducer", () => {
328
331
  expect(result).toMatchInlineSnapshot(`"axy"`);
329
332
  });
330
333
 
334
+ test("cursor up preserves lines below the cursor", () => {
335
+ const reducer = new AnsiReducer();
336
+ reducer.append("outer: 0%");
337
+ reducer.append("\n");
338
+ reducer.append("inner: 0%");
339
+ reducer.append("\u001B[A");
340
+
341
+ expect(reducer.render()).toBe("outer: 0%\ninner: 0%");
342
+
343
+ // tqdm writes nested progress updates as separate, flushed chunks. The
344
+ // newline moves back to the preserved inner line before it is rewritten.
345
+ reducer.append("\n");
346
+ reducer.append("\rinner: 1%");
347
+ reducer.append("\u001B[A");
348
+
349
+ expect(reducer.render()).toBe("outer: 0%\ninner: 1%");
350
+ });
351
+
331
352
  test("reduce ignores control characters below space", () => {
332
353
  const reducer = new AnsiReducer();
333
354
  const result = reducer.reduce("hello\u0000\u0001\u0007world");
@@ -361,7 +382,11 @@ describe("ansiReduce", () => {
361
382
 
362
383
  test("multi-line with cursor movement", () => {
363
384
  const result = ansiReduce("Line 1\nLine 2\nLine 3\u001B[2AModified");
364
- expect(result).toMatchInlineSnapshot(`"Line 1Modified"`);
385
+ expect(result).toMatchInlineSnapshot(`
386
+ "Line 1Modified
387
+ Line 2
388
+ Line 3"
389
+ `);
365
390
  });
366
391
 
367
392
  test("erase and rewrite", () => {
@@ -434,7 +459,8 @@ describe("ansiReduce", () => {
434
459
  Running chain 0: 100%|█████████████████████████████| 101000/101000 [00:07<00:00, 12749.04it/s]
435
460
  Running chain 1: 100%|█████████████████████████████| 101000/101000 [00:08<00:00, 12442.67it/s]
436
461
  Running chain 2: 100%|█████████████████████████████| 101000/101000 [00:08<00:00, 12373.90it/s]
437
- "
462
+ Running chain 2: 95%|████████████████████████████▌ | 95950/101000 [00:07<00:00, 12922.54it/s]
463
+ Running chain 2: 100%|█████████████████████████████| 101000/101000 [00:08<00:00, 14869.01it/s]"
438
464
  `);
439
465
  });
440
466
  });
@@ -475,7 +501,8 @@ describe("AnsiReducer streaming with append()", () => {
475
501
  reducer.append("XYZ");
476
502
  expect(reducer.render()).toMatchInlineSnapshot(`
477
503
  "abc
478
- XYZ"
504
+ XYZ
505
+ "
479
506
  `);
480
507
  });
481
508
 
@@ -552,14 +579,12 @@ describe("AnsiReducer color preservation", () => {
552
579
  test("preserves color codes with cursor movements", () => {
553
580
  const reducer = new AnsiReducer();
554
581
  // Test that color codes work alongside cursor movements
555
- // Note: when cursor moves up, lines below are discarded (tqdm behavior)
556
582
  const result = reducer.reduce(
557
583
  "Line1\n\u001B[31mRed\u001B[0m\u001B[1A\u001B[32mGreen\u001B[0m",
558
584
  );
559
- // After moving up from row 1 to row 0, row 1 is discarded
560
- // Green is written at the end of row 0
561
- expect(result).toMatchInlineSnapshot(
562
- `"Line1 \u001B[32mGreen\u001B[0m"`,
585
+ // Green is written at the end of row 0; row 1 remains intact.
586
+ expect(result).toBe(
587
+ "Line1 \u001B[32mGreen\u001B[0m\n\u001B[31mRed\u001B[0m",
563
588
  );
564
589
  });
565
590
  });
@@ -623,7 +648,8 @@ describe("StatefulOutputMessage", () => {
623
648
 
624
649
  expect(stateful.data).toMatchInlineSnapshot(`
625
650
  "Line 1
626
- Xine 2"
651
+ Xine 2
652
+ "
627
653
  `);
628
654
  });
629
655
  });
@@ -25,16 +25,11 @@ export class TerminalBuffer {
25
25
 
26
26
  /** Move cursor by relative offsets. */
27
27
  private moveCursor(rowDelta: number, colDelta: number) {
28
- const oldRow = this.cursor.row;
29
28
  this.cursor.row = Math.max(0, this.cursor.row + rowDelta);
30
29
  this.cursor.col = Math.max(0, this.cursor.col + colDelta);
31
30
  this.ensureLine(this.cursor.row);
32
-
33
- // When moving up, discard lines below the new cursor position
34
- // This simulates a fixed terminal window where tqdm overwrites content
35
- if (rowDelta < 0 && this.cursor.row < oldRow) {
36
- this.lines.splice(this.cursor.row + 1);
37
- }
31
+ // Cursor movement does not erase content. This matters for multi-line
32
+ // progress renderers, which move up before rewriting a preserved row.
38
33
  }
39
34
 
40
35
  /** Move cursor to a specific absolute position. */
@@ -1,8 +1,70 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
- import { describe, expect, it } from "vitest";
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
4
  import type { OutputMessage } from "@/core/kernel/messages";
5
- import { groupCellsByColumn, shouldHideCode } from "../vertical-layout";
5
+ import {
6
+ getInitialShowCode,
7
+ groupCellsByColumn,
8
+ shouldHideCode,
9
+ updateShowCodeQueryParam,
10
+ } from "../utils";
11
+
12
+ const originalHref = window.location.href;
13
+ let originalStaticState: typeof window.__MARIMO_STATIC__;
14
+
15
+ describe("show code", () => {
16
+ beforeEach(() => {
17
+ originalStaticState = window.__MARIMO_STATIC__;
18
+ window.history.replaceState(null, "", "/?existing=value#section");
19
+ window.__MARIMO_STATIC__ = {
20
+ files: {},
21
+ modelNotifications: [],
22
+ };
23
+ });
24
+
25
+ afterEach(() => {
26
+ window.history.replaceState(null, "", originalHref);
27
+ window.__MARIMO_STATIC__ = originalStaticState;
28
+ });
29
+
30
+ it("does not add a query parameter when reading the initial value", () => {
31
+ expect(
32
+ getInitialShowCode({
33
+ showCodeInRunModePreference: true,
34
+ kioskMode: false,
35
+ }),
36
+ ).toBe(true);
37
+ expect(window.location.search).toBe("?existing=value");
38
+ });
39
+
40
+ it("reflects static notebook toggles in the URL", () => {
41
+ updateShowCodeQueryParam(false);
42
+ expect(window.location.search).toBe("?existing=value&show-code=false");
43
+ expect(window.location.hash).toBe("#section");
44
+ expect(
45
+ getInitialShowCode({
46
+ showCodeInRunModePreference: true,
47
+ kioskMode: false,
48
+ }),
49
+ ).toBe(false);
50
+
51
+ updateShowCodeQueryParam(true);
52
+ expect(window.location.search).toBe("?existing=value&show-code=true");
53
+ expect(
54
+ getInitialShowCode({
55
+ showCodeInRunModePreference: true,
56
+ kioskMode: false,
57
+ }),
58
+ ).toBe(true);
59
+ });
60
+
61
+ it("does not reflect non-static notebook toggles in the URL", () => {
62
+ delete window.__MARIMO_STATIC__;
63
+
64
+ updateShowCodeQueryParam(false);
65
+ expect(window.location.search).toBe("?existing=value");
66
+ });
67
+ });
6
68
 
7
69
  describe("groupCellsByColumn", () => {
8
70
  it("should group cells by column and maintain order", () => {
@@ -0,0 +1,73 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { isOutputEmpty } from "@/core/cells/outputs";
4
+ import type { CellData, CellRuntimeState } from "@/core/cells/types";
5
+ import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
6
+ import { KnownQueryParams } from "@/core/constants";
7
+ import type { OutputMessage } from "@/core/kernel/messages";
8
+ import { isStaticNotebook } from "@/core/static/static-state";
9
+ import { isWasm } from "@/core/wasm/utils";
10
+ import { updateQueryParams } from "@/utils/urls";
11
+
12
+ export function getInitialShowCode({
13
+ showCodeInRunModePreference,
14
+ kioskMode,
15
+ }: {
16
+ showCodeInRunModePreference: boolean;
17
+ kioskMode: boolean;
18
+ }): boolean {
19
+ // The mount option takes precedence over other defaults.
20
+ if (!showCodeInRunModePreference) {
21
+ return false;
22
+ }
23
+
24
+ const showCodeByQueryParam = new URLSearchParams(window.location.search).get(
25
+ KnownQueryParams.showCode,
26
+ );
27
+
28
+ // Static notebooks, WASM notebooks, and kiosk mode show code by default.
29
+ return showCodeByQueryParam === null
30
+ ? isStaticNotebook() || isWasm() || kioskMode
31
+ : showCodeByQueryParam === "true";
32
+ }
33
+
34
+ export function updateShowCodeQueryParam(showCode: boolean): void {
35
+ // Persist explicit user choices in shareable static notebook URLs. Avoid
36
+ // adding the parameter on startup so unchanged URLs retain their defaults.
37
+ if (isStaticNotebook()) {
38
+ updateQueryParams((params) => {
39
+ params.set(KnownQueryParams.showCode, String(showCode));
40
+ });
41
+ }
42
+ }
43
+
44
+ export function groupCellsByColumn(
45
+ cells: (CellRuntimeState & CellData)[],
46
+ ): [number, (CellRuntimeState & CellData)[]][] {
47
+ // Group cells by column
48
+ const cellsByColumn = new Map<number, (CellRuntimeState & CellData)[]>();
49
+ let lastSeenColumn = 0;
50
+ cells.forEach((cell) => {
51
+ const column = cell.config.column ?? lastSeenColumn;
52
+ lastSeenColumn = column;
53
+ if (!cellsByColumn.has(column)) {
54
+ cellsByColumn.set(column, []);
55
+ }
56
+ cellsByColumn.get(column)?.push(cell);
57
+ });
58
+
59
+ // Sort columns by index
60
+ return [...cellsByColumn.entries()].toSorted(([a], [b]) => a - b);
61
+ }
62
+
63
+ /**
64
+ * Determine if the code should be hidden.
65
+ *
66
+ * This is used to hide the code if it's pure markdown and there's an output,
67
+ * or if the code is empty.
68
+ */
69
+ export function shouldHideCode(code: string, output: OutputMessage | null) {
70
+ const isPureMarkdown = new MarkdownLanguageAdapter().isSupported(code);
71
+ const hasOutput = output !== null && !isOutputEmpty(output);
72
+ return (isPureMarkdown && hasOutput) || code.trim() === "";
73
+ }
@@ -27,13 +27,11 @@ import {
27
27
  } from "@/components/ui/dropdown-menu";
28
28
  import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
29
29
  import type { CellId } from "@/core/cells/ids";
30
- import { isOutputEmpty } from "@/core/cells/outputs";
31
30
  import type { CellData, CellRuntimeState } from "@/core/cells/types";
32
31
  import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
33
32
  import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
34
33
  import { useResolvedMarimoConfig } from "@/core/config/config";
35
- import { CSSClasses, KnownQueryParams } from "@/core/constants";
36
- import type { OutputMessage } from "@/core/kernel/messages";
34
+ import { CSSClasses } from "@/core/constants";
37
35
  import { kernelStateAtom } from "@/core/kernel/state";
38
36
  import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
39
37
  import { showCodeInRunModeAtom } from "@/core/meta/state";
@@ -46,7 +44,6 @@ import { useRequestClient } from "@/core/network/requests";
46
44
  import type { CellConfig } from "@/core/network/types";
47
45
  import { downloadAsHTML } from "@/core/static/download-html";
48
46
  import { isStaticNotebook } from "@/core/static/static-state";
49
- import { isWasm } from "@/core/wasm/utils";
50
47
  import { cn } from "@/utils/cn";
51
48
  import {
52
49
  ADD_PRINTING_CLASS,
@@ -58,6 +55,12 @@ import { FloatingOutline } from "../../chrome/panels/outline/floating-outline";
58
55
  import { cellDomProps } from "../../common";
59
56
  import type { ICellRendererPlugin, ICellRendererProps } from "../types";
60
57
  import { useDelayVisibility } from "./useDelayVisibility";
58
+ import {
59
+ getInitialShowCode,
60
+ groupCellsByColumn,
61
+ shouldHideCode,
62
+ updateShowCodeQueryParam,
63
+ } from "./utils";
61
64
  import { VerticalLayoutWrapper } from "./vertical-layout-wrapper";
62
65
 
63
66
  type VerticalLayout = null;
@@ -74,22 +77,20 @@ const VerticalLayoutRenderer: React.FC<VerticalLayoutProps> = ({
74
77
  const [userConfig] = useResolvedMarimoConfig();
75
78
  const showCodeInRunModePreference = useAtomValue(showCodeInRunModeAtom);
76
79
 
77
- const urlParams = new URLSearchParams(window.location.search);
78
- const [showCode, setShowCode] = useState(() => {
79
- // Check if the setting was set in the mount options
80
- if (!showCodeInRunModePreference) {
81
- return false;
82
- }
83
- // If 'auto' or not found, use URL param
84
- // If url param is not set, we default to true for static notebooks, wasm notebooks, and kiosk mode
85
- const showCodeByQueryParam = urlParams.get(KnownQueryParams.showCode);
86
- return showCodeByQueryParam === null
87
- ? isStaticNotebook() || isWasm() || kioskMode
88
- : showCodeByQueryParam === "true";
89
- });
80
+ const [showCode, setShowCode] = useState(() =>
81
+ getInitialShowCode({ showCodeInRunModePreference, kioskMode }),
82
+ );
90
83
 
91
84
  const canShowCode = useNotebookCodeAvailable(cells);
92
85
 
86
+ const handleToggleShowCode = () => {
87
+ setShowCode((currentShowCode) => {
88
+ const nextShowCode = !currentShowCode;
89
+ updateShowCodeQueryParam(nextShowCode);
90
+ return nextShowCode;
91
+ });
92
+ };
93
+
93
94
  const renderCell = (cell: CellRuntimeState & CellData) => {
94
95
  return (
95
96
  <VerticalCell
@@ -169,7 +170,7 @@ const VerticalLayoutRenderer: React.FC<VerticalLayoutProps> = ({
169
170
  <ActionButtons
170
171
  canShowCode={canShowCode}
171
172
  showCode={showCode}
172
- onToggleShowCode={() => setShowCode((v) => !v)}
173
+ onToggleShowCode={handleToggleShowCode}
173
174
  />
174
175
  )}
175
176
  <FloatingOutline />
@@ -469,34 +470,3 @@ export const VerticalLayoutPlugin: ICellRendererPlugin<
469
470
  serializeLayout: (layout) => layout,
470
471
  getInitialLayout: () => null,
471
472
  };
472
-
473
- export function groupCellsByColumn(
474
- cells: (CellRuntimeState & CellData)[],
475
- ): [number, (CellRuntimeState & CellData)[]][] {
476
- // Group cells by column
477
- const cellsByColumn = new Map<number, (CellRuntimeState & CellData)[]>();
478
- let lastSeenColumn = 0;
479
- cells.forEach((cell) => {
480
- const column = cell.config.column ?? lastSeenColumn;
481
- lastSeenColumn = column;
482
- if (!cellsByColumn.has(column)) {
483
- cellsByColumn.set(column, []);
484
- }
485
- cellsByColumn.get(column)?.push(cell);
486
- });
487
-
488
- // Sort columns by index
489
- return [...cellsByColumn.entries()].toSorted(([a], [b]) => a - b);
490
- }
491
-
492
- /**
493
- * Determine if the code should be hidden.
494
- *
495
- * This is used to hide the code if it's pure markdown and there's an output,
496
- * or if the code is empty.
497
- */
498
- export function shouldHideCode(code: string, output: OutputMessage | null) {
499
- const isPureMarkdown = new MarkdownLanguageAdapter().isSupported(code);
500
- const hasOutput = output !== null && !isOutputEmpty(output);
501
- return (isPureMarkdown && hasOutput) || code.trim() === "";
502
- }
@@ -0,0 +1,112 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { userConfigAtom } from "@/core/config/config";
7
+ import { defaultUserConfig } from "@/core/config/config-schema";
8
+ import { codeAtom, filenameAtom } from "@/core/saving/file-state";
9
+ import { getStaticNotebookRunTarget, StaticBanner } from "../static-banner";
10
+
11
+ function renderBanner(sharing?: {
12
+ html?: boolean;
13
+ wasm?: boolean;
14
+ molab?: boolean;
15
+ }) {
16
+ const store = createStore();
17
+ store.set(codeAtom, "import marimo\n");
18
+ store.set(filenameAtom, "/notebooks/my notebook.py");
19
+ store.set(userConfigAtom, {
20
+ ...defaultUserConfig(),
21
+ sharing,
22
+ });
23
+ window.__MARIMO_STATIC__ = { files: {} };
24
+
25
+ return render(
26
+ <Provider store={store}>
27
+ <StaticBanner />
28
+ </Provider>,
29
+ );
30
+ }
31
+
32
+ async function openDialog() {
33
+ fireEvent.click(screen.getByTestId("static-notebook-dialog-trigger"));
34
+ return screen.findByRole("dialog");
35
+ }
36
+
37
+ describe("StaticBanner", () => {
38
+ afterEach(() => {
39
+ delete window.__MARIMO_STATIC__;
40
+ });
41
+
42
+ it("shows the molab option by default", async () => {
43
+ renderBanner();
44
+ await openDialog();
45
+
46
+ expect(screen.getByRole("link", { name: "Open in molab" })).toHaveAttribute(
47
+ "href",
48
+ expect.stringContaining("molab.marimo.io/new"),
49
+ );
50
+ });
51
+
52
+ it("hides the molab option when sharing is disabled", async () => {
53
+ renderBanner({ html: false, wasm: false, molab: false });
54
+ await openDialog();
55
+
56
+ expect(
57
+ screen.queryByRole("link", { name: "Open in molab" }),
58
+ ).not.toBeInTheDocument();
59
+ expect(
60
+ screen.queryByText(/cloud-hosted notebook platform/),
61
+ ).not.toBeInTheDocument();
62
+ });
63
+
64
+ it("offers uv, pip, and conda commands", async () => {
65
+ renderBanner({ molab: false });
66
+ await openDialog();
67
+
68
+ expect(screen.getByRole("tab", { name: "uv" })).toBeVisible();
69
+ expect(
70
+ screen.getByText("uvx marimo edit --sandbox 'my notebook.py'"),
71
+ ).toBeVisible();
72
+
73
+ fireEvent.mouseDown(screen.getByRole("tab", { name: "pip" }), {
74
+ button: 0,
75
+ });
76
+ expect(screen.getByText(/pip install marimo/)).toBeVisible();
77
+
78
+ fireEvent.mouseDown(screen.getByRole("tab", { name: "conda" }), {
79
+ button: 0,
80
+ });
81
+ expect(
82
+ screen.getByText(/conda install -c conda-forge marimo/),
83
+ ).toBeVisible();
84
+ });
85
+ });
86
+
87
+ describe("getStaticNotebookRunTarget", () => {
88
+ it.each([
89
+ ["file:///tmp/notebook.html", "notebook.py"],
90
+ ["http://localhost:8000/report", "notebook.py"],
91
+ ["https://example.com/report", "notebook.py"],
92
+ ])("falls back to the downloaded filename for %s", (href, expected) => {
93
+ expect(getStaticNotebookRunTarget(href, "notebook.py")).toBe(expected);
94
+ });
95
+
96
+ it.each([
97
+ [
98
+ "https://example.com/notebook.html#section",
99
+ "https://example.com/notebook.html",
100
+ ],
101
+ [
102
+ "https://static.marimo.app/static/example",
103
+ "https://static.marimo.app/static/example",
104
+ ],
105
+ [
106
+ "https://molab.marimo.io/notebooks/nb_example/app",
107
+ "https://molab.marimo.io/notebooks/nb_example/app",
108
+ ],
109
+ ])("uses URLs supported by marimo edit: %s", (href, expected) => {
110
+ expect(getStaticNotebookRunTarget(href, "notebook.py")).toBe(expected);
111
+ });
112
+ });