feat : allow taking a screenshot for bug reports (#1678)

This PR implements allowing users to take a screenshot for bug reports
and addresses issue #1125









<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds screenshot capture to bug reports. Users can take a screenshot
that’s copied to the clipboard, then continue to file the issue.

- **New Features**
  - New BugScreenshotDialog to take a screenshot or report without one.
- ScreenshotSuccessDialog confirms capture and guides users to the
GitHub issue.
- HelpDialog now opens the screenshot flow; issue template includes an
optional Screenshot section.
- Electron IPC handler take-screenshot captures the focused window and
writes the image to the clipboard; exposed via IpcClient and whitelisted
in preload.

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

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Mohamed Aziz Mejri
2025-11-19 04:31:02 +01:00
committed by GitHub
parent d436596024
commit 8dee2552bb
6 changed files with 169 additions and 2 deletions

View File

@@ -0,0 +1,91 @@
import { IpcClient } from "@/ipc/ipc_client";
import { Dialog, DialogTitle } from "@radix-ui/react-dialog";
import { DialogContent, DialogHeader } from "./ui/dialog";
import { Button } from "./ui/button";
import { BugIcon, Camera } from "lucide-react";
import { useState } from "react";
import { ScreenshotSuccessDialog } from "./ScreenshotSuccessDialog";
interface BugScreenshotDialogProps {
isOpen: boolean;
onClose: () => void;
handleReportBug: () => Promise<void>;
isLoading: boolean;
}
export function BugScreenshotDialog({
isOpen,
onClose,
handleReportBug,
isLoading,
}: BugScreenshotDialogProps) {
const [isScreenshotSuccessOpen, setIsScreenshotSuccessOpen] = useState(false);
const [screenshotError, setScreenshotError] = useState<string | null>(null);
const handleReportBugWithScreenshot = async () => {
setScreenshotError(null);
onClose();
setTimeout(async () => {
try {
await IpcClient.getInstance().takeScreenshot();
setIsScreenshotSuccessOpen(true);
} catch (error) {
setScreenshotError(
error instanceof Error ? error.message : "Failed to take screenshot",
);
}
}, 200); // Small delay for dialog to close
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Take a screenshot?</DialogTitle>
</DialogHeader>
<div className="flex flex-col space-y-4 w-full">
<div className="flex flex-col space-y-2">
<Button
variant="default"
onClick={handleReportBugWithScreenshot}
className="w-full py-6 border-primary/50 shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
>
<Camera className="mr-2 h-5 w-5" /> Take a screenshot
(recommended)
</Button>
<p className="text-sm text-muted-foreground px-2">
You'll get better and faster responses if you do this!
</p>
</div>
<div className="flex flex-col space-y-2">
<Button
variant="outline"
onClick={() => {
handleReportBug();
}}
className="w-full py-6 bg-(--background-lightest)"
>
<BugIcon className="mr-2 h-5 w-5" />{" "}
{isLoading
? "Preparing Report..."
: "File bug report without screenshot"}
</Button>
<p className="text-sm text-muted-foreground px-2">
We'll still try to respond but might not be able to help as much.
</p>
</div>
{screenshotError && (
<p className="text-sm text-destructive px-2">
Failed to take screenshot: {screenshotError}
</p>
)}
</div>
</DialogContent>
<ScreenshotSuccessDialog
isOpen={isScreenshotSuccessOpen}
onClose={() => setIsScreenshotSuccessOpen(false)}
handleReportBug={handleReportBug}
isLoading={isLoading}
/>
</Dialog>
);
}

View File

@@ -25,6 +25,7 @@ import { ChatLogsData } from "@/ipc/ipc_types";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { HelpBotDialog } from "./HelpBotDialog"; import { HelpBotDialog } from "./HelpBotDialog";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { BugScreenshotDialog } from "./BugScreenshotDialog";
interface HelpDialogProps { interface HelpDialogProps {
isOpen: boolean; isOpen: boolean;
@@ -39,6 +40,7 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const [uploadComplete, setUploadComplete] = useState(false); const [uploadComplete, setUploadComplete] = useState(false);
const [sessionId, setSessionId] = useState(""); const [sessionId, setSessionId] = useState("");
const [isHelpBotOpen, setIsHelpBotOpen] = useState(false); const [isHelpBotOpen, setIsHelpBotOpen] = useState(false);
const [isBugScreenshotOpen, setIsBugScreenshotOpen] = useState(false);
const selectedChatId = useAtomValue(selectedChatIdAtom); const selectedChatId = useAtomValue(selectedChatIdAtom);
const { settings } = useSettings(); const { settings } = useSettings();
@@ -91,6 +93,9 @@ Issues that do not meet these requirements will be closed and may need to be res
## Actual Behavior (required) ## Actual Behavior (required)
<!-- What actually happened? --> <!-- What actually happened? -->
## Screenshot (Optional)
<!-- Screenshot of the bug -->
## System Information ## System Information
- Dyad Version: ${debugInfo.dyadVersion} - Dyad Version: ${debugInfo.dyadVersion}
- Platform: ${debugInfo.platform} - Platform: ${debugInfo.platform}
@@ -427,7 +432,10 @@ Session ID: ${sessionId}
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<Button <Button
variant="outline" variant="outline"
onClick={handleReportBug} onClick={() => {
handleClose();
setIsBugScreenshotOpen(true);
}}
disabled={isLoading} disabled={isLoading}
className="w-full py-6 bg-(--background-lightest)" className="w-full py-6 bg-(--background-lightest)"
> >
@@ -460,6 +468,12 @@ Session ID: ${sessionId}
isOpen={isHelpBotOpen} isOpen={isHelpBotOpen}
onClose={() => setIsHelpBotOpen(false)} onClose={() => setIsHelpBotOpen(false)}
/> />
<BugScreenshotDialog
isOpen={isBugScreenshotOpen}
onClose={() => setIsBugScreenshotOpen(false)}
handleReportBug={handleReportBug}
isLoading={isLoading}
/>
</Dialog> </Dialog>
); );
} }

