@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev8

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 (33) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-DXBx_nQg.js} +4 -4
  2. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DYBcNEdd.js} +81 -81
  3. package/dist/{code-visibility-C90Am97q.js → code-visibility-DKavYkBl.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-CGp_08St.js → html-to-image-MqcD07Bw.js} +73 -72
  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-R6JsYrv3.js → process-output-Dt3icftd.js} +1 -1
  11. package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CuFatyYX.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/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  24. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  25. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  26. package/src/components/editor/errors/__tests__/auto-fix.test.ts +119 -0
  27. package/src/components/editor/errors/auto-fix.tsx +108 -34
  28. package/src/components/editor/errors/fix-mode.ts +1 -1
  29. package/src/components/editor/output/MarimoTracebackOutput.tsx +10 -1
  30. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  31. package/src/core/ai/config.ts +2 -2
  32. package/src/core/ai/state.ts +11 -0
  33. package/src/core/codemirror/utils.ts +15 -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-CGp_08St.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-CGp_08St.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-C90Am97q.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-DKavYkBl.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-dev6",
3
+ "version": "0.23.14-dev8",
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,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
+ });
@@ -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
+ }