Add SQLite flag for resume picker

This commit is contained in:
pap
2026-02-04 18:07:48 +00:00
parent 1ca97fe145
commit ad4673e46a
4 changed files with 201 additions and 3 deletions

View File

@@ -1313,6 +1313,7 @@ impl App {
&self.config.codex_home,
&self.config.model_provider_id,
false,
self.config.features.enabled(Feature::Sqlite),
)
.await?
{

View File

@@ -27,6 +27,7 @@ use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::ConfigLoadError;
use codex_core::config_loader::format_config_error_with_source;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::features::Feature;
use codex_core::find_thread_path_by_id_str;
use codex_core::find_thread_path_by_name_str;
use codex_core::path_utils;
@@ -581,6 +582,7 @@ async fn run_ratatui_app(
&config.codex_home,
&config.model_provider_id,
cli.fork_show_all,
config.features.enabled(Feature::Sqlite),
)
.await?
{
@@ -639,6 +641,7 @@ async fn run_ratatui_app(
&config.codex_home,
&config.model_provider_id,
cli.resume_show_all,
config.features.enabled(Feature::Sqlite),
)
.await?
{

View File

@@ -14,6 +14,8 @@ use codex_core::ThreadSortKey;
use codex_core::ThreadsPage;
use codex_core::find_thread_names_by_ids;
use codex_core::path_utils;
use codex_core::state_db;
use codex_core::state_db::StateDbHandle;
use codex_protocol::items::TurnItem;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
@@ -107,6 +109,7 @@ pub async fn run_resume_picker(
codex_home: &Path,
default_provider: &str,
show_all: bool,
sqlite_enabled: bool,
) -> Result<SessionSelection> {
run_session_picker(
tui,
@@ -114,6 +117,7 @@ pub async fn run_resume_picker(
default_provider,
show_all,
SessionPickerAction::Resume,
sqlite_enabled,
)
.await
}
@@ -123,6 +127,7 @@ pub async fn run_fork_picker(
codex_home: &Path,
default_provider: &str,
show_all: bool,
sqlite_enabled: bool,
) -> Result<SessionSelection> {
run_session_picker(
tui,
@@ -130,6 +135,7 @@ pub async fn run_fork_picker(
default_provider,
show_all,
SessionPickerAction::Fork,
sqlite_enabled,
)
.await
}
@@ -140,6 +146,7 @@ async fn run_session_picker(
default_provider: &str,
show_all: bool,
action: SessionPickerAction,
sqlite_enabled: bool,
) -> Result<SessionSelection> {
let alt = AltScreenGuard::enter(tui);
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
@@ -183,6 +190,7 @@ async fn run_session_picker(
filter_cwd,
action,
);
state.configure_name_lookup(sqlite_enabled).await;
state.start_initial_load();
state.request_frame();
@@ -261,6 +269,8 @@ struct PickerState {
filter_cwd: Option<PathBuf>,
action: SessionPickerAction,
thread_name_cache: HashMap<ThreadId, Option<String>>,
state_db: Option<StateDbHandle>,
sqlite_names_enabled: bool,
}
struct PaginationState {
@@ -377,6 +387,21 @@ impl PickerState {
filter_cwd,
action,
thread_name_cache: HashMap::new(),
state_db: None,
sqlite_names_enabled: false,
}
}
async fn configure_name_lookup(&mut self, sqlite_enabled: bool) {
self.sqlite_names_enabled = sqlite_enabled;
if sqlite_enabled {
self.state_db = state_db::open_if_present(
self.codex_home.as_path(),
self.default_provider.as_str(),
)
.await;
} else {
self.state_db = None;
}
}
@@ -550,9 +575,13 @@ impl PickerState {
return;
}
let names = find_thread_names_by_ids(&self.codex_home, &missing_ids)
.await
.unwrap_or_default();
let names = if self.sqlite_names_enabled {
self.thread_names_from_state_db(&missing_ids).await
} else {
find_thread_names_by_ids(&self.codex_home, &missing_ids)
.await
.unwrap_or_default()
};
for thread_id in missing_ids {
let thread_name = names.get(&thread_id).cloned();
self.thread_name_cache.insert(thread_id, thread_name);
@@ -576,6 +605,29 @@ impl PickerState {
}
}
async fn thread_names_from_state_db(
&self,
thread_ids: &HashSet<ThreadId>,
) -> HashMap<ThreadId, String> {
let Some(state_db) = self.state_db.as_deref() else {
return HashMap::new();
};
let mut names = HashMap::with_capacity(thread_ids.len());
for thread_id in thread_ids {
let Ok(metadata) = state_db.get_thread(*thread_id).await else {
continue;
};
let Some(metadata) = metadata else {
continue;
};
let name = metadata.name.trim();
if !name.is_empty() {
names.insert(*thread_id, name.to_string());
}
}
names
}
fn apply_filter(&mut self) {
let base_iter = self
.all_rows
@@ -1678,6 +1730,148 @@ mod tests {
assert_snapshot!("resume_picker_thread_names", snapshot);
}
#[tokio::test]
async fn update_thread_names_uses_state_db_when_sqlite_enabled() {
let tempdir = tempfile::tempdir().expect("tempdir");
let session_index_path = tempdir.path().join("session_index.jsonl");
let thread_id =
ThreadId::from_string("33333333-3333-3333-3333-333333333333").expect("thread id");
let session_index_entry = json!({
"id": thread_id,
"thread_name": "name from session index",
"updated_at": "2025-01-01T00:00:00Z",
});
let mut out = serde_json::to_string(&session_index_entry).expect("session index entry");
out.push('\n');
std::fs::write(&session_index_path, out).expect("write session index");
let state_db = codex_state::StateRuntime::init(
tempdir.path().to_path_buf(),
"openai".to_string(),
None,
)
.await
.expect("init state db");
let created_at = DateTime::<Utc>::from_timestamp(1_735_689_600, 0).expect("timestamp");
let mut metadata = codex_state::ThreadMetadataBuilder::new(
thread_id,
PathBuf::from("/tmp/state-db-session.jsonl"),
created_at,
codex_protocol::protocol::SessionSource::Cli,
)
.build("openai");
metadata.name = "name from sqlite".to_string();
metadata.has_user_event = true;
state_db
.upsert_thread(&metadata)
.await
.expect("upsert thread metadata");
let loader: PageLoader = Arc::new(|_| {});
let mut state = PickerState::new(
tempdir.path().to_path_buf(),
FrameRequester::test_dummy(),
loader,
String::from("openai"),
true,
None,
SessionPickerAction::Resume,
);
state.configure_name_lookup(true).await;
let rows = vec![Row {
path: PathBuf::from("/tmp/a.jsonl"),
preview: String::from("preview"),
thread_id: Some(thread_id),
thread_name: None,
created_at: None,
updated_at: None,
cwd: None,
git_branch: None,
}];
state.all_rows = rows.clone();
state.filtered_rows = rows;
state.update_thread_names().await;
assert_eq!(
state.filtered_rows[0].thread_name.as_deref(),
Some("name from sqlite")
);
}
#[tokio::test]
async fn update_thread_names_falls_back_to_session_index_when_sqlite_disabled() {
let tempdir = tempfile::tempdir().expect("tempdir");
let session_index_path = tempdir.path().join("session_index.jsonl");
let thread_id =
ThreadId::from_string("44444444-4444-4444-4444-444444444444").expect("thread id");
let session_index_entry = json!({
"id": thread_id,
"thread_name": "name from session index",
"updated_at": "2025-01-01T00:00:00Z",
});
let mut out = serde_json::to_string(&session_index_entry).expect("session index entry");
out.push('\n');
std::fs::write(&session_index_path, out).expect("write session index");
let state_db = codex_state::StateRuntime::init(
tempdir.path().to_path_buf(),
"openai".to_string(),
None,
)
.await
.expect("init state db");
let created_at = DateTime::<Utc>::from_timestamp(1_735_689_601, 0).expect("timestamp");
let mut metadata = codex_state::ThreadMetadataBuilder::new(
thread_id,
PathBuf::from("/tmp/state-db-session-disabled.jsonl"),
created_at,
codex_protocol::protocol::SessionSource::Cli,
)
.build("openai");
metadata.name = "name from sqlite".to_string();
metadata.has_user_event = true;
state_db
.upsert_thread(&metadata)
.await
.expect("upsert thread metadata");
let loader: PageLoader = Arc::new(|_| {});
let mut state = PickerState::new(
tempdir.path().to_path_buf(),
FrameRequester::test_dummy(),
loader,
String::from("openai"),
true,
None,
SessionPickerAction::Resume,
);
state.configure_name_lookup(false).await;
let rows = vec![Row {
path: PathBuf::from("/tmp/a.jsonl"),
preview: String::from("preview"),
thread_id: Some(thread_id),
thread_name: None,
created_at: None,
updated_at: None,
cwd: None,
git_branch: None,
}];
state.all_rows = rows.clone();
state.filtered_rows = rows;
state.update_thread_names().await;
assert_eq!(
state.filtered_rows[0].thread_name.as_deref(),
Some("name from session index")
);
}
#[test]
fn pageless_scrolling_deduplicates_and_keeps_order() {
let loader: PageLoader = Arc::new(|_| {});