From ba04e99bea6aec57017e7d57ddd3e7415b5afad0 Mon Sep 17 00:00:00 2001 From: Syed Ayman Quadri <87442765+saymanq@users.noreply.github.com> Date: Fri, 22 May 2026 00:33:52 -0600 Subject: [PATCH] fix(cli): prevent Termux relaunch and resize remount loops (#27110) Co-authored-by: Spencer --- packages/cli/index.ts | 2 +- packages/cli/src/ui/AppContainer.tsx | 22 +------------------ packages/cli/src/ui/hooks/useSuspend.test.ts | 14 ------------ packages/cli/src/ui/hooks/useSuspend.ts | 23 ++------------------ packages/cli/src/utils/relaunch.test.ts | 21 ++++++++++++++++++ packages/cli/src/utils/relaunch.ts | 2 +- 6 files changed, 26 insertions(+), 58 deletions(-) diff --git a/packages/cli/index.ts b/packages/cli/index.ts index f13d4707b0..3bcec4d301 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -126,7 +126,7 @@ async function run() { while (true) { try { const exitCode = await runner(); - if (exitCode !== RELAUNCH_EXIT_CODE) { + if (process.platform === 'android' || exitCode !== RELAUNCH_EXIT_CODE) { process.exit(exitCode); } } catch (error: unknown) { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 702346ece9..e11282788b 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -254,7 +254,6 @@ export const AppContainer = (props: AppContainerProps) => { }, [mouseMode, setOptions]); const [corgiMode, setCorgiMode] = useState(false); - const [forceRerenderKey, setForceRerenderKey] = useState(0); const [debugMessage, setDebugMessage] = useState(''); const [quittingMessages, setQuittingMessages] = useState< HistoryItem[] | null @@ -1687,8 +1686,6 @@ Logging in with Google... Restarting Gemini CLI to continue. needsRestart: ideNeedsRestart, restartReason: ideTrustRestartReason, } = useIdeTrustListener(); - const isInitialMount = useRef(true); - useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog); const tabFocusTimeoutRef = useRef(null); @@ -1741,8 +1738,6 @@ Logging in with Google... Restarting Gemini CLI to continue. const { handleSuspend } = useSuspend({ handleWarning, setRawMode, - refreshStatic, - setForceRerenderKey, shouldUseAlternateScreen, }); @@ -1753,21 +1748,6 @@ Logging in with Google... Restarting Gemini CLI to continue. } }, [ideNeedsRestart]); - useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - return; - } - - const handler = setTimeout(() => { - refreshStatic(); - }, 300); - - return () => { - clearTimeout(handler); - }; - }, [terminalWidth, refreshStatic]); - useEffect(() => { const unsubscribe = ideContextStore.subscribe(setIdeContextState); setIdeContextState(ideContextStore.get()); @@ -2872,7 +2852,7 @@ Logging in with Google... Restarting Gemini CLI to continue. - + diff --git a/packages/cli/src/ui/hooks/useSuspend.test.ts b/packages/cli/src/ui/hooks/useSuspend.test.ts index 7e4d8808d3..6f66571874 100644 --- a/packages/cli/src/ui/hooks/useSuspend.test.ts +++ b/packages/cli/src/ui/hooks/useSuspend.test.ts @@ -83,8 +83,6 @@ describe('useSuspend', () => { it('cleans terminal state on suspend and restores/repaints on resume in alternate screen mode', async () => { const handleWarning = vi.fn(); const setRawMode = vi.fn(); - const refreshStatic = vi.fn(); - const setForceRerenderKey = vi.fn(); const enableSupportedModes = terminalCapabilityManager.enableSupportedModes as unknown as Mock; @@ -92,8 +90,6 @@ describe('useSuspend', () => { useSuspend({ handleWarning, setRawMode, - refreshStatic, - setForceRerenderKey, shouldUseAlternateScreen: true, }), ); @@ -131,8 +127,6 @@ describe('useSuspend', () => { expect(enableSupportedModes).toHaveBeenCalledTimes(1); expect(enableMouseEvents).toHaveBeenCalledTimes(1); expect(setRawMode).toHaveBeenCalledWith(true); - expect(refreshStatic).toHaveBeenCalledTimes(1); - expect(setForceRerenderKey).toHaveBeenCalledTimes(1); unmount(); }); @@ -140,15 +134,11 @@ describe('useSuspend', () => { it('does not toggle alternate screen or mouse restore when alternate screen mode is disabled', async () => { const handleWarning = vi.fn(); const setRawMode = vi.fn(); - const refreshStatic = vi.fn(); - const setForceRerenderKey = vi.fn(); const { result, unmount } = await renderHook(() => useSuspend({ handleWarning, setRawMode, - refreshStatic, - setForceRerenderKey, shouldUseAlternateScreen: false, }), ); @@ -174,15 +164,11 @@ describe('useSuspend', () => { const handleWarning = vi.fn(); const setRawMode = vi.fn(); - const refreshStatic = vi.fn(); - const setForceRerenderKey = vi.fn(); const { result, unmount } = await renderHook(() => useSuspend({ handleWarning, setRawMode, - refreshStatic, - setForceRerenderKey, shouldUseAlternateScreen: true, }), ); diff --git a/packages/cli/src/ui/hooks/useSuspend.ts b/packages/cli/src/ui/hooks/useSuspend.ts index b5e92fb80b..7082a76e7e 100644 --- a/packages/cli/src/ui/hooks/useSuspend.ts +++ b/packages/cli/src/ui/hooks/useSuspend.ts @@ -26,16 +26,12 @@ import { Command } from '../key/keyBindings.js'; interface UseSuspendProps { handleWarning: (message: string) => void; setRawMode: (mode: boolean) => void; - refreshStatic: () => void; - setForceRerenderKey: (updater: (prev: number) => number) => void; shouldUseAlternateScreen: boolean; } export function useSuspend({ handleWarning, setRawMode, - refreshStatic, - setForceRerenderKey, shouldUseAlternateScreen, }: UseSuspendProps) { const [ctrlZPressCount, setCtrlZPressCount] = useState(0); @@ -108,16 +104,8 @@ export function useSuspend({ enableMouseEvents(); } - // Force Ink to do a complete repaint by: - // 1. Emitting a resize event (tricks Ink into full redraw) - // 2. Remounting components via state changes + // Force Ink to do a complete repaint without remounting the app. process.stdout.emit('resize'); - - // Give a tick for resize to process, then trigger remount - setImmediate(() => { - refreshStatic(); - setForceRerenderKey((prev) => prev + 1); - }); } finally { if (onResumeHandlerRef.current === onResume) { onResumeHandlerRef.current = null; @@ -142,14 +130,7 @@ export function useSuspend({ ctrlZTimerRef.current = null; }, WARNING_PROMPT_DURATION_MS); } - }, [ - ctrlZPressCount, - handleWarning, - setRawMode, - refreshStatic, - setForceRerenderKey, - shouldUseAlternateScreen, - ]); + }, [ctrlZPressCount, handleWarning, setRawMode, shouldUseAlternateScreen]); const handleSuspend = useCallback(() => { setCtrlZPressCount((prev) => prev + 1); diff --git a/packages/cli/src/utils/relaunch.test.ts b/packages/cli/src/utils/relaunch.test.ts index 56282b4612..3a6de66122 100644 --- a/packages/cli/src/utils/relaunch.test.ts +++ b/packages/cli/src/utils/relaunch.test.ts @@ -46,6 +46,14 @@ import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js'; describe('relaunchOnExitCode', () => { let processExitSpy: MockInstance; let stdinResumeSpy: MockInstance; + const originalPlatform = process.platform; + + const setPlatform = (platform: NodeJS.Platform) => { + Object.defineProperty(process, 'platform', { + value: platform, + configurable: true, + }); + }; beforeEach(() => { processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { @@ -60,6 +68,7 @@ describe('relaunchOnExitCode', () => { afterEach(() => { vi.unstubAllEnvs(); + setPlatform(originalPlatform); processExitSpy.mockRestore(); stdinResumeSpy.mockRestore(); }); @@ -92,6 +101,18 @@ describe('relaunchOnExitCode', () => { expect(processExitSpy).toHaveBeenCalledWith(0); }); + it('should not relaunch on Android when RELAUNCH_EXIT_CODE is returned', async () => { + setPlatform('android'); + const runner = vi.fn().mockResolvedValue(RELAUNCH_EXIT_CODE); + + await expect(relaunchOnExitCode(runner)).rejects.toThrow( + 'PROCESS_EXIT_CALLED', + ); + + expect(runner).toHaveBeenCalledTimes(1); + expect(processExitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE); + }); + it('should handle runner errors', async () => { const error = new Error('Runner failed'); const runner = vi.fn().mockRejectedValue(error); diff --git a/packages/cli/src/utils/relaunch.ts b/packages/cli/src/utils/relaunch.ts index d7a6ab52ae..752918539b 100644 --- a/packages/cli/src/utils/relaunch.ts +++ b/packages/cli/src/utils/relaunch.ts @@ -20,7 +20,7 @@ export async function relaunchOnExitCode(runner: () => Promise) { try { const exitCode = await runner(); - if (exitCode !== RELAUNCH_EXIT_CODE) { + if (process.platform === 'android' || exitCode !== RELAUNCH_EXIT_CODE) { process.exit(exitCode); } } catch (error) {