From 8596728143efb81e7b211e0d126ca6accb6e3f44 Mon Sep 17 00:00:00 2001 From: Osraka <98612432+Osraka@users.noreply.github.com> Date: Tue, 30 Jun 2026 02:54:40 +0300 Subject: [PATCH 1/4] fix(providers): treat unicode punctuation as image path terminators --- crates/goose-providers/src/images.rs | 50 ++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/crates/goose-providers/src/images.rs b/crates/goose-providers/src/images.rs index 5a1a11b8460e..554a7aaa6268 100644 --- a/crates/goose-providers/src/images.rs +++ b/crates/goose-providers/src/images.rs @@ -49,8 +49,7 @@ pub fn detect_image_path(text: &str) -> Option> { }; let terminator = text.get(end..).and_then(|rest| rest.chars().next()); - let terminated = - terminator.is_none_or(|c| c == '/' || c.is_whitespace() || c == '"' || c == '\''); + let terminated = terminator.is_none_or(is_path_terminator); if terminated { let mut floor = end.saturating_sub(MAX_PATH_LEN); @@ -140,6 +139,18 @@ fn is_existing_image_path(candidate: &str) -> bool { path.is_absolute() && path.is_file() && is_image_file(path) } +fn is_path_terminator(c: char) -> bool { + c == '/' + || c.is_whitespace() + || matches!( + c, + '"' | '\'' | '\u{00AB}' | '\u{00BB}' | '\u{2013}' + ..='\u{201F}' | '\u{2026}' | '\u{2039}' | '\u{203A}' + ) + || ('\u{2600}'..='\u{27BF}').contains(&c) + || ('\u{1F300}'..='\u{1FAFF}').contains(&c) +} + /// Case-insensitive ASCII substring search returning a byte index into /// `haystack` (no allocation, so the index stays valid for slicing). fn find_ascii_ci(haystack: &str, needle: &str, from: usize) -> Option { @@ -355,6 +366,41 @@ mod tests { assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); } + #[test] + fn test_detect_image_path_with_unicode_separators() { + let temp_dir = tempfile::tempdir().unwrap(); + let png_data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + let png_path = temp_dir.path().join("photo.png"); + std::fs::write(&png_path, png_data).unwrap(); + let png_path_str = png_path.to_str().unwrap(); + + assert_eq!( + detect_image_path(&format!("{png_path_str}πŸ™‚")).as_deref(), + Some(png_path_str) + ); + assert_eq!( + detect_image_path(&format!("{png_path_str}… more text")).as_deref(), + Some(png_path_str) + ); + assert_eq!( + detect_image_path(&format!("{png_path_str}\u{2014}more text")).as_deref(), + Some(png_path_str) + ); + + assert_eq!( + detect_image_path(&format!("{png_path_str}\u{200B}.backup")).as_deref(), + None + ); + assert_eq!( + detect_image_path(&format!("{png_path_str}\u{0301}")).as_deref(), + None + ); + assert_eq!( + detect_image_path(&format!("file:{png_path_str}")).as_deref(), + None + ); + } + #[test] fn test_detect_image_path_ignores_urls_and_longer_extensions() { let temp_dir = tempfile::tempdir().unwrap(); From 22900a1ffaea77e786df8730b4bc4d5f657cbf99 Mon Sep 17 00:00:00 2001 From: Osraka <98612432+Osraka@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:13:21 +0300 Subject: [PATCH 2/4] test(providers): cover flag emoji path terminators --- crates/goose-providers/src/images.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/goose-providers/src/images.rs b/crates/goose-providers/src/images.rs index 554a7aaa6268..a8ae0c2394c6 100644 --- a/crates/goose-providers/src/images.rs +++ b/crates/goose-providers/src/images.rs @@ -148,6 +148,7 @@ fn is_path_terminator(c: char) -> bool { ..='\u{201F}' | '\u{2026}' | '\u{2039}' | '\u{203A}' ) || ('\u{2600}'..='\u{27BF}').contains(&c) + || ('\u{1F1E6}'..='\u{1F1FF}').contains(&c) || ('\u{1F300}'..='\u{1FAFF}').contains(&c) } @@ -378,6 +379,10 @@ mod tests { detect_image_path(&format!("{png_path_str}πŸ™‚")).as_deref(), Some(png_path_str) ); + assert_eq!( + detect_image_path(&format!("{png_path_str}πŸ‡ΊπŸ‡Έ")).as_deref(), + Some(png_path_str) + ); assert_eq!( detect_image_path(&format!("{png_path_str}… more text")).as_deref(), Some(png_path_str) From 0ac42487c665988c62644b415ea853a79d953af9 Mon Sep 17 00:00:00 2001 From: Osraka <98612432+Osraka@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:21:50 +0300 Subject: [PATCH 3/4] fix(providers): cover common emoji path terminators --- crates/goose-providers/src/images.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/goose-providers/src/images.rs b/crates/goose-providers/src/images.rs index a8ae0c2394c6..38e472e1cf25 100644 --- a/crates/goose-providers/src/images.rs +++ b/crates/goose-providers/src/images.rs @@ -147,7 +147,9 @@ fn is_path_terminator(c: char) -> bool { '"' | '\'' | '\u{00AB}' | '\u{00BB}' | '\u{2013}' ..='\u{201F}' | '\u{2026}' | '\u{2039}' | '\u{203A}' ) + || ('\u{2300}'..='\u{23FF}').contains(&c) || ('\u{2600}'..='\u{27BF}').contains(&c) + || ('\u{2B00}'..='\u{2BFF}').contains(&c) || ('\u{1F1E6}'..='\u{1F1FF}').contains(&c) || ('\u{1F300}'..='\u{1FAFF}').contains(&c) } @@ -383,6 +385,14 @@ mod tests { detect_image_path(&format!("{png_path_str}πŸ‡ΊπŸ‡Έ")).as_deref(), Some(png_path_str) ); + assert_eq!( + detect_image_path(&format!("{png_path_str}⌚")).as_deref(), + Some(png_path_str) + ); + assert_eq!( + detect_image_path(&format!("{png_path_str}⭐")).as_deref(), + Some(png_path_str) + ); assert_eq!( detect_image_path(&format!("{png_path_str}… more text")).as_deref(), Some(png_path_str) From 29c47afa54bcf2d194b87993f712fccb9df2c3bf Mon Sep 17 00:00:00 2001 From: Osraka <98612432+Osraka@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:13:17 +0300 Subject: [PATCH 4/4] fix(providers): accept unicode quote path boundaries --- crates/goose-providers/src/images.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/goose-providers/src/images.rs b/crates/goose-providers/src/images.rs index 38e472e1cf25..5c27bd710bf1 100644 --- a/crates/goose-providers/src/images.rs +++ b/crates/goose-providers/src/images.rs @@ -62,7 +62,7 @@ pub fn detect_image_path(text: &str) -> Option> { let preceded_by_boundary = text .get(..start) .and_then(|prefix| prefix.chars().next_back()) - .is_none_or(|c| c.is_whitespace() || c == '"' || c == '\''); + .is_none_or(is_path_leading_boundary); if !preceded_by_boundary { continue; } @@ -139,6 +139,15 @@ fn is_existing_image_path(candidate: &str) -> bool { path.is_absolute() && path.is_file() && is_image_file(path) } +fn is_path_leading_boundary(c: char) -> bool { + c.is_whitespace() + || matches!( + c, + '"' | '\'' | '\u{00AB}' | '\u{00BB}' | '\u{2018}' + ..='\u{201F}' | '\u{2039}' | '\u{203A}' + ) +} + fn is_path_terminator(c: char) -> bool { c == '/' || c.is_whitespace() @@ -300,6 +309,10 @@ mod tests { assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); let text = format!("describe '{}'", png_path_str); assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); + let text = format!("describe β€œ{}” please", png_path_str); + assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); + let text = format!("describe Β«{}Β» please", png_path_str); + assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); // A stray closing quote in prose must not act as a terminator for an // unquoted path.