Unverified 提交 678cd327 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

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
上级 52205be9
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
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
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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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
//////////////////////////////// ////////////////////////////////
......
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 }) => { ...@@ -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 @@ ...@@ -14,5 +14,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
- 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
\ No newline at end of file
===
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.
\ No newline at end of file
- 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'.
\ No newline at end of file
- paragraph: No problems found
- button "Recheck":
- img
\ No newline at end of file
- 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'.
\ No newline at end of file
- paragraph: No problems found
- button "Recheck":
- img
\ No newline at end of file
- 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'.
\ No newline at end of file
- paragraph: No problems found
- button "Recheck":
- img
\ No newline at end of file
- 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
\ No newline at end of file
- 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
\ No newline at end of file
- 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
\ No newline at end of file
- 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.
\ No newline at end of file
- 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
\ No newline at end of file
...@@ -11,5 +11,6 @@ ...@@ -11,5 +11,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -11,5 +11,6 @@ ...@@ -11,5 +11,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -11,5 +11,6 @@ ...@@ -11,5 +11,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
"enableProLazyEditsMode": true, "enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": true,
"isTestMode": true "isTestMode": true
} }
\ No newline at end of file
...@@ -3,10 +3,7 @@ import { expect } from "@playwright/test"; ...@@ -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");
......
// 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."
`;
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); ...@@ -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<
......
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"; ...@@ -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 }) { ...@@ -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 }) { ...@@ -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"; ...@@ -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): { ...@@ -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[] { ...@@ -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( ...@@ -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;
......
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"; ...@@ -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 { ...@@ -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 { ...@@ -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 = ({ ...@@ -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;
</div> case "problems":
<div className="flex items-center"> targetRef = problemsRef;
<button break;
onClick={onRestart} default:
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors" return;
title="Restart App" }
>
<Power size={16} /> if (targetRef.current) {
<span>Restart</span> const button = targetRef.current;
</button> const container = button.parentElement;
<DropdownMenu> if (container) {
<DropdownMenuTrigger asChild> const containerRect = container.getBoundingClientRect();
<button const buttonRect = button.getBoundingClientRect();
data-testid="preview-more-options-button" const left = buttonRect.left - containerRect.left;
className="flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors" const width = buttonRect.width;
title="More options"
> setIndicatorStyle({ left, width });
<MoreVertical size={16} /> }
</button> }
</DropdownMenuTrigger> };
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}> // Small delay to ensure DOM is updated
<Cog size={16} /> const timeoutId = setTimeout(updateIndicator, 10);
<div className="flex flex-col"> return () => clearTimeout(timeoutId);
<span>Rebuild</span> }, [previewMode, displayCount]);
<span className="text-xs text-muted-foreground">
Re-installs node_modules and restarts return (
</span> <div className="flex items-center justify-between px-4 py-2 border-b border-border">
</div> <div className="relative flex bg-[var(--background-darkest)] rounded-md p-0.5">
</DropdownMenuItem> <motion.div
<DropdownMenuItem onClick={onClearSessionData}> className="absolute top-0.5 bottom-0.5 bg-[var(--background-lightest)] shadow rounded-md"
<Trash2 size={16} /> animate={{
<div className="flex flex-col"> left: indicatorStyle.left,
<span>Clear Preview Data</span> width: indicatorStyle.width,
<span className="text-xs text-muted-foreground"> }}
Clears cookies and local storage for the app preview transition={{
</span> type: "spring",
</div> stiffness: 600,
</DropdownMenuItem> damping: 35,
</DropdownMenuContent> mass: 0.6,
</DropdownMenu> }}
/>
<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> );
); };
// Console header component // Console header component
const ConsoleHeader = ({ const ConsoleHeader = ({
...@@ -262,8 +340,10 @@ export function PreviewPanel() { ...@@ -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>
......
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>
);
}
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"; ...@@ -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({ ...@@ -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({ ...@@ -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({ ...@@ -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({ ...@@ -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 {
......
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;
}
});
}
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论