Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,28 @@ <h2 class="modal-title">New Session Configuration</h2>
</div>

<div class="form-group">
<label class="form-label">Session Type</label>
<select id="session-type" class="form-select">
<option value="worktree">Worktree</option>
<option value="local">Local</option>
</select>
<div class="text-xs text-gray-400 mt-2">
<div id="worktree-description">Creates an isolated git branch in a separate directory. Best for experimenting with changes without affecting your main workspace.</div>
<div id="local-description" style="display: none;">Works directly in your project directory. Best when you need to see changes immediately (e.g., app restart required) or working on non-git projects.</div>
</div>
</div>

<div class="form-group" id="parent-branch-group">
<label class="form-label">Parent Branch</label>
<select id="parent-branch" class="form-select">
<option value="">Loading branches...</option>
</select>
</div>

<div class="form-group">
<div class="form-group" id="branch-name-group">
<label class="form-label">Branch Name (optional)</label>
<input type="text" id="branch-name" class="form-input" placeholder="e.g., feature/add-login" />
<span class="text-xs text-gray-400 mt-1 block">Custom branch name (will also be used as session name)</span>
<span class="text-xs text-gray-400 mt-1 block">Custom branch name for worktree (will also be used as session name)</span>
</div>

<div class="form-group">
Expand Down
79 changes: 58 additions & 21 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -244,7 +253,7 @@ function spawnSessionPty(
name: "xterm-color",
cols: 80,
rows: 30,
cwd: worktreePath,
cwd: workingDirectory,
env: process.env,
});

Expand Down Expand Up @@ -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,
Expand All @@ -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 = {
Expand All @@ -436,7 +467,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
worktreePath,
createdAt: Date.now(),
sessionUuid,
mcpConfigPath: mcpConfigPath || undefined,
mcpConfigPath,
gitBranch: branchName,
};

Expand All @@ -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) {
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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
Expand Down
57 changes: 54 additions & 3 deletions renderer.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -11,7 +11,7 @@ interface Session {
element: HTMLDivElement | null;
name: string;
config: SessionConfig;
worktreePath: string;
worktreePath?: string;
hasActivePty: boolean;
}

Expand Down Expand Up @@ -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;
Expand All @@ -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");
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,7 +18,7 @@ export interface PersistedSession {
number: number;
name: string;
config: SessionConfig;
worktreePath: string;
worktreePath?: string;
createdAt: number;
sessionUuid: string;
mcpConfigPath?: string;
Expand Down