Skip to content

Commit cd4e0f6

Browse files
committed
Add login shell support, session creation loading state, and refactoring
- Spawn PTY as login shell (-l flag) to properly source shell configs (fixes pyenv issues) - Add animated loading spinner to session creation button - Refactor duplicate PTY spawning logic into shared spawnSessionPty() function - Fix unread indicator bugs (clear timers on session switch/close/delete) - Configure electron-builder to publish releases instead of drafts - Remove Windows builds (node-pty compilation issues)
1 parent 0a1c6fd commit cd4e0f6

File tree

4 files changed

+119
-140
lines changed

4 files changed

+119
-140
lines changed

main.ts

Lines changed: 74 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,78 @@ function getNextSessionNumber(): number {
4848
return Math.max(...sessions.map(s => s.number)) + 1;
4949
}
5050

51+
// Helper function to spawn PTY and setup coding agent
52+
function spawnSessionPty(
53+
sessionId: string,
54+
worktreePath: string,
55+
config: SessionConfig,
56+
sessionUuid: string,
57+
isNewSession: boolean
58+
) {
59+
const shell = os.platform() === "darwin" ? "zsh" : "bash";
60+
const ptyProcess = pty.spawn(shell, ["-l"], {
61+
name: "xterm-color",
62+
cols: 80,
63+
rows: 30,
64+
cwd: worktreePath,
65+
env: process.env,
66+
});
67+
68+
activePtyProcesses.set(sessionId, ptyProcess);
69+
70+
let terminalReady = false;
71+
let dataBuffer = "";
72+
73+
ptyProcess.onData((data) => {
74+
// Only send data if window still exists and is not destroyed
75+
if (mainWindow && !mainWindow.isDestroyed()) {
76+
mainWindow.webContents.send("session-output", sessionId, data);
77+
}
78+
79+
// Detect when terminal is ready
80+
if (!terminalReady) {
81+
dataBuffer += data;
82+
83+
// Method 1: Look for bracketed paste mode enable sequence
84+
// Method 2: Fallback - look for common prompt indicators
85+
const isReady = dataBuffer.includes("\x1b[?2004h") ||
86+
dataBuffer.includes("$ ") || dataBuffer.includes("% ") ||
87+
dataBuffer.includes("> ") || dataBuffer.includes("➜ ") ||
88+
dataBuffer.includes("➜ ") || dataBuffer.includes("✗ ") ||
89+
dataBuffer.includes("✓ ") || dataBuffer.endsWith("$") ||
90+
dataBuffer.endsWith("%") || dataBuffer.endsWith(">") ||
91+
dataBuffer.endsWith("➜") || dataBuffer.endsWith("✗") ||
92+
dataBuffer.endsWith("✓");
93+
94+
if (isReady) {
95+
terminalReady = true;
96+
97+
// Run setup commands if provided
98+
if (config.setupCommands && config.setupCommands.length > 0) {
99+
config.setupCommands.forEach(cmd => {
100+
ptyProcess.write(cmd + "\r");
101+
});
102+
}
103+
104+
// Auto-run the selected coding agent
105+
if (config.codingAgent === "claude") {
106+
const sessionFlag = isNewSession
107+
? `--session-id ${sessionUuid}`
108+
: `--resume ${sessionUuid}`;
109+
const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
110+
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
111+
const claudeCmd = `claude ${flags}\r`;
112+
ptyProcess.write(claudeCmd);
113+
} else if (config.codingAgent === "codex") {
114+
ptyProcess.write("codex\r");
115+
}
116+
}
117+
}
118+
});
119+
120+
return ptyProcess;
121+
}
122+
51123
// Git worktree helper functions
52124
async function ensureFleetcodeExcluded(projectDir: string) {
53125
// Check if we've already initialized this project (persisted across app restarts)
@@ -204,85 +276,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
204276
savePersistedSessions(sessions);
205277

206278
// Spawn PTY in worktree directory
207-
const shell = os.platform() === "darwin" ? "zsh" : "bash";
208-
const ptyProcess = pty.spawn(shell, [], {
209-
name: "xterm-color",
210-
cols: 80,
211-
rows: 30,
212-
cwd: worktreePath,
213-
env: process.env,
214-
});
215-
216-
activePtyProcesses.set(sessionId, ptyProcess);
217-
218-
let terminalReady = false;
219-
let dataBuffer = "";
220-
221-
ptyProcess.onData((data) => {
222-
// Only send data if window still exists and is not destroyed
223-
if (mainWindow && !mainWindow.isDestroyed()) {
224-
mainWindow.webContents.send("session-output", sessionId, data);
225-
}
226-
227-
// Detect when terminal is ready
228-
if (!terminalReady) {
229-
dataBuffer += data;
230-
231-
// Method 1: Look for bracketed paste mode enable sequence
232-
if (dataBuffer.includes("\x1b[?2004h")) {
233-
terminalReady = true;
234-
235-
// Run setup commands if provided
236-
if (config.setupCommands && config.setupCommands.length > 0) {
237-
config.setupCommands.forEach(cmd => {
238-
ptyProcess.write(cmd + "\r");
239-
});
240-
}
241-
242-
// Auto-run the selected coding agent
243-
if (config.codingAgent === "claude") {
244-
// New session always uses --session-id
245-
const sessionFlag = `--session-id ${sessionUuid}`;
246-
const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
247-
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
248-
const claudeCmd = `claude ${flags}\r`;
249-
ptyProcess.write(claudeCmd);
250-
} else if (config.codingAgent === "codex") {
251-
ptyProcess.write("codex\r");
252-
}
253-
}
254-
255-
// Method 2: Fallback - look for common prompt indicators
256-
else if (dataBuffer.includes("$ ") || dataBuffer.includes("% ") ||
257-
dataBuffer.includes("> ") || dataBuffer.includes("➜ ") ||
258-
dataBuffer.includes("➜ ") ||
259-
dataBuffer.includes("✗ ") || dataBuffer.includes("✓ ") ||
260-
dataBuffer.endsWith("$") || dataBuffer.endsWith("%") ||
261-
dataBuffer.endsWith(">") || dataBuffer.endsWith("➜") ||
262-
dataBuffer.endsWith("✗") || dataBuffer.endsWith("✓")) {
263-
terminalReady = true;
264-
265-
// Run setup commands if provided
266-
if (config.setupCommands && config.setupCommands.length > 0) {
267-
config.setupCommands.forEach(cmd => {
268-
ptyProcess.write(cmd + "\r");
269-
});
270-
}
271-
272-
// Auto-run the selected coding agent
273-
if (config.codingAgent === "claude") {
274-
// New session always uses --session-id
275-
const sessionFlag = `--session-id ${sessionUuid}`;
276-
const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
277-
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
278-
const claudeCmd = `claude ${flags}\r`;
279-
ptyProcess.write(claudeCmd);
280-
} else if (config.codingAgent === "codex") {
281-
ptyProcess.write("codex\r");
282-
}
283-
}
284-
}
285-
});
279+
spawnSessionPty(sessionId, worktreePath, config, sessionUuid, true);
286280

287281
event.reply("session-created", sessionId, persistedSession);
288282
} catch (error) {
@@ -326,56 +320,7 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
326320
}
327321

