Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -237,44 +235,39 @@ impl Perform for AnsiCleaner {
}
'H' | 'f' => {
// \033[<row>;<col>H or \033[<row>;<col>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
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/crates/core/src/service/mcp/config/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,6 @@ impl MCPConfigService {
&mut id_index,
);

info!("Loaded {} MCP server config(s)", configs.len());
Ok(configs)
}

Expand Down
57 changes: 53 additions & 4 deletions src/crates/core/src/service/terminal/src/pty/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))]
Expand Down Expand Up @@ -627,6 +627,34 @@ pub fn spawn_pty(
})
}

fn apply_sanitized_environment(
cmd: &mut CommandBuilder,
overlay_env: &std::collections::HashMap<String, String>,
) {
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)
// ============================================================================
Expand All @@ -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()));
}
}
Loading