diff --git a/e2e-tests/problems.spec.ts b/e2e-tests/problems.spec.ts
index fd22175..909c37d 100644
--- a/e2e-tests/problems.spec.ts
+++ b/e2e-tests/problems.spec.ts
@@ -1,4 +1,4 @@
-import { test, testSkipIfWindows } from "./helpers/test_helper";
+import { test, testSkipIfWindows, Timeout } from "./helpers/test_helper";
import { expect } from "@playwright/test";
import fs from "fs";
import path from "path";
@@ -83,6 +83,72 @@ export default App;
await po.snapshotMessages({ replaceDumpPath: true });
});
+testSkipIfWindows(
+ "problems - select specific problems and fix",
+ async ({ po }) => {
+ await po.setUp();
+ await po.importApp(MINIMAL_APP);
+
+ // Create multiple TS errors in one file
+ const appPath = await po.getCurrentAppPath();
+ const badFilePath = path.join(appPath, "src", "bad-file.tsx");
+ fs.writeFileSync(
+ badFilePath,
+ `const App = () =>
Minimal imported app
;
+nonExistentFunction1();
+nonExistentFunction2();
+nonExistentFunction3();
+
+export default App;
+`,
+ );
+
+ await po.ensurePnpmInstall();
+
+ // Trigger creation of problems and open problems panel
+ // await po.sendPrompt("tc=create-ts-errors");
+ await po.selectPreviewMode("problems");
+ await po.clickRecheckProblems();
+
+ // Initially, all selected: button shows Fix X problems and Clear all is visible
+ const fixButton = po.page.getByTestId("fix-all-button");
+ await expect(fixButton).toBeVisible();
+ await expect(fixButton).toContainText(/Fix \d+ problems/);
+
+ // Click first two rows to toggle off (deselect)
+ const rows = po.page.getByTestId("problem-row");
+ const rowCount = await rows.count();
+ expect(rowCount).toBeGreaterThan(2);
+ await rows.nth(0).click();
+ await rows.nth(1).click();
+
+ // Button should update to reflect remaining selected
+ await expect(fixButton).toContainText(/Fix 1 problem/);
+
+ // Clear all should switch to Select all when none selected
+ // Deselect remaining rows
+ for (let i = 2; i < rowCount; i++) {
+ await rows.nth(i).click();
+ }
+
+ const selectButton = po.page.getByRole("button", {
+ name: /Select all/,
+ });
+ await expect(selectButton).toHaveText("Select all");
+
+ // Select all, then fix selected
+ await selectButton.click();
+ // Unselect the second row
+ await rows.nth(1).click();
+ await expect(fixButton).toContainText(/Fix 2 problems/);
+
+ await fixButton.click();
+ await po.waitForChatCompletion();
+ await po.snapshotServerDump("last-message");
+ await po.snapshotMessages({ replaceDumpPath: true });
+ },
+);
+
testSkipIfWindows("problems - manual edit (react/vite)", async ({ po }) => {
await po.setUp({ enableAutoFixProblems: true });
await po.sendPrompt("tc=1");
@@ -101,13 +167,15 @@ export default App;
await po.clickTogglePreviewPanel();
await po.selectPreviewMode("problems");
- await po.clickRecheckProblems();
- await po.snapshotProblemsPane();
+ const fixButton = po.page.getByTestId("fix-all-button");
+ await expect(fixButton).toBeEnabled({ timeout: Timeout.LONG });
+ await expect(fixButton).toContainText(/Fix 1 problem/);
fs.unlinkSync(badFilePath);
await po.clickRecheckProblems();
- await po.snapshotProblemsPane();
+ await expect(fixButton).toBeDisabled({ timeout: Timeout.LONG });
+ await expect(fixButton).toContainText(/Fix 0 problems/);
});
testSkipIfWindows("problems - manual edit (next.js)", async ({ po }) => {
@@ -129,11 +197,13 @@ testSkipIfWindows("problems - manual edit (next.js)", async ({ po }) => {
await po.clickTogglePreviewPanel();
await po.selectPreviewMode("problems");
- await po.clickRecheckProblems();
- await po.snapshotProblemsPane();
+ const fixButton = po.page.getByTestId("fix-all-button");
+ await expect(fixButton).toBeEnabled({ timeout: Timeout.LONG });
+ await expect(fixButton).toContainText(/Fix 1 problem/);
fs.unlinkSync(badFilePath);
await po.clickRecheckProblems();
- await po.snapshotProblemsPane();
+ await expect(fixButton).toBeDisabled({ timeout: Timeout.LONG });
+ await expect(fixButton).toContainText(/Fix 0 problems/);
});
diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml
deleted file mode 100644
index 77aa353..0000000
--- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-- img
-- text: 1 error
-- button "Run checks":
- - 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
diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-2.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-2.aria.yml
deleted file mode 100644
index 59c7712..0000000
--- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-2.aria.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-- paragraph: No problems found
-- img
-- button "Run checks":
- - img
\ No newline at end of file
diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml
deleted file mode 100644
index 2f0fefa..0000000
--- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-- img
-- text: 1 error
-- button "Run checks":
- - 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
diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml
deleted file mode 100644
index 59c7712..0000000
--- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-- paragraph: No problems found
-- img
-- button "Run checks":
- - img
\ No newline at end of file
diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.aria.yml
new file mode 100644
index 0000000..e843206
--- /dev/null
+++ b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.aria.yml
@@ -0,0 +1,21 @@
+- paragraph: "Fix these 2 TypeScript compile-time errors:"
+- list:
+ - listitem: src/bad-file.tsx:2:1 - Cannot find name 'nonExistentFunction1'. (TS2304)
+- code: const App = () => Minimal imported app
; nonExistentFunction1(); // <-- TypeScript compiler error here nonExistentFunction2();
+- list:
+ - listitem: src/bad-file.tsx:4:1 - Cannot find name 'nonExistentFunction3'. (TS2304)
+- code: nonExistentFunction2(); nonExistentFunction3(); // <-- TypeScript compiler error here
+- paragraph: Please fix all errors in a concise way.
+- img
+- text: bad-file.ts
+- button "Edit":
+ - img
+- img
+- text: "src/bad-file.ts Summary: Fix 2 errors and introduce a new error."
+- paragraph: "[[dyad-dump-path=*]]"
+- button:
+ - img
+- img
+- text: less than a minute ago
+- button "Retry":
+ - img
\ No newline at end of file
diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.txt b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.txt
new file mode 100644
index 0000000..3ff5fd1
--- /dev/null
+++ b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.txt
@@ -0,0 +1,19 @@
+===
+role: user
+message: Fix these 2 TypeScript compile-time errors:
+
+1. src/bad-file.tsx:2:1 - Cannot find name 'nonExistentFunction1'. (TS2304)
+```
+const App = () => Minimal imported app
;
+nonExistentFunction1(); // <-- TypeScript compiler error here
+nonExistentFunction2();
+```
+
+2. src/bad-file.tsx:4:1 - Cannot find name 'nonExistentFunction3'. (TS2304)
+```
+nonExistentFunction2();
+nonExistentFunction3(); // <-- TypeScript compiler error here
+```
+
+
+Please fix all errors in a concise way.
\ No newline at end of file
diff --git a/src/components/preview_panel/Problems.tsx b/src/components/preview_panel/Problems.tsx
index 2f88f38..cb13cb8 100644
--- a/src/components/preview_panel/Problems.tsx
+++ b/src/components/preview_panel/Problems.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { useAtom, useAtomValue } from "jotai";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
@@ -12,6 +12,7 @@ import {
} from "lucide-react";
import { Problem, ProblemReport } from "@/ipc/ipc_types";
import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
import { useStreamChat } from "@/hooks/useStreamChat";
import { useCheckProblems } from "@/hooks/useCheckProblems";
@@ -20,11 +21,26 @@ import { showError } from "@/lib/toast";
interface ProblemItemProps {
problem: Problem;
+ checked: boolean;
+ onToggle: () => void;
}
-const ProblemItem = ({ problem }: ProblemItemProps) => {
+const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => {
return (
-
+
+
e.stopPropagation()}
+ className="mt-0.5"
+ aria-label="Select problem"
+ />
@@ -56,6 +72,7 @@ interface RecheckButtonProps {
| "ghost"
| "link";
className?: string;
+ onBeforeRecheck?: () => void;
}
const RecheckButton = ({
@@ -63,11 +80,15 @@ const RecheckButton = ({
size = "sm",
variant = "outline",
className = "h-7 px-3 text-xs",
+ onBeforeRecheck,
}: RecheckButtonProps) => {
const { checkProblems, isChecking } = useCheckProblems(appId);
const [showingFeedback, setShowingFeedback] = useState(false);
const handleRecheck = async () => {
+ if (onBeforeRecheck) {
+ onBeforeRecheck();
+ }
setShowingFeedback(true);
const res = await checkProblems();
@@ -102,23 +123,24 @@ const RecheckButton = ({
interface ProblemsSummaryProps {
problemReport: ProblemReport;
appId: number;
+ selectedCount: number;
+ onClearAll: () => void;
+ onFixSelected: () => void;
+ onSelectAll: () => void;
}
-const ProblemsSummary = ({ problemReport, appId }: ProblemsSummaryProps) => {
- const { streamMessage } = useStreamChat();
+const ProblemsSummary = ({
+ problemReport,
+ appId,
+ selectedCount,
+ onClearAll,
+ onFixSelected,
+ onSelectAll,
+}: ProblemsSummaryProps) => {
const { problems } = problemReport;
const totalErrors = problems.length;
- const [selectedChatId] = useAtom(selectedChatIdAtom);
- const handleFixAll = () => {
- if (!selectedChatId) {
- return;
- }
- streamMessage({
- prompt: createProblemFixPrompt(problemReport),
- chatId: selectedChatId,
- });
- };
+ // Keep stream hook mounted; actual fix action is provided via onFixSelected
if (problems.length === 0) {
return (
@@ -148,16 +170,36 @@ const ProblemsSummary = ({ problemReport, appId }: ProblemsSummaryProps) => {
)}
-
+
+ {selectedCount === 0 ? (
+
+ ) : (
+
+ )}
@@ -175,6 +217,20 @@ export function Problems() {
export function _Problems() {
const selectedAppId = useAtomValue(selectedAppIdAtom);
const { problemReport } = useCheckProblems(selectedAppId);
+ const [selectedKeys, setSelectedKeys] = useState>(new Set());
+ const problemKey = (p: Problem) =>
+ `${p.file}:${p.line}:${p.column}:${p.code}`;
+ const { streamMessage } = useStreamChat();
+ const [selectedChatId] = useAtom(selectedChatIdAtom);
+
+ // Whenever the problems pane is shown or the report updates, select all problems
+ useEffect(() => {
+ if (problemReport?.problems?.length) {
+ setSelectedKeys(new Set(problemReport.problems.map(problemKey)));
+ } else {
+ setSelectedKeys(new Set());
+ }
+ }, [problemReport]);
if (!selectedAppId) {
return (
@@ -200,21 +256,65 @@ export function _Problems() {
Run checks to scan your app for TypeScript errors and other problems.
-
+ setSelectedKeys(new Set())}
+ />
);
}
return (
-
+
+ problemReport.problems.some((p) => problemKey(p) === key),
+ ).length
+ }
+ onClearAll={() => setSelectedKeys(new Set())}
+ onSelectAll={() =>
+ setSelectedKeys(
+ new Set(problemReport.problems.map((p) => problemKey(p))),
+ )
+ }
+ onFixSelected={() => {
+ if (!selectedChatId) return;
+ const selectedProblems = problemReport.problems.filter((p) =>
+ selectedKeys.has(problemKey(p)),
+ );
+ const subsetReport: ProblemReport = { problems: selectedProblems };
+ streamMessage({
+ prompt: createProblemFixPrompt(subsetReport),
+ chatId: selectedChatId,
+ });
+ }}
+ />
- {problemReport.problems.map((problem, index) => (
-
- ))}
+ {problemReport.problems.map((problem) => {
+ const selKey = problemKey(problem);
+ const checked = selectedKeys.has(selKey);
+ return (
+
{
+ setSelectedKeys((prev) => {
+ const next = new Set(prev);
+ if (next.has(selKey)) {
+ next.delete(selKey);
+ } else {
+ next.add(selKey);
+ }
+ return next;
+ });
+ }}
+ />
+ );
+ })}
);