328322
// Spawn new PTY in worktree directory
329-
const shell = os.platform() === "darwin" ? "zsh" : "bash";
330-
const ptyProcess = pty.spawn(shell, [], {
331-
name: "xterm-color",
332-
cols: 80,
333-
rows: 30,
334-
cwd: session.worktreePath,
335-
env: process.env,
336-
});
337-
338-
activePtyProcesses.set(sessionId, ptyProcess);
339-
340-
let terminalReady = false;
341-
let dataBuffer = "";
342-
343-
ptyProcess.onData((data) => {
344-
// Only send data if window still exists and is not destroyed
345-
if (mainWindow && !mainWindow.isDestroyed()) {
346-
mainWindow.webContents.send("session-output", sessionId, data);
347-
}
348-
349-
// Detect when terminal is ready and auto-run agent
350-
if (!terminalReady) {
351-
dataBuffer += data;
352-
353-
if (dataBuffer.includes("\x1b[?2004h") ||
354-
dataBuffer.includes("$ ") || dataBuffer.includes("% ") ||
355-
dataBuffer.includes("➜ ") || dataBuffer.includes("✗ ") ||
356-
dataBuffer.includes("✓ ")) {
357-
terminalReady = true;
358-
359-
// Run setup commands if provided
360-
if (session.config.setupCommands && session.config.setupCommands.length > 0) {
361-
session.config.setupCommands.forEach(cmd => {
362-
ptyProcess.write(cmd + "\r");
363-
});
364-
}
365-
366-
if (session.config.codingAgent === "claude") {
367-
// Reopened session always uses --resume
368-
const sessionFlag = `--resume ${session.sessionUuid}`;
369-
const skipPermissionsFlag = session.config.skipPermissions ? "--dangerously-skip-permissions" : "";
370-
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
371-
const claudeCmd = `claude ${flags}\r`;
372-
ptyProcess.write(claudeCmd);
373-
} else if (session.config.codingAgent === "codex") {
374-
ptyProcess.write("codex\r");
375-
}
376-
}
377-
}
378-
});
323+
spawnSessionPty(sessionId, session.worktreePath, session.config, session.sessionUuid, false);
379324

