some graphing updates
This commit is contained in:
@@ -57,6 +57,7 @@ pub enum TuiMessage {
|
||||
total: u32,
|
||||
percentage: f32,
|
||||
},
|
||||
SSEReceived, // New message type for SSE events (including pings)
|
||||
Error(String),
|
||||
Exit,
|
||||
}
|
||||
@@ -105,10 +106,14 @@ struct TerminalState {
|
||||
should_exit: bool,
|
||||
/// Track the last tool header line index for updating it
|
||||
last_tool_header_index: Option<usize>,
|
||||
/// Token rate tracking for chart
|
||||
token_rate_history: VecDeque<(f64, f64)>, // (time_seconds, tokens_per_second)
|
||||
/// Token rate tracking for wave animation
|
||||
token_wave_history: VecDeque<f64>, // Wave animation values for tokens
|
||||
/// SSE rate tracking for wave animation
|
||||
sse_wave_history: VecDeque<f64>, // Wave animation values for SSEs
|
||||
/// Start time for token tracking
|
||||
session_start: Instant,
|
||||
/// SSE counter (including pings)
|
||||
sse_count: u32,
|
||||
/// Last token count for rate calculation
|
||||
last_token_count: u32,
|
||||
}
|
||||
@@ -144,9 +149,11 @@ impl TerminalState {
|
||||
is_processing: false,
|
||||
should_exit: false,
|
||||
last_tool_header_index: None,
|
||||
token_rate_history: VecDeque::with_capacity(60), // Keep last 60 data points
|
||||
token_wave_history: VecDeque::with_capacity(40), // Keep 40 points for wave animation
|
||||
sse_wave_history: VecDeque::with_capacity(40), // Keep 40 points for wave animation
|
||||
session_start: Instant::now(),
|
||||
last_token_count: 0,
|
||||
sse_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,21 +418,37 @@ impl RetroTui {
|
||||
} => {
|
||||
state.context_info = (used, total, percentage);
|
||||
|
||||
// Update token rate history for the chart
|
||||
let elapsed = state.session_start.elapsed().as_secs_f64();
|
||||
|
||||
// Calculate tokens per second since last update
|
||||
// Update token wave animation
|
||||
let tokens_since_last = used.saturating_sub(state.last_token_count) as f64;
|
||||
let rate = if tokens_since_last > 0.0 { tokens_since_last } else { 0.0 };
|
||||
|
||||
state.token_rate_history.push_back((elapsed, rate));
|
||||
// Add a wave point based on token rate (normalized 0-1)
|
||||
let wave_value = (tokens_since_last / 100.0).min(1.0); // Normalize to 0-1
|
||||
state.token_wave_history.push_back(wave_value);
|
||||
|
||||
// Keep only last 60 data points (about 1 minute of history at 1 update/sec)
|
||||
while state.token_rate_history.len() > 60 {
|
||||
state.token_rate_history.pop_front();
|
||||
// Keep only last 40 data points for smooth animation
|
||||
while state.token_wave_history.len() > 40 {
|
||||
state.token_wave_history.pop_front();
|
||||
}
|
||||
|
||||
state.last_token_count = used;
|
||||
}
|
||||
TuiMessage::SSEReceived => {
|
||||
state.sse_count += 1;
|
||||
|
||||
// Add a pulse to the SSE wave animation
|
||||
state.sse_wave_history.push_back(1.0); // Full pulse for each SSE
|
||||
|
||||
// Decay older values for smooth animation
|
||||
for i in 0..state.sse_wave_history.len().saturating_sub(1) {
|
||||
if let Some(val) = state.sse_wave_history.get_mut(i) {
|
||||
*val *= 0.85; // Decay factor
|
||||
}
|
||||
}
|
||||
|
||||
while state.sse_wave_history.len() > 40 {
|
||||
state.sse_wave_history.pop_front();
|
||||
}
|
||||
}
|
||||
TuiMessage::Error(err) => {
|
||||
state.add_output(&format!("ERROR: {}", err));
|
||||
}
|
||||
@@ -881,16 +904,16 @@ impl RetroTui {
|
||||
|
||||
f.render_widget(tool_output, chunks[0]);
|
||||
|
||||
// Draw right half - Token Chart
|
||||
Self::draw_token_chart(f, chunks[1], &state.token_rate_history, state.is_processing, opacity);
|
||||
// Draw right half - Activity graphs with wave animations
|
||||
Self::draw_activity_graphs(f, chunks[1], &state.token_wave_history, &state.sse_wave_history, opacity);
|
||||
}
|
||||
|
||||
/// Draw a line chart showing tokens received over time
|
||||
fn draw_token_chart(
|
||||
/// Draw activity graphs with wave animations for tokens and SSEs
|
||||
fn draw_activity_graphs(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
token_history: &VecDeque<(f64, f64)>,
|
||||
is_processing: bool,
|
||||
token_wave: &VecDeque<f64>,
|
||||
sse_wave: &VecDeque<f64>,
|
||||
opacity: f32,
|
||||
) {
|
||||
// Apply fade effect by adjusting colors based on opacity
|
||||
@@ -908,7 +931,7 @@ impl RetroTui {
|
||||
|
||||
// Create the chart block
|
||||
let block = Block::default()
|
||||
.title(" TOKENS RECEIVED ")
|
||||
.title(" ACTIVITY ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(fade_color(TERMINAL_DIM_GREEN)))
|
||||
@@ -920,82 +943,89 @@ impl RetroTui {
|
||||
// Render the block first
|
||||
f.render_widget(block, area);
|
||||
|
||||
// If no data or area too small, show placeholder
|
||||
if token_history.is_empty() || inner.width < 10 || inner.height < 3 {
|
||||
let placeholder = Paragraph::new(vec![Line::from(Span::styled(
|
||||
" Waiting for token data...",
|
||||
Style::default().fg(fade_color(TERMINAL_DIM_GREEN)).add_modifier(Modifier::ITALIC),
|
||||
))])
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(placeholder, inner);
|
||||
// If area too small, don't render graphs
|
||||
if inner.width < 10 || inner.height < 4 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate cumulative tokens for Y axis
|
||||
let mut cumulative_tokens: Vec<(f64, f64)> = Vec::new();
|
||||
let mut total = 0.0;
|
||||
for (time, rate) in token_history.iter() {
|
||||
total += rate;
|
||||
cumulative_tokens.push((*time, total));
|
||||
// Split the inner area into two graphs (top and bottom)
|
||||
let graph_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage(50), // Top graph for tokens
|
||||
Constraint::Percentage(50), // Bottom graph for SSEs
|
||||
])
|
||||
.split(inner);
|
||||
|
||||
// Draw token wave graph (top)
|
||||
Self::draw_wave_graph(
|
||||
f,
|
||||
graph_chunks[0],
|
||||
token_wave,
|
||||
"TOKENS",
|
||||
fade_color(TERMINAL_CYAN),
|
||||
fade_color(TERMINAL_DIM_GREEN),
|
||||
opacity,
|
||||
);
|
||||
|
||||
// Draw SSE wave graph (bottom)
|
||||
Self::draw_wave_graph(
|
||||
f,
|
||||
graph_chunks[1],
|
||||
sse_wave,
|
||||
"SSE",
|
||||
fade_color(TERMINAL_GREEN),
|
||||
fade_color(TERMINAL_DIM_GREEN),
|
||||
opacity,
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw a single wave animation graph
|
||||
fn draw_wave_graph(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
wave_data: &VecDeque<f64>,
|
||||
label: &str,
|
||||
wave_color: Color,
|
||||
axis_color: Color,
|
||||
_opacity: f32,
|
||||
) {
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
|
||||
if height < 2 || width < 5 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find max for scaling
|
||||
let max_tokens = cumulative_tokens
|
||||
.iter()
|
||||
.map(|(_, tokens)| *tokens)
|
||||
.fold(10.0, f64::max); // Minimum scale of 10 tokens
|
||||
// Wave characters for smooth animation
|
||||
let wave_chars = vec!['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
|
||||
let chart_height = inner.height as usize;
|
||||
let chart_width = inner.width as usize;
|
||||
// Build the wave line
|
||||
let mut wave_line = String::new();
|
||||
wave_line.push_str(&format!("{:<6}", label)); // Left-aligned label
|
||||
|
||||
// Create sparkline visualization
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
// Calculate how many data points to show
|
||||
let display_width = width.saturating_sub(6); // Account for label
|
||||
|
||||
// Add Y-axis label at top
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
format!("{:>5.0}", max_tokens),
|
||||
Style::default().fg(fade_color(TERMINAL_AMBER)),
|
||||
),
|
||||
Span::styled(" ┤", Style::default().fg(fade_color(TERMINAL_DIM_GREEN))),
|
||||
]));
|
||||
|
||||
// Draw the sparkline chart
|
||||
if chart_height > 3 && !cumulative_tokens.is_empty() {
|
||||
let sparkline_chars = vec!['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
let mut chart_line = String::from(" │");
|
||||
// Generate wave visualization
|
||||
for i in 0..display_width {
|
||||
let idx = wave_data.len().saturating_sub(display_width) + i;
|
||||
|
||||
// Sample the data to fit the width
|
||||
let sample_step = cumulative_tokens.len() as f64 / (chart_width - 7) as f64;
|
||||
|
||||
for x in 0..(chart_width - 7) {
|
||||
let idx = (x as f64 * sample_step) as usize;
|
||||
if idx < cumulative_tokens.len() {
|
||||
let (_, tokens) = cumulative_tokens[idx];
|
||||
let normalized = (tokens / max_tokens).min(1.0);
|
||||
let char_idx = ((normalized * 7.0) as usize).min(7);
|
||||
chart_line.push(sparkline_chars[char_idx]);
|
||||
} else {
|
||||
chart_line.push(' ');
|
||||
}
|
||||
if idx < wave_data.len() {
|
||||
let value = wave_data[idx].min(1.0).max(0.0);
|
||||
let char_idx = ((value * 7.0) as usize).min(7);
|
||||
wave_line.push(wave_chars[char_idx]);
|
||||
} else {
|
||||
wave_line.push(wave_chars[0]); // Baseline
|
||||
}
|
||||
|
||||
let color = if is_processing { fade_color(TERMINAL_CYAN) } else { fade_color(TERMINAL_GREEN) };
|
||||
lines.push(Line::from(Span::styled(chart_line, Style::default().fg(color))));
|
||||
|
||||
// Add bottom axis
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" 0", Style::default().fg(fade_color(TERMINAL_AMBER))),
|
||||
Span::styled(" └", Style::default().fg(fade_color(TERMINAL_DIM_GREEN))),
|
||||
Span::styled(
|
||||
format!("{}T (seconds)", "─".repeat(chart_width.saturating_sub(15))),
|
||||
Style::default().fg(fade_color(TERMINAL_DIM_GREEN)),
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
let chart_paragraph = Paragraph::new(lines);
|
||||
f.render_widget(chart_paragraph, inner);
|
||||
// Create the wave line with color
|
||||
let wave_paragraph = Paragraph::new(vec![
|
||||
Line::from(Span::styled(wave_line, Style::default().fg(wave_color))),
|
||||
]);
|
||||
|
||||
f.render_widget(wave_paragraph, area);
|
||||
}
|
||||
|
||||
/// Draw the status bar
|
||||
@@ -1135,6 +1165,11 @@ impl RetroTui {
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify that an SSE was received (including pings)
|
||||
pub fn sse_received(&self) {
|
||||
let _ = self.tx.send(TuiMessage::SSEReceived);
|
||||
}
|
||||
|
||||
/// Send error message
|
||||
pub fn error(&self, error: &str) {
|
||||
let _ = self.tx.send(TuiMessage::Error(error.to_string()));
|
||||
|
||||
@@ -78,6 +78,10 @@ impl UiWriter for ConsoleUiWriter {
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
fn notify_sse_received(&self) {
|
||||
// No-op for console - we don't track SSEs in console mode
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
@@ -244,6 +248,11 @@ impl UiWriter for RetroTuiWriter {
|
||||
self.tui.output(content);
|
||||
}
|
||||
|
||||
fn notify_sse_received(&self) {
|
||||
// Notify the TUI that an SSE was received
|
||||
self.tui.sse_received();
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
// No-op for TUI since it handles its own rendering
|
||||
}
|
||||
|
||||
@@ -1232,6 +1232,9 @@ The tool will execute immediately and you'll receive the result (success or erro
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
match chunk_result {
|
||||
Ok(chunk) => {
|
||||
// Notify UI about SSE received (including pings)
|
||||
self.ui_writer.notify_sse_received();
|
||||
|
||||
// Store raw chunk for debugging (limit to first 20 and last 5)
|
||||
if chunks_received < 20 || chunk.finished {
|
||||
raw_chunks.push(format!(
|
||||
|
||||
@@ -41,6 +41,9 @@ pub trait UiWriter: Send + Sync {
|
||||
/// Print agent response inline (for streaming)
|
||||
fn print_agent_response(&self, content: &str);
|
||||
|
||||
/// Notify that an SSE event was received (including pings)
|
||||
fn notify_sse_received(&self);
|
||||
|
||||
/// Flush any buffered output
|
||||
fn flush(&self);
|
||||
}
|
||||
@@ -62,5 +65,6 @@ impl UiWriter for NullUiWriter {
|
||||
fn print_tool_timing(&self, _duration_str: &str) {}
|
||||
fn print_agent_prompt(&self) {}
|
||||
fn print_agent_response(&self, _content: &str) {}
|
||||
fn notify_sse_received(&self) {}
|
||||
fn flush(&self) {}
|
||||
}
|
||||
Reference in New Issue
Block a user