diff --git a/desktop/src-tauri/src/commands/terminal.rs b/desktop/src-tauri/src/commands/terminal.rs index e6bd9a735b..949f5ed2bd 100644 --- a/desktop/src-tauri/src/commands/terminal.rs +++ b/desktop/src-tauri/src/commands/terminal.rs @@ -38,3 +38,9 @@ pub fn terminal_close_session( ) -> Result { state.terminal_registry.close_session(&input.session_id) } + +/// Close all active PTY sessions. Called on workspace switch. +#[tauri::command] +pub fn terminal_close_all_sessions(state: State<'_, AppState>) -> Result { + state.terminal_registry.close_all_sessions() +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 4c7f3ed8bc..b1fd720d94 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -589,6 +589,7 @@ pub fn run() { terminal_write, terminal_resize, terminal_close_session, + terminal_close_all_sessions, ]) .build(tauri::generate_context!()) .expect("error while building tauri application"); diff --git a/desktop/src-tauri/src/terminal.rs b/desktop/src-tauri/src/terminal.rs index 901cda6ead..01303af79f 100644 --- a/desktop/src-tauri/src/terminal.rs +++ b/desktop/src-tauri/src/terminal.rs @@ -215,6 +215,21 @@ impl TerminalRegistry { let _ = session.killer.kill(); Ok(true) } + + /// Close all active PTY sessions. Called on workspace switch to prevent + /// orphaned shell processes from accumulating. + pub fn close_all_sessions(&self) -> Result { + let mut guard = self.sessions.lock().map_err(|e| e.to_string())?; + let count = guard.by_session_id.len() as u32; + guard.session_id_by_channel.clear(); + let sessions = std::mem::take(&mut guard.by_session_id); + drop(guard); + + for mut session in sessions.into_values() { + let _ = session.killer.kill(); + } + Ok(count) + } } impl Drop for TerminalRegistry { diff --git a/desktop/src/features/terminal/TerminalInstance.tsx b/desktop/src/features/terminal/TerminalInstance.tsx index 61e16c1db2..4ffa439f51 100644 --- a/desktop/src/features/terminal/TerminalInstance.tsx +++ b/desktop/src/features/terminal/TerminalInstance.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { Terminal } from "@xterm/xterm"; +import type { ITheme } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; import { WebLinksAddon } from "@xterm/addon-web-links"; import { invoke } from "@tauri-apps/api/core"; @@ -7,6 +8,61 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import "@xterm/xterm/css/xterm.css"; +// Tokyo Night — high-contrast dark terminal surface. +const DARK_THEME: ITheme = { + background: "#1a1b26", + foreground: "#c0caf5", + cursor: "#c0caf5", + selectionBackground: "#33467c", + black: "#15161e", + red: "#f7768e", + green: "#9ece6a", + yellow: "#e0af68", + blue: "#7aa2f7", + magenta: "#bb9af7", + cyan: "#7dcfff", + white: "#a9b1d6", + brightBlack: "#414868", + brightRed: "#f7768e", + brightGreen: "#9ece6a", + brightYellow: "#e0af68", + brightBlue: "#7aa2f7", + brightMagenta: "#bb9af7", + brightCyan: "#7dcfff", + brightWhite: "#c0caf5", +}; + +// Light mode: terminal stays dark (high contrast against light app chrome). +// Uses the same Tokyo Night bg with slightly warmer foreground for readability. +const LIGHT_THEME: ITheme = { + background: "#1a1b26", + foreground: "#c8d3f5", + cursor: "#c8d3f5", + selectionBackground: "#2f3549", + black: "#1a1b26", + red: "#ff757f", + green: "#c3e88d", + yellow: "#ffc777", + blue: "#82aaff", + magenta: "#c099ff", + cyan: "#86e1fc", + white: "#c8d3f5", + brightBlack: "#545c7e", + brightRed: "#ff757f", + brightGreen: "#c3e88d", + brightYellow: "#ffc777", + brightBlue: "#82aaff", + brightMagenta: "#c099ff", + brightCyan: "#86e1fc", + brightWhite: "#e4f0fb", +}; + +function getTerminalTheme(): ITheme { + return document.documentElement.classList.contains("dark") + ? DARK_THEME + : LIGHT_THEME; +} + type TerminalDataPayload = { sessionId: string; data: string; @@ -53,28 +109,7 @@ export function TerminalInstance({ lineHeight: 1.3, scrollback: 10000, allowProposedApi: true, - theme: { - background: "#1a1b26", - foreground: "#c0caf5", - cursor: "#c0caf5", - selectionBackground: "#33467c", - black: "#15161e", - red: "#f7768e", - green: "#9ece6a", - yellow: "#e0af68", - blue: "#7aa2f7", - magenta: "#bb9af7", - cyan: "#7dcfff", - white: "#a9b1d6", - brightBlack: "#414868", - brightRed: "#f7768e", - brightGreen: "#9ece6a", - brightYellow: "#e0af68", - brightBlue: "#7aa2f7", - brightMagenta: "#bb9af7", - brightCyan: "#7dcfff", - brightWhite: "#c0caf5", - }, + theme: getTerminalTheme(), }); const fitAddon = new FitAddon(); @@ -137,6 +172,15 @@ export function TerminalInstance({ }); }); + // Watch for theme class changes on to sync xterm colors. + const themeObserver = new MutationObserver(() => { + term.options.theme = getTerminalTheme(); + }); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + // Resize observer. const resizeObserver = new ResizeObserver(() => { if (fitAddonRef.current && isVisible) { @@ -155,6 +199,7 @@ export function TerminalInstance({ resizeObserver.observe(containerRef.current); return () => { + themeObserver.disconnect(); resizeObserver.disconnect(); unlistenDataRef.current?.(); unlistenExitRef.current?.(); diff --git a/desktop/src/features/terminal/TerminalPanel.tsx b/desktop/src/features/terminal/TerminalPanel.tsx index 409a260cd3..781f8cefd8 100644 --- a/desktop/src/features/terminal/TerminalPanel.tsx +++ b/desktop/src/features/terminal/TerminalPanel.tsx @@ -100,7 +100,7 @@ export function TerminalPanel({ {/* Header bar */} -
+
Terminal