follow models

This commit is contained in:
James Peter
2025-11-21 09:59:05 +10:00
parent 62b8c390c8
commit d765fb9c47
11 changed files with 1117 additions and 96 deletions
+116 -4
View File
@@ -106,12 +106,24 @@ pub struct Config {
/// Optional override of model selection.
pub model: String,
/// Planning model (used when in Plan mode / Read Only access preset). Falls back to `model`.
pub planning_model: String,
/// Reasoning effort for planning model.
pub planning_model_reasoning_effort: ReasoningEffort,
/// Whether planning should inherit the chat model instead of using a dedicated override.
pub planning_use_chat_model: bool,
/// Model used specifically for review sessions. Defaults to "gpt-5.1-codex-mini".
pub review_model: String,
/// Reasoning effort used when running review sessions.
pub review_model_reasoning_effort: ReasoningEffort,
/// Whether review should inherit the chat model instead of using a dedicated override.
pub review_use_chat_model: bool,
pub model_family: ModelFamily,
/// Size of the context window for the model, in tokens.
@@ -255,6 +267,8 @@ pub struct Config {
/// Shared Auto Drive defaults.
pub auto_drive: AutoDriveSettings,
/// Whether Auto Drive should inherit the chat model instead of a dedicated override.
pub auto_drive_use_chat_model: bool,
/// Path to the `codex-linux-sandbox` executable. This must be set if
/// [`crate::exec::SandboxType::LinuxSeccomp`] is used. Note that this
@@ -987,6 +1001,7 @@ pub fn set_review_model(
code_home: &Path,
model: &str,
effort: ReasoningEffort,
use_chat_model: bool,
) -> anyhow::Result<()> {
let trimmed = model.trim();
if trimmed.is_empty() {
@@ -1001,9 +1016,53 @@ pub fn set_review_model(
Err(e) => return Err(e.into()),
};
doc["review_model"] = toml_edit::value(trimmed);
doc["review_model_reasoning_effort"] =
toml_edit::value(effort.to_string().to_ascii_lowercase());
doc["review_use_chat_model"] = toml_edit::value(use_chat_model);
if use_chat_model {
doc.remove("review_model");
doc.remove("review_model_reasoning_effort");
} else {
doc["review_model"] = toml_edit::value(trimmed);
doc["review_model_reasoning_effort"] =
toml_edit::value(effort.to_string().to_ascii_lowercase());
}
std::fs::create_dir_all(code_home)?;
let tmp_file = NamedTempFile::new_in(code_home)?;
std::fs::write(tmp_file.path(), doc.to_string())?;
tmp_file.persist(config_path)?;
Ok(())
}
/// Persist the planning model + reasoning effort into `CODEX_HOME/config.toml`.
pub fn set_planning_model(
code_home: &Path,
model: &str,
effort: ReasoningEffort,
use_chat_model: bool,
) -> anyhow::Result<()> {
let config_path = code_home.join(CONFIG_TOML_FILE);
let read_path = resolve_code_path_for_read(code_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(contents) => contents.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
};
doc["planning_use_chat_model"] = toml_edit::value(use_chat_model);
if use_chat_model {
doc.remove("planning_model");
doc.remove("planning_model_reasoning_effort");
} else {
let trimmed = model.trim();
if trimmed.is_empty() {
return Err(anyhow::anyhow!("planning model cannot be empty"));
}
doc["planning_model"] = toml_edit::value(trimmed);
doc["planning_model_reasoning_effort"] =
toml_edit::value(effort.to_string().to_ascii_lowercase());
}
std::fs::create_dir_all(code_home)?;
let tmp_file = NamedTempFile::new_in(code_home)?;
@@ -1017,6 +1076,7 @@ pub fn set_review_model(
pub fn set_auto_drive_settings(
code_home: &Path,
settings: &AutoDriveSettings,
use_chat_model: bool,
) -> anyhow::Result<()> {
let config_path = code_home.join(CONFIG_TOML_FILE);
let read_path = resolve_code_path_for_read(code_home, Path::new(CONFIG_TOML_FILE));
@@ -1031,6 +1091,8 @@ pub fn set_auto_drive_settings(
tui_tbl.remove("auto_drive");
}
doc["auto_drive_use_chat_model"] = toml_edit::value(use_chat_model);
doc["auto_drive"]["review_enabled"] = toml_edit::value(settings.review_enabled);
doc["auto_drive"]["agents_enabled"] = toml_edit::value(settings.agents_enabled);
doc["auto_drive"]["qa_automation_enabled"] =
@@ -1073,8 +1135,9 @@ pub fn set_auto_drive_settings(
pub fn set_tui_auto_drive_settings(
code_home: &Path,
settings: &AutoDriveSettings,
use_chat_model: bool,
) -> anyhow::Result<()> {
set_auto_drive_settings(code_home, settings)
set_auto_drive_settings(code_home, settings, use_chat_model)
}
/// Persist the GitHub workflow check preference under `[github].check_workflows_on_push`.
@@ -1670,10 +1733,16 @@ pub struct ConfigToml {
pub planning_model: Option<String>,
/// Reasoning effort override used for the planning model.
pub planning_model_reasoning_effort: Option<ReasoningEffort>,
/// Inherit chat model for planning mode when true.
#[serde(default)]
pub planning_use_chat_model: bool,
/// Review model override used by the `/review` feature.
pub review_model: Option<String>,
/// Reasoning effort override used for the review model.
pub review_model_reasoning_effort: Option<ReasoningEffort>,
/// Inherit chat model for review flows when true.
#[serde(default)]
pub review_use_chat_model: bool,
/// Provider to use from the model_providers map.
pub model_provider: Option<String>,
@@ -1771,6 +1840,9 @@ pub struct ConfigToml {
/// Auto Drive behavioral defaults.
pub auto_drive: Option<AutoDriveSettings>,
/// If true, Auto Drive inherits the chat model instead of a dedicated override.
pub auto_drive_use_chat_model: Option<bool>,
#[serde(default)]
pub auto_drive_observer_cadence: Option<u32>,
@@ -2385,10 +2457,49 @@ impl Config {
.or(cfg.review_model_reasoning_effort)
.unwrap_or(ReasoningEffort::High);
let planning_use_chat_model = config_profile
.planning_use_chat_model
.or(Some(cfg.planning_use_chat_model))
.unwrap_or_else(|| {
config_profile.planning_model.is_none() && cfg.planning_model.is_none()
});
let planning_model = if planning_use_chat_model {
model.clone()
} else {
config_profile
.planning_model
.or(cfg.planning_model)
.unwrap_or_else(|| model.clone())
};
let planning_model_reasoning_effort = config_profile
.planning_model_reasoning_effort
.or(cfg.planning_model_reasoning_effort)
.unwrap_or_else(|| {
config_profile
.model_reasoning_effort
.unwrap_or(ReasoningEffort::Medium)
});
let review_use_chat_model = config_profile
.review_use_chat_model
.or(Some(cfg.review_use_chat_model))
.unwrap_or(false);
let review_model = if review_use_chat_model {
model.clone()
} else {
review_model
};
let auto_drive_use_chat_model = cfg.auto_drive_use_chat_model.unwrap_or(false);
let config = Self {
model,
planning_model,
planning_model_reasoning_effort,
planning_use_chat_model,
review_model,
review_model_reasoning_effort,
review_use_chat_model,
model_family,
model_context_window,
model_max_output_tokens,
@@ -2441,6 +2552,7 @@ impl Config {
.clone()
.or_else(|| cfg.tui.as_ref().and_then(|t| t.auto_drive.clone()))
.unwrap_or_default(),
auto_drive_use_chat_model,
code_linux_sandbox_exe,
active_profile: active_profile_name,
+4
View File
@@ -11,6 +11,9 @@ use crate::protocol::AskForApproval;
#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
pub struct ConfigProfile {
pub model: Option<String>,
pub planning_model: Option<String>,
pub planning_model_reasoning_effort: Option<ReasoningEffort>,
pub planning_use_chat_model: Option<bool>,
pub review_model: Option<String>,
/// The key in the `model_providers` map identifying the
/// [`ModelProviderInfo`] to use.
@@ -19,6 +22,7 @@ pub struct ConfigProfile {
pub disable_response_storage: Option<bool>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub review_model_reasoning_effort: Option<ReasoningEffort>,
pub review_use_chat_model: Option<bool>,
pub model_reasoning_summary: Option<ReasoningSummary>,
pub model_text_verbosity: Option<TextVerbosity>,
pub chatgpt_base_url: Option<String>,
+26
View File
@@ -1164,6 +1164,12 @@ impl App<'_> {
AppEvent::RequestRedraw => {
self.schedule_redraw();
}
AppEvent::UpdatePlanningUseChatModel(use_chat) => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.set_planning_use_chat_model(use_chat);
}
self.schedule_redraw();
}
AppEvent::FlushPendingExecEnds => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.flush_pending_exec_ends();
@@ -2297,11 +2303,26 @@ impl App<'_> {
widget.apply_review_model_selection(model, effort);
}
}
AppEvent::UpdateReviewUseChatModel(use_chat) => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.set_review_use_chat_model(use_chat);
}
}
AppEvent::UpdatePlanningModelSelection { model, effort } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.apply_planning_model_selection(model, effort);
}
}
AppEvent::UpdateAutoDriveModelSelection { model, effort } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.apply_auto_drive_model_selection(model, effort);
}
}
AppEvent::UpdateAutoDriveUseChatModel(use_chat) => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.set_auto_drive_use_chat_model(use_chat);
}
}
AppEvent::UpdateTextVerbosity(new_verbosity) => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.set_text_verbosity(new_verbosity);
@@ -2418,6 +2439,11 @@ impl App<'_> {
widget.show_review_model_selector();
}
}
AppEvent::ShowPlanningModelSelector => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.show_planning_model_selector();
}
}
AppEvent::ShowAutoDriveModelSelector => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.show_auto_drive_model_selector();
+13
View File
@@ -223,12 +223,23 @@ pub(crate) enum AppEvent {
model: String,
effort: ReasoningEffort,
},
/// Toggle review model inheritance from chat model
UpdateReviewUseChatModel(bool),
/// Update the planning (read-only) model + reasoning effort
UpdatePlanningModelSelection {
model: String,
effort: ReasoningEffort,
},
/// Toggle planning model inheritance from chat model
UpdatePlanningUseChatModel(bool),
/// Update the Auto Drive model + reasoning effort
UpdateAutoDriveModelSelection {
model: String,
effort: ReasoningEffort,
},
/// Toggle Auto Drive model inheritance from chat model
UpdateAutoDriveUseChatModel(bool),
/// Update the text verbosity level
UpdateTextVerbosity(TextVerbosity),
@@ -243,6 +254,8 @@ pub(crate) enum AppEvent {
UpdateReviewAutoResolveAttempts(u32),
/// Open the review model selector overlay
ShowReviewModelSelector,
/// Open the planning model selector overlay
ShowPlanningModelSelector,
/// Open the Auto Drive model selector overlay
ShowAutoDriveModelSelector,
/// Enable/disable a specific validation tool
+4
View File
@@ -50,6 +50,7 @@ mod login_accounts_view;
mod textarea;
pub mod form_text_field;
mod theme_selection_view;
mod planning_settings_view;
mod verbosity_selection_view;
pub(crate) mod validation_settings_view;
mod update_settings_view;
@@ -88,6 +89,7 @@ pub(crate) use update_settings_view::{UpdateSettingsView, UpdateSharedState};
pub(crate) use notifications_settings_view::{NotificationsMode, NotificationsSettingsView};
pub(crate) use validation_settings_view::ValidationSettingsView;
pub(crate) use review_settings_view::ReviewSettingsView;
pub(crate) use planning_settings_view::PlanningSettingsView;
use approval_modal_view::ApprovalModalView;
#[cfg(feature = "code-fork")]
use approval_ui::ApprovalUi;
@@ -678,12 +680,14 @@ impl BottomPane<'_> {
presets: Vec<ModelPreset>,
current_model: String,
current_effort: ReasoningEffort,
use_chat_model: bool,
target: ModelSelectionTarget,
) {
let view = ModelSelectionView::new(
presets,
current_model,
current_effort,
use_chat_model,
target,
self.app_event_tx.clone(),
);
@@ -56,6 +56,7 @@ impl FlatPreset {
pub(crate) enum ModelSelectionTarget {
Session,
Review,
Planning,
AutoDrive,
}
@@ -64,6 +65,7 @@ impl ModelSelectionTarget {
match self {
ModelSelectionTarget::Session => "Select Model & Reasoning",
ModelSelectionTarget::Review => "Select Review Model & Reasoning",
ModelSelectionTarget::Planning => "Select Planning Model & Reasoning",
ModelSelectionTarget::AutoDrive => "Select Auto Drive Model & Reasoning",
}
}
@@ -72,6 +74,7 @@ impl ModelSelectionTarget {
match self {
ModelSelectionTarget::Session => "Current model",
ModelSelectionTarget::Review => "Review model",
ModelSelectionTarget::Planning => "Planning model",
ModelSelectionTarget::AutoDrive => "Auto Drive model",
}
}
@@ -80,9 +83,14 @@ impl ModelSelectionTarget {
match self {
ModelSelectionTarget::Session => "Reasoning effort",
ModelSelectionTarget::Review => "Review reasoning",
ModelSelectionTarget::Planning => "Planning reasoning",
ModelSelectionTarget::AutoDrive => "Auto Drive reasoning",
}
}
fn supports_follow_chat(self) -> bool {
!matches!(self, ModelSelectionTarget::Session)
}
}
pub(crate) struct ModelSelectionView {
@@ -90,16 +98,24 @@ pub(crate) struct ModelSelectionView {
selected_index: usize,
current_model: String,
current_effort: ReasoningEffort,
use_chat_model: bool,
app_event_tx: AppEventSender,
is_complete: bool,
target: ModelSelectionTarget,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum EntryKind {
FollowChat,
Preset(usize),
}
impl ModelSelectionView {
pub fn new(
presets: Vec<ModelPreset>,
current_model: String,
current_effort: ReasoningEffort,
use_chat_model: bool,
target: ModelSelectionTarget,
app_event_tx: AppEventSender,
) -> Self {
@@ -108,12 +124,19 @@ impl ModelSelectionView {
.flat_map(FlatPreset::from_model_preset)
.collect();
let initial_index = Self::initial_selection(&flat_presets, &current_model, current_effort);
let initial_index = Self::initial_selection(
target.supports_follow_chat(),
use_chat_model,
&flat_presets,
&current_model,
current_effort,
);
Self {
flat_presets,
selected_index: initial_index,
current_model,
current_effort,
use_chat_model,
app_event_tx,
is_complete: false,
target,
@@ -121,14 +144,20 @@ impl ModelSelectionView {
}
fn initial_selection(
include_follow_chat: bool,
use_chat_model: bool,
flat_presets: &[FlatPreset],
current_model: &str,
current_effort: ReasoningEffort,
) -> usize {
if include_follow_chat && use_chat_model {
return 0;
}
if let Some((idx, _)) = flat_presets.iter().enumerate().find(|(_, preset)| {
preset.model.eq_ignore_ascii_case(current_model) && preset.effort == current_effort
}) {
return idx;
return idx + if include_follow_chat { 1 } else { 0 };
}
if let Some((idx, _)) = flat_presets
@@ -136,10 +165,18 @@ impl ModelSelectionView {
.enumerate()
.find(|(_, preset)| preset.model.eq_ignore_ascii_case(current_model))
{
return idx;
return idx + if include_follow_chat { 1 } else { 0 };
}
0
if include_follow_chat {
if flat_presets.is_empty() {
0
} else {
1
}
} else {
0
}
}
fn format_model_header(model: &str) -> String {
@@ -172,72 +209,109 @@ impl ModelSelectionView {
parts.join("-")
}
fn move_selection_up(&mut self) {
if self.flat_presets.is_empty() {
return;
fn entries(&self) -> Vec<EntryKind> {
let mut entries = Vec::new();
if self.target.supports_follow_chat() {
entries.push(EntryKind::FollowChat);
}
let sorted = self.sorted_indices();
if sorted.is_empty() {
return;
for idx in self.sorted_indices() {
entries.push(EntryKind::Preset(idx));
}
entries
}
let current_pos = sorted
.iter()
.position(|&idx| idx == self.selected_index)
.unwrap_or(0);
let new_pos = if current_pos == 0 {
sorted.len() - 1
fn move_selection_up(&mut self) {
let total = self.entries().len();
if total == 0 {
return;
}
self.selected_index = if self.selected_index == 0 {
total - 1
} else {
current_pos - 1
self.selected_index.saturating_sub(1)
};
self.selected_index = sorted[new_pos];
}
fn move_selection_down(&mut self) {
if self.flat_presets.is_empty() {
let total = self.entries().len();
if total == 0 {
return;
}
let sorted = self.sorted_indices();
if sorted.is_empty() {
return;
}
let current_pos = sorted
.iter()
.position(|&idx| idx == self.selected_index)
.unwrap_or(0);
let new_pos = (current_pos + 1) % sorted.len();
self.selected_index = sorted[new_pos];
self.selected_index = (self.selected_index + 1) % total;
}
fn confirm_selection(&mut self) {
if let Some(flat_preset) = self.flat_presets.get(self.selected_index) {
match self.target {
ModelSelectionTarget::Session => {
let _ = self.app_event_tx.send(AppEvent::UpdateModelSelection {
model: flat_preset.model.clone(),
effort: Some(flat_preset.effort),
});
let entries = self.entries();
if let Some(entry) = entries.get(self.selected_index) {
match entry {
EntryKind::FollowChat => {
match self.target {
ModelSelectionTarget::Session => {}
ModelSelectionTarget::Review => {
let _ =
self.app_event_tx.send(AppEvent::UpdateReviewUseChatModel(true));
}
ModelSelectionTarget::Planning => {
let _ = self
.app_event_tx
.send(AppEvent::UpdatePlanningUseChatModel(true));
}
ModelSelectionTarget::AutoDrive => {
let _ = self
.app_event_tx
.send(AppEvent::UpdateAutoDriveUseChatModel(true));
}
}
self.is_complete = true;
return;
}
ModelSelectionTarget::Review => {
let _ = self.app_event_tx.send(AppEvent::UpdateReviewModelSelection {
model: flat_preset.model.clone(),
effort: flat_preset.effort,
});
}
ModelSelectionTarget::AutoDrive => {
let _ = self.app_event_tx.send(AppEvent::UpdateAutoDriveModelSelection {
model: flat_preset.model.clone(),
effort: flat_preset.effort,
});
EntryKind::Preset(idx) => {
if let Some(flat_preset) = self.flat_presets.get(*idx) {
match self.target {
ModelSelectionTarget::Session => {
let _ = self.app_event_tx.send(AppEvent::UpdateModelSelection {
model: flat_preset.model.clone(),
effort: Some(flat_preset.effort),
});
}
ModelSelectionTarget::Review => {
let _ = self
.app_event_tx
.send(AppEvent::UpdateReviewModelSelection {
model: flat_preset.model.clone(),
effort: flat_preset.effort,
});
}
ModelSelectionTarget::Planning => {
let _ = self
.app_event_tx
.send(AppEvent::UpdatePlanningModelSelection {
model: flat_preset.model.clone(),
effort: flat_preset.effort,
});
}
ModelSelectionTarget::AutoDrive => {
let _ = self
.app_event_tx
.send(AppEvent::UpdateAutoDriveModelSelection {
model: flat_preset.model.clone(),
effort: flat_preset.effort,
});
}
}
}
self.is_complete = true;
}
}
}
self.is_complete = true;
}
fn content_line_count(&self) -> u16 {
let mut lines: u16 = 3;
if self.target.supports_follow_chat() {
// Follow row + spacer
lines = lines.saturating_add(2);
}
let mut previous_model: Option<&str> = None;
for idx in self.sorted_indices() {
@@ -377,7 +451,11 @@ impl ModelSelectionView {
Style::default().fg(crate::colors::text_dim()),
),
Span::styled(
self.current_model.clone(),
if self.target.supports_follow_chat() && self.use_chat_model {
"Follow Chat model".to_string()
} else {
self.current_model.clone()
},
Style::default()
.fg(crate::colors::warning())
.add_modifier(Modifier::BOLD),
@@ -389,7 +467,11 @@ impl ModelSelectionView {
Style::default().fg(crate::colors::text_dim()),
),
Span::styled(
format!("{}", self.current_effort),
if self.target.supports_follow_chat() && self.use_chat_model {
"From chat".to_string()
} else {
format!("{}", self.current_effort)
},
Style::default()
.fg(crate::colors::warning())
.add_modifier(Modifier::BOLD),
@@ -397,9 +479,50 @@ impl ModelSelectionView {
]));
lines.push(Line::from(""));
if self.target.supports_follow_chat() {
let is_selected = self.selected_index == 0;
let mut label_style = Style::default().fg(crate::colors::text());
if is_selected {
label_style = label_style
.bg(crate::colors::selection())
.add_modifier(Modifier::BOLD);
}
let mut arrow_style = Style::default().fg(crate::colors::text_dim());
if is_selected {
arrow_style = label_style.clone();
}
let mut desc_style = Style::default().fg(crate::colors::text_dim());
if is_selected {
desc_style = desc_style
.bg(crate::colors::selection())
.add_modifier(Modifier::BOLD);
}
let mut status = String::new();
if self.use_chat_model {
status.push_str("(current)");
}
let arrow = if is_selected { "› " } else { " " };
let mut spans = vec![
Span::styled(arrow, arrow_style),
Span::styled("Follow Chat Model", label_style),
Span::raw(" "),
Span::styled(
"Uses the same model and reasoning as active chat",
desc_style,
),
];
if !status.is_empty() {
spans.push(Span::raw(format!(" {}", status)));
}
lines.push(Line::from(spans));
lines.push(Line::from(""));
}
let mut previous_model: Option<&str> = None;
for preset_index in self.sorted_indices() {
let flat_preset = &self.flat_presets[preset_index];
let entries = self.entries();
for (entry_idx, entry) in entries.iter().enumerate() {
let EntryKind::Preset(preset_index) = entry else { continue };
let flat_preset = &self.flat_presets[*preset_index];
if previous_model
.map(|m| !m.eq_ignore_ascii_case(&flat_preset.model))
.unwrap_or(true)
@@ -422,8 +545,9 @@ impl ModelSelectionView {
previous_model = Some(&flat_preset.model);
}
let is_selected = preset_index == self.selected_index;
let is_current = flat_preset.model.eq_ignore_ascii_case(&self.current_model)
let is_selected = entry_idx == self.selected_index;
let is_current = !self.use_chat_model
&& flat_preset.model.eq_ignore_ascii_case(&self.current_model)
&& flat_preset.effort == self.current_effort;
let label = Self::effort_label(flat_preset.effort);
let mut row_text = label.to_string();
@@ -0,0 +1,264 @@
use code_core::config_types::ReasoningEffort;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget};
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::colors;
use super::bottom_pane_view::BottomPaneView;
use super::scroll_state::ScrollState;
use super::BottomPane;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum PlanningRow {
FollowChat,
CustomModel,
}
pub(crate) struct PlanningSettingsView {
use_chat_model: bool,
planning_model: String,
planning_reasoning: ReasoningEffort,
app_event_tx: AppEventSender,
state: ScrollState,
is_complete: bool,
}
impl PlanningSettingsView {
pub fn new(
use_chat_model: bool,
planning_model: String,
planning_reasoning: ReasoningEffort,
app_event_tx: AppEventSender,
) -> Self {
let mut state = ScrollState::new();
state.selected_idx = Some(0);
Self {
use_chat_model,
planning_model,
planning_reasoning,
app_event_tx,
state,
is_complete: false,
}
}
pub fn set_planning_model(&mut self, model: String, effort: ReasoningEffort) {
self.planning_model = model;
self.planning_reasoning = effort;
}
pub fn set_use_chat_model(&mut self, use_chat: bool) {
self.use_chat_model = use_chat;
}
pub fn handle_key_event_direct(&mut self, key_event: KeyEvent) {
self.handle_key(key_event);
}
fn rows(&self) -> Vec<PlanningRow> {
vec![PlanningRow::FollowChat, PlanningRow::CustomModel]
}
fn handle_enter(&mut self, row: PlanningRow) {
match row {
PlanningRow::FollowChat => {
self.use_chat_model = true;
let _ = self
.app_event_tx
.send(AppEvent::UpdatePlanningUseChatModel(true));
}
PlanningRow::CustomModel => {
if self.use_chat_model {
self.use_chat_model = false;
let _ = self
.app_event_tx
.send(AppEvent::UpdatePlanningUseChatModel(false));
}
let _ = self.app_event_tx.send(AppEvent::ShowPlanningModelSelector);
}
}
}
fn render_row(&self, row: PlanningRow, selected: bool) -> Line<'static> {
let arrow = if selected { "› " } else { " " };
let arrow_style = if selected {
Style::default().fg(colors::primary())
} else {
Style::default().fg(colors::text_dim())
};
match row {
PlanningRow::FollowChat => {
let label_style = if selected {
Style::default()
.fg(colors::primary())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(colors::text()).add_modifier(Modifier::BOLD)
};
let status = if self.use_chat_model {
Span::styled("Active", Style::default().fg(colors::success()))
} else {
Span::styled("Select to follow chat model", Style::default().fg(colors::text_dim()))
};
let mut spans = vec![
Span::styled(arrow, arrow_style),
Span::styled("Follow Chat Model", label_style),
Span::raw(" "),
status,
];
spans.push(Span::raw(" "));
spans.push(Span::styled(
"Uses the same model and reasoning as active chat",
Style::default().fg(colors::text_dim()),
));
Line::from(spans)
}
PlanningRow::CustomModel => {
let label_style = if selected {
Style::default()
.fg(colors::primary())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(colors::text()).add_modifier(Modifier::BOLD)
};
let value_style = if selected {
Style::default()
.fg(colors::function())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(colors::text())
};
let (value_text, hint_text) = if self.use_chat_model {
(
"Follow Chat model".to_string(),
Some("Select to choose a custom planning model".to_string()),
)
} else {
(
format!(
"{} ({})",
self.planning_model,
Self::reasoning_label(self.planning_reasoning)
),
Some("Enter to change".to_string()),
)
};
let mut spans = vec![
Span::styled(arrow, arrow_style),
Span::styled("Custom planning model", label_style),
Span::raw(" "),
Span::styled(value_text, value_style),
];
if selected {
if let Some(hint) = hint_text {
spans.push(Span::raw(" "));
spans.push(Span::styled(hint, Style::default().fg(colors::text_dim())));
}
}
Line::from(spans)
}
}
}
fn reasoning_label(effort: ReasoningEffort) -> &'static str {
match effort {
ReasoningEffort::XHigh => "XHigh",
ReasoningEffort::High => "High",
ReasoningEffort::Medium => "Medium",
ReasoningEffort::Low => "Low",
ReasoningEffort::Minimal => "Minimal",
ReasoningEffort::None => "None",
}
}
fn handle_key(&mut self, key: KeyEvent) {
let rows = self.rows();
if self.state.selected_idx.is_none() {
self.state.selected_idx = Some(0);
}
let total = rows.len();
self.state.ensure_visible(total, 4);
match key.code {
KeyCode::Up => self.state.move_up_wrap(total),
KeyCode::Down => self.state.move_down_wrap(total),
KeyCode::Char(' ') | KeyCode::Enter => {
if let Some(sel) = self.state.selected_idx {
if let Some(row) = rows.get(sel).copied() {
self.handle_enter(row);
}
}
}
KeyCode::Esc => {
self.is_complete = true;
}
_ => {}
}
}
}
impl<'a> BottomPaneView<'a> for PlanningSettingsView {
fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, key_event: KeyEvent) {
if !matches!(key_event.modifiers, KeyModifiers::NONE) {
return;
}
self.handle_key(key_event);
}
fn is_complete(&self) -> bool {
self.is_complete
}
fn desired_height(&self, _width: u16) -> u16 {
6
}
fn render(&self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
Clear.render(area, buf);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(colors::border()))
.style(Style::default().bg(colors::background()).fg(colors::text()))
.title(" Planning Settings ")
.title_alignment(Alignment::Center);
let inner = block.inner(area);
block.render(area, buf);
let header_lines = vec![
Line::from(Span::styled(
"Select the model used when you’re in Plan Mode (Read Only).",
Style::default().fg(colors::text_dim()),
)),
Line::from(Span::styled(
"Use ↑↓ to navigate · Enter/Space to toggle/open · Esc close",
Style::default().fg(colors::text_dim()),
)),
Line::from(""),
];
let rows = self.rows();
let selected_idx = self.state.selected_idx.unwrap_or(0).min(rows.len().saturating_sub(1));
let mut lines: Vec<Line> = Vec::new();
lines.extend(header_lines.into_iter());
for (idx, row) in rows.iter().enumerate() {
let selected = idx == selected_idx;
lines.push(self.render_row(*row, selected));
}
Paragraph::new(lines)
.alignment(Alignment::Left)
.style(Style::default().bg(colors::background()).fg(colors::text()))
.render(inner, buf);
}
}
@@ -25,12 +25,14 @@ enum SelectionKind {
}
enum RowData {
Model,
FollowChat,
CustomModel,
Toggle,
Attempts,
}
pub(crate) struct ReviewSettingsView {
use_chat_model: bool,
review_model: String,
review_reasoning: ReasoningEffort,
auto_resolve_enabled: bool,
@@ -49,7 +51,12 @@ impl ReviewSettingsView {
self.review_reasoning = effort;
}
pub fn set_use_chat_model(&mut self, use_chat: bool) {
self.use_chat_model = use_chat;
}
pub fn new(
use_chat_model: bool,
review_model: String,
review_reasoning: ReasoningEffort,
auto_resolve_enabled: bool,
@@ -67,6 +74,7 @@ impl ReviewSettingsView {
.position(|&value| value == auto_resolve_attempts)
.unwrap_or(default_index);
Self {
use_chat_model,
review_model,
review_reasoning,
auto_resolve_enabled,
@@ -118,9 +126,10 @@ impl ReviewSettingsView {
}
fn build_rows(&self) -> (Vec<RowData>, Vec<usize>, Vec<SelectionKind>) {
let rows = vec![RowData::Model, RowData::Toggle, RowData::Attempts];
let selection_rows = vec![0, 1, 2];
let rows = vec![RowData::FollowChat, RowData::CustomModel, RowData::Toggle, RowData::Attempts];
let selection_rows = vec![0, 1, 2, 3];
let selection_kinds = vec![
SelectionKind::Model,
SelectionKind::Model,
SelectionKind::Toggle,
SelectionKind::Attempts,
@@ -156,7 +165,33 @@ impl ReviewSettingsView {
Style::default().fg(colors::text_dim())
};
match row {
RowData::Model => {
RowData::FollowChat => {
let label_style = if selected {
Style::default()
.fg(colors::primary())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(colors::text()).add_modifier(Modifier::BOLD)
};
let status = if self.use_chat_model {
Span::styled("Active", Style::default().fg(colors::success()))
} else {
Span::styled("Select to follow chat model", Style::default().fg(colors::text_dim()))
};
let mut spans = vec![
Span::styled(arrow, arrow_style),
Span::styled("Follow Chat Model", label_style),
Span::raw(" "),
status,
];
spans.push(Span::raw(" "));
spans.push(Span::styled(
"Uses the same model and reasoning as active chat",
Style::default().fg(colors::text_dim()),
));
Line::from(spans)
}
RowData::CustomModel => {
let label_style = if selected {
Style::default()
.fg(colors::primary())
@@ -171,25 +206,32 @@ impl ReviewSettingsView {
} else {
Style::default().fg(colors::text())
};
let model_label = self.review_model.trim();
let model_display = if model_label.is_empty() {
"(not set)"
let (value_text, hint_text) = if self.use_chat_model {
(
"Follow Chat model".to_string(),
Some("Select to choose a custom review model".to_string()),
)
} else {
model_label
(
format!(
"{} ({})",
self.review_model,
Self::reasoning_label(self.review_reasoning)
),
Some("Enter to change".to_string()),
)
};
let value_text = format!(
"{} · {} reasoning",
model_display,
Self::reasoning_label(self.review_reasoning)
);
let mut spans = vec![
Span::styled(arrow, arrow_style),
Span::styled("Review model", label_style),
Span::styled("Custom review model", label_style),
Span::raw(" "),
Span::styled(value_text, value_style),
];
if selected {
spans.push(Span::raw(" (Enter to change)"));
if let Some(hint) = hint_text {
spans.push(Span::raw(" "));
spans.push(Span::styled(hint, Style::default().fg(colors::text_dim())));
}
}
Line::from(spans)
}
@@ -317,7 +359,30 @@ impl ReviewSettingsView {
match kind {
SelectionKind::Toggle => self.toggle_auto_resolve(),
SelectionKind::Attempts => self.adjust_auto_resolve_attempts(true),
SelectionKind::Model => self.open_review_model_selector(),
SelectionKind::Model => {
if let Some(sel) = self.state.selected_idx {
if let Some(row) = self.build_rows().0.get(sel) {
match row {
RowData::FollowChat => {
if !self.use_chat_model {
self.use_chat_model = true;
self.app_event_tx
.send(AppEvent::UpdateReviewUseChatModel(true));
}
}
RowData::CustomModel => {
if self.use_chat_model {
self.use_chat_model = false;
self.app_event_tx
.send(AppEvent::UpdateReviewUseChatModel(false));
}
self.open_review_model_selector();
}
_ => {}
}
}
}
}
}
}
}
@@ -2,6 +2,7 @@
pub(crate) enum SettingsSection {
Model,
Theme,
Planning,
Updates,
Agents,
AutoDrive,
@@ -15,9 +16,10 @@ pub(crate) enum SettingsSection {
}
impl SettingsSection {
pub(crate) const ALL: [SettingsSection; 12] = [
pub(crate) const ALL: [SettingsSection; 13] = [
SettingsSection::Model,
SettingsSection::Theme,
SettingsSection::Planning,
SettingsSection::Updates,
SettingsSection::Agents,
SettingsSection::AutoDrive,
@@ -34,6 +36,7 @@ impl SettingsSection {
match self {
SettingsSection::Model => "Model",
SettingsSection::Theme => "Theme",
SettingsSection::Planning => "Planning",
SettingsSection::Updates => "Updates",
SettingsSection::Agents => "Agents",
SettingsSection::AutoDrive => "Auto Drive",
@@ -51,6 +54,7 @@ impl SettingsSection {
match self {
SettingsSection::Model => "Choose the language model used for new completions.",
SettingsSection::Theme => "Switch between preset color palettes and adjust contrast.",
SettingsSection::Planning => "Choose the model used in Plan Mode (Read Only).",
SettingsSection::Updates => "Control CLI auto-update cadence and release channels.",
SettingsSection::Agents => "Configure linked agents and default task permissions.",
SettingsSection::AutoDrive => "Manage Auto Drive defaults for review and cadence.",
@@ -68,6 +72,7 @@ impl SettingsSection {
match self {
SettingsSection::Model => "Model settings coming soon.",
SettingsSection::Theme => "Theme settings coming soon.",
SettingsSection::Planning => "Planning settings coming soon.",
SettingsSection::Updates => "Upgrade Codex and manage automatic updates.",
SettingsSection::Agents => "Agents configuration coming soon.",
SettingsSection::AutoDrive => "Auto Drive controls coming soon.",
@@ -85,6 +90,7 @@ impl SettingsSection {
match value.trim().to_ascii_lowercase().as_str() {
"model" | "models" => Some(SettingsSection::Model),
"theme" | "themes" => Some(SettingsSection::Theme),
"planning" | "plan" => Some(SettingsSection::Planning),
"update" | "updates" => Some(SettingsSection::Updates),
"agent" | "agents" => Some(SettingsSection::Agents),
"auto" | "autodrive" | "drive" => Some(SettingsSection::AutoDrive),
+363 -20
View File
@@ -179,6 +179,7 @@ use crate::bottom_pane::{
ThemeSelectionView,
agent_editor_view::AgentEditorView,
AutoDriveSettingsView,
PlanningSettingsView,
UpdateSettingsView,
ReviewSettingsView,
ValidationSettingsView,
@@ -1235,6 +1236,7 @@ pub(crate) struct ChatWidget<'a> {
context_last_sequence: Option<u64>,
context_browser_sequence: Option<u64>,
config: Config,
planning_restore: Option<(String, ReasoningEffort)>,
history_debug_events: Option<RefCell<Vec<String>>>,
latest_upgrade_version: Option<String>,
initial_user_message: Option<UserMessage>,
@@ -1999,6 +2001,7 @@ use self::settings_overlay::{
ChromeSettingsContent,
McpSettingsContent,
ModelSettingsContent,
PlanningSettingsContent,
NotificationsSettingsContent,
ReviewSettingsContent,
ThemeSettingsContent,
@@ -4996,6 +4999,7 @@ impl ChatWidget<'_> {
active_exec_cell: None,
history_cells,
config: config.clone(),
planning_restore: None,
history_debug_events: if history_cell_logging_enabled() {
Some(RefCell::new(Vec::new()))
} else {
@@ -5310,6 +5314,7 @@ impl ChatWidget<'_> {
active_exec_cell: None,
history_cells,
config: config.clone(),
planning_restore: None,
history_debug_events: if history_cell_logging_enabled() {
Some(RefCell::new(Vec::new()))
} else {
@@ -13886,6 +13891,7 @@ impl ChatWidget<'_> {
let auto_resolve_enabled = self.config.tui.review_auto_resolve;
let attempts = self.configured_auto_resolve_re_reviews();
let view = ReviewSettingsView::new(
self.config.review_use_chat_model,
self.config.review_model.clone(),
self.config.review_model_reasoning_effort,
auto_resolve_enabled,
@@ -13895,6 +13901,16 @@ impl ChatWidget<'_> {
ReviewSettingsContent::new(view)
}
fn build_planning_settings_content(&mut self) -> PlanningSettingsContent {
let view = PlanningSettingsView::new(
self.config.planning_use_chat_model,
self.config.planning_model.clone(),
self.config.planning_model_reasoning_effort,
self.app_event_tx.clone(),
);
PlanningSettingsContent::new(view)
}
fn build_github_settings_content(&mut self) -> GithubSettingsContent {
let enabled = self.config.github.check_workflows_on_push;
let token_info = gh_actions::get_github_token().map(|(_, src)| src);
@@ -15222,8 +15238,11 @@ fi\n\
self.config.auto_drive.continue_mode = auto_continue_to_config(continue_mode);
if let Ok(home) = code_core::config::find_code_home() {
if let Err(err) =
code_core::config::set_auto_drive_settings(&home, &self.config.auto_drive)
if let Err(err) = code_core::config::set_auto_drive_settings(
&home,
&self.config.auto_drive,
self.config.auto_drive_use_chat_model,
)
{
tracing::warn!("Failed to persist Auto Drive settings: {err}");
}
@@ -18746,6 +18765,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
self.config.model.clone(),
self.config.model_reasoning_effort,
false,
ModelSelectionTarget::Session,
);
}
@@ -18766,10 +18786,39 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
self.config.review_model.clone(),
self.config.review_model_reasoning_effort,
self.config.review_use_chat_model,
ModelSelectionTarget::Review,
);
}
pub(crate) fn show_planning_model_selector(&mut self) {
let presets = self.available_model_presets();
if presets.is_empty() {
self.bottom_pane.flash_footer_notice(
"No model presets are available for planning. Update configuration to define models."
.to_string(),
);
return;
}
if self.settings.overlay.is_some() {
self.close_settings_overlay();
}
let current = if self.config.planning_use_chat_model {
self.config.model.clone()
} else {
self.config.planning_model.clone()
};
let effort = self.config.planning_model_reasoning_effort;
self.bottom_pane
.show_model_selection(
presets,
current,
effort,
self.config.planning_use_chat_model,
ModelSelectionTarget::Planning,
);
}
pub(crate) fn show_auto_drive_model_selector(&mut self) {
let presets = self.available_model_presets();
if presets.is_empty() {
@@ -18786,6 +18835,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
self.config.auto_drive.model.clone(),
self.config.auto_drive.model_reasoning_effort,
self.config.auto_drive_use_chat_model,
ModelSelectionTarget::AutoDrive,
);
}
@@ -18857,6 +18907,8 @@ Have we met every part of this goal and is there no further work to do?"#
return;
}
self.config.review_use_chat_model = false;
let mut updated = false;
if !self.config.review_model.eq_ignore_ascii_case(trimmed) {
self.config.review_model = trimmed.to_string();
@@ -18879,6 +18931,7 @@ Have we met every part of this goal and is there no further work to do?"#
&home,
&self.config.review_model,
self.config.review_model_reasoning_effort,
self.config.review_use_chat_model,
) {
Ok(_) => format!(
"Review model set to {} ({} reasoning)",
@@ -18907,6 +18960,232 @@ Have we met every part of this goal and is there no further work to do?"#
self.request_redraw();
}
pub(crate) fn set_review_use_chat_model(&mut self, use_chat: bool) {
if self.config.review_use_chat_model == use_chat {
return;
}
self.config.review_use_chat_model = use_chat;
if use_chat {
self.config.review_model = self.config.model.clone();
self.config.review_model_reasoning_effort = self.config.model_reasoning_effort;
}
if let Ok(home) = code_core::config::find_code_home() {
if let Err(err) = code_core::config::set_review_model(
&home,
&self.config.review_model,
self.config.review_model_reasoning_effort,
use_chat,
) {
tracing::warn!("Failed to persist review use-chat toggle: {err}");
}
}
let notice = if use_chat {
"Review model now follows Chat model".to_string()
} else {
format!(
"Review model set to {} ({} reasoning)",
self.config.review_model,
Self::format_reasoning_effort(self.config.review_model_reasoning_effort)
)
};
self.bottom_pane.flash_footer_notice(notice);
self.update_review_settings_model_row();
self.refresh_settings_overview_rows();
self.request_redraw();
}
pub(crate) fn set_auto_drive_use_chat_model(&mut self, use_chat: bool) {
if self.config.auto_drive_use_chat_model == use_chat {
return;
}
self.config.auto_drive_use_chat_model = use_chat;
if use_chat {
self.config.auto_drive.model = self.config.model.clone();
self.config.auto_drive.model_reasoning_effort = self.config.model_reasoning_effort;
}
if let Ok(home) = code_core::config::find_code_home() {
if let Err(err) = code_core::config::set_auto_drive_settings(
&home,
&self.config.auto_drive,
use_chat,
) {
tracing::warn!("Failed to persist Auto Drive use-chat toggle: {err}");
}
}
let notice = if use_chat {
"Auto Drive model now follows Chat model".to_string()
} else {
format!(
"Auto Drive model set to {} ({} reasoning)",
self.config.auto_drive.model,
Self::format_reasoning_effort(self.config.auto_drive.model_reasoning_effort)
)
};
self.bottom_pane.flash_footer_notice(notice);
self.refresh_settings_overview_rows();
self.update_auto_drive_settings_model_row();
self.request_redraw();
}
pub(crate) fn apply_planning_model_selection(
&mut self,
model: String,
effort: ReasoningEffort,
) {
let trimmed = model.trim();
if trimmed.is_empty() {
return;
}
self.config.planning_use_chat_model = false;
let mut updated = false;
if !self.config.planning_model.eq_ignore_ascii_case(trimmed) {
self.config.planning_model = trimmed.to_string();
updated = true;
}
if self.config.planning_model_reasoning_effort != effort {
self.config.planning_model_reasoning_effort = effort;
updated = true;
}
if !updated {
self.bottom_pane
.flash_footer_notice("Planning model unchanged.".to_string());
return;
}
if let Ok(home) = code_core::config::find_code_home() {
if let Err(err) = code_core::config::set_planning_model(
&home,
&self.config.planning_model,
self.config.planning_model_reasoning_effort,
false,
) {
tracing::warn!("Failed to persist planning model: {err}");
}
}
self.bottom_pane.flash_footer_notice(format!(
"Planning model set to {} ({} reasoning)",
self.config.planning_model,
Self::format_reasoning_effort(self.config.planning_model_reasoning_effort)
));
self.refresh_settings_overview_rows();
self.update_planning_settings_model_row();
// If we're currently in plan mode, switch the session model immediately.
if matches!(self.config.sandbox_policy, code_core::protocol::SandboxPolicy::ReadOnly) {
self.apply_planning_session_model();
}
self.request_redraw();
}
fn apply_planning_session_model(&mut self) {
if self.config.planning_use_chat_model {
self.restore_planning_session_model();
return;
}
// If we're already on the planning model, do nothing.
if self.config.model.eq_ignore_ascii_case(&self.config.planning_model)
&& self.config.model_reasoning_effort == self.config.planning_model_reasoning_effort
{
return;
}
// Save current chat model to restore later.
self.planning_restore = Some((
self.config.model.clone(),
self.config.model_reasoning_effort,
));
self.config.model = self.config.planning_model.clone();
self.config.model_reasoning_effort = self.config.planning_model_reasoning_effort;
let op = Op::ConfigureSession {
provider: self.config.model_provider.clone(),
model: self.config.model.clone(),
model_reasoning_effort: self.config.model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
sandbox_policy: self.config.sandbox_policy.clone(),
disable_response_storage: self.config.disable_response_storage,
notify: self.config.notify.clone(),
cwd: self.config.cwd.clone(),
resume_path: None,
};
self.submit_op(op);
}
fn restore_planning_session_model(&mut self) {
if let Some((model, effort)) = self.planning_restore.take() {
self.config.model = model;
self.config.model_reasoning_effort = effort;
let op = Op::ConfigureSession {
provider: self.config.model_provider.clone(),
model: self.config.model.clone(),
model_reasoning_effort: self.config.model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
sandbox_policy: self.config.sandbox_policy.clone(),
disable_response_storage: self.config.disable_response_storage,
notify: self.config.notify.clone(),
cwd: self.config.cwd.clone(),
resume_path: None,
};
self.submit_op(op);
}
}
pub(crate) fn set_planning_use_chat_model(&mut self, use_chat: bool) {
if self.config.planning_use_chat_model == use_chat {
return;
}
self.config.planning_use_chat_model = use_chat;
if let Ok(home) = code_core::config::find_code_home() {
if let Err(err) = code_core::config::set_planning_model(
&home,
&self.config.planning_model,
self.config.planning_model_reasoning_effort,
use_chat,
) {
tracing::warn!("Failed to persist planning use-chat toggle: {err}");
}
}
if use_chat {
self.bottom_pane
.flash_footer_notice("Planning model now follows Chat model".to_string());
} else {
self.bottom_pane.flash_footer_notice(format!(
"Planning model set to {} ({} reasoning)",
self.config.planning_model,
Self::format_reasoning_effort(self.config.planning_model_reasoning_effort)
));
}
self.update_planning_settings_model_row();
self.refresh_settings_overview_rows();
if matches!(self.config.sandbox_policy, code_core::protocol::SandboxPolicy::ReadOnly) {
self.apply_planning_session_model();
}
self.request_redraw();
}
pub(crate) fn apply_auto_drive_model_selection(
&mut self,
model: String,
@@ -18917,6 +19196,8 @@ Have we met every part of this goal and is there no further work to do?"#
return;
}
self.config.auto_drive_use_chat_model = false;
let mut updated = false;
if !self.config.auto_drive.model.eq_ignore_ascii_case(trimmed) {
self.config.auto_drive.model = trimmed.to_string();
@@ -18935,7 +19216,11 @@ Have we met every part of this goal and is there no further work to do?"#
}
let message = if let Ok(home) = code_core::config::find_code_home() {
match code_core::config::set_auto_drive_settings(&home, &self.config.auto_drive) {
match code_core::config::set_auto_drive_settings(
&home,
&self.config.auto_drive,
self.config.auto_drive_use_chat_model,
) {
Ok(_) => format!(
"Auto Drive model set to {} ({} reasoning)",
self.config.auto_drive.model,
@@ -19002,6 +19287,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
self.config.model.clone(),
self.config.model_reasoning_effort,
false,
ModelSelectionTarget::Session,
);
return;
@@ -19309,6 +19595,7 @@ Have we met every part of this goal and is there no further work to do?"#
let mut overlay = SettingsOverlayView::new(initial_section);
overlay.set_model_content(self.build_model_settings_content());
overlay.set_planning_content(self.build_planning_settings_content());
overlay.set_theme_content(self.build_theme_settings_content());
if let Some(update_content) = self.build_updates_settings_content() {
overlay.set_updates_content(update_content);
@@ -19360,6 +19647,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
current_model,
current_effort,
false,
ModelSelectionTarget::Session,
self.app_event_tx.clone(),
);
@@ -19666,6 +19954,7 @@ Have we met every part of this goal and is there no further work to do?"#
let summary = match section {
SettingsSection::Model => self.settings_summary_model(),
SettingsSection::Theme => self.settings_summary_theme(),
SettingsSection::Planning => self.settings_summary_planning(),
SettingsSection::Updates => self.settings_summary_updates(),
SettingsSection::Agents => self.settings_summary_agents(),
SettingsSection::AutoDrive => self.settings_summary_auto_drive(),
@@ -19705,6 +19994,22 @@ Have we met every part of this goal and is there no further work to do?"#
Some(parts.join(" · "))
}
fn settings_summary_planning(&self) -> Option<String> {
if self.config.planning_use_chat_model {
return Some("Model: Follow Chat model".to_string());
}
let model = self.config.planning_model.trim();
let model_display_storage;
let model_display = if model.is_empty() {
"(default)"
} else {
model_display_storage = Self::format_model_label(model);
&model_display_storage
};
let effort = Self::format_reasoning_effort(self.config.planning_model_reasoning_effort);
Some(format!("Model: {} ({})", model_display, effort))
}
fn settings_summary_theme(&self) -> Option<String> {
let theme_label = Self::theme_display_name(self.config.tui.theme.name);
let spinner_name = &self.config.tui.spinner.name;
@@ -19752,16 +20057,25 @@ Have we met every part of this goal and is there no further work to do?"#
fn settings_summary_auto_drive(&self) -> Option<String> {
let diagnostics_enabled = self.auto_state.qa_automation_enabled
&& (self.auto_state.review_enabled || self.auto_state.cross_check_enabled);
let model_label = if self.config.auto_drive.model.trim().is_empty() {
"(default)".to_string()
let (model_text, effort_text) = if self.config.auto_drive_use_chat_model {
("Follow Chat model".to_string(), None)
} else {
Self::format_model_label(&self.config.auto_drive.model)
let model_label = if self.config.auto_drive.model.trim().is_empty() {
"(default)".to_string()
} else {
Self::format_model_label(&self.config.auto_drive.model)
};
let effort = Self::format_reasoning_effort(self.config.auto_drive.model_reasoning_effort);
(model_label, Some(effort))
};
let model_segment = if let Some(effort) = effort_text {
format!("Model: {} ({})", model_text, effort)
} else {
format!("Model: {}", model_text)
};
let effort = Self::format_reasoning_effort(self.config.auto_drive.model_reasoning_effort);
Some(format!(
"Model: {} ({}) · Agents: {} · Diagnostics: {} · Continue: {}",
model_label,
effort,
"{} · Agents: {} · Diagnostics: {} · Continue: {}",
model_segment,
Self::on_off_label(self.auto_state.subagents_enabled),
Self::on_off_label(diagnostics_enabled),
self.auto_state.continue_mode.label()
@@ -19788,17 +20102,21 @@ Have we met every part of this goal and is there no further work to do?"#
} else {
format!("Auto Resolve: On (max {} re-reviews)", attempts)
};
let model = self.config.review_model.trim();
let model_display = if model.is_empty() {
"(not set)".to_string()
let model_part = if self.config.review_use_chat_model {
"Model: Follow Chat model".to_string()
} else {
Self::format_model_label(model)
let model = self.config.review_model.trim();
let model_display = if model.is_empty() {
"(not set)".to_string()
} else {
Self::format_model_label(model)
};
format!(
"Model: {} ({})",
model_display,
Self::format_reasoning_effort(self.config.review_model_reasoning_effort)
)
};
let model_part = format!(
"Model: {} ({})",
model_display,
Self::format_reasoning_effort(self.config.review_model_reasoning_effort)
);
Some(format!("{} · {}", model_part, auto_label))
}
@@ -19976,6 +20294,7 @@ Have we met every part of this goal and is there no further work to do?"#
let handled = match section {
SettingsSection::Model
| SettingsSection::Theme
| SettingsSection::Planning
| SettingsSection::Updates
| SettingsSection::Review
| SettingsSection::Validation
@@ -20177,6 +20496,13 @@ Have we met every part of this goal and is there no further work to do?"#
),
};
// Apply planning model when entering plan mode; restore when leaving it.
if next == 0 {
self.apply_planning_session_model();
} else if idx == 0 {
self.restore_planning_session_model();
}
// Update local config
self.config.approval_policy = approval;
self.config.sandbox_policy = sandbox;
@@ -27938,7 +28264,11 @@ impl ChatWidget<'_> {
}
let message = if let Ok(home) = code_core::config::find_code_home() {
match code_core::config::set_auto_drive_settings(&home, &self.config.auto_drive) {
match code_core::config::set_auto_drive_settings(
&home,
&self.config.auto_drive,
self.config.auto_drive_use_chat_model,
) {
Ok(_) => {
tracing::info!(
"Persisted auto resolve attempt limit: {}",
@@ -27971,6 +28301,19 @@ impl ChatWidget<'_> {
self.config.review_model.clone(),
self.config.review_model_reasoning_effort,
);
content.set_use_chat_model(self.config.review_use_chat_model);
}
}
}
fn update_planning_settings_model_row(&mut self) {
if let Some(overlay) = self.settings.overlay.as_mut() {
if let Some(content) = overlay.planning_content_mut() {
content.update_planning_model(
self.config.planning_model.clone(),
self.config.planning_model_reasoning_effort,
);
content.set_use_chat_model(self.config.planning_use_chat_model);
}
}
}
@@ -19,6 +19,7 @@ use crate::bottom_pane::{
McpSettingsView,
ModelSelectionView,
NotificationsSettingsView,
PlanningSettingsView,
SettingsSection,
ThemeSelectionView,
UpdateSettingsView,
@@ -187,12 +188,45 @@ pub(crate) struct ThemeSettingsContent {
view: ThemeSelectionView,
}
pub(crate) struct PlanningSettingsContent {
view: PlanningSettingsView,
}
impl ThemeSettingsContent {
pub(crate) fn new(view: ThemeSelectionView) -> Self {
Self { view }
}
}
impl PlanningSettingsContent {
pub(crate) fn new(view: PlanningSettingsView) -> Self {
Self { view }
}
pub(crate) fn update_planning_model(&mut self, model: String, effort: ReasoningEffort) {
self.view.set_planning_model(model, effort);
}
pub(crate) fn set_use_chat_model(&mut self, use_chat: bool) {
self.view.set_use_chat_model(use_chat);
}
}
impl SettingsContent for PlanningSettingsContent {
fn render(&self, area: Rect, buf: &mut Buffer) {
self.view.render(area, buf);
}
fn handle_key(&mut self, key: KeyEvent) -> bool {
self.view.handle_key_event_direct(key);
true
}
fn is_complete(&self) -> bool {
self.view.is_complete()
}
}
impl SettingsContent for ThemeSettingsContent {
fn render(&self, area: Rect, buf: &mut Buffer) {
self.view.render(area, buf);
@@ -278,6 +312,10 @@ impl ReviewSettingsContent {
pub(crate) fn update_review_model(&mut self, model: String, effort: ReasoningEffort) {
self.view.set_review_model(model, effort);
}
pub(crate) fn set_use_chat_model(&mut self, use_chat: bool) {
self.view.set_use_chat_model(use_chat);
}
}
impl SettingsContent for ReviewSettingsContent {
@@ -1168,6 +1206,7 @@ pub(crate) struct SettingsOverlayView {
last_section: SettingsSection,
help: Option<SettingsHelpOverlay>,
model_content: Option<ModelSettingsContent>,
planning_content: Option<PlanningSettingsContent>,
theme_content: Option<ThemeSettingsContent>,
updates_content: Option<UpdatesSettingsContent>,
notifications_content: Option<NotificationsSettingsContent>,
@@ -1190,6 +1229,7 @@ impl SettingsOverlayView {
last_section: section,
help: None,
model_content: None,
planning_content: None,
theme_content: None,
updates_content: None,
notifications_content: None,
@@ -1261,6 +1301,10 @@ impl SettingsOverlayView {
self.model_content = Some(content);
}
pub(crate) fn set_planning_content(&mut self, content: PlanningSettingsContent) {
self.planning_content = Some(content);
}
pub(crate) fn set_theme_content(&mut self, content: ThemeSettingsContent) {
self.theme_content = Some(content);
}
@@ -1318,6 +1362,10 @@ impl SettingsOverlayView {
self.review_content.as_mut()
}
pub(crate) fn planning_content_mut(&mut self) -> Option<&mut PlanningSettingsContent> {
self.planning_content.as_mut()
}
pub(crate) fn auto_drive_content_mut(&mut self) -> Option<&mut AutoDriveSettingsContent> {
self.auto_drive_content.as_mut()
}
@@ -1764,6 +1812,7 @@ impl SettingsOverlayView {
match section {
SettingsSection::Model => "Select Model & Reasoning",
SettingsSection::Theme => "Theme Settings",
SettingsSection::Planning => "Planning Settings",
SettingsSection::Updates => "Upgrade",
SettingsSection::Agents => "Agents",
SettingsSection::AutoDrive => "Auto Drive Settings",
@@ -2056,6 +2105,13 @@ impl SettingsOverlayView {
}
self.render_placeholder(area, buf, SettingsSection::Model.placeholder());
}
SettingsSection::Planning => {
if let Some(content) = self.planning_content.as_ref() {
content.render(area, buf);
return;
}
self.render_placeholder(area, buf, SettingsSection::Planning.placeholder());
}
SettingsSection::Theme => {
if let Some(content) = self.theme_content.as_ref() {
content.render(area, buf);
@@ -2153,6 +2209,10 @@ impl SettingsOverlayView {
.model_content
.as_mut()
.map(|content| content as &mut dyn SettingsContent),
SettingsSection::Planning => self
.planning_content
.as_mut()
.map(|content| content as &mut dyn SettingsContent),
SettingsSection::Theme => self
.theme_content
.as_mut()