mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Extract resume picker loading
This commit is contained in:
@@ -2,7 +2,6 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::clipboard_paste::normalize_pasted_search_query;
|
||||
@@ -33,10 +32,7 @@ use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::adaptive_wrap_lines;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadListCwdFilter;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -63,7 +59,16 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use tracing::warn;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const PAGE_SIZE: usize = 25;
|
||||
mod loading;
|
||||
|
||||
use loading::BackgroundEvent;
|
||||
use loading::PageCursor;
|
||||
use loading::PageLoadRequest;
|
||||
use loading::PickerLoadRequest;
|
||||
use loading::PickerLoader;
|
||||
use loading::PickerPage;
|
||||
use loading::spawn_app_server_page_loader;
|
||||
|
||||
const LOAD_NEAR_THRESHOLD: usize = 5;
|
||||
const SESSION_META_INDENT_WIDTH: usize = 2;
|
||||
const SESSION_META_DATE_WIDTH: usize = 12;
|
||||
@@ -137,22 +142,6 @@ impl SessionPickerAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PageLoadRequest {
|
||||
cursor: Option<PageCursor>,
|
||||
request_token: usize,
|
||||
search_token: Option<usize>,
|
||||
cwd_filter: Option<PathBuf>,
|
||||
provider_filter: ProviderFilter,
|
||||
sort_key: ThreadSortKey,
|
||||
}
|
||||
|
||||
enum PickerLoadRequest {
|
||||
Page(PageLoadRequest),
|
||||
Preview { thread_id: ThreadId },
|
||||
Transcript { thread_id: ThreadId },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ProviderFilter {
|
||||
Any,
|
||||
@@ -238,35 +227,6 @@ impl From<SessionListDensity> for SessionPickerViewMode {
|
||||
}
|
||||
}
|
||||
|
||||
type PickerLoader = Arc<dyn Fn(PickerLoadRequest) + Send + Sync>;
|
||||
enum BackgroundEvent {
|
||||
Page {
|
||||
request_token: usize,
|
||||
search_token: Option<usize>,
|
||||
page: std::io::Result<PickerPage>,
|
||||
},
|
||||
Preview {
|
||||
thread_id: ThreadId,
|
||||
preview: std::io::Result<Vec<TranscriptPreviewLine>>,
|
||||
},
|
||||
Transcript {
|
||||
thread_id: ThreadId,
|
||||
transcript: std::io::Result<TranscriptCells>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum PageCursor {
|
||||
AppServer(String),
|
||||
}
|
||||
|
||||
struct PickerPage {
|
||||
rows: Vec<Row>,
|
||||
next_cursor: Option<PageCursor>,
|
||||
num_scanned_files: usize,
|
||||
reached_scan_cap: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SessionPickerViewPersistence {
|
||||
codex_home: PathBuf,
|
||||
@@ -547,63 +507,6 @@ fn picker_cwd_filter(
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_app_server_page_loader(
|
||||
app_server: AppServerSession,
|
||||
include_non_interactive: bool,
|
||||
raw_reasoning_visibility: RawReasoningVisibility,
|
||||
bg_tx: mpsc::UnboundedSender<BackgroundEvent>,
|
||||
) -> PickerLoader {
|
||||
let (request_tx, mut request_rx) = mpsc::unbounded_channel::<PickerLoadRequest>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut app_server = app_server;
|
||||
while let Some(request) = request_rx.recv().await {
|
||||
match request {
|
||||
PickerLoadRequest::Page(request) => {
|
||||
let cursor = request.cursor.map(|PageCursor::AppServer(cursor)| cursor);
|
||||
let page = load_app_server_page(
|
||||
&mut app_server,
|
||||
cursor,
|
||||
request.cwd_filter.as_deref(),
|
||||
request.provider_filter,
|
||||
request.sort_key,
|
||||
include_non_interactive,
|
||||
)
|
||||
.await;
|
||||
let _ = bg_tx.send(BackgroundEvent::Page {
|
||||
request_token: request.request_token,
|
||||
search_token: request.search_token,
|
||||
page,
|
||||
});
|
||||
}
|
||||
PickerLoadRequest::Preview { thread_id } => {
|
||||
let preview = load_transcript_preview(&mut app_server, thread_id).await;
|
||||
let _ = bg_tx.send(BackgroundEvent::Preview { thread_id, preview });
|
||||
}
|
||||
PickerLoadRequest::Transcript { thread_id } => {
|
||||
let transcript = load_session_transcript(
|
||||
&mut app_server,
|
||||
thread_id,
|
||||
raw_reasoning_visibility,
|
||||
)
|
||||
.await;
|
||||
let _ = bg_tx.send(BackgroundEvent::Transcript {
|
||||
thread_id,
|
||||
transcript,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(err) = app_server.shutdown().await {
|
||||
warn!(%err, "Failed to shut down app-server picker session");
|
||||
}
|
||||
});
|
||||
|
||||
Arc::new(move |request: PickerLoadRequest| {
|
||||
let _ = request_tx.send(request);
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the human-readable column header for the given sort key.
|
||||
fn sort_key_label(sort_key: ThreadSortKey) -> &'static str {
|
||||
match sort_key {
|
||||
@@ -730,38 +633,6 @@ impl LoadingState {
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_app_server_page(
|
||||
app_server: &mut AppServerSession,
|
||||
cursor: Option<String>,
|
||||
cwd_filter: Option<&Path>,
|
||||
provider_filter: ProviderFilter,
|
||||
sort_key: ThreadSortKey,
|
||||
include_non_interactive: bool,
|
||||
) -> std::io::Result<PickerPage> {
|
||||
let response = app_server
|
||||
.thread_list(thread_list_params(
|
||||
cursor,
|
||||
cwd_filter,
|
||||
provider_filter,
|
||||
sort_key,
|
||||
include_non_interactive,
|
||||
))
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let num_scanned_files = response.data.len();
|
||||
|
||||
Ok(PickerPage {
|
||||
rows: response
|
||||
.data
|
||||
.into_iter()
|
||||
.filter_map(row_from_app_server_thread)
|
||||
.collect(),
|
||||
next_cursor: response.next_cursor.map(PageCursor::AppServer),
|
||||
num_scanned_files,
|
||||
reached_scan_cap: false,
|
||||
})
|
||||
}
|
||||
|
||||
async fn load_transcript_preview(
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
@@ -1279,65 +1150,6 @@ impl PickerState {
|
||||
}));
|
||||
}
|
||||
|
||||
async fn handle_background_event(&mut self, event: BackgroundEvent) -> Result<()> {
|
||||
match event {
|
||||
BackgroundEvent::Page {
|
||||
request_token,
|
||||
search_token,
|
||||
page,
|
||||
} => {
|
||||
let pending = match self.pagination.loading {
|
||||
LoadingState::Pending(pending) => pending,
|
||||
LoadingState::Idle => return Ok(()),
|
||||
};
|
||||
if pending.request_token != request_token {
|
||||
return Ok(());
|
||||
}
|
||||
self.pagination.loading = LoadingState::Idle;
|
||||
let page = page.map_err(color_eyre::Report::from)?;
|
||||
self.ingest_page(page);
|
||||
self.complete_pending_page_down();
|
||||
let completed_token = pending.search_token.or(search_token);
|
||||
self.continue_search_if_token_matches(completed_token);
|
||||
}
|
||||
BackgroundEvent::Preview { thread_id, preview } => {
|
||||
self.transcript_previews.insert(
|
||||
thread_id,
|
||||
match preview {
|
||||
Ok(lines) => TranscriptPreviewState::Loaded(lines),
|
||||
Err(_) => TranscriptPreviewState::Failed,
|
||||
},
|
||||
);
|
||||
self.request_frame();
|
||||
}
|
||||
BackgroundEvent::Transcript {
|
||||
thread_id,
|
||||
transcript,
|
||||
} => match transcript {
|
||||
Ok(cells) => {
|
||||
let should_open = self.pending_transcript_open == Some(thread_id);
|
||||
self.transcript_cells
|
||||
.insert(thread_id, SessionTranscriptState::Loaded(cells.clone()));
|
||||
if should_open {
|
||||
self.open_pending_transcript_if_ready();
|
||||
}
|
||||
self.request_frame();
|
||||
}
|
||||
Err(_) => {
|
||||
self.transcript_cells
|
||||
.insert(thread_id, SessionTranscriptState::Failed);
|
||||
if self.pending_transcript_open == Some(thread_id) {
|
||||
self.pending_transcript_open = None;
|
||||
self.transcript_loading_frame_shown = false;
|
||||
self.inline_error = Some("Could not load transcript preview".to_string());
|
||||
}
|
||||
self.request_frame();
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_pagination(&mut self) {
|
||||
self.pagination.next_cursor = None;
|
||||
self.pagination.num_scanned_files = 0;
|
||||
@@ -1776,59 +1588,6 @@ impl PickerState {
|
||||
}
|
||||
}
|
||||
|
||||
fn row_from_app_server_thread(thread: Thread) -> Option<Row> {
|
||||
let thread_id = match ThreadId::from_string(&thread.id) {
|
||||
Ok(thread_id) => thread_id,
|
||||
Err(err) => {
|
||||
warn!(thread_id = thread.id, %err, "Skipping app-server picker row with invalid id");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let preview = thread.preview.trim();
|
||||
Some(Row {
|
||||
path: thread.path,
|
||||
preview: if preview.is_empty() {
|
||||
String::from("(no message yet)")
|
||||
} else {
|
||||
preview.to_string()
|
||||
},
|
||||
thread_id: Some(thread_id),
|
||||
thread_name: thread.name,
|
||||
created_at: chrono::DateTime::from_timestamp(thread.created_at, 0)
|
||||
.map(|dt| dt.with_timezone(&Utc)),
|
||||
updated_at: chrono::DateTime::from_timestamp(thread.updated_at, 0)
|
||||
.map(|dt| dt.with_timezone(&Utc)),
|
||||
cwd: Some(thread.cwd.to_path_buf()),
|
||||
git_branch: thread.git_info.and_then(|git_info| git_info.branch),
|
||||
})
|
||||
}
|
||||
|
||||
fn thread_list_params(
|
||||
cursor: Option<String>,
|
||||
cwd_filter: Option<&Path>,
|
||||
provider_filter: ProviderFilter,
|
||||
sort_key: ThreadSortKey,
|
||||
include_non_interactive: bool,
|
||||
) -> ThreadListParams {
|
||||
ThreadListParams {
|
||||
cursor,
|
||||
limit: Some(PAGE_SIZE as u32),
|
||||
sort_key: Some(sort_key),
|
||||
sort_direction: None,
|
||||
model_providers: match provider_filter {
|
||||
ProviderFilter::Any => None,
|
||||
ProviderFilter::MatchDefault(default_provider) => Some(vec![default_provider]),
|
||||
},
|
||||
source_kinds: Some(crate::resume_source_kinds(include_non_interactive)),
|
||||
archived: Some(false),
|
||||
parent_thread_id: None,
|
||||
ancestor_thread_id: None,
|
||||
cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().into_owned())),
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn paths_match(a: &Path, b: &Path) -> bool {
|
||||
path_utils::paths_match_after_normalization(a, b)
|
||||
}
|
||||
@@ -3198,7 +2957,7 @@ fn render_empty_state_line(state: &PickerState) -> Line<'static> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Duration;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
@@ -3299,28 +3058,6 @@ mod tests {
|
||||
assert_eq!(row.display_preview(), "My session");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_picker_thread_list_params_include_cwd_filter() {
|
||||
let cwd_filter = picker_cwd_filter(
|
||||
Path::new("/tmp/project"),
|
||||
/*show_all*/ false,
|
||||
/*uses_remote_workspace*/ false,
|
||||
/*remote_cwd_override*/ None,
|
||||
);
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
cwd_filter.as_deref(),
|
||||
ProviderFilter::MatchDefault(String::from("openai")),
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
params.cwd,
|
||||
Some(ThreadListCwdFilter::One(String::from("/tmp/project")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_search_matches_metadata_fields() {
|
||||
let thread_id =
|
||||
@@ -3530,44 +3267,6 @@ mod tests {
|
||||
assert!(first_index < second_index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_thread_list_params_omit_provider_filter() {
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
Some(Path::new("repo/on/server")),
|
||||
ProviderFilter::Any,
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(params.cursor, Some(String::from("cursor-1")));
|
||||
assert_eq!(params.model_providers, None);
|
||||
assert_eq!(
|
||||
params.source_kinds,
|
||||
Some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode])
|
||||
);
|
||||
assert_eq!(
|
||||
params.cwd,
|
||||
Some(ThreadListCwdFilter::One(String::from("repo/on/server")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_thread_list_params_can_include_non_interactive_sources() {
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
/*cwd_filter*/ None,
|
||||
ProviderFilter::Any,
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(params.cursor, Some(String::from("cursor-1")));
|
||||
assert_eq!(params.model_providers, None);
|
||||
let source_kinds = crate::resume_source_kinds(/*include_non_interactive*/ true);
|
||||
assert_eq!(params.source_kinds, Some(source_kinds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_picker_sends_cwd_filter_without_local_post_filtering() {
|
||||
let recorded_requests: Arc<Mutex<Vec<PageLoadRequest>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -5716,42 +5415,6 @@ session_picker_view = "dense"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_row_keeps_pathless_threads() {
|
||||
let thread_id = ThreadId::new();
|
||||
let thread = Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::from("remote thread"),
|
||||
ephemeral: false,
|
||||
history_mode: Default::default(),
|
||||
model_provider: String::from("openai"),
|
||||
created_at: 1,
|
||||
updated_at: 2,
|
||||
recency_at: Some(2),
|
||||
status: codex_app_server_protocol::ThreadStatus::Idle,
|
||||
path: None,
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cli_version: String::from("0.0.0"),
|
||||
source: codex_app_server_protocol::SessionSource::Cli,
|
||||
thread_source: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
git_info: None,
|
||||
name: Some(String::from("Named thread")),
|
||||
turns: Vec::new(),
|
||||
};
|
||||
|
||||
let row = row_from_app_server_thread(thread).expect("row should be preserved");
|
||||
|
||||
assert_eq!(row.path, None);
|
||||
assert_eq!(row.thread_id, Some(thread_id));
|
||||
assert_eq!(row.thread_name, Some(String::from("Named thread")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_to_transcript_cells_renders_core_message_types() {
|
||||
use crate::thread_transcript::thread_to_transcript_cells;
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
//! Session picker loading and background event handling.
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadListCwdFilter;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_protocol::ThreadId;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::warn;
|
||||
|
||||
use super::AppServerSession;
|
||||
use super::LoadingState;
|
||||
use super::PickerState;
|
||||
use super::ProviderFilter;
|
||||
use super::RawReasoningVisibility;
|
||||
use super::Row;
|
||||
use super::SessionTranscriptState;
|
||||
use super::TranscriptCells;
|
||||
use super::TranscriptPreviewLine;
|
||||
use super::TranscriptPreviewState;
|
||||
use super::load_session_transcript;
|
||||
use super::load_transcript_preview;
|
||||
|
||||
const PAGE_SIZE: usize = 25;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct PageLoadRequest {
|
||||
pub(super) cursor: Option<PageCursor>,
|
||||
pub(super) request_token: usize,
|
||||
pub(super) search_token: Option<usize>,
|
||||
pub(super) cwd_filter: Option<PathBuf>,
|
||||
pub(super) provider_filter: ProviderFilter,
|
||||
pub(super) sort_key: ThreadSortKey,
|
||||
}
|
||||
|
||||
pub(super) enum PickerLoadRequest {
|
||||
Page(PageLoadRequest),
|
||||
Preview { thread_id: ThreadId },
|
||||
Transcript { thread_id: ThreadId },
|
||||
}
|
||||
|
||||
pub(super) type PickerLoader = Arc<dyn Fn(PickerLoadRequest) + Send + Sync>;
|
||||
|
||||
pub(super) enum BackgroundEvent {
|
||||
Page {
|
||||
request_token: usize,
|
||||
search_token: Option<usize>,
|
||||
page: io::Result<PickerPage>,
|
||||
},
|
||||
Preview {
|
||||
thread_id: ThreadId,
|
||||
preview: io::Result<Vec<TranscriptPreviewLine>>,
|
||||
},
|
||||
Transcript {
|
||||
thread_id: ThreadId,
|
||||
transcript: io::Result<TranscriptCells>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) enum PageCursor {
|
||||
AppServer(String),
|
||||
}
|
||||
|
||||
pub(super) struct PickerPage {
|
||||
pub(super) rows: Vec<Row>,
|
||||
pub(super) next_cursor: Option<PageCursor>,
|
||||
pub(super) num_scanned_files: usize,
|
||||
pub(super) reached_scan_cap: bool,
|
||||
}
|
||||
|
||||
pub(super) fn spawn_app_server_page_loader(
|
||||
app_server: AppServerSession,
|
||||
include_non_interactive: bool,
|
||||
raw_reasoning_visibility: RawReasoningVisibility,
|
||||
bg_tx: mpsc::UnboundedSender<BackgroundEvent>,
|
||||
) -> PickerLoader {
|
||||
let (request_tx, mut request_rx) = mpsc::unbounded_channel::<PickerLoadRequest>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut app_server = app_server;
|
||||
while let Some(request) = request_rx.recv().await {
|
||||
match request {
|
||||
PickerLoadRequest::Page(request) => {
|
||||
let cursor = request.cursor.map(|PageCursor::AppServer(cursor)| cursor);
|
||||
let page = load_app_server_page(
|
||||
&mut app_server,
|
||||
cursor,
|
||||
request.cwd_filter.as_deref(),
|
||||
request.provider_filter,
|
||||
request.sort_key,
|
||||
include_non_interactive,
|
||||
)
|
||||
.await;
|
||||
let _ = bg_tx.send(BackgroundEvent::Page {
|
||||
request_token: request.request_token,
|
||||
search_token: request.search_token,
|
||||
page,
|
||||
});
|
||||
}
|
||||
PickerLoadRequest::Preview { thread_id } => {
|
||||
let preview = load_transcript_preview(&mut app_server, thread_id).await;
|
||||
let _ = bg_tx.send(BackgroundEvent::Preview { thread_id, preview });
|
||||
}
|
||||
PickerLoadRequest::Transcript { thread_id } => {
|
||||
let transcript = load_session_transcript(
|
||||
&mut app_server,
|
||||
thread_id,
|
||||
raw_reasoning_visibility,
|
||||
)
|
||||
.await;
|
||||
let _ = bg_tx.send(BackgroundEvent::Transcript {
|
||||
thread_id,
|
||||
transcript,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(err) = app_server.shutdown().await {
|
||||
warn!(%err, "Failed to shut down app-server picker session");
|
||||
}
|
||||
});
|
||||
|
||||
Arc::new(move |request: PickerLoadRequest| {
|
||||
let _ = request_tx.send(request);
|
||||
})
|
||||
}
|
||||
|
||||
async fn load_app_server_page(
|
||||
app_server: &mut AppServerSession,
|
||||
cursor: Option<String>,
|
||||
cwd_filter: Option<&Path>,
|
||||
provider_filter: ProviderFilter,
|
||||
sort_key: ThreadSortKey,
|
||||
include_non_interactive: bool,
|
||||
) -> io::Result<PickerPage> {
|
||||
let response = app_server
|
||||
.thread_list(thread_list_params(
|
||||
cursor,
|
||||
cwd_filter,
|
||||
provider_filter,
|
||||
sort_key,
|
||||
include_non_interactive,
|
||||
))
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
let num_scanned_files = response.data.len();
|
||||
|
||||
Ok(PickerPage {
|
||||
rows: response
|
||||
.data
|
||||
.into_iter()
|
||||
.filter_map(row_from_app_server_thread)
|
||||
.collect(),
|
||||
next_cursor: response.next_cursor.map(PageCursor::AppServer),
|
||||
num_scanned_files,
|
||||
reached_scan_cap: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn row_from_app_server_thread(thread: Thread) -> Option<Row> {
|
||||
let thread_id = match ThreadId::from_string(&thread.id) {
|
||||
Ok(thread_id) => thread_id,
|
||||
Err(err) => {
|
||||
warn!(thread_id = thread.id, %err, "Skipping app-server picker row with invalid id");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let preview = thread.preview.trim();
|
||||
Some(Row {
|
||||
path: thread.path,
|
||||
preview: if preview.is_empty() {
|
||||
String::from("(no message yet)")
|
||||
} else {
|
||||
preview.to_string()
|
||||
},
|
||||
thread_id: Some(thread_id),
|
||||
thread_name: thread.name,
|
||||
created_at: chrono::DateTime::from_timestamp(thread.created_at, 0)
|
||||
.map(|dt| dt.with_timezone(&Utc)),
|
||||
updated_at: chrono::DateTime::from_timestamp(thread.updated_at, 0)
|
||||
.map(|dt| dt.with_timezone(&Utc)),
|
||||
cwd: Some(thread.cwd.to_path_buf()),
|
||||
git_branch: thread.git_info.and_then(|git_info| git_info.branch),
|
||||
})
|
||||
}
|
||||
|
||||
fn thread_list_params(
|
||||
cursor: Option<String>,
|
||||
cwd_filter: Option<&Path>,
|
||||
provider_filter: ProviderFilter,
|
||||
sort_key: ThreadSortKey,
|
||||
include_non_interactive: bool,
|
||||
) -> ThreadListParams {
|
||||
ThreadListParams {
|
||||
cursor,
|
||||
limit: Some(PAGE_SIZE as u32),
|
||||
sort_key: Some(sort_key),
|
||||
sort_direction: None,
|
||||
model_providers: match provider_filter {
|
||||
ProviderFilter::Any => None,
|
||||
ProviderFilter::MatchDefault(default_provider) => Some(vec![default_provider]),
|
||||
},
|
||||
source_kinds: Some(crate::resume_source_kinds(include_non_interactive)),
|
||||
archived: Some(false),
|
||||
parent_thread_id: None,
|
||||
ancestor_thread_id: None,
|
||||
cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().into_owned())),
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerState {
|
||||
pub(super) async fn handle_background_event(
|
||||
&mut self,
|
||||
event: BackgroundEvent,
|
||||
) -> color_eyre::eyre::Result<()> {
|
||||
match event {
|
||||
BackgroundEvent::Page {
|
||||
request_token,
|
||||
search_token,
|
||||
page,
|
||||
} => {
|
||||
let pending = match self.pagination.loading {
|
||||
LoadingState::Pending(pending) => pending,
|
||||
LoadingState::Idle => return Ok(()),
|
||||
};
|
||||
if pending.request_token != request_token {
|
||||
return Ok(());
|
||||
}
|
||||
self.pagination.loading = LoadingState::Idle;
|
||||
let page = page.map_err(color_eyre::Report::from)?;
|
||||
self.ingest_page(page);
|
||||
self.complete_pending_page_down();
|
||||
let completed_token = pending.search_token.or(search_token);
|
||||
self.continue_search_if_token_matches(completed_token);
|
||||
}
|
||||
BackgroundEvent::Preview { thread_id, preview } => {
|
||||
self.transcript_previews.insert(
|
||||
thread_id,
|
||||
match preview {
|
||||
Ok(lines) => TranscriptPreviewState::Loaded(lines),
|
||||
Err(_) => TranscriptPreviewState::Failed,
|
||||
},
|
||||
);
|
||||
self.request_frame();
|
||||
}
|
||||
BackgroundEvent::Transcript {
|
||||
thread_id,
|
||||
transcript,
|
||||
} => match transcript {
|
||||
Ok(cells) => {
|
||||
let should_open = self.pending_transcript_open == Some(thread_id);
|
||||
self.transcript_cells
|
||||
.insert(thread_id, SessionTranscriptState::Loaded(cells.clone()));
|
||||
if should_open {
|
||||
self.open_pending_transcript_if_ready();
|
||||
}
|
||||
self.request_frame();
|
||||
}
|
||||
Err(_) => {
|
||||
self.transcript_cells
|
||||
.insert(thread_id, SessionTranscriptState::Failed);
|
||||
if self.pending_transcript_open == Some(thread_id) {
|
||||
self.pending_transcript_open = None;
|
||||
self.transcript_loading_frame_shown = false;
|
||||
self.inline_error = Some("Could not load transcript preview".to_string());
|
||||
}
|
||||
self.request_frame();
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "loading_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,109 @@
|
||||
use std::path::Path;
|
||||
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadListCwdFilter;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::resume_picker::picker_cwd_filter;
|
||||
|
||||
#[test]
|
||||
fn local_picker_thread_list_params_include_cwd_filter() {
|
||||
let cwd_filter = picker_cwd_filter(
|
||||
Path::new("/tmp/project"),
|
||||
/*show_all*/ false,
|
||||
/*uses_remote_workspace*/ false,
|
||||
/*remote_cwd_override*/ None,
|
||||
);
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
cwd_filter.as_deref(),
|
||||
ProviderFilter::MatchDefault(String::from("openai")),
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
params.cwd,
|
||||
Some(ThreadListCwdFilter::One(String::from("/tmp/project")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_thread_list_params_omit_provider_filter() {
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
Some(Path::new("repo/on/server")),
|
||||
ProviderFilter::Any,
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(params.cursor, Some(String::from("cursor-1")));
|
||||
assert_eq!(params.model_providers, None);
|
||||
assert_eq!(
|
||||
params.source_kinds,
|
||||
Some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode])
|
||||
);
|
||||
assert_eq!(
|
||||
params.cwd,
|
||||
Some(ThreadListCwdFilter::One(String::from("repo/on/server")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_thread_list_params_can_include_non_interactive_sources() {
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
/*cwd_filter*/ None,
|
||||
ProviderFilter::Any,
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(params.cursor, Some(String::from("cursor-1")));
|
||||
assert_eq!(params.model_providers, None);
|
||||
let source_kinds = crate::resume_source_kinds(/*include_non_interactive*/ true);
|
||||
assert_eq!(params.source_kinds, Some(source_kinds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_row_keeps_pathless_threads() {
|
||||
let thread_id = ThreadId::new();
|
||||
let thread = Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::from("remote thread"),
|
||||
ephemeral: false,
|
||||
history_mode: Default::default(),
|
||||
model_provider: String::from("openai"),
|
||||
created_at: 1,
|
||||
updated_at: 2,
|
||||
recency_at: Some(2),
|
||||
status: codex_app_server_protocol::ThreadStatus::Idle,
|
||||
path: None,
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cli_version: String::from("0.0.0"),
|
||||
source: codex_app_server_protocol::SessionSource::Cli,
|
||||
thread_source: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
git_info: None,
|
||||
name: Some(String::from("Named thread")),
|
||||
turns: Vec::new(),
|
||||
};
|
||||
|
||||
let row = row_from_app_server_thread(thread).expect("row should be preserved");
|
||||
|
||||
assert_eq!(row.path, None);
|
||||
assert_eq!(row.thread_id, Some(thread_id));
|
||||
assert_eq!(row.thread_name, Some(String::from("Named thread")));
|
||||
}
|
||||
Reference in New Issue
Block a user