[codex] extract external agent import picker renderer (#27065)

## Why

The external-agent import picker is easier to review when its rendering
refactor lands separately from new state and interaction behavior. This
layer is intended to be behavior-neutral.

## What changed

- extract external-agent migration rendering into a dedicated `render`
module
- preserve existing behavior while separating presentation from
interaction logic
- establish a smaller foundation for the import picker UX in the next PR

## Validation

- `just test -p codex-tui external_agent_config_migration` (10 passed)

## Stack

1. [#27064](https://github.com/openai/codex/pull/27064): remove the
startup migration flow
2. [#27065](https://github.com/openai/codex/pull/27065): extract the
picker renderer
3. [#27070](https://github.com/openai/codex/pull/27070): add the
external-agent import picker UX
4. [#27071](https://github.com/openai/codex/pull/27071): expose the flow
through `/import`

**This PR is stack item 2.** Draft while the lower stack dependency is
reviewed.
This commit is contained in:
stefanstokic-oai
2026-06-10 14:48:30 -04:00
committed by GitHub
Unverified
parent ccf1a18518
commit 72667f4f41
2 changed files with 139 additions and 128 deletions
@@ -1,9 +1,5 @@
use crate::diff_render::display_path_for;
use crate::key_hint;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::render::Insets;
use crate::render::RectExt as _;
use crate::selection_list::selection_option_row_with_dim;
use crate::style::accent_style;
use crate::tui::FrameRequester;
use crate::tui::Tui;
@@ -15,18 +11,14 @@ use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint;
use ratatui::layout::Layout;
use ratatui::layout::Rect;
use ratatui::prelude::Stylize as _;
use ratatui::text::Line;
use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use tokio_stream::StreamExt;
mod render;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ExternalAgentConfigMigrationOutcome {
Proceed(Vec<ExternalAgentConfigMigrationItem>),
@@ -633,124 +625,6 @@ impl ExternalAgentConfigMigrationScreen {
}
}
impl WidgetRef for &ExternalAgentConfigMigrationScreen {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Clear.render(area, buf);
let inner_area = area.inset(Insets::vh(/*v*/ 1, /*h*/ 2));
let error_height = u16::from(self.error.is_some());
let fixed_height = 1u16 + 2u16 + error_height + 1u16 + 4u16 + 1u16;
let list_height =
self.render_line_count()
.max(1)
.min(inner_area.height.saturating_sub(fixed_height) as usize) as u16;
let [
header_area,
intro_area,
error_area,
list_area,
list_gap_area,
actions_area,
footer_area,
_spacer_area,
] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(error_height),
Constraint::Length(list_height),
Constraint::Length(1),
Constraint::Length(4),
Constraint::Length(1),
Constraint::Fill(1),
])
.areas(inner_area);
let heading = Line::from(vec!["> ".into(), "External agent config detected".bold()]);
heading.render(header_area, buf);
Paragraph::new(vec![
Line::from("We found settings from another agent that you can add to this project."),
Line::from("Select what to import"),
])
.wrap(Wrap { trim: false })
.render(intro_area, buf);
if let Some(error) = &self.error {
Paragraph::new(error.clone().red().to_string())
.wrap(Wrap { trim: false })
.render(error_area, buf);
}
self.render_items(list_area, buf);
Clear.render(list_gap_area, buf);
let [
actions_intro_area,
proceed_area,
skip_area,
skip_forever_area,
] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.areas(actions_area);
let actions_intro = format!(
"Selected {} of {} item(s).",
self.selected_count(),
self.items.len()
);
Paragraph::new(actions_intro)
.wrap(Wrap { trim: false })
.render(actions_intro_area, buf);
selection_option_row_with_dim(
/*index*/ 0,
ActionMenuOption::Proceed.label().to_string(),
self.focus == FocusArea::Actions
&& self.highlighted_action == ActionMenuOption::Proceed,
/*dim*/ self.focus != FocusArea::Actions || !self.proceed_enabled(),
)
.render(proceed_area, buf);
selection_option_row_with_dim(
/*index*/ 1,
ActionMenuOption::Skip.label().to_string(),
self.focus == FocusArea::Actions && self.highlighted_action == ActionMenuOption::Skip,
/*dim*/ self.focus != FocusArea::Actions,
)
.render(skip_area, buf);
selection_option_row_with_dim(
/*index*/ 2,
ActionMenuOption::SkipForever.label().to_string(),
self.focus == FocusArea::Actions
&& self.highlighted_action == ActionMenuOption::SkipForever,
/*dim*/ self.focus != FocusArea::Actions,
)
.render(skip_forever_area, buf);
Line::from(vec![
"Use ".dim(),
key_hint::plain(KeyCode::Up).into(),
"/".dim(),
key_hint::plain(KeyCode::Down).into(),
" to move, ".dim(),
key_hint::plain(KeyCode::Char(' ')).into(),
" to toggle, ".dim(),
"1".cyan(),
"/".dim(),
"2".cyan(),
"/".dim(),
"3".cyan(),
" to choose, ".dim(),
"a".cyan(),
"/".dim(),
"n".cyan(),
" for all/none".dim(),
])
.render(footer_area, buf);
}
}
fn is_ctrl_exit_combo(key_event: KeyEvent) -> bool {
key_event.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key_event.code, KeyCode::Char('c') | KeyCode::Char('d'))
@@ -0,0 +1,137 @@
use super::ActionMenuOption;
use super::ExternalAgentConfigMigrationScreen;
use super::FocusArea;
use crate::key_hint;
use crate::render::Insets;
use crate::render::RectExt as _;
use crate::selection_list::selection_option_row_with_dim;
use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint;
use ratatui::layout::Layout;
use ratatui::layout::Rect;
use ratatui::prelude::Stylize as _;
use ratatui::text::Line;
use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
impl WidgetRef for &ExternalAgentConfigMigrationScreen {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Clear.render(area, buf);
let inner_area = area.inset(Insets::vh(/*v*/ 1, /*h*/ 2));
let error_height = u16::from(self.error.is_some());
let fixed_height = 1u16 + 2u16 + error_height + 1u16 + 4u16 + 1u16;
let list_height =
self.render_line_count()
.max(1)
.min(inner_area.height.saturating_sub(fixed_height) as usize) as u16;
let [
header_area,
intro_area,
error_area,
list_area,
list_gap_area,
actions_area,
footer_area,
_spacer_area,
] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(error_height),
Constraint::Length(list_height),
Constraint::Length(1),
Constraint::Length(4),
Constraint::Length(1),
Constraint::Fill(1),
])
.areas(inner_area);
let heading = Line::from(vec!["> ".into(), "External agent config detected".bold()]);
heading.render(header_area, buf);
Paragraph::new(vec![
Line::from("We found settings from another agent that you can add to this project."),
Line::from("Select what to import"),
])
.wrap(Wrap { trim: false })
.render(intro_area, buf);
if let Some(error) = &self.error {
Paragraph::new(error.clone().red().to_string())
.wrap(Wrap { trim: false })
.render(error_area, buf);
}
self.render_items(list_area, buf);
Clear.render(list_gap_area, buf);
let [
actions_intro_area,
proceed_area,
skip_area,
skip_forever_area,
] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.areas(actions_area);
let actions_intro = format!(
"Selected {} of {} item(s).",
self.selected_count(),
self.items.len()
);
Paragraph::new(actions_intro)
.wrap(Wrap { trim: false })
.render(actions_intro_area, buf);
selection_option_row_with_dim(
/*index*/ 0,
ActionMenuOption::Proceed.label().to_string(),
self.focus == FocusArea::Actions
&& self.highlighted_action == ActionMenuOption::Proceed,
/*dim*/ self.focus != FocusArea::Actions || !self.proceed_enabled(),
)
.render(proceed_area, buf);
selection_option_row_with_dim(
/*index*/ 1,
ActionMenuOption::Skip.label().to_string(),
self.focus == FocusArea::Actions && self.highlighted_action == ActionMenuOption::Skip,
/*dim*/ self.focus != FocusArea::Actions,
)
.render(skip_area, buf);
selection_option_row_with_dim(
/*index*/ 2,
ActionMenuOption::SkipForever.label().to_string(),
self.focus == FocusArea::Actions
&& self.highlighted_action == ActionMenuOption::SkipForever,
/*dim*/ self.focus != FocusArea::Actions,
)
.render(skip_forever_area, buf);
Line::from(vec![
"Use ".dim(),
key_hint::plain(KeyCode::Up).into(),
"/".dim(),
key_hint::plain(KeyCode::Down).into(),
" to move, ".dim(),
key_hint::plain(KeyCode::Char(' ')).into(),
" to toggle, ".dim(),
"1".cyan(),
"/".dim(),
"2".cyan(),
"/".dim(),
"3".cyan(),
" to choose, ".dim(),
"a".cyan(),
"/".dim(),
"n".cyan(),
" for all/none".dim(),
])
.render(footer_area, buf);
}
}