Skip to content

Commit cb24822

Browse files
committed
wip
1 parent 15d3eaa commit cb24822

File tree

15 files changed

+392
-32
lines changed

15 files changed

+392
-32
lines changed

frontend/src/components/chat/chat-panel.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ import {
3636
type ChatId,
3737
chatStateAtom,
3838
} from "@/core/ai/state";
39-
import { FRONTEND_TOOL_REGISTRY } from "@/core/ai/tools/registry";
39+
import {
40+
type CopilotMode,
41+
FRONTEND_TOOL_REGISTRY,
42+
} from "@/core/ai/tools/registry";
4043
import { aiAtom, aiEnabledAtom } from "@/core/config/config";
4144
import { DEFAULT_AI_MODEL } from "@/core/config/config-schema";
4245
import { FeatureFlagged } from "@/core/config/feature-flag";
@@ -301,7 +304,11 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
301304

302305
const { saveModeChange } = useModelChange();
303306

304-
const modeOptions = [
307+
const modeOptions: {
308+
value: CopilotMode;
309+
label: string;
310+
subtitle: string;
311+
}[] = [
305312
{
306313
value: "ask",
307314
label: "Ask",
@@ -313,6 +320,11 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
313320
label: "Manual",
314321
subtitle: "Pure chat, no tool usage",
315322
},
323+
{
324+
value: "agent",
325+
label: "Agent",
326+
subtitle: "Use AI with access to read and write tools",
327+
},
316328
];
317329

318330
const isAttachmentSupported =

frontend/src/components/editor/ai/ai-completion-editor.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ import { customPythonLanguageSupport } from "@/core/codemirror/language/language
1111
import "./merge-editor.css";
1212
import { storePrompt } from "@marimo-team/codemirror-ai";
1313
import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
14-
import { useAtom } from "jotai";
14+
import { useAtom, useAtomValue } from "jotai";
1515
import { AIModelDropdown } from "@/components/ai/ai-model-dropdown";
1616
import { Checkbox } from "@/components/ui/checkbox";
1717
import { Label } from "@/components/ui/label";
1818
import { Switch } from "@/components/ui/switch";
1919
import { Tooltip } from "@/components/ui/tooltip";
2020
import { toast } from "@/components/ui/use-toast";
21-
import { includeOtherCellsAtom } from "@/core/ai/state";
21+
import { stagedAICellsAtom } from "@/core/ai/staged-cells";
22+
import { type AiCompletionCell, includeOtherCellsAtom } from "@/core/ai/state";
23+
import type { CellId } from "@/core/cells/ids";
2224
import { getCodes } from "@/core/codemirror/copilot/getCodes";
2325
import type { LanguageAdapterType } from "@/core/codemirror/language/types";
2426
import { selectAllText } from "@/core/codemirror/utils";
@@ -40,15 +42,14 @@ const Original = CodeMirrorMerge.Original;
4042
const Modified = CodeMirrorMerge.Modified;
4143

4244
interface Props {
45+
cellId: CellId;
46+
aiCompletionCell: AiCompletionCell | null;
4347
className?: string;
4448
currentCode: string;
4549
currentLanguageAdapter: LanguageAdapterType | undefined;
46-
initialPrompt: string | undefined;
4750
onChange: (code: string) => void;
4851
declineChange: () => void;
4952
acceptChange: (rightHandCode: string) => void;
50-
enabled: boolean;
51-
triggerImmediately?: boolean;
5253
runCell: () => void;
5354
/**
5455
* Children shown when there is no completion
@@ -64,15 +65,14 @@ const baseExtensions = [customPythonLanguageSupport(), EditorView.lineWrapping];
6465
* This shows a left/right split with the original and modified code.
6566
*/
6667
export const AiCompletionEditor: React.FC<Props> = ({
68+
cellId,
69+
aiCompletionCell,
6770
className,
6871
onChange,
69-
initialPrompt,
7072
currentLanguageAdapter,
7173
currentCode,
7274
declineChange,
7375
acceptChange,
74-
enabled,
75-
triggerImmediately,
7676
runCell,
7777
children,
7878
}) => {
@@ -86,6 +86,20 @@ export const AiCompletionEditor: React.FC<Props> = ({
8686

8787
const runtimeManager = useRuntimeManager();
8888

89+
const {
90+
initialPrompt,
91+
triggerImmediately,
92+
cellId: aiCellId,
93+
} = aiCompletionCell ?? {};
94+
const enabled = aiCellId === cellId;
95+
96+
const stagedAICells = useAtomValue(stagedAICellsAtom);
97+
const updatedCell = stagedAICells.get(cellId);
98+
let previousCellCode: string | undefined;
99+
if (updatedCell?.type === "update_cell") {
100+
previousCellCode = updatedCell.previousCode;
101+
}
102+
89103
const {
90104
completion: untrimmedCompletion,
91105
input,
@@ -307,7 +321,18 @@ export const AiCompletionEditor: React.FC<Props> = ({
307321
/>
308322
</CodeMirrorMerge>
309323
)}
310-
{(!completion || !enabled) && children}
324+
{previousCellCode && (
325+
<CodeMirrorMerge className="cm" theme={theme}>
326+
<Original value={previousCellCode} extensions={baseExtensions} />
327+
<Modified
328+
value={currentCode}
329+
editable={false}
330+
readOnly={true}
331+
extensions={baseExtensions}
332+
/>
333+
</CodeMirrorMerge>
334+
)}
335+
{(!completion || !enabled) && !previousCellCode && children}
311336
<div
312337
className={cn(
313338
"w-full bg-(--cm-background) flex justify-center transition-all duration-300 ease-in-out overflow-hidden",

frontend/src/components/editor/cell/StagedAICell.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import { useAtomValue, useStore } from "jotai";
44
import { stagedAICellsAtom, useStagedCells } from "@/core/ai/staged-cells";
5+
import { cellHandleAtom } from "@/core/cells/cells";
56
import type { CellId } from "@/core/cells/ids";
7+
import { updateEditorCodeFromPython } from "@/core/codemirror/language/utils";
68
import { cn } from "@/utils/cn";
9+
import { Logger } from "@/utils/Logger";
710
import { CompletionActionsCellFooter } from "../ai/completion-handlers";
811

912
export const StagedAICellBackground: React.FC<{
@@ -24,9 +27,11 @@ export const StagedAICellFooter: React.FC<{ cellId: CellId }> = ({
2427
}) => {
2528
const store = useStore();
2629
const stagedAICells = useAtomValue(stagedAICellsAtom);
30+
const stagedAiCell = stagedAICells.get(cellId);
31+
2732
const { deleteStagedCell, removeStagedCell } = useStagedCells(store);
2833

29-
if (!stagedAICells.has(cellId)) {
34+
if (!stagedAiCell) {
3035
return null;
3136
}
3237

@@ -35,7 +40,27 @@ export const StagedAICellFooter: React.FC<{ cellId: CellId }> = ({
3540
};
3641

3742
const handleDeclineCompletion = () => {
38-
deleteStagedCell(cellId);
43+
switch (stagedAiCell.type) {
44+
case "update_cell": {
45+
// Revert cell code
46+
const cellHandle = store.get(cellHandleAtom(cellId));
47+
const editorView = cellHandle?.current?.editorView;
48+
if (!editorView) {
49+
Logger.error("Editor for this cell not found", { cellId });
50+
break;
51+
}
52+
53+
updateEditorCodeFromPython(editorView, stagedAiCell.previousCode);
54+
removeStagedCell(cellId);
55+
break;
56+
}
57+
case "add_cell":
58+
// Delete the cell since it's newly created
59+
deleteStagedCell(cellId);
60+
break;
61+
case "delete_cell":
62+
break;
63+
}
3964
};
4065

4166
return (

frontend/src/components/editor/cell/code/cell-editor.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,8 @@ const CellEditorInternal = ({
405405

406406
return (
407407
<AiCompletionEditor
408-
enabled={aiCompletionCell?.cellId === cellId}
409-
initialPrompt={aiCompletionCell?.initialPrompt}
410-
triggerImmediately={aiCompletionCell?.triggerImmediately}
408+
cellId={cellId}
409+
aiCompletionCell={aiCompletionCell}
411410
currentCode={editorViewRef.current?.state.doc.toString() ?? code}
412411
currentLanguageAdapter={languageAdapter}
413412
declineChange={useEvent(() => {

frontend/src/core/ai/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type { Role } from "@marimo-team/llm-info";
44
import { useAtom } from "jotai";
55
import type { QualifiedModelId } from "@/core/ai/ids/ids";
66
import { userConfigAtom } from "@/core/config/config";
7-
import type { AIModelKey, UserConfig } from "@/core/config/config-schema";
7+
import type {
8+
AIModelKey,
9+
CopilotMode,
10+
UserConfig,
11+
} from "@/core/config/config-schema";
812
import { useRequestClient } from "@/core/network/requests";
913

1014
// Extract only the supported roles from the Role type
@@ -60,7 +64,7 @@ export const useModelChange = () => {
6064
saveConfig(newConfig);
6165
};
6266

63-
const saveModeChange = async (newMode: "ask" | "manual") => {
67+
const saveModeChange = async (newMode: CopilotMode) => {
6468
const newConfig: UserConfig = {
6569
...userConfig,
6670
ai: {

frontend/src/core/ai/staged-cells.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,24 @@ import {
1919
import type { LanguageAdapterType } from "../codemirror/language/types";
2020
import { updateEditorCodeFromPython } from "../codemirror/language/utils";
2121
import type { JotaiStore } from "../state/jotai";
22+
import type { EditType } from "./tools/edit-notebook-tool";
2223

2324
/**
2425
* Cells that are staged for AI completion
25-
* They function similarly to cells in the notebook, but they can be deleted or accepted by the user.
26-
* We only track one set of staged cells at a time.
26+
* They function similarly to cells in the notebook, but they can be accepted or rejected by the user.
27+
* We track edited, new and deleted cells.
28+
* And we only track one set of staged cells at a time.
2729
*/
2830

29-
const initialState = (): Set<CellId> => {
30-
return new Set();
31+
type Edit =
32+
| { type: Extract<EditType, "update_cell">; previousCode: string }
33+
| { type: Extract<EditType, "add_cell"> }
34+
| { type: Extract<EditType, "delete_cell">; previousCode: string };
35+
36+
type StagedAICells = Map<CellId, Edit>;
37+
38+
const initialState = (): StagedAICells => {
39+
return new Map();
3140
};
3241

3342
const {
@@ -38,10 +47,12 @@ const {
3847
} = createReducerAndAtoms(initialState, {
3948
addStagedCell: (state, action: { cellId: CellId }) => {
4049
const { cellId } = action;
41-
return new Set([...state, cellId]);
50+
return new Map([...state, [cellId, { type: "add_cell" }]]);
4251
},
4352
removeStagedCell: (state, cellId: CellId) => {
44-
return new Set([...state].filter((id) => id !== cellId));
53+
const newState = new Map(state);
54+
newState.delete(cellId);
55+
return newState;
4556
},
4657
clearStagedCells: () => {
4758
return initialState();
@@ -105,7 +116,7 @@ export function useStagedCells(store: JotaiStore) {
105116
// Delete all staged cells and the corresponding cells in the notebook.
106117
const deleteAllStagedCells = () => {
107118
const stagedAICells = store.get(stagedAICellsAtom);
108-
for (const cellId of stagedAICells) {
119+
for (const cellId of stagedAICells.keys()) {
109120
deleteCellCallback({ cellId });
110121
}
111122
clearStagedCells();
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/* Copyright 2024 Marimo. All rights reserved. */
2+
3+
import { z } from "zod";
4+
import { notebookAtom } from "@/core/cells/cells";
5+
import type { CellId } from "@/core/cells/ids";
6+
import { updateEditorCodeFromPython } from "@/core/codemirror/language/utils";
7+
import type { JotaiStore } from "@/core/state/jotai";
8+
import { stagedAICellsAtom } from "../staged-cells";
9+
import {
10+
type AiTool,
11+
ToolExecutionError,
12+
type ToolOutputBase,
13+
toolOutputBaseSchema,
14+
} from "./base";
15+
import type { CopilotMode } from "./registry";
16+
17+
const description = `
18+
Perform editing operations on the current notebook.
19+
Call this tool multiple times to perform multiple edits.
20+
21+
Args:
22+
- edit (object): The editing operation to perform. Must be one of:
23+
- update_cell: Update the code of an existing cell.
24+
- add_cell: Add a new cell to the notebook.
25+
- delete_cell: Delete an existing cell.
26+
27+
Returns:
28+
- A result object containing standard tool metadata.
29+
`;
30+
31+
const editNotebookSchema = z.object({
32+
edit: z.discriminatedUnion("type", [
33+
z.object({
34+
type: z.literal("update_cell"),
35+
cellId: z.string() as unknown as z.ZodType<CellId>,
36+
code: z.string(),
37+
}),
38+
z.object({
39+
type: z.literal("add_cell"),
40+
cellId: z.string() as unknown as z.ZodType<CellId>,
41+
code: z.string(),
42+
language: z.enum(["python", "sql", "markdown"]).optional(),
43+
}),
44+
z.object({
45+
type: z.literal("delete_cell"),
46+
cellId: z.string() as unknown as z.ZodType<CellId>,
47+
}),
48+
]),
49+
});
50+
51+
type EditNotebookInput = z.infer<typeof editNotebookSchema>;
52+
type EditOperation = EditNotebookInput["edit"];
53+
export type EditType = EditOperation["type"];
54+
55+
export class EditNotebookTool
56+
implements AiTool<EditNotebookInput, ToolOutputBase>
57+
{
58+
private readonly store: JotaiStore;
59+
readonly name = "edit_notebook_tool";
60+
readonly description = description;
61+
readonly schema = editNotebookSchema;
62+
readonly outputSchema = toolOutputBaseSchema;
63+
readonly mode: CopilotMode[] = ["agent"];
64+
65+
constructor(store: JotaiStore) {
66+
this.store = store;
67+
}
68+
69+
handler = async ({ edit }: EditNotebookInput): Promise<ToolOutputBase> => {
70+
switch (edit.type) {
71+
case "update_cell": {
72+
const { cellId, code } = edit;
73+
74+
const notebook = this.store.get(notebookAtom);
75+
const cellIds = notebook.cellIds;
76+
if (!cellIds.getColumns().some((column) => column.idSet.has(cellId))) {
77+
throw new ToolExecutionError(
78+
"Cell not found",
79+
"CELL_NOT_FOUND",
80+
false,
81+
"Check which cells exist in the notebook",
82+
);
83+
}
84+
85+
const cellHandles = notebook.cellHandles;
86+
const cellHandle = cellHandles[cellId].current;
87+
if (!cellHandle?.editorView) {
88+
throw new ToolExecutionError(
89+
"Cell editor not found",
90+
"CELL_EDITOR_NOT_FOUND",
91+
false,
92+
"Internal error, ask the user to report this error",
93+
);
94+
}
95+
96+
const currentCellCode = cellHandle.editorView.state.doc.toString();
97+
98+
const stagedAICells = this.store.get(stagedAICellsAtom);
99+
const newStagedAICells = new Map([
100+
...stagedAICells,
101+
[cellId, { type: "update_cell", previousCode: currentCellCode }],
102+
]);
103+
this.store.set(stagedAICellsAtom, newStagedAICells);
104+
105+
updateEditorCodeFromPython(cellHandle.editorView, code);
106+
107+
break;
108+
}
109+
case "add_cell":
110+
// TODO
111+
break;
112+
case "delete_cell":
113+
// TODO
114+
break;
115+
}
116+
return {
117+
status: "success",
118+
};
119+
};
120+
}

0 commit comments

Comments
 (0)