Add filtered resume picker snapshots

This commit is contained in:
Josh McKinney
2026-01-04 13:01:23 -08:00
parent 279283fe02
commit e775b31d37
6 changed files with 476 additions and 23 deletions

View File

@@ -78,11 +78,8 @@ pub async fn run_resume_picker(
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
let default_provider = default_provider.to_string();
let filter_cwd = if show_all {
None
} else {
std::env::current_dir().ok()
};
let current_dir = std::env::current_dir().ok();
let filter_cwd = if show_all { None } else { current_dir.clone() };
let loader_tx = bg_tx.clone();
let page_loader: PageLoader = Arc::new(move |request: PageLoadRequest| {
@@ -113,6 +110,7 @@ pub async fn run_resume_picker(
default_provider.clone(),
show_all,
filter_cwd,
current_dir,
);
state.start_initial_load();
state.request_frame();
@@ -134,7 +132,7 @@ pub async fn run_resume_picker(
}
TuiEvent::Draw => {
if let Ok(size) = alt.tui.terminal.size() {
let list_height = size.height.saturating_sub(4) as usize;
let list_height = size.height.saturating_sub(5) as usize;
state.update_view_rows(list_height);
state.ensure_minimum_rows_for_view(list_height);
}
@@ -190,6 +188,7 @@ struct PickerState {
default_provider: String,
show_all: bool,
filter_cwd: Option<PathBuf>,
display_cwd: Option<PathBuf>,
}
struct PaginationState {
@@ -259,6 +258,7 @@ impl PickerState {
default_provider: String,
show_all: bool,
filter_cwd: Option<PathBuf>,
display_cwd: Option<PathBuf>,
) -> Self {
Self {
codex_home,
@@ -283,6 +283,7 @@ impl PickerState {
default_provider,
show_all,
filter_cwd,
display_cwd,
}
}
@@ -703,16 +704,44 @@ fn preview_from_head(head: &[serde_json::Value]) -> Option<String> {
})
}
fn render_filter_hint_line(state: &PickerState) -> Line<'static> {
let cwd = state
.display_cwd
.as_ref()
.map(|path| display_path_for(path, Path::new("/")))
.unwrap_or_else(|| String::from("unknown"));
if state.show_all {
vec![
"Showing sessions from all directories ".dim(),
"(--all)".cyan(),
" · Current directory: ".dim(),
Span::from(cwd).cyan(),
]
.into()
} else {
vec![
"Filtering to current directory: ".dim(),
Span::from(cwd).cyan(),
" · Use ".dim(),
"--all".cyan(),
" to show sessions from all directories".dim(),
]
.into()
}
}
fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
// Render full-screen overlay
let height = tui.terminal.size()?.height;
tui.draw(height, |frame| {
let area = frame.area();
let [header, search, columns, list, hint] = Layout::vertical([
let [header, search, filter_hint, columns, list, hint] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(4)),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(5)),
Constraint::Length(1),
])
.areas(area);
@@ -731,6 +760,9 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
};
frame.render_widget_ref(Line::from(q), search);
let filter_hint_line = render_filter_hint_line(state);
frame.render_widget_ref(filter_hint_line, filter_hint);
let metrics = calculate_column_metrics(&state.filtered_rows, state.show_all);
// Column headers and list
@@ -1200,6 +1232,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
let now = Utc::now();
@@ -1349,6 +1382,7 @@ mod tests {
String::from("openai"),
true,
None,
Some(PathBuf::from("/tmp/project")),
);
let page = RolloutRecorder::list_conversations(
@@ -1381,11 +1415,12 @@ mod tests {
{
let mut frame = terminal.get_frame();
let area = frame.area();
let [header, search, columns, list, hint] = Layout::vertical([
let [header, search, filter_hint, columns, list, hint] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(4)),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(5)),
Constraint::Length(1),
])
.areas(area);
@@ -1397,6 +1432,9 @@ mod tests {
frame.render_widget_ref(Line::from("Type to search".dim()), search);
let filter_hint_line = render_filter_hint_line(&state);
frame.render_widget_ref(filter_hint_line, filter_hint);
render_column_headers(&mut frame, columns, &metrics);
render_list(&mut frame, list, &state, &metrics);
@@ -1419,6 +1457,172 @@ mod tests {
assert_snapshot!("resume_picker_screen", snapshot);
}
#[tokio::test]
async fn resume_picker_screen_filtered_snapshot() {
use crate::custom_terminal::Terminal;
use crate::test_backend::VT100Backend;
use uuid::Uuid;
// Create real rollout files so the snapshot uses the actual listing pipeline.
let tempdir = tempfile::tempdir().expect("tempdir");
let sessions_root = tempdir.path().join("sessions");
std::fs::create_dir_all(&sessions_root).expect("mkdir sessions root");
let now = Utc::now();
// Helper to write a rollout file with minimal meta + one user message.
let write_rollout = |ts: DateTime<Utc>, cwd: &str, branch: &str, preview: &str| {
let dir = sessions_root
.join(ts.format("%Y").to_string())
.join(ts.format("%m").to_string())
.join(ts.format("%d").to_string());
std::fs::create_dir_all(&dir).expect("mkdir date dirs");
let filename = format!(
"rollout-{}-{}.jsonl",
ts.format("%Y-%m-%dT%H-%M-%S"),
Uuid::new_v4()
);
let path = dir.join(filename);
let meta = serde_json::json!({
"timestamp": ts.to_rfc3339(),
"item": {
"SessionMeta": {
"meta": {
"id": Uuid::new_v4(),
"timestamp": ts.to_rfc3339(),
"cwd": cwd,
"originator": "user",
"cli_version": "0.0.0",
"instructions": null,
"source": "Cli",
"model_provider": "openai",
}
}
}
});
let user = serde_json::json!({
"timestamp": ts.to_rfc3339(),
"item": {
"EventMsg": {
"UserMessage": {
"message": preview,
"images": null
}
}
}
});
let branch_meta = serde_json::json!({
"timestamp": ts.to_rfc3339(),
"item": {
"EventMsg": {
"SessionMeta": {
"meta": {
"git_branch": branch
}
}
}
}
});
std::fs::write(&path, format!("{meta}\n{user}\n{branch_meta}\n"))
.expect("write rollout");
};
write_rollout(
now - Duration::seconds(42),
"/tmp/project",
"feature/resume",
"Fix resume picker timestamps",
);
write_rollout(
now - Duration::minutes(35),
"/tmp/other",
"main",
"Investigate lazy pagination cap",
);
let loader: PageLoader = Arc::new(|_| {});
let mut state = PickerState::new(
PathBuf::from("/tmp"),
FrameRequester::test_dummy(),
loader,
String::from("openai"),
false,
Some(PathBuf::from("/tmp/project")),
Some(PathBuf::from("/tmp/project")),
);
let page = RolloutRecorder::list_conversations(
&state.codex_home,
PAGE_SIZE,
None,
INTERACTIVE_SESSION_SOURCES,
Some(&[String::from("openai")]),
"openai",
)
.await
.expect("list conversations");
let rows = rows_from_items(page.items);
state.all_rows = rows.clone();
state.filtered_rows = rows;
state.view_rows = Some(4);
state.selected = 0;
state.scroll_top = 0;
state.update_view_rows(4);
let metrics = calculate_column_metrics(&state.filtered_rows, state.show_all);
let width: u16 = 80;
let height: u16 = 9;
let backend = VT100Backend::new(width, height);
let mut terminal = Terminal::with_options(backend).expect("terminal");
terminal.set_viewport_area(Rect::new(0, 0, width, height));
{
let mut frame = terminal.get_frame();
let area = frame.area();
let [header, search, filter_hint, columns, list, hint] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(5)),
Constraint::Length(1),
])
.areas(area);
frame.render_widget_ref(
Line::from(vec!["Resume a previous session".bold().cyan()]),
header,
);
frame.render_widget_ref(Line::from("Type to search".dim()), search);
let filter_hint_line = render_filter_hint_line(&state);
frame.render_widget_ref(filter_hint_line, filter_hint);
render_column_headers(&mut frame, columns, &metrics);
render_list(&mut frame, list, &state, &metrics);
let hint_line: Line = vec![
key_hint::plain(KeyCode::Enter).into(),
" to resume ".dim(),
" ".dim(),
key_hint::plain(KeyCode::Esc).into(),
" to start new ".dim(),
" ".dim(),
key_hint::ctrl(KeyCode::Char('c')).into(),
" to quit ".dim(),
]
.into();
frame.render_widget_ref(hint_line, hint);
}
terminal.flush().expect("flush");
let snapshot = terminal.backend().to_string();
assert_snapshot!("resume_picker_screen_filtered", snapshot);
}
#[test]
fn pageless_scrolling_deduplicates_and_keeps_order() {
let loader: PageLoader = Arc::new(|_| {});
@@ -1429,6 +1633,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
state.reset_pagination();
@@ -1497,6 +1702,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
state.reset_pagination();
state.ingest_page(page(
@@ -1528,6 +1734,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
let mut items = Vec::new();
@@ -1572,6 +1779,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
let mut items = Vec::new();
@@ -1616,6 +1824,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
state.reset_pagination();
state.ingest_page(page(

View File

@@ -1,14 +1,14 @@
---
source: tui/src/resume_picker.rs
assertion_line: 1438
assertion_line: 1457
expression: snapshot
---
Resume a previous session
Type to search
Showing sessions from all directories (--all) · Current directory: /tmp/project
Updated Branch CWD Conversation
No sessions yet
enter to resume esc to start new ctrl + c to quit

View File

@@ -0,0 +1,14 @@
---
source: tui/src/resume_picker.rs
assertion_line: 1623
expression: snapshot
---
Resume a previous session
Type to search
Filtering to current directory: /tmp/project · Use --all to show sessions from a
Updated Branch Conversation
No sessions yet
enter to resume esc to start new ctrl + c to quit

View File

@@ -78,11 +78,8 @@ pub async fn run_resume_picker(
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
let default_provider = default_provider.to_string();
let filter_cwd = if show_all {
None
} else {
std::env::current_dir().ok()
};
let current_dir = std::env::current_dir().ok();
let filter_cwd = if show_all { None } else { current_dir.clone() };
let loader_tx = bg_tx.clone();
let page_loader: PageLoader = Arc::new(move |request: PageLoadRequest| {
@@ -113,6 +110,7 @@ pub async fn run_resume_picker(
default_provider.clone(),
show_all,
filter_cwd,
current_dir,
);
state.start_initial_load();
state.request_frame();
@@ -134,7 +132,7 @@ pub async fn run_resume_picker(
}
TuiEvent::Draw => {
if let Ok(size) = alt.tui.terminal.size() {
let list_height = size.height.saturating_sub(4) as usize;
let list_height = size.height.saturating_sub(5) as usize;
state.update_view_rows(list_height);
state.ensure_minimum_rows_for_view(list_height);
}
@@ -190,6 +188,7 @@ struct PickerState {
default_provider: String,
show_all: bool,
filter_cwd: Option<PathBuf>,
display_cwd: Option<PathBuf>,
}
struct PaginationState {
@@ -259,6 +258,7 @@ impl PickerState {
default_provider: String,
show_all: bool,
filter_cwd: Option<PathBuf>,
display_cwd: Option<PathBuf>,
) -> Self {
Self {
codex_home,
@@ -283,6 +283,7 @@ impl PickerState {
default_provider,
show_all,
filter_cwd,
display_cwd,
}
}
@@ -703,16 +704,50 @@ fn preview_from_head(head: &[serde_json::Value]) -> Option<String> {
})
}
fn render_filter_hint_line(state: &PickerState) -> Line<'static> {
let cwd = state
.display_cwd
.as_ref()
.map(|path| {
if path.is_absolute() {
path.display().to_string()
} else {
display_path_for(path, Path::new("/"))
}
})
.unwrap_or_else(|| String::from("unknown"));
if state.show_all {
vec![
"Showing sessions from all directories ".dim(),
"(--all)".cyan(),
" · Current directory: ".dim(),
Span::from(cwd).cyan(),
]
.into()
} else {
vec![
"Filtering to current directory: ".dim(),
Span::from(cwd).cyan(),
" · Use ".dim(),
"--all".cyan(),
" to show sessions from all directories".dim(),
]
.into()
}
}
fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
// Render full-screen overlay
let height = tui.terminal.size()?.height;
tui.draw(height, |frame| {
let area = frame.area();
let [header, search, columns, list, hint] = Layout::vertical([
let [header, search, filter_hint, columns, list, hint] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(4)),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(5)),
Constraint::Length(1),
])
.areas(area);
@@ -731,6 +766,9 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
};
frame.render_widget_ref(Line::from(q), search);
let filter_hint_line = render_filter_hint_line(state);
frame.render_widget_ref(filter_hint_line, filter_hint);
let metrics = calculate_column_metrics(&state.filtered_rows, state.show_all);
// Column headers and list
@@ -1200,6 +1238,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
let now = Utc::now();
@@ -1349,6 +1388,7 @@ mod tests {
String::from("openai"),
true,
None,
Some(PathBuf::from("/tmp/project")),
);
let page = RolloutRecorder::list_conversations(
@@ -1381,11 +1421,12 @@ mod tests {
{
let mut frame = terminal.get_frame();
let area = frame.area();
let [header, search, columns, list, hint] = Layout::vertical([
let [header, search, filter_hint, columns, list, hint] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(4)),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(5)),
Constraint::Length(1),
])
.areas(area);
@@ -1397,6 +1438,9 @@ mod tests {
frame.render_widget_ref(Line::from("Type to search".dim()), search);
let filter_hint_line = render_filter_hint_line(&state);
frame.render_widget_ref(filter_hint_line, filter_hint);
render_column_headers(&mut frame, columns, &metrics);
render_list(&mut frame, list, &state, &metrics);
@@ -1419,6 +1463,172 @@ mod tests {
assert_snapshot!("resume_picker_screen", snapshot);
}
#[tokio::test]
async fn resume_picker_screen_filtered_snapshot() {
use crate::custom_terminal::Terminal;
use crate::test_backend::VT100Backend;
use uuid::Uuid;
// Create real rollout files so the snapshot uses the actual listing pipeline.
let tempdir = tempfile::tempdir().expect("tempdir");
let sessions_root = tempdir.path().join("sessions");
std::fs::create_dir_all(&sessions_root).expect("mkdir sessions root");
let now = Utc::now();
// Helper to write a rollout file with minimal meta + one user message.
let write_rollout = |ts: DateTime<Utc>, cwd: &str, branch: &str, preview: &str| {
let dir = sessions_root
.join(ts.format("%Y").to_string())
.join(ts.format("%m").to_string())
.join(ts.format("%d").to_string());
std::fs::create_dir_all(&dir).expect("mkdir date dirs");
let filename = format!(
"rollout-{}-{}.jsonl",
ts.format("%Y-%m-%dT%H-%M-%S"),
Uuid::new_v4()
);
let path = dir.join(filename);
let meta = serde_json::json!({
"timestamp": ts.to_rfc3339(),
"item": {
"SessionMeta": {
"meta": {
"id": Uuid::new_v4(),
"timestamp": ts.to_rfc3339(),
"cwd": cwd,
"originator": "user",
"cli_version": "0.0.0",
"instructions": null,
"source": "Cli",
"model_provider": "openai",
}
}
}
});
let user = serde_json::json!({
"timestamp": ts.to_rfc3339(),
"item": {
"EventMsg": {
"UserMessage": {
"message": preview,
"images": null
}
}
}
});
let branch_meta = serde_json::json!({
"timestamp": ts.to_rfc3339(),
"item": {
"EventMsg": {
"SessionMeta": {
"meta": {
"git_branch": branch
}
}
}
}
});
std::fs::write(&path, format!("{meta}\n{user}\n{branch_meta}\n"))
.expect("write rollout");
};
write_rollout(
now - Duration::seconds(42),
"/tmp/project",
"feature/resume",
"Fix resume picker timestamps",
);
write_rollout(
now - Duration::minutes(35),
"/tmp/other",
"main",
"Investigate lazy pagination cap",
);
let loader: PageLoader = Arc::new(|_| {});
let mut state = PickerState::new(
PathBuf::from("/tmp"),
FrameRequester::test_dummy(),
loader,
String::from("openai"),
false,
Some(PathBuf::from("/tmp/project")),
Some(PathBuf::from("/tmp/project")),
);
let page = RolloutRecorder::list_conversations(
&state.codex_home,
PAGE_SIZE,
None,
INTERACTIVE_SESSION_SOURCES,
Some(&[String::from("openai")]),
"openai",
)
.await
.expect("list conversations");
let rows = rows_from_items(page.items);
state.all_rows = rows.clone();
state.filtered_rows = rows;
state.view_rows = Some(4);
state.selected = 0;
state.scroll_top = 0;
state.update_view_rows(4);
let metrics = calculate_column_metrics(&state.filtered_rows, state.show_all);
let width: u16 = 80;
let height: u16 = 9;
let backend = VT100Backend::new(width, height);
let mut terminal = Terminal::with_options(backend).expect("terminal");
terminal.set_viewport_area(Rect::new(0, 0, width, height));
{
let mut frame = terminal.get_frame();
let area = frame.area();
let [header, search, filter_hint, columns, list, hint] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(area.height.saturating_sub(5)),
Constraint::Length(1),
])
.areas(area);
frame.render_widget_ref(
Line::from(vec!["Resume a previous session".bold().cyan()]),
header,
);
frame.render_widget_ref(Line::from("Type to search".dim()), search);
let filter_hint_line = render_filter_hint_line(&state);
frame.render_widget_ref(filter_hint_line, filter_hint);
render_column_headers(&mut frame, columns, &metrics);
render_list(&mut frame, list, &state, &metrics);
let hint_line: Line = vec![
key_hint::plain(KeyCode::Enter).into(),
" to resume ".dim(),
" ".dim(),
key_hint::plain(KeyCode::Esc).into(),
" to start new ".dim(),
" ".dim(),
key_hint::ctrl(KeyCode::Char('c')).into(),
" to quit ".dim(),
]
.into();
frame.render_widget_ref(hint_line, hint);
}
terminal.flush().expect("flush");
let snapshot = terminal.backend().to_string();
assert_snapshot!("resume_picker_screen_filtered", snapshot);
}
#[test]
fn pageless_scrolling_deduplicates_and_keeps_order() {
let loader: PageLoader = Arc::new(|_| {});
@@ -1429,6 +1639,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
state.reset_pagination();
@@ -1497,6 +1708,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
state.reset_pagination();
state.ingest_page(page(
@@ -1528,6 +1740,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
let mut items = Vec::new();
@@ -1572,6 +1785,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
let mut items = Vec::new();
@@ -1616,6 +1830,7 @@ mod tests {
String::from("openai"),
true,
None,
None,
);
state.reset_pagination();
state.ingest_page(page(

View File

@@ -1,13 +1,14 @@
---
source: tui2/src/resume_picker.rs
assertion_line: 1463
expression: snapshot
---
Resume a previous session
Type to search
Showing sessions from all directories (--all) · Current directory: /tmp/project
Updated Branch CWD Conversation
No sessions yet
enter to resume esc to start new ctrl + c to quit

View File

@@ -0,0 +1,14 @@
---
source: tui2/src/resume_picker.rs
assertion_line: 1629
expression: snapshot
---
Resume a previous session
Type to search
Filtering to current directory: /tmp/project · Use --all to show sessions from a
Updated Branch Conversation
No sessions yet
enter to resume esc to start new ctrl + c to quit