mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-21 06:35:50 +00:00
* fix(tui): avoid submitting rapid multi-line paste bursts * chore(tui): remove paste burst settings picker * fix(tui): reset paste burst on DEL backspace
530 lines
No EOL
14 KiB
Markdown
530 lines
No EOL
14 KiB
Markdown
# 多行粘贴误提交解决方案:Bracketed Paste + Paste-Burst Fallback
|
||
|
||
## 1. 背景
|
||
|
||
在 iTerm2 + tmux 环境下,向 Kimi Code CLI 输入框粘贴包含换行的文本时,预期行为是整段文本进入输入框;但在某些环境下,回车前的语句会被直接提交执行。
|
||
|
||
实测结论:
|
||
|
||
- 当 bracketed paste 标记 `\x1b[200~` / `\x1b[201~` 正常到达时,pi-tui 能正确识别成一次粘贴,不会逐行提交。
|
||
- 当 bracketed paste 标记没有到达时,粘贴内容里的 `\r` 会被当成裸 Enter,触发提交。
|
||
- 单独的 `\n` 在 pi-tui 的 `Editor` 里会被当成换行插入;单独的 `\r` 才是主要提交来源。
|
||
|
||
关键代码位置:
|
||
|
||
- `packages/pi-tui/src/terminal.ts:147`:启动时写入 `\x1b[?2004h`,启用 bracketed paste。
|
||
- `packages/pi-tui/src/terminal.ts:412`:退出时写入 `\x1b[?2004l`,关闭 bracketed paste。
|
||
- `packages/pi-tui/src/stdin-buffer.ts:315-369`:识别 `\x1b[200~` / `\x1b[201~`,组装 bracketed paste。
|
||
- `packages/pi-tui/src/keys.ts:921`:裸 `\r` 被映射成 `enter`。
|
||
- `packages/pi-tui/src/components/editor.ts:780`:单独的 `\n` 被当成换行。
|
||
- `packages/pi-tui/src/components/editor.ts:792`:`enter` 触发 `submitValue()`。
|
||
|
||
## 2. 根因
|
||
|
||
bracketed paste 是由程序主动启用的,流程是:
|
||
|
||
```text
|
||
app 输出 \x1b[?2004h
|
||
→ terminal 把粘贴内容包成 \x1b[200~ ... \x1b[201~
|
||
→ app 识别标记,整段作为 paste 处理
|
||
```
|
||
|
||
如果中间链路正常:
|
||
|
||
```text
|
||
iTerm2 → tmux → pi-tui
|
||
```
|
||
|
||
粘贴内容会被作为一次 paste,不会逐行提交。
|
||
|
||
如果 `\x1b[200~` / `\x1b[201~` 没有到达程序,程序只能看到裸字节流:
|
||
|
||
```text
|
||
line1\rline2\rline3\r
|
||
```
|
||
|
||
此时:
|
||
|
||
- `\r` → `enter` → `submitValue()`
|
||
- 于是第一行被直接提交。
|
||
|
||
需要注意:
|
||
|
||
- iTerm2 的 “Applications in terminal may access clipboard” 是 OSC 52 剪贴板访问,不是 bracketed paste 开关。
|
||
- tmux 的 `set-clipboard` / `allow-passthrough` 主要服务于 OSC 52 / escape passthrough,也不是 bracketed paste 开关。
|
||
- 真正决定 bracketed paste 是否生效的是:终端是否在 app 启用 `\x1b[?2004h` 后,把 `\x1b[200~` / `\x1b[201~` 透传给 app。
|
||
|
||
## 3. 目标
|
||
|
||
解决方案需要满足:
|
||
|
||
1. bracketed paste 正常时,多行粘贴作为整体插入,不逐行提交。
|
||
2. bracketed paste 失效时,多行粘贴尽量不逐行提交。
|
||
3. 不影响正常手敲输入和正常 Enter 提交。
|
||
4. 可测试、可关闭、可灰度。
|
||
5. 不依赖用户手动修改 tmux 配置作为唯一修复手段。
|
||
|
||
## 4. 现有实现对比
|
||
|
||
### 4.1 kimi-code pi-tui
|
||
|
||
已有:
|
||
|
||
- 启用 `\x1b[?2004h`
|
||
- `StdinBuffer` 识别 `\x1b[200~` / `\x1b[201~`
|
||
- `Editor.handlePaste()` 处理粘贴内容
|
||
|
||
缺少:
|
||
|
||
- bracketed paste 标记完全没到时的启发式兜底。
|
||
|
||
### 4.2 oh-my-pi
|
||
|
||
oh-my-pi 是 pi-tui 的演进 fork,核心机制相同,但健壮性更强:
|
||
|
||
- 同样启用 `\x1b[?2004h`
|
||
- `StdinBuffer` 组装 bracketed paste,性能更好
|
||
- 有 paste watchdog,防止结束标记丢失后卡死
|
||
- 有 paste byte limit,限制内存
|
||
- `bracketed-paste.ts` 解码 tmux extended-keys 重编码的控制字节
|
||
|
||
但它仍然依赖 bracketed paste 标记到达,**没有解决“标记完全没到”的情况**。
|
||
|
||
### 4.3 Codex
|
||
|
||
Codex 的处理最值得参考。它是两层方案:
|
||
|
||
1. 正常路径:使用 crossterm 的 bracketed paste。
|
||
2. 兜底路径:在没有 bracketed paste 的终端上,用 `PasteBurst` 启发式识别“快速连续输入 + Enter”的粘贴突发。
|
||
|
||
参考:
|
||
|
||
- [`codex-rs/tui/Cargo.toml`](https://github.com/openai/codex/blob/main/codex-rs/tui/Cargo.toml)
|
||
- [`codex-rs/tui/src/tui.rs`](https://github.com/openai/codex/blob/main/codex-rs/tui/src/tui.rs)
|
||
- [`codex-rs/tui/src/tui/event_stream.rs`](https://github.com/openai/codex/blob/main/codex-rs/tui/src/tui/event_stream.rs)
|
||
- [`codex-rs/tui/src/bottom_pane/paste_burst.rs`](https://github.com/openai/codex/blob/main/codex-rs/tui/src/bottom_pane/paste_burst.rs)
|
||
- [`codex-rs/tui/src/bottom_pane/chat_composer.rs`](https://github.com/openai/codex/blob/main/codex-rs/tui/src/bottom_pane/chat_composer.rs)
|
||
|
||
Codex 的 `handle_paste()` 会把 `\r\n` / `\r` 归一化成 `\n`,避免粘贴里的 CR 触发提交。
|
||
|
||
## 5. 推荐方案
|
||
|
||
采用两层防御:
|
||
|
||
```text
|
||
第一层:bracketed paste 正常路径
|
||
第二层:paste-burst 启发式兜底
|
||
```
|
||
|
||
### 5.1 第一层:保持并增强 bracketed paste
|
||
|
||
保留现有 bracketed paste 流程,并可从 oh-my-pi backport 以下健壮性改进:
|
||
|
||
1. tmux extended-keys 重编码解码
|
||
处理 tmux 在 bracketed paste 内把控制字节重编码成:
|
||
- `\x1b[106;5u`
|
||
- `\x1b[27;5;106~`
|
||
|
||
2. paste watchdog
|
||
开始标记到了但结束标记丢失时,不能永远卡在 paste mode。
|
||
|
||
3. paste byte limit
|
||
防止异常输入无限增长内存。
|
||
|
||
4. O(n) paste 组装
|
||
避免大粘贴时字符串重复拼接导致卡顿。
|
||
|
||
这些改进能增强“标记到了之后”的稳定性,但不能解决“标记没到”的问题。
|
||
|
||
### 5.2 第二层:新增 paste-burst 启发式兜底
|
||
|
||
新增一个 `PasteBurst` 状态机,只在**没有 bracketed paste 标记**时生效,用于识别这种输入:
|
||
|
||
```text
|
||
短时间内连续收到多个普通字符 + Enter
|
||
```
|
||
|
||
典型触发场景:
|
||
|
||
```text
|
||
line1\rline2\rline3\r
|
||
```
|
||
|
||
但没有 `\x1b[200~` / `\x1b[201~` 包裹。
|
||
|
||
进入 paste-burst 后:
|
||
|
||
- 普通字符仍然按现有逻辑正常插入,避免改变当前输入体验。
|
||
- `PasteBurst` 只记录最近普通字符的时间与数量。
|
||
- 当短时间内累计到足够多的普通字符后,接下来的 Enter / `\r` 会被当成换行,而不是提交。
|
||
- burst 结束后的短窗口内,Enter 仍按换行处理,避免粘贴末尾的 Enter 误提交。
|
||
|
||
## 6. PasteBurst 详细设计
|
||
|
||
### 6.1 放置位置
|
||
|
||
建议新增:
|
||
|
||
```text
|
||
packages/pi-tui/src/paste-burst.ts
|
||
```
|
||
|
||
由 `Editor` 使用:
|
||
|
||
```text
|
||
packages/pi-tui/src/components/editor.ts
|
||
```
|
||
|
||
理由:
|
||
|
||
- 裸 `\r` / `\n` 是在 `Editor.handleInput()` 里被解释成 submit / newline 的。
|
||
- `StdinBuffer` 只负责序列切分,不应引入输入框语义。
|
||
- `PasteBurst` 是纯状态机,便于单独测试。
|
||
|
||
### 6.2 输入分类
|
||
|
||
`PasteBurst` 只处理这些输入:
|
||
|
||
1. 单个可打印字符
|
||
包括 ASCII、中文、Emoji 等。
|
||
|
||
2. Enter
|
||
主要是裸 `\r`。
|
||
|
||
3. 非字符输入
|
||
例如方向键、Ctrl/Alt 组合键、popup 导航键等。遇到这些输入时,应调用 `reset()` 清空 burst 状态,再按正常逻辑处理。
|
||
|
||
不要把带修饰键的输入喂给 `PasteBurst`,避免把快捷键误判成粘贴。
|
||
|
||
### 6.3 推荐参数
|
||
|
||
初始值建议参考 Codex,并根据实测调整:
|
||
|
||
```ts
|
||
const PASTE_BURST_MIN_CHARS = 8;
|
||
const PASTE_BURST_CHAR_INTERVAL_MS = 8;
|
||
const PASTE_BURST_ACTIVE_IDLE_TIMEOUT_MS = 30;
|
||
const PASTE_ENTER_SUPPRESS_WINDOW_MS = 120;
|
||
```
|
||
|
||
含义:
|
||
|
||
- `PASTE_BURST_MIN_CHARS`:连续多少个普通字符后才可能是粘贴。
|
||
- `PASTE_BURST_CHAR_INTERVAL_MS`:两个字符间隔小于该值才认为属于同一突发。
|
||
- `PASTE_BURST_ACTIVE_IDLE_TIMEOUT_MS`:进入 burst 后,Enter 在这个窗口内仍按换行处理。
|
||
- `PASTE_ENTER_SUPPRESS_WINDOW_MS`:burst 结束后的一小段时间内,Enter 仍按换行处理,避免粘贴末尾的 Enter 误提交。
|
||
|
||
### 6.4 状态
|
||
|
||
```text
|
||
Idle
|
||
PendingFirstChar
|
||
Buffering
|
||
EnterSuppressWindow
|
||
```
|
||
|
||
状态说明:
|
||
|
||
- `Idle`:没有最近输入状态。
|
||
- `BurstActive`:已经连续收到足够多的快速普通字符,Enter 应按换行处理。
|
||
- `EnterSuppressWindow`:burst 结束后的短窗口,Enter 仍按换行处理。
|
||
|
||
### 6.5 核心方法
|
||
|
||
建议接口:
|
||
|
||
```ts
|
||
class PasteBurst {
|
||
onPlainChar(now: number): void;
|
||
shouldInsertNewlineInsteadOfSubmit(now: number): boolean;
|
||
extendWindow(now: number): void;
|
||
reset(): void;
|
||
}
|
||
```
|
||
|
||
### 6.6 ASCII / 非 ASCII 差异
|
||
|
||
为了尽量降低对正常手敲的影响,当前实现不 hold 任何字符:
|
||
|
||
- ASCII 和非 ASCII 字符都立即插入。
|
||
- `PasteBurst` 只根据输入时序判断接下来的 Enter 是否应该被当成换行。
|
||
- 这样不会引入“先显示一个字符,再突然变成 paste”的闪烁,也不会影响 IME 输入。
|
||
|
||
### 6.7 与 `Editor.handleInput()` 集成
|
||
|
||
处理顺序建议:
|
||
|
||
1. 如果当前输入是 bracketed paste 标记:
|
||
- 走现有 `handlePaste()`。
|
||
- 调用 `pasteBurst.reset()`,避免显式 paste 和 burst 状态互相污染。
|
||
2. 如果当前输入不是普通字符、也不是 Enter:
|
||
- 调用 `pasteBurst.reset()`,避免状态泄漏到后续输入。
|
||
3. 如果当前输入是裸 Enter:
|
||
- 先保留现有 backslash workaround。
|
||
- 如果 `shouldInsertNewlineInsteadOfSubmit(now)` 为 true,则 `addNewLine()`,并 `extendWindow(now)`。
|
||
- 否则走原来的 submit 逻辑。
|
||
4. 如果当前输入是单个可打印字符:
|
||
- 调用 `onPlainChar(now)` 记录时序。
|
||
- 然后按现有逻辑插入字符。
|
||
|
||
## 7. 配置与灰度
|
||
|
||
建议增加一个开关,避免影响所有用户:
|
||
|
||
```ts
|
||
disablePasteBurst?: boolean;
|
||
```
|
||
|
||
或者使用实验 flag:
|
||
|
||
```text
|
||
KIMI_CODE_EXPERIMENTAL_PASTE_BURST
|
||
```
|
||
|
||
推荐发布顺序:
|
||
|
||
1. 默认关闭,先在测试和内部使用。
|
||
2. 手动验证 iTerm2 + tmux、Windows Terminal、普通 macOS Terminal。
|
||
3. 观察是否有正常手敲被误判成 paste。
|
||
4. 默认开启。
|
||
5. 保留关闭开关作为 escape hatch。
|
||
|
||
## 8. 测试计划
|
||
|
||
建议把测试加到现有 `Editor` 测试文件里,避免新增过多测试文件。
|
||
|
||
### 8.1 bracketed paste 路径不变
|
||
|
||
- 输入 `\x1b[200~a\r\nb\x1b[201~`
|
||
- 应作为一次 paste 插入
|
||
- 不触发 submit
|
||
|
||
### 8.2 非 bracketed paste burst 被识别
|
||
|
||
输入:
|
||
|
||
```text
|
||
a
|
||
b
|
||
c
|
||
<idle>
|
||
```
|
||
|
||
字符间隔小于 `PASTE_BURST_CHAR_INTERVAL_MS` 时:
|
||
|
||
- 字符仍按现有逻辑插入
|
||
- 不触发 submit
|
||
- 随后的 Enter 在 burst 窗口内按换行处理
|
||
|
||
### 8.3 burst 内 Enter 不提交
|
||
|
||
输入:
|
||
|
||
```text
|
||
abc\rdef\r
|
||
<idle>
|
||
```
|
||
|
||
期望:
|
||
|
||
- `\r` 被当成换行处理
|
||
- 不触发 submit
|
||
- 输入框保留多行文本
|
||
|
||
### 8.4 正常手敲不受影响
|
||
|
||
输入:
|
||
|
||
```text
|
||
a
|
||
<等待超过 interval>
|
||
b
|
||
<等待超过 interval>
|
||
Enter
|
||
```
|
||
|
||
期望:
|
||
|
||
- `a`、`b` 作为正常输入
|
||
- Enter 正常 submit
|
||
|
||
### 8.5 Enter suppression window
|
||
|
||
输入:
|
||
|
||
```text
|
||
abcdefgh
|
||
Enter
|
||
```
|
||
|
||
如果 Enter 距离上次 burst 活动小于 `PASTE_ENTER_SUPPRESS_WINDOW_MS`:
|
||
|
||
- 先按换行处理
|
||
|
||
超过窗口后:
|
||
|
||
- 恢复 submit 行为
|
||
|
||
### 8.6 非 ASCII / IME
|
||
|
||
输入中文:
|
||
|
||
```text
|
||
测
|
||
试
|
||
文
|
||
本
|
||
```
|
||
|
||
快速连续到达时:
|
||
|
||
- 中文字符仍按现有逻辑立即插入
|
||
- 快速连续输入后的 Enter 应按换行处理
|
||
|
||
### 8.7 disable 开关
|
||
|
||
当 `disablePasteBurst = true`:
|
||
|
||
- 所有输入按正常 typing 处理
|
||
- 不做 paste-burst Enter 抑制
|
||
|
||
## 9. 手动验证
|
||
|
||
### 9.1 bracketed paste 正常
|
||
|
||
在 tmux 里运行:
|
||
|
||
```sh
|
||
printf '\033[?2004h'; cat -A; printf '\033[?2004l'
|
||
```
|
||
|
||
粘贴多行文本,应看到:
|
||
|
||
```text
|
||
^[[200~ ...
|
||
...
|
||
^[[201~
|
||
```
|
||
|
||
### 9.2 CRLF 文件粘贴
|
||
|
||
使用:
|
||
|
||
```text
|
||
paste-test-crlf.txt
|
||
```
|
||
|
||
粘贴到 Kimi Code 输入框:
|
||
|
||
- 修复前:如果 bracketed paste 生效,也不会复现。
|
||
- 修复后:无论 bracketed paste 是否生效,都应尽量作为整段粘贴处理。
|
||
|
||
### 9.3 send-keys 强制模拟裸 Enter
|
||
|
||
```sh
|
||
tmux send-keys -t <target> -l '复现第一行'
|
||
tmux send-keys -t <target> Enter
|
||
tmux send-keys -t <target> -l '复现第二行'
|
||
tmux send-keys -t <target> Enter
|
||
```
|
||
|
||
修复前:裸 Enter 会提交。
|
||
|
||
修复后:如果输入足够快,应被 paste-burst 合并为一次粘贴,至少不应逐行误提交。
|
||
|
||
注意:`send-keys` 的时序可能受 tmux 和系统调度影响,不能完全等同于真实粘贴,只能作为近似验证。
|
||
|
||
## 10. 风险与缓解
|
||
|
||
### 10.1 快速手敲被误判成粘贴
|
||
|
||
风险:
|
||
|
||
```text
|
||
用户快速敲一个短命令 + Enter
|
||
```
|
||
|
||
可能被误判成 paste。
|
||
|
||
缓解:
|
||
|
||
- 只处理无修饰键的普通字符。
|
||
- 要求至少 `PASTE_BURST_MIN_CHARS` 个连续快速字符。
|
||
- 提供 disable 开关。
|
||
- 初始默认关闭,先灰度。
|
||
|
||
### 10.2 短 Enter suppression 影响正常提交
|
||
|
||
`PASTE_ENTER_SUPPRESS_WINDOW_MS` 可能让用户在快速输入后按 Enter 时被当成换行。
|
||
|
||
缓解:
|
||
|
||
- 窗口保持较短,例如 80-120ms。
|
||
- 只在前一个输入被判定为 burst 后开启。
|
||
- 正常慢速输入不开启。
|
||
|
||
### 10.3 慢速粘贴无法识别
|
||
|
||
如果终端把粘贴拆成很慢的 chunk,间隔超过 `PASTE_BURST_CHAR_INTERVAL_MS`,启发式可能失效。
|
||
|
||
缓解:
|
||
|
||
- 可适当提高 interval。
|
||
- 但不能无限提高,否则会误伤正常输入。
|
||
- bracketed paste 仍是主路径,启发式只是兜底。
|
||
|
||
## 11. 环境侧建议
|
||
|
||
应用层启发式是兜底,环境侧仍建议保持健康配置。
|
||
|
||
`~/.tmux.conf`:
|
||
|
||
```conf
|
||
set -g extended-keys on
|
||
```
|
||
|
||
然后重启 tmux:
|
||
|
||
```sh
|
||
tmux kill-server
|
||
tmux new -s test
|
||
```
|
||
|
||
可选,不是 bracketed paste 必需:
|
||
|
||
```conf
|
||
set -g set-clipboard on
|
||
set -g allow-passthrough on
|
||
```
|
||
|
||
这两行主要用于 OSC 52 / escape passthrough,不应被当作 bracketed paste 修复手段。
|
||
|
||
## 12. 推荐落地步骤
|
||
|
||
1. 新增 `PasteBurst` 状态机,先写单元测试。
|
||
2. 在 `Editor.handleInput()` 接入,默认关闭。
|
||
3. 确保 bracketed paste 显式路径不变。
|
||
4. 增加 disable 开关或实验 flag。
|
||
5. 跑现有测试,确认无回归。
|
||
6. 手动验证 iTerm2 + tmux、`send-keys`、CRLF 文件粘贴。
|
||
7. 内部试用一段时间。
|
||
8. 默认开启,保留关闭开关。
|
||
|
||
## 13. 结论
|
||
|
||
根本机制是 bracketed paste:
|
||
|
||
```text
|
||
标记到了 → 一次粘贴 → 不逐行提交
|
||
```
|
||
|
||
但不能保证所有终端、tmux、SSH、Windows 环境都可靠传递标记。
|
||
|
||
因此完整方案是:
|
||
|
||
```text
|
||
bracketed paste 作为主路径;
|
||
paste-burst 启发式作为失效兜底。
|
||
```
|
||
|
||
这和 Codex 的方向一致。它不能从协议层面“根本消除”标记不到的情况,但能把实际误提交概率降到足够低,并且可测试、可关闭、可灰度。 |