Problems: auto-fix & problem panel (#541)
Test cases: - [x] create-ts-errors - [x] with auto-fix - [x] without auto-fix - [x] create-unfixable-ts-errors - [x] manually edit file & click recheck - [x] fix all - [x] delete and rename case THINGS - [x] error handling for checkProblems isn't working as expected - [x] make sure it works for both default templates (add tests) - [x] fix bad animation - [x] change file context (prompt/files) IF everything passes in Windows AND defensive try catch... then enable by default - [x] enable auto-fix by default
This commit is contained in:
9
e2e-tests/fixtures/create-ts-errors-complex.md
Normal file
9
e2e-tests/fixtures/create-ts-errors-complex.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Tests delete-rename-write order
|
||||||
|
<dyad-delete path="src/main.tsx">
|
||||||
|
</dyad-delete>
|
||||||
|
<dyad-rename from="src/App.tsx" to="src/main.tsx">
|
||||||
|
</dyad-rename>
|
||||||
|
<dyad-write path="src/main.tsx" description="final main.tsx file.">
|
||||||
|
finalMainTsxFileWithError();
|
||||||
|
</dyad-write>
|
||||||
|
EOM
|
||||||
10
e2e-tests/fixtures/create-ts-errors.md
Normal file
10
e2e-tests/fixtures/create-ts-errors.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
This will get a TypeScript error.
|
||||||
|
|
||||||
|
<dyad-write path="src/bad-file.ts" description="This will get a TypeScript error.">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod();
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
EOM
|
||||||
11
e2e-tests/fixtures/create-unfixable-ts-errors.md
Normal file
11
e2e-tests/fixtures/create-unfixable-ts-errors.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
This should not get fixed
|
||||||
|
|
||||||
|
<dyad-write path="src/bad-file.ts" description="This will produce 5 TypeScript errors.">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
import NonExistentClass2 from 'non-existent-class';
|
||||||
|
import NonExistentClass3 from 'non-existent-class';
|
||||||
|
import NonExistentClass4 from 'non-existent-class';
|
||||||
|
import NonExistentClass5 from 'non-existent-class';
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
EOM
|
||||||
@@ -205,7 +205,12 @@ export class PageObject {
|
|||||||
async setUp({
|
async setUp({
|
||||||
autoApprove = false,
|
autoApprove = false,
|
||||||
nativeGit = false,
|
nativeGit = false,
|
||||||
}: { autoApprove?: boolean; nativeGit?: boolean } = {}) {
|
disableAutoFixProblems = false,
|
||||||
|
}: {
|
||||||
|
autoApprove?: boolean;
|
||||||
|
nativeGit?: boolean;
|
||||||
|
disableAutoFixProblems?: boolean;
|
||||||
|
} = {}) {
|
||||||
await this.baseSetup();
|
await this.baseSetup();
|
||||||
await this.goToSettingsTab();
|
await this.goToSettingsTab();
|
||||||
if (autoApprove) {
|
if (autoApprove) {
|
||||||
@@ -214,6 +219,9 @@ export class PageObject {
|
|||||||
if (nativeGit) {
|
if (nativeGit) {
|
||||||
await this.toggleNativeGit();
|
await this.toggleNativeGit();
|
||||||
}
|
}
|
||||||
|
if (disableAutoFixProblems) {
|
||||||
|
await this.toggleAutoFixProblems();
|
||||||
|
}
|
||||||
await this.setUpTestProvider();
|
await this.setUpTestProvider();
|
||||||
await this.setUpTestModel();
|
await this.setUpTestModel();
|
||||||
|
|
||||||
@@ -231,6 +239,61 @@ export class PageObject {
|
|||||||
await this.goToAppsTab();
|
await this.goToAppsTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async runPnpmInstall() {
|
||||||
|
const appPath = await this.getCurrentAppPath();
|
||||||
|
if (!appPath) {
|
||||||
|
throw new Error("No app selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxRetries = 3;
|
||||||
|
let lastError: any;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
`Running 'pnpm install' in ${appPath} (attempt ${attempt}/${maxRetries})`,
|
||||||
|
);
|
||||||
|
execSync("pnpm install", {
|
||||||
|
cwd: appPath,
|
||||||
|
stdio: "pipe",
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
console.log(`'pnpm install' succeeded on attempt ${attempt}`);
|
||||||
|
return; // Success, exit the function
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error;
|
||||||
|
console.error(
|
||||||
|
`Attempt ${attempt}/${maxRetries} failed to run 'pnpm install' in ${appPath}`,
|
||||||
|
);
|
||||||
|
console.error(`Exit code: ${error.status}`);
|
||||||
|
console.error(`Command: ${error.cmd || "pnpm install"}`);
|
||||||
|
|
||||||
|
if (error.stdout) {
|
||||||
|
console.error(`STDOUT:\n${error.stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.stderr) {
|
||||||
|
console.error(`STDERR:\n${error.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this wasn't the last attempt, wait a bit before retrying
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
const delayMs = 1000 * attempt; // Exponential backoff: 1s, 2s
|
||||||
|
console.log(`Waiting ${delayMs}ms before retry...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All attempts failed, throw the last error with enhanced message
|
||||||
|
throw new Error(
|
||||||
|
`pnpm install failed in ${appPath} after ${maxRetries} attempts. ` +
|
||||||
|
`Exit code: ${lastError.status}. ` +
|
||||||
|
`${lastError.stderr ? `Error: ${lastError.stderr}` : ""}` +
|
||||||
|
`${lastError.stdout ? ` Output: ${lastError.stdout}` : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async setUpDyadProvider() {
|
async setUpDyadProvider() {
|
||||||
await this.page
|
await this.page
|
||||||
.locator("div")
|
.locator("div")
|
||||||
@@ -335,7 +398,7 @@ export class PageObject {
|
|||||||
throw new Error("Messages list not found");
|
throw new Error("Messages list not found");
|
||||||
}
|
}
|
||||||
messagesList.innerHTML = messagesList.innerHTML.replace(
|
messagesList.innerHTML = messagesList.innerHTML.replace(
|
||||||
/\[\[dyad-dump-path=([^\]]+)\]\]/,
|
/\[\[dyad-dump-path=([^\]]+)\]\]/g,
|
||||||
"[[dyad-dump-path=*]]",
|
"[[dyad-dump-path=*]]",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -355,6 +418,27 @@ export class PageObject {
|
|||||||
await this.page.getByRole("button", { name: "Restart" }).click();
|
await this.page.getByRole("button", { name: "Restart" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Preview panel
|
||||||
|
////////////////////////////////
|
||||||
|
|
||||||
|
async selectPreviewMode(mode: "code" | "problems" | "preview") {
|
||||||
|
await this.page.getByTestId(`${mode}-mode-button`).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickRecheckProblems() {
|
||||||
|
await this.page.getByTestId("recheck-button").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickFixAllProblems() {
|
||||||
|
await this.page.getByTestId("fix-all-button").click();
|
||||||
|
await this.waitForChatCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
async snapshotProblemsPane() {
|
||||||
|
await expect(this.page.getByTestId("problems-pane")).toMatchAriaSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
async clickRebuild() {
|
async clickRebuild() {
|
||||||
await this.clickPreviewMoreOptions();
|
await this.clickPreviewMoreOptions();
|
||||||
await this.page.getByText("Rebuild").click();
|
await this.page.getByText("Rebuild").click();
|
||||||
@@ -402,6 +486,12 @@ export class PageObject {
|
|||||||
return this.page.getByTestId("preview-iframe-element");
|
return this.page.getByTestId("preview-iframe-element");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectPreviewIframeIsVisible() {
|
||||||
|
return expect(this.getPreviewIframeElement()).toBeVisible({
|
||||||
|
timeout: Timeout.LONG,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async clickFixErrorWithAI() {
|
async clickFixErrorWithAI() {
|
||||||
await this.page.getByRole("button", { name: "Fix error with AI" }).click();
|
await this.page.getByRole("button", { name: "Fix error with AI" }).click();
|
||||||
}
|
}
|
||||||
@@ -438,23 +528,46 @@ export class PageObject {
|
|||||||
|
|
||||||
async snapshotServerDump(
|
async snapshotServerDump(
|
||||||
type: "all-messages" | "last-message" | "request" = "all-messages",
|
type: "all-messages" | "last-message" | "request" = "all-messages",
|
||||||
{ name = "" }: { name?: string } = {},
|
{ name = "", dumpIndex = -1 }: { name?: string; dumpIndex?: number } = {},
|
||||||
) {
|
) {
|
||||||
// Get the text content of the messages list
|
// Get the text content of the messages list
|
||||||
const messagesListText = await this.page
|
const messagesListText = await this.page
|
||||||
.getByTestId("messages-list")
|
.getByTestId("messages-list")
|
||||||
.textContent();
|
.textContent();
|
||||||
|
|
||||||
// Find the dump path using regex
|
// Find ALL dump paths using global regex
|
||||||
const dumpPathMatch = messagesListText?.match(
|
const dumpPathMatches = messagesListText?.match(
|
||||||
/.*\[\[dyad-dump-path=([^\]]+)\]\]/,
|
/\[\[dyad-dump-path=([^\]]+)\]\]/g,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dumpPathMatch) {
|
if (!dumpPathMatches || dumpPathMatches.length === 0) {
|
||||||
throw new Error("No dump path found in messages list");
|
throw new Error("No dump path found in messages list");
|
||||||
}
|
}
|
||||||
|
|
||||||
const dumpFilePath = dumpPathMatch[1];
|
// Extract the actual paths from the matches
|
||||||
|
const dumpPaths = dumpPathMatches
|
||||||
|
.map((match) => {
|
||||||
|
const pathMatch = match.match(/\[\[dyad-dump-path=([^\]]+)\]\]/);
|
||||||
|
return pathMatch ? pathMatch[1] : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Select the dump path based on index
|
||||||
|
// -1 means last, -2 means second to last, etc.
|
||||||
|
// 0 means first, 1 means second, etc.
|
||||||
|
const selectedIndex =
|
||||||
|
dumpIndex < 0 ? dumpPaths.length + dumpIndex : dumpIndex;
|
||||||
|
|
||||||
|
if (selectedIndex < 0 || selectedIndex >= dumpPaths.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Dump index ${dumpIndex} is out of range. Found ${dumpPaths.length} dump paths.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dumpFilePath = dumpPaths[selectedIndex];
|
||||||
|
if (!dumpFilePath) {
|
||||||
|
throw new Error("No dump file path found");
|
||||||
|
}
|
||||||
|
|
||||||
// Read the JSON file
|
// Read the JSON file
|
||||||
const dumpContent: string = (
|
const dumpContent: string = (
|
||||||
@@ -701,6 +814,10 @@ export class PageObject {
|
|||||||
await this.page.getByRole("switch", { name: "Enable Native Git" }).click();
|
await this.page.getByRole("switch", { name: "Enable Native Git" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toggleAutoFixProblems() {
|
||||||
|
await this.page.getByRole("switch", { name: "Auto-fix problems" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
async snapshotSettings() {
|
async snapshotSettings() {
|
||||||
const settings = path.join(this.userDataDir, "user-settings.json");
|
const settings = path.join(this.userDataDir, "user-settings.json");
|
||||||
const settingsContent = fs.readFileSync(settings, "utf-8");
|
const settingsContent = fs.readFileSync(settings, "utf-8");
|
||||||
@@ -740,10 +857,16 @@ export class PageObject {
|
|||||||
await this.page.getByRole("link", { name: "Hub" }).click();
|
await this.page.getByRole("link", { name: "Hub" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectTemplate(templateName: string) {
|
private async selectTemplate(templateName: string) {
|
||||||
await this.page.getByRole("img", { name: templateName }).click();
|
await this.page.getByRole("img", { name: templateName }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async selectHubTemplate(templateName: "Next.js Template") {
|
||||||
|
await this.goToHubTab();
|
||||||
|
await this.selectTemplate(templateName);
|
||||||
|
await this.goToAppsTab();
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// Toast assertions
|
// Toast assertions
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
|
|||||||
145
e2e-tests/problems.spec.ts
Normal file
145
e2e-tests/problems.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { test, testSkipIfWindows } from "./helpers/test_helper";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const MINIMAL_APP = "minimal-with-ai-rules";
|
||||||
|
|
||||||
|
testSkipIfWindows("problems auto-fix - enabled", async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.importApp(MINIMAL_APP);
|
||||||
|
await po.expectPreviewIframeIsVisible();
|
||||||
|
|
||||||
|
await po.sendPrompt("tc=create-ts-errors");
|
||||||
|
|
||||||
|
await po.snapshotServerDump("all-messages", { dumpIndex: -2 });
|
||||||
|
await po.snapshotServerDump("all-messages", { dumpIndex: -1 });
|
||||||
|
|
||||||
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
testSkipIfWindows(
|
||||||
|
"problems auto-fix - gives up after 2 attempts",
|
||||||
|
async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.importApp(MINIMAL_APP);
|
||||||
|
await po.expectPreviewIframeIsVisible();
|
||||||
|
|
||||||
|
await po.sendPrompt("tc=create-unfixable-ts-errors");
|
||||||
|
|
||||||
|
await po.snapshotServerDump("all-messages", { dumpIndex: -2 });
|
||||||
|
await po.snapshotServerDump("all-messages", { dumpIndex: -1 });
|
||||||
|
|
||||||
|
await po.page.getByTestId("problem-summary").last().click();
|
||||||
|
await expect(
|
||||||
|
po.page.getByTestId("problem-summary").last(),
|
||||||
|
).toMatchAriaSnapshot();
|
||||||
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testSkipIfWindows(
|
||||||
|
"problems auto-fix - complex delete-rename-write",
|
||||||
|
async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.importApp(MINIMAL_APP);
|
||||||
|
await po.expectPreviewIframeIsVisible();
|
||||||
|
|
||||||
|
await po.sendPrompt("tc=create-ts-errors-complex");
|
||||||
|
|
||||||
|
await po.snapshotServerDump("all-messages", { dumpIndex: -2 });
|
||||||
|
await po.snapshotServerDump("all-messages", { dumpIndex: -1 });
|
||||||
|
|
||||||
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("problems auto-fix - disabled", async ({ po }) => {
|
||||||
|
await po.setUp({ disableAutoFixProblems: true });
|
||||||
|
await po.importApp(MINIMAL_APP);
|
||||||
|
await po.expectPreviewIframeIsVisible();
|
||||||
|
|
||||||
|
await po.sendPrompt("tc=create-ts-errors");
|
||||||
|
|
||||||
|
await po.snapshotMessages();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problems - fix all", async ({ po }) => {
|
||||||
|
await po.setUp({ disableAutoFixProblems: true });
|
||||||
|
await po.importApp(MINIMAL_APP);
|
||||||
|
const appPath = await po.getCurrentAppPath();
|
||||||
|
const badFilePath = path.join(appPath, "src", "bad-file.tsx");
|
||||||
|
fs.writeFileSync(
|
||||||
|
badFilePath,
|
||||||
|
`const App = () => <div>Minimal imported app</div>;
|
||||||
|
nonExistentFunction1();
|
||||||
|
nonExistentFunction2();
|
||||||
|
nonExistentFunction3();
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
await po.runPnpmInstall();
|
||||||
|
|
||||||
|
await po.sendPrompt("tc=create-ts-errors");
|
||||||
|
await po.selectPreviewMode("problems");
|
||||||
|
await po.clickFixAllProblems();
|
||||||
|
|
||||||
|
await po.snapshotServerDump("last-message");
|
||||||
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problems - manual edit (react/vite)", async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.sendPrompt("tc=1");
|
||||||
|
|
||||||
|
const appPath = await po.getCurrentAppPath();
|
||||||
|
const badFilePath = path.join(appPath, "src", "bad-file.tsx");
|
||||||
|
fs.writeFileSync(
|
||||||
|
badFilePath,
|
||||||
|
`const App = () => <div>Minimal imported app</div>;
|
||||||
|
nonExistentFunction();
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
await po.runPnpmInstall();
|
||||||
|
await po.clickTogglePreviewPanel();
|
||||||
|
|
||||||
|
await po.selectPreviewMode("problems");
|
||||||
|
await po.clickRecheckProblems();
|
||||||
|
await po.snapshotProblemsPane();
|
||||||
|
|
||||||
|
fs.unlinkSync(badFilePath);
|
||||||
|
|
||||||
|
await po.clickRecheckProblems();
|
||||||
|
await po.snapshotProblemsPane();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problems - manual edit (next.js)", async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.selectHubTemplate("Next.js Template");
|
||||||
|
await po.sendPrompt("tc=1");
|
||||||
|
|
||||||
|
const appPath = await po.getCurrentAppPath();
|
||||||
|
const badFilePath = path.join(appPath, "src", "bad-file.tsx");
|
||||||
|
fs.writeFileSync(
|
||||||
|
badFilePath,
|
||||||
|
`const App = () => <div>Minimal imported app</div>;
|
||||||
|
nonExistentFunction();
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
await po.runPnpmInstall();
|
||||||
|
await po.clickTogglePreviewPanel();
|
||||||
|
|
||||||
|
await po.selectPreviewMode("problems");
|
||||||
|
await po.clickRecheckProblems();
|
||||||
|
await po.snapshotProblemsPane();
|
||||||
|
|
||||||
|
fs.unlinkSync(badFilePath);
|
||||||
|
|
||||||
|
await po.clickRecheckProblems();
|
||||||
|
await po.snapshotProblemsPane();
|
||||||
|
});
|
||||||
@@ -81,10 +81,7 @@ testSkipIfWindows("upgrade app to select component", async ({ po }) => {
|
|||||||
testSkipIfWindows("select component next.js", async ({ po }) => {
|
testSkipIfWindows("select component next.js", async ({ po }) => {
|
||||||
await po.setUp();
|
await po.setUp();
|
||||||
|
|
||||||
// Select Next.js template
|
await po.selectHubTemplate("Next.js Template");
|
||||||
await po.goToHubTab();
|
|
||||||
await po.selectTemplate("Next.js Template");
|
|
||||||
await po.goToAppsTab();
|
|
||||||
|
|
||||||
await po.sendPrompt("tc=basic");
|
await po.sendPrompt("tc=basic");
|
||||||
await po.clickTogglePreviewPanel();
|
await po.clickTogglePreviewPanel();
|
||||||
|
|||||||
@@ -14,5 +14,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
- paragraph: tc=create-ts-errors
|
||||||
|
- paragraph: This will get a TypeScript error.
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: This will get a TypeScript error."
|
||||||
|
- paragraph: EOM
|
||||||
|
- paragraph: "Fix these 3 TypeScript compile-time errors:"
|
||||||
|
- list:
|
||||||
|
- listitem: src/bad-file.tsx:2:1 - Cannot find name 'nonExistentFunction1'. (TS2304)
|
||||||
|
- listitem: src/bad-file.tsx:3:1 - Cannot find name 'nonExistentFunction2'. (TS2304)
|
||||||
|
- listitem: src/bad-file.tsx:4:1 - Cannot find name 'nonExistentFunction3'. (TS2304)
|
||||||
|
- paragraph: Please fix all errors in a concise way.
|
||||||
|
- img
|
||||||
|
- text: file1.txt
|
||||||
|
- img
|
||||||
|
- text: file1.txt
|
||||||
|
- paragraph: More EOM
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- button "Retry":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 3 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/bad-file.tsx:2:1 - Cannot find name 'nonExistentFunction1'. (TS2304)
|
||||||
|
2. src/bad-file.tsx:3:1 - Cannot find name 'nonExistentFunction2'. (TS2304)
|
||||||
|
3. src/bad-file.tsx:4:1 - Cannot find name 'nonExistentFunction3'. (TS2304)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- img
|
||||||
|
- text: 1 error
|
||||||
|
- button "Recheck":
|
||||||
|
- img
|
||||||
|
- button "Fix All":
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: src/bad-file.tsx 2:1
|
||||||
|
- paragraph: Cannot find name 'nonExistentFunction'.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- paragraph: No problems found
|
||||||
|
- button "Recheck":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- img
|
||||||
|
- text: 1 error
|
||||||
|
- button "Recheck":
|
||||||
|
- img
|
||||||
|
- button "Fix All":
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: src/bad-file.tsx 2:3
|
||||||
|
- paragraph: Cannot find name 'nonExistentFunction'.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- paragraph: No problems found
|
||||||
|
- button "Recheck":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- img
|
||||||
|
- text: 1 error
|
||||||
|
- button "Recheck":
|
||||||
|
- img
|
||||||
|
- button "Fix All":
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: src/bad-file.tsx 2:1
|
||||||
|
- paragraph: Cannot find name 'nonExistentFunction'.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- paragraph: No problems found
|
||||||
|
- button "Recheck":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
- paragraph: tc=create-ts-errors-complex
|
||||||
|
- paragraph: Tests delete-rename-write order
|
||||||
|
- img
|
||||||
|
- text: main.tsx Delete src/main.tsx
|
||||||
|
- img
|
||||||
|
- text: "App.tsx → main.tsx Rename From: src/App.tsx To: src/main.tsx"
|
||||||
|
- img
|
||||||
|
- text: main.tsx
|
||||||
|
- img
|
||||||
|
- text: "src/main.tsx Summary: final main.tsx file."
|
||||||
|
- paragraph: EOM
|
||||||
|
- img
|
||||||
|
- text: Auto-fix1 problems
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: Fix remaining error."
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- img
|
||||||
|
- text: Auto-fix1 problems
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: Fix remaining error."
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- button "Retry":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
===
|
||||||
|
role: system
|
||||||
|
message:
|
||||||
|
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||||
|
You make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||||
|
|
||||||
|
# App Preview / Commands
|
||||||
|
|
||||||
|
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||||
|
|
||||||
|
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||||
|
- **Restart**: This will restart the app server.
|
||||||
|
- **Refresh**: This will refresh the app preview page.
|
||||||
|
|
||||||
|
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||||
|
<dyad-command type="rebuild"></dyad-command>
|
||||||
|
<dyad-command type="restart"></dyad-command>
|
||||||
|
<dyad-command type="refresh"></dyad-command>
|
||||||
|
|
||||||
|
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||||
|
|
||||||
|
# Guidelines
|
||||||
|
|
||||||
|
Always reply to the user in the same language they are using.
|
||||||
|
|
||||||
|
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||||
|
- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||||
|
- Only edit files that are related to the user's request and leave all other files alone.
|
||||||
|
|
||||||
|
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||||
|
|
||||||
|
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||||
|
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||||
|
- Use <dyad-rename> for renaming files.
|
||||||
|
- Use <dyad-delete> for removing files.
|
||||||
|
- Use <dyad-add-dependency> for installing packages.
|
||||||
|
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||||
|
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||||
|
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||||
|
|
||||||
|
Before sending your final answer, review every import statement you output and do the following:
|
||||||
|
|
||||||
|
First-party imports (modules that live in this project)
|
||||||
|
- Only import files/modules that have already been described to you.
|
||||||
|
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||||
|
|
||||||
|
Third-party imports (anything that would come from npm)
|
||||||
|
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||||
|
|
||||||
|
Do not leave any import unresolved.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: Adding a new component
|
||||||
|
|
||||||
|
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||||
|
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||||
|
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||||
|
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from './components/Button';
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||||
|
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||||
|
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of the component) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||||
|
|
||||||
|
## Example 2: Installing a package and creating a Toast notification system
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add a toast
|
||||||
|
### System response
|
||||||
|
|
||||||
|
OK first, you need to install the package.
|
||||||
|
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||||
|
|
||||||
|
### System response
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
const ToastProvider = () => { <Toaster/> };
|
||||||
|
export default ToastProvider;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import ToastProvider from './components/ToastProvider';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ToastProvider />
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
export const showSuccess = (message) => {
|
||||||
|
toast.success(message);
|
||||||
|
};
|
||||||
|
export const showError = (message) => {
|
||||||
|
toast.error(message);
|
||||||
|
};
|
||||||
|
export const showLoading = (message) => {
|
||||||
|
return toast.loading(message);
|
||||||
|
};
|
||||||
|
export const dismissToast = (toastId) => {
|
||||||
|
toast.dismiss(toastId);
|
||||||
|
};
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||||
|
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||||
|
|
||||||
|
## Example 3: Renaming and deleting files
|
||||||
|
|
||||||
|
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
|
||||||
|
const ProfileCard = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{user.avatar ? (
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-12 h-12 rounded-full mr-4"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<User className="text-gray-500" size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||||
|
<p className="text-gray-500">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (user details section) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileCard;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ProfileCard from '../components/ProfileCard';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<ProfileCard user={currentUser} />
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of dashboard content) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||||
|
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||||
|
|
||||||
|
# Additional Guidelines
|
||||||
|
|
||||||
|
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.
|
||||||
|
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||||
|
|
||||||
|
Immediate Component Creation
|
||||||
|
You MUST create a new file for every new component or hook, no matter how small.
|
||||||
|
Never add new components to existing files, even if they seem related.
|
||||||
|
Aim for components that are 100 lines of code or less.
|
||||||
|
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||||
|
|
||||||
|
Important Rules for dyad-write operations:
|
||||||
|
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||||
|
- Always specify the correct file path when using dyad-write.
|
||||||
|
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||||
|
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||||
|
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||||
|
- Prioritize creating small, focused files and components.
|
||||||
|
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||||
|
|
||||||
|
Coding guidelines
|
||||||
|
- ALWAYS generate responsive designs.
|
||||||
|
- Use toasts components to inform the user about important events.
|
||||||
|
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||||
|
|
||||||
|
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||||
|
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||||
|
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
|
||||||
|
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||||
|
|
||||||
|
# REMEMBER
|
||||||
|
|
||||||
|
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||||
|
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||||
|
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||||
|
> Using ``` for code is **PROHIBITED**.
|
||||||
|
> Using <dyad-write> for code is **MANDATORY**.
|
||||||
|
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||||
|
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||||
|
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||||
|
tell them that they need to add supabase to their app.
|
||||||
|
|
||||||
|
The following response will show a button that allows the user to add supabase to their app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: User wants to use Supabase
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to use supabase in my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
## Example 2: User wants to add auth to their app
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add auth to my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app and then we can add auth.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: This is my codebase. <dyad-file path="AI_RULES.md">
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="index.html">
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dyad-generated-app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/main.tsx">
|
||||||
|
finalMainTsxFileWithError();
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/vite-env.d.ts">
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: OK, got it. I'm ready to help
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: tc=create-ts-errors-complex
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: Tests delete-rename-write order
|
||||||
|
<dyad-delete path="src/main.tsx">
|
||||||
|
</dyad-delete>
|
||||||
|
<dyad-rename from="src/App.tsx" to="src/main.tsx">
|
||||||
|
</dyad-rename>
|
||||||
|
<dyad-write path="src/main.tsx" description="final main.tsx file.">
|
||||||
|
finalMainTsxFileWithError();
|
||||||
|
</dyad-write>
|
||||||
|
EOM
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 1 TypeScript compile-time error:
|
||||||
|
|
||||||
|
1. src/main.tsx:1:1 - Cannot find name 'finalMainTsxFileWithError'. (TS2304)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
===
|
||||||
|
role: system
|
||||||
|
message:
|
||||||
|
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||||
|
You make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||||
|
|
||||||
|
# App Preview / Commands
|
||||||
|
|
||||||
|
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||||
|
|
||||||
|
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||||
|
- **Restart**: This will restart the app server.
|
||||||
|
- **Refresh**: This will refresh the app preview page.
|
||||||
|
|
||||||
|
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||||
|
<dyad-command type="rebuild"></dyad-command>
|
||||||
|
<dyad-command type="restart"></dyad-command>
|
||||||
|
<dyad-command type="refresh"></dyad-command>
|
||||||
|
|
||||||
|
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||||
|
|
||||||
|
# Guidelines
|
||||||
|
|
||||||
|
Always reply to the user in the same language they are using.
|
||||||
|
|
||||||
|
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||||
|
- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||||
|
- Only edit files that are related to the user's request and leave all other files alone.
|
||||||
|
|
||||||
|
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||||
|
|
||||||
|
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||||
|
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||||
|
- Use <dyad-rename> for renaming files.
|
||||||
|
- Use <dyad-delete> for removing files.
|
||||||
|
- Use <dyad-add-dependency> for installing packages.
|
||||||
|
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||||
|
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||||
|
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||||
|
|
||||||
|
Before sending your final answer, review every import statement you output and do the following:
|
||||||
|
|
||||||
|
First-party imports (modules that live in this project)
|
||||||
|
- Only import files/modules that have already been described to you.
|
||||||
|
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||||
|
|
||||||
|
Third-party imports (anything that would come from npm)
|
||||||
|
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||||
|
|
||||||
|
Do not leave any import unresolved.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: Adding a new component
|
||||||
|
|
||||||
|
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||||
|
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||||
|
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||||
|
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from './components/Button';
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||||
|
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||||
|
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of the component) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||||
|
|
||||||
|
## Example 2: Installing a package and creating a Toast notification system
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add a toast
|
||||||
|
### System response
|
||||||
|
|
||||||
|
OK first, you need to install the package.
|
||||||
|
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||||
|
|
||||||
|
### System response
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
const ToastProvider = () => { <Toaster/> };
|
||||||
|
export default ToastProvider;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import ToastProvider from './components/ToastProvider';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ToastProvider />
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
export const showSuccess = (message) => {
|
||||||
|
toast.success(message);
|
||||||
|
};
|
||||||
|
export const showError = (message) => {
|
||||||
|
toast.error(message);
|
||||||
|
};
|
||||||
|
export const showLoading = (message) => {
|
||||||
|
return toast.loading(message);
|
||||||
|
};
|
||||||
|
export const dismissToast = (toastId) => {
|
||||||
|
toast.dismiss(toastId);
|
||||||
|
};
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||||
|
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||||
|
|
||||||
|
## Example 3: Renaming and deleting files
|
||||||
|
|
||||||
|
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
|
||||||
|
const ProfileCard = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{user.avatar ? (
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-12 h-12 rounded-full mr-4"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<User className="text-gray-500" size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||||
|
<p className="text-gray-500">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (user details section) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileCard;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ProfileCard from '../components/ProfileCard';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<ProfileCard user={currentUser} />
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of dashboard content) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||||
|
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||||
|
|
||||||
|
# Additional Guidelines
|
||||||
|
|
||||||
|
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.
|
||||||
|
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||||
|
|
||||||
|
Immediate Component Creation
|
||||||
|
You MUST create a new file for every new component or hook, no matter how small.
|
||||||
|
Never add new components to existing files, even if they seem related.
|
||||||
|
Aim for components that are 100 lines of code or less.
|
||||||
|
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||||
|
|
||||||
|
Important Rules for dyad-write operations:
|
||||||
|
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||||
|
- Always specify the correct file path when using dyad-write.
|
||||||
|
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||||
|
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||||
|
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||||
|
- Prioritize creating small, focused files and components.
|
||||||
|
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||||
|
|
||||||
|
Coding guidelines
|
||||||
|
- ALWAYS generate responsive designs.
|
||||||
|
- Use toasts components to inform the user about important events.
|
||||||
|
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||||
|
|
||||||
|
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||||
|
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||||
|
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
|
||||||
|
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||||
|
|
||||||
|
# REMEMBER
|
||||||
|
|
||||||
|
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||||
|
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||||
|
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||||
|
> Using ``` for code is **PROHIBITED**.
|
||||||
|
> Using <dyad-write> for code is **MANDATORY**.
|
||||||
|
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||||
|
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||||
|
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||||
|
tell them that they need to add supabase to their app.
|
||||||
|
|
||||||
|
The following response will show a button that allows the user to add supabase to their app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: User wants to use Supabase
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to use supabase in my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
## Example 2: User wants to add auth to their app
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add auth to my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app and then we can add auth.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: This is my codebase. <dyad-file path="AI_RULES.md">
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="index.html">
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dyad-generated-app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/bad-file.ts">
|
||||||
|
// Import doesn't exist
|
||||||
|
// import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.toString(); // replaced with existing method
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/main.tsx">
|
||||||
|
finalMainTsxFileWithError();
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/vite-env.d.ts">
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: OK, got it. I'm ready to help
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: tc=create-ts-errors-complex
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: Tests delete-rename-write order
|
||||||
|
<dyad-delete path="src/main.tsx">
|
||||||
|
</dyad-delete>
|
||||||
|
<dyad-rename from="src/App.tsx" to="src/main.tsx">
|
||||||
|
</dyad-rename>
|
||||||
|
<dyad-write path="src/main.tsx" description="final main.tsx file.">
|
||||||
|
finalMainTsxFileWithError();
|
||||||
|
</dyad-write>
|
||||||
|
EOM
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 1 TypeScript compile-time error:
|
||||||
|
|
||||||
|
1. src/main.tsx:1:1 - Cannot find name 'finalMainTsxFileWithError'. (TS2304)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message:
|
||||||
|
<dyad-write path="src/bad-file.ts" description="Fix remaining error.">
|
||||||
|
// Import doesn't exist
|
||||||
|
// import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.toString(); // replaced with existing method
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[dyad-dump-path=*]]
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 1 TypeScript compile-time error:
|
||||||
|
|
||||||
|
1. src/main.tsx:1:1 - Cannot find name 'finalMainTsxFileWithError'. (TS2304)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
- paragraph: tc=create-ts-errors
|
||||||
|
- paragraph: This will get a TypeScript error.
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: This will get a TypeScript error."
|
||||||
|
- paragraph: EOM
|
||||||
|
- button "Retry":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
- paragraph: tc=create-ts-errors
|
||||||
|
- paragraph: This will get a TypeScript error.
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: This will get a TypeScript error."
|
||||||
|
- paragraph: EOM
|
||||||
|
- img
|
||||||
|
- text: Auto-fix2 problems
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: Fix 2 errors and introduce a new error."
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- img
|
||||||
|
- text: Auto-fix1 problems
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: Fix remaining error."
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- button "Retry":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
===
|
||||||
|
role: system
|
||||||
|
message:
|
||||||
|
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||||
|
You make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||||
|
|
||||||
|
# App Preview / Commands
|
||||||
|
|
||||||
|
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||||
|
|
||||||
|
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||||
|
- **Restart**: This will restart the app server.
|
||||||
|
- **Refresh**: This will refresh the app preview page.
|
||||||
|
|
||||||
|
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||||
|
<dyad-command type="rebuild"></dyad-command>
|
||||||
|
<dyad-command type="restart"></dyad-command>
|
||||||
|
<dyad-command type="refresh"></dyad-command>
|
||||||
|
|
||||||
|
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||||
|
|
||||||
|
# Guidelines
|
||||||
|
|
||||||
|
Always reply to the user in the same language they are using.
|
||||||
|
|
||||||
|
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||||
|
- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||||
|
- Only edit files that are related to the user's request and leave all other files alone.
|
||||||
|
|
||||||
|
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||||
|
|
||||||
|
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||||
|
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||||
|
- Use <dyad-rename> for renaming files.
|
||||||
|
- Use <dyad-delete> for removing files.
|
||||||
|
- Use <dyad-add-dependency> for installing packages.
|
||||||
|
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||||
|
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||||
|
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||||
|
|
||||||
|
Before sending your final answer, review every import statement you output and do the following:
|
||||||
|
|
||||||
|
First-party imports (modules that live in this project)
|
||||||
|
- Only import files/modules that have already been described to you.
|
||||||
|
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||||
|
|
||||||
|
Third-party imports (anything that would come from npm)
|
||||||
|
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||||
|
|
||||||
|
Do not leave any import unresolved.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: Adding a new component
|
||||||
|
|
||||||
|
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||||
|
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||||
|
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||||
|
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from './components/Button';
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||||
|
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||||
|
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of the component) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||||
|
|
||||||
|
## Example 2: Installing a package and creating a Toast notification system
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add a toast
|
||||||
|
### System response
|
||||||
|
|
||||||
|
OK first, you need to install the package.
|
||||||
|
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||||
|
|
||||||
|
### System response
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
const ToastProvider = () => { <Toaster/> };
|
||||||
|
export default ToastProvider;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import ToastProvider from './components/ToastProvider';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ToastProvider />
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
export const showSuccess = (message) => {
|
||||||
|
toast.success(message);
|
||||||
|
};
|
||||||
|
export const showError = (message) => {
|
||||||
|
toast.error(message);
|
||||||
|
};
|
||||||
|
export const showLoading = (message) => {
|
||||||
|
return toast.loading(message);
|
||||||
|
};
|
||||||
|
export const dismissToast = (toastId) => {
|
||||||
|
toast.dismiss(toastId);
|
||||||
|
};
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||||
|
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||||
|
|
||||||
|
## Example 3: Renaming and deleting files
|
||||||
|
|
||||||
|
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
|
||||||
|
const ProfileCard = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{user.avatar ? (
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-12 h-12 rounded-full mr-4"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<User className="text-gray-500" size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||||
|
<p className="text-gray-500">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (user details section) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileCard;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ProfileCard from '../components/ProfileCard';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<ProfileCard user={currentUser} />
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of dashboard content) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||||
|
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||||
|
|
||||||
|
# Additional Guidelines
|
||||||
|
|
||||||
|
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.
|
||||||
|
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||||
|
|
||||||
|
Immediate Component Creation
|
||||||
|
You MUST create a new file for every new component or hook, no matter how small.
|
||||||
|
Never add new components to existing files, even if they seem related.
|
||||||
|
Aim for components that are 100 lines of code or less.
|
||||||
|
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||||
|
|
||||||
|
Important Rules for dyad-write operations:
|
||||||
|
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||||
|
- Always specify the correct file path when using dyad-write.
|
||||||
|
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||||
|
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||||
|
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||||
|
- Prioritize creating small, focused files and components.
|
||||||
|
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||||
|
|
||||||
|
Coding guidelines
|
||||||
|
- ALWAYS generate responsive designs.
|
||||||
|
- Use toasts components to inform the user about important events.
|
||||||
|
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||||
|
|
||||||
|
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||||
|
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||||
|
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
|
||||||
|
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||||
|
|
||||||
|
# REMEMBER
|
||||||
|
|
||||||
|
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||||
|
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||||
|
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||||
|
> Using ``` for code is **PROHIBITED**.
|
||||||
|
> Using <dyad-write> for code is **MANDATORY**.
|
||||||
|
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||||
|
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||||
|
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||||
|
tell them that they need to add supabase to their app.
|
||||||
|
|
||||||
|
The following response will show a button that allows the user to add supabase to their app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: User wants to use Supabase
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to use supabase in my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
## Example 2: User wants to add auth to their app
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add auth to my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app and then we can add auth.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: This is my codebase. <dyad-file path="AI_RULES.md">
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="index.html">
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dyad-generated-app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/App.tsx">
|
||||||
|
const App = () => <div>Minimal imported app</div>;
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/bad-file.ts">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod();
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/main.tsx">
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/vite-env.d.ts">
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: OK, got it. I'm ready to help
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: tc=create-ts-errors
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: This will get a TypeScript error.
|
||||||
|
|
||||||
|
<dyad-write path="src/bad-file.ts" description="This will get a TypeScript error.">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod();
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
EOM
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 2 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/bad-file.ts:1:30 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
2. src/bad-file.ts:4:3 - Property 'nonExistentMethod' does not exist on type 'Object'. (TS2339)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
===
|
||||||
|
role: system
|
||||||
|
message:
|
||||||
|
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||||
|
You make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||||
|
|
||||||
|
# App Preview / Commands
|
||||||
|
|
||||||
|
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||||
|
|
||||||
|
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||||
|
- **Restart**: This will restart the app server.
|
||||||
|
- **Refresh**: This will refresh the app preview page.
|
||||||
|
|
||||||
|
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||||
|
<dyad-command type="rebuild"></dyad-command>
|
||||||
|
<dyad-command type="restart"></dyad-command>
|
||||||
|
<dyad-command type="refresh"></dyad-command>
|
||||||
|
|
||||||
|
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||||
|
|
||||||
|
# Guidelines
|
||||||
|
|
||||||
|
Always reply to the user in the same language they are using.
|
||||||
|
|
||||||
|
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||||
|
- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||||
|
- Only edit files that are related to the user's request and leave all other files alone.
|
||||||
|
|
||||||
|
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||||
|
|
||||||
|
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||||
|
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||||
|
- Use <dyad-rename> for renaming files.
|
||||||
|
- Use <dyad-delete> for removing files.
|
||||||
|
- Use <dyad-add-dependency> for installing packages.
|
||||||
|
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||||
|
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||||
|
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||||
|
|
||||||
|
Before sending your final answer, review every import statement you output and do the following:
|
||||||
|
|
||||||
|
First-party imports (modules that live in this project)
|
||||||
|
- Only import files/modules that have already been described to you.
|
||||||
|
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||||
|
|
||||||
|
Third-party imports (anything that would come from npm)
|
||||||
|
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||||
|
|
||||||
|
Do not leave any import unresolved.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: Adding a new component
|
||||||
|
|
||||||
|
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||||
|
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||||
|
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||||
|
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from './components/Button';
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||||
|
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||||
|
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of the component) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||||
|
|
||||||
|
## Example 2: Installing a package and creating a Toast notification system
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add a toast
|
||||||
|
### System response
|
||||||
|
|
||||||
|
OK first, you need to install the package.
|
||||||
|
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||||
|
|
||||||
|
### System response
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
const ToastProvider = () => { <Toaster/> };
|
||||||
|
export default ToastProvider;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import ToastProvider from './components/ToastProvider';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ToastProvider />
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
export const showSuccess = (message) => {
|
||||||
|
toast.success(message);
|
||||||
|
};
|
||||||
|
export const showError = (message) => {
|
||||||
|
toast.error(message);
|
||||||
|
};
|
||||||
|
export const showLoading = (message) => {
|
||||||
|
return toast.loading(message);
|
||||||
|
};
|
||||||
|
export const dismissToast = (toastId) => {
|
||||||
|
toast.dismiss(toastId);
|
||||||
|
};
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||||
|
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||||
|
|
||||||
|
## Example 3: Renaming and deleting files
|
||||||
|
|
||||||
|
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
|
||||||
|
const ProfileCard = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{user.avatar ? (
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-12 h-12 rounded-full mr-4"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<User className="text-gray-500" size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||||
|
<p className="text-gray-500">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (user details section) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileCard;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ProfileCard from '../components/ProfileCard';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<ProfileCard user={currentUser} />
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of dashboard content) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||||
|
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||||
|
|
||||||
|
# Additional Guidelines
|
||||||
|
|
||||||
|
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.
|
||||||
|
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||||
|
|
||||||
|
Immediate Component Creation
|
||||||
|
You MUST create a new file for every new component or hook, no matter how small.
|
||||||
|
Never add new components to existing files, even if they seem related.
|
||||||
|
Aim for components that are 100 lines of code or less.
|
||||||
|
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||||
|
|
||||||
|
Important Rules for dyad-write operations:
|
||||||
|
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||||
|
- Always specify the correct file path when using dyad-write.
|
||||||
|
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||||
|
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||||
|
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||||
|
- Prioritize creating small, focused files and components.
|
||||||
|
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||||
|
|
||||||
|
Coding guidelines
|
||||||
|
- ALWAYS generate responsive designs.
|
||||||
|
- Use toasts components to inform the user about important events.
|
||||||
|
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||||
|
|
||||||
|
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||||
|
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||||
|
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
|
||||||
|
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||||
|
|
||||||
|
# REMEMBER
|
||||||
|
|
||||||
|
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||||
|
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||||
|
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||||
|
> Using ``` for code is **PROHIBITED**.
|
||||||
|
> Using <dyad-write> for code is **MANDATORY**.
|
||||||
|
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||||
|
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||||
|
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||||
|
tell them that they need to add supabase to their app.
|
||||||
|
|
||||||
|
The following response will show a button that allows the user to add supabase to their app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: User wants to use Supabase
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to use supabase in my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
## Example 2: User wants to add auth to their app
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add auth to my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app and then we can add auth.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: This is my codebase. <dyad-file path="AI_RULES.md">
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="index.html">
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dyad-generated-app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/App.tsx">
|
||||||
|
const App = () => <div>Minimal imported app</div>;
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/bad-file.ts">
|
||||||
|
// Import doesn't exist
|
||||||
|
// import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod2();
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/main.tsx">
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/vite-env.d.ts">
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: OK, got it. I'm ready to help
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: tc=create-ts-errors
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: This will get a TypeScript error.
|
||||||
|
|
||||||
|
<dyad-write path="src/bad-file.ts" description="This will get a TypeScript error.">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod();
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
EOM
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 2 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/bad-file.ts:1:30 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
2. src/bad-file.ts:4:3 - Property 'nonExistentMethod' does not exist on type 'Object'. (TS2339)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message:
|
||||||
|
<dyad-write path="src/bad-file.ts" description="Fix 2 errors and introduce a new error.">
|
||||||
|
// Import doesn't exist
|
||||||
|
// import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod2();
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[dyad-dump-path=*]]
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 1 TypeScript compile-time error:
|
||||||
|
|
||||||
|
1. src/bad-file.ts:6:3 - Property 'nonExistentMethod2' does not exist on type 'Object'. (TS2339)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
- img
|
||||||
|
- text: Auto-fix5 problems
|
||||||
|
- img
|
||||||
|
- text: "1"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 1:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "2"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 2:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "3"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 3:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "4"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 4:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "5"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 5:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
===
|
||||||
|
role: system
|
||||||
|
message:
|
||||||
|
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||||
|
You make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||||
|
|
||||||
|
# App Preview / Commands
|
||||||
|
|
||||||
|
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||||
|
|
||||||
|
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||||
|
- **Restart**: This will restart the app server.
|
||||||
|
- **Refresh**: This will refresh the app preview page.
|
||||||
|
|
||||||
|
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||||
|
<dyad-command type="rebuild"></dyad-command>
|
||||||
|
<dyad-command type="restart"></dyad-command>
|
||||||
|
<dyad-command type="refresh"></dyad-command>
|
||||||
|
|
||||||
|
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||||
|
|
||||||
|
# Guidelines
|
||||||
|
|
||||||
|
Always reply to the user in the same language they are using.
|
||||||
|
|
||||||
|
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||||
|
- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||||
|
- Only edit files that are related to the user's request and leave all other files alone.
|
||||||
|
|
||||||
|
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||||
|
|
||||||
|
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||||
|
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||||
|
- Use <dyad-rename> for renaming files.
|
||||||
|
- Use <dyad-delete> for removing files.
|
||||||
|
- Use <dyad-add-dependency> for installing packages.
|
||||||
|
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||||
|
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||||
|
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||||
|
|
||||||
|
Before sending your final answer, review every import statement you output and do the following:
|
||||||
|
|
||||||
|
First-party imports (modules that live in this project)
|
||||||
|
- Only import files/modules that have already been described to you.
|
||||||
|
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||||
|
|
||||||
|
Third-party imports (anything that would come from npm)
|
||||||
|
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||||
|
|
||||||
|
Do not leave any import unresolved.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: Adding a new component
|
||||||
|
|
||||||
|
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||||
|
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||||
|
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||||
|
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from './components/Button';
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||||
|
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||||
|
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of the component) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||||
|
|
||||||
|
## Example 2: Installing a package and creating a Toast notification system
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add a toast
|
||||||
|
### System response
|
||||||
|
|
||||||
|
OK first, you need to install the package.
|
||||||
|
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||||
|
|
||||||
|
### System response
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
const ToastProvider = () => { <Toaster/> };
|
||||||
|
export default ToastProvider;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import ToastProvider from './components/ToastProvider';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ToastProvider />
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
export const showSuccess = (message) => {
|
||||||
|
toast.success(message);
|
||||||
|
};
|
||||||
|
export const showError = (message) => {
|
||||||
|
toast.error(message);
|
||||||
|
};
|
||||||
|
export const showLoading = (message) => {
|
||||||
|
return toast.loading(message);
|
||||||
|
};
|
||||||
|
export const dismissToast = (toastId) => {
|
||||||
|
toast.dismiss(toastId);
|
||||||
|
};
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||||
|
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||||
|
|
||||||
|
## Example 3: Renaming and deleting files
|
||||||
|
|
||||||
|
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
|
||||||
|
const ProfileCard = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{user.avatar ? (
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-12 h-12 rounded-full mr-4"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<User className="text-gray-500" size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||||
|
<p className="text-gray-500">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (user details section) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileCard;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ProfileCard from '../components/ProfileCard';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<ProfileCard user={currentUser} />
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of dashboard content) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||||
|
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||||
|
|
||||||
|
# Additional Guidelines
|
||||||
|
|
||||||
|
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.
|
||||||
|
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||||
|
|
||||||
|
Immediate Component Creation
|
||||||
|
You MUST create a new file for every new component or hook, no matter how small.
|
||||||
|
Never add new components to existing files, even if they seem related.
|
||||||
|
Aim for components that are 100 lines of code or less.
|
||||||
|
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||||
|
|
||||||
|
Important Rules for dyad-write operations:
|
||||||
|
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||||
|
- Always specify the correct file path when using dyad-write.
|
||||||
|
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||||
|
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||||
|
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||||
|
- Prioritize creating small, focused files and components.
|
||||||
|
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||||
|
|
||||||
|
Coding guidelines
|
||||||
|
- ALWAYS generate responsive designs.
|
||||||
|
- Use toasts components to inform the user about important events.
|
||||||
|
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||||
|
|
||||||
|
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||||
|
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||||
|
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
|
||||||
|
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||||
|
|
||||||
|
# REMEMBER
|
||||||
|
|
||||||
|
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||||
|
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||||
|
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||||
|
> Using ``` for code is **PROHIBITED**.
|
||||||
|
> Using <dyad-write> for code is **MANDATORY**.
|
||||||
|
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||||
|
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||||
|
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||||
|
tell them that they need to add supabase to their app.
|
||||||
|
|
||||||
|
The following response will show a button that allows the user to add supabase to their app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: User wants to use Supabase
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to use supabase in my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
## Example 2: User wants to add auth to their app
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add auth to my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app and then we can add auth.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: This is my codebase. <dyad-file path="AI_RULES.md">
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="index.html">
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dyad-generated-app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/App.tsx">
|
||||||
|
const App = () => <div>Minimal imported app</div>;
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/bad-file.ts">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
import NonExistentClass2 from 'non-existent-class';
|
||||||
|
import NonExistentClass3 from 'non-existent-class';
|
||||||
|
import NonExistentClass4 from 'non-existent-class';
|
||||||
|
import NonExistentClass5 from 'non-existent-class';
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/main.tsx">
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/vite-env.d.ts">
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: OK, got it. I'm ready to help
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: tc=create-unfixable-ts-errors
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: This should not get fixed
|
||||||
|
|
||||||
|
<dyad-write path="src/bad-file.ts" description="This will produce 5 TypeScript errors.">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
import NonExistentClass2 from 'non-existent-class';
|
||||||
|
import NonExistentClass3 from 'non-existent-class';
|
||||||
|
import NonExistentClass4 from 'non-existent-class';
|
||||||
|
import NonExistentClass5 from 'non-existent-class';
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
EOM
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 5 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/bad-file.ts:1:30 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
2. src/bad-file.ts:2:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
3. src/bad-file.ts:3:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
4. src/bad-file.ts:4:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
5. src/bad-file.ts:5:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
- paragraph: tc=create-unfixable-ts-errors
|
||||||
|
- paragraph: This should not get fixed
|
||||||
|
- img
|
||||||
|
- text: bad-file.ts
|
||||||
|
- img
|
||||||
|
- text: "src/bad-file.ts Summary: This will produce 5 TypeScript errors."
|
||||||
|
- paragraph: EOM
|
||||||
|
- img
|
||||||
|
- text: Auto-fix5 problems
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: file1.txt
|
||||||
|
- img
|
||||||
|
- text: file1.txt
|
||||||
|
- paragraph: More EOM
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- img
|
||||||
|
- text: Auto-fix5 problems
|
||||||
|
- img
|
||||||
|
- text: "1"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 1:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "2"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 2:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "3"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 3:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "4"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 4:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- text: "5"
|
||||||
|
- img
|
||||||
|
- text: /src\/bad-file\.ts 5:\d+ TS2307/
|
||||||
|
- paragraph: Cannot find module 'non-existent-class' or its corresponding type declarations.
|
||||||
|
- img
|
||||||
|
- text: file1.txt
|
||||||
|
- img
|
||||||
|
- text: file1.txt
|
||||||
|
- paragraph: More EOM
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- button "Retry":
|
||||||
|
- img
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
===
|
||||||
|
role: system
|
||||||
|
message:
|
||||||
|
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||||
|
You make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||||
|
|
||||||
|
# App Preview / Commands
|
||||||
|
|
||||||
|
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||||
|
|
||||||
|
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||||
|
- **Restart**: This will restart the app server.
|
||||||
|
- **Refresh**: This will refresh the app preview page.
|
||||||
|
|
||||||
|
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||||
|
<dyad-command type="rebuild"></dyad-command>
|
||||||
|
<dyad-command type="restart"></dyad-command>
|
||||||
|
<dyad-command type="refresh"></dyad-command>
|
||||||
|
|
||||||
|
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||||
|
|
||||||
|
# Guidelines
|
||||||
|
|
||||||
|
Always reply to the user in the same language they are using.
|
||||||
|
|
||||||
|
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||||
|
- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||||
|
- Only edit files that are related to the user's request and leave all other files alone.
|
||||||
|
|
||||||
|
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||||
|
|
||||||
|
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||||
|
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||||
|
- Use <dyad-rename> for renaming files.
|
||||||
|
- Use <dyad-delete> for removing files.
|
||||||
|
- Use <dyad-add-dependency> for installing packages.
|
||||||
|
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||||
|
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||||
|
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||||
|
|
||||||
|
Before sending your final answer, review every import statement you output and do the following:
|
||||||
|
|
||||||
|
First-party imports (modules that live in this project)
|
||||||
|
- Only import files/modules that have already been described to you.
|
||||||
|
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||||
|
|
||||||
|
Third-party imports (anything that would come from npm)
|
||||||
|
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||||
|
|
||||||
|
Do not leave any import unresolved.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: Adding a new component
|
||||||
|
|
||||||
|
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||||
|
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||||
|
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||||
|
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from './components/Button';
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||||
|
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||||
|
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of the component) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||||
|
|
||||||
|
## Example 2: Installing a package and creating a Toast notification system
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add a toast
|
||||||
|
### System response
|
||||||
|
|
||||||
|
OK first, you need to install the package.
|
||||||
|
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||||
|
|
||||||
|
### System response
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
const ToastProvider = () => { <Toaster/> };
|
||||||
|
export default ToastProvider;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import ToastProvider from './components/ToastProvider';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ToastProvider />
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
export const showSuccess = (message) => {
|
||||||
|
toast.success(message);
|
||||||
|
};
|
||||||
|
export const showError = (message) => {
|
||||||
|
toast.error(message);
|
||||||
|
};
|
||||||
|
export const showLoading = (message) => {
|
||||||
|
return toast.loading(message);
|
||||||
|
};
|
||||||
|
export const dismissToast = (toastId) => {
|
||||||
|
toast.dismiss(toastId);
|
||||||
|
};
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||||
|
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||||
|
|
||||||
|
## Example 3: Renaming and deleting files
|
||||||
|
|
||||||
|
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
|
||||||
|
const ProfileCard = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{user.avatar ? (
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-12 h-12 rounded-full mr-4"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<User className="text-gray-500" size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||||
|
<p className="text-gray-500">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ... keep existing code (user details section) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileCard;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ProfileCard from '../components/ProfileCard';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<ProfileCard user={currentUser} />
|
||||||
|
|
||||||
|
{/* ... keep existing code (rest of dashboard content) */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
</dyad-write>
|
||||||
|
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||||
|
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||||
|
|
||||||
|
# Additional Guidelines
|
||||||
|
|
||||||
|
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.
|
||||||
|
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||||
|
|
||||||
|
Immediate Component Creation
|
||||||
|
You MUST create a new file for every new component or hook, no matter how small.
|
||||||
|
Never add new components to existing files, even if they seem related.
|
||||||
|
Aim for components that are 100 lines of code or less.
|
||||||
|
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||||
|
|
||||||
|
Important Rules for dyad-write operations:
|
||||||
|
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||||
|
- Always specify the correct file path when using dyad-write.
|
||||||
|
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||||
|
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||||
|
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||||
|
- Prioritize creating small, focused files and components.
|
||||||
|
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||||
|
|
||||||
|
Coding guidelines
|
||||||
|
- ALWAYS generate responsive designs.
|
||||||
|
- Use toasts components to inform the user about important events.
|
||||||
|
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||||
|
|
||||||
|
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||||
|
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||||
|
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
|
||||||
|
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||||
|
|
||||||
|
# REMEMBER
|
||||||
|
|
||||||
|
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||||
|
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||||
|
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||||
|
> Using ``` for code is **PROHIBITED**.
|
||||||
|
> Using <dyad-write> for code is **MANDATORY**.
|
||||||
|
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||||
|
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||||
|
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||||
|
tell them that they need to add supabase to their app.
|
||||||
|
|
||||||
|
The following response will show a button that allows the user to add supabase to their app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Example 1: User wants to use Supabase
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to use supabase in my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
## Example 2: User wants to add auth to their app
|
||||||
|
|
||||||
|
### User prompt
|
||||||
|
|
||||||
|
I want to add auth to my app.
|
||||||
|
|
||||||
|
### Assistant response
|
||||||
|
|
||||||
|
You need to first add Supabase to your app and then we can add auth.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: This is my codebase. <dyad-file path="AI_RULES.md">
|
||||||
|
[[beginning of AI_RULES.md]]
|
||||||
|
There's already AI rules...
|
||||||
|
[[end of AI_RULES.md]]
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="file1.txt">
|
||||||
|
A file (2)
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="index.html">
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dyad-generated-app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/App.tsx">
|
||||||
|
const App = () => <div>Minimal imported app</div>;
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/bad-file.ts">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
import NonExistentClass2 from 'non-existent-class';
|
||||||
|
import NonExistentClass3 from 'non-existent-class';
|
||||||
|
import NonExistentClass4 from 'non-existent-class';
|
||||||
|
import NonExistentClass5 from 'non-existent-class';
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/main.tsx">
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="src/vite-env.d.ts">
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
<dyad-file path="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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
</dyad-file>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: OK, got it. I'm ready to help
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: tc=create-unfixable-ts-errors
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message: This should not get fixed
|
||||||
|
|
||||||
|
<dyad-write path="src/bad-file.ts" description="This will produce 5 TypeScript errors.">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
import NonExistentClass2 from 'non-existent-class';
|
||||||
|
import NonExistentClass3 from 'non-existent-class';
|
||||||
|
import NonExistentClass4 from 'non-existent-class';
|
||||||
|
import NonExistentClass5 from 'non-existent-class';
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
EOM
|
||||||
|
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 5 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/bad-file.ts:1:30 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
2. src/bad-file.ts:2:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
3. src/bad-file.ts:3:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
4. src/bad-file.ts:4:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
5. src/bad-file.ts:5:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
|
|
||||||
|
===
|
||||||
|
role: assistant
|
||||||
|
message:
|
||||||
|
<dyad-write path="file1.txt">
|
||||||
|
A file (2)
|
||||||
|
</dyad-write>
|
||||||
|
More
|
||||||
|
EOM
|
||||||
|
|
||||||
|
[[dyad-dump-path=*]]
|
||||||
|
|
||||||
|
===
|
||||||
|
role: user
|
||||||
|
message: Fix these 5 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/bad-file.ts:1:30 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
2. src/bad-file.ts:2:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
3. src/bad-file.ts:3:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
4. src/bad-file.ts:4:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
5. src/bad-file.ts:5:31 - Cannot find module 'non-existent-class' or its corresponding type declarations. (TS2307)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way.
|
||||||
@@ -11,5 +11,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -11,5 +11,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -11,5 +11,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,6 @@
|
|||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,7 @@ import { expect } from "@playwright/test";
|
|||||||
|
|
||||||
test("create next.js app", async ({ po }) => {
|
test("create next.js app", async ({ po }) => {
|
||||||
await po.setUp();
|
await po.setUp();
|
||||||
// Select Next.js template
|
await po.selectHubTemplate("Next.js Template");
|
||||||
await po.goToHubTab();
|
|
||||||
await po.selectTemplate("Next.js Template");
|
|
||||||
await po.goToAppsTab();
|
|
||||||
|
|
||||||
// Create an app
|
// Create an app
|
||||||
await po.sendPrompt("tc=edit-made-with-dyad");
|
await po.sendPrompt("tc=edit-made-with-dyad");
|
||||||
|
|||||||
63
src/__tests__/__snapshots__/problem_prompt.test.ts.snap
Normal file
63
src/__tests__/__snapshots__/problem_prompt.test.ts.snap
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`problem_prompt > createConciseProblemFixPrompt > should format a concise prompt for multiple errors 1`] = `
|
||||||
|
"Fix these 2 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/main.ts:5:12 - Cannot find module 'react-dom/client' or its corresponding type declarations. (TS2307)
|
||||||
|
2. src/components/Modal.tsx:35:20 - Property 'isOpen' does not exist on type 'IntrinsicAttributes & ModalProps'. (TS2339)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way."
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`problem_prompt > createConciseProblemFixPrompt > should format a concise prompt for single error 1`] = `
|
||||||
|
"Fix these 1 TypeScript compile-time error:
|
||||||
|
|
||||||
|
1. src/App.tsx:10:5 - Cannot find name 'consol'. Did you mean 'console'? (TS2552)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way."
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`problem_prompt > createConciseProblemFixPrompt > should return a short message when no problems exist 1`] = `"No TypeScript problems detected."`;
|
||||||
|
|
||||||
|
exports[`problem_prompt > createProblemFixPrompt > should format a single error correctly 1`] = `
|
||||||
|
"Fix these 1 TypeScript compile-time error:
|
||||||
|
|
||||||
|
1. src/components/Button.tsx:15:23 - Property 'onClick' does not exist on type 'ButtonProps'. (TS2339)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way."
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`problem_prompt > createProblemFixPrompt > should format multiple errors across multiple files 1`] = `
|
||||||
|
"Fix these 4 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/components/Button.tsx:15:23 - Property 'onClick' does not exist on type 'ButtonProps'. (TS2339)
|
||||||
|
2. src/components/Button.tsx:8:12 - Type 'string | undefined' is not assignable to type 'string'. (TS2322)
|
||||||
|
3. src/hooks/useApi.ts:42:5 - Argument of type 'unknown' is not assignable to parameter of type 'string'. (TS2345)
|
||||||
|
4. src/utils/helpers.ts:45:8 - Function lacks ending return statement and return type does not include 'undefined'. (TS2366)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way."
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`problem_prompt > createProblemFixPrompt > should handle realistic React TypeScript errors 1`] = `
|
||||||
|
"Fix these 4 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/components/UserProfile.tsx:12:35 - Type '{ children: string; }' is missing the following properties from type 'UserProfileProps': user, onEdit (TS2739)
|
||||||
|
2. src/components/UserProfile.tsx:25:15 - Object is possibly 'null'. (TS2531)
|
||||||
|
3. src/hooks/useLocalStorage.ts:18:12 - Type 'string | null' is not assignable to type 'T'. (TS2322)
|
||||||
|
4. src/types/api.ts:45:3 - Duplicate identifier 'UserRole'. (TS2300)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way."
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`problem_prompt > createProblemFixPrompt > should return a message when no problems exist 1`] = `"No TypeScript problems detected."`;
|
||||||
|
|
||||||
|
exports[`problem_prompt > realistic TypeScript error scenarios > should handle common React + TypeScript errors 1`] = `
|
||||||
|
"Fix these 4 TypeScript compile-time errors:
|
||||||
|
|
||||||
|
1. src/components/ProductCard.tsx:22:18 - Property 'price' is missing in type '{ name: string; description: string; }' but required in type 'Product'. (TS2741)
|
||||||
|
2. src/components/SearchInput.tsx:15:45 - Type '(value: string) => void' is not assignable to type 'ChangeEventHandler<HTMLInputElement>'. (TS2322)
|
||||||
|
3. src/api/userService.ts:8:1 - Function lacks ending return statement and return type does not include 'undefined'. (TS2366)
|
||||||
|
4. src/utils/dataProcessor.ts:34:25 - Object is possibly 'undefined'. (TS2532)
|
||||||
|
|
||||||
|
Please fix all errors in a concise way."
|
||||||
|
`;
|
||||||
214
src/__tests__/problem_prompt.test.ts
Normal file
214
src/__tests__/problem_prompt.test.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { createProblemFixPrompt } from "../shared/problem_prompt";
|
||||||
|
import type { ProblemReport } from "../ipc/ipc_types";
|
||||||
|
|
||||||
|
describe("problem_prompt", () => {
|
||||||
|
describe("createProblemFixPrompt", () => {
|
||||||
|
it("should return a message when no problems exist", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should format a single error correctly", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [
|
||||||
|
{
|
||||||
|
file: "src/components/Button.tsx",
|
||||||
|
line: 15,
|
||||||
|
column: 23,
|
||||||
|
message: "Property 'onClick' does not exist on type 'ButtonProps'.",
|
||||||
|
code: 2339,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should format multiple errors across multiple files", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [
|
||||||
|
{
|
||||||
|
file: "src/components/Button.tsx",
|
||||||
|
line: 15,
|
||||||
|
column: 23,
|
||||||
|
message: "Property 'onClick' does not exist on type 'ButtonProps'.",
|
||||||
|
code: 2339,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "src/components/Button.tsx",
|
||||||
|
line: 8,
|
||||||
|
column: 12,
|
||||||
|
message:
|
||||||
|
"Type 'string | undefined' is not assignable to type 'string'.",
|
||||||
|
code: 2322,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "src/hooks/useApi.ts",
|
||||||
|
line: 42,
|
||||||
|
column: 5,
|
||||||
|
message:
|
||||||
|
"Argument of type 'unknown' is not assignable to parameter of type 'string'.",
|
||||||
|
code: 2345,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "src/utils/helpers.ts",
|
||||||
|
line: 45,
|
||||||
|
column: 8,
|
||||||
|
message:
|
||||||
|
"Function lacks ending return statement and return type does not include 'undefined'.",
|
||||||
|
code: 2366,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle realistic React TypeScript errors", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [
|
||||||
|
{
|
||||||
|
file: "src/components/UserProfile.tsx",
|
||||||
|
line: 12,
|
||||||
|
column: 35,
|
||||||
|
message:
|
||||||
|
"Type '{ children: string; }' is missing the following properties from type 'UserProfileProps': user, onEdit",
|
||||||
|
code: 2739,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "src/components/UserProfile.tsx",
|
||||||
|
line: 25,
|
||||||
|
column: 15,
|
||||||
|
message: "Object is possibly 'null'.",
|
||||||
|
code: 2531,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "src/hooks/useLocalStorage.ts",
|
||||||
|
line: 18,
|
||||||
|
column: 12,
|
||||||
|
message: "Type 'string | null' is not assignable to type 'T'.",
|
||||||
|
code: 2322,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "src/types/api.ts",
|
||||||
|
line: 45,
|
||||||
|
column: 3,
|
||||||
|
message: "Duplicate identifier 'UserRole'.",
|
||||||
|
code: 2300,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createConciseProblemFixPrompt", () => {
|
||||||
|
it("should return a short message when no problems exist", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should format a concise prompt for single error", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [
|
||||||
|
{
|
||||||
|
file: "src/App.tsx",
|
||||||
|
line: 10,
|
||||||
|
column: 5,
|
||||||
|
message: "Cannot find name 'consol'. Did you mean 'console'?",
|
||||||
|
code: 2552,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should format a concise prompt for multiple errors", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [
|
||||||
|
{
|
||||||
|
file: "src/main.ts",
|
||||||
|
line: 5,
|
||||||
|
column: 12,
|
||||||
|
message:
|
||||||
|
"Cannot find module 'react-dom/client' or its corresponding type declarations.",
|
||||||
|
code: 2307,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "src/components/Modal.tsx",
|
||||||
|
line: 35,
|
||||||
|
column: 20,
|
||||||
|
message:
|
||||||
|
"Property 'isOpen' does not exist on type 'IntrinsicAttributes & ModalProps'.",
|
||||||
|
code: 2339,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("realistic TypeScript error scenarios", () => {
|
||||||
|
it("should handle common React + TypeScript errors", () => {
|
||||||
|
const problemReport: ProblemReport = {
|
||||||
|
problems: [
|
||||||
|
// Missing interface property
|
||||||
|
{
|
||||||
|
file: "src/components/ProductCard.tsx",
|
||||||
|
line: 22,
|
||||||
|
column: 18,
|
||||||
|
message:
|
||||||
|
"Property 'price' is missing in type '{ name: string; description: string; }' but required in type 'Product'.",
|
||||||
|
code: 2741,
|
||||||
|
},
|
||||||
|
// Incorrect event handler type
|
||||||
|
{
|
||||||
|
file: "src/components/SearchInput.tsx",
|
||||||
|
line: 15,
|
||||||
|
column: 45,
|
||||||
|
message:
|
||||||
|
"Type '(value: string) => void' is not assignable to type 'ChangeEventHandler<HTMLInputElement>'.",
|
||||||
|
code: 2322,
|
||||||
|
},
|
||||||
|
// Async/await without Promise return type
|
||||||
|
{
|
||||||
|
file: "src/api/userService.ts",
|
||||||
|
line: 8,
|
||||||
|
column: 1,
|
||||||
|
message:
|
||||||
|
"Function lacks ending return statement and return type does not include 'undefined'.",
|
||||||
|
code: 2366,
|
||||||
|
},
|
||||||
|
// Strict null check
|
||||||
|
{
|
||||||
|
file: "src/utils/dataProcessor.ts",
|
||||||
|
line: 34,
|
||||||
|
column: 25,
|
||||||
|
message: "Object is possibly 'undefined'.",
|
||||||
|
code: 2532,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProblemFixPrompt(problemReport);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ export const selectedAppIdAtom = atom<number | null>(null);
|
|||||||
export const appsListAtom = atom<App[]>([]);
|
export const appsListAtom = atom<App[]>([]);
|
||||||
export const appBasePathAtom = atom<string>("");
|
export const appBasePathAtom = atom<string>("");
|
||||||
export const versionsListAtom = atom<Version[]>([]);
|
export const versionsListAtom = atom<Version[]>([]);
|
||||||
export const previewModeAtom = atom<"preview" | "code">("preview");
|
export const previewModeAtom = atom<"preview" | "code" | "problems">("preview");
|
||||||
export const selectedVersionIdAtom = atom<string | null>(null);
|
export const selectedVersionIdAtom = atom<string | null>(null);
|
||||||
export const appOutputAtom = atom<AppOutput[]>([]);
|
export const appOutputAtom = atom<AppOutput[]>([]);
|
||||||
export const appUrlAtom = atom<
|
export const appUrlAtom = atom<
|
||||||
|
|||||||
21
src/components/AutoFixProblemsSwitch.tsx
Normal file
21
src/components/AutoFixProblemsSwitch.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
|
export function AutoFixProblemsSwitch() {
|
||||||
|
const { settings, updateSettings } = useSettings();
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="auto-fix-problems"
|
||||||
|
checked={settings?.enableAutoFixProblems}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
updateSettings({
|
||||||
|
enableAutoFixProblems: !settings?.enableAutoFixProblems,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="auto-fix-problems">Auto-fix problems</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ import { ChatInputControls } from "../ChatInputControls";
|
|||||||
import { ChatErrorBox } from "./ChatErrorBox";
|
import { ChatErrorBox } from "./ChatErrorBox";
|
||||||
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
||||||
import { SelectedComponentDisplay } from "./SelectedComponentDisplay";
|
import { SelectedComponentDisplay } from "./SelectedComponentDisplay";
|
||||||
|
import { useCheckProblems } from "@/hooks/useCheckProblems";
|
||||||
|
|
||||||
const showTokenBarAtom = atom(false);
|
const showTokenBarAtom = atom(false);
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
|||||||
const [selectedComponent, setSelectedComponent] = useAtom(
|
const [selectedComponent, setSelectedComponent] = useAtom(
|
||||||
selectedComponentPreviewAtom,
|
selectedComponentPreviewAtom,
|
||||||
);
|
);
|
||||||
|
const { checkProblems } = useCheckProblems(appId);
|
||||||
// Use the attachments hook
|
// Use the attachments hook
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
@@ -207,6 +208,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
|||||||
setIsApproving(false);
|
setIsApproving(false);
|
||||||
setIsPreviewOpen(true);
|
setIsPreviewOpen(true);
|
||||||
refreshVersions();
|
refreshVersions();
|
||||||
|
checkProblems();
|
||||||
|
|
||||||
// Keep same as handleReject
|
// Keep same as handleReject
|
||||||
refreshProposal();
|
refreshProposal();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useAtomValue } from "jotai";
|
|||||||
import { isStreamingAtom } from "@/atoms/chatAtoms";
|
import { isStreamingAtom } from "@/atoms/chatAtoms";
|
||||||
import { CustomTagState } from "./stateTypes";
|
import { CustomTagState } from "./stateTypes";
|
||||||
import { DyadOutput } from "./DyadOutput";
|
import { DyadOutput } from "./DyadOutput";
|
||||||
|
import { DyadProblemSummary } from "./DyadProblemSummary";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
|
||||||
interface DyadMarkdownParserProps {
|
interface DyadMarkdownParserProps {
|
||||||
@@ -117,6 +118,7 @@ function preprocessUnclosedTags(content: string): {
|
|||||||
"dyad-execute-sql",
|
"dyad-execute-sql",
|
||||||
"dyad-add-integration",
|
"dyad-add-integration",
|
||||||
"dyad-output",
|
"dyad-output",
|
||||||
|
"dyad-problem-report",
|
||||||
"dyad-chat-summary",
|
"dyad-chat-summary",
|
||||||
"dyad-edit",
|
"dyad-edit",
|
||||||
"dyad-codebase-context",
|
"dyad-codebase-context",
|
||||||
@@ -182,6 +184,7 @@ function parseCustomTags(content: string): ContentPiece[] {
|
|||||||
"dyad-execute-sql",
|
"dyad-execute-sql",
|
||||||
"dyad-add-integration",
|
"dyad-add-integration",
|
||||||
"dyad-output",
|
"dyad-output",
|
||||||
|
"dyad-problem-report",
|
||||||
"dyad-chat-summary",
|
"dyad-chat-summary",
|
||||||
"dyad-edit",
|
"dyad-edit",
|
||||||
"dyad-codebase-context",
|
"dyad-codebase-context",
|
||||||
@@ -404,6 +407,13 @@ function renderCustomTag(
|
|||||||
</DyadOutput>
|
</DyadOutput>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "dyad-problem-report":
|
||||||
|
return (
|
||||||
|
<DyadProblemSummary summary={attributes.summary}>
|
||||||
|
{content}
|
||||||
|
</DyadProblemSummary>
|
||||||
|
);
|
||||||
|
|
||||||
case "dyad-chat-summary":
|
case "dyad-chat-summary":
|
||||||
// Don't render anything for dyad-chat-summary
|
// Don't render anything for dyad-chat-summary
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
154
src/components/chat/DyadProblemSummary.tsx
Normal file
154
src/components/chat/DyadProblemSummary.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
ChevronsDownUp,
|
||||||
|
ChevronsUpDown,
|
||||||
|
AlertTriangle,
|
||||||
|
FileText,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { Problem } from "@/ipc/ipc_types";
|
||||||
|
|
||||||
|
interface DyadProblemSummaryProps {
|
||||||
|
summary?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProblemItemProps {
|
||||||
|
problem: Problem;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProblemItem: React.FC<ProblemItemProps> = ({ problem, index }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-3 py-2 px-3 border-b border-gray-200 dark:border-gray-700 last:border-b-0">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mt-0.5">
|
||||||
|
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<FileText size={14} className="text-gray-500 flex-shrink-0" />
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||||
|
{problem.file}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{problem.line}:{problem.column}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded text-gray-600 dark:text-gray-300">
|
||||||
|
TS{problem.code}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
|
{problem.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DyadProblemSummary: React.FC<DyadProblemSummaryProps> = ({
|
||||||
|
summary,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [isContentVisible, setIsContentVisible] = useState(false);
|
||||||
|
|
||||||
|
// Parse problems from children content if available
|
||||||
|
const problems: Problem[] = React.useMemo(() => {
|
||||||
|
if (!children || typeof children !== "string") return [];
|
||||||
|
|
||||||
|
// Parse structured format with <problem> tags
|
||||||
|
const problemTagRegex =
|
||||||
|
/<problem\s+file="([^"]+)"\s+line="(\d+)"\s+column="(\d+)"\s+code="(\d+)">([^<]+)<\/problem>/g;
|
||||||
|
const problems: Problem[] = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = problemTagRegex.exec(children)) !== null) {
|
||||||
|
try {
|
||||||
|
problems.push({
|
||||||
|
file: match[1],
|
||||||
|
line: parseInt(match[2], 10),
|
||||||
|
column: parseInt(match[3], 10),
|
||||||
|
message: match[5].trim(),
|
||||||
|
code: parseInt(match[4], 10),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
file: "unknown",
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
message: children,
|
||||||
|
code: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return problems;
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
const totalProblems = problems.length;
|
||||||
|
const displaySummary =
|
||||||
|
summary || `${totalProblems} problems found (TypeScript errors)`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-(--background-lightest) hover:bg-(--background-lighter) rounded-lg px-4 py-2 border border-border my-2 cursor-pointer"
|
||||||
|
onClick={() => setIsContentVisible(!isContentVisible)}
|
||||||
|
data-testid="problem-summary"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertTriangle
|
||||||
|
size={16}
|
||||||
|
className="text-amber-600 dark:text-amber-500"
|
||||||
|
/>
|
||||||
|
<span className="text-gray-700 dark:text-gray-300 font-medium text-sm">
|
||||||
|
<span className="font-bold mr-2 outline-2 outline-amber-200 dark:outline-amber-700 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 rounded-md px-1">
|
||||||
|
Auto-fix
|
||||||
|
</span>
|
||||||
|
{displaySummary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{isContentVisible ? (
|
||||||
|
<ChevronsDownUp
|
||||||
|
size={20}
|
||||||
|
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronsUpDown
|
||||||
|
size={20}
|
||||||
|
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content area - show individual problems */}
|
||||||
|
{isContentVisible && totalProblems > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
|
{problems.map((problem, index) => (
|
||||||
|
<ProblemItem
|
||||||
|
key={`${problem.file}-${problem.line}-${problem.column}-${index}`}
|
||||||
|
problem={problem}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fallback content area for raw children */}
|
||||||
|
{isContentVisible && totalProblems === 0 && children && (
|
||||||
|
<div className="mt-4 text-sm text-gray-800 dark:text-gray-200">
|
||||||
|
<pre className="whitespace-pre-wrap font-mono text-xs bg-gray-100 dark:bg-gray-800 p-3 rounded">
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import { IpcClient } from "@/ipc/ipc_client";
|
|||||||
|
|
||||||
import { CodeView } from "./CodeView";
|
import { CodeView } from "./CodeView";
|
||||||
import { PreviewIframe } from "./PreviewIframe";
|
import { PreviewIframe } from "./PreviewIframe";
|
||||||
|
import { Problems } from "./Problems";
|
||||||
import {
|
import {
|
||||||
Eye,
|
Eye,
|
||||||
Code,
|
Code,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
Cog,
|
Cog,
|
||||||
Power,
|
Power,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
AlertTriangle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useEffect, useRef, useState, useCallback } from "react";
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
@@ -33,8 +35,9 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { showError, showSuccess } from "@/lib/toast";
|
import { showError, showSuccess } from "@/lib/toast";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useCheckProblems } from "@/hooks/useCheckProblems";
|
||||||
|
|
||||||
type PreviewMode = "preview" | "code";
|
type PreviewMode = "preview" | "code" | "problems";
|
||||||
|
|
||||||
interface PreviewHeaderProps {
|
interface PreviewHeaderProps {
|
||||||
previewMode: PreviewMode;
|
previewMode: PreviewMode;
|
||||||
@@ -57,81 +60,156 @@ const PreviewHeader = ({
|
|||||||
onRestart,
|
onRestart,
|
||||||
onCleanRestart,
|
onCleanRestart,
|
||||||
onClearSessionData,
|
onClearSessionData,
|
||||||
}: PreviewHeaderProps) => (
|
}: PreviewHeaderProps) => {
|
||||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||||
<div className="relative flex space-x-2 bg-[var(--background-darkest)] rounded-md p-0.5">
|
const previewRef = useRef<HTMLButtonElement>(null);
|
||||||
<button
|
const codeRef = useRef<HTMLButtonElement>(null);
|
||||||
className="relative flex items-center space-x-1 px-3 py-1 rounded-md text-sm z-10"
|
const problemsRef = useRef<HTMLButtonElement>(null);
|
||||||
onClick={() => setPreviewMode("preview")}
|
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
|
||||||
>
|
const { problemReport } = useCheckProblems(selectedAppId);
|
||||||
{previewMode === "preview" && (
|
// Get the problem count for the selected app
|
||||||
<motion.div
|
const problemCount = problemReport ? problemReport.problems.length : 0;
|
||||||
layoutId="activeIndicator"
|
|
||||||
className="absolute inset-0 bg-(--background-lightest) shadow rounded-md -z-1"
|
// Format the problem count for display
|
||||||
transition={{ type: "spring", stiffness: 500, damping: 35 }}
|
const formatProblemCount = (count: number): string => {
|
||||||
/>
|
if (count === 0) return "";
|
||||||
)}
|
if (count > 100) return "100+";
|
||||||
<Eye size={16} />
|
return count.toString();
|
||||||
<span>Preview</span>
|
};
|
||||||
</button>
|
|
||||||
<button
|
const displayCount = formatProblemCount(problemCount);
|
||||||
className="relative flex items-center space-x-1 px-3 py-1 rounded-md text-sm z-10"
|
|
||||||
onClick={() => setPreviewMode("code")}
|
// Update indicator position when mode changes
|
||||||
>
|
useEffect(() => {
|
||||||
{previewMode === "code" && (
|
const updateIndicator = () => {
|
||||||
<motion.div
|
let targetRef: React.RefObject<HTMLButtonElement | null>;
|
||||||
layoutId="activeIndicator"
|
|
||||||
className="absolute inset-0 bg-(--background-lightest) shadow rounded-md -z-1"
|
switch (previewMode) {
|
||||||
transition={{ type: "spring", stiffness: 500, damping: 35 }}
|
case "preview":
|
||||||
/>
|
targetRef = previewRef;
|
||||||
)}
|
break;
|
||||||
<Code size={16} />
|
case "code":
|
||||||
<span>Code</span>
|
targetRef = codeRef;
|
||||||
</button>
|
break;
|
||||||
|
case "problems":
|
||||||
|
targetRef = problemsRef;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRef.current) {
|
||||||
|
const button = targetRef.current;
|
||||||
|
const container = button.parentElement;
|
||||||
|
if (container) {
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const buttonRect = button.getBoundingClientRect();
|
||||||
|
const left = buttonRect.left - containerRect.left;
|
||||||
|
const width = buttonRect.width;
|
||||||
|
|
||||||
|
setIndicatorStyle({ left, width });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Small delay to ensure DOM is updated
|
||||||
|
const timeoutId = setTimeout(updateIndicator, 10);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [previewMode, displayCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
||||||
|
<div className="relative flex bg-[var(--background-darkest)] rounded-md p-0.5">
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0.5 bottom-0.5 bg-[var(--background-lightest)] shadow rounded-md"
|
||||||
|
animate={{
|
||||||
|
left: indicatorStyle.left,
|
||||||
|
width: indicatorStyle.width,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 600,
|
||||||
|
damping: 35,
|
||||||
|
mass: 0.6,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="preview-mode-button"
|
||||||
|
ref={previewRef}
|
||||||
|
className="relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
||||||
|
onClick={() => setPreviewMode("preview")}
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
<span>Preview</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-testid="problems-mode-button"
|
||||||
|
ref={problemsRef}
|
||||||
|
className="relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
||||||
|
onClick={() => setPreviewMode("problems")}
|
||||||
|
>
|
||||||
|
<AlertTriangle size={14} />
|
||||||
|
<span>Problems</span>
|
||||||
|
{displayCount && (
|
||||||
|
<span className="ml-0.5 px-1 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full min-w-[16px] text-center">
|
||||||
|
{displayCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-testid="code-mode-button"
|
||||||
|
ref={codeRef}
|
||||||
|
className="relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
||||||
|
onClick={() => setPreviewMode("code")}
|
||||||
|
>
|
||||||
|
<Code size={14} />
|
||||||
|
<span>Code</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={onRestart}
|
||||||
|
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
||||||
|
title="Restart App"
|
||||||
|
>
|
||||||
|
<Power size={16} />
|
||||||
|
<span>Restart</span>
|
||||||
|
</button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
data-testid="preview-more-options-button"
|
||||||
|
className="flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
||||||
|
title="More options"
|
||||||
|
>
|
||||||
|
<MoreVertical size={16} />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-60">
|
||||||
|
<DropdownMenuItem onClick={onCleanRestart}>
|
||||||
|
<Cog size={16} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>Rebuild</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Re-installs node_modules and restarts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={onClearSessionData}>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>Clear Preview Data</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Clears cookies and local storage for the app preview
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
);
|
||||||
<button
|
};
|
||||||
onClick={onRestart}
|
|
||||||
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
|
||||||
title="Restart App"
|
|
||||||
>
|
|
||||||
<Power size={16} />
|
|
||||||
<span>Restart</span>
|
|
||||||
</button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
data-testid="preview-more-options-button"
|
|
||||||
className="flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
|
||||||
title="More options"
|
|
||||||
>
|
|
||||||
<MoreVertical size={16} />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-60">
|
|
||||||
<DropdownMenuItem onClick={onCleanRestart}>
|
|
||||||
<Cog size={16} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>Rebuild</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Re-installs node_modules and restarts
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={onClearSessionData}>
|
|
||||||
<Trash2 size={16} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>Clear Preview Data</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Clears cookies and local storage for the app preview
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Console header component
|
// Console header component
|
||||||
const ConsoleHeader = ({
|
const ConsoleHeader = ({
|
||||||
@@ -262,8 +340,10 @@ export function PreviewPanel() {
|
|||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
{previewMode === "preview" ? (
|
{previewMode === "preview" ? (
|
||||||
<PreviewIframe key={key} loading={loading} />
|
<PreviewIframe key={key} loading={loading} />
|
||||||
) : (
|
) : previewMode === "code" ? (
|
||||||
<CodeView loading={loading} app={app} />
|
<CodeView loading={loading} app={app} />
|
||||||
|
) : (
|
||||||
|
<Problems />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
208
src/components/preview_panel/Problems.tsx
Normal file
208
src/components/preview_panel/Problems.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
|
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||||
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
XCircle,
|
||||||
|
FileText,
|
||||||
|
Wrench,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Problem, ProblemReport } from "@/ipc/ipc_types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
|
import { useCheckProblems } from "@/hooks/useCheckProblems";
|
||||||
|
import { createProblemFixPrompt } from "@/shared/problem_prompt";
|
||||||
|
import { showError } from "@/lib/toast";
|
||||||
|
|
||||||
|
interface ProblemItemProps {
|
||||||
|
problem: Problem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProblemItem = ({ problem }: ProblemItemProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-3 p-3 border-b border-border hover:bg-[var(--background-darkest)] transition-colors">
|
||||||
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
|
<XCircle size={16} className="text-red-500" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<FileText size={14} className="text-muted-foreground flex-shrink-0" />
|
||||||
|
<span className="text-sm font-medium truncate">{problem.file}</span>
|
||||||
|
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{problem.line}:{problem.column}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-foreground leading-relaxed">
|
||||||
|
{problem.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RecheckButtonProps {
|
||||||
|
appId: number;
|
||||||
|
size?: "sm" | "default" | "lg";
|
||||||
|
variant?:
|
||||||
|
| "default"
|
||||||
|
| "destructive"
|
||||||
|
| "outline"
|
||||||
|
| "secondary"
|
||||||
|
| "ghost"
|
||||||
|
| "link";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecheckButton = ({
|
||||||
|
appId,
|
||||||
|
size = "sm",
|
||||||
|
variant = "outline",
|
||||||
|
className = "h-7 px-3 text-xs",
|
||||||
|
}: RecheckButtonProps) => {
|
||||||
|
const { checkProblems, isChecking } = useCheckProblems(appId);
|
||||||
|
|
||||||
|
const handleRecheck = async () => {
|
||||||
|
const res = await checkProblems();
|
||||||
|
if (res.error) {
|
||||||
|
showError(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size={size}
|
||||||
|
variant={variant}
|
||||||
|
onClick={handleRecheck}
|
||||||
|
disabled={isChecking}
|
||||||
|
className={className}
|
||||||
|
data-testid="recheck-button"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
size={14}
|
||||||
|
className={`mr-1 ${isChecking ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
{isChecking ? "Checking..." : "Recheck"}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProblemsSummaryProps {
|
||||||
|
problemReport: ProblemReport;
|
||||||
|
appId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProblemsSummary = ({ problemReport, appId }: ProblemsSummaryProps) => {
|
||||||
|
const { streamMessage } = useStreamChat();
|
||||||
|
const { problems } = problemReport;
|
||||||
|
const totalErrors = problems.length;
|
||||||
|
const [selectedChatId] = useAtom(selectedChatIdAtom);
|
||||||
|
const handleFixAll = () => {
|
||||||
|
if (!selectedChatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamMessage({
|
||||||
|
prompt: createProblemFixPrompt(problemReport),
|
||||||
|
chatId: selectedChatId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (problems.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-32 text-center">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center mb-3">
|
||||||
|
<div className="w-6 h-6 rounded-full bg-green-500"></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">No problems found</p>
|
||||||
|
<RecheckButton appId={appId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 bg-[var(--background-darkest)] border-b border-border">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{totalErrors > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<XCircle size={16} className="text-red-500" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{totalErrors} {totalErrors === 1 ? "error" : "errors"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RecheckButton appId={appId} />
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
onClick={handleFixAll}
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
data-testid="fix-all-button"
|
||||||
|
>
|
||||||
|
<Wrench size={14} className="mr-1" />
|
||||||
|
Fix All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Problems() {
|
||||||
|
return (
|
||||||
|
<div data-testid="problems-pane">
|
||||||
|
<_Problems />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _Problems() {
|
||||||
|
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||||
|
const { problemReport } = useCheckProblems(selectedAppId);
|
||||||
|
|
||||||
|
if (!selectedAppId) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center p-8">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-[var(--background-darkest)] flex items-center justify-center mb-4">
|
||||||
|
<AlertTriangle size={24} className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">No App Selected</h3>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-md">
|
||||||
|
Select an app to view TypeScript problems and diagnostic information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!problemReport) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center p-8">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-[var(--background-darkest)] flex items-center justify-center mb-4">
|
||||||
|
<FileText size={24} className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">No Problems Data</h3>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-md mb-4">
|
||||||
|
No TypeScript diagnostics available for this app yet. Problems will
|
||||||
|
appear here after running type checking.
|
||||||
|
</p>
|
||||||
|
<RecheckButton appId={selectedAppId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<ProblemsSummary problemReport={problemReport} appId={selectedAppId} />
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{problemReport.problems.map((problem, index) => (
|
||||||
|
<ProblemItem
|
||||||
|
key={`${problem.file}-${problem.line}-${problem.column}-${index}`}
|
||||||
|
problem={problem}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/hooks/useCheckProblems.ts
Normal file
30
src/hooks/useCheckProblems.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
import type { ProblemReport } from "@/ipc/ipc_types";
|
||||||
|
|
||||||
|
export function useCheckProblems(appId: number | null) {
|
||||||
|
const {
|
||||||
|
data: problemReport,
|
||||||
|
isLoading: isChecking,
|
||||||
|
error,
|
||||||
|
refetch: checkProblems,
|
||||||
|
} = useQuery<ProblemReport, Error>({
|
||||||
|
queryKey: ["problems", appId],
|
||||||
|
queryFn: async (): Promise<ProblemReport> => {
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("App ID is required");
|
||||||
|
}
|
||||||
|
const ipcClient = IpcClient.getInstance();
|
||||||
|
return ipcClient.checkProblems({ appId });
|
||||||
|
},
|
||||||
|
enabled: !!appId,
|
||||||
|
// DO NOT SHOW ERROR TOAST.
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
problemReport,
|
||||||
|
isChecking,
|
||||||
|
error,
|
||||||
|
checkProblems,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { useRunApp } from "./useRunApp";
|
|||||||
import { useCountTokens } from "./useCountTokens";
|
import { useCountTokens } from "./useCountTokens";
|
||||||
import { useUserBudgetInfo } from "./useUserBudgetInfo";
|
import { useUserBudgetInfo } from "./useUserBudgetInfo";
|
||||||
import { usePostHog } from "posthog-js/react";
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
import { useCheckProblems } from "./useCheckProblems";
|
||||||
|
|
||||||
export function getRandomNumberId() {
|
export function getRandomNumberId() {
|
||||||
return Math.floor(Math.random() * 1_000_000_000_000_000);
|
return Math.floor(Math.random() * 1_000_000_000_000_000);
|
||||||
@@ -41,6 +42,7 @@ export function useStreamChat({
|
|||||||
const { refreshAppIframe } = useRunApp();
|
const { refreshAppIframe } = useRunApp();
|
||||||
const { countTokens } = useCountTokens();
|
const { countTokens } = useCountTokens();
|
||||||
const { refetchUserBudget } = useUserBudgetInfo();
|
const { refetchUserBudget } = useUserBudgetInfo();
|
||||||
|
const { checkProblems } = useCheckProblems(selectedAppId);
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
let chatId: number | undefined;
|
let chatId: number | undefined;
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ export function useStreamChat({
|
|||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
|
|
||||||
let hasIncrementedStreamCount = false;
|
let hasIncrementedStreamCount = false;
|
||||||
try {
|
try {
|
||||||
IpcClient.getInstance().streamMessage(prompt, {
|
IpcClient.getInstance().streamMessage(prompt, {
|
||||||
@@ -92,6 +95,7 @@ export function useStreamChat({
|
|||||||
if (response.updatedFiles) {
|
if (response.updatedFiles) {
|
||||||
setIsPreviewOpen(true);
|
setIsPreviewOpen(true);
|
||||||
refreshAppIframe();
|
refreshAppIframe();
|
||||||
|
checkProblems();
|
||||||
}
|
}
|
||||||
if (response.extraFiles) {
|
if (response.extraFiles) {
|
||||||
showExtraFilesToast({
|
showExtraFilesToast({
|
||||||
@@ -129,7 +133,14 @@ export function useStreamChat({
|
|||||||
setError(error instanceof Error ? error.message : String(error));
|
setError(error instanceof Error ? error.message : String(error));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setMessages, setIsStreaming, setIsPreviewOpen, refetchUserBudget],
|
[
|
||||||
|
setMessages,
|
||||||
|
setIsStreaming,
|
||||||
|
setIsPreviewOpen,
|
||||||
|
checkProblems,
|
||||||
|
selectedAppId,
|
||||||
|
refetchUserBudget,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { CoreMessage, TextPart, ImagePart, streamText } from "ai";
|
import {
|
||||||
|
CoreMessage,
|
||||||
|
TextPart,
|
||||||
|
ImagePart,
|
||||||
|
streamText,
|
||||||
|
ToolSet,
|
||||||
|
TextStreamPart,
|
||||||
|
} from "ai";
|
||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
import { chats, messages } from "../../db/schema";
|
import { chats, messages } from "../../db/schema";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
@@ -14,11 +21,11 @@ import {
|
|||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import { readSettings } from "../../main/settings";
|
import { readSettings } from "../../main/settings";
|
||||||
import type { ChatResponseEnd, ChatStreamParams } from "../ipc_types";
|
import type { ChatResponseEnd, ChatStreamParams } from "../ipc_types";
|
||||||
import { extractCodebase } from "../../utils/codebase";
|
import { extractCodebase, readFileWithCache } from "../../utils/codebase";
|
||||||
import { processFullResponseActions } from "../processors/response_processor";
|
import { processFullResponseActions } from "../processors/response_processor";
|
||||||
import { streamTestResponse } from "./testing_chat_handlers";
|
import { streamTestResponse } from "./testing_chat_handlers";
|
||||||
import { getTestResponse } from "./testing_chat_handlers";
|
import { getTestResponse } from "./testing_chat_handlers";
|
||||||
import { getModelClient } from "../utils/get_model_client";
|
import { getModelClient, ModelClient } from "../utils/get_model_client";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import {
|
import {
|
||||||
getSupabaseContext,
|
getSupabaseContext,
|
||||||
@@ -39,6 +46,12 @@ import { getExtraProviderOptions } from "../utils/thinking_utils";
|
|||||||
|
|
||||||
import { safeSend } from "../utils/safe_sender";
|
import { safeSend } from "../utils/safe_sender";
|
||||||
import { cleanFullResponse } from "../utils/cleanFullResponse";
|
import { cleanFullResponse } from "../utils/cleanFullResponse";
|
||||||
|
import { generateProblemReport } from "../processors/tsc";
|
||||||
|
import { createProblemFixPrompt } from "@/shared/problem_prompt";
|
||||||
|
import { AsyncVirtualFileSystem } from "@/utils/VirtualFilesystem";
|
||||||
|
import { fileExists } from "../utils/file_utils";
|
||||||
|
|
||||||
|
type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;
|
||||||
|
|
||||||
const logger = log.scope("chat_stream_handlers");
|
const logger = log.scope("chat_stream_handlers");
|
||||||
|
|
||||||
@@ -68,11 +81,76 @@ async function isTextFile(filePath: string): Promise<boolean> {
|
|||||||
return TEXT_FILE_EXTENSIONS.includes(ext);
|
return TEXT_FILE_EXTENSIONS.includes(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeXml(unsafe: string): string {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the temp directory exists
|
// Ensure the temp directory exists
|
||||||
if (!fs.existsSync(TEMP_DIR)) {
|
if (!fs.existsSync(TEMP_DIR)) {
|
||||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to process stream chunks
|
||||||
|
async function processStreamChunks({
|
||||||
|
fullStream,
|
||||||
|
fullResponse,
|
||||||
|
abortController,
|
||||||
|
chatId,
|
||||||
|
processResponseChunkUpdate,
|
||||||
|
}: {
|
||||||
|
fullStream: AsyncIterableStream<TextStreamPart<ToolSet>>;
|
||||||
|
fullResponse: string;
|
||||||
|
abortController: AbortController;
|
||||||
|
chatId: number;
|
||||||
|
processResponseChunkUpdate: (params: {
|
||||||
|
fullResponse: string;
|
||||||
|
}) => Promise<string>;
|
||||||
|
}): Promise<{ fullResponse: string; incrementalResponse: string }> {
|
||||||
|
let incrementalResponse = "";
|
||||||
|
let inThinkingBlock = false;
|
||||||
|
|
||||||
|
for await (const part of fullStream) {
|
||||||
|
let chunk = "";
|
||||||
|
if (part.type === "text-delta") {
|
||||||
|
if (inThinkingBlock) {
|
||||||
|
chunk = "</think>";
|
||||||
|
inThinkingBlock = false;
|
||||||
|
}
|
||||||
|
chunk += part.textDelta;
|
||||||
|
} else if (part.type === "reasoning") {
|
||||||
|
if (!inThinkingBlock) {
|
||||||
|
chunk = "<think>";
|
||||||
|
inThinkingBlock = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk += escapeDyadTags(part.textDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chunk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullResponse += chunk;
|
||||||
|
incrementalResponse += chunk;
|
||||||
|
fullResponse = cleanFullResponse(fullResponse);
|
||||||
|
fullResponse = await processResponseChunkUpdate({
|
||||||
|
fullResponse,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the stream was aborted, exit early
|
||||||
|
if (abortController.signal.aborted) {
|
||||||
|
logger.log(`Stream for chat ${chatId} was aborted`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fullResponse, incrementalResponse };
|
||||||
|
}
|
||||||
|
|
||||||
export function registerChatStreamHandlers() {
|
export function registerChatStreamHandlers() {
|
||||||
ipcMain.handle("chat:stream", async (event, req: ChatStreamParams) => {
|
ipcMain.handle("chat:stream", async (event, req: ChatStreamParams) => {
|
||||||
try {
|
try {
|
||||||
@@ -263,32 +341,24 @@ ${componentSnippet}
|
|||||||
// Normal AI processing for non-test prompts
|
// Normal AI processing for non-test prompts
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
|
|
||||||
// Extract codebase information if app is associated with the chat
|
const appPath = getDyadAppPath(updatedChat.app.path);
|
||||||
let codebaseInfo = "";
|
const chatContext = req.selectedComponent
|
||||||
let files: { path: string; content: string }[] = [];
|
? {
|
||||||
if (updatedChat.app) {
|
contextPaths: [
|
||||||
const appPath = getDyadAppPath(updatedChat.app.path);
|
{
|
||||||
try {
|
globPath: req.selectedComponent.relativePath,
|
||||||
const out = await extractCodebase({
|
},
|
||||||
appPath,
|
],
|
||||||
chatContext: req.selectedComponent
|
smartContextAutoIncludes: [],
|
||||||
? {
|
}
|
||||||
contextPaths: [
|
: validateChatContext(updatedChat.app.chatContext);
|
||||||
{
|
|
||||||
globPath: req.selectedComponent.relativePath,
|
const { formattedOutput: codebaseInfo, files } = await extractCodebase({
|
||||||
},
|
appPath,
|
||||||
],
|
chatContext,
|
||||||
smartContextAutoIncludes: [],
|
});
|
||||||
}
|
|
||||||
: validateChatContext(updatedChat.app.chatContext),
|
logger.log(`Extracted codebase information from ${appPath}`);
|
||||||
});
|
|
||||||
codebaseInfo = out.formattedOutput;
|
|
||||||
files = out.files;
|
|
||||||
logger.log(`Extracted codebase information from ${appPath}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error extracting codebase:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.log(
|
logger.log(
|
||||||
"codebaseInfo: length",
|
"codebaseInfo: length",
|
||||||
codebaseInfo.length,
|
codebaseInfo.length,
|
||||||
@@ -396,7 +466,7 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
: ([
|
: ([
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: "This is my codebase. " + codebaseInfo,
|
content: createCodebasePrompt(codebaseInfo),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -413,8 +483,8 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
// and eats up extra tokens.
|
// and eats up extra tokens.
|
||||||
content:
|
content:
|
||||||
settings.selectedChatMode === "ask"
|
settings.selectedChatMode === "ask"
|
||||||
? removeDyadTags(removeThinkingTags(msg.content))
|
? removeDyadTags(removeNonEssentialTags(msg.content))
|
||||||
: removeThinkingTags(msg.content),
|
: removeNonEssentialTags(msg.content),
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -453,8 +523,10 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
|
|
||||||
const simpleStreamText = async ({
|
const simpleStreamText = async ({
|
||||||
chatMessages,
|
chatMessages,
|
||||||
|
modelClient,
|
||||||
}: {
|
}: {
|
||||||
chatMessages: CoreMessage[];
|
chatMessages: CoreMessage[];
|
||||||
|
modelClient: ModelClient;
|
||||||
}) => {
|
}) => {
|
||||||
return streamText({
|
return streamText({
|
||||||
maxTokens: await getMaxTokens(settings.selectedModel),
|
maxTokens: await getMaxTokens(settings.selectedModel),
|
||||||
@@ -531,51 +603,21 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
};
|
};
|
||||||
|
|
||||||
// When calling streamText, the messages need to be properly formatted for mixed content
|
// When calling streamText, the messages need to be properly formatted for mixed content
|
||||||
const { fullStream } = await simpleStreamText({ chatMessages });
|
const { fullStream } = await simpleStreamText({
|
||||||
|
chatMessages,
|
||||||
|
modelClient,
|
||||||
|
});
|
||||||
|
|
||||||
// Process the stream as before
|
// Process the stream as before
|
||||||
let inThinkingBlock = false;
|
|
||||||
try {
|
try {
|
||||||
for await (const part of fullStream) {
|
const result = await processStreamChunks({
|
||||||
let chunk = "";
|
fullStream,
|
||||||
if (part.type === "text-delta") {
|
fullResponse,
|
||||||
if (inThinkingBlock) {
|
abortController,
|
||||||
chunk = "</think>";
|
chatId: req.chatId,
|
||||||
inThinkingBlock = false;
|
processResponseChunkUpdate,
|
||||||
}
|
});
|
||||||
chunk += part.textDelta;
|
fullResponse = result.fullResponse;
|
||||||
} else if (part.type === "reasoning") {
|
|
||||||
if (!inThinkingBlock) {
|
|
||||||
chunk = "<think>";
|
|
||||||
inThinkingBlock = true;
|
|
||||||
}
|
|
||||||
// Escape dyad tags in reasoning content
|
|
||||||
// We are replacing the opening tag with a look-alike character
|
|
||||||
// to avoid issues where thinking content includes dyad tags
|
|
||||||
// and are mishandled by:
|
|
||||||
// 1. FE markdown parser
|
|
||||||
// 2. Main process response processor
|
|
||||||
chunk += part.textDelta
|
|
||||||
.replace(/<dyad/g, "<dyad")
|
|
||||||
.replace(/<\/dyad/g, "</dyad");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chunk) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fullResponse += chunk;
|
|
||||||
fullResponse = cleanFullResponse(fullResponse);
|
|
||||||
fullResponse = await processResponseChunkUpdate({
|
|
||||||
fullResponse,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the stream was aborted, exit early
|
|
||||||
if (abortController.signal.aborted) {
|
|
||||||
logger.log(`Stream for chat ${req.chatId} was aborted`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!abortController.signal.aborted &&
|
!abortController.signal.aborted &&
|
||||||
@@ -599,6 +641,7 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
...chatMessages,
|
...chatMessages,
|
||||||
{ role: "assistant", content: fullResponse },
|
{ role: "assistant", content: fullResponse },
|
||||||
],
|
],
|
||||||
|
modelClient,
|
||||||
});
|
});
|
||||||
for await (const part of contStream) {
|
for await (const part of contStream) {
|
||||||
// If the stream was aborted, exit early
|
// If the stream was aborted, exit early
|
||||||
@@ -615,6 +658,117 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!abortController.signal.aborted &&
|
||||||
|
settings.enableAutoFixProblems &&
|
||||||
|
settings.selectedChatMode !== "ask"
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// IF auto-fix is enabled
|
||||||
|
let problemReport = await generateProblemReport({
|
||||||
|
fullResponse,
|
||||||
|
appPath: getDyadAppPath(updatedChat.app.path),
|
||||||
|
});
|
||||||
|
|
||||||
|
let autoFixAttempts = 0;
|
||||||
|
const originalFullResponse = fullResponse;
|
||||||
|
const previousAttempts: CoreMessage[] = [];
|
||||||
|
while (
|
||||||
|
problemReport.problems.length > 0 &&
|
||||||
|
autoFixAttempts < 2 &&
|
||||||
|
!abortController.signal.aborted
|
||||||
|
) {
|
||||||
|
fullResponse += `<dyad-problem-report summary="${problemReport.problems.length} problems">
|
||||||
|
${problemReport.problems
|
||||||
|
.map(
|
||||||
|
(problem) =>
|
||||||
|
`<problem file="${escapeXml(problem.file)}" line="${problem.line}" column="${problem.column}" code="${problem.code}">${escapeXml(problem.message)}</problem>`,
|
||||||
|
)
|
||||||
|
.join("\n")}
|
||||||
|
</dyad-problem-report>`;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Attempting to auto-fix problems, attempt #${autoFixAttempts + 1}`,
|
||||||
|
);
|
||||||
|
autoFixAttempts++;
|
||||||
|
const problemFixPrompt = createProblemFixPrompt(problemReport);
|
||||||
|
|
||||||
|
const virtualFileSystem = new AsyncVirtualFileSystem(
|
||||||
|
getDyadAppPath(updatedChat.app.path),
|
||||||
|
{
|
||||||
|
fileExists: (fileName: string) => fileExists(fileName),
|
||||||
|
readFile: (fileName: string) => readFileWithCache(fileName),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
virtualFileSystem.applyResponseChanges(fullResponse);
|
||||||
|
|
||||||
|
const { formattedOutput: codebaseInfo, files } =
|
||||||
|
await extractCodebase({
|
||||||
|
appPath,
|
||||||
|
chatContext,
|
||||||
|
virtualFileSystem,
|
||||||
|
});
|
||||||
|
const { modelClient } = await getModelClient(
|
||||||
|
settings.selectedModel,
|
||||||
|
settings,
|
||||||
|
files,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fullStream } = await simpleStreamText({
|
||||||
|
modelClient,
|
||||||
|
chatMessages: [
|
||||||
|
...chatMessages.map((msg, index) => {
|
||||||
|
if (
|
||||||
|
index === 0 &&
|
||||||
|
msg.role === "user" &&
|
||||||
|
typeof msg.content === "string" &&
|
||||||
|
msg.content.startsWith(CODEBASE_PROMPT_PREFIX)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
role: "user",
|
||||||
|
content: createCodebasePrompt(codebaseInfo),
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: originalFullResponse,
|
||||||
|
},
|
||||||
|
...previousAttempts,
|
||||||
|
{ role: "user", content: problemFixPrompt },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
previousAttempts.push({
|
||||||
|
role: "user",
|
||||||
|
content: problemFixPrompt,
|
||||||
|
});
|
||||||
|
const result = await processStreamChunks({
|
||||||
|
fullStream,
|
||||||
|
fullResponse,
|
||||||
|
abortController,
|
||||||
|
chatId: req.chatId,
|
||||||
|
processResponseChunkUpdate,
|
||||||
|
});
|
||||||
|
fullResponse = result.fullResponse;
|
||||||
|
previousAttempts.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: result.incrementalResponse,
|
||||||
|
});
|
||||||
|
|
||||||
|
problemReport = await generateProblemReport({
|
||||||
|
fullResponse,
|
||||||
|
appPath: getDyadAppPath(updatedChat.app.path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Error generating problem report or auto-fixing:",
|
||||||
|
settings.enableAutoFixProblems,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
// Check if this was an abort error
|
// Check if this was an abort error
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
@@ -901,11 +1055,21 @@ async function prepareMessageWithAttachments(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeNonEssentialTags(text: string): string {
|
||||||
|
return removeProblemReportTags(removeThinkingTags(text));
|
||||||
|
}
|
||||||
|
|
||||||
function removeThinkingTags(text: string): string {
|
function removeThinkingTags(text: string): string {
|
||||||
const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
|
const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
|
||||||
return text.replace(thinkRegex, "").trim();
|
return text.replace(thinkRegex, "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeProblemReportTags(text: string): string {
|
||||||
|
const problemReportRegex =
|
||||||
|
/<dyad-problem-report[^>]*>[\s\S]*?<\/dyad-problem-report>/g;
|
||||||
|
return text.replace(problemReportRegex, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function removeDyadTags(text: string): string {
|
export function removeDyadTags(text: string): string {
|
||||||
const dyadRegex = /<dyad-[^>]*>[\s\S]*?<\/dyad-[^>]*>/g;
|
const dyadRegex = /<dyad-[^>]*>[\s\S]*?<\/dyad-[^>]*>/g;
|
||||||
return text.replace(dyadRegex, "").trim();
|
return text.replace(dyadRegex, "").trim();
|
||||||
@@ -932,3 +1096,18 @@ export function hasUnclosedDyadWrite(text: string): boolean {
|
|||||||
|
|
||||||
return !hasClosingTag;
|
return !hasClosingTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeDyadTags(text: string): string {
|
||||||
|
// Escape dyad tags in reasoning content
|
||||||
|
// We are replacing the opening tag with a look-alike character
|
||||||
|
// to avoid issues where thinking content includes dyad tags
|
||||||
|
// and are mishandled by:
|
||||||
|
// 1. FE markdown parser
|
||||||
|
// 2. Main process response processor
|
||||||
|
return text.replace(/<dyad/g, "<dyad").replace(/<\/dyad/g, "</dyad");
|
||||||
|
}
|
||||||
|
|
||||||
|
const CODEBASE_PROMPT_PREFIX = "This is my codebase.";
|
||||||
|
function createCodebasePrompt(codebaseInfo: string): string {
|
||||||
|
return `${CODEBASE_PROMPT_PREFIX} ${codebaseInfo}`;
|
||||||
|
}
|
||||||
|
|||||||
36
src/ipc/handlers/problems_handlers.ts
Normal file
36
src/ipc/handlers/problems_handlers.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { db } from "../../db";
|
||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { apps } from "../../db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { generateProblemReport } from "../processors/tsc";
|
||||||
|
import { getDyadAppPath } from "@/paths/paths";
|
||||||
|
import { logger } from "./app_upgrade_handlers";
|
||||||
|
|
||||||
|
export function registerProblemsHandlers() {
|
||||||
|
// Handler to check problems using autofix with empty response
|
||||||
|
ipcMain.handle("check-problems", async (event, params: { appId: number }) => {
|
||||||
|
try {
|
||||||
|
// Get the app to find its path
|
||||||
|
const app = await db.query.apps.findFirst({
|
||||||
|
where: eq(apps.id, params.appId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(`App not found: ${params.appId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPath = getDyadAppPath(app.path);
|
||||||
|
|
||||||
|
// Call autofix with empty full response to just run TypeScript checking
|
||||||
|
const problemReport = await generateProblemReport({
|
||||||
|
fullResponse: "",
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
return problemReport;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error checking problems:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,6 +4,16 @@ import { cleanFullResponse } from "../utils/cleanFullResponse";
|
|||||||
// e.g. [dyad-qa=add-dep]
|
// e.g. [dyad-qa=add-dep]
|
||||||
// Canned responses for test prompts
|
// Canned responses for test prompts
|
||||||
const TEST_RESPONSES: Record<string, string> = {
|
const TEST_RESPONSES: Record<string, string> = {
|
||||||
|
"ts-error": `This will get a TypeScript error.
|
||||||
|
|
||||||
|
<dyad-write path="src/bad-file.ts" description="This will get a TypeScript error.">
|
||||||
|
import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod();
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
EOM`,
|
||||||
"add-dep": `I'll add that dependency for you.
|
"add-dep": `I'll add that dependency for you.
|
||||||
|
|
||||||
<dyad-add-dependency packages="deno"></dyad-add-dependency>
|
<dyad-add-dependency packages="deno"></dyad-add-dependency>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
AppOutput,
|
AppOutput,
|
||||||
Chat,
|
Chat,
|
||||||
ChatResponseEnd,
|
ChatResponseEnd,
|
||||||
|
ChatProblemsEvent,
|
||||||
CreateAppParams,
|
CreateAppParams,
|
||||||
CreateAppResult,
|
CreateAppResult,
|
||||||
ListAppsResponse,
|
ListAppsResponse,
|
||||||
@@ -35,6 +36,7 @@ import type {
|
|||||||
App,
|
App,
|
||||||
ComponentSelection,
|
ComponentSelection,
|
||||||
AppUpgrade,
|
AppUpgrade,
|
||||||
|
ProblemReport,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
|
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
@@ -233,6 +235,7 @@ export class IpcClient {
|
|||||||
onUpdate: (messages: Message[]) => void;
|
onUpdate: (messages: Message[]) => void;
|
||||||
onEnd: (response: ChatResponseEnd) => void;
|
onEnd: (response: ChatResponseEnd) => void;
|
||||||
onError: (error: string) => void;
|
onError: (error: string) => void;
|
||||||
|
onProblems?: (problems: ChatProblemsEvent) => void;
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
const {
|
const {
|
||||||
@@ -934,4 +937,10 @@ export class IpcClient {
|
|||||||
public async openAndroid(params: { appId: number }): Promise<void> {
|
public async openAndroid(params: { appId: number }): Promise<void> {
|
||||||
return this.ipcRenderer.invoke("open-android", params);
|
return this.ipcRenderer.invoke("open-android", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async checkProblems(params: {
|
||||||
|
appId: number;
|
||||||
|
}): Promise<ProblemReport> {
|
||||||
|
return this.ipcRenderer.invoke("check-problems", params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { registerProHandlers } from "./handlers/pro_handlers";
|
|||||||
import { registerContextPathsHandlers } from "./handlers/context_paths_handlers";
|
import { registerContextPathsHandlers } from "./handlers/context_paths_handlers";
|
||||||
import { registerAppUpgradeHandlers } from "./handlers/app_upgrade_handlers";
|
import { registerAppUpgradeHandlers } from "./handlers/app_upgrade_handlers";
|
||||||
import { registerCapacitorHandlers } from "./handlers/capacitor_handlers";
|
import { registerCapacitorHandlers } from "./handlers/capacitor_handlers";
|
||||||
|
import { registerProblemsHandlers } from "./handlers/problems_handlers";
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
// Register all IPC handlers by category
|
// Register all IPC handlers by category
|
||||||
@@ -33,6 +34,7 @@ export function registerIpcHandlers() {
|
|||||||
registerDependencyHandlers();
|
registerDependencyHandlers();
|
||||||
registerGithubHandlers();
|
registerGithubHandlers();
|
||||||
registerNodeHandlers();
|
registerNodeHandlers();
|
||||||
|
registerProblemsHandlers();
|
||||||
registerProposalHandlers();
|
registerProposalHandlers();
|
||||||
registerDebugHandlers();
|
registerDebugHandlers();
|
||||||
registerSupabaseHandlers();
|
registerSupabaseHandlers();
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ export interface ChatResponseEnd {
|
|||||||
extraFilesError?: string;
|
extraFilesError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatProblemsEvent {
|
||||||
|
chatId: number;
|
||||||
|
appId: number;
|
||||||
|
problems: ProblemReport;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateAppParams {
|
export interface CreateAppParams {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
@@ -240,3 +246,15 @@ export interface AppUpgrade {
|
|||||||
manualUpgradeUrl: string;
|
manualUpgradeUrl: string;
|
||||||
isNeeded: boolean;
|
isNeeded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Problem {
|
||||||
|
file: string;
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
message: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemReport {
|
||||||
|
problems: Problem[];
|
||||||
|
}
|
||||||
|
|||||||
10
src/ipc/processors/normalizePath.ts
Normal file
10
src/ipc/processors/normalizePath.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Normalize the path to use forward slashes instead of backslashes.
|
||||||
|
* This is important to prevent weird Git issues, particularly on Windows.
|
||||||
|
* @param path Source path.
|
||||||
|
* @returns Normalized path.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function normalizePath(path: string): string {
|
||||||
|
return path.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
@@ -19,20 +19,11 @@ import { SqlQuery, UserSettings } from "../../lib/schemas";
|
|||||||
import { gitCommit } from "../utils/git_utils";
|
import { gitCommit } from "../utils/git_utils";
|
||||||
import { readSettings } from "@/main/settings";
|
import { readSettings } from "@/main/settings";
|
||||||
import { writeMigrationFile } from "../utils/file_utils";
|
import { writeMigrationFile } from "../utils/file_utils";
|
||||||
|
import { normalizePath } from "./normalizePath";
|
||||||
|
|
||||||
const readFile = fs.promises.readFile;
|
const readFile = fs.promises.readFile;
|
||||||
const logger = log.scope("response_processor");
|
const logger = log.scope("response_processor");
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize the path to use forward slashes instead of backslashes.
|
|
||||||
* This is important to prevent weird Git issues, particularly on Windows.
|
|
||||||
* @param path Source path.
|
|
||||||
* @returns Normalized path.
|
|
||||||
*/
|
|
||||||
function normalizePath(path: string): string {
|
|
||||||
return path.replace(/\\/g, "/");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDyadWriteTags(fullResponse: string): {
|
export function getDyadWriteTags(fullResponse: string): {
|
||||||
path: string;
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
|||||||
211
src/ipc/processors/tsc.ts
Normal file
211
src/ipc/processors/tsc.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
import { ProblemReport } from "../ipc_types";
|
||||||
|
import { Problem } from "../ipc_types";
|
||||||
|
|
||||||
|
import { normalizePath } from "./normalizePath";
|
||||||
|
import { SyncVirtualFileSystem } from "../../utils/VirtualFilesystem";
|
||||||
|
import log from "electron-log";
|
||||||
|
|
||||||
|
const logger = log.scope("tsc");
|
||||||
|
|
||||||
|
function loadLocalTypeScript(appPath: string): typeof import("typescript") {
|
||||||
|
try {
|
||||||
|
// Try to load TypeScript from the project's node_modules
|
||||||
|
const requirePath = require.resolve("typescript", { paths: [appPath] });
|
||||||
|
logger.info(`Loading TypeScript from ${requirePath} for app ${appPath}`);
|
||||||
|
const ts = require(requirePath);
|
||||||
|
return ts;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load TypeScript from ${appPath} because of ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateProblemReport({
|
||||||
|
fullResponse,
|
||||||
|
appPath,
|
||||||
|
}: {
|
||||||
|
fullResponse: string;
|
||||||
|
appPath: string;
|
||||||
|
}): Promise<ProblemReport> {
|
||||||
|
// Load the local TypeScript version from the app's node_modules
|
||||||
|
const ts = loadLocalTypeScript(appPath);
|
||||||
|
|
||||||
|
// Create virtual file system with TypeScript system delegate and apply changes from response
|
||||||
|
const vfs = new SyncVirtualFileSystem(appPath, {
|
||||||
|
fileExists: (fileName: string) => ts.sys.fileExists(fileName),
|
||||||
|
readFile: (fileName: string) => ts.sys.readFile(fileName),
|
||||||
|
});
|
||||||
|
vfs.applyResponseChanges(fullResponse);
|
||||||
|
|
||||||
|
// Find TypeScript config - throw error if not found
|
||||||
|
const tsconfigPath = findTypeScriptConfig(appPath);
|
||||||
|
|
||||||
|
// Create TypeScript program with virtual file system
|
||||||
|
const result = await runTypeScriptCheck(ts, appPath, tsconfigPath, vfs);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTypeScriptConfig(appPath: string): string {
|
||||||
|
const possibleConfigs = [
|
||||||
|
// For vite applications, we want to check tsconfig.app.json, since it's the
|
||||||
|
// most important one (client-side app).
|
||||||
|
// The tsconfig.json in vite apps is a project reference and doesn't
|
||||||
|
// actually check anything unless you do "--build" which requires a complex
|
||||||
|
// programmatic approach
|
||||||
|
"tsconfig.app.json",
|
||||||
|
// For Next.js applications, it typically has a single tsconfig.json file
|
||||||
|
"tsconfig.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const config of possibleConfigs) {
|
||||||
|
const configPath = path.join(appPath, config);
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
return configPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`No TypeScript configuration file found in ${appPath}. Expected one of: ${possibleConfigs.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTypeScriptCheck(
|
||||||
|
ts: typeof import("typescript"),
|
||||||
|
appPath: string,
|
||||||
|
tsconfigPath: string,
|
||||||
|
vfs: SyncVirtualFileSystem,
|
||||||
|
): Promise<ProblemReport> {
|
||||||
|
return runSingleProject(ts, appPath, tsconfigPath, vfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSingleProject(
|
||||||
|
ts: typeof import("typescript"),
|
||||||
|
appPath: string,
|
||||||
|
tsconfigPath: string,
|
||||||
|
vfs: SyncVirtualFileSystem,
|
||||||
|
): Promise<ProblemReport> {
|
||||||
|
// Use the idiomatic way to parse TypeScript config
|
||||||
|
const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
|
||||||
|
tsconfigPath,
|
||||||
|
undefined, // No additional options
|
||||||
|
{
|
||||||
|
// Custom system object that can handle our virtual files
|
||||||
|
...ts.sys,
|
||||||
|
fileExists: (fileName: string) => vfs.fileExists(fileName),
|
||||||
|
readFile: (fileName: string) => vfs.readFile(fileName),
|
||||||
|
onUnRecoverableConfigFileDiagnostic: (
|
||||||
|
diagnostic: import("typescript").Diagnostic,
|
||||||
|
) => {
|
||||||
|
throw new Error(
|
||||||
|
`TypeScript config error: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!parsedCommandLine) {
|
||||||
|
throw new Error(`Failed to parse TypeScript config: ${tsconfigPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootNames = parsedCommandLine.fileNames;
|
||||||
|
|
||||||
|
// Add any virtual files that aren't already included
|
||||||
|
const virtualTsFiles = vfs
|
||||||
|
.getVirtualFiles()
|
||||||
|
.map((file) => path.resolve(appPath, file.path))
|
||||||
|
.filter(isTypeScriptFile);
|
||||||
|
|
||||||
|
// Remove deleted files from rootNames
|
||||||
|
const deletedFiles = vfs
|
||||||
|
.getDeletedFiles()
|
||||||
|
.map((file) => path.resolve(appPath, file));
|
||||||
|
rootNames = rootNames.filter((fileName) => {
|
||||||
|
const resolvedPath = path.resolve(fileName);
|
||||||
|
return !deletedFiles.includes(resolvedPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const virtualFile of virtualTsFiles) {
|
||||||
|
if (!rootNames.includes(virtualFile)) {
|
||||||
|
rootNames.push(virtualFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create custom compiler host
|
||||||
|
const host = createVirtualCompilerHost(
|
||||||
|
ts,
|
||||||
|
appPath,
|
||||||
|
vfs,
|
||||||
|
parsedCommandLine.options,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create TypeScript program - this is the idiomatic way
|
||||||
|
const program = ts.createProgram(rootNames, parsedCommandLine.options, host);
|
||||||
|
|
||||||
|
// Get diagnostics
|
||||||
|
const diagnostics = [
|
||||||
|
...program.getSyntacticDiagnostics(),
|
||||||
|
...program.getSemanticDiagnostics(),
|
||||||
|
...program.getGlobalDiagnostics(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convert diagnostics to our format
|
||||||
|
const problems: Problem[] = [];
|
||||||
|
|
||||||
|
for (const diagnostic of diagnostics) {
|
||||||
|
if (!diagnostic.file) continue;
|
||||||
|
|
||||||
|
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
|
||||||
|
diagnostic.start!,
|
||||||
|
);
|
||||||
|
const message = ts.flattenDiagnosticMessageText(
|
||||||
|
diagnostic.messageText,
|
||||||
|
"\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (diagnostic.category !== ts.DiagnosticCategory.Error) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
problems.push({
|
||||||
|
file: normalizePath(path.relative(appPath, diagnostic.file.fileName)),
|
||||||
|
line: line + 1, // Convert to 1-based
|
||||||
|
column: character + 1, // Convert to 1-based
|
||||||
|
message,
|
||||||
|
code: diagnostic.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
problems,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVirtualCompilerHost(
|
||||||
|
ts: typeof import("typescript"),
|
||||||
|
appPath: string,
|
||||||
|
vfs: SyncVirtualFileSystem,
|
||||||
|
compilerOptions: import("typescript").CompilerOptions,
|
||||||
|
): import("typescript").CompilerHost {
|
||||||
|
const host = ts.createCompilerHost(compilerOptions);
|
||||||
|
|
||||||
|
// Override file reading to use virtual files
|
||||||
|
host.readFile = (fileName: string) => {
|
||||||
|
return vfs.readFile(fileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override file existence check
|
||||||
|
host.fileExists = (fileName: string) => {
|
||||||
|
return vfs.fileExists(fileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTypeScriptFile(fileName: string): boolean {
|
||||||
|
const ext = path.extname(fileName).toLowerCase();
|
||||||
|
return [".ts", ".tsx", ".js", ".jsx"].includes(ext);
|
||||||
|
}
|
||||||
@@ -92,3 +92,10 @@ export async function writeMigrationFile(
|
|||||||
|
|
||||||
await fsExtra.writeFile(migrationFilePath, queryContent);
|
await fsExtra.writeFile(migrationFilePath, queryContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fileExists(filePath: string) {
|
||||||
|
return fsPromises
|
||||||
|
.access(filePath)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export const UserSettingsSchema = z.object({
|
|||||||
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
||||||
selectedChatMode: ChatModeSchema.optional(),
|
selectedChatMode: ChatModeSchema.optional(),
|
||||||
|
|
||||||
|
enableAutoFixProblems: z.boolean().optional(),
|
||||||
enableNativeGit: z.boolean().optional(),
|
enableNativeGit: z.boolean().optional(),
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const DEFAULT_SETTINGS: UserSettings = {
|
|||||||
enableProLazyEditsMode: true,
|
enableProLazyEditsMode: true,
|
||||||
enableProSmartFilesContextMode: true,
|
enableProSmartFilesContextMode: true,
|
||||||
selectedChatMode: "build",
|
selectedChatMode: "build",
|
||||||
|
enableAutoFixProblems: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_FILE = "user-settings.json";
|
const SETTINGS_FILE = "user-settings.json";
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { GitHubIntegration } from "@/components/GitHubIntegration";
|
|||||||
import { SupabaseIntegration } from "@/components/SupabaseIntegration";
|
import { SupabaseIntegration } from "@/components/SupabaseIntegration";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { AutoFixProblemsSwitch } from "@/components/AutoFixProblemsSwitch";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
@@ -110,6 +111,13 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 mt-4">
|
||||||
|
<AutoFixProblemsSwitch />
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
This will automatically fix TypeScript errors.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 mt-4">
|
<div className="space-y-1 mt-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const validInvokeChannels = [
|
|||||||
"sync-capacitor",
|
"sync-capacitor",
|
||||||
"open-ios",
|
"open-ios",
|
||||||
"open-android",
|
"open-android",
|
||||||
|
"check-problems",
|
||||||
// Test-only channels
|
// Test-only channels
|
||||||
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
|
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
|
||||||
// We can't detect with IS_TEST_BUILD in the preload script because
|
// We can't detect with IS_TEST_BUILD in the preload script because
|
||||||
|
|||||||
24
src/shared/problem_prompt.ts
Normal file
24
src/shared/problem_prompt.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { ProblemReport } from "../ipc/ipc_types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a more concise version of the problem fix prompt for cases where
|
||||||
|
* brevity is preferred.
|
||||||
|
*/
|
||||||
|
export function createProblemFixPrompt(problemReport: ProblemReport): string {
|
||||||
|
const { problems } = problemReport;
|
||||||
|
|
||||||
|
if (problems.length === 0) {
|
||||||
|
return "No TypeScript problems detected.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalProblems = problems.length;
|
||||||
|
let prompt = `Fix these ${totalProblems} TypeScript compile-time error${totalProblems === 1 ? "" : "s"}:\n\n`;
|
||||||
|
|
||||||
|
problems.forEach((problem, index) => {
|
||||||
|
prompt += `${index + 1}. ${problem.file}:${problem.line}:${problem.column} - ${problem.message} (TS${problem.code})\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
prompt += "\nPlease fix all errors in a concise way.";
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
373
src/utils/VirtualFilesystem.ts
Normal file
373
src/utils/VirtualFilesystem.ts
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import {
|
||||||
|
getDyadWriteTags,
|
||||||
|
getDyadRenameTags,
|
||||||
|
getDyadDeleteTags,
|
||||||
|
} from "../ipc/processors/response_processor";
|
||||||
|
|
||||||
|
import log from "electron-log";
|
||||||
|
|
||||||
|
const logger = log.scope("VirtualFileSystem");
|
||||||
|
|
||||||
|
export interface VirtualFile {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VirtualRename {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncFileSystemDelegate {
|
||||||
|
fileExists?: (fileName: string) => boolean;
|
||||||
|
readFile?: (fileName: string) => string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncFileSystemDelegate {
|
||||||
|
fileExists?: (fileName: string) => Promise<boolean>;
|
||||||
|
readFile?: (fileName: string) => Promise<string | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class containing shared virtual filesystem functionality
|
||||||
|
*/
|
||||||
|
export abstract class BaseVirtualFileSystem {
|
||||||
|
protected virtualFiles = new Map<string, string>();
|
||||||
|
protected deletedFiles = new Set<string>();
|
||||||
|
protected baseDir: string;
|
||||||
|
|
||||||
|
constructor(baseDir: string) {
|
||||||
|
this.baseDir = baseDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply changes from a response containing dyad tags
|
||||||
|
*/
|
||||||
|
public applyResponseChanges(fullResponse: string): void {
|
||||||
|
const writeTags = getDyadWriteTags(fullResponse);
|
||||||
|
const renameTags = getDyadRenameTags(fullResponse);
|
||||||
|
const deletePaths = getDyadDeleteTags(fullResponse);
|
||||||
|
|
||||||
|
// Process deletions
|
||||||
|
for (const deletePath of deletePaths) {
|
||||||
|
this.deleteFile(deletePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process renames (delete old, create new)
|
||||||
|
for (const rename of renameTags) {
|
||||||
|
this.renameFile(rename.from, rename.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process writes
|
||||||
|
for (const writeTag of writeTags) {
|
||||||
|
this.writeFile(writeTag.path, writeTag.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a file to the virtual filesystem
|
||||||
|
*/
|
||||||
|
public writeFile(relativePath: string, content: string): void {
|
||||||
|
const absolutePath = path.resolve(this.baseDir, relativePath);
|
||||||
|
this.virtualFiles.set(absolutePath, content);
|
||||||
|
// Remove from deleted files if it was previously deleted
|
||||||
|
this.deletedFiles.delete(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file from the virtual filesystem
|
||||||
|
*/
|
||||||
|
public deleteFile(relativePath: string): void {
|
||||||
|
const absolutePath = path.resolve(this.baseDir, relativePath);
|
||||||
|
this.deletedFiles.add(absolutePath);
|
||||||
|
// Remove from virtual files if it exists there
|
||||||
|
this.virtualFiles.delete(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a file in the virtual filesystem
|
||||||
|
*/
|
||||||
|
public renameFile(fromPath: string, toPath: string): void {
|
||||||
|
const fromAbsolute = path.resolve(this.baseDir, fromPath);
|
||||||
|
const toAbsolute = path.resolve(this.baseDir, toPath);
|
||||||
|
|
||||||
|
// Mark old file as deleted
|
||||||
|
this.deletedFiles.add(fromAbsolute);
|
||||||
|
|
||||||
|
// If the source file exists in virtual files, move its content
|
||||||
|
if (this.virtualFiles.has(fromAbsolute)) {
|
||||||
|
const content = this.virtualFiles.get(fromAbsolute)!;
|
||||||
|
this.virtualFiles.delete(fromAbsolute);
|
||||||
|
this.virtualFiles.set(toAbsolute, content);
|
||||||
|
} else {
|
||||||
|
// Try to read from actual filesystem
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(fromAbsolute, "utf8");
|
||||||
|
this.virtualFiles.set(toAbsolute, content);
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't read the source file, we'll let the consumer handle it
|
||||||
|
logger.warn(
|
||||||
|
`Could not read source file for rename: ${fromPath}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove destination from deleted files if it was previously deleted
|
||||||
|
this.deletedFiles.delete(toAbsolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all virtual files (files that have been written or modified)
|
||||||
|
*/
|
||||||
|
public getVirtualFiles(): VirtualFile[] {
|
||||||
|
return Array.from(this.virtualFiles.entries()).map(
|
||||||
|
([absolutePath, content]) => ({
|
||||||
|
path: path.relative(this.baseDir, absolutePath),
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all deleted file paths (relative to base directory)
|
||||||
|
*/
|
||||||
|
public getDeletedFiles(): string[] {
|
||||||
|
return Array.from(this.deletedFiles).map((absolutePath) =>
|
||||||
|
path.relative(this.baseDir, absolutePath),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all files that should be considered (existing + virtual - deleted)
|
||||||
|
*/
|
||||||
|
public getAllFiles(): string[] {
|
||||||
|
const allFiles = new Set<string>();
|
||||||
|
|
||||||
|
// Add virtual files
|
||||||
|
for (const [absolutePath] of this.virtualFiles.entries()) {
|
||||||
|
allFiles.add(path.relative(this.baseDir, absolutePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add existing files (this is a simplified version - in practice you might want to scan the directory)
|
||||||
|
// This method is mainly for getting the current state, consumers can combine with directory scanning
|
||||||
|
|
||||||
|
return Array.from(allFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file has been modified in the virtual filesystem
|
||||||
|
*/
|
||||||
|
public isFileModified(filePath: string): boolean {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.virtualFiles.has(absolutePath) || this.deletedFiles.has(absolutePath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all virtual changes
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this.virtualFiles.clear();
|
||||||
|
this.deletedFiles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base directory
|
||||||
|
*/
|
||||||
|
public getBaseDir(): string {
|
||||||
|
return this.baseDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is deleted in the virtual filesystem
|
||||||
|
*/
|
||||||
|
protected isDeleted(filePath: string): boolean {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
return this.deletedFiles.has(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exists in virtual files
|
||||||
|
*/
|
||||||
|
protected hasVirtualFile(filePath: string): boolean {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
return this.virtualFiles.has(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get virtual file content
|
||||||
|
*/
|
||||||
|
protected getVirtualFileContent(filePath: string): string | undefined {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
return this.virtualFiles.get(absolutePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous virtual filesystem
|
||||||
|
*/
|
||||||
|
export class SyncVirtualFileSystem extends BaseVirtualFileSystem {
|
||||||
|
private delegate: SyncFileSystemDelegate;
|
||||||
|
|
||||||
|
constructor(baseDir: string, delegate?: SyncFileSystemDelegate) {
|
||||||
|
super(baseDir);
|
||||||
|
this.delegate = delegate || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exists in the virtual filesystem
|
||||||
|
*/
|
||||||
|
public fileExists(filePath: string): boolean {
|
||||||
|
// Check if file is deleted
|
||||||
|
if (this.isDeleted(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists in virtual files
|
||||||
|
if (this.hasVirtualFile(filePath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to custom fileExists if provided
|
||||||
|
if (this.delegate.fileExists) {
|
||||||
|
return this.delegate.fileExists(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to actual filesystem
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
return fs.existsSync(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a file from the virtual filesystem
|
||||||
|
*/
|
||||||
|
public readFile(filePath: string): string | undefined {
|
||||||
|
// Check if file is deleted
|
||||||
|
if (this.isDeleted(filePath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check virtual files first
|
||||||
|
const virtualContent = this.getVirtualFileContent(filePath);
|
||||||
|
if (virtualContent !== undefined) {
|
||||||
|
return virtualContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to custom readFile if provided
|
||||||
|
if (this.delegate.readFile) {
|
||||||
|
return this.delegate.readFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to actual filesystem
|
||||||
|
try {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
return fs.readFileSync(absolutePath, "utf8");
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a custom file system interface for other tools
|
||||||
|
*/
|
||||||
|
public createFileSystemInterface() {
|
||||||
|
return {
|
||||||
|
fileExists: (fileName: string) => this.fileExists(fileName),
|
||||||
|
readFile: (fileName: string) => this.readFile(fileName),
|
||||||
|
writeFile: (fileName: string, content: string) =>
|
||||||
|
this.writeFile(fileName, content),
|
||||||
|
deleteFile: (fileName: string) => this.deleteFile(fileName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous virtual filesystem
|
||||||
|
*/
|
||||||
|
export class AsyncVirtualFileSystem extends BaseVirtualFileSystem {
|
||||||
|
private delegate: AsyncFileSystemDelegate;
|
||||||
|
|
||||||
|
constructor(baseDir: string, delegate?: AsyncFileSystemDelegate) {
|
||||||
|
super(baseDir);
|
||||||
|
this.delegate = delegate || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exists in the virtual filesystem
|
||||||
|
*/
|
||||||
|
public async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
// Check if file is deleted
|
||||||
|
if (this.isDeleted(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists in virtual files
|
||||||
|
if (this.hasVirtualFile(filePath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to custom fileExists if provided
|
||||||
|
if (this.delegate.fileExists) {
|
||||||
|
return this.delegate.fileExists(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to actual filesystem
|
||||||
|
try {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
await fs.promises.access(absolutePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a file from the virtual filesystem
|
||||||
|
*/
|
||||||
|
public async readFile(filePath: string): Promise<string | undefined> {
|
||||||
|
// Check if file is deleted
|
||||||
|
if (this.isDeleted(filePath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check virtual files first
|
||||||
|
const virtualContent = this.getVirtualFileContent(filePath);
|
||||||
|
if (virtualContent !== undefined) {
|
||||||
|
return virtualContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to custom readFile if provided
|
||||||
|
if (this.delegate.readFile) {
|
||||||
|
return this.delegate.readFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to actual filesystem
|
||||||
|
try {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.resolve(this.baseDir, filePath);
|
||||||
|
return await fs.promises.readFile(absolutePath, "utf8");
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
|
|||||||
import { glob } from "glob";
|
import { glob } from "glob";
|
||||||
import { AppChatContext } from "../lib/schemas";
|
import { AppChatContext } from "../lib/schemas";
|
||||||
import { readSettings } from "@/main/settings";
|
import { readSettings } from "@/main/settings";
|
||||||
|
import { AsyncVirtualFileSystem } from "./VirtualFilesystem";
|
||||||
|
|
||||||
const logger = log.scope("utils/codebase");
|
const logger = log.scope("utils/codebase");
|
||||||
|
|
||||||
@@ -156,8 +157,19 @@ async function isGitIgnored(
|
|||||||
/**
|
/**
|
||||||
* Read file contents with caching based on last modified time
|
* Read file contents with caching based on last modified time
|
||||||
*/
|
*/
|
||||||
async function readFileWithCache(filePath: string): Promise<string | null> {
|
export async function readFileWithCache(
|
||||||
|
filePath: string,
|
||||||
|
virtualFileSystem?: AsyncVirtualFileSystem,
|
||||||
|
): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
|
// Check virtual filesystem first if provided
|
||||||
|
if (virtualFileSystem) {
|
||||||
|
const virtualContent = await virtualFileSystem.readFile(filePath);
|
||||||
|
if (virtualContent != null) {
|
||||||
|
return cleanContent({ content: virtualContent, filePath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get file stats to check the modification time
|
// Get file stats to check the modification time
|
||||||
const stats = await fsAsync.stat(filePath);
|
const stats = await fsAsync.stat(filePath);
|
||||||
const currentMtime = stats.mtimeMs;
|
const currentMtime = stats.mtimeMs;
|
||||||
@@ -193,7 +205,7 @@ async function readFileWithCache(filePath: string): Promise<string | null> {
|
|||||||
return content;
|
return content;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error reading file: ${filePath}`, error);
|
logger.error(`Error reading file: ${filePath}`, error);
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +316,11 @@ const OMITTED_FILE_CONTENT = "// Contents omitted for brevity";
|
|||||||
/**
|
/**
|
||||||
* Format a file for inclusion in the codebase extract
|
* Format a file for inclusion in the codebase extract
|
||||||
*/
|
*/
|
||||||
async function formatFile(filePath: string, baseDir: string): Promise<string> {
|
async function formatFile(
|
||||||
|
filePath: string,
|
||||||
|
baseDir: string,
|
||||||
|
virtualFileSystem?: AsyncVirtualFileSystem,
|
||||||
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const relativePath = path
|
const relativePath = path
|
||||||
.relative(baseDir, filePath)
|
.relative(baseDir, filePath)
|
||||||
@@ -320,9 +336,9 @@ ${OMITTED_FILE_CONTENT}
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await readFileWithCache(filePath);
|
const content = await readFileWithCache(filePath, virtualFileSystem);
|
||||||
|
|
||||||
if (content === null) {
|
if (content == null) {
|
||||||
return `<dyad-file path="${relativePath}">
|
return `<dyad-file path="${relativePath}">
|
||||||
// Error reading file
|
// Error reading file
|
||||||
</dyad-file>
|
</dyad-file>
|
||||||
@@ -354,14 +370,17 @@ export type CodebaseFile = {
|
|||||||
/**
|
/**
|
||||||
* Extract and format codebase files as a string to be included in prompts
|
* Extract and format codebase files as a string to be included in prompts
|
||||||
* @param appPath - Path to the codebase to extract
|
* @param appPath - Path to the codebase to extract
|
||||||
|
* @param virtualFileSystem - Optional virtual filesystem to apply modifications
|
||||||
* @returns Object containing formatted output and individual files
|
* @returns Object containing formatted output and individual files
|
||||||
*/
|
*/
|
||||||
export async function extractCodebase({
|
export async function extractCodebase({
|
||||||
appPath,
|
appPath,
|
||||||
chatContext,
|
chatContext,
|
||||||
|
virtualFileSystem,
|
||||||
}: {
|
}: {
|
||||||
appPath: string;
|
appPath: string;
|
||||||
chatContext: AppChatContext;
|
chatContext: AppChatContext;
|
||||||
|
virtualFileSystem?: AsyncVirtualFileSystem;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
formattedOutput: string;
|
formattedOutput: string;
|
||||||
files: CodebaseFile[];
|
files: CodebaseFile[];
|
||||||
@@ -383,6 +402,26 @@ export async function extractCodebase({
|
|||||||
// Collect all relevant files
|
// Collect all relevant files
|
||||||
let files = await collectFiles(appPath, appPath);
|
let files = await collectFiles(appPath, appPath);
|
||||||
|
|
||||||
|
// Apply virtual filesystem modifications if provided
|
||||||
|
if (virtualFileSystem) {
|
||||||
|
// Filter out deleted files
|
||||||
|
const deletedFiles = new Set(
|
||||||
|
virtualFileSystem
|
||||||
|
.getDeletedFiles()
|
||||||
|
.map((relativePath) => path.resolve(appPath, relativePath)),
|
||||||
|
);
|
||||||
|
files = files.filter((file) => !deletedFiles.has(file));
|
||||||
|
|
||||||
|
// Add virtual files
|
||||||
|
const virtualFiles = virtualFileSystem.getVirtualFiles();
|
||||||
|
for (const virtualFile of virtualFiles) {
|
||||||
|
const absolutePath = path.resolve(appPath, virtualFile.path);
|
||||||
|
if (!files.includes(absolutePath)) {
|
||||||
|
files.push(absolutePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Collect files from contextPaths and smartContextAutoIncludes
|
// Collect files from contextPaths and smartContextAutoIncludes
|
||||||
const { contextPaths, smartContextAutoIncludes } = chatContext;
|
const { contextPaths, smartContextAutoIncludes } = chatContext;
|
||||||
const includedFiles = new Set<string>();
|
const includedFiles = new Set<string>();
|
||||||
@@ -443,7 +482,7 @@ export async function extractCodebase({
|
|||||||
// Format files and collect individual file contents
|
// Format files and collect individual file contents
|
||||||
const filesArray: CodebaseFile[] = [];
|
const filesArray: CodebaseFile[] = [];
|
||||||
const formatPromises = sortedFiles.map(async (file) => {
|
const formatPromises = sortedFiles.map(async (file) => {
|
||||||
const formattedContent = await formatFile(file, appPath);
|
const formattedContent = await formatFile(file, appPath, virtualFileSystem);
|
||||||
|
|
||||||
// Get raw content for the files array
|
// Get raw content for the files array
|
||||||
const relativePath = path
|
const relativePath = path
|
||||||
@@ -456,8 +495,8 @@ export async function extractCodebase({
|
|||||||
|
|
||||||
const fileContent = isOmittedFile(relativePath)
|
const fileContent = isOmittedFile(relativePath)
|
||||||
? OMITTED_FILE_CONTENT
|
? OMITTED_FILE_CONTENT
|
||||||
: await readFileWithCache(file);
|
: await readFileWithCache(file, virtualFileSystem);
|
||||||
if (fileContent !== null) {
|
if (fileContent != null) {
|
||||||
filesArray.push({
|
filesArray.push({
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
content: fileContent,
|
content: fileContent,
|
||||||
@@ -498,7 +537,8 @@ async function sortFilesByModificationTime(files: string[]): Promise<string[]> {
|
|||||||
return { file, mtime: stats.mtimeMs };
|
return { file, mtime: stats.mtimeMs };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If there's an error getting stats, use current time as fallback
|
// If there's an error getting stats, use current time as fallback
|
||||||
logger.error(`Error getting file stats for ${file}:`, error);
|
// This can happen with virtual files, so it's not a big deal.
|
||||||
|
logger.warn(`Error getting file stats for ${file}:`, error);
|
||||||
return { file, mtime: Date.now() };
|
return { file, mtime: Date.now() };
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -25,6 +25,55 @@ export const createChatCompletionHandler =
|
|||||||
|
|
||||||
let messageContent = CANNED_MESSAGE;
|
let messageContent = CANNED_MESSAGE;
|
||||||
|
|
||||||
|
// TS auto-fix prefixes
|
||||||
|
if (
|
||||||
|
lastMessage &&
|
||||||
|
typeof lastMessage.content === "string" &&
|
||||||
|
lastMessage.content.startsWith(
|
||||||
|
"Fix these 2 TypeScript compile-time error",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Fix errors in create-ts-errors.md and introduce a new error
|
||||||
|
messageContent = `
|
||||||
|
<dyad-write path="src/bad-file.ts" description="Fix 2 errors and introduce a new error.">
|
||||||
|
// Import doesn't exist
|
||||||
|
// import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.nonExistentMethod2();
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
lastMessage &&
|
||||||
|
typeof lastMessage.content === "string" &&
|
||||||
|
lastMessage.content.startsWith(
|
||||||
|
"Fix these 1 TypeScript compile-time error",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Fix errors in create-ts-errors.md and introduce a new error
|
||||||
|
messageContent = `
|
||||||
|
<dyad-write path="src/bad-file.ts" description="Fix remaining error.">
|
||||||
|
// Import doesn't exist
|
||||||
|
// import NonExistentClass from 'non-existent-class';
|
||||||
|
|
||||||
|
|
||||||
|
const x = new Object();
|
||||||
|
x.toString(); // replaced with existing method
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastMessage &&
|
||||||
|
typeof lastMessage.content === "string" &&
|
||||||
|
lastMessage.content.includes("TypeScript compile-time error")
|
||||||
|
) {
|
||||||
|
messageContent += "\n\n" + generateDump(req);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
lastMessage &&
|
lastMessage &&
|
||||||
typeof lastMessage.content === "string" &&
|
typeof lastMessage.content === "string" &&
|
||||||
|
|||||||
Reference in New Issue
Block a user