@marimo-team/frontend 0.24.1-dev58 → 0.24.1-dev59
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/assets/add-cell-with-ai-DVGZNNUv.js +76 -0
- package/dist/assets/{agent-panel-D7mo8YLQ.js → agent-panel-BTY3oeHD.js} +3 -3
- package/dist/assets/cell-editor-D3YsOG7a.js +21 -0
- package/dist/assets/{chat-display-Cgg0xLir.js → chat-display-NTNQQyoE.js} +1 -1
- package/dist/assets/chat-panel-Bow0QKrA.js +4 -0
- package/dist/assets/{chat-ui-DYLpA8l4.js → chat-ui-Cg69zAqt.js} +4 -4
- package/dist/assets/{command-palette-DiT0E_K7.js → command-palette-QFmIvDcT.js} +1 -1
- package/dist/assets/{edit-page-DAC4cNKe.js → edit-page-ds4edlkF.js} +6 -6
- package/dist/assets/{index-W5CUNEV9.js → index-B36dhEV6.js} +3 -3
- package/dist/assets/index-ecw9ngZu.css +2 -0
- package/dist/assets/{layout-D3zwsTvR.js → layout-iNJ-1JI2.js} +2 -2
- package/dist/assets/{panels-ChFSBlsU.js → panels-BbkuRtEi.js} +1 -1
- package/dist/assets/{reveal-component-BFRmOcQr.js → reveal-component-C4pF9OE7.js} +1 -1
- package/dist/assets/{run-page-MZCCEXct.js → run-page-Bo57bjbp.js} +1 -1
- package/dist/assets/{scratchpad-panel-BqYT2vj3.js → scratchpad-panel-vBIq9BfA.js} +1 -1
- package/dist/assets/{skeleton-gmxAkHfa.js → skeleton-B5iAa5n_.js} +1 -1
- package/dist/assets/{useNotebookActions-BoFJmccb.js → useNotebookActions-DGuDG0Px.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +0 -178
- package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +106 -0
- package/src/components/editor/ai/add-cell-with-ai.tsx +83 -62
- package/src/components/editor/ai/ai-completion-editor.tsx +71 -37
- package/src/components/editor/ai/completion-handlers.tsx +4 -0
- package/src/components/editor/ai/completion-utils.ts +0 -92
- package/src/components/editor/ai/staged-cell-submission.ts +46 -0
- package/src/components/editor/cell/StagedAICell.tsx +4 -2
- package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +64 -0
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +77 -0
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +8 -2
- package/src/core/ai/__tests__/staged-cells.test.ts +442 -125
- package/src/core/ai/__tests__/stream-completion-text.test.ts +74 -0
- package/src/core/ai/completion-output.ts +56 -0
- package/src/core/ai/staged-cells.ts +245 -164
- package/src/core/ai/stream-completion-text.ts +23 -6
- package/dist/assets/add-cell-with-ai-0dqk0f7O.js +0 -77
- package/dist/assets/cell-editor-e7_o4mEh.js +0 -24
- package/dist/assets/chat-panel-BaG-5LiN.js +0 -4
- package/dist/assets/index-DIUt_9oR.css +0 -2
- package/src/core/ai/__tests__/strip-wrapping-backticks.test.ts +0 -133
- package/src/core/ai/strip-wrapping-backticks.ts +0 -88
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { streamCompletionText } from "../stream-completion-text";
|
|
5
|
+
|
|
6
|
+
function streamResponse(...chunks: object[]): Response {
|
|
7
|
+
const body = [
|
|
8
|
+
...chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`),
|
|
9
|
+
"data: [DONE]\n\n",
|
|
10
|
+
].join("");
|
|
11
|
+
return new Response(body, {
|
|
12
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("streamCompletionText", () => {
|
|
17
|
+
it("returns validated cell code", async () => {
|
|
18
|
+
const response = streamResponse(
|
|
19
|
+
{ type: "start" },
|
|
20
|
+
{
|
|
21
|
+
type: "data-cell-completion",
|
|
22
|
+
data: { code: "print('```')" },
|
|
23
|
+
},
|
|
24
|
+
{ type: "finish", finishReason: "stop" },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
await expect(streamCompletionText(response)).resolves.toBe("print('```')");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects a missing structured completion", async () => {
|
|
31
|
+
const response = streamResponse(
|
|
32
|
+
{ type: "start" },
|
|
33
|
+
{ type: "finish", finishReason: "stop" },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await expect(streamCompletionText(response)).rejects.toThrow(
|
|
37
|
+
"AI completion returned no cell code",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects malformed completion data", async () => {
|
|
42
|
+
const response = streamResponse({
|
|
43
|
+
type: "data-cell-completion",
|
|
44
|
+
data: { code: 42 },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await expect(streamCompletionText(response)).rejects.toThrow();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects a partial completion without successful final validation", async () => {
|
|
51
|
+
const response = streamResponse({
|
|
52
|
+
type: "data-cell-completion",
|
|
53
|
+
data: { code: "partial" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await expect(streamCompletionText(response)).rejects.toThrow(
|
|
57
|
+
"AI completion ended before final validation",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rejects a completion with an error finish reason", async () => {
|
|
62
|
+
const response = streamResponse(
|
|
63
|
+
{
|
|
64
|
+
type: "data-cell-completion",
|
|
65
|
+
data: { code: "partial" },
|
|
66
|
+
},
|
|
67
|
+
{ type: "finish", finishReason: "error" },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await expect(streamCompletionText(response)).rejects.toThrow(
|
|
71
|
+
"AI completion ended before final validation",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { DataUIPart, UIMessage, UIMessageChunk } from "ai";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
// Mirrored from marimo/_server/ai/completion_output.py; SSE data parts are not
|
|
7
|
+
// represented in OpenAPI.
|
|
8
|
+
const CELL_COMPLETION_DATA_PART = "cell-completion" as const;
|
|
9
|
+
const NOTEBOOK_CELLS_COMPLETION_DATA_PART =
|
|
10
|
+
"notebook-cells-completion" as const;
|
|
11
|
+
|
|
12
|
+
export const CELL_COMPLETION_DATA_TYPE =
|
|
13
|
+
`data-${CELL_COMPLETION_DATA_PART}` as const;
|
|
14
|
+
export const NOTEBOOK_CELLS_COMPLETION_DATA_TYPE =
|
|
15
|
+
`data-${NOTEBOOK_CELLS_COMPLETION_DATA_PART}` as const;
|
|
16
|
+
|
|
17
|
+
export const cellCompletionSchema = z.object({
|
|
18
|
+
code: z.string(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const generatedCellSchema = z.object({
|
|
22
|
+
language: z.enum(["python", "sql", "markdown"]),
|
|
23
|
+
code: z.string().min(1),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const notebookCellsCompletionSchema = z.object({
|
|
27
|
+
cells: z.array(generatedCellSchema).min(1),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type CellCompletion = z.infer<typeof cellCompletionSchema>;
|
|
31
|
+
export type GeneratedCell = z.infer<typeof generatedCellSchema>;
|
|
32
|
+
export type NotebookCellsCompletion = z.infer<
|
|
33
|
+
typeof notebookCellsCompletionSchema
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
// Keep parsing data parts explicitly at the SSE boundary. AI SDK 7.0.37 types
|
|
37
|
+
// schema keys without `data-`, but its runtime lookup uses the full wire type.
|
|
38
|
+
const completionDataSchemas = {
|
|
39
|
+
[CELL_COMPLETION_DATA_PART]: cellCompletionSchema,
|
|
40
|
+
[NOTEBOOK_CELLS_COMPLETION_DATA_PART]: notebookCellsCompletionSchema,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type CompletionDataParts = {
|
|
44
|
+
[Name in keyof typeof completionDataSchemas]: z.infer<
|
|
45
|
+
(typeof completionDataSchemas)[Name]
|
|
46
|
+
>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type CompletionUIMessage = UIMessage<unknown, CompletionDataParts>;
|
|
50
|
+
export type CompletionDataPart = DataUIPart<CompletionDataParts>;
|
|
51
|
+
|
|
52
|
+
type DataChunk = Extract<UIMessageChunk, { type: `data-${string}` }>;
|
|
53
|
+
|
|
54
|
+
export function isDataChunk(chunk: UIMessageChunk): chunk is DataChunk {
|
|
55
|
+
return chunk.type.startsWith("data-");
|
|
56
|
+
}
|
|
@@ -1,34 +1,32 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { useAtomValue } from "jotai";
|
|
3
|
+
import { atom, useAtomValue } from "jotai";
|
|
5
4
|
import { selectAtom } from "jotai/utils";
|
|
6
5
|
import { useMemo, useRef } from "react";
|
|
7
|
-
import {
|
|
8
|
-
type AiCompletion,
|
|
9
|
-
codeToCells,
|
|
10
|
-
} from "@/components/editor/ai/completion-utils";
|
|
11
6
|
import { useDeleteCellCallback } from "@/components/editor/cell/useDeleteCell";
|
|
12
7
|
import { CellId } from "@/core/cells/ids";
|
|
13
|
-
import { logNever } from "@/utils/assertNever";
|
|
14
8
|
import { createReducerAndAtoms } from "@/utils/createReducer";
|
|
15
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
16
|
import { maybeAddMarimoImport } from "../cells/add-missing-import";
|
|
17
17
|
import {
|
|
18
18
|
type CreateNewCellAction,
|
|
19
19
|
getCellEditorView,
|
|
20
20
|
useCellActions,
|
|
21
21
|
} from "../cells/cells";
|
|
22
|
-
import type { LanguageAdapterType } from "../codemirror/language/types";
|
|
23
22
|
import { updateEditorCodeFromPython } from "../codemirror/language/utils";
|
|
24
23
|
import type { JotaiStore } from "../state/jotai";
|
|
25
24
|
import type { EditType } from "./tools/edit-notebook-tool";
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* And we only track one set of staged cells at a time.
|
|
27
|
+
* Pending AI edits shared by completion, chat, and agent workflows.
|
|
28
|
+
* Generation ownership keeps one workflow from accepting or discarding another's
|
|
29
|
+
* staged cells.
|
|
32
30
|
*/
|
|
33
31
|
|
|
34
32
|
export type Edit =
|
|
@@ -38,6 +36,25 @@ export type Edit =
|
|
|
38
36
|
|
|
39
37
|
export type StagedAICells = Map<CellId, Edit>;
|
|
40
38
|
|
|
39
|
+
type StagedGeneration =
|
|
40
|
+
| {
|
|
41
|
+
id: symbol;
|
|
42
|
+
status: "in_progress";
|
|
43
|
+
cellIds: readonly CellId[];
|
|
44
|
+
}
|
|
45
|
+
| { id: symbol; status: "complete" };
|
|
46
|
+
|
|
47
|
+
interface OwnedStagedGeneration {
|
|
48
|
+
id: symbol;
|
|
49
|
+
status: "in_progress" | "complete";
|
|
50
|
+
reconciler: StagedCellReconciler;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stagedGenerationAtom = atom<StagedGeneration | null>(null);
|
|
54
|
+
export const stagedGenerationInProgressAtom = atom(
|
|
55
|
+
(get) => get(stagedGenerationAtom)?.status === "in_progress",
|
|
56
|
+
);
|
|
57
|
+
|
|
41
58
|
const initialState = (): StagedAICells => {
|
|
42
59
|
return new Map();
|
|
43
60
|
};
|
|
@@ -60,16 +77,9 @@ const {
|
|
|
60
77
|
newState.delete(cellId);
|
|
61
78
|
return newState;
|
|
62
79
|
},
|
|
63
|
-
clearStagedCells: () => {
|
|
64
|
-
return initialState();
|
|
65
|
-
},
|
|
66
80
|
});
|
|
67
81
|
|
|
68
|
-
export {
|
|
69
|
-
useStagedAICellsActions,
|
|
70
|
-
createActions as createStagedAICellsActions,
|
|
71
|
-
reducer as stagedAICellsReducer,
|
|
72
|
-
};
|
|
82
|
+
export { useStagedAICellsActions };
|
|
73
83
|
|
|
74
84
|
export function useStagedAICell(cellId: CellId): Edit | undefined {
|
|
75
85
|
const stagedCellAtom = useMemo(
|
|
@@ -83,20 +93,14 @@ export function useStagedAICell(cellId: CellId): Edit | undefined {
|
|
|
83
93
|
interface UpdateStagedCellAction {
|
|
84
94
|
cellId: CellId;
|
|
85
95
|
code: string;
|
|
86
|
-
language?: LanguageAdapterType;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
/**
|
|
90
|
-
* Helper functions to create and delete staged cells.
|
|
91
|
-
*/
|
|
98
|
+
/** Manage staged cells without assuming which AI workflow owns them. */
|
|
92
99
|
export function useStagedCells(store: JotaiStore) {
|
|
93
|
-
const { addStagedCell, removeStagedCell
|
|
94
|
-
useStagedAICellsActions();
|
|
100
|
+
const { addStagedCell, removeStagedCell } = useStagedAICellsActions();
|
|
95
101
|
const { createNewCell, updateCellCode } = useCellActions();
|
|
96
102
|
const deleteCellCallback = useDeleteCellCallback();
|
|
97
103
|
|
|
98
|
-
const cellCreationStream = useRef<CellCreationStream | null>(null);
|
|
99
|
-
|
|
100
104
|
const createStagedCell = (code: string): CellId => {
|
|
101
105
|
const newCellId = CellId.create();
|
|
102
106
|
addStagedCell({ cellId: newCellId, edit: { type: "add_cell" } });
|
|
@@ -118,8 +122,6 @@ export function useStagedCells(store: JotaiStore) {
|
|
|
118
122
|
return;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
// Update the editor code if the cell is mounted
|
|
122
|
-
// Else, update the cell code in the notebook
|
|
123
125
|
const editorView = getCellEditorView(cellId);
|
|
124
126
|
if (editorView) {
|
|
125
127
|
updateEditorCodeFromPython(editorView, code);
|
|
@@ -128,190 +130,269 @@ export function useStagedCells(store: JotaiStore) {
|
|
|
128
130
|
}
|
|
129
131
|
};
|
|
130
132
|
|
|
131
|
-
// Delete a staged cell and the corresponding cell in the notebook.
|
|
132
133
|
const deleteStagedCell = (cellId: CellId) => {
|
|
133
134
|
removeStagedCell(cellId);
|
|
134
135
|
deleteCellCallback({ cellId });
|
|
135
136
|
};
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
return {
|
|
139
|
+
createStagedCell,
|
|
140
|
+
updateStagedCell,
|
|
141
|
+
addStagedCell,
|
|
142
|
+
removeStagedCell,
|
|
143
|
+
deleteStagedCell,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Owns the lifecycle of cells created by one structured completion flow. */
|
|
148
|
+
export function useStagedCellGeneration(store: JotaiStore) {
|
|
149
|
+
const stagedCellActions = useStagedCells(store);
|
|
150
|
+
const { createNewCell } = useCellActions();
|
|
151
|
+
const ownedGeneration = useRef<OwnedStagedGeneration | null>(null);
|
|
152
|
+
// Ownership lives in a ref, so subscribe to its backing atoms to rerender
|
|
153
|
+
// consumers when completion or per-cell acceptance changes.
|
|
154
|
+
useAtomValue(stagedGenerationAtom, { store });
|
|
155
|
+
useAtomValue(stagedAICellsAtom, { store });
|
|
156
|
+
|
|
157
|
+
const discardStagedCellIds = (cellIds: readonly CellId[]) => {
|
|
139
158
|
const stagedAICells = store.get(stagedAICellsAtom);
|
|
140
|
-
for (const cellId of
|
|
141
|
-
|
|
159
|
+
for (const cellId of cellIds.toReversed()) {
|
|
160
|
+
if (stagedAICells.has(cellId)) {
|
|
161
|
+
stagedCellActions.deleteStagedCell(cellId);
|
|
162
|
+
}
|
|
142
163
|
}
|
|
143
|
-
clearStagedCells();
|
|
144
164
|
};
|
|
145
165
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
166
|
+
const beginStagedCellGeneration = () => {
|
|
167
|
+
const activeGeneration = store.get(stagedGenerationAtom);
|
|
168
|
+
if (activeGeneration?.status === "in_progress") {
|
|
169
|
+
discardStagedCellIds(activeGeneration.cellIds);
|
|
170
|
+
}
|
|
171
|
+
ownedGeneration.current?.reconciler.discard(store.get(stagedAICellsAtom));
|
|
172
|
+
const generationId = Symbol("staged-cell-generation");
|
|
173
|
+
ownedGeneration.current = {
|
|
174
|
+
id: generationId,
|
|
175
|
+
status: "in_progress",
|
|
176
|
+
reconciler: new StagedCellReconciler({
|
|
177
|
+
...stagedCellActions,
|
|
178
|
+
createNewCell,
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
store.set(stagedGenerationAtom, {
|
|
182
|
+
id: generationId,
|
|
183
|
+
status: "in_progress",
|
|
184
|
+
cellIds: [],
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const finishStagedCellGeneration = (successful: boolean) => {
|
|
189
|
+
const generation = ownedGeneration.current;
|
|
190
|
+
if (generation?.status !== "in_progress") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const stagedGeneration = store.get(stagedGenerationAtom);
|
|
195
|
+
const ownsActiveGeneration =
|
|
196
|
+
stagedGeneration?.id === generation.id &&
|
|
197
|
+
stagedGeneration.status === "in_progress";
|
|
198
|
+
if (!ownsActiveGeneration || !successful) {
|
|
199
|
+
generation.reconciler.discard(store.get(stagedAICellsAtom));
|
|
200
|
+
if (ownsActiveGeneration) {
|
|
201
|
+
store.set(stagedGenerationAtom, null);
|
|
202
|
+
}
|
|
203
|
+
ownedGeneration.current = null;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
generation.status = "complete";
|
|
208
|
+
store.set(stagedGenerationAtom, {
|
|
209
|
+
id: generation.id,
|
|
210
|
+
status: "complete",
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const hasOwnedStagedCells = () => {
|
|
215
|
+
const generation = ownedGeneration.current;
|
|
216
|
+
const stagedGeneration = store.get(stagedGenerationAtom);
|
|
217
|
+
return (
|
|
218
|
+
generation !== null &&
|
|
219
|
+
generation.status === "complete" &&
|
|
220
|
+
stagedGeneration?.status === "complete" &&
|
|
221
|
+
stagedGeneration.id === generation.id &&
|
|
222
|
+
generation.reconciler.hasStagedCells(store.get(stagedAICellsAtom))
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const acceptOwnedStagedCells = () => {
|
|
227
|
+
if (!hasOwnedStagedCells()) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
ownedGeneration.current?.reconciler.accept();
|
|
232
|
+
store.set(stagedGenerationAtom, null);
|
|
233
|
+
ownedGeneration.current = null;
|
|
234
|
+
return true;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const discardOwnedStagedCells = () => {
|
|
238
|
+
const generation = ownedGeneration.current;
|
|
239
|
+
const stagedGeneration = store.get(stagedGenerationAtom);
|
|
240
|
+
if (generation === null || stagedGeneration?.id !== generation.id) {
|
|
241
|
+
return false;
|
|
212
242
|
}
|
|
243
|
+
|
|
244
|
+
generation.reconciler.discard(store.get(stagedAICellsAtom));
|
|
245
|
+
store.set(stagedGenerationAtom, null);
|
|
246
|
+
ownedGeneration.current = null;
|
|
247
|
+
return true;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const onData = (part: CompletionDataPart) => {
|
|
251
|
+
if (part.type !== NOTEBOOK_CELLS_COMPLETION_DATA_TYPE) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const generation = ownedGeneration.current;
|
|
255
|
+
const stagedGeneration = store.get(stagedGenerationAtom);
|
|
256
|
+
if (
|
|
257
|
+
generation?.status !== "in_progress" ||
|
|
258
|
+
stagedGeneration?.id !== generation.id ||
|
|
259
|
+
stagedGeneration.status !== "in_progress"
|
|
260
|
+
) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const completion = notebookCellsCompletionSchema.parse(part.data);
|
|
264
|
+
generation.reconciler.reconcile(completion.cells);
|
|
265
|
+
store.set(stagedGenerationAtom, {
|
|
266
|
+
...stagedGeneration,
|
|
267
|
+
cellIds: generation.reconciler.stagedCellIds(),
|
|
268
|
+
});
|
|
213
269
|
};
|
|
214
270
|
|
|
215
271
|
return {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
deleteAllStagedCells,
|
|
223
|
-
onStream,
|
|
272
|
+
beginStagedCellGeneration,
|
|
273
|
+
finishStagedCellGeneration,
|
|
274
|
+
hasOwnedStagedCells,
|
|
275
|
+
acceptOwnedStagedCells,
|
|
276
|
+
discardOwnedStagedCells,
|
|
277
|
+
onData,
|
|
224
278
|
};
|
|
225
279
|
}
|
|
226
280
|
|
|
227
281
|
export { stagedAICellsAtom };
|
|
228
282
|
export const visibleForTesting = {
|
|
283
|
+
stagedGenerationAtom,
|
|
229
284
|
createActions,
|
|
230
285
|
reducer,
|
|
231
286
|
initialState,
|
|
232
|
-
useStagedAICellsActions,
|
|
233
287
|
};
|
|
234
288
|
|
|
235
|
-
type TextDeltaChunk = Extract<UIMessageChunk, { type: "text-delta" }>;
|
|
236
|
-
|
|
237
289
|
interface CreatedCell {
|
|
238
290
|
cellId: CellId;
|
|
239
|
-
|
|
291
|
+
code: string;
|
|
240
292
|
}
|
|
241
293
|
|
|
242
|
-
|
|
294
|
+
interface StagedCellReconcilerActions {
|
|
295
|
+
createStagedCell: (code: string) => CellId;
|
|
296
|
+
updateStagedCell: (opts: UpdateStagedCellAction) => void;
|
|
297
|
+
deleteStagedCell: (cellId: CellId) => void;
|
|
298
|
+
removeStagedCell: (cellId: CellId) => void;
|
|
299
|
+
addStagedCell: (payload: { cellId: CellId; edit: Edit }) => void;
|
|
300
|
+
createNewCell: (opts: CreateNewCellAction) => void;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
class StagedCellReconciler {
|
|
243
304
|
private createdCells: CreatedCell[] = [];
|
|
244
|
-
private
|
|
245
|
-
|
|
246
|
-
private onCreateCell: (code: string) => CellId;
|
|
247
|
-
private onUpdateCell: (opts: UpdateStagedCellAction) => void;
|
|
248
|
-
private addStagedCell: (payload: { cellId: CellId; edit: Edit }) => void;
|
|
249
|
-
private createNewCell: (opts: CreateNewCellAction) => void;
|
|
250
|
-
private hasMarimoImport = false;
|
|
251
|
-
|
|
252
|
-
constructor(
|
|
253
|
-
onCreateCell: (code: string) => CellId,
|
|
254
|
-
onUpdateCell: (opts: UpdateStagedCellAction) => void,
|
|
255
|
-
addStagedCell: (payload: { cellId: CellId; edit: Edit }) => void,
|
|
256
|
-
createNewCell: (opts: CreateNewCellAction) => void,
|
|
257
|
-
) {
|
|
258
|
-
this.onCreateCell = onCreateCell;
|
|
259
|
-
this.onUpdateCell = onUpdateCell;
|
|
260
|
-
this.addStagedCell = addStagedCell;
|
|
261
|
-
this.createNewCell = createNewCell;
|
|
262
|
-
}
|
|
305
|
+
private readonly actions: StagedCellReconcilerActions;
|
|
306
|
+
private marimoImportCellId: CellId | null = null;
|
|
263
307
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const completionCells = codeToCells(this.buffer);
|
|
308
|
+
constructor(actions: StagedCellReconcilerActions) {
|
|
309
|
+
this.actions = actions;
|
|
310
|
+
}
|
|
268
311
|
|
|
269
|
-
|
|
270
|
-
// we parse the buffer into cells and determine which parts correspond to which cell.
|
|
271
|
-
// For each parsed cell, we either update an existing staged cell or create a new one.
|
|
312
|
+
reconcile(completionCells: GeneratedCell[]) {
|
|
272
313
|
for (const [idx, cell] of completionCells.entries()) {
|
|
273
314
|
if (idx < this.createdCells.length) {
|
|
274
|
-
this.addMarimoImport(cell.language);
|
|
275
315
|
const existingCell = this.createdCells[idx];
|
|
276
|
-
|
|
277
|
-
this.
|
|
316
|
+
const codeChanged = existingCell.code !== cell.code;
|
|
317
|
+
this.createdCells[idx] = { ...existingCell, code: cell.code };
|
|
318
|
+
if (!codeChanged) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
this.actions.updateStagedCell({
|
|
278
322
|
cellId: existingCell.cellId,
|
|
279
323
|
code: cell.code,
|
|
280
|
-
language: cell.language,
|
|
281
324
|
});
|
|
282
325
|
} else {
|
|
283
|
-
const newCellId = this.
|
|
284
|
-
this.createdCells.push({ cellId: newCellId, cell });
|
|
326
|
+
const newCellId = this.actions.createStagedCell(cell.code);
|
|
327
|
+
this.createdCells.push({ cellId: newCellId, code: cell.code });
|
|
285
328
|
}
|
|
286
329
|
}
|
|
330
|
+
|
|
331
|
+
const removedCells = this.createdCells.splice(completionCells.length);
|
|
332
|
+
for (const { cellId } of removedCells.toReversed()) {
|
|
333
|
+
this.actions.deleteStagedCell(cellId);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.syncMarimoImport(completionCells);
|
|
287
337
|
}
|
|
288
338
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
339
|
+
discard(stagedCells: StagedAICells) {
|
|
340
|
+
for (const { cellId } of this.createdCells.toReversed()) {
|
|
341
|
+
if (stagedCells.has(cellId)) {
|
|
342
|
+
this.actions.deleteStagedCell(cellId);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
this.createdCells = [];
|
|
346
|
+
|
|
347
|
+
if (this.marimoImportCellId && stagedCells.has(this.marimoImportCellId)) {
|
|
348
|
+
this.actions.deleteStagedCell(this.marimoImportCellId);
|
|
349
|
+
}
|
|
350
|
+
this.marimoImportCellId = null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
accept() {
|
|
354
|
+
for (const cellId of this.stagedCellIds()) {
|
|
355
|
+
this.actions.removeStagedCell(cellId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
hasStagedCells(stagedCells: StagedAICells) {
|
|
360
|
+
return this.stagedCellIds().some((cellId) => stagedCells.has(cellId));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
stagedCellIds() {
|
|
364
|
+
const cellIds = this.createdCells.map(({ cellId }) => cellId);
|
|
365
|
+
if (this.marimoImportCellId) {
|
|
366
|
+
cellIds.push(this.marimoImportCellId);
|
|
367
|
+
}
|
|
368
|
+
return cellIds;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Keep the generated marimo import consistent with the latest snapshot. */
|
|
372
|
+
private syncMarimoImport(completionCells: GeneratedCell[]) {
|
|
373
|
+
const requiresMarimo = completionCells.some(
|
|
374
|
+
(cell) => cell.language !== "python",
|
|
375
|
+
);
|
|
376
|
+
if (!requiresMarimo) {
|
|
377
|
+
if (this.marimoImportCellId) {
|
|
378
|
+
this.actions.deleteStagedCell(this.marimoImportCellId);
|
|
379
|
+
this.marimoImportCellId = null;
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (this.marimoImportCellId) {
|
|
292
384
|
return;
|
|
293
385
|
}
|
|
294
386
|
|
|
295
387
|
const cellId = maybeAddMarimoImport({
|
|
296
388
|
autoInstantiate: false,
|
|
297
|
-
createNewCell: this.createNewCell,
|
|
389
|
+
createNewCell: this.actions.createNewCell,
|
|
298
390
|
fromCellId: this.createdCells[0]?.cellId,
|
|
299
391
|
before: true,
|
|
300
392
|
});
|
|
301
393
|
if (cellId) {
|
|
302
|
-
this.addStagedCell({ cellId, edit: { type: "add_cell" } });
|
|
394
|
+
this.actions.addStagedCell({ cellId, edit: { type: "add_cell" } });
|
|
395
|
+
this.marimoImportCellId = cellId;
|
|
303
396
|
}
|
|
304
|
-
this.hasMarimoImport = true;
|
|
305
397
|
}
|
|
306
|
-
|
|
307
|
-
stop() {
|
|
308
|
-
// Clear all state
|
|
309
|
-
this.buffer = "";
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
type DataChunk = Extract<UIMessageChunk, { type: `data-${string}` }>;
|
|
314
|
-
|
|
315
|
-
function isDataChunk(chunk: UIMessageChunk): chunk is DataChunk {
|
|
316
|
-
return chunk.type.startsWith("data-");
|
|
317
398
|
}
|