mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why Ctrl+C can take a noticeable amount of time to finish when the TUI is waiting for the app-server thread shutdown path to complete. Before this change, the UI could look like it had not accepted the shutdown request because the composer and cursor remained in their normal interactive state during that wait. This PR makes the accepted shutdown visible immediately. It does not add an artificial sleep or change the shutdown timeout; it only draws one final feedback frame before continuing through the existing shutdown flow. ## What Changed - On `ExitMode::ShutdownFirst`, the TUI now renders shutdown feedback before awaiting the existing thread shutdown future. - The bottom pane disables composer input, which hides the cursor through the existing disabled-input cursor path. - The composer shows `Shutting down...` as the disabled input hint and suppresses footer content so the shutdown acknowledgement is not competing with shortcut/status text. - The logout path uses the same feedback path before shutting down. ## How to Test 1. Start Codex from this branch. 2. Press `Ctrl+C` to request shutdown. 3. If shutdown takes long enough to observe, confirm the composer changes to `› Shutting down...`, the cursor disappears, and no footer hint is rendered below it. 4. Regression check: repeat with text already typed in the composer and confirm the visible row still switches to `Shutting down...` while the draft remains preserved internally until the process exits. Targeted tests: - `cargo test -p codex-tui shutdown_in_progress_disables_input_and_uses_hint_without_footer` - `cargo test -p codex-tui bottom_pane::footer::tests::` ## Local Validation Note `cargo test -p codex-tui` still aborts in `app::tests::discard_side_thread_removes_agent_navigation_entry` with a stack overflow. That same test also failed when run alone locally, and the failure appears unrelated to this shutdown feedback path.
198 lines
7.2 KiB
Rust
198 lines
7.2 KiB
Rust
//! App-level handlers for ambient terminal pet events.
|
|
|
|
use super::*;
|
|
|
|
impl App {
|
|
pub(super) fn disable_ambient_pet_before_shutdown(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
|
self.chat_widget.disable_ambient_pet_for_session();
|
|
if let Err(clear_err) = tui.clear_ambient_pet_image() {
|
|
match clear_err {
|
|
crate::pets::PetImageRenderError::Terminal(err) => return Err(err.into()),
|
|
crate::pets::PetImageRenderError::Asset(err) => {
|
|
tracing::warn!(
|
|
error = %err,
|
|
"failed to clear ambient pet image before shutdown feedback"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn handle_ambient_pet_image_render_error(
|
|
&mut self,
|
|
tui: &mut tui::Tui,
|
|
err: crate::pets::PetImageRenderError,
|
|
) -> Result<()> {
|
|
match err {
|
|
crate::pets::PetImageRenderError::Terminal(err) => Err(err.into()),
|
|
crate::pets::PetImageRenderError::Asset(err) => {
|
|
tracing::warn!(
|
|
error = %err,
|
|
"failed to render ambient pet image; disabling pet for session"
|
|
);
|
|
self.chat_widget.disable_ambient_pet_for_session();
|
|
if let Err(clear_err) = tui.clear_ambient_pet_image() {
|
|
match clear_err {
|
|
crate::pets::PetImageRenderError::Terminal(err) => return Err(err.into()),
|
|
crate::pets::PetImageRenderError::Asset(err) => {
|
|
tracing::warn!(
|
|
error = %err,
|
|
"failed to clear ambient pet image after render failure"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn handle_pet_picker_preview_image_render_error(
|
|
&mut self,
|
|
tui: &mut tui::Tui,
|
|
err: crate::pets::PetImageRenderError,
|
|
) -> Result<()> {
|
|
match err {
|
|
crate::pets::PetImageRenderError::Terminal(err) => Err(err.into()),
|
|
crate::pets::PetImageRenderError::Asset(err) => {
|
|
tracing::warn!(error = %err, "failed to render pet picker preview image");
|
|
self.chat_widget
|
|
.fail_pet_picker_preview_render(err.to_string());
|
|
if let Err(clear_err) = tui.draw_pet_picker_preview_image(/*request*/ None) {
|
|
match clear_err {
|
|
crate::pets::PetImageRenderError::Terminal(err) => return Err(err.into()),
|
|
crate::pets::PetImageRenderError::Asset(err) => {
|
|
tracing::warn!(
|
|
error = %err,
|
|
"failed to clear pet picker preview image after render failure"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn handle_pet_selected(&mut self, tui: &mut tui::Tui, pet_id: String) {
|
|
let request_id = self.chat_widget.show_pet_selection_loading_popup();
|
|
tui.frame_requester().schedule_frame();
|
|
let codex_home = self.config.codex_home.clone();
|
|
let frame_requester = tui.frame_requester();
|
|
let animations_enabled = self.config.animations;
|
|
let tx = self.app_event_tx.clone();
|
|
std::mem::drop(tokio::task::spawn_blocking(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,
|
|
)
|
|
})
|
|
.map(Some)
|
|
.map_err(|err| err.to_string());
|
|
tx.send(AppEvent::PetSelectionLoaded {
|
|
request_id,
|
|
pet_id,
|
|
result,
|
|
});
|
|
}));
|
|
}
|
|
|
|
pub(super) async fn handle_pet_disabled(&mut self, tui: &mut tui::Tui) {
|
|
let edit = crate::legacy_core::config::edit::tui_pet_edit(crate::pets::DISABLED_PET_ID);
|
|
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
|
|
.with_edits([edit])
|
|
.apply()
|
|
.await;
|
|
match apply_result {
|
|
Ok(()) => {
|
|
self.sync_tui_pet_disabled();
|
|
tui.frame_requester().schedule_frame();
|
|
}
|
|
Err(err) => {
|
|
self.chat_widget
|
|
.add_error_message(format!("Failed to disable pets: {err}"));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn handle_pet_preview_loaded(
|
|
&mut self,
|
|
tui: &mut tui::Tui,
|
|
request_id: u64,
|
|
result: Result<crate::pets::AmbientPet, String>,
|
|
) {
|
|
self.chat_widget
|
|
.finish_pet_picker_preview_load(request_id, result);
|
|
tui.frame_requester().schedule_frame();
|
|
}
|
|
|
|
pub(super) async fn handle_pet_selection_loaded(
|
|
&mut self,
|
|
tui: &mut tui::Tui,
|
|
request_id: u64,
|
|
pet_id: String,
|
|
result: Result<Option<crate::pets::AmbientPet>, String>,
|
|
) -> Result<AppRunControl> {
|
|
if !self
|
|
.chat_widget
|
|
.finish_pet_selection_loading_popup(request_id)
|
|
{
|
|
return Ok(AppRunControl::Continue);
|
|
}
|
|
match result {
|
|
Ok(ambient_pet) => {
|
|
let edit = crate::legacy_core::config::edit::tui_pet_edit(&pet_id);
|
|
match ConfigEditsBuilder::new(&self.config.codex_home)
|
|
.with_edits([edit])
|
|
.apply()
|
|
.await
|
|
{
|
|
Ok(()) => {
|
|
self.config.tui_pet = Some(pet_id.clone());
|
|
self.chat_widget
|
|
.set_tui_pet_loaded(Some(pet_id), ambient_pet);
|
|
}
|
|
Err(err) => {
|
|
self.chat_widget
|
|
.add_error_message(format!("Failed to save pet selection: {err}"));
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
self.chat_widget
|
|
.add_error_message(format!("Failed to load pet: {err}"));
|
|
}
|
|
}
|
|
tui.frame_requester().schedule_frame();
|
|
Ok(AppRunControl::Continue)
|
|
}
|
|
|
|
pub(super) fn handle_configured_pet_loaded(
|
|
&mut self,
|
|
tui: &mut tui::Tui,
|
|
pet_id: String,
|
|
result: Result<Option<crate::pets::AmbientPet>, String>,
|
|
) {
|
|
if self.config.tui_pet.as_deref() != Some(pet_id.as_str()) {
|
|
return;
|
|
}
|
|
|
|
match result {
|
|
Ok(ambient_pet) => {
|
|
self.chat_widget
|
|
.set_tui_pet_loaded(Some(pet_id), ambient_pet);
|
|
tui.frame_requester().schedule_frame();
|
|
}
|
|
Err(err) => {
|
|
self.chat_widget
|
|
.add_warning_message(format!("Failed to load configured pet: {err}"));
|
|
}
|
|
}
|
|
}
|
|
}
|