From c16c9fd23406b839596362235945d41ee4241ea8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 19 Jun 2026 05:24:48 -0700 Subject: [PATCH] FN-6697: preserve terminal shortcut keyboard focus Preserve xterm focus while terminal shortcut buttons send control and literal input. - Prevent shortcut button mousedown from stealing focus away from the xterm helper textarea. - Refocus the terminal after modifier, literal, arrow, and Ctrl/Alt shortcut actions. - Add desktop and touch-primary regression coverage for shortcut byte delivery with hardware-keyboard focus. - Quarantine an unrelated QuickEntryBox focus-timing flake observed during workspace verification. Files changed: .../dashboard/app/components/TerminalModal.tsx | 79 ++++++++--- .../components/__tests__/TerminalModal.test.tsx | 151 +++++++++++++++++++++ packages/dashboard/vitest.config.ts | 9 +- scripts/lib/test-quarantine.json | 5 + 4 files changed, 225 insertions(+), 19 deletions(-) Fusion-Task-Id: FN-6697 Fusion-Task-Lineage: 558cfb44-ca48-4223-97d1-c3b6bc39b2fd --- .../app/components/TerminalModal.tsx | 79 ++++++--- .../__tests__/TerminalModal.test.tsx | 151 ++++++++++++++++++ packages/dashboard/vitest.config.ts | 9 +- scripts/lib/test-quarantine.json | 5 + 4 files changed, 225 insertions(+), 19 deletions(-) diff --git a/packages/dashboard/app/components/TerminalModal.tsx b/packages/dashboard/app/components/TerminalModal.tsx index fb1a3516cf..40fd5d5053 100644 --- a/packages/dashboard/app/components/TerminalModal.tsx +++ b/packages/dashboard/app/components/TerminalModal.tsx @@ -1,5 +1,12 @@ import "./TerminalModal.css"; -import { useState, useEffect, useRef, useCallback, type CSSProperties } from "react"; +import { + useState, + useEffect, + useRef, + useCallback, + type CSSProperties, + type MouseEvent as ReactMouseEvent, +} from "react"; import { useTranslation } from "react-i18next"; import { getErrorMessage } from "@fusion/core"; import { @@ -1246,35 +1253,65 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG [setFontSize], ); - const toggleModifier = useCallback((modifier: "ctrl" | "alt") => { - setStickyModifier((current) => (current === modifier ? null : modifier)); + /* + FNXC:Terminal 2026-06-19-05:05: + FN-6697 root cause: shortcut-bar buttons took browser focus on hardware-keyboard surfaces before their click handlers injected bytes, leaving xterm's helper textarea blurred even though the active session's sendInput path was correct. Preserve focus on mousedown and refocus xterm after every shortcut action so sticky modifiers, literal keys, arrows, and Ctrl-letter shortcuts deliver input without stranding subsequent hardware-keyboard typing across desktop and touch surfaces. + */ + const preserveShortcutFocus = useCallback((event: ReactMouseEvent) => { + event.preventDefault(); }, []); + const refocusTerminalAfterShortcut = useCallback(() => { + xtermRef.current?.focus(); + handleTerminalGestureFocus(); + }, [handleTerminalGestureFocus]); + + const runShortcutAction = useCallback( + (action: () => void) => { + action(); + refocusTerminalAfterShortcut(); + }, + [refocusTerminalAfterShortcut], + ); + + const toggleModifier = useCallback( + (modifier: "ctrl" | "alt") => { + runShortcutAction(() => { + setStickyModifier((current) => (current === modifier ? null : modifier)); + }); + }, + [runShortcutAction], + ); + const sendShortcutKey = useCallback( (key: string) => { - if (stickyModifier === "ctrl") { - sendInput(ctrlChar(key)); - setStickyModifier(null); - return; - } + runShortcutAction(() => { + if (stickyModifier === "ctrl") { + sendInput(ctrlChar(key)); + setStickyModifier(null); + return; + } - if (stickyModifier === "alt") { - sendInput(altChar(key)); - setStickyModifier(null); - return; - } + if (stickyModifier === "alt") { + sendInput(altChar(key)); + setStickyModifier(null); + return; + } - sendInput(key); + sendInput(key); + }); }, - [sendInput, stickyModifier], + [runShortcutAction, sendInput, stickyModifier], ); const sendLiteralShortcut = useCallback( (value: string) => { - sendInput(value); - setStickyModifier(null); + runShortcutAction(() => { + sendInput(value); + setStickyModifier(null); + }); }, - [sendInput], + [runShortcutAction, sendInput], ); if (!isOpen) return null; @@ -1532,6 +1569,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG stickyModifier === "ctrl" ? "is-active" : "" }`} data-testid="terminal-modifier-ctrl" + onMouseDown={preserveShortcutFocus} onClick={() => toggleModifier("ctrl")} aria-pressed={stickyModifier === "ctrl"} > @@ -1543,6 +1581,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG stickyModifier === "alt" ? "is-active" : "" }`} data-testid="terminal-modifier-alt" + onMouseDown={preserveShortcutFocus} onClick={() => toggleModifier("alt")} aria-pressed={stickyModifier === "alt"} > @@ -1551,6 +1590,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG