diff --git a/src/crates/core/src/agentic/tools/implementations/tool-runtime/src/util/ansi_cleaner.rs b/src/crates/core/src/agentic/tools/implementations/tool-runtime/src/util/ansi_cleaner.rs index 635e5bb3..34aec7dd 100644 --- a/src/crates/core/src/agentic/tools/implementations/tool-runtime/src/util/ansi_cleaner.rs +++ b/src/crates/core/src/agentic/tools/implementations/tool-runtime/src/util/ansi_cleaner.rs @@ -56,14 +56,7 @@ impl AnsiCleaner { parser.advance(self, input.as_bytes()); // Save last line if it has content if !self.current_line.is_empty() { - while self.lines.len() <= self.cursor_row { - self.lines.push(String::new()); - self.line_is_real.push(false); - } - self.lines[self.cursor_row] = std::mem::take(&mut self.current_line); - // Non-empty lines are always included regardless of is_real, - // but mark true for consistency. - self.line_is_real[self.cursor_row] = true; + self.save_current_line(true); } self.build_output() } @@ -74,12 +67,7 @@ impl AnsiCleaner { parser.advance(self, input); // Save last line if it has content if !self.current_line.is_empty() { - while self.lines.len() <= self.cursor_row { - self.lines.push(String::new()); - self.line_is_real.push(false); - } - self.lines[self.cursor_row] = std::mem::take(&mut self.current_line); - self.line_is_real[self.cursor_row] = true; + self.save_current_line(true); } self.build_output() } @@ -108,6 +96,21 @@ impl AnsiCleaner { result.join("\n") } + fn ensure_row_exists(&mut self, row: usize) { + while self.lines.len() <= row { + self.lines.push(String::new()); + self.line_is_real.push(false); + } + } + + fn save_current_line(&mut self, is_real: bool) { + self.ensure_row_exists(self.cursor_row); + self.lines[self.cursor_row] = std::mem::take(&mut self.current_line); + if is_real { + self.line_is_real[self.cursor_row] = true; + } + } + /// Reset the cleaner state for reuse. pub fn reset(&mut self) { self.lines.clear(); @@ -159,14 +162,9 @@ impl Perform for AnsiCleaner { // Line feed: move to next line. // Intermediate rows pushed here are phantom (cursor jumped over them // via a prior B/H sequence without writing). - while self.lines.len() <= self.cursor_row { - self.lines.push(String::new()); - self.line_is_real.push(false); - } // Save current line content at cursor_row position (overwrites if exists). // Mark as real: \n explicitly visited this row. - self.lines[self.cursor_row] = std::mem::take(&mut self.current_line); - self.line_is_real[self.cursor_row] = true; + self.save_current_line(true); self.cursor_col = 0; self.cursor_row += 1; self.line_cleared = false; @@ -237,44 +235,39 @@ impl Perform for AnsiCleaner { } 'H' | 'f' => { // \033[;H or \033[;f - Cursor Position - // Get row parameter (first) and column parameter (second) - let row = param; + // + // For terminal log cleanup we prefer reconstructing linear text over + // faithfully emulating the viewport. ConPTY often emits absolute screen + // coordinates here, which can reuse the same screen rows after scrolling. + // Treat column 1 as "start a fresh logical line"; treat high-column + // positions immediately after a line break as a wrapped continuation of + // the previous logical line. let col = params .iter() .nth(1) .and_then(|group| group.iter().next()) .unwrap_or(&1u16); - - // Move to specified row (1-based) - let target_row = row.saturating_sub(1) as usize; let target_col = col.saturating_sub(1) as usize; - // If we need to move to a different row, handle current line first - if target_row != self.cursor_row { + if target_col == 0 { if !self.current_line.is_empty() { - // Ensure lines has enough space; new slots are phantom. - while self.lines.len() <= self.cursor_row { - self.lines.push(String::new()); - self.line_is_real.push(false); - } - self.lines[self.cursor_row] = std::mem::take(&mut self.current_line); - // Line has content so it will always be included; - // no need to update line_is_real here. - } - - // Fill rows between current position and target with phantom entries. - // These rows are never written to by \n and are pure screen-layout - // artifacts of the absolute-positioning sequence. - while self.lines.len() <= target_row { - self.lines.push(String::new()); - self.line_is_real.push(false); + self.save_current_line(true); + self.cursor_row += 1; } + self.ensure_row_exists(self.cursor_row); + self.current_line = self.lines[self.cursor_row].clone(); + self.cursor_col = 0; + return; + } - // Load the target row - self.current_line = self.lines[target_row].clone(); + if self.current_line.is_empty() && self.cursor_row > 0 { + self.cursor_row -= 1; + self.ensure_row_exists(self.cursor_row); + self.current_line = self.lines[self.cursor_row].clone(); + self.cursor_col = self.current_line.len().saturating_sub(1); + return; } - self.cursor_row = target_row; self.cursor_col = target_col; } // Erase sequences @@ -475,9 +468,7 @@ mod tests { #[test] fn test_cursor_position() { - // \x1b[5;1H moves cursor to row 5, column 1. - // Rows 1-4 are phantom (H-jump fillers, never written by \n), so they - // are omitted from output. Only real content rows are included. + // \x1b[5;1H is treated as moving to the next logical line. let input = "Header\x1b[5;1HNew content"; assert_eq!(strip_ansi(input), "Header\nNew content"); } @@ -512,6 +503,35 @@ mod tests { assert_eq!(strip_ansi(input), expected_output); } + #[test] + fn test_cursor_position_soft_wrap_continuation() { + let input = + "This will stop working in the next maj\r\n\x1b[49;83Hjor version of npm.\r\nNext line"; + assert_eq!( + strip_ansi(input), + "This will stop working in the next major version of npm.\nNext line" + ); + } + + #[test] + fn test_cursor_position_reused_screen_rows_do_not_overwrite_previous_lines() { + let input = concat!( + "First warning line ends in maj\r\n", + "\x1b[49;83Hjor version one.\r\n", + "Second warning line ends in maj\r\n", + "\x1b[49;83Hjor version two.\r\n", + "Final line" + ); + assert_eq!( + strip_ansi(input), + concat!( + "First warning line ends in major version one.\n", + "Second warning line ends in major version two.\n", + "Final line" + ) + ); + } + #[test] fn test_multibyte_chinese_basic() { // Basic Chinese text should pass through correctly diff --git a/src/crates/core/src/service/mcp/config/service.rs b/src/crates/core/src/service/mcp/config/service.rs index aaabc188..13c7b51c 100644 --- a/src/crates/core/src/service/mcp/config/service.rs +++ b/src/crates/core/src/service/mcp/config/service.rs @@ -218,7 +218,6 @@ impl MCPConfigService { &mut id_index, ); - info!("Loaded {} MCP server config(s)", configs.len()); Ok(configs) } diff --git a/src/crates/core/src/service/terminal/src/pty/process.rs b/src/crates/core/src/service/terminal/src/pty/process.rs index c895b046..ee823294 100644 --- a/src/crates/core/src/service/terminal/src/pty/process.rs +++ b/src/crates/core/src/service/terminal/src/pty/process.rs @@ -14,6 +14,7 @@ //! - Special handling for Git Bash (longer delay needed) //! - Resize confirmation events for frontend synchronization +use std::ffi::OsStr; use std::io::{Read, Write}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; @@ -362,10 +363,9 @@ pub fn spawn_pty( }); cmd.cwd(&cwd); - // Set environment variables - for (key, value) in &shell_config.env { - cmd.env(key, value); - } + // Sanitize inherited host environment to avoid leaking Tauri dev/build + // configuration into user terminals, then overlay terminal-specific env. + apply_sanitized_environment(&mut cmd, &shell_config.env); // Set terminal type #[cfg(not(windows))] @@ -627,6 +627,34 @@ pub fn spawn_pty( }) } +fn apply_sanitized_environment( + cmd: &mut CommandBuilder, + overlay_env: &std::collections::HashMap, +) { + cmd.env_clear(); + + for (key, value) in std::env::vars_os() { + if should_preserve_parent_env(&key) { + cmd.env(&key, &value); + } + } + + for (key, value) in overlay_env { + cmd.env(key, value); + } +} + +fn should_preserve_parent_env(key: &OsStr) -> bool { + !is_tauri_host_env(key) +} + +fn is_tauri_host_env(key: &OsStr) -> bool { + let key = key.to_string_lossy().to_ascii_uppercase(); + key == "TAURI_CONFIG" + || key.starts_with("TAURI_ENV_") + || key.starts_with("TAURI_ANDROID_PACKAGE_NAME_") +} + // ============================================================================ // Legacy compatibility - PtyCommand enum (for external use if needed) // ============================================================================ @@ -643,3 +671,24 @@ pub enum PtyCommand { /// Shutdown the process Shutdown { immediate: bool }, } + +#[cfg(test)] +mod tests { + use super::is_tauri_host_env; + + #[test] + fn strips_tauri_host_configuration_from_parent_env() { + assert!(is_tauri_host_env("TAURI_CONFIG".as_ref())); + assert!(is_tauri_host_env("TAURI_ENV_TARGET_TRIPLE".as_ref())); + assert!(is_tauri_host_env( + "TAURI_ANDROID_PACKAGE_NAME_PREFIX".as_ref() + )); + } + + #[test] + fn keeps_non_host_tauri_and_normal_env_vars() { + assert!(!is_tauri_host_env("TAURI_PRIVATE_KEY".as_ref())); + assert!(!is_tauri_host_env("PATH".as_ref())); + assert!(!is_tauri_host_env("TERMINAL_NONCE".as_ref())); + } +}