Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fbb8716
add yolocode cloud sandbox integration
caffeinum Nov 11, 2025
616db32
add new cloud worktree button
caffeinum Nov 11, 2025
2e7920b
create preview tab before refreshing ui
caffeinum Nov 11, 2025
f59a57a
expand worktree and select tab after cloud worktree creation
caffeinum Nov 11, 2025
ccb56d1
add template id and better error logging for cloud sandbox creation
caffeinum Nov 11, 2025
4a2bcd1
use port 7030 for cloud ide web ui
caffeinum Nov 11, 2025
d3589aa
use random two-word names for cloud worktrees instead of timestamps
caffeinum Nov 11, 2025
5562c19
pass claude auth token to cloud VMs via env vars
caffeinum Nov 11, 2025
8cb4d15
add kill vm option to cloud ide tab context menu
caffeinum Nov 11, 2025
17135f7
fix cloud ide tab detection by checking url instead of name
caffeinum Nov 11, 2025
f11525c
add debug logging for cloud ide tab detection
caffeinum Nov 11, 2025
a9d8dc7
simplify cloud ide tab detection - just check for e2b.app url
caffeinum Nov 11, 2025
b367bf8
add direct sandbox ID deletion via url extraction
caffeinum Nov 11, 2025
f94e525
add bidirectional vm/worktree deletion
caffeinum Nov 11, 2025
f4c4a58
fix sandbox deletion and token leaks
caffeinum Nov 11, 2025
095f747
use localhost:3001 for e2b sandbox api temporarily
caffeinum Nov 11, 2025
e916fc0
add loading state to kill vm button
caffeinum Nov 12, 2025
701610d
add timestamp to cloud worktree branch names for uniqueness
caffeinum Nov 12, 2025
da53f04
pass CLAUDE_CODE_OAUTH_TOKEN to vm env vars
caffeinum Nov 12, 2025
d182a6d
restore
caffeinum Nov 19, 2025
f9ddbdf
replace removeListener with off for ipc event cleanup
caffeinum Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions apps/desktop/src/main/lib/cloud-api-client.ts
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;
}
}
Comment on lines +42 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use async alternatives to avoid blocking the main thread.

execSync blocks the Electron main process, freezing the UI while waiting for the gh CLI command to complete. In desktop applications, this creates a poor user experience.

Refactor to use async exec from node:child_process/promises:

+import { exec } from "node:child_process";
+import { promisify } from "node:util";
+
+const execAsync = promisify(exec);
+
 	/**
 	 * Get GitHub token from gh CLI
 	 */
-	private getGithubToken(): string | null {
+	private async getGithubToken(): Promise<string | null> {
 		try {
-			const token = execSync("gh auth token", {
+			const { stdout } = await execAsync("gh auth token", {
 				encoding: "utf-8",
-				stdio: ["pipe", "pipe", "pipe"],
-			}).trim();
-			return token;
+			});
+			return stdout.trim();
 		} catch (error) {
 			console.error("Failed to get GitHub token:", error);
 			return null;
 		}
 	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/main/lib/cloud-api-client.ts around lines 42 to 53, the sync
execSync call blocks the Electron main thread; change getGithubToken to an async
function that uses exec from node:child_process/promises, await exec("gh auth
token"), read stdout.trim() and return it (or null on error), catch and log
errors, and update any callers to await the new Promise-returning getGithubToken
so the main process is non-blocking.


/**
* 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use actual API response status instead of hardcoding "running".

Line 115 hardcodes status: "running" regardless of the actual status returned by the API (data.status). This could mask sandbox creation failures or delays.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sandbox: CloudSandbox = {
id: data.id,
name: data.name,
status: "running",
websshHost: data.websshHost,
claudeHost: claudeHost,
createdAt: data.createdAt,
};
const sandbox: CloudSandbox = {
id: data.id,
name: data.name,
status: data.status as CloudSandbox["status"],
websshHost: data.websshHost,
claudeHost: claudeHost,
createdAt: data.createdAt,
};
🤖 Prompt for AI Agents
In apps/desktop/src/main/lib/cloud-api-client.ts around lines 112 to 119, the
sandbox object incorrectly hardcodes status: "running"; change it to use the API
response by assigning status: data.status (and optionally fallback to a safe
value like 'unknown' if data.status is missing) so the client reflects the real
sandbox state returned by the server.


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;
195 changes: 195 additions & 0 deletions apps/desktop/src/main/lib/workspace-ipcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Guard against duplicate sandboxes per worktree and avoid execSync in the main process.

This handler looks solid overall (good workspace/worktree validation and config persistence), but:

  • It will happily create multiple cloud sandboxes for the same worktree if called repeatedly, only tracking the last one in worktree.cloudSandbox.
  • It shells out with execSync("git remote get-url origin") on the main thread, which can briefly freeze the UI on slow repos.

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 execSync usage, and existing async patterns.


🏁 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.ts

Length 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 -40

Length of output: 4194


Guard against duplicate sandboxes and replace execSync with async exec to prevent UI blocking.

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 worktree.cloudSandbox. Additionally, execSync("git remote get-url origin") blocks the main thread on line 804, freezing the UI during the git operation. Other handlers in the same file guard state checks (e.g., worktree-open-cloud-sandbox at line 871), establishing the pattern you should follow.

Add a guard check and switch to async exec:

 				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),
};
}
},
);
}
Loading
Loading