fix(terminals): decouple PTY lifecycle from view lifecycle (no kill on navigation)
Navigating (layout/tab switch) tore the xterm view down and called handle.close(), killing the backend PTY and cutting off running AIs. Now the view's cleanup only detaches; only an explicit user action kills a PTY. Backend: - PortablePtyAdapter: per-session scrollback ring buffer (~100KB, most recent) + re-subscribable fan-out broadcast replacing the single-take output_rx. Reader thread feeds both the ring buffer and current subscribers; on EOF it closes subscribers (streams end) while keeping scrollback for late re-attach. - PtyPort: new scrollback() method; subscribe_output is now re-subscribable (all impls + test fakes updated). - reattach_terminal IPC command: returns scrollback and re-wires a fresh output channel on the live session without re-spawning. - CloseRequested hook kills all live PTYs cleanly on app shutdown. - TerminalSessions::handles() to enumerate live sessions at shutdown. Frontend: - TerminalHandle.detach(); TerminalGateway/AgentGateway.reattach() + mocks. - TerminalView cleanup detaches (never close); on mount it re-attaches to a persisted session (repainting scrollback) instead of opening a new PTY. - LayoutGrid persists the cell's session id via setSession; AgentsPanel tracks per-agent session ids — both drive reattach-vs-open. Tests: ring buffer bounds to 100KB keeping newest bytes; scrollback retained; re-subscription delivers post-reattach output; TerminalView detaches (not closes) on unmount and reattaches with a known session; mock detach/reattach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -48,6 +48,28 @@ pub fn run() {
|
||||
events::spawn_relay(app.handle().clone(), &app_state.event_bus);
|
||||
|
||||
app.manage(app_state);
|
||||
|
||||
// Kill all live PTYs cleanly when the main window is closing. This is
|
||||
// independent of the per-view (navigation/layout) lifecycle — those
|
||||
// must NEVER kill a PTY — and only fires on a genuine app shutdown.
|
||||
// A brutal crash is best-effort and out of scope.
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let handle = app.handle().clone();
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||
if let Some(state) = handle.try_state::<AppState>() {
|
||||
let pty = std::sync::Arc::clone(&state.pty_port);
|
||||
let handles = state.terminal_sessions.handles();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
for h in handles {
|
||||
let _ = pty.kill(&h).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@ -60,6 +82,7 @@ pub fn run() {
|
||||
commands::write_terminal,
|
||||
commands::resize_terminal,
|
||||
commands::close_terminal,
|
||||
commands::reattach_terminal,
|
||||
commands::load_layout,
|
||||
commands::mutate_layout,
|
||||
commands::list_layouts,
|
||||
|
||||
Reference in New Issue
Block a user