Fix bug where users are unable to re-enter disconnected terminals. (#8765)

This commit is contained in:
Jacob Richman
2025-09-20 10:59:37 -07:00
committed by GitHub
parent 2216856e3c
commit 375b8522fc
15 changed files with 267 additions and 55 deletions

View File

@@ -7,12 +7,16 @@
import { render } from 'ink-testing-library';
import type React from 'react';
import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
import { ShellFocusContext } from '../ui/contexts/ShellFocusContext.js';
export const renderWithProviders = (
component: React.ReactElement,
{ shellFocus = true } = {},
): ReturnType<typeof render> =>
render(
<KeypressProvider kittyProtocolEnabled={true}>
{component}
</KeypressProvider>,
<ShellFocusContext.Provider value={shellFocus}>
<KeypressProvider kittyProtocolEnabled={true}>
{component}
</KeypressProvider>
</ShellFocusContext.Provider>,
);

View File

@@ -86,7 +86,7 @@ import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
import { useSessionStats } from './contexts/SessionContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
import { useExtensionUpdates } from './hooks/useExtensionUpdates.js';
import { FocusContext } from './contexts/FocusContext.js';
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
@@ -135,7 +135,7 @@ export const AppContainer = (props: AppContainerProps) => {
initializationResult.themeError,
);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [shellFocused, setShellFocused] = useState(false);
const [embeddedShellFocused, setEmbeddedShellFocused] = useState(false);
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(
initializationResult.geminiMdFileCount,
@@ -557,10 +557,10 @@ Logging in with Google... Please restart Gemini CLI to continue.
setModelSwitchedFromQuotaError,
refreshStatic,
() => cancelHandlerRef.current(),
setShellFocused,
setEmbeddedShellFocused,
terminalWidth,
terminalHeight,
shellFocused,
embeddedShellFocused,
);
// Auto-accept indicator
@@ -917,8 +917,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
) {
setConstrainHeight(false);
} else if (keyMatchers[Command.TOGGLE_SHELL_INPUT_FOCUS](key)) {
if (activePtyId || shellFocused) {
setShellFocused((prev) => !prev);
if (activePtyId || embeddedShellFocused) {
setEmbeddedShellFocused((prev) => !prev);
}
}
},
@@ -941,7 +941,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
handleSlashCommand,
cancelOngoingRequest,
activePtyId,
shellFocused,
embeddedShellFocused,
settings.merged.general?.debugKeystrokeLogging,
],
);
@@ -1069,7 +1069,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
isRestarting,
extensionsUpdateState,
activePtyId,
shellFocused,
embeddedShellFocused,
}),
[
historyManager.history,
@@ -1145,7 +1145,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
currentModel,
extensionsUpdateState,
activePtyId,
shellFocused,
embeddedShellFocused,
],
);
@@ -1207,9 +1207,9 @@ Logging in with Google... Please restart Gemini CLI to continue.
startupWarnings: props.startupWarnings || [],
}}
>
<FocusContext.Provider value={isFocused}>
<ShellFocusContext.Provider value={isFocused}>
<App />
</FocusContext.Provider>
</ShellFocusContext.Provider>
</AppContext.Provider>
</ConfigContext.Provider>
</UIActionsContext.Provider>

View File

