fix: split submit key and prompt pasting
This commit is contained in:
+30
-14
@@ -43,6 +43,13 @@ _VALID_PERMISSION_MODES: frozenset[str] = frozenset(
|
||||
_DEFAULT_DRAIN_CHUNK = 65536
|
||||
_DEFAULT_OUTPUT_BUFFER_CAP = 1_000_000
|
||||
|
||||
# Gap between the bracketed-paste closing marker and the Enter keystroke
|
||||
# in `PtyClaudeProcess.write()`. Claude's Ink-based TUI reads stdin in
|
||||
# chunks; if `\r` is glued to `ESC [ 201 ~` it gets absorbed by the
|
||||
# paste-event handler and Submit never fires. 50ms is enough on a slow
|
||||
# Pi for the TUI to surface the paste event before the Enter arrives.
|
||||
_SUBMIT_KEY_DELAY = 0.05
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PtyProcessOptions:
|
||||
@@ -358,16 +365,21 @@ class PtyClaudeProcess:
|
||||
async def write(self, data: str | bytes, *, newline: bool = True) -> int:
|
||||
r"""Write bytes to the child's stdin.
|
||||
|
||||
Strings are UTF-8 encoded. When `newline=True` (the default) the
|
||||
payload is wrapped in xterm bracketed-paste markers (`ESC [ 200 ~`
|
||||
... `ESC [ 201 ~`) and followed by a carriage return — that is the
|
||||
Enter-key keycode interactive `claude` expects.
|
||||
Strings are UTF-8 encoded. When `newline=True` (the default):
|
||||
|
||||
Without the bracketed-paste framing, the TUI heuristically treats
|
||||
bursts longer than ~63 bytes as a paste and *buffers* them in the
|
||||
input box without submitting; the trailing `\r` is then absorbed
|
||||
as a newline inside the box rather than acting as Submit.
|
||||
Bracketed paste makes the framing explicit for any length payload.
|
||||
1. The payload is wrapped in xterm bracketed-paste markers
|
||||
(`ESC [ 200 ~` ... `ESC [ 201 ~`) and written first. Without
|
||||
that framing, the TUI heuristically treats bursts longer than
|
||||
~63 bytes as a paste and *buffers* them in the input box
|
||||
without submitting.
|
||||
2. A short pause (`_SUBMIT_KEY_DELAY`) gives the TUI's stdin
|
||||
reader a chance to surface the paste as a discrete event.
|
||||
Claude's TUI (Ink-based) reads stdin in chunks; if a `\r`
|
||||
is glued to the closing `ESC [ 201 ~`, the paste-event handler
|
||||
consumes the whole chunk and the Enter is dropped — the
|
||||
message lands in the input box but never submits.
|
||||
3. A separate write sends the `\r` as a fresh keystroke that
|
||||
triggers Submit.
|
||||
|
||||
Callers that need raw byte streaming (e.g. arrow keys, individual
|
||||
keypresses) pass `newline=False` and write the framing themselves.
|
||||
@@ -376,12 +388,16 @@ class PtyClaudeProcess:
|
||||
msg = "PtyClaudeProcess not started"
|
||||
raise RuntimeError(msg)
|
||||
payload = data.encode("utf-8") if isinstance(data, str) else bytes(data)
|
||||
if newline:
|
||||
if payload.endswith(b"\r"):
|
||||
payload = payload[:-1]
|
||||
payload = b"\x1b[200~" + payload + b"\x1b[201~\r"
|
||||
pty = self._pty
|
||||
return await asyncio.to_thread(pty.write, payload)
|
||||
if not newline:
|
||||
return await asyncio.to_thread(pty.write, payload)
|
||||
if payload.endswith(b"\r"):
|
||||
payload = payload[:-1]
|
||||
paste_chunk = b"\x1b[200~" + payload + b"\x1b[201~"
|
||||
n1 = await asyncio.to_thread(pty.write, paste_chunk)
|
||||
await asyncio.sleep(_SUBMIT_KEY_DELAY)
|
||||
n2 = await asyncio.to_thread(pty.write, b"\r")
|
||||
return n1 + n2
|
||||
|
||||
async def send_control(self, char: str) -> None:
|
||||
"""Send a control character (e.g. 'c' for Ctrl-C, 'd' for Ctrl-D)."""
|
||||
|
||||
Reference in New Issue
Block a user