//! Workflow status panel rendering — agent cards with state badges. use super::theme::Theme; use crate::state::AgentState; use ratatui::layout::Rect; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; use ratatui::Frame; use tracing::{debug, instrument}; fn state_icon(state: AgentState) -> &'static str { match state { AgentState::Idle => "○", AgentState::Running => "▶", AgentState::Completed => "✓", AgentState::Failed => "✗", } } fn state_label(state: AgentState) -> &'static str { match state { AgentState::Idle => "Idle", AgentState::Running => "Running", AgentState::Completed => "Done", AgentState::Failed => "Failed", } } fn state_color(state: AgentState) -> Color { match state { AgentState::Idle => Theme::TEXT_DIM, AgentState::Running => Theme::WARNING, AgentState::Completed => Theme::SUCCESS, AgentState::Failed => Theme::ERROR, } } /// Render the workflow status panel showing agent cards and a header. /// /// Flow: render titled block → split into header (command hint + status) /// and body (agent cards with icon/name/label/duration, or session stats /// when no workflow is running). #[instrument(skip_all)] pub fn draw_workflow_panel( frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest, ) { debug!("draw_workflow_panel — rendering workflow panel"); use ratatui::layout::{Constraint, Direction, Layout}; let title = Span::styled( " Workflow ", Style::default() .fg(Theme::PRIMARY) .add_modifier(Modifier::BOLD), ); let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(Theme::BORDER)) .title(title); let inner = block.inner(area); frame.render_widget(block, area); let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(4)]) .split(inner); // Header area let mut header_lines: Vec = Vec::new(); header_lines.push(Line::from(vec![ Span::styled( "/workflow run ", Style::default() .fg(Theme::PRIMARY) .add_modifier(Modifier::BOLD), ), Span::styled("", Style::default().fg(Theme::TEXT_DIM)), ])); header_lines.push(Line::from(vec![ Span::styled("Status: ", Style::default().fg(Theme::TEXT_DIM)), if state.turn_in_flight() { Span::styled( "● Running", Style::default() .fg(Theme::WARNING) .add_modifier(Modifier::BOLD), ) } else { Span::styled("● Idle", Style::default().fg(Theme::SUCCESS)) }, Span::raw(" "), Span::styled( format!( "Agents: {} | Findings: {}", state.workflow_engine.agents.len(), state.workflow_engine.findings.len(), ), Style::default().fg(Theme::TEXT_DIM), ), ])); let header = Paragraph::new(header_lines); frame.render_widget(header, chunks[0]); // Body: agent cards if state.workflow_engine.agents.is_empty() { let session_lines = build_session_lines(state); let placeholder = Paragraph::new(session_lines).wrap(Wrap { trim: false }); frame.render_widget(placeholder, chunks[1]); } else { let mut card_lines: Vec = Vec::new(); for agent in &state.workflow_engine.agents { let color = state_color(agent.state); let icon = state_icon(agent.state); let label = state_label(agent.state); let duration_str = match (agent.started_at, agent.completed_at) { (Some(s), Some(e)) => format!(" {}ms", e.saturating_sub(s)), (Some(_), None) => " (running)".to_string(), _ => String::new(), }; card_lines.push(Line::from(vec![ Span::styled( format!(" {icon} "), Style::default().fg(color).add_modifier(Modifier::BOLD), ), Span::styled( format!(" {}", agent.display_name), Style::default() .fg(Theme::TEXT) .add_modifier(Modifier::BOLD), ), Span::styled(format!(" [{label}]"), Style::default().fg(color)), Span::styled(duration_str, Style::default().fg(Theme::TEXT_DIM)), ])); if let Some(ref err) = agent.error { card_lines.push(Line::from(vec![ Span::styled(" ⚠ ", Style::default().fg(Theme::ERROR)), Span::styled(err.clone(), Style::default().fg(Theme::ERROR)), ])); } else if let Some(ref prog) = agent.progress { for line in prog.lines().take(2) { card_lines.push(Line::from(vec![ Span::styled(" ", Style::default()), Span::styled( line.to_string(), Style::default() .fg(Theme::TEXT_DIM) .add_modifier(Modifier::ITALIC), ), ])); } } } let list = Paragraph::new(card_lines); frame.render_widget(list, chunks[1]); } } /// Build placeholder lines for the workflow panel body when no agents are running. /// /// Shows session stats (message count, tool calls, pending queue, bash jobs) /// or a "(no active session)" fallback. #[instrument(skip(state))] fn build_session_lines(state: &crate::state::AppStateRest) -> Vec> { let mut lines: Vec> = Vec::new(); lines.push(Line::from(Span::styled( " No workflow running.", Style::default().fg(Theme::TEXT_DIM), ))); lines.push(Line::from(Span::raw(""))); if let Some(ref rt) = state.session_runtime { let tool_count = rt.tool_call_results.len(); let pending = rt.pending_tool_queue.len(); let bash_count = rt.bash_jobs.len(); let msg_count = rt.messages.len(); lines.push(Line::from(vec![ Span::styled(" Messages ", Style::default().fg(Theme::TEXT_DIM)), Span::styled( msg_count.to_string(), Style::default() .fg(Theme::INFO) .add_modifier(Modifier::BOLD), ), ])); lines.push(Line::from(vec![ Span::styled(" Tool calls", Style::default().fg(Theme::TEXT_DIM)), Span::styled( format!(" {tool_count}"), Style::default().fg(Theme::SUCCESS), ), ])); if pending > 0 { lines.push(Line::from(vec![ Span::styled(" Pending ", Style::default().fg(Theme::TEXT_DIM)), Span::styled(format!(" {pending}"), Style::default().fg(Theme::WARNING)), ])); } if bash_count > 0 { lines.push(Line::from(vec![ Span::styled(" Bash jobs ", Style::default().fg(Theme::TEXT_DIM)), Span::styled( format!(" {bash_count}"), Style::default().fg(Theme::WARNING), ), ])); } } else { lines.push(Line::from(Span::styled( " (no active session)", Style::default().fg(Theme::TEXT_DIM), ))); } lines.push(Line::from(Span::raw(""))); lines.push(Line::from(Span::styled( " The Hive is dormant. Complex tasks will stir it.", Style::default() .fg(Theme::TEXT_DIM) .add_modifier(Modifier::ITALIC), ))); lines }