@@ -19,7 +19,6 @@ import { OverflowProvider } from '../contexts/OverflowContext.js';
import { theme } from '../semantic-colors.js';
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useFocusState } from '../contexts/FocusContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
@@ -32,7 +31,6 @@ export const Composer = () => {
const config = useConfig();
const settings = useSettings();
const uiState = useUIState();
const isFocused = useFocusState();
const uiActions = useUIActions();
const { vimEnabled, vimMode } = useVimMode();
const terminalWidth = process.stdout.columns;
@@ -69,7 +67,7 @@ export const Composer = () => {
return (
<Box flexDirection="column">
{!uiState.shellFocused && (
{!uiState.embeddedShellFocused && (
<LoadingIndicator
thought={
uiState.streamingState === StreamingState.WaitingForConfirmation ||
@@ -167,9 +165,9 @@ export const Composer = () => {
setShellModeActive={uiActions.setShellModeActive}
approvalMode={showAutoAcceptIndicator}
onEscapePromptChange={uiActions.onEscapePromptChange}
focus={isFocused}
focus={true}
vimHandleInput={uiActions.vimHandleInput}
isShellFocused={uiState.shellFocused}
isEmbeddedShellFocused={uiState.embeddedShellFocused}
placeholder={
vimEnabled
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."

View File

@@ -33,7 +33,7 @@ interface HistoryItemDisplayProps {
isFocused?: boolean;
commands?: readonly SlashCommand[];
activeShellPtyId?: number | null;
shellFocused?: boolean;
embeddedShellFocused?: boolean;
}
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
@@ -44,7 +44,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
commands,
isFocused = true,
activeShellPtyId,
shellFocused,
embeddedShellFocused,
}) => (
<Box flexDirection="column" key={item.id}>
{/* Render standard message types */}
@@ -93,7 +93,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
terminalWidth={terminalWidth}
isFocused={isFocused}
activeShellPtyId={activeShellPtyId}
shellFocused={shellFocused}
embeddedShellFocused={embeddedShellFocused}
/>
)}
{item.type === 'compression' && (

View File

@@ -1336,6 +1336,128 @@ describe('InputPrompt', () => {
expect(frame).toContain(`hello 👍${chalk.inverse(' ')}`);
unmount();
});
it('should display cursor on an empty line', async () => {
mockBuffer.text = '';
mockBuffer.lines = [''];
mockBuffer.viewportVisualLines = [''];
mockBuffer.visualCursor = [0, 0];
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const frame = stdout.lastFrame();
expect(frame).toContain(chalk.inverse(' '));
unmount();
});
it('should display cursor on a space between words', async () => {
mockBuffer.text = 'hello world';
mockBuffer.lines = ['hello world'];
mockBuffer.viewportVisualLines = ['hello world'];
mockBuffer.visualCursor = [0, 5]; // cursor on the space
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const frame = stdout.lastFrame();
expect(frame).toContain(`hello${chalk.inverse(' ')}world`);
unmount();
});
it('should display cursor in the middle of a line in a multiline block', async () => {
const text = 'first line\nsecond line\nthird line';
mockBuffer.text = text;
mockBuffer.lines = text.split('\n');
mockBuffer.viewportVisualLines = text.split('\n');
mockBuffer.visualCursor = [1, 3]; // cursor on 'o' in 'second'
mockBuffer.visualToLogicalMap = [
[0, 0],
[1, 0],
[2, 0],
];
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const frame = stdout.lastFrame();
expect(frame).toContain(`sec${chalk.inverse('o')}nd line`);
unmount();
});
it('should display cursor at the beginning of a line in a multiline block', async () => {
const text = 'first line\nsecond line';
mockBuffer.text = text;
mockBuffer.lines = text.split('\n');
mockBuffer.viewportVisualLines = text.split('\n');
mockBuffer.visualCursor = [1, 0]; // cursor on 's' in 'second'
mockBuffer.visualToLogicalMap = [
[0, 0],
[1, 0],
];
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const frame = stdout.lastFrame();
expect(frame).toContain(`${chalk.inverse('s')}econd line`);
unmount();
});
it('should display cursor at the end of a line in a multiline block', async () => {
const text = 'first line\nsecond line';
mockBuffer.text = text;
mockBuffer.lines = text.split('\n');
mockBuffer.viewportVisualLines = text.split('\n');
mockBuffer.visualCursor = [0, 10]; // cursor after 'first line'
mockBuffer.visualToLogicalMap = [
[0, 0],
[1, 0],
];
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const frame = stdout.lastFrame();
expect(frame).toContain(`first line${chalk.inverse(' ')}`);
unmount();
});
it('should display cursor on a blank line in a multiline block', async () => {
const text = 'first line\n\nthird line';
mockBuffer.text = text;
mockBuffer.lines = text.split('\n');
mockBuffer.viewportVisualLines = text.split('\n');
mockBuffer.visualCursor = [1, 0]; // cursor on the blank line
mockBuffer.visualToLogicalMap = [
[0, 0],
[1, 0],
[2, 0],
];
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const frame = stdout.lastFrame();
const lines = frame!.split('\n');
// The line with the cursor should just be an inverted space inside the box border
expect(
lines.find((l) => l.includes(chalk.inverse(' '))),
).not.toBeUndefined();
unmount();
});
});
describe('multiline rendering', () => {
@@ -1966,6 +2088,33 @@ describe('InputPrompt', () => {
expect(stdout.lastFrame()).toMatchSnapshot();
unmount();
});
it('should not show inverted cursor when shell is focused', async () => {
props.isEmbeddedShellFocused = true;
props.focus = false;
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
// This snapshot is good to make sure there was an input prompt but does
// not show the inverted cursor because snapshots do not show colors.
expect(stdout.lastFrame()).toMatchSnapshot();
unmount();
});
});
it('should still allow input when shell is not focused', async () => {
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
shellFocus: false,
});
await wait();
stdin.write('a');
await wait();
expect(mockBuffer.handleInput).toHaveBeenCalled();
unmount();
});
});
function clean(str: string | undefined): string {

View File

@@ -35,6 +35,7 @@ import {
} from '../utils/clipboardUtils.js';
import * as path from 'node:path';
import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
export interface InputPromptProps {
buffer: TextBuffer;
onSubmit: (value: string) => void;
@@ -52,7 +53,7 @@ export interface InputPromptProps {
approvalMode: ApprovalMode;
onEscapePromptChange?: (showPrompt: boolean) => void;
vimHandleInput?: (key: Key) => boolean;
isShellFocused?: boolean;
isEmbeddedShellFocused?: boolean;
}
// The input content, input container, and input suggestions list may have different widths
@@ -97,8 +98,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
approvalMode,
onEscapePromptChange,
vimHandleInput,
isShellFocused,
isEmbeddedShellFocused,
}) => {
const isShellFocused = useShellFocusState();
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const [escPressCount, setEscPressCount] = useState(0);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
@@ -154,6 +156,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const resetCommandSearchCompletionState =
commandSearchCompletion.resetCompletionState;
const showCursor = focus && isShellFocused && !isEmbeddedShellFocused;
const resetEscapeState = useCallback(() => {
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
@@ -291,6 +295,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const handleInput = useCallback(
(key: Key) => {
// TODO(jacobr): this special case is likely not needed anymore.
// We should probably stop supporting paste if the InputPrompt is not
// focused.
/// We want to handle paste even when not focused to support drag and drop.
if (!focus && !key.paste) {
return;
@@ -689,9 +696,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
],
);
useKeypress(handleInput, {
isActive: !isShellFocused,
});
useKeypress(handleInput, { isActive: !isEmbeddedShellFocused });
const linesToRender = buffer.viewportVisualLines;
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
@@ -842,7 +847,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
<Box
borderStyle="round"
borderColor={
statusColor ?? (focus ? theme.border.focused : theme.border.default)
isShellFocused && !isEmbeddedShellFocused
? (statusColor ?? theme.border.focused)
: theme.border.default
}
paddingX={1}
>
@@ -871,7 +878,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
</Text>
<Box flexGrow={1} flexDirection="column">
{buffer.text.length === 0 && placeholder ? (
focus ? (
showCursor ? (
<Text>
{chalk.inverse(placeholder.slice(0, 1))}
<Text color={theme.text.secondary}>{placeholder.slice(1)}</Text>
@@ -926,7 +933,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
relativeVisualColForHighlight - segStart,
relativeVisualColForHighlight - segStart + 1,
);
const highlighted = chalk.inverse(charToHighlight);
const highlighted = showCursor
? chalk.inverse(charToHighlight)
: charToHighlight;
display =
cpSlice(
seg.text,
@@ -962,7 +971,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
if (!currentLineGhost) {
renderedLine.push(
<Text key={`cursor-end-${cursorVisualColAbsolute}`}>
{chalk.inverse(' ')}
{showCursor ? chalk.inverse(' ') : ' '}
</Text>,
);
}
@@ -978,7 +987,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
<Box key={`line-${visualIdxInRenderedSet}`} height={1}>
<Text>
{renderedLine}
{showCursorBeforeGhost && chalk.inverse(' ')}
{showCursorBeforeGhost &&
(showCursor ? chalk.inverse(' ') : ' ')}
{currentLineGhost && (
<Text color={theme.text.secondary}>
{currentLineGhost}

View File

@@ -55,7 +55,7 @@ export const MainContent = () => {
isPending={true}
isFocused={!uiState.isEditorDialogOpen}
activeShellPtyId={uiState.activePtyId}
shellFocused={uiState.shellFocused}
embeddedShellFocused={uiState.embeddedShellFocused}
/>
))}
<ShowMoreLines constrainHeight={uiState.constrainHeight} />

View File

@@ -32,6 +32,12 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match
git commit -m "feat: add search" in src/app"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ > Type your message or @path/to/file │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ! Type your message or @path/to/file │

View File

@@ -22,7 +22,7 @@ interface ToolGroupMessageProps {
terminalWidth: number;
isFocused?: boolean;
activeShellPtyId?: number | null;
shellFocused?: boolean;
embeddedShellFocused?: boolean;
onShellInputSubmit?: (input: string) => void;
}
@@ -33,10 +33,10 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
terminalWidth,
isFocused = true,
activeShellPtyId,
shellFocused,
embeddedShellFocused,
}) => {
const isShellFocused =
shellFocused &&
const isEmbeddedShellFocused =
embeddedShellFocused &&
toolCalls.some(
(t) =>
t.ptyId === activeShellPtyId && t.status === ToolCallStatus.Executing,
@@ -51,7 +51,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
);
const borderColor =
isShellCommand || isShellFocused
isShellCommand || isEmbeddedShellFocused
? theme.ui.symbol
: hasPending
? theme.status.warning
@@ -98,7 +98,9 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
*/
width="100%"
marginLeft={1}
borderDimColor={hasPending}
borderDimColor={
hasPending && (!isShellCommand || !isEmbeddedShellFocused)
}
borderColor={borderColor}
gap={1}
>
@@ -119,7 +121,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
: 'medium'
}
activeShellPtyId={activeShellPtyId}
shellFocused={shellFocused}
embeddedShellFocused={embeddedShellFocused}
config={config}
/>
</Box>

View File

@@ -38,7 +38,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
emphasis?: TextEmphasis;
renderOutputAsMarkdown?: boolean;
activeShellPtyId?: number | null;
shellFocused?: boolean;
embeddedShellFocused?: boolean;
config?: Config;
}
@@ -52,7 +52,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
emphasis = 'medium',
renderOutputAsMarkdown = true,
activeShellPtyId,
shellFocused,
embeddedShellFocused,
ptyId,
config,
}) => {
@@ -60,7 +60,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
status === ToolCallStatus.Executing &&
ptyId === activeShellPtyId &&
shellFocused;
embeddedShellFocused;
const isThisShellFocusable =
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
@@ -149,7 +149,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
<ShellInputPrompt
activeShellPtyId={activeShellPtyId ?? null}
focus={shellFocused}
focus={embeddedShellFocused}
/>
</Box>
)}

View File

@@ -388,6 +388,9 @@ export function KeypressProvider({
};
const handleKeypress = (_: unknown, key: Key) => {
if (key.sequence === FOCUS_IN || key.sequence === FOCUS_OUT) {
return;
}
if (key.name === 'paste-start') {
isPaste = true;
return;

View File

@@ -6,6 +6,6 @@
import { createContext, useContext } from 'react';
export const FocusContext = createContext<boolean>(true);
export const ShellFocusContext = createContext<boolean>(true);
export const useFocusState = () => useContext(FocusContext);
export const useShellFocusState = () => useContext(ShellFocusContext);

View File

@@ -110,7 +110,7 @@ export interface UIState {
isRestarting: boolean;
extensionsUpdateState: Map<string, ExtensionUpdateState>;
activePtyId: number | undefined;
shellFocused: boolean;
embeddedShellFocused: boolean;
}
export const UIStateContext = createContext<UIState | null>(null);

View File

@@ -9,6 +9,8 @@ import { EventEmitter } from 'node:events';
import { useFocus } from './useFocus.js';
import { vi } from 'vitest';
import { useStdin, useStdout } from 'ink';
import { KeypressProvider } from '../contexts/KeypressContext.js';
import React from 'react';
// Mock the ink hooks
vi.mock('ink', async (importOriginal) => {
@@ -23,12 +25,17 @@ vi.mock('ink', async (importOriginal) => {
const mockedUseStdin = vi.mocked(useStdin);
const mockedUseStdout = vi.mocked(useStdout);
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(KeypressProvider, null, children);
describe('useFocus', () => {
let stdin: EventEmitter;
let stdout: { write: vi.Func };
beforeEach(() => {
stdin = new EventEmitter();
stdin.resume = vi.fn();
stdin.pause = vi.fn();
stdout = { write: vi.fn() };
mockedUseStdin.mockReturnValue({ stdin } as ReturnType<typeof useStdin>);
mockedUseStdout.mockReturnValue({ stdout } as unknown as ReturnType<
@@ -38,17 +45,18 @@ describe('useFocus', () => {
afterEach(() => {
vi.clearAllMocks();
stdin.removeAllListeners();
});
it('should initialize with focus and enable focus reporting', () => {
const { result } = renderHook(() => useFocus());
const { result } = renderHook(() => useFocus(), { wrapper });
expect(result.current).toBe(true);
expect(stdout.write).toHaveBeenCalledWith('\x1b[?1004h');
});
it('should set isFocused to false when a focus-out event is received', () => {
const { result } = renderHook(() => useFocus());
const { result } = renderHook(() => useFocus(), { wrapper });
// Initial state is focused
expect(result.current).toBe(true);
@@ -63,7 +71,7 @@ describe('useFocus', () => {
});
it('should set isFocused to true when a focus-in event is received', () => {
const { result } = renderHook(() => useFocus());
const { result } = renderHook(() => useFocus(), { wrapper });
// Simulate focus-out to set initial state to false
act(() => {
@@ -81,20 +89,22 @@ describe('useFocus', () => {
});
it('should clean up and disable focus reporting on unmount', () => {
const { unmount } = renderHook(() => useFocus());
const { unmount } = renderHook(() => useFocus(), { wrapper });
// Ensure listener was attached
expect(stdin.listenerCount('data')).toBe(1);
// At this point we should have listeners from both KeypressProvider and useFocus
const listenerCountAfterMount = stdin.listenerCount('data');
expect(listenerCountAfterMount).toBeGreaterThanOrEqual(1);
unmount();
// Assert that the cleanup function was called
expect(stdout.write).toHaveBeenCalledWith('\x1b[?1004l');
expect(stdin.listenerCount('data')).toBe(0);
// Ensure useFocus listener was removed (but KeypressProvider listeners may remain)
expect(stdin.listenerCount('data')).toBeLessThan(listenerCountAfterMount);
});
it('should handle multiple focus events correctly', () => {
const { result } = renderHook(() => useFocus());
const { result } = renderHook(() => useFocus(), { wrapper });
act(() => {
stdin.emit('data', Buffer.from('\x1b[O'));
@@ -116,4 +126,20 @@ describe('useFocus', () => {
});
expect(result.current).toBe(true);
});
it('restores focus on keypress after focus is lost', () => {
const { result } = renderHook(() => useFocus(), { wrapper });
// Simulate focus-out event
act(() => {
stdin.emit('data', Buffer.from('\x1b[O'));
});
expect(result.current).toBe(false);
// Simulate a keypress
act(() => {
stdin.emit('data', Buffer.from('a'));
});
expect(result.current).toBe(true);
});
});

View File

@@ -6,6 +6,7 @@
import { useStdin, useStdout } from 'ink';
import { useEffect, useState } from 'react';
import { useKeypress } from './useKeypress.js';
// ANSI escape codes to enable/disable terminal focus reporting
export const ENABLE_FOCUS_REPORTING = '\x1b[?1004h';
@@ -44,5 +45,18 @@ export const useFocus = () => {
};
}, [stdin, stdout]);
useKeypress(
(_) => {
if (!isFocused) {
// If the user has typed a key, and we cannot possibly be focused out.
// This is a workaround for some tmux use cases. It is still useful to
// listen for the true FOCUS_IN event as well as that will update the
// focus state earlier than waiting for a keypress.
setIsFocused(true);
}
},
{ isActive: true },
);
return isFocused;
};