1+ use anyhow:: Result ;
2+ use serde_json;
3+ use tauri:: AppHandle ;
4+ use uuid:: Uuid ;
5+
6+ use crate :: protocol:: { CodexConfig , InputItem , Op , Submission } ;
7+
8+ use super :: { CommandBuilder , ProcessManager , EventHandler } ;
9+
10+ pub struct CodexClient {
11+ #[ allow( dead_code) ]
12+ app : AppHandle ,
13+ session_id : String ,
14+ process_manager : ProcessManager ,
15+ #[ allow( dead_code) ]
16+ config : CodexConfig ,
17+ }
18+
19+ impl CodexClient {
20+ pub async fn new ( app : & AppHandle , session_id : String , config : CodexConfig ) -> Result < Self > {
21+ log:: debug!(
22+ "Creating CodexClient for session and config: {} {:?}" ,
23+ session_id,
24+ config
25+ ) ;
26+
27+ // Build the command and environment variables
28+ let ( cmd, env_vars) = CommandBuilder :: build_command ( & config) . await ?;
29+
30+ // Start the process
31+ let mut process_manager = ProcessManager :: start_process ( cmd, env_vars, & config) . await ?;
32+
33+ // Set up event handlers for stdout and stderr
34+ if let Some ( process) = & mut process_manager. process {
35+ let stdout = process. stdout . take ( ) . expect ( "Failed to open stdout" ) ;
36+ let stderr = process. stderr . take ( ) . expect ( "Failed to open stderr" ) ;
37+
38+ EventHandler :: start_stdout_handler ( app. clone ( ) , stdout, session_id. clone ( ) ) ;
39+ EventHandler :: start_stderr_handler ( stderr, session_id. clone ( ) ) ;
40+ }
41+
42+ let client = Self {
43+ app : app. clone ( ) ,
44+ session_id,
45+ process_manager,
46+ config : config. clone ( ) ,
47+ } ;
48+
49+ Ok ( client)
50+ }
51+
52+ async fn send_submission ( & self , submission : Submission ) -> Result < ( ) > {
53+ let json = serde_json:: to_string ( & submission) ?;
54+ log:: debug!( "📤 Sending JSON to codex: {}" , json) ;
55+ self . process_manager . send_to_stdin ( json) ?;
56+ Ok ( ( ) )
57+ }
58+
59+ pub async fn send_user_input ( & self , message : String ) -> Result < ( ) > {
60+ let submission = Submission {
61+ id : Uuid :: new_v4 ( ) . to_string ( ) ,
62+ op : Op :: UserInput {
63+ items : vec ! [ InputItem :: Text { text: message } ] ,
64+ } ,
65+ } ;
66+
67+ self . send_submission ( submission) . await
68+ }
69+
70+ pub async fn send_user_input_with_media (
71+ & self ,
72+ message : String ,
73+ media_paths : Vec < String > ,
74+ ) -> Result < ( ) > {
75+ log:: debug!( "🎯 [CodexClient] send_user_input_with_media called:" ) ;
76+ log:: debug!( " 💬 message: {}" , message) ;
77+ log:: debug!( " 📸 media_paths: {:?}" , media_paths) ;
78+ log:: debug!( " 📊 media_paths count: {}" , media_paths. len( ) ) ;
79+
80+ let mut items = vec ! [ InputItem :: Text { text: message } ] ;
81+
82+ // Add media files as LocalImage items - codex will convert to base64 automatically
83+ for path in media_paths {
84+ let path_buf = std:: path:: PathBuf :: from ( path. clone ( ) ) ;
85+ log:: debug!( " 🔗 Adding local image path: {}" , path) ;
86+ items. push ( InputItem :: LocalImage { path : path_buf } ) ;
87+ }
88+
89+ log:: debug!( " 📦 Total items in submission: {}" , items. len( ) ) ;
90+
91+ let submission = Submission {
92+ id : Uuid :: new_v4 ( ) . to_string ( ) ,
93+ op : Op :: UserInput { items } ,
94+ } ;
95+
96+ log:: debug!( " 🚀 Sending submission to codex" ) ;
97+ self . send_submission ( submission) . await
98+ }
99+
100+ pub async fn send_exec_approval ( & self , approval_id : String , approved : bool ) -> Result < ( ) > {
101+ let decision = if approved { "approved" } else { "denied" } . to_string ( ) ;
102+
103+ let submission = Submission {
104+ id : Uuid :: new_v4 ( ) . to_string ( ) ,
105+ op : Op :: ExecApproval {
106+ id : approval_id,
107+ decision,
108+ } ,
109+ } ;
110+
111+ self . send_submission ( submission) . await
112+ }
113+
114+ #[ allow( dead_code) ]
115+ pub async fn send_patch_approval ( & self , approval_id : String , approved : bool ) -> Result < ( ) > {
116+ let decision = if approved { "approved" } else { "denied" } . to_string ( ) ;
117+
118+ let submission = Submission {
119+ id : Uuid :: new_v4 ( ) . to_string ( ) ,
120+ op : Op :: PatchApproval {
121+ id : approval_id,
122+ decision,
123+ } ,
124+ } ;
125+
126+ self . send_submission ( submission) . await
127+ }
128+
129+ pub async fn send_apply_patch_approval ( & self , approval_id : String , approved : bool ) -> Result < ( ) > {
130+ let decision = if approved { "approved" } else { "denied" } . to_string ( ) ;
131+
132+ let submission = Submission {
133+ id : Uuid :: new_v4 ( ) . to_string ( ) ,
134+ op : Op :: PatchApproval {
135+ id : approval_id,
136+ decision,
137+ } ,
138+ } ;
139+
140+ self . send_submission ( submission) . await
141+ }
142+
143+ pub async fn interrupt ( & self ) -> Result < ( ) > {
144+ let submission = Submission {
145+ id : Uuid :: new_v4 ( ) . to_string ( ) ,
146+ op : Op :: Interrupt ,
147+ } ;
148+
149+ self . send_submission ( submission) . await
150+ }
151+
152+ pub async fn close_session ( & mut self ) -> Result < ( ) > {
153+ log:: debug!( "Closing session: {}" , self . session_id) ;
154+
155+ // Send shutdown command to codex (graceful shutdown)
156+ let submission = Submission {
157+ id : Uuid :: new_v4 ( ) . to_string ( ) ,
158+ op : Op :: Shutdown ,
159+ } ;
160+
161+ if let Err ( e) = self . send_submission ( submission) . await {
162+ log:: error!( "Failed to send shutdown command: {}" , e) ;
163+ }
164+
165+ // Terminate the process
166+ self . process_manager . terminate ( ) . await ?;
167+
168+ log:: debug!( "Session {} closed" , self . session_id) ;
169+ Ok ( ( ) )
170+ }
171+
172+ #[ allow( dead_code) ]
173+ pub async fn shutdown ( & mut self ) -> Result < ( ) > {
174+ self . close_session ( ) . await
175+ }
176+
177+ #[ allow( dead_code) ]
178+ pub fn is_active ( & self ) -> bool {
179+ self . process_manager . is_active ( )
180+ }
181+ }
0 commit comments