@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
@@ -7,9 +7,9 @@ import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
7
7
  import "./zod-CijjQh4u.js";
8
8
  import { n as ErrorBanner } from "./error-banner-DFPfz_Qf.js";
9
9
  import { t as isEmpty_default } from "./isEmpty-CJJMn-QP.js";
10
- import { n as useTheme } from "./useTheme-BrYvK-_A.js";
10
+ import { n as useTheme } from "./useTheme-yGsGEk82.js";
11
11
  import { t as purify } from "./purify.es-H92eMd9-.js";
12
- import { t as useAsyncData } from "./useAsyncData-1Dhzjfwf.js";
12
+ import { t as useAsyncData } from "./useAsyncData-BMc8itk2.js";
13
13
  import { a as decodeEntities, f as isDetailedError, g as utils_default, h as removeDirectives, i as cleanAndMerge, o as encodeEntities } from "./chunk-S3R3BYOJ-oAe3dEbO.js";
14
14
  import { a as setLogLevel, i as log, r as __name, t as select_default } from "./src-Bf2iLOlr.js";
15
15
  import { t as package_default } from "./chunk-DR5Q36YT-BflwErH1.js";
@@ -1,6 +1,6 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
3
- import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-DXwLcQ6l.js";
3
+ import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-MqcD07Bw.js";
4
4
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
5
5
  import { t as Strings } from "./strings-Dq_j3Rxw.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
@@ -6,17 +6,17 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
6
6
  import { _ as Logger, g as cn, h as Events, l as useEventListener, t as Button } from "./button-BacYv-bE.js";
7
7
  import { t as require_react } from "./react-DA-nE2FX.js";
8
8
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
- import { lt as kioskModeAtom } from "./html-to-image-DXwLcQ6l.js";
9
+ import { lt as kioskModeAtom } from "./html-to-image-MqcD07Bw.js";
10
10
  import "./chunk-5FQGJX7Z-BbqSm5gU.js";
11
11
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
12
- import { a as DEFAULT_SLIDE_TYPE, an as EyeOff, c as Slide, ct as PanelResizeHandle, i as DEFAULT_DECK_TRANSITION, ln as Code, on as Expand, ot as Panel, s as SlideSidebar, st as PanelGroup, t as useNotebookCodeAvailable } from "./code-visibility-aprTe3au.js";
13
- import { X as useDebouncedCallback } from "./input-CbEz_aj_.js";
14
- import "./toDate-D-l5s8nn.js";
12
+ import { a as DEFAULT_SLIDE_TYPE, an as EyeOff, c as Slide, ct as PanelResizeHandle, i as DEFAULT_DECK_TRANSITION, ln as Code, on as Expand, ot as Panel, s as SlideSidebar, st as PanelGroup, t as useNotebookCodeAvailable } from "./code-visibility-6TEcNJ_h.js";
13
+ import { X as useDebouncedCallback } from "./input-BSdZp5Ng.js";
14
+ import "./toDate-D1Z7ZXWh.js";
15
15
  import "./react-dom-BTJzcVJ9.js";
16
16
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
17
17
  import { X as useFullScreenElement } from "./zod-CijjQh4u.js";
18
18
  import { t as Tooltip } from "./tooltip-Czds6Qr8.js";
19
- import { T as useEvent_default, _ as useAtomValue, m as isIslands } from "./useTheme-BrYvK-_A.js";
19
+ import { D as useEvent_default, g as isIslands, y as useAtomValue } from "./useTheme-yGsGEk82.js";
20
20
  import "./dist-U4F-tbMs.js";
21
21
  import "./main-Tj_-QTyF.js";
22
22
  import "./dist-Dv_Y15yk.js";
@@ -1,5 +1,5 @@
1
1
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
2
- import { s as Hash } from "./useLifecycle-AHlswLw-.js";
2
+ import { s as Hash } from "./useLifecycle-CxffarYV.js";
3
3
  import { i as Table, o as ChartPie } from "./useDateFormatter-CMnRuVmN.js";
4
4
  import { C as logNever } from "./strings-Dq_j3Rxw.js";
