Unverified 提交 c71638a5 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Simplify handlers & IPC client: move from Result pattern to throwing errors (#120)

上级 26305ee0
......@@ -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
......
......@@ -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 {
......
......@@ -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);
......
......@@ -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 (
<TooltipProvider>
......
......@@ -89,14 +89,13 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
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(
......
......@@ -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;
}
......
......@@ -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)));
......
......@@ -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;
},
);
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 };
}
handle("delete-chat", async (_, chatId: number): Promise<void> => {
await db.delete(chats).where(eq(chats.id, chatId));
});
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-messages", async (_, chatId: number): Promise<void> => {
await db.delete(messages).where(eq(messages.chatId, chatId));
});
}
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,
}),
): 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}` },
});
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" };
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}`);
// 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" };
}
): Promise<void> {
logger.log(`Disconnecting GitHub repo for appId: ${appId}`);
// Update app in database to remove GitHub repo and org
await db
.update(apps)
.set({
githubRepo: null,
githubOrg: null,
})
.where(eq(apps.id, appId));
// Get the app from the database
const app = await db.query.apps.findFirst({
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");
}
}
......
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}`,
);
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." };
}
// 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,
},
});
// 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 (!messageToApprove?.content) {
throw new Error(
`Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`,
);
}
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}`,
};
}
// 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
);
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",
};
if (processResult.error) {
throw new Error(
`Error processing actions for message ${messageId}: ${processResult.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 },
});
if (!messageToReject) {
logger.error(
`Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`,
);
return { success: false, error: "Assistant message not found." };
}
// 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 },
});
// 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);
}
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>) => {
......
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);
});
}
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}`);
});
}
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}`);
}
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);
// 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 input tokens
const inputTokens = estimateTokens(req.input);
// Count system prompt tokens
let systemPrompt = SYSTEM_PROMPT;
let supabaseContext = "";
// 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;
}
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,
);
const systemPromptTokens = estimateTokens(systemPrompt + supabaseContext);
// Extract codebase information if app is associated with the chat
let codebaseInfo = "";
let codebaseTokens = 0;
// 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);
}
}
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;
// 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;
}
return {
totalTokens,
messageHistoryTokens,
codebaseTokens,
inputTokens,
systemPromptTokens,
contextWindow: getContextWindow(),
};
},
);
}
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");
try {
// 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");
}
// 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}`,
);
}
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),
};
}
},
);
handle("upload-to-signed-url", async (_, params: UploadToSignedUrlParams) => {
const { url, contentType, data } = params;
logger.debug("IPC: upload-to-signed-url called");
// 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");
}
// 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}`,
);
}
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 }) => {
......
差异被折叠。
......@@ -111,7 +111,6 @@ export interface LocalModel {
export type LocalModelListResponse = {
models: LocalModel[];
error: string | null;
};
export interface TokenCountParams {
......
......@@ -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);
}
......
......@@ -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(
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论