import {FitAddon} from "@xterm/addon-fit"; import {ipcRenderer} from "electron"; import {Terminal} from "xterm"; import {PersistedSession, SessionConfig} from "./types"; interface Session { id: string; terminal: Terminal | null; fitAddon: FitAddon | null; element: HTMLDivElement | null; name: string; config: SessionConfig; worktreePath: string; hasActivePty: boolean; hasUnreadActivity: boolean; } interface McpServer { name: string; connected?: boolean; command?: string; args?: string[]; env?: Record; url?: string; type?: "stdio" | "sse"; } interface TerminalSettings { fontFamily: string; fontSize: number; theme: string; // Theme preset name cursorBlink: boolean; worktreeDir: string; } interface ThemeColors { background: string; foreground: string; cursor?: string; cursorAccent?: string; selection?: string; black?: string; red?: string; green?: string; yellow?: string; blue?: string; magenta?: string; cyan?: string; white?: string; brightBlack?: string; brightRed?: string; brightGreen?: string; brightYellow?: string; brightBlue?: string; brightMagenta?: string; brightCyan?: string; brightWhite?: string; } // Theme presets const THEME_PRESETS: Record = { "macos-light": { background: "#ffffff", foreground: "#000000", cursor: "#000000", selection: "#b4d5fe", black: "#000000", red: "#c23621", green: "#25bc24", yellow: "#adad27", blue: "#492ee1", magenta: "#d338d3", cyan: "#33bbc8", white: "#cbcccd", brightBlack: "#818383", brightRed: "#fc391f", brightGreen: "#31e722", brightYellow: "#eaec23", brightBlue: "#5833ff", brightMagenta: "#f935f8", brightCyan: "#14f0f0", brightWhite: "#e9ebeb", }, "macos-dark": { background: "#000000", foreground: "#ffffff", cursor: "#ffffff", selection: "#4d4d4d", black: "#000000", red: "#c23621", green: "#25bc24", yellow: "#adad27", blue: "#492ee1", magenta: "#d338d3", cyan: "#33bbc8", white: "#cbcccd", brightBlack: "#818383", brightRed: "#fc391f", brightGreen: "#31e722", brightYellow: "#eaec23", brightBlue: "#5833ff", brightMagenta: "#f935f8", brightCyan: "#14f0f0", brightWhite: "#e9ebeb", }, "solarized-dark": { background: "#002b36", foreground: "#839496", cursor: "#839496", selection: "#073642", black: "#073642", red: "#dc322f", green: "#859900", yellow: "#b58900", blue: "#268bd2", magenta: "#d33682", cyan: "#2aa198", white: "#eee8d5", brightBlack: "#002b36", brightRed: "#cb4b16", brightGreen: "#586e75", brightYellow: "#657b83", brightBlue: "#839496", brightMagenta: "#6c71c4", brightCyan: "#93a1a1", brightWhite: "#fdf6e3", }, "dracula": { background: "#282a36", foreground: "#f8f8f2", cursor: "#f8f8f2", selection: "#44475a", black: "#21222c", red: "#ff5555", green: "#50fa7b", yellow: "#f1fa8c", blue: "#bd93f9", magenta: "#ff79c6", cyan: "#8be9fd", white: "#f8f8f2", brightBlack: "#6272a4", brightRed: "#ff6e6e", brightGreen: "#69ff94", brightYellow: "#ffffa5", brightBlue: "#d6acff", brightMagenta: "#ff92df", brightCyan: "#a4ffff", brightWhite: "#ffffff", }, "one-dark": { background: "#282c34", foreground: "#abb2bf", cursor: "#528bff", selection: "#3e4451", black: "#282c34", red: "#e06c75", green: "#98c379", yellow: "#e5c07b", blue: "#61afef", magenta: "#c678dd", cyan: "#56b6c2", white: "#abb2bf", brightBlack: "#5c6370", brightRed: "#e06c75", brightGreen: "#98c379", brightYellow: "#e5c07b", brightBlue: "#61afef", brightMagenta: "#c678dd", brightCyan: "#56b6c2", brightWhite: "#ffffff", }, "github-dark": { background: "#0d1117", foreground: "#c9d1d9", cursor: "#58a6ff", selection: "#163c61", black: "#484f58", red: "#ff7b72", green: "#3fb950", yellow: "#d29922", blue: "#58a6ff", magenta: "#bc8cff", cyan: "#39c5cf", white: "#b1bac4", brightBlack: "#6e7681", brightRed: "#ffa198", brightGreen: "#56d364", brightYellow: "#e3b341", brightBlue: "#79c0ff", brightMagenta: "#d2a8ff", brightCyan: "#56d4dd", brightWhite: "#f0f6fc", }, }; // Detect system theme function getSystemTheme(): "light" | "dark" { return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } // Default settings - macOS Terminal matching system theme const DEFAULT_SETTINGS: TerminalSettings = { fontFamily: "Menlo, Monaco, 'Courier New', monospace", fontSize: 11, theme: getSystemTheme() === "dark" ? "macos-dark" : "macos-light", cursorBlink: false, worktreeDir: require("path").join(require("os").homedir(), "worktrees"), }; const sessions = new Map(); let activeSessionId: string | null = null; let mcpServers: McpServer[] = []; let mcpPollerActive = false; let terminalSettings: TerminalSettings = { ...DEFAULT_SETTINGS }; function createTerminalUI(sessionId: string) { const themeColors = THEME_PRESETS[terminalSettings.theme] || THEME_PRESETS["macos-dark"]; const term = new Terminal({ cursorBlink: terminalSettings.cursorBlink, fontSize: terminalSettings.fontSize, fontFamily: terminalSettings.fontFamily, theme: themeColors, }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); const sessionElement = document.createElement("div"); sessionElement.className = "session-wrapper"; sessionElement.id = `session-${sessionId}`; const container = document.getElementById("session-container"); if (container) { container.appendChild(sessionElement); } term.open(sessionElement); term.onData((data) => { ipcRenderer.send("session-input", sessionId, data); }); // Listen for bell character to mark unread activity term.onBell(() => { if (activeSessionId !== sessionId) { markSessionAsUnread(sessionId); } }); const resizeHandler = () => { if (activeSessionId === sessionId) { const proposedDimensions = fitAddon.proposeDimensions(); if (proposedDimensions) { fitAddon.fit(); ipcRenderer.send("session-resize", sessionId, proposedDimensions.cols, proposedDimensions.rows); } } }; window.addEventListener("resize", resizeHandler); return { terminal: term, fitAddon, element: sessionElement }; } function addSession(persistedSession: PersistedSession, hasActivePty: boolean) { const session: Session = { id: persistedSession.id, terminal: null, fitAddon: null, element: null, name: persistedSession.name, config: persistedSession.config, worktreePath: persistedSession.worktreePath, hasActivePty, hasUnreadActivity: false, }; sessions.set(persistedSession.id, session); // Add to sidebar addToSidebar(persistedSession.id, persistedSession.name, hasActivePty); // Only add tab if terminal is active if (hasActivePty) { addTab(persistedSession.id, persistedSession.name); } return session; } function activateSession(sessionId: string) { const session = sessions.get(sessionId); if (!session) return; // If terminal UI doesn't exist yet, create it if (!session.terminal) { const ui = createTerminalUI(sessionId); session.terminal = ui.terminal; session.fitAddon = ui.fitAddon; session.element = ui.element; } session.hasActivePty = true; updateSessionState(sessionId, true); // Add tab if it doesn't exist if (!document.getElementById(`tab-${sessionId}`)) { addTab(sessionId, session.name); } // Switch to this session switchToSession(sessionId); } function updateSessionState(sessionId: string, isActive: boolean) { const sidebarItem = document.getElementById(`sidebar-${sessionId}`); const indicator = sidebarItem?.querySelector(".session-indicator"); if (indicator) { if (isActive) { indicator.classList.add("active"); } else { indicator.classList.remove("active"); } } } function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) { const list = document.getElementById("session-list"); if (!list) return; const item = document.createElement("div"); item.id = `sidebar-${sessionId}`; item.className = "session-list-item"; item.innerHTML = `
${name}
`; // Click on session name to edit const nameText = item.querySelector(".session-name-text"); const nameInput = item.querySelector(".session-name-input") as HTMLInputElement; nameText?.addEventListener("click", (e) => { e.stopPropagation(); startEditingSessionName(sessionId); }); // Handle input blur and enter key nameInput?.addEventListener("blur", () => { finishEditingSessionName(sessionId); }); nameInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") { finishEditingSessionName(sessionId); } else if (e.key === "Escape") { cancelEditingSessionName(sessionId); } }); item.addEventListener("click", (e) => { const target = e.target as HTMLElement; if (!target.classList.contains("session-delete-btn") && !target.classList.contains("session-name-text") && !target.classList.contains("session-name-input")) { handleSessionClick(sessionId); } }); const deleteBtn = item.querySelector(".session-delete-btn"); deleteBtn?.addEventListener("click", (e) => { e.stopPropagation(); deleteSession(sessionId); }); list.appendChild(item); } function startEditingSessionName(sessionId: string) { const sidebarItem = document.getElementById(`sidebar-${sessionId}`); const nameText = sidebarItem?.querySelector(".session-name-text"); const nameInput = sidebarItem?.querySelector(".session-name-input") as HTMLInputElement; if (nameText && nameInput) { nameText.classList.add("hidden"); nameInput.classList.remove("hidden"); nameInput.focus(); nameInput.select(); } } function finishEditingSessionName(sessionId: string) { const sidebarItem = document.getElementById(`sidebar-${sessionId}`); const nameText = sidebarItem?.querySelector(".session-name-text"); const nameInput = sidebarItem?.querySelector(".session-name-input") as HTMLInputElement; const session = sessions.get(sessionId); if (nameText && nameInput && session) { const newName = nameInput.value.trim(); if (newName && newName !== session.name) { // Update session name session.name = newName; nameText.textContent = newName; // Update tab name if exists const tab = document.getElementById(`tab-${sessionId}`); const tabName = tab?.querySelector(".tab-name"); if (tabName) { tabName.textContent = newName; } // Save to backend ipcRenderer.send("rename-session", sessionId, newName); } nameInput.classList.add("hidden"); nameText.classList.remove("hidden"); } } function cancelEditingSessionName(sessionId: string) { const sidebarItem = document.getElementById(`sidebar-${sessionId}`); const nameText = sidebarItem?.querySelector(".session-name-text"); const nameInput = sidebarItem?.querySelector(".session-name-input") as HTMLInputElement; const session = sessions.get(sessionId); if (nameText && nameInput && session) { // Reset to original name nameInput.value = session.name; nameInput.classList.add("hidden"); nameText.classList.remove("hidden"); } } function handleSessionClick(sessionId: string) { const session = sessions.get(sessionId); if (!session) return; if (session.hasActivePty) { // Just switch to it switchToSession(sessionId); } else { // Reopen the session ipcRenderer.send("reopen-session", sessionId); } } function addTab(sessionId: string, name: string) { const tabsContainer = document.getElementById("tabs"); if (!tabsContainer) return; const tab = document.createElement("div"); tab.id = `tab-${sessionId}`; tab.className = "tab"; tab.innerHTML = ` ${name} `; tab.addEventListener("click", (e) => { if (!(e.target as HTMLElement).classList.contains("tab-close-btn")) { switchToSession(sessionId); } }); const closeBtn = tab.querySelector(".tab-close-btn"); closeBtn?.addEventListener("click", (e) => { e.stopPropagation(); closeSession(sessionId); }); tabsContainer.appendChild(tab); } function markSessionAsUnread(sessionId: string) { const session = sessions.get(sessionId); if (!session) return; session.hasUnreadActivity = true; // Add unread indicator to tab const tab = document.getElementById(`tab-${sessionId}`); if (tab) { tab.classList.add("unread"); } } function clearUnreadStatus(sessionId: string) { const session = sessions.get(sessionId); if (!session) return; session.hasUnreadActivity = false; // Remove unread indicator from tab const tab = document.getElementById(`tab-${sessionId}`); if (tab) { tab.classList.remove("unread"); } } function switchToSession(sessionId: string) { // Hide all sessions sessions.forEach((session, id) => { if (session.element) { session.element.classList.remove("active"); } document.getElementById(`tab-${id}`)?.classList.remove("active"); document.getElementById(`sidebar-${id}`)?.classList.remove("active"); }); // Show active session const session = sessions.get(sessionId); if (session && session.element && session.terminal && session.fitAddon) { session.element.classList.add("active"); document.getElementById(`tab-${sessionId}`)?.classList.add("active"); document.getElementById(`sidebar-${sessionId}`)?.classList.add("active"); activeSessionId = sessionId; // Show MCP section when a session is active const mcpSection = document.getElementById("mcp-section"); if (mcpSection) { mcpSection.style.display = "block"; } // Clear MCP servers from previous session and re-render mcpServers = []; renderMcpServers(); // Load MCP servers for this session loadMcpServers(); // Clear unread status when switching to this session clearUnreadStatus(sessionId); // Clear any pending idle timer for this session (Bug 1 fix) const existingTimer = sessionIdleTimers.get(sessionId); if (existingTimer) { clearTimeout(existingTimer); sessionIdleTimers.delete(sessionId); } // Focus and resize session.terminal.focus(); // Dispatch resize event to trigger terminal resize window.dispatchEvent(new Event("resize")); } } function closeSession(sessionId: string) { const session = sessions.get(sessionId); if (!session) return; // Remove terminal UI if (session.element) { session.element.remove(); } if (session.terminal) { session.terminal.dispose(); } // Remove tab document.getElementById(`tab-${sessionId}`)?.remove(); // Update session state session.terminal = null; session.fitAddon = null; session.element = null; session.hasActivePty = false; // Update UI indicator updateSessionState(sessionId, false); // Clean up idle timer (Bug 2 fix) const existingTimer = sessionIdleTimers.get(sessionId); if (existingTimer) { clearTimeout(existingTimer); sessionIdleTimers.delete(sessionId); } // Close PTY in main process ipcRenderer.send("close-session", sessionId); // Switch to another active session if (activeSessionId === sessionId) { const activeSessions = Array.from(sessions.values()).filter(s => s.hasActivePty); if (activeSessions.length > 0) { switchToSession(activeSessions[0].id); } else { activeSessionId = null; // Hide MCP section when no sessions are active const mcpSection = document.getElementById("mcp-section"); if (mcpSection) { mcpSection.style.display = "none"; } } } } function deleteSession(sessionId: string) { const session = sessions.get(sessionId); if (!session) return; // Confirm deletion if (!confirm(`Delete ${session.name}? This will remove the git worktree.`)) { return; } // Remove from UI if (session.element) { session.element.remove(); } if (session.terminal) { session.terminal.dispose(); } document.getElementById(`tab-${sessionId}`)?.remove(); document.getElementById(`sidebar-${sessionId}`)?.remove(); // Remove from sessions map sessions.delete(sessionId); // Clean up idle timer (Bug 2 fix) const existingTimer = sessionIdleTimers.get(sessionId); if (existingTimer) { clearTimeout(existingTimer); sessionIdleTimers.delete(sessionId); } // Delete in main process (handles worktree removal) ipcRenderer.send("delete-session", sessionId); // Switch to another session if (activeSessionId === sessionId) { const remainingSessions = Array.from(sessions.values()).filter(s => s.hasActivePty); if (remainingSessions.length > 0) { switchToSession(remainingSessions[0].id); } else { activeSessionId = null; // Hide MCP section when no sessions are active const mcpSection = document.getElementById("mcp-section"); if (mcpSection) { mcpSection.style.display = "none"; } } } } // Track idle timers per session to detect when output stops (Claude is done) const sessionIdleTimers = new Map(); const IDLE_DELAY_MS = 500; // 0.5 seconds of no output = Claude is done // Handle session output ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => { const session = sessions.get(sessionId); if (session && session.terminal) { // Filter out [3J (clear scrollback) to prevent viewport resets during interactive menus // Keep [2J (clear screen) which is needed for the menu redraw const filteredData = data.replace(/\x1b\[3J/g, ''); session.terminal.write(filteredData); // Only mark as unread if this is not the active session if (activeSessionId !== sessionId && session.hasActivePty && !session.hasUnreadActivity) { // Only track substantive output (ignore cursor movements, keepalives, etc) // Look for actual text content or common escape sequences that indicate real output const hasSubstantiveOutput = /[a-zA-Z0-9]/.test(filteredData) || filteredData.includes('\n') || filteredData.includes('\r'); if (hasSubstantiveOutput) { // Clear any existing idle timer const existingTimer = sessionIdleTimers.get(sessionId); if (existingTimer) { clearTimeout(existingTimer); } // Set a new timer - if no output for IDLE_DELAY_MS, mark as unread const timer = setTimeout(() => { markSessionAsUnread(sessionId); sessionIdleTimers.delete(sessionId); }, IDLE_DELAY_MS); sessionIdleTimers.set(sessionId, timer); } } } }); // Handle session created 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 branchNameInput = document.getElementById("branch-name") as HTMLInputElement; 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 (branchNameInput) { branchNameInput.value = ""; } if (setupCommandsTextarea) { setupCommandsTextarea.value = ""; } }); // Handle session reopened ipcRenderer.on("session-reopened", (_event, sessionId: string) => { activateSession(sessionId); }); // Handle session deleted ipcRenderer.on("session-deleted", (_event, sessionId: string) => { const session = sessions.get(sessionId); if (session) { if (session.element) session.element.remove(); if (session.terminal) session.terminal.dispose(); document.getElementById(`tab-${sessionId}`)?.remove(); document.getElementById(`sidebar-${sessionId}`)?.remove(); sessions.delete(sessionId); if (activeSessionId === sessionId) { const remainingSessions = Array.from(sessions.values()).filter(s => s.hasActivePty); if (remainingSessions.length > 0) { switchToSession(remainingSessions[0].id); } else { activeSessionId = null; } } } }); // Load persisted sessions on startup ipcRenderer.on("load-persisted-sessions", (_event, persistedSessions: PersistedSession[]) => { persistedSessions.forEach(ps => { addSession(ps, false); }); }); // Modal handling const modal = document.getElementById("config-modal"); const projectDirInput = document.getElementById("project-dir") as HTMLInputElement; const parentBranchSelect = document.getElementById("parent-branch") as HTMLSelectElement; const codingAgentSelect = document.getElementById("coding-agent") as HTMLSelectElement; const skipPermissionsCheckbox = document.getElementById("skip-permissions") as HTMLInputElement; const skipPermissionsGroup = skipPermissionsCheckbox?.parentElement?.parentElement; const browseDirBtn = document.getElementById("browse-dir"); const cancelBtn = document.getElementById("cancel-session"); const createBtn = document.getElementById("create-session") as HTMLButtonElement; let selectedDirectory = ""; // Toggle skip permissions checkbox visibility based on coding agent codingAgentSelect?.addEventListener("change", () => { if (codingAgentSelect.value === "claude") { skipPermissionsGroup?.classList.remove("hidden"); } else { skipPermissionsGroup?.classList.add("hidden"); } }); // New session button - opens modal document.getElementById("new-session")?.addEventListener("click", async () => { modal?.classList.remove("hidden"); // Load last used settings const lastSettings = await ipcRenderer.invoke("get-last-settings"); if (lastSettings.projectDir) { selectedDirectory = lastSettings.projectDir; projectDirInput.value = lastSettings.projectDir; // Load git branches for the last directory const branches = await ipcRenderer.invoke("get-branches", lastSettings.projectDir); parentBranchSelect.innerHTML = ""; if (branches.length === 0) { parentBranchSelect.innerHTML = ''; } else { branches.forEach((branch: string) => { const option = document.createElement("option"); option.value = branch; option.textContent = branch; if (branch === lastSettings.parentBranch) { option.selected = true; } parentBranchSelect.appendChild(option); }); } } // Set last used coding agent if (lastSettings.codingAgent) { codingAgentSelect.value = lastSettings.codingAgent; } // Set last used skip permissions setting and visibility if (lastSettings.skipPermissions !== undefined) { skipPermissionsCheckbox.checked = lastSettings.skipPermissions; } // Set last used setup commands const setupCommandsTextarea = document.getElementById("setup-commands") as HTMLTextAreaElement; if (lastSettings.setupCommands && setupCommandsTextarea) { setupCommandsTextarea.value = lastSettings.setupCommands.join("\n"); } // Show/hide skip permissions based on coding agent if (lastSettings.codingAgent === "codex") { skipPermissionsGroup?.classList.add("hidden"); } else { skipPermissionsGroup?.classList.remove("hidden"); } }); // Browse directory browseDirBtn?.addEventListener("click", async () => { const dir = await ipcRenderer.invoke("select-directory"); if (dir) { selectedDirectory = dir; projectDirInput.value = dir; // Load git branches const branches = await ipcRenderer.invoke("get-branches", dir); parentBranchSelect.innerHTML = ""; if (branches.length === 0) { parentBranchSelect.innerHTML = ''; } else { branches.forEach((branch: string) => { const option = document.createElement("option"); option.value = branch; option.textContent = branch; parentBranchSelect.appendChild(option); }); } } }); // Cancel button cancelBtn?.addEventListener("click", () => { modal?.classList.add("hidden"); projectDirInput.value = ""; selectedDirectory = ""; parentBranchSelect.innerHTML = ''; }); // Create session button createBtn?.addEventListener("click", () => { if (!selectedDirectory) { alert("Please select a project directory"); return; } const setupCommandsTextarea = document.getElementById("setup-commands") as HTMLTextAreaElement; const setupCommandsText = setupCommandsTextarea?.value.trim(); const setupCommands = setupCommandsText ? setupCommandsText.split("\n").filter(cmd => cmd.trim()) : undefined; const branchNameInput = document.getElementById("branch-name") as HTMLInputElement; const branchName = branchNameInput?.value.trim() || undefined; const config: SessionConfig = { projectDir: selectedDirectory, parentBranch: parentBranchSelect.value, branchName, codingAgent: codingAgentSelect.value, skipPermissions: codingAgentSelect.value === "claude" ? skipPermissionsCheckbox.checked : false, 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); }); // MCP Server management functions async function loadMcpServers() { // Only load MCP servers if there's an active session if (!activeSessionId) { return; } try { await ipcRenderer.invoke("list-mcp-servers", activeSessionId); // Results will come via mcp-servers-updated event } catch (error) { console.error("Failed to load MCP servers:", error); } } function renderMcpServers() { const list = document.getElementById("mcp-server-list"); if (!list) return; list.innerHTML = ""; mcpServers.forEach(server => { const item = document.createElement("div"); item.className = "session-list-item"; const indicatorClass = server.connected ? "active" : "disconnected"; item.innerHTML = `
${server.name}
`; // Click to show details item.addEventListener("click", async (e) => { const target = e.target as HTMLElement; if (!target.classList.contains("mcp-remove-btn")) { await showMcpServerDetails(server.name); } }); const removeBtn = item.querySelector(".mcp-remove-btn"); removeBtn?.addEventListener("click", async (e) => { e.stopPropagation(); if (confirm(`Remove MCP server "${server.name}"?`)) { try { await ipcRenderer.invoke("remove-mcp-server", activeSessionId, server.name); await loadMcpServers(); } catch (error) { alert(`Failed to remove server: ${error}`); } } }); list.appendChild(item); }); } async function showMcpServerDetails(name: string) { const detailsModal = document.getElementById("mcp-details-modal"); const detailsTitle = document.getElementById("mcp-details-title"); const detailsContent = document.getElementById("mcp-details-content"); // Show modal immediately with loading state if (detailsTitle) { detailsTitle.textContent = name; } if (detailsContent) { detailsContent.innerHTML = '
'; } detailsModal?.classList.remove("hidden"); try { const details = await ipcRenderer.invoke("get-mcp-server-details", activeSessionId, name); if (detailsContent) { let html = ""; if (details.scope) { html += `
Scope: ${details.scope}
`; } if (details.status) { html += `
Status: ${details.status}
`; } if (details.type) { html += `
Type: ${details.type}
`; } if (details.url) { html += `
URL: ${details.url}
`; } if (details.command) { html += `
Command: ${details.command}
`; } if (details.args) { html += `
Args: ${details.args}
`; } detailsContent.innerHTML = html; } // Store current server name for remove button const removeMcpDetailsBtn = document.getElementById("remove-mcp-details") as HTMLButtonElement; if (removeMcpDetailsBtn) { removeMcpDetailsBtn.dataset.serverName = name; } } catch (error) { console.error("Failed to get server details:", error); if (detailsContent) { detailsContent.innerHTML = `
Failed to load server details
`; } } } // MCP Modal handling const mcpModal = document.getElementById("mcp-modal"); const mcpNameInput = document.getElementById("mcp-name") as HTMLInputElement; const mcpTypeSelect = document.getElementById("mcp-type") as HTMLSelectElement; const mcpCommandInput = document.getElementById("mcp-command") as HTMLInputElement; const mcpArgsInput = document.getElementById("mcp-args") as HTMLInputElement; const mcpEnvInput = document.getElementById("mcp-env") as HTMLTextAreaElement; const mcpUrlInput = document.getElementById("mcp-url") as HTMLInputElement; const mcpHeadersInput = document.getElementById("mcp-headers") as HTMLTextAreaElement; const mcpAlwaysAllowInput = document.getElementById("mcp-always-allow") as HTMLInputElement; const localFields = document.getElementById("local-fields"); const remoteFields = document.getElementById("remote-fields"); const cancelMcpBtn = document.getElementById("cancel-mcp"); const addMcpBtn = document.getElementById("add-mcp") as HTMLButtonElement; // Toggle fields based on server type mcpTypeSelect?.addEventListener("change", () => { if (mcpTypeSelect.value === "local") { localFields!.style.display = "block"; remoteFields!.style.display = "none"; } else { localFields!.style.display = "none"; remoteFields!.style.display = "block"; } }); // Add MCP server button - opens modal document.getElementById("add-mcp-server")?.addEventListener("click", () => { mcpModal?.classList.remove("hidden"); mcpNameInput.value = ""; mcpTypeSelect.value = "local"; mcpCommandInput.value = ""; mcpArgsInput.value = ""; mcpEnvInput.value = ""; mcpUrlInput.value = ""; mcpHeadersInput.value = ""; mcpAlwaysAllowInput.value = ""; localFields!.style.display = "block"; remoteFields!.style.display = "none"; }); // Cancel MCP button cancelMcpBtn?.addEventListener("click", () => { mcpModal?.classList.add("hidden"); }); // Add MCP button addMcpBtn?.addEventListener("click", async () => { const name = mcpNameInput.value.trim(); const serverType = mcpTypeSelect.value; if (!name) { alert("Please enter a server name"); return; } const config: any = {}; if (serverType === "local") { config.type = "stdio"; const command = mcpCommandInput.value.trim(); const argsInput = mcpArgsInput.value.trim(); if (!command) { alert("Please enter a command"); return; } config.command = command; if (argsInput) { config.args = argsInput.split(" ").filter(a => a.trim()); } // Parse environment variables if provided const envInput = mcpEnvInput.value.trim(); if (envInput) { try { config.env = JSON.parse(envInput); } catch (error) { alert("Invalid JSON for environment variables"); return; } } } else { // Remote server config.type = "sse"; const url = mcpUrlInput.value.trim(); if (!url) { alert("Please enter a server URL"); return; } config.url = url; // Parse headers if provided const headersInput = mcpHeadersInput.value.trim(); if (headersInput) { try { config.headers = JSON.parse(headersInput); } catch (error) { alert("Invalid JSON for headers"); return; } } } // Parse always allow tools const alwaysAllowInput = mcpAlwaysAllowInput.value.trim(); if (alwaysAllowInput) { config.alwaysAllow = alwaysAllowInput.split(",").map(t => t.trim()).filter(t => t); } // Show loading state const originalText = addMcpBtn.textContent; addMcpBtn.textContent = "Adding..."; addMcpBtn.disabled = true; addMcpBtn.classList.add("opacity-50", "cursor-not-allowed"); try { await ipcRenderer.invoke("add-mcp-server", activeSessionId, name, config); await loadMcpServers(); mcpModal?.classList.add("hidden"); } catch (error) { console.error("Error adding server:", error); alert(`Failed to add server: ${error}`); } finally { // Reset button state addMcpBtn.textContent = originalText; addMcpBtn.disabled = false; addMcpBtn.classList.remove("opacity-50", "cursor-not-allowed"); } }); // MCP Details Modal handling const closeMcpDetailsBtn = document.getElementById("close-mcp-details"); const removeMcpDetailsBtn = document.getElementById("remove-mcp-details") as HTMLButtonElement; const mcpDetailsModal = document.getElementById("mcp-details-modal"); closeMcpDetailsBtn?.addEventListener("click", () => { mcpDetailsModal?.classList.add("hidden"); }); removeMcpDetailsBtn?.addEventListener("click", async () => { const serverName = removeMcpDetailsBtn.dataset.serverName; if (!serverName) return; if (confirm(`Remove MCP server "${serverName}"?`)) { try { await ipcRenderer.invoke("remove-mcp-server", activeSessionId, serverName); mcpDetailsModal?.classList.add("hidden"); await loadMcpServers(); } catch (error) { alert(`Failed to remove server: ${error}`); } } }); // Listen for MCP polling started event ipcRenderer.on("mcp-polling-started", (_event, sessionId: string) => { console.log(`[MCP UI] Polling started for session ${sessionId}, active: ${activeSessionId}`); if (sessionId === activeSessionId) { const addMcpServerBtn = document.getElementById("add-mcp-server"); if (addMcpServerBtn) { console.log("[MCP UI] Setting add button to loading state"); addMcpServerBtn.innerHTML = ''; addMcpServerBtn.classList.add("pointer-events-none"); } } }); // Listen for MCP server updates from main process ipcRenderer.on("mcp-servers-updated", (_event, sessionId: string, servers: McpServer[]) => { console.log(`[MCP UI] Received update for session ${sessionId}, active: ${activeSessionId}`); // Only update if this is for the active session if (sessionId === activeSessionId) { mcpServers = servers; renderMcpServers(); // Restore add button console.log("[MCP UI] Restoring add button from loading state"); const addMcpServerBtn = document.getElementById("add-mcp-server"); if (addMcpServerBtn) { addMcpServerBtn.innerHTML = '+'; addMcpServerBtn.classList.remove("pointer-events-none"); } } }); // Settings Modal handling const settingsModal = document.getElementById("settings-modal"); const openSettingsBtn = document.getElementById("open-settings"); const cancelSettingsBtn = document.getElementById("cancel-settings"); const resetSettingsBtn = document.getElementById("reset-settings"); const saveSettingsBtn = document.getElementById("save-settings"); const settingsTheme = document.getElementById("settings-theme") as HTMLSelectElement; const settingsFontFamily = document.getElementById("settings-font-family") as HTMLSelectElement; const settingsFontSize = document.getElementById("settings-font-size") as HTMLInputElement; const settingsCursorBlink = document.getElementById("settings-cursor-blink") as HTMLInputElement; const settingsWorktreeDir = document.getElementById("settings-worktree-dir") as HTMLInputElement; const browseWorktreeDirBtn = document.getElementById("browse-worktree-dir"); // Load saved settings on startup async function loadSettings() { const savedSettings = await ipcRenderer.invoke("get-terminal-settings"); if (savedSettings) { terminalSettings = { ...DEFAULT_SETTINGS, ...savedSettings }; } } // Populate settings form function populateSettingsForm() { // Set theme settingsTheme.value = terminalSettings.theme; // Set font family - match against dropdown options const fontOptions = Array.from(settingsFontFamily.options); const matchingOption = fontOptions.find(opt => opt.value === terminalSettings.fontFamily); if (matchingOption) { settingsFontFamily.value = matchingOption.value; } else { // Default to first option (Menlo) if no match settingsFontFamily.selectedIndex = 0; } settingsFontSize.value = terminalSettings.fontSize.toString(); settingsCursorBlink.checked = terminalSettings.cursorBlink; settingsWorktreeDir.value = terminalSettings.worktreeDir; } // Apply settings to all existing terminals function applySettingsToAllTerminals() { const themeColors = THEME_PRESETS[terminalSettings.theme] || THEME_PRESETS["macos-dark"]; sessions.forEach((session) => { if (session.terminal) { session.terminal.options.fontFamily = terminalSettings.fontFamily; session.terminal.options.fontSize = terminalSettings.fontSize; session.terminal.options.cursorBlink = terminalSettings.cursorBlink; session.terminal.options.theme = themeColors; // Refresh terminal to apply changes if (session.fitAddon) { session.fitAddon.fit(); } } }); } // Open settings modal openSettingsBtn?.addEventListener("click", () => { populateSettingsForm(); settingsModal?.classList.remove("hidden"); }); // Cancel settings cancelSettingsBtn?.addEventListener("click", () => { settingsModal?.classList.add("hidden"); }); // Reset settings to default resetSettingsBtn?.addEventListener("click", () => { terminalSettings = { ...DEFAULT_SETTINGS }; populateSettingsForm(); }); // Browse worktree directory browseWorktreeDirBtn?.addEventListener("click", async () => { const dir = await ipcRenderer.invoke("select-directory"); if (dir) { settingsWorktreeDir.value = dir; } }); // Save settings saveSettingsBtn?.addEventListener("click", async () => { // Read values from form terminalSettings.theme = settingsTheme.value; terminalSettings.fontFamily = settingsFontFamily.value || DEFAULT_SETTINGS.fontFamily; terminalSettings.fontSize = parseInt(settingsFontSize.value) || DEFAULT_SETTINGS.fontSize; terminalSettings.cursorBlink = settingsCursorBlink.checked; terminalSettings.worktreeDir = settingsWorktreeDir.value || DEFAULT_SETTINGS.worktreeDir; // Save to electron-store await ipcRenderer.invoke("save-terminal-settings", terminalSettings); // Apply to all existing terminals applySettingsToAllTerminals(); // Close modal settingsModal?.classList.add("hidden"); }); // Load settings on startup loadSettings();