From 0272f79f858079baf43217f62eb4fc43294a5a31 Mon Sep 17 00:00:00 2001 From: Amrit Subramanian Date: Fri, 24 Oct 2025 18:32:33 -0400 Subject: [PATCH] local session --- index.html | 16 +++++++++-- main.ts | 79 +++++++++++++++++++++++++++++++++++++++-------------- renderer.ts | 57 ++++++++++++++++++++++++++++++++++++-- styles.css | 2 +- types.ts | 10 +++++-- 5 files changed, 135 insertions(+), 29 deletions(-) diff --git a/index.html b/index.html index 5ee5d7b..8a23804 100644 --- a/index.html +++ b/index.html @@ -65,16 +65,28 @@
+ + +
+
Creates an isolated git branch in a separate directory. Best for experimenting with changes without affecting your main workspace.
+ +
+
+ +
-
+
- Custom branch name (will also be used as session name) + Custom branch name for worktree (will also be used as session name)
diff --git a/main.ts b/main.ts index 5b41825..6dfcf97 100644 --- a/main.ts +++ b/main.ts @@ -8,9 +8,9 @@ import * as path from "path"; import {simpleGit} from "simple-git"; import {promisify} from "util"; import {v4 as uuidv4} from "uuid"; -import {PersistedSession, SessionConfig} from "./types"; -import {isTerminalReady} from "./terminal-utils"; import {getBranches} from "./git-utils"; +import {isTerminalReady} from "./terminal-utils"; +import {PersistedSession, SessionConfig, SessionType} from "./types"; const execAsync = promisify(exec); @@ -22,7 +22,16 @@ const store = new Store(); // Helper functions for session management function getPersistedSessions(): PersistedSession[] { - return (store as any).get("sessions", []); + const sessions = (store as any).get("sessions", []) as PersistedSession[]; + + // Migrate old sessions that don't have sessionType field + return sessions.map(session => { + if (!session.config.sessionType) { + // If session has a worktreePath, it's a worktree session; otherwise local + session.config.sessionType = session.worktreePath ? SessionType.WORKTREE : SessionType.LOCAL; + } + return session; + }); } function getWorktreeBaseDir(): string { @@ -232,7 +241,7 @@ function parseMcpOutput(output: string): any[] { // Helper function to spawn PTY and setup coding agent function spawnSessionPty( sessionId: string, - worktreePath: string, + workingDirectory: string, config: SessionConfig, sessionUuid: string, isNewSession: boolean, @@ -244,7 +253,7 @@ function spawnSessionPty( name: "xterm-color", cols: 80, rows: 30, - cwd: worktreePath, + cwd: workingDirectory, env: process.env, }); @@ -397,6 +406,7 @@ ipcMain.handle("get-branches", async (_event, dirPath: string) => { ipcMain.handle("get-last-settings", () => { return (store as any).get("lastSessionConfig", { projectDir: "", + sessionType: SessionType.WORKTREE, parentBranch: "", codingAgent: "claude", skipPermissions: true, @@ -417,15 +427,36 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => { // Use custom branch name as session name if provided, otherwise default const sessionName = config.branchName || `Session ${sessionNumber}`; - // Generate UUID for this session (before creating worktree) + // Generate UUID for this session const sessionUuid = uuidv4(); - // Create git worktree with custom or default branch name - const { worktreePath, branchName } = await createWorktree(config.projectDir, config.parentBranch, sessionNumber, sessionUuid, config.branchName); + let worktreePath: string | undefined; + let workingDirectory: string; + let branchName: string | undefined; + let mcpConfigPath: string | undefined; - // Extract and write MCP config - const mcpServers = extractProjectMcpConfig(config.projectDir); - const mcpConfigPath = writeMcpConfigFile(config.projectDir, mcpServers); + if (config.sessionType === SessionType.WORKTREE) { + // Validate that parentBranch is provided for worktree sessions + if (!config.parentBranch) { + throw new Error("Parent branch is required for worktree sessions"); + } + + // Create git worktree with custom or default branch name + const worktreeResult = await createWorktree(config.projectDir, config.parentBranch, sessionNumber, sessionUuid, config.branchName); + worktreePath = worktreeResult.worktreePath; + workingDirectory = worktreeResult.worktreePath; + branchName = worktreeResult.branchName; + + // Extract and write MCP config + const mcpServers = extractProjectMcpConfig(config.projectDir); + mcpConfigPath = writeMcpConfigFile(config.projectDir, mcpServers) || undefined; + } else { + // For local sessions, use the project directory directly (no worktree) + worktreePath = undefined; + workingDirectory = config.projectDir; + branchName = undefined; + mcpConfigPath = undefined; + } // Create persisted session metadata const persistedSession: PersistedSession = { @@ -436,7 +467,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => { worktreePath, createdAt: Date.now(), sessionUuid, - mcpConfigPath: mcpConfigPath || undefined, + mcpConfigPath, gitBranch: branchName, }; @@ -445,8 +476,8 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => { sessions.push(persistedSession); savePersistedSessions(sessions); - // Spawn PTY in worktree directory - spawnSessionPty(sessionId, worktreePath, config, sessionUuid, true, mcpConfigPath || undefined, config.projectDir); + // Spawn PTY in the appropriate directory + spawnSessionPty(sessionId, workingDirectory, config, sessionUuid, true, mcpConfigPath, config.projectDir); event.reply("session-created", sessionId, persistedSession); } catch (error) { @@ -490,8 +521,11 @@ ipcMain.on("reopen-session", (event, sessionId: string) => { return; } - // Spawn new PTY in worktree directory - spawnSessionPty(sessionId, session.worktreePath, session.config, session.sessionUuid, false, session.mcpConfigPath, session.config.projectDir); + // For non-worktree sessions, use project directory; otherwise use worktree path + const workingDir = session.worktreePath || session.config.projectDir; + + // Spawn new PTY in the appropriate directory + spawnSessionPty(sessionId, workingDir, session.config, session.sessionUuid, false, session.mcpConfigPath, session.config.projectDir); event.reply("session-reopened", sessionId); }); @@ -540,12 +574,15 @@ ipcMain.on("delete-session", async (_event, sessionId: string) => { const session = sessions[sessionIndex]; - // Remove git worktree - await removeWorktree(session.config.projectDir, session.worktreePath); + // Only clean up git worktree and branch for worktree sessions + if (session.config.sessionType === SessionType.WORKTREE && session.worktreePath) { + // 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 git branch if it exists + if (session.gitBranch) { + await removeGitBranch(session.config.projectDir, session.gitBranch); + } } // Remove from store diff --git a/renderer.ts b/renderer.ts index 378d2b4..8f14100 100644 --- a/renderer.ts +++ b/renderer.ts @@ -1,7 +1,7 @@ import {FitAddon} from "@xterm/addon-fit"; import {ipcRenderer} from "electron"; import {Terminal} from "xterm"; -import {PersistedSession, SessionConfig} from "./types"; +import {PersistedSession, SessionConfig, SessionType} from "./types"; import {isClaudeSessionReady} from "./terminal-utils"; interface Session { @@ -11,7 +11,7 @@ interface Session { element: HTMLDivElement | null; name: string; config: SessionConfig; - worktreePath: string; + worktreePath?: string; hasActivePty: boolean; } @@ -800,6 +800,11 @@ const parentBranchSelect = document.getElementById("parent-branch") as HTMLSelec const codingAgentSelect = document.getElementById("coding-agent") as HTMLSelectElement; const skipPermissionsCheckbox = document.getElementById("skip-permissions") as HTMLInputElement; const skipPermissionsGroup = skipPermissionsCheckbox?.parentElement?.parentElement; +const sessionTypeSelect = document.getElementById("session-type") as HTMLSelectElement; +const parentBranchGroup = document.getElementById("parent-branch-group"); +const branchNameGroup = document.getElementById("branch-name-group"); +const worktreeDescription = document.getElementById("worktree-description"); +const localDescription = document.getElementById("local-description"); const browseDirBtn = document.getElementById("browse-dir"); const cancelBtn = document.getElementById("cancel-session"); const createBtn = document.getElementById("create-session") as HTMLButtonElement; @@ -815,6 +820,22 @@ codingAgentSelect?.addEventListener("change", () => { } }); +// Toggle parent branch and branch name visibility based on session type +sessionTypeSelect?.addEventListener("change", () => { + const isWorktree = sessionTypeSelect.value === SessionType.WORKTREE; + if (isWorktree) { + parentBranchGroup?.classList.remove("hidden"); + branchNameGroup?.classList.remove("hidden"); + worktreeDescription?.style.setProperty("display", "block"); + localDescription?.style.setProperty("display", "none"); + } else { + parentBranchGroup?.classList.add("hidden"); + branchNameGroup?.classList.add("hidden"); + worktreeDescription?.style.setProperty("display", "none"); + localDescription?.style.setProperty("display", "block"); + } +}); + // New session button - opens modal document.getElementById("new-session")?.addEventListener("click", async () => { modal?.classList.remove("hidden"); @@ -830,6 +851,27 @@ document.getElementById("new-session")?.addEventListener("click", async () => { await loadAndPopulateBranches(lastSettings.projectDir, lastSettings.parentBranch); } + // Set last used session type (default to worktree if not set) + if (lastSettings.sessionType) { + sessionTypeSelect.value = lastSettings.sessionType; + } else { + sessionTypeSelect.value = SessionType.WORKTREE; + } + + // Show/hide parent branch, branch name, and descriptions based on session type + const isWorktree = sessionTypeSelect.value === SessionType.WORKTREE; + if (isWorktree) { + parentBranchGroup?.classList.remove("hidden"); + branchNameGroup?.classList.remove("hidden"); + worktreeDescription?.style.setProperty("display", "block"); + localDescription?.style.setProperty("display", "none"); + } else { + parentBranchGroup?.classList.add("hidden"); + branchNameGroup?.classList.add("hidden"); + worktreeDescription?.style.setProperty("display", "none"); + localDescription?.style.setProperty("display", "block"); + } + // Set last used coding agent if (lastSettings.codingAgent) { codingAgentSelect.value = lastSettings.codingAgent; @@ -881,6 +923,14 @@ createBtn?.addEventListener("click", () => { return; } + const sessionType = sessionTypeSelect.value as SessionType; + + // Validate parent branch is selected for worktree sessions + if (sessionType === SessionType.WORKTREE && !parentBranchSelect.value) { + alert("Please select a parent branch for worktree session"); + return; + } + const setupCommandsTextarea = document.getElementById("setup-commands") as HTMLTextAreaElement; const setupCommandsText = setupCommandsTextarea?.value.trim(); const setupCommands = setupCommandsText @@ -892,7 +942,8 @@ createBtn?.addEventListener("click", () => { const config: SessionConfig = { projectDir: selectedDirectory, - parentBranch: parentBranchSelect.value, + sessionType, + parentBranch: sessionType === SessionType.WORKTREE ? parentBranchSelect.value : undefined, branchName, codingAgent: codingAgentSelect.value, skipPermissions: codingAgentSelect.value === "claude" ? skipPermissionsCheckbox.checked : false, diff --git a/styles.css b/styles.css index 4ac5f75..a1d4d0a 100644 --- a/styles.css +++ b/styles.css @@ -166,7 +166,7 @@ } .modal { - @apply bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6; + @apply bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6 max-h-[90vh] overflow-y-auto; } .modal-title { diff --git a/types.ts b/types.ts index bc80a52..910ab09 100644 --- a/types.ts +++ b/types.ts @@ -1,6 +1,12 @@ +export enum SessionType { + WORKTREE = "worktree", + LOCAL = "local" +} + export interface SessionConfig { projectDir: string; - parentBranch: string; + sessionType: SessionType; + parentBranch?: string; branchName?: string; codingAgent: string; skipPermissions: boolean; @@ -12,7 +18,7 @@ export interface PersistedSession { number: number; name: string; config: SessionConfig; - worktreePath: string; + worktreePath?: string; createdAt: number; sessionUuid: string; mcpConfigPath?: string;