From fbb8716a9426df42ba46be07b7bcd6c0277cc988 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 00:27:49 -0800 Subject: [PATCH 01/21] add yolocode cloud sandbox integration enables one-click creation of cloud coding environments (with claude code) for any worktree via yolocode api --- apps/desktop/src/main/lib/cloud-api-client.ts | 153 +++++++++++++++ apps/desktop/src/main/lib/workspace-ipcs.ts | 177 ++++++++++++++++++ .../main/components/Layout/NewLayoutMain.tsx | 120 ++++++++++-- apps/desktop/src/shared/ipc-channels.ts | 1 - .../src/shared/ipc-channels/worktree.ts | 21 ++- apps/desktop/src/shared/types.ts | 12 ++ 6 files changed, 466 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/main/lib/cloud-api-client.ts diff --git a/apps/desktop/src/main/lib/cloud-api-client.ts b/apps/desktop/src/main/lib/cloud-api-client.ts new file mode 100644 index 00000000000..7d25c9028c0 --- /dev/null +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -0,0 +1,153 @@ +import { execSync } from "node:child_process"; +import type { CloudSandbox } from "shared/types"; + +interface CreateSandboxParams { + name: string; + githubRepo?: string; + taskDescription?: 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 = "https://staging.yolocode.ai/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 { + const response = await fetch(this.baseUrl, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: params.name, + githubRepo: params.githubRepo, + taskDescription: params.taskDescription, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("API error:", errorText); + return { + success: false, + error: `Failed to create sandbox: ${response.statusText}`, + }; + } + + const data: CreateSandboxResponse = await response.json(); + + const sandbox: CloudSandbox = { + id: data.id, + name: data.name, + status: "running", + websshHost: data.websshHost, + claudeHost: data.claudeHost, + createdAt: data.createdAt, + }; + + 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; diff --git a/apps/desktop/src/main/lib/workspace-ipcs.ts b/apps/desktop/src/main/lib/workspace-ipcs.ts index 4f44ca803f8..c92ebca8ff6 100644 --- a/apps/desktop/src/main/lib/workspace-ipcs.ts +++ b/apps/desktop/src/main/lib/workspace-ipcs.ts @@ -771,4 +771,181 @@ 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), + }; + } + }, + ); + + 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), + }; + } + }, + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx index 39fb62cfbed..b489792f68d 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx @@ -240,12 +240,12 @@ function enrichWorktreesWithTasks( isPending: true, // Mark as pending for UI task: pending.taskData ? { - id: pending.id, - slug: pending.taskData.slug, - title: pending.taskData.name, - status: pending.taskData.status, - description: pending.description || "", - } + id: pending.id, + slug: pending.taskData.slug, + title: pending.taskData.name, + status: pending.taskData.status, + description: pending.description || "", + } : undefined, }), ); @@ -289,7 +289,9 @@ export const MainLayout: React.FC = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [showSidebarOverlay, setShowSidebarOverlay] = useState(false); const [isAddTaskModalOpen, setIsAddTaskModalOpen] = useState(false); - const [addTaskModalInitialMode, setAddTaskModalInitialMode] = useState<"list" | "new">("list"); + const [addTaskModalInitialMode, setAddTaskModalInitialMode] = useState< + "list" | "new" + >("list"); const [branches, setBranches] = useState([]); const [isCreatingWorktree, setIsCreatingWorktree] = useState(false); const [setupStatus, setSetupStatus] = useState(undefined); @@ -526,7 +528,10 @@ export const MainLayout: React.FC = () => { // If we deleted the selected worktree, select the first available one if (selectedWorktreeId === worktreeId) { - if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) { + if ( + refreshedWorkspace.worktrees && + refreshedWorkspace.worktrees.length > 0 + ) { const firstWorktree = refreshedWorkspace.worktrees[0]; setSelectedWorktreeId(firstWorktree.id); if (firstWorktree.tabs && firstWorktree.tabs.length > 0) { @@ -581,7 +586,7 @@ export const MainLayout: React.FC = () => { const handleOpenAddTaskModal = (mode: "list" | "new" = "list") => { setAddTaskModalInitialMode(mode); setIsAddTaskModalOpen(true); - + // Fetch branches when opening in new mode if (mode === "new" && currentWorkspace) { void (async () => { @@ -715,7 +720,10 @@ export const MainLayout: React.FC = () => { }), }); - window.ipcRenderer.removeListener("worktree-setup-progress", progressHandler); + window.ipcRenderer.removeListener( + "worktree-setup-progress", + progressHandler, + ); if (result.success) { // Display setup result if available @@ -747,8 +755,12 @@ export const MainLayout: React.FC = () => { handleTabSelect(result.worktree.id, result.worktree.tabs[0].id); } } + <<<<<<< HEAD:apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx } else { - console.error("[NewLayoutMain] Failed to create worktree:", result.error); + console.error( + "[NewLayoutMain] Failed to create worktree:", + result.error, + ); setSetupStatus("Failed to create worktree"); setSetupOutput(result.error); setIsCreatingWorktree(false); @@ -759,7 +771,10 @@ export const MainLayout: React.FC = () => { setSetupStatus("Error creating worktree"); setSetupOutput(String(error)); setIsCreatingWorktree(false); - window.ipcRenderer.removeListener("worktree-setup-progress", progressHandler); + window.ipcRenderer.removeListener( + "worktree-setup-progress", + progressHandler, + ); } }; @@ -838,6 +853,77 @@ export const MainLayout: React.FC = () => { } }; + const handleCreateCloudSandbox = async () => { + if (!currentWorkspace || !selectedWorktreeId) return; + + const worktree = currentWorkspace.worktrees?.find( + (wt) => wt.id === selectedWorktreeId, + ); + if (!worktree) return; + + try { + const result = await window.ipcRenderer.invoke( + "worktree-create-cloud-sandbox", + { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + }, + ); + + if (result.success) { + // Reload workspace to show updated sandbox state + const refreshedWorkspace = await window.ipcRenderer.invoke( + "workspace-get", + currentWorkspace.id, + ); + if (refreshedWorkspace) { + setCurrentWorkspace(refreshedWorkspace); + } + alert("Cloud sandbox created! Opening in browser..."); + + // Auto-open the sandbox + if (result.sandbox?.claudeHost) { + await window.ipcRenderer.invoke("worktree-open-cloud-sandbox", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + }); + } + } else { + alert( + `Failed to create cloud sandbox: ${result.error || "Unknown error"}`, + ); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + alert(`Failed to create cloud sandbox: ${errorMessage}`); + } + }; + + const handleOpenCloudSandbox = async () => { + if (!currentWorkspace || !selectedWorktreeId) return; + + try { + const result = await window.ipcRenderer.invoke( + "worktree-open-cloud-sandbox", + { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + }, + ); + + if (!result.success) { + alert( + `Failed to open cloud sandbox: ${result.error || "Unknown error"}`, + ); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + alert(`Failed to open cloud sandbox: ${errorMessage}`); + } + }; + // Load active workspace on mount useEffect(() => { const loadActiveWorkspace = async () => { @@ -960,6 +1046,8 @@ export const MainLayout: React.FC = () => { onAddTask={handleOpenAddTaskModal} onCreatePR={handleCreatePR} onMergePR={handleMergePR} + onCreateCloudSandbox={handleCreateCloudSandbox} + onOpenCloudSandbox={handleOpenCloudSandbox} worktrees={enrichWorktreesWithTasks( currentWorkspace?.worktrees || [], pendingWorktrees, @@ -1035,10 +1123,10 @@ export const MainLayout: React.FC = () => { {/* Main content panel */} {loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( + error || + !currentWorkspace || + !selectedTab || + !selectedWorktree ? ( Date: Tue, 11 Nov 2025 01:20:39 -0800 Subject: [PATCH 02/21] add new cloud worktree button creates worktree + cloud sandbox + preview tab in one click --- .../main/components/Sidebar/Sidebar.tsx | 82 ++++++++++++++++++- .../CreateWorktreeButton.tsx | 54 +++++++++--- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx index 8f6c0bba9e4..cf09136a539 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx @@ -12,6 +12,7 @@ import { import { FileTree } from "../DiffView"; import type { FileDiff } from "../DiffView/types"; import { + CreateWorktreeButton, CreateWorktreeModal, WorktreeList, } from "./components"; @@ -26,14 +27,18 @@ export function Sidebar({ isDragging = false, onDiffModeChange, }: SidebarProps) { - const { workspaces, currentWorkspace, handleWorkspaceSelect } = useWorkspaceContext(); - const { selectedTabId, selectedWorktreeId, handleTabSelect } = useTabContext(); - const { handleWorktreeCreated, handleUpdateWorktree } = useWorktreeOperationsContext(); + const { workspaces, currentWorkspace, handleWorkspaceSelect } = + useWorkspaceContext(); + const { selectedTabId, selectedWorktreeId, handleTabSelect } = + useTabContext(); + const { handleWorktreeCreated, handleUpdateWorktree } = + useWorktreeOperationsContext(); const { handleCollapseSidebar } = useSidebarContext(); const [expandedWorktrees, setExpandedWorktrees] = useState>( new Set(), ); const [isCreatingWorktree, setIsCreatingWorktree] = useState(false); + const [isCreatingCloudWorktree, setIsCreatingCloudWorktree] = useState(false); const [isScanningWorktrees, setIsScanningWorktrees] = useState(false); const [showWorktreeModal, setShowWorktreeModal] = useState(false); const [title, setTitle] = useState(""); @@ -63,7 +68,8 @@ export function Sidebar({ }; window.addEventListener("workspace-changed", handleWorkspaceChanged); - return () => window.removeEventListener("workspace-changed", handleWorkspaceChanged); + return () => + window.removeEventListener("workspace-changed", handleWorkspaceChanged); }, [handleWorktreeCreated]); // Fetch diff data when in changes mode @@ -173,6 +179,65 @@ export function Sidebar({ setShowWorktreeModal(true); }; + const handleCreateCloudWorktree = async () => { + if (!currentWorkspace) return; + + // For now, create a simple worktree and immediately create a cloud sandbox for it + const title = "Cloud Development"; + const branch = `cloud-dev-${Date.now()}`; + + try { + setIsCreatingCloudWorktree(true); + + // Create worktree + const result = await window.ipcRenderer.invoke("worktree-create", { + workspaceId: currentWorkspace.id, + title, + branch, + createBranch: true, + description: "Cloud development environment", + }); + + if (result.success && result.worktree) { + onWorktreeCreated(); + + // Immediately create cloud sandbox for this worktree + const sandboxResult = await window.ipcRenderer.invoke( + "worktree-create-cloud-sandbox", + { + workspaceId: currentWorkspace.id, + worktreeId: result.worktree.id, + }, + ); + + if (sandboxResult.success && sandboxResult.sandbox?.claudeHost) { + // Create a preview tab with the claude host URL + const claudeUrl = sandboxResult.sandbox.claudeHost.startsWith("http") + ? sandboxResult.sandbox.claudeHost + : `https://${sandboxResult.sandbox.claudeHost}`; + + await window.ipcRenderer.invoke("tab-create", { + workspaceId: currentWorkspace.id, + worktreeId: result.worktree.id, + name: "Cloud IDE", + type: "preview", + url: claudeUrl, + }); + } + } else { + alert( + `Failed to create cloud worktree: ${result.error || "Unknown error"}`, + ); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + alert(`Failed to create cloud worktree: ${errorMessage}`); + } finally { + setIsCreatingCloudWorktree(false); + } + }; + const handleCloneWorktree = (worktreeId: string, branch: string) => { // Pre-populate modal for cloning: use the clicked worktree's branch as source // and clone its tabs to the new worktree @@ -387,6 +452,15 @@ export function Sidebar({ } showWorkspaceHeader={true} /> + + {currentWorkspace && ( + + )} ); }} diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/CreateWorktreeButton/CreateWorktreeButton.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/CreateWorktreeButton/CreateWorktreeButton.tsx index de0c126aecd..dfe7606373b 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/CreateWorktreeButton/CreateWorktreeButton.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/CreateWorktreeButton/CreateWorktreeButton.tsx @@ -1,26 +1,56 @@ import { Button } from "@superset/ui/button"; -import { Plus } from "lucide-react"; +import { Cloud, Loader2, Plus } from "lucide-react"; interface CreateWorktreeButtonProps { onClick: () => void; + onCreateCloud?: () => void; isCreating: boolean; + isCreatingCloud?: boolean; } export function CreateWorktreeButton({ onClick, + onCreateCloud, isCreating, + isCreatingCloud = false, }: CreateWorktreeButtonProps) { return ( - +
+ + + {onCreateCloud && ( + + )} +
); } From 2e7920b64737d986a771d423cf09c7ea9acf5bb0 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 01:23:01 -0800 Subject: [PATCH 03/21] create preview tab before refreshing ui ensures worktree shows with cloud ide tab already loaded --- .../src/renderer/screens/main/components/Sidebar/Sidebar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx index cf09136a539..12e44e17108 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx @@ -199,8 +199,6 @@ export function Sidebar({ }); if (result.success && result.worktree) { - onWorktreeCreated(); - // Immediately create cloud sandbox for this worktree const sandboxResult = await window.ipcRenderer.invoke( "worktree-create-cloud-sandbox", @@ -224,6 +222,9 @@ export function Sidebar({ url: claudeUrl, }); } + + // Refresh UI after everything is created + onWorktreeCreated(); } else { alert( `Failed to create cloud worktree: ${result.error || "Unknown error"}`, From f59a57a5b085f4a2e21cfe0b61c65f4acc3a5c46 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 01:24:29 -0800 Subject: [PATCH 04/21] expand worktree and select tab after cloud worktree creation --- .../screens/main/components/Sidebar/Sidebar.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx index 12e44e17108..bf27912b813 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx @@ -214,13 +214,25 @@ export function Sidebar({ ? sandboxResult.sandbox.claudeHost : `https://${sandboxResult.sandbox.claudeHost}`; - await window.ipcRenderer.invoke("tab-create", { + const tabResult = await window.ipcRenderer.invoke("tab-create", { workspaceId: currentWorkspace.id, worktreeId: result.worktree.id, name: "Cloud IDE", type: "preview", url: claudeUrl, }); + + console.log("Tab creation result:", tabResult); + + // Expand the worktree and select the newly created tab + if (tabResult.success && tabResult.tab && result.worktree) { + setExpandedWorktrees((prev) => { + const next = new Set(prev); + next.add(result.worktree!.id); + return next; + }); + onTabSelect(result.worktree.id, tabResult.tab.id); + } } // Refresh UI after everything is created From ccb56d195bb06c8484d44b9f8334e3d1b6ab833b Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 01:28:49 -0800 Subject: [PATCH 05/21] add template id and better error logging for cloud sandbox creation --- apps/desktop/src/main/lib/cloud-api-client.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/lib/cloud-api-client.ts b/apps/desktop/src/main/lib/cloud-api-client.ts index 7d25c9028c0..d59c59b2077 100644 --- a/apps/desktop/src/main/lib/cloud-api-client.ts +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -66,25 +66,32 @@ class CloudApiClient { } try { + const requestBody = { + name: params.name, + template: "yolocode", + githubRepo: params.githubRepo, + taskDescription: params.taskDescription, + }; + + console.log("Creating sandbox with params:", requestBody); + const response = await fetch(this.baseUrl, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, - body: JSON.stringify({ - name: params.name, - githubRepo: params.githubRepo, - taskDescription: params.taskDescription, - }), + 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}`, + error: `Failed to create sandbox: ${response.statusText}. Details: ${errorText}`, }; } From 4a2bcd12b63359be23dc2cb442925681e4959b3f Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 01:29:22 -0800 Subject: [PATCH 06/21] use port 7030 for cloud ide web ui --- apps/desktop/src/main/lib/cloud-api-client.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/lib/cloud-api-client.ts b/apps/desktop/src/main/lib/cloud-api-client.ts index d59c59b2077..4288debf922 100644 --- a/apps/desktop/src/main/lib/cloud-api-client.ts +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -97,15 +97,21 @@ class CloudApiClient { 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: data.claudeHost, + claudeHost: claudeHost, createdAt: data.createdAt, }; + console.log("Created sandbox:", sandbox); + return { success: true, sandbox }; } catch (error) { console.error("Failed to create sandbox:", error); From d3589aa8caf2491dbbe3c7e00ffba7c610231626 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 01:31:07 -0800 Subject: [PATCH 07/21] use random two-word names for cloud worktrees instead of timestamps --- .../main/components/Sidebar/Sidebar.tsx | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx index bf27912b813..a2e01587b8a 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx @@ -182,9 +182,38 @@ export function Sidebar({ const handleCreateCloudWorktree = async () => { if (!currentWorkspace) return; + // Generate random two-word name + const adjectives = [ + "happy", + "sleepy", + "brave", + "clever", + "gentle", + "bright", + "calm", + "bold", + "swift", + "quiet", + ]; + const nouns = [ + "cat", + "fox", + "owl", + "bear", + "wolf", + "deer", + "hawk", + "lynx", + "seal", + "dove", + ]; + const randomAdj = adjectives[Math.floor(Math.random() * adjectives.length)]; + const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]; + const randomName = `${randomAdj}-${randomNoun}`; + // For now, create a simple worktree and immediately create a cloud sandbox for it - const title = "Cloud Development"; - const branch = `cloud-dev-${Date.now()}`; + const title = `Cloud ${randomAdj} ${randomNoun}`; + const branch = `cloud-dev-${randomName}`; try { setIsCreatingCloudWorktree(true); From 5562c193e4e8a99ad2d0ea4d799b19d339adf189 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 12:31:44 -0800 Subject: [PATCH 08/21] pass claude auth token to cloud VMs via env vars - add support for passing CLAUDE_CODE_OAUTH_TOKEN from .env to VMs - cloud sandboxes now receive ANTHROPIC_AUTH_TOKEN env var - enables claude code in VMs to authenticate with user credentials - see docs: https://gist.github.com/caffeinum/c1e68a6f8fe66f7bd06b9509559d701a --- apps/desktop/src/main/lib/cloud-api-client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/desktop/src/main/lib/cloud-api-client.ts b/apps/desktop/src/main/lib/cloud-api-client.ts index 4288debf922..48efb15568d 100644 --- a/apps/desktop/src/main/lib/cloud-api-client.ts +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -5,6 +5,7 @@ interface CreateSandboxParams { name: string; githubRepo?: string; taskDescription?: string; + envVars?: Record; } interface CreateSandboxResponse { @@ -66,11 +67,18 @@ class CloudApiClient { } 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 }), + }, }; console.log("Creating sandbox with params:", requestBody); From 8cb4d1538085a1b4c897b6cb25ea7062e606aede Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 12:36:13 -0800 Subject: [PATCH 09/21] add kill vm option to cloud ide tab context menu --- .../components/TabItem/TabItem.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx index 72ff0e0209f..9e8327ae109 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx @@ -6,6 +6,7 @@ import { ContextMenuTrigger, } from "@superset/ui/context-menu"; import { + Cloud, Edit2, FolderOutput, FolderTree, @@ -124,11 +125,49 @@ export function TabItem({ } }; + const handleKillVM = async () => { + if (!workspaceId || !worktreeId) return; + + const confirmed = window.confirm( + "Are you sure you want to delete this cloud sandbox? This cannot be undone.", + ); + if (!confirmed) return; + + try { + const result = await window.ipcRenderer.invoke( + "worktree-delete-cloud-sandbox", + { + workspaceId, + worktreeId, + }, + ); + + if (result.success) { + // Close the tab after successful deletion + onTabRemove?.(tab.id); + } else { + alert( + `Failed to delete cloud sandbox: ${result.error || "Unknown error"}`, + ); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + alert(`Failed to delete cloud sandbox: ${errorMessage}`); + } + }; + const isSelected = selectedTabId === tab.id; const isMultiSelected = selectedTabIds.has(tab.id); const showMultiSelectHighlight = isMultiSelected && selectedTabIds.size > 1; const isInsideGroup = !!parentTabId; + // Check if this is a Cloud IDE tab + const isCloudIDETab = + tab.type === "preview" && + tab.name === "Cloud IDE" && + worktree?.cloudSandbox; + const IconComponent = (() => { switch (tab.type) { case "preview": @@ -201,6 +240,12 @@ export function TabItem({ Group {selectedTabIds.size} Tabs )} + {isCloudIDETab && ( + + + Kill VM + + )} ); From 17135f79d3939adab24aa9835f9d76ec4029c50c Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 12:52:50 -0800 Subject: [PATCH 10/21] fix cloud ide tab detection by checking url instead of name --- .../components/WorktreeItem/components/TabItem/TabItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx index 9e8327ae109..92cb013e5c0 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx @@ -165,7 +165,7 @@ export function TabItem({ // Check if this is a Cloud IDE tab const isCloudIDETab = tab.type === "preview" && - tab.name === "Cloud IDE" && + tab.url?.includes("e2b.app") && worktree?.cloudSandbox; const IconComponent = (() => { From f11525c052f9b712dd5342f17c61a23e782dd8c6 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 12:54:37 -0800 Subject: [PATCH 11/21] add debug logging for cloud ide tab detection --- .../WorktreeItem/components/TabItem/TabItem.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx index 92cb013e5c0..f9ecfb60291 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx @@ -168,6 +168,16 @@ export function TabItem({ tab.url?.includes("e2b.app") && worktree?.cloudSandbox; + // Debug logging + if (tab.type === "preview" && tab.url?.includes("e2b.app")) { + console.log("Cloud IDE tab detected:", { + tabName: tab.name, + tabUrl: tab.url, + hasCloudSandbox: !!worktree?.cloudSandbox, + isCloudIDETab, + }); + } + const IconComponent = (() => { switch (tab.type) { case "preview": From a9d8dc7bc924e0fc7af7b90ffd238fc38a1c1d13 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 12:55:51 -0800 Subject: [PATCH 12/21] simplify cloud ide tab detection - just check for e2b.app url --- .../WorktreeItem/components/TabItem/TabItem.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx index f9ecfb60291..68ac7de05eb 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx @@ -163,20 +163,9 @@ export function TabItem({ const isInsideGroup = !!parentTabId; // Check if this is a Cloud IDE tab + // Just check if it's a preview tab with e2b.app URL (cloud sandbox URL) const isCloudIDETab = - tab.type === "preview" && - tab.url?.includes("e2b.app") && - worktree?.cloudSandbox; - - // Debug logging - if (tab.type === "preview" && tab.url?.includes("e2b.app")) { - console.log("Cloud IDE tab detected:", { - tabName: tab.name, - tabUrl: tab.url, - hasCloudSandbox: !!worktree?.cloudSandbox, - isCloudIDETab, - }); - } + tab.type === "preview" && (tab.url?.includes("e2b.app") ?? false); const IconComponent = (() => { switch (tab.type) { From b367bf8150d3e9c18867bcec18e8b13d5f263167 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 13:02:02 -0800 Subject: [PATCH 13/21] add direct sandbox ID deletion via url extraction --- apps/desktop/src/main/lib/workspace-ipcs.ts | 18 ++++++++++++++++++ .../components/TabItem/TabItem.tsx | 19 +++++++++++++------ .../src/shared/ipc-channels/worktree.ts | 5 +++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/lib/workspace-ipcs.ts b/apps/desktop/src/main/lib/workspace-ipcs.ts index c92ebca8ff6..568270270ac 100644 --- a/apps/desktop/src/main/lib/workspace-ipcs.ts +++ b/apps/desktop/src/main/lib/workspace-ipcs.ts @@ -948,4 +948,22 @@ export function registerWorkspaceIPCs() { } }, ); + + // 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), + }; + } + }, + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx index 68ac7de05eb..60ca7a08d37 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx @@ -126,7 +126,7 @@ export function TabItem({ }; const handleKillVM = async () => { - if (!workspaceId || !worktreeId) return; + if (!tab.url) return; const confirmed = window.confirm( "Are you sure you want to delete this cloud sandbox? This cannot be undone.", @@ -134,12 +134,19 @@ export function TabItem({ if (!confirmed) return; try { + // Extract sandbox ID from URL (format: https://7030-SANDBOX_ID.e2b.app/) + const urlMatch = tab.url.match(/\/\/\d+-([^.]+)\.e2b\.app/); + const sandboxId = urlMatch?.[1]; + + if (!sandboxId) { + alert("Could not extract sandbox ID from URL"); + return; + } + + // Delete via sandbox ID directly const result = await window.ipcRenderer.invoke( - "worktree-delete-cloud-sandbox", - { - workspaceId, - worktreeId, - }, + "cloud-sandbox-delete-by-id", + { sandboxId }, ); if (result.success) { diff --git a/apps/desktop/src/shared/ipc-channels/worktree.ts b/apps/desktop/src/shared/ipc-channels/worktree.ts index debe9042a0e..77089e47114 100644 --- a/apps/desktop/src/shared/ipc-channels/worktree.ts +++ b/apps/desktop/src/shared/ipc-channels/worktree.ts @@ -201,4 +201,9 @@ export interface WorktreeChannels { request: { workspaceId: string; worktreeId: string }; response: SuccessResponse; }; + + "cloud-sandbox-delete-by-id": { + request: { sandboxId: string }; + response: SuccessResponse; + }; } From f94e52588bf74cd2e96be6832b903ff1d9cc7895 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 13:06:57 -0800 Subject: [PATCH 14/21] add bidirectional vm/worktree deletion - kill vm now also deletes the worktree - removing worktree now also kills the cloud sandbox - ensures cleanup is complete in both directions --- .../components/WorktreeItem/WorktreeItem.tsx | 13 +++++++ .../components/TabItem/TabItem.tsx | 38 ++++++++++++++----- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx index 27f6fe2704f..f1d092f4396 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx @@ -566,6 +566,19 @@ export function WorktreeItem({ setShowRemoveDialog(false); setRemoveWarning(""); + // If this worktree has a cloud sandbox, delete it first + if (worktree.cloudSandbox) { + try { + await window.ipcRenderer.invoke("worktree-delete-cloud-sandbox", { + workspaceId, + worktreeId: worktree.id, + }); + } catch (error) { + console.error("Failed to delete cloud sandbox:", error); + // Continue with worktree removal even if cloud sandbox deletion fails + } + } + const result = await window.ipcRenderer.invoke("worktree-remove", { workspaceId, worktreeId: worktree.id, diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx index 60ca7a08d37..6a432e654bc 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx @@ -126,10 +126,10 @@ export function TabItem({ }; const handleKillVM = async () => { - if (!tab.url) return; + if (!tab.url || !workspaceId || !worktreeId) return; const confirmed = window.confirm( - "Are you sure you want to delete this cloud sandbox? This cannot be undone.", + "Are you sure you want to delete this cloud sandbox and worktree? This cannot be undone.", ); if (!confirmed) return; @@ -143,24 +143,42 @@ export function TabItem({ return; } - // Delete via sandbox ID directly - const result = await window.ipcRenderer.invoke( + // Delete the cloud sandbox + const sandboxResult = await window.ipcRenderer.invoke( "cloud-sandbox-delete-by-id", { sandboxId }, ); - if (result.success) { - // Close the tab after successful deletion - onTabRemove?.(tab.id); - } else { + if (!sandboxResult.success) { alert( - `Failed to delete cloud sandbox: ${result.error || "Unknown error"}`, + `Failed to delete cloud sandbox: ${sandboxResult.error || "Unknown error"}`, + ); + return; + } + + // Delete the worktree + const worktreeResult = await window.ipcRenderer.invoke( + "worktree-remove", + { + workspaceId, + worktreeId, + }, + ); + + if ( + worktreeResult && + typeof worktreeResult === "object" && + "success" in worktreeResult && + !worktreeResult.success + ) { + alert( + `Cloud sandbox deleted, but failed to delete worktree: ${worktreeResult.error || "Unknown error"}`, ); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - alert(`Failed to delete cloud sandbox: ${errorMessage}`); + alert(`Failed to delete cloud sandbox and worktree: ${errorMessage}`); } }; From f4c4a588df89576b7f8f26a6f3461c7e528a14df Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 13:11:30 -0800 Subject: [PATCH 15/21] fix sandbox deletion and token leaks - extract sandbox ID from tab URLs when cloudSandbox property not set - mask ANTHROPIC_AUTH_TOKEN in logs to prevent leaking credentials - fix env var name: use ANTHROPIC_AUTH_TOKEN instead of CLAUDE_CODE_OAUTH_TOKEN --- apps/desktop/src/main/lib/cloud-api-client.ts | 11 ++++++-- .../components/WorktreeItem/WorktreeItem.tsx | 26 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/lib/cloud-api-client.ts b/apps/desktop/src/main/lib/cloud-api-client.ts index 48efb15568d..b9cd440a508 100644 --- a/apps/desktop/src/main/lib/cloud-api-client.ts +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -77,11 +77,18 @@ class CloudApiClient { taskDescription: params.taskDescription, envVars: { ...params.envVars, - ...(claudeAuthToken && { CLAUDE_CODE_OAUTH_TOKEN: claudeAuthToken }), + ...(claudeAuthToken && { ANTHROPIC_AUTH_TOKEN: claudeAuthToken }), }, }; - console.log("Creating sandbox with params:", requestBody); + // 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 ? { ANTHROPIC_AUTH_TOKEN: "***" } : undefined, + }); const response = await fetch(this.baseUrl, { method: "POST", diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx index f1d092f4396..dc9c0282509 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx @@ -566,13 +566,31 @@ export function WorktreeItem({ setShowRemoveDialog(false); setRemoveWarning(""); - // If this worktree has a cloud sandbox, delete it first + // Check if this worktree has a cloud sandbox + // Try to find sandbox ID from cloudSandbox property or from tab URLs + let sandboxId: string | undefined; + if (worktree.cloudSandbox) { + sandboxId = worktree.cloudSandbox.id; + } else { + // Check if any tab has an e2b.app URL (Cloud IDE tab) + const cloudTab = worktree.tabs.find( + (tab) => tab.type === "preview" && tab.url?.includes("e2b.app"), + ); + if (cloudTab?.url) { + // Extract sandbox ID from URL (format: https://7030-SANDBOX_ID.e2b.app/) + const urlMatch = cloudTab.url.match(/\/\/\d+-([^.]+)\.e2b\.app/); + sandboxId = urlMatch?.[1]; + } + } + + // If we found a sandbox ID, delete the cloud sandbox first + if (sandboxId) { try { - await window.ipcRenderer.invoke("worktree-delete-cloud-sandbox", { - workspaceId, - worktreeId: worktree.id, + await window.ipcRenderer.invoke("cloud-sandbox-delete-by-id", { + sandboxId, }); + console.log(`Deleted cloud sandbox: ${sandboxId}`); } catch (error) { console.error("Failed to delete cloud sandbox:", error); // Continue with worktree removal even if cloud sandbox deletion fails From 095f7477f097a11723e8243078b55c54e5c3a349 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 13:15:54 -0800 Subject: [PATCH 16/21] use localhost:3001 for e2b sandbox api temporarily --- apps/desktop/src/main/lib/cloud-api-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/main/lib/cloud-api-client.ts b/apps/desktop/src/main/lib/cloud-api-client.ts index b9cd440a508..3686efbda35 100644 --- a/apps/desktop/src/main/lib/cloud-api-client.ts +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -34,7 +34,7 @@ interface CreateSandboxResponse { * Uses GitHub token for authentication */ class CloudApiClient { - private baseUrl = "https://staging.yolocode.ai/api/e2b-sandboxes"; + private baseUrl = "http://localhost:3001/api/e2b-sandboxes"; /** * Get GitHub token from gh CLI From e916fc08e45b411d93e5ff19f9dc0c171d1c5973 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 16:46:06 -0800 Subject: [PATCH 17/21] add loading state to kill vm button --- .../components/TabItem/TabItem.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx index 6a432e654bc..97e9b7ea3a4 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/components/TabItem/TabItem.tsx @@ -11,6 +11,7 @@ import { FolderOutput, FolderTree, Globe2, + Loader2, Monitor, SquareTerminal, X, @@ -49,6 +50,7 @@ export function TabItem({ }: TabItemProps) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(tab.name); + const [isKillingVM, setIsKillingVM] = useState(false); const inputRef = useRef(null); // Focus input when entering edit mode @@ -133,6 +135,7 @@ export function TabItem({ ); if (!confirmed) return; + setIsKillingVM(true); try { // Extract sandbox ID from URL (format: https://7030-SANDBOX_ID.e2b.app/) const urlMatch = tab.url.match(/\/\/\d+-([^.]+)\.e2b\.app/); @@ -179,6 +182,8 @@ export function TabItem({ const errorMessage = error instanceof Error ? error.message : String(error); alert(`Failed to delete cloud sandbox and worktree: ${errorMessage}`); + } finally { + setIsKillingVM(false); } }; @@ -265,9 +270,17 @@ export function TabItem({ )} {isCloudIDETab && ( - - - Kill VM + + {isKillingVM ? ( + + ) : ( + + )} + {isKillingVM ? "Deleting..." : "Kill VM"} )} From 701610dcb9bfeaf5153a2f5f83c8c90e16ba09e1 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 16:47:01 -0800 Subject: [PATCH 18/21] add timestamp to cloud worktree branch names for uniqueness prevents 'branch already exists' error by appending base36 timestamp --- .../src/renderer/screens/main/components/Sidebar/Sidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx index a2e01587b8a..16aa34ec65c 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx @@ -209,7 +209,8 @@ export function Sidebar({ ]; const randomAdj = adjectives[Math.floor(Math.random() * adjectives.length)]; const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]; - const randomName = `${randomAdj}-${randomNoun}`; + const timestamp = Date.now().toString(36); // Add timestamp to ensure uniqueness + const randomName = `${randomAdj}-${randomNoun}-${timestamp}`; // For now, create a simple worktree and immediately create a cloud sandbox for it const title = `Cloud ${randomAdj} ${randomNoun}`; From da53f043697708706447d9f852ca57c4fc3dc227 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 11 Nov 2025 16:47:42 -0800 Subject: [PATCH 19/21] pass CLAUDE_CODE_OAUTH_TOKEN to vm env vars - now passes both CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_AUTH_TOKEN - ensures compatibility with different claude code versions --- apps/desktop/src/main/lib/cloud-api-client.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/main/lib/cloud-api-client.ts b/apps/desktop/src/main/lib/cloud-api-client.ts index 3686efbda35..3297967d01e 100644 --- a/apps/desktop/src/main/lib/cloud-api-client.ts +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -77,7 +77,9 @@ class CloudApiClient { taskDescription: params.taskDescription, envVars: { ...params.envVars, - ...(claudeAuthToken && { ANTHROPIC_AUTH_TOKEN: claudeAuthToken }), + ...(claudeAuthToken && { + CLAUDE_CODE_OAUTH_TOKEN: claudeAuthToken, + }), }, }; @@ -87,7 +89,9 @@ class CloudApiClient { template: requestBody.template, githubRepo: requestBody.githubRepo, taskDescription: requestBody.taskDescription, - envVars: claudeAuthToken ? { ANTHROPIC_AUTH_TOKEN: "***" } : undefined, + envVars: claudeAuthToken + ? { CLAUDE_CODE_OAUTH_TOKEN: "***" } + : undefined, }); const response = await fetch(this.baseUrl, { From d182a6da1106e4859c6ec52478fe6d1d594a3e8f Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 18 Nov 2025 17:30:10 -0800 Subject: [PATCH 20/21] restore --- .../main/components/Layout/NewLayoutMain.tsx | 47 +++++++------------ .../main/components/Sidebar/Sidebar.tsx | 12 ++--- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx index b489792f68d..5faeb75a68f 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx @@ -240,12 +240,12 @@ function enrichWorktreesWithTasks( isPending: true, // Mark as pending for UI task: pending.taskData ? { - id: pending.id, - slug: pending.taskData.slug, - title: pending.taskData.name, - status: pending.taskData.status, - description: pending.description || "", - } + id: pending.id, + slug: pending.taskData.slug, + title: pending.taskData.name, + status: pending.taskData.status, + description: pending.description || "", + } : undefined, }), ); @@ -289,9 +289,7 @@ export const MainLayout: React.FC = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [showSidebarOverlay, setShowSidebarOverlay] = useState(false); const [isAddTaskModalOpen, setIsAddTaskModalOpen] = useState(false); - const [addTaskModalInitialMode, setAddTaskModalInitialMode] = useState< - "list" | "new" - >("list"); + const [addTaskModalInitialMode, setAddTaskModalInitialMode] = useState<"list" | "new">("list"); const [branches, setBranches] = useState([]); const [isCreatingWorktree, setIsCreatingWorktree] = useState(false); const [setupStatus, setSetupStatus] = useState(undefined); @@ -528,10 +526,7 @@ export const MainLayout: React.FC = () => { // If we deleted the selected worktree, select the first available one if (selectedWorktreeId === worktreeId) { - if ( - refreshedWorkspace.worktrees && - refreshedWorkspace.worktrees.length > 0 - ) { + if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) { const firstWorktree = refreshedWorkspace.worktrees[0]; setSelectedWorktreeId(firstWorktree.id); if (firstWorktree.tabs && firstWorktree.tabs.length > 0) { @@ -586,7 +581,7 @@ export const MainLayout: React.FC = () => { const handleOpenAddTaskModal = (mode: "list" | "new" = "list") => { setAddTaskModalInitialMode(mode); setIsAddTaskModalOpen(true); - + // Fetch branches when opening in new mode if (mode === "new" && currentWorkspace) { void (async () => { @@ -720,10 +715,7 @@ export const MainLayout: React.FC = () => { }), }); - window.ipcRenderer.removeListener( - "worktree-setup-progress", - progressHandler, - ); + window.ipcRenderer.removeListener("worktree-setup-progress", progressHandler); if (result.success) { // Display setup result if available @@ -755,12 +747,8 @@ export const MainLayout: React.FC = () => { handleTabSelect(result.worktree.id, result.worktree.tabs[0].id); } } - <<<<<<< HEAD:apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx } else { - console.error( - "[NewLayoutMain] Failed to create worktree:", - result.error, - ); + console.error("[NewLayoutMain] Failed to create worktree:", result.error); setSetupStatus("Failed to create worktree"); setSetupOutput(result.error); setIsCreatingWorktree(false); @@ -771,10 +759,7 @@ export const MainLayout: React.FC = () => { setSetupStatus("Error creating worktree"); setSetupOutput(String(error)); setIsCreatingWorktree(false); - window.ipcRenderer.removeListener( - "worktree-setup-progress", - progressHandler, - ); + window.ipcRenderer.removeListener("worktree-setup-progress", progressHandler); } }; @@ -1123,10 +1108,10 @@ export const MainLayout: React.FC = () => { {/* Main content panel */} {loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( + error || + !currentWorkspace || + !selectedTab || + !selectedWorktree ? ( >( new Set(), @@ -68,8 +65,7 @@ export function Sidebar({ }; window.addEventListener("workspace-changed", handleWorkspaceChanged); - return () => - window.removeEventListener("workspace-changed", handleWorkspaceChanged); + return () => window.removeEventListener("workspace-changed", handleWorkspaceChanged); }, [handleWorktreeCreated]); // Fetch diff data when in changes mode From f9ddbdf834ad604fbc68a20c8f23f3c2a39731c1 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 18 Nov 2025 17:32:13 -0800 Subject: [PATCH 21/21] replace removeListener with off for ipc event cleanup - use ipcRenderer.off instead of removeListener for worktree-setup-progress - removes trailing whitespace --- .../screens/main/components/Layout/NewLayoutMain.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx index 5faeb75a68f..330506498e8 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx @@ -581,7 +581,7 @@ export const MainLayout: React.FC = () => { const handleOpenAddTaskModal = (mode: "list" | "new" = "list") => { setAddTaskModalInitialMode(mode); setIsAddTaskModalOpen(true); - + // Fetch branches when opening in new mode if (mode === "new" && currentWorkspace) { void (async () => { @@ -715,7 +715,7 @@ export const MainLayout: React.FC = () => { }), }); - window.ipcRenderer.removeListener("worktree-setup-progress", progressHandler); + window.ipcRenderer.off("worktree-setup-progress", progressHandler); if (result.success) { // Display setup result if available @@ -759,7 +759,7 @@ export const MainLayout: React.FC = () => { setSetupStatus("Error creating worktree"); setSetupOutput(String(error)); setIsCreatingWorktree(false); - window.ipcRenderer.removeListener("worktree-setup-progress", progressHandler); + window.ipcRenderer.off("worktree-setup-progress", progressHandler); } };