@marimo-team/islands 0.23.14-dev0 → 0.23.14-dev10
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-DYBcNEdd.js} +81 -81
- package/dist/{code-visibility-Dd0Rop7P.js → code-visibility-6TEcNJ_h.js} +1030 -837
- package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-qpmKyAPn.js} +2 -2
- package/dist/{html-to-image-DXwLcQ6l.js → html-to-image-MqcD07Bw.js} +2218 -2186
- package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
- package/dist/main.js +254 -254
- package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
- package/dist/{process-output-C6_e1pT_.js → process-output-Dt3icftd.js} +1 -1
- package/dist/{reveal-component-C8c6Dj--.js → reveal-component-BT4BI95e.js} +5 -5
- package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +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 +1 -1
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +22 -0
- package/src/components/editor/chrome/panels/snippet-display.ts +27 -0
- 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/errors/__tests__/auto-fix.test.ts +119 -0
- package/src/components/editor/errors/auto-fix.tsx +108 -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/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/core/ai/config.ts +2 -2
- package/src/core/ai/state.ts +11 -0
- 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/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- 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/core/wasm/store.ts +1 -1
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- /package/dist/{files → export_demos}/wasm-intro.py +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { cellId } from "@/__tests__/branded";
|
|
5
|
+
import type { MarimoError } from "@/core/kernel/messages";
|
|
6
|
+
|
|
7
|
+
const getDatasourceContext = vi.fn<(id: unknown) => string | null>(() => null);
|
|
8
|
+
|
|
9
|
+
vi.mock("@/core/ai/context/providers/datasource", () => ({
|
|
10
|
+
getDatasourceContext: (id: unknown) => getDatasourceContext(id),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const { buildFixPrompt, buildFixPromptFromText } = await import("../auto-fix");
|
|
14
|
+
|
|
15
|
+
describe("buildFixPromptFromText", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getDatasourceContext.mockReset();
|
|
18
|
+
getDatasourceContext.mockReturnValue(null);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("includes the cell id in the header when provided", () => {
|
|
22
|
+
const prompt = buildFixPromptFromText("boom", cellId("cell-1"));
|
|
23
|
+
expect(prompt).toBe(
|
|
24
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nboom",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("uses a generic header when no cell id is provided", () => {
|
|
29
|
+
const prompt = buildFixPromptFromText("boom");
|
|
30
|
+
expect(prompt).toBe(
|
|
31
|
+
"My code gives the following error. Please fix it:\n\nboom",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("appends datasource context when explicitly requested for the cell", () => {
|
|
36
|
+
getDatasourceContext.mockReturnValue("@datasource://my_db");
|
|
37
|
+
const prompt = buildFixPromptFromText("boom", cellId("cell-1"), {
|
|
38
|
+
includeDatasourceContext: true,
|
|
39
|
+
});
|
|
40
|
+
expect(prompt).toBe(
|
|
41
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nboom\n\nDatabase schema: @datasource://my_db",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not append datasource context by default", () => {
|
|
46
|
+
getDatasourceContext.mockReturnValue("@datasource://my_db");
|
|
47
|
+
const prompt = buildFixPromptFromText("boom", cellId("cell-1"));
|
|
48
|
+
expect(prompt).toBe(
|
|
49
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nboom",
|
|
50
|
+
);
|
|
51
|
+
expect(getDatasourceContext).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not look up datasource context without a cell id", () => {
|
|
55
|
+
buildFixPromptFromText("boom", undefined, {
|
|
56
|
+
includeDatasourceContext: true,
|
|
57
|
+
});
|
|
58
|
+
expect(getDatasourceContext).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("buildFixPrompt", () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
getDatasourceContext.mockReset();
|
|
65
|
+
getDatasourceContext.mockReturnValue(null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("uses the error message when present", () => {
|
|
69
|
+
const errors: MarimoError[] = [
|
|
70
|
+
{ type: "sql-error", msg: "syntax error", sql_statement: "SELECT" },
|
|
71
|
+
];
|
|
72
|
+
expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
|
|
73
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nsyntax error",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("joins multiple errors with newlines", () => {
|
|
78
|
+
const errors: MarimoError[] = [
|
|
79
|
+
{ type: "syntax", msg: "bad syntax" },
|
|
80
|
+
{
|
|
81
|
+
type: "exception",
|
|
82
|
+
exception_type: "ValueError",
|
|
83
|
+
msg: "bad value",
|
|
84
|
+
raising_cell: null,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
|
|
88
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nbad syntax\nbad value",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("falls back to the error type when there is no message", () => {
|
|
93
|
+
const errors: MarimoError[] = [
|
|
94
|
+
{ type: "multiple-defs", name: "foo", cells: [cellId("foo")] },
|
|
95
|
+
];
|
|
96
|
+
expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
|
|
97
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nmultiple-defs",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("appends datasource context for SQL errors", () => {
|
|
102
|
+
getDatasourceContext.mockReturnValue("@datasource://my_db");
|
|
103
|
+
const errors: MarimoError[] = [
|
|
104
|
+
{ type: "sql-error", msg: "syntax error", sql_statement: "SELECT" },
|
|
105
|
+
];
|
|
106
|
+
expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
|
|
107
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nsyntax error\n\nDatabase schema: @datasource://my_db",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not append datasource context for non-SQL errors", () => {
|
|
112
|
+
getDatasourceContext.mockReturnValue("@datasource://my_db");
|
|
113
|
+
const errors: MarimoError[] = [{ type: "syntax", msg: "bad syntax" }];
|
|
114
|
+
expect(buildFixPrompt(errors, cellId("cell-1"))).toBe(
|
|
115
|
+
"My cell (id: cell-1) produced the following error. Please fix it:\n\nbad syntax",
|
|
116
|
+
);
|
|
117
|
+
expect(getDatasourceContext).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -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,
|
|
@@ -14,11 +21,46 @@ import { aiCompletionCellAtom } from "@/core/ai/state";
|
|
|
14
21
|
import { notebookAtom, useCellActions } from "@/core/cells/cells";
|
|
15
22
|
import type { CellId } from "@/core/cells/ids";
|
|
16
23
|
import { aiFeaturesEnabledAtom } from "@/core/config/config";
|
|
24
|
+
import { getDatasourceContext } from "@/core/ai/context/providers/datasource";
|
|
17
25
|
import { getAutoFixes } from "@/core/errors/errors";
|
|
18
26
|
import type { MarimoError } from "@/core/kernel/messages";
|
|
19
27
|
import { cn } from "@/utils/cn";
|
|
28
|
+
import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant";
|
|
20
29
|
import { type FixMode, useFixMode } from "./fix-mode";
|
|
21
30
|
|
|
31
|
+
export function buildFixPromptFromText(
|
|
32
|
+
errorText: string,
|
|
33
|
+
cellId?: CellId,
|
|
34
|
+
{
|
|
35
|
+
includeDatasourceContext = false,
|
|
36
|
+
}: { includeDatasourceContext?: boolean } = {},
|
|
37
|
+
): string {
|
|
38
|
+
const header =
|
|
39
|
+
cellId != null
|
|
40
|
+
? `My cell (id: ${cellId}) produced the following error. Please fix it:`
|
|
41
|
+
: "My code gives the following error. Please fix it:";
|
|
42
|
+
let prompt = `${header}\n\n${errorText}`;
|
|
43
|
+
if (cellId != null && includeDatasourceContext) {
|
|
44
|
+
const datasourceContext = getDatasourceContext(cellId);
|
|
45
|
+
if (datasourceContext) {
|
|
46
|
+
prompt += `\n\nDatabase schema: ${datasourceContext}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return prompt;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildFixPrompt(errors: MarimoError[], cellId: CellId): string {
|
|
53
|
+
const errorText = errors
|
|
54
|
+
.map((error) => ("msg" in error && error.msg ? error.msg : error.type))
|
|
55
|
+
.join("\n");
|
|
56
|
+
const includeDatasourceContext = errors.some(
|
|
57
|
+
(error) => error.type === "sql-error",
|
|
58
|
+
);
|
|
59
|
+
return buildFixPromptFromText(errorText, cellId, {
|
|
60
|
+
includeDatasourceContext,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
22
64
|
export const AutoFixButton = ({
|
|
23
65
|
errors,
|
|
24
66
|
cellId,
|
|
@@ -35,6 +77,7 @@ export const AutoFixButton = ({
|
|
|
35
77
|
getAutoFixes(error, { aiEnabled: aiFeaturesEnabled }),
|
|
36
78
|
);
|
|
37
79
|
const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
|
|
80
|
+
const openAiAssistant = useOpenAiAssistant();
|
|
38
81
|
|
|
39
82
|
if (autoFixes.length === 0) {
|
|
40
83
|
return null;
|
|
@@ -67,6 +110,12 @@ export const AutoFixButton = ({
|
|
|
67
110
|
editorView?.focus();
|
|
68
111
|
};
|
|
69
112
|
|
|
113
|
+
const openAISidebar = () => {
|
|
114
|
+
openAiAssistant({
|
|
115
|
+
prompt: buildFixPrompt(errors, cellId),
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
70
119
|
return (
|
|
71
120
|
<div className={cn("my-2", className)}>
|
|
72
121
|
{firstFix.fixType === "ai" ? (
|
|
@@ -74,6 +123,7 @@ export const AutoFixButton = ({
|
|
|
74
123
|
tooltip={firstFix.description}
|
|
75
124
|
openPrompt={() => handleFix(false)}
|
|
76
125
|
applyAutofix={() => handleFix(true)}
|
|
126
|
+
openChat={openAISidebar}
|
|
77
127
|
/>
|
|
78
128
|
) : (
|
|
79
129
|
<Tooltip content={firstFix.description} align="start">
|
|
@@ -92,23 +142,53 @@ export const AutoFixButton = ({
|
|
|
92
142
|
);
|
|
93
143
|
};
|
|
94
144
|
|
|
95
|
-
|
|
96
|
-
|
|
145
|
+
interface FixModeConfig {
|
|
146
|
+
Icon: LucideIcon;
|
|
147
|
+
title: string;
|
|
148
|
+
description: string;
|
|
149
|
+
}
|
|
97
150
|
|
|
98
|
-
const
|
|
99
|
-
|
|
151
|
+
const MODE_CONFIG: Record<FixMode, FixModeConfig> = {
|
|
152
|
+
autofix: {
|
|
153
|
+
Icon: WrenchIcon,
|
|
154
|
+
title: "Inline AI Fix",
|
|
155
|
+
description: "Apply AI fixes inline in the cell",
|
|
156
|
+
},
|
|
157
|
+
chat: {
|
|
158
|
+
Icon: HatGlasses,
|
|
159
|
+
title: "Fix with AI assistant",
|
|
160
|
+
description: "Open the AI sidebar to fix",
|
|
161
|
+
},
|
|
162
|
+
prompt: {
|
|
163
|
+
Icon: SparklesIcon,
|
|
164
|
+
title: "Suggest a prompt",
|
|
165
|
+
description: "Edit the prompt before applying",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const FIX_MODES: FixMode[] = ["autofix", "prompt", "chat"];
|
|
100
170
|
|
|
101
171
|
export const AIFixButton = ({
|
|
102
172
|
tooltip,
|
|
103
173
|
openPrompt,
|
|
104
174
|
applyAutofix,
|
|
175
|
+
openChat,
|
|
105
176
|
}: {
|
|
106
177
|
tooltip: string;
|
|
107
178
|
openPrompt: () => void;
|
|
108
179
|
applyAutofix: () => void;
|
|
180
|
+
openChat: () => void;
|
|
109
181
|
}) => {
|
|
110
182
|
const { fixMode, setFixMode } = useFixMode();
|
|
111
183
|
|
|
184
|
+
let onAction = openPrompt;
|
|
185
|
+
if (fixMode === "chat") {
|
|
186
|
+
onAction = openChat;
|
|
187
|
+
} else if (fixMode === "autofix") {
|
|
188
|
+
onAction = applyAutofix;
|
|
189
|
+
}
|
|
190
|
+
const { Icon, title } = MODE_CONFIG[fixMode];
|
|
191
|
+
|
|
112
192
|
return (
|
|
113
193
|
<div className="flex">
|
|
114
194
|
<Tooltip content={tooltip} align="start">
|
|
@@ -116,14 +196,10 @@ export const AIFixButton = ({
|
|
|
116
196
|
size="xs"
|
|
117
197
|
variant="outline"
|
|
118
198
|
className="font-normal rounded-r-none border-r-0"
|
|
119
|
-
onClick={
|
|
199
|
+
onClick={onAction}
|
|
120
200
|
>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
) : (
|
|
124
|
-
<AutofixIcon className="h-3 w-3 mr-2 mb-0.5" />
|
|
125
|
-
)}
|
|
126
|
-
{fixMode === "prompt" ? PromptTitle : AutofixTitle}
|
|
201
|
+
<Icon className="h-3 w-3 mr-2 mb-0.5" />
|
|
202
|
+
{title}
|
|
127
203
|
</Button>
|
|
128
204
|
</Tooltip>
|
|
129
205
|
<DropdownMenu>
|
|
@@ -138,40 +214,38 @@ export const AIFixButton = ({
|
|
|
138
214
|
</Button>
|
|
139
215
|
</DropdownMenuTrigger>
|
|
140
216
|
<DropdownMenuContent align="end" className="w-56">
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
217
|
+
{FIX_MODES.map((mode) => (
|
|
218
|
+
<DropdownMenuItem
|
|
219
|
+
key={mode}
|
|
220
|
+
className="flex items-center gap-2"
|
|
221
|
+
onClick={() => setFixMode(mode)}
|
|
222
|
+
>
|
|
223
|
+
<AiModeItem mode={mode} selected={mode === fixMode} />
|
|
224
|
+
</DropdownMenuItem>
|
|
225
|
+
))}
|
|
149
226
|
</DropdownMenuContent>
|
|
150
227
|
</DropdownMenu>
|
|
151
228
|
</div>
|
|
152
229
|
);
|
|
153
230
|
};
|
|
154
231
|
|
|
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";
|
|
232
|
+
const AiModeItem = ({
|
|
233
|
+
mode,
|
|
234
|
+
selected,
|
|
235
|
+
}: {
|
|
236
|
+
mode: FixMode;
|
|
237
|
+
selected: boolean;
|
|
238
|
+
}) => {
|
|
239
|
+
const { Icon, title, description } = MODE_CONFIG[mode];
|
|
167
240
|
|
|
168
241
|
return (
|
|
169
|
-
<div className="flex items-center gap-2">
|
|
170
|
-
|
|
242
|
+
<div className="flex items-center gap-2 w-full">
|
|
243
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
171
244
|
<div className="flex flex-col">
|
|
172
245
|
<span className="font-medium">{title}</span>
|
|
173
246
|
<span className="text-xs text-muted-foreground">{description}</span>
|
|
174
247
|
</div>
|
|
248
|
+
{selected && <CheckIcon className="h-4 w-4 ml-auto shrink-0" />}
|
|
175
249
|
</div>
|
|
176
250
|
);
|
|
177
251
|
};
|
|
@@ -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, buildFixPromptFromText } 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: buildFixPromptFromText(lastTracebackLine, cellId),
|
|
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 && (
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { render } from "@testing-library/react";
|
|
4
|
+
import { Provider } from "jotai";
|
|
4
5
|
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
|
6
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
5
7
|
import { Tracebacks } from "@/__mocks__/tracebacks";
|
|
6
8
|
import { cellId } from "@/__tests__/branded";
|
|
7
9
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
10
|
import { initialModeAtom } from "@/core/mode";
|
|
11
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
9
12
|
import { store } from "@/core/state/jotai";
|
|
10
13
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
11
14
|
import {
|
|
@@ -20,13 +23,16 @@ describe("traceback component", () => {
|
|
|
20
23
|
beforeEach(() => {
|
|
21
24
|
vi.resetAllMocks();
|
|
22
25
|
store.set(initialModeAtom, "edit");
|
|
26
|
+
store.set(requestClientAtom, MockRequestClient.create());
|
|
23
27
|
});
|
|
24
28
|
|
|
25
29
|
test("extracts cell-link", () => {
|
|
26
30
|
const traceback = (
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
31
|
+
<Provider store={store}>
|
|
32
|
+
<TooltipProvider>
|
|
33
|
+
<MarimoTracebackOutput traceback={Tracebacks.raw} cellId={cid} />
|
|
34
|
+
</TooltipProvider>
|
|
35
|
+
</Provider>
|
|
30
36
|
);
|
|
31
37
|
const { unmount, getAllByRole } = render(traceback);
|
|
32
38
|
|
|
@@ -44,9 +50,11 @@ describe("traceback component", () => {
|
|
|
44
50
|
|
|
45
51
|
test("renames File to Cell for relevant lines", () => {
|
|
46
52
|
const traceback = (
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
53
|
+
<Provider store={store}>
|
|
54
|
+
<TooltipProvider>
|
|
55
|
+
<MarimoTracebackOutput traceback={Tracebacks.raw} cellId={cid} />
|
|
56
|
+
</TooltipProvider>
|
|
57
|
+
</Provider>
|
|
50
58
|
);
|
|
51
59
|
const { unmount, container } = render(traceback);
|
|
52
60
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { type Atom, useAtomValue } from "jotai";
|
|
4
|
+
import type React from "react";
|
|
5
|
+
import type { PropsWithChildren } from "react";
|
|
6
|
+
import { useDelayElapsed } from "@/hooks/useDelayElapsed";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Children render once this atom resolves true. */
|
|
10
|
+
requires: Atom<boolean>;
|
|
11
|
+
fallback?: React.ReactNode;
|
|
12
|
+
/** Suppress the fallback for this many ms; avoids spinner flashes. */
|
|
13
|
+
delay?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <ProgressiveBoundary requires={canPaintAtom} delay={2000} fallback={<Spinner />}>
|
|
19
|
+
* <Editor />
|
|
20
|
+
* </ProgressiveBoundary>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const ProgressiveBoundary: React.FC<PropsWithChildren<Props>> = ({
|
|
24
|
+
requires,
|
|
25
|
+
fallback = null,
|
|
26
|
+
delay = 0,
|
|
27
|
+
children,
|
|
28
|
+
}) => {
|
|
29
|
+
const ready = useAtomValue(requires);
|
|
30
|
+
// Key the delay off `ready` so the suppression window re-arms whenever the
|
|
31
|
+
// gate closes again — otherwise a `true → false` flip would show the
|
|
32
|
+
// fallback immediately and reintroduce the flash `delay` is meant to avoid.
|
|
33
|
+
const delayElapsed = useDelayElapsed(ready ? 0 : delay);
|
|
34
|
+
|
|
35
|
+
if (ready) {
|
|
36
|
+
return children;
|
|
37
|
+
}
|
|
38
|
+
if (!delayElapsed) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import { AlertCircleIcon } from "lucide-react";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { Spinner } from "@/components/icons/spinner";
|
|
7
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
8
|
+
import { type AdapterState, runtimeAdapterAtom } from "@/core/runtime/adapter";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Footer pill that surfaces the active runtime's connection/initialization
|
|
12
|
+
* status via its adapter. Hides itself once the runtime is ready.
|
|
13
|
+
*/
|
|
14
|
+
export const RuntimeStatusBadge: React.FC = () => {
|
|
15
|
+
const adapter = useAtomValue(runtimeAdapterAtom);
|
|
16
|
+
const state = useAtomValue(adapter.state);
|
|
17
|
+
|
|
18
|
+
if (state.kind === "ready") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const icon =
|
|
23
|
+
state.kind === "failed" ? (
|
|
24
|
+
<AlertCircleIcon className="w-4 h-4 text-destructive" />
|
|
25
|
+
) : (
|
|
26
|
+
<Spinner size="small" />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Tooltip
|
|
31
|
+
content={
|
|
32
|
+
<div className="text-sm whitespace-pre-line">
|
|
33
|
+
{tooltipFor(state, adapter.label)}
|
|
34
|
+
</div>
|
|
35
|
+
}
|
|
36
|
+
data-testid="footer-runtime-status"
|
|
37
|
+
>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
className="p-1 hover:bg-accent rounded flex items-center gap-1.5 text-xs text-muted-foreground"
|
|
41
|
+
data-testid="runtime-status-footer"
|
|
42
|
+
>
|
|
43
|
+
{icon}
|
|
44
|
+
<span>{adapter.label}</span>
|
|
45
|
+
</button>
|
|
46
|
+
</Tooltip>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function tooltipFor(state: AdapterState, label: string): string {
|
|
51
|
+
switch (state.kind) {
|
|
52
|
+
case "failed":
|
|
53
|
+
return state.error.message;
|
|
54
|
+
case "connecting":
|
|
55
|
+
return state.progress?.label ?? `${label} starting…`;
|
|
56
|
+
case "ready":
|
|
57
|
+
return `${label} ready`;
|
|
58
|
+
}
|
|
59
|
+
}
|