mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
3 Commits
shijie/lin
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5158d49d75 | ||
|
|
dcb75ca8e7 | ||
|
|
7cb7f07a5b |
@@ -1,4 +1,5 @@
|
||||
use codex_core::protocol_config_types::ReasoningEffort;
|
||||
use codex_protocol::mcp_protocol::AuthMode;
|
||||
|
||||
/// A simple preset pairing a model slug with a reasoning effort.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -13,6 +14,8 @@ pub struct ModelPreset {
|
||||
pub model: &'static str,
|
||||
/// Reasoning effort to apply for this preset.
|
||||
pub effort: Option<ReasoningEffort>,
|
||||
/// Authentication modes this preset is enabled for.
|
||||
pub enabled_for_auth: &'static [AuthMode],
|
||||
}
|
||||
|
||||
/// Built-in list of model presets that pair a model with a reasoning effort.
|
||||
@@ -27,6 +30,7 @@ pub fn builtin_model_presets() -> &'static [ModelPreset] {
|
||||
description: "",
|
||||
model: "swiftfox-low",
|
||||
effort: None,
|
||||
enabled_for_auth: &[AuthMode::ChatGPT],
|
||||
},
|
||||
ModelPreset {
|
||||
id: "swiftfox-medium",
|
||||
@@ -34,6 +38,7 @@ pub fn builtin_model_presets() -> &'static [ModelPreset] {
|
||||
description: "",
|
||||
model: "swiftfox-medium",
|
||||
effort: None,
|
||||
enabled_for_auth: &[AuthMode::ChatGPT],
|
||||
},
|
||||
ModelPreset {
|
||||
id: "swiftfox-high",
|
||||
@@ -41,6 +46,7 @@ pub fn builtin_model_presets() -> &'static [ModelPreset] {
|
||||
description: "",
|
||||
model: "swiftfox-high",
|
||||
effort: None,
|
||||
enabled_for_auth: &[AuthMode::ChatGPT],
|
||||
},
|
||||
ModelPreset {
|
||||
id: "gpt-5-minimal",
|
||||
@@ -48,6 +54,7 @@ pub fn builtin_model_presets() -> &'static [ModelPreset] {
|
||||
description: "— fastest responses with limited reasoning; ideal for coding, instructions, or lightweight tasks",
|
||||
model: "gpt-5",
|
||||
effort: Some(ReasoningEffort::Minimal),
|
||||
enabled_for_auth: &[AuthMode::ApiKey, AuthMode::ChatGPT],
|
||||
},
|
||||
ModelPreset {
|
||||
id: "gpt-5-low",
|
||||
@@ -55,6 +62,7 @@ pub fn builtin_model_presets() -> &'static [ModelPreset] {
|
||||
description: "— balances speed with some reasoning; useful for straightforward queries and short explanations",
|
||||
model: "gpt-5",
|
||||
effort: Some(ReasoningEffort::Low),
|
||||
enabled_for_auth: &[AuthMode::ApiKey, AuthMode::ChatGPT],
|
||||
},
|
||||
ModelPreset {
|
||||
id: "gpt-5-medium",
|
||||
@@ -62,6 +70,7 @@ pub fn builtin_model_presets() -> &'static [ModelPreset] {
|
||||
description: "— default setting; provides a solid balance of reasoning depth and latency for general-purpose tasks",
|
||||
model: "gpt-5",
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
enabled_for_auth: &[AuthMode::ApiKey, AuthMode::ChatGPT],
|
||||
},
|
||||
ModelPreset {
|
||||
id: "gpt-5-high",
|
||||
@@ -69,6 +78,7 @@ pub fn builtin_model_presets() -> &'static [ModelPreset] {
|
||||
description: "— maximizes reasoning depth for complex or ambiguous problems",
|
||||
model: "gpt-5",
|
||||
effort: Some(ReasoningEffort::High),
|
||||
enabled_for_auth: &[AuthMode::ApiKey, AuthMode::ChatGPT],
|
||||
},
|
||||
];
|
||||
PRESETS
|
||||
|
||||
@@ -185,12 +185,14 @@ impl WidgetRef for CommandPopup {
|
||||
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
|
||||
is_current: false,
|
||||
description: Some(cmd.description().to_string()),
|
||||
enabled: true,
|
||||
},
|
||||
CommandItem::UserPrompt(i) => GenericDisplayRow {
|
||||
name: format!("/{}", self.prompts[i].name),
|
||||
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
|
||||
is_current: false,
|
||||
description: Some("send saved prompt".to_string()),
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -128,6 +128,7 @@ impl WidgetRef for &FileSearchPopup {
|
||||
.map(|v| v.iter().map(|&i| i as usize).collect()),
|
||||
is_current: false,
|
||||
description: None,
|
||||
enabled: true,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(crate) struct SelectionItem {
|
||||
pub description: Option<String>,
|
||||
pub is_current: bool,
|
||||
pub actions: Vec<SelectionAction>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct ListSelectionView {
|
||||
@@ -48,6 +49,27 @@ impl ListSelectionView {
|
||||
let para = Paragraph::new(Line::from(Self::dim_prefix_span()));
|
||||
para.render(area, buf);
|
||||
}
|
||||
|
||||
fn ensure_selected_is_enabled(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
self.state.selected_idx = None;
|
||||
self.state.scroll_top = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if self
|
||||
.state
|
||||
.selected_idx
|
||||
.is_some_and(|idx| self.items.get(idx).is_some_and(|item| item.enabled))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.state.selected_idx = self.items.iter().position(|item| item.enabled);
|
||||
if self.state.selected_idx.is_none() {
|
||||
self.state.scroll_top = 0;
|
||||
}
|
||||
}
|
||||
pub fn new(
|
||||
title: String,
|
||||
subtitle: Option<String>,
|
||||
@@ -69,25 +91,64 @@ impl ListSelectionView {
|
||||
s.state.selected_idx = Some(idx);
|
||||
}
|
||||
s.state.clamp_selection(len);
|
||||
s.ensure_selected_is_enabled();
|
||||
s.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
|
||||
s
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
let len = self.items.len();
|
||||
self.state.move_up_wrap(len);
|
||||
if len == 0 {
|
||||
self.state.selected_idx = None;
|
||||
self.state.scroll_top = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
self.ensure_selected_is_enabled();
|
||||
let Some(mut idx) = self.state.selected_idx else {
|
||||
return;
|
||||
};
|
||||
for _ in 0..len {
|
||||
idx = if idx == 0 { len - 1 } else { idx - 1 };
|
||||
if self.items[idx].enabled {
|
||||
self.state.selected_idx = Some(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
|
||||
}
|
||||
|
||||
fn move_down(&mut self) {
|
||||
let len = self.items.len();
|
||||
self.state.move_down_wrap(len);
|
||||
if len == 0 {
|
||||
self.state.selected_idx = None;
|
||||
self.state.scroll_top = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
self.ensure_selected_is_enabled();
|
||||
let Some(mut idx) = self.state.selected_idx else {
|
||||
return;
|
||||
};
|
||||
for _ in 0..len {
|
||||
idx = (idx + 1) % len;
|
||||
if self.items[idx].enabled {
|
||||
self.state.selected_idx = Some(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
|
||||
}
|
||||
|
||||
fn accept(&mut self) {
|
||||
self.ensure_selected_is_enabled();
|
||||
if let Some(idx) = self.state.selected_idx {
|
||||
if let Some(item) = self.items.get(idx) {
|
||||
if !item.enabled {
|
||||
return;
|
||||
}
|
||||
for act in &item.actions {
|
||||
act(&self.app_event_tx);
|
||||
}
|
||||
@@ -220,6 +281,7 @@ impl BottomPaneView for ListSelectionView {
|
||||
match_indices: None,
|
||||
is_current: it.is_current,
|
||||
description: it.description.clone(),
|
||||
enabled: it.enabled,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -266,12 +328,14 @@ mod tests {
|
||||
description: Some("Codex can read files".to_string()),
|
||||
is_current: true,
|
||||
actions: vec![],
|
||||
enabled: true,
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Full Access".to_string(),
|
||||
description: Some("Codex can edit files".to_string()),
|
||||
is_current: false,
|
||||
actions: vec![],
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
ListSelectionView::new(
|
||||
@@ -321,4 +385,32 @@ mod tests {
|
||||
let view = make_selection_view(Some("Switch between Codex approval presets"));
|
||||
assert_snapshot!("list_selection_spacing_with_subtitle", render_lines(&view));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_disabled_items_when_initial_selection_is_disabled() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Swiftfox".to_string(),
|
||||
description: None,
|
||||
is_current: true,
|
||||
actions: vec![],
|
||||
enabled: false,
|
||||
},
|
||||
SelectionItem {
|
||||
name: "GPT".to_string(),
|
||||
description: None,
|
||||
is_current: false,
|
||||
actions: vec![],
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
let view = ListSelectionView::new("Select Model".to_string(), None, None, items, tx);
|
||||
let rendered = render_lines(&view);
|
||||
assert!(
|
||||
rendered.contains("> 2. GPT"),
|
||||
"expected enabled item to be selected: {rendered}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub(crate) struct GenericDisplayRow {
|
||||
pub match_indices: Option<Vec<usize>>, // indices to bold (char positions)
|
||||
pub is_current: bool,
|
||||
pub description: Option<String>, // optional grey text after the name
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl GenericDisplayRow {}
|
||||
@@ -73,6 +74,7 @@ pub(crate) fn render_rows(
|
||||
match_indices,
|
||||
is_current: _is_current,
|
||||
description,
|
||||
enabled,
|
||||
} = row;
|
||||
|
||||
// Highlight fuzzy indices when present.
|
||||
@@ -97,12 +99,15 @@ pub(crate) fn render_rows(
|
||||
}
|
||||
|
||||
let mut cell = Cell::from(Line::from(spans));
|
||||
if Some(i) == state.selected_idx {
|
||||
let is_selected = Some(i) == state.selected_idx;
|
||||
if is_selected && *enabled {
|
||||
cell = cell.style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
} else if !*enabled || is_selected {
|
||||
cell = cell.style(Style::default().add_modifier(Modifier::DIM));
|
||||
}
|
||||
rows.push(Row::new(vec![cell]));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::auth::CodexAuth;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
@@ -85,7 +86,9 @@ use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::mcp_protocol::AuthMode;
|
||||
use codex_protocol::mcp_protocol::ConversationId;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Track information about an in-flight exec command.
|
||||
struct RunningCommand {
|
||||
@@ -1173,41 +1176,46 @@ impl ChatWidget {
|
||||
let current_effort = self.config.model_reasoning_effort;
|
||||
let presets: &[ModelPreset] = builtin_model_presets();
|
||||
|
||||
let auth_mode = self.auth_mode_for_model_popup();
|
||||
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
let mut disabled_families_shown: HashSet<String> = HashSet::new();
|
||||
|
||||
for preset in presets.iter() {
|
||||
let name = preset.label.to_string();
|
||||
let description = Some(preset.description.to_string());
|
||||
let disabled_for_auth = Self::is_preset_disabled_for_auth(preset, auth_mode);
|
||||
|
||||
if disabled_for_auth {
|
||||
let family = Self::model_family_key_from_slug(preset.model);
|
||||
if disabled_families_shown.insert(family.clone()) {
|
||||
let description =
|
||||
Self::description_with_auth_hint(preset.description, auth_mode, true);
|
||||
let is_current =
|
||||
Self::is_current_for_family(¤t_model, &family, preset.model);
|
||||
items.push(SelectionItem {
|
||||
name: family,
|
||||
description,
|
||||
is_current,
|
||||
enabled: false,
|
||||
actions: Vec::new(),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let description =
|
||||
Self::description_with_auth_hint(preset.description, auth_mode, false);
|
||||
let is_current = preset.model == current_model && preset.effort == current_effort;
|
||||
let model_slug = preset.model.to_string();
|
||||
let effort = preset.effort;
|
||||
let current_model = current_model.clone();
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
model: Some(model_slug.clone()),
|
||||
effort: Some(effort),
|
||||
summary: None,
|
||||
}));
|
||||
tx.send(AppEvent::UpdateModel(model_slug.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
tracing::info!(
|
||||
"New model: {}, New effort: {}, Current model: {}, Current effort: {}",
|
||||
model_slug.clone(),
|
||||
effort
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
current_model,
|
||||
current_effort
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string())
|
||||
);
|
||||
})];
|
||||
let actions: Vec<SelectionAction> = vec![Self::build_model_selection_action(
|
||||
preset.model.to_string(),
|
||||
preset.effort,
|
||||
current_model.clone(),
|
||||
current_effort,
|
||||
)];
|
||||
items.push(SelectionItem {
|
||||
name,
|
||||
name: preset.label.to_string(),
|
||||
description,
|
||||
is_current,
|
||||
enabled: true,
|
||||
actions,
|
||||
});
|
||||
}
|
||||
@@ -1249,6 +1257,7 @@ impl ChatWidget {
|
||||
name,
|
||||
description,
|
||||
is_current,
|
||||
enabled: true,
|
||||
actions,
|
||||
});
|
||||
}
|
||||
@@ -1402,6 +1411,93 @@ impl ChatWidget {
|
||||
let [_, bottom_pane_area] = self.layout_areas(area);
|
||||
self.bottom_pane.cursor_pos(bottom_pane_area)
|
||||
}
|
||||
|
||||
fn auth_mode_for_model_popup(&self) -> Option<AuthMode> {
|
||||
if self.config.model_provider.requires_openai_auth {
|
||||
match CodexAuth::from_codex_home(&self.config.codex_home) {
|
||||
Ok(Some(auth)) => Some(auth.mode),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
debug!(?err, "failed to read auth state for model popup");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_preset_disabled_for_auth(preset: &ModelPreset, auth_mode: Option<AuthMode>) -> bool {
|
||||
match auth_mode {
|
||||
Some(mode) => !preset.enabled_for_auth.contains(&mode),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn model_family_key_from_slug(slug: &str) -> String {
|
||||
if let Some(s) = slug.strip_suffix("-low") {
|
||||
return s.to_string();
|
||||
}
|
||||
if let Some(s) = slug.strip_suffix("-medium") {
|
||||
return s.to_string();
|
||||
}
|
||||
if let Some(s) = slug.strip_suffix("-high") {
|
||||
return s.to_string();
|
||||
}
|
||||
slug.to_string()
|
||||
}
|
||||
|
||||
fn description_with_auth_hint(
|
||||
base: &str,
|
||||
auth_mode: Option<AuthMode>,
|
||||
disabled_for_auth: bool,
|
||||
) -> Option<String> {
|
||||
let mut out = base.to_string();
|
||||
if disabled_for_auth && matches!(auth_mode, Some(AuthMode::ApiKey)) {
|
||||
if out.is_empty() {
|
||||
out.push_str("(coming soon in API)");
|
||||
} else {
|
||||
out.push(' ');
|
||||
out.push_str("(coming soon in API)");
|
||||
}
|
||||
}
|
||||
if out.is_empty() { None } else { Some(out) }
|
||||
}
|
||||
|
||||
fn is_current_for_family(current_model: &str, family: &str, preset_model: &str) -> bool {
|
||||
current_model == preset_model || current_model.starts_with(&format!("{family}-"))
|
||||
}
|
||||
|
||||
fn build_model_selection_action(
|
||||
model_slug: String,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
current_model_for_log: String,
|
||||
current_effort: Option<ReasoningEffortConfig>,
|
||||
) -> SelectionAction {
|
||||
Box::new(move |tx| {
|
||||
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
model: Some(model_slug.clone()),
|
||||
effort: Some(effort),
|
||||
summary: None,
|
||||
}));
|
||||
tx.send(AppEvent::UpdateModel(model_slug.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
tracing::info!(
|
||||
"New model: {}, New effort: {}, Current model: {}, Current effort: {}",
|
||||
model_slug.clone(),
|
||||
effort
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
current_model_for_log,
|
||||
current_effort
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string())
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for &ChatWidget {
|
||||
|
||||
Reference in New Issue
Block a user