
如果你对我的 Code Agent项目感兴趣,可以看这里:
Github Repo: Memo Code - Github
大概四年前,我刚接触编程。学的是 C 语言,第一个程序当然是 hello world。
很简单,几行就写完。run 一下,弹出来一个 terminal(我已经忘了当时用的是什么:cmd?PowerShell?反正不重要),然后打印了一行:
"hello, world!"
从那以后,在我还没接触前端之前,我写的所有程序几乎都靠终端完成输入输出:猜数字、九九乘法表,再到后来刷算法,基本就是------写、跑、看终端、改、再跑。
那时候倒也没觉得有什么问题,只是偶尔会突然有点空虚: 难道以后我工作的成果,就是一直对着这个黑框框吗?
第一种认知:终端就是编程的全部
一开始我对"程序结果形态"的理解非常单一:
输入 → 运行 → 终端输出
终端就是一切:日志、交互、结果展示,全在那儿。 它很直接,很原始,也很"学生气"。
第二种认知:页面才是"程序结果"的另一种世界
后来我接触了前端。直到靠前端拿到第一份实习、第一份工作,我才对"程序结果形态"形成了第二种认知:
不仅仅是终端里几行日志,也可以是页面效果、动画、交互。 之后陆续做过小程序、App、桌面程序......那段时间里,终端更像"开发过程的工具",而不是"产品本体"。
也就是从那时候开始,我对 terminal 的误解更深了: 它好像就应该是黑框框 + 命令 + 日志,仅此而已。
第三种认知:原来 terminal 也可以玩得这么花
25 年下半年,自从 Gemini CLI 这类东西开始出现之后,我对"程序结果形态"有了第三种认知:
原来 terminal 也可以玩得这么花。
彩色输出、输入框、选择框、进度条......该有的都有。 当时我没怎么深入研究,只是隐约意识到:以前我对 terminal 的理解偏了,它并不等于"只能打印"。
现实把我拉回来了:写 Agent,界面到底做什么?
后面开始做值班,又不得不把 Linux 命令捡起来:从 pwd / cat / tail / find,到 vi / vim......慢慢也熟练了。
直到最近两个月,我开始认真做我的 code agent:memo github.com/minorcell/m...
等我写好了 MVP,写好了 runtime,写好了 toolrouter、tools 等;下一步突然被一个看起来很"产品"、但本质上很"工程"的问题卡住了:
界面到底做什么? 传统 Web 页面?还是终端交互?
如果做传统 Web UI,我其实很拿手:加一个 HTTP server 包,再来个 Web UI 包就够了。 但现实是,市面上大多数 code agent(比如 claude code cli、codex cli)都是从终端交互做起的。后续再补 VSCode 插件、桌面版,甚至浏览器插件。
题外话:这里也不得不感慨一下------原来我大前端确实挺"六"的:只要界面能画出来,基本都能做。也更坚定一个想法:AI 时代,大前端技术只会更普及。
最终,可能是"理所当然",也可能是对陌生技术栈的兴趣使然,我决定: 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 里): github.com/minorcell/m...
解决方案:在 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 快速帮我实现了一个版本。

结果:我解决了剩下 20% 里最烦的 15%
这套方案不可能一把梭把所有边界问题抹平。 不同终端模拟器、不同输入法路径、极端大文本性能......仍然需要持续打磨。
但至少到这里,我觉得我把剩下 20% 里最难受、最影响体验的那 15% 解决掉了:
- 多行输入稳定
- 粘贴不再玄学
- 光标不再乱飞
- Enter / Shift+Enter 行为可控
收尾:终端不只是输入输出,它可以是简易版 Web App
memo 的这段实践,让我对终端交互有了更清晰的认知:
它不再只是我最开始学编程时那种"输入输出 + 打日志"。 它完全可以是简易版本的 Web App:有组件、有状态、有布局,甚至能长出一点"编辑器"的味道。
这感觉有点像当年最早的 HTML 刚出来时:朴素、克制,但足够表达。 而我现在做的,就是在这个黑框框里,把"能表达的东西"再往前推一点点。
如果你对我的 Code Agent项目感兴趣,可以看这里:
Github Repo: Memo Code - Github