@@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize};
22use serde_json:: { json, Map , Value } ;
33use std:: collections:: HashMap ;
44use std:: env;
5+ use std:: io:: ErrorKind ;
56use std:: path:: PathBuf ;
7+ use std:: time:: Duration ;
68use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
79use std:: sync:: Arc ;
810use git2:: { DiffOptions , Repository , Sort , Status , StatusOptions , Tree } ;
@@ -12,6 +14,7 @@ use tauri::{WebviewUrl, WebviewWindowBuilder};
1214use tokio:: io:: { AsyncBufReadExt , AsyncWriteExt , BufReader } ;
1315use tokio:: process:: { Child , ChildStdin , Command } ;
1416use tokio:: sync:: { Mutex , oneshot} ;
17+ use tokio:: time:: timeout;
1518use uuid:: Uuid ;
1619
1720#[ derive( Debug , Serialize , Deserialize , Clone ) ]
@@ -250,10 +253,58 @@ fn build_codex_command(entry: &WorkspaceEntry) -> Command {
250253 command
251254}
252255
256+ async fn check_codex_installation ( entry : & WorkspaceEntry ) -> Result < Option < String > , String > {
257+ let mut command = build_codex_command ( entry) ;
258+ command. arg ( "--version" ) ;
259+ command. stdout ( std:: process:: Stdio :: piped ( ) ) ;
260+ command. stderr ( std:: process:: Stdio :: piped ( ) ) ;
261+
262+ let output = match timeout ( Duration :: from_secs ( 5 ) , command. output ( ) ) . await {
263+ Ok ( result) => result. map_err ( |e| {
264+ if e. kind ( ) == ErrorKind :: NotFound {
265+ "Codex CLI not found. Install Codex and ensure `codex` is on your PATH."
266+ . to_string ( )
267+ } else {
268+ e. to_string ( )
269+ }
270+ } ) ?,
271+ Err ( _) => {
272+ return Err (
273+ "Timed out while checking Codex CLI. Make sure `codex --version` runs in Terminal."
274+ . to_string ( ) ,
275+ ) ;
276+ }
277+ } ;
278+
279+ if !output. status . success ( ) {
280+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
281+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
282+ let detail = if stderr. trim ( ) . is_empty ( ) {
283+ stdout. trim ( )
284+ } else {
285+ stderr. trim ( )
286+ } ;
287+ if detail. is_empty ( ) {
288+ return Err (
289+ "Codex CLI failed to start. Try running `codex --version` in Terminal."
290+ . to_string ( ) ,
291+ ) ;
292+ }
293+ return Err ( format ! (
294+ "Codex CLI failed to start: {detail}. Try running `codex --version` in Terminal."
295+ ) ) ;
296+ }
297+
298+ let version = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
299+ Ok ( if version. is_empty ( ) { None } else { Some ( version) } )
300+ }
301+
253302async fn spawn_workspace_session (
254303 entry : WorkspaceEntry ,
255304 app_handle : AppHandle ,
256305) -> Result < Arc < WorkspaceSession > , String > {
306+ let _ = check_codex_installation ( & entry) . await ?;
307+
257308 let mut command = build_codex_command ( & entry) ;
258309 command. arg ( "app-server" ) ;
259310 command. stdin ( std:: process:: Stdio :: piped ( ) ) ;
@@ -351,7 +402,23 @@ async fn spawn_workspace_session(
351402 "version" : "0.1.0"
352403 }
353404 } ) ;
354- session. send_request ( "initialize" , init_params) . await ?;
405+ let init_result = timeout (
406+ Duration :: from_secs ( 15 ) ,
407+ session. send_request ( "initialize" , init_params) ,
408+ )
409+ . await ;
410+ let init_response = match init_result {
411+ Ok ( response) => response,
412+ Err ( _) => {
413+ let mut child = session. child . lock ( ) . await ;
414+ let _ = child. kill ( ) . await ;
415+ return Err (
416+ "Codex app-server did not respond to initialize. Check that `codex app-server` works in Terminal."
417+ . to_string ( ) ,
418+ ) ;
419+ }
420+ } ;
421+ init_response?;
355422 session. send_notification ( "initialized" , None ) . await ?;
356423
357424 let payload = AppServerEvent {
0 commit comments