From 4fe89e656aaed1c518530635aeeeb5001e2356ea Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 28 Apr 2026 03:34:19 -0400 Subject: [PATCH] fix mobile terminal geometry sync Co-authored-by: Orca --- .../app/h/[hostId]/session/[worktreeId].tsx | 74 +++++++++++++------ mobile/scripts/test-subscribe.ts | 12 +++ mobile/src/terminal/TerminalWebView.tsx | 4 +- src/main/ipc/pty.ts | 62 +++++++++++++++- src/main/runtime/orca-runtime.ts | 12 +++ src/main/runtime/rpc/methods/terminal.ts | 13 +++- src/preload/api-types.ts | 7 ++ src/preload/index.ts | 7 +- .../terminal-pane/pty-buffer-serializer.ts | 2 +- 9 files changed, 161 insertions(+), 32 deletions(-) diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx index daf4c7bcfc0..13638c52358 100644 --- a/mobile/app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx @@ -34,6 +34,7 @@ export default function SessionScreen() { const termRef = useRef(null) const unsubRef = useRef<(() => void) | null>(null) const activeHandleRef = useRef(null) + const subscribeSeqRef = useRef(0) const terminalSizeRef = useRef({ cols: 80, rows: 24 }) useEffect(() => { @@ -79,33 +80,58 @@ export default function SessionScreen() { if (!client) return termRef.current?.clear() + const seq = subscribeSeqRef.current + 1 + subscribeSeqRef.current = seq - const unsub = client.subscribe('terminal.subscribe', { terminal: handle }, (result) => { - const data = result as Record - if (data.type === 'scrollback') { - // Why: init xterm at the desktop's exact cols/rows so escape - // codes with absolute cursor positioning render correctly. - // CSS transform: scale() in TerminalWebView shrinks the canvas - // to fit the phone viewport, producing a 1:1 miniature. - const cols = (data.cols as number) || 80 - const rows = (data.rows as number) || 24 - terminalSizeRef.current = { cols, rows } - termRef.current?.init(cols, rows) - // Why: prefer serialized xterm buffer (ANSI escape string that - // reconstructs the exact screen state) over line-based tail - // because TUI apps use absolute cursor positioning that only - // works at the original terminal dimensions. - if (typeof data.serialized === 'string' && data.serialized.length > 0) { - termRef.current?.write(data.serialized) - } else { - writeLines(data.lines as string[] | string | undefined) - } - } else if (data.type === 'data') { - termRef.current?.write(data.chunk as string) + void (async () => { + try { + // Why: hidden/new desktop panes can still be at their 80x24 spawn + // geometry. Focusing first lets the desktop renderer fit xterm, + // resize the PTY, and register a serializer before mobile subscribes. + await client.sendRequest('terminal.focus', { terminal: handle }) + await new Promise((resolve) => setTimeout(resolve, 150)) + } catch { + // Continue with best-effort streaming if desktop focus is unavailable. } - }) - unsubRef.current = unsub + if (subscribeSeqRef.current !== seq || activeHandleRef.current !== handle) { + return + } + + const unsub = client.subscribe('terminal.subscribe', { terminal: handle }, (result) => { + if (subscribeSeqRef.current !== seq || activeHandleRef.current !== handle) { + return + } + const data = result as Record + if (data.type === 'scrollback') { + // Why: init xterm at the desktop's exact cols/rows so escape + // codes with absolute cursor positioning render correctly. + // CSS transform: scale() in TerminalWebView shrinks the canvas + // to fit the phone viewport, producing a 1:1 miniature. + const cols = (data.cols as number) || 80 + const rows = (data.rows as number) || 24 + terminalSizeRef.current = { cols, rows } + termRef.current?.init(cols, rows) + // Why: prefer serialized xterm buffer (ANSI escape string that + // reconstructs the exact screen state) over line-based tail + // because TUI apps use absolute cursor positioning that only + // works at the original terminal dimensions. + if (typeof data.serialized === 'string' && data.serialized.length > 0) { + termRef.current?.write(data.serialized) + } else { + writeLines(data.lines as string[] | string | undefined) + } + } else if (data.type === 'data') { + termRef.current?.write(data.chunk as string) + } + }) + + if (subscribeSeqRef.current === seq && activeHandleRef.current === handle) { + unsubRef.current = unsub + } else { + unsub() + } + })() }, [client, writeLines] ) diff --git a/mobile/scripts/test-subscribe.ts b/mobile/scripts/test-subscribe.ts index 17b97a4b0a7..9cf07ea8fdc 100644 --- a/mobile/scripts/test-subscribe.ts +++ b/mobile/scripts/test-subscribe.ts @@ -41,6 +41,9 @@ let streamSawMarker = false let readSawMarker = false let activeHandle: string | null = null let runtimeId = '' +let scrollbackCols: number | null = null +let scrollbackRows: number | null = null +let serializedLength = 0 function nextId(): string { reqId += 1 @@ -168,6 +171,9 @@ async function run(ws: WebSocket): Promise { } console.log(`runtime: ${runtimeId || '(unknown)'}`) + console.log(`scrollbackCols: ${scrollbackCols ?? '(none)'}`) + console.log(`scrollbackRows: ${scrollbackRows ?? '(none)'}`) + console.log(`serializedLength: ${serializedLength}`) console.log(`streamSawMarker: ${streamSawMarker}`) console.log(`readSawMarker: ${readSawMarker}`) @@ -207,9 +213,15 @@ ws.on('message', (data) => { if (response.streaming && result?.type === 'scrollback') { const lines = Array.isArray(result.lines) ? result.lines.join('\n') : String(result.lines ?? '') + scrollbackCols = typeof result.cols === 'number' ? result.cols : null + scrollbackRows = typeof result.rows === 'number' ? result.rows : null + serializedLength = typeof result.serialized === 'string' ? result.serialized.length : 0 if (lines.includes(marker)) { streamSawMarker = true } + if (typeof result.serialized === 'string' && result.serialized.includes(marker)) { + streamSawMarker = true + } } const pendingRequest = pending.get(response.id) diff --git a/mobile/src/terminal/TerminalWebView.tsx b/mobile/src/terminal/TerminalWebView.tsx index 1557165c7e5..11a0006228c 100644 --- a/mobile/src/terminal/TerminalWebView.tsx +++ b/mobile/src/terminal/TerminalWebView.tsx @@ -28,7 +28,7 @@ const XTERM_HTML = ` - +