diff --git a/apps/desktop/src/main/terminal-host/session-lifecycle.test.ts b/apps/desktop/src/main/terminal-host/session-lifecycle.test.ts index 19e456cb5e4..6b0b4691dbf 100644 --- a/apps/desktop/src/main/terminal-host/session-lifecycle.test.ts +++ b/apps/desktop/src/main/terminal-host/session-lifecycle.test.ts @@ -686,6 +686,11 @@ describe("Terminal Host Session Lifecycle", () => { await sendRequest(control, createRequest); + const isReady = await waitForSessionReady(control, "test-session-kill"); + expect(isReady).toBe(true); + + const exitPromise = waitForEvent(stream, "exit", 5000); + // Kill session const killRequest: IpcRequest = { id: "test-kill-2", @@ -699,7 +704,7 @@ describe("Terminal Host Session Lifecycle", () => { expect(killResponse.ok).toBe(true); // Wait for exit event - const exitEvent = await waitForEvent(stream, "exit", 5000); + const exitEvent = await exitPromise; expect(exitEvent.sessionId).toBe("test-session-kill"); } finally { control.destroy(); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/ChangesContent/components/DiffViewer/DiffViewer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/ChangesContent/components/DiffViewer/DiffViewer.tsx index 46ec01f90ec..2453df26867 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/ChangesContent/components/DiffViewer/DiffViewer.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/ChangesContent/components/DiffViewer/DiffViewer.tsx @@ -75,6 +75,18 @@ export function DiffViewer({ const [isEditorMounted, setIsEditorMounted] = useState(false); const hasScrolledToFirstDiffRef = useRef(false); + useEffect(() => { + if (!isMonacoReady) return; + if (!isEditorMounted) return; + + requestAnimationFrame(() => { + const modifiedEditor = modifiedEditorRef.current; + if (modifiedEditor) { + modifiedEditor.layout(); + } + }); + }, [isMonacoReady, isEditorMounted]); + // biome-ignore lint/correctness/useExhaustiveDependencies: Reset on file change only useEffect(() => { hasScrolledToFirstDiffRef.current = false; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx index d2f4aed315f..8e82a2d5289 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx @@ -16,7 +16,6 @@ interface TabPaneProps { paneId: string; path: MosaicBranch[]; isActive: boolean; - isTabVisible: boolean; tabId: string; workspaceId: string; splitPaneAuto: ( @@ -46,7 +45,6 @@ export function TabPane({ paneId, path, isActive, - isTabVisible, tabId, workspaceId, splitPaneAuto, @@ -125,11 +123,7 @@ export function TabPane({ onMoveToNewTab={onMoveToNewTab} >
- +
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx index 6b7e00c4dbd..4e3355474a7 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx @@ -21,10 +21,9 @@ import { TabPane } from "./TabPane"; interface TabViewProps { tab: Tab; - isTabVisible?: boolean; } -export function TabView({ tab, isTabVisible = true }: TabViewProps) { +export function TabView({ tab }: TabViewProps) { const updateTabLayout = useTabsStore((s) => s.updateTabLayout); const removePane = useTabsStore((s) => s.removePane); const removeTab = useTabsStore((s) => s.removeTab); @@ -149,7 +148,6 @@ export function TabView({ tab, isTabVisible = true }: TabViewProps) { paneId={paneId} path={path} isActive={isActive} - isTabVisible={isTabVisible} tabId={tab.id} workspaceId={tab.workspaceId} splitPaneAuto={splitPaneAuto} @@ -166,7 +164,6 @@ export function TabView({ tab, isTabVisible = true }: TabViewProps) { [ tabPanes, focusedPaneId, - isTabVisible, tab.id, tab.workspaceId, worktreePath, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index 802aee8f0cc..40f9073fff4 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -94,11 +94,7 @@ type CreateOrAttachResult = { }; }; -export const Terminal = ({ - tabId, - workspaceId, - isTabVisible, -}: TerminalProps) => { +export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { const paneId = tabId; // Use granular selectors to avoid re-renders when other panes change const pane = useTabsStore((s) => s.panes[paneId]); @@ -160,8 +156,6 @@ export const Terminal = ({ const initialThemeRef = useRef(terminalTheme); const isFocused = focusedPaneId === paneId; - const isTabVisibleRef = useRef(isTabVisible); - isTabVisibleRef.current = isTabVisible; // Gate streaming until initial state restoration is applied to avoid interleaving output. const isStreamReadyRef = useRef(false); @@ -972,16 +966,13 @@ export const Terminal = ({ }, [isFocused]); useEffect(() => { - if (isFocused && isTabVisible && xtermRef.current) { - xtermRef.current.focus(); - } - }, [isFocused, isTabVisible]); + const xterm = xtermRef.current; + if (!xterm) return; - useEffect(() => { - if (!isTabVisible && xtermRef.current) { - xtermRef.current.blur(); + if (isFocused) { + xterm.focus(); } - }, [isTabVisible]); + }, [isFocused]); useAppHotkey( "FIND_IN_TERMINAL", @@ -1124,9 +1115,6 @@ export const Terminal = ({ if (isRestoredModeRef.current || connectionErrorRef.current) { return; } - if (!isTabVisibleRef.current) { - return; - } if (isExitedRef.current) { if (!isFocusedRef.current || wasKilledByUserRef.current) { return; @@ -1145,9 +1133,6 @@ export const Terminal = ({ if (isRestoredModeRef.current || connectionErrorRef.current) { return; } - if (!isTabVisibleRef.current) { - return; - } const { domEvent } = event; if (domEvent.key === "Enter") { // Don't auto-title from keyboard when in alternate screen (TUI apps like vim, codex) @@ -1197,7 +1182,7 @@ export const Terminal = ({ const cancelInitialAttach = scheduleTerminalAttach({ paneId, - priority: isTabVisible ? (isFocusedRef.current ? 0 : 1) : 2, + priority: isFocusedRef.current ? 0 : 1, run: (done) => { if (isTerminalKilledByUser(paneId)) { wasKilledByUserRef.current = true; @@ -1338,7 +1323,7 @@ export const Terminal = ({ }; const handleWrite = (data: string) => { - if (!isTabVisibleRef.current || isExitedRef.current) { + if (isExitedRef.current) { return; } writeRef.current({ paneId, data }); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/types.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/types.ts index 2a99ae85548..aa767114c9a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/types.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/types.ts @@ -1,7 +1,6 @@ export interface TerminalProps { tabId: string; workspaceId: string; - isTabVisible: boolean; } export type TerminalStreamEvent = diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx index b6f6bd21314..5751f508037 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx @@ -1,40 +1,21 @@ import { useParams } from "@tanstack/react-router"; -import { useEffect, useMemo, useState } from "react"; -import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useMemo } from "react"; import { useSidebarStore } from "renderer/stores"; import { MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, } from "renderer/stores/sidebar-state"; import { useTabsStore } from "renderer/stores/tabs/store"; -import type { Pane, Tab } from "renderer/stores/tabs/types"; -import { - extractPaneIdsFromLayout, - resolveActiveTabIdForWorkspace, -} from "renderer/stores/tabs/utils"; +import { resolveActiveTabIdForWorkspace } from "renderer/stores/tabs/utils"; import { ResizablePanel } from "../../../ResizablePanel"; import { Sidebar } from "../../Sidebar"; import { EmptyTabView } from "./EmptyTabView"; import { TabView } from "./TabView"; -const WARM_TERMINAL_TAB_LIMIT = 8; - -/** - * Check if a tab contains at least one terminal pane. - * Used to determine which tabs need to stay mounted for persistence. - */ -function hasTerminalPane(tab: Tab, panes: Record): boolean { - const paneIds = extractPaneIdsFromLayout(tab.layout); - return paneIds.some((paneId) => panes[paneId]?.type === "terminal"); -} - export function TabsContent() { const { workspaceId: activeWorkspaceId } = useParams({ strict: false }); - const { data: terminalPersistence } = - electronTrpc.settings.getTerminalPersistence.useQuery(); const allTabs = useTabsStore((s) => s.tabs); const activeTabIds = useTabsStore((s) => s.activeTabIds); - const panes = useTabsStore((s) => s.panes); const tabHistoryStacks = useTabsStore((s) => s.tabHistoryStacks); const { @@ -66,118 +47,11 @@ export function TabsContent() { return allTabs.find((tab) => tab.id === activeTabId) || null; }, [activeTabId, allTabs]); - const activeTabHasTerminal = useMemo(() => { - if (!tabToRender) return false; - return hasTerminalPane(tabToRender, panes); - }, [tabToRender, panes]); - - // Per-run warm set of terminal tab IDs (MRU order). Not persisted. - const [warmTerminalTabIds, setWarmTerminalTabIds] = useState([]); - - // Track terminal tab visits to keep a bounded set mounted for smooth switching. - useEffect(() => { - if (!terminalPersistence) return; - if (!activeTabId) return; - if (!activeTabHasTerminal) return; - - setWarmTerminalTabIds((prev) => { - const next = [activeTabId, ...prev.filter((id) => id !== activeTabId)]; - return next.slice(0, WARM_TERMINAL_TAB_LIMIT); - }); - }, [terminalPersistence, activeTabId, activeTabHasTerminal]); - - // When terminal persistence is enabled, keep a bounded set of terminal tabs - // mounted across workspace/tab switches. This prevents TUI white screen issues - // for recently used terminals by avoiding the unmount/remount cycle that - // requires complex reattach/rehydration, while avoiding startup fan-out. - // Non-terminal tabs use normal unmount behavior to save memory. - // Uses visibility:hidden (not display:none) to preserve xterm dimensions. - if (terminalPersistence) { - // Partition tabs: a bounded set of terminal tabs stay mounted, non-terminal tabs unmount when inactive. - const terminalTabs = allTabs.filter((tab) => hasTerminalPane(tab, panes)); - const terminalTabsById = new Map(terminalTabs.map((tab) => [tab.id, tab])); - - const warmIdsFiltered = warmTerminalTabIds.filter((id) => - terminalTabsById.has(id), - ); - - // Ensure active terminal tab is included in the mounted set even before the - // warm-set effect runs (first render after tab switch). - const terminalTabIdsToRender = (() => { - const ids = [...warmIdsFiltered]; - if (activeTabHasTerminal && activeTabId && !ids.includes(activeTabId)) { - ids.unshift(activeTabId); - } - return ids.slice(0, WARM_TERMINAL_TAB_LIMIT); - })(); - - const terminalTabsToRender = terminalTabIdsToRender - .map((id) => terminalTabsById.get(id)) - .filter((tab): tab is Tab => !!tab); - - const activeNonTerminalTab = - tabToRender && !activeTabHasTerminal ? tabToRender : null; - - return ( -
-
- {/* Terminal tabs: keep mounted with visibility toggle */} - {terminalTabsToRender.map((tab) => { - const isVisible = - tab.workspaceId === activeWorkspaceId && tab.id === activeTabId; - - return ( -
- -
- ); - })} - {/* Active non-terminal tab: render normally (unmounts when switching) */} - {activeNonTerminalTab && ( -
- -
- )} - {/* Fallback: show empty view without unmounting terminal tabs */} - {!activeNonTerminalTab && !tabToRender && ( -
- -
- )} -
- {isSidebarOpen && ( - - - - )} -
- ); - } - // Original behavior when persistence disabled: only render active tab return (
- {tabToRender ? ( - - ) : ( - - )} + {tabToRender ? : }
{isSidebarOpen && (