diff --git a/src/App.tsx b/src/App.tsx index a0b7056ed..804646ba3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1013,6 +1013,7 @@ function MainApp() { terminalState, ensureTerminalWithTitle, restartTerminalSession, + requestTerminalFocus, } = useTerminalController({ activeWorkspaceId, activeWorkspace, @@ -1026,10 +1027,33 @@ function MainApp() { [ensureTerminalWithTitle], ); + const openTerminalWithFocus = useCallback(() => { + if (!activeWorkspaceId) { + return; + } + requestTerminalFocus(); + openTerminal(); + }, [activeWorkspaceId, openTerminal, requestTerminalFocus]); + + const handleToggleTerminalWithFocus = useCallback(() => { + if (!activeWorkspaceId) { + return; + } + if (!terminalOpen) { + requestTerminalFocus(); + } + handleToggleTerminal(); + }, [ + activeWorkspaceId, + handleToggleTerminal, + requestTerminalFocus, + terminalOpen, + ]); + const launchScriptState = useWorkspaceLaunchScript({ activeWorkspace, updateWorkspaceSettings, - openTerminal, + openTerminal: openTerminalWithFocus, ensureLaunchTerminal, restartLaunchSession: restartTerminalSession, terminalState, @@ -1039,7 +1063,7 @@ function MainApp() { const launchScriptsState = useWorkspaceLaunchScripts({ activeWorkspace, updateWorkspaceSettings, - openTerminal, + openTerminal: openTerminalWithFocus, ensureLaunchTerminal: (workspaceId, entry, title) => { const label = entry.label?.trim() || entry.icon; return ensureTerminalWithTitle( @@ -1882,7 +1906,7 @@ function MainApp() { onCycleAgent: handleCycleAgent, onCycleWorkspace: handleCycleWorkspace, onToggleDebug: handleDebugClick, - onToggleTerminal: handleToggleTerminal, + onToggleTerminal: handleToggleTerminalWithFocus, sidebarCollapsed, rightPanelCollapsed, onExpandSidebar: expandSidebar, @@ -2032,7 +2056,7 @@ function MainApp() { handleCheckoutPullRequest(pullRequest.number), onCreateBranch: handleCreateBranch, onCopyThread: handleCopyThread, - onToggleTerminal: handleToggleTerminal, + onToggleTerminal: handleToggleTerminalWithFocus, showTerminalButton: !isCompact, showWorkspaceTools: !isCompact, launchScript: launchScriptState.launchScript, diff --git a/src/features/terminal/hooks/useTerminalController.ts b/src/features/terminal/hooks/useTerminalController.ts index 2274b9aaa..33d915cac 100644 --- a/src/features/terminal/hooks/useTerminalController.ts +++ b/src/features/terminal/hooks/useTerminalController.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import type { DebugEntry, WorkspaceInfo } from "../../../types"; import { closeTerminalSession } from "../../../services/tauri"; import { buildErrorDebugEntry } from "../../../utils/debugEntries"; @@ -23,6 +23,10 @@ export function useTerminalController({ const cleanupTerminalRef = useRef<((workspaceId: string, terminalId: string) => void) | null>( null, ); + const [focusRequestVersion, setFocusRequestVersion] = useState(0); + const requestTerminalFocus = useCallback(() => { + setFocusRequestVersion((prev) => prev + 1); + }, []); const shouldIgnoreTerminalCloseError = useCallback((error: unknown) => { const message = error instanceof Error ? error.message : String(error); return message.includes("Terminal session not found"); @@ -66,6 +70,7 @@ export function useTerminalController({ activeWorkspace, activeTerminalId, isVisible: terminalOpen, + focusRequestVersion, onDebug, onSessionExit: (workspaceId, terminalId) => { const shouldClosePanel = @@ -88,17 +93,19 @@ export function useTerminalController({ if (!activeWorkspaceId) { return; } + requestTerminalFocus(); setActiveTerminal(activeWorkspaceId, terminalId); }, - [activeWorkspaceId, setActiveTerminal], + [activeWorkspaceId, requestTerminalFocus, setActiveTerminal], ); const onNewTerminal = useCallback(() => { if (!activeWorkspaceId) { return; } + requestTerminalFocus(); createTerminal(activeWorkspaceId); - }, [activeWorkspaceId, createTerminal]); + }, [activeWorkspaceId, createTerminal, requestTerminalFocus]); const onCloseTerminal = useCallback( (terminalId: string) => { @@ -139,5 +146,6 @@ export function useTerminalController({ terminalState, ensureTerminalWithTitle, restartTerminalSession, + requestTerminalFocus, }; } diff --git a/src/features/terminal/hooks/useTerminalSession.ts b/src/features/terminal/hooks/useTerminalSession.ts index 3726bb9aa..ef68be830 100644 --- a/src/features/terminal/hooks/useTerminalSession.ts +++ b/src/features/terminal/hooks/useTerminalSession.ts @@ -23,6 +23,7 @@ type UseTerminalSessionOptions = { activeWorkspace: WorkspaceInfo | null; activeTerminalId: string | null; isVisible: boolean; + focusRequestVersion: number; onDebug?: (entry: DebugEntry) => void; onSessionExit?: (workspaceId: string, terminalId: string) => void; }; @@ -114,6 +115,7 @@ export function useTerminalSession({ activeWorkspace, activeTerminalId, isVisible, + focusRequestVersion, onDebug, onSessionExit, }: UseTerminalSessionOptions): TerminalSessionState { @@ -127,6 +129,7 @@ export function useTerminalSession({ const renderedKeyRef = useRef(null); const activeWorkspaceRef = useRef(null); const activeTerminalIdRef = useRef(null); + const pendingFocusRef = useRef(false); const [status, setStatus] = useState("idle"); const [message, setMessage] = useState("Open a terminal to start a session."); const [hasSession, setHasSession] = useState(false); @@ -165,6 +168,14 @@ export function useTerminalSession({ terminalRef.current?.write(data); }, []); + const focusTerminalIfRequested = useCallback(() => { + if (!pendingFocusRef.current) { + return; + } + pendingFocusRef.current = false; + terminalRef.current?.focus(); + }, []); + const refreshTerminal = useCallback(() => { const terminal = terminalRef.current; if (!terminal) { @@ -172,8 +183,8 @@ export function useTerminalSession({ } const lastRow = Math.max(0, terminal.rows - 1); terminal.refresh(0, lastRow); - terminal.focus(); - }, []); + focusTerminalIfRequested(); + }, [focusTerminalIfRequested]); const syncActiveBuffer = useCallback( (key: string) => { @@ -353,6 +364,14 @@ export function useTerminalSession({ sessionResetCounter, ]); + useEffect(() => { + if (!isVisible || focusRequestVersion === 0) { + return; + } + pendingFocusRef.current = true; + focusTerminalIfRequested(); + }, [focusRequestVersion, focusTerminalIfRequested, isVisible]); + useEffect(() => { if (!isVisible || !activeKey || !terminalRef.current || !fitAddonRef.current) { return;