adding a button for fixing all errors (#1785)

closes #1688 



<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Add a “Fix All Errors” button to the chat that collects all error
messages and sends a single request to resolve them. This helps users
fix multiple errors in one step.

- New Features
- Parse dyad-output type=error messages and track count/last index in
DyadMarkdownParser.
- Show FixAllErrorsButton after the last error when there are 2+ errors,
not streaming, and chatId is present.
- Button streams a prompt listing all errors, shows a loading state, and
displays the error count.

<sup>Written for commit b9762955d3b9cecd3b00c9efb478ce599f60e32d.
Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Mohamed Aziz Mejri
2025-12-05 07:20:35 +01:00
committed by GitHub
parent 40aeed1456
commit 90c5805b57
6 changed files with 229 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
import { testSkipIfWindows } from "./helpers/test_helper"; import { testSkipIfWindows, test } from "./helpers/test_helper";
testSkipIfWindows("fix error with AI", async ({ po }) => { testSkipIfWindows("fix error with AI", async ({ po }) => {
await po.setUp({ autoApprove: true }); await po.setUp({ autoApprove: true });
@@ -19,3 +19,13 @@ testSkipIfWindows("fix error with AI", async ({ po }) => {
// await po.locatePreviewErrorBanner().waitFor({ state: "hidden" }); // await po.locatePreviewErrorBanner().waitFor({ state: "hidden" });
await po.snapshotPreview(); await po.snapshotPreview();
}); });
test("fix all errors button", async ({ po }) => {
await po.setUp({ autoApprove: true });
await po.sendPrompt("tc=create-multiple-errors");
await po.clickFixAllErrors();
await po.waitForChatCompletion();
await po.snapshotMessages();
});

View File

@@ -0,0 +1,55 @@
I will intentionally add multiple errors to test the Fix All Errors button
<dyad-write path="src/pages/Index.tsx" description="intentionally add first error">
// Update this page (the content is just a fallback if you fail to update the page)
import { MadeWithDyad } from "@/components/made-with-dyad";
const Index = () => {
throw new Error("First error in Index");
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
<p className="text-xl text-gray-600">
Start building your amazing project here!
</p>
</div>
<MadeWithDyad />
</div>
);
};
export default Index;
</dyad-write>
<dyad-output type="error" message="First error in Index">
Error: First error in Index
at Index (http://localhost:5173/src/pages/Index.tsx:6:7)
</dyad-output>
<dyad-write path="src/components/ErrorComponent.tsx" description="intentionally add second error">
const ErrorComponent = () => {
throw new Error("Second error in ErrorComponent");
return <div>This will never render</div>;
};
export default ErrorComponent;
</dyad-write>
<dyad-output type="error" message="Second error in ErrorComponent">
Error: Second error in ErrorComponent
at ErrorComponent (http://localhost:5173/src/components/ErrorComponent.tsx:2:9)
</dyad-output>
<dyad-write path="src/utils/helper.ts" description="intentionally add third error">
export const brokenHelper = () => {
throw new Error("Third error in helper");
};
</dyad-write>
<dyad-output type="error" message="Third error in helper">
Error: Third error in helper
at brokenHelper (http://localhost:5173/src/utils/helper.ts:2:9)
</dyad-output>

View File

@@ -575,6 +575,10 @@ export class PageObject {
await this.page.getByRole("button", { name: "Fix error with AI" }).click(); await this.page.getByRole("button", { name: "Fix error with AI" }).click();
} }
async clickFixAllErrors() {
await this.page.getByRole("button", { name: /Fix All Errors/ }).click();
}
async snapshotPreviewErrorBanner() { async snapshotPreviewErrorBanner() {
await expect(this.locatePreviewErrorBanner()).toMatchAriaSnapshot({ await expect(this.locatePreviewErrorBanner()).toMatchAriaSnapshot({
timeout: Timeout.LONG, timeout: Timeout.LONG,

View File

@@ -0,0 +1,72 @@
- paragraph: tc=create-multiple-errors
- paragraph: I will intentionally add multiple errors to test the Fix All Errors button
- img
- text: Index.tsx
- button "Edit":
- img
- img
- text: "src/pages/Index.tsx Summary: intentionally add first error"
- img
- text: Error
- button "Fix with AI":
- img
- text: First error in Index...
- img
- img
- text: ErrorComponent.tsx
- button "Edit":
- img
- img
- text: "src/components/ErrorComponent.tsx Summary: intentionally add second error"
- img
- text: Error
- button "Fix with AI":
- img
- text: Second error in ErrorComponent...
- img
- img
- text: helper.ts
- button "Edit":
- img
- img
- text: "src/utils/helper.ts Summary: intentionally add third error"
- img
- text: Error
- button "Fix with AI":
- img
- text: Third error in helper...
- img
- button "Fix All Errors (3)":
- img
- button:
- img
- img
- text: Approved
- img
- text: less than a minute ago
- img
- text: wrote 3 file(s)
- paragraph: "Fix all of the following errors:"
- list:
- listitem: First error in Index
- listitem: Second error in ErrorComponent
- listitem: Third error in helper
- img
- text: file1.txt
- button "Edit":
- img
- img
- text: file1.txt
- paragraph: More EOM
- button:
- img
- img
- text: Approved
- img
- text: less than a minute ago
- img
- text: wrote 1 file(s)
- button "Undo":
- img
- button "Retry":
- img

View File

@@ -28,6 +28,7 @@ import { DyadCodeSearch } from "./DyadCodeSearch";
import { DyadRead } from "./DyadRead"; import { DyadRead } from "./DyadRead";
import { mapActionToButton } from "./ChatInput"; import { mapActionToButton } from "./ChatInput";
import { SuggestedAction } from "@/lib/schemas"; import { SuggestedAction } from "@/lib/schemas";
import { FixAllErrorsButton } from "./FixAllErrorsButton";
interface DyadMarkdownParserProps { interface DyadMarkdownParserProps {
content: string; content: string;
@@ -90,6 +91,34 @@ export const DyadMarkdownParser: React.FC<DyadMarkdownParserProps> = ({
return parseCustomTags(content); return parseCustomTags(content);
}, [content]); }, [content]);
// Extract error messages and track positions
const { errorMessages, lastErrorIndex, errorCount } = useMemo(() => {
const errors: string[] = [];
let lastIndex = -1;
let count = 0;
contentPieces.forEach((piece, index) => {
if (
piece.type === "custom-tag" &&
piece.tagInfo.tag === "dyad-output" &&
piece.tagInfo.attributes.type === "error"
) {
const errorMessage = piece.tagInfo.attributes.message;
if (errorMessage?.trim()) {
errors.push(errorMessage.trim());
count++;
lastIndex = index;
}
}
});
return {
errorMessages: errors,
lastErrorIndex: lastIndex,
errorCount: count,
};
}, [contentPieces]);
return ( return (
<> <>
{contentPieces.map((piece, index) => ( {contentPieces.map((piece, index) => (
@@ -106,6 +135,17 @@ export const DyadMarkdownParser: React.FC<DyadMarkdownParserProps> = ({
</ReactMarkdown> </ReactMarkdown>
) )
: renderCustomTag(piece.tagInfo, { isStreaming })} : renderCustomTag(piece.tagInfo, { isStreaming })}
{index === lastErrorIndex &&
errorCount > 1 &&
!isStreaming &&
chatId && (
<div className="mt-3 w-full flex">
<FixAllErrorsButton
errorMessages={errorMessages}
chatId={chatId}
/>
</div>
)}
</React.Fragment> </React.Fragment>
))} ))}
</> </>

View File

@@ -0,0 +1,47 @@
import { Button } from "@/components/ui/button";
import { useStreamChat } from "@/hooks/useStreamChat";
import { Sparkles, Loader2 } from "lucide-react";
import { useState } from "react";
interface FixAllErrorsButtonProps {
errorMessages: string[];
chatId: number;
}
export function FixAllErrorsButton({
errorMessages,
chatId,
}: FixAllErrorsButtonProps) {
const { streamMessage } = useStreamChat();
const [isLoading, setIsLoading] = useState(false);
const handleFixAllErrors = () => {
setIsLoading(true);
const allErrors = errorMessages
.map((msg, i) => `${i + 1}. ${msg}`)
.join("\n");
streamMessage({
prompt: `Fix all of the following errors:\n\n${allErrors}`,
chatId,
onSettled: () => setIsLoading(false),
});
};
return (
<Button
variant="outline"
size="sm"
disabled={isLoading}
onClick={handleFixAllErrors}
className="bg-red-50 hover:bg-red-100 dark:bg-red-950 dark:hover:bg-red-900 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800 ml-auto hover:cursor-pointer"
>
{isLoading ? (
<Loader2 size={16} className="mr-1 animate-spin" />
) : (
<Sparkles size={16} className="mr-1" />
)}
Fix All Errors ({errorMessages.length})
</Button>
);
}