@marimo-team/islands 0.23.14-dev5 → 0.23.14-dev50

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 (128) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
  2. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DXPRhRO2.js} +3248 -3044
  3. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  4. package/dist/{code-visibility-rxZi4Phe.js → code-visibility-BFDT-GEQ.js} +1153 -946
  5. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  6. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  7. package/dist/{formats-d6MhLuQ9.js → formats-WsOgyW_K.js} +1 -1
  8. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-7wxMGXjG.js} +2084 -1889
  9. package/dist/{html-to-image-CGp_08St.js → html-to-image-DOqQBSQc.js} +2367 -2301
  10. package/dist/{input-CbEz_aj_.js → input-BSdZp5Ng.js} +1 -1
  11. package/dist/main.js +2707 -1186
  12. package/dist/{mermaid-CJW9vIyO.js → mermaid-D-HYBMEV.js} +2 -2
  13. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  14. package/dist/{process-output-R6JsYrv3.js → process-output-kGk2Jc2x.js} +1 -1
  15. package/dist/{reveal-component-sEb3Wd1x.js → reveal-component-uSCMaaUE.js} +600 -596
  16. package/dist/{spec-Bv-XlYiv.js → spec-CnTgI25l.js} +1 -1
  17. package/dist/style.css +1 -1
  18. package/dist/{toDate-D-l5s8nn.js → toDate-D1Z7ZXWh.js} +1 -1
  19. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-BMc8itk2.js} +1 -1
  20. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-ZwmDBRDY.js} +1 -1
  21. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-CxffarYV.js} +1 -1
  22. package/dist/{useTheme-BrYvK-_A.js → useTheme-yGsGEk82.js} +26 -24
  23. package/dist/{vega-component-Pk6lyc_a.js → vega-component-BFJTyykA.js} +5 -5
  24. package/package.json +3 -3
  25. package/src/__mocks__/requests.ts +1 -0
  26. package/src/components/app-config/user-config-form.tsx +26 -0
  27. package/src/components/chat/acp/agent-panel.tsx +35 -1
  28. package/src/components/chat/chat-panel.tsx +68 -29
  29. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  30. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  31. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  32. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  33. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  34. package/src/components/data-table/charts/charts.tsx +23 -1
  35. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  36. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  37. package/src/components/debugger/debugger-code.tsx +7 -2
  38. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  39. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  40. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  41. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  42. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  43. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  44. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  45. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  46. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  47. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  48. package/src/components/editor/errors/auto-fix.tsx +88 -34
  49. package/src/components/editor/errors/fix-mode.ts +1 -1
  50. package/src/components/editor/notebook-cell.tsx +7 -0
  51. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  52. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  53. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  54. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  55. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  56. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  57. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  58. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  59. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  60. package/src/components/slides/minimap.tsx +7 -1
  61. package/src/components/slides/reveal-component.tsx +3 -0
  62. package/src/components/slides/slide-cell-view.tsx +10 -6
  63. package/src/components/slides/slide.tsx +16 -13
  64. package/src/core/ai/config.ts +2 -2
  65. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  66. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  67. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  68. package/src/core/ai/context/providers/datasource.ts +27 -2
  69. package/src/core/ai/context/providers/error.ts +226 -36
  70. package/src/core/ai/context/registry.ts +77 -43
  71. package/src/core/ai/state.ts +11 -0
  72. package/src/core/cells/__tests__/cell.test.ts +39 -0
  73. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  74. package/src/core/cells/cell.ts +5 -3
  75. package/src/core/cells/readonly-code-display.ts +35 -0
  76. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  77. package/src/core/codemirror/ai/resources.ts +15 -10
  78. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  79. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +139 -0
  80. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  81. package/src/core/codemirror/cells/debugger-state.ts +100 -0
  82. package/src/core/codemirror/cells/extensions.ts +23 -4
  83. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  84. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  85. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  86. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  87. package/src/core/codemirror/utils.ts +15 -0
  88. package/src/core/config/feature-flag.tsx +2 -0
  89. package/src/core/edit-app.tsx +8 -5
  90. package/src/core/errors/errors.ts +2 -1
  91. package/src/core/islands/bootstrap.ts +1 -0
  92. package/src/core/islands/bridge.ts +1 -0
  93. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  94. package/src/core/lifecycle/render-policy.ts +125 -0
  95. package/src/core/mime.ts +11 -4
  96. package/src/core/network/__tests__/api.test.ts +17 -0
  97. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  98. package/src/core/network/api.ts +17 -6
  99. package/src/core/network/requests-lazy.ts +1 -0
  100. package/src/core/network/requests-network.ts +8 -0
  101. package/src/core/network/requests-static.ts +1 -0
  102. package/src/core/network/requests-toasting.tsx +10 -1
  103. package/src/core/network/types.ts +2 -0
  104. package/src/core/run-app.tsx +27 -23
  105. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  106. package/src/core/runtime/adapter.ts +182 -0
  107. package/src/core/wasm/PyodideLoader.tsx +20 -62
  108. package/src/core/wasm/bridge.ts +13 -4
  109. package/src/core/wasm/state.ts +8 -19
  110. package/src/core/websocket/useMarimoKernelConnection.tsx +17 -0
  111. package/src/css/app/codemirror.css +14 -0
  112. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  113. package/src/hooks/useDelayElapsed.ts +27 -0
  114. package/src/plugins/core/RenderHTML.tsx +35 -1
  115. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  116. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  117. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +53 -2
  118. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +52 -2
  119. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  120. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  121. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  122. package/src/utils/errors.ts +15 -0
  123. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  124. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  125. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  126. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  127. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  128. package/src/core/wasm/__tests__/state.test.ts +0 -124
