tui: reject repeated model modifiers

Track whether /model effort and scope modifiers were provided so repeated sentinel values like default and global are rejected consistently.

Validation: cargo test -p codex-tui; just fix -p codex-tui; just fmt

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-15 23:32:50 -07:00
co-authored by Codex
parent 8c45c1acfc
commit bcb3555e70
3 changed files with 38 additions and 18 deletions
+1
View File
@@ -881,6 +881,7 @@ impl App {
resume_config.clone(),
target_session.path.clone(),
self.auth_manager.clone(),
None,
)
.await
{
+11 -18
View File
@@ -5671,41 +5671,34 @@ impl ChatWidget {
fn parse_model_selection_args(
args: &str,
) -> Result<(String, Option<ReasoningEffortConfig>, ModelSelectionScope), String> {
const MODEL_USAGE: &str = "Usage: /model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]";
let mut tokens = args.split_whitespace();
let Some(model) = tokens.next() else {
return Err(
"Usage: /model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]"
.to_string(),
);
return Err(MODEL_USAGE.to_string());
};
let mut effort = None;
let mut saw_effort = false;
let mut scope = ModelSelectionScope::Global;
let mut saw_scope = false;
for token in tokens {
if let Some(parsed_effort) = Self::parse_model_reasoning_effort_token(token) {
if effort.is_some() {
return Err(
"Usage: /model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]"
.to_string(),
);
if saw_effort {
return Err(MODEL_USAGE.to_string());
}
saw_effort = true;
effort = parsed_effort;
continue;
}
if let Some(parsed_scope) = Self::parse_model_scope_token(token) {
if scope != ModelSelectionScope::Global {
return Err(
"Usage: /model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]"
.to_string(),
);
if saw_scope {
return Err(MODEL_USAGE.to_string());
}
saw_scope = true;
scope = parsed_scope;
continue;
}
return Err(
"Usage: /model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]"
.to_string(),
);
return Err(MODEL_USAGE.to_string());
}
Ok((model.to_string(), effort, scope))
+26
View File
@@ -8089,6 +8089,32 @@ async fn single_reasoning_option_skips_selection() {
);
}
#[test]
fn model_parser_rejects_repeated_effort_when_first_token_is_default() {
let parsed = ChatWidget::parse_model_selection_args("gpt-5 default high");
assert_eq!(
parsed,
Err(
"Usage: /model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]"
.to_string(),
)
);
}
#[test]
fn model_parser_rejects_repeated_scope_when_first_token_is_global() {
let parsed = ChatWidget::parse_model_selection_args("gpt-5 global plan-only");
assert_eq!(
parsed,
Err(
"Usage: /model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]"
.to_string(),
)
);
}
#[tokio::test]
async fn feedback_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;