Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 8 additions & 22 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
91 changes: 91 additions & 0 deletions packages/cli/src/utils/devtoolsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -92,6 +100,7 @@ vi.mock('gemini-cli-devtools', () => ({
import {
setupInitialActivityLogger,
startDevToolsServer,
toggleDevToolsPanel,
resetForTesting,
} from './devtoolsService.js';

Expand Down Expand Up @@ -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);
});
});
});
29 changes: 29 additions & 0 deletions packages/cli/src/utils/devtoolsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,35 @@ async function startDevToolsServerImpl(config: Config): Promise<string> {
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<void> {
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;
Expand Down
Loading