mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 11:23:32 +08:00
feat: add ecc2 global output search
This commit is contained in:
@@ -75,6 +75,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
|||||||
(_, KeyCode::Char('c')) => dashboard.toggle_conflict_protocol_mode(),
|
(_, KeyCode::Char('c')) => dashboard.toggle_conflict_protocol_mode(),
|
||||||
(_, KeyCode::Char('e')) => dashboard.toggle_output_filter(),
|
(_, KeyCode::Char('e')) => dashboard.toggle_output_filter(),
|
||||||
(_, KeyCode::Char('f')) => dashboard.cycle_output_time_filter(),
|
(_, KeyCode::Char('f')) => dashboard.cycle_output_time_filter(),
|
||||||
|
(_, KeyCode::Char('A')) => dashboard.toggle_search_scope(),
|
||||||
(_, KeyCode::Char('m')) => dashboard.merge_selected_worktree().await,
|
(_, KeyCode::Char('m')) => dashboard.merge_selected_worktree().await,
|
||||||
(_, KeyCode::Char('M')) => dashboard.merge_ready_worktrees().await,
|
(_, KeyCode::Char('M')) => dashboard.merge_ready_worktrees().await,
|
||||||
(_, KeyCode::Char('l')) => dashboard.cycle_pane_layout(),
|
(_, KeyCode::Char('l')) => dashboard.cycle_pane_layout(),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use ratatui::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use super::widgets::{budget_state, format_currency, format_token_count, BudgetState, TokenMeter};
|
use super::widgets::{budget_state, format_currency, format_token_count, BudgetState, TokenMeter};
|
||||||
@@ -84,7 +84,8 @@ pub struct Dashboard {
|
|||||||
pane_size_percent: u16,
|
pane_size_percent: u16,
|
||||||
search_input: Option<String>,
|
search_input: Option<String>,
|
||||||
search_query: Option<String>,
|
search_query: Option<String>,
|
||||||
search_matches: Vec<usize>,
|
search_scope: SearchScope,
|
||||||
|
search_matches: Vec<SearchMatch>,
|
||||||
selected_search_match: usize,
|
selected_search_match: usize,
|
||||||
session_table_state: TableState,
|
session_table_state: TableState,
|
||||||
}
|
}
|
||||||
@@ -133,6 +134,18 @@ enum OutputTimeFilter {
|
|||||||
Last24Hours,
|
Last24Hours,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum SearchScope {
|
||||||
|
SelectedSession,
|
||||||
|
AllSessions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct SearchMatch {
|
||||||
|
session_id: String,
|
||||||
|
line_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct PaneAreas {
|
struct PaneAreas {
|
||||||
sessions: Rect,
|
sessions: Rect,
|
||||||
@@ -222,6 +235,7 @@ impl Dashboard {
|
|||||||
pane_size_percent,
|
pane_size_percent,
|
||||||
search_input: None,
|
search_input: None,
|
||||||
search_query: None,
|
search_query: None,
|
||||||
|
search_scope: SearchScope::SelectedSession,
|
||||||
search_matches: Vec::new(),
|
search_matches: Vec::new(),
|
||||||
selected_search_match: 0,
|
selected_search_match: 0,
|
||||||
session_table_state,
|
session_table_state,
|
||||||
@@ -488,8 +502,9 @@ impl Dashboard {
|
|||||||
self.output_filter.title_suffix(),
|
self.output_filter.title_suffix(),
|
||||||
self.output_time_filter.title_suffix()
|
self.output_time_filter.title_suffix()
|
||||||
);
|
);
|
||||||
|
let scope = self.search_scope.title_suffix();
|
||||||
if let Some(input) = self.search_input.as_ref() {
|
if let Some(input) = self.search_input.as_ref() {
|
||||||
return format!(" Output{filter} /{input}_ ");
|
return format!(" Output{filter}{scope} /{input}_ ");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(query) = self.search_query.as_ref() {
|
if let Some(query) = self.search_query.as_ref() {
|
||||||
@@ -499,10 +514,10 @@ impl Dashboard {
|
|||||||
} else {
|
} else {
|
||||||
self.selected_search_match.min(total.saturating_sub(1)) + 1
|
self.selected_search_match.min(total.saturating_sub(1)) + 1
|
||||||
};
|
};
|
||||||
return format!(" Output{filter} /{query} {current}/{total} ");
|
return format!(" Output{filter}{scope} /{query} {current}/{total} ");
|
||||||
}
|
}
|
||||||
|
|
||||||
format!(" Output{filter} ")
|
format!(" Output{filter}{scope} ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_output_message(&self) -> &'static str {
|
fn empty_output_message(&self) -> &'static str {
|
||||||
@@ -526,6 +541,9 @@ impl Dashboard {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let selected_session_id = self.selected_session_id();
|
||||||
|
let active_match = self.search_matches.get(self.selected_search_match);
|
||||||
|
|
||||||
Text::from(
|
Text::from(
|
||||||
lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
@@ -534,7 +552,13 @@ impl Dashboard {
|
|||||||
highlight_output_line(
|
highlight_output_line(
|
||||||
&line.text,
|
&line.text,
|
||||||
query,
|
query,
|
||||||
self.search_matches.get(self.selected_search_match).copied() == Some(index),
|
active_match
|
||||||
|
.zip(selected_session_id)
|
||||||
|
.map(|(search_match, session_id)| {
|
||||||
|
search_match.session_id == session_id
|
||||||
|
&& search_match.line_index == index
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
self.theme_palette(),
|
self.theme_palette(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -623,13 +647,16 @@ impl Dashboard {
|
|||||||
|
|
||||||
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
||||||
let base_text = format!(
|
let base_text = format!(
|
||||||
" [n]ew session [a]ssign re[b]alance global re[B]alance dra[i]n inbox [g]lobal dispatch coordinate [G]lobal [v]iew diff conflict proto[c]ol [e]rrors time [f]ilter [m]erge merge ready [M] auto-worktree [t] auto-merge [w] toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup prune inactive [X] [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [l]ayout {} [T]heme {} [?] help [q]uit ",
|
" [n]ew session [a]ssign re[b]alance global re[B]alance dra[i]n inbox [g]lobal dispatch coordinate [G]lobal [v]iew diff conflict proto[c]ol [e]rrors time [f]ilter search scope [A] [m]erge merge ready [M] auto-worktree [t] auto-merge [w] toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup prune inactive [X] [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [l]ayout {} [T]heme {} [?] help [q]uit ",
|
||||||
self.layout_label(),
|
self.layout_label(),
|
||||||
self.theme_label()
|
self.theme_label()
|
||||||
);
|
);
|
||||||
|
|
||||||
let search_prefix = if let Some(input) = self.search_input.as_ref() {
|
let search_prefix = if let Some(input) = self.search_input.as_ref() {
|
||||||
format!(" /{input}_ | [Enter] apply [Esc] cancel |")
|
format!(
|
||||||
|
" /{input}_ | {} | [Enter] apply [Esc] cancel |",
|
||||||
|
self.search_scope.label()
|
||||||
|
)
|
||||||
} else if let Some(query) = self.search_query.as_ref() {
|
} else if let Some(query) = self.search_query.as_ref() {
|
||||||
let total = self.search_matches.len();
|
let total = self.search_matches.len();
|
||||||
let current = if total == 0 {
|
let current = if total == 0 {
|
||||||
@@ -637,7 +664,10 @@ impl Dashboard {
|
|||||||
} else {
|
} else {
|
||||||
self.selected_search_match.min(total.saturating_sub(1)) + 1
|
self.selected_search_match.min(total.saturating_sub(1)) + 1
|
||||||
};
|
};
|
||||||
format!(" /{query} {current}/{total} | [n/N] navigate [Esc] clear |")
|
format!(
|
||||||
|
" /{query} {current}/{total} | {} | [n/N] navigate [Esc] clear |",
|
||||||
|
self.search_scope.label()
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
@@ -696,6 +726,7 @@ impl Dashboard {
|
|||||||
" c Show conflict-resolution protocol for selected conflicted worktree",
|
" c Show conflict-resolution protocol for selected conflicted worktree",
|
||||||
" e Toggle output filter between all lines and stderr only",
|
" e Toggle output filter between all lines and stderr only",
|
||||||
" f Cycle output time filter between all/15m/1h/24h",
|
" f Cycle output time filter between all/15m/1h/24h",
|
||||||
|
" A Toggle search scope between selected session and all sessions",
|
||||||
" m Merge selected ready worktree into base and clean it up",
|
" m Merge selected ready worktree into base and clean it up",
|
||||||
" M Merge all ready inactive worktrees and clean them up",
|
" M Merge all ready inactive worktrees and clean them up",
|
||||||
" l Cycle pane layout and persist it",
|
" l Cycle pane layout and persist it",
|
||||||
@@ -1624,6 +1655,29 @@ impl Dashboard {
|
|||||||
self.search_query.is_some()
|
self.search_query.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_search_scope(&mut self) {
|
||||||
|
if self.output_mode != OutputMode::SessionOutput {
|
||||||
|
self.set_operator_note(
|
||||||
|
"search scope is only available in session output view".to_string(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.search_scope = self.search_scope.next();
|
||||||
|
self.recompute_search_matches();
|
||||||
|
self.sync_output_scroll(self.last_output_height.max(1));
|
||||||
|
|
||||||
|
if self.search_query.is_some() {
|
||||||
|
self.set_operator_note(format!(
|
||||||
|
"search scope set to {} | {} match(es)",
|
||||||
|
self.search_scope.label(),
|
||||||
|
self.search_matches.len()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.set_operator_note(format!("search scope set to {}", self.search_scope.label()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn begin_search(&mut self) {
|
pub fn begin_search(&mut self) {
|
||||||
if self.output_mode != OutputMode::SessionOutput {
|
if self.output_mode != OutputMode::SessionOutput {
|
||||||
self.set_operator_note("search is only available in session output view".to_string());
|
self.set_operator_note("search is only available in session output view".to_string());
|
||||||
@@ -1675,8 +1729,9 @@ impl Dashboard {
|
|||||||
self.set_operator_note(format!("search /{query} found no matches"));
|
self.set_operator_note(format!("search /{query} found no matches"));
|
||||||
} else {
|
} else {
|
||||||
self.set_operator_note(format!(
|
self.set_operator_note(format!(
|
||||||
"search /{query} matched {} line(s) | n/N navigate matches",
|
"search /{query} matched {} line(s) across {} session(s) | n/N navigate matches",
|
||||||
self.search_matches.len()
|
self.search_matches.len(),
|
||||||
|
self.search_match_session_count()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1878,6 +1933,7 @@ impl Dashboard {
|
|||||||
self.sync_worktree_health_by_session();
|
self.sync_worktree_health_by_session();
|
||||||
self.sync_global_handoff_backlog();
|
self.sync_global_handoff_backlog();
|
||||||
self.sync_daemon_activity();
|
self.sync_daemon_activity();
|
||||||
|
self.sync_output_cache();
|
||||||
self.sync_selection_by_id(selected_id.as_deref());
|
self.sync_selection_by_id(selected_id.as_deref());
|
||||||
self.ensure_selected_pane_visible();
|
self.ensure_selected_pane_visible();
|
||||||
self.sync_selected_output();
|
self.sync_selected_output();
|
||||||
@@ -1910,6 +1966,28 @@ impl Dashboard {
|
|||||||
self.sync_selection();
|
self.sync_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_output_cache(&mut self) {
|
||||||
|
let active_session_ids: HashSet<_> = self
|
||||||
|
.sessions
|
||||||
|
.iter()
|
||||||
|
.map(|session| session.id.as_str())
|
||||||
|
.collect();
|
||||||
|
self.session_output_cache
|
||||||
|
.retain(|session_id, _| active_session_ids.contains(session_id.as_str()));
|
||||||
|
|
||||||
|
for session in &self.sessions {
|
||||||
|
match self.db.get_output_lines(&session.id, OUTPUT_BUFFER_LIMIT) {
|
||||||
|
Ok(lines) => {
|
||||||
|
self.output_store.replace_lines(&session.id, lines.clone());
|
||||||
|
self.session_output_cache.insert(session.id.clone(), lines);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!("Failed to load session output for {}: {error}", session.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_selected_pane_visible(&mut self) {
|
fn ensure_selected_pane_visible(&mut self) {
|
||||||
if !self.visible_panes().contains(&self.selected_pane) {
|
if !self.visible_panes().contains(&self.selected_pane) {
|
||||||
self.selected_pane = Pane::Sessions;
|
self.selected_pane = Pane::Sessions;
|
||||||
@@ -1978,24 +2056,15 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sync_selected_output(&mut self) {
|
fn sync_selected_output(&mut self) {
|
||||||
let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else {
|
if self.selected_session_id().is_none() {
|
||||||
self.output_scroll_offset = 0;
|
self.output_scroll_offset = 0;
|
||||||
self.output_follow = true;
|
self.output_follow = true;
|
||||||
self.search_matches.clear();
|
self.search_matches.clear();
|
||||||
self.selected_search_match = 0;
|
self.selected_search_match = 0;
|
||||||
return;
|
return;
|
||||||
};
|
|
||||||
|
|
||||||
match self.db.get_output_lines(&session_id, OUTPUT_BUFFER_LIMIT) {
|
|
||||||
Ok(lines) => {
|
|
||||||
self.output_store.replace_lines(&session_id, lines.clone());
|
|
||||||
self.session_output_cache.insert(session_id, lines);
|
|
||||||
self.recompute_search_matches();
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
tracing::warn!("Failed to load session output: {error}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.recompute_search_matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_selected_diff(&mut self) {
|
fn sync_selected_diff(&mut self) {
|
||||||
@@ -2219,13 +2288,25 @@ impl Dashboard {
|
|||||||
.unwrap_or(&[])
|
.unwrap_or(&[])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible_output_lines(&self) -> Vec<&OutputLine> {
|
fn visible_output_lines_for_session(&self, session_id: &str) -> Vec<&OutputLine> {
|
||||||
self.selected_output_lines()
|
self.session_output_cache
|
||||||
.iter()
|
.get(session_id)
|
||||||
.filter(|line| {
|
.map(|lines| {
|
||||||
self.output_filter.matches(line.stream) && self.output_time_filter.matches(line)
|
lines
|
||||||
|
.iter()
|
||||||
|
.filter(|line| {
|
||||||
|
self.output_filter.matches(line.stream)
|
||||||
|
&& self.output_time_filter.matches(line)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
})
|
})
|
||||||
.collect()
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible_output_lines(&self) -> Vec<&OutputLine> {
|
||||||
|
self.selected_session_id()
|
||||||
|
.map(|session_id| self.visible_output_lines_for_session(session_id))
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recompute_search_matches(&mut self) {
|
fn recompute_search_matches(&mut self) {
|
||||||
@@ -2242,10 +2323,21 @@ impl Dashboard {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.search_matches = self
|
self.search_matches = self
|
||||||
.visible_output_lines()
|
.search_scope
|
||||||
.iter()
|
.session_ids(self.selected_session_id(), &self.sessions)
|
||||||
.enumerate()
|
.into_iter()
|
||||||
.filter_map(|(index, line)| regex.is_match(&line.text).then_some(index))
|
.flat_map(|session_id| {
|
||||||
|
self.visible_output_lines_for_session(session_id)
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, line)| {
|
||||||
|
regex.is_match(&line.text).then_some(SearchMatch {
|
||||||
|
session_id: session_id.to_string(),
|
||||||
|
line_index: index,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if self.search_matches.is_empty() {
|
if self.search_matches.is_empty() {
|
||||||
@@ -2260,13 +2352,25 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_selected_search_match(&mut self) {
|
fn focus_selected_search_match(&mut self) {
|
||||||
let Some(line_index) = self.search_matches.get(self.selected_search_match).copied() else {
|
let Some(search_match) = self.search_matches.get(self.selected_search_match).cloned()
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self.selected_session_id() != Some(search_match.session_id.as_str()) {
|
||||||
|
self.sync_selection_by_id(Some(&search_match.session_id));
|
||||||
|
self.sync_selected_output();
|
||||||
|
self.sync_selected_diff();
|
||||||
|
self.sync_selected_messages();
|
||||||
|
self.sync_selected_lineage();
|
||||||
|
self.refresh_logs();
|
||||||
|
}
|
||||||
|
|
||||||
self.output_follow = false;
|
self.output_follow = false;
|
||||||
let viewport_height = self.last_output_height.max(1);
|
let viewport_height = self.last_output_height.max(1);
|
||||||
let offset = line_index.saturating_sub(viewport_height.saturating_sub(1) / 2);
|
let offset = search_match
|
||||||
|
.line_index
|
||||||
|
.saturating_sub(viewport_height.saturating_sub(1) / 2);
|
||||||
self.output_scroll_offset = offset.min(self.max_output_scroll());
|
self.output_scroll_offset = offset.min(self.max_output_scroll());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2279,7 +2383,18 @@ impl Dashboard {
|
|||||||
self.selected_search_match.min(total.saturating_sub(1)) + 1
|
self.selected_search_match.min(total.saturating_sub(1)) + 1
|
||||||
};
|
};
|
||||||
|
|
||||||
format!("search /{query} match {current}/{total}")
|
format!(
|
||||||
|
"search /{query} match {current}/{total} | {}",
|
||||||
|
self.search_scope.label()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_match_session_count(&self) -> usize {
|
||||||
|
self.search_matches
|
||||||
|
.iter()
|
||||||
|
.map(|search_match| search_match.session_id.as_str())
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_output_scroll(&mut self, viewport_height: usize) {
|
fn sync_output_scroll(&mut self, viewport_height: usize) {
|
||||||
@@ -2952,6 +3067,40 @@ impl OutputTimeFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SearchScope {
|
||||||
|
fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::SelectedSession => Self::AllSessions,
|
||||||
|
Self::AllSessions => Self::SelectedSession,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::SelectedSession => "selected session",
|
||||||
|
Self::AllSessions => "all sessions",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_suffix(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::SelectedSession => "",
|
||||||
|
Self::AllSessions => " all sessions",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_ids<'a>(
|
||||||
|
self,
|
||||||
|
selected_session_id: Option<&'a str>,
|
||||||
|
sessions: &'a [Session],
|
||||||
|
) -> Vec<&'a str> {
|
||||||
|
match self {
|
||||||
|
Self::SelectedSession => selected_session_id.into_iter().collect(),
|
||||||
|
Self::AllSessions => sessions.iter().map(|session| session.id.as_str()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SessionSummary {
|
impl SessionSummary {
|
||||||
fn from_sessions(
|
fn from_sessions(
|
||||||
sessions: &[Session],
|
sessions: &[Session],
|
||||||
@@ -4257,11 +4406,23 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
dashboard.submit_search();
|
dashboard.submit_search();
|
||||||
|
|
||||||
assert_eq!(dashboard.search_query.as_deref(), Some("alpha.*"));
|
assert_eq!(dashboard.search_query.as_deref(), Some("alpha.*"));
|
||||||
assert_eq!(dashboard.search_matches, vec![0, 2]);
|
assert_eq!(
|
||||||
|
dashboard.search_matches,
|
||||||
|
vec![
|
||||||
|
SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 0,
|
||||||
|
},
|
||||||
|
SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
assert_eq!(dashboard.selected_search_match, 0);
|
assert_eq!(dashboard.selected_search_match, 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dashboard.operator_note.as_deref(),
|
dashboard.operator_note.as_deref(),
|
||||||
Some("search /alpha.* matched 2 line(s) | n/N navigate matches")
|
Some("search /alpha.* matched 2 line(s) across 1 session(s) | n/N navigate matches")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4295,7 +4456,7 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
assert_eq!(dashboard.output_scroll_offset, 2);
|
assert_eq!(dashboard.output_scroll_offset, 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dashboard.operator_note.as_deref(),
|
dashboard.operator_note.as_deref(),
|
||||||
Some(r"search /alpha-\d match 2/2")
|
Some(r"search /alpha-\d match 2/2 | selected session")
|
||||||
);
|
);
|
||||||
|
|
||||||
dashboard.next_search_match();
|
dashboard.next_search_match();
|
||||||
@@ -4338,7 +4499,16 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
let mut dashboard = test_dashboard(Vec::new(), 0);
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
dashboard.search_input = Some("draft".to_string());
|
dashboard.search_input = Some("draft".to_string());
|
||||||
dashboard.search_query = Some("alpha".to_string());
|
dashboard.search_query = Some("alpha".to_string());
|
||||||
dashboard.search_matches = vec![1, 3];
|
dashboard.search_matches = vec![
|
||||||
|
SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 1,
|
||||||
|
},
|
||||||
|
SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
dashboard.selected_search_match = 1;
|
dashboard.selected_search_match = 1;
|
||||||
|
|
||||||
dashboard.clear_search();
|
dashboard.clear_search();
|
||||||
@@ -4412,7 +4582,13 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
|
|
||||||
dashboard.recompute_search_matches();
|
dashboard.recompute_search_matches();
|
||||||
|
|
||||||
assert_eq!(dashboard.search_matches, vec![0]);
|
assert_eq!(
|
||||||
|
dashboard.search_matches,
|
||||||
|
vec![SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 0,
|
||||||
|
}]
|
||||||
|
);
|
||||||
assert_eq!(dashboard.visible_output_text(), "alpha stderr\nbeta stderr");
|
assert_eq!(dashboard.visible_output_text(), "alpha stderr\nbeta stderr");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4479,10 +4655,121 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
|
|
||||||
dashboard.recompute_search_matches();
|
dashboard.recompute_search_matches();
|
||||||
|
|
||||||
assert_eq!(dashboard.search_matches, vec![0]);
|
assert_eq!(
|
||||||
|
dashboard.search_matches,
|
||||||
|
vec![SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 0,
|
||||||
|
}]
|
||||||
|
);
|
||||||
assert_eq!(dashboard.visible_output_text(), "alpha recent\nbeta recent");
|
assert_eq!(dashboard.visible_output_text(), "alpha recent\nbeta recent");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_scope_all_sessions_matches_across_output_buffers() {
|
||||||
|
let mut dashboard = test_dashboard(
|
||||||
|
vec![
|
||||||
|
sample_session(
|
||||||
|
"focus-12345678",
|
||||||
|
"planner",
|
||||||
|
SessionState::Running,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
sample_session(
|
||||||
|
"review-87654321",
|
||||||
|
"reviewer",
|
||||||
|
SessionState::Running,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
dashboard.session_output_cache.insert(
|
||||||
|
"focus-12345678".to_string(),
|
||||||
|
vec![test_output_line(OutputStream::Stdout, "alpha local")],
|
||||||
|
);
|
||||||
|
dashboard.session_output_cache.insert(
|
||||||
|
"review-87654321".to_string(),
|
||||||
|
vec![test_output_line(OutputStream::Stdout, "alpha global")],
|
||||||
|
);
|
||||||
|
dashboard.search_query = Some("alpha.*".to_string());
|
||||||
|
|
||||||
|
dashboard.toggle_search_scope();
|
||||||
|
|
||||||
|
assert_eq!(dashboard.search_scope, SearchScope::AllSessions);
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.search_matches,
|
||||||
|
vec![
|
||||||
|
SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 0,
|
||||||
|
},
|
||||||
|
SearchMatch {
|
||||||
|
session_id: "review-87654321".to_string(),
|
||||||
|
line_index: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some("search scope set to all sessions | 2 match(es)")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.output_title(),
|
||||||
|
" Output all sessions /alpha.* 1/2 "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn next_search_match_switches_selected_session_in_all_sessions_scope() {
|
||||||
|
let mut dashboard = test_dashboard(
|
||||||
|
vec![
|
||||||
|
sample_session(
|
||||||
|
"focus-12345678",
|
||||||
|
"planner",
|
||||||
|
SessionState::Running,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
sample_session(
|
||||||
|
"review-87654321",
|
||||||
|
"reviewer",
|
||||||
|
SessionState::Running,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
dashboard.session_output_cache.insert(
|
||||||
|
"focus-12345678".to_string(),
|
||||||
|
vec![test_output_line(OutputStream::Stdout, "alpha local")],
|
||||||
|
);
|
||||||
|
dashboard.session_output_cache.insert(
|
||||||
|
"review-87654321".to_string(),
|
||||||
|
vec![test_output_line(OutputStream::Stdout, "alpha global")],
|
||||||
|
);
|
||||||
|
dashboard.search_scope = SearchScope::AllSessions;
|
||||||
|
dashboard.search_query = Some("alpha.*".to_string());
|
||||||
|
dashboard.last_output_height = 1;
|
||||||
|
dashboard.recompute_search_matches();
|
||||||
|
|
||||||
|
dashboard.next_search_match();
|
||||||
|
|
||||||
|
assert_eq!(dashboard.selected_session_id(), Some("review-87654321"));
|
||||||
|
assert_eq!(dashboard.selected_search_match, 1);
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some("search /alpha.* match 2/2 | all sessions")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stop_selected_uses_session_manager_transition() -> Result<()> {
|
async fn stop_selected_uses_session_manager_transition() -> 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()));
|
||||||
@@ -5236,6 +5523,7 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
last_output_height: 0,
|
last_output_height: 0,
|
||||||
search_input: None,
|
search_input: None,
|
||||||
search_query: None,
|
search_query: None,
|
||||||
|
search_scope: SearchScope::SelectedSession,
|
||||||
search_matches: Vec::new(),
|
search_matches: Vec::new(),
|
||||||
selected_search_match: 0,
|
selected_search_match: 0,
|
||||||
session_table_state,
|
session_table_state,
|
||||||
|
|||||||
Reference in New Issue
Block a user