@marimo-team/islands 0.23.14-dev9 → 0.23.15-dev0

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 (160) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DXBx_nQg.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  3. package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
  4. package/dist/{chat-ui-DYBcNEdd.js → chat-ui-DinOvbWu.js} +3256 -3052
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-BZArWgy6.js → code-visibility-DDwWvOQa.js} +905 -889
  7. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  8. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  9. package/dist/{formats-WsOgyW_K.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-qpmKyAPn.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-MqcD07Bw.js → html-to-image-_wGfk8V-.js} +2389 -2323
  12. package/dist/{input-BSdZp5Ng.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3667 -1838
  14. package/dist/{mermaid-D-HYBMEV.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  16. package/dist/{process-output-Dt3icftd.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-CKomb1B5.js → reveal-component-CHu9H-QX.js} +600 -596
  18. package/dist/{spec-CnTgI25l.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D1Z7ZXWh.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-BMc8itk2.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-ZwmDBRDY.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-CxffarYV.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-yGsGEk82.js → useTheme-6eZ3GOTS.js} +5 -2
  25. package/dist/{vega-component-BFJTyykA.js → vega-component-DzyyM9fc.js} +5 -5
  26. package/package.json +4 -4
  27. package/src/__mocks__/requests.ts +1 -0
  28. package/src/__tests__/CellStatus.test.tsx +1 -2
  29. package/src/components/app-config/user-config-form.tsx +52 -0
  30. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  31. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  32. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  33. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  34. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  35. package/src/components/data-table/charts/charts.tsx +23 -1
  36. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  37. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  38. package/src/components/debugger/debugger-code.tsx +7 -2
  39. package/src/components/editor/cell/CellStatus.tsx +1 -20
  40. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  41. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  42. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  43. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  44. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  45. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  46. package/src/components/editor/errors/__tests__/auto-fix.test.ts +11 -107
  47. package/src/components/editor/errors/auto-fix.tsx +11 -31
  48. package/src/components/editor/notebook-cell.tsx +7 -0
  49. package/src/components/editor/output/ImageOutput.tsx +20 -6
  50. package/src/components/editor/output/MarimoTracebackOutput.tsx +20 -3
  51. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  52. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  53. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  54. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  55. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  56. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  57. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  58. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  59. package/src/components/slides/minimap.tsx +7 -1
  60. package/src/components/slides/reveal-component.tsx +3 -0
  61. package/src/components/slides/slide-cell-view.tsx +10 -6
  62. package/src/components/slides/slide.tsx +16 -13
  63. package/src/components/tracing/tracing.tsx +2 -1
  64. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  65. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  66. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  67. package/src/core/ai/context/providers/datasource.ts +27 -2
  68. package/src/core/ai/context/providers/error.ts +226 -36
  69. package/src/core/ai/context/registry.ts +77 -43
  70. package/src/core/cells/__tests__/cell.test.ts +39 -0
  71. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  72. package/src/core/cells/cell.ts +5 -3
  73. package/src/core/cells/readonly-code-display.ts +35 -0
  74. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  75. package/src/core/codemirror/ai/resources.ts +15 -10
  76. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  77. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  78. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  79. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  80. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  81. package/src/core/codemirror/cells/extensions.ts +32 -4
  82. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  83. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  84. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  85. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  86. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  87. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  88. package/src/core/config/config-schema.ts +1 -0
  89. package/src/core/config/config.ts +10 -0
  90. package/src/core/config/feature-flag.tsx +4 -0
  91. package/src/core/edit-app.tsx +8 -5
  92. package/src/core/errors/errors.ts +2 -1
  93. package/src/core/islands/bootstrap.ts +4 -3
  94. package/src/core/islands/bridge.ts +1 -0
  95. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  96. package/src/core/lifecycle/render-policy.ts +125 -0
  97. package/src/core/mime.ts +11 -4
  98. package/src/core/network/__tests__/api.test.ts +17 -0
  99. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  100. package/src/core/network/api.ts +17 -6
  101. package/src/core/network/requests-lazy.ts +1 -0
  102. package/src/core/network/requests-network.ts +8 -0
  103. package/src/core/network/requests-static.ts +1 -0
  104. package/src/core/network/requests-toasting.tsx +10 -1
  105. package/src/core/network/types.ts +2 -0
  106. package/src/core/run-app.tsx +27 -23
  107. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  108. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  109. package/src/core/runtime/adapter.ts +182 -0
  110. package/src/core/runtime/runtime.ts +30 -16
  111. package/src/core/wasm/PyodideLoader.tsx +20 -62
  112. package/src/core/wasm/bridge.ts +13 -4
  113. package/src/core/wasm/state.ts +8 -19
  114. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  115. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  116. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  117. package/src/core/websocket/transports/sse.ts +308 -0
  118. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  119. package/src/core/websocket/useWebSocket.tsx +12 -2
  120. package/src/css/app/codemirror.css +14 -0
  121. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  122. package/src/hooks/useDelayElapsed.ts +27 -0
  123. package/src/mount.tsx +3 -3
  124. package/src/plugins/core/RenderHTML.tsx +35 -1
  125. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  126. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  127. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  128. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  129. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  130. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  131. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  132. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  133. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  134. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  135. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  136. package/src/plugins/impl/anywidget/host.ts +54 -0
  137. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  138. package/src/plugins/impl/anywidget/model.ts +59 -239
  139. package/src/plugins/impl/anywidget/registry.ts +268 -0
  140. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  141. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  142. package/src/plugins/impl/anywidget/types.ts +14 -0
  143. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  144. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  145. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  146. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  147. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  148. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  149. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  150. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  151. package/src/utils/errors.ts +15 -0
  152. package/src/utils/time.ts +20 -0
  153. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  154. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  155. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  156. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  157. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  158. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  159. package/src/core/wasm/__tests__/state.test.ts +0 -124
  160. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -14,7 +14,7 @@ import { Tooltip } from "../../ui/tooltip";
14
14
 
15
15
  import "./cell-status.css";
16
16
  import { formatDistanceToNow } from "date-fns";
17
- import { Time } from "@/utils/time";
17
+ import { formatElapsedTime, Time } from "@/utils/time";
18
18
 
19
19
  export interface CellStatusComponentProps extends Pick<
20
20
  CellRuntimeState,
@@ -348,25 +348,6 @@ const LastRanTime = (props: { lastRanTime: number }) => {
348
348
  );
349
349
  };
350
350
 
351
- export function formatElapsedTime(elapsedTime: number | null) {
352
- if (elapsedTime === null) {
353
- return "";
354
- }
355
-
356
- const milliseconds = elapsedTime;
357
- const seconds = milliseconds / 1000;
358
-
359
- if (seconds >= 60) {
360
- const minutes = Math.floor(seconds / 60);
361
- const remainingSeconds = Math.floor(seconds % 60);
362
- return `${minutes}m${remainingSeconds}s`;
363
- }
364
- if (seconds >= 1) {
365
- return `${seconds.toFixed(2).toString()}s`;
366
- }
367
- return `${milliseconds.toFixed(0).toString()}ms`;
368
- }
369
-
370
351
  const CellTimer = (props: { startTime: Time }) => {
371
352
  const time = useElapsedTime(props.startTime.toMilliseconds());
372
353
  return <span>{formatElapsedTime(time)}</span>;
@@ -3,12 +3,12 @@
3
3
  import { AlertTriangleIcon } from "lucide-react";
4
4
  import React from "react";
5
5
  import { FocusScope } from "react-aria";
6
- import { formatElapsedTime } from "@/components/editor/cell/CellStatus";
7
6
  import { CellLink } from "@/components/editor/links/cell-link";
8
7
  import { Button } from "@/components/ui/button";
9
8
  import type { CellId } from "@/core/cells/ids";
10
9
  import { usePendingDelete } from "@/core/cells/pending-delete-service";
11
10
  import { cn } from "@/utils/cn";
11
+ import { formatElapsedTime } from "@/utils/time";
12
12
 
13
13
  export const PendingDeleteConfirmation: React.FC<{ cellId: CellId }> = ({
14
14
  cellId,
@@ -23,7 +23,7 @@ import { toast } from "@/components/ui/use-toast";
23
23
  import { useCellData, useCellRuntime } from "@/core/cells/cells";
24
24
  import { CellOutputId } from "@/core/cells/ids";
25
25
  import { isOutputEmpty } from "@/core/cells/outputs";
26
- import { goToDefinitionAtCursorPosition } from "@/core/codemirror/go-to-definition/utils";
26
+ import { goToDefinitionWithLspFallback } from "@/core/codemirror/go-to-definition/utils";
27
27
  import { sendToPanelManager } from "@/core/vscode/vscode-bindings";
28
28
  import { copyImageToClipboard, copyToClipboard } from "@/utils/copy";
29
29
  import { getImageExtension } from "@/utils/filenames";
@@ -170,7 +170,7 @@ export const CellActionsContextMenu = ({
170
170
  // Only suppress focus restoration when we actually navigated;
171
171
  // otherwise let Radix return focus to the trigger cell.
172
172
  suppressCloseAutoFocus.current =
173
- goToDefinitionAtCursorPosition(editorView);
173
+ goToDefinitionWithLspFallback(editorView);
174
174
  }
175
175
  },
176
176
  },
@@ -31,7 +31,7 @@ import { cn } from "@/utils/cn";
31
31
  import { HideInKioskMode } from "../../kiosk-mode";
32
32
  import { ContributeSnippetButton } from "../components/contribute-snippet-button";
33
33
  import { usePanelOrientation, usePanelSection } from "./panel-context";
34
- import { getSnippetDisplay } from "./snippet-display";
34
+ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
35
35
 
36
36
  const extensions = [EditorView.lineWrapping];
37
37
 
@@ -188,7 +188,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
188
188
  return null;
189
189
  }
190
190
 
191
- const { language, value } = getSnippetDisplay(code);
191
+ const { language, code: displayCode } = getReadonlyCodeDisplay(code);
192
192
 
193
193
  return (
194
194
  <div
@@ -216,7 +216,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
216
216
  language={language}
217
217
  className="cm border rounded overflow-hidden"
218
218
  extensions={extensions}
219
- value={value}
219
+ value={displayCode}
220
220
  readOnly={true}
221
221
  />
222
222
  </Suspense>
@@ -1,47 +1,17 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useAtomValue } from "jotai";
4
- import { AlertCircleIcon } from "lucide-react";
5
3
  import type React from "react";
6
- import { Spinner } from "@/components/icons/spinner";
7
- import { Tooltip } from "@/components/ui/tooltip";
8
- import { wasmInitializationAtom, wasmInitStatusAtom } from "@/core/wasm/state";
4
+ import { RuntimeStatusBadge } from "@/components/lifecycle/RuntimeStatusBadge";
9
5
  import { isWasm } from "@/core/wasm/utils";
10
6
 
11
7
  /**
12
- * Footer indicator that surfaces Pyodide initialization progress. Mirrors
13
- * the "Kernel" indicator but tracks the WASM runtime instead of the server
14
- * connection. Hides itself once Pyodide is ready.
8
+ * Footer indicator that surfaces Pyodide initialization progress. Thin
9
+ * wrapper around `<RuntimeStatusBadge>` so the rest of the footer chrome
10
+ * doesn't need to know about adapters.
15
11
  */
16
12
  export const PyodideStatus: React.FC = () => {
17
- const status = useAtomValue(wasmInitStatusAtom);
18
- const message = useAtomValue(wasmInitializationAtom);
19
-
20
- if (!isWasm() || status === "ready") {
13
+ if (!isWasm()) {
21
14
  return null;
22
15
  }
23
-
24
- const icon =
25
- status === "error" ? (
26
- <AlertCircleIcon className="w-4 h-4 text-destructive" />
27
- ) : (
28
- <Spinner size="small" />
29
- );
30
-
31
- const tooltip = status === "error" ? "Pyodide failed to initialize" : message;
32
-
33
- return (
34
- <Tooltip
35
- content={<div className="text-sm whitespace-pre-line">{tooltip}</div>}
36
- data-testid="footer-pyodide-status"
37
- >
38
- <div
39
- className="p-1 hover:bg-accent rounded flex items-center gap-1.5 text-xs text-muted-foreground"
40
- data-testid="pyodide-status"
41
- >
42
- {icon}
43
- <span>Pyodide</span>
44
- </div>
45
- </Tooltip>
46
- );
16
+ return <RuntimeStatusBadge />;
47
17
  };
@@ -0,0 +1,79 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { beforeAll, describe, expect, it } from "vitest";
5
+ import { SetupMocks } from "@/__mocks__/common";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import { ReadonlyCode } from "../readonly-python-code";
8
+
9
+ beforeAll(() => {
10
+ SetupMocks.resizeObserver();
11
+ });
12
+
13
+ /**
14
+ * The only button is the show/hide toggle: copy is disabled and the
15
+ * insert-cell button is off by default.
16
+ */
17
+ function renderReadonly(props: { initiallyHideCode?: boolean }) {
18
+ return render(
19
+ <TooltipProvider>
20
+ <ReadonlyCode code="x = 1" showCopyCode={false} {...props} />
21
+ </TooltipProvider>,
22
+ );
23
+ }
24
+
25
+ function isCollapsed(root: ParentNode) {
26
+ return root.querySelector(".cm")?.classList.contains("opacity-20") ?? false;
27
+ }
28
+
29
+ describe("ReadonlyCode", () => {
30
+ it("starts collapsed when initiallyHideCode is true", () => {
31
+ const { container } = renderReadonly({ initiallyHideCode: true });
32
+ expect(isCollapsed(container)).toBe(true);
33
+ });
34
+
35
+ it("starts expanded when initiallyHideCode is false", () => {
36
+ const { container } = renderReadonly({ initiallyHideCode: false });
37
+ expect(isCollapsed(container)).toBe(false);
38
+ });
39
+
40
+ it("starts expanded when initiallyHideCode is unset", () => {
41
+ const { container } = renderReadonly({});
42
+ expect(isCollapsed(container)).toBe(false);
43
+ });
44
+
45
+ it("toggles visibility locally on click", () => {
46
+ const { container } = renderReadonly({ initiallyHideCode: true });
47
+ const toggle = screen.getByRole("button");
48
+
49
+ fireEvent.click(toggle);
50
+ expect(isCollapsed(container)).toBe(false);
51
+
52
+ fireEvent.click(toggle);
53
+ expect(isCollapsed(container)).toBe(true);
54
+ });
55
+
56
+ it("keeps each instance's visibility independent", () => {
57
+ const { container } = render(
58
+ <TooltipProvider>
59
+ <ReadonlyCode
60
+ code="a = 1"
61
+ showCopyCode={false}
62
+ initiallyHideCode={true}
63
+ />
64
+ <ReadonlyCode
65
+ code="b = 2"
66
+ showCopyCode={false}
67
+ initiallyHideCode={true}
68
+ />
69
+ </TooltipProvider>,
70
+ );
71
+ const [firstToggle] = screen.getAllByRole("button");
72
+ const [first, second] = container.querySelectorAll(".cm");
73
+
74
+ fireEvent.click(firstToggle);
75
+
76
+ expect(first.classList.contains("opacity-20")).toBe(false);
77
+ expect(second.classList.contains("opacity-20")).toBe(true);
78
+ });
79
+ });
@@ -1,17 +1,25 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { markdown } from "@codemirror/lang-markdown";
3
4
  import { sql } from "@codemirror/lang-sql";
5
+ import {
6
+ defaultHighlightStyle,
7
+ syntaxHighlighting,
8
+ } from "@codemirror/language";
4
9
  import CodeMirror, {
5
10
  EditorView,
6
11
  type ReactCodeMirrorProps,
7
12
  } from "@uiw/react-codemirror";
8
13
  import { CopyIcon, EyeIcon, EyeOffIcon, PlusIcon } from "lucide-react";
9
- import { memo, useState } from "react";
14
+ import { memo, useMemo, useState } from "react";
10
15
  import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
11
16
  import { Button } from "@/components/ui/button";
12
17
  import { Tooltip } from "@/components/ui/tooltip";
13
18
  import { toast } from "@/components/ui/use-toast";
19
+ import type { LanguageAdapterType } from "@/core/codemirror/language/types";
14
20
  import { customPythonLanguageSupport } from "@/core/codemirror/language/languages/python";
21
+ import { darkTheme } from "@/core/codemirror/theme/dark";
22
+ import { lightTheme } from "@/core/codemirror/theme/light";
15
23
  import { useTheme } from "@/theme/useTheme";
16
24
  import { cn } from "@/utils/cn";
17
25
  import { copyToClipboard } from "@/utils/copy";
@@ -22,6 +30,18 @@ const pythonExtensions = [
22
30
  EditorView.lineWrapping,
23
31
  ];
24
32
  const sqlExtensions = [sql(), EditorView.lineWrapping];
33
+ const markdownExtensions = [markdown(), EditorView.lineWrapping];
34
+
35
+ function readonlyCodeExtensions(language: LanguageAdapterType) {
36
+ switch (language) {
37
+ case "sql":
38
+ return sqlExtensions;
39
+ case "markdown":
40
+ return markdownExtensions;
41
+ default:
42
+ return pythonExtensions;
43
+ }
44
+ }
25
45
 
26
46
  /**
27
47
  * A readonly code component that can be used to display code in a readonly state.
@@ -42,7 +62,7 @@ export const ReadonlyCode = memo(
42
62
  showHideCode?: boolean;
43
63
  showCopyCode?: boolean;
44
64
  insertNewCell?: boolean;
45
- language?: "python" | "sql";
65
+ language?: LanguageAdapterType;
46
66
  } & ReactCodeMirrorProps,
47
67
  ) => {
48
68
  const { theme } = useTheme();
@@ -56,35 +76,41 @@ export const ReadonlyCode = memo(
56
76
  language = "python",
57
77
  ...rest
58
78
  } = props;
59
- const [hideCode, setHideCode] = useState(initiallyHideCode);
79
+ const [hideCode, setHideCode] = useState(!!initiallyHideCode);
80
+
81
+ const extensions = useMemo(
82
+ () => [
83
+ theme === "dark" ? darkTheme : lightTheme,
84
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
85
+ ...readonlyCodeExtensions(language),
86
+ ],
87
+ [theme, language],
88
+ );
60
89
 
61
90
  return (
62
91
  <div
63
92
  className={cn(
64
- "relative hover-actions-parent w-full overflow-hidden",
93
+ "relative hover-actions-parent w-full overflow-hidden pb-1",
65
94
  className,
66
95
  )}
67
96
  >
68
- {showHideCode && hideCode && (
69
- <HideCodeButton
70
- tooltip="Show code"
71
- onClick={() => setHideCode(false)}
72
- />
73
- )}
74
97
  <div className="absolute top-0 right-0 my-1 mx-2 z-10 hover-action flex gap-2">
75
98
  {showCopyCode && <CopyButton text={code} />}
76
99
  {insertNewCell && <InsertNewCell code={code} />}
77
- {showHideCode && !hideCode && (
78
- <EyeCloseButton onClick={() => setHideCode(true)} />
100
+ {showHideCode && (
101
+ <ToggleCodeButton
102
+ hidden={hideCode}
103
+ onClick={() => setHideCode((prev) => !prev)}
104
+ />
79
105
  )}
80
106
  </div>
81
107
  <CodeMirror
82
108
  {...rest}
83
109
  className={cn("cm", hideCode && "opacity-20 h-8 overflow-hidden")}
84
- theme={theme === "dark" ? "dark" : "light"}
110
+ theme="none"
85
111
  height="100%"
86
- editable={!hideCode}
87
- extensions={language === "python" ? pythonExtensions : sqlExtensions}
112
+ editable={false}
113
+ extensions={extensions}
88
114
  value={code}
89
115
  readOnly={true}
90
116
  />
@@ -102,42 +128,42 @@ const CopyButton = (props: { text: string }) => {
102
128
 
103
129
  return (
104
130
  <Tooltip content="Copy code" usePortal={false}>
105
- <Button onClick={copy} size="xs" className="py-0" variant="secondary">
131
+ <Button
132
+ onClick={copy}
133
+ size="xs"
134
+ className="py-0"
135
+ variant="secondary"
136
+ aria-label="Copy code"
137
+ >
106
138
  <CopyIcon size={14} strokeWidth={1.5} />
107
139
  </Button>
108
140
  </Tooltip>
109
141
  );
110
142
  };
111
143
 
112
- const EyeCloseButton = (props: { onClick: () => void }) => {
144
+ const ToggleCodeButton = (props: { hidden: boolean; onClick: () => void }) => {
113
145
  return (
114
- <Tooltip content="Hide code" usePortal={false}>
146
+ <Tooltip
147
+ content={props.hidden ? "Show code" : "Hide code"}
148
+ usePortal={false}
149
+ >
115
150
  <Button
116
151
  onClick={props.onClick}
152
+ aria-label={props.hidden ? "Show code" : "Hide code"}
117
153
  size="xs"
118
154
  className="py-0"
119
155
  variant="secondary"
120
156
  >
121
- <EyeOffIcon size={14} strokeWidth={1.5} />
157
+ {props.hidden ? (
158
+ <EyeIcon size={14} strokeWidth={1.5} />
159
+ ) : (
160
+ <EyeOffIcon size={14} strokeWidth={1.5} />
161
+ )}
122
162
  </Button>
123
163
  </Tooltip>
124
164
  );
125
165
  };
126
166
 
127
- export const HideCodeButton = (props: {
128
- tooltip?: string;
129
- className?: string;
130
- onClick: () => void;
131
- }) => {
132
- return (
133
- <div className={props.className} onClick={props.onClick}>
134
- <Tooltip usePortal={false} content={props.tooltip}>
135
- <EyeIcon className="hover-action w-5 h-5 text-muted-foreground cursor-pointer absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-80 hover:opacity-100 z-20" />
136
- </Tooltip>
137
- </div>
138
- );
139
- };
140
-
141
167
  const InsertNewCell = (props: { code: string }) => {
142
168
  const addCodeToNewCell = useAddCodeToNewCell();
143
169
 
@@ -152,6 +178,7 @@ const InsertNewCell = (props: { code: string }) => {
152
178
  size="xs"
153
179
  className="py-0"
154
180
  variant="secondary"
181
+ aria-label="Add code to notebook"
155
182
  >
156
183
  <PlusIcon size={14} strokeWidth={1.5} />
157
184
  </Button>
@@ -1,119 +1,23 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { describe, expect, it } from "vitest";
4
4
  import { cellId } from "@/__tests__/branded";
5
- import type { MarimoError } from "@/core/kernel/messages";
5
+ import { buildFixInChatPrompt } from "../auto-fix";
6
6
 
7
- const getDatasourceContext = vi.fn<(id: unknown) => string | null>(() => null);
8
-
9
- vi.mock("@/core/ai/context/providers/datasource", () => ({
10
- getDatasourceContext: (id: unknown) => getDatasourceContext(id),
11
- }));
12
-
13
- const { buildFixPrompt, buildFixPromptFromText } = await import("../auto-fix");
14
-
15
- describe("buildFixPromptFromText", () => {
16
- beforeEach(() => {
17
- getDatasourceContext.mockReset();
18
- getDatasourceContext.mockReturnValue(null);
19
- });
20
-
21
- it("includes the cell id in the header when provided", () => {
22
- const prompt = buildFixPromptFromText("boom", cellId("cell-1"));
23
- expect(prompt).toBe(
24
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nboom",
25
- );
26
- });
27
-
28
- it("uses a generic header when no cell id is provided", () => {
29
- const prompt = buildFixPromptFromText("boom");
30
- expect(prompt).toBe(
31
- "My code gives the following error. Please fix it:\n\nboom",
7
+ describe("buildFixInChatPrompt", () => {
8
+ it("references the per-cell error context URI", () => {
9
+ expect(buildFixInChatPrompt(cellId("cell-1"))).toBe(
10
+ "@error://cell-1\n\nPlease fix this error.",
32
11
  );
33
12
  });
34
13
 
35
- it("appends datasource context when explicitly requested for the cell", () => {
36
- getDatasourceContext.mockReturnValue("@datasource://my_db");
37
- const prompt = buildFixPromptFromText("boom", cellId("cell-1"), {
38
- includeDatasourceContext: true,
39
- });
40
- expect(prompt).toBe(
41
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nboom\n\nDatabase schema: @datasource://my_db",
14
+ it("falls back to inline error text when no cell id", () => {
15
+ expect(buildFixInChatPrompt(undefined, "ValueError: boom")).toBe(
16
+ "My code gives the following error. Please fix it:\n\nValueError: boom",
42
17
  );
43
18
  });
44
19
 
45
- it("does not append datasource context by default", () => {
46
- getDatasourceContext.mockReturnValue("@datasource://my_db");
47
- const prompt = buildFixPromptFromText("boom", cellId("cell-1"));
48
- expect(prompt).toBe(
49
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nboom",
50
- );
51
- expect(getDatasourceContext).not.toHaveBeenCalled();
52
- });
53
-
54
- it("does not look up datasource context without a cell id", () => {
55
- buildFixPromptFromText("boom", undefined, {
56
- includeDatasourceContext: true,
57
- });
58
- expect(getDatasourceContext).not.toHaveBeenCalled();
59
- });
60
- });
61
-
62
- describe("buildFixPrompt", () => {
63
- beforeEach(() => {
64
- getDatasourceContext.mockReset();
65
- getDatasourceContext.mockReturnValue(null);
66
- });
67
-
68
- it("uses the error message when present", () => {
69
- const errors: MarimoError[] = [
70
- { type: "sql-error", msg: "syntax error", sql_statement: "SELECT" },
71
- ];
72
- expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
73
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nsyntax error",
74
- );
75
- });
76
-
77
- it("joins multiple errors with newlines", () => {
78
- const errors: MarimoError[] = [
79
- { type: "syntax", msg: "bad syntax" },
80
- {
81
- type: "exception",
82
- exception_type: "ValueError",
83
- msg: "bad value",
84
- raising_cell: null,
85
- },
86
- ];
87
- expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
88
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nbad syntax\nbad value",
89
- );
90
- });
91
-
92
- it("falls back to the error type when there is no message", () => {
93
- const errors: MarimoError[] = [
94
- { type: "multiple-defs", name: "foo", cells: [cellId("foo")] },
95
- ];
96
- expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
97
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nmultiple-defs",
98
- );
99
- });
100
-
101
- it("appends datasource context for SQL errors", () => {
102
- getDatasourceContext.mockReturnValue("@datasource://my_db");
103
- const errors: MarimoError[] = [
104
- { type: "sql-error", msg: "syntax error", sql_statement: "SELECT" },
105
- ];
106
- expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
107
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nsyntax error\n\nDatabase schema: @datasource://my_db",
108
- );
109
- });
110
-
111
- it("does not append datasource context for non-SQL errors", () => {
112
- getDatasourceContext.mockReturnValue("@datasource://my_db");
113
- const errors: MarimoError[] = [{ type: "syntax", msg: "bad syntax" }];
114
- expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
115
- "My cell (id: cell-1) produced the following error. Please fix it:\n\nbad syntax",
116
- );
117
- expect(getDatasourceContext).not.toHaveBeenCalled();
20
+ it("uses a generic prompt when no cell id and no error text", () => {
21
+ expect(buildFixInChatPrompt(undefined)).toBe("Please fix this error.");
118
22
  });
119
23
  });
@@ -21,44 +21,24 @@ import { aiCompletionCellAtom } from "@/core/ai/state";
21
21
  import { notebookAtom, useCellActions } from "@/core/cells/cells";
22
22
  import type { CellId } from "@/core/cells/ids";
23
23
  import { aiFeaturesEnabledAtom } from "@/core/config/config";
24
- import { getDatasourceContext } from "@/core/ai/context/providers/datasource";
25
24
  import { getAutoFixes } from "@/core/errors/errors";
26
25
  import type { MarimoError } from "@/core/kernel/messages";
27
26
  import { cn } from "@/utils/cn";
28
27
  import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant";
29
28
  import { type FixMode, useFixMode } from "./fix-mode";
30
29
 
31
- export function buildFixPromptFromText(
32
- errorText: string,
33
- cellId?: CellId,
34
- {
35
- includeDatasourceContext = false,
36
- }: { includeDatasourceContext?: boolean } = {},
30
+ export function buildFixInChatPrompt(
31
+ cellId: CellId | undefined,
32
+ fallbackErrorText?: string,
37
33
  ): string {
38
- const header =
39
- cellId != null
40
- ? `My cell (id: ${cellId}) produced the following error. Please fix it:`
41
- : "My code gives the following error. Please fix it:";
42
- let prompt = `${header}\n\n${errorText}`;
43
- if (cellId != null && includeDatasourceContext) {
44
- const datasourceContext = getDatasourceContext(cellId);
45
- if (datasourceContext) {
46
- prompt += `\n\nDatabase schema: ${datasourceContext}`;
47
- }
34
+ if (cellId != null) {
35
+ return `@error://${cellId}\n\nPlease fix this error.`;
48
36
  }
49
- return prompt;
50
- }
51
-
52
- export function buildFixPrompt(errors: MarimoError[], cellId: CellId): string {
53
- const errorText = errors
54
- .map((error) => ("msg" in error && error.msg ? error.msg : error.type))
55
- .join("\n");
56
- const includeDatasourceContext = errors.some(
57
- (error) => error.type === "sql-error",
58
- );
59
- return buildFixPromptFromText(errorText, cellId, {
60
- includeDatasourceContext,
61
- });
37
+ const errorText = fallbackErrorText?.trim();
38
+ if (errorText) {
39
+ return `My code gives the following error. Please fix it:\n\n${errorText}`;
40
+ }
41
+ return "Please fix this error.";
62
42
  }
63
43
 
64
44
  export const AutoFixButton = ({
@@ -112,7 +92,7 @@ export const AutoFixButton = ({
112
92
 
113
93
  const openAISidebar = () => {
114
94
  openAiAssistant({
115
- prompt: buildFixPrompt(errors, cellId),
95
+ prompt: buildFixInChatPrompt(cellId),
116
96
  });
117
97
  };
118
98
 
@@ -29,6 +29,7 @@ import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
29
29
  import { isOutputEmpty } from "@/core/cells/outputs";
30
30
  import { useIsPendingCut } from "@/core/cells/pending-cut-service";
31
31
  import { autocompletionKeymap } from "@/core/codemirror/cm";
32
+ import { clearCellBreakpoints } from "@/core/codemirror/cells/debugger-state";
32
33
  import type { LanguageAdapterType } from "@/core/codemirror/language/types";
33
34
  import { CSSClasses } from "@/core/constants";
34
35
  import { canCollapseOutline } from "@/core/dom/outline";
@@ -746,6 +747,9 @@ const EditableCellComponent = ({
746
747
  onRefactorWithAI={handleRefactorWithAI}
747
748
  onClear={() => {
748
749
  actions.clearCellConsoleOutput({ cellId });
750
+ // The debugger "Clear" (trashcan) also drops this cell's
751
+ // breakpoints; no-op when none are set.
752
+ clearCellBreakpoints(cellId);
749
753
  }}
750
754
  onSubmitDebugger={(text, index) => {
751
755
  actions.setStdinResponse({
@@ -1211,6 +1215,9 @@ const SetupCellComponent = ({
1211
1215
  onRefactorWithAI={handleRefactorWithAI}
1212
1216
  onClear={() => {
1213
1217
  actions.clearCellConsoleOutput({ cellId });
1218
+ // The debugger "Clear" (trashcan) also drops this cell's
1219
+ // breakpoints; no-op when none are set.
1220
+ clearCellBreakpoints(cellId);
1214
1221
  }}
1215
1222
  onSubmitDebugger={(text, index) => {
1216
1223
  actions.setStdinResponse({
@@ -17,18 +17,32 @@ export const ImageOutput = ({
17
17
  height,
18
18
  className,
19
19
  }: Props): JSX.Element => {
20
- // Convert numeric values to pixel strings, pass string values (like "100%") as-is
20
+ // Numeric dimensions are set as HTML attributes rather than inline styles.
21
+ // Attributes give the browser the image's intrinsic size and aspect ratio,
22
+ // but can still be overridden by stylesheet rules (`max-width: 100%;
23
+ // height: auto` from preflight), so images shrink proportionally in
24
+ // width-constrained containers like mo.hstack. Inline styles would win
25
+ // over `height: auto` and distort the aspect ratio.
26
+ //
27
+ // String values like "100%" are only valid in CSS, so they go in the
28
+ // style attribute.
21
29
  const style: React.CSSProperties = {};
22
- if (width !== undefined) {
23
- style.width = typeof width === "number" ? `${width}px` : width;
30
+ if (typeof width === "string") {
31
+ style.width = width;
24
32
  }
25
- if (height !== undefined) {
26
- style.height = typeof height === "number" ? `${height}px` : height;
33
+ if (typeof height === "string") {
34
+ style.height = height;
27
35
  }
28
36
 
29
37
  return (
30
38
  <span className={className}>
31
- <img src={src} alt={alt} style={style} />
39
+ <img
40
+ src={src}
41
+ alt={alt}
42
+ width={typeof width === "number" ? width : undefined}
43
+ height={typeof height === "number" ? height : undefined}
44
+ style={style}
45
+ />
32
46
  </span>
33
47
  );
34
48
  };