@@ -4,12 +4,14 @@ use gpui::{
44} ;
55
66use super :: blink:: BlinkManager ;
7+ use super :: state:: ReconnectState ;
78use super :: terminal_element:: scroll_delta_lines;
89
910use super :: input;
1011use super :: state:: Terminal ;
1112use super :: terminal_element:: { MouseState , TerminalElement } ;
1213use super :: types:: TerminalContent ;
14+ use crate :: daemon_client;
1315use crate :: theme;
1416use crate :: views:: main_view:: keybindings:: UiKeybindings ;
1517
@@ -34,6 +36,8 @@ pub struct TerminalView {
3436 mouse_state : MouseState ,
3537 /// Parsed keybindings for routing keys between PTY and MainView.
3638 keybindings : UiKeybindings ,
39+ /// In-flight reconnection task. Stored to prevent cancellation.
40+ _reconnect_task : Option < Task < ( ) > > ,
3741}
3842
3943impl TerminalView {
@@ -88,6 +92,7 @@ impl TerminalView {
8892 cmd_held : false ,
8993 } ,
9094 keybindings,
95+ _reconnect_task : None ,
9196 }
9297 }
9398
@@ -138,6 +143,7 @@ impl TerminalView {
138143 cmd_held : false ,
139144 } ,
140145 keybindings,
146+ _reconnect_task : None ,
141147 }
142148 }
143149
@@ -199,12 +205,142 @@ impl TerminalView {
199205 }
200206 }
201207
208+ /// Attempt to reconnect a disconnected daemon terminal.
209+ ///
210+ /// Spawns an async task that re-attaches to the daemon session, builds a
211+ /// fresh `Terminal::from_daemon()`, and swaps it in along with a new batch
212+ /// loop task. No-op for local terminals or if a reconnect is already in
213+ /// progress.
214+ fn try_reconnect ( & mut self , cx : & mut Context < Self > ) {
215+ let session_id = match self . terminal . daemon_session_id ( ) {
216+ Some ( id) => id. to_string ( ) ,
217+ None => return ,
218+ } ;
219+
220+ if self . terminal . reconnect_state ( ) == ReconnectState :: Connecting {
221+ return ;
222+ }
223+
224+ tracing:: info!(
225+ event = "ui.terminal.reconnect_started" ,
226+ session_id = session_id
227+ ) ;
228+ self . terminal
229+ . set_reconnect_state ( ReconnectState :: Connecting ) ;
230+ let ( rows, cols) = self . terminal . current_size ( ) ;
231+ cx. notify ( ) ;
232+
233+ let task = cx. spawn ( async move |this, cx : & mut gpui:: AsyncApp | {
234+ let sid = session_id. clone ( ) ;
235+ let conn_result = cx
236+ . background_executor ( )
237+ . spawn ( async move { daemon_client:: connect_for_attach ( & sid, rows, cols) . await } )
238+ . await ;
239+
240+ match conn_result {
241+ Err ( e) => {
242+ tracing:: error!(
243+ event = "ui.terminal.reconnect_failed" ,
244+ session_id = session_id,
245+ error = %e,
246+ ) ;
247+ let msg = e. to_string ( ) ;
248+ let _ = this. update ( cx, |view, cx| {
249+ view. terminal
250+ . set_reconnect_state ( ReconnectState :: Failed ( msg) ) ;
251+ view. _reconnect_task = None ;
252+ cx. notify ( ) ;
253+ } ) ;
254+ }
255+ Ok ( conn) => {
256+ let _ = this. update ( cx, |view, cx| {
257+ match Terminal :: from_daemon ( session_id. clone ( ) , conn, cx) {
258+ Ok ( mut new_terminal) => {
259+ let ( byte_rx, event_rx) = match new_terminal. take_channels ( ) {
260+ Ok ( ch) => ch,
261+ Err ( e) => {
262+ tracing:: error!(
263+ event = "ui.terminal.reconnect_channels_failed" ,
264+ error = %e,
265+ ) ;
266+ view. terminal . set_reconnect_state ( ReconnectState :: Failed (
267+ e. to_string ( ) ,
268+ ) ) ;
269+ view. _reconnect_task = None ;
270+ cx. notify ( ) ;
271+ return ;
272+ }
273+ } ;
274+
275+ let term = new_terminal. term ( ) . clone ( ) ;
276+ let pty_writer = new_terminal. pty_writer ( ) . clone ( ) ;
277+ let error_state = new_terminal. error_state ( ) . clone ( ) ;
278+ let exited = new_terminal. exited_flag ( ) . clone ( ) ;
279+ let executor = cx. background_executor ( ) . clone ( ) ;
280+
281+ let event_task =
282+ cx. spawn ( async move |this2, cx2 : & mut gpui:: AsyncApp | {
283+ Terminal :: run_batch_loop (
284+ term,
285+ pty_writer,
286+ error_state,
287+ exited,
288+ byte_rx,
289+ event_rx,
290+ executor,
291+ || {
292+ let _ = this2. update ( cx2, |_, cx| cx. notify ( ) ) ;
293+ } ,
294+ )
295+ . await ;
296+ } ) ;
297+
298+ view. terminal = new_terminal;
299+ view. _event_task = event_task;
300+ view. _reconnect_task = None ;
301+ tracing:: info!(
302+ event = "ui.terminal.reconnect_completed" ,
303+ session_id = session_id,
304+ ) ;
305+ cx. notify ( ) ;
306+ }
307+ Err ( e) => {
308+ tracing:: error!(
309+ event = "ui.terminal.reconnect_terminal_failed" ,
310+ session_id = session_id,
311+ error = %e,
312+ ) ;
313+ view. terminal
314+ . set_reconnect_state ( ReconnectState :: Failed ( e. to_string ( ) ) ) ;
315+ view. _reconnect_task = None ;
316+ cx. notify ( ) ;
317+ }
318+ }
319+ } ) ;
320+ }
321+ }
322+ } ) ;
323+
324+ self . _reconnect_task = Some ( task) ;
325+ }
326+
202327 fn on_key_down ( & mut self , event : & KeyDownEvent , _window : & mut Window , cx : & mut Context < Self > ) {
203328 self . blink . reset ( cx) ;
204329
205330 let key = event. keystroke . key . as_str ( ) ;
206331 let cmd = event. keystroke . modifiers . platform ;
207332
333+ // Intercept R key on dead daemon terminals to trigger reconnect.
334+ if key. eq_ignore_ascii_case ( "r" )
335+ && !event. keystroke . modifiers . control
336+ && !cmd
337+ && self . terminal . has_exited ( )
338+ && self . terminal . daemon_session_id ( ) . is_some ( )
339+ {
340+ self . try_reconnect ( cx) ;
341+ return ;
342+ }
343+
208344 if event. keystroke . modifiers . control && key == "tab" {
209345 cx. propagate ( ) ;
210346 return ;
@@ -314,18 +450,49 @@ impl Render for TerminalView {
314450 . bg ( theme:: terminal_background ( ) ) ;
315451
316452 if let Some ( msg) = error {
453+ let is_daemon = self . terminal . daemon_session_id ( ) . is_some ( ) ;
454+ let reconnect = self . terminal . reconnect_state ( ) ;
455+
456+ let ( banner_bg, banner_text) = if is_daemon {
457+ match & reconnect {
458+ ReconnectState :: Connecting => (
459+ theme:: surface ( ) ,
460+ "Reconnecting to daemon session..." . to_string ( ) ,
461+ ) ,
462+ ReconnectState :: Failed ( err) => (
463+ theme:: ember ( ) ,
464+ format ! (
465+ "Reconnect failed: {err}. Press R to retry or {} to return." ,
466+ self . keybindings. terminal. focus_escape. hint_str( )
467+ ) ,
468+ ) ,
469+ ReconnectState :: Idle => (
470+ theme:: ember ( ) ,
471+ format ! (
472+ "Terminal error: {msg}. Press R to reconnect or {} to return." ,
473+ self . keybindings. terminal. focus_escape. hint_str( )
474+ ) ,
475+ ) ,
476+ }
477+ } else {
478+ (
479+ theme:: ember ( ) ,
480+ format ! (
481+ "Terminal error: {msg}. {} to return." ,
482+ self . keybindings. terminal. focus_escape. hint_str( )
483+ ) ,
484+ )
485+ } ;
486+
317487 container = container. child (
318488 div ( )
319489 . w_full ( )
320490 . px ( px ( theme:: SPACE_3 ) )
321491 . py ( px ( theme:: SPACE_2 ) )
322- . bg ( theme :: ember ( ) )
492+ . bg ( banner_bg )
323493 . text_color ( theme:: text_white ( ) )
324494 . text_size ( px ( theme:: TEXT_SM ) )
325- . child ( format ! (
326- "Terminal error: {msg}. {} to return." ,
327- self . keybindings. terminal. focus_escape. hint_str( )
328- ) ) ,
495+ . child ( banner_text) ,
329496 ) ;
330497 }
331498
0 commit comments