Files
orca/mobile/src/session/TerminalPaneView.tsx
T
92a754a088 fix(mobile): suspend hidden and background terminal gestures (#31468)
* fix(mobile): suspend gestures in hidden and background terminals

Adapt the retained-pane interaction lifecycle from community PR #30324.
Restore policy across readiness/recovery and keep output/query replies flowing.
Release cancelled gesture frames, discard touches interrupted by suspension,
and restore the resting pinch transform on every gesture cancellation.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Neil <nwparker@users.noreply.github.com>

* test(mobile): stub route focus where the web mount renders terminal panes

* test(mobile): send production-shaped file taps and the clipboard prop in activity tests

* fix(mobile): keep inactive and interrupted terminal moves away from xterm

---------

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Neil <nwparker@users.noreply.github.com>
2026-10-28 18:53:58 +00:00

139 lines
4.4 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react'
import { AppState, StyleSheet, View } from 'react-native'
import { useFocusEffect } from 'expo-router'
import type { ParsedFileLinkLocation } from '../../../src/shared/file-link-location'
import { TerminalWebView } from '../terminal/TerminalWebView'
import type {
MobileTerminalTheme,
TerminalKeyboardAvoidanceMetrics,
TerminalModes,
TerminalPointerInput,
TerminalWebViewHandle
} from '../terminal/terminal-webview-contract'
type TerminalPaneViewProps = {
handle: string
active: boolean
keyboardLift: number
terminalTheme?: MobileTerminalTheme
textScale: number
onRef: (handle: string, ref: TerminalWebViewHandle | null) => void
onWebReady: (handle: string) => void
onSelectionMode: (handle: string, active: boolean) => void
onSelectionCopy: (handle: string, text: string) => void
onSelectionEvicted: (handle: string) => void
onClipboardWrite: (handle: string, text: string) => void
onModesChanged: (handle: string, modes: TerminalModes) => void
onKeyboardAvoidanceMetrics: (handle: string, metrics: TerminalKeyboardAvoidanceMetrics) => void
onHaptic: (kind: 'selection' | 'success' | 'error' | 'edge-bump') => void
onTerminalInput: (handle: string, bytes: string, input?: TerminalPointerInput) => void
onTerminalQueryReply: (handle: string, bytes: string) => void
onTerminalTap: (handle: string) => void
onFileTap: (handle: string, candidates: readonly ParsedFileLinkLocation[]) => void
onOpenUrl: (handle: string, url: string) => void
onTextScaleChange: (scale: number) => void
onCellBoxChange: (handle: string) => void
}
export function TerminalPaneView({
handle,
active,
keyboardLift,
terminalTheme,
textScale,
onRef,
onWebReady,
onSelectionMode,
onSelectionCopy,
onSelectionEvicted,
onClipboardWrite,
onModesChanged,
onKeyboardAvoidanceMetrics,
onHaptic,
onTerminalInput,
onTerminalQueryReply,
onTerminalTap,
onFileTap,
onOpenUrl,
onTextScaleChange,
onCellBoxChange
}: TerminalPaneViewProps) {
const [visited, setVisited] = useState(active)
if (active && !visited) {
setVisited(true)
}
const [routeFocused, setRouteFocused] = useState(false)
useFocusEffect(
useCallback(() => {
setRouteFocused(true)
return () => setRouteFocused(false)
}, [])
)
const [appActive, setAppActive] = useState(() => AppState.currentState === 'active')
useEffect(() => {
const subscription = AppState.addEventListener('change', (state) =>
setAppActive(state === 'active')
)
return () => subscription.remove()
}, [])
const setRef = useCallback(
(ref: TerminalWebViewHandle | null) => {
onRef(handle, ref)
},
[handle, onRef]
)
if (!active && !visited) {
return null
}
return (
<View
// Why: inactive terminal WebViews stay mounted to preserve xterm state,
// while touch and visibility are disabled until the tab is active again.
pointerEvents={active ? 'auto' : 'none'}
style={[
styles.terminalPane,
keyboardLift > 0 && { transform: [{ translateY: -keyboardLift }] },
!active && styles.terminalPaneHidden
]}
>
<TerminalWebView
ref={setRef}
style={styles.terminalWebView}
terminalTheme={terminalTheme}
textScale={textScale}
shownAtMount={active}
interactionActive={active && appActive && routeFocused}
onWebReady={() => onWebReady(handle)}
onSelectionMode={(a) => onSelectionMode(handle, a)}
onSelectionCopy={(t) => onSelectionCopy(handle, t)}
onSelectionEvicted={() => onSelectionEvicted(handle)}
onClipboardWrite={(t) => onClipboardWrite(handle, t)}
onModesChanged={(m) => onModesChanged(handle, m)}
onKeyboardAvoidanceMetrics={(m) => onKeyboardAvoidanceMetrics(handle, m)}
onHaptic={onHaptic}
onTerminalInput={(bytes, input) => onTerminalInput(handle, bytes, input)}
onTerminalQueryReply={(bytes) => onTerminalQueryReply(handle, bytes)}
onTerminalTap={() => onTerminalTap(handle)}
onFileTap={(candidates) => onFileTap(handle, candidates)}
onOpenUrl={(url) => onOpenUrl(handle, url)}
onTextScaleChange={onTextScaleChange}
onCellBoxChange={() => onCellBoxChange(handle)}
/>
</View>
)
}
const styles = StyleSheet.create({
terminalPane: {
...StyleSheet.absoluteFillObject
},
terminalPaneHidden: {
opacity: 0
},
terminalWebView: {
flex: 1
}
})