feat(tui): add sortable resume picker with created/updated timestamp toggle (#10752)

## Summary

- Add sorting support to the resume session picker with Tab key toggle
- Sessions can now be sorted by either creation time or last updated
time
- Display the current sort mode in the picker header
- Default to sorting by creation time (most recent first)

## Changes

- Add `sort_key` field to `PickerState` to track current sort order
- Pass sort key to `RolloutRecorder::list_threads()` for proper backend
sorting
- Add Tab key handler to toggle between `CreatedAt` and `UpdatedAt`
sorting
- Show current sort mode ("Created at" / "Updated at") in header
- Add "Tab to toggle sort" keyboard hint
- Intelligently hide secondary date column when terminal is narrow
- Reload session list when sort order changes

## Test plan

- [x] Unit tests for sort key toggle functionality
- [x] Snapshot tests updated for new header format
- [x] Test that Tab key triggers reload with new sort key
- [x] Test column visibility adapts to narrow terminals
This commit is contained in:
Felipe Coury 2026-02-05 14:08:31 -03:00 committed by GitHub
parent b0e5a6305b
commit 22545bf206
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 411 additions and 43 deletions

View file

@ -83,6 +83,7 @@ struct PageLoadRequest {
request_token: usize,
search_token: Option<usize>,
default_provider: String,
sort_key: ThreadSortKey,
}
type PageLoader = Arc<dyn Fn(PageLoadRequest) + Send + Sync>;
@ -96,9 +97,21 @@ enum BackgroundEvent {
}
/// Interactive session picker that lists recorded rollout files with simple
/// search and pagination. Shows the session name when available, otherwise the
/// first user input as the preview, relative time (e.g., "5 seconds ago"), and
/// the absolute path.
/// search and pagination.
///
/// The picker displays sessions in a table with timestamp columns (created/updated),
/// git branch, working directory, and conversation preview. Users can toggle
/// between sorting by creation time and last-updated time using the Tab key.
///
/// Sessions are loaded on-demand via cursor-based pagination. The backend
/// `RolloutRecorder::list_threads` returns pages ordered by the selected sort key,
/// and the picker deduplicates across pages to handle overlapping windows when
/// new sessions appear during pagination.
///
/// Filtering happens in two layers:
/// 1. Provider and source filtering at the backend (only interactive CLI sessions
/// for the current model provider).
/// 2. Working-directory filtering at the picker (unless `--all` is passed).
pub async fn run_resume_picker(
tui: &mut Tui,
codex_home: &Path,
@ -157,7 +170,7 @@ async fn run_session_picker(
&request.codex_home,
PAGE_SIZE,
request.cursor.as_ref(),
ThreadSortKey::CreatedAt,
request.sort_key,
INTERACTIVE_SESSION_SOURCES,
Some(provider_filter.as_slice()),
request.default_provider.as_str(),
@ -220,6 +233,14 @@ async fn run_session_picker(
Ok(SessionSelection::StartFresh)
}
/// Returns the human-readable column header for the given sort key.
fn sort_key_label(sort_key: ThreadSortKey) -> &'static str {
match sort_key {
ThreadSortKey::CreatedAt => "Created at",
ThreadSortKey::UpdatedAt => "Updated at",
}
}
/// RAII guard that ensures we leave the alt-screen on scope exit.
struct AltScreenGuard<'a> {
tui: &'a mut Tui,
@ -257,6 +278,7 @@ struct PickerState {
show_all: bool,
filter_cwd: Option<PathBuf>,
action: SessionPickerAction,
sort_key: ThreadSortKey,
thread_name_cache: HashMap<ThreadId, Option<String>>,
}
@ -373,6 +395,7 @@ impl PickerState {
show_all,
filter_cwd,
action,
sort_key: ThreadSortKey::CreatedAt,
thread_name_cache: HashMap::new(),
}
}
@ -429,6 +452,10 @@ impl PickerState {
self.request_frame();
}
}
KeyCode::Tab => {
self.toggle_sort_key();
self.request_frame();
}
KeyCode::Backspace => {
let mut new_query = self.query.clone();
new_query.pop();
@ -456,13 +483,21 @@ impl PickerState {
self.all_rows.clear();
self.filtered_rows.clear();
self.seen_paths.clear();
self.search_state = SearchState::Idle;
self.selected = 0;
let search_token = if self.query.is_empty() {
self.search_state = SearchState::Idle;
None
} else {
let token = self.allocate_search_token();
self.search_state = SearchState::Active { token };
Some(token)
};
let request_token = self.allocate_request_token();
self.pagination.loading = LoadingState::Pending(PendingLoad {
request_token,
search_token: None,
search_token,
});
self.request_frame();
@ -470,8 +505,9 @@ impl PickerState {
codex_home: self.codex_home.clone(),
cursor: None,
request_token,
search_token: None,
search_token,
default_provider: self.default_provider.clone(),
sort_key: self.sort_key,
});
}
@ -742,6 +778,7 @@ impl PickerState {
request_token,
search_token,
default_provider: self.default_provider.clone(),
sort_key: self.sort_key,
});
}
@ -756,6 +793,19 @@ impl PickerState {
self.next_search_token = self.next_search_token.wrapping_add(1);
token
}
/// Cycles the sort order between creation time and last-updated time.
///
/// Triggers a full reload because the backend must re-sort all sessions.
/// The existing `all_rows` are cleared and pagination restarts from the
/// beginning with the new sort key.
fn toggle_sort_key(&mut self) {
self.sort_key = match self.sort_key {
ThreadSortKey::CreatedAt => ThreadSortKey::UpdatedAt,
ThreadSortKey::UpdatedAt => ThreadSortKey::CreatedAt,
};
self.start_initial_load();
}
}
fn rows_from_items(items: Vec<ThreadItem>) -> Vec<Row> {
@ -821,7 +871,15 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
.areas(area);
// Header
frame.render_widget_ref(Line::from(vec![state.action.title().bold().cyan()]), header);
let header_line: Line = vec![
state.action.title().bold().cyan(),
" ".into(),
"Sort:".dim(),
" ".into(),
sort_key_label(state.sort_key).magenta(),
]
.into();
frame.render_widget_ref(header_line, header);
// Search line
let q = if state.query.is_empty() {
@ -834,7 +892,7 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
let metrics = calculate_column_metrics(&state.filtered_rows, state.show_all);
// Column headers and list
render_column_headers(frame, columns, &metrics);
render_column_headers(frame, columns, &metrics, state.sort_key);
render_list(frame, list, state, &metrics);
// Hint line
@ -849,6 +907,9 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
key_hint::ctrl(KeyCode::Char('c')).into(),
" to quit ".dim(),
" ".dim(),
key_hint::plain(KeyCode::Tab).into(),
" to toggle sort ".dim(),
" ".dim(),
key_hint::plain(KeyCode::Up).into(),
"/".dim(),
key_hint::plain(KeyCode::Down).into(),
@ -882,11 +943,13 @@ fn render_list(
let labels = &metrics.labels;
let mut y = area.y;
let visibility = column_visibility(area.width, metrics, state.sort_key);
let max_created_width = metrics.max_created_width;
let max_updated_width = metrics.max_updated_width;
let max_branch_width = metrics.max_branch_width;
let max_cwd_width = metrics.max_cwd_width;
for (idx, (row, (updated_label, branch_label, cwd_label))) in rows[start..end]
for (idx, (row, (created_label, updated_label, branch_label, cwd_label))) in rows[start..end]
.iter()
.zip(labels[start..end].iter())
.enumerate()
@ -894,12 +957,17 @@ fn render_list(
let is_sel = start + idx == state.selected;
let marker = if is_sel { "> ".bold() } else { " ".into() };
let marker_width = 2usize;
let updated_span = if max_updated_width == 0 {
None
let created_span = if visibility.show_created {
Some(Span::from(format!("{created_label:<max_created_width$}")).dim())
} else {
Some(Span::from(format!("{updated_label:<max_updated_width$}")).dim())
None
};
let branch_span = if max_branch_width == 0 {
let updated_span = if visibility.show_updated {
Some(Span::from(format!("{updated_label:<max_updated_width$}")).dim())
} else {
None
};
let branch_span = if !visibility.show_branch {
None
} else if branch_label.is_empty() {
Some(
@ -913,7 +981,7 @@ fn render_list(
} else {
Some(Span::from(format!("{branch_label:<max_branch_width$}")).cyan())
};
let cwd_span = if max_cwd_width == 0 {
let cwd_span = if !visibility.show_cwd {
None
} else if cwd_label.is_empty() {
Some(
@ -930,21 +998,31 @@ fn render_list(
let mut preview_width = area.width as usize;
preview_width = preview_width.saturating_sub(marker_width);
if max_updated_width > 0 {
if visibility.show_created {
preview_width = preview_width.saturating_sub(max_created_width + 2);
}
if visibility.show_updated {
preview_width = preview_width.saturating_sub(max_updated_width + 2);
}
if max_branch_width > 0 {
if visibility.show_branch {
preview_width = preview_width.saturating_sub(max_branch_width + 2);
}
if max_cwd_width > 0 {
if visibility.show_cwd {
preview_width = preview_width.saturating_sub(max_cwd_width + 2);
}
let add_leading_gap = max_updated_width == 0 && max_branch_width == 0 && max_cwd_width == 0;
let add_leading_gap = !visibility.show_created
&& !visibility.show_updated
&& !visibility.show_branch
&& !visibility.show_cwd;
if add_leading_gap {
preview_width = preview_width.saturating_sub(2);
}
let preview = truncate_text(row.display_preview(), preview_width);
let mut spans: Vec<Span> = vec![marker];
if let Some(created) = created_span {
spans.push(created);
spans.push(" ".into());
}
if let Some(updated) = updated_span {
spans.push(updated);
spans.push(" ".into());
@ -1046,26 +1124,44 @@ fn format_updated_label(row: &Row) -> String {
}
}
fn format_created_label(row: &Row) -> String {
match row.created_at {
Some(created) => human_time_ago(created),
None => "-".to_string(),
}
}
fn render_column_headers(
frame: &mut crate::custom_terminal::Frame,
area: Rect,
metrics: &ColumnMetrics,
sort_key: ThreadSortKey,
) {
if area.height == 0 {
return;
}
let mut spans: Vec<Span> = vec![" ".into()];
if metrics.max_updated_width > 0 {
let visibility = column_visibility(area.width, metrics, sort_key);
if visibility.show_created {
let label = format!(
"{text:<width$}",
text = "Updated",
text = "Created at",
width = metrics.max_created_width
);
spans.push(Span::from(label).bold());
spans.push(" ".into());
}
if visibility.show_updated {
let label = format!(
"{text:<width$}",
text = "Updated at",
width = metrics.max_updated_width
);
spans.push(Span::from(label).bold());
spans.push(" ".into());
}
if metrics.max_branch_width > 0 {
if visibility.show_branch {
let label = format!(
"{text:<width$}",
text = "Branch",
@ -1074,7 +1170,7 @@ fn render_column_headers(
spans.push(Span::from(label).bold());
spans.push(" ".into());
}
if metrics.max_cwd_width > 0 {
if visibility.show_cwd {
let label = format!(
"{text:<width$}",
text = "CWD",
@ -1087,11 +1183,30 @@ fn render_column_headers(
frame.render_widget_ref(Line::from(spans), area);
}
/// Pre-computed column widths and formatted labels for all visible rows.
///
/// Widths are measured in Unicode display width (not byte length) so columns
/// align correctly when labels contain non-ASCII characters.
struct ColumnMetrics {
max_created_width: usize,
max_updated_width: usize,
max_branch_width: usize,
max_cwd_width: usize,
labels: Vec<(String, String, String)>,
/// (created_label, updated_label, branch_label, cwd_label) per row.
labels: Vec<(String, String, String, String)>,
}
/// Determines which columns to render given available terminal width.
///
/// When the terminal is narrow, only one timestamp column is shown (whichever
/// matches the current sort key). Branch and CWD are hidden if their max
/// widths are zero (no data to show).
#[derive(Debug, PartialEq, Eq)]
struct ColumnVisibility {
show_created: bool,
show_updated: bool,
show_branch: bool,
show_cwd: bool,
}
fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
@ -1114,8 +1229,9 @@ fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
format!("{tail}")
}
let mut labels: Vec<(String, String, String)> = Vec::with_capacity(rows.len());
let mut max_updated_width = UnicodeWidthStr::width("Updated");
let mut labels: Vec<(String, String, String, String)> = Vec::with_capacity(rows.len());
let mut max_created_width = UnicodeWidthStr::width("Created at");
let mut max_updated_width = UnicodeWidthStr::width("Updated at");
let mut max_branch_width = UnicodeWidthStr::width("Branch");
let mut max_cwd_width = if include_cwd {
UnicodeWidthStr::width("CWD")
@ -1124,6 +1240,7 @@ fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
};
for row in rows {
let created = format_created_label(row);
let updated = format_updated_label(row);
let branch_raw = row.git_branch.clone().unwrap_or_default();
let branch = right_elide(&branch_raw, 24);
@ -1137,13 +1254,15 @@ fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
} else {
String::new()
};
max_created_width = max_created_width.max(UnicodeWidthStr::width(created.as_str()));
max_updated_width = max_updated_width.max(UnicodeWidthStr::width(updated.as_str()));
max_branch_width = max_branch_width.max(UnicodeWidthStr::width(branch.as_str()));
max_cwd_width = max_cwd_width.max(UnicodeWidthStr::width(cwd.as_str()));
labels.push((updated, branch, cwd));
labels.push((created, updated, branch, cwd));
}
ColumnMetrics {
max_created_width,
max_updated_width,
max_branch_width,
max_cwd_width,
@ -1151,15 +1270,79 @@ fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
}
}
/// Computes which columns fit in the available width.
///
/// The algorithm reserves at least `MIN_PREVIEW_WIDTH` characters for the
/// conversation preview. If both timestamp columns don't fit, only the one
/// matching the current sort key is shown.
fn column_visibility(
area_width: u16,
metrics: &ColumnMetrics,
sort_key: ThreadSortKey,
) -> ColumnVisibility {
const MIN_PREVIEW_WIDTH: usize = 10;
let show_branch = metrics.max_branch_width > 0;
let show_cwd = metrics.max_cwd_width > 0;
// Calculate remaining width after all optional columns.
let mut preview_width = area_width as usize;
preview_width = preview_width.saturating_sub(2); // marker
if metrics.max_created_width > 0 {
preview_width = preview_width.saturating_sub(metrics.max_created_width + 2);
}
if metrics.max_updated_width > 0 {
preview_width = preview_width.saturating_sub(metrics.max_updated_width + 2);
}
if show_branch {
preview_width = preview_width.saturating_sub(metrics.max_branch_width + 2);
}
if show_cwd {
preview_width = preview_width.saturating_sub(metrics.max_cwd_width + 2);
}
// If preview would be too narrow, hide the non-active timestamp column.
let show_both = preview_width >= MIN_PREVIEW_WIDTH;
let show_created = if show_both {
metrics.max_created_width > 0
} else {
sort_key == ThreadSortKey::CreatedAt
};
let show_updated = if show_both {
metrics.max_updated_width > 0
} else {
sort_key == ThreadSortKey::UpdatedAt
};
ColumnVisibility {
show_created,
show_updated,
show_branch,
show_cwd,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
use codex_protocol::ThreadId;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SessionMeta;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::fs::FileTimes;
use std::fs::OpenOptions;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
@ -1200,6 +1383,105 @@ mod tests {
}
}
fn set_rollout_mtime(path: &Path, updated_at: DateTime<Utc>) {
let times = FileTimes::new().set_modified(updated_at.into());
OpenOptions::new()
.append(true)
.open(path)
.expect("open rollout")
.set_times(times)
.expect("set times");
}
#[tokio::test]
async fn resume_picker_orders_by_updated_at() {
use uuid::Uuid;
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();
let write_rollout = |ts: DateTime<Utc>, preview: &str| -> PathBuf {
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 = SessionMeta {
id: ThreadId::new(),
forked_from_id: None,
timestamp: ts.to_rfc3339(),
cwd: PathBuf::from("/tmp"),
originator: String::from("user"),
cli_version: String::from("0.0.0"),
source: SessionSource::Cli,
model_provider: Some(String::from("openai")),
base_instructions: None,
dynamic_tools: None,
};
let meta_line = RolloutLine {
timestamp: ts.to_rfc3339(),
item: RolloutItem::SessionMeta(SessionMetaLine { meta, git: None }),
};
let user_line = RolloutLine {
timestamp: ts.to_rfc3339(),
item: RolloutItem::ResponseItem(ResponseItem::Message {
id: None,
role: String::from("user"),
content: vec![ContentItem::InputText {
text: preview.to_string(),
}],
end_turn: None,
phase: None,
}),
};
let meta_json = serde_json::to_string(&meta_line).expect("serialize meta");
let user_json = serde_json::to_string(&user_line).expect("serialize user");
std::fs::write(&path, format!("{meta_json}\n{user_json}\n")).expect("write rollout");
path
};
let created_a = now - Duration::minutes(1);
let created_b = now - Duration::minutes(2);
let path_a = write_rollout(created_a, "A (created newer)");
let path_b = write_rollout(created_b, "B (created older)");
set_rollout_mtime(&path_a, now - Duration::minutes(10));
set_rollout_mtime(&path_b, now - Duration::seconds(10));
let page = RolloutRecorder::list_threads(
tempdir.path(),
PAGE_SIZE,
None,
ThreadSortKey::UpdatedAt,
INTERACTIVE_SESSION_SOURCES,
Some(&[String::from("openai")]),
"openai",
)
.await
.expect("list threads");
let rows = rows_from_items(page.items);
let previews: Vec<String> = rows.iter().map(|row| row.preview.clone()).collect();
assert_eq!(
previews,
vec![
"B (created older)".to_string(),
"A (created newer)".to_string()
]
);
}
#[test]
fn head_to_row_uses_first_user_message() {
let item = ThreadItem {
@ -1374,7 +1656,7 @@ mod tests {
let area = frame.area();
let segments =
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(area);
render_column_headers(&mut frame, segments[0], &metrics);
render_column_headers(&mut frame, segments[0], &metrics, state.sort_key);
render_list(&mut frame, segments[1], &state, &metrics);
}
terminal.flush().expect("flush");
@ -1517,13 +1799,19 @@ mod tests {
.areas(area);
frame.render_widget_ref(
Line::from(vec!["Resume a previous session".bold().cyan()]),
Line::from(vec![
"Resume a previous session".bold().cyan(),
" ".into(),
"Sort:".dim(),
" ".into(),
"Created at".magenta(),
]),
header,
);
frame.render_widget_ref(Line::from("Type to search".dim()), search);
render_column_headers(&mut frame, columns, &metrics);
render_column_headers(&mut frame, columns, &metrics, state.sort_key);
render_list(&mut frame, list, &state, &metrics);
let hint_line: Line = vec![
@ -1535,6 +1823,9 @@ mod tests {
" ".dim(),
key_hint::ctrl(KeyCode::Char('c')).into(),
" to quit ".dim(),
" ".dim(),
key_hint::plain(KeyCode::Tab).into(),
" to toggle sort ".dim(),
]
.into();
frame.render_widget_ref(hint_line, hint);
@ -1634,7 +1925,7 @@ mod tests {
let area = frame.area();
let segments =
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(area);
render_column_headers(&mut frame, segments[0], &metrics);
render_column_headers(&mut frame, segments[0], &metrics, state.sort_key);
render_list(&mut frame, segments[1], &state, &metrics);
}
terminal.flush().expect("flush");
@ -1744,6 +2035,85 @@ mod tests {
assert!(guard[0].search_token.is_none());
}
#[test]
fn column_visibility_hides_extra_date_column_when_narrow() {
let metrics = ColumnMetrics {
max_created_width: 8,
max_updated_width: 12,
max_branch_width: 0,
max_cwd_width: 0,
labels: Vec::new(),
};
let created = column_visibility(30, &metrics, ThreadSortKey::CreatedAt);
assert_eq!(
created,
ColumnVisibility {
show_created: true,
show_updated: false,
show_branch: false,
show_cwd: false,
}
);
let updated = column_visibility(30, &metrics, ThreadSortKey::UpdatedAt);
assert_eq!(
updated,
ColumnVisibility {
show_created: false,
show_updated: true,
show_branch: false,
show_cwd: false,
}
);
let wide = column_visibility(40, &metrics, ThreadSortKey::CreatedAt);
assert_eq!(
wide,
ColumnVisibility {
show_created: true,
show_updated: true,
show_branch: false,
show_cwd: false,
}
);
}
#[tokio::test]
async fn toggle_sort_key_reloads_with_new_sort() {
let recorded_requests: Arc<Mutex<Vec<PageLoadRequest>>> = Arc::new(Mutex::new(Vec::new()));
let request_sink = recorded_requests.clone();
let loader: PageLoader = Arc::new(move |req: PageLoadRequest| {
request_sink.lock().unwrap().push(req);
});
let mut state = PickerState::new(
PathBuf::from("/tmp"),
FrameRequester::test_dummy(),
loader,
String::from("openai"),
true,
None,
SessionPickerAction::Resume,
);
state.start_initial_load();
{
let guard = recorded_requests.lock().unwrap();
assert_eq!(guard.len(), 1);
assert_eq!(guard[0].sort_key, ThreadSortKey::CreatedAt);
}
state
.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
.await
.unwrap();
let guard = recorded_requests.lock().unwrap();
assert_eq!(guard.len(), 2);
assert_eq!(guard[1].sort_key, ThreadSortKey::UpdatedAt);
}
#[tokio::test]
async fn page_navigation_uses_view_rows() {
let loader: PageLoader = Arc::new(|_| {});

View file

@ -1,14 +1,13 @@
---
source: tui/src/resume_picker.rs
assertion_line: 1438
expression: snapshot
---
Resume a previous session
Resume a previous session Sort: Created at
Type to search
Updated Branch CWD Conversation
Created at Updated at Branch CWD Conversation
No sessions yet
enter to resume esc to start new ctrl + c to quit
enter to resume esc to start new ctrl + c to quit tab to toggle sort

View file

@ -2,7 +2,7 @@
source: tui/src/resume_picker.rs
expression: snapshot
---
Updated Branch CWD Conversation
42 seconds ago - - Fix resume picker timestamps
> 35 minutes ago - - Investigate lazy pagination cap
2 hours ago - - Explain the codebase
Created at Updated at Branch CWD Conversation
16 minutes ago 42 seconds ago - - Fix resume picker timestamps
> 1 hour ago 35 minutes ago - - Investigate lazy pagination cap
2 hours ago 2 hours ago - - Explain the codebase

View file

@ -1,8 +1,7 @@
---
source: tui/src/resume_picker.rs
assertion_line: 1683
expression: snapshot
---
Updated Branch CWD Conversation
> 2 days ago - - Keep this for now
3 days ago - - Named thread
Created at Updated at Branch CWD Conversation
> - 2 days ago - - Keep this for now
- 3 days ago - - Named thread