Select thinking mode from the chat

This commit is contained in:
Georg Bauer
2026-08-01 09:49:37 +02:00
parent d0431edaff
commit c0e4ec13cc
5 changed files with 104 additions and 9 deletions

View File

@@ -18,7 +18,10 @@ tool work continues concurrently.
The composer status row shows the current project and, for Git repositories, The composer status row shows the current project and, for Git repositories,
the current local branch. An unsaved draft can be moved with the project menu; the current local branch. An unsaved draft can be moved with the project menu;
saved chats keep their original project. Use the branch menu to switch local saved chats keep their original project. Use the branch menu to switch local
branches when that project's chats are idle. branches when that project's chats are idle. The thinking menu is preselected
to the model's current mode: **Thinking** is the DS4 default, while **Direct**
answers without hidden reasoning. **Think Max** is available when the context
window is at least 393216 tokens, matching DS4's long-context requirement.
The **Git** pane shows added, changed, and deleted files from the current The **Git** pane shows added, changed, and deleted files from the current
worktree, including separate staged and worktree markers. Select files to stage, worktree, including separate staged and worktree markers. Select files to stage,

View File

@@ -449,6 +449,7 @@ pub(crate) enum Message {
CreateSession(i32), CreateSession(i32),
DraftProjectChanged(i32), DraftProjectChanged(i32),
SwitchGitBranch(String), SwitchGitBranch(String),
ReasoningModeChanged(ReasoningMode),
PermissionModeChanged(PermissionMode), PermissionModeChanged(PermissionMode),
ToggleGitFile(PathBuf), ToggleGitFile(PathBuf),
OpenGitDiff(PathBuf), OpenGitDiff(PathBuf),
@@ -1522,6 +1523,34 @@ impl App {
} }
Message::DraftProjectChanged(project_id) => self.move_draft_to_project(project_id), Message::DraftProjectChanged(project_id) => self.move_draft_to_project(project_id),
Message::SwitchGitBranch(branch) => self.switch_git_branch(&branch), Message::SwitchGitBranch(branch) => self.switch_git_branch(&branch),
Message::ReasoningModeChanged(mode) => {
if self.active_chat_count() > 0 {
self.error =
Some("Stop all active generations before changing thinking mode.".into());
} else if !self
.config
.generation
.supported_reasoning_modes()
.contains(&mode)
{
self.error = Some(
"Think Max requires a context window of at least 393216 tokens.".into(),
);
} else {
let mut config = self.config.clone();
config.generation.reasoning_mode = mode;
match config.save(&config_path()) {
Ok(()) => {
self.config = config;
self.preference_draft.reasoning_mode = mode;
self.error = None;
#[cfg(target_os = "macos")]
preferences::update_runtime_config(&self.runtime_config, &self.config);
}
Err(error) => self.error = Some(error),
}
}
}
Message::PermissionModeChanged(mode) => { Message::PermissionModeChanged(mode) => {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if self.active_tools.is_some() { if self.active_tools.is_some() {

View File

@@ -345,7 +345,7 @@ fn optional_string<T: ToString>(value: Option<T>) -> String {
value.map_or_else(String::new, |value| value.to_string()) value.map_or_else(String::new, |value| value.to_string())
} }
fn update_runtime_config(runtime_config: &RwLock<Config>, config: &Config) { pub(super) fn update_runtime_config(runtime_config: &RwLock<Config>, config: &Config) {
*runtime_config *runtime_config
.write() .write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = config.clone(); .unwrap_or_else(|poisoned| poisoned.into_inner()) = config.clone();

View File

@@ -305,6 +305,19 @@ impl App {
.text_size(12) .text_size(12)
.padding([2, 6]) .padding([2, 6])
}); });
let reasoning_mode = self.config.generation.effective_reasoning_mode();
let reasoning_control: Element<'_, Message> = if self.active_chat_count() > 0 {
text(reasoning_mode.to_string()).size(12).into()
} else {
pick_list(
self.config.generation.supported_reasoning_modes(),
Some(reasoning_mode),
Message::ReasoningModeChanged,
)
.text_size(12)
.padding([2, 6])
.into()
};
composer_content = composer_content =
composer_content.push( composer_content.push(
row![ row![
@@ -341,6 +354,7 @@ impl App {
) )
.text_size(12) .text_size(12)
.padding([2, 6]), .padding([2, 6]),
reasoning_control,
icon(ICON_MODEL, 16), icon(ICON_MODEL, 16),
text(self.config.model.to_string()).size(12), text(self.config.model.to_string()).size(12),
action, action,

View File

@@ -8,6 +8,7 @@ pub(crate) const REASONING_MODES: [ReasoningMode; 3] = [
ReasoningMode::Max, ReasoningMode::Max,
ReasoningMode::Direct, ReasoningMode::Direct,
]; ];
const STANDARD_REASONING_MODES: [ReasoningMode; 2] = [ReasoningMode::High, ReasoningMode::Direct];
const THINK_MAX_MIN_CONTEXT: i32 = 393_216; const THINK_MAX_MIN_CONTEXT: i32 = 393_216;
const MAX_CPU_THREADS: u32 = 32; const MAX_CPU_THREADS: u32 = 32;
const MAX_MTP_DRAFT_TOKENS: i32 = 16; const MAX_MTP_DRAFT_TOKENS: i32 = 16;
@@ -555,6 +556,23 @@ impl Default for GenerationPreferences {
} }
impl GenerationPreferences { impl GenerationPreferences {
pub(crate) fn effective_reasoning_mode(&self) -> ReasoningMode {
if self.reasoning_mode == ReasoningMode::Max && self.context_tokens < THINK_MAX_MIN_CONTEXT
{
ReasoningMode::High
} else {
self.reasoning_mode
}
}
pub(crate) fn supported_reasoning_modes(&self) -> &'static [ReasoningMode] {
if self.context_tokens >= THINK_MAX_MIN_CONTEXT {
&REASONING_MODES
} else {
&STANDARD_REASONING_MODES
}
}
pub(crate) fn validate(&self) -> Result<(), String> { pub(crate) fn validate(&self) -> Result<(), String> {
if self.context_tokens <= 0 { if self.context_tokens <= 0 {
return Err("Context tokens must be a positive whole number.".into()); return Err("Context tokens must be a positive whole number.".into());
@@ -588,13 +606,7 @@ impl GenerationPreferences {
top_k: 0, top_k: 0,
stops: Vec::new(), stops: Vec::new(),
seed: self.seed, seed: self.seed,
reasoning_mode: if self.reasoning_mode == ReasoningMode::Max reasoning_mode: self.effective_reasoning_mode(),
&& self.context_tokens < THINK_MAX_MIN_CONTEXT
{
ReasoningMode::High
} else {
self.reasoning_mode
},
} }
} }
} }
@@ -680,6 +692,11 @@ mod tests {
let glm = defaults.turn_settings(ModelChoice::Glm52, cache); let glm = defaults.turn_settings(ModelChoice::Glm52, cache);
assert_eq!((glm.temperature, glm.top_p, glm.min_p), (1.0, 0.95, 0.0)); assert_eq!((glm.temperature, glm.top_p, glm.min_p), (1.0, 0.95, 0.0));
assert_eq!(
defaults.supported_reasoning_modes(),
&[ReasoningMode::High, ReasoningMode::Direct]
);
assert_eq!(defaults.effective_reasoning_mode(), ReasoningMode::High);
let explicit = GenerationPreferences { let explicit = GenerationPreferences {
top_p: Some(0.4), top_p: Some(0.4),
@@ -690,6 +707,38 @@ mod tests {
let effective = explicit.turn_settings(ModelChoice::Glm52, cache); let effective = explicit.turn_settings(ModelChoice::Glm52, cache);
assert_eq!((effective.top_p, effective.min_p), (0.4, 0.2)); assert_eq!((effective.top_p, effective.min_p), (0.4, 0.2));
assert_eq!(effective.reasoning_mode, ReasoningMode::High); assert_eq!(effective.reasoning_mode, ReasoningMode::High);
let long_context = GenerationPreferences {
context_tokens: THINK_MAX_MIN_CONTEXT,
reasoning_mode: ReasoningMode::Max,
..GenerationPreferences::default()
};
assert_eq!(long_context.supported_reasoning_modes(), &REASONING_MODES);
assert_eq!(long_context.effective_reasoning_mode(), ReasoningMode::Max);
}
#[test]
fn reasoning_selection_reaches_every_model_turn() {
let cache = KvCachePreferences::default().settings();
for model in crate::model::MODEL_CHOICES {
for mode in [ReasoningMode::Direct, ReasoningMode::High] {
let generation = GenerationPreferences {
reasoning_mode: mode,
..GenerationPreferences::default()
};
assert_eq!(generation.turn_settings(model, cache).reasoning_mode, mode);
}
let generation = GenerationPreferences {
context_tokens: THINK_MAX_MIN_CONTEXT,
reasoning_mode: ReasoningMode::Max,
..GenerationPreferences::default()
};
assert_eq!(
generation.turn_settings(model, cache).reasoning_mode,
ReasoningMode::Max
);
}
} }
#[test] #[test]