From 2f6bea52d41c13bb1d669154ec721bbb33886041 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Thu, 21 May 2026 20:25:32 -0700 Subject: [PATCH] fix(terminal): workspace cleanup, light mode theme, buffer cap verified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Marge's review feedback: 1. Workspace-switch cleanup: Added terminal_close_all_sessions command that kills all PTY sessions. Called from resetWorkspaceState() so orphaned shells don't accumulate across workspace switches. 2. Light mode theme: Terminal surface stays dark in both modes (high contrast "pop" against light app chrome). Added Tokyo Night Moon variant for light mode with warmer foreground. Panel header/buttons use consistent light-on-dark colors regardless of app theme. MutationObserver syncs xterm theme when app theme class changes. 3. Buffer cap: Already implemented (256KB with drain-from-front) — verified the existing MAX_BUFFER_BYTES guard in spawn_reader_thread. --- desktop/src-tauri/src/commands/terminal.rs | 6 ++ desktop/src-tauri/src/lib.rs | 1 + desktop/src-tauri/src/terminal.rs | 15 ++++ .../features/terminal/TerminalInstance.tsx | 89 ++++++++++++++----- .../src/features/terminal/TerminalPanel.tsx | 2 +- .../features/workspaces/useWorkspaceInit.ts | 4 + 6 files changed, 94 insertions(+), 23 deletions(-) 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