@@ -449,13 +458,29 @@ pub const ACTORS_HTML: &str = r##"
function renderActorTable() {
var filter = document.getElementById('actorSearch').value.toLowerCase();
- var filtered = currentActors;
- if (filter) {
- filtered = currentActors.filter(function(a) {
+ var workerFilter = document.getElementById('workerFilter').value;
+ var statusFilter = document.getElementById('statusFilter').value;
+ var minDepthVal = document.getElementById('minDepth').value;
+ var minDepth = minDepthVal ? parseInt(minDepthVal, 10) : 0;
+
+ var filtered = currentActors.filter(function(a) {
+ // Text search
+ if (filter) {
var hex = addrToHex(a.address).toLowerCase();
- return hex.indexOf(filter) >= 0 || ('w' + a.worker_id).indexOf(filter) >= 0;
- });
- }
+ var msgType = (a.last_msg_type || '').toLowerCase();
+ if (hex.indexOf(filter) < 0 && ('w' + a.worker_id).indexOf(filter) < 0 && msgType.indexOf(filter) < 0) {
+ return false;
+ }
+ }
+ // Worker filter
+ if (workerFilter && a.worker_id !== parseInt(workerFilter, 10)) return false;
+ // Status filter
+ if (statusFilter === 'healthy' && a.poisoned) return false;
+ if (statusFilter === 'poisoned' && !a.poisoned) return false;
+ // Min depth
+ if (minDepth > 0 && a.mailbox_depth < minDepth) return false;
+ return true;
+ });
// Sort
filtered.sort(function(a, b) {
@@ -647,11 +672,17 @@ pub const ACTORS_HTML: &str = r##"
});
}
- // ── Search handler ─────────────────────────────────────
+ // ── Search/filter handlers ─────────────────────────────
document.getElementById('actorSearch').addEventListener('keyup', function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(renderActorTable, 150);
});
+ document.getElementById('workerFilter').addEventListener('change', renderActorTable);
+ document.getElementById('statusFilter').addEventListener('change', renderActorTable);
+ document.getElementById('minDepth').addEventListener('input', function() {
+ clearTimeout(searchTimer);
+ searchTimer = setTimeout(renderActorTable, 150);
+ });
// ── Status helpers ─────────────────────────────────────
function setStatus(s) {
@@ -697,6 +728,21 @@ pub const ACTORS_HTML: &str = r##"
if (!liveAddrs[key]) delete depthHistory[key];
}
+ // Update worker filter dropdown
+ var wSelect = document.getElementById('workerFilter');
+ var curVal = wSelect.value;
+ var workerIds = {};
+ for (var i = 0; i < currentActors.length; i++) workerIds[currentActors[i].worker_id] = true;
+ var wids = Object.keys(workerIds).sort(function(a,b) { return +a - +b; });
+ wSelect.innerHTML = '
';
+ wids.forEach(function(wid) {
+ var opt = document.createElement('option');
+ opt.value = wid;
+ opt.textContent = 'W' + wid;
+ wSelect.appendChild(opt);
+ });
+ wSelect.value = curVal;
+
renderActorTable();
if (focusedAddrHex) updateDetailPanel();
} catch(err) { console.error('stats parse error', err); }
diff --git a/crates/runtime-dashboard/src/tui/app.rs b/crates/runtime-dashboard/src/tui/app.rs
index fb5a0d5..539826e 100644
--- a/crates/runtime-dashboard/src/tui/app.rs
+++ b/crates/runtime-dashboard/src/tui/app.rs
@@ -92,6 +92,9 @@ pub struct App {
pub prev_view_mode: ViewMode,
pub focused_worker: usize,
pub focused_actor: Option
,
+ pub search_active: bool,
+ pub search_query: String,
+ pub search_locked: bool,
#[cfg(feature = "distribution")]
pub distribution: Option,
@@ -128,6 +131,9 @@ impl App {
prev_view_mode: ViewMode::Overview,
focused_worker: 0,
focused_actor: None,
+ search_active: false,
+ search_query: String::new(),
+ search_locked: false,
#[cfg(feature = "distribution")]
distribution: None,
#[cfg(feature = "distribution")]
@@ -148,6 +154,21 @@ impl App {
self.distribution = Some(snapshot);
}
+ /// Get visible actor rows (filtered by search query if active).
+ pub fn visible_actor_rows(&self) -> Vec<&ActorRow> {
+ if self.search_query.is_empty() {
+ self.actor_rows.iter().collect()
+ } else {
+ let q = self.search_query.to_lowercase();
+ self.actor_rows.iter().filter(|r| {
+ let addr = format!("{}", r.address).to_lowercase();
+ let msg = r.last_msg_type.as_deref().unwrap_or("").to_lowercase();
+ let worker = format!("w{}", r.worker_id);
+ addr.contains(&q) || msg.contains(&q) || worker.contains(&q)
+ }).collect()
+ }
+ }
+
/// Get the focused actor's data (for actor detail view).
pub fn focused_actor_row(&self) -> Option<&ActorRow> {
self.focused_actor.as_ref().and_then(|addr| {
@@ -304,12 +325,45 @@ impl App {
}
pub fn handle_key(&mut self, key: KeyEvent) {
+ // Search mode input handling
+ if self.search_active {
+ match key.code {
+ KeyCode::Esc => {
+ self.search_active = false;
+ if !self.search_locked {
+ self.search_query.clear();
+ }
+ return;
+ }
+ KeyCode::Enter => {
+ self.search_active = false;
+ self.search_locked = !self.search_query.is_empty();
+ return;
+ }
+ KeyCode::Backspace => {
+ self.search_query.pop();
+ return;
+ }
+ KeyCode::Char(c) => {
+ self.search_query.push(c);
+ return;
+ }
+ _ => return,
+ }
+ }
+
// Global keys
match key.code {
KeyCode::Char('q') => { self.should_quit = true; return; }
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.should_quit = true; return;
}
+ KeyCode::Char('/') => {
+ self.search_active = true;
+ self.search_locked = false;
+ self.search_query.clear();
+ return;
+ }
KeyCode::Char('s') => {
self.sort_column = self.sort_column.next();
self.sort_actors();
@@ -346,7 +400,8 @@ impl App {
}
fn handle_key_overview(&mut self, key: KeyEvent) {
- let max = if self.actor_rows.is_empty() { 0 } else { self.actor_rows.len() - 1 };
+ let visible = self.visible_actor_rows();
+ let max = if visible.is_empty() { 0 } else { visible.len() - 1 };
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
self.selected = self.selected.saturating_sub(1);
@@ -363,8 +418,8 @@ impl App {
KeyCode::Home => { self.selected = 0; }
KeyCode::End => { self.selected = max; }
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => {
- // Enter actor detail for the selected actor
- if let Some(row) = self.actor_rows.get(self.selected) {
+ // Enter actor detail for the selected visible actor
+ if let Some(&row) = visible.get(self.selected) {
self.focused_actor = Some(row.address);
self.prev_view_mode = ViewMode::Overview;
self.view_mode = ViewMode::ActorDetail;
diff --git a/crates/runtime-dashboard/src/tui/ui.rs b/crates/runtime-dashboard/src/tui/ui.rs
index 21987ae..13b906d 100644
--- a/crates/runtime-dashboard/src/tui/ui.rs
+++ b/crates/runtime-dashboard/src/tui/ui.rs
@@ -194,8 +194,33 @@ fn draw_summary(f: &mut Frame, app: &App, area: Rect) {
f.render_widget(Paragraph::new(line), area);
}
-/// Render the actor table with scrolling and selection.
+/// Render the actor table with scrolling, selection, and search.
fn draw_actor_table(f: &mut Frame, app: &App, table_state: &mut TableState, area: Rect) {
+ // Split area: optional search bar + table
+ let has_search = app.search_active || !app.search_query.is_empty();
+ let search_height = if has_search { 1u16 } else { 0 };
+ let chunks = Layout::vertical([
+ Constraint::Length(search_height),
+ Constraint::Fill(1),
+ ])
+ .split(area);
+
+ // Draw search bar
+ if has_search {
+ let search_style = if app.search_active {
+ Style::default().fg(Color::Yellow)
+ } else {
+ Style::default().fg(Color::DarkGray)
+ };
+ let cursor = if app.search_active { "\u{2588}" } else { "" };
+ let prefix = if app.search_locked { " [locked] /" } else { " /" };
+ let line = Line::from(vec![
+ Span::styled(prefix, Style::default().fg(Color::DarkGray)),
+ Span::styled(format!("{}{}", app.search_query, cursor), search_style),
+ ]);
+ f.render_widget(Paragraph::new(line), chunks[0]);
+ }
+
let sort_arrow = if app.sort_desc { " \u{25bc}" } else { " \u{25b2}" };
let columns = [
@@ -218,8 +243,8 @@ fn draw_actor_table(f: &mut Frame, app: &App, table_state: &mut TableState, area
});
let header = Row::new(header_cells).height(1);
- let rows: Vec = app
- .actor_rows
+ let visible = app.visible_actor_rows();
+ let rows: Vec = visible
.iter()
.map(|a| actor_row_cells(a))
.collect();
@@ -227,7 +252,22 @@ fn draw_actor_table(f: &mut Frame, app: &App, table_state: &mut TableState, area
table_state.select(Some(app.selected));
let help_text =
- " q: quit \u{2191}\u{2193}: scroll s: sort column r: reverse Tab: worker view Enter: drill in";
+ " q: quit /: search \u{2191}\u{2193}: scroll s: sort r: reverse Tab: worker view Enter: detail";
+
+ let title = if !app.search_query.is_empty() {
+ format!(
+ " Actors ({} of {} matching \"{}\") ",
+ visible.len(),
+ app.actor_rows.len(),
+ app.search_query,
+ )
+ } else {
+ format!(
+ " Actors (sorted by {}{}) ",
+ app.sort_column.label(),
+ sort_arrow
+ )
+ };
let table = Table::new(
rows,
@@ -243,11 +283,7 @@ fn draw_actor_table(f: &mut Frame, app: &App, table_state: &mut TableState, area
.block(
Block::default()
.borders(Borders::ALL)
- .title(format!(
- " Actors (sorted by {}{}) ",
- app.sort_column.label(),
- sort_arrow
- ))
+ .title(title)
.title_bottom(Line::from(help_text).centered()),
)
.row_highlight_style(