Download src/utils/suggestions/commandSuggestions.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/suggestions/commandSuggestions.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/suggestions/commandSuggestions.ts
-
curl -L -o commandSuggestions.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/suggestions/commandSuggestions.ts
18.6 kB
| import Fuse from 'fuse.js' | |
| import { | |
| type Command, | |
| formatDescriptionWithSource, | |
| getCommand, | |
| getCommandName, | |
| } from '../../commands.js' | |
| import type { SuggestionItem } from '../../components/PromptInput/PromptInputFooterSuggestions.js' | |
| import { getSkillUsageScore } from './skillUsageTracking.js' | |
| // Treat these characters as word separators for command search | |
| const SEPARATORS = /[:_-]/g | |
| type CommandSearchItem = { | |
| descriptionKey: string[] | |
| partKey: string[] | undefined | |
| commandName: string | |
| command: Command | |
| aliasKey: string[] | undefined | |
| } | |
| // Cache the Fuse index keyed by the commands array identity. The commands | |
| // array is stable (memoized in REPL.tsx), so we only rebuild when it changes | |
| // rather than on every keystroke. | |
| let fuseCache: { | |
| commands: Command[] | |
| fuse: Fuse<CommandSearchItem> | |
| } | null = null | |
| function getCommandFuse(commands: Command[]): Fuse<CommandSearchItem> { | |
| if (fuseCache?.commands === commands) { | |
| return fuseCache.fuse | |
| } | |
| const commandData: CommandSearchItem[] = commands | |
| .filter(cmd => !cmd.isHidden) | |
| .map(cmd => { | |
| const commandName = getCommandName(cmd) | |
| const parts = commandName.split(SEPARATORS).filter(Boolean) | |
| return { | |
| descriptionKey: (cmd.description ?? '') | |
| .split(' ') | |
| .map(word => cleanWord(word)) | |
| .filter(Boolean), | |
| partKey: parts.length > 1 ? parts : undefined, | |
| commandName, | |
| command: cmd, | |
| aliasKey: cmd.aliases, | |
| } | |
| }) | |
| const fuse = new Fuse(commandData, { | |
| includeScore: true, | |
| threshold: 0.3, // relatively strict matching | |
| location: 0, // prefer matches at the beginning of strings | |
| distance: 100, // increased to allow matching in descriptions | |
| keys: [ | |
| { | |
| name: 'commandName', | |
| weight: 3, // Highest priority for command names | |
| }, | |
| { | |
| name: 'partKey', | |
| weight: 2, // Next highest priority for command parts | |
| }, | |
| { | |
| name: 'aliasKey', | |
| weight: 2, // Same high priority for aliases | |
| }, | |
| { | |
| name: 'descriptionKey', | |
| weight: 0.5, // Lower priority for descriptions | |
| }, | |
| ], | |
| }) | |
| fuseCache = { commands, fuse } | |
| return fuse | |
| } | |
| /** | |
| * Type guard to check if a suggestion's metadata is a Command. | |
| * Commands have a name string and a type property. | |
| */ | |
| function isCommandMetadata(metadata: unknown): metadata is Command { | |
| return ( | |
| typeof metadata === 'object' && | |
| metadata !== null && | |
| 'name' in metadata && | |
| typeof (metadata as { name: unknown }).name === 'string' && | |
| 'type' in metadata | |
| ) | |
| } | |
| /** | |
| * Represents a slash command found mid-input (not at the start) | |
| */ | |
| export type MidInputSlashCommand = { | |
| token: string // e.g., "/com" | |
| startPos: number // Position of "/" | |
| partialCommand: string // e.g., "com" | |
| } | |
| /** | |
| * Finds a slash command token that appears mid-input (not at position 0). | |
| * A mid-input slash command is a "/" preceded by whitespace, where the cursor | |
| * is at or after the "/". | |
| * | |
| * @param input The full input string | |
| * @param cursorOffset The current cursor position | |
| * @returns The mid-input slash command info, or null if not found | |
| */ | |
| export function findMidInputSlashCommand( | |
| input: string, | |
| cursorOffset: number, | |
| ): MidInputSlashCommand | null { | |
| // If input starts with "/", this is start-of-input case (handled elsewhere) | |
| if (input.startsWith('/')) { | |
| return null | |
| } | |
| // Look backwards from cursor to find a "/" preceded by whitespace | |
| const beforeCursor = input.slice(0, cursorOffset) | |
| // Find the last "/" in the text before cursor | |
| // Pattern: whitespace followed by "/" then optional alphanumeric/dash characters. | |
| // Lookbehind (?<=\s) is avoided — it defeats YARR JIT in JSC, and the | |
| // interpreter scans O(n) even with the $ anchor. Capture the whitespace | |
| // instead and offset match.index by 1. | |
| const match = beforeCursor.match(/\s\/([a-zA-Z0-9_:-]*)$/) | |
| if (!match || match.index === undefined) { | |
| return null | |
| } | |
| // Get the full token (may extend past cursor) | |
| const slashPos = match.index + 1 | |
| const textAfterSlash = input.slice(slashPos + 1) | |
| // Extract the command portion (until whitespace or end) | |
| const commandMatch = textAfterSlash.match(/^[a-zA-Z0-9_:-]*/) | |
| const fullCommand = commandMatch ? commandMatch[0] : '' | |
| // If cursor is past the command (after a space), don't show ghost text | |
| if (cursorOffset > slashPos + 1 + fullCommand.length) { | |
| return null | |
| } | |
| return { | |
| token: '/' + fullCommand, | |
| startPos: slashPos, | |
| partialCommand: fullCommand, | |
| } | |
| } | |
| /** | |
| * Finds the best matching command for a partial command string. | |
| * Delegates to generateCommandSuggestions and filters to prefix matches. | |
| * | |
| * @param partialCommand The partial command typed by the user (without "/") | |
| * @param commands Available commands | |
| * @returns The completion suffix (e.g., "mit" for partial "com" matching "commit"), or null | |
| */ | |
| export function getBestCommandMatch( | |
| partialCommand: string, | |
| commands: Command[], | |
| ): { suffix: string; fullCommand: string } | null { | |
| if (!partialCommand) { | |
| return null | |
| } | |
| // Use existing suggestion logic | |
| const suggestions = generateCommandSuggestions('/' + partialCommand, commands) | |
| if (suggestions.length === 0) { | |
| return null | |
| } | |
| // Find first suggestion that is a prefix match (for inline completion) | |
| const query = partialCommand.toLowerCase() | |
| for (const suggestion of suggestions) { | |
| if (!isCommandMetadata(suggestion.metadata)) { | |
| continue | |
| } | |
| const name = getCommandName(suggestion.metadata) | |
| if (name.toLowerCase().startsWith(query)) { | |
| const suffix = name.slice(partialCommand.length) | |
| // Only return if there's something to complete | |
| if (suffix) { | |
| return { suffix, fullCommand: name } | |
| } | |
| } | |
| } | |
| return null | |
| } | |
| /** | |
| * Checks if input is a command (starts with slash) | |
| */ | |
| export function isCommandInput(input: string): boolean { | |
| return input.startsWith('/') | |
| } | |
| /** | |
| * Checks if a command input has arguments | |
| * A command with just a trailing space is considered to have no arguments | |
| */ | |
| export function hasCommandArgs(input: string): boolean { | |
| if (!isCommandInput(input)) return false | |
| if (!input.includes(' ')) return false | |
| if (input.endsWith(' ')) return false | |
| return true | |
| } | |
| /** | |
| * Formats a command with proper notation | |
| */ | |
| export function formatCommand(command: string): string { | |
| return `/${command} ` | |
| } | |
| /** | |
| * Generates a deterministic unique ID for a command suggestion. | |
| * Commands with the same name from different sources get unique IDs. | |
| * | |
| * Only prompt commands can have duplicates (from user settings, project | |
| * settings, plugins, etc). Built-in commands (local, local-jsx) are | |
| * defined once in code and can't have duplicates. | |
| */ | |
| function getCommandId(cmd: Command): string { | |
| const commandName = getCommandName(cmd) | |
| if (cmd.type === 'prompt') { | |
| // For plugin commands, include the repository to disambiguate | |
| if (cmd.source === 'plugin' && cmd.pluginInfo?.repository) { | |
| return `${commandName}:${cmd.source}:${cmd.pluginInfo.repository}` | |
| } | |
| return `${commandName}:${cmd.source}` | |
| } | |
| // Built-in commands include type as fallback for future-proofing | |
| return `${commandName}:${cmd.type}` | |
| } | |
| /** | |
| * Checks if a query matches any of the command's aliases. | |
| * Returns the matched alias if found, otherwise undefined. | |
| */ | |
| function findMatchedAlias( | |
| query: string, | |
| aliases?: string[], | |
| ): string | undefined { | |
| if (!aliases || aliases.length === 0 || query === '') { | |
| return undefined | |
| } | |
| // Check if query is a prefix of any alias (case-insensitive) | |
| return aliases.find(alias => alias.toLowerCase().startsWith(query)) | |
| } | |
| /** | |
| * Creates a suggestion item from a command. | |
| * Only shows the matched alias in parentheses if the user typed an alias. | |
| */ | |
| function createCommandSuggestionItem( | |
| cmd: Command, | |
| matchedAlias?: string, | |
| ): SuggestionItem { | |
| const commandName = getCommandName(cmd) | |
| // Only show the alias if the user typed it | |
| const aliasText = matchedAlias ? ` (${matchedAlias})` : '' | |
| const isWorkflow = cmd.type === 'prompt' && cmd.kind === 'workflow' | |
| const fullDescription = | |
| (isWorkflow ? cmd.description : formatDescriptionWithSource(cmd)) + | |
| (cmd.type === 'prompt' && cmd.argNames?.length | |
| ? ` (arguments: ${cmd.argNames.join(', ')})` | |
| : '') | |
| return { | |
| id: getCommandId(cmd), | |
| displayText: `/${commandName}${aliasText}`, | |
| tag: isWorkflow ? 'workflow' : undefined, | |
| description: fullDescription, | |
| metadata: cmd, | |
| } | |
| } | |
| /** | |
| * Generate command suggestions based on input | |
| */ | |
| export function generateCommandSuggestions( | |
| input: string, | |
| commands: Command[], | |
| ): SuggestionItem[] { | |
| // Only process command input | |
| if (!isCommandInput(input)) { | |
| return [] | |
| } | |
| // If there are arguments, don't show suggestions | |
| if (hasCommandArgs(input)) { | |
| return [] | |
| } | |
| const query = input.slice(1).toLowerCase().trim() | |
| // When just typing '/' without additional text | |
| if (query === '') { | |
| const visibleCommands = commands.filter(cmd => !cmd.isHidden) | |
| // Find recently used skills (only prompt commands have usage tracking) | |
| const recentlyUsed: Command[] = [] | |
| const commandsWithScores = visibleCommands | |
| .filter(cmd => cmd.type === 'prompt') | |
| .map(cmd => ({ | |
| cmd, | |
| score: getSkillUsageScore(getCommandName(cmd)), | |
| })) | |
| .filter(item => item.score > 0) | |
| .sort((a, b) => b.score - a.score) | |
| // Take top 5 recently used skills | |
| for (const item of commandsWithScores.slice(0, 5)) { | |
| recentlyUsed.push(item.cmd) | |
| } | |
| // Create a set of recently used command IDs to avoid duplicates | |
| const recentlyUsedIds = new Set(recentlyUsed.map(cmd => getCommandId(cmd))) | |
| // Categorize remaining commands (excluding recently used) | |
| const builtinCommands: Command[] = [] | |
| const userCommands: Command[] = [] | |
| const projectCommands: Command[] = [] | |
| const policyCommands: Command[] = [] | |
| const otherCommands: Command[] = [] | |
| visibleCommands.forEach(cmd => { | |
| // Skip if already in recently used | |
| if (recentlyUsedIds.has(getCommandId(cmd))) { | |
| return | |
| } | |
| if (cmd.type === 'local' || cmd.type === 'local-jsx') { | |
| builtinCommands.push(cmd) | |
| } else if ( | |
| cmd.type === 'prompt' && | |
| (cmd.source === 'userSettings' || cmd.source === 'localSettings') | |
| ) { | |
| userCommands.push(cmd) | |
| } else if (cmd.type === 'prompt' && cmd.source === 'projectSettings') { | |
| projectCommands.push(cmd) | |
| } else if (cmd.type === 'prompt' && cmd.source === 'policySettings') { | |
| policyCommands.push(cmd) | |
| } else { | |
| otherCommands.push(cmd) | |
| } | |
| }) | |
| // Sort each category alphabetically | |
| const sortAlphabetically = (a: Command, b: Command) => | |
| getCommandName(a).localeCompare(getCommandName(b)) | |
| builtinCommands.sort(sortAlphabetically) | |
| userCommands.sort(sortAlphabetically) | |
| projectCommands.sort(sortAlphabetically) | |
| policyCommands.sort(sortAlphabetically) | |
| otherCommands.sort(sortAlphabetically) | |
| // Combine with built-in commands prioritized after recently used, | |
| // so they remain visible even when many skills are installed | |
| return [ | |
| ...recentlyUsed, | |
| ...builtinCommands, | |
| ...userCommands, | |
| ...projectCommands, | |
| ...policyCommands, | |
| ...otherCommands, | |
| ].map(cmd => createCommandSuggestionItem(cmd)) | |
| } | |
| // The Fuse index filters isHidden at build time and is keyed on the | |
| // (memoized) commands array identity, so a command that is hidden when Fuse | |
| // first builds stays invisible to Fuse for the whole session. If the user | |
| // types the exact name of a currently-hidden command, prepend it to the | |
| // Fuse results so exact-name always wins over weak description fuzzy | |
| // matches — but only when no visible command shares the name (that would | |
| // be the user's explicit override and should win). Prepend rather than | |
| // early-return so visible prefix siblings (e.g. /voice-memo) still appear | |
| // below, and getBestCommandMatch can still find a non-empty suffix. | |
| let hiddenExact = commands.find( | |
| cmd => cmd.isHidden && getCommandName(cmd).toLowerCase() === query, | |
| ) | |
| if ( | |
| hiddenExact && | |
| commands.some( | |
| cmd => !cmd.isHidden && getCommandName(cmd).toLowerCase() === query, | |
| ) | |
| ) { | |
| hiddenExact = undefined | |
| } | |
| const fuse = getCommandFuse(commands) | |
| const searchResults = fuse.search(query) | |
| // Sort results prioritizing exact/prefix command name matches over fuzzy description matches | |
| // Priority order: | |
| // 1. Exact name match (highest) | |
| // 2. Exact alias match | |
| // 3. Prefix name match | |
| // 4. Prefix alias match | |
| // 5. Fuzzy match (lowest) | |
| // Precompute per-item values once to avoid O(n log n) recomputation in comparator | |
| const withMeta = searchResults.map(r => { | |
| const name = r.item.commandName.toLowerCase() | |
| const aliases = r.item.aliasKey?.map(alias => alias.toLowerCase()) ?? [] | |
| const usage = | |
| r.item.command.type === 'prompt' | |
| ? getSkillUsageScore(getCommandName(r.item.command)) | |
| : 0 | |
| return { r, name, aliases, usage } | |
| }) | |
| const sortedResults = withMeta.sort((a, b) => { | |
| const aName = a.name | |
| const bName = b.name | |
| const aAliases = a.aliases | |
| const bAliases = b.aliases | |
| // Check for exact name match (highest priority) | |
| const aExactName = aName === query | |
| const bExactName = bName === query | |
| if (aExactName && !bExactName) return -1 | |
| if (bExactName && !aExactName) return 1 | |
| // Check for exact alias match | |
| const aExactAlias = aAliases.some(alias => alias === query) | |
| const bExactAlias = bAliases.some(alias => alias === query) | |
| if (aExactAlias && !bExactAlias) return -1 | |
| if (bExactAlias && !aExactAlias) return 1 | |
| // Check for prefix name match | |
| const aPrefixName = aName.startsWith(query) | |
| const bPrefixName = bName.startsWith(query) | |
| if (aPrefixName && !bPrefixName) return -1 | |
| if (bPrefixName && !aPrefixName) return 1 | |
| // Among prefix name matches, prefer the shorter name (closer to exact) | |
| if (aPrefixName && bPrefixName && aName.length !== bName.length) { | |
| return aName.length - bName.length | |
| } | |
| // Check for prefix alias match | |
| const aPrefixAlias = aAliases.find(alias => alias.startsWith(query)) | |
| const bPrefixAlias = bAliases.find(alias => alias.startsWith(query)) | |
| if (aPrefixAlias && !bPrefixAlias) return -1 | |
| if (bPrefixAlias && !aPrefixAlias) return 1 | |
| // Among prefix alias matches, prefer the shorter alias | |
| if ( | |
| aPrefixAlias && | |
| bPrefixAlias && | |
| aPrefixAlias.length !== bPrefixAlias.length | |
| ) { | |
| return aPrefixAlias.length - bPrefixAlias.length | |
| } | |
| // For similar match types, use Fuse score with usage as tiebreaker | |
| const scoreDiff = (a.r.score ?? 0) - (b.r.score ?? 0) | |
| if (Math.abs(scoreDiff) > 0.1) { | |
| return scoreDiff | |
| } | |
| // For similar Fuse scores, prefer more frequently used skills | |
| return b.usage - a.usage | |
| }) | |
| // Map search results to suggestion items | |
| // Note: We intentionally don't deduplicate here because commands with the same name | |
| // from different sources (e.g., projectSettings vs userSettings) may have different | |
| // implementations and should both be available to the user | |
| const fuseSuggestions = sortedResults.map(result => { | |
| const cmd = result.r.item.command | |
| // Only show alias in parentheses if the user typed an alias | |
| const matchedAlias = findMatchedAlias(query, cmd.aliases) | |
| return createCommandSuggestionItem(cmd, matchedAlias) | |
| }) | |
| // Skip the prepend if hiddenExact is already in fuseSuggestions — this | |
| // happens when isHidden flips false→true mid-session (OAuth expiry, | |
| // GrowthBook kill-switch) and the stale Fuse index still holds the | |
| // command. Fuse already sorts exact-name matches first, so no reorder | |
| // is needed; we just don't want a duplicate id (duplicate React keys, | |
| // both rows rendering as selected). | |
| if (hiddenExact) { | |
| const hiddenId = getCommandId(hiddenExact) | |
| if (!fuseSuggestions.some(s => s.id === hiddenId)) { | |
| return [createCommandSuggestionItem(hiddenExact), ...fuseSuggestions] | |
| } | |
| } | |
| return fuseSuggestions | |
| } | |
| /** | |
| * Apply selected command to input | |
| */ | |
| export function applyCommandSuggestion( | |
| suggestion: string | SuggestionItem, | |
| shouldExecute: boolean, | |
| commands: Command[], | |
| onInputChange: (value: string) => void, | |
| setCursorOffset: (offset: number) => void, | |
| onSubmit: (value: string, isSubmittingSlashCommand?: boolean) => void, | |
| ): void { | |
| // Extract command name and object from string or SuggestionItem metadata | |
| let commandName: string | |
| let commandObj: Command | undefined | |
| if (typeof suggestion === 'string') { | |
| commandName = suggestion | |
| commandObj = shouldExecute ? getCommand(commandName, commands) : undefined | |
| } else { | |
| if (!isCommandMetadata(suggestion.metadata)) { | |
| return // Invalid suggestion, nothing to apply | |
| } | |
| commandName = getCommandName(suggestion.metadata) | |
| commandObj = suggestion.metadata | |
| } | |
| // Format the command input with trailing space | |
| const newInput = formatCommand(commandName) | |
| onInputChange(newInput) | |
| setCursorOffset(newInput.length) | |
| // Execute command if requested and it takes no arguments | |
| if (shouldExecute && commandObj) { | |
| if ( | |
| commandObj.type !== 'prompt' || | |
| (commandObj.argNames ?? []).length === 0 | |
| ) { | |
| onSubmit(newInput, /* isSubmittingSlashCommand */ true) | |
| } | |
| } | |
| } | |
| // Helper function at bottom of file per CLAUDE.md | |
| function cleanWord(word: string) { | |
| return word.toLowerCase().replace(/[^a-z0-9]/g, '') | |
| } | |
| /** | |
| * Find all /command patterns in text for highlighting. | |
| * Returns array of {start, end} positions. | |
| * Requires whitespace or start-of-string before the slash to avoid | |
| * matching paths like /usr/bin. | |
| */ | |
| export function findSlashCommandPositions( | |
| text: string, | |
| ): Array<{ start: number; end: number }> { | |
| const positions: Array<{ start: number; end: number }> = [] | |
| // Match /command patterns preceded by whitespace or start-of-string | |
| const regex = /(^|[\s])(\/[a-zA-Z][a-zA-Z0-9:\-_]*)/g | |
| let match: RegExpExecArray | null = null | |
| while ((match = regex.exec(text)) !== null) { | |
| const precedingChar = match[1] ?? '' | |
| const commandName = match[2] ?? '' | |
| // Start position is after the whitespace (if any) | |
| const start = match.index + precedingChar.length | |
| positions.push({ start, end: start + commandName.length }) | |
| } | |
| return positions | |
| } | |