-
Notifications
You must be signed in to change notification settings - Fork 51
add cloud vm integration with yolocode e2b sandboxes #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fbb8716
616db32
2e7920b
f59a57a
ccb56d1
4a2bcd1
d3589aa
5562c19
8cb4d15
17135f7
f11525c
a9d8dc7
b367bf8
f94e525
f4c4a58
095f747
e916fc0
701610d
da53f04
d182a6d
f9ddbdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,185 @@ | ||||||||||||||||||||||||||||||||||
| import { execSync } from "node:child_process"; | ||||||||||||||||||||||||||||||||||
| import type { CloudSandbox } from "shared/types"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| interface CreateSandboxParams { | ||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||
| githubRepo?: string; | ||||||||||||||||||||||||||||||||||
| taskDescription?: string; | ||||||||||||||||||||||||||||||||||
| envVars?: Record<string, string>; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| interface CreateSandboxResponse { | ||||||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||
| template: string; | ||||||||||||||||||||||||||||||||||
| status: string; | ||||||||||||||||||||||||||||||||||
| createdAt: string; | ||||||||||||||||||||||||||||||||||
| metadata: { | ||||||||||||||||||||||||||||||||||
| userId: string; | ||||||||||||||||||||||||||||||||||
| userLogin: string; | ||||||||||||||||||||||||||||||||||
| displayName: string; | ||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||
| actualSandboxName: string; | ||||||||||||||||||||||||||||||||||
| githubRepo?: string; | ||||||||||||||||||||||||||||||||||
| autoPause: string; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| githubRepo?: string; | ||||||||||||||||||||||||||||||||||
| host: string; | ||||||||||||||||||||||||||||||||||
| websshHost: string; | ||||||||||||||||||||||||||||||||||
| claudeHost: string; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Client for interacting with yolocode cloud API | ||||||||||||||||||||||||||||||||||
| * Uses GitHub token for authentication | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| class CloudApiClient { | ||||||||||||||||||||||||||||||||||
| private baseUrl = "http://localhost:3001/api/e2b-sandboxes"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Get GitHub token from gh CLI | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| private getGithubToken(): string | null { | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const token = execSync("gh auth token", { | ||||||||||||||||||||||||||||||||||
| encoding: "utf-8", | ||||||||||||||||||||||||||||||||||
| stdio: ["pipe", "pipe", "pipe"], | ||||||||||||||||||||||||||||||||||
| }).trim(); | ||||||||||||||||||||||||||||||||||
| return token; | ||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||
| console.error("Failed to get GitHub token:", error); | ||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Create a new cloud sandbox | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| async createSandbox( | ||||||||||||||||||||||||||||||||||
| params: CreateSandboxParams, | ||||||||||||||||||||||||||||||||||
| ): Promise<{ success: boolean; sandbox?: CloudSandbox; error?: string }> { | ||||||||||||||||||||||||||||||||||
| const token = this.getGithubToken(); | ||||||||||||||||||||||||||||||||||
| if (!token) { | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||
| error: "GitHub authentication required. Please run 'gh auth login'", | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| // Get Claude Code auth token from .env.local | ||||||||||||||||||||||||||||||||||
| const claudeAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const requestBody = { | ||||||||||||||||||||||||||||||||||
| name: params.name, | ||||||||||||||||||||||||||||||||||
| template: "yolocode", | ||||||||||||||||||||||||||||||||||
| githubRepo: params.githubRepo, | ||||||||||||||||||||||||||||||||||
| taskDescription: params.taskDescription, | ||||||||||||||||||||||||||||||||||
| envVars: { | ||||||||||||||||||||||||||||||||||
| ...params.envVars, | ||||||||||||||||||||||||||||||||||
| ...(claudeAuthToken && { | ||||||||||||||||||||||||||||||||||
| CLAUDE_CODE_OAUTH_TOKEN: claudeAuthToken, | ||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Log request but mask sensitive data | ||||||||||||||||||||||||||||||||||
| console.log("Creating sandbox with params:", { | ||||||||||||||||||||||||||||||||||
| name: requestBody.name, | ||||||||||||||||||||||||||||||||||
| template: requestBody.template, | ||||||||||||||||||||||||||||||||||
| githubRepo: requestBody.githubRepo, | ||||||||||||||||||||||||||||||||||
| taskDescription: requestBody.taskDescription, | ||||||||||||||||||||||||||||||||||
| envVars: claudeAuthToken | ||||||||||||||||||||||||||||||||||
| ? { CLAUDE_CODE_OAUTH_TOKEN: "***" } | ||||||||||||||||||||||||||||||||||
| : undefined, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const response = await fetch(this.baseUrl, { | ||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${token}`, | ||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| body: JSON.stringify(requestBody), | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||
| const errorText = await response.text(); | ||||||||||||||||||||||||||||||||||
| console.error("API error:", errorText); | ||||||||||||||||||||||||||||||||||
| console.error("Response status:", response.status); | ||||||||||||||||||||||||||||||||||
| console.error("Response statusText:", response.statusText); | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||
| error: `Failed to create sandbox: ${response.statusText}. Details: ${errorText}`, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const data: CreateSandboxResponse = await response.json(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Override claudeHost to use port 7030 for web UI | ||||||||||||||||||||||||||||||||||
| const claudeHost = | ||||||||||||||||||||||||||||||||||
| data.claudeHost?.replace(/:\d+/, ":7030") || data.claudeHost; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const sandbox: CloudSandbox = { | ||||||||||||||||||||||||||||||||||
| id: data.id, | ||||||||||||||||||||||||||||||||||
| name: data.name, | ||||||||||||||||||||||||||||||||||
| status: "running", | ||||||||||||||||||||||||||||||||||
| websshHost: data.websshHost, | ||||||||||||||||||||||||||||||||||
| claudeHost: claudeHost, | ||||||||||||||||||||||||||||||||||
| createdAt: data.createdAt, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+123
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use actual API response status instead of hardcoding "running". Line 115 hardcodes Apply this diff to use the actual status: const sandbox: CloudSandbox = {
id: data.id,
name: data.name,
- status: "running",
+ status: data.status as CloudSandbox["status"],
websshHost: data.websshHost,
claudeHost: claudeHost,
createdAt: data.createdAt,
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| console.log("Created sandbox:", sandbox); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { success: true, sandbox }; | ||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||
| console.error("Failed to create sandbox:", error); | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : String(error), | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Delete a cloud sandbox | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| async deleteSandbox( | ||||||||||||||||||||||||||||||||||
| sandboxId: string, | ||||||||||||||||||||||||||||||||||
| ): Promise<{ success: boolean; error?: string }> { | ||||||||||||||||||||||||||||||||||
| const token = this.getGithubToken(); | ||||||||||||||||||||||||||||||||||
| if (!token) { | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||
| error: "GitHub authentication required", | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const response = await fetch(`${this.baseUrl}/${sandboxId}`, { | ||||||||||||||||||||||||||||||||||
| method: "DELETE", | ||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${token}`, | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||
| error: `Failed to delete sandbox: ${response.statusText}`, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||
| console.error("Failed to delete sandbox:", error); | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : String(error), | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export const cloudApiClient = new CloudApiClient(); | ||||||||||||||||||||||||||||||||||
| export default cloudApiClient; | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -771,4 +771,199 @@ export function registerWorkspaceIPCs() { | |
| }; | ||
| } | ||
| }); | ||
|
|
||
| // Cloud sandbox operations | ||
| ipcMain.handle( | ||
| "worktree-create-cloud-sandbox", | ||
| async (_event, input: { workspaceId: string; worktreeId: string }) => { | ||
| try { | ||
| const workspace = await workspaceManager.getWorkspace( | ||
| input.workspaceId, | ||
| ); | ||
| if (!workspace) { | ||
| return { | ||
| success: false, | ||
| error: "Workspace not found", | ||
| }; | ||
| } | ||
|
|
||
| const worktree = workspace.worktrees.find( | ||
| (wt) => wt.id === input.worktreeId, | ||
| ); | ||
| if (!worktree) { | ||
| return { | ||
| success: false, | ||
| error: "Worktree not found", | ||
| }; | ||
| } | ||
|
|
||
| // Get GitHub repo URL | ||
| let githubRepo: string | undefined; | ||
| try { | ||
| const { execSync } = await import("node:child_process"); | ||
| const remoteUrl = execSync("git remote get-url origin", { | ||
| cwd: workspace.repoPath, | ||
| encoding: "utf-8", | ||
| }).trim(); | ||
|
|
||
| // Convert git URL to repo format (owner/repo) | ||
| const match = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/); | ||
| if (match?.[1]) { | ||
| githubRepo = match[1]; | ||
| } | ||
| } catch (error) { | ||
| console.warn("Could not determine GitHub repo:", error); | ||
| } | ||
|
|
||
| // Import cloud API client | ||
| const { cloudApiClient } = await import("./cloud-api-client"); | ||
|
|
||
| // Create sandbox | ||
| const result = await cloudApiClient.createSandbox({ | ||
| name: `${workspace.name}-${worktree.branch}`, | ||
| githubRepo, | ||
| taskDescription: worktree.description || `Work on ${worktree.branch}`, | ||
| }); | ||
|
|
||
| if (!result.success || !result.sandbox) { | ||
| return result; | ||
| } | ||
|
|
||
| // Store sandbox info in worktree config | ||
| worktree.cloudSandbox = result.sandbox; | ||
| await workspaceManager.saveConfig(); | ||
|
|
||
| return result; | ||
| } catch (error) { | ||
| console.error("Failed to create cloud sandbox:", error); | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }; | ||
| } | ||
| }, | ||
| ); | ||
|
Comment on lines
+775
to
+845
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainGuard against duplicate sandboxes per worktree and avoid This handler looks solid overall (good workspace/worktree validation and config persistence), but:
Consider: const worktree = workspace.worktrees.find(
(wt) => wt.id === input.worktreeId,
);
if (!worktree) {
@@
};
}
+
+ if (worktree.cloudSandbox) {
+ return {
+ success: false,
+ error: "Cloud sandbox already exists for this worktree",
+ };
+ }
@@
- try {
- const { execSync } = await import("node:child_process");
- const remoteUrl = execSync("git remote get-url origin", {
- cwd: workspace.repoPath,
- encoding: "utf-8",
- }).trim();
+ try {
+ const { exec } = await import("node:child_process");
+ const { stdout } = await new Promise<{
+ stdout: string;
+ stderr: string;
+ }>((resolve, reject) =>
+ exec(
+ "git remote get-url origin",
+ { cwd: workspace.repoPath },
+ (err, stdout, stderr) =>
+ err ? reject(err) : resolve({ stdout, stderr }),
+ ),
+ );
+ const remoteUrl = stdout.trim();(Or similar async child-process usage that fits your existing patterns.) I'll verify the concerns raised in this review by examining the codebase. Let me gather information about the duplicate sandbox guard, the blocking 🏁 Script executed: #!/bin/bash
# Check full context of cloud sandbox handlers and search for existing guards/patterns
cd /tmp/repo
# 1. Get full handler implementation to check for duplicate guards
rg -n -A 100 "worktree-create-cloud-sandbox" apps/desktop/src/main/lib/workspace-ipcs.ts | head -120
# 2. Search for how cloudSandbox is used elsewhere
rg -n "cloudSandbox" apps/desktop/src/main/lib/workspace-ipcs.ts
# 3. Look for existing async child_process patterns in the file
rg -n -B 2 -A 5 "exec\(" apps/desktop/src/main/lib/workspace-ipcs.ts | head -60
# 4. Check if execSync is used elsewhere and how
rg -n "execSync" apps/desktop/src/main/lib/workspace-ipcs.tsLength of output: 395 Let me search the codebase with proper cloning enabled: 🏁 Script executed: #!/bin/bash
# Check full context of cloud sandbox handlers and existing patterns
# 1. Get full handler implementation to check for duplicate guards
rg -n -A 100 "worktree-create-cloud-sandbox" apps/desktop/src/main/lib/workspace-ipcs.ts | head -120
# 2. Search for how cloudSandbox is used elsewhere
rg -n "cloudSandbox" apps/desktop/src/main/lib/workspace-ipcs.ts
# 3. Look for existing async child_process patterns in the file
rg -n -B 2 -A 5 "exec\(" apps/desktop/src/main/lib/workspace-ipcs.ts | head -80
# 4. Check if execSync is used elsewhere and how
rg -n "execSync" apps/desktop/src/main/lib/workspace-ipcs.ts
# 5. Check other ipcMain.handle for duplicate state guards
rg -n -B 5 -A 10 "already exists" apps/desktop/src/main/lib/workspace-ipcs.ts | head -40Length of output: 4194 Guard against duplicate sandboxes and replace The handler creates multiple cloud sandboxes for the same worktree without checking if one already exists, which leaks VMs since only the latest is tracked in Add a guard check and switch to async const worktree = workspace.worktrees.find(
(wt) => wt.id === input.worktreeId,
);
if (!worktree) {
return {
success: false,
error: "Worktree not found",
};
}
+
+ if (worktree.cloudSandbox) {
+ return {
+ success: false,
+ error: "Cloud sandbox already exists for this worktree",
+ };
+ }
// Get GitHub repo URL
let githubRepo: string | undefined;
try {
- const { execSync } = await import("node:child_process");
- const remoteUrl = execSync("git remote get-url origin", {
- cwd: workspace.repoPath,
- encoding: "utf-8",
- }).trim();
+ const { exec } = await import("node:child_process");
+ const remoteUrl = await new Promise<string>((resolve, reject) =>
+ exec(
+ "git remote get-url origin",
+ { cwd: workspace.repoPath },
+ (err, stdout) =>
+ err ? reject(err) : resolve(stdout.trim()),
+ ),
+ ); |
||
|
|
||
| ipcMain.handle( | ||
| "worktree-open-cloud-sandbox", | ||
| async (_event, input: { workspaceId: string; worktreeId: string }) => { | ||
| try { | ||
| const workspace = await workspaceManager.getWorkspace( | ||
| input.workspaceId, | ||
| ); | ||
| if (!workspace) { | ||
| return { | ||
| success: false, | ||
| error: "Workspace not found", | ||
| }; | ||
| } | ||
|
|
||
| const worktree = workspace.worktrees.find( | ||
| (wt) => wt.id === input.worktreeId, | ||
| ); | ||
| if (!worktree) { | ||
| return { | ||
| success: false, | ||
| error: "Worktree not found", | ||
| }; | ||
| } | ||
|
|
||
| if (!worktree.cloudSandbox?.claudeHost) { | ||
| return { | ||
| success: false, | ||
| error: "No cloud sandbox found for this worktree", | ||
| }; | ||
| } | ||
|
|
||
| // Open Claude host in browser | ||
| const url = worktree.cloudSandbox.claudeHost.startsWith("http") | ||
| ? worktree.cloudSandbox.claudeHost | ||
| : `https://${worktree.cloudSandbox.claudeHost}`; | ||
|
|
||
| await shell.openExternal(url); | ||
|
|
||
| return { success: true }; | ||
| } catch (error) { | ||
| console.error("Failed to open cloud sandbox:", error); | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| ipcMain.handle( | ||
| "worktree-delete-cloud-sandbox", | ||
| async (_event, input: { workspaceId: string; worktreeId: string }) => { | ||
| try { | ||
| const workspace = await workspaceManager.getWorkspace( | ||
| input.workspaceId, | ||
| ); | ||
| if (!workspace) { | ||
| return { | ||
| success: false, | ||
| error: "Workspace not found", | ||
| }; | ||
| } | ||
|
|
||
| const worktree = workspace.worktrees.find( | ||
| (wt) => wt.id === input.worktreeId, | ||
| ); | ||
| if (!worktree) { | ||
| return { | ||
| success: false, | ||
| error: "Worktree not found", | ||
| }; | ||
| } | ||
|
|
||
| if (!worktree.cloudSandbox) { | ||
| return { | ||
| success: false, | ||
| error: "No cloud sandbox found for this worktree", | ||
| }; | ||
| } | ||
|
|
||
| // Import cloud API client | ||
| const { cloudApiClient } = await import("./cloud-api-client"); | ||
|
|
||
| // Delete sandbox | ||
| const result = await cloudApiClient.deleteSandbox( | ||
| worktree.cloudSandbox.id, | ||
| ); | ||
|
|
||
| if (result.success) { | ||
| // Remove sandbox info from worktree config | ||
| delete worktree.cloudSandbox; | ||
| await workspaceManager.saveConfig(); | ||
| } | ||
|
|
||
| return result; | ||
| } catch (error) { | ||
| console.error("Failed to delete cloud sandbox:", error); | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| // Delete cloud sandbox by ID directly (doesn't require worktree) | ||
| ipcMain.handle( | ||
| "cloud-sandbox-delete-by-id", | ||
| async (_event, input: { sandboxId: string }) => { | ||
| try { | ||
| const { cloudApiClient } = await import("./cloud-api-client"); | ||
| const result = await cloudApiClient.deleteSandbox(input.sandboxId); | ||
| return result; | ||
| } catch (error) { | ||
| console.error("Failed to delete cloud sandbox by ID:", error); | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }; | ||
| } | ||
| }, | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use async alternatives to avoid blocking the main thread.
execSyncblocks the Electron main process, freezing the UI while waiting for theghCLI command to complete. In desktop applications, this creates a poor user experience.Refactor to use async
execfromnode:child_process/promises:🤖 Prompt for AI Agents