diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 78e31aa27e..7994816efc 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -6,6 +6,7 @@ import type { Config } from '@google/gemini-cli-core'; import { + debugLogger, KittySequenceOverflowEvent, logKittySequenceOverflow, } from '@google/gemini-cli-core'; @@ -450,7 +451,7 @@ export function KeypressProvider({ const flushKittyBufferOnInterrupt = (reason: string) => { if (kittySequenceBuffer) { if (debugKeystrokeLogging) { - console.log( + debugLogger.log( `[DEBUG] Kitty sequence flushed due to ${reason}:`, JSON.stringify(kittySequenceBuffer), ); @@ -585,7 +586,7 @@ export function KeypressProvider({ key.sequence === `${ESC}${KITTY_CTRL_C}` ) { if (kittySequenceBuffer && debugKeystrokeLogging) { - console.log( + debugLogger.log( '[DEBUG] Kitty buffer cleared on Ctrl+C:', kittySequenceBuffer, ); @@ -631,7 +632,7 @@ export function KeypressProvider({ kittySequenceBuffer += key.sequence; if (debugKeystrokeLogging) { - console.log( + debugLogger.log( '[DEBUG] Kitty buffer accumulating:', JSON.stringify(kittySequenceBuffer), ); @@ -647,7 +648,7 @@ export function KeypressProvider({ if (parsed) { if (debugKeystrokeLogging) { const parsedSequence = remainingBuffer.slice(0, parsed.length); - console.log( + debugLogger.log( '[DEBUG] Kitty sequence parsed successfully:', JSON.stringify(parsedSequence), ); @@ -664,7 +665,7 @@ export function KeypressProvider({ if (nextEscIndex !== -1) { const garbage = remainingBuffer.slice(0, nextEscIndex); if (debugKeystrokeLogging) { - console.log( + debugLogger.log( '[DEBUG] Dropping incomplete sequence before next ESC:', JSON.stringify(garbage), ); @@ -681,7 +682,7 @@ export function KeypressProvider({ if (!couldBeValid) { // Not a kitty sequence - flush as regular input immediately if (debugKeystrokeLogging) { - console.log( + debugLogger.log( '[DEBUG] Not a kitty sequence, flushing:', JSON.stringify(remainingBuffer), ); @@ -699,7 +700,7 @@ export function KeypressProvider({ } else if (remainingBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) { // Buffer overflow - log and clear if (debugKeystrokeLogging) { - console.log( + debugLogger.log( '[DEBUG] Kitty buffer overflow, clearing:', JSON.stringify(remainingBuffer), ); @@ -724,7 +725,7 @@ export function KeypressProvider({ parsedAny = true; } else { if (config?.getDebugMode() || debugKeystrokeLogging) { - console.warn( + debugLogger.warn( 'Kitty sequence buffer has content:', JSON.stringify(kittySequenceBuffer), ); @@ -733,7 +734,7 @@ export function KeypressProvider({ kittySequenceTimeout = setTimeout(() => { if (kittySequenceBuffer) { if (debugKeystrokeLogging) { - console.log( + debugLogger.log( '[DEBUG] Kitty sequence timeout, flushing:', JSON.stringify(kittySequenceBuffer), ); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cb5ff1dd11..bc8fb83308 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -58,6 +58,7 @@ export * from './utils/ignorePatterns.js'; export * from './utils/partUtils.js'; export * from './utils/promptIdContext.js'; export * from './utils/thoughtUtils.js'; +export * from './utils/debugLogger.js'; // Export services export * from './services/fileDiscoveryService.js'; diff --git a/packages/core/src/utils/debugLogger.test.ts b/packages/core/src/utils/debugLogger.test.ts new file mode 100644 index 0000000000..ac52f2f691 --- /dev/null +++ b/packages/core/src/utils/debugLogger.test.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { debugLogger } from './debugLogger.js'; + +describe('DebugLogger', () => { + // Spy on all console methods before each test + beforeEach(() => { + vi.spyOn(console, 'log').mockImplementation(() => {}); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.spyOn(console, 'debug').mockImplementation(() => {}); + }); + + // Restore original console methods after each test + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should call console.log with the correct arguments', () => { + const message = 'This is a log message'; + const data = { key: 'value' }; + debugLogger.log(message, data); + expect(console.log).toHaveBeenCalledWith(message, data); + expect(console.log).toHaveBeenCalledTimes(1); + }); + + it('should call console.warn with the correct arguments', () => { + const message = 'This is a warning message'; + const data = [1, 2, 3]; + debugLogger.warn(message, data); + expect(console.warn).toHaveBeenCalledWith(message, data); + expect(console.warn).toHaveBeenCalledTimes(1); + }); + + it('should call console.error with the correct arguments', () => { + const message = 'This is an error message'; + const error = new Error('Something went wrong'); + debugLogger.error(message, error); + expect(console.error).toHaveBeenCalledWith(message, error); + expect(console.error).toHaveBeenCalledTimes(1); + }); + + it('should call console.debug with the correct arguments', () => { + const message = 'This is a debug message'; + const obj = { a: { b: 'c' } }; + debugLogger.debug(message, obj); + expect(console.debug).toHaveBeenCalledWith(message, obj); + expect(console.debug).toHaveBeenCalledTimes(1); + }); + + it('should handle multiple arguments correctly for all methods', () => { + debugLogger.log('one', 2, true); + expect(console.log).toHaveBeenCalledWith('one', 2, true); + + debugLogger.warn('one', 2, false); + expect(console.warn).toHaveBeenCalledWith('one', 2, false); + + debugLogger.error('one', 2, null); + expect(console.error).toHaveBeenCalledWith('one', 2, null); + + debugLogger.debug('one', 2, undefined); + expect(console.debug).toHaveBeenCalledWith('one', 2, undefined); + }); + + it('should handle calls with no arguments', () => { + debugLogger.log(); + expect(console.log).toHaveBeenCalledWith(); + expect(console.log).toHaveBeenCalledTimes(1); + + debugLogger.warn(); + expect(console.warn).toHaveBeenCalledWith(); + expect(console.warn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/core/src/utils/debugLogger.ts b/packages/core/src/utils/debugLogger.ts new file mode 100644 index 0000000000..e16d045adb --- /dev/null +++ b/packages/core/src/utils/debugLogger.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * A simple, centralized logger for developer-facing debug messages. + * + * WHY USE THIS? + * - It makes the INTENT of the log clear (it's for developers, not users). + * - It provides a single point of control for debug logging behavior. + * - We can lint against direct `console.*` usage to enforce this pattern. + * + * HOW IT WORKS: + * This is a thin wrapper around the native `console` object. The `ConsolePatcher` + * will intercept these calls and route them to the debug drawer UI. + */ +class DebugLogger { + log(...args: unknown[]): void { + console.log(...args); + } + + warn(...args: unknown[]): void { + console.warn(...args); + } + + error(...args: unknown[]): void { + console.error(...args); + } + + debug(...args: unknown[]): void { + console.debug(...args); + } +} + +export const debugLogger = new DebugLogger();