[Bug已解决] 多行粘贴时首行被立即提交-剩余行残留-终端 bracketed-paste-方案

[Bug已解决] 多行粘贴时首行被立即提交-剩余行残留-终端 bracketed-paste-方案
[Bug已解决] 多行粘贴时首行被立即提交、剩余行残留终端 bracketed paste 模式解决方案一、现象长什么样你在 CLI 的输入框里粘贴了一大段多行文本比如一段代码、一段 prompt。结果只有第一行被立即当作「回车提交」发出了去而剩下的行还留在输入框里或也被逐行错误提交。即 GitHub Issues (openai/codex) #31416Multiline paste in Codex CLI submits the first line immediately and leaves remaining lines in the input box。 本质是CLI 的输入读取按「行遇到换行符 \n 即提交」处理但「粘贴」是把多行内容一次性灌进 stdin每行末尾的 \n 被当成「用户按了回车」于是第一行及其实 \n触发提交后续行被当作新的输入残留。这是 TTY 行规则与「粘贴」语义冲突的经典问题。 本文聚焦通用技术终端的「行规则line discipline」、为什么粘贴会触发逐行提交、bracketed paste mode括号粘贴模式\e[200~/\e[201~如何区分「粘贴」与「真实回车」用真实可运行代码说明不依赖具体产品。二、背景为什么「粘贴」会触发回车终端PTY默认开启ICANON规范模式行规则输入按行缓冲遇到\n回车就递交给程序一行。程序读输入通常用readline()读到\n就返回这一行。 当你粘贴多行时剪贴板内容含多个\n被终端一次性注入 stdin。在规范模式下每一行的\n都触发「一行就绪」——于是程序读到第一行 \n→ 当作用户按回车提交剩余行继续在缓冲区程序下一个读取又拿到一行……表现就是「首行被提交、其余残留/逐行冒出」。 而「真实回车」和「粘贴里的 \n」在规范模式下无法区分——都是\n。要让程序知道「这是一次粘贴别逐行提交」需要括号粘贴模式。三、bracketed paste mode括号粘贴是什么现代终端支持bracketed paste mode程序开启后终端在粘贴内容的首尾自动包裹转义序列粘贴开始\e[200~ESC[200~粘贴结束\e[201~ESC[201~ 即你粘贴的内容在 stdin 里变成\e[200~第一行\n第二行\n第三行\n\e[201~程序读到\e[200~就知道「接下来到\e[201~之间是一整段粘贴不要遇 \n 就提交」于是整段作为一块处理或进入多行编辑模式而不是逐行提交。 这是解决「多行粘贴误提交」的标准机制几乎所有现代终端iTerm2、Terminal.app、Windows Terminal、tmux和编辑器vim、readline都支持。四、最小可运行检测 bracketed paste 序列下面用 Python 演示如何识别粘贴的包裹序列从而把多行内容当一块而非逐行提交。import sys, select BRACKET_START \x1b[200~ # 粘贴开始 BRACKET_END \x1b[201~ # 粘贴结束 def read_input_respecting_paste(): buf while True: if not select.select([sys.stdin], [], [], 0.2)[0]: if buf: return (line, buf) # 超时按普通行返回 continue ch sys.stdin.read(1) if ch \x1b: # 可能是转义序列尝试读完整 seq ch sys.stdin.read(5) # 粗略读 [200~ / [201~ if seq.startswith(BRACKET_START): # 进入粘贴模式读到 END 前的内容都算一块 pasted while True: c sys.stdin.read(1) if c \x1b: tail c sys.stdin.read(5) if tail.startswith(BRACKET_END): break pasted tail else: pasted c return (paste, pasted) # 整块返回不逐行提交 else: buf seq elif ch in (\n, \r): # 普通回车提交当前行 return (line, buf) else: buf ch # 用法若返回类型是 paste整段交给多行处理器line 才按单条提交要点识别\e[200~/\e[201~把中间多行当「一块粘贴」整体处理不遇 \n 即提交。五、解决方案一开启 bracketed paste mode核心程序启动时向终端发送开启括号粘贴的转义序列并在读取逻辑里处理包裹import sys, os, tty, termios def enable_bracketed_paste(): # 发送开启序列 sys.stdout.write(\x1b[?2004h) sys.stdout.flush() def disable_bracketed_paste(): sys.stdout.write(\x1b[?2004l) sys.stdout.flush() # 开启后终端会在粘贴时包裹 \e[200~ ... \e[201~ # 读取逻辑见第四节识别包裹整块处理?2004h是「开启 bracketed paste」?2004l是关闭。开启后粘贴内容被包裹程序据此区分「粘贴」与「真实回车」。六、解决方案二读取逻辑区分 paste 与 line结合方案一读取循环见第四节必须收到\e[200~→ 进入「累积粘贴」状态直到\e[201~整块交给多行处理器收到裸\n不在粘贴态→ 才是「用户按回车提交单条」这样多行粘贴不会被首行 \n 触发提交剩余行也不会残留。 这是修复 #31416 的直接手段首行不再被粘贴里的 \n 误提交整段进入多行编辑/一次性提交。七、解决方案三若不用 bracketed paste则禁用规范模式逐行提交若终端不支持 bracketed paste老旧环境可把终端设成非规范模式raw自己实现「回车 提交」的判定import tty, termios, sys def set_raw(): fd sys.stdin.fileno() old termios.tcgetattr(fd) tty.setraw(fd) return old # raw 模式下程序逐字符读自己判断 # - 单独 Enter\r- 提交 # - 粘贴的多行含 \n- 累积不遇 \r 不提交 # 但 raw 模式下「真实回车」是 \r 而非 \n粘贴里是 \n # 于是可区分粘贴用 \n用户回车用 \r作为兜底注意raw 模式要自己处理退格、转义等复杂优先用 bracketed paste方案一。八、解决方案四用成熟输入库readline / prompt_toolkit手搓输入读取易漏。生产用GNU readline默认支持 bracketed paste新版本input()/readline已处理prompt_toolkit内置多行编辑 bracketed paste开箱即用它们已正确区分粘贴/回车避免 #31416。 自研 TUI 才需自己实现方案一二。九、排查清单多行粘贴误提交粘贴多行时首行被提交、其余残留 → 粘贴的 \n 被当回车#31416 类。开启 bracketed paste发\e[?2004h读\e[200~/\e[201~包裹。读取逻辑粘贴态内累积整块不遇 END 不提交裸 \r/\n 才提交单条。兜底raw 模式用\r(回车) vs\n(粘贴) 区分。用成熟库readline / prompt_toolkit 已处理。测试在支持 bracketed paste 的终端iTerm2/Windows Terminal/tmux验证多行粘贴不误提交。十、小结Multiline paste in Codex CLI submits the first line immediately and leaves remaining lines in the input boxopenai/codex #31416的本质是CLI 输入按「行规则遇 \n 即提交」读取而「粘贴多行」把所有行的 \n 一次性灌入 stdin每行 \n 被当成「用户回车」触发提交于是首行被发、剩余行残留。规范模式下「真实回车」与「粘贴的 \n」无法区分。 通用解决方案适用于任何 TTY 行输入程序开启 bracketed paste发\e[?2004h终端会在粘贴内容首尾包裹\e[200~/\e[201~读取区分识别包裹序列把中间多行当「一块粘贴」整体处理不遇 \n 即提交裸回车\r/\n 不在粘贴态才是「用户提交」兜底 raw 模式用\r回车vs\n粘贴区分用成熟库readline / prompt_toolkit 已正确支持 bracketed paste。 记住粘贴和回车在规范模式下都是 \n无法区分。bracketed paste mode 是终端提供的「区分粘贴与回车」的标准机制——开启它多行粘贴才不会被逐行误提交。