@@ -39,6 +39,12 @@ internal class Program
3939 private static CancellationTokenSource ? _connectionCts ;
4040 private static Task ? _sseListenerTask ;
4141 private static string ? _latestVersion ;
42+ private static string ? _currentSseUrl ;
43+ private static string ? _currentApiKey ;
44+ private static bool _autoReconnectEnabled = true ;
45+ private static int _reconnectAttempts = 0 ;
46+ private const int MaxReconnectAttempts = 10 ;
47+ private static readonly int [ ] ReconnectDelaysMs = [ 1000 , 2000 , 5000 , 10000 , 30000 ] ;
4248
4349 private static async Task < int > Main ( string [ ] args )
4450 {
@@ -584,6 +590,12 @@ await AnsiConsole.Status()
584590 new System . Net . Http . Headers . AuthenticationHeaderValue ( "Bearer" , apiKey ) ;
585591
586592 _connectionCts = new CancellationTokenSource ( ) ;
593+
594+ // Store connection info for reconnection
595+ _currentSseUrl = sseUrl ;
596+ _currentApiKey = apiKey ;
597+ _autoReconnectEnabled = true ;
598+ _reconnectAttempts = 0 ;
587599
588600 // Start listening to SSE events in background
589601 _sseListenerTask = ListenToSseAsync ( sseUrl , _connectionCts . Token ) ;
@@ -664,20 +676,55 @@ private static async Task ListenToSseAsync(string url, CancellationToken cancell
664676 }
665677 catch ( OperationCanceledException )
666678 {
667- // Normal disconnection
679+ // Normal disconnection - don't auto-reconnect
680+ _autoReconnectEnabled = false ;
668681 }
669682 catch ( Exception ex )
670683 {
671684 AnsiConsole . WriteLine ( ) ;
672- AnsiConsole . MarkupLine ( $ "[red]SSE connection error:[/] { ex . Message } ") ;
685+ AnsiConsole . MarkupLine ( $ "[red]SSE connection error:[/] { Markup . Escape ( ex . Message ) } ") ;
673686 }
674687 finally
675688 {
676689 _isConnected = false ;
677690 AnsiConsole . WriteLine ( ) ;
691+ }
692+
693+ // Attempt auto-reconnect if not manually disconnected (moved outside finally)
694+ if ( _autoReconnectEnabled && ! cancellationToken . IsCancellationRequested && _reconnectAttempts < MaxReconnectAttempts )
695+ {
696+ _reconnectAttempts ++ ;
697+ var delayIndex = Math . Min ( _reconnectAttempts - 1 , ReconnectDelaysMs . Length - 1 ) ;
698+ var delayMs = ReconnectDelaysMs [ delayIndex ] ;
699+
700+ AnsiConsole . MarkupLine ( $ "[yellow]Connection lost. Reconnecting in { delayMs / 1000 } s (attempt { _reconnectAttempts } /{ MaxReconnectAttempts } )...[/]") ;
701+
702+ try
703+ {
704+ await Task . Delay ( delayMs , cancellationToken ) ;
705+
706+ if ( ! cancellationToken . IsCancellationRequested && _currentSseUrl != null )
707+ {
708+ // Reconnect
709+ await ListenToSseAsync ( _currentSseUrl , cancellationToken ) ;
710+ return ; // Don't show disconnected message
711+ }
712+ }
713+ catch ( OperationCanceledException )
714+ {
715+ // Cancelled during reconnect delay
716+ }
717+ }
718+ else if ( _reconnectAttempts >= MaxReconnectAttempts )
719+ {
720+ AnsiConsole . MarkupLine ( "[red]Max reconnection attempts reached. Use 'connect' to reconnect manually.[/]" ) ;
721+ }
722+ else if ( ! _autoReconnectEnabled || cancellationToken . IsCancellationRequested )
723+ {
678724 AnsiConsole . MarkupLine ( "[yellow]Disconnected from server.[/]" ) ;
679- AnsiConsole . Markup ( _isConnected ? "[green]●[/] [green]hookreplay[/]> " : "[red]●[/] [blue]hookreplay[/]> " ) ;
680725 }
726+
727+ AnsiConsole . Markup ( _isConnected ? "[green]●[/] [green]hookreplay[/]> " : "[red]●[/] [blue]hookreplay[/]> " ) ;
681728 }
682729
683730 private static async Task HandleSseEvent ( string eventType , string data )
@@ -686,6 +733,7 @@ private static async Task HandleSseEvent(string eventType, string data)
686733 {
687734 case "connected" :
688735 _isConnected = true ;
736+ _reconnectAttempts = 0 ; // Reset reconnect counter on successful connection
689737 try
690738 {
691739 var connectedEvent = JsonSerializer . Deserialize ( data , CliJsonContext . Default . SseConnectedEvent ) ;
@@ -835,9 +883,12 @@ private static async Task ExecuteRequest(ReplayRequest request, string fullUrl)
835883
836884 if ( displayBody . Length > 500 )
837885 {
838- displayBody = displayBody [ ..500 ] + "\n [silver] ... (truncated)[/] " ;
886+ displayBody = displayBody [ ..500 ] + "... (truncated)" ;
839887 }
840888
889+ // Escape markup characters to prevent Spectre.Console from interpreting them
890+ displayBody = Markup . Escape ( displayBody ) ;
891+
841892 AnsiConsole . Write ( new Panel ( displayBody )
842893 . Header ( "[silver]Response Body[/]" )
843894 . Border ( BoxBorder . Rounded )
@@ -859,6 +910,9 @@ private static async Task DisconnectAsync()
859910 return ;
860911 }
861912
913+ // Disable auto-reconnect for manual disconnect
914+ _autoReconnectEnabled = false ;
915+
862916 await _connectionCts ? . CancelAsync ( ) ;
863917
864918 if ( _sseListenerTask != null )
0 commit comments