Add sorting/backwardsCursor to thread/list and new thread/turns/list api (#17305)

To improve performance of UI loads from the app, add two main
improvements:
1. The `thread/list` api now gets a `sortDirection` request field and a
`backwardsCursor` to the response, which lets you paginate forwards and
backwards from a window. This lets you fetch the first few items to
display immediately while you paginate to fill in history, then can
paginate "backwards" on future loads to catch up with any changes since
the last UI load without a full reload of the entire data set.
2. Added a new `thread/turns/list` api which also has sortDirection and
backwardsCursor for the same behavior as `thread/list`, allowing you the
same small-fetch for immediate display followed by background fill-in
and resync catchup.
This commit is contained in:
David de Regt
2026-04-17 11:49:02 -07:00
committed by GitHub
Unverified
parent 29bc2ad2f4
commit eaf78e43f2
54 changed files with 3510 additions and 219 deletions
+1
View File
@@ -27,6 +27,7 @@ pub use types::OptionalStringPatch;
pub use types::ReadThreadParams;
pub use types::ResumeThreadRecorderParams;
pub use types::SetThreadNameParams;
pub use types::SortDirection;
pub use types::StoredThread;
pub use types::StoredThreadHistory;
pub use types::ThreadEventPersistenceMode;
@@ -100,6 +100,7 @@ mod tests {
page_size: 10,
cursor: None,
sort_key: ThreadSortKey::CreatedAt,
sort_direction: crate::SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
archived: true,
@@ -5,6 +5,7 @@ use codex_rollout::parse_cursor;
use super::LocalThreadStore;
use super::helpers::stored_thread_from_rollout_item;
use crate::ListThreadsParams;
use crate::SortDirection;
use crate::ThreadPage;
use crate::ThreadSortKey;
use crate::ThreadStoreError;
@@ -27,7 +28,18 @@ pub(super) async fn list_threads(
ThreadSortKey::CreatedAt => codex_rollout::ThreadSortKey::CreatedAt,
ThreadSortKey::UpdatedAt => codex_rollout::ThreadSortKey::UpdatedAt,
};
let page = list_rollout_threads(&store.config, &params, cursor.as_ref(), sort_key).await?;
let sort_direction = match params.sort_direction {
SortDirection::Asc => codex_rollout::SortDirection::Asc,
SortDirection::Desc => codex_rollout::SortDirection::Desc,
};
let page = list_rollout_threads(
&store.config,
&params,
cursor.as_ref(),
sort_key,
sort_direction,
)
.await?;
let next_cursor = page
.next_cursor
@@ -54,6 +66,7 @@ async fn list_rollout_threads(
params: &ListThreadsParams,
cursor: Option<&codex_rollout::Cursor>,
sort_key: codex_rollout::ThreadSortKey,
sort_direction: codex_rollout::SortDirection,
) -> ThreadStoreResult<codex_rollout::ThreadsPage> {
let page = if params.archived {
RolloutRecorder::list_archived_threads(
@@ -61,6 +74,7 @@ async fn list_rollout_threads(
params.page_size,
cursor,
sort_key,
sort_direction,
params.allowed_sources.as_slice(),
params.model_providers.as_deref(),
config.model_provider_id.as_str(),
@@ -73,6 +87,7 @@ async fn list_rollout_threads(
params.page_size,
cursor,
sort_key,
sort_direction,
params.allowed_sources.as_slice(),
params.model_providers.as_deref(),
config.model_provider_id.as_str(),
@@ -122,6 +137,7 @@ mod tests {
page_size: 10,
cursor: None,
sort_key: ThreadSortKey::CreatedAt,
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
archived: false,
@@ -177,6 +193,7 @@ mod tests {
page_size: 10,
cursor: None,
sort_key: ThreadSortKey::CreatedAt,
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
archived: false,
@@ -213,6 +230,7 @@ mod tests {
page_size: 10,
cursor: None,
sort_key: ThreadSortKey::CreatedAt,
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
archived: false,
@@ -225,6 +243,7 @@ mod tests {
page_size: 10,
cursor: None,
sort_key: ThreadSortKey::CreatedAt,
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
archived: true,
@@ -273,6 +292,7 @@ mod tests {
page_size: 10,
cursor: None,
sort_key: ThreadSortKey::CreatedAt,
sort_direction: SortDirection::Desc,
allowed_sources: vec![SessionSource::Cli],
model_providers: Some(vec!["test-provider".to_string()]),
archived: false,
@@ -306,6 +326,7 @@ mod tests {
page_size: 10,
cursor: Some("not-a-cursor".to_string()),
sort_key: ThreadSortKey::CreatedAt,
sort_direction: SortDirection::Desc,
allowed_sources: Vec::new(),
model_providers: None,
archived: false,
@@ -161,6 +161,7 @@ mod tests {
page_size: 2,
cursor: Some("cursor-1".to_string()),
sort_key: ThreadSortKey::UpdatedAt,
sort_direction: crate::SortDirection::Desc,
allowed_sources: vec![SessionSource::Cli],
model_providers: Some(vec!["openai".to_string()]),
archived: true,
+12
View File
@@ -101,6 +101,16 @@ pub enum ThreadSortKey {
UpdatedAt,
}
/// The direction to use when listing stored threads.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum SortDirection {
/// Return older threads before newer threads.
Asc,
/// Return newer threads before older threads.
#[default]
Desc,
}
/// Parameters for listing threads.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListThreadsParams {
@@ -110,6 +120,8 @@ pub struct ListThreadsParams {
pub cursor: Option<String>,
/// Sort order requested by the caller.
pub sort_key: ThreadSortKey,
/// Sort direction requested by the caller.
pub sort_direction: SortDirection,
/// Allowed session sources. Empty means implementation default.
pub allowed_sources: Vec<SessionSource>,
/// Optional model provider filter. `None` means implementation default, while an empty vector