@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61

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 (171) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.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-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
  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-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
  12. package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3630 -1801
  14. package/dist/{mermaid-CJW9vIyO.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-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
  18. package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
  25. package/dist/{vega-component-Pk6lyc_a.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/chat/acp/agent-panel.tsx +35 -1
  31. package/src/components/chat/chat-panel.tsx +68 -29
  32. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  33. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  34. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  35. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  36. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  37. package/src/components/data-table/charts/charts.tsx +23 -1
  38. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  39. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  40. package/src/components/debugger/debugger-code.tsx +7 -2
  41. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  42. package/src/components/editor/cell/CellStatus.tsx +1 -20
  43. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  44. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  45. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  46. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  47. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  48. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  49. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  50. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  51. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  52. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  53. package/src/components/editor/errors/auto-fix.tsx +88 -34
  54. package/src/components/editor/errors/fix-mode.ts +1 -1
  55. package/src/components/editor/notebook-cell.tsx +7 -0
  56. package/src/components/editor/output/ImageOutput.tsx +20 -6
  57. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  58. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  59. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  60. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  61. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  62. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  63. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  64. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  65. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  66. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  67. package/src/components/slides/minimap.tsx +7 -1
  68. package/src/components/slides/reveal-component.tsx +3 -0
  69. package/src/components/slides/slide-cell-view.tsx +10 -6
  70. package/src/components/slides/slide.tsx +16 -13
  71. package/src/components/tracing/tracing.tsx +2 -1
  72. package/src/core/ai/config.ts +2 -2
  73. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  74. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  75. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  76. package/src/core/ai/context/providers/datasource.ts +27 -2
  77. package/src/core/ai/context/providers/error.ts +226 -36
  78. package/src/core/ai/context/registry.ts +77 -43
  79. package/src/core/ai/state.ts +11 -0
  80. package/src/core/cells/__tests__/cell.test.ts +39 -0
  81. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  82. package/src/core/cells/cell.ts +5 -3
  83. package/src/core/cells/readonly-code-display.ts +35 -0
  84. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  85. package/src/core/codemirror/ai/resources.ts +15 -10
  86. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  87. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  88. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  89. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  90. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  91. package/src/core/codemirror/cells/extensions.ts +32 -4
  92. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  93. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  94. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  95. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  96. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  97. package/src/core/codemirror/utils.ts +15 -0
  98. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  99. package/src/core/config/config-schema.ts +1 -0
  100. package/src/core/config/config.ts +10 -0
  101. package/src/core/config/feature-flag.tsx +4 -0
  102. package/src/core/edit-app.tsx +8 -5
  103. package/src/core/errors/errors.ts +2 -1
  104. package/src/core/islands/bootstrap.ts +4 -3
  105. package/src/core/islands/bridge.ts +1 -0
  106. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  107. package/src/core/lifecycle/render-policy.ts +125 -0
  108. package/src/core/mime.ts +11 -4
  109. package/src/core/network/__tests__/api.test.ts +17 -0
  110. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  111. package/src/core/network/api.ts +17 -6
  112. package/src/core/network/requests-lazy.ts +1 -0
  113. package/src/core/network/requests-network.ts +8 -0
  114. package/src/core/network/requests-static.ts +1 -0
  115. package/src/core/network/requests-toasting.tsx +10 -1
  116. package/src/core/network/types.ts +2 -0
  117. package/src/core/run-app.tsx +27 -23
  118. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  119. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  120. package/src/core/runtime/adapter.ts +182 -0
  121. package/src/core/runtime/runtime.ts +30 -16
  122. package/src/core/wasm/PyodideLoader.tsx +20 -62
  123. package/src/core/wasm/bridge.ts +13 -4
  124. package/src/core/wasm/state.ts +8 -19
  125. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  126. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  127. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  128. package/src/core/websocket/transports/sse.ts +308 -0
  129. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  130. package/src/core/websocket/useWebSocket.tsx +12 -2
  131. package/src/css/app/codemirror.css +14 -0
  132. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  133. package/src/hooks/useDelayElapsed.ts +27 -0
  134. package/src/mount.tsx +3 -3
  135. package/src/plugins/core/RenderHTML.tsx +35 -1
  136. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  137. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  138. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  139. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  140. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  141. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  142. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  143. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  144. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  145. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  146. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  147. package/src/plugins/impl/anywidget/host.ts +54 -0
  148. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  149. package/src/plugins/impl/anywidget/model.ts +59 -239
  150. package/src/plugins/impl/anywidget/registry.ts +268 -0
  151. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  152. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  153. package/src/plugins/impl/anywidget/types.ts +14 -0
  154. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  155. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  156. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  157. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  158. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  159. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  160. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  161. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  162. package/src/utils/errors.ts +15 -0
  163. package/src/utils/time.ts +20 -0
  164. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  165. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  166. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  167. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  168. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  169. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  170. package/src/core/wasm/__tests__/state.test.ts +0 -124
  171. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -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>
@@ -0,0 +1,23 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { cellId } from "@/__tests__/branded";
5
+ import { buildFixInChatPrompt } from "../auto-fix";
6
+
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.",
11
+ );
12
+ });
13
+
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",
17
+ );
18
+ });
19
+
20
+ it("uses a generic prompt when no cell id and no error text", () => {
21
+ expect(buildFixInChatPrompt(undefined)).toBe("Please fix this error.");
22
+ });
23
+ });
@@ -1,7 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { useAtomValue, useSetAtom, useStore } from "jotai";
4
- import { ChevronDownIcon, SparklesIcon, WrenchIcon } from "lucide-react";
4
+ import {
5
+ CheckIcon,
6
+ ChevronDownIcon,
7
+ HatGlasses,
8
+ type LucideIcon,
9
+ SparklesIcon,
10
+ WrenchIcon,
11
+ } from "lucide-react";
5
12
  import { Button } from "@/components/ui/button";
