diff --git a/main.ts b/main.ts index 988cb1f..0190018 100644 --- a/main.ts +++ b/main.ts @@ -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 os from "os"; import * as path from "path"; -import * as fs from "fs"; -import { simpleGit } from "simple-git"; -import Store from "electron-store"; -import { exec } from "child_process"; -import { promisify } from "util"; -import { v4 as uuidv4 } from "uuid"; +import {simpleGit} from "simple-git"; +import {promisify} from "util"; +import {v4 as uuidv4} from "uuid"; +import {PersistedSession, SessionConfig} from "./types"; 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; const activePtyProcesses = new Map(); const mcpPollerPtyProcesses = new Map(); @@ -344,10 +325,10 @@ async function ensureFleetcodeExcluded(projectDir: string) { } } -async function createWorktree(projectDir: string, parentBranch: string, sessionNumber: number, sessionUuid: string, customBranchName?: string): Promise { +async function createWorktree(projectDir: string, parentBranch: string, sessionNumber: number, sessionUuid: string, customBranchName?: string): Promise<{ worktreePath: string; branchName: string }> { const git = simpleGit(projectDir); const fleetcodeDir = path.join(projectDir, ".fleetcode"); - const worktreeName = `session${sessionNumber}`; + const worktreeName = customBranchName || `session${sessionNumber}`; const worktreePath = path.join(fleetcodeDir, worktreeName); // Use custom branch name if provided, otherwise generate default @@ -357,7 +338,7 @@ async function createWorktree(projectDir: string, parentBranch: string, sessionN } else { // Include short UUID to ensure branch uniqueness across deletes/recreates const shortUuid = sessionUuid.split('-')[0]; - branchName = `fleetcode/session${sessionNumber}-${shortUuid}`; + branchName = `fleetcode/${worktreeName}-${shortUuid}`; } // 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" starting from the parent branch await git.raw(["worktree", "add", "-b", branchName, worktreePath, parentBranch]); - return worktreePath; + return { worktreePath, branchName }; } 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 ipcMain.handle("select-directory", async () => { const result = await dialog.showOpenDialog(mainWindow, { @@ -453,7 +443,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => { ensureFleetcodeExcluded(config.projectDir); // 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 const mcpServers = extractProjectMcpConfig(config.projectDir); @@ -469,6 +459,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => { createdAt: Date.now(), sessionUuid, mcpConfigPath: mcpConfigPath || undefined, + gitBranch: branchName, }; // Save to store @@ -572,6 +563,11 @@ ipcMain.on("delete-session", async (_event, sessionId: string) => { // Remove git worktree 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 sessions.splice(sessionIndex, 1); savePersistedSessions(sessions); @@ -778,6 +774,7 @@ const createWindow = () => { app.whenReady().then(() => { createWindow(); + // Handles launch from dock on macos app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); diff --git a/renderer.ts b/renderer.ts index 44a6e46..0754932 100644 --- a/renderer.ts +++ b/renderer.ts @@ -1,25 +1,7 @@ -import { ipcRenderer } from "electron"; -import { Terminal } from "xterm"; -import { FitAddon } from "@xterm/addon-fit"; - -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; -} +import {FitAddon} from "@xterm/addon-fit"; +import {ipcRenderer} from "electron"; +import {Terminal} from "xterm"; +import {PersistedSession, SessionConfig} from "./types"; interface Session { id: string; @@ -252,7 +234,6 @@ function createTerminalUI(sessionId: string) { } term.open(sessionElement); - fitAddon.fit(); term.onData((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 = () => { if (activeSessionId === sessionId) { - // Clear any pending resize - if (resizeTimeout) { - clearTimeout(resizeTimeout); + const proposedDimensions = fitAddon.proposeDimensions(); + if (proposedDimensions) { + fitAddon.fit(); + ipcRenderer.send("session-resize", sessionId, proposedDimensions.cols, proposedDimensions.rows); } - - // 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(); - - 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); @@ -599,22 +540,8 @@ function switchToSession(sessionId: string) { // Focus and resize session.terminal.focus(); - setTimeout(() => { - if (session.fitAddon && session.terminal) { - // 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); + // Dispatch resize event to trigger terminal resize + window.dispatchEvent(new Event("resize")); } } diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..bc80a52 --- /dev/null +++ b/types.ts @@ -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; +}