Skip to content

Fix: Enable Bracketed Paste Mode in useTerminalInput#45

Closed
hqwlkj wants to merge 6 commits into
lessweb:mainfrom
hqwlkj:main
Closed

Fix: Enable Bracketed Paste Mode in useTerminalInput#45
hqwlkj wants to merge 6 commits into
lessweb:mainfrom
hqwlkj:main

Conversation

@hqwlkj
Copy link
Copy Markdown
Contributor

@hqwlkj hqwlkj commented May 11, 2026

Summary

修复 useTerminalInput 中 Bracketed Paste Mode(括号粘贴模式)的粘贴处理代码因未启用而成为死代码的问题,使粘贴多行代码时能够作为原子输入正确处理。

Problem

在之前的 commit 4c015a3 中,useTerminalInput 添加了完整的 Bracketed Paste Mode 处理逻辑:

  • 解析终端发送的粘贴起止标记 \u001B[200~ / \u001B[201~
  • 累加粘贴内容并在结束后一次性交付
  • 行尾归一化处理(\r\n / \r\n
  • 新增 paste 标志位区分粘贴与按键输入

但在实际运行中,Bracketed Paste Mode 从未被启用

  1. Ink 框架将 setBracketedPasteMode 方法仅暴露在内部 API(useStdinContext)中,公开的 useStdin() hook 不包含此方法
  2. useTerminalInput 从未调用该方法或直接写入 ANSI 转义序列来启用该模式
  3. 终端未进入 bracketed paste 模式,不会发送 \u001B[200~ / \u001B[201~ 标记
  4. 粘贴处理分支(约 40 行代码)永远不会被执行

因此粘贴多行文本时,终端逐行发送数据,粘贴内容中的 \r 字符会被 parseTerminalInput 误判为 Enter 键,导致粘贴内容被拆解为零散的按键事件,无法正确保留原始格式。

Solution

useTerminalInput 中与 setRawMode(true) 同级启用 bracketed paste mode,通过直接写入 ANSI 转义序列到 stdout:

// 启用 bracketed paste mode
process.stdout.write("\u001B[?2004h");

// 组件卸载时关闭
process.stdout.write("\u001B[?2004l");

这与 Ink 内部 App 组件的 handleSetBracketedPasteMode 实现方式完全一致,都是向终端发送 \u001B[?2004h(启用)/ \u001B[?2004l(关闭)来控制 bracketed paste mode。

Changes

File Change
src/ui/prompt/useTerminalInput.ts useEffect 中新增 process.stdout.write("\u001B[?2004h") 和对应的清理逻辑

Diff

   useEffect(() => {
     if (!isActive) {
       return;
     }
     setRawMode(true);
+    // Enable bracketed paste mode so the terminal wraps pasted text in
+    // \u001B[200~ / \u001B[201~ markers, allowing us to deliver the full
+    // paste as a single atomic input instead of fragmented key events.
+    process.stdout.write("\u001B[?2004h");
     return () => {
       setRawMode(false);
+      process.stdout.write("\u001B[?2004l");
     };
   }, [isActive, setRawMode]);

Effect

Before(Bracketed Paste 未启用)

粘贴 hello\nworld 时,终端发送 h e l l o \r w o r l d,其中 \rparseTerminalInput 识别为 key.return = true,粘贴内容被拆分为多个独立事件。

After(Bracketed Paste 已启用)

粘贴 hello\nworld 时,终端发送包裹后的完整内容:

\u001B[200~hello\rworld\u001B[201~

useTerminalInput 累加起止标记之间的全部文本,归一化行尾符后作为一次原子操作交付给 PromptInput.tsxkey.return = false,所有其他按键标志也为 false,仅 paste = true

效果对比

场景 修改前 修改后
粘贴多行代码 每行作为独立事件,\r 被误判为 Enter 完整粘贴,一次原子操作,换行正确保留
粘贴含控制字符的内容 Tab 等可能触发自动补全 所有控制字符安全传递(key.tab = false
跨平台换行符 仅靠 PromptInput.tsx 归一化 双重保障:hook 层 + PromptInput 层

Verification

typecheck  : ✅ 无错误
lint       : ✅ 0 errors, 0 warnings
format     : ✅ All matched files use Prettier code style
test       : ✅ 150/150 pass, 0 fail

Notes

  • Ink 5.x 的 setBracketedPasteMode 仅通过内部 useStdinContext 暴露,不在公开 API useStdin 中。由于本项目使用自定义的 useTerminalInput(直接监听 stdin.on("data") 而非 Ink 的 useInput / usePaste),直接写入 ANSI 转义序列是最简洁且与 Ink 内部行为一致的方案。
  • paste 标志位在 InputKey 中已就绪,可供后续需要区分粘贴与普通输入的场景使用。

hqwlkj added 6 commits May 11, 2026 15:20
- 添加粘贴标记以区分粘贴输入和普通输入
- 启用终端的括号粘贴模式,使用特殊起止标记包裹粘贴内容
- 累积起止标记之间的文本,作为一个整体传递给输入处理函数
- 规范化粘贴内容中的各种换行符为统一的 \n
- 确保粘贴内容作为单次原子输入,避免碎片化事件
- 在输入状态管理中增加对粘贴事件的识别和处理
- 添加粘贴标记以区分粘贴输入和普通输入
- 启用终端的括号粘贴模式,使用特殊起止标记包裹粘贴内容
- 累积起止标记之间的文本,作为一个整体传递给输入处理函数
- 规范化粘贴内容中的各种换行符为统一的 \n
- 确保粘贴内容作为单次原子输入,避免碎片化事件
- 在输入状态管理中增加对粘贴事件的识别和处理
- 新增通用 DropdownMenu 组件,实现带标题、帮助文本、滚动支持和状态指示的下拉列表
- 将 PromptInput 中技能选择列表替换为 DropdownMenu,简化代码结构
- 将模型和思考模式选择菜单同样替换为 DropdownMenu 组件实现
- 调整文本颜色,将光标前缀颜色改为 #229ac3,加强视觉一致性
- 优化 Footer 文本显示逻辑,仅在菜单或下拉框激活时显示
- 规范异常处理逻辑,改进 ignore 文件读取和路径过滤流程
- 修复 session.ts 中目录读取声明,避免未定义赋值风险
@hqwlkj hqwlkj closed this by deleting the head repository May 13, 2026
jeoor added a commit to jeoor/deepcode-cli that referenced this pull request May 20, 2026
…psing

- Detect bracketed paste (ESC[200~ / ESC[201~) and dispatch as atomic paste event
- Large pastes (>10 lines or >1000 chars) are stored and replaced with a compact marker [paste #N]
- Ctrl+O toggles expand/collapse, backspace/delete atomically remove the entire marker
- Markers are highlighted with chalk.yellow and expanded back on submit
- Follows existing terminal hook patterns (useBracketedPaste alongside useTerminalExtendedKeys)
- Array-based chunk buffering to avoid O(n²) string concatenation on multi-chunk pastes
- Lazy text cleaning deferred to expand/submit time

Known limitation: expand/collapse briefly clears Ink <Static> content above the prompt (React render pipeline constraint).

Reference: PR lessweb#45 (closed), inspired by pi project's paste marker approach.
jeoor added a commit to jeoor/deepcode-cli that referenced this pull request May 20, 2026
…psing

- Detect bracketed paste (ESC[200~ / ESC[201~) and dispatch as atomic paste event
- Large pastes (>10 lines or >1000 chars) are stored and replaced with a compact marker [paste #N]
- Ctrl+O toggles expand/collapse, backspace/delete atomically remove the entire marker
- Markers are highlighted with chalk.yellow and expanded back on submit
- Follows existing terminal hook patterns (useBracketedPaste alongside useTerminalExtendedKeys)
- Array-based chunk buffering to avoid O(n²) string concatenation on multi-chunk pastes
- Lazy text cleaning deferred to expand/submit time

Known limitation: expand/collapse briefly clears Ink <Static> content above the prompt (React render pipeline constraint).

Reference: PR lessweb#45 (closed), inspired by pi project's paste marker approach.
jeoor added a commit to jeoor/deepcode-cli that referenced this pull request May 20, 2026
…psing

- Detect bracketed paste (ESC[200~ / ESC[201~) and dispatch as atomic paste event
- Large pastes (>10 lines or >1000 chars) stored and replaced with compact marker
- Ctrl+O toggles expand/collapse with setTimeout(0) to mitigate render flicker
- Backspace/delete atomically removes entire marker (only for real paste IDs)
- Markers highlighted with chalk.yellow only when ID exists in pastesRef
- Follows existing terminal hook patterns (useBracketedPaste alongside useTerminalExtendedKeys)
- Array-based chunk buffering to avoid O(n²) string concat on multi-chunk pastes
- Lazy text cleaning deferred to expand/submit time
- Regex requires line/char suffix to avoid false positives on literal [paste #N]

Known limitation: expand/collapse briefly clears Ink <Static> content (React render pipeline)

Reference: PR lessweb#45 (closed), inspired by pi project's segmentWithMarkers approach
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant