feat: UI improvements
This commit is contained in:
parent
bc07e0723d
commit
10e5ad1396
|
|
@ -35,7 +35,7 @@ export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsF
|
|||
export { default as ChatSettingsFields } from './chat/ChatSettings/ChatSettingsFields.svelte';
|
||||
export { default as ChatSettingsImportExportTab } from './chat/ChatSettings/ChatSettingsImportExportTab.svelte';
|
||||
export { default as ChatSettingsParameterSourceIndicator } from './chat/ChatSettings/ChatSettingsParameterSourceIndicator.svelte';
|
||||
export { default as McpSettingsSection } from './chat/ChatSettings/McpSettingsSection.svelte';
|
||||
export { default as McpSettingsSection } from './mcp/McpSettingsSection.svelte';
|
||||
|
||||
export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte';
|
||||
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { cn } from '$lib/components/ui/utils';
|
||||
import { SearchableDropdownMenu } from '$lib/components/app';
|
||||
import McpLogo from '$lib/components/app/misc/McpLogo.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { parseMcpServerSettings, parseMcpServerUsageStats } from '$lib/config/mcp';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||
|
|
@ -24,38 +24,32 @@
|
|||
|
||||
let { class: className = '', disabled = false, onSettingsClick }: Props = $props();
|
||||
|
||||
let currentConfig = $derived(config());
|
||||
let searchQuery = $state('');
|
||||
|
||||
// MCP servers state
|
||||
let mcpServers = $derived<MCPServerSettingsEntry[]>(
|
||||
parseMcpServerSettings(currentConfig.mcpServers)
|
||||
);
|
||||
let mcpServers = $derived.by(() => {
|
||||
return parseMcpServerSettings(settingsStore.config.mcpServers);
|
||||
});
|
||||
|
||||
// Usage stats for sorting by popularity
|
||||
let mcpUsageStats = $derived(parseMcpServerUsageStats(currentConfig.mcpServerUsageStats));
|
||||
let hasMcpServers = $derived(mcpServers.length > 0);
|
||||
|
||||
let mcpUsageStats = $derived(parseMcpServerUsageStats(settingsStore.config.mcpServerUsageStats));
|
||||
|
||||
// Get usage count for a server
|
||||
function getServerUsageCount(serverId: string): number {
|
||||
return mcpUsageStats[serverId] || 0;
|
||||
}
|
||||
|
||||
// Helper to check if server is enabled for current chat (per-chat override or global)
|
||||
function isServerEnabledForChat(server: MCPServerSettingsEntry): boolean {
|
||||
return conversationsStore.isMcpServerEnabledForChat(server.id, server.enabled);
|
||||
}
|
||||
|
||||
// Helper to check if server has per-chat override
|
||||
function hasPerChatOverride(serverId: string): boolean {
|
||||
return conversationsStore.getMcpServerOverride(serverId) !== undefined;
|
||||
}
|
||||
|
||||
// Servers enabled for current chat (considering per-chat overrides)
|
||||
let enabledMcpServersForChat = $derived(
|
||||
mcpServers.filter((s) => isServerEnabledForChat(s) && s.url.trim())
|
||||
);
|
||||
|
||||
// Filter out servers with health check errors
|
||||
let healthyEnabledMcpServers = $derived(
|
||||
enabledMcpServersForChat.filter((s) => {
|
||||
const healthState = mcpGetHealthCheckState(s.id);
|
||||
|
|
@ -65,21 +59,21 @@
|
|||
|
||||
let hasEnabledMcpServers = $derived(enabledMcpServersForChat.length > 0);
|
||||
|
||||
// Sort servers: globally enabled first (by popularity), then rest (by popularity)
|
||||
let sortedMcpServers = $derived(
|
||||
[...mcpServers].sort((a, b) => {
|
||||
// First: globally enabled servers come first
|
||||
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
||||
|
||||
// Then sort by usage count (descending)
|
||||
const usageA = getServerUsageCount(a.id);
|
||||
const usageB = getServerUsageCount(b.id);
|
||||
if (usageB !== usageA) return usageB - usageA;
|
||||
|
||||
// Then alphabetically by name
|
||||
return getServerDisplayName(a).localeCompare(getServerDisplayName(b));
|
||||
})
|
||||
);
|
||||
|
||||
// Filtered servers for display
|
||||
let filteredMcpServers = $derived(() => {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
if (query) {
|
||||
|
|
@ -89,19 +83,16 @@
|
|||
return name.includes(query) || url.includes(query);
|
||||
});
|
||||
}
|
||||
// When not searching, show max 4 items
|
||||
|
||||
return sortedMcpServers.slice(0, 4);
|
||||
});
|
||||
|
||||
// Count of extra servers beyond the 3 shown as favicons (excluding error servers)
|
||||
let extraServersCount = $derived(Math.max(0, healthyEnabledMcpServers.length - 3));
|
||||
|
||||
// Toggle server enabled state for current chat (per-chat override only)
|
||||
async function toggleServerForChat(serverId: string, globalEnabled: boolean) {
|
||||
await conversationsStore.toggleMcpServerForChat(serverId, globalEnabled);
|
||||
}
|
||||
|
||||
// Get display name for server
|
||||
function getServerDisplayName(server: MCPServerSettingsEntry): string {
|
||||
if (server.name) return server.name;
|
||||
try {
|
||||
|
|
@ -114,7 +105,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Get favicon URL for server
|
||||
function getFaviconUrl(server: MCPServerSettingsEntry): string | null {
|
||||
try {
|
||||
const url = new URL(server.url);
|
||||
|
|
@ -134,10 +124,8 @@
|
|||
.filter((f) => f.url !== null)
|
||||
);
|
||||
|
||||
// All servers with valid URLs (for health checks)
|
||||
let serversWithUrls = $derived(mcpServers.filter((s) => s.url.trim()));
|
||||
|
||||
// Run health checks on mount for ALL servers with URLs
|
||||
onMount(() => {
|
||||
for (const server of serversWithUrls) {
|
||||
if (!mcpHasHealthCheck(server.id)) {
|
||||
|
|
@ -147,93 +135,110 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<SearchableDropdownMenu
|
||||
bind:searchValue={searchQuery}
|
||||
placeholder="Search servers..."
|
||||
emptyMessage="No servers found"
|
||||
isEmpty={filteredMcpServers().length === 0}
|
||||
{disabled}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
'inline-flex cursor-pointer items-center rounded-sm bg-muted-foreground/10 px-1.5 py-1 text-xs transition hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
hasEnabledMcpServers ? 'text-foreground' : 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{disabled}
|
||||
aria-label="MCP Servers"
|
||||
>
|
||||
<McpLogo style="width: 0.875rem; height: 0.875rem;" />
|
||||
{#if hasMcpServers}
|
||||
<SearchableDropdownMenu
|
||||
bind:searchValue={searchQuery}
|
||||
placeholder="Search servers..."
|
||||
emptyMessage="No servers found"
|
||||
isEmpty={filteredMcpServers().length === 0}
|
||||
{disabled}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
'inline-flex cursor-pointer items-center rounded-sm bg-muted-foreground/10 px-1.5 py-1 text-xs transition hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
hasEnabledMcpServers ? 'text-foreground' : 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{disabled}
|
||||
aria-label="MCP Servers"
|
||||
>
|
||||
<McpLogo style="width: 0.875rem; height: 0.875rem;" />
|
||||
|
||||
<span class="mx-1.5 font-medium">MCP</span>
|
||||
<span class="mx-1.5 font-medium">MCP</span>
|
||||
|
||||
{#if hasEnabledMcpServers && mcpFavicons.length > 0}
|
||||
<div class="flex -space-x-1">
|
||||
{#each mcpFavicons as favicon (favicon.id)}
|
||||
{#if hasEnabledMcpServers && mcpFavicons.length > 0}
|
||||
<div class="flex -space-x-1">
|
||||
{#each mcpFavicons as favicon (favicon.id)}
|
||||
<img
|
||||
src={favicon.url}
|
||||
alt=""
|
||||
class="h-3.5 w-3.5 rounded-sm"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if extraServersCount > 0}
|
||||
<span class="ml-1 text-muted-foreground">+{extraServersCount}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<ChevronDown class="h-3 w-3.5" />
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#each filteredMcpServers() as server (server.id)}
|
||||
{@const healthState = mcpGetHealthCheckState(server.id)}
|
||||
{@const hasError = healthState.status === 'error'}
|
||||
{@const isEnabledForChat = isServerEnabledForChat(server)}
|
||||
{@const hasOverride = hasPerChatOverride(server.id)}
|
||||
|
||||
<div class="flex items-center justify-between gap-2 px-2 py-2">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
{#if getFaviconUrl(server)}
|
||||
<img
|
||||
src={favicon.url}
|
||||
src={getFaviconUrl(server)}
|
||||
alt=""
|
||||
class="h-3.5 w-3.5 rounded-sm"
|
||||
class="h-4 w-4 shrink-0 rounded-sm"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
<span class="truncate text-sm">{getServerDisplayName(server)}</span>
|
||||
{#if hasError}
|
||||
<span class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
|
||||
>Error</span
|
||||
>
|
||||
{:else if server.enabled}
|
||||
<span class="shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-xs text-primary"
|
||||
>Global</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if extraServersCount > 0}
|
||||
<span class="ml-1 text-muted-foreground">+{extraServersCount}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<ChevronDown class="h-3 w-3.5" />
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#each filteredMcpServers() as server (server.id)}
|
||||
{@const healthState = mcpGetHealthCheckState(server.id)}
|
||||
{@const hasError = healthState.status === 'error'}
|
||||
{@const isEnabledForChat = isServerEnabledForChat(server)}
|
||||
{@const hasOverride = hasPerChatOverride(server.id)}
|
||||
|
||||
<div class="flex items-center justify-between gap-2 px-2 py-2">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
{#if getFaviconUrl(server)}
|
||||
<img
|
||||
src={getFaviconUrl(server)}
|
||||
alt=""
|
||||
class="h-4 w-4 shrink-0 rounded-sm"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<span class="truncate text-sm">{getServerDisplayName(server)}</span>
|
||||
{#if hasError}
|
||||
<span class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
|
||||
>Error</span
|
||||
>
|
||||
{:else if server.enabled}
|
||||
<span class="shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-xs text-primary"
|
||||
>Global</span
|
||||
>
|
||||
{/if}
|
||||
<Switch
|
||||
checked={isEnabledForChat}
|
||||
onCheckedChange={() => toggleServerForChat(server.id, server.enabled)}
|
||||
disabled={hasError}
|
||||
class={hasOverride ? 'ring-2 ring-primary/50 ring-offset-1' : ''}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabledForChat}
|
||||
onCheckedChange={() => toggleServerForChat(server.id, server.enabled)}
|
||||
disabled={hasError}
|
||||
class={hasOverride ? 'ring-2 ring-primary/50 ring-offset-1' : ''}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#snippet footer()}
|
||||
<DropdownMenu.Item class="flex cursor-pointer items-center gap-2" onclick={onSettingsClick}>
|
||||
<Settings class="h-4 w-4" />
|
||||
<span>Manage MCP Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
{/snippet}
|
||||
</SearchableDropdownMenu>
|
||||
{#snippet footer()}
|
||||
<DropdownMenu.Item class="flex cursor-pointer items-center gap-2" onclick={onSettingsClick}>
|
||||
<Settings class="h-4 w-4" />
|
||||
<span>Manage MCP Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
{/snippet}
|
||||
</SearchableDropdownMenu>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
'inline-flex cursor-pointer items-center rounded-sm bg-muted-foreground/10 px-1.5 py-1 text-xs transition hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{disabled}
|
||||
aria-label="MCP Servers"
|
||||
onclick={onSettingsClick}
|
||||
>
|
||||
<McpLogo style="width: 0.875rem; height: 0.875rem;" />
|
||||
<span class="mx-1.5 font-medium">MCP</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
ChevronDown,
|
||||
ChevronRight,
|
||||
Pencil,
|
||||
Check,
|
||||
X,
|
||||
ExternalLink
|
||||
} from '@lucide/svelte';
|
||||
|
|
@ -15,7 +14,8 @@
|
|||
import { Switch } from '$lib/components/ui/switch';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import McpServerForm from './McpServerForm.svelte';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import McpServerForm from '$lib/components/app/mcp/McpServerForm.svelte';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||
import {
|
||||
mcpGetHealthCheckState,
|
||||
|
|
@ -36,7 +36,6 @@
|
|||
|
||||
let { server, displayName, faviconUrl, onToggle, onUpdate, onDelete }: Props = $props();
|
||||
|
||||
// Get health state from store
|
||||
let healthState = $derived<HealthCheckState>(mcpGetHealthCheckState(server.id));
|
||||
let isHealthChecking = $derived(healthState.status === 'loading');
|
||||
let isConnected = $derived(healthState.status === 'success');
|
||||
|
|
@ -45,15 +44,14 @@
|
|||
let tools = $derived(healthState.status === 'success' ? healthState.tools : []);
|
||||
let toolsCount = $derived(tools.length);
|
||||
|
||||
// Expandable details state
|
||||
let isExpanded = $state(false);
|
||||
|
||||
// Edit mode state - default to edit mode if no URL
|
||||
let showDeleteDialog = $state(false);
|
||||
|
||||
let isEditing = $state(!server.url.trim());
|
||||
let editUrl = $state(server.url);
|
||||
let editHeaders = $state(server.headers || '');
|
||||
|
||||
// Validation
|
||||
let urlError = $derived.by(() => {
|
||||
if (!editUrl.trim()) return 'URL is required';
|
||||
try {
|
||||
|
|
@ -66,7 +64,6 @@
|
|||
|
||||
let canSave = $derived(!urlError);
|
||||
|
||||
// Run health check on first mount if not already checked and server is enabled with URL
|
||||
onMount(() => {
|
||||
if (!mcpHasHealthCheck(server.id) && server.enabled && server.url.trim()) {
|
||||
mcpRunHealthCheck(server);
|
||||
|
|
@ -84,11 +81,12 @@
|
|||
}
|
||||
|
||||
function cancelEditing() {
|
||||
// Only allow cancel if server has valid URL
|
||||
if (server.url.trim()) {
|
||||
editUrl = server.url;
|
||||
editHeaders = server.headers || '';
|
||||
isEditing = false;
|
||||
} else {
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,51 +97,27 @@
|
|||
headers: editHeaders.trim() || undefined
|
||||
});
|
||||
isEditing = false;
|
||||
// Run health check after saving
|
||||
|
||||
if (server.enabled && editUrl.trim()) {
|
||||
setTimeout(() => mcpRunHealthCheck({ ...server, url: editUrl.trim() }), 100);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="!gap-1.5 bg-muted/30 p-4">
|
||||
<Card.Root class="!gap-4 bg-muted/30 p-4">
|
||||
{#if isEditing}
|
||||
<!-- Edit Mode -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="font-medium">Configure Server</p>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if server.url.trim()}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
onclick={cancelEditing}
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-700 dark:text-green-500 dark:hover:bg-green-950"
|
||||
onclick={saveEditing}
|
||||
disabled={!canSave}
|
||||
aria-label="Save"
|
||||
>
|
||||
<Check class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="hover:text-destructive-foreground h-7 w-7 text-destructive hover:bg-destructive"
|
||||
onclick={onDelete}
|
||||
aria-label="Delete"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
onclick={cancelEditing}
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<McpServerForm
|
||||
|
|
@ -154,12 +128,21 @@
|
|||
urlError={editUrl ? urlError : null}
|
||||
id={server.id}
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onclick={saveEditing}
|
||||
disabled={!canSave}
|
||||
aria-label="Save"
|
||||
>
|
||||
{server.url.trim() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- View Mode -->
|
||||
<!-- Header Row -->
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<!-- Left: Favicon + Name + Status Badge -->
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
{#if faviconUrl}
|
||||
<img
|
||||
|
|
@ -199,18 +182,15 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Right: Switch -->
|
||||
<div class="flex shrink-0 items-center">
|
||||
<Switch checked={server.enabled} onCheckedChange={onToggle} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if isError && errorMessage}
|
||||
<p class="mt-3 text-xs text-destructive">{errorMessage}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Actions Row (when no tools available) -->
|
||||
{#if tools.length === 0 && server.url.trim()}
|
||||
<div class="mt-3 flex items-center justify-end gap-1">
|
||||
<Button
|
||||
|
|
@ -236,72 +216,92 @@
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
class="hover:text-destructive-foreground h-7 w-7 text-destructive hover:bg-destructive"
|
||||
onclick={onDelete}
|
||||
onclick={() => (showDeleteDialog = true)}
|
||||
aria-label="Delete"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Expandable Details + Actions Row (when tools available) -->
|
||||
{#if tools.length > 0}
|
||||
<Collapsible.Root bind:open={isExpanded}>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Collapsible.Trigger
|
||||
class="flex flex-1 items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
<span>{toolsCount} tools available · Show details</span>
|
||||
</Collapsible.Trigger>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
onclick={startEditing}
|
||||
aria-label="Edit"
|
||||
{#if tools.length > 0}
|
||||
<Collapsible.Root bind:open={isExpanded}>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Collapsible.Trigger
|
||||
class="flex flex-1 items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Pencil class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
onclick={handleHealthCheck}
|
||||
disabled={isHealthChecking}
|
||||
aria-label="Refresh"
|
||||
>
|
||||
<RefreshCw class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="hover:text-destructive-foreground h-7 w-7 text-destructive hover:bg-destructive"
|
||||
onclick={onDelete}
|
||||
aria-label="Delete"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
<span>{toolsCount} tools available · Show details</span>
|
||||
</Collapsible.Trigger>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
onclick={startEditing}
|
||||
aria-label="Edit"
|
||||
>
|
||||
<Pencil class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
onclick={handleHealthCheck}
|
||||
disabled={isHealthChecking}
|
||||
aria-label="Refresh"
|
||||
>
|
||||
<RefreshCw class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="hover:text-destructive-foreground h-7 w-7 text-destructive hover:bg-destructive"
|
||||
onclick={() => (showDeleteDialog = true)}
|
||||
aria-label="Delete"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Collapsible.Content class="mt-2">
|
||||
<div class="max-h-64 space-y-3 overflow-y-auto">
|
||||
{#each tools as tool (tool.name)}
|
||||
<div>
|
||||
<Badge variant="secondary">{tool.name}</Badge>
|
||||
{#if tool.description}
|
||||
<p class="mt-1 text-xs text-muted-foreground">{tool.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
<Collapsible.Content class="mt-2">
|
||||
<div class="max-h-64 space-y-3 overflow-y-auto">
|
||||
{#each tools as tool (tool.name)}
|
||||
<div>
|
||||
<Badge variant="secondary">{tool.name}</Badge>
|
||||
{#if tool.description}
|
||||
<p class="mt-1 text-xs text-muted-foreground">{tool.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
{/if}
|
||||
{/if}
|
||||
</Card.Root>
|
||||
|
||||
<AlertDialog.Root bind:open={showDeleteDialog}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Delete Server</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Are you sure you want to delete <strong>{displayName}</strong>? This action cannot be
|
||||
undone.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
class="text-destructive-foreground bg-destructive hover:bg-destructive/90"
|
||||
onclick={onDelete}
|
||||
>
|
||||
Delete
|
||||
</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { Plus, X, Check } from '@lucide/svelte';
|
||||
import { Plus, X } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { parseMcpServerSettings } from '$lib/config/mcp';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||
import type { SettingsConfigType } from '$lib/types/settings';
|
||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||
import McpServerCard from './McpServerCard.svelte';
|
||||
import McpServerCard from '$lib/components/app/mcp/McpServerCard.svelte';
|
||||
import McpServerForm from './McpServerForm.svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -121,9 +121,9 @@
|
|||
<!-- Add New Server Form -->
|
||||
{#if isAddingServer}
|
||||
<Card.Root class="bg-muted/30 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<p class="font-medium">Add New Server</p>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="font-medium">Add New Server</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
|
@ -133,26 +133,29 @@
|
|||
>
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<McpServerForm
|
||||
url={newServerUrl}
|
||||
headers={newServerHeaders}
|
||||
onUrlChange={(v) => (newServerUrl = v)}
|
||||
onHeadersChange={(v) => (newServerHeaders = v)}
|
||||
urlError={newServerUrl ? newServerUrlError : null}
|
||||
id="new-server"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-700 dark:text-green-500 dark:hover:bg-green-950"
|
||||
variant="default"
|
||||
size="sm"
|
||||
onclick={saveNewServer}
|
||||
disabled={!!newServerUrlError}
|
||||
aria-label="Save"
|
||||
>
|
||||
<Check class="h-3.5 w-3.5" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<McpServerForm
|
||||
url={newServerUrl}
|
||||
headers={newServerHeaders}
|
||||
onUrlChange={(v) => (newServerUrl = v)}
|
||||
onHeadersChange={(v) => (newServerHeaders = v)}
|
||||
urlError={newServerUrl ? newServerUrlError : null}
|
||||
id="new-server"
|
||||
/>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
|
||||
Loading…
Reference in New Issue