Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
67 changes: 67 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ pub struct Config {
/// and turn completions when not focused.
pub tui_notifications: Notifications,

/// Hide the startup tips showing command examples.
pub tui_hide_startup_tips: Option<bool>,

/// Hide the session header showing version, model, and directory info.
pub tui_hide_session_header: Option<bool>,

/// Custom placeholder text for the input area.
pub tui_input_placeholder: Option<String>,

/// The directory that should be treated as the current working directory
/// for the session. All relative paths inside the business-logic layer are
/// resolved against this path.
Expand Down Expand Up @@ -1170,6 +1179,9 @@ impl Config {
.as_ref()
.map(|t| t.notifications.clone())
.unwrap_or_default(),
tui_hide_startup_tips: cfg.tui.as_ref().and_then(|t| t.hide_startup_tips),
tui_hide_session_header: cfg.tui.as_ref().and_then(|t| t.hide_session_header),
tui_input_placeholder: cfg.tui.as_ref().and_then(|t| t.input_placeholder.clone()),
otel: {
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
Expand Down Expand Up @@ -1340,6 +1352,49 @@ persistence = "none"
assert_eq!(tui.notifications, Notifications::Enabled(false));
}

#[test]
fn tui_config_parses_customization_fields() {
let cfg_with_all = r#"
[tui]
hide_startup_tips = true
hide_session_header = true
input_placeholder = "Custom placeholder"
"#;

let parsed = toml::from_str::<ConfigToml>(cfg_with_all)
.expect("TUI config with customization fields should succeed");
let tui = parsed.tui.expect("config should include tui section");

assert_eq!(tui.hide_startup_tips, Some(true));
assert_eq!(tui.hide_session_header, Some(true));
assert_eq!(tui.input_placeholder, Some("Custom placeholder".to_string()));

let cfg_empty = r#"
[tui]
"#;

let parsed_empty = toml::from_str::<ConfigToml>(cfg_empty)
.expect("TUI config without customization fields should succeed");
let tui_empty = parsed_empty.tui.expect("config should include tui section");

assert_eq!(tui_empty.hide_startup_tips, None);
assert_eq!(tui_empty.hide_session_header, None);
assert_eq!(tui_empty.input_placeholder, None);

let cfg_false = r#"
[tui]
hide_startup_tips = false
hide_session_header = false
"#;

let parsed_false = toml::from_str::<ConfigToml>(cfg_false)
.expect("TUI config with false values should succeed");
let tui_false = parsed_false.tui.expect("config should include tui section");

assert_eq!(tui_false.hide_startup_tips, Some(false));
assert_eq!(tui_false.hide_session_header, Some(false));
}

#[test]
fn test_sandbox_config_parsing() {
let sandbox_full_access = r#"
Expand Down Expand Up @@ -2912,6 +2967,9 @@ model_verbosity = "high"
notices: Default::default(),
disable_paste_burst: false,
tui_notifications: Default::default(),
tui_hide_startup_tips: None,
tui_hide_session_header: None,
tui_input_placeholder: None,
otel: OtelConfig::default(),
},
o3_profile_config
Expand Down Expand Up @@ -2984,6 +3042,9 @@ model_verbosity = "high"
notices: Default::default(),
disable_paste_burst: false,
tui_notifications: Default::default(),
tui_hide_startup_tips: None,
tui_hide_session_header: None,
tui_input_placeholder: None,
otel: OtelConfig::default(),
};

Expand Down Expand Up @@ -3071,6 +3132,9 @@ model_verbosity = "high"
notices: Default::default(),
disable_paste_burst: false,
tui_notifications: Default::default(),
tui_hide_startup_tips: None,
tui_hide_session_header: None,
tui_input_placeholder: None,
otel: OtelConfig::default(),
};

Expand Down Expand Up @@ -3144,6 +3208,9 @@ model_verbosity = "high"
notices: Default::default(),
disable_paste_burst: false,
tui_notifications: Default::default(),
tui_hide_startup_tips: None,
tui_hide_session_header: None,
tui_input_placeholder: None,
otel: OtelConfig::default(),
};

