Skip to content

Commit dcf47c4

Browse files
committed
Improve model selection
1 parent 2a07ca7 commit dcf47c4

File tree

4 files changed

+267
-75
lines changed

4 files changed

+267
-75
lines changed

src/claude.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ use tracing::{debug, info, warn};
99
use crate::config::{self, Config};
1010
use crate::setup;
1111

12+
/// Available models for Claude Code, newest first.
13+
pub const MODELS: &[(&str, &str)] = &[
14+
("claude-opus-4-6", "Claude Opus 4.6"),
15+
("claude-opus-4-5", "Claude Opus 4.5"),
16+
("claude-sonnet-4-5", "Claude Sonnet 4.5"),
17+
];
18+
1219
/// Response from Claude CLI in JSON format
1320
#[derive(Debug, Deserialize)]
1421
struct ClaudeResponse {
@@ -30,7 +37,7 @@ pub struct QueryOptions {
3037
pub cwd: Option<String>,
3138
/// Skip permission prompts (for automated flows)
3239
pub skip_permissions: bool,
33-
/// Model alias or full model name (e.g. "sonnet", "opus")
40+
/// Model alias ("sonnet", "opus") or full model ID (e.g. "claude-sonnet-4-5-20250929")
3441
pub model: Option<String>,
3542
}
3643

src/cmd/init.rs

Lines changed: 183 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use dialoguer::{Input, Password, Select, theme::ColorfulTheme};
33
use tracing::info;
44

55
use crate::channels::{self, signal, slack, telegram};
6+
use crate::claude;
67
use crate::config::{self, AiBackend, Config, SignalConfig, SlackConfig, TelegramConfig};
8+
use crate::cursor;
79
use crate::setup;
810

911
/// Run the init command
@@ -37,13 +39,11 @@ pub async fn run() -> Result<()> {
3739
println!("Current setup: {}", status.join(", "));
3840
println!();
3941

40-
// Build menu options dynamically
4142
let mut choices = vec![
4243
"Add/configure a channel",
4344
"Configure AI backend (Claude Code or Cursor CLI)",
4445
];
4546

46-
// Add switch option if both backends are configured
4747
let can_switch = config.is_claude_configured() && config.is_cursor_configured();
4848
if can_switch {
4949
choices.push("Switch active AI backend");
@@ -67,7 +67,7 @@ pub async fn run() -> Result<()> {
6767
} else if selected == "Switch active AI backend" {
6868
return switch_ai_backend(config).await;
6969
} else if selected == "Reconfigure from scratch" {
70-
// Continue to fresh setup below
70+
// fall through to fresh setup
7171
} else {
7272
println!("Cancelled.");
7373
return Ok(());
@@ -97,6 +97,54 @@ async fn setup_ai_backend(existing_config: Option<Config>) -> Result<()> {
9797
println!("AI Backend Setup");
9898
println!("────────────────");
9999
println!();
100+
101+
let has_backend = existing_config
102+
.as_ref()
103+
.is_some_and(|c| c.is_backend_configured());
104+
105+
if has_backend {
106+
let config = existing_config.as_ref().unwrap();
107+
let backend_name = match config.backend {
108+
AiBackend::Claude => "Claude Code",
109+
AiBackend::Cursor => "Cursor CLI",
110+
};
111+
let current_model = match config.backend {
112+
AiBackend::Claude => config.claude.model.as_deref(),
113+
AiBackend::Cursor => config.cursor.model.as_deref(),
114+
};
115+
println!(
116+
"Current: {} (model: {})",
117+
backend_name,
118+
current_model.unwrap_or("default")
119+
);
120+
println!();
121+
122+
let choices = vec![
123+
"Change model",
124+
"Reconfigure backend (Claude Code or Cursor CLI)",
125+
"Cancel",
126+
];
127+
128+
let selection = Select::with_theme(&ColorfulTheme::default())
129+
.with_prompt("What would you like to do?")
130+
.items(&choices)
131+
.default(0)
132+
.interact()?;
133+
134+
return match selection {
135+
0 => change_model(existing_config.unwrap()).await,
136+
1 => pick_backend(existing_config).await,
137+
_ => {
138+
println!("Cancelled.");
139+
Ok(())
140+
}
141+
};
142+
}
143+
144+
pick_backend(existing_config).await
145+
}
146+
147+
async fn pick_backend(existing_config: Option<Config>) -> Result<()> {
100148
println!("Cica can use either Claude Code or Cursor CLI as its AI backend.");
101149
println!();
102150

@@ -118,6 +166,58 @@ async fn setup_ai_backend(existing_config: Option<Config>) -> Result<()> {
118166
}
119167
}
120168

169+
/// Change the model for the active backend
170+
async fn change_model(mut config: Config) -> Result<()> {
171+
let (backend_name, current_model) = match config.backend {
172+
AiBackend::Claude => ("Claude Code", config.claude.model.as_deref()),
173+
AiBackend::Cursor => ("Cursor CLI", config.cursor.model.as_deref()),
174+
};
175+
176+
println!();
177+
println!("Change Model");
178+
println!("────────────");
179+
println!();
180+
println!(
181+
"Backend: {} | Current model: {}",
182+
backend_name,
183+
current_model.unwrap_or("default")
184+
);
185+
println!();
186+
187+
let new_model = match config.backend {
188+
AiBackend::Claude => select_model(backend_name, claude::MODELS, current_model)?,
189+
AiBackend::Cursor => {
190+
let api_key = config
191+
.cursor
192+
.api_key
193+
.as_deref()
194+
.unwrap_or_default()
195+
.to_string();
196+
print!("Fetching available models... ");
197+
std::io::Write::flush(&mut std::io::stdout())?;
198+
let models = cursor::list_models(&api_key).await;
199+
println!("OK ({} models)", models.len());
200+
println!();
201+
select_model(backend_name, &models, current_model)?
202+
}
203+
};
204+
205+
match config.backend {
206+
AiBackend::Claude => config.claude.model = new_model.clone(),
207+
AiBackend::Cursor => config.cursor.model = new_model.clone(),
208+
}
209+
210+
config.save()?;
211+
212+
println!();
213+
println!(
214+
"Model set to: {}",
215+
new_model.as_deref().unwrap_or("default")
216+
);
217+
218+
Ok(())
219+
}
220+
121221
/// Switch between configured AI backends
122222
async fn switch_ai_backend(mut config: Config) -> Result<()> {
123223
println!();
@@ -1016,52 +1116,11 @@ async fn setup_claude(existing_config: Option<Config>) -> Result<()> {
10161116

10171117
// Model selection
10181118
println!();
1019-
println!("Claude Code supports multiple models.");
1020-
println!("Aliases always point to the latest version.");
1021-
println!();
1022-
1023-
let model_choices = vec![
1024-
"default Recommended for your account type",
1025-
"sonnet Latest Sonnet (fast, daily coding)",
1026-
"opus Latest Opus (complex reasoning)",
1027-
"haiku Latest Haiku (fast, simple tasks)",
1028-
"opusplan Opus for planning, Sonnet for execution",
1029-
"Custom Enter a full model name",
1030-
];
1031-
1032-
let current_model_idx = config
1033-
.claude
1034-
.model
1035-
.as_deref()
1036-
.map(|m| match m {
1037-
"default" => 0,
1038-
"sonnet" => 1,
1039-
"opus" => 2,
1040-
"haiku" => 3,
1041-
"opusplan" => 4,
1042-
_ => 5,
1043-
})
1044-
.unwrap_or(0);
1045-
1046-
let model_selection = Select::with_theme(&ColorfulTheme::default())
1047-
.with_prompt("Which model would you like to use?")
1048-
.items(&model_choices)
1049-
.default(current_model_idx)
1050-
.interact()?;
1051-
1052-
config.claude.model = match model_selection {
1053-
0 => None,
1054-
1 => Some("sonnet".to_string()),
1055-
2 => Some("opus".to_string()),
1056-
3 => Some("haiku".to_string()),
1057-
4 => Some("opusplan".to_string()),
1058-
_ => {
1059-
let custom: String = Input::with_theme(&ColorfulTheme::default())
1060-
.with_prompt("Model name (e.g. claude-sonnet-4-5-20250929)")
1061-
.interact_text()?;
1062-
Some(custom.trim().to_string())
1063-
}
1064-
};
1119+
config.claude.model = select_model(
1120+
"Claude Code",
1121+
claude::MODELS,
1122+
config.claude.model.as_deref(),
1123+
)?;
10651124

10661125
// Ask whether to switch if another backend was active
10671126
if was_using_cursor {
@@ -1102,6 +1161,75 @@ async fn setup_claude(existing_config: Option<Config>) -> Result<()> {
11021161
Ok(())
11031162
}
11041163

1164+
/// Interactive model picker shared across backends.
1165+
fn select_model<S: AsRef<str>>(
1166+
backend_name: &str,
1167+
models: &[(S, S)],
1168+
current: Option<&str>,
1169+
) -> Result<Option<String>> {
1170+
println!("Select a model for {}:", backend_name);
1171+
println!();
1172+
1173+
let max_id_len = models
1174+
.iter()
1175+
.map(|(id, _)| id.as_ref().len())
1176+
.max()
1177+
.unwrap_or(20);
1178+
let pad = max_id_len.max(20);
1179+
1180+
let mut choices: Vec<String> = Vec::with_capacity(models.len() + 2);
1181+
choices.push(format!(
1182+
"{:<pad$} Use the default model",
1183+
"default",
1184+
pad = pad
1185+
));
1186+
for (id, name) in models {
1187+
choices.push(format!(
1188+
"{:<pad$} {}",
1189+
id.as_ref(),
1190+
name.as_ref(),
1191+
pad = pad
1192+
));
1193+
}
1194+
choices.push(format!(
1195+
"{:<pad$} Enter a model name manually",
1196+
"custom",
1197+
pad = pad
1198+
));
1199+
1200+
let current_idx = current
1201+
.and_then(|c| {
1202+
models
1203+
.iter()
1204+
.position(|(id, _)| id.as_ref() == c)
1205+
.map(|i| i + 1)
1206+
})
1207+
.unwrap_or(0);
1208+
1209+
let selection = Select::with_theme(&ColorfulTheme::default())
1210+
.with_prompt("Which model would you like to use?")
1211+
.items(&choices)
1212+
.default(current_idx)
1213+
.interact()?;
1214+
1215+
if selection == 0 {
1216+
Ok(None)
1217+
} else if selection == choices.len() - 1 {
1218+
let custom: String = Input::with_theme(&ColorfulTheme::default())
1219+
.with_prompt("Model name")
1220+
.interact_text()?;
1221+
let trimmed = custom.trim().to_string();
1222+
if trimmed.is_empty() {
1223+
Ok(None)
1224+
} else {
1225+
Ok(Some(trimmed))
1226+
}
1227+
} else {
1228+
let (id, _) = &models[selection - 1];
1229+
Ok(Some(id.as_ref().to_string()))
1230+
}
1231+
}
1232+
11051233
/// Set up Cursor CLI
11061234
async fn setup_cursor(existing_config: Option<Config>) -> Result<()> {
11071235
println!();
@@ -1148,32 +1276,14 @@ async fn setup_cursor(existing_config: Option<Config>) -> Result<()> {
11481276
}
11491277
}
11501278

1151-
// Ask about model preference
1279+
// Model selection
11521280
println!();
1153-
println!("Cursor CLI supports multiple AI models.");
1281+
print!("Fetching available models... ");
1282+
std::io::Write::flush(&mut std::io::stdout())?;
1283+
let cursor_models = cursor::list_models(&api_key).await;
1284+
println!("OK ({} models)", cursor_models.len());
11541285
println!();
1155-
1156-
let model_choices = vec![
1157-
"sonnet-4.5 Claude Sonnet 4.5 (recommended)",
1158-
"opus-4.5 Claude Opus 4.5",
1159-
"gpt-5.2 OpenAI GPT 5.2",
1160-
"gemini-3-pro Google Gemini 3 Pro",
1161-
"auto Let Cursor choose",
1162-
];
1163-
1164-
let model_selection = Select::with_theme(&ColorfulTheme::default())
1165-
.with_prompt("Which model would you like to use?")
1166-
.items(&model_choices)
1167-
.default(0)
1168-
.interact()?;
1169-
1170-
let model = match model_selection {
1171-
0 => Some("sonnet-4.5".to_string()),
1172-
1 => Some("opus-4.5".to_string()),
1173-
2 => Some("gpt-5.2".to_string()),
1174-
3 => Some("gemini-3-pro".to_string()),
1175-
_ => Some("auto".to_string()),
1176-
};
1286+
let model = select_model("Cursor CLI", &cursor_models, None)?;
11771287

11781288
// Save config
11791289
let mut config = existing_config.unwrap_or_default();

src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ impl Config {
262262
pub struct ClaudeConfig {
263263
/// Anthropic API key or OAuth token (used when not using Vertex AI)
264264
pub api_key: Option<String>,
265-
/// Model alias or full model name (e.g. "sonnet", "opus", "claude-sonnet-4-5-20250929")
265+
/// Model to use: an alias ("sonnet", "opus") or full model ID from the API (e.g. "claude-sonnet-4-5-20250929")
266266
pub model: Option<String>,
267267
/// Use Google Vertex AI instead of Anthropic API
268268
#[serde(default)]

0 commit comments

Comments
 (0)