fix(pty): drain late terminal query reply on teardown#1258
Conversation
PR Review SummarySize
Affected crates
Blast radius — ContainedThis PR touches: source code Updated automatically on each push to this PR. |
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to discard late terminal query replies (such as cursor-position reports) during final teardown to prevent them from leaking into the shell prompt. This is achieved by waiting briefly via poll and flushing the input queue using tcflush. Feedback is provided regarding a potential race condition where tcflush might be called before the entire multi-byte escape sequence has fully arrived, suggesting a brief sleep and a check on pfd.revents before flushing.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if unsafe { libc::poll(&mut pfd, 1, timeout_ms) } > 0 { | ||
| // SAFETY: `fd` is the live terminal fd for the duration of the call. | ||
| let tty = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }; | ||
| let _ = nix::sys::termios::tcflush(tty, nix::sys::termios::FlushArg::TCIFLUSH); | ||
| } |
There was a problem hiding this comment.
When poll returns > 0, it indicates that at least one byte of the terminal query reply (such as the initial ESC character) has arrived in the input queue. However, because the terminal query reply is a multi-byte sequence (e.g., ESC[3;1R), calling tcflush immediately might only discard the first few bytes if the remaining bytes are still in transit or have not yet been processed by the OS tty layer. This race condition can result in the tail of the sequence (like 3;1R) still being left in the input queue and handed to the shell.
To prevent this, we should:
- Verify that
POLLINis actually set inpfd.reventsto ensure we only proceed when there is readable data. - Introduce a very brief sleep (e.g., 10ms) after
pollsucceeds to allow the entire multi-byte sequence to be fully received before flushing.
if unsafe { libc::poll(&mut pfd, 1, timeout_ms) } > 0 && (pfd.revents & libc::POLLIN) != 0 {
// Sleep briefly to ensure the entire multi-byte escape sequence has arrived in the input queue.
std::thread::sleep(Duration::from_millis(10));
// SAFETY: fd is the live terminal fd for the duration of the call.
let tty = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
let _ = nix::sys::termios::tcflush(tty, nix::sys::termios::FlushArg::TCIFLUSH);
}6bff299 to
41fe77c
Compare
Signed-off-by: Florian Müller <florian@tomueller.de>
…view Signed-off-by: Florian Müller <florian@tomueller.de>
41fe77c to
92d7874
Compare
Linked Issue
Closes #1257
Summary
Quitting an interactive TUI (e.g. Claude Code) under
nono runleft stray characters such as3;1Rin the next shell prompt.3;1Ris the tail of a terminal Cursor Position Report (ESC [ 3 ; 1 R), the reply to aESC[6nquery the TUI emits during shutdown. The proxy forwards the query to the terminal, but the reply round-trips back after the child has exited, so nothing on the PTY consumes it.release_terminal_for_promptrestores cooked mode withtcsetattr(..., TCSANOW, ...), which does not flush the input queue, so the stray reply is handed to the shell.A bare
tcflushdoes not help: the reply has usually not arrived at that instant. The fix waits for it, then flushes.Agent Disclosure (if applicable)
This change was prepared with the assistance of an AI coding agent (Claude Code), reviewed, altered and applied by the human contributor.
Test Plan
make fmt-checkmake clippymake test-clinono run -- claude,/q, confirmed no3;1Rin the shell prompt.Checklist
CHANGELOG.mdif needed