perf: clean mobile live input focus timer (#4050)

This commit is contained in:
Neil 2026-05-31 02:36:49 -07:00 committed by GitHub
parent b032289056
commit fcd267a309
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 4 deletions

View File

@ -70,8 +70,10 @@ import {
loadTerminalAccessoryLayout
} from '../../../../src/terminal/terminal-accessory-layout'
import {
clearTerminalLiveInputFocusTimer,
getTerminalLiveSpecialKeyBytes,
isTerminalLiveInputWithinByteLimit
isTerminalLiveInputWithinByteLimit,
scheduleTerminalLiveInputFocus
} from '../../../../src/terminal/terminal-live-input'
import { countTerminalGestureInputSequences } from '../../../../src/terminal/terminal-gesture-input'
import { MobileBrowserPane, type MobileBrowserTab } from '../../../../src/browser/MobileBrowserPane'
@ -1048,6 +1050,7 @@ export default function SessionScreen() {
const viewportMeasuredRef = useRef(false)
const terminalRefs = useRef<Map<string, TerminalWebViewHandle>>(new Map())
const liveInputRef = useRef<TextInput>(null)
const liveInputFocusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const terminalUnsubsRef = useRef<Map<string, () => void>>(new Map())
const subscribingHandlesRef = useRef<Set<string>>(new Set())
const initializedHandlesRef = useRef<Set<string>>(new Set())
@ -1132,6 +1135,7 @@ export default function SessionScreen() {
// so pending animation callbacks cannot clear a newer/unmounted surface.
toastSeqRef.current += 1
clearToastHideTimer()
clearTerminalLiveInputFocusTimer(liveInputFocusTimerRef)
}
}, [clearToastHideTimer])
@ -2714,8 +2718,9 @@ export default function SessionScreen() {
})
setLiveInputCapture('')
if (nextEnabled) {
setTimeout(() => liveInputRef.current?.focus(), 50)
scheduleTerminalLiveInputFocus(liveInputFocusTimerRef, () => liveInputRef.current?.focus())
} else {
clearTerminalLiveInputFocusTimer(liveInputFocusTimerRef)
liveInputRef.current?.blur()
}
}, [activeHandle, liveInputTerminalHandles])

View File

@ -1,11 +1,22 @@
import { describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
TERMINAL_LIVE_INPUT_MAX_BYTES,
clearTerminalLiveInputFocusTimer,
getTerminalLiveSpecialKeyBytes,
isTerminalLiveInputWithinByteLimit
isTerminalLiveInputWithinByteLimit,
scheduleTerminalLiveInputFocus,
type TerminalLiveInputFocusTimerRef
} from './terminal-live-input'
function createTimerRef(): TerminalLiveInputFocusTimerRef {
return { current: null }
}
describe('terminal live input', () => {
afterEach(() => {
vi.useRealTimers()
})
it('maps phone keyboard special keys to PTY bytes', () => {
expect(getTerminalLiveSpecialKeyBytes('Backspace')).toBe('\x7f')
expect(getTerminalLiveSpecialKeyBytes('Enter')).toBeNull()
@ -22,4 +33,32 @@ describe('terminal live input', () => {
isTerminalLiveInputWithinByteLimit('é'.repeat(TERMINAL_LIVE_INPUT_MAX_BYTES / 2 + 1))
).toBe(false)
})
it('replaces pending deferred focus work', () => {
vi.useFakeTimers()
const timerRef = createTimerRef()
const staleFocus = vi.fn()
const nextFocus = vi.fn()
scheduleTerminalLiveInputFocus(timerRef, staleFocus)
scheduleTerminalLiveInputFocus(timerRef, nextFocus)
vi.runOnlyPendingTimers()
expect(staleFocus).not.toHaveBeenCalled()
expect(nextFocus).toHaveBeenCalledTimes(1)
expect(timerRef.current).toBeNull()
})
it('clears pending deferred focus work', () => {
vi.useFakeTimers()
const timerRef = createTimerRef()
const focus = vi.fn()
scheduleTerminalLiveInputFocus(timerRef, focus)
clearTerminalLiveInputFocusTimer(timerRef)
vi.runOnlyPendingTimers()
expect(focus).not.toHaveBeenCalled()
expect(timerRef.current).toBeNull()
})
})

View File

@ -2,6 +2,10 @@ const TERMINAL_LIVE_INPUT_MAX_BYTES = 256 * 1024
const encoder = new TextEncoder()
export type TerminalLiveInputFocusTimerRef = {
current: ReturnType<typeof setTimeout> | null
}
export function getTerminalLiveSpecialKeyBytes(key: string): string | null {
if (key === 'Backspace') {
return '\x7f'
@ -16,4 +20,26 @@ export function isTerminalLiveInputWithinByteLimit(
return encoder.encode(text).byteLength <= maxBytes
}
export function clearTerminalLiveInputFocusTimer(timerRef: TerminalLiveInputFocusTimerRef): void {
if (timerRef.current === null) {
return
}
clearTimeout(timerRef.current)
timerRef.current = null
}
export function scheduleTerminalLiveInputFocus(
timerRef: TerminalLiveInputFocusTimerRef,
focus: () => void,
delayMs = 50
): void {
// Why: live input can be toggled during route changes; replacing the pending
// focus timer prevents stale native TextInput focus after unmount/disable.
clearTerminalLiveInputFocusTimer(timerRef)
timerRef.current = setTimeout(() => {
timerRef.current = null
focus()
}, delayMs)
}
export { TERMINAL_LIVE_INPUT_MAX_BYTES }