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
This commit is contained in:
Benjamin Carlsson
2026-10-03 03:00:46 +00:00
committed by copyberry
parent 9ce35d337a
commit d42aecc56b
47 changed files with 649 additions and 213 deletions
@@ -157,7 +157,7 @@ impl App {
..Default::default()
},
],
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
@@ -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!(
+1 -1
View File
@@ -122,7 +122,7 @@ impl App {
..Default::default()
},
],
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
}
+16 -2
View File
@@ -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);
}
})?;
@@ -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;
@@ -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
@@ -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
@@ -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
@@ -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
+7 -7
View File
@@ -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(())
}
+1 -1
View File
@@ -324,7 +324,7 @@ impl App {
)),
footer_hint: Some(standard_popup_hint_line()),
items,
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
@@ -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) {}
@@ -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()
}
}
@@ -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)
}
+45 -3
View File
@@ -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<CenteredView<'_>> {
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<dyn BottomPaneView>],
) -> 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
@@ -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 {
@@ -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.."
".. .."
+122
View File
@@ -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<dyn BottomPaneView>]);
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;
@@ -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<Box<dyn BottomPaneView>> = 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"));
}
+1 -1
View File
@@ -1079,7 +1079,7 @@ impl ChatWidget {
..Default::default()
},
],
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
+1 -1
View File
@@ -67,7 +67,7 @@ impl ChatWidget {
..Default::default()
},
],
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
@@ -223,7 +223,7 @@ impl ChatWidget {
},
],
allow_cancel: false,
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
@@ -512,7 +512,7 @@ impl ChatWidget {
self.bottom_pane.show_selection_view(SelectionViewParams {
items,
header: Box::new(header),
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
}
@@ -447,7 +447,7 @@ impl ChatWidget {
response: plugins_response_for_on_cancel.clone(),
});
})),
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
}
}
+21 -1
View File
@@ -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<crate::bottom_pane::CenteredView<'_>> {
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)
}
}
@@ -289,7 +289,7 @@ impl ChatWidget {
..Default::default()
},
],
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
}
@@ -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();
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
+1 -1
View File
@@ -272,7 +272,7 @@ impl ChatWidget {
on_cancel: Some(Box::new(move |_| {
confirmation_gate.store(true, Ordering::Release);
})),
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
true
}
@@ -379,7 +379,7 @@ impl ChatWidget {
..Default::default()
},
],
..SelectionViewParams::picker()
..SelectionViewParams::confirmation()
});
}
}
@@ -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