Files
orca/mobile/src/session/TerminalPaneView.tsx
T
NeilandOrca fd6805a299 Fix mobile terminal query reply authority (#8227)
* Fix mobile terminal query reply authority

* fix(terminal): harden mobile query reply handoffs

* fix(terminal): exclude passive mobile query responders

* fix(terminal): gate mobile query replies on host capability

Older hosts strip terminal.send's inputKind (zod drops unknown keys), so a
forwarded xterm reply would land as ordinary floor-taking shell input. Hosts
now advertise terminal.query-reply-input.v1 via status.get and mobile drops
replies unless the host advertises it (pre-fix behavior). Also documents the
bounded desktop-to-mobile handoff double-reply residual.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): advance snapshot seq across recovery snapshots

The pending-overflow recovery loop trims buffered output against
recovery.seq while query replay and boundary strips kept using the
initial snapshot seq. Unreachable under today's control flow (no await
separates the initial-overflow consume from the loop), but the stale
seq would silently drop covered query replies if that ordering ever
changes. Track the seq that actually covered the buffered chunks.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-11 02:13:15 -07:00

106 lines
3.3 KiB
TypeScript

import { useCallback } from 'react'
import { StyleSheet, View } from 'react-native'
import { TerminalWebView } from '../terminal/TerminalWebView'
import type {
MobileTerminalTheme,
TerminalKeyboardAvoidanceMetrics,
TerminalModes,
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
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) => void
onTerminalQueryReply: (handle: string, bytes: string) => void
onTerminalTap: (handle: string) => void
onFileTap: (handle: string, pathText: string, line: number | null, column: number | null) => void
onOpenUrl: (handle: string, url: string) => void
onTextScaleChange: (scale: number) => void
}
export function TerminalPaneView({
handle,
active,
keyboardLift,
terminalTheme,
textScale,
onRef,
onWebReady,
onSelectionMode,
onSelectionCopy,
onSelectionEvicted,
onModesChanged,
onKeyboardAvoidanceMetrics,
onHaptic,
onTerminalInput,
onTerminalQueryReply,
onTerminalTap,
onFileTap,
onOpenUrl,
onTextScaleChange
}: TerminalPaneViewProps) {
const setRef = useCallback(
(ref: TerminalWebViewHandle | null) => {
onRef(handle, ref)
},
[handle, onRef]
)
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}
onWebReady={() => onWebReady(handle)}
onSelectionMode={(a) => onSelectionMode(handle, a)}
onSelectionCopy={(t) => onSelectionCopy(handle, t)}
onSelectionEvicted={() => onSelectionEvicted(handle)}
onModesChanged={(m) => onModesChanged(handle, m)}
onKeyboardAvoidanceMetrics={(m) => onKeyboardAvoidanceMetrics(handle, m)}
onHaptic={onHaptic}
onTerminalInput={(bytes) => onTerminalInput(handle, bytes)}
onTerminalQueryReply={(bytes) => onTerminalQueryReply(handle, bytes)}
onTerminalTap={() => onTerminalTap(handle)}
onFileTap={(pathText, line, column) => onFileTap(handle, pathText, line, column)}
onOpenUrl={(url) => onOpenUrl(handle, url)}
onTextScaleChange={onTextScaleChange}
/>
</View>
)
}
const styles = StyleSheet.create({
terminalPane: {
...StyleSheet.absoluteFillObject
},
terminalPaneHidden: {
opacity: 0
},
terminalWebView: {
flex: 1
}
})