Skip to main content

limit_cli/tui/
activity.rs

1//! Activity message formatting for TUI
2//!
3//! Formats tool execution messages for display in the activity feed.
4
5/// Format a tool activity message based on tool name and arguments
6pub fn format_activity_message(tool_name: &str, args: &serde_json::Value) -> String {
7    match tool_name {
8        "file_read" => format_file_read(args),
9        "file_write" => format_file_write(args),
10        "file_edit" => format_file_edit(args),
11        "bash" => format_bash(args),
12        "git_status" => "Checking git status...".to_string(),
13        "git_diff" => "Checking git diff...".to_string(),
14        "git_log" => "Checking git log...".to_string(),
15        "git_add" => "Staging files...".to_string(),
16        "git_commit" => "Creating commit...".to_string(),
17        "git_push" => "Pushing to remote...".to_string(),
18        "git_pull" => "Pulling from remote...".to_string(),
19        "git_clone" => format_git_clone(args),
20        "grep" => format_grep(args),
21        "ast_grep" => format_ast_grep(args),
22        "lsp" => format_lsp(args),
23        "browser" => format_browser(args),
24        _ => format!("Executing {}...", tool_name),
25    }
26}
27
28/// Format file read operation
29fn format_file_read(args: &serde_json::Value) -> String {
30    args.get("path")
31        .and_then(|p| p.as_str())
32        .map(|p| format!("Reading {}...", truncate_path(p, 150)))
33        .unwrap_or_else(|| "Reading file...".to_string())
34}
35
36/// Format file write operation
37fn format_file_write(args: &serde_json::Value) -> String {
38    args.get("path")
39        .and_then(|p| p.as_str())
40        .map(|p| format!("Writing {}...", truncate_path(p, 150)))
41        .unwrap_or_else(|| "Writing file...".to_string())
42}
43
44/// Format file edit operation
45fn format_file_edit(args: &serde_json::Value) -> String {
46    args.get("path")
47        .and_then(|p| p.as_str())
48        .map(|p| format!("Editing {}...", truncate_path(p, 150)))
49        .unwrap_or_else(|| "Editing file...".to_string())
50}
51
52/// Format bash command
53fn format_bash(args: &serde_json::Value) -> String {
54    args.get("command")
55        .and_then(|c| c.as_str())
56        .map(|c| format!("Running {}...", truncate_command(c, 150)))
57        .unwrap_or_else(|| "Executing command...".to_string())
58}
59
60/// Format git clone operation
61fn format_git_clone(args: &serde_json::Value) -> String {
62    args.get("url")
63        .and_then(|u| u.as_str())
64        .map(|u| format!("Cloning {}...", truncate_path(u, 150)))
65        .unwrap_or_else(|| "Cloning repository...".to_string())
66}
67
68/// Format grep search
69fn format_grep(args: &serde_json::Value) -> String {
70    args.get("pattern")
71        .and_then(|p| p.as_str())
72        .map(|p| format!("Searching for '{}'...", truncate_command(p, 150)))
73        .unwrap_or_else(|| "Searching...".to_string())
74}
75
76/// Format AST grep operation
77fn format_ast_grep(args: &serde_json::Value) -> String {
78    let command = args
79        .get("command")
80        .and_then(|c| c.as_str())
81        .unwrap_or("search");
82
83    match command {
84        "replace" => {
85            let pattern = args.get("pattern").and_then(|p| p.as_str()).unwrap_or("?");
86            let rewrite = args.get("rewrite").and_then(|r| r.as_str()).unwrap_or("?");
87            format!(
88                "AST replacing '{}' → '{}'...",
89                truncate_command(pattern, 80),
90                truncate_command(rewrite, 80)
91            )
92        }
93        "scan" => {
94            let rule = args
95                .get("rule")
96                .and_then(|r| r.as_str())
97                .map(|r| truncate_command(r, 100));
98            let inline = args
99                .get("inline_rules")
100                .and_then(|r| r.as_str())
101                .map(|r| truncate_command(r, 100));
102            match (rule, inline) {
103                (Some(r), _) => format!("AST scanning with rule '{}'...", r),
104                (_, Some(i)) => format!("AST scanning with inline rule '{}'...", i),
105                _ => "AST scanning...".to_string(),
106            }
107        }
108        _ => args
109            .get("pattern")
110            .and_then(|p| p.as_str())
111            .map(|p| format!("AST searching '{}'...", truncate_command(p, 150)))
112            .unwrap_or_else(|| "AST searching...".to_string()),
113    }
114}
115
116/// Format LSP operation
117fn format_lsp(args: &serde_json::Value) -> String {
118    args.get("command")
119        .and_then(|c| c.as_str())
120        .map(|c| format!("Running LSP {}...", c))
121        .unwrap_or_else(|| "Running LSP...".to_string())
122}
123
124fn format_browser(args: &serde_json::Value) -> String {
125    let action = args
126        .get("action")
127        .and_then(|a| a.as_str())
128        .unwrap_or("unknown");
129
130    let detail = match action {
131        "open" => args
132            .get("url")
133            .and_then(|u| u.as_str())
134            .map(|u| format!("opening {}", truncate_path(u, 80)))
135            .unwrap_or_else(|| "opening URL".to_string()),
136        "close" => "closing browser".to_string(),
137        "snapshot" => "taking DOM snapshot".to_string(),
138        "screenshot" => args
139            .get("path")
140            .and_then(|p| p.as_str())
141            .map(|p| format!("taking screenshot to {}", truncate_path(p, 60)))
142            .unwrap_or_else(|| "taking screenshot".to_string()),
143        "back" => "navigating back".to_string(),
144        "forward" => "navigating forward".to_string(),
145        "reload" => "reloading page".to_string(),
146
147        "click" => args
148            .get("selector")
149            .and_then(|s| s.as_str())
150            .map(|s| format!("clicking {}", truncate_command(s, 80)))
151            .unwrap_or_else(|| "clicking element".to_string()),
152        "dblclick" => args
153            .get("selector")
154            .and_then(|s| s.as_str())
155            .map(|s| format!("double-clicking {}", truncate_command(s, 80)))
156            .unwrap_or_else(|| "double-clicking element".to_string()),
157        "hover" => args
158            .get("selector")
159            .and_then(|s| s.as_str())
160            .map(|s| format!("hovering {}", truncate_command(s, 80)))
161            .unwrap_or_else(|| "hovering element".to_string()),
162        "focus" => args
163            .get("selector")
164            .and_then(|s| s.as_str())
165            .map(|s| format!("focusing {}", truncate_command(s, 80)))
166            .unwrap_or_else(|| "focusing element".to_string()),
167        "scrollintoview" => args
168            .get("selector")
169            .and_then(|s| s.as_str())
170            .map(|s| format!("scrolling to {}", truncate_command(s, 80)))
171            .unwrap_or_else(|| "scrolling element into view".to_string()),
172
173        "fill" => {
174            let selector = args.get("selector").and_then(|s| s.as_str());
175            let text = args.get("text").and_then(|t| t.as_str());
176            match (selector, text) {
177                (Some(s), Some(t)) => format!(
178                    "filling {} with \"{}\"",
179                    truncate_command(s, 40),
180                    truncate_command(t, 40)
181                ),
182                (Some(s), None) => format!("filling {}", truncate_command(s, 80)),
183                _ => "filling input".to_string(),
184            }
185        }
186        "type" => args
187            .get("selector")
188            .and_then(|s| s.as_str())
189            .map(|s| format!("typing into {}", truncate_command(s, 80)))
190            .unwrap_or_else(|| "typing".to_string()),
191        "press" => args
192            .get("key")
193            .and_then(|k| k.as_str())
194            .map(|k| format!("pressing {}", k))
195            .unwrap_or_else(|| "pressing key".to_string()),
196        "select" => {
197            let selector = args.get("selector").and_then(|s| s.as_str());
198            let value = args.get("value").and_then(|v| v.as_str());
199            match (selector, value) {
200                (Some(s), Some(v)) => format!(
201                    "selecting \"{}\" in {}",
202                    truncate_command(v, 40),
203                    truncate_command(s, 40)
204                ),
205                (Some(s), None) => format!("selecting option in {}", truncate_command(s, 80)),
206                _ => "selecting option".to_string(),
207            }
208        }
209        "check" => args
210            .get("selector")
211            .and_then(|s| s.as_str())
212            .map(|s| format!("checking {}", truncate_command(s, 80)))
213            .unwrap_or_else(|| "checking checkbox".to_string()),
214        "uncheck" => args
215            .get("selector")
216            .and_then(|s| s.as_str())
217            .map(|s| format!("unchecking {}", truncate_command(s, 80)))
218            .unwrap_or_else(|| "unchecking checkbox".to_string()),
219        "drag" => {
220            let source = args.get("source").and_then(|s| s.as_str());
221            let target = args.get("target").and_then(|t| t.as_str());
222            match (source, target) {
223                (Some(s), Some(t)) => format!(
224                    "dragging {} to {}",
225                    truncate_command(s, 40),
226                    truncate_command(t, 40)
227                ),
228                _ => "dragging element".to_string(),
229            }
230        }
231        "upload" => {
232            let selector = args.get("selector").and_then(|s| s.as_str());
233            let path = args.get("path").and_then(|p| p.as_str());
234            match (selector, path) {
235                (Some(s), Some(p)) => format!(
236                    "uploading {} to {}",
237                    truncate_path(p, 40),
238                    truncate_command(s, 40)
239                ),
240                _ => "uploading file".to_string(),
241            }
242        }
243        "pdf" => args
244            .get("path")
245            .and_then(|p| p.as_str())
246            .map(|p| format!("saving PDF to {}", truncate_path(p, 60)))
247            .unwrap_or_else(|| "saving PDF".to_string()),
248
249        "get" => args
250            .get("selector")
251            .and_then(|s| s.as_str())
252            .map(|s| format!("getting element {}", truncate_command(s, 80)))
253            .unwrap_or_else(|| "getting element".to_string()),
254        "get_attr" => {
255            let selector = args.get("selector").and_then(|s| s.as_str());
256            let attr = args.get("attr").and_then(|a| a.as_str());
257            match (selector, attr) {
258                (Some(s), Some(a)) => {
259                    format!("getting {} attribute from {}", a, truncate_command(s, 60))
260                }
261                _ => "getting attribute".to_string(),
262            }
263        }
264        "get_count" => args
265            .get("selector")
266            .and_then(|s| s.as_str())
267            .map(|s| format!("counting {}", truncate_command(s, 80)))
268            .unwrap_or_else(|| "counting elements".to_string()),
269        "get_box" => args
270            .get("selector")
271            .and_then(|s| s.as_str())
272            .map(|s| format!("getting bounding box of {}", truncate_command(s, 60)))
273            .unwrap_or_else(|| "getting bounding box".to_string()),
274        "get_styles" => args
275            .get("selector")
276            .and_then(|s| s.as_str())
277            .map(|s| format!("getting styles of {}", truncate_command(s, 80)))
278            .unwrap_or_else(|| "getting styles".to_string()),
279
280        "wait" => args
281            .get("ms")
282            .and_then(|m| m.as_u64())
283            .map(|m| format!("waiting {}ms", m))
284            .unwrap_or_else(|| "waiting".to_string()),
285        "wait_for_text" => args
286            .get("text")
287            .and_then(|t| t.as_str())
288            .map(|t| format!("waiting for text \"{}\"", truncate_command(t, 60)))
289            .unwrap_or_else(|| "waiting for text".to_string()),
290        "wait_for_url" => args
291            .get("url")
292            .and_then(|u| u.as_str())
293            .map(|u| format!("waiting for URL {}", truncate_path(u, 60)))
294            .unwrap_or_else(|| "waiting for URL".to_string()),
295        "wait_for_load" => "waiting for page load".to_string(),
296        "wait_for_download" => "waiting for download".to_string(),
297        "wait_for_fn" => "waiting for function result".to_string(),
298        "wait_for_state" => args
299            .get("state")
300            .and_then(|s| s.as_str())
301            .map(|s| format!("waiting for {} state", s))
302            .unwrap_or_else(|| "waiting for state".to_string()),
303
304        "find" => args
305            .get("selector")
306            .and_then(|s| s.as_str())
307            .map(|s| format!("finding {}", truncate_command(s, 80)))
308            .unwrap_or_else(|| "finding element".to_string()),
309        "scroll" => {
310            let x = args.get("x").and_then(|v| v.as_i64());
311            let y = args.get("y").and_then(|v| v.as_i64());
312            match (x, y) {
313                (Some(x), Some(y)) => format!("scrolling to ({}, {})", x, y),
314                _ => "scrolling".to_string(),
315            }
316        }
317        "is" => {
318            let selector = args.get("selector").and_then(|s| s.as_str());
319            let state = args.get("state").and_then(|s| s.as_str());
320            match (selector, state) {
321                (Some(s), Some(st)) => {
322                    format!("checking if {} is {}", truncate_command(s, 60), st)
323                }
324                _ => "checking element state".to_string(),
325            }
326        }
327        "download" => args
328            .get("url")
329            .and_then(|u| u.as_str())
330            .map(|u| format!("downloading {}", truncate_path(u, 60)))
331            .unwrap_or_else(|| "downloading".to_string()),
332
333        "tab_list" => "listing tabs".to_string(),
334        "tab_new" => args
335            .get("url")
336            .and_then(|u| u.as_str())
337            .map(|u| format!("opening new tab {}", truncate_path(u, 60)))
338            .unwrap_or_else(|| "opening new tab".to_string()),
339        "tab_close" => args
340            .get("index")
341            .and_then(|i| i.as_u64())
342            .map(|i| format!("closing tab {}", i))
343            .unwrap_or_else(|| "closing tab".to_string()),
344        "tab_select" => args
345            .get("index")
346            .and_then(|i| i.as_u64())
347            .map(|i| format!("selecting tab {}", i))
348            .unwrap_or_else(|| "selecting tab".to_string()),
349
350        "dialog_accept" => args
351            .get("text")
352            .and_then(|t| t.as_str())
353            .map(|t| format!("accepting dialog with \"{}\"", truncate_command(t, 40)))
354            .unwrap_or_else(|| "accepting dialog".to_string()),
355        "dialog_dismiss" => "dismissing dialog".to_string(),
356
357        "cookies" => args
358            .get("url")
359            .and_then(|u| u.as_str())
360            .map(|u| format!("getting cookies for {}", truncate_path(u, 60)))
361            .unwrap_or_else(|| "getting cookies".to_string()),
362        "cookies_set" => "setting cookies".to_string(),
363        "storage_get" => args
364            .get("key")
365            .and_then(|k| k.as_str())
366            .map(|k| format!("getting storage \"{}\"", truncate_command(k, 40)))
367            .unwrap_or_else(|| "getting from storage".to_string()),
368        "storage_set" => args
369            .get("key")
370            .and_then(|k| k.as_str())
371            .map(|k| format!("setting storage \"{}\"", truncate_command(k, 40)))
372            .unwrap_or_else(|| "setting storage".to_string()),
373        "network_requests" => "getting network requests".to_string(),
374
375        "set_viewport" => {
376            let width = args.get("width").and_then(|w| w.as_u64());
377            let height = args.get("height").and_then(|h| h.as_u64());
378            match (width, height) {
379                (Some(w), Some(h)) => format!("setting viewport to {}x{}", w, h),
380                _ => "setting viewport".to_string(),
381            }
382        }
383        "set_device" => args
384            .get("device")
385            .and_then(|d| d.as_str())
386            .map(|d| format!("setting device to {}", d))
387            .unwrap_or_else(|| "setting device".to_string()),
388        "set_geo" => {
389            let lat = args.get("latitude").and_then(|l| l.as_f64());
390            let lon = args.get("longitude").and_then(|l| l.as_f64());
391            match (lat, lon) {
392                (Some(lat), Some(lon)) => format!("setting geolocation to ({}, {})", lat, lon),
393                _ => "setting geolocation".to_string(),
394            }
395        }
396
397        "eval" => args
398            .get("script")
399            .and_then(|s| s.as_str())
400            .map(|s| format!("evaluating {}", truncate_command(s, 60)))
401            .unwrap_or_else(|| "evaluating script".to_string()),
402
403        _ => action.to_string(),
404    };
405
406    format!("Executing browser: {}...", detail)
407}
408
409/// Truncate a path for display, showing the end
410fn truncate_path(s: &str, max_len: usize) -> String {
411    let char_count = s.chars().count();
412    if char_count <= max_len {
413        s.to_string()
414    } else {
415        let skip = char_count.saturating_sub(max_len - 3);
416        let truncated: String = s.chars().skip(skip).collect();
417        format!("...{truncated}")
418    }
419}
420
421/// Truncate a command for display, showing the beginning
422fn truncate_command(s: &str, max_len: usize) -> String {
423    let char_count = s.chars().count();
424    if char_count <= max_len {
425        s.to_string()
426    } else {
427        let truncated: String = s.chars().take(max_len.saturating_sub(3)).collect();
428        format!("{truncated}...")
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use serde_json::json;
436
437    #[test]
438    fn test_format_file_read() {
439        let args = json!({"path": "/some/long/path/to/file.txt"});
440        let msg = format_activity_message("file_read", &args);
441        assert!(msg.contains("Reading"));
442    }
443
444    #[test]
445    fn test_format_bash() {
446        let args = json!({"command": "echo hello"});
447        let msg = format_activity_message("bash", &args);
448        assert!(msg.contains("Running"));
449    }
450
451    #[test]
452    fn test_format_unknown_tool() {
453        let args = json!({});
454        let msg = format_activity_message("unknown_tool", &args);
455        assert_eq!(msg, "Executing unknown_tool...");
456    }
457
458    #[test]
459    fn test_truncate_path() {
460        assert_eq!(truncate_path("short", 10), "short");
461        assert_eq!(
462            truncate_path("very_long_path_to_file.txt", 14),
463            "...to_file.txt"
464        );
465    }
466
467    #[test]
468    fn test_truncate_path_utf8() {
469        let multi_byte = "════════════════════════════════════════════════";
470        let result = truncate_path(multi_byte, 10);
471        assert!(result.starts_with("..."));
472        assert!(result.chars().count() <= 10);
473    }
474
475    #[test]
476    fn test_truncate_command() {
477        assert_eq!(truncate_command("short", 10), "short");
478        assert_eq!(truncate_command("very_long_command_here", 10), "very_lo...");
479    }
480
481    #[test]
482    fn test_truncate_command_utf8() {
483        let multi_byte = "════════════════════════════════════════════════";
484        let result = truncate_command(multi_byte, 10);
485        assert!(result.ends_with("..."));
486        assert!(result.chars().count() <= 10);
487    }
488
489    #[test]
490    fn test_git_commands() {
491        assert_eq!(
492            format_activity_message("git_status", &json!({})),
493            "Checking git status..."
494        );
495        assert_eq!(
496            format_activity_message("git_diff", &json!({})),
497            "Checking git diff..."
498        );
499        assert_eq!(
500            format_activity_message("git_add", &json!({})),
501            "Staging files..."
502        );
503        assert_eq!(
504            format_activity_message("git_commit", &json!({})),
505            "Creating commit..."
506        );
507    }
508
509    #[test]
510    fn test_file_operations() {
511        let args = json!({"path": "/test.txt"});
512        assert!(format_activity_message("file_read", &args).contains("Reading"));
513        assert!(format_activity_message("file_write", &args).contains("Writing"));
514        assert!(format_activity_message("file_edit", &args).contains("Editing"));
515    }
516
517    #[test]
518    fn test_browser_navigation() {
519        assert_eq!(
520            format_activity_message(
521                "browser",
522                &json!({"action": "open", "url": "https://example.com"})
523            ),
524            "Executing browser: opening https://example.com..."
525        );
526        assert_eq!(
527            format_activity_message("browser", &json!({"action": "close"})),
528            "Executing browser: closing browser..."
529        );
530        assert_eq!(
531            format_activity_message(
532                "browser",
533                &json!({"action": "screenshot", "path": "/tmp/shot.png"})
534            ),
535            "Executing browser: taking screenshot to /tmp/shot.png..."
536        );
537        assert_eq!(
538            format_activity_message("browser", &json!({"action": "back"})),
539            "Executing browser: navigating back..."
540        );
541        assert_eq!(
542            format_activity_message("browser", &json!({"action": "reload"})),
543            "Executing browser: reloading page..."
544        );
545    }
546
547    #[test]
548    fn test_browser_interaction() {
549        assert_eq!(
550            format_activity_message(
551                "browser",
552                &json!({"action": "click", "selector": "#submit"})
553            ),
554            "Executing browser: clicking #submit..."
555        );
556        assert_eq!(
557            format_activity_message(
558                "browser",
559                &json!({"action": "fill", "selector": "#email", "text": "test@example.com"})
560            ),
561            "Executing browser: filling #email with \"test@example.com\"..."
562        );
563        assert_eq!(
564            format_activity_message("browser", &json!({"action": "press", "key": "Enter"})),
565            "Executing browser: pressing Enter..."
566        );
567        assert_eq!(
568            format_activity_message("browser", &json!({"action": "hover", "selector": ".menu"})),
569            "Executing browser: hovering .menu..."
570        );
571        assert_eq!(
572            format_activity_message("browser", &json!({"action": "check", "selector": "#terms"})),
573            "Executing browser: checking #terms..."
574        );
575        assert_eq!(
576            format_activity_message(
577                "browser",
578                &json!({"action": "uncheck", "selector": "#newsletter"})
579            ),
580            "Executing browser: unchecking #newsletter..."
581        );
582    }
583
584    #[test]
585    fn test_browser_wait() {
586        assert_eq!(
587            format_activity_message("browser", &json!({"action": "wait", "ms": 1000})),
588            "Executing browser: waiting 1000ms..."
589        );
590        assert_eq!(
591            format_activity_message(
592                "browser",
593                &json!({"action": "wait_for_text", "text": "Login"})
594            ),
595            "Executing browser: waiting for text \"Login\"..."
596        );
597        assert_eq!(
598            format_activity_message(
599                "browser",
600                &json!({"action": "wait_for_url", "url": "/dashboard"})
601            ),
602            "Executing browser: waiting for URL /dashboard..."
603        );
604        assert_eq!(
605            format_activity_message("browser", &json!({"action": "wait_for_load"})),
606            "Executing browser: waiting for page load..."
607        );
608    }
609
610    #[test]
611    fn test_browser_tabs() {
612        assert_eq!(
613            format_activity_message("browser", &json!({"action": "tab_list"})),
614            "Executing browser: listing tabs..."
615        );
616        assert_eq!(
617            format_activity_message(
618                "browser",
619                &json!({"action": "tab_new", "url": "https://example.com"})
620            ),
621            "Executing browser: opening new tab https://example.com..."
622        );
623        assert_eq!(
624            format_activity_message("browser", &json!({"action": "tab_select", "index": 2})),
625            "Executing browser: selecting tab 2..."
626        );
627        assert_eq!(
628            format_activity_message("browser", &json!({"action": "tab_close", "index": 1})),
629            "Executing browser: closing tab 1..."
630        );
631    }
632
633    #[test]
634    fn test_browser_query() {
635        assert_eq!(
636            format_activity_message("browser", &json!({"action": "get", "selector": ".item"})),
637            "Executing browser: getting element .item..."
638        );
639        assert_eq!(
640            format_activity_message(
641                "browser",
642                &json!({"action": "get_attr", "selector": "a", "attr": "href"})
643            ),
644            "Executing browser: getting href attribute from a..."
645        );
646        assert_eq!(
647            format_activity_message(
648                "browser",
649                &json!({"action": "get_count", "selector": ".items"})
650            ),
651            "Executing browser: counting .items..."
652        );
653    }
654
655    #[test]
656    fn test_browser_settings() {
657        assert_eq!(
658            format_activity_message(
659                "browser",
660                &json!({"action": "set_viewport", "width": 1920, "height": 1080})
661            ),
662            "Executing browser: setting viewport to 1920x1080..."
663        );
664        assert_eq!(
665            format_activity_message(
666                "browser",
667                &json!({"action": "set_device", "device": "iPhone 12"})
668            ),
669            "Executing browser: setting device to iPhone 12..."
670        );
671        assert_eq!(
672            format_activity_message(
673                "browser",
674                &json!({"action": "set_geo", "latitude": 37.7749, "longitude": -122.4194})
675            ),
676            "Executing browser: setting geolocation to (37.7749, -122.4194)..."
677        );
678    }
679
680    #[test]
681    fn test_browser_dialog() {
682        assert_eq!(
683            format_activity_message("browser", &json!({"action": "dialog_accept", "text": "OK"})),
684            "Executing browser: accepting dialog with \"OK\"..."
685        );
686        assert_eq!(
687            format_activity_message("browser", &json!({"action": "dialog_dismiss"})),
688            "Executing browser: dismissing dialog..."
689        );
690    }
691
692    #[test]
693    fn test_browser_storage() {
694        assert_eq!(
695            format_activity_message(
696                "browser",
697                &json!({"action": "cookies", "url": "https://example.com"})
698            ),
699            "Executing browser: getting cookies for https://example.com..."
700        );
701        assert_eq!(
702            format_activity_message("browser", &json!({"action": "storage_get", "key": "token"})),
703            "Executing browser: getting storage \"token\"..."
704        );
705        assert_eq!(
706            format_activity_message(
707                "browser",
708                &json!({"action": "storage_set", "key": "session"})
709            ),
710            "Executing browser: setting storage \"session\"..."
711        );
712    }
713
714    #[test]
715    fn test_browser_eval() {
716        assert_eq!(
717            format_activity_message(
718                "browser",
719                &json!({"action": "eval", "script": "return 1 + 1"})
720            ),
721            "Executing browser: evaluating return 1 + 1..."
722        );
723    }
724
725    #[test]
726    fn test_browser_unknown_action() {
727        assert_eq!(
728            format_activity_message("browser", &json!({"action": "unknown_action"})),
729            "Executing browser: unknown_action..."
730        );
731    }
732
733    #[test]
734    fn test_browser_missing_args() {
735        assert_eq!(
736            format_activity_message("browser", &json!({"action": "open"})),
737            "Executing browser: opening URL..."
738        );
739        assert_eq!(
740            format_activity_message("browser", &json!({"action": "click"})),
741            "Executing browser: clicking element..."
742        );
743        assert_eq!(
744            format_activity_message("browser", &json!({"action": "wait"})),
745            "Executing browser: waiting..."
746        );
747    }
748
749    #[test]
750    fn test_browser_navigation_extras() {
751        assert_eq!(
752            format_activity_message("browser", &json!({"action": "snapshot"})),
753            "Executing browser: taking DOM snapshot..."
754        );
755        assert_eq!(
756            format_activity_message("browser", &json!({"action": "forward"})),
757            "Executing browser: navigating forward..."
758        );
759    }
760
761    #[test]
762    fn test_browser_interaction_extras() {
763        assert_eq!(
764            format_activity_message(
765                "browser",
766                &json!({"action": "dblclick", "selector": "#btn"})
767            ),
768            "Executing browser: double-clicking #btn..."
769        );
770        assert_eq!(
771            format_activity_message("browser", &json!({"action": "focus", "selector": "#input"})),
772            "Executing browser: focusing #input..."
773        );
774        assert_eq!(
775            format_activity_message(
776                "browser",
777                &json!({"action": "scrollintoview", "selector": "#footer"})
778            ),
779            "Executing browser: scrolling to #footer..."
780        );
781        assert_eq!(
782            format_activity_message("browser", &json!({"action": "type", "selector": "#search"})),
783            "Executing browser: typing into #search..."
784        );
785        assert_eq!(
786            format_activity_message(
787                "browser",
788                &json!({"action": "select", "selector": "#country", "value": "BR"})
789            ),
790            "Executing browser: selecting \"BR\" in #country..."
791        );
792        assert_eq!(
793            format_activity_message(
794                "browser",
795                &json!({"action": "drag", "source": "#item", "target": "#dropzone"})
796            ),
797            "Executing browser: dragging #item to #dropzone..."
798        );
799        assert_eq!(
800            format_activity_message(
801                "browser",
802                &json!({"action": "upload", "selector": "#file", "path": "/tmp/file.txt"})
803            ),
804            "Executing browser: uploading /tmp/file.txt to #file..."
805        );
806        assert_eq!(
807            format_activity_message(
808                "browser",
809                &json!({"action": "pdf", "path": "/tmp/page.pdf"})
810            ),
811            "Executing browser: saving PDF to /tmp/page.pdf..."
812        );
813    }
814
815    #[test]
816    fn test_browser_query_extras() {
817        assert_eq!(
818            format_activity_message(
819                "browser",
820                &json!({"action": "get_box", "selector": "#modal"})
821            ),
822            "Executing browser: getting bounding box of #modal..."
823        );
824        assert_eq!(
825            format_activity_message(
826                "browser",
827                &json!({"action": "get_styles", "selector": ".button"})
828            ),
829            "Executing browser: getting styles of .button..."
830        );
831    }
832
833    #[test]
834    fn test_browser_wait_extras() {
835        assert_eq!(
836            format_activity_message("browser", &json!({"action": "wait_for_download"})),
837            "Executing browser: waiting for download..."
838        );
839        assert_eq!(
840            format_activity_message("browser", &json!({"action": "wait_for_fn"})),
841            "Executing browser: waiting for function result..."
842        );
843        assert_eq!(
844            format_activity_message(
845                "browser",
846                &json!({"action": "wait_for_state", "state": "visible"})
847            ),
848            "Executing browser: waiting for visible state..."
849        );
850    }
851
852    #[test]
853    fn test_browser_state() {
854        assert_eq!(
855            format_activity_message("browser", &json!({"action": "find", "selector": ".item"})),
856            "Executing browser: finding .item..."
857        );
858        assert_eq!(
859            format_activity_message("browser", &json!({"action": "scroll", "x": 0, "y": 500})),
860            "Executing browser: scrolling to (0, 500)..."
861        );
862        assert_eq!(
863            format_activity_message(
864                "browser",
865                &json!({"action": "is", "selector": "#btn", "state": "visible"})
866            ),
867            "Executing browser: checking if #btn is visible..."
868        );
869        assert_eq!(
870            format_activity_message(
871                "browser",
872                &json!({"action": "download", "url": "https://example.com/file.zip"})
873            ),
874            "Executing browser: downloading https://example.com/file.zip..."
875        );
876    }
877
878    #[test]
879    fn test_browser_storage_network() {
880        assert_eq!(
881            format_activity_message("browser", &json!({"action": "cookies_set"})),
882            "Executing browser: setting cookies..."
883        );
884        assert_eq!(
885            format_activity_message("browser", &json!({"action": "network_requests"})),
886            "Executing browser: getting network requests..."
887        );
888    }
889}