@marimo-team/islands 0.23.14-dev3 → 0.23.14-dev30
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.
- package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
- package/dist/{chat-ui-CsPewo4h.js → chat-ui-CO8WuXGb.js} +3243 -3039
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-BbnxGblD.js → code-visibility-CAkgOEA5.js} +1122 -924
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-7wxMGXjG.js} +2084 -1889
- package/dist/{html-to-image-DXwLcQ6l.js → html-to-image-DLSOS-bE.js} +2351 -2282
- package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
- package/dist/main.js +1233 -1133
- package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-C6_e1pT_.js → process-output-B9ysqk1y.js} +1 -1
- package/dist/{reveal-component-B2onAQFS.js → reveal-component-x6IwgqPf.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
- package/package.json +3 -3
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/charts.tsx +24 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -2
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/readonly-python-code.tsx +16 -2
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/output/MarimoTracebackOutput.tsx +10 -1
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +6 -2
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/extensions.ts +1 -4
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +94 -0
- package/src/core/codemirror/completion/completer.ts +12 -1
- package/src/core/codemirror/completion/signature-hint.ts +68 -0
- package/src/core/codemirror/language/languages/python.ts +10 -4
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +9 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +53 -2
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +52 -2
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
|
@@ -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 {
|
|
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
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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: "
|
|
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, "
|
|
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=
|
|
216
|
+
language={language}
|
|
214
217
|
className="cm border rounded overflow-hidden"
|
|
215
218
|
extensions={extensions}
|
|
216
|
-
value={
|
|
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 {
|
|
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.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
|
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 {
|
|
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
|
-
|
|
96
|
-
|
|
125
|
+
interface FixModeConfig {
|
|
126
|
+
Icon: LucideIcon;
|
|
127
|
+
title: string;
|
|
128
|
+
description: string;
|
|
129
|
+
}
|
|
97
130
|
|
|
98
|
-
const
|
|
99
|
-
|
|
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={
|
|
179
|
+
onClick={onAction}
|
|
120
180
|
>
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 = ({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
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
|
-
|
|
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 {
|
|
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 && (
|