## 🚀 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:
committed by
GitHub
parent
8c3fdb0ad0
commit
2edd122d9b
@@ -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 });
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user