What happened
Bug 分析报告
架构背景
npm 的 freebuff 包只是个启动器(launcher.js 负责下载 Bun 二进制到 ~/.config/manicode/freebuff.exe),真正的 TUI 不在 npm 包里。上游源码开源在 CodebuffAI/codebuff,TUI 技术栈为 @opentui/react 0.3.4(Zig 原生渲染核心)+ React 19,全屏 alternate-screen 模式(cli/src/index.tsx:404)。
根因链条
输入框的光标是“假”的:cli/src/components/multiline-input.tsx 用普通 组件渲染输入内容,用 InputCursor 组件手绘一个 ▍ 字符模拟光标(还会闪烁,闪烁时替换为空格)。
真实终端光标被隐藏且从不归位:
OpenTUI 渲染器启动即隐藏硬件光标(renderer.zig:526 setCursorPosition(1, 1, false),每帧写 \x1b[?25l)
每帧结束时,只有当光标状态为 visible 时才会把它 move 到指定位置(renderer.zig:2671 if (cursorPos.visible));不可见时它就地停在本帧 diff 最后写入的那个格子
整个 freebuff 的 cli/src 中没有任何一处调用 renderer.setCursorPosition(...)
IME 候选窗锚点 = 终端硬件光标位置:Windows Terminal / ConPTY 将中文输入法的组字串和候选框定位在终端当前光标坐标处。所以候选框永远出现在“页面文字变化的地方”(diff 渲染终点),而不是输入框。
旁证(同类问题)
Claude Code #13660(已修)、#30982/#50650 —— 同机理:“Ink 渲染循环移动了终端光标,IME 拿到的是渲染位置”
Kimi Code Just after 1.1 session says renews in 16 hour. #1022 —— 完全相同的症状和根因
gemini-cli #8595 —— 同类
反例:opencode 同样用 OpenTUI 却没此 bug —— 因为它的输入框用 TextareaRenderable/EditBufferRenderable,其 renderCursor() 会调用 _ctx.setCursorPosition(cursorX, cursorY, true) 把真实光标放到插入符位置(opentui packages/core/src/renderables/EditBufferRenderable.ts:1011)
修复方向
最小改动:在 multiline-input.tsx 中利用已有的 cursorRow 和 lineInfo.lineStartCols 计算 caret 的 cell 坐标(加上 scrollbox 的屏幕偏移和滚动量),每次 value/cursor 变化后调 renderer.setCursorPosition(col, row, true);同时隐藏自绘 ▍ 避免双光标。
彻底方案:改用 OpenTUI 的 textarea/edit-buffer renderable(内置上述机制)。
Steps to reproduce
Where does this happen?
CLI (terminal client)
Operating system
Windows
Version
0.0.156
Model
No response
Logs or screenshots
What happened
Bug 分析报告
架构背景
npm 的 freebuff 包只是个启动器(launcher.js 负责下载 Bun 二进制到 ~/.config/manicode/freebuff.exe),真正的 TUI 不在 npm 包里。上游源码开源在 CodebuffAI/codebuff,TUI 技术栈为 @opentui/react 0.3.4(Zig 原生渲染核心)+ React 19,全屏 alternate-screen 模式(cli/src/index.tsx:404)。
根因链条
旁证(同类问题)
修复方向
Steps to reproduce
Where does this happen?
CLI (terminal client)
Operating system
Windows
Version
0.0.156
Model
No response
Logs or screenshots