Skip to content

Commit c7f5956

Browse files
authored
Add apply to project feature for worktree sessions (#22)
* Add apply to project feature for worktree sessions * Fix patch application using 3-way merge and include unstaged changes * Remove 3-way merge from patch application
1 parent 67a2ca1 commit c7f5956

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
@@ -259,6 +259,29 @@ <h2 class="modal-title">Terminal Settings</h2>
259259
</div>
260260
</div>
261261

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

main.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,72 @@ ipcMain.handle("get-app-version", () => {
611611
return app.getVersion();
612612
});
613613

614+
// Apply session changes to project
615+
ipcMain.handle("apply-session-to-project", async (_event, sessionId: string) => {
616+
try {
617+
const sessions = getPersistedSessions();
618+
const session = sessions.find(s => s.id === sessionId);
619+
620+
if (!session) {
621+
return { success: false, error: "Session not found" };
622+
}
623+
624+
if (session.config.sessionType !== SessionType.WORKTREE) {
625+
return { success: false, error: "Only worktree sessions can be applied to project" };
626+
}
627+
628+
if (!session.worktreePath || !session.config.parentBranch) {
629+
return { success: false, error: "Session missing worktree path or parent branch" };
630+
}
631+
632+
const projectDir = session.config.projectDir;
633+
const worktreePath = session.worktreePath;
634+
const parentBranch = session.config.parentBranch;
635+
const patchFilename = `fleetcode-patch-${Date.now()}.patch`;
636+
const patchPath = path.join("/tmp", patchFilename);
637+
638+
// Generate patch file from parent branch to current state (includes commits + unstaged changes)
639+
// Using diff against parent branch to capture all changes
640+
const { stdout: patchContent } = await execAsync(
641+
`git diff ${parentBranch}`,
642+
{ cwd: worktreePath }
643+
);
644+
645+
// If patch is empty, there are no changes to apply
646+
if (!patchContent.trim()) {
647+
return { success: false, error: "No changes to apply" };
648+
}
649+
650+
// Write patch to temp file
651+
fs.writeFileSync(patchPath, patchContent);
652+
653+
// Apply patch to original project directory
654+
try {
655+
await execAsync(`git apply "${patchPath}"`, { cwd: projectDir });
656+
657+
// Clean up patch file on success
658+
fs.unlinkSync(patchPath);
659+
660+
return { success: true };
661+
} catch (applyError: any) {
662+
// Clean up patch file on error
663+
if (fs.existsSync(patchPath)) {
664+
fs.unlinkSync(patchPath);
665+
}
666+
667+
return {
668+
success: false,
669+
error: `Failed to apply patch: ${applyError.message || applyError}`
670+
};
671+
}
672+
} catch (error: any) {
673+
return {
674+
success: false,
675+
error: error.message || String(error)
676+
};
677+
}
678+
});
679+
614680
// MCP Server management functions
615681
async function listMcpServers() {
616682
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);
@@ -1504,6 +1537,45 @@ saveSettingsBtn?.addEventListener("click", async () => {
15041537
// Load settings on startup
15051538
loadSettings();
15061539

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

0 commit comments

Comments
 (0)