diff --git a/src/components/ChatList.tsx b/src/components/ChatList.tsx index 703d39f..bc5e904 100644 --- a/src/components/ChatList.tsx +++ b/src/components/ChatList.tsx @@ -92,11 +92,7 @@ export function ChatList({ show }: { show?: boolean }) { const handleDeleteChat = async (chatId: number) => { try { - const result = await IpcClient.getInstance().deleteChat(chatId); - if (!result.success) { - showError("Failed to delete chat"); - return; - } + await IpcClient.getInstance().deleteChat(chatId); showSuccess("Chat deleted successfully"); // If the deleted chat was selected, navigate to home diff --git a/src/components/GitHubConnector.tsx b/src/components/GitHubConnector.tsx index 4e21181..14266ce 100644 --- a/src/components/GitHubConnector.tsx +++ b/src/components/GitHubConnector.tsx @@ -153,18 +153,14 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { setIsCreatingRepo(true); setCreateRepoSuccess(false); try { - const result = await IpcClient.getInstance().createGithubRepo( + await IpcClient.getInstance().createGithubRepo( githubOrg, repoName, appId!, ); - if (result.success) { - setCreateRepoSuccess(true); - setRepoCheckError(null); - refreshApp(); - } else { - setCreateRepoError(result.error || "Failed to create repository."); - } + setCreateRepoSuccess(true); + setRepoCheckError(null); + refreshApp(); } catch (err: any) { setCreateRepoError(err.message || "Failed to create repository."); } finally { @@ -180,12 +176,8 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { setIsDisconnecting(true); setDisconnectError(null); try { - const result = await IpcClient.getInstance().disconnectGithubRepo(appId); - if (result.success) { - refreshApp(); - } else { - setDisconnectError(result.error || "Failed to disconnect repository."); - } + await IpcClient.getInstance().disconnectGithubRepo(appId); + refreshApp(); } catch (err: any) { setDisconnectError(err.message || "Failed to disconnect repository."); } finally { diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 7848eed..96758d3 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -171,17 +171,12 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} const { uploadUrl, filename } = await response.json(); - // Upload to the signed URL using IPC - const uploadResult = await IpcClient.getInstance().uploadToSignedUrl( + await IpcClient.getInstance().uploadToSignedUrl( uploadUrl, "application/json", chatLogsJson, ); - if (!uploadResult.success) { - throw new Error(`Failed to upload logs: ${uploadResult.error}`); - } - // Extract session ID (filename without extension) const sessionId = filename.replace(".json", ""); setSessionId(sessionId); diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 043b35d..0953dc3 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -58,7 +58,7 @@ import { useVersions } from "@/hooks/useVersions"; import { useAttachments } from "@/hooks/useAttachments"; import { AttachmentsList } from "./AttachmentsList"; import { DragDropOverlay } from "./DragDropOverlay"; -import { showUncommittedFilesWarning } from "@/lib/toast"; +import { showError, showUncommittedFilesWarning } from "@/lib/toast"; const showTokenBarAtom = atom(false); export function ChatInput({ chatId }: { chatId?: number }) { @@ -182,13 +182,6 @@ export function ChatInput({ chatId }: { chatId?: number }) { chatId, messageId, }); - if (result.success) { - console.log("Proposal approved successfully"); - // TODO: Maybe refresh proposal state or show confirmation? - } else { - console.error("Failed to approve proposal:", result.error); - setError(result.error || "Failed to approve proposal"); - } if (result.uncommittedFiles) { showUncommittedFilesWarning(result.uncommittedFiles); } @@ -215,17 +208,10 @@ export function ChatInput({ chatId }: { chatId?: number }) { setIsRejecting(true); posthog.capture("chat:reject"); try { - const result = await IpcClient.getInstance().rejectProposal({ + await IpcClient.getInstance().rejectProposal({ chatId, messageId, }); - if (result.success) { - console.log("Proposal rejected successfully"); - // TODO: Maybe refresh proposal state or show confirmation? - } else { - console.error("Failed to reject proposal:", result.error); - setError(result.error || "Failed to reject proposal"); - } } catch (err) { console.error("Error rejecting proposal:", err); setError((err as Error)?.message || "An error occurred while rejecting"); @@ -389,13 +375,17 @@ function SummarizeInNewChatButton() { console.error("No app id found"); return; } - const newChatId = await IpcClient.getInstance().createChat(appId); - // navigate to new chat - await navigate({ to: "/chat", search: { id: newChatId } }); - await streamMessage({ - prompt: "Summarize from chat-id=" + chatId, - chatId: newChatId, - }); + try { + const newChatId = await IpcClient.getInstance().createChat(appId); + // navigate to new chat + await navigate({ to: "/chat", search: { id: newChatId } }); + await streamMessage({ + prompt: "Summarize from chat-id=" + chatId, + chatId: newChatId, + }); + } catch (err) { + showError(err); + } }; return ( diff --git a/src/components/chat/MessagesList.tsx b/src/components/chat/MessagesList.tsx index a34eed7..474bdcf 100644 --- a/src/components/chat/MessagesList.tsx +++ b/src/components/chat/MessagesList.tsx @@ -89,14 +89,13 @@ export const MessagesList = forwardRef( await revertVersion({ versionId: chat.initialCommitHash, }); - const result = + try { await IpcClient.getInstance().deleteMessages( selectedChatId, ); - if (result.success) { setMessages([]); - } else { - showError(result.error); + } catch (err) { + showError(err); } } else { showWarning( diff --git a/src/components/preview_panel/FileEditor.tsx b/src/components/preview_panel/FileEditor.tsx index a48c2ae..296d376 100644 --- a/src/components/preview_panel/FileEditor.tsx +++ b/src/components/preview_panel/FileEditor.tsx @@ -6,6 +6,7 @@ import { ChevronRight, Circle } from "lucide-react"; import "@/components/chat/monaco"; import { IpcClient } from "@/ipc/ipc_client"; import { useSettings } from "@/hooks/useSettings"; +import { showError } from "@/lib/toast"; interface FileEditorProps { appId: number | null; @@ -132,8 +133,7 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => { needsSaveRef.current = false; setDisplayUnsavedChanges(false); } catch (error) { - console.error("Error saving file:", error); - // Could add error notification here + showError(error); } finally { isSavingRef.current = false; } diff --git a/src/hooks/useSupabase.ts b/src/hooks/useSupabase.ts index 76cad29..f00dbcc 100644 --- a/src/hooks/useSupabase.ts +++ b/src/hooks/useSupabase.ts @@ -42,14 +42,8 @@ export function useSupabase() { async (projectId: string, appId: number) => { setLoading(true); try { - const result = await ipcClient.setSupabaseAppProject(projectId, appId); - - if (result.success) { - setError(null); - return result; - } else { - throw new Error("Failed to set project for app"); - } + await ipcClient.setSupabaseAppProject(projectId, appId); + setError(null); } catch (error) { console.error("Error setting Supabase project for app:", error); setError(error instanceof Error ? error : new Error(String(error))); @@ -68,14 +62,8 @@ export function useSupabase() { async (appId: number) => { setLoading(true); try { - const result = await ipcClient.unsetSupabaseAppProject(appId); - - if (result.success) { - setError(null); - return result; - } else { - throw new Error("Failed to unset project for app"); - } + await ipcClient.unsetSupabaseAppProject(appId); + setError(null); } catch (error) { console.error("Error unsetting Supabase project for app:", error); setError(error instanceof Error ? error : new Error(String(error))); diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts index 91ed247..92cf0f7 100644 --- a/src/ipc/handlers/app_handlers.ts +++ b/src/ipc/handlers/app_handlers.ts @@ -32,8 +32,10 @@ import killPort from "kill-port"; import util from "util"; import log from "electron-log"; import { getSupabaseProjectName } from "../../supabase_admin/supabase_management_client"; +import { createLoggedHandler } from "./safe_handle"; const logger = log.scope("app_handlers"); +const handle = createLoggedHandler(logger); // Needed, otherwise electron in MacOS/Linux will not be able // to find node/pnpm. @@ -137,75 +139,80 @@ async function killProcessOnPort(port: number): Promise { } export function registerAppHandlers() { - ipcMain.handle("create-app", async (_, params: CreateAppParams) => { - const appPath = params.name; - const fullAppPath = getDyadAppPath(appPath); - if (fs.existsSync(fullAppPath)) { - throw new Error(`App already exists at: ${fullAppPath}`); - } - // Create a new app - const [app] = await db - .insert(apps) - .values({ - name: params.name, - // Use the name as the path for now - path: appPath, - }) - .returning(); - - // Create an initial chat for this app - const [chat] = await db - .insert(chats) - .values({ - appId: app.id, - }) - .returning(); - - // Start async operations in background - try { - // Copy scaffold asynchronously - await copyDirectoryRecursive( - path.join(__dirname, "..", "..", "scaffold"), - fullAppPath, - ); - // Initialize git repo and create first commit - await git.init({ - fs: fs, - dir: fullAppPath, - defaultBranch: "main", - }); - - // Stage all files - await git.add({ - fs: fs, - dir: fullAppPath, - filepath: ".", - }); - - // Create initial commit - const commitHash = await git.commit({ - fs: fs, - dir: fullAppPath, - message: "Init from react vite template", - author: await getGitAuthor(), - }); - - // Update chat with initial commit hash - await db - .update(chats) - .set({ - initialCommitHash: commitHash, + handle( + "create-app", + async ( + _, + params: CreateAppParams, + ): Promise<{ app: any; chatId: number }> => { + const appPath = params.name; + const fullAppPath = getDyadAppPath(appPath); + if (fs.existsSync(fullAppPath)) { + throw new Error(`App already exists at: ${fullAppPath}`); + } + // Create a new app + const [app] = await db + .insert(apps) + .values({ + name: params.name, + // Use the name as the path for now + path: appPath, }) - .where(eq(chats.id, chat.id)); - } catch (error) { - logger.error("Error in background app initialization:", error); - } - // })(); + .returning(); - return { app, chatId: chat.id }; - }); + // Create an initial chat for this app + const [chat] = await db + .insert(chats) + .values({ + appId: app.id, + }) + .returning(); - ipcMain.handle("get-app", async (_, appId: number): Promise => { + // Start async operations in background + try { + // Copy scaffold asynchronously + await copyDirectoryRecursive( + path.join(__dirname, "..", "..", "scaffold"), + fullAppPath, + ); + // Initialize git repo and create first commit + await git.init({ + fs: fs, + dir: fullAppPath, + defaultBranch: "main", + }); + + // Stage all files + await git.add({ + fs: fs, + dir: fullAppPath, + filepath: ".", + }); + + // Create initial commit + const commitHash = await git.commit({ + fs: fs, + dir: fullAppPath, + message: "Init from react vite template", + author: await getGitAuthor(), + }); + + // Update chat with initial commit hash + await db + .update(chats) + .set({ + initialCommitHash: commitHash, + }) + .where(eq(chats.id, chat.id)); + } catch (error) { + logger.error("Error in background app initialization:", error); + } + + return { app, chatId: chat.id }; + }, + ); + + handle("get-app", async (_, appId: number): Promise => { const app = await db.query.apps.findFirst({ where: eq(apps.id, appId), }); @@ -281,6 +288,7 @@ export function registerAppHandlers() { }, ); + // Do NOT use handle for this, it contains sensitive information. ipcMain.handle("get-env-vars", async () => { const envVars: Record = {}; for (const key of ALLOWED_ENV_VARS) { @@ -294,13 +302,12 @@ export function registerAppHandlers() { async ( event: Electron.IpcMainInvokeEvent, { appId }: { appId: number }, - ) => { + ): Promise => { return withLock(appId, async () => { // Check if app is already running if (runningApps.has(appId)) { logger.debug(`App ${appId} is already running.`); - // Potentially return the existing process info or confirm status - return { success: true, message: "App already running." }; + return; } const app = await db.query.apps.findFirst({ @@ -315,9 +322,9 @@ export function registerAppHandlers() { const appPath = getDyadAppPath(app.path); try { - const currentProcessId = await executeApp({ appPath, appId, event }); + await executeApp({ appPath, appId, event }); - return { success: true, processId: currentProcessId }; + return; } catch (error: any) { logger.error(`Error running app ${appId}:`, error); // Ensure cleanup if error happens during setup but before process events are handled @@ -333,56 +340,56 @@ export function registerAppHandlers() { }, ); - ipcMain.handle("stop-app", async (_, { appId }: { appId: number }) => { - logger.log( - `Attempting to stop app ${appId}. Current running apps: ${runningApps.size}`, - ); - return withLock(appId, async () => { - const appInfo = runningApps.get(appId); - - if (!appInfo) { - logger.log( - `App ${appId} not found in running apps map. Assuming already stopped.`, - ); - return { - success: true, - message: "App not running.", - }; - } - - const { process, processId } = appInfo; + ipcMain.handle( + "stop-app", + async (_, { appId }: { appId: number }): Promise => { logger.log( - `Found running app ${appId} with processId ${processId} (PID: ${process.pid}). Attempting to stop.`, + `Attempting to stop app ${appId}. Current running apps: ${runningApps.size}`, ); + return withLock(appId, async () => { + const appInfo = runningApps.get(appId); - // Check if the process is already exited or closed - if (process.exitCode !== null || process.signalCode !== null) { + if (!appInfo) { + logger.log( + `App ${appId} not found in running apps map. Assuming already stopped.`, + ); + return; + } + + const { process, processId } = appInfo; logger.log( - `Process for app ${appId} (PID: ${process.pid}) already exited (code: ${process.exitCode}, signal: ${process.signalCode}). Cleaning up map.`, + `Found running app ${appId} with processId ${processId} (PID: ${process.pid}). Attempting to stop.`, ); - runningApps.delete(appId); // Ensure cleanup if somehow missed - return { success: true, message: "Process already exited." }; - } - try { - // Use the killProcess utility to stop the process - await killProcess(process); + // Check if the process is already exited or closed + if (process.exitCode !== null || process.signalCode !== null) { + 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 + return; + } - // Now, safely remove the app from the map *after* confirming closure - removeAppIfCurrentProcess(appId, process); + try { + // Use the killProcess utility to stop the process + await killProcess(process); - return { success: true }; - } catch (error: any) { - logger.error( - `Error stopping app ${appId} (PID: ${process.pid}, processId: ${processId}):`, - error, - ); - // Attempt cleanup even if an error occurred during the stop process - removeAppIfCurrentProcess(appId, process); - throw new Error(`Failed to stop app ${appId}: ${error.message}`); - } - }); - }); + // Now, safely remove the app from the map *after* confirming closure + removeAppIfCurrentProcess(appId, process); + + return; + } catch (error: any) { + logger.error( + `Error stopping app ${appId} (PID: ${process.pid}, processId: ${processId}):`, + error, + ); + // Attempt cleanup even if an error occurred during the stop process + removeAppIfCurrentProcess(appId, process); + throw new Error(`Failed to stop app ${appId}: ${error.message}`); + } + }); + }, + ); ipcMain.handle( "restart-app", @@ -392,7 +399,7 @@ export function registerAppHandlers() { appId, removeNodeModules, }: { appId: number; removeNodeModules?: boolean }, - ) => { + ): Promise => { logger.log(`Restarting app ${appId}`); return withLock(appId, async () => { try { @@ -447,7 +454,7 @@ export function registerAppHandlers() { await executeApp({ appPath, appId, event }); // This will handle starting either mode - return { success: true }; + return; } catch (error) { logger.error(`Error restarting app ${appId}:`, error); throw error; @@ -465,7 +472,7 @@ export function registerAppHandlers() { filePath, content, }: { appId: number; filePath: string; content: string }, - ) => { + ): Promise => { const app = await db.query.apps.findFirst({ where: eq(apps.id, appId), }); @@ -505,7 +512,7 @@ export function registerAppHandlers() { }); } - return { success: true }; + return; } catch (error: any) { logger.error(`Error writing file ${filePath} for app ${appId}:`, error); throw new Error(`Failed to write file: ${error.message}`); @@ -513,52 +520,57 @@ export function registerAppHandlers() { }, ); - ipcMain.handle("delete-app", async (_, { appId }: { appId: number }) => { - // Static server worker is NOT terminated here anymore + ipcMain.handle( + "delete-app", + async (_, { appId }: { appId: number }): Promise => { + // Static server worker is NOT terminated here anymore - return withLock(appId, async () => { - // Check if app exists - const app = await db.query.apps.findFirst({ - where: eq(apps.id, appId), - }); + return withLock(appId, async () => { + // Check if app exists + const app = await db.query.apps.findFirst({ + where: eq(apps.id, appId), + }); - if (!app) { - throw new Error("App not found"); - } - - // Stop the app if it's running - if (runningApps.has(appId)) { - const appInfo = runningApps.get(appId)!; - try { - logger.log(`Stopping app ${appId} before deletion.`); // Adjusted log - await killProcess(appInfo.process); - runningApps.delete(appId); - } catch (error: any) { - logger.error(`Error stopping app ${appId} before deletion:`, error); // Adjusted log - // Continue with deletion even if stopping fails + if (!app) { + throw new Error("App not found"); } - } - // Delete app files - const appPath = getDyadAppPath(app.path); - try { - await fsPromises.rm(appPath, { recursive: true, force: true }); - } catch (error: any) { - logger.error(`Error deleting app files for app ${appId}:`, error); - throw new Error(`Failed to delete app files: ${error.message}`); - } + // Stop the app if it's running + if (runningApps.has(appId)) { + const appInfo = runningApps.get(appId)!; + try { + logger.log(`Stopping app ${appId} before deletion.`); // Adjusted log + await killProcess(appInfo.process); + runningApps.delete(appId); + } catch (error: any) { + logger.error(`Error stopping app ${appId} before deletion:`, error); // Adjusted log + // Continue with deletion even if stopping fails + } + } - // Delete app from database - try { - await db.delete(apps).where(eq(apps.id, appId)); - // Note: Associated chats will cascade delete if that's set up in the schema - return { success: true }; - } catch (error: any) { - logger.error(`Error deleting app ${appId} from database:`, error); - throw new Error(`Failed to delete app from database: ${error.message}`); - } - }); - }); + // Delete app files + const appPath = getDyadAppPath(app.path); + try { + await fsPromises.rm(appPath, { recursive: true, force: true }); + } catch (error: any) { + logger.error(`Error deleting app files for app ${appId}:`, error); + throw new Error(`Failed to delete app files: ${error.message}`); + } + + // Delete app from database + try { + await db.delete(apps).where(eq(apps.id, appId)); + // Note: Associated chats will cascade delete if that's set up in the schema + return; + } catch (error: any) { + logger.error(`Error deleting app ${appId} from database:`, error); + throw new Error( + `Failed to delete app from database: ${error.message}`, + ); + } + }); + }, + ); ipcMain.handle( "rename-app", @@ -569,7 +581,7 @@ export function registerAppHandlers() { appName, appPath, }: { appId: number; appName: string; appPath: string }, - ) => { + ): Promise => { return withLock(appId, async () => { // Check if app exists const app = await db.query.apps.findFirst({ @@ -642,7 +654,7 @@ export function registerAppHandlers() { // Update app in database try { - const [updatedApp] = await db + await db .update(apps) .set({ name: appName, @@ -651,7 +663,7 @@ export function registerAppHandlers() { .where(eq(apps.id, appId)) .returning(); - return { success: true, app: updatedApp }; + return; } catch (error: any) { // Attempt to rollback the file move if (newAppPath !== oldAppPath) { @@ -672,7 +684,7 @@ export function registerAppHandlers() { }, ); - ipcMain.handle("reset-all", async () => { + ipcMain.handle("reset-all", async (): Promise => { logger.log("start: resetting all apps and settings."); // Stop all running apps first logger.log("stopping all running apps..."); @@ -722,10 +734,9 @@ export function registerAppHandlers() { } logger.log("all app files removed."); logger.log("reset all complete."); - return { success: true, message: "Successfully reset everything" }; }); - ipcMain.handle("get-app-version", async () => { + ipcMain.handle("get-app-version", async (): Promise<{ version: string }> => { // Read version from package.json at project root const packageJsonPath = path.resolve(__dirname, "..", "..", "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); diff --git a/src/ipc/handlers/chat_handlers.ts b/src/ipc/handlers/chat_handlers.ts index f4ed7b8..8b81bcc 100644 --- a/src/ipc/handlers/chat_handlers.ts +++ b/src/ipc/handlers/chat_handlers.ts @@ -5,14 +5,16 @@ import { desc, eq } from "drizzle-orm"; import type { ChatSummary } from "../../lib/schemas"; import * as git from "isomorphic-git"; import * as fs from "fs"; +import { createLoggedHandler } from "./safe_handle"; import log from "electron-log"; import { getDyadAppPath } from "../../paths/paths"; const logger = log.scope("chat_handlers"); +const handle = createLoggedHandler(logger); export function registerChatHandlers() { - ipcMain.handle("create-chat", async (_, appId: number) => { + handle("create-chat", async (_, appId: number): Promise => { // Get the app's path first const app = await db.query.apps.findFirst({ where: eq(apps.id, appId), @@ -74,54 +76,38 @@ export function registerChatHandlers() { return chat; }); - ipcMain.handle( - "get-chats", - async (_, appId?: number): Promise => { - // If appId is provided, filter chats for that app - const query = appId - ? db.query.chats.findMany({ - where: eq(chats.appId, appId), - columns: { - id: true, - title: true, - createdAt: true, - appId: true, - }, - orderBy: [desc(chats.createdAt)], - }) - : db.query.chats.findMany({ - columns: { - id: true, - title: true, - createdAt: true, - appId: true, - }, - orderBy: [desc(chats.createdAt)], - }); + handle("get-chats", async (_, appId?: number): Promise => { + // If appId is provided, filter chats for that app + const query = appId + ? db.query.chats.findMany({ + where: eq(chats.appId, appId), + columns: { + id: true, + title: true, + createdAt: true, + appId: true, + }, + orderBy: [desc(chats.createdAt)], + }) + : db.query.chats.findMany({ + columns: { + id: true, + title: true, + createdAt: true, + appId: true, + }, + orderBy: [desc(chats.createdAt)], + }); - const allChats = await query; - return allChats; - }, - ); - - ipcMain.handle("delete-chat", async (_, chatId: number) => { - try { - // Delete the chat and its associated messages - await db.delete(chats).where(eq(chats.id, chatId)); - return { success: true }; - } catch (error) { - logger.error("Error deleting chat:", error); - return { success: false, error: (error as Error).message }; - } + const allChats = await query; + return allChats; }); - ipcMain.handle("delete-messages", async (_, chatId: number) => { - try { - await db.delete(messages).where(eq(messages.chatId, chatId)); - return { success: true }; - } catch (error) { - logger.error("Error deleting messages:", error); - return { success: false, error: (error as Error).message }; - } + handle("delete-chat", async (_, chatId: number): Promise => { + await db.delete(chats).where(eq(chats.id, chatId)); + }); + + handle("delete-messages", async (_, chatId: number): Promise => { + await db.delete(messages).where(eq(messages.chatId, chatId)); }); } diff --git a/src/ipc/handlers/dependency_handlers.ts b/src/ipc/handlers/dependency_handlers.ts index 0703079..1278869 100644 --- a/src/ipc/handlers/dependency_handlers.ts +++ b/src/ipc/handlers/dependency_handlers.ts @@ -1,17 +1,21 @@ -import { ipcMain } from "electron"; import { db } from "../../db"; import { messages, apps, chats } from "../../db/schema"; import { eq } from "drizzle-orm"; import { getDyadAppPath } from "../../paths/paths"; import { executeAddDependency } from "../processors/executeAddDependency"; +import { createLoggedHandler } from "./safe_handle"; +import log from "electron-log"; + +const logger = log.scope("dependency_handlers"); +const handle = createLoggedHandler(logger); export function registerDependencyHandlers() { - ipcMain.handle( + handle( "chat:add-dep", async ( _event, { chatId, packages }: { chatId: number; packages: string[] }, - ) => { + ): Promise => { // Find the message from the database const foundMessages = await db.query.messages.findMany({ where: eq(messages.chatId, chatId), diff --git a/src/ipc/handlers/github_handlers.ts b/src/ipc/handlers/github_handlers.ts index 8e30f2f..083f468 100644 --- a/src/ipc/handlers/github_handlers.ts +++ b/src/ipc/handlers/github_handlers.ts @@ -285,7 +285,7 @@ function handleStartGithubFlow( async function handleIsRepoAvailable( event: IpcMainInvokeEvent, { org, repo }: { org: string; repo: string }, -) { +): Promise<{ available: boolean; error?: string }> { try { // Get access token from settings const settings = readSettings(); @@ -323,49 +323,44 @@ async function handleIsRepoAvailable( async function handleCreateRepo( event: IpcMainInvokeEvent, { org, repo, appId }: { org: string; repo: string; appId: number }, -) { - try { - // Get access token from settings - const settings = readSettings(); - const accessToken = settings.githubAccessToken?.value; - if (!accessToken) { - return { success: false, error: "Not authenticated with GitHub." }; - } - // If org is empty, create for the authenticated user - let owner = org; - if (!owner) { - const userRes = await fetch("https://api.github.com/user", { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - const user = await userRes.json(); - owner = user.login; - } - // Create repo - const createUrl = org - ? `https://api.github.com/orgs/${owner}/repos` - : `https://api.github.com/user/repos`; - const res = await fetch(createUrl, { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - name: repo, - private: true, - }), - }); - if (!res.ok) { - const data = await res.json(); - return { success: false, error: data.message || "Failed to create repo" }; - } - // Store org and repo in the app's DB row (apps table) - await updateAppGithubRepo(appId, owner, repo); - return { success: true }; - } catch (err: any) { - return { success: false, error: err.message || "Unknown error" }; +): Promise { + // Get access token from settings + const settings = readSettings(); + const accessToken = settings.githubAccessToken?.value; + if (!accessToken) { + throw new Error("Not authenticated with GitHub."); } + // If org is empty, create for the authenticated user + let owner = org; + if (!owner) { + const userRes = await fetch("https://api.github.com/user", { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + const user = await userRes.json(); + owner = user.login; + } + // Create repo + const createUrl = org + ? `https://api.github.com/orgs/${owner}/repos` + : `https://api.github.com/user/repos`; + const res = await fetch(createUrl, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/vnd.github+json", + }, + body: JSON.stringify({ + name: repo, + private: true, + }), + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.message || "Failed to create repo"); + } + // Store org and repo in the app's DB row (apps table) + await updateAppGithubRepo(appId, owner, repo); } // --- GitHub Push Handler --- @@ -420,36 +415,26 @@ async function handlePushToGithub( async function handleDisconnectGithubRepo( event: IpcMainInvokeEvent, { appId }: { appId: number }, -) { - try { - logger.log(`Disconnecting GitHub repo for appId: ${appId}`); +): Promise { + logger.log(`Disconnecting GitHub repo for appId: ${appId}`); - // Get the app from the database - const app = await db.query.apps.findFirst({ - where: eq(apps.id, appId), - }); + // Get the app from the database + const app = await db.query.apps.findFirst({ + where: eq(apps.id, appId), + }); - if (!app) { - return { success: false, error: "App not found" }; - } - - // Update app in database to remove GitHub repo and org - await db - .update(apps) - .set({ - githubRepo: null, - githubOrg: null, - }) - .where(eq(apps.id, appId)); - - return { success: true }; - } catch (error) { - logger.error(`Error disconnecting GitHub repo: ${error}`); - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + if (!app) { + throw new Error("App not found"); } + + // Update app in database to remove GitHub repo and org + await db + .update(apps) + .set({ + githubRepo: null, + githubOrg: null, + }) + .where(eq(apps.id, appId)); } // --- Registration --- diff --git a/src/ipc/handlers/local_model_lmstudio_handler.ts b/src/ipc/handlers/local_model_lmstudio_handler.ts index 056c876..67b8dc8 100644 --- a/src/ipc/handlers/local_model_lmstudio_handler.ts +++ b/src/ipc/handlers/local_model_lmstudio_handler.ts @@ -18,28 +18,24 @@ export interface LMStudioModel { } export async function fetchLMStudioModels(): Promise { - try { - const modelsResponse: Response = await fetch( - "http://localhost:1234/api/v0/models", - ); - if (!modelsResponse.ok) { - throw new Error("Failed to fetch models from LM Studio"); - } - const modelsJson = await modelsResponse.json(); - const downloadedModels = modelsJson.data as LMStudioModel[]; - const models: LocalModel[] = downloadedModels - .filter((model: any) => model.type === "llm") - .map((model: any) => ({ - modelName: model.id, - displayName: model.id, - provider: "lmstudio", - })); - - logger.info(`Successfully fetched ${models.length} models from LM Studio`); - return { models, error: null }; - } catch { - return { models: [], error: "Failed to fetch models from LM Studio" }; + const modelsResponse: Response = await fetch( + "http://localhost:1234/api/v0/models", + ); + if (!modelsResponse.ok) { + throw new Error("Failed to fetch models from LM Studio"); } + const modelsJson = await modelsResponse.json(); + const downloadedModels = modelsJson.data as LMStudioModel[]; + const models: LocalModel[] = downloadedModels + .filter((model: any) => model.type === "llm") + .map((model: any) => ({ + modelName: model.id, + displayName: model.id, + provider: "lmstudio", + })); + + logger.info(`Successfully fetched ${models.length} models from LM Studio`); + return { models }; } export function registerLMStudioHandlers() { diff --git a/src/ipc/handlers/local_model_ollama_handler.ts b/src/ipc/handlers/local_model_ollama_handler.ts index d409d0a..8b9c9cf 100644 --- a/src/ipc/handlers/local_model_ollama_handler.ts +++ b/src/ipc/handlers/local_model_ollama_handler.ts @@ -47,20 +47,17 @@ export async function fetchOllamaModels(): Promise { }; }); logger.info(`Successfully fetched ${models.length} models from Ollama`); - return { models, error: null }; + return { models }; } catch (error) { if ( error instanceof TypeError && (error as Error).message.includes("fetch failed") ) { - logger.error("Could not connect to Ollama"); - return { - models: [], - error: - "Could not connect to Ollama. Make sure it's running at http://localhost:11434", - }; + throw new Error( + "Could not connect to Ollama. Make sure it's running at http://localhost:11434", + ); } - return { models: [], error: "Failed to fetch models from Ollama" }; + throw new Error("Failed to fetch models from Ollama"); } } diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts index 25e1e94..6e44a60 100644 --- a/src/ipc/handlers/proposal_handlers.ts +++ b/src/ipc/handlers/proposal_handlers.ts @@ -1,4 +1,4 @@ -import { ipcMain, type IpcMainInvokeEvent } from "electron"; +import { type IpcMainInvokeEvent } from "electron"; import type { CodeProposal, ProposalResult, @@ -29,9 +29,9 @@ import { import { extractCodebase } from "../../utils/codebase"; import { getDyadAppPath } from "../../paths/paths"; import { withLock } from "../utils/lock_utils"; - +import { createLoggedHandler } from "./safe_handle"; const logger = log.scope("proposal_handlers"); - +const handle = createLoggedHandler(logger); // Cache for codebase token counts interface CodebaseTokenCache { chatId: number; @@ -317,115 +317,83 @@ const approveProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId, messageId }: { chatId: number; messageId: number }, ): Promise<{ - success: boolean; - error?: string; uncommittedFiles?: string[]; }> => { - logger.log( - `IPC: approve-proposal called for chatId: ${chatId}, messageId: ${messageId}`, + // 1. Fetch the specific assistant message + const messageToApprove = await db.query.messages.findFirst({ + where: and( + eq(messages.id, messageId), + eq(messages.chatId, chatId), + eq(messages.role, "assistant"), + ), + columns: { + content: true, + }, + }); + + if (!messageToApprove?.content) { + throw new Error( + `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`, + ); + } + + // 2. Process the actions defined in the message content + const chatSummary = getDyadChatSummaryTag(messageToApprove.content); + const processResult = await processFullResponseActions( + messageToApprove.content, + chatId, + { + chatSummary: chatSummary ?? undefined, + messageId, + }, // Pass summary if found ); - try { - // 1. Fetch the specific assistant message - const messageToApprove = await db.query.messages.findFirst({ - where: and( - eq(messages.id, messageId), - eq(messages.chatId, chatId), - eq(messages.role, "assistant"), - ), - columns: { - content: true, - }, - }); - - if (!messageToApprove?.content) { - logger.error( - `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`, - ); - return { success: false, error: "Assistant message not found." }; - } - - // 2. Process the actions defined in the message content - const chatSummary = getDyadChatSummaryTag(messageToApprove.content); - const processResult = await processFullResponseActions( - messageToApprove.content, - chatId, - { - chatSummary: chatSummary ?? undefined, - messageId, - }, // Pass summary if found + if (processResult.error) { + throw new Error( + `Error processing actions for message ${messageId}: ${processResult.error}`, ); - - if (processResult.error) { - logger.error( - `Error processing actions for message ${messageId}:`, - processResult.error, - ); - // Optionally: Update message state to 'error' or similar? - // For now, just return error to frontend - return { - success: false, - error: `Action processing failed: ${processResult.error}`, - }; - } - - return { success: true, uncommittedFiles: processResult.uncommittedFiles }; - } catch (error) { - logger.error(`Error approving proposal for messageId ${messageId}:`, error); - return { - success: false, - error: (error as Error)?.message || "Unknown error", - }; } + + return { uncommittedFiles: processResult.uncommittedFiles }; }; // Handler to reject a proposal (just update message state) const rejectProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId, messageId }: { chatId: number; messageId: number }, -): Promise<{ success: boolean; error?: string }> => { +): Promise => { logger.log( `IPC: reject-proposal called for chatId: ${chatId}, messageId: ${messageId}`, ); - try { - // 1. Verify the message exists and is an assistant message - const messageToReject = await db.query.messages.findFirst({ - where: and( - eq(messages.id, messageId), - eq(messages.chatId, chatId), - eq(messages.role, "assistant"), - ), - columns: { id: true }, - }); + // 1. Verify the message exists and is an assistant message + const messageToReject = await db.query.messages.findFirst({ + where: and( + eq(messages.id, messageId), + eq(messages.chatId, chatId), + eq(messages.role, "assistant"), + ), + columns: { id: true }, + }); - if (!messageToReject) { - logger.error( - `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`, - ); - return { success: false, error: "Assistant message not found." }; - } - - // 2. Update the message's approval state to 'rejected' - await db - .update(messages) - .set({ approvalState: "rejected" }) - .where(eq(messages.id, messageId)); - - logger.log(`Message ${messageId} marked as rejected.`); - return { success: true }; - } catch (error) { - logger.error(`Error rejecting proposal for messageId ${messageId}:`, error); - return { - success: false, - error: (error as Error)?.message || "Unknown error", - }; + if (!messageToReject) { + throw new Error( + `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`, + ); } + + // 2. Update the message's approval state to 'rejected' + await db + .update(messages) + .set({ approvalState: "rejected" }) + .where(eq(messages.id, messageId)); + + logger.log(`Message ${messageId} marked as rejected.`); }; // Function to register proposal-related handlers export function registerProposalHandlers() { - ipcMain.handle("get-proposal", getProposalHandler); - ipcMain.handle("approve-proposal", approveProposalHandler); - ipcMain.handle("reject-proposal", rejectProposalHandler); + handle("get-proposal", getProposalHandler); + handle("approve-proposal", approveProposalHandler); + handle("reject-proposal", rejectProposalHandler); } diff --git a/src/ipc/handlers/safe_handle.ts b/src/ipc/handlers/safe_handle.ts index 9e43410..4357ee8 100644 --- a/src/ipc/handlers/safe_handle.ts +++ b/src/ipc/handlers/safe_handle.ts @@ -1,7 +1,7 @@ import { ipcMain, IpcMainInvokeEvent } from "electron"; import log from "electron-log"; -export function createSafeHandler(logger: log.LogFunctions) { +export function createLoggedHandler(logger: log.LogFunctions) { return ( channel: string, fn: (event: IpcMainInvokeEvent, ...args: any[]) => Promise, @@ -9,8 +9,11 @@ export function createSafeHandler(logger: log.LogFunctions) { ipcMain.handle( channel, async (event: IpcMainInvokeEvent, ...args: any[]) => { + logger.log(`IPC: ${channel} called with args: ${JSON.stringify(args)}`); try { - return await fn(event, ...args); + const result = await fn(event, ...args); + logger.log(`IPC: ${channel} returned: ${JSON.stringify(result)}`); + return result; } catch (error) { logger.error( `Error in ${fn.name}: args: ${JSON.stringify(args)}`, diff --git a/src/ipc/handlers/settings_handlers.ts b/src/ipc/handlers/settings_handlers.ts index cbf88a0..89ecc53 100644 --- a/src/ipc/handlers/settings_handlers.ts +++ b/src/ipc/handlers/settings_handlers.ts @@ -4,11 +4,13 @@ import { writeSettings } from "../../main/settings"; import { readSettings } from "../../main/settings"; export function registerSettingsHandlers() { + // Intentionally do NOT use handle because it could log sensitive data from the return value. ipcMain.handle("get-user-settings", async () => { const settings = readSettings(); return settings; }); + // Intentionally do NOT use handle because it could log sensitive data from the args. ipcMain.handle( "set-user-settings", async (_, settings: Partial) => { diff --git a/src/ipc/handlers/shell_handler.ts b/src/ipc/handlers/shell_handler.ts index a543720..d23021e 100644 --- a/src/ipc/handlers/shell_handler.ts +++ b/src/ipc/handlers/shell_handler.ts @@ -1,45 +1,29 @@ -import { ipcMain, shell } from "electron"; +import { shell } from "electron"; import log from "electron-log"; +import { createLoggedHandler } from "./safe_handle"; const logger = log.scope("shell_handlers"); +const handle = createLoggedHandler(logger); export function registerShellHandlers() { - ipcMain.handle("open-external-url", async (_event, url: string) => { - try { - // 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 }; - } - 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) { - logger.error(`Failed to open external URL ${url}:`, error); - return { success: false, error: (error as Error).message }; + handle("open-external-url", async (_event, url: string) => { + if (!url) { + throw new Error("No URL provided."); } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + throw new Error("Attempted to open invalid or non-http URL: " + url); + } + await shell.openExternal(url); + logger.debug("Opened external URL:", url); }); - ipcMain.handle("show-item-in-folder", async (_event, fullPath: string) => { - try { - // Validate that a path was provided - if (!fullPath) { - logger.error("Attempted to show item with empty path"); - return { - success: false, - error: "No file path provided.", - }; - } - - shell.showItemInFolder(fullPath); - logger.debug("Showed item in folder:", fullPath); - return { success: true }; - } catch (error) { - logger.error(`Failed to show item in folder ${fullPath}:`, error); - return { success: false, error: (error as Error).message }; + handle("show-item-in-folder", async (_event, fullPath: string) => { + // Validate that a path was provided + if (!fullPath) { + throw new Error("No file path provided."); } + + shell.showItemInFolder(fullPath); + logger.debug("Showed item in folder:", fullPath); }); } diff --git a/src/ipc/handlers/supabase_handlers.ts b/src/ipc/handlers/supabase_handlers.ts index dd3f00b..6df3886 100644 --- a/src/ipc/handlers/supabase_handlers.ts +++ b/src/ipc/handlers/supabase_handlers.ts @@ -1,63 +1,39 @@ -import { ipcMain } from "electron"; import log from "electron-log"; import { db } from "../../db"; import { eq } from "drizzle-orm"; import { apps } from "../../db/schema"; import { getSupabaseClient } from "../../supabase_admin/supabase_management_client"; +import { createLoggedHandler } from "./safe_handle"; const logger = log.scope("supabase_handlers"); +const handle = createLoggedHandler(logger); export function registerSupabaseHandlers() { - // List all Supabase projects - ipcMain.handle("supabase:list-projects", async () => { - try { - const supabase = await getSupabaseClient(); - // Call the API according to supabase-management-js structure - const projects = await supabase.getProjects(); - return projects; - } catch (error) { - logger.error("Error listing Supabase projects:", error); - throw error; - } + handle("supabase:list-projects", async () => { + const supabase = await getSupabaseClient(); + return supabase.getProjects(); }); // Set app project - links a Dyad app to a Supabase project - ipcMain.handle( + handle( "supabase:set-app-project", async (_, { project, app }: { project: string; app: number }) => { - try { - // Here you could store the project-app association in your database - // For example: - await db - .update(apps) - .set({ supabaseProjectId: project }) - .where(eq(apps.id, app)); + await db + .update(apps) + .set({ supabaseProjectId: project }) + .where(eq(apps.id, app)); - logger.info(`Associated app ${app} with Supabase project ${project}`); - return { success: true, appId: app, projectId: project }; - } catch (error) { - logger.error("Error setting Supabase project for app:", error); - throw error; - } + logger.info(`Associated app ${app} with Supabase project ${project}`); }, ); // Unset app project - removes the link between a Dyad app and a Supabase project - ipcMain.handle( - "supabase:unset-app-project", - async (_, { app }: { app: number }) => { - try { - await db - .update(apps) - .set({ supabaseProjectId: null }) - .where(eq(apps.id, app)); + handle("supabase:unset-app-project", async (_, { app }: { app: number }) => { + await db + .update(apps) + .set({ supabaseProjectId: null }) + .where(eq(apps.id, app)); - logger.info(`Removed Supabase project association for app ${app}`); - return { success: true, appId: app }; - } catch (error) { - logger.error("Error unsetting Supabase project for app:", error); - throw error; - } - }, - ); + logger.info(`Removed Supabase project association for app ${app}`); + }); } diff --git a/src/ipc/handlers/token_count_handlers.ts b/src/ipc/handlers/token_count_handlers.ts index 18ef1db..111495b 100644 --- a/src/ipc/handlers/token_count_handlers.ts +++ b/src/ipc/handlers/token_count_handlers.ts @@ -1,4 +1,3 @@ -import { ipcMain } from "electron"; import { db } from "../../db"; import { chats } from "../../db/schema"; import { eq } from "drizzle-orm"; @@ -15,91 +14,82 @@ import { getSupabaseContext } from "../../supabase_admin/supabase_context"; import { TokenCountParams } from "../ipc_types"; import { TokenCountResult } from "../ipc_types"; import { estimateTokens, getContextWindow } from "../utils/token_utils"; +import { createLoggedHandler } from "./safe_handle"; const logger = log.scope("token_count_handlers"); +const handle = createLoggedHandler(logger); + export function registerTokenCountHandlers() { - ipcMain.handle( + handle( "chat:count-tokens", async (event, req: TokenCountParams): Promise => { - try { - // Get the chat with messages - const chat = await db.query.chats.findFirst({ - where: eq(chats.id, req.chatId), - with: { - messages: { - orderBy: (messages, { asc }) => [asc(messages.createdAt)], - }, - app: true, + const chat = await db.query.chats.findFirst({ + where: eq(chats.id, req.chatId), + with: { + messages: { + orderBy: (messages, { asc }) => [asc(messages.createdAt)], }, - }); + app: true, + }, + }); - if (!chat) { - throw new Error(`Chat not found: ${req.chatId}`); - } - - // Prepare message history for token counting - const messageHistory = chat.messages - .map((message) => message.content) - .join(""); - const messageHistoryTokens = estimateTokens(messageHistory); - - // Count input tokens - const inputTokens = estimateTokens(req.input); - - // Count system prompt tokens - let systemPrompt = SYSTEM_PROMPT; - let supabaseContext = ""; - - if (chat.app?.supabaseProjectId) { - systemPrompt += "\n\n" + SUPABASE_AVAILABLE_SYSTEM_PROMPT; - supabaseContext = await getSupabaseContext({ - supabaseProjectId: chat.app.supabaseProjectId, - }); - } else { - systemPrompt += "\n\n" + SUPABASE_NOT_AVAILABLE_SYSTEM_PROMPT; - } - - const systemPromptTokens = estimateTokens( - systemPrompt + supabaseContext, - ); - - // Extract codebase information if app is associated with the chat - let codebaseInfo = ""; - let codebaseTokens = 0; - - if (chat.app) { - const appPath = getDyadAppPath(chat.app.path); - try { - codebaseInfo = await extractCodebase(appPath); - codebaseTokens = estimateTokens(codebaseInfo); - logger.log( - `Extracted codebase information from ${appPath}, tokens: ${codebaseTokens}`, - ); - } catch (error) { - logger.error("Error extracting codebase:", error); - } - } - - // Calculate total tokens - const totalTokens = - messageHistoryTokens + - inputTokens + - systemPromptTokens + - codebaseTokens; - - return { - totalTokens, - messageHistoryTokens, - codebaseTokens, - inputTokens, - systemPromptTokens, - contextWindow: getContextWindow(), - }; - } catch (error) { - logger.error("Error counting tokens:", error); - throw error; + if (!chat) { + throw new Error(`Chat not found: ${req.chatId}`); } + + // Prepare message history for token counting + const messageHistory = chat.messages + .map((message) => message.content) + .join(""); + const messageHistoryTokens = estimateTokens(messageHistory); + + // Count input tokens + const inputTokens = estimateTokens(req.input); + + // Count system prompt tokens + let systemPrompt = SYSTEM_PROMPT; + let supabaseContext = ""; + + if (chat.app?.supabaseProjectId) { + systemPrompt += "\n\n" + SUPABASE_AVAILABLE_SYSTEM_PROMPT; + supabaseContext = await getSupabaseContext({ + supabaseProjectId: chat.app.supabaseProjectId, + }); + } else { + systemPrompt += "\n\n" + SUPABASE_NOT_AVAILABLE_SYSTEM_PROMPT; + } + + const systemPromptTokens = estimateTokens(systemPrompt + supabaseContext); + + // Extract codebase information if app is associated with the chat + let codebaseInfo = ""; + let codebaseTokens = 0; + + if (chat.app) { + const appPath = getDyadAppPath(chat.app.path); + codebaseInfo = await extractCodebase(appPath); + codebaseTokens = estimateTokens(codebaseInfo); + logger.log( + `Extracted codebase information from ${appPath}, tokens: ${codebaseTokens}`, + ); + } + + // Calculate total tokens + const totalTokens = + messageHistoryTokens + + inputTokens + + systemPromptTokens + + codebaseTokens; + + return { + totalTokens, + messageHistoryTokens, + codebaseTokens, + inputTokens, + systemPromptTokens, + contextWindow: getContextWindow(), + }; }, ); } diff --git a/src/ipc/handlers/upload_handlers.ts b/src/ipc/handlers/upload_handlers.ts index af7f974..e1253b8 100644 --- a/src/ipc/handlers/upload_handlers.ts +++ b/src/ipc/handlers/upload_handlers.ts @@ -1,9 +1,11 @@ -import { ipcMain } from "electron"; import log from "electron-log"; import fetch from "node-fetch"; +import { createLoggedHandler } from "./safe_handle"; const logger = log.scope("upload_handlers"); +const handle = createLoggedHandler(logger); + interface UploadToSignedUrlParams { url: string; contentType: string; @@ -11,49 +13,37 @@ interface UploadToSignedUrlParams { } export function registerUploadHandlers() { - ipcMain.handle( - "upload-to-signed-url", - async (_, params: UploadToSignedUrlParams) => { - const { url, contentType, data } = params; - logger.debug("IPC: upload-to-signed-url called"); + handle("upload-to-signed-url", async (_, params: UploadToSignedUrlParams) => { + const { url, contentType, data } = params; + logger.debug("IPC: upload-to-signed-url called"); - try { - // Validate the signed URL - if (!url || typeof url !== "string" || !url.startsWith("https://")) { - throw new Error("Invalid signed URL provided"); - } + // Validate the signed URL + if (!url || typeof url !== "string" || !url.startsWith("https://")) { + throw new Error("Invalid signed URL provided"); + } - // Validate content type - if (!contentType || typeof contentType !== "string") { - throw new Error("Invalid content type provided"); - } + // Validate content type + if (!contentType || typeof contentType !== "string") { + throw new Error("Invalid content type provided"); + } - // Perform the upload to the signed URL - const response = await fetch(url, { - method: "PUT", - headers: { - "Content-Type": contentType, - }, - body: JSON.stringify(data), - }); + // Perform the upload to the signed URL + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": contentType, + }, + body: JSON.stringify(data), + }); - if (!response.ok) { - throw new Error( - `Upload failed with status ${response.status}: ${response.statusText}`, - ); - } + if (!response.ok) { + throw new Error( + `Upload failed with status ${response.status}: ${response.statusText}`, + ); + } - logger.debug("Successfully uploaded data to signed URL"); - return { success: true }; - } catch (error) { - logger.error("Failed to upload to signed URL:", error); - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - } - }, - ); + logger.debug("Successfully uploaded data to signed URL"); + }); logger.debug("Registered upload IPC handlers"); } diff --git a/src/ipc/handlers/version_handlers.ts b/src/ipc/handlers/version_handlers.ts index b78c336..9363d11 100644 --- a/src/ipc/handlers/version_handlers.ts +++ b/src/ipc/handlers/version_handlers.ts @@ -10,11 +10,11 @@ import { promises as fsPromises } from "node:fs"; import { withLock } from "../utils/lock_utils"; import { getGitAuthor } from "../utils/git_author"; import log from "electron-log"; -import { createSafeHandler } from "./safe_handle"; +import { createLoggedHandler } from "./safe_handle"; const logger = log.scope("version_handlers"); -const handle = createSafeHandler(logger); +const handle = createLoggedHandler(logger); export function registerVersionHandlers() { handle("list-versions", async (_, { appId }: { appId: number }) => { diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 5ef7a50..f0e12e5 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -150,23 +150,11 @@ export class IpcClient { // Create a new app with an initial chat public async createApp(params: CreateAppParams): Promise { - try { - const result = await this.ipcRenderer.invoke("create-app", params); - return result as CreateAppResult; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("create-app", params); } public async getApp(appId: number): Promise { - try { - const data = await this.ipcRenderer.invoke("get-app", appId); - return data; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("get-app", appId); } public async getChat(chatId: number): Promise { @@ -192,28 +180,14 @@ export class IpcClient { // Get all apps public async listApps(): Promise { - try { - const data = await this.ipcRenderer.invoke("list-apps"); - return data; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("list-apps"); } - // Read a file from an app directory public async readAppFile(appId: number, filePath: string): Promise { - try { - const content = await this.ipcRenderer.invoke("read-app-file", { - appId, - filePath, - }); - return content as string; - } catch (error) { - // No toast because sometimes the file will disappear. - console.error(error); - throw error; - } + return this.ipcRenderer.invoke("read-app-file", { + appId, + filePath, + }); } // Edit a file in an app directory @@ -221,18 +195,12 @@ export class IpcClient { appId: number, filePath: string, content: string, - ): Promise<{ success: boolean }> { - try { - const result = await this.ipcRenderer.invoke("edit-app-file", { - appId, - filePath, - content, - }); - return result as { success: boolean }; - } catch (error) { - showError(error); - throw error; - } + ): Promise { + await this.ipcRenderer.invoke("edit-app-file", { + appId, + filePath, + content, + }); } // New method for streaming responses @@ -321,91 +289,38 @@ export class IpcClient { // Create a new chat for an app public async createChat(appId: number): Promise { - try { - const chatId = await this.ipcRenderer.invoke("create-chat", appId); - return chatId as number; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("create-chat", appId); } - public async deleteChat( - chatId: number, - ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("delete-chat", chatId); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - throw error; - } + public async deleteChat(chatId: number): Promise { + await this.ipcRenderer.invoke("delete-chat", chatId); } - public async deleteMessages( - chatId: number, - ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("delete-messages", chatId); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - throw error; - } + public async deleteMessages(chatId: number): Promise { + await this.ipcRenderer.invoke("delete-messages", chatId); } // Open an external URL using the default browser - public async openExternalUrl( - url: string, - ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("open-external-url", url); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - throw error; - } + public async openExternalUrl(url: string): Promise { + await this.ipcRenderer.invoke("open-external-url", url); } - public async showItemInFolder( - fullPath: string, - ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke( - "show-item-in-folder", - fullPath, - ); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - throw error; - } + public async showItemInFolder(fullPath: string): Promise { + await this.ipcRenderer.invoke("show-item-in-folder", fullPath); } // Run an app public async runApp( appId: number, onOutput: (output: AppOutput) => void, - ): Promise<{ success: boolean }> { - try { - const result = await this.ipcRenderer.invoke("run-app", { appId }); - this.appStreams.set(appId, { onOutput }); - return result; - } catch (error) { - showError(error); - throw error; - } + ): Promise { + await this.ipcRenderer.invoke("run-app", { appId }); + this.appStreams.set(appId, { onOutput }); } // Stop a running app - public async stopApp(appId: number): Promise<{ success: boolean }> { - try { - const result = await this.ipcRenderer.invoke("stop-app", { appId }); - return result; - } catch (error) { - showError(error); - throw error; - } + public async stopApp(appId: number): Promise { + await this.ipcRenderer.invoke("stop-app", { appId }); } // Restart a running app @@ -514,14 +429,8 @@ export class IpcClient { } // Delete an app and all its files - public async deleteApp(appId: number): Promise<{ success: boolean }> { - try { - const result = await this.ipcRenderer.invoke("delete-app", { appId }); - return result as { success: boolean }; - } catch (error) { - showError(error); - throw error; - } + public async deleteApp(appId: number): Promise { + await this.ipcRenderer.invoke("delete-app", { appId }); } // Rename an app (update name and path) @@ -533,29 +442,17 @@ export class IpcClient { appId: number; appName: string; appPath: string; - }): Promise<{ success: boolean; app: App }> { - try { - const result = await this.ipcRenderer.invoke("rename-app", { - appId, - appName, - appPath, - }); - return result as { success: boolean; app: App }; - } catch (error) { - showError(error); - throw error; - } + }): Promise { + await this.ipcRenderer.invoke("rename-app", { + appId, + appName, + appPath, + }); } // Reset all - removes all app files, settings, and drops the database - public async resetAll(): Promise<{ success: boolean; message: string }> { - try { - const result = await this.ipcRenderer.invoke("reset-all"); - return result as { success: boolean; message: string }; - } catch (error) { - showError(error); - throw error; - } + public async resetAll(): Promise { + await this.ipcRenderer.invoke("reset-all"); } public async addDependency({ @@ -565,26 +462,15 @@ export class IpcClient { chatId: number; packages: string[]; }): Promise { - try { - await this.ipcRenderer.invoke("chat:add-dep", { - chatId, - packages, - }); - } catch (error) { - showError(error); - throw error; - } + await this.ipcRenderer.invoke("chat:add-dep", { + chatId, + packages, + }); } // Check Node.js and npm status public async getNodejsStatus(): Promise { - try { - const result = await this.ipcRenderer.invoke("nodejs-status"); - return result; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("nodejs-status"); } // --- GitHub Device Flow --- @@ -631,11 +517,6 @@ export class IpcClient { this.ipcRenderer.removeListener("github:flow-error", listener); }; } - - // TODO: Implement cancel method if needed - // public cancelGithubDeviceFlow(): void { - // this.ipcRenderer.sendMessage("github:cancel-flow"); - // } // --- End GitHub Device Flow --- // --- GitHub Repo Management --- @@ -643,32 +524,22 @@ export class IpcClient { org: string, repo: string, ): Promise<{ available: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("github:is-repo-available", { - org, - repo, - }); - return result; - } catch (error: any) { - return { available: false, error: error.message || "Unknown error" }; - } + return this.ipcRenderer.invoke("github:is-repo-available", { + org, + repo, + }); } public async createGithubRepo( org: string, repo: string, appId: number, - ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("github:create-repo", { - org, - repo, - appId, - }); - return result; - } catch (error: any) { - return { success: false, error: error.message || "Unknown error" }; - } + ): Promise { + await this.ipcRenderer.invoke("github:create-repo", { + org, + repo, + appId, + }); } // Sync (push) local repo to GitHub @@ -684,30 +555,17 @@ export class IpcClient { } } - public async disconnectGithubRepo( - appId: number, - ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("github:disconnect", { - appId, - }); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - throw error; - } + public async disconnectGithubRepo(appId: number): Promise { + await this.ipcRenderer.invoke("github:disconnect", { + appId, + }); } // --- End GitHub Repo Management --- // Get the main app version public async getAppVersion(): Promise { - try { - const result = await this.ipcRenderer.invoke("get-app-version"); - return result.version as string; - } catch (error) { - showError(error); - throw error; - } + const result = await this.ipcRenderer.invoke("get-app-version"); + return result.version as string; } // Get proposal details @@ -734,24 +592,12 @@ export class IpcClient { chatId: number; messageId: number; }): Promise<{ - success: boolean; - error?: string; uncommittedFiles?: string[]; }> { - try { - const result = await this.ipcRenderer.invoke("approve-proposal", { - chatId, - messageId, - }); - return result as { - success: boolean; - error?: string; - uncommittedFiles?: string[]; - }; - } catch (error) { - showError(error); - return { success: false, error: (error as Error).message }; - } + return this.ipcRenderer.invoke("approve-proposal", { + chatId, + messageId, + }); } public async rejectProposal({ @@ -760,132 +606,66 @@ export class IpcClient { }: { chatId: number; messageId: number; - }): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("reject-proposal", { - chatId, - messageId, - }); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - return { success: false, error: (error as Error).message }; - } + }): Promise { + await this.ipcRenderer.invoke("reject-proposal", { + chatId, + messageId, + }); } // --- End Proposal Management --- // --- Supabase Management --- public async listSupabaseProjects(): Promise { - try { - const projects = await this.ipcRenderer.invoke("supabase:list-projects"); - return projects; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("supabase:list-projects"); } public async setSupabaseAppProject( project: string, app: number, - ): Promise<{ success: boolean; appId: number; projectId: string }> { - try { - const result = await this.ipcRenderer.invoke("supabase:set-app-project", { - project, - app, - }); - return result; - } catch (error) { - showError(error); - throw error; - } + ): Promise { + await this.ipcRenderer.invoke("supabase:set-app-project", { + project, + app, + }); } - public async unsetSupabaseAppProject( - app: number, - ): Promise<{ success: boolean; appId: number }> { - try { - const result = await this.ipcRenderer.invoke( - "supabase:unset-app-project", - { - app, - }, - ); - return result; - } catch (error) { - showError(error); - throw error; - } + public async unsetSupabaseAppProject(app: number): Promise { + await this.ipcRenderer.invoke("supabase:unset-app-project", { + app, + }); } // --- End Supabase Management --- - // Get system debug information public async getSystemDebugInfo(): Promise { - try { - const data = await this.ipcRenderer.invoke("get-system-debug-info"); - return data as SystemDebugInfo; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("get-system-debug-info"); } public async getChatLogs(chatId: number): Promise { - try { - const data = await this.ipcRenderer.invoke("get-chat-logs", chatId); - return data as ChatLogsData; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("get-chat-logs", chatId); } public async uploadToSignedUrl( url: string, contentType: string, data: any, - ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("upload-to-signed-url", { - url, - contentType, - data, - }); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - throw error; - } + ): Promise { + await this.ipcRenderer.invoke("upload-to-signed-url", { + url, + contentType, + data, + }); } public async listLocalOllamaModels(): Promise { - try { - const response = await this.ipcRenderer.invoke( - "local-models:list-ollama", - ); - return response?.models || []; - } catch (error) { - if (error instanceof Error) { - throw new Error(`Failed to fetch Ollama models: ${error.message}`); - } - throw new Error("Failed to fetch Ollama models: Unknown error occurred"); - } + const response = await this.ipcRenderer.invoke("local-models:list-ollama"); + return response?.models || []; } public async listLocalLMStudioModels(): Promise { - try { - const response = await this.ipcRenderer.invoke( - "local-models:list-lmstudio", - ); - return response?.models || []; - } catch (error) { - if (error instanceof Error) { - throw new Error(`Failed to fetch LM Studio models: ${error.message}`); - } - throw new Error( - "Failed to fetch LM Studio models: Unknown error occurred", - ); - } + const response = await this.ipcRenderer.invoke( + "local-models:list-lmstudio", + ); + return response?.models || []; } // Listen for deep link events diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index e7d3368..2942abe 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -111,7 +111,6 @@ export interface LocalModel { export type LocalModelListResponse = { models: LocalModel[]; - error: string | null; }; export interface TokenCountParams { diff --git a/src/pages/app-details.tsx b/src/pages/app-details.tsx index 418dcd3..37288d9 100644 --- a/src/pages/app-details.tsx +++ b/src/pages/app-details.tsx @@ -28,6 +28,7 @@ import { } from "@/components/ui/dialog"; import { GitHubConnector } from "@/components/GitHubConnector"; import { SupabaseConnector } from "@/components/SupabaseConnector"; +import { showError } from "@/lib/toast"; export default function AppDetailsPage() { const navigate = useNavigate(); @@ -62,7 +63,7 @@ export default function AppDetailsPage() { await refreshApps(); navigate({ to: "/", search: {} }); } catch (error) { - console.error("Failed to delete app:", error); + showError(error); } finally { setIsDeleting(false); } diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 29336e0..a226a41 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -27,12 +27,8 @@ export default function SettingsPage() { setIsResetting(true); try { const ipcClient = IpcClient.getInstance(); - const result = await ipcClient.resetAll(); - if (result.success) { - showSuccess("Successfully reset everything. Restart the application."); - } else { - showError(result.message || "Failed to reset everything."); - } + await ipcClient.resetAll(); + showSuccess("Successfully reset everything. Restart the application."); } catch (error) { console.error("Error resetting:", error); showError(