返回 CodeWhale
effort.rs
根目录 / crates / tui / src / commands / groups / core / effort.rs
1 //! Effort / Thinking slash commands — `/effort` and `/thinking`.
2 //!
3 //! Lets users change the model's reasoning effort via slash command as well as `Ctrl+T`.
4 //! Mirrors the Muse Spark UX where `/effort [<none|minimal|low|medium|high|xhigh|ultra>]`
5 //! is offered via autocomplete. Both `/effort` and `/thinking` are aliases for the same
6 //! underlying `ReasoningEffort` state, keeping `Thinking` as the canonical UI label
7 //! (per user request to keep it as Thinking and fix for everything).
8
9 use crate::commands::{CommandResult, traits::CommandInfo};
10 use crate::localization::{MessageId, tr};
11 use crate::tui::app::{App, ReasoningEffort};
12
13 pub const EFFORT_INFO: CommandInfo = CommandInfo {
14 name: "effort",
15 aliases: &["thinking", "reasoning", "reason"],
16 usage: "/effort [<none|minimal|low|medium|high|xhigh|ultra|max|auto|off>]",
17 description_id: MessageId::CmdEffortDescription,
18 };
19
20 pub fn effort(app: &mut App, args: Option<&str>) -> CommandResult {
21 let arg = args.unwrap_or("").trim().to_ascii_lowercase();
22 // Determine available efforts for the current model (like model picker does)
23 let available: Vec<ReasoningEffort> = {
24 let provider = app.api_provider;
25 let base_url = app.active_route_base_url.clone();
26 // Try to get the current wire model; fallback to app.model
27 let wire_model = crate::tui::model_picker::picker_efforts_for_route(
28 provider,
29 &base_url,
30 &app.model,
31 app.auto_model,
32 );
33 // If picker returns empty, fallback to the model's catalog efforts
34 if !wire_model.is_empty() {
35 wire_model
36 } else {
37 vec![]
38 }
39 };
40 let available_labels: Vec<String> = if available.is_empty() {
41 vec![
42 "none".into(),
43 "minimal".into(),
44 "low".into(),
45 "medium".into(),
46 "high".into(),
47 "xhigh".into(),
48 "ultra".into(),
49 "max".into(),
50 "auto".into(),
51 "off".into(),
52 ]
53 } else {
54 available
55 .iter()
56 .map(|e| e.display_label_for_provider(app.api_provider).to_string())
57 .collect()
58 };
59 if arg.is_empty() {
60 // No arg: cycle to next (same as Ctrl+T) and show available
61 let prev = app.reasoning_effort;
62 let next = prev.cycle_next_for_provider(app.api_provider);
63 app.reasoning_effort_preference = Some(next);
64 app.reasoning_effort = next;
65 app.invalidate_route_receipts_for_reasoning_change();
66 app.update_model_compaction_budget();
67 return CommandResult::message(format!(
68 "Effort: {} → {} (available: {})",
69 prev.display_label_for_provider(app.api_provider),
70 next.display_label_for_provider(app.api_provider),
71 available_labels.join("|")
72 ));
73 }
74 // Validate against the current model's available set (not all 10)
75 let is_available = available_labels
76 .iter()
77 .any(|l| l.eq_ignore_ascii_case(&arg));
78 if !is_available && !arg.eq_ignore_ascii_case("auto") && !arg.eq_ignore_ascii_case("off") {
79 return CommandResult::error(format!(
80 "'{}' not available for {} — available: {}",
81 arg,
82 app.model,
83 available_labels.join("|")
84 ));
85 }
86 // from_setting always returns a ReasoningEffort (defaults to Max on unknown), so validate against available set first
87 if let Ok(effort) = ReasoningEffort::parse_strict(&arg) {
88 let normalized = effort.normalize_for_provider(app.api_provider);
89 app.reasoning_effort_preference = Some(normalized);
90 app.reasoning_effort = normalized;
91 app.invalidate_route_receipts_for_reasoning_change();
92 app.update_model_compaction_budget();
93 CommandResult::message(format!(
94 "Effort set to {}",
95 normalized.display_label_for_provider(app.api_provider)
96 ))
97 } else {
98 let hint = format!(
99 "Usage: /effort <{}> (also /thinking)",
100 available_labels.join("|")
101 );
102 CommandResult::error(format!(
103 "{} — {}",
104 tr(app.ui_locale, MessageId::CmdEffortDescription),
105 hint
106 ))
107 }
108 }
109
109 lines RUST