@@ -83,6 +83,9 @@ type BudgetBannerDebugMode = "actual" | Exclude<BudgetBannerStatus, "healthy">;
8383
8484const budgetBannerDebugStorageKey = "nexu_budget_banner_debug_mode" ;
8585const showBudgetBannerDebugPanel = import . meta. env . DEV ;
86+ const STARTUP_GRACE_MS = 15_000 ;
87+ const GATEWAY_RESTART_COALESCE_MS = 10_000 ;
88+ const GATEWAY_HEALTHY_POLLS_TO_SETTLE = 2 ;
8689
8790function formatRelativeTime (
8891 date : string | null | undefined ,
@@ -373,12 +376,15 @@ export function HomePage() {
373376 const videoRef = useRef < HTMLVideoElement > ( null ) ;
374377 const [ videoHover , setVideoHover ] = useState ( false ) ;
375378 const [ pendingChannelId , setPendingChannelId ] = useState < string | null > ( null ) ;
379+ const [ pendingGatewayRestartUntil , setPendingGatewayRestartUntil ] = useState <
380+ number | null
381+ > ( null ) ;
376382 const connectingToastIdRef = useRef < string | number | null > ( null ) ;
377383 const previousLiveStatusesRef = useRef < Record < string , ChannelLiveStatus > > ( { } ) ;
384+ const stableGatewayPollsRef = useRef ( 0 ) ;
378385 // Suppress status-change toasts during startup grace period (first config
379386 // push triggers SIGUSR1 → brief disconnect → reconnect cycle).
380387 const mountedAtRef = useRef ( Date . now ( ) ) ;
381- const STARTUP_GRACE_MS = 15_000 ;
382388
383389 const CHANNEL_OPTIONS = useMemo ( ( ) => getChannelOptions ( t ) , [ t ] ) ;
384390
@@ -596,10 +602,25 @@ export function HomePage() {
596602 return new Map ( entries . map ( ( entry ) => [ entry . channelId , entry ] ) ) ;
597603 } , [ liveStatus ] ) ;
598604
605+ const isPendingGatewayRestartCoalesced = useMemo ( ( ) => {
606+ return (
607+ pendingChannelId !== null &&
608+ pendingGatewayRestartUntil !== null &&
609+ Date . now ( ) < pendingGatewayRestartUntil
610+ ) ;
611+ } , [ pendingChannelId , pendingGatewayRestartUntil ] ) ;
612+
599613 const agentIndicator = useMemo ( ( ) => {
600614 if ( ! hasOperationalContext || ! liveStatus ?. agent ) {
601615 return null ;
602616 }
617+ if ( isPendingGatewayRestartCoalesced ) {
618+ return {
619+ colorClass : "bg-[var(--color-warning)]" ,
620+ pulse : true ,
621+ label : t ( "home.agent.starting" ) ,
622+ } ;
623+ }
603624 return liveStatus . agent . alive
604625 ? {
605626 colorClass : "bg-[var(--color-success)]" ,
@@ -611,7 +632,7 @@ export function HomePage() {
611632 pulse : true ,
612633 label : t ( "home.agent.starting" ) ,
613634 } ;
614- } , [ hasOperationalContext , liveStatus , t ] ) ;
635+ } , [ hasOperationalContext , isPendingGatewayRestartCoalesced , liveStatus , t ] ) ;
615636 const budgetBannerDebugPanel = showBudgetBannerDebugPanel ? (
616637 < BudgetBannerDebugPanel
617638 actualStatus = "healthy"
@@ -638,6 +659,8 @@ export function HomePage() {
638659 const handleChannelCreated = useCallback (
639660 ( channelId : string ) => {
640661 setPendingChannelId ( channelId ) ;
662+ setPendingGatewayRestartUntil ( Date . now ( ) + GATEWAY_RESTART_COALESCE_MS ) ;
663+ stableGatewayPollsRef . current = 0 ;
641664 connectingToastIdRef . current = toast . loading (
642665 t ( "home.channel.phase.connecting" ) ,
643666 ) ;
@@ -656,26 +679,47 @@ export function HomePage() {
656679 toast . loading ( t ( "home.channel.phase.configuring" ) , { id : toastId } ) ;
657680 return ;
658681 }
659- if ( pending . status === "connected" ) {
682+ const gatewayHealthy =
683+ liveStatus ?. gatewayConnected === true && liveStatus ?. agent . alive === true ;
684+ if ( pending . status === "connected" && gatewayHealthy ) {
685+ stableGatewayPollsRef . current += 1 ;
686+ const shouldSettleNow =
687+ ! isPendingGatewayRestartCoalesced ||
688+ stableGatewayPollsRef . current >= GATEWAY_HEALTHY_POLLS_TO_SETTLE ;
689+ if ( ! shouldSettleNow ) {
690+ toast . loading ( t ( "home.channel.phase.configuring" ) , { id : toastId } ) ;
691+ return ;
692+ }
660693 toast . success ( t ( "home.channel.phase.done" ) , { id : toastId } ) ;
661694 connectingToastIdRef . current = null ;
662695 setPendingChannelId ( null ) ;
696+ setPendingGatewayRestartUntil ( null ) ;
697+ stableGatewayPollsRef . current = 0 ;
663698 return ;
664699 }
700+ stableGatewayPollsRef . current = 0 ;
665701 if ( pending . status === "error" ) {
666702 toast . error ( pending . lastError ?? t ( "home.channel.error" ) , {
667703 id : toastId ,
668704 } ) ;
669705 connectingToastIdRef . current = null ;
670706 setPendingChannelId ( null ) ;
707+ setPendingGatewayRestartUntil ( null ) ;
671708 return ;
672709 }
673- if ( pending . status === "restarting" ) {
710+ if ( pending . status === "restarting" || isPendingGatewayRestartCoalesced ) {
674711 toast . loading ( t ( "home.channel.phase.configuring" ) , { id : toastId } ) ;
675712 return ;
676713 }
677714 toast . loading ( t ( "home.channel.phase.almostReady" ) , { id : toastId } ) ;
678- } , [ liveStatusByChannelId , pendingChannelId , t ] ) ;
715+ } , [
716+ isPendingGatewayRestartCoalesced ,
717+ liveStatus ?. agent . alive ,
718+ liveStatus ?. gatewayConnected ,
719+ liveStatusByChannelId ,
720+ pendingChannelId ,
721+ t ,
722+ ] ) ;
679723
680724 useEffect ( ( ) => {
681725 const previous = previousLiveStatusesRef . current ;
@@ -1063,9 +1107,12 @@ export function HomePage() {
10631107 const isPendingChannel =
10641108 actionableChannelId === pendingChannelId ;
10651109 const effectiveStatus : ChannelLiveStatus | undefined =
1066- isPendingChannel &&
1067- ( ! statusEntry || statusEntry . status === "disconnected" )
1068- ? "connecting"
1110+ isPendingChannel && pendingGatewayRestartUntil !== null
1111+ ? isPendingGatewayRestartCoalesced
1112+ ? "restarting"
1113+ : ! statusEntry || statusEntry . status === "disconnected"
1114+ ? "connecting"
1115+ : statusEntry . status
10691116 : statusEntry ?. status ;
10701117 const statusMeta = getChannelStatusMeta (
10711118 effectiveStatus ,
0 commit comments