mcp servers and a bunch of stuff
This commit is contained in:
parent
1c614c9d28
commit
d01e715e68
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -139,3 +139,5 @@ dist
|
|||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vite/
|
||||
|
||||
.fleetcode/
|
||||
23
index.html
23
index.html
|
|
@ -78,6 +78,13 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" id="skip-permissions" class="form-checkbox" checked />
|
||||
<span class="text-sm text-gray-300">Skip permissions (use --dangerously-skip-permissions)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="cancel-session" class="btn-secondary">Cancel</button>
|
||||
<button id="create-session" class="btn-primary">Create Session</button>
|
||||
|
|
@ -85,6 +92,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP Server Details Modal -->
|
||||
<div id="mcp-details-modal" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
<h2 class="modal-title" id="mcp-details-title">Server Details</h2>
|
||||
|
||||
<div id="mcp-details-content" class="space-y-3 text-sm text-gray-300">
|
||||
<!-- Details will be populated here -->
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="close-mcp-details" class="btn-secondary">Close</button>
|
||||
<button id="remove-mcp-details" class="btn-danger">Remove Server</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP Server Modal -->
|
||||
<div id="mcp-modal" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
|
|
|
|||
83
main.ts
83
main.ts
|
|
@ -14,6 +14,7 @@ interface SessionConfig {
|
|||
projectDir: string;
|
||||
parentBranch: string;
|
||||
codingAgent: string;
|
||||
skipPermissions: boolean;
|
||||
}
|
||||
|
||||
interface PersistedSession {
|
||||
|
|
@ -120,6 +121,7 @@ ipcMain.handle("get-last-settings", () => {
|
|||
projectDir: "",
|
||||
parentBranch: "",
|
||||
codingAgent: "claude",
|
||||
skipPermissions: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -181,7 +183,8 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
|||
|
||||
// Auto-run the selected coding agent
|
||||
if (config.codingAgent === "claude") {
|
||||
ptyProcess.write("claude\r");
|
||||
const claudeCmd = config.skipPermissions ? "claude --dangerously-skip-permissions\r" : "claude\r";
|
||||
ptyProcess.write(claudeCmd);
|
||||
} else if (config.codingAgent === "codex") {
|
||||
ptyProcess.write("codex\r");
|
||||
}
|
||||
|
|
@ -199,7 +202,8 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
|||
|
||||
// Auto-run the selected coding agent
|
||||
if (config.codingAgent === "claude") {
|
||||
ptyProcess.write("claude\r");
|
||||
const claudeCmd = config.skipPermissions ? "claude --dangerously-skip-permissions\r" : "claude\r";
|
||||
ptyProcess.write(claudeCmd);
|
||||
} else if (config.codingAgent === "codex") {
|
||||
ptyProcess.write("codex\r");
|
||||
}
|
||||
|
|
@ -277,7 +281,8 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
|
|||
terminalReady = true;
|
||||
|
||||
if (session.config.codingAgent === "claude") {
|
||||
ptyProcess.write("claude\r");
|
||||
const claudeCmd = session.config.skipPermissions ? "claude --dangerously-skip-permissions\r" : "claude\r";
|
||||
ptyProcess.write(claudeCmd);
|
||||
} else if (session.config.codingAgent === "codex") {
|
||||
ptyProcess.write("codex\r");
|
||||
}
|
||||
|
|
@ -332,6 +337,17 @@ ipcMain.handle("get-all-sessions", () => {
|
|||
return getPersistedSessions();
|
||||
});
|
||||
|
||||
// Rename session
|
||||
ipcMain.on("rename-session", (_event, sessionId: string, newName: string) => {
|
||||
const sessions = getPersistedSessions();
|
||||
const session = sessions.find(s => s.id === sessionId);
|
||||
|
||||
if (session) {
|
||||
session.name = newName;
|
||||
savePersistedSessions(sessions);
|
||||
}
|
||||
});
|
||||
|
||||
// MCP Server management functions
|
||||
async function listMcpServers() {
|
||||
try {
|
||||
|
|
@ -355,12 +371,18 @@ async function listMcpServers() {
|
|||
}
|
||||
|
||||
// Parse format: "name: url (type) - status" or just "name"
|
||||
// Extract just the server name (before the colon)
|
||||
// Extract server name (before the colon) and status
|
||||
const colonIndex = line.indexOf(":");
|
||||
const serverName = colonIndex > 0 ? line.substring(0, colonIndex).trim() : line.trim();
|
||||
|
||||
// Check if server is connected (✓ Connected or similar)
|
||||
const isConnected = line.includes("✓") || line.includes("Connected");
|
||||
|
||||
if (serverName) {
|
||||
servers.push({ name: serverName });
|
||||
servers.push({
|
||||
name: serverName,
|
||||
connected: isConnected
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,13 +396,45 @@ async function listMcpServers() {
|
|||
async function addMcpServer(name: string, config: any) {
|
||||
// Use add-json to support full configuration including env vars, headers, etc.
|
||||
const jsonConfig = JSON.stringify(config);
|
||||
await execAsync(`claude mcp add-json "${name}" '${jsonConfig}'`);
|
||||
await execAsync(`claude mcp add-json --scope user "${name}" '${jsonConfig}'`);
|
||||
}
|
||||
|
||||
async function removeMcpServer(name: string) {
|
||||
await execAsync(`claude mcp remove "${name}"`);
|
||||
}
|
||||
|
||||
async function getMcpServerDetails(name: string) {
|
||||
try {
|
||||
const { stdout } = await execAsync(`claude mcp get "${name}"`);
|
||||
|
||||
// Parse the output to extract details
|
||||
const details: any = { name };
|
||||
const lines = stdout.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.includes("Scope:")) {
|
||||
details.scope = trimmed.replace("Scope:", "").trim();
|
||||
} else if (trimmed.includes("Status:")) {
|
||||
details.status = trimmed.replace("Status:", "").trim();
|
||||
} else if (trimmed.includes("Type:")) {
|
||||
details.type = trimmed.replace("Type:", "").trim();
|
||||
} else if (trimmed.includes("URL:")) {
|
||||
details.url = trimmed.replace("URL:", "").trim();
|
||||
} else if (trimmed.includes("Command:")) {
|
||||
details.command = trimmed.replace("Command:", "").trim();
|
||||
} else if (trimmed.includes("Args:")) {
|
||||
details.args = trimmed.replace("Args:", "").trim();
|
||||
}
|
||||
}
|
||||
|
||||
return details;
|
||||
} catch (error) {
|
||||
console.error("Error getting MCP server details:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle("list-mcp-servers", async () => {
|
||||
try {
|
||||
return await listMcpServers();
|
||||
|
|
@ -408,6 +462,15 @@ ipcMain.handle("remove-mcp-server", async (_event, name: string) => {
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-mcp-server-details", async (_event, name: string) => {
|
||||
try {
|
||||
return await getMcpServerDetails(name);
|
||||
} catch (error) {
|
||||
console.error("Error getting MCP server details:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
const createWindow = () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
|
|
@ -430,6 +493,14 @@ const createWindow = () => {
|
|||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
// Refresh MCP server list every minute and broadcast to all windows
|
||||
setInterval(async () => {
|
||||
const servers = await listMcpServers();
|
||||
BrowserWindow.getAllWindows().forEach(window => {
|
||||
window.webContents.send("mcp-servers-updated", servers);
|
||||
});
|
||||
}, 60000); // 60000ms = 1 minute
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
|
|
|
|||
237
renderer.ts
237
renderer.ts
|
|
@ -6,6 +6,7 @@ interface SessionConfig {
|
|||
projectDir: string;
|
||||
parentBranch: string;
|
||||
codingAgent: string;
|
||||
skipPermissions: boolean;
|
||||
}
|
||||
|
||||
interface PersistedSession {
|
||||
|
|
@ -30,6 +31,7 @@ interface Session {
|
|||
|
||||
interface McpServer {
|
||||
name: string;
|
||||
connected?: boolean;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
|
@ -153,16 +155,41 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
|
|||
item.id = `sidebar-${sessionId}`;
|
||||
item.className = "session-list-item";
|
||||
item.innerHTML = `
|
||||
<div class="flex items-center space-x-2 flex-1">
|
||||
<div class="flex items-center space-x-2 flex-1 session-name-container">
|
||||
<span class="session-indicator ${hasActivePty ? 'active' : ''}"></span>
|
||||
<span class="truncate">${name}</span>
|
||||
<span class="truncate session-name-text" data-id="${sessionId}">${name}</span>
|
||||
<input type="text" class="session-name-input hidden" data-id="${sessionId}" value="${name}" />
|
||||
</div>
|
||||
<button class="session-delete-btn" data-id="${sessionId}" title="Delete session">×</button>
|
||||
`;
|
||||
|
||||
// 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 = '<span class="loading-spinner"></span>';
|
||||
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 = `
|
||||
<div class="flex items-center space-x-2 flex-1">
|
||||
<span class="session-indicator active"></span>
|
||||
<span class="session-indicator ${indicatorClass}"></span>
|
||||
<span class="truncate">${server.name}</span>
|
||||
</div>
|
||||
<button class="session-delete-btn mcp-remove-btn" data-name="${server.name}" title="Remove server">×</button>
|
||||
`;
|
||||
|
||||
// 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 = '<div class="flex items-center justify-center py-8"><span class="loading-spinner" style="width: 24px; height: 24px; border-width: 3px;"></span></div>';
|
||||
}
|
||||
|
||||
detailsModal?.classList.remove("hidden");
|
||||
|
||||
try {
|
||||
const details = await ipcRenderer.invoke("get-mcp-server-details", name);
|
||||
|
||||
if (detailsContent) {
|
||||
let html = "";
|
||||
if (details.scope) {
|
||||
html += `<div><strong>Scope:</strong> ${details.scope}</div>`;
|
||||
}
|
||||
if (details.status) {
|
||||
html += `<div><strong>Status:</strong> ${details.status}</div>`;
|
||||
}
|
||||
if (details.type) {
|
||||
html += `<div><strong>Type:</strong> ${details.type}</div>`;
|
||||
}
|
||||
if (details.url) {
|
||||
html += `<div><strong>URL:</strong> ${details.url}</div>`;
|
||||
}
|
||||
if (details.command) {
|
||||
html += `<div><strong>Command:</strong> ${details.command}</div>`;
|
||||
}
|
||||
if (details.args) {
|
||||
html += `<div><strong>Args:</strong> ${details.args}</div>`;
|
||||
}
|
||||
|
||||
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 = `<div class="text-red-400">Failed to load server details</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
|
|
|||
36
styles.css
36
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue