rusty_commit/utils/
thinking_strip.rs1use regex::Regex;
8
9pub fn strip_thinking(text: &str) -> String {
27 let mut result = String::with_capacity(text.len());
29 let mut current = text;
30 let mut has_thinking = false;
31
32 while !current.is_empty() {
33 let opening_pos = find_thinking_opening(current);
35
36 if let Some(start) = opening_pos {
37 result.push_str(¤t[..start]);
39
40 let (after_content, found) = find_and_consume_thinking_block(¤t[start..]);
42
43 if found {
44 has_thinking = true;
45 current = after_content;
46 } else {
47 break;
49 }
50 } else {
51 result.push_str(current);
53 break;
54 }
55 }
56
57 if has_thinking {
59 cleanup_thinking_artifacts(&result)
60 } else {
61 result
62 }
63}
64
65fn find_thinking_opening(text: &str) -> Option<usize> {
67 let lower = text.to_ascii_lowercase();
68
69 let patterns = [
71 "<thinking>",
72 "<think>",
73 "[thinking]",
74 "[[thinking]]",
75 "```thinking",
76 "<!--thinking",
77 ];
78
79 let mut min_pos = None;
80
81 for pattern in &patterns {
82 if let Some(pos) = lower.find(pattern) {
83 if let Some(current_min) = min_pos {
84 if pos < current_min {
85 min_pos = Some(pos);
86 }
87 } else {
88 min_pos = Some(pos);
89 }
90 }
91 }
92
93 min_pos
94}
95
96fn find_and_consume_thinking_block(text: &str) -> (&str, bool) {
98 let lower = text.to_ascii_lowercase();
99
100 let tag_pairs = [
102 ("<thinking>", "</thinking>"),
103 ("<think>", "</think>"),
104 ("[thinking]", "[/thinking]"),
105 ("[[thinking]]", "[[/thinking]]"),
106 ("```thinking", "```"),
107 ("<!--thinking", "-->"),
108 ];
109
110 let mut earliest_closing: Option<usize> = None;
112 let mut earliest_after_closing: Option<&str> = None;
113
114 for (opening, closing) in &tag_pairs {
115 if let Some(opening_pos) = lower.find(opening) {
116 let content_after_opening = &text[opening_pos + opening.len()..];
117 if let Some(closing_pos) = content_after_opening.to_ascii_lowercase().find(closing) {
118 let absolute_closing = opening_pos + opening.len() + closing_pos;
120
121 if let Some(current) = earliest_closing {
122 if absolute_closing < current {
123 earliest_closing = Some(absolute_closing);
124 earliest_after_closing = Some(&text[absolute_closing + closing.len()..]);
125 }
126 } else {
127 earliest_closing = Some(absolute_closing);
128 earliest_after_closing = Some(&text[absolute_closing + closing.len()..]);
129 }
130 }
131 }
132 }
133
134 if let Some(after) = earliest_after_closing {
135 (after, true)
136 } else {
137 (text, false)
138 }
139}
140
141fn cleanup_thinking_artifacts(text: &str) -> String {
143 let re = Regex::new(r"\n\s*\n\s*\n+").unwrap();
145 let result = re.replace_all(text, "\n\n");
146 result.trim().to_string()
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_strip_thinking_basic() {
155 let input = "feat: add login\n<thinking>I should write a clear message</thinking>";
156 let output = strip_thinking(input);
157 assert_eq!(output.trim(), "feat: add login");
158 }
159
160 #[test]
161 fn test_strip_thinking_anthropic_format() {
162 let input = "feat: add login\n<think> I should write a clear message</think>";
163 let output = strip_thinking(input);
164 assert_eq!(output.trim(), "feat: add login");
165 }
166
167 #[test]
168 fn test_strip_thinking_multiple_blocks() {
169 let input =
170 "First part\n<thinking>block 1</thinking>\nMiddle\n<thinking>block 2</thinking>\nLast";
171 let output = strip_thinking(input);
172 assert!(output.contains("First part"));
173 assert!(output.contains("Middle"));
174 assert!(output.contains("Last"));
175 assert!(!output.contains("<thinking>"));
176 }
177
178 #[test]
179 fn test_strip_thinking_no_thinking() {
180 let input = "feat: add login feature";
181 let output = strip_thinking(input);
182 assert_eq!(output, "feat: add login feature");
183 }
184
185 #[test]
186 fn test_strip_thinking_unclosed_tag() {
187 let input = "feat: add login\n<thinking unclosed";
188 let output = strip_thinking(input);
189 assert!(output.contains("<thinking"));
191 }
192}