feat(tui): implement status bar with connection and turn state indicators feat(tui): create workflow panel for agent status and progress visualization feat(web): introduce web frontend interface with static file serving feat(ws): add WebSocket interface for real-time communication and session management
169 lines
5.6 KiB
Rust
169 lines
5.6 KiB
Rust
//! 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<Line> = 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<Line> = 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<Line> = 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,
|
|
}
|
|
}
|