resizing works, delete git branch when deleting session
This commit is contained in:
parent
0de5e77e99
commit
2930290662
61
main.ts
61
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 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();
|
||||||
|
|
|
||||||
91
renderer.ts
91
renderer.ts
|
|
@ -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
20
types.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue