@marimo-team/islands 0.23.14-dev2 → 0.23.14-dev23

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 (79) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
  2. package/dist/{chat-ui-CsPewo4h.js → chat-ui-CO8WuXGb.js} +3243 -3039
  3. package/dist/{code-visibility-CRYdBxcA.js → code-visibility-B3ntFQLc.js} +1115 -917
  4. package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
  5. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-qpmKyAPn.js} +2 -2
  6. package/dist/{html-to-image-DXwLcQ6l.js → html-to-image-DLSOS-bE.js} +2351 -2282
  7. package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
  8. package/dist/main.js +1139 -1071
  9. package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
  10. package/dist/{process-output-C6_e1pT_.js → process-output-B9ysqk1y.js} +1 -1
  11. package/dist/{reveal-component-CbqUMhp8.js → reveal-component-C8j14RKU.js} +16 -16
  12. package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
  13. package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
  14. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
  15. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
  16. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
  17. package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
  18. package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
  19. package/package.json +2 -2
  20. package/src/components/chat/acp/agent-panel.tsx +35 -1
  21. package/src/components/chat/chat-panel.tsx +68 -29
  22. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  23. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  24. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  25. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  26. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  27. package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -2
  28. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  29. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  30. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  31. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  32. package/src/components/editor/code/readonly-python-code.tsx +16 -2
  33. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  34. package/src/components/editor/errors/auto-fix.tsx +88 -34
  35. package/src/components/editor/errors/fix-mode.ts +1 -1
  36. package/src/components/editor/output/MarimoTracebackOutput.tsx +10 -1
  37. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  38. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +6 -2
  39. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  40. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  41. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  42. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  43. package/src/components/slides/slide-cell-view.tsx +7 -6
  44. package/src/core/ai/config.ts +2 -2
  45. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  46. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  47. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  48. package/src/core/ai/context/providers/datasource.ts +27 -2
  49. package/src/core/ai/context/providers/error.ts +226 -36
  50. package/src/core/ai/context/registry.ts +77 -43
  51. package/src/core/ai/state.ts +11 -0
  52. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  53. package/src/core/cells/readonly-code-display.ts +35 -0
  54. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  55. package/src/core/codemirror/ai/resources.ts +15 -10
  56. package/src/core/codemirror/cells/extensions.ts +1 -4
  57. package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +94 -0
  58. package/src/core/codemirror/completion/completer.ts +12 -1
  59. package/src/core/codemirror/completion/signature-hint.ts +68 -0
  60. package/src/core/codemirror/language/languages/python.ts +10 -4
  61. package/src/core/codemirror/utils.ts +15 -0
  62. package/src/core/edit-app.tsx +8 -5
  63. package/src/core/errors/errors.ts +2 -1
  64. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  65. package/src/core/lifecycle/render-policy.ts +125 -0
  66. package/src/core/mime.ts +11 -4
  67. package/src/core/run-app.tsx +27 -23
  68. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  69. package/src/core/runtime/adapter.ts +182 -0
  70. package/src/core/wasm/PyodideLoader.tsx +20 -62
  71. package/src/core/wasm/bridge.ts +9 -4
  72. package/src/core/wasm/state.ts +8 -19
  73. package/src/core/wasm/store.ts +1 -1
  74. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  75. package/src/hooks/useDelayElapsed.ts +27 -0
  76. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  77. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  78. package/src/core/wasm/__tests__/state.test.ts +0 -124
  79. /package/dist/{files → export_demos}/wasm-intro.py +0 -0
