diff --git a/e2e-tests/fixtures/rename-edit.md b/e2e-tests/fixtures/rename-edit.md
new file mode 100644
index 0000000..20b079e
--- /dev/null
+++ b/e2e-tests/fixtures/rename-edit.md
@@ -0,0 +1,6 @@
+
+
+
+
+// newly added content to renamed file should exist
+
diff --git a/e2e-tests/rename_edit.spec.ts b/e2e-tests/rename_edit.spec.ts
new file mode 100644
index 0000000..7244e4f
--- /dev/null
+++ b/e2e-tests/rename_edit.spec.ts
@@ -0,0 +1,9 @@
+import { test } from "./helpers/test_helper";
+
+test("rename then edit works", async ({ po }) => {
+ await po.setUp({ autoApprove: true });
+ await po.importApp("minimal");
+
+ await po.sendPrompt("tc=rename-edit");
+ await po.snapshotAppFiles({ name: "rename-edit" });
+});
diff --git a/e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt b/e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt
new file mode 100644
index 0000000..a8eae25
--- /dev/null
+++ b/e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt
@@ -0,0 +1,185 @@
+=== .gitignore ===
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+
+=== file1.txt ===
+A file (2)
+
+=== index.html ===
+
+
+
+
+
+ dyad-generated-app
+
+
+
+
+
+
+
+
+
+=== package.json ===
+{
+ "name": "vite_react_shadcn_ts",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "build:dev": "vite build --mode development",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/node": "^22.5.5",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react-swc": "^3.9.0",
+ "typescript": "^5.5.3",
+ "vite": "^6.3.4"
+ },
+ "packageManager": ""
+}
+
+=== src/main.tsx ===
+import { createRoot } from "react-dom/client";
+import App from "./App.tsx";
+
+createRoot(document.getElementById("root")!).render();
+
+
+=== src/Renamed.tsx ===
+// newly added content to renamed file should exist
+
+=== src/vite-env.d.ts ===
+///
+
+
+=== tsconfig.app.json ===
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noImplicitAny": false,
+ "noFallthroughCasesInSwitch": false,
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
+
+
+=== tsconfig.json ===
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "noImplicitAny": false,
+ "noUnusedParameters": false,
+ "skipLibCheck": true,
+ "allowJs": true,
+ "noUnusedLocals": false,
+ "strictNullChecks": false
+ }
+}
+
+
+=== tsconfig.node.json ===
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
+
+
+=== vite.config.ts ===
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+import path from "path";
+
+export default defineConfig(() => ({
+ server: {
+ host: "::",
+ port: 8080,
+ },
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+}));
diff --git a/playwright.config.ts b/playwright.config.ts
index 3b01438..f6db8fa 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -3,8 +3,7 @@ import { PlaywrightTestConfig } from "@playwright/test";
const config: PlaywrightTestConfig = {
testDir: "./e2e-tests",
workers: 1,
- retries: process.env.CI ? 2 : 1,
- // maxFailures: 1,
+ retries: process.env.CI ? 2 : 0,
timeout: process.env.CI ? 180_000 : 30_000,
// Use a custom snapshot path template because Playwright's default
// is platform-specific which isn't necessary for Dyad e2e tests
diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts
index 4d9d7e0..4e89b71 100644
--- a/src/ipc/processors/response_processor.ts
+++ b/src/ipc/processors/response_processor.ts
@@ -305,30 +305,55 @@ export async function processFullResponseActions(
}
}
- // Process all file writes
- for (const tag of dyadWriteTags) {
- const filePath = tag.path;
- const content = tag.content;
+ //////////////////////
+ // File operations //
+ // Do it in this order:
+ // 1. Deletes
+ // 2. Renames
+ // 3. Writes
+ //
+ // Why?
+ // - Deleting first avoids path conflicts before the other operations.
+ // - LLMs like to rename and then edit the same file.
+ //////////////////////
+
+ // Process all file deletions
+ for (const filePath of dyadDeletePaths) {
const fullFilePath = path.join(appPath, filePath);
- // Ensure directory exists
- const dirPath = path.dirname(fullFilePath);
- fs.mkdirSync(dirPath, { recursive: true });
+ // Delete the file if it exists
+ if (fs.existsSync(fullFilePath)) {
+ if (fs.lstatSync(fullFilePath).isDirectory()) {
+ fs.rmdirSync(fullFilePath, { recursive: true });
+ } else {
+ fs.unlinkSync(fullFilePath);
+ }
+ logger.log(`Successfully deleted file: ${fullFilePath}`);
+ deletedFiles.push(filePath);
- // Write file content
- fs.writeFileSync(fullFilePath, content);
- logger.log(`Successfully wrote file: ${fullFilePath}`);
- writtenFiles.push(filePath);
+ // Remove the file from git
+ try {
+ await git.remove({
+ fs,
+ dir: appPath,
+ filepath: filePath,
+ });
+ } catch (error) {
+ logger.warn(`Failed to git remove deleted file ${filePath}:`, error);
+ // Continue even if remove fails as the file was still deleted
+ }
+ } else {
+ logger.warn(`File to delete does not exist: ${fullFilePath}`);
+ }
if (isServerFunction(filePath)) {
try {
- await deploySupabaseFunctions({
+ await deleteSupabaseFunction({
supabaseProjectId: chatWithApp.app.supabaseProjectId!,
- functionName: path.basename(path.dirname(filePath)),
- content: content,
+ functionName: getFunctionNameFromPath(filePath),
});
} catch (error) {
errors.push({
- message: `Failed to deploy Supabase function: ${filePath}`,
+ message: `Failed to delete Supabase function: ${filePath}`,
error: error,
});
}
@@ -398,43 +423,30 @@ export async function processFullResponseActions(
}
}
- // Process all file deletions
- for (const filePath of dyadDeletePaths) {
+ // Process all file writes
+ for (const tag of dyadWriteTags) {
+ const filePath = tag.path;
+ const content = tag.content;
const fullFilePath = path.join(appPath, filePath);
- // Delete the file if it exists
- if (fs.existsSync(fullFilePath)) {
- if (fs.lstatSync(fullFilePath).isDirectory()) {
- fs.rmdirSync(fullFilePath, { recursive: true });
- } else {
- fs.unlinkSync(fullFilePath);
- }
- logger.log(`Successfully deleted file: ${fullFilePath}`);
- deletedFiles.push(filePath);
+ // Ensure directory exists
+ const dirPath = path.dirname(fullFilePath);
+ fs.mkdirSync(dirPath, { recursive: true });
- // Remove the file from git
- try {
- await git.remove({
- fs,
- dir: appPath,
- filepath: filePath,
- });
- } catch (error) {
- logger.warn(`Failed to git remove deleted file ${filePath}:`, error);
- // Continue even if remove fails as the file was still deleted
- }
- } else {
- logger.warn(`File to delete does not exist: ${fullFilePath}`);
- }
+ // Write file content
+ fs.writeFileSync(fullFilePath, content);
+ logger.log(`Successfully wrote file: ${fullFilePath}`);
+ writtenFiles.push(filePath);
if (isServerFunction(filePath)) {
try {
- await deleteSupabaseFunction({
+ await deploySupabaseFunctions({
supabaseProjectId: chatWithApp.app.supabaseProjectId!,
- functionName: getFunctionNameFromPath(filePath),
+ functionName: path.basename(path.dirname(filePath)),
+ content: content,
});
} catch (error) {
errors.push({
- message: `Failed to delete Supabase function: ${filePath}`,
+ message: `Failed to deploy Supabase function: ${filePath}`,
error: error,
});
}