From d42aecc56be30bda8df6137c1d74d6ed9365a23f Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Sat, 3 Oct 2026 02:51:18 +0000 Subject: [PATCH] Center TUI confirmations over their retained backdrop (#50504) ## What changed Render confirmation dialogs as centered overlays while preserving the composer or parent picker behind them. Share dialog sizing and cursor placement across stacked views and full transcript rendering, with input still routed exclusively to the active dialog. Use the shared confirmation preset for prompts including full access, session archive/delete, goal changes, and log upload consent. ## Testing Add coverage for backdrop retention and cursor routing at multiple terminal sizes, and for preserving the compact viewport and composer draft after dismissing a confirmation without replaying history. Update confirmation snapshots to show the retained backdrop. GitOrigin-RevId: 026af89904ec345e2b330317b8cc844512125d0e --- .../tui/src/app/agents_overview_actions.rs | 2 +- .../src/app/agents_overview_actions_tests.rs | 9 +- codex-rs/tui/src/app/daemon_menu.rs | 2 +- codex-rs/tui/src/app/owned_transcript.rs | 18 ++- .../tui/src/app/owned_transcript_tests.rs | 33 +++++ ...s__actions__archive_task_confirmation.snap | 29 ++++- ...ts__actions__delete_task_confirmation.snap | 29 ++++- ..._menu__tests__daemon_cli_confirmation.snap | 30 +++-- ...nu__tests__daemon_stable_confirmation.snap | 29 +++-- codex-rs/tui/src/app/tests.rs | 14 +- codex-rs/tui/src/app/thread_goal_actions.rs | 2 +- .../tui/src/bottom_pane/bottom_pane_view.rs | 13 ++ codex-rs/tui/src/bottom_pane/feedback_view.rs | 2 +- .../src/bottom_pane/list_selection_view.rs | 8 ++ codex-rs/tui/src/bottom_pane/mod.rs | 48 ++++++- .../tui/src/bottom_pane/picker_presets.rs | 9 ++ ...og_retains_backdrop_and_routes_cursor.snap | 28 ++++ codex-rs/tui/src/bottom_pane/view_stack.rs | 122 ++++++++++++++++++ .../tui/src/bottom_pane/view_stack_tests.rs | 83 ++++++++++++ codex-rs/tui/src/chatwidget.rs | 2 +- codex-rs/tui/src/chatwidget/goal_menu.rs | 2 +- .../tui/src/chatwidget/misalignment_policy.rs | 2 +- .../tui/src/chatwidget/permission_popups.rs | 2 +- codex-rs/tui/src/chatwidget/plugin_catalog.rs | 2 +- codex-rs/tui/src/chatwidget/rendering.rs | 22 +++- .../tui/src/chatwidget/safety_buffering.rs | 2 +- codex-rs/tui/src/chatwidget/slash_dispatch.rs | 4 +- ...sts__clipped_full_access_confirmation.snap | 44 +++---- ...s__feedback_good_result_consent_popup.snap | 28 ++-- ..._tests__feedback_upload_consent_popup.snap | 36 +++--- ...tests__full_access_confirmation_popup.snap | 16 +-- ...widget__tests__memories_enable_prompt.snap | 16 +-- ...et__tests__misalignment_review_narrow.snap | 9 +- ...get__tests__multi_agent_enable_prompt.snap | 14 +- ...popup_marketplace_remove_confirmation.snap | 15 ++- ..._tests__rate_limit_reset_confirmation.snap | 15 ++- ...get__tests__resume_paused_goal_prompt.snap | 13 +- ...__safety_buffering_retry_confirmation.snap | 18 +-- ...y_buffering_retry_confirmation_narrow.snap | 21 +-- ...sts__slash_archive_confirmation_popup.snap | 12 +- ...sh_delete_confirmation_command_center.snap | 15 ++- ...ests__slash_delete_confirmation_popup.snap | 12 +- ..._worktree_browser_delete_confirmation.snap | 12 +- .../tests/misalignment_policy_tests.rs | 2 + codex-rs/tui/src/chatwidget/usage.rs | 2 +- .../tui/src/chatwidget/worktree_picker.rs | 2 +- ...app__tests__replace_goal_confirmation.snap | 12 +- 47 files changed, 649 insertions(+), 213 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__view_stack__tests__centered_dialog_retains_backdrop_and_routes_cursor.snap create mode 100644 codex-rs/tui/src/bottom_pane/view_stack.rs create mode 100644 codex-rs/tui/src/bottom_pane/view_stack_tests.rs 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