unifiedSuggestions.ts
hooks/unifiedSuggestions.ts
No strong subsystem tag
203
Lines
5837
Bytes
1
Exports
10
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 203 lines, 10 detected imports, and 1 detected exports.
Important relationships
Detected exports
generateUnifiedSuggestions
Keywords
displaytextsourcedescriptionscoreagentnamequeryresourceagenttypefuse
Detected imports
fuse.jspathsrc/components/PromptInput/PromptInputFooterSuggestions.jssrc/hooks/fileSuggestions.jssrc/services/mcp/types.jssrc/tools/AgentTool/agentColorManager.jssrc/tools/AgentTool/loadAgentsDir.jssrc/utils/format.jssrc/utils/log.jssrc/utils/theme.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
/**
* Creates a unified suggestion item from a source
*/
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, // Use displayText as path for files
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]
// Score non-file sources with Fuse.js
// File sources are already scored by Rust/nucleo
type ScoredSource = { source: SuggestionSource; score: number }
const scoredResults: ScoredSource[] = []
// Add file sources with their nucleo scores (already 0-1, lower is better)
for (const fileSource of fileSources) {
scoredResults.push({
source: fileSource,
score: fileSource.score ?? 0.5, // Default to middle score if missing
})
}
// Score non-file sources with Fuse.js and add them
if (nonFileSources.length > 0) {
const fuse = new Fuse(nonFileSources, {
includeScore: true,
threshold: 0.6, // Allow more matches through, we'll sort by score
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,
})
}
}
// Sort all results by score (lower is better) and return top results
scoredResults.sort((a, b) => a.score - b.score)
return scoredResults
.slice(0, MAX_UNIFIED_SUGGESTIONS)
.map(r => r.source)
.map(createSuggestionFromSource)
}