mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 17:59:43 +08:00
feat: add ecc2 output time filters
This commit is contained in:
parent
077f46b777
commit
3b700c8715
@ -32,6 +32,31 @@ impl OutputStream {
|
||||
pub struct OutputLine {
|
||||
pub stream: OutputStream,
|
||||
pub text: String,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
impl OutputLine {
|
||||
pub fn new(
|
||||
stream: OutputStream,
|
||||
text: impl Into<String>,
|
||||
timestamp: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
text: text.into(),
|
||||
timestamp: timestamp.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_current_timestamp(stream: OutputStream, text: impl Into<String>) -> Self {
|
||||
Self::new(stream, text, chrono::Utc::now().to_rfc3339())
|
||||
}
|
||||
|
||||
pub fn occurred_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
|
||||
chrono::DateTime::parse_from_rfc3339(&self.timestamp)
|
||||
.ok()
|
||||
.map(|timestamp| timestamp.with_timezone(&chrono::Utc))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@ -70,10 +95,7 @@ impl SessionOutputStore {
|
||||
}
|
||||
|
||||
pub fn push_line(&self, session_id: &str, stream: OutputStream, text: impl Into<String>) {
|
||||
let line = OutputLine {
|
||||
stream,
|
||||
text: text.into(),
|
||||
};
|
||||
let line = OutputLine::with_current_timestamp(stream, text);
|
||||
|
||||
{
|
||||
let mut buffers = self.lock_buffers();
|
||||
@ -145,5 +167,6 @@ mod tests {
|
||||
assert_eq!(event.session_id, "session-1");
|
||||
assert_eq!(event.line.stream, OutputStream::Stderr);
|
||||
assert_eq!(event.line.text, "problem");
|
||||
assert!(event.line.occurred_at().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@ -961,9 +961,9 @@ impl StateStore {
|
||||
|
||||
pub fn get_output_lines(&self, session_id: &str, limit: usize) -> Result<Vec<OutputLine>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT stream, line
|
||||
"SELECT stream, line, timestamp
|
||||
FROM (
|
||||
SELECT id, stream, line
|
||||
SELECT id, stream, line, timestamp
|
||||
FROM session_output
|
||||
WHERE session_id = ?1
|
||||
ORDER BY id DESC
|
||||
@ -976,11 +976,13 @@ impl StateStore {
|
||||
.query_map(rusqlite::params![session_id, limit as i64], |row| {
|
||||
let stream: String = row.get(0)?;
|
||||
let text: String = row.get(1)?;
|
||||
let timestamp: String = row.get(2)?;
|
||||
|
||||
Ok(OutputLine {
|
||||
stream: OutputStream::from_db_value(&stream),
|
||||
Ok(OutputLine::new(
|
||||
OutputStream::from_db_value(&stream),
|
||||
text,
|
||||
})
|
||||
timestamp,
|
||||
))
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
||||
(_, KeyCode::Char('v')) => dashboard.toggle_output_mode(),
|
||||
(_, KeyCode::Char('c')) => dashboard.toggle_conflict_protocol_mode(),
|
||||
(_, KeyCode::Char('e')) => dashboard.toggle_output_filter(),
|
||||
(_, KeyCode::Char('f')) => dashboard.cycle_output_time_filter(),
|
||||
(_, KeyCode::Char('m')) => dashboard.merge_selected_worktree().await,
|
||||
(_, KeyCode::Char('M')) => dashboard.merge_ready_worktrees().await,
|
||||
(_, KeyCode::Char('l')) => dashboard.cycle_pane_layout(),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{
|
||||
@ -72,6 +73,7 @@ pub struct Dashboard {
|
||||
selected_merge_readiness: Option<worktree::MergeReadiness>,
|
||||
output_mode: OutputMode,
|
||||
output_filter: OutputFilter,
|
||||
output_time_filter: OutputTimeFilter,
|
||||
selected_pane: Pane,
|
||||
selected_session: usize,
|
||||
show_help: bool,
|
||||
@ -123,6 +125,14 @@ enum OutputFilter {
|
||||
ErrorsOnly,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum OutputTimeFilter {
|
||||
AllTime,
|
||||
Last15Minutes,
|
||||
LastHour,
|
||||
Last24Hours,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PaneAreas {
|
||||
sessions: Rect,
|
||||
@ -201,6 +211,7 @@ impl Dashboard {
|
||||
selected_merge_readiness: None,
|
||||
output_mode: OutputMode::SessionOutput,
|
||||
output_filter: OutputFilter::All,
|
||||
output_time_filter: OutputTimeFilter::AllTime,
|
||||
selected_pane: Pane::Sessions,
|
||||
selected_session: 0,
|
||||
show_help: false,
|
||||
@ -472,7 +483,11 @@ impl Dashboard {
|
||||
}
|
||||
|
||||
fn output_title(&self) -> String {
|
||||
let filter = self.output_filter_label();
|
||||
let filter = format!(
|
||||
"{}{}",
|
||||
self.output_filter.title_suffix(),
|
||||
self.output_time_filter.title_suffix()
|
||||
);
|
||||
if let Some(input) = self.search_input.as_ref() {
|
||||
return format!(" Output{filter} /{input}_ ");
|
||||
}
|
||||
@ -490,17 +505,14 @@ impl Dashboard {
|
||||
format!(" Output{filter} ")
|
||||
}
|
||||
|
||||
fn output_filter_label(&self) -> &'static str {
|
||||
match self.output_filter {
|
||||
OutputFilter::All => "",
|
||||
OutputFilter::ErrorsOnly => " errors",
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_output_message(&self) -> &'static str {
|
||||
match self.output_filter {
|
||||
OutputFilter::All => "Waiting for session output...",
|
||||
OutputFilter::ErrorsOnly => "No stderr output for this session yet.",
|
||||
match (self.output_filter, self.output_time_filter) {
|
||||
(OutputFilter::All, OutputTimeFilter::AllTime) => "Waiting for session output...",
|
||||
(OutputFilter::ErrorsOnly, OutputTimeFilter::AllTime) => {
|
||||
"No stderr output for this session yet."
|
||||
}
|
||||
(OutputFilter::All, _) => "No output lines in the selected time range.",
|
||||
(OutputFilter::ErrorsOnly, _) => "No stderr output in the selected time range.",
|
||||
}
|
||||
}
|
||||
|
||||
@ -611,7 +623,7 @@ impl Dashboard {
|
||||
|
||||
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
||||
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 [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 [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.theme_label()
|
||||
);
|
||||
@ -683,6 +695,7 @@ impl Dashboard {
|
||||
" v Toggle selected worktree diff in output pane",
|
||||
" c Show conflict-resolution protocol for selected conflicted worktree",
|
||||
" e Toggle output filter between all lines and stderr only",
|
||||
" f Cycle output time filter between all/15m/1h/24h",
|
||||
" m Merge selected ready worktree into base and clean it up",
|
||||
" M Merge all ready inactive worktrees and clean them up",
|
||||
" l Cycle pane layout and persist it",
|
||||
@ -1724,6 +1737,23 @@ impl Dashboard {
|
||||
));
|
||||
}
|
||||
|
||||
pub fn cycle_output_time_filter(&mut self) {
|
||||
if self.output_mode != OutputMode::SessionOutput {
|
||||
self.set_operator_note(
|
||||
"output time filters are only available in session output view".to_string(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
self.output_time_filter = self.output_time_filter.next();
|
||||
self.recompute_search_matches();
|
||||
self.sync_output_scroll(self.last_output_height.max(1));
|
||||
self.set_operator_note(format!(
|
||||
"output time filter set to {}",
|
||||
self.output_time_filter.label()
|
||||
));
|
||||
}
|
||||
|
||||
pub fn toggle_auto_dispatch_policy(&mut self) {
|
||||
self.cfg.auto_dispatch_unread_handoffs = !self.cfg.auto_dispatch_unread_handoffs;
|
||||
match self.cfg.save() {
|
||||
@ -2192,7 +2222,9 @@ impl Dashboard {
|
||||
fn visible_output_lines(&self) -> Vec<&OutputLine> {
|
||||
self.selected_output_lines()
|
||||
.iter()
|
||||
.filter(|line| self.output_filter.matches(line.stream))
|
||||
.filter(|line| {
|
||||
self.output_filter.matches(line.stream) && self.output_time_filter.matches(line)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -2864,6 +2896,60 @@ impl OutputFilter {
|
||||
OutputFilter::ErrorsOnly => "errors",
|
||||
}
|
||||
}
|
||||
|
||||
fn title_suffix(self) -> &'static str {
|
||||
match self {
|
||||
OutputFilter::All => "",
|
||||
OutputFilter::ErrorsOnly => " errors",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputTimeFilter {
|
||||
fn next(self) -> Self {
|
||||
match self {
|
||||
Self::AllTime => Self::Last15Minutes,
|
||||
Self::Last15Minutes => Self::LastHour,
|
||||
Self::LastHour => Self::Last24Hours,
|
||||
Self::Last24Hours => Self::AllTime,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(self, line: &OutputLine) -> bool {
|
||||
match self {
|
||||
Self::AllTime => true,
|
||||
Self::Last15Minutes => line
|
||||
.occurred_at()
|
||||
.map(|timestamp| timestamp >= Utc::now() - Duration::minutes(15))
|
||||
.unwrap_or(false),
|
||||
Self::LastHour => line
|
||||
.occurred_at()
|
||||
.map(|timestamp| timestamp >= Utc::now() - Duration::hours(1))
|
||||
.unwrap_or(false),
|
||||
Self::Last24Hours => line
|
||||
.occurred_at()
|
||||
.map(|timestamp| timestamp >= Utc::now() - Duration::hours(24))
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::AllTime => "all time",
|
||||
Self::Last15Minutes => "last 15m",
|
||||
Self::LastHour => "last 1h",
|
||||
Self::Last24Hours => "last 24h",
|
||||
}
|
||||
}
|
||||
|
||||
fn title_suffix(self) -> &'static str {
|
||||
match self {
|
||||
Self::AllTime => "",
|
||||
Self::Last15Minutes => " last 15m",
|
||||
Self::LastHour => " last 1h",
|
||||
Self::Last24Hours => " last 24h",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionSummary {
|
||||
@ -3320,10 +3406,7 @@ mod tests {
|
||||
);
|
||||
dashboard.session_output_cache.insert(
|
||||
"focus-12345678".to_string(),
|
||||
vec![OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "last useful output".to_string(),
|
||||
}],
|
||||
vec![test_output_line(OutputStream::Stdout, "last useful output")],
|
||||
);
|
||||
dashboard.selected_diff_summary = Some("1 file changed, 2 insertions(+)".to_string());
|
||||
dashboard.selected_diff_preview = vec![
|
||||
@ -4160,18 +4243,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
dashboard.session_output_cache.insert(
|
||||
"focus-12345678".to_string(),
|
||||
vec![
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "alpha".to_string(),
|
||||
},
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "beta".to_string(),
|
||||
},
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "alpha tail".to_string(),
|
||||
},
|
||||
test_output_line(OutputStream::Stdout, "alpha"),
|
||||
test_output_line(OutputStream::Stdout, "beta"),
|
||||
test_output_line(OutputStream::Stdout, "alpha tail"),
|
||||
],
|
||||
);
|
||||
dashboard.last_output_height = 2;
|
||||
@ -4207,18 +4281,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
dashboard.session_output_cache.insert(
|
||||
"focus-12345678".to_string(),
|
||||
vec![
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "alpha-1".to_string(),
|
||||
},
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "beta".to_string(),
|
||||
},
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "alpha-2".to_string(),
|
||||
},
|
||||
test_output_line(OutputStream::Stdout, "alpha-1"),
|
||||
test_output_line(OutputStream::Stdout, "beta"),
|
||||
test_output_line(OutputStream::Stdout, "alpha-2"),
|
||||
],
|
||||
);
|
||||
dashboard.search_query = Some(r"alpha-\d".to_string());
|
||||
@ -4304,14 +4369,8 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
dashboard.session_output_cache.insert(
|
||||
"focus-12345678".to_string(),
|
||||
vec![
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "stdout line".to_string(),
|
||||
},
|
||||
OutputLine {
|
||||
stream: OutputStream::Stderr,
|
||||
text: "stderr line".to_string(),
|
||||
},
|
||||
test_output_line(OutputStream::Stdout, "stdout line"),
|
||||
test_output_line(OutputStream::Stderr, "stderr line"),
|
||||
],
|
||||
);
|
||||
|
||||
@ -4342,18 +4401,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
dashboard.session_output_cache.insert(
|
||||
"focus-12345678".to_string(),
|
||||
vec![
|
||||
OutputLine {
|
||||
stream: OutputStream::Stdout,
|
||||
text: "alpha stdout".to_string(),
|
||||
},
|
||||
OutputLine {
|
||||
stream: OutputStream::Stderr,
|
||||
text: "alpha stderr".to_string(),
|
||||
},
|
||||
OutputLine {
|
||||
stream: OutputStream::Stderr,
|
||||
text: "beta stderr".to_string(),
|
||||
},
|
||||
test_output_line(OutputStream::Stdout, "alpha stdout"),
|
||||
test_output_line(OutputStream::Stderr, "alpha stderr"),
|
||||
test_output_line(OutputStream::Stderr, "beta stderr"),
|
||||
],
|
||||
);
|
||||
dashboard.output_filter = OutputFilter::ErrorsOnly;
|
||||
@ -4366,6 +4416,73 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
assert_eq!(dashboard.visible_output_text(), "alpha stderr\nbeta stderr");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_output_time_filter_keeps_only_recent_lines() {
|
||||
let mut dashboard = test_dashboard(
|
||||
vec![sample_session(
|
||||
"focus-12345678",
|
||||
"planner",
|
||||
SessionState::Running,
|
||||
None,
|
||||
1,
|
||||
1,
|
||||
)],
|
||||
0,
|
||||
);
|
||||
dashboard.session_output_cache.insert(
|
||||
"focus-12345678".to_string(),
|
||||
vec![
|
||||
test_output_line_minutes_ago(OutputStream::Stdout, "recent line", 5),
|
||||
test_output_line_minutes_ago(OutputStream::Stdout, "older line", 45),
|
||||
test_output_line_minutes_ago(OutputStream::Stdout, "stale line", 180),
|
||||
],
|
||||
);
|
||||
|
||||
dashboard.cycle_output_time_filter();
|
||||
|
||||
assert_eq!(
|
||||
dashboard.output_time_filter,
|
||||
OutputTimeFilter::Last15Minutes
|
||||
);
|
||||
assert_eq!(dashboard.visible_output_text(), "recent line");
|
||||
assert_eq!(dashboard.output_title(), " Output last 15m ");
|
||||
assert_eq!(
|
||||
dashboard.operator_note.as_deref(),
|
||||
Some("output time filter set to last 15m")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_matches_respect_time_filter() {
|
||||
let mut dashboard = test_dashboard(
|
||||
vec![sample_session(
|
||||
"focus-12345678",
|
||||
"planner",
|
||||
SessionState::Running,
|
||||
None,
|
||||
1,
|
||||
1,
|
||||
)],
|
||||
0,
|
||||
);
|
||||
dashboard.session_output_cache.insert(
|
||||
"focus-12345678".to_string(),
|
||||
vec![
|
||||
test_output_line_minutes_ago(OutputStream::Stdout, "alpha recent", 10),
|
||||
test_output_line_minutes_ago(OutputStream::Stdout, "beta recent", 10),
|
||||
test_output_line_minutes_ago(OutputStream::Stdout, "alpha stale", 180),
|
||||
],
|
||||
);
|
||||
dashboard.output_time_filter = OutputTimeFilter::Last15Minutes;
|
||||
dashboard.search_query = Some("alpha.*".to_string());
|
||||
dashboard.last_output_height = 1;
|
||||
|
||||
dashboard.recompute_search_matches();
|
||||
|
||||
assert_eq!(dashboard.search_matches, vec![0]);
|
||||
assert_eq!(dashboard.visible_output_text(), "alpha recent\nbeta recent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_selected_uses_session_manager_transition() -> Result<()> {
|
||||
let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
|
||||
@ -5056,6 +5173,22 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
assert_eq!(dashboard.theme_palette().row_highlight_bg, Color::Gray);
|
||||
}
|
||||
|
||||
fn test_output_line(stream: OutputStream, text: &str) -> OutputLine {
|
||||
OutputLine::new(stream, text, Utc::now().to_rfc3339())
|
||||
}
|
||||
|
||||
fn test_output_line_minutes_ago(
|
||||
stream: OutputStream,
|
||||
text: &str,
|
||||
minutes_ago: i64,
|
||||
) -> OutputLine {
|
||||
OutputLine::new(
|
||||
stream,
|
||||
text,
|
||||
(Utc::now() - chrono::Duration::minutes(minutes_ago)).to_rfc3339(),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_dashboard(sessions: Vec<Session>, selected_session: usize) -> Dashboard {
|
||||
let selected_session = selected_session.min(sessions.len().saturating_sub(1));
|
||||
let cfg = Config::default();
|
||||
@ -5093,6 +5226,7 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
selected_merge_readiness: None,
|
||||
output_mode: OutputMode::SessionOutput,
|
||||
output_filter: OutputFilter::All,
|
||||
output_time_filter: OutputTimeFilter::AllTime,
|
||||
selected_pane: Pane::Sessions,
|
||||
selected_session,
|
||||
show_help: false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user