<!-- This is an auto-generated description by cubic. --> ## Summary by cubic Adds multi-component selection in the preview and sends all selected components to chat for targeted edits. Updates overlays, UI, and IPC to support arrays, smarter context focusing, and cross-platform path normalization. - **New Features** - Select multiple components in the iframe; selection mode stays active until you deactivate it. - Show a scrollable list of selections with remove buttons and a Clear all; remove from the list or click an overlay in the preview to deselect. Sending clears all overlays. - Separate hover vs selected overlays with labels on hover; overlays persist after deactivation and re-position on layout changes/resizes. - Chat input and streaming now send selectedComponents; server builds per-component snippets and focuses their files in smart context. - **Migration** - Replace selectedComponentPreviewAtom with selectedComponentsPreviewAtom (ComponentSelection[]). - ChatStreamParams now uses selectedComponents; migrate any single-selection usages. - previewIframeRefAtom added for clearing overlays from the parent. <sup>Written for commit da0d64cc9e9f83fbf4b975278f6c869f0d3a8c7d. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
1327 lines
35 KiB
TypeScript
1327 lines
35 KiB
TypeScript
import type { IpcRenderer } from "electron";
|
||
import {
|
||
type ChatSummary,
|
||
ChatSummariesSchema,
|
||
type UserSettings,
|
||
type ContextPathResults,
|
||
ChatSearchResultsSchema,
|
||
AppSearchResultsSchema,
|
||
} from "../lib/schemas";
|
||
import type {
|
||
AppOutput,
|
||
Chat,
|
||
ChatResponseEnd,
|
||
ChatProblemsEvent,
|
||
CreateAppParams,
|
||
CreateAppResult,
|
||
ListAppsResponse,
|
||
NodeSystemInfo,
|
||
Message,
|
||
Version,
|
||
SystemDebugInfo,
|
||
LocalModel,
|
||
TokenCountParams,
|
||
TokenCountResult,
|
||
ChatLogsData,
|
||
BranchResult,
|
||
LanguageModelProvider,
|
||
LanguageModel,
|
||
CreateCustomLanguageModelProviderParams,
|
||
CreateCustomLanguageModelParams,
|
||
DoesReleaseNoteExistParams,
|
||
ApproveProposalResult,
|
||
ImportAppResult,
|
||
ImportAppParams,
|
||
RenameBranchParams,
|
||
UserBudgetInfo,
|
||
CopyAppParams,
|
||
App,
|
||
ComponentSelection,
|
||
AppUpgrade,
|
||
ProblemReport,
|
||
EditAppFileReturnType,
|
||
GetAppEnvVarsParams,
|
||
SetAppEnvVarsParams,
|
||
ConnectToExistingVercelProjectParams,
|
||
IsVercelProjectAvailableResponse,
|
||
CreateVercelProjectParams,
|
||
VercelDeployment,
|
||
GetVercelDeploymentsParams,
|
||
DisconnectVercelProjectParams,
|
||
SecurityReviewResult,
|
||
IsVercelProjectAvailableParams,
|
||
SaveVercelAccessTokenParams,
|
||
VercelProject,
|
||
UpdateChatParams,
|
||
FileAttachment,
|
||
CreateNeonProjectParams,
|
||
NeonProject,
|
||
GetNeonProjectParams,
|
||
GetNeonProjectResponse,
|
||
RevertVersionResponse,
|
||
RevertVersionParams,
|
||
RespondToAppInputParams,
|
||
PromptDto,
|
||
CreatePromptParamsDto,
|
||
UpdatePromptParamsDto,
|
||
McpServerUpdate,
|
||
CreateMcpServer,
|
||
CloneRepoParams,
|
||
SupabaseBranch,
|
||
SetSupabaseAppProjectParams,
|
||
SelectNodeFolderResult,
|
||
} from "./ipc_types";
|
||
import type { Template } from "../shared/templates";
|
||
import type {
|
||
AppChatContext,
|
||
AppSearchResult,
|
||
ChatSearchResult,
|
||
ProposalResult,
|
||
} from "@/lib/schemas";
|
||
import { showError } from "@/lib/toast";
|
||
import { DeepLinkData } from "./deep_link_data";
|
||
|
||
export interface ChatStreamCallbacks {
|
||
onUpdate: (messages: Message[]) => void;
|
||
onEnd: (response: ChatResponseEnd) => void;
|
||
onError: (error: string) => void;
|
||
}
|
||
|
||
export interface AppStreamCallbacks {
|
||
onOutput: (output: AppOutput) => void;
|
||
}
|
||
|
||
export interface GitHubDeviceFlowUpdateData {
|
||
userCode?: string;
|
||
verificationUri?: string;
|
||
message?: string;
|
||
}
|
||
|
||
export interface GitHubDeviceFlowSuccessData {
|
||
message?: string;
|
||
}
|
||
|
||
export interface GitHubDeviceFlowErrorData {
|
||
error: string;
|
||
}
|
||
|
||
interface DeleteCustomModelParams {
|
||
providerId: string;
|
||
modelApiName: string;
|
||
}
|
||
|
||
export class IpcClient {
|
||
private static instance: IpcClient;
|
||
private ipcRenderer: IpcRenderer;
|
||
private chatStreams: Map<number, ChatStreamCallbacks>;
|
||
private appStreams: Map<number, AppStreamCallbacks>;
|
||
private helpStreams: Map<
|
||
string,
|
||
{
|
||
onChunk: (delta: string) => void;
|
||
onEnd: () => void;
|
||
onError: (error: string) => void;
|
||
}
|
||
>;
|
||
private mcpConsentHandlers: Map<string, (payload: any) => void>;
|
||
private constructor() {
|
||
this.ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
|
||
this.chatStreams = new Map();
|
||
this.appStreams = new Map();
|
||
this.helpStreams = new Map();
|
||
this.mcpConsentHandlers = new Map();
|
||
// Set up listeners for stream events
|
||
this.ipcRenderer.on("chat:response:chunk", (data) => {
|
||
if (
|
||
data &&
|
||
typeof data === "object" &&
|
||
"chatId" in data &&
|
||
"messages" in data
|
||
) {
|
||
const { chatId, messages } = data as {
|
||
chatId: number;
|
||
messages: Message[];
|
||
};
|
||
|
||
const callbacks = this.chatStreams.get(chatId);
|
||
if (callbacks) {
|
||
callbacks.onUpdate(messages);
|
||
} else {
|
||
console.warn(
|
||
`[IPC] No callbacks found for chat ${chatId}`,
|
||
this.chatStreams,
|
||
);
|
||
}
|
||
} else {
|
||
showError(new Error(`[IPC] Invalid chunk data received: ${data}`));
|
||
}
|
||
});
|
||
|
||
this.ipcRenderer.on("app:output", (data) => {
|
||
if (
|
||
data &&
|
||
typeof data === "object" &&
|
||
"type" in data &&
|
||
"message" in data &&
|
||
"appId" in data
|
||
) {
|
||
const { type, message, appId } = data as unknown as AppOutput;
|
||
const callbacks = this.appStreams.get(appId);
|
||
if (callbacks) {
|
||
callbacks.onOutput({ type, message, appId, timestamp: Date.now() });
|
||
}
|
||
} else {
|
||
showError(new Error(`[IPC] Invalid app output data received: ${data}`));
|
||
}
|
||
});
|
||
|
||
this.ipcRenderer.on("chat:response:end", (payload) => {
|
||
const { chatId } = payload as unknown as ChatResponseEnd;
|
||
const callbacks = this.chatStreams.get(chatId);
|
||
if (callbacks) {
|
||
callbacks.onEnd(payload as unknown as ChatResponseEnd);
|
||
console.debug("chat:response:end");
|
||
this.chatStreams.delete(chatId);
|
||
} else {
|
||
console.error(
|
||
new Error(
|
||
`[IPC] No callbacks found for chat ${chatId} on stream end`,
|
||
),
|
||
);
|
||
}
|
||
});
|
||
|
||
this.ipcRenderer.on("chat:response:error", (payload) => {
|
||
console.debug("chat:response:error");
|
||
if (
|
||
payload &&
|
||
typeof payload === "object" &&
|
||
"chatId" in payload &&
|
||
"error" in payload
|
||
) {
|
||
const { chatId, error } = payload as { chatId: number; error: string };
|
||
const callbacks = this.chatStreams.get(chatId);
|
||
if (callbacks) {
|
||
callbacks.onError(error);
|
||
this.chatStreams.delete(chatId);
|
||
} else {
|
||
console.warn(
|
||
`[IPC] No callbacks found for chat ${chatId} on error`,
|
||
this.chatStreams,
|
||
);
|
||
}
|
||
} else {
|
||
console.error("[IPC] Invalid error data received:", payload);
|
||
}
|
||
});
|
||
|
||
// Help bot events
|
||
this.ipcRenderer.on("help:chat:response:chunk", (data) => {
|
||
if (
|
||
data &&
|
||
typeof data === "object" &&
|
||
"sessionId" in data &&
|
||
"delta" in data
|
||
) {
|
||
const { sessionId, delta } = data as {
|
||
sessionId: string;
|
||
delta: string;
|
||
};
|
||
const callbacks = this.helpStreams.get(sessionId);
|
||
if (callbacks) callbacks.onChunk(delta);
|
||
}
|
||
});
|
||
|
||
this.ipcRenderer.on("help:chat:response:end", (data) => {
|
||
if (data && typeof data === "object" && "sessionId" in data) {
|
||
const { sessionId } = data as { sessionId: string };
|
||
const callbacks = this.helpStreams.get(sessionId);
|
||
if (callbacks) callbacks.onEnd();
|
||
this.helpStreams.delete(sessionId);
|
||
}
|
||
});
|
||
this.ipcRenderer.on("help:chat:response:error", (data) => {
|
||
if (
|
||
data &&
|
||
typeof data === "object" &&
|
||
"sessionId" in data &&
|
||
"error" in data
|
||
) {
|
||
const { sessionId, error } = data as {
|
||
sessionId: string;
|
||
error: string;
|
||
};
|
||
const callbacks = this.helpStreams.get(sessionId);
|
||
if (callbacks) callbacks.onError(error);
|
||
this.helpStreams.delete(sessionId);
|
||
}
|
||
});
|
||
|
||
// MCP tool consent request from main
|
||
this.ipcRenderer.on("mcp:tool-consent-request", (payload) => {
|
||
const handler = this.mcpConsentHandlers.get("consent");
|
||
if (handler) handler(payload);
|
||
});
|
||
}
|
||
|
||
public static getInstance(): IpcClient {
|
||
if (!IpcClient.instance) {
|
||
IpcClient.instance = new IpcClient();
|
||
}
|
||
return IpcClient.instance;
|
||
}
|
||
|
||
public async restartDyad(): Promise<void> {
|
||
await this.ipcRenderer.invoke("restart-dyad");
|
||
}
|
||
|
||
public async reloadEnvPath(): Promise<void> {
|
||
await this.ipcRenderer.invoke("reload-env-path");
|
||
}
|
||
|
||
// Create a new app with an initial chat
|
||
public async createApp(params: CreateAppParams): Promise<CreateAppResult> {
|
||
return this.ipcRenderer.invoke("create-app", params);
|
||
}
|
||
|
||
public async getApp(appId: number): Promise<App> {
|
||
return this.ipcRenderer.invoke("get-app", appId);
|
||
}
|
||
|
||
public async addAppToFavorite(
|
||
appId: number,
|
||
): Promise<{ isFavorite: boolean }> {
|
||
try {
|
||
const result = await this.ipcRenderer.invoke("add-to-favorite", {
|
||
appId,
|
||
});
|
||
return result;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
public async getAppEnvVars(
|
||
params: GetAppEnvVarsParams,
|
||
): Promise<{ key: string; value: string }[]> {
|
||
return this.ipcRenderer.invoke("get-app-env-vars", params);
|
||
}
|
||
|
||
public async setAppEnvVars(params: SetAppEnvVarsParams): Promise<void> {
|
||
return this.ipcRenderer.invoke("set-app-env-vars", params);
|
||
}
|
||
|
||
public async getChat(chatId: number): Promise<Chat> {
|
||
try {
|
||
const data = await this.ipcRenderer.invoke("get-chat", chatId);
|
||
return data;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Get all chats
|
||
public async getChats(appId?: number): Promise<ChatSummary[]> {
|
||
try {
|
||
const data = await this.ipcRenderer.invoke("get-chats", appId);
|
||
return ChatSummariesSchema.parse(data);
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// search for chats
|
||
public async searchChats(
|
||
appId: number,
|
||
query: string,
|
||
): Promise<ChatSearchResult[]> {
|
||
try {
|
||
const data = await this.ipcRenderer.invoke("search-chats", appId, query);
|
||
return ChatSearchResultsSchema.parse(data);
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Get all apps
|
||
public async listApps(): Promise<ListAppsResponse> {
|
||
return this.ipcRenderer.invoke("list-apps");
|
||
}
|
||
|
||
// Search apps by name
|
||
public async searchApps(searchQuery: string): Promise<AppSearchResult[]> {
|
||
try {
|
||
const data = await this.ipcRenderer.invoke("search-app", searchQuery);
|
||
return AppSearchResultsSchema.parse(data);
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
public async readAppFile(appId: number, filePath: string): Promise<string> {
|
||
return this.ipcRenderer.invoke("read-app-file", {
|
||
appId,
|
||
filePath,
|
||
});
|
||
}
|
||
|
||
// Edit a file in an app directory
|
||
public async editAppFile(
|
||
appId: number,
|
||
filePath: string,
|
||
content: string,
|
||
): Promise<EditAppFileReturnType> {
|
||
return this.ipcRenderer.invoke("edit-app-file", {
|
||
appId,
|
||
filePath,
|
||
content,
|
||
});
|
||
}
|
||
|
||
// New method for streaming responses
|
||
public streamMessage(
|
||
prompt: string,
|
||
options: {
|
||
selectedComponents?: ComponentSelection[];
|
||
chatId: number;
|
||
redo?: boolean;
|
||
attachments?: FileAttachment[];
|
||
onUpdate: (messages: Message[]) => void;
|
||
onEnd: (response: ChatResponseEnd) => void;
|
||
onError: (error: string) => void;
|
||
onProblems?: (problems: ChatProblemsEvent) => void;
|
||
},
|
||
): void {
|
||
const {
|
||
chatId,
|
||
redo,
|
||
attachments,
|
||
selectedComponents,
|
||
onUpdate,
|
||
onEnd,
|
||
onError,
|
||
} = options;
|
||
this.chatStreams.set(chatId, { onUpdate, onEnd, onError });
|
||
|
||
// Handle file attachments if provided
|
||
if (attachments && attachments.length > 0) {
|
||
// Process each file attachment and convert to base64
|
||
Promise.all(
|
||
attachments.map(async (attachment) => {
|
||
return new Promise<{
|
||
name: string;
|
||
type: string;
|
||
data: string;
|
||
attachmentType: "upload-to-codebase" | "chat-context";
|
||
}>((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
resolve({
|
||
name: attachment.file.name,
|
||
type: attachment.file.type,
|
||
data: reader.result as string,
|
||
attachmentType: attachment.type,
|
||
});
|
||
};
|
||
reader.onerror = () =>
|
||
reject(new Error(`Failed to read file: ${attachment.file.name}`));
|
||
reader.readAsDataURL(attachment.file);
|
||
});
|
||
}),
|
||
)
|
||
.then((fileDataArray) => {
|
||
// Use invoke to start the stream and pass the chatId and attachments
|
||
this.ipcRenderer
|
||
.invoke("chat:stream", {
|
||
prompt,
|
||
chatId,
|
||
redo,
|
||
selectedComponents,
|
||
attachments: fileDataArray,
|
||
})
|
||
.catch((err) => {
|
||
console.error("Error streaming message:", err);
|
||
showError(err);
|
||
onError(String(err));
|
||
this.chatStreams.delete(chatId);
|
||
});
|
||
})
|
||
.catch((err) => {
|
||
console.error("Error streaming message:", err);
|
||
showError(err);
|
||
onError(String(err));
|
||
this.chatStreams.delete(chatId);
|
||
});
|
||
} else {
|
||
// No attachments, proceed normally
|
||
this.ipcRenderer
|
||
.invoke("chat:stream", {
|
||
prompt,
|
||
chatId,
|
||
redo,
|
||
selectedComponents,
|
||
})
|
||
.catch((err) => {
|
||
console.error("Error streaming message:", err);
|
||
showError(err);
|
||
onError(String(err));
|
||
this.chatStreams.delete(chatId);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Method to cancel an ongoing stream
|
||
public cancelChatStream(chatId: number): void {
|
||
this.ipcRenderer.invoke("chat:cancel", chatId);
|
||
}
|
||
|
||
// Create a new chat for an app
|
||
public async createChat(appId: number): Promise<number> {
|
||
return this.ipcRenderer.invoke("create-chat", appId);
|
||
}
|
||
|
||
public async updateChat(params: UpdateChatParams): Promise<void> {
|
||
return this.ipcRenderer.invoke("update-chat", params);
|
||
}
|
||
|
||
public async deleteChat(chatId: number): Promise<void> {
|
||
await this.ipcRenderer.invoke("delete-chat", chatId);
|
||
}
|
||
|
||
public async deleteMessages(chatId: number): Promise<void> {
|
||
await this.ipcRenderer.invoke("delete-messages", chatId);
|
||
}
|
||
|
||
// Open an external URL using the default browser
|
||
public async openExternalUrl(url: string): Promise<void> {
|
||
await this.ipcRenderer.invoke("open-external-url", url);
|
||
}
|
||
|
||
public async showItemInFolder(fullPath: string): Promise<void> {
|
||
await this.ipcRenderer.invoke("show-item-in-folder", fullPath);
|
||
}
|
||
|
||
// Run an app
|
||
public async runApp(
|
||
appId: number,
|
||
onOutput: (output: AppOutput) => void,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("run-app", { appId });
|
||
this.appStreams.set(appId, { onOutput });
|
||
}
|
||
|
||
// Stop a running app
|
||
public async stopApp(appId: number): Promise<void> {
|
||
await this.ipcRenderer.invoke("stop-app", { appId });
|
||
}
|
||
|
||
// Restart a running app
|
||
public async restartApp(
|
||
appId: number,
|
||
onOutput: (output: AppOutput) => void,
|
||
removeNodeModules?: boolean,
|
||
): Promise<{ success: boolean }> {
|
||
try {
|
||
const result = await this.ipcRenderer.invoke("restart-app", {
|
||
appId,
|
||
removeNodeModules,
|
||
});
|
||
this.appStreams.set(appId, { onOutput });
|
||
return result;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Respond to an app input request (y/n prompts)
|
||
public async respondToAppInput(
|
||
params: RespondToAppInputParams,
|
||
): Promise<void> {
|
||
try {
|
||
await this.ipcRenderer.invoke("respond-to-app-input", params);
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Get allow-listed environment variables
|
||
public async getEnvVars(): Promise<Record<string, string | undefined>> {
|
||
try {
|
||
const envVars = await this.ipcRenderer.invoke("get-env-vars");
|
||
return envVars as Record<string, string | undefined>;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// List all versions (commits) of an app
|
||
public async listVersions({ appId }: { appId: number }): Promise<Version[]> {
|
||
try {
|
||
const versions = await this.ipcRenderer.invoke("list-versions", {
|
||
appId,
|
||
});
|
||
return versions;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Revert to a specific version
|
||
public async revertVersion(
|
||
params: RevertVersionParams,
|
||
): Promise<RevertVersionResponse> {
|
||
return this.ipcRenderer.invoke("revert-version", params);
|
||
}
|
||
|
||
// Checkout a specific version without creating a revert commit
|
||
public async checkoutVersion({
|
||
appId,
|
||
versionId,
|
||
}: {
|
||
appId: number;
|
||
versionId: string;
|
||
}): Promise<void> {
|
||
await this.ipcRenderer.invoke("checkout-version", {
|
||
appId,
|
||
versionId,
|
||
});
|
||
}
|
||
|
||
// Get the current branch of an app
|
||
public async getCurrentBranch(appId: number): Promise<BranchResult> {
|
||
return this.ipcRenderer.invoke("get-current-branch", {
|
||
appId,
|
||
});
|
||
}
|
||
|
||
// Get user settings
|
||
public async getUserSettings(): Promise<UserSettings> {
|
||
try {
|
||
const settings = await this.ipcRenderer.invoke("get-user-settings");
|
||
return settings;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Update user settings
|
||
public async setUserSettings(
|
||
settings: Partial<UserSettings>,
|
||
): Promise<UserSettings> {
|
||
try {
|
||
const updatedSettings = await this.ipcRenderer.invoke(
|
||
"set-user-settings",
|
||
settings,
|
||
);
|
||
return updatedSettings;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Delete an app and all its files
|
||
public async deleteApp(appId: number): Promise<void> {
|
||
await this.ipcRenderer.invoke("delete-app", { appId });
|
||
}
|
||
|
||
// Rename an app (update name and path)
|
||
public async renameApp({
|
||
appId,
|
||
appName,
|
||
appPath,
|
||
}: {
|
||
appId: number;
|
||
appName: string;
|
||
appPath: string;
|
||
}): Promise<void> {
|
||
await this.ipcRenderer.invoke("rename-app", {
|
||
appId,
|
||
appName,
|
||
appPath,
|
||
});
|
||
}
|
||
|
||
public async copyApp(params: CopyAppParams): Promise<{ app: App }> {
|
||
return this.ipcRenderer.invoke("copy-app", params);
|
||
}
|
||
|
||
// Reset all - removes all app files, settings, and drops the database
|
||
public async resetAll(): Promise<void> {
|
||
await this.ipcRenderer.invoke("reset-all");
|
||
}
|
||
|
||
public async addDependency({
|
||
chatId,
|
||
packages,
|
||
}: {
|
||
chatId: number;
|
||
packages: string[];
|
||
}): Promise<void> {
|
||
await this.ipcRenderer.invoke("chat:add-dep", {
|
||
chatId,
|
||
packages,
|
||
});
|
||
}
|
||
|
||
// Check Node.js and npm status
|
||
public async getNodejsStatus(): Promise<NodeSystemInfo> {
|
||
return this.ipcRenderer.invoke("nodejs-status");
|
||
}
|
||
|
||
// --- GitHub Device Flow ---
|
||
public startGithubDeviceFlow(appId: number | null): void {
|
||
this.ipcRenderer.invoke("github:start-flow", { appId });
|
||
}
|
||
|
||
public onGithubDeviceFlowUpdate(
|
||
callback: (data: GitHubDeviceFlowUpdateData) => void,
|
||
): () => void {
|
||
const listener = (data: any) => {
|
||
console.log("github:flow-update", data);
|
||
callback(data as GitHubDeviceFlowUpdateData);
|
||
};
|
||
this.ipcRenderer.on("github:flow-update", listener);
|
||
// Return a function to remove the listener
|
||
return () => {
|
||
this.ipcRenderer.removeListener("github:flow-update", listener);
|
||
};
|
||
}
|
||
|
||
public onGithubDeviceFlowSuccess(
|
||
callback: (data: GitHubDeviceFlowSuccessData) => void,
|
||
): () => void {
|
||
const listener = (data: any) => {
|
||
console.log("github:flow-success", data);
|
||
callback(data as GitHubDeviceFlowSuccessData);
|
||
};
|
||
this.ipcRenderer.on("github:flow-success", listener);
|
||
return () => {
|
||
this.ipcRenderer.removeListener("github:flow-success", listener);
|
||
};
|
||
}
|
||
|
||
public onGithubDeviceFlowError(
|
||
callback: (data: GitHubDeviceFlowErrorData) => void,
|
||
): () => void {
|
||
const listener = (data: any) => {
|
||
console.log("github:flow-error", data);
|
||
callback(data as GitHubDeviceFlowErrorData);
|
||
};
|
||
this.ipcRenderer.on("github:flow-error", listener);
|
||
return () => {
|
||
this.ipcRenderer.removeListener("github:flow-error", listener);
|
||
};
|
||
}
|
||
// --- End GitHub Device Flow ---
|
||
|
||
// --- GitHub Repo Management ---
|
||
public async listGithubRepos(): Promise<
|
||
{ name: string; full_name: string; private: boolean }[]
|
||
> {
|
||
return this.ipcRenderer.invoke("github:list-repos");
|
||
}
|
||
|
||
public async getGithubRepoBranches(
|
||
owner: string,
|
||
repo: string,
|
||
): Promise<{ name: string; commit: { sha: string } }[]> {
|
||
return this.ipcRenderer.invoke("github:get-repo-branches", {
|
||
owner,
|
||
repo,
|
||
});
|
||
}
|
||
|
||
public async connectToExistingGithubRepo(
|
||
owner: string,
|
||
repo: string,
|
||
branch: string,
|
||
appId: number,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("github:connect-existing-repo", {
|
||
owner,
|
||
repo,
|
||
branch,
|
||
appId,
|
||
});
|
||
}
|
||
|
||
public async checkGithubRepoAvailable(
|
||
org: string,
|
||
repo: string,
|
||
): Promise<{ available: boolean; error?: string }> {
|
||
return this.ipcRenderer.invoke("github:is-repo-available", {
|
||
org,
|
||
repo,
|
||
});
|
||
}
|
||
|
||
public async createGithubRepo(
|
||
org: string,
|
||
repo: string,
|
||
appId: number,
|
||
branch?: string,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("github:create-repo", {
|
||
org,
|
||
repo,
|
||
appId,
|
||
branch,
|
||
});
|
||
}
|
||
|
||
// Sync (push) local repo to GitHub
|
||
public async syncGithubRepo(
|
||
appId: number,
|
||
force?: boolean,
|
||
): Promise<{ success: boolean; error?: string }> {
|
||
return this.ipcRenderer.invoke("github:push", {
|
||
appId,
|
||
force,
|
||
});
|
||
}
|
||
|
||
public async disconnectGithubRepo(appId: number): Promise<void> {
|
||
await this.ipcRenderer.invoke("github:disconnect", {
|
||
appId,
|
||
});
|
||
}
|
||
// --- End GitHub Repo Management ---
|
||
|
||
// --- Vercel Token Management ---
|
||
public async saveVercelAccessToken(
|
||
params: SaveVercelAccessTokenParams,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("vercel:save-token", params);
|
||
}
|
||
// --- End Vercel Token Management ---
|
||
|
||
// --- Vercel Project Management ---
|
||
public async listVercelProjects(): Promise<VercelProject[]> {
|
||
return this.ipcRenderer.invoke("vercel:list-projects", undefined);
|
||
}
|
||
|
||
public async connectToExistingVercelProject(
|
||
params: ConnectToExistingVercelProjectParams,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("vercel:connect-existing-project", params);
|
||
}
|
||
|
||
public async isVercelProjectAvailable(
|
||
params: IsVercelProjectAvailableParams,
|
||
): Promise<IsVercelProjectAvailableResponse> {
|
||
return this.ipcRenderer.invoke("vercel:is-project-available", params);
|
||
}
|
||
|
||
public async createVercelProject(
|
||
params: CreateVercelProjectParams,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("vercel:create-project", params);
|
||
}
|
||
|
||
// Get Vercel Deployments
|
||
public async getVercelDeployments(
|
||
params: GetVercelDeploymentsParams,
|
||
): Promise<VercelDeployment[]> {
|
||
return this.ipcRenderer.invoke("vercel:get-deployments", params);
|
||
}
|
||
|
||
public async disconnectVercelProject(
|
||
params: DisconnectVercelProjectParams,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("vercel:disconnect", params);
|
||
}
|
||
// --- End Vercel Project Management ---
|
||
|
||
// Get the main app version
|
||
public async getAppVersion(): Promise<string> {
|
||
const result = await this.ipcRenderer.invoke("get-app-version");
|
||
return result.version as string;
|
||
}
|
||
|
||
// --- MCP Client Methods ---
|
||
public async listMcpServers() {
|
||
return this.ipcRenderer.invoke("mcp:list-servers");
|
||
}
|
||
|
||
public async createMcpServer(params: CreateMcpServer) {
|
||
return this.ipcRenderer.invoke("mcp:create-server", params);
|
||
}
|
||
|
||
public async updateMcpServer(params: McpServerUpdate) {
|
||
return this.ipcRenderer.invoke("mcp:update-server", params);
|
||
}
|
||
|
||
public async deleteMcpServer(id: number) {
|
||
return this.ipcRenderer.invoke("mcp:delete-server", id);
|
||
}
|
||
|
||
public async listMcpTools(serverId: number) {
|
||
return this.ipcRenderer.invoke("mcp:list-tools", serverId);
|
||
}
|
||
|
||
// Removed: upsertMcpTools and setMcpToolActive – tools are fetched dynamically at runtime
|
||
|
||
public async getMcpToolConsents() {
|
||
return this.ipcRenderer.invoke("mcp:get-tool-consents");
|
||
}
|
||
|
||
public async setMcpToolConsent(params: {
|
||
serverId: number;
|
||
toolName: string;
|
||
consent: "ask" | "always" | "denied";
|
||
}) {
|
||
return this.ipcRenderer.invoke("mcp:set-tool-consent", params);
|
||
}
|
||
|
||
public onMcpToolConsentRequest(
|
||
handler: (payload: {
|
||
requestId: string;
|
||
serverId: number;
|
||
serverName: string;
|
||
toolName: string;
|
||
toolDescription?: string | null;
|
||
inputPreview?: string | null;
|
||
}) => void,
|
||
) {
|
||
this.mcpConsentHandlers.set("consent", handler as any);
|
||
return () => {
|
||
this.mcpConsentHandlers.delete("consent");
|
||
};
|
||
}
|
||
|
||
public respondToMcpConsentRequest(
|
||
requestId: string,
|
||
decision: "accept-once" | "accept-always" | "decline",
|
||
) {
|
||
this.ipcRenderer.invoke("mcp:tool-consent-response", {
|
||
requestId,
|
||
decision,
|
||
});
|
||
}
|
||
|
||
// Get proposal details
|
||
public async getProposal(chatId: number): Promise<ProposalResult | null> {
|
||
try {
|
||
const data = await this.ipcRenderer.invoke("get-proposal", { chatId });
|
||
// Assuming the main process returns data matching the ProposalResult interface
|
||
// Add a type check/guard if necessary for robustness
|
||
return data as ProposalResult | null;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Example methods for listening to events (if needed)
|
||
// public on(channel: string, func: (...args: any[]) => void): void {
|
||
|
||
// --- Proposal Management ---
|
||
public async approveProposal({
|
||
chatId,
|
||
messageId,
|
||
}: {
|
||
chatId: number;
|
||
messageId: number;
|
||
}): Promise<ApproveProposalResult> {
|
||
return this.ipcRenderer.invoke("approve-proposal", {
|
||
chatId,
|
||
messageId,
|
||
});
|
||
}
|
||
|
||
public async rejectProposal({
|
||
chatId,
|
||
messageId,
|
||
}: {
|
||
chatId: number;
|
||
messageId: number;
|
||
}): Promise<void> {
|
||
await this.ipcRenderer.invoke("reject-proposal", {
|
||
chatId,
|
||
messageId,
|
||
});
|
||
}
|
||
// --- End Proposal Management ---
|
||
|
||
// --- Supabase Management ---
|
||
public async listSupabaseProjects(): Promise<any[]> {
|
||
return this.ipcRenderer.invoke("supabase:list-projects");
|
||
}
|
||
|
||
public async listSupabaseBranches(params: {
|
||
projectId: string;
|
||
}): Promise<SupabaseBranch[]> {
|
||
return this.ipcRenderer.invoke("supabase:list-branches", params);
|
||
}
|
||
|
||
public async setSupabaseAppProject(
|
||
params: SetSupabaseAppProjectParams,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("supabase:set-app-project", params);
|
||
}
|
||
|
||
public async unsetSupabaseAppProject(app: number): Promise<void> {
|
||
await this.ipcRenderer.invoke("supabase:unset-app-project", {
|
||
app,
|
||
});
|
||
}
|
||
|
||
public async fakeHandleSupabaseConnect(params: {
|
||
appId: number;
|
||
fakeProjectId: string;
|
||
}): Promise<void> {
|
||
await this.ipcRenderer.invoke(
|
||
"supabase:fake-connect-and-set-project",
|
||
params,
|
||
);
|
||
}
|
||
|
||
// --- End Supabase Management ---
|
||
|
||
// --- Neon Management ---
|
||
public async fakeHandleNeonConnect(): Promise<void> {
|
||
await this.ipcRenderer.invoke("neon:fake-connect");
|
||
}
|
||
|
||
public async createNeonProject(
|
||
params: CreateNeonProjectParams,
|
||
): Promise<NeonProject> {
|
||
return this.ipcRenderer.invoke("neon:create-project", params);
|
||
}
|
||
|
||
public async getNeonProject(
|
||
params: GetNeonProjectParams,
|
||
): Promise<GetNeonProjectResponse> {
|
||
return this.ipcRenderer.invoke("neon:get-project", params);
|
||
}
|
||
|
||
// --- End Neon Management ---
|
||
|
||
// --- Portal Management ---
|
||
public async portalMigrateCreate(params: {
|
||
appId: number;
|
||
}): Promise<{ output: string }> {
|
||
return this.ipcRenderer.invoke("portal:migrate-create", params);
|
||
}
|
||
|
||
// --- End Portal Management ---
|
||
|
||
public async getSystemDebugInfo(): Promise<SystemDebugInfo> {
|
||
return this.ipcRenderer.invoke("get-system-debug-info");
|
||
}
|
||
|
||
public async getChatLogs(chatId: number): Promise<ChatLogsData> {
|
||
return this.ipcRenderer.invoke("get-chat-logs", chatId);
|
||
}
|
||
|
||
public async uploadToSignedUrl(
|
||
url: string,
|
||
contentType: string,
|
||
data: any,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("upload-to-signed-url", {
|
||
url,
|
||
contentType,
|
||
data,
|
||
});
|
||
}
|
||
|
||
public async listLocalOllamaModels(): Promise<LocalModel[]> {
|
||
const response = await this.ipcRenderer.invoke("local-models:list-ollama");
|
||
return response?.models || [];
|
||
}
|
||
|
||
public async listLocalLMStudioModels(): Promise<LocalModel[]> {
|
||
const response = await this.ipcRenderer.invoke(
|
||
"local-models:list-lmstudio",
|
||
);
|
||
return response?.models || [];
|
||
}
|
||
|
||
// Listen for deep link events
|
||
public onDeepLinkReceived(
|
||
callback: (data: DeepLinkData) => void,
|
||
): () => void {
|
||
const listener = (data: any) => {
|
||
callback(data as DeepLinkData);
|
||
};
|
||
this.ipcRenderer.on("deep-link-received", listener);
|
||
return () => {
|
||
this.ipcRenderer.removeListener("deep-link-received", listener);
|
||
};
|
||
}
|
||
|
||
// Count tokens for a chat and input
|
||
public async countTokens(
|
||
params: TokenCountParams,
|
||
): Promise<TokenCountResult> {
|
||
try {
|
||
const result = await this.ipcRenderer.invoke("chat:count-tokens", params);
|
||
return result as TokenCountResult;
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Window control methods
|
||
public async minimizeWindow(): Promise<void> {
|
||
try {
|
||
await this.ipcRenderer.invoke("window:minimize");
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
public async maximizeWindow(): Promise<void> {
|
||
try {
|
||
await this.ipcRenderer.invoke("window:maximize");
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
public async closeWindow(): Promise<void> {
|
||
try {
|
||
await this.ipcRenderer.invoke("window:close");
|
||
} catch (error) {
|
||
showError(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Get system platform (win32, darwin, linux)
|
||
public async getSystemPlatform(): Promise<string> {
|
||
return this.ipcRenderer.invoke("get-system-platform");
|
||
}
|
||
|
||
public async doesReleaseNoteExist(
|
||
params: DoesReleaseNoteExistParams,
|
||
): Promise<{ exists: boolean; url?: string }> {
|
||
return this.ipcRenderer.invoke("does-release-note-exist", params);
|
||
}
|
||
|
||
public async getLanguageModelProviders(): Promise<LanguageModelProvider[]> {
|
||
return this.ipcRenderer.invoke("get-language-model-providers");
|
||
}
|
||
|
||
public async getLanguageModels(params: {
|
||
providerId: string;
|
||
}): Promise<LanguageModel[]> {
|
||
return this.ipcRenderer.invoke("get-language-models", params);
|
||
}
|
||
|
||
public async getLanguageModelsByProviders(): Promise<
|
||
Record<string, LanguageModel[]>
|
||
> {
|
||
return this.ipcRenderer.invoke("get-language-models-by-providers");
|
||
}
|
||
|
||
public async createCustomLanguageModelProvider({
|
||
id,
|
||
name,
|
||
apiBaseUrl,
|
||
envVarName,
|
||
}: CreateCustomLanguageModelProviderParams): Promise<LanguageModelProvider> {
|
||
return this.ipcRenderer.invoke("create-custom-language-model-provider", {
|
||
id,
|
||
name,
|
||
apiBaseUrl,
|
||
envVarName,
|
||
});
|
||
}
|
||
public async editCustomLanguageModelProvider(
|
||
params: CreateCustomLanguageModelProviderParams,
|
||
): Promise<LanguageModelProvider> {
|
||
return this.ipcRenderer.invoke(
|
||
"edit-custom-language-model-provider",
|
||
params,
|
||
);
|
||
}
|
||
|
||
public async createCustomLanguageModel(
|
||
params: CreateCustomLanguageModelParams,
|
||
): Promise<void> {
|
||
await this.ipcRenderer.invoke("create-custom-language-model", params);
|
||
}
|
||
|
||
public async deleteCustomLanguageModel(modelId: string): Promise<void> {
|
||
return this.ipcRenderer.invoke("delete-custom-language-model", modelId);
|
||
}
|
||
|
||
async deleteCustomModel(params: DeleteCustomModelParams): Promise<void> {
|
||
return this.ipcRenderer.invoke("delete-custom-model", params);
|
||
}
|
||
|
||
async deleteCustomLanguageModelProvider(providerId: string): Promise<void> {
|
||
return this.ipcRenderer.invoke("delete-custom-language-model-provider", {
|
||
providerId,
|
||
});
|
||
}
|
||
|
||
public async selectAppFolder(): Promise<{
|
||
path: string | null;
|
||
name: string | null;
|
||
}> {
|
||
return this.ipcRenderer.invoke("select-app-folder");
|
||
}
|
||
|
||
// Add these methods to IpcClient class
|
||
|
||
public async selectNodeFolder(): Promise<SelectNodeFolderResult> {
|
||
return this.ipcRenderer.invoke("select-node-folder");
|
||
}
|
||
|
||
public async getNodePath(): Promise<string | null> {
|
||
return this.ipcRenderer.invoke("get-node-path");
|
||
}
|
||
|
||
public async checkAiRules(params: {
|
||
path: string;
|
||
}): Promise<{ exists: boolean }> {
|
||
return this.ipcRenderer.invoke("check-ai-rules", params);
|
||
}
|
||
|
||
public async getLatestSecurityReview(
|
||
appId: number,
|
||
): Promise<SecurityReviewResult> {
|
||
return this.ipcRenderer.invoke("get-latest-security-review", appId);
|
||
}
|
||
|
||
public async importApp(params: ImportAppParams): Promise<ImportAppResult> {
|
||
return this.ipcRenderer.invoke("import-app", params);
|
||
}
|
||
|
||
async checkAppName(params: {
|
||
appName: string;
|
||
}): Promise<{ exists: boolean }> {
|
||
return this.ipcRenderer.invoke("check-app-name", params);
|
||
}
|
||
|
||
public async renameBranch(params: RenameBranchParams): Promise<void> {
|
||
await this.ipcRenderer.invoke("rename-branch", params);
|
||
}
|
||
|
||
async clearSessionData(): Promise<void> {
|
||
return this.ipcRenderer.invoke("clear-session-data");
|
||
}
|
||
|
||
// Method to get user budget information
|
||
public async getUserBudget(): Promise<UserBudgetInfo | null> {
|
||
return this.ipcRenderer.invoke("get-user-budget");
|
||
}
|
||
|
||
public async getChatContextResults(params: {
|
||
appId: number;
|
||
}): Promise<ContextPathResults> {
|
||
return this.ipcRenderer.invoke("get-context-paths", params);
|
||
}
|
||
|
||
public async setChatContext(params: {
|
||
appId: number;
|
||
chatContext: AppChatContext;
|
||
}): Promise<void> {
|
||
await this.ipcRenderer.invoke("set-context-paths", params);
|
||
}
|
||
|
||
public async getAppUpgrades(params: {
|
||
appId: number;
|
||
}): Promise<AppUpgrade[]> {
|
||
return this.ipcRenderer.invoke("get-app-upgrades", params);
|
||
}
|
||
|
||
public async executeAppUpgrade(params: {
|
||
appId: number;
|
||
upgradeId: string;
|
||
}): Promise<void> {
|
||
return this.ipcRenderer.invoke("execute-app-upgrade", params);
|
||
}
|
||
|
||
// Capacitor methods
|
||
public async isCapacitor(params: { appId: number }): Promise<boolean> {
|
||
return this.ipcRenderer.invoke("is-capacitor", params);
|
||
}
|
||
|
||
public async syncCapacitor(params: { appId: number }): Promise<void> {
|
||
return this.ipcRenderer.invoke("sync-capacitor", params);
|
||
}
|
||
|
||
public async openIos(params: { appId: number }): Promise<void> {
|
||
return this.ipcRenderer.invoke("open-ios", params);
|
||
}
|
||
|
||
public async openAndroid(params: { appId: number }): Promise<void> {
|
||
return this.ipcRenderer.invoke("open-android", params);
|
||
}
|
||
|
||
public async checkProblems(params: {
|
||
appId: number;
|
||
}): Promise<ProblemReport> {
|
||
return this.ipcRenderer.invoke("check-problems", params);
|
||
}
|
||
|
||
// Template methods
|
||
public async getTemplates(): Promise<Template[]> {
|
||
return this.ipcRenderer.invoke("get-templates");
|
||
}
|
||
|
||
// --- Prompts Library ---
|
||
public async listPrompts(): Promise<PromptDto[]> {
|
||
return this.ipcRenderer.invoke("prompts:list");
|
||
}
|
||
|
||
public async createPrompt(params: CreatePromptParamsDto): Promise<PromptDto> {
|
||
return this.ipcRenderer.invoke("prompts:create", params);
|
||
}
|
||
|
||
public async updatePrompt(params: UpdatePromptParamsDto): Promise<void> {
|
||
await this.ipcRenderer.invoke("prompts:update", params);
|
||
}
|
||
|
||
public async deletePrompt(id: number): Promise<void> {
|
||
await this.ipcRenderer.invoke("prompts:delete", id);
|
||
}
|
||
public async cloneRepoFromUrl(
|
||
params: CloneRepoParams,
|
||
): Promise<{ app: App; hasAiRules: boolean } | { error: string }> {
|
||
return this.ipcRenderer.invoke("github:clone-repo-from-url", params);
|
||
}
|
||
|
||
// --- Help bot ---
|
||
public startHelpChat(
|
||
sessionId: string,
|
||
message: string,
|
||
options: {
|
||
onChunk: (delta: string) => void;
|
||
onEnd: () => void;
|
||
onError: (error: string) => void;
|
||
},
|
||
): void {
|
||
this.helpStreams.set(sessionId, options);
|
||
this.ipcRenderer
|
||
.invoke("help:chat:start", { sessionId, message })
|
||
.catch((err) => {
|
||
this.helpStreams.delete(sessionId);
|
||
showError(err);
|
||
options.onError(String(err));
|
||
});
|
||
}
|
||
|
||
public cancelHelpChat(sessionId: string): void {
|
||
this.ipcRenderer.invoke("help:chat:cancel", sessionId).catch(() => {});
|
||
}
|
||
}
|