Skip to content

Latest commit

 

History

History
529 lines (424 loc) · 15.1 KB

File metadata and controls

529 lines (424 loc) · 15.1 KB

终端输入框最佳实践 - 全局最优方案

基于对 Gemini CLI, Claude Code 和当前 Kode 的深入分析

核心发现:不要直接使用 Ink 的 TextInput!

问题根源

经过分析业界最先进的终端 AI 工具,我们发现:

❌ 错误做法(当前 Kode):

// 直接使用 Ink 的 TextInput 组件
import TextInput from '#ui-ink/components/TextInput'

<TextInput 
  value={input}
  onChange={onChange}
  // ... 各种 props
/>

✅ 正确做法(Gemini CLI):

// 实现自定义的 TextBuffer 管理输入状态
// 直接控制终端渲染,不依赖 Ink 的 TextInput

export class TextBuffer {
  private lines: string[] = ['']
  private cursorLine: number = 0
  private cursorCol: number = 0
  // ... 完全自主的状态管理
}

Gemini CLI 的架构优势

1. TextBuffer - 核心抽象层

文件: packages/cli/src/ui/components/shared/text-buffer.ts

/**
 * TextBuffer 完全接管输入状态管理
 * - 多行编辑支持
 * - Unicode 字符正确处理(中文、emoji、组合字符)
 * - 精确的光标位置计算
 * - 独立于渲染层
 */
export class TextBuffer {
  // 使用 Code Points 而非字符串索引
  private toCodePoints(text: string): string[]
  
  // 处理 Unicode 脚本边界(中文、日文、韩文等)
  isDifferentScript(char1: string, char2: string): boolean
  
  // 支持组合字符(带音标的字母等)
  isCombiningMark(char: string): boolean
  
  // 精确的视觉宽度计算
  getCachedStringWidth(text: string): number
}

关键优势:

  • 字符宽度正确: 使用 string-width 库正确计算中文等宽字符
  • IME 友好: 独立状态管理,不受 React 重渲染影响
  • 性能优化: 可以实现细粒度的更新策略
  • 灵活性高: 可以添加任意自定义行为(vim模式、语法高亮等)

2. 分离输入逻辑和渲染逻辑

当前 Kode 的问题:

// PromptInput.tsx - 混合了太多职责
function PromptInput() {
  const [input, setInput] = useState('')
  const [cursorOffset, setCursorOffset] = useState(0)
  // ... 补全逻辑
  // ... 历史记录
  // ... 按键处理
  // ... 渲染逻辑
  
  return <TextInput /> // 所有逻辑耦合在一起
}

Gemini CLI 的做法:

// InputPrompt.tsx - 只负责协调
export const InputPrompt = ({ buffer, ... }) => {
  // buffer 是独立的 TextBuffer 实例
  // 使用 hooks 管理各种功能
  const completion = useCommandCompletion(buffer, ...)
  const history = useInputHistory(buffer)
  const vim = useVimMode(buffer)
  
  // 渲染逻辑非常简单
  return (
    <Box>
      {renderInputLines(buffer)}
      {completion.suggestions && <SuggestionsDisplay />}
    </Box>
  )
}

3. 鼠标支持和高级交互

Gemini CLI 实现了完整的鼠标协议:

