@marimo-team/islands 0.25.3-dev266 → 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.
- package/dist/{ConnectedDataExplorerComponent-C_hj7Poi.js → ConnectedDataExplorerComponent-C4yCN38h.js} +1 -1
- package/dist/{ErrorBoundary-CvxlC3F1.js → ErrorBoundary-CewXPyL5.js} +64 -69
- package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-o6Ay6Q63.js} +2 -2
- package/dist/{any-language-editor-CFeWfC3o.js → any-language-editor-DnG26QXh.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-iF_snyFL.js → architectureDiagram-5GKGNRK7-BhiwwlVg.js} +1 -1
- package/dist/{chat-ui-DFxNgzXF.js → chat-ui-CbgR5ivy.js} +1964 -1740
- package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-WMSMf4Nj.js} +1 -1
- package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-BRfADrzD.js} +1 -1
- package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CuK5QFre.js} +1 -1
- package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-Bcun_jhJ.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-CgWDbUDT.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-DYmc9SFU.js} +2 -2
- package/dist/{code-visibility-SRM1JiD_.js → code-visibility-BKBl9Rh6.js} +9 -9
- package/dist/{common-yiRhZ6ZZ.js → common-N3Jd_6m9.js} +5944 -5626
- package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-PPKWipjT.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-niRBZh--.js} +1 -1
- package/dist/{data-grid-overlay-editor-DLTzcQ4O.js → data-grid-overlay-editor-Cp59CrqZ.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-DPiP9rot.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-B0WZuaxV.js} +1 -1
- package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-Rn42WBuN.js} +1 -1
- package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-BB93--ry.js} +1 -1
- package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-BpImEfn3.js} +1211 -1205
- package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-V4t5h6mZ.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-C1MsAKXd.js} +1 -1
- package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
- package/dist/{error-entries-CMoaUX9V.js → error-entries-CJGdD2ky.js} +49 -42
- package/dist/{feedback-button-DBUrE4OL.js → feedback-button-Qc1f9iPq.js} +70 -70
- package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-DHjeJP96.js} +2 -2
- package/dist/{glide-data-editor-Cfv4HeOh.js → glide-data-editor-hCq3K0sd.js} +2042 -2041
- package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-DC3sk6EX.js} +28 -34
- package/dist/{globals-dIbBIR5-.js → globals-B2_uMYDY.js} +1 -1
- package/dist/{html-to-image-D_5CMreP.js → html-to-image-S7spRQCi.js} +5526 -5828
- package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-Dovv6iKx.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-D2nyygsc.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-KyVx0uTY.js} +1 -1
- package/dist/main.js +1288 -1287
- package/dist/{mermaid-DFYN_PXd.js → mermaid-B-jKSwGt.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-Bz9tKQFl.js} +1 -1
- package/dist/{number-overlay-editor-CTiJp5Mj.js → number-overlay-editor-CfNYvbdz.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-C_qpq6Tv.js} +2 -2
- package/dist/pencil-DcT8AE0h.js +10 -0
- package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-Bp9ucgI4.js} +1 -1
- package/dist/{process-output-CGFNVC60.js → process-output-f34bkXTm.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-tq-VEH6j.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-DW-vHadb.js} +1 -1
- package/dist/{reveal-component-DH09ygHo.js → reveal-component-CUxv-kkP.js} +9 -9
- package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-Dv9U9qRP.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-DTmPQ0EB.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-CkCif0Iw.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-21kCq6l8.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BpRJP6KU.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-Ckgh4lS4.js} +1 -1
- package/dist/{toast-CY1_Qv41.js → toast-DpczUryP.js} +1 -1
- package/dist/{vega-component-C-lqjV0q.js → vega-component-CF9kI6C4.js} +1 -1
- package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-CDei2Bt3.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-CXavT601.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-CvJhcZri.js} +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
- package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
- package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
- package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
- package/src/components/chat/__tests__/user-message.test.tsx +27 -0
- package/src/components/chat/acp/agent-panel.tsx +2 -0
- package/src/components/chat/agent-activity.tsx +132 -0
- package/src/components/chat/chat-display.tsx +125 -96
- package/src/components/chat/chat-panel.tsx +123 -106
- package/src/components/chat/chat-utils.ts +12 -24
- package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
- package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
- package/src/components/chat/tool-call/shared.ts +6 -0
- package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
- package/src/components/chat/tool-call/tool-labels.ts +172 -0
- package/src/components/chat/tool-call/tool-step.tsx +145 -0
- package/src/components/chat/turn-timing.ts +73 -0
- package/src/components/chat/use-assistant-turn.ts +65 -0
- package/src/components/chat/use-chat-generation.ts +5 -4
- package/src/components/chat/user-message.tsx +145 -0
- package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +18 -1
- package/src/components/editor/actions/useCellActionButton.tsx +3 -0
- package/src/components/editor/ai/completion-handlers.tsx +0 -37
- package/src/components/editor/cell/StagedAICell.tsx +51 -144
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
- package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +6 -0
- package/src/components/editor/links/cell-link.tsx +4 -4
- package/src/components/editor/notebook-cell.tsx +3 -11
- package/src/components/ui/toast.tsx +1 -1
- package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
- package/src/core/ai/staged-cells.ts +48 -38
- package/src/core/cells/scrollCellIntoView.ts +6 -0
- package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
- package/src/core/codemirror/placeholder/extensions.ts +23 -0
- package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
- package/src/core/session/useNotebookSession.tsx +7 -2
- package/src/css/app/Cell.css +7 -36
- package/src/plugins/impl/chat/chat-ui.tsx +2 -3
- package/src/plugins/impl/common/error-banner.tsx +5 -3
- package/src/components/chat/reasoning-accordion.tsx +0 -61
- package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
- package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
- package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
- package/src/components/icons/loading-ellipsis.tsx +0 -46
|
@@ -4,16 +4,23 @@ 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
6
|
import type { ChatStatus } from "ai";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isChatBusy,
|
|
9
|
+
useChatGeneration,
|
|
10
|
+
} from "@/components/chat/use-chat-generation";
|
|
8
11
|
import { cellId } from "@/__tests__/branded";
|
|
12
|
+
import { HTMLCellId } from "@/core/cells/ids";
|
|
13
|
+
import { scrollToCell } from "@/core/cells/scrollCellIntoView";
|
|
9
14
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
10
15
|
import { stagedAICellsAtom } from "@/core/ai/staged-cells";
|
|
11
|
-
import { scrollAndHighlightCell } from "../../../links/cell-link";
|
|
12
|
-
import { StagedAICellFooter } from "../../../cell/StagedAICell";
|
|
13
16
|
import { PendingAICells } from "../pending-ai-cells";
|
|
17
|
+
import { PendingAIChangesBadge } from "../pending-ai-changes-badge";
|
|
18
|
+
|
|
19
|
+
const deleteCell = vi.hoisted(() => vi.fn());
|
|
20
|
+
const setCellCode = vi.hoisted(() => vi.fn());
|
|
14
21
|
|
|
15
22
|
vi.mock("@/components/editor/cell/useDeleteCell", () => ({
|
|
16
|
-
useDeleteCellCallback: () =>
|
|
23
|
+
useDeleteCellCallback: () => deleteCell,
|
|
17
24
|
}));
|
|
18
25
|
|
|
19
26
|
vi.mock("@/core/cells/cells", () => ({
|
|
@@ -22,7 +29,7 @@ vi.mock("@/core/cells/cells", () => ({
|
|
|
22
29
|
vi.mock("@/core/cells/actions", () => ({
|
|
23
30
|
useCellActions: () => ({
|
|
24
31
|
createCell: vi.fn(),
|
|
25
|
-
setCellCode
|
|
32
|
+
setCellCode,
|
|
26
33
|
}),
|
|
27
34
|
}));
|
|
28
35
|
|
|
@@ -31,8 +38,8 @@ vi.mock("@/components/editor/cell/useRunCells", () => ({
|
|
|
31
38
|
useRunCell: () => vi.fn(),
|
|
32
39
|
}));
|
|
33
40
|
|
|
34
|
-
vi.mock("@/
|
|
35
|
-
|
|
41
|
+
vi.mock("@/core/cells/scrollCellIntoView", () => ({
|
|
42
|
+
scrollToCell: vi.fn(),
|
|
36
43
|
}));
|
|
37
44
|
|
|
38
45
|
describe("PendingAICells", () => {
|
|
@@ -63,60 +70,60 @@ describe("PendingAICells", () => {
|
|
|
63
70
|
name: "Previous pending AI cell",
|
|
64
71
|
});
|
|
65
72
|
|
|
66
|
-
fireEvent.click(
|
|
67
|
-
expect(
|
|
68
|
-
expect(screen.getByText("1
|
|
73
|
+
fireEvent.click(screen.getByRole("button", { name: "Cell 1 of 3" }));
|
|
74
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(first);
|
|
75
|
+
expect(screen.getByText("Cell 1 of 3")).toBeInTheDocument();
|
|
69
76
|
|
|
70
77
|
fireEvent.click(next);
|
|
71
|
-
expect(
|
|
72
|
-
expect(screen.getByText("2
|
|
78
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(second);
|
|
79
|
+
expect(screen.getByText("Cell 2 of 3")).toBeInTheDocument();
|
|
73
80
|
|
|
74
81
|
fireEvent.click(previous);
|
|
75
|
-
expect(
|
|
76
|
-
expect(screen.getByText("1
|
|
82
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(first);
|
|
83
|
+
expect(screen.getByText("Cell 1 of 3")).toBeInTheDocument();
|
|
77
84
|
|
|
78
85
|
fireEvent.click(next);
|
|
79
|
-
expect(
|
|
80
|
-
expect(screen.getByText("2
|
|
86
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(second);
|
|
87
|
+
expect(screen.getByText("Cell 2 of 3")).toBeInTheDocument();
|
|
81
88
|
|
|
82
89
|
fireEvent.click(next);
|
|
83
|
-
expect(
|
|
84
|
-
expect(screen.getByText("3
|
|
90
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(third);
|
|
91
|
+
expect(screen.getByText("Cell 3 of 3")).toBeInTheDocument();
|
|
85
92
|
|
|
86
93
|
fireEvent.click(next);
|
|
87
|
-
expect(
|
|
88
|
-
expect(screen.getByText("1
|
|
94
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(first);
|
|
95
|
+
expect(screen.getByText("Cell 1 of 3")).toBeInTheDocument();
|
|
89
96
|
|
|
90
97
|
fireEvent.click(previous);
|
|
91
|
-
expect(
|
|
92
|
-
expect(screen.getByText("3
|
|
98
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(third);
|
|
99
|
+
expect(screen.getByText("Cell 3 of 3")).toBeInTheDocument();
|
|
93
100
|
});
|
|
94
101
|
});
|
|
95
102
|
|
|
96
103
|
function ChatGeneration({ status }: { status: ChatStatus }) {
|
|
97
|
-
useChatGeneration(status);
|
|
104
|
+
useChatGeneration(isChatBusy(status));
|
|
98
105
|
return null;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
it("locks staged edits during generation and unlocks after completion, stop, error, or unmount", () => {
|
|
102
109
|
const store = createStore();
|
|
103
110
|
const pendingCell = cellId("pending-cell");
|
|
104
|
-
store.set(
|
|
111
|
+
store.set(
|
|
112
|
+
stagedAICellsAtom,
|
|
113
|
+
new Map([
|
|
114
|
+
[pendingCell, { type: "add_cell" }],
|
|
115
|
+
[cellId("other-pending-cell"), { type: "add_cell" }],
|
|
116
|
+
]),
|
|
117
|
+
);
|
|
105
118
|
const ui = (status?: ChatStatus) => (
|
|
106
119
|
<Provider store={store}>
|
|
107
120
|
<TooltipProvider>
|
|
108
121
|
{status && <ChatGeneration status={status} />}
|
|
109
122
|
<PendingAICells />
|
|
110
|
-
<StagedAICellFooter cellId={pendingCell} />
|
|
111
123
|
</TooltipProvider>
|
|
112
124
|
</Provider>
|
|
113
125
|
);
|
|
114
126
|
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
127
|
for (const status of [
|
|
121
128
|
"submitted",
|
|
122
129
|
"streaming",
|
|
@@ -130,15 +137,169 @@ it("locks staged edits during generation and unlocks after completion, stop, err
|
|
|
130
137
|
] as const) {
|
|
131
138
|
rerender(ui(status));
|
|
132
139
|
const isGenerating = status === "submitted" || status === "streaming";
|
|
133
|
-
|
|
140
|
+
const keep = screen.getByRole("button", { name: "Keep all" });
|
|
141
|
+
expect(keep.hasAttribute("disabled")).toBe(isGenerating);
|
|
142
|
+
expect(
|
|
143
|
+
screen.queryByRole("button", { name: "Keep all" }),
|
|
144
|
+
).toBeInTheDocument();
|
|
145
|
+
const keepCell = screen.getByRole("button", { name: "Keep" });
|
|
146
|
+
const discardCell = screen.getByRole("button", { name: "Discard" });
|
|
147
|
+
for (const button of [keepCell, discardCell]) {
|
|
134
148
|
expect(button.hasAttribute("disabled")).toBe(isGenerating);
|
|
135
149
|
if (isGenerating) {
|
|
136
150
|
fireEvent.click(button);
|
|
137
151
|
}
|
|
138
152
|
}
|
|
139
153
|
expect(store.get(stagedAICellsAtom).has(pendingCell)).toBe(true);
|
|
154
|
+
expect(screen.queryByRole("button", { name: "Keep" })).toBeInTheDocument();
|
|
140
155
|
}
|
|
141
156
|
|
|
142
|
-
fireEvent.click(
|
|
157
|
+
fireEvent.click(screen.getByRole("button", { name: "Keep all" }));
|
|
143
158
|
expect(store.get(stagedAICellsAtom).size).toBe(0);
|
|
144
159
|
});
|
|
160
|
+
|
|
161
|
+
it.each(["Delete cell", "Keep cell"])(
|
|
162
|
+
"resolves a proposed deletion with %s and advances review",
|
|
163
|
+
(label) => {
|
|
164
|
+
deleteCell.mockClear();
|
|
165
|
+
const store = createStore();
|
|
166
|
+
const first = cellId("deleted-cell");
|
|
167
|
+
const second = cellId("added-cell");
|
|
168
|
+
store.set(
|
|
169
|
+
stagedAICellsAtom,
|
|
170
|
+
new Map([
|
|
171
|
+
[first, { type: "delete_cell", previousCode: "x = 1" }],
|
|
172
|
+
[second, { type: "add_cell" }],
|
|
173
|
+
]),
|
|
174
|
+
);
|
|
175
|
+
render(
|
|
176
|
+
<Provider store={store}>
|
|
177
|
+
<PendingAICells />
|
|
178
|
+
</Provider>,
|
|
179
|
+
);
|
|
180
|
+
fireEvent.click(screen.getByRole("button", { name: label }));
|
|
181
|
+
expect(store.get(stagedAICellsAtom).has(first)).toBe(false);
|
|
182
|
+
expect(deleteCell).toHaveBeenCalledTimes(label === "Delete cell" ? 1 : 0);
|
|
183
|
+
expect(scrollToCell).toHaveBeenLastCalledWith(second);
|
|
184
|
+
expect(screen.getByText("1 cell changed")).toBeInTheDocument();
|
|
185
|
+
expect(
|
|
186
|
+
screen.queryByRole("button", { name: "Keep all" }),
|
|
187
|
+
).not.toBeInTheDocument();
|
|
188
|
+
expect(
|
|
189
|
+
screen.queryByRole("button", { name: "Discard all" }),
|
|
190
|
+
).not.toBeInTheDocument();
|
|
191
|
+
fireEvent.click(screen.getByRole("button", { name: "Keep" }));
|
|
192
|
+
expect(
|
|
193
|
+
screen.queryByRole("region", { name: "Review AI changes" }),
|
|
194
|
+
).not.toBeInTheDocument();
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
it("keeps batch discard visible and locks it during generation", () => {
|
|
199
|
+
deleteCell.mockClear();
|
|
200
|
+
const store = createStore();
|
|
201
|
+
const pendingCell = cellId("discarded-cell");
|
|
202
|
+
store.set(
|
|
203
|
+
stagedAICellsAtom,
|
|
204
|
+
new Map([
|
|
205
|
+
[pendingCell, { type: "add_cell" }],
|
|
206
|
+
[cellId("other-pending-cell"), { type: "add_cell" }],
|
|
207
|
+
]),
|
|
208
|
+
);
|
|
209
|
+
const ui = (status: ChatStatus) => (
|
|
210
|
+
<Provider store={store}>
|
|
211
|
+
<ChatGeneration status={status} />
|
|
212
|
+
<PendingAICells />
|
|
213
|
+
</Provider>
|
|
214
|
+
);
|
|
215
|
+
const { rerender } = render(ui("streaming"));
|
|
216
|
+
const discard = screen.getByRole("button", { name: "Discard all" });
|
|
217
|
+
expect(discard).toBeVisible();
|
|
218
|
+
expect(discard).toBeDisabled();
|
|
219
|
+
fireEvent.click(discard);
|
|
220
|
+
expect(store.get(stagedAICellsAtom).has(pendingCell)).toBe(true);
|
|
221
|
+
rerender(ui("ready"));
|
|
222
|
+
expect(discard).toBeEnabled();
|
|
223
|
+
fireEvent.click(discard);
|
|
224
|
+
expect(deleteCell).toHaveBeenCalledTimes(2);
|
|
225
|
+
expect(deleteCell).toHaveBeenCalledWith({ cellId: pendingCell });
|
|
226
|
+
expect(store.get(stagedAICellsAtom).size).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("clears the panel badge when all pending changes are accepted", () => {
|
|
230
|
+
const store = createStore();
|
|
231
|
+
store.set(
|
|
232
|
+
stagedAICellsAtom,
|
|
233
|
+
new Map([[cellId("pending"), { type: "add_cell" }]]),
|
|
234
|
+
);
|
|
235
|
+
render(
|
|
236
|
+
<Provider store={store}>
|
|
237
|
+
<PendingAIChangesBadge />
|
|
238
|
+
<PendingAICells />
|
|
239
|
+
</Provider>,
|
|
240
|
+
);
|
|
241
|
+
expect(screen.getByText("1 pending AI change")).toBeInTheDocument();
|
|
242
|
+
fireEvent.click(screen.getByRole("button", { name: "Keep" }));
|
|
243
|
+
expect(screen.queryByText("1 pending AI change")).not.toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("discards an edit to a cell whose editor is not mounted", () => {
|
|
247
|
+
setCellCode.mockClear();
|
|
248
|
+
const store = createStore();
|
|
249
|
+
const collapsed = cellId("collapsed-cell");
|
|
250
|
+
store.set(
|
|
251
|
+
stagedAICellsAtom,
|
|
252
|
+
new Map([[collapsed, { type: "update_cell", previousCode: "x = 1" }]]),
|
|
253
|
+
);
|
|
254
|
+
render(
|
|
255
|
+
<Provider store={store}>
|
|
256
|
+
<PendingAICells />
|
|
257
|
+
</Provider>,
|
|
258
|
+
);
|
|
259
|
+
fireEvent.click(screen.getByRole("button", { name: "Discard" }));
|
|
260
|
+
expect(setCellCode).toHaveBeenCalledWith({
|
|
261
|
+
cellId: collapsed,
|
|
262
|
+
code: "x = 1",
|
|
263
|
+
});
|
|
264
|
+
expect(store.get(stagedAICellsAtom).size).toBe(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("outlines only the reviewed cell, moving the outline with the selection", () => {
|
|
268
|
+
const store = createStore();
|
|
269
|
+
const first = cellId("outlined-first");
|
|
270
|
+
const second = cellId("outlined-second");
|
|
271
|
+
store.set(
|
|
272
|
+
stagedAICellsAtom,
|
|
273
|
+
new Map([
|
|
274
|
+
[first, { type: "add_cell" }],
|
|
275
|
+
[second, { type: "add_cell" }],
|
|
276
|
+
]),
|
|
277
|
+
);
|
|
278
|
+
const cells = [first, second].map((id) => {
|
|
279
|
+
const element = document.createElement("div");
|
|
280
|
+
element.id = HTMLCellId.create(id);
|
|
281
|
+
document.body.append(element);
|
|
282
|
+
return element;
|
|
283
|
+
});
|
|
284
|
+
const { unmount } = render(
|
|
285
|
+
<Provider store={store}>
|
|
286
|
+
<TooltipProvider>
|
|
287
|
+
<PendingAICells />
|
|
288
|
+
</TooltipProvider>
|
|
289
|
+
</Provider>,
|
|
290
|
+
);
|
|
291
|
+
const outlined = () =>
|
|
292
|
+
cells.map((cell) => cell.hasAttribute("data-ai-review-selected"));
|
|
293
|
+
|
|
294
|
+
expect(outlined()).toEqual([false, false]);
|
|
295
|
+
fireEvent.click(screen.getByRole("button", { name: "Cell 1 of 2" }));
|
|
296
|
+
expect(outlined()).toEqual([true, false]);
|
|
297
|
+
fireEvent.click(screen.getByRole("button", { name: "Next pending AI cell" }));
|
|
298
|
+
expect(outlined()).toEqual([false, true]);
|
|
299
|
+
|
|
300
|
+
unmount();
|
|
301
|
+
expect(outlined()).toEqual([false, false]);
|
|
302
|
+
for (const cell of cells) {
|
|
303
|
+
cell.remove();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
import { BackendConnectionStatus } from "./footer-items/backend-status";
|
|
69
69
|
import { LspStatus } from "./footer-items/lsp-status";
|
|
70
70
|
import { PanelsWrapper } from "./panels";
|
|
71
|
-
import {
|
|
71
|
+
import { PendingAIChangesBadge } from "./pending-ai-changes-badge";
|
|
72
72
|
import { useAiPanelTab } from "./useAiPanel";
|
|
73
73
|
import { useDependencyPanelTab } from "./useDependencyPanelTab";
|
|
74
74
|
import { handleDragging } from "./utils";
|
|
@@ -558,6 +558,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
558
558
|
)}
|
|
559
559
|
/>
|
|
560
560
|
{panel.label}
|
|
561
|
+
{panel.type === "ai" && <PendingAIChangesBadge />}
|
|
561
562
|
</div>
|
|
562
563
|
)}
|
|
563
564
|
/>
|
|
@@ -618,7 +619,6 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
618
619
|
</Panel>
|
|
619
620
|
<ContextAwarePanel />
|
|
620
621
|
</PanelGroup>
|
|
621
|
-
<PendingAICells />
|
|
622
622
|
<ErrorBoundary>
|
|
623
623
|
<TooltipProvider>
|
|
624
624
|
<Footer />
|
|
@@ -1,38 +1,47 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { useAtomValue, useStore } from "jotai";
|
|
4
|
-
import {
|
|
5
|
-
import { useState } from "react";
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type Edit,
|
|
9
|
+
stagedAICellsAtom,
|
|
10
|
+
useStagedCells,
|
|
11
|
+
} from "@/core/ai/staged-cells";
|
|
8
12
|
import { chatGenerationInProgressAtom } from "@/core/ai/state";
|
|
9
|
-
import type
|
|
13
|
+
import { type CellId, HTMLCellId } from "@/core/cells/ids";
|
|
14
|
+
import { scrollToCell } from "@/core/cells/scrollCellIntoView";
|
|
10
15
|
import { getNextIndex } from "@/utils/arrays";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import { scrollAndHighlightCell } from "../../links/cell-link";
|
|
16
|
+
|
|
17
|
+
const REVIEW_LABELS: Record<Edit["type"], { accept: string; reject: string }> =
|
|
18
|
+
{
|
|
19
|
+
add_cell: { accept: "Keep", reject: "Discard" },
|
|
20
|
+
update_cell: { accept: "Keep", reject: "Discard" },
|
|
21
|
+
delete_cell: { accept: "Delete cell", reject: "Keep cell" },
|
|
22
|
+
};
|
|
19
23
|
|
|
20
24
|
export const PendingAICells: React.FC = () => {
|
|
21
|
-
const [
|
|
25
|
+
const [selectedCellId, setSelectedCellId] = useState<CellId | null>(null);
|
|
22
26
|
|
|
23
27
|
const stagedAiCells = useAtomValue(stagedAICellsAtom);
|
|
24
28
|
const isGenerating = useAtomValue(chatGenerationInProgressAtom);
|
|
25
29
|
const listStagedCells = [...stagedAiCells.keys()];
|
|
30
|
+
const selectedIndex =
|
|
31
|
+
selectedCellId === null ? -1 : listStagedCells.indexOf(selectedCellId);
|
|
32
|
+
const currentIndex = Math.max(0, selectedIndex);
|
|
26
33
|
const store = useStore();
|
|
27
|
-
const {
|
|
28
|
-
const
|
|
34
|
+
const { acceptStagedCell, rejectStagedCell } = useStagedCells();
|
|
35
|
+
const reviewedCellId = selectedIndex === -1 ? null : selectedCellId;
|
|
36
|
+
useReviewedCellMarker(reviewedCellId);
|
|
29
37
|
|
|
30
38
|
if (stagedAiCells.size === 0) {
|
|
31
39
|
return null;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
const
|
|
35
|
-
|
|
42
|
+
const selectCell = (cellId: CellId) => {
|
|
43
|
+
setSelectedCellId(cellId);
|
|
44
|
+
scrollToCell(cellId);
|
|
36
45
|
};
|
|
37
46
|
|
|
38
47
|
const clickNext = (direction: "up" | "down") => {
|
|
@@ -41,86 +50,141 @@ export const PendingAICells: React.FC = () => {
|
|
|
41
50
|
listStagedCells.length,
|
|
42
51
|
direction,
|
|
43
52
|
);
|
|
44
|
-
|
|
45
|
-
scrollToCell(listStagedCells[newIndex]);
|
|
53
|
+
selectCell(listStagedCells[newIndex]);
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
const acceptAllCompletions = () => {
|
|
49
57
|
for (const [cellId, edit] of stagedAiCells) {
|
|
50
|
-
acceptStagedCell(cellId, edit
|
|
58
|
+
acceptStagedCell(cellId, edit);
|
|
51
59
|
}
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
const rejectAllCompletions = () => {
|
|
55
63
|
for (const [cellId, edit] of stagedAiCells) {
|
|
56
|
-
rejectStagedCell(cellId, edit
|
|
64
|
+
rejectStagedCell(cellId, edit);
|
|
57
65
|
}
|
|
58
66
|
};
|
|
59
67
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
runCell(cellsToRun);
|
|
65
|
-
};
|
|
68
|
+
const selectedCell = listStagedCells[currentIndex];
|
|
69
|
+
const selectedEdit = stagedAiCells.get(selectedCell);
|
|
70
|
+
const hasMultipleCells = listStagedCells.length > 1;
|
|
66
71
|
|
|
67
|
-
const
|
|
72
|
+
const resolveSelected = (decision: "accept" | "reject") => {
|
|
73
|
+
if (!selectedEdit || store.get(chatGenerationInProgressAtom)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (decision === "accept") {
|
|
77
|
+
acceptStagedCell(selectedCell, selectedEdit);
|
|
78
|
+
} else {
|
|
79
|
+
rejectStagedCell(selectedCell, selectedEdit);
|
|
80
|
+
}
|
|
81
|
+
if (hasMultipleCells) {
|
|
82
|
+
clickNext("up");
|
|
83
|
+
} else {
|
|
84
|
+
setSelectedCellId(null);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
68
87
|
|
|
69
88
|
return (
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
cyanShadow,
|
|
74
|
-
)}
|
|
89
|
+
<section
|
|
90
|
+
aria-label="Review AI changes"
|
|
91
|
+
className="@container/review shrink-0 border-t border-border bg-background px-3 py-2 text-sm"
|
|
75
92
|
>
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
93
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
94
|
+
{hasMultipleCells && (
|
|
95
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
96
|
+
<span className="whitespace-nowrap text-muted-foreground">
|
|
97
|
+
{listStagedCells.length} cells changed
|
|
98
|
+
</span>
|
|
99
|
+
<Button
|
|
100
|
+
variant="outline"
|
|
101
|
+
size="sm"
|
|
102
|
+
className="chat-control chat-selector h-7 px-2 rounded-sm font-normal"
|
|
103
|
+
onClick={acceptAllCompletions}
|
|
104
|
+
disabled={isGenerating}
|
|
105
|
+
>
|
|
106
|
+
Keep all
|
|
107
|
+
</Button>
|
|
108
|
+
<Button
|
|
109
|
+
variant="outline"
|
|
110
|
+
size="sm"
|
|
111
|
+
className="chat-control chat-selector h-7 px-2 rounded-sm font-normal"
|
|
112
|
+
onClick={rejectAllCompletions}
|
|
113
|
+
disabled={isGenerating}
|
|
114
|
+
>
|
|
115
|
+
Discard all
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
120
|
+
{hasMultipleCells && (
|
|
121
|
+
<span
|
|
122
|
+
aria-hidden="true"
|
|
123
|
+
className="hidden h-4 w-px bg-border @min-[600px]/review:block"
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
<div className="flex h-7 w-fit items-center">
|
|
127
|
+
{hasMultipleCells && (
|
|
128
|
+
<Button
|
|
129
|
+
aria-label="Previous pending AI cell"
|
|
130
|
+
variant="ghost"
|
|
131
|
+
size="icon"
|
|
132
|
+
className="chat-control size-6 rounded-sm"
|
|
133
|
+
onClick={() => clickNext("down")}
|
|
134
|
+
>
|
|
135
|
+
<ChevronLeft className="size-4" />
|
|
136
|
+
</Button>
|
|
137
|
+
)}
|
|
138
|
+
<Button
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="sm"
|
|
141
|
+
className="chat-control h-6 px-1 rounded-sm font-normal tabular-nums whitespace-nowrap"
|
|
142
|
+
onClick={() => selectCell(selectedCell)}
|
|
143
|
+
>
|
|
144
|
+
<span aria-live="polite">
|
|
145
|
+
{hasMultipleCells
|
|
146
|
+
? `Cell ${currentIndex + 1} of ${listStagedCells.length}`
|
|
147
|
+
: "1 cell changed"}
|
|
148
|
+
</span>
|
|
149
|
+
</Button>
|
|
150
|
+
{hasMultipleCells && (
|
|
151
|
+
<Button
|
|
152
|
+
aria-label="Next pending AI cell"
|
|
153
|
+
variant="ghost"
|
|
154
|
+
size="icon"
|
|
155
|
+
className="chat-control size-6 rounded-sm"
|
|
156
|
+
onClick={() => clickNext("up")}
|
|
157
|
+
>
|
|
158
|
+
<ChevronRight className="size-4" />
|
|
159
|
+
</Button>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
{selectedEdit &&
|
|
163
|
+
(["accept", "reject"] as const).map((decision) => (
|
|
164
|
+
<Button
|
|
165
|
+
key={decision}
|
|
166
|
+
variant="outline"
|
|
167
|
+
size="sm"
|
|
168
|
+
className="chat-control chat-selector h-7 px-2 rounded-sm font-normal"
|
|
169
|
+
disabled={isGenerating}
|
|
170
|
+
onClick={() => resolveSelected(decision)}
|
|
171
|
+
>
|
|
172
|
+
{REVIEW_LABELS[selectedEdit.type][decision]}
|
|
173
|
+
</Button>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
123
176
|
</div>
|
|
124
|
-
</
|
|
177
|
+
</section>
|
|
125
178
|
);
|
|
126
179
|
};
|
|
180
|
+
|
|
181
|
+
function useReviewedCellMarker(cellId: CellId | null) {
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (cellId === null) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const cell = document.getElementById(HTMLCellId.create(cellId));
|
|
187
|
+
cell?.setAttribute("data-ai-review-selected", "");
|
|
188
|
+
return () => cell?.removeAttribute("data-ai-review-selected");
|
|
189
|
+
}, [cellId]);
|
|
190
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import { stagedAICellsAtom } from "@/core/ai/staged-cells";
|
|
5
|
+
import { cn } from "@/utils/cn";
|
|
6
|
+
|
|
7
|
+
export function PendingAIChangesBadge({ className }: { className?: string }) {
|
|
8
|
+
const count = useAtomValue(stagedAICellsAtom).size;
|
|
9
|
+
if (count === 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
<span
|
|
14
|
+
className={cn(
|
|
15
|
+
"inline-flex h-4 min-w-4 items-center justify-center rounded-full border border-border bg-muted px-1 text-[10px] leading-none tabular-nums text-foreground",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
>
|
|
19
|
+
<span aria-hidden={true}>{count}</span>
|
|
20
|
+
<span className="sr-only">
|
|
21
|
+
{count} pending AI {count === 1 ? "change" : "changes"}
|
|
22
|
+
</span>
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
PANELS,
|
|
23
23
|
type PanelDescriptor,
|
|
24
24
|
} from "../types";
|
|
25
|
+
import { PendingAIChangesBadge } from "./pending-ai-changes-badge";
|
|
25
26
|
import { PANEL_PRELOADERS } from "./lazy-panels";
|
|
26
27
|
|
|
27
28
|
export const Sidebar: React.FC = () => {
|
|
@@ -150,6 +151,11 @@ export const Sidebar: React.FC = () => {
|
|
|
150
151
|
>
|
|
151
152
|
{panel.type === "errors" ? (
|
|
152
153
|
<ErrorPanelIcon Icon={panel.Icon} />
|
|
154
|
+
) : panel.type === "ai" ? (
|
|
155
|
+
<span className="relative">
|
|
156
|
+
{renderIcon(panel)}
|
|
157
|
+
<PendingAIChangesBadge className="absolute -top-2 -right-2" />
|
|
158
|
+
</span>
|
|
153
159
|
) : (
|
|
154
160
|
renderIcon(panel)
|
|
155
161
|
)}
|