Skip to content

Commit 0097c71

Browse files
authored
local session (#16)
1 parent cdbae45 commit 0097c71

File tree

5 files changed

+135
-29
lines changed

5 files changed

+135
-29
lines changed

index.html

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,28 @@ <h2 class="modal-title">New Session Configuration</h2>
6565
</div>
6666

6767
<div class="form-group">
68+
<label class="form-label">Session Type</label>
69+
<select id="session-type" class="form-select">
70+
<option value="worktree">Worktree</option>
71+
<option value="local">Local</option>
72+
</select>
73+
<div class="text-xs text-gray-400 mt-2">
74+
<div id="worktree-description">Creates an isolated git branch in a separate directory. Best for experimenting with changes without affecting your main workspace.</div>
75+
<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>
76+
</div>
77+
</div>
78+
79+
<div class="form-group" id="parent-branch-group">
6880
<label class="form-label">Parent Branch</label>
6981
<select id="parent-branch" class="form-select">
7082
<option value="">Loading branches...</option>
7183
</select>
7284
</div>
7385

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

8092
<div class="form-group">

main.ts

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import * as path from "path";
88
import {simpleGit} from "simple-git";
99
import {promisify} from "util";
1010
import {v4 as uuidv4} from "uuid";
11-
import {PersistedSession, SessionConfig} from "./types";
12-
import {isTerminalReady} from "./terminal-utils";
1311
import {getBranches} from "./git-utils";
12+
import {isTerminalReady} from "./terminal-utils";
13+
import {PersistedSession, SessionConfig, SessionType} from "./types";
1414

1515
const execAsync = promisify(exec);
1616

@@ -22,7 +22,16 @@ const store = new Store();
2222

2323
// Helper functions for session management
2424
function getPersistedSessions(): PersistedSession[] {
25-
return (store as any).get("sessions", []);
25+
const sessions = (store as any).get("sessions", []) as PersistedSession[];
26+
27+
// Migrate old sessions that don't have sessionType field
28+
return sessions.map(session => {
29+
if (!session.config.sessionType) {
30+
// If session has a worktreePath, it's a worktree session; otherwise local
31+
session.config.sessionType = session.worktreePath ? SessionType.WORKTREE : SessionType.LOCAL;
32+
}
33+
return session;
34+
});
2635
}
2736

2837
function getWorktreeBaseDir(): string {
@@ -232,7 +241,7 @@ function parseMcpOutput(output: string): any[] {
232241
// Helper function to spawn PTY and setup coding agent
233242
function spawnSessionPty(
234243
sessionId: string,
235-
worktreePath: string,
244+
workingDirectory: string,
236245
config: SessionConfig,
237246
sessionUuid: string,
238247
isNewSession: boolean,
@@ -244,7 +253,7 @@ function spawnSessionPty(
244253
name: "xterm-color",
245254
cols: 80,
246255
rows: 30,
247-
cwd: worktreePath,
256+
cwd: workingDirectory,
248257
env: process.env,
249258
});
250259

@@ -397,6 +406,7 @@ ipcMain.handle("get-branches", async (_event, dirPath: string) => {
397406
ipcMain.handle("get-last-settings", () => {
398407
return (store as any).get("lastSessionConfig", {
399408
projectDir: "",
409+
sessionType: SessionType.WORKTREE,
400410
parentBranch: "",
401411
codingAgent: "claude",
402412
skipPermissions: true,
@@ -417,15 +427,36 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
417427
// Use custom branch name as session name if provided, otherwise default
418428
const sessionName = config.branchName || `Session ${sessionNumber}`;
419429

420-
// Generate UUID for this session (before creating worktree)
430+
// Generate UUID for this session
421431
const sessionUuid = uuidv4();
422432

423-
// Create git worktree with custom or default branch name
424-
const { worktreePath, branchName } = await createWorktree(config.projectDir, config.parentBranch, sessionNumber, sessionUuid, config.branchName);
433+
let worktreePath: string | undefined;
434+
let workingDirectory: string;
435+
let branchName: string | undefined;
436+
let mcpConfigPath: string | undefined;
425437

426-
// Extract and write MCP config
427-
const mcpServers = extractProjectMcpConfig(config.projectDir);
428-
const mcpConfigPath = writeMcpConfigFile(config.projectDir, mcpServers);
438+
if (config.sessionType === SessionType.WORKTREE) {
439+
// Validate that parentBranch is provided for worktree sessions
440+
if (!config.parentBranch) {
441+
throw new Error("Parent branch is required for worktree sessions");
442+
}
443+
444+
// Create git worktree with custom or default branch name
445+
const worktreeResult = await createWorktree(config.projectDir, config.parentBranch, sessionNumber, sessionUuid, config.branchName);
446+
worktreePath = worktreeResult.worktreePath;
447+
workingDirectory = worktreeResult.worktreePath;
448+
branchName = worktreeResult.branchName;
449+
450+
// Extract and write MCP config
451+
const mcpServers = extractProjectMcpConfig(config.projectDir);
452+
mcpConfigPath = writeMcpConfigFile(config.projectDir, mcpServers) || undefined;
453+
} else {
454+
// For local sessions, use the project directory directly (no worktree)
455+
worktreePath = undefined;
456+
workingDirectory = config.projectDir;
457+
branchName = undefined;
458+
mcpConfigPath = undefined;
459+
}
429460

430461
// Create persisted session metadata
431462
const persistedSession: PersistedSession = {
@@ -436,7 +467,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
436467
worktreePath,
437468
createdAt: Date.now(),
438469
sessionUuid,
439-
mcpConfigPath: mcpConfigPath || undefined,
470+
mcpConfigPath,
440471
gitBranch: branchName,
441472
};
442473

@@ -445,8 +476,8 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
445476
sessions.push(persistedSession);
446477
savePersistedSessions(sessions);
447478

448-
// Spawn PTY in worktree directory
449-
spawnSessionPty(sessionId, worktreePath, config, sessionUuid, true, mcpConfigPath || undefined, config.projectDir);
479+
// Spawn PTY in the appropriate directory
480+
spawnSessionPty(sessionId, workingDirectory, config, sessionUuid, true, mcpConfigPath, config.projectDir);
450481

451482
event.reply("session-created", sessionId, persistedSession);
452483
} catch (error) {
@@ -490,8 +521,11 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
490521
return;
491522
}
492523

493-
// Spawn new PTY in worktree directory
494-
spawnSessionPty(sessionId, session.worktreePath, session.config, session.sessionUuid, false, session.mcpConfigPath, session.config.projectDir);
524+
// For non-worktree sessions, use project directory; otherwise use worktree path
525+
const workingDir = session.worktreePath || session.config.projectDir;
526+
527+
// Spawn new PTY in the appropriate directory
528+
spawnSessionPty(sessionId, workingDir, session.config, session.sessionUuid, false, session.mcpConfigPath, session.config.projectDir);
495529

496530
event.reply("session-reopened", sessionId);
497531
});
@@ -540,12 +574,15 @@ ipcMain.on("delete-session", async (_event, sessionId: string) => {
540574

541575
const session = sessions[sessionIndex];
542576

543-
// Remove git worktree
544-
await removeWorktree(session.config.projectDir, session.worktreePath);
577+
// Only clean up git worktree and branch for worktree sessions
578+
if (session.config.sessionType === SessionType.WORKTREE && session.worktreePath) {
579+
// Remove git worktree
580+
await removeWorktree(session.config.projectDir, session.worktreePath);
545581

546-
// Remove git branch if it exists
547-
if (session.gitBranch) {
548-
await removeGitBranch(session.config.projectDir, session.gitBranch);
582+
// Remove git branch if it exists
583+
if (session.gitBranch) {
584+
await removeGitBranch(session.config.projectDir, session.gitBranch);
585+
}
549586
}
550587

551588
// Remove from store

renderer.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {FitAddon} from "@xterm/addon-fit";
22
import {ipcRenderer} from "electron";
33
import {Terminal} from "xterm";
4-
import {PersistedSession, SessionConfig} from "./types";
4+
import {PersistedSession, SessionConfig, SessionType} from "./types";
55
import {isClaudeSessionReady} from "./terminal-utils";
66

77
interface Session {
@@ -11,7 +11,7 @@ interface Session {
1111
element: HTMLDivElement | null;
1212
name: string;
1313
config: SessionConfig;
14-
worktreePath: string;
14+
worktreePath?: string;
1515
hasActivePty: boolean;
1616
}
1717

@@ -800,6 +800,11 @@ const parentBranchSelect = document.getElementById("parent-branch") as HTMLSelec
800800
const codingAgentSelect = document.getElementById("coding-agent") as HTMLSelectElement;
801801
const skipPermissionsCheckbox = document.getElementById("skip-permissions") as HTMLInputElement;
802802
const skipPermissionsGroup = skipPermissionsCheckbox?.parentElement?.parentElement;
803+
const sessionTypeSelect = document.getElementById("session-type") as HTMLSelectElement;
804+
const parentBranchGroup = document.getElementById("parent-branch-group");
805+
const branchNameGroup = document.getElementById("branch-name-group");
806+
const worktreeDescription = document.getElementById("worktree-description");
807+
const localDescription = document.getElementById("local-description");
803808
const browseDirBtn = document.getElementById("browse-dir");
804809
const cancelBtn = document.getElementById("cancel-session");
805810
const createBtn = document.getElementById("create-session") as HTMLButtonElement;
@@ -815,6 +820,22 @@ codingAgentSelect?.addEventListener("change", () => {
815820
}
816821
});
817822

823+
// Toggle parent branch and branch name visibility based on session type
824+
sessionTypeSelect?.addEventListener("change", () => {
825+
const isWorktree = sessionTypeSelect.value === SessionType.WORKTREE;
826+
if (isWorktree) {
827+
parentBranchGroup?.classList.remove("hidden");
828+
branchNameGroup?.classList.remove("hidden");
829+
worktreeDescription?.style.setProperty("display", "block");
830+
localDescription?.style.setProperty("display", "none");
831+
} else {
832+
parentBranchGroup?.classList.add("hidden");
833+
branchNameGroup?.classList.add("hidden");
834+
worktreeDescription?.style.setProperty("display", "none");
835+
localDescription?.style.setProperty("display", "block");
836+
}
837+
});
838+
818839
// New session button - opens modal
819840
document.getElementById("new-session")?.addEventListener("click", async () => {
820841
modal?.classList.remove("hidden");
@@ -830,6 +851,27 @@ document.getElementById("new-session")?.addEventListener("click", async () => {
830851
await loadAndPopulateBranches(lastSettings.projectDir, lastSettings.parentBranch);
831852
}
832853

854+
// Set last used session type (default to worktree if not set)
855+
if (lastSettings.sessionType) {
856+
sessionTypeSelect.value = lastSettings.sessionType;
857+
} else {
858+
sessionTypeSelect.value = SessionType.WORKTREE;
859+
}
860+
861+
// Show/hide parent branch, branch name, and descriptions based on session type
862+
const isWorktree = sessionTypeSelect.value === SessionType.WORKTREE;
863+
if (isWorktree) {
864+
parentBranchGroup?.classList.remove("hidden");
865+
branchNameGroup?.classList.remove("hidden");
866+
worktreeDescription?.style.setProperty("display", "block");
867+
localDescription?.style.setProperty("display", "none");
868+
} else {
869+
parentBranchGroup?.classList.add("hidden");
870+
branchNameGroup?.classList.add("hidden");
871+
worktreeDescription?.style.setProperty("display", "none");
872+
localDescription?.style.setProperty("display", "block");
873+
}
874+
833875
// Set last used coding agent
834876
if (lastSettings.codingAgent) {
835877
codingAgentSelect.value = lastSettings.codingAgent;
@@ -881,6 +923,14 @@ createBtn?.addEventListener("click", () => {
881923
return;
882924
}
883925

926+
const sessionType = sessionTypeSelect.value as SessionType;
927+
928+
// Validate parent branch is selected for worktree sessions
929+
if (sessionType === SessionType.WORKTREE && !parentBranchSelect.value) {
930+
alert("Please select a parent branch for worktree session");
931+
return;
932+
}
933+
884934
const setupCommandsTextarea = document.getElementById("setup-commands") as HTMLTextAreaElement;
885935
const setupCommandsText = setupCommandsTextarea?.value.trim();
886936
const setupCommands = setupCommandsText
@@ -892,7 +942,8 @@ createBtn?.addEventListener("click", () => {
892942

893943
const config: SessionConfig = {
894944
projectDir: selectedDirectory,
895-
parentBranch: parentBranchSelect.value,
945+
sessionType,
946+
parentBranch: sessionType === SessionType.WORKTREE ? parentBranchSelect.value : undefined,
896947
branchName,
897948
codingAgent: codingAgentSelect.value,
898949
skipPermissions: codingAgentSelect.value === "claude" ? skipPermissionsCheckbox.checked : false,

styles.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@
166166
}
167167

168168
.modal {
169-
@apply bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6;
169+
@apply bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6 max-h-[90vh] overflow-y-auto;
170170
}
171171

172172
.modal-title {

types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
export enum SessionType {
2+
WORKTREE = "worktree",
3+
LOCAL = "local"
4+
}
5+
16
export interface SessionConfig {
27
projectDir: string;
3-
parentBranch: string;
8+
sessionType: SessionType;
9+
parentBranch?: string;
410
branchName?: string;
511
codingAgent: string;
612
skipPermissions: boolean;
@@ -12,7 +18,7 @@ export interface PersistedSession {
1218
number: number;
1319
name: string;
1420
config: SessionConfig;
15-
worktreePath: string;
21+
worktreePath?: string;
1622
createdAt: number;
1723
sessionUuid: string;
1824
mcpConfigPath?: string;

0 commit comments

Comments
 (0)