use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::Span,
widgets::Widget,
};
use crate::widgets::ai_chat::components::theme::ChatColors;
use super::block_tool::BlockTool;
use super::inline_tool::ToolStatus;
#[derive(Debug, Clone, PartialEq)]
pub struct SubAgentToolCall {
pub tool_name: String,
pub arguments: String,
}
pub struct ToolTask<'a> {
subagent_type: &'a str,
tool_calls: Vec<SubAgentToolCall>,
current_operation: Option<String>,
error: Option<String>,
status: ToolStatus,
expanded: bool,
}
impl<'a> ToolTask<'a> {
pub fn new(subagent_type: &'a str) -> Self {
Self {
subagent_type,
tool_calls: Vec::new(),
current_operation: None,
error: None,
status: ToolStatus::Pending,
expanded: false,
}
}
pub fn add_tool_call(mut self, call: SubAgentToolCall) -> Self {
self.tool_calls.push(call);
self
}
pub fn tool_calls(mut self, calls: Vec<SubAgentToolCall>) -> Self {
self.tool_calls = calls;
self
}
pub fn current_operation(mut self, op: Option<String>) -> Self {
self.current_operation = op;
self
}
pub fn error(mut self, err: Option<String>) -> Self {
self.error = err;
if err.is_some() {
self.status = ToolStatus::Error;
}
self
}
pub fn status(mut self, status: ToolStatus) -> Self {
self.status = status;
self
}
pub fn expanded(mut self, expanded: bool) -> Self {
self.expanded = expanded;
self
}
fn border_color(&self, colors: &ChatColors) -> Color {
match self.status {
ToolStatus::Pending => colors.warning,
ToolStatus::Complete => colors.success,
ToolStatus::Error => colors.error,
ToolStatus::PermissionPending => Color::Rgb(255, 165, 0),
}
}
pub fn render(&self, area: Rect, buf: &mut Buffer, colors: &ChatColors) {
if area.height < 1 {
return;
}
let max_y = area.y + area.height;
let mut y = area.y;
let border_color = self.border_color(colors);
for y_pos in area.y..max_y {
buf.get_mut(area.x, y_pos)
.set_char('│')
.set_style(Style::default().fg(border_color));
}
let icon = '#';
let expand_icon = if self.expanded { "▼" } else { "▶" };
let tool_count = self.tool_calls.len();
let status_text = match self.status {
ToolStatus::Pending => "Working...",
ToolStatus::Complete => "Complete",
ToolStatus::Error => "Failed",
ToolStatus::PermissionPending => "Waiting...",
};
let header = format!(
"{} Task: {} [{}] {}",
icon, self.subagent_type, tool_count, expand_icon
);
let header_span = Span::styled(
header,
Style::default()
.fg(colors.accent)
.add_modifier(Modifier::BOLD),
);
buf.set_span(area.x + 2, y, &header_span, area.width.saturating_sub(3));
y += 1;
if y < max_y {
buf.set_span(
area.x + 2,
y,
&Span::styled(
"─".repeat(25),
Style::default()
.fg(border_color)
.add_modifier(Modifier::DIM),
),
area.width.saturating_sub(3),
);
y += 1;
}
if let Some(op) = &self.current_operation {
if y < max_y {
let op_span = Span::styled(
format!(" ⚙ {}", op),
Style::default()
.fg(colors.warning)
.add_modifier(Modifier::ITALIC),
);
buf.set_span(area.x + 2, y, &op_span, area.width.saturating_sub(3));
y += 1;
}
}
if let Some(err) = &self.error {
if y < max_y {
let err_span = Span::styled(
format!(" ✗ Error: {}", err),
Style::default()
.fg(colors.error)
.add_modifier(Modifier::BOLD),
);
buf.set_span(area.x + 2, y, &err_span, area.width.saturating_sub(3));
y += 1;
}
}
if self.expanded && !self.tool_calls.is_empty() && y < max_y {
let calls_header = Span::styled(
" Tool calls:",
Style::default()
.fg(colors.text_muted)
.add_modifier(Modifier::BOLD),
);
buf.set_span(area.x + 2, y, &calls_header, area.width.saturating_sub(3));
y += 1;
for call in self
.tool_calls
.iter()
.take(area.height.saturating_sub(y - area.y) as usize)
{
if y >= max_y {
break;
}
let call_span = Span::styled(
format!(" → {}: {}", call.tool_name, call.arguments),
Style::default().fg(colors.text_muted),
);
buf.set_span(area.x + 2, y, &call_span, area.width.saturating_sub(3));
y += 1;
}
}
if y < max_y {
let status_text = match self.status {
ToolStatus::Pending => "⏳ Subagent is working...",
ToolStatus::Complete => "✓ Task completed",
ToolStatus::Error => "✗ Task failed",
ToolStatus::PermissionPending => "⚠ Waiting for approval",
};
let status_span = Span::styled(
status_text,
Style::default()
.fg(border_color)
.add_modifier(Modifier::BOLD),
);
buf.set_span(area.x + 2, y, &status_span, area.width.saturating_sub(3));
}
}
pub fn toggle_expanded(&mut self) {
self.expanded = !self.expanded;
}
pub fn is_clickable(&self, x: u16, y: u16, area: Rect) -> bool {
y == area.y && x >= area.x + 2
}
}
impl Widget for ToolTask<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = ChatColors::default();
self.render(area, buf, &colors);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_task_basic() {
let task = ToolTask::new("explore");
assert_eq!(task.subagent_type, "explore");
}
#[test]
fn test_tool_task_with_calls() {
let task = ToolTask::new("code").add_tool_call(SubAgentToolCall {
tool_name: "read".to_string(),
arguments: "src/main.rs".to_string(),
});
assert_eq!(task.tool_calls.len(), 1);
}
#[test]
fn test_tool_task_error() {
let task = ToolTask::new("test").error(Some("Connection failed".to_string()));
assert!(task.error.is_some());
assert_eq!(task.status, ToolStatus::Error);
}
}