@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
@@ -1,30 +1,19 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { atom, useAtomValue } from "jotai";
3
+ import { useAtomValue } from "jotai";
4
4
  import { selectAtom } from "jotai/utils";
5
- import { useMemo, useRef } from "react";
5
+ import { useMemo } from "react";
6
6
  import { useDeleteCellCallback } from "@/components/editor/cell/useDeleteCell";
7
7
  import { CellId } from "@/core/cells/ids";
8
8
  import { createReducerAndAtoms } from "@/utils/createReducer";
9
9
  import { Logger } from "@/utils/Logger";
10
- import {
11
- type CompletionDataPart,
12
- NOTEBOOK_CELLS_COMPLETION_DATA_TYPE,
13
- type GeneratedCell,
14
- notebookCellsCompletionSchema,
15
- } from "./completion-output";
16
- import { maybeAddMarimoImport } from "../cells/add-missing-import";
17
10
  import { getCellEditorView } from "../cells/cells";
18
- import { type CreateCell, useCellActions } from "../cells/actions";
11
+ import { useCellActions } from "../cells/actions";
19
12
  import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
20
13
  import type { JotaiStore } from "../state/jotai";
21
14
  import type { EditType } from "./tools/edit-notebook-tool";
22
15
 
23
- /**
24
- * Pending AI edits shared by completion, chat, and agent workflows.
25
- * Generation ownership keeps one workflow from accepting or discarding another's
26
- * staged cells.
27
- */
16
+ /** Pending AI edits shared by chat and agent workflows. */
28
17
 
29
18
  export type Edit =
30
19
  | { type: Extract<EditType, "update_cell">; previousCode: string }
@@ -33,25 +22,6 @@ export type Edit =
33
22
 
34
23
  export type StagedAICells = Map<CellId, Edit>;
35
24
 
36
- type StagedGeneration =
37
- | {
38
- id: symbol;
39
- status: "in_progress";
40
- cellIds: readonly CellId[];
41
- }
42
- | { id: symbol; status: "complete" };
43
-
44
- interface OwnedStagedGeneration {
45
- id: symbol;
46
- status: "in_progress" | "complete";
47
- reconciler: StagedCellReconciler;
48
- }
49
-
50
- const stagedGenerationAtom = atom<StagedGeneration | null>(null);
51
- export const stagedGenerationInProgressAtom = atom(
52
- (get) => get(stagedGenerationAtom)?.status === "in_progress",
53
- );
54
-
55
25
  const initialState = (): StagedAICells => {
56
26
  return new Map();
57
27
  };
@@ -143,255 +113,9 @@ export function useStagedCells(store: JotaiStore) {
143
113
  };
144
114
  }
145
115
 
