@marimo-team/islands 0.25.3-dev265 → 0.25.3-dev267

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 (106) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C_hj7Poi.js → ConnectedDataExplorerComponent-C4yCN38h.js} +1 -1
  2. package/dist/{ErrorBoundary-DzYPyiOs.js → ErrorBoundary-CewXPyL5.js} +64 -69
  3. package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-o6Ay6Q63.js} +2 -2
  4. package/dist/{any-language-editor-CFeWfC3o.js → any-language-editor-DnG26QXh.js} +1 -1
  5. package/dist/{architectureDiagram-5GKGNRK7-iF_snyFL.js → architectureDiagram-5GKGNRK7-BhiwwlVg.js} +1 -1
  6. package/dist/{chat-ui-DFxNgzXF.js → chat-ui-CbgR5ivy.js} +1964 -1740
  7. package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-WMSMf4Nj.js} +1 -1
  8. package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-BRfADrzD.js} +1 -1
  9. package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CuK5QFre.js} +1 -1
  10. package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-Bcun_jhJ.js} +1 -1
  11. package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-CgWDbUDT.js} +2 -2
  12. package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-DYmc9SFU.js} +2 -2
  13. package/dist/{code-visibility-CJExrSFe.js → code-visibility-BKBl9Rh6.js} +9 -9
  14. package/dist/{common-5mM7a8cm.js → common-N3Jd_6m9.js} +5944 -5626
  15. package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-PPKWipjT.js} +1 -1
  16. package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-niRBZh--.js} +1 -1
  17. package/dist/{data-grid-overlay-editor--Un2Lk6m.js → data-grid-overlay-editor-Cp59CrqZ.js} +1 -1
  18. package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-DPiP9rot.js} +1 -1
  19. package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-B0WZuaxV.js} +1 -1
  20. package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-Rn42WBuN.js} +1 -1
  21. package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-BB93--ry.js} +1 -1
  22. package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-BpImEfn3.js} +1211 -1205
  23. package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-V4t5h6mZ.js} +2 -2
  24. package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-C1MsAKXd.js} +1 -1
  25. package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
  26. package/dist/{error-entries-CMoaUX9V.js → error-entries-CJGdD2ky.js} +49 -42
  27. package/dist/{feedback-button-C2NUXbzh.js → feedback-button-Qc1f9iPq.js} +70 -70
  28. package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-DHjeJP96.js} +2 -2
  29. package/dist/{glide-data-editor-Dj9U-NK9.js → glide-data-editor-hCq3K0sd.js} +2042 -2041
  30. package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-DC3sk6EX.js} +28 -34
  31. package/dist/{globals-CkbsxIrq.js → globals-B2_uMYDY.js} +1 -1
  32. package/dist/{html-to-image-D_5CMreP.js → html-to-image-S7spRQCi.js} +5526 -5828
  33. package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-Dovv6iKx.js} +1 -1
  34. package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-D2nyygsc.js} +1 -1
  35. package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-KyVx0uTY.js} +1 -1
  36. package/dist/main.js +1288 -1287
  37. package/dist/{mermaid-DFYN_PXd.js → mermaid-B-jKSwGt.js} +33 -33
  38. package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-Bz9tKQFl.js} +1 -1
  39. package/dist/{number-overlay-editor-H8QAAmdr.js → number-overlay-editor-CfNYvbdz.js} +1 -1
  40. package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-C_qpq6Tv.js} +2 -2
  41. package/dist/pencil-DcT8AE0h.js +10 -0
  42. package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-Bp9ucgI4.js} +1 -1
  43. package/dist/{process-output-CGFNVC60.js → process-output-f34bkXTm.js} +2 -2
  44. package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-tq-VEH6j.js} +2 -2
  45. package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-DW-vHadb.js} +1 -1
  46. package/dist/{reveal-component-BperrCFw.js → reveal-component-CUxv-kkP.js} +9 -9
  47. package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-Dv9U9qRP.js} +1 -1
  48. package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-DTmPQ0EB.js} +2 -2
  49. package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-CkCif0Iw.js} +2 -2
  50. package/dist/style.css +1 -1
  51. package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-21kCq6l8.js} +1 -1
  52. package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BpRJP6KU.js} +2 -2
  53. package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-Ckgh4lS4.js} +1 -1
  54. package/dist/{toast-CY1_Qv41.js → toast-DpczUryP.js} +1 -1
  55. package/dist/{vega-component-C-lqjV0q.js → vega-component-CF9kI6C4.js} +1 -1
  56. package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-CDei2Bt3.js} +1 -1
  57. package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-CXavT601.js} +1 -1
  58. package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-CvJhcZri.js} +1 -1
  59. package/package.json +1 -1
  60. package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
  61. package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
  62. package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
  63. package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
  64. package/src/components/chat/__tests__/user-message.test.tsx +27 -0
  65. package/src/components/chat/acp/agent-panel.tsx +2 -0
  66. package/src/components/chat/agent-activity.tsx +132 -0
  67. package/src/components/chat/chat-display.tsx +125 -96
  68. package/src/components/chat/chat-panel.tsx +123 -106
  69. package/src/components/chat/chat-utils.ts +12 -24
  70. package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
  71. package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
  72. package/src/components/chat/tool-call/shared.ts +6 -0
  73. package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
  74. package/src/components/chat/tool-call/tool-labels.ts +172 -0
  75. package/src/components/chat/tool-call/tool-step.tsx +145 -0
  76. package/src/components/chat/turn-timing.ts +73 -0
  77. package/src/components/chat/use-assistant-turn.ts +65 -0
  78. package/src/components/chat/use-chat-generation.ts +5 -4
  79. package/src/components/chat/user-message.tsx +145 -0
  80. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +18 -1
  81. package/src/components/editor/actions/useCellActionButton.tsx +3 -0
  82. package/src/components/editor/ai/completion-handlers.tsx +0 -37
  83. package/src/components/editor/cell/StagedAICell.tsx +51 -144
  84. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
  85. package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
  86. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
  87. package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
  88. package/src/components/editor/chrome/wrapper/sidebar.tsx +6 -0
  89. package/src/components/editor/links/cell-link.tsx +4 -4
  90. package/src/components/editor/notebook-cell.tsx +3 -11
  91. package/src/components/ui/toast.tsx +1 -1
  92. package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
  93. package/src/core/ai/staged-cells.ts +48 -38
  94. package/src/core/cells/scrollCellIntoView.ts +6 -0
  95. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
  96. package/src/core/codemirror/placeholder/extensions.ts +23 -0
  97. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
  98. package/src/core/session/useNotebookSession.tsx +7 -2
  99. package/src/css/app/Cell.css +7 -36
  100. package/src/plugins/impl/chat/chat-ui.tsx +2 -3
  101. package/src/plugins/impl/common/error-banner.tsx +5 -3
  102. package/src/components/chat/reasoning-accordion.tsx +0 -61
  103. package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
  104. package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
  105. package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
  106. package/src/components/icons/loading-ellipsis.tsx +0 -46
@@ -0,0 +1,145 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
4
+ import type { FileUIPart, TextUIPart, UIMessage } from "ai";
5
+ import { PencilIcon } from "lucide-react";
6
+ import React from "react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Tooltip } from "@/components/ui/tooltip";
9
+ import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
10
+ import { PromptInput } from "../editor/ai/prompt-input";
11
+ import { CopyClipboardIcon } from "../icons/copy-icon";
12
+ import { AttachmentRenderer } from "./chat-components";
13
+
14
+ interface UserMessageProps {
15
+ message: UIMessage;
16
+ canEdit: boolean;
17
+ onEdit: (newValue: string) => void;
18
+ }
19
+
20
+ export const UserMessage: React.FC<UserMessageProps> = ({
21
+ message,
22
+ canEdit,
23
+ onEdit,
24
+ }) => {
25
+ const content = message.parts
26
+ .filter((p): p is TextUIPart => p.type === "text")
27
+ .map((p) => p.text)
28
+ .join("\n");
29
+ const attachments = message.parts
30
+ .filter((p): p is FileUIPart => p.type === "file")
31
+ .map((part, idx) => (
32
+ // Sent parts never reorder, and a file attached twice repeats its URL.
33
+ // react-doctor-disable-next-line react-doctor/no-array-index-as-key
34
+ <AttachmentRenderer attachment={part} key={idx} />
35
+ ));
36
+
37
+ const [draft, setDraft] = React.useState<string | null>(null);
38
+ const inputRef = React.useRef<ReactCodeMirrorRef>(null);
39
+ const editButtonRef = React.useRef<HTMLButtonElement>(null);
40
+ const editing = draft != null;
41
+
42
+ const startEditing = () => {
43
+ setDraft(content);
44
+ focusInputAndMoveToEnd(inputRef);
45
+ };
46
+
47
+ const cancel = () => {
48
+ setDraft(null);
49
+ requestAnimationFrame(() => editButtonRef.current?.focus());
50
+ };
51
+
52
+ const submit = (value: string) => {
53
+ if (!canEdit || !value.trim()) {
54
+ return;
55
+ }
56
+ onEdit(value);
57
+ setDraft(null);
58
+ };
59
+
60
+ if (editing) {
61
+ return (
62
+ <div className="w-full rounded-[10px] border border-border bg-background">
63
+ {attachments}
64
+ <div className="px-2 py-1.5">
65
+ <PromptInput
66
+ inputRef={inputRef}
67
+ className="max-h-[400px]"
68
+ value={draft}
69
+ onChange={setDraft}
70
+ onSubmit={(_e, value) => submit(value)}
71
+ onClose={cancel}
72
+ placeholder="Type your message. Use @ to reference variables and cells."
73
+ />
74
+ </div>
75
+ <div className="flex items-center gap-2 border-t border-border/20 px-3 py-1.5">
76
+ <span className="mr-auto text-xs text-muted-foreground">
77
+ Responses after this message will be replaced.
78
+ </span>
79
+ <Button variant="text" size="xs" onClick={cancel}>
80
+ Cancel
81
+ </Button>
82
+ <Button
83
+ size="xs"
84
+ disabled={!canEdit || !draft.trim()}
85
+ onClick={() => submit(draft)}
86
+ >
87
+ Send
88
+ </Button>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <div className="flex max-w-[90%] flex-col items-end gap-1">
96
+ {attachments}
97
+ <div className="rounded-[10px] bg-(--slate-3) px-3 py-1.5 whitespace-pre-wrap wrap-break-word">
98
+ <MentionText text={content} />
99
+ </div>
100
+ <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
101
+ <Tooltip content="Edit">
102
+ <Button
103
+ ref={editButtonRef}
104
+ variant="text"
105
+ size="icon"
106
+ aria-label="Edit message"
107
+ className="chat-control"
108
+ disabled={!canEdit}
109
+ onClick={startEditing}
110
+ >
111
+ <PencilIcon className="size-3" />
112
+ </Button>
113
+ </Tooltip>
114
+ <CopyClipboardIcon
115
+ value={content}
116
+ className="size-3"
117
+ buttonClassName="chat-control flex size-6 items-center justify-center rounded-md"
118
+ />
119
+ </div>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ const MENTION = /([@#])[\w-]+:\/\/([\w./-]+)/g;
125
+
126
+ const MentionText: React.FC<{ text: string }> = ({ text }) => {
127
+ const nodes: React.ReactNode[] = [];
128
+ let last = 0;
129
+ for (const match of text.matchAll(MENTION)) {
130
+ nodes.push(text.slice(last, match.index));
131
+ nodes.push(
132
+ <span
133
+ key={match.index}
134
+ title={match[0].slice(1)}
135
+ className="rounded-sm bg-(--slate-5) px-1 font-mono text-[0.85em]"
136
+ >
137
+ {match[1]}
138
+ {match[2]}
139
+ </span>,
140
+ );
141
+ last = match.index + match[0].length;
142
+ }
143
+ nodes.push(text.slice(last));
144
+ return nodes;
145
+ };
@@ -10,6 +10,7 @@ import { partial } from "@/__mocks__/partial";
10
10
  import { MockRequestClient } from "@/__mocks__/requests";
11
11
  import { ModalProvider } from "@/components/modal/ImperativeModal";
12
12
  import { toast } from "@/components/ui/use-toast";
13
+ import { type Edit, stagedAICellsAtom } from "@/core/ai/staged-cells";
13
14
  import { CellId } from "@/core/cells/ids";
14
15
  import { requestClientAtom } from "@/core/network/requests";
15
16
  import type { NotebookReplica } from "@/core/notebook/replica";
@@ -21,8 +22,11 @@ import { useCellActionButtons } from "../useCellActionButton";
21
22
  vi.mock("@/utils/copy", () => ({ copyToClipboard: vi.fn() }));
22
23
  vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() }));
23
24
 
24
- function renderActions(cellId: CellId, name: string) {
25
+ function renderActions(cellId: CellId, name: string, edit?: Edit) {
25
26
  const store = createStore();
27
+ if (edit) {
28
+ store.set(stagedAICellsAtom, new Map([[cellId, edit]]));
29
+ }
26
30
  const { model, documentAtom, stop } = mockDocumentModel(store);
27
31
  onTestFinished(stop);
28
32
  store.set(requestClientAtom, MockRequestClient.create());
@@ -111,3 +115,16 @@ describe("useCellActionButtons", () => {
111
115
  expect(getAction(groups, "Copy cell ID").disabled).toBeUndefined();
112
116
  });
113
117
  });
118
+
119
+ it("offers individual AI review actions in the cell menu only for pending changes", () => {
120
+ const id = CellId.create();
121
+ const pending = renderActions(id, "_", { type: "add_cell" });
122
+ expect(pending[0].map((action) => action.label)).toEqual([
123
+ "Keep cell",
124
+ "Discard cell",
125
+ ]);
126
+ const ordinary = renderActions(id, "_");
127
+ expect(ordinary.flat().some((action) => action.label === "Keep cell")).toBe(
128
+ false,
129
+ );
130
+ });
@@ -63,6 +63,7 @@ import { copyToClipboard } from "@/utils/copy";
63
63
  import { downloadCellOutputAsImage } from "@/utils/download";
64
64
  import { MarkdownIcon, PythonIcon } from "../cell/code/icons";
65
65
  import { useDeleteCellCallback } from "../cell/useDeleteCell";
66
+ import { useStagedAICellActions } from "../cell/StagedAICell";
66
67
  import { useRunCell } from "../cell/useRunCells";
67
68
  import { useSplitCellCallback } from "../cell/useSplitCell";
68
69
  import { NameCellInput } from "./name-cell-input";
@@ -93,6 +94,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
93
94
  clearCellOutput,
94
95
  createCell,
95
96
  } = useCellActions();
97
+ const stagedActions = useStagedAICellActions(cell?.cellId);
96
98
  const showHiddenCode = useSetAtom(showHiddenCodeAtom);
97
99
  const edit = useNotebookDocumentEdit();
98
100
  const splitCell = useSplitCellCallback();
@@ -144,6 +146,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
144
146
 
145
147
  // Actions
146
148
  const actions: ActionButton[][] = [
149
+ stagedActions,
147
150
  [
148
151
  {
149
152
  icon: <PlayIcon size={13} strokeWidth={1.5} />,
@@ -46,43 +46,6 @@ export const createAiCompletionOnKeydown = (opts: {
46
46
  };
47
47
  };
48
48
 
49
- export const CompletionActionsCellFooter: React.FC<{
50
- isLoading: boolean;
51
- onAccept: () => void;
52
- onDecline: () => void;
53
- size?: "xs" | "sm";
54
- multipleCompletions?: boolean;
55
- runCell?: () => void;
56
- acceptLabel?: string;
57
- declineLabel?: string;
58
- }> = ({
59
- isLoading,
60
- onAccept,
61
- onDecline,
62
- runCell,
63
- acceptLabel,
64
- declineLabel,
65
- size,
66
- }) => {
67
- return (
68
- <>
69
- <AcceptCompletionButton
70
- isLoading={isLoading}
71
- onAccept={onAccept}
72
- size={size}
73
- runCell={runCell}
74
- label={acceptLabel}
75
- />
76
- <RejectCompletionButton
77
- onDecline={onDecline}
78
- size={size}
79
- label={declineLabel}
80
- disabled={isLoading}
81
- />
82
- </>
83
- );
84
- };
85
-
86
49
  export const AcceptCompletionButton: React.FC<{
87
50
  isLoading: boolean;
88
51
  onAccept: () => void;
@@ -1,24 +1,17 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useAtomValue, useStore } from "jotai";
4
- import { SparklesIcon } from "lucide-react";
5
- import { Tooltip } from "@/components/ui/tooltip";
3
+ import { atom, useAtomValue, useStore } from "jotai";
4
+ import { useMemo } from "react";
6
5
  import {
7
6
  type Edit,
7
+ stagedAICellsAtom,
8
8
  useStagedAICell,
9
9
  useStagedCells,
10
10
  } from "@/core/ai/staged-cells";
11
11
  import { chatGenerationInProgressAtom } from "@/core/ai/state";
12
- import { getCellEditorView } from "@/core/cells/cells";
13
12
  import type { CellId } from "@/core/cells/ids";
14
- import { prepareCellCodeUpdate } from "@/core/codemirror/language/cell-code";
15
- import { cn } from "@/utils/cn";
16
- import { Logger } from "@/utils/Logger";
17
- import {
18
- AI_EDIT_ACTION_LABELS,
19
- CompletionActionsCellFooter,
20
- } from "../ai/completion-handlers";
21
- import { useRunCell } from "./useRunCells";
13
+ import { AI_EDIT_ACTION_LABELS } from "../ai/completion-handlers";
14
+ import type { ActionButton } from "../actions/types";
22
15
 
23
16
  const actionLabels: Record<Edit["type"], { accept: string; decline: string }> =
24
17
  {
@@ -27,142 +20,56 @@ const actionLabels: Record<Edit["type"], { accept: string; decline: string }> =
27
20
  delete_cell: { accept: "Delete cell", decline: "Keep cell" },
28
21
  };
29
22
 
30
- export const StagedAICellBackground: React.FC<{
31
- cellId: CellId;
32
- className?: string;
33
- }> = ({ cellId, className }) => {
34
- const stagedCell = useStagedAICell(cellId);
35
-
36
- if (!stagedCell) {
37
- return null;
38
- }
39
-
40
- const cellClass =
41
- stagedCell.type === "delete_cell"
42
- ? "mo-ai-deleted-cell"
43
- : "mo-ai-generated-cell";
44
-
45
- return <div className={cn(cellClass, className)} />;
46
- };
47
-
48
- export const StagedAICellFooter: React.FC<{ cellId: CellId }> = ({
23
+ export const StagedAIDeletionMarker: React.FC<{ cellId: CellId }> = ({
49
24
  cellId,
50
- }) => {
25
+ }) =>
26
+ useStagedAICell(cellId)?.type === "delete_cell" ? (
27
+ <div className="mo-ai-deleted-cell" />
28
+ ) : null;
29
+
30
+ export function useStagedAICellActions(
31
+ cellId: CellId | undefined,
32
+ ): ActionButton[] {
51
33
  const store = useStore();
52
- const stagedAiCell = useStagedAICell(cellId);
53
- const isGenerating = useAtomValue(chatGenerationInProgressAtom);
54
- const runCell = useRunCell(cellId);
55
-
56
- const { deleteStagedCell, removeStagedCell } = useStagedCells(store);
57
-
58
- if (!stagedAiCell) {
59
- return null;
60
- }
61
-
62
- const isDeletion = stagedAiCell.type === "delete_cell";
63
- const labels = actionLabels[stagedAiCell.type];
64
-
65
- const handleCompletion = (type: "accept" | "reject") => {
66
- const completionFunc =
67
- type === "accept" ? acceptStagedCell : rejectStagedCell;
68
- completionFunc(cellId, stagedAiCell, removeStagedCell, deleteStagedCell);
69
- };
70
-
71
- const tooltipContent =
72
- stagedAiCell.type === "add_cell"
73
- ? "AI-generated cell"
74
- : isDeletion
75
- ? "AI suggests deleting this cell"
76
- : "AI changed this cell";
77
-
78
- return (
79
- <div className="mo-ai-cell-footer">
80
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
81
- <Tooltip content={tooltipContent}>
82
- <span
83
- className="inline-flex items-center gap-1.5"
84
- aria-label={tooltipContent}
85
- >
86
- <SparklesIcon
87
- aria-hidden="true"
88
- className="size-3.5 text-(--blue-11)"
89
- />
90
- <span>AI</span>
91
- </span>
92
- </Tooltip>
93
- </div>
94
- <div className="flex items-center gap-1.5">
95
- <CompletionActionsCellFooter
96
- isLoading={isGenerating}
97
- onAccept={() => handleCompletion("accept")}
98
- onDecline={() => handleCompletion("reject")}
99
- size="xs"
100
- runCell={isDeletion ? undefined : runCell}
101
- acceptLabel={labels.accept}
102
- declineLabel={labels.decline}
103
- />
104
- </div>
105
- </div>
34
+ const edit = useStagedAICell(cellId);
35
+ // Every cell calls this; only cells with a pending edit care about the lock.
36
+ const isLocked = useAtomValue(
37
+ useMemo(
38
+ () =>
39
+ atom(
40
+ (get) =>
41
+ cellId !== undefined &&
42
+ get(stagedAICellsAtom).has(cellId) &&
43
+ get(chatGenerationInProgressAtom),
44
+ ),
45
+ [cellId],
46
+ ),
106
47
  );
107
- };
48
+ const { acceptStagedCell, rejectStagedCell } = useStagedCells();
108
49
 
109
- /**
110
- * Accept a staged cell and apply the changes.
111
- */
112
- export function acceptStagedCell(
113
- cellId: CellId,
114
- edit: Edit,
115
- removeStagedCell: (cellId: CellId) => void,
116
- deleteStagedCell: (cellId: CellId) => void,
117
- ): void {
118
- switch (edit.type) {
119
- case "delete_cell":
120
- // For delete cells, the cell is deleted when the completion is accepted
121
- deleteStagedCell(cellId);
122
- break;
123
- default:
124
- removeStagedCell(cellId);
125
- break;
50
+ if (!edit || cellId === undefined) {
51
+ return [];
126
52
  }
127
- }
128
-
129
- /**
130
- * Reject a staged cell and revert the changes.
131
- */
132
- export function rejectStagedCell(
133
- cellId: CellId,
134
- edit: Edit,
135
- removeStagedCell: (cellId: CellId) => void,
136
- deleteStagedCell: (cellId: CellId) => void,
137
- ): void {
138
- switch (edit.type) {
139
- case "update_cell": {
140
- // Revert cell code
141
- const editorView = getCellEditorView(cellId);
142
- if (!editorView) {
143
- Logger.error("Editor for this cell not found", { cellId });
144
- break;
145
- }
146
53
 
147
- const update = prepareCellCodeUpdate(
148
- editorView.state,
149
- edit.previousCode,
150
- { initialize: true },
151
- );
152
-
153
- if (update) {
154
- editorView.dispatch(update);
155
- }
156
- removeStagedCell(cellId);
157
- break;
158
- }
159
- case "add_cell":
160
- // Delete the cell since it's newly created
161
- deleteStagedCell(cellId);
162
- break;
163
- case "delete_cell":
164
- // Just remove the deletion marker - cell stays in notebook
165
- removeStagedCell(cellId);
166
- break;
167
- }
54
+ const labels = actionLabels[edit.type];
55
+ return [
56
+ {
57
+ label: labels.accept,
58
+ disabled: isLocked,
59
+ handle: () => {
60
+ if (!store.get(chatGenerationInProgressAtom)) {
61
+ acceptStagedCell(cellId, edit);
62
+ }
63
+ },
64
+ },
65
+ {
66
+ label: labels.decline,
67
+ disabled: isLocked,
68
+ handle: () => {
69
+ if (!store.get(chatGenerationInProgressAtom)) {
70
+ rejectStagedCell(cellId, edit);
71
+ }
72
+ },
73
+ },
74
+ ];
168
75
  }