fix: MCP Resources Browser selection & attaching
This commit is contained in:
parent
5508ad0bee
commit
1d28b6b1be
|
|
@ -58,7 +58,8 @@
|
|||
|
||||
function handleResourceSelect(resource: MCPResourceInfo, shiftKey: boolean = false) {
|
||||
if (shiftKey && lastSelectedUri) {
|
||||
const allResources = getAllResourcesFlat();
|
||||
// Get all resources in tree order (matching the display order in McpResourceBrowser)
|
||||
const allResources = getAllResourcesFlatInTreeOrder();
|
||||
const lastIndex = allResources.findIndex((r) => r.uri === lastSelectedUri);
|
||||
const currentIndex = allResources.findIndex((r) => r.uri === resource.uri);
|
||||
|
||||
|
|
@ -87,7 +88,20 @@
|
|||
lastSelectedUri = resource.uri;
|
||||
}
|
||||
|
||||
function getAllResourcesFlat(): MCPResourceInfo[] {
|
||||
function getResourceDisplayName(resource: MCPResourceInfo): string {
|
||||
// Extract the display name from the resource URI (last path segment)
|
||||
try {
|
||||
const uriWithoutProtocol = resource.uri.replace(/^[a-z]+:\/\//, '');
|
||||
const parts = uriWithoutProtocol.split('/');
|
||||
return parts[parts.length - 1] || resource.name || resource.uri;
|
||||
} catch {
|
||||
return resource.name || resource.uri;
|
||||
}
|
||||
}
|
||||
|
||||
function getAllResourcesFlatInTreeOrder(): MCPResourceInfo[] {
|
||||
// Get resources in the same order as displayed in McpResourceBrowser tree
|
||||
// Sort by: folders first (if applicable), then alphabetically by display name
|
||||
const allResources: MCPResourceInfo[] = [];
|
||||
const resourcesMap = mcpResources();
|
||||
|
||||
|
|
@ -97,7 +111,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
return allResources;
|
||||
// Sort to match tree display order: folders first, then alphabetically
|
||||
return allResources.sort((a, b) => {
|
||||
const aName = getResourceDisplayName(a);
|
||||
const bName = getResourceDisplayName(b);
|
||||
return aName.localeCompare(bName);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllResourcesFlat(): MCPResourceInfo[] {
|
||||
// Fallback for other uses (like attaching)
|
||||
return getAllResourcesFlatInTreeOrder();
|
||||
}
|
||||
|
||||
async function handleAttach() {
|
||||
|
|
@ -129,27 +153,11 @@
|
|||
isAttaching = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQuickAttach(resource: MCPResourceInfo) {
|
||||
isAttaching = true;
|
||||
|
||||
try {
|
||||
await mcpStore.attachResource(resource.uri);
|
||||
|
||||
onAttach?.(resource);
|
||||
|
||||
toast.success(`Resource attached: ${resource.name}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to attach resource:', error);
|
||||
} finally {
|
||||
isAttaching = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={handleOpenChange}>
|
||||
<Dialog.Content class="max-h-[80vh] !max-w-4xl overflow-hidden p-0">
|
||||
<Dialog.Header class="border-b px-6 py-4">
|
||||
<Dialog.Header class="border-b border-border/30 px-6 py-4">
|
||||
<Dialog.Title class="flex items-center gap-2">
|
||||
<FolderOpen class="h-5 w-5" />
|
||||
|
||||
|
|
@ -165,18 +173,17 @@
|
|||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex h-[500px]">
|
||||
<div class="w-72 shrink-0 overflow-y-auto border-r p-4">
|
||||
<div class="flex h-[500px] min-w-0">
|
||||
<div class="w-72 shrink-0 overflow-y-auto border-r border-border/30 p-4">
|
||||
<McpResourceBrowser
|
||||
onSelect={handleResourceSelect}
|
||||
onToggle={handleResourceToggle}
|
||||
onAttach={handleQuickAttach}
|
||||
selectedUris={selectedResources}
|
||||
expandToUri={preSelectedUri}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="min-w-0 flex-1 overflow-auto p-4">
|
||||
{#if selectedResources.size === 1}
|
||||
{@const allResources = getAllResourcesFlat()}
|
||||
{@const selectedResource = allResources.find((r) => selectedResources.has(r.uri))}
|
||||
|
|
@ -194,7 +201,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="border-t px-6 py-4">
|
||||
<Dialog.Footer class="border-t border-border/30 px-6 py-4">
|
||||
<Button variant="outline" onclick={() => handleOpenChange(false)}>Cancel</Button>
|
||||
|
||||
<Button onclick={handleAttach} disabled={selectedResources.size === 0 || isAttaching}>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
let {
|
||||
onSelect,
|
||||
onToggle,
|
||||
onAttach,
|
||||
selectedUris = new Set(),
|
||||
expandToUri,
|
||||
class: className
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { FolderOpen, ChevronDown, ChevronRight, Loader2 } from '@lucide/svelte';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import type { MCPResourceInfo, MCPServerResources } from '$lib/types';
|
||||
|
|
@ -26,7 +25,7 @@
|
|||
onToggleFolder: (folderId: string) => void;
|
||||
onSelect?: (resource: MCPResourceInfo, shiftKey?: boolean) => void;
|
||||
onToggle?: (resource: MCPResourceInfo, checked: boolean) => void;
|
||||
onAttach?: (resource: MCPResourceInfo) => void;
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -55,11 +54,6 @@
|
|||
onToggle?.(resource, checked);
|
||||
}
|
||||
|
||||
function handleAttachClick(e: Event, resource: MCPResourceInfo) {
|
||||
e.stopPropagation();
|
||||
onAttach?.(resource);
|
||||
}
|
||||
|
||||
function isResourceSelected(resource: MCPResourceInfo): boolean {
|
||||
return selectedUris.has(resource.uri);
|
||||
}
|
||||
|
|
@ -81,10 +75,14 @@
|
|||
{:else}
|
||||
<ChevronRight class="h-3 w-3" />
|
||||
{/if}
|
||||
|
||||
<FolderOpen class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
|
||||
<span class="font-medium">{node.name}</span>
|
||||
|
||||
<span class="text-xs text-muted-foreground">({folderCount})</span>
|
||||
</Collapsible.Trigger>
|
||||
|
||||
<Collapsible.Content>
|
||||
<div class="ml-4 flex flex-col gap-0.5 border-l border-border/50 pl-2">
|
||||
{#each sortTreeChildren( [...node.children.values()] ) as child (child.resource?.uri || `${serverName}:${parentPath}/${node.name}/${child.name}`)}
|
||||
|
|
@ -98,6 +96,7 @@
|
|||
{@const ResourceIcon = getResourceIcon(resource)}
|
||||
{@const isSelected = isResourceSelected(resource)}
|
||||
{@const resourceDisplayName = resource.title || getDisplayName(node.name)}
|
||||
|
||||
<div class="group flex w-full items-center gap-2">
|
||||
{#if onToggle}
|
||||
<Checkbox
|
||||
|
|
@ -107,6 +106,7 @@
|
|||
class="h-4 w-4"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class={cn(
|
||||
'flex flex-1 items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors',
|
||||
|
|
@ -117,19 +117,10 @@
|
|||
title={resourceDisplayName}
|
||||
>
|
||||
<ResourceIcon class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
|
||||
<span class="min-w-0 flex-1 truncate text-left">
|
||||
{resourceDisplayName}
|
||||
</span>
|
||||
{#if onAttach}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-5 px-1.5 text-xs opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100"
|
||||
onclick={(e: MouseEvent) => handleAttachClick(e, resource)}
|
||||
>
|
||||
Attach
|
||||
</Button>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -144,6 +135,7 @@
|
|||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
|
||||
{#if favicon}
|
||||
<img
|
||||
src={favicon}
|
||||
|
|
@ -154,10 +146,13 @@
|
|||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span class="font-medium">{displayName}</span>
|
||||
|
||||
<span class="text-xs text-muted-foreground">
|
||||
({serverRes.resources.length})
|
||||
</span>
|
||||
|
||||
{#if serverRes.loading}
|
||||
<Loader2 class="ml-auto h-3 w-3 animate-spin text-muted-foreground" />
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Reference in New Issue