Compare commits

...

26 Commits

Author SHA1 Message Date
Cynthia Long 0ea49d486b tempt to fix browser bundle 2026-03-23 22:43:22 +00:00
Cynthia Long 4d4fc80f25 remove unncessary test change 2026-03-23 19:03:41 +00:00
cynthialong0-0 dfa56c37b4 Merge branch 'main' into fix/headless-log 2026-03-23 11:24:41 -07:00
cynthialong0-0 4249de5e23 Merge branch 'main' into fix/headless-log 2026-03-18 08:02:11 -07:00
cynthialong0-0 536bbdf73f Merge branch 'main' into fix/headless-log 2026-03-18 07:45:02 -07:00
Cynthia Long 201efbd07f update comment 2026-03-18 14:44:07 +00:00
Cynthia Long 8ae078b4d7 update comment 2026-03-18 14:43:25 +00:00
Cynthia Long d84b173330 rename headlessmode to interactive, add comments 2026-03-18 14:37:45 +00:00
cynthialong0-0 33d654e7d9 Merge branch 'main' into fix/headless-log 2026-03-17 16:08:29 -07:00
cynthialong0-0 6eb100cc41 Merge branch 'main' into fix/headless-log 2026-03-17 15:31:47 -07:00
Cynthia Long dca1ef09cb add debug to extensions-reload install command 2026-03-17 22:30:50 +00:00
cynthialong0-0 616c944350 Merge branch 'main' into fix/headless-log 2026-03-17 12:47:44 -07:00
Cynthia Long c0f919310e add -debug since it treat as noninteractive in e2e 2026-03-17 16:30:34 +00:00
Cynthia Long 92a09c8d8d Update tests 2026-03-17 16:29:45 +00:00
Cynthia Long ecae62e25a fix unit test 2026-03-17 14:32:04 +00:00
Cynthia Long abb4583554 introduce headless param 2026-03-17 14:19:45 +00:00
cynthialong0-0 fc7a269f0a Merge branch 'main' into fix/headless-log 2026-03-17 07:19:32 -07:00
cynthialong0-0 9c85ef1fda Merge branch 'main' into fix/headless-log 2026-03-16 21:55:25 -07:00
Cynthia Long bd60ba9e68 refactor the fix based on the e2e test failure 2026-03-17 04:43:10 +00:00
Cynthia Long 2e466bd4cb fix(cli): conditionally suppress console output in headless and non-interactive modes 2026-03-16 23:36:20 +00:00
Cynthia Long 55778583b9 update test 2026-03-16 22:05:01 +00:00
cynthialong0-0 661bd39362 Merge branch 'main' into fix/headless-log 2026-03-16 14:55:07 -07:00
Cynthia Long 04eb2ee9a3 test(cli): fix type errors and lint in console patcher tests 2026-03-16 21:45:11 +00:00
Cynthia Long c6f6fb00c9 add unit test for console patcher & noninteractive 2026-03-16 21:30:16 +00:00
Cynthia Long 02eff4efb3 set true/false based on if headless mode 2026-03-16 21:06:52 +00:00
Cynthia Long db9eef50ef add suppressConsoleOutput 2026-03-16 18:22:10 +00:00
9 changed files with 322 additions and 22 deletions
+10
View File
@@ -7,5 +7,15 @@
},
"general": {
"devtools": true
},
"agents": {
"overrides": {
"browser_agent": { "enabled": true }
},
"browser": {
"headless": true,
"sessionMode": "isolated",
"allowedDomains": ["*.com"]
}
}
}
+7 -3
View File
@@ -34,16 +34,20 @@ describe('extension install', () => {
writeFileSync(testServerPath, extension);
try {
const result = await rig.runCommand(
['extensions', 'install', `${rig.testDir!}`],
['--debug', 'extensions', 'install', `${rig.testDir!}`],
{ stdin: 'y\n' },
);
expect(result).toContain('test-extension-install');
const listResult = await rig.runCommand(['extensions', 'list']);
const listResult = await rig.runCommand([
'--debug',
'extensions',
'list',
]);
expect(listResult).toContain('test-extension-install');
writeFileSync(testServerPath, extensionUpdate);
const updateResult = await rig.runCommand(
['extensions', 'update', `test-extension-install`],
['--debug', 'extensions', 'update', `test-extension-install`],
{ stdin: 'y\n' },
);
expect(updateResult).toContain('0.0.2');
+1 -1
View File
@@ -66,7 +66,7 @@ describe('extension reloading', () => {
}
const result = await rig.runCommand(
['extensions', 'install', `${rig.testDir!}`],
['--debug', 'extensions', 'install', `${rig.testDir!}`],
{ stdin: 'y\n' },
);
expect(result).toContain('test-extension');
+2
View File
@@ -32,6 +32,7 @@ import {
ValidationRequiredError,
type AdminControlsSettings,
debugLogger,
isHeadlessMode,
} from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments } from './config/config.js';
@@ -296,6 +297,7 @@ export async function main() {
const isDebugMode = cliConfig.isDebugMode(argv);
const consolePatcher = new ConsolePatcher({
stderr: true,
interactive: isHeadlessMode() ? false : true,
debugMode: isDebugMode,
onNewMessage: (msg) => {
coreEvents.emitConsoleLog(msg.type, msg.content);
+1
View File
@@ -65,6 +65,7 @@ export async function runNonInteractive({
return promptIdContext.run(prompt_id, async () => {
const consolePatcher = new ConsolePatcher({
stderr: true,
interactive: false,
debugMode: config.getDebugMode(),
onNewMessage: (msg) => {
coreEvents.emitConsoleLog(msg.type, msg.content);
@@ -0,0 +1,236 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable no-console */
import { describe, it, expect, vi, afterEach } from 'vitest';
import { ConsolePatcher } from './ConsolePatcher.js';
describe('ConsolePatcher', () => {
let patcher: ConsolePatcher;
const onNewMessage = vi.fn();
afterEach(() => {
if (patcher) {
patcher.cleanup();
}
vi.restoreAllMocks();
vi.clearAllMocks();
});
it('should patch and restore console methods', () => {
const beforeLog = console.log;
const beforeWarn = console.warn;
const beforeError = console.error;
const beforeDebug = console.debug;
const beforeInfo = console.info;
patcher = new ConsolePatcher({ onNewMessage, debugMode: false });
patcher.patch();
expect(console.log).not.toBe(beforeLog);
expect(console.warn).not.toBe(beforeWarn);
expect(console.error).not.toBe(beforeError);
expect(console.debug).not.toBe(beforeDebug);
expect(console.info).not.toBe(beforeInfo);
patcher.cleanup();
expect(console.log).toBe(beforeLog);
expect(console.warn).toBe(beforeWarn);
expect(console.error).toBe(beforeError);
expect(console.debug).toBe(beforeDebug);
expect(console.info).toBe(beforeInfo);
});
describe('Interactive mode', () => {
it('should ignore log and info when it is not interactive and debugMode is false', () => {
patcher = new ConsolePatcher({
onNewMessage,
debugMode: false,
interactive: false,
});
patcher.patch();
console.log('test log');
console.info('test info');
expect(onNewMessage).not.toHaveBeenCalled();
});
it('should not ignore log and info when it is not interactive and debugMode is true', () => {
patcher = new ConsolePatcher({
onNewMessage,
debugMode: true,
interactive: false,
});
patcher.patch();
console.log('test log');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'log',
content: 'test log',
count: 1,
});
console.info('test info');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'info',
content: 'test info',
count: 1,
});
});
it('should not ignore log and info when it is interactive', () => {
patcher = new ConsolePatcher({
onNewMessage,
debugMode: false,
interactive: true,
});
patcher.patch();
console.log('test log');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'log',
content: 'test log',
count: 1,
});
console.info('test info');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'info',
content: 'test info',
count: 1,
});
});
});
describe('when stderr is false', () => {
it('should call onNewMessage for log, warn, error, and info', () => {
patcher = new ConsolePatcher({
onNewMessage,
debugMode: false,
stderr: false,
});
patcher.patch();
console.log('test log');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'log',
content: 'test log',
count: 1,
});
console.warn('test warn');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'warn',
content: 'test warn',
count: 1,
});
console.error('test error');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'error',
content: 'test error',
count: 1,
});
console.info('test info');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'info',
content: 'test info',
count: 1,
});
});
it('should not call onNewMessage for debug when debugMode is false', () => {
patcher = new ConsolePatcher({
onNewMessage,
debugMode: false,
stderr: false,
});
patcher.patch();
console.debug('test debug');
expect(onNewMessage).not.toHaveBeenCalled();
});
it('should call onNewMessage for debug when debugMode is true', () => {
patcher = new ConsolePatcher({
onNewMessage,
debugMode: true,
stderr: false,
});
patcher.patch();
console.debug('test debug');
expect(onNewMessage).toHaveBeenCalledWith({
type: 'debug',
content: 'test debug',
count: 1,
});
});
it('should format multiple arguments using util.format', () => {
patcher = new ConsolePatcher({
onNewMessage,
debugMode: false,
stderr: false,
});
patcher.patch();
console.log('test %s %d', 'string', 123);
expect(onNewMessage).toHaveBeenCalledWith({
type: 'log',
content: 'test string 123',
count: 1,
});
});
});
describe('when stderr is true', () => {
it('should redirect warn and error to originalConsoleError', () => {
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
patcher.patch();
console.warn('test warn');
expect(spyError).toHaveBeenCalledWith('test warn');
console.error('test error');
expect(spyError).toHaveBeenCalledWith('test error');
});
it('should redirect log and info to originalConsoleError when debugMode is true', () => {
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
patcher.patch();
console.log('test log');
expect(spyError).toHaveBeenCalledWith('test log');
console.info('test info');
expect(spyError).toHaveBeenCalledWith('test info');
});
it('should ignore debug when debugMode is false', () => {
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
patcher.patch();
console.debug('test debug');
expect(spyError).not.toHaveBeenCalled();
});
it('should redirect debug to originalConsoleError when debugMode is true', () => {
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
patcher.patch();
console.debug('test debug');
expect(spyError).toHaveBeenCalledWith('test debug');
});
});
});
+13 -5
View File
@@ -13,6 +13,7 @@ interface ConsolePatcherParams {
onNewMessage?: (message: Omit<ConsoleMessageItem, 'id'>) => void;
debugMode: boolean;
stderr?: boolean;
interactive?: boolean;
}
export class ConsolePatcher {
@@ -49,12 +50,19 @@ export class ConsolePatcher {
private patchConsoleMethod =
(type: 'log' | 'warn' | 'error' | 'debug' | 'info') =>
(...args: unknown[]) => {
if (this.params.stderr) {
if (type !== 'debug' || this.params.debugMode) {
this.originalConsoleError(this.formatArgs(args));
// When it is non interactive mode, do not show info logging unless
// it is debug mode. default to true if it is undefined.
if (this.params.interactive === false) {
if ((type === 'info' || type === 'log') && !this.params.debugMode) {
return;
}
} else {
if (type !== 'debug' || this.params.debugMode) {
}
// When it is in the debug mode, redirect console output to stderr
// depending on if it is stderr only mode.
if (type !== 'debug' || this.params.debugMode) {
if (this.params.stderr) {
this.originalConsoleError(this.formatArgs(args));
} else {
this.params.onNewMessage?.({
type,
content: this.formatArgs(args),
+45 -8
View File
@@ -83,18 +83,55 @@ async function bundle() {
}
fs.cpSync(srcThirdParty, destThirdParty, {
recursive: true,
filter: (src) => {
// Skip large/unnecessary bundles that are either explicitly excluded
// or not required for the browser agent functionality.
return (
!src.includes('lighthouse-devtools-mcp-bundle.js') &&
!src.includes('devtools-formatter-worker.js')
);
},
});
} else {
console.warn(`Warning: third_party assets not found at ${srcThirdParty}`);
}
// Copy watchdog scripts and dependencies
const srcTelemetry = path.resolve(
__dirname,
'../../../node_modules/chrome-devtools-mcp/build/src/telemetry',
);
const destWatchdog = path.resolve(
__dirname,
'../dist/bundled/watchdog',
);
if (fs.existsSync(srcTelemetry)) {
fs.mkdirSync(destWatchdog, { recursive: true });
// Copy main watchdog directory
fs.cpSync(path.join(srcTelemetry, 'watchdog'), destWatchdog, { recursive: true });
// Copy shared types needed by watchdog
fs.copyFileSync(
path.join(srcTelemetry, 'types.js'),
path.resolve(__dirname, '../dist/bundled/types.js')
);
// Copy logger needed by watchdog
fs.copyFileSync(
path.join(srcTelemetry, '../logger.js'),
path.resolve(__dirname, '../dist/bundled/logger.js')
);
// Patch imports in watchdog files to reflect the flattened structure in dist/bundled/
const watchdogFiles = fs.readdirSync(destWatchdog);
for (const file of watchdogFiles) {
if (file.endsWith('.js')) {
const filePath = path.join(destWatchdog, file);
let content = fs.readFileSync(filePath, 'utf-8');
content = content.replace(/\.\.\/\.\.\/logger\.js/g, '../logger.js');
content = content.replace(/\.\.\/types\.js/g, '../types.js');
fs.writeFileSync(filePath, content);
}
}
} else {
console.warn(`Warning: telemetry directory not found at ${srcTelemetry}`);
}
// Patch the bundled file to point to the correct watchdog path
// The original code uses new URL("./watchdog/main.js", import.meta.url)
// which resolves relative to the bundled file.
// Our bundling script copies the watchdog to ./watchdog/main.js relative to the bundle.
// So the original code should work IF esbuild doesn't mangle import.meta.url.
} catch (error) {
console.error('Error bundling chrome-devtools-mcp:', error);
process.exit(1);
@@ -386,7 +386,7 @@ export class BrowserManager {
})
.join(', ');
mcpArgs.push(
`--chromeArg="--host-rules=MAP * 127.0.0.1, ${exclusionRules}, EXCLUDE 127.0.0.1"`,
`--chromeArg=--host-rules=MAP * 127.0.0.1, ${exclusionRules}, EXCLUDE 127.0.0.1`,
);
}
@@ -427,10 +427,12 @@ export class BrowserManager {
}
this.mcpTransport.onclose = () => {
debugLogger.error(
'chrome-devtools-mcp transport closed unexpectedly. ' +
'The MCP server process may have crashed.',
);
if (this.rawMcpClient) {
debugLogger.error(
'chrome-devtools-mcp transport closed unexpectedly. ' +
'The MCP server process may have crashed.',
);
}
this.rawMcpClient = undefined;
};
this.mcpTransport.onerror = (error: Error) => {