//! Persistent right-hand dashboard sidebar: Workflow, Tasks, and Usage //! widgets stacked in three vertical thirds. use super::theme::Theme; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; /// Render the persistent right-hand dashboard: Workflow, Tasks, and Usage. pub fn draw_sidebar(frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) { let has_workflow = !state.workflow_engine.agents.is_empty(); let constraints = if has_workflow { vec![ Constraint::Ratio(1, 2), Constraint::Ratio(1, 4), Constraint::Ratio(1, 4), ] } else { vec![ Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), ] }; let chunks = Layout::default() .direction(Direction::Vertical) .constraints(constraints) .split(area); super::workflow::draw_workflow_panel(frame, chunks[0], state); draw_tasks_widget(frame, chunks[1], state); draw_usage_widget(frame, chunks[2], state); } fn draw_tasks_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) { let block = Block::default() .title(Span::styled( " Tasks ", Style::default() .fg(Theme::ACCENT_PURPLE) .add_modifier(Modifier::BOLD), )) .borders(Borders::ALL) .border_style(Style::default().fg(Theme::BORDER)); let budget = (block.inner(area).height as usize).max(1); let content = &state.misc.todo_content; let task_lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect(); let lines: Vec = if task_lines.is_empty() { vec![Line::from(Span::styled( " No tasks yet.", Style::default().fg(Theme::TEXT_DIM), ))] } else { let show_hint = task_lines.len() > budget; let item_budget = if show_hint { budget.saturating_sub(1).max(1) } else { budget }; let (visible, hidden) = super::split_for_display(&task_lines, item_budget); let mut lines: Vec = visible .iter() .map(|l| { Line::from(Span::styled( format!(" {l}"), Style::default().fg(Theme::TEXT), )) }) .collect(); if show_hint { lines.push(super::overflow_hint_line(hidden, "/todo")); } lines }; let paragraph = Paragraph::new(lines).block(block); frame.render_widget(paragraph, area); } fn draw_usage_widget(frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) { let block = Block::default() .title(Span::styled( " Usage ", Style::default() .fg(Theme::INFO) .add_modifier(Modifier::BOLD), )) .borders(Borders::ALL) .border_style(Style::default().fg(Theme::BORDER)); let lines: Vec = if let Some(ref rt) = state.session_runtime { let now_ms = chrono::Utc::now().timestamp_millis(); let summary = compute_usage_summary(&rt.usage, rt.session_start, now_ms); vec![ Line::from(Span::styled( format!(" {:>6}: {} tok", "total", summary.total_tokens), Style::default() .fg(Theme::TEXT) .add_modifier(Modifier::BOLD), )), Line::from(Span::styled( format!(" {:>6}: {} tok", "main", summary.main_tokens), Style::default().fg(Theme::TEXT_DIM), )), Line::from(Span::styled( format!(" {:>6}: {} tok", "learn", summary.self_learning_tokens), Style::default().fg(Theme::TEXT_DIM), )), Line::from(Span::styled( format!(" {:>6}: {}", "calls", summary.api_calls), Style::default().fg(Theme::TEXT_DIM), )), Line::from(Span::styled( format!( " {:>6}: {}h {:02}m {:02}s", "time", summary.elapsed_hours, summary.elapsed_minutes, summary.elapsed_seconds ), Style::default().fg(Theme::TEXT_DIM), )), ] } else { vec![Line::from(Span::styled( " No active session.", Style::default().fg(Theme::TEXT_DIM), ))] }; let paragraph = Paragraph::new(lines).block(block); frame.render_widget(paragraph, area); } pub(crate) struct UsageSummary { pub main_tokens: u64, pub self_learning_tokens: u64, pub total_tokens: u64, pub api_calls: u64, pub elapsed_hours: i64, pub elapsed_minutes: i64, pub elapsed_seconds: i64, } pub(crate) fn compute_usage_summary( usage: &zesdex_domain::core::UsageStats, session_start: i64, now_ms: i64, ) -> UsageSummary { let total_tokens = usage.tokens_in.saturating_add(usage.tokens_out); let self_learning_tokens = usage.review_tokens; let main_tokens = total_tokens.saturating_sub(self_learning_tokens); let elapsed_ms = now_ms.saturating_sub(session_start); let elapsed_hours = elapsed_ms / 3_600_000; let elapsed_minutes = (elapsed_ms % 3_600_000) / 60_000; let elapsed_seconds = (elapsed_ms % 60_000) / 1000; UsageSummary { main_tokens, self_learning_tokens, total_tokens, api_calls: usage.api_calls, elapsed_hours, elapsed_minutes, elapsed_seconds, } }