From 2edd122d9b6cf54b8023d869bd8370f8eaa2b152 Mon Sep 17 00:00:00 2001 From: Adeniji Adekunle James Date: Wed, 17 Sep 2025 07:03:22 +0100 Subject: [PATCH] Feat: Add inline code editor (#1156) (#1232) (#1220) (#1235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🚀 Feature: Inline Code Editor This PR adds a comprehensive inline code editing experience to the DyadWrite component. ### ✨ What's New - **Inline Monaco Editor**: Edit code directly within the component using Monaco Editor - **Cancel/Revert**: Cancel changes and revert to original code state - **Language Detection**: Automatic syntax highlighting based on file extensions - **Theme Support**: Proper dark/light mode theming integration https://github.com/user-attachments/assets/c44ab622-6b86-403c-904d-3f327f9719e8 --- ## Summary by cubic Adds an inline Monaco-based code editor to DyadWrite so users can edit code blocks in place, then save or cancel changes. Saves stream edits back to the chat as a dyad-edit block. - **New Features** - Inline editor with Edit, Save, and Cancel; preserves original code and auto-expands when editing. - Language detection from file extension and dark/light theme support. - Save streams edits via useStreamChat as ... tied to the selected chat. - Non-edit view still uses CodeHighlight; visibility toggle and in-progress state respected. - **Refactors** - ChatMessage now uses DyadMarkdownParser instead of VanillaMarkdownParser. --- e2e-tests/chat_mode.spec.ts | 34 +++++++++++ e2e-tests/helpers/test_helper.ts | 21 +++++++ ...ec.ts_dyadwrite-edit-and-cancel-1.aria.yml | 20 ++++++ ...rite-edit-and-save---basic-flow-1.aria.yml | 24 ++++++++ src/components/chat/DyadWrite.tsx | 61 +++++++++++++++++-- 5 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-cancel-1.aria.yml create mode 100644 e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-save---basic-flow-1.aria.yml diff --git a/e2e-tests/chat_mode.spec.ts b/e2e-tests/chat_mode.spec.ts index 329113b..d2ff33d 100644 --- a/e2e-tests/chat_mode.spec.ts +++ b/e2e-tests/chat_mode.spec.ts @@ -22,3 +22,37 @@ test("chat mode selector - ask mode", async ({ po }) => { await po.snapshotServerDump("all-messages"); await po.snapshotMessages({ replaceDumpPath: true }); }); + +test("dyadwrite edit and save - basic flow", async ({ po }) => { + await po.setUp({ autoApprove: true }); + await po.importApp("minimal"); + await po.clickNewChat(); + + await po.sendPrompt( + "Create a simple React component in src/components/Hello.tsx", + ); + await po.waitForChatCompletion(); + + await po.clickEditButton(); + await po.editFileContent("// Test modification\n"); + + await po.saveFile(); + + await po.snapshotMessages({ replaceDumpPath: true }); +}); + +test("dyadwrite edit and cancel", async ({ po }) => { + await po.setUp({ autoApprove: true }); + await po.importApp("minimal"); + await po.clickNewChat(); + + await po.sendPrompt("Create a utility function in src/utils/helper.ts"); + await po.waitForChatCompletion(); + + await po.clickEditButton(); + + await po.editFileContent("// This should be discarded\n"); + await po.cancelEdit(); + + await po.snapshotMessages({ replaceDumpPath: true }); +}); diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index 4bea2bb..8d3c6b7 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -431,6 +431,27 @@ export class PageObject { async clickRestart() { await this.page.getByRole("button", { name: "Restart" }).click(); } + //////////////////////////////// + // Inline code editor + //////////////////////////////// + async clickEditButton() { + await this.page.locator('button:has-text("Edit")').first().click(); + } + + async editFileContent(content: string) { + const editor = this.page.locator(".monaco-editor textarea").first(); + await editor.focus(); + await editor.press("Home"); + await editor.type(content); + } + + async saveFile() { + await this.page.locator('[data-testid="save-file-button"]').click(); + } + + async cancelEdit() { + await this.page.locator('button:has-text("Cancel")').first().click(); + } //////////////////////////////// // Preview panel diff --git a/e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-cancel-1.aria.yml b/e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-cancel-1.aria.yml new file mode 100644 index 0000000..40baf11 --- /dev/null +++ b/e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-cancel-1.aria.yml @@ -0,0 +1,20 @@ +- paragraph: Create a utility function in src/utils/helper.ts +- img +- text: file1.txt +- button "Edit": + - img +- img +- text: file1.txt typescript +- button "Copy": + - img +- paragraph: More EOM +- img +- text: Approved +- img +- text: less than a minute ago +- img +- text: wrote 1 file(s) +- button "Undo": + - img +- button "Retry": + - img \ No newline at end of file diff --git a/e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-save---basic-flow-1.aria.yml b/e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-save---basic-flow-1.aria.yml new file mode 100644 index 0000000..8a18bd3 --- /dev/null +++ b/e2e-tests/snapshots/chat_mode.spec.ts_dyadwrite-edit-and-save---basic-flow-1.aria.yml @@ -0,0 +1,24 @@ +- paragraph: Create a simple React component in src/components/Hello.tsx +- img +- text: file1.txt +- button "Cancel": + - img +- img +- text: file1.txt file1.txt +- button [disabled]: + - img +- img +- code: + - textbox "Editor content" + - list +- paragraph: More EOM +- img +- text: Approved +- img +- text: less than a minute ago +- img +- text: wrote 1 file(s) +- button "Undo": + - img +- button "Retry": + - img \ No newline at end of file diff --git a/src/components/chat/DyadWrite.tsx b/src/components/chat/DyadWrite.tsx index 49dc09f..6b45709 100644 --- a/src/components/chat/DyadWrite.tsx +++ b/src/components/chat/DyadWrite.tsx @@ -7,9 +7,14 @@ import { Pencil, Loader, CircleX, + Edit, + X, } from "lucide-react"; import { CodeHighlight } from "./CodeHighlight"; import { CustomTagState } from "./stateTypes"; +import { FileEditor } from "../preview_panel/FileEditor"; +import { useAtomValue } from "jotai"; +import { selectedAppIdAtom } from "@/atoms/appAtoms"; interface DyadWriteProps { children?: ReactNode; @@ -30,9 +35,20 @@ export const DyadWrite: React.FC = ({ const path = pathProp || node?.properties?.path || ""; const description = descriptionProp || node?.properties?.description || ""; const state = node?.properties?.state as CustomTagState; - const inProgress = state === "pending"; - const aborted = state === "aborted"; + const aborted = state === "aborted"; + const appId = useAtomValue(selectedAppIdAtom); + const [isEditing, setIsEditing] = useState(false); + const inProgress = state === "pending"; + + const handleCancel = () => { + setIsEditing(false); + }; + + const handleEdit = () => { + setIsEditing(true); + setIsContentVisible(true); + }; // Extract filename from path const fileName = path ? path.split("/").pop() : ""; @@ -69,6 +85,35 @@ export const DyadWrite: React.FC = ({ )}
+ {!inProgress && ( + <> + {isEditing ? ( + <> + + + ) : ( + + )} + + )} {isContentVisible ? ( = ({ className="text-xs cursor-text" onClick={(e) => e.stopPropagation()} > - - {children} - + {isEditing ? ( +
+ +
+ ) : ( + + {children} + + )}
)}