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