diff --git a/poc-agent/src/tools/bash.rs b/poc-agent/src/tools/bash.rs index cf5bcac..fdf6d0e 100644 --- a/poc-agent/src/tools/bash.rs +++ b/poc-agent/src/tools/bash.rs @@ -7,6 +7,7 @@ // display running commands and the user can kill them (Ctrl+K). use anyhow::{Context, Result}; +use serde::Deserialize; use serde_json::json; use std::process::Stdio; use std::sync::Arc; @@ -16,6 +17,15 @@ use tokio::sync::Mutex; use crate::types::ToolDef; +#[derive(Deserialize)] +struct Args { + command: String, + #[serde(default = "default_timeout")] + timeout_secs: u64, +} + +fn default_timeout() -> u64 { 120 } + /// Info about a running child process, visible to the TUI. #[derive(Debug, Clone)] pub struct ProcessInfo { @@ -94,8 +104,10 @@ pub fn definition() -> ToolDef { } pub async fn run_bash(args: &serde_json::Value, tracker: &ProcessTracker) -> Result { - let command = args["command"].as_str().context("command is required")?; - let timeout_secs = args["timeout_secs"].as_u64().unwrap_or(120); + let a: Args = serde_json::from_value(args.clone()) + .context("invalid bash arguments")?; + let command = &a.command; + let timeout_secs = a.timeout_secs; let mut child = tokio::process::Command::new("bash") .arg("-c") diff --git a/poc-agent/src/tools/grep.rs b/poc-agent/src/tools/grep.rs index 64e0bde..f49f5da 100644 --- a/poc-agent/src/tools/grep.rs +++ b/poc-agent/src/tools/grep.rs @@ -4,11 +4,25 @@ // isn't installed. Both produce compatible output. use anyhow::{Context, Result}; +use serde::Deserialize; use serde_json::json; use std::process::Command; use crate::types::ToolDef; +#[derive(Deserialize)] +struct Args { + pattern: String, + #[serde(default = "default_path")] + path: String, + glob: Option, + #[serde(default)] + show_content: bool, + context_lines: Option, +} + +fn default_path() -> String { ".".into() } + pub fn definition() -> ToolDef { ToolDef::new( "grep", @@ -51,16 +65,13 @@ fn has_rg() -> bool { } pub fn grep(args: &serde_json::Value) -> Result { - let pattern = get_str(args, "pattern")?; - let path = args["path"].as_str().unwrap_or("."); - let file_glob = args["glob"].as_str(); - let show_content = args["show_content"].as_bool().unwrap_or(false); - let context = args["context_lines"].as_u64(); + let a: Args = serde_json::from_value(args.clone()) + .context("invalid grep arguments")?; let output = if has_rg() { - run_search("rg", pattern, path, file_glob, show_content, context, true)? + run_search("rg", &a.pattern, &a.path, a.glob.as_deref(), a.show_content, a.context_lines, true)? } else { - run_search("grep", pattern, path, file_glob, show_content, context, false)? + run_search("grep", &a.pattern, &a.path, a.glob.as_deref(), a.show_content, a.context_lines, false)? }; if output.is_empty() { @@ -116,10 +127,3 @@ fn run_search( let output = cmd.output().with_context(|| format!("Failed to run {}", tool))?; Ok(String::from_utf8_lossy(&output.stdout).to_string()) } - -/// Helper: get required string argument. -fn get_str<'a>(args: &'a serde_json::Value, name: &'a str) -> Result<&'a str> { - args.get(name) - .and_then(|v| v.as_str()) - .context(format!("{} is required", name)) -} diff --git a/poc-agent/src/tools/vision.rs b/poc-agent/src/tools/vision.rs index 01173ed..f9ed968 100644 --- a/poc-agent/src/tools/vision.rs +++ b/poc-agent/src/tools/vision.rs @@ -6,10 +6,21 @@ use anyhow::{Context, Result}; use base64::Engine; +use serde::Deserialize; use super::ToolOutput; use crate::types::ToolDef; +#[derive(Deserialize)] +struct Args { + file_path: Option, + pane_id: Option, + #[serde(default = "default_lines")] + lines: usize, +} + +fn default_lines() -> usize { 50 } + pub fn definition() -> ToolDef { ToolDef::new( "view_image", @@ -39,13 +50,15 @@ pub fn definition() -> ToolDef { /// View an image file or capture a tmux pane. pub fn view_image(args: &serde_json::Value) -> Result { - if let Some(pane_id) = args.get("pane_id").and_then(|v| v.as_str()) { - return capture_tmux_pane(pane_id, args); + let a: Args = serde_json::from_value(args.clone()) + .context("invalid view_image arguments")?; + + if let Some(ref pane_id) = a.pane_id { + return capture_tmux_pane(pane_id, a.lines); } - let file_path = args - .get("file_path") - .and_then(|v| v.as_str()) + let file_path = a.file_path + .as_deref() .context("view_image requires either file_path or pane_id")?; let path = std::path::Path::new(file_path); @@ -83,13 +96,8 @@ pub fn view_image(args: &serde_json::Value) -> Result { }) } -/// Capture a tmux pane to a PNG screenshot using tmux's capture-pane. -/// Falls back to text capture if image capture isn't available. -fn capture_tmux_pane(pane_id: &str, args: &serde_json::Value) -> Result { - let lines = args - .get("lines") - .and_then(|v| v.as_u64()) - .unwrap_or(50) as usize; +/// Capture a tmux pane's text content. +fn capture_tmux_pane(pane_id: &str, lines: usize) -> Result { // Use tmux capture-pane to get text content, then render to image // via a simple approach: capture text and return it (the model can