Skip to content

Commit b4ac6b8

Browse files
author
Jathin Sreenivas
committed
fix(terminal): address review feedback on send_keys special key handling
1 parent 5ea2370 commit b4ac6b8

3 files changed

Lines changed: 66 additions & 41 deletions

File tree

openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@
3737

3838
ENTER = b"\n"
3939

40+
# Map normalized special key names to ANSI escape bytes for PTY.
41+
_SUBPROCESS_SPECIALS: dict[str, bytes] = {
42+
"ENTER": ENTER,
43+
"TAB": b"\t",
44+
"BS": b"\x7f", # Backspace (DEL)
45+
"ESC": b"\x1b",
46+
"UP": b"\x1b[A",
47+
"DOWN": b"\x1b[B",
48+
"RIGHT": b"\x1b[C",
49+
"LEFT": b"\x1b[D",
50+
"HOME": b"\x1b[H",
51+
"END": b"\x1b[F",
52+
"PGUP": b"\x1b[5~",
53+
"PGDN": b"\x1b[6~",
54+
"C-L": b"\x0c", # Ctrl+L
55+
"C-D": b"\x04", # Ctrl+D (EOF)
56+
"C-C": b"\x03", # Ctrl+C (SIGINT)
57+
}
58+
4059

4160
def _normalize_eols(raw: bytes) -> bytes:
4261
# CRLF/LF/CR -> CR, so each logical line is terminated with \r for the TTY
@@ -351,36 +370,16 @@ def send_keys(self, text: str, enter: bool = True) -> None:
351370
if not self._initialized:
352371
raise RuntimeError("PTY terminal is not initialized")
353372

354-
specials = {
355-
"ENTER": ENTER,
356-
"TAB": b"\t",
357-
"BS": b"\x7f", # Backspace (DEL)
358-
"ESC": b"\x1b",
359-
"UP": b"\x1b[A",
360-
"DOWN": b"\x1b[B",
361-
"RIGHT": b"\x1b[C",
362-
"LEFT": b"\x1b[D",
363-
"HOME": b"\x1b[H",
364-
"END": b"\x1b[F",
365-
"PGUP": b"\x1b[5~",
366-
"PGDN": b"\x1b[6~",
367-
"C-L": b"\x0c", # Ctrl+L
368-
"C-D": b"\x04", # Ctrl+D (EOF)
369-
"C-C": b"\x03", # Ctrl+C (SIGINT)
370-
}
371-
372373
upper = text.upper().strip()
373374
payload: bytes | None = None
374375

375376
# Named specials
376-
if upper in specials:
377-
payload = specials[upper]
377+
if upper in _SUBPROCESS_SPECIALS:
378+
payload = _SUBPROCESS_SPECIALS[upper]
378379
# Do NOT auto-append another EOL; special already includes it when needed.
379380
append_eol = False
380381
# Generic Ctrl-<letter>
381-
elif parse_ctrl_key(text) is not None:
382-
ctrl = parse_ctrl_key(text)
383-
assert ctrl is not None
382+
elif (ctrl := parse_ctrl_key(text)) is not None:
384383
# ctrl is "C-x" — extract the letter
385384
key_char = ctrl[-1].upper()
386385
payload = bytes([ord(key_char) & 0x1F])

openhands-tools/openhands/tools/terminal/terminal/tmux_terminal.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@
2020

2121
logger = get_logger(__name__)
2222

23+
# Map normalized special key names to tmux key names.
24+
_TMUX_SPECIALS: dict[str, str] = {
25+
"ENTER": "Enter",
26+
"TAB": "Tab",
27+
"BS": "BSpace",
28+
"ESC": "Escape",
29+
"UP": "Up",
30+
"DOWN": "Down",
31+
"LEFT": "Left",
32+
"RIGHT": "Right",
33+
"HOME": "Home",
34+
"END": "End",
35+
"PGUP": "PPage",
36+
"PGDN": "NPage",
37+
"C-L": "C-l",
38+
"C-D": "C-d",
39+
"C-C": "C-c",
40+
}
41+
2342

2443
class TmuxTerminal(TerminalInterface):
2544
"""Tmux-based terminal backend.
@@ -130,24 +149,6 @@ def send_keys(self, text: str, enter: bool = True) -> None:
130149
raise RuntimeError("Tmux terminal is not initialized")
131150

132151
# Map normalized names to tmux key names
133-
_TMUX_SPECIALS = {
134-
"ENTER": "Enter",
135-
"TAB": "Tab",
136-
"BS": "BSpace",
137-
"ESC": "Escape",
138-
"UP": "Up",
139-
"DOWN": "Down",
140-
"LEFT": "Left",
141-
"RIGHT": "Right",
142-
"HOME": "Home",
143-
"END": "End",
144-
"PGUP": "PPage",
145-
"PGDN": "NPage",
146-
"C-L": "C-l",
147-
"C-D": "C-d",
148-
"C-C": "C-c",
149-
}
150-
151152
upper = text.strip().upper()
152153

153154
# 1) Named specials

tests/tools/terminal/test_send_keys.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ def test_supported_special_keys_contains_essentials() -> None:
5454
assert key in SUPPORTED_SPECIAL_KEYS
5555

5656

57+
def test_subprocess_specials_match_contract() -> None:
58+
"""Backend specials dicts must stay in sync with SUPPORTED_SPECIAL_KEYS."""
59+
from openhands.tools.terminal.terminal.subprocess_terminal import (
60+
_SUBPROCESS_SPECIALS,
61+
)
62+
63+
assert set(_SUBPROCESS_SPECIALS.keys()) == SUPPORTED_SPECIAL_KEYS
64+
65+
66+
def test_tmux_specials_match_contract() -> None:
67+
from openhands.tools.terminal.terminal.tmux_terminal import (
68+
_TMUX_SPECIALS,
69+
)
70+
71+
assert set(_TMUX_SPECIALS.keys()) == SUPPORTED_SPECIAL_KEYS
72+
73+
5774
# ── SubprocessTerminal.send_keys ────────────────────────────────────
5875

5976

@@ -90,6 +107,14 @@ def test_subprocess_send_keys_ctrl_variants(subprocess_terminal) -> None:
90107
subprocess_terminal.send_keys("CTRL+e")
91108

92109

110+
def test_subprocess_send_keys_echo(subprocess_terminal) -> None:
111+
"""Verify data actually flows through the PTY dispatch path."""
112+
subprocess_terminal.send_keys("echo hello_subprocess")
113+
time.sleep(0.5)
114+
screen = subprocess_terminal.read_screen()
115+
assert "hello_subprocess" in screen
116+
117+
93118
# ── TmuxTerminal.send_keys ─────────────────────────────────────────
94119

95120

0 commit comments

Comments
 (0)