Skip to content

Commit d177ec4

Browse files
committed
Add apply to project feature for worktree sessions
1 parent a3065e3 commit d177ec4

File tree

3 files changed

+163
-2
lines changed

3 files changed

+163
-2
lines changed

index.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,29 @@ <h2 class="modal-title">Terminal Settings</h2>
255255
</div>
256256
</div>
257257

258+
<!-- Apply to Project Modal -->
259+
<div id="apply-to-project-modal" class="modal-overlay hidden">
260+
<div class="modal">
261+
<h2 class="modal-title">Apply Changes to Project</h2>
262+
263+
<div class="space-y-3 text-sm text-gray-300">
264+
<p>This will apply all changes from this session to the original project directory:</p>
265+
<ul class="list-disc list-inside space-y-1 text-gray-400">
266+
<li>Branch: <span id="apply-branch-name" class="text-white"></span></li>
267+
<li>Parent: <span id="apply-parent-branch" class="text-white"></span></li>
268+
</ul>
269+
<div class="bg-yellow-900 bg-opacity-30 border border-yellow-700 rounded p-3 mt-3">
270+
<p class="text-yellow-300 text-xs">⚠️ This will include all commits and unstaged changes from this branch.</p>
271+
</div>
272+
</div>
273+
274+
<div class="btn-group">
275+
<button id="cancel-apply-to-project" class="btn-secondary">Cancel</button>
276+
<button id="confirm-apply-to-project" class="btn-primary">Apply Changes</button>
277+
</div>
278+
</div>
279+
</div>
280+
258281
<script>
259282
require('./dist/renderer.js');
260283
</script>

main.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,72 @@ ipcMain.handle("save-terminal-settings", (_event, settings: any) => {
606606
(store as any).set("terminalSettings", settings);
607607
});
608608

609+
// Apply session changes to project
610+
ipcMain.handle("apply-session-to-project", async (_event, sessionId: string) => {
611+
try {
612+
const sessions = getPersistedSessions();
613+
const session = sessions.find(s => s.id === sessionId);
614+
615+
if (!session) {
616+
return { success: false, error: "Session not found" };
617+
}
618+
619+
if (session.config.sessionType !== SessionType.WORKTREE) {
620+
return { success: false, error: "Only worktree sessions can be applied to project" };
621+
}
622+
623+
if (!session.worktreePath || !session.config.parentBranch) {
624+
return { success: false, error: "Session missing worktree path or parent branch" };
625+
}
626+
627+
const projectDir = session.config.projectDir;
628+
const worktreePath = session.worktreePath;
629+
const parentBranch = session.config.parentBranch;
630+
const patchFilename = `fleetcode-patch-${Date.now()}.patch`;
631+
const patchPath = path.join("/tmp", patchFilename);
632+
633+
// Create git instance for worktree
634+
const worktreeGit = simpleGit(worktreePath);
635+
636+
// Generate patch file from parent branch to current HEAD (includes all commits and staged changes)
637+
// Using diff to capture all changes since divergence from parent branch
638+
const diffCommand = `${parentBranch}...HEAD`;
639+
const { stdout: patchContent } = await execAsync(
640+
`git diff ${diffCommand}`,
641+
{ cwd: worktreePath }
642+
);
643+
644+
// Write patch to temp file
645+
fs.writeFileSync(patchPath, patchContent);
646+
647+
// Apply patch to original project directory
648+
const projectGit = simpleGit(projectDir);
649+
try {
650+
await execAsync(`git apply "${patchPath}"`, { cwd: projectDir });
651+
652+
// Clean up patch file on success
653+
fs.unlinkSync(patchPath);
654+
655+
return { success: true };
656+
} catch (applyError: any) {
657+
// Clean up patch file on error
658+
if (fs.existsSync(patchPath)) {
659+
fs.unlinkSync(patchPath);
660+
}
661+
662+
return {
663+
success: false,
664+
error: `Failed to apply patch: ${applyError.message || applyError}`
665+
};
666+
}
667+
} catch (error: any) {
668+
return {
669+
success: false,
670+
error: error.message || String(error)
671+
};
672+
}
673+
});
674+
609675
// MCP Server management functions
610676
async function listMcpServers() {
611677
try {

renderer.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ function addSession(persistedSession: PersistedSession, hasActivePty: boolean) {
304304
sessions.set(persistedSession.id, session);
305305

306306
// Add to sidebar
307-
addToSidebar(persistedSession.id, persistedSession.name, hasActivePty);
307+
addToSidebar(persistedSession.id, persistedSession.name, hasActivePty, persistedSession.config);
308308

309309
// Only add tab if terminal is active
310310
if (hasActivePty) {
@@ -351,10 +351,13 @@ function updateSessionState(sessionId: string, isActive: boolean) {
351351
}
352352
}
353353

354-
function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
354+
function addToSidebar(sessionId: string, name: string, hasActivePty: boolean, config: SessionConfig) {
355355
const list = document.getElementById("session-list");
356356
if (!list) return;
357357

358+
const isWorktree = config.sessionType === SessionType.WORKTREE;
359+
const applyMenuItem = isWorktree ? `<button class="session-menu-item apply-to-project-btn" data-id="${sessionId}">Apply to project</button>` : '';
360+
358361
const item = document.createElement("div");
359362
item.id = `sidebar-${sessionId}`;
360363
item.className = "session-list-item";
@@ -368,6 +371,7 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
368371
<button class="session-menu-btn" data-id="${sessionId}" title="Session options">⋯</button>
369372
<div class="session-menu hidden" data-id="${sessionId}">
370373
<button class="session-menu-item rename-session-btn" data-id="${sessionId}">Rename</button>
374+
${applyMenuItem}
371375
<button class="session-menu-item delete-session-btn" data-id="${sessionId}">Delete</button>
372376
</div>
373377
</div>
@@ -430,6 +434,14 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
430434
deleteSession(sessionId);
431435
});
432436

437+
// Apply to project button (only for worktree sessions)
438+
const applyBtn = item.querySelector(".apply-to-project-btn");
439+
applyBtn?.addEventListener("click", (e) => {
440+
e.stopPropagation();
441+
menu?.classList.add("hidden");
442+
showApplyToProjectDialog(sessionId);
443+
});
444+
433445
list.appendChild(item);
434446
}
435447

@@ -721,6 +733,27 @@ function deleteSession(sessionId: string) {
721733
}
722734
}
723735

