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)
This commit is contained in:
parent
0a1c6fd7b4
commit
cd4e0f64c8
203
main.ts
203
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,6 +47,12 @@
|
|||
"index.html",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"releaseType": "release"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
"target": [
|
||||
|
|
|
|||
42
renderer.ts
42
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 = '<option value="">Loading branches...</option>';
|
||||
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 = '<span class="loading-spinner"></span> 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 = '<option value="">Loading branches...</option>';
|
||||
codingAgentSelect.value = "claude";
|
||||
if (setupCommandsTextarea) {
|
||||
setupCommandsTextarea.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
// MCP Server management functions
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue