fix(cli): strip leaked bracketed-paste wrappers
This commit is contained in:
parent
7c63c24613
commit
a0fe73bada
41
cli.py
41
cli.py
@ -15,6 +15,7 @@ Usage:
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import json
|
||||
@ -1547,6 +1548,32 @@ def _should_auto_attach_clipboard_image_on_paste(pasted_text: str) -> bool:
|
||||
return not pasted_text.strip()
|
||||
|
||||
|
||||
def _strip_leaked_bracketed_paste_wrappers(text: str) -> str:
|
||||
"""Strip leaked bracketed-paste wrapper markers from user-visible text.
|
||||
|
||||
Defensive normalization for cases where terminal/prompt_toolkit parsing
|
||||
fails and bracketed-paste markers end up in the buffer as literal text.
|
||||
|
||||
We strip canonical wrappers unconditionally and also handle degraded
|
||||
visible forms like ``[200~`` / ``[201~`` and ``00~`` / ``01~`` when they
|
||||
look like wrapper boundaries, not arbitrary user content.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
text = (
|
||||
text.replace("\x1b[200~", "")
|
||||
.replace("\x1b[201~", "")
|
||||
.replace("^[[200~", "")
|
||||
.replace("^[[201~", "")
|
||||
)
|
||||
text = re.sub(r"(^|[\s\n>:\]\)])\[200~", r"\1", text)
|
||||
text = re.sub(r"\[201~(?=$|[\s\n<\[\(\):;.,!?])", "", text)
|
||||
text = re.sub(r"(^|[\s\n>:\]\)])00~", r"\1", text)
|
||||
text = re.sub(r"01~(?=$|[\s\n<\[\(\):;.,!?])", "", text)
|
||||
return text
|
||||
|
||||
|
||||
def _collect_query_images(query: str | None, image_arg: str | None = None) -> tuple[str, list[Path]]:
|
||||
"""Collect local image attachments for single-query CLI flows."""
|
||||
message = query or ""
|
||||
@ -9759,6 +9786,7 @@ class HermesCLI:
|
||||
# Normalise line endings — Windows \r\n and old Mac \r both become \n
|
||||
# so the 5-line collapse threshold and display are consistent.
|
||||
pasted_text = pasted_text.replace('\r\n', '\n').replace('\r', '\n')
|
||||
pasted_text = _strip_leaked_bracketed_paste_wrappers(pasted_text)
|
||||
if _should_auto_attach_clipboard_image_on_paste(pasted_text) and self._try_attach_clipboard_image():
|
||||
event.app.invalidate()
|
||||
if pasted_text:
|
||||
@ -9900,7 +9928,15 @@ class HermesCLI:
|
||||
still batch newlines. Alt+Enter only adds 1 newline per
|
||||
event so it never triggers this.
|
||||
"""
|
||||
text = buf.text
|
||||
text = _strip_leaked_bracketed_paste_wrappers(buf.text)
|
||||
if text != buf.text:
|
||||
cursor = min(buf.cursor_position, len(text))
|
||||
_paste_just_collapsed[0] = True
|
||||
buf.text = text
|
||||
buf.cursor_position = cursor
|
||||
_prev_text_len[0] = len(text)
|
||||
_prev_newline_count[0] = text.count('\n')
|
||||
return
|
||||
chars_added = len(text) - _prev_text_len[0]
|
||||
_prev_text_len[0] = len(text)
|
||||
if _paste_just_collapsed[0] or self._skip_paste_collapse:
|
||||
@ -10648,6 +10684,9 @@ class HermesCLI:
|
||||
submit_images = []
|
||||
if isinstance(user_input, tuple):
|
||||
user_input, submit_images = user_input
|
||||
|
||||
if isinstance(user_input, str):
|
||||
user_input = _strip_leaked_bracketed_paste_wrappers(user_input)
|
||||
|
||||
# Check for commands — but detect dragged/pasted file paths first.
|
||||
# See _detect_file_drop() for details.
|
||||
|
||||
49
tests/cli/test_cli_bracketed_paste_sanitizer.py
Normal file
49
tests/cli/test_cli_bracketed_paste_sanitizer.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Tests for defensive bracketed-paste wrapper stripping in the CLI."""
|
||||
|
||||
from cli import _strip_leaked_bracketed_paste_wrappers
|
||||
|
||||
|
||||
class TestStripLeakedBracketedPasteWrappers:
|
||||
def test_plain_text_unchanged(self):
|
||||
text = "hello world"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == text
|
||||
|
||||
def test_strips_canonical_escape_wrappers(self):
|
||||
text = "\x1b[200~hello\x1b[201~"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "hello"
|
||||
|
||||
def test_strips_visible_caret_escape_wrappers(self):
|
||||
text = "^[[200~hello^[[201~"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "hello"
|
||||
|
||||
def test_strips_degraded_bracket_only_wrappers(self):
|
||||
text = "[200~hello[201~"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "hello"
|
||||
|
||||
def test_strips_degraded_bracket_only_wrappers_after_whitespace(self):
|
||||
text = "prefix [200~hello[201~ suffix"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "prefix hello suffix"
|
||||
|
||||
def test_strips_wrapper_fragments_at_boundaries(self):
|
||||
text = "00~hello world01~"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "hello world"
|
||||
|
||||
def test_strips_wrapper_fragments_after_whitespace(self):
|
||||
text = "prefix 00~hello world01~ suffix"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "prefix hello world suffix"
|
||||
|
||||
def test_does_not_strip_non_wrapper_00_tilde_in_normal_text(self):
|
||||
text = "build00~tag should stay"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == text
|
||||
|
||||
def test_does_not_strip_non_wrapper_bracket_forms_in_normal_text(self):
|
||||
text = "literal[200~tag and literal[201~tag should stay"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == text
|
||||
|
||||
def test_preserves_multiline_content_while_stripping_wrappers(self):
|
||||
text = "^[[200~line 1\nline 2\nline 3^[[201~"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "line 1\nline 2\nline 3"
|
||||
|
||||
def test_preserves_multiline_content_while_stripping_degraded_bracket_only_wrappers(self):
|
||||
text = "[200~line 1\nline 2\nline 3[201~"
|
||||
assert _strip_leaked_bracketed_paste_wrappers(text) == "line 1\nline 2\nline 3"
|
||||
Loading…
Reference in New Issue
Block a user