mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 00:02:54 +00:00
Polish mobile terminal keyboard behavior
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
||||
} from '../../../../src/platform/haptics'
|
||||
import {
|
||||
TerminalWebView,
|
||||
type TerminalKeyboardAvoidanceMetrics,
|
||||
type TerminalModes,
|
||||
type TerminalWebViewHandle
|
||||
} from '../../../../src/terminal/TerminalWebView'
|
||||
@@ -157,6 +158,7 @@ function TerminalPaneView({
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
onModesChanged,
|
||||
onKeyboardAvoidanceMetrics,
|
||||
onHaptic
|
||||
}: {
|
||||
handle: string
|
||||
@@ -168,6 +170,7 @@ function TerminalPaneView({
|
||||
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
|
||||
}) {
|
||||
const setRef = useCallback(
|
||||
@@ -194,6 +197,7 @@ function TerminalPaneView({
|
||||
onSelectionCopy={(t) => onSelectionCopy(handle, t)}
|
||||
onSelectionEvicted={() => onSelectionEvicted(handle)}
|
||||
onModesChanged={(m) => onModesChanged(handle, m)}
|
||||
onKeyboardAvoidanceMetrics={(m) => onKeyboardAvoidanceMetrics(handle, m)}
|
||||
onHaptic={onHaptic}
|
||||
/>
|
||||
</View>
|
||||
@@ -349,6 +353,9 @@ export default function SessionScreen() {
|
||||
// Why: server-authoritative display mode per terminal. The runtime is the
|
||||
// single source of truth — this state is populated from subscribe responses.
|
||||
const [terminalModes, setTerminalModes] = useState<Map<string, MobileDisplayMode>>(new Map())
|
||||
const [terminalKeyboardMetrics, setTerminalKeyboardMetrics] = useState<
|
||||
Map<string, TerminalKeyboardAvoidanceMetrics>
|
||||
>(new Map())
|
||||
const [selectModeActive, setSelectModeActive] = useState(false)
|
||||
const [canPaste, setCanPaste] = useState(false)
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
||||
@@ -441,6 +448,7 @@ export default function SessionScreen() {
|
||||
webReadyHandlesRef.current.clear()
|
||||
subscribeSeqRef.current.clear()
|
||||
layoutSeqRef.current.clear()
|
||||
setTerminalKeyboardMetrics(new Map())
|
||||
for (const term of terminalRefs.current.values()) {
|
||||
term.clear()
|
||||
}
|
||||
@@ -744,6 +752,12 @@ export default function SessionScreen() {
|
||||
unsubscribeTerminal(handle)
|
||||
terminalRefs.current.delete(handle)
|
||||
initializedHandlesRef.current.delete(handle)
|
||||
setTerminalKeyboardMetrics((prev) => {
|
||||
if (!prev.has(handle)) return prev
|
||||
const next = new Map(prev)
|
||||
next.delete(handle)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
lastKnownTerminalCountRef.current = result.terminals.length
|
||||
@@ -1614,6 +1628,24 @@ export default function SessionScreen() {
|
||||
initialModesSeenRef.current.add(handle)
|
||||
}, [])
|
||||
|
||||
const handleKeyboardAvoidanceMetrics = useCallback(
|
||||
(handle: string, metrics: TerminalKeyboardAvoidanceMetrics) => {
|
||||
setTerminalKeyboardMetrics((prev) => {
|
||||
const current = prev.get(handle)
|
||||
if (
|
||||
current &&
|
||||
current.cursorY === metrics.cursorY &&
|
||||
current.rows === metrics.rows &&
|
||||
current.altScreen === metrics.altScreen
|
||||
) {
|
||||
return prev
|
||||
}
|
||||
return new Map(prev).set(handle, metrics)
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleHaptic = useCallback((kind: 'selection' | 'success' | 'error' | 'edge-bump') => {
|
||||
if (kind === 'selection') triggerSelection()
|
||||
else if (kind === 'success') triggerSuccess()
|
||||
@@ -1809,7 +1841,7 @@ export default function SessionScreen() {
|
||||
connState === 'connected' && terminalsLoaded && visibleTabs.length === 0 && !activeHandle
|
||||
const terminalSummary =
|
||||
connState === 'connected'
|
||||
? !terminalsLoaded
|
||||
? showLoadingState
|
||||
? 'Loading terminals'
|
||||
: visibleTabs.length === 1
|
||||
? '1 tab'
|
||||
@@ -1825,6 +1857,23 @@ export default function SessionScreen() {
|
||||
? Math.max(0, keyboardHeight - insets.bottom)
|
||||
: keyboardHeight
|
||||
: 0
|
||||
const activeTerminalKeyboardLift = (() => {
|
||||
if (keyboardLift <= 0 || !activeHandle) return 0
|
||||
const metrics = terminalKeyboardMetrics.get(activeHandle)
|
||||
if (!metrics || metrics.rows <= 0 || terminalFrameHeightRef.current <= 0) {
|
||||
return keyboardLift
|
||||
}
|
||||
if (metrics.altScreen) {
|
||||
return keyboardLift
|
||||
}
|
||||
const rowHeight = terminalFrameHeightRef.current / metrics.rows
|
||||
const cursorBottom = (metrics.cursorY + 1) * rowHeight
|
||||
const dockTop = terminalFrameHeightRef.current - keyboardLift
|
||||
const margin = rowHeight
|
||||
// Why: only move the terminal when the active cursor would sit under the
|
||||
// raised input dock. Short shell output near the top should stay put.
|
||||
return Math.min(keyboardLift, Math.max(0, cursorBottom + margin - dockTop))
|
||||
})()
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -1970,13 +2019,14 @@ export default function SessionScreen() {
|
||||
key={terminal.handle}
|
||||
handle={terminal.handle}
|
||||
active={terminal.handle === activeHandle}
|
||||
keyboardLift={keyboardLift}
|
||||
keyboardLift={terminal.handle === activeHandle ? activeTerminalKeyboardLift : 0}
|
||||
onRef={setTerminalWebViewRef}
|
||||
onWebReady={handleTerminalWebReady}
|
||||
onSelectionMode={handleSelectionMode}
|
||||
onSelectionCopy={handleSelectionCopy}
|
||||
onSelectionEvicted={handleSelectionEvicted}
|
||||
onModesChanged={handleModesChanged}
|
||||
onKeyboardAvoidanceMetrics={handleKeyboardAvoidanceMetrics}
|
||||
onHaptic={handleHaptic}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -9,11 +9,18 @@ export type TerminalModes = {
|
||||
altScreen: boolean
|
||||
}
|
||||
|
||||
export type TerminalKeyboardAvoidanceMetrics = {
|
||||
cursorY: number
|
||||
rows: number
|
||||
altScreen: boolean
|
||||
}
|
||||
|
||||
export type TerminalSelectionEvents = {
|
||||
onSelectionMode?: (active: boolean) => void
|
||||
onSelectionCopy?: (text: string) => void
|
||||
onSelectionEvicted?: () => void
|
||||
onModesChanged?: (modes: TerminalModes) => void
|
||||
onKeyboardAvoidanceMetrics?: (metrics: TerminalKeyboardAvoidanceMetrics) => void
|
||||
onHaptic?: (kind: 'selection' | 'success' | 'error' | 'edge-bump') => void
|
||||
}
|
||||
|
||||
@@ -716,14 +723,34 @@ const XTERM_HTML = `<!DOCTYPE html>
|
||||
}
|
||||
var lastEmittedModes = { bracketedPasteMode: false, altScreen: false };
|
||||
|
||||
function emitKeyboardAvoidanceMetrics() {
|
||||
if (!term) return;
|
||||
var alt = false;
|
||||
try { alt = term.buffer && term.buffer.active && term.buffer.active.type === 'alternate'; } catch (e) {}
|
||||
notify({
|
||||
type: 'keyboard-avoidance-metrics',
|
||||
cursorY: term.buffer && term.buffer.active ? term.buffer.active.cursorY : 0,
|
||||
rows: term.rows || 0,
|
||||
altScreen: alt
|
||||
});
|
||||
}
|
||||
|
||||
function attachTermObservers() {
|
||||
if (!term) return;
|
||||
try { term.onLineFeed(logFeedAndEvict); } catch (e) {}
|
||||
// Why: emit modes on every parsed write so RN's mirror stays current
|
||||
// without round-trip; covers \\x1b[?2004h/l and alt-screen toggles.
|
||||
try { term.onWriteParsed && term.onWriteParsed(emitModesIfChanged); } catch (e) {}
|
||||
try {
|
||||
term.onWriteParsed && term.onWriteParsed(function() {
|
||||
emitModesIfChanged();
|
||||
emitKeyboardAvoidanceMetrics();
|
||||
});
|
||||
} catch (e) {}
|
||||
// Initial emit once buffer settles.
|
||||
afterWritesDrained(function() { emitModesIfChanged(); });
|
||||
afterWritesDrained(function() {
|
||||
emitModesIfChanged();
|
||||
emitKeyboardAvoidanceMetrics();
|
||||
});
|
||||
}
|
||||
|
||||
function viewportToCell(clientX, clientY) {
|
||||
@@ -1282,6 +1309,7 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
onModesChanged,
|
||||
onKeyboardAvoidanceMetrics,
|
||||
onHaptic
|
||||
},
|
||||
ref
|
||||
@@ -1371,6 +1399,14 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
bracketedPasteMode: !!msg.bracketedPasteMode,
|
||||
altScreen: !!msg.altScreen
|
||||
})
|
||||
} else if (msg.type === 'keyboard-avoidance-metrics') {
|
||||
const cursorY = typeof msg.cursorY === 'number' ? msg.cursorY : 0
|
||||
const rows = typeof msg.rows === 'number' ? msg.rows : 0
|
||||
onKeyboardAvoidanceMetrics?.({
|
||||
cursorY,
|
||||
rows,
|
||||
altScreen: !!msg.altScreen
|
||||
})
|
||||
} else if (msg.type === 'haptic') {
|
||||
const kind = msg.kind
|
||||
if (
|
||||
@@ -1393,6 +1429,7 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
onModesChanged,
|
||||
onKeyboardAvoidanceMetrics,
|
||||
onHaptic
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user