mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
Revert "Keep fitted terminal bottom row visible"
This reverts commit 4be9e88e018d8f10917f970324e84c3bd9c23463.
This commit is contained in:
@@ -8,8 +8,7 @@ import {
|
||||
Pressable,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
type LayoutChangeEvent
|
||||
ActivityIndicator
|
||||
} from 'react-native'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router'
|
||||
@@ -47,7 +46,6 @@ type TerminalCreateResult = {
|
||||
}
|
||||
|
||||
type AccessoryKey = { label: string; bytes: string; accessibilityLabel?: string }
|
||||
type TerminalViewport = { width: number; height: number }
|
||||
|
||||
const ACCESSORY_KEYS: AccessoryKey[] = [
|
||||
{ label: 'Esc', bytes: '\x1b' },
|
||||
@@ -73,7 +71,6 @@ const ACCESSORY_KEYS: AccessoryKey[] = [
|
||||
// Capped to avoid unbounded growth over long app sessions.
|
||||
const MAX_PERSISTED_WORKTREES = 50
|
||||
const persistedFittedHandles = new Map<string, Set<string>>()
|
||||
const FIT_LAYOUT_SETTLE_MS = 250
|
||||
|
||||
const STATUS_LABELS: Record<ConnectionState, string> = {
|
||||
connecting: 'Connecting',
|
||||
@@ -84,28 +81,16 @@ const STATUS_LABELS: Record<ConnectionState, string> = {
|
||||
'auth-failed': 'Auth failed'
|
||||
}
|
||||
|
||||
function waitForFitLayoutSettle(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(resolve, FIT_LAYOUT_SETTLE_MS)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function TerminalPaneView({
|
||||
handle,
|
||||
active,
|
||||
onRef,
|
||||
onWebReady,
|
||||
onLayout
|
||||
onWebReady
|
||||
}: {
|
||||
handle: string
|
||||
active: boolean
|
||||
onRef: (handle: string, ref: TerminalWebViewHandle | null) => void
|
||||
onWebReady: (handle: string) => void
|
||||
onLayout: (handle: string, viewport: TerminalViewport) => void
|
||||
}) {
|
||||
const setRef = useCallback(
|
||||
(ref: TerminalWebViewHandle | null) => {
|
||||
@@ -113,19 +98,9 @@ function TerminalPaneView({
|
||||
},
|
||||
[handle, onRef]
|
||||
)
|
||||
const handleLayout = useCallback(
|
||||
(event: LayoutChangeEvent) => {
|
||||
const { width, height } = event.nativeEvent.layout
|
||||
if (width > 0 && height > 0) {
|
||||
onLayout(handle, { width, height })
|
||||
}
|
||||
},
|
||||
[handle, onLayout]
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={handleLayout}
|
||||
pointerEvents={active ? 'auto' : 'none'}
|
||||
style={[styles.terminalPane, !active && styles.terminalPaneHidden]}
|
||||
>
|
||||
@@ -197,7 +172,6 @@ export default function SessionScreen() {
|
||||
// Without this, switching away and back would auto-fit them again.
|
||||
const manuallyRestoredRef = useRef<Set<string>>(new Set())
|
||||
const terminalRefs = useRef<Map<string, TerminalWebViewHandle>>(new Map())
|
||||
const terminalViewportRef = useRef<Map<string, TerminalViewport>>(new Map())
|
||||
const terminalUnsubsRef = useRef<Map<string, () => void>>(new Map())
|
||||
const subscribingHandlesRef = useRef<Set<string>>(new Set())
|
||||
const initializedHandlesRef = useRef<Set<string>>(new Set())
|
||||
@@ -212,10 +186,6 @@ export default function SessionScreen() {
|
||||
return handle ? terminalRefs.current.get(handle) : undefined
|
||||
}, [])
|
||||
|
||||
const setTerminalViewport = useCallback((handle: string, viewport: TerminalViewport) => {
|
||||
terminalViewportRef.current.set(handle, viewport)
|
||||
}, [])
|
||||
|
||||
const unsubscribeTerminal = useCallback((handle: string) => {
|
||||
terminalUnsubsRef.current.get(handle)?.()
|
||||
terminalUnsubsRef.current.delete(handle)
|
||||
@@ -316,13 +286,7 @@ export default function SessionScreen() {
|
||||
if (!clientId) return
|
||||
const seq = subscribeSeqRef.current.get(handle)
|
||||
|
||||
// Why: the first session render can measure the WebView before the
|
||||
// accessory/input chrome has fully settled. Waiting matches the remount
|
||||
// path, where phone-fit already gets the correct unobscured height.
|
||||
await waitForFitLayoutSettle()
|
||||
const dims = await getTerminalRef(handle)?.measureFitDimensions(
|
||||
terminalViewportRef.current.get(handle)
|
||||
)
|
||||
const dims = await getTerminalRef(handle)?.measureFitDimensions()
|
||||
if (!dims) return
|
||||
if (
|
||||
clientRef.current !== rpcClient ||
|
||||
@@ -541,9 +505,7 @@ export default function SessionScreen() {
|
||||
// is already at the correct phone size.
|
||||
if (shouldAutoFit && !fitPendingRef.current) {
|
||||
void (async () => {
|
||||
const dims = await getTerminalRef(handle)?.measureFitDimensions(
|
||||
terminalViewportRef.current.get(handle)
|
||||
)
|
||||
const dims = await getTerminalRef(handle)?.measureFitDimensions()
|
||||
if (!dims) return
|
||||
if (cols !== dims.cols || rows !== dims.rows) {
|
||||
if (
|
||||
@@ -718,7 +680,6 @@ export default function SessionScreen() {
|
||||
subscribeToTerminal(handle)
|
||||
} else {
|
||||
terminalRefs.current.delete(handle)
|
||||
terminalViewportRef.current.delete(handle)
|
||||
}
|
||||
},
|
||||
[subscribeToTerminal]
|
||||
@@ -974,7 +935,6 @@ export default function SessionScreen() {
|
||||
active={terminal.handle === activeHandle}
|
||||
onRef={setTerminalWebViewRef}
|
||||
onWebReady={handleTerminalWebReady}
|
||||
onLayout={setTerminalViewport}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -8,10 +8,7 @@ export type TerminalWebViewHandle = {
|
||||
write: (data: string) => void
|
||||
init: (cols: number, rows: number, initialData?: string) => void
|
||||
clear: () => void
|
||||
measureFitDimensions: (viewport?: { width: number; height: number }) => Promise<{
|
||||
cols: number
|
||||
rows: number
|
||||
} | null>
|
||||
measureFitDimensions: () => Promise<{ cols: number; rows: number } | null>
|
||||
resetZoom: () => void
|
||||
}
|
||||
|
||||
@@ -24,7 +21,7 @@ type TerminalMessage =
|
||||
| { type: 'write'; id?: number; data: string }
|
||||
| { type: 'init'; id?: number; cols: number; rows: number; initialData?: string }
|
||||
| { type: 'clear'; id?: number }
|
||||
| { type: 'measure'; id?: number; viewportWidth?: number; viewportHeight?: number }
|
||||
| { type: 'measure'; id?: number }
|
||||
| { type: 'reset-zoom'; id?: number }
|
||||
|
||||
// Why: TUI apps (Claude Code / Ink) emit escape codes with absolute cursor
|
||||
@@ -142,6 +139,11 @@ const XTERM_HTML = `<!DOCTYPE html>
|
||||
// managed by xterm, not a separate HTML gap. Never shrink below the
|
||||
// original init row count to avoid clipping active terminal content.
|
||||
function adjustRowsForViewport() {
|
||||
// Why: mobile replays a live PTY snapshot and then applies live cursor-
|
||||
// relative chunks from that same PTY. Resizing only the WebView xterm
|
||||
// changes cursor coordinates and makes TUI repaint chunks duplicate or
|
||||
// overlap existing frames. Keep xterm rows identical to the PTY.
|
||||
return;
|
||||
if (!term || !term.element) return;
|
||||
// Why: active alternate-screen TUIs (Claude Code, vim, etc.) are exact
|
||||
// screen snapshots. Locally resizing the mobile xterm after replay can
|
||||
@@ -177,14 +179,6 @@ const XTERM_HTML = `<!DOCTYPE html>
|
||||
return on !== -1 && on > off;
|
||||
}
|
||||
|
||||
function getAltScreenTransition(data) {
|
||||
if (typeof data !== 'string') return null;
|
||||
var on = data.lastIndexOf(ESC + '[?1049h');
|
||||
var off = data.lastIndexOf(ESC + '[?1049l');
|
||||
if (on === -1 && off === -1) return null;
|
||||
return on > off;
|
||||
}
|
||||
|
||||
function normalizeInitialData(data) {
|
||||
if (!isAltScreenActive(data)) return data;
|
||||
var on = data.lastIndexOf(ESC + '[?1049h');
|
||||
@@ -283,18 +277,7 @@ const XTERM_HTML = `<!DOCTYPE html>
|
||||
}
|
||||
|
||||
function write(data) {
|
||||
var nextAltScreen = getAltScreenTransition(data);
|
||||
if (nextAltScreen === true && !activeAltScreenSnapshot) {
|
||||
activeAltScreenSnapshot = true;
|
||||
// Why: alternate-screen chunks are generated for the PTY row count.
|
||||
// Undo display-only row growth before xterm parses TUI coordinates.
|
||||
if (term && term.rows !== initRows) term.resize(term.cols, initRows);
|
||||
}
|
||||
writeQueue.push(data);
|
||||
if (nextAltScreen === false && activeAltScreenSnapshot) {
|
||||
activeAltScreenSnapshot = false;
|
||||
afterWritesDrained(adjustRowsForViewport);
|
||||
}
|
||||
pumpWrites(terminalGeneration);
|
||||
}
|
||||
|
||||
@@ -304,7 +287,7 @@ const XTERM_HTML = `<!DOCTYPE html>
|
||||
}
|
||||
}
|
||||
|
||||
function measureFitDimensions(request) {
|
||||
function measureFitDimensions() {
|
||||
if (!term || !term.element) {
|
||||
notify({ type: 'measure-result', cols: null, rows: null });
|
||||
return;
|
||||
@@ -323,17 +306,10 @@ const XTERM_HTML = `<!DOCTYPE html>
|
||||
notify({ type: 'measure-result', cols: null, rows: null });
|
||||
return;
|
||||
}
|
||||
var vpWidth = request && typeof request.viewportWidth === 'number'
|
||||
? request.viewportWidth
|
||||
: window.innerWidth;
|
||||
var vpHeight = request && typeof request.viewportHeight === 'number'
|
||||
? request.viewportHeight
|
||||
: window.innerHeight;
|
||||
var vpWidth = window.innerWidth;
|
||||
var vpHeight = window.innerHeight;
|
||||
var cols = Math.floor(vpWidth / cellWidth);
|
||||
// Why: Android WebView can report a viewport that is a little taller than
|
||||
// the visibly unobscured terminal area next to the native input chrome.
|
||||
// Reserve one row so max scroll leaves the final line fully visible.
|
||||
var rows = Math.max(8, Math.floor(vpHeight / cellHeight) - 1);
|
||||
var rows = Math.floor(vpHeight / cellHeight);
|
||||
notify({ type: 'measure-result', cols: cols, rows: rows });
|
||||
}
|
||||
|
||||
@@ -354,7 +330,7 @@ const XTERM_HTML = `<!DOCTYPE html>
|
||||
writesDraining = false;
|
||||
if (term) { term.clear(); term.reset(); }
|
||||
} else if (msg.type === 'measure') {
|
||||
measureFitDimensions(msg);
|
||||
measureFitDimensions();
|
||||
} else if (msg.type === 'reset-zoom') {
|
||||
applyFitScale();
|
||||
}
|
||||
@@ -596,19 +572,12 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
clear() {
|
||||
postMessage({ type: 'clear' })
|
||||
},
|
||||
measureFitDimensions(viewport?: {
|
||||
width: number
|
||||
height: number
|
||||
}): Promise<{ cols: number; rows: number } | null> {
|
||||
measureFitDimensions(): Promise<{ cols: number; rows: number } | null> {
|
||||
if (!isWebReadyRef.current) return Promise.resolve(null)
|
||||
return new Promise((resolve) => {
|
||||
measureResolveRef.current?.(null)
|
||||
measureResolveRef.current = resolve
|
||||
sendToWebView({
|
||||
type: 'measure',
|
||||
viewportWidth: viewport?.width,
|
||||
viewportHeight: viewport?.height
|
||||
})
|
||||
sendToWebView({ type: 'measure' })
|
||||
// Why: if the WebView doesn't respond within 2s (e.g., xterm
|
||||
// failed to load), resolve null so the caller can disable
|
||||
// Fit to Phone rather than hanging indefinitely.
|
||||
|
||||
Reference in New Issue
Block a user