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 authored and GitHub committed 2026-04-17 11:49:02 -07:00
1 parent 29bc2ad2f4
commit eaf78e43f2
54 files changed
+3510 -219

No files matched your search

+8 -31
View File
@@ -707,8 +707,7 @@ async fn test_pagination_cursor() {
.join(format!("rollout-2025-03-04T09-00-00-{u4}.jsonl"));
let updated_page1: Vec<Option<String>> =
page1.items.iter().map(|i| i.updated_at.clone()).collect();
let expected_cursor1: Cursor =
serde_json::from_str(&format!("\"2025-03-04T09-00-00|{u4}\"")).unwrap();
let expected_cursor1: Cursor = serde_json::from_str("\"2025-03-04T09-00-00\"").unwrap();
let expected_page1 = ThreadsPage {
items: vec![
ThreadItem {
@@ -775,8 +774,7 @@ async fn test_pagination_cursor() {
.join(format!("rollout-2025-03-02T09-00-00-{u2}.jsonl"));
let updated_page2: Vec<Option<String>> =
page2.items.iter().map(|i| i.updated_at.clone()).collect();
let expected_cursor2: Cursor =
serde_json::from_str(&format!("\"2025-03-02T09-00-00|{u2}\"")).unwrap();
let expected_cursor2: Cursor = serde_json::from_str("\"2025-03-02T09-00-00\"").unwrap();
let expected_page2 = ThreadsPage {
items: vec![
ThreadItem {
@@ -1207,7 +1205,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
}
#[tokio::test]
async fn test_stable_ordering_same_second_pagination() {
async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
let temp = TempDir::new().unwrap();
let home = temp.path();
@@ -1268,7 +1266,7 @@ async fn test_stable_ordering_same_second_pagination() {
.join(format!("rollout-2025-07-01T00-00-00-{u2}.jsonl"));
let updated_page1: Vec<Option<String>> =
page1.items.iter().map(|i| i.updated_at.clone()).collect();
let expected_cursor1: Cursor = serde_json::from_str(&format!("\"{ts}|{u2}\"")).unwrap();
let expected_cursor1: Cursor = serde_json::from_str(&format!("\"{ts}\"")).unwrap();
let expected_page1 = ThreadsPage {
items: vec![
ThreadItem {
@@ -1321,33 +1319,12 @@ async fn test_stable_ordering_same_second_pagination() {
)
.await
.unwrap();
let p1 = home
.join("sessions")
.join("2025")
.join("07")
.join("01")
.join(format!("rollout-2025-07-01T00-00-00-{u1}.jsonl"));
let updated_page2: Vec<Option<String>> =
page2.items.iter().map(|i| i.updated_at.clone()).collect();
// The filesystem fallback only has second-precision timestamps in filenames. The primary
// SQLite-backed listing uses unique millisecond timestamps and does not have this tie.
let expected_page2 = ThreadsPage {
items: vec![ThreadItem {
path: p1,
thread_id: Some(thread_id_from_uuid(u1)),
first_user_message: Some("Hello from user".to_string()),
cwd: Some(Path::new(".").to_path_buf()),
git_branch: None,
git_sha: None,
git_origin_url: None,
source: Some(SessionSource::VSCode),
agent_nickname: None,
agent_role: None,
model_provider: Some(TEST_PROVIDER.to_string()),
cli_version: Some("test_version".to_string()),
created_at: Some(ts.to_string()),
updated_at: updated_page2.first().cloned().flatten(),
}],
items: Vec::new(),
next_cursor: None,
num_scanned_files: 3, // scanned u3, u2 (anchor), u1
num_scanned_files: 3,
reached_scan_cap: false,
};
assert_eq!(page2, expected_page2);