| import Fuse from 'fuse.js' |
| import { basename } from 'path' |
| import type { SuggestionItem } from 'src/components/PromptInput/PromptInputFooterSuggestions.js' |
| import { generateFileSuggestions } from 'src/hooks/fileSuggestions.js' |
| import type { ServerResource } from 'src/services/mcp/types.js' |
| import { getAgentColor } from 'src/tools/AgentTool/agentColorManager.js' |
| import type { AgentDefinition } from 'src/tools/AgentTool/loadAgentsDir.js' |
| import { truncateToWidth } from 'src/utils/format.js' |
| import { logError } from 'src/utils/log.js' |
| import type { Theme } from 'src/utils/theme.js' |
|
|
| type FileSuggestionSource = { |
| type: 'file' |
| displayText: string |
| description?: string |
| path: string |
| filename: string |
| score?: number |
| } |
|
|
| type McpResourceSuggestionSource = { |
| type: 'mcp_resource' |
| displayText: string |
| description: string |
| server: string |
| uri: string |
| name: string |
| } |
|
|
| type AgentSuggestionSource = { |
| type: 'agent' |
| displayText: string |
| description: string |
| agentType: string |
| color?: keyof Theme |
| } |
|
|
| type SuggestionSource = |
| | FileSuggestionSource |
| | McpResourceSuggestionSource |
| | AgentSuggestionSource |
|
|
| |
| |
| |
| function createSuggestionFromSource(source: SuggestionSource): SuggestionItem { |
| switch (source.type) { |
| case 'file': |
| return { |
| id: `file-${source.path}`, |
| displayText: source.displayText, |
| description: source.description, |
| } |
| case 'mcp_resource': |
| return { |
| id: `mcp-resource-${source.server}__${source.uri}`, |
| displayText: source.displayText, |
| description: source.description, |
| } |
| case 'agent': |
| return { |
| id: `agent-${source.agentType}`, |
| displayText: source.displayText, |
| description: source.description, |
| color: source.color, |
| } |
| } |
| } |
|
|
| const MAX_UNIFIED_SUGGESTIONS = 15 |
| const DESCRIPTION_MAX_LENGTH = 60 |
|
|
| function truncateDescription(description: string): string { |
| return truncateToWidth(description, DESCRIPTION_MAX_LENGTH) |
| } |
|
|
| function generateAgentSuggestions( |
| agents: AgentDefinition[], |
| query: string, |
| showOnEmpty = false, |
| ): AgentSuggestionSource[] { |
| if (!query && !showOnEmpty) { |
| return [] |
| } |
|
|
| try { |
| const agentSources: AgentSuggestionSource[] = agents.map(agent => ({ |
| type: 'agent' as const, |
| displayText: `${agent.agentType} (agent)`, |
| description: truncateDescription(agent.whenToUse), |
| agentType: agent.agentType, |
| color: getAgentColor(agent.agentType), |
| })) |
|
|
| if (!query) { |
| return agentSources |
| } |
|
|
| const queryLower = query.toLowerCase() |
| return agentSources.filter( |
| agent => |
| agent.agentType.toLowerCase().includes(queryLower) || |
| agent.displayText.toLowerCase().includes(queryLower), |
| ) |
| } catch (error) { |
| logError(error as Error) |
| return [] |
| } |
| } |
|
|
| export async function generateUnifiedSuggestions( |
| query: string, |
| mcpResources: Record<string, ServerResource[]>, |
| agents: AgentDefinition[], |
| showOnEmpty = false, |
| ): Promise<SuggestionItem[]> { |
| if (!query && !showOnEmpty) { |
| return [] |
| } |
|
|
| const [fileSuggestions, agentSources] = await Promise.all([ |
| generateFileSuggestions(query, showOnEmpty), |
| Promise.resolve(generateAgentSuggestions(agents, query, showOnEmpty)), |
| ]) |
|
|
| const fileSources: FileSuggestionSource[] = fileSuggestions.map( |
| suggestion => ({ |
| type: 'file' as const, |
| displayText: suggestion.displayText, |
| description: suggestion.description, |
| path: suggestion.displayText, |
| filename: basename(suggestion.displayText), |
| score: (suggestion.metadata as { score?: number } | undefined)?.score, |
| }), |
| ) |
|
|
| const mcpSources: McpResourceSuggestionSource[] = Object.values(mcpResources) |
| .flat() |
| .map(resource => ({ |
| type: 'mcp_resource' as const, |
| displayText: `${resource.server}:${resource.uri}`, |
| description: truncateDescription( |
| resource.description || resource.name || resource.uri, |
| ), |
| server: resource.server, |
| uri: resource.uri, |
| name: resource.name || resource.uri, |
| })) |
|
|
| if (!query) { |
| const allSources = [...fileSources, ...mcpSources, ...agentSources] |
| return allSources |
| .slice(0, MAX_UNIFIED_SUGGESTIONS) |
| .map(createSuggestionFromSource) |
| } |
|
|
| const nonFileSources: SuggestionSource[] = [...mcpSources, ...agentSources] |
|
|
| |
| |
| type ScoredSource = { source: SuggestionSource; score: number } |
| const scoredResults: ScoredSource[] = [] |
|
|
| |
| for (const fileSource of fileSources) { |
| scoredResults.push({ |
| source: fileSource, |
| score: fileSource.score ?? 0.5, |
| }) |
| } |
|
|
| |
| if (nonFileSources.length > 0) { |
| const fuse = new Fuse(nonFileSources, { |
| includeScore: true, |
| threshold: 0.6, |
| keys: [ |
| { name: 'displayText', weight: 2 }, |
| { name: 'name', weight: 3 }, |
| { name: 'server', weight: 1 }, |
| { name: 'description', weight: 1 }, |
| { name: 'agentType', weight: 3 }, |
| ], |
| }) |
|
|
| const fuseResults = fuse.search(query, { limit: MAX_UNIFIED_SUGGESTIONS }) |
| for (const result of fuseResults) { |
| scoredResults.push({ |
| source: result.item, |
| score: result.score ?? 0.5, |
| }) |
| } |
| } |
|
|
| |
| scoredResults.sort((a, b) => a.score - b.score) |
|
|
| return scoredResults |
| .slice(0, MAX_UNIFIED_SUGGESTIONS) |
| .map(r => r.source) |
| .map(createSuggestionFromSource) |
| } |
|
|