Keep Android terminal keyboard input stable (#2830)
* fix(app): stabilize native terminal keyboard input Terminal focus requests blurred and remounted the hidden input even when the software keyboard was already visible, causing visible IME cycles and leaving clipboard commits coupled to stale native text. Keep focus requests idempotent while resetting only the native input buffer. * test(app): remove misleading keyboard device scenario Agent Device cannot durably assert software-keyboard continuity or commit a keyboard clipboard suggestion, so the scenario overstated coverage already owned by focused input-policy tests. * fix(app): derive terminal keyboard focus from visibility Use the keyboard-controller inset as the source of truth so a system-dismissed keyboard cannot leave focus activation suppressed. Avoid a duplicate focus request while explicitly showing the keyboard. * fix(app): track terminal keyboard visibility across layouts Keep keyboard visibility independent from compact-only terminal padding so tablet taps remain idempotent while the software keyboard is open.
This commit is contained in:
@@ -34,6 +34,7 @@ export interface TerminalEmulatorProps {
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
keyboardInset?: number;
|
||||
isKeyboardVisible?: boolean;
|
||||
swipeGesturesEnabled?: boolean;
|
||||
onSwipeLeft?: () => void;
|
||||
onSwipeRight?: () => void;
|
||||
|
||||
@@ -168,6 +168,7 @@ function NativeTerminalEmulator({
|
||||
fontFamily,
|
||||
fontSize,
|
||||
keyboardInset = 0,
|
||||
isKeyboardVisible = false,
|
||||
initialSnapshot = null,
|
||||
onInput,
|
||||
onTerminalKey,
|
||||
@@ -993,6 +994,7 @@ function NativeTerminalEmulator({
|
||||
{terminalGrid}
|
||||
<TerminalInput
|
||||
ref={inputRef}
|
||||
isKeyboardVisible={isKeyboardVisible}
|
||||
onFocus={handleTerminalFocus}
|
||||
onInput={handleTerminalInput}
|
||||
onTerminalKey={handleNativeTerminalKey}
|
||||
|
||||
@@ -106,6 +106,7 @@ interface TerminalEmulatorProps {
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
keyboardInset?: number;
|
||||
isKeyboardVisible?: boolean;
|
||||
swipeGesturesEnabled?: boolean;
|
||||
onSwipeLeft?: () => void;
|
||||
onSwipeRight?: () => void;
|
||||
|
||||
@@ -222,6 +222,7 @@ export function TerminalPane({
|
||||
enabled: isMobile,
|
||||
});
|
||||
const [keyboardInset, setKeyboardInset] = useState(0);
|
||||
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
||||
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
@@ -298,8 +299,8 @@ export function TerminalPane({
|
||||
}, [keyboardInset, refreshClipboardAvailability]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsKeyboardToggleVisible(keyboardInset > 0);
|
||||
}, [keyboardInset]);
|
||||
setIsKeyboardToggleVisible(isKeyboardVisible);
|
||||
}, [isKeyboardVisible]);
|
||||
|
||||
const handleSelectionChange = useCallback((nextHasSelection: boolean) => {
|
||||
setHasSelection(nextHasSelection);
|
||||
@@ -418,12 +419,13 @@ export function TerminalPane({
|
||||
);
|
||||
}, [clearKeyboardRefitTimeouts, requestTerminalReflow]);
|
||||
|
||||
const handleKeyboardInsetChange = useCallback(
|
||||
(nextInset: number) => {
|
||||
setKeyboardInset(nextInset);
|
||||
const handleKeyboardChange = useCallback(
|
||||
(nextShift: number) => {
|
||||
setKeyboardInset(isMobile ? nextShift : 0);
|
||||
setIsKeyboardVisible(nextShift > 0);
|
||||
pulseKeyboardRefits();
|
||||
},
|
||||
[pulseKeyboardRefits],
|
||||
[isMobile, pulseKeyboardRefits],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -431,14 +433,14 @@ export function TerminalPane({
|
||||
}, [clearKeyboardRefitTimeouts]);
|
||||
|
||||
useAnimatedReaction(
|
||||
() => (isMobile ? Math.round(keyboardShift.value) : 0),
|
||||
() => Math.round(keyboardShift.value),
|
||||
(next, prev) => {
|
||||
if (next === prev) {
|
||||
return;
|
||||
}
|
||||
runOnJS(handleKeyboardInsetChange)(next);
|
||||
runOnJS(handleKeyboardChange)(next);
|
||||
},
|
||||
[isMobile, handleKeyboardInsetChange],
|
||||
[handleKeyboardChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -842,9 +844,8 @@ export function TerminalPane({
|
||||
|
||||
setIsKeyboardToggleVisible(true);
|
||||
emulatorRef.current?.showKeyboard();
|
||||
requestTerminalFocus();
|
||||
requestTerminalReflow();
|
||||
}, [isKeyboardToggleVisible, requestTerminalFocus, requestTerminalReflow]);
|
||||
}, [isKeyboardToggleVisible, requestTerminalReflow]);
|
||||
|
||||
const handleInputModeChange = useCallback((state: TerminalInputModeState) => {
|
||||
inputModeRef.current = state;
|
||||
@@ -1014,6 +1015,7 @@ export function TerminalPane({
|
||||
fontFamily={terminalFontFamily}
|
||||
fontSize={settings.codeFontSize}
|
||||
keyboardInset={keyboardInset}
|
||||
isKeyboardVisible={isKeyboardVisible}
|
||||
swipeGesturesEnabled={swipeGesturesEnabled}
|
||||
initialSnapshot={initialSnapshot}
|
||||
onRendererReadyChange={handleRendererReadyChange}
|
||||
|
||||
Reference in New Issue
Block a user