+
- ${name}
+ ${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")) {
+ if (!target.classList.contains("session-delete-btn") &&
+ !target.classList.contains("session-name-text") &&
+ !target.classList.contains("session-name-input")) {
handleSessionClick(sessionId);
}
});
@@ -176,6 +203,62 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
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;
@@ -371,12 +454,23 @@ 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");
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");
@@ -411,6 +505,18 @@ document.getElementById("new-session")?.addEventListener("click", async () => {
if (lastSettings.codingAgent) {
codingAgentSelect.value = lastSettings.codingAgent;
}
+
+ // Set last used skip permissions setting and visibility
+ if (lastSettings.skipPermissions !== undefined) {
+ skipPermissionsCheckbox.checked = lastSettings.skipPermissions;
+ }
+
+ // Show/hide skip permissions based on coding agent
+ if (lastSettings.codingAgent === "codex") {
+ skipPermissionsGroup?.classList.add("hidden");
+ } else {
+ skipPermissionsGroup?.classList.remove("hidden");
+ }
});
// Browse directory
@@ -456,6 +562,7 @@ createBtn?.addEventListener("click", () => {
projectDir: selectedDirectory,
parentBranch: parentBranchSelect.value,
codingAgent: codingAgentSelect.value,
+ skipPermissions: codingAgentSelect.value === "claude" ? skipPermissionsCheckbox.checked : false,
};
// Save settings for next time
@@ -474,12 +581,26 @@ createBtn?.addEventListener("click", () => {
// MCP Server management functions
async function loadMcpServers() {
+ const addMcpServerBtn = document.getElementById("add-mcp-server");
+
+ // Show loading spinner
+ if (addMcpServerBtn) {
+ addMcpServerBtn.innerHTML = '
';
+ addMcpServerBtn.classList.add("pointer-events-none");
+ }
+
try {
const servers = await ipcRenderer.invoke("list-mcp-servers");
mcpServers = servers;
renderMcpServers();
} catch (error) {
console.error("Failed to load MCP servers:", error);
+ } finally {
+ // Restore button
+ if (addMcpServerBtn) {
+ addMcpServerBtn.innerHTML = '+';
+ addMcpServerBtn.classList.remove("pointer-events-none");
+ }
}
}
@@ -492,14 +613,23 @@ function renderMcpServers() {
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();
@@ -517,6 +647,62 @@ function renderMcpServers() {
});
}
+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", 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;
@@ -530,7 +716,7 @@ const mcpAlwaysAllowInput = document.getElementById("mcp-always-allow") as HTMLI
const localFields = document.getElementById("local-fields");
const remoteFields = document.getElementById("remote-fields");
const cancelMcpBtn = document.getElementById("cancel-mcp");
-const addMcpBtn = document.getElementById("add-mcp");
+const addMcpBtn = document.getElementById("add-mcp") as HTMLButtonElement;
// Toggle fields based on server type
mcpTypeSelect?.addEventListener("change", () => {
@@ -632,6 +818,12 @@ addMcpBtn?.addEventListener("click", async () => {
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", name, config);
await loadMcpServers();
@@ -639,8 +831,43 @@ addMcpBtn?.addEventListener("click", async () => {
} 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", serverName);
+ mcpDetailsModal?.classList.add("hidden");
+ await loadMcpServers();
+ } catch (error) {
+ alert(`Failed to remove server: ${error}`);
+ }
+ }
+});
+
+// Listen for MCP server updates from main process
+ipcRenderer.on("mcp-servers-updated", (_event, servers: McpServer[]) => {
+ mcpServers = servers;
+ renderMcpServers();
+});
+
// Load MCP servers on startup
loadMcpServers();
diff --git a/styles.css b/styles.css
index 04fdcfd..936c297 100644
--- a/styles.css
+++ b/styles.css
@@ -36,6 +36,17 @@
@apply text-gray-400 hover:text-white hover:bg-gray-700 text-sm w-5 h-5 flex items-center justify-center rounded transition-all;
}
+ .loading-spinner {
+ @apply inline-block w-3 h-3 border-2 border-gray-400 border-t-transparent rounded-full;
+ animation: spin 0.6s linear infinite;
+ }
+
+ @keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
/* Session List Item */
.session-list-item {
@apply px-3 py-2 rounded cursor-pointer hover:bg-gray-700 text-gray-300 text-sm flex items-center justify-between;
@@ -54,6 +65,10 @@
@apply bg-green-500;
}
+ .session-indicator.disconnected {
+ @apply bg-red-500;
+ }
+
.session-delete-btn {
@apply text-gray-500 hover:text-red-500;
opacity: 0;
@@ -64,6 +79,19 @@
opacity: 1;
}
+ .session-name-text {
+ cursor: pointer;
+ }
+
+ .session-name-text:hover {
+ @apply text-gray-100;
+ }
+
+ .session-name-input {
+ @apply bg-gray-700 border border-gray-600 rounded px-1 py-0 text-gray-100 text-sm focus:outline-none focus:border-blue-500;
+ min-width: 80px;
+ }
+
/* Tabs */
.tabs-container {
@apply flex bg-gray-900 border-b border-gray-700 overflow-x-auto;
@@ -134,10 +162,18 @@
@apply w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500;
}
+ .form-checkbox {
+ @apply w-4 h-4 rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-blue-500 focus:ring-2 cursor-pointer;
+ }
+
.btn-secondary {
@apply bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded text-sm font-medium transition;
}
+ .btn-danger {
+ @apply bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded text-sm font-medium transition;
+ }
+
.btn-group {
@apply flex justify-end space-x-3 mt-6;
}