diff --git a/index.html b/index.html index 14d18f2..2dec007 100644 --- a/index.html +++ b/index.html @@ -259,6 +259,29 @@ + + + diff --git a/main.ts b/main.ts index 2f6e01d..d3a52f9 100644 --- a/main.ts +++ b/main.ts @@ -611,6 +611,72 @@ ipcMain.handle("get-app-version", () => { return app.getVersion(); }); +// Apply session changes to project +ipcMain.handle("apply-session-to-project", async (_event, sessionId: string) => { + try { + const sessions = getPersistedSessions(); + const session = sessions.find(s => s.id === sessionId); + + if (!session) { + return { success: false, error: "Session not found" }; + } + + if (session.config.sessionType !== SessionType.WORKTREE) { + return { success: false, error: "Only worktree sessions can be applied to project" }; + } + + if (!session.worktreePath || !session.config.parentBranch) { + return { success: false, error: "Session missing worktree path or parent branch" }; + } + + const projectDir = session.config.projectDir; + const worktreePath = session.worktreePath; + const parentBranch = session.config.parentBranch; + const patchFilename = `fleetcode-patch-${Date.now()}.patch`; + const patchPath = path.join("/tmp", patchFilename); + + // Generate patch file from parent branch to current state (includes commits + unstaged changes) + // Using diff against parent branch to capture all changes + const { stdout: patchContent } = await execAsync( + `git diff ${parentBranch}`, + { cwd: worktreePath } + ); + + // If patch is empty, there are no changes to apply + if (!patchContent.trim()) { + return { success: false, error: "No changes to apply" }; + } + + // Write patch to temp file + fs.writeFileSync(patchPath, patchContent); + + // Apply patch to original project directory + try { + await execAsync(`git apply "${patchPath}"`, { cwd: projectDir }); + + // Clean up patch file on success + fs.unlinkSync(patchPath); + + return { success: true }; + } catch (applyError: any) { + // Clean up patch file on error + if (fs.existsSync(patchPath)) { + fs.unlinkSync(patchPath); + } + + return { + success: false, + error: `Failed to apply patch: ${applyError.message || applyError}` + }; + } + } catch (error: any) { + return { + success: false, + error: error.message || String(error) + }; + } +}); + // MCP Server management functions async function listMcpServers() { try { diff --git a/renderer.ts b/renderer.ts index e7ac5af..095de3d 100644 --- a/renderer.ts +++ b/renderer.ts @@ -304,7 +304,7 @@ function addSession(persistedSession: PersistedSession, hasActivePty: boolean) { sessions.set(persistedSession.id, session); // Add to sidebar - addToSidebar(persistedSession.id, persistedSession.name, hasActivePty); + addToSidebar(persistedSession.id, persistedSession.name, hasActivePty, persistedSession.config); // Only add tab if terminal is active if (hasActivePty) { @@ -351,10 +351,13 @@ function updateSessionState(sessionId: string, isActive: boolean) { } } -function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) { +function addToSidebar(sessionId: string, name: string, hasActivePty: boolean, config: SessionConfig) { const list = document.getElementById("session-list"); if (!list) return; + const isWorktree = config.sessionType === SessionType.WORKTREE; + const applyMenuItem = isWorktree ? `` : ''; + const item = document.createElement("div"); item.id = `sidebar-${sessionId}`; item.className = "session-list-item"; @@ -368,6 +371,7 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) { @@ -430,6 +434,14 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) { deleteSession(sessionId); }); + // Apply to project button (only for worktree sessions) + const applyBtn = item.querySelector(".apply-to-project-btn"); + applyBtn?.addEventListener("click", (e) => { + e.stopPropagation(); + menu?.classList.add("hidden"); + showApplyToProjectDialog(sessionId); + }); + list.appendChild(item); } @@ -721,6 +733,27 @@ function deleteSession(sessionId: string) { } } +function showApplyToProjectDialog(sessionId: string) { + const session = sessions.get(sessionId); + if (!session) return; + + const modal = document.getElementById("apply-to-project-modal"); + const branchName = document.getElementById("apply-branch-name"); + const parentBranch = document.getElementById("apply-parent-branch"); + + if (branchName && session.config.branchName) { + branchName.textContent = session.config.branchName; + } + if (parentBranch && session.config.parentBranch) { + parentBranch.textContent = session.config.parentBranch; + } + + // Store session ID for later use in confirm handler + modal?.setAttribute("data-session-id", sessionId); + + modal?.classList.remove("hidden"); +} + // Handle session output ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => { const session = sessions.get(sessionId); @@ -1504,6 +1537,45 @@ saveSettingsBtn?.addEventListener("click", async () => { // Load settings on startup loadSettings(); +// Apply to Project Modal +const applyToProjectModal = document.getElementById("apply-to-project-modal"); +const cancelApplyToProjectBtn = document.getElementById("cancel-apply-to-project"); +const confirmApplyToProjectBtn = document.getElementById("confirm-apply-to-project"); + +cancelApplyToProjectBtn?.addEventListener("click", () => { + applyToProjectModal?.classList.add("hidden"); +}); + +confirmApplyToProjectBtn?.addEventListener("click", async () => { + const sessionId = applyToProjectModal?.getAttribute("data-session-id"); + if (!sessionId) return; + + // Disable button during operation + if (confirmApplyToProjectBtn) { + confirmApplyToProjectBtn.textContent = "Applying..."; + confirmApplyToProjectBtn.setAttribute("disabled", "true"); + } + + try { + const result = await ipcRenderer.invoke("apply-session-to-project", sessionId); + + if (result.success) { + alert(`Changes applied successfully to project directory.`); + applyToProjectModal?.classList.add("hidden"); + } else { + alert(`Failed to apply changes: ${result.error}`); + } + } catch (error) { + alert(`Error applying changes: ${error}`); + } finally { + // Re-enable button + if (confirmApplyToProjectBtn) { + confirmApplyToProjectBtn.textContent = "Apply Changes"; + confirmApplyToProjectBtn.removeAttribute("disabled"); + } + } +}); + // Close session menus when clicking outside document.addEventListener("click", (e) => { const target = e.target as HTMLElement;