@@ -27,6 +27,7 @@ import { useDebouncedCallback } from "@/hooks/useDebounce";
27
27
  import type { GetDataUrl } from "@/plugins/impl/DataTablePlugin";
28
28
  import { vegaLoadData } from "@/plugins/impl/vega/loader";
29
29
  import { useTheme } from "@/theme/useTheme";
30
+ import { uniqueBy } from "@/utils/arrays";
30
31
  import { inferFieldTypes } from "../columns";
31
32
  import {
32
33
  type FieldTypesWithExternalType,
@@ -54,6 +55,22 @@ const CHART_HEIGHT = 290;
54
55
  const CHART_MAX_ROWS = 50_000;
55
56
  const CHART_MAX_COLUMNS = 50;
56
57
 
58
+ /**
59
+ * Append row-header (index) fields to the chart field list, skipping any whose
60
+ * name already exists as a data column so the chart builder never offers the
61
+ * same axis twice.
62
+ */
63
+ export function mergeIndexFields(
64
+ fieldTypes: FieldTypesWithExternalType | null | undefined,
65
+ rowHeaders: FieldTypesWithExternalType | null | undefined,
66
+ ): FieldTypesWithExternalType {
67
+ const base = fieldTypes ?? [];
68
+ if (!rowHeaders || rowHeaders.length === 0) {
69
+ return base;
70
+ }
71
+ return uniqueBy([...base, ...rowHeaders], (f) => f[0]);
72
+ }
73
+
57
74
  export interface TablePanelProps {
58
75
  cellId: CellId | null;
59
76
  data: unknown[];
@@ -64,6 +81,7 @@ export interface TablePanelProps {
64
81
  onCloseChartBuilder?: () => void;
65
82
  getDataUrl?: GetDataUrl;
66
83
  fieldTypes?: FieldTypesWithExternalType | null;
84
+ rowHeaders?: FieldTypesWithExternalType | null;
67
85
  }
68
86
 
69
87
  export const TablePanel: React.FC<TablePanelProps> = ({
@@ -74,6 +92,7 @@ export const TablePanel: React.FC<TablePanelProps> = ({
74
92
  columns,
75
93
  getDataUrl,
76
94
  fieldTypes,
95
+ rowHeaders,
77
96
  displayHeader,
78
97
  onCloseChartBuilder,
79
98
  }) => {
@@ -265,7 +284,10 @@ export const TablePanel: React.FC<TablePanelProps> = ({
265
284
  saveChart={saveChart}
266
285
  saveChartType={saveChartType}
267
286
  getDataUrl={getDataUrl}
268
- fieldTypes={fieldTypes ?? inferFieldTypes(dataTable.props.data)}
287
+ fieldTypes={mergeIndexFields(
288
+ fieldTypes ?? inferFieldTypes(dataTable.props.data),
289
+ rowHeaders,
290
+ )}
269
291
  isLargeDataset={isLargeDataset}
270
292
  />
271
293
  </TabsContent>
@@ -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
  ) : (
@@ -208,7 +208,7 @@ export const DebuggerControls: React.FC<{
208
208
  </Button>
209
209
  </Tooltip>
210
210
  {onClear && (
211
- <Tooltip content="Clear">
211
+ <Tooltip content="Quit and clear">
212
212
  <Button
213
213
  variant="text"
214
214
  size="icon"
@@ -217,7 +217,12 @@ export const DebuggerControls: React.FC<{
217
217
  buttonClasses,
218
218
  "text-(--red-11) hover:text-(--red-11) hover:bg-(--red-2) hover:border-(--red-8)",
219
219
  )}
220
- onClick={onClear}
220
+ onClick={() => {
221
+ // Quit the debugger (`q`) before tearing down the console, so
222
+ // the stdin response targets the still-present pdb prompt.
223
+ onSubmit("q");
224
+ onClear();
225
+ }}
221
226
  >
222
227
  <TrashIcon fontSize={36} className={iconClasses} />
223
228
  </Button>
@@ -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
 
@@ -23,7 +23,7 @@ import { toast } from "@/components/ui/use-toast";
23
23
  import { useCellData, useCellRuntime } from "@/core/cells/cells";
24
24
  import { CellOutputId } from "@/core/cells/ids";
25
25
  import { isOutputEmpty } from "@/core/cells/outputs";
26
- import { goToDefinitionAtCursorPosition } from "@/core/codemirror/go-to-definition/utils";
26
+ import { goToDefinitionWithLspFallback } from "@/core/codemirror/go-to-definition/utils";
27
27
  import { sendToPanelManager } from "@/core/vscode/vscode-bindings";
28
28
  import { copyImageToClipboard, copyToClipboard } from "@/utils/copy";
29
29
  import { getImageExtension } from "@/utils/filenames";
@@ -170,7 +170,7 @@ export const CellActionsContextMenu = ({
170
170
  // Only suppress focus restoration when we actually navigated;
171
171
  // otherwise let Radix return focus to the trigger cell.
172
172
  suppressCloseAutoFocus.current =
173
- goToDefinitionAtCursorPosition(editorView);
173
+ goToDefinitionWithLspFallback(editorView);
174
174
  }
175
175
  },
176
176
  },
@@ -31,7 +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 { getSnippetDisplay } from "./snippet-display";
34
+ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
35
35
 
36
36
  const extensions = [EditorView.lineWrapping];
37
37
 
@@ -188,7 +188,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
188
188
  return null;
189
189
  }
190
190
 
191
- const { language, value } = getSnippetDisplay(code);
191
+ const { language, code: displayCode } = getReadonlyCodeDisplay(code);
192
192
 
193
193
  return (
194
194
  <div
@@ -216,7 +216,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
216
216
  language={language}
217
217
  className="cm border rounded overflow-hidden"
218
218
  extensions={extensions}
219
- value={value}
219
+ value={displayCode}
220
220
  readOnly={true}
221
221
  />
222
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
+ }
@@ -0,0 +1,79 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { beforeAll, describe, expect, it } from "vitest";
5
+ import { SetupMocks } from "@/__mocks__/common";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import { ReadonlyCode } from "../readonly-python-code";
8
+
9
+ beforeAll(() => {
10
+ SetupMocks.resizeObserver();
11
+ });
12
+
13
+ /**
14
+ * The only button is the show/hide toggle: copy is disabled and the
15
+ * insert-cell button is off by default.
16
+ */
17
+ function renderReadonly(props: { initiallyHideCode?: boolean }) {
18
+ return render(
19
+ <TooltipProvider>
20
+ <ReadonlyCode code="x = 1" showCopyCode={false} {...props} />
21
+ </TooltipProvider>,
22
+ );
23
+ }
24
+
25
+ function isCollapsed(root: ParentNode) {
26
+ return root.querySelector(".cm")?.classList.contains("opacity-20") ?? false;
27
+ }
28
+
29
+ describe("ReadonlyCode", () => {
30
+ it("starts collapsed when initiallyHideCode is true", () => {
31
+ const { container } = renderReadonly({ initiallyHideCode: true });
32
+ expect(isCollapsed(container)).toBe(true);
33
+ });
34
+
35
+ it("starts expanded when initiallyHideCode is false", () => {
36
+ const { container } = renderReadonly({ initiallyHideCode: false });
37
+ expect(isCollapsed(container)).toBe(false);
38
+ });
39
+
40
+ it("starts expanded when initiallyHideCode is unset", () => {
41
+ const { container } = renderReadonly({});
42
+ expect(isCollapsed(container)).toBe(false);
43
+ });
44
+
45
+ it("toggles visibility locally on click", () => {
46
+ const { container } = renderReadonly({ initiallyHideCode: true });
47
+ const toggle = screen.getByRole("button");
48
+
49
+ fireEvent.click(toggle);
50
+ expect(isCollapsed(container)).toBe(false);
51
+
52
+ fireEvent.click(toggle);
53
+ expect(isCollapsed(container)).toBe(true);
54
+ });
55
+
56
+ it("keeps each instance's visibility independent", () => {
57
+ const { container } = render(
58
+ <TooltipProvider>
59
+ <ReadonlyCode
60
+ code="a = 1"
61
+ showCopyCode={false}
62
+ initiallyHideCode={true}
63
+ />
64
+ <ReadonlyCode
65
+ code="b = 2"
66
+ showCopyCode={false}
67
+ initiallyHideCode={true}
68
+ />
69
+ </TooltipProvider>,
70
+ );
71
+ const [firstToggle] = screen.getAllByRole("button");
72
+ const [first, second] = container.querySelectorAll(".cm");
73
+
74
+ fireEvent.click(firstToggle);
75
+
76
+ expect(first.classList.contains("opacity-20")).toBe(false);
77
+ expect(second.classList.contains("opacity-20")).toBe(true);
78
+ });
79
+ });