From fc3916b64153c1b4c367342fa888cb8b65408f5d Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:41:51 -0700 Subject: [PATCH] fix(mobile): harden native-chat resize suppression --- mobile/src/terminal/terminal-viewport-refit-state.ts | 2 ++ mobile/src/terminal/terminal-viewport-refit.test.ts | 4 ++++ mobile/src/terminal/terminal-viewport-refit.ts | 1 + src/main/runtime/rpc/methods/terminal.ts | 3 ++- src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts | 3 ++- 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mobile/src/terminal/terminal-viewport-refit-state.ts b/mobile/src/terminal/terminal-viewport-refit-state.ts index 96995fd8ecd..dfa4daadf4c 100644 --- a/mobile/src/terminal/terminal-viewport-refit-state.ts +++ b/mobile/src/terminal/terminal-viewport-refit-state.ts @@ -7,6 +7,7 @@ export type TerminalViewportRefitTargetState = { expectedHandle: string currentRef: unknown expectedRef: unknown + nativeChatCovered: boolean disposed: boolean runSeq: number currentRunSeq: number @@ -94,6 +95,7 @@ export function isTerminalViewportRefitTargetCurrent( state: TerminalViewportRefitTargetState ): boolean { return ( + !state.nativeChatCovered && !state.disposed && state.runSeq === state.currentRunSeq && state.activeHandle === state.expectedHandle && diff --git a/mobile/src/terminal/terminal-viewport-refit.test.ts b/mobile/src/terminal/terminal-viewport-refit.test.ts index 5312e64d188..b472fb7847e 100644 --- a/mobile/src/terminal/terminal-viewport-refit.test.ts +++ b/mobile/src/terminal/terminal-viewport-refit.test.ts @@ -313,6 +313,7 @@ describe('terminal viewport refit', () => { expectedHandle: 'term-1', currentRef: expectedRef, expectedRef, + nativeChatCovered: false, disposed: false, runSeq: 2, currentRunSeq: 2 @@ -325,5 +326,8 @@ describe('terminal viewport refit', () => { ).toBe(false) expect(isTerminalViewportRefitTargetCurrent({ ...current, currentRunSeq: 3 })).toBe(false) expect(isTerminalViewportRefitTargetCurrent({ ...current, disposed: true })).toBe(false) + expect(isTerminalViewportRefitTargetCurrent({ ...current, nativeChatCovered: true })).toBe( + false + ) }) }) diff --git a/mobile/src/terminal/terminal-viewport-refit.ts b/mobile/src/terminal/terminal-viewport-refit.ts index 3091ac8c301..8a9ab281455 100644 --- a/mobile/src/terminal/terminal-viewport-refit.ts +++ b/mobile/src/terminal/terminal-viewport-refit.ts @@ -116,6 +116,7 @@ export function useTerminalViewportRefit( expectedHandle: handle, currentRef: terminalRefs.current.get(handle), expectedRef: ref, + nativeChatCovered: nativeChatCoveredRef.current, disposed: disposedRef.current, runSeq, currentRunSeq: refitRunSeqRef.current diff --git a/src/main/runtime/rpc/methods/terminal.ts b/src/main/runtime/rpc/methods/terminal.ts index 02dd619336b..6c10f518d93 100644 --- a/src/main/runtime/rpc/methods/terminal.ts +++ b/src/main/runtime/rpc/methods/terminal.ts @@ -2540,7 +2540,8 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [ .then(() => runtime.cleanupSubscription(subscriptionId)) .catch(() => runtime.cleanupSubscription(subscriptionId)) try { - await runtime.handleMobileSubscribe(ptyId, clientId, params.viewport) + // Why: a lease-only subscriber has no terminal view, so its cached viewport must never phone-fit the PTY. + await runtime.handleMobileSubscribe(ptyId, clientId, undefined) if (closed || signal?.aborted) { // Why: a disconnect can win the awaited subscribe and resurrect mobile presence after cleanup already released it. runtime.handleMobileUnsubscribe(ptyId, clientId) diff --git a/src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts b/src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts index fab3d5a09cd..3aba4b58200 100644 --- a/src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts +++ b/src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts @@ -12,12 +12,13 @@ const request: RpcRequest = { params: { terminal: 'terminal-1', client: { id: 'phone-1', type: 'mobile' }, + viewport: { cols: 40, rows: 20 }, capabilities: { terminalBinaryStream: 1, mobileInputLeaseOnly: 1 } } } describe('terminal lease-only subscription', () => { - it('keeps mobile input ownership without registering output delivery', async () => { + it('keeps mobile input ownership without viewport resize or output delivery', async () => { const messages: string[] = [] const cleanups = new Map void>() const runtime = {