fix(cli): resolve devtools build issues and update third-party notices

This commit is contained in:
Alisa Novikova
2026-02-19 16:23:16 -08:00
parent f1c0a695f8
commit 4a599db2bf
45 changed files with 5353 additions and 2960 deletions
+2
View File
@@ -31,6 +31,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/gemini-cli-devtools": "file:../devtools",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.23.0",
@@ -81,6 +82,7 @@
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32",
"@xterm/headless": "^5.5.0",
"ink-testing-library": "^4.0.0",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
@@ -292,9 +292,8 @@ System using model: \${MODEL_NAME}
});
expect(extension.skills![0].body).toContain('Value is: first');
const { updateSetting, ExtensionSettingScope } = await import(
'./extensions/extensionSettings.js'
);
const { updateSetting, ExtensionSettingScope } =
await import('./extensions/extensionSettings.js');
const extensionConfig =
await extensionManager.loadExtensionConfig(extensionPath);
+2 -3
View File
@@ -746,9 +746,8 @@ describe('gemini.tsx main function kitty protocol', () => {
});
it.skip('should log error when cleanupExpiredSessions fails', async () => {
const { cleanupExpiredSessions } = await import(
'./utils/sessionCleanup.js'
);
const { cleanupExpiredSessions } =
await import('./utils/sessionCleanup.js');
vi.mocked(cleanupExpiredSessions).mockRejectedValue(
new Error('Cleanup failed'),
);
+7 -3
View File
@@ -568,10 +568,14 @@ export async function main() {
adminControlsListner.setConfig(config);
if (config.isInteractive() && settings.merged.general.devtools) {
const { setupInitialActivityLogger } = await import(
'./utils/devtoolsService.js'
);
const { setupInitialActivityLogger, stopDevTools } =
await import('./utils/devtoolsService.js');
const { ActivityLogger } = await import('./utils/activityLogger.js');
await setupInitialActivityLogger(config);
registerCleanup(async () => {
await stopDevTools();
ActivityLogger.getInstance().dispose();
});
}
// Register config for telemetry shutdown
+2 -3
View File
@@ -180,9 +180,8 @@ describe('gemini.tsx main function cleanup', () => {
});
it('should log error when cleanupExpiredSessions fails', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadCliConfig, parseArguments } =
await import('./config/config.js');
const { loadSettings } = await import('./config/settings.js');
cleanupMockState.shouldThrow = true;
cleanupMockState.called = false;
+10 -15
View File
@@ -212,9 +212,8 @@ describe('runNonInteractive', () => {
computeMergedSettings: vi.fn(),
} as unknown as LoadedSettings;
const { handleAtCommand } = await import(
'./ui/hooks/atCommandProcessor.js'
);
const { handleAtCommand } =
await import('./ui/hooks/atCommandProcessor.js');
vi.mocked(handleAtCommand).mockImplementation(async ({ query }) => ({
processedQuery: [{ text: query }],
}));
@@ -636,9 +635,8 @@ describe('runNonInteractive', () => {
it('should preprocess @include commands before sending to the model', async () => {
// 1. Mock the imported atCommandProcessor
const { handleAtCommand } = await import(
'./ui/hooks/atCommandProcessor.js'
);
const { handleAtCommand } =
await import('./ui/hooks/atCommandProcessor.js');
const mockHandleAtCommand = vi.mocked(handleAtCommand);
// 2. Define the raw input and the expected processed output
@@ -995,9 +993,8 @@ describe('runNonInteractive', () => {
});
it('should handle slash commands', async () => {
const nonInteractiveCliCommands = await import(
'./nonInteractiveCliCommands.js'
);
const nonInteractiveCliCommands =
await import('./nonInteractiveCliCommands.js');
const handleSlashCommandSpy = vi.spyOn(
nonInteractiveCliCommands,
'handleSlashCommand',
@@ -1276,13 +1273,11 @@ describe('runNonInteractive', () => {
it('should instantiate CommandService with correct loaders for slash commands', async () => {
// This test indirectly checks that handleSlashCommand is using the right loaders.
const { FileCommandLoader } = await import(
'./services/FileCommandLoader.js'
);
const { FileCommandLoader } =
await import('./services/FileCommandLoader.js');
const { McpPromptLoader } = await import('./services/McpPromptLoader.js');
const { BuiltinCommandLoader } = await import(
'./services/BuiltinCommandLoader.js'
);
const { BuiltinCommandLoader } =
await import('./services/BuiltinCommandLoader.js');
mockGetCommands.mockReturnValue([]); // No commands found, so it will fall through
const events: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Acknowledged' },
+2 -3
View File
@@ -72,9 +72,8 @@ export async function runNonInteractive({
});
if (process.env['GEMINI_CLI_ACTIVITY_LOG_TARGET']) {
const { setupInitialActivityLogger } = await import(
'./utils/devtoolsService.js'
);
const { setupInitialActivityLogger } =
await import('./utils/devtoolsService.js');
await setupInitialActivityLogger(config);
}
+2 -3
View File
@@ -42,9 +42,8 @@ import { AuthState } from '../ui/types.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();
const { MockShellExecutionService: MockService } = await import(
'./MockShellExecutionService.js'
);
const { MockShellExecutionService: MockService } =
await import('./MockShellExecutionService.js');
// Register the real execution logic so MockShellExecutionService can fall back to it
MockService.setOriginalImplementation(original.ShellExecutionService.execute);
+2 -1
View File
@@ -357,7 +357,8 @@ export const render = (
debug: false,
exitOnCtrlC: false,
patchConsole: false,
onRender: (metrics: { output: string; staticOutput?: string }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onRender: (metrics: any) => {
stdout.onRender(metrics.staticOutput ?? '', metrics.output);
},
});
+4 -6
View File
@@ -3292,9 +3292,8 @@ describe('AppContainer State Management', () => {
describe('Permission Handling', () => {
it('shows permission dialog when checkPermissions returns paths', async () => {
const { checkPermissions } = await import(
'./hooks/atCommandProcessor.js'
);
const { checkPermissions } =
await import('./hooks/atCommandProcessor.js');
vi.mocked(checkPermissions).mockResolvedValue(['/test/file.txt']);
let unmount: () => void;
@@ -3316,9 +3315,8 @@ describe('AppContainer State Management', () => {
it.each([true, false])(
'handles permissions when allowed is %s',
async (allowed) => {
const { checkPermissions } = await import(
'./hooks/atCommandProcessor.js'
);
const { checkPermissions } =
await import('./hooks/atCommandProcessor.js');
vi.mocked(checkPermissions).mockResolvedValue(['/test/file.txt']);
const addReadOnlyPathSpy = vi.spyOn(
mockConfig.getWorkspaceContext(),
+8 -11
View File
@@ -1392,9 +1392,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
}, []);
const shouldShowIdePrompt = Boolean(
currentIDE &&
!config.getIdeMode() &&
!settings.merged.ide.hasSeenNudge &&
!idePromptAnswered,
!config.getIdeMode() &&
!settings.merged.ide.hasSeenNudge &&
!idePromptAnswered,
);
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
@@ -1656,9 +1656,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
if (settings.merged.general.devtools) {
void (async () => {
const { toggleDevToolsPanel } = await import(
'../utils/devtoolsService.js'
);
const { toggleDevToolsPanel } =
await import('../utils/devtoolsService.js');
await toggleDevToolsPanel(
config,
showErrorDetails,
@@ -2073,11 +2072,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
let isMounted = true;
const fetchBannerTexts = async () => {
const [defaultBanner, warningBanner] = await Promise.all([
// TODO: temporarily disabling the banner, it will be re-added.
'',
config.getBannerTextCapacityIssues(),
]);
// TODO: temporarily disabling the banner, it will be re-added.
const defaultBanner = '';
const warningBanner = await config.getBannerTextCapacityIssues();
if (isMounted) {
setDefaultBannerText(defaultBanner);
@@ -73,9 +73,8 @@ describe('authCommand', () => {
const logoutCommand = authCommand.subCommands?.[1];
expect(logoutCommand?.name).toBe('logout');
const { clearCachedCredentialFile } = await import(
'@google/gemini-cli-core'
);
const { clearCachedCredentialFile } =
await import('@google/gemini-cli-core');
await logoutCommand!.action!(mockContext, '');
@@ -1048,9 +1048,8 @@ describe('extensionsCommand', () => {
const prompts = (await import('prompts')).default;
vi.mocked(prompts).mockResolvedValue({ overwrite: true });
const { getScopedEnvContents } = await import(
'../../config/extensions/extensionSettings.js'
);
const { getScopedEnvContents } =
await import('../../config/extensions/extensionSettings.js');
vi.mocked(getScopedEnvContents).mockResolvedValue({});
});
+4 -4
View File
@@ -142,15 +142,15 @@ const inScreen = (): boolean =>
const isSSH = (): boolean =>
Boolean(
process.env['SSH_TTY'] ||
process.env['SSH_CONNECTION'] ||
process.env['SSH_CLIENT'],
process.env['SSH_CONNECTION'] ||
process.env['SSH_CLIENT'],
);
const isWSL = (): boolean =>
Boolean(
process.env['WSL_DISTRO_NAME'] ||
process.env['WSLENV'] ||
process.env['WSL_INTEROP'],
process.env['WSLENV'] ||
process.env['WSL_INTEROP'],
);
const isWindowsTerminal = (): boolean =>
+10 -15
View File
@@ -220,9 +220,8 @@ describe('rewindFileOps', () => {
});
it('reverts exact match', async () => {
const { getFileDiffFromResultDisplay } = await import(
'@google/gemini-cli-core'
);
const { getFileDiffFromResultDisplay } =
await import('@google/gemini-cli-core');
vi.mocked(getFileDiffFromResultDisplay).mockReturnValue({
filePath: '/abs/path/test.ts',
fileName: 'test.ts',
@@ -270,9 +269,8 @@ describe('rewindFileOps', () => {
});
it('deletes new file on revert', async () => {
const { getFileDiffFromResultDisplay } = await import(
'@google/gemini-cli-core'
);
const { getFileDiffFromResultDisplay } =
await import('@google/gemini-cli-core');
vi.mocked(getFileDiffFromResultDisplay).mockReturnValue({
filePath: '/abs/path/new.ts',
fileName: 'new.ts',
@@ -317,9 +315,8 @@ describe('rewindFileOps', () => {
});
it('handles smart revert (patching) successfully', async () => {
const { getFileDiffFromResultDisplay } = await import(
'@google/gemini-cli-core'
);
const { getFileDiffFromResultDisplay } =
await import('@google/gemini-cli-core');
vi.mocked(getFileDiffFromResultDisplay).mockReturnValue({
filePath: '/abs/path/test.ts',
fileName: 'test.ts',
@@ -369,9 +366,8 @@ describe('rewindFileOps', () => {
});
it('emits warning on smart revert failure', async () => {
const { getFileDiffFromResultDisplay } = await import(
'@google/gemini-cli-core'
);
const { getFileDiffFromResultDisplay } =
await import('@google/gemini-cli-core');
vi.mocked(getFileDiffFromResultDisplay).mockReturnValue({
filePath: '/abs/path/test.ts',
fileName: 'test.ts',
@@ -421,9 +417,8 @@ describe('rewindFileOps', () => {
});
it('emits error if fs.readFile fails with a generic error', async () => {
const { getFileDiffFromResultDisplay } = await import(
'@google/gemini-cli-core'
);
const { getFileDiffFromResultDisplay } =
await import('@google/gemini-cli-core');
vi.mocked(getFileDiffFromResultDisplay).mockReturnValue({
filePath: '/abs/path/test.ts',
fileName: 'test.ts',
+3 -2
View File
@@ -47,13 +47,14 @@ describe('textUtils', () => {
describe('getCachedStringWidth', () => {
it('should handle unicode characters that crash string-width', () => {
// U+0602 caused string-width to crash (see #16418)
// Newer versions of string-width correctly identify it as width 0.
const char = '؂';
expect(getCachedStringWidth(char)).toBe(1);
expect(getCachedStringWidth(char)).toBe(0);
});
it('should handle unicode characters that crash string-width with ANSI codes', () => {
const charWithAnsi = '\u001b[31m' + '؂' + '\u001b[0m';
expect(getCachedStringWidth(charWithAnsi)).toBe(1);
expect(getCachedStringWidth(charWithAnsi)).toBe(0);
});
});
+13
View File
@@ -561,6 +561,13 @@ export class ActivityLogger extends EventEmitter {
}
this.emit('console', enriched);
}
dispose() {
this.networkLoggingEnabled = false;
this.removeAllListeners();
this.clearBufferedLogs();
this.emit('dispose');
}
}
/**
@@ -820,6 +827,12 @@ function setupNetworkLogging(
flushBuffer();
});
capture.on('dispose', () => {
if (reconnectTimer) clearTimeout(reconnectTimer);
if (ws) ws.close();
cleanup();
});
// Cleanup on process exit
process.on('exit', () => {
if (reconnectTimer) clearTimeout(reconnectTimer);
+6 -6
View File
@@ -100,14 +100,14 @@ async function drainStdin() {
await new Promise((resolve) => setTimeout(resolve, 50));
}
export async function cleanupCheckpoints() {
const storage = new Storage(process.cwd());
await storage.initialize();
const tempDir = storage.getProjectTempDir();
const checkpointsDir = join(tempDir, 'checkpoints');
export async function cleanupCheckpoints(cwd?: string) {
try {
const storage = new Storage(cwd || process.cwd());
await storage.initialize();
const tempDir = storage.getProjectTempDir();
const checkpointsDir = join(tempDir, 'checkpoints');
await fs.rm(checkpointsDir, { recursive: true, force: true });
} catch {
// Ignore errors if the directory doesn't exist or fails to delete.
// Ignore errors if the directory doesn't exist, process.cwd() fails, or it fails to delete.
}
}
+17 -3
View File
@@ -230,9 +230,8 @@ export async function toggleDevToolsPanel(
}
try {
const { openBrowserSecurely, shouldLaunchBrowser } = await import(
'@google/gemini-cli-core'
);
const { openBrowserSecurely, shouldLaunchBrowser } =
await import('@google/gemini-cli-core');
const url = await startDevToolsServer(config);
if (shouldLaunchBrowser()) {
try {
@@ -257,3 +256,18 @@ export function resetForTesting() {
serverStartPromise = null;
connectedUrl = null;
}
/**
* Stops the DevTools server if it was started by this process.
*/
export async function stopDevTools() {
try {
const mod = await import('@google/gemini-cli-devtools');
const devtools: IDevTools = mod.DevTools.getInstance();
await devtools.stop();
serverStartPromise = null;
connectedUrl = null;
} catch (err) {
debugLogger.debug('Failed to stop DevTools:', err);
}
}
+1 -1
View File
@@ -14,5 +14,5 @@
"./package.json"
],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "../core" }]
"references": [{ "path": "../core" }, { "path": "../devtools" }]
}