@@ -3,7 +3,9 @@ use dialoguer::{Input, Password, Select, theme::ColorfulTheme};
33use tracing:: info;
44
55use crate :: channels:: { self , signal, slack, telegram} ;
6+ use crate :: claude;
67use crate :: config:: { self , AiBackend , Config , SignalConfig , SlackConfig , TelegramConfig } ;
8+ use crate :: cursor;
79use 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
122222async 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
11061234async 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 ( ) ;
0 commit comments