fix: better "enter" after pasted text

This commit is contained in:
h
2026-05-23 00:31:29 +02:00
parent b5e0166c48
commit 3394617163
+80 -4
View File
@@ -59,9 +59,26 @@ _SNAPSHOT_INTERVAL = 10.0
# 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
# paste-event handler and Submit never fires. 250ms is conservative for
# a slow Pi under load — observed 50ms races on opus-high turns where
# the paste handler is still scheduling when Enter arrives.
_SUBMIT_KEY_DELAY = 0.25
# After sending Enter we poll the PTY output for the paste-indicator
# (claude's TUI renders ``[Pasted text #...`` while the paste is still
# buffered in the input box). If it's still there after this window,
# Enter never registered — we retry up to ``_SUBMIT_RETRIES`` times.
_SUBMIT_VERIFY_WINDOW = 1.5
_SUBMIT_VERIFY_POLL = 0.1
_SUBMIT_RETRIES = 3
_PASTE_INDICATOR = b"[Pasted text"
# Size of the most-recent slice of PTY output considered "the current
# screen render" when verifying a submit. The TUI emits a full clear +
# redraw per state change; the latest redraw fits comfortably in this
# window, while earlier intermediate frames (which may still show the
# paste indicator) live further back in the same buffer.
_TUI_TAIL_WINDOW = 8192
@dataclass(frozen=True)
@@ -326,6 +343,35 @@ class PtyClaudeProcess:
if overflow > 0:
del self._output_buffer[:overflow]
async def _wait_for_submit(self, pre_enter_len: int) -> bool:
"""Poll the PTY output to confirm Enter actually submitted the paste.
Claude's TUI renders ``[Pasted text #N +M lines]`` in the input
box while the paste is still buffered (unsubmitted). After Enter
registers, the input clears and the TUI redraws — the next
screen render no longer contains the indicator.
We compare bytes written *after* the Enter write (``pre_enter_len``
was snapshotted just before). Within those new bytes, only the
most recent ``_TUI_TAIL_WINDOW`` matter — that's the current
rendered screen; earlier bytes are intermediate redraws (one of
which would still show the indicator).
Returns True once the indicator is absent from the recent tail,
False on timeout.
"""
deadline = asyncio.get_running_loop().time() + _SUBMIT_VERIFY_WINDOW
while asyncio.get_running_loop().time() < deadline:
buf = self.captured_output()
new_bytes = buf[pre_enter_len:]
tail = new_bytes[-_TUI_TAIL_WINDOW:]
# Only check once the TUI has produced at least one redraw
# after our Enter (otherwise tail is empty / pre-render).
if tail and _PASTE_INDICATOR not in tail:
return True
await asyncio.sleep(_SUBMIT_VERIFY_POLL)
return False
async def _snapshot_loop(self, snapshot_dir: pathlib.Path) -> None:
"""Periodically dump the captured PTY buffer to a file.
@@ -530,7 +576,37 @@ class PtyClaudeProcess:
)
n1 = await asyncio.to_thread(pty.write, paste_chunk)
await asyncio.sleep(_SUBMIT_KEY_DELAY)
n2 = await asyncio.to_thread(pty.write, b"\r")
n2 = 0
for attempt in range(1, _SUBMIT_RETRIES + 1):
# Snapshot just before each Enter write so the verifier only
# looks at bytes the TUI produces in response to *this*
# Enter, not earlier renders (which still show the paste
# indicator from when the paste was first buffered).
pre_enter_len = len(self.captured_output())
n2 += await asyncio.to_thread(pty.write, b"\r")
submitted = await self._wait_for_submit(pre_enter_len)
if submitted:
if attempt > 1:
_log.info(
"write: session_id=%s SUBMIT registered after %d Enter(s)",
self._session_id,
attempt,
)
break
_log.warning(
"write: session_id=%s Enter #%d did not clear paste indicator "
"(buf=%d bytes) — retrying",
self._session_id,
attempt,
len(self.captured_output()),
)
else:
_log.error(
"write: session_id=%s SUBMIT never registered after %d "
"Enter attempts — paste likely stuck in input box",
self._session_id,
_SUBMIT_RETRIES,
)
_log.info(
"write: session_id=%s SUBMIT done (paste=%d Enter=%d)",
self._session_id,