mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-12 20:53:34 +08:00
feat: add ecc2 output content filters
This commit is contained in:
@@ -125,6 +125,8 @@ enum OutputMode {
|
|||||||
enum OutputFilter {
|
enum OutputFilter {
|
||||||
All,
|
All,
|
||||||
ErrorsOnly,
|
ErrorsOnly,
|
||||||
|
ToolCallsOnly,
|
||||||
|
FileChangesOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -535,8 +537,18 @@ impl Dashboard {
|
|||||||
(OutputFilter::ErrorsOnly, OutputTimeFilter::AllTime) => {
|
(OutputFilter::ErrorsOnly, OutputTimeFilter::AllTime) => {
|
||||||
"No stderr output for this session yet."
|
"No stderr output for this session yet."
|
||||||
}
|
}
|
||||||
|
(OutputFilter::ToolCallsOnly, OutputTimeFilter::AllTime) => {
|
||||||
|
"No tool-call output for this session yet."
|
||||||
|
}
|
||||||
|
(OutputFilter::FileChangesOnly, OutputTimeFilter::AllTime) => {
|
||||||
|
"No file-change output for this session yet."
|
||||||
|
}
|
||||||
(OutputFilter::All, _) => "No output lines in the selected time range.",
|
(OutputFilter::All, _) => "No output lines in the selected time range.",
|
||||||
(OutputFilter::ErrorsOnly, _) => "No stderr output in the selected time range.",
|
(OutputFilter::ErrorsOnly, _) => "No stderr output in the selected time range.",
|
||||||
|
(OutputFilter::ToolCallsOnly, _) => "No tool-call output in the selected time range.",
|
||||||
|
(OutputFilter::FileChangesOnly, _) => {
|
||||||
|
"No file-change output in the selected time range."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +668,7 @@ 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 search scope [A] agent filter [o] [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 cont[e]nt filter time [f]ilter search scope [A] agent filter [o] [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()
|
||||||
);
|
);
|
||||||
@@ -735,7 +747,7 @@ impl Dashboard {
|
|||||||
" G Dispatch then rebalance backlog across lead teams",
|
" G Dispatch then rebalance backlog across lead teams",
|
||||||
" v Toggle selected worktree diff in output pane",
|
" v Toggle selected worktree diff in output pane",
|
||||||
" 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 Cycle output content filter: all/errors/tool calls/file changes",
|
||||||
" 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",
|
" A Toggle search scope between selected session and all sessions",
|
||||||
" o Toggle search agent filter between all agents and selected agent type",
|
" o Toggle search agent filter between all agents and selected agent type",
|
||||||
@@ -1826,10 +1838,7 @@ impl Dashboard {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.output_filter = match self.output_filter {
|
self.output_filter = self.output_filter.next();
|
||||||
OutputFilter::All => OutputFilter::ErrorsOnly,
|
|
||||||
OutputFilter::ErrorsOnly => OutputFilter::All,
|
|
||||||
};
|
|
||||||
self.recompute_search_matches();
|
self.recompute_search_matches();
|
||||||
self.sync_output_scroll(self.last_output_height.max(1));
|
self.sync_output_scroll(self.last_output_height.max(1));
|
||||||
self.set_operator_note(format!(
|
self.set_operator_note(format!(
|
||||||
@@ -2363,8 +2372,7 @@ impl Dashboard {
|
|||||||
lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|line| {
|
.filter(|line| {
|
||||||
self.output_filter.matches(line.stream)
|
self.output_filter.matches(line) && self.output_time_filter.matches(line)
|
||||||
&& self.output_time_filter.matches(line)
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
@@ -3082,10 +3090,21 @@ impl Pane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OutputFilter {
|
impl OutputFilter {
|
||||||
fn matches(self, stream: OutputStream) -> bool {
|
fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::All => Self::ErrorsOnly,
|
||||||
|
Self::ErrorsOnly => Self::ToolCallsOnly,
|
||||||
|
Self::ToolCallsOnly => Self::FileChangesOnly,
|
||||||
|
Self::FileChangesOnly => Self::All,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(self, line: &OutputLine) -> bool {
|
||||||
match self {
|
match self {
|
||||||
OutputFilter::All => true,
|
OutputFilter::All => true,
|
||||||
OutputFilter::ErrorsOnly => stream == OutputStream::Stderr,
|
OutputFilter::ErrorsOnly => line.stream == OutputStream::Stderr,
|
||||||
|
OutputFilter::ToolCallsOnly => looks_like_tool_call(&line.text),
|
||||||
|
OutputFilter::FileChangesOnly => looks_like_file_change(&line.text),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3093,6 +3112,8 @@ impl OutputFilter {
|
|||||||
match self {
|
match self {
|
||||||
OutputFilter::All => "all",
|
OutputFilter::All => "all",
|
||||||
OutputFilter::ErrorsOnly => "errors",
|
OutputFilter::ErrorsOnly => "errors",
|
||||||
|
OutputFilter::ToolCallsOnly => "tool calls",
|
||||||
|
OutputFilter::FileChangesOnly => "file changes",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3100,10 +3121,97 @@ impl OutputFilter {
|
|||||||
match self {
|
match self {
|
||||||
OutputFilter::All => "",
|
OutputFilter::All => "",
|
||||||
OutputFilter::ErrorsOnly => " errors",
|
OutputFilter::ErrorsOnly => " errors",
|
||||||
|
OutputFilter::ToolCallsOnly => " tool calls",
|
||||||
|
OutputFilter::FileChangesOnly => " file changes",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn looks_like_tool_call(text: &str) -> bool {
|
||||||
|
let lower = text.trim().to_ascii_lowercase();
|
||||||
|
if lower.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOL_PREFIXES: &[&str] = &[
|
||||||
|
"tool ",
|
||||||
|
"tool:",
|
||||||
|
"[tool",
|
||||||
|
"tool call",
|
||||||
|
"calling tool",
|
||||||
|
"running tool",
|
||||||
|
"invoking tool",
|
||||||
|
"using tool",
|
||||||
|
"read(",
|
||||||
|
"write(",
|
||||||
|
"edit(",
|
||||||
|
"multi_edit(",
|
||||||
|
"bash(",
|
||||||
|
"grep(",
|
||||||
|
"glob(",
|
||||||
|
"search(",
|
||||||
|
"ls(",
|
||||||
|
"apply_patch(",
|
||||||
|
];
|
||||||
|
|
||||||
|
TOOL_PREFIXES.iter().any(|prefix| lower.starts_with(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn looks_like_file_change(text: &str) -> bool {
|
||||||
|
let lower = text.trim().to_ascii_lowercase();
|
||||||
|
if lower.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if lower.contains("applied patch")
|
||||||
|
|| lower.contains("patch applied")
|
||||||
|
|| lower.starts_with("diff --git ")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILE_CHANGE_VERBS: &[&str] = &[
|
||||||
|
"updated ",
|
||||||
|
"created ",
|
||||||
|
"deleted ",
|
||||||
|
"renamed ",
|
||||||
|
"modified ",
|
||||||
|
"wrote ",
|
||||||
|
"editing ",
|
||||||
|
"edited ",
|
||||||
|
"writing ",
|
||||||
|
];
|
||||||
|
|
||||||
|
FILE_CHANGE_VERBS
|
||||||
|
.iter()
|
||||||
|
.any(|prefix| lower.starts_with(prefix) && contains_path_like_token(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_path_like_token(text: &str) -> bool {
|
||||||
|
text.split_whitespace().any(|token| {
|
||||||
|
let trimmed = token.trim_matches(|ch: char| {
|
||||||
|
matches!(
|
||||||
|
ch,
|
||||||
|
'[' | ']' | '(' | ')' | '{' | '}' | ',' | ':' | ';' | '"' | '\''
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
trimmed.contains('/')
|
||||||
|
|| trimmed.contains('\\')
|
||||||
|
|| trimmed.starts_with("./")
|
||||||
|
|| trimmed.starts_with("../")
|
||||||
|
|| trimmed
|
||||||
|
.rsplit_once('.')
|
||||||
|
.map(|(stem, ext)| {
|
||||||
|
!stem.is_empty()
|
||||||
|
&& !ext.is_empty()
|
||||||
|
&& ext.len() <= 10
|
||||||
|
&& ext.chars().all(|ch| ch.is_ascii_alphanumeric())
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl OutputTimeFilter {
|
impl OutputTimeFilter {
|
||||||
fn next(self) -> Self {
|
fn next(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
@@ -4658,6 +4766,55 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_output_filter_cycles_tool_calls_and_file_changes() {
|
||||||
|
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(OutputStream::Stdout, "normal output"),
|
||||||
|
test_output_line(OutputStream::Stdout, "Read(src/lib.rs)"),
|
||||||
|
test_output_line(OutputStream::Stdout, "Updated ecc2/src/tui/dashboard.rs"),
|
||||||
|
test_output_line(OutputStream::Stderr, "stderr line"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboard.toggle_output_filter();
|
||||||
|
assert_eq!(dashboard.output_filter, OutputFilter::ErrorsOnly);
|
||||||
|
assert_eq!(dashboard.visible_output_text(), "stderr line");
|
||||||
|
|
||||||
|
dashboard.toggle_output_filter();
|
||||||
|
assert_eq!(dashboard.output_filter, OutputFilter::ToolCallsOnly);
|
||||||
|
assert_eq!(dashboard.visible_output_text(), "Read(src/lib.rs)");
|
||||||
|
assert_eq!(dashboard.output_title(), " Output tool calls ");
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some("output filter set to tool calls")
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboard.toggle_output_filter();
|
||||||
|
assert_eq!(dashboard.output_filter, OutputFilter::FileChangesOnly);
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.visible_output_text(),
|
||||||
|
"Updated ecc2/src/tui/dashboard.rs"
|
||||||
|
);
|
||||||
|
assert_eq!(dashboard.output_title(), " Output file changes ");
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some("output filter set to file changes")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn search_matches_respect_error_only_filter() {
|
fn search_matches_respect_error_only_filter() {
|
||||||
let mut dashboard = test_dashboard(
|
let mut dashboard = test_dashboard(
|
||||||
@@ -4695,6 +4852,86 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
assert_eq!(dashboard.visible_output_text(), "alpha stderr\nbeta stderr");
|
assert_eq!(dashboard.visible_output_text(), "alpha stderr\nbeta stderr");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_matches_respect_tool_call_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(OutputStream::Stdout, "alpha normal"),
|
||||||
|
test_output_line(OutputStream::Stdout, "Read(alpha.rs)"),
|
||||||
|
test_output_line(OutputStream::Stdout, "Write(beta.rs)"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
dashboard.output_filter = OutputFilter::ToolCallsOnly;
|
||||||
|
dashboard.search_query = Some("alpha.*".to_string());
|
||||||
|
dashboard.last_output_height = 1;
|
||||||
|
|
||||||
|
dashboard.recompute_search_matches();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.search_matches,
|
||||||
|
vec![SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 0,
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.visible_output_text(),
|
||||||
|
"Read(alpha.rs)\nWrite(beta.rs)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_matches_respect_file_change_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(OutputStream::Stdout, "alpha normal"),
|
||||||
|
test_output_line(OutputStream::Stdout, "Updated alpha.rs"),
|
||||||
|
test_output_line(OutputStream::Stdout, "Renamed beta.rs to gamma.rs"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
dashboard.output_filter = OutputFilter::FileChangesOnly;
|
||||||
|
dashboard.search_query = Some("alpha.*".to_string());
|
||||||
|
dashboard.last_output_height = 1;
|
||||||
|
|
||||||
|
dashboard.recompute_search_matches();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.search_matches,
|
||||||
|
vec![SearchMatch {
|
||||||
|
session_id: "focus-12345678".to_string(),
|
||||||
|
line_index: 0,
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.visible_output_text(),
|
||||||
|
"Updated alpha.rs\nRenamed beta.rs to gamma.rs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_output_time_filter_keeps_only_recent_lines() {
|
fn cycle_output_time_filter_keeps_only_recent_lines() {
|
||||||
let mut dashboard = test_dashboard(
|
let mut dashboard = test_dashboard(
|
||||||
|
|||||||
Reference in New Issue
Block a user