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();
71 const initialized = await mcpStore.ensureInitialized(perChatOverrides);
79 await mcpStore.fetchAllResources();
80 resources = mcpResourceStore.getAllResourceInfos();
82 console.error('[ChatFormResourcePicker] Failed to load resources:', error);
89 function handleResourceClick(resource: MCPResourceInfo) {
90 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;
147 return orderA - orderB;
150 const query = (searchQuery || internalSearchQuery).toLowerCase();
151 if (!query) return sortedResources;
153 return sortedResources.filter(
155 resource.name.toLowerCase().includes(query) ||
156 resource.title?.toLowerCase().includes(query) ||
157 resource.description?.toLowerCase().includes(query) ||
158 resource.uri.toLowerCase().includes(query)
162 let showSearchInput = $derived(resources.length > 3);
165 <ChatFormPickerPopover
168 srLabel="Open resource picker"
170 onKeydown={handleKeydown}
173 items={filteredResources}
176 bind:searchQuery={internalSearchQuery}
178 searchPlaceholder="Search resources..."
179 emptyMessage="No MCP resources available"
180 itemKey={(resource) => resource.serverName + ':' + resource.uri}
182 {#snippet item(resource, index, isSelected)}
183 {@const server = serverSettingsMap.get(resource.serverName)}
184 {@const serverLabel = server ? mcpStore.getServerLabel(server) : resource.serverName}
186 <ChatFormPickerListItem
189 onClick={() => handleResourceClick(resource)}
191 <ChatFormPickerItemHeader
194 title={resource.title || resource.name}
195 description={resource.description}
197 {#snippet titleExtra()}
198 {#if isResourceAttached(resource.uri)}
200 class="inline-flex items-center rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
207 {#snippet subtitle()}
208 <p class="mt-0.5 truncate text-xs text-muted-foreground/60">
212 </ChatFormPickerItemHeader>
213 </ChatFormPickerListItem>
216 {#snippet skeleton()}
217 <ChatFormPickerListItemSkeleton />
221 {#if onBrowse && resources.length > 3}
223 class="fixed right-3 bottom-3"
229 <FolderOpen class="h-3 w-3" />
235 </ChatFormPickerList>
236 </ChatFormPickerPopover>