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.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
.vite/
|
.vite/
|
||||||
|
|
||||||
|
.fleetcode/
|
||||||
23
index.html
23
index.html
|
|
@ -78,6 +78,13 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="btn-group">
|
||||||
<button id="cancel-session" class="btn-secondary">Cancel</button>
|
<button id="cancel-session" class="btn-secondary">Cancel</button>
|
||||||
<button id="create-session" class="btn-primary">Create Session</button>
|
<button id="create-session" class="btn-primary">Create Session</button>
|
||||||
|
|
@ -85,6 +92,22 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- MCP Server Modal -->
|
||||||
<div id="mcp-modal" class="modal-overlay hidden">
|
<div id="mcp-modal" class="modal-overlay hidden">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
|
|
|
||||||
83
main.ts
83
main.ts
|
|
@ -14,6 +14,7 @@ interface SessionConfig {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
parentBranch: string;
|
parentBranch: string;
|
||||||
codingAgent: string;
|
codingAgent: string;
|
||||||
|
skipPermissions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PersistedSession {
|
interface PersistedSession {
|
||||||
|
|
@ -120,6 +121,7 @@ ipcMain.handle("get-last-settings", () => {
|
||||||
projectDir: "",
|
projectDir: "",
|
||||||
parentBranch: "",
|
parentBranch: "",
|
||||||
codingAgent: "claude",
|
codingAgent: "claude",
|
||||||
|
skipPermissions: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -181,7 +183,8 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
||||||
|
|
||||||
// Auto-run the selected coding agent
|
// Auto-run the selected coding agent
|
||||||
if (config.codingAgent === "claude") {
|
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") {
|
} else if (config.codingAgent === "codex") {
|
||||||
ptyProcess.write("codex\r");
|
ptyProcess.write("codex\r");
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +202,8 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
||||||
|
|
||||||
// Auto-run the selected coding agent
|
// Auto-run the selected coding agent
|
||||||
if (config.codingAgent === "claude") {
|
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") {
|
} else if (config.codingAgent === "codex") {
|
||||||
ptyProcess.write("codex\r");
|
ptyProcess.write("codex\r");
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +281,8 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
|
||||||
terminalReady = true;
|
terminalReady = true;
|
||||||
|
|
||||||
if (session.config.codingAgent === "claude") {
|
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") {
|
} else if (session.config.codingAgent === "codex") {
|
||||||
ptyProcess.write("codex\r");
|
ptyProcess.write("codex\r");
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +337,17 @@ ipcMain.handle("get-all-sessions", () => {
|
||||||
return getPersistedSessions();
|
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
|
// MCP Server management functions
|
||||||
async function listMcpServers() {
|
async function listMcpServers() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -355,12 +371,18 @@ async function listMcpServers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse format: "name: url (type) - status" or just "name"
|
// 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 colonIndex = line.indexOf(":");
|
||||||
const serverName = colonIndex > 0 ? line.substring(0, colonIndex).trim() : line.trim();
|
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) {
|
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) {
|
async function addMcpServer(name: string, config: any) {
|
||||||
// Use add-json to support full configuration including env vars, headers, etc.
|
// Use add-json to support full configuration including env vars, headers, etc.
|
||||||
const jsonConfig = JSON.stringify(config);
|
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) {
|
async function removeMcpServer(name: string) {
|
||||||
await execAsync(`claude mcp remove "${name}"`);
|
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 () => {
|
ipcMain.handle("list-mcp-servers", async () => {
|
||||||
try {
|
try {
|
||||||
return await listMcpServers();
|
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 = () => {
|
const createWindow = () => {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1400,
|
width: 1400,
|
||||||
|
|
@ -430,6 +493,14 @@ const createWindow = () => {
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
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", () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
|
||||||
237
renderer.ts
237
renderer.ts
|
|
@ -6,6 +6,7 @@ interface SessionConfig {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
parentBranch: string;
|
parentBranch: string;
|
||||||
codingAgent: string;
|
codingAgent: string;
|
||||||
|
skipPermissions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PersistedSession {
|
interface PersistedSession {
|
||||||
|
|
@ -30,6 +31,7 @@ interface Session {
|
||||||
|
|
||||||
interface McpServer {
|
interface McpServer {
|
||||||
name: string;
|
name: string;
|
||||||
|
connected?: boolean;
|
||||||
command?: string;
|
command?: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
|
@ -153,16 +155,41 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
|
||||||
item.id = `sidebar-${sessionId}`;
|
item.id = `sidebar-${sessionId}`;
|
||||||
item.className = "session-list-item";
|
item.className = "session-list-item";
|
||||||
item.innerHTML = `
|
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="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>
|
</div>
|
||||||
<button class="session-delete-btn" data-id="${sessionId}" title="Delete session">×</button>
|
<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) => {
|
item.addEventListener("click", (e) => {
|
||||||
const target = e.target as HTMLElement;
|
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);
|
handleSessionClick(sessionId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -176,6 +203,62 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
|
||||||
list.appendChild(item);
|
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) {
|
function handleSessionClick(sessionId: string) {
|
||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
@ -371,12 +454,23 @@ const modal = document.getElementById("config-modal");
|
||||||
const projectDirInput = document.getElementById("project-dir") as HTMLInputElement;
|
const projectDirInput = document.getElementById("project-dir") as HTMLInputElement;
|
||||||
const parentBranchSelect = document.getElementById("parent-branch") as HTMLSelectElement;
|
const parentBranchSelect = document.getElementById("parent-branch") as HTMLSelectElement;
|
||||||
const codingAgentSelect = document.getElementById("coding-agent") 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 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");
|
||||||
|
|
||||||
let selectedDirectory = "";
|
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
|
// New session button - opens modal
|
||||||
document.getElementById("new-session")?.addEventListener("click", async () => {
|
document.getElementById("new-session")?.addEventListener("click", async () => {
|
||||||
modal?.classList.remove("hidden");
|
modal?.classList.remove("hidden");
|
||||||
|
|
@ -411,6 +505,18 @@ document.getElementById("new-session")?.addEventListener("click", async () => {
|
||||||
if (lastSettings.codingAgent) {
|
if (lastSettings.codingAgent) {
|
||||||
codingAgentSelect.value = 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
|
// Browse directory
|
||||||
|
|
@ -456,6 +562,7 @@ createBtn?.addEventListener("click", () => {
|
||||||
projectDir: selectedDirectory,
|
projectDir: selectedDirectory,
|
||||||
parentBranch: parentBranchSelect.value,
|
parentBranch: parentBranchSelect.value,
|
||||||
codingAgent: codingAgentSelect.value,
|
codingAgent: codingAgentSelect.value,
|
||||||
|
skipPermissions: codingAgentSelect.value === "claude" ? skipPermissionsCheckbox.checked : false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save settings for next time
|
// Save settings for next time
|
||||||
|
|
@ -474,12 +581,26 @@ createBtn?.addEventListener("click", () => {
|
||||||
|
|
||||||
// MCP Server management functions
|
// MCP Server management functions
|
||||||
async function loadMcpServers() {
|
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 {
|
try {
|
||||||
const servers = await ipcRenderer.invoke("list-mcp-servers");
|
const servers = await ipcRenderer.invoke("list-mcp-servers");
|
||||||
mcpServers = servers;
|
mcpServers = servers;
|
||||||
renderMcpServers();
|
renderMcpServers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load MCP servers:", 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 => {
|
mcpServers.forEach(server => {
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.className = "session-list-item";
|
item.className = "session-list-item";
|
||||||
|
const indicatorClass = server.connected ? "active" : "disconnected";
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="flex items-center space-x-2 flex-1">
|
<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>
|
<span class="truncate">${server.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="session-delete-btn mcp-remove-btn" data-name="${server.name}" title="Remove server">×</button>
|
<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");
|
const removeBtn = item.querySelector(".mcp-remove-btn");
|
||||||
removeBtn?.addEventListener("click", async (e) => {
|
removeBtn?.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
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
|
// MCP Modal handling
|
||||||
const mcpModal = document.getElementById("mcp-modal");
|
const mcpModal = document.getElementById("mcp-modal");
|
||||||
const mcpNameInput = document.getElementById("mcp-name") as HTMLInputElement;
|
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 localFields = document.getElementById("local-fields");
|
||||||
const remoteFields = document.getElementById("remote-fields");
|
const remoteFields = document.getElementById("remote-fields");
|
||||||
const cancelMcpBtn = document.getElementById("cancel-mcp");
|
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
|
// Toggle fields based on server type
|
||||||
mcpTypeSelect?.addEventListener("change", () => {
|
mcpTypeSelect?.addEventListener("change", () => {
|
||||||
|
|
@ -632,6 +818,12 @@ addMcpBtn?.addEventListener("click", async () => {
|
||||||
config.alwaysAllow = alwaysAllowInput.split(",").map(t => t.trim()).filter(t => t);
|
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 {
|
try {
|
||||||
await ipcRenderer.invoke("add-mcp-server", name, config);
|
await ipcRenderer.invoke("add-mcp-server", name, config);
|
||||||
await loadMcpServers();
|
await loadMcpServers();
|
||||||
|
|
@ -639,8 +831,43 @@ addMcpBtn?.addEventListener("click", async () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding server:", error);
|
console.error("Error adding server:", error);
|
||||||
alert(`Failed to add 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
|
// Load MCP servers on startup
|
||||||
loadMcpServers();
|
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;
|
@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 */
|
||||||
.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;
|
@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;
|
@apply bg-green-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-indicator.disconnected {
|
||||||
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
.session-delete-btn {
|
.session-delete-btn {
|
||||||
@apply text-gray-500 hover:text-red-500;
|
@apply text-gray-500 hover:text-red-500;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -64,6 +79,19 @@
|
||||||
opacity: 1;
|
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 */
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
@apply flex bg-gray-900 border-b border-gray-700 overflow-x-auto;
|
@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;
|
@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 {
|
.btn-secondary {
|
||||||
@apply bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded text-sm font-medium transition;
|
@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 {
|
.btn-group {
|
||||||
@apply flex justify-end space-x-3 mt-6;
|
@apply flex justify-end space-x-3 mt-6;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue