@@ -140,7 +140,7 @@ exports.checkAvailability = async function () {
140140 * aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete
141141 */
142142exports . sendPrompt = async function ( params ) {
143- const { prompt, projectPath, sessionAction, model, locale, selectionContext, images } = params ;
143+ const { prompt, projectPath, sessionAction, model, locale, selectionContext, images, envOverrides } = params ;
144144 const requestId = Date . now ( ) . toString ( 36 ) + Math . random ( ) . toString ( 36 ) . slice ( 2 , 7 ) ;
145145
146146 // Handle session
@@ -183,7 +183,7 @@ exports.sendPrompt = async function (params) {
183183 }
184184
185185 // Run the query asynchronously — don't await here so we return requestId immediately
186- _runQuery ( requestId , enrichedPrompt , projectPath , model , currentAbortController . signal , locale , images )
186+ _runQuery ( requestId , enrichedPrompt , projectPath , model , currentAbortController . signal , locale , images , envOverrides )
187187 . catch ( err => {
188188 console . error ( "[Phoenix AI] Query error:" , err ) ;
189189 } ) ;
@@ -287,10 +287,11 @@ exports.clearClarification = async function () {
287287/**
288288 * Internal: run a Claude SDK query and stream results back to the browser.
289289 */
290- async function _runQuery ( requestId , prompt , projectPath , model , signal , locale , images ) {
290+ async function _runQuery ( requestId , prompt , projectPath , model , signal , locale , images , envOverrides ) {
291291 let editCount = 0 ;
292292 let toolCounter = 0 ;
293293 let queryFn ;
294+ let connectionTimer = null ;
294295
295296 try {
296297 queryFn = await getQueryFn ( ) ;
@@ -315,10 +316,24 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
315316 phase : "start"
316317 } ) ;
317318
319+ if ( envOverrides ) {
320+ const keys = Object . keys ( envOverrides ) ;
321+ console . log ( "[AI] Using env overrides:" , keys . map ( k => k + "=" + ( k . includes ( "TOKEN" ) || k . includes ( "KEY" ) ? "***" : envOverrides [ k ] ) ) . join ( ", " ) ) ;
322+ }
323+
324+ let _lastStderrLines = [ ] ;
325+ const MAX_STDERR_LINES = 20 ;
326+
318327 const queryOptions = {
319328 cwd : projectPath || process . cwd ( ) ,
320329 maxTurns : undefined ,
321- stderr : ( data ) => console . log ( "[AI stderr]" , data ) ,
330+ stderr : ( data ) => {
331+ console . log ( "[AI stderr]" , data ) ;
332+ _lastStderrLines . push ( data ) ;
333+ if ( _lastStderrLines . length > MAX_STDERR_LINES ) {
334+ _lastStderrLines . shift ( ) ;
335+ }
336+ } ,
322337 allowedTools : [
323338 "Read" , "Edit" , "Write" , "Glob" , "Grep" , "Bash" ,
324339 "AskUserQuestion" , "Task" ,
@@ -377,6 +392,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
377392 : "" ) ,
378393 includePartialMessages : true ,
379394 abortController : currentAbortController ,
395+ env : envOverrides ? Object . assign ( { } , process . env , envOverrides ) : undefined ,
380396 hooks : {
381397 PreToolUse : [
382398 {
@@ -650,7 +666,37 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
650666 let textDeltaCount = 0 ;
651667 let textStreamSendCount = 0 ;
652668
669+ // Connection timeout — abort if no messages within 60s
670+ let receivedFirstMessage = false ;
671+ const CONNECTION_TIMEOUT_MS = 60000 ;
672+ connectionTimer = setTimeout ( ( ) => {
673+ if ( ! receivedFirstMessage && ! signal . aborted ) {
674+ _log ( "Connection timeout — no response in " + ( CONNECTION_TIMEOUT_MS / 1000 ) + "s" ) ;
675+ const stderrHint = _lastStderrLines
676+ . filter ( line => ! line . startsWith ( "Spawning Claude Code" ) )
677+ . join ( "\n" ) . trim ( ) ;
678+ let timeoutMsg = "Connection timed out — no response from API after " +
679+ ( CONNECTION_TIMEOUT_MS / 1000 ) + " seconds." ;
680+ if ( envOverrides && envOverrides . ANTHROPIC_BASE_URL ) {
681+ timeoutMsg += " Check that the Base URL (" + envOverrides . ANTHROPIC_BASE_URL +
682+ ") is correct and reachable." ;
683+ }
684+ if ( stderrHint ) {
685+ timeoutMsg += "\n" + stderrHint ;
686+ }
687+ nodeConnector . triggerPeer ( "aiError" , {
688+ requestId : requestId ,
689+ error : timeoutMsg
690+ } ) ;
691+ currentAbortController . abort ( ) ;
692+ }
693+ } , CONNECTION_TIMEOUT_MS ) ;
694+
653695 for await ( const message of result ) {
696+ if ( ! receivedFirstMessage ) {
697+ receivedFirstMessage = true ;
698+ clearTimeout ( connectionTimer ) ;
699+ }
654700 // Check abort
655701 if ( signal . aborted ) {
656702 _log ( "Aborted" ) ;
@@ -858,6 +904,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
858904 } ) ;
859905 }
860906
907+ clearTimeout ( connectionTimer ) ;
861908 _log ( "Complete: tools=" + toolCounter , "edits=" + editCount ,
862909 "textDeltas=" + textDeltaCount , "textSent=" + textStreamSendCount ) ;
863910
@@ -868,6 +915,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
868915 } ) ;
869916
870917 } catch ( err ) {
918+ clearTimeout ( connectionTimer ) ;
871919 const errMsg = err . message || String ( err ) ;
872920 const isAbort = signal . aborted || / a b o r t / i. test ( errMsg ) ;
873921
@@ -886,12 +934,31 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
886934
887935 _log ( "Error:" , errMsg . slice ( 0 , 200 ) ) ;
888936
937+ // Build a detailed error message including stderr context
938+ let detailedError = errMsg ;
939+ const stderrContext = _lastStderrLines
940+ . filter ( line => ! line . startsWith ( "Spawning Claude Code" ) )
941+ . join ( "\n" ) . trim ( ) ;
942+ if ( stderrContext ) {
943+ detailedError += "\n" + stderrContext ;
944+ }
945+ // Add hint for custom API settings when process exits with code 1
946+ if ( / e x i t e d w i t h c o d e 1 / . test ( errMsg ) && envOverrides ) {
947+ if ( envOverrides . ANTHROPIC_AUTH_TOKEN ) {
948+ detailedError += "\nThis may be caused by an invalid API key. " +
949+ "Check your API key in Claude Code Settings." ;
950+ }
951+ if ( envOverrides . ANTHROPIC_BASE_URL ) {
952+ detailedError += "\nCustom Base URL: " + envOverrides . ANTHROPIC_BASE_URL ;
953+ }
954+ }
955+
889956 // Clear session after error to prevent cascading failures from resuming a broken session
890957 currentSessionId = null ;
891958
892959 nodeConnector . triggerPeer ( "aiError" , {
893960 requestId : requestId ,
894- error : errMsg
961+ error : detailedError
895962 } ) ;
896963
897964 // Always send aiComplete after aiError so the UI exits streaming state
0 commit comments