From 76fdb6f3d740071283d9e82c902dfb54d58bd4a8 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:48:28 +0000 Subject: [PATCH 1/2] fix(cli): restore terminal state after Ctrl+C in interactive commands Agent-Logs-Url: https://github.com/voidzero-dev/vite-plus/sessions/a9e99ef9-0713-4570-8c7a-d2cd4752e1c3 Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> --- crates/vite_command/Cargo.toml | 2 +- crates/vite_command/src/lib.rs | 73 +++++++++++++++++++++++ packages/cli/binding/src/cli/execution.rs | 20 +++++-- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/crates/vite_command/Cargo.toml b/crates/vite_command/Cargo.toml index 58bf046da2..a50bf56073 100644 --- a/crates/vite_command/Cargo.toml +++ b/crates/vite_command/Cargo.toml @@ -16,7 +16,7 @@ vite_path = { workspace = true } which = { workspace = true, features = ["tracing"] } [target.'cfg(not(target_os = "windows"))'.dependencies] -nix = { workspace = true } +nix = { workspace = true, features = ["term"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/vite_command/src/lib.rs b/crates/vite_command/src/lib.rs index f1789ce063..f705d7ff64 100644 --- a/crates/vite_command/src/lib.rs +++ b/crates/vite_command/src/lib.rs @@ -10,6 +10,9 @@ use tokio_util::sync::CancellationToken; use vite_error::Error; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; +#[cfg(unix)] +use std::os::fd::{BorrowedFd, RawFd}; + /// Result of running a command with fspy tracking. #[derive(Debug)] pub struct FspyCommandResult { @@ -59,6 +62,32 @@ pub fn build_command(bin_path: &AbsolutePath, cwd: &AbsolutePath) -> Command { cmd } +/// Execute a command while preserving terminal state. +/// This prevents escape sequences from appearing in the prompt when the child process +/// is interrupted (e.g., via Ctrl+C) while the terminal is in a non-standard state. +/// +/// On Unix, saves the terminal state before spawning the child process and restores +/// it after the child exits. On Windows, this is a simple pass-through. +pub async fn execute_with_terminal_guard(mut cmd: Command) -> Result { + #[cfg(unix)] + { + use nix::libc::STDIN_FILENO; + + // Save terminal state before spawning child + let _guard = TerminalStateGuard::save(STDIN_FILENO); + + // Spawn and wait for child - guard will restore terminal state on drop + let mut child = cmd.spawn().map_err(|e| Error::Anyhow(e.into()))?; + child.wait().await.map_err(|e| Error::Anyhow(e.into())) + } + + #[cfg(not(unix))] + { + let mut child = cmd.spawn().map_err(|e| Error::Anyhow(e.into()))?; + child.wait().await.map_err(|e| Error::Anyhow(e.into())) + } +} + /// Build a `tokio::process::Command` for shell execution. /// Uses `/bin/sh -c` on Unix, `cmd.exe /C` on Windows. pub fn build_shell_command(shell_cmd: &str, cwd: &AbsolutePath) -> Command { @@ -230,6 +259,50 @@ pub fn fix_stdio_streams() { clear_cloexec(unsafe { BorrowedFd::borrow_raw(STDERR_FILENO) }); } +/// Guard that saves terminal state and restores it on drop. +/// This prevents escape sequences from appearing in the prompt when a child process +/// is interrupted (e.g., via Ctrl+C) while the terminal is in a non-standard state. +#[cfg(unix)] +struct TerminalStateGuard { + fd: RawFd, + original: nix::sys::termios::Termios, +} + +#[cfg(unix)] +impl TerminalStateGuard { + /// Save the current terminal state for the given file descriptor. + /// Returns None if the fd is not a terminal or if saving fails. + fn save(fd: RawFd) -> Option { + use nix::sys::termios::tcgetattr; + + // SAFETY: fd comes from a valid stdin/stdout/stderr file descriptor + let borrowed_fd = unsafe { BorrowedFd::borrow_raw(fd) }; + + // Only save state if this is actually a terminal + if !nix::unistd::isatty(borrowed_fd).unwrap_or(false) { + return None; + } + + match tcgetattr(borrowed_fd) { + Ok(original) => Some(Self { fd, original }), + Err(_) => None, + } + } +} + +#[cfg(unix)] +impl Drop for TerminalStateGuard { + fn drop(&mut self) { + use nix::sys::termios::{SetArg, tcsetattr}; + + // SAFETY: fd comes from stdin/stdout/stderr and the guard does not outlive the process + let borrowed_fd = unsafe { BorrowedFd::borrow_raw(self.fd) }; + + // Best effort: ignore errors during cleanup + let _ = tcsetattr(borrowed_fd, SetArg::TCSANOW, &self.original); + } +} + #[cfg(test)] mod tests { use tempfile::{TempDir, tempdir}; diff --git a/packages/cli/binding/src/cli/execution.rs b/packages/cli/binding/src/cli/execution.rs index 887db64efc..9e84363a9d 100644 --- a/packages/cli/binding/src/cli/execution.rs +++ b/packages/cli/binding/src/cli/execution.rs @@ -57,12 +57,24 @@ pub(super) async fn resolve_and_execute( cwd: &AbsolutePathBuf, cwd_arc: &Arc, ) -> Result { - let mut cmd = + let is_interactive = matches!( + subcommand, + SynthesizableSubcommand::Dev { .. } | SynthesizableSubcommand::Preview { .. } + ); + + let cmd = resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) .await?; - let mut child = cmd.spawn().map_err(|e| Error::Anyhow(e.into()))?; - let status = child.wait().await.map_err(|e| Error::Anyhow(e.into()))?; - Ok(ExitStatus(status.code().unwrap_or(1) as u8)) + + // For interactive commands (dev, preview), use terminal guard to restore terminal state on exit + if is_interactive { + let status = vite_command::execute_with_terminal_guard(cmd).await?; + Ok(ExitStatus(status.code().unwrap_or(1) as u8)) + } else { + let mut child = cmd.spawn().map_err(|e| Error::Anyhow(e.into()))?; + let status = child.wait().await.map_err(|e| Error::Anyhow(e.into()))?; + Ok(ExitStatus(status.code().unwrap_or(1) as u8)) + } } pub(super) enum FilterStream { From 1bfd725a8614074d61296197ce0cb889b51fbb05 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:39:42 +0000 Subject: [PATCH 2/2] fix(cli): resolve compilation and formatting issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make cmd mutable in execution.rs to allow spawning child process - Move #[cfg(unix)] import to top of file for proper formatting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> --- crates/vite_command/src/lib.rs | 5 ++--- packages/cli/binding/src/cli/execution.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/vite_command/src/lib.rs b/crates/vite_command/src/lib.rs index f705d7ff64..9f724e3aac 100644 --- a/crates/vite_command/src/lib.rs +++ b/crates/vite_command/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(unix)] +use std::os::fd::{BorrowedFd, RawFd}; use std::{ collections::HashMap, ffi::OsStr, @@ -10,9 +12,6 @@ use tokio_util::sync::CancellationToken; use vite_error::Error; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; -#[cfg(unix)] -use std::os::fd::{BorrowedFd, RawFd}; - /// Result of running a command with fspy tracking. #[derive(Debug)] pub struct FspyCommandResult { diff --git a/packages/cli/binding/src/cli/execution.rs b/packages/cli/binding/src/cli/execution.rs index 9e84363a9d..e8840c2d23 100644 --- a/packages/cli/binding/src/cli/execution.rs +++ b/packages/cli/binding/src/cli/execution.rs @@ -62,7 +62,7 @@ pub(super) async fn resolve_and_execute( SynthesizableSubcommand::Dev { .. } | SynthesizableSubcommand::Preview { .. } ); - let cmd = + let mut cmd = resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) .await?;