UNPKG

pyb-ts

Version:

PYB-CLI - Minimal AI Agent with multi-model support and CLI interface

930 lines (929 loc) 35 kB
import { useState, useCallback, useEffect, useRef } from "react"; import { useInput } from "ink"; import { existsSync, statSync, readdirSync } from "fs"; import { join, dirname, basename, resolve } from "path"; import { getCwd } from "@utils/state"; import { getActiveAgents } from "@utils/agentLoader"; import { getModelManager } from "@utils/model"; import { matchCommands } from "@utils/fuzzyMatcher"; import { getCommonSystemCommands, getCommandPriority, getEssentialCommands, getMinimalFallbackCommands } from "@utils/commonUnixCommands"; const INITIAL_STATE = { suggestions: [], selectedIndex: 0, isActive: false, context: null, preview: null, emptyDirMessage: "", suppressUntil: 0 }; function useUnifiedCompletion({ input, cursorOffset, onInputChange, setCursorOffset, commands, onSubmit }) { const [state, setState] = useState(INITIAL_STATE); const updateState = useCallback((updates) => { setState((prev) => ({ ...prev, ...updates })); }, []); const resetCompletion = useCallback(() => { setState((prev) => ({ ...prev, suggestions: [], selectedIndex: 0, isActive: false, context: null, preview: null, emptyDirMessage: "" })); }, []); const activateCompletion = useCallback((suggestions2, context) => { setState((prev) => ({ ...prev, suggestions: suggestions2, // Keep the order from generateSuggestions (already sorted with weights) selectedIndex: 0, isActive: true, context, preview: null })); }, []); const { suggestions, selectedIndex, isActive, emptyDirMessage } = state; const findCommonPrefix = useCallback((suggestions2) => { if (suggestions2.length === 0) return ""; if (suggestions2.length === 1) return suggestions2[0].value; let prefix = suggestions2[0].value; for (let i = 1; i < suggestions2.length; i++) { const str = suggestions2[i].value; let j = 0; while (j < prefix.length && j < str.length && prefix[j] === str[j]) { j++; } prefix = prefix.slice(0, j); if (prefix.length === 0) return ""; } return prefix; }, []); const getWordAtCursor = useCallback(() => { if (!input) return null; let start = cursorOffset; while (start > 0) { const char = input[start - 1]; if (/\s/.test(char)) break; if (char === "@" && start < cursorOffset) { start--; break; } if (char === "/") { const collectedSoFar = input.slice(start, cursorOffset); if (collectedSoFar.includes("/") || collectedSoFar.includes(".")) { start--; continue; } if (start > 1) { const prevChar = input[start - 2]; if (prevChar === "." || prevChar === "~") { start--; continue; } } if (start === 1 || start > 1 && /\s/.test(input[start - 2])) { start--; break; } start--; continue; } if (char === "." && start > 0) { const nextChar = start < input.length ? input[start] : ""; if (nextChar === "/" || nextChar === ".") { start--; continue; } } start--; } const word = input.slice(start, cursorOffset); if (!word) return null; if (word.startsWith("/")) { const beforeWord = input.slice(0, start).trim(); const isCommand = beforeWord === "" && !word.includes("/", 1); return { type: isCommand ? "command" : "file", prefix: isCommand ? word.slice(1) : word, startPos: start, endPos: cursorOffset // Use cursor position as end }; } if (word.startsWith("@")) { const content = word.slice(1); if (word.includes("@", 1)) { return null; } return { type: "agent", // This will trigger mixed agent+file completion prefix: content, startPos: start, endPos: cursorOffset // Use cursor position as end }; } return { type: "file", prefix: word, startPos: start, endPos: cursorOffset // Use cursor position as end }; }, [input, cursorOffset]); const [systemCommands, setSystemCommands] = useState([]); const [isLoadingCommands, setIsLoadingCommands] = useState(false); const classifyCommand = useCallback((cmd) => { const lowerCmd = cmd.toLowerCase(); let score = 0; if (cmd.length <= 4) score += 40; else if (cmd.length <= 6) score += 20; else if (cmd.length <= 8) score += 10; else if (cmd.length > 15) score -= 30; if (/^[a-z]+$/.test(lowerCmd)) score += 30; if (/[A-Z]/.test(cmd)) score -= 15; if (/\d/.test(cmd)) score -= 20; if (cmd.includes(".")) score -= 25; if (cmd.includes("-")) score -= 10; if (cmd.includes("_")) score -= 15; const commonWords = ["list", "copy", "move", "find", "print", "show", "edit", "view"]; if (commonWords.some((word) => lowerCmd.includes(word.slice(0, 3)))) score += 25; const devPrefixes = ["git", "npm", "node", "py", "docker", "kubectl"]; if (devPrefixes.some((prefix) => lowerCmd.startsWith(prefix))) score += 15; const systemIndicators = ["daemon", "helper", "responder", "service", "d$", "ctl$"]; if (systemIndicators.some( (indicator) => indicator.endsWith("$") ? lowerCmd.endsWith(indicator.slice(0, -1)) : lowerCmd.includes(indicator) )) score -= 40; if (/\.(pl|py|sh|rb|js)$/.test(lowerCmd)) score -= 35; const buildToolPatterns = ["bindep", "render", "mako", "webpack", "babel", "eslint"]; if (buildToolPatterns.some((pattern) => lowerCmd.includes(pattern))) score -= 25; const vowelRatio = (lowerCmd.match(/[aeiou]/g) || []).length / lowerCmd.length; if (vowelRatio < 0.2) score += 15; if (vowelRatio > 0.5) score -= 10; if (score >= 50) return "core"; if (score >= 20) return "common"; if (score >= -10) return "dev"; return "system"; }, []); const loadSystemCommands = useCallback(async () => { if (systemCommands.length > 0 || isLoadingCommands) return; setIsLoadingCommands(true); try { const { readdirSync: readdirSync2, statSync: statSync2 } = await import("fs"); const pathDirs = (process.env.PATH || "").split(":").filter(Boolean); const commandSet = /* @__PURE__ */ new Set(); const essentialCommands = getEssentialCommands(); essentialCommands.forEach((cmd) => commandSet.add(cmd)); for (const dir of pathDirs) { try { if (readdirSync2 && statSync2) { const entries = readdirSync2(dir); for (const entry of entries) { try { const fullPath = `${dir}/${entry}`; const stats = statSync2(fullPath); if (stats.isFile() && (stats.mode & 73) !== 0) { commandSet.add(entry); } } catch { } } } } catch { } } const commands2 = Array.from(commandSet).sort(); setSystemCommands(commands2); } catch (error) { console.warn("Failed to load system commands, using fallback:", error); setSystemCommands(getMinimalFallbackCommands()); } finally { setIsLoadingCommands(false); } }, [systemCommands.length, isLoadingCommands]); useEffect(() => { loadSystemCommands(); }, [loadSystemCommands]); const generateCommandSuggestions = useCallback((prefix) => { const filteredCommands = commands.filter((cmd) => !cmd.isHidden); if (!prefix) { return filteredCommands.map((cmd) => ({ value: cmd.userFacingName(), displayValue: `/${cmd.userFacingName()}`, type: "command", score: 100 })); } return filteredCommands.filter((cmd) => { const names = [cmd.userFacingName(), ...cmd.aliases || []]; return names.some((name) => name.toLowerCase().startsWith(prefix.toLowerCase())); }).map((cmd) => ({ value: cmd.userFacingName(), displayValue: `/${cmd.userFacingName()}`, type: "command", score: 100 - prefix.length + (cmd.userFacingName().startsWith(prefix) ? 10 : 0) })); }, [commands]); const calculateUnixCommandScore = useCallback((cmd, prefix) => { const result = matchCommands([cmd], prefix); return result.length > 0 ? result[0].score : 0; }, []); const generateUnixCommandSuggestions = useCallback((prefix) => { if (!prefix) return []; if (isLoadingCommands) { return [{ value: "loading...", displayValue: `\u23F3 Loading system commands...`, type: "file", score: 0, metadata: { isLoading: true } }]; } const commonCommands = getCommonSystemCommands(systemCommands); const uniqueCommands = Array.from(new Set(commonCommands)); const matches = matchCommands(uniqueCommands, prefix); const boostedMatches = matches.map((match) => { const priority = getCommandPriority(match.command); return { ...match, score: match.score + priority * 0.5 // Add priority boost }; }).sort((a, b) => b.score - a.score); let results = boostedMatches.slice(0, 8); const perfectMatches = boostedMatches.filter((m) => m.score >= 900); if (perfectMatches.length > 0 && perfectMatches.length <= 3) { results = perfectMatches; } else if (boostedMatches.length > 8) { const goodMatches = boostedMatches.filter((m) => m.score >= 100); if (goodMatches.length <= 5) { results = goodMatches; } } return results.map((item) => ({ value: item.command, displayValue: `$ ${item.command}`, type: "command", score: item.score, metadata: { isUnixCommand: true } })); }, [systemCommands, isLoadingCommands]); const [agentSuggestions, setAgentSuggestions] = useState([]); const [modelSuggestions, setModelSuggestions] = useState([]); useEffect(() => { try { const modelManager = getModelManager(); const allModels = modelManager.getAllAvailableModelNames(); const suggestions2 = allModels.map((modelId) => { return { value: `ask-${modelId}`, displayValue: `\u{1F99C} ask-${modelId} :: Consult ${modelId} for expert opinion and specialized analysis`, type: "ask", score: 90, // Higher than agents - put ask-models on top metadata: { modelId } }; }); setModelSuggestions(suggestions2); } catch (error) { console.warn("[useUnifiedCompletion] Failed to load models:", error); setModelSuggestions([]); } }, []); useEffect(() => { getActiveAgents().then((agents) => { const suggestions2 = agents.map((config) => { let shortDesc = config.whenToUse; const prefixPatterns = [ /^Use this agent when you need (assistance with: )?/i, /^Use PROACTIVELY (when|to) /i, /^Specialized in /i, /^Implementation specialist for /i, /^Design validation specialist\.? Use PROACTIVELY to /i, /^Task validation specialist\.? Use PROACTIVELY to /i, /^Requirements validation specialist\.? Use PROACTIVELY to /i ]; for (const pattern of prefixPatterns) { shortDesc = shortDesc.replace(pattern, ""); } const findSmartBreak = (text, maxLength) => { if (text.length <= maxLength) return text; const sentenceEndings = /[.!。!]/; const firstSentenceMatch = text.search(sentenceEndings); if (firstSentenceMatch !== -1) { const firstSentence = text.slice(0, firstSentenceMatch).trim(); if (firstSentence.length >= 5) { return firstSentence; } } if (text.length > maxLength) { const commaEndings = /[,,]/; const commas = []; let match; const regex = new RegExp(commaEndings, "g"); while ((match = regex.exec(text)) !== null) { commas.push(match.index); } for (let i = commas.length - 1; i >= 0; i--) { const commaPos = commas[i]; if (commaPos < maxLength) { const clause = text.slice(0, commaPos).trim(); if (clause.length >= 5) { return clause; } } } } return text.slice(0, maxLength) + "..."; }; shortDesc = findSmartBreak(shortDesc.trim(), 80); if (!shortDesc || shortDesc.length < 5) { shortDesc = findSmartBreak(config.whenToUse, 80); } return { value: `run-agent-${config.agentType}`, displayValue: `\u{1F464} run-agent-${config.agentType} :: ${shortDesc}`, // 人类图标 + run-agent前缀 + 简洁描述 type: "agent", score: 85, // Lower than ask-models metadata: config }; }); setAgentSuggestions(suggestions2); }).catch((error) => { console.warn("[useUnifiedCompletion] Failed to load agents:", error); setAgentSuggestions([]); }); }, []); const generateMentionSuggestions = useCallback((prefix) => { const allSuggestions = [...agentSuggestions, ...modelSuggestions]; if (!prefix) { return allSuggestions.sort((a, b) => { if (a.type === "ask" && b.type === "agent") return -1; if (a.type === "agent" && b.type === "ask") return 1; return b.score - a.score; }); } const candidates = allSuggestions.map((s) => s.value); const matches = matchCommands(candidates, prefix); const fuzzyResults = matches.map((match) => { const suggestion = allSuggestions.find((s) => s.value === match.command); return { ...suggestion, score: match.score // Use fuzzy match score instead of simple scoring }; }).sort((a, b) => { if (a.type === "ask" && b.type === "agent") return -1; if (a.type === "agent" && b.type === "ask") return 1; return b.score - a.score; }); return fuzzyResults; }, [agentSuggestions, modelSuggestions]); const generateFileSuggestions = useCallback((prefix, isAtReference = false) => { try { const cwd = getCwd(); const userPath = prefix || "."; const isAbsolutePath = userPath.startsWith("/"); const isHomePath = userPath.startsWith("~"); let searchPath; if (isHomePath) { searchPath = userPath.replace("~", process.env.HOME || ""); } else if (isAbsolutePath) { searchPath = userPath; } else { searchPath = resolve(cwd, userPath); } const endsWithSlash = userPath.endsWith("/"); const searchStat = existsSync(searchPath) ? statSync(searchPath) : null; let searchDir; let nameFilter; if (endsWithSlash || searchStat?.isDirectory()) { searchDir = searchPath; nameFilter = ""; } else { searchDir = dirname(searchPath); nameFilter = basename(searchPath); } if (!existsSync(searchDir)) return []; const showHidden = nameFilter.startsWith(".") || userPath.includes("/."); const entries = readdirSync(searchDir).filter((entry) => { if (!showHidden && entry.startsWith(".")) return false; if (nameFilter && !entry.toLowerCase().startsWith(nameFilter.toLowerCase())) return false; return true; }).sort((a, b) => { const aPath = join(searchDir, a); const bPath = join(searchDir, b); const aIsDir = statSync(aPath).isDirectory(); const bIsDir = statSync(bPath).isDirectory(); if (aIsDir && !bIsDir) return -1; if (!aIsDir && bIsDir) return 1; return a.toLowerCase().localeCompare(b.toLowerCase()); }).slice(0, 25); return entries.map((entry) => { const entryPath = join(searchDir, entry); const isDir = statSync(entryPath).isDirectory(); const icon = isDir ? "\u{1F4C1}" : "\u{1F4C4}"; let value; if (userPath.includes("/")) { if (endsWithSlash) { value = userPath + entry + (isDir ? "/" : ""); } else if (searchStat?.isDirectory()) { value = userPath + "/" + entry + (isDir ? "/" : ""); } else { const userDir = userPath.includes("/") ? userPath.substring(0, userPath.lastIndexOf("/")) : ""; value = userDir ? userDir + "/" + entry + (isDir ? "/" : "") : entry + (isDir ? "/" : ""); } } else { if (searchStat?.isDirectory()) { value = userPath + "/" + entry + (isDir ? "/" : ""); } else { value = entry + (isDir ? "/" : ""); } } return { value, displayValue: `${icon} ${entry}${isDir ? "/" : ""}`, type: "file", score: isDir ? 80 : 70 }; }); } catch { return []; } }, []); const calculateMatchScore = useCallback((suggestion, prefix) => { const lowerPrefix = prefix.toLowerCase(); const value = suggestion.value.toLowerCase(); const displayValue = suggestion.displayValue.toLowerCase(); let matchFound = false; let score = 0; if (value.startsWith(lowerPrefix)) { matchFound = true; score = 100; } else if (value.includes(lowerPrefix)) { matchFound = true; score = 95; } else if (displayValue.includes(lowerPrefix)) { matchFound = true; score = 90; } else { const words = value.split(/[-_]/); if (words.some((word) => word.startsWith(lowerPrefix))) { matchFound = true; score = 93; } else { const acronym = words.map((word) => word[0]).join(""); if (acronym.startsWith(lowerPrefix)) { matchFound = true; score = 88; } } } if (!matchFound) return 0; if (suggestion.type === "ask") score += 2; if (suggestion.type === "agent") score += 1; return score; }, []); const generateSmartMentionSuggestions = useCallback((prefix, sourceContext = "file") => { if (!prefix || prefix.length < 2) return []; const allSuggestions = [...agentSuggestions, ...modelSuggestions]; return allSuggestions.map((suggestion) => { const matchScore = calculateMatchScore(suggestion, prefix); if (matchScore === 0) return null; return { ...suggestion, score: matchScore, isSmartMatch: true, originalContext: sourceContext, // Only modify display for clarity, keep value clean displayValue: `\u{1F3AF} ${suggestion.displayValue}` }; }).filter(Boolean).sort((a, b) => b.score - a.score).slice(0, 5); }, [agentSuggestions, modelSuggestions, calculateMatchScore]); const generateSuggestions = useCallback((context) => { switch (context.type) { case "command": return generateCommandSuggestions(context.prefix); case "agent": { const mentionSuggestions = generateMentionSuggestions(context.prefix); const fileSuggestions = generateFileSuggestions(context.prefix, true); const weightedSuggestions = [ ...mentionSuggestions.map((s) => ({ ...s, // In @ context, agents/models get high priority weightedScore: s.score + 150 })), ...fileSuggestions.map((s) => ({ ...s, // Files get lower priority but still visible weightedScore: s.score + 10 // Small boost to ensure visibility })) ]; return weightedSuggestions.sort((a, b) => b.weightedScore - a.weightedScore).map(({ weightedScore, ...suggestion }) => suggestion); } case "file": { const fileSuggestions = generateFileSuggestions(context.prefix, false); const unixSuggestions = generateUnixCommandSuggestions(context.prefix); const mentionMatches = generateMentionSuggestions(context.prefix).map((s) => ({ ...s, isSmartMatch: true, // Show that @ will be added when selected displayValue: `\u2192 ${s.displayValue}` // Arrow to indicate it will transform })); const weightedSuggestions = [ ...unixSuggestions.map((s) => ({ ...s, // Unix commands get boost, but exact matches get huge boost sourceWeight: s.score >= 1e4 ? 5e3 : 200, // Exact match gets massive boost weightedScore: s.score >= 1e4 ? s.score + 5e3 : s.score + 200 })), ...mentionMatches.map((s) => ({ ...s, // Agents/models get medium priority boost (but less to avoid overriding exact Unix) sourceWeight: 50, weightedScore: s.score + 50 })), ...fileSuggestions.map((s) => ({ ...s, // Files get no boost (baseline) sourceWeight: 0, weightedScore: s.score })) ]; const seen = /* @__PURE__ */ new Set(); const deduplicatedResults = weightedSuggestions.sort((a, b) => b.weightedScore - a.weightedScore).filter((item) => { if (seen.has(item.value)) return false; seen.add(item.value); return true; }).map(({ weightedScore, sourceWeight, ...suggestion }) => suggestion); return deduplicatedResults; } default: return []; } }, [generateCommandSuggestions, generateMentionSuggestions, generateFileSuggestions, generateUnixCommandSuggestions, generateSmartMentionSuggestions]); const completeWith = useCallback((suggestion, context) => { let completion; if (context.type === "command") { completion = `/${suggestion.value} `; } else if (context.type === "agent") { if (suggestion.type === "agent") { completion = `@${suggestion.value} `; } else if (suggestion.type === "ask") { completion = `@${suggestion.value} `; } else { const isDirectory = suggestion.value.endsWith("/"); completion = `@${suggestion.value}${isDirectory ? "" : " "}`; } } else { if (suggestion.isSmartMatch) { completion = `@${suggestion.value} `; } else { const isDirectory = suggestion.value.endsWith("/"); completion = suggestion.value + (isDirectory ? "" : " "); } } let actualEndPos; if (context.type === "file" && suggestion.value.startsWith("/") && !suggestion.isSmartMatch) { let end = context.startPos; while (end < input.length && input[end] !== " " && input[end] !== "\n") { end++; } actualEndPos = end; } else { const currentWord = input.slice(context.startPos); const nextSpaceIndex = currentWord.indexOf(" "); actualEndPos = nextSpaceIndex === -1 ? input.length : context.startPos + nextSpaceIndex; } const newInput = input.slice(0, context.startPos) + completion + input.slice(actualEndPos); onInputChange(newInput); setCursorOffset(context.startPos + completion.length); }, [input, onInputChange, setCursorOffset, onSubmit, commands]); const partialComplete = useCallback((prefix, context) => { const completion = context.type === "command" ? `/${prefix}` : context.type === "agent" ? `@${prefix}` : prefix; const newInput = input.slice(0, context.startPos) + completion + input.slice(context.endPos); onInputChange(newInput); setCursorOffset(context.startPos + completion.length); }, [input, onInputChange, setCursorOffset]); useInput((input_str, key) => { if (!key.tab) return false; if (key.shift) return false; const context = getWordAtCursor(); if (!context) return false; if (state.isActive && state.suggestions.length > 0) { const nextIndex = (state.selectedIndex + 1) % state.suggestions.length; const nextSuggestion = state.suggestions[nextIndex]; if (state.context) { const currentWord = input.slice(state.context.startPos); const wordEnd = currentWord.search(/\s/); const actualEndPos = wordEnd === -1 ? input.length : state.context.startPos + wordEnd; let preview; if (state.context.type === "command") { preview = `/${nextSuggestion.value}`; } else if (state.context.type === "agent") { preview = `@${nextSuggestion.value}`; } else if (nextSuggestion.isSmartMatch) { preview = `@${nextSuggestion.value}`; } else { preview = nextSuggestion.value; } const newInput = input.slice(0, state.context.startPos) + preview + input.slice(actualEndPos); onInputChange(newInput); setCursorOffset(state.context.startPos + preview.length); updateState({ selectedIndex: nextIndex, preview: { isActive: true, originalInput: input, wordRange: [state.context.startPos, state.context.startPos + preview.length] } }); } return true; } const currentSuggestions = generateSuggestions(context); if (currentSuggestions.length === 0) { return false; } else if (currentSuggestions.length === 1) { completeWith(currentSuggestions[0], context); return true; } else { activateCompletion(currentSuggestions, context); const firstSuggestion = currentSuggestions[0]; const currentWord = input.slice(context.startPos); const wordEnd = currentWord.search(/\s/); const actualEndPos = wordEnd === -1 ? input.length : context.startPos + wordEnd; let preview; if (context.type === "command") { preview = `/${firstSuggestion.value}`; } else if (context.type === "agent") { preview = `@${firstSuggestion.value}`; } else if (firstSuggestion.isSmartMatch) { preview = `@${firstSuggestion.value}`; } else { preview = firstSuggestion.value; } const newInput = input.slice(0, context.startPos) + preview + input.slice(actualEndPos); onInputChange(newInput); setCursorOffset(context.startPos + preview.length); updateState({ preview: { isActive: true, originalInput: input, wordRange: [context.startPos, context.startPos + preview.length] } }); return true; } }); useInput((inputChar, key) => { if (key.return && state.isActive && state.suggestions.length > 0) { const selectedSuggestion = state.suggestions[state.selectedIndex]; if (selectedSuggestion && state.context) { let completion; if (state.context.type === "command") { completion = `/${selectedSuggestion.value} `; } else if (state.context.type === "agent") { if (selectedSuggestion.type === "agent") { completion = `@${selectedSuggestion.value} `; } else if (selectedSuggestion.type === "ask") { completion = `@${selectedSuggestion.value} `; } else { completion = `@${selectedSuggestion.value} `; } } else if (selectedSuggestion.isSmartMatch) { completion = `@${selectedSuggestion.value} `; } else { completion = selectedSuggestion.value + " "; } const currentWord = input.slice(state.context.startPos); const nextSpaceIndex = currentWord.indexOf(" "); const actualEndPos = nextSpaceIndex === -1 ? input.length : state.context.startPos + nextSpaceIndex; const newInput = input.slice(0, state.context.startPos) + completion + input.slice(actualEndPos); onInputChange(newInput); setCursorOffset(state.context.startPos + completion.length); } resetCompletion(); return true; } if (!state.isActive || state.suggestions.length === 0) return false; const handleNavigation = (newIndex) => { const preview = state.suggestions[newIndex].value; if (state.preview?.isActive && state.context) { const newInput = input.slice(0, state.context.startPos) + preview + input.slice(state.preview.wordRange[1]); onInputChange(newInput); setCursorOffset(state.context.startPos + preview.length); updateState({ selectedIndex: newIndex, preview: { ...state.preview, wordRange: [state.context.startPos, state.context.startPos + preview.length] } }); } else { updateState({ selectedIndex: newIndex }); } }; if (key.downArrow) { const nextIndex = (state.selectedIndex + 1) % state.suggestions.length; handleNavigation(nextIndex); return true; } if (key.upArrow) { const nextIndex = state.selectedIndex === 0 ? state.suggestions.length - 1 : state.selectedIndex - 1; handleNavigation(nextIndex); return true; } if (inputChar === " " && state.isActive && state.suggestions.length > 0) { const selectedSuggestion = state.suggestions[state.selectedIndex]; const isDirectory = selectedSuggestion.value.endsWith("/"); if (!state.context) return false; const currentWordAtContext = input.slice( state.context.startPos, state.context.startPos + selectedSuggestion.value.length ); if (currentWordAtContext !== selectedSuggestion.value) { completeWith(selectedSuggestion, state.context); } resetCompletion(); if (isDirectory) { setTimeout(() => { const newContext = { ...state.context, prefix: selectedSuggestion.value, endPos: state.context.startPos + selectedSuggestion.value.length }; const newSuggestions = generateSuggestions(newContext); if (newSuggestions.length > 0) { activateCompletion(newSuggestions, newContext); } else { updateState({ emptyDirMessage: `Directory is empty: ${selectedSuggestion.value}` }); setTimeout(() => updateState({ emptyDirMessage: "" }), 3e3); } }, 50); } return true; } if (key.rightArrow) { const selectedSuggestion = state.suggestions[state.selectedIndex]; const isDirectory = selectedSuggestion.value.endsWith("/"); if (!state.context) return false; const currentWordAtContext = input.slice( state.context.startPos, state.context.startPos + selectedSuggestion.value.length ); if (currentWordAtContext !== selectedSuggestion.value) { completeWith(selectedSuggestion, state.context); } resetCompletion(); if (isDirectory) { setTimeout(() => { const newContext = { ...state.context, prefix: selectedSuggestion.value, endPos: state.context.startPos + selectedSuggestion.value.length }; const newSuggestions = generateSuggestions(newContext); if (newSuggestions.length > 0) { activateCompletion(newSuggestions, newContext); } else { updateState({ emptyDirMessage: `Directory is empty: ${selectedSuggestion.value}` }); setTimeout(() => updateState({ emptyDirMessage: "" }), 3e3); } }, 50); } return true; } if (key.escape) { if (state.preview?.isActive && state.context) { onInputChange(state.preview.originalInput); setCursorOffset(state.context.startPos + state.context.prefix.length); } resetCompletion(); return true; } return false; }); useInput((input_str, key) => { if (key.backspace || key.delete) { if (state.isActive) { resetCompletion(); const suppressionTime = input.length > 10 ? 200 : 100; updateState({ suppressUntil: Date.now() + suppressionTime }); return true; } } return false; }); const lastInputRef = useRef(""); useEffect(() => { if (lastInputRef.current === input) return; const inputLengthChange = Math.abs(input.length - lastInputRef.current.length); const isHistoryNavigation = (inputLengthChange > 10 || // Large content change inputLengthChange > 5 && !input.includes(lastInputRef.current.slice(-5))) && input !== lastInputRef.current; lastInputRef.current = input; if (state.preview?.isActive || Date.now() < state.suppressUntil) { return; } if (isHistoryNavigation && state.isActive) { resetCompletion(); return; } const context = getWordAtCursor(); if (context && shouldAutoTrigger(context)) { const newSuggestions = generateSuggestions(context); if (newSuggestions.length === 0) { resetCompletion(); } else if (newSuggestions.length === 1 && shouldAutoHideSingleMatch(newSuggestions[0], context)) { resetCompletion(); } else { activateCompletion(newSuggestions, context); } } else if (state.context) { const contextChanged = !context || state.context.type !== context.type || state.context.startPos !== context.startPos || !context.prefix.startsWith(state.context.prefix); if (contextChanged) { resetCompletion(); } } }, [input, cursorOffset]); const shouldAutoTrigger = useCallback((context) => { switch (context.type) { case "command": return true; case "agent": return true; case "file": const prefix = context.prefix; if (prefix.startsWith("./") || prefix.startsWith("../") || prefix.startsWith("/") || prefix.startsWith("~") || prefix.includes("/")) { return true; } if (prefix.startsWith(".") && prefix.length >= 2) { return true; } return false; default: return false; } }, []); const shouldAutoHideSingleMatch = useCallback((suggestion, context) => { const currentInput = input.slice(context.startPos, context.endPos); if (context.type === "file") { if (suggestion.value.endsWith("/")) { return false; } if (currentInput === suggestion.value) { return true; } if (currentInput.endsWith("/" + suggestion.value) || currentInput.endsWith(suggestion.value)) { return true; } return false; } if (context.type === "command") { const fullCommand = `/${suggestion.value}`; const matches = currentInput === fullCommand; return matches; } if (context.type === "agent") { const fullAgent = `@${suggestion.value}`; const matches = currentInput === fullAgent; return matches; } return false; }, [input]); return { suggestions, selectedIndex, isActive, emptyDirMessage }; } export { useUnifiedCompletion }; //# sourceMappingURL=useUnifiedCompletion.js.map