Expand Down
14 changes: 14 additions & 0 deletions codex-rs/core/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,20 @@ pub struct Tui {
/// Defaults to `false`.
#[serde(default)]
pub notifications: Notifications,

/// Hide the startup tips showing command examples.
/// Defaults to `false`.
#[serde(default)]
pub hide_startup_tips: Option<bool>,

/// Hide the session header showing version, model, and directory info.
/// Defaults to `false`.
#[serde(default)]
pub hide_session_header: Option<bool>,

/// Custom placeholder text for the input area.
#[serde(default)]
pub input_placeholder: Option<String>,
}

/// Settings for notices we display to users via the tui and app-server clients
Expand Down
34 changes: 25 additions & 9 deletions codex-rs/tui/src/bottom_pane/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,19 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
is_task_running: props.is_task_running,
})],
FooterMode::ShortcutSummary => {
let mut line = context_window_line(props.context_window_percent);
line.push_span(" · ".dim());
line.extend(vec![
key_hint::plain(KeyCode::Char('?')).into(),
" for shortcuts".dim(),
]);
vec![line]
if props.is_task_running {
vec![context_window_line(props.context_window_percent)]
} else {
let mut line = context_window_line(props.context_window_percent);
if props.context_window_percent.is_some() {
line.push_span(" · ".dim());
}
line.extend(vec![
key_hint::plain(KeyCode::Char('?')).into(),
" for shortcuts".dim(),
]);
vec![line]
}
}
FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState {
use_shift_enter_hint: props.use_shift_enter_hint,
Expand Down Expand Up @@ -222,8 +228,18 @@ fn build_columns(entries: Vec<Line<'static>>) -> Vec<Line<'static>> {
}

fn context_window_line(percent: Option<i64>) -> Line<'static> {
let percent = percent.unwrap_or(100).clamp(0, 100);
Line::from(vec![Span::from(format!("{percent}% context left")).dim()])
let mut spans: Vec<Span<'static>> = Vec::new();
match percent {
Some(percent) => {
let percent = percent.min(100).max(0) as u8;
spans.push(format!("{percent}%").bold());
spans.push(" context left".dim());
}
None => {
spans.push("? for shortcuts".dim());
}
}
Line::from(spans)
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
Expand Down
15 changes: 11 additions & 4 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -980,8 +980,7 @@ impl ChatWidget {
auth_manager,
feedback,
} = common;
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
let placeholder = get_placeholder_text(&config);
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);

Self {
Expand Down Expand Up @@ -1044,8 +1043,7 @@ impl ChatWidget {
auth_manager,
feedback,
} = common;
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
let placeholder = get_placeholder_text(&config);

let codex_op_tx =
spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone());
Expand Down Expand Up @@ -2449,6 +2447,15 @@ const EXAMPLE_PROMPTS: [&str; 6] = [
"Improve documentation in @filename",
];

fn get_placeholder_text(config: &Config) -> String {
if let Some(custom_placeholder) = &config.tui_input_placeholder {
custom_placeholder.clone()
} else {
let mut rng = rand::rng();
EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string()
}
}

// Extract the first bold (Markdown) element in the form **...** from `s`.
// Returns the inner text if found; otherwise `None`.
fn extract_first_bold(s: &str) -> Option<String> {
Expand Down
91 changes: 48 additions & 43 deletions codex-rs/tui/src/history_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,53 +556,58 @@ pub(crate) fn new_session_info(
rollout_path: _,
} = event;
if is_first_event {
let mut parts: Vec<Box<dyn HistoryCell>> = Vec::new();

// Header box rendered as history (so it appears at the very top)
let header = SessionHeaderHistoryCell::new(
model,
reasoning_effort,
config.cwd.clone(),
crate::version::CODEX_CLI_VERSION,
);
let hide_header = config.tui_hide_session_header.unwrap_or(false);
if !hide_header {
let header = SessionHeaderHistoryCell::new(
model,
reasoning_effort,
config.cwd.clone(),
crate::version::CODEX_CLI_VERSION,
);
parts.push(Box::new(header));
}

// Help lines below the header (new copy and list)
let help_lines: Vec<Line<'static>> = vec![
" To get started, describe a task or try one of these commands:"
.dim()
.into(),
Line::from(""),
Line::from(vec![
" ".into(),
"/init".into(),
" - create an AGENTS.md file with instructions for Codex".dim(),
]),
Line::from(vec![
" ".into(),
"/status".into(),
" - show current session configuration".dim(),
]),
Line::from(vec![
" ".into(),
"/approvals".into(),
" - choose what Codex can do without approval".dim(),
]),
Line::from(vec![
" ".into(),
"/model".into(),
" - choose what model and reasoning effort to use".dim(),
]),
Line::from(vec![
" ".into(),
"/review".into(),
" - review any changes and find issues".dim(),
]),
];

CompositeHistoryCell {
parts: vec![
Box::new(header),
Box::new(PlainHistoryCell { lines: help_lines }),
],
let hide_tips = config.tui_hide_startup_tips.unwrap_or(false);
if !hide_tips {
let help_lines: Vec<Line<'static>> = vec![
" To get started, describe a task or try one of these commands:"
.dim()
.into(),
Line::from(""),
Line::from(vec![
" ".into(),
"/init".into(),
" - create an AGENTS.md file with instructions for Codex".dim(),
]),
Line::from(vec![
" ".into(),
"/status".into(),
" - show current session configuration".dim(),
]),
Line::from(vec![
" ".into(),
"/approvals".into(),
" - choose what Codex can do without approval".dim(),
]),
Line::from(vec![
" ".into(),
"/model".into(),
" - choose what model and reasoning effort to use".dim(),
]),
Line::from(vec![
" ".into(),
"/review".into(),
" - review any changes and find issues".dim(),
]),
];
parts.push(Box::new(PlainHistoryCell { lines: help_lines }));
}

CompositeHistoryCell { parts }
} else if config.model == model {
CompositeHistoryCell { parts: vec![] }
} else {
Expand Down
18 changes: 17 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -829,12 +829,25 @@ notifications = true
# You can optionally filter to specific notification types.
# Available types are "agent-turn-complete" and "approval-requested".
notifications = [ "agent-turn-complete", "approval-requested" ]

# Hide the startup tips showing command examples like /init, /status, etc.
# Defaults to false (tips are shown).
hide_startup_tips = true

# Hide the session header showing "OpenAI Codex (vX)", model, and directory info.
# Defaults to false (header is shown).
hide_session_header = true

# Custom placeholder text for the input area. If not set, a random example
# from the built-in list will be used.
input_placeholder = "Ask me anything..."
```

> [!NOTE]
> Codex emits desktop notifications using terminal escape codes. Not all terminals support these (notably, macOS Terminal.app and VS Code's terminal do not support custom notifications. iTerm2, Ghostty and WezTerm do support these notifications).

> [!NOTE] > `tui.notifications` is built‑in and limited to the TUI session. For programmatic or cross‑environment notifications—or to integrate with OS‑specific notifiers—use the top‑level `notify` option to run an external program that receives event JSON. The two settings are independent and can be used together.
> [!NOTE]
> `tui.notifications` is built‑in and limited to the TUI session. For programmatic or cross‑environment notifications—or to integrate with OS‑specific notifiers—use the top‑level `notify` option to run an external program that receives event JSON. The two settings are independent and can be used together.

## Authentication and authorization

Expand Down Expand Up @@ -914,6 +927,9 @@ Valid values:
| `file_opener` | `vscode` \| `vscode-insiders` \| `windsurf` \| `cursor` \| `none` | URI scheme for clickable citations (default: `vscode`). |
| `tui` | table | TUI‑specific options. |
| `tui.notifications` | boolean \| array<string> | Enable desktop notifications in the tui (default: false). |
| `tui.hide_startup_tips` | boolean | Hide startup command tips (default: false). |
| `tui.hide_session_header` | boolean | Hide session header with version/model info (default: false). |
| `tui.input_placeholder` | string | Custom input placeholder text (optional). |
| `hide_agent_reasoning` | boolean | Hide model reasoning events. |
| `show_raw_agent_reasoning` | boolean | Show raw reasoning (when available). |
| `model_reasoning_effort` | `minimal` \| `low` \| `medium` \| `high` | Responses API reasoning effort. |
Expand Down