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 000000000..3297967d0 --- /dev/null +++ b/apps/desktop/src/main/lib/cloud-api-client.ts @@ -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; +} + +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, + }; + + 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; diff --git a/apps/desktop/src/main/lib/workspace-ipcs.ts b/apps/desktop/src/main/lib/workspace-ipcs.ts index 4f44ca803..568270270 100644 --- a/apps/desktop/src/main/lib/workspace-ipcs.ts +++ b/apps/desktop/src/main/lib/workspace-ipcs.ts @@ -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), + }; + } + }, + ); + + 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), + }; + } + }, + ); } 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 39fb62cfb..330506498 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); } }; @@ -838,6 +838,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 +1031,8 @@ export const MainLayout: React.FC = () => { onAddTask={handleOpenAddTaskModal} onCreatePR={handleCreatePR} onMergePR={handleMergePR} + onCreateCloudSandbox={handleCreateCloudSandbox} + onOpenCloudSandbox={handleOpenCloudSandbox} worktrees={enrichWorktreesWithTasks( currentWorkspace?.worktrees || [], pendingWorktrees, 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 8f6c0bba9..3d642ee7a 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"; @@ -34,6 +35,7 @@ export function Sidebar({ 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(""); @@ -173,6 +175,108 @@ export function Sidebar({ setShowWorktreeModal(true); }; + 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 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}`; + const branch = `cloud-dev-${randomName}`; + + 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) { + // 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}`; + + 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 + onWorktreeCreated(); + } 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 +491,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 de0c126ae..dfe760637 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 && ( + + )} +
); } 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 27f6fe270..dc9c02825 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,37 @@ export function WorktreeItem({ setShowRemoveDialog(false); setRemoveWarning(""); + // 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("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 + } + } + 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 72ff0e020..97e9b7ea3 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,10 +6,12 @@ import { ContextMenuTrigger, } from "@superset/ui/context-menu"; import { + Cloud, Edit2, FolderOutput, FolderTree, Globe2, + Loader2, Monitor, SquareTerminal, X, @@ -48,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 @@ -124,11 +127,76 @@ export function TabItem({ } }; + const handleKillVM = async () => { + if (!tab.url || !workspaceId || !worktreeId) return; + + const confirmed = window.confirm( + "Are you sure you want to delete this cloud sandbox and worktree? This cannot be undone.", + ); + 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/); + const sandboxId = urlMatch?.[1]; + + if (!sandboxId) { + alert("Could not extract sandbox ID from URL"); + return; + } + + // Delete the cloud sandbox + const sandboxResult = await window.ipcRenderer.invoke( + "cloud-sandbox-delete-by-id", + { sandboxId }, + ); + + if (!sandboxResult.success) { + alert( + `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 and worktree: ${errorMessage}`); + } finally { + setIsKillingVM(false); + } + }; + 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 + // 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") ?? false); + const IconComponent = (() => { switch (tab.type) { case "preview": @@ -201,6 +269,20 @@ export function TabItem({ Group {selectedTabIds.size} Tabs )} + {isCloudIDETab && ( + + {isKillingVM ? ( + + ) : ( + + )} + {isKillingVM ? "Deleting..." : "Kill VM"} + + )} ); diff --git a/apps/desktop/src/shared/ipc-channels.ts b/apps/desktop/src/shared/ipc-channels.ts index ce6ea4661..f64a122cc 100644 --- a/apps/desktop/src/shared/ipc-channels.ts +++ b/apps/desktop/src/shared/ipc-channels.ts @@ -3,4 +3,3 @@ * This maintains backward compatibility with existing imports */ export * from "./ipc-channels/index"; - diff --git a/apps/desktop/src/shared/ipc-channels/worktree.ts b/apps/desktop/src/shared/ipc-channels/worktree.ts index 58c09838e..77089e471 100644 --- a/apps/desktop/src/shared/ipc-channels/worktree.ts +++ b/apps/desktop/src/shared/ipc-channels/worktree.ts @@ -181,5 +181,29 @@ export interface WorktreeChannels { request: { workspaceId: string; worktreeId: string }; response: SuccessResponse; }; -} + // Cloud sandbox operations + "worktree-create-cloud-sandbox": { + request: { workspaceId: string; worktreeId: string }; + response: { + success: boolean; + sandbox?: import("../types").CloudSandbox; + error?: string; + }; + }; + + "worktree-open-cloud-sandbox": { + request: { workspaceId: string; worktreeId: string }; + response: SuccessResponse; + }; + + "worktree-delete-cloud-sandbox": { + request: { workspaceId: string; worktreeId: string }; + response: SuccessResponse; + }; + + "cloud-sandbox-delete-by-id": { + request: { sandboxId: string }; + response: SuccessResponse; + }; +} diff --git a/apps/desktop/src/shared/types.ts b/apps/desktop/src/shared/types.ts index 95eef46b1..9974ce21e 100644 --- a/apps/desktop/src/shared/types.ts +++ b/apps/desktop/src/shared/types.ts @@ -55,6 +55,17 @@ export interface Tab { createdAt: string; } +// Cloud sandbox types +export interface CloudSandbox { + id: string; // E2B sandbox ID + name: string; // Sandbox name + status: "creating" | "running" | "stopped" | "error"; + websshHost?: string; // WebSSH terminal URL (port 8888) + claudeHost?: string; // Claude SSE server URL (port 9999) + createdAt: string; + error?: string; // Error message if status is "error" +} + export interface Worktree { id: string; branch: string; @@ -65,6 +76,7 @@ export interface Worktree { merged?: boolean; // Indicates if this worktree has been merged into another worktree description?: string; // Optional description of the worktree's goal or purpose prUrl?: string; // Pull request URL if a PR has been created for this worktree + cloudSandbox?: CloudSandbox; // Cloud sandbox instance for this worktree } export interface Workspace {