View File

@@ -0,0 +1,42 @@
import { DialogTitle } from "@radix-ui/react-dialog";
import { Dialog, DialogContent, DialogHeader } from "./ui/dialog";
import { Button } from "./ui/button";
import { BugIcon } from "lucide-react";
interface ScreenshotSuccessDialogProps {
isOpen: boolean;
onClose: () => void;
handleReportBug: () => Promise<void>;
isLoading: boolean;
}
export function ScreenshotSuccessDialog({
isOpen,
onClose,
handleReportBug,
isLoading,
}: ScreenshotSuccessDialogProps) {
const handleSubmit = async () => {
await handleReportBug();
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Screenshot captured to clipboard! Please paste in GitHub issue.
</DialogTitle>
</DialogHeader>
<Button
variant="default"
onClick={handleSubmit}
className="w-full py-6 border-primary/50 shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
>
<BugIcon className="mr-2 h-5 w-5" />{" "}
{isLoading ? "Preparing Report..." : "Create GitHub issue"}
</Button>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,4 +1,4 @@
import { ipcMain } from "electron"; import { BrowserWindow, clipboard, ipcMain } from "electron";
import { platform, arch } from "os"; import { platform, arch } from "os";
import { SystemDebugInfo, ChatLogsData } from "../ipc_types"; import { SystemDebugInfo, ChatLogsData } from "../ipc_types";
import { readSettings } from "../../main/settings"; import { readSettings } from "../../main/settings";
@@ -196,6 +196,20 @@ export function registerDebugHandlers() {
); );
console.log("Registered debug IPC handlers"); console.log("Registered debug IPC handlers");
ipcMain.handle("take-screenshot", async () => {
const win = BrowserWindow.getFocusedWindow();
if (!win) throw new Error("No focused window to capture");
// Capture the window's current contents as a NativeImage
const image = await win.capturePage();
// Validate image
if (!image || image.isEmpty()) {
throw new Error("Failed to capture screenshot");
}
// Write the image to the clipboard
clipboard.writeImage(image);
});
} }
function serializeModelForDebug(model: LargeLanguageModel): string { function serializeModelForDebug(model: LargeLanguageModel): string {

View File

@@ -1320,6 +1320,10 @@ export class IpcClient {
}); });
} }
public async takeScreenshot(): Promise<void> {
await this.ipcRenderer.invoke("take-screenshot");
}
public cancelHelpChat(sessionId: string): void { public cancelHelpChat(sessionId: string): void {
this.ipcRenderer.invoke("help:chat:cancel", sessionId).catch(() => {}); this.ipcRenderer.invoke("help:chat:cancel", sessionId).catch(() => {});
} }

View File

@@ -121,6 +121,8 @@ const validInvokeChannels = [
"mcp:set-tool-consent", "mcp:set-tool-consent",
// MCP consent response from renderer to main // MCP consent response from renderer to main
"mcp:tool-consent-response", "mcp:tool-consent-response",
// Help
"take-screenshot",
// Help bot // Help bot
"help:chat:start", "help:chat:start",
"help:chat:cancel", "help:chat:cancel",