diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 108f85ff8..dd019946f 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1452,8 +1452,11 @@ impl App { ) .await? { - Some(cwd) => cwd, - None => current_cwd.clone(), + crate::ResolveCwdOutcome::Continue(Some(cwd)) => cwd, + crate::ResolveCwdOutcome::Continue(None) => current_cwd.clone(), + crate::ResolveCwdOutcome::Exit => { + return Ok(AppRunControl::Exit(ExitReason::UserRequested)); + } }; let mut resume_config = if crate::cwds_differ(¤t_cwd, &resume_cwd) { match self.rebuild_config_for_cwd(resume_cwd).await { diff --git a/codex-rs/tui/src/cwd_prompt.rs b/codex-rs/tui/src/cwd_prompt.rs index 2a9c016a1..cb04aa0b4 100644 --- a/codex-rs/tui/src/cwd_prompt.rs +++ b/codex-rs/tui/src/cwd_prompt.rs @@ -51,6 +51,12 @@ pub(crate) enum CwdSelection { Session, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CwdPromptOutcome { + Selection(CwdSelection), + Exit, +} + impl CwdSelection { fn next(self) -> Self { match self { @@ -72,7 +78,7 @@ pub(crate) async fn run_cwd_selection_prompt( action: CwdPromptAction, current_cwd: &Path, session_cwd: &Path, -) -> Result { +) -> Result { let mut screen = CwdPromptScreen::new( tui.frame_requester(), action, @@ -102,7 +108,13 @@ pub(crate) async fn run_cwd_selection_prompt( } } - Ok(screen.selection().unwrap_or(CwdSelection::Session)) + if screen.should_exit { + Ok(CwdPromptOutcome::Exit) + } else { + Ok(CwdPromptOutcome::Selection( + screen.selection().unwrap_or(CwdSelection::Session), + )) + } } struct CwdPromptScreen { @@ -112,6 +124,7 @@ struct CwdPromptScreen { session_cwd: String, highlighted: CwdSelection, selection: Option, + should_exit: bool, } impl CwdPromptScreen { @@ -128,6 +141,7 @@ impl CwdPromptScreen { session_cwd, highlighted: CwdSelection::Session, selection: None, + should_exit: false, } } @@ -138,7 +152,9 @@ impl CwdPromptScreen { if key_event.modifiers.contains(KeyModifiers::CONTROL) && matches!(key_event.code, KeyCode::Char('c') | KeyCode::Char('d')) { - self.select(CwdSelection::Session); + self.selection = None; + self.should_exit = true; + self.request_frame.schedule_frame(); return; } match key_event.code { @@ -166,7 +182,7 @@ impl CwdPromptScreen { } fn is_done(&self) -> bool { - self.selection.is_some() + self.should_exit || self.selection.is_some() } fn selection(&self) -> Option { @@ -283,4 +299,12 @@ mod tests { screen.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_eq!(screen.selection(), Some(CwdSelection::Current)); } + + #[test] + fn cwd_prompt_ctrl_c_exits_instead_of_selecting() { + let mut screen = new_prompt(); + screen.handle_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + assert_eq!(screen.selection(), None); + assert!(screen.is_done()); + } } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index cb4e31157..20c9fc21b 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -44,6 +44,7 @@ use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_oss::ensure_oss_provider_ready; use codex_utils_oss::get_default_model_for_oss_provider; use cwd_prompt::CwdPromptAction; +use cwd_prompt::CwdPromptOutcome; use cwd_prompt::CwdSelection; use std::fs::OpenOptions; use std::path::Path; @@ -672,8 +673,22 @@ async fn run_ratatui_app( }; let fallback_cwd = match action_and_path_if_resume_or_fork { Some((action, path)) => { - resolve_cwd_for_resume_or_fork(&mut tui, ¤t_cwd, path, action, allow_prompt) + match resolve_cwd_for_resume_or_fork(&mut tui, ¤t_cwd, path, action, allow_prompt) .await? + { + ResolveCwdOutcome::Continue(cwd) => cwd, + ResolveCwdOutcome::Exit => { + restore(); + session_log::log_session_end(); + return Ok(AppExitInfo { + token_usage: codex_core::protocol::TokenUsage::default(), + thread_id: None, + thread_name: None, + update_action: None, + exit_reason: ExitReason::UserRequested, + }); + } + } } None => None, }; @@ -780,25 +795,35 @@ pub(crate) fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool { } } +pub(crate) enum ResolveCwdOutcome { + Continue(Option), + Exit, +} + pub(crate) async fn resolve_cwd_for_resume_or_fork( tui: &mut Tui, current_cwd: &Path, path: &Path, action: CwdPromptAction, allow_prompt: bool, -) -> color_eyre::Result> { +) -> color_eyre::Result { let Some(history_cwd) = read_session_cwd(path).await else { - return Ok(None); + return Ok(ResolveCwdOutcome::Continue(None)); }; if allow_prompt && cwds_differ(current_cwd, &history_cwd) { - let selection = + let selection_outcome = cwd_prompt::run_cwd_selection_prompt(tui, action, current_cwd, &history_cwd).await?; - return Ok(Some(match selection { - CwdSelection::Current => current_cwd.to_path_buf(), - CwdSelection::Session => history_cwd, - })); + return Ok(match selection_outcome { + CwdPromptOutcome::Selection(CwdSelection::Current) => { + ResolveCwdOutcome::Continue(Some(current_cwd.to_path_buf())) + } + CwdPromptOutcome::Selection(CwdSelection::Session) => { + ResolveCwdOutcome::Continue(Some(history_cwd)) + } + CwdPromptOutcome::Exit => ResolveCwdOutcome::Exit, + }); } - Ok(Some(history_cwd)) + Ok(ResolveCwdOutcome::Continue(Some(history_cwd))) } #[expect(