diff --git a/codex-rs/tui/src/app/agents_overview_actions.rs b/codex-rs/tui/src/app/agents_overview_actions.rs index f6031c8200..11d99d6c0b 100644 --- a/codex-rs/tui/src/app/agents_overview_actions.rs +++ b/codex-rs/tui/src/app/agents_overview_actions.rs @@ -157,7 +157,7 @@ impl App { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } diff --git a/codex-rs/tui/src/app/agents_overview_actions_tests.rs b/codex-rs/tui/src/app/agents_overview_actions_tests.rs index cc0ce9c895..bf1354171a 100644 --- a/codex-rs/tui/src/app/agents_overview_actions_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_actions_tests.rs @@ -26,10 +26,6 @@ async fn archive_confirmation_number_keys_act_immediately() { )), ); app.confirm_agents_overview_action(id, AgentsOverviewAction::Archive); - insta::assert_snapshot!( - "archive_task_confirmation", - render_bottom_popup(&app.chat_widget, /*width*/ 72) - ); app.chat_widget.handle_key_event(KeyCode::Char(key).into()); @@ -684,7 +680,10 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb Box::pin(app.handle_event(&mut tui, &mut app_server, confirmation)).await?; insta::assert_snapshot!( format!("{snapshot}_confirmation"), - render_bottom_popup(&app.chat_widget, /*width*/ 72) + normalize_agent_center_snapshot(render_bottom_popup( + &app.chat_widget, + /*width*/ 80 + )) ); app.chat_widget.handle_key_event(KeyCode::Enter.into()); assert!( diff --git a/codex-rs/tui/src/app/daemon_menu.rs b/codex-rs/tui/src/app/daemon_menu.rs index bf9934dfcf..d5a354a0e9 100644 --- a/codex-rs/tui/src/app/daemon_menu.rs +++ b/codex-rs/tui/src/app/daemon_menu.rs @@ -122,7 +122,7 @@ impl App { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } } diff --git a/codex-rs/tui/src/app/owned_transcript.rs b/codex-rs/tui/src/app/owned_transcript.rs index 1db2c4b80d..fd46ecc012 100644 --- a/codex-rs/tui/src/app/owned_transcript.rs +++ b/codex-rs/tui/src/app/owned_transcript.rs @@ -271,9 +271,23 @@ impl App { feedback_tick = view.render_composer_gap(follow_area, composer_hint.as_ref(), frame.buffer, now); chat_widget.note_rendered_width(screen_size.width); - rendered_cursor = bottom.cursor_pos(bottom_area); + let dialog = chat_widget.centered_dialog(); + let (foreground, foreground_area): (&dyn Renderable, Rect) = + if let Some(dialog) = &dialog { + let area = Rect::new( + /*x*/ 0, + /*y*/ 0, + screen_size.width, + screen_size.height, + ); + dialog.render(area, frame.buffer); + (dialog, area) + } else { + (&bottom, bottom_area) + }; + rendered_cursor = foreground.cursor_pos(foreground_area); if let Some(position) = rendered_cursor { - frame.set_cursor_style(bottom.cursor_style(bottom_area)); + frame.set_cursor_style(foreground.cursor_style(foreground_area)); frame.set_cursor_position(position); } })?; diff --git a/codex-rs/tui/src/app/owned_transcript_tests.rs b/codex-rs/tui/src/app/owned_transcript_tests.rs index 50343b7ec4..ca6fd6e98f 100644 --- a/codex-rs/tui/src/app/owned_transcript_tests.rs +++ b/codex-rs/tui/src/app/owned_transcript_tests.rs @@ -1582,6 +1582,39 @@ async fn fullscreen_composer_mouse_copy_and_input_ownership() -> Result<()> { Ok(()) } +#[tokio::test] +async fn inline_confirmation_preserves_compact_viewport_without_replaying_history() -> Result<()> { + let mut app = crate::app::test_support::make_test_app().await; + attach_thread(&mut app, ThreadId::new()); + app.local_settings.tui.animations = false; + app.chat_widget + .apply_external_edit("preserved draft".to_string()); + app.transcript_cells = vec![user_cell("Completed conversation behind the confirmation")]; + let mut tui = crate::tui::test_support::make_test_tui()?; + tui.set_owned_screen(/*owned*/ false)?; + let size = Size::new(/*width*/ 80, /*height*/ 24); + app.show_replace_thread_goal_confirmation( + ThreadId::new(), + crate::goal_files::GoalDraft { + objective: "A replacement goal".to_string(), + ..Default::default() + }, + ); + app.render_chat_widget_frame(&mut tui, size)?; + assert!(tui.terminal.viewport_area.height < size.height); + app.chat_widget.handle_key_event(KeyCode::Esc.into()); + app.render_chat_widget_frame(&mut tui, size)?; + assert!(tui.terminal.viewport_area.height < size.height); + assert!(app.last_rendered_history_tail.is_none()); + assert!( + buffer_text(crate::custom_terminal::test_support::last_rendered_buffer( + &tui.terminal + )) + .contains("preserved draft") + ); + Ok(()) +} + #[tokio::test] async fn fullscreen_composer_scrolls_without_moving_caret_or_transcript() -> Result<()> { let mut app = crate::app::test_support::make_test_app().await; diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__archive_task_confirmation.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__archive_task_confirmation.snap index 9597a03379..595ac77642 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__archive_task_confirmation.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__archive_task_confirmation.snap @@ -1,13 +1,28 @@ --- source: tui/src/app/agents_overview_actions_tests.rs -expression: "render_bottom_popup(&app.chat_widget, 72)" +expression: "render_bottom_popup(&app.chat_widget, 80)" --- - Archive “Current task”? - This stops any running work in this task and its child agents, then - archives them. Their history can be restored from the resume picker. + Agent command center Group: Project g + All 1 Needs you 0 Working 0 Ready 1 Inactive 0 + ──────────────────────────────────────────────────────────────────────────── + Tasks Status Updated + /tmp/project 1 + › ○ Current task Ready 2m ago -› 1. Cancel - 2. Archive task and child agents + Archive “Current task”? + This stops any running work in this task and its child agents, then + archives them. Their history can be restored from the resume picker. - enter select · esc back + + › 1. Cancel + 2. Archive task and child agents + + enter select · esc back + + + + + + + ? help esc back ↑/↓ move enter open n new diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__delete_task_confirmation.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__delete_task_confirmation.snap index d5a6bb23c8..2b1c063293 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__delete_task_confirmation.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__actions__delete_task_confirmation.snap @@ -1,13 +1,28 @@ --- source: tui/src/app/agents_overview_actions_tests.rs -expression: "render_bottom_popup(&app.chat_widget, 72)" +expression: "render_bottom_popup(&app.chat_widget, 80)" --- - Permanently delete “Current task”? - This stops any running work in this task and its child agents, then - permanently deletes their history. This cannot be undone. + Agent command center Group: Project g + All 1 Needs you 0 Working 0 Ready 1 Inactive 0 + ──────────────────────────────────────────────────────────────────────────── + Tasks Status Updated + /tmp/project 1 + › ○ Current task Ready 2m ago -› 1. Cancel - 2. Permanently delete task and child agents + Permanently delete “Current task”? + This stops any running work in this task and its child agents, then + permanently deletes their history. This cannot be undone. - enter select · esc back + + › 1. Cancel + 2. Permanently delete task and child agents + + enter select · esc back + + + + + + + ? help esc back ↑/↓ move enter open n new diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_cli_confirmation.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_cli_confirmation.snap index 4de9db012c..4a94611994 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_cli_confirmation.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_cli_confirmation.snap @@ -1,15 +1,29 @@ --- source: tui/src/app/daemon_menu_tests.rs +assertion_line: 125 expression: "render_bottom_popup(&app.chat_widget,\nwidth).replace(r\"C:\\cli-build\\bin\\codex\", \"/x/cli-build/bin/codex\")" --- - Update daemon and exit Codex? - Use this CLI package v0.0.0 from /x/cli-build/bin/codex. Copy the complete package and pin it - against automatic updates. - This may restart the daemon and interrupt active or queued work. - Codex exits to update in this terminal. Relaunch it afterward. + Agent command center Group: Project g + All 0 Needs you 0 Working 0 Ready 0 Inactive 0 tab/⇧tab filter + ──────────────────────────────────────────────────────────────────────────────────────────────── + No tasks yet │ + │ + │ + + Update daemon and exit Codex? + Use this CLI package v0.0.0 from /x/cli-build/bin/codex. Copy the + complete package and pin it against automatic updates. + This may restart the daemon and interrupt active or queued work. + Codex exits to update in this terminal. Relaunch it afterward. -› 1. Cancel - 2. Update and exit + › 1. Cancel + 2. Update and exit - enter select · esc back + enter select · esc back + │ + │ + │ + │ + │ + Service v0.153.0 < Codex CLI v0.154.0 · /daemon diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_stable_confirmation.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_stable_confirmation.snap index f49bcbef1c..b7f4e1a152 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_stable_confirmation.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__daemon_menu__tests__daemon_stable_confirmation.snap @@ -2,14 +2,27 @@ source: tui/src/app/daemon_menu_tests.rs expression: "render_bottom_popup(&app.chat_widget,\nwidth).replace(r\"C:\\cli-build\\bin\\codex\", \"/x/cli-build/bin/codex\")" --- - Update daemon and exit Codex? - Install the latest public stable release. Restore production updates; keep - your automatic-update setting. - This may restart the daemon and interrupt active or queued work. - Codex exits to update in this terminal. Relaunch it afterward. + Agent command center Group: Project g + All 0 Needs you 0 Working 0 Ready 0 Inactive 0 + ──────────────────────────────────────────────────────────────────────────── + No tasks yet -› 1. Cancel - 2. Update and exit - enter select · esc back + Update daemon and exit Codex? + Install the latest public stable release. Restore production + updates; keep your automatic-update setting. + This may restart the daemon and interrupt active or queued work. + Codex exits to update in this terminal. Relaunch it afterward. + + + › 1. Cancel + 2. Update and exit + + enter select · esc back + + + + + + Service v0.153.0 < Codex CLI v0.154.0 · /daemon diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 4c68189bd9..a76ac39d9c 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3060,15 +3060,15 @@ async fn open_agent_picker_prompts_when_subagents_disabled() -> Result<()> { Box::pin(app.open_agent_picker(&mut app_server)).await; assert_snapshot!(render_bottom_popup(&app.chat_widget, /*width*/ 80), @r###" - Enable subagents? - Subagents are disabled in this TUI session. + Enable subagents? + › As Subagents are disabled in this TUI session. + gp + › 1. Yes, enable Save on the server for new threads without changing + this thread + 2. Not now Keep subagents disabled - › 1. Yes, enable Save on the server for new threads without changing this - thread - 2. Not now Keep subagents disabled - - enter select · esc back + enter select · esc back "###); Ok(()) } diff --git a/codex-rs/tui/src/app/thread_goal_actions.rs b/codex-rs/tui/src/app/thread_goal_actions.rs index b46853dba8..166e090109 100644 --- a/codex-rs/tui/src/app/thread_goal_actions.rs +++ b/codex-rs/tui/src/app/thread_goal_actions.rs @@ -324,7 +324,7 @@ impl App { )), footer_hint: Some(standard_popup_hint_line()), items, - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } diff --git a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs index 84e373830e..951e80706b 100644 --- a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs +++ b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs @@ -16,8 +16,21 @@ pub(crate) enum ViewCompletion { Cancelled, } +/// How a view is placed over the preceding view in the bottom-pane stack. +#[derive(Clone, Copy, Default, PartialEq, Eq)] +pub(crate) enum ViewPresentation { + #[default] + Inline, + Centered, +} + /// Trait implemented by every view that can be shown in the bottom pane. pub(crate) trait BottomPaneView: Renderable { + /// Centered views preserve the preceding view as their backdrop. + fn presentation(&self) -> ViewPresentation { + ViewPresentation::Inline + } + /// Handle a key event while the view is active. A redraw is always /// scheduled after this call. fn handle_key_event(&mut self, _key_event: KeyEvent) {} diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index 544fce1baa..f576f13bb5 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -331,7 +331,7 @@ pub(crate) fn feedback_upload_consent_params( header: Box::new(crate::render::renderable::ColumnRenderable::with( header_lines, )), - ..super::SelectionViewParams::picker() + ..super::SelectionViewParams::confirmation() } } diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index df8d924f03..f9940ae38c 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -193,6 +193,7 @@ pub(crate) struct SelectionItem { /// `row_display` controls whether rows can wrap or stay single-line with ellipsis truncation /// `description_layout` optionally hides descriptions when their column would become too narrow. pub(crate) struct SelectionViewParams { + pub presentation: super::ViewPresentation, pub picker_surface: PickerSurface, /// Upper row budget; compact completion menus retain the default of eight. pub max_visible_rows: usize, @@ -257,6 +258,7 @@ impl Default for SelectionViewParams { picker_surface: PickerSurface::default(), max_visible_rows: MAX_POPUP_ROWS, reserve_result_rows: false, + presentation: super::ViewPresentation::Inline, view_id: None, title: None, subtitle: None, @@ -294,6 +296,7 @@ impl Default for SelectionViewParams { /// visible rows and source items and for preserving selection while filters /// change. pub(crate) struct ListSelectionView { + presentation: super::ViewPresentation, picker_surface: PickerSurface, max_visible_rows: usize, reserve_result_rows: bool, @@ -436,6 +439,7 @@ impl ListSelectionView { picker_surface: params.picker_surface, max_visible_rows: params.max_visible_rows.max(/*other*/ 1), reserve_result_rows: params.reserve_result_rows, + presentation: params.presentation, view_id: params.view_id, footer_note: params.footer_note, footer_hint: params.footer_hint, @@ -1085,6 +1089,10 @@ impl ListSelectionView { } impl BottomPaneView for ListSelectionView { + fn presentation(&self) -> super::ViewPresentation { + self.presentation + } + fn keymap_contexts(&self) -> crate::keymap::KeymapContextSet { crate::keymap::KeymapContextSet::new(crate::keymap::KeymapContext::List) } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index e1128431ba..5caeb1ad54 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -2,7 +2,8 @@ //! //! The pane owns the [`ChatComposer`] (editable prompt input) and a stack of transient //! [`BottomPaneView`]s (popups/modals) that temporarily replace the composer for focused -//! interactions like selection lists. +//! interactions like selection lists. Centered views retain earlier views as a backdrop, +//! while input remains routed exclusively to the top of the stack. //! //! Input routing is layered: `BottomPane` decides which local surface receives a key (view vs //! composer), while higher-level intent such as "interrupt" or "quit" is decided by the parent @@ -47,6 +48,7 @@ use crate::terminal_palette::effective_stdout_color_level; use crate::tui::FrameRequester; pub(crate) use bottom_pane_view::BottomPaneView; pub(crate) use bottom_pane_view::ViewCompletion; +pub(crate) use bottom_pane_view::ViewPresentation; use codex_app_server_protocol::SkillMetadata; use codex_app_server_protocol::ToolRequestUserInputParams; use codex_features::Features; @@ -119,6 +121,9 @@ pub(crate) use voice_strip::VoiceStripState; mod bottom_pane_view; mod composer_gap; mod effort_ignition; +mod view_stack; +pub(crate) use view_stack::CenteredView; +pub(crate) use view_stack::DialogOverlay; #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct LocalImageAttachment { @@ -1730,6 +1735,11 @@ impl BottomPane { .is_some_and(bottom_pane_view::BottomPaneView::terminal_title_requires_action) } + pub(crate) fn has_centered_view(&self) -> bool { + self.active_view() + .is_some_and(|view| view.presentation() == ViewPresentation::Centered) + } + pub(crate) fn has_active_view(&self) -> bool { self.warnings_view.is_some() || self.has_active_modal() } @@ -2169,8 +2179,36 @@ impl BottomPane { } pub(crate) fn as_renderable_with_options<'a>( + &'a self, + options: ComposerRenderOptions<'a>, + ) -> RenderableItem<'a> { + self.renderable_for_views(options, &self.view_stack) + } + + pub(crate) fn centered_dialog(&self) -> Option> { + self.active_view() + .filter(|view| { + !self.warnings_active() && view.presentation() == ViewPresentation::Centered + }) + .map(CenteredView) + } + + pub(crate) fn backdrop_with_options<'a>( + &'a self, + options: ComposerRenderOptions<'a>, + ) -> RenderableItem<'a> { + let views = if self.centered_dialog().is_some() { + &self.view_stack[..self.view_stack.len() - 1] + } else { + &self.view_stack + }; + self.renderable_for_views(options, views) + } + + fn renderable_for_views<'a>( &'a self, mut options: ComposerRenderOptions<'a>, + views: &'a [Box], ) -> RenderableItem<'a> { if self.warnings_active() && let Some(warnings) = &self.warnings_view @@ -2182,8 +2220,12 @@ impl BottomPane { { banner.visible.set(false); } - if let Some(view) = self.active_view() { - RenderableItem::Borrowed(view) + if let Some(view) = views.last() { + if view.presentation() == ViewPresentation::Centered { + RenderableItem::Owned(Box::new(view_stack::ViewStack(views))) + } else { + RenderableItem::Borrowed(view.as_ref()) + } } else { let mut flex = FlexRenderable::new(); if let Some(banner) = self diff --git a/codex-rs/tui/src/bottom_pane/picker_presets.rs b/codex-rs/tui/src/bottom_pane/picker_presets.rs index 26db4f5588..3cde745c7a 100644 --- a/codex-rs/tui/src/bottom_pane/picker_presets.rs +++ b/codex-rs/tui/src/bottom_pane/picker_presets.rs @@ -8,6 +8,15 @@ use super::SelectionDescriptionLayout; use super::SelectionViewParams; impl SelectionViewParams { + /// A confirmation floats over its retained parent using the shared dialog geometry. + /// Actions, initial selection, and cancellation remain the caller's responsibility. + pub(crate) fn confirmation() -> Self { + Self { + presentation: super::ViewPresentation::Centered, + ..Self::picker() + } + } + /// Start a panel picker with stable columns, hiding descriptions when they become too narrow. /// Titles and subtitles wrap; standard hints follow the active list keymap. pub(crate) fn picker() -> Self { diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__view_stack__tests__centered_dialog_retains_backdrop_and_routes_cursor.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__view_stack__tests__centered_dialog_retains_backdrop_and_routes_cursor.snap new file mode 100644 index 0000000000..30f816bb8e --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__view_stack__tests__centered_dialog_retains_backdrop_and_routes_cursor.snap @@ -0,0 +1,28 @@ +--- +source: tui/src/bottom_pane/view_stack_tests.rs +assertion_line: 86 +expression: "snapshots.join(\"\\n\")" +--- +80x9 +"................................................................................" +"................................................................................" +"................................................................................" +"....DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD...." +".... ...." +".... ...." +"................................................................................" +"................................................................................" +"................................................................................" + +24x7 +"........................" +"........................" +"..DDDDDDDDDDDDDDDDDDDD.." +".. .." +".. .." +"........................" +"........................" + +12x2 +"..DDDDDDDD.." +".. .." diff --git a/codex-rs/tui/src/bottom_pane/view_stack.rs b/codex-rs/tui/src/bottom_pane/view_stack.rs new file mode 100644 index 0000000000..0f90ee53bb --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/view_stack.rs @@ -0,0 +1,122 @@ +//! Shared centered-dialog geometry for retained view stacks and full transcript surfaces. +//! Only the top view receives input; painting a backdrop never changes focus. + +use super::BottomPaneView; +use super::ViewPresentation; +use crate::render::renderable::Renderable; +use crate::render::renderable::RenderableItem; +use crossterm::cursor::SetCursorStyle; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::Clear; +use ratatui::widgets::Widget; + +pub(super) struct ViewStack<'a>(pub(super) &'a [Box]); + +fn dialog_area(view: &dyn BottomPaneView, area: Rect) -> Rect { + let width = area + .width + .saturating_sub(/*rhs*/ 4) + .max(area.width.min(/*other*/ 8)) + .min(/*other*/ 72); + let height = view.desired_height(width).min(area.height); + Rect::new( + area.x + (area.width - width) / 2, + area.y + (area.height - height) / 2, + width, + height, + ) +} + +impl Renderable for ViewStack<'_> { + fn render(&self, area: Rect, buf: &mut Buffer) { + let Some((view, previous)) = self.0.split_last() else { + return; + }; + if view.presentation() == ViewPresentation::Centered { + Self(previous).render(area, buf); + CenteredView(view.as_ref()).render(area, buf); + } else { + view.render(area, buf); + } + } + + fn desired_height(&self, width: u16) -> u16 { + let Some((view, previous)) = self.0.split_last() else { + return 0; + }; + if view.presentation() == ViewPresentation::Centered { + CenteredView(view.as_ref()) + .desired_height(width) + .max(Self(previous).desired_height(width)) + } else { + view.desired_height(width) + } + } + + fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { + let view = self.0.last()?; + view.cursor_pos(dialog_area(view.as_ref(), area)) + } + + fn cursor_style(&self, area: Rect) -> SetCursorStyle { + self.0 + .last() + .map_or(SetCursorStyle::DefaultUserShape, |view| { + view.cursor_style(dialog_area(view.as_ref(), area)) + }) + } +} + +/// A dialog placed within the supplied viewport, without taking over its backdrop. +pub(crate) struct CenteredView<'a>(pub(super) &'a dyn BottomPaneView); + +impl Renderable for CenteredView<'_> { + fn render(&self, area: Rect, buf: &mut Buffer) { + let dialog = dialog_area(self.0, area); + Clear.render(dialog, buf); + self.0.render(dialog, buf); + } + + fn desired_height(&self, width: u16) -> u16 { + dialog_area(self.0, Rect::new(/*x*/ 0, /*y*/ 0, width, u16::MAX)).height + } + + fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { + self.0.cursor_pos(dialog_area(self.0, area)) + } + + fn cursor_style(&self, area: Rect) -> SetCursorStyle { + self.0.cursor_style(dialog_area(self.0, area)) + } +} + +pub(crate) struct DialogOverlay<'a> { + pub(crate) backdrop: RenderableItem<'a>, + pub(crate) dialog: CenteredView<'a>, +} + +impl Renderable for DialogOverlay<'_> { + fn render(&self, area: Rect, buf: &mut Buffer) { + self.backdrop.render(area, buf); + self.dialog.render(area, buf); + } + + fn desired_height(&self, width: u16) -> u16 { + self.backdrop + .desired_height(width) + .max(self.dialog.desired_height(width)) + } + + fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { + self.dialog.cursor_pos(area) + } + + fn cursor_style(&self, area: Rect) -> SetCursorStyle { + self.dialog.cursor_style(area) + } +} + +#[cfg(test)] +#[path = "view_stack_tests.rs"] +mod tests; diff --git a/codex-rs/tui/src/bottom_pane/view_stack_tests.rs b/codex-rs/tui/src/bottom_pane/view_stack_tests.rs new file mode 100644 index 0000000000..1d7cae757f --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/view_stack_tests.rs @@ -0,0 +1,83 @@ +//! Exercises dialog layout independently of any production confirmation caller. + +use super::*; +use pretty_assertions::assert_eq; +use ratatui::Terminal; +use ratatui::backend::TestBackend; + +struct TestPane { + presentation: ViewPresentation, +} + +impl BottomPaneView for TestPane { + fn presentation(&self) -> ViewPresentation { + self.presentation + } +} + +impl Renderable for TestPane { + fn render(&self, area: Rect, buf: &mut Buffer) { + for y in area.top()..area.bottom() { + for x in area.left()..area.right() { + if self.presentation == ViewPresentation::Inline { + buf[(x, y)].set_symbol("."); + } else if y == area.top() { + buf[(x, y)].set_symbol("D"); + } + } + } + } + + fn desired_height(&self, _width: u16) -> u16 { + 3 + } + + fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { + Some((area.x, area.y)) + } + + fn cursor_style(&self, _area: Rect) -> SetCursorStyle { + SetCursorStyle::SteadyBar + } +} + +#[test] +fn centered_dialog_retains_backdrop_and_routes_cursor() { + let backdrop = TestPane { + presentation: ViewPresentation::Inline, + }; + let dialog = TestPane { + presentation: super::super::SelectionViewParams::confirmation().presentation, + }; + let overlay = DialogOverlay { + backdrop: RenderableItem::Borrowed(&backdrop), + dialog: CenteredView(&dialog), + }; + let mut snapshots = Vec::new(); + for (width, height, expected_cursor) in [(80, 9, (4, 3)), (24, 7, (2, 2)), (12, 2, (2, 0))] { + let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); + terminal + .draw(|frame| overlay.render(frame.area(), frame.buffer_mut())) + .unwrap(); + let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height); + assert_eq!( + (overlay.cursor_pos(area), overlay.cursor_style(area)), + (Some(expected_cursor), SetCursorStyle::SteadyBar) + ); + let views: Vec> = vec![ + Box::new(TestPane { + presentation: ViewPresentation::Inline, + }), + Box::new(TestPane { + presentation: ViewPresentation::Centered, + }), + ]; + let stack = ViewStack(&views); + let mut stacked = Buffer::empty(area); + stack.render(area, &mut stacked); + assert_eq!(&stacked, terminal.backend().buffer()); + assert_eq!(stack.cursor_pos(area), overlay.cursor_pos(area)); + snapshots.push(format!("{width}x{height}\n{}", terminal.backend())); + } + insta::assert_snapshot!(snapshots.join("\n")); +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 807a8fcbb1..4af61b2c62 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1079,7 +1079,7 @@ impl ChatWidget { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } diff --git a/codex-rs/tui/src/chatwidget/goal_menu.rs b/codex-rs/tui/src/chatwidget/goal_menu.rs index 151ac79a14..e8bfa62b23 100644 --- a/codex-rs/tui/src/chatwidget/goal_menu.rs +++ b/codex-rs/tui/src/chatwidget/goal_menu.rs @@ -67,7 +67,7 @@ impl ChatWidget { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } diff --git a/codex-rs/tui/src/chatwidget/misalignment_policy.rs b/codex-rs/tui/src/chatwidget/misalignment_policy.rs index eb7283fa19..98c41afcac 100644 --- a/codex-rs/tui/src/chatwidget/misalignment_policy.rs +++ b/codex-rs/tui/src/chatwidget/misalignment_policy.rs @@ -223,7 +223,7 @@ impl ChatWidget { }, ], allow_cancel: false, - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs index 756f7dc776..62877cfdf8 100644 --- a/codex-rs/tui/src/chatwidget/permission_popups.rs +++ b/codex-rs/tui/src/chatwidget/permission_popups.rs @@ -512,7 +512,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { items, header: Box::new(header), - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } } diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index 241c9b244a..017c4afcd8 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -447,7 +447,7 @@ impl ChatWidget { response: plugins_response_for_on_cancel.clone(), }); })), - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() } } diff --git a/codex-rs/tui/src/chatwidget/rendering.rs b/codex-rs/tui/src/chatwidget/rendering.rs index ebfc641a2c..adfe1cee5c 100644 --- a/codex-rs/tui/src/chatwidget/rendering.rs +++ b/codex-rs/tui/src/chatwidget/rendering.rs @@ -127,10 +127,30 @@ impl ExternalWriterNotice { impl ChatWidget { pub(crate) fn as_renderable(&self) -> RenderableItem<'_> { + let backdrop = self.as_backdrop_renderable(); + match self.centered_dialog() { + Some(dialog) => RenderableItem::Owned(Box::new(crate::bottom_pane::DialogOverlay { + backdrop, + dialog, + })), + None => backdrop, + } + } + + pub(crate) fn centered_dialog(&self) -> Option> { + self.bottom_pane.centered_dialog() + } + + fn as_backdrop_renderable(&self) -> RenderableItem<'_> { if self .bottom_pane .selected_index_for_active_view(crate::app::AGENTS_OVERVIEW_VIEW_ID) .is_some() + || (self.bottom_pane.has_centered_view() + && self + .bottom_pane + .selected_index_for_present_view(crate::app::AGENTS_OVERVIEW_VIEW_ID) + .is_some()) { return self .bottom_pane_renderable(crate::bottom_pane::ComposerRenderOptions::default()); @@ -236,7 +256,7 @@ impl ChatWidget { options.textarea_right_reserve = right_reserve; options.separate_status_line = options.command_popup_placement != crate::bottom_pane::CommandPopupPlacement::AboveComposer; - self.bottom_pane.as_renderable_with_options(options) + self.bottom_pane.backdrop_with_options(options) } } diff --git a/codex-rs/tui/src/chatwidget/safety_buffering.rs b/codex-rs/tui/src/chatwidget/safety_buffering.rs index dacfbc5db0..dc3fb0b805 100644 --- a/codex-rs/tui/src/chatwidget/safety_buffering.rs +++ b/codex-rs/tui/src/chatwidget/safety_buffering.rs @@ -289,7 +289,7 @@ impl ChatWidget { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } } diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index fc23f85fb9..9a39bfa473 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -258,7 +258,7 @@ impl ChatWidget { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); self.request_redraw(); } @@ -291,7 +291,7 @@ impl ChatWidget { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); self.request_redraw(); } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__clipped_full_access_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__clipped_full_access_confirmation.snap index e9077bbc3e..8912eda3e0 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__clipped_full_access_confirmation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__clipped_full_access_confirmation.snap @@ -4,30 +4,30 @@ expression: "snapshots.join(\"\\n\\n\")" --- specialty=None 40x10 - Enable full access? - When Codex runs with full access, it - can edit any file on your computer - and run commands with network, - without your approval. Exercise - [… 9 lines] -› 1. Yes, continue anyway - 2. Cancel - enter select · esc back + Enable full access? + When Codex runs with full + access, it can edit any file on + your computer and run commands + with network, without your + [… 10 lines] + › 1. Yes, continue anyway + 2. Cancel + enter select · esc back specialty=Some("cyber") 40x16 - Enable full access? + Enable full access? - When Codex runs with full access, it - can edit any file on your computer - and run commands with network, - without your approval. + When Codex runs with full + access, it can edit any file on + your computer and run commands + with network, without your + approval. - Cyber models carry a higher risk of - dangerous actions. Ensure proper - safeguards are in place before - granting full access. We strongly - [… 13 lines] -› 1. Yes, continue anyway - 2. Cancel - enter select · esc back + Cyber models carry a higher risk + of dangerous actions. Ensure + proper safeguards are in place + [… 14 lines] + › 1. Yes, continue anyway + 2. Cancel + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap index dd81733077..ff69171a61 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap @@ -2,20 +2,20 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- - Upload logs? - - The following files will be sent: - • codex-logs.log - • client-logs.txt (if available) - • codex-doctor-report.json - • codex-apps-tools-cache.json (if available) - • codex-app-directory-cache.json (if available) - • auto-review-rollout-thread-1.jsonl - • codex-connectivity-diagnostics.txt + Upload logs? +› As + The following files will be sent: + GP • codex-logs.log + • client-logs.txt (if available) + • codex-doctor-report.json + • codex-apps-tools-cache.json (if available) + • codex-app-directory-cache.json (if available) + • auto-review-rollout-thread-1.jsonl + • codex-connectivity-diagnostics.txt -› 1. Yes Share the current Codex session logs and diagnostics with the team for - troubleshooting - 2. No + › 1. Yes Share the current Codex session logs and diagnostics with the + team for troubleshooting + 2. No - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap index 3f27314c86..99a9916b3f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap @@ -2,25 +2,25 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- - Upload logs? + Upload logs? +› As + The following files will be sent: + GP • codex-logs.log + • client-logs.txt (if available) + • codex-doctor-report.json + • codex-apps-tools-cache.json (if available) + • codex-app-directory-cache.json (if available) + • windows-sandbox.log + • auto-review-rollout-thread-1.jsonl + • codex-connectivity-diagnostics.txt - The following files will be sent: - • codex-logs.log - • client-logs.txt (if available) - • codex-doctor-report.json - • codex-apps-tools-cache.json (if available) - • codex-app-directory-cache.json (if available) - • windows-sandbox.log - • auto-review-rollout-thread-1.jsonl - • codex-connectivity-diagnostics.txt - - Connectivity diagnostics - - Proxy environment variables are set and may affect connectivity. - - HTTPS_PROXY = hello + Connectivity diagnostics + - Proxy environment variables are set and may affect connectivity. + - HTTPS_PROXY = hello -› 1. Yes Share the current Codex session logs and diagnostics with the team for - troubleshooting - 2. No + › 1. Yes Share the current Codex session logs and diagnostics with the + team for troubleshooting + 2. No - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap index f151f3adf4..20835a2d68 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap @@ -2,14 +2,14 @@ source: tui/src/chatwidget/tests/permissions.rs expression: popup --- - Enable full access? - When Codex runs with full access, it can edit any file on your computer and - run commands with network, without your approval. Exercise caution when - enabling full access. This significantly increases the risk of data loss, - leaks, or unexpected behavior. + Enable full access? +› As When Codex runs with full access, it can edit any file on your + computer and run commands with network, without your approval. + GP Exercise caution when enabling full access. This significantly + increases the risk of data loss, leaks, or unexpected behavior. -› 1. Yes, continue anyway Apply full access for this session - 2. Cancel Go back without enabling full access + › 1. Yes, continue anyway Apply full access for this session + 2. Cancel Go back without enabling full access - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap index 33fddb5fcf..489d5d2bb9 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap @@ -2,13 +2,13 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs expression: "render_bottom_popup(&chat, 80)" --- - Enable memories? - Memories are disabled in this TUI session. + Enable memories? +› As Memories are disabled in this TUI session. + GP + › 1. Yes, enable Save on the server for new threads without changing + this thread + 2. Not now Keep memories disabled -› 1. Yes, enable Save on the server for new threads without changing this - thread - 2. Not now Keep memories disabled - - Learn more: https://developers.openai.com/codex/memories - enter select · esc back + Learn more: https://developers.openai.com/codex/memories + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__misalignment_review_narrow.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__misalignment_review_narrow.snap index 5a56080db4..bb7acc1a37 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__misalignment_review_narrow.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__misalignment_review_narrow.snap @@ -2,8 +2,9 @@ source: tui/src/chatwidget/tests/misalignment_policy_tests.rs expression: "render_bottom_popup(&chat, 38)" --- - Chat paused as a precaution + Chat paused as a precaution +› - -› 1. Acknowledge findings and continue - 2. Back + › 1. Acknowledge findings and + continue + 2. Back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap index 6711141f37..2bef7a92e8 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap @@ -2,12 +2,12 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs expression: "render_bottom_popup(&chat, 80)" --- - Enable subagents? - Subagents are disabled in this TUI session. + Enable subagents? +› As Subagents are disabled in this TUI session. + GP + › 1. Yes, enable Save on the server for new threads without changing + this thread + 2. Not now Keep subagents disabled -› 1. Yes, enable Save on the server for new threads without changing this - thread - 2. Not now Keep subagents disabled - - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap index 83da795e20..f48a5f6971 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap @@ -2,12 +2,13 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs expression: confirmation --- - Plugins - Remove Repo Marketplace marketplace? - This removes the configured marketplace from Codex. + Plugins +› Ask Codex to Remove Repo Marketplace marketplace? + This removes the configured marketplace from Codex. + GPT-5.6-Sol + › 1. Remove marketplace Remove this marketplace from the available + plugin list + 2. Back to plugins Keep this marketplace installed -› 1. Remove marketplace Remove this marketplace from the available plugin list - 2. Back to plugins Keep this marketplace installed - - enter select · esc close + enter select · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_reset_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_reset_confirmation.snap index e2f0810bc7..4889cd5d6a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_reset_confirmation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_reset_confirmation.snap @@ -2,11 +2,12 @@ source: tui/src/chatwidget/tests/usage.rs expression: "render_bottom_popup(&chat, 80)" --- - Use this reset? - Full reset (Weekly + 5 hr) · Expires 09:39 on 18 Jun 2026 + Use this reset? + Us Full reset (Weekly + 5 hr) · Expires 09:39 on 18 Jun 2026 + 1 - - 1. Yes, use reset Reset your weekly and 5-hour usage limits. -› 2. No, go back Choose a different reset - - enter confirm · esc back + 1. Yes, use reset Reset your weekly and 5-hour usage limits. +› 1.› 2. No, go back Choose a different reset + 2. + enter confirm · esc back + enter choose · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__resume_paused_goal_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__resume_paused_goal_prompt.snap index 54af7039c1..1482540600 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__resume_paused_goal_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__resume_paused_goal_prompt.snap @@ -2,11 +2,12 @@ source: tui/src/chatwidget/tests/goal_menu.rs expression: "render_bottom_popup(&chat, 100)" --- - Resume paused goal? - Goal: Keep improving the bare goal command until it feels calm and useful. + Resume paused goal? +› Ask Codex to Goal: Keep improving the bare goal command until it feels calm and + useful. + GPT-5.6-Sol + › 1. Resume goal Mark it active and continue when idle + 2. Leave paused Keep it paused; use /goal resume later -› 1. Resume goal Mark it active and continue when idle - 2. Leave paused Keep it paused; use /goal resume later - - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation.snap index d160632e4b..2906e3764c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation.snap @@ -2,14 +2,14 @@ source: tui/src/chatwidget/tests/app_server.rs expression: "render_bottom_popup(&chat, 80)" --- - Stop this attempt and retry? +• Wo Stop this attempt and retry? + └ ry + This will stop the current attempt and retry in a new thread. Any . + file changes or other actions already taken will remain. - This will stop the current attempt and retry in a new thread. Any file - changes or other actions already taken will remain. +› As Your message will be sent again using Faster Model, which may be + less capable on complex tasks. + gp - Your message will be sent again using Faster Model, which may be less - capable on complex tasks. - - -› 1. Keep waiting - 2. Stop and retry + › 1. Keep waiting + 2. Stop and retry diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation_narrow.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation_narrow.snap index 94fa472e5c..c35e4b3520 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation_narrow.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation_narrow.snap @@ -2,17 +2,18 @@ source: tui/src/chatwidget/tests/app_server.rs expression: "render_bottom_popup(&chat, 40)" --- - Stop this attempt and retry? +• Stop this attempt and retry? - This will stop the current attempt - and retry in a new thread. Any file - changes or other actions already - taken will remain. + This will stop the current + attempt and retry in a new be + thread. Any file changes or + other actions already taken will + remain. - Your message will be sent again - using Faster Model, which may be - less capable on complex tasks. +› Your message will be sent again + using Faster Model, which may be + less capable on complex tasks. -› 1. Keep waiting - 2. Stop and retry + › 1. Keep waiting + 2. Stop and retry diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_confirmation_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_confirmation_popup.snap index ef89361480..b5281c3214 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_confirmation_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_confirmation_popup.snap @@ -2,11 +2,11 @@ source: tui/src/chatwidget/tests/slash_commands.rs expression: popup --- - Archive this session? - Are you sure? This will archive the current session + Archive this session? +› As Are you sure? This will archive the current session + GP + › 1. No, don't archive Return to the current session + 2. Yes, archive Archive this session now -› 1. No, don't archive Return to the current session - 2. Yes, archive Archive this session now - - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_command_center.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_command_center.snap index 506f6e9690..cd9e610ef6 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_command_center.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_command_center.snap @@ -2,12 +2,13 @@ source: tui/src/chatwidget/tests/slash_commands.rs expression: popup --- - Delete this session? - Cannot be undone. Subagent threads will also be deleted. + Delete this session? +› As Cannot be undone. Subagent threads will also be deleted. + GP + › 1. No, keep this session Return to the current + session + 2. Yes, delete and return to command center Permanently delete this + session now -› 1. No, keep this session Return to the current session - 2. Yes, delete and return to command center Permanently delete this session - now - - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_popup.snap index 1067996b0c..85506a0ab4 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_delete_confirmation_popup.snap @@ -2,11 +2,11 @@ source: tui/src/chatwidget/tests/slash_commands.rs expression: popup --- - Delete this session? - Cannot be undone. Subagent threads will also be deleted. + Delete this session? +› As Cannot be undone. Subagent threads will also be deleted. + GP + › 1. No, keep this session Return to the current session + 2. Yes, delete and exit Permanently delete this session now -› 1. No, keep this session Return to the current session - 2. Yes, delete and exit Permanently delete this session now - - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktree_browser_delete_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktree_browser_delete_confirmation.snap index 901993b4ec..7dafeaf2ea 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktree_browser_delete_confirmation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktree_browser_delete_confirmation.snap @@ -2,11 +2,11 @@ source: tui/src/chatwidget/tests/worktree_picker_tests.rs expression: "render_bottom_popup(&chat, 80)" --- - Delete this worktree? - /repo/worktree + Delete this worktree? +› As /repo/worktree + GP + › 1. Cancel + 2. Delete worktree Keeps thread history; may disrupt other sessions -› 1. Cancel - 2. Delete worktree Keeps thread history; may disrupt other sessions - - enter select · esc back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs b/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs index 84b2f8c6e3..b01059cc41 100644 --- a/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs @@ -267,6 +267,8 @@ async fn misalignment_review_requires_current_findings() { chat.on_misalignment_error(Some("turn-1".to_string()), Some(review_details())); let review = select_review(&mut chat, &mut rx); chat.show_misalignment_review_confirmation(Arc::clone(&review)); + chat.bottom_pane + .set_status_line(Some("test project".into())); assert_chatwidget_snapshot!( "misalignment_review_narrow", render_bottom_popup(&chat, /*width*/ 38) diff --git a/codex-rs/tui/src/chatwidget/usage.rs b/codex-rs/tui/src/chatwidget/usage.rs index 11f0d31338..7d1f4201ee 100644 --- a/codex-rs/tui/src/chatwidget/usage.rs +++ b/codex-rs/tui/src/chatwidget/usage.rs @@ -272,7 +272,7 @@ impl ChatWidget { on_cancel: Some(Box::new(move |_| { confirmation_gate.store(true, Ordering::Release); })), - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); true } diff --git a/codex-rs/tui/src/chatwidget/worktree_picker.rs b/codex-rs/tui/src/chatwidget/worktree_picker.rs index ef06ff2776..6ca63f0d8c 100644 --- a/codex-rs/tui/src/chatwidget/worktree_picker.rs +++ b/codex-rs/tui/src/chatwidget/worktree_picker.rs @@ -379,7 +379,7 @@ impl ChatWidget { ..Default::default() }, ], - ..SelectionViewParams::picker() + ..SelectionViewParams::confirmation() }); } } diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__tests__replace_goal_confirmation.snap b/codex-rs/tui/src/snapshots/codex_tui__app__tests__replace_goal_confirmation.snap index 5f013425e3..1aacd04c31 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__app__tests__replace_goal_confirmation.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__app__tests__replace_goal_confirmation.snap @@ -2,11 +2,11 @@ source: tui/src/app/tests.rs expression: "render_bottom_popup(&app.chat_widget, 80)" --- - Replace goal? - New objective: New goal + Replace goal? +› As New objective: New goal + GP + › 1. Replace current goal Set the new objective and start it now + 2. Cancel Keep the current goal -› 1. Replace current goal Set the new objective and start it now - 2. Cancel Keep the current goal - - enter select · esc back + enter select · esc back