2025-12-24 17:17:35 +00:00
use crate ::commands ::{ fs , search , shell } ;
use crate ::llm ::ollama ::OllamaProvider ;
2025-12-25 13:10:03 +00:00
use crate ::llm ::prompts ::SYSTEM_PROMPT ;
2025-12-24 17:17:35 +00:00
use crate ::llm ::types ::{
Message , ModelProvider , Role , ToolCall , ToolDefinition , ToolFunctionDefinition ,
} ;
use crate ::state ::SessionState ;
2025-12-24 17:32:46 +00:00
use serde ::Deserialize ;
2025-12-24 17:17:35 +00:00
use serde_json ::json ;
2025-12-25 12:39:20 +00:00
use tauri ::{ AppHandle , Emitter , State } ;
2025-12-27 15:36:58 +00:00
use tokio ::select ;
2025-12-24 17:17:35 +00:00
#[ derive(Deserialize) ]
pub struct ProviderConfig {
pub provider : String , // "ollama"
pub model : String ,
pub base_url : Option < String > ,
2025-12-24 17:32:46 +00:00
pub enable_tools : Option < bool > ,
2025-12-24 17:17:35 +00:00
}
2025-12-25 15:18:12 +00:00
const MAX_TURNS : usize = 30 ;
2025-12-24 17:17:35 +00:00
2025-12-25 12:21:58 +00:00
#[ tauri::command ]
pub async fn get_ollama_models ( base_url : Option < String > ) -> Result < Vec < String > , String > {
let url = base_url . unwrap_or_else ( | | " http://localhost:11434 " . to_string ( ) ) ;
OllamaProvider ::get_models ( & url ) . await
}
2025-12-27 15:36:58 +00:00
#[ tauri::command ]
pub async fn cancel_chat ( state : State < '_ , SessionState > ) -> Result < ( ) , String > {
state . cancel_tx . send ( true ) . map_err ( | e | e . to_string ( ) ) ? ;
Ok ( ( ) )
}
2025-12-24 17:17:35 +00:00
#[ tauri::command ]
pub async fn chat (
2025-12-25 12:39:20 +00:00
app : AppHandle ,
2025-12-24 17:17:35 +00:00
messages : Vec < Message > ,
config : ProviderConfig ,
state : State < '_ , SessionState > ,
) -> Result < Vec < Message > , String > {
2025-12-27 15:36:58 +00:00
// Reset cancellation flag at start
let _ = state . cancel_tx . send ( false ) ;
let mut cancel_rx = state . cancel_rx . clone ( ) ;
2025-12-24 17:17:35 +00:00
// 1. Setup Provider
let provider : Box < dyn ModelProvider > = match config . provider . as_str ( ) {
" ollama " = > Box ::new ( OllamaProvider ::new (
config
. base_url
. unwrap_or_else ( | | " http://localhost:11434 " . to_string ( ) ) ,
) ) ,
_ = > return Err ( format! ( " Unsupported provider: {} " , config . provider ) ) ,
} ;
// 2. Define Tools
2025-12-24 17:32:46 +00:00
let tool_defs = get_tool_definitions ( ) ;
let tools = if config . enable_tools . unwrap_or ( true ) {
tool_defs . as_slice ( )
} else {
& [ ]
} ;
2025-12-24 17:17:35 +00:00
// 3. Agent Loop
let mut current_history = messages . clone ( ) ;
2025-12-25 13:10:03 +00:00
// Inject System Prompt
current_history . insert (
0 ,
Message {
role : Role ::System ,
content : SYSTEM_PROMPT . to_string ( ) ,
tool_calls : None ,
tool_call_id : None ,
} ,
) ;
2025-12-25 15:39:22 +00:00
// Inject reminder as a second system message
2025-12-25 15:18:12 +00:00
current_history . insert (
1 ,
Message {
role : Role ::System ,
2025-12-25 15:39:22 +00:00
content : " REMINDER: Distinguish between showing examples (use code blocks in chat) vs implementing changes (use write_file tool). Keywords like 'show me', 'example', 'how does' = chat response. Keywords like 'create', 'add', 'implement', 'fix' = use tools. " . to_string ( ) ,
2025-12-25 15:18:12 +00:00
tool_calls : None ,
tool_call_id : None ,
} ,
) ;
2025-12-24 17:17:35 +00:00
let mut new_messages : Vec < Message > = Vec ::new ( ) ;
let mut turn_count = 0 ;
loop {
if turn_count > = MAX_TURNS {
return Err ( " Max conversation turns reached. " . to_string ( ) ) ;
}
turn_count + = 1 ;
2025-12-27 15:36:58 +00:00
// Call LLM with cancellation support
let chat_future = provider . chat ( & config . model , & current_history , tools ) ;
let response = select! {
result = chat_future = > {
result . map_err ( | e | format! ( " LLM Error: {} " , e ) ) ?
}
_ = cancel_rx . changed ( ) = > {
if * cancel_rx . borrow ( ) {
return Err ( " Chat cancelled by user " . to_string ( ) ) ;
}
// False alarm, continue
provider . chat ( & config . model , & current_history , tools )
. await
. map_err ( | e | format! ( " LLM Error: {} " , e ) ) ?
}
} ;
2025-12-24 17:17:35 +00:00
// Process Response
if let Some ( tool_calls ) = response . tool_calls {
// The Assistant wants to run tools
let assistant_msg = Message {
role : Role ::Assistant ,
content : response . content . unwrap_or_default ( ) ,
tool_calls : Some ( tool_calls . clone ( ) ) ,
tool_call_id : None ,
} ;
current_history . push ( assistant_msg . clone ( ) ) ;
new_messages . push ( assistant_msg ) ;
2025-12-25 15:18:12 +00:00
// Emit history excluding system prompts (indices 0 and 1)
app . emit ( " chat:update " , & current_history [ 2 .. ] )
2025-12-25 12:39:20 +00:00
. map_err ( | e | e . to_string ( ) ) ? ;
2025-12-24 17:17:35 +00:00
// Execute Tools
for call in tool_calls {
let output = execute_tool ( & call , & state ) . await ;
let tool_msg = Message {
role : Role ::Tool ,
content : output ,
tool_calls : None ,
// For Ollama/Simple flow, we just append.
// For OpenAI strict, this needs to match call.id.
tool_call_id : call . id ,
} ;
current_history . push ( tool_msg . clone ( ) ) ;
new_messages . push ( tool_msg ) ;
2025-12-25 15:18:12 +00:00
// Emit history excluding system prompts (indices 0 and 1)
app . emit ( " chat:update " , & current_history [ 2 .. ] )
2025-12-25 12:39:20 +00:00
. map_err ( | e | e . to_string ( ) ) ? ;
2025-12-24 17:17:35 +00:00
}
} else {
// Final text response
let assistant_msg = Message {
role : Role ::Assistant ,
content : response . content . unwrap_or_default ( ) ,
tool_calls : None ,
tool_call_id : None ,
} ;
// We don't push to current_history needed for next loop, because we are done.
2025-12-25 12:39:20 +00:00
new_messages . push ( assistant_msg . clone ( ) ) ;
current_history . push ( assistant_msg ) ;
2025-12-25 15:18:12 +00:00
// Emit history excluding system prompts (indices 0 and 1)
app . emit ( " chat:update " , & current_history [ 2 .. ] )
2025-12-25 12:39:20 +00:00
. map_err ( | e | e . to_string ( ) ) ? ;
2025-12-24 17:17:35 +00:00
break ;
}
}
Ok ( new_messages )
}
async fn execute_tool ( call : & ToolCall , state : & State < '_ , SessionState > ) -> String {
let name = call . function . name . as_str ( ) ;
// Parse arguments. They come as a JSON string from the LLM abstraction.
let args : serde_json ::Value = match serde_json ::from_str ( & call . function . arguments ) {
Ok ( v ) = > v ,
Err ( e ) = > return format! ( " Error parsing arguments: {} " , e ) ,
} ;
match name {
" read_file " = > {
let path = args [ " path " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
match fs ::read_file ( path , state . clone ( ) ) . await {
Ok ( content ) = > content ,
Err ( e ) = > format! ( " Error: {} " , e ) ,
}
}
" write_file " = > {
let path = args [ " path " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
let content = args [ " content " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
match fs ::write_file ( path , content , state . clone ( ) ) . await {
Ok ( _ ) = > " File written successfully. " . to_string ( ) ,
Err ( e ) = > format! ( " Error: {} " , e ) ,
}
}
" list_directory " = > {
let path = args [ " path " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
match fs ::list_directory ( path , state . clone ( ) ) . await {
Ok ( entries ) = > serde_json ::to_string ( & entries ) . unwrap_or_default ( ) ,
Err ( e ) = > format! ( " Error: {} " , e ) ,
}
}
" search_files " = > {
let query = args [ " query " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
match search ::search_files ( query , state . clone ( ) ) . await {
Ok ( results ) = > serde_json ::to_string ( & results ) . unwrap_or_default ( ) ,
Err ( e ) = > format! ( " Error: {} " , e ) ,
}
}
" exec_shell " = > {
let command = args [ " command " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
let args_vec : Vec < String > = args [ " args " ]
. as_array ( )
. map ( | arr | {
arr . iter ( )
. map ( | v | v . as_str ( ) . unwrap_or ( " " ) . to_string ( ) )
. collect ( )
} )
. unwrap_or_default ( ) ;
match shell ::exec_shell ( command , args_vec , state . clone ( ) ) . await {
Ok ( output ) = > serde_json ::to_string ( & output ) . unwrap_or_default ( ) ,
Err ( e ) = > format! ( " Error: {} " , e ) ,
}
}
_ = > format! ( " Unknown tool: {} " , name ) ,
}
}
fn get_tool_definitions ( ) -> Vec < ToolDefinition > {
vec! [
ToolDefinition {
kind : " function " . to_string ( ) ,
function : ToolFunctionDefinition {
name : " read_file " . to_string ( ) ,
2025-12-25 15:18:12 +00:00
description : " Reads the complete content of a file from the project. Use this to understand existing code before making changes. " . to_string ( ) ,
2025-12-24 17:17:35 +00:00
parameters : json ! ( {
" type " : " object " ,
" properties " : {
2025-12-25 15:18:12 +00:00
" path " : { " type " : " string " , " description " : " Relative path to the file from project root " }
2025-12-24 17:17:35 +00:00
} ,
" required " : [ " path " ]
} ) ,
} ,
} ,
ToolDefinition {
kind : " function " . to_string ( ) ,
function : ToolFunctionDefinition {
name : " write_file " . to_string ( ) ,
2025-12-25 15:18:12 +00:00
description : " Creates or completely overwrites a file with new content. YOU MUST USE THIS to implement code changes - do not suggest code to the user. The content parameter must contain the COMPLETE file including all imports, functions, and unchanged code. " . to_string ( ) ,
2025-12-24 17:17:35 +00:00
parameters : json ! ( {
" type " : " object " ,
" properties " : {
2025-12-25 15:18:12 +00:00
" path " : { " type " : " string " , " description " : " Relative path to the file from project root " } ,
" content " : { " type " : " string " , " description " : " The complete file content to write (not a diff or partial code) " }
2025-12-24 17:17:35 +00:00
} ,
" required " : [ " path " , " content " ]
} ) ,
} ,
} ,
ToolDefinition {
kind : " function " . to_string ( ) ,
function : ToolFunctionDefinition {
name : " list_directory " . to_string ( ) ,
2025-12-25 15:18:12 +00:00
description : " Lists all files and directories at a given path. Use this to explore the project structure. " . to_string ( ) ,
2025-12-24 17:17:35 +00:00
parameters : json ! ( {
" type " : " object " ,
" properties " : {
2025-12-25 15:18:12 +00:00
" path " : { " type " : " string " , " description " : " Relative path to list (use '.' for project root) " }
2025-12-24 17:17:35 +00:00
} ,
" required " : [ " path " ]
} ) ,
} ,
} ,
ToolDefinition {
kind : " function " . to_string ( ) ,
function : ToolFunctionDefinition {
name : " search_files " . to_string ( ) ,
2025-12-25 15:18:12 +00:00
description : " Searches for text patterns across all files in the project. Use this to find functions, variables, or code patterns when you don't know which file they're in. "
2025-12-24 17:17:35 +00:00
. to_string ( ) ,
parameters : json ! ( {
" type " : " object " ,
" properties " : {
2025-12-25 15:18:12 +00:00
" query " : { " type " : " string " , " description " : " The text pattern to search for across all files " }
2025-12-24 17:17:35 +00:00
} ,
" required " : [ " query " ]
} ) ,
} ,
} ,
ToolDefinition {
kind : " function " . to_string ( ) ,
function : ToolFunctionDefinition {
name : " exec_shell " . to_string ( ) ,
2025-12-25 15:18:12 +00:00
description : " Executes a shell command in the project root directory. Use this to run tests, build commands, git operations, or any command-line tool. Examples: cargo check, npm test, git status. " . to_string ( ) ,
2025-12-24 17:17:35 +00:00
parameters : json ! ( {
" type " : " object " ,
" properties " : {
" command " : {
" type " : " string " ,
2025-12-25 15:18:12 +00:00
" description " : " The command binary to execute (e.g., 'git', 'cargo', 'npm', 'ls') "
2025-12-24 17:17:35 +00:00
} ,
" args " : {
" type " : " array " ,
" items " : { " type " : " string " } ,
2025-12-25 15:18:12 +00:00
" description " : " Array of arguments to pass to the command (e.g., ['status'] for git status) "
2025-12-24 17:17:35 +00:00
}
} ,
" required " : [ " command " , " args " ]
} ) ,
} ,
} ,
]
}