736+
function showApplyToProjectDialog(sessionId: string) {
737+
const session = sessions.get(sessionId);
738+
if (!session) return;
739+
740+
const modal = document.getElementById("apply-to-project-modal");
741+
const branchName = document.getElementById("apply-branch-name");
742+
const parentBranch = document.getElementById("apply-parent-branch");
743+
744+
if (branchName && session.config.branchName) {
745+
branchName.textContent = session.config.branchName;
746+
}
747+
if (parentBranch && session.config.parentBranch) {
748+
parentBranch.textContent = session.config.parentBranch;
749+
}
750+
751+
// Store session ID for later use in confirm handler
752+
modal?.setAttribute("data-session-id", sessionId);
753+
754+
modal?.classList.remove("hidden");
755+
}
756+
724757
// Handle session output
725758
ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
726759
const session = sessions.get(sessionId);
@@ -1496,6 +1529,45 @@ saveSettingsBtn?.addEventListener("click", async () => {
14961529
// Load settings on startup
14971530
loadSettings();
14981531

1532+
// Apply to Project Modal
1533+
const applyToProjectModal = document.getElementById("apply-to-project-modal");
1534+
const cancelApplyToProjectBtn = document.getElementById("cancel-apply-to-project");
1535+
const confirmApplyToProjectBtn = document.getElementById("confirm-apply-to-project");
1536+
1537+
cancelApplyToProjectBtn?.addEventListener("click", () => {
1538+
applyToProjectModal?.classList.add("hidden");
1539+
});
1540+
1541+
confirmApplyToProjectBtn?.addEventListener("click", async () => {
1542+
const sessionId = applyToProjectModal?.getAttribute("data-session-id");
1543+
if (!sessionId) return;
1544+
1545+
// Disable button during operation
1546+
if (confirmApplyToProjectBtn) {
1547+
confirmApplyToProjectBtn.textContent = "Applying...";
1548+
confirmApplyToProjectBtn.setAttribute("disabled", "true");
1549+
}
1550+
1551+
try {
1552+
const result = await ipcRenderer.invoke("apply-session-to-project", sessionId);
1553+
1554+
if (result.success) {
1555+
alert(`Changes applied successfully to project directory.`);
1556+
applyToProjectModal?.classList.add("hidden");
1557+
} else {
1558+
alert(`Failed to apply changes: ${result.error}`);
1559+
}
1560+
} catch (error) {
1561+
alert(`Error applying changes: ${error}`);
1562+
} finally {
1563+
// Re-enable button
1564+
if (confirmApplyToProjectBtn) {
1565+
confirmApplyToProjectBtn.textContent = "Apply Changes";
1566+
confirmApplyToProjectBtn.removeAttribute("disabled");
1567+
}
1568+
}
1569+
});
1570+
14991571
// Close session menus when clicking outside
15001572
document.addEventListener("click", (e) => {
15011573
const target = e.target as HTMLElement;

0 commit comments

Comments
 (0)