5
5
  var AlignCenterVertical = createLucideIcon("align-center-vertical", [
@@ -6,7 +6,7 @@ import { _ as Logger } from "./button-BacYv-bE.js";
6
6
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
7
7
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
8
8
  import { r as KnownQueryParams } from "./constants-T20xxyNf.js";
9
- import { b as atom, d as store, m as isIslands, p as waitFor } from "./useTheme-BrYvK-_A.js";
9
+ import { S as atom, g as isIslands, h as waitFor, p as store } from "./useTheme-yGsGEk82.js";
10
10
  import { t as invariant } from "./invariant-wRzNXIsJ.js";
11
11
  var CircleQuestionMark = createLucideIcon("circle-question-mark", [
12
12
  ["circle", {
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DA-nE2FX.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
4
- import { T as useEvent_default } from "./useTheme-BrYvK-_A.js";
4
+ import { D as useEvent_default } from "./useTheme-yGsGEk82.js";
5
5
  import { t as invariant } from "./invariant-wRzNXIsJ.js";
6
6
  var import_compiler_runtime = require_compiler_runtime(), import_react = /* @__PURE__ */ __toESM(require_react(), 1), Result = {
7
7
  error(e, s) {
@@ -1,6 +1,6 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DA-nE2FX.js";
3
- import { w as dequal } from "./useTheme-BrYvK-_A.js";
3
+ import { E as dequal } from "./useTheme-yGsGEk82.js";
4
4
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
5
5
  function useDeepCompareMemoize(e) {
6
6
  let i = import_react.useRef(e);
@@ -4,7 +4,7 @@ import { t as require_react } from "./react-DA-nE2FX.js";
4
4
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
5
5
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
7
- import { b as atom, v as useSetAtom } from "./useTheme-BrYvK-_A.js";
7
+ import { S as atom, b as useSetAtom } from "./useTheme-yGsGEk82.js";
8
8
  var Calendar = createLucideIcon("calendar", [
9
9
  ["path", {
10
10
  d: "M8 2v4",
@@ -709,8 +709,8 @@ function useResolvedMarimoConfig() {
709
709
  function getResolvedMarimoConfig() {
710
710
  return store.get(resolvedMarimoConfigAtom);
711
711
  }
712
- atom((e) => isAiEnabled(e(resolvedMarimoConfigAtom))), atom((e) => isAiModelConfigured(e(resolvedMarimoConfigAtom)));
713
- const aiFeaturesEnabledAtom = atom((e) => isAiFeatureEnabled(e(resolvedMarimoConfigAtom)));
712
+ atom((e) => isAiEnabled(e(resolvedMarimoConfigAtom)));
713
+ const aiModelConfiguredAtom = atom((e) => isAiModelConfigured(e(resolvedMarimoConfigAtom))), aiFeaturesEnabledAtom = atom((e) => isAiFeatureEnabled(e(resolvedMarimoConfigAtom)));
714
714
  atom((e) => e(resolvedMarimoConfigAtom).display.code_editor_font_size);
715
715
  const localeAtom = atom((e) => e(resolvedMarimoConfigAtom).display.locale);
716
716
  function isAiEnabled(e) {
@@ -790,29 +790,31 @@ function useTheme() {
790
790
  return e[1] === j ? M = e[2] : (M = { theme: j }, e[1] = j, e[2] = M), M;
791
791
  }
792
792
  export {
793
- getBuildingBlocks as C,
794
- buildStore as S,
795
- useEvent_default as T,
796
- useAtomValue as _,
797
- getResolvedMarimoConfig as a,
798
- atom as b,
799
- useResolvedMarimoConfig as c,
800
- store as d,
801
- useJotaiEffect as f,
802
- useAtom as g,
803
- Provider as h,
804
- autoInstantiateAtom as i,
805
- AppConfigSchema as l,
806
- isIslands as m,
793
+ createStore as C,
794
+ useEvent_default as D,
795
+ dequal as E,
796
+ atom as S,
797
+ getBuildingBlocks as T,
798
+ Provider as _,
799
+ autoInstantiateAtom as a,
800
+ useSetAtom as b,
801
+ resolvedMarimoConfigAtom as c,
802
+ AppConfigSchema as d,
803
+ createDeepEqualAtom as f,
804
+ isIslands as g,
805
+ waitFor as h,
806
+ aiModelConfiguredAtom as i,
807
+ useResolvedMarimoConfig as l,
808
+ useJotaiEffect as m,
807
809
  useTheme as n,
808
- localeAtom as o,
809
- waitFor as p,
810
+ getResolvedMarimoConfig as o,
811
+ store as p,
810
812
  aiFeaturesEnabledAtom as r,
811
- resolvedMarimoConfigAtom as s,
813
+ localeAtom as s,
812
814
  resolvedThemeAtom as t,
813
- createDeepEqualAtom as u,
814
- useSetAtom as v,
815
- dequal as w,
816
- createStore as x,
817
- useStore as y
815
+ userConfigAtom as u,
816
+ useAtom as v,
817
+ buildStore as w,
818
+ useStore as x,
819
+ useAtomValue as y
818
820
  };
@@ -2,23 +2,23 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { _ as Logger, c as Objects, g as cn, h as Events } from "./button-BacYv-bE.js";
3
3
  import { t as require_react } from "./react-DA-nE2FX.js";
4
4
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
5
- import { c as asRemoteURL, v as CircleQuestionMark } from "./toDate-D-l5s8nn.js";
5
+ import { c as asRemoteURL, v as CircleQuestionMark } from "./toDate-D1Z7ZXWh.js";
6
6
  import "./react-dom-BTJzcVJ9.js";
7
7
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
8
8
  import "./zod-CijjQh4u.js";
9
9
  import { n as ErrorBanner } from "./error-banner-DFPfz_Qf.js";
10
10
  import { t as Tooltip } from "./tooltip-Czds6Qr8.js";
11
11
  import { i as debounce_default } from "./constants-T20xxyNf.js";
12
- import { T as useEvent_default, n as useTheme } from "./useTheme-BrYvK-_A.js";
12
+ import { D as useEvent_default, n as useTheme } from "./useTheme-yGsGEk82.js";
13
13
  import { s as uniq } from "./arrays-sEtDRoG4.js";
14
- import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-d6MhLuQ9.js";
14
+ import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-WsOgyW_K.js";
15
15
  import { n as formats } from "./vega-loader.browser-CZ-J8Py3.js";
16
16
  import { a as getContainerWidth, n as vegaLoadData, s as tooltipHandler } from "./loader-Boph2xIS.js";
17
17
  import { t as j } from "./react-vega-B0sAlDTL.js";
18
18
  import "./defaultLocale-u-3osm0P.js";
19
19
  import "./defaultLocale-BoHTsDG6.js";
20
- import { t as useAsyncData } from "./useAsyncData-1Dhzjfwf.js";
21
- import { t as useDeepCompareMemoize } from "./useDeepCompareMemoize-CDWT3BDz.js";
20
+ import { t as useAsyncData } from "./useAsyncData-BMc8itk2.js";
21
+ import { t as useDeepCompareMemoize } from "./useDeepCompareMemoize-ZwmDBRDY.js";
22
22
  import { t as Semaphore } from "./semaphore-CNDGTzkX.js";
23
23
  var import_compiler_runtime = require_compiler_runtime(), import_react = /* @__PURE__ */ __toESM(require_react(), 1);
24
24
  function fixRelativeUrl(e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.14-dev1",
3
+ "version": "0.23.14-dev10",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -49,6 +49,7 @@ import {
49
49
  addContextCompletion,
50
50
  CONTEXT_TRIGGER,
51
51
  } from "@/components/editor/ai/completion-utils";
52
+ import { pendingAiPromptAtom } from "@/core/ai/state";
52
53
  import {
53
54
  Select,
54
55
  SelectContent,
@@ -72,6 +73,7 @@ import {
72
73
  SendButton,
73
74
  } from "../chat-components";
74
75
  import { useFileState } from "../chat-utils";
76
+ import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
75
77
  import { ReadyToChatBlock } from "./blocks";
76
78
  import {
77
79
  convertFilesToResourceLinks,
@@ -305,6 +307,7 @@ interface PromptAreaProps {
305
307
  onModeChange?: (mode: string) => void;
306
308
  sessionModels?: SessionModelState | null;
307
309
  onModelChange?: (modelId: string) => void;
310
+ inputRef: React.RefObject<ReactCodeMirrorRef | null>;
308
311
  }
309
312
 
310
313
  const PromptArea = memo<PromptAreaProps>(
@@ -322,8 +325,8 @@ const PromptArea = memo<PromptAreaProps>(
322
325
  onModeChange,
323
326
  sessionModels,
324
327
  onModelChange,
328
+ inputRef,
325
329
  }) => {
326
- const inputRef = useRef<ReactCodeMirrorRef | null>(null);
327
330
  const promptCompletions: AdditionalCompletions | undefined = useMemo(() => {
328
331
  if (!commands) {
329
332
  return undefined;
@@ -660,6 +663,8 @@ const AgentPanel: React.FC = () => {
660
663
  const [isLoading, setIsLoading] = useState(false);
661
664
  const [error, setError] = useState<Error | string | null>(null);
662
665
  const [promptValue, setPromptValue] = useState("");
666
+ const promptInputRef = useRef<ReactCodeMirrorRef | null>(null);
667
+ const [pendingPrompt, setPendingPrompt] = useAtom(pendingAiPromptAtom);
663
668
  const { files, addFiles, clearFiles, removeFile } = useFileState();
664
669
  const [sessionModels, setSessionModels] = useState<SessionModelState | null>(
665
670
  null,
@@ -973,6 +978,34 @@ const AgentPanel: React.FC = () => {
973
978
  },
974
979
  );
975
980
 
981
+ // Consume a prompt queued by another part of the app (e.g. error auto-fix).
982
+ useEffect(() => {
983
+ if (
984
+ !activeSessionId ||
985
+ !agent ||
986
+ isLoading ||
987
+ connectionState.status !== "connected" ||
988
+ !pendingPrompt
989
+ ) {
990
+ return;
991
+ }
992
+ setPendingPrompt(null);
993
+ if (pendingPrompt.submit) {
994
+ void handlePromptSubmit(undefined, pendingPrompt.prompt);
995
+ } else {
996
+ setPromptValue(pendingPrompt.prompt);
997
+ focusInputAndMoveToEnd(promptInputRef);
998
+ }
999
+ }, [
1000
+ activeSessionId,
1001
+ agent,
1002
+ isLoading,
1003
+ connectionState.status,
1004
+ pendingPrompt,
1005
+ setPendingPrompt,
1006
+ handlePromptSubmit,
1007
+ ]);
1008
+
976
1009
  // Handler for stopping the current operation
977
1010
  const handleStop = useEvent(async () => {
978
1011
  if (!activeSessionId || !agent) {
@@ -1187,6 +1220,7 @@ const AgentPanel: React.FC = () => {
1187
1220
  onModeChange={handleModeChange}
1188
1221
  sessionModels={sessionModels}
1189
1222
  onModelChange={handleModelChange}
1223
+ inputRef={promptInputRef}
1190
1224
  />
1191
1225
  </>
1192
1226
  );
@@ -45,6 +45,7 @@ import {
45
45
  type Chat,
46
46
  type ChatId,
47
47
  chatStateAtom,
48
+ pendingAiPromptAtom,
48
49
  } from "@/core/ai/state";
49
50
  import type { ToolNotebookContext } from "@/core/ai/tools/base";
50
51
  import {
@@ -94,6 +95,7 @@ import {
94
95
  useFileState,
95
96
  } from "./chat-utils";
96
97
  import { getCodes } from "@/core/codemirror/copilot/getCodes";
98
+ import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
97
99
 
98
100
  // Default mode for the AI
99
101
  const DEFAULT_MODE = "manual";
@@ -407,6 +409,7 @@ const ChatInput: React.FC<ChatInputProps> = memo(
407
409
  <div className="relative shrink-0 min-h-[80px] flex flex-col border-t">
408
410
  <div className={cn("px-2 py-3 flex-1", inputClassName)}>
409
411
  <PromptInput
412
+ className="max-h-[400px]"
410
413
  inputRef={inputRef}
411
414
  value={input}
412
415
  onChange={setInput}
@@ -481,6 +484,7 @@ const ChatPanel = () => {
481
484
  const ChatPanelBody = () => {
482
485
  const setChatState = useSetAtom(chatStateAtom);
483
486
  const [activeChat, setActiveChat] = useAtom(activeChatAtom);
487
+ const [pendingPrompt, setPendingPrompt] = useAtom(pendingAiPromptAtom);
484
488
  const [input, setInput] = useState("");
485
489
  const [newThreadInput, setNewThreadInput] = useState("");
486
490
  const { files, addFiles, clearFiles, removeFile } = useFileState();
@@ -600,10 +604,7 @@ const ChatPanelBody = () => {
600
604
  requestAnimationFrame(scrollToBottom);
601
605
  }, [activeChatId]);
602
606
 
603
- const createNewThread = async (
604
- initialMessage: string,
605
- initialAttachments?: File[],
606
- ) => {
607
+ const startNewChatState = useEvent((initialMessage: string) => {
607
608
  const now = Date.now();
608
609
  const newChat: Chat = {
609
610
  id: chatId as ChatId,
@@ -613,41 +614,41 @@ const ChatPanelBody = () => {
613
614
  updatedAt: now,
614
615
  };
615
616
 
616
- // Create new chat and set as active
617
617
  setChatState((prev) => {
618
618
  const newChats = new Map(prev.chats);
619
619
  newChats.set(newChat.id, newChat);
620
- const newState = {
620
+ return {
621
621
  ...prev,
622
622
  chats: newChats,
623
623
  activeChatId: newChat.id,
624
624
  };
625
- return newState;
626
625
  });
626
+ });
627
627
 
628
- const fileParts =
629
- initialAttachments && initialAttachments.length > 0
630
- ? await convertToFileUIPart(initialAttachments)
631
- : undefined;
632
- const { contextPart, attachments } =
633
- await resolveChatContext(initialMessage);
628
+ const createNewThread = useEvent(
629
+ async (initialMessage: string, initialAttachments?: File[]) => {
630
+ startNewChatState(initialMessage);
634
631
 
635
- // Trigger AI conversation with append
636
- sendMessage({
637
- role: "user",
638
- parts: [
639
- {
640
- type: "text" as const,
641
- text: initialMessage,
642
- },
643
- ...(contextPart ? [contextPart] : []),
644
- ...(fileParts ?? []),
645
- ...attachments,
646
- ],
647
- });
648
- clearFiles();
649
- setInput("");
650
- };
632
+ const fileParts =
633
+ initialAttachments && initialAttachments.length > 0
634
+ ? await convertToFileUIPart(initialAttachments)
635
+ : undefined;
636
+ const { contextPart, attachments } =
637
+ await resolveChatContext(initialMessage);
638
+
639
+ sendMessage({
640
+ role: "user",
641
+ parts: [
642
+ { type: "text" as const, text: initialMessage },
643
+ ...(contextPart ? [contextPart] : []),
644
+ ...(fileParts ?? []),
645
+ ...attachments,
646
+ ],
647
+ });
648
+ clearFiles();
649
+ setInput("");
650
+ },
651
+ );
651
652
 
652
653
  const handleNewChat = useEvent(() => {
653
654
  setActiveChat(null);
@@ -728,7 +729,45 @@ const ChatPanelBody = () => {
728
729
 
729
730
  const handleOnCloseThread = () => newThreadInputRef.current?.editor?.blur();
730
731
 
732
+ const submitPendingPrompt = useEvent(async (prompt: string) => {
733
+ if (activeChatId == null) {
734
+ startNewChatState(prompt);
735
+ // Starting a chat swaps the new-thread input for the regular input;
736
+ // carry over any draft the user had typed so it isn't lost.
737
+ setInput(newThreadInput);
738
+ }
739
+ const { contextPart, attachments } = await resolveChatContext(prompt);
740
+ sendMessage({
741
+ role: "user",
742
+ parts: [
743
+ { type: "text", text: prompt },
744
+ ...(contextPart ? [contextPart] : []),
745
+ ...attachments,
746
+ ],
747
+ });
748
+ });
749
+
731
750
  const isNewThread = messages.length === 0;
751
+
752
+ // Deliver a prompt queued elsewhere (e.g. error auto-fix) to the chat,
753
+ // appending to the active thread or starting one if none exists.
754
+ useEffect(() => {
755
+ if (!pendingPrompt) {
756
+ return;
757
+ }
758
+ setPendingPrompt(null);
759
+ const { prompt, submit } = pendingPrompt;
760
+ if (submit) {
761
+ void submitPendingPrompt(prompt);
762
+ } else if (isNewThread) {
763
+ setNewThreadInput(prompt);
764
+ focusInputAndMoveToEnd(newThreadInputRef);
765
+ } else {
766
+ setInput(prompt);
767
+ focusInputAndMoveToEnd(newMessageInputRef);
768
+ }
769
+ }, [pendingPrompt, setPendingPrompt, isNewThread, submitPendingPrompt]);
770
+
732
771
  const chatInput = isNewThread ? (
733
772
  <ChatInput
734
773
  key="new-thread-input"
@@ -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
 
@@ -0,0 +1,22 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { getSnippetDisplay } from "../snippet-display";
5
+
6
+ describe("getSnippetDisplay", () => {
7
+ it("shows sql cells as the sql query", () => {
8
+ const { language, value } = getSnippetDisplay(
9
+ 'df = mo.sql("""SELECT * FROM users LIMIT 5""")',
10
+ );
11
+ expect(language).toBe("sql");
12
+ expect(value).toBe("SELECT * FROM users LIMIT 5");
13
+ });
14
+
15
+ it("keeps plain python cells as python", () => {
16
+ const code = "x = 1 + 2\nprint(x)";
17
+ expect(getSnippetDisplay(code)).toEqual({
18
+ language: "python",
19
+ value: code,
20
+ });
21
+ });
22
+ });
@@ -0,0 +1,27 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { SQLParser } from "@marimo-team/smart-cells";
4
+
5
+ export type SnippetLanguage = "python" | "sql";
6
+
7
+ export interface SnippetDisplay {
8
+ language: SnippetLanguage;
9
+ value: string;
10
+ }
11
+
12
+ const sqlParser = new SQLParser();
13
+
14
+ /**
15
+ * Decide how a snippet's code should be shown in the panel.
16
+ *
17
+ * SQL cells are stored as python `mo.sql(...)`. Unwrap the inner query and
18
+ * highlight it as SQL, matching how the cell renders once the snippet is
19
+ * inserted. Everything else stays python.
20
+ */
21
+ export function getSnippetDisplay(code: string): SnippetDisplay {
22
+ if (sqlParser.isSupported(code)) {
23
+ const { code: query } = sqlParser.transformIn(code);
24
+ return { language: "sql", value: query };
25
+ }
26
+ return { language: "python", value: code };
27
+ }
@@ -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 { getSnippetDisplay } from "./snippet-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, value } = getSnippetDisplay(code);
192
+
190
193
  return (
191
194
  <div
192
195
  className="relative hover-actions-parent pr-2"
@@ -210,10 +213,10 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
210
213
  <LazyAnyLanguageCodeMirror
211
214
  key={`${snippet.title}-${id}`}
212
215
  theme={theme === "dark" ? "dark" : "light"}
213
- language="python"
216
+ language={language}
214
217
  className="cm border rounded overflow-hidden"
215
218
  extensions={extensions}
216
- value={code}
219
+ value={value}
217
220
  readOnly={true}
218
221
  />
219
222
  </Suspense>
@@ -0,0 +1,36 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const getFeatureFlag = vi.fn<(flag: string) => boolean>();
6
+
7
+ vi.mock("@/core/config/feature-flag", () => ({
8
+ getFeatureFlag: (flag: string) => getFeatureFlag(flag),
9
+ }));
10
+
11
+ const { resolveAiPanelTab } = await import("../useOpenAiAssistant");
12
+
13
+ describe("resolveAiPanelTab", () => {
14
+ beforeEach(() => {
15
+ getFeatureFlag.mockReset();
16
+ });
17
+
18
+ it("honors an explicit panel regardless of the feature flag", () => {
19
+ getFeatureFlag.mockReturnValue(false);
20
+ expect(resolveAiPanelTab("agents", "chat")).toBe("agents");
21
+ expect(resolveAiPanelTab("chat", "agents")).toBe("chat");
22
+ });
23
+
24
+ it("uses the stored tab when external agents are enabled", () => {
25
+ getFeatureFlag.mockReturnValue(true);
26
+ expect(resolveAiPanelTab(undefined, "agents")).toBe("agents");
27
+ expect(resolveAiPanelTab(undefined, "chat")).toBe("chat");
28
+ });
29
+
30
+ it("falls back to chat when external agents are disabled", () => {
31
+ getFeatureFlag.mockReturnValue(false);
32
+ // A stale "agents" preference must not strand the prompt.
33
+ expect(resolveAiPanelTab(undefined, "agents")).toBe("chat");
34
+ expect(resolveAiPanelTab(undefined, "chat")).toBe("chat");
35
+ });
36
+ });
@@ -1,47 +1,17 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useAtomValue } from "jotai";
4
- import { AlertCircleIcon } from "lucide-react";
5
3
  import type React from "react";
6
- import { Spinner } from "@/components/icons/spinner";
7
- import { Tooltip } from "@/components/ui/tooltip";
8
- import { wasmInitializationAtom, wasmInitStatusAtom } from "@/core/wasm/state";
4
+ import { RuntimeStatusBadge } from "@/components/lifecycle/RuntimeStatusBadge";
9
5
  import { isWasm } from "@/core/wasm/utils";
10
6
 
11
7
  /**
12
- * Footer indicator that surfaces Pyodide initialization progress. Mirrors
13
- * the "Kernel" indicator but tracks the WASM runtime instead of the server
14
- * connection. Hides itself once Pyodide is ready.
8
+ * Footer indicator that surfaces Pyodide initialization progress. Thin
9
+ * wrapper around `<RuntimeStatusBadge>` so the rest of the footer chrome
10
+ * doesn't need to know about adapters.
15
11
  */
16
12
  export const PyodideStatus: React.FC = () => {
17
- const status = useAtomValue(wasmInitStatusAtom);
18
- const message = useAtomValue(wasmInitializationAtom);
19
-
20
- if (!isWasm() || status === "ready") {
13
+ if (!isWasm()) {
21
14
  return null;
22
15
  }
23
-
24
- const icon =
25
- status === "error" ? (
26
- <AlertCircleIcon className="w-4 h-4 text-destructive" />
27
- ) : (
28
- <Spinner size="small" />
29
- );
30
-
31
- const tooltip = status === "error" ? "Pyodide failed to initialize" : message;
32
-
33
- return (
34
- <Tooltip
35
- content={<div className="text-sm whitespace-pre-line">{tooltip}</div>}
36
- data-testid="footer-pyodide-status"
37
- >
38
- <div
39
- className="p-1 hover:bg-accent rounded flex items-center gap-1.5 text-xs text-muted-foreground"
40
- data-testid="pyodide-status"
41
- >
42
- {icon}
43
- <span>Pyodide</span>
44
- </div>
45
- </Tooltip>
46
- );
16
+ return <RuntimeStatusBadge />;
47
17
  };
@@ -6,7 +6,9 @@ import { jotaiJsonStorage } from "@/utils/storage/jotai";
6
6
 
7
7
  const AI_PANEL_TAB_KEY = "marimo:chrome:ai-panel-tab";
8
8
 
9
- const aiPanelTabAtom = atomWithStorage<"chat" | "agents">(
9
+ export type AiPanelTab = "chat" | "agents";
10
+
11
+ export const aiPanelTabAtom = atomWithStorage<AiPanelTab>(
10
12
  AI_PANEL_TAB_KEY,
11
13
  "chat",
12
14
  jotaiJsonStorage,