diff --git a/codex-rs/tui/src/bottom_pane/app_link_view.rs b/codex-rs/tui/src/bottom_pane/app_link_view.rs index fab39e2502..44fae4d28d 100644 --- a/codex-rs/tui/src/bottom_pane/app_link_view.rs +++ b/codex-rs/tui/src/bottom_pane/app_link_view.rs @@ -629,7 +629,12 @@ impl AppLinkView { } .dim(), ])); - let url_line = Line::from(vec![self.url.clone().cyan().underlined()]); + let url_line = Line::from(vec![ + self.url + .clone() + .fg(crate::style::accent_color()) + .underlined(), + ]); lines.extend(adaptive_wrap_lines( vec![url_line], RtOptions::new(usable_width), @@ -649,6 +654,8 @@ impl AppLinkView { ' ' }; GenericDisplayRow { + category_tag: None, + selection_style: Some(crate::bottom_pane::selection_style()), name: format!("{prefix} {}. {label}", index + 1), ..Default::default() } @@ -669,19 +676,25 @@ impl AppLinkView { } fn hint_line(&self) -> Line<'static> { - let mut spans = vec!["Use ".into(), key_hint::plain(KeyCode::Tab).into()]; + let mut spans = vec![key_hint::plain(KeyCode::Tab).into()]; if let Some(move_up) = self.list_keymap.primary_hint(ListAction::MoveUp) { - spans.extend([" / ".into(), move_up.into()]); + spans.push(" / ".into()); + spans.extend(move_up.spans()); } if let Some(move_down) = self.list_keymap.primary_hint(ListAction::MoveDown) { - spans.extend([" ".into(), move_down.into()]); + spans.push(" ".into()); + spans.extend(move_down.spans()); } - spans.push(" to move".into()); + spans.push(" move".into()); if let Some(accept) = self.list_keymap.primary_hint(ListAction::Accept) { - spans.extend([", ".into(), accept.into(), " to select".into()]); + spans.push(" · ".into()); + spans.extend(accept.spans()); + spans.push(" select".into()); } if let Some(cancel) = self.list_keymap.primary_hint(ListAction::Cancel) { - spans.extend([", ".into(), cancel.into(), " to close".into()]); + spans.push(" · ".into()); + spans.extend(cancel.spans()); + spans.push(" close".into()); } Line::from(spans) } @@ -811,8 +824,11 @@ impl crate::render::renderable::Renderable for AppLinkView { .wrap(Wrap { trim: false }) .line_count(content_width) .max(1) as u16; - let action_rows_height = self.action_rows_height(content_width); - content_rows + action_rows_height + 3 + let action_rows_height = self.action_rows_height(width); + let hint_rows = Paragraph::new(self.hint_line()) + .wrap(Wrap { trim: false }) + .line_count(width.saturating_sub(/*rhs*/ 2)) as u16; + content_rows + action_rows_height + hint_rows + 2 } fn render(&self, area: Rect, buf: &mut Buffer) { @@ -824,11 +840,15 @@ impl crate::render::renderable::Renderable for AppLinkView { .style(user_message_style()) .render(area, buf); - let actions_height = self.action_rows_height(area.width.saturating_sub(4)); + let actions_height = self.action_rows_height(area.width); + let hint = Paragraph::new(self.hint_line()) + .dim() + .wrap(Wrap { trim: false }); + let hint_height = hint.line_count(area.width.saturating_sub(/*rhs*/ 2)) as u16; let [content_area, actions_area, hint_area] = Layout::vertical([ Constraint::Fill(1), Constraint::Length(actions_height), - Constraint::Length(1), + Constraint::Length(hint_height), ]) .areas(area); @@ -841,12 +861,6 @@ impl crate::render::renderable::Renderable for AppLinkView { crate::terminal_hyperlinks::mark_url_hyperlink(buf, inner, &self.url); if actions_area.height > 0 { - let actions_area = Rect { - x: actions_area.x.saturating_add(2), - y: actions_area.y, - width: actions_area.width.saturating_sub(2), - height: actions_area.height, - }; let action_rows = self.action_rows(); let action_state = self.action_state(); render_rows( @@ -866,7 +880,7 @@ impl crate::render::renderable::Renderable for AppLinkView { width: hint_area.width.saturating_sub(2), height: hint_area.height, }; - self.hint_line().dim().render(hint_area, buf); + hint.render(hint_area, buf); } } } diff --git a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs index 4697cdc7f2..a51f04b937 100644 --- a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs +++ b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs @@ -22,7 +22,9 @@ use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Block; +use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; +use ratatui::widgets::Wrap; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; @@ -30,19 +32,17 @@ use crate::key_hint; use crate::key_hint::KeyBindingListExt; use crate::keymap::ListAction; use crate::keymap::ListKeymap; -use crate::render::Insets; -use crate::render::RectExt as _; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; use crate::style::user_message_style; use super::CancellationEvent; use super::bottom_pane_view::BottomPaneView; +use super::picker_rows::measure_rows_height; +use super::picker_rows::render_rows; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::GenericDisplayRow; -use super::selection_popup_common::measure_rows_height; -use super::selection_popup_common::render_rows; pub(crate) struct ExperimentalFeatureItem { pub key: String, @@ -102,13 +102,15 @@ impl ExperimentalFeaturesView { fn header(&self, width: u16) -> impl Renderable { let mut header = ColumnRenderable::new(); - header.push(Line::from("Experimental features".bold())); + header.push( + Paragraph::new(Line::from("Experimental features".bold())).wrap(Wrap { trim: false }), + ); for text in [ "Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server.", self.discovery_status.as_str(), ].into_iter().filter(|text| !text.is_empty()) { for line in textwrap::wrap(text, usize::from(width.max(1))) { - header.push(Line::from(line.into_owned().dim())); + header.push(Paragraph::new(Line::from(line.into_owned().dim())).wrap(Wrap { trim: false })); } } header @@ -122,6 +124,16 @@ impl ExperimentalFeaturesView { } } + fn current_hint(&self) -> Line<'static> { + if self.write_rx.is_some() { + Line::from("Saving… Closing this popup will not cancel the write.") + } else if !self.unconfirmed.is_empty() { + Line::from("Selections retained. Save to retry, or cancel to close.") + } else { + self.footer_hint.clone() + } + } + fn visible_len(&self) -> usize { self.features.len() } @@ -139,6 +151,9 @@ impl ExperimentalFeaturesView { let read_only = if item.writable { "" } else { " (read-only)" }; let name = format!("{prefix} [{marker}] {}{read_only}", item.name); rows.push(GenericDisplayRow { + category_tag: None, + selection_style: Some(super::picker_style::selection_style()), + wrap_indent: Some(6), name, description: Some(item.description.clone()), is_disabled: !item.writable || self.write_rx.is_some(), @@ -256,10 +271,6 @@ impl ExperimentalFeaturesView { self.complete = true; } } - - fn rows_width(total_width: u16) -> u16 { - total_width.saturating_sub(2) - } } impl BottomPaneView for ExperimentalFeaturesView { @@ -410,8 +421,16 @@ impl Renderable for ExperimentalFeaturesView { return; } - let [content_area, footer_area] = - Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area); + let hint = self.current_hint(); + let hint_lines = super::selection_popup_common::wrap_styled_line( + &hint, + area.width.saturating_sub(/*rhs*/ 2), + ); + let [content_area, footer_area] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(hint_lines.len() as u16), + ]) + .areas(area); Block::default() .style(user_message_style()) @@ -420,24 +439,14 @@ impl Renderable for ExperimentalFeaturesView { let header = self.header(content_area.width.saturating_sub(4)); let header_height = header.desired_height(content_area.width.saturating_sub(4)); let rows = self.build_rows(); - let rows_width = Self::rows_width(content_area.width); - let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, rows_width); - let [header_area, _, list_area] = Layout::vertical([ - Constraint::Max(header_height), - Constraint::Max(1), - Constraint::Length(rows_height), - ]) - .areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2))); + let rows_height = + measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, content_area.width); + let [header_area, _, render_area] = + super::picker_rows::layout(content_area, header_height, /*search*/ 0, rows_height); header.render(header_area, buf); - if list_area.height > 0 && (!rows.is_empty() || self.catalog_rx.is_none()) { - let render_area = Rect { - x: list_area.x.saturating_sub(2), - y: list_area.y, - width: rows_width.max(1), - height: list_area.height, - }; + if render_area.height > 0 && (!rows.is_empty() || self.catalog_rx.is_none()) { render_rows( render_area, buf, @@ -454,37 +463,38 @@ impl Renderable for ExperimentalFeaturesView { width: footer_area.width.saturating_sub(2), height: footer_area.height, }; - let hint = if self.write_rx.is_some() { - Line::from("Saving… Closing this popup will not cancel the write.") - } else if !self.unconfirmed.is_empty() { - Line::from("Selections retained. Save to retry, or cancel to close.") - } else { - self.footer_hint.clone() - }; - hint.dim().render(hint_area, buf); + Paragraph::new(hint_lines).dim().render(hint_area, buf); } fn desired_height(&self, width: u16) -> u16 { let rows = self.build_rows(); - let rows_width = Self::rows_width(width); - let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, rows_width); + let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, width); let mut height = self .header(width.saturating_sub(4)) .desired_height(width.saturating_sub(4)); height = height.saturating_add(rows_height + 3); - height.saturating_add(1) + height.saturating_add( + super::selection_popup_common::wrap_styled_line( + &self.current_hint(), + width.saturating_sub(/*rhs*/ 2), + ) + .len() as u16, + ) } } fn experimental_popup_hint_line(keymap: &ListKeymap) -> Line<'static> { - let mut spans = vec![ - "Press ".into(), - key_hint::plain(KeyCode::Char(' ')).into(), - " to select".into(), - ]; + let mut spans = vec![key_hint::plain(KeyCode::Char(' ')).into(), " toggle".into()]; if let Some(accept) = keymap.primary_hint(ListAction::Accept) { - spans.extend([" or ".into(), accept.into(), " to save".into()]); + spans.push(" · ".into()); + spans.extend(accept.spans()); + spans.push(" save".into()); + } + if let Some(cancel) = keymap.primary_hint(ListAction::Cancel) { + spans.push(" · ".into()); + spans.extend(cancel.spans()); + spans.push(" save/close".into()); } Line::from(spans) } diff --git a/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs b/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs index f5ac6e3ec4..c0385e440a 100644 --- a/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs +++ b/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs @@ -28,7 +28,7 @@ fn experimental_features_keep_following_choice_at_wrap_boundary() { ); let mut snapshots = Vec::new(); - for width in [70, 71, 72] { + for width in [68, 69, 70, 71, 72] { let area = Rect::new( /*x*/ 0, /*y*/ 0, diff --git a/codex-rs/tui/src/bottom_pane/hooks_browser_render.rs b/codex-rs/tui/src/bottom_pane/hooks_browser_render.rs new file mode 100644 index 0000000000..bd71c0f264 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/hooks_browser_render.rs @@ -0,0 +1,162 @@ +//! Responsive hook browsing presentation; trust and toggle policies stay in the view. + +use super::*; +use crate::bottom_pane::picker_style; +use crate::bottom_pane::selection_popup_common::RenderedRows; +use crate::render::Insets; +use crate::render::RectExt; +use ratatui::widgets::Wrap; + +impl HooksBrowserView { + // Keep the existing yellow warning treatment for hooks awaiting review. + #[allow(clippy::disallowed_methods)] + fn event_parts(&self, width: u16) -> (Vec>, Vec>) { + let events = self.event_rows(); + let mut header = self.event_page_lines(); + let mut rows = header.split_off(header.len().saturating_sub(events.len())); + if width < 72 { + header.pop(); + header.push("Event Active/Installed".dim().into()); + rows = events + .iter() + .enumerate() + .map(|(index, row)| { + let name = event_label(row.event_name); + let review = if row.needs_review > 0 { " !" } else { "" }; + let line = Line::from(format!( + "{name:<18} {}/{}{review}", + row.active, row.installed + )); + if self.state.selected_idx == Some(index) { + line.style(selection_style()) + } else if row.needs_review > 0 { + line.yellow() + } else { + line + } + }) + .collect(); + } + for (index, line) in rows.iter_mut().enumerate() { + let prefix = if self.state.selected_idx == Some(index) { + "› " + } else { + " " + }; + line.spans.insert(/*index*/ 0, prefix.into()); + } + (header, rows) + } +} + +fn render_line_rows( + area: Rect, + buf: &mut Buffer, + rows: Vec>, + mut state: ScrollState, +) { + let body = area.inset(Insets::vh(u16::from(area.height >= 3), /*h*/ 0)); + let visible = usize::from(body.height); + state.ensure_visible(rows.len(), visible.max(/*other*/ 1)); + for (offset, line) in rows.iter().skip(state.scroll_top).take(visible).enumerate() { + let row_area = Rect::new( + body.x, + body.y + offset as u16, + body.width, + /*height*/ 1, + ); + line.clone().render(row_area, buf); + if line.style.bg.is_some() { + buf.set_style(row_area, line.style); + } + } + picker_style::render_scroll_indicators( + area, + buf, + RenderedRows { + has_above: state.scroll_top > 0, + has_below: state.scroll_top + visible < rows.len(), + ..Default::default() + }, + ); +} + +impl Renderable for HooksBrowserView { + fn desired_height(&self, width: u16) -> u16 { + let inner_width = width.saturating_sub(/*rhs*/ 4).max(/*other*/ 1); + let (header, rows, detail) = match self.page { + HooksBrowserPage::Events => { + let (header, rows) = self.event_parts(inner_width); + (header, rows.len(), 0) + } + HooksBrowserPage::Handlers(event) => { + let rows = self.handlers_for_event(event).count(); + let detail = if rows == 0 { + 0 + } else { + self.detail_lines(event, usize::from(inner_width)).len() + 1 + }; + ( + Self::handler_header_lines(event, self.review_needed_count(event)), + rows, + detail, + ) + } + }; + let header_height = Paragraph::new(header) + .wrap(Wrap { trim: false }) + .line_count(inner_width); + u16::try_from(header_height + rows.clamp(/*min*/ 1, MAX_POPUP_ROWS) + detail + 5) + .unwrap_or(u16::MAX) + } + + fn render(&self, area: Rect, buf: &mut Buffer) { + if area.is_empty() { + return; + } + let [panel, footer] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area); + let content = render_menu_surface(panel, buf); + if content.is_empty() { + self.render_footer(footer, buf); + return; + } + let (header, rows) = match self.page { + HooksBrowserPage::Events => self.event_parts(content.width), + HooksBrowserPage::Handlers(event) => ( + Self::handler_header_lines(event, self.review_needed_count(event)), + self.handler_row_lines(event, usize::from(area.width)), + ), + }; + let header = Paragraph::new(header).wrap(Wrap { trim: false }); + let header_height = + (header.line_count(content.width) as u16).min(content.height.saturating_sub(/*rhs*/ 3)); + let [header_area, body] = + Layout::vertical([Constraint::Length(header_height), Constraint::Fill(1)]) + .areas(content); + header.render(header_area, buf); + let body = Rect::new(area.x, body.y, area.width, body.height); + match self.page { + HooksBrowserPage::Events => render_line_rows(body, buf, rows, self.state), + HooksBrowserPage::Handlers(event) => { + if rows.is_empty() { + Paragraph::new(" No hooks installed for this event.") + .dim() + .render(body, buf); + } else { + let desired = rows.len().min(MAX_POPUP_ROWS) as u16 + 2; + let list_height = desired + .min(body.height.saturating_sub(/*rhs*/ 3).max(/*other*/ 3)) + .min(body.height); + let [list, details] = + Layout::vertical([Constraint::Length(list_height), Constraint::Fill(1)]) + .areas(body); + render_line_rows(list, buf, rows, self.state); + Paragraph::new(self.detail_lines(event, usize::from(content.width))) + .render(details.inset(Insets::vh(/*v*/ 0, /*h*/ 2)), buf); + } + } + } + self.render_footer(footer, buf); + } +} diff --git a/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs b/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs index 1729e4b611..ceedc2faf4 100644 --- a/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs +++ b/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs @@ -22,6 +22,7 @@ use unicode_width::UnicodeWidthStr; use super::CancellationEvent; use super::bottom_pane_view::BottomPaneView; +use super::picker_style::selection_style; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::render_menu_surface; @@ -36,7 +37,6 @@ use crate::keymap::ListKeymap; use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; use crate::render::renderable::Renderable; use crate::status::format_directory_display; -use crate::style::accent_style; const EVENT_COLUMN_WIDTH: usize = 22; const COUNT_COLUMN_WIDTH: usize = 12; @@ -371,7 +371,7 @@ impl HooksBrowserView { row_line.push(Span::from(event_description(row.event_name))); if selected { - let style = accent_style(); + let style = selection_style(); for span in &mut row_line { *span = span.clone().set_style(style); } @@ -384,7 +384,11 @@ impl HooksBrowserView { let description_idx = row_line.len() - 1; row_line[description_idx] = row_line[description_idx].clone().dim(); } - lines.push(Line::from(row_line)); + lines.push(if selected { + Line::from(row_line).style(selection_style()) + } else { + Line::from(row_line) + }); } lines } @@ -451,15 +455,16 @@ impl HooksBrowserView { format!("[{marker}] {}", hook_title(idx)) } }; - let mut line = Line::from(row); + let prefix = if self.state.selected_idx == Some(idx) { + "› " + } else { + " " + }; + let mut line = Line::from(format!("{prefix}{row}")); line = truncate_line_with_ellipsis_if_overflow(line, width); let needs_review = hook_needs_review(hook); if self.state.selected_idx == Some(idx) { - if needs_review { - line = line.yellow().bold(); - } else { - line = line.patch_style(accent_style()); - } + line = line.patch_style(selection_style()); } else if needs_review { line = line.yellow(); } else if hook.is_managed { @@ -540,56 +545,45 @@ impl HooksBrowserView { .keymap .primary_hint(ListAction::Cancel) .unwrap_or_else(|| key_hint::plain(KeyCode::Esc).into()); - let footer = match self.page { - HooksBrowserPage::Events if self.review_needed_total_count() > 0 => { - let mut spans = vec![ - "Press ".into(), - key_hint::plain(KeyCode::Char('t')).into(), - " to trust all; ".into(), - ]; - if let Some(accept) = accept { - spans.extend([accept.into(), " to review hooks; ".into()]); - } - spans.extend([cancel.into(), " to close".into()]); - Line::from(spans) - } + let mut spans = Vec::new(); + match self.page { HooksBrowserPage::Events => { - let mut spans = vec!["Press ".into()]; - if let Some(accept) = accept { - spans.extend([accept.into(), " to view hooks; ".into()]); + if self.review_needed_total_count() > 0 { + spans.extend(key_hint::plain(KeyCode::Char('t')).spans()); + spans.push(" trust all · ".dim()); + if let Some(accept) = accept { + spans.extend(accept.spans()); + spans.push(" review · ".dim()); + } + } else if let Some(accept) = accept { + spans.extend(accept.spans()); + spans.push(" details · ".dim()); } - spans.extend([cancel.into(), " to close".into()]); - Line::from(spans) + spans.extend(cancel.spans()); + spans.push(" close".dim()); } HooksBrowserPage::Handlers(event_name) => { - let selected_hook = self.selected_hook(event_name); - if selected_hook.is_none() { - Line::from(vec!["Press ".into(), cancel.into(), " to go back".into()]) - } else if selected_hook.is_some_and(|hook| hook.is_managed) { - Line::from(vec![ - "Managed hooks are always on; press ".into(), - cancel.into(), - " to go back".into(), - ]) - } else if selected_hook.is_some_and(hook_needs_review) { - Line::from(vec![ - "Press ".into(), - key_hint::plain(KeyCode::Char('t')).into(), - " to trust; ".into(), - cancel.into(), - " to go back".into(), - ]) - } else { - let mut spans = - vec!["Press ".into(), key_hint::plain(KeyCode::Char(' ')).into()]; - if let Some(accept) = accept { - spans.extend([" or ".into(), accept.into()]); + if let Some(hook) = self.selected_hook(event_name) { + if hook.is_managed { + spans.push("Managed (always on) · ".dim()); + } else if hook_needs_review(hook) { + spans.extend(key_hint::plain(KeyCode::Char('t')).spans()); + spans.push(" trust · ".dim()); + } else { + let mut toggle = key_hint::plain(KeyCode::Char(' ')).display_label(); + if let Some(accept) = accept { + toggle.push('/'); + toggle.push_str(&accept.display_label()); + } + spans.extend(key_hint::key_label_spans(&toggle)); + spans.push(" toggle · ".dim()); } - spans.extend([" to toggle; ".into(), cancel.into(), " to go back".into()]); - Line::from(spans) } + spans.extend(cancel.spans()); + spans.push(" back".dim()); } - }; + } + let footer = Line::from(spans); footer.dim().render(hint_area, buf); } } @@ -656,83 +650,8 @@ impl BottomPaneView for HooksBrowserView { } } -impl Renderable for HooksBrowserView { - fn desired_height(&self, width: u16) -> u16 { - let content_width = width.saturating_sub(4) as usize; - let height = match self.page { - HooksBrowserPage::Events => self.event_page_lines().len(), - HooksBrowserPage::Handlers(event_name) => { - let row_count = self.handler_row_lines(event_name, content_width).len(); - let header_line_count = - Self::handler_header_lines(event_name, self.review_needed_count(event_name)) - .len(); - if row_count == 0 { - header_line_count + 2 - } else { - let visible_row_count = row_count.min(MAX_POPUP_ROWS); - header_line_count - + 1 - + visible_row_count - + 1 - + self.detail_lines(event_name, content_width).len() - } - } - }; - (height + 3).try_into().unwrap_or(u16::MAX) - } - - fn render(&self, area: Rect, buf: &mut Buffer) { - if area.is_empty() { - return; - } - - let [content_area, footer_area] = - Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area); - let content_area = render_menu_surface(content_area, buf); - let width = content_area.width as usize; - let lines = match self.page { - HooksBrowserPage::Events => self.event_page_lines(), - HooksBrowserPage::Handlers(event_name) => { - let mut lines = - Self::handler_header_lines(event_name, self.review_needed_count(event_name)); - let rows = self.handler_row_lines(event_name, width); - if rows.is_empty() { - lines.push(Line::default()); - lines.push(Line::from( - "No hooks installed for this event.".dim().italic(), - )); - lines.push(Line::default()); - Paragraph::new(lines).render(content_area, buf); - self.render_footer(footer_area, buf); - return; - } - let list_height = rows.len().clamp(1, MAX_POPUP_ROWS) as u16; - lines.push(Line::default()); - let header_height = lines.len() as u16; - let [header_area, list_area, detail_area] = Layout::vertical([ - Constraint::Length(header_height), - Constraint::Length(list_height), - Constraint::Fill(1), - ]) - .areas(content_area); - Paragraph::new(lines.clone()).render(header_area, buf); - let visible_rows = rows - .into_iter() - .skip(self.state.scroll_top) - .take(list_height as usize) - .collect::>(); - Paragraph::new(visible_rows).render(list_area, buf); - let mut detail_lines = vec![Line::default()]; - detail_lines.extend(self.detail_lines(event_name, width)); - Paragraph::new(detail_lines).render(detail_area, buf); - self.render_footer(footer_area, buf); - return; - } - }; - Paragraph::new(lines).render(content_area, buf); - self.render_footer(footer_area, buf); - } -} +#[path = "hooks_browser_render.rs"] +mod render; fn hook_is_active(hook: &HookMetadata) -> bool { hook.enabled @@ -1045,14 +964,44 @@ mod tests { let mut view = view(); assert_snapshot!("hooks_browser_events", render_lines(&view, /*width*/ 112)); view.keymap.accept.clear(); - assert!(!render_lines(&view, /*width*/ 112).contains("enter to view hooks")); + assert!(!render_lines(&view, /*width*/ 112).contains("enter details")); } #[test] - fn selected_event_rows_use_the_shared_accent_style() { + fn compact_hooks_keep_the_last_event_and_selection_visible() { + let mut view = view(); + view.state.selected_idx = Some(view.event_rows().len() - 1); + let area = Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 40, /*height*/ 16, + ); + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); + let text = (0..area.height) + .map(|y| { + (0..area.width) + .map(|x| buf[(x, y)].symbol()) + .collect::() + .trim_end() + .to_owned() + }) + .collect::>() + .join("\n"); + insta::assert_snapshot!(text); + let selected_y = text + .lines() + .position(|line| line.starts_with("› ")) + .unwrap() as u16; + assert_eq!(buf[(0, selected_y)].bg, selection_style().bg.unwrap()); + assert_eq!(buf[(39, selected_y)].bg, selection_style().bg.unwrap()); + assert!(text.contains("Interrupt")); + assert!(text.contains("Active/Installed")); + } + + #[test] + fn selected_event_rows_use_the_shared_selection_style() { let view = view(); let buf = render_buffer(&view, /*width*/ 112); - let expected = accent_style(); + let expected = selection_style(); let selected_cell = buf .content @@ -1093,7 +1042,16 @@ mod tests { "hooks_browser_events_with_review_column", render_lines(&view, /*width*/ 112) ); - assert_eq!(view.event_table_lines()[1].spans[3].style, accent_style()); + assert_eq!( + view.event_table_lines()[1].spans[3].style.fg, + selection_style().fg + ); + assert!( + view.event_table_lines()[1].spans[3] + .style + .add_modifier + .contains(ratatui::style::Modifier::BOLD) + ); } #[test] diff --git a/codex-rs/tui/src/bottom_pane/memories_settings_view.rs b/codex-rs/tui/src/bottom_pane/memories_settings_view.rs index 1afbaa2fd7..561e581147 100644 --- a/codex-rs/tui/src/bottom_pane/memories_settings_view.rs +++ b/codex-rs/tui/src/bottom_pane/memories_settings_view.rs @@ -8,11 +8,13 @@ use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Block; +use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; +use ratatui::widgets::Wrap; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; -use crate::bottom_pane::popup_consts::standard_popup_hint_line_for_keymap; +use crate::bottom_pane::popup_consts::picker_hint_line_for_keymap; use crate::key_hint; use crate::key_hint::KeyBindingListExt; use crate::keymap::ListAction; @@ -25,11 +27,11 @@ use crate::style::user_message_style; use super::CancellationEvent; use super::bottom_pane_view::BottomPaneView; +use super::picker_rows::measure_rows_height; +use super::picker_rows::render_rows; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::GenericDisplayRow; -use super::selection_popup_common::measure_rows_height; -use super::selection_popup_common::render_rows; const MEMORIES_DOC_URL: &str = "https://developers.openai.com/codex/memories"; @@ -101,7 +103,9 @@ impl MemoriesSettingsView { app_event_tx, docs_link: Line::from(vec![ "Learn more: ".dim(), - MEMORIES_DOC_URL.cyan().underlined(), + MEMORIES_DOC_URL + .fg(crate::style::accent_color()) + .underlined(), ]), keymap, }; @@ -115,20 +119,29 @@ impl MemoriesSettingsView { fn settings_header(&self) -> ColumnRenderable<'_> { let mut header = ColumnRenderable::new(); - header.push(Line::from("Memories".bold())); - header.push(Line::from( - "Choose how Codex uses and creates memories. Changes are saved to config.toml".dim(), - )); + header.push(Paragraph::new(Line::from("Memories".bold())).wrap(Wrap { trim: false })); + header.push( + Paragraph::new(Line::from( + "Choose how Codex uses and creates memories. Changes are saved to config.toml" + .dim(), + )) + .wrap(Wrap { trim: false }), + ); header } fn reset_confirmation_header(&self) -> ColumnRenderable<'_> { let mut header = ColumnRenderable::new(); - header.push(Line::from("Reset all memories?".bold())); - header.push(Line::from( - "This clears local memory files and rollout summaries for the current Codex home." - .dim(), - )); + header.push( + Paragraph::new(Line::from("Reset all memories?".bold())).wrap(Wrap { trim: false }), + ); + header.push( + Paragraph::new(Line::from( + "This clears local memory files and rollout summaries for the current Codex home." + .dim(), + )) + .wrap(Wrap { trim: false }), + ); header } @@ -154,6 +167,9 @@ impl MemoriesSettingsView { .into_iter() .enumerate() .map(|(idx, name)| GenericDisplayRow { + category_tag: None, + selection_style: Some(super::picker_style::selection_style()), + wrap_indent: Some(2), name: if state.selected_idx == Some(idx) { format!("› {name}") } else { @@ -194,6 +210,13 @@ impl MemoriesSettingsView { } => (format!("{prefix} {name}"), description), }; GenericDisplayRow { + category_tag: None, + selection_style: Some(super::picker_style::selection_style()), + wrap_indent: Some(if matches!(item, MemoriesMenuItem::Setting { .. }) { + 6 + } else { + 2 + }), name, description: Some((*description).to_string()), ..Default::default() @@ -260,10 +283,6 @@ impl MemoriesSettingsView { } } - fn rows_width(total_width: u16) -> u16 { - total_width.saturating_sub(2) - } - fn current_setting(&self, setting: MemoriesSetting) -> bool { self.items .iter() @@ -291,7 +310,7 @@ impl MemoriesSettingsView { fn footer_hint(&self) -> Line<'static> { if self.reset_confirmation.is_some() { - standard_popup_hint_line_for_keymap(&self.keymap) + picker_hint_line_for_keymap(&self.keymap) } else { memories_settings_hint_line(&self.keymap) } @@ -376,8 +395,16 @@ impl Renderable for MemoriesSettingsView { return; } - let [content_area, footer_area] = - Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area); + let hint = self.footer_hint(); + let hint_lines = super::selection_popup_common::wrap_styled_line( + &hint, + area.width.saturating_sub(/*rhs*/ 2), + ); + let [content_area, footer_area] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(hint_lines.len() as u16), + ]) + .areas(area); Block::default() .style(user_message_style()) @@ -390,27 +417,26 @@ impl Renderable for MemoriesSettingsView { }; let header_height = header.desired_height(content_area.width.saturating_sub(4)); let rows = self.build_rows(); - let rows_width = Self::rows_width(content_area.width); - let rows_height = - measure_rows_height(&rows, self.active_state(), MAX_POPUP_ROWS, rows_width); - let [header_area, _, list_area, _, docs_area] = Layout::vertical([ - Constraint::Max(header_height), - Constraint::Max(1), - Constraint::Length(rows_height), - Constraint::Max(1), - Constraint::Length(1), - ]) - .areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2))); + let rows_height = measure_rows_height( + &rows, + self.active_state(), + MAX_POPUP_ROWS, + content_area.width, + ); + let docs_height = if self.reset_confirmation.is_some() { + 0 + } else { + 2 + }; + let [body_area, docs_area] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(docs_height)]) + .areas(content_area); + let [header_area, _, render_area] = + super::picker_rows::layout(body_area, header_height, /*search*/ 0, rows_height); header.render(header_area, buf); - if list_area.height > 0 { - let render_area = Rect { - x: list_area.x.saturating_sub(2), - y: list_area.y, - width: rows_width.max(1), - height: list_area.height, - }; + if render_area.height > 0 { render_rows( render_area, buf, @@ -421,6 +447,9 @@ impl Renderable for MemoriesSettingsView { ); } if self.reset_confirmation.is_none() { + let docs_area = docs_area.inset(Insets::tlbr( + /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 2, + )); self.docs_link.clone().render(docs_area, buf); crate::terminal_hyperlinks::mark_url_hyperlink(buf, docs_area, MEMORIES_DOC_URL); } @@ -431,7 +460,7 @@ impl Renderable for MemoriesSettingsView { width: footer_area.width.saturating_sub(2), height: footer_area.height, }; - self.footer_hint().render(hint_area, buf); + Paragraph::new(hint_lines).dim().render(hint_area, buf); } fn desired_height(&self, width: u16) -> u16 { @@ -441,9 +470,7 @@ impl Renderable for MemoriesSettingsView { self.settings_header() }; let rows = self.build_rows(); - let rows_width = Self::rows_width(width); - let rows_height = - measure_rows_height(&rows, self.active_state(), MAX_POPUP_ROWS, rows_width); + let rows_height = measure_rows_height(&rows, self.active_state(), MAX_POPUP_ROWS, width); let docs_height = if self.reset_confirmation.is_some() { 0 @@ -452,18 +479,27 @@ impl Renderable for MemoriesSettingsView { }; let mut height = header.desired_height(width.saturating_sub(4)); height = height.saturating_add(rows_height + 4 + docs_height); - height.saturating_add(1) + height.saturating_add( + super::selection_popup_common::wrap_styled_line( + &self.footer_hint(), + width.saturating_sub(/*rhs*/ 2), + ) + .len() as u16, + ) } } fn memories_settings_hint_line(keymap: &ListKeymap) -> Line<'static> { - let mut spans = vec![ - "Press ".into(), - key_hint::plain(KeyCode::Char(' ')).into(), - " to toggle".into(), - ]; + let mut spans = vec![key_hint::plain(KeyCode::Char(' ')).into(), " toggle".into()]; if let Some(accept) = keymap.primary_hint(ListAction::Accept) { - spans.extend(["; ".into(), accept.into(), " to save or select".into()]); + spans.push(" · ".into()); + spans.extend(accept.spans()); + spans.push(" save/select".into()); + } + if let Some(cancel) = keymap.primary_hint(ListAction::Cancel) { + spans.push(" · ".into()); + spans.extend(cancel.spans()); + spans.push(" cancel".into()); } Line::from(spans) } diff --git a/codex-rs/tui/src/bottom_pane/memories_settings_view_tests.rs b/codex-rs/tui/src/bottom_pane/memories_settings_view_tests.rs index 198ccc1962..21d985fe84 100644 --- a/codex-rs/tui/src/bottom_pane/memories_settings_view_tests.rs +++ b/codex-rs/tui/src/bottom_pane/memories_settings_view_tests.rs @@ -14,7 +14,7 @@ fn reset_confirmation_keeps_both_choices_at_wrap_boundary() { view.open_reset_confirmation(); let mut snapshots = Vec::new(); - for width in [70, 71, 72] { + for width in [68, 69, 70, 71, 72] { let area = Rect::new( /*x*/ 0, /*y*/ 0, diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 61b1c097b9..b197ae2571 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -177,7 +177,9 @@ mod paste_burst; mod pending_input_preview; mod pending_thread_approvals; mod picker_presets; +mod picker_rows; mod picker_style; +pub(crate) use picker_style::selection_style; pub(crate) mod popup_consts; mod scroll_state; mod selection_picker_layout; diff --git a/codex-rs/tui/src/bottom_pane/multi_select_picker.rs b/codex-rs/tui/src/bottom_pane/multi_select_picker.rs index 7f89e332eb..fba6779e79 100644 --- a/codex-rs/tui/src/bottom_pane/multi_select_picker.rs +++ b/codex-rs/tui/src/bottom_pane/multi_select_picker.rs @@ -8,11 +8,13 @@ //! - **Reordering**: Optional left/right arrow support to reorder items //! - **Live preview**: Optional callback to show a preview of current selections //! - **Callbacks**: Hooks for change, confirm, and cancel events +//! - **Picker appearance**: Full-width focus with overflow hints in spacer rows; +//! checkboxes remain independent of the focused row //! //! # Example //! //! ```ignore -//! let picker = MultiSelectPicker::new( +//! let picker = MultiSelectPicker::builder( //! "Select Items".to_string(), //! Some("Choose which items to enable".to_string()), //! app_event_tx, @@ -51,15 +53,21 @@ use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::Block; +use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; +use ratatui::widgets::Wrap; +use super::picker_rows; +use super::picker_style; +use super::selection_popup_common::ColumnWidthConfig; use super::selection_popup_common::GenericDisplayRow; +use super::selection_popup_common::wrap_styled_line; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::CancellationEvent; use crate::bottom_pane::bottom_pane_view::BottomPaneView; use crate::bottom_pane::popup_consts::MAX_POPUP_ROWS; use crate::bottom_pane::scroll_state::ScrollState; -use crate::bottom_pane::selection_popup_common::render_rows_single_line; +use crate::bottom_pane::selection_popup_common::render_rows_single_line_with_col_width_mode; use crate::key_hint; use crate::key_hint::KeyBindingListExt; use crate::key_hint::is_plain_text_key_event; @@ -80,9 +88,6 @@ const ITEM_NAME_TRUNCATE_LEN: usize = 21; /// Placeholder text shown in the search input when empty. const SEARCH_PLACEHOLDER: &str = "Type to search"; -/// Prefix displayed before the search query (mimics a command prompt). -const SEARCH_PROMPT_PREFIX: &str = "> "; - const SECTION_BREAK_ROW: &str = " ───────────────────────"; /// Direction for reordering items in the list. @@ -158,7 +163,7 @@ struct BuiltRows { /// - Press Escape to cancel and close /// - Use Left/Right arrows to reorder items (if ordering is enabled) /// -/// Create instances using the builder pattern via [`MultiSelectPicker::new`]. +/// Create instances using the builder pattern via [`MultiSelectPicker::builder`]. pub(crate) struct MultiSelectPicker { /// All items in the picker (unfiltered). items: Vec, @@ -281,18 +286,19 @@ impl MultiSelectPicker { MAX_POPUP_ROWS.min(len.max(1)) } - /// Calculates the width available for row content (accounts for borders). - fn rows_width(total_width: u16) -> u16 { - total_width.saturating_sub(2) + fn footer_lines(&self, width: u16) -> Vec> { + wrap_styled_line(&self.footer_hint, width.saturating_sub(/*rhs*/ 2)) } /// Calculates the height needed for the row list area. fn rows_height(&self, rows: &BuiltRows) -> u16 { - rows.rows + let height: u16 = rows + .rows .len() - .clamp(1, MAX_POPUP_ROWS) + .clamp(/*min*/ 1, MAX_POPUP_ROWS) .try_into() - .unwrap_or(1) + .unwrap_or(/*default*/ 1); + height.saturating_add(/*rhs*/ 2) } /// Builds the display rows for all currently visible (filtered) items. @@ -313,13 +319,16 @@ impl MultiSelectPicker { let item_name = truncate_text(&item.name, ITEM_NAME_TRUNCATE_LEN); let name = format!("{prefix} [{marker}] {item_name}"); rows.push(GenericDisplayRow { + category_tag: None, name, description: item.description.clone(), + selection_style: Some(picker_style::selection_style()), ..Default::default() }); if item.section_break_after && visible_idx + 1 < self.filtered_indices.len() { rows.push(GenericDisplayRow { + category_tag: None, name: SECTION_BREAK_ROW.to_string(), is_disabled: true, ..Default::default() @@ -601,8 +610,7 @@ impl Renderable for MultiSelectPicker { let mut height = self.header.desired_height(width.saturating_sub(4)); height = height.saturating_add(rows_height + 3); - height = height.saturating_add(2); - height.saturating_add(1 + preview_height) + height.saturating_add(self.footer_lines(width).len() as u16 + preview_height) } fn render(&self, area: Rect, buf: &mut Buffer) { @@ -612,7 +620,9 @@ impl Renderable for MultiSelectPicker { // Reserve the footer line for the key-hint row. let preview_height = if self.preview_line.is_some() { 1 } else { 0 }; - let footer_height = 1 + preview_height; + let hint_lines = self.footer_lines(area.width); + let hint_height = hint_lines.len() as u16; + let footer_height = hint_height + preview_height; let [content_area, footer_area] = Layout::vertical([Constraint::Fill(1), Constraint::Length(footer_height)]).areas(area); @@ -624,33 +634,12 @@ impl Renderable for MultiSelectPicker { .header .desired_height(content_area.width.saturating_sub(4)); let rows = self.build_rows(); - let rows_width = Self::rows_width(content_area.width); let rows_height = self.rows_height(&rows); - let [header_area, _, search_area, list_area] = Layout::vertical([ - Constraint::Max(header_height), - Constraint::Max(1), - Constraint::Length(2), - Constraint::Length(rows_height), - ]) - .areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2))); + let [header_area, search_area, list_area] = + picker_rows::layout(content_area, header_height, /*search*/ 1, rows_height); self.header.render(header_area, buf); - - // Render the search prompt as two lines to mimic the composer. - if search_area.height >= 2 { - let [placeholder_area, input_area] = - Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(search_area); - Line::from(SEARCH_PLACEHOLDER.dim()).render(placeholder_area, buf); - let line = if self.search_query.is_empty() { - Line::from(vec![SEARCH_PROMPT_PREFIX.dim()]) - } else { - Line::from(vec![ - SEARCH_PROMPT_PREFIX.dim(), - self.search_query.clone().into(), - ]) - }; - line.render(input_area, buf); - } else if search_area.height > 0 { + if search_area.height > 0 { let query_span = if self.search_query.is_empty() { SEARCH_PLACEHOLDER.dim() } else { @@ -660,25 +649,23 @@ impl Renderable for MultiSelectPicker { } if list_area.height > 0 { - let render_area = Rect { - x: list_area.x.saturating_sub(2), - y: list_area.y, - width: rows_width.max(1), - height: list_area.height, - }; - render_rows_single_line( - render_area, + let body = list_area.inset(Insets::vh(u16::from(list_area.height >= 3), /*h*/ 0)); + let rendered = render_rows_single_line_with_col_width_mode( + body, buf, &rows.rows, &rows.state, - render_area.height as usize, + usize::from(body.height), "no matches", + ColumnWidthConfig::default(), ); + picker_style::render_scroll_indicators(list_area, buf, rendered); } let hint_area = if let Some(preview_line) = &self.preview_line { let [preview_area, hint_area] = - Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(footer_area); + Layout::vertical([Constraint::Length(1), Constraint::Length(hint_height)]) + .areas(footer_area); let preview_area = Rect { x: preview_area.x + 2, y: preview_area.y, @@ -699,7 +686,7 @@ impl Renderable for MultiSelectPicker { width: hint_area.width.saturating_sub(2), height: hint_area.height, }; - self.footer_hint.clone().dim().render(hint_area, buf); + Paragraph::new(hint_lines).dim().render(hint_area, buf); } } @@ -708,7 +695,7 @@ impl Renderable for MultiSelectPicker { /// # Example /// /// ```ignore -/// let picker = MultiSelectPicker::new("Title".into(), None, tx) +/// let picker = MultiSelectPicker::builder("Title".into(), /*subtitle*/ None, tx) /// .items(items) /// .enable_ordering() /// .on_preview(|items| Some(Line::from("Preview"))) @@ -818,39 +805,33 @@ impl MultiSelectPickerBuilder { /// preview line if a preview callback was set. pub fn build(self) -> MultiSelectPicker { let mut header = ColumnRenderable::new(); - header.push(Line::from(self.title.bold())); - + header.push(Paragraph::new(self.title.bold()).wrap(Wrap { trim: false })); if let Some(subtitle) = self.subtitle { - header.push(Line::from(subtitle.dim())); + header.push(Paragraph::new(subtitle.dim()).wrap(Wrap { trim: false })); } let instructions = if self.instructions.is_empty() { - let mut spans = vec![ - "Press ".into(), - key_hint::plain(KeyCode::Char(' ')).into(), - " to toggle".into(), - ]; + let mut spans = vec![key_hint::plain(KeyCode::Char(' ')).into(), " toggle".dim()]; if self.ordering_enabled - && let (Some(move_left), Some(move_right)) = ( + && let (Some(left), Some(right)) = ( self.keymap.primary_hint(ListAction::MoveLeft), self.keymap.primary_hint(ListAction::MoveRight), ) { - spans.push("; ".into()); - spans.push(move_left.into()); - spans.push("/".into()); - spans.push(move_right.into()); - spans.push(" to move".into()); + spans.push(" · ".dim()); + spans.extend(key_hint::key_label_spans(&format!( + "{}/{}", + left.display_label(), + right.display_label() + ))); + spans.push(" reorder".dim()); } - if let Some(accept) = self.keymap.primary_hint(ListAction::Accept) { - spans.push("; ".into()); - spans.push(accept.into()); - spans.push(" to confirm and close".into()); - } - if let Some(cancel) = self.keymap.primary_hint(ListAction::Cancel) { - spans.push("; ".into()); - spans.push(cancel.into()); - spans.push(" to close".into()); + for (action, label) in [(ListAction::Accept, "save"), (ListAction::Cancel, "cancel")] { + if let Some(hint) = self.keymap.primary_hint(action) { + spans.push(" · ".dim()); + spans.extend(hint.spans()); + spans.push(format!(" {label}").dim()); + } } spans } else { @@ -1096,4 +1077,110 @@ mod tests { picker.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)); assert_eq!(picker.state.selected_idx, Some(0)); } + + #[test] + fn short_picker_preserves_selected_row_when_overflow_spacers_yield() { + let mut picker = test_picker( + (0..12) + .map(|index| { + item( + &format!("field-{index:02}"), + /*orderable*/ true, + /*section_break_after*/ false, + ) + }) + .collect(), + ); + picker.header = Box::new(()); + picker.jump_bottom(); + let mut snapshots = Vec::new(); + for height in 1..=7 { + let area = Rect::new(/*x*/ 0, /*y*/ 0, /*width*/ 80, height); + let mut buf = Buffer::empty(area); + picker.render(area, &mut buf); + let text = buf + .content + .chunks(usize::from(area.width)) + .map(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + .trim_end() + .to_string() + }) + .collect::>() + .join("\n"); + if height >= 4 { + assert!(text.contains("› [ ] field-11"), "{text}"); + } + assert_eq!(picker.state.selected_idx, Some(11)); + snapshots.push(format!("height={height}\n{text}")); + } + picker.toggle_selected(); + assert_eq!( + picker + .items + .iter() + .filter(|item| item.enabled) + .map(|item| item.id.as_str()) + .collect::>(), + ["field-11"] + ); + insta::assert_snapshot!(snapshots.join("\n\n")); + } + + #[test] + fn picker_appearance_renders_checkboxes_preview_and_overflow() { + let (tx, _rx) = unbounded_channel::(); + let mut picker = MultiSelectPicker::builder( + "Status line".to_string(), + /*subtitle*/ None, + AppEventSender::new(tx), + ) + .items( + (0..12) + .map(|index| MultiSelectItem { + id: format!("field-{index}"), + name: format!("Field {index}"), + description: Some("Status detail".to_string()), + enabled: index == 0, + section_break_after: index == 0, + ..Default::default() + }) + .collect(), + ) + .enable_ordering() + .on_preview(|items| { + Some(Line::from( + items + .iter() + .filter(|item| item.enabled) + .map(|item| item.name.as_str()) + .collect::>() + .join(" · "), + )) + }) + .build(); + let render = |picker: &MultiSelectPicker| { + let area = Rect::new( + /*x*/ 0, + /*y*/ 0, + /*width*/ 48, + picker.desired_height(/*width*/ 48), + ); + let mut buf = Buffer::empty(area); + picker.render(area, &mut buf); + buf + }; + let first = render(&picker); + picker.jump_bottom(); + picker.toggle_selected(); + picker.move_selected_item(Direction::Up); + let last = render(&picker); + picker.handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)); + let empty = render(&picker); + insta::assert_snapshot!(format!( + "First page:\n{first:?}\nLast page:\n{last:?}\nNo matches:\n{empty:?}" + )); + } } diff --git a/codex-rs/tui/src/bottom_pane/picker_rows.rs b/codex-rs/tui/src/bottom_pane/picker_rows.rs new file mode 100644 index 0000000000..bb23c85f10 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/picker_rows.rs @@ -0,0 +1,108 @@ +//! Shared row presentation for dedicated picker workflows. +//! +//! These adapters keep domain-specific state and save policies with their views. +//! Measurement and painting share columns, wrapping, and overflow spacer rows. + +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; + +use super::picker_style; +use super::scroll_state::ScrollState; +use super::selection_picker_layout::PickerLayoutSizes; +use super::selection_picker_layout::picker_areas; +use super::selection_popup_common as rows; +use super::selection_popup_common::ColumnWidthConfig; +use super::selection_popup_common::ColumnWidthMode; +use super::selection_popup_common::GenericDisplayRow; +use super::selection_row_layout::SelectionDescriptionLayout; +use crate::render::Insets; +use crate::render::RectExt; + +const COLUMNS: ColumnWidthConfig = ColumnWidthConfig { + mode: ColumnWidthMode::AutoAllRows, + name_column_width: None, + description_layout: SelectionDescriptionLayout::HideWhenNarrow { + min_description_width: 24, + }, +}; + +pub(super) fn measure_rows_height( + rows: &[GenericDisplayRow], + state: &ScrollState, + max_results: usize, + width: u16, +) -> u16 { + rows::measure_rows_height_with_col_width_mode(rows, state, max_results, width, COLUMNS) + .saturating_add(/*rhs*/ 2) +} + +pub(super) fn render_rows( + area: Rect, + buf: &mut Buffer, + rows: &[GenericDisplayRow], + state: &ScrollState, + max_results: usize, + empty_message: &str, +) -> u16 { + let body = area.inset(Insets::vh(u16::from(area.height >= 3), /*h*/ 0)); + let rendered = rows::render_rows_with_col_width_mode( + body, + buf, + rows, + state, + max_results, + empty_message, + COLUMNS, + ); + picker_style::render_scroll_indicators(area, buf, rendered); + rendered.lines.saturating_add(/*rhs*/ 2).min(area.height) +} + +pub(super) fn render_rows_single_line( + area: Rect, + buf: &mut Buffer, + rows: &[GenericDisplayRow], + state: &ScrollState, + max_results: usize, + empty_message: &str, +) -> u16 { + let body = area.inset(Insets::vh(u16::from(area.height >= 3), /*h*/ 0)); + let rendered = rows::render_rows_single_line_with_col_width_mode( + body, + buf, + rows, + state, + max_results, + empty_message, + COLUMNS, + ); + picker_style::render_scroll_indicators(area, buf, rendered); + rendered.lines.saturating_add(/*rhs*/ 2).min(area.height) +} + +/// Keep headers and search above a list, yielding optional gaps on short screens. +pub(super) fn layout(area: Rect, header: u16, search: u16, rows: u16) -> [Rect; 3] { + let [header, _, _, _, search, above, rows, below, _, _] = picker_areas( + area.inset(Insets::tlbr( + /*top*/ 1, /*left*/ 2, /*bottom*/ 0, /*right*/ 2, + )), + PickerLayoutSizes { + header, + header_gap: 1, + tabs: 0, + search, + rows: rows.saturating_sub(/*rhs*/ 2), + side: 0, + }, + ); + [ + header, + search, + Rect::new( + area.x, + above.y, + area.width, + above.height + rows.height + below.height, + ), + ] +} diff --git a/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs b/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs index d4844dd038..d1532d44a9 100644 --- a/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs +++ b/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs @@ -9,7 +9,9 @@ use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Block; +use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; +use ratatui::widgets::Wrap; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; @@ -19,8 +21,6 @@ use crate::key_hint::ShortcutHint; use crate::key_hint::is_plain_text_key_event; use crate::keymap::ListAction; use crate::keymap::ListKeymap; -use crate::render::Insets; -use crate::render::RectExt as _; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; use crate::skills_helpers::match_skill; @@ -28,13 +28,12 @@ use crate::style::user_message_style; use super::CancellationEvent; use super::bottom_pane_view::BottomPaneView; +use super::picker_rows::render_rows_single_line; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::GenericDisplayRow; -use super::selection_popup_common::render_rows_single_line; const SEARCH_PLACEHOLDER: &str = "Type to search skills"; -const SEARCH_PROMPT_PREFIX: &str = "> "; pub(crate) struct SkillsToggleItem { pub name: String, @@ -63,10 +62,15 @@ impl SkillsToggleView { keymap: ListKeymap, ) -> Self { let mut header = ColumnRenderable::new(); - header.push(Line::from("Enable/Disable Skills".bold())); - header.push(Line::from( - "Turn skills on or off. Your changes are saved automatically.".dim(), - )); + header.push( + Paragraph::new(Line::from("Enable/Disable Skills".bold())).wrap(Wrap { trim: false }), + ); + header.push( + Paragraph::new(Line::from( + "Turn skills on or off. Your changes are saved automatically.".dim(), + )) + .wrap(Wrap { trim: false }), + ); let mut view = Self { items, @@ -148,6 +152,8 @@ impl SkillsToggleView { let item_name = &item.name; let name = format!("{prefix} [{marker}] {item_name}"); GenericDisplayRow { + category_tag: None, + selection_style: Some(super::picker_style::selection_style()), name, description: Some(item.description.clone()), ..Default::default() @@ -223,12 +229,8 @@ impl SkillsToggleView { .list_skills(Vec::new(), /*force_reload*/ true); } - fn rows_width(total_width: u16) -> u16 { - total_width.saturating_sub(2) - } - fn rows_height(&self, rows: &[GenericDisplayRow]) -> u16 { - rows.len().clamp(1, MAX_POPUP_ROWS).try_into().unwrap_or(1) + (rows.len().clamp(/*min*/ 1, MAX_POPUP_ROWS) as u16).saturating_add(/*rhs*/ 2) } } @@ -309,7 +311,13 @@ impl Renderable for SkillsToggleView { let mut height = self.header.desired_height(width.saturating_sub(4)); height = height.saturating_add(rows_height + 3); height = height.saturating_add(2); - height.saturating_add(1) + height.saturating_add( + super::selection_popup_common::wrap_styled_line( + &self.footer_hint, + width.saturating_sub(/*rhs*/ 2), + ) + .len() as u16, + ) } fn render(&self, area: Rect, buf: &mut Buffer) { @@ -317,9 +325,15 @@ impl Renderable for SkillsToggleView { return; } - // Reserve the footer line for the key-hint row. - let [content_area, footer_area] = - Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area); + let hint_lines = super::selection_popup_common::wrap_styled_line( + &self.footer_hint, + area.width.saturating_sub(/*rhs*/ 2), + ); + let [content_area, footer_area] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(hint_lines.len() as u16), + ]) + .areas(area); Block::default() .style(user_message_style()) @@ -329,33 +343,13 @@ impl Renderable for SkillsToggleView { .header .desired_height(content_area.width.saturating_sub(4)); let rows = self.build_rows(); - let rows_width = Self::rows_width(content_area.width); let rows_height = self.rows_height(&rows); - let [header_area, _, search_area, list_area] = Layout::vertical([ - Constraint::Max(header_height), - Constraint::Max(1), - Constraint::Length(2), - Constraint::Length(rows_height), - ]) - .areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2))); + let [header_area, search_area, render_area] = + super::picker_rows::layout(content_area, header_height, /*search*/ 1, rows_height); self.header.render(header_area, buf); - // Render the search prompt as two lines to mimic the composer. - if search_area.height >= 2 { - let [placeholder_area, input_area] = - Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(search_area); - Line::from(SEARCH_PLACEHOLDER.dim()).render(placeholder_area, buf); - let line = if self.search_query.is_empty() { - Line::from(vec![SEARCH_PROMPT_PREFIX.dim()]) - } else { - Line::from(vec![ - SEARCH_PROMPT_PREFIX.dim(), - self.search_query.clone().into(), - ]) - }; - line.render(input_area, buf); - } else if search_area.height > 0 { + if search_area.height > 0 { let query_span = if self.search_query.is_empty() { SEARCH_PLACEHOLDER.dim() } else { @@ -364,13 +358,7 @@ impl Renderable for SkillsToggleView { Line::from(query_span).render(search_area, buf); } - if list_area.height > 0 { - let render_area = Rect { - x: list_area.x.saturating_sub(2), - y: list_area.y, - width: rows_width.max(1), - height: list_area.height, - }; + if render_area.height > 0 { render_rows_single_line( render_area, buf, @@ -387,7 +375,7 @@ impl Renderable for SkillsToggleView { width: footer_area.width.saturating_sub(2), height: footer_area.height, }; - self.footer_hint.clone().dim().render(hint_area, buf); + Paragraph::new(hint_lines).dim().render(hint_area, buf); } } @@ -398,32 +386,19 @@ fn skills_toggle_hint_line(keymap: &ListKeymap) -> Line<'static> { .filter(|binding| *binding != ShortcutHint::Single(space)); let cancel = keymap.primary_hint(ListAction::Cancel); - match (accept, cancel) { - (Some(accept), Some(cancel)) => Line::from(vec![ - "Press ".into(), - space.into(), - " or ".into(), - accept.into(), - " to toggle; ".into(), - cancel.into(), - " to close".into(), - ]), - (Some(accept), None) => Line::from(vec![ - "Press ".into(), - space.into(), - " or ".into(), - accept.into(), - " to toggle".into(), - ]), - (None, Some(cancel)) => Line::from(vec![ - "Press ".into(), - space.into(), - " to toggle; ".into(), - cancel.into(), - " to close".into(), - ]), - (None, None) => Line::from(vec!["Press ".into(), space.into(), " to toggle".into()]), + let mut toggle = space.display_label(); + if let Some(accept) = accept { + toggle.push('/'); + toggle.push_str(&accept.display_label()); } + let mut spans = key_hint::key_label_spans(&toggle); + spans.push(" toggle".dim()); + if let Some(cancel) = cancel { + spans.push(" · ".dim()); + spans.extend(cancel.spans()); + spans.push(" close".dim()); + } + spans.into() } #[cfg(test)] diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_auth_suggestion_with_reason.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_auth_suggestion_with_reason.snap index 0cf228c129..f908a1cb89 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_auth_suggestion_with_reason.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_auth_suggestion_with_reason.snap @@ -13,6 +13,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72)) Sign in to this app in your browser, then return here. - › 1. Open sign-in URL - 2. Back - Use tab / ↑ ↓ to move, enter to select, esc to close +› 1. Open sign-in URL + 2. Back + tab / ↑ ↓ move · enter select · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_enable_suggestion_with_reason.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_enable_suggestion_with_reason.snap index 94980ff650..270505dcdf 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_enable_suggestion_with_reason.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_enable_suggestion_with_reason.snap @@ -14,7 +14,7 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72)) Newly installed apps can take a few minutes to appear in /apps. - › 1. Manage on ChatGPT - 2. Enable app - 3. Back - Use tab / ↑ ↓ to move, enter to select, esc to close +› 1. Manage on ChatGPT + 2. Enable app + 3. Back + tab / ↑ ↓ move · enter select · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation.snap index ec95ab96fa..540d511551 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation.snap @@ -14,6 +14,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72)) Complete the requested action in your browser, then return here. - › 1. Open link - 2. Back - Use tab / ↑ ↓ to move, enter to select, esc to close +› 1. Open link + 2. Back + tab / ↑ ↓ move · enter select · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation_confirmation.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation_confirmation.snap index 14236e2e76..d31381dce5 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation_confirmation.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_generic_url_elicitation_confirmation.snap @@ -12,6 +12,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72)) Link: https://payments.example/checkout/123 - › 1. I finished - 2. Back - Use tab / ↑ ↓ to move, enter to select, esc to close +› 1. I finished + 2. Back + tab / ↑ ↓ move · enter select · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_install_suggestion_with_reason.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_install_suggestion_with_reason.snap index ac47f87418..65f6bc42f5 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_install_suggestion_with_reason.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__app_link_view__tests__app_link_view_install_suggestion_with_reason.snap @@ -13,6 +13,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72)) After installed, use $ to insert this app into the prompt. - › 1. Install on ChatGPT - 2. Back - Use tab / ↑ ↓ to move, enter to select, esc to close +› 1. Install on ChatGPT + 2. Back + tab / ↑ ↓ move · enter select · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__daemon_auto_start_menu.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__daemon_auto_start_menu.snap index 7627e249b9..7532e9c198 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__daemon_auto_start_menu.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__daemon_auto_start_menu.snap @@ -2,13 +2,16 @@ source: tui/src/bottom_pane/experimental_features_view_tests.rs expression: buffer_text(&buffer) --- + Experimental features Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. -› [ ] Automatically start the background server Use the shared local server - for new, resumed, and forked + +› [ ] Automatically start the background server Use the shared local server for + new, resumed, and forked sessions. Takes effect next launch. - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_analytics_plan_history.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_analytics_plan_history.snap index 048354f30c..2ac57b83a8 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_analytics_plan_history.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_analytics_plan_history.snap @@ -7,7 +7,9 @@ expression: buffer_text(&buffer) Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. -› [ ] Analytics plan history Preview five-hour and weekly allowance history - for consumer accounts in /analytics. - Press space to select or enter to save +› [ ] Analytics plan history Preview five-hour and weekly allowance history for + consumer accounts in /analytics. + + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_changed_default.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_changed_default.snap index 905d975203..4ad81a1304 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_changed_default.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_changed_default.snap @@ -7,6 +7,8 @@ expression: buffer_text(&buffer) Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. + › [x] Server network_proxy Description supplied by the server. - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_empty.snap index f3d09a6abd..e0a9047c4a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_empty.snap @@ -8,6 +8,8 @@ expression: buffer_text(&buffer) only in new tasks or after restarting the Codex server. No server experiments available. + No experimental features available for now - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_keep_following_choice_at_wrap_boundary.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_keep_following_choice_at_wrap_boundary.snap index 39750c5915..9bf7859783 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_keep_following_choice_at_wrap_boundary.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_keep_following_choice_at_wrap_boundary.snap @@ -2,6 +2,36 @@ source: tui/src/bottom_pane/experimental_features_view_tests.rs expression: "snapshots.join(\"\\n\\n\")" --- +width=68 + + Experimental features + Checked features are configured on. Some experimental features + take effect only in new tasks or after restarting the Codex + server. + + +› [x] First feature A description that fits exactly at this + boundary. + [ ] Last feature Keep this choice visible. + + + space toggle · enter save · esc save/close + +width=69 + + Experimental features + Checked features are configured on. Some experimental features + take effect only in new tasks or after restarting the Codex + server. + + +› [x] First feature A description that fits exactly at this + boundary. + [ ] Last feature Keep this choice visible. + + + space toggle · enter save · esc save/close + width=70 Experimental features @@ -9,11 +39,12 @@ width=70 take effect only in new tasks or after restarting the Codex server. -› [x] First feature A description that fits exactly at this - boundary. + +› [x] First feature A description that fits exactly at this boundary. [ ] Last feature Keep this choice visible. - Press space to select or enter to save + + space toggle · enter save · esc save/close width=71 @@ -21,11 +52,12 @@ width=71 Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. -› [x] First feature A description that fits exactly at this - boundary. + +› [x] First feature A description that fits exactly at this boundary. [ ] Last feature Keep this choice visible. - Press space to select or enter to save + + space toggle · enter save · esc save/close width=72 @@ -33,7 +65,9 @@ width=72 Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. + › [x] First feature A description that fits exactly at this boundary. [ ] Last feature Keep this choice visible. - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_save_failed.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_save_failed.snap index f529b54d80..1eab5f8c24 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_save_failed.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_save_failed.snap @@ -8,8 +8,10 @@ expression: buffer_text(&buffer) only in new tasks or after restarting the Codex server. Saved but readback failed + › [ ] Server network_proxy Description supplied by the server. [ ] Server future_feature Description supplied by the server. [ ] Server prevent_idle_sleep Description supplied by the server. + Selections retained. Save to retry, or cancel to close. diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_saving.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_saving.snap index 3b24a44fa5..3e35b16993 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_saving.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_saving.snap @@ -8,8 +8,10 @@ expression: buffer_text(&buffer) only in new tasks or after restarting the Codex server. Saving experimental features… + › [ ] Server network_proxy Description supplied by the server. [ ] Server future_feature Description supplied by the server. [ ] Server prevent_idle_sleep Description supplied by the server. + Saving… Closing this popup will not cancel the write. diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_server_discovery.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_server_discovery.snap index 0b35d3e6de..1c0a7e3799 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_server_discovery.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_server_discovery.snap @@ -7,8 +7,10 @@ expression: buffer_text(&buffer) Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. + › [x] Server network_proxy Description supplied by the server. [x] Server future_feature Description supplied by the server. [x] Server prevent_idle_sleep Description supplied by the server. - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_unavailable.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_unavailable.snap index fb446fea9b..1bd7296b47 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_unavailable.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_unavailable.snap @@ -9,6 +9,8 @@ expression: buffer_text(&buffer) Server experiments unavailable. Reopen /experimental to retry; restart this Codex client if requests remain unanswered. + No experimental features available for now - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap index b1159ac9ff..34de22d1e4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap @@ -1,6 +1,5 @@ --- source: tui/src/bottom_pane/experimental_features_view_tests.rs -assertion_line: 208 expression: buffer_text(&buffer) --- @@ -8,6 +7,8 @@ expression: buffer_text(&buffer) Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. + › [x] Server realtime_conversation Description supplied by the server. - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_unavailable.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_unavailable.snap index 12654f5dc0..e0a9047c4a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_unavailable.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_unavailable.snap @@ -1,6 +1,5 @@ --- source: tui/src/bottom_pane/experimental_features_view_tests.rs -assertion_line: 208 expression: buffer_text(&buffer) --- @@ -9,6 +8,8 @@ expression: buffer_text(&buffer) only in new tasks or after restarting the Codex server. No server experiments available. + No experimental features available for now - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__compact_hooks_keep_the_last_event_and_selection_visible.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__compact_hooks_keep_the_last_event_and_selection_visible.snap new file mode 100644 index 0000000000..41ad692c00 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__compact_hooks_keep_the_last_event_and_selection_visible.snap @@ -0,0 +1,20 @@ +--- +source: tui/src/bottom_pane/hooks_browser_view.rs +expression: text +--- + + Hooks + Lifecycle hooks from config and + enabled plugins. + + Event Active/Installed +↑ + SessionEnd 0/0 + UserPromptSubmit 0/0 + SubagentStart 0/0 + SubagentStop 0/0 + Stop 0/0 +› Interrupt 0/0 + + + enter details · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap index 5df46276dc..48bbf99787 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap @@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)" PreToolUse hooks Turn hooks on or off. Your changes are saved automatically. - [x] Hook 1 +› [x] Hook 1 Event PreToolUse Matcher Bash @@ -17,4 +17,5 @@ expression: "render_lines(&view, 112)" Context unlimited Trust Trusted - Press space or enter to toggle; esc to go back + + space/enter toggle · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap index 6597de15be..e630643ea1 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap @@ -4,9 +4,10 @@ expression: "render_lines(&view, 44)" --- PreToolUse hooks - Turn hooks on or off. Your changes are s + Turn hooks on or off. Your changes are + saved automatically. - [x] Hook 1 +› [x] Hook 1 Event PreToolUse Matcher Bash @@ -18,4 +19,5 @@ expression: "render_lines(&view, 44)" Timeout 30s Trust Trusted - Press space or enter to toggle; esc to go + + space/enter toggle · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap index 33321eec2b..b749a4c2e5 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap @@ -5,7 +5,8 @@ expression: "render_lines(&view, 112)" PermissionRequest hooks Turn hooks on or off. Your changes are saved automatically. - No hooks installed for this event. - Press esc to go back + + + esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap index 4e7fb4917d..af194e902f 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap @@ -7,7 +7,8 @@ expression: "render_lines(&view, 112)" Lifecycle hooks from config and enabled plugins. Event Installed Active Description - PreToolUse 2 1 Before a tool executes + +› PreToolUse 2 1 Before a tool executes PermissionRequest 1 1 When permission is requested PostToolUse 0 0 After a tool executes PreCompact 0 0 Before context compaction @@ -15,9 +16,6 @@ expression: "render_lines(&view, 112)" SessionStart 0 0 When a new session starts SessionEnd 0 0 Right before a session ends UserPromptSubmit 0 0 When the user submits a prompt - SubagentStart 0 0 When a subagent is created - SubagentStop 0 0 Right before a subagent ends its turn - Stop 0 0 Right before Codex ends its turn - Interrupt 0 0 Right before an interrupted turn is aborted +↓ - Press enter to view hooks; esc to close + enter details · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap index 81762244d8..d5fb7a86ec 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap @@ -11,7 +11,8 @@ expression: "render_lines(&view, 112)" ■ /tmp/hooks.json: failed to parse hooks config Event Installed Active Description - PreToolUse 0 0 Before a tool executes + +› PreToolUse 0 0 Before a tool executes PermissionRequest 0 0 When permission is requested PostToolUse 0 0 After a tool executes PreCompact 0 0 Before context compaction @@ -19,9 +20,6 @@ expression: "render_lines(&view, 112)" SessionStart 0 0 When a new session starts SessionEnd 0 0 Right before a session ends UserPromptSubmit 0 0 When the user submits a prompt - SubagentStart 0 0 When a subagent is created - SubagentStop 0 0 Right before a subagent ends its turn - Stop 0 0 Right before Codex ends its turn - Interrupt 0 0 Right before an interrupted turn is aborted +↓ - Press enter to view hooks; esc to close + enter details · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_review_column.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_review_column.snap index 4ad2307aff..0149313756 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_review_column.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_review_column.snap @@ -9,7 +9,8 @@ expression: "render_lines(&view, 112)" ⚠ 1 hook needs review before it can run. Event Installed Active Review Description - PreToolUse 1 0 1 Before a tool executes + +› PreToolUse 1 0 1 Before a tool executes PermissionRequest 0 0 0 When permission is requested PostToolUse 0 0 0 After a tool executes PreCompact 0 0 0 Before context compaction @@ -17,9 +18,6 @@ expression: "render_lines(&view, 112)" SessionStart 0 0 0 When a new session starts SessionEnd 0 0 0 Right before a session ends UserPromptSubmit 0 0 0 When the user submits a prompt - SubagentStart 0 0 0 When a subagent is created - SubagentStop 0 0 0 Right before a subagent ends its turn - Stop 0 0 0 Right before Codex ends its turn - Interrupt 0 0 0 Right before an interrupted turn is aborted +↓ - Press t to trust all; enter to review hooks; esc to close + t trust all · enter review · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap index 53998ecf72..7da3c9e7c4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap @@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)" PreToolUse hooks Turn hooks on or off. Your changes are saved automatically. - [x] Hook 1 +› [x] Hook 1 [ ] Hook 2 Event PreToolUse @@ -17,4 +17,5 @@ expression: "render_lines(&view, 112)" Timeout 30s Trust Trusted - Press space or enter to toggle; esc to go back + + space/enter toggle · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap index 646741c283..e3d9d9a242 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap @@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)" PermissionRequest hooks Turn hooks on or off. Your changes are saved automatically. - [x] Hook 1 +› [x] Hook 1 Event PermissionRequest Matcher Bash @@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)" Timeout 30s Trust Managed - Managed hooks are always on; press esc to go back + + Managed (always on) · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_mcp_tool_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_mcp_tool_handler.snap index e3ba7d099c..e9374bf510 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_mcp_tool_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_mcp_tool_handler.snap @@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)" PreToolUse hooks Turn hooks on or off. Your changes are saved automatically. - [x] Hook 1 +› [x] Hook 1 Event PreToolUse Matcher Bash @@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)" Timeout 30s Trust Trusted - Press space or enter to toggle; esc to go back + + space/enter toggle · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_review_needed_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_review_needed_handler.snap index f08bb8cc81..2d86de75c6 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_review_needed_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_review_needed_handler.snap @@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)" PreToolUse hooks 1 hook needs review before it can run. - [!] Hook 1 · new +› [!] Hook 1 · new Event PreToolUse Matcher Bash @@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)" Timeout 30s Trust New hook - review required - Press t to trust; esc to go back + + t trust · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap index 5622c6d188..8b807f9f0e 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap @@ -5,7 +5,7 @@ expression: "render_lines(&view, 112)" PreToolUse hooks Turn hooks on or off. Your changes are saved automatically. - +↑ [x] Hook 2 [x] Hook 3 [x] Hook 4 @@ -13,7 +13,7 @@ expression: "render_lines(&view, 112)" [x] Hook 6 [x] Hook 7 [x] Hook 8 - [x] Hook 9 +› [x] Hook 9 Event PreToolUse Matcher Bash @@ -23,4 +23,5 @@ expression: "render_lines(&view, 112)" Timeout 30s Trust Trusted - Press space or enter to toggle; esc to go back + + space/enter toggle · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap index 3884c083b0..9023c899d7 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap @@ -7,7 +7,7 @@ expression: "render_lines(&view, 112)" Turn hooks on or off. Your changes are saved automatically. [x] Hook 1 - [x] Hook 2 +› [x] Hook 2 Event PreToolUse Matcher Bash @@ -17,4 +17,5 @@ expression: "render_lines(&view, 112)" Timeout 30s Trust Managed - Managed hooks are always on; press esc to go back + + Managed (always on) · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap index ffd59c8e08..c9b8cdb97a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap @@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)" PreToolUse hooks 1 hook needs review before it can run. - [!] Hook 1 · new +› [!] Hook 1 · new Event PreToolUse Matcher Bash @@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)" Timeout 30s Trust New hook - review required - Press t to trust; esc to go back + + t trust · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__memories_settings_view__tests__reset_confirmation_keeps_both_choices_at_wrap_boundary.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__memories_settings_view__tests__reset_confirmation_keeps_both_choices_at_wrap_boundary.snap index a94435ea9b..9bd31d9e83 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__memories_settings_view__tests__reset_confirmation_keeps_both_choices_at_wrap_boundary.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__memories_settings_view__tests__reset_confirmation_keeps_both_choices_at_wrap_boundary.snap @@ -2,37 +2,74 @@ source: tui/src/bottom_pane/memories_settings_view_tests.rs expression: "snapshots.join(\"\\n\\n\")" --- +width=68 + + Reset all memories? + This clears local memory files and rollout summaries for the + current Codex home. + + +› Reset all memories Delete local memory files and rollout + summaries. + Go back Return to memory settings. + + + + enter select · esc back + +width=69 + + Reset all memories? + This clears local memory files and rollout summaries for the + current Codex home. + + +› Reset all memories Delete local memory files and rollout + summaries. + Go back Return to memory settings. + + + + enter select · esc back + width=70 Reset all memories? + This clears local memory files and rollout summaries for the + current Codex home. -› Reset all memories Delete local memory files and rollout - summaries. - Go back Return to memory settings. - - - - Press enter to confirm or esc to go back - -width=71 - - Reset all memories? - -› Reset all memories Delete local memory files and rollout - summaries. - Go back Return to memory settings. - - - - Press enter to confirm or esc to go back - -width=72 - - Reset all memories? › Reset all memories Delete local memory files and rollout summaries. Go back Return to memory settings. - Press enter to confirm or esc to go back + enter select · esc back + +width=71 + + Reset all memories? + This clears local memory files and rollout summaries for the + current Codex home. + + +› Reset all memories Delete local memory files and rollout summaries. + Go back Return to memory settings. + + + + enter select · esc back + +width=72 + + Reset all memories? + This clears local memory files and rollout summaries for the current + Codex home. + + +› Reset all memories Delete local memory files and rollout summaries. + Go back Return to memory settings. + + + + enter select · esc back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__multi_select_picker__tests__picker_appearance_renders_checkboxes_preview_and_overflow.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__multi_select_picker__tests__picker_appearance_renders_checkboxes_preview_and_overflow.snap new file mode 100644 index 0000000000..ecfb407d5b --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__multi_select_picker__tests__picker_appearance_renders_checkboxes_preview_and_overflow.snap @@ -0,0 +1,152 @@ +--- +source: tui/src/bottom_pane/multi_select_picker.rs +expression: "format!(\"First page:\\n{first:?}\\nLast page:\\n{last:?}\\nNo matches:\\n{empty:?}\")" +--- +First page: +Buffer { + area: Rect { x: 0, y: 0, width: 48, height: 17 }, + content: [ + " ", + " Status line ", + " ", + " Type to search ", + " ", + "› [x] Field 0 Status detail ", + " ─────────────────────── ", + " [ ] Field 1 Status detail ", + " [ ] Field 2 Status detail ", + " [ ] Field 3 Status detail ", + " [ ] Field 4 Status detail ", + " [ ] Field 5 Status detail ", + " [ ] Field 6 Status detail ", + "↓ ", + " Field 0 ", + " space toggle · ←/→ reorder · enter save · esc ", + " cancel ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 16, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 27, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 40, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 25, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 27, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 40, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 27, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 40, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 27, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 40, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 27, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 40, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 27, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 40, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 27, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 40, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 31, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 36, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 44, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 47, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 0, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + ] +} +Last page: +Buffer { + area: Rect { x: 0, y: 0, width: 48, height: 17 }, + content: [ + " ", + " Status line ", + " ", + " Type to search ", + "↑ ", + " [ ] Field 4 Status detail ", + " [ ] Field 5 Status detail ", + " [ ] Field 6 Status detail ", + " [ ] Field 7 Status detail ", + " [ ] Field 8 Status detail ", + " [ ] Field 9 Status detail ", + "› [x] Field 11 Status detail ", + " [ ] Field 10 Status detail ", + " ", + " Field 0 · Field 11 ", + " space toggle · ←/→ reorder · enter save · esc ", + " cancel ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 16, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 16, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 29, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 16, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 29, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 16, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 29, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 16, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 29, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 16, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 29, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 16, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 29, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 16, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 29, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 16, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 29, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 31, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 36, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 44, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 47, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 0, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + ] +} +No matches: +Buffer { + area: Rect { x: 0, y: 0, width: 48, height: 10 }, + content: [ + " ", + " Status line ", + " ", + " z ", + " ", + "no matches ", + " ", + " Field 0 · Field 11 ", + " space toggle · ←/→ reorder · enter save · esc ", + " cancel ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC, + x: 10, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 31, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 36, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 44, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 47, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 0, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + ] +} diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__multi_select_picker__tests__short_picker_preserves_selected_row_when_overflow_spacers_yield.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__multi_select_picker__tests__short_picker_preserves_selected_row_when_overflow_spacers_yield.snap new file mode 100644 index 0000000000..c4587a40da --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__multi_select_picker__tests__short_picker_preserves_selected_row_when_overflow_spacers_yield.snap @@ -0,0 +1,45 @@ +--- +source: tui/src/bottom_pane/multi_select_picker.rs +expression: "snapshots.join(\"\\n\\n\")" +--- +height=1 + space toggle · ←/→ reorder · enter save · esc cancel + +height=2 + + space toggle · ←/→ reorder · enter save · esc cancel + +height=3 + + Type to search + space toggle · ←/→ reorder · enter save · esc cancel + +height=4 + + Type to search +› [ ] field-11 + space toggle · ←/→ reorder · enter save · esc cancel + +height=5 + + Type to search + [ ] field-10 +› [ ] field-11 + space toggle · ←/→ reorder · enter save · esc cancel + +height=6 + + Type to search +↑ +› [ ] field-11 + + space toggle · ←/→ reorder · enter save · esc cancel + +height=7 + + Type to search +↑ + [ ] field-10 +› [ ] field-11 + + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_basic.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_basic.snap index e5984581a4..76b91ee8fd 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_basic.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_basic.snap @@ -7,8 +7,10 @@ expression: "render_lines(&view, 72)" Turn skills on or off. Your changes are saved automatically. Type to search skills - > + › [x] Repo Scout Summarize the repo layout [ ] Changelog Writer Draft release notes - Press space or enter to toggle; esc to close + + + space/enter toggle · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_at_narrow_width.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_at_narrow_width.snap index d2e4dc3c5f..30a6bf0b97 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_at_narrow_width.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_at_narrow_width.snap @@ -1,14 +1,18 @@ --- source: tui/src/bottom_pane/skills_toggle_view.rs +assertion_line: 569 expression: "render_lines(&view, 48)" --- Enable/Disable Skills - Turn skills on or off. Your changes are save + Turn skills on or off. Your changes are + saved automatically. Type to search skills - > -› [x] superpowers-systematic-d… Find root cau… - [ ] superpowers-verification… Verify comple… - Press space or enter to toggle; esc to close +› [x] superpowers-systematic-debugging (polish) + [ ] superpowers-verification-before-completio… + + + + space/enter toggle · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_use_available_width.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_use_available_width.snap index e2f2f67a7c..a6aaf9c259 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_use_available_width.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skills_toggle_view__tests__skills_toggle_long_names_use_available_width.snap @@ -7,8 +7,10 @@ expression: "render_lines(&view, 96)" Turn skills on or off. Your changes are saved automatically. Type to search skills - > -› [x] superpowers-systematic-debugging (polish) Find root causes before fixing bugs - [ ] superpowers-verification-before-completion (polish) Verify completion before claiming … - Press space or enter to toggle; esc to close +› [x] superpowers-systematic-debugging (polish) Find root causes before fixing bugs + [ ] superpowers-verification-before-completion (polish) Verify completion before claiming su… + + + + space/enter toggle · esc close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_includes_hostname.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_includes_hostname.snap index 48a0a8a176..0385e0fb3c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_includes_hostname.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_includes_hostname.snap @@ -7,7 +7,7 @@ expression: "render_lines(&view,\n100).lines().map(str::trim_end).collect:: + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── [x] hostname Current machine hostname (omitted when unavailable) @@ -16,6 +16,6 @@ expression: "render_lines(&view,\n100).lines().map(str::trim_end).collect:: + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── - [x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitte… - [x] estimated-thread-cost Estimated current-thread cost in USD (Enterprise workspaces only; om… + [x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitted … + [x] estimated-thread-cost Estimated current-thread cost in USD (Enterprise workspaces only; omit… [ ] model Current model name [ ] model-with-reasoning Current model name with reasoning level [ ] reasoning Current reasoning level [ ] current-dir Current working directory - +↓ 5.2 credits · ~$1.82 - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap index 132ce3dd23..9a79c172f0 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap @@ -7,15 +7,15 @@ expression: "render_lines(&view, 72)" Select which items to display in the status line. Type to search - > + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── [x] model Current model name [x] current-dir Current working directory - [x] git-branch Current Git branch (omitted when unavaila… + [x] git-branch Current Git branch (omitted when unavailabl… [ ] model-with-reasoning Current model name with reasoning level [ ] reasoning Current reasoning level [ ] project-name Project name (omitted when unavailable) - +↓ gpt-5-codex · ~/codex-rs · jif/statusline-preview - Press space to toggle; ←/→ to move; enter to confirm and close; esc to + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap index dfdcbdf887..f4a7068606 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap @@ -7,7 +7,7 @@ expression: "render_lines(&view, 84)" Select which items to display in the terminal title. Type to search - > + › [x] project-name Project name (falls back to current directory name) [x] activity Spinner while working, action-required message while blocked. [x] run-state Compact session run-state text (Ready, Working, Thinking) @@ -16,6 +16,6 @@ expression: "render_lines(&view, 84)" [ ] current-dir Current working directory [ ] thread-name Current thread name (omitted when unnamed) [ ] git-branch Current Git branch (omitted when unavailable) - +↓ [ ! ] Action Required | my-project | Working | thread title - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 4e5c853ff5..c04a606c88 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -305,7 +305,6 @@ use crate::key_hint::KeyBindingListExt; use crate::keymap::ChatKeymap; use crate::keymap::RuntimeKeymap; use crate::render::Insets; -use crate::render::renderable::ColumnRenderable; use crate::render::renderable::FlexRenderable; use crate::render::renderable::Renderable; use crate::render::renderable::RenderableExt; diff --git a/codex-rs/tui/src/chatwidget/connectors.rs b/codex-rs/tui/src/chatwidget/connectors.rs index 2f8bbf06c5..edc7af43fb 100644 --- a/codex-rs/tui/src/chatwidget/connectors.rs +++ b/codex-rs/tui/src/chatwidget/connectors.rs @@ -213,32 +213,27 @@ impl ChatWidget { } fn connectors_loading_popup_params(&self) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Apps".bold())); - header.push(Line::from("Loading installed and available apps...".dim())); - SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(CONNECTORS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Apps".to_string()), + subtitle: Some("Loading installed and available apps...".to_string()), items: vec![SelectionItem { name: "Loading apps...".to_string(), description: Some("This updates when the full list is ready.".to_string()), is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } fn connectors_error_popup_params(&self) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Apps".bold())); - header.push(Line::from("Failed to load apps.".dim())); - SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(CONNECTORS_SELECTION_VIEW_ID), - header: Box::new(header), - footer_hint: Some(self.bottom_pane.standard_popup_hint_line()), + title: Some("Apps".to_string()), + subtitle: Some("Failed to load apps.".to_string()), items: vec![ SelectionItem { name: "App directory unavailable".to_string(), @@ -260,7 +255,7 @@ impl ChatWidget { ..Default::default() }, ], - ..Default::default() + ..SelectionViewParams::picker() } } @@ -274,14 +269,12 @@ impl ChatWidget { .iter() .filter(|connector| connector.is_accessible) .count(); - let mut header = ColumnRenderable::new(); - header.push(Line::from("Apps".bold())); - header.push(Line::from( - "Use $ to insert an installed app into your prompt.".dim(), - )); - header.push(Line::from( - format!("Installed {installed} of {total} available apps.").dim(), - )); + let header = Paragraph::new(vec![ + Line::from("Apps".bold()), + Line::from("Use $ to insert an installed app into your prompt.".dim()), + Line::from(format!("Installed {installed} of {total} available apps.").dim()), + ]) + .wrap(Wrap { trim: false }); let initial_selected_idx = selected_connector_id.and_then(|selected_connector_id| { connectors .iter() @@ -351,15 +344,15 @@ impl ChatWidget { } SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(CONNECTORS_SELECTION_VIEW_ID), header: Box::new(header), - footer_hint: Some(self.bottom_pane.standard_popup_hint_line()), items, is_searchable: true, search_placeholder: Some("Type to search apps".to_string()), col_width_mode: ColumnWidthMode::AutoAllRows, initial_selected_idx, - ..Default::default() + ..SelectionViewParams::picker() } } diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index d87b051e08..8a233c80ff 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -340,6 +340,7 @@ impl Renderable for PluginDisclosureLine { impl ChatWidget { pub(super) fn plugins_loading_popup_params(&self) -> SelectionViewParams { SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -353,12 +354,13 @@ impl ChatWidget { is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } pub(super) fn marketplace_add_loading_popup_params(&self) -> SelectionViewParams { SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -374,7 +376,7 @@ impl ChatWidget { is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } @@ -384,14 +386,12 @@ impl ChatWidget { marketplace_name: String, marketplace_display_name: String, ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("Remove {marketplace_display_name} marketplace?").dim(), - )); - header.push(Line::from( - "This removes the configured marketplace from Codex.".dim(), - )); + let header = Paragraph::new(vec![ + Line::from("Plugins".bold()), + Line::from(format!("Remove {marketplace_display_name} marketplace?").dim()), + Line::from("This removes the configured marketplace from Codex.".dim()), + ]) + .wrap(Wrap { trim: false }); let cwd_for_remove = self.config.cwd.to_path_buf(); let cwd_for_cancel = self.config.cwd.to_path_buf(); @@ -400,6 +400,7 @@ impl ChatWidget { let plugins_response_for_on_cancel = plugins_response.clone(); SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(header), footer_hint: Some(Line::from(vec![ @@ -449,7 +450,7 @@ impl ChatWidget { response: plugins_response_for_on_cancel.clone(), }); })), - ..Default::default() + ..SelectionViewParams::picker() } } @@ -457,22 +458,18 @@ impl ChatWidget { &self, marketplace_display_name: &str, ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("Removing {marketplace_display_name}...").dim(), - )); - SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Plugins".to_string()), + subtitle: Some(format!("Removing {marketplace_display_name}...")), items: vec![SelectionItem { name: "Removing marketplace...".to_string(), description: Some("This updates when marketplace removal completes.".to_string()), is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } @@ -484,6 +481,7 @@ impl ChatWidget { .map(|name| format!("Upgrading {name} marketplace...")) .unwrap_or_else(|| "Upgrading marketplaces...".to_string()); SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -497,7 +495,7 @@ impl ChatWidget { is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } @@ -506,6 +504,7 @@ impl ChatWidget { plugin_display_name: &str, ) -> SelectionViewParams { SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -519,7 +518,7 @@ impl ChatWidget { is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } @@ -527,22 +526,18 @@ impl ChatWidget { &self, plugin_display_name: &str, ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("Installing {plugin_display_name}...").dim(), - )); - SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Plugins".to_string()), + subtitle: Some(format!("Installing {plugin_display_name}...")), items: vec![SelectionItem { name: "Installing plugin...".to_string(), description: Some("This updates when plugin installation completes.".to_string()), is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } @@ -550,48 +545,38 @@ impl ChatWidget { &self, plugin_display_name: &str, ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("Uninstalling {plugin_display_name}...").dim(), - )); - SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Plugins".to_string()), + subtitle: Some(format!("Uninstalling {plugin_display_name}...")), items: vec![SelectionItem { name: "Uninstalling plugin...".to_string(), description: Some("This updates when the plugin removal completes.".to_string()), is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } pub(super) fn plugins_error_popup_params(&self, err: &str) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from("Failed to load plugins.".dim())); - SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Plugins".to_string()), + subtitle: Some("Failed to load plugins.".to_string()), items: vec![SelectionItem { name: "Plugin marketplace unavailable".to_string(), description: Some(err.to_string()), is_disabled: true, ..Default::default() }], - ..Default::default() + ..SelectionViewParams::picker() } } pub(super) fn marketplace_add_error_popup_params(&self) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from("Failed to add marketplace.".dim())); - let mut items = vec![ SelectionItem { name: "Marketplace add failed".to_string(), @@ -629,11 +614,13 @@ impl ChatWidget { } SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Plugins".to_string()), + subtitle: Some("Failed to add marketplace.".to_string()), footer_hint: Some(plugin_detail_hint_line()), items, - ..Default::default() + ..SelectionViewParams::picker() } } @@ -642,10 +629,6 @@ impl ChatWidget { marketplace_name: &str, marketplace_display_name: &str, ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from("Failed to remove marketplace.".dim())); - let marketplace_name = marketplace_name.to_string(); let marketplace_display_name = marketplace_display_name.to_string(); let mut items = vec![ @@ -686,11 +669,13 @@ impl ChatWidget { } SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Plugins".to_string()), + subtitle: Some("Failed to remove marketplace.".to_string()), footer_hint: Some(plugin_detail_hint_line()), items, - ..Default::default() + ..SelectionViewParams::picker() } } @@ -699,10 +684,6 @@ impl ChatWidget { err: &str, plugins_response: Option<&PluginListResponse>, ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from("Failed to load plugin details.".dim())); - let mut items = vec![SelectionItem { name: "Plugin detail unavailable".to_string(), description: Some(err.to_string()), @@ -726,11 +707,13 @@ impl ChatWidget { } SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), + title: Some("Plugins".to_string()), + subtitle: Some("Failed to load plugin details.".to_string()), footer_hint: Some(plugin_detail_hint_line()), items, - ..Default::default() + ..SelectionViewParams::picker() } } @@ -755,7 +738,8 @@ impl ChatWidget { PLUGIN_ROW_PREFIX_WIDTH + UnicodeWidthStr::width(display_name.as_str()) }) .chain([UnicodeWidthStr::width("Add marketplace")]) - .max(); + .max() + .map(|width| width.min(/*other*/ 36)); let installed_entries = all_entries .iter() .filter(|(_, plugin, _)| plugin.installed) @@ -950,6 +934,7 @@ impl ChatWidget { active_tab_id.and_then(|tab_id| plugin_tab_id_matching_saved_id(&tab_id, &tabs)); SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(()), footer_hint: Some(plugins_popup_hint_line( @@ -957,6 +942,7 @@ impl ChatWidget { )), tab_footer_hints, tabs, + reserve_result_rows: true, initial_tab_id, is_searchable: true, search_placeholder: Some("Type to search plugins".to_string()), @@ -964,7 +950,7 @@ impl ChatWidget { row_display: SelectionRowDisplay::SingleLine, name_column_width, initial_selected_idx, - ..Default::default() + ..SelectionViewParams::picker() } } @@ -1007,10 +993,16 @@ impl ChatWidget { let display_name = plugin_display_name(&plugin.summary); let detail_status_label = plugin_detail_status_label(&plugin.summary); let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(), - )); + header.push( + ratatui::widgets::Paragraph::new(Line::from("Plugins".bold())) + .wrap(ratatui::widgets::Wrap { trim: false }), + ); + header.push( + ratatui::widgets::Paragraph::new(Line::from( + format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(), + )) + .wrap(ratatui::widgets::Wrap { trim: false }), + ); if !plugin.summary.installed { header.push(PluginDisclosureLine { line: Line::from(vec![ @@ -1025,7 +1017,10 @@ impl ChatWidget { }); } if let Some(description) = plugin_detail_description(plugin) { - header.push(Line::from(description.dim())); + header.push( + ratatui::widgets::Paragraph::new(Line::from(description.dim())) + .wrap(ratatui::widgets::Wrap { trim: false }), + ); } let cwd = self.config.cwd.to_path_buf(); @@ -1156,12 +1151,13 @@ impl ChatWidget { }); SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(header), footer_hint: Some(plugin_detail_hint_line()), items, col_width_mode: ColumnWidthMode::AutoAllRows, - ..Default::default() + ..SelectionViewParams::picker() } } @@ -1302,30 +1298,31 @@ fn plugins_popup_hint_line( ) -> Line<'static> { match (can_remove_marketplace, can_upgrade_marketplace) { (true, true) => Line::from( - "ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close", + "ctrl+u upgrade · ctrl+r remove · space toggle · ←/→ tabs · enter details · esc close", ), (true, false) => { - Line::from("ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close") + Line::from("ctrl+r remove · space toggle · ←/→ tabs · enter details · esc close") } (false, true) => { - Line::from("ctrl + u upgrade · space toggle · ←/→ tabs · enter details · esc close") + Line::from("ctrl+u upgrade · space toggle · ←/→ tabs · enter details · esc close") } - (false, false) => Line::from( - "space enable/disable · ←/→ select marketplace · enter view details · esc close", - ), + (false, false) => Line::from("←/→ tabs · enter details · space toggle · esc close"), } } pub(super) fn plugin_detail_hint_line() -> Line<'static> { - Line::from("Press esc to close.") + Line::from("esc close") } pub(super) fn plugins_header(subtitle: String, count_line: String) -> Box { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from(subtitle.dim())); - header.push(Line::from(count_line.dim())); - Box::new(header) + Box::new( + Paragraph::new(vec![ + Line::from("Plugins".bold()), + Line::from(subtitle.dim()), + Line::from(count_line.dim()), + ]) + .wrap(Wrap { trim: false }), + ) } fn dedupe_plugin_entries<'a>( diff --git a/codex-rs/tui/src/chatwidget/plugins.rs b/codex-rs/tui/src/chatwidget/plugins.rs index 06f5e7be11..dcb4bc1066 100644 --- a/codex-rs/tui/src/chatwidget/plugins.rs +++ b/codex-rs/tui/src/chatwidget/plugins.rs @@ -865,14 +865,26 @@ impl ChatWidget { "Install the required Apps in ChatGPT to continue:" }; let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("{} plugin installed.", flow.plugin_display_name).bold(), - )); - header.push(Line::from( - format!("App setup {current}/{total}: {}", app.name).dim(), - )); - header.push(Line::from(status_label.dim())); + header.push( + ratatui::widgets::Paragraph::new(Line::from("Plugins".bold())) + .wrap(ratatui::widgets::Wrap { trim: false }), + ); + header.push( + ratatui::widgets::Paragraph::new(Line::from( + format!("{} plugin installed.", flow.plugin_display_name).bold(), + )) + .wrap(ratatui::widgets::Wrap { trim: false }), + ); + header.push( + ratatui::widgets::Paragraph::new(Line::from( + format!("App setup {current}/{total}: {}", app.name).dim(), + )) + .wrap(ratatui::widgets::Wrap { trim: false }), + ); + header.push( + ratatui::widgets::Paragraph::new(Line::from(status_label.dim())) + .wrap(ratatui::widgets::Wrap { trim: false }), + ); let mut items = Vec::new(); @@ -943,12 +955,13 @@ impl ChatWidget { }); Some(SelectionViewParams { + appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(header), footer_hint: Some(plugin_detail_hint_line()), items, col_width_mode: ColumnWidthMode::AutoAllRows, - ..Default::default() + ..SelectionViewParams::picker() }) } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap index 7f04355834..a985b87d73 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap @@ -1,5 +1,6 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs +assertion_line: 2317 expression: snapshot --- › $ @@ -14,9 +15,10 @@ expression: snapshot App setup 1/1: % Arabica UAE Already installed in this session. + ChatGPT apps link unavailable This app did not provide an install/manage URL. › 1. Continue Advance to the next app. 2. Skip remaining app setup Stop this follow-up flow for this plugin. - Press esc to close. + esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_error_state.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_error_state.snap index c669150bb5..45afa12172 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_error_state.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_error_state.snap @@ -1,13 +1,13 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 2796 expression: popup --- Apps Failed to load apps. + App directory unavailable The app directory request failed. Retry, or press Esc to continue. › 1. Retry Reload installed and available apps. - Press enter to confirm or esc to go back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_loading_state.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_loading_state.snap index a13cb7a009..0646248b4a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_loading_state.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_popup_loading_state.snap @@ -5,6 +5,7 @@ expression: before Apps Loading installed and available apps... + › Loading apps... This updates when the full list is ready. - Press enter to confirm or esc to go back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__connector_scope_invalidation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__connector_scope_invalidation.snap index cc03687523..19269aa4dd 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__connector_scope_invalidation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__connector_scope_invalidation.snap @@ -8,9 +8,10 @@ Before account change: Installed 1 of 1 available apps. Type to search apps + › Previous Account App Installed. App link unavailable. - Press enter to confirm or esc to go back + enter select · esc back After account change: › Ask Codex to do anything diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_available_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_available_popup.snap index 8ff8096740..5c260d762e 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_available_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_available_popup.snap @@ -1,16 +1,17 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 3142 +assertion_line: 3203 expression: popup --- Experimental features Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. -› [ ] Network proxy Apply network proxy restrictions to - sandboxed sessions that already have - network access. + +› [ ] Network proxy Apply network proxy restrictions to sandboxed + sessions that already have network access. [ ] Prevent sleep while running Keep your computer awake while Codex is running a thread. - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_loading.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_loading.snap index f787758e0f..693ea0c7e5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_loading.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_loading.snap @@ -1,5 +1,6 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs +assertion_line: 3167 expression: "render_bottom_popup(&chat, 80)" --- Experimental features @@ -9,4 +10,6 @@ expression: "render_bottom_popup(&chat, 80)" - Press space to select or enter to save + + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap index eff5dff649..2b435d7570 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap @@ -1,16 +1,17 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 3083 expression: popup --- Experimental features Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. + › [ ] JavaScript REPL Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. [x] Shell tool Allow the model to run shell commands. [ ] Voice conversations Talk with Codex using /voice. - Press space to select or enter to save + + space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup_configured_key_chords.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup_configured_key_chords.snap index e0a3ed0cc3..5b8f4a6f8b 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup_configured_key_chords.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup_configured_key_chords.snap @@ -6,6 +6,8 @@ expression: popup Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server. + › [x] Shell tool Allow the model to run shell commands. - Press space to select or ctrl+x enter to save + + space toggle · ctrl+x enter save · esc save/close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap index 13c983b0b5..f6408e4928 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap @@ -10,7 +10,8 @@ expression: popup ■ /tmp/hooks.json: failed to parse hooks config Event Installed Active Description - PreToolUse 0 0 Before a tool executes + +› PreToolUse 0 0 Before a tool executes PermissionRequest 0 0 When permission is requested PostToolUse 0 0 After a tool executes PreCompact 0 0 Before context compaction @@ -18,9 +19,6 @@ expression: popup SessionStart 0 0 When a new session starts SessionEnd 0 0 Right before a session ends UserPromptSubmit 0 0 When the user submits a prompt - SubagentStart 0 0 When a subagent is created - SubagentStop 0 0 Right before a subagent ends its turn - Stop 0 0 Right before Codex ends its turn - Interrupt 0 0 Right before an interrupted turn is aborted +↓ - Press enter to view hooks; esc to close + enter details · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_reset_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_reset_confirmation.snap index 2538e2b461..89c61f00e8 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_reset_confirmation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_reset_confirmation.snap @@ -3,10 +3,13 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Reset all memories? + This clears local memory files and rollout summaries for the current Codex + home. + › Reset all memories Delete local memory files and rollout summaries. Go back Return to memory settings. - Press enter to confirm or esc to go back + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_settings_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_settings_popup.snap index 2fbe641e29..364dbce344 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_settings_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_settings_popup.snap @@ -5,13 +5,15 @@ expression: popup Memories Choose how Codex uses and creates memories. Changes are saved to config.toml -› [x] Use memories Use memories in the following threads. Applied at - next thread. + +› [x] Use memories Use memories in the following threads. Applied at next + thread. [ ] Generate memories Generate memories from the following threads. Current thread included. Reset all memories Clear local memory files and summaries. Existing threads stay intact. + Learn more: https://developers.openai.com/codex/memories - Press space to toggle; enter to save or select + space toggle · enter save/select · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap index 7350ccc0b9..648782443c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap @@ -5,7 +5,8 @@ expression: popup Plugins Failed to load plugin details. + Plugin detail unavailable Failed to load plugin details. › 1. Back to plugins Return to the plugin list. - Press esc to close. + esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap index e5d14a4c51..c1c9bce115 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap @@ -8,6 +8,7 @@ expression: strip_osc8_for_snapshot(&popup) more. Turn Figma files into implementation context. + › 1. Back to plugins Return to the plugin list. 2. Install plugin Install this plugin now. Source Local @@ -17,4 +18,4 @@ expression: strip_osc8_for_snapshot(&popup) Apps Figma, Slack MCP Servers figma-mcp, docs-mcp - Press esc to close. + esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap index bf1a4165b2..5aa4a1f4d2 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap @@ -6,6 +6,7 @@ expression: strip_osc8_for_snapshot(&popup) Figma · Installed by admin · ChatGPT Marketplace Turn Figma files into implementation context. + › 1. Back to plugins Return to the plugin list. Installed by admin This plugin is installed by your workspace admin. Source Local @@ -15,4 +16,4 @@ expression: strip_osc8_for_snapshot(&popup) Apps Figma, Slack MCP Servers figma-mcp, docs-mcp - Press esc to close. + esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_local_share_read_only.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_local_share_read_only.snap index 31ff7ba462..7595e2282a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_local_share_read_only.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_local_share_read_only.snap @@ -7,6 +7,7 @@ expression: strip_osc8_for_snapshot(&popup) Data shared with this app is subject to the app's terms of service and privacy policy. Learn more. Workspace docs. + › 1. Back to plugins Return to the plugin list. 2. Install plugin Install this plugin now. Source Local @@ -15,5 +16,5 @@ expression: strip_osc8_for_snapshot(&popup) Sharing Private · creator Test User · https://chatgpt.com/codex/plugins/share/docs Skills No plugin skills. Hooks No plugin hooks. - - Press esc to close. +↓ + esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_npm_source.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_npm_source.snap index cb07253718..ae4fe7831f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_npm_source.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_npm_source.snap @@ -8,6 +8,7 @@ expression: strip_osc8_for_snapshot(&popup) more. Turn Figma files into implementation context. + › 1. Back to plugins Return to the plugin list. 2. Install plugin Install this plugin now. Source npm · @acme/figma-plugin@^1.2.0 @@ -17,4 +18,4 @@ expression: strip_osc8_for_snapshot(&popup) Apps Figma, Slack MCP Servers figma-mcp, docs-mcp - Press esc to close. + esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_admin_disabled_installed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_admin_disabled_installed.snap index b8f109a9fc..e902b862e1 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_admin_disabled_installed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_admin_disabled_installed.snap @@ -6,9 +6,17 @@ expression: popup Browse plugins from available marketplaces. Installed 1 of 1 available plugins. - [All Plugins] Installed (1) OpenAI Curated Workspace Add Marketplace + All Plugins Installed (1) OpenAI Curated Workspace Add Marketplace Type to search plugins + › [!] Admin Blocked Disabled Press Enter to view plugin details. - space enable/disable · ←/→ select marketplace · enter view details · esc close + + + + + + + + ←/→ tabs · enter details · space toggle · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap index fbf5104c37..f45379f04c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap @@ -6,12 +6,17 @@ expression: popup Browse plugins from available marketplaces. Installed 1 of 4 available plugins. - [All Plugins] Installed (1) OpenAI Curated Workspace Repo Marketplace Add Marketplace + All Plugins Installed (1) OpenAI Curated Workspace Repo Marketplace Add Marketplace Type to search plugins + › [ ] Alpha Sync Disabled Space to enable; Enter view details. [-] Bravo Search Available · OpenAI Curated · Search docs and tickets. [-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins. [-] Starter Admin assigned · OpenAI Curated · Included by default. - space enable/disable · ←/→ select marketplace · enter view details · esc close + + + + + ←/→ tabs · enter details · space toggle · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap index 146d02a5a2..a945e7ad30 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap @@ -6,9 +6,17 @@ expression: loaded_popup Workspace. This section loaded successfully. - All Plugins Installed (0) OpenAI Curated [Workspace] Add Marketplace + All Plugins Installed (0) OpenAI Curated Workspace Add Marketplace Type to search plugins + › No workspace plugins available No workspace directory plugins are available. - space enable/disable · ←/→ select marketplace · enter view details · esc close + + + + + + + + ←/→ tabs · enter details · space toggle · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_loading_state.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_loading_state.snap index ff09ca3343..0589babff1 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_loading_state.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_loading_state.snap @@ -6,6 +6,7 @@ expression: popup Loading available plugins... This updates when the marketplace list is ready. + › Loading plugins... This updates when the marketplace list is ready. - Press enter to confirm or esc to go 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 f46bbec9e7..e7912bbc6d 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 @@ -1,12 +1,12 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 411 expression: confirmation --- Plugins Remove Repo Marketplace marketplace? This removes the configured marketplace from Codex. + › 1. Remove marketplace Remove this marketplace from the available plugin list. 2. Back to plugins Keep this marketplace installed. diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap index 60ebee6f9a..39bf026558 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap @@ -1,15 +1,22 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 473 expression: popup --- Plugins Debug Marketplace installed successfully. Select the plugins you want to use and press Enter to install or view details. - All Plugins Installed (0) OpenAI Curated Workspace [Debug Marketplace] Add Marketplace + All Plugins Installed (0) OpenAI Curated Workspace Debug Marketplace Add Marketplace Type to search plugins + › [-] Debug Plugin Available Press Enter to install or view plugin details. - ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close + + + + + + + + ctrl+u upgrade · ctrl+r remove · space toggle · ←/→ tabs · enter details · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap index 3a2e455f6d..34e9416789 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap @@ -1,15 +1,22 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 1684 expression: popup --- Plugins Browse plugins from available marketplaces. Installed 0 of 3 available plugins. - [All Plugins] Installed (0) OpenAI Curated Workspace Add Marketplace + All Plugins Installed (0) OpenAI Curated Workspace Add Marketplace sla + › [-] Slack Available Press Enter to install or view plugin details. - space enable/disable · ←/→ select marketplace · enter view details · esc close + + + + + + + + ←/→ tabs · enter details · space toggle · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_hardcoded_only.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_hardcoded_only.snap index 2a91d27c1b..770ed596bf 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_hardcoded_only.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_hardcoded_only.snap @@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat) Select which items to display in the status line. Type to search - > + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── [x] project-name Project name (omitted when unavailable) @@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat) [ ] model Current model name [ ] model-with-reasoning Current model name with reasoning level [ ] reasoning Current reasoning level - +↓ my-project · feat/awesome-feature · thread title - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_live_only.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_live_only.snap index 709f7734df..b4eb9250d1 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_live_only.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_live_only.snap @@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat) Select which items to display in the status line. Type to search - > + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── [x] project-name Project name (omitted when unavailable) @@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat) [ ] model Current model name [ ] model-with-reasoning Current model name with reasoning level [ ] reasoning Current reasoning level - +↓ preview-live-root · feature/live-preview-branch · Live preview thread - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_mixed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_mixed.snap index b0e29ea479..8b3db4c492 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_mixed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_mixed.snap @@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat) Select which items to display in the status line. Type to search - > + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── [x] project-name Project name (omitted when unavailable) @@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat) [ ] model Current model name [ ] model-with-reasoning Current model name with reasoning level [ ] reasoning Current reasoning level - +↓ my-project · feature/mixed-preview · Mixed preview thread - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_rate_limits.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_rate_limits.snap index 7dca3c19d5..6b6cd080bd 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_rate_limits.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_rate_limits.snap @@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat) Select which items to display in the status line. Type to search - > + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── [x] monthly-limit Remaining usage on the monthly usage limit (omitted when unavailable) @@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat) [ ] model-with-reasoning Current model name with reasoning level [ ] reasoning Current reasoning level [ ] current-dir Current working directory - +↓ monthly 65% left · weekly 50% left - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_workspace_headline.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_workspace_headline.snap index 32ad18ec65..5fe11ae96a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_workspace_headline.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_setup_popup_workspace_headline.snap @@ -6,15 +6,15 @@ expression: status_line_popup_snapshot(&mut chat) Select which items to display in the status line. Type to search - > + › [x] Use theme colors Apply colors from the active /theme ─────────────────────── - [x] workspace-headline Workspace notification headline (Enterprise workspaces only; omitted … + [x] workspace-headline Workspace notification headline (Enterprise workspaces only; omitted wh… [ ] model Current model name [ ] model-with-reasoning Current model name with reasoning level [ ] reasoning Current reasoning level [ ] current-dir Current working directory [ ] project-name Project name (omitted when unavailable) - +↓ Workspace maintenance starts at 5pm - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap index 8ff4227504..a4de3c5a36 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap @@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat) Select which items to display in the terminal title. Type to search - > + › [x] thread-title Current thread title, or thread identifier when unnamed [x] git-branch Current Git branch (omitted when unavailable) [x] task-progress Latest task progress from update_plan (omitted until available) @@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat) [ ] current-dir Current working directory [ ] activity Spinner while working, action-required message while blocked. [ ] run-state Compact session run-state text (Ready, Working, Thinking) - +↓ thread title | feat/awesome-feature | Tasks 0/0 - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap index 8c149b8cc9..dd24632059 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap @@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat) Select which items to display in the terminal title. Type to search - > + › [x] project-name Project name (falls back to current directory name) [x] thread-title Current thread title, or thread identifier when unnamed [x] git-branch Current Git branch (omitted when unavailable) @@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat) [ ] current-dir Current working directory [ ] activity Spinner while working, action-required message while blocked. [ ] run-state Compact session run-state text (Ready, Working, Thinking) - +↓ preview-live-root | Live preview thread | feature/live-preview-branch | Tasks 2/5 - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap index b4aec24fc3..a6e15a7fd5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap @@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat) Select which items to display in the terminal title. Type to search - > + › [x] project-name Project name (falls back to current directory name) [x] thread-title Current thread title, or thread identifier when unnamed [x] task-progress Latest task progress from update_plan (omitted until available) @@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat) [ ] activity Spinner while working, action-required message while blocked. [ ] run-state Compact session run-state text (Ready, Working, Thinking) [ ] thread-name Current thread name (omitted when unnamed) - +↓ project | Mixed preview thread | Tasks 0/0 - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_rate_limits.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_rate_limits.snap index f177011f07..c305956fa5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_rate_limits.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_rate_limits.snap @@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat) Select which items to display in the terminal title. Type to search - > + › [x] monthly-limit Remaining usage on the monthly usage limit (omitted when unavailable) [x] weekly-limit Remaining usage on the weekly usage limit (omitted when unavailable) [ ] app-name Codex app name @@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat) [ ] activity Spinner while working, action-required message while blocked. [ ] run-state Compact session run-state text (Ready, Working, Thinking) [ ] thread-name Current thread name (omitted when unnamed) - +↓ monthly 65% left | weekly 50% left - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_thread_usage.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_thread_usage.snap index 7ad1582ee4..760c7705f0 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_thread_usage.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_thread_usage.snap @@ -6,15 +6,15 @@ expression: terminal_title_popup_snapshot(&mut chat) Select which items to display in the terminal title. Type to search - > -› [x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitte… - [x] estimated-thread-cost Estimated current-thread cost (Enterprise workspaces only; omitted w… + +› [x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitted … + [x] estimated-thread-cost Estimated current-thread cost (Enterprise workspaces only; omitted whe… [ ] app-name Codex app name [ ] project-name Project name (falls back to current directory name) [ ] current-dir Current working directory [ ] activity Spinner while working, action-required message while blocked. [ ] run-state Compact session run-state text (Ready, Working, Thinking) [ ] thread-name Current thread name (omitted when unnamed) - +↓ 5.2 credits | ~$1.82 - Press space to toggle; ←/→ to move; enter to confirm and close; esc to close + space toggle · ←/→ reorder · enter save · esc cancel diff --git a/codex-rs/tui/src/chatwidget/tests/plugin_catalog_tests.rs b/codex-rs/tui/src/chatwidget/tests/plugin_catalog_tests.rs index b133c7ed04..c558af4220 100644 --- a/codex-rs/tui/src/chatwidget/tests/plugin_catalog_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/plugin_catalog_tests.rs @@ -84,7 +84,7 @@ async fn plugins_popup_uses_product_labels_for_remote_and_personal_tabs() { ] .into_iter() .map(|(selected_tab, product_label, plugin_name, raw_label)| { - let popup = select_plugins_tab_containing(&mut chat, /*width*/ 120, selected_tab); + let popup = select_plugins_tab_containing(&mut chat, /*width*/ 120, product_label); assert!( popup.contains(product_label) && popup.contains(plugin_name) diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index e30f48a91b..fc9446df2b 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -277,7 +277,7 @@ async fn plugins_popup_truncates_long_descriptions_in_list_rows() { .expect("expected verbose plugin row in popup"); insta::assert_snapshot!( verbose_row, - @" [-] Verbose Plugin Available · OpenAI Curated · This description…" + @" [-] Verbose Plugin Available · OpenAI Curated · This description k…" ); assert!( !popup @@ -379,8 +379,8 @@ async fn plugins_popup_upgrades_user_configured_git_marketplace_from_marketplace let popup = select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace."); assert!( popup.contains("Repo Marketplace.") - && popup.contains("ctrl + u upgrade") - && popup.contains("ctrl + r remove") + && popup.contains("ctrl+u upgrade") + && popup.contains("ctrl+r remove") && popup.contains("Debug Plugin"), "expected upgradeable user-configured marketplace tab, got:\n{popup}" ); @@ -476,8 +476,8 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() { assert_chatwidget_snapshot!("plugins_popup_newly_installed_marketplace", popup); assert!( popup.contains("Debug Marketplace installed successfully.") - && popup.contains("ctrl + u upgrade") - && popup.contains("ctrl + r remove") + && popup.contains("ctrl+u upgrade") + && popup.contains("ctrl+r remove") && popup.contains("Debug Plugin"), "expected marketplace add refresh to switch to the new marketplace tab, got:\n{popup}" ); @@ -487,7 +487,7 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() { let reopened_popup = (0..8) .find_map(|_| { let popup = render_bottom_popup(&chat, /*width*/ 100); - if popup.contains("[Debug Marketplace]") { + if popup.contains("Installed 0 of 1 Debug Marketplace plugins.") { Some(popup) } else { chat.handle_key_event(KeyEvent::from(KeyCode::Right)); @@ -542,8 +542,8 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() { select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace."); assert!( repo_tab.contains("Repo Marketplace.") - && repo_tab.contains("ctrl + u upgrade") - && repo_tab.contains("ctrl + r remove") + && repo_tab.contains("ctrl+u upgrade") + && repo_tab.contains("ctrl+r remove") && repo_tab.contains("Debug Plugin"), "expected removable user-configured marketplace tab, got:\n{repo_tab}" ); @@ -611,7 +611,7 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() { refreshed.contains("Browse plugins from available marketplaces.") && !refreshed.contains("Repo Marketplace") && !refreshed.contains("Debug Plugin") - && !refreshed.contains("ctrl + r remove"), + && !refreshed.contains("ctrl+r remove"), "expected refreshed plugin list without removed marketplace, got:\n{refreshed}" ); } diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index d4e39f1245..6a48824bb8 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -1,6 +1,8 @@ //! Windows sandbox prompts and warning surfaces for `ChatWidget`. use super::*; +#[cfg(any(target_os = "windows", test))] +use crate::render::renderable::ColumnRenderable; impl ChatWidget { #[cfg(any(target_os = "windows", test))]