feat(auto-drive): allow model selection and clamp verbosity

This commit is contained in:
James Peter
2025-11-21 03:03:28 +10:00
parent 8a97572fe5
commit 61209e062d
11 changed files with 393 additions and 32 deletions
@@ -57,6 +57,20 @@ pub const MODEL_SLUG: &str = "gpt-5.1";
const USER_TURN_SCHEMA_NAME: &str = "auto_coordinator_user_turn";
const COORDINATOR_PROMPT: &str = include_str!("../../core/prompt_coordinator.md");
const ALL_TEXT_VERBOSITY: &[TextVerbosity] = &[
TextVerbosity::Low,
TextVerbosity::Medium,
TextVerbosity::High,
];
fn supported_text_verbosity_for_model(model: &str) -> &'static [TextVerbosity] {
if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
&[TextVerbosity::Medium]
} else {
ALL_TEXT_VERBOSITY
}
}
#[derive(Clone)]
pub struct AutoCoordinatorEventSender {
inner: Arc<dyn Fn(AutoCoordinatorEvent) + Send + Sync>,
@@ -901,8 +915,19 @@ fn run_auto_loop(
derive_goal_from_history: bool,
) -> Result<()> {
let mut config = config;
config.model_reasoning_effort = ReasoningEffort::High;
config.model_text_verbosity = TextVerbosity::Low;
if config.model.trim().is_empty() {
config.model = MODEL_SLUG.to_string();
}
if matches!(config.model_reasoning_effort, ReasoningEffort::None) {
config.model_reasoning_effort = ReasoningEffort::High;
}
let allowed_verbosity = supported_text_verbosity_for_model(&config.model);
config.model_text_verbosity = allowed_verbosity
.iter()
.find(|v| matches!(v, TextVerbosity::Medium))
.copied()
.or_else(|| allowed_verbosity.first().copied())
.unwrap_or(TextVerbosity::Medium);
let compact_prompt_text =
resolve_compact_prompt_text(config.compact_prompt_override.as_deref());
@@ -1013,7 +1038,7 @@ fn run_auto_loop(
let mut requests_completed: u64 = 0;
let mut consecutive_decision_failures: u32 = 0;
let mut session_metrics = SessionMetrics::default();
let mut active_model_slug = MODEL_SLUG.to_string();
let mut active_model_slug = config.model.clone();
let mut prev_compact_summary: Option<String> = None;
loop {
@@ -1068,6 +1093,7 @@ fn run_auto_loop(
auto_instructions.as_deref(),
&event_tx,
&cancel_token,
&active_model_slug,
) {
Ok(ParsedCoordinatorDecision {
status,
@@ -1249,6 +1275,7 @@ fn run_auto_loop(
auto_instructions.as_deref(),
&event_tx,
&cancel_token,
&active_model_slug,
) {
Ok((user_response, cli_command)) => {
if let Some(response_text) = user_response.clone() {
@@ -1616,6 +1643,7 @@ fn request_coordinator_decision(
auto_instructions: Option<&str>,
event_tx: &AutoCoordinatorEventSender,
cancel_token: &CancellationToken,
preferred_model_slug: &str,
) -> Result<ParsedCoordinatorDecision, DecisionFailure> {
let RequestStreamResult {
output_text,
@@ -1633,6 +1661,7 @@ fn request_coordinator_decision(
auto_instructions,
event_tx,
cancel_token,
preferred_model_slug,
)
.map_err(|err| DecisionFailure::new(err, "coordinator_decision", None))?;
if output_text.trim().is_empty() && response_items.is_empty() {
@@ -1662,6 +1691,7 @@ fn request_decision(
auto_instructions: Option<&str>,
event_tx: &AutoCoordinatorEventSender,
cancel_token: &CancellationToken,
preferred_model_slug: &str,
) -> Result<RequestStreamResult> {
match request_decision_with_model(
runtime,
@@ -1674,14 +1704,20 @@ fn request_decision(
auto_instructions,
event_tx,
cancel_token,
MODEL_SLUG,
preferred_model_slug,
) {
Ok(result) => Ok(result),
Err(err) => {
let fallback_slug = client.default_model_slug().to_string();
if fallback_slug != MODEL_SLUG && should_retry_with_default_model(&err) {
let preferred = preferred_model_slug;
let fallback_candidate = client.default_model_slug().to_string();
let fallback_slug = if fallback_candidate.eq_ignore_ascii_case(preferred) {
MODEL_SLUG.to_string()
} else {
fallback_candidate
};
if fallback_slug != preferred_model_slug && should_retry_with_default_model(&err) {
debug!(
preferred = %MODEL_SLUG,
preferred = %preferred,
fallback = %fallback_slug,
"auto coordinator falling back to configured model after invalid model error"
);
@@ -1741,6 +1777,7 @@ fn request_user_turn_decision(
auto_instructions: Option<&str>,
event_tx: &AutoCoordinatorEventSender,
cancel_token: &CancellationToken,
preferred_model_slug: &str,
) -> Result<(Option<String>, Option<String>), DecisionFailure> {
let result = request_decision(
runtime,
@@ -1753,6 +1790,7 @@ fn request_user_turn_decision(
auto_instructions,
event_tx,
cancel_token,
preferred_model_slug,
)
.map_err(|err| DecisionFailure::new(err, "auto_coordinator_user_turn", None))?;
let (user_response, cli_command) = parse_user_turn_reply(&result.output_text)
+27
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use code_app_server_protocol::AuthMode;
use code_core::config_types::TextVerbosity as TextVerbosityConfig;
use code_core::protocol_config_types::ReasoningEffort;
use once_cell::sync::Lazy;
@@ -31,11 +32,18 @@ pub struct ModelPreset {
pub description: &'static str,
pub default_reasoning_effort: ReasoningEffort,
pub supported_reasoning_efforts: &'static [ReasoningEffortPreset],
pub supported_text_verbosity: &'static [TextVerbosityConfig],
pub is_default: bool,
pub upgrade: Option<ModelUpgrade>,
pub show_in_picker: bool,
}
const ALL_TEXT_VERBOSITY: &[TextVerbosityConfig] = &[
TextVerbosityConfig::Low,
TextVerbosityConfig::Medium,
TextVerbosityConfig::High,
];
static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
vec![
ModelPreset {
@@ -62,6 +70,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Extra high reasoning depth for complex problems",
},
],
supported_text_verbosity: &[TextVerbosityConfig::Medium],
is_default: true,
upgrade: None,
show_in_picker: true,
@@ -86,6 +95,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Maximizes reasoning depth for complex or ambiguous problems",
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max",
@@ -110,6 +120,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Maximizes reasoning depth for complex or ambiguous problems",
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max",
@@ -140,6 +151,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Maximizes reasoning depth for complex or ambiguous problems",
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max",
@@ -169,6 +181,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Maximizes reasoning depth for complex or ambiguous problems",
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max",
@@ -193,6 +206,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Maximizes reasoning depth for complex or ambiguous problems",
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-mini",
@@ -227,6 +241,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Maximizes reasoning depth for complex or ambiguous problems",
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max",
@@ -253,6 +268,18 @@ pub fn all_model_presets() -> &'static Vec<ModelPreset> {
&PRESETS
}
pub fn allowed_text_verbosity_for_model(model: &str) -> &'static [TextVerbosityConfig] {
let model_lower = model.to_ascii_lowercase();
PRESETS
.iter()
.find(|preset| {
preset.model.eq_ignore_ascii_case(&model_lower)
|| preset.id.eq_ignore_ascii_case(&model_lower)
})
.map(|preset| preset.supported_text_verbosity)
.unwrap_or(ALL_TEXT_VERBOSITY)
}
#[cfg(test)]
mod tests {
use super::*;
+49 -6
View File
@@ -188,6 +188,7 @@ impl ModelClient {
session_id: Uuid,
debug_logger: Arc<Mutex<DebugLogger>>,
) -> Self {
let effective_verbosity = clamp_text_verbosity_for_model(config.model.as_str(), verbosity);
let client = create_client(&config.responses_originator_header);
Self {
@@ -199,7 +200,7 @@ impl ModelClient {
session_id,
effort,
summary,
verbosity,
verbosity: effective_verbosity,
debug_logger,
}
}
@@ -434,21 +435,25 @@ impl ModelClient {
})
});
let request_model = prompt
.model_override
.as_deref()
.unwrap_or(self.config.model.as_str());
let effective_verbosity = clamp_text_verbosity_for_model(request_model, self.verbosity);
let verbosity = match &self.config.model_family.family {
family if family == "gpt-5" || family == "gpt-5.1" => {
Some(self.config.model_text_verbosity)
}
family if family == "gpt-5" || family == "gpt-5.1" => Some(effective_verbosity),
_ => None,
};
let text = match (auth_mode, want_format, verbosity) {
(Some(AuthMode::ChatGPT), None, _) => None,
(_, Some(fmt), _) => Some(crate::client_common::Text {
verbosity: self.verbosity.into(),
verbosity: effective_verbosity.into(),
format: Some(fmt),
}),
(_, None, Some(_)) => Some(crate::client_common::Text {
verbosity: self.verbosity.into(),
verbosity: effective_verbosity.into(),
format: None,
}),
(_, None, None) => None,
@@ -981,6 +986,44 @@ impl ModelClient {
}
}
fn clamp_text_verbosity_for_model(
model: &str,
requested: TextVerbosityConfig,
) -> TextVerbosityConfig {
let allowed = supported_text_verbosity_for_model(model);
if allowed.iter().any(|v| v == &requested) {
return requested;
}
if let Some(medium) = allowed.iter().find(|v| matches!(v, TextVerbosityConfig::Medium)) {
tracing::debug!(
model,
requested = ?requested,
fallback = ?medium,
"text verbosity clamped to supported value for model",
);
return *medium;
}
let fallback = *allowed.first().unwrap_or(&TextVerbosityConfig::Medium);
tracing::debug!(
model,
requested = ?requested,
fallback = ?fallback,
"text verbosity clamped to first supported value for model",
);
fallback
}
fn supported_text_verbosity_for_model(model: &str) -> &'static [TextVerbosityConfig] {
if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
return &[TextVerbosityConfig::Medium];
}
const ALL: &[TextVerbosityConfig] = &[TextVerbosityConfig::Low, TextVerbosityConfig::Medium, TextVerbosityConfig::High];
ALL
}
#[derive(Debug, Deserialize, Serialize)]
struct SseEvent {
#[serde(rename = "type")]
+11
View File
@@ -1041,6 +1041,13 @@ pub fn set_auto_drive_settings(
toml_edit::value(settings.observer_enabled);
doc["auto_drive"]["coordinator_routing"] =
toml_edit::value(settings.coordinator_routing);
doc["auto_drive"]["model"] = toml_edit::value(settings.model.trim());
doc["auto_drive"]["model_reasoning_effort"] = toml_edit::value(
settings
.model_reasoning_effort
.to_string()
.to_ascii_lowercase(),
);
doc["auto_drive"]["auto_resolve_review_attempts"] =
toml_edit::value(settings.auto_resolve_review_attempts.get() as i64);
@@ -1659,6 +1666,10 @@ fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
pub struct ConfigToml {
/// Optional override of model selection.
pub model: Option<String>,
/// Planning model override used when in Read Only (Plan Mode).
pub planning_model: Option<String>,
/// Reasoning effort override used for the planning model.
pub planning_model_reasoning_effort: Option<ReasoningEffort>,
/// Review model override used by the `/review` feature.
pub review_model: Option<String>,
/// Reasoning effort override used for the review model.
+19
View File
@@ -800,6 +800,14 @@ pub struct AutoDriveSettings {
#[serde(default)]
pub continue_mode: AutoDriveContinueMode,
/// Model used for the Auto Drive coordinator and follow-on turns.
#[serde(default = "default_auto_drive_model")]
pub model: String,
/// Reasoning effort applied to the Auto Drive model.
#[serde(default = "default_auto_drive_reasoning_effort")]
pub model_reasoning_effort: ReasoningEffort,
#[serde(default)]
pub auto_resolve_review_attempts: AutoResolveAttemptLimit,
}
@@ -814,11 +822,22 @@ impl Default for AutoDriveSettings {
observer_enabled: true,
coordinator_routing: true,
continue_mode: AutoDriveContinueMode::TenSeconds,
model: default_auto_drive_model(),
model_reasoning_effort: default_auto_drive_reasoning_effort(),
auto_resolve_review_attempts: AutoResolveAttemptLimit::default(),
}
}
}
fn default_auto_drive_model() -> String {
// Keep aligned with the coordinator's preferred model fallback.
String::from("gpt-5.1")
}
const fn default_auto_drive_reasoning_effort() -> ReasoningEffort {
ReasoningEffort::High
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum AutoDriveContinueMode {
+10
View File
@@ -2297,6 +2297,11 @@ impl App<'_> {
widget.apply_review_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::UpdateTextVerbosity(new_verbosity) => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.set_text_verbosity(new_verbosity);
@@ -2413,6 +2418,11 @@ impl App<'_> {
widget.show_review_model_selector();
}
}
AppEvent::ShowAutoDriveModelSelector => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.show_auto_drive_model_selector();
}
}
AppEvent::RunReviewWithScope {
prompt,
hint,
+8
View File
@@ -224,6 +224,12 @@ pub(crate) enum AppEvent {
effort: ReasoningEffort,
},
/// Update the Auto Drive model + reasoning effort
UpdateAutoDriveModelSelection {
model: String,
effort: ReasoningEffort,
},
/// Update the text verbosity level
UpdateTextVerbosity(TextVerbosity),
@@ -237,6 +243,8 @@ pub(crate) enum AppEvent {
UpdateReviewAutoResolveAttempts(u32),
/// Open the review model selector overlay
ShowReviewModelSelector,
/// Open the Auto Drive model selector overlay
ShowAutoDriveModelSelector,
/// Enable/disable a specific validation tool
UpdateValidationTool { name: String, enable: bool },
/// Enable/disable an entire validation group
@@ -1,6 +1,7 @@
use crate::app_event::{AppEvent, AutoContinueMode};
use crate::app_event_sender::AppEventSender;
use crate::colors;
use code_core::config_types::ReasoningEffort;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -16,6 +17,8 @@ use super::settings_panel::{render_panel, PanelFrameStyle};
pub(crate) struct AutoDriveSettingsView {
app_event_tx: AppEventSender,
selected_index: usize,
model: String,
model_reasoning: ReasoningEffort,
review_enabled: bool,
agents_enabled: bool,
cross_check_enabled: bool,
@@ -30,6 +33,8 @@ impl AutoDriveSettingsView {
pub fn new(
app_event_tx: AppEventSender,
model: String,
model_reasoning: ReasoningEffort,
review_enabled: bool,
agents_enabled: bool,
cross_check_enabled: bool,
@@ -41,6 +46,8 @@ impl AutoDriveSettingsView {
Self {
app_event_tx,
selected_index: 0,
model,
model_reasoning,
review_enabled,
agents_enabled,
cross_check_enabled,
@@ -52,7 +59,7 @@ impl AutoDriveSettingsView {
}
fn option_count() -> usize {
3
4
}
fn send_update(&self) {
@@ -65,6 +72,11 @@ impl AutoDriveSettingsView {
});
}
pub fn set_model(&mut self, model: String, effort: ReasoningEffort) {
self.model = model;
self.model_reasoning = effort;
}
fn set_diagnostics(&mut self, enabled: bool) {
self.review_enabled = enabled;
self.cross_check_enabled = enabled;
@@ -73,6 +85,17 @@ impl AutoDriveSettingsView {
self.qa_automation_enabled && (self.review_enabled || self.cross_check_enabled);
}
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 cycle_continue_mode(&mut self, forward: bool) {
self.continue_mode = if forward {
self.continue_mode.cycle_forward()
@@ -85,15 +108,18 @@ impl AutoDriveSettingsView {
fn toggle_selected(&mut self) {
match self.selected_index {
0 => {
self.app_event_tx.send(AppEvent::ShowAutoDriveModelSelector);
}
1 => {
self.agents_enabled = !self.agents_enabled;
self.send_update();
}
1 => {
2 => {
let next = !self.diagnostics_enabled;
self.set_diagnostics(next);
self.send_update();
}
2 => self.cycle_continue_mode(true),
3 => self.cycle_continue_mode(true),
_ => {}
}
}
@@ -126,15 +152,16 @@ impl AutoDriveSettingsView {
let indicator = if selected { "›" } else { " " };
let prefix = format!("{indicator} ");
let (label, enabled) = match index {
0 => (
0 => ("Auto Drive model", true),
1 => (
"Agents enabled (uses multiple agents to speed up complex tasks)",
self.agents_enabled,
),
1 => (
2 => (
"Diagnostics enabled (monitors and adjusts system in real time)",
self.diagnostics_enabled,
),
2 => (
3 => (
"Auto-continue delay",
matches!(self.continue_mode, AutoContinueMode::Manual),
),
@@ -151,14 +178,30 @@ impl AutoDriveSettingsView {
let mut spans = vec![Span::styled(prefix, label_style)];
match index {
0 | 1 => {
0 => {
let model_label = self.model.trim();
let display = if model_label.is_empty() {
"(not set)".to_string()
} else {
format!(
"{} · {}",
model_label,
Self::reasoning_label(self.model_reasoning)
)
};
spans.push(Span::styled(display, label_style));
if selected {
spans.push(Span::raw(" (Enter to change)"));
}
}
1 | 2 => {
let checkbox = if enabled { "[x]" } else { "[ ]" };
spans.push(Span::styled(
format!("{checkbox} {label}"),
label_style,
));
}
2 => {
3 => {
spans.push(Span::styled(label.to_string(), label_style));
spans.push(Span::raw(" "));
spans.push(Span::styled(
@@ -179,12 +222,13 @@ impl AutoDriveSettingsView {
lines.push(self.option_label(0));
lines.push(self.option_label(1));
lines.push(self.option_label(2));
lines.push(self.option_label(3));
lines.push(Line::default());
let footer_style = Style::default().fg(colors::text_dim());
lines.push(Line::from(vec![
Span::styled("Enter", Style::default().fg(colors::primary())),
Span::styled(" toggle", footer_style),
Span::styled(" select/toggle", footer_style),
Span::raw(" "),
Span::styled("←/→", Style::default().fg(colors::primary())),
Span::styled(" adjust delay", footer_style),
@@ -56,6 +56,7 @@ impl FlatPreset {
pub(crate) enum ModelSelectionTarget {
Session,
Review,
AutoDrive,
}
impl ModelSelectionTarget {
@@ -63,6 +64,7 @@ impl ModelSelectionTarget {
match self {
ModelSelectionTarget::Session => "Select Model & Reasoning",
ModelSelectionTarget::Review => "Select Review Model & Reasoning",
ModelSelectionTarget::AutoDrive => "Select Auto Drive Model & Reasoning",
}
}
@@ -70,6 +72,7 @@ impl ModelSelectionTarget {
match self {
ModelSelectionTarget::Session => "Current model",
ModelSelectionTarget::Review => "Review model",
ModelSelectionTarget::AutoDrive => "Auto Drive model",
}
}
@@ -77,6 +80,7 @@ impl ModelSelectionTarget {
match self {
ModelSelectionTarget::Session => "Reasoning effort",
ModelSelectionTarget::Review => "Review reasoning",
ModelSelectionTarget::AutoDrive => "Auto Drive reasoning",
}
}
}
@@ -221,6 +225,12 @@ impl ModelSelectionView {
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;
+153 -10
View File
@@ -13918,6 +13918,8 @@ impl ChatWidget<'_> {
}
fn build_auto_drive_settings_content(&mut self) -> AutoDriveSettingsContent {
let model = self.config.auto_drive.model.clone();
let model_effort = self.config.auto_drive.model_reasoning_effort;
let review = self.auto_state.review_enabled;
let agents = self.auto_state.subagents_enabled;
let cross = self.auto_state.cross_check_enabled;
@@ -13925,6 +13927,8 @@ impl ChatWidget<'_> {
let mode = self.auto_state.continue_mode;
let view = AutoDriveSettingsView::new(
self.app_event_tx.clone(),
model,
model_effort,
review,
agents,
cross,
@@ -15061,11 +15065,18 @@ fi\n\
})
};
let mut auto_config = self.config.clone();
auto_config.model = self.config.auto_drive.model.trim().to_string();
if auto_config.model.is_empty() {
auto_config.model = code_auto_drive_core::MODEL_SLUG.to_string();
}
auto_config.model_reasoning_effort = self.config.auto_drive.model_reasoning_effort;
match start_auto_coordinator(
coordinator_events,
goal.clone(),
conversation,
self.config.clone(),
auto_config,
self.config.debug,
derive_goal_from_history,
) {
@@ -18748,6 +18759,9 @@ Have we met every part of this goal and is there no further work to do?"#
);
return;
}
if self.settings.overlay.is_some() {
self.close_settings_overlay();
}
self.bottom_pane.show_model_selection(
presets,
self.config.review_model.clone(),
@@ -18756,6 +18770,26 @@ Have we met every part of this goal and is there no further work to do?"#
);
}
pub(crate) fn show_auto_drive_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 Auto Drive. Update configuration to define models."
.to_string(),
);
return;
}
if self.settings.overlay.is_some() {
self.close_settings_overlay();
}
self.bottom_pane.show_model_selection(
presets,
self.config.auto_drive.model.clone(),
self.config.auto_drive.model_reasoning_effort,
ModelSelectionTarget::AutoDrive,
);
}
pub(crate) fn apply_model_selection(&mut self, model: String, effort: Option<ReasoningEffort>) {
let trimmed = model.trim();
if trimmed.is_empty() {
@@ -18873,6 +18907,62 @@ Have we met every part of this goal and is there no further work to do?"#
self.request_redraw();
}
pub(crate) fn apply_auto_drive_model_selection(
&mut self,
model: String,
effort: ReasoningEffort,
) {
let trimmed = model.trim();
if trimmed.is_empty() {
return;
}
let mut updated = false;
if !self.config.auto_drive.model.eq_ignore_ascii_case(trimmed) {
self.config.auto_drive.model = trimmed.to_string();
updated = true;
}
if self.config.auto_drive.model_reasoning_effort != effort {
self.config.auto_drive.model_reasoning_effort = effort;
updated = true;
}
if !updated {
self.bottom_pane
.flash_footer_notice("Auto Drive model unchanged.".to_string());
return;
}
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) {
Ok(_) => format!(
"Auto Drive model set to {} ({} reasoning)",
self.config.auto_drive.model,
Self::format_reasoning_effort(self.config.auto_drive.model_reasoning_effort)
),
Err(err) => {
tracing::warn!("Failed to persist Auto Drive model: {err}");
format!(
"Auto Drive model set for this session (failed to persist): {}",
self.config.auto_drive.model
)
}
}
} else {
tracing::warn!("Could not locate Code home to persist Auto Drive model");
format!(
"Auto Drive model set for this session: {}",
self.config.auto_drive.model
)
};
self.bottom_pane.flash_footer_notice(message);
self.refresh_settings_overview_rows();
self.update_auto_drive_settings_model_row();
self.request_redraw();
}
pub(crate) fn handle_reasoning_command(&mut self, command_args: String) {
// command_args contains only the arguments after the command (e.g., "high" not "/reasoning high")
let trimmed = command_args.trim();
@@ -19593,14 +19683,16 @@ Have we met every part of this goal and is there no further work to do?"#
}
fn settings_summary_model(&self) -> Option<String> {
let mut parts: Vec<String> = Vec::new();
let model = self.config.model.trim();
let model_display = if model.is_empty() { "—" } else { model };
parts.push(format!("Model: {}", model_display));
parts.push(format!(
"Effort: {}",
Self::format_reasoning_effort(self.config.model_reasoning_effort)
));
let model_display_storage;
let model_display = if model.is_empty() {
"—"
} else {
model_display_storage = Self::format_model_label(model);
&model_display_storage
};
let effort = Self::format_reasoning_effort(self.config.model_reasoning_effort);
let mut parts: Vec<String> = vec![format!("Model: {} ({})", model_display, effort)];
if let Some(profile) = self
.config
.active_profile
@@ -19660,8 +19752,16 @@ 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()
} else {
Self::format_model_label(&self.config.auto_drive.model)
};
let effort = Self::format_reasoning_effort(self.config.auto_drive.model_reasoning_effort);
Some(format!(
"Agents: {} · Diagnostics: {} · Continue: {}",
"Model: {} ({}) · Agents: {} · Diagnostics: {} · Continue: {}",
model_label,
effort,
Self::on_off_label(self.auto_state.subagents_enabled),
Self::on_off_label(diagnostics_enabled),
self.auto_state.continue_mode.label()
@@ -19689,7 +19789,11 @@ Have we met every part of this goal and is there no further work to do?"#
format!("Auto Resolve: On (max {} re-reviews)", attempts)
};
let model = self.config.review_model.trim();
let model_display = if model.is_empty() { "(not set)" } else { model };
let model_display = if model.is_empty() {
"(not set)".to_string()
} else {
Self::format_model_label(model)
};
let model_part = format!(
"Model: {} ({})",
model_display,
@@ -19762,6 +19866,34 @@ Have we met every part of this goal and is there no further work to do?"#
}
}
fn format_model_label(model: &str) -> String {
let mut parts = Vec::new();
for (idx, part) in model.split('-').enumerate() {
if idx == 0 {
parts.push(part.to_ascii_uppercase());
continue;
}
let mut chars = part.chars();
let formatted = match chars.next() {
Some(first) if first.is_ascii_alphabetic() => {
let mut s = String::new();
s.push(first.to_ascii_uppercase());
s.push_str(chars.as_str());
s
}
Some(first) => {
let mut s = String::new();
s.push(first);
s.push_str(chars.as_str());
s
}
None => String::new(),
};
parts.push(formatted);
}
parts.join("-")
}
fn on_off_label(value: bool) -> &'static str {
if value { "On" } else { "Off" }
}
@@ -27820,6 +27952,17 @@ impl ChatWidget<'_> {
}
}
fn update_auto_drive_settings_model_row(&mut self) {
if let Some(overlay) = self.settings.overlay.as_mut() {
if let Some(content) = overlay.auto_drive_content_mut() {
content.update_model(
self.config.auto_drive.model.clone(),
self.config.auto_drive.model_reasoning_effort,
);
}
}
}
pub(crate) fn show_review_commit_loading(&mut self) {
let loading_item = SelectionItem {
name: "Loading recent commits…".to_string(),
@@ -343,6 +343,10 @@ impl AutoDriveSettingsContent {
pub(crate) fn new(view: AutoDriveSettingsView) -> Self {
Self { view }
}
pub(crate) fn update_model(&mut self, model: String, effort: ReasoningEffort) {
self.view.set_model(model, effort);
}
}
impl SettingsContent for AutoDriveSettingsContent {
@@ -1314,6 +1318,10 @@ impl SettingsOverlayView {
self.review_content.as_mut()
}
pub(crate) fn auto_drive_content_mut(&mut self) -> Option<&mut AutoDriveSettingsContent> {
self.auto_drive_content.as_mut()
}
pub(crate) fn limits_content_mut(&mut self) -> Option<&mut LimitsSettingsContent> {
self.limits_content.as_mut()
}