2 import { conversationsStore } from '$lib/stores/conversations.svelte';
3 import { mcpStore } from '$lib/stores/mcp.svelte';
4 import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
5 import { KeyboardKey } from '$lib/enums';
6 import type { MCPResourceInfo, MCPServerSettingsEntry } from '$lib/types';
7 import { SvelteMap } from 'svelte/reactivity';
8 import { FolderOpen } from '@lucide/svelte';
9 import { Button } from '$lib/components/ui/button';
11 ChatFormPickerPopover,
13 ChatFormPickerListItem,
14 ChatFormPickerItemHeader,
15 ChatFormPickerListItemSkeleton
16 } from '$lib/components/app/chat';
23 onResourceSelect?: (resource: MCPResourceInfo) => void;
24 onBrowse?: () => void;
28 class: className = '',
36 let resources = $state<MCPResourceInfo[]>([]);
37 let isLoading = $state(false);
38 let selectedIndex = $state(0);
39 let internalSearchQuery = $state('');
41 let serverSettingsMap = $derived.by(() => {
42 const servers = mcpStore.getServers();
43 const map = new SvelteMap<string, MCPServerSettingsEntry>();
45 for (const server of servers) {
46 map.set(server.id, server);
60 if (filteredResources.length > 0 && selectedIndex >= filteredResources.length) {
65 async function loadResources() {
69 const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
70 const initialized = await mcpStore.ensureInitialized(perChatOverrides);
78 await mcpStore.fetchAllResources();
79 resources = mcpResourceStore.getAllResourceInfos();
81 console.error('[ChatFormPickerMcpResources] Failed to load resources:', error);
88 function handleResourceClick(resource: MCPResourceInfo) {
89 mcpStore.attachResource(resource.uri);
91 onResourceSelect?.(resource);
95 function isResourceAttached(uri: string): boolean {
96 return mcpResourceStore.isAttached(uri);
99 export function handleKeydown(event: KeyboardEvent): boolean {
100 if (!isOpen) return false;
102 if (event.key === KeyboardKey.ESCAPE) {
103 event.preventDefault();
109 if (event.key === KeyboardKey.ARROW_DOWN) {
110 event.preventDefault();
112 if (filteredResources.length > 0) {
113 selectedIndex = (selectedIndex + 1) % filteredResources.length;
119 if (event.key === KeyboardKey.ARROW_UP) {
120 event.preventDefault();
121 if (filteredResources.length > 0) {
122 selectedIndex = selectedIndex === 0 ? filteredResources.length - 1 : selectedIndex - 1;
128 if (event.key === KeyboardKey.ENTER) {
129 event.preventDefault();
130 if (filteredResources[selectedIndex]) {
131 handleResourceClick(filteredResources[selectedIndex]);
140 let filteredResources = $derived.by(() => {
141 const sortedServers = mcpStore.getServersSorted();
142 const serverOrderMap = new Map(sortedServers.map((server, index) => [server.id, index]));
144 const sortedResources = [...resources].sort((a, b) => {
145 const orderA = serverOrderMap.get(a.serverName) ?? Number.MAX_SAFE_INTEGER;
146 const orderB = serverOrderMap.get(b.serverName) ?? Number.MAX_SAFE_INTEGER;
148 return orderA - orderB;
151 const query = (searchQuery || internalSearchQuery).toLowerCase();
152 if (!query) return sortedResources;
154 return sortedResources.filter(
156 resource.name.toLowerCase().includes(query) ||
157 resource.title?.toLowerCase().includes(query) ||
158 resource.description?.toLowerCase().includes(query) ||
159 resource.uri.toLowerCase().includes(query)
163 let showSearchInput = $derived(resources.length > 3);
166 <ChatFormPickerPopover
169 srLabel="Open resource picker"
171 onKeydown={handleKeydown}
174 items={filteredResources}
177 bind:searchQuery={internalSearchQuery}
179 searchPlaceholder="Search resources..."
180 emptyMessage="No MCP resources available"
181 itemKey={(resource) => resource.serverName + ':' + resource.uri}
183 {#snippet item(resource, index, isSelected)}
184 {@const server = serverSettingsMap.get(resource.serverName)}
185 {@const serverLabel = server ? mcpStore.getServerLabel(server) : resource.serverName}
187 <ChatFormPickerListItem
190 onclick={() => handleResourceClick(resource)}
192 <ChatFormPickerItemHeader
195 title={resource.title || resource.name}
196 description={resource.description}
198 {#snippet titleExtra()}
199 {#if isResourceAttached(resource.uri)}
201 class="inline-flex items-center rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
208 {#snippet subtitle()}
209 <p class="mt-0.5 truncate text-xs text-muted-foreground/60">
213 </ChatFormPickerItemHeader>
214 </ChatFormPickerListItem>
217 {#snippet skeleton()}
218 <ChatFormPickerListItemSkeleton />
222 {#if onBrowse && resources.length > 3}
224 class="fixed right-3 bottom-3"
230 <FolderOpen class="h-3 w-3" />
236 </ChatFormPickerList>
237 </ChatFormPickerPopover>