diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 60e6aae813..5f6a189ffe 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -256,6 +256,16 @@ impl ChatComposer<'_> { self.textarea.select_all(); self.textarea.cut(); let _ = self.textarea.insert_str(format!("/{} ", cmd.command())); + } else { + // Special case: if the command is `/model` and the line is exactly "/model" + // (no trailing space), add a space to trigger the model dropdown. + if *cmd == SlashCommand::Model { + let trimmed = first_line.trim_start(); + let expected = format!("/{}", cmd.command()); + if trimmed == expected { + let _ = self.textarea.insert_str(" "); + } + } } } (InputResult::None, true) @@ -392,6 +402,39 @@ impl ChatComposer<'_> { self.active_popup = ActivePopup::None; return (InputResult::None, true); } + // No selection in the list: treat the typed argument as the model name. + // Extract arguments after `/model` from the first line. + let first_line = self + .textarea + .lines() + .first() + .map(|s| s.as_str()) + .unwrap_or(""); + + let args = if let Some(stripped) = first_line.strip_prefix('/') { + let token = stripped.trim_start(); + let cmd_token = token.split_whitespace().next().unwrap_or(""); + if cmd_token == SlashCommand::Model.command() { + let rest = &token[cmd_token.len()..]; + rest.trim_start().to_string() + } else { + String::new() + } + } else { + String::new() + }; + + if !args.trim().is_empty() { + // Dispatch as a command with args so normalization is applied centrally. + self.app_event_tx + .send(AppEvent::DispatchCommandWithArgs(SlashCommand::Model, args)); + // Clear composer input and close the popup. + self.textarea.select_all(); + self.textarea.cut(); + self.pending_pastes.clear(); + self.active_popup = ActivePopup::None; + return (InputResult::None, true); + } (InputResult::None, false) } input => self.handle_input_basic(input), diff --git a/codex-rs/tui/src/bottom_pane/model_selection_popup.rs b/codex-rs/tui/src/bottom_pane/model_selection_popup.rs index 980c337e5f..27aec5abc3 100644 --- a/codex-rs/tui/src/bottom_pane/model_selection_popup.rs +++ b/codex-rs/tui/src/bottom_pane/model_selection_popup.rs @@ -70,7 +70,10 @@ impl ModelSelectionPopup { /// Move selection cursor down. pub(crate) fn move_down(&mut self) { let len = self.visible_rows().len(); - if len == 0 { self.selected_idx = None; return; } + if len == 0 { + self.selected_idx = None; + return; + } match self.selected_idx { Some(idx) if idx + 1 < len => self.selected_idx = Some(idx + 1), None => self.selected_idx = Some(0), @@ -81,9 +84,8 @@ impl ModelSelectionPopup { /// Currently selected model name, if any. pub(crate) fn selected_model(&self) -> Option { let rows = self.visible_rows(); - self.selected_idx.and_then(|idx| match rows.get(idx) { - Some(DisplayRow::Model { name, .. }) => Some(name.clone()), - None => None, + self.selected_idx.and_then(|idx| { + rows.get(idx).map(|DisplayRow::Model { name, .. }| name.clone()) }) }