feat: scroll ecc2 metrics across full teams

This commit is contained in:
Affaan Mustafa
2026-04-09 05:10:40 -07:00
parent f29e70883c
commit 6fc3f7c3f4

View File

@@ -83,6 +83,8 @@ pub struct Dashboard {
output_follow: bool, output_follow: bool,
output_scroll_offset: usize, output_scroll_offset: usize,
last_output_height: usize, last_output_height: usize,
metrics_scroll_offset: usize,
last_metrics_height: usize,
pane_size_percent: u16, pane_size_percent: u16,
search_input: Option<String>, search_input: Option<String>,
spawn_input: Option<String>, spawn_input: Option<String>,
@@ -267,6 +269,8 @@ impl Dashboard {
output_follow: true, output_follow: true,
output_scroll_offset: 0, output_scroll_offset: 0,
last_output_height: 0, last_output_height: 0,
metrics_scroll_offset: 0,
last_metrics_height: 0,
pane_size_percent, pane_size_percent,
search_input: None, search_input: None,
spawn_input: None, spawn_input: None,
@@ -631,7 +635,7 @@ impl Dashboard {
) )
} }
fn render_metrics(&self, frame: &mut Frame, area: Rect) { fn render_metrics(&mut self, frame: &mut Frame, area: Rect) {
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(" Metrics ") .title(" Metrics ")
@@ -670,9 +674,12 @@ impl Dashboard {
chunks[1], chunks[1],
); );
frame.render_widget( frame.render_widget(
Paragraph::new(self.selected_session_metrics_text()).wrap(Wrap { trim: true }), Paragraph::new(self.selected_session_metrics_text())
.scroll((self.metrics_scroll_offset as u16, 0))
.wrap(Wrap { trim: true }),
chunks[2], chunks[2],
); );
self.sync_metrics_scroll(chunks[2].height as usize);
} }
fn render_log(&self, frame: &mut Frame, area: Rect) { fn render_log(&self, frame: &mut Frame, area: Rect) {
@@ -1060,6 +1067,7 @@ impl Dashboard {
self.selected_session = (self.selected_session + 1).min(self.sessions.len() - 1); self.selected_session = (self.selected_session + 1).min(self.sessions.len() - 1);
self.sync_selection(); self.sync_selection();
self.reset_output_view(); self.reset_output_view();
self.reset_metrics_view();
self.sync_selected_output(); self.sync_selected_output();
self.sync_selected_diff(); self.sync_selected_diff();
self.sync_selected_messages(); self.sync_selected_messages();
@@ -1079,7 +1087,11 @@ impl Dashboard {
self.output_scroll_offset = self.output_scroll_offset.saturating_add(1); self.output_scroll_offset = self.output_scroll_offset.saturating_add(1);
} }
} }
Pane::Metrics => {} Pane::Metrics => {
let max_scroll = self.max_metrics_scroll();
self.metrics_scroll_offset =
self.metrics_scroll_offset.saturating_add(1).min(max_scroll);
}
Pane::Log => { Pane::Log => {
self.output_follow = false; self.output_follow = false;
self.output_scroll_offset = self.output_scroll_offset.saturating_add(1); self.output_scroll_offset = self.output_scroll_offset.saturating_add(1);
@@ -1094,6 +1106,7 @@ impl Dashboard {
self.selected_session = self.selected_session.saturating_sub(1); self.selected_session = self.selected_session.saturating_sub(1);
self.sync_selection(); self.sync_selection();
self.reset_output_view(); self.reset_output_view();
self.reset_metrics_view();
self.sync_selected_output(); self.sync_selected_output();
self.sync_selected_diff(); self.sync_selected_diff();
self.sync_selected_messages(); self.sync_selected_messages();
@@ -1108,7 +1121,9 @@ impl Dashboard {
self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1); self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1);
} }
Pane::Metrics => {} Pane::Metrics => {
self.metrics_scroll_offset = self.metrics_scroll_offset.saturating_sub(1);
}
Pane::Log => { Pane::Log => {
self.output_follow = false; self.output_follow = false;
self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1); self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1);
@@ -2584,7 +2599,6 @@ impl Dashboard {
delegate.session_id.clone(), delegate.session_id.clone(),
) )
}); });
delegated.truncate(3);
delegated delegated
} }
Err(error) => { Err(error) => {
@@ -2830,6 +2844,19 @@ impl Dashboard {
.saturating_sub(self.last_output_height.max(1)) .saturating_sub(self.last_output_height.max(1))
} }
fn sync_metrics_scroll(&mut self, viewport_height: usize) {
self.last_metrics_height = viewport_height.max(1);
let max_scroll = self.max_metrics_scroll();
self.metrics_scroll_offset = self.metrics_scroll_offset.min(max_scroll);
}
fn max_metrics_scroll(&self) -> usize {
self.selected_session_metrics_text()
.lines()
.count()
.saturating_sub(self.last_metrics_height.max(1))
}
#[cfg(test)] #[cfg(test)]
fn visible_output_text(&self) -> String { fn visible_output_text(&self) -> String {
self.visible_output_lines() self.visible_output_lines()
@@ -2844,6 +2871,10 @@ impl Dashboard {
self.output_scroll_offset = 0; self.output_scroll_offset = 0;
} }
fn reset_metrics_view(&mut self) {
self.metrics_scroll_offset = 0;
}
fn refresh_logs(&mut self) { fn refresh_logs(&mut self) {
let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else { let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else {
self.logs.clear(); self.logs.clear();
@@ -3242,6 +3273,7 @@ impl Dashboard {
self.refresh(); self.refresh();
self.sync_selection_by_id(select_session_id); self.sync_selection_by_id(select_session_id);
self.reset_output_view(); self.reset_output_view();
self.reset_metrics_view();
self.sync_selected_output(); self.sync_selected_output();
self.sync_selected_diff(); self.sync_selected_diff();
self.sync_selected_messages(); self.sync_selected_messages();
@@ -5275,6 +5307,50 @@ diff --git a/src/next.rs b/src/next.rs
); );
} }
#[test]
fn sync_selected_lineage_keeps_all_delegate_rows() {
let lead = sample_session(
"lead-12345678",
"planner",
SessionState::Running,
Some("ecc/lead"),
512,
42,
);
let mut sessions = vec![lead.clone()];
let mut dashboard = test_dashboard(vec![lead.clone()], 0);
dashboard.db.insert_session(&lead).unwrap();
for index in 0..5 {
let child_id = format!("worker-{index}");
let child = sample_session(
&child_id,
"planner",
SessionState::Running,
Some(&format!("ecc/{child_id}")),
64,
6,
);
sessions.push(child.clone());
dashboard.db.insert_session(&child).unwrap();
dashboard
.db
.send_message(
"lead-12345678",
&child_id,
"{\"task\":\"Delegated work\",\"context\":\"Delegated from lead\"}",
"task_handoff",
)
.unwrap();
}
dashboard.sessions = sessions;
dashboard.sync_selected_lineage();
assert_eq!(dashboard.selected_child_sessions.len(), 5);
}
#[test] #[test]
fn aggregate_cost_summary_mentions_total_cost() { fn aggregate_cost_summary_mentions_total_cost() {
let db = StateStore::open(Path::new(":memory:")).unwrap(); let db = StateStore::open(Path::new(":memory:")).unwrap();
@@ -5454,7 +5530,7 @@ diff --git a/src/next.rs b/src/next.rs
} }
#[test] #[test]
fn metrics_scroll_does_not_mutate_output_scroll() -> Result<()> { fn metrics_scroll_uses_independent_offset() -> Result<()> {
let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4())); let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
let db = StateStore::open(&db_path)?; let db = StateStore::open(&db_path)?;
let now = Utc::now(); let now = Utc::now();
@@ -5484,10 +5560,13 @@ diff --git a/src/next.rs b/src/next.rs
let previous_scroll = dashboard.output_scroll_offset; let previous_scroll = dashboard.output_scroll_offset;
dashboard.selected_pane = Pane::Metrics; dashboard.selected_pane = Pane::Metrics;
dashboard.last_metrics_height = 2;
dashboard.scroll_up(); dashboard.scroll_up();
dashboard.scroll_down(); dashboard.scroll_down();
dashboard.scroll_down();
assert_eq!(dashboard.output_scroll_offset, previous_scroll); assert_eq!(dashboard.output_scroll_offset, previous_scroll);
assert_eq!(dashboard.metrics_scroll_offset, 2);
let _ = std::fs::remove_file(db_path); let _ = std::fs::remove_file(db_path);
Ok(()) Ok(())
} }
@@ -6989,6 +7068,8 @@ diff --git a/src/next.rs b/src/next.rs
output_follow: true, output_follow: true,
output_scroll_offset: 0, output_scroll_offset: 0,
last_output_height: 0, last_output_height: 0,
metrics_scroll_offset: 0,
last_metrics_height: 0,
search_input: None, search_input: None,
spawn_input: None, spawn_input: None,
search_query: None, search_query: None,