为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
做 memo code 时我在 Web UI 和 CLI 之间犹豫过,最后还是选了终端。结果真正卡住我的不是模型和工具,而是一个看起来很基础的东西:输入框。
做 memo code(https://github.com/minorcell/memo-code)的时候,MVP、runtime、toolrouter、tools 都写好了,下一步突然被一个很“产品”但本质很“工程”的问题卡住了:界面到底做什么?传统 Web 页面还是终端交互?
做 Web UI 我很拿手,但市面上大多数 code agent(claude code cli、codex cli)都是从终端做起的。最终我决定:memo code 的第一种产品形态,先做 CLI。
选 Ink:看起来都正常,直到输入框
调研开源的 Gemini CLI 时,我发现他们用的是 Ink(React for CLI)。我也就直接跟了:选 Ink。
一开始真的很顺:
- 会话记录渲染没问题
- slash 指令也能做
- 封装组件库也舒服
似乎都挺好……直到我碰到最难的一块:输入框。
以前做 Web app:
- 单行用
input - 多行用
textarea
天然、顺滑、毫无心理负担。
但在终端里,多行输入并不是默认就“应该支持”的体验。甚至 Ink 的 input 组件,也只有单行。
这时候你才会意识到:在终端里,“输入框”不是 UI 控件,它更像是一个小型编辑器。
我以为我解决了,结果只是解决了“最简单的部分”
我一开始尝试的方案很朴素,比如:
- Shift + Enter 插入换行符
表面看起来能用了。 但很快更真实的问题出现了:粘贴文本。
粘贴一段文本时,你会遇到:
- 显示残缺
- 粘贴后光标位置不对
- 输入状态偶尔乱跳
这时候我才明白:我不是在做“多行 input”,我是在终端里硬写一个“半个 textarea”。
如果对照二八法则:掌握 20% 的技术,就能做出 80% 的功能。 但要把剩下 20% 做好,往往需要补齐另外 80% 的细节。 终端交互就是这样:你很快能做出一个“能用”的 CLI;但要做得像样,细节多到离谱。
于是我最后认真设计了一套方案(写在这个 issue 里): https://github.com/minorcell/memo-code/issues/152
解决方案:在 Ink 里做一个“可控的多行编辑器内核”
我最后没有继续纠结“有没有更好的 input 组件”,而是换了一个思路:
把多行输入当成一个小型编辑器来做。 在 Ink 的限制里,把“编辑状态”和“渲染”解耦,然后在输入事件层做适配。
整个方案我拆成三个核心模块。
编辑器状态管理层
我不再把输入框当成“一个字符串”,而是当成一个状态机。
核心结构就是:
value: string(当前文本)cursor: number(光标在文本中的位置)
听起来很简单,但一旦涉及多行、上下移动、终端折行,坑就开始密集出现:
- 光标移动要能跨行
- 上下键移动不能乱跳(要记住“我想待在哪一列”)
- Unicode 也得小心:emoji / 代理对如果按字符串下标移动,光标很容易卡在“半个字符”上
- 所以要做 clamp,保证 cursor 永远落在合法边界
这一层的目标只有一个: 不管 Ink 怎么渲染,我内部都能稳定得到“当前文本是什么 + 光标在哪里”。
粘贴检测
真正让我没绷住的,其实是粘贴。
终端里粘贴一坨文本时,底层输入事件会被拆成很多个 keypress,然后 Ink / 渲染层每次都会触发更新。你会遇到一种非常诡异的现象:
你粘贴的是 A,但 UI 看起来像是 A 的碎片; 光标也像在“追不上输入”,最后漂到一个你完全无法理解的位置。
所以我做了一个“粘贴 burst 检测”:用启发式规则把粘贴从普通输入里识别出来,然后改成 缓冲 + 批量插入:
- 时间间隔规则(主机制):字符到达间隔
< 8ms基本视为粘贴(人不可能这么快) - 字符数量规则(备用机制):连续字符
≥ 16时也按粘贴处理(对中文/emoji 路径更稳) - 识别到粘贴后进入状态机:
pending → active → flush先塞 buffer,等“粘贴结束”再一次性写入value,避免每个字符都触发一轮复杂计算
这一步做完之后,“粘贴残缺 / 光标乱跳”基本从玄学变成可控问题了。
输入处理适配器:快捷键 + 换行策略 + 视觉换行
终端输入要像编辑器,光靠“插入字符”是不够的,你还得补齐肌肉记忆:
- 支持常见快捷键(Ctrl+A / Ctrl+E / Ctrl+U / Ctrl+K / Ctrl+W 这类)
- 换行与提交要分开:
- Shift + Enter 永远插入新行
- Enter 默认提交
- 但如果处在粘贴期间(或粘贴后的短窗口期),Enter 当作插入新行
- 防止用户“粘贴完顺手一回车”直接把消息提交出去了(这个真的很常见)
还有一个关键点:逻辑行 vs 视觉行分离
- 逻辑行:真正的
\n - 视觉行:终端宽度导致的自动折行
编辑用逻辑行,展示按视觉行计算,这样长段落在不同宽度终端也能保持一致体验。
同时,视觉换行还要能响应终端 resize(不然窗口一变宽/变窄,光标又漂移)。
这一层本质上就是: 把终端输入从“能打字”推到“像个 textarea”。
念头通达,交给 codex 快速帮我实现了一个版本。

收尾
这套方案不可能一把梭抹平所有边界问题,但至少把最影响体验的部分解决掉了:多行输入稳定、粘贴不再玄学、光标不再乱飞、Enter / Shift+Enter 行为可控。
memo 的这段实践让我对终端交互有了更清晰的认知:它不再只是”输入输出 + 打日志”,完全可以是简易版 Web App——有组件、有状态、有布局,甚至能长出一点”编辑器”的味道。
Github Repo: Memo Code 站点:Memo Web Site
Discussion
留言区 · GitHub-powered comments via Giscus