Skip to content

Commit d97bad1

Browse files
authored
Display YOLO mode permissions if set when launching TUI (#17877)
- When launching the TUI client, if YOLO mode is enabled, display this in the header. - Eligibility is determined by `approval_policy = "never"` and `sandbox_mode = "danger-full-access"` <img width="886" height="230" alt="image" src="https://github.com/user-attachments/assets/d7064778-e32c-4123-8e44-ca0c9016ab09" />
1 parent d63ba2d commit d97bad1

4 files changed

Lines changed: 81 additions & 11 deletions

File tree

codex-rs/tui/src/app.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,7 @@ impl App {
15991599
self.config.cwd.to_path_buf(),
16001600
version,
16011601
)
1602+
.with_yolo_mode(history_cell::is_yolo_mode(&self.config))
16021603
.display_lines(width)
16031604
}
16041605

codex-rs/tui/src/chatwidget.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9722,14 +9722,17 @@ impl ChatWidget {
97229722
/// Build a placeholder header cell while the session is configuring.
97239723
fn placeholder_session_header_cell(config: &Config) -> Box<dyn HistoryCell> {
97249724
let placeholder_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
9725-
Box::new(history_cell::SessionHeaderHistoryCell::new_with_style(
9726-
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
9727-
placeholder_style,
9728-
/*reasoning_effort*/ None,
9729-
/*show_fast_status*/ false,
9730-
config.cwd.to_path_buf(),
9731-
CODEX_CLI_VERSION,
9732-
))
9725+
Box::new(
9726+
history_cell::SessionHeaderHistoryCell::new_with_style(
9727+
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
9728+
placeholder_style,
9729+
/*reasoning_effort*/ None,
9730+
/*show_fast_status*/ false,
9731+
config.cwd.to_path_buf(),
9732+
CODEX_CLI_VERSION,
9733+
)
9734+
.with_yolo_mode(history_cell::is_yolo_mode(config)),
9735+
)
97339736
}
97349737

97359738
/// Merge the real session info cell with any placeholder header to avoid double boxes.

codex-rs/tui/src/history_cell.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@ use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
6161
use codex_protocol::plan_tool::PlanItemArg;
6262
use codex_protocol::plan_tool::StepStatus;
6363
use codex_protocol::plan_tool::UpdatePlanArgs;
64+
use codex_protocol::protocol::AskForApproval;
6465
use codex_protocol::protocol::FileChange;
6566
use codex_protocol::protocol::McpAuthStatus;
6667
use codex_protocol::protocol::McpInvocation;
68+
use codex_protocol::protocol::SandboxPolicy;
6769
use codex_protocol::protocol::SessionConfiguredEvent;
6870
use codex_protocol::request_user_input::RequestUserInputAnswer;
6971
use codex_protocol::request_user_input::RequestUserInputQuestion;
@@ -1186,6 +1188,8 @@ pub(crate) fn new_session_info(
11861188
let SessionConfiguredEvent {
11871189
model,
11881190
reasoning_effort,
1191+
approval_policy,
1192+
sandbox_policy,
11891193
..
11901194
} = event;
11911195
// Header box rendered as history (so it appears at the very top)
@@ -1195,7 +1199,8 @@ pub(crate) fn new_session_info(
11951199
show_fast_status,
11961200
config.cwd.to_path_buf(),
11971201
CODEX_CLI_VERSION,
1198-
);
1202+
)
1203+
.with_yolo_mode(has_yolo_permissions(approval_policy, &sandbox_policy));
11991204
let mut parts: Vec<Box<dyn HistoryCell>> = vec![Box::new(header)];
12001205

12011206
if is_first_event {
@@ -1259,6 +1264,17 @@ pub(crate) fn new_session_info(
12591264
SessionInfoCell(CompositeHistoryCell { parts })
12601265
}
12611266

1267+
pub(crate) fn is_yolo_mode(config: &Config) -> bool {
1268+
has_yolo_permissions(
1269+
config.permissions.approval_policy.value(),
1270+
config.permissions.sandbox_policy.get(),
1271+
)
1272+
}
1273+
1274+
fn has_yolo_permissions(approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy) -> bool {
1275+
approval_policy == AskForApproval::Never && *sandbox_policy == SandboxPolicy::DangerFullAccess
1276+
}
1277+
12621278
pub(crate) fn new_user_prompt(
12631279
message: String,
12641280
text_elements: Vec<TextElement>,
@@ -1281,6 +1297,7 @@ pub(crate) struct SessionHeaderHistoryCell {
12811297
reasoning_effort: Option<ReasoningEffortConfig>,
12821298
show_fast_status: bool,
12831299
directory: PathBuf,
1300+
yolo_mode: bool,
12841301
}
12851302

12861303
impl SessionHeaderHistoryCell {
@@ -1316,9 +1333,15 @@ impl SessionHeaderHistoryCell {
13161333
reasoning_effort,
13171334
show_fast_status,
13181335
directory,
1336+
yolo_mode: false,
13191337
}
13201338
}
13211339

1340+
pub(crate) fn with_yolo_mode(mut self, yolo_mode: bool) -> Self {
1341+
self.yolo_mode = yolo_mode;
1342+
self
1343+
}
1344+
13221345
fn format_directory(&self, max_width: Option<usize>) -> String {
13231346
Self::format_directory_inner(&self.directory, max_width)
13241347
}
@@ -1377,7 +1400,12 @@ impl HistoryCell for SessionHeaderHistoryCell {
13771400
const CHANGE_MODEL_HINT_COMMAND: &str = "/model";
13781401
const CHANGE_MODEL_HINT_EXPLANATION: &str = " to change";
13791402
const DIR_LABEL: &str = "directory:";
1380-
let label_width = DIR_LABEL.len();
1403+
const PERMISSIONS_LABEL: &str = "permissions:";
1404+
let label_width = if self.yolo_mode {
1405+
DIR_LABEL.len().max(PERMISSIONS_LABEL.len())
1406+
} else {
1407+
DIR_LABEL.len()
1408+
};
13811409

13821410
let model_label = format!(
13831411
"{model_label:<label_width$}",
@@ -1411,13 +1439,21 @@ impl HistoryCell for SessionHeaderHistoryCell {
14111439
let dir = self.format_directory(Some(dir_max_width));
14121440
let dir_spans = vec![Span::from(dir_prefix).dim(), Span::from(dir)];
14131441

1414-
let lines = vec![
1442+
let mut lines = vec![
14151443
make_row(title_spans),
14161444
make_row(Vec::new()),
14171445
make_row(model_spans),
14181446
make_row(dir_spans),
14191447
];
14201448

1449+
if self.yolo_mode {
1450+
let permissions_label = format!("{PERMISSIONS_LABEL:<label_width$}");
1451+
lines.push(make_row(vec![
1452+
Span::from(format!("{permissions_label} ")).dim(),
1453+
"YOLO mode".magenta().bold(),
1454+
]));
1455+
}
1456+
14211457
with_border(lines)
14221458
}
14231459
}
@@ -3956,6 +3992,25 @@ mod tests {
39563992
assert!(!model_line.contains("fast"));
39573993
}
39583994

3995+
#[test]
3996+
#[cfg_attr(
3997+
target_os = "windows",
3998+
ignore = "snapshot path rendering differs on Windows"
3999+
)]
4000+
fn session_header_indicates_yolo_mode() {
4001+
let cell = SessionHeaderHistoryCell::new(
4002+
"gpt-5".to_string(),
4003+
/*reasoning_effort*/ None,
4004+
/*show_fast_status*/ false,
4005+
test_path_buf("/tmp/project").abs().to_path_buf(),
4006+
"test",
4007+
)
4008+
.with_yolo_mode(/*yolo_mode*/ true);
4009+
4010+
let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n");
4011+
insta::assert_snapshot!(rendered);
4012+
}
4013+
39594014
#[test]
39604015
fn session_header_directory_center_truncates() {
39614016
let mut dir = home_dir().expect("home directory");
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: tui/src/history_cell.rs
3+
expression: rendered
4+
---
5+
╭───────────────────────────────────────╮
6+
>_ OpenAI Codex (vtest) │
7+
│ │
8+
model: gpt-5 /model to change
9+
directory: /tmp/project
10+
permissions: YOLO mode
11+
╰───────────────────────────────────────╯

0 commit comments

Comments
 (0)