- Complete architectural overhaul of useUnifiedCompletion hook - Unified state management: 8 separate states → single CompletionState interface - Simplified core logic: getWordAtCursor 194 lines → 42 lines (78% reduction) - Fixed infinite React update loops with ref-based input tracking - Smart triggering mechanism replacing aggressive auto-completion - Integrated @agent and @file mention system with system reminders - Added comprehensive agent loading and mention processing - Enhanced Tab/Arrow/Enter key handling with clean event management - Maintained 100% functional compatibility across all completion types Key improvements: • File path completion (relative, absolute, ~expansion, @references) • Slash command completion (/help, /model, etc.) • Agent completion (@agent-xxx with intelligent descriptions) • System command completion (PATH scanning with fallback) • Terminal-style Tab cycling, Enter confirmation, Escape cancellation • Preview mode with boundary calculation • History navigation compatibility • Empty directory handling with user feedback Architecture: Event-driven @mention detection → system reminder injection → LLM tool usage Performance: Eliminated 7-layer nested conditionals, reduced state synchronization issues Reliability: Fixed maximum update depth exceeded warnings, stable state management
8.4 KiB
8.4 KiB
输入框补全系统完整分析
一、补全系统架构概览
核心组件
- useUnifiedCompletion Hook: 统一补全系统的核心
- 三种补全类型: 文件路径、系统命令、slash命令
- 触发机制: Tab键手动触发 + 特殊字符自动触发
- Terminal行为模式: 模仿终端Tab补全行为
二、补全算法详细分析
1. 文件路径补全
触发条件 (getWordAtCursor)
// Line 108-125: 处理以/结尾的输入
if (lastChar === '/') {
if (input === '/') {
// 单独/视为slash命令
return { type: 'command', prefix: '', startPos: 0, endPos: 1 }
}
// 其他所有/情况都是文件路径 (src/, ./, ../, /usr/)
return { type: 'file', prefix: fullPath, startPos: pathStart, endPos: input.length }
}
// Line 161-163: 包含路径特征的词
if (word.startsWith('/') && !isSlashCommand) {
return { type: 'file', prefix: word, startPos: start, endPos: end }
}
// Line 182-189: 文件命令后的参数
const afterFileCommand = /\b(cat|ls|cd|vim|code|open|read|edit|...)\s*$/.test(beforeWord)
const hasPathChars = word.includes('/') || word.includes('.') || word.startsWith('~')
生成算法 (generateFileSuggestions)
// Line 439-536
1. 解析路径(支持~扩展)
2. 检查是否@引用路径(特殊处理)
3. 读取目录内容
4. 过滤匹配的文件/目录
5. 生成正确的补全值(处理复杂路径情况)
🔴 问题1: 路径拼接逻辑复杂易错
// Line 483-521: 极其复杂的路径拼接逻辑
if (prefix.includes('/')) {
if (prefix.endsWith('/')) {
value = prefix + entry + (isDir ? '/' : '')
} else {
if (existsSync(absolutePath) && statSync(absolutePath).isDirectory()) {
value = prefix + '/' + entry + (isDir ? '/' : '')
} else {
// 更复杂的部分路径补全...
}
}
}
问题: 多层嵌套if-else,边界条件处理不一致
🔴 问题2: @引用路径处理不完整
// Line 448-452
if (searchPath.startsWith('@')) {
actualSearchPath = searchPath.slice(1) // 简单去掉@
}
问题: @引用应该保持语义,但文件系统查找又需要去掉@,导致混乱
2. 系统命令补全
加载机制 (loadSystemCommands)
// Line 201-254
1. 从$PATH环境变量获取目录列表
2. 扫描每个目录的可执行文件
3. 使用fallback命令列表兜底
4. 缓存结果避免重复扫描
生成算法 (generateUnixCommandSuggestions)
// Line 289-315
1. 过滤匹配前缀的命令
2. 限制最多20个结果
3. 使用◆符号标记系统命令
4. score为85(低于agent的90)
🔴 问题3: 系统命令检测不准确
// Line 228-232
if (stats.isFile() && (stats.mode & 0o111) !== 0) {
commandSet.add(entry)
}
问题: 仅通过文件权限判断可执行性,在Windows/Mac上可能不准确
🔴 问题4: 系统命令与文件补全混淆
// Line 309
type: 'file' as const, // 系统命令被标记为file类型
问题: 类型混用导致逻辑混乱
3. Slash命令补全
触发条件
// Line 145-159
if (word.startsWith('/')) {
if (beforeWord === '' && word === '/') {
// 单独/显示所有命令
return { type: 'command', prefix: '', ... }
} else if (beforeWord === '' && /^\/[a-zA-Z]*$/.test(word)) {
// /help, /model等
return { type: 'command', prefix: word.slice(1), ... }
}
}
生成算法 (generateCommandSuggestions)
// Line 262-286
1. 过滤隐藏命令
2. 匹配前缀(包括别名)
3. 返回带/前缀的命令名
4. score基于匹配程度
🔴 问题5: Slash命令与绝对路径冲突
// Line 111-113
if (input === '/') {
return { type: 'command', ... } // 可能是绝对路径的开始
}
问题: /usr/bin会被误判为slash命令开始
4. @Agent补全(扩展功能)
触发和生成
// Line 166-176: @触发检测
if (word.startsWith('@')) {
return { type: 'agent', prefix: word.slice(1), ... }
}
// Line 543-587: 混合agent和文件建议
const agentSuggestions = generateAgentSuggestions(context.prefix)
const fileSuggestions = generateFileSuggestions(context.prefix)
// 混合显示,agent优先
🔴 问题6: @符号语义不一致
问题: @既用于agent引用,又用于文件引用,导致混淆
三、Tab键行为分析
Terminal兼容行为
// Line 654-745: Tab键处理逻辑
1. 无匹配 → 让Tab通过
2. 单个匹配 → 立即补全
3. 多个匹配 → 检查公共前缀或显示菜单
4. 菜单显示时 → 循环选择
🔴 问题7: Preview模式边界计算错误
// Line 684-689
const currentTail = input.slice(originalContext.startPos)
const nextSpaceIndex = currentTail.indexOf(' ')
const afterPos = nextSpaceIndex === -1 ? '' : currentTail.slice(nextSpaceIndex)
问题: 在输入变化后,原始context位置可能不准确
四、自动触发机制
触发条件 (shouldAutoTrigger)
// Line 1141-1155
case 'command': return true // /总是触发
case 'agent': return true // @总是触发
case 'file': return context.prefix.includes('/') ||
context.prefix.includes('.') ||
context.prefix.startsWith('~')
🔴 问题8: 过度触发
问题: 任何包含/的输入都会触发文件补全,包括URL、正则表达式等
五、复杂边界条件问题汇总
🔴 严重问题
-
路径补全在复杂嵌套目录下失效
src/tools/../../utils/无法正确解析- 符号链接处理不当
-
空格路径处理缺失
"My Documents/"无法补全- 需要引号包裹的路径无法识别
-
Windows路径不兼容
C:\Users\无法识别- 反斜杠路径完全不支持
-
并发状态管理混乱
- 快速输入时状态更新不同步
- Preview模式与实际输入不一致
-
目录权限处理不当
- 无权限目录导致崩溃
- 空目录消息显示后立即消失
🟡 中等问题
-
系统命令缓存永不刷新
- 新安装的命令无法识别
- PATH变化不会更新
-
@引用语义混乱
- @agent-xxx vs @src/file.ts
- 补全后@符号处理不一致
-
Space键行为不一致
- 目录继续导航 vs 文件结束补全
- 逻辑判断复杂易错
-
History导航破坏补全状态
- 上下箭头切换历史时补全面板残留
- isHistoryNavigation判断不准确
-
删除键抑制机制过于简单
- 100ms固定延迟不适合所有场景
- 可能导致正常触发被误抑制
🟢 优化建议
-
简化路径拼接逻辑
- 使用path.join统一处理
- 分离绝对/相对路径逻辑
-
明确类型系统
- 系统命令应有独立type
- @引用应有明确的子类型
-
改进触发机制
- 增加上下文感知(代码 vs 文本)
- 可配置的触发规则
-
优化性能
- 限制文件系统访问频率
- 使用虚拟滚动处理大量建议
-
增强错误处理
- 权限错误优雅降级
- 异步操作超时控制
六、核心设计缺陷
1. 过度复杂的条件判断
- 483-521行的路径拼接有7层嵌套
- 难以理解和维护
2. 类型系统滥用
- 系统命令使用file类型
- agent和file共享@触发器
3. 状态管理混乱
- terminalState、lastTabContext、suggestions等多个状态源
- 同步更新困难
4. 缺乏抽象层
- 直接操作文件系统
- 没有统一的补全提供者接口
七、改进方案建议
// 建议的架构
interface CompletionProvider {
trigger: RegExp | string
canProvide(context: Context): boolean
provide(context: Context): Promise<Suggestion[]>
}
class FileCompletionProvider implements CompletionProvider { }
class CommandCompletionProvider implements CompletionProvider { }
class SystemCommandProvider implements CompletionProvider { }
class AgentCompletionProvider implements CompletionProvider { }
// 统一管理
class CompletionManager {
providers: CompletionProvider[]
async getSuggestions(context: Context) {
const applicable = providers.filter(p => p.canProvide(context))
const results = await Promise.all(applicable.map(p => p.provide(context)))
return merge(results)
}
}
这样可以解决类型混淆、逻辑耦合、扩展困难等核心问题。