Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions codex-rs/tui/src/bottom_pane/approval_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ impl ApprovalOverlay {
),
};

let header = Box::new(ColumnRenderable::new([
Box::new(Line::from(title.bold())),
Box::new(Line::from("")),
let header = Box::new(ColumnRenderable::with([
Line::from(title.bold()).into(),
Line::from("").into(),
header,
]));

Expand Down Expand Up @@ -327,7 +327,7 @@ impl From<ApprovalRequest> for ApprovalRequestState {
header.push(DiffSummary::new(changes, cwd).into());
Self {
variant: ApprovalVariant::ApplyPatch { id },
header: Box::new(ColumnRenderable::new(header)),
header: Box::new(ColumnRenderable::with(header)),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/bottom_pane/list_selection_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl ListSelectionView {
if params.title.is_some() || params.subtitle.is_some() {
let title = params.title.map(|title| Line::from(title.bold()));
let subtitle = params.subtitle.map(|subtitle| Line::from(subtitle.dim()));
header = Box::new(ColumnRenderable::new([
header = Box::new(ColumnRenderable::with([
header,
Box::new(title),
Box::new(subtitle),
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/diff_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ impl From<DiffSummary> for Box<dyn Renderable> {
)));
}

Box::new(ColumnRenderable::new(rows))
Box::new(ColumnRenderable::with(rows))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: tui/src/onboarding/trust_directory.rs
expression: terminal.backend()
---
> You are running Codex in /workspace/project

Since this folder is version controlled, you may wish to allow Codex
to work in this folder without asking for approval.

› 1. Yes, allow Codex to work in this folder without asking for
approval
2. No, ask me to approve edits and commands

Press enter to continue
169 changes: 114 additions & 55 deletions codex-rs/tui/src/onboarding/trust_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Widget;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::style::Styled as _;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;

use crate::key_hint;
use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::StepStateProvider;
use crate::render::Insets;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use crate::render::renderable::RenderableExt as _;
use crate::render::renderable::RowRenderable;

use super::onboarding_screen::StepState;
use unicode_width::UnicodeWidthStr;

pub(crate) struct TrustDirectoryWidget {
pub codex_home: PathBuf,
Expand All @@ -38,74 +44,104 @@ pub enum TrustDirectorySelection {

impl WidgetRef for &TrustDirectoryWidget {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let mut lines: Vec<Line> = vec![
Line::from(vec![
"> ".into(),
"You are running Codex in ".bold(),
self.cwd.to_string_lossy().to_string().into(),
]),
"".into(),
];
let mut column = ColumnRenderable::new();

if self.is_git_repo {
lines.push(
" Since this folder is version controlled, you may wish to allow Codex".into(),
);
lines.push(" to work in this folder without asking for approval.".into());
column.push(Line::from(vec![
"> ".into(),
"You are running Codex in ".bold(),
self.cwd.to_string_lossy().to_string().into(),
]));
column.push("");

let guidance = if self.is_git_repo {
"Since this folder is version controlled, you may wish to allow Codex to work in this folder without asking for approval."
} else {
lines.push(
" Since this folder is not version controlled, we recommend requiring".into(),
);
lines.push(" approval of all edits and commands.".into());
}
lines.push("".into());

let create_option =
|idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> {
let is_selected = self.highlighted == option;
if is_selected {
Line::from(format!("> {}. {text}", idx + 1)).cyan()
} else {
Line::from(format!(" {}. {}", idx + 1, text))
}
};
"Since this folder is not version controlled, we recommend requiring approval of all edits and commands."
};

column.push(
Paragraph::new(guidance.to_string())
.wrap(Wrap { trim: true })
.inset(Insets::tlbr(0, 2, 0, 0)),
);
column.push("");

let mut options: Vec<(&str, TrustDirectorySelection)> = Vec::new();
if self.is_git_repo {
lines.push(create_option(
0,
TrustDirectorySelection::Trust,
options.push((
"Yes, allow Codex to work in this folder without asking for approval",
TrustDirectorySelection::Trust,
));
lines.push(create_option(
1,
TrustDirectorySelection::DontTrust,
options.push((
"No, ask me to approve edits and commands",
TrustDirectorySelection::DontTrust,
));
} else {
lines.push(create_option(
0,
TrustDirectorySelection::Trust,
options.push((
"Allow Codex to work in this folder without asking for approval",
TrustDirectorySelection::Trust,
));
lines.push(create_option(
1,
TrustDirectorySelection::DontTrust,
options.push((
"Require approval of edits and commands",
TrustDirectorySelection::DontTrust,
));
}
lines.push("".into());

for (idx, (text, selection)) in options.iter().enumerate() {
column.push(new_option_row(
idx,
text.to_string(),
self.highlighted == *selection,
));
}

column.push("");

if let Some(error) = &self.error {
lines.push(Line::from(format!(" {error}")).fg(Color::Red));
lines.push("".into());
column.push(
Paragraph::new(error.to_string())
.red()
.wrap(Wrap { trim: true })
.inset(Insets::tlbr(0, 2, 0, 0)),
);
column.push("");
}
// AE: Following styles.md, this should probably be Cyan because it's a user input tip.
// But leaving this for a future cleanup.
lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));

Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(area, buf);
column.push(
Line::from(vec![
"Press ".dim(),
key_hint::plain(KeyCode::Enter).into(),
" to continue".dim(),
])
.inset(Insets::tlbr(0, 2, 0, 0)),
);

column.render(area, buf);
}
}

fn new_option_row(index: usize, label: String, is_selected: bool) -> Box<dyn Renderable> {
let prefix = if is_selected {
format!("› {}. ", index + 1)
} else {
format!(" {}. ", index + 1)
};

let mut style = Style::default();
if is_selected {
style = style.cyan();
}

let mut row = RowRenderable::new();
row.push(prefix.width() as u16, prefix.set_style(style));
row.push(
u16::MAX,
Paragraph::new(label)
.style(style)
.wrap(Wrap { trim: false }),
);

row.into()
}

impl KeyboardHandler for TrustDirectoryWidget {
Expand All @@ -121,8 +157,8 @@ impl KeyboardHandler for TrustDirectoryWidget {
KeyCode::Down | KeyCode::Char('j') => {
self.highlighted = TrustDirectorySelection::DontTrust;
}
KeyCode::Char('1') => self.handle_trust(),
KeyCode::Char('2') => self.handle_dont_trust(),
KeyCode::Char('1') | KeyCode::Char('y') => self.handle_trust(),
KeyCode::Char('2') | KeyCode::Char('n') => self.handle_dont_trust(),
KeyCode::Enter => match self.highlighted {
TrustDirectorySelection::Trust => self.handle_trust(),
TrustDirectorySelection::DontTrust => self.handle_dont_trust(),
Expand Down Expand Up @@ -161,12 +197,16 @@ impl TrustDirectoryWidget {

#[cfg(test)]
mod tests {
use crate::test_backend::VT100Backend;

use super::*;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use pretty_assertions::assert_eq;
use ratatui::Terminal;

use std::path::PathBuf;

#[test]
Expand All @@ -191,4 +231,23 @@ mod tests {
widget.handle_key_event(press);
assert_eq!(widget.selection, Some(TrustDirectorySelection::DontTrust));
}

#[test]
fn renders_snapshot_for_git_repo() {
let widget = TrustDirectoryWidget {
codex_home: PathBuf::from("."),
cwd: PathBuf::from("/workspace/project"),
is_git_repo: true,
selection: None,
highlighted: TrustDirectorySelection::Trust,
error: None,
};

let mut terminal = Terminal::new(VT100Backend::new(70, 14)).expect("terminal");
terminal
.draw(|f| (&widget).render_ref(f.area(), f.buffer_mut()))
.expect("draw");

insta::assert_snapshot!(terminal.backend());
}
}
8 changes: 4 additions & 4 deletions codex-rs/tui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ pub mod renderable;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Insets {
pub left: u16,
pub top: u16,
pub right: u16,
pub bottom: u16,
left: u16,
top: u16,
right: u16,
bottom: u16,
}

impl Insets {
Expand Down
Loading
Loading