mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
follow models
This commit is contained in:
+116
-4
@@ -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,
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ¤t_model, current_effort);
|
||||
let initial_index = Self::initial_selection(
|
||||
target.supports_follow_chat(),
|
||||
use_chat_model,
|
||||
&flat_presets,
|
||||
¤t_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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user