Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,29 @@ <h2 class="modal-title">Terminal Settings</h2>
</div>
</div>

<!-- Apply to Project Modal -->
<div id="apply-to-project-modal" class="modal-overlay hidden">
<div class="modal">
<h2 class="modal-title">Apply Changes to Project</h2>

<div class="space-y-3 text-sm text-gray-300">
<p>This will apply all changes from this session to the original project directory:</p>
<ul class="list-disc list-inside space-y-1 text-gray-400">
<li>Branch: <span id="apply-branch-name" class="text-white"></span></li>
<li>Parent: <span id="apply-parent-branch" class="text-white"></span></li>
</ul>
<div class="bg-yellow-900 bg-opacity-30 border border-yellow-700 rounded p-3 mt-3">
<p class="text-yellow-300 text-xs">⚠️ This will include all commits and unstaged changes from this branch.</p>
</div>
</div>

<div class="btn-group">
<button id="cancel-apply-to-project" class="btn-secondary">Cancel</button>
<button id="confirm-apply-to-project" class="btn-primary">Apply Changes</button>
</div>
</div>
</div>

<script>
require('./dist/renderer.js');
</script>
Expand Down
66 changes: 66 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
76 changes: 74 additions & 2 deletions renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 ? `<button class="session-menu-item apply-to-project-btn" data-id="${sessionId}">Apply to project</button>` : '';

const item = document.createElement("div");
item.id = `sidebar-${sessionId}`;
item.className = "session-list-item";
Expand All @@ -368,6 +371,7 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
<button class="session-menu-btn" data-id="${sessionId}" title="Session options">⋯</button>
<div class="session-menu hidden" data-id="${sessionId}">
<button class="session-menu-item rename-session-btn" data-id="${sessionId}">Rename</button>
${applyMenuItem}
<button class="session-menu-item delete-session-btn" data-id="${sessionId}">Delete</button>
</div>
</div>
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down