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) {
|
function handleResourceSelect(resource: MCPResourceInfo, shiftKey: boolean = false) {
|
||||||
if (shiftKey && lastSelectedUri) {
|
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 lastIndex = allResources.findIndex((r) => r.uri === lastSelectedUri);
|
||||||
const currentIndex = allResources.findIndex((r) => r.uri === resource.uri);
|
const currentIndex = allResources.findIndex((r) => r.uri === resource.uri);
|
||||||
|
|
||||||
|
|
@ -87,7 +88,20 @@
|
||||||
lastSelectedUri = resource.uri;
|
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 allResources: MCPResourceInfo[] = [];
|
||||||
const resourcesMap = mcpResources();
|
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() {
|
async function handleAttach() {
|
||||||
|
|
@ -129,27 +153,11 @@
|
||||||
isAttaching = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root {open} onOpenChange={handleOpenChange}>
|
<Dialog.Root {open} onOpenChange={handleOpenChange}>
|
||||||
<Dialog.Content class="max-h-[80vh] !max-w-4xl overflow-hidden p-0">
|
<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">
|
<Dialog.Title class="flex items-center gap-2">
|
||||||
<FolderOpen class="h-5 w-5" />
|
<FolderOpen class="h-5 w-5" />
|
||||||
|
|
||||||
|
|
@ -165,18 +173,17 @@
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<div class="flex h-[500px]">
|
<div class="flex h-[500px] min-w-0">
|
||||||
<div class="w-72 shrink-0 overflow-y-auto border-r p-4">
|
<div class="w-72 shrink-0 overflow-y-auto border-r border-border/30 p-4">
|
||||||
<McpResourceBrowser
|
<McpResourceBrowser
|
||||||
onSelect={handleResourceSelect}
|
onSelect={handleResourceSelect}
|
||||||
onToggle={handleResourceToggle}
|
onToggle={handleResourceToggle}
|
||||||
onAttach={handleQuickAttach}
|
|
||||||
selectedUris={selectedResources}
|
selectedUris={selectedResources}
|
||||||
expandToUri={preSelectedUri}
|
expandToUri={preSelectedUri}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}
|
{#if selectedResources.size === 1}
|
||||||
{@const allResources = getAllResourcesFlat()}
|
{@const allResources = getAllResourcesFlat()}
|
||||||
{@const selectedResource = allResources.find((r) => selectedResources.has(r.uri))}
|
{@const selectedResource = allResources.find((r) => selectedResources.has(r.uri))}
|
||||||
|
|
@ -194,7 +201,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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 variant="outline" onclick={() => handleOpenChange(false)}>Cancel</Button>
|
||||||
|
|
||||||
<Button onclick={handleAttach} disabled={selectedResources.size === 0 || isAttaching}>
|
<Button onclick={handleAttach} disabled={selectedResources.size === 0 || isAttaching}>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@
|
||||||
let {
|
let {
|
||||||
onSelect,
|
onSelect,
|
||||||
onToggle,
|
onToggle,
|
||||||
onAttach,
|
|
||||||
selectedUris = new Set(),
|
selectedUris = new Set(),
|
||||||
expandToUri,
|
expandToUri,
|
||||||
class: className
|
class: className
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import { FolderOpen, ChevronDown, ChevronRight, Loader2 } from '@lucide/svelte';
|
import { FolderOpen, ChevronDown, ChevronRight, Loader2 } from '@lucide/svelte';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
import { cn } from '$lib/components/ui/utils';
|
import { cn } from '$lib/components/ui/utils';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
import type { MCPResourceInfo, MCPServerResources } from '$lib/types';
|
import type { MCPResourceInfo, MCPServerResources } from '$lib/types';
|
||||||
|
|
@ -26,7 +25,7 @@
|
||||||
onToggleFolder: (folderId: string) => void;
|
onToggleFolder: (folderId: string) => void;
|
||||||
onSelect?: (resource: MCPResourceInfo, shiftKey?: boolean) => void;
|
onSelect?: (resource: MCPResourceInfo, shiftKey?: boolean) => void;
|
||||||
onToggle?: (resource: MCPResourceInfo, checked: boolean) => void;
|
onToggle?: (resource: MCPResourceInfo, checked: boolean) => void;
|
||||||
onAttach?: (resource: MCPResourceInfo) => void;
|
searchQuery?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -55,11 +54,6 @@
|
||||||
onToggle?.(resource, checked);
|
onToggle?.(resource, checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachClick(e: Event, resource: MCPResourceInfo) {
|
|
||||||
e.stopPropagation();
|
|
||||||
onAttach?.(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isResourceSelected(resource: MCPResourceInfo): boolean {
|
function isResourceSelected(resource: MCPResourceInfo): boolean {
|
||||||
return selectedUris.has(resource.uri);
|
return selectedUris.has(resource.uri);
|
||||||
}
|
}
|
||||||
|
|
@ -81,10 +75,14 @@
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronRight class="h-3 w-3" />
|
<ChevronRight class="h-3 w-3" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<FolderOpen class="h-3.5 w-3.5 text-muted-foreground" />
|
<FolderOpen class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
|
||||||
<span class="font-medium">{node.name}</span>
|
<span class="font-medium">{node.name}</span>
|
||||||
|
|
||||||
<span class="text-xs text-muted-foreground">({folderCount})</span>
|
<span class="text-xs text-muted-foreground">({folderCount})</span>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
|
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<div class="ml-4 flex flex-col gap-0.5 border-l border-border/50 pl-2">
|
<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}`)}
|
{#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 ResourceIcon = getResourceIcon(resource)}
|
||||||
{@const isSelected = isResourceSelected(resource)}
|
{@const isSelected = isResourceSelected(resource)}
|
||||||
{@const resourceDisplayName = resource.title || getDisplayName(node.name)}
|
{@const resourceDisplayName = resource.title || getDisplayName(node.name)}
|
||||||
|
|
||||||
<div class="group flex w-full items-center gap-2">
|
<div class="group flex w-full items-center gap-2">
|
||||||
{#if onToggle}
|
{#if onToggle}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
@ -107,6 +106,7 @@
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class={cn(
|
class={cn(
|
||||||
'flex flex-1 items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors',
|
'flex flex-1 items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors',
|
||||||
|
|
@ -117,19 +117,10 @@
|
||||||
title={resourceDisplayName}
|
title={resourceDisplayName}
|
||||||
>
|
>
|
||||||
<ResourceIcon class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
<ResourceIcon class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
|
||||||
<span class="min-w-0 flex-1 truncate text-left">
|
<span class="min-w-0 flex-1 truncate text-left">
|
||||||
{resourceDisplayName}
|
{resourceDisplayName}
|
||||||
</span>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -144,6 +135,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronRight class="h-3.5 w-3.5" />
|
<ChevronRight class="h-3.5 w-3.5" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if favicon}
|
{#if favicon}
|
||||||
<img
|
<img
|
||||||
src={favicon}
|
src={favicon}
|
||||||
|
|
@ -154,10 +146,13 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span class="font-medium">{displayName}</span>
|
<span class="font-medium">{displayName}</span>
|
||||||
|
|
||||||
<span class="text-xs text-muted-foreground">
|
<span class="text-xs text-muted-foreground">
|
||||||
({serverRes.resources.length})
|
({serverRes.resources.length})
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{#if serverRes.loading}
|
{#if serverRes.loading}
|
||||||
<Loader2 class="ml-auto h-3 w-3 animate-spin text-muted-foreground" />
|
<Loader2 class="ml-auto h-3 w-3 animate-spin text-muted-foreground" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue