Skip to content

fix(pty): respond to DSR cursor position queries for inline TUI compatibility#9

Open
cavanaug wants to merge 2 commits intomsmps:mainfrom
cavanaug:fix/dsr-cursor-response-inline
Open

fix(pty): respond to DSR cursor position queries for inline TUI compatibility#9
cavanaug wants to merge 2 commits intomsmps:mainfrom
cavanaug:fix/dsr-cursor-response-inline

Conversation

@cavanaug
Copy link

Summary

  • Add PTY-side handling for Device Status Report cursor-position requests (ESC[6n) so apps running in Pilotty can receive a valid cursor-position response (ESC[{row};{col}R).
  • Track cursor position from PTY output stream and emit synthetic DSR replies through the existing PTY input writer path.
  • Add focused tests for DSR detection (including chunk boundaries), response coordinate correctness (1-indexed), and end-to-end async PTY response behavior.

Problem

Apps using inline terminal viewports (e.g. ratatui Viewport::Inline, similar to Atuin-style overlays) depend on cursor-position queries succeeding during startup/rendering.

In Pilotty sessions, these apps could fail with errors equivalent to:

  • The cursor position could not be read within a normal duration

Root cause: PTY output was parsed for display/snapshot state but DSR cursor queries (ESC[6n) were not answered, so querying apps timed out waiting for terminal response.

Solution Approach

  • Introduce a small DSR parser in PTY read path to detect ESC[6n reliably, including split/chunked sequences.
  • Keep a cursor tracker (vt100-backed) updated from PTY output.
  • On DSR query, enqueue a synthetic response ESC[{row};{col}R (1-indexed terminal coordinates) back to child stdin via the existing write channel.
  • Keep tracker dimensions synchronized on resize.

Validation

  • cargo test -p pilotty-cli daemon::pty::tests
  • cargo test -p pilotty-cli

Both pass locally.

Copilot AI review requested due to automatic review settings March 13, 2026 05:26
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds PTY-side support for answering Device Status Report cursor-position queries (ESC[6n) so inline/overlay TUIs can successfully query the cursor position when running under Pilotty.

Changes:

  • Introduces a CursorTracker (vt100-backed) to track cursor position and detect ESC[6n across chunk boundaries.
  • On detecting DSR queries in the PTY output stream, enqueues synthetic cursor-position replies (ESC[{row};{col}R) back to child stdin via the existing write channel.
  • Adds unit tests for DSR detection and 1-indexed coordinates, plus an async end-to-end test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +37 to +40
fn process_output(&mut self, bytes: &[u8]) -> usize {
self.parser.process(bytes);
self.count_dsr_queries(bytes)
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated CursorTracker::process_output to process output incrementally and capture (row, col) at each detected ESC[6n boundary. It now returns Vec<(u16, u16)>, so DSR replies use the cursor position at query time even when trailing bytes exist in the same chunk.

Comment on lines +343 to +358
let dsr_queries = match cursor_tracker.lock() {
Ok(mut tracker) => tracker.process_output(&buf[..n]),
Err(_) => {
warn!("Cursor tracker mutex poisoned; skipping DSR detection");
0
}
};

if dsr_queries > 0 {
let (row, col) = match cursor_tracker.lock() {
Ok(tracker) => tracker.cursor_position_one_indexed(),
Err(_) => {
warn!("Cursor tracker mutex poisoned; using fallback cursor response");
(1, 1)
}
};
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to a single lock per read chunk. reader_loop now calls process_output once, receives all DSR boundary positions, and emits responses from that vector. This removes the previous double-lock and keeps parse/position capture consistent.

Comment on lines +361 to +364
for _ in 0..dsr_queries {
if write_tx.blocking_send(response.clone()).is_err() {
debug!("PTY write channel closed while sending DSR response");
break;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed synthetic DSR enqueue from blocking_send to try_send. On full channel, we log and drop the synthetic response rather than blocking the reader thread; on closed channel, we stop sending. This keeps PTY output draining even under backpressure.

Comment on lines +673 to +679
let output = String::from_utf8_lossy(&bytes);
assert!(
output.contains("ok:"),
"Expected shell to receive DSR response, got: {:?}",
output
);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strengthened the async E2E test to validate the actual cursor report. The test now writes a known sequence (abc + ESC[6n + xyz), parses ok:\x1b[{row};{col}, and asserts (row, col) == (1, 4).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants