Skip to content
This repository was archived by the owner on Nov 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2867,6 +2867,7 @@ impl App {
session.model.clone(),
session.cwd.clone(),
session.session_id.clone(),
session.provider.label().to_string(),
];

fields
Expand Down Expand Up @@ -4335,6 +4336,32 @@ mod tests {
assert_eq!(visible, vec![14]);
}

/// Searching by product name must select on the Agent column.
///
/// Every fixture session is Claude, so "claude" matching everything and
/// "codex" matching nothing is the discriminating pair: a provider field
/// missing from the search set would make the first return nothing.
#[test]
fn search_query_matches_the_agent_column() {
let mut app = make_test_app();
let all: usize = app.visible_sessions().len();
assert!(all > 0, "fixture must have sessions to discriminate");

app.search_query = "claude".into();
assert_eq!(
app.visible_sessions().len(),
all,
"every fixture session is Claude, so all must match"
);

app.search_query = "codex".into();
assert_eq!(
app.visible_sessions().len(),
0,
"no fixture session is Codex"
);
}

#[test]
fn normalize_selection_clamps_to_filtered_session_count() {
let mut app = make_test_app();
Expand Down
1 change: 1 addition & 0 deletions src/ui/detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub fn render_detail_panel(frame: &mut Frame, area: Rect, session: &AgentSession

let mut lines = vec![
detail_line("PID", &pid, t),
detail_line("Agent", session.provider.label(), t),
detail_line("Session ID", &session.session_id, t),
detail_line("CWD", &session.cwd, t),
detail_line("Project", &session.project_name, t),
Expand Down
86 changes: 71 additions & 15 deletions src/ui/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,30 @@ use super::status_bar::render_status_bar;
/// ORIGIN is last deliberately: `sort_header_idx` below maps sort columns to
/// header positions by hand, so inserting mid-table would silently mis-point
/// the sort arrow.
const HEADER_NAMES: [&str; 14] = [
"PID", "Name", "Project", "Status", "Context", "Cost", "$/hr", "Elapsed", "Last", "CPU%",
"MEM", "In/Out", "Activity", "Origin",
const HEADER_NAMES: [&str; 15] = [
"PID", "Agent", "Name", "Project", "Status", "Context", "Cost", "$/hr", "Elapsed", "Last",
"CPU%", "MEM", "In/Out", "Activity", "Origin",
];

/// Header position carrying the sort arrow, for a `SORT_COLUMNS` index.
///
/// Hand-maintained, and the reason `HEADER_NAMES` cannot be reordered casually.
/// `sort_arrow_lands_on_the_column_it_names` asserts every arm against the
/// header it claims, so a shifted column fails a test instead of pointing the
/// arrow one column off.
fn sort_header_idx(sort_column: usize) -> usize {
match sort_column {
0 => 4, // Status
1 => 5, // Context
2 => 6, // Cost
3 => 7, // $/hr
4 => 8, // Elapsed
5 => 9, // Last
6 => 2, // Name
_ => usize::MAX,
}
}

pub fn render(frame: &mut Frame, area: Rect, app: &App) {
let t = &app.theme;
let visible_sessions = app.visible_sessions();
Expand Down Expand Up @@ -148,18 +167,7 @@ pub fn render(frame: &mut Frame, area: Rect, app: &App) {
// Build header with sort indicator
let header_names = HEADER_NAMES;

// Map sort_column index to header index:
// 0=Status->3, 1=Context->4, 2=Cost->5, 3=$/hr->6, 4=Elapsed->7, 5=Last->8, 6=Name->1
let sort_header_idx = match app.sort_column {
0 => 3, // Status
1 => 4, // Context
2 => 5, // Cost
3 => 6, // $/hr
4 => 7, // Elapsed
5 => 8, // Last
6 => 1, // Name
_ => usize::MAX,
};
let sort_header_idx = sort_header_idx(app.sort_column);

let sort_arrow = if app.sort_reversed {
'\u{25b2}' // ▲ reversed (arrow points "up" = top is the end)
Expand Down Expand Up @@ -267,6 +275,7 @@ pub fn render(frame: &mut Frame, area: Rect, app: &App) {

let widths = [
Constraint::Length(7), // PID
Constraint::Length(7), // Agent (fits "Claude")
Constraint::Min(14), // Name (flex)
Constraint::Min(10), // Project (flex)
Constraint::Length(14), // Status (wider for * indicator)
Expand Down Expand Up @@ -595,6 +604,7 @@ fn session_row(s: &AgentSession, app: &App) -> Row<'static> {

Row::new(vec![
Cell::from(s.pid.to_string()),
Cell::from(s.provider.label()),
Cell::from(name_text),
Cell::from(project_text),
Cell::from(status_text).style(status_style),
Expand Down Expand Up @@ -666,6 +676,7 @@ fn subagent_row(
};

Row::new(vec![
Cell::from(""),
Cell::from(""),
name_cell,
project_cell,
Expand Down Expand Up @@ -1151,4 +1162,49 @@ mod tests {
"the highlighted row and the session Tab acts on must agree"
);
}

/// The sort arrow must land on the header it names, for every sort column.
///
/// `sort_header_idx` is a hand-written index map, so adding or moving a
/// column silently points the arrow one place off. Asserting against the
/// header's NAME rather than a literal index means the map cannot rot
/// without failing here.
#[test]
fn sort_arrow_lands_on_the_column_it_names() {
for (sort_column, name) in crate::app::SORT_COLUMNS.iter().enumerate() {
let idx = sort_header_idx(sort_column);
assert!(
idx < HEADER_NAMES.len(),
"sort column {sort_column} ({name}) maps outside the header row"
);
assert_eq!(
HEADER_NAMES[idx], *name,
"sort column {sort_column} claims header {idx}, which is {:?}, not {name:?}",
HEADER_NAMES[idx]
);
}
}

/// Every sort column is reachable and none share a header slot.
#[test]
fn no_two_sort_columns_share_a_header() {
let mut seen = std::collections::HashSet::new();
for sort_column in 0..crate::app::SORT_COLUMNS.len() {
assert!(
seen.insert(sort_header_idx(sort_column)),
"sort column {sort_column} reuses a header slot"
);
}
}

#[test]
fn the_agent_column_sits_between_pid_and_name() {
assert_eq!(
HEADER_NAMES.iter().position(|h| *h == "Agent"),
Some(1),
"Agent belongs between PID and Name, got {HEADER_NAMES:?}"
);
assert_eq!(HEADER_NAMES[0], "PID");
assert_eq!(HEADER_NAMES[2], "Name");
}
}
Loading