380325
event.reply("session-reopened", sessionId);
381326
});

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
"index.html",
4848
"node_modules/**/*"
4949
],
50+
"publish": [
51+
{
52+
"provider": "github",
53+
"releaseType": "release"
54+
}
55+
],
5056
"mac": {
5157
"category": "public.app-category.developer-tools",
5258
"target": [

renderer.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,29 @@ ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
740740
ipcRenderer.on("session-created", (_event, sessionId: string, persistedSession: any) => {
741741
const session = addSession(persistedSession, true);
742742
activateSession(sessionId);
743+
744+
// Reset button state and close modal
745+
const createBtn = document.getElementById("create-session") as HTMLButtonElement;
746+
const modal = document.getElementById("config-modal");
747+
const projectDirInput = document.getElementById("project-dir") as HTMLInputElement;
748+
const parentBranchSelect = document.getElementById("parent-branch") as HTMLSelectElement;
749+
const setupCommandsTextarea = document.getElementById("setup-commands") as HTMLTextAreaElement;
750+
751+
if (createBtn) {
752+
createBtn.disabled = false;
753+
createBtn.textContent = "Create Session";
754+
createBtn.classList.remove("loading");
755+
}
756+
757+
modal?.classList.add("hidden");
758+
759+
// Reset form
760+
projectDirInput.value = "";
761+
selectedDirectory = "";
762+
parentBranchSelect.innerHTML = '<option value="">Loading branches...</option>';
763+
if (setupCommandsTextarea) {
764+
setupCommandsTextarea.value = "";
765+
}
743766
});
744767

745768
// Handle session reopened
@@ -784,7 +807,7 @@ const skipPermissionsCheckbox = document.getElementById("skip-permissions") as H
784807
const skipPermissionsGroup = skipPermissionsCheckbox?.parentElement?.parentElement;
785808
const browseDirBtn = document.getElementById("browse-dir");
786809
const cancelBtn = document.getElementById("cancel-session");
787-
const createBtn = document.getElementById("create-session");
810+
const createBtn = document.getElementById("create-session") as HTMLButtonElement;
788811

789812
let selectedDirectory = "";
790813

@@ -904,21 +927,18 @@ createBtn?.addEventListener("click", () => {
904927
setupCommands,
905928
};
906929

930+
// Show loading state
931+
if (createBtn) {
932+
createBtn.disabled = true;
933+
createBtn.innerHTML = '<span class="loading-spinner"></span> Creating...';
934+
createBtn.classList.add("loading");
935+
}
936+
907937
// Save settings for next time
908938
ipcRenderer.send("save-settings", config);
909939

910940
// Create the session
911941
ipcRenderer.send("create-session", config);
912-
modal?.classList.add("hidden");
913-
914-
// Reset form
915-
projectDirInput.value = "";
916-
selectedDirectory = "";
917-
parentBranchSelect.innerHTML = '<option value="">Loading branches...</option>';
918-
codingAgentSelect.value = "claude";
919-
if (setupCommandsTextarea) {
920-
setupCommandsTextarea.value = "";
921-
}
922942
});
923943

924944
// MCP Server management functions

styles.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,12 @@
208208
.btn-icon {
209209
@apply bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded text-sm font-medium transition;
210210
}
211+
212+
.btn-primary.loading {
213+
@apply opacity-75 cursor-not-allowed;
214+
}
215+
216+
.btn-primary.loading .loading-spinner {
217+
@apply border-white border-t-transparent;
218+
}
211219
}

0 commit comments

Comments
 (0)