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;
|
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
|
// Git worktree helper functions
|
||||||
async function ensureFleetcodeExcluded(projectDir: string) {
|
async function ensureFleetcodeExcluded(projectDir: string) {
|
||||||
// Check if we've already initialized this project (persisted across app restarts)
|
// 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);
|
savePersistedSessions(sessions);
|
||||||
|
|
||||||
// Spawn PTY in worktree directory
|
// Spawn PTY in worktree directory
|
||||||
const shell = os.platform() === "darwin" ? "zsh" : "bash";
|
spawnSessionPty(sessionId, worktreePath, config, sessionUuid, true);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
event.reply("session-created", sessionId, persistedSession);
|
event.reply("session-created", sessionId, persistedSession);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -326,56 +320,7 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn new PTY in worktree directory
|
// Spawn new PTY in worktree directory
|
||||||
const shell = os.platform() === "darwin" ? "zsh" : "bash";
|
spawnSessionPty(sessionId, session.worktreePath, session.config, session.sessionUuid, false);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
event.reply("session-reopened", sessionId);
|
event.reply("session-reopened", sessionId);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,12 @@
|
||||||
"index.html",
|
"index.html",
|
||||||
"node_modules/**/*"
|
"node_modules/**/*"
|
||||||
],
|
],
|
||||||
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "github",
|
||||||
|
"releaseType": "release"
|
||||||
|
}
|
||||||
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
"category": "public.app-category.developer-tools",
|
"category": "public.app-category.developer-tools",
|
||||||
"target": [
|
"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) => {
|
ipcRenderer.on("session-created", (_event, sessionId: string, persistedSession: any) => {
|
||||||
const session = addSession(persistedSession, true);
|
const session = addSession(persistedSession, true);
|
||||||
activateSession(sessionId);
|
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
|
// Handle session reopened
|
||||||
|
|
@ -784,7 +807,7 @@ const skipPermissionsCheckbox = document.getElementById("skip-permissions") as H
|
||||||
const skipPermissionsGroup = skipPermissionsCheckbox?.parentElement?.parentElement;
|
const skipPermissionsGroup = skipPermissionsCheckbox?.parentElement?.parentElement;
|
||||||
const browseDirBtn = document.getElementById("browse-dir");
|
const browseDirBtn = document.getElementById("browse-dir");
|
||||||
const cancelBtn = document.getElementById("cancel-session");
|
const cancelBtn = document.getElementById("cancel-session");
|
||||||
const createBtn = document.getElementById("create-session");
|
const createBtn = document.getElementById("create-session") as HTMLButtonElement;
|
||||||
|
|
||||||
let selectedDirectory = "";
|
let selectedDirectory = "";
|
||||||
|
|
||||||
|
|
@ -904,21 +927,18 @@ createBtn?.addEventListener("click", () => {
|
||||||
setupCommands,
|
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
|
// Save settings for next time
|
||||||
ipcRenderer.send("save-settings", config);
|
ipcRenderer.send("save-settings", config);
|
||||||
|
|
||||||
// Create the session
|
// Create the session
|
||||||
ipcRenderer.send("create-session", config);
|
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
|
// MCP Server management functions
|
||||||
|
|
|
||||||
|
|
@ -208,4 +208,12 @@
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
@apply bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded text-sm font-medium transition;
|
@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