diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index a865f505af..d656169c51 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -129,7 +129,7 @@ import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js'; import { type UpdateObject } from './utils/updateCheck.js'; import { setUpdateHandler } from '../utils/handleAutoUpdate.js'; import { registerCleanup, runExitCleanup } from '../utils/cleanup.js'; -import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js'; +import { relaunchApp } from '../utils/processUtils.js'; import type { SessionInfo } from '../utils/sessionUtils.js'; import { useMessageQueue } from './hooks/useMessageQueue.js'; import { useMcpStatus } from './hooks/useMcpStatus.js'; @@ -781,13 +781,12 @@ export const AppContainer = (props: AppContainerProps) => { authType === AuthType.LOGIN_WITH_GOOGLE && config.isBrowserLaunchSuppressed() ) { - await runExitCleanup(); writeToStdout(` ---------------------------------------------------------------- Logging in with Google... Restarting Gemini CLI to continue. ---------------------------------------------------------------- `); - process.exit(RELAUNCH_EXIT_CODE); + await relaunchApp(); } } setAuthState(AuthState.Authenticated); @@ -2497,8 +2496,7 @@ Logging in with Google... Restarting Gemini CLI to continue. }); } } - await runExitCleanup(); - process.exit(RELAUNCH_EXIT_CODE); + await relaunchApp(); }, handleNewAgentsSelect: async (choice: NewAgentsChoice) => { if (newAgents && choice === NewAgentsChoice.ACKNOWLEDGE) { diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index 43d88160fb..58956e5f86 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -21,9 +21,8 @@ import { } from '@google/gemini-cli-core'; import { useKeypress } from '../hooks/useKeypress.js'; import { AuthState } from '../types.js'; -import { runExitCleanup } from '../../utils/cleanup.js'; import { validateAuthMethodWithSettings } from './useAuth.js'; -import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js'; +import { relaunchApp } from '../../utils/processUtils.js'; interface AuthDialogProps { config: Config; @@ -133,10 +132,7 @@ export function AuthDialog({ config.isBrowserLaunchSuppressed() ) { setExiting(true); - setTimeout(async () => { - await runExitCleanup(); - process.exit(RELAUNCH_EXIT_CODE); - }, 100); + setTimeout(relaunchApp, 100); return; } diff --git a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx index 9079358348..77310e3069 100644 --- a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx +++ b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx @@ -9,7 +9,10 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { LoginWithGoogleRestartDialog } from './LoginWithGoogleRestartDialog.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { runExitCleanup } from '../../utils/cleanup.js'; -import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js'; +import { + RELAUNCH_EXIT_CODE, + _resetRelaunchStateForTesting, +} from '../../utils/processUtils.js'; import { type Config } from '@google/gemini-cli-core'; // Mocks @@ -38,6 +41,7 @@ describe('LoginWithGoogleRestartDialog', () => { vi.clearAllMocks(); exitSpy.mockClear(); vi.useRealTimers(); + _resetRelaunchStateForTesting(); }); it('renders correctly', async () => { diff --git a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.tsx b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.tsx index 86cd645fee..94ca359b59 100644 --- a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.tsx +++ b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.tsx @@ -8,8 +8,7 @@ import { type Config } from '@google/gemini-cli-core'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; -import { runExitCleanup } from '../../utils/cleanup.js'; -import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js'; +import { relaunchApp } from '../../utils/processUtils.js'; interface LoginWithGoogleRestartDialogProps { onDismiss: () => void; @@ -36,8 +35,7 @@ export const LoginWithGoogleRestartDialog = ({ }); } } - await runExitCleanup(); - process.exit(RELAUNCH_EXIT_CODE); + await relaunchApp(); }, 100); return true; } diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index c86a4ba8d3..5119c1b343 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -21,8 +21,7 @@ import { ProQuotaDialog } from './ProQuotaDialog.js'; import { ValidationDialog } from './ValidationDialog.js'; import { OverageMenuDialog } from './OverageMenuDialog.js'; import { EmptyWalletDialog } from './EmptyWalletDialog.js'; -import { runExitCleanup } from '../../utils/cleanup.js'; -import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js'; +import { relaunchApp } from '../../utils/processUtils.js'; import { SessionBrowser } from './SessionBrowser.js'; import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js'; import { ModelDialog } from './ModelDialog.js'; @@ -231,10 +230,7 @@ export const DialogManager = ({ uiActions.closeSettingsDialog()} - onRestartRequest={async () => { - await runExitCleanup(); - process.exit(RELAUNCH_EXIT_CODE); - }} + onRestartRequest={relaunchApp} availableTerminalHeight={terminalHeight - staticExtraHeight} /> diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx index bbda51d8f0..012b2aab2f 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx @@ -246,7 +246,9 @@ describe('FolderTrustDialog', () => { it('should call relaunchApp when isRestarting is true', async () => { vi.useFakeTimers(); - const relaunchApp = vi.spyOn(processUtils, 'relaunchApp'); + const relaunchApp = vi + .spyOn(processUtils, 'relaunchApp') + .mockResolvedValue(undefined); const { waitUntilReady, unmount } = renderWithProviders( , ); @@ -259,7 +261,9 @@ describe('FolderTrustDialog', () => { it('should not call relaunchApp if unmounted before timeout', async () => { vi.useFakeTimers(); - const relaunchApp = vi.spyOn(processUtils, 'relaunchApp'); + const relaunchApp = vi + .spyOn(processUtils, 'relaunchApp') + .mockResolvedValue(undefined); const { waitUntilReady, unmount } = renderWithProviders( , ); diff --git a/packages/cli/src/ui/components/FolderTrustDialog.tsx b/packages/cli/src/ui/components/FolderTrustDialog.tsx index 2067a5dc3a..5f154a4d1a 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.tsx @@ -54,9 +54,7 @@ export const FolderTrustDialog: React.FC = ({ useEffect(() => { let timer: ReturnType; if (isRestarting) { - timer = setTimeout(async () => { - await relaunchApp(); - }, 250); + timer = setTimeout(relaunchApp, 250); } return () => { if (timer) clearTimeout(timer); diff --git a/packages/cli/src/ui/components/IdeTrustChangeDialog.test.tsx b/packages/cli/src/ui/components/IdeTrustChangeDialog.test.tsx index 3202fbb0d1..24a53b82de 100644 --- a/packages/cli/src/ui/components/IdeTrustChangeDialog.test.tsx +++ b/packages/cli/src/ui/components/IdeTrustChangeDialog.test.tsx @@ -62,7 +62,9 @@ describe('IdeTrustChangeDialog', () => { }); it('calls relaunchApp when "r" is pressed', async () => { - const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp'); + const relaunchAppSpy = vi + .spyOn(processUtils, 'relaunchApp') + .mockResolvedValue(undefined); const { stdin, waitUntilReady, unmount } = renderWithProviders( , ); @@ -78,7 +80,9 @@ describe('IdeTrustChangeDialog', () => { }); it('calls relaunchApp when "R" is pressed', async () => { - const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp'); + const relaunchAppSpy = vi + .spyOn(processUtils, 'relaunchApp') + .mockResolvedValue(undefined); const { stdin, waitUntilReady, unmount } = renderWithProviders( , ); @@ -94,7 +98,9 @@ describe('IdeTrustChangeDialog', () => { }); it('does not call relaunchApp when another key is pressed', async () => { - const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp'); + const relaunchAppSpy = vi + .spyOn(processUtils, 'relaunchApp') + .mockResolvedValue(undefined); const { stdin, waitUntilReady, unmount } = renderWithProviders( , ); diff --git a/packages/cli/src/utils/processUtils.test.ts b/packages/cli/src/utils/processUtils.test.ts index 009c17a9d4..3e6b7913e9 100644 --- a/packages/cli/src/utils/processUtils.test.ts +++ b/packages/cli/src/utils/processUtils.test.ts @@ -5,7 +5,11 @@ */ import { vi } from 'vitest'; -import { RELAUNCH_EXIT_CODE, relaunchApp } from './processUtils.js'; +import { + RELAUNCH_EXIT_CODE, + relaunchApp, + _resetRelaunchStateForTesting, +} from './processUtils.js'; import * as cleanup from './cleanup.js'; import * as handleAutoUpdate from './handleAutoUpdate.js'; @@ -19,6 +23,10 @@ describe('processUtils', () => { .mockReturnValue(undefined as never); const runExitCleanup = vi.spyOn(cleanup, 'runExitCleanup'); + beforeEach(() => { + _resetRelaunchStateForTesting(); + }); + afterEach(() => vi.clearAllMocks()); it('should wait for updates, run cleanup, and exit with the relaunch code', async () => { diff --git a/packages/cli/src/utils/processUtils.ts b/packages/cli/src/utils/processUtils.ts index c55caf023b..c43f5c54fd 100644 --- a/packages/cli/src/utils/processUtils.ts +++ b/packages/cli/src/utils/processUtils.ts @@ -15,7 +15,16 @@ export const RELAUNCH_EXIT_CODE = 199; /** * Exits the process with a special code to signal that the parent process should relaunch it. */ +let isRelaunching = false; + +/** @internal only for testing */ +export function _resetRelaunchStateForTesting(): void { + isRelaunching = false; +} + export async function relaunchApp(): Promise { + if (isRelaunching) return; + isRelaunching = true; await waitForUpdateCompletion(); await runExitCleanup(); process.exit(RELAUNCH_EXIT_CODE);