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