mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
Protect stdout and stderr so JavaScript code can't accidentally write to stdout corrupting ink rendering (#13247)
Bypassing rules as link checker failure is spurious.
This commit is contained in:
@@ -33,7 +33,7 @@ import {
|
||||
const mockCoreEvents = vi.hoisted(() => ({
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
drainFeedbackBacklog: vi.fn(),
|
||||
drainBacklogs: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -42,6 +42,11 @@ const mockIdeClient = vi.hoisted(() => ({
|
||||
getInstance: vi.fn().mockReturnValue(new Promise(() => {})),
|
||||
}));
|
||||
|
||||
// Mock stdout
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockStdout: { write: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
@@ -61,12 +66,11 @@ import {
|
||||
} from './contexts/UIActionsContext.js';
|
||||
|
||||
// Mock useStdout to capture terminal title writes
|
||||
let mockStdout: { write: ReturnType<typeof vi.fn> };
|
||||
vi.mock('ink', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('ink')>();
|
||||
return {
|
||||
...actual,
|
||||
useStdout: () => ({ stdout: mockStdout }),
|
||||
useStdout: () => ({ stdout: mocks.mockStdout }),
|
||||
measureElement: vi.fn(),
|
||||
};
|
||||
});
|
||||
@@ -122,6 +126,19 @@ vi.mock('./utils/mouse.js', () => ({
|
||||
enableMouseEvents: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils/stdio.js', () => ({
|
||||
writeToStdout: vi.fn((...args) =>
|
||||
process.stdout.write(...(args as Parameters<typeof process.stdout.write>)),
|
||||
),
|
||||
writeToStderr: vi.fn((...args) =>
|
||||
process.stderr.write(...(args as Parameters<typeof process.stderr.write>)),
|
||||
),
|
||||
patchStdio: vi.fn(() => () => {}),
|
||||
createInkStdio: vi.fn(() => ({
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
})),
|
||||
}));
|
||||
|
||||
import { useHistory } from './hooks/useHistoryManager.js';
|
||||
import { useThemeCommand } from './hooks/useThemeCommand.js';
|
||||
@@ -149,6 +166,7 @@ import { useTerminalSize } from './hooks/useTerminalSize.js';
|
||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||
import { enableMouseEvents, disableMouseEvents } from './utils/mouse.js';
|
||||
import { writeToStdout } from '../utils/stdio.js';
|
||||
|
||||
describe('AppContainer State Management', () => {
|
||||
let mockConfig: Config;
|
||||
@@ -215,7 +233,7 @@ describe('AppContainer State Management', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Initialize mock stdout for terminal title tests
|
||||
mockStdout = { write: vi.fn() };
|
||||
mocks.mockStdout.write.mockClear();
|
||||
|
||||
// Mock computeWindowTitle function to centralize title logic testing
|
||||
vi.mock('../utils/windowTitle.js', async () => ({
|
||||
@@ -886,7 +904,13 @@ describe('AppContainer State Management', () => {
|
||||
describe('Terminal Title Update Feature', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mock stdout for each test
|
||||
mockStdout = { write: vi.fn() };
|
||||
mocks.mockStdout.write.mockClear();
|
||||
});
|
||||
|
||||
it('verifies useStdout is mocked', async () => {
|
||||
const { useStdout } = await import('ink');
|
||||
const { stdout } = useStdout();
|
||||
expect(stdout).toBe(mocks.mockStdout);
|
||||
});
|
||||
|
||||
it('should not update terminal title when showStatusInTitle is false', () => {
|
||||
@@ -909,9 +933,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that no title-related writes occurred
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(0);
|
||||
unmount();
|
||||
});
|
||||
@@ -936,9 +961,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that no title-related writes occurred
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(0);
|
||||
unmount();
|
||||
});
|
||||
@@ -974,9 +1000,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that title was updated with thought subject
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(1);
|
||||
expect(titleWrites[0][0]).toBe(
|
||||
`\x1b]2;${thoughtSubject.padEnd(80, ' ')}\x07`,
|
||||
@@ -1014,9 +1041,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that title was updated with default Idle text
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(1);
|
||||
expect(titleWrites[0][0]).toBe(
|
||||
`\x1b]2;${'Gemini - workspace'.padEnd(80, ' ')}\x07`,
|
||||
@@ -1055,9 +1083,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that title was updated with confirmation text
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(1);
|
||||
expect(titleWrites[0][0]).toBe(
|
||||
`\x1b]2;${thoughtSubject.padEnd(80, ' ')}\x07`,
|
||||
@@ -1096,9 +1125,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that title is padded to exactly 80 characters
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(1);
|
||||
const calledWith = titleWrites[0][0];
|
||||
const expectedTitle = shortTitle.padEnd(80, ' ');
|
||||
@@ -1141,9 +1171,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that the correct ANSI escape sequence is used
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(1);
|
||||
const expectedEscapeSequence = `\x1b]2;${title.padEnd(80, ' ')}\x07`;
|
||||
expect(titleWrites[0][0]).toBe(expectedEscapeSequence);
|
||||
@@ -1183,9 +1214,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Assert: Check that title was updated with CLI_TITLE value
|
||||
const titleWrites = mockStdout.write.mock.calls.filter((call) =>
|
||||
const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
|
||||
call[0].includes('\x1b]2;'),
|
||||
);
|
||||
|
||||
expect(titleWrites).toHaveLength(1);
|
||||
expect(titleWrites[0][0]).toBe(
|
||||
`\x1b]2;${'Custom Gemini Title'.padEnd(80, ' ')}\x07`,
|
||||
@@ -1493,7 +1525,7 @@ describe('AppContainer State Management', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockStdout.write.mockClear();
|
||||
mocks.mockStdout.write.mockClear();
|
||||
mockedUseKeypress.mockImplementation((callback: (key: Key) => void) => {
|
||||
handleGlobalKeypress = callback;
|
||||
});
|
||||
@@ -1518,7 +1550,7 @@ describe('AppContainer State Management', () => {
|
||||
])('$modeName', ({ isAlternateMode, shouldEnable }) => {
|
||||
it(`should ${shouldEnable ? 'toggle' : 'NOT toggle'} mouse off when Ctrl+S is pressed`, async () => {
|
||||
await setupCopyModeTest(isAlternateMode);
|
||||
mockStdout.write.mockClear(); // Clear initial enable call
|
||||
mocks.mockStdout.write.mockClear(); // Clear initial enable call
|
||||
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
@@ -1544,7 +1576,7 @@ describe('AppContainer State Management', () => {
|
||||
if (shouldEnable) {
|
||||
it('should toggle mouse back on when Ctrl+S is pressed again', async () => {
|
||||
await setupCopyModeTest(isAlternateMode);
|
||||
mockStdout.write.mockClear();
|
||||
(writeToStdout as Mock).mockClear();
|
||||
|
||||
// Turn it on (disable mouse)
|
||||
act(() => {
|
||||
@@ -1596,7 +1628,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
rerender();
|
||||
|
||||
mockStdout.write.mockClear();
|
||||
(writeToStdout as Mock).mockClear();
|
||||
|
||||
// Press any other key
|
||||
act(() => {
|
||||
@@ -1665,7 +1697,7 @@ describe('AppContainer State Management', () => {
|
||||
CoreEvent.UserFeedback,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCoreEvents.drainFeedbackBacklog).toHaveBeenCalledTimes(1);
|
||||
expect(mockCoreEvents.drainBacklogs).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -1782,7 +1814,7 @@ describe('AppContainer State Management', () => {
|
||||
// Helper to extract arguments from the useGeminiStream hook call
|
||||
// This isolates the positional argument dependency to a single location
|
||||
const extractUseGeminiStreamArgs = (args: unknown[]) => ({
|
||||
onCancelSubmit: args[14] as (shouldRestorePrompt?: boolean) => void,
|
||||
onCancelSubmit: args[13] as (shouldRestorePrompt?: boolean) => void,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1846,9 +1878,19 @@ describe('AppContainer State Management', () => {
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
// Mock logger to resolve so userMessages gets populated
|
||||
let resolveLoggerPromise: (val: string[]) => void;
|
||||
const loggerPromise = new Promise<string[]>((resolve) => {
|
||||
resolveLoggerPromise = resolve;
|
||||
});
|
||||
|
||||
// Mock logger to control when userMessages updates
|
||||
const getPreviousUserMessagesMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([]) // Initial mount
|
||||
.mockReturnValueOnce(loggerPromise); // Second render (simulated update)
|
||||
|
||||
mockedUseLogger.mockReturnValue({
|
||||
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
|
||||
getPreviousUserMessages: getPreviousUserMessagesMock,
|
||||
});
|
||||
|
||||
const { unmount, rerender } = renderAppContainer();
|
||||
@@ -1871,20 +1913,25 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Rerender to reflect the history change.
|
||||
// This triggers the effect to update userMessages, but it's async.
|
||||
// This triggers the effect to update userMessages, but it hangs on loggerPromise.
|
||||
rerender(getAppContainer());
|
||||
|
||||
const { onCancelSubmit } = extractUseGeminiStreamArgs(
|
||||
mockedUseGeminiStream.mock.lastCall!,
|
||||
);
|
||||
|
||||
// Call onCancelSubmit immediately (simulating the race condition where
|
||||
// the overflow event comes in before the effect updates userMessages)
|
||||
// Call onCancelSubmit immediately. userMessages is still stale (has only 'Previous Prompt')
|
||||
// because the effect is waiting on loggerPromise.
|
||||
act(() => {
|
||||
onCancelSubmit(true);
|
||||
});
|
||||
|
||||
// With the fix, it should wait for userMessages to update and then set the new prompt
|
||||
// Now resolve the promise to let the effect complete and update userMessages
|
||||
await act(async () => {
|
||||
resolveLoggerPromise!([]);
|
||||
});
|
||||
|
||||
// With the fix, it should have waited for userMessages to update and then set the new prompt
|
||||
await waitFor(() => {
|
||||
expect(mockSetText).toHaveBeenCalledWith(newPrompt);
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ import { useVimMode } from './contexts/VimModeContext.js';
|
||||
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
|
||||
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
||||
import { calculatePromptWidths } from './components/InputPrompt.js';
|
||||
import { useStdout, useStdin } from 'ink';
|
||||
import { useApp, useStdout, useStdin } from 'ink';
|
||||
import { calculateMainAreaWidth } from './utils/ui-sizing.js';
|
||||
import ansiEscapes from 'ansi-escapes';
|
||||
import * as fs from 'node:fs';
|
||||
@@ -91,7 +91,6 @@ import { type IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
|
||||
import { appEvents, AppEvent } from '../utils/events.js';
|
||||
import { type UpdateObject } from './utils/updateCheck.js';
|
||||
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
||||
import { ConsolePatcher } from './utils/ConsolePatcher.js';
|
||||
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||
@@ -111,6 +110,7 @@ import { disableMouseEvents, enableMouseEvents } from './utils/mouse.js';
|
||||
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
||||
import { useSettings } from './contexts/SettingsContext.js';
|
||||
import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js';
|
||||
import { writeToStdout } from '../utils/stdio.js';
|
||||
|
||||
const WARNING_PROMPT_DURATION_MS = 1000;
|
||||
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
|
||||
@@ -250,6 +250,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize();
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
const { stdout } = useStdout();
|
||||
const app = useApp();
|
||||
|
||||
// Additional hooks moved from App.tsx
|
||||
const { stats: sessionStats } = useSessionStats();
|
||||
@@ -304,20 +305,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
};
|
||||
}, [getEffectiveModel]);
|
||||
|
||||
const {
|
||||
consoleMessages,
|
||||
handleNewMessage,
|
||||
clearConsoleMessages: clearConsoleMessagesState,
|
||||
} = useConsoleMessages();
|
||||
|
||||
useEffect(() => {
|
||||
const consolePatcher = new ConsolePatcher({
|
||||
onNewMessage: handleNewMessage,
|
||||
debugMode: config.getDebugMode(),
|
||||
});
|
||||
consolePatcher.patch();
|
||||
registerCleanup(consolePatcher.cleanup);
|
||||
}, [handleNewMessage, config]);
|
||||
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
|
||||
useConsoleMessages();
|
||||
|
||||
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, settings);
|
||||
// Derive widths for InputPrompt using shared helper
|
||||
@@ -381,12 +370,25 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
stdout.write(ansiEscapes.clearTerminal);
|
||||
}
|
||||
setHistoryRemountKey((prev) => prev + 1);
|
||||
}, [setHistoryRemountKey, stdout, isAlternateBuffer]);
|
||||
|
||||
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
|
||||
const handleEditorClose = useCallback(() => {
|
||||
if (isAlternateBuffer) {
|
||||
// The editor may have exited alternate buffer mode so we need to
|
||||
// enter it again to be safe.
|
||||
writeToStdout(ansiEscapes.enterAlternativeScreen);
|
||||
enableMouseEvents();
|
||||
app.rerender();
|
||||
}
|
||||
enableSupportedProtocol();
|
||||
refreshStatic();
|
||||
}, [refreshStatic]);
|
||||
}, [refreshStatic, isAlternateBuffer, app]);
|
||||
|
||||
useEffect(() => {
|
||||
coreEvents.on(CoreEvent.ExternalEditorClosed, handleEditorClose);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.ExternalEditorClosed, handleEditorClose);
|
||||
};
|
||||
}, [handleEditorClose]);
|
||||
|
||||
const {
|
||||
isThemeDialogOpen,
|
||||
@@ -717,7 +719,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
handleEditorClose,
|
||||
onCancelSubmit,
|
||||
setEmbeddedShellFocused,
|
||||
terminalWidth,
|
||||
@@ -1034,20 +1035,10 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
};
|
||||
appEvents.on(AppEvent.OpenDebugConsole, openDebugConsole);
|
||||
|
||||
const logErrorHandler = (errorMessage: unknown) => {
|
||||
handleNewMessage({
|
||||
type: 'error',
|
||||
content: String(errorMessage),
|
||||
count: 1,
|
||||
});
|
||||
};
|
||||
appEvents.on(AppEvent.LogError, logErrorHandler);
|
||||
|
||||
return () => {
|
||||
appEvents.off(AppEvent.OpenDebugConsole, openDebugConsole);
|
||||
appEvents.off(AppEvent.LogError, logErrorHandler);
|
||||
};
|
||||
}, [handleNewMessage, config]);
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ctrlCTimerRef.current) {
|
||||
@@ -1283,7 +1274,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
|
||||
// Flush any messages that happened during startup before this component
|
||||
// mounted.
|
||||
coreEvents.drainFeedbackBacklog();
|
||||
coreEvents.drainBacklogs();
|
||||
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { AuthDialog } from './AuthDialog.js';
|
||||
import { AuthType, type Config } from '@google/gemini-cli-core';
|
||||
import { AuthType, type Config, debugLogger } from '@google/gemini-cli-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { AuthState } from '../types.js';
|
||||
@@ -232,7 +232,7 @@ describe('AuthDialog', () => {
|
||||
const exitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const logSpy = vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
|
||||
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from './setupGithubCommand.js';
|
||||
import type { CommandContext, ToolActionReturn } from './types.js';
|
||||
import * as commandUtils from '../utils/commandUtils.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('child_process');
|
||||
|
||||
@@ -257,7 +258,9 @@ describe('updateGitignore', () => {
|
||||
});
|
||||
|
||||
it('handles permission errors gracefully', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(debugLogger, 'debug')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const fsModule = await import('node:fs');
|
||||
const writeFileSpy = vi
|
||||
|
||||
@@ -17,6 +17,7 @@ import { debugState } from '../debug.js';
|
||||
describe('DebugProfiler', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
profiler.profilersActive = 1;
|
||||
profiler.numFrames = 0;
|
||||
profiler.totalIdleFrames = 0;
|
||||
profiler.lastFrameStartTime = 0;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { theme } from '../semantic-colors.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { debugState } from '../debug.js';
|
||||
import { appEvents, AppEvent } from '../../utils/events.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
// Frames that render at least this far before or after an action are considered
|
||||
// idle frames.
|
||||
@@ -21,6 +22,7 @@ export const FRAME_TIMESTAMP_CAPACITY = 2048;
|
||||
|
||||
// Exported for testing purposes.
|
||||
export const profiler = {
|
||||
profilersActive: 0,
|
||||
numFrames: 0,
|
||||
totalIdleFrames: 0,
|
||||
totalFlickerFrames: 0,
|
||||
@@ -47,25 +49,25 @@ export const profiler = {
|
||||
},
|
||||
|
||||
reportFrameRendered() {
|
||||
if (this.profilersActive === 0) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
// Simple frame detection logic (a write after at least 16ms is a new frame)
|
||||
if (now - this.lastFrameStartTime > 16) {
|
||||
this.lastFrameStartTime = now;
|
||||
this.numFrames++;
|
||||
if (debugState.debugNumAnimatedComponents === 0) {
|
||||
if (this.possiblyIdleFrameTimestamps.size >= FRAME_TIMESTAMP_CAPACITY) {
|
||||
this.possiblyIdleFrameTimestamps.shift();
|
||||
}
|
||||
this.possiblyIdleFrameTimestamps.push(now);
|
||||
} else {
|
||||
// If a spinner is present, consider this an action that both prevents
|
||||
// this frame from being idle and also should prevent a follow on frame
|
||||
// from being considered idle.
|
||||
if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
|
||||
this.actionTimestamps.shift();
|
||||
}
|
||||
this.actionTimestamps.push(now);
|
||||
this.lastFrameStartTime = now;
|
||||
this.numFrames++;
|
||||
if (debugState.debugNumAnimatedComponents === 0) {
|
||||
if (this.possiblyIdleFrameTimestamps.size >= FRAME_TIMESTAMP_CAPACITY) {
|
||||
this.possiblyIdleFrameTimestamps.shift();
|
||||
}
|
||||
this.possiblyIdleFrameTimestamps.push(now);
|
||||
} else {
|
||||
// If a spinner is present, consider this an action that both prevents
|
||||
// this frame from being idle and also should prevent a follow on frame
|
||||
// from being considered idle.
|
||||
if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
|
||||
this.actionTimestamps.shift();
|
||||
}
|
||||
this.actionTimestamps.push(now);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -108,8 +110,7 @@ export const profiler = {
|
||||
this.openedDebugConsole = true;
|
||||
appEvents.emit(AppEvent.OpenDebugConsole);
|
||||
}
|
||||
appEvents.emit(
|
||||
AppEvent.LogError,
|
||||
debugLogger.error(
|
||||
`${idleInPastSecond} frames rendered while the app was ` +
|
||||
`idle in the past second. This likely indicates severe infinite loop ` +
|
||||
`React state management bugs.`,
|
||||
@@ -130,8 +131,7 @@ export const profiler = {
|
||||
|
||||
if (!this.hasLoggedFirstFlicker) {
|
||||
this.hasLoggedFirstFlicker = true;
|
||||
appEvents.emit(
|
||||
AppEvent.LogError,
|
||||
debugLogger.error(
|
||||
'A flicker frame was detected. This will cause UI instability. Type `/profile` for more info.',
|
||||
);
|
||||
}
|
||||
@@ -149,6 +149,7 @@ export const DebugProfiler = () => {
|
||||
|
||||
// Effect for listening to stdin for keypresses and stdout for resize events.
|
||||
useEffect(() => {
|
||||
profiler.profilersActive++;
|
||||
const stdin = process.stdin;
|
||||
const stdout = process.stdout;
|
||||
|
||||
@@ -162,31 +163,7 @@ export const DebugProfiler = () => {
|
||||
return () => {
|
||||
stdin.off('data', handler);
|
||||
stdout.off('resize', handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Effect for patching stdout to count frames and detect idle ones
|
||||
useEffect(() => {
|
||||
const originalWrite = process.stdout.write;
|
||||
const boundOriginalWrite = originalWrite.bind(process.stdout);
|
||||
|
||||
process.stdout.write = (
|
||||
chunk: Uint8Array | string,
|
||||
encodingOrCb?:
|
||||
| BufferEncoding
|
||||
| ((err?: NodeJS.ErrnoException | null) => void),
|
||||
cb?: (err?: NodeJS.ErrnoException | null) => void,
|
||||
) => {
|
||||
profiler.reportFrameRendered();
|
||||
|
||||
if (typeof encodingOrCb === 'function') {
|
||||
return boundOriginalWrite(chunk, encodingOrCb);
|
||||
}
|
||||
return boundOriginalWrite(chunk, encodingOrCb, cb);
|
||||
};
|
||||
|
||||
return () => {
|
||||
process.stdout.write = originalWrite;
|
||||
profiler.profilersActive--;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import pathMod from 'node:path';
|
||||
import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||
import { unescapePath } from '@google/gemini-cli-core';
|
||||
import { unescapePath, coreEvents, CoreEvent } from '@google/gemini-cli-core';
|
||||
import {
|
||||
toCodePoints,
|
||||
cpLen,
|
||||
@@ -1893,6 +1893,7 @@ export function useTextBuffer({
|
||||
console.error('[useTextBuffer] external editor error', err);
|
||||
} finally {
|
||||
enableSupportedProtocol();
|
||||
coreEvents.emit(CoreEvent.ExternalEditorClosed);
|
||||
if (wasRaw) setRawMode?.(true);
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import type React from 'react';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
@@ -322,17 +323,16 @@ describe('KeypressContext', () => {
|
||||
});
|
||||
|
||||
describe('debug keystroke logging', () => {
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
||||
let debugLoggerSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
debugLoggerSpy = vi
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
debugLoggerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not log keystrokes when debugKeystrokeLogging is false', async () => {
|
||||
@@ -354,7 +354,7 @@ describe('KeypressContext', () => {
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalled();
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect(debugLoggerSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('[DEBUG] Kitty'),
|
||||
);
|
||||
});
|
||||
@@ -375,7 +375,7 @@ describe('KeypressContext', () => {
|
||||
// Send a complete kitty sequence for escape
|
||||
act(() => stdin.write('\x1b[27u'));
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerSpy).toHaveBeenCalledWith(
|
||||
`[DEBUG] Raw StdIn: ${JSON.stringify('\x1b[27u')}`,
|
||||
);
|
||||
});
|
||||
@@ -397,7 +397,7 @@ describe('KeypressContext', () => {
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Verify debug logging for accumulation
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(debugLoggerSpy).toHaveBeenCalledWith(
|
||||
`[DEBUG] Raw StdIn: ${JSON.stringify(INCOMPLETE_KITTY_SEQUENCE)}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { writeToStdout } from '../../utils/stdio.js';
|
||||
|
||||
const ENABLE_BRACKETED_PASTE = '\x1b[?2004h';
|
||||
const DISABLE_BRACKETED_PASTE = '\x1b[?2004l';
|
||||
@@ -17,11 +18,11 @@ const DISABLE_BRACKETED_PASTE = '\x1b[?2004l';
|
||||
*/
|
||||
export const useBracketedPaste = () => {
|
||||
const cleanup = () => {
|
||||
process.stdout.write(DISABLE_BRACKETED_PASTE);
|
||||
writeToStdout(DISABLE_BRACKETED_PASTE);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
process.stdout.write(ENABLE_BRACKETED_PASTE);
|
||||
writeToStdout(ENABLE_BRACKETED_PASTE);
|
||||
|
||||
process.on('exit', cleanup);
|
||||
process.on('SIGINT', cleanup);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { act, useEffect } from 'react';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useCommandCompletion } from './useCommandCompletion.js';
|
||||
import type { CommandContext } from '../commands/types.js';
|
||||
@@ -132,7 +132,7 @@ describe('useCommandCompletion', () => {
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
renderWithProviders(<TestComponent />);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
@@ -516,7 +516,7 @@ describe('useCommandCompletion', () => {
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
renderWithProviders(<TestComponent />);
|
||||
|
||||
// Should not trigger prompt completion for comments
|
||||
expect(hookResult!.suggestions.length).toBe(0);
|
||||
@@ -549,7 +549,7 @@ describe('useCommandCompletion', () => {
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
renderWithProviders(<TestComponent />);
|
||||
|
||||
// Should not trigger prompt completion for comments
|
||||
expect(hookResult!.suggestions.length).toBe(0);
|
||||
@@ -582,7 +582,7 @@ describe('useCommandCompletion', () => {
|
||||
hookResult = { ...completion, textBuffer };
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
renderWithProviders(<TestComponent />);
|
||||
|
||||
// This test verifies that comments are filtered out while regular text is not
|
||||
expect(hookResult!.textBuffer.text).toBe(
|
||||
|
||||
@@ -8,28 +8,56 @@ import { act, useCallback } from 'react';
|
||||
import { vi } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useConsoleMessages } from './useConsoleMessages.js';
|
||||
import { CoreEvent, type ConsoleLogPayload } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock coreEvents
|
||||
let consoleLogHandler: ((payload: ConsoleLogPayload) => void) | undefined;
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
coreEvents: {
|
||||
on: vi.fn((event, handler) => {
|
||||
if (event === CoreEvent.ConsoleLog) {
|
||||
consoleLogHandler = handler;
|
||||
}
|
||||
}),
|
||||
off: vi.fn((event) => {
|
||||
if (event === CoreEvent.ConsoleLog) {
|
||||
consoleLogHandler = undefined;
|
||||
}
|
||||
}),
|
||||
emitConsoleLog: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('useConsoleMessages', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
consoleLogHandler = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const useTestableConsoleMessages = () => {
|
||||
const { handleNewMessage, ...rest } = useConsoleMessages();
|
||||
const log = useCallback(
|
||||
(content: string) => handleNewMessage({ type: 'log', content, count: 1 }),
|
||||
[handleNewMessage],
|
||||
);
|
||||
const error = useCallback(
|
||||
(content: string) =>
|
||||
handleNewMessage({ type: 'error', content, count: 1 }),
|
||||
[handleNewMessage],
|
||||
);
|
||||
const { ...rest } = useConsoleMessages();
|
||||
const log = useCallback((content: string) => {
|
||||
if (consoleLogHandler) {
|
||||
consoleLogHandler({ type: 'log', content });
|
||||
}
|
||||
}, []);
|
||||
const error = useCallback((content: string) => {
|
||||
if (consoleLogHandler) {
|
||||
consoleLogHandler({ type: 'error', content });
|
||||
}
|
||||
}, []);
|
||||
return {
|
||||
...rest,
|
||||
log,
|
||||
@@ -145,7 +173,7 @@ describe('useConsoleMessages', () => {
|
||||
});
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
clearTimeoutSpy.mockRestore();
|
||||
// clearTimeoutSpy.mockRestore() is handled by afterEach restoreAllMocks
|
||||
});
|
||||
|
||||
it('should clean up the timeout on unmount', () => {
|
||||
@@ -159,6 +187,5 @@ describe('useConsoleMessages', () => {
|
||||
unmount();
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
clearTimeoutSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,10 +12,14 @@ import {
|
||||
useTransition,
|
||||
} from 'react';
|
||||
import type { ConsoleMessageItem } from '../types.js';
|
||||
import {
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
type ConsoleLogPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export interface UseConsoleMessagesReturn {
|
||||
consoleMessages: ConsoleMessageItem[];
|
||||
handleNewMessage: (message: ConsoleMessageItem) => void;
|
||||
clearConsoleMessages: () => void;
|
||||
}
|
||||
|
||||
@@ -85,6 +89,37 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
|
||||
[processQueue],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleConsoleLog = (payload: ConsoleLogPayload) => {
|
||||
handleNewMessage({
|
||||
type: payload.type,
|
||||
content: payload.content,
|
||||
count: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOutput = (payload: {
|
||||
isStderr: boolean;
|
||||
chunk: Uint8Array | string;
|
||||
}) => {
|
||||
const content =
|
||||
typeof payload.chunk === 'string'
|
||||
? payload.chunk
|
||||
: new TextDecoder().decode(payload.chunk);
|
||||
// It would be nice if we could show stderr as 'warn' but unfortunately
|
||||
// we log non warning info to stderr before the app starts so that would
|
||||
// be misleading.
|
||||
handleNewMessage({ type: 'log', content, count: 1 });
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
coreEvents.on(CoreEvent.Output, handleOutput);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
coreEvents.off(CoreEvent.Output, handleOutput);
|
||||
};
|
||||
}, [handleNewMessage]);
|
||||
|
||||
const clearConsoleMessages = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
@@ -106,5 +141,5 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
|
||||
[],
|
||||
);
|
||||
|
||||
return { consoleMessages, handleNewMessage, clearConsoleMessages };
|
||||
return { consoleMessages, clearConsoleMessages };
|
||||
}
|
||||
|
||||
@@ -347,7 +347,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
);
|
||||
@@ -419,7 +418,6 @@ describe('useGeminiStream', () => {
|
||||
setShellInputFocused?: (focused: boolean) => void;
|
||||
performMemoryRefresh?: () => Promise<void>;
|
||||
onAuthError?: () => void;
|
||||
onEditorClose?: () => void;
|
||||
setModelSwitched?: Mock;
|
||||
modelSwitched?: boolean;
|
||||
} = {},
|
||||
@@ -430,7 +428,6 @@ describe('useGeminiStream', () => {
|
||||
setShellInputFocused = () => {},
|
||||
performMemoryRefresh = () => Promise.resolve(),
|
||||
onAuthError = () => {},
|
||||
onEditorClose = () => {},
|
||||
setModelSwitched = vi.fn(),
|
||||
modelSwitched = false,
|
||||
} = options;
|
||||
@@ -450,7 +447,6 @@ describe('useGeminiStream', () => {
|
||||
performMemoryRefresh,
|
||||
modelSwitched,
|
||||
setModelSwitched,
|
||||
onEditorClose,
|
||||
onCancelSubmit,
|
||||
setShellInputFocused,
|
||||
80,
|
||||
@@ -594,7 +590,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -677,7 +672,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -789,7 +783,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -903,7 +896,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -1035,7 +1027,6 @@ describe('useGeminiStream', () => {
|
||||
() => Promise.resolve(),
|
||||
false,
|
||||
() => {},
|
||||
() => {},
|
||||
cancelSubmitSpy,
|
||||
() => {},
|
||||
80,
|
||||
@@ -1076,7 +1067,6 @@ describe('useGeminiStream', () => {
|
||||
() => Promise.resolve(),
|
||||
false,
|
||||
() => {},
|
||||
() => {},
|
||||
vi.fn(),
|
||||
setShellInputFocusedSpy, // Pass the spy here
|
||||
80,
|
||||
@@ -1413,7 +1403,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -1487,7 +1476,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -1544,7 +1532,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -1846,7 +1833,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -1953,7 +1939,6 @@ describe('useGeminiStream', () => {
|
||||
() => Promise.resolve(),
|
||||
false,
|
||||
() => {},
|
||||
() => {},
|
||||
onCancelSubmitSpy,
|
||||
() => {},
|
||||
80,
|
||||
@@ -2098,7 +2083,6 @@ describe('useGeminiStream', () => {
|
||||
vi.fn(), // performMemoryRefresh
|
||||
false, // modelSwitched
|
||||
vi.fn(), // setModelSwitched
|
||||
vi.fn(), // onEditorClose
|
||||
vi.fn(), // onCancelSubmit
|
||||
vi.fn(), // setShellInputFocused
|
||||
80, // terminalWidth
|
||||
@@ -2171,7 +2155,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -2252,7 +2235,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -2322,7 +2304,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
@@ -2380,7 +2361,6 @@ describe('useGeminiStream', () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
|
||||
@@ -105,7 +105,6 @@ export const useGeminiStream = (
|
||||
performMemoryRefresh: () => Promise<void>,
|
||||
modelSwitchedFromQuotaError: boolean,
|
||||
setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
onEditorClose: () => void,
|
||||
onCancelSubmit: (shouldRestorePrompt?: boolean) => void,
|
||||
setShellInputFocused: (value: boolean) => void,
|
||||
terminalWidth: number,
|
||||
@@ -178,7 +177,6 @@ export const useGeminiStream = (
|
||||
},
|
||||
config,
|
||||
getPreferredEditor,
|
||||
onEditorClose,
|
||||
);
|
||||
|
||||
const pendingToolCallGroupDisplay = useMemo(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useInputHistoryStore } from './useInputHistoryStore.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
describe('useInputHistoryStore', () => {
|
||||
beforeEach(() => {
|
||||
@@ -108,7 +109,9 @@ describe('useInputHistoryStore', () => {
|
||||
.mockRejectedValue(new Error('Logger error')),
|
||||
};
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(debugLogger, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useInputHistoryStore());
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ describe('useReactToolScheduler', () => {
|
||||
it('only creates one instance of CoreToolScheduler even if props change', () => {
|
||||
const onComplete = vi.fn();
|
||||
const getPreferredEditor = vi.fn();
|
||||
const onEditorClose = vi.fn();
|
||||
const config = {} as Config;
|
||||
|
||||
const { rerender } = renderHook(
|
||||
@@ -38,14 +37,12 @@ describe('useReactToolScheduler', () => {
|
||||
props.onComplete,
|
||||
props.config,
|
||||
props.getPreferredEditor,
|
||||
props.onEditorClose,
|
||||
),
|
||||
{
|
||||
initialProps: {
|
||||
onComplete,
|
||||
config,
|
||||
getPreferredEditor,
|
||||
onEditorClose,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -58,7 +55,6 @@ describe('useReactToolScheduler', () => {
|
||||
onComplete: newOnComplete,
|
||||
config,
|
||||
getPreferredEditor,
|
||||
onEditorClose,
|
||||
});
|
||||
expect(mockCoreToolScheduler).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -68,17 +64,13 @@ describe('useReactToolScheduler', () => {
|
||||
onComplete: newOnComplete,
|
||||
config,
|
||||
getPreferredEditor: newGetPreferredEditor,
|
||||
onEditorClose,
|
||||
});
|
||||
expect(mockCoreToolScheduler).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Rerender with a new onEditorClose function
|
||||
const newOnEditorClose = vi.fn();
|
||||
rerender({
|
||||
onComplete: newOnComplete,
|
||||
config,
|
||||
getPreferredEditor: newGetPreferredEditor,
|
||||
onEditorClose: newOnEditorClose,
|
||||
});
|
||||
expect(mockCoreToolScheduler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -68,7 +68,6 @@ export function useReactToolScheduler(
|
||||
onComplete: (tools: CompletedToolCall[]) => Promise<void>,
|
||||
config: Config,
|
||||
getPreferredEditor: () => EditorType | undefined,
|
||||
onEditorClose: () => void,
|
||||
): [
|
||||
TrackedToolCall[],
|
||||
ScheduleFn,
|
||||
@@ -83,7 +82,6 @@ export function useReactToolScheduler(
|
||||
// Store callbacks in refs to keep them up-to-date without causing re-renders.
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
const getPreferredEditorRef = useRef(getPreferredEditor);
|
||||
const onEditorCloseRef = useRef(onEditorClose);
|
||||
|
||||
useEffect(() => {
|
||||
onCompleteRef.current = onComplete;
|
||||
@@ -93,10 +91,6 @@ export function useReactToolScheduler(
|
||||
getPreferredEditorRef.current = getPreferredEditor;
|
||||
}, [getPreferredEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
onEditorCloseRef.current = onEditorClose;
|
||||
}, [onEditorClose]);
|
||||
|
||||
const outputUpdateHandler: OutputUpdateHandler = useCallback(
|
||||
(toolCallId, outputChunk) => {
|
||||
setToolCallsForDisplay((prevCalls) =>
|
||||
@@ -158,7 +152,6 @@ export function useReactToolScheduler(
|
||||
() => getPreferredEditorRef.current(),
|
||||
[],
|
||||
);
|
||||
const stableOnEditorClose = useCallback(() => onEditorCloseRef.current(), []);
|
||||
|
||||
const scheduler = useMemo(
|
||||
() =>
|
||||
@@ -168,7 +161,6 @@ export function useReactToolScheduler(
|
||||
onToolCallsUpdate: toolCallsUpdateHandler,
|
||||
getPreferredEditor: stableGetPreferredEditor,
|
||||
config,
|
||||
onEditorClose: stableOnEditorClose,
|
||||
}),
|
||||
[
|
||||
config,
|
||||
@@ -176,7 +168,6 @@ export function useReactToolScheduler(
|
||||
allToolCallsCompleteHandler,
|
||||
toolCallsUpdateHandler,
|
||||
stableGetPreferredEditor,
|
||||
stableOnEditorClose,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -4,13 +4,21 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { renderHookWithProviders } from '../../test-utils/render.js';
|
||||
import { useReverseSearchCompletion } from './useReverseSearchCompletion.js';
|
||||
import { useTextBuffer } from '../components/shared/text-buffer.js';
|
||||
|
||||
describe('useReverseSearchCompletion', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function useTextBufferForTest(text: string) {
|
||||
return useTextBuffer({
|
||||
initialText: text,
|
||||
@@ -26,7 +34,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
it('should initialize with default state', () => {
|
||||
const mockShellHistory = ['echo hello'];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(
|
||||
useTextBufferForTest(''),
|
||||
mockShellHistory,
|
||||
@@ -43,7 +51,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
|
||||
it('should reset state when reverseSearchActive becomes false', () => {
|
||||
const mockShellHistory = ['echo hello'];
|
||||
const { result, rerender } = renderHook(
|
||||
const { result, rerender } = renderHookWithProviders(
|
||||
({ text, active }) => {
|
||||
const textBuffer = useTextBufferForTest(text);
|
||||
return useReverseSearchCompletion(
|
||||
@@ -68,7 +76,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
it('should handle navigateUp with no suggestions', () => {
|
||||
const mockShellHistory = ['echo hello'];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(
|
||||
useTextBufferForTest('grep'),
|
||||
mockShellHistory,
|
||||
@@ -85,7 +93,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
|
||||
it('should handle navigateDown with no suggestions', () => {
|
||||
const mockShellHistory = ['echo hello'];
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(
|
||||
useTextBufferForTest('grep'),
|
||||
mockShellHistory,
|
||||
@@ -110,7 +118,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
'echo Hi',
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(
|
||||
useTextBufferForTest('echo'),
|
||||
mockShellHistory,
|
||||
@@ -137,7 +145,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
'echo "Hello, World!"',
|
||||
'echo Hi',
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(
|
||||
useTextBufferForTest('ls'),
|
||||
mockShellHistory,
|
||||
@@ -165,7 +173,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
'echo "Hi all"',
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(
|
||||
useTextBufferForTest('l'),
|
||||
mockShellHistory,
|
||||
@@ -208,7 +216,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
(_, i) => `echo ${i}`,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(
|
||||
useTextBufferForTest('echo'),
|
||||
largeMockCommands,
|
||||
@@ -234,7 +242,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
describe('Filtering', () => {
|
||||
it('filters history by buffer.text and sets showSuggestions', () => {
|
||||
const history = ['foo', 'barfoo', 'baz'];
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(useTextBufferForTest('foo'), history, true),
|
||||
);
|
||||
|
||||
@@ -248,7 +256,7 @@ describe('useReverseSearchCompletion', () => {
|
||||
|
||||
it('hides suggestions when there are no matches', () => {
|
||||
const history = ['alpha', 'beta'];
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useReverseSearchCompletion(useTextBufferForTest('γ'), history, true),
|
||||
);
|
||||
|
||||
|
||||
@@ -135,7 +135,6 @@ describe('useReactToolScheduler in YOLO Mode', () => {
|
||||
onComplete,
|
||||
mockConfig as unknown as Config,
|
||||
() => undefined,
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -264,7 +263,6 @@ describe('useReactToolScheduler', () => {
|
||||
onComplete,
|
||||
mockConfig as unknown as Config,
|
||||
() => undefined,
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { CustomTheme } from './theme.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import type * as osActual from 'node:os';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('node:fs');
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
@@ -164,7 +165,7 @@ describe('ThemeManager', () => {
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(mockTheme));
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.spyOn(debugLogger, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const result = themeManager.setActiveTheme('/untrusted/my-theme.json');
|
||||
|
||||
@@ -27,11 +27,11 @@ export class ConsolePatcher {
|
||||
}
|
||||
|
||||
patch() {
|
||||
console.log = this.patchConsoleMethod('log', this.originalConsoleLog);
|
||||
console.warn = this.patchConsoleMethod('warn', this.originalConsoleWarn);
|
||||
console.error = this.patchConsoleMethod('error', this.originalConsoleError);
|
||||
console.debug = this.patchConsoleMethod('debug', this.originalConsoleDebug);
|
||||
console.info = this.patchConsoleMethod('info', this.originalConsoleInfo);
|
||||
console.log = this.patchConsoleMethod('log');
|
||||
console.warn = this.patchConsoleMethod('warn');
|
||||
console.error = this.patchConsoleMethod('error');
|
||||
console.debug = this.patchConsoleMethod('debug');
|
||||
console.info = this.patchConsoleMethod('info');
|
||||
}
|
||||
|
||||
cleanup = () => {
|
||||
@@ -45,20 +45,13 @@ export class ConsolePatcher {
|
||||
private formatArgs = (args: unknown[]): string => util.format(...args);
|
||||
|
||||
private patchConsoleMethod =
|
||||
(
|
||||
type: 'log' | 'warn' | 'error' | 'debug' | 'info',
|
||||
originalMethod: (...args: unknown[]) => void,
|
||||
) =>
|
||||
(type: 'log' | 'warn' | 'error' | 'debug' | 'info') =>
|
||||
(...args: unknown[]) => {
|
||||
if (this.params.stderr) {
|
||||
if (type !== 'debug' || this.params.debugMode) {
|
||||
this.originalConsoleError(this.formatArgs(args));
|
||||
}
|
||||
} else {
|
||||
if (this.params.debugMode) {
|
||||
originalMethod.apply(console, args);
|
||||
}
|
||||
|
||||
if (type !== 'debug' || this.params.debugMode) {
|
||||
this.params.onNewMessage?.({
|
||||
type,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import process from 'node:process';
|
||||
import { writeToStdout } from '../../utils/stdio.js';
|
||||
import {
|
||||
SGR_MOUSE_REGEX,
|
||||
X11_MOUSE_REGEX,
|
||||
@@ -234,10 +234,10 @@ export function enableMouseEvents() {
|
||||
// Enable mouse tracking with SGR format
|
||||
// ?1002h = button event tracking (clicks + drags + scroll wheel)
|
||||
// ?1006h = SGR extended mouse mode (better coordinate handling)
|
||||
process.stdout.write('\u001b[?1002h\u001b[?1006h');
|
||||
writeToStdout('\u001b[?1002h\u001b[?1006h');
|
||||
}
|
||||
|
||||
export function disableMouseEvents() {
|
||||
// Disable mouse tracking with SGR format
|
||||
process.stdout.write('\u001b[?1006l\u001b[?1002l');
|
||||
writeToStdout('\u001b[?1006l\u001b[?1002l');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user