mirror of
https://github.com/openai/codex.git
synced 2026-03-03 05:03:20 +00:00
Compare commits
9 Commits
fix/notify
...
pap/thread
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cebf8225cf | ||
|
|
62d1c1f6f5 | ||
|
|
76ed8d516b | ||
|
|
60d906a5a0 | ||
|
|
ad4673e46a | ||
|
|
1ca97fe145 | ||
|
|
a07bb9a907 | ||
|
|
68d8d12578 | ||
|
|
dba5a93873 |
@@ -3168,6 +3168,13 @@ mod handlers {
|
||||
sess.send_event_raw(event).await;
|
||||
return;
|
||||
}
|
||||
if let Some(state_db) = sess.services.state_db.as_ref()
|
||||
&& let Err(err) = state_db
|
||||
.update_thread_name(sess.conversation_id, name.as_str())
|
||||
.await
|
||||
{
|
||||
warn!("Failed to update thread name in state db: {err}");
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = sess.state.lock().await;
|
||||
|
||||
@@ -3,6 +3,7 @@ use codex_core::features::Feature;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
@@ -16,6 +17,7 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
@@ -308,3 +310,67 @@ async fn tool_call_logs_include_thread_id() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn thread_rename_updates_state_db_name() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.features.enable(Feature::Sqlite);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let db_path = codex_state::state_db_path(test.config.codex_home.as_path());
|
||||
for _ in 0..100 {
|
||||
if tokio::fs::try_exists(&db_path).await.unwrap_or(false) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
|
||||
let db = test.codex.state_db().expect("state db enabled");
|
||||
let thread_id = test.session_configured.session_id;
|
||||
let new_name = "renamed thread";
|
||||
|
||||
let mut metadata = None;
|
||||
for _ in 0..100 {
|
||||
metadata = db.get_thread(thread_id).await?;
|
||||
if metadata.is_some() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
let metadata = metadata.expect("thread should exist in state db");
|
||||
let previous_updated_at = metadata.updated_at;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
test.codex
|
||||
.submit(Op::SetThreadName {
|
||||
name: new_name.to_string(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::ThreadNameUpdated(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut metadata = None;
|
||||
for _ in 0..100 {
|
||||
metadata = db.get_thread(thread_id).await?;
|
||||
if metadata
|
||||
.as_ref()
|
||||
.is_some_and(|entry| entry.name == new_name)
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
|
||||
let metadata = metadata.expect("thread should exist in state db");
|
||||
assert_eq!(metadata.name, new_name);
|
||||
assert!(
|
||||
metadata.updated_at > previous_updated_at,
|
||||
"expected updated_at to bump after rename"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ use codex_core::default_client::set_default_client_residency_requirement;
|
||||
use codex_core::default_client::set_default_originator;
|
||||
use codex_core::find_thread_path_by_id_str;
|
||||
use codex_core::find_thread_path_by_name_str;
|
||||
use codex_core::state_db;
|
||||
|
||||
enum InitialOperation {
|
||||
UserTurn {
|
||||
@@ -628,8 +629,20 @@ async fn resolve_resume_path(
|
||||
let path = find_thread_path_by_id_str(&config.codex_home, id_str).await?;
|
||||
Ok(path)
|
||||
} else {
|
||||
let path = find_thread_path_by_name_str(&config.codex_home, id_str).await?;
|
||||
Ok(path)
|
||||
let db_path = if let Some(db) = state_db::get_state_db(config, None).await {
|
||||
db.find_rollout_path_by_name(id_str, Some(false))
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match db_path {
|
||||
Some(path) => Ok(Some(path)),
|
||||
None => find_thread_path_by_name_str(&config.codex_home, id_str)
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
|
||||
1
codex-rs/state/migrations/0005_threads_name.sql
Normal file
1
codex-rs/state/migrations/0005_threads_name.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE threads RENAME COLUMN title TO name;
|
||||
@@ -62,8 +62,8 @@ fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) {
|
||||
}
|
||||
EventMsg::UserMessage(user) => {
|
||||
metadata.has_user_event = true;
|
||||
if metadata.title.is_empty() {
|
||||
metadata.title = strip_user_message_prefix(user.message.as_str()).to_string();
|
||||
if metadata.name.is_empty() {
|
||||
metadata.name = strip_user_message_prefix(user.message.as_str()).to_string();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -71,7 +71,7 @@ fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) {
|
||||
}
|
||||
|
||||
fn apply_response_item(_metadata: &mut ThreadMetadata, _item: &ResponseItem) {
|
||||
// Title and has_user_event are derived from EventMsg::UserMessage only.
|
||||
// Thread name and has_user_event are derived from EventMsg::UserMessage only.
|
||||
}
|
||||
|
||||
fn strip_user_message_prefix(text: &str) -> &str {
|
||||
@@ -108,7 +108,7 @@ mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn response_item_user_messages_do_not_set_title_or_has_user_event() {
|
||||
fn response_item_user_messages_do_not_set_name_or_has_user_event() {
|
||||
let mut metadata = metadata_for_test();
|
||||
let item = RolloutItem::ResponseItem(ResponseItem::Message {
|
||||
id: None,
|
||||
@@ -123,11 +123,11 @@ mod tests {
|
||||
apply_rollout_item(&mut metadata, &item, "test-provider");
|
||||
|
||||
assert_eq!(metadata.has_user_event, false);
|
||||
assert_eq!(metadata.title, "");
|
||||
assert_eq!(metadata.name, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_msg_user_messages_set_title_and_has_user_event() {
|
||||
fn event_msg_user_messages_set_name_and_has_user_event() {
|
||||
let mut metadata = metadata_for_test();
|
||||
let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
message: format!("{USER_MESSAGE_BEGIN} actual user request"),
|
||||
@@ -139,7 +139,7 @@ mod tests {
|
||||
apply_rollout_item(&mut metadata, &item, "test-provider");
|
||||
|
||||
assert_eq!(metadata.has_user_event, true);
|
||||
assert_eq!(metadata.title, "actual user request");
|
||||
assert_eq!(metadata.name, "actual user request");
|
||||
}
|
||||
|
||||
fn metadata_for_test() -> ThreadMetadata {
|
||||
@@ -153,7 +153,7 @@ mod tests {
|
||||
source: "cli".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
title: String::new(),
|
||||
name: String::new(),
|
||||
sandbox_policy: "read-only".to_string(),
|
||||
approval_mode: "on-request".to_string(),
|
||||
tokens_used: 1,
|
||||
@@ -169,11 +169,11 @@ mod tests {
|
||||
fn diff_fields_detects_changes() {
|
||||
let mut base = metadata_for_test();
|
||||
base.id = ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id");
|
||||
base.title = "hello".to_string();
|
||||
base.name = "hello".to_string();
|
||||
let mut other = base.clone();
|
||||
other.tokens_used = 2;
|
||||
other.title = "world".to_string();
|
||||
other.name = "world".to_string();
|
||||
let diffs = base.diff_fields(&other);
|
||||
assert_eq!(diffs, vec!["title", "tokens_used"]);
|
||||
assert_eq!(diffs, vec!["name", "tokens_used"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ pub struct ThreadMetadata {
|
||||
pub model_provider: String,
|
||||
/// The working directory for the thread.
|
||||
pub cwd: PathBuf,
|
||||
/// A best-effort thread title.
|
||||
pub title: String,
|
||||
/// Thread name.
|
||||
pub name: String,
|
||||
/// The sandbox policy (stringified enum).
|
||||
pub sandbox_policy: String,
|
||||
/// The approval mode (stringified enum).
|
||||
@@ -163,7 +163,7 @@ impl ThreadMetadataBuilder {
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_provider.to_string()),
|
||||
cwd: self.cwd.clone(),
|
||||
title: String::new(),
|
||||
name: String::new(),
|
||||
sandbox_policy,
|
||||
approval_mode,
|
||||
tokens_used: 0,
|
||||
@@ -201,8 +201,8 @@ impl ThreadMetadata {
|
||||
if self.cwd != other.cwd {
|
||||
diffs.push("cwd");
|
||||
}
|
||||
if self.title != other.title {
|
||||
diffs.push("title");
|
||||
if self.name != other.name {
|
||||
diffs.push("name");
|
||||
}
|
||||
if self.sandbox_policy != other.sandbox_policy {
|
||||
diffs.push("sandbox_policy");
|
||||
@@ -245,7 +245,7 @@ pub(crate) struct ThreadRow {
|
||||
source: String,
|
||||
model_provider: String,
|
||||
cwd: String,
|
||||
title: String,
|
||||
name: String,
|
||||
sandbox_policy: String,
|
||||
approval_mode: String,
|
||||
tokens_used: i64,
|
||||
@@ -266,7 +266,7 @@ impl ThreadRow {
|
||||
source: row.try_get("source")?,
|
||||
model_provider: row.try_get("model_provider")?,
|
||||
cwd: row.try_get("cwd")?,
|
||||
title: row.try_get("title")?,
|
||||
name: row.try_get("name")?,
|
||||
sandbox_policy: row.try_get("sandbox_policy")?,
|
||||
approval_mode: row.try_get("approval_mode")?,
|
||||
tokens_used: row.try_get("tokens_used")?,
|
||||
@@ -291,7 +291,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
source,
|
||||
model_provider,
|
||||
cwd,
|
||||
title,
|
||||
name,
|
||||
sandbox_policy,
|
||||
approval_mode,
|
||||
tokens_used,
|
||||
@@ -309,7 +309,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
source,
|
||||
model_provider,
|
||||
cwd: PathBuf::from(cwd),
|
||||
title,
|
||||
name,
|
||||
sandbox_policy,
|
||||
approval_mode,
|
||||
tokens_used,
|
||||
|
||||
@@ -101,7 +101,7 @@ SELECT
|
||||
source,
|
||||
model_provider,
|
||||
cwd,
|
||||
title,
|
||||
name,
|
||||
sandbox_policy,
|
||||
approval_mode,
|
||||
tokens_used,
|
||||
@@ -177,6 +177,31 @@ ORDER BY position ASC
|
||||
.map(PathBuf::from))
|
||||
}
|
||||
|
||||
/// Find a rollout path by thread name using the underlying database.
|
||||
pub async fn find_rollout_path_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
archived_only: Option<bool>,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
let mut builder =
|
||||
QueryBuilder::<Sqlite>::new("SELECT rollout_path FROM threads WHERE name = ");
|
||||
builder.push_bind(name);
|
||||
match archived_only {
|
||||
Some(true) => {
|
||||
builder.push(" AND archived = 1");
|
||||
}
|
||||
Some(false) => {
|
||||
builder.push(" AND archived = 0");
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
builder.push(" ORDER BY updated_at DESC, id DESC LIMIT 1");
|
||||
let row = builder.build().fetch_optional(self.pool.as_ref()).await?;
|
||||
Ok(row
|
||||
.and_then(|r| r.try_get::<String, _>("rollout_path").ok())
|
||||
.map(PathBuf::from))
|
||||
}
|
||||
|
||||
/// List threads using the underlying database.
|
||||
pub async fn list_threads(
|
||||
&self,
|
||||
@@ -199,7 +224,7 @@ SELECT
|
||||
source,
|
||||
model_provider,
|
||||
cwd,
|
||||
title,
|
||||
name,
|
||||
sandbox_policy,
|
||||
approval_mode,
|
||||
tokens_used,
|
||||
@@ -353,7 +378,7 @@ INSERT INTO threads (
|
||||
source,
|
||||
model_provider,
|
||||
cwd,
|
||||
title,
|
||||
name,
|
||||
sandbox_policy,
|
||||
approval_mode,
|
||||
tokens_used,
|
||||
@@ -371,7 +396,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
source = excluded.source,
|
||||
model_provider = excluded.model_provider,
|
||||
cwd = excluded.cwd,
|
||||
title = excluded.title,
|
||||
name = excluded.name,
|
||||
sandbox_policy = excluded.sandbox_policy,
|
||||
approval_mode = excluded.approval_mode,
|
||||
tokens_used = excluded.tokens_used,
|
||||
@@ -390,7 +415,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
.bind(metadata.source.as_str())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(metadata.cwd.display().to_string())
|
||||
.bind(metadata.title.as_str())
|
||||
.bind(metadata.name.as_str())
|
||||
.bind(metadata.sandbox_policy.as_str())
|
||||
.bind(metadata.approval_mode.as_str())
|
||||
.bind(metadata.tokens_used)
|
||||
@@ -405,6 +430,18 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the thread name for an existing thread.
|
||||
pub async fn update_thread_name(&self, thread_id: ThreadId, name: &str) -> anyhow::Result<()> {
|
||||
let updated_at = datetime_to_epoch_seconds(Utc::now());
|
||||
sqlx::query("UPDATE threads SET name = ?, updated_at = ? WHERE id = ?")
|
||||
.bind(name)
|
||||
.bind(updated_at)
|
||||
.bind(thread_id.to_string())
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist dynamic tools for a thread if none have been stored yet.
|
||||
///
|
||||
/// Dynamic tools are defined at thread start and should not change afterward.
|
||||
|
||||
@@ -1313,6 +1313,7 @@ impl App {
|
||||
&self.config.codex_home,
|
||||
&self.config.model_provider_id,
|
||||
false,
|
||||
self.config.features.enabled(Feature::Sqlite),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
|
||||
@@ -27,11 +27,13 @@ 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;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::read_session_meta_line;
|
||||
use codex_core::state_db;
|
||||
use codex_core::terminal::Multiplexer;
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_protocol::config_types::AltScreenMode;
|
||||
@@ -407,6 +409,24 @@ pub async fn run_main(
|
||||
.map_err(|err| std::io::Error::other(err.to_string()))
|
||||
}
|
||||
|
||||
async fn find_rollout_path_by_name(
|
||||
config: &Config,
|
||||
name: &str,
|
||||
archived_only: Option<bool>,
|
||||
) -> std::io::Result<Option<PathBuf>> {
|
||||
if let Some(db) = state_db::get_state_db(config, None).await
|
||||
&& let Some(path) = db
|
||||
.find_rollout_path_by_name(name, archived_only)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
return Ok(Some(path));
|
||||
}
|
||||
|
||||
find_thread_path_by_name_str(&config.codex_home, name).await
|
||||
}
|
||||
|
||||
async fn run_ratatui_app(
|
||||
cli: Cli,
|
||||
initial_config: Config,
|
||||
@@ -530,7 +550,7 @@ async fn run_ratatui_app(
|
||||
let path = if is_uuid {
|
||||
find_thread_path_by_id_str(&config.codex_home, id_str).await?
|
||||
} else {
|
||||
find_thread_path_by_name_str(&config.codex_home, id_str).await?
|
||||
find_rollout_path_by_name(&config, id_str, Some(false)).await?
|
||||
};
|
||||
match path {
|
||||
Some(path) => resume_picker::SessionSelection::Fork(path),
|
||||
@@ -562,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?
|
||||
{
|
||||
@@ -586,7 +607,7 @@ async fn run_ratatui_app(
|
||||
let path = if is_uuid {
|
||||
find_thread_path_by_id_str(&config.codex_home, id_str).await?
|
||||
} else {
|
||||
find_thread_path_by_name_str(&config.codex_home, id_str).await?
|
||||
find_rollout_path_by_name(&config, id_str, Some(false)).await?
|
||||
};
|
||||
match path {
|
||||
Some(path) => resume_picker::SessionSelection::Resume(path),
|
||||
@@ -620,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?
|
||||
{
|
||||
|
||||
@@ -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(|_| {});
|
||||
|
||||
Reference in New Issue
Block a user