1- import React , { useState } from 'react' ;
1+ import React , { useState , useRef , useEffect } from 'react' ;
22import {
33 Button ,
44 Stack ,
@@ -207,6 +207,39 @@ export default function InteractiveJobCard({
207207
208208 const tunnelReady = Boolean ( tunnelData ?. is_ready ) ;
209209
210+ // Track how long tunnel has been not-ready while job is INTERACTIVE.
211+ // After a timeout, show a warning so the user isn't stuck on "Launching…" forever.
212+ const TUNNEL_TIMEOUT_MS = 3 * 60 * 1000 ; // 3 minutes
213+ const waitingSinceRef = useRef < number | null > ( null ) ;
214+ const [ tunnelTimedOut , setTunnelTimedOut ] = useState ( false ) ;
215+
216+ useEffect ( ( ) => {
217+ if ( tunnelReady || ! isInteractive ) {
218+ // Reset when tunnel becomes ready or job is no longer interactive.
219+ waitingSinceRef . current = null ;
220+ setTunnelTimedOut ( false ) ;
221+ return ;
222+ }
223+
224+ // Start the clock if not already running.
225+ if ( waitingSinceRef . current === null ) {
226+ waitingSinceRef . current = Date . now ( ) ;
227+ }
228+
229+ const elapsed = Date . now ( ) - waitingSinceRef . current ;
230+ if ( elapsed >= TUNNEL_TIMEOUT_MS ) {
231+ setTunnelTimedOut ( true ) ;
232+ return ;
233+ }
234+
235+ // Schedule a check for when the timeout would fire.
236+ const timer = setTimeout (
237+ ( ) => setTunnelTimedOut ( true ) ,
238+ TUNNEL_TIMEOUT_MS - elapsed ,
239+ ) ;
240+ return ( ) => clearTimeout ( timer ) ;
241+ } , [ tunnelReady , isInteractive , tunnelData ] ) ;
242+
210243 return (
211244 < Card
212245 variant = "outlined"
@@ -283,6 +316,11 @@ export default function InteractiveJobCard({
283316 job . status === 'INTERACTIVE' ? 99 : undefined
284317 }
285318 />
319+ { tunnelTimedOut && ! tunnelReady && (
320+ < Typography level = "body-xs" color = "warning" sx = { { mt : 0.5 } } >
321+ Startup may have stalled. Check Logs.
322+ </ Typography >
323+ ) }
286324 </ Box >
287325
288326 { showActions && (
@@ -304,7 +342,11 @@ export default function InteractiveJobCard({
304342 disabled = { ! tunnelReady }
305343 onClick = { ( ) => setInteractOpen ( true ) }
306344 >
307- { tunnelReady ? 'Interact' : 'Launching…' }
345+ { tunnelReady
346+ ? 'Interact'
347+ : tunnelTimedOut
348+ ? 'Waiting…'
349+ : 'Launching…' }
308350 </ Button >
309351 </ Stack >
310352 </ >
0 commit comments