Unverified 提交 2edd122d authored 作者: Adeniji Adekunle James's avatar Adeniji Adekunle James 提交者: GitHub

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. -->
上级 8c3fdb0a
...@@ -22,3 +22,37 @@ test("chat mode selector - ask mode", async ({ po }) => { ...@@ -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 { ...@@ -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
......
- 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
- 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
...@@ -7,9 +7,14 @@ import { ...@@ -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> = ({ ...@@ -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> = ({ ...@@ -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> = ({ ...@@ -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>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论