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_DRAIN_CHUNK = 65536
|
||||||
_DEFAULT_OUTPUT_BUFFER_CAP = 1_000_000
|
_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)
|
@dataclass(frozen=True)
|
||||||
class PtyProcessOptions:
|
class PtyProcessOptions:
|
||||||
@@ -358,16 +365,21 @@ class PtyClaudeProcess:
|
|||||||
async def write(self, data: str | bytes, *, newline: bool = True) -> int:
|
async def write(self, data: str | bytes, *, newline: bool = True) -> int:
|
||||||
r"""Write bytes to the child's stdin.
|
r"""Write bytes to the child's stdin.
|
||||||
|
|
||||||
Strings are UTF-8 encoded. When `newline=True` (the default) the
|
Strings are UTF-8 encoded. When `newline=True` (the default):
|
||||||
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.
|
|
||||||
|
|
||||||
Without the bracketed-paste framing, the TUI heuristically treats
|
1. The payload is wrapped in xterm bracketed-paste markers
|
||||||
bursts longer than ~63 bytes as a paste and *buffers* them in the
|
(`ESC [ 200 ~` ... `ESC [ 201 ~`) and written first. Without
|
||||||
input box without submitting; the trailing `\r` is then absorbed
|
that framing, the TUI heuristically treats bursts longer than
|
||||||
as a newline inside the box rather than acting as Submit.
|
~63 bytes as a paste and *buffers* them in the input box
|
||||||
Bracketed paste makes the framing explicit for any length payload.
|
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
|
Callers that need raw byte streaming (e.g. arrow keys, individual
|
||||||
keypresses) pass `newline=False` and write the framing themselves.
|
keypresses) pass `newline=False` and write the framing themselves.
|
||||||
@@ -376,12 +388,16 @@ class PtyClaudeProcess:
|
|||||||
msg = "PtyClaudeProcess not started"
|
msg = "PtyClaudeProcess not started"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
payload = data.encode("utf-8") if isinstance(data, str) else bytes(data)
|
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
|
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:
|
async def send_control(self, char: str) -> None:
|
||||||
"""Send a control character (e.g. 'c' for Ctrl-C, 'd' for Ctrl-D)."""
|
"""Send a control character (e.g. 'c' for Ctrl-C, 'd' for Ctrl-D)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user