mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Add configuration support for TUI input presets (#54379)
## What changed Add `tui.presets` entries keyed by a single digit from `0` to `9`, each with an `input` prompt or slash command and an optional `label`. Expose presets in the configuration schema, load them into runtime configuration, and preserve them in TUI local settings. Reject invalid slots, blank inputs, shell commands beginning with `!` after leading whitespace, and inputs exceeding 8,000 UTF-8 bytes. ## Testing Extend the runtime configuration test to verify loading both a slash command with a label and a prompt without one. GitOrigin-RevId: d2911e540bc50f05b4a5224abf17ce4ef5a18ea3
This commit is contained in:
@@ -822,6 +822,32 @@ pub struct ModelAvailabilityNuxConfig {
|
||||
/// Fallback resize-reflow row cap when Codex cannot identify a terminal-specific scrollback size.
|
||||
pub const DEFAULT_TERMINAL_RESIZE_REFLOW_FALLBACK_MAX_ROWS: usize = 1_000;
|
||||
|
||||
/// One reusable prompt or slash command submitted through the TUI composer.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields, rename = "Preset")]
|
||||
pub struct QuickDial {
|
||||
/// Plain-text input, limited to 8,000 UTF-8 bytes.
|
||||
pub input: String,
|
||||
/// Optional short name for the picker and shortcut hints.
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
impl QuickDial {
|
||||
/// Keep stored inputs nonempty and below the context-item limit even with byte tokenization.
|
||||
pub fn validate(&self) -> Result<(), &'static str> {
|
||||
if self.input.trim().is_empty() {
|
||||
return Err("Preset input cannot be empty.");
|
||||
}
|
||||
if self.input.trim_start().starts_with('!') {
|
||||
return Err("Presets support prompts and slash commands, not shell commands.");
|
||||
}
|
||||
if self.input.len() > 8_000 {
|
||||
return Err("Preset input cannot exceed 8,000 UTF-8 bytes.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of settings that are specific to the TUI.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
@@ -978,6 +1004,10 @@ pub struct Tui {
|
||||
#[serde(default)]
|
||||
pub keymap: TuiKeymap,
|
||||
|
||||
/// Saved inputs keyed by a single digit (`0` through `9`).
|
||||
#[serde(default, rename = "presets")]
|
||||
pub quick_dials: BTreeMap<String, QuickDial>,
|
||||
|
||||
/// Startup tooltip availability NUX state persisted by the TUI.
|
||||
#[serde(default)]
|
||||
pub model_availability_nux: ModelAvailabilityNuxConfig,
|
||||
|
||||
@@ -3767,6 +3767,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Preset": {
|
||||
"additionalProperties": false,
|
||||
"description": "One reusable prompt or slash command submitted through the TUI composer.",
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "Plain-text input, limited to 8,000 UTF-8 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"description": "Optional short name for the picker and shortcut hints.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProfileTui": {
|
||||
"additionalProperties": false,
|
||||
"description": "TUI settings supported inside a named profile.",
|
||||
@@ -4963,6 +4981,14 @@
|
||||
"default": "composer",
|
||||
"description": "Where the terminal pet should anchor vertically.\n\nDefaults to `composer`, which follows the current TUI composer viewport."
|
||||
},
|
||||
"presets": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Preset"
|
||||
},
|
||||
"default": {},
|
||||
"description": "Saved inputs keyed by a single digit (`0` through `9`).",
|
||||
"type": "object"
|
||||
},
|
||||
"question_esc_back": {
|
||||
"default": true,
|
||||
"description": "Escape returns from async questions to the composer, preserving the answer draft.",
|
||||
|
||||
@@ -1284,6 +1284,7 @@ fn config_toml_deserializes_model_availability_nux() {
|
||||
agents_overview_grouping: Default::default(),
|
||||
resume_cwd: None,
|
||||
keymap: TuiKeymap::default(),
|
||||
quick_dials: BTreeMap::new(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig {
|
||||
shown_count: HashMap::from([
|
||||
("gpt-bar".to_string(), 4),
|
||||
@@ -1364,6 +1365,13 @@ async fn runtime_config_uses_tui_raw_output_mode() {
|
||||
let toml = r#"
|
||||
[tui]
|
||||
raw_output_mode = true
|
||||
|
||||
[tui.presets.1]
|
||||
input = "/model gpt-5.6-sol high fast"
|
||||
label = "fast coding"
|
||||
|
||||
[tui.presets.2]
|
||||
input = "Review the current changes"
|
||||
"#;
|
||||
let cfg_toml: ConfigToml = toml::from_str(toml).expect("deserialize raw_output_mode=true");
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
@@ -1375,6 +1383,26 @@ async fn runtime_config_uses_tui_raw_output_mode() {
|
||||
.expect("load config");
|
||||
|
||||
assert!(cfg.tui_raw_output_mode);
|
||||
assert_eq!(
|
||||
cfg.tui_quick_dials,
|
||||
[
|
||||
(
|
||||
"1".into(),
|
||||
QuickDial {
|
||||
input: "/model gpt-5.6-sol high fast".into(),
|
||||
label: Some("fast coding".into()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"2".into(),
|
||||
QuickDial {
|
||||
input: "Review the current changes".into(),
|
||||
label: None,
|
||||
},
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4496,6 +4524,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
agents_overview_grouping: Default::default(),
|
||||
resume_cwd: None,
|
||||
keymap: TuiKeymap::default(),
|
||||
quick_dials: BTreeMap::new(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
terminal_resize_reflow_max_rows: None,
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ use codex_config::types::MemoriesConfig;
|
||||
use codex_config::types::ModelAvailabilityNuxConfig;
|
||||
use codex_config::types::Notice;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_config::types::QuickDial;
|
||||
use codex_config::types::ResumeCwdMode;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
@@ -892,6 +893,9 @@ pub struct Config {
|
||||
/// 3. built-in defaults
|
||||
pub tui_keymap: TuiKeymap,
|
||||
|
||||
/// Saved TUI inputs keyed by a single digit (`0` through `9`).
|
||||
pub tui_quick_dials: BTreeMap<String, QuickDial>,
|
||||
|
||||
/// The absolute 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.
|
||||
@@ -3307,6 +3311,20 @@ impl Config {
|
||||
|
||||
validate_model_providers(&cfg.model_providers)
|
||||
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
|
||||
for (slot, dial) in cfg.tui.iter().flat_map(|tui| &tui.quick_dials) {
|
||||
if !matches!(slot.as_bytes(), [b'0'..=b'9']) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("tui.presets.{slot}: slot must be a single digit from 0 to 9"),
|
||||
));
|
||||
}
|
||||
dial.validate().map_err(|reason| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("tui.presets.{slot}: {reason}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
if cfg.model_post_turn_compact_threshold_percent.is_some_and(|percent| percent > 100) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
@@ -4705,6 +4723,11 @@ impl Config {
|
||||
.as_ref()
|
||||
.map(|t| t.keymap.clone())
|
||||
.unwrap_or_default(),
|
||||
tui_quick_dials: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
.map(|t| t.quick_dials.clone())
|
||||
.unwrap_or_default(),
|
||||
otel,
|
||||
};
|
||||
if let Some(message) = config.credential_masking_warning(config.permissions.permission_profile()) {
|
||||
|
||||
@@ -308,6 +308,7 @@ async fn new_config(
|
||||
tui_pet_anchor: TuiPetAnchor::Composer,
|
||||
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
|
||||
tui_keymap: TuiKeymap::default(),
|
||||
tui_quick_dials: BTreeMap::new(),
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
tui_agents_overview_grouping: Default::default(),
|
||||
tui_resume_cwd: None,
|
||||
|
||||
@@ -84,6 +84,7 @@ impl LocalSettings {
|
||||
agents_overview_grouping: config.tui_agents_overview_grouping,
|
||||
resume_cwd: config.tui_resume_cwd,
|
||||
keymap: config.tui_keymap.clone(),
|
||||
quick_dials: config.tui_quick_dials.clone(),
|
||||
model_availability_nux: config.model_availability_nux.clone(),
|
||||
terminal_resize_reflow_max_rows: match config.terminal_resize_reflow.max_rows {
|
||||
TerminalResizeReflowMaxRows::Auto => None,
|
||||
|
||||
Reference in New Issue
Block a user