resizing works, delete git branch when deleting session

This commit is contained in:
Amrit Subramanian 2025-10-06 18:05:14 -04:00
parent 0de5e77e99
commit 2930290662
3 changed files with 59 additions and 115 deletions

61
main.ts
View file

@ -1,36 +1,17 @@
import { app, BrowserWindow, ipcMain, dialog } from "electron"; import {exec} from "child_process";
import {app, BrowserWindow, dialog, ipcMain} from "electron";
import Store from "electron-store";
import * as fs from "fs";
import * as pty from "node-pty"; import * as pty from "node-pty";
import * as os from "os"; import * as os from "os";
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import {simpleGit} from "simple-git";
import { simpleGit } from "simple-git"; import {promisify} from "util";
import Store from "electron-store"; import {v4 as uuidv4} from "uuid";
import { exec } from "child_process"; import {PersistedSession, SessionConfig} from "./types";
import { promisify } from "util";
import { v4 as uuidv4 } from "uuid";
const execAsync = promisify(exec); const execAsync = promisify(exec);
interface SessionConfig {
projectDir: string;
parentBranch: string;
branchName?: string;
codingAgent: string;
skipPermissions: boolean;
setupCommands?: string[];
}
interface PersistedSession {
id: string;
number: number;
name: string;
config: SessionConfig;
worktreePath: string;
createdAt: number;
sessionUuid: string;
mcpConfigPath?: string;
}
let mainWindow: BrowserWindow; let mainWindow: BrowserWindow;
const activePtyProcesses = new Map<string, pty.IPty>(); const activePtyProcesses = new Map<string, pty.IPty>();
const mcpPollerPtyProcesses = new Map<string, pty.IPty>(); const mcpPollerPtyProcesses = new Map<string, pty.IPty>();
@ -344,10 +325,10 @@ async function ensureFleetcodeExcluded(projectDir: string) {
} }
} }
async function createWorktree(projectDir: string, parentBranch: string, sessionNumber: number, sessionUuid: string, customBranchName?: string): Promise<string> { async function createWorktree(projectDir: string, parentBranch: string, sessionNumber: number, sessionUuid: string, customBranchName?: string): Promise<{ worktreePath: string; branchName: string }> {
const git = simpleGit(projectDir); const git = simpleGit(projectDir);
const fleetcodeDir = path.join(projectDir, ".fleetcode"); const fleetcodeDir = path.join(projectDir, ".fleetcode");
const worktreeName = `session${sessionNumber}`; const worktreeName = customBranchName || `session${sessionNumber}`;
const worktreePath = path.join(fleetcodeDir, worktreeName); const worktreePath = path.join(fleetcodeDir, worktreeName);
// Use custom branch name if provided, otherwise generate default // Use custom branch name if provided, otherwise generate default
@ -357,7 +338,7 @@ async function createWorktree(projectDir: string, parentBranch: string, sessionN
} else { } else {
// Include short UUID to ensure branch uniqueness across deletes/recreates // Include short UUID to ensure branch uniqueness across deletes/recreates
const shortUuid = sessionUuid.split('-')[0]; const shortUuid = sessionUuid.split('-')[0];
branchName = `fleetcode/session${sessionNumber}-${shortUuid}`; branchName = `fleetcode/${worktreeName}-${shortUuid}`;
} }
// Create .fleetcode directory if it doesn't exist // Create .fleetcode directory if it doesn't exist
@ -385,7 +366,7 @@ async function createWorktree(projectDir: string, parentBranch: string, sessionN
// This creates a new branch named "fleetcode/session<N>" starting from the parent branch // This creates a new branch named "fleetcode/session<N>" starting from the parent branch
await git.raw(["worktree", "add", "-b", branchName, worktreePath, parentBranch]); await git.raw(["worktree", "add", "-b", branchName, worktreePath, parentBranch]);
return worktreePath; return { worktreePath, branchName };
} }
async function removeWorktree(projectDir: string, worktreePath: string) { async function removeWorktree(projectDir: string, worktreePath: string) {
@ -397,6 +378,15 @@ async function removeWorktree(projectDir: string, worktreePath: string) {
} }
} }
async function removeGitBranch(projectDir: string, branchName: string) {
const git = simpleGit(projectDir);
try {
await git.raw(["branch", "-D", branchName]);
} catch (error) {
console.error("Error removing git branch:", error);
}
}
// Open directory picker // Open directory picker
ipcMain.handle("select-directory", async () => { ipcMain.handle("select-directory", async () => {
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
@ -453,7 +443,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
ensureFleetcodeExcluded(config.projectDir); ensureFleetcodeExcluded(config.projectDir);
// Create git worktree with custom or default branch name // Create git worktree with custom or default branch name
const worktreePath = await createWorktree(config.projectDir, config.parentBranch, sessionNumber, sessionUuid, config.branchName); const { worktreePath, branchName } = await createWorktree(config.projectDir, config.parentBranch, sessionNumber, sessionUuid, config.branchName);
// Extract and write MCP config // Extract and write MCP config
const mcpServers = extractProjectMcpConfig(config.projectDir); const mcpServers = extractProjectMcpConfig(config.projectDir);
@ -469,6 +459,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
createdAt: Date.now(), createdAt: Date.now(),
sessionUuid, sessionUuid,
mcpConfigPath: mcpConfigPath || undefined, mcpConfigPath: mcpConfigPath || undefined,
gitBranch: branchName,
}; };
// Save to store // Save to store
@ -572,6 +563,11 @@ ipcMain.on("delete-session", async (_event, sessionId: string) => {
// Remove git worktree // Remove git worktree
await removeWorktree(session.config.projectDir, session.worktreePath); await removeWorktree(session.config.projectDir, session.worktreePath);
// Remove git branch if it exists
if (session.gitBranch) {
await removeGitBranch(session.config.projectDir, session.gitBranch);
}
// Remove from store // Remove from store
sessions.splice(sessionIndex, 1); sessions.splice(sessionIndex, 1);
savePersistedSessions(sessions); savePersistedSessions(sessions);
@ -778,6 +774,7 @@ const createWindow = () => {
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow(); createWindow();
// Handles launch from dock on macos
app.on("activate", () => { app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); createWindow();

View file

@ -1,25 +1,7 @@
import { ipcRenderer } from "electron"; import {FitAddon} from "@xterm/addon-fit";
import { Terminal } from "xterm"; import {ipcRenderer} from "electron";
import { FitAddon } from "@xterm/addon-fit"; import {Terminal} from "xterm";
import {PersistedSession, SessionConfig} from "./types";
interface SessionConfig {
projectDir: string;
parentBranch: string;
branchName?: string;
codingAgent: string;
skipPermissions: boolean;
setupCommands?: string[];
}
interface PersistedSession {
id: string;
number: number;
name: string;
config: SessionConfig;
worktreePath: string;
createdAt: number;
sessionUuid: string;
}
interface Session { interface Session {
id: string; id: string;
@ -252,7 +234,6 @@ function createTerminalUI(sessionId: string) {
} }
term.open(sessionElement); term.open(sessionElement);
fitAddon.fit();
term.onData((data) => { term.onData((data) => {
ipcRenderer.send("session-input", sessionId, data); ipcRenderer.send("session-input", sessionId, data);
@ -265,53 +246,13 @@ function createTerminalUI(sessionId: string) {
} }
}); });
// Handle resize - only refit if dimensions actually changed
let lastCols = term.cols;
let lastRows = term.rows;
let resizeTimeout: NodeJS.Timeout | null = null;
const resizeHandler = () => { const resizeHandler = () => {
if (activeSessionId === sessionId) { if (activeSessionId === sessionId) {
// Clear any pending resize const proposedDimensions = fitAddon.proposeDimensions();
if (resizeTimeout) { if (proposedDimensions) {
clearTimeout(resizeTimeout);
}
// Debounce the fit call
resizeTimeout = setTimeout(() => {
// Calculate what the new dimensions would be
const container = sessionElement;
if (!container) return;
const rect = container.getBoundingClientRect();
const core = (term as any)._core;
if (!core) return;
// Estimate new dimensions based on container size
const newCols = Math.floor(rect.width / core._renderService.dimensions.actualCellWidth);
const newRows = Math.floor(rect.height / core._renderService.dimensions.actualCellHeight);
// Only fit if dimensions actually changed significantly (more than 1 char difference)
if (Math.abs(newCols - lastCols) > 1 || Math.abs(newRows - lastRows) > 1) {
// Save scroll position before fitting
const wasAtBottom = term.buffer.active.viewportY === term.buffer.active.baseY;
const savedScrollPosition = term.buffer.active.viewportY;
fitAddon.fit(); fitAddon.fit();
ipcRenderer.send("session-resize", sessionId, proposedDimensions.cols, proposedDimensions.rows);
lastCols = term.cols;
lastRows = term.rows;
// Restore scroll position unless we were at the bottom (in which case stay at bottom)
if (!wasAtBottom && savedScrollPosition !== term.buffer.active.viewportY) {
term.scrollToLine(savedScrollPosition);
} }
ipcRenderer.send("session-resize", sessionId, term.cols, term.rows);
}
resizeTimeout = null;
}, 100); // 100ms debounce
} }
}; };
window.addEventListener("resize", resizeHandler); window.addEventListener("resize", resizeHandler);
@ -599,22 +540,8 @@ function switchToSession(sessionId: string) {
// Focus and resize // Focus and resize
session.terminal.focus(); session.terminal.focus();
setTimeout(() => { // Dispatch resize event to trigger terminal resize
if (session.fitAddon && session.terminal) { window.dispatchEvent(new Event("resize"));
// Save scroll position before fitting
const wasAtBottom = session.terminal.buffer.active.viewportY === session.terminal.buffer.active.baseY;
const savedScrollPosition = session.terminal.buffer.active.viewportY;
session.fitAddon.fit();
// Restore scroll position unless we were at the bottom
if (!wasAtBottom && savedScrollPosition !== session.terminal.buffer.active.viewportY) {
session.terminal.scrollToLine(savedScrollPosition);
}
ipcRenderer.send("session-resize", sessionId, session.terminal.cols, session.terminal.rows);
}
}, 0);
} }
} }

20
types.ts Normal file
View file

@ -0,0 +1,20 @@
export interface SessionConfig {
projectDir: string;
parentBranch: string;
branchName?: string;
codingAgent: string;
skipPermissions: boolean;
setupCommands?: string[];
}
export interface PersistedSession {
id: string;
number: number;
name: string;
config: SessionConfig;
worktreePath: string;
createdAt: number;
sessionUuid: string;
mcpConfigPath?: string;
gitBranch?: string;
}