Simplify handlers & IPC client: move from Result pattern to throwing errors (#120)
This commit is contained in:
@@ -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<void> {
|
||||
}
|
||||
|
||||
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<App> => {
|
||||
// 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<App> => {
|
||||
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<string, string | undefined> = {};
|
||||
for (const key of ALLOWED_ENV_VARS) {
|
||||
@@ -294,13 +302,12 @@ export function registerAppHandlers() {
|
||||
async (
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
{ appId }: { appId: number },
|
||||
) => {
|
||||
): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
// 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<void> => {
|
||||
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<void> => {
|
||||
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"));
|
||||
|
||||
@@ -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<number> => {
|
||||
// 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<ChatSummary[]> => {
|
||||
// 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<ChatSummary[]> => {
|
||||
// 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<void> => {
|
||||
await db.delete(chats).where(eq(chats.id, chatId));
|
||||
});
|
||||
|
||||
handle("delete-messages", async (_, chatId: number): Promise<void> => {
|
||||
await db.delete(messages).where(eq(messages.chatId, chatId));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<void> => {
|
||||
// Find the message from the database
|
||||
const foundMessages = await db.query.messages.findMany({
|
||||
where: eq(messages.chatId, chatId),
|
||||
|
||||
@@ -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<void> {
|
||||
// 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<void> {
|
||||
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 ---
|
||||
|
||||
@@ -18,28 +18,24 @@ export interface LMStudioModel {
|
||||
}
|
||||
|
||||
export async function fetchLMStudioModels(): Promise<LocalModelListResponse> {
|
||||
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() {
|
||||
|
||||
@@ -47,20 +47,17 @@ export async function fetchOllamaModels(): Promise<LocalModelListResponse> {
|
||||
};
|
||||
});
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<any>,
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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<UserSettings>) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<TokenCountResult> => {
|
||||
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(),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user