Add help button which opens GitHub issue w/ system report | explicitly log with electron-log & scrub logs

This commit is contained in:
Will Chen
2025-04-21 14:27:58 -07:00
parent 57af6078a0
commit 87ff4ee870
18 changed files with 374 additions and 256 deletions

View File

@@ -9,6 +9,7 @@ import {
import { Button } from "@/components/ui/button";
import { BookOpenIcon, BugIcon } from "lucide-react";
import { IpcClient } from "@/ipc/ipc_client";
import { useState } from "react";
interface HelpDialogProps {
isOpen: boolean;
@@ -16,6 +17,60 @@ interface HelpDialogProps {
}
export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const [isLoading, setIsLoading] = useState(false);
const handleReportBug = async () => {
setIsLoading(true);
try {
// Get system debug info
const debugInfo = await IpcClient.getInstance().getSystemDebugInfo();
// Create a formatted issue body with the debug info
const issueBody = `
## Bug Description
<!-- Please describe the issue you're experiencing -->
## Steps to Reproduce
<!-- Please list the steps to reproduce the issue -->
## Expected Behavior
<!-- What did you expect to happen? -->
## Actual Behavior
<!-- What actually happened? -->
## System Information
- Dyad Version: ${debugInfo.dyadVersion}
- Platform: ${debugInfo.platform}
- Architecture: ${debugInfo.architecture}
- Node Version: ${debugInfo.nodeVersion || "Not available"}
- PNPM Version: ${debugInfo.pnpmVersion || "Not available"}
- Node Path: ${debugInfo.nodePath || "Not available"}
## Logs
\`\`\`
${debugInfo.logs.slice(-3_500) || "No logs available"}
\`\`\`
`;
// Create the GitHub issue URL with the pre-filled body
const encodedBody = encodeURIComponent(issueBody);
const encodedTitle = encodeURIComponent("[bug] <add title>");
const githubIssueUrl = `https://github.com/dyad-sh/dyad/issues/new?title=${encodedTitle}&labels=bug,filed-from-app&body=${encodedBody}`;
// Open the pre-filled GitHub issue page
IpcClient.getInstance().openExternalUrl(githubIssueUrl);
} catch (error) {
console.error("Failed to prepare bug report:", error);
// Fallback to opening the regular GitHub issue page
IpcClient.getInstance().openExternalUrl(
"https://github.com/dyad-sh/dyad/issues/new"
);
} finally {
setIsLoading(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
@@ -46,17 +101,15 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
<div className="flex flex-col space-y-2">
<Button
variant="outline"
onClick={() =>
IpcClient.getInstance().openExternalUrl(
"https://github.com/dyad-sh/dyad/issues/new"
)
}
onClick={handleReportBug}
disabled={isLoading}
className="w-full py-6 bg-(--background-lightest)"
>
<BugIcon className="mr-2 h-5 w-5" /> Report a Bug
<BugIcon className="mr-2 h-5 w-5" />{" "}
{isLoading ? "Preparing Report..." : "Report a Bug"}
</Button>
<p className="text-sm text-muted-foreground px-2">
Well auto-fill your report with system info and logs. You can
We'll auto-fill your report with system info and logs. You can
review it for any sensitive info before submitting.
</p>
</div>

View File

@@ -20,7 +20,6 @@ export function ProviderSettingsGrid({
const navigate = useNavigate();
const handleProviderClick = (provider: ModelProvider) => {
console.log("PROVIDER", provider);
navigate({
to: providerSettingsRoute.id,
params: { provider },

View File

@@ -9,6 +9,9 @@ import path from "node:path";
import fs from "node:fs";
import { getDyadAppPath, getUserDataPath } from "../paths/paths";
import { eq } from "drizzle-orm";
import log from "electron-log";
const logger = log.scope("db");
// Database connection factory
let _db: ReturnType<typeof drizzle> | null = null;
@@ -29,7 +32,7 @@ export function initializeDatabase(): BetterSQLite3Database<typeof schema> & {
if (_db) return _db as any;
const dbPath = getDatabasePath();
console.log("Initializing database at:", dbPath);
logger.log("Initializing database at:", dbPath);
// Check if the database file exists and remove it if it has issues
try {
@@ -38,14 +41,12 @@ export function initializeDatabase(): BetterSQLite3Database<typeof schema> & {
const stats = fs.statSync(dbPath);
// If the file is very small, it might be corrupted
if (stats.size < 100) {
console.log(
"Database file exists but may be corrupted. Removing it..."
);
logger.log("Database file exists but may be corrupted. Removing it...");
fs.unlinkSync(dbPath);
}
}
} catch (error) {
console.error("Error checking database file:", error);
logger.error("Error checking database file:", error);
}
fs.mkdirSync(getUserDataPath(), { recursive: true });
@@ -62,21 +63,15 @@ export function initializeDatabase(): BetterSQLite3Database<typeof schema> & {
_db = drizzle(sqlite, { schema });
try {
// Run migrations programmatically
const migrationsFolder = path.join(__dirname, "..", "..", "drizzle");
console.log("MIGRATIONS FOLDER INITIALIZE", migrationsFolder);
// Verify migrations folder exists
if (!fs.existsSync(migrationsFolder)) {
console.error("Migrations folder not found:", migrationsFolder);
logger.error("Migrations folder not found:", migrationsFolder);
} else {
console.log("Running migrations from:", migrationsFolder);
logger.log("Running migrations from:", migrationsFolder);
migrate(_db, { migrationsFolder });
}
} catch (error) {
console.error("Migration error:", error);
logger.error("Migration error:", error);
}
return _db as any;
@@ -86,7 +81,7 @@ export function initializeDatabase(): BetterSQLite3Database<typeof schema> & {
try {
initializeDatabase();
} catch (error) {
console.error("Failed to initialize database:", error);
logger.error("Failed to initialize database:", error);
}
export const db = _db as any as BetterSQLite3Database<typeof schema> & {

View File

@@ -36,15 +36,14 @@ import fixPath from "fix-path";
import { getGitAuthor } from "../utils/git_author";
import killPort from "kill-port";
import util from "util";
import log from "electron-log";
const logger = log.scope("app_handlers");
// Needed, otherwise electron in MacOS/Linux will not be able
// to find node/pnpm.
fixPath();
// Keep track of the static file server worker
let staticServerWorker: Worker | null = null;
let staticServerPort: number | null = null;
// let staticServerRootDir: string | null = null; // Store the root dir it's serving - Removed
async function executeApp({
appPath,
appId,
@@ -96,7 +95,7 @@ async function executeAppLocalNode({
// Log output
process.stdout?.on("data", (data) => {
const message = util.stripVTControlCharacters(data.toString());
console.log(`App ${appId} (PID: ${process.pid}) stdout: ${message}`);
logger.debug(`App ${appId} (PID: ${process.pid}) stdout: ${message}`);
event.sender.send("app:output", {
type: "stdout",
message,
@@ -106,7 +105,7 @@ async function executeAppLocalNode({
process.stderr?.on("data", (data) => {
const message = util.stripVTControlCharacters(data.toString());
console.error(`App ${appId} (PID: ${process.pid}) stderr: ${message}`);
logger.error(`App ${appId} (PID: ${process.pid}) stderr: ${message}`);
event.sender.send("app:output", {
type: "stderr",
message,
@@ -116,7 +115,7 @@ async function executeAppLocalNode({
// Handle process exit/close
process.on("close", (code, signal) => {
console.log(
logger.log(
`App ${appId} (PID: ${process.pid}) process closed with code ${code}, signal ${signal}.`
);
removeAppIfCurrentProcess(appId, process);
@@ -124,7 +123,7 @@ async function executeAppLocalNode({
// Handle errors during process lifecycle (e.g., command not found)
process.on("error", (err) => {
console.error(
logger.error(
`Error in app ${appId} (PID: ${process.pid}) process: ${err.message}`
);
removeAppIfCurrentProcess(appId, process);
@@ -196,7 +195,7 @@ export function registerAppHandlers() {
author: await getGitAuthor(),
});
} catch (error) {
console.error("Error in background app initialization:", error);
logger.error("Error in background app initialization:", error);
}
// })();
@@ -219,7 +218,7 @@ export function registerAppHandlers() {
try {
files = getFilesRecursively(appPath, appPath);
} catch (error) {
console.error(`Error reading files for app ${appId}:`, error);
logger.error(`Error reading files for app ${appId}:`, error);
// Return app even if files couldn't be read
}
@@ -266,10 +265,7 @@ export function registerAppHandlers() {
const contents = fs.readFileSync(fullPath, "utf-8");
return contents;
} catch (error) {
console.error(
`Error reading file ${filePath} for app ${appId}:`,
error
);
logger.error(`Error reading file ${filePath} for app ${appId}:`, error);
throw new Error("Failed to read file");
}
}
@@ -292,7 +288,7 @@ export function registerAppHandlers() {
return withLock(appId, async () => {
// Check if app is already running
if (runningApps.has(appId)) {
console.debug(`App ${appId} is already running.`);
logger.debug(`App ${appId} is already running.`);
// Potentially return the existing process info or confirm status
return { success: true, message: "App already running." };
}
@@ -305,7 +301,7 @@ export function registerAppHandlers() {
throw new Error("App not found");
}
console.debug(`Starting app ${appId} in path ${app.path}`);
logger.debug(`Starting app ${appId} in path ${app.path}`);
const appPath = getDyadAppPath(app.path);
try {
@@ -313,7 +309,7 @@ export function registerAppHandlers() {
return { success: true, processId: currentProcessId };
} catch (error: any) {
console.error(`Error running app ${appId}:`, error);
logger.error(`Error running app ${appId}:`, error);
// Ensure cleanup if error happens during setup but before process events are handled
if (
runningApps.has(appId) &&
@@ -328,15 +324,15 @@ export function registerAppHandlers() {
);
ipcMain.handle("stop-app", async (_, { appId }: { appId: number }) => {
console.log(
`Attempting to stop app ${appId} (local-node only). Current running apps: ${runningApps.size}`
logger.log(
`Attempting to stop app ${appId}. Current running apps: ${runningApps.size}`
);
return withLock(appId, async () => {
const appInfo = runningApps.get(appId);
if (!appInfo) {
console.log(
`App ${appId} not found in running apps map (local-node). Assuming already stopped.`
logger.log(
`App ${appId} not found in running apps map. Assuming already stopped.`
);
return {
success: true,
@@ -345,13 +341,13 @@ export function registerAppHandlers() {
}
const { process, processId } = appInfo;
console.log(
logger.log(
`Found running app ${appId} with processId ${processId} (PID: ${process.pid}). Attempting to stop.`
);
// Check if the process is already exited or closed
if (process.exitCode !== null || process.signalCode !== null) {
console.log(
logger.log(
`Process for app ${appId} (PID: ${process.pid}) already exited (code: ${process.exitCode}, signal: ${process.signalCode}). Cleaning up map.`
);
runningApps.delete(appId); // Ensure cleanup if somehow missed
@@ -367,7 +363,7 @@ export function registerAppHandlers() {
return { success: true };
} catch (error: any) {
console.error(
logger.error(
`Error stopping app ${appId} (PID: ${process.pid}, processId: ${processId}):`,
error
);
@@ -384,24 +380,21 @@ export function registerAppHandlers() {
event: Electron.IpcMainInvokeEvent,
{ appId }: { appId: number }
) => {
// Static server worker is NOT terminated here anymore
logger.log(`Restarting app ${appId}`);
return withLock(appId, async () => {
try {
// First stop the app if it's running
const appInfo = runningApps.get(appId);
if (appInfo) {
const { process, processId } = appInfo;
console.log(
`Stopping local-node app ${appId} (processId ${processId}) before restart` // Adjusted log
logger.log(
`Stopping app ${appId} (processId ${processId}) before restart`
);
await killProcess(process);
runningApps.delete(appId);
} else {
console.log(
`App ${appId} not running in local-node mode, proceeding to start.`
);
logger.log(`App ${appId} not running. Proceeding to start.`);
}
// Kill any orphaned process on port 32100 (in case previous run left it)
@@ -417,7 +410,7 @@ export function registerAppHandlers() {
}
const appPath = getDyadAppPath(app.path);
console.debug(
logger.debug(
`Executing app ${appId} in path ${app.path} after restart request`
); // Adjusted log
@@ -425,7 +418,7 @@ export function registerAppHandlers() {
return { success: true };
} catch (error) {
console.error(`Error restarting app ${appId}:`, error);
logger.error(`Error restarting app ${appId}:`, error);
throw error;
}
});
@@ -461,7 +454,7 @@ export function registerAppHandlers() {
timestamp: commit.commit.author.timestamp,
})) satisfies Version[];
} catch (error: any) {
console.error(`Error listing versions for app ${appId}:`, error);
logger.error(`Error listing versions for app ${appId}:`, error);
throw new Error(`Failed to list versions: ${error.message}`);
}
});
@@ -552,7 +545,7 @@ export function registerAppHandlers() {
return { success: true };
} catch (error: any) {
console.error(
logger.error(
`Error reverting to version ${previousVersionId} for app ${appId}:`,
error
);
@@ -587,7 +580,7 @@ export function registerAppHandlers() {
return { success: true };
} catch (error: any) {
console.error(
logger.error(
`Error checking out version ${versionId} for app ${appId}:`,
error
);
@@ -614,7 +607,7 @@ export function registerAppHandlers() {
try {
return await extractCodebase(appPath, maxFiles);
} catch (error) {
console.error(`Error extracting codebase for app ${appId}:`, error);
logger.error(`Error extracting codebase for app ${appId}:`, error);
throw new Error(
`Failed to extract codebase: ${(error as any).message}`
);
@@ -673,10 +666,7 @@ export function registerAppHandlers() {
return { success: true };
} catch (error: any) {
console.error(
`Error writing file ${filePath} for app ${appId}:`,
error
);
logger.error(`Error writing file ${filePath} for app ${appId}:`, error);
throw new Error(`Failed to write file: ${error.message}`);
}
}
@@ -699,14 +689,11 @@ export function registerAppHandlers() {
if (runningApps.has(appId)) {
const appInfo = runningApps.get(appId)!;
try {
console.log(`Stopping local-node app ${appId} before deletion.`); // Adjusted log
logger.log(`Stopping app ${appId} before deletion.`); // Adjusted log
await killProcess(appInfo.process);
runningApps.delete(appId);
} catch (error: any) {
console.error(
`Error stopping local-node app ${appId} before deletion:`,
error
); // Adjusted log
logger.error(`Error stopping app ${appId} before deletion:`, error); // Adjusted log
// Continue with deletion even if stopping fails
}
}
@@ -716,7 +703,7 @@ export function registerAppHandlers() {
try {
await fsPromises.rm(appPath, { recursive: true, force: true });
} catch (error: any) {
console.error(`Error deleting app files for app ${appId}:`, error);
logger.error(`Error deleting app files for app ${appId}:`, error);
throw new Error(`Failed to delete app files: ${error.message}`);
}
@@ -726,7 +713,7 @@ export function registerAppHandlers() {
// Note: Associated chats will cascade delete if that's set up in the schema
return { success: true };
} catch (error: any) {
console.error(`Error deleting app ${appId} from database:`, error);
logger.error(`Error deleting app ${appId} from database:`, error);
throw new Error(`Failed to delete app from database: ${error.message}`);
}
});
@@ -776,10 +763,7 @@ export function registerAppHandlers() {
await killProcess(appInfo.process);
runningApps.delete(appId);
} catch (error: any) {
console.error(
`Error stopping app ${appId} before renaming:`,
error
);
logger.error(`Error stopping app ${appId} before renaming:`, error);
throw new Error(
`Failed to stop app before renaming: ${error.message}`
);
@@ -807,7 +791,7 @@ export function registerAppHandlers() {
// Move the files
await fsPromises.rename(oldAppPath, newAppPath);
} catch (error: any) {
console.error(
logger.error(
`Error moving app files from ${oldAppPath} to ${newAppPath}:`,
error
);
@@ -833,14 +817,14 @@ export function registerAppHandlers() {
try {
await fsPromises.rename(newAppPath, oldAppPath);
} catch (rollbackError) {
console.error(
logger.error(
`Failed to rollback file move during rename error:`,
rollbackError
);
}
}
console.error(`Error updating app ${appId} in database:`, error);
logger.error(`Error updating app ${appId} in database:`, error);
throw new Error(`Failed to update app in database: ${error.message}`);
}
});
@@ -848,16 +832,9 @@ export function registerAppHandlers() {
);
ipcMain.handle("reset-all", async () => {
console.log("start: resetting all apps and settings.");
// Terminate static server worker if it's running
if (staticServerWorker) {
console.log(`Terminating static server worker on reset-all command.`);
await staticServerWorker.terminate();
staticServerWorker = null;
staticServerPort = null;
}
logger.log("start: resetting all apps and settings.");
// Stop all running apps first
console.log("stopping all running apps...");
logger.log("stopping all running apps...");
const runningAppIds = Array.from(runningApps.keys());
for (const appId of runningAppIds) {
try {
@@ -865,12 +842,12 @@ export function registerAppHandlers() {
await killProcess(appInfo.process);
runningApps.delete(appId);
} catch (error) {
console.error(`Error stopping app ${appId} during reset:`, error);
logger.error(`Error stopping app ${appId} during reset:`, error);
// Continue with reset even if stopping fails
}
}
console.log("all running apps stopped.");
console.log("deleting database...");
logger.log("all running apps stopped.");
logger.log("deleting database...");
// 1. Drop the database by deleting the SQLite file
const dbPath = getDatabasePath();
if (fs.existsSync(dbPath)) {
@@ -879,31 +856,31 @@ export function registerAppHandlers() {
db.$client.close();
}
await fsPromises.unlink(dbPath);
console.log(`Database file deleted: ${dbPath}`);
logger.log(`Database file deleted: ${dbPath}`);
}
console.log("database deleted.");
console.log("deleting settings...");
logger.log("database deleted.");
logger.log("deleting settings...");
// 2. Remove settings
const userDataPath = getUserDataPath();
const settingsPath = path.join(userDataPath, "user-settings.json");
if (fs.existsSync(settingsPath)) {
await fsPromises.unlink(settingsPath);
console.log(`Settings file deleted: ${settingsPath}`);
logger.log(`Settings file deleted: ${settingsPath}`);
}
console.log("settings deleted.");
logger.log("settings deleted.");
// 3. Remove all app files recursively
// Doing this last because it's the most time-consuming and the least important
// in terms of resetting the app state.
console.log("removing all app files...");
logger.log("removing all app files...");
const dyadAppPath = getDyadAppPath(".");
if (fs.existsSync(dyadAppPath)) {
await fsPromises.rm(dyadAppPath, { recursive: true, force: true });
// Recreate the base directory
await fsPromises.mkdir(dyadAppPath, { recursive: true });
}
console.log("all app files removed.");
console.log("reset all complete.");
logger.log("all app files removed.");
logger.log("reset all complete.");
return { success: true, message: "Successfully reset everything" };
});

View File

@@ -12,6 +12,9 @@ import { processFullResponseActions } from "../processors/response_processor";
import { streamTestResponse } from "./testing_chat_handlers";
import { getTestResponse } from "./testing_chat_handlers";
import { getModelClient } from "../utils/get_model_client";
import log from "electron-log";
const logger = log.scope("chat_stream_handlers");
// Track active streams for cancellation
const activeStreams = new Map<number, AbortController>();
@@ -125,12 +128,12 @@ export function registerChatStreamHandlers() {
const appPath = getDyadAppPath(updatedChat.app.path);
try {
codebaseInfo = await extractCodebase(appPath);
console.log(`Extracted codebase information from ${appPath}`);
logger.log(`Extracted codebase information from ${appPath}`);
} catch (error) {
console.error("Error extracting codebase:", error);
logger.error("Error extracting codebase:", error);
}
}
console.log(
logger.log(
"codebaseInfo: length",
codebaseInfo.length,
"estimated tokens",
@@ -170,7 +173,7 @@ export function registerChatStreamHandlers() {
},
],
onError: (error) => {
console.error("Error streaming text:", error);
logger.error("Error streaming text:", error);
const message =
(error as any)?.error?.message || JSON.stringify(error);
event.sender.send(
@@ -204,7 +207,7 @@ export function registerChatStreamHandlers() {
// If the stream was aborted, exit early
if (abortController.signal.aborted) {
console.log(`Stream for chat ${req.chatId} was aborted`);
logger.log(`Stream for chat ${req.chatId} was aborted`);
break;
}
}
@@ -222,10 +225,10 @@ export function registerChatStreamHandlers() {
role: "assistant",
content: `${partialResponse}\n\n[Response cancelled by user]`,
});
console.log(`Saved partial response for chat ${chatId}`);
logger.log(`Saved partial response for chat ${chatId}`);
partialResponses.delete(chatId);
} catch (error) {
console.error(
logger.error(
`Error saving partial response for chat ${chatId}:`,
error
);
@@ -305,7 +308,7 @@ export function registerChatStreamHandlers() {
// Return the chat ID for backwards compatibility
return req.chatId;
} catch (error) {
console.error("[MAIN] API error:", error);
logger.error("Error calling LLM:", error);
event.sender.send(
"chat:response:error",
`Sorry, there was an error processing your request: ${error}`
@@ -324,9 +327,9 @@ export function registerChatStreamHandlers() {
// Abort the stream
abortController.abort();
activeStreams.delete(chatId);
console.log(`Aborted stream for chat ${chatId}`);
logger.log(`Aborted stream for chat ${chatId}`);
} else {
console.warn(`No active stream found for chat ${chatId}`);
logger.warn(`No active stream found for chat ${chatId}`);
}
// Send the end event to the renderer

View File

@@ -0,0 +1,94 @@
import { ipcMain, app } from "electron";
import { platform, arch } from "os";
import { SystemDebugInfo } from "../ipc_types";
import { readSettings } from "../../main/settings";
import { execSync } from "child_process";
import log from "electron-log";
import path from "path";
import fs from "fs";
import { runShellCommand } from "../utils/runShellCommand";
export function registerDebugHandlers() {
ipcMain.handle(
"get-system-debug-info",
async (): Promise<SystemDebugInfo> => {
console.log("IPC: get-system-debug-info called");
// Get Node.js and pnpm versions
let nodeVersion: string | null = null;
let pnpmVersion: string | null = null;
let nodePath: string | null = null;
try {
nodeVersion = await runShellCommand("node --version");
} catch (err) {
console.error("Failed to get Node.js version:", err);
}
try {
pnpmVersion = await runShellCommand("pnpm --version");
} catch (err) {
console.error("Failed to get pnpm version:", err);
}
try {
if (platform() === "win32") {
nodePath = await runShellCommand("where.exe node");
} else {
nodePath = await runShellCommand("which node");
}
} catch (err) {
console.error("Failed to get node path:", err);
}
// Get Dyad version from package.json
const packageJsonPath = path.resolve(
__dirname,
"..",
"..",
"package.json"
);
let dyadVersion = "unknown";
try {
const packageJson = JSON.parse(
fs.readFileSync(packageJsonPath, "utf8")
);
dyadVersion = packageJson.version;
} catch (err) {
console.error("Failed to read package.json:", err);
}
// Get telemetry info from settings
const settings = readSettings();
const telemetryId = settings.telemetryUserId || "unknown";
// Get logs from electron-log
let logs = "";
try {
const logPath = log.transports.file.getFile().path;
if (fs.existsSync(logPath)) {
const logContent = fs.readFileSync(logPath, "utf8");
const logLines = logContent.split("\n");
logs = logLines.slice(-100).join("\n");
}
} catch (err) {
console.error("Failed to read log file:", err);
logs = `Error reading logs: ${err}`;
}
return {
nodeVersion,
pnpmVersion,
nodePath,
telemetryId,
telemetryConsent: settings.telemetryConsent || "unknown",
telemetryUrl: "https://us.i.posthog.com", // Hardcoded from renderer.tsx
dyadVersion,
platform: process.platform,
architecture: arch(),
logs,
};
}
);
console.log("Registered debug IPC handlers");
}

View File

@@ -16,6 +16,9 @@ import { db } from "../../db";
import { apps } from "../../db/schema";
import { eq } from "drizzle-orm";
import { GithubUser } from "../../lib/schemas";
import log from "electron-log";
const logger = log.scope("github_handlers");
// --- GitHub Device Flow Constants ---
// TODO: Fetch this securely, e.g., from environment variables or a config file
@@ -65,7 +68,7 @@ export async function getGithubUser(): Promise<GithubUser | null> {
});
return { email };
} catch (err) {
console.error("[GitHub Handler] Failed to get GitHub username:", err);
logger.error("[GitHub Handler] Failed to get GitHub username:", err);
return null;
}
}
@@ -78,15 +81,13 @@ export async function getGithubUser(): Promise<GithubUser | null> {
async function pollForAccessToken(event: IpcMainInvokeEvent) {
if (!currentFlowState || !currentFlowState.isPolling) {
console.log("[GitHub Handler] Polling stopped or no active flow.");
logger.debug("[GitHub Handler] Polling stopped or no active flow.");
return;
}
const { deviceCode, interval } = currentFlowState;
console.log(
`[GitHub Handler] Polling for token with device code: ${deviceCode}`
);
logger.debug("[GitHub Handler] Polling for token with device code");
event.sender.send("github:flow-update", {
message: "Polling GitHub for authorization...",
});
@@ -108,10 +109,7 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) {
const data = await response.json();
if (response.ok && data.access_token) {
// --- SUCCESS ---
console.log(
"[GitHub Handler] Successfully obtained GitHub Access Token."
); // TODO: Store this token securely!
logger.log("Successfully obtained GitHub Access Token.");
event.sender.send("github:flow-success", {
message: "Successfully connected!",
});
@@ -120,13 +118,13 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) {
value: data.access_token,
},
});
// TODO: Associate token with appId if provided
stopPolling();
return;
} else if (data.error) {
switch (data.error) {
case "authorization_pending":
console.log("[GitHub Handler] Authorization pending...");
logger.debug("Authorization pending...");
event.sender.send("github:flow-update", {
message: "Waiting for user authorization...",
});
@@ -138,9 +136,7 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) {
break;
case "slow_down":
const newInterval = interval + 5;
console.log(
`[GitHub Handler] Slow down requested. New interval: ${newInterval}s`
);
logger.debug(`Slow down requested. New interval: ${newInterval}s`);
currentFlowState.interval = newInterval; // Update interval
event.sender.send("github:flow-update", {
message: `GitHub asked to slow down. Retrying in ${newInterval}s...`,
@@ -151,24 +147,22 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) {
);
break;
case "expired_token":
console.error("[GitHub Handler] Device code expired.");
logger.error("Device code expired.");
event.sender.send("github:flow-error", {
error: "Verification code expired. Please try again.",
});
stopPolling();
break;
case "access_denied":
console.error("[GitHub Handler] Access denied by user.");
logger.error("Access denied by user.");
event.sender.send("github:flow-error", {
error: "Authorization denied by user.",
});
stopPolling();
break;
default:
console.error(
`[GitHub Handler] Unknown GitHub error: ${
data.error_description || data.error
}`
logger.error(
`Unknown GitHub error: ${data.error_description || data.error}`
);
event.sender.send("github:flow-error", {
error: `GitHub authorization error: ${
@@ -182,10 +176,7 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) {
throw new Error(`Unknown response structure: ${JSON.stringify(data)}`);
}
} catch (error) {
console.error(
"[GitHub Handler] Error polling for GitHub access token:",
error
);
logger.error("Error polling for GitHub access token:", error);
event.sender.send("github:flow-error", {
error: `Network or unexpected error during polling: ${
error instanceof Error ? error.message : String(error)
@@ -202,12 +193,9 @@ function stopPolling() {
}
currentFlowState.isPolling = false;
currentFlowState.timeoutId = null;
// Maybe keep window reference for a bit if needed, or clear it
// currentFlowState.window = null;
console.log("[GitHub Handler] Polling stopped.");
logger.debug("[GitHub Handler] Polling stopped.");
}
// Setting to null signifies no active flow
// currentFlowState = null; // Decide if you want to clear immediately or allow potential restart
}
// --- IPC Handlers ---
@@ -216,15 +204,11 @@ function handleStartGithubFlow(
event: IpcMainInvokeEvent,
args: { appId: number | null }
) {
console.log(
`[GitHub Handler] Received github:start-flow for appId: ${args.appId}`
);
logger.debug(`Received github:start-flow for appId: ${args.appId}`);
// If a flow is already in progress, maybe cancel it or send an error
if (currentFlowState && currentFlowState.isPolling) {
console.warn(
"[GitHub Handler] Another GitHub flow is already in progress."
);
logger.warn("Another GitHub flow is already in progress.");
event.sender.send("github:flow-error", {
error: "Another connection process is already active.",
});
@@ -234,7 +218,7 @@ function handleStartGithubFlow(
// Store the window that initiated the request
const window = BrowserWindow.fromWebContents(event.sender);
if (!window) {
console.error("[GitHub Handler] Could not get BrowserWindow instance.");
logger.error("Could not get BrowserWindow instance.");
return;
}
@@ -272,7 +256,7 @@ function handleStartGithubFlow(
return res.json();
})
.then((data) => {
console.log("[GitHub Handler] Received device code response:", data);
logger.info("Received device code response");
if (!currentFlowState) return; // Flow might have been cancelled
currentFlowState.deviceCode = data.device_code;
@@ -293,10 +277,7 @@ function handleStartGithubFlow(
);
})
.catch((error) => {
console.error(
"[GitHub Handler] Error initiating GitHub device flow:",
error
);
logger.error("Error initiating GitHub device flow:", error);
event.sender.send("github:flow-error", {
error: `Failed to start GitHub connection: ${error.message}`,
});
@@ -305,15 +286,6 @@ function handleStartGithubFlow(
});
}
// Optional: Handle cancellation from renderer
// function handleCancelGithubFlow(event: IpcMainEvent) {
// console.log('[GitHub Handler] Received github:cancel-flow');
// stopPolling();
// currentFlowState = null; // Clear state on cancel
// // Optionally send confirmation back
// event.sender.send('github:flow-cancelled', { message: 'GitHub flow cancelled.' });
// }
// --- GitHub Repo Availability Handler ---
async function handleIsRepoAvailable(
event: IpcMainInvokeEvent,
@@ -453,7 +425,6 @@ async function handlePushToGithub(
// --- Registration ---
export function registerGithubHandlers() {
ipcMain.handle("github:start-flow", handleStartGithubFlow);
// ipcMain.on('github:cancel-flow', handleCancelGithubFlow); // Uncomment if you add cancellation
ipcMain.handle("github:is-repo-available", handleIsRepoAvailable);
ipcMain.handle("github:create-repo", handleCreateRepo);
ipcMain.handle("github:push", handlePushToGithub);

View File

@@ -1,51 +1,28 @@
import { ipcMain, app } from "electron";
import { exec, execSync, spawn } from "child_process";
import { exec, execSync } from "child_process";
import { platform, arch } from "os";
import { NodeSystemInfo } from "../ipc_types";
import fixPath from "fix-path";
import { runShellCommand } from "../utils/runShellCommand";
import log from "electron-log";
function checkCommandExists(command: string): Promise<string | null> {
return new Promise((resolve) => {
let output = "";
const process = spawn(command, {
shell: true,
stdio: ["ignore", "pipe", "pipe"], // ignore stdin, pipe stdout/stderr
});
process.stdout?.on("data", (data) => {
output += data.toString();
});
process.stderr?.on("data", (data) => {
// Log stderr but don't treat it as a failure unless the exit code is non-zero
console.warn(`Stderr from "${command}": ${data.toString().trim()}`);
});
process.on("error", (error) => {
console.error(`Error executing command "${command}":`, error.message);
resolve(null); // Command execution failed
});
process.on("close", (code) => {
if (code === 0) {
resolve(output.trim()); // Command succeeded, return trimmed output
} else {
console.error(`Command "${command}" failed with code ${code}`);
resolve(null); // Command failed
}
});
});
}
const logger = log.scope("node_handlers");
export function registerNodeHandlers() {
ipcMain.handle("nodejs-status", async (): Promise<NodeSystemInfo> => {
logger.log(
"handling ipc: nodejs-status for platform:",
platform(),
"and arch:",
arch()
);
// Run checks in parallel
const [nodeVersion, pnpmVersion] = await Promise.all([
checkCommandExists("node --version"),
runShellCommand("node --version"),
// First, check if pnpm is installed.
// If not, try to install it using corepack.
// If both fail, then pnpm is not available.
checkCommandExists(
runShellCommand(
"pnpm --version || (corepack enable pnpm && pnpm --version) || (npm install -g pnpm@latest-10 && pnpm --version)"
),
]);
@@ -66,7 +43,7 @@ export function registerNodeHandlers() {
});
ipcMain.handle("reload-env-path", async (): Promise<void> => {
console.debug("Reloading env path, previously:", process.env.PATH);
logger.debug("Reloading env path, previously:", process.env.PATH);
if (platform() === "win32") {
const newPath = execSync("cmd /c echo %PATH%", {
encoding: "utf8",
@@ -75,6 +52,6 @@ export function registerNodeHandlers() {
} else {
fixPath();
}
console.debug("Reloaded env path, now:", process.env.PATH);
logger.debug("Reloaded env path, now:", process.env.PATH);
});
}

View File

@@ -10,6 +10,9 @@ import {
getDyadWriteTags,
processFullResponseActions,
} from "../processors/response_processor";
import log from "electron-log";
const logger = log.scope("proposal_handlers");
// Placeholder Proposal data (can be removed or kept for reference)
// const placeholderProposal: Proposal = { ... };
@@ -33,7 +36,7 @@ const getProposalHandler = async (
_event: IpcMainInvokeEvent,
{ chatId }: { chatId: number }
): Promise<ProposalResult | null> => {
console.log(`IPC: get-proposal called for chatId: ${chatId}`);
logger.log(`IPC: get-proposal called for chatId: ${chatId}`);
try {
// Find the latest ASSISTANT message for the chat
@@ -56,7 +59,7 @@ const getProposalHandler = async (
if (latestAssistantMessage?.content && latestAssistantMessage.id) {
const messageId = latestAssistantMessage.id; // Get the message ID
console.log(
logger.log(
`Found latest assistant message (ID: ${messageId}), parsing content...`
);
const messageContent = latestAssistantMessage.content;
@@ -78,20 +81,25 @@ const getProposalHandler = async (
summary: tag.description ?? "(no change summary found)", // Generic summary
})),
};
console.log("Generated proposal on the fly:", proposal);
logger.log(
"Generated code proposal. title=",
proposal.title,
"files=",
proposal.filesChanged.length
);
return { proposal, chatId, messageId }; // Return proposal and messageId
} else {
console.log(
logger.log(
"No relevant tags found in the latest assistant message content."
);
return null; // No proposal could be generated
}
} else {
console.log(`No assistant message found for chatId: ${chatId}`);
logger.log(`No assistant message found for chatId: ${chatId}`);
return null; // No message found
}
} catch (error) {
console.error(`Error processing proposal for chatId ${chatId}:`, error);
logger.error(`Error processing proposal for chatId ${chatId}:`, error);
return null; // Indicate DB or processing error
}
};
@@ -101,7 +109,7 @@ const approveProposalHandler = async (
_event: IpcMainInvokeEvent,
{ chatId, messageId }: { chatId: number; messageId: number }
): Promise<{ success: boolean; error?: string }> => {
console.log(
logger.log(
`IPC: approve-proposal called for chatId: ${chatId}, messageId: ${messageId}`
);
@@ -119,7 +127,7 @@ const approveProposalHandler = async (
});
if (!messageToApprove?.content) {
console.error(
logger.error(
`Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`
);
return { success: false, error: "Assistant message not found." };
@@ -137,7 +145,7 @@ const approveProposalHandler = async (
);
if (processResult.error) {
console.error(
logger.error(
`Error processing actions for message ${messageId}:`,
processResult.error
);
@@ -151,10 +159,7 @@ const approveProposalHandler = async (
return { success: true };
} catch (error) {
console.error(
`Error approving proposal for messageId ${messageId}:`,
error
);
logger.error(`Error approving proposal for messageId ${messageId}:`, error);
return {
success: false,
error: (error as Error)?.message || "Unknown error",
@@ -167,7 +172,7 @@ const rejectProposalHandler = async (
_event: IpcMainInvokeEvent,
{ chatId, messageId }: { chatId: number; messageId: number }
): Promise<{ success: boolean; error?: string }> => {
console.log(
logger.log(
`IPC: reject-proposal called for chatId: ${chatId}, messageId: ${messageId}`
);
@@ -183,7 +188,7 @@ const rejectProposalHandler = async (
});
if (!messageToReject) {
console.error(
logger.error(
`Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`
);
return { success: false, error: "Assistant message not found." };
@@ -195,13 +200,10 @@ const rejectProposalHandler = async (
.set({ approvalState: "rejected" })
.where(eq(messages.id, messageId));
console.log(`Message ${messageId} marked as rejected.`);
logger.log(`Message ${messageId} marked as rejected.`);
return { success: true };
} catch (error) {
console.error(
`Error rejecting proposal for messageId ${messageId}:`,
error
);
logger.error(`Error rejecting proposal for messageId ${messageId}:`, error);
return {
success: false,
error: (error as Error)?.message || "Unknown error",
@@ -214,5 +216,4 @@ export function registerProposalHandlers() {
ipcMain.handle("get-proposal", getProposalHandler);
ipcMain.handle("approve-proposal", approveProposalHandler);
ipcMain.handle("reject-proposal", rejectProposalHandler);
console.log("Registered proposal IPC handlers (get, approve, reject)");
}

View File

@@ -1,4 +1,7 @@
import { ipcMain, shell } from "electron";
import log from "electron-log";
const logger = log.scope("shell_handlers");
export function registerShellHandlers() {
ipcMain.handle("open-external-url", async (_event, url: string) => {
@@ -6,15 +9,16 @@ export function registerShellHandlers() {
// Basic validation to ensure it's a http/https url
if (url && (url.startsWith("http://") || url.startsWith("https://"))) {
await shell.openExternal(url);
logger.debug("Opened external URL:", url);
return { success: true };
}
console.error("Attempted to open invalid or non-http URL:", url);
return {
success: false,
error: "Invalid URL provided. Only http/https URLs are allowed.",
};
logger.error("Attempted to open invalid or non-http URL:", url);
return {
success: false,
error: "Invalid URL provided. Only http/https URLs are allowed.",
};
} catch (error) {
console.error(`Failed to open external URL ${url}:`, error);
logger.error(`Failed to open external URL ${url}:`, error);
return { success: false, error: (error as Error).message };
}
});

View File

@@ -1,16 +0,0 @@
import type { IpcMainInvokeEvent } from "electron";
import { shell } from "electron";
export async function handleShellOpenExternal(
_event: IpcMainInvokeEvent,
url: string
): Promise<void> {
// Basic validation to ensure it's likely a URL
if (url && (url.startsWith("http://") || url.startsWith("https://"))) {
await shell.openExternal(url);
} else {
console.error(`Invalid URL attempt blocked: ${url}`);
// Optionally, you could throw an error back to the renderer
// throw new Error("Invalid or insecure URL provided.");
}
}

View File

@@ -16,6 +16,7 @@ import type {
NodeSystemInfo,
Message,
Version,
SystemDebugInfo,
} from "./ipc_types";
import type { CodeProposal, ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast";
@@ -670,4 +671,15 @@ export class IpcClient {
}
}
// --- End Proposal Management ---
// Get system debug information
public async getSystemDebugInfo(): Promise<SystemDebugInfo> {
try {
const result = await this.ipcRenderer.invoke("get-system-debug-info");
return result;
} catch (error) {
showError(error);
throw error;
}
}
}

View File

@@ -7,6 +7,7 @@ import { registerDependencyHandlers } from "./handlers/dependency_handlers";
import { registerGithubHandlers } from "./handlers/github_handlers";
import { registerNodeHandlers } from "./handlers/node_handlers";
import { registerProposalHandlers } from "./handlers/proposal_handlers";
import { registerDebugHandlers } from "./handlers/debug_handlers";
export function registerIpcHandlers() {
// Register all IPC handlers by category
@@ -19,4 +20,5 @@ export function registerIpcHandlers() {
registerGithubHandlers();
registerNodeHandlers();
registerProposalHandlers();
registerDebugHandlers();
}

View File

@@ -76,3 +76,16 @@ export interface NodeSystemInfo {
pnpmVersion: string | null;
nodeDownloadUrl: string;
}
export interface SystemDebugInfo {
nodeVersion: string | null;
pnpmVersion: string | null;
nodePath: string | null;
telemetryId: string;
telemetryConsent: string;
telemetryUrl: string;
dyadVersion: string;
platform: string;
architecture: string;
logs: string;
}

View File

@@ -0,0 +1,41 @@
import { spawn } from "child_process";
import log from "electron-log";
const logger = log.scope("runShellCommand");
export function runShellCommand(command: string): Promise<string | null> {
logger.log(`Running command: ${command}`);
return new Promise((resolve) => {
let output = "";
const process = spawn(command, {
shell: true,
stdio: ["ignore", "pipe", "pipe"], // ignore stdin, pipe stdout/stderr
});
process.stdout?.on("data", (data) => {
output += data.toString();
});
process.stderr?.on("data", (data) => {
// Log stderr but don't treat it as a failure unless the exit code is non-zero
logger.warn(`Stderr from "${command}": ${data.toString().trim()}`);
});
process.on("error", (error) => {
logger.error(`Error executing command "${command}":`, error.message);
resolve(null); // Command execution failed
});
process.on("close", (code) => {
if (code === 0) {
logger.debug(
`Command "${command}" succeeded with code ${code}: ${output.trim()}`
);
resolve(output.trim()); // Command succeeded, return trimmed output
} else {
logger.error(`Command "${command}" failed with code ${code}`);
resolve(null); // Command failed
}
});
});
}

View File

@@ -7,11 +7,9 @@ import started from "electron-squirrel-startup";
import { updateElectronApp } from "update-electron-app";
import log from "electron-log";
console.log = log.log;
console.error = log.error;
console.warn = log.warn;
console.info = log.info;
console.debug = log.debug;
log.errorHandler.startCatching();
log.eventLogger.startLogging();
log.log("HELLO WORLD");
updateElectronApp(); // additional configuration options available

View File

@@ -41,6 +41,7 @@ const validInvokeChannels = [
"get-proposal",
"approve-proposal",
"reject-proposal",
"get-system-debug-info",
] as const;
// Add valid receive channels