diff --git a/main.ts b/main.ts
index 8bdd161..bc778dc 100644
--- a/main.ts
+++ b/main.ts
@@ -48,6 +48,78 @@ function getNextSessionNumber(): number {
return Math.max(...sessions.map(s => s.number)) + 1;
}
+// Helper function to spawn PTY and setup coding agent
+function spawnSessionPty(
+ sessionId: string,
+ worktreePath: string,
+ config: SessionConfig,
+ sessionUuid: string,
+ isNewSession: boolean
+) {
+ const shell = os.platform() === "darwin" ? "zsh" : "bash";
+ const ptyProcess = pty.spawn(shell, ["-l"], {
+ name: "xterm-color",
+ cols: 80,
+ rows: 30,
+ cwd: worktreePath,
+ env: process.env,
+ });
+
+ activePtyProcesses.set(sessionId, ptyProcess);
+
+ let terminalReady = false;
+ let dataBuffer = "";
+
+ ptyProcess.onData((data) => {
+ // Only send data if window still exists and is not destroyed
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ 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
+ // Method 2: Fallback - look for common prompt indicators
+ const isReady = dataBuffer.includes("\x1b[?2004h") ||
+ 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("✓");
+
+ if (isReady) {
+ terminalReady = true;
+
+ // Run setup commands if provided
+ if (config.setupCommands && config.setupCommands.length > 0) {
+ config.setupCommands.forEach(cmd => {
+ ptyProcess.write(cmd + "\r");
+ });
+ }
+
+ // Auto-run the selected coding agent
+ if (config.codingAgent === "claude") {
+ const sessionFlag = isNewSession
+ ? `--session-id ${sessionUuid}`
+ : `--resume ${sessionUuid}`;
+ const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
+ const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
+ const claudeCmd = `claude ${flags}\r`;
+ ptyProcess.write(claudeCmd);
+ } else if (config.codingAgent === "codex") {
+ ptyProcess.write("codex\r");
+ }
+ }
+ }
+ });
+
+ return ptyProcess;
+}
+
// Git worktree helper functions
async function ensureFleetcodeExcluded(projectDir: string) {
// Check if we've already initialized this project (persisted across app restarts)
@@ -204,85 +276,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
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) => {
- // Only send data if window still exists and is not destroyed
- if (mainWindow && !mainWindow.isDestroyed()) {
- 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;
-
- // Run setup commands if provided
- if (config.setupCommands && config.setupCommands.length > 0) {
- config.setupCommands.forEach(cmd => {
- ptyProcess.write(cmd + "\r");
- });
- }
-
- // Auto-run the selected coding agent
- if (config.codingAgent === "claude") {
- // New session always uses --session-id
- const sessionFlag = `--session-id ${sessionUuid}`;
- const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
- const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
- const claudeCmd = `claude ${flags}\r`;
- ptyProcess.write(claudeCmd);
- } 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;
-
- // Run setup commands if provided
- if (config.setupCommands && config.setupCommands.length > 0) {
- config.setupCommands.forEach(cmd => {
- ptyProcess.write(cmd + "\r");
- });
- }
-
- // Auto-run the selected coding agent
- if (config.codingAgent === "claude") {
- // New session always uses --session-id
- const sessionFlag = `--session-id ${sessionUuid}`;
- const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
- const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
- const claudeCmd = `claude ${flags}\r`;
- ptyProcess.write(claudeCmd);
- } else if (config.codingAgent === "codex") {
- ptyProcess.write("codex\r");
- }
- }
- }
- });
+ spawnSessionPty(sessionId, worktreePath, config, sessionUuid, true);
event.reply("session-created", sessionId, persistedSession);
} catch (error) {
@@ -326,56 +320,7 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
}
// 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: session.worktreePath,
- env: process.env,
- });
-
- activePtyProcesses.set(sessionId, ptyProcess);
-
- let terminalReady = false;
- let dataBuffer = "";
-
- ptyProcess.onData((data) => {
- // Only send data if window still exists and is not destroyed
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send("session-output", sessionId, data);
- }
-
- // Detect when terminal is ready and auto-run agent
- if (!terminalReady) {
- dataBuffer += data;
-
- if (dataBuffer.includes("\x1b[?2004h") ||
- dataBuffer.includes("$ ") || dataBuffer.includes("% ") ||
- dataBuffer.includes("➜ ") || dataBuffer.includes("✗ ") ||
- dataBuffer.includes("✓ ")) {
- terminalReady = true;
-
- // Run setup commands if provided
- if (session.config.setupCommands && session.config.setupCommands.length > 0) {
- session.config.setupCommands.forEach(cmd => {
- ptyProcess.write(cmd + "\r");
- });
- }
-
- if (session.config.codingAgent === "claude") {
- // Reopened session always uses --resume
- const sessionFlag = `--resume ${session.sessionUuid}`;
- const skipPermissionsFlag = session.config.skipPermissions ? "--dangerously-skip-permissions" : "";
- const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
- const claudeCmd = `claude ${flags}\r`;
- ptyProcess.write(claudeCmd);
- } else if (session.config.codingAgent === "codex") {
- ptyProcess.write("codex\r");
- }
- }
- }
- });
+ spawnSessionPty(sessionId, session.worktreePath, session.config, session.sessionUuid, false);
event.reply("session-reopened", sessionId);
});
diff --git a/package.json b/package.json
index 9696358..3d1bcac 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,12 @@
"index.html",
"node_modules/**/*"
],
+ "publish": [
+ {
+ "provider": "github",
+ "releaseType": "release"
+ }
+ ],
"mac": {
"category": "public.app-category.developer-tools",
"target": [
diff --git a/renderer.ts b/renderer.ts
index f62efda..779a739 100644
--- a/renderer.ts
+++ b/renderer.ts
@@ -740,6 +740,29 @@ ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
ipcRenderer.on("session-created", (_event, sessionId: string, persistedSession: any) => {
const session = addSession(persistedSession, true);
activateSession(sessionId);
+
+ // Reset button state and close modal
+ const createBtn = document.getElementById("create-session") as HTMLButtonElement;
+ const modal = document.getElementById("config-modal");
+ const projectDirInput = document.getElementById("project-dir") as HTMLInputElement;
+ const parentBranchSelect = document.getElementById("parent-branch") as HTMLSelectElement;
+ const setupCommandsTextarea = document.getElementById("setup-commands") as HTMLTextAreaElement;
+
+ if (createBtn) {
+ createBtn.disabled = false;
+ createBtn.textContent = "Create Session";
+ createBtn.classList.remove("loading");
+ }
+
+ modal?.classList.add("hidden");
+
+ // Reset form
+ projectDirInput.value = "";
+ selectedDirectory = "";
+ parentBranchSelect.innerHTML = '';
+ if (setupCommandsTextarea) {
+ setupCommandsTextarea.value = "";
+ }
});
// Handle session reopened
@@ -784,7 +807,7 @@ const skipPermissionsCheckbox = document.getElementById("skip-permissions") as H
const skipPermissionsGroup = skipPermissionsCheckbox?.parentElement?.parentElement;
const browseDirBtn = document.getElementById("browse-dir");
const cancelBtn = document.getElementById("cancel-session");
-const createBtn = document.getElementById("create-session");
+const createBtn = document.getElementById("create-session") as HTMLButtonElement;
let selectedDirectory = "";
@@ -904,21 +927,18 @@ createBtn?.addEventListener("click", () => {
setupCommands,
};
+ // Show loading state
+ if (createBtn) {
+ createBtn.disabled = true;
+ createBtn.innerHTML = ' Creating...';
+ createBtn.classList.add("loading");
+ }
+
// Save settings for next time
ipcRenderer.send("save-settings", config);
// Create the session
ipcRenderer.send("create-session", config);
- modal?.classList.add("hidden");
-
- // Reset form
- projectDirInput.value = "";
- selectedDirectory = "";
- parentBranchSelect.innerHTML = '';
- codingAgentSelect.value = "claude";
- if (setupCommandsTextarea) {
- setupCommandsTextarea.value = "";
- }
});
// MCP Server management functions
diff --git a/styles.css b/styles.css
index 55ec8ae..f2650ee 100644
--- a/styles.css
+++ b/styles.css
@@ -208,4 +208,12 @@
.btn-icon {
@apply bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded text-sm font-medium transition;
}
+
+ .btn-primary.loading {
+ @apply opacity-75 cursor-not-allowed;
+ }
+
+ .btn-primary.loading .loading-spinner {
+ @apply border-white border-t-transparent;
+ }
}