fix: auto-close terminal tab/window when shell exits (#15448)

This commit is contained in:
alonginwind
2026-07-02 16:43:27 +08:00
committed by GitHub
parent dce221be5a
commit a2b79462ab
4 changed files with 48 additions and 3 deletions

View File

@@ -95,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
// Auto-close tab when shell exits
_terminalModel.onClosed = () {
if (mounted) {
widget.tabController.closeBy(widget.tabKey);
}
};
// Initialize terminal connection
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);

View File

@@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
// Auto-close connection when shell exits
_terminalModel.onClosed = () {
if (mounted) {
closeConnection(id: widget.id);
}
};
// Web desktop users have full hardware keyboard access, so the on-screen
// terminal extra keys bar is unnecessary and disabled.
_showTerminalExtraKeys = !isWebDesktop &&

View File

@@ -38,6 +38,10 @@ class TerminalModel with ChangeNotifier {
void Function(int w, int h, int pw, int ph)? onResizeExternal;
/// Called when the terminal session ends (shell exits).
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
VoidCallback? onClosed;
Future<void> _handleInput(String data) async {
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
@@ -473,6 +477,8 @@ class TerminalModel with ChangeNotifier {
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
_terminalOpened = false;
notifyListeners();
// Auto-close the tab/page
onClosed?.call();
}
void _handleTerminalError(Map<String, dynamic> evt) {

View File

@@ -1857,15 +1857,33 @@ impl TerminalServiceProxy {
// Process each session with its own lock
for (terminal_id, session_arc) in sessions {
if let Ok(mut session) = session_arc.try_lock() {
// Check if reader thread is still alive and we haven't sent closed message yet
// Check if the session has ended (reader thread finished or child exited).
// On Linux, the PTY reader thread may not return EOF when the shell exits
// (the cloned master fd keeps the read side open), so we also poll the child
// process via try_wait() as a fallback detection mechanism.
let mut should_send_closed = false;
if !session.closed_message_sent {
if let Some(thread) = &session.reader_thread {
if thread.is_finished() {
should_send_closed = true;
session.closed_message_sent = true;
}
}
if !should_send_closed {
if let Some(child) = &mut session.child {
match child.try_wait() {
Ok(Some(_)) => {
should_send_closed = true;
}
Ok(None) => {} // still running
Err(e) => {
log::warn!("Terminal {} child wait error: {}", terminal_id, e);
}
}
}
}
if should_send_closed {
session.closed_message_sent = true;
}
}
// It's Ok to put the closed message here.
// Because the `reader_thread` is joined in `stop()`,
@@ -2018,7 +2036,8 @@ impl TerminalServiceProxy {
}
}
} else {
// For persistent sessions, just clear the child reference
// For persistent sessions, clear the child reference and remove the session
// if the closed message has been sent (shell has exited).
if let Some(session_arc) = sessions.get(&terminal_id) {
let mut session = session_arc.lock().unwrap();
if let Some(mut child) = session.child.take() {
@@ -2028,6 +2047,12 @@ impl TerminalServiceProxy {
}
add_to_reaper(child);
}
if session.closed_message_sent {
// Shell has exited, remove the dead session
drop(session);
sessions.remove(&terminal_id);
service.lock().unwrap().sessions.remove(&terminal_id);
}
}
}