mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(auto-drive): allow model selection and clamp verbosity
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user