@marimo-team/islands 0.23.14-dev1 → 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.
Files changed (60) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
  2. package/dist/{chat-ui-CsPewo4h.js → chat-ui-DYBcNEdd.js} +81 -81
  3. package/dist/{code-visibility-aprTe3au.js → code-visibility-6TEcNJ_h.js} +1030 -837
  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-MqcD07Bw.js} +2218 -2186
  7. package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
  8. package/dist/main.js +254 -254
  9. package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
  10. package/dist/{process-output-C6_e1pT_.js → process-output-Dt3icftd.js} +1 -1
  11. package/dist/{reveal-component-j-ug9bTS.js → reveal-component-BT4BI95e.js} +5 -5
  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 +1 -1
  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/editor/actions/useNotebookActions.tsx +2 -2
  23. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +22 -0
  24. package/src/components/editor/chrome/panels/snippet-display.ts +27 -0
  25. package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -2
  26. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  27. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  28. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  29. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  30. package/src/components/editor/errors/__tests__/auto-fix.test.ts +119 -0
  31. package/src/components/editor/errors/auto-fix.tsx +108 -34
  32. package/src/components/editor/errors/fix-mode.ts +1 -1
  33. package/src/components/editor/output/MarimoTracebackOutput.tsx +10 -1
  34. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  35. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  36. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  37. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  38. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  39. package/src/core/ai/config.ts +2 -2
  40. package/src/core/ai/state.ts +11 -0
  41. package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +94 -0
  42. package/src/core/codemirror/completion/completer.ts +12 -1
  43. package/src/core/codemirror/completion/signature-hint.ts +68 -0
  44. package/src/core/codemirror/language/languages/python.ts +10 -4
  45. package/src/core/codemirror/utils.ts +15 -0
  46. package/src/core/edit-app.tsx +8 -5
  47. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  48. package/src/core/lifecycle/render-policy.ts +125 -0
  49. package/src/core/run-app.tsx +27 -23
  50. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  51. package/src/core/runtime/adapter.ts +182 -0
  52. package/src/core/wasm/PyodideLoader.tsx +20 -62
  53. package/src/core/wasm/bridge.ts +9 -4
  54. package/src/core/wasm/state.ts +8 -19
  55. package/src/core/wasm/store.ts +1 -1
  56. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  57. package/src/hooks/useDelayElapsed.ts +27 -0
  58. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  59. package/src/core/wasm/__tests__/state.test.ts +0 -124
  60. /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 { 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,
@@ -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
- const PromptIcon = SparklesIcon;
96
- const AutofixIcon = WrenchIcon;
145
+ interface FixModeConfig {
146
+ Icon: LucideIcon;
147
+ title: string;
148
+ description: string;
149
+ }
97
150
 
98
- const PromptTitle = "Suggest a prompt";
99
- const AutofixTitle = "Fix with AI";
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={fixMode === "prompt" ? openPrompt : applyAutofix}
199
+ onClick={onAction}
120
200
  >
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}
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
- <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>
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 = ({ 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";
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
- {icon}
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 { AIFixButton } from "../errors/auto-fix";
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
- <TooltipProvider>
28
- <MarimoTracebackOutput traceback={Tracebacks.raw} cellId={cid} />
29
- </TooltipProvider>
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
- <TooltipProvider>
48
- <MarimoTracebackOutput traceback={Tracebacks.raw} cellId={cid} />
49
- </TooltipProvider>
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
+ }