@ -20,14 +20,10 @@
//! | `id` | string | `:` (substring) |
//! | `id` | string | `:` (substring) |
//! | `agents` | numeric | `:`, `>`, `<` |
//! | `agents` | numeric | `:`, `>`, `<` |
//! | `messages` | numeric | `:`, `>`, `<` |
//! | `messages` | numeric | `:`, `>`, `<` |
//! | `in` | numeric | `:`, `>`, `<` |
//! | `out` | numeric | `:`, `>`, `<` |
//! | `tokens` | numeric | `:`, `>`, `<` |
//! | `tokens` | numeric | `:`, `>`, `<` |
//! | `date` | date | `:`, `>`, `<` |
//! | `date` | date | `:`, `>`, `<` |
//!
//! # Not-yet-wired fields
//!
//! - `tokens` — the [`SessionRow`] data available from [`crate::tui::state::SessionListItem`]
//! does not yet carry a total token count; `tokens:` / `tokens>` / `tokens<` filters will
//! always return `false` until the field is added to `SessionListItem`.
use chrono ::NaiveDate ;
use chrono ::NaiveDate ;
@ -52,9 +48,17 @@ pub trait SessionRow {
fn agents ( & self ) -> u64 ;
fn agents ( & self ) -> u64 ;
/// Total message count (all roles).
/// Total message count (all roles).
fn messages ( & self ) -> u64 ;
fn messages ( & self ) -> u64 ;
/// Total token count.
/// Input (prompt) token count.
///
///
/// Returns `None` when the token count is not available for this row type.
/// Returns `None` when the token count is not available for this row type.
fn input_tokens ( & self ) -> Option < u64 > ;
/// Output (completion) token count.
///
/// Returns `None` when the token count is not available for this row type.
fn output_tokens ( & self ) -> Option < u64 > ;
/// Total token count (input + output).
///
/// Returns `None` when token counts are not available for this row type.
fn tokens ( & self ) -> Option < u64 > ;
fn tokens ( & self ) -> Option < u64 > ;
/// Session date.
/// Session date.
///
///
@ -78,9 +82,14 @@ impl SessionRow for crate::tui::state::SessionListItem {
fn messages ( & self ) -> u64 {
fn messages ( & self ) -> u64 {
self . msg_count as u64
self . msg_count as u64
}
}
/// Token count is not yet tracked in `SessionListItem`; always returns `None`.
fn input_tokens ( & self ) -> Option < u64 > {
Some ( self . input_tokens )
}
fn output_tokens ( & self ) -> Option < u64 > {
Some ( self . output_tokens )
}
fn tokens ( & self ) -> Option < u64 > {
fn tokens ( & self ) -> Option < u64 > {
None
Some( self . input_tokens + self . output_tokens )
}
}
fn date ( & self ) -> Option < NaiveDate > {
fn date ( & self ) -> Option < NaiveDate > {
// `date` field is e.g. "2026-03-30 14:22:01"; parse just the date portion.
// `date` field is e.g. "2026-03-30 14:22:01"; parse just the date portion.
@ -113,6 +122,11 @@ enum Key {
Id ,
Id ,
Agents ,
Agents ,
Messages ,
Messages ,
/// Input (prompt) token count — `in:`, `in>`, `in<`.
In ,
/// Output (completion) token count — `out:`, `out>`, `out<`.
Out ,
/// Total token count (input + output) — `tokens:`, `tokens>`, `tokens<`.
Tokens ,
Tokens ,
Date ,
Date ,
}
}
@ -125,6 +139,8 @@ impl Key {
"id" = > Some ( Key ::Id ) ,
"id" = > Some ( Key ::Id ) ,
"agents" = > Some ( Key ::Agents ) ,
"agents" = > Some ( Key ::Agents ) ,
"messages" = > Some ( Key ::Messages ) ,
"messages" = > Some ( Key ::Messages ) ,
"in" = > Some ( Key ::In ) ,
"out" = > Some ( Key ::Out ) ,
"tokens" = > Some ( Key ::Tokens ) ,
"tokens" = > Some ( Key ::Tokens ) ,
"date" = > Some ( Key ::Date ) ,
"date" = > Some ( Key ::Date ) ,
_ = > None ,
_ = > None ,
@ -214,7 +230,7 @@ fn parse_atom(atom: &str) -> Result<Predicate> {
let key = Key ::parse ( key_str ) . ok_or_else ( | | {
let key = Key ::parse ( key_str ) . ok_or_else ( | | {
AppError ::Parse ( format! (
AppError ::Parse ( format! (
"unknown filter key '{key_str}' — valid keys: model, project, id, agents, messages, tokens, date"
"unknown filter key '{key_str}' — valid keys: model, project, id, agents, messages, in, out, tokens, date"
) )
) )
} ) ? ;
} ) ? ;
@ -396,6 +412,14 @@ fn eval_pred<R: SessionRow>(pred: &Predicate, row: &R) -> bool {
// ── Numeric keys ────────────────────────────────────────────────────
// ── Numeric keys ────────────────────────────────────────────────────
Key ::Agents = > eval_numeric ( row . agents ( ) , & pred . op , & pred . value ) ,
Key ::Agents = > eval_numeric ( row . agents ( ) , & pred . op , & pred . value ) ,
Key ::Messages = > eval_numeric ( row . messages ( ) , & pred . op , & pred . value ) ,
Key ::Messages = > eval_numeric ( row . messages ( ) , & pred . op , & pred . value ) ,
Key ::In = > match row . input_tokens ( ) {
Some ( v ) = > eval_numeric ( v , & pred . op , & pred . value ) ,
None = > false ,
} ,
Key ::Out = > match row . output_tokens ( ) {
Some ( v ) = > eval_numeric ( v , & pred . op , & pred . value ) ,
None = > false ,
} ,
Key ::Tokens = > match row . tokens ( ) {
Key ::Tokens = > match row . tokens ( ) {
Some ( v ) = > eval_numeric ( v , & pred . op , & pred . value ) ,
Some ( v ) = > eval_numeric ( v , & pred . op , & pred . value ) ,
// Token count not available for this row type — filter does not match.
// Token count not available for this row type — filter does not match.
@ -479,6 +503,23 @@ mod tests {
model : model . to_string ( ) ,
model : model . to_string ( ) ,
msg_count ,
msg_count ,
agent_count ,
agent_count ,
input_tokens : 0 ,
output_tokens : 0 ,
}
}
/// Build a row with the given token counts; all other fields use fixed defaults.
fn make_row_with_tokens ( full_id : & str , input_tokens : u64 , output_tokens : u64 ) -> SessionListItem {
SessionListItem {
short_id : full_id . get ( .. 8 ) . unwrap_or ( full_id ) . to_string ( ) ,
full_id : full_id . to_string ( ) ,
date : "2026-03-30 10:00:00" . to_string ( ) ,
project : "/proj" . to_string ( ) ,
model : "model" . to_string ( ) ,
msg_count : 5 ,
agent_count : 0 ,
input_tokens ,
output_tokens ,
}
}
}
}
@ -979,12 +1020,67 @@ mod tests {
assert! ( f . matches ( & haiku_agents ) ) ;
assert! ( f . matches ( & haiku_agents ) ) ;
}
}
// ── Token s not available ─────────────────────────────────────────────────
// ── Token filter fields ─ ─────────────────────────────────────────────────
#[ test ]
#[ test ]
fn tokens_filter_returns_false_for_session_list_item ( ) {
fn tokens_gt_matches_when_total_exceeds_threshold ( ) {
// SessionListItem does not track token count; tokens filter always false.
let f = Filter ::parse ( "tokens>50000" ) . unwrap ( ) ;
let f = Filter ::parse ( "tokens>100" ) . unwrap ( ) ;
let big = make_row_with_tokens ( "aaaaaaaa-0000-0000-0000-000000000000" , 40000 , 20000 ) ;
let small = make_row_with_tokens ( "bbbbbbbb-0000-0000-0000-000000000000" , 1000 , 500 ) ;
assert! ( f . matches ( & big ) ) ; // 60000 > 50000
assert! ( ! f . matches ( & small ) ) ; // 1500 is not > 50000
}
#[ test ]
fn tokens_lt_matches_when_total_below_threshold ( ) {
let f = Filter ::parse ( "tokens<10000" ) . unwrap ( ) ;
let small = make_row_with_tokens ( "aaaaaaaa-0000-0000-0000-000000000000" , 3000 , 2000 ) ;
let big = make_row_with_tokens ( "bbbbbbbb-0000-0000-0000-000000000000" , 20000 , 5000 ) ;
assert! ( f . matches ( & small ) ) ; // 5000 < 10000
assert! ( ! f . matches ( & big ) ) ; // 25000 is not < 10000
}
#[ test ]
fn tokens_colon_equality ( ) {
let f = Filter ::parse ( "tokens:1000" ) . unwrap ( ) ;
let exact = make_row_with_tokens ( "aaaaaaaa-0000-0000-0000-000000000000" , 700 , 300 ) ;
let other = make_row_with_tokens ( "bbbbbbbb-0000-0000-0000-000000000000" , 700 , 400 ) ;
assert! ( f . matches ( & exact ) ) ; // 700+300 == 1000
assert! ( ! f . matches ( & other ) ) ; // 1100 != 1000
}
#[ test ]
fn in_gt_matches_input_tokens ( ) {
let f = Filter ::parse ( "in>5000" ) . unwrap ( ) ;
let high = make_row_with_tokens ( "aaaaaaaa-0000-0000-0000-000000000000" , 10000 , 1000 ) ;
let low = make_row_with_tokens ( "bbbbbbbb-0000-0000-0000-000000000000" , 500 , 1000 ) ;
assert! ( f . matches ( & high ) ) ; // 10000 > 5000
assert! ( ! f . matches ( & low ) ) ; // 500 is not > 5000
}
#[ test ]
fn out_lt_matches_output_tokens ( ) {
let f = Filter ::parse ( "out<1000" ) . unwrap ( ) ;
let low_out = make_row_with_tokens ( "aaaaaaaa-0000-0000-0000-000000000000" , 5000 , 200 ) ;
let high_out = make_row_with_tokens ( "bbbbbbbb-0000-0000-0000-000000000000" , 5000 , 5000 ) ;
assert! ( f . matches ( & low_out ) ) ; // 200 < 1000
assert! ( ! f . matches ( & high_out ) ) ; // 5000 is not < 1000
}
#[ test ]
fn in_and_out_combined ( ) {
// Large input AND small output — very "reading-heavy" sessions.
let f = Filter ::parse ( "in>10000 AND out<500" ) . unwrap ( ) ;
let reading_heavy = make_row_with_tokens ( "aaaaaaaa-0000-0000-0000-000000000000" , 50000 , 200 ) ;
let balanced = make_row_with_tokens ( "bbbbbbbb-0000-0000-0000-000000000000" , 50000 , 2000 ) ;
assert! ( f . matches ( & reading_heavy ) ) ;
assert! ( ! f . matches ( & balanced ) ) ;
}
#[ test ]
fn tokens_zero_default_with_colon_zero ( ) {
// make_row defaults tokens to 0; tokens:0 should match.
let f = Filter ::parse ( "tokens:0" ) . unwrap ( ) ;
let row = make_row (
let row = make_row (
"model" ,
"model" ,
"/proj" ,
"/proj" ,
@ -993,6 +1089,6 @@ mod tests {
0 ,
0 ,
"2026-03-30 10:00:00" ,
"2026-03-30 10:00:00" ,
) ;
) ;
assert! ( ! f . matches ( & row ) ) ;
assert! ( f . matches ( & row ) ) ;
}
}
}
}