From 2af5a3a01e5e0a63d7742f5dbac61b1bf3ef2a7c Mon Sep 17 00:00:00 2001 From: Sandy Tao Date: Tue, 10 Feb 2026 21:37:23 -0800 Subject: [PATCH] fix(cli): allow closing debug console after auto-open via flicker (#18795) --- packages/cli/src/ui/AppContainer.tsx | 30 ++---- .../cli/src/utils/devtoolsService.test.ts | 91 +++++++++++++++++++ packages/cli/src/utils/devtoolsService.ts | 29 ++++++ 3 files changed, 128 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index c18b9f24e8..7d863a638f 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1520,28 +1520,14 @@ Logging in with Google... Restarting Gemini CLI to continue. if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) { if (settings.merged.general.devtools) { void (async () => { - try { - const { startDevToolsServer } = await import( - '../utils/devtoolsService.js' - ); - const { openBrowserSecurely, shouldLaunchBrowser } = await import( - '@google/gemini-cli-core' - ); - const url = await startDevToolsServer(config); - if (shouldLaunchBrowser()) { - try { - await openBrowserSecurely(url); - } catch (e) { - setShowErrorDetails((prev) => !prev); - debugLogger.warn('Failed to open browser securely:', e); - } - } else { - setShowErrorDetails((prev) => !prev); - } - } catch (e) { - setShowErrorDetails(true); - debugLogger.error('Failed to start DevTools server:', e); - } + const { toggleDevToolsPanel } = await import( + '../utils/devtoolsService.js' + ); + await toggleDevToolsPanel( + config, + () => setShowErrorDetails((prev) => !prev), + () => setShowErrorDetails(true), + ); })(); } else { setShowErrorDetails((prev) => !prev); diff --git a/packages/cli/src/utils/devtoolsService.test.ts b/packages/cli/src/utils/devtoolsService.test.ts index 69d46dee7d..922d4d1483 100644 --- a/packages/cli/src/utils/devtoolsService.test.ts +++ b/packages/cli/src/utils/devtoolsService.test.ts @@ -70,12 +70,20 @@ vi.mock('./activityLogger.js', () => ({ }, })); +const mockShouldLaunchBrowser = vi.hoisted(() => vi.fn(() => true)); +const mockOpenBrowserSecurely = vi.hoisted(() => + vi.fn(() => Promise.resolve()), +); + vi.mock('@google/gemini-cli-core', () => ({ debugLogger: { log: vi.fn(), debug: vi.fn(), error: vi.fn(), + warn: vi.fn(), }, + shouldLaunchBrowser: mockShouldLaunchBrowser, + openBrowserSecurely: mockOpenBrowserSecurely, })); vi.mock('ws', () => ({ @@ -92,6 +100,7 @@ vi.mock('gemini-cli-devtools', () => ({ import { setupInitialActivityLogger, startDevToolsServer, + toggleDevToolsPanel, resetForTesting, } from './devtoolsService.js'; @@ -426,4 +435,86 @@ describe('devtoolsService', () => { expect(mockAddNetworkTransport).toHaveBeenCalledTimes(3); }); }); + + describe('toggleDevToolsPanel', () => { + it('calls toggle when browser opens successfully', async () => { + const config = createMockConfig(); + const toggle = vi.fn(); + const setOpen = vi.fn(); + + mockShouldLaunchBrowser.mockReturnValue(true); + mockOpenBrowserSecurely.mockResolvedValue(undefined); + mockDevToolsInstance.start.mockResolvedValue('http://127.0.0.1:25417'); + mockDevToolsInstance.getPort.mockReturnValue(25417); + + const promise = toggleDevToolsPanel(config, toggle, setOpen); + + await vi.waitFor(() => expect(MockWebSocket.instances.length).toBe(1)); + MockWebSocket.instances[0].simulateError(); + + await promise; + + expect(toggle).toHaveBeenCalledTimes(1); + expect(setOpen).not.toHaveBeenCalled(); + }); + + it('calls toggle when browser fails to open', async () => { + const config = createMockConfig(); + const toggle = vi.fn(); + const setOpen = vi.fn(); + + mockShouldLaunchBrowser.mockReturnValue(true); + mockOpenBrowserSecurely.mockRejectedValue(new Error('no browser')); + mockDevToolsInstance.start.mockResolvedValue('http://127.0.0.1:25417'); + mockDevToolsInstance.getPort.mockReturnValue(25417); + + const promise = toggleDevToolsPanel(config, toggle, setOpen); + + await vi.waitFor(() => expect(MockWebSocket.instances.length).toBe(1)); + MockWebSocket.instances[0].simulateError(); + + await promise; + + expect(toggle).toHaveBeenCalledTimes(1); + expect(setOpen).not.toHaveBeenCalled(); + }); + + it('calls toggle when shouldLaunchBrowser returns false', async () => { + const config = createMockConfig(); + const toggle = vi.fn(); + const setOpen = vi.fn(); + + mockShouldLaunchBrowser.mockReturnValue(false); + mockDevToolsInstance.start.mockResolvedValue('http://127.0.0.1:25417'); + mockDevToolsInstance.getPort.mockReturnValue(25417); + + const promise = toggleDevToolsPanel(config, toggle, setOpen); + + await vi.waitFor(() => expect(MockWebSocket.instances.length).toBe(1)); + MockWebSocket.instances[0].simulateError(); + + await promise; + + expect(toggle).toHaveBeenCalledTimes(1); + expect(setOpen).not.toHaveBeenCalled(); + }); + + it('calls setOpen when DevTools server fails to start', async () => { + const config = createMockConfig(); + const toggle = vi.fn(); + const setOpen = vi.fn(); + + mockDevToolsInstance.start.mockRejectedValue(new Error('fail')); + + const promise = toggleDevToolsPanel(config, toggle, setOpen); + + await vi.waitFor(() => expect(MockWebSocket.instances.length).toBe(1)); + MockWebSocket.instances[0].simulateError(); + + await promise; + + expect(toggle).not.toHaveBeenCalled(); + expect(setOpen).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/cli/src/utils/devtoolsService.ts b/packages/cli/src/utils/devtoolsService.ts index ed5fb36387..35abf0ec96 100644 --- a/packages/cli/src/utils/devtoolsService.ts +++ b/packages/cli/src/utils/devtoolsService.ts @@ -210,6 +210,35 @@ async function startDevToolsServerImpl(config: Config): Promise { return url; } +/** + * Handles the F12 key toggle for the DevTools panel. + * Starts the DevTools server, attempts to open the browser, + * and always calls the toggle callback regardless of the outcome. + */ +export async function toggleDevToolsPanel( + config: Config, + toggle: () => void, + setOpen: () => void, +): Promise { + try { + const { openBrowserSecurely, shouldLaunchBrowser } = await import( + '@google/gemini-cli-core' + ); + const url = await startDevToolsServer(config); + if (shouldLaunchBrowser()) { + try { + await openBrowserSecurely(url); + } catch (e) { + debugLogger.warn('Failed to open browser securely:', e); + } + } + toggle(); + } catch (e) { + setOpen(); + debugLogger.error('Failed to start DevTools server:', e); + } +} + /** Reset module-level state — test only. */ export function resetForTesting() { promotionAttempts = 0;