generated from nhcarrigan/template
feat: add Plugin and MCP Server Management panels
Backend (Rust): - Add plugin management commands: list, install, uninstall, enable, disable, update - Add MCP server management commands: list, get details, remove - Integrate with Claude CLI's 'plugin' and 'mcp' subcommands - Export PluginInfo and McpServerInfo types Frontend (Svelte): - Create PluginManagementPanel component with full CRUD operations - Create McpManagementPanel component with server listing and removal - Add buttons to StatusBar for both panels - Beautiful UI with lucide-svelte icons - Theme-aware styling using CSS variables - Real-time loading states and error handling Closes #133 Closes #134
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Manager, State};
|
use tauri::{AppHandle, Manager, State};
|
||||||
use tauri_plugin_http::reqwest;
|
use tauri_plugin_http::reqwest;
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
@@ -1257,6 +1258,313 @@ pub async fn get_claude_version() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Plugin Management Commands ====================
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PluginInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_plugins() -> Result<Vec<PluginInfo>, String> {
|
||||||
|
tracing::debug!("Listing Claude Code plugins");
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("list")
|
||||||
|
.arg("--json")
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
match serde_json::from_str::<Vec<PluginInfo>>(&stdout) {
|
||||||
|
Ok(plugins) => {
|
||||||
|
tracing::info!("Listed {} plugins", plugins.len());
|
||||||
|
Ok(plugins)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to parse plugin list: {}", e);
|
||||||
|
Err(format!("Failed to parse plugin list: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to list plugins: {}", error);
|
||||||
|
Err(format!("Failed to list plugins: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin list: {}", e);
|
||||||
|
Err(format!("Failed to execute claude plugin list: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn install_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Installing plugin: {}", plugin_name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("install")
|
||||||
|
.arg(&plugin_name)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully installed plugin: {}", plugin_name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to install plugin {}: {}", plugin_name, error);
|
||||||
|
Err(format!("Failed to install plugin: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin install: {}", e);
|
||||||
|
Err(format!("Failed to execute claude plugin install: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn uninstall_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Uninstalling plugin: {}", plugin_name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("uninstall")
|
||||||
|
.arg(&plugin_name)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully uninstalled plugin: {}", plugin_name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to uninstall plugin {}: {}", plugin_name, error);
|
||||||
|
Err(format!("Failed to uninstall plugin: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin uninstall: {}", e);
|
||||||
|
Err(format!("Failed to execute claude plugin uninstall: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Enabling plugin: {}", plugin_name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("enable")
|
||||||
|
.arg(&plugin_name)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully enabled plugin: {}", plugin_name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to enable plugin {}: {}", plugin_name, error);
|
||||||
|
Err(format!("Failed to enable plugin: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin enable: {}", e);
|
||||||
|
Err(format!("Failed to execute claude plugin enable: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Disabling plugin: {}", plugin_name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("disable")
|
||||||
|
.arg(&plugin_name)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully disabled plugin: {}", plugin_name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to disable plugin {}: {}", plugin_name, error);
|
||||||
|
Err(format!("Failed to disable plugin: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin disable: {}", e);
|
||||||
|
Err(format!("Failed to execute claude plugin disable: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Updating plugin: {}", plugin_name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("update")
|
||||||
|
.arg(&plugin_name)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully updated plugin: {}", plugin_name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to update plugin {}: {}", plugin_name, error);
|
||||||
|
Err(format!("Failed to update plugin: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin update: {}", e);
|
||||||
|
Err(format!("Failed to execute claude plugin update: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MCP Management Commands ====================
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct McpServerInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub command: Option<String>,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub transport: String, // "stdio", "http", or "sse"
|
||||||
|
pub env: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_mcp_servers() -> Result<Vec<McpServerInfo>, String> {
|
||||||
|
tracing::debug!("Listing MCP servers");
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("mcp")
|
||||||
|
.arg("list")
|
||||||
|
.arg("--json")
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
match serde_json::from_str::<Vec<McpServerInfo>>(&stdout) {
|
||||||
|
Ok(servers) => {
|
||||||
|
tracing::info!("Listed {} MCP servers", servers.len());
|
||||||
|
Ok(servers)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to parse MCP server list: {}", e);
|
||||||
|
Err(format!("Failed to parse MCP server list: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to list MCP servers: {}", error);
|
||||||
|
Err(format!("Failed to list MCP servers: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude mcp list: {}", e);
|
||||||
|
Err(format!("Failed to execute claude mcp list: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_mcp_server(name: String) -> Result<McpServerInfo, String> {
|
||||||
|
tracing::debug!("Getting MCP server details: {}", name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("mcp")
|
||||||
|
.arg("get")
|
||||||
|
.arg(&name)
|
||||||
|
.arg("--json")
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
match serde_json::from_str::<McpServerInfo>(&stdout) {
|
||||||
|
Ok(server) => {
|
||||||
|
tracing::info!("Got MCP server details for: {}", name);
|
||||||
|
Ok(server)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to parse MCP server details: {}", e);
|
||||||
|
Err(format!("Failed to parse MCP server details: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to get MCP server {}: {}", name, error);
|
||||||
|
Err(format!("Failed to get MCP server: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude mcp get: {}", e);
|
||||||
|
Err(format!("Failed to execute claude mcp get: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_mcp_server(name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Removing MCP server: {}", name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("mcp")
|
||||||
|
.arg("remove")
|
||||||
|
.arg(&name)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully removed MCP server: {}", name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to remove MCP server {}: {}", name, error);
|
||||||
|
Err(format!("Failed to remove MCP server: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude mcp remove: {}", e);
|
||||||
|
Err(format!("Failed to execute claude mcp remove: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -195,6 +195,15 @@ pub fn run() {
|
|||||||
close_application,
|
close_application,
|
||||||
list_memory_files,
|
list_memory_files,
|
||||||
get_claude_version,
|
get_claude_version,
|
||||||
|
list_plugins,
|
||||||
|
install_plugin,
|
||||||
|
uninstall_plugin,
|
||||||
|
enable_plugin,
|
||||||
|
disable_plugin,
|
||||||
|
update_plugin,
|
||||||
|
list_mcp_servers,
|
||||||
|
get_mcp_server,
|
||||||
|
remove_mcp_server,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Trash2, RefreshCw, Server, Globe, Terminal } from "lucide-svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpServerInfo {
|
||||||
|
name: string;
|
||||||
|
command: string | null;
|
||||||
|
url: string | null;
|
||||||
|
transport: string; // "stdio", "http", or "sse"
|
||||||
|
env: any | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onClose }: Props = $props();
|
||||||
|
|
||||||
|
let servers = $state<McpServerInfo[]>([]);
|
||||||
|
let isLoading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let selectedServer = $state<McpServerInfo | null>(null);
|
||||||
|
let isLoadingDetails = $state(false);
|
||||||
|
let actionInProgress = $state<string | null>(null);
|
||||||
|
|
||||||
|
async function loadServers(): Promise<void> {
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
servers = await invoke<McpServerInfo[]>("list_mcp_servers");
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to load MCP servers: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadServerDetails(name: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
isLoadingDetails = true;
|
||||||
|
error = null;
|
||||||
|
selectedServer = await invoke<McpServerInfo>("get_mcp_server", { name });
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to load server details: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoadingDetails = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeServer(name: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
actionInProgress = name;
|
||||||
|
error = null;
|
||||||
|
await invoke("remove_mcp_server", { name });
|
||||||
|
if (selectedServer?.name === name) {
|
||||||
|
selectedServer = null;
|
||||||
|
}
|
||||||
|
await loadServers();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to remove server: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
actionInProgress = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransportIcon(transport: string) {
|
||||||
|
switch (transport) {
|
||||||
|
case "http":
|
||||||
|
return Globe;
|
||||||
|
case "stdio":
|
||||||
|
return Terminal;
|
||||||
|
case "sse":
|
||||||
|
return Server;
|
||||||
|
default:
|
||||||
|
return Server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransportColor(transport: string) {
|
||||||
|
switch (transport) {
|
||||||
|
case "http":
|
||||||
|
return "text-blue-400";
|
||||||
|
case "stdio":
|
||||||
|
return "text-green-400";
|
||||||
|
case "sse":
|
||||||
|
return "text-purple-400";
|
||||||
|
default:
|
||||||
|
return "text-[var(--text-secondary)]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadServers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed top-0 right-0 h-full w-[700px] bg-[var(--bg-primary)] border-l border-[var(--accent-primary)]/30 shadow-2xl flex flex-col z-50"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between p-4 border-b border-[var(--accent-primary)]/30">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="text-[var(--accent-primary)]">
|
||||||
|
<Server class="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]">MCP Server Management</h2>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)]">
|
||||||
|
{servers.length} server{servers.length !== 1 ? "s" : ""} configured
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={onClose}
|
||||||
|
class="text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors p-1 rounded-lg hover:bg-[var(--bg-secondary)]"
|
||||||
|
aria-label="Close MCP panel"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Box -->
|
||||||
|
<div class="mx-4 mt-4 p-3 bg-[var(--accent-primary)]/10 border border-[var(--accent-primary)]/30 rounded-lg">
|
||||||
|
<p class="text-sm text-[var(--text-primary)]">
|
||||||
|
💡 To add new MCP servers, use the Settings panel or edit your configuration directly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Display -->
|
||||||
|
{#if error}
|
||||||
|
<div class="mx-4 mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
||||||
|
<p class="text-sm text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-4 flex gap-4">
|
||||||
|
<!-- Server List -->
|
||||||
|
<div class="flex-1">
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="flex items-center justify-center h-full text-[var(--text-secondary)]">
|
||||||
|
<RefreshCw class="w-8 h-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
{:else if servers.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center h-full text-[var(--text-secondary)]">
|
||||||
|
<Server class="w-16 h-16 mb-4 opacity-50" />
|
||||||
|
<p class="text-center">No MCP servers configured</p>
|
||||||
|
<p class="text-sm text-center mt-2">Add servers via Settings</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each servers as server (server.name)}
|
||||||
|
<button
|
||||||
|
onclick={() => loadServerDetails(server.name)}
|
||||||
|
class="w-full bg-[var(--bg-secondary)]/50 rounded-lg p-3 border border-[var(--border-color)] hover:border-[var(--accent-primary)]/50 transition-all text-left"
|
||||||
|
class:border-[var(--accent-primary)]={selectedServer?.name === server.name}
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-medium text-[var(--text-primary)] flex items-center gap-2">
|
||||||
|
<svelte:component
|
||||||
|
this={getTransportIcon(server.transport)}
|
||||||
|
class="w-4 h-4 {getTransportColor(server.transport)}"
|
||||||
|
/>
|
||||||
|
{server.name}
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)] mt-1">
|
||||||
|
{server.transport.toUpperCase()}
|
||||||
|
{#if server.url}
|
||||||
|
• {server.url}
|
||||||
|
{:else if server.command}
|
||||||
|
• {server.command}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Details Panel -->
|
||||||
|
{#if selectedServer}
|
||||||
|
<div class="w-80 bg-[var(--bg-secondary)]/50 rounded-lg p-4 border border-[var(--border-color)]">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-4">Server Details</h3>
|
||||||
|
|
||||||
|
{#if isLoadingDetails}
|
||||||
|
<div class="flex items-center justify-center h-32">
|
||||||
|
<RefreshCw class="w-6 h-6 animate-spin text-[var(--text-secondary)]" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||||
|
>Name</label
|
||||||
|
>
|
||||||
|
<p class="text-sm text-[var(--text-primary)] mt-1">{selectedServer.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transport -->
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||||
|
>Transport</label
|
||||||
|
>
|
||||||
|
<p class="text-sm text-[var(--text-primary)] mt-1 flex items-center gap-2">
|
||||||
|
<svelte:component
|
||||||
|
this={getTransportIcon(selectedServer.transport)}
|
||||||
|
class="w-4 h-4 {getTransportColor(selectedServer.transport)}"
|
||||||
|
/>
|
||||||
|
{selectedServer.transport.toUpperCase()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL or Command -->
|
||||||
|
{#if selectedServer.url}
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||||
|
>URL</label
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="text-sm text-[var(--text-primary)] mt-1 break-all font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)]"
|
||||||
|
>
|
||||||
|
{selectedServer.url}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedServer.command}
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||||
|
>Command</label
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="text-sm text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)]"
|
||||||
|
>
|
||||||
|
{selectedServer.command}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Environment Variables -->
|
||||||
|
{#if selectedServer.env}
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||||
|
>Environment</label
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="text-xs text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)] overflow-x-auto">{JSON.stringify(selectedServer.env, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="pt-4 border-t border-[var(--border-color)]">
|
||||||
|
<button
|
||||||
|
onclick={() => removeServer(selectedServer.name)}
|
||||||
|
disabled={actionInProgress === selectedServer.name}
|
||||||
|
class="w-full px-4 py-2 bg-red-500/20 border border-red-500/30 rounded-lg text-sm text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
Remove Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Download, Trash2, Power, PowerOff, RefreshCw } from "lucide-svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginInfo {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description: string | null;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onClose }: Props = $props();
|
||||||
|
|
||||||
|
let plugins = $state<PluginInfo[]>([]);
|
||||||
|
let isLoading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let newPluginName = $state("");
|
||||||
|
let isInstalling = $state(false);
|
||||||
|
let actionInProgress = $state<string | null>(null);
|
||||||
|
|
||||||
|
async function loadPlugins(): Promise<void> {
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
plugins = await invoke<PluginInfo[]>("list_plugins");
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to load plugins: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installPlugin(): Promise<void> {
|
||||||
|
if (!newPluginName.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isInstalling = true;
|
||||||
|
error = null;
|
||||||
|
await invoke("install_plugin", { pluginName: newPluginName.trim() });
|
||||||
|
newPluginName = "";
|
||||||
|
await loadPlugins();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to install plugin: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isInstalling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uninstallPlugin(pluginName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
actionInProgress = pluginName;
|
||||||
|
error = null;
|
||||||
|
await invoke("uninstall_plugin", { pluginName });
|
||||||
|
await loadPlugins();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to uninstall plugin: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
actionInProgress = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function togglePlugin(plugin: PluginInfo): Promise<void> {
|
||||||
|
try {
|
||||||
|
actionInProgress = plugin.name;
|
||||||
|
error = null;
|
||||||
|
if (plugin.enabled) {
|
||||||
|
await invoke("disable_plugin", { pluginName: plugin.name });
|
||||||
|
} else {
|
||||||
|
await invoke("enable_plugin", { pluginName: plugin.name });
|
||||||
|
}
|
||||||
|
await loadPlugins();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to ${plugin.enabled ? "disable" : "enable"} plugin: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
actionInProgress = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePlugin(pluginName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
actionInProgress = pluginName;
|
||||||
|
error = null;
|
||||||
|
await invoke("update_plugin", { pluginName });
|
||||||
|
await loadPlugins();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to update plugin: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
actionInProgress = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadPlugins();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed top-0 right-0 h-full w-[600px] bg-[var(--bg-primary)] border-l border-[var(--accent-primary)]/30 shadow-2xl flex flex-col z-50"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between p-4 border-b border-[var(--accent-primary)]/30">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="text-[var(--accent-primary)]">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Plugin Management</h2>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)]">
|
||||||
|
{plugins.length} plugin{plugins.length !== 1 ? "s" : ""} installed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={onClose}
|
||||||
|
class="text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors p-1 rounded-lg hover:bg-[var(--bg-secondary)]"
|
||||||
|
aria-label="Close plugin panel"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Install Plugin Section -->
|
||||||
|
<div class="p-4 border-b border-[var(--border-color)]">
|
||||||
|
<h3 class="text-sm font-medium text-[var(--text-primary)] mb-2">Install New Plugin</h3>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newPluginName}
|
||||||
|
placeholder="Plugin name..."
|
||||||
|
class="flex-1 px-3 py-2 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
onkeydown={(e) => e.key === "Enter" && installPlugin()}
|
||||||
|
disabled={isInstalling}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={installPlugin}
|
||||||
|
disabled={isInstalling || !newPluginName.trim()}
|
||||||
|
class="px-4 py-2 bg-[var(--accent-primary)] text-white rounded-lg text-sm font-medium hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{#if isInstalling}
|
||||||
|
<RefreshCw class="w-4 h-4 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Download class="w-4 h-4" />
|
||||||
|
{/if}
|
||||||
|
Install
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Display -->
|
||||||
|
{#if error}
|
||||||
|
<div class="mx-4 mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
||||||
|
<p class="text-sm text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Plugins List -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-4">
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="flex items-center justify-center h-full text-[var(--text-secondary)]">
|
||||||
|
<RefreshCw class="w-8 h-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
{:else if plugins.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center h-full text-[var(--text-secondary)]">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-16 w-16 mb-4 opacity-50"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-center">No plugins installed</p>
|
||||||
|
<p class="text-sm text-center mt-2">Install a plugin using the form above</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-3">
|
||||||
|
{#each plugins as plugin (plugin.name)}
|
||||||
|
<div
|
||||||
|
class="bg-[var(--bg-secondary)]/50 rounded-lg p-4 border border-[var(--border-color)] hover:border-[var(--accent-primary)]/50 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-medium text-[var(--text-primary)] flex items-center gap-2">
|
||||||
|
{plugin.name}
|
||||||
|
{#if plugin.enabled}
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-[var(--success-color)]/20 text-[var(--success-color)] text-xs rounded border border-[var(--success-color)]/30"
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-[var(--text-secondary)]/20 text-[var(--text-secondary)] text-xs rounded border border-[var(--border-color)]"
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)] mt-1">v{plugin.version}</p>
|
||||||
|
{#if plugin.description}
|
||||||
|
<p class="text-sm text-[var(--text-secondary)] mt-2">{plugin.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-3">
|
||||||
|
<button
|
||||||
|
onclick={() => togglePlugin(plugin)}
|
||||||
|
disabled={actionInProgress === plugin.name}
|
||||||
|
class="flex-1 px-3 py-1.5 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] hover:border-[var(--accent-primary)]/50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{#if plugin.enabled}
|
||||||
|
<PowerOff class="w-4 h-4" />
|
||||||
|
Disable
|
||||||
|
{:else}
|
||||||
|
<Power class="w-4 h-4" />
|
||||||
|
Enable
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => updatePlugin(plugin.name)}
|
||||||
|
disabled={actionInProgress === plugin.name}
|
||||||
|
class="px-3 py-1.5 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] hover:border-[var(--accent-primary)]/50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCw class="w-4 h-4" />
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => uninstallPlugin(plugin.name)}
|
||||||
|
disabled={actionInProgress === plugin.name}
|
||||||
|
class="px-3 py-1.5 bg-red-500/20 border border-red-500/30 rounded text-sm text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
Uninstall
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,8 @@
|
|||||||
import GitPanel from "./GitPanel.svelte";
|
import GitPanel from "./GitPanel.svelte";
|
||||||
import ProfilePanel from "./ProfilePanel.svelte";
|
import ProfilePanel from "./ProfilePanel.svelte";
|
||||||
import AgentMonitorPanel from "./AgentMonitorPanel.svelte";
|
import AgentMonitorPanel from "./AgentMonitorPanel.svelte";
|
||||||
|
import PluginManagementPanel from "./PluginManagementPanel.svelte";
|
||||||
|
import McpManagementPanel from "./McpManagementPanel.svelte";
|
||||||
import { conversationsStore } from "$lib/stores/conversations";
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
import {
|
import {
|
||||||
generateContextInjection,
|
generateContextInjection,
|
||||||
@@ -54,6 +56,8 @@
|
|||||||
let showGitPanel = $state(false);
|
let showGitPanel = $state(false);
|
||||||
let showProfile = $state(false);
|
let showProfile = $state(false);
|
||||||
let showAgentMonitor = $state(false);
|
let showAgentMonitor = $state(false);
|
||||||
|
let showPluginPanel = $state(false);
|
||||||
|
let showMcpPanel = $state(false);
|
||||||
let isSummarising = $state(false);
|
let isSummarising = $state(false);
|
||||||
const progress = $derived($achievementProgress);
|
const progress = $derived($achievementProgress);
|
||||||
const activeAgentCount = $derived($runningAgentCount);
|
const activeAgentCount = $derived($runningAgentCount);
|
||||||
@@ -468,6 +472,34 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => (showPluginPanel = true)}
|
||||||
|
class="p-1 text-gray-500 icon-trans-hover"
|
||||||
|
title="Plugin Management"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => (showMcpPanel = true)}
|
||||||
|
class="p-1 text-gray-500 icon-trans-hover"
|
||||||
|
title="MCP Server Management"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={toggleEditor}
|
onclick={toggleEditor}
|
||||||
disabled={connectionStatus !== "connected"}
|
disabled={connectionStatus !== "connected"}
|
||||||
@@ -704,3 +736,11 @@
|
|||||||
{#if showAgentMonitor}
|
{#if showAgentMonitor}
|
||||||
<AgentMonitorPanel isOpen={showAgentMonitor} onClose={() => (showAgentMonitor = false)} />
|
<AgentMonitorPanel isOpen={showAgentMonitor} onClose={() => (showAgentMonitor = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showPluginPanel}
|
||||||
|
<PluginManagementPanel onClose={() => (showPluginPanel = false)} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showMcpPanel}
|
||||||
|
<McpManagementPanel onClose={() => (showMcpPanel = false)} />
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user