Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/crates/agent-tui-infra/src/infra/daemon/pty_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ impl PtySession {
Self { handle }
}

pub fn writer_handle(
&self,
) -> std::sync::Arc<std::sync::Mutex<Box<dyn std::io::Write + Send>>> {
self.handle.writer_handle()
}

pub fn pid(&self) -> Option<u32> {
self.handle.pid()
}
Expand Down
33 changes: 32 additions & 1 deletion cli/crates/agent-tui-infra/src/infra/daemon/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,36 @@ fn sanitize_command_timeline_value(value: &str) -> String {
}
}

/// Adapter that wraps an `Arc<Mutex<Box<dyn Write>>>` into a `Write` impl,
/// allowing the virtual terminal to write query responses (DSR/CPR) back to
/// the child process through the shared PTY writer.
struct ArcWriter(Arc<Mutex<Box<dyn std::io::Write + Send>>>);

impl std::io::Write for ArcWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut guard = mutex_lock_or_recover(&self.0);
let mut offset = 0;
while offset < buf.len() {
match guard.write(&buf[offset..]) {
Ok(0) => {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"PTY writer closed",
));
}
Ok(n) => offset += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(buf.len())
}

fn flush(&mut self) -> std::io::Result<()> {
mutex_lock_or_recover(&self.0).flush()
}
}

pub struct Session {
pub id: SessionId,
pub command: String,
Expand All @@ -600,12 +630,13 @@ impl Session {
let stream = Arc::new(StreamBuffer::new(STREAM_MAX_BUFFER_BYTES));
let mut pty = PtySession::new(pty);
let pty_rx = pty.take_read_rx();
let pty_writer: Box<dyn std::io::Write + Send> = Box::new(ArcWriter(pty.writer_handle()));
Self {
id,
command,
created_at: Utc::now(),
pty,
terminal: TerminalState::new(cols, rows),
terminal: TerminalState::new(cols, rows, pty_writer),
held_modifiers: ModifierState::default(),
stream,
command_timeline: CommandTimeline::default(),
Expand Down
4 changes: 2 additions & 2 deletions cli/crates/agent-tui-infra/src/infra/daemon/terminal_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ pub struct TerminalState {
}

impl TerminalState {
pub fn new(cols: u16, rows: u16) -> Self {
pub fn new(cols: u16, rows: u16, writer: Box<dyn std::io::Write + Send>) -> Self {
Self {
terminal: VirtualTerminal::new(cols, rows),
terminal: VirtualTerminal::new(cols, rows, writer),
}
}

Expand Down
6 changes: 6 additions & 0 deletions cli/crates/agent-tui-infra/src/infra/terminal/pty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ impl PtyHandle {
.unwrap_or(false)
}

/// Returns a clone of the PTY writer handle for feeding terminal responses
/// (e.g. DSR/CPR) back to the child process.
pub fn writer_handle(&self) -> Arc<Mutex<Box<dyn Write + Send>>> {
Arc::clone(&self.writer)
}

pub fn write(&self, data: &[u8]) -> Result<(), PtyError> {
if data.is_empty() {
return Ok(());
Expand Down
57 changes: 52 additions & 5 deletions cli/crates/agent-tui-infra/src/infra/terminal/vterm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub struct VirtualTerminal {
}

impl VirtualTerminal {
pub fn new(cols: u16, rows: u16) -> Self {
pub fn new(cols: u16, rows: u16, writer: Box<dyn io::Write + Send>) -> Self {
let size = TerminalSize {
rows: rows as usize,
cols: cols as usize,
Expand All @@ -82,7 +82,6 @@ impl VirtualTerminal {
};
let config: Arc<dyn TerminalConfiguration + Send + Sync> =
Arc::new(DefaultTerminalConfig::default());
let writer: Box<dyn io::Write + Send> = Box::new(io::sink());
let terminal = Terminal::new(size, config, "agent-tui", env!("CARGO_PKG_VERSION"), writer);
Self {
terminal,
Expand Down Expand Up @@ -274,15 +273,15 @@ mod tests {

#[test]
fn test_basic_terminal() {
let mut term = VirtualTerminal::new(80, 24);
let mut term = VirtualTerminal::new(80, 24, Box::new(io::sink()));
term.process(b"Hello, World!");
let text = term.screen_text();
assert!(text.contains("Hello, World!"));
}

#[test]
fn test_cursor_position() {
let mut term = VirtualTerminal::new(80, 24);
let mut term = VirtualTerminal::new(80, 24, Box::new(io::sink()));
term.process(b"ABC");
let cursor = term.cursor();
assert_eq!(cursor.col, 3);
Expand All @@ -291,11 +290,59 @@ mod tests {

#[test]
fn test_screen_buffer() {
let mut term = VirtualTerminal::new(80, 24);
let mut term = VirtualTerminal::new(80, 24, Box::new(io::sink()));
term.process(b"\x1b[1mBold\x1b[0m Normal");
let buffer = term.screen_buffer();

assert!(buffer.cells[0][0].style.bold);
assert_eq!(buffer.cells[0][0].char, 'B');
}

#[test]
fn test_dsr_cursor_position_response() {
// DSR (Device Status Report) with param 6 requests cursor position.
// The terminal should write back a CPR (Cursor Position Report).
// wezterm may write the response on an internal thread, so we
// poll briefly for the result.
let writer = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
let writer_clone = Arc::clone(&writer);
let boxed: Box<dyn io::Write + Send> = Box::new(WriterSpy(writer_clone));

let mut term = VirtualTerminal::new(80, 24, boxed);
// Move cursor to row 3, col 5 (1-indexed)
term.process(b"\x1b[3;5H");
// Send DSR query
term.process(b"\x1b[6n");

let expected = b"\x1b[3;5R";
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
let output = writer.lock().unwrap();
if output.windows(expected.len()).any(|w| w == expected) {
break; // success
}
drop(output);
if std::time::Instant::now() >= deadline {
let output = writer.lock().unwrap();
panic!(
"expected CPR response '\\x1b[3;5R' within 2s, got {:?}",
String::from_utf8_lossy(&output)
);
}
std::hint::spin_loop();
}
}

/// Helper that captures writes into a shared Vec.
struct WriterSpy(Arc<std::sync::Mutex<Vec<u8>>>);

impl io::Write for WriterSpy {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
}