mirror of
https://github.com/openai/codex.git
synced 2026-11-03 16:23:51 +00:00
Route pet asset downloads through the shared HTTP client (#36008)
## Why Built-in pet downloads used a standalone blocking client, so CDN redirects did not use Codex's configured route handling. ## What changed - Download pet spritesheets asynchronously with a reusable `RouteAwareClientPool`. - Keep cache validation, asset installation, and pet decoding off the async runtime while sharing one asset-and-load path across startup, previews, and selection. - Preserve the download size limit while consuming streamed response chunks, and remove the TUI's direct `reqwest` dependency. ## Testing Add coverage for oversized response chunks, cached built-in assets, pet loads without an existing Tokio runtime, and stale preview and selection completions. GitOrigin-RevId: 725e76f3ddb7e84b7212769892f28dc0add15741
This commit is contained in:
1 parent
ad6fc66b6d
commit
09cf609218
11 files changed
+293
-95
No files matched your search
@@ -85,11 +85,16 @@ impl ChatWidget {
|
||||
&chat_keymap.edit_queued_message,
|
||||
current_terminal_info,
|
||||
);
|
||||
let pet_http_client = codex_http_client::RouteAwareClientPool::new(
|
||||
config.http_client_factory(),
|
||||
codex_http_client::ClientRouteClass::Other,
|
||||
);
|
||||
pets::start_configured_pet_load_if_needed(
|
||||
&config,
|
||||
/*ambient_pet_missing*/ true,
|
||||
frame_requester.clone(),
|
||||
app_event_tx.clone(),
|
||||
pet_http_client.clone(),
|
||||
);
|
||||
let mut widget = Self {
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
@@ -184,6 +189,7 @@ impl ChatWidget {
|
||||
status_state: StatusState::default(),
|
||||
review: ReviewState::default(),
|
||||
active_hook_cell: None,
|
||||
pet_http_client,
|
||||
ambient_pet: None,
|
||||
pet_picker_preview_state: crate::pets::PetPickerPreviewState::default(),
|
||||
pet_picker_preview_pet: None,
|
||||
|
||||
@@ -26,6 +26,7 @@ pub(super) fn start_configured_pet_load_if_needed(
|
||||
ambient_pet_missing: bool,
|
||||
frame_requester: FrameRequester,
|
||||
app_event_tx: AppEventSender,
|
||||
pet_http_client: codex_http_client::RouteAwareClientPool,
|
||||
) {
|
||||
let Some(pet_id) = config.tui_pet.clone() else {
|
||||
return;
|
||||
@@ -36,20 +37,26 @@ pub(super) fn start_configured_pet_load_if_needed(
|
||||
|
||||
let codex_home = config.codex_home.clone();
|
||||
let animations_enabled = config.animations;
|
||||
spawn_pet_load(move || {
|
||||
let result = crate::pets::ensure_builtin_pack_for_pet(&pet_id, &codex_home)
|
||||
.and_then(|()| {
|
||||
crate::pets::AmbientPet::load(
|
||||
Some(&pet_id),
|
||||
&codex_home,
|
||||
frame_requester,
|
||||
animations_enabled,
|
||||
)
|
||||
})
|
||||
let event_pet_id = pet_id.clone();
|
||||
spawn_pet_load(
|
||||
async move {
|
||||
crate::pets::load_pet_with_assets(
|
||||
pet_id,
|
||||
codex_home,
|
||||
frame_requester,
|
||||
animations_enabled,
|
||||
&pet_http_client,
|
||||
)
|
||||
.await
|
||||
.map(Some)
|
||||
.map_err(|err| err.to_string());
|
||||
app_event_tx.send(AppEvent::ConfiguredPetLoaded { pet_id, result });
|
||||
});
|
||||
.map_err(|err| err.to_string())
|
||||
},
|
||||
app_event_tx,
|
||||
move |result| AppEvent::ConfiguredPetLoaded {
|
||||
pet_id: event_pet_id,
|
||||
result,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl ChatWidget {
|
||||
@@ -234,19 +241,22 @@ impl ChatWidget {
|
||||
let codex_home = self.config.codex_home.clone();
|
||||
let frame_requester = self.frame_requester.clone();
|
||||
let tx = self.app_event_tx.clone();
|
||||
spawn_pet_load(move || {
|
||||
let result = crate::pets::ensure_builtin_pack_for_pet(&pet_id, &codex_home)
|
||||
.and_then(|()| {
|
||||
crate::pets::AmbientPet::load(
|
||||
Some(&pet_id),
|
||||
&codex_home,
|
||||
frame_requester,
|
||||
/*animations_enabled*/ false,
|
||||
)
|
||||
})
|
||||
.map_err(|err| err.to_string());
|
||||
tx.send(AppEvent::PetPreviewLoaded { request_id, result });
|
||||
});
|
||||
let pet_http_client = self.pet_http_client.clone();
|
||||
spawn_pet_load(
|
||||
async move {
|
||||
crate::pets::load_pet_with_assets(
|
||||
pet_id,
|
||||
codex_home,
|
||||
frame_requester,
|
||||
/*animations_enabled*/ false,
|
||||
&pet_http_client,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.to_string())
|
||||
},
|
||||
tx,
|
||||
move |result| AppEvent::PetPreviewLoaded { request_id, result },
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn finish_pet_picker_preview_load(
|
||||
@@ -326,10 +336,34 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_pet_load(f: impl FnOnce() + Send + 'static) {
|
||||
fn spawn_pet_load<T>(
|
||||
future: impl std::future::Future<Output = Result<T, String>> + Send + 'static,
|
||||
app_event_tx: AppEventSender,
|
||||
completion_event: impl FnOnce(Result<T, String>) -> AppEvent + Send + 'static,
|
||||
) where
|
||||
T: Send + 'static,
|
||||
{
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
std::mem::drop(handle.spawn_blocking(f));
|
||||
std::mem::drop(handle.spawn(async move {
|
||||
app_event_tx.send(completion_event(future.await));
|
||||
}));
|
||||
} else {
|
||||
let _ = std::thread::spawn(f);
|
||||
let _ = std::thread::spawn(move || {
|
||||
let result = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(runtime) => runtime.block_on(future),
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "failed to start pet load runtime");
|
||||
Err(format!("failed to start pet load runtime: {err}"))
|
||||
}
|
||||
};
|
||||
app_event_tx.send(completion_event(result));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pets_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,86 @@
|
||||
use super::*;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn pet_load_without_runtime_sends_completion_event() {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let app_event_tx = AppEventSender::new(tx);
|
||||
|
||||
spawn_pet_load(
|
||||
async { Ok::<Option<crate::pets::AmbientPet>, String>(None) },
|
||||
app_event_tx,
|
||||
|result| AppEvent::ConfiguredPetLoaded {
|
||||
pet_id: crate::pets::DEFAULT_PET_ID.to_string(),
|
||||
result,
|
||||
},
|
||||
);
|
||||
|
||||
match rx.blocking_recv().expect("pet load completion event") {
|
||||
AppEvent::ConfiguredPetLoaded { pet_id, result } => {
|
||||
assert_eq!(pet_id, crate::pets::DEFAULT_PET_ID);
|
||||
assert!(result.expect("successful pet load").is_none());
|
||||
}
|
||||
event => panic!("expected configured pet completion, got {event:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shared_pet_load_uses_cached_builtin_assets() {
|
||||
let (chat, _tx, _rx, _op_rx) =
|
||||
crate::chatwidget::tests::make_chatwidget_manual_with_sender().await;
|
||||
let codex_home = tempfile::tempdir().unwrap();
|
||||
crate::pets::write_test_pack(codex_home.path());
|
||||
|
||||
crate::pets::load_pet_with_assets(
|
||||
crate::pets::DEFAULT_PET_ID.to_string(),
|
||||
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("absolute temporary path"),
|
||||
chat.frame_requester.clone(),
|
||||
/*animations_enabled*/ false,
|
||||
&chat.pet_http_client,
|
||||
)
|
||||
.await
|
||||
.expect("load cached built-in pet");
|
||||
|
||||
assert!(
|
||||
codex_home
|
||||
.path()
|
||||
.join("cache")
|
||||
.join("tui-pets")
|
||||
.join("frame-cache")
|
||||
.join(crate::pets::DEFAULT_PET_ID)
|
||||
.is_dir()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_pet_preview_completion_keeps_current_preview() {
|
||||
let (mut chat, _tx, _rx, _op_rx) =
|
||||
crate::chatwidget::tests::make_chatwidget_manual_with_sender().await;
|
||||
chat.pet_picker_preview_request_id = 2;
|
||||
chat.pet_picker_preview_pet = Some(crate::pets::test_ambient_pet(
|
||||
chat.frame_requester.clone(),
|
||||
/*animations_enabled*/ false,
|
||||
));
|
||||
|
||||
chat.finish_pet_picker_preview_load(/*request_id*/ 1, Err("stale preview".to_string()));
|
||||
|
||||
assert!(chat.pet_picker_preview_pet.is_some());
|
||||
assert_eq!(chat.pet_picker_preview_request_id, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_pet_selection_completion_keeps_current_loading_popup() {
|
||||
let (mut chat, _tx, _rx, _op_rx) =
|
||||
crate::chatwidget::tests::make_chatwidget_manual_with_sender().await;
|
||||
let current_request_id = chat.show_pet_selection_loading_popup();
|
||||
let stale_request_id = current_request_id.wrapping_sub(/*rhs*/ 1);
|
||||
|
||||
assert!(!chat.finish_pet_selection_loading_popup(stale_request_id));
|
||||
assert_eq!(
|
||||
chat.bottom_pane.active_view_id(),
|
||||
Some(crate::chatwidget::PET_SELECTION_LOADING_VIEW_ID)
|
||||
);
|
||||
assert!(chat.finish_pet_selection_loading_popup(current_request_id));
|
||||
assert_eq!(chat.bottom_pane.active_view_id(), None);
|
||||
}
|
||||
Reference in new issue
Block a user