fix mobile terminal replay row mismatch

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-04-30 18:03:53 -04:00
co-authored by Orca
parent 11f45095cd
commit d2ce1c5ff1
2 changed files with 33 additions and 4 deletions
+24 -2
View File
@@ -69,11 +69,13 @@ const STATUS_LABELS: Record<ConnectionState, string> = {
function TerminalPaneView({
handle,
active,
onRef
onRef,
onWebReady
}: {
handle: string
active: boolean
onRef: (handle: string, ref: TerminalWebViewHandle | null) => void
onWebReady: (handle: string) => void
}) {
const setRef = useCallback(
(ref: TerminalWebViewHandle | null) => {
@@ -87,7 +89,11 @@ function TerminalPaneView({
pointerEvents={active ? 'auto' : 'none'}
style={[styles.terminalPane, !active && styles.terminalPaneHidden]}
>
<TerminalWebView ref={setRef} style={styles.terminalWebView} />
<TerminalWebView
ref={setRef}
style={styles.terminalWebView}
onWebReady={() => onWebReady(handle)}
/>
</View>
)
}
@@ -541,6 +547,21 @@ export default function SessionScreen() {
[subscribeToTerminal]
)
const handleTerminalWebReady = useCallback(
(handle: string) => {
if (!initializedHandlesRef.current.has(handle)) {
return
}
// Why: the native WebView can reload independently of the session
// screen during Metro reloads or Android process churn. The old xterm
// buffer is gone, so force a fresh scrollback snapshot for this handle.
unsubscribeTerminal(handle)
initializedHandlesRef.current.delete(handle)
subscribeToTerminal(handle)
},
[subscribeToTerminal, unsubscribeTerminal]
)
async function handleSend() {
if (!client || !activeHandle || !input.trim()) return
@@ -771,6 +792,7 @@ export default function SessionScreen() {
handle={terminal.handle}
active={terminal.handle === activeHandle}
onRef={setTerminalWebViewRef}
onWebReady={handleTerminalWebReady}
/>
))}
</View>
+9 -2
View File
@@ -14,6 +14,7 @@ export type TerminalWebViewHandle = {
type Props = {
style?: StyleProp<ViewStyle>
onWebReady?: () => void
}
type TerminalMessage =
@@ -138,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
@@ -495,7 +501,7 @@ const XTERM_HTML = `<!DOCTYPE html>
</html>`
export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function TerminalWebView(
{ style },
{ style, onWebReady },
ref
) {
const webViewRef = useRef<WebView>(null)
@@ -541,6 +547,7 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
if (msg.type === 'web-ready') {
isWebReadyRef.current = true
onWebReady?.()
flushPendingMessages()
} else if (msg.type === 'measure-result') {
const resolve = measureResolveRef.current
@@ -552,7 +559,7 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
}
}
},
[flushPendingMessages]
[flushPendingMessages, onWebReady]
)
const handleLoadStart = useCallback(() => {