@@ -50,7 +50,10 @@ import {
50
50
  import { smartMatch } from "@/utils/smartMatch";
51
51
  import type { Column, Table } from "@tanstack/react-table";
52
52
  import { cn } from "@/utils/cn";
53
- import { getColumnCountForDisplay } from "../hooks/use-column-visibility";
53
+ import {
54
+ getColumnCountForDisplay,
55
+ getUserColumnVisibilityCounts,
56
+ } from "../hooks/use-column-visibility";
54
57
 
55
58
  interface ColumnExplorerPanelProps<TData> {
56
59
  previewColumn: PreviewColumn;
@@ -90,6 +93,7 @@ export function ColumnExplorerPanel<TData>({
90
93
  totalColumns: effectiveTotalColumns,
91
94
  hiddenColumns: hiddenColumnCount,
92
95
  } = getColumnCountForDisplay(table, totalColumns);
96
+ const { visible: visibleColumnCount } = getUserColumnVisibilityCounts(table);
93
97
 
94
98
  const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
95
99
  numRows: totalRows,
@@ -108,17 +112,34 @@ export function ColumnExplorerPanel<TData>({
108
112
  value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
109
113
  className="h-3 w-3"
110
114
  />
111
- {hiddenColumnCount > 0 && (
112
- <Button
113
- type="button"
114
- variant="link"
115
- size="xs"
116
- className="h-auto p-0"
117
- onClick={() => table.resetColumnVisibility(true)}
118
- >
119
- Unhide all
120
- </Button>
121
- )}
115
+ <div className="ml-auto flex items-center gap-1">
116
+ <Tooltip content="Show all columns" delayDuration={400}>
117
+ <Button
118
+ type="button"
119
+ variant="ghost"
120
+ size="xs"
121
+ className="gap-1"
122
+ disabled={hiddenColumnCount === 0}
123
+ onClick={() => table.toggleAllColumnsVisible(true)}
124
+ >
125
+ <EyeIcon className="w-3 h-3" />
126
+ Show all
127
+ </Button>
128
+ </Tooltip>
129
+ <Tooltip content="Hide all columns" delayDuration={400}>
130
+ <Button
131
+ type="button"
132
+ variant="ghost"
133
+ size="xs"
134
+ className="gap-1"
135
+ disabled={visibleColumnCount === 0}
136
+ onClick={() => table.toggleAllColumnsVisible(false)}
137
+ >
138
+ <EyeOffIcon className="w-3 h-3" />
139
+ Hide all
140
+ </Button>
141
+ </Tooltip>
142
+ </div>
122
143
  </div>
123
144
  <Command className="h-5/6 bg-background" shouldFilter={false}>
124
145
  <CommandInput
@@ -59,6 +59,12 @@ export const ColumnVisibilityDropdown = <TData,>({
59
59
  const hiddenIds = userColumns
60
60
  .filter((column) => !column.getIsVisible())
61
61
  .map((column) => column.id);
62
+ const hideableIds = userColumns
63
+ .filter((column) => column.getCanHide())
64
+ .map((column) => column.id);
65
+ const visibleHideableCount = hideableIds.filter(
66
+ (id) => !hiddenIds.includes(id),
67
+ ).length;
62
68
 
63
69
  const applyHidden = (next: string[] | string | null) => {
64
70
  const hidden = new Set(Array.isArray(next) ? next : []);
@@ -127,6 +133,15 @@ export const ColumnVisibilityDropdown = <TData,>({
127
133
  <EyeIcon className="w-3 h-3 mr-1.5" />
128
134
  Show all
129
135
  </CommandItem>
136
+ <CommandItem
137
+ value="__hide_all__"
138
+ disabled={visibleHideableCount === 0}
139
+ onSelect={() => applyHidden(hideableIds)}
140
+ className="cursor-pointer"
141
+ >
142
+ <EyeOffIcon className="w-3 h-3 mr-1.5" />
143
+ Hide all
144
+ </CommandItem>
130
145
  <CommandSeparator />
131
146
  </>
132
147
  ) : (
@@ -688,13 +688,13 @@ export function useNotebookActions() {
688
688
  {
689
689
  divider: true,
690
690
  icon: <Home size={14} strokeWidth={1.5} />,
691
- label: "Return home",
691
+ label: "Open home",
692
692
  // If file is in the url, then we ran `marimo edit`
693
693
  // without a specific file
694
694
  hidden: !location.search.includes("file"),
695
695
  handle: () => {
696
696
  const withoutSearch = document.baseURI.split("?")[0];
697
- window.open(withoutSearch, "_self");
697
+ window.open(withoutSearch, "_blank", "noopener");
698
698
  },
699
699
  },
700
700
 
@@ -31,6 +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 { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
34
35
 
35
36
  const extensions = [EditorView.lineWrapping];
36
37
 
@@ -187,6 +188,8 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
187
188
  return null;
188
189
  }
189
190
 
191
+ const { language, code: displayCode } = getReadonlyCodeDisplay(code);
192
+
190
193
  return (
191
194
  <div
192
195
  className="relative hover-actions-parent pr-2"
@@ -210,10 +213,10 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
210
213
  <LazyAnyLanguageCodeMirror
211
214
  key={`${snippet.title}-${id}`}
212
215
  theme={theme === "dark" ? "dark" : "light"}
213
- language="python"
216
+ language={language}
214
217
  className="cm border rounded overflow-hidden"
215
218
  extensions={extensions}
216
- value={code}
219
+ value={displayCode}
217
220
  readOnly={true}
218
221
  />
219
222
  </Suspense>
@@ -0,0 +1,36 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const getFeatureFlag = vi.fn<(flag: string) => boolean>();
6
+
7
+ vi.mock("@/core/config/feature-flag", () => ({
8
+ getFeatureFlag: (flag: string) => getFeatureFlag(flag),
9
+ }));
10
+
11
+ const { resolveAiPanelTab } = await import("../useOpenAiAssistant");
12
+
13
+ describe("resolveAiPanelTab", () => {
14
+ beforeEach(() => {
15
+ getFeatureFlag.mockReset();
16
+ });
17
+
18
+ it("honors an explicit panel regardless of the feature flag", () => {
19
+ getFeatureFlag.mockReturnValue(false);
20
+ expect(resolveAiPanelTab("agents", "chat")).toBe("agents");
21
+ expect(resolveAiPanelTab("chat", "agents")).toBe("chat");
22
+ });
23
+
24
+ it("uses the stored tab when external agents are enabled", () => {
25
+ getFeatureFlag.mockReturnValue(true);
26
+ expect(resolveAiPanelTab(undefined, "agents")).toBe("agents");
27
+ expect(resolveAiPanelTab(undefined, "chat")).toBe("chat");
28
+ });
29
+
30
+ it("falls back to chat when external agents are disabled", () => {
31
+ getFeatureFlag.mockReturnValue(false);
32
+ // A stale "agents" preference must not strand the prompt.
33
+ expect(resolveAiPanelTab(undefined, "agents")).toBe("chat");
34
+ expect(resolveAiPanelTab(undefined, "chat")).toBe("chat");
35
+ });
36
+ });
@@ -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
  };
@@ -6,7 +6,9 @@ import { jotaiJsonStorage } from "@/utils/storage/jotai";
6
6
 
7
7
  const AI_PANEL_TAB_KEY = "marimo:chrome:ai-panel-tab";
8
8
 
9
- const aiPanelTabAtom = atomWithStorage<"chat" | "agents">(
9
+ export type AiPanelTab = "chat" | "agents";
10
+
11
+ export const aiPanelTabAtom = atomWithStorage<AiPanelTab>(
10
12
  AI_PANEL_TAB_KEY,
11
13
  "chat",
12
14
  jotaiJsonStorage,
@@ -0,0 +1,88 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useSetAtom, useStore } from "jotai";
4
+ import useEvent from "react-use-event-hook";
5
+ import { agentSessionStateAtom } from "@/components/chat/acp/state";
6
+ import { toast } from "@/components/ui/use-toast";
7
+ import { useModelChange } from "@/core/ai/config";
8
+ import { pendingAiPromptAtom } from "@/core/ai/state";
9
+ import type { CopilotMode } from "@/core/ai/tools/registry";
10
+ import { aiModelConfiguredAtom } from "@/core/config/config";
11
+ import { getFeatureFlag } from "@/core/config/feature-flag";
12
+ import { Logger } from "@/utils/Logger";
13
+ import { useChromeActions } from "../state";
14
+ import { type AiPanelTab, aiPanelTabAtom, useAiPanelTab } from "./useAiPanel";
15
+
16
+ // Error fixes work best with kernel access, so we default the chat panel to
17
+ // code mode. The agents panel manages its own mode, so this is chat-only.
18
+ const FIX_CHAT_MODE: CopilotMode = "code_mode";
19
+
20
+ export interface OpenAiAssistantOptions {
21
+ prompt: string;
22
+ submit?: boolean;
23
+ /** Override the user's AI sidebar tab. When omitted, the stored tab is used. */
24
+ panel?: AiPanelTab;
25
+ }
26
+
27
+ // Resolve which AI panel to open.
28
+ export function resolveAiPanelTab(
29
+ panel: AiPanelTab | undefined,
30
+ storedTab: AiPanelTab,
31
+ ): AiPanelTab {
32
+ if (panel) {
33
+ return panel;
34
+ }
35
+ return getFeatureFlag("external_agents") ? storedTab : "chat";
36
+ }
37
+
38
+ /**
39
+ * Opens the AI assistant sidebar panel and delivers a prompt to it.
40
+ *
41
+ * If no `panel` is given, the user's configured tab is respected.
42
+ */
43
+ export function useOpenAiAssistant() {
44
+ const { openApplication } = useChromeActions();
45
+ const { setAiPanelTab } = useAiPanelTab();
46
+ const { saveModeChange } = useModelChange();
47
+ const setPendingPrompt = useSetAtom(pendingAiPromptAtom);
48
+ const store = useStore();
49
+
50
+ return useEvent(async (opts: OpenAiAssistantOptions) => {
51
+ const tab = resolveAiPanelTab(opts.panel, store.get(aiPanelTabAtom));
52
+
53
+ const chatPanelReady = store.get(aiModelConfiguredAtom);
54
+ const agentsPanelReady =
55
+ getFeatureFlag("external_agents") &&
56
+ store.get(agentSessionStateAtom).sessions.length > 0;
57
+ const isReady = tab === "agents" ? agentsPanelReady : chatPanelReady;
58
+
59
+ if (!isReady) {
60
+ toast({
61
+ title: tab === "agents" ? "No agent session" : "AI not configured",
62
+ description:
63
+ tab === "agents"
64
+ ? "Start an agent session or switch to the chat panel."
65
+ : "Configure an AI provider and chat model to fix with AI.",
66
+ variant: "danger",
67
+ });
68
+ return;
69
+ }
70
+
71
+ if (opts.panel) {
72
+ setAiPanelTab(opts.panel);
73
+ }
74
+
75
+ if (tab === "chat") {
76
+ await saveModeChange(FIX_CHAT_MODE).catch(() =>
77
+ Logger.error("Failed to save mode change"),
78
+ );
79
+ }
80
+
81
+ setPendingPrompt({
82
+ prompt: opts.prompt,
83
+ submit: opts.submit ?? false,
84
+ });
85
+
86
+ openApplication("ai");
87
+ });
88
+ }
@@ -1,5 +1,6 @@
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";
4
5
  import CodeMirror, {
5
6
  EditorView,
@@ -11,6 +12,7 @@ import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
11
12
  import { Button } from "@/components/ui/button";
12
13
  import { Tooltip } from "@/components/ui/tooltip";
13
14
  import { toast } from "@/components/ui/use-toast";
15
+ import type { LanguageAdapterType } from "@/core/codemirror/language/types";
14
16
  import { customPythonLanguageSupport } from "@/core/codemirror/language/languages/python";
15
17
  import { useTheme } from "@/theme/useTheme";
16
18
  import { cn } from "@/utils/cn";
@@ -22,6 +24,18 @@ const pythonExtensions = [
22
24
  EditorView.lineWrapping,
23
25
  ];
24
26
  const sqlExtensions = [sql(), EditorView.lineWrapping];
27
+ const markdownExtensions = [markdown(), EditorView.lineWrapping];
28
+
29
+ function readonlyCodeExtensions(language: LanguageAdapterType) {
30
+ switch (language) {
31
+ case "sql":
32
+ return sqlExtensions;
33
+ case "markdown":
34
+ return markdownExtensions;
35
+ default:
36
+ return pythonExtensions;
37
+ }
38
+ }
25
39
 
26
40
  /**
27
41
  * A readonly code component that can be used to display code in a readonly state.
@@ -42,7 +56,7 @@ export const ReadonlyCode = memo(
42
56
  showHideCode?: boolean;
43
57
  showCopyCode?: boolean;
44
58
  insertNewCell?: boolean;
45
- language?: "python" | "sql";
59
+ language?: LanguageAdapterType;
46
60
  } & ReactCodeMirrorProps,
47
61
  ) => {
48
62
  const { theme } = useTheme();
@@ -84,7 +98,7 @@ export const ReadonlyCode = memo(
84
98
  theme={theme === "dark" ? "dark" : "light"}
85
99
  height="100%"
86
100
  editable={!hideCode}
87
- extensions={language === "python" ? pythonExtensions : sqlExtensions}
101
+ extensions={readonlyCodeExtensions(language)}
88
102
  value={code}
89
103
  readOnly={true}
90
104
  />
@@ -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
 
@@ -41,7 +41,8 @@ import {
41
41
  extractAllTracebackInfo,
42
42
  getTracebackInfo,
43
43
  } from "@/utils/traceback";
44
- import { AIFixButton } from "../errors/auto-fix";
44
+ import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant";
45
+ import { AIFixButton, buildFixInChatPrompt } from "../errors/auto-fix";
45
46
  import { MangledSegments } from "../errors/mangled-local-chip";
46
47
  import { CellLinkTraceback } from "../links/cell-link";
47
48
  import type { OnRefactorWithAI } from "../Output";
@@ -71,6 +72,7 @@ export const MarimoTracebackOutput = ({
71
72
 
72
73
  const lastTracebackLine = lastLine(traceback);
73
74
  const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
75
+ const openAiAssistant = useOpenAiAssistant();
74
76
 
75
77
  // Get last traceback info
76
78
  const tracebackInfo = extractAllTracebackInfo(traceback)?.at(0);
@@ -97,6 +99,12 @@ export const MarimoTracebackOutput = ({
97
99
  });
98
100
  };
99
101
 
102
+ const openAISidebar = () => {
103
+ openAiAssistant({
104
+ prompt: buildFixInChatPrompt(cellId, lastTracebackLine),
105
+ });
106
+ };
107
+
100
108
  return (
101
109
  <div className="flex flex-col gap-2 min-w-full w-fit">
102
110
  <button
@@ -126,6 +134,7 @@ export const MarimoTracebackOutput = ({
126
134
  tooltip="Fix with AI"
127
135
  openPrompt={() => handleRefactorWithAI(false)}
128
136
  applyAutofix={() => handleRefactorWithAI(true)}
137
+ openChat={openAISidebar}
129
138
  />
130
139
  )}
131
140
  {showDebugger && (