diff --git a/Cargo.lock b/Cargo.lock index ee63ff7..54c27f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5228,7 +5228,3 @@ name = "zmij" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" - -[[patch.unused]] -name = "ratatui-core" -version = "0.1.0-beta.0" diff --git a/src/tools/impls/agent_management.rs b/src/tools/impls/agent_management.rs index ecdfd82..30670a2 100644 --- a/src/tools/impls/agent_management.rs +++ b/src/tools/impls/agent_management.rs @@ -10,161 +10,44 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use super::{handlers, Tool, ToolPipeline}; -use crate::impl_tool_block; +use crate::define_simple_tool_block; use crate::transcript::{render_approval_prompt, render_prefix, render_result, Block, BlockType, Status}; // ============================================================================= // ListAgents block // ============================================================================= -/// Block for list_agents - shows as `list_agents()` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListAgentsBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, -} - -impl ListAgentsBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, +define_simple_tool_block! { + /// Block for list_agents - shows as `list_agents()` + pub struct ListAgentsBlock { + max_lines: 10, + render_header(self, params) { + vec![ + Span::styled("list_agents", Style::default().fg(Color::Magenta)), + Span::styled("()", Style::default().fg(Color::DarkGray)), + ] } } } -#[typetag::serde] -impl Block for ListAgentsBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let spans = vec![ - self.render_status(), - render_prefix(self.background), - Span::styled("list_agents", Style::default().fg(Color::Magenta)), - Span::styled("()", Style::default().fg(Color::DarkGray)), - ]; - lines.push(Line::from(spans)); - - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 10)); - } - - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); - } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } -} - // ============================================================================= // GetAgent block // ============================================================================= -/// Block for get_agent - shows as `get_agent(agent_id)` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetAgentBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, -} - -impl GetAgentBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - } - } -} - -#[typetag::serde] -impl Block for GetAgentBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let label = self.params["label"].as_str() - .unwrap_or("?"); - - let spans = vec![ - self.render_status(), - render_prefix(self.background), - Span::styled("get_agent", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(label.to_string(), Style::default().fg(Color::Yellow)), - Span::styled(")", Style::default().fg(Color::DarkGray)), - ]; - lines.push(Line::from(spans)); - - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 10)); - } - - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); +define_simple_tool_block! { + /// Block for get_agent - shows as `get_agent(agent_id)` + pub struct GetAgentBlock { + max_lines: 10, + render_header(self, params) { + let label = params["label"].as_str().unwrap_or("?"); + + vec![ + Span::styled("get_agent", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(label.to_string(), Style::default().fg(Color::Yellow)), + Span::styled(")", Style::default().fg(Color::DarkGray)), + ] } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) } } diff --git a/src/tools/impls/background_tasks.rs b/src/tools/impls/background_tasks.rs index 42291bd..89d59f8 100644 --- a/src/tools/impls/background_tasks.rs +++ b/src/tools/impls/background_tasks.rs @@ -10,168 +10,44 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use super::{handlers, Tool, ToolPipeline}; -use crate::impl_tool_block; +use crate::define_simple_tool_block; use crate::transcript::{render_approval_prompt, render_prefix, render_result, Block, BlockType, Status}; // ============================================================================= // ListBackgroundTasks block // ============================================================================= -/// Block for list_background_tasks - shows as `list_background_tasks()` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListBackgroundTasksBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, -} - -impl ListBackgroundTasksBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, +define_simple_tool_block! { + /// Block for list_background_tasks - shows as `list_background_tasks()` + pub struct ListBackgroundTasksBlock { + max_lines: 10, + render_header(self, params) { + vec![ + Span::styled("list_background_tasks", Style::default().fg(Color::Magenta)), + Span::styled("()", Style::default().fg(Color::DarkGray)), + ] } } } -#[typetag::serde] -impl Block for ListBackgroundTasksBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - // Format: list_background_tasks() - let spans = vec![ - self.render_status(), - render_prefix(self.background), - Span::styled("list_background_tasks", Style::default().fg(Color::Magenta)), - Span::styled("()", Style::default().fg(Color::DarkGray)), - ]; - lines.push(Line::from(spans)); - - // Approval prompt if pending - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - // Output if completed - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 10)); - } - - // Denied message - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); - } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } -} - // ============================================================================= // GetBackgroundTask block // ============================================================================= -/// Block for get_background_task - shows as `get_background_task(task_id)` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetBackgroundTaskBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, -} - -impl GetBackgroundTaskBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - } - } -} - -#[typetag::serde] -impl Block for GetBackgroundTaskBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let task_id = self.params["task_id"].as_str().unwrap_or(""); - - // Format: get_background_task(task_id) - let spans = vec![ - self.render_status(), - render_prefix(self.background), - Span::styled("get_background_task", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(task_id, Style::default().fg(Color::White)), - Span::styled(")", Style::default().fg(Color::DarkGray)), - ]; - lines.push(Line::from(spans)); - - // Approval prompt if pending - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - // Output if completed - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 10)); - } - - // Denied message - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); +define_simple_tool_block! { + /// Block for get_background_task - shows as `get_background_task(task_id)` + pub struct GetBackgroundTaskBlock { + max_lines: 10, + render_header(self, params) { + let task_id = params["task_id"].as_str().unwrap_or(""); + + vec![ + Span::styled("get_background_task", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(task_id.to_string(), Style::default().fg(Color::White)), + Span::styled(")", Style::default().fg(Color::DarkGray)), + ] } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) } } diff --git a/src/tools/impls/edit_file.rs b/src/tools/impls/edit_file.rs index 80a6124..dfcc13d 100644 --- a/src/tools/impls/edit_file.rs +++ b/src/tools/impls/edit_file.rs @@ -26,7 +26,7 @@ use serde_json::json; use super::{handlers, Tool, ToolPipeline}; use crate::ide::Edit; -use crate::impl_tool_block; +use crate::define_tool_block; use crate::tools::pipeline::{EffectHandler, Step}; use crate::transcript::{ render_agent_label, render_approval_prompt, render_prefix, render_result, Block, BlockType, Status, ToolBlock, @@ -76,115 +76,34 @@ impl EffectHandler for ValidateEdits { } } -/// Edit file display block -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EditFileBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, - /// Agent label for sub-agent tools - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent_label: Option, -} - -impl EditFileBlock { - pub fn new( - call_id: impl Into, - tool_name: impl Into, - params: serde_json::Value, - background: bool, - ) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - agent_label: None, - } - } - - pub fn from_params(call_id: &str, tool_name: &str, params: serde_json::Value, background: bool) -> Option { - let _: EditFileParams = serde_json::from_value(params.clone()).ok()?; - Some(Self::new(call_id, tool_name, params, background)) - } -} - -#[typetag::serde] -impl Block for EditFileBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let path = self.params["path"].as_str().unwrap_or(""); - let edit_count = self - .params - .get("edits") - .and_then(|v| v.as_array()) - .map(|a| a.len()) - .unwrap_or(0); - - // Format: [agent_label] edit_file(path, N edits) - lines.push(Line::from(vec![ - self.render_status(), - render_agent_label(self.agent_label.as_deref()), - render_prefix(self.background), - Span::styled("edit_file", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(path, Style::default().fg(Color::Yellow)), - Span::styled( - format!( - ", {} edit{}", - edit_count, - if edit_count == 1 { "" } else { "s" } +define_tool_block! { + /// Edit file display block + pub struct EditFileBlock { + max_lines: 5, + params_type: EditFileParams, + render_header(self, params) { + let path = params["path"].as_str().unwrap_or(""); + let edit_count = params + .get("edits") + .and_then(|v| v.as_array()) + .map(|a| a.len()) + .unwrap_or(0); + + vec![ + Span::styled("edit_file", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(path.to_string(), Style::default().fg(Color::Yellow)), + Span::styled( + format!( + ", {} edit{}", + edit_count, + if edit_count == 1 { "" } else { "s" } + ), + Style::default().fg(Color::DarkGray), ), - Style::default().fg(Color::DarkGray), - ), - Span::styled(")", Style::default().fg(Color::DarkGray)), - ])); - - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 5)); - } - - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); + Span::styled(")", Style::default().fg(Color::DarkGray)), + ] } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } - - fn set_agent_label(&mut self, label: String) { - self.agent_label = Some(label); - } - - fn agent_label(&self) -> Option<&str> { - self.agent_label.as_deref() } } diff --git a/src/tools/impls/fetch_html.rs b/src/tools/impls/fetch_html.rs index 7bbc8d7..817af59 100644 --- a/src/tools/impls/fetch_html.rs +++ b/src/tools/impls/fetch_html.rs @@ -11,104 +11,26 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use super::{handlers, Tool, ToolPipeline}; -use crate::impl_tool_block; +use crate::define_tool_block; use crate::transcript::{ render_agent_label, render_approval_prompt, render_prefix, render_result, Block, BlockType, Status, ToolBlock, }; -/// Fetch HTML display block -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FetchHtmlBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, - #[serde(default)] - pub agent_label: Option, -} - -impl FetchHtmlBlock { - pub fn new( - call_id: impl Into, - tool_name: impl Into, - params: serde_json::Value, - background: bool, - ) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - agent_label: None, - } - } - - pub fn from_params(call_id: &str, tool_name: &str, params: serde_json::Value, background: bool) -> Option { - let _: FetchHtmlParams = serde_json::from_value(params.clone()).ok()?; - Some(Self::new(call_id, tool_name, params, background)) - } -} - -#[typetag::serde] -impl Block for FetchHtmlBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let url = self.params["url"].as_str().unwrap_or(""); - - lines.push(Line::from(vec![ - self.render_status(), - render_prefix(self.background), - render_agent_label(self.agent_label.as_deref()), - Span::styled("fetch_html", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(url, Style::default().fg(Color::Blue)), - Span::styled(")", Style::default().fg(Color::DarkGray)), - ])); - - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 5)); +define_tool_block! { + /// Fetch HTML display block + pub struct FetchHtmlBlock { + max_lines: 5, + params_type: FetchHtmlParams, + render_header(self, params) { + let url = params["url"].as_str().unwrap_or(""); + + vec![ + Span::styled("fetch_html", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(url.to_string(), Style::default().fg(Color::Blue)), + Span::styled(")", Style::default().fg(Color::DarkGray)), + ] } - - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); - } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } - - fn set_agent_label(&mut self, label: String) { - self.agent_label = Some(label); - } - - fn agent_label(&self) -> Option<&str> { - self.agent_label.as_deref() } } diff --git a/src/tools/impls/fetch_url.rs b/src/tools/impls/fetch_url.rs index 336fe3b..1bf6467 100644 --- a/src/tools/impls/fetch_url.rs +++ b/src/tools/impls/fetch_url.rs @@ -1,7 +1,7 @@ //! URL fetching tool use super::{handlers, Tool, ToolPipeline}; -use crate::impl_tool_block; +use crate::define_tool_block; use crate::transcript::{render_agent_label, render_approval_prompt, render_prefix, render_result, Block, BlockType, ToolBlock, Status}; use ratatui::{ style::{Color, Style}, @@ -10,95 +10,21 @@ use ratatui::{ use serde::{Deserialize, Serialize}; use serde_json::json; -/// Fetch URL display block -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FetchUrlBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, - #[serde(default)] - pub agent_label: Option, -} - -impl FetchUrlBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - agent_label: None, - } - } - - pub fn from_params(call_id: &str, tool_name: &str, params: serde_json::Value, background: bool) -> Option { - let _: FetchUrlParams = serde_json::from_value(params.clone()).ok()?; - Some(Self::new(call_id, tool_name, params, background)) - } -} - -#[typetag::serde] -impl Block for FetchUrlBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let url = self.params["url"].as_str().unwrap_or(""); - - // Format: fetch_url(url) - lines.push(Line::from(vec![ - self.render_status(), - render_prefix(self.background), - render_agent_label(self.agent_label.as_deref()), - Span::styled("fetch_url", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(url, Style::default().fg(Color::Blue)), - Span::styled(")", Style::default().fg(Color::DarkGray)), - ])); - - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 5)); +define_tool_block! { + /// Fetch URL display block + pub struct FetchUrlBlock { + max_lines: 5, + params_type: FetchUrlParams, + render_header(self, params) { + let url = params["url"].as_str().unwrap_or(""); + + vec![ + Span::styled("fetch_url", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(url.to_string(), Style::default().fg(Color::Blue)), + Span::styled(")", Style::default().fg(Color::DarkGray)), + ] } - - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); - } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } - - fn set_agent_label(&mut self, label: String) { - self.agent_label = Some(label); - } - - fn agent_label(&self) -> Option<&str> { - self.agent_label.as_deref() } } diff --git a/src/tools/impls/read_file.rs b/src/tools/impls/read_file.rs index 9bb9e82..29df35b 100644 --- a/src/tools/impls/read_file.rs +++ b/src/tools/impls/read_file.rs @@ -1,7 +1,7 @@ //! Read file tool use super::{handlers, Tool, ToolPipeline}; -use crate::impl_tool_block; +use crate::define_tool_block; use crate::transcript::{render_agent_label, render_approval_prompt, render_prefix, render_result, Block, BlockType, ToolBlock, Status}; use ratatui::{ style::{Color, Style}, @@ -11,106 +11,33 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::path::PathBuf; -/// Read file display block -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReadFileBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, - #[serde(default)] - pub agent_label: Option, -} - -impl ReadFileBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - agent_label: None, +define_tool_block! { + /// Read file display block + pub struct ReadFileBlock { + max_lines: 10, + params_type: ReadFileParams, + render_header(self, params) { + let path = params["path"].as_str().unwrap_or(""); + let start_line = params.get("start_line").and_then(|v| v.as_i64()); + let end_line = params.get("end_line").and_then(|v| v.as_i64()); + + // Format: read_file(path:start-end) or read_file(path) + let range_str = match (start_line, end_line) { + (Some(s), Some(e)) => format!(":{}:{}", s, e), + (Some(s), None) => format!(":{}:", s), + (None, Some(e)) => format!(":{}", e), + (None, None) => String::new(), + }; + + vec![ + Span::styled("read_file", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(path.to_string(), Style::default().fg(Color::Cyan)), + Span::styled(range_str, Style::default().fg(Color::DarkGray)), + Span::styled(")", Style::default().fg(Color::DarkGray)), + ] } } - - pub fn from_params(call_id: &str, tool_name: &str, params: serde_json::Value, background: bool) -> Option { - let _: ReadFileParams = serde_json::from_value(params.clone()).ok()?; - Some(Self::new(call_id, tool_name, params, background)) - } -} - -#[typetag::serde] -impl Block for ReadFileBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let path = self.params["path"].as_str().unwrap_or(""); - let start_line = self.params.get("start_line").and_then(|v| v.as_i64()); - let end_line = self.params.get("end_line").and_then(|v| v.as_i64()); - - // Format: read_file(path:start-end) or read_file(path) - let range_str = match (start_line, end_line) { - (Some(s), Some(e)) => format!(":{}:{}", s, e), - (Some(s), None) => format!(":{}:", s), - (None, Some(e)) => format!(":{}", e), - (None, None) => String::new(), - }; - - lines.push(Line::from(vec![ - self.render_status(), - render_prefix(self.background), - render_agent_label(self.agent_label.as_deref()), - Span::styled("read_file", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(path, Style::default().fg(Color::Cyan)), - Span::styled(range_str, Style::default().fg(Color::DarkGray)), - Span::styled(")", Style::default().fg(Color::DarkGray)), - ])); - - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 10)); - } - - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); - } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } - - fn set_agent_label(&mut self, label: String) { - self.agent_label = Some(label); - } - - fn agent_label(&self) -> Option<&str> { - self.agent_label.as_deref() - } } /// Tool for reading file contents diff --git a/src/tools/impls/shell.rs b/src/tools/impls/shell.rs index 023c364..82f42ad 100644 --- a/src/tools/impls/shell.rs +++ b/src/tools/impls/shell.rs @@ -1,7 +1,7 @@ //! Shell command execution tool use super::{handlers, Tool, ToolPipeline}; -use crate::impl_tool_block; +use crate::define_tool_block; use crate::transcript::{render_agent_label, render_approval_prompt, render_prefix, render_result, Block, BlockType, ToolBlock, Status}; use ratatui::{ style::{Color, Style}, @@ -10,105 +10,26 @@ use ratatui::{ use serde::{Deserialize, Serialize}; use serde_json::json; -/// Shell command block - shows the command cleanly -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShellBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, - /// Agent label for sub-agent tools - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent_label: Option, -} - -impl ShellBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - agent_label: None, - } - } - - /// Create from tool params JSON - pub fn from_params(call_id: &str, tool_name: &str, params: serde_json::Value, background: bool) -> Option { - let _: ShellParams = serde_json::from_value(params.clone()).ok()?; - Some(Self::new(call_id, tool_name, params, background)) - } -} - -#[typetag::serde] -impl Block for ShellBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let command = self.params["command"].as_str().unwrap_or(""); - let working_dir = self.params.get("working_dir").and_then(|v| v.as_str()); - - // Format: [agent_label] shell(command) or shell(command, in dir) - let mut spans = vec![ - self.render_status(), - render_agent_label(self.agent_label.as_deref()), - render_prefix(self.background), - Span::styled("shell", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(command, Style::default().fg(Color::White)), - ]; - if let Some(dir) = working_dir { - spans.push(Span::styled(format!(", in {}", dir), Style::default().fg(Color::DarkGray))); - } - spans.push(Span::styled(")", Style::default().fg(Color::DarkGray))); - lines.push(Line::from(spans)); - - // Approval prompt if pending - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - // Output if completed - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 10)); - } - - // Denied message - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); +define_tool_block! { + /// Shell command block - shows the command cleanly + pub struct ShellBlock { + max_lines: 10, + params_type: ShellParams, + render_header(self, params) { + let command = params["command"].as_str().unwrap_or(""); + let working_dir = params.get("working_dir").and_then(|v| v.as_str()); + + let mut spans = vec![ + Span::styled("shell", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(command.to_string(), Style::default().fg(Color::White)), + ]; + if let Some(dir) = working_dir { + spans.push(Span::styled(format!(", in {}", dir), Style::default().fg(Color::DarkGray))); + } + spans.push(Span::styled(")", Style::default().fg(Color::DarkGray))); + spans } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } - - fn set_agent_label(&mut self, label: String) { - self.agent_label = Some(label); - } - - fn agent_label(&self) -> Option<&str> { - self.agent_label.as_deref() } } diff --git a/src/tools/impls/write_file.rs b/src/tools/impls/write_file.rs index 58b3f05..d96c32d 100644 --- a/src/tools/impls/write_file.rs +++ b/src/tools/impls/write_file.rs @@ -13,7 +13,7 @@ use super::{handlers, Tool, ToolPipeline}; use crate::ide::ToolPreview; -use crate::impl_tool_block; +use crate::define_tool_block; use crate::transcript::{render_agent_label, render_approval_prompt, render_prefix, render_result, Block, BlockType, ToolBlock, Status}; use ratatui::{ style::{Color, Style}, @@ -23,98 +23,23 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::path::PathBuf; -/// Write file display block -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WriteFileBlock { - pub call_id: String, - pub tool_name: String, - pub params: serde_json::Value, - pub status: Status, - pub text: String, - #[serde(default)] - pub background: bool, - /// Agent label for sub-agent tools - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent_label: Option, -} - -impl WriteFileBlock { - pub fn new(call_id: impl Into, tool_name: impl Into, params: serde_json::Value, background: bool) -> Self { - Self { - call_id: call_id.into(), - tool_name: tool_name.into(), - params, - status: Status::Pending, - text: String::new(), - background, - agent_label: None, - } - } - - pub fn from_params(call_id: &str, tool_name: &str, params: serde_json::Value, background: bool) -> Option { - let _: WriteFileParams = serde_json::from_value(params.clone()).ok()?; - Some(Self::new(call_id, tool_name, params, background)) - } -} - -#[typetag::serde] -impl Block for WriteFileBlock { - impl_tool_block!(BlockType::Tool); - - fn render(&self, _width: u16) -> Vec> { - let mut lines = Vec::new(); - - let path = self.params["path"].as_str().unwrap_or(""); - let content_len = self.params.get("content").and_then(|v| v.as_str()).map(|s| s.len()).unwrap_or(0); - - // Format: [agent_label] write_file(path, N bytes) - lines.push(Line::from(vec![ - self.render_status(), - render_agent_label(self.agent_label.as_deref()), - render_prefix(self.background), - Span::styled("write_file", Style::default().fg(Color::Magenta)), - Span::styled("(", Style::default().fg(Color::DarkGray)), - Span::styled(path, Style::default().fg(Color::Green)), - Span::styled(format!(", {} bytes", content_len), Style::default().fg(Color::DarkGray)), - Span::styled(")", Style::default().fg(Color::DarkGray)), - ])); - - if self.status == Status::Pending { - lines.push(render_approval_prompt()); - } - - if !self.text.is_empty() { - lines.extend(render_result(&self.text, 5)); +define_tool_block! { + /// Write file display block + pub struct WriteFileBlock { + max_lines: 5, + params_type: WriteFileParams, + render_header(self, params) { + let path = params["path"].as_str().unwrap_or(""); + let content_len = params.get("content").and_then(|v| v.as_str()).map(|s| s.len()).unwrap_or(0); + + vec![ + Span::styled("write_file", Style::default().fg(Color::Magenta)), + Span::styled("(", Style::default().fg(Color::DarkGray)), + Span::styled(path.to_string(), Style::default().fg(Color::Green)), + Span::styled(format!(", {} bytes", content_len), Style::default().fg(Color::DarkGray)), + Span::styled(")", Style::default().fg(Color::DarkGray)), + ] } - - if self.status == Status::Denied { - lines.push(Line::from(Span::styled( - " Denied by user", - Style::default().fg(Color::DarkGray), - ))); - } - - lines - } - - fn call_id(&self) -> Option<&str> { - Some(&self.call_id) - } - - fn tool_name(&self) -> Option<&str> { - Some(&self.tool_name) - } - - fn params(&self) -> Option<&serde_json::Value> { - Some(&self.params) - } - - fn set_agent_label(&mut self, label: String) { - self.agent_label = Some(label); - } - - fn agent_label(&self) -> Option<&str> { - self.agent_label.as_deref() } } diff --git a/src/transcript.rs b/src/transcript.rs index f63ba47..efc74c3 100644 --- a/src/transcript.rs +++ b/src/transcript.rs @@ -227,6 +227,250 @@ macro_rules! impl_tool_block { }; } +/// Macro to define a complete tool block with minimal boilerplate. +/// +/// This macro generates the struct definition, constructors, and Block trait +/// implementation. You only need to provide the custom header rendering logic. +/// +/// # Usage +/// +/// ```ignore +/// define_tool_block! { +/// /// Doc comment for the block +/// pub struct MyToolBlock { +/// // Maximum lines to show in result output (default: 10) +/// max_lines: 5, +/// // Type to validate params against +/// params_type: MyToolParams, +/// // Custom header rendering - returns Vec +/// render_header(self, params) { +/// vec![ +/// Span::styled("my_tool", Style::default().fg(Color::Magenta)), +/// Span::styled("(", Style::default().fg(Color::DarkGray)), +/// Span::styled(params["arg"].as_str().unwrap_or(""), Style::default().fg(Color::Cyan)), +/// Span::styled(")", Style::default().fg(Color::DarkGray)), +/// ] +/// } +/// } +/// } +/// ``` +#[macro_export] +macro_rules! define_tool_block { + ( + $(#[$attr:meta])* + $vis:vis struct $name:ident { + max_lines: $max_lines:expr, + params_type: $params_type:ty, + render_header($self:ident, $params:ident) $render_body:block + } + ) => { + $(#[$attr])* + #[derive(Debug, Clone, Serialize, Deserialize)] + $vis struct $name { + pub call_id: String, + pub tool_name: String, + pub params: serde_json::Value, + pub status: Status, + pub text: String, + #[serde(default)] + pub background: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_label: Option, + } + + impl $name { + pub fn new( + call_id: impl Into, + tool_name: impl Into, + params: serde_json::Value, + background: bool, + ) -> Self { + Self { + call_id: call_id.into(), + tool_name: tool_name.into(), + params, + status: Status::Pending, + text: String::new(), + background, + agent_label: None, + } + } + + pub fn from_params( + call_id: &str, + tool_name: &str, + params: serde_json::Value, + background: bool, + ) -> Option { + let _: $params_type = serde_json::from_value(params.clone()).ok()?; + Some(Self::new(call_id, tool_name, params, background)) + } + + #[allow(unused_variables)] + fn render_header_spans(&$self) -> Vec> { + let $params = &$self.params; + $render_body + } + } + + #[typetag::serde] + impl Block for $name { + $crate::impl_tool_block!(BlockType::Tool); + + fn render(&self, _width: u16) -> Vec> { + let mut lines = Vec::new(); + + // Build header line: status + agent_label + prefix + custom spans + let mut spans = vec![ + self.render_status(), + render_agent_label(self.agent_label.as_deref()), + render_prefix(self.background), + ]; + spans.extend(self.render_header_spans()); + lines.push(Line::from(spans)); + + // Approval prompt if pending + if self.status == Status::Pending { + lines.push(render_approval_prompt()); + } + + // Result output + if !self.text.is_empty() { + lines.extend(render_result(&self.text, $max_lines)); + } + + // Denied message + if self.status == Status::Denied { + lines.push(Line::from(Span::styled( + " Denied by user", + Style::default().fg(Color::DarkGray), + ))); + } + + lines + } + + fn call_id(&self) -> Option<&str> { + Some(&self.call_id) + } + + fn tool_name(&self) -> Option<&str> { + Some(&self.tool_name) + } + + fn params(&self) -> Option<&serde_json::Value> { + Some(&self.params) + } + + fn set_agent_label(&mut self, label: String) { + self.agent_label = Some(label); + } + + fn agent_label(&self) -> Option<&str> { + self.agent_label.as_deref() + } + } + }; +} + +/// Macro for simple tool blocks without agent_label support. +/// Used for internal tools like list_agents, get_agent, list_background_tasks, etc. +#[macro_export] +macro_rules! define_simple_tool_block { + ( + $(#[$attr:meta])* + $vis:vis struct $name:ident { + max_lines: $max_lines:expr, + render_header($self:ident, $params:ident) $render_body:block + } + ) => { + $(#[$attr])* + #[derive(Debug, Clone, Serialize, Deserialize)] + $vis struct $name { + pub call_id: String, + pub tool_name: String, + pub params: serde_json::Value, + pub status: Status, + pub text: String, + #[serde(default)] + pub background: bool, + } + + impl $name { + pub fn new( + call_id: impl Into, + tool_name: impl Into, + params: serde_json::Value, + background: bool, + ) -> Self { + Self { + call_id: call_id.into(), + tool_name: tool_name.into(), + params, + status: Status::Pending, + text: String::new(), + background, + } + } + + #[allow(unused_variables)] + fn render_header_spans(&$self) -> Vec> { + let $params = &$self.params; + $render_body + } + } + + #[typetag::serde] + impl Block for $name { + $crate::impl_tool_block!(BlockType::Tool); + + fn render(&self, _width: u16) -> Vec> { + let mut lines = Vec::new(); + + // Build header line: status + prefix + custom spans + let mut spans = vec![ + self.render_status(), + render_prefix(self.background), + ]; + spans.extend(self.render_header_spans()); + lines.push(Line::from(spans)); + + // Approval prompt if pending + if self.status == Status::Pending { + lines.push(render_approval_prompt()); + } + + // Result output + if !self.text.is_empty() { + lines.extend(render_result(&self.text, $max_lines)); + } + + // Denied message + if self.status == Status::Denied { + lines.push(Line::from(Span::styled( + " Denied by user", + Style::default().fg(Color::DarkGray), + ))); + } + + lines + } + + fn call_id(&self) -> Option<&str> { + Some(&self.call_id) + } + + fn tool_name(&self) -> Option<&str> { + Some(&self.tool_name) + } + + fn params(&self) -> Option<&serde_json::Value> { + Some(&self.params) + } + } + }; +} + /// Simple text content #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TextBlock {