1use std::collections::HashMap;
6
7use once_cell::sync::Lazy;
8use regex::Regex;
9
10use super::types::{
11 ConfirmAction, ConfirmInfo, ConfirmKey, ConfirmOption, ConfirmParser, ConfirmResponse,
12 ConfirmType, ParserContext, ParserMeta, ToolInfo,
13};
14
15static OPTION_CONFIRM_PATTERN: Lazy<Regex> =
17 Lazy::new(|| Regex::new(r"(?mi)^[\s❯>]*1\.\s*(Yes|Allow)").unwrap());
18
19static YES_NO_CONFIRM_PATTERN: Lazy<Regex> =
20 Lazy::new(|| Regex::new(r"(?i)\[Y/n\]|\(yes/no\)|Allow\?|Do you want to proceed").unwrap());
21
22static TOOL_INFO_PATTERN: Lazy<Regex> =
24 Lazy::new(|| Regex::new(r"(\S+)\s*-\s*(\w+)\s*\(([^)]*)\)(?:\s*\(MCP\))?").unwrap());
25
26static PARAM_PATTERN: Lazy<Regex> =
28 Lazy::new(|| Regex::new(r#"(\w+):\s*("[^"]*"|[^,)]+)"#).unwrap());
29
30static OPTION_LINE_PATTERN: Lazy<Regex> =
32 Lazy::new(|| Regex::new(r"^[\s❯>]*(\d+)\.\s*(.+)$").unwrap());
33
34static YN_CLEANUP_PATTERN: Lazy<Regex> =
36 Lazy::new(|| Regex::new(r"(?i)\s*\[Y/n\].*|\s*\(yes/no\).*").unwrap());
37
38pub struct ClaudeCodeConfirmParser {
44 meta: ParserMeta,
45}
46
47impl Default for ClaudeCodeConfirmParser {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl ClaudeCodeConfirmParser {
54 pub fn new() -> Self {
56 Self {
57 meta: ParserMeta {
58 name: "claude-code-confirm".to_string(),
59 description: "Parses Claude Code tool confirmation dialogs".to_string(),
60 priority: 100,
61 version: "1.0.0".to_string(),
62 },
63 }
64 }
65
66 fn is_option_confirm(&self, text: &str) -> bool {
68 OPTION_CONFIRM_PATTERN.is_match(text) && text.contains("Esc to cancel")
69 }
70
71 fn is_yes_no_confirm(&self, text: &str) -> bool {
73 YES_NO_CONFIRM_PATTERN.is_match(text)
74 }
75
76 fn parse_tool_info(&self, text: &str) -> Option<ToolInfo> {
82 let caps = TOOL_INFO_PATTERN.captures(text)?;
83
84 let mcp_server = caps.get(1)?.as_str().to_string();
85 let name = caps.get(2)?.as_str().to_string();
86 let params_str = caps.get(3)?.as_str();
87
88 let mut params = HashMap::new();
90 for caps in PARAM_PATTERN.captures_iter(params_str) {
91 if let (Some(key), Some(value)) = (caps.get(1), caps.get(2)) {
92 let key = key.as_str().to_string();
93 let mut value = value.as_str().to_string();
94
95 if value.starts_with('"') && value.ends_with('"') {
97 value = value[1..value.len() - 1].to_string();
98 }
99
100 params.insert(key, value);
101 }
102 }
103
104 Some(ToolInfo {
105 name,
106 mcp_server: Some(mcp_server),
107 params,
108 })
109 }
110
111 fn parse_options(&self, text: &str) -> Option<Vec<ConfirmOption>> {
113 let mut options = Vec::new();
114
115 for line in text.lines() {
116 if let Some(caps) = OPTION_LINE_PATTERN.captures(line) {
117 if let (Some(num_match), Some(label_match)) = (caps.get(1), caps.get(2)) {
118 if let Ok(num) = num_match.as_str().parse::<u32>() {
119 options.push(ConfirmOption {
120 key: ConfirmKey::Number(num),
121 label: label_match.as_str().trim().to_string(),
122 is_default: num == 1,
123 });
124 }
125 }
126 }
127 }
128
129 if options.is_empty() {
130 None
131 } else {
132 Some(options)
133 }
134 }
135
136 fn extract_prompt(&self, text: &str) -> String {
138 let mut prompt_lines = Vec::new();
139
140 for line in text.lines() {
141 if OPTION_LINE_PATTERN.is_match(line) {
143 break;
144 }
145
146 if YES_NO_CONFIRM_PATTERN.is_match(line) {
148 let cleaned = YN_CLEANUP_PATTERN.replace(line, "");
149 let trimmed = cleaned.trim();
150 if !trimmed.is_empty() {
151 prompt_lines.push(trimmed.to_string());
152 }
153 break;
154 }
155
156 let trimmed = line.trim();
157 if !trimmed.is_empty() {
158 prompt_lines.push(trimmed.to_string());
159 }
160 }
161
162 prompt_lines.join("\n")
163 }
164}
165
166impl ConfirmParser for ClaudeCodeConfirmParser {
167 fn meta(&self) -> &ParserMeta {
168 &self.meta
169 }
170
171 fn detect_confirm(&self, context: &ParserContext) -> Option<ConfirmInfo> {
172 let text = context.text();
173
174 if self.is_option_confirm(&text) {
177 let tool = self.parse_tool_info(&text);
178 let options = self.parse_options(&text);
179
180 return Some(ConfirmInfo {
181 confirm_type: ConfirmType::Options,
182 prompt: self.extract_prompt(&text),
183 options,
184 tool,
185 raw_prompt: text,
186 });
187 }
188
189 if self.is_yes_no_confirm(&text) {
191 return Some(ConfirmInfo {
192 confirm_type: ConfirmType::YesNo,
193 prompt: self.extract_prompt(&text),
194 options: Some(vec![
195 ConfirmOption {
196 key: ConfirmKey::Char("y".to_string()),
197 label: "Yes".to_string(),
198 is_default: true,
199 },
200 ConfirmOption {
201 key: ConfirmKey::Char("n".to_string()),
202 label: "No".to_string(),
203 is_default: false,
204 },
205 ]),
206 tool: None,
207 raw_prompt: text,
208 });
209 }
210
211 None
212 }
213
214 fn format_response(&self, info: &ConfirmInfo, response: &ConfirmResponse) -> String {
215 match response.action {
219 ConfirmAction::Confirm => {
220 "\r".to_string()
222 }
223 ConfirmAction::Deny => {
224 match info.confirm_type {
225 ConfirmType::Options => {
226 "\x1b[B\x1b[B\r".to_string()
229 }
230 ConfirmType::YesNo => {
231 "n\r".to_string()
233 }
234 }
235 }
236 ConfirmAction::Select => {
237 if let Some(option) = response.option {
239 if option > 1 {
240 let down_keys = "\x1b[B".repeat((option - 1) as usize);
241 format!("{}\r", down_keys)
242 } else {
243 "\r".to_string()
244 }
245 } else {
246 "\r".to_string()
247 }
248 }
249 ConfirmAction::Input => {
250 if let Some(ref value) = response.value {
252 format!("{}\r", value)
253 } else {
254 "\r".to_string()
255 }
256 }
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 fn make_context(lines: &[&str]) -> ParserContext {
266 ParserContext::new(lines.iter().map(|s| s.to_string()).collect())
267 }
268
269 #[test]
270 fn test_parse_tool_info() {
271 let parser = ClaudeCodeConfirmParser::new();
272
273 let text = r#"xjp-mcp - xjp_secret_get(key: "test_value")"#;
275 let tool = parser.parse_tool_info(text);
276 assert!(tool.is_some());
277 let tool = tool.unwrap();
278 assert_eq!(tool.name, "xjp_secret_get");
279 assert_eq!(tool.mcp_server, Some("xjp-mcp".to_string()));
280 assert_eq!(tool.params.get("key"), Some(&"test_value".to_string()));
281
282 let text = r#"xjp-mcp - xjp_secret_get(key: "value") (MCP)"#;
284 let tool = parser.parse_tool_info(text);
285 assert!(tool.is_some());
286 assert_eq!(tool.unwrap().name, "xjp_secret_get");
287
288 let text = r#"server - tool_name(param1: "val1", param2: "val2")"#;
290 let tool = parser.parse_tool_info(text);
291 assert!(tool.is_some());
292 let tool = tool.unwrap();
293 assert_eq!(tool.params.get("param1"), Some(&"val1".to_string()));
294 assert_eq!(tool.params.get("param2"), Some(&"val2".to_string()));
295 }
296
297 #[test]
298 fn test_parse_options() {
299 let parser = ClaudeCodeConfirmParser::new();
300
301 let text = "❯ 1. Yes, allow this action\n 2. Yes, allow for this session\n 3. No, deny this action";
302 let options = parser.parse_options(text);
303 assert!(options.is_some());
304 let options = options.unwrap();
305 assert_eq!(options.len(), 3);
306
307 assert!(matches!(options[0].key, ConfirmKey::Number(1)));
308 assert!(options[0].label.contains("Yes, allow this action"));
309 assert!(options[0].is_default);
310
311 assert!(matches!(options[1].key, ConfirmKey::Number(2)));
312 assert!(!options[1].is_default);
313
314 assert!(matches!(options[2].key, ConfirmKey::Number(3)));
315 assert!(options[2].label.contains("No"));
316 }
317
318 #[test]
319 fn test_detect_option_confirm() {
320 let parser = ClaudeCodeConfirmParser::new();
321
322 let context = make_context(&[
323 "xjp-mcp - xjp_secret_get(key: \"test\")",
324 "❯ 1. Yes, allow this action",
325 " 2. Yes, allow for this session",
326 " 3. No, deny this action",
327 "Esc to cancel",
328 ]);
329
330 let result = parser.detect_confirm(&context);
331 assert!(result.is_some());
332 let info = result.unwrap();
333
334 assert_eq!(info.confirm_type, ConfirmType::Options);
335 assert!(info.tool.is_some());
336 assert_eq!(info.tool.as_ref().unwrap().name, "xjp_secret_get");
337 assert!(info.options.is_some());
338 assert_eq!(info.options.as_ref().unwrap().len(), 3);
339 }
340
341 #[test]
342 fn test_detect_yesno_confirm() {
343 let parser = ClaudeCodeConfirmParser::new();
344
345 let context = make_context(&["Do you want to continue? [Y/n]"]);
347 let result = parser.detect_confirm(&context);
348 assert!(result.is_some());
349 let info = result.unwrap();
350 assert_eq!(info.confirm_type, ConfirmType::YesNo);
351 assert!(info.options.is_some());
352 assert_eq!(info.options.as_ref().unwrap().len(), 2);
353
354 let context = make_context(&["Proceed with action? (yes/no)"]);
356 let result = parser.detect_confirm(&context);
357 assert!(result.is_some());
358 assert_eq!(result.unwrap().confirm_type, ConfirmType::YesNo);
359 }
360
361 #[test]
362 fn test_extract_prompt() {
363 let parser = ClaudeCodeConfirmParser::new();
364
365 let text =
367 "Do you want to allow this?\n❯ 1. Yes\n 2. No\nEsc to cancel";
368 let prompt = parser.extract_prompt(text);
369 assert_eq!(prompt, "Do you want to allow this?");
370
371 let text = "Continue? [Y/n]";
373 let prompt = parser.extract_prompt(text);
374 assert_eq!(prompt, "Continue?");
375 }
376
377 #[test]
378 fn test_format_response_confirm() {
379 let parser = ClaudeCodeConfirmParser::new();
380
381 let info = ConfirmInfo {
382 confirm_type: ConfirmType::Options,
383 prompt: "Test".to_string(),
384 options: None,
385 tool: None,
386 raw_prompt: "Test".to_string(),
387 };
388
389 let response = ConfirmResponse::confirm();
391 assert_eq!(parser.format_response(&info, &response), "\r");
392 }
393
394 #[test]
395 fn test_format_response_deny_options() {
396 let parser = ClaudeCodeConfirmParser::new();
397
398 let info = ConfirmInfo {
399 confirm_type: ConfirmType::Options,
400 prompt: "Test".to_string(),
401 options: None,
402 tool: None,
403 raw_prompt: "Test".to_string(),
404 };
405
406 let response = ConfirmResponse::deny();
408 assert_eq!(parser.format_response(&info, &response), "\x1b[B\x1b[B\r");
409 }
410
411 #[test]
412 fn test_format_response_deny_yesno() {
413 let parser = ClaudeCodeConfirmParser::new();
414
415 let info = ConfirmInfo {
416 confirm_type: ConfirmType::YesNo,
417 prompt: "Test".to_string(),
418 options: None,
419 tool: None,
420 raw_prompt: "Test".to_string(),
421 };
422
423 let response = ConfirmResponse::deny();
425 assert_eq!(parser.format_response(&info, &response), "n\r");
426 }
427
428 #[test]
429 fn test_format_response_select() {
430 let parser = ClaudeCodeConfirmParser::new();
431
432 let info = ConfirmInfo {
433 confirm_type: ConfirmType::Options,
434 prompt: "Test".to_string(),
435 options: None,
436 tool: None,
437 raw_prompt: "Test".to_string(),
438 };
439
440 let response = ConfirmResponse::select(1);
442 assert_eq!(parser.format_response(&info, &response), "\r");
443
444 let response = ConfirmResponse::select(2);
446 assert_eq!(parser.format_response(&info, &response), "\x1b[B\r");
447
448 let response = ConfirmResponse::select(3);
450 assert_eq!(parser.format_response(&info, &response), "\x1b[B\x1b[B\r");
451 }
452
453 #[test]
454 fn test_format_response_input() {
455 let parser = ClaudeCodeConfirmParser::new();
456
457 let info = ConfirmInfo {
458 confirm_type: ConfirmType::YesNo,
459 prompt: "Test".to_string(),
460 options: None,
461 tool: None,
462 raw_prompt: "Test".to_string(),
463 };
464
465 let response = ConfirmResponse::input("custom value");
467 assert_eq!(
468 parser.format_response(&info, &response),
469 "custom value\r"
470 );
471 }
472
473 #[test]
474 fn test_no_detection() {
475 let parser = ClaudeCodeConfirmParser::new();
476
477 let context = make_context(&["random text", "nothing special"]);
478 let result = parser.detect_confirm(&context);
479 assert!(result.is_none());
480 }
481}