6
13
  import {
7
14
  DropdownMenu,
@@ -17,8 +24,23 @@ import { aiFeaturesEnabledAtom } from "@/core/config/config";
17
24
  import { getAutoFixes } from "@/core/errors/errors";
18
25
  import type { MarimoError } from "@/core/kernel/messages";
19
26
  import { cn } from "@/utils/cn";
27
+ import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant";
20
28
  import { type FixMode, useFixMode } from "./fix-mode";
21
29
 
30
+ export function buildFixInChatPrompt(
31
+ cellId: CellId | undefined,
32
+ fallbackErrorText?: string,
33
+ ): string {
34
+ if (cellId != null) {
35
+ return `@error://${cellId}\n\nPlease fix this error.`;
36
+ }
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.";
42
+ }
43
+
22
44
  export const AutoFixButton = ({
23
45
  errors,
24
46
  cellId,
@@ -35,6 +57,7 @@ export const AutoFixButton = ({
35
57
  getAutoFixes(error, { aiEnabled: aiFeaturesEnabled }),
36
58
  );
37
59
  const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
60
+ const openAiAssistant = useOpenAiAssistant();
38
61
 
39
62
  if (autoFixes.length === 0) {
40
63
  return null;
@@ -67,6 +90,12 @@ export const AutoFixButton = ({
67
90
  editorView?.focus();
68
91
  };
69
92
 
93
+ const openAISidebar = () => {
94
+ openAiAssistant({
95
+ prompt: buildFixInChatPrompt(cellId),
96
+ });
97
+ };
98
+
70
99
  return (
71
100
  <div className={cn("my-2", className)}>
72
101
  {firstFix.fixType === "ai" ? (
@@ -74,6 +103,7 @@ export const AutoFixButton = ({
74
103
  tooltip={firstFix.description}
75
104
  openPrompt={() => handleFix(false)}
76
105
  applyAutofix={() => handleFix(true)}
106
+ openChat={openAISidebar}
77
107
  />
78
108
  ) : (
79
109
  <Tooltip content={firstFix.description} align="start">
@@ -92,23 +122,53 @@ export const AutoFixButton = ({
92
122
  );
93
123
  };
94
124
 
95
- const PromptIcon = SparklesIcon;
96
- const AutofixIcon = WrenchIcon;
125
+ interface FixModeConfig {
126
+ Icon: LucideIcon;
127
+ title: string;
128
+ description: string;
129
+ }
97
130
 
98
- const PromptTitle = "Suggest a prompt";
99
- const AutofixTitle = "Fix with AI";
131
+ const MODE_CONFIG: Record<FixMode, FixModeConfig> = {
132
+ autofix: {
133
+ Icon: WrenchIcon,
134
+ title: "Inline AI Fix",
135
+ description: "Apply AI fixes inline in the cell",
136
+ },
137
+ chat: {
138
+ Icon: HatGlasses,
139
+ title: "Fix with AI assistant",
140
+ description: "Open the AI sidebar to fix",
141
+ },
142
+ prompt: {
143
+ Icon: SparklesIcon,
144
+ title: "Suggest a prompt",
145
+ description: "Edit the prompt before applying",
146
+ },
147
+ };
148
+
149
+ const FIX_MODES: FixMode[] = ["autofix", "prompt", "chat"];
100
150
 
101
151
  export const AIFixButton = ({
102
152
  tooltip,
103
153
  openPrompt,
104
154
  applyAutofix,
155
+ openChat,
105
156
  }: {
106
157
  tooltip: string;
107
158
  openPrompt: () => void;
108
159
  applyAutofix: () => void;
160
+ openChat: () => void;
109
161
  }) => {
110
162
  const { fixMode, setFixMode } = useFixMode();
111
163
 
164
+ let onAction = openPrompt;
165
+ if (fixMode === "chat") {
166
+ onAction = openChat;
167
+ } else if (fixMode === "autofix") {
168
+ onAction = applyAutofix;
169
+ }
170
+ const { Icon, title } = MODE_CONFIG[fixMode];
171
+
112
172
  return (
113
173
  <div className="flex">
114
174
  <Tooltip content={tooltip} align="start">
@@ -116,14 +176,10 @@ export const AIFixButton = ({
116
176
  size="xs"
117
177
  variant="outline"
118
178
  className="font-normal rounded-r-none border-r-0"
119
- onClick={fixMode === "prompt" ? openPrompt : applyAutofix}
179
+ onClick={onAction}
120
180
  >
121
- {fixMode === "prompt" ? (
122
- <PromptIcon className="h-3 w-3 mr-2 mb-0.5" />
123
- ) : (
124
- <AutofixIcon className="h-3 w-3 mr-2 mb-0.5" />
125
- )}
126
- {fixMode === "prompt" ? PromptTitle : AutofixTitle}
181
+ <Icon className="h-3 w-3 mr-2 mb-0.5" />
182
+ {title}
127
183
  </Button>
128
184
  </Tooltip>
129
185
  <DropdownMenu>
@@ -138,40 +194,38 @@ export const AIFixButton = ({
138
194
  </Button>
139
195
  </DropdownMenuTrigger>
140
196
  <DropdownMenuContent align="end" className="w-56">
141
- <DropdownMenuItem
142
- className="flex items-center gap-2"
143
- onClick={() => {
144
- setFixMode(fixMode === "prompt" ? "autofix" : "prompt");
145
- }}
146
- >
147
- <AiModeItem mode={fixMode === "prompt" ? "autofix" : "prompt"} />
148
- </DropdownMenuItem>
197
+ {FIX_MODES.map((mode) => (
198
+ <DropdownMenuItem
199
+ key={mode}
200
+ className="flex items-center gap-2"
201
+ onClick={() => setFixMode(mode)}
202
+ >
203
+ <AiModeItem mode={mode} selected={mode === fixMode} />
204
+ </DropdownMenuItem>
205
+ ))}
149
206
  </DropdownMenuContent>
150
207
  </DropdownMenu>
151
208
  </div>
152
209
  );
153
210
  };
154
211
 
155
- const AiModeItem = ({ mode }: { mode: FixMode }) => {
156
- const icon =
157
- mode === "prompt" ? (
158
- <PromptIcon className="h-4 w-4" />
159
- ) : (
160
- <AutofixIcon className="h-4 w-4" />
161
- );
162
- const title = mode === "prompt" ? PromptTitle : AutofixTitle;
163
- const description =
164
- mode === "prompt"
165
- ? "Edit the prompt before applying"
166
- : "Apply AI fixes automatically";
212
+ const AiModeItem = ({
213
+ mode,
214
+ selected,
215
+ }: {
216
+ mode: FixMode;
217
+ selected: boolean;
218
+ }) => {
219
+ const { Icon, title, description } = MODE_CONFIG[mode];
167
220
 
168
221
  return (
169
- <div className="flex items-center gap-2">
170
- {icon}
222
+ <div className="flex items-center gap-2 w-full">
223
+ <Icon className="h-4 w-4 shrink-0" />
171
224
  <div className="flex flex-col">
172
225
  <span className="font-medium">{title}</span>
173
226
  <span className="text-xs text-muted-foreground">{description}</span>
174
227
  </div>
228
+ {selected && <CheckIcon className="h-4 w-4 ml-auto shrink-0" />}
175
229
  </div>
176
230
  );
177
231
  };
@@ -4,7 +4,7 @@ import { useAtom } from "jotai";
4
4
  import { atomWithStorage } from "jotai/utils";
5
5
  import { jotaiJsonStorage } from "@/utils/storage/jotai";
6
6
 
7
- export type FixMode = "prompt" | "autofix";
7
+ export type FixMode = "prompt" | "autofix" | "chat";
8
8
 
9
9
  const BASE_KEY = "marimo:ai-autofix-mode";
10
10
 
@@ -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
  };
@@ -24,8 +24,10 @@ import { Tooltip } from "@/components/ui/tooltip";
24
24
  import { getCellEditorView } from "@/core/cells/cells";
25
25
  import type { CellId } from "@/core/cells/ids";
26
26
  import { SCRATCH_CELL_ID } from "@/core/cells/ids";
27
+ import { toggleBreakpoint } from "@/core/codemirror/cells/debugger-state";
27
28
  import { insertDebuggerAtLine } from "@/core/codemirror/editing/debugging";
28
29
  import { aiFeaturesEnabledAtom } from "@/core/config/config";
30
+ import { getFeatureFlag } from "@/core/config/feature-flag";
29
31
  import { getRequestClient } from "@/core/network/requests";
30
32
  import { isStaticNotebook } from "@/core/static/static-state";
31
33
  import { isWasm } from "@/core/wasm/utils";
@@ -41,7 +43,8 @@ import {
41
43
  extractAllTracebackInfo,
42
44
  getTracebackInfo,
43
45
  } from "@/utils/traceback";
44
- import { AIFixButton } from "../errors/auto-fix";
46
+ import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant";
47
+ import { AIFixButton, buildFixInChatPrompt } from "../errors/auto-fix";
45
48
  import { MangledSegments } from "../errors/mangled-local-chip";
46
49
  import { CellLinkTraceback } from "../links/cell-link";
47
50
  import type { OnRefactorWithAI } from "../Output";
@@ -71,6 +74,7 @@ export const MarimoTracebackOutput = ({
71
74
 
72
75
  const lastTracebackLine = lastLine(traceback);
73
76
  const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
77
+ const openAiAssistant = useOpenAiAssistant();
74
78
 
75
79
  // Get last traceback info
76
80
  const tracebackInfo = extractAllTracebackInfo(traceback)?.at(0);
@@ -97,6 +101,12 @@ export const MarimoTracebackOutput = ({
97
101
  });
98
102
  };
99
103
 
104
+ const openAISidebar = () => {
105
+ openAiAssistant({
106
+ prompt: buildFixInChatPrompt(cellId, lastTracebackLine),
107
+ });
108
+ };
109
+
100
110
  return (
101
111
  <div className="flex flex-col gap-2 min-w-full w-fit">
102
112
  <button
@@ -126,6 +136,7 @@ export const MarimoTracebackOutput = ({
126
136
  tooltip="Fix with AI"
127
137
  openPrompt={() => handleRefactorWithAI(false)}
128
138
  applyAutofix={() => handleRefactorWithAI(true)}
139
+ openChat={openAISidebar}
129
140
  />
130
141
  )}
131
142
  {showDebugger && (
@@ -203,7 +214,14 @@ function lastLine(text: string): string {
203
214
  export const replaceTracebackFilenames = (domNode: DOMNode) => {
204
215
  const info = getTracebackInfo(domNode);
205
216
  if (info?.kind === "cell") {
206
- const tooltipContent = <InsertBreakpointContent />;
217
+ // With the live debugger enabled, toggle a real gutter breakpoint
218
+ // instead of mutating the user's code with a `breakpoint()` statement.
219
+ const debuggerEnabled = getFeatureFlag("debugger");
220
+ const tooltipContent = debuggerEnabled ? (
221
+ <ToggleBreakpointContent />
222
+ ) : (
223
+ <InsertBreakpointContent />
224
+ );
207
225
  return (
208
226
  <span className="nb">
209
227
  <span className="inline-flex items-center">
@@ -219,6 +237,10 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
219
237
  >
220
238
  <BugPlayIcon
221
239
  onClick={() => {
240
+ if (debuggerEnabled && !isStaticNotebook()) {
241
+ toggleBreakpoint(info.cellId, info.lineNumber);
242
+ return;
243
+ }
222
244
  const view = getCellEditorView(info.cellId);
223
245
  if (view) {
224
246
  insertDebuggerAtLine(view, info.lineNumber);
@@ -285,3 +307,7 @@ const InsertBreakpointContent = () => {
285
307
  </>
286
308
  );
287
309
  };
310
+
311
+ const ToggleBreakpointContent = () => {
312
+ return <>Toggle a breakpoint at this line</>;
313
+ };