worktrees
This commit is contained in:
parent
fe287a2f53
commit
1d873aa82a
302
main.ts
302
main.ts
|
|
@ -1,6 +1,8 @@
|
|||
import { app, BrowserWindow, ipcMain, dialog } from "electron";
|
||||
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";
|
||||
|
||||
|
|
@ -10,10 +12,79 @@ interface SessionConfig {
|
|||
codingAgent: string;
|
||||
}
|
||||
|
||||
interface PersistedSession {
|
||||
id: string;
|
||||
number: number;
|
||||
name: string;
|
||||
config: SessionConfig;
|
||||
worktreePath: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
let mainWindow: BrowserWindow;
|
||||
const sessions = new Map<string, pty.IPty>();
|
||||
const activePtyProcesses = new Map<string, pty.IPty>();
|
||||
const store = new Store();
|
||||
|
||||
// Helper functions for session management
|
||||
function getPersistedSessions(): PersistedSession[] {
|
||||
return (store as any).get("sessions", []);
|
||||
}
|
||||
|
||||
function savePersistedSessions(sessions: PersistedSession[]) {
|
||||
(store as any).set("sessions", sessions);
|
||||
}
|
||||
|
||||
function getNextSessionNumber(): number {
|
||||
const sessions = getPersistedSessions();
|
||||
if (sessions.length === 0) return 1;
|
||||
return Math.max(...sessions.map(s => s.number)) + 1;
|
||||
}
|
||||
|
||||
// Git worktree helper functions
|
||||
async function createWorktree(projectDir: string, parentBranch: string, sessionNumber: number): Promise<string> {
|
||||
const git = simpleGit(projectDir);
|
||||
const fleetcodeDir = path.join(projectDir, ".fleetcode");
|
||||
const worktreeName = `session${sessionNumber}`;
|
||||
const worktreePath = path.join(fleetcodeDir, worktreeName);
|
||||
const branchName = `fleetcode/session${sessionNumber}`;
|
||||
|
||||
// Create .fleetcode directory if it doesn't exist
|
||||
if (!fs.existsSync(fleetcodeDir)) {
|
||||
fs.mkdirSync(fleetcodeDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Check if worktree already exists and remove it
|
||||
if (fs.existsSync(worktreePath)) {
|
||||
try {
|
||||
await git.raw(["worktree", "remove", worktreePath, "--force"]);
|
||||
} catch (error) {
|
||||
console.error("Error removing existing worktree:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the branch if it exists
|
||||
try {
|
||||
await git.raw(["branch", "-D", branchName]);
|
||||
} catch (error) {
|
||||
// Branch doesn't exist, that's fine
|
||||
}
|
||||
|
||||
// Create new worktree with a new branch from 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]);
|
||||
|
||||
return worktreePath;
|
||||
}
|
||||
|
||||
async function removeWorktree(projectDir: string, worktreePath: string) {
|
||||
const git = simpleGit(projectDir);
|
||||
try {
|
||||
await git.raw(["worktree", "remove", worktreePath, "--force"]);
|
||||
} catch (error) {
|
||||
console.error("Error removing worktree:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Open directory picker
|
||||
ipcMain.handle("select-directory", async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
|
|
@ -54,19 +125,136 @@ ipcMain.on("save-settings", (_event, config: SessionConfig) => {
|
|||
});
|
||||
|
||||
// Create new session
|
||||
ipcMain.on("create-session", (event, config: SessionConfig) => {
|
||||
const sessionId = `session-${Date.now()}`;
|
||||
const shell = os.platform() === "darwin" ? "zsh" : "bash";
|
||||
ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
||||
try {
|
||||
const sessionNumber = getNextSessionNumber();
|
||||
const sessionId = `session-${Date.now()}`;
|
||||
const sessionName = `Session ${sessionNumber}`;
|
||||
|
||||
// Create git worktree
|
||||
const worktreePath = await createWorktree(config.projectDir, config.parentBranch, sessionNumber);
|
||||
|
||||
// Create persisted session metadata
|
||||
const persistedSession: PersistedSession = {
|
||||
id: sessionId,
|
||||
number: sessionNumber,
|
||||
name: sessionName,
|
||||
config,
|
||||
worktreePath,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
// Save to store
|
||||
const sessions = getPersistedSessions();
|
||||
sessions.push(persistedSession);
|
||||
savePersistedSessions(sessions);
|
||||
|
||||
// Spawn PTY in worktree directory
|
||||
const shell = os.platform() === "darwin" ? "zsh" : "bash";
|
||||
const ptyProcess = pty.spawn(shell, [], {
|
||||
name: "xterm-color",
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
cwd: worktreePath,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
activePtyProcesses.set(sessionId, ptyProcess);
|
||||
|
||||
let terminalReady = false;
|
||||
let dataBuffer = "";
|
||||
|
||||
ptyProcess.onData((data) => {
|
||||
mainWindow.webContents.send("session-output", sessionId, data);
|
||||
|
||||
// Detect when terminal is ready
|
||||
if (!terminalReady) {
|
||||
dataBuffer += data;
|
||||
|
||||
// Method 1: Look for bracketed paste mode enable sequence
|
||||
if (dataBuffer.includes("\x1b[?2004h")) {
|
||||
terminalReady = true;
|
||||
|
||||
// Auto-run the selected coding agent
|
||||
if (config.codingAgent === "claude") {
|
||||
ptyProcess.write("claude\r");
|
||||
} else if (config.codingAgent === "codex") {
|
||||
ptyProcess.write("codex\r");
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Fallback - look for common prompt indicators
|
||||
else if (dataBuffer.includes("$ ") || dataBuffer.includes("% ") ||
|
||||
dataBuffer.includes("> ") || dataBuffer.includes("➜ ") ||
|
||||
dataBuffer.includes("➜ ") ||
|
||||
dataBuffer.includes("✗ ") || dataBuffer.includes("✓ ") ||
|
||||
dataBuffer.endsWith("$") || dataBuffer.endsWith("%") ||
|
||||
dataBuffer.endsWith(">") || dataBuffer.endsWith("➜") ||
|
||||
dataBuffer.endsWith("✗") || dataBuffer.endsWith("✓")) {
|
||||
terminalReady = true;
|
||||
|
||||
// Auto-run the selected coding agent
|
||||
if (config.codingAgent === "claude") {
|
||||
ptyProcess.write("claude\r");
|
||||
} else if (config.codingAgent === "codex") {
|
||||
ptyProcess.write("codex\r");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
event.reply("session-created", sessionId, persistedSession);
|
||||
} catch (error) {
|
||||
console.error("Error creating session:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
event.reply("session-error", errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle session input
|
||||
ipcMain.on("session-input", (_event, sessionId: string, data: string) => {
|
||||
const ptyProcess = activePtyProcesses.get(sessionId);
|
||||
if (ptyProcess) {
|
||||
ptyProcess.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle session resize
|
||||
ipcMain.on("session-resize", (_event, sessionId: string, cols: number, rows: number) => {
|
||||
const ptyProcess = activePtyProcesses.get(sessionId);
|
||||
if (ptyProcess) {
|
||||
ptyProcess.resize(cols, rows);
|
||||
}
|
||||
});
|
||||
|
||||
// Reopen session (spawn new PTY for existing session)
|
||||
ipcMain.on("reopen-session", (event, sessionId: string) => {
|
||||
// Check if PTY already active
|
||||
if (activePtyProcesses.has(sessionId)) {
|
||||
event.reply("session-reopened", sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find persisted session
|
||||
const sessions = getPersistedSessions();
|
||||
const session = sessions.find(s => s.id === sessionId);
|
||||
|
||||
if (!session) {
|
||||
console.error("Session not found:", sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn new PTY in worktree directory
|
||||
const shell = os.platform() === "darwin" ? "zsh" : "bash";
|
||||
const ptyProcess = pty.spawn(shell, [], {
|
||||
name: "xterm-color",
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
cwd: config.projectDir || process.env.HOME,
|
||||
cwd: session.worktreePath,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
sessions.set(sessionId, ptyProcess);
|
||||
activePtyProcesses.set(sessionId, ptyProcess);
|
||||
|
||||
let terminalReady = false;
|
||||
let dataBuffer = "";
|
||||
|
|
@ -74,72 +262,72 @@ ipcMain.on("create-session", (event, config: SessionConfig) => {
|
|||
ptyProcess.onData((data) => {
|
||||
mainWindow.webContents.send("session-output", sessionId, data);
|
||||
|
||||
// Detect when terminal is ready
|
||||
// Detect when terminal is ready and auto-run agent
|
||||
if (!terminalReady) {
|
||||
dataBuffer += data;
|
||||
|
||||
// Method 1: Look for bracketed paste mode enable sequence
|
||||
// This is sent by modern shells (zsh, bash) when ready for input
|
||||
if (dataBuffer.includes("\x1b[?2004h")) {
|
||||
if (dataBuffer.includes("\x1b[?2004h") ||
|
||||
dataBuffer.includes("$ ") || dataBuffer.includes("% ") ||
|
||||
dataBuffer.includes("➜ ") || dataBuffer.includes("✗ ") ||
|
||||
dataBuffer.includes("✓ ")) {
|
||||
terminalReady = true;
|
||||
|
||||
// Auto-run the selected coding agent
|
||||
if (config.codingAgent === "claude") {
|
||||
if (session.config.codingAgent === "claude") {
|
||||
ptyProcess.write("claude\r");
|
||||
} else if (config.codingAgent === "codex") {
|
||||
ptyProcess.write("codex\r");
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Fallback - look for common prompt indicators
|
||||
// In case bracketed paste mode is disabled
|
||||
else if (dataBuffer.includes("$ ") || dataBuffer.includes("% ") ||
|
||||
dataBuffer.includes("> ") || dataBuffer.includes("➜ ") ||
|
||||
dataBuffer.includes("➜ ") ||
|
||||
dataBuffer.includes("✗ ") || dataBuffer.includes("✓ ") ||
|
||||
dataBuffer.endsWith("$") || dataBuffer.endsWith("%") ||
|
||||
dataBuffer.endsWith(">") || dataBuffer.endsWith("➜") ||
|
||||
dataBuffer.endsWith("✗") || dataBuffer.endsWith("✓")) {
|
||||
terminalReady = true;
|
||||
|
||||
// Auto-run the selected coding agent
|
||||
if (config.codingAgent === "claude") {
|
||||
ptyProcess.write("claude\r");
|
||||
} else if (config.codingAgent === "codex") {
|
||||
} else if (session.config.codingAgent === "codex") {
|
||||
ptyProcess.write("codex\r");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
event.reply("session-created", sessionId, config);
|
||||
event.reply("session-reopened", sessionId);
|
||||
});
|
||||
|
||||
// Handle session input
|
||||
ipcMain.on("session-input", (_event, sessionId: string, data: string) => {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle session resize
|
||||
ipcMain.on("session-resize", (_event, sessionId: string, cols: number, rows: number) => {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.resize(cols, rows);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle session close
|
||||
// Close session (kill PTY but keep session)
|
||||
ipcMain.on("close-session", (_event, sessionId: string) => {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.kill();
|
||||
sessions.delete(sessionId);
|
||||
const ptyProcess = activePtyProcesses.get(sessionId);
|
||||
if (ptyProcess) {
|
||||
ptyProcess.kill();
|
||||
activePtyProcesses.delete(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete session (kill PTY, remove worktree, delete from store)
|
||||
ipcMain.on("delete-session", async (_event, sessionId: string) => {
|
||||
// Kill PTY if active
|
||||
const ptyProcess = activePtyProcesses.get(sessionId);
|
||||
if (ptyProcess) {
|
||||
ptyProcess.kill();
|
||||
activePtyProcesses.delete(sessionId);
|
||||
}
|
||||
|
||||
// Find and remove from persisted sessions
|
||||
const sessions = getPersistedSessions();
|
||||
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
|
||||
|
||||
if (sessionIndex === -1) {
|
||||
console.error("Session not found:", sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = sessions[sessionIndex];
|
||||
|
||||
// Remove git worktree
|
||||
await removeWorktree(session.config.projectDir, session.worktreePath);
|
||||
|
||||
// Remove from store
|
||||
sessions.splice(sessionIndex, 1);
|
||||
savePersistedSessions(sessions);
|
||||
|
||||
mainWindow.webContents.send("session-deleted", sessionId);
|
||||
});
|
||||
|
||||
// Get all persisted sessions
|
||||
ipcMain.handle("get-all-sessions", () => {
|
||||
return getPersistedSessions();
|
||||
});
|
||||
|
||||
const createWindow = () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
|
|
@ -151,6 +339,12 @@ const createWindow = () => {
|
|||
});
|
||||
|
||||
mainWindow.loadFile("index.html");
|
||||
|
||||
// Load persisted sessions once window is ready
|
||||
mainWindow.webContents.on("did-finish-load", () => {
|
||||
const sessions = getPersistedSessions();
|
||||
mainWindow.webContents.send("load-persisted-sessions", sessions);
|
||||
});
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
|
|
|
|||
250
renderer.ts
250
renderer.ts
|
|
@ -8,19 +8,30 @@ interface SessionConfig {
|
|||
codingAgent: string;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
interface PersistedSession {
|
||||
id: string;
|
||||
terminal: Terminal;
|
||||
fitAddon: FitAddon;
|
||||
element: HTMLDivElement;
|
||||
number: number;
|
||||
name: string;
|
||||
config: SessionConfig;
|
||||
worktreePath: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
terminal: Terminal | null;
|
||||
fitAddon: FitAddon | null;
|
||||
element: HTMLDivElement | null;
|
||||
name: string;
|
||||
config: SessionConfig;
|
||||
worktreePath: string;
|
||||
hasActivePty: boolean;
|
||||
}
|
||||
|
||||
const sessions = new Map<string, Session>();
|
||||
let activeSessionId: string | null = null;
|
||||
|
||||
function createSession(sessionId: string, name: string, config: SessionConfig) {
|
||||
function createTerminalUI(sessionId: string) {
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
|
|
@ -50,26 +61,6 @@ function createSession(sessionId: string, name: string, config: SessionConfig) {
|
|||
ipcRenderer.send("session-input", sessionId, data);
|
||||
});
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
terminal: term,
|
||||
fitAddon,
|
||||
element: sessionElement,
|
||||
name,
|
||||
config,
|
||||
};
|
||||
|
||||
sessions.set(sessionId, session);
|
||||
|
||||
// Add to sidebar
|
||||
addToSidebar(sessionId, name);
|
||||
|
||||
// Add tab
|
||||
addTab(sessionId, name);
|
||||
|
||||
// Switch to this session
|
||||
switchToSession(sessionId);
|
||||
|
||||
// Handle resize
|
||||
const resizeHandler = () => {
|
||||
if (activeSessionId === sessionId) {
|
||||
|
|
@ -79,10 +70,72 @@ function createSession(sessionId: string, name: string, config: SessionConfig) {
|
|||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
return { terminal: term, fitAddon, element: sessionElement };
|
||||
}
|
||||
|
||||
function addSession(persistedSession: PersistedSession, hasActivePty: boolean) {
|
||||
const session: Session = {
|
||||
id: persistedSession.id,
|
||||
terminal: null,
|
||||
fitAddon: null,
|
||||
element: null,
|
||||
name: persistedSession.name,
|
||||
config: persistedSession.config,
|
||||
worktreePath: persistedSession.worktreePath,
|
||||
hasActivePty,
|
||||
};
|
||||
|
||||
sessions.set(persistedSession.id, session);
|
||||
|
||||
// Add to sidebar
|
||||
addToSidebar(persistedSession.id, persistedSession.name, hasActivePty);
|
||||
|
||||
// Only add tab if terminal is active
|
||||
if (hasActivePty) {
|
||||
addTab(persistedSession.id, persistedSession.name);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
function addToSidebar(sessionId: string, name: string) {
|
||||
function activateSession(sessionId: string) {
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
// If terminal UI doesn't exist yet, create it
|
||||
if (!session.terminal) {
|
||||
const ui = createTerminalUI(sessionId);
|
||||
session.terminal = ui.terminal;
|
||||
session.fitAddon = ui.fitAddon;
|
||||
session.element = ui.element;
|
||||
}
|
||||
|
||||
session.hasActivePty = true;
|
||||
updateSessionState(sessionId, true);
|
||||
|
||||
// Add tab if it doesn't exist
|
||||
if (!document.getElementById(`tab-${sessionId}`)) {
|
||||
addTab(sessionId, session.name);
|
||||
}
|
||||
|
||||
// Switch to this session
|
||||
switchToSession(sessionId);
|
||||
}
|
||||
|
||||
function updateSessionState(sessionId: string, isActive: boolean) {
|
||||
const sidebarItem = document.getElementById(`sidebar-${sessionId}`);
|
||||
const indicator = sidebarItem?.querySelector(".session-indicator");
|
||||
|
||||
if (indicator) {
|
||||
if (isActive) {
|
||||
indicator.classList.add("active");
|
||||
} else {
|
||||
indicator.classList.remove("active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
|
||||
const list = document.getElementById("session-list");
|
||||
if (!list) return;
|
||||
|
||||
|
|
@ -90,25 +143,42 @@ function addToSidebar(sessionId: string, name: string) {
|
|||
item.id = `sidebar-${sessionId}`;
|
||||
item.className = "session-list-item";
|
||||
item.innerHTML = `
|
||||
<span class="truncate">${name}</span>
|
||||
<button class="session-close-btn" data-id="${sessionId}">×</button>
|
||||
<div class="flex items-center space-x-2 flex-1">
|
||||
<span class="session-indicator ${hasActivePty ? 'active' : ''}"></span>
|
||||
<span class="truncate">${name}</span>
|
||||
</div>
|
||||
<button class="session-delete-btn" data-id="${sessionId}" title="Delete session">×</button>
|
||||
`;
|
||||
|
||||
item.addEventListener("click", (e) => {
|
||||
if (!(e.target as HTMLElement).classList.contains("session-close-btn")) {
|
||||
switchToSession(sessionId);
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.classList.contains("session-delete-btn")) {
|
||||
handleSessionClick(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
const closeBtn = item.querySelector(".session-close-btn");
|
||||
closeBtn?.addEventListener("click", (e) => {
|
||||
const deleteBtn = item.querySelector(".session-delete-btn");
|
||||
deleteBtn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
closeSession(sessionId);
|
||||
deleteSession(sessionId);
|
||||
});
|
||||
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
function handleSessionClick(sessionId: string) {
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (session.hasActivePty) {
|
||||
// Just switch to it
|
||||
switchToSession(sessionId);
|
||||
} else {
|
||||
// Reopen the session
|
||||
ipcRenderer.send("reopen-session", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function addTab(sessionId: string, name: string) {
|
||||
const tabsContainer = document.getElementById("tabs");
|
||||
if (!tabsContainer) return;
|
||||
|
|
@ -139,14 +209,16 @@ function addTab(sessionId: string, name: string) {
|
|||
function switchToSession(sessionId: string) {
|
||||
// Hide all sessions
|
||||
sessions.forEach((session, id) => {
|
||||
session.element.classList.remove("active");
|
||||
if (session.element) {
|
||||
session.element.classList.remove("active");
|
||||
}
|
||||
document.getElementById(`tab-${id}`)?.classList.remove("active");
|
||||
document.getElementById(`sidebar-${id}`)?.classList.remove("active");
|
||||
});
|
||||
|
||||
// Show active session
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) {
|
||||
if (session && session.element && session.terminal && session.fitAddon) {
|
||||
session.element.classList.add("active");
|
||||
document.getElementById(`tab-${sessionId}`)?.classList.add("active");
|
||||
document.getElementById(`sidebar-${sessionId}`)?.classList.add("active");
|
||||
|
|
@ -155,8 +227,10 @@ function switchToSession(sessionId: string) {
|
|||
// Focus and resize
|
||||
session.terminal.focus();
|
||||
setTimeout(() => {
|
||||
session.fitAddon.fit();
|
||||
ipcRenderer.send("session-resize", sessionId, session.terminal.cols, session.terminal.rows);
|
||||
if (session.fitAddon && session.terminal) {
|
||||
session.fitAddon.fit();
|
||||
ipcRenderer.send("session-resize", sessionId, session.terminal.cols, session.terminal.rows);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -165,23 +239,70 @@ function closeSession(sessionId: string) {
|
|||
const session = sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
// Remove terminal UI
|
||||
if (session.element) {
|
||||
session.element.remove();
|
||||
}
|
||||
if (session.terminal) {
|
||||
session.terminal.dispose();
|
||||
}
|
||||
|
||||
// Remove tab
|
||||
document.getElementById(`tab-${sessionId}`)?.remove();
|
||||
|
||||
// Update session state
|
||||
session.terminal = null;
|
||||
session.fitAddon = null;
|
||||
session.element = null;
|
||||
session.hasActivePty = false;
|
||||
|
||||
// Update UI indicator
|
||||
updateSessionState(sessionId, false);
|
||||
|
||||
// Close PTY in main process
|
||||
ipcRenderer.send("close-session", sessionId);
|
||||
|
||||
// Switch to another active session
|
||||
if (activeSessionId === sessionId) {
|
||||
const activeSessions = Array.from(sessions.values()).filter(s => s.hasActivePty);
|
||||
if (activeSessions.length > 0) {
|
||||
switchToSession(activeSessions[0].id);
|
||||
} else {
|
||||
activeSessionId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSession(sessionId: string) {
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
// Confirm deletion
|
||||
if (!confirm(`Delete ${session.name}? This will remove the git worktree.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from UI
|
||||
session.element.remove();
|
||||
if (session.element) {
|
||||
session.element.remove();
|
||||
}
|
||||
if (session.terminal) {
|
||||
session.terminal.dispose();
|
||||
}
|
||||
document.getElementById(`tab-${sessionId}`)?.remove();
|
||||
document.getElementById(`sidebar-${sessionId}`)?.remove();
|
||||
|
||||
// Dispose terminal
|
||||
session.terminal.dispose();
|
||||
// Remove from sessions map
|
||||
sessions.delete(sessionId);
|
||||
|
||||
// Close in main process
|
||||
ipcRenderer.send("close-session", sessionId);
|
||||
// Delete in main process (handles worktree removal)
|
||||
ipcRenderer.send("delete-session", sessionId);
|
||||
|
||||
// Switch to another session
|
||||
if (activeSessionId === sessionId) {
|
||||
const remainingSessions = Array.from(sessions.keys());
|
||||
const remainingSessions = Array.from(sessions.values()).filter(s => s.hasActivePty);
|
||||
if (remainingSessions.length > 0) {
|
||||
switchToSession(remainingSessions[0]);
|
||||
switchToSession(remainingSessions[0].id);
|
||||
} else {
|
||||
activeSessionId = null;
|
||||
}
|
||||
|
|
@ -191,15 +312,48 @@ function closeSession(sessionId: string) {
|
|||
// Handle session output
|
||||
ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) {
|
||||
if (session && session.terminal) {
|
||||
session.terminal.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle session created
|
||||
ipcRenderer.on("session-created", (_event, sessionId: string, config: SessionConfig) => {
|
||||
const name = `Session ${sessions.size + 1}`;
|
||||
createSession(sessionId, name, config);
|
||||
ipcRenderer.on("session-created", (_event, sessionId: string, persistedSession: any) => {
|
||||
const session = addSession(persistedSession, true);
|
||||
activateSession(sessionId);
|
||||
});
|
||||
|
||||
// Handle session reopened
|
||||
ipcRenderer.on("session-reopened", (_event, sessionId: string) => {
|
||||
activateSession(sessionId);
|
||||
});
|
||||
|
||||
// Handle session deleted
|
||||
ipcRenderer.on("session-deleted", (_event, sessionId: string) => {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) {
|
||||
if (session.element) session.element.remove();
|
||||
if (session.terminal) session.terminal.dispose();
|
||||
document.getElementById(`tab-${sessionId}`)?.remove();
|
||||
document.getElementById(`sidebar-${sessionId}`)?.remove();
|
||||
sessions.delete(sessionId);
|
||||
|
||||
if (activeSessionId === sessionId) {
|
||||
const remainingSessions = Array.from(sessions.values()).filter(s => s.hasActivePty);
|
||||
if (remainingSessions.length > 0) {
|
||||
switchToSession(remainingSessions[0].id);
|
||||
} else {
|
||||
activeSessionId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load persisted sessions on startup
|
||||
ipcRenderer.on("load-persisted-sessions", (_event, persistedSessions: PersistedSession[]) => {
|
||||
persistedSessions.forEach(ps => {
|
||||
addSession(ps, false);
|
||||
});
|
||||
});
|
||||
|
||||
// Modal handling
|
||||
|
|
|
|||
15
styles.css
15
styles.css
|
|
@ -41,13 +41,22 @@
|
|||
@apply bg-gray-700;
|
||||
}
|
||||
|
||||
.session-close-btn {
|
||||
@apply text-gray-500 hover:text-red-500 ml-2;
|
||||
.session-indicator {
|
||||
@apply w-2 h-2 rounded-full bg-gray-600;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.session-indicator.active {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
|
||||
.session-delete-btn {
|
||||
@apply text-gray-500 hover:text-red-500;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.session-list-item:hover .session-close-btn {
|
||||
.session-list-item:hover .session-delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue