@marimo-team/islands 0.25.3-dev250 → 0.25.3-dev252

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 (147) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BBihxHrr.js → ConnectedDataExplorerComponent-DGOAQFtf.js} +11 -11
  2. package/dist/{ErrorBoundary-Ks8L_DFj.js → ErrorBoundary-By2DNeOL.js} +7 -7
  3. package/dist/{abnfDiagram-VCTEODGH-DBcJVxYP.js → abnfDiagram-VCTEODGH-DzrZZXRD.js} +2 -2
  4. package/dist/{any-language-editor-jGaR0EAo.js → any-language-editor-Be7dA2VK.js} +3 -3
  5. package/dist/{architectureDiagram-5GKGNRK7-BcHZXF4S.js → architectureDiagram-5GKGNRK7-iF_snyFL.js} +1 -1
  6. package/dist/{button-B6RjDKJx.js → button-0-RqY1DW.js} +2 -1
  7. package/dist/{capabilities-CkVzWJEX.js → capabilities-B6yy_CBN.js} +1 -1
  8. package/dist/{chat-ui-DgaVxFrc.js → chat-ui-zbfp80GW.js} +1547 -1482
  9. package/dist/{check-Z_vtI3OL.js → check-CSvxZk5I.js} +1 -1
  10. package/dist/{chunk-IMKFNOWR-CTZlwBAA.js → chunk-IMKFNOWR-B7xytD4B.js} +1 -1
  11. package/dist/{chunk-SHT3W25Y-53g_RZHm.js → chunk-SHT3W25Y-I8ZFP5L1.js} +1 -1
  12. package/dist/{chunk-SVP7TREG-P6abNGV_.js → chunk-SVP7TREG-Di-uAPdT.js} +1 -1
  13. package/dist/{chunk-TICWLB2K-BTJJqkyF.js → chunk-TICWLB2K-BFpUy0ud.js} +1 -1
  14. package/dist/{classDiagram-ZZMXUADV-BUGqOrTm.js → classDiagram-ZZMXUADV-Cqm4JGpx.js} +2 -2
  15. package/dist/{classDiagram-v2-VYDZK3BY-CXpQvO-s.js → classDiagram-v2-VYDZK3BY-DFHRJpN3.js} +2 -2
  16. package/dist/{code-visibility-CCCqFZYh.js → code-visibility-Byb3BN4h.js} +4 -4
  17. package/dist/{common-JXDK9IjO.js → common-CgWyYqpC.js} +262 -262
  18. package/dist/{config-DbjzjwxX.js → config-mwEjAmvP.js} +2 -2
  19. package/dist/{copy-icon-CG57cxwb.js → copy-icon-DD8bdw4L.js} +3 -3
  20. package/dist/{createReducer-DePYJ0F3.js → createReducer-Bx81cw8_.js} +1 -1
  21. package/dist/{cynefinDiagram-5FMLGOSQ-DOcnl8wW.js → cynefinDiagram-5FMLGOSQ-cuGOeTBC.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-CuHR9QfG.js → dagre-GXQ25YYZ-DJ76aC4k.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-QAJSeJpH.js → data-grid-overlay-editor-BP_fYnBa.js} +1 -1
  24. package/dist/{diagram-S7CK7UJ4-BPiG4kso.js → diagram-S7CK7UJ4-DLLc0sw7.js} +1 -1
  25. package/dist/{diagram-UQ7AKVKN-_piBOvOC.js → diagram-UQ7AKVKN-Tz_d6qmk.js} +1 -1
  26. package/dist/{diagram-VX7I27RA-DCzVeJ8M.js → diagram-VX7I27RA-D7SzPrDN.js} +1 -1
  27. package/dist/{diagram-Z3DM3KII-D64CF1jQ.js → diagram-Z3DM3KII-qr37XV58.js} +1 -1
  28. package/dist/{dialog-DsZ49mmU.js → dialog-BHYPX87J.js} +3 -3
  29. package/dist/{dropdown-menu-BRiE7Vnp.js → dropdown-menu-Blu7V6Mk.js} +6 -6
  30. package/dist/{ebnfDiagram-PWID7BFC-BapQPML-.js → ebnfDiagram-PWID7BFC-BKOQb4J0.js} +2 -2
  31. package/dist/{erDiagram-RLTQ6QDP-CNQvC2_Q.js → erDiagram-RLTQ6QDP-CM15Vq68.js} +1 -1
  32. package/dist/{error-banner-IsonnvA0.js → error-banner-DbRFOd2O.js} +2 -2
  33. package/dist/{error-entries-CRPfxMqm.js → error-entries-BuIE3Ius.js} +9 -9
  34. package/dist/{feedback-button-Bg7zGcid.js → feedback-button-DPWFUVWl.js} +7 -7
  35. package/dist/{flowDiagram-HODETNUW-BBt6S6oe.js → flowDiagram-HODETNUW-Dt3N5yFT.js} +2 -2
  36. package/dist/{formats-BK4VefeF.js → formats-gGospKWJ.js} +2 -2
  37. package/dist/{glide-data-editor-BuFe7QfV.js → glide-data-editor-GvhMU_kg.js} +10 -10
  38. package/dist/{glide-utils-CQddGpa-.js → glide-utils-qbkRZpEy.js} +3 -3
  39. package/dist/{globals-8EK96XwF.js → globals-RlhgvYCL.js} +2 -2
  40. package/dist/{html-to-image-Gx4q3ZoD.js → html-to-image-BXrAd5q8.js} +460 -455
  41. package/dist/{infoDiagram-27XIBGKW-Hd8Ifz82.js → infoDiagram-27XIBGKW-C7axJMCl.js} +1 -1
  42. package/dist/{ishikawaDiagram-5VMMS53U-CP7hnHWx.js → ishikawaDiagram-5VMMS53U-CdC9Eur7.js} +1 -1
  43. package/dist/{kanban-definition-UXKFOSKX-CX91sa9b.js → kanban-definition-UXKFOSKX-BHnMIgD7.js} +1 -1
  44. package/dist/{loader-BisjOLiW.js → loader-BVEtQ4F3.js} +1 -1
  45. package/dist/main.js +236 -236
  46. package/dist/{mermaid-Djb3Ni9t.js → mermaid-DFYN_PXd.js} +35 -35
  47. package/dist/{mindmap-definition-YA3MSWOX-DgCNo07O.js → mindmap-definition-YA3MSWOX-BagHoCGU.js} +1 -1
  48. package/dist/{number-overlay-editor-BDJF1c8h.js → number-overlay-editor-Dg1EfcEs.js} +1 -1
  49. package/dist/{pegDiagram-XKGWAZYB-SGJhnIKo.js → pegDiagram-XKGWAZYB-8IIZczip.js} +2 -2
  50. package/dist/{pieDiagram-E7YTZNPT-1stXIWCQ.js → pieDiagram-E7YTZNPT-CE6dKBIr.js} +1 -1
  51. package/dist/{process-output-D2ISIyIs.js → process-output-D2ZjxrHe.js} +4 -4
  52. package/dist/{railroadDiagram-O6MQD6OU-DY1mojzB.js → railroadDiagram-O6MQD6OU-CkexPq3b.js} +2 -2
  53. package/dist/{requirementDiagram-BXWQKSXE-gHzhUp0D.js → requirementDiagram-BXWQKSXE-B0-OkuAe.js} +1 -1
  54. package/dist/{reveal-component-B_hNJfj-.js → reveal-component-UPs4Rd1M.js} +8 -8
  55. package/dist/{select-BSxlITBo.js → select-CcidPqlM.js} +5 -5
  56. package/dist/{sequenceDiagram-WJ2MYXX4-rjPtD9fB.js → sequenceDiagram-WJ2MYXX4-Dxysg8DX.js} +1 -1
  57. package/dist/{spec-Dq8RNplp.js → spec-ZwfZcgsm.js} +4 -4
  58. package/dist/{stateDiagram-D77RDMKH-DMNxjUxV.js → stateDiagram-D77RDMKH-DTEN3-OJ.js} +2 -2
  59. package/dist/{stateDiagram-v2-MP3YSRHH-CumAaqGo.js → stateDiagram-v2-MP3YSRHH-vktk3Xc5.js} +2 -2
  60. package/dist/{strings-BGW-fXVJ.js → strings-BYJNyKmg.js} +3 -3
  61. package/dist/style.css +1 -1
  62. package/dist/{swimlanes-42K2YHIH-BU14_Tej.js → swimlanes-42K2YHIH-Cjveh9OM.js} +1 -1
  63. package/dist/{swimlanesDiagram-VR7AAH4N-CieAnPqJ.js → swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js} +2 -2
  64. package/dist/{swiper-component-Dir1o8ml.js → swiper-component-Dvj-goaD.js} +2 -2
  65. package/dist/{timeline-definition-24CTP7MA-CrVj4E1K.js → timeline-definition-24CTP7MA-KIuDcefn.js} +1 -1
  66. package/dist/{toast-BWIPi3aF.js → toast-CkB5Jq9K.js} +4 -4
  67. package/dist/{tooltip-C3UnvsRe.js → tooltip-Bzl4k00l.js} +2 -2
  68. package/dist/{use-restore-focus-BWO0z1lo.js → use-restore-focus-CmflVLWJ.js} +1 -1
  69. package/dist/{useDateFormatter-ChB4eyim.js → useDateFormatter-CYo5fm8s.js} +2 -2
  70. package/dist/{useIframeCapabilities-cMNQ80CK.js → useIframeCapabilities-BIolGsF1.js} +1 -1
  71. package/dist/{useLifecycle-BquH9ET-.js → useLifecycle-GNgvNwI-.js} +2 -2
  72. package/dist/{useTheme-DgOLVISn.js → useTheme-BZ2GGLgm.js} +141 -139
  73. package/dist/{vega-component-Bwg_Kuh-.js → vega-component-DOtem7Hf.js} +7 -7
  74. package/dist/{vennDiagram-4TSXK5OY-ZLxQ8yhm.js → vennDiagram-4TSXK5OY-DrIOUHfb.js} +1 -1
  75. package/dist/{wardleyDiagram-VM6X3IG4-D88pcLY1.js → wardleyDiagram-VM6X3IG4-CJNjf4OH.js} +1 -1
  76. package/dist/{xychartDiagram-S5SC5T6Z-_13d4g8g.js → xychartDiagram-S5SC5T6Z-DEshFho-.js} +1 -1
  77. package/package.json +1 -1
  78. package/src/__tests__/fixtures/pair-config.ts +1 -1
  79. package/src/components/app-config/__tests__/state.test.tsx +50 -0
  80. package/src/components/app-config/ai-config.tsx +1 -1
  81. package/src/components/chat/__tests__/capabilities-popover.test.tsx +0 -1
  82. package/src/components/chat/__tests__/chat-display.test.tsx +125 -0
  83. package/src/components/chat/__tests__/chat-markdown.test.tsx +43 -0
  84. package/src/components/chat/__tests__/chat-utils.test.ts +51 -1
  85. package/src/components/chat/acp/__tests__/agent-panel-initialization.test.tsx +1 -1
  86. package/src/components/chat/acp/__tests__/agent-panel.test.tsx +22 -17
  87. package/src/components/chat/acp/agent-panel.tsx +4 -3
  88. package/src/components/chat/acp/blocks.tsx +10 -10
  89. package/src/components/chat/acp/common.tsx +4 -5
  90. package/src/components/chat/acp/model-selector.tsx +1 -1
  91. package/src/components/chat/capabilities-popover.tsx +1 -6
  92. package/src/components/chat/chat-components.tsx +7 -4
  93. package/src/components/chat/chat-display.tsx +51 -6
  94. package/src/components/chat/chat-history-popover.tsx +6 -1
  95. package/src/components/chat/chat-markdown.tsx +15 -0
  96. package/src/components/chat/chat-panel.tsx +105 -119
  97. package/src/components/chat/chat-typography.css +108 -0
  98. package/src/components/chat/chat-utils.ts +8 -3
  99. package/src/components/chat/notebook-access-selector.tsx +1 -1
  100. package/src/components/chat/reasoning-accordion.tsx +3 -3
  101. package/src/components/chat/tool-call/tool-activity-group.tsx +49 -0
  102. package/src/components/chat/tool-call/tool-history-row.tsx +3 -30
  103. package/src/components/chat/tool-call/tool-result.tsx +1 -1
  104. package/src/components/chat/use-chat-generation.ts +19 -0
  105. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +18 -37
  106. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +5 -8
  107. package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +5 -19
  108. package/src/components/editor/actions/pair-with-agent-commands.ts +0 -1
  109. package/src/components/editor/actions/pair-with-agent-modal.tsx +7 -11
  110. package/src/components/editor/actions/pair-with-agent.tsx +3 -31
  111. package/src/components/editor/actions/useCellActionButton.tsx +1 -1
  112. package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
  113. package/src/components/editor/ai/prompt-input.tsx +189 -0
  114. package/src/components/editor/cell/StagedAICell.tsx +3 -3
  115. package/src/components/editor/cell/code/cell-editor.tsx +14 -1
  116. package/src/components/editor/chrome/panels/panel-header.tsx +13 -0
  117. package/src/components/editor/chrome/types.ts +1 -1
  118. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +53 -45
  119. package/src/components/editor/chrome/wrapper/__tests__/useAskAiAboutCell.test.tsx +58 -0
  120. package/src/components/editor/chrome/wrapper/app-chrome.tsx +89 -72
  121. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +5 -8
  122. package/src/components/editor/chrome/wrapper/useAskAiAboutCell.ts +36 -0
  123. package/src/components/editor/renderers/__tests__/add-cell-buttons.test.tsx +18 -0
  124. package/src/components/editor/renderers/cell-array.tsx +10 -67
  125. package/src/components/markdown/markdown-renderer.tsx +35 -20
  126. package/src/components/mcp/mcp-status-indicator.tsx +18 -14
  127. package/src/core/ai/__tests__/staged-cells.test.ts +1 -467
  128. package/src/core/ai/context/context.ts +2 -0
  129. package/src/core/ai/context/providers/__tests__/cell.test.ts +49 -0
  130. package/src/core/ai/context/providers/cell.ts +54 -0
  131. package/src/core/ai/staged-cells.ts +4 -280
  132. package/src/core/ai/state.ts +3 -0
  133. package/src/core/codemirror/__tests__/setup.test.ts +32 -0
  134. package/src/core/codemirror/cells/state.ts +1 -0
  135. package/src/core/codemirror/cm.ts +5 -1
  136. package/src/core/codemirror/config/types.ts +1 -1
  137. package/src/core/codemirror/language/languages/python.ts +3 -3
  138. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +16 -0
  139. package/src/core/codemirror/placeholder/extensions.ts +57 -19
  140. package/src/core/hotkeys/hotkeys.ts +2 -1
  141. package/src/core/notebook/document/__tests__/codemirror.test.ts +1 -0
  142. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +1 -1
  143. package/src/plugins/impl/chat/chat-ui.tsx +1 -1
  144. package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +0 -106
  145. package/src/components/editor/ai/add-cell-with-ai.tsx +0 -506
  146. package/src/components/editor/ai/staged-cell-submission.ts +0 -46
  147. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +0 -66
@@ -0,0 +1,189 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ autocompletion,
5
+ type Completion,
6
+ type CompletionContext,
7
+ type CompletionSource,
8
+ } from "@codemirror/autocomplete";
9
+ import { markdown } from "@codemirror/lang-markdown";
10
+ import { Prec } from "@codemirror/state";
11
+ import { promptHistory } from "@marimo-team/codemirror-ai";
12
+ import ReactCodeMirror, {
13
+ EditorView,
14
+ keymap,
15
+ minimalSetup,
16
+ type ReactCodeMirrorRef,
17
+ } from "@uiw/react-codemirror";
18
+ import { useStore } from "jotai";
19
+ import { useMemo } from "react";
20
+ import useEvent from "react-use-event-hook";
21
+ import { z } from "zod";
22
+ import { clickablePlaceholderExtension } from "@/core/codemirror/placeholder/extensions";
23
+ import { resourceExtension } from "@/core/codemirror/ai/resources";
24
+ import { useTheme } from "@/theme/useTheme";
25
+ import { cn } from "@/utils/cn";
26
+ import { ZodLocalStorage } from "@/utils/storage/typed";
27
+ import { CONTEXT_TRIGGER, mentionsCompletionSource } from "./completion-utils";
28
+
29
+ const KEY = "marimo:ai-prompt-history";
30
+ const promptHistoryStorage = new ZodLocalStorage(z.array(z.string()), () => []);
31
+
32
+ export interface AdditionalCompletions {
33
+ triggerCompletionRegex: RegExp;
34
+ completions: Completion[];
35
+ }
36
+
37
+ interface PromptInputProps {
38
+ inputRef?: React.RefObject<ReactCodeMirrorRef | null>;
39
+ placeholder?: string;
40
+ placeholderAction?: Parameters<typeof clickablePlaceholderExtension>[0];
41
+ value: string;
42
+ className?: string;
43
+ onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
44
+ onClose: () => void;
45
+ onChange: (value: string) => void;
46
+ onSubmit: (e: KeyboardEvent | undefined, value: string) => void;
47
+ additionalCompletions?: AdditionalCompletions;
48
+ maxHeight?: string;
49
+ onAddFiles?: (files: File[]) => void;
50
+ }
51
+
52
+ /**
53
+ * CodeMirror-based input for the AI prompt.
54
+ *
55
+ * This is just text (no language support), but we use codemirror to get autocomplete
56
+ * for @dataframe and @sql_table.
57
+ */
58
+ export const PromptInput = ({
59
+ value,
60
+ placeholder,
61
+ placeholderAction,
62
+ inputRef,
63
+ className,
64
+ onChange,
65
+ onSubmit,
66
+ onKeyDown,
67
+ onClose,
68
+ onAddFiles,
69
+ additionalCompletions,
70
+ maxHeight,
71
+ }: PromptInputProps) => {
72
+ const handleSubmit = useEvent(onSubmit);
73
+ const handleEscape = useEvent(onClose);
74
+ const store = useStore();
75
+ const { theme } = useTheme();
76
+
77
+ const additionalCompletionsSource: CompletionSource = useEvent(
78
+ (context: CompletionContext) => {
79
+ if (!additionalCompletions) {
80
+ return null;
81
+ }
82
+
83
+ return mentionsCompletionSource(
84
+ [additionalCompletions.triggerCompletionRegex],
85
+ additionalCompletions.completions,
86
+ )(context);
87
+ },
88
+ );
89
+
90
+ // Changing extensions can be expensive, so
91
+ // it is worth making sure this is memoized well.
92
+ const extensions = useMemo(() => {
93
+ const markdownLanguage = markdown();
94
+ return [
95
+ ...(placeholderAction
96
+ ? clickablePlaceholderExtension(placeholderAction)
97
+ : []),
98
+ autocompletion({}),
99
+ markdownLanguage,
100
+ resourceExtension({
101
+ language: markdownLanguage.language,
102
+ store,
103
+ onAddFiles,
104
+ }),
105
+ markdownLanguage.language.data.of({
106
+ autocomplete: additionalCompletionsSource,
107
+ }),
108
+ promptHistory({
109
+ storage: {
110
+ load: () => promptHistoryStorage.get(KEY),
111
+ save: (prompts) => promptHistoryStorage.set(KEY, prompts),
112
+ },
113
+ }),
114
+ EditorView.lineWrapping,
115
+ minimalSetup(),
116
+ Prec.highest(
117
+ keymap.of([
118
+ {
119
+ preventDefault: true,
120
+ stopPropagation: true,
121
+ any: (view, event) => {
122
+ const pressedModOrShift =
123
+ event.metaKey || event.ctrlKey || event.shiftKey;
124
+ // If no mod key is pressed, submit
125
+ if (event.key === "Enter" && !pressedModOrShift) {
126
+ handleSubmit(event, view.state.doc.toString());
127
+ event.preventDefault();
128
+ event.stopPropagation();
129
+ return true;
130
+ }
131
+ // Mod+Enter does add a new line already by codemirror
132
+ // But Shift+Enter does not, so we need to handle it manually
133
+ if (event.key === "Enter" && event.shiftKey) {
134
+ view.dispatch(view.state.replaceSelection("\n"));
135
+ event.preventDefault();
136
+ event.stopPropagation();
137
+ return true;
138
+ }
139
+
140
+ return false;
141
+ },
142
+ },
143
+ ]),
144
+ ),
145
+ keymap.of([
146
+ {
147
+ key: "Escape",
148
+ preventDefault: true,
149
+ stopPropagation: true,
150
+ run: () => {
151
+ handleEscape();
152
+ return true;
153
+ },
154
+ },
155
+ ]),
156
+ ];
157
+ }, [
158
+ store,
159
+ placeholderAction,
160
+ onAddFiles,
161
+ additionalCompletionsSource,
162
+ handleSubmit,
163
+ handleEscape,
164
+ ]);
165
+
166
+ return (
167
+ <ReactCodeMirror
168
+ ref={inputRef}
169
+ className={cn(
170
+ "mo-ai-prompt flex-1 font-sans text-sm leading-6 overflow-auto my-1",
171
+ className,
172
+ )}
173
+ width="100%"
174
+ maxHeight={maxHeight}
175
+ value={value}
176
+ basicSetup={false}
177
+ extensions={extensions}
178
+ onChange={onChange}
179
+ onKeyDown={onKeyDown}
180
+ theme={theme === "dark" ? "dark" : "light"}
181
+ placeholder={
182
+ placeholderAction
183
+ ? undefined
184
+ : placeholder ||
185
+ `Generate with AI, ${CONTEXT_TRIGGER} to include context`
186
+ }
187
+ />
188
+ );
189
+ };
@@ -5,10 +5,10 @@ import { SparklesIcon } from "lucide-react";
5
5
  import { Tooltip } from "@/components/ui/tooltip";
6
6
  import {
7
7
  type Edit,
8
- stagedGenerationInProgressAtom,
9
8
  useStagedAICell,
10
9
  useStagedCells,
11
10
  } from "@/core/ai/staged-cells";
11
+ import { chatGenerationInProgressAtom } from "@/core/ai/state";
12
12
  import { getCellEditorView } from "@/core/cells/cells";
13
13
  import type { CellId } from "@/core/cells/ids";
14
14
  import { prepareCellCodeUpdate } from "@/core/codemirror/language/cell-code";
@@ -49,8 +49,8 @@ export const StagedAICellFooter: React.FC<{ cellId: CellId }> = ({
49
49
  cellId,
50
50
  }) => {
51
51
  const store = useStore();
52
- const generationInProgress = useAtomValue(stagedGenerationInProgressAtom);
53
52
  const stagedAiCell = useStagedAICell(cellId);
53
+ const isGenerating = useAtomValue(chatGenerationInProgressAtom);
54
54
  const runCell = useRunCell(cellId);
55
55
 
56
56
  const { deleteStagedCell, removeStagedCell } = useStagedCells(store);
@@ -93,7 +93,7 @@ export const StagedAICellFooter: React.FC<{ cellId: CellId }> = ({
93
93
  </div>
94
94
  <div className="flex items-center gap-1.5">
95
95
  <CompletionActionsCellFooter
96
- isLoading={generationInProgress}
96
+ isLoading={isGenerating}
97
97
  onAccept={() => handleCompletion("accept")}
98
98
  onDecline={() => handleCompletion("reject")}
99
99
  size="xs"
@@ -31,7 +31,11 @@ import {
31
31
  } from "@/core/codemirror/language/extension";
32
32
  import { MARKDOWN_INITIAL_HIDE_CODE } from "@/core/codemirror/language/languages/markdown";
33
33
  import type { LanguageAdapterType } from "@/core/codemirror/language/types";
34
- import { autoInstantiateAtom, isAiFeatureEnabled } from "@/core/config/config";
34
+ import {
35
+ autoInstantiateAtom,
36
+ isAiEnabled,
37
+ isAiFeatureEnabled,
38
+ } from "@/core/config/config";
35
39
  import type { UserConfig } from "@/core/config/config-schema";
36
40
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
37
41
  import { useSaveNotebook } from "@/core/saving/save-component";
@@ -39,6 +43,7 @@ import type { Theme } from "@/theme/useTheme";
39
43
  import { cn } from "@/utils/cn";
40
44
  import { invariant } from "@/utils/invariant";
41
45
  import { mergeRefs } from "@/utils/mergeRefs";
46
+ import { useAskAiAboutCell } from "../../chrome/wrapper/useAskAiAboutCell";
42
47
  import { AiCompletionEditor } from "../../ai/ai-completion-editor";
43
48
  import {
44
49
  closeSignatureHelp,
@@ -189,12 +194,15 @@ const CellEditorInternal = ({
189
194
  });
190
195
 
191
196
  const aiFeaturesEnabled = isAiFeatureEnabled(userConfig);
197
+ const askAiAboutCell = useAskAiAboutCell();
198
+ const aiChatEnabled = isAiEnabled(userConfig);
192
199
 
193
200
  const extensions = useMemo(() => {
194
201
  const extensions = setupCodeMirror({
195
202
  cellId,
196
203
  showPlaceholder,
197
204
  enableAI: aiFeaturesEnabled,
205
+ enableAIChat: aiChatEnabled,
198
206
  cellActions: {
199
207
  ...cellActions,
200
208
  afterToggleMarkdown,
@@ -213,6 +221,9 @@ const CellEditorInternal = ({
213
221
  },
214
222
  splitCell,
215
223
  toggleHideCode,
224
+ askAiAboutCell: () => {
225
+ void askAiAboutCell(cellId);
226
+ },
216
227
  aiCellCompletion: () => {
217
228
  if (!aiFeaturesEnabled) {
218
229
  return false;
@@ -296,6 +307,8 @@ const CellEditorInternal = ({
296
307
  inlineAiTooltip,
297
308
  tooltipParentSelector,
298
309
  aiFeaturesEnabled,
310
+ aiChatEnabled,
311
+ askAiAboutCell,
299
312
  theme,
300
313
  showPlaceholder,
301
314
  cellActions,
@@ -0,0 +1,13 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { createContext, useContext, type PropsWithChildren } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ export const PanelHeaderActionsContext = createContext<HTMLElement | null>(
7
+ null,
8
+ );
9
+
10
+ export function PanelHeaderActions({ children }: PropsWithChildren) {
11
+ const target = useContext(PanelHeaderActionsContext);
12
+ return target ? createPortal(children, target) : children;
13
+ }
@@ -97,7 +97,7 @@ export const PANELS: PanelDescriptor[] = [
97
97
  type: "ai",
98
98
  Icon: BotIcon,
99
99
  label: "AI",
100
- tooltip: "Chat & Agents",
100
+ tooltip: "Chat",
101
101
  defaultSection: "sidebar",
102
102
  additionalKeywords: ["chat", "copilot", "assistant"],
103
103
  },
@@ -1,12 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { act, fireEvent, render, screen, within } from "@testing-library/react";
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
4
  import { createStore, Provider } from "jotai";
5
5
  import { describe, expect, it, vi } from "vitest";
6
+ import type { ChatStatus } from "ai";
7
+ import { useChatGeneration } from "@/components/chat/use-chat-generation";
6
8
  import { cellId } from "@/__tests__/branded";
7
9
  import { TooltipProvider } from "@/components/ui/tooltip";
8
- import { stagedAICellsAtom, visibleForTesting } from "@/core/ai/staged-cells";
10
+ import { stagedAICellsAtom } from "@/core/ai/staged-cells";
9
11
  import { scrollAndHighlightCell } from "../../../links/cell-link";
12
+ import { StagedAICellFooter } from "../../../cell/StagedAICell";
10
13
  import { PendingAICells } from "../pending-ai-cells";
11
14
 
12
15
  vi.mock("@/components/editor/cell/useDeleteCell", () => ({
@@ -25,6 +28,7 @@ vi.mock("@/core/cells/actions", () => ({
25
28
 
26
29
  vi.mock("@/components/editor/cell/useRunCells", () => ({
27
30
  useRunCells: () => vi.fn(),
31
+ useRunCell: () => vi.fn(),
28
32
  }));
29
33
 
30
34
  vi.mock("@/components/editor/links/cell-link", () => ({
@@ -87,50 +91,54 @@ describe("PendingAICells", () => {
87
91
  expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(third, "focus");
88
92
  expect(screen.getByText("3 / 3")).toBeInTheDocument();
89
93
  });
94
+ });
90
95
 
91
- it("disables bulk actions while cells are still generating", () => {
92
- const store = createStore();
93
- const generationId = Symbol("staged-cell-generation");
94
- store.set(
95
- stagedAICellsAtom,
96
- new Map([[cellId("generated-cell"), { type: "add_cell" }]]),
97
- );
98
- store.set(visibleForTesting.stagedGenerationAtom, {
99
- id: generationId,
100
- status: "in_progress",
101
- cellIds: [cellId("generated-cell")],
102
- });
103
-
104
- const view = (
105
- <Provider store={store}>
106
- <TooltipProvider>
107
- <PendingAICells />
108
- </TooltipProvider>
109
- </Provider>
110
- );
111
- const { rerender } = render(view);
112
-
113
- const acceptButton = screen.getByRole("button", { name: "Keep all" });
114
- const acceptButtons = within(
115
- acceptButton.parentElement as HTMLElement,
116
- ).getAllByRole("button");
117
- expect(acceptButtons).toHaveLength(2);
118
- for (const button of acceptButtons) {
119
- expect(button).toBeDisabled();
96
+ function ChatGeneration({ status }: { status: ChatStatus }) {
97
+ useChatGeneration(status);
98
+ return null;
99
+ }
100
+
101
+ it("locks staged edits during generation and unlocks after completion, stop, error, or unmount", () => {
102
+ const store = createStore();
103
+ const pendingCell = cellId("pending-cell");
104
+ store.set(stagedAICellsAtom, new Map([[pendingCell, { type: "add_cell" }]]));
105
+ const ui = (status?: ChatStatus) => (
106
+ <Provider store={store}>
107
+ <TooltipProvider>
108
+ {status && <ChatGeneration status={status} />}
109
+ <PendingAICells />
110
+ <StagedAICellFooter cellId={pendingCell} />
111
+ </TooltipProvider>
112
+ </Provider>
113
+ );
114
+ const { rerender } = render(ui("ready"));
115
+ const keep = screen.getByRole("button", { name: "Keep all" });
116
+ const discard = screen.getByRole("button", { name: "Discard all" });
117
+ const keepCell = screen.getByRole("button", { name: "Keep cell" });
118
+ const discardCell = screen.getByRole("button", { name: "Discard cell" });
119
+ const keepAndRun = keep.parentElement!.querySelectorAll("button")[1];
120
+ for (const status of [
121
+ "submitted",
122
+ "streaming",
123
+ "ready",
124
+ "submitted",
125
+ "ready",
126
+ "submitted",
127
+ "error",
128
+ "streaming",
129
+ undefined,
130
+ ] as const) {
131
+ rerender(ui(status));
132
+ const isGenerating = status === "submitted" || status === "streaming";
133
+ for (const button of [keep, discard, keepAndRun, keepCell, discardCell]) {
134
+ expect(button.hasAttribute("disabled")).toBe(isGenerating);
135
+ if (isGenerating) {
136
+ fireEvent.click(button);
137
+ }
120
138
  }
121
- expect(screen.getByRole("button", { name: "Discard all" })).toBeDisabled();
139
+ expect(store.get(stagedAICellsAtom).has(pendingCell)).toBe(true);
140
+ }
122
141
 
123
- act(() => {
124
- store.set(visibleForTesting.stagedGenerationAtom, {
125
- id: generationId,
126
- status: "complete",
127
- });
128
- });
129
- rerender(view);
130
-
131
- for (const button of acceptButtons) {
132
- expect(button).toBeEnabled();
133
- }
134
- expect(screen.getByRole("button", { name: "Discard all" })).toBeEnabled();
135
- });
142
+ fireEvent.click(keep);
143
+ expect(store.get(stagedAICellsAtom).size).toBe(0);
136
144
  });
@@ -0,0 +1,58 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import type { PropsWithChildren } from "react";
6
+ import { beforeEach, expect, it, vi } from "vitest";
7
+ import { pendingAiPromptAtom } from "@/core/ai/state";
8
+ import { CellId } from "@/core/cells/ids";
9
+ import { aiPanelTabAtom } from "../useAiPanel";
10
+ import { useAskAiAboutCell } from "../useAskAiAboutCell";
11
+
12
+ const { aiAtom, openApplication, saveModeChange } = await vi.hoisted(
13
+ async () => {
14
+ const { atom } = await import("jotai");
15
+ return {
16
+ aiAtom: atom<{ mode?: "ask" | "agent" | "code_mode" }>({}),
17
+ openApplication: vi.fn(),
18
+ saveModeChange: vi.fn().mockResolvedValue(undefined),
19
+ };
20
+ },
21
+ );
22
+ vi.mock("@/core/config/config", async (importOriginal) => ({
23
+ ...(await importOriginal<object>()),
24
+ aiAtom,
25
+ }));
26
+ vi.mock("@/core/ai/config", () => ({
27
+ useAIConfigActions: () => ({ saveModeChange }),
28
+ }));
29
+ vi.mock("../../state", () => ({
30
+ useChromeActions: () => ({ openApplication }),
31
+ }));
32
+
33
+ beforeEach(() => vi.clearAllMocks());
34
+ it.each([undefined, "ask", "agent", "code_mode"] as const)(
35
+ "opens chat from %s mode without submitting or replacing the draft",
36
+ async (mode) => {
37
+ const store = createStore();
38
+ store.set(aiAtom, { mode });
39
+ store.set(aiPanelTabAtom, "agents");
40
+ const cellId = CellId.create();
41
+ const { result } = renderHook(useAskAiAboutCell, {
42
+ wrapper: ({ children }: PropsWithChildren) => (
43
+ <Provider store={store}>{children}</Provider>
44
+ ),
45
+ });
46
+ await act(() => result.current(cellId));
47
+ expect(openApplication).toHaveBeenCalledWith("ai");
48
+ expect(store.get(aiPanelTabAtom)).toBe("chat");
49
+ expect(saveModeChange.mock.calls).toEqual(
50
+ mode === "agent" || mode === "code_mode" ? [] : [["agent"]],
51
+ );
52
+ expect(store.get(pendingAiPromptAtom)).toEqual({
53
+ prompt: `@cell://${cellId} `,
54
+ submit: false,
55
+ append: true,
56
+ });
57
+ },
58
+ );