diff --git a/index.html b/index.html
index 5ee5d7b..8a23804 100644
--- a/index.html
+++ b/index.html
@@ -65,16 +65,28 @@
+
- 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;