// utils/input.ts
export const SGR_MOUSE_REGEX = /^\x1b\[<(\d+);(\d+);(\d+)([mM])/
export const X11_MOUSE_REGEX = /^\x1b\[M([\s\S]{3})/

// 检测鼠标事件
export function couldBeMouseSequence(buffer: string): boolean {
  // 支持 SGR 和 X11 两种鼠标协议
}

功能:

  • ✅ 点击补全建议
  • ✅ 拖拽选择文本
  • ✅ 滚动历史记录
  • ✅ 上下文菜单

4. 键盘协议支持

Kitty Keyboard Protocol - 现代终端的增强键盘支持:

// hooks/useKittyKeyboardProtocol.ts
export function useKittyKeyboardProtocol() {
  useEffect(() => {
    // 启用 Kitty 协议
    process.stdout.write('\x1b[>1u')
    
    return () => {
      // 清理
      process.stdout.write('\x1b[<1u')
    }
  }, [])
}

好处:

  • ✅ 区分 Ctrl+ITab
  • ✅ 检测所有修饰键组合
  • ✅ 更准确的按键事件
  • ✅ 支持粘贴保护

5. 中文输入优化

Gemini CLI 的 IME 处理策略:

// 检测连续快速输入(可能是 IME)
const timeSinceLastInput = now - lastInputTime
const isPossiblyIMEInput = timeSinceLastInput > 0 && timeSinceLastInput < 150

// 暂停自动补全
if (isPossiblyIMEInput && !completionActive) {
  return // 不触发补全
}

// 使用 Code Points 处理 Unicode
const chars = toCodePoints(line)
for (const char of chars) {
  // 正确处理每个 Unicode 字符
}

// 检测脚本边界(中文 vs 英文等)
if (isDifferentScript(currentChar, nextChar)) {
  // 单词边界
}

当前 Kode 的具体问题

问题 1: 依赖 Ink 的 TextInput

位置: apps/cli/src/ui/components/TextInput.tsx

// 当前代码使用 Ink 的 useInput hook
import { useInput } from 'ink'

export default function TextInput({ value, onChange, ... }) {
  useInput((input, key) => {
    // Ink 的 useInput 有以下问题:
    // 1. 不能精确控制 IME 输入
    // 2. 中文字符宽度计算不准确
    // 3. 性能问题(每次按键都重渲染)
    // 4. 难以实现高级功能(vim 模式等)
  })
}

问题 2: 状态管理混乱

位置: apps/cli/src/ui/components/PromptInput/PromptInput.tsx

// 太多 state 在一个组件中
const [input, setInput] = useState('')
const [cursorOffset, setCursorOffset] = useState(0)
const [exitMessage, setExitMessage] = useState({})
const [message, setMessage] = useState({})
const [modelSwitchMessage, setModelSwitchMessage] = useState({})
// ... 还有更多

// 导致:
// - 每次按键都可能触发多次重渲染
// - 状态更新顺序难以控制
// - 难以调试和维护

问题 3: 光标位置计算错误

位置: apps/cli/src/ui/hooks/useTextInput.ts

// 当前使用字符索引作为 cursorOffset
const cursor = Cursor.fromText(originalValue, columns, offset)

// 问题:
// - offset 是字符串索引,不是视觉位置
// - 中文字符占 2 个视觉宽度,但索引只是 1
// - 导致光标显示位置错误

全局最优方案

架构设计

┌─────────────────────────────────────────────────────┐
│                   React 组件层                        │
│  (InputPrompt, SuggestionsDisplay, etc.)            │
└────────────────┬────────────────────────────────────┘
                 │
                 │ props & callbacks
                 │
┌────────────────▼────────────────────────────────────┐
│              TextBuffer 核心层                        │
│  - 状态管理(文本、光标、选择)                          │
│  - Unicode 正确处理                                   │
│  - 独立于渲染的逻辑                                    │
└────────────────┬────────────────────────────────────┘
                 │
                 │ state queries & mutations
                 │
┌────────────────▼────────────────────────────────────┐
│              功能增强层(Hooks)                       │
│  - useCommandCompletion                             │
│  - useInputHistory                                  │
│  - useVimMode                                       │
│  - useReverseSearch                                 │
│  - useMouse                                         │
└────────────────┬────────────────────────────────────┘
                 │
                 │ terminal I/O
                 │
┌────────────────▼────────────────────────────────────┐
│              终端协议层                               │
│  - Kitty Keyboard Protocol                         │
│  - SGR Mouse Events                                 │
│  - Bracketed Paste Mode                            │
└─────────────────────────────────────────────────────┘

实现步骤

阶段 1: 创建 TextBuffer 核心(1-2 周)

// packages/core/src/input/TextBuffer.ts

export class TextBuffer {
  private lines: string[] = ['']
  private cursorLine: number = 0
  private cursorCol: number = 0
  private selection: Selection | null = null
  
  // 核心 API
  getText(): string
  setText(text: string): void
  getCursorPosition(): [number, number]
  setCursorPosition(line: number, col: number): void
  
  // 编辑操作
  insertText(text: string): void
  deleteRange(start: Position, end: Position): void
  
  // 移动光标
  moveCursor(direction: Direction): void
  moveToWordBoundary(direction: 'start' | 'end'): void
  
  // Unicode 支持
  toCodePoints(text: string): string[]
  getVisualWidth(text: string): number
  
  // 历史记录
  undo(): void
  redo(): void
}

阶段 2: 重构 InputPrompt 组件(1 周)

// apps/cli/src/ui/components/PromptInput/PromptInput.tsx

export function PromptInput() {
  // 使用 useTextBuffer hook 管理 TextBuffer 实例
  const buffer = useTextBuffer()
  
  // 各种功能通过 hooks 组合
  const completion = useCommandCompletion(buffer)
  const history = useInputHistory(buffer)
  const keyboard = useKeyboardHandler(buffer)
  
  // 渲染逻辑简化
  return (
    <Box>
      <InputDisplay buffer={buffer} />
      {completion.visible && (
        <CompletionPanel 
          suggestions={completion.suggestions}
          selectedIndex={completion.selectedIndex}
        />
      )}
    </Box>
  )
}

阶段 3: 优化性能和体验(1 周)

// 1. 使用 React.memo 减少重渲染
const InputDisplay = React.memo(({ buffer }) => {
  // 只在 buffer 实际改变时重渲染
}, (prev, next) => prev.buffer.version === next.buffer.version)

// 2. 实现细粒度更新
const CompletionPanel = React.memo(({ suggestions, selectedIndex }) => {
  // 只在建议列表或选中项改变时重渲染
})

// 3. 添加防抖/节流
const debouncedCompletion = useDebouncedCallback(
  () => updateCompletions(buffer),
  150 // 对中文输入友好
)

阶段 4: 高级功能(可选,1-2 周)

// Vim 模式
const useVimMode = (buffer: TextBuffer) => {
  const [mode, setMode] = useState<'normal' | 'insert'>('insert')
  // 实现 vim 按键映射
}

// 鼠标支持
const useMouse = (buffer: TextBuffer) => {
  // 处理鼠标点击、拖拽
  // 使用 SGR mouse protocol
}

// 语法高亮
const useSyntaxHighlight = (buffer: TextBuffer, language: string) => {
  // 实时语法高亮
  // 使用 highlight.js 或 shiki
}

测试策略

单元测试

describe('TextBuffer', () => {
  it('should handle Chinese input correctly', () => {
    const buffer = new TextBuffer()
    buffer.insertText('你好世界')
    expect(buffer.getText()).toBe('你好世界')
    expect(buffer.getCursorPosition()).toEqual([0, 4]) // 4 个字符
    expect(buffer.getVisualWidth()).toBe(8) // 8 个视觉宽度
  })
  
  it('should handle emoji correctly', () => {
    const buffer = new TextBuffer()
    buffer.insertText('Hello 👨‍👩‍👧‍👦 World')
    // 测试组合 emoji 的正确处理
  })
  
  it('should handle IME composition', () => {
    // 模拟 IME 输入序列
    // 验证不会触发过早的补全
  })
})

集成测试

describe('InputPrompt', () => {
  it('should not show completion during Chinese input', async () => {
    // 模拟快速连续输入(< 150ms)
    // 验证补全面板不闪烁
  })
  
  it('should close completion when deleting trigger character', () => {
    // 输入 "/"
    // 验证补全面板打开
    // 删除 "/"
    // 验证补全面板关闭
  })
})

性能测试

describe('Performance', () => {
  it('should handle 10000 character input', () => {
    const buffer = new TextBuffer()
    const start = Date.now()
    buffer.insertText('a'.repeat(10000))
    const end = Date.now()
    expect(end - start).toBeLessThan(100) // 应该 < 100ms
  })
  
  it('should not re-render on every keystroke', () => {
    let renderCount = 0
    // 使用 react-testing-library 计数渲染次数
    // 验证优化是否生效
  })
})

迁移路径

渐进式迁移(推荐)

阶段 1: 并行实现

// 保留旧的 PromptInput
// 创建新的 PromptInputV2 使用 TextBuffer
// 通过环境变量切换
const PromptInput = process.env.USE_NEW_INPUT 
  ? PromptInputV2 
  : PromptInputV1

阶段 2: A/B 测试

// 随机给用户分配新旧版本
// 收集性能数据和用户反馈

阶段 3: 完全切换

// 删除旧实现
// TextBuffer 成为唯一方案

性能对比(预期)

指标 当前 Kode 新方案(TextBuffer) 改善
中文输入流畅度 +80%
补全面板闪烁 极低 -90%
大文本性能 +60%
渲染次数/按键 3-5次 1次 -70%
CPU 使用率 ~15% ~5% -65%
内存占用 基准 -20% -20%

参考实现

Gemini CLI

  • 优点: 最先进、最完整的实现
  • TextBuffer: packages/cli/src/ui/components/shared/text-buffer.ts
  • InputPrompt: packages/cli/src/ui/components/InputPrompt.tsx
  • Mouse/Keyboard: packages/cli/src/ui/utils/input.ts

其他工具对比

工具 输入方案 优点 缺点
Gemini CLI 自定义 TextBuffer + Ink 最完整、性能最好 代码量大
Claude Code 未知(打包) 官方实现 无法学习
Kode (当前) Ink TextInput 简单 问题多
Goose Rust/ratatui 性能极佳 不适用于 Node.js

总结

不要尝试修补当前的 Ink TextInput 方案!

基于业界最佳实践(Gemini CLI),正确的做法是:

  1. 实现自定义 TextBuffer - 完全控制输入状态
  2. 分离关注点 - 输入逻辑、渲染、功能增强各司其职
  3. Unicode 优先 - 从设计之初就正确处理多语言
  4. 性能优化 - 细粒度更新、减少重渲染
  5. 渐进增强 - 先实现核心,再添加高级功能

这个方案虽然初期投入大,但长期收益巨大:

  • 🎯 彻底解决中文输入问题
  • 🎯 补全面板不再闪烁
  • 🎯 性能显著提升
  • 🎯 为未来功能(vim模式、鼠标支持等)打好基础
  • 🎯 代码更易维护和测试

预计总开发时间: 3-4 周 预计代码量: ~2000-3000 行 风险等级: 中(需要充分测试) 优先级: 🔴 高(影响核心用户体验)


更新日期: 2026-01-07
分析基于: Gemini CLI v0.21.0, Kode v2.0.3
建议审核: 核心团队