Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
19 kB · 567 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568import 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 searchconst 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.mdfunction 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}