diff --git a/src/apps/cli/src/root_handlers.rs b/src/apps/cli/src/root_handlers.rs index c80ae51b5..12f09c2b6 100644 --- a/src/apps/cli/src/root_handlers.rs +++ b/src/apps/cli/src/root_handlers.rs @@ -6,6 +6,7 @@ use std::path::Path; use crate::{ config::CliConfig, modes::exec::{ExecMode, ExecOutputFormat, ExecSessionOptions}, + ui::string_utils::truncate_str, ConfigAction, SessionAction, }; @@ -202,7 +203,7 @@ pub async fn handle_session_action(action: SessionAction) -> Result format!("[Tool result: {}]", tool_name), }; let preview = if content_preview.len() > 80 { - format!("{}...", &content_preview[..77]) + truncate_str(&content_preview, 77) } else { content_preview }; @@ -237,10 +238,9 @@ pub async fn handle_session_action(action: SessionAction) -> Result &str { + if text.len() <= max_bytes { + return text; + } + + let mut end = max_bytes; + while end > 0 && !text.is_char_boundary(end) { + end -= 1; + } + &text[..end] +} + /// Messages received from the desktop via WebSocket. #[derive(Debug, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] @@ -120,7 +132,7 @@ fn handle_text_message( ) { debug!( "Received from conn_id={conn_id}: {}", - &text[..text.len().min(200)] + truncate_preview(text, 200) ); let msg: InboundMessage = match serde_json::from_str(text) { Ok(m) => m, @@ -193,6 +205,18 @@ fn handle_text_message( } } +#[cfg(test)] +mod tests { + use super::truncate_preview; + + #[test] + fn truncate_preview_respects_utf8_boundaries() { + let text = format!("{}{}", "a".repeat(199), "你"); + + assert_eq!(truncate_preview(&text, 200), "a".repeat(199)); + } +} + fn send_json(tx: &mpsc::UnboundedSender, msg: &T) { if let Ok(json) = serde_json::to_string(msg) { let _ = tx.send(OutboundMessage { text: json }); diff --git a/src/crates/core/src/service/remote_ssh/manager.rs b/src/crates/core/src/service/remote_ssh/manager.rs index e42bd6c96..db6aae3c8 100644 --- a/src/crates/core/src/service/remote_ssh/manager.rs +++ b/src/crates/core/src/service/remote_ssh/manager.rs @@ -7,6 +7,7 @@ use crate::service::remote_ssh::types::{ SSHAuthMethod, SSHCommandOptions, SSHCommandResult, SSHConfigEntry, SSHConfigLookupResult, SSHConnectionConfig, SSHConnectionResult, SavedConnection, ServerInfo, }; +use crate::util::truncate_at_char_boundary; use anyhow::{anyhow, Context}; use async_trait::async_trait; use russh::client::{DisconnectReason, Handle, Handler, Msg}; @@ -1396,7 +1397,7 @@ impl SSHConnectionManager { ) -> std::result::Result { let execution_started_at = Instant::now(); let command_preview = if command.len() > 160 { - format!("{}...", &command[..160]) + format!("{}...", truncate_at_char_boundary(command, 160)) } else { command.to_string() };