From dfe70719b3b281f29235ed97e78c543371e2b0b1 Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Wed, 22 Oct 2025 21:32:05 +0900 Subject: [PATCH 1/8] Fix panic on CJK character truncation in prompts - Replace unsafe byte-index slicing with UTF-8 safe char_indices() - Fixes #3117, #3170 where truncate_description panicked on multibyte characters - Ensures truncation respects character boundaries for CJK languages - Maintains backward compatibility with ASCII text --- crates/chat-cli/src/cli/chat/cli/prompts.rs | 111 ++++++++++++++------ 1 file changed, 78 insertions(+), 33 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/cli/prompts.rs b/crates/chat-cli/src/cli/chat/cli/prompts.rs index 20a275016..2941fda18 100644 --- a/crates/chat-cli/src/cli/chat/cli/prompts.rs +++ b/crates/chat-cli/src/cli/chat/cli/prompts.rs @@ -240,12 +240,30 @@ fn format_description(description: Option<&String>) -> String { /// Truncates a description string to the specified maximum length. /// /// If truncation is needed, adds "..." ellipsis and trims trailing whitespace -/// to ensure clean formatting. +/// to ensure clean formatting. This function is UTF-8 safe and will not panic +/// on multibyte characters (e.g., CJK languages). fn truncate_description(text: &str, max_length: usize) -> String { if text.len() <= max_length { text.to_string() } else { - let truncated = &text[..max_length.saturating_sub(3)]; + // UTF-8 safe truncation: find the last valid character boundary + let target_len = max_length.saturating_sub(3); + let mut truncate_at = 0; + + for (idx, _) in text.char_indices() { + if idx > target_len { + break; + } + truncate_at = idx; + } + + // If we found a valid boundary, use it; otherwise use the last character start + if truncate_at == 0 && !text.is_empty() { + // Edge case: even the first character is too long + truncate_at = text.char_indices().next().map(|(i, _)| i).unwrap_or(0); + } + + let truncated = &text[..truncate_at]; format!("{}...", truncated.trim_end()) } } @@ -2213,37 +2231,7 @@ mod tests { assert_eq!(format_description(multiline_desc.as_ref()), "First line"); } - #[test] - fn test_truncate_description() { - // Test normal length - let short = "Short description"; - assert_eq!(truncate_description(short, 40), "Short description"); - - // Test truncation - let long = - "This is a very long description that should be truncated because it exceeds the maximum length limit"; - let result = truncate_description(long, 40); - assert!(result.len() <= 40); - assert!(result.ends_with("...")); - // Length may be less than 40 due to trim_end() removing trailing spaces - assert!(result.len() >= 37); // At least max_length - 3 chars - - // Test exact length - let exact = "A".repeat(40); - assert_eq!(truncate_description(&exact, 40), exact); - - // Test very short max length - let result = truncate_description("Hello world", 5); - assert_eq!(result, "He..."); - assert_eq!(result.len(), 5); - - // Test space trimming before ellipsis - let with_space = "Prompt to explain available tools and how"; - let result = truncate_description(with_space, 40); - assert!(!result.contains(" ...")); - assert!(result.ends_with("...")); - assert_eq!(result, "Prompt to explain available tools and..."); - } + #[test] fn test_parse_all_mcp_error_details() { @@ -2532,4 +2520,61 @@ mod tests { }; assert_eq!(actual_prompt_name, "my_prompt"); } + + #[test] + fn test_truncate_description_ascii() { + // Test with ASCII text + assert_eq!(truncate_description("Hello World", 20), "Hello World"); + assert_eq!(truncate_description("Hello World", 11), "Hello World"); + assert_eq!(truncate_description("Hello World", 8), "Hello..."); + assert_eq!(truncate_description("Hello World", 5), "He..."); + } + + #[test] + fn test_truncate_description_cjk() { + // Test with CJK characters (3 bytes each in UTF-8) + // Korean text from issue #3117 + let korean = "사용자가 작성한 글의 어색한 표현이나 오타를 수정하고 싶을 때"; + let result = truncate_description(korean, 40); + assert!(result.len() <= 40); + assert!(result.ends_with("...")); + + // Chinese text from issue #3170 + let chinese = "移除 eagleeye-ec-databases 任務狀況確認,最後完成後把"; + let result = truncate_description(chinese, 60); + assert!(result.len() <= 60); + + // Japanese text + let japanese = "これは日本語のテキストです。長い文章をテストします。"; + let result = truncate_description(japanese, 30); + assert!(result.len() <= 30); + assert!(result.ends_with("...")); + } + + #[test] + fn test_truncate_description_mixed() { + // Test with mixed ASCII and CJK + let mixed = "CSR 페이지를 렌더링하고 있는데, html, 이미지는 화면에 잘 나타나는데"; + let result = truncate_description(mixed, 60); + assert!(result.len() <= 60); + + // Ensure no panic on exact boundary + let text = "移除 eagleeye-ec-databases 任務狀況確認"; + let result = truncate_description(text, 60); + assert!(result.len() <= 60); + } + + #[test] + fn test_truncate_description_edge_cases() { + // Empty string + assert_eq!(truncate_description("", 10), ""); + + // Very short max_length + assert_eq!(truncate_description("Hello", 3), "..."); + + // Single multibyte character + let emoji = "😀"; + let result = truncate_description(emoji, 10); + assert!(result.len() <= 10); + } } From 8ae8d5d5ed4f6eda31d67b6656106f730ef9b7b7 Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Wed, 22 Oct 2025 21:37:36 +0900 Subject: [PATCH 2/8] Regenerate Cargo.lock to fix TOML parse error --- Cargo.lock | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0d3338c7..223671f61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "aws-sdk-cognitoidentity" -version = "1.87.0" +version = "1.88.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc53737ec00bf68e7591b23fb6b604ac2cbfcc829ba3abd3839877afd83a1da2" +checksum = "9a1eb04ebb4cd91b3f2570087e076861d8dee0c3b5d6339433ca3208376f8e67" dependencies = [ "aws-credential-types", "aws-runtime", @@ -671,7 +671,7 @@ dependencies = [ "indexmap", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", @@ -1420,7 +1420,7 @@ dependencies = [ "ring", "rmcp", "rusqlite", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-native-certs 0.8.2", "rustls-pemfile 2.2.0", "rustyline", @@ -3129,9 +3129,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -3433,7 +3433,7 @@ dependencies = [ "http 1.3.1", "hyper 1.7.0", "hyper-util", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", @@ -3676,9 +3676,12 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "inout" @@ -5315,9 +5318,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" dependencies = [ "unicode-ident", ] @@ -5503,7 +5506,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.33", + "rustls 0.23.34", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -5523,7 +5526,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -5922,7 +5925,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", @@ -5965,9 +5968,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e35d31f89beb59c83bc31363426da25b323ce0c2e5b53c7bf29867d16ee7898" +checksum = "1fdad1258f7259fdc0f2dfc266939c82c3b5d1fd72bcde274d600cdc27e60243" dependencies = [ "base64 0.22.1", "chrono", @@ -5994,9 +5997,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d88518b38110c439a03f0f4eee40e5105d648a530711cb87f98991e3f324a664" +checksum = "ede0589a208cc7ce81d1be68aa7e74b917fcd03c81528408bab0457e187dcd9b" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -6100,9 +6103,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.33" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "aws-lc-rs", "log", @@ -7269,7 +7272,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.33", + "rustls 0.23.34", "tokio", ] From b43dd79449f37f0a9e05b46281e983726ed2537d Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Wed, 22 Oct 2025 22:16:18 +0900 Subject: [PATCH 3/8] Restore original test_truncate_description test - Consolidate ASCII and CJK test cases into single test function - Reduces diff size while maintaining comprehensive coverage - Ensures backward compatibility verification --- crates/chat-cli/src/cli/chat/cli/prompts.rs | 73 ++++++++++----------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/cli/prompts.rs b/crates/chat-cli/src/cli/chat/cli/prompts.rs index 2941fda18..34ba440a2 100644 --- a/crates/chat-cli/src/cli/chat/cli/prompts.rs +++ b/crates/chat-cli/src/cli/chat/cli/prompts.rs @@ -2522,59 +2522,52 @@ mod tests { } #[test] - fn test_truncate_description_ascii() { - // Test with ASCII text - assert_eq!(truncate_description("Hello World", 20), "Hello World"); - assert_eq!(truncate_description("Hello World", 11), "Hello World"); - assert_eq!(truncate_description("Hello World", 8), "Hello..."); - assert_eq!(truncate_description("Hello World", 5), "He..."); - } + fn test_truncate_description() { + // Test normal length + let short = "Short description"; + assert_eq!(truncate_description(short, 40), "Short description"); + + // Test truncation + let long = + "This is a very long description that should be truncated because it exceeds the maximum length limit"; + let result = truncate_description(long, 40); + assert!(result.len() <= 40); + assert!(result.ends_with("...")); + // Length may be less than 40 due to trim_end() removing trailing spaces + assert!(result.len() >= 37); // At least max_length - 3 chars + + // Test exact length + let exact = "A".repeat(40); + assert_eq!(truncate_description(&exact, 40), exact); + + // Test very short max length + let result = truncate_description("Hello world", 5); + assert_eq!(result, "He..."); + assert_eq!(result.len(), 5); + + // Test space trimming before ellipsis + let with_space = "Prompt to explain available tools and how"; + let result = truncate_description(with_space, 40); + assert!(!result.contains(" ...")); + assert!(result.ends_with("...")); + assert_eq!(result, "Prompt to explain available tools and..."); - #[test] - fn test_truncate_description_cjk() { - // Test with CJK characters (3 bytes each in UTF-8) - // Korean text from issue #3117 + // Test CJK characters (fixes #3117, #3170) let korean = "사용자가 작성한 글의 어색한 표현이나 오타를 수정하고 싶을 때"; let result = truncate_description(korean, 40); assert!(result.len() <= 40); assert!(result.ends_with("...")); - - // Chinese text from issue #3170 + let chinese = "移除 eagleeye-ec-databases 任務狀況確認,最後完成後把"; let result = truncate_description(chinese, 60); assert!(result.len() <= 60); - - // Japanese text + let japanese = "これは日本語のテキストです。長い文章をテストします。"; let result = truncate_description(japanese, 30); assert!(result.len() <= 30); assert!(result.ends_with("...")); - } - #[test] - fn test_truncate_description_mixed() { - // Test with mixed ASCII and CJK - let mixed = "CSR 페이지를 렌더링하고 있는데, html, 이미지는 화면에 잘 나타나는데"; - let result = truncate_description(mixed, 60); - assert!(result.len() <= 60); - - // Ensure no panic on exact boundary - let text = "移除 eagleeye-ec-databases 任務狀況確認"; - let result = truncate_description(text, 60); - assert!(result.len() <= 60); - } - - #[test] - fn test_truncate_description_edge_cases() { - // Empty string + // Test empty string assert_eq!(truncate_description("", 10), ""); - - // Very short max_length - assert_eq!(truncate_description("Hello", 3), "..."); - - // Single multibyte character - let emoji = "😀"; - let result = truncate_description(emoji, 10); - assert!(result.len() <= 10); } } From 066f6ecf17f40f06b1420dc040a864fb44df7ad1 Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Wed, 22 Oct 2025 22:19:42 +0900 Subject: [PATCH 4/8] refactor: code format --- crates/chat-cli/src/cli/chat/cli/prompts.rs | 98 ++++++++++----------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/cli/prompts.rs b/crates/chat-cli/src/cli/chat/cli/prompts.rs index 34ba440a2..4dbf82e52 100644 --- a/crates/chat-cli/src/cli/chat/cli/prompts.rs +++ b/crates/chat-cli/src/cli/chat/cli/prompts.rs @@ -2231,7 +2231,55 @@ mod tests { assert_eq!(format_description(multiline_desc.as_ref()), "First line"); } + #[test] + fn test_truncate_description() { + // Test normal length + let short = "Short description"; + assert_eq!(truncate_description(short, 40), "Short description"); + // Test truncation + let long = + "This is a very long description that should be truncated because it exceeds the maximum length limit"; + let result = truncate_description(long, 40); + assert!(result.len() <= 40); + assert!(result.ends_with("...")); + // Length may be less than 40 due to trim_end() removing trailing spaces + assert!(result.len() >= 37); // At least max_length - 3 chars + + // Test exact length + let exact = "A".repeat(40); + assert_eq!(truncate_description(&exact, 40), exact); + + // Test very short max length + let result = truncate_description("Hello world", 5); + assert_eq!(result, "He..."); + assert_eq!(result.len(), 5); + + // Test space trimming before ellipsis + let with_space = "Prompt to explain available tools and how"; + let result = truncate_description(with_space, 40); + assert!(!result.contains(" ...")); + assert!(result.ends_with("...")); + assert_eq!(result, "Prompt to explain available tools and..."); + + // Test CJK characters (fixes #3117, #3170) + let korean = "사용자가 작성한 글의 어색한 표현이나 오타를 수정하고 싶을 때"; + let result = truncate_description(korean, 40); + assert!(result.len() <= 40); + assert!(result.ends_with("...")); + + let chinese = "移除 eagleeye-ec-databases 任務狀況確認,最後完成後把"; + let result = truncate_description(chinese, 60); + assert!(result.len() <= 60); + + let japanese = "これは日本語のテキストです。長い文章をテストします。"; + let result = truncate_description(japanese, 30); + assert!(result.len() <= 30); + assert!(result.ends_with("...")); + + // Test empty string + assert_eq!(truncate_description("", 10), ""); + } #[test] fn test_parse_all_mcp_error_details() { @@ -2520,54 +2568,4 @@ mod tests { }; assert_eq!(actual_prompt_name, "my_prompt"); } - - #[test] - fn test_truncate_description() { - // Test normal length - let short = "Short description"; - assert_eq!(truncate_description(short, 40), "Short description"); - - // Test truncation - let long = - "This is a very long description that should be truncated because it exceeds the maximum length limit"; - let result = truncate_description(long, 40); - assert!(result.len() <= 40); - assert!(result.ends_with("...")); - // Length may be less than 40 due to trim_end() removing trailing spaces - assert!(result.len() >= 37); // At least max_length - 3 chars - - // Test exact length - let exact = "A".repeat(40); - assert_eq!(truncate_description(&exact, 40), exact); - - // Test very short max length - let result = truncate_description("Hello world", 5); - assert_eq!(result, "He..."); - assert_eq!(result.len(), 5); - - // Test space trimming before ellipsis - let with_space = "Prompt to explain available tools and how"; - let result = truncate_description(with_space, 40); - assert!(!result.contains(" ...")); - assert!(result.ends_with("...")); - assert_eq!(result, "Prompt to explain available tools and..."); - - // Test CJK characters (fixes #3117, #3170) - let korean = "사용자가 작성한 글의 어색한 표현이나 오타를 수정하고 싶을 때"; - let result = truncate_description(korean, 40); - assert!(result.len() <= 40); - assert!(result.ends_with("...")); - - let chinese = "移除 eagleeye-ec-databases 任務狀況確認,最後完成後把"; - let result = truncate_description(chinese, 60); - assert!(result.len() <= 60); - - let japanese = "これは日本語のテキストです。長い文章をテストします。"; - let result = truncate_description(japanese, 30); - assert!(result.len() <= 30); - assert!(result.ends_with("...")); - - // Test empty string - assert_eq!(truncate_description("", 10), ""); - } } From c53bdf2eb803a83422fe044a9839aa38fede71d2 Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Wed, 22 Oct 2025 22:46:36 +0900 Subject: [PATCH 5/8] test: add edge case tests for truncate_description Add comprehensive test coverage for edge cases: - Very small max_length values - CJK characters that don't fit in target length - Emoji (4-byte UTF-8 characters) - Mixed ASCII and CJK text - Single CJK character within limit All tests verify UTF-8 safe truncation behavior. --- crates/chat-cli/src/cli/chat/cli/prompts.rs | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/chat-cli/src/cli/chat/cli/prompts.rs b/crates/chat-cli/src/cli/chat/cli/prompts.rs index 4dbf82e52..cc6f9106c 100644 --- a/crates/chat-cli/src/cli/chat/cli/prompts.rs +++ b/crates/chat-cli/src/cli/chat/cli/prompts.rs @@ -2279,6 +2279,34 @@ mod tests { // Test empty string assert_eq!(truncate_description("", 10), ""); + + // Edge case: max_length very small (result will be just "...") + let result = truncate_description("한국어", 5); + assert_eq!(result, "..."); + + let result = truncate_description("ABCDEF", 5); + assert_eq!(result, "AB..."); + + // Edge case: first CJK character (3 bytes) + "..." = 6 bytes minimum + let result = truncate_description("한국어", 6); + assert_eq!(result, "한..."); + assert!(result.len() <= 6); + + // Edge case: emoji (4-byte characters) + let emoji = "😀😀😀😀😀"; + let result = truncate_description(emoji, 15); + assert!(result.len() <= 15); + assert!(result.ends_with("...")); + + // Edge case: mixed ASCII and CJK + let mixed = "Hello世界こんにちは"; + let result = truncate_description(mixed, 15); + assert!(result.len() <= 15); + assert!(result.ends_with("...")); + + // Edge case: single CJK character that fits + let result = truncate_description("한", 10); + assert_eq!(result, "한"); } #[test] From 4e1c8e5db8cfd0b05ad9e04a1c87563ceb932e08 Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Thu, 23 Oct 2025 07:01:09 +0900 Subject: [PATCH 6/8] refactor: remove redundant edge case check in truncate_description Remove the unnecessary if-check that was doing nothing. char_indices().next() always returns index 0 for the first character, so this code was just reassigning truncate_at = 0 without any effect. All tests pass without this code, confirming it was redundant. --- crates/chat-cli/src/cli/chat/cli/prompts.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/cli/prompts.rs b/crates/chat-cli/src/cli/chat/cli/prompts.rs index cc6f9106c..da9bdf951 100644 --- a/crates/chat-cli/src/cli/chat/cli/prompts.rs +++ b/crates/chat-cli/src/cli/chat/cli/prompts.rs @@ -257,12 +257,6 @@ fn truncate_description(text: &str, max_length: usize) -> String { truncate_at = idx; } - // If we found a valid boundary, use it; otherwise use the last character start - if truncate_at == 0 && !text.is_empty() { - // Edge case: even the first character is too long - truncate_at = text.char_indices().next().map(|(i, _)| i).unwrap_or(0); - } - let truncated = &text[..truncate_at]; format!("{}...", truncated.trim_end()) } From e2c2a9fcbc9caae4e71c68994f7053537a709ef5 Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Fri, 24 Oct 2025 22:37:05 +0900 Subject: [PATCH 7/8] chore: regenerate Cargo.lock Updated Cargo.lock to enable local testing and verification of the fix. --- Cargo.lock | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 223671f61..e0d3338c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "aws-sdk-cognitoidentity" -version = "1.88.0" +version = "1.87.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb04ebb4cd91b3f2570087e076861d8dee0c3b5d6339433ca3208376f8e67" +checksum = "dc53737ec00bf68e7591b23fb6b604ac2cbfcc829ba3abd3839877afd83a1da2" dependencies = [ "aws-credential-types", "aws-runtime", @@ -671,7 +671,7 @@ dependencies = [ "indexmap", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.34", + "rustls 0.23.33", "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", @@ -1420,7 +1420,7 @@ dependencies = [ "ring", "rmcp", "rusqlite", - "rustls 0.23.34", + "rustls 0.23.33", "rustls-native-certs 0.8.2", "rustls-pemfile 2.2.0", "rustyline", @@ -3129,9 +3129,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305" dependencies = [ "aho-corasick", "bstr", @@ -3433,7 +3433,7 @@ dependencies = [ "http 1.3.1", "hyper 1.7.0", "hyper-util", - "rustls 0.23.34", + "rustls 0.23.33", "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", @@ -3676,12 +3676,9 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.7" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "inout" @@ -5318,9 +5315,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.102" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -5506,7 +5503,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.34", + "rustls 0.23.33", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -5526,7 +5523,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.34", + "rustls 0.23.33", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -5925,7 +5922,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.34", + "rustls 0.23.33", "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", @@ -5968,9 +5965,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdad1258f7259fdc0f2dfc266939c82c3b5d1fd72bcde274d600cdc27e60243" +checksum = "4e35d31f89beb59c83bc31363426da25b323ce0c2e5b53c7bf29867d16ee7898" dependencies = [ "base64 0.22.1", "chrono", @@ -5997,9 +5994,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede0589a208cc7ce81d1be68aa7e74b917fcd03c81528408bab0457e187dcd9b" +checksum = "d88518b38110c439a03f0f4eee40e5105d648a530711cb87f98991e3f324a664" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -6103,9 +6100,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.34" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ "aws-lc-rs", "log", @@ -7272,7 +7269,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.34", + "rustls 0.23.33", "tokio", ] From 21fb022535fa1b2e217b1c41d5565fb906b6693e Mon Sep 17 00:00:00 2001 From: mzkmnk Date: Sat, 25 Oct 2025 11:50:19 +0900 Subject: [PATCH 8/8] refactor: use truncate_safe_in_place for UTF-8 safe truncation Replace custom truncate logic in truncate_description with the existing truncate_safe_in_place utility function to ensure consistency across the codebase and leverage tested UTF-8 safe truncation logic. --- crates/chat-cli/src/cli/chat/cli/prompts.rs | 27 ++++++--------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/cli/prompts.rs b/crates/chat-cli/src/cli/chat/cli/prompts.rs index da9bdf951..cd7f7c9b4 100644 --- a/crates/chat-cli/src/cli/chat/cli/prompts.rs +++ b/crates/chat-cli/src/cli/chat/cli/prompts.rs @@ -33,6 +33,7 @@ use unicode_width::UnicodeWidthStr; use crate::cli::chat::cli::editor::open_editor_file; use crate::cli::chat::tool_manager::PromptBundle; +use crate::cli::chat::util::truncate_safe_in_place; use crate::cli::chat::{ ChatError, ChatSession, @@ -243,23 +244,11 @@ fn format_description(description: Option<&String>) -> String { /// to ensure clean formatting. This function is UTF-8 safe and will not panic /// on multibyte characters (e.g., CJK languages). fn truncate_description(text: &str, max_length: usize) -> String { - if text.len() <= max_length { - text.to_string() - } else { - // UTF-8 safe truncation: find the last valid character boundary - let target_len = max_length.saturating_sub(3); - let mut truncate_at = 0; - - for (idx, _) in text.char_indices() { - if idx > target_len { - break; - } - truncate_at = idx; - } - - let truncated = &text[..truncate_at]; - format!("{}...", truncated.trim_end()) - } + let mut result = text.to_string(); + + truncate_safe_in_place(&mut result, max_length, "..."); + + result } /// Represents parsed MCP error details for generating user-friendly messages. @@ -2277,7 +2266,7 @@ mod tests { // Edge case: max_length very small (result will be just "...") let result = truncate_description("한국어", 5); assert_eq!(result, "..."); - + let result = truncate_description("ABCDEF", 5); assert_eq!(result, "AB..."); @@ -2297,7 +2286,7 @@ mod tests { let result = truncate_description(mixed, 15); assert!(result.len() <= 15); assert!(result.ends_with("...")); - + // Edge case: single CJK character that fits let result = truncate_description("한", 10); assert_eq!(result, "한");