146
- /** Owns the lifecycle of cells created by one structured completion flow. */
147
- export function useStagedCellGeneration(store: JotaiStore) {
148
- const stagedCellActions = useStagedCells(store);
149
- const { createCell } = useCellActions();
150
- const ownedGeneration = useRef<OwnedStagedGeneration | null>(null);
151
- // Ownership lives in a ref, so subscribe to its backing atoms to rerender
152
- // consumers when completion or per-cell acceptance changes.
153
- useAtomValue(stagedGenerationAtom, { store });
154
- useAtomValue(stagedAICellsAtom, { store });
155
-
156
- const discardStagedCellIds = (cellIds: readonly CellId[]) => {
157
- const stagedAICells = store.get(stagedAICellsAtom);
158
- for (const cellId of cellIds.toReversed()) {
159
- if (stagedAICells.has(cellId)) {
160
- stagedCellActions.deleteStagedCell(cellId);
161
- }
162
- }
163
- };
164
-
165
- const beginStagedCellGeneration = () => {
166
- const activeGeneration = store.get(stagedGenerationAtom);
167
- if (activeGeneration?.status === "in_progress") {
168
- discardStagedCellIds(activeGeneration.cellIds);
169
- }
170
- ownedGeneration.current?.reconciler.discard(store.get(stagedAICellsAtom));
171
- const generationId = Symbol("staged-cell-generation");
172
- ownedGeneration.current = {
173
- id: generationId,
174
- status: "in_progress",
175
- reconciler: new StagedCellReconciler({
176
- ...stagedCellActions,
177
- createCell,
178
- }),
179
- };
180
- store.set(stagedGenerationAtom, {
181
- id: generationId,
182
- status: "in_progress",
183
- cellIds: [],
184
- });
185
- };
186
-
187
- const finishStagedCellGeneration = (successful: boolean) => {
188
- const generation = ownedGeneration.current;
189
- if (generation?.status !== "in_progress") {
190
- return;
191
- }
192
-
193
- const stagedGeneration = store.get(stagedGenerationAtom);
194
- const ownsActiveGeneration =
195
- stagedGeneration?.id === generation.id &&
196
- stagedGeneration.status === "in_progress";
197
- if (!ownsActiveGeneration || !successful) {
198
- generation.reconciler.discard(store.get(stagedAICellsAtom));
199
- if (ownsActiveGeneration) {
200
- store.set(stagedGenerationAtom, null);
201
- }
202
- ownedGeneration.current = null;
203
- return;
204
- }
205
-
206
- generation.status = "complete";
207
- store.set(stagedGenerationAtom, {
208
- id: generation.id,
209
- status: "complete",
210
- });
211
- };
212
-
213
- const hasOwnedStagedCells = () => {
214
- const generation = ownedGeneration.current;
215
- const stagedGeneration = store.get(stagedGenerationAtom);
216
- return (
217
- generation !== null &&
218
- generation.status === "complete" &&
219
- stagedGeneration?.status === "complete" &&
220
- stagedGeneration.id === generation.id &&
221
- generation.reconciler.hasStagedCells(store.get(stagedAICellsAtom))
222
- );
223
- };
224
-
225
- const acceptOwnedStagedCells = () => {
226
- if (!hasOwnedStagedCells()) {
227
- return false;
228
- }
229
-
230
- ownedGeneration.current?.reconciler.accept();
231
- store.set(stagedGenerationAtom, null);
232
- ownedGeneration.current = null;
233
- return true;
234
- };
235
-
236
- const discardOwnedStagedCells = () => {
237
- const generation = ownedGeneration.current;
238
- const stagedGeneration = store.get(stagedGenerationAtom);
239
- if (generation === null || stagedGeneration?.id !== generation.id) {
240
- return false;
241
- }
242
-
243
- generation.reconciler.discard(store.get(stagedAICellsAtom));
244
- store.set(stagedGenerationAtom, null);
245
- ownedGeneration.current = null;
246
- return true;
247
- };
248
-
249
- const onData = (part: CompletionDataPart) => {
250
- if (part.type !== NOTEBOOK_CELLS_COMPLETION_DATA_TYPE) {
251
- return;
252
- }
253
- const generation = ownedGeneration.current;
254
- const stagedGeneration = store.get(stagedGenerationAtom);
255
- if (
256
- generation?.status !== "in_progress" ||
257
- stagedGeneration?.id !== generation.id ||
258
- stagedGeneration.status !== "in_progress"
259
- ) {
260
- return;
261
- }
262
- const completion = notebookCellsCompletionSchema.parse(part.data);
263
- generation.reconciler.reconcile(completion.cells);
264
- store.set(stagedGenerationAtom, {
265
- ...stagedGeneration,
266
- cellIds: generation.reconciler.stagedCellIds(),
267
- });
268
- };
269
-
270
- return {
271
- beginStagedCellGeneration,
272
- finishStagedCellGeneration,
273
- hasOwnedStagedCells,
274
- acceptOwnedStagedCells,
275
- discardOwnedStagedCells,
276
- onData,
277
- };
278
- }
279
-
280
116
  export { stagedAICellsAtom };
281
117
  export const visibleForTesting = {
282
- stagedGenerationAtom,
283
118
  createActions,
284
119
  reducer,
285
120
  initialState,
286
121
  };
