@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
|
@@ -5,6 +5,7 @@ import { useCellIds, useCellNames } from "@/core/cells/cells";
|
|
|
5
5
|
import { useCellActions } from "@/core/cells/actions";
|
|
6
6
|
import { type CellId, HTMLCellId, SCRATCH_CELL_ID } from "@/core/cells/ids";
|
|
7
7
|
import { displayCellName } from "@/core/cells/names";
|
|
8
|
+
import { scrollToCell } from "@/core/cells/scrollCellIntoView";
|
|
8
9
|
import { goToCellLine } from "@/core/codemirror/go-to-definition/utils";
|
|
9
10
|
import { useFilename } from "@/core/saving/filename";
|
|
10
11
|
import { cn } from "@/utils/cn";
|
|
@@ -106,16 +107,15 @@ export function scrollAndHighlightCell(
|
|
|
106
107
|
skipScroll?: boolean,
|
|
107
108
|
): boolean {
|
|
108
109
|
const cellHtmlId = HTMLCellId.create(cellId);
|
|
109
|
-
const cell
|
|
110
|
+
const cell = skipScroll
|
|
111
|
+
? document.getElementById(cellHtmlId)
|
|
112
|
+
: scrollToCell(cellId);
|
|
110
113
|
const isCellErrored = cell?.classList.contains("has-error");
|
|
111
114
|
|
|
112
115
|
if (cell === null) {
|
|
113
116
|
Logger.error(`Cell ${cellHtmlId} not found on page.`);
|
|
114
117
|
return false;
|
|
115
118
|
}
|
|
116
|
-
if (!skipScroll) {
|
|
117
|
-
cell.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
118
|
-
}
|
|
119
119
|
|
|
120
120
|
if (variant === "destructive" || (isCellErrored && variant === undefined)) {
|
|
121
121
|
cell.classList.add("error-outline");
|
|
@@ -78,10 +78,7 @@ import { CollapsedCellBanner, CollapseToggle } from "./cell/collapse";
|
|
|
78
78
|
import { DeleteButton } from "./cell/DeleteButton";
|
|
79
79
|
import { PendingDeleteConfirmation } from "./cell/PendingDeleteConfirmation";
|
|
80
80
|
import { RunButton } from "./cell/RunButton";
|
|
81
|
-
import {
|
|
82
|
-
StagedAICellBackground,
|
|
83
|
-
StagedAICellFooter,
|
|
84
|
-
} from "./cell/StagedAICell";
|
|
81
|
+
import { StagedAIDeletionMarker } from "./cell/StagedAICell";
|
|
85
82
|
import { useDeleteCellCallback } from "./cell/useDeleteCell";
|
|
86
83
|
import { useRunCell } from "./cell/useRunCells";
|
|
87
84
|
import { cellDomProps } from "./common";
|
|
@@ -674,7 +671,7 @@ const EditableCellComponent = ({
|
|
|
674
671
|
data-has-output-above={hasOutputAbove}
|
|
675
672
|
data-hidden={isMarkdownCodeHidden}
|
|
676
673
|
>
|
|
677
|
-
<
|
|
674
|
+
<StagedAIDeletionMarker cellId={cellId} />
|
|
678
675
|
<div className="absolute right-2 -top-4 z-10">
|
|
679
676
|
<CellToolbar
|
|
680
677
|
edited={cellData.edited}
|
|
@@ -837,7 +834,6 @@ const EditableCellComponent = ({
|
|
|
837
834
|
/>
|
|
838
835
|
)}
|
|
839
836
|
<PendingDeleteConfirmation cellId={cellId} />
|
|
840
|
-
{!isPresenting && <StagedAICellFooter cellId={cellId} />}
|
|
841
837
|
</div>
|
|
842
838
|
{isCollapsed && !isPresenting && (
|
|
843
839
|
<CollapsedCellBanner
|
|
@@ -1198,10 +1194,7 @@ const SetupCellComponent = ({
|
|
|
1198
1194
|
data-has-output-above={false}
|
|
1199
1195
|
data-hidden={!isCellCodeShown}
|
|
1200
1196
|
>
|
|
1201
|
-
<
|
|
1202
|
-
cellId={cellId}
|
|
1203
|
-
className="mo-ai-setup-cell"
|
|
1204
|
-
/>
|
|
1197
|
+
<StagedAIDeletionMarker cellId={cellId} />
|
|
1205
1198
|
<div className="absolute right-2 -top-4 z-10">
|
|
1206
1199
|
<CellToolbar
|
|
1207
1200
|
edited={cellData.edited}
|
|
@@ -1326,7 +1319,6 @@ const SetupCellComponent = ({
|
|
|
1326
1319
|
cellId={cellId}
|
|
1327
1320
|
debuggerActive={cellRuntime.debuggerActive}
|
|
1328
1321
|
/>
|
|
1329
|
-
<StagedAICellFooter cellId={cellId} />
|
|
1330
1322
|
</div>
|
|
1331
1323
|
</div>
|
|
1332
1324
|
</CellActionsContextMenu>
|
|
@@ -29,7 +29,7 @@ const VARIANT_CLASSES = {
|
|
|
29
29
|
default:
|
|
30
30
|
"bg-background border forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]",
|
|
31
31
|
danger:
|
|
32
|
-
"group destructive text-
|
|
32
|
+
"group destructive text-(--red-11) border-destructive bg-(--red-1) shadow-md-solid shadow-error forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]",
|
|
33
33
|
} as const satisfies Record<string, string>;
|
|
34
34
|
|
|
35
35
|
type ToastVariant = keyof typeof VARIANT_CLASSES;
|
|
@@ -5,8 +5,9 @@ import { getDefaultStore, Provider } from "jotai";
|
|
|
5
5
|
import { createElement, type ReactNode } from "react";
|
|
6
6
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { cellId } from "@/__tests__/branded";
|
|
8
|
-
import { CellId } from "@/core/cells/ids";
|
|
8
|
+
import type { CellId } from "@/core/cells/ids";
|
|
9
9
|
import {
|
|
10
|
+
type Edit,
|
|
10
11
|
stagedAICellsAtom,
|
|
11
12
|
useStagedAICell,
|
|
12
13
|
useStagedCells,
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
const { createActions, reducer, initialState } = visibleForTesting;
|
|
17
18
|
|
|
18
19
|
// Mock the dependencies
|
|
19
|
-
const mockCreateCell = vi.fn();
|
|
20
20
|
const mockSetCellCode = vi.fn();
|
|
21
21
|
const mockDeleteCellCallback = vi.fn();
|
|
22
22
|
|
|
@@ -31,7 +31,6 @@ const mockCellHandle = {
|
|
|
31
31
|
|
|
32
32
|
vi.mock("../../cells/actions", () => ({
|
|
33
33
|
useCellActions: () => ({
|
|
34
|
-
createCell: mockCreateCell,
|
|
35
34
|
setCellCode: mockSetCellCode,
|
|
36
35
|
}),
|
|
37
36
|
}));
|
|
@@ -55,13 +54,6 @@ vi.mock("../../codemirror/language/cell-code", () => ({
|
|
|
55
54
|
prepareCellCodeUpdate: vi.fn(() => ({ effects: [] })),
|
|
56
55
|
}));
|
|
57
56
|
|
|
58
|
-
// Mock CellId.create
|
|
59
|
-
vi.mock("@/core/cells/ids", () => ({
|
|
60
|
-
CellId: {
|
|
61
|
-
create: vi.fn(),
|
|
62
|
-
},
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
57
|
describe("staged-cells", () => {
|
|
66
58
|
let store: ReturnType<typeof getDefaultStore>;
|
|
67
59
|
let cellId1: CellId;
|
|
@@ -74,7 +66,6 @@ describe("staged-cells", () => {
|
|
|
74
66
|
|
|
75
67
|
// Reset mocks
|
|
76
68
|
vi.clearAllMocks();
|
|
77
|
-
vi.mocked(CellId.create).mockReset();
|
|
78
69
|
|
|
79
70
|
// Reset the atom state
|
|
80
71
|
store.set(stagedAICellsAtom, new Map());
|
|
@@ -143,6 +134,31 @@ describe("staged-cells", () => {
|
|
|
143
134
|
});
|
|
144
135
|
});
|
|
145
136
|
|
|
137
|
+
it("drops a replaced cell while preserving the four remaining pending edits", () => {
|
|
138
|
+
const original = cellId("original");
|
|
139
|
+
const remaining = [cellId("a"), cellId("b"), cellId("c"), cellId("d")];
|
|
140
|
+
const state = new Map<CellId, Edit>([
|
|
141
|
+
[original, { type: "add_cell" }],
|
|
142
|
+
[remaining[0], { type: "add_cell" }],
|
|
143
|
+
[remaining[1], { type: "add_cell" }],
|
|
144
|
+
[remaining[2], { type: "update_cell", previousCode: "x = 1" }],
|
|
145
|
+
[remaining[3], { type: "delete_cell", previousCode: "y = 2" }],
|
|
146
|
+
]);
|
|
147
|
+
const next = reducer(state, {
|
|
148
|
+
type: "reconcileStagedCells",
|
|
149
|
+
payload: remaining,
|
|
150
|
+
});
|
|
151
|
+
expect(next).toEqual(
|
|
152
|
+
new Map([...state].filter(([id]) => id !== original)),
|
|
153
|
+
);
|
|
154
|
+
expect(
|
|
155
|
+
reducer(next, { type: "reconcileStagedCells", payload: remaining }),
|
|
156
|
+
).toBe(next);
|
|
157
|
+
expect(
|
|
158
|
+
reducer(next, { type: "reconcileStagedCells", payload: [] }),
|
|
159
|
+
).toEqual(new Map());
|
|
160
|
+
});
|
|
161
|
+
|
|
146
162
|
it("should remove cell IDs", () => {
|
|
147
163
|
const state = new Map([
|
|
148
164
|
[cellId1, { type: "add_cell" as const }],
|
|
@@ -242,134 +258,66 @@ describe("staged-cells", () => {
|
|
|
242
258
|
});
|
|
243
259
|
});
|
|
244
260
|
|
|
245
|
-
describe("useStagedCells
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const testCode = "print('hello world')";
|
|
249
|
-
|
|
250
|
-
// Mock CellId.create to return a predictable ID
|
|
251
|
-
const mockCellId = cellId("mock-cell-id");
|
|
252
|
-
vi.mocked(CellId.create).mockReturnValue(mockCellId);
|
|
253
|
-
|
|
254
|
-
const returnedCellId = result.current.createStagedCell(testCode);
|
|
255
|
-
|
|
256
|
-
expect(returnedCellId).toBe(mockCellId);
|
|
257
|
-
expect(mockCreateCell).toHaveBeenCalledWith({
|
|
258
|
-
code: testCode,
|
|
259
|
-
id: mockCellId,
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("should delete a staged cell", () => {
|
|
264
|
-
const { result } = renderHook(() => useStagedCells(store));
|
|
265
|
-
const testCellId = cellId("test-cell-id");
|
|
266
|
-
|
|
267
|
-
result.current.deleteStagedCell(testCellId);
|
|
261
|
+
describe("useStagedCells", () => {
|
|
262
|
+
const stage = (id: CellId, edit: Edit) =>
|
|
263
|
+
store.set(stagedAICellsAtom, new Map([[id, edit]]));
|
|
268
264
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
it("should add staged cell with edit info", () => {
|
|
275
|
-
const { result } = renderHook(() => useStagedCells(store));
|
|
276
|
-
|
|
277
|
-
result.current.addStagedCell({
|
|
278
|
-
cellId: cellId1,
|
|
279
|
-
edit: { type: "update_cell", previousCode: "old code" },
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// Check that the cell was added to the atom with edit info
|
|
283
|
-
const state = store.get(stagedAICellsAtom);
|
|
284
|
-
expect(state.has(cellId1)).toBe(true);
|
|
285
|
-
expect(state.get(cellId1)).toEqual({
|
|
265
|
+
it("keeps added and updated cells by unstaging them", () => {
|
|
266
|
+
const { result } = renderHook(() => useStagedCells());
|
|
267
|
+
stage(cellId1, { type: "update_cell", previousCode: "x = 1" });
|
|
268
|
+
result.current.acceptStagedCell(cellId1, {
|
|
286
269
|
type: "update_cell",
|
|
287
|
-
previousCode: "
|
|
270
|
+
previousCode: "x = 1",
|
|
288
271
|
});
|
|
272
|
+
expect(store.get(stagedAICellsAtom).size).toBe(0);
|
|
273
|
+
expect(mockDeleteCellCallback).not.toHaveBeenCalled();
|
|
289
274
|
});
|
|
290
275
|
|
|
291
|
-
it("
|
|
292
|
-
const { result } = renderHook(() => useStagedCells(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
result.current.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
});
|
|
299
|
-
result.current.addStagedCell({
|
|
300
|
-
cellId: cellId2,
|
|
301
|
-
edit: { type: "add_cell" },
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// Then remove one
|
|
305
|
-
result.current.removeStagedCell(cellId1);
|
|
306
|
-
|
|
307
|
-
// Check that only the remaining cell is in the map
|
|
308
|
-
const state = store.get(stagedAICellsAtom);
|
|
309
|
-
expect(state.has(cellId1)).toBe(false);
|
|
310
|
-
expect(state.has(cellId2)).toBe(true);
|
|
276
|
+
it("deletes the cell when a proposed deletion is kept", () => {
|
|
277
|
+
const { result } = renderHook(() => useStagedCells());
|
|
278
|
+
const edit: Edit = { type: "delete_cell", previousCode: "x = 1" };
|
|
279
|
+
stage(cellId1, edit);
|
|
280
|
+
result.current.acceptStagedCell(cellId1, edit);
|
|
281
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId1 });
|
|
282
|
+
expect(store.get(stagedAICellsAtom).size).toBe(0);
|
|
311
283
|
});
|
|
312
284
|
|
|
313
|
-
it("
|
|
314
|
-
const { result } = renderHook(() => useStagedCells(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// Verify it was created and added
|
|
323
|
-
expect(createdCellId).toBe(mockCellId);
|
|
324
|
-
expect(mockCreateCell).toHaveBeenCalled();
|
|
325
|
-
|
|
326
|
-
let state = store.get(stagedAICellsAtom);
|
|
327
|
-
expect(state.has(mockCellId)).toBe(true);
|
|
328
|
-
expect(state.get(mockCellId)).toEqual({ type: cellId("add_cell") });
|
|
329
|
-
|
|
330
|
-
// Delete the staged cell
|
|
331
|
-
result.current.deleteStagedCell(mockCellId);
|
|
332
|
-
expect(mockDeleteCellCallback).toHaveBeenCalledWith({
|
|
333
|
-
cellId: mockCellId,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// Verify it was removed from staged cells
|
|
337
|
-
state = store.get(stagedAICellsAtom);
|
|
338
|
-
expect(state.has(mockCellId)).toBe(false);
|
|
285
|
+
it("restores the previous code when an update is discarded", () => {
|
|
286
|
+
const { result } = renderHook(() => useStagedCells());
|
|
287
|
+
const edit: Edit = { type: "update_cell", previousCode: "x = 1" };
|
|
288
|
+
stage(cellId1, edit);
|
|
289
|
+
result.current.rejectStagedCell(cellId1, edit);
|
|
290
|
+
expect(
|
|
291
|
+
mockCellHandle.current.editorViewOrNull.dispatch,
|
|
292
|
+
).toHaveBeenCalled();
|
|
293
|
+
expect(store.get(stagedAICellsAtom).size).toBe(0);
|
|
339
294
|
});
|
|
340
295
|
|
|
341
|
-
it("
|
|
342
|
-
const {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
296
|
+
it("writes to the document when the editor is not mounted", async () => {
|
|
297
|
+
const { getCellEditorView } = await import("../../cells/cells");
|
|
298
|
+
vi.mocked(getCellEditorView).mockReturnValueOnce(undefined);
|
|
299
|
+
const { result } = renderHook(() => useStagedCells());
|
|
300
|
+
const edit: Edit = { type: "update_cell", previousCode: "x = 1" };
|
|
301
|
+
stage(cellId1, edit);
|
|
302
|
+
result.current.rejectStagedCell(cellId1, edit);
|
|
303
|
+
expect(mockSetCellCode).toHaveBeenCalledWith({
|
|
346
304
|
cellId: cellId1,
|
|
347
|
-
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
const state = store.get(stagedAICellsAtom);
|
|
351
|
-
const edit = state.get(cellId1);
|
|
352
|
-
expect(edit).toEqual({
|
|
353
|
-
type: "update_cell",
|
|
354
|
-
previousCode: "previous code",
|
|
305
|
+
code: "x = 1",
|
|
355
306
|
});
|
|
356
307
|
});
|
|
357
308
|
|
|
358
|
-
it("
|
|
359
|
-
const { result } = renderHook(() => useStagedCells(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
expect(
|
|
370
|
-
type: "delete_cell",
|
|
371
|
-
previousCode: "deleted content",
|
|
372
|
-
});
|
|
309
|
+
it("deletes an added cell and leaves a proposed deletion in place when discarded", () => {
|
|
310
|
+
const { result } = renderHook(() => useStagedCells());
|
|
311
|
+
stage(cellId1, { type: "add_cell" });
|
|
312
|
+
result.current.rejectStagedCell(cellId1, { type: "add_cell" });
|
|
313
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId1 });
|
|
314
|
+
|
|
315
|
+
mockDeleteCellCallback.mockClear();
|
|
316
|
+
const deletion: Edit = { type: "delete_cell", previousCode: "x = 1" };
|
|
317
|
+
stage(cellId2, deletion);
|
|
318
|
+
result.current.rejectStagedCell(cellId2, deletion);
|
|
319
|
+
expect(mockDeleteCellCallback).not.toHaveBeenCalled();
|
|
320
|
+
expect(store.get(stagedAICellsAtom).size).toBe(0);
|
|
373
321
|
});
|
|
374
322
|
});
|
|
375
323
|
});
|
|
@@ -4,13 +4,11 @@ import { useAtomValue } from "jotai";
|
|
|
4
4
|
import { selectAtom } from "jotai/utils";
|
|
5
5
|
import { useMemo } from "react";
|
|
6
6
|
import { useDeleteCellCallback } from "@/components/editor/cell/useDeleteCell";
|
|
7
|
-
import { CellId } from "@/core/cells/ids";
|
|
7
|
+
import type { CellId } from "@/core/cells/ids";
|
|
8
8
|
import { createReducerAndAtoms } from "@/utils/createReducer";
|
|
9
|
-
import { Logger } from "@/utils/Logger";
|
|
10
9
|
import { getCellEditorView } from "../cells/cells";
|
|
11
10
|
import { useCellActions } from "../cells/actions";
|
|
12
11
|
import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
|
|
13
|
-
import type { JotaiStore } from "../state/jotai";
|
|
14
12
|
import type { EditType } from "./tools/edit-notebook-tool";
|
|
15
13
|
|
|
16
14
|
/** Pending AI edits shared by chat and agent workflows. */
|
|
@@ -36,6 +34,18 @@ const {
|
|
|
36
34
|
const { cellId, edit } = action;
|
|
37
35
|
return new Map([...state, [cellId, edit]]);
|
|
38
36
|
},
|
|
37
|
+
reconcileStagedCells: (state, cellIds: readonly string[]) => {
|
|
38
|
+
const existing = new Set(cellIds);
|
|
39
|
+
const removed = [...state.keys()].filter((id) => !existing.has(id));
|
|
40
|
+
if (removed.length === 0) {
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
const next = new Map(state);
|
|
44
|
+
for (const id of removed) {
|
|
45
|
+
next.delete(id);
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
},
|
|
39
49
|
removeStagedCell: (state, cellId: CellId) => {
|
|
40
50
|
if (!state.has(cellId)) {
|
|
41
51
|
return state;
|
|
@@ -48,44 +58,27 @@ const {
|
|
|
48
58
|
|
|
49
59
|
export { useStagedAICellsActions };
|
|
50
60
|
|
|
51
|
-
export function useStagedAICell(cellId: CellId): Edit | undefined {
|
|
61
|
+
export function useStagedAICell(cellId: CellId | undefined): Edit | undefined {
|
|
52
62
|
const stagedCellAtom = useMemo(
|
|
53
63
|
() =>
|
|
54
|
-
selectAtom(
|
|
64
|
+
selectAtom(
|
|
65
|
+
stagedAICellsAtom,
|
|
66
|
+
(cells) => (cellId === undefined ? undefined : cells.get(cellId)),
|
|
67
|
+
Object.is,
|
|
68
|
+
),
|
|
55
69
|
[cellId],
|
|
56
70
|
);
|
|
57
71
|
return useAtomValue(stagedCellAtom);
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Manage staged cells without assuming which AI workflow owns them. */
|
|
66
|
-
export function useStagedCells(store: JotaiStore) {
|
|
67
|
-
const { addStagedCell, removeStagedCell } = useStagedAICellsActions();
|
|
68
|
-
const { setCellCode, createCell } = useCellActions();
|
|
74
|
+
/** Keep or discard staged cells, whichever AI workflow staged them. */
|
|
75
|
+
export function useStagedCells() {
|
|
76
|
+
const { removeStagedCell } = useStagedAICellsActions();
|
|
77
|
+
const { setCellCode } = useCellActions();
|
|
69
78
|
const deleteCellCallback = useDeleteCellCallback();
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
addStagedCell({ cellId: newCellId, edit: { type: "add_cell" } });
|
|
74
|
-
createCell({ code, id: newCellId });
|
|
75
|
-
return newCellId;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const updateStagedCell = (opts: UpdateStagedCellAction) => {
|
|
79
|
-
const { cellId, code } = opts;
|
|
80
|
-
const stagedAICells = store.get(stagedAICellsAtom);
|
|
81
|
-
|
|
82
|
-
if (!stagedAICells.has(cellId)) {
|
|
83
|
-
Logger.error("Staged cell not found", { cellId });
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// The AI may change the cell's language, and only the editor infers
|
|
88
|
-
// one. Without a mounted editor the code arrives through the document.
|
|
80
|
+
// Only a mounted editor infers the cell's language; otherwise use the document.
|
|
81
|
+
const writeCellCode = (cellId: CellId, code: string) => {
|
|
89
82
|
const editorView = getCellEditorView(cellId);
|
|
90
83
|
if (editorView) {
|
|
91
84
|
const update = prepareCellCodeUpdate(editorView.state, code, {
|
|
@@ -104,13 +97,30 @@ export function useStagedCells(store: JotaiStore) {
|
|
|
104
97
|
deleteCellCallback({ cellId });
|
|
105
98
|
};
|
|
106
99
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
const acceptStagedCell = (cellId: CellId, edit: Edit) => {
|
|
101
|
+
if (edit.type === "delete_cell") {
|
|
102
|
+
deleteStagedCell(cellId);
|
|
103
|
+
} else {
|
|
104
|
+
removeStagedCell(cellId);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const rejectStagedCell = (cellId: CellId, edit: Edit) => {
|
|
109
|
+
switch (edit.type) {
|
|
110
|
+
case "update_cell":
|
|
111
|
+
writeCellCode(cellId, edit.previousCode);
|
|
112
|
+
removeStagedCell(cellId);
|
|
113
|
+
break;
|
|
114
|
+
case "add_cell":
|
|
115
|
+
deleteStagedCell(cellId);
|
|
116
|
+
break;
|
|
117
|
+
case "delete_cell":
|
|
118
|
+
removeStagedCell(cellId);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
113
121
|
};
|
|
122
|
+
|
|
123
|
+
return { acceptStagedCell, rejectStagedCell };
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
export { stagedAICellsAtom };
|
|
@@ -160,3 +160,9 @@ export function scrollCellIntoView(cellId: CellId): void {
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
export function scrollToCell(cellId: CellId): HTMLElement | null {
|
|
165
|
+
const cell = document.getElementById(HTMLCellId.create(cellId));
|
|
166
|
+
cell?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
167
|
+
return cell;
|
|
168
|
+
}
|
|
@@ -145,4 +145,63 @@ describe("clickablePlaceholderExtension", () => {
|
|
|
145
145
|
|
|
146
146
|
view.destroy();
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
describe("caret coordinates", () => {
|
|
150
|
+
const original = Range.prototype.getClientRects;
|
|
151
|
+
let rects: Array<Pick<DOMRect, "left" | "right" | "top" | "bottom">>;
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
rects = [{ left: 10, right: 50, top: 2, bottom: 18 }];
|
|
155
|
+
Range.prototype.getClientRects = () => rects as unknown as DOMRectList;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
Range.prototype.getClientRects = original;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
function renderWidget(beforeText: string) {
|
|
163
|
+
const view = new EditorView({
|
|
164
|
+
state: EditorState.create({
|
|
165
|
+
doc: "",
|
|
166
|
+
extensions: clickablePlaceholderExtension({
|
|
167
|
+
beforeText,
|
|
168
|
+
linkText: "here",
|
|
169
|
+
afterText: "",
|
|
170
|
+
onClick: vi.fn(),
|
|
171
|
+
}),
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
const widget = view.state
|
|
175
|
+
.facet(EditorView.decorations)
|
|
176
|
+
.flatMap((deco) => (typeof deco === "function" ? [deco(view)] : [deco]))
|
|
177
|
+
.map((set) => set.iter().value?.spec.widget)
|
|
178
|
+
.find(Boolean);
|
|
179
|
+
const dom = widget.toDOM(view);
|
|
180
|
+
document.body.append(dom);
|
|
181
|
+
return { view, widget, dom };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
it("collapses the caret to the start of the first line of text", () => {
|
|
185
|
+
const { view, widget, dom } = renderWidget("Type or ");
|
|
186
|
+
expect(widget.coordsAt(dom, 0, 1)).toEqual({
|
|
187
|
+
left: 10,
|
|
188
|
+
right: 10,
|
|
189
|
+
top: 2,
|
|
190
|
+
bottom: 18,
|
|
191
|
+
});
|
|
192
|
+
dom.style.direction = "rtl";
|
|
193
|
+
expect(widget.coordsAt(dom, 0, 1)).toMatchObject({ left: 50, right: 50 });
|
|
194
|
+
dom.remove();
|
|
195
|
+
view.destroy();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("skips empty text and falls back when nothing is laid out", () => {
|
|
199
|
+
const { view, widget, dom } = renderWidget("");
|
|
200
|
+
expect(widget.coordsAt(dom, 0, 1)).toMatchObject({ left: 10 });
|
|
201
|
+
rects = [];
|
|
202
|
+
expect(widget.coordsAt(dom, 0, 1)).toBeNull();
|
|
203
|
+
dom.remove();
|
|
204
|
+
view.destroy();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
148
207
|
});
|
|
@@ -90,6 +90,29 @@ export function clickablePlaceholderExtension(opts: {
|
|
|
90
90
|
return wrapper;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
public override coordsAt(dom: HTMLElement) {
|
|
94
|
+
// A wrapped placeholder must not make the caret span every line.
|
|
95
|
+
const walker = document.createTreeWalker(dom, NodeFilter.SHOW_TEXT);
|
|
96
|
+
let text = walker.nextNode();
|
|
97
|
+
while (text && !text.textContent) {
|
|
98
|
+
text = walker.nextNode();
|
|
99
|
+
}
|
|
100
|
+
if (!text) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const range = document.createRange();
|
|
104
|
+
range.selectNodeContents(text);
|
|
105
|
+
const rect = range.getClientRects()[0];
|
|
106
|
+
if (!rect) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const left =
|
|
110
|
+
window.getComputedStyle(dom).direction === "rtl"
|
|
111
|
+
? rect.right
|
|
112
|
+
: rect.left;
|
|
113
|
+
return { left, right: left, top: rect.top, bottom: rect.bottom };
|
|
114
|
+
}
|
|
115
|
+
|
|
93
116
|
public override ignoreEvent(event: Event) {
|
|
94
117
|
return (
|
|
95
118
|
event.target instanceof Element &&
|
|
@@ -39,6 +39,7 @@ import { MockNotebook } from "@/__mocks__/notebook";
|
|
|
39
39
|
import { Deferred } from "@/utils/Deferred";
|
|
40
40
|
import { cellId } from "@/__tests__/branded";
|
|
41
41
|
import { alertAtom, getPackageAlert } from "@/core/alerts/state";
|
|
42
|
+
import { stagedAICellsAtom } from "@/core/ai/staged-cells";
|
|
42
43
|
import { notebookAtom } from "@/core/cells/cells";
|
|
43
44
|
import { AppConfigSchema } from "@/core/config/config-schema";
|
|
44
45
|
import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
|
|
@@ -260,6 +261,39 @@ describe("useNotebookSession document model", () => {
|
|
|
260
261
|
await expect(model.flush()).rejects.toThrow("not started");
|
|
261
262
|
});
|
|
262
263
|
|
|
264
|
+
it("removes deleted cells from pending AI review while retaining surviving edits", async () => {
|
|
265
|
+
const fake = withTransport();
|
|
266
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
267
|
+
const store = createStore();
|
|
268
|
+
const { result, unmount } = renderConnectionHook(store);
|
|
269
|
+
await started();
|
|
270
|
+
act(() => {
|
|
271
|
+
connection(fake).open();
|
|
272
|
+
fixtureMessages("notebook-events.sse").forEach(connection(fake).receive);
|
|
273
|
+
});
|
|
274
|
+
const removed = cellId("c3");
|
|
275
|
+
const retained = cellId("k7f2");
|
|
276
|
+
act(() =>
|
|
277
|
+
store.set(
|
|
278
|
+
stagedAICellsAtom,
|
|
279
|
+
new Map([
|
|
280
|
+
[removed, { type: "add_cell" }],
|
|
281
|
+
[retained, { type: "add_cell" }],
|
|
282
|
+
]),
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
act(() =>
|
|
286
|
+
store.set(result.current.documentModel.editAtom, (tx) =>
|
|
287
|
+
tx.deleteCell(removed),
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
expect(store.get(notebookAtom).cellIds).not.toContain(removed);
|
|
291
|
+
expect(store.get(stagedAICellsAtom)).toEqual(
|
|
292
|
+
new Map([[retained, { type: "add_cell" }]]),
|
|
293
|
+
);
|
|
294
|
+
unmount();
|
|
295
|
+
});
|
|
296
|
+
|
|
263
297
|
it("follows the host document into the notebook's cells", async () => {
|
|
264
298
|
const fake = withTransport();
|
|
265
299
|
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
notebookAtom,
|
|
11
11
|
useReducerActions,
|
|
12
12
|
} from "@/core/cells/cells";
|
|
13
|
+
import { useStagedAICellsActions } from "@/core/ai/staged-cells";
|
|
13
14
|
import { followDocument } from "@/core/cells/document";
|
|
14
15
|
import { AUTOCOMPLETER } from "@/core/codemirror/completion/Autocompleter";
|
|
15
16
|
import { createDocumentModel } from "@/core/notebook/document";
|
|
@@ -96,6 +97,7 @@ export function useNotebookSession(opts: {
|
|
|
96
97
|
setLastRun,
|
|
97
98
|
resetCellRuntimes,
|
|
98
99
|
} = useReducerActions();
|
|
100
|
+
const { reconcileStagedCells } = useStagedAICellsActions();
|
|
99
101
|
const { addCellNotification } = useRunsActions();
|
|
100
102
|
const setKernelState = useSetAtom(kernelStateAtom);
|
|
101
103
|
const setInitialRunCompleted = useSetAtom(initialRunCompletedAtom);
|
|
@@ -441,7 +443,10 @@ export function useNotebookSession(opts: {
|
|
|
441
443
|
followDocument({
|
|
442
444
|
store,
|
|
443
445
|
model: documentModel,
|
|
444
|
-
reconcile:
|
|
446
|
+
reconcile: (document) => {
|
|
447
|
+
reconcileDocument(document);
|
|
448
|
+
reconcileStagedCells(document.order);
|
|
449
|
+
},
|
|
445
450
|
signal: controller.signal,
|
|
446
451
|
});
|
|
447
452
|
return () => {
|
|
@@ -450,7 +455,7 @@ export function useNotebookSession(opts: {
|
|
|
450
455
|
store.set(activeDocumentModelAtom, null);
|
|
451
456
|
}
|
|
452
457
|
};
|
|
453
|
-
}, [documentModel, store, reconcileDocument]);
|
|
458
|
+
}, [documentModel, store, reconcileDocument, reconcileStagedCells]);
|
|
454
459
|
|
|
455
460
|
// Subscribe before passive connection setup: static/WASM transports can emit synchronously.
|
|
456
461
|
useLayoutEffect(() => {
|