generated from nhcarrigan/template
feat: enhance plugin and MCP management panels
Add comprehensive management capabilities to both panels: MCP Management Panel: - Add "Add New Server" form with transport selection (STDIO/HTTP/SSE) - Add enhanced server details view showing full CLI output - Dynamic placeholder text based on transport type - Support for environment variables and headers (backend) Plugin Management Panel: - Add collapsible marketplace management section - Add/remove marketplaces from GitHub repos - List all configured marketplaces with sources - Enhanced plugin installation with marketplace syntax support Backend Commands: - add_mcp_server: Add MCP servers with transport/env/headers - get_mcp_server_details: Fetch full server details from CLI - list_marketplaces: Parse and list plugin marketplaces - add_marketplace: Add marketplace from GitHub source - remove_marketplace: Remove marketplace by name All operations use Claude CLI directly, avoiding cross-platform path issues by letting the CLI handle file system operations internally. ✨ This enhancement was built by Hikari~ 🌸
This commit is contained in:
@@ -1487,6 +1487,142 @@ pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Plugin Marketplace Commands ====================
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MarketplaceInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, String> {
|
||||||
|
tracing::debug!("Listing plugin marketplaces");
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("marketplace")
|
||||||
|
.arg("list")
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if !output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to list marketplaces: {}", error);
|
||||||
|
return Err(format!("Failed to list marketplaces: {}", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let mut marketplaces = Vec::new();
|
||||||
|
|
||||||
|
// Parse format:
|
||||||
|
// Configured marketplaces:
|
||||||
|
//
|
||||||
|
// ❯ claude-plugins-official
|
||||||
|
// Source: GitHub (anthropics/claude-plugins-official)
|
||||||
|
//
|
||||||
|
// ❯ macrodata
|
||||||
|
// Source: GitHub (ascorbic/macrodata)
|
||||||
|
|
||||||
|
let mut current_name: Option<String> = None;
|
||||||
|
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
|
||||||
|
// Look for marketplace names starting with ❯
|
||||||
|
if trimmed.starts_with("❯ ") {
|
||||||
|
current_name = Some(trimmed[2..].trim().to_string());
|
||||||
|
}
|
||||||
|
// Look for Source line
|
||||||
|
else if trimmed.starts_with("Source: ") && current_name.is_some() {
|
||||||
|
let source = trimmed[8..].trim().to_string();
|
||||||
|
marketplaces.push(MarketplaceInfo {
|
||||||
|
name: current_name.take().unwrap(),
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Found {} marketplaces", marketplaces.len());
|
||||||
|
Ok(marketplaces)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin marketplace list: {}", e);
|
||||||
|
Err(format!(
|
||||||
|
"Failed to execute claude plugin marketplace list: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_marketplace(source: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Adding marketplace: {}", source);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("marketplace")
|
||||||
|
.arg("add")
|
||||||
|
.arg(&source)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully added marketplace: {}", source);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to add marketplace {}: {}", source, error);
|
||||||
|
Err(format!("Failed to add marketplace: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin marketplace add: {}", e);
|
||||||
|
Err(format!(
|
||||||
|
"Failed to execute claude plugin marketplace add: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_marketplace(name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Removing marketplace: {}", name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("plugin")
|
||||||
|
.arg("marketplace")
|
||||||
|
.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 marketplace: {}", name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to remove marketplace {}: {}", name, error);
|
||||||
|
Err(format!("Failed to remove marketplace: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude plugin marketplace remove: {}", e);
|
||||||
|
Err(format!(
|
||||||
|
"Failed to execute claude plugin marketplace remove: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== MCP Management Commands ====================
|
// ==================== MCP Management Commands ====================
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -1632,6 +1768,89 @@ pub async fn remove_mcp_server(name: String) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_mcp_server(
|
||||||
|
name: String,
|
||||||
|
command_or_url: String,
|
||||||
|
transport: String,
|
||||||
|
env_vars: Option<Vec<String>>,
|
||||||
|
headers: Option<Vec<String>>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
tracing::debug!("Adding MCP server: {} with transport {}", name, transport);
|
||||||
|
|
||||||
|
let mut cmd = std::process::Command::new("claude");
|
||||||
|
cmd.arg("mcp").arg("add");
|
||||||
|
|
||||||
|
// Add transport flag
|
||||||
|
cmd.arg("--transport").arg(&transport);
|
||||||
|
|
||||||
|
// Add environment variables if provided
|
||||||
|
if let Some(env_vars) = env_vars {
|
||||||
|
for env_var in env_vars {
|
||||||
|
cmd.arg("-e").arg(env_var);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add headers if provided (for HTTP/SSE)
|
||||||
|
if let Some(headers) = headers {
|
||||||
|
for header in headers {
|
||||||
|
cmd.arg("-H").arg(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add name and command/URL
|
||||||
|
cmd.arg(&name).arg(&command_or_url);
|
||||||
|
|
||||||
|
let output = cmd.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::info!("Successfully added MCP server: {}", name);
|
||||||
|
Ok(message)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to add MCP server {}: {}", name, error);
|
||||||
|
Err(format!("Failed to add MCP server: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude mcp add: {}", e);
|
||||||
|
Err(format!("Failed to execute claude mcp add: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_mcp_server_details(name: String) -> Result<String, String> {
|
||||||
|
tracing::debug!("Getting detailed info for MCP server: {}", name);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("claude")
|
||||||
|
.arg("mcp")
|
||||||
|
.arg("get")
|
||||||
|
.arg(&name)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let details = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
tracing::debug!("Got MCP server details: {}", details);
|
||||||
|
Ok(details)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::error!("Failed to get MCP server details for {}: {}", name, error);
|
||||||
|
Err(format!("Failed to get server details: {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to execute claude mcp get: {}", e);
|
||||||
|
Err(format!("Failed to execute claude mcp get: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -201,9 +201,14 @@ pub fn run() {
|
|||||||
enable_plugin,
|
enable_plugin,
|
||||||
disable_plugin,
|
disable_plugin,
|
||||||
update_plugin,
|
update_plugin,
|
||||||
|
list_marketplaces,
|
||||||
|
add_marketplace,
|
||||||
|
remove_marketplace,
|
||||||
list_mcp_servers,
|
list_mcp_servers,
|
||||||
get_mcp_server,
|
get_mcp_server,
|
||||||
remove_mcp_server,
|
remove_mcp_server,
|
||||||
|
add_mcp_server,
|
||||||
|
get_mcp_server_details,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -24,6 +24,14 @@
|
|||||||
let selectedServer = $state<McpServerInfo | null>(null);
|
let selectedServer = $state<McpServerInfo | null>(null);
|
||||||
let isLoadingDetails = $state(false);
|
let isLoadingDetails = $state(false);
|
||||||
let actionInProgress = $state<string | null>(null);
|
let actionInProgress = $state<string | null>(null);
|
||||||
|
let showAddForm = $state(false);
|
||||||
|
let serverDetails = $state<string>("");
|
||||||
|
|
||||||
|
// Add server form fields
|
||||||
|
let newServerName = $state("");
|
||||||
|
let newServerUrl = $state("");
|
||||||
|
let newServerTransport = $state("stdio");
|
||||||
|
let isAdding = $state(false);
|
||||||
|
|
||||||
async function loadServers(): Promise<void> {
|
async function loadServers(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +51,7 @@
|
|||||||
isLoadingDetails = true;
|
isLoadingDetails = true;
|
||||||
error = null;
|
error = null;
|
||||||
selectedServer = await invoke<McpServerInfo>("get_mcp_server", { name });
|
selectedServer = await invoke<McpServerInfo>("get_mcp_server", { name });
|
||||||
|
serverDetails = await invoke<string>("get_mcp_server_details", { name });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = `Failed to load server details: ${e}`;
|
error = `Failed to load server details: ${e}`;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -58,6 +67,7 @@
|
|||||||
await invoke("remove_mcp_server", { name });
|
await invoke("remove_mcp_server", { name });
|
||||||
if (selectedServer?.name === name) {
|
if (selectedServer?.name === name) {
|
||||||
selectedServer = null;
|
selectedServer = null;
|
||||||
|
serverDetails = "";
|
||||||
}
|
}
|
||||||
await loadServers();
|
await loadServers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -68,6 +78,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addServer(): Promise<void> {
|
||||||
|
if (!newServerName.trim() || !newServerUrl.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isAdding = true;
|
||||||
|
error = null;
|
||||||
|
await invoke("add_mcp_server", {
|
||||||
|
name: newServerName.trim(),
|
||||||
|
commandOrUrl: newServerUrl.trim(),
|
||||||
|
transport: newServerTransport,
|
||||||
|
envVars: null,
|
||||||
|
headers: null,
|
||||||
|
});
|
||||||
|
newServerName = "";
|
||||||
|
newServerUrl = "";
|
||||||
|
newServerTransport = "stdio";
|
||||||
|
showAddForm = false;
|
||||||
|
await loadServers();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to add server: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isAdding = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTransportIcon(transport: string) {
|
function getTransportIcon(transport: string) {
|
||||||
switch (transport) {
|
switch (transport) {
|
||||||
case "http":
|
case "http":
|
||||||
@@ -135,13 +171,75 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Add Server Button -->
|
||||||
<div class="mx-4 mt-4 p-3 bg-[var(--accent-primary)]/10 border border-[var(--accent-primary)]/30 rounded-lg">
|
<div class="p-4 border-b border-[var(--border-color)]">
|
||||||
<p class="text-sm text-[var(--text-primary)]">
|
<button
|
||||||
💡 To add new MCP servers, use the Settings panel or edit your configuration directly.
|
onclick={() => (showAddForm = !showAddForm)}
|
||||||
</p>
|
class="w-full px-4 py-2 bg-[var(--accent-primary)] text-white rounded-lg text-sm font-medium hover:opacity-80 transition-opacity flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Server class="w-4 h-4" />
|
||||||
|
{showAddForm ? "Cancel" : "Add New Server"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Server Form -->
|
||||||
|
{#if showAddForm}
|
||||||
|
<div class="mx-4 mt-4 p-4 bg-[var(--bg-secondary)]/50 border border-[var(--border-color)] rounded-lg">
|
||||||
|
<h3 class="text-sm font-medium text-[var(--text-primary)] mb-3">Add MCP Server</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
||||||
|
>Server Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newServerName}
|
||||||
|
placeholder="my-server"
|
||||||
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
||||||
|
>Transport</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
bind:value={newServerTransport}
|
||||||
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
>
|
||||||
|
<option value="stdio">STDIO</option>
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
<option value="sse">SSE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1">
|
||||||
|
{newServerTransport === "stdio" ? "Command" : "URL"}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newServerUrl}
|
||||||
|
placeholder={newServerTransport === "stdio"
|
||||||
|
? "npx my-mcp-server"
|
||||||
|
: "https://mcp.example.com"}
|
||||||
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={addServer}
|
||||||
|
disabled={isAdding || !newServerName.trim() || !newServerUrl.trim()}
|
||||||
|
class="w-full 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 justify-center gap-2"
|
||||||
|
>
|
||||||
|
{#if isAdding}
|
||||||
|
<RefreshCw class="w-4 h-4 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Server class="w-4 h-4" />
|
||||||
|
{/if}
|
||||||
|
Add Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Error Display -->
|
<!-- Error Display -->
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="mx-4 mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
<div class="mx-4 mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
||||||
@@ -282,6 +380,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Full Server Details -->
|
||||||
|
{#if serverDetails}
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||||
|
>Full Details</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 whitespace-pre-wrap">{serverDetails}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="pt-4 border-t border-[var(--border-color)]">
|
<div class="pt-4 border-t border-[var(--border-color)]">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -14,14 +14,24 @@
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MarketplaceInfo {
|
||||||
|
name: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
const { onClose }: Props = $props();
|
const { onClose }: Props = $props();
|
||||||
|
|
||||||
let plugins = $state<PluginInfo[]>([]);
|
let plugins = $state<PluginInfo[]>([]);
|
||||||
|
let marketplaces = $state<MarketplaceInfo[]>([]);
|
||||||
let isLoading = $state(true);
|
let isLoading = $state(true);
|
||||||
|
let isLoadingMarketplaces = $state(false);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
let newPluginName = $state("");
|
let newPluginName = $state("");
|
||||||
let isInstalling = $state(false);
|
let isInstalling = $state(false);
|
||||||
let actionInProgress = $state<string | null>(null);
|
let actionInProgress = $state<string | null>(null);
|
||||||
|
let showMarketplaces = $state(false);
|
||||||
|
let newMarketplaceSource = $state("");
|
||||||
|
let isAddingMarketplace = $state(false);
|
||||||
|
|
||||||
async function loadPlugins(): Promise<void> {
|
async function loadPlugins(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -36,6 +46,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadMarketplaces(): Promise<void> {
|
||||||
|
try {
|
||||||
|
isLoadingMarketplaces = true;
|
||||||
|
error = null;
|
||||||
|
marketplaces = await invoke<MarketplaceInfo[]>("list_marketplaces");
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to load marketplaces: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoadingMarketplaces = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function installPlugin(): Promise<void> {
|
async function installPlugin(): Promise<void> {
|
||||||
if (!newPluginName.trim()) return;
|
if (!newPluginName.trim()) return;
|
||||||
|
|
||||||
@@ -99,6 +122,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addMarketplace(): Promise<void> {
|
||||||
|
if (!newMarketplaceSource.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isAddingMarketplace = true;
|
||||||
|
error = null;
|
||||||
|
await invoke("add_marketplace", { source: newMarketplaceSource.trim() });
|
||||||
|
newMarketplaceSource = "";
|
||||||
|
await loadMarketplaces();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to add marketplace: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isAddingMarketplace = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMarketplace(name: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
actionInProgress = name;
|
||||||
|
error = null;
|
||||||
|
await invoke("remove_marketplace", { name });
|
||||||
|
await loadMarketplaces();
|
||||||
|
} catch (e) {
|
||||||
|
error = `Failed to remove marketplace: ${e}`;
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
actionInProgress = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadPlugins();
|
loadPlugins();
|
||||||
});
|
});
|
||||||
@@ -156,11 +210,14 @@
|
|||||||
<!-- Install Plugin Section -->
|
<!-- Install Plugin Section -->
|
||||||
<div class="p-4 border-b border-[var(--border-color)]">
|
<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>
|
<h3 class="text-sm font-medium text-[var(--text-primary)] mb-2">Install New Plugin</h3>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)] mb-3">
|
||||||
|
Enter plugin name (e.g., "macrodata" or "macrodata@macrodata" for specific marketplace)
|
||||||
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newPluginName}
|
bind:value={newPluginName}
|
||||||
placeholder="Plugin name..."
|
placeholder="plugin-name or plugin@marketplace"
|
||||||
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)]"
|
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()}
|
onkeydown={(e) => e.key === "Enter" && installPlugin()}
|
||||||
disabled={isInstalling}
|
disabled={isInstalling}
|
||||||
@@ -180,6 +237,97 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Marketplace Management Section -->
|
||||||
|
<div class="p-4 border-b border-[var(--border-color)]">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
showMarketplaces = !showMarketplaces;
|
||||||
|
if (showMarketplaces && marketplaces.length === 0) {
|
||||||
|
loadMarketplaces();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="w-full text-left flex items-center justify-between text-sm font-medium text-[var(--text-primary)] hover:text-[var(--accent-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<span>Manage Marketplaces</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4 transition-transform"
|
||||||
|
class:rotate-180={showMarketplaces}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if showMarketplaces}
|
||||||
|
<div class="mt-3 space-y-3">
|
||||||
|
<!-- Add Marketplace Form -->
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)] mb-2">
|
||||||
|
Add a marketplace from GitHub (e.g., "ascorbic/macrodata")
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newMarketplaceSource}
|
||||||
|
placeholder="owner/repo"
|
||||||
|
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" && addMarketplace()}
|
||||||
|
disabled={isAddingMarketplace}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={addMarketplace}
|
||||||
|
disabled={isAddingMarketplace || !newMarketplaceSource.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 isAddingMarketplace}
|
||||||
|
<RefreshCw class="w-4 h-4 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Download class="w-4 h-4" />
|
||||||
|
{/if}
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Marketplaces List -->
|
||||||
|
{#if isLoadingMarketplaces}
|
||||||
|
<div class="flex items-center justify-center py-4">
|
||||||
|
<RefreshCw class="w-5 h-5 animate-spin text-[var(--text-secondary)]" />
|
||||||
|
</div>
|
||||||
|
{:else if marketplaces.length > 0}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each marketplaces as marketplace (marketplace.name)}
|
||||||
|
<div
|
||||||
|
class="bg-[var(--bg-secondary)]/50 rounded-lg p-3 border border-[var(--border-color)]"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-medium text-[var(--text-primary)]">{marketplace.name}</h4>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)] mt-1">{marketplace.source}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={() => removeMarketplace(marketplace.name)}
|
||||||
|
disabled={actionInProgress === marketplace.name}
|
||||||
|
class="px-2 py-1 text-red-400 hover:bg-red-500/20 rounded transition-colors disabled:opacity-40"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm text-[var(--text-secondary)] text-center py-4">
|
||||||
|
No marketplaces configured
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Error Display -->
|
<!-- Error Display -->
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="mx-4 mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
<div class="mx-4 mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
||||||
|
|||||||
Reference in New Issue
Block a user