287
-
288
- interface CreatedCell {
289
- cellId: CellId;
290
- code: string;
291
- }
292
-
293
- interface StagedCellReconcilerActions {
294
- createStagedCell: (code: string) => CellId;
295
- updateStagedCell: (opts: UpdateStagedCellAction) => void;
296
- deleteStagedCell: (cellId: CellId) => void;
297
- removeStagedCell: (cellId: CellId) => void;
298
- addStagedCell: (payload: { cellId: CellId; edit: Edit }) => void;
299
- createCell: CreateCell;
300
- }
301
-
302
- class StagedCellReconciler {
303
- private createdCells: CreatedCell[] = [];
304
- private readonly actions: StagedCellReconcilerActions;
305
- private marimoImportCellId: CellId | null = null;
306
-
307
- public constructor(actions: StagedCellReconcilerActions) {
308
- this.actions = actions;
309
- }
310
-
311
- public reconcile(completionCells: GeneratedCell[]) {
312
- for (const [idx, cell] of completionCells.entries()) {
313
- if (idx < this.createdCells.length) {
314
- const existingCell = this.createdCells[idx];
315
- const codeChanged = existingCell.code !== cell.code;
316
- this.createdCells[idx] = { ...existingCell, code: cell.code };
317
- if (!codeChanged) {
318
- continue;
319
- }
320
- this.actions.updateStagedCell({
321
- cellId: existingCell.cellId,
322
- code: cell.code,
323
- });
324
- } else {
325
- const newCellId = this.actions.createStagedCell(cell.code);
326
- this.createdCells.push({ cellId: newCellId, code: cell.code });
327
- }
328
- }
329
-
330
- const removedCells = this.createdCells.splice(completionCells.length);
331
- for (const { cellId } of removedCells.toReversed()) {
332
- this.actions.deleteStagedCell(cellId);
333
- }
334
-
335
- this.syncMarimoImport(completionCells);
336
- }
337
-
338
- public discard(stagedCells: StagedAICells) {
339
- for (const { cellId } of this.createdCells.toReversed()) {
340
- if (stagedCells.has(cellId)) {
341
- this.actions.deleteStagedCell(cellId);
342
- }
343
- }
344
- this.createdCells = [];
345
-
346
- if (this.marimoImportCellId && stagedCells.has(this.marimoImportCellId)) {
347
- this.actions.deleteStagedCell(this.marimoImportCellId);
348
- }
349
- this.marimoImportCellId = null;
350
- }
351
-
352
- public accept() {
353
- for (const cellId of this.stagedCellIds()) {
354
- this.actions.removeStagedCell(cellId);
355
- }
356
- }
357
-
358
- public hasStagedCells(stagedCells: StagedAICells) {
359
- return this.stagedCellIds().some((cellId) => stagedCells.has(cellId));
360
- }
361
-
362
- public stagedCellIds() {
363
- const cellIds = this.createdCells.map(({ cellId }) => cellId);
364
- if (this.marimoImportCellId) {
365
- cellIds.push(this.marimoImportCellId);
366
- }
367
- return cellIds;
368
- }
369
-
370
- /** Keep the generated marimo import consistent with the latest snapshot. */
371
- private syncMarimoImport(completionCells: GeneratedCell[]) {
372
- const requiresMarimo = completionCells.some(
373
- (cell) => cell.language !== "python",
374
- );
375
- if (!requiresMarimo) {
376
- if (this.marimoImportCellId) {
377
- this.actions.deleteStagedCell(this.marimoImportCellId);
378
- this.marimoImportCellId = null;
379
- }
380
- return;
381
- }
382
- if (this.marimoImportCellId) {
383
- return;
384
- }
385
-
386
- const cellId = maybeAddMarimoImport({
387
- autoInstantiate: false,
388
- createCell: this.actions.createCell,
389
- fromCellId: this.createdCells[0]?.cellId,
390
- before: true,
391
- });
392
- if (cellId) {
393
- this.actions.addStagedCell({ cellId, edit: { type: "add_cell" } });
394
- this.marimoImportCellId = cellId;
395
- }
396
- }
397
- }
@@ -31,10 +31,13 @@ export const aiCompletionCellAtom = atom<AiCompletionCell | null>(null);
31
31
  export interface PendingAiPrompt {
32
32
  prompt: string;
33
33
  submit: boolean;
34
+ append?: boolean;
34
35
  }
35
36
 
36
37
  export const pendingAiPromptAtom = atom<PendingAiPrompt | null>(null);
37
38
 
39
+ export const chatGenerationInProgressAtom = atom(false);
40
+
38
41
  const CHAT_OPTIONS_KEY = "marimo:ai:chatOptions";
39
42
  export const chatOptionsAtom = atomWithStorage<Required<ChatOptions>>(
40
43
  CHAT_OPTIONS_KEY,
@@ -40,6 +40,7 @@ function getOpts() {
40
40
  enableAI: false,
41
41
  cellActions: {
42
42
  toggleHideCode: namedFunction("toggleHideCode"),
43
+ askAiAboutCell: namedFunction("askAiAboutCell"),
43
44
  aiCellCompletion: namedFunction("aiCellCompletion"),
44
45
  createManyBelow: namedFunction("createManyBelow"),
45
46
  onRun: namedFunction("onRun"),
@@ -240,3 +241,34 @@ test("ai adds more extensions", () => {
240
241
  .flat();
241
242
  expect(withAI.length - 2).toBe(withoutAI.length);
242
243
  });
244
+
245
+ test("empty-cell AI action opens chat without invoking the local refactor prompt", () => {
246
+ const askAiAboutCell = vi.fn();
247
+ const aiCellCompletion = vi.fn();
248
+ const view = new EditorView({
249
+ state: EditorState.create({
250
+ doc: "",
251
+ extensions: setup({
252
+ showPlaceholder: true,
253
+ enableAI: false,
254
+ enableAIChat: true,
255
+ cellActions: {
256
+ ...getOpts().cellActions,
257
+ askAiAboutCell,
258
+ aiCellCompletion,
259
+ },
260
+ }),
261
+ }),
262
+ });
263
+ try {
264
+ const button = view.dom.querySelector<HTMLButtonElement>(
265
+ ".cm-clickable-placeholder",
266
+ );
267
+ expect(button?.textContent).toBe("generate with AI");
268
+ button?.click();
269
+ expect(askAiAboutCell).toHaveBeenCalledOnce();
270
+ expect(aiCellCompletion).not.toHaveBeenCalled();
271
+ } finally {
272
+ view.destroy();
273
+ }
274
+ });
@@ -6,6 +6,7 @@ import type { CellId } from "@/core/cells/ids";
6
6
 
7
7
  export interface CodemirrorCellActions extends CellActions {
8
8
  toggleHideCode: () => boolean;
9
+ askAiAboutCell: () => void;
9
10
  aiCellCompletion: () => boolean;
10
11
  createManyBelow: (content: string[]) => void;
11
12
  onRun: () => void;
@@ -74,6 +74,7 @@ export interface CodeMirrorSetupOpts {
74
74
  cellId: CellId;
75
75
  showPlaceholder: boolean;
76
76
  enableAI: boolean;
77
+ enableAIChat?: boolean;
77
78
  acceptCompletionOnEnter?: boolean;
78
79
  cellActions: CodemirrorCellActions;
79
80
  completionConfig: CompletionConfig;
@@ -92,7 +93,10 @@ export interface CodeMirrorSetupOpts {
92
93
  }
93
94
 
94
95
  function getPlaceholderType(opts: CodeMirrorSetupOpts) {
95
- const { showPlaceholder, enableAI } = opts;
96
+ const { showPlaceholder, enableAI, enableAIChat } = opts;
97
+ if (enableAIChat) {
98
+ return "ai";
99
+ }
96
100
  return showPlaceholder ? "marimo-import" : enableAI ? "ai" : "none";
97
101
  }
98
102
 
@@ -4,7 +4,7 @@
4
4
  * The type of placeholder to use for the editor.
5
5
  *
6
6
  * - "marimo-import" -> Shows `import marimo as mo`
7
- * - "ai" -> Shows `Start coding or Generate with AI`
7
+ * - "ai" -> Shows `Start coding or generate with AI`
8
8
  * - "none" -> No placeholder
9
9
  */
10
10
  export type PlaceholderType = "marimo-import" | "ai" | "none";
@@ -422,11 +422,11 @@ function getPlaceholderExtension(placeholderType: PlaceholderType): Extension {
422
422
  if (placeholderType === "ai") {
423
423
  return clickablePlaceholderExtension({
424
424
  beforeText: "Start coding or ",
425
- linkText: "generate",
426
- afterText: " with AI.",
425
+ linkText: "generate with AI",
426
+ afterText: ".",
427
427
  onClick: (ev) => {
428
428
  const cellActions = ev.state.facet(cellActionsState);
429
- cellActions.aiCellCompletion();
429
+ cellActions.askAiAboutCell();
430
430
  },
431
431
  });
432
432
  }
@@ -81,16 +81,32 @@ describe("clickablePlaceholderExtension", () => {
81
81
 
82
82
  // Check text content
83
83
  expect(placeholder.textContent).toBe("Click here to continue");
84
+ expect(
85
+ [...placeholder.querySelectorAll('[aria-hidden="true"]')].map(
86
+ (node) => node.textContent,
87
+ ),
88
+ ).toEqual(["Click ", " to continue"]);
89
+ expect(
90
+ placeholder.querySelector("button")?.closest('[aria-hidden="true"]'),
91
+ ).toBeNull();
84
92
 
85
93
  // Find and check the clickable link
86
94
  const link = placeholder.querySelector(".cm-clickable-placeholder")!;
87
95
  expect(link).toBeTruthy();
88
96
  expect(link.textContent).toBe("here");
97
+ expect(link.tagName).toBe("BUTTON");
98
+ expect(link.closest('[aria-hidden="true"]')).toBeNull();
99
+ expect(link.getAttribute("type")).toBe("button");
89
100
 
90
101
  // Simulate click
91
102
  (link as HTMLElement).click();
92
103
  expect(onClick).toHaveBeenCalled();
93
104
 
105
+ view.dispatch({ changes: { from: 0, insert: "Hello" } });
106
+ expect(view.dom.querySelector(".cm-clickable-placeholder")).toBeNull();
107
+ view.dispatch({ changes: { from: 0, to: 5 } });
108
+ expect(view.dom.querySelector("button")?.textContent).toBe("here");
109
+
94
110
  // Check theme classes
95
111
  expect(link.classList.contains("cm-clickable-placeholder")).toBe(true);
96
112
 
@@ -1,6 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { Extension } from "@codemirror/state";
3
- import { EditorView, keymap, placeholder } from "@codemirror/view";
3
+ import {
4
+ Decoration,
5
+ EditorView,
6
+ keymap,
7
+ placeholder,
8
+ WidgetType,
9
+ } from "@codemirror/view";
4
10
  import { isEditorReadonly } from "../readonly/extension";
5
11
 
6
12
  /**
@@ -60,37 +66,69 @@ export function clickablePlaceholderExtension(opts: {
60
66
  }): Extension[] {
61
67
  const { beforeText, linkText, afterText, onClick } = opts;
62
68
 
63
- // Create a placeholder
64
- // Needs to be a function to keep event listeners
65
- // See https://github.com/codemirror/dev/issues/1457
66
- const createPlaceholder = (ev: EditorView) => {
67
- const placeholderText = document.createElement("span");
68
- placeholderText.append(document.createTextNode(beforeText));
69
- const link = document.createElement("span");
70
- link.textContent = linkText;
71
- link.classList.add("cm-clickable-placeholder");
72
- link.onclick = (evt) => {
73
- evt.stopPropagation();
74
- onClick(ev);
75
- };
76
- placeholderText.append(link);
77
- placeholderText.append(document.createTextNode(afterText));
78
- return placeholderText;
79
- };
69
+ class ActionPlaceholder extends WidgetType {
70
+ public toDOM(ev: EditorView) {
71
+ const wrapper = document.createElement("span");
72
+ wrapper.className = "cm-placeholder";
73
+ wrapper.contentEditable = "false";
74
+ const before = document.createElement("span");
75
+ before.textContent = beforeText;
76
+ before.setAttribute("aria-hidden", "true");
77
+ wrapper.append(before);
78
+ const button = document.createElement("button");
79
+ button.type = "button";
80
+ button.textContent = linkText;
81
+ button.className = "cm-clickable-placeholder";
82
+ button.onclick = (event) => {
83
+ event.stopPropagation();
84
+ onClick(ev);
85
+ };
86
+ const after = document.createElement("span");
87
+ after.textContent = afterText;
88
+ after.setAttribute("aria-hidden", "true");
89
+ wrapper.append(button, after);
90
+ return wrapper;
91
+ }
92
+
93
+ public override ignoreEvent(event: Event) {
94
+ return (
95
+ event.target instanceof Element &&
96
+ event.target.closest(".cm-clickable-placeholder") !== null
97
+ );
98
+ }
99
+ }
80
100
 
81
101
  return [
82
- placeholder(createPlaceholder),
102
+ EditorView.decorations.compute(["doc"], (state) =>
103
+ state.doc.length === 0
104
+ ? Decoration.set([
105
+ Decoration.widget({
106
+ widget: new ActionPlaceholder(),
107
+ side: 1,
108
+ }).range(0),
109
+ ])
110
+ : Decoration.none,
111
+ ),
83
112
  EditorView.theme({
84
113
  ".cm-placeholder": {
85
114
  color: "var(--slate-8)",
86
115
  },
87
116
  ".cm-clickable-placeholder": {
88
117
  cursor: "pointer !important",
118
+ font: "inherit",
119
+ background: "transparent",
120
+ border: "none",
121
+ padding: "0",
89
122
  pointerEvents: "auto",
90
123
  color: "var(--slate-9)",
91
124
  textDecoration: "underline",
92
125
  textUnderlineOffset: "0.2em",
93
126
  },
127
+ ".cm-clickable-placeholder:focus-visible": {
128
+ outline: "1px solid var(--muted-foreground)",
129
+ outlineOffset: "2px",
130
+ borderRadius: "2px",
131
+ },
94
132
  ".cm-clickable-placeholder:hover": {
95
133
  color: "var(--sky-11)",
96
134
  },
@@ -221,7 +221,8 @@ const DEFAULT_HOT_KEY = {
221
221
  key: "Mod-h",
222
222
  },
223
223
  "cell.aiCompletion": {
224
- name: "AI completion",
224
+ name: "Refactor cell",
225
+ additionalKeywords: ["AI", "AI completion"],
225
226
  group: "Editing",
226
227
  key: "Mod-Shift-e",
227
228
  },
@@ -63,6 +63,7 @@ function languages(id: CellId) {
63
63
  cellActionsState.of({
64
64
  ...createNotebookActions(vi.fn()),
65
65
  toggleHideCode: () => false,
66
+ askAiAboutCell: () => undefined,
66
67
  aiCellCompletion: () => false,
67
68
  createManyBelow: vi.fn(),
68
69
  addOrAppendSetupCell: vi.fn(),
@@ -7,7 +7,7 @@ import type { PopoverContent } from "@/components/ui/popover";
7
7
  import { TooltipProvider } from "@/components/ui/tooltip";
8
8
  import { Chatbot } from "../chat-ui";
9
9
 
10
- vi.mock("@/components/editor/ai/add-cell-with-ai", () => ({
10
+ vi.mock("@/components/editor/ai/prompt-input", () => ({
11
11
  PromptInput: ({ value }: { value: string }) => (
12
12
  <textarea aria-label="Chat prompt" value={value} readOnly={true} />
13
13
  ),
@@ -39,7 +39,7 @@ import {
39
39
  import {
40
40
  type AdditionalCompletions,
41
41
  PromptInput,
42
- } from "@/components/editor/ai/add-cell-with-ai";
42
+ } from "@/components/editor/ai/prompt-input";
43
43
  import { CopyClipboardIcon } from "@/components/icons/copy-icon";
44
44
  import { Spinner } from "@/components/icons/spinner";
45
45
  import { Button } from "@/components/ui/button";