Feat: Add inline code editor (#1156) (#1232) (#1220) (#1235)

## 🚀 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


    
<!-- This is an auto-generated description by cubic. -->
---

## 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 <dyad-edit
path="...">...</dyad-edit> 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.

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Adeniji Adekunle James
2025-09-17 07:03:22 +01:00
committed by GitHub
parent 8c3fdb0ad0
commit 2edd122d9b
5 changed files with 155 additions and 5 deletions

View File

@@ -22,3 +22,37 @@ test("chat mode selector - ask mode", async ({ po }) => {
await po.snapshotServerDump("all-messages"); await po.snapshotServerDump("all-messages");
await po.snapshotMessages({ replaceDumpPath: true }); 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 });
});

View File

@@ -431,6 +431,27 @@ export class PageObject {
async clickRestart() { async clickRestart() {
await this.page.getByRole("button", { name: "Restart" }).click(); 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 // Preview panel

View File

@@ -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

View File

@@ -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

View File

@@ -7,9 +7,14 @@ import {
Pencil, Pencil,
Loader, Loader,
CircleX, CircleX,
Edit,
X,
} from "lucide-react"; } from "lucide-react";
import { CodeHighlight } from "./CodeHighlight"; import { CodeHighlight } from "./CodeHighlight";
import { CustomTagState } from "./stateTypes"; import { CustomTagState } from "./stateTypes";
import { FileEditor } from "../preview_panel/FileEditor";
import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
interface DyadWriteProps { interface DyadWriteProps {
children?: ReactNode; children?: ReactNode;
@@ -30,9 +35,20 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
const path = pathProp || node?.properties?.path || ""; const path = pathProp || node?.properties?.path || "";
const description = descriptionProp || node?.properties?.description || ""; const description = descriptionProp || node?.properties?.description || "";
const state = node?.properties?.state as CustomTagState; 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 // Extract filename from path
const fileName = path ? path.split("/").pop() : ""; const fileName = path ? path.split("/").pop() : "";
@@ -69,6 +85,35 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
)} )}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
{!inProgress && (
<>
{isEditing ? (
<>
<button
onClick={(e) => {
e.stopPropagation();
handleCancel();
}}
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 px-2 py-1 rounded cursor-pointer"
>
<X size={14} />
Cancel
</button>
</>
) : (
<button
onClick={(e) => {
e.stopPropagation();
handleEdit();
}}
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 px-2 py-1 rounded cursor-pointer"
>
<Edit size={14} />
Edit
</button>
)}
</>
)}
{isContentVisible ? ( {isContentVisible ? (
<ChevronsDownUp <ChevronsDownUp
size={20} size={20}
@@ -98,9 +143,15 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
className="text-xs cursor-text" className="text-xs cursor-text"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<CodeHighlight className="language-typescript"> {isEditing ? (
{children} <div className="h-96 min-h-96 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
</CodeHighlight> <FileEditor appId={appId ?? null} filePath={path} />
</div>
) : (
<CodeHighlight className="language-typescript">
{children}
</CodeHighlight>
)}
</div> </div>
)} )}
</div> </div>