2026-07-11 20:21:59 +07:00
use std ::collections ::VecDeque ;
use crate ::app ::harness ::Verdict ;
use sha2 ::Digest ;
2026-07-11 13:16:10 +07:00
use crate ::app ::mode ::ModeKind ;
2026-07-11 20:21:59 +07:00
use crate ::app ::review ::{ should_trigger_review , trigger_review };
use crate ::app ::state ::rest ::{ AppStateRest , ChatMessageDisplay };
use crate ::app ::state ::runtime ::TurnEvent ;
2026-07-12 03:14:52 +07:00
use crate ::app ::state ::types ::{ Origin , Overlay , Toast , ToastKind };
2026-07-11 20:21:59 +07:00
use crate ::dto ::chat ::message ::{ ChatMessage , Role };
2026-07-12 03:56:43 +07:00
const MAX_TOOL_ONLY_TURNS : usize = 1000 ;
2026-07-11 22:40:45 +07:00
2026-07-12 03:56:43 +07:00
const MAX_AGENT_STEPS : usize = 1000 ;
2026-07-11 13:16:10 +07:00
#[derive(Debug, Clone)]
pub enum Action {
ForceQuit ,
SwitchMode ( ModeKind ),
SubmitInput ( String ),
InsertChar ( char ),
DeleteChar ,
DeleteCharRight ,
CursorLeft ,
CursorRight ,
HistoryUp ,
HistoryDown ,
ScrollUp ,
ScrollDown ,
OpenOverlay ( Overlay ),
CloseOverlay ,
SystemNote {
kind : String ,
message : String ,
},
QuitConfirm ,
Resize ( u16 , u16 ),
Tick ,
2026-07-11 18:23:01 +07:00
LessonExport {
path : String ,
},
LessonImport {
path : String ,
},
2026-07-11 21:06:22 +07:00
LessonAccept {
name : String ,
},
LessonReject {
name : String ,
},
2026-07-11 23:45:13 +07:00
StartOAuth {
provider : String ,
},
2026-07-12 01:25:52 +07:00
OpenEditor {
path : String ,
},
McpAdd {
name : String ,
command : String ,
},
2026-07-12 02:06:46 +07:00
ModelList ,
2026-07-12 03:14:52 +07:00
AbortTurn ,
2026-07-11 13:16:10 +07:00
}
pub fn apply_action ( state : & mut AppStateRest , action : Action ) {
match action {
Action ::ForceQuit => {
2026-07-11 20:21:59 +07:00
save_current_session ( state );
2026-07-11 13:16:10 +07:00
state . quit = true ;
}
Action ::SwitchMode ( mode ) => {
state . misc . overlay = match mode {
ModeKind ::Chat
| ModeKind ::Bash
| ModeKind ::Workflow => Overlay ::None ,
ModeKind ::Help => Overlay ::Help ,
ModeKind ::Settings => Overlay ::Settings ,
ModeKind ::QuitConfirm => Overlay ::QuitConfirm ,
2026-07-12 03:14:52 +07:00
2026-07-11 13:16:10 +07:00
ModeKind ::KeyInput => Overlay ::KeyInput ,
ModeKind ::Editor => Overlay ::Editor ,
ModeKind ::Effort => Overlay ::Effort ,
ModeKind ::Mcp => Overlay ::Mcp ,
ModeKind ::Todo => Overlay ::Todo ,
ModeKind ::Rewind => Overlay ::Rewind ,
ModeKind ::Loading => Overlay ::Loading ,
};
state . dirty = true ;
}
Action ::SubmitInput ( text ) => {
2026-07-11 20:21:59 +07:00
state . input . submit ();
let text = text . trim (). to_string ();
if text . is_empty () {
state . dirty = true ;
return ;
2026-07-11 13:16:10 +07:00
}
2026-07-11 20:21:59 +07:00
state . push_transcript ( ChatMessageDisplay ::new ( Role ::User , text . clone ()));
if let Some ( ref mut rt ) = state . session_runtime {
rt . push_message ( ChatMessage ::user ( text ));
}
2026-07-11 23:45:13 +07:00
state . misc . thinking = true ;
2026-07-11 20:21:59 +07:00
spawn_turn ( state );
2026-07-11 13:16:10 +07:00
state . dirty = true ;
}
Action ::InsertChar ( c ) => {
state . input . insert ( c );
state . dirty = true ;
}
Action ::DeleteChar => {
state . input . delete_left ();
state . dirty = true ;
}
Action ::DeleteCharRight => {
state . input . delete_right ();
state . dirty = true ;
}
Action ::CursorLeft => {
state . input . char_left ();
}
Action ::CursorRight => {
state . input . char_right ();
}
Action ::HistoryUp => {
state . input . history_up ();
state . dirty = true ;
}
Action ::HistoryDown => {
state . input . history_down ();
state . dirty = true ;
}
Action ::ScrollUp => {
2026-07-12 03:14:52 +07:00
state . scroll . scroll_up ( 5 );
2026-07-11 13:16:10 +07:00
state . dirty = true ;
}
Action ::ScrollDown => {
2026-07-12 03:14:52 +07:00
state . scroll . scroll_down ( 5 );
2026-07-11 13:16:10 +07:00
state . dirty = true ;
}
Action ::OpenOverlay ( overlay ) => {
state . misc . overlay = overlay ;
state . dirty = true ;
}
2026-07-12 01:25:52 +07:00
Action ::OpenEditor { path } => {
let resolved = crate ::tool ::resolve_path ( & state . workspace_roots , & path );
match resolved {
Ok ( abs_path ) => {
let content = std ::fs ::read_to_string ( & abs_path )
. unwrap_or_default ();
let lines : Vec < String > = content . lines (). map ( | l | l . to_string ()). collect ();
let ed = crate ::app ::mode ::editor ::EditorState ::open (
abs_path . to_string_lossy (). to_string (),
Some ( lines ),
);
state . misc . editor = Some ( ed );
state . misc . overlay = Overlay ::Editor ;
state . push_toast ( Toast ::new ( ToastKind ::Info , format! ( "Editing {} " , path )));
}
Err ( e ) => {
state . push_toast ( Toast ::new ( ToastKind ::Error , format! ( "Failed to open {} : {} " , path , e )));
}
}
state . dirty = true ;
}
Action ::McpAdd { name , command } => {
let extra_args : Vec < String > = command . split_whitespace (). map ( | s | s . to_string ()). collect ();
let cmd = extra_args . first (). cloned (). unwrap_or_default ();
let args : Vec < String > = extra_args . into_iter (). skip ( 1 ). collect ();
match state . mcp_manager . connect_stdio ( & name , & cmd , & args ) {
Ok ( _ ) => {
let tool_count = state . mcp_manager . servers . last ()
. map ( | s | s . tools . len ())
. unwrap_or ( 0 );
state . push_toast ( Toast ::new ( ToastKind ::Success ,
format! ( "Connected MCP server ' {} ' ( {} tools)" , name , tool_count )));
state . dirty = true ;
}
Err ( e ) => {
state . push_toast ( Toast ::new ( ToastKind ::Error ,
format! ( "MCP connect failed: {} " , e )));
}
}
}
2026-07-12 02:06:46 +07:00
Action ::ModelList => {
state . misc . selected_index = 0 ;
state . misc . overlay = Overlay ::ModelSelector ;
state . dirty = true ;
}
2026-07-11 13:16:10 +07:00
Action ::CloseOverlay => {
2026-07-12 01:25:52 +07:00
// If the overlay is the Editor, dismiss it properly first
if state . misc . overlay == Overlay ::Editor {
crate ::app ::mode ::editor ::handle_editor_dismiss ( state );
}
2026-07-11 13:16:10 +07:00
state . misc . overlay = Overlay ::None ;
state . dirty = true ;
}
Action ::SystemNote { kind : _kind , message } => {
let toast = crate ::app ::state ::types ::Toast ::new (
crate ::app ::state ::types ::ToastKind ::Info ,
message ,
);
state . push_toast ( toast );
}
Action ::QuitConfirm => {
state . misc . overlay = Overlay ::QuitConfirm ;
state . dirty = true ;
}
Action ::Resize ( w , _h ) => {
state . scroll . set_max_visible ( w as usize );
state . dirty = true ;
}
2026-07-11 18:23:01 +07:00
Action ::LessonExport { path } => {
let dest = std ::path ::Path ::new ( & path );
if let Some ( parent ) = dest . parent () {
let _ = std ::fs ::create_dir_all ( parent );
}
match crate ::model ::memory ::export_lessons ( & state . memory_dir , dest ) {
Ok ( _ ) => {
state . push_toast ( crate ::app ::state ::types ::Toast ::new (
crate ::app ::state ::types ::ToastKind ::Success ,
format! ( "lessons exported to {} " , path ),
));
}
Err ( e ) => {
state . push_toast ( crate ::app ::state ::types ::Toast ::new (
crate ::app ::state ::types ::ToastKind ::Error ,
format! ( "export failed: {} " , e ),
));
}
}
state . dirty = true ;
}
Action ::LessonImport { path } => {
let src = std ::path ::Path ::new ( & path );
match crate ::model ::memory ::import_lessons ( & state . memory_dir , src ) {
Ok ( count ) => {
state . push_toast ( crate ::app ::state ::types ::Toast ::new (
crate ::app ::state ::types ::ToastKind ::Success ,
format! ( "imported {} lessons from {} " , count , path ),
));
}
Err ( e ) => {
state . push_toast ( crate ::app ::state ::types ::Toast ::new (
crate ::app ::state ::types ::ToastKind ::Error ,
format! ( "import failed: {} " , e ),
));
}
}
state . dirty = true ;
}
2026-07-11 23:45:13 +07:00
Action ::StartOAuth { provider } => {
let turn_events = state . turn_events . clone ();
let provider_clone = provider . clone ();
std ::thread ::spawn ( move || {
let result = run_oauth_flow ( & provider_clone );
let message = match result {
Ok ( msg ) => msg ,
Err ( e ) => format! ( "OAuth login failed: {} " , e ),
};
if let Ok ( mut q ) = turn_events . lock () {
q . push_back ( TurnEvent ::SystemNote {
kind : "oauth" . to_string (),
message ,
});
}
});
let toast = Toast ::new ( ToastKind ::Info , format! ( "Opening browser for {} login..." , provider ));
state . push_toast ( toast );
state . dirty = true ;
}
2026-07-11 13:16:10 +07:00
Action ::Tick => {
2026-07-12 03:19:25 +07:00
state . misc . tick_count = state . misc . tick_count . wrapping_add ( 1 );
2026-07-11 13:16:10 +07:00
let now_ms = chrono ::Utc ::now (). timestamp_millis ();
state . misc . drain_expired_toasts ( now_ms );
2026-07-11 21:06:22 +07:00
crate ::app ::review ::maybe_run_staleness_sweep ( state );
if let Some ( ref rt ) = state . session_runtime {
let _ = crate ::app ::review ::process_pending_lessons ( & rt . session_dir , & state . memory_dir );
}
let events : Vec < TurnEvent > = {
2026-07-11 20:21:59 +07:00
if let Ok ( mut q ) = state . turn_events . lock () {
q . drain ( .. ). collect ()
} else {
Vec ::new ()
}
2026-07-11 13:16:10 +07:00
};
2026-07-11 20:21:59 +07:00
let mut turn_finished = false ;
for event in events {
match event {
TurnEvent ::AssistantMessage ( msg ) => {
2026-07-11 23:45:13 +07:00
state . misc . thinking = false ;
2026-07-12 02:26:53 +07:00
state . misc . api_connected = true ;
2026-07-11 20:21:59 +07:00
let display_content = msg . content . clone (). unwrap_or_default ();
if ! display_content . is_empty () {
2026-07-11 23:45:13 +07:00
state . push_transcript ( ChatMessageDisplay ::new ( Role ::Assistant , display_content ));
2026-07-11 20:21:59 +07:00
}
if let Some ( ref mut rt ) = state . session_runtime {
rt . push_message ( msg );
}
}
2026-07-11 22:40:45 +07:00
TurnEvent ::ToolResult { tool_call_id , tool_name , output , is_error , path } => {
2026-07-11 23:45:13 +07:00
state . misc . thinking = false ;
2026-07-11 22:40:45 +07:00
let display_path = path . unwrap_or_default ();
let display = if tool_name == "read" {
let line_count = output . lines (). count ();
if ! display_path . is_empty () {
format! ( "read: {} ( {} lines)" , display_path , line_count )
} else {
format! ( "read: {} line(s)" , line_count )
}
} else {
format! ( " {} : {} " , tool_name , output )
};
2026-07-11 20:21:59 +07:00
state . push_transcript ( ChatMessageDisplay ::new (
Role ::Tool ,
2026-07-11 22:40:45 +07:00
display ,
2026-07-11 20:21:59 +07:00
));
if let Some ( ref mut rt ) = state . session_runtime {
rt . push_message ( ChatMessage ::tool_result ( tool_call_id . clone (), output . clone ()));
rt . tool_call_results . push ( crate ::app ::state ::runtime ::ToolCallResult {
tool_call_id ,
tool_name ,
output ,
is_error ,
duration_ms : 0 ,
});
}
}
TurnEvent ::SystemNote { kind , message } => {
if kind == "edits" {
if let Some ( ref mut rt ) = state . session_runtime {
if let Ok ( count ) = message . parse ::< u32 > () {
rt . edit_count += count ;
}
}
if should_trigger_review ( state , Origin ::Main ) {
let _ = trigger_review ( state );
}
2026-07-11 21:06:22 +07:00
} else if kind == "review" {
let lessons_found = if message . contains ( "lesson" ) || message . contains ( "Lesson" ) {
message . rsplit ( ' ' ). next (). and_then ( | w | {
w . trim_end_matches ( ')' ). trim_end_matches ( 's' )
. split ( '(' ). next_back ()
. and_then ( | n | n . parse ::< u32 > (). ok ())
}). unwrap_or ( 0 )
} else {
0
};
if let Some ( ref mut rt ) = state . session_runtime {
if lessons_found > 0 {
rt . consecutive_empty_reviews = 0 ;
rt . lesson_count += lessons_found ;
} else {
rt . consecutive_empty_reviews += 1 ;
}
}
state . push_toast ( Toast ::new ( ToastKind ::Info , message ));
2026-07-12 03:56:43 +07:00
} else if kind == "task_retry" {
state . push_transcript ( ChatMessageDisplay ::new (
crate ::dto ::chat ::message ::Role ::System ,
message . clone (),
));
state . push_toast ( Toast ::new ( ToastKind ::Info , "Auto-continuing unfinished tasks..." . to_string ()));
if let Some ( ref mut rt ) = state . session_runtime {
rt . push_message ( crate ::dto ::chat ::message ::ChatMessage ::system ( message . clone ()));
}
2026-07-11 21:06:22 +07:00
} else {
state . push_toast ( Toast ::new ( ToastKind ::Info , message ));
2026-07-11 20:21:59 +07:00
}
}
2026-07-12 01:25:52 +07:00
TurnEvent ::StreamStart => {
state . misc . thinking = false ;
2026-07-12 02:26:53 +07:00
state . misc . api_connected = true ;
2026-07-12 01:25:52 +07:00
state . push_transcript ( ChatMessageDisplay ::new ( Role ::Assistant , String ::new ()));
}
TurnEvent ::StreamToken ( delta ) => {
if let Some ( last ) = state . transcript_cache . messages . last_mut () {
if last . role == Role ::Assistant {
last . content . push_str ( & delta );
state . transcript_cache . dirty = true ;
}
}
}
TurnEvent ::StreamDone ( msg ) => {
state . misc . thinking = false ;
if let Some ( ref mut rt ) = state . session_runtime {
rt . push_message ( msg );
}
}
TurnEvent ::Usage { tokens_in , tokens_out } => {
if let Some ( ref mut rt ) = state . session_runtime {
rt . usage . tokens_in += tokens_in ;
rt . usage . tokens_out += tokens_out ;
rt . usage . api_calls += 1 ;
}
}
2026-07-11 20:21:59 +07:00
TurnEvent ::Error ( msg ) => {
2026-07-12 02:26:53 +07:00
state . misc . api_connected = false ;
2026-07-11 22:10:17 +07:00
let long_toast = Toast {
kind : ToastKind ::Error ,
message : msg . clone (),
created_at : chrono ::Utc ::now (). timestamp_millis (),
lifetime_ms : 15000 ,
};
state . push_toast ( long_toast );
state . push_transcript ( ChatMessageDisplay ::new (
crate ::dto ::chat ::message ::Role ::System ,
format! ( "Error: {} " , msg ),
));
2026-07-11 20:21:59 +07:00
turn_finished = true ;
}
TurnEvent ::Done => {
2026-07-11 23:45:13 +07:00
state . misc . thinking = false ;
2026-07-11 20:21:59 +07:00
turn_finished = true ;
2026-07-11 13:16:10 +07:00
}
}
2026-07-11 20:21:59 +07:00
}
if turn_finished {
maybe_trigger_review ( state );
}
if turn_finished || state . dirty {
2026-07-11 13:16:10 +07:00
state . dirty = true ;
}
}
2026-07-12 03:14:52 +07:00
Action ::AbortTurn => {
state . abort_flag . store ( true , std ::sync ::atomic ::Ordering ::SeqCst );
state . push_toast ( Toast ::new ( ToastKind ::Warning , "Aborting generation..." . to_string ()));
}
2026-07-11 21:06:22 +07:00
Action ::LessonAccept { name } => {
if let Some ( ref rt ) = state . session_runtime {
let _ = crate ::app ::review ::resolve_pending_lesson (
& rt . session_dir , & state . memory_dir , & name , true ,
);
}
state . push_toast ( Toast ::new ( ToastKind ::Success ,
format! ( "accepted lesson: {} " , name )));
state . dirty = true ;
}
Action ::LessonReject { name } => {
if let Some ( ref rt ) = state . session_runtime {
let _ = crate ::app ::review ::resolve_pending_lesson (
& rt . session_dir , & state . memory_dir , & name , false ,
);
}
state . push_toast ( Toast ::new ( ToastKind ::Info ,
format! ( "rejected lesson: {} " , name )));
state . dirty = true ;
}
2026-07-11 13:16:10 +07:00
}
}
2026-07-11 20:21:59 +07:00
fn spawn_turn ( state : & AppStateRest ) {
let in_flight = if let Ok ( guard ) = state . turn_in_flight . lock () {
* guard
} else {
return ;
};
if in_flight {
return ;
}
let messages = state
. session_runtime
. as_ref ()
. map ( | rt | rt . messages . clone ())
. unwrap_or_default ();
if messages . is_empty () {
return ;
}
2026-07-12 03:14:52 +07:00
let mut api_key = state . settings . api_keys . get ( & state . settings . provider ). cloned (). unwrap_or_default ();
2026-07-11 20:21:59 +07:00
let model = state . settings . model . clone ();
2026-07-12 01:25:52 +07:00
let base_url = state . app_config . providers . get ( & state . settings . provider )
. map ( | p | p . api_base . clone ());
2026-07-12 02:19:08 +07:00
if api_key . is_empty () {
if let Some ( provider_cfg ) = state . app_config . providers . get ( & state . settings . provider ) {
api_key = provider_cfg . api_key_env . as_ref ()
. and_then ( | env | std ::env ::var ( env ). ok ())
. or_else ( || provider_cfg . default_api_key . clone ())
. unwrap_or_default ();
}
}
2026-07-12 02:26:53 +07:00
if api_key . is_empty () {
api_key = crate ::service ::provider ::DEFAULT_API_KEY . to_string ();
}
2026-07-12 01:25:52 +07:00
let ( temperature , max_tokens ) = crate ::app ::mode ::effort ::generation_params (
state . misc . effort_level ,
state . settings . max_tokens ,
);
2026-07-11 20:21:59 +07:00
let mut tools = crate ::tool ::all_tools ();
tools . extend ( state . mcp_manager . as_tools ());
let tool_defs = crate ::tool ::tool_defs ( & tools );
let ctx = state . tool_ctx ();
2026-07-12 03:14:52 +07:00
2026-07-11 22:10:17 +07:00
let edit_session_dir = state . session_dir . clone ();
2026-07-11 20:21:59 +07:00
let session_id = state . session_id . clone ();
let turn_events = state . turn_events . clone ();
let in_flight_flag = state . turn_in_flight . clone ();
let workspace_roots : Vec < std ::path ::PathBuf > = ctx . workspaces . clone ();
2026-07-12 03:14:52 +07:00
let abort_flag = state . abort_flag . clone ();
abort_flag . store ( false , std ::sync ::atomic ::Ordering ::SeqCst );
2026-07-11 20:21:59 +07:00
* in_flight_flag . lock (). unwrap () = true ;
let events_q = turn_events . clone ();
std ::thread ::spawn ( move || {
2026-07-11 23:45:13 +07:00
let db = crate ::model ::msglog ::open_or_create ( & edit_session_dir )
. ok ()
. map ( | c | std ::sync ::Arc ::new ( std ::sync ::Mutex ::new ( c )));
2026-07-11 20:21:59 +07:00
let tc = TurnCtx {
2026-07-12 01:25:52 +07:00
client : crate ::service ::provider ::LlmClient ::new ( api_key , model , base_url ),
2026-07-11 20:21:59 +07:00
tdefs : tool_defs ,
tools ,
ctx ,
2026-07-12 03:14:52 +07:00
2026-07-11 20:21:59 +07:00
workspace_roots ,
2026-07-11 22:10:17 +07:00
edit_log_session_dir : edit_session_dir ,
2026-07-11 20:21:59 +07:00
session_id ,
2026-07-11 23:45:13 +07:00
db ,
2026-07-12 01:25:52 +07:00
temperature ,
max_tokens ,
2026-07-12 03:14:52 +07:00
abort_flag ,
2026-07-11 20:21:59 +07:00
};
let result = run_agent_turn ( tc , & messages , & events_q );
if let Err ( e ) = result {
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::Error ( e . to_string ()));
}
}
if let Ok ( mut flag ) = in_flight_flag . lock () {
* flag = false ;
}
});
}
struct TurnCtx {
2026-07-11 22:10:17 +07:00
client : crate ::service ::provider ::LlmClient ,
tdefs : Vec < crate ::dto ::provider ::request ::ToolDef > ,
2026-07-11 20:21:59 +07:00
tools : Vec < Box < dyn crate ::tool ::Tool >> ,
ctx : crate ::tool ::ToolCtx ,
2026-07-12 03:14:52 +07:00
2026-07-11 20:21:59 +07:00
workspace_roots : Vec < std ::path ::PathBuf > ,
2026-07-11 22:10:17 +07:00
edit_log_session_dir : std ::path ::PathBuf ,
2026-07-11 20:21:59 +07:00
session_id : String ,
2026-07-11 23:45:13 +07:00
db : Option < std ::sync ::Arc < std ::sync ::Mutex < rusqlite ::Connection >>> ,
2026-07-12 01:25:52 +07:00
temperature : f32 ,
max_tokens : u32 ,
2026-07-12 03:14:52 +07:00
abort_flag : std ::sync ::Arc < std ::sync ::atomic ::AtomicBool > ,
2026-07-11 23:45:13 +07:00
}
2026-07-12 03:28:06 +07:00
fn generate_workspace_tree ( roots : & [ std ::path ::PathBuf ]) -> String {
let mut out = String ::new ();
out . push_str ( "Current Workspace Directory Structure: \n " );
for root in roots {
out . push_str ( & format! ( "Root: {} \n " , root . display ()));
let walker = ignore ::WalkBuilder ::new ( root )
. hidden ( true )
. git_ignore ( true )
. build ();
let mut count = 0 ;
for result in walker {
if let Ok ( entry ) = result {
let path = entry . path ();
if let Ok ( rel ) = path . strip_prefix ( root ) {
if rel . as_os_str (). is_empty () { continue ; }
let is_dir = entry . file_type (). map ( | ft | ft . is_dir ()). unwrap_or ( false );
let prefix = if is_dir { "[DIR] " } else { " " };
out . push_str ( & format! ( " {}{} \n " , prefix , rel . display ()));
count += 1 ;
if count > 1000 {
out . push_str ( " ... (truncated) \n " );
break ;
}
}
}
}
}
out
}
2026-07-11 23:45:13 +07:00
fn archive_message ( db : & Option < std ::sync ::Arc < std ::sync ::Mutex < rusqlite ::Connection >>> , session_id : & str , msg : & ChatMessage ) {
if let Some ( ref arc ) = db {
if let Ok ( conn ) = arc . lock () {
let _ = crate ::model ::msglog ::insert_message ( & conn , session_id , msg );
}
}
2026-07-11 20:21:59 +07:00
}
fn run_agent_turn (
tc : TurnCtx ,
messages : & [ ChatMessage ],
events_q : & std ::sync ::Mutex < VecDeque < TurnEvent >> ,
) -> anyhow ::Result < () > {
let mut msgs = messages . to_vec ();
let mut edits_this_turn = 0 u32 ;
2026-07-11 22:40:45 +07:00
let mut tool_only_rounds = 0 usize ;
2026-07-11 23:45:13 +07:00
let mut prev_shaped = false ;
2026-07-11 20:21:59 +07:00
2026-07-12 03:28:06 +07:00
let tree_info = generate_workspace_tree ( & tc . workspace_roots );
2026-07-11 22:16:47 +07:00
let system_text = format! (
2026-07-12 03:28:06 +07:00
" {} \n\n {} \n\n {} " ,
2026-07-11 22:16:47 +07:00
crate ::resources ::SYSTEM_PROMPT ,
crate ::resources ::SYSTEM_TOOLS ,
2026-07-12 03:28:06 +07:00
tree_info
2026-07-11 22:16:47 +07:00
);
if ! msgs . iter (). any ( | m | matches! ( m . role , crate ::dto ::chat ::message ::Role ::System )) {
2026-07-11 23:45:13 +07:00
let sys = ChatMessage ::system ( system_text );
archive_message ( & tc . db , & tc . session_id , & sys );
msgs . insert ( 0 , sys );
2026-07-11 22:16:47 +07:00
}
2026-07-11 20:21:59 +07:00
for _step in 0 .. MAX_AGENT_STEPS {
2026-07-11 22:40:45 +07:00
if tool_only_rounds >= MAX_TOOL_ONLY_TURNS {
2026-07-11 23:45:13 +07:00
let stop_msg = ChatMessage ::user (
2026-07-11 22:40:45 +07:00
"Stop calling tools. Respond naturally now." . to_string (),
2026-07-11 23:45:13 +07:00
);
archive_message ( & tc . db , & tc . session_id , & stop_msg );
msgs . push ( stop_msg );
2026-07-11 22:40:45 +07:00
tool_only_rounds = 0 ;
}
2026-07-11 23:45:13 +07:00
let wire_msgs = if crate ::app ::runtime ::shortsend ::should_shape ( msgs . len (), prev_shaped ) {
let total_chars : usize = msgs . iter ()
. filter_map ( | m | m . content . as_deref ())
. map ( | c | c . len ())
. sum ();
let token_estimate = total_chars / 4 ;
prev_shaped = true ;
crate ::app ::runtime ::shortsend ::shape_messages ( & msgs , token_estimate )
} else {
prev_shaped = false ;
msgs . clone ()
};
2026-07-12 01:25:52 +07:00
let mut stream_started = false ;
2026-07-12 03:37:27 +07:00
let mut reasoning_started = false ;
let mut reasoning_ended = false ;
2026-07-12 03:14:52 +07:00
let mut usage = None ;
let result = tc . client . chat_with_tools_streaming (
2026-07-12 01:25:52 +07:00
& wire_msgs ,
Some ( tc . tdefs . clone ()),
Some ( tc . temperature ),
Some ( tc . max_tokens ),
2026-07-12 03:14:52 +07:00
| event | -> bool {
if tc . abort_flag . load ( std ::sync ::atomic ::Ordering ::SeqCst ) {
return false ;
}
if let Ok ( mut q ) = events_q . lock () {
match event {
crate ::app ::runtime ::stream ::StreamEvent ::Token ( tok ) => {
if ! stream_started {
q . push_back ( TurnEvent ::StreamStart );
stream_started = true ;
}
2026-07-12 03:37:27 +07:00
if reasoning_started && ! reasoning_ended {
reasoning_ended = true ;
q . push_back ( TurnEvent ::StreamToken ( " \n </think> \n\n " . to_string ()));
}
q . push_back ( TurnEvent ::StreamToken ( tok . clone ()));
}
crate ::app ::runtime ::stream ::StreamEvent ::Reasoning ( tok ) => {
if ! stream_started {
q . push_back ( TurnEvent ::StreamStart );
stream_started = true ;
}
if ! reasoning_started {
reasoning_started = true ;
q . push_back ( TurnEvent ::StreamToken ( "<think> \n " . to_string ()));
}
2026-07-12 03:14:52 +07:00
q . push_back ( TurnEvent ::StreamToken ( tok . clone ()));
2026-07-12 01:25:52 +07:00
}
2026-07-12 03:14:52 +07:00
crate ::app ::runtime ::stream ::StreamEvent ::Usage { prompt_tokens , completion_tokens , .. } => {
usage = Some (( * prompt_tokens , * completion_tokens ));
}
_ => {}
2026-07-12 01:25:52 +07:00
}
}
2026-07-12 03:14:52 +07:00
true
2026-07-12 01:25:52 +07:00
},
2026-07-12 03:14:52 +07:00
);
2026-07-12 03:37:27 +07:00
if reasoning_started && ! reasoning_ended {
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::StreamToken ( " \n </think> \n\n " . to_string ()));
}
}
2026-07-12 03:14:52 +07:00
let ( response , final_usage ) = match result {
Ok (( msg , u )) => ( msg , u . or ( usage )),
Err ( e ) => {
if tc . abort_flag . load ( std ::sync ::atomic ::Ordering ::SeqCst ) || e . to_string (). contains ( "aborted" ) {
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::Error ( "Generation aborted by user" . to_string ()));
}
return Ok (());
}
2026-07-12 03:56:43 +07:00
match tc . client . chat_with_tools_non_streaming ( & wire_msgs , Some ( tc . tdefs . clone ())) {
Ok (( msg , usage_fb )) => ( msg , usage_fb ),
Err ( api_err ) => {
let todo_path = tc . ctx . session_dir . join ( "todo.md" );
let mut has_unfinished = false ;
if let Ok ( todo_text ) = std ::fs ::read_to_string ( & todo_path ) {
if todo_text . lines (). any ( | l | l . trim_start (). starts_with ( "- [ ]" )) {
has_unfinished = true ;
}
}
if has_unfinished {
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::SystemNote {
kind : "task_retry" . to_string (),
message : format ! ( "Network/API error: {}. Auto-retrying to finish tasks..." , api_err ),
});
}
std ::thread ::sleep ( std ::time ::Duration ::from_secs ( 5 ));
continue ;
}
return Err ( api_err );
}
}
2026-07-12 01:43:57 +07:00
}
};
2026-07-11 20:21:59 +07:00
2026-07-12 03:14:52 +07:00
if let Some (( tok_in , tok_out )) = final_usage {
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::Usage { tokens_in : tok_in , tokens_out : tok_out });
}
}
2026-07-11 20:21:59 +07:00
let has_tool_calls = response . tool_calls . is_some ()
&& response . tool_calls . as_ref (). is_some_and ( | tc | ! tc . is_empty ());
let content = response . content . clone (). unwrap_or_default ();
if has_tool_calls {
2026-07-11 22:40:45 +07:00
tool_only_rounds += 1 ;
2026-07-11 20:21:59 +07:00
let tool_calls = response . tool_calls . clone (). unwrap_or_default ();
2026-07-11 23:45:13 +07:00
archive_message ( & tc . db , & tc . session_id , & response );
2026-07-11 20:21:59 +07:00
msgs . push ( response );
for tool_call in tool_calls {
2026-07-12 03:14:52 +07:00
if tc . abort_flag . load ( std ::sync ::atomic ::Ordering ::SeqCst ) {
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::Error ( "Turn aborted by user" . to_string ()));
}
return Ok (());
}
2026-07-11 20:21:59 +07:00
let tool_name = tool_call . function . name . clone ();
let args = crate ::dto ::chat ::tool ::sanitize_tool_arguments (
& tool_call . function . arguments ,
);
let ws_roots : Vec <& std ::path ::Path > =
tc . workspace_roots . iter (). map ( | p | p . as_path ()). collect ();
let verdict = crate ::app ::harness ::Harness ::gate_tool_call (
& tool_name ,
& args ,
2026-07-12 03:14:52 +07:00
2026-07-11 20:21:59 +07:00
& ws_roots ,
);
let is_edit_tool = tool_name == "write" || tool_name == "edit" ;
let ( output , is_error , is_edit ) = match verdict {
Verdict ::Allow => match execute_one_tool (
& tc . tools ,
& tc . ctx ,
& tool_name ,
2026-07-12 01:25:52 +07:00
& tool_call . id ,
2026-07-11 20:21:59 +07:00
& args ,
2026-07-11 22:10:17 +07:00
& tc . edit_log_session_dir ,
2026-07-11 20:21:59 +07:00
& tc . session_id ,
2026-07-12 01:25:52 +07:00
& tc . db ,
2026-07-11 20:21:59 +07:00
) {
Ok ( result ) => ( result , false , is_edit_tool ),
Err ( e ) => ( e . to_string (), true , false ),
},
Verdict ::Block ( reason ) => ( format! ( "Blocked: {} " , reason ), true , false ),
};
if is_edit {
edits_this_turn += 1 ;
}
2026-07-11 22:40:45 +07:00
let tool_path = args . get ( "path" ). and_then ( | v | v . as_str ()). map ( | s | s . to_string ());
2026-07-11 20:21:59 +07:00
{
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::ToolResult {
tool_call_id : tool_call . id . clone (),
tool_name : tool_name . clone (),
output : output . clone (),
is_error ,
2026-07-11 22:40:45 +07:00
path : tool_path ,
2026-07-11 20:21:59 +07:00
});
}
}
let tool_msg = ChatMessage ::tool_result ( tool_call . id . clone (), output );
2026-07-11 23:45:13 +07:00
archive_message ( & tc . db , & tc . session_id , & tool_msg );
2026-07-11 20:21:59 +07:00
msgs . push ( tool_msg );
}
} else {
if ! content . is_empty () {
2026-07-11 23:45:13 +07:00
archive_message ( & tc . db , & tc . session_id , & response );
2026-07-11 20:21:59 +07:00
if let Ok ( mut q ) = events_q . lock () {
2026-07-12 01:25:52 +07:00
if stream_started {
2026-07-12 03:56:43 +07:00
q . push_back ( TurnEvent ::StreamDone ( response . clone ()));
2026-07-12 01:25:52 +07:00
} else {
2026-07-12 03:56:43 +07:00
q . push_back ( TurnEvent ::AssistantMessage ( response . clone ()));
2026-07-12 01:25:52 +07:00
}
2026-07-11 20:21:59 +07:00
}
}
2026-07-12 03:56:43 +07:00
let todo_path = tc . ctx . session_dir . join ( "todo.md" );
let mut has_unfinished = false ;
if let Ok ( todo_text ) = std ::fs ::read_to_string ( & todo_path ) {
if todo_text . lines (). any ( | l | l . trim_start (). starts_with ( "- [ ]" )) {
has_unfinished = true ;
}
}
if has_unfinished {
let sys_text = "You stopped, but you still have unfinished tasks in todo.md (marked with '- [ ]'). You MUST continue working and use tools to finish them, or edit todo.md to mark them as done if they are finished." ;
let msg = ChatMessage ::system ( sys_text );
archive_message ( & tc . db , & tc . session_id , & msg );
msgs . push ( msg );
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::SystemNote {
kind : "task_retry" . to_string (),
message : sys_text . to_string (),
});
}
continue ;
}
2026-07-11 20:21:59 +07:00
break ;
}
}
if edits_this_turn > 0 {
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::SystemNote {
kind : "edits" . to_string (),
message : edits_this_turn . to_string (),
});
}
}
if let Ok ( mut q ) = events_q . lock () {
q . push_back ( TurnEvent ::Done );
}
Ok (())
}
2026-07-12 01:25:52 +07:00
#[allow(clippy::too_many_arguments)]
2026-07-11 20:21:59 +07:00
fn execute_one_tool (
tools : & [ Box < dyn crate ::tool ::Tool > ],
ctx : & crate ::tool ::ToolCtx ,
name : & str ,
2026-07-12 01:25:52 +07:00
tool_call_id : & str ,
2026-07-11 20:21:59 +07:00
args : & serde_json ::Value ,
2026-07-11 22:10:17 +07:00
session_dir : & std ::path ::Path ,
2026-07-11 20:21:59 +07:00
session_id : & str ,
2026-07-12 01:25:52 +07:00
db : & Option < std ::sync ::Arc < std ::sync ::Mutex < rusqlite ::Connection >>> ,
2026-07-11 20:21:59 +07:00
) -> anyhow ::Result < String > {
for tool in tools {
if tool . name () == name {
2026-07-12 01:25:52 +07:00
// Snapshot current file content before write/edit for rewind
if ( name == "write" || name == "edit" ) && ! tool_call_id . is_empty () {
if let Some ( ref arc ) = db {
if let Ok ( conn ) = arc . lock () {
let path = args . get ( "path" ). and_then ( | v | v . as_str ()). unwrap_or ( "" );
if let Ok ( abs_path ) = crate ::tool ::resolve_path ( & ctx . workspaces , path ) {
if let Ok ( bytes ) = std ::fs ::read ( & abs_path ) {
let _ = crate ::model ::msglog ::store_blob (
& conn , session_id , tool_call_id , & bytes , None ,
);
}
}
}
}
}
2026-07-11 20:21:59 +07:00
let result = tool . run ( ctx , args ) ? ;
if name == "write" || name == "edit" {
let reason = args
. get ( "reason" )
. and_then ( | v | v . as_str ())
. unwrap_or ( "unnamed" );
let path = args
. get ( "path" )
. and_then ( | v | v . as_str ())
. unwrap_or ( "unknown" );
let content_sha256 = {
let content = args . get ( "content" ). or_else ( || args . get ( "new" ));
let hash = sha2 ::Sha256 ::digest (
content . and_then ( | v | v . as_str ()). unwrap_or ( "" ). as_bytes (),
);
format! ( " {:x} " , hash )
};
2026-07-11 22:10:17 +07:00
let bytes_delta = if name == "write" {
args . get ( "content" )
. and_then ( | v | v . as_str ())
. map ( | s | s . len () as i64 )
. unwrap_or ( 0 )
} else {
let old = args . get ( "old" ). and_then ( | v | v . as_str ()). unwrap_or ( "" );
let new = args . get ( "new" ). and_then ( | v | v . as_str ()). unwrap_or ( "" );
( new . len () as i64 - old . len () as i64 ). abs ()
};
2026-07-11 20:21:59 +07:00
let entry = crate ::model ::editlog ::EditLogEntry {
ts : chrono ::Utc ::now (). timestamp_millis (),
tool : name . to_string (),
path : path . to_string (),
reason : reason . to_string (),
content_sha256 ,
2026-07-11 22:10:17 +07:00
bytes_delta ,
2026-07-11 20:21:59 +07:00
origin : ctx . origin . tag (),
session_id : session_id . to_string (),
};
2026-07-11 22:10:17 +07:00
let mut el = crate ::model ::editlog ::EditLog ::new ( session_dir );
2026-07-11 20:21:59 +07:00
el . append ( entry ). ok ();
}
return Ok ( result );
}
}
anyhow ::bail! ( "tool not found: {}" , name )
}
fn maybe_trigger_review ( state : & mut AppStateRest ) {
if ! state . settings . review_enabled {
return ;
}
let edit_count = state
. session_runtime
. as_ref ()
. map ( | rt | rt . edit_count )
. unwrap_or ( 0 );
if edit_count == 0 {
return ;
}
state . push_toast ( Toast ::new (
ToastKind ::Info ,
format! ( " {} file(s) modified this turn. Review available." , edit_count ),
));
}
fn save_current_session ( state : & AppStateRest ) {
let base = state . store_base_dir ();
let session = crate ::model ::session ::Session ::new (
state . session_id . clone (),
"session" . to_string (),
);
let _ = session . save ( & base );
if let Some ( ref rt ) = state . session_runtime {
let conv_path = session . conversation_path ( & base );
if let Ok ( data ) = serde_json ::to_string ( & rt . messages ) {
let _ = std ::fs ::write ( & conv_path , data );
}
}
}
2026-07-11 21:06:22 +07:00
2026-07-11 23:45:13 +07:00
fn run_oauth_flow ( provider : & str ) -> anyhow ::Result < String > {
use crate ::service ::oauth ::manager ::{ OAuthConfig , OAuthManager };
use crate ::service ::oauth ::loopback ::LoopbackServer ;
use crate ::service ::oauth ::pkce ::CodeVerifier ;
let config = match provider {
"zen" | "opencode" => OAuthConfig {
auth_url : "https://opencode.ai/zen/oauth/authorize" . to_string (),
token_url : "https://opencode.ai/zen/oauth/token" . to_string (),
client_id : std ::env ::var ( "ZEN_CLIENT_ID" )
. unwrap_or_else ( | _ | "zesdex" . to_string ()),
client_secret : std ::env ::var ( "ZEN_CLIENT_SECRET" ). ok (),
scopes : vec ! [ "openid" . to_string (), "profile" . to_string (), "email" . to_string ()],
},
"openai" => OAuthConfig {
auth_url : "https://auth0.openai.com/authorize" . to_string (),
token_url : "https://auth0.openai.com/oauth/token" . to_string (),
client_id : std ::env ::var ( "OPENAI_CLIENT_ID" )
. unwrap_or_else ( | _ | "zesdex" . to_string ()),
client_secret : std ::env ::var ( "OPENAI_CLIENT_SECRET" ). ok (),
scopes : vec ! [ "openid" . to_string (), "profile" . to_string (), "email" . to_string ()],
},
other => {
let auth_url = std ::env ::var ( format! ( " {} _AUTH_URL" , other . to_uppercase ()))
. map_err ( | _ | anyhow ::anyhow! ( "unknown provider '{}'. Set {}_AUTH_URL env var." , other , other . to_uppercase ())) ? ;
let token_url = std ::env ::var ( format! ( " {} _TOKEN_URL" , other . to_uppercase ()))
. map_err ( | _ | anyhow ::anyhow! ( "{}_TOKEN_URL not set" , other . to_uppercase ())) ? ;
let client_id = std ::env ::var ( format! ( " {} _CLIENT_ID" , other . to_uppercase ()))
. unwrap_or_else ( | _ | "zesdex" . to_string ());
OAuthConfig {
auth_url ,
token_url ,
client_id ,
client_secret : std ::env ::var ( format! ( " {} _CLIENT_SECRET" , other . to_uppercase ())). ok (),
scopes : vec ! [ "openid" . to_string (), "profile" . to_string (), "email" . to_string ()],
2026-07-11 21:06:22 +07:00
}
}
2026-07-11 23:45:13 +07:00
};
let server = LoopbackServer ::bind () ? ;
let redirect_uri = server . redirect_uri ();
let verifier = CodeVerifier ::new ();
let challenge = verifier . challenge ();
let state_token = format! ( " {:x} " , sha2 ::Sha256 ::digest ( rand_bytes ( 16 )));
let mut manager = OAuthManager ::new ( config . clone ());
2026-07-12 03:56:43 +07:00
let _auth_url = manager . build_auth_url ( & redirect_uri , & state_token , challenge . as_str ());
2026-07-11 23:45:13 +07:00
2026-07-12 03:56:43 +07:00
// let _ = webbrowser::open(&auth_url);
2026-07-11 23:45:13 +07:00
let code = server . wait_for_code ( 120_000 ) ? ;
manager . exchange_code ( & code , & redirect_uri , verifier . as_str ())
. map_err ( | e | anyhow ::anyhow! ( "{}" , e )) ? ;
if let Some ( ref token ) = manager . token {
let token_path = dirs ::config_dir ()
. unwrap_or_else ( || std ::path ::PathBuf ::from ( "." ))
. join ( "zesdex" )
. join ( format! ( "oauth_ {} .json" , provider ));
if let Some ( parent ) = token_path . parent () {
let _ = std ::fs ::create_dir_all ( parent );
}
let _ = std ::fs ::write ( & token_path , serde_json ::to_string_pretty ( token ). unwrap_or_default ());
2026-07-11 21:06:22 +07:00
}
2026-07-11 23:45:13 +07:00
Ok ( format! ( "Successfully authenticated with {} ." , provider ))
2026-07-11 21:06:22 +07:00
}
2026-07-11 23:45:13 +07:00
fn rand_bytes ( n : usize ) -> Vec < u8 > {
use std ::time ::{ SystemTime , UNIX_EPOCH };
let seed = SystemTime ::now (). duration_since ( UNIX_EPOCH ). unwrap_or_default (). subsec_nanos ();
( 0 .. n ). map ( | i | (( seed >> ( i % 4 * 8 )) ^ ( i as u32 * 2654435761 )) as u8 ). collect ()
}