fix: better "enter" after pasted text
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user