diff --git a/crates/tui/src/tools/shell.rs b/crates/tui/src/tools/shell.rs index 9eb88b4020..c386301d65 100644 --- a/crates/tui/src/tools/shell.rs +++ b/crates/tui/src/tools/shell.rs @@ -37,6 +37,8 @@ use windows::core::PCWSTR; #[cfg(not(target_env = "ohos"))] use portable_pty::{CommandBuilder, PtySize, native_pty_system}; +mod output; + use super::shell_output::{summarize_output, truncate_with_meta}; use crate::child_env; use crate::sandbox::{ @@ -47,6 +49,7 @@ use crate::sandbox::{ SandboxType, }; use crate::worker_profile::ShellPolicy; +use output::{tail_from_buffer, take_delta_from_buffer}; /// Status of a shell process #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -1854,54 +1857,6 @@ impl ShellManager { } } -fn take_delta_from_buffer(buffer: &Arc>>, cursor: &mut usize) -> (Vec, usize) { - let guard = buffer.lock().unwrap_or_else(|e| e.into_inner()); - let total = guard.len(); - let start = (*cursor).min(total); - // Clone only the unread portion (the delta), not the entire accumulated buffer. - // Long-running processes can produce megabytes of output; cloning the full - // buffer on every poll held the ShellManager mutex for O(total_bytes) time. - let delta = guard[start..].to_vec(); - *cursor = total; - (delta, total) -} - -/// Read only the tail of a byte buffer and return (total_len, tail_string). -/// -/// Avoids cloning the full buffer when only a trailing excerpt is needed -/// (e.g. for the job-panel display). `max_tail_chars` is in Unicode scalar -/// values; we read at most `max_tail_chars * 4` bytes from the end to account -/// for multi-byte UTF-8 sequences. -fn tail_from_buffer(buffer: &Arc>>, max_tail_chars: usize) -> (usize, String) { - let guard = buffer.lock().unwrap_or_else(|e| e.into_inner()); - let total = guard.len(); - // Over-estimate byte count (4 bytes per char worst case for UTF-8). - let mut tail_start = total.saturating_sub(max_tail_chars.saturating_mul(4)); - // Snap forward to the next valid UTF-8 codepoint boundary so we don't - // pass a slice beginning with continuation bytes (0x80–0xBF) to - // from_utf8_lossy, which would emit a leading U+FFFD replacement char. - while tail_start < total && (guard[tail_start] & 0xC0) == 0x80 { - tail_start += 1; - } - let tail_str = String::from_utf8_lossy(&guard[tail_start..]).into_owned(); - (total, tail_text(&tail_str, max_tail_chars)) -} - -fn tail_text(text: &str, max_chars: usize) -> String { - if text.chars().count() <= max_chars { - return text.to_string(); - } - let tail = text - .chars() - .rev() - .take(max_chars) - .collect::>() - .into_iter() - .rev() - .collect::(); - format!("...{tail}") -} - fn job_status_rank(status: &ShellStatus, stale: bool) -> u8 { if stale { return 4; diff --git a/crates/tui/src/tools/shell/output.rs b/crates/tui/src/tools/shell/output.rs new file mode 100644 index 0000000000..8cce1eb16b --- /dev/null +++ b/crates/tui/src/tools/shell/output.rs @@ -0,0 +1,55 @@ +use std::sync::{Arc, Mutex}; + +pub(super) fn take_delta_from_buffer( + buffer: &Arc>>, + cursor: &mut usize, +) -> (Vec, usize) { + let guard = buffer.lock().unwrap_or_else(|e| e.into_inner()); + let total = guard.len(); + let start = (*cursor).min(total); + // Clone only the unread portion (the delta), not the entire accumulated buffer. + // Long-running processes can produce megabytes of output; cloning the full + // buffer on every poll held the ShellManager mutex for O(total_bytes) time. + let delta = guard[start..].to_vec(); + *cursor = total; + (delta, total) +} + +/// Read only the tail of a byte buffer and return (total_len, tail_string). +/// +/// Avoids cloning the full buffer when only a trailing excerpt is needed +/// (e.g. for the job-panel display). `max_tail_chars` is in Unicode scalar +/// values; we read at most `max_tail_chars * 4` bytes from the end to account +/// for multi-byte UTF-8 sequences. +pub(super) fn tail_from_buffer( + buffer: &Arc>>, + max_tail_chars: usize, +) -> (usize, String) { + let guard = buffer.lock().unwrap_or_else(|e| e.into_inner()); + let total = guard.len(); + // Over-estimate byte count (4 bytes per char worst case for UTF-8). + let mut tail_start = total.saturating_sub(max_tail_chars.saturating_mul(4)); + // Snap forward to the next valid UTF-8 codepoint boundary so we don't + // pass a slice beginning with continuation bytes (0x80-0xBF) to + // from_utf8_lossy, which would emit a leading U+FFFD replacement char. + while tail_start < total && (guard[tail_start] & 0xC0) == 0x80 { + tail_start += 1; + } + let tail_str = String::from_utf8_lossy(&guard[tail_start..]).into_owned(); + (total, tail_text(&tail_str, max_tail_chars)) +} + +fn tail_text(text: &str, max_chars: usize) -> String { + if text.chars().count() <= max_chars { + return text.to_string(); + } + let tail = text + .chars() + .rev() + .take(max_chars) + .collect::>() + .into_iter() + .rev() + .collect::(